3. LCD1602控制器(HD44780)初探

说实话,很多初学者一上来就对着LCD1602的数据手册发懵。80多条指令,一堆时序图,看着就头大。我当年第一次接触HD44780的时候也是这样,翻了几页就合上了。

但后来我发现,真正在项目里用到的,其实就那么几个核心东西。今天咱们就把HD44780的底裤扒开,看看它到底是怎么工作的。

3.1 HD44780内部结构

HD44780是LCD1602的心脏。它内部其实分成了几个功能模块,我画了张图帮你理清关系。

HD44780 内部结构框图 指令寄存器 (IR) 数据寄存器 (DR) 忙标志 (BF) 地址计数器 (AC) DDRAM 显示数据RAM 80字节 (40×2行) CGROM 字符生成ROM 固化5×8点阵字库 CGRAM 自定义字符RAM (64字节) 时序发生器 振荡器 & 分频器 I/O缓冲 DB0~DB7 / RS/RW/E 💡 关键点 • 写数据→DDRAM→显示 • 写指令→IR→控制逻辑 • 读BF=1→忙,不能操作

从这张图你能看到,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位初始化流程是这样的:

  1. 上电等待:VCC上升到4.5V后,等15ms以上
  2. 写0x30:发功能设置指令(8位总线,1行,5×8字型)
  3. 等待4.1ms:再发一次0x30
  4. 等待100μs:再发一次0x30
  5. 写0x38:设置8位总线,2行,5×8字型
  6. 写0x08:关闭显示
  7. 写0x01:清屏
  8. 写0x06:设置输入模式(光标右移,画面不动)
  9. 写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%是时序细节和实际调试。

我个人习惯在写驱动前,先用逻辑分析仪抓一遍初始化波形,确认每个指令的时序都对。这一步能省下后面大量的调试时间。你想想看,如果初始化都没过,后面写再多显示代码也是白搭。