用PCF8574驱动LCD1602:如何用2根线控制一块屏?
你有没有遇到过这样的窘境?手头的MCU引脚快被掏空了,ADC、UART、SPI、按键、LED一个接一个,结果还要加个LCD1602显示状态——光是RS、E、D4~D7就得再占6个GPIO。这在STM8、nRF52或QFN封装的小型MCU上几乎是“致命”的开销。
别急,今天我们就来解决这个经典问题:只用I²C两根线(SCL + SDA),通过PCF8574 GPIO扩展芯片,完美驱动LCD1602。不仅节省资源,还能让电路更整洁、系统更具扩展性。
为什么非得“绕路”?直接连不行吗?
先说结论:能连,但代价太大。
LCD1602基于HD44780控制器,标准工作模式需要至少6条控制/数据线(RS、E、D4-D7)。如果你主控还有富裕引脚,当然可以直接接。但在以下场景中,这种“奢侈”就玩不起了:
- 使用STM32F030K6T6这类仅28个可用GPIO的紧凑型MCU;
- 设计低功耗IoT节点,每个多余引脚都意味着布线复杂度和潜在漏电流;
- 需要为未来预留接口(比如CAN、USB、触摸检测);
这时候,“串行转并行”就成了最优解。而PCF8574正是为此而生。
PCF8574:I²C上的“隐形手臂”
它是什么?
PCF8574是一款由NXP推出的8位I/O扩展器,专为I²C总线设计。你可以把它想象成MCU伸出的一只“远程手”,这只手有8个手指(P0-P7),每个都能当输入或输出用。
它的最大魅力在于:
- 只需SCL和SDA两根线连接到MCU;
- 支持最多8片并联(地址由A0-A2决定,范围0x20~0x27);
- 每次通信只需发送一个字节,即可同步更新全部8个引脚电平;
- 工作电压兼容3.3V与5V系统,静态功耗仅约10μA。
小知识:它没有独立的方向寄存器!方向靠“写高=输入,写低=输出”隐式切换——这是使用时最容易踩坑的地方。
引脚功能一览
| 引脚 | 功能说明 |
|---|---|
| P0-P7 | 8个准双向I/O口,用于连接外设 |
| A0-A2 | 地址选择引脚,接地为0,接VCC为1 |
| SDA / SCL | I²C数据/时钟线 |
| VDD / VSS | 电源正负极(2.5V~6V) |
| INT | 中断输出(本应用暂不用) |
我们通常将A0-A2全部接地,得到最常见地址0x27或0x3F(注意部分模块内置上拉,实际地址可能偏移)。
LCD1602怎么被“遥控”?
核心思路:映射 + 分时传输
既然PCF8574只有8个输出位,而LCD1602在4位模式下也需要6个信号(RS、RW、EN、D4、D5、D6、D7),那怎么安排?
很简单:把PCF8574的P0~P7分别接到LCD的对应引脚上。典型接法如下:
| PCF8574 | → | LCD1602 | 功能 |
|---|---|---|---|
| P0 | → | RS | 寄存器选择 |
| P1 | → | RW | 读写控制(常接地) |
| P2 | → | EN | 使能信号 |
| P3 | → | — | 空闲(可作背光控制) |
| P4 | → | D4 | 数据线低半字节 |
| P5 | → | D5 | 数据线 |
| P6 | → | D6 | 数据线 |
| P7 | → | D7 | 数据线高半字节 |
提示:RW脚一般固定接GND(只写不读),所以P1也可以不用接。省下的位可用于控制背光!
软件实现:从协议到底层操作
初始化流程的关键点
LCD1602的初始化必须严格遵循HD44780规范,尤其是在4位模式下。难点在于:上电后不知道当前数据长度,必须先以“伪8位”方式发送三次0x03,才能安全切换到4位模式。
整个过程如下:
- 上电延时 ≥15ms
- 发送0x03(高四位)
- 延时 >4.1ms
- 再发0x03
- 延时 >100μs
- 再发0x03
- 发送0x02,正式进入4位模式
- 后续发送完整命令(如0x28设置双行显示)
这些步骤不能跳,否则LCD会“失联”。
核心代码拆解(基于Arduino Wire库)
#include <Wire.h> // I²C地址(确认你的模块是0x27还是0x3F!) #define PCF8574_ADDR 0x27 // 映射定义 #define LCD_RS (1 << 0) #define LCD_RW (1 << 1) // 若未使用可忽略 #define LCD_EN (1 << 2) #define LCD_BACKLIGHT (1 << 3) // 可选:背光控制 #define LCD_D4 (1 << 4) #define LCD_D5 (1 << 5) #define LCD_D6 (1 << 6) #define LCD_D7 (1 << 7) static uint8_t lcd_data = 0; void pcf8574_write(uint8_t data) { Wire.beginTransmission(PCF8574_ADDR); Wire.write(data); Wire.endTransmission(); } // 发送半字节(关键函数) void lcd_send_nibble(uint8_t nibble, bool is_command) { // 清除旧D4-D7数据 lcd_data &= ~(LCD_D4 | LCD_D5 | LCD_D6 | LCD_D7); // 设置新数据 if (nibble & 0x01) lcd_data |= LCD_D4; if (nibble & 0x02) lcd_data |= LCD_D5; if (nibble & 0x04) lcd_data |= LCD_D6; if (nibble & 0x08) lcd_data |= LCD_D7; // 设置RS if (!is_command) lcd_data |= LCD_RS; else lcd_data &= ~LCD_RS; // EN脉冲:高→低触发 lcd_data |= LCD_EN; pcf8574_write(lcd_data); delayMicroseconds(1); lcd_data &= ~LCD_EN; pcf8574_write(lcd_data); delayMicroseconds(50); // 给LCD反应时间 } // 发送完整字节 void lcd_write_byte(uint8_t byte, bool is_command) { lcd_send_nibble(byte >> 4, is_command); // 高四位 lcd_send_nibble(byte & 0x0F, is_command); // 低四位 delayMicroseconds(40); } // 初始化LCD void lcd_init() { delay(50); // 上电稳定 lcd_data = 0; pcf8574_write(lcd_data); // 初始同步序列(必须三次) lcd_send_nibble(0x03, true); delay(5); lcd_send_nibble(0x03, true); delay(5); lcd_send_nibble(0x03, true); delay(1); lcd_send_nibble(0x02, true); // 切换至4位模式 delay(1); // 正式配置 lcd_write_byte(0x28, true); // 4位,2行,5x7字体 lcd_write_byte(0x0C, true); // 开显示,关光标 lcd_write_byte(0x06, true); // 自动增址,不移屏 lcd_write_byte(0x01, true); // 清屏 delay(2); }如何打印字符串?
加个简单封装就行:
void lcd_print(const char* str) { while (*str) { lcd_write_byte(*str++, false); // false表示是数据 } } void lcd_set_cursor(uint8_t row, uint8_t col) { uint8_t addr = col; if (row == 1) addr += 0x40; lcd_write_byte(0x80 | addr, true); }调用示例:
lcd_init(); lcd_print("Hello World!"); lcd_set_cursor(1, 0); lcd_print("From I2C LCD");实际搭建注意事项(避坑指南)
1. 上拉电阻不能少
I²C是开漏结构,SCL和SDA必须接4.7kΩ上拉电阻到VCC。很多PCF8574模块已集成,但自己画板时务必加上。
2. 地址搞不清?试试扫描工具
不确定模块地址?用Arduino写个I²C扫描程序:
void scan_i2c() { Wire.begin(); Serial.println("Scanning I2C..."); for (uint8_t i = 1; i < 127; i++) { Wire.beginTransmission(i); if (Wire.endTransmission() == 0) { Serial.printf("Found device at 0x%02X\n", i); } } }常见地址:0x27(无上拉)、0x3F(带P0-P7上拉版本)
3. 对比度调不好?检查V0
LCD的第3脚V0是用来调对比度的。建议接一个10kΩ电位器,两端接VCC和GND,中间抽头接V0。否则可能出现全黑或无显示。
4. 背光太耗电?用MOSFET控制
如果背光常亮,电流可达150mA以上。可以用P3控制一个N-MOS管来开关背光,进一步节能。
5. 通信失败?看看电源退耦
在PCF8574和LCD的VCC引脚附近各放一个0.1μF陶瓷电容,防止电源噪声导致通信异常。
进阶玩法:不只是显示文字
掌握了基础,还可以拓展更多功能:
- 动态刷新状态:实时显示传感器温度、湿度;
- 菜单导航系统:配合两个按键实现上下选择;
- 低功耗唤醒显示:睡眠中关闭背光,按键唤醒后短暂显示;
- 多设备共存:同一I²C总线上挂载多个PCF8574,分别控制LCD和键盘矩阵;
甚至可以用MCP23017(16位I/O扩展)替代PCF8574,获得更多控制自由度。
总结:这不是妥协,是进化
表面上看,我们“绕了个弯”才点亮屏幕;但实际上,这是一种更高层次的设计思维:
✅节省宝贵GPIO资源
✅简化PCB布局,减少干扰风险
✅提高代码复用性和移植性
✅支持热插拔与模块化设计
对于追求极致小型化、低功耗、高可靠性的嵌入式产品来说,这种“桥接式驱动”早已成为行业惯例。
下次当你面对“引脚不够”的困境时,不妨想想:是不是该给MCU配个“助手”了?
如果你已经在项目中用了类似方案,或者遇到了奇怪的初始化问题,欢迎在评论区分享你的经验和解决方案!