STM32双I2C接口实战:如何让多个传感器各走各路,互不打架?
你有没有遇到过这种情况:项目里接了温湿度传感器、触摸屏、OLED显示屏、EEPROM……全都用I2C通信,结果一通电,总线“卡死”,读不到数据?或者某个设备偶尔失联,调试半天发现是地址冲突或总线负载太重?
别急,这不怪你代码写得差,也不是芯片不行——而是你该上双I2C接口资源管理这堂课了。
在STM32这类主流MCU中,通常自带两个甚至更多的硬件I2C外设(比如I2C1和I2C2)。但很多人只把它当“备用通道”来用,其实只要稍加规划,就能实现真正的并行通信、任务解耦、响应提速。今天我们就来聊聊:怎么把这两条I2C总线用明白,让你的系统稳如老狗。
为什么一个I2C不够用了?
先说清楚问题根源。
I2C协议本身很优雅:两根线(SDA + SCL),支持多从机挂载,地址寻址,布线简单。但它也有硬伤:
- 半双工:同一时间只能发或收;
- 共享总线:所有设备共用一条物理链路;
- 时序敏感:时钟拉伸、ACK丢失、NACK响应都可能引发阻塞;
- 地址唯一性要求高:一旦两个设备地址相同,直接“撞车”。
所以当你在一个I2C总线上挂了五六个设备,还要频繁轮询触摸屏、刷新屏幕、保存配置到EEPROM……CPU就得不停地排队处理请求,稍有不慎就会超时、锁死,甚至拖垮整个系统。
那怎么办?有人会想:“我用软件模拟I2C不就行了?”
——可以,但代价是CPU占用飙升,实时性大打折扣。
更聪明的做法是:利用STM32内置的双硬件I2C模块,物理隔离不同功能的外设,各走各道,互不干扰。
硬件I2C不只是“自动发起始信号”那么简单
STM32的硬件I2C不是摆设。它不只是帮你生成Start/Stop条件,而是一个完整的协议控制器。我们以STM32F4系列为例,看看它到底强在哪:
它能自己搞定这些事:
- 自动生成起始/停止信号
- 自动发送从机地址 + 读写位
- 每字节后自动等待ACK
- 支持7位和10位寻址
- 可配置通信速率(标准100kbps、快速400kbps、高速可达1Mbps)
- 内建错误检测:总线错误(BERR)、仲裁丢失(ARLO)、NACK检测等
- 支持DMA传输,大批量数据无需CPU干预
这意味着什么?意味着你可以发起一次HAL_I2C_Master_Transmit()调用后,就去干别的事了——数据搬运交给DMA,完成中断再来通知你。
// 初始化I2C1为400kHz快速模式 hi2c1.Instance = I2C1; hi2c1.Init.Timing = 0x00702991; // 根据PCLK1=42MHz计算得出 hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; HAL_I2C_Init(&hi2c1); // 发送命令给OLED,非阻塞式 uint8_t cmd = 0xAF; // 开显示 HAL_I2C_Master_Transmit_IT(&hi2c1, OLED_ADDR << 1, &cmd, 1);你看,连中断版本都有。只要开了中断,发完就走,完全不用死等。
⚠️ 小贴士:
Timing寄存器值不能乱填!建议用STM32CubeMX自动生成,否则时钟分频错一点,通信就翻车。
双I2C不是“多一条线”这么简单,关键在于“怎么分工”
现在重点来了:你有两个I2C接口(I2C1 和 I2C2),该怎么分配才最合理?
别以为随便分就行。如果设计不当,照样会出现中断抢占、DMA通道争用、CPU调度混乱等问题。
真正高效的策略,是按性能需求+任务优先级+通信频率进行功能分区。
✅ 推荐分工方式:高频归一路,低频归另一路
举个典型场景:
| 设备 | 地址 | 通信频率 | 响应要求 |
|---|---|---|---|
| 触摸屏控制器(FT6236) | 0x38 | 每10ms轮询一次 | 高 |
| OLED显示屏(SSD1306) | 0x3C | 每50ms刷新一次 | 中 |
| 温湿度传感器(SHT30) | 0x44 | 每2秒读一次 | 低 |
| EEPROM(AT24C02) | 0x50 | 写入不频繁 | 极低 |
显然,前两者属于“高频+实时性强”的任务,后两者则是“后台慢速操作”。
于是我们可以这样划分:
- I2C1:专供触摸屏 + OLED → 负责人机交互主线
- I2C2:连接SHT30 + EEPROM → 处理环境采集与参数存储
这样一来:
- 主界面流畅度不受传感器采样影响;
- 即使EEPROM写入延时较长,也不会卡住屏幕刷新;
- 各总线负载均衡,避免单条总线过载。
🧩 更进一步:结合RTOS做任务隔离
如果你用了FreeRTOS之类的操作系统,那就更好办了——直接拆成两个独立任务:
void Task_TouchAndDisplay(void *pvParams) { while(1) { // 读取触摸状态 uint8_t touch_data[4]; HAL_I2C_Master_Receive(&hi2c1, TOUCH_ADDR << 1, touch_data, 4, 100); // 更新UI oled_update_cursor(touch_data); vTaskDelay(pdMS_TO_TICKS(10)); // 10ms轮询 } } void Task_SensorAndStorage(void *pvParams) { while(1) { // 每2秒读一次温湿度 uint8_t temp_humi[6]; HAL_I2C_Master_Transmit(&hi2c2, SHT30_ADDR << 1, &trigger_cmd, 1, 100); vTaskDelay(pdMS_TO_TICKS(10)); // 等待转换完成 HAL_I2C_Master_Receive(&hi2c2, SHT30_ADDR << 1, temp_humi, 6, 100); // 每分钟存一次日志到EEPROM(伪逻辑) if (should_save_log) { HAL_I2C_Master_Transmit(&hi2c2, EEPROM_ADDR << 1, log_data, 16, 500); } vTaskDelay(pdMS_TO_TICKS(2000)); } }两个任务分别使用不同的I2C句柄,运行在不同优先级下(可设前者更高),真正做到并行不悖、互不影响。
实战避坑指南:那些文档里不说的“潜规则”
再好的架构也怕细节翻车。以下是我在实际项目中踩过的坑,总结成几条“生存法则”:
🔌 1. GPIO复用必须检查清楚!
STM32很多引脚是多功能复用的。比如PB6/PB7常作I2C1_SCL/SDA,但如果同时开启了串口或定时器PWM,就会冲突。
✅ 正确做法:使用STM32CubeMX图形化配置,一键查看Pinout冲突;或手动查《Datasheet》确认AF功能编号。
⚖️ 2. 中断优先级要分主次
假设I2C1用于触摸中断唤醒,I2C2只是定时采集温度。如果不设置优先级:
HAL_NVIC_SetPriority(I2C1_EV_IRQn, 1, 0); // 高优先级 HAL_NVIC_SetPriority(I2C2_EV_IRQn, 3, 0); // 低优先级那么当I2C2正在传输时,I2C1事件可能被延迟响应,导致触摸“迟钝”。
📦 3. DMA通道不能抢
STM32的DMA资源有限。例如:
- I2C1_TX → DMA1_Stream6_Channel1
- I2C2_RX → DMA1_Stream2_Channel7
如果两个都试图用Stream6,就会冲突。务必查参考手册《RM0090》中的DMA请求映射表,确保无重叠。
💡 4. 上拉电阻要独立配置
虽然I2C协议要求上拉,但很多人图省事只在一端加上拉电阻。问题是:
如果I2C1和I2C2共用电源域,但走线很长,不上拉或上拉不足,会导致上升沿缓慢,高速通信失败!
✅ 正确做法:每条I2C总线单独加4.7kΩ上拉至VDD,尤其是跨板连接或使用排线时。
🛑 5. 超时与恢复机制不能少
别相信“永远能通信成功”。现实世界总有干扰、设备掉电、冷启动未就绪等情况。
一定要在调用HAL函数时设置合理的超时时间,并加入软复位逻辑:
if (HAL_I2C_Master_Transmit(&hi2c1, dev_addr, data, len, 100) != HAL_OK) { // 尝试复位I2C外设 __HAL_I2C_DISABLE(&hi2c1); HAL_Delay(10); __HAL_I2C_ENABLE(&hi2c1); }否则一旦BUSY标志置位,后续所有操作都会失败。
总结:双I2C的本质是“系统级解耦”
你以为双I2C只是多了个通信口?错。
它的真正价值在于:通过物理层隔离,实现系统级的任务解耦、性能优化与故障隔离。
- 当一条总线异常时,不影响另一条正常工作;
- 高频任务不再被低速操作拖累;
- 结合RTOS后,可构建清晰的任务层级结构;
- 整体系统的稳定性、可维护性和扩展性大幅提升。
特别是在工业控制、智能家居网关、便携式仪表这类需要长期稳定运行的设备中,这种设计思维尤为重要。
最后一句话
高手和新手的区别,从来不在会不会写I2C驱动,而在懂不懂如何让多个I2C“和平共处”。
下次你在画PCB之前,不妨先问自己一句:
“我的这两个I2C,到底该谁管什么?”
答案想明白了,系统就已经成功了一半。
如果你也在做类似项目,欢迎留言交流你的I2C布局策略!