
1. 项目概述与核心价值在嵌入式开发中尤其是使用像STM32、ESP32这类微控制器时GPIO通用输入输出引脚不够用是个老生常谈的问题。你可能遇到过这样的场景主控芯片的引脚已经用尽但还需要连接十几个LED指示灯、几个按键甚至还要驱动一些继电器或读取传感器状态。这时候I2C总线扩展器就成了你的“救星”。它就像给你的主控芯片增加了一个“外挂”的I/O端口芯片通过仅有的两根线SDA和SCL就能额外控制多达16个、32个甚至更多的数字引脚。今天要深入聊的就是这类芯片中非常经典且实用的一款——NXP的PCA9535A。PCA9535A是一款16位、低电压的I2C总线I/O端口扩展器自带中断输出功能。它的核心价值在于用极简的硬件连接两根I2C线一根中断线电源和地解决了微控制器I/O资源紧张的痛点。我曾在多个电池供电的物联网节点和工控模块上使用过它其宽电压范围1.65V至5.5V让它能轻松适配3.3V和5V系统而微安级的静态功耗对于追求长续航的设备来说更是至关重要。更妙的是它的中断功能你不需要让主控芯片不停地轮询Polling按键是否被按下或传感器状态是否改变PCA9535A会在输入端口状态变化时通过INT引脚主动“通知”主控这极大地降低了系统功耗并释放了CPU资源。接下来我会结合数据手册和实际项目经验带你从电路设计、寄存器配置到代码驱动彻底吃透这颗芯片。2. 芯片深度解析架构、引脚与核心特性2.1 内部架构与引脚定义拿到一颗芯片首先要看懂它的“身体构造”。PCA9535A通常有两种封装TSSOP24和HWQFN24。对于手工焊接或小批量生产TSSOP24是更友好的选择而HWQFN24无引线四方扁平封装则体积更小适合高密度贴片生产。抛开电源VDD和地VSS引脚其核心引脚可以分为三组I2C通信接口SDA串行数据线和SCL串行时钟线。这是芯片与主控对话的“嘴巴”和“耳朵”。地址选择引脚A0,A1,A2。这三个引脚决定了芯片在I2C总线上的“门牌号”。通过将它们接高电平VDD或低电平VSS可以设置从0x20到0x278位地址最低位为读写位共8个不同的器件地址。这意味着在同一组I2C总线上你最多可以挂载8颗PCA9535A从而将16个I/O口扩展为惊人的128个在实际布线时我习惯用0欧电阻或跳线帽来配置地址方便调试和后期修改。中断输出引脚INT。这是一个开漏输出引脚需要外接一个上拉电阻通常4.7kΩ到10kΩ到VDD。当任何配置为输入的端口状态发生改变时此引脚会被拉低向主控发出中断信号。这是实现低功耗事件驱动的关键。16个I/O端口P00到P07端口0P10到P17端口1。这16个引脚每一个都可以通过软件独立配置为输入或输出。注意INT引脚是开漏输出这意味着它只能主动拉低电平而不能驱动高电平。必须外接上拉电阻否则该引脚将无法产生有效的高电平信号导致主控无法检测到中断撤销状态。2.2 核心特性与电气参数解读数据手册里图表很多我们挑最关键的几个来看并理解其工程意义。1. 功耗特性图19, 20, 21这是低功耗设计的基石。从Supply current versus supply voltage供电电流 vs. 供电电压图表可以看出在VDD3.3V、常温下其工作电流IDD典型值仅在10μA左右。而待机电流IDD(stb)更是低至纳安级别约400-1400nA取决于电压。这意味着在电池供电的睡眠模式下这颗芯片本身几乎不耗电。在设计时如果你的系统对功耗极其敏感甚至可以完全切断其电源但通常没必要因为这点电流在大多数电池方案中已可忽略不计。2. 驱动能力图22, 23这是决定它能驱动什么负载的关键。芯片的每个I/O口在作为输出时都有一定的电流输出Source和灌入Sink能力。我们主要关注灌电流Sink因为它常用于驱动LED阴极接I/O口阳极通过限流电阻接VDD。在VDD5V时从I/O sink current versus LOW-level output voltage曲线可知当输出低电平VOL被拉高到0.3V时其灌电流Isink大约在30-50mA具体值随温度变化。但请注意这是单个引脚的极限值并且会导致输出电压升高。为了稳定和寿命我强烈建议将每个引脚的持续灌电流限制在10-15mA以内。数据手册也给出了VOL在10mA灌电流下的典型值在-40°C到85°C范围内VOL最大不超过0.4VVDD5V时这个压降对于驱动LED和逻辑电路是完全可接受的。在VDD3.3V时驱动能力会有所下降但驱动一个普通的5-10mA的LED依然绰绰有余。设计LED电路时务必计算好限流电阻R (VDD - Vf_LED - VOL) / I_LED。其中Vf_LED是LED正向压降通常1.8V-2.2VVOL可以保守取0.5V。3. 中断与端口时序图27, 28理解时序是稳定通信的保障。tv(INT)参数从端口变化到INT有效最大为1μs这意味着端口状态变化几乎能立即触发中断。trst(INT)参数从SCL信号到INT复位也最大为1μs这意味着主控在读取输入端口寄存器后中断信号能快速被清除。这些时间对于微控制器来说都是非常充裕的。在编写中断服务程序时一个标准的流程是检测到INT引脚低电平 - 通过I2C读取PCA9535A的输入寄存器 - 读取操作本身会清除中断标志INT恢复高电平- 处理读取到的数据。3. 寄存器详解与通信协议实战PCA9535A的所有行为都通过内部寄存器来控制。它共有6个主要的8位寄存器成对管理两个8位端口Port 0和Port 1。3.1 寄存器映射与功能寄存器对地址Hex名称功能描述复位后默认值输入端口0x00, 0x01Input Port 0, 1只读。反映I/O引脚上的实时逻辑电平无论引脚配置为输入还是输出。引脚电平输出端口0x02, 0x03Output Port 0, 1读写。当引脚配置为输出时向此寄存器写入值控制输出电平读取则返回上次写入的值。0xFF (高电平)极性反转0x04, 0x05Polarity Inversion 0, 1读写。控制输入端口寄存器的值是否取反。0不反转默认1反转。此设置不影响实际引脚电平只影响读取到的值。0x00 (不反转)配置寄存器0x06, 0x07Configuration 0, 1读写。最重要的寄存器。每一位对应一个I/O引脚0输出1输入。0xFF (全输入)关键点解析上电默认状态芯片上电或复位后所有I/O口默认被配置为输入模式Configuration0xFF且输出寄存器为高电平Output0xFF。这意味着如果你不进行任何配置所有引脚都呈现高阻输入状态。如果你希望某个引脚一上电就输出低电平驱动LED就必须先向配置寄存器写入0设为输出再向输出寄存器写入0。极性反转寄存器的妙用这个功能非常实用。例如你用一个引脚通过上拉电阻接按键按键另一端接地。通常按键未按下时读到的值是1高电平按下后是0低电平。但你的程序逻辑可能更希望将“按下”视为“1”有效。此时只需将该引脚对应的极性反转位设为1那么你从输入端口寄存器读到的值就会自动取反按键按下读到的就是1了简化了软件判断逻辑。输入端口寄存器的特殊性它反映的是引脚的实际电压。即使你将一个引脚配置为输出你仍然可以通过读取输入端口寄存器来获取该引脚上的实际电平这在开漏输出或检查外部短路时有用。3.2 I2C通信时序与数据帧分析PCA9535A严格遵循标准I2C和快速I2C400kHz协议。通信总是以一个**命令字节Command Byte**开始这个字节实际上就是写入到芯片内部的一个8位指针寄存器Pointer Register它决定了后续读写操作针对的是哪个寄存器。1. 写操作流程主控向PCA9535A写入数据假设我们要将Port 0配置为输出Configuration 0 0x00并让其所有引脚输出低电平Output Port 0 0x00。器件地址设为0x20A2A1A00。步骤1发送起始条件S和器件地址0x20 W0。步骤2发送命令字节0x06指向Configuration 0寄存器。步骤3发送要写入的数据0x00将Port 0全部设为输出。步骤4此时指针会自动递增Auto-Increment。这是一个非常方便的特性。这意味着我们不需要再次发送命令字节下一个字节会自动写入到下一个寄存器0x07即Configuration 1。如果我们连续发送多个数据字节指针会依次指向0x06, 0x07, 0x00, 0x01... 循环。步骤5发送第二个数据字节0xFF将Port 1保持为输入默认。步骤6发送停止条件P。步骤7开始新的写事务设置输出电平。发送起始条件、地址0x20、命令字节0x02指向Output Port 0、数据0x00、停止条件。波形示意简化S | 0x20 (W) | ACK | 0x06 | ACK | 0x00 | ACK | 0xFF | ACK | P S | 0x20 (W) | ACK | 0x02 | ACK | 0x00 | ACK | P在实际代码中许多I2C库函数支持连续写入多个字节上述步骤1-6可以合并为一次发送地址 [0x06, 0x00, 0xFF]。2. 读操作流程主控从PCA9535A读取数据读操作稍复杂因为它分为两个阶段设置指针和读取数据。假设我们要读取Port 0和Port 1的输入状态地址0x00, 0x01。阶段一设置指针写模式发送起始条件S。发送器件地址 写位0x20。发送命令字节0x00指向Input Port 0寄存器。不发送停止条件而是发送一个重复起始条件Sr。这是关键阶段二读取数据读模式发送器件地址 读位0x21。读取第一个字节来自Input Port 0主控回复ACK。读取第二个字节来自Input Port 1指针自动递增主控回复NACK非应答表示读取结束。发送停止条件P。波形示意S | 0x20 (W) | ACK | 0x00 | ACK | Sr | 0x21 (R) | ACK | [Data0] | ACK | [Data1] | NACK | P实操心得很多初学者的常见错误是在设置指针后直接发送了停止条件然后发起新的读事务。这样会导致指针复位读到的可能不是想要寄存器的数据。一定要使用“重复起始条件Repeated Start”来衔接写和读阶段。好在像Arduino的Wire库、STM32的HAL库等其beginTransmission、write、endTransmissionfalse和requestFrom函数组合通常已经帮你处理好了这个流程。4. 电路设计要点与PCB布局建议纸上谈兵终觉浅硬件设计才是真战场。根据我的踩坑经验以下几个细节决定了项目的成败。4.1 典型应用电路设计一个稳健的PCA9535A外围电路通常包含以下部分电源去耦在VDD和VSS引脚附近必须放置一个0.1μF的陶瓷电容并尽量靠近芯片引脚。如果电源线较长或噪声较大可以再并联一个10μF的钽电容或电解电容。这是保证芯片稳定工作的第一道防线。I2C总线SDA和SCL线需要上拉电阻。阻值根据总线电容和速度选择通常介于2.2kΩ400kHz总线短到10kΩ100kHz总线长或低功耗之间。即使主控内部有上拉也建议在总线上保留外部上拉电阻以确保电平稳定。中断引脚INT引脚是开漏输出必须接一个上拉电阻如4.7kΩ到VDD。此引脚连接到主控的一个具有外部中断功能的GPIO上。地址选择引脚A0,A1,A2不能悬空。必须通过电阻或直接连接到VDD或VSS来设定固定电平。我推荐使用0欧电阻或焊盘跳线方便调试时修改地址。I/O端口驱动LED当I/O口灌电流驱动LED时LED阳极接VDD通过限流电阻阴极接PCA9535A的I/O口。电阻计算R (VDD - Vf_LED) / I_LED。假设VDD3.3VVf2.0V期望电流I5mA则R (3.3-2.0)/0.005 260Ω取标准值270Ω。务必确保总电流不超过芯片极限数据手册有每引脚和总VDD/VSS电流限制。读取按键按键一端接I/O口另一端接地。I/O口配置为输入并使能芯片内部的上拉电阻这是PCA9535A的一个隐藏优势当引脚配置为输入时内部有一个约100kΩ的高阻上拉。这样按键未按下时读为高电平按下时为低电平。如果使用外部上拉内部上拉会自动断开。4.2 PCB布局与焊接注意事项从数据手册的“Soldering of SMD packages”章节可知PCA9535A是表面贴装器件需要回流焊。封装选择TSSOP24引脚间距为0.65mm对于有经验的爱好者或使用热风枪/焊台可以手动焊接。HWQFN24底部有散热焊盘需要更专业的回流焊设备手工焊接难度大不推荐初学者使用。PCB焊盘设计务必参考数据手册第33、34页的PCB footprint推荐图纸。特别是HWQFN24的封装中间的热焊盘Thermal Pad必须正确设计过孔和钢网开窗以确保良好焊接和散热。ESD防护虽然芯片有基本的ESD保护但在生产、拿取和焊接过程中仍需遵循静电防护规范佩戴防静电手环使用接地焊台。5. 软件驱动开发与代码实战理论最终要落地为代码。下面以常见的开发环境为例展示如何驱动PCA9535A。5.1 基础驱动函数基于模拟I2C或硬件I2C首先定义一些基础宏和函数。这里以C语言为例假设你已有基本的I2C读写函数I2C_WriteBytes,I2C_ReadBytes。#define PCA9535A_ADDR_BASE 0x20 // 基础地址A2A1A0000 // 假设A2,A1,A0均接地则写地址为0x40 (0x20 1) 读地址为0x41 // 许多库函数使用7位地址则直接传入0x20 // 寄存器指针地址 #define REG_INPUT_0 0x00 #define REG_INPUT_1 0x01 #define REG_OUTPUT_0 0x02 #define REG_OUTPUT_1 0x03 #define REG_POLARITY_0 0x04 #define REG_POLARITY_1 0x05 #define REG_CONFIG_0 0x06 #define REG_CONFIG_1 0x07 uint8_t PCA9535A_ReadRegister(uint8_t devAddr, uint8_t regAddr) { uint8_t data; // 先写指针再读数据使用重复起始条件 I2C_WriteBytes(devAddr, regAddr, 1, true); // 发送寄存器地址不发送停止位 I2C_ReadBytes(devAddr, data, 1); // 读取一个字节数据 return data; } void PCA9535A_WriteRegister(uint8_t devAddr, uint8_t regAddr, uint8_t data) { uint8_t buffer[2] {regAddr, data}; I2C_WriteBytes(devAddr, buffer, 2, true); // 发送寄存器地址和数据 } // 一次性读取所有16位输入状态 uint16_t PCA9535A_ReadAllInputs(uint8_t devAddr) { uint8_t data[2]; I2C_WriteBytes(devAddr, REG_INPUT_0, 1, true); // 设置指针到输入端口0 I2C_ReadBytes(devAddr, data, 2); // 连续读取两个字节 return (data[1] 8) | data[0]; // Port1为高8位Port0为低8位 } // 设置16位输出状态 void PCA9535A_WriteAllOutputs(uint8_t devAddr, uint16_t data) { uint8_t buffer[3] {REG_OUTPUT_0, (uint8_t)(data 0xFF), (uint8_t)(data 8)}; I2C_WriteBytes(devAddr, buffer, 3, true); } // 配置16位端口方向 (1input, 0output) void PCA9535A_SetPortDirection(uint8_t devAddr, uint16_t config) { uint8_t buffer[3] {REG_CONFIG_0, (uint8_t)(config 0xFF), (uint8_t)(config 8)}; I2C_WriteBytes(devAddr, buffer, 3, true); }5.2 完整初始化与使用示例假设我们用PCA9535A驱动8个LEDP00-P07并读取8个按键P10-P17。按键采用内部上拉按下为低电平。void PCA9535A_Init(void) { uint8_t devAddr 0x20; // 7位地址 // 1. 配置端口方向: Port0全输出Port1全输入 PCA9535A_SetPortDirection(devAddr, 0xFF00); // 低8位(P0)0x00(输出)高8位(P1)0xFF(输入) // 2. (可选)配置极性反转: 将Port1的输入极性反转这样按键按下读到的就是1 uint8_t polarityBuf[3] {REG_POLARITY_1, 0xFF}; // 只设置Port1反转 I2C_WriteBytes(devAddr, polarityBuf, 2, true); // 3. 初始化输出状态: 关闭所有LED (输出高电平因为LED阴极接IO) PCA9535A_WriteAllOutputs(devAddr, 0xFFFF); // 输出高电平LED灭 // 注意若LED阳极接IO则输出低电平点亮此处逻辑相反。 } // 主循环或中断服务函数中 void Handle_PCA9535A_Tasks(void) { static uint16_t lastKeyState 0; uint16_t currentKeyState; // 读取所有输入状态Port1的按键 currentKeyState PCA9535A_ReadAllInputs(0x20) 8; // 取高8位Port1 // 检查按键变化下降沿或上升沿 uint16_t keyPressed (lastKeyState ^ currentKeyState) currentKeyState; // 检测按下从1变0注意我们反转了极性 // 更通用的方法是直接比较因为极性反转后按下1释放0 // 假设我们想要检测按下事件从0变1 uint16_t keyChanged lastKeyState ^ currentKeyState; uint16_t keyPressedEvents keyChanged currentKeyState; // 位为1表示该按键从0变成了1按下 if (keyPressedEvents) { // 处理按键事件例如点亮对应的LED // 将按键事件映射到Port0的LED输出取反因为LED阴极接IO输出0点亮 uint8_t ledPattern ~(uint8_t)(keyPressedEvents); PCA9535A_WriteRegister(0x20, REG_OUTPUT_0, ledPattern); } lastKeyState currentKeyState; } // 中断服务函数示例 (如果使用INT引脚) void EXTI_IRQHandler(void) { // 假设INT接在某个外部中断引脚上 if(EXTI_GetITStatus(INT_PIN) ! RESET) { // 1. 读取输入寄存器以清除中断标志 uint16_t inputs PCA9535A_ReadAllInputs(0x20); // 2. 处理输入变化 Process_Input_Change(inputs); // 3. 清除外部中断标志 EXTI_ClearITPendingBit(INT_PIN); } }5.3 高级应用驱动多个器件与中断管理当总线上挂载多个PCA9535A时软件设计需要一些技巧。地址分配为每个芯片的A2,A1,A0引脚设置不同的电平分配唯一的I2C地址0x20-0x27。中断引脚合并多个PCA9535A的INT引脚可以**通过一个与门或直接线或**连接到主控的一个中断引脚。因为INT是开漏输出多个开漏输出直接连在一起并共用一个上拉电阻就实现了“线与”逻辑。只要任何一个芯片产生中断公共的INT线就会被拉低。中断源判断当公共中断触发后主控需要轮询每个PCA9535A的输入寄存器。读取某个芯片的输入寄存器会自动清除该芯片的中断标志。因此轮询顺序应该是依次读取每个芯片的输入寄存器直到所有芯片的中断都被清除公共INT线恢复高电平。在代码中可以在中断服务程序里用一个循环完成这个操作。uint8_t pca_devices[] {0x20, 0x21, 0x22}; // 三个PCA9535A的地址 void EXTI_IRQHandler(void) { if(EXTI_GetITStatus(INT_PIN) ! RESET) { // 可能多个器件产生中断需要轮询所有 for(int i 0; i sizeof(pca_devices); i) { uint16_t port_state PCA9535A_ReadAllInputs(pca_devices[i]); // 处理该器件的状态变化... Process_Device_Input(pca_devices[i], port_state); } // 所有器件的中断在读取时已被清除 EXTI_ClearITPendingBit(INT_PIN); } }6. 常见问题排查与调试心得即使按照手册设计调试阶段也难免遇到问题。以下是我总结的“排坑指南”。6.1 I2C通信失败症状主控发送地址后无应答NACK或读取的数据全是0xFF/0x00。排查步骤检查硬件连接用万用表测量SDA、SCL和VDD电压。确保上拉电阻已正确连接电压符合预期3.3V或5V。检查地址确认A2,A1,A0引脚电平设置正确计算出的7位地址与代码中一致。特别注意有些I2C库函数要求输入7位地址如0x20有些要求输入8位地址左移一位如0x40。这是最常见的错误来源。用逻辑分析仪抓取波形这是最直接的调试手段。观察起始条件、地址字节、ACK信号、数据字节的波形是否正常。检查时钟频率是否过高特别是长导线时尝试降低到100kHz。检查是否有毛刺或信号完整性问题。检查电源和复位确保VDD稳定且在芯片工作电压范围内1.65V-5.5V。检查是否有电源毛刺导致芯片复位。6.2 中断功能不正常症状INT引脚一直为低或从未变低。排查步骤确认INT上拉电阻必须接通常4.7kΩ。检查输入端口配置只有配置为输入的引脚状态变化才会触发中断。输出引脚的变化不会触发。理解中断清除机制中断标志在读取输入端口寄存器时被清除。如果你的程序在中断服务中只处理了数据但没有读取输入寄存器或者读取了错误的寄存器如输出寄存器中断将无法清除INT会保持低电平。多个中断源冲突如果多个PCA9535A的INT引脚“线与”确保在中断服务程序中读取了所有可能触发中断的芯片。如果漏掉一个公共INT线将无法恢复高电平。6.3 输出驱动能力不足症状LED亮度不足或输出高/低电平达不到预期值。排查步骤测量实际负载电流用万用表电流档串联测量。确保单引脚电流未超过推荐值建议10mA持续电流。检查输出电压在负载情况下测量输出引脚对地的电压。输出低电平VOL会随着灌电流增大而升高。如果VOL过高例如0.8V可能无法有效关闭某些逻辑器件或使LED完全熄灭。总电流限制数据手册有VDD和VSS的总电流限制。如果你同时驱动很多个LED需要计算总电流是否超标。超标可能导致芯片发热甚至损坏。6.4 软件配置后端口无反应症状写配置或输出寄存器后引脚电平没有变化。排查步骤确认配置寄存器这是最关键的步骤引脚方向由配置寄存器控制而不是输出寄存器。你必须先向配置寄存器写入0将引脚设为输出模式然后向输出寄存器写入值才能控制电平。检查写操作是否成功在写操作后立刻进行一次读操作读回配置寄存器或输出寄存器的值看是否与写入的一致。这可以验证I2C通信和芯片响应是否正常。注意指针自动递增如果你连续写入多个字节要清楚当前指针指向哪里。一个良好的编程习惯是在每次需要操作不同寄存器组时显式地发送命令字节来设置指针而不是依赖自动递增的残余状态。经过以上从理论到实践从硬件到软件的梳理相信你已经对PCA9535A这颗经典的I2C GPIO扩展器有了全面的认识。它的价值在于以极低的成本和复杂度为资源受限的嵌入式系统提供了强大的扩展能力。在功耗敏感的应用中其低静态电流和中断唤醒机制更是无可替代。下次当你的主控GPIO捉襟见肘时不妨考虑一下这个可靠的老朋友。在实际项目中清晰的文档、稳健的硬件设计和模块化的驱动代码是高效使用这类外设芯片的不二法门。