3. LCD1602控制器(HD44780)初探
说实话,很多初学者一上来就对着LCD1602的数据手册发懵。80多条指令,一堆时序图,看着就头大。我当年第一次接触HD44780的时候也是这样,翻了几页就合上了。
但后来我发现,真正在项目里用到的,其实就那么几个核心东西。今天咱们就把HD44780的底裤扒开,看看它到底是怎么工作的。
3.1 HD44780内部结构
HD44780是LCD1602的心脏。它内部其实分成了几个功能模块,我画了张图帮你理清关系。
从这张图你能看到,HD44780内部有几个关键部件:
- 指令寄存器(IR):只写,存你发过来的指令码
- 数据寄存器(DR):读写,暂存要显示的数据
- 忙标志(BF):1表示芯片正忙,不能接收新指令
- 地址计数器(AC):指向当前操作的DDRAM或CGRAM地址
- DDRAM:显示数据RAM,存你要显示的字
- CGROM:固化字库,存了192个标准字符的点阵
- CGRAM:自定义字库,你可以自己造8个字符
💡 我的经验:刚接触时别纠结CGROM里每个字符的编码。你只需要记住:'A'的编码是0x41,'0'是0x30,跟ASCII表完全一致。这就够了。
3.2 指令集概览
HD44780的指令集一共11条。听起来不多,但每条指令的位定义你得搞清楚。我习惯把它们分成三类:
| 类别 | 指令 | 代码 (RS R/W DB7-DB0) | 说明 |
|---|---|---|---|
| 初始化类 | 清屏 | 0 0 0 0 0 0 0 0 0 1 | 清除显示,AC归零 |
| 归位 | 0 0 0 0 0 0 0 0 1 x | AC归零,显示不动 | |
| 输入模式 | 0 0 0 0 0 0 0 1 I/D S | 设置光标移动方向 | |
| 显示开关 | 0 0 0 0 0 0 1 D C B | 控制显示/光标/闪烁 | |
| 配置类 | 功能设置 | 0 0 0 0 1 DL N F x x | 设置数据位/行数/字型 |
| CGRAM地址 | 0 0 0 1 A5 A4 A3 A2 A1 A0 | 设置CGRAM地址 | |
| DDRAM地址 | 0 0 1 A6 A5 A4 A3 A2 A1 A0 | 设置DDRAM地址 | |
| 数据类 | 读忙标志 | 0 1 BF AC6 AC5 AC4 AC3 AC2 AC1 AC0 | 读BF和AC值 |
| 写数据 | 1 0 D7 D6 D5 D4 D3 D2 D1 D0 | 写数据到DDRAM/CGRAM | |
| 读数据 | 1 1 D7 D6 D5 D4 D3 D2 D1 D0 | 从DDRAM/CGRAM读数据 |
⚠️ 注意:我曾经在项目里犯过一个低级错误——写数据时忘了把RS拉高。结果折腾了半小时,屏幕死活不显示字符。后来用逻辑分析仪一看,RS一直低电平,数据全被当成指令处理了。记住:RS=0写指令,RS=1写数据,这个区分是基础中的基础。
3.3 初始化序列
LCD1602上电后,不会自动进入你想要的模式。你必须按照HD44780规定的时序给它初始化。这个序列我背得滚瓜烂熟,因为每次写驱动都要用。
标准的8位初始化流程是这样的:
- 上电等待:VCC上升到4.5V后,等15ms以上
- 写0x30:发功能设置指令(8位总线,1行,5×8字型)
- 等待4.1ms:再发一次0x30
- 等待100μs:再发一次0x30
- 写0x38:设置8位总线,2行,5×8字型
- 写0x08:关闭显示
- 写0x01:清屏
- 写0x06:设置输入模式(光标右移,画面不动)
- 写0x0C:开启显示,关闭光标
写成代码就是这样的:
void LCD_Init(void) {
// 步骤1:上电等待
delay_ms(20); // 至少15ms
// 步骤2-4:三次0x30,建立8位模式
LCD_WriteCmd(0x30);
delay_ms(5); // 至少4.1ms
LCD_WriteCmd(0x30);
delay_us(150); // 至少100μs
LCD_WriteCmd(0x30);
delay_ms(2);
// 步骤5:功能设置 - 8位总线,2行,5×8字型
LCD_WriteCmd(0x38);
delay_ms(2);
// 步骤6:关闭显示
LCD_WriteCmd(0x08);
delay_ms(2);
// 步骤7:清屏
LCD_WriteCmd(0x01);
delay_ms(2);
// 步骤8:输入模式 - 光标右移,画面不动
LCD_WriteCmd(0x06);
delay_ms(2);
// 步骤9:开启显示,关闭光标
LCD_WriteCmd(0x0C);
delay_ms(2);
}
🔑 核心要点:初始化序列里,前三次发0x30是最容易出问题的地方。为什么?因为上电后HD44780默认是8位模式,但如果你直接发0x38,它可能还没准备好。三次0x30的作用是确保芯片稳定进入8位模式。如果你用4位总线,这里要改成发0x33→0x32→0x28,逻辑类似。
3.4 忙标志检查
每次操作前要不要查忙标志?这个问题我纠结过很久。查了,代码更可靠;不查,代码更简单。
我的建议是:在初始化阶段用延时,在正常操作阶段查忙标志。原因很简单:初始化时芯片状态确定,延时足够;但正常操作时,清屏指令需要1.64ms,写数据只要37μs,统一延时太浪费CPU。
查忙标志的代码:
void LCD_WaitBusy(void) {
uint8_t status;
// 设置数据口为输入
DATA_DDR &= 0x0F; // 高4位输入
// RS=0, RW=1 读状态
LCD_RS_LOW;
LCD_RW_HIGH;
LCD_EN_HIGH;
delay_us(1);
status = DATA_PIN & 0xF0; // 读高4位
LCD_EN_LOW;
// 恢复数据口为输出
DATA_DDR |= 0xF0;
// 检查BF位(DB7)
if (status & 0x80) {
LCD_WaitBusy(); // 忙则递归等待
}
}
💡 小技巧:如果你用I2C扩展芯片(比如PCF8574)驱动LCD,查忙标志会变得很麻烦,因为每次读写都要通过I2C总线。这种情况下,我通常直接用固定延时替代查忙。虽然效率低一点,但代码简洁很多。对于1602这种慢速设备,延时方案完全够用。
3.5 地址映射关系
LCD1602有2行,每行16个字符。但DDRAM的地址不是连续的0~15和16~31。它有个奇怪的映射:
| 行 | 列0 | 列1 | ... | 列15 |
|---|---|---|---|---|
| 第1行 | 0x00 | 0x01 | ... | 0x0F |
| 第2行 | 0x40 | 0x41 | ... | 0x4F |
看到没?第2行的起始地址是0x40,不是0x10。我第一次写驱动时想当然地认为第2行从0x10开始,结果字符全跑到了第1行的后半段。嗯,这个坑我替你们踩过了。
所以写字符到指定位置时,要这样算地址:
void LCD_SetCursor(uint8_t row, uint8_t col) {
uint8_t addr;
if (row == 0) {
addr = 0x00 + col; // 第1行:0x00~0x0F
} else {
addr = 0x40 + col; // 第2行:0x40~0x4F
}
LCD_WriteCmd(0x80 | addr); // 设置DDRAM地址指令
}
这里0x80是DDRAM地址设置指令的标志位,后面跟6位地址。所以0x80 | addr就是完整的指令码。
⚠️ 注意:有些LCD1602模块的物理地址可能不同。我遇到过一款模块,第2行起始地址是0x00+0x40没错,但第1行只有0x00~0x07,第2行是0x40~0x47,剩下的地址是空的。买模块时最好看下数据手册,或者直接写个测试程序把所有地址扫一遍。
3.6 本章小结
HD44780的核心就这些东西:内部结构、11条指令、初始化序列、地址映射。你把这些搞明白了,LCD1602的驱动就掌握了80%。剩下的20%是时序细节和实际调试。
我个人习惯在写驱动前,先用逻辑分析仪抓一遍初始化波形,确认每个指令的时序都对。这一步能省下后面大量的调试时间。你想想看,如果初始化都没过,后面写再多显示代码也是白搭。