简介:四针LCD1602的IIC驱动方案面向使用51单片机、希望在有限IO下扩展字符液晶显示的开发者与学习者,重点解决传统并行接口占用引脚多、接线复杂的问题。方案以PCF8574T作为I2C转8位GPIO的桥接器件,涉及I2C时序、从机地址、LCD1602初始化序列、指令集与数据写入等关键环节,适合嵌入式入门、课程实验和小型设备显示模块参考。
压缩包共25个文件,约55KB,包含3个C源文件与3个头文件,另有Keil工程文件、HEX烧录文件,以及lst、obj、map等编译与链接产物,可用于对照源码结构、工程配置和构建结果。已有2066人学习,说明该方案在51单片机显示扩展场景中具有一定参考价值。
读者可从中获得较完整的四针LCD1602驱动实现思路,结合PCF8574T的GPIO映射理解控制线与数据线的组织方式,并参考延时、初始化、字符写入与工程配置进行移植调试,减少从并行驱动迁移到I2C驱动时的试错成本。
1. 四根线里没有一根叫"数据"
LCD1602 的并口本来要接 11 根信号线,但市面上流通的模块常常只剩四根:VCC、GND、SDA、SCL。把 51单片机、IIC 总线、LCD1602 和 PCF8574T 这四样东西凑到一起,真正反直觉的地方在于,四根线里没有一根叫"数据":8 位数据、RS、RW、EN 全被塞进一颗 8 位 IO 扩展芯片的同一个字节里,靠两次半字节写和一个下降沿完成搬运。绝大多数人卡住,是因为把它当成"接线活";实际上,这是一条被压缩到极致的时序链,理顺它之后,任何"IO 不够用"的场合你都能自己设计映射方案,而不只是抄一份能跑的驱动。
2. PCF8574T 的角色:一颗准双向口扩展芯片怎么当上总线搬运工
2.1 从 HD44780 的锁存时序反推:数据为什么要"凑齐 8 位"
LCD1602 背后是一颗 HD44780 兼容控制器,对外有三根控制线 RS、RW、E 和 8 根数据线 DB0~DB7。一次写操作的完整动作是这样:RS 置成"指令"或"数据",RW 拉低表示写方向,然后把待写字节摆到 DB0~DB7 上,最后给 E 一个高脉冲,控制器在 E 的下降沿把数据线上的电平抓进内部寄存器。
这意味着两件事。数据必须在 E 下降之前稳定一段时间(数据建立时间在 5V 下约 80ns,低压下放宽到 195ns 量级),并在下降沿之后再保持一小段;而 E 高电平本身也需要维持至少 450ns。这三个参数决定了后面所有的延时设计。
4 位模式的原理,是控制器内部有一个 4 位暂存器。你把高半字节先送进来,它先存着不执行;第二个半字节到达后,才拼成一个完整字节交给指令译码器。这不是模块厂商发明的省线技巧,而是控制器本身就支持的寻址方式,设计动机很朴素——宁可把传输时间翻倍,也要把 8 根数据线压成 4 根。
2.2 PCF8574T 的寄存器模型:地址字节之后没有"寄存器地址"
I2C 从机大致分两类。一类像 EEPROM、温湿度传感器,地址字节之后还要跟一个寄存器地址或命令字节,用来指定"我要读写哪个位置";另一类像 PCF8574,地址字节之后的所有数据字节都直接落到同一个 8 位端口寄存器上,没有内部地址的概念。
PCF8574T 属于后者,写时序只有一种格式:起始位 → 从机地址 + 写位 → 应答 → 数据字节 → 应答 → … → 停止位。你连写三个字节,效果就是端口寄存器被覆盖三次,只有最后一个值留在引脚上。
地址这里有一处最容易出错的地方。PCF8574 的 7 位地址是0100 A2 A1 A0,范围 0x20~0x27;PCF8574A 的前缀是0111 A2 A1 A0,范围 0x38~0x3F。字母 T 只代表封装形式,不改变地址段。市面上标着 0x27 的 LCD 背包板绝大多数把 A2/A1/A0 全部接地,而用 PCF8574AT 的板子地址是 0x3F——同一块板子外形一模一样,扫描程序却只认其中一个,这是"扫不到设备"最常见的答案之一。
还有一点常被忽略:端口寄存器是锁存的,写一次就一直保持,直到下一次写。上电复位后默认全 1,如果背光控制脚接的是高电平点亮,现象就是"一上电背光先亮着,屏幕却什么都没有"。很多驱动会在初始化最前面先写一次0x00把状态确定下来,理由就在这里。
2.3 准双向口的电流能力:背光为什么要过一颗三极管
PCF8574 的 8 个 IO 是"准双向"结构,这个称呼来自它的输出级:高电平时靠一个很弱的上拉,源电流能力只有 100µA 量级;低电平时是一个强下拉 MOS,灌电流能力标称 25mA。两头严重不对称,看着别扭,实际是有意为之——引脚短接到地是安全的,而且同一根线既能当输出又能当输入:写 1 就等于"松手",让外部器件把电平拉低,读引脚就能读到真实状态。
这个特性直接决定了一件事:背光LED 需要几毫安到几十毫安,靠引脚输出高电平是绝对点不亮的,必须让电流"灌"进去。所以常见背包板上,背光控制脚接的是一个三极管(或直接接 LED 限流电阻到地),P3 写 1 时三极管导通、电流从 LED 经三极管流入地;写 0 时三极管截止。你在代码里写BIT_BL是为了"松手让外部上拉把基极拉高",而并不是在"输出电流"。
同样的结构在 51 单片机上也有:P1/P2/P3 都是准双向口,P0 才是真正的开漏,必须外接上拉。所以我一般用 P2.0/P2.1 模拟 I2C——写 1 就是释放总线,写 0 就是拉低,正好符合 I2C 对开漏输出的要求,连方向寄存器都不用配。
3. I2C 在 51 上的软件模拟:三条边沿约定和一次采样窗口
3.1 起始、停止、应答:信息全编码在"SCL 高电平期间 SDA 怎么动"
I2C 的电气规则可以浓缩成一句话:SCL 为高时 SDA 不允许随意变化,唯一的例外是两个特殊边沿。SDA 在 SCL 高电平期间下降,是起始位;在 SCL 高电平期间上升,是停止位。其余时间 SDA 只能在 SCL 低电平期间改变,这样从机在 SCL 高电平采样时看到的电平总是稳定的。
应答位是第 9 个时钟周期:主机先把 SDA 释放成高,从机若要应答,就在这个周期内把 SDA 拉低。所以"有没有应答"这件事,本质是读第 9 个高电平期间的 SDA 电平——读到 0 是 ACK,读到 1 是无应答。
#include <reg52.h> #include <intrins.h> sbit SCL = P2^0; sbit SDA = P2^1; /* 5 个 _nop_() 在 11.0592MHz 下约 5.4us,加上函数调用开销接近 8us, 对应 SCL 周期约 16us,即 60kHz 左右,稳妥地落在标准模式 100kHz 之内 */ static void I2C_Delay(void) { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); } void I2C_Start(void) { SDA = 1; I2C_Delay(); /* 先释放两根线,保证总线空闲 */ SCL = 1; I2C_Delay(); SDA = 0; I2C_Delay(); /* SCL 高期间 SDA 下降:起始位 */ SCL = 0; I2C_Delay(); /* 钳住时钟,准备发第一位 */ } void I2C_Stop(void) { SDA = 0; I2C_Delay(); SCL = 0; I2C_Delay(); SCL = 1; I2C_Delay(); SDA = 1; I2C_Delay(); /* SCL 高期间 SDA 上升:停止位 */ }I2C_Start里"先把 SDA 和 SCL 都写成 1"这一步不能省。如果上一次传输被意外中断、SDA 还停在低电平,直接拉 SCL 会产生一个假的下降沿,从机可能把它当成一次起始,后续所有字节都错位。先释放再操作,是一个成本极低、收益很高的防御动作。
3.2 写一个字节:8 次"摆数据 → 抬高 → 拉低",外加一次释放
位传输的节奏是固定的:在 SCL 低电平期间把数据摆到 SDA 上,抬高 SCL 让从机采样,再拉低 SCL 准备下一位。8 位走完之后,主机必须先把 SDA 写成 1,把自己"松手",才轮到从机拉低表示应答。
/* 返回 0 表示从机应答(ACK),返回 1 表示无应答 */ unsigned char I2C_WriteByte(unsigned char dat) { unsigned char i, ack; for (i = 0; i < 8; i++) { if (dat & 0x80) SDA = 1; else SDA = 0; dat <<= 1; /* 高位先行,左移把下一位顶到 bit7 */ I2C_Delay(); SCL = 1; /* 从机在这一点采样 */ I2C_Delay(); SCL = 0; I2C_Delay(); } SDA = 1; /* 关键:先释放,把引脚让给从机 */ I2C_Delay(); SCL = 1; /* 第 9 个时钟 */ I2C_Delay(); ack = SDA; /* 0 = ACK,1 = NAK */ SCL = 0; I2C_Delay(); return ack; }ack = SDA这一句在 51 上能直接读引脚,前提是前面已经写过SDA = 1。准双向口的引脚读回的是外部真实电平,但如果你之前写的是 0,强下拉会把线死死拽在低电平,读回来永远是 0。这个坑我在别的总线上也遇到过,症状是"应答永远是 ACK",比无应答更难查。
3.3 11.0592MHz 下的延时账本与总线锁死
51 的一个机器周期是 12 个晶振周期,11.0592MHz 下约 1.085µs。一条_nop_()占一个机器周期。标准模式 I2C 的 SCL 周期是 10µs,高低各 5µs;快速模式 400kHz 对应 2.5µs,靠软件循环根本压不准。
我一般不会去精算循环次数,而是先用示波器或逻辑分析仪量一次 SCL 的实际频率,再倒推延时。上面的 5 个_nop_()量出来大约 60kHz,偏慢一点完全没问题——PCF8574 没有最小时钟频率,慢只会让一次刷屏多花几十毫秒,不会出错。反过来,为了追求速度把延时压到 1~2µs,很容易撞上 51 端口的边沿特性:某些 8051 衍生型号在引脚从 0 翻转到 1 时,会有两个时钟周期的强上拉,在 SDA 上产生一个尖锐的过冲,从机可能把它误读成毛刺。常见的做法是在 SDA/SCL 上串 100~330Ω 的小电阻,配合 4.7kΩ 上拉,把边沿压缓。
另一个必须知道的救援手段是"总线解锁"。如果主控在传输中途复位,从机还停在"等着继续收时钟"的状态,可能一直把 SDA 拉低,主控连起始位都发不出去。解法是在初始化开头手动打 9 个 SCL 脉冲(SDA 保持释放),让从机把剩下的位吐完、自己回到空闲态。这段代码很短,但没有它,某些板子会出现"第一次上电好使,热复位后彻底失联"的情况。
4. 字节映射:RS、RW、EN 和 D4~D7 怎么挤进同一个字节
4.1 引脚分配:0x27 背包板的事实标准
一颗 PCF8574T 有 8 个 IO,LCD1602 需要 RS、RW、EN、D4~D7 共 7 个信号,再加背光控制刚好 8 个,一位不剩。常见的背包板走线高度统一,基本就是下面这张表:
| 位 | PCF8574T 引脚 | 接到 LCD1602 | 含义 |
|---|---|---|---|
| bit0 | P0 | 引脚 4 (RS) | 0 = 指令,1 = 数据 |
| bit1 | P1 | 引脚 5 (RW) | 只写方案里恒为 0 |
| bit2 | P2 | 引脚 6 (E) | 下降沿锁存 |
| bit3 | P3 | 背光三极管基极 | 1 = 点亮 |
| bit4~bit7 | P4~P7 | 引脚 11~14 (D4~D7) | 高半字节在前 |
RW 固定接 0 是一个明确的取舍:省下一次"读忙标志"的总线往返,代价是只能靠固定延时等待指令执行完。这个取舍后面还会展开。
4.2 两拍写入:EN 的下降沿才是锁存时刻
把上面那张表翻译成一次完整的字符写入,需要两次半字节传输,每次半字节又需要"EN 抬高、EN 拉低"两次 I2C 写——因为 EN 的电平变化必须通过写端口寄存器来实现。算下来写一个字符要 4 次 I2C 字节传输,每次 9 个时钟(8 位数据 + 1 位应答)。60kHz 下大约 150µs/字节,一个字符接近 600µs。
这个数字有意思的地方在于:I2C 的传输时间本身远远超过了 LCD 的建立时间和保持时间要求。HD44780 要求数据建立 80ns、EN 脉宽 450ns,而一次 I2C 写就花了 150µs,中间那几个微秒级的I2C_Delay早就把时序裕量填得满满当当。所以驱动里那句Delay_us(50)其实主要不是为了满足 E 的脉宽,而是为了满足指令执行时间(普通指令 37µs,清屏和回车要 1.52ms)。
换句话说,这套方案之所以稳,是因为它慢。拿 SPI 或者并口直接驱,反而要老老实实算时序。
4.3 完整代码:从初始化序列到写一个字符
#define LCD_ADDR 0x27 /* 7 位地址,A2/A1/A0 接地;0x3F 的板子改这里 */ #define BIT_RS 0x01 #define BIT_RW 0x02 #define BIT_EN 0x04 #define BIT_BL 0x08 /* 送一个半字节:nib 只用低 4 位,会被搬到 P4~P7 */ static void LCD_WriteNibble(unsigned char nib, unsigned char rs) { unsigned char v = (unsigned char)((nib & 0x0F) << 4) | BIT_BL; if (rs) v |= BIT_RS; /* RS=1 数据,RS=0 指令;RW 恒 0 */ I2C_Start(); I2C_WriteByte((unsigned char)(LCD_ADDR << 1)); /* 写方向 */ I2C_WriteByte(v | BIT_EN); /* EN=1,数据摆到 D4~D7 上 */ I2C_WriteByte(v & ~BIT_EN); /* EN=0,下降沿锁存 */ I2C_Stop(); Delay_us(50); /* 普通指令 37us,留余量 */ } void LCD_WriteCmd(unsigned char cmd) { LCD_WriteNibble(cmd >> 4, 0); /* 先高半字节 */ LCD_WriteNibble(cmd & 0x0F, 0); /* 再低半字节,控制器此时才执行 */ } void LCD_WriteData(unsigned char dat) { LCD_WriteNibble(dat >> 4, 1); LCD_WriteNibble(dat & 0x0F, 1); } void LCD_Init(void) { Delay_ms(50); /* 上电后必须等 >40ms,等内部复位完成 */ LCD_WriteNibble(0x03, 0); /* 此时控制器还在 8 位模式,0x3 即 0x30 */ Delay_ms(5); /* >4.1ms */ LCD_WriteNibble(0x03, 0); Delay_us(150); /* >100us */ LCD_WriteNibble(0x03, 0); LCD_WriteNibble(0x02, 0); /* 0x20:正式切到 4 位模式 */ LCD_WriteCmd(0x28); /* 4 位总线、双行显示、5x8 点阵 */ LCD_WriteCmd(0x0C); /* 开显示、关光标、不闪烁 */ LCD_WriteCmd(0x06); /* 地址自增,画面不滚动 */ LCD_WriteCmd(0x01); /* 清屏 */ Delay_ms(2); /* 清屏要 1.52ms */ }LCD_Init开头那三次0x03是最容易被抄漏的一段。原理是:上电后控制器未必处于确定状态,标准做法是连发三次功能设置指令把它"打醒"。第一次发0x30时它按 8 位模式理解(低 4 位被忽略),第二次、第三次同理;等状态稳定后,再发一次0x20,控制器才会把后续的半字节当成 4 位模式的两拍来处理。少了这段,常见现象是屏幕亮着但只显示一行黑方块。
LCD_WriteNibble里的v | BIT_EN和v & ~BIT_EN两次写是整套代码的核心:第一次把数据摆上去并抬高 E,第二次只把 E 拉低,数据位保持不变。因为中间隔着完整的 I2C 传输,数据在 E 下降沿之前早就稳定了几十微秒,时序裕量极大。
参数上唯一需要动的是LCD_ADDR。若板子用的是 PCF8574AT,改成0x3F;若背光接法不同(例如低电平点亮),把BIT_BL从初值里去掉、并在需要点亮时清零即可。
5. 避坑与排查:从"扫不到 0x27"到"满屏方块"的五种现场
5.1 现象:I2C 扫描程序一个地址都不响应
原因通常是三类。一是芯片型号其实是 PCF8574AT,地址段在 0x38~0x3F,扫 0x20~0x27 自然全空。二是 SCL/SDA 接反或上拉电阻缺失——很多模块自带 4.7kΩ 上拉,但如果你是从芯片单独引线出来搭的电路,忘了接上拉,总线在开漏状态下永远浮不起来。三是电平不匹配,5V 的主控配 3.3V 供电的扩展板,高电平被判成不确定。
解决顺序建议固定下来:先用万用表量 SCL、SDA 对 VCC 的电阻,确认有 4.7k 左右;再跑一遍 0x08~0x77 的全段扫描(连读地址一起扫,能发现地址位算错的情况);最后才怀疑芯片型号。
5.2 现象:背光亮着,屏幕一个字符都没有
原因几乎都出在对比度。LCD1602 的 V0 引脚需要接到一个分压点,通常是一只 10kΩ 电位器的中间抽头,两端接 VCC 和 GND。V0 悬空时对比度处于极值,屏幕看似"通电但空白",很多人误判成驱动没跑起来。另一个原因是 EN 从来没有真正拉低过——如果LCD_WriteNibble里两次写用的是同一个值(比如漏了& ~BIT_EN),就没有下降沿,控制器一次都没锁存。
解决办法很土但有效:先把电位器拧到中间位置,写一个LCD_WriteCmd(0x0F)(开显示 + 开光标 + 闪烁),如果能看到光标方块,说明链路是通的,问题只在对比度或显示开关。
5.3 现象:只显示一排黑方块,或者第一行有字第二行全黑
黑方块是 DDRAM 里的随机内容和 5x8 点阵全亮的组合,本质是"初始化没走完"。三种可能:上电延时不足 40ms 就开始发指令;漏了0x03 / 0x03 / 0x03 / 0x02这段唤醒序列;或者0x28写成了0x20(单行模式),此时第二行根本没有被使能,写进去的字符都被映射到第一行的隐藏区域。
排查方法:在LCD_Init每一步之间点亮一个 GPIO 做标记,用逻辑分析仪抓 I2C 波形,看实际发出去的字节序列是否和预期一致。这一步比反复改延时有用得多。
5.4 现象:上电第一次正常,复位后彻底失联
这是总线锁死。主控在传输中途被复位,从机还停在"等待后续时钟"的状态,可能持续把 SDA 拉低;主控重新初始化后发出起始位,从机把它当成数据位继续收,两边永远对不上。
解决办法是在 I2C 初始化最前面加一段解锁:SDA 写 1,然后手动打 9 个 SCL 脉冲,最后补一个停止位。从机收满 9 个时钟后会把剩余的位吐完并释放 SDA。这段代码不到十行,但不加的话,某些板子会在调试阶段反复出现"改一行代码就得断电重上"的情况。
5.5 现象:字符能显示,但位置错乱、写到第 17 个字符就消失了
这是 DDRAM 地址映射的问题,不是驱动 bug。LCD1602 的两行不是连续的 80 字节,第一行是 0x00~0x27,第二行是 0x40~0x67。用0x80 | addr设置地址时,如果你按 0x00~0x27 连续写满 40 个字符再想换行,写进去的其实是第一行的隐藏区域。
正确做法是显式指定行首地址:LCD_WriteCmd(0x80)到第一行开头,LCD_WriteCmd(0xC0)到第二行开头。两个地址分别对应 0x00 和 0x40。这一点在写"滚动字幕"或"居中显示"时特别容易犯错,症状看着像缓存溢出,其实是地址算错了。
6. 取舍:固定延时、忙标志与硬件 I2C 之间怎么选
把 RW 固定接地的方案,代价是只能靠固定延时等指令执行完。另一条路是把 RW 接到 P1 上,写完之后把 P7 临时切成输入,读回忙标志(BF)和地址计数器。听上去更"专业",实际在 4 位模式下很别扭:BF 要从 D7 读回来,得先发一个半字节、再发一次带读方向的传输,一次判断要占用总线两次往返;而且切换到读模式时 RW 拉高、P7 变成输入,一旦时序稍有偏差,就会在总线上产生竞争。我个人的判断是,除非你要做高频刷新的动态显示,否则固定延时的确定性和可调试性更值钱。
硬件 I2C 与软件模拟之间也是同样的取舍。硬件外设能跑到 400kHz,还能用中断,但 51 的硬件 I2C 一旦被干扰卡住,恢复流程比软件模拟复杂得多;软件模拟的每一位都是你亲手翻的,出问题可以随时插桩、随时降速。这类"慢但透明"的实现在小系统里往往更难翻车。
验证方法我给一个固定套路:先不接 LCD,只接 PCF8574T,反复写 0xAA、0x55,用万用表量 P4~P7 是否跟着翻转——这一步能确认地址、上拉、位序三件事;再接上 LCD,抓一次完整的LCD_Init波形,核对发出去的字节序列;最后才去调对比度。顺序反了,你就会在"到底是总线没通还是屏幕没点亮"之间来回猜。
从那以后我每次上电新板子,都强制先跑一遍"写 0xAA / 0x55 看引脚"这一步,再谈初始化序列。吃过一次教训:整晚在改延时参数,最后发现是板子上的芯片其实是 A 版本,地址差了 0x18。希望帮到你。
本文还有配套的精品资源,点击获取