news 2026/10/30 3:42:09

51单片机通过I2C驱动LCD1602:PCF8574T时序与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
51单片机通过I2C驱动LCD1602:PCF8574T时序与避坑

简介:四针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含义
bit0P0引脚 4 (RS)0 = 指令,1 = 数据
bit1P1引脚 5 (RW)只写方案里恒为 0
bit2P2引脚 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; /* 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。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 3:41:55

三屏输出技术解析与HDC203-PM24实战指南

1. 三屏输出的技术挑战与解决方案在当今多任务处理需求日益增长的办公环境中&#xff0c;单显示器已经无法满足高效工作的需求。三屏输出方案能够显著提升工作效率&#xff0c;特别是在金融交易、编程开发、视频剪辑等专业领域。然而&#xff0c;实现三屏输出并非简单的显示器堆…

作者头像 李华
网站建设 2026/10/30 3:41:54

基于CNN的猫咪疲劳状态识别技术实现

1. 项目背景与核心价值猫的疲劳状态识别是一个典型的计算机视觉分类问题&#xff0c;在宠物健康监测领域具有实际应用价值。通过分析猫咪的眼部特征、姿态变化等视觉信号&#xff0c;我们可以建立可靠的疲劳判断模型。这个毕设选题结合了当下热门的深度学习技术&#xff0c;同时…

作者头像 李华
网站建设 2026/10/30 3:41:25

Linux audit内核审计原理与生产级规则实战

1. 审计这件事&#xff0c;远不是“记个日志”那么简单很多人第一次接触 audit&#xff0c;是在某次安全检查后被要求“补上系统审计策略”&#xff0c;或者在排查一个权限异常问题时&#xff0c;发现/var/log/audit/audit.log里密密麻麻全是看不懂的typeSYSCALL记录。于是下意…

作者头像 李华
网站建设 2026/10/30 3:40:42

MATLAB工程化Nelson-Siegel国债收益率曲线拟合实现

简介&#xff1a;本资源是一套基于Matlab实现的Nelson-Siegel及Nelson-Siegel-Svensson模型完整研究代码与分析材料&#xff0c;面向金融工程、计量经济学方向的本科生、研究生及实证研究者&#xff0c;用于国债收益率曲线建模、主成分分解及宏观经济变量关联分析。包内共41个文…

作者头像 李华
网站建设 2026/10/30 3:39:53

大模型微调中偏好对齐数据集的构建与应用

1. 模型微调与偏好对齐数据集的核心价值在大模型技术快速发展的当下&#xff0c;模型微调&#xff08;Fine-tuning&#xff09;已成为提升预训练模型在特定任务上表现的关键手段。而偏好对齐&#xff08;Preference Alignment&#xff09;则是确保模型输出符合人类价值观和特定…

作者头像 李华
网站建设 2026/10/30 3:39:35

STM32轻量级JSON解析:裁剪cJSON、栈内存管理与OTA实战

简介&#xff1a;本资源是一份面向STM32嵌入式开发者的CJSON轻量级JSON解析与生成实战工程&#xff0c;适用于具备C语言基础和Keil开发经验的中级开发者&#xff0c;解决在资源受限MCU上高效处理JSON数据交换的实际问题&#xff0c;如物联网设备配置下发、传感器数据上报等典型…

作者头像 李华