1. 项目概述:当IO口不够用时,PCF8574如何成为1602液晶的“救星”
玩单片机开发的朋友,尤其是用51或者STM32这类IO资源不算特别宽裕的MCU时,经常会遇到一个头疼的问题:想接个液晶屏显示点信息,结果发现IO口被占得差不多了。经典的1602字符液晶,虽然显示内容简单,但它需要至少6个IO口(4位数据模式)甚至11个IO口(8位数据模式)才能驱动。这对于一个只有几十个IO的MCU来说,尤其是在项目已经接了不少传感器、按键之后,无疑是个沉重的负担。我当时做一个小型数据采集器就卡在了这里,主控的IO所剩无几,但显示功能又必不可少。
上网搜了一圈解决方案,发现除了直接用带更多IO口的MCU(成本高、板子得重画),更常见的思路是使用IO扩展芯片。在众多方案中,PCF8574这款通过IIC总线扩展8位IO的芯片吸引了我的注意。IIC总线只需要两根线(SDA和SCL)就能挂载多个设备,理论上用两个IO口就能换来8个,简直是“空手套白狼”的好买卖。但实际动手把PCF8574和1602液晶接起来,让代码跑通,这个过程远没有想象中顺利,我前后折腾了三四天,踩遍了从芯片选型、地址设置到程序时序的几乎所有“坑”。今天就把这段经历和最终验证成功的方案整理出来,重点不只是给你看代码,而是告诉你每一步背后的原理和那些容易翻车的地方。
2. 核心思路解析:为什么是PCF8574+IIC?
在深入代码之前,我们得先搞清楚两个核心问题:为什么选择PCF8574?以及IIC总线驱动1602的基本逻辑是什么?这决定了整个方案的底层稳定性。
2.1 PCF8574芯片的定位与优势
PCF8574是NXP(恩智浦)生产的一款远程8位I/O扩展器,它通过IIC总线与主控制器通信。你可以把它理解成一个“带网络接口的8位锁存器”。主控MCU通过IIC发送一个字节的数据,这个字节的8个bit就会锁存在PCF8574的8个输出引脚上;反之,MCU也能读取这8个引脚的电平状态。
它的核心优势在于:
- 极简的接口:仅需两根线(SCL时钟线、SDA数据线)和电源,就能扩展出8个双向IO口。这极大地节省了主控MCU的引脚资源。
- 驱动能力强:PCF8574每个引脚的拉电流能力可达25mA,整个芯片的驱动电流总和高达200mA。这意味着它可以直接驱动LED、小型继电器等,而1602液晶的数据/控制线所需的电流很小,对它来说绰绰有余。
- 地址可配置:通过芯片上的A0, A1, A2三个地址引脚接高电平(VCC)或低电平(GND),可以设置不同的IIC设备地址。这允许你在同一条IIC总线上挂载最多8个PCF8574(或PCF8574A),扩展出64个IO口。
- 中断输出:PCF8574提供了一个开漏的中断引脚(INT),当输入状态发生变化时,可以通知MCU,实现事件驱动的输入检测,这在扩展按键矩阵时非常有用。不过在本项目中驱动1602纯输出,这个功能暂未用到。
注意:市面上常见的有PCF8574和PCF8574A两种型号。它们功能完全一样,但默认的IIC设备地址不同!PCF8574的固定地址部分是
0100,而PCF8574A是0111。这是一个巨大的坑,很多朋友调不通就是因为地址写错了。购买和设计电路时一定要看清型号。
2.2 1602液晶的驱动原理与IO需求
1602液晶模块内部有一个HD44780或兼容的控制器。我们与它的通信,本质上是向这个控制器发送命令和数据。通信模式主要有8位并行和4位并行两种。
- 8位模式:需要8根数据线(D0-D7),外加3根控制线(RS, RW, EN),共11根线。
- 4位模式:使用数据线的高4位(D4-D7),分两次发送一个字节,需要4根数据线加3根控制线,共7根线。
为了进一步节省IO,我们通常会将RW(读/写选择)引脚直接接地,因为我们只向液晶写数据,不读取其忙碌状态。这样就变成了6根线(4位数据模式)或10根线(8位数据模式)。
PCF8574驱动1602的核心思想:用PCF8574的8个IO口(P0-P7)来模拟这6根或更多根线,然后通过IIC总线,用MCU的两个IO口去控制PCF8574的8个IO口,从而间接控制1602。这相当于在MCU和1602之间加了一个“翻译官”和“缓冲器”。
2.3 硬件连接方案设计
我采用的是最常见的4位数据模式、只写不读的连接方式,这也是最节省IO且稳定的方式。具体连接如下表所示:
| PCF8574 引脚 | 连接至 1602 引脚 | 功能说明 |
|---|---|---|
| P0 | RS (数据/命令选择) | 高电平:数据;低电平:命令 |
| P1 | RW (读/写选择) | 直接接地,因为我们只写不读。注意:PCF8574的P1虽然连接了,但在程序中我们始终将其置为低电平(0)。 |
| P2 | EN (使能信号) | 高脉冲有效,用于锁存数据 |
| P3 | 背光控制 (可选) | 通过一个三极管或直接控制背光LED的阳极(如果背光由模块上的限流电阻决定,此引脚可悬空或接固定电平) |
| P4 | D4 (数据位4) | 4位数据模式的高半字节最低位 |
| P5 | D5 (数据位5) | |
| P6 | D6 (数据位6) | |
| P7 | D7 (数据位7) | 4位数据模式的高半字节最高位 |
硬件搭建注意事项:
- 上拉电阻:IIC总线(SDA, SCL)必须接上拉电阻,通常阻值在4.7kΩ到10kΩ之间,接至VCC。这是IIC总线规范要求的,否则信号无法被正确识别为高电平。
- 电源去耦:在PCF8574的VCC和GND引脚附近,务必放置一个0.1uF的陶瓷电容,用于滤除高频噪声,保证芯片稳定工作。
- 地址引脚:根据你使用的芯片型号(PCF8574或PCF8574A)以及A0,A1,A2的接法(接地或接VCC),计算出正确的7位IIC设备地址。例如,如果使用PCF8574A,且A2,A1,A0都接地,那么7位地址就是
0111 000(二进制),即0x70(8位写地址,最低位为0)。这是最容易出错的一步! - 1602的VO引脚:这是对比度调节引脚,通常接一个10kΩ的可调电阻中间抽头,通过调节电阻来改变电压(0-VCC),从而获得清晰的显示效果。如果直接接地,对比度可能过高,显示全黑方块。
3. 软件驱动层:从IIC时序到液晶指令
理解了硬件连接,软件部分就是如何用代码精确地模拟出通信时序。整个过程分为三层:最底层是IIC总线时序模拟,中间层是PCF8574字节写入函数,最上层是1602液晶的指令/数据发送函数。
3.1 IIC总线时序的精确模拟
很多MCU有硬件IIC外设,但在51单片机或一些简单应用中,用GPIO模拟(软件IIC)更常见,也更容易理解原理。IIC通信有几个关键信号必须严格模拟:
- 起始条件(S):在SCL为高电平期间,SDA产生一个下降沿。
- 停止条件(P):在SCL为高电平期间,SDA产生一个上升沿。
- 数据有效性:在SCL为高电平期间,SDA必须保持稳定。数据的变化只能发生在SCL为低电平期间。
- 应答(ACK):发送器每发送完一个字节(8位),在第9个时钟脉冲期间释放SDA线(置高),接收器需要将SDA拉低,作为应答信号。
在我的代码中,Start_8574(),Stop_8574(),IIC_WriteByte()函数就负责实现这些时序。其中_nop_()函数(空操作)产生的短暂延时至关重要,它保证了信号边沿之间有足够的建立和保持时间。不同MCU的主频不同,这些_nop_()的个数可能需要微调。
// 起始信号:SCL高电平期间,SDA从高变低 void Start_8574(void) { SDA = 1; _nop_(); SCL = 1; _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); // 延时,确保SCL稳定在高电平 SDA = 0; // 产生下降沿 _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); SCL = 0; // 将SCL拉低,为后续发送数据做准备 }3.2 整合数据:构造发送给PCF8574的字节
这是整个驱动逻辑中最关键的一步。我们需要根据1602当前需要的状态(是发命令还是数据?数据内容是什么?),构造一个8位的字节(data1),然后通过Puts_8574函数发送给PCF8574。
这个8位字节(data1)的每一位对应PCF8574的一个输出引脚(P7-P0)。根据我们的硬件连接表:
bit0-> P0 -> RSbit1-> P1 -> RW (恒为0)bit2-> P2 -> ENbit3-> P3 -> 背光 (可以置1点亮)bit4-> P4 -> D4bit5-> P5 -> D5bit6-> P6 -> D6bit7-> P7 -> D7
例如,我们要通过4位模式发送一个高4位为0101(即0x5)的数据,并且这是一条指令(RS=0),那么过程如下:
- 先发送高4位:我们需要让P7-P4输出
0101,同时RS=0, RW=0, EN=0。所以构造的字节是:0101 0000(高四位是数据,低四位是控制),即0x50。 - 然后,我们需要产生一个EN使能脉冲:将EN位(bit2)先置1,再置0。所以先发送
0101 0100(EN=1),即0x54,短暂延时后,再发送0101 0000(EN=0),即0x50。
在4位模式下,发送一个完整的字节需要这样操作两次(先高4位,后低4位)。我的代码里WriteInstruc和WriteData函数已经封装了这些细节。
3.3 1602的初始化与关键延时
1602上电后必须进行一系列正确的初始化操作才能进入工作状态。对于4位模式,初始化序列有严格的要求。我的InitLCD()函数完成了这个序列:
- 等待液晶内部复位完成:上电后延时至少40ms。
delay1(50)确保了这一点。 - 功能设置:发送指令
0x3c(二进制0011 1100)。这里解释一下:0011是功能设置命令,1表示8位数据线(这里是伪8位,实际我们只用高4位),1表示两行显示,0x表示5x10点阵(这里通常用0表示5x7点阵,可能是代码笔误或特定屏的设定,标准5x7点阵应为0x38的4位模式形式)。这里有个坑:很多资料和代码初始化时发送的是0x28(4位模式,两行,5x7点阵)。我最初用0x28没成功,后来参考的代码用了0x3c反而可以了。这可能与我使用的具体液晶模块有关,建议如果0x3c不行,可以尝试标准的0x28初始化序列。 - 显示开关控制:发送指令
0x0c,打开显示,关闭光标。 - 清屏:发送指令
0x01。
实操心得:那个“救命的”长延时:这是我踩过最大的坑,也是本文最想分享的经验。在
main函数的while(1)循环里,我在调用显示函数DispCharacter之前,加了一个delay1(500)(约500ms)。这个延时至关重要!原因在于,PCF8574通过IIC总线操作,其速度相比直接GPIO操作慢得多。1602液晶在执行完一条指令(如清屏、移动光标、写入数据)后,需要一定的时间来处理(称为“忙”状态)。在直接GPIO驱动时,我们通常通过读取“忙标志位”来等待。但在本方案中,RW引脚被接地,我们无法读取忙状态。如果MCU发送指令的速度快于1602处理的速度,后续的指令就会丢失或出错,导致显示乱码、不显示或全黑。这个长延时,就是牺牲一点响应速度,换取通信的绝对可靠。你可以根据实际情况调整这个延时,比如降到100ms试试,但必须有。
4. 完整代码分析与逐行解读
结合上面的原理,我们来详细剖析一下提供的代码,理解每一部分的作用和潜在优化点。
#include <reg52.h> // 51芯片头文件,定义特殊功能寄存器 #include <intrins.h> // 包含_nop_()函数 #define uchar unsigned char #define uint unsigned int #define SetDDRAM(Address) WriteInstruc(0x80|Address) // 设置DDRAM地址的宏 // 定义IIC和1602控制引脚(实际连接至PCF8574,再由PCF8574控制1602) sbit SDA = P1^1; // IIC数据线 sbit SCL = P1^0; // IIC时钟线 // 以下两个引脚定义在代码中并未直接用于GPIO操作,而是通过PCF8574控制 // 它们在这里定义可能用于逻辑理解,实际输出由PCF8574的P0和P2位控制 sbit RS = P1^3; // 对应PCF8574的P0 sbit EN = P1^2; // 对应PCF8574的P2 // 毫秒级延时函数,基于_nop_()实现 void delay1(int ms) { unsigned char y; while(ms--) { for(y = 0; y<250; y++) { _nop_(); _nop_(); _nop_(); _nop_(); } } } // ... 后续是IIC起始、停止、写字节等函数,上文已分析 ...关键函数解析:
Puts_8574(uchar SlaveAddr, uchar data1):这是与PCF8574通信的核心函数。它接收一个从机地址SlaveAddr和一个数据字节data1。函数内部依次产生起始信号、发送地址字节、发送数据字节、产生停止信号。注意,这里的SlaveAddr是8位的写地址,即(7位设备地址 << 1) | 0。例如,对于PCF8574A(地址0111),且A2A1A0=000,那么7位地址是0x38(二进制0111000),8位写地址就是0x70(0x38<<1)。代码中直接使用了0x70。WriteInstruc(uint temp)和WriteData(uint data1):这两个函数是驱动1602的接口。它们根据是命令(RS=0)还是数据(RS=1),构造不同的控制位,并调用Puts_8574发送出去。函数内部包含了产生EN使能脉冲的步骤:先置EN=0,发送数据,然后置EN=1,短暂延时,再置EN=0。这个脉冲的宽度(延时_nop_()的次数)需要满足1602的数据建立时间要求。DispCharacter(uint x, uint y, uint data1):这是面向应用的显示函数。x是行号(0或1),y是列号(0-15)。函数内部先将行列号转换为1602控制器要求的DDRAM地址(第一行基址0x80,第二行基址0xC0),然后调用WriteData发送要显示的字符编码。main()函数:程序入口。首先调用InitLCD()初始化液晶。然后进入死循环,在每次循环显示“welcome”单词前,先进行一个delay1(500)的长延时。正如前文所述,这个延时是保证显示稳定的关键。如果没有它,MCU会以极快的速度反复刷新显示,1602根本来不及处理,导致显示异常。
5. 调试血泪史:常见问题与排查指南
我的调试过程堪称一部“血泪史”,几乎遇到了所有典型问题。这里总结出来,希望你能绕过这些坑。
5.1 问题一:液晶完全无任何显示,背光也不亮
- 排查步骤:
- 检查电源:用万用表测量1602的VCC和GND引脚是否有5V(或3.3V,视模块而定)电压。背光引脚(LED+和LED-)电压是否正确。
- 检查背光:如果背光不亮,单独给背光引脚供电(通常LED+接VCC,LED-通过一个限流电阻接地),看是否点亮。可能是背光损坏或接线错误。
- 检查PCF8574:用万用表测量PCF8574的8个输出引脚。在程序运行时,这些引脚的电平应该会变化。如果全部为高或全部为低且不变,说明IIC通信可能没成功。重点检查:
- IIC总线的上拉电阻是否接好?
- SDA和SCL线是否接反?
- PCF8574的电源和地是否接好?
- 芯片型号和地址:这是最最最常见的坑!我一开始用的
0x40(PCF8574的地址)没反应,后来发现我买的是PCF8574A,地址是0x70。用0x70一试,PCF8574的输出引脚就开始跳变了。
5.2 问题二:液晶背光亮,但显示全黑方块(或第一行有黑影)
- 排查步骤:
- 调节对比度:立即调节连接在VO引脚上的可调电阻。全黑方块通常是对比度过高,向降低电压的方向调节(如果VO接的是电阻分压,则调节中间抽头向GND方向)。
- 检查初始化序列:对比度调了也没用,大概率是初始化没成功。1602没有正确进入4位工作模式。仔细核对
InitLCD()函数中发送的指令序列和延时。可以尝试替换成更标准的4位初始化序列:void InitLCD() { delay1(50); // 尝试发送三次0x28,确保进入4位模式 WriteInstruc(0x30); delay1(5); WriteInstruc(0x30); delay1(1); WriteInstruc(0x30); delay1(1); WriteInstruc(0x20); delay1(1); // 实际发送0x20,但4位模式只认高4位0x2,功能设置指令 // 正式功能设置:4位,两行,5x7点阵 WriteInstruc(0x28); delay1(1); WriteInstruc(0x0c); // 显示开,关光标 WriteInstruc(0x06); // 光标右移,字符不移 WriteInstruc(0x01); // 清屏 delay1(2); } - 检查EN使能脉冲:用示波器或逻辑分析仪观察PCF8574的P2(EN)引脚。在每次
WriteInstruc或WriteData时,应该能看到一个清晰的高电平脉冲。如果脉冲宽度太窄(_nop_()个数太少),1602可能无法锁存数据。
5.3 问题三:显示乱码,或字符出现在错误位置
- 排查步骤:
- 检查数据位连接:确认PCF8574的P4-P7(D4-D7)与1602的D4-D7是否一一对应,没有接错、接反。
- 检查通信时序:IIC的时序是否太紧张?尝试增加
IIC_WriteByte函数中_nop_()的数量,降低通信速度。MCU主频太高可能导致软件IIC时序不符合要求。 - 检查“忙”等待问题:这就是我遇到的核心问题。在
main循环中,务必在连续进行显示操作之间加入足够的延时。比如我加的delay1(500)。你可以先给一个很长的延时(如1秒),如果能正常显示,再逐步缩短,找到稳定工作的最小延时。 - 检查DDRAM地址设置:
DispCharacter函数中地址计算是否正确?第一行地址范围0x00-0x27,第二行0x40-0x67。但1602只显示前16个字符。确保你的行列号计算正确。
5.4 问题四:显示内容闪烁或不稳定
- 排查步骤:
- 电源噪声:在PCF8574和1602的VCC引脚附近增加一个10uF的电解电容并联一个0.1uF的陶瓷电容,进行电源滤波。
- 总线干扰:确保IIC总线走线不要太长,并且远离高频噪声源。
- 程序逻辑:避免在中断服务程序中操作液晶显示,这可能会打断正常的通信时序。如果非要这么做,需要做好临界区保护。
6. 方案优化与扩展思考
经过以上步骤,你的1602应该已经能够稳定显示了。但这个基础方案还有优化和扩展的空间。
6.1 优化一:实现“忙”检测(无需长延时)
长延时虽然简单可靠,但效率低下,且会阻塞MCU。一个更专业的做法是利用PCF8574的输入功能,实现1602的“忙”状态检测。这需要改变硬件连接:将1602的RW引脚不再接地,而是连接到PCF8574的某个IO口(例如P1),并将该IO口配置为输入模式。然后,在发送每条指令/数据前,先读取1602的状态寄存器(最高位BF是忙标志),等待BF为0后再继续操作。
这需要修改代码,使PCF8574的P1引脚在需要读忙时变为输入(向PCF8574写入相应位为高阻态),并实现IIC的读字节函数。复杂度增加,但通信效率最高。
6.2 优化二:使用硬件IIC
如果你的MCU(如STM32、ESP32等)支持硬件IIC,强烈建议使用硬件IIC库来驱动PCF8574。硬件IIC由控制器处理时序,更稳定、速度更快,且不占用CPU时间。你只需要调用HAL_I2C_Master_Transmit这样的函数发送数据即可,底层时序无需操心。
6.3 扩展:驱动多个1602或其它设备
PCF8574的地址可配置特性使得驱动多个1602成为可能。例如,你可以将两个PCF8574的A0引脚分别接GND和VCC,赋予它们不同的地址(如0x70和0x72)。然后用两套控制线分别连接两个1602。MCU通过切换IIC地址,就可以分别控制两个屏幕。
更进一步,PCF8574的8个IO口除了驱动1602,剩下的口(比如我们只用了6个)还可以用来接按键、LED等。你可以设计一个复合功能模块,用一颗PCF8574同时管理显示和输入输出。
折腾PCF8574驱动1602的过程,虽然一开始各种不顺利,但最终成功点亮屏幕的那一刻,成就感是巨大的。它不仅仅是一个节省IO口的技巧,更是一次对IIC通信协议、外设驱动时序和嵌入式调试方法的深入实践。记住最关键的三点:确认芯片型号与地址、保证初始化序列正确、在关键操作点增加足够的延时。希望这份详细的总结和代码,能帮你快速跨过这些坑,顺利实现你的显示功能。