STM32的I/O口不够用?手把手教你用PCF8574扩展8个端口(附中断处理与完整代码)
在嵌入式开发中,STM32系列MCU因其出色的性能和丰富的外设资源而广受欢迎。然而,随着项目复杂度提升,开发者常常会遇到一个棘手问题:I/O口不够用。想象一下,当你需要连接多个按键、LED、传感器时,发现MCU引脚已经捉襟见肘——这正是我们今天要解决的痛点。
PCF8574这颗售价仅几元的芯片,能通过I²C总线为STM32扩展8个双向I/O口,且支持中断功能。不同于简单的端口复制器,它实现了真正的"按需扩展":仅需2根信号线(SCL/SDA)即可管理多个扩展芯片,特别适合智能家居控制板、工业数据采集器等需要密集I/O的场景。下面我们将从硬件设计到代码实现,完整展示如何让这颗小芯片发挥大作用。
1. 硬件设计与电路搭建
1.1 器件选型对比
市场上常见的I/O扩展方案主要有三种:
| 方案类型 | 典型芯片 | 总线占用 | 扩展能力 | 成本 | 适用场景 |
|---|---|---|---|---|---|
| 串行转并行 | 74HC595 | 3线 | 8输出 | 低 | LED驱动等纯输出场景 |
| GPIO扩展器 | PCF8574 | 2线(I²C) | 8双向 | 中 | 按键/传感器混合场景 |
| 专用接口芯片 | MAX7313 | 2线(I²C) | 16双向 | 高 | 高端设备 |
PCF8574的独特优势在于:
- 准双向端口:无需配置方向寄存器,内部自动处理输入/输出切换
- 中断响应:INT引脚可实时通知MCU输入状态变化
- 地址扩展:3个硬件地址引脚支持最多8片级联(总计64个I/O)
1.2 核心电路连接
典型应用电路如下图所示(注:实际需根据STM32型号调整电压匹配):
STM32F103C8T6 PCF8574 PB6(SCL) ----------- SCL PB7(SDA) ----------- SDA | | | A0-A2 -- GND (地址0x40) | INT ---- PB12(EXTI) | VCC ---- 3.3V | GND ---- GND | P0-P7 ---- 外接按键/LED等关键细节:
- 上拉电阻:I²C总线的SCL/SDA需接4.7KΩ上拉电阻
- 中断处理:INT引脚推荐连接至STM32的EXTI中断引脚(如PB12)
- 端口保护:每个I/O口建议串联220Ω电阻防止过流
注意:当多个PCF8574级联时,需为每个芯片分配唯一地址。例如A0接VCC,A1-A2接GND时地址变为0x42。
2. 寄存器配置与通信协议
2.1 I²C地址解析
PCF8574的7位设备地址构成如下:
固定部分(0100) + 硬件地址(A2A1A0)地址计算示例:
- A2A1A0=000时:0100000 (0x20) → 写地址0x40,读地址0x41
- A2A1A0=001时:0100001 (0x21) → 写地址0x42,读地址0x43
2.2 中断工作机制
PCF8574的中断系统工作流程:
- 任一输入端口电平变化(高→低或低→高)
- INT引脚立即拉低(下降沿触发)
- MCU检测到中断后,必须执行一次I²C读/写操作清除中断
- INT引脚恢复高电平,准备下次检测
典型问题排查:
- 中断不触发:检查INT引脚是否配置为上拉输入模式
- 中断不恢复:确认MCU已执行I²C访问操作
- 信号抖动:在INT引脚添加0.1μF电容滤波
3. 软件驱动开发
3.1 初始化流程
// 初始化代码示例(HAL库) void PCF8574_Init(void) { // 1. 配置I²C外设 hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; // 标准模式100kHz hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; HAL_I2C_Init(&hi2c1); // 2. 配置中断引脚 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_12; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 3. 初始状态设置(所有端口高电平) uint8_t init_val = 0xFF; HAL_I2C_Master_Transmit(&hi2c1, 0x40, &init_val, 1, 100); }3.2 中断服务例程
// 中断回调函数 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin == GPIO_PIN_12) { uint8_t port_state; // 读取端口状态清除中断 HAL_I2C_Master_Receive(&hi2c1, 0x41, &port_state, 1, 100); // 判断具体变化的位 static uint8_t last_state = 0xFF; uint8_t changed_bits = last_state ^ port_state; last_state = port_state; // 示例:P0下降沿触发LED切换 if((changed_bits & 0x01) && !(port_state & 0x01)) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); } } }4. 实战:智能灯光控制器
4.1 功能需求
- 4路触摸按键输入(P0-P3)
- 4路LED调光输出(P4-P7)
- 按键长按/短按识别
- 灯光亮度记忆功能
4.2 关键代码实现
// 亮度控制PWM函数 void SetLEDBrightness(uint8_t led_num, uint8_t brightness) { static uint8_t port_state = 0xFF; // 保留其他位状态 port_state &= ~(0x01 << (led_num + 4)); port_state |= (brightness > 128) ? (0x01 << (led_num + 4)) : 0; // 写入PCF8574(实际项目需加入PWM软实现) HAL_I2C_Master_Transmit(&hi2c1, 0x40, &port_state, 1, 100); } // 按键扫描状态机 void KeyScan_Task(void) { static uint32_t press_time[4] = {0}; uint8_t current_state = PCF8574_ReadPort() & 0x0F; for(int i=0; i<4; i++) { if(!(current_state & (1<<i))) { if(press_time[i] == 0) { press_time[i] = HAL_GetTick(); // 记录按下时刻 } } else { if(press_time[i] != 0) { uint32_t duration = HAL_GetTick() - press_time[i]; if(duration > 1000) { LED_LongPressAction(i); // 长按处理 } else { LED_ShortPressAction(i); // 短按处理 } press_time[i] = 0; } } } }4.3 性能优化技巧
- 批量传输优化:
// 单次传输代替多次位操作 void UpdateAllLEDs(uint8_t states) { HAL_I2C_Master_Transmit(&hi2c1, 0x40, &states, 1, 100); }- 中断防抖处理:
// 在EXTI回调中加入去抖延时 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t last_time = 0; if(HAL_GetTick() - last_time > 50) { // 50ms防抖 // 实际处理逻辑 } last_time = HAL_GetTick(); }- 低功耗设计:
// 睡眠模式下唤醒配置 void EnterLowPowerMode(void) { // 配置PCF8574中断唤醒 HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); SystemClock_Config(); // 唤醒后重新初始化时钟 }在完成一个实际项目后,发现PCF8574的中断响应延迟通常在微秒级,完全能满足大多数实时性要求。但要注意避免在中断服务程序中执行复杂操作,推荐采用"中断标记+主循环处理"的方式。当需要驱动多个LED时,可以结合PWM软实现来扩展调光功能——虽然不如硬件PWM精确,但对大多数场景已经足够。