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的内部结构画成一张图,这样好理解:

HD44780 内部结构框图 MCU 接口 RS/RW/E/DB0~DB7 指令寄存器 IR 写指令/读忙标志 数据寄存器 DR 写数据/读数据 地址计数器 AC DDRAM/CGRAM 地址 DDRAM 显示数据 RAM 80 字节 CGRAM 自定义字符 RAM 时序发生器 内部时钟/分频 显示数据锁存器 行/列驱动 LCD 驱动电路 COM/SEG 输出 驱动液晶面板 振荡器 外接 RC 或晶振 MCU 通过 RS/RW/E 控制 IR 存指令 DR 存数据 DDRAM 存显示内容 CGRAM 存自定义字

从图上你能看到,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的流程其实很简单:

  1. 发指令 0x80 + 地址(设置DDRAM地址指针)
  2. 发数据(要显示的字符ASCII码)
  3. 地址计数器自动+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的步骤:

  1. 发指令 0x40 + 地址(设置CGRAM地址,地址=字符编号×8)
  2. 连续写入8字节点阵数据
  3. 切回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才能显示
  • 指令寄存器:控制器的操作入口,初始化顺序不能乱,执行时间要等够