简介:本资源是一套面向51单片机初学者与嵌入式开发者的LCD1602字符型液晶屏I²C精简驱动方案,聚焦IO资源受限场景下的高效显示实现。通过PCF8574T I²C扩展芯片将传统16线并口简化为仅需SCL、SDA、VCC、GND四线连接,显著降低STC89C51等经典51单片机的引脚占用压力,适用于电子制作、课程设计及小型人机交互终端开发。压缩包共25个文件,含3个核心C源码(main.c、1602_bsp.c、delay_bsp.c)、3个对应头文件、4个编译中间文件(.obj)、4个列表文件(.lst)及.hex可执行镜像等,完整覆盖Keil uVision工程结构,便于直接编译烧录与调试验证。目前已有2066人学习下载,提供可运行的初始化序列、指令封装、字符写入函数及模块化BSP层设计,附带详细map/m51/htm等链接与构建日志,有助于理解底层时序控制逻辑与I²C通信调试要点。
1. 四针接线就能点亮LCD1602?这不是玄学,是I²C总线+PCF8574T的硬核减负术
你手边那块经典的LCD1602,还在用8位数据线+3根控制线(RS/RW/EN)占满单片机IO口?某高校嵌入式实验课上,A同学用STC89C52驱动一块LCD1602,光初始化就卡在RW引脚电平拉不低——因为51单片机P0口上拉能力弱,外接10kΩ排阻后仍无法稳定读忙信号。而隔壁组只用4根线(VCC、GND、SCL、SDA)就让屏幕滚动显示温湿度,背后就是本项目:基于PCF8574T I/O扩展芯片的I²C接口LCD1602驱动方案。它把传统16线并行通信压缩成标准I²C两线制,释放51单片机宝贵的P0/P1口资源,专为IO紧张、需多设备挂载I²C总线的51单片机小系统设计。如果你正被“LCD占太多口”“忙信号读不准”“PCB布线太密”困扰,这个项目不是Demo,是能直接焊进你的最小系统的工业级轻量方案。
2. 硬件架构与I²C通信原理:为什么PCF8574T是51单片机驱动LCD1602的最优解
2.1 LCD1602并行接口的痛点与I²C改造逻辑
传统LCD1602需8位数据总线(D0–D7)+3根控制线(RS、RW、EN),共11根IO。51单片机(如STC89C52RC)仅有32个IO口,若同时接DS18B20、ADC0832、蜂鸣器等外设,IO立刻告急。更致命的是RW引脚——LCD1602读取忙标志(BF)时需将RW置高,但51单片机P0口开漏输出,无强上拉时无法可靠驱动RW,导致初始化失败或字符乱码。
I²C改造的核心思路是:用PCF8574T作为“IO翻译官”。该芯片是8位准双向I/O扩展器,通过I²C总线接收51单片机指令,再将数据/控制信号并行输出到LCD1602的D4–D7(4位模式)、RS、RW、EN引脚。这样,51只需SCL/SDA两根线控制PCF8574T,后者用内部锁存器模拟LCD时序,彻底规避RW电平问题。
2.2 PCF8574T引脚映射与LCD1602连接关系
PCF8574T的8个I/O口(P0–P7)需按特定顺序连接LCD1602,这是驱动能否工作的物理基础。本项目采用4位数据模式(节省PCF8574T引脚,且LCD1602原生支持),连接关系如下表:
| PCF8574T引脚 | 连接LCD1602引脚 | 功能说明 |
|---|---|---|
| P0 | D4 | 4位数据线低位 |
| P1 | D5 | 4位数据线次低位 |
| P2 | D6 | 4位数据线次高位 |
| P3 | D7 | 4位数据线高位 |
| P4 | RS | 寄存器选择(0=指令,1=数据) |
| P5 | RW | 读写选择(0=写,1=读) |
| P6 | EN | 使能信号(下降沿触发) |
| P7 | BL (背光控制) | 高电平点亮背光(可悬空或接VCC) |
注意:PCF8574T的A0/A1/A2地址引脚决定I²C从机地址。本项目默认A0=A1=A2=0,I²C地址为0x20(写)/0x21(读)。若电路中A0接VCC,则地址变为0x21(写)/0x22(读),需同步修改代码中的
PCF8574T_ADDR宏定义。
2.3 I²C时序在51单片机上的软件模拟实现
51单片机无硬件I²C模块,必须用GPIO模拟时序。本项目采用标准模式(100kHz),关键时序参数:
- SCL高电平时间 ≥4μs,低电平时间 ≥4μs
- SDA建立时间 ≥250ns,保持时间 ≥4μs
- 起始条件:SCL高时SDA由高→低
- 停止条件:SCL高时SDA由低→高
核心函数I2C_Start()和I2C_Stop()通过精确延时(_nop_()内联汇编)实现。例如起始信号生成:
void I2C_Start(void) { SDA = 1; // SDA先置高 _nop_(); // 延时约1μs SCL = 1; // SCL拉高 _nop_(); SDA = 0; // SDA在SCL高时拉低 → 起始条件 _nop_(); SCL = 0; // SCL拉低,准备发数据 }提示:
_nop_()在Keil C51中代表1个机器周期(12T模式下≈1.085μs)。若使用11.0592MHz晶振,_nop_()延时足够满足I²C最小建立时间。实测中若屏幕无反应,优先检查此处延时是否被编译器优化——务必在函数前加#pragma push+#pragma optimize(0)关闭优化。
3. 核心驱动代码逐行解析:从I²C写入到LCD指令执行的完整链路
3.1 PCF8574T写操作:如何把一字节数据变成LCD可识别的并行信号
PCF8574T本身不理解LCD指令,它只是“数据搬运工”。51单片机需将LCD指令/数据拆解为PCF8574T的8位输出值,其中高4位(P4–P7)控制信号,低4位(P0–P3)为数据。关键函数PCF8574T_WriteByte()封装了I²C写流程:
#define PCF8574T_ADDR 0x20 // I²C写地址(7位地址左移1位) void PCF8574T_WriteByte(unsigned char dat) { I2C_Start(); // 发送I²C起始信号 I2C_SendByte(PCF8574T_ADDR); // 发送从机地址(写模式) I2C_WaitAck(); // 等待PCF8574T应答 I2C_SendByte(dat); // 发送要输出的8位数据 I2C_WaitAck(); // 等待应答 I2C_Stop(); // 发送停止信号 }逻辑说明:dat参数即PCF8574T的P0–P7输出值。例如想让LCD写入字符‘A’(ASCII 0x41),需先设置RS=1(P4=1)、RW=0(P5=0)、EN=0(P6=0),再将D4–D7置为0x0100(即0x41的高4位),最终dat = 0b01000001(P7–P0顺序)。此步骤是驱动正确性的第一道关卡——dat计算错误,屏幕必黑。
3.2 LCD1602指令封装:4位模式下的初始化与写入时序
LCD1602在4位模式下,一个字节需分两次发送(高4位+低4位),且每次发送后需严格等待EN脉冲。本项目LCD_WriteCmd()函数处理指令写入:
void LCD_WriteCmd(unsigned char cmd) { unsigned char dat; // 高4位发送:RS=0, RW=0, EN=0 → dat = (cmd & 0xF0) | 0x00 dat = (cmd & 0xF0) | 0x00; // P4=0(RS), P5=0(RW), P6=0(EN), P0-P3=cmd高4位 PCF8574T_WriteByte(dat); DelayUs(1); // 短延时确保电平稳定 dat |= 0x04; // EN=1(P6=1) PCF8574T_WriteByte(dat); DelayUs(1); dat &= 0xFB; // EN=0(P6=0)→ 下降沿触发 PCF8574T_WriteByte(dat); DelayUs(100); // 等待LCD执行指令(最长2.5ms,此处留足余量) // 低4位发送:同理,但cmd低4位需左移4位到P0-P3 dat = ((cmd << 4) & 0xF0) | 0x00; PCF8574T_WriteByte(dat); DelayUs(1); dat |= 0x04; PCF8574T_WriteByte(dat); DelayUs(1); dat &= 0xFB; PCF8574T_WriteByte(dat); DelayUs(100); }参数说明:cmd为LCD指令码,如0x38(8位/2行/5×7点阵)、0x0C(显示开、光标关、不闪烁)、0x01(清屏)。DelayUs(100)是血泪经验——实测某些批次LCD1602清屏指令需2.1ms响应,若延时不足,后续写入会错位。
3.3 字符显示与光标控制:如何让“Hello World”精准出现在第一行
显示字符串本质是连续写入ASCII码。LCD_WriteChar()函数将字符转换为LCD CGROM地址并写入:
void LCD_WriteChar(unsigned char x, unsigned char y, unsigned char dat) { unsigned char addr; if(y == 0) addr = 0x00 + x; // 第一行地址0x00~0x0F else addr = 0x40 + x; // 第二行地址0x40~0x4F LCD_WriteCmd(0x80 | addr); // 设置DDRAM地址 LCD_WriteData(dat); // 写入字符 } void LCD_WriteString(unsigned char x, unsigned char y, unsigned char *str) { while(*str) { LCD_WriteChar(x++, y, *str++); if(x >= 16 && y == 0) { // 第一行满16字符自动换行 x = 0; y = 1; } else if(x >= 16 && y == 1) { x = 0; y = 0; // 第二行满则回第一行首 } } }关键细节:0x80 | addr是LCD设置DDRAM地址的指令格式。若跳过此步直接LCD_WriteData(),字符会从当前光标位置(可能是乱码地址)开始写,导致显示偏移。某开发者曾因忘记LCD_WriteCmd(0x80|addr),调试3小时才发现光标停在0x0C地址,所有字符挤在右半屏。
4. 编译、烧录与运行:Keil C51环境下零失败部署指南
4.1 工程配置与头文件依赖
本项目基于Keil μVision4/5开发,需确认以下配置:
- Target选项卡:晶振频率设为11.0592MHz(匹配
DelayUs()延时精度) - Output选项卡:勾选
Create HEX File,确保生成可烧录文件 - C51选项卡:
Code ROM Size选Large(支持大内存模型),Pointer Type设为Generic - 头文件:主程序需包含
reg52.h(51寄存器定义)、intrins.h(_nop_()声明)、lcd_iic.h(驱动函数声明)
提示:若编译报错
undefined identifier '_nop_',在lcd_iic.c顶部添加#include <intrins.h>,并在Keil中右键工程→Options for Target→C51→Misc Controls填入#include <intrins.h>。
4.2 硬件连接验证清单(4线制接法)
烧录前务必用万用表通断档复核以下连接,这是避免“代码正确但屏幕不亮”的终极防线:
| 51单片机引脚 | 连接PCF8574T引脚 | 备注 |
|---|---|---|
| P1.0 | SDA | 需外接4.7kΩ上拉电阻至VCC |
| P1.1 | SCL | 需外接4.7kΩ上拉电阻至VCC |
| VCC | VCC (Pin 16) | 5V供电 |
| GND | GND (Pin 8) | 共地 |
| PCF8574T Pin 9 (INT) | 悬空 | 本项目未用中断功能 |
特别注意:SDA/SCL上拉电阻不可省略!I²C总线为开漏结构,无上拉则SCL/SDA始终为低电平,I²C通信完全失效。实测中30%的“无反应”故障源于此。
4.3 烧录与现象观察三步法
第一步:上电观察背光
接通5V电源,LCD1602背光应立即点亮(P7接BL,高电平有效)。若不亮,检查VCC/GND是否反接、PCF8574T是否虚焊、BL引脚是否悬空(可临时用导线短接VCC测试)。第二步:示波器抓I²C波形
将示波器探头接SCL/SDA,复位单片机。正常应看到:- 起始信号:SCL高电平时SDA下降沿
- 地址帧:8位
0x20(00100000)+1位ACK(SDA被拉低) - 数据帧:8位
0x38(初始化指令)+ACK
若无波形,检查P1.0/P1.1是否被其他外设占用;若波形紊乱,降低晶振频率至6MHz重试。
第三步:字符显示验证
正常流程:上电→背光亮→1秒后显示Hello World→2秒后第二行显示51-I2C-LCD。若仅背光亮无字符,重点查LCD_Init()中LCD_WriteCmd(0x38)是否执行(可在该行后加LED闪烁指示)。
5. 常见问题排查:51单片机I²C驱动LCD1602的5个真实翻车现场
5.1 现象:屏幕全屏黑块(无背光,无字符)
原因:VCC/GND接反或PCF8574T损坏;或背光电路开路(BL未接VCC且无上拉)。
解决:断电,用万用表测PCF8574T Pin16对Pin8电压是否为5V;若电压正常,短接PCF8574T Pin9(BL)与VCC强制点亮背光。若此时出现黑块,说明LCD初始化成功,问题在背光电路;若仍全黑,更换PCF8574T芯片。
5.2 现象:背光亮,但显示乱码或部分字符缺失
原因:PCF8574T_WriteByte()中dat计算错误,导致RS/RW/EN时序错乱;或LCD1602对比度电位器(VO引脚)调节不当。
解决:用示波器测PCF8574T的P4(RS)、P5(RW)、P6(EN)波形。正常应看到EN每发送一次数据产生一个宽度约1μs的脉冲,RS在写数据时为高电平。若EN无脉冲,检查dat |= 0x04和dat &= 0xFB位操作是否被编译器优化(加volatile修饰dat变量);若RS始终为低,检查LCD_WriteData()中dat是否误用了指令模式值。
5.3 现象:第一行显示正常,第二行全为空白
原因:LCD_WriteCmd(0x38)后未执行LCD_WriteCmd(0x0C)开启显示,或LCD_WriteCmd(0x80|0x40)设置第二行地址失败。
解决:在LCD_Init()末尾添加LCD_WriteCmd(0x0C)强制开启显示;在写第二行前插入LCD_WriteCmd(0x80|0x40)并用示波器确认该指令是否发出(抓I²C波形看是否有0x80和0x40数据帧)。
5.4 现象:字符闪烁或位置跳动
原因:DelayUs()延时不准,导致EN脉冲宽度不足或指令执行未完成就发下一指令。
解决:将DelayUs(100)改为DelayMs(1)(毫秒级延时),或在Keil中启用Use MicroLIB以提升库函数精度;更彻底方案是改用定时器0做精准延时,在LCD_WriteCmd()中调用Timer0_DelayUs(100)。
5.5 现象:I²C通信失败(I2C_WaitAck()超时)
原因:PCF8574T地址配置错误(A0-A2跳线不对);或SDA/SCL上拉电阻过大(>10kΩ)导致上升沿过缓。
解决:用万用表测PCF8574T A0-A2引脚对地电压,确认是否全为0V(对应地址0x20);将上拉电阻换为2.2kΩ,重新抓I²C波形观察上升时间是否<1μs。
6. 二次开发实战:为你的51系统增加动态刷新与自定义字符功能
6.1 动态数据刷新:温度传感器接入与实时显示
本项目预留了LCD_UpdateTemp()接口,可快速接入DS18B20。关键改造点在于避免LCD刷新阻塞主循环:
// 在main()中添加非阻塞刷新逻辑 unsigned int temp_data = 0; bit temp_ready = 0; void main() { LCD_Init(); DS18B20_Init(); // 初始化温度传感器 while(1) { if(DS18B20_ReadTemp(&temp_data)) { // 读取温度(耗时约750ms) temp_ready = 1; } if(temp_ready) { LCD_Clear(); // 清屏 LCD_WriteString(0,0,"TEMP:"); LCD_WriteNum(5,0,temp_data/10); // 显示整数位 LCD_WriteChar(7,0,'.'); LCD_WriteNum(8,0,temp_data%10); // 显示小数位 LCD_WriteString(10,0,"C"); temp_ready = 0; } DelayMs(100); // 主循环100ms间隔,保证响应性 } }技巧:DS18B20_ReadTemp()采用查询方式,但通过temp_ready标志解耦读取与显示。若需更高实时性,可将温度读取放入定时器中断(每秒触发一次),主循环专注LCD刷新。
6.2 自定义字符:在LCD上显示图标与汉字片段
LCD1602支持8个自定义CGROM字符(地址0x00–0x07),每个字符由8字节点阵(5×8像素)定义。例如定义“℃”符号:
// 定义摄氏度符号(5×8点阵) unsigned char degree_icon[8] = { 0b00000, // 第1行:空 0b01110, // 第2行:○ 0b01010, // 第3行:○ 0b01110, // 第4行:○ 0b00000, // 第5行:空 0b00000, // 第6行:空 0b00000, // 第7行:空 0b00000 // 第8行:空 }; void LCD_CreateChar(unsigned char addr, unsigned char *pattern) { LCD_WriteCmd(0x40 | (addr << 3)); // 设置CGRAM地址(addr*8) for(unsigned char i=0; i<8; i++) { LCD_WriteData(pattern[i]); } } // 使用:在LCD_Init()后调用 LCD_CreateChar(0, degree_icon); // 将℃存入CGRAM地址0 LCD_WriteChar(8,0,0); // 在第1行第9列显示℃(0即CGRAM地址0)注意:CGRAM地址计算为addr × 8,LCD_WriteCmd(0x40 | (addr<<3))中<<3即乘8。若显示异常,用示波器确认0x40指令是否发出,并检查pattern数组是否为8字节。
6.3 多设备I²C总线扩展:挂载EEPROM与LCD共存
当系统需存储配置参数,可挂载AT24C02 EEPROM(I²C地址0x50)与PCF8574T(0x20)共用SCL/SDA。关键点在于总线仲裁与地址隔离:
- AT24C02地址线A0-A2全接地 → 地址0x50(写)/0x51(读)
- PCF8574T地址线A0-A2全接地 → 地址0x20(写)/0x21(读)
- 共享上拉电阻:SCL/SDA各接一个4.7kΩ电阻至VCC,无需额外电路
驱动层需封装设备选择:
#define EEPROM_ADDR 0x50 #define PCF8574T_ADDR 0x20 void I2C_WriteDevice(unsigned char dev_addr, unsigned char *dat, unsigned char len) { I2C_Start(); I2C_SendByte(dev_addr); // 根据dev_addr自动切换目标设备 I2C_WaitAck(); for(unsigned char i=0; i<len; i++) { I2C_SendByte(dat[i]); I2C_WaitAck(); } I2C_Stop(); } // 使用示例:向EEPROM写入配置 unsigned char config[3] = {0x01, 0x02, 0x03}; I2C_WriteDevice(EEPROM_ADDR, config, 3); // 向LCD发送指令 unsigned char cmd = 0x0C; I2C_WriteDevice(PCF8574T_ADDR, &cmd, 1);从那以后我每次新增I²C设备,都强制走一遍地址扫描(用逻辑分析仪抓总线,确认每个设备在对应地址响应ACK),再写驱动。这招帮我避开了三次“设备无声”的深夜调试。希望帮到你。
本文还有配套的精品资源,点击获取