作为最为常见的显示模块LCD1602和LCD12864常常会被用来调试,也曾遇到用LCD作为显示器显示传感器测量结果的小项目,这篇博客简单总结一下LCD的使用。
如何判断自己拿的是不是带字库的LCD?
我之前一直以为1602和12864是一样的,只是屏幕大小的区别,但后来发现,其实使用方法上也不一样,12864具有串行数据传输的功能,而1602只能使用并行数据传输。
先来看看1602的引脚定义,如下图所示:
使用时,将VDD、BLA接5V电源,VSS、BLK接地,VL接一个0-5V的电压信号,其大小会影响实际显示效果,需要根据实际情况调整。
信号方面,RS、R/W、E为控制信号,D0~D7为数据传输引脚,用来输入或输出指令(状态)和数据。
读操作时序
写操作时序
时序参数:
总结来说:
其中,读取到的状态字定义如下:
除显示数据的传输外,LCD1602的使用就是靠写入不同的指令来实现,其指令总结如下:
初始化的顺序:
#include //包含头文件
#define uint unsigned int //预定义
#define uchar unsigned char
sbit rs=P2^6; //1602的数据/指令选择控制线
sbit rw=P2^5; //1602的读写控制线
sbit en=P2^7; //1602的使能控制线
/*P0口接1602的D0~D7*/
uchar code table[]="1234"; //要显示的内容放入数组table
void delay(uint n) //延时函数
{
uint x,y;
for(x=n;x>0;x--)
for(y=110;y>0;y--);
}
void lcd_wcom(uchar com) //1602写命令函数
{
rs=0; //选择指令寄存器
rw=0; //选择写
P0=com; //把命令字送入P2
delay(5); //延时一小会儿,让1602准备接收数据
en=1; //使能线电平变化,命令送入1602的8位数据口
en=0;
}
void lcd_wdat(uchar dat) //1602写数据函数
{
rs=1; //选择数据寄存器
rw=0; //选择写
P0=dat; //把要显示的数据送入P2
delay(5); //延时一小会儿,让1602准备接收数据
en=1; //使能线电平变化,数据送入1602的8位数据口
en=0;
}
void lcd_init() //1602初始化函数
{
lcd_wcom(0x38); //8位数据,双列,5*7字形
lcd_wcom(0x0c); //开启显示屏,关光标,光标不闪烁
lcd_wcom(0x06); //显示地址递增,即写一个数据后,显示位置右移一位
lcd_wcom(0x01); //清屏
}
void main() //主函数
{
uchar m=0;
lcd_init(); //液晶初始化
lcd_wcom(0x80); //显示地址设为80H(即00H,)上排第一位
for(m=0;m<4;m++) //将table[]中的数据依次写入1602显示
{
lcd_wdat(table[m]);
delay(200);
}
while(1); //动态停机
}
根据上面判断显示模块是否带字库的方法,我们可以发现1602只有两个芯片,即不带字库,那有没有办法可以显示汉字和自定义的字符呢?还真有。
在LCD1602模块中,不同位置显示的字符实际上是来自于DDRAM中不同地址的数据,在某个位置显示内容即在对应地址的DDRAM中写入数据。因此,这样显示出来的数据都是其自带的数据,也就是ASCII中的字符。
除此之外,LCD1602模块中还有CGRAM和CGROM两个储存位置。其中CGROM可以看作是储存ASCII字库的位置,不能更改,掉电信息不消失。而CGRAM可随机读写,有8个字节的空间,用来存放自定义字符的代码。
仔细观察LCD1602的显示背景可以发现,它所有显示的内容都是在一个5x8的点阵中显示的,而且最底下那行没有使用,即5x7点阵,这也是关于LCD显示的指令中5x7点阵的来源。
因此,如果需要显示自定义的字符,那就需要将设置5x8点阵的数据传递给LCD显示模块,如下图就是一个自定义的°C的符号:
其中,每一行对应一个8位的数据(高三位没有使用,固定为0),一共需要8个数据,正好可以放在CGRAM中。因此,显示自定义字符时,首先要在CGRAM中写入字符代码,然后再设置CGRAM中的数据传输到DDRAM的位置。其中,写入CGRAM的指令如下图所示:
注意:上图为12864的CGRAM指令格式,而1602的CGRAM的地址只有从000~111共8个地址为有效地址,对应指令为0x40 ~ 0x47。
其51程序如下所示:
#include //包含头文件
#define uint unsigned int //预定义
#define uchar unsigned char
sbit rs=P2^6; //1602的数据/指令选择控制线
sbit rw=P2^5; //1602的读写控制线
sbit en=P2^7; //1602的使能控制线
/*P0口接1602的D0~D7*/
uchar code table[]={0x10,0x06,0x09,0x08,0x08,0x09,0x06,0x00}; //要显示的内容放入数组table
void delay(uint n) //延时函数
{
uint x,y;
for(x=n;x>0;x--)
for(y=110;y>0;y--);
}
void lcd_wcom(uchar com) //1602写命令函数
{
rs=0; //选择指令寄存器
rw=0; //选择写
P0=com; //把命令字送入P2
delay(5); //延时一小会儿,让1602准备接收数据
en=1; //使能线电平变化,命令送入1602的8位数据口
en=0;
}
void lcd_wdat(uchar dat) //1602写数据函数
{
rs=1; //选择数据寄存器
rw=0; //选择写
P0=dat; //把要显示的数据送入P2
delay(5); //延时一小会儿,让1602准备接收数据
en=1; //使能线电平变化,数据送入1602的8位数据口
en=0;
}
void lcd_init() //1602初始化函数
{
lcd_wcom(0x38); //8位数据,双列,5*7字形
lcd_wcom(0x0c); //开启显示屏,关光标,光标不闪烁
lcd_wcom(0x06); //显示地址递增,即写一个数据后,显示位置右移一位
lcd_wcom(0x01); //清屏
}
void main() //主函数
{
uchar m;
lcd_init(); //液晶初始化
lcd_wcom(0x40);//设定CGRAM地址,把自定义字符存储进去
for(m=0;m<8;m++) //将table[]中的数据依次写入1602显示
{
lcd_wdat(table[m]);
delay(200);
}
lcd_wcom(0x85); //显示地址设为85H,上排中间位
lcd_wdat(0);
while(1); //动态停机
}
需要注意:在最后写入自定义字符时,还写入了一个0,我的理解是,这一步的就是正常的写入显示内容的指令,但没有从D0~D7引脚输入数据,模块就自动调用CGRAM中的数据,这个0不能换成其他任何数据!
经过测试发现,如果要显示两个不同的自定义字符很有可能会发生冲突的情况,显示效果较差。
对于LCD12864,有两种工作模式,串行和并行,当PSB引脚为低电平时,其工作在串行模式下,此时其通信模式类似于SPI,靠三根引脚CS(片选)、SID(数据输入端)、CLK(时钟输入端) 来进行通信,因此其数据传输端口DB0~DB7无效;当PSB引脚为高电平时,其工作在并行模式下,此时RS(CS)、R/W(SID)、E(CLK) 为控制信号输入端,DB0~DB7为数据输入输出端。
因此,串行和并行方式下使用的引脚也不相同,如下图所示:
其中,由于并行引脚工作方式与LCD1602十分接近,而且目前串行操作更加流行,因此这里只介绍串行控制方法,并行控制方法可以参考1602
在LCD12864中,具有两套指令:基本指令和扩展指令,选择哪一套指令可以通过输入指令来选择,指令具体如下所示:
初始化流程
可以对照上述指令表根据自己的需要来设置。
这张图需要仔细看。首先是CS信号,在传输数据时必须为高电平,如果不需要考虑那么多的话,可以直接连接VCC,使其始终有效。
然后是SCLK信号,仔细观察可以发现,在SCLK上升沿产生数据传输,即SCLK上升沿之前要把数据准备好。
最后是SID信号,从图中可以看出,每次传输一个字节的数据,需要24个时钟,即传输3个字节。其中第一个字节为选择传输数据还是传输指令,第二个字节为数据字节的高4位加4个0,第三个字节为数据字节的低4位加4个0。
#include "LCD.h"
void delay_us(uint8_t time)
{
time *= 0.9; //晶振为11.0592MHz
while(time--);
}
void delay_ms(uint8_t times)
{
while(times--)
{
delay_us(1000);
}
}
void send_byte(uint8_t byte)
{
uint8_t i;
for(i=0; i<8; i++)
{
if((byte << i) & 0x80) //从最高位开始
{
LCD_SID = 1;
}
else
{
LCD_SID = 0;
}
LCD_SCK = 0;
// delay_us(5);
LCD_SCK = 1;
}
}
void write_cmd(unsigned char cmdcode)
{
// delay_ms(1);
send_byte(0xf8); //告诉12864接下来传送指令
send_byte(cmdcode & 0xf0); //先传输高4位
send_byte((cmdcode << 4) & 0xf0); //后传输低4位
// delay_us(100); //延时待数据写入
}
void write_data(unsigned char Dispdata)
{
// delay_ms(1);
send_byte(0xfa); //告诉12864接下来传送数据
send_byte(Dispdata & 0xf0); //先传输高4位
send_byte((Dispdata << 4) & 0xf0); //后传输低4位
// delay_us(100); //延时待数据写入
}
void LCD_Init(void)
{
delay_ms(200); //等待液晶自检,延时50ms
write_cmd(0x30); //基本指令操作,8bit
// delay_us(150); //延时137us以上
write_cmd(0x0c); //显示开关闭光标
// delay_us(110); //延时100us以上
write_cmd(0x01); //清屏
delay_ms(100); //清屏后等待一段时间实现稳定
// write_cmd(0x06);
}
void write_str(char *s)
{
// while(*s > 0)
// {
// write_data(*s);
// s++;
// delay_ms(5);
// }
unsigned char i = 0;
while(s[i]!='\0')
{
write_data(s[i]);
i++;
delay_ms(5);
}
}
void write_title(void)
{
write_cmd(0x80); //第一行首位
write_str("距离为");
write_cmd(0x90); //第二行首位
write_str("速度为");
write_cmd(0x88); //第三行首位
write_str("角度为");
write_cmd(0x98); //第四行首位
write_str("加速度为");
delay_ms(50);
}
上述例程中有一点需要注意:由于Keil_C51的编译器太垃圾,经过测试,传输字符串函数中的指针部分无法识别,对应那被注释掉的部分代码。
在调试上面那部分代码时,发现一个很严重的问题,那就是LCD12864一旦显示中文,总是显示乱码。
在网上查找资料时发现,有说Keil缺少某一个文件的,需要把它添加到根目录下的bin文件夹中,但我试了并不管用;还有说需要把Keil中含有中文字符的代码文件转换为ASCII编码格式的文件,这一点我也试了【而且不知道ASCII是个什么编码格式】,并不管用。
但第二种方法启发了我,我试着将我的Keil编码格式改为GB2312【原来为了更好看的字体改为了UTF-8】,然后通过Notepads将文件以GB2312编码格式保存,意外发现问题已经解决!
和1602一样,12864中也具备显示自定义字符的功能,而且使用方法也非常类似,也是向CGRAM中写入自定义字符的代码,然后再写入到DDRAM,从而显示出来。
值得一提的是,12864中的CGRAM有4组16x16的空间,共128个字节,可以显示4个16x16的自定义汉字或符号。其指令如下图所示:
参考指令表可以得出:该四个汉字的指令地址为0x40~0x4F、0x50 ~ 0x5F、0x60 ~ 0x6F、0x70 ~ 0x7F,配合取模软件,即可得到自定义的字符,其中,每一行的16位拆分为高8位和低8位,两个字节,然后开启下一行。