简介四针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~0x27PCF8574A 的前缀是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 ACK1 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 个 IOLCD1602 需要 RS、RW、EN、D4~D7 共 7 个信号再加背光控制刚好 8 个一位不剩。常见的背包板走线高度统一基本就是下面这张表位PCF8574T 引脚接到 LCD1602含义bit0P0引脚 4 (RS)0 指令1 数据bit1P1引脚 5 (RW)只写方案里恒为 0bit2P2引脚 6 (E)下降沿锁存bit3P3背光三极管基极1 点亮bit4~bit7P4~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; /* RS1 数据RS0 指令RW 恒 0 */ I2C_Start(); I2C_WriteByte((unsigned char)(LCD_ADDR 1)); /* 写方向 */ I2C_WriteByte(v | BIT_EN); /* EN1数据摆到 D4~D7 上 */ I2C_WriteByte(v ~BIT_EN); /* EN0下降沿锁存 */ 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。希望帮到你。本文还有配套的精品资源点击获取