简介:一份面向51单片机开发者的LCD1602显示驱动资源,基于PCF8574T芯片实现四线I2C(IIC)通信,可将原本需要最少6根并行线的LCD1602缩减为SCL、SDA、VCC、GND共4根线,从而大幅节省单片机IO资源。压缩包共25个文件,包括C语言源码(main.c、1602_bsp.c、delay_bsp.c)、头文件、Keil工程文件(.uvproj)以及编译生成的hex、lst、obj、map等中间文件,整体大小仅55KB,非常适合初学者或项目参考。目前已有2065人浏览学习,可直接在STC89C51等常见51芯片上移植使用。资源内含LCD1602初始化序列、指令写入、数据写入及通过PCF8574T扩展GPIO的完整驱动代码,并且工程结构清晰,包含延时函数、底层驱动与主控逻辑分层,可帮助读者快速理解I2C时序和LCD控制方法,作为小型嵌入式显示模块的现成方案十分实用。
1. 四针LCD1602:为什么大家都在用PCF8574T把并行屏改成I2C
传统LCD1602要占掉至少6个GPIO,这在引脚紧张的开发板上简直是灾难。四针LCD1602方案用PCF8574T把并行接口转成I2C总线,只留VCC、GND、SDA、SCL四根线,彻底解放单片机引脚。我最早接触这个方案是在一个温湿度显示项目里,当时主控引脚被传感器和按键占满,用I2C转接板后不仅接线从十几根缩到四根,连背光控制都省了一个三极管。这个方案适合所有I2C资源富余、但GPIO捉襟见肘的场景,从51到STM32再到树莓派都能用,而且PCF8574T价格便宜,是LCD1602最常见的外设扩展方案。
很多人以为四针LCD1602是屏幕本身改了接口,其实你买到的可能只是一块普通1602加一块转接板。核心变化在驱动方式:单片机通过I2C协议向PCF8574T写入8位数据,其中高4位给数据线D4-D7,P3脚控制背光,剩下的引脚控制RS和E信号。这相当于把原来的并行时序搬到I2C总线上软模拟,理解这一点,后面的代码和踩坑就都能串起来了。
2. PCF8574T与四针接线:从I2C地址到寄存器映射
2.1 PCF8574T的I2C地址为什么会翻车
PCF8574T是带I2C接口的8位双向IO扩展芯片,通过PCA9546这类I2C开关级联也能挂多块,但最常见的用法就是直连LCD1602。这块芯片的I2C地址由A0、A1、A2三个引脚的电平决定,基地址是0x20(PCF8574T)或0x38(PCF8574AT)。问题来了——市面上很多转接板把A0-A2全部接地,所以地址固定是0x27;但PCF8574AT的基地址不同,同样全部接地时地址变成0x3F。我第一次用某厂家的转接板,扫描I2C设备发现地址是0x3F,查了芯片丝印才知道是AT后缀。所以拿到转接板第一件事是看芯片型号,然后写个I2C扫描程序确认地址,别硬套网上的驱动。
| 芯片后缀 | A0-A2全接地时地址 | A0-A2全接VCC时地址 |
|---|---|---|
| PCF8574T | 0x27 | 0x2A |
| PCF8574AT | 0x3F | 0x42 |
如果你在驱动里发现初始化函数有lcd_addr = 0x27或lcd_addr = 0x3F两种写法,多半是芯片型号差异。我一般会在代码里留一个宏定义,换硬件时只改一个地方,不用全局搜地址。
2.2 四针接线的真实引脚定义
所谓四针LCD1602,不是屏幕只有四个引脚,而是转接板对外只引出VCC、GND、SDA、SCL。SDA和SCL要接主控的I2C引脚,VCC接5V还是3.3V取决于转接板上的电平转换电路。有些转接板带双向电平转换芯片,可以接3.3V主控;有些就是直连,必须接5V。我踩过一个坑:某开发板的I2C引脚是开漏输出,外部上拉到3.3V,转接板却是5V供电,结果SDA和SCL电平不匹配,逻辑分析仪上看波形全是乱的。后来统一把转接板供电改到主控同电压,问题消失。接线顺序建议:先接GND,再接VCC,最后接SDA和SCL——这个顺序能避免带电插拔时I2C总线上产生毛刺导致锁死。
PCF8574T的P0到P7八个引脚在转接板上通常标着对应的LCD引脚编号,默认映射是P0->RS、P1->RW(常接地)、P2->E、P3->背光,P4-P7分别接D4、D5、D6、D7。这套映射不是芯片官方规定的,是转接板厂家定的。所以买转接板时看一眼背面丝印,确认P3是背光控制,否则写背光开关时会误操作其他引脚。
2.3 寄存器操作与模拟并口时序
PCF8574T本质上是一个I2C控制的8位锁存器,写入的字节会直接出现在P0-P7引脚上。所以驱动LCD1602的关键不是操作芯片内部寄存器,而是通过I2C写字节模拟出液晶模块需要的时序。LCD1602的写命令流程是:RS置低表示写命令,E引脚拉高再拉低产生下降沿,数据在下降沿被锁存。在四针方案里,这些操作全部通过写不同字节到PCF8574T完成。比如要发送一个命令字节0x01(清屏),先将高4位0x0放到D4-D7上,同时RS为0,E为高,得到一个字节写入;再将E拉低,得到另一个字节写入。两次写入之间延时至少要1ms,清屏命令要等更久。
我在实际驱动中会把这种时序封装成lcd_write_nibble函数,先写高4位再写低4位,每次操作E引脚电平翻转。用逻辑分析仪查看时序时,能看到I2C总线上每写一个字节,对应的P2(E)引脚就出现一次脉冲,这就是模拟并口的直接证据。这个思路理解了,后面不管换什么单片机,移植驱动都很快。
3. 用代码驱动四针LCD1602:初始化流程与显示函数
3.1 初始化序列为什么是固定的
LCD1602的初始化时序不能乱来,因为芯片内部的上电状态不确定,必须用连续的几次命令把它拉回已知状态。四针模式下,I2C转接板刚上电时PCF8574T输出全为高,LCD的数据线和控制线都是高电平,此时直接发命令会失败。标准的初始化序列是:延时40ms以上等LCD内部复位,然后连续发送三次0x30(8位模式指令),再切换到4位模式发送0x20,最后设置显示参数。这套序列里的每个延时都来源LCD控制芯片HD44780的数据手册——上电后需要等待内部初始化完成,期间不能发命令。
void lcd_init(void) { delay_ms(50); // 等待LCD上电稳定 lcd_write_cmd(0x30); // 8位模式指令 delay_ms(5); lcd_write_cmd(0x30); // 重复发送,确保进入已知状态 delay_ms(1); lcd_write_cmd(0x30); delay_ms(1); lcd_write_cmd(0x20); // 切到4位模式 delay_ms(1); lcd_write_cmd(0x28); // 4位模式,2行,5x8点阵 lcd_write_cmd(0x0C); // 显示开,光标关,闪烁关 lcd_write_cmd(0x01); // 清屏 delay_ms(2); // 清屏要等2ms以上 }这里有几个参数值得推敲。0x28是在4位模式下发送的,先发高4位0x2再发低4位0x8,如果初始化时忘了切4位模式直接发0x28,LCD会按8位模式解析,导致显示乱码。0x0C控制显示开关,如果想看到光标调试就改成0x0E,调试完再改回来。0x01清屏命令需要延时至少1.6ms,实际用2ms比较稳妥。整个初始化序列的顺序和延时都是硬性的,我见过有人把延时全删了,结果屏幕偶尔能亮偶尔白屏,最后老老实实加回来。
3.2 写字节与写命令的底层拆分
四针模式的核心函数是lcd_write_byte,它把一次8位传输拆成两个4位小段,每个小段都要带上RS和E的电平组合。这里最容易搞混的是RS电平:写命令时RS必须为0,写数据时RS必须为1。而E引脚在每个小段传输时都要产生一个下降沿,所以一共要写4个字节到PCF8574T:高4位E高、高4位E低、低4位E高、低4位E低。如果只写两个字节,E没有翻转,数据全丢。
void lcd_send_nibble(uint8_t nibble, uint8_t rs) { uint8_t value = (nibble << 4) | (rs ? 0x01 : 0x00) | 0x04; // P4-P7为数据,P0为RS,P2为E,P1保持低电平(RW=0) i2c_write_byte(PCF8574_ADDR, value); // E高,锁存数据 delay_us(1); // 保持高电平时间 i2c_write_byte(PCF8574_ADDR, value & ~0x04); // E拉低,产生下降沿 delay_us(1); } void lcd_write_cmd(uint8_t cmd) { lcd_send_nibble(cmd >> 4, 0); // 高4位,RS=0 lcd_send_nibble(cmd & 0x0F, 0); // 低4位,RS=0 delay_ms(1); // 命令执行时间 } void lcd_write_data(uint8_t data) { lcd_send_nibble(data >> 4, 1); // 高4位,RS=1 lcd_send_nibble(data & 0x0F, 1); // 低4位,RS=1 delay_us(50); // 写数据比命令快 }上面代码里的| 0x04是把E引脚初始化为高,然后在第二个I2C写操作里通过& ~0x04把E拉低。我特意加了delay_us(1),虽然很多I2C从设备不需要这么短的延时,但LCD1602的HD44780控制器要求E高电平至少保持一定宽度,延时太短会出现偶发漏字。lcd_write_cmd里的delay_ms(1)对应命令执行时间,清屏和回家命令要更长。实际调试时如果发现显示字符偶尔错位,优先加大命令间的延时,而不是调I2C速率。
3.3 显示字符串与光标定位
有了底层写字节函数,上层的显示操作就简单了。LCD1602内部有DDRAM地址,第一行从0x00开始,第二行从0x40开始。写字符前要先发设置地址命令,注意这个命令的地址要加上0x80作为命令标志位。
void lcd_set_cursor(uint8_t row, uint8_t col) { uint8_t addr = (row == 0) ? col : (0x40 + col); lcd_write_cmd(0x80 | addr); } void lcd_print_string(const char *str) { while (*str) { lcd_write_data((uint8_t)(*str)); str++; } } void lcd_demo(void) { lcd_set_cursor(0, 0); lcd_print_string("Hello, LCD1602!"); lcd_set_cursor(1, 0); lcd_print_string("I2C via PCF8574T"); }lcd_set_cursor里的row参数只建议传0或1,第二行地址是从0x40开始的,不信邪传2进去会写到第三行甚至DDRAM末尾,显示直接飞了。lcd_print_string每次写一个字符,每个字符都要经过高4位低4位两次传输,共四次I2C操作。如果字符串很长,I2C速率不够会出现肉眼可见的刷新延迟,比如连续显示几十个字符时要几百毫秒。实际项目中我通常只在状态变化时才刷新局部,不整屏重刷。
3.4 背光控制与I2C时序的耦合
PCF8574T的P3引脚在多数转接板上控制背光,写1打开背光,写0关闭。问题在于背光操作和LCD数据共用同一个锁存器,你在发送命令或数据时,P3的电平状态会参与组成整个字节。比如发送高4位数据时,value的计算会带上背光位,如果这个位被意外清零,背光就会灭掉。
uint8_t lcd_backlight = 0x08; // P3置高,背光开 void lcd_send_nibble(uint8_t nibble, uint8_t rs) { uint8_t value = (nibble << 4) | (rs ? 0x01 : 0x00) | 0x04 | lcd_backlight; i2c_write_byte(PCF8574_ADDR, value); delay_us(1); i2c_write_byte(PCF8574_ADDR, (value & ~0x04) | lcd_backlight); delay_us(1); } void lcd_backlight_ctrl(uint8_t on) { lcd_backlight = on ? 0x08 : 0x00; uint8_t value = lcd_backlight; i2c_write_byte(PCF8574_ADDR, value); // 仅改背光,不碰其他引脚 }我看到网上很多驱动把背光位直接写死为0x08,没有独立变量,这样想关背光就得改函数本体。我习惯用一个全局变量lcd_backlight,所有写操作都加上它,背光开关函数只更新这个变量并写一次当前状态。有个细节:关背光时直接写0x00会把所有引脚拉到低,如果此刻LCD正处于E高电平状态,相当于产生了一个意外的下降沿,可能误触发锁存。所以我通常在关背光前保证E引脚是低电平,或者在写背光状态时保留当前E电平。
4. 四针LCD1602的5个高频踩坑:从白屏到乱码的排查手册
4.1 I2C扫描不到设备:地址问题还是接线问题
现象:执行I2C扫描程序,返回设备列表为空,或者找不到0x27/0x3F。原因可能有三:转接板供电没到位,SDA或SCL接反,或者地址就是和默认驱动不匹配。解决:先测转接板VCC引脚对地电压,确保是工作电压;然后单独跑一个I2C从设备扫描程序,把所有发现的地址打出来。如果扫描结果里有0x27以外的地址,就在驱动里改成那个地址。我还遇到过某转接板把A0跳线焊到VCC,地址变成0x2A,这种非标地址防不胜防,只信扫描结果。
// I2C扫描代码,逐个地址发探测帧 for (uint8_t addr = 0x08; addr < 0x78; addr++) { uint8_t ret = i2c_start(addr << 1); if (ret == 0) { printf("Found device at 0x%02X\n", addr); i2c_stop(); } }4.2 白屏但有背光:初始化时序没跑对
现象:屏幕亮起,背光正常,但光标处没有任何字符。原因:LCD1602的HD44780控制器没有正确进入4位模式,或者初始化序列被I2C传输速率打乱。解决:把初始化函数里的延时全部放大2倍试试,尤其是三次0x30之间的延时。我遇到过一个现象是运行两次初始化后才正常,后来加了上电后delay_ms(100)才根治。如果你用的主控I2C速率超过100kHz,建议先用i2c_set_clock(100000)把速率降下来,四针方案对时序敏感,高速下容易丢位。
4.3 显示乱码:高4位和低4位顺序搞反
现象:屏幕上出现不认识的字符,或是想显示的“A”变成奇怪的符号。原因:这类乱码基本都是在发送高4位和低4位时顺序颠倒,或者RS电平在写数据时被置低。解决:在lcd_send_nibble里打印或逻辑分析仪抓取每次写入的字节,对照value计算方式,确认高4位确实来自cmd >> 4,低4位来自cmd & 0x0F。如果RS为0时写字符,LCD会把它当命令解析,出来的自然是乱码。调试时先把所有的字符串写操作换成单个固定字符,比如写'A',看是否正常,缩小排查范围。
4.4 第一行正常第二行乱码:DDRAM地址偏移错了
现象:第一行显示完全正确,第二行要么空白要么显示的内容和第一行一样。原因:写第二行时光标定位用了0x40 + col,但忘了在命令字节加0x80标志位。解决:检查lcd_set_cursor函数,确保地址最终拼成0x80 | addr。还有一个偷懒写法是直接把第二行地址硬编码为0xC0,第一行为0x80,这样写死虽能用,但换屏幕型号可能不兼容,还是用计算方式稳妥。
4.5 字符闪烁或背景跳动:背光P3引脚干扰数据总线
现象:显示内容正常,但背光忽明忽暗,或者字符在某些操作后消失。原因:操作背光时写入了非预期值,导致数据引脚电平变化。解决:所有写入PCF8574T的字节都要带上当前的lcd_backlight状态值,不能只写数据。我习惯在lcd_send_nibble里把背光位用变量组合进去,背光控制函数单独处理,确保数据引脚和背光引脚互不干扰。如果你发现背光跳动和字符显示同步发生,多半是这里出了问题。
5. 进阶优化:四针LCD1602的刷新提速与自定义字符
5.1 通过批量写减少I2C事务次数
每个字符要4次I2C写操作,一段文本刷下来I2C总线负载不小。可以优化的是:把lcd_write_data拆成内部直接操作,不做函数嵌套,减少函数调用开销;或者用一个缓冲数组,先拼好所有要写的字节,再一次性通过I2C写入。我实测过,在100kHz I2C速率下,逐个字节写和批量写相比,批量方式在长文本下能省掉将近一半的时间。这是因为I2C每次写操作都有起始条件、地址帧、数据帧、停止条件,拆成小段后这些开销会被放大。
void lcd_print_at(uint8_t row, uint8_t col, const char *str) { lcd_set_cursor(row, col); while (*str) { uint8_t data = (uint8_t)(*str); // 高4位带E脉冲 uint8_t v = (data >> 4) << 4 | lcd_backlight | (row ? 0x80 : 0x00); // 低4位带E脉冲 str++; } }上面这段是示意,实际优化时我直接用数组把高4位和低4位对应的四个PCF8574值算好,然后按顺序I2C发送。注意第二行的定位命令也要正确加入,否则优化完字符全挤在第一行。这个优化对I2C总线上还挂了其他从设备的项目特别有用,能降低总线占用率,给传感器读取留出带宽。
5.2 自定义字符与复用非标准字符
LCD1602支持8个自定义字符,存储在CGRAM区域,地址从0x40到0x7F。四针模式下写CGRAM和写DDRAM走同一个通道,只是先要发一条设置CGRAM地址的命令。很多项目需要显示特殊符号,比如温度的小圆圈、进度条方块,都可以用自定义字符实现。需要注意的是,自定义字符一旦写入,掉电就没了,每次上电都要重新写一遍。
void lcd_create_char(uint8_t cgram_addr, uint8_t *bitmap) { lcd_write_cmd(0x40 | (cgram_addr << 3)); // 设置CGRAM基地址 for (uint8_t i = 0; i < 8; i++) { lcd_write_data(bitmap[i]); // 每行5位点阵 } }cgram_addr范围0到7,对应LCD1602的8个自定义字符位置。bitmap数组每字节只用低5位,高3位忽略。写入后想显示这个自定义字符,直接往DDRAM写的位置数字0-7即可。很多人把这里搞混,以为写0x01是显示一个字符,其实它显示的是第一个自定义图形。
5.3 I2C速率与LCD时序的平衡点
四针LCD1602的I2C速率不是越高越好。太高会导致E引脚高低电平宽度不足,LCD控制器采样失败;太低则刷新慢。我调过多次,100kHz是稳定点,400kHz也能用但要保证从设备支持快速模式。PCF8574T本身支持400kHz,但LCD1602的接口时序是瓶颈。如果你在高速下遇到偶发乱码,降低到100kHz通常能解决。反过来,如果显示刷新明显偏慢,可以考虑把时钟升到200kHz并加一点延时补偿。
| I2C速率 | E脉冲宽度 | 稳定性 | 适用场景 |
|---|---|---|---|
| 50kHz | 足够 | 极稳 | 调试排障 |
| 100kHz | 合适 | 稳定 | 默认推荐 |
| 200kHz | 偏窄 | 偶发乱码 | 需要刷新速度 |
| 400kHz | 危险 | 不稳定 | 不推荐用于LCD |
我一般把100kHz作为默认值,只有确认转接板质量好才尝试更高的速率。实际项目中,如果屏幕上需要高频动态更新,比如毫秒级数值刷新,我更建议换用带并口的屏幕或用SPI接口,而不是硬扛I2C的带宽。
6. 四针LCD1602的最终选择:什么场景用转接板,什么场景直接并行
这套方案省引脚的效果立竿见影,但它不是万能的。如果你的主控只剩一个I2C接口且还要挂多个传感器,又或者刷新率要求很高,并行接口仍是更好的选择。我做过一个测试台,每秒要更新屏幕上的多个参数,I2C方案的刷新率只能到大概几十赫兹,并行方案能到上百赫兹。反过来说,如果你的项目是数据采集器、小仪表盘、家庭自动化面板这类低频刷新场景,四针LCD1602绝对是首选。它省下的GPIO可以扩展按键矩阵、LED指示灯,或者干脆留给通信模块。从成本看,转接板比裸屏贵不了多少,但布线难度和故障排查成本降了一个量级。
我个人的习惯是:在Arduino、ESP32、STM32这些I2C资源充足的主控上,直接用四针方案;遇到引脚真的多到用不完的场合,比如某开发板自带并口LCD接口,才考虑直连。四针方案的电路结构简单,故障点多集中在连接器接触不良,只要焊接过关、地址确认无误,基本不会出问题。这句话不是玄学,是我焊过几十块转接板后的真实体感。
最后分享一个我踩了两次的教训:换主控平台时,驱动代码里的I2C读操作要特别小心,PCF8574T是准双向IO,读引脚前要先写1。我曾在某平台上用标准I2C库读转接板引脚,结果读回来的数据全是0,花了大半天排查发现是读流程里少了上拉写操作。这类经验记不住,只能靠多掉坑。希望帮到你。
本文还有配套的精品资源,点击获取