简介:面向51单片机初学者与电子制作爱好者,这份四针LCD1602 IIC驱动PCF8574T资料聚焦以两根I2C线加电源线控制字符液晶、缓解IO资源紧张的问题,可用于STC89C51等8051内核平台的显示模块实验与小项目移植。压缩包共25个文件、约55KB,包含C源码与头文件、Keil工程及项目配置文件,以及hex、lst、obj、map等编译与链接产物,另有构建日志和说明页面,便于对照源码、工程设置与最终烧录结果。已有2066人学习下载,说明其在I2C扩展LCD1602这一常见需求中具有较高参考度。资料围绕PCF8574T的GPIO映射、LCD1602初始化序列、指令与数据写入、延时处理和工程组织展开,读者可借助1602_bsp、delay_bsp、main等模块理解四针接线下的通信流程,并结合map、lst排查编译与资源占用问题,快速搭建可验证的显示系统。
1. 十一根线压成四根:LCD1602 挂上 PCF8574 之后到底省在哪
51 单片机上点一块 LCD1602,最反直觉的地方在于:真正难的不是让它亮,而是让它稳定地不花屏。并行接法要吃掉 11 根 IO(8 数据 + RS/RW/EN),对只有 32 根口的 51 来说太奢侈。于是 PCF8574T 这类 IIC 转 8 位并口的芯片被贴到屏幕背面,把两根线变成 RS、RW、EN 和 D4~D7,四针接法就此流行。可它并不是"少接几根线"这么简单——它把问题从"电平对不对"换成了"时序咬不咬得合",而后者才是黑块、乱码、第二行不亮的真正来源。
2. 准双向口的脾气:为什么读之前必须先写 1
2.1 一个引脚内部的四件套
拆开 PCF8574 的单个 IO,内部结构其实只有四样东西:一个输出锁存器、一个强下拉 NMOS、一个弱上拉电流源、一个输入缓冲器。厂商把这种结构叫"准双向口",意思是它没有真正的方向寄存器,方向是靠"你写进去什么"隐式决定的。
写 1 的时候,下拉管关断,引脚只靠那个大约 100µA 的弱上拉电流源往上拽。这个电流非常小,所以任何外部器件都能轻松把它拉低——这正是它能当输入用的物理基础。写 0 的时候,下拉管导通,灌电流能力能到 25mA,直接点一颗 LED 都没问题。
有意思的是,51 单片机自己的 P1、P2 口也是同样的结构。所以用 51 去驱 PCF8574,本质上是两个准双向口在对话,双方都遵守"想读先写 1"的规矩,谁先释放总线谁就掌握主动。
2.2 读引脚还是读锁存器
I2C 读操作返回的是引脚的实际电平,不是锁存器里的值。这个区别决定了整个驱动能不能工作。
假设锁存器里某一位是 0,下拉管就处于导通状态,引脚被硬按在低电平上。这时候哪怕外面的 LCD 拼命把 DB7 往上拉,读回来永远是 0,因为那颗 25mA 的下拉管比 LCD 的输出强得多。所以读之前必须先把这一位写成 1,让下拉管关断,把引脚"让"出来。
这也是读忙标志函数里那句pcf_write(base | 0xF0 | BIT_EN)的真正含义:它不是随便写个 0xF0,而是在把 P4~P7 四个锁存器置 1,好让 LCD 的 DB7~DB4 能驱动这几根线。上电复位后锁存器默认全是 1,所以第一次读不用管;但只要你在某个时刻把它们写成过 0,下一次读之前就必须写回来。
有个容易被忽略的细节:写 1 之后到真正读之间需要建立时间。引脚从 0 变 1 靠的是弱上拉给线电容充电,如果排线长、分布电容大,这个上升沿会变缓。100kHz 的 IIC 下一般感觉不到,但把速率提到 400kHz 又用了长排线,就可能读到中间态。稳妥的做法是在写 1 和读之间插一小段延时,几微秒就够。
2.3 整字节写入带来的"影子寄存器"问题
PCF8574 没有寄存器地址的概念,一次 I2C 事务就是往 8 个锁存器里写一个字节。它不支持位操作,你想改一个引脚,另外 7 个的状态必须一起写进去。
这就带来一个很实际的麻烦。新手常写的代码是pcf_write(0x04)来拉 EN,觉得"我只操作 EN 这一位"。实际上这一笔把 RS、RW、背光全清零了:RS 变 0 意味着 LCD 把接下来的数据当命令;背光变 0 意味着屏幕当场暗一下;RW 变 0 倒还好,但整体状态已经乱了。
常见的解法是维护一个影子变量,代码里叫g_ctrl,专门存那些"常驻位",比如背光。每次写之前用按位或把常驻位拼进去,写完再算数据位。这样背光不会闪,RS 也不会在两次写之间莫名其妙跳变。这个习惯看着啰嗦,但它能省掉后面一堆"为什么屏幕偶尔抖一下"的排查时间。
3. 八根口线对十六根引脚:映射表是怎么被定死的
3.1 主流模块的 P0~P7 分配
市面上带 PCF8574 的 LCD1602 模块,背面那颗芯片的 8 根口线基本按同一套逻辑分配:
| PCF8574 位 | 接到 LCD1602 | 作用 |
|---|---|---|
| P0 | RS | 0 = 命令,1 = 数据 |
| P1 | RW | 0 = 写,1 = 读 |
| P2 | EN | 使能脉冲,下降沿采样 |
| P3 | 背光控制 | 一般经三极管,注意极性 |
| P4 | D4 | 数据 bit4 |
| P5 | D5 | 数据 bit5 |
| P6 | D6 | 数据 bit6 |
| P7 | D7 | 数据 bit7 |
这套分配不是随便定的:低 4 位放控制信号,高 4 位放数据,正好对应 4 位模式下"数据从 DB4~DB7 进出"的硬件设计。高 4 位和低 4 位在字节里天然分开,写 nibble 的时候一个左移就到位,不用来回搬。
但也不是所有模块都这么排。有少数板子把 D4~D7 放在 P0~P3,控制信号放到 P4~P6。所以驱动代码里这几个位必须做成宏,不能写死成 0x01、0x02、0x04。换一块模块就全乱了,这是血泪经验。
3.2 背光位为什么必须参与每一次写
背光这一位看着无关紧要,实际上它是整个驱动里最容易翻车的地方。
背光通常不直接挂在 PCF8574 的引脚上,中间串了一颗三极管做电流放大,因为 LCD 背光动辄几十毫安,PCF8574 单脚 25mA 灌电流扛不住。三极管的接法决定了极性:有的是 P3 输出低电平点亮,有的是输出高电平点亮。写代码之前得先量一下,或者干脆两种都试。
更关键的是,因为 PCF8574 只能整字节写,背光位必须出现在每一次写操作里。写命令要带上它,写数据要带上它,拉 EN 更要带上它。漏掉一次,屏幕就闪一下。人眼对这种几十微秒的暗变其实很敏感,尤其是连续刷字符的时候,看起来就像屏幕在抖。
用影子寄存器能彻底解决这个问题:初始化时把背光位设好,之后所有写操作都从影子变量出发,只在局部临时拼上 EN 和数据位,写完不修改影子。这样背光位永远在,不会因为某次疏忽被清掉。
3.3 4 位模式的物理原因
HD44780 这类控制器本来设计了 8 位和 4 位两种总线模式。8 位模式一次送一个完整字节,速度快;4 位模式分两次送,先高 4 位后低 4 位,速度减半但省 4 根线。
PCF8574 只有 8 位口线,如果 8 位数据全接,RS、RW、EN 三根控制线就没地方放了,背光更别提。所以 4 位模式不是可选项,是唯一解。8 根口线刚好切成 4 数据 + 3 控制 + 1 背光,一位不浪费。
代价是每次写一个字节要做两次 EN 脉冲、两次 I2C 事务。按 100kHz 的 IIC 算,一次事务大约 200~300µs,写一个字符要 4 次事务(两个 nibble,每个 nibble 三次 pcf_write,实际更多),一屏 32 个字符刷下来要几十毫秒。对于显示温度、时间这类低速场景完全够用,但想刷动画就别指望了。
4. 时序咬合:一个字节为什么要拆成两次 EN 脉冲
4.1 一次 EN 脉冲能送进去几位
HD44780 的写入时序其实很简单:数据在 DB 线上就位,EN 拉高,保持至少 450ns,然后 EN 拉低,下降沿那一刻控制器把数据取走。在 4 位模式下,它只认 DB7~DB4 这四根线,所以一次 EN 脉冲只能送进去半个字节。
这就意味着写一个完整字节要两个 EN 脉冲:第一个送高 4 位,第二个送低 4 位。顺序不能反,反了就是乱码。而且这两个脉冲之间,RS 和 RW 必须保持不变,否则 LCD 会把第二个 nibble 当成另一个字节的高 4 位,整个字节流就错位了。
在 PCF8574 的框架下,一个 EN 脉冲至少对应三次写操作:先把数据摆好、EN 保持低;再拉高 EN;最后拉低 EN。三次 I2C 事务,每次都要带上正确的 RS、RW 和背光位。这就是为什么一个字符写下去要花掉一千多微秒。
4.2 写字节、写命令、写数据的三层封装
代码上最好分三层:最底层是 nibble,中间是字节,最上面是命令和数据。分层之后,上层完全不用关心 EN 怎么拉、数据怎么移位。
#include <reg52.h> sbit SCL = P1^0; sbit SDA = P1^1; #define LCD_ADDR 0x27 // PCF8574T 的 7 位地址,A2/A1/A0 全接高 #define BIT_RS 0x01 #define BIT_RW 0x02 #define BIT_EN 0x04 #define BIT_BL 0x08 static unsigned char g_ctrl = BIT_BL; // 影子寄存器,常驻背光位 static void i2c_delay(void) { unsigned char i = 8; // 11.0592MHz 下约几微秒 while (i--) ; } static void i2c_start(void) { SDA = 1; SCL = 1; i2c_delay(); SDA = 0; i2c_delay(); SCL = 0; } static void i2c_stop(void) { SDA = 0; SCL = 1; i2c_delay(); SDA = 1; i2c_delay(); } static void i2c_send(unsigned char b) { unsigned char i; for (i = 0; i < 8; i++) { SDA = (b & 0x80) ? 1 : 0; b <<= 1; SCL = 1; i2c_delay(); SCL = 0; i2c_delay(); } SDA = 1; // 释放 SDA,交给从机应答 SCL = 1; i2c_delay(); SCL = 0; // 第 9 个时钟,ACK 位这里不检查 } static void pcf_write(unsigned char data) { i2c_start(); i2c_send((unsigned char)(LCD_ADDR << 1)); // 写方向,最低位为 0 i2c_send(data); i2c_stop(); }i2c_send里有两个细节值得说。第一,数据位在 SCL 低电平期间改变,在高电平期间被采样,这是 IIC 的基本规则,所以移位必须在拉高 SCL 之前完成。第二,第 9 个时钟把 SDA 释放成 1,让从机有机会把它拉低表示 ACK;这段代码没检查 ACK,因为 LCD 模块一般不会掉线,省掉判断能快一点。真要做健壮性,可以把这个位读回来判断。
nibble 层的封装:
static void lcd_nibble(unsigned char nib, unsigned char is_data) { unsigned char base = g_ctrl | (is_data ? BIT_RS : 0); unsigned char d = (unsigned char)((nib & 0x0F) << 4); pcf_write(base | d); // EN=0,先把数据摆到 P4~P7 pcf_write(base | d | BIT_EN); // EN=1,LCD 准备采样 pcf_write(base | d); // EN=0,下降沿写入 } static void lcd_byte(unsigned char v, unsigned char is_data) { lcd_nibble((unsigned char)(v >> 4), is_data); // 先高 4 位 lcd_nibble((unsigned char)(v & 0x0F), is_data); // 再低 4 位 } void lcd_command(unsigned char cmd) { lcd_byte(cmd, 0); } void lcd_data(unsigned char dat) { lcd_byte(dat, 1); }lcd_nibble里三次写是刻意为之的。第一次写只摆数据不拉 EN,是为了让 D4~D7 在 EN 上升之前就稳定;如果数据和 EN 同时变化,LCD 可能在上升沿采到还没稳定的电平。中间那次拉高 EN 之后没有额外延时,是因为pcf_write本身要走一次完整 I2C 事务,耗时远超 HD44780 要求的 450ns 最小脉宽。真要抠时序,可以在拉高和拉低之间插一句i2c_delay(),但实测没必要。
lcd_byte里的移位方向是硬规矩:先v >> 4再v & 0x0F,先高后低。搞反了就是典型的"能显示但全是乱码"。
4.3 忙标志 BF 与 AC:要不要读
HD44780 有个忙标志位 BF,在 DB7 上。BF 为 1 表示控制器正在执行上一条指令(比如清屏要 1.52ms),这时候写进去的东西会被丢掉。读 BF 的方法是 RS=0、RW=1,然后读两次 nibble:第一次读到 BF 加上地址计数器的高 3 位,第二次读到地址计数器的低 4 位。
static unsigned char pcf_read(void) { unsigned char v = 0, i; i2c_start(); i2c_send((unsigned char)((LCD_ADDR << 1) | 0x01)); // 读方向 SDA = 1; // 释放 SDA for (i = 0; i < 8; i++) { SCL = 1; i2c_delay(); v = (unsigned char)((v << 1) | (SDA ? 1 : 0)); SCL = 0; i2c_delay(); } SDA = 1; SCL = 1; i2c_delay(); SCL = 0; // 主机回 NACK i2c_stop(); return v; } unsigned char lcd_read_bf(void) { unsigned char hi, lo; unsigned char base = g_ctrl | BIT_RW; // RS=0, RW=1 pcf_write(base | 0xF0 | BIT_EN); // P4~P7 写成 1 当输入,同时 EN=1 hi = (unsigned char)(pcf_read() & 0xF0); pcf_write(base | 0xF0); // EN=0 pcf_write(base | 0xF0 | BIT_EN); lo = (unsigned char)(pcf_read() << 4); pcf_write(base | 0xF0); return (unsigned char)(hi | lo); }这段代码里0xF0不是随便写的。它把 P4~P7 四个锁存器置 1,关掉下拉管,DB7~DB4 才能被 LCD 驱动。如果这里写成 0x00,读回来永远是 0,BF 判断就失效了,程序会一直认为"不忙",然后写进去一堆被丢弃的指令。
但实际做 51 项目时,读 BF 往往不划算。一次读 BF
本文还有配套的精品资源,点击获取