"); //-->
首先再次声明我是菜鸟,我写出来的目的是给没有接触过lcd的朋友看得,我本人水平有限,错误在所难免,欢迎指出 *博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。
之所以产生写这个东西的想法,是因为趁着寒假这段时间看了看学习板的源程序,在看lcd的时候很不顺利,花了五个晚上才看完(白天偷懒了^_^!,毕竟在春节嘛)。联想起还有很多水平和我差不多甚至可能还低一点的菜鸟,就觉得有义务把五个晚上的学习心得写出来,顺便也给自己整理整理思路,嘿嘿。
这个心得是针对学习板上的lcd 驱动芯片SSD181X系列写的,当然思想应该是相通的。其实是我没有用过其他系列lcd(众人呕吐ing……)。
首先你要去网上下载一个SSD181X系列的PDF文件,这种文件网上到处都是,偶就不多说了。刚开始需要借用51论坛电子白菜大虾的文章作为总领,因为偶比较懒,不想写这么多拉~~~(不要飞鸡蛋……)当然还是有部分内容增加的拉。
我介绍的这个是MOTO的手机屏,大小为96*54(96列54行)。不过这个小小的LCD屏也是很不错的。对比度可调,而且还有背光:)具体驱动器是:SSD1815,是黑白点阵驱动芯片,最多支持132个SEG和64个COM,还有一个ICON(功能设置)行。说起SEG,COM,也许很多人没接触过,其实玻璃屏的LCD都是使用SEG,COM的扫描来驱动的,为使需要点亮的内容呈显示状态,须将交流驱动电压加在LCD的段电极与公共电极之间。原理涉及物理化学问题,这里就不多说,只打个比喻:一个‘井’字,是两行两列组成的,共有四个交点,其实就好比是2个SEG,2个COM,要驱动一个固定的点,要相应的一条COM和一条SEG都有效如果要几个点亮,几个点灭,就需要用到扫描的原理,还记得行列键盘的原理吗?其实它的驱动原理就差不多了:)
上面说的它支持132个SEG,64个COM,所以它支持最多132*64个点的LCD。但实际上如果我们的LCD没有那么多个点话,就需要在上电的时候对1815进行初始化,指定COM和SEG的数目,还有BIAS数,说到BIAS,唉,又是个专业问题了,LCD的驱动波形由几级电平组成,为防止对比度不均匀,在不点亮象素对应的电极上仍加有一定电压,这对降低点亮象素产生 的交叉干扰和防止对比度不均匀很重要。LCD中非点亮象素(非选点)的电压有效值与点亮象素(选择点)电压有效值之比(1/n)称为偏压比。确实比较难以理解,你可以这样看:BIAS是电平强度,和COM有关的东西。以上这些都要在LCD上电的时候初始化好,才能正确地显示图文。
1815还自带显示RAM,英文是Graphic Display Data Ram就是图象显示数据存储器,简称GDRAM。对于现在很多现成的LCM屏来说,控制芯片都带有了GDRAM的,大小就和他的最大显示点数相当。
我估计你看到现在还是一头雾水,呵呵,没关系,当初我看的时候也是一头雾水,看到后面你就会明白他们的原理了。
接着是1815的接口问题了,它支持8080,6800,I2C总线。8080,就是和我们51一样的总线,有CS,WR,RD,然后是数据地址线;6800又叫摩托罗拉总线,有R/W,E,然后是地址数据线;I2C总线,呵呵,当然就是I2C总线咯,还用说么?
由于这个是摩托罗拉的LCD屏,出厂的时候已经配置为6800总线了,所以我们不能用正常的51总线来驱动,但实际上6800是可以兼容8080总线的,先等我介绍下这个总线吧:
R/W脚:读写脚,为1的时候是读,0的时候是写。
E脚:使能脚,功能如同51总线的CS,也是低有效的,但数据D0-D7在高的时候锁存。
D/C脚,这个是数据,命令选择脚,1的时候代表总线传输的是数据(不论是读还是写),0的时候代表总线传输命令(不论是读还是写)。
D0-D7:理所当然的数据脚咯。
驱动原理很简单,在E为低的时候对R/W脚输入读写信号,读就是1,写就是0;然后选择你读写的是数据还是命令,D/C脚1为数据,0为命令;最后就是数据脚的数据了。
以上原理,用单片机IO做是很简单的事情,不过使用IO的缺点就是不能复用,浪费了很多的资源,所以在学习板上是使用总线驱动LCD的。之前说了,某些6800总线是可以用8080总线模拟的,而这个LCD就是可以使用8080总线的了。
6800总线读写控制只需要1只脚R/W,1为读0为写,而8080总线是两只脚RD,WR,其中的某个脚为低就对应响应功能。根据这个逻辑关系,我们很容易就猜想到为什么6800的R/W脚可以直接接上8080的WR了。
这些都是垫场的开场白拉。接下来我就以丁丁编写的1815的使用程序,来介绍使用1815驱动lcd的详细步骤。
为了让大家能够理解清楚,我绝对按照分段的形式,一段一段解释。当然由于本菜鸟水平有限,如果解释错误还请大家不吝赐教。开始的程序我会讲的很细,后面的我就讲个大概了,只要理解了这个过程就不难了。
注意哦,这个程序是来自"51单片机世界",作者版主丁丁(聂小猛),未经允许,不得抄袭作为商业用途。
/*****************************************
#define uchar unsigned char
#define uint unsigned int
#define ulong unsigned long
#i nclude
#i nclude
#i nclude "study.h"
#define LCMD XBYTE[0xAf00] // 液晶数据口
#define LCMC XBYTE[0xAb00] // 液晶命令口
#define LCD_DOT_X 98 // lcd 的宽点数
#define LCD_DOT_Y 54 //lcd 的高点数
#define LCD_MAX_X 11 //每行字符, ( LCD_MAX_X +1 ) * 8 点
#define LCD_MAX_Y 5 //共多少行,( LCD_MAX_Y +1 ) * 8 点
********************************************/
如果你写过单片机程序,就会知道任何程序的初始化阶段都是这么些步骤,定义再定义,最后是某些功能的初始化函数。这些就是刚开始的声明定义过程。前面的几个define,include我就不多说了,如果你连这个都看不懂,只能先去补习补习c语言了。
#define LCMD XBYTE[0xAf00]和#define LCMC XBYTE[0xAb00]两句是用来定义外部总线接口地址的。0xAf00用来传递数据,0xAb00用来传递命令,之前说过,是传输命令还是数据主要是取决于A10的,上面两个地址对应的A10其实就是0xaf00->A10=1,0xab00->A10=0。照电子白菜大虾的话说,地址的取值只要确保在A000~AFFF 之间就可以了,所以以下定义一样能正确驱动LCD:
#define LCMD XBYTE[0xA400] // 液晶数据口
#define LCMC XBYTE[0xA000] // 液晶命令口
虽然地址不同了,但对A10的作用依然不变。
接下来的两句则是用LCD_DOT_X来表示lcd 的宽点数,LCD_DOT_Y表示高点数。想象坐标轴,呵呵,是不是好理解了?这就说明我们现在用的lcd是LCD_DOT_X宽LCD_DOT_Y高的。
最后两句则是告诉我们,这个lcd可以显示5行,每行11个字符。这些参数都是可以根据lcd不同而变化的。
/*****************************************
struct cursortype
{
uchar x;
uchar y;
};
struct cursortype cursor;
void wridata(uchar ch)
{
LCMD=ch;
}
void wricmd(uchar ch)
{
LCMC=ch;
}
*****************************************/
这几句也都属于初始化步骤。首先struct cursortype这个结构体是用来定义光标的,光标在屏幕上当然可以用x和y两个值表示,这样我们到时候用cursor.x或者 cursor.y确定光标位置就方便多了。
Wridata和 wricmd两个函数则是写数据和写命令功能。仔细看,LCMD就是液晶数据口,LCMC就是液晶命令口,我们对他们进行的操作实际上就是对0xAf00或0xA000地址进行操作。例如我们用wridata(0x33),也就是说我们把0x33这个数据通过0xAf00这个外部总线接口地址送到SSD181X中。由于是0xAf00的地址,A10必然为1,控制器就知道此时你要传递的是数据拉~怎么样?很方便吧。
void light(uchar n)
{
LCMC=0x81;
LCMC=n;
}
把这个程序单独列出来是因为想讲讲程序如何传递能够作用的有效参数。首先这是一个调节lcd亮度(其实更加严格得说是对比度)的函数。我们看到他用的LCMC,说明此时我们是发送程序给lcd,让他知道我们要调整亮度了。那么究竟传递什么东西,控制器才能知道我们的意图呢?不用担心,pdf上会告诉你按什么格式发送指令给控制器才有效。这是SSD181X pdf上面的格式之一:
为了方便我只截取其中一个功能设置来进行说明。最左边的数字表示你发送的命令必须是这样的格式,中间的英文表示该格式命令所起的作用,右边的文字表示这些XXXX,也就是不确定数字的值该如何选取才能获得我们想要的效果。
就以这个方框为例吧,首先我们看中间这个小方框,它告诉我们这条指令的作用是设置对比度寄存器,也就是说我们可以通过对该寄存器的修改来实现调节对比度得调整。那么具体如何调整呢?我们再来看看最左边的方框,这个内容告诉我们,我们需要用LCMC发送两个字节数据,其中一个字节是固定的10000001就是0x81,第二个字节则是自己决定。怎么决定呢?再看看第三个小方框吧。如果你e文好,马上就能看懂它的意思。如果你e文不好……嘿嘿,建议金山词霸!(倒,谁扔的香蕉皮????!)
这里的意思就是说,对比度分为64个等级,这样XXXXXX6位二进制数就可以表示完,那么我们需要用哪个等级就设置哪个数,比如现在我们需要等级n的对比度,因此程序中出现了LCMC=n。这个n哪里来的?void light(uchar n),嘿嘿,主函数传递进来的。
呵呵,现在明白命令是怎么传送的了吧?一会儿还有例子,如果不明白不要慌张。
/*****************************************
void cls(uchar ch)
{
uchar i,j;
for(j=0x0;j
{
wricmd(0xb0+j);//set page //1011xxxx
wricmd(0x10);//set column msb
wricmd(0x0);//set column lsb
wricmd(0xe0);//set modify-read mode
for(i=0;i<98;i++)
{
wridata(0x00); //填满0即清屏
}
wricmd(0xee);//reset modify-read mode
}
}
*****************************************/
这里就开始进入实质性的编程部分了。这是一个清屏函数,传递的参数ch=9就表示连带ICON一起清除,如果ch=8则表示不清除ICON。这里的ch实质上表示GDDRAM中的page页面值。
这里有点难以理解。先看看GDDRAM是怎么构成的。
这是pdf中关于GDDRAM地址分布图的说明,我们可以从这里清晰看到page的概念:一个page实际上是一个拥有8个高点的行,64点高的屏幕就有8个page,每个page的宽度都是和整个RAM宽度是一样的。从pdf的最开始我们就可以看到1815的特性是132×64+1 icon line,也就是说高为64,换算过来就是8个page。而GDDRAM的数据分布图与lcd实际的数据显示图是完全一致的,实际上lcd的显示的就是GDDRAM的映射。因此我们清屏的目的就是为了清除GDDRAM中的值。因此从for(j=0x0;j语句中我们得知,循环次数是由ch决定的。如果ch=8,那么循环8次,就只能清除这8个page(页面)。只有ch=9,才能清除最后的那一个1 icon line。
好,我们接着往下看。进入循环之后,程序要做的事就是一个page一个page得清除信息。首先wricmd(0xb0+j),翻翻pdf,找到相关信息:
这下知道了,这个语句的意思就是选择要进行读写操作的页面。因为总共只有8个页面,所以4个不确定位就够拉。这也就是wricmd() 的参数是“0xb0+j”的原因:page数只由j,也就是ch控制。
接下来两句wricmd(0x10)和wricmd(0x0),我们同样可以通过pdf的信息得知,作用时用来设置列的低地址和列的高地址。参数高四位为0001的表示设置高地址,0000的表示设置低地址。默认情况下高低地址均为0X0000。
接下来就是设置工作模式wricmd(0xe0)。这个语句的作用是set read-modify-read mode,就是设置成读-改-写模式。???????????
最后把本次循环我们选择的page填0,也就达到了请零的目的。需要注意的是I的范围是0-97,为什么?因为我们现在举例用的lcd宽点数只有98。wridata(0x00)的作用是把某一列写入0。没想到吧?呵呵,因为液晶上一个字节的显示是竖的一排排的,存储在GDDRAM中当然也是竖着拉~也就是一列代表一个字节8位^_^。
刚才我们把工作模式设置为读-改-写,现在当然要结束这个模式拉,这就是wricmd(0xee)这个语句所起的作用。
最后继续循环。Cls的工作就完成了~
/*****************************************
show_asc(uchar ch);
//***********************
//初始化液晶
uchar lcdlight="32";
void initlcd(void)
{ P2=0x00; //P2作为总线时,其寄存器的值对总线没有影响。
//初始化为0,是为了更好的配合液晶的6800总线。motorola的液晶内部固定了6800总线方式。
//如果液晶是8080总线,则无需这样做。
wricmd(0x2f);//SET POWER CONTROL,开启一系列与电源有关的功能
wricmd(0x20);//REGULATOR RESISTOR SELECT,内部反馈增益最小
wricmd(0x81);
wricmd(lcdlight);设置对比度值
wricmd(0x40);//设置初始显示线,从哪里开始是玻璃上的布线决定的
wricmd(0xa0);//ADC=0(SEG1~SEG132)
wricmd(0xc8);//SHL=0(COM1~COM64)
wricmd(0xa2);//设置LCD BIAS为1/9
cls(9);//全部清除,包括icon
wricmd(0xaf);//开启显示,也就是把GDRAM上的数值显示到屏上
setcursor(0,0);//设置光标到左上角
}
*****************************************/
到这里,就正是进入我们的初始化了。什么?弄了半天你还没有讲初始化啊?各位看官不要着急,此初始化非彼初始化也。刚才我们讲的都是整个程序的初始化,而现在我们进入的是液晶屏的初始化。让我们看看这个过程吧。首先是uchar lcdlight="32",顾名思义,这个参数的作用就是调节lcd对比度的拉。motorola的液晶默认对比度为32。先让lcdlight=32,到时候利用wricmd函数,直接一个wricmd(lcdlight)语句,多方便~^_^。
闲话少说,接着往下看。下面是一个initlcd函数,这是可是如假包换的液晶初始化函数。液晶在每次上电使用都需要初始化,而大多数初始化程序我们都可以不去理会,因为那些都是按照说明书所说的,用于设置COM数和SEG数还有BIAS值的。
这里再解释一下P2=0x00;的作用。开始的时候我们就说过,这个液晶使用的是6800总线,这段,其实是为了兼容6800总线加上的,LCD的D/C脚在一开始的时候应该设为0,也就是写成:P22=0;就可以了,P22就是接到LCD的D/C脚上的。另外再提醒一点,一些必须的值,如COM,SEG,BIAS,显示模式等,在使用中这些设置用户是不应该改变的。
到这里,LCD初始化正式完成,已经可以供用户正常使用了。
/*****************************************
show_asc(uchar ch)
{
uint addr;
uchar hzdata[16];
uchar xdot,i;
addr=16*ch;
readeprom(addr,hzdata,16); //读出16个字节的点阵数据
xdot=cursor.x*8;
wricmd(0xb0+cursor.y); //将y位置送入液晶
wricmd(xdot & 0x0f); //将x位置送入液晶
wricmd( 0x10 | (xdot >> 4 ));
wricmd(0xe0);
for(i=0;i<8;i++)
{
wridata(hzdata[i]); //写上半个字符
}
wricmd(0xee);
wricmd(0xb0+cursor.y+1);
wricmd(xdot & 0x0f);
wricmd(0x10 | (xdot >> 4 ));
wricmd(0xe0);
for(;i<16;i++) //写下半个字符
{
wridata(hzdata[i]);
}
wricmd(0xee);
}
*****************************************/
要理解这段程序,必须先知道点阵表示的含义。我们知道,字母和汉字是按字模位信息显示的,那如何得到汉字的字模信息呢?难道要我们自己去做?NO。DOS前辈们经过艰辛的努力,将制作好的字模放到了一个个标准的库中以免去后辈的麻烦,这就是点阵字库文件。一般我们使用16*16的点阵宋体字库,所谓16*16,是每一个汉字在纵、横各16点的区域内显示的,前一个16表示列,后一个十六表示行。不过后来又有了HZK12、HZK24,HZK32和HZK48字库及黑体、楷体和隶书字库。
这段程序是用来在当前光标位置显示一个6×12 点阵的ASC码字符的。其实标准的ASC码字符应该是8×16点阵区域表示,这也就是为什么我们常说“一个汉字占据两个字符位”的原因。Ch表示的是asc字符的值。首先说明一点,一个ASC字符分两部分显示,也就是说,把分为上下两个“半ASC码字符”。汉字同样应该如此显示。当然这只是我们目前介绍的这个lcd的特性。如果你使用的是别的种类,千万不要生搬硬套,一定要仔细阅读说明书。
再回头看程序。首先就是一堆定义,无符号整形数addr表示的是点阵在flash中的物理位置,表示ASC码字符‘1’的字符点阵占据的物理位置是0-15。为什么?你看,16*8点阵区域,一个点用1bit表示,‘0’就是灭,‘1’就是亮,那么总共128bit,是不是就是16byte?同理,字符‘2’是16-30,以此类推。所以addr=16*ch。比如我传递进来的ch是4,则addr为64。而hzdata这个数组是用来存储读出的数据的。Xdot表示的是横向点位置,在下面我们可以知道它的计算公式是cursor.x*8,就是光标横坐标值乘以8。由于之前我们将光标设置在左上角,所以cursor.x为0,因此此时横向点位置也为0。如果我们已经显示了一个ASC字符,此时的cursor.x就应该为1,那么xdot就应该为8:这应该很好理解,从点阵区域的大小我们可以知道一个ASC码字符从横坐标上看占用的是8个点(0-7),下一个ASC码字符当然应该从8开始拉。
接下来是readeprom(addr,hzdata,16)这个函数。什么意思呢?由于它是属于另外一个.c文件,这里只是给出原型:
Readeprom函数原型如下:
readeprom(ulong ad,uchar *pst,uint n)
{
union {ulong addr_l;struct {uint a32;uint a10;}addr_i;struct {uchar a3;uint a21;uchar a0;}addr;} address;
uint i;
uchar xdata *flash;
P1=0xff; //P1口如被占用暂停读取
while(P1!=0xff);
address.addr_l=ad;
P1=(P1&0xc0) | (address.addr.a21/0x20); //设置bank线,每块8K字节
flash=0x8000+address.addr_i.a10%0x2000; //flash窗口地址范围0x8000-0x9ffff
for(i=0;i 读N个字节//
{
*(pst++)=*(flash++);
if(flash==0xa000) //如果地址跨页则翻到下一个bank
{
P1++;
flash=0x8000;
}
}
}
在这里我们就不单独解释了,只是说说他的大概作用,即读N个flash中的字节,每次最多65535字节。入口参数的含义分别是:ulong ad为字符在flash中的物理地址,uchar *pst表示读出来放在内存中的指针首位置,uint n表示读出多少个字节。
也就是说,readeprom(addr,hzdata,16)后,我们已经把表示ch需要用的16个字节传到了hzdata数组中,到时候就可以直接拿来用了。
在接下来是送x,y的位置,为什么y位置是0xb0+cursor.y呢?0xb0是一个命令指令,表示设置page。刚才说了,一个page是8行组成,也就是说高8个点,也就是说一个字符或者汉字都应该由两个page来提供数据。刚才还说了,显示是分上下两半部分组成的,所以先page设置成0xb0+cursor.y,然后再设置成0xb0+cursor.y+1,这样是不是先后选中两个page?嘿嘿,很顺理成章吧?由于我们这个系列lcd默认是132点宽,所以横向点的数目(也就是列地址)至少需要8位表示才够了(7位只有128),但是x位置送的列地址是xdot & 0x0f,高8位的4个0是命令标志,只有低4位,明显不能表示完。怎么办呢?我们就分两步送,先送低4位,再说高四位。忘了这个设置列地址的指令?回头去看看cls函数中的内容吧^_^。高4位怎么送?wricmd( 0x10 | (xdot >> 4 ));这是设置高4位列地址的指令。低四位表示列的高地址。
继续。wricmd(0xe0)的作用是设置成读-改-写模式。这个前面已经介绍过了。在接下来就是送我们要送显的数据到GDDRAM中的过程了,这个过程很简单,大体就是一列一列的送,送了一个字节后列GDDRAM中的列地址自动加一,数组下标也加一,然后再继续送,其实我们从cls函数的过程中就能领悟到。送完上半部分,高低列地址重新送,page+1,再重复这个过程。具体指令就不介绍了,聪明的你一定能够理解~呵呵。
/*****************************************
show_hz(uchar ch1, uchar ch2)
{
ulong addr;
uchar hzdata[32];
uchar xdot,i;
if (ch1>=0xb0) //寻址汉字在flash中的物理位置
{
addr=(ch1-0xb0)*94+ ch2-0xa1;
addr=addr*32+0x5a40;
}
else
{
addr=(ch1-0xa1)*94+ ch2-0xa1;
addr=addr*32+0x800;
}
readeprom(addr,hzdata,32); //读出32个汉字点阵数据
xdot=cursor.x*8; //计算X位置
wricmd(0xb0+cursor.y);
wricmd(xdot & 0x0f);
wricmd( 0x10 | (xdot >> 4 ));
wricmd(0xe0);
for(i=0;i<16;i++) //显示上半个汉字
{
wridata(hzdata[i]);
}
wricmd(0xee);
wricmd( 0xb0+cursor.y+1);
wricmd(xdot & 0x0f);
wricmd( 0x10 | (xdot >> 4 ));
wricmd(0xe0);
for(;i<32;i++) //显示下半个汉字
{
wridata(hzdata[i]);
}
wricmd(0xee);
}
*****************************************/
这个程序的作用就是在当前光标位置显示一个16*16 汉字或者全角字符。入口参数ch1表示该汉字的区码,入口参数ch2表示位码。
说到这里可能有一些朋友不理解了。下面我来讲一下。前面已经介绍了汉字库的一些知识,虽然汉字库种类繁多,但都是按照区位的顺序排列的。前一个字节为该汉字的区号,后一个字节为该字的位号。每一个区记录94个汉字,位号则为该字在该区中的位置。因此,汉字在汉字库中的具体位置计算公式为:94*(区号-1)+位号-1。减1是因为数组是以0为开始而区号位号是以1为开始的。这仅为以汉字为单位该汉字在汉字库中的位置,那么,如何得到以字节为单位得到该汉字在汉字库中的位置呢?只需乘上一个汉字字模占用的字节数即可,即:(94*(区号-1)+位号-1)*一个汉字字模占用字节数,而按每种汉字库的汉字大小不同又会得到不同的结果。以16*16点阵字库为例,计算公式则为:(94*(区号-1)+(位号-1))*32。汉字库文该从该位置起的32字节信息即记录了该字的字模信息。
Ok,明白了这些,这个程序就很好理解了,其实质是和我们分析的上一个显示ASC字符的程序是差不多的,首先除了定义,就是寻址汉字在flash中的物理位置。
由于在中文环境下,输入的是汉字的内码,我们必须将之转换成区位码,算出偏移量,从字库中找到对应的汉字,将其字模显示即可。内码转换成区位码方法如下:
qh=c1-0xa0 wh=c2-0xa0
其区位码就是:
qw=qh*0xff+wh
该汉字在字库中离起点的位置是:
offset=(94*(qh-1)+(wh-1))*32L??????????????
其他步骤和上一个函数几乎一模一样,就不多废话了~。
/*****************************************
void setcursor(uchar x, uchar y)
{
if ( x<= LCD_MAX_X )
cursor.x= x;
else
cursor.x= LCD_MAX_X;
if ( y<= LCD_MAX_Y )
cursor.y= y;
else
cursor.y= LCD_MAX_Y-1;
}
*****************************************/
再看接下来的这个程序。这个程序的作用是设置光标位置,以8×8点阵为一个光标单位,入口参数表示x方向和y方向光标位置。LCD_MAX_X和LCD_MAX_Y的含义和计算公式在刚开始的时候我们就已经说过了。至于这里为什么是LCD_MAX_Y-1,前面已经说了一个字符或者汉字都应该由两个page来提供数据,现在我需要在7,8页显示东西,那么我应该是把纵坐标点设置到7页吧?呵呵,其实点破就很简单了。
/*****************************************
void lcdstring( uchar *pst)
{
while ( *pst != 0 )
{
if ( *pst < 0x80 ) //小于0x80是字符
{
if (*pst==0x0a) setcursor(0, cursor.y+2); //处理回车换行
else if (*pst== 0x0d) setcursor(0, cursor.y );
else
{
show_asc(*pst); //显示ASC字符
cursor.x++;
if (( cu