
干嵌入式这行GPIO不够用是个永远绕不开的老大难。MCU选型时引脚数差两三个要么被迫上大封装要么加一颗74HC595再不行只好换芯片重新画板子。我自己的习惯是只要系统里已经有I2C总线遇到GPIO不够第一个想到的就是PCA9555这颗16位I2C GPIO扩展芯片两颗芯片的占用面积只有SOP-24的一半不到却换来16个可配置输入输出的IO口在当下的PCB密度下非常实用。这篇文章就把这颗芯片从管脚、寄存器到Linux设备树驱动再到实际调试踩坑一次性讲透。做驱动开发久了你会发现芯片本身倒是简单真正吃时间的是那些藏在数据手册角落的细节地址怎么拼、上拉电阻该选多大、中断脚为什么总是不触发。这里我把PCA9555的完整原理和项目实战经验整理出来涉及I2C时序、寄存器读写、Linux内核GPIO子系统、裸机驱动编写以及常见问题排查希望给正在做IoT网关、工控板或者MCU小项目的朋友省点时间。1. PCA9555到底解决什么问题1.1 为什么MCU的GPIO总是不够用现在的智能硬件恨不得把所有器件都挂在主控上LED指示灯、按键、拨码开关、继电器、蜂鸣器、LCD背光、传感器使能脚……每个功能动辄两三个引脚。以STM32F103RCT6为例51个GPIO看着不少可一旦接了屏幕16位并口要20根线加一两个外设立刻捉襟见肘。更麻烦的是很多MCU的某些IO自带复用功能比如串口、I2C、PWM输出引脚一旦被占死想改功能得重新做板子。GPIO扩展方案其实不少常见的有这几个74HC595串行移位寄存器只出不进能扩展输出但读输入状态非常别扭需要另外加并行输入芯片。74HC165并行输入转串行输出只进不出适合按键矩阵这类纯输入场景。74HC4051等模拟开关本质是通道选择不是真正的GPIO扩展。MCP23017Microchip的16位I2C GPIO扩展和PCA9555功能几乎一样。PCA9555NXP家的16位I2C GPIO扩展带中断输出输入端/输出端可独立配置。相比之下PCA9555的优势在于双向IO可配置、带极性反转寄存器、硬件中断引脚、I2C接口只占两根线。板子上省下的走线空间和MCU引脚资源对小型化设计很有价值。很多国产工控主板和边缘网关里你都能看到它的身影专门用来接前面板的LED和按键。1.2 PCA9555在系统中的典型位置一个很常见的应用场景是这样的主控通过I2C总线挂载一颗PCA95558个引脚接LED指示灯灌电流驱动8个引脚接按键输入再用INT引脚把按键变化以中断方式通知主控。这样一来原先需要16个GPIO加一个外部中断的功能最终只占主控两个I2C引脚加一个中断引脚。PCA9555的电源范围是2.3V到5.5V所以3.3V和5V系统都能用。它挂在I2C总线上时地址由A0、A1、A2三个硬件引脚的电平决定一根I2C总线上最多可以并联8颗PCA9555也就是最多扩展128个IO。这8颗芯片地址区分很干净不会冲突前提是硬件上把地址引脚接对。1.3 PCA9555与PCF8574、MCP23017的选型差异经常有人问PCA9555和PCF8574怎么选。PCF8574是8位扩展芯片更便宜但走的是准双向IO结构每个引脚内部有一个弱上拉电流源读输入时直接读就行写输出时拉低靠管子、拉高靠内部上拉驱动能力很弱。PCA9555则是有独立的方向配置寄存器每个引脚能配置成输入或者开漏输出输出低电平时灌电流能力明显更强。MCP23017和PCA9555定位几乎一致都是16位、I2C、带INT。区别在于MCP23017的I2C地址引脚有电平比较逻辑除了高和低还能接在SDA或SCL上形成额外的地址组合最多能扩展更多地址PCA9555就纯粹是高或低两种状态8个地址。选哪个看供应链习惯两者在Linux内核里都有原生驱动支持器件树节点名不一样而已。2. 芯片内部原理拆解从引脚到寄存器2.1 I2C地址是如何拼出来的PCA9555的I2C从机地址是7位固定部分为0100对应二进制是0b0100后面跟硬件地址位A2、A1、A0。三个硬件引脚接地时7位地址是0b0100000也就是0x20。在Linux设备树或者裸机驱动里我们写的设备地址默认就是7位地址加读写位之前的那个7位地址。以实际换算来举个例子A20A10A01时7位地址是0b01000010x21A21A10A00时地址是0b01001000x24。硬件上这三个脚可以直接接VCC或GND也可以串一个电阻再接但要注意绝对不能悬空悬空导致的电平不确定会让I2C扫描出乱跳的地址那种问题查起来非常头疼。2.2 寄存器地图与控制逻辑PCA9555内部有8个8位寄存器分为两组分别对应端口0P07~P00和端口1P17~P10寄存器地址功能说明输入寄存器00x00端口0当前电平只读输入寄存器10x01端口1当前电平只读输出寄存器00x02端口0输出锁存值可读写输出寄存器10x03端口1输出锁存值可读写极性反转寄存器00x04端口0输入极性是否反转1为反转极性反转寄存器10x05端口1输入极性是否反转1为反转配置寄存器00x06端口0方向1为输入0为输出配置寄存器10x07端口1方向1为输入0为输出这个寄存器地址排布值得好好理解。输入寄存器和输出寄存器是分开的意味着输出模式下也能读到引脚的实际电平很多GPIO控制器没有这个能力PCA9555可以这对做回读诊断很有用。极性反转寄存器只对输入模式生效如果某路按键是低电平有效给它配0x01反转极性软件里读到的就是高电平表示按下省去逻辑取反的麻烦。配置寄存器上电默认值是0xFF也就是所有引脚默认都是输入。这一点非常重要后面讲驱动时会反复提到。上电瞬间引脚处于高阻状态如果外部设备没有额外上下拉引脚电平是浮动的。2.3 开漏输出为什么必须配上拉电阻PCA9555每个引脚输出级是开漏结构也就是说内部只有一个下拉的NMOS管。引脚输出低电平时NMOS导通电流从外部灌进芯片这叫灌电流输出高电平时NMOS截止引脚并不主动拉高而是靠外部上拉电阻把电平拉高。这个设计和MCU内部常见的推挽输出很不一样。推挽输出上面是PMOS管输出高电平时能主动供电流而开漏输出高电平时电流能力几乎为零。这就解释了为什么你在很多原理图上看到PCA9555的IO引脚外面都要挂上拉电阻尤其是驱动LED这种负载时更常见的是LED阳极接电源、阴极接PCA9555引脚PCA9555引脚输出低电平把LED点亮。I2C总线本身也是开漏结构所有从机设备的SDA和SCL都是开漏所以总线上必须有上拉电阻。为什么不用推挽输出因为I2C是主从多设备共享两根线如果两个设备一个想发高、一个想发低推挽输出会直接短路。开漏加外部上拉任意设备都可以把线拉低谁都不拉低时线自然恢复高电平靠这个机制实现了安全的“线与”。PCA9555的GPIO引脚沿用开漏结构也是因为它的应用场景经常是多路复用避免电平打架。2.4 中断输出INT的工作机制PCA9555的INT引脚是一个真正的硬件中断信号它也是开漏输出。任意配置为输入模式的引脚电平发生变化INT引脚就会拉低主控读取该端口对应的输入寄存器后INT引脚自动释放恢复高电平。这个机制非常适合按键、门磁、拨码开关这类状态变化型输入。注意三点细节第一只有输入引脚发生变化才会触发INT输出引脚的电平变化不会。第二如果多个引脚同时变化INT会持续拉低直到主机把两个输入寄存器全部读一遍所以中断服务程序里不能只读一半。第三INT引脚本身没有内部上拉外部必须接一个上拉电阻到VDD一般10k即可否则你永远检测不到低电平。3. 驱动开发实战Linux平台从设备树到GPIO子系统3.1 设备树如何描述一个PCA9555Linux内核里gpio-pca953x.c驱动支持PCA9555这一整个家族包括PCA953x、PCA955x系列设备树中compatible字段和reg字段配置好之后驱动会注册成一个标准的gpio_chip系统中其他驱动和用户态程序可以像使用普通GPIO一样使用它。下面是我在实际项目里用过的I2C1总线设备树节点写法i2c1 { status okay; clock-frequency 100000; pca9555: gpio-expander20 { compatible nxp,pca9555; reg 0x20; gpio-controller; #gpio-cells 2; interrupt-parent gpio0; interrupts 5 IRQ_TYPE_LEVEL_LOW; gpio-line-names led_red, led_green, btn_mode, led_blue, relay1, relay2, buzzer, sw_dip1, sw_dip2, sw_dip3, sw_dip4, rs485_dir, spare1, spare2, spare3, spare4; }; };这里重点看gpio-line-names的作用。这个属性不仅是为每个引脚起个名字方便调试更重要的是把引脚的用途写进了内核的GPIO描述符系统当使用libgpiod工具操作时可以直接用“led_red”这样的语义化名称去定位引脚不需要再记第几个GPIO对应什么功能。项目大了以后这个价值非常大。interrupt-parent和interrupts字段表明INT引脚接在主控的GPIO0_5上触发方式是低电平。PCA9555的INT是持续低电平状态所以用IRQ_TYPE_LEVEL_LOW比边沿触发更稳妥因为在中断处理函数里如果读取寄存器不及时电平会一直保持低。3.2 内核配置与驱动注册使用PCA9555前要确认内核开启了GPIO_PCA953X相关配置。在menuconfig里路径一般是Device Drivers - GPIO Support - PCA953x, PCA955x, and MAX7310 GPIO expansion。这个选项打开后上述设备树节点在系统启动时就会自动匹配驱动。驱动注册完成后系统里会出现一个名为gpiochipN的节点。用下面的命令查看cat /sys/kernel/debug/gpio输出信息里你会看到类似“gpiochip2: GPIOs 512-527, parent: i2c-1, can sleep: yes”这样的行。这个“can sleep: yes”很关键——I2C总线上的GPIO操作是可能睡眠的所以在内核里使用它时不能用在原子上下文比如自旋锁保护的临界区必须使用gpiod_set_value_cansleep这类带睡眠许可的函数。这是新手写驱动最容易翻车的地方。PCA9555在I2C总线上扫描到的地址如果和设备树里reg不一致内核日志会报错。排查时用i2cdetect扫一下实际地址再和设备树对比基本一眼就能定位。3.3 用户态操作sysfs和libgpiod两种方式旧内核里我们习惯用sysfs方式导出GPIOecho 520 /sys/class/gpio/export echo out /sys/class/gpio/gpio520/direction echo 1 /sys/class/gpio/gpio520/value但sysfs接口在内核5.x之后已标记为废弃新项目建议直接用libgpiod工具链。以pca9555芯片在系统里对应gpiochip2为例查看引脚状态gpiodetect gpioinfo gpiochip2 gpioget gpiochip2 3 gpioset gpiochip2 01gpioinfo命令会直接把每个引脚对应的gpio-line-names显示出来对着名字操作不容易出错。写代码时优先用libgpiod的API请求一个描述符后可以监听line的上升沿、下降沿事件这对按键和脉冲采集比裸读GPIO可靠得多。3.4 基于MCU的裸机驱动核心代码设计如果你用的主控芯片没有Linux比如STM32、ESP32或者国产小家电主控裸机驱动也很简单核心就是I2C读写函数。下面是一段我在STM32上用的精简寄存器读写代码#define PCA9555_BASE_ADDR 0x20 #define PCA9555_REG_IN_PORT0 0x00 #define PCA9555_REG_OUT_PORT0 0x02 #define PCA9555_REG_POL_PORT0 0x04 #define PCA9555_REG_CFG_PORT0 0x06 static uint8_t pca9555_read_reg(uint8_t addr, uint8_t reg) { uint8_t val 0; i2c_start(); i2c_write_byte((addr 1) | 0); // 写方向选中寄存器 i2c_write_byte(reg); i2c_start(); // 重复起始 i2c_write_byte((addr 1) | 1); // 读方向 val i2c_read_byte(I2C_NACK); i2c_stop(); return val; } static void pca9555_write_reg(uint8_t addr, uint8_t reg, uint8_t val) { i2c_start(); i2c_write_byte((addr 1) | 0); i2c_write_byte(reg); i2c_write_byte(val); i2c_stop(); } void pca9555_init(void) { // 端口0低4位输出高4位输入端口1全部输出 pca9555_write_reg(PCA9555_BASE_ADDR, PCA9555_REG_CFG_PORT0, 0xF0); pca9555_write_reg(PCA9555_BASE_ADDR, PCA9555_REG_CFG_PORT0 1, 0x00); // 初始输出全为低 pca9555_write_reg(PCA9555_BASE_ADDR, PCA9555_REG_OUT_PORT0, 0x00); pca9555_write_reg(PCA9555_BASE_ADDR, PCA9555_REG_OUT_PORT0 1, 0x00); }这段代码最需要关注的是读寄存器时用的“重复起始”操作。有些时候读写方向切换从机芯片在收到REPEATED START后才会重新进入读模式如果中间用一个STOP和START代替存在极小的时序风险特别在400kHz快速模式时更容易暴露所以这里我统一用重复起始。还需要注意PCA9555没有自动递增寄存器地址的功能要按字节地址逐个读。如果你一次要读16个引脚状态就得发起两次读操作。想要提高效率的话扫一遍输入寄存器很快两次I2C读也就十几微秒的事绝大多数应用足够。4. I2C通信与GPIO模式的深度细节4.1 I2C时序与波形要点详解I2C通信的底层核心其实只有两个关键点数据什么时候算有效什么时候算传输结束。数据有效规则是在SCL高电平期间SDA线上的电平必须保持稳定只有SCL为低电平期间SDA才被允许变化。这个规则一旦违背接收方会把它误判为起始或停止条件整个通信直接乱掉。用逻辑分析仪抓信号时重点看起始条件SCL高电平时SDA从高跳低、停止条件SCL高电平时SDA从低跳高、ACK应答位。PCA9555处于标准模式和快速模式都能工作标准模式最高100kHz快速模式最高400kHz。我在实际项目中主频设置在100kHz这个频率下布线的容错性更好抗干扰能力更强如果不是板内短走线而是通过排线引到面板上建议老老实实用100kHz。SPI和I2C的选择上有个经验如果只是单个从设备并且对吞吐有要求SPI更简单快如果设备多、需要的引脚少I2C这种两根线挂一堆从机的方式优势很大。PCA9555这类GPIO扩展芯片本质上对带宽要求极低按键扫描和LED控制偶尔读写一两个字节100kHz已经绰绰有余。4.2 上拉电阻的取值问题I2C上拉电阻的选择在PCA9555相关设计里经常出问题。先说结论100kHz标准模式总线设备数量不多时4.7k到10k都行400kHz快速模式建议2.2k总线挂载超过4个设备时等效上拉电阻会变小可以降到1k但1k以下要小心。上拉电阻过大信号上升沿太慢波形圆头圆脑SCL高电平时间不足从机采样就会出错上拉电阻过小挂载设备的总线变得更难拉低或者静态功耗偏大同时也可能让主控I2C端口驱动能力不足导致总线卡死、读回数据一直是0xFF。我在一块板子上吃过亏因为从别的设计里抄了一个220欧的上拉电阻结果I2C通信完全不通示波器一看SCL低电平都拉不到0.4V以下后来换成2.2k才恢复正常。另外一个容易忽略的点是如果MCU内部I2C引脚的上拉电阻已经打开和外部上拉并联后会降低总等效电阻。设计时要么关内部上拉要么把外部上拉相应调大。不过PCA9555只是I2C从机它自己内部是没有上拉的只有总线外部的主控电路负责提供上拉。4.3 GPIO模式映射PCA9555虽然没有8种模式但要注意方向切换STM32的GPIO有输入、输出、复用、模拟4大类细分下来8种模式。PCA9555没有这么丰富它的模式只有输入和输出输出只有开漏一种结构。但方向切换这个行为要特别重视。PCA9555的配置寄存器是可以在运行时修改的也就是说一个引脚可以动态地从输入切到输出。但切换的时候输出寄存器里之前的残留状态会立刻生效。举个例子某个引脚之前一直作为输入输出寄存器里恰好是1你把配置寄存器改成输出后这个引脚会瞬间以开漏方式放出高电平靠外部上拉如果外部接的是继电器就可能误动作。所以动态切换方向前一定要先把输出寄存器写到目标安全电平再切方向。4.4 与PMBus的关联别被协议名字吓到PMBus和I2C说到这里顺便提一嘴因为很多人刚看到PMBus时会疑惑它和I2C什么关系。PMBus是以I2C为物理传输层定义了电源管理相关的命令集和寄存器规范通常用于电源模块的监控和配置。PCA9555这种纯粹的数字IO芯片走的还是原生I2C读写不涉及PMBus命令。但如果你在电源板上看到PMBus接口底层时序同样遵循I2C的标准规则调试思路和工具是完全通用的。5. 实战调试与问题排查实录5.1 用i2c-tools快速验证硬件是否正常芯片焊接上电后第一件事不是写驱动而是确认I2C总线上能否正确枚举到设备。以Linux系统I2C总线1为例i2cdetect -y -r 1正常情况会看到类似下面的输出地址0x20处显示“20”0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --这里要提醒一句i2cdetect的扫描方式是依次访问总线上的每个地址。对大多数24C02这类的EEPROM没问题但有个别器件对访问地址比较敏感扫描会影响器件状态。所以更稳妥的办法是用i2cget直接读目标寄存器i2cget -y 1 0x20 0x07返回0xFF表示端口1配置寄存器默认值是全输入符合上电默认状态。再读输入寄存器i2cget -y 1 0x20 0x00就能看到端口0当前电平。把外部按键按下再读一次电平变化说明硬件通路OK。手动配置输出测试# 将端口0配置为全部输出 i2cset -y 1 0x20 0x06 0x00 # 端口0输出0x55 i2cset -y 1 0x20 0x02 0x55用万用表或者示波器量引脚电平应该能看到P00、P02、P04、P06为高其他为低。这套方法做产线自检也够用。5.2 扫描不到设备的几个原因最常见的是地址不对。A0、A1、A2三个引脚的电平和你的软件配置不一致比如硬件全接地但你写成0x27扫描自然找不到。还有就是地址线虚焊这种情况最坑——有时能扫到、有时扫不到而且扫描地址可能随机变化。其次是I2C总线上拉电阻没焊或焊错。没有上拉电阻时SDA和SCL无法回到高电平主控根本测不到有效通信。量一下SCL对地电压如果一直保持低电平首先查上拉。再有就是芯片供电没起来。PCA9555的电源脚VDD如果没焊好从机不工作自然扫不到。用万用表直接量芯片电源脚对地电压。还有一个检查点是I2C地址冲突。总线上已经有其他设备占了同一个地址时两者同时应答通信数据混乱。把总线上其他设备临时移走再扫描一次就能确认。5.3 读回来的电平全是1或者全是0现象是i2cget读输入寄存器返回0xFF或者无论如何变化都是0x00。如果是0xFF通常是引脚实际悬浮、被外部上拉拉高了但PCA9555配置寄存器却是输入状态默认读回高电平符合事实。如果是0x00且怎么按按键都没反应优先检查I2C通信时序是否不稳定比如上拉电阻过小导致信号占空比畸变或者信号线太长有严重的振铃。这个场景可以配合逻辑分析仪来看波形重点确认ACK位是否正常。如果SDA线上没有出现ACK低电平脉冲说明从机根本没正确应答。逻辑分析仪抓取一次i2cget操作对照数据手册的时序图逐位核对基本十分钟内能定位问题。5.4 GPIO输出驱动能力不足PCA9555的输出是开漏结构输出高电平时不主动供电流需要外部上拉。很多人第一次用它驱动LED时习惯性地把LED阳极接芯片引脚、阴极接地结果LED死活不亮。这是因为PCA9555的输出高电平本身电流能力极弱。正确的接法是LED阳极接电源、阴极接PCA9555引脚输出低电平点灯也就是灌电流方式。PCA9555的灌电流能力比拉电流强得多datasheet里标注的输出低电平灌电流能力是十几毫安到几十毫安的量级但别把每个脚都跑满整个芯片的功率耗散也要留余量。如果是继电器或者电磁阀这类感性负载绝对不能直接接在脚上要加三极管或者ULN2003驱动。5.5 中断触发不了或者频繁误触发先说触发不了。检查INT引脚的硬件连接有没有接上拉电阻是否连接到了主控的正确中断输入设备树里中断触发类型是否和实际匹配。PCA9555的INT是电平触发型低有效信号中断服务函数里读完输入寄存器就会自动释放所以设备树里用IRQ_TYPE_LEVEL_LOW是最合适的如果错配成边沿触发很可能漏掉中断。频繁误触发的情况多半是按键没有做去抖处理。PCA9555只检测电平变化机械按键在按下的几十毫秒内会产生大量抖动INT引脚会被反复拉低。解决办法是在中断服务函数里加软件延时候再读数或者外层用内核的debounce机制。裸机环境建议加一个状态机连续几次读到相同电平才算真正的按键事件。5.6 I2C总线挂多个PCA9555时的注意点一根总线上挂了多颗PCA9555时地址分配是A0-A2的排列组合这一点设计原理图时就要规划好。我的习惯是从0x20到0x27按顺序分配并且把每颗芯片对应的功能区域写清楚避免后续软件和硬件对不上。多设备挂载还有一种典型坑I2C总线长度太长设备分散在不同板卡上通过连接器相连。此时总线的寄生电容变大信号上升沿变慢需要适当降低I2C时钟频率并且关注上拉电阻的匹配。如果跨板连接长度超过20厘米我更建议用PCA9555A或者加电平转换芯片或者直接把I2C转成差分信号来传输。6. 驱动开发中容易忽略的要点整理以上内容较多这里把一些特别容易踩、但常规文档里不怎么强调的要点集中整理一下算是我个人做PCA9555项目时反复提醒自己的几条第一上电默认全是输入、没有内部上拉。这意味着在驱动初始化完成之前PCA9555所有引脚都是高阻状态外部没有下拉的话电平会悬浮接在引脚上的负载可能随机动作。尤其是接继电器、三极管基极这类对电平敏感的电路硬件上一定要加上下拉电阻保证上电瞬间电平确定。第二Linux下使用的设备树节点里reg字段是7位I2C地址不是带读写位的8位地址。很多从单片机能转过来的同学习惯性地写0x40结果驱动死活匹配不上。PCA9555地址0x20对应的是7位地址注意区分。第三I2C GPIO扩展芯片的GPIO操作会睡眠内核中不能在中断上下文直接操作需要借助线程化中断或者workqueue。一个直观的验证是你在中断处理函数里调用gpiod_set_value_cansleep内核会直接告警实际表现为调度时出现BUG。第四PCA9555的极性反转寄存器只对输入模式生效。做按键板时把所有低电平有效的按键统一配置成极性反转主控读回来的状态就是1表示按下逻辑判断上能省去一堆取反操作代码可读性也提升不少。第五逻辑分析仪是排查I2C问题最趁手的工具。我自己的行李箱里常年备着一个几十块钱的8通道逻辑分析仪配合开源的sigrok/PulseView软件抓I2C时序时直接按协议解析每一个字节、每一位都能看到比拿示波器一格一格量波形高效得多。我自己在实际项目中反复体会最深的一点是PCA9555这颗芯片本身并不复杂但它牵出的I2C时序、上拉电阻、开漏输出、Linux GPIO子系统、中断处理等这些知识点几乎是嵌入式开发绕不开的基本功。把这一颗小芯片吃透等于把整个I2C GPIO扩展这一类方案的套路都掌握了。后面不管换PCA9538、MCP23017还是国产替代IC无非是寄存器地址排列不同核心思路都是通用的。如果你正在做一个GPIO不够用的项目我建议从PCA9555入门先i2cdetect验证硬件、再设备树挂驱动、最后写一个按键LED联动的小用例一个晚上就能把整条链路跑通以后遇到类似的扩展需求基本心里有底。