1. 从一次“不够用”的窘境说起
做嵌入式开发的朋友,十有八九都撞到过这堵墙:主控芯片引脚算来算去就是差那么几个,要么砍功能,要么换大封装主控,要么硬着头皮飞线。我之前做过一个带多路按键、LED、传感器供电控制的采集节点,主控就一个36脚的MCU,功能排下来GPIO缺口将近10个,板子尺寸又卡得死死的,换主控不现实。
那会儿第一反应是“要不要上CPLD”,后来冷静算了一笔账:CPLD成本、开发环境、烧录器,全加在一起解决10个IO的问题,性价比实在太低。后来把目光放在I2C IO扩展器上,一套流程跑下来,板子顺利量产,成本只多了几块钱。这个经历让我对IO扩展选型有了不少切身体会,今天就把这些经验掰开揉碎聊一聊。
这篇文章适合谁看?硬件工程师评估方案阶段、嵌入式软件工程师在驱动层踩坑、以及自己画板子玩单片机的创客朋友。我会把方案对比、选型参数、实际代码、常见坑全部整理出来,尽量让不同基础的读者都能拿去直接用。
2. 主控GPIO不够用,先别急着加芯片
2.1 四种扩展方案的底层逻辑
听到“GPIO扩展”,很多人第一反应就是加一颗IO扩展芯片。实际上GPIO不够用有更朴素的解法,只是大家习惯往复杂了想。我把常见的思路排了一下,每个方案背后的本质逻辑完全不同。
第一种是逻辑器件扩展。原理特别简单:用74HC595这种串行移位寄存器,把数据一位一位送进去,然后并行输出。控制3个引脚就能换来8个输出,级联以后还能继续翻倍。反过来输入端可以用74HC165,把并行的电平一个一个读出来。优点是非常便宜,一颗595几毛钱,缺点也很明显:想读输入就要换芯片,想同时读又要加逻辑,输出状态在上电瞬间是乱的,得额外处理。
第二种是I2C IO扩展。走的是标准通信协议,主控I2C总线上挂一颗从设备,通过写寄存器来控制引脚。一颗PCF8574有8个IO,地址通过硬件引脚可以拼出8个地址,一条总线上最多挂64个IO(8颗芯片)。优点是双向输入输出、占用引脚少(两个引脚SCL/SDA即可)、上电状态可控、可以直接级联。缺点是有通信时序的要求,软件上比直接拉引脚多几行代码。
第三种是SPI IO扩展,比如MCP23S17。本质和I2C一样,但SPI速率高不少,适合需要高频翻转的场景,代价是占用的引脚多一个(至少SCK、MOSI、MISO、CS四个),而且SPI设备没有像I2C那样灵活的地址硬件配置。
第四种是模拟开关/译码器,用74HC138这种3-8译码器组合出多路选择。这个方法看着省引脚,实际用起来非常难受,因为输出不锁存、没有寄存器概念,每一次切换都要重新控制选择引脚。适合做扫描场景,不适合做需要保持状态的输出。
我在实际项目里的取舍逻辑是这样的:纯输出点LED指示、没有输入需求、对成本极其敏感的,用595。按键扫描、输入输出混用、需要保存输出状态的,直接上I2C扩展。SPI扩展芯片除非速率确实不够,否则不推荐,纯属浪费引脚。
2.2 I2C扩展器相对其他方案的三点优势
I2C IO扩展器能够成为大多数场景的默认选择,不是营销吹出来的,而是三个硬核优势拍板的。
第一个优势是引脚效率极高。整条I2C总线就两根线(SCL、SDA),只要地址不冲突,想挂几个扩展器都可以。对比SPI的4根线,以及595的至少3根控制线再加级联逻辑,I2C完全是数量级的节省。更妙的是I2C天生支持多设备共用总线——你如果已经有传感器在总线上,扩展器就是顺路捎带的事,不需要额外占用任何引脚。
第二个优势是输入输出双向可编程。PCF8574这类芯片虽然简单,但每个引脚既可以配置为输入也可以驱动为输出,这是595做不到的。实际项目中特别常见的一种需求是“这两个引脚平时当输入用,我需要的时候要能把它拉低”——比方说去控制一个使能脚,又想去检测它外部的电平状态。I2C扩展器一条指令切换方向,硬件逻辑上天然支持。
第三个优势是上电状态有保证。595在MCU配置之前输出是随机的,可能导致继电器误动作。而I2C扩展器比如MCP23017带硬件复位引脚和上电默认寄存器值,或者PCF8574上电后输出为高,在设计电路时把这些状态考虑进去,系统上电的确定性就高了。这一点在很多工业控制类产品里是硬性要求。
3. 选型前必须搞懂的几个核心参数
3.1 地址配置:决定一条总线能挂多少芯片
I2C通信之所以只需要两根线,靠的是每个设备有独一无二的地址。IO扩展器选型第一个要关注的就是地址位宽。PCF8574有A0/A1/A2三个硬件引脚,每个引脚能接高电平或低电平,所以有2的3次方等于8种组合,基地址是0x20(后面细讲),实际占用0x20到0x27这8个地址。MCP23017一样是3个地址引脚。TCA9535也是这样。
AW9523是4个地址引脚,支持16种地址组合。重点提醒一句:看芯片手册时要注意区分“地址引脚数量”和“可挂载芯片数量”,可挂载数量理论上等于2的地址引脚次方,但是还要扣除总线上其他设备的地址占用。比如总线上已经有一个地址为0x20的传感器,那么PCF8574从0x20开始的8个地址就有一个被占了,实际最多只能挂7颗。设计时先把总线地址图列出来,再决定选几颗芯片,避免做到一半发现地址不够用只能改板。
3.2 输出结构与驱动能力:不是所有引脚都能推挽输出
这是一个非常多新手栽跟头的地方。
PCF8574的输出结构是开漏,引脚内部没有推挽驱动,只能拉低或者释放。释放以后靠外部上拉电阻把电平拉高。这意味着两件事:第一,引脚输出高电平的驱动能力取决于上拉电阻阻值,想驱动LED必须外部加上拉电阻并且计算电流;第二,开漏结构天然适合“线与”,多个引脚可以直接并联做逻辑或,这在某些特殊电路里反而是个优势。
MCP23017则不同,它的输出寄存器默认是推挽输出,可以直接推高和拉低,驱动能力也更强,单引脚能到25mA(具体看电压),带LED完全不需要外部上拉。TCA9535又回到了开漏结构。
AW9523是这几颗里比较特殊的存在:它支持推挽输出模式,每个引脚可以通过寄存器配置为推挽或开漏。默认是开漏,但改一个bit就能变推挽,对驱动LED、继电器这种负载非常友好。而且AW9523内置了LED呼吸灯控制,硬件自动产生PWM波形,软件写寄存器就能做呼吸效果,不用MCU软件模拟。
选择逻辑很简单:要驱动LED灯板、数码管段位,选推挽输出或者开漏+外部上拉的方案;要接I2C总线做电平匹配,开漏反而更方便。
3.3 中断能力:轮询还是等中断,直接影响系统功耗
很多人选IO扩展器只关心IO数量和地址,完全忽略中断引脚。等到实际做低功耗项目的时候才后悔:设备睡眠状态下IO扩展器检测到按键动作,主控还在睡,按键事件丢失。
PCF8574是靠读引脚电平来感知外部变化的,想检测按键必须不停轮询,或者用一个独立的IO口同时接按键和扩展器。MCP23017带了INTA/INTB两个中断输出引脚,当配置为输入的引脚电平发生变化时,中断引脚会拉低,主控可以从睡眠中唤醒再通过I2C读具体是哪个引脚变化了。TCA9535也是类似设计。
AW9523的中断设计更灵活,每个引脚可以独立使能中断,并且支持边沿触发和电平触发。实际做低功耗产品时,把扩展器所有输入引脚的中断都打开,主控平时进入睡眠,有外部事件通过INT引脚唤醒,再读寄存器知道哪个按键触发,整机功耗能压得很低。
我建议只要目标产品要考虑电池供电或者待机功耗,就直接选带中断的型号。哪怕第一版用不到,多这个功能也不多花钱,后续升级固件就能用上。
3.4 速率等级与总线上拉电阻:稳定性隐患从这里出
I2C协议有标准模式100kHz、快速模式400kHz、快速+模式1MHz这些等级。多数IO扩展器支持到400kHz,看起来随便够用,但很多人忽略了一个重要细节:总线上拉电阻的取值和总线上挂的设备数量直接相关。
总线电容越大,上拉电阻就要越小,否则信号上升沿太慢,通信会出现随机错误。几个设备的电容叠加起来,总线可能就不达标了。我做过多设备挂载的项目,总线上挂了2颗PCF8574、1颗BH1750光照传感器、1颗EEPROM,上拉电阻用的是4.7k,总线速率跑到400kHz就偶尔出错,换到2.2k才稳定。后来查了总线电容计算公式,总线上挂的设备越多,上拉电阻确实需要相应减小。
需要注意是上拉电阻太小了会增大静态电流,IO扩展器的输出低电平驱动能力有限,如果上拉电阻太小,引脚拉低时电流太大,同样会导致通信失败。工程实践建议:挂载设备少(2~3个)、总线长度短(10cm以内)用4.7k没问题;设备多、走线长的用2.2k试;极端情况可以上1k,此时注意确认芯片输出低电平时的灌电流能力在规格书范围内。
3.5 特殊功能:是不是有“隐藏加分项”
除了基础的IO读写,现在新出的IO扩展器开始卷内置功能。AW9523的LED呼吸灯就是典型代表,芯片内部有PWM模块,可以独立控制每个引脚的呼吸频率和占空比。对于做氛围灯、跑马灯、键盘背光的项目,这个功能能把MCU从软件模拟PWM中解放出来,一个寄存器指令就搞定了。
还有一类加分项是I2C地址可编程范围和两种供电域支持。AW9523支持1.8V到5V供电,逻辑电平也可以单独匹配,这样两边电压不一样也能直接用。选型时多翻几页手册的特殊功能章节,经常能发现一些小惊喜。
4. 五款主流I2C IO扩展芯片横向对比
数据手册的参数比较枯燥,我整理了一张表,把目前市面上最常遇到的几款芯片放在一起,方便快速定位。
| 芯片型号 | IO数量 | 输出结构 | 地址数 | 中断 | 最大IO速率 | 典型驱动能力 | 特色功能 | 典型价格区间 |
|---|---|---|---|---|---|---|---|---|
| PCF8574 | 8 | 开漏 | 8 | 无 | 100k/400k | 准双向,灌电流约20mA | 最经典,资料最多 | 0.5~1.5元 |
| MCP23017 | 16 | 推挽 | 8 | 有 | 1.7MHz(约100k/400k兼容) | 单引脚25mA | 输入极性可配置,内部上拉可选 | 3~6元 |
| TCA9535 | 16 | 推挽(准双向) | 8 | 有 | 400k | 单引脚约10mA | TI主流,低静态功耗 | 2~5元 |
| AW9523 | 16 | 推挽/开漏可配 | 16 | 有 | 400k | 单引脚约20mA | LED呼吸灯/PWM,电压范围宽 | 2~4元 |
| PCA9555 | 16 | 推挽 | 8 | 有 | 400k | 单引脚约10mA | 老牌经典,替代型号多 | 2~5元 |
选型的时候我一般这么走:只做简单输出和少量输入、预算敏感,选PCF8574,成本最低,代码网上满天飞;IO需求超过8个、且要中断+推挽,看MCP23017和PCA9555,MCP23017的资料和库更多;做LED灯效类的优先考虑AW9523,呼吸灯功能省掉大量软件工作;功耗敏感、电池供电优先考虑TCA9535,静态电流更低。
实际项目中我用的最多的是PCF8574和AW9523,前者纯打杂,后者是后来发现的“真香”芯片。
5. 实操演示:PCF8574从硬件连接到驱动代码
5.1 硬件连接与地址计算
以STM32F103的主控为例,I2C1的PB6连SCL,PB7连SDA,接PCF8574。SCL和SDA各接一颗4.7k上拉电阻到3.3V,这是I2C标准接法,不管芯片供电是3.3还是5V都要有上拉。PCF8574的供电接3.3V还是5V,取决于后级负载的电平要求,如果只控制LED和读取按键,3.3V完全够用。
A0/A1/A2三个脚决定从机地址,接地时全部为0。PCF8574的7位地址是0100 000,写操作时最低位为0,所以完整写地址是0x40,读地址是0x41。很多人的代码里写X=0x20,那是7位地址左移一位后的写法,两者等价,但要注意驱动库到底用的是7位地址还是8位地址。STM32标准库的I2C从机地址参数要去掉最低位,所以填0x20;有些封装好的Arduino库内部会自动左移,填0x20它会再移一次,结果就变成0x40了,导致通信失败。这一点是新手最容易迷糊的地方。
地址配置示例:A2=1、A1=0、A0=1时,7位地址是0100 101,即0x25,读写地址分别是0x4A和0x4B。
5.2 代码实现:先搞懂寄存器,再写读写函数
PCF8574只有一个8位寄存器,往里面写0x00就是所有引脚拉低输出低电平,写0xFF就是所有引脚释放(开漏输出高,外部上拉生效)。读它的时候,引脚的当前电平会以8位数据的形式返回。
先看写操作代码,我用的是STM32标准库风格的伪代码,方便各种主控的朋友理解核心逻辑:
// 向PCF8574写数据,控制8个IO输出 void PCF8574_Write(uint8_t devAddr, uint8_t data) { I2C_Start(); I2C_SendByte(devAddr << 1 | 0); // 7位地址左移一位,最低位写方向0 I2C_WaitAck(); I2C_SendByte(data); // 写入8位IO状态 I2C_WaitAck(); I2C_Stop(); }读操作需要注意一个细节:读数据之前,必须先把寄存器写成0xFF,把所有引脚释放成输入状态,否则引脚如果之前被锁存为低电平,读回来的永远是0。很多人在读按键的时候卡住就是这个原因。
// 从PCF8574读数据,读取输入引脚电平 uint8_t PCF8574_Read(uint8_t devAddr) { uint8_t data; PCF8574_Write(devAddr, 0xFF); // 关键:先释放引脚,才能正确读取外部电平 I2C_Start(); I2C_SendByte(devAddr << 1 | 1); // 7位地址左移一位,最低位写方向1(读) I2C_WaitAck(); data = I2C_ReadByte(); // 读取8位IO状态 I2C_SendNack(); // 读最后一字节回NACK I2C_Stop(); return data; }实际用的时候,PCF8574的输入输出是准双向的,写0x00引脚拉低输出,写0xFF引脚释放变成输入。如果想某个引脚既当输入又当输出,只能靠程序频繁切换,这是开漏结构的局限,设计时要想清楚哪些引脚固定输入、哪些固定输出。
5.3 按键输入的消抖与中断替代方案
PCF8574没有中断引脚,按键检测只能轮询。实测中轮询频率设在10~20ms就够,过高浪费CPU,过低则按键手感迟钝。按键消抖用软件延时或定时器实现,状态机处理按下和释放两个沿。
我在实际项目中的做法是:用一个1ms的定时器,每次进入中断扫描一次PCF8574,把扫描结果存入全局变量,主循环里做状态机判断。这样子按键响应时间1~5ms可调,比较平滑。
如果按键数量多且需要睡眠唤醒,还是建议换到MCP23017或者AW9523,靠外部INT引脚唤醒主控,效率和功耗完全不是一个级别。
6. AW9523进阶玩法:一颗芯片解决LED灯效和按键输入
6.1 为什么我会在项目中转投AW9523
最开始用PCF8574做了一批样机,功能没问题,但客户提了新需求:按键要多几个,LED要做成呼吸效果。用PCF8574做呼吸灯只能在MCU里用PWM模拟,8个LED就要8路PWM,MCU的硬件定时器压根不够用。翻资料的时候看到AW9523,发现它专门为这类场景设计,内部有LED驱动模块,每个引脚都可以独立配置PWM呼吸效果。
从项目角度算了一笔账:PCF8574加8路PWM占用的MCU资源、代码复杂度、调试时间,比起AW9523多出来的两块多成本,完全不划算。这个案例也让我认识到,选型不能只看单价,要把整个BOM成本、开发工时、调试风险全都算进去。
6.2 AW9523关键寄存器速览与配置流程
AW9523的寄存器映射比PCF8574复杂,但常用也就那么几个。重点提两个:一个是0x00的GCR(Global Control Register),bit0是SWRST软件复位;另一个是0x11到0x12的LED模式寄存器,把对应位置1后引脚进入LED模式,此时输出由PWM寄存器控制。
配置一个引脚为推挽输出并做呼吸灯效果的流程如下:
// 1. 软件复位 AW9523_Write(0x00, 0x01); // GCR写0x01触发软复位 delay_ms(5); // 2. 配置引脚方向,P0_0为输出 AW9523_Write(0x02, 0xFE); // 方向寄存器,1为输入,0为输出 // 3. 使能P0_0的LED模式 AW9523_Write(0x11, 0x01); // LED模式寄存器,bit0对应P0_0 // 4. 设置呼吸频率和占空比 AW9523_Write(0x20, 0x80); // 呼吸频率控制 AW9523_Write(0x21, 0xFF); // 最大电流配置完以后,引脚的呼吸效果由硬件自动产生,MCU只需要在需要改变亮度或频率时才写一次寄存器。我做项目时一共控制了12路呼吸灯,MCU负载几乎没有增加。
6.3 AW9523实际项目中的注意事项
AW9523有几个坑必须提醒。
第一是默认状态不是推挽。芯片上电后是开漏模式,直接驱动LED会发现亮度不对劲或者干脆不亮,一定要按上面的配置流程把LED模式打开。
第二是中断脚要加外部上拉。INT引脚是开漏输出,必须接上拉电阻到供电电压,否则电平不确定。我第一次画板子就忽略了这一点,中断偶尔触发偶尔不触发,排查了半天。
第三是地址引脚不能悬空。AW9523有4个地址引脚,数据手册要求必须接高或接低,悬空会导致地址不稳。而PCF8574的地址引脚可以悬空默认有内部下拉,习惯画PCB的人容易想当然。
7. Linux和软件I2C场景下的扩展器调试技巧
7.1 在Linux用户态直接操作IO扩展器
不少嵌入式Linux项目也面临GPIO不够用的问题,比如RK3229这种主控芯片,原生的GPIO被系统占用了一堆,想多控制几个设备还得靠扩展器。Linux内核里已经有PCF857x、MCP23017等驱动,设备树配置好之后,扩展器引脚会直接映射成gpiochip,应用层用libgpiod操作即可。
调试阶段最实用的是i2c-tools工具包里的i2cdetect,扫描总线上挂载的设备地址:
i2cdetect -y 1输出如下说明0x20地址上有PCF8574:
0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --遇到扫描不到设备的情况,优先检查:
- 上拉电阻是否焊接,以及阻值大小;
- SCL和SDA是否接反;
- 地址引脚配置是否和代码一致;
- 目标设备供电是否正常。
7.2 软件模拟I2C的时序要点
很多低成本方案用GPIO模拟I2C,尤其是BH1750这类对时序要求不高的传感器配合软件I2C跑得很顺。但用软件模拟I2C驱动PCF8574这类扩展器时,时延参数特别关键。
I2C协议中起始条件要求SCL高电平时SDA拉低,然后SCL再拉低;时钟高电平的保持时间、数据建立时间都有最小要求。如果用GPIO模拟且不加延时,时钟频率可能冲到1MHz以上,部分扩展器跟不上,数据出错。
实测下来,软件模拟I2C的GPIO翻转之间建议加至少1us的延时,总体时钟频率控制在200kHz以内,稳定性和摸查成本都比较好。注意GPIO配置为开漏输出模式,并且在外部加上拉电阻,不要用推挽模式去模拟I2C——否则多个设备挂同一总线时,电平冲突会损坏端口。
7.3 快速排查:I2C总线被拉死怎么办
实际调试中最高频的故障是总线被拉死,现象是SDA或者SCL始终为低电平,i2cdetect直接报总线错误。原因通常是以下几种:
- 某个从设备地址冲突,同时响应导致总线混乱;
- 设备供电异常,从设备闩锁进入异常状态把SDA拉低;
- 通信过程中主控复位,I2C时序停在半截,从设备在等待后续时钟。
解决办法排优先级来试:先把所有从设备断开,只留扩展器,看总线是否恢复;再检查每个设备的上拉电阻,确保所有上拉都接到电源;最后,如果主控支持时钟恢复,发9个SCL脉冲把所有从设备的状态机复位。
软件上还有一个临时办法:每包数据发送前,先把SDA和SCL都设置为输出高,手动产生几个脉冲,把从设备状态机踢回IDLE状态。这在Linux和裸机代码里都适用。
8. 选型决策树与常见问题速查表
8.1 按项目需求快速定位芯片
我把这几年总结的选型逻辑整理成一套决策判断链,拿去套用基本不会跑偏。
先看功能需求,需要输出大于8路且同时有输入需求,选择IO扩展器。然后看是否有LED呼吸、PWM调光需求,有则优先AW9523,省MCU资源。如果没有特殊需求,再看是否需要中断唤醒,需要则选MCP23017或TCA9535,不需要则PCF8574够用。最后看IO总数,超过16个IO需求就评估多颗扩展器方案,或者换更大引脚的主控。
因为芯片本身单价差异不大,MCP23017裸片价通常也就PCF8574的三四倍,但开发工时、调试难度的差别远比芯片差价大。关键还是一句话:优先把方案做简单。
8.2 问题排查速查表
实际项目中踩过的坑整理成表格,方便现场排查时对照:
| 故障现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| i2cdetect扫描不到设备 | 上拉电阻没焊接或阻值过大 | 检查SCL/SDA对电源的4.7k~10k电阻,示波器看SDA/SCL波形 |
| 地址对了但读写无响应 | 地址引脚接错或悬空 | 用万用表量A0/A1/A2的电平,确保和软件配置一致 |
| 读按键返回值恒为0 | 读之前没写0xFF释放引脚 | 在读操作前先写0xFF,把手动输出配置排除掉 |
| 数据偶尔错一两个bit | 总线电容过大、上拉电阻太大 | 换2.2k或1k上拉,降低I2C速率到100kHz验证 |
| 扩展器引脚输出高电平异常 | 开漏结构没加上拉或上拉阻值大 | 推挽输出芯片检查方向寄存器,开漏芯片补外部上拉 |
| 中断不触发 | 中断脚上拉缺失或中断寄存器没使能 | 检查INT引脚外部上拉,逐个核对中断使能寄存器 |
| 睡眠功耗偏高 | 上拉电阻太小导致静态电流过大 | 总线空闲时关闭I2C、引脚输出高,优化整机功耗 |
| 总线上多个设备互相干扰 | 从机地址重叠 | 列出总线地址分配表,逐颗核对地址引脚组合 |
8.3 关于电平匹配
扩展器供电3.3V时,IO输出高电平为3.3V。如果后级负载需要5V电平,不能直接驱动,TCA9535这类芯片虽然供电电压范围宽,但IO高电平不会超过供电电压。做法是:输出控制的负载如果兼容3.3V逻辑就直接接,不兼容就加三极管或MOS管做电平转换。
很多人在这里翻了车:3.3V供电的PCF8574拉高LED是没问题的,LED压降1.8V加上限流电阻,3.3V减去后还有几毫安的亮度,但用同样的接法驱动5V继电器就不行——继电器线圈需要5V驱动,3.3V可能不够。控制这类负载建议改输出开漏+外部上拉到5V的方式,或者直接换5V供电的扩展器,但要注意此时I2C总线上拉也要接到5V,而主控是3.3V的话要把I2C做电平转换。
9. 从扩展器到整个GPIO资源规划的思路
项目做多了以后我发现,扩展器只是手段,真正的核心是资源规划。
画原理图之前,先把整板所有需要控制的信号列个清单,按性质分类:哪些是固定输出(LED、继电器、使能脚),哪些是固定输入(按键、拨码开关、传感器状态),哪些是双向且需要频繁切换(数据/控制复用脚)。分类之后,先用主控的原生GPIO去匹配那些时序敏感的、需要中断的、需要模拟特殊协议的信号,把类型固定、数量大的用扩展器解决,这样主控GPIO的压力会小很多。
再一个经验是别把扩展器放在I2C总线的末端,如果总线上还有需要高速通信的传感器,扩展器这种低速设备放在总线的中间或者靠近主控一端,总线的电容分布会更均匀,信号质量更好。
我以前在画ESP32的开发板时就吃过亏,把两颗PCF8574挂在总线最远端,总线上还有一个IMU,结果IMU的400kHz数据偶尔被扩展器拉歪,后来把扩展器移到总线上部,问题消失。
10. 一个实际案例:ESP32上的I2C IO扩展实战记录
光说理论显得虚,分享一个最近给客户做的实际项目。客户需要做一块控制板,主控是ESP32,IO要控制3路步进电机驱动器的方向/使能脚(6个输出)、4路按键输入、2路LED指示,外加一个OLED显示屏。原生GPIO只有十几个,上面需求一共占了12个,还要留4个给下载和通讯,彻底不够。
方案:OLED走硬件I2C总线的0x3C地址,扩展器挂两颗PCF8574,第一颗地址0x20管按键和LED,第二颗地址0x21管电机驱动器控制脚。
实际运作中有一个细节值得分享:电机驱动器控制脚在系统初始化时必须拉到确定电平(比如使能为低),但PCF8574上电默认全高,所以电路上我把使能脚设计成低电平有效,PCF8574默认高电平正好让电机驱动器使能脚处于禁止状态,避免了上电瞬间电机乱动。这就是前面说的“利用上电默认状态做安全设计”,选型时就要想清楚。
ESP32这边,Arduino环境直接能用Adafruit的PCF8574库,初始化后就是一个digitalRead/digitalWrite的透明接口。实际测试延时大概在几十微秒级别,对低速控制完全够用。
如果要用ESP32的硬件I2C,记得查一下自己的板子用的Wire.begin的引脚编号。有些老代码里写Wire.begin(4, 5),但新版Arduino-ESP32的引脚分配可能不是这个,接错了总线直接扫不到设备。
11. 最后再聊几句体己话
最开始做扩展器选型时,我也被五花八门的型号搞晕过,后来定了一个原则:选最成熟的,不选最新的。PCF8574技术老、资料全、生态大,什么问题都能搜到答案。AWS逻辑里那些新功能是加分项,但加分项只有在基础需求满足之后才有意义。
还有一点是调试顺序。焊接完板子不要急着写驱动,第零步永远是裸板量硬件:通上电,用万用表量扩展器VCC和GND,确认SCL/SDA的电压都在高电平(一般3.3V或5V),然后示波器点一下扩展器SCL引脚和主控发出数据时是否有波形。这一步没问题了,再跑软件扫描。
希望大家做完这个项目,不只是拿到一块能用的板子,而是真正把I2C这套总线协议、扩展器选型逻辑和调试方法沉淀下来。以后不管是I2C扩展器还是别的I2C从设备,只要总线通信有问题,都能靠这套思路自己去定位,而不至于到处搜帖子看代码。
12. 扩展器常见参数对比表汇总
把目前最常用的几款Io扩展器再汇总成一张表,方便画板前对照查阅。
| 参数 | PCF8574 | MCP23017 | TCA9535 | AW9523 |
|---|---|---|---|---|
| I2C地址范围 | 0x20~0x27 | 0x20~0x27 | 0x20~0x27 | 0x40~0x4F(16个组合) |
| 输出模式 | 开漏 | 推挽 | 推挽 | 推挽/开漏可选 |
| 中断引脚 | 无 | 2个 | 2个 | 1个 |
| 输入内部上拉 | 无 | 有(可配置) | 无 | 有(可配置) |
| LED驱动 | 无,需外接限流电阻 | 无,但灌电流能力强 | 无 | 有,内置PWM呼吸灯 |
| 电压范围 | 2.5V~5.5V | 1.8V~5.5V | 1.65V~5.5V | 1.8V~5V |
| 常用封装 | SOIC-16/TSSOP-16 | SOIC-28/SSOP-28 | SOIC-16/TSSOP-16 | QFN-24 |
选型时先把这张表存下来,对照自己的需求圈定1~2款,再去看完整的数据手册。
13. 关于驱动库与代码移植的补充
很多朋友会问要不要用现成的驱动库。我的建议:如果是快速原型验证,用现成的库完全没问题,省时间;如果是做量产产品,驱动代码建议自己写,因为现有库通常做了太多兼容判断,效率不高,而且有些库的接口和硬件平台绑定,移植到新主控反而处处受限。
自己写I2C扩展器驱动其实很薄很薄。以PCF8574为例,核心就两个函数:写一个字节、读一个字节。底层I2C如果自己的工程里已经有稳定驱动,那扩展器驱动就是上面代码里的几行而已。与其花时间去理解一个几百行的库,不如看手册搞明白芯片内部寄存器,自己动手写,20分钟就搞定。
另外提醒一下用HAL库的朋友,STM32CubeMX生成的I2C代码默认是轮询模式,配合扩展器读写没问题,但禁止在中断回调函数里直接调用HAL_I2C_Master_Transmit,I2C通信时序会被中断抢断,导致错误。我自己遇到过几次HAL库标志位卡死的情况,换为定时器轮询方式之后世界清净了。
写到最后,还是那句话,工具是拿来用的,不是拿来供的。I2C IO扩展器很简单,但用好的前提是对芯片特性、总线机制和你的应用场景都心里有数。希望大家看完这篇文章,能少踩几个我当年踩过的坑,做板子顺顺利利。