4. LCD1602/2004 硬件原理:液晶显示原理、HD44780 控制器、DDRAM/CGRAM/指令寄存器
说实话,搞 LCD 驱动这么多年,我见过太多人一上来就抄代码,结果屏幕死活不亮。其实你只要把 HD44780 这颗控制器的脾气摸透了,剩下的事就是按规矩办事。今天咱们就把液晶显示原理、控制器架构、还有那几个关键存储器掰开揉碎了讲清楚。
4.1 液晶显示原理:怎么让笔画亮起来?
LCD1602 和 LCD2004,名字里的数字就说明了规格——16列×2行,或者20列×4行。每个显示位其实是一个5×8的点阵,说白了就是5列8行的小格子。
你想想看,显示一个字符「A」,就是在这40个点里,把构成A形状的那些点点亮。控制器里存了每个字符对应的点阵数据,你只需要告诉它「在第1行第3列显示字符A」,剩下的活它自己干。
关键参数速查:
- LCD1602:16列 × 2行 = 32个字符位
- LCD2004:20列 × 4行 = 80个字符位
- 每个字符位:5×8 点阵(部分型号支持5×10)
- 驱动电压:通常 4.5V~5.5V
我在项目里遇到过一块2004屏,显示到第3行就花屏。查了半天,原来是供电电压掉到4.2V了。嗯,这里要注意——LCD对电压挺敏感的,尤其长线供电时压降不能忽略。
4.2 HD44780 控制器:这颗芯片才是大脑
市面上几乎所有1602/2004模组,核心都是日立的HD44780控制器(或其兼容品)。它负责三件事:接收指令、管理显示数据、驱动液晶电极。
我个人习惯把HD44780的内部结构画成一张图,这样好理解:
从图上你能看到,MCU通过RS、RW、E三条控制线和8位数据总线跟HD44780打交道。数据先到指令寄存器或数据寄存器,然后地址计数器AC决定数据写到DDRAM还是CGRAM,最后通过锁存器和驱动电路点亮液晶。
4.3 DDRAM:显示数据 RAM,屏幕上的每个字符都在这
DDRAM(Display Data RAM)是HD44780内部的一块80字节的RAM。每个字节对应屏幕上的一个字符位,存的是该位置要显示的字符的ASCII码(或自定义字符编号)。
但这里有个坑——DDRAM地址和屏幕行列不是一一对应的。1602的地址映射是这样的:
| 行号 | 起始地址 | 结束地址 | 说明 |
|---|---|---|---|
| 第1行 | 0x00 | 0x0F | 对应屏幕第1~16列 |
| 第2行 | 0x40 | 0x4F | 对应屏幕第1~16列 |
| 第3行(2004) | 0x14 | 0x23 | 对应屏幕第1~16列 |
| 第4行(2004) | 0x54 | 0x63 | 对应屏幕第1~16列 |
常见翻车现场:
我曾经调试一块2004屏,第3行怎么都不显示。查了半天,原来我往0x14写数据,但忘了设置地址指针。HD44780的地址计数器不会自动跳到下一行开头,你得手动写指令设置DDRAM地址。
写DDRAM的流程其实很简单:
- 发指令 0x80 + 地址(设置DDRAM地址指针)
- 发数据(要显示的字符ASCII码)
- 地址计数器自动+1,继续发下一个字符
4.4 CGRAM:自定义字符 RAM,造字就靠它
CGRAM(Character Generator RAM)是64字节的RAM,用来存放用户自定义的点阵数据。每个自定义字符占8字节(5×8点阵),所以最多能定义8个自定义字符(编号0~7)。
你想想看,如果你要显示一个温度计符号、一个箭头、或者一个电池图标,ASCII码表里没有怎么办?这时候CGRAM就派上用场了。
自定义字符示例:
比如画一个向上的箭头,5×8点阵数据可以这样定义:
// 箭头图案(5×8)
byte arrowUp[8] = {
0b00100, // 第1行:中间一点
0b01110, // 第2行:三点
0b11111, // 第3行:五点
0b00100, // 第4行:中间一点
0b00100, // 第5行:中间一点
0b00100, // 第6行:中间一点
0b00100, // 第7行:中间一点
0b00000 // 第8行:空
};
写入CGRAM的步骤:
- 发指令 0x40 + 地址(设置CGRAM地址,地址=字符编号×8)
- 连续写入8字节点阵数据
- 切回DDRAM模式,显示时直接写字符编号0~7
我记得有一次做温控器项目,需要在LCD上显示一个旋转的风扇动画。就是靠CGRAM定义了4个不同角度的风扇帧,然后定时切换显示,效果还挺唬人的。
4.5 指令寄存器:控制器的操作手册
HD44780的指令寄存器(IR)只接受写入,用来告诉控制器「接下来你要干什么」。常用的指令就那么几条,我列个表:
| 指令 | 代码 | 功能 | 执行时间 |
|---|---|---|---|
| 清屏 | 0x01 | 清除DDRAM所有内容,光标回原点 | 1.64ms |
| 光标归位 | 0x02 | 光标回原点,DDRAM内容不变 | 1.64ms |
| 输入模式设置 | 0x04~0x07 | 设置光标移动方向和是否移位 | 40μs |
| 显示开关控制 | 0x08~0x0F | 控制显示/光标/闪烁的开关 | 40μs |
| 光标/显示移位 | 0x10~0x1F | 移动光标或滚动显示 | 40μs |
| 功能设置 | 0x20~0x3F | 设置数据位、行数、字体 | 40μs |
| 设置CGRAM地址 | 0x40 + 地址 | 设置CGRAM地址指针 | 40μs |
| 设置DDRAM地址 | 0x80 + 地址 | 设置DDRAM地址指针 | 40μs |
| 读忙标志/地址 | 读操作 | 读取BF标志和AC值 | — |
初始化序列(4位模式):
这是我在每个项目里都会用的标准初始化流程,少一步都可能翻车:
// 上电后等待15ms以上
delay(15);
// 发0x30三次,确保进入8位模式
write4bit(0x30); delay(5);
write4bit(0x30); delay(1);
write4bit(0x30); delay(1);
// 切换到4位模式
write4bit(0x20); delay(1);
// 功能设置:4位、2行、5×8字体
writeCommand(0x28); delay(1);
// 显示关闭
writeCommand(0x08); delay(1);
// 清屏
writeCommand(0x01); delay(2);
// 输入模式:光标右移,不移位
writeCommand(0x06); delay(1);
// 显示开启,光标关闭,不闪烁
writeCommand(0x0C); delay(1);
这里有个细节——执行时间。清屏和归位需要1.64ms,其他指令只要40μs。我见过有人清屏后马上发下一条指令,结果指令被吞了。嗯,一定要等够时间,或者读忙标志判断是否空闲。
4.6 读忙标志:别急着发下一条
HD44780的DB7引脚在读取时就是忙标志(BF)。BF=1表示控制器正在处理上一条指令,这时候不能写新指令或数据。
我个人习惯在每次写指令前都读一下忙标志,虽然会多几行代码,但可靠性高很多。尤其是在高速MCU上,40μs眨眼就过去了,你不读BF很容易踩坑。
// 读忙标志函数
uint8_t lcd_busy(void) {
uint8_t busy;
// 设置数据口为输入
DDR_DATA &= 0x0F;
// RS=0, RW=1, E=1
PORT_CTRL &= ~(1 << RS_PIN);
PORT_CTRL |= (1 << RW_PIN);
PORT_CTRL |= (1 << E_PIN);
delay_us(1);
// 读高4位
busy = (PIN_DATA & 0xF0) >> 4;
// E=0
PORT_CTRL &= ~(1 << E_PIN);
// 再读低4位(4位模式下需要)
PORT_CTRL |= (1 << E_PIN);
delay_us(1);
busy |= (PIN_DATA & 0xF0) >> 0;
PORT_CTRL &= ~(1 << E_PIN);
// 恢复数据口为输出
DDR_DATA |= 0xF0;
return (busy & 0x08) ? 1 : 0; // DB7=BF
}
不过说实话,如果你用的MCU主频不高,或者指令间隔足够长(比如加个1ms延时),不读忙标志也能跑。但做产品的话,我建议还是老老实实读BF,毕竟可靠性是第一位的。
4.7 地址计数器 AC:自动递增的指针
地址计数器AC(Address Counter)是个好东西。你往DDRAM或CGRAM写数据时,AC会自动+1。这意味着你连续写一串字符时,不需要每次都设置地址。
但AC也有个让人头疼的地方——它不会自动换行。写到第16列后,AC指向第17列,但1602的第17列在物理上不存在,数据就写到DDRAM的0x10地址去了,屏幕上看不到。
换行陷阱:
我曾经写一个滚动显示的程序,字符串超过16个字符后,后半段全丢了。后来才发现,1602的第2行起始地址是0x40,不是0x10。你得在写到第16个字符时手动设置地址到0x40,才能继续在第2行显示。
AC还有一个用途——读它的值可以知道当前光标位置。这在做菜单选择、文本编辑时特别有用。
4.8 小结:记住这三条
- DDRAM:存显示内容,地址映射要记牢,1602第2行从0x40开始
- CGRAM:存自定义字,64字节最多8个字符,写入后切回DDRAM才能显示
- 指令寄存器:控制器的操作入口,初始化顺序不能乱,执行时间要等够