2. I2C接口GPIO扩展芯片详解:PCF8574/PCF8575原理、地址配置与读写时序

好,咱们进入正题。上一章聊了为什么需要GPIO扩展,这一章咱们直接看芯片——PCF8574和PCF8575。这两颗芯片,可以说是I2C GPIO扩展领域的「老兵」了。我入行那会儿,它们就已经在市场上站稳了脚跟,直到现在,很多工业产品和消费电子里依然能看到它们的身影。

说白了,它们干的事很简单:用两根线(I2C总线)换回8个或16个GPIO口。但你别小看这个功能,在引脚紧张的MCU项目里,这简直就是救星。

2.1 PCF8574与PCF8575的核心区别

先看个表格,一目了然:

参数 PCF8574 PCF8575
GPIO数量 8位(P0~P7) 16位(P0~P15)
封装 SO-16 / DIP-16 SO-24 / SSOP-24
I2C地址范围 0x20 ~ 0x27(3位地址线) 0x20 ~ 0x27(3位地址线)
最大挂载数量 8片 8片
输出驱动能力 推挽/开漏,25mA灌电流 推挽/开漏,25mA灌电流
中断输出 有(INT引脚) 有(INT引脚)

嗯,这里要注意:PCF8574是8位的,PCF8575是16位的。如果你只需要扩展几个按键或者LED,8574就够了。但如果你要驱动一个16x2的字符液晶,或者同时控制一堆继电器,那8575会更合适。

我个人习惯是:能用8574就不上8575。为什么?因为8574的封装更小,布线更简单,而且8位IO在大多数场景下已经够用了。多出来的8个口,很多时候反而是浪费PCB面积。

2.2 地址配置:别让地址冲突坑了你

I2C总线上每个设备必须有唯一的地址。PCF8574/8575的地址由两部分组成:

  • 固定部分:高4位,固定为 0100(即0x4)
  • 可配置部分:低3位,由A0、A1、A2引脚的电平决定

所以完整的地址格式是:0100 A2 A1 A0,范围从 0100 000(0x20)到 0100 111(0x27)。

举个例子:

  • A0接GND,A1接GND,A2接GND → 地址 0x20
  • A0接VCC,A1接GND,A2接GND → 地址 0x21
  • A0接GND,A1接VCC,A2接VCC → 地址 0x26
⚠️ 我曾经踩过的坑: 有一次做项目,用了3片PCF8574,地址分别设成了0x20、0x21、0x22。结果调试时发现第二片怎么都读不到数据。查了半天,原来是A1引脚虚焊了,导致地址变成了0x20,和第一片冲突。从那以后,我每次焊接完都会用万用表量一下地址引脚的电平,确保没问题再上电。

另外,PCF8574和PCF8575的地址范围完全一样。如果你在一条I2C总线上同时使用这两种芯片,要注意地址不能重复。比如你用了0x20的8574,就不能再用0x20的8575了。

2.3 读写时序:I2C通信的核心

PCF8574/8575的I2C时序其实很简单,但有几个细节容易出错。咱们一个一个说。

2.3.1 写操作(输出)

写操作就是MCU向芯片的IO口输出电平。时序如下:

  1. 发送起始条件(START)
  2. 发送设备地址 + 写位(0x40 | 地址 << 1)
  3. 等待从机应答(ACK)
  4. 发送数据字节(要输出的IO状态)
  5. 等待从机应答(ACK)
  6. 发送停止条件(STOP)

代码示例(基于STM32 HAL库):

// 向PCF8574写入一个字节
// addr: 芯片地址(0x20~0x27)
// data: 要输出的数据,bit0对应P0,bit7对应P7
HAL_StatusTypeDef PCF8574_Write(uint8_t addr, uint8_t data)
{
    return HAL_I2C_Master_Transmit(&hi2c1, addr << 1, &data, 1, 100);
}

你看,就这么简单。但有个坑:PCF8574的IO口在复位后默认是高电平。如果你要驱动LED,记得把LED的阴极接IO口,阳极通过电阻接VCC。这样IO输出低电平时LED亮,输出高电平时LED灭。我刚开始做的时候没注意这个,直接把LED阳极接IO口了,结果上电瞬间所有LED都亮了,吓我一跳。

2.3.2 读操作(输入)

读操作稍微复杂一点,因为PCF8574的IO口是准双向的。什么意思?就是说,你要先写一个高电平到某个引脚,然后才能把它当输入用。

时序如下:

  1. 发送起始条件(START)
  2. 发送设备地址 + 读位(0x40 | 地址 << 1 | 0x01)
  3. 等待从机应答(ACK)
  4. 读取数据字节(从机发送,主机应答)
  5. 发送非应答(NACK)
  6. 发送停止条件(STOP)

代码示例:

// 从PCF8574读取一个字节
// addr: 芯片地址(0x20~0x27)
// 返回值: 读取到的IO状态
uint8_t PCF8574_Read(uint8_t addr)
{
    uint8_t data = 0;
    HAL_I2C_Master_Receive(&hi2c1, (addr << 1) | 0x01, &data, 1, 100);
    return data;
}
💡 我的经验: 读操作之前,最好先写一个0xFF到芯片,确保所有引脚都被拉高。这样外部电路才能正常拉低引脚。如果你不写0xFF直接读,可能会读到错误的数据。这个细节,数据手册里其实有写,但很多人会忽略。

2.4 中断功能:别让CPU空转

PCF8574/8575有一个INT引脚,当输入引脚状态发生变化时,INT会输出低电平。这个功能在按键检测中特别有用。

你想想看,如果没有中断,你就得不停地轮询IO口,CPU啥也干不了。有了中断,CPU可以安心睡大觉,等按键按下时再被唤醒。

使用中断的步骤:

  • 将INT引脚连接到MCU的外部中断引脚
  • 配置MCU的外部中断为下降沿触发
  • 在中断服务函数中读取PCF8574的IO状态
⚠️ 注意: INT引脚是开漏输出,需要外接上拉电阻。我一般用4.7kΩ,如果总线电容比较大,可以换成2.2kΩ。另外,中断服务函数里要尽量少做事,最好只设置一个标志位,然后在主循环里处理。否则容易造成中断嵌套或者响应延迟。

2.5 实际项目中的选型建议

最后,给几个实际建议:

  • 按键扫描:用PCF8574,8个口可以接8个按键,配合中断,效率很高
  • LED控制:如果只是控制几个指示灯,8574够用;如果要控制数码管或者点阵,建议上8575
  • 多片级联:一条I2C总线上最多挂8片,总共64个IO口(8574)或128个IO口(8575)。但要注意总线电容,距离长了要加I2C中继器
  • 电平匹配:PCF8574/8575的工作电压是2.5V~5.5V,但I2C总线电平要和MCU匹配。如果MCU是3.3V,芯片是5V,最好加电平转换

嗯,这一章的内容差不多就这些。PCF8574和PCF8575虽然老,但胜在稳定、便宜、资料多。下一章咱们会实际动手,写一个完整的驱动代码,包括初始化、读写、中断处理,到时候你就知道怎么用了。