教你在12864上打点(基于ST7920控制器)
基于ST7920控制的12864液晶用于字符显示很方便的,但网友说用它显示图形并不合适,原因就是它绘图时先要关闭显示,绘完后又要打开,速度会较慢。我没有用过别的液晶,手中只有这一款,摆弄了几天,掌握了一点东西,写出来共享。首先,我们知道,图形都是由像素点组成的,绘图的基础其实就是画点。只要我们能点亮液晶的任意一个像素点,那么绘图就不是什么难事了。万丈高楼平地起嘛,先要做的,当然是要打好基础。
ST7920提供了用于绘图的GDRAM(graph display RAM)。共 64×32 个字节的空间(由扩充指令设定绘图 RAM 地址),最多可以控制 256×64点阵的二维绘图缓冲空间。在它的Datasheet给出了GDRAM的坐标地址对照表:
http://cache.amobbs.com/bbs_upload782111/files_28/ourdev_540908.png
(原文件名:图片1.png)
用坐标表示,就是这样:
http://cache.amobbs.com/bbs_upload782111/files_28/ourdev_540909.png
(原文件名:图片2.png)
它的横坐标每一个地址都是16 位的。共16个地址,256位。
很明显,它能控制256*64像素的液晶屏,而我们的只是128*64像素液晶屏,显然只用到它的一部分。
我刚开始以为它对应屏幕的绘图RAM是这样分布的(如红色部分):
http://cache.amobbs.com/bbs_upload782111/files_28/ourdev_540910.png
(原文件名:图片3.png)
结果栽了大根头,后来终于弄明白,原来它对应屏幕的GDRAM是这样分布的: 汗,发了一半不小心提交了,继续
http://cache.amobbs.com/bbs_upload782111/files_28/ourdev_540911.png
(原文件名:图片4.png)
只要我们清楚了它的GDRAM和屏幕上像素点的映射(对应)关系,点亮对应的像素点就容易多了。要点亮某一个像素点,就是将这个像素点在GDRAM中对应的位置1,这个相信没人会不知道吧?
我们先讨论一下思路,再一步步写代码。我觉得,思路要比代码重要的多,只要你的思路通了,正确了,那么写出代码肯定会很容易。
首先,给你x,y的坐标,要你点亮一个点,要怎么做呢?从上面的图我们知道,它是分为两个半屏的,首先,我们要确定这个点是在上半屏还是下半屏,然后确定它是在那一行(纵坐标Y),再确定它是在哪一个字节的哪一个位(也就是确定它在那一列,即横坐标X)。这些都确定后我们就定位到某一个具体的位上了,只就将这个位置1,就OK了。
下面我们边写代码边讨论。
因为这里仅仅是讨论如何在12864上打点的,而不是给12864写一个驱动,所以对于基本的数据读写函数,我们不做讨论,这里假设已经有了如下基本函数:
void lcd_write_cmd(unsigned char); //lcd 命令写
void lcd_write_data(unsigned char);//lcd 数据写
unsigend char lcd_read_data(void); //lcd 数据读
好了,就这些了。
为了方便,我们定义如下宏:
#defineBASIC_SET 0x00 //基本指令集,后面的数字查数据手册,下同。
#defineEXTEND_SET0x00 //扩展指令集
#defineDRAW_ON 0x00 //绘图显示开
#defineDRAW_OFF 0x00 //绘图显示关
我们现在开始写点亮某一个点的函数:
void lcd_set_dot(unsigned char x, unsigned char y)
{
unsigned char x_byet, x_bit; //在横坐标的哪一个字节,哪一个位
unsigned char y_byte, y_bit;
x_byte = x / 16; //算出它在哪一个字节(地址)
//注意一个地址是16位的
x_bit = x % 16; //算出它在哪一个位
y_byte = y /32; //y是没在哪个字节这个说法
//这里只是确定它在上半屏还是下半屏
//0:上半屏 1:下半屏
y_bit = y % 32; //y_bit确定它是在第几行
lcd_write_cmd(EXTEND_SET); //扩展指令集
lcd_write_cmd(DRAW_OFF); //绘图显示关闭
lcd_write_cmd(0x80 + y_bit); //先写垂直地址
//具体参照数据手册
lcd_write_cmd(0x80 + x_byte + 8 * y_byte); //水平坐标
//下半屏的水平坐标起始地址为0x88
//(+8*y_byte)就是用来确定在上半屏还是下半屏
if (x_bit < 8) //如果x_bit位数小于8
{
lcd_write_data(0x01 << (7 - x_bit)); //写高字节。因为坐标是从左向右的
//而GDRAM高位在左,底位在右
lcd_write_data(0x00); //低字节全部填0
}
else
{
lcd_write_data(0x00); //高字节全部填0
lcd_write_data(0x01 << (15 - x_bit));
}
lcd_write_cmd(DRAW_ON); //打开绘图显示
lcd_write_cmd(BASIC_SET); //回到基本指令集,毕竟ST7920是以字符为主的
return ;
}
基本画点函数算是完成了,但是我们如果使用这个函数,就会发现问题。你且用它沿横坐标画几个连续的点试试,肯定不是你想要的结果。
出现问题的原因是因为我们画点时对其余的位全部填0处理了,造成对原来的信息的破坏。所以我们要读出要写的那个地址原来的数据,再进行加工,写回去就可以解决问题了。
改进后的代码:
void lcd_set_dot(unsigned char x, unsigned char y)
{
unsigned char x_byet, x_bit; //在横坐标的哪一个字节,哪一个位
unsigned char y_byte, y_bit;
unsigned char tmph, tmpl; //定义两个临时变量,用于存放读出来的数据
x &= 0x7F;
y &= 0x3F;
x_byte = x / 16; //算出它在哪一个字节(地址)
//注意一个地址是16位的
x_bit = x&0x0F; //算出它在哪一个位
y_byte = y /32; //y是没在哪个字节这个说法
//这里只是确定它在上半屏还是下半屏
//0:上半屏 1:下半屏
y_bit = y&0x3F; //y_bit确定它是在第几行
lcd_write_cmd(EXTEND_SET); //扩展指令集
lcd_write_cmd(DRAW_OFF); //绘图显示关闭
lcd_write_cmd(0x80 + y_bit); //先写垂直地址(最高位必须为1)
//具体参照数据手册
lcd_write_cmd(0x80 + x_byte + 8 * y_byte); //水平坐标
//下半屏的水平坐标起始地址为0x88
//(+8*y_byte)就是用来确定
//在上半屏还是下半屏
lcd_read_data(); //先空读一次
tmph = lcd_read_data(); //读高位
tmpl = lcd_read_data();
lcd_write_cmd(0x80 + y_bit); //读操作会改变AC,所以重新设置一次
lcd_write_cmd(0x80 + x_byte + 8 * y_byte);
if (x_bit < 8) //如果x_bit位数小于8
{
lcd_write_data(tmph | (0x01 << (7 - x_bit)));//写高字节。因为坐标是从左向右的
//而GDRAM高位在左,底位在右
lcd_write_data(tmpl); //原数据送回
}
else
{
lcd_write_data(tmph); //原数据送回
lcd_write_data(tmpl | (0x01 << (15 - x_bit)));
}
lcd_write_cmd(DRAW_ON); //打开绘图显示
lcd_write_cmd(BASIC_SET); //回到基本指令集,毕竟ST7920是以字符为主的
return ;
}
画点函数到此就完成了,剩下的事情就是对函数的优化了。例如对入口参数的检查,对乘除法的优化等等。 mark!!! 支持下。 谢谢
!! 谢谢 这么好的资料,对新手来说太好了。谢谢! 谢谢 ding mark 看了很有启发,我以前做过画点函数,没有成功,有空按这个思路重新做下。 支持一下 mark 顶起来 看了好大收获,一起研究的加我QQ 865115958 顶一下 mark 记号~ 不错,多谢分享! mark 牛 1 mark mark 楼主 我真是爱死你了 这几天我正在弄这个 不过我不会c语言 我准备把它翻译成汇编 艾悲剧 谢谢楼主~~~! 回复【24楼】asj1989
-----------------------------------------------------------------------
其实只要知道了原理,C和汇编都一样的。 Good ! 顶 cool啊,介绍了很多细节上的东西,不像很多人介绍的那样,抄来抄去的,都是废话、没几句是有用的! 学习。好文章,我转载了。楼主哥,不要介意。 再贴两个函数。没经过优化,大家多指教。
/*********************************************************************
* 函数名: lcd_draw_bmp(); *
* 功能: 在LCD指定坐标绘制一指定大小的BMP图像 *
* 输入: x(0 - 7)y(0 -63) width(1 - 128) height(1 - 63) *
* ******************************************************************/
extern void lcd_draw_bmp(uchar x, uchar y, uchar width, uchar height, char *bmp)
{
uchar i, j;
uchar y_byte = y / 32;
uchar y_bit = y % 32;
lcd_write_cmd(EXTEND_SET); //扩展指令集
lcd_write_cmd(DRAW_OFF); //绘图显示关
if (y >= 32 || (y < 32 && y + height <= 32)) //要绘制的图案全部在上半屏或下半屏
{
for (i = 0; i < height; i++)
{
for (j = 0; j < width / 16; j++)
{
lcd_write_cmd(0x80 + y_bit + i);
lcd_write_cmd(0x80 + x + j + y_byte * 8); //通过y_byte * 8 来选定上半屏或下半屏
lcd_write_data(*bmp++);
lcd_write_data(*bmp++);
}
}
}
else if (y < 32 && y + height > 32)//要绘制的图案一半在上半屏,一半在下半屏
{
for (i = 0; i < 32 - y; i++) //上半屏部分
{
for (j = 0; j < width / 16; j++)
{
lcd_write_cmd(0x80 + y_bit + i);
lcd_write_cmd(0x80 + x + j);
lcd_write_data(*bmp++);
lcd_write_data(*bmp++);
}
}
for (i = 0; i < y + height - 32; i++) //下半屏部分
{
for (j = 0; j < width / 16; j++)
{
lcd_write_cmd(0x80 + i);
lcd_write_cmd(0x88 + x + j);
lcd_write_data(*bmp++);
lcd_write_data(*bmp++);
}
}
}
lcd_write_cmd(DRAW_ON); //绘图显示开
lcd_write_cmd(BASIC_SET); //基本指令集
return ;
}
/*******************************************************
* 函数名: lcd_put_var() *
* 功能: 向指定坐标输出一个两位数变量 *
* 输入: x坐标(1 - 14) y坐标(1 - 4) *
* ****************************************************/
void lcd_put_var(uchar x, uchar y, uchar var)
{
uchar xx, addr, tmp, varh;
x--;
y--;
varh = (var / 10 % 10) + '0'; //算出变量高位的ASCII码
var = (var % 10) + '0'; //变量低位的ASCII码
//xx = x % 2;
xx = x & 0x01; //x是奇数还是偶数
//x = x / 2;
x >>=1; //x右移一位, 相当于 x / 2
//根据y的值算出对应的地址
if (y == 0)
{
addr = 0x80 + x;
}
else if (y == 1)
{
addr = 0x90 + x;
}
else if (y == 2)
{
addr = 0x88 + x;
}
else if (y == 3)
{
addr = 0x98 + x;
}
if (xx) //如果x % 2不为零, 说明数据在某一地址的低八位上
{
lcd_write_cmd(addr); //先写地址
lcd_read_data(); //空读一次
tmp = lcd_read_data();//读出高八位数据
lcd_write_cmd(addr); //设置地址
lcd_write_data(tmp); //写原来高八位的数据
lcd_write_data(varh); //写变量的高位
lcd_write_data(var); //写变量的低位
}
else //数据在某地址上, 高八位一个数字, 低八位一个数字, 直接写入
{
lcd_write_cmd(addr);
lcd_putc(varh);
lcd_putc(var);
}
return ;
}
我试着贴了一下软件生成的Html代码,给果出来效果不理想,还是贴文本吧。 回复【29楼】wenpeijun
-----------------------------------------------------------------------
谢谢。
回复【30楼】tear086.COM 缺氧
-----------------------------------------------------------------------
很乐意看到有人转载:) lcd_draw_bmp 有缺陷,只能做到宽度是16的倍数。 否则会覆盖掉原来图案。x坐标无法任意位置。
不过这个函数用于一般场合足矣。对于ST7920,如果没有开辟RAM 缓冲映射,图形输出函数做到,x ,y 坐标任意位置,widht 和 height任意,是很复杂的一件事情,要做大量的移位处理,同时涉及到读-修改-写,因此效率很低。 兼顾内存,效率等等因素,比较容易实现的是,任意坐标,任意高度,长度为8 的倍数。
譬如在坐标 17,17 处输出一幅 24*37的图片
//位图信息定义
typedef struct tagBitmap
{
uint8 m_u8Width ; //宽度(必须为8的整数倍,取图片数据时注意!)
uint8 m_u8Height ; //高度
prog_uchar* m_pBitmapCode ;
}BITMAP,*PBITMAP ;
//显示位图
void ST7920ShowBitmap(uint8 u8X, uint8 u8Y,PBITMAP pBitmap)
{
uint8 i,j;
for(i = 0 ; i < pBitmap->m_u8Height; i++)
{
for(j = 0 ; j < pBitmap->m_u8Width/8; j++)
{
ST7920WriteByte(u8X+j*8,i+u8Y,pgm_read_byte(pBitmap->m_pBitmapCode+(i*(pBitmap->m_u8Width/8))+j));
}
}
}
其中最关键的一个函数是ST7920WriteByte()
它的功能是在屏幕上面任意位置写一个字节数据,譬如在 坐标19,20 处写一个字节 0xa5
ST7920WriteByte(19,20,0xa5) 即可。 之前也是对12864的液晶感兴趣,玩了很长一段时间,后来发现其实没有多大必要。找块内存大点的单片机,开辟一个RAM,想实现啥功能都容易的很。 学习 谢谢楼主分享 回复【33楼】mcu_lover
-----------------------------------------------------------------------
是的。只不过现在刚开始学51不久,我用的51芯片开显示缓存不太现实。呵呵。只不过这个函数对我目前够用了。 学习 谢谢楼主分享 学习中…… 顶~ mark 好资料啊 顶下 好啊.多谢. mark MARK MARK 正要用12864 好东西呀!!谢谢楼主哟! 7920的用起来怎么都感觉不方便似的。 mark 楼主的程序是串口的还是 并口的啊 mark mark~ 楼主呀 我的怎么不能显示点呢 只是一趟线呀画波形也是 //************打点***********************
#include <reg51.h>
#include<intrins.h>
#include<math.h>
#define uchar unsigned char
#define uint unsigned int
sbit lcdrs=P2^2;
sbit lcdrw=P2^3;
sbit lcden=P2^4;
sbit lcdpsb=P2^5;
uchar code aa[]={"逝者如斯夫,"};
uchar code bb[]={"不舍昼夜"};
void delayus()
{
_nop_();
_nop_();
_nop_();
_nop_();
}
void delayms(uchar ms)
{
uchar i;
while(ms--)
for(i=0;i<110;i++);
}
void checkbusy()
{
uchar busy;
do{
lcdrs=0;
lcden=0;
lcdrw=1;
delayus();
lcden=1;
busy=P0;
delayus();
lcden=0;
}
while(busy&0x80); //判断最高位是否为0,为0则处于空闲状态
}
void write_cmd(uchar cmd)
{
checkbusy();
lcdrs=0;
lcdrw=0;
lcden=0;
delayus();
P0=cmd;
lcden=1;
delayus();
lcden=0;
}
void write_dat(uchar dat)
{
checkbusy();
lcdrs=1;
lcdrw=0;
lcden=0;
delayus();
P0=dat;
lcden=1;
delayus();
lcden=0;
}
uchar Read_byte(void) //读数据
{
uchar cc;
checkbusy();
P1=0xff;
lcdrs=1;
lcdrw=1;
lcden=0;
delayus();
lcden=1;
cc=P1;
delayus();
lcden=0;
return(cc);
}
void lcd_set_dot(unsigned char x, unsigned char y)
{
unsigned char x_byte, x_bit; //在横坐标的哪一个字节,哪一个位
unsigned char y_byte, y_bit;
unsigned char tmph, tmpl; //定义两个临时变量,用于存放读出来的数据
x &= 0x7F;
y &= 0x3F;
x_byte = x / 16; //算出它在哪一个字节(地址)
//注意一个地址是16位的
x_bit = x&0x0F; //算出它在哪一个位
y_byte = y /32; //y是没在哪个字节这个说法
//这里只是确定它在上半屏还是下半屏
//0:上半屏 1:下半屏
y_bit = y&0x3F; //y_bit确定它是在第几行
write_cmd(0x34); //扩展指令集
// write_cmd(DRAW_OFF); //绘图显示关闭
write_cmd(0x80 + y_bit); //先写垂直地址(最高位必须为1)
//具体参照数据手册
write_cmd(0x80 + x_byte + 8 * y_byte); //水平坐标
//下半屏的水平坐标起始地址为0x88
//(+8*y_byte)就是用来确定
//在上半屏还是下半屏
Read_byte(); //先空读一次
tmph = Read_byte(); //读高位
tmpl = Read_byte();
write_cmd(0x80 + y_bit); //读操作会改变AC,所以重新设置一次
write_cmd(0x80 + x_byte + 8 * y_byte);
if (x_bit < 8) //如果x_bit位数小于8
{
write_dat(tmph | (0x01 << (7 - x_bit)));//写高字节。因为坐标是从左向右的
//而GDRAM高位在左,底位在右
write_dat(tmpl); //原数据送回
}
else
{
write_dat(tmph); //原数据送回
write_dat(tmpl | (0x01 << (15 - x_bit)));
}
write_cmd(0x36); //打开绘图显示
write_cmd(0x30); //回到基本指令集,毕竟ST7920是以字符为主的
return ;
}
void clear_GDRAM()
{
uchar i,j;
write_cmd(0x34);
for(i=0;i<32;i++)
{
write_cmd(0x80+i);
write_cmd(0x80);
for(j=0;j<16;j++)
write_dat(0x00);
}
for(i=0;i<32;i++)
{
write_cmd(0x80+i);
write_cmd(0x88);
for(j=0;j<16;j++)
write_dat(0x00);
}
write_cmd(0x30);
}
void init()
{
lcdpsb=1;
write_cmd(0x30);
delayms(5);
write_cmd(0x0c);
delayms(5);
write_cmd(0x01);
delayms(5);
}
void main()
{
uchar i;
init();
write_cmd(0x90);
while(aa!='\0')
{
write_dat(aa);
i++;
delayms(50);
}
delayms(300);
write_cmd(0x88);
i=0;
while(bb!='\0')
{
write_dat(bb);
i++;
delayms(50);
}
init();
clear_GDRAM();
lcd_set_dot(31, 31) ;
while(1);
} b帮忙看看吧 mark 正在学习LCD to 【55楼】 xuanxuanno1
看程序是并行模式,感觉对7920来说并行模式没有任何优势可言,无论是使用还是价钱, mark 不错,谢谢分享。
收益匪浅~~ mark make 顶!
楼主不错 继续关注 我参考了楼主的程序,但是调试的时候发现不能画水平的线,也不能画挨着很近的竖线,怀疑画点那段程序有bug
我用的是ST7920和12864液晶并行方式,各位高手能指点一下吗?很急啊,拜托了各位
void Draw_Point(unsigned char x,unsigned char y)
{
unsigned char x_byte, x_bit,str; //在横坐标的哪一个字节,哪一个位
unsigned char y_byte, y_bit;
unsigned char tmph, tmpl,test; //定义两个临时变量,用于存放读出来的数据
x &= 0x7F;
y &= 0x3F;
x_byte = x / 16; //算出它在哪一个字节(地址)
//注意一个地址是16位的
x_bit = x&0x0F; //算出它在哪一个位
y_byte = y / 32; //y是没在哪个字节这个说法
//这里只是确定它在上半屏还是下半屏
//0:上半屏 1:下半屏
y_bit = y&0x1F; //y_bit确定它是在第几行
// WriteCmd12864(0x36); //扩展指令集
// delay(1);
WriteCmd12864(0x34);
delay(1);
WriteCmd12864(0x80 + y_bit); //读操作会改变AC,所以重新设置一次
delay(1);
WriteCmd12864(0x80 + x_byte + 8 * y_byte);
delay(1);
LCD12864_Read(); //先空读一次
tmph = LCD12864_Read(); //读高位
tmpl = LCD12864_Read();
delay(1);
WriteCmd12864(0x80 + y_bit); //读操作会改变AC,所以重新设置一次
delay(5);
WriteCmd12864(0x80 + x_byte + 8 * y_byte);
delay(5);
if (x_bit < 8) //如果x_bit位数小于8
{
WriteData12864(tmph | (0x01 << (7 - x_bit))); //写高字节。因为坐标是从左向右的
//而GDRAM高位在左,底位在右
delay(1);
WriteData12864(tmpl); //原数据送回
delay(1);
/* WriteCmd12864(0x30);
delay(1);
LcdClear();
sprintf(str,"%x",tmph);
DisplayCgrom(0x80, str);
test=tmph | (0x01 << (7 - x_bit));
sprintf(str,"%x",test);
DisplayCgrom(0x88, str);
delayTT(1000);*//这一段我在屏幕上打印调试信息,当我写(0,0)(1,0)两个点时,返回的tmph是0x80和0x40
//这个第二次返回应该是0xc0才对啊!
}
else
{
WriteData12864(tmph); //原数据送回
delay(1);
WriteData12864(tmpl | (0x01 << (15 - x_bit)));
delay(1);
}
WriteCmd12864(0x36); //扩展指令集
delay(1);
WriteCmd12864(0x30); //扩展指令集
delay(1);
} mark!!! 我也在这里分享一下自己写的12232打点函数,也是ST7920主控
void ST7920AdvancedMode(){
ST7920WriteCommand(ST7920_8BIT_MODE | ST7920_ADVANCED_MODE); //扩展指令集
ST7920WriteCommand(ST7920_8BIT_MODE | ST7920_ADVANCED_MODE | ST7920_GD_ENABLE);
}
void ST7920BaseMode(){
ST7920WriteCommand(ST7920_8BIT_MODE); //8位数据端口 基本指令集
ST7920WriteCommand(ST7920_8BIT_MODE); //8位数据端口 基本指令集
}
void ST7920PrintDot(BYTE x, BYTE y){
BYTE Byte0 = 0,Byte1 = 0;
ST7920WriteCommand(ST7920_GDRAM_ADDRESS | y);
ST7920WriteCommand(ST7920_GDRAM_ADDRESS | (x>>4));
ST7920ReadData(); //Dummy Read
Byte1 = ST7920ReadData();
Byte0 = ST7920ReadData();
ST7920WriteCommand(ST7920_GDRAM_ADDRESS | y);
ST7920WriteCommand(ST7920_GDRAM_ADDRESS | (x>>4));
x&=0x0F;
if(x >= 8){
x &= 0x07;
ST7920WriteData(Byte1);
ST7920WriteData(Byte0 | (0x80>>x));
}else{
ST7920WriteData(Byte1 | (0x80>>x));
ST7920WriteData(Byte0);
}
} 这个帖子真不错,我学习了,谢谢各位。 mark 这帖子真不错,受益匪浅,顶 好帖!顶! 印象中好像在哪看过? mark 引用图片【楼主位】AIHHLI 咚冬
-----------------------------------------------------------------------
http://cache.amobbs.com/bbs_upload782111/files_28/ourdev_540908.png
(原文件名:图片1.png) mark mark 建立一个显存也比这个要舒服呀 mark 好文件! 很是不错 回复【34楼】mcu_lover
-----------------------------------------------------------------------
厉害! mark mark 对楼主膜拜!!强帖昂…… 学习了 mark mark! lcd_write_data(tmph | (0x01 << (7 - x_bit)));//写高字节。因为坐标是从左向右的 lcd_write_data(tmph | (0x01 << (7 - x_bit)));什么意思啊,如果x_bit=3时,结果是什么。 #defineBASIC_SET 0x00 //基本指令集,后面的数字查数据手册,下同。
#defineEXTEND_SET0x00 //扩展指令集
#defineDRAW_ON 0x00 //绘图显示开
#defineDRAW_OFF 0x00 //绘图显示关
都是0x00? mark鸟 cool mark MARK下 学习..学习... 感谢lz给分析的这么详细,看了之后,对ST7920又有了进一步的了解。 好好····最近正在玩这个额啊···· xiexie l a 12864学习中,楼主的的解释让俺是受益非浅啊!顶,顶,我顶!我用你的打点函数,出现乱码现象,可以有人帮忙不? 回复【98楼】lihp1603
-----------------------------------------------------------------------
主程序中有开绘图显示吗
页:
[1]
2