news 2026/10/16 7:00:25

STM32F103模拟I2C访问PCA9555:GPIO时序与寄存器配置实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103模拟I2C访问PCA9555:GPIO时序与寄存器配置实战

简介:一份基于STM32F103单片机模拟IIC协议访问PCA9555输入输出扩展芯片的工程资料,主要面向需要扩充数字口、熟悉软件模拟IIC时序的嵌入式开发者。工程采用普通IO口模拟时钟线和数据线,不占用芯片自带的硬件I2C外设,完整实现了起始信号、停止信号、从机地址发送、读写控制以及应答检测,并针对PCA9555的输入寄存器、输出寄存器、中断配置寄存器给出了操作示例。压缩包内共有127个文件,以C语言源文件、头文件、Keil工程配置文件以及编译生成的中间文件和固件为主,整体包体约2MB,结构清晰,适合直接打开对照学习或移植。该工程目前已有4837人学习下载,作者在说明中称‘亲测有效’。借助工程内代码,可以快速掌握用普通引脚模拟IIC的方法,也能理解PCA9555的16路IO扩展机制,为后续在STM32平台上扩展更多I2C器件打下基础。

1. 为什么绕开硬件I2C:用STM32F103模拟IIC访问PCA9555,这条路我走通了

用STM32F103的普通GPIO去模拟IIC,访问PCA9555这颗16位I/O扩展芯片,把原本挂在I2C总线上的16路输入输出全部跑通,这件事我亲测有效。之前在某设备的改版中,我最初用的是F103自带的I2C外设,结果在总线忙标志和EV5等待上耗了整整两天,最后换成模拟IIC,一个晚上就把读写时序全部调通了。对F103这颗芯片来说,模拟IIC不是退而求其次,反而是更可控的方案:时钟自己给、时序自己调、出错一眼能看穿。这篇适合正在调PCA9555却读不到正确电平的人,也适合那些准备给MCU扩展IO口、但不想再跟硬件I2C状态机纠缠的人。

2. 模拟IIC时序基本功:起始、停止、字节读写和应答检测的代码实现

2.1 引脚配置:为什么SCL和SDA都要开漏输出

模拟IIC的物理层说起来就两根线:SCL时钟、SDA数据。跟硬件I2C一样,SCL由主机控制输出,SDA则是双向的——主机发送数据和地址时输出,检测从机ACK时读入,读取从机数据时也是读入。要让一个GPIO既能输出又能读回,最干净的做法就是把SDA配置成开漏输出。开漏输出意味着MCU最多只能把引脚拉低,或者释放引脚;释放时引脚电平由外部上拉电阻决定。这样才能实现真正的“线与”逻辑,也才能让PCA9555在应答时把SDA拉低,而不会和MCU输出高电平打架。

我推荐直接把SCL和SDA都配成开漏输出,配合外部4.7kΩ上拉电阻。SDA在读之前不需要切换GPIO方向,输出寄存器写1就是释放,读IDR寄存器就能拿到引脚真实电平。很多老代码在SDA读之前会把GPIO重新初始化成输入模式,这在实际工程里会让时序变乱,因为GPIO_Init重配消耗的时钟周期往往不可控。

#define SCL_GPIO GPIOB #define SCL_PIN GPIO_Pin_6 #define SDA_GPIO GPIOB #define SDA_PIN GPIO_Pin_7 void SoftI2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = SCL_PIN | SDA_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(SDA_GPIO, SCL_PIN | SDA_PIN); // 释放总线,初始为高 }

上面这段用的是标准外设库,PB6做SCL、PB7做SDA。GPIO_Mode_Out_OD是关键,推挽输出在这个场景下会带来电平冲突,不建议用。GPIO_Speed_50MHz保证边沿足够快,但真正的速度还是由后面的延时函数控制。上电后先把两根线都置高,这是I2C总线空闲状态的标志。如果你手头的板子用到了其它引脚,只要同时改掉宏里的端口和Pin,以及RCC对应的时钟使能即可。

2.2 起始信号与停止信号:两条边沿的先后顺序

I2C协议的起始和停止在物理上都是同一根SDA上的边沿跳变,区别在于SCL当时的状态。起始条件是SCL高电平期间SDA产生下降沿;停止条件是SCL高电平期间SDA产生上升沿。模拟实现时,顺序写错一个就会导致从机状态机错乱,所以我把这两个函数单独拆出来讲。

void SoftI2C_Delay(void) { uint8_t i = 10; // 72MHz主频下大约1~2us while (i--); } void SoftI2C_Start(void) { SDA_HIGH(); SCL_HIGH(); SoftI2C_Delay(); SDA_LOW(); // SCL为高时,SDA拉低 SoftI2C_Delay(); SCL_LOW(); // 完成后SCL回到低电平 } void SoftI2C_Stop(void) { SDA_LOW(); SCL_HIGH(); SoftI2C_Delay(); SDA_HIGH(); // SCL为高时,SDA拉高 SoftI2C_Delay(); }

Stop函数的开头先把SDA拉低,是为了在SCL升高的瞬间保证SDA还是稳定低电平,避免意外产生一个额外的起始条件。SoftI2C_Delay这个延时属于模拟IIC的节拍器,主频72MHz时一次空循环大约1~2us。如果把它改小到1,SCL频率会逼近500kHz,接近PCA9555的400kHz上限,建议留足余量,实际工程里我一般把i调到8~12之间,跑250kHz左右最稳。这个延时同时决定了SCL高电平和低电平的保持时间,PCA9555的数据手册要求SCL高低电平最小分别不小于1.3us和0.6us,2us的节拍留有余量。

2.3 字节写入与ACK检测:第九个时钟的释放时机

主机的写字节流程是:先发送8个数据位,每个位在SCL低电平时改变SDA,在SCL高电平时保持稳定;发完8位后,主机释放SDA,等第9个SCL高电平到来时读取SDA,如果从机拉低,表示收到了ACK。

uint8_t SoftI2C_WriteByte(uint8_t dat) { uint8_t i; uint8_t ack; for (i = 0; i < 8; i++) { if (dat & 0x80) SDA_HIGH(); else SDA_LOW(); dat <<= 1; SoftI2C_Delay(); SCL_HIGH(); // 数据在SCL高电平期间必须稳定 SoftI2C_Delay(); SCL_LOW(); SoftI2C_Delay(); } SDA_HIGH(); // 释放SDA,等待从机应答 SoftI2C_Delay(); SCL_HIGH(); SoftI2C_Delay(); ack = SDA_READ(); // 0为ACK,1为NACK SCL_LOW(); return ack; }

这里有一个多数人在模拟IIC上翻车的细节:第9个时钟来临之前,一定要先把SDA置高释放。如果SDA还停留在输出状态且输出的是最后一bit的电平,那么无论PCA9555有没有应答,读到的都是自己驱动出来的电平。另外,再次强调不要在SCL高电平期间伸手往SDA写数据,要在SCL低的时候把SDA准备好。ack返回值在高电平时读入,读完后SCL要立刻拉低,为下一个字节的传输做准备。

2.4 字节读取与应答发送:连续读多个字节时结尾要回NACK

读字节和写字节正好相反,是主机在每个SCL高电平期间取样SDA。读进来的位先放进变量,顺序是高位先出。读完8位后,如果还要继续读下一个字节,主机需要回一个ACK;如果这是最后一个字节,回NACK,从机收到NACK就知道读取结束,会释放总线。

uint8_t SoftI2C_ReadByte(void) { uint8_t i; uint8_t dat = 0; SDA_HIGH(); // 释放SDA,把总线交给从机 for (i = 0; i < 8; i++) { dat <<= 1; SCL_HIGH(); SoftI2C_Delay(); if (SDA_READ()) dat |= 0x01; SCL_LOW(); SoftI2C_Delay(); } return dat; } void SoftI2C_SendAck(uint8_t ack) { if (ack == 0) SDA_LOW(); // 应答:拉低 else SDA_HIGH(); // 非应答:释放 SoftI2C_Delay(); SCL_HIGH(); SoftI2C_Delay(); SCL_LOW(); }

读取函数里SDA并没有切方向,因为开漏输出模式下释放总线之后,读IDR得到的就是外部从机的电平,这是模拟IIC最省心的一个技巧。SoftI2C_SendAck的第一个参数传0表示ACK,传1表示NACK。连续读多个字节时,前几个字节回ACK,最后一个字节回NACK,这个习惯和新手常见的“每个字节都回ACK”不一样,跟从机协议对不上就会导致总线卡死。

提示:如果改用切换GPIO方向的方式读取SDA,切换动作本身可能要消耗不少时钟周期,且方向切换后SDA电平需要重新稳定。开漏输出+读IDR的方式没有这些开销,推荐先跑通这版再考虑优化。

3. PCA9555读写协议:从设备地址到寄存器配置

3.1 设备地址换算:0x40、0xA0和A0/A1/A2引脚的关系

PCA9555的7位从机地址由固定高4位0100和外部引脚A2、A1、A0组成。三个地址引脚的电平组合出8个地址:0x40到0x47。I2C总线上实际发送的字节是7位地址加读写位,所以写方向发送0xA0,读方向发送0xA1。不少人在代码里写0x40去探测设备,自然得不到应答,因为0x40只是7位地址,协议层的首个字节必须带读写位。

A2A1A07位地址写操作字节读操作字节
0000x400xA00xA1
0010x410xA20xA3
0100x420xA40xA5
1110x470xAE0xAF

做PCB时A0、A1、A2一般直接接地,地址就是0x40。如果板子上的PCA9555地址不是0x40,先检查这三个引脚有没有被错误的电平拉走,这比查代码更快。I2C总线上可以同时挂多颗PCA9555,每颗芯片地址不同就行,但同一总线上的上拉电阻需要相应调整,挂得越多上拉要越弱,不要让总线电平被拖垮。

3.2 寄存器表:配置、输入、输出和极性翻转

PCA9555有8个寄存器,按端口P0和P1成对出现,寄存器地址从0x00到0x07。一次写寄存器只能操作一个8位端口,16路IO需要分别操作两个端口。

寄存器地址名称读写方向说明
0x00Input Port 0只读读取P0输入电平
0x01Input Port 1只读读取P1输入电平
0x02Output Port 0读写P0输出锁存值
0x03Output Port 1读写P1输出锁存值
0x04Polarity Inversion 0读写P0极性翻转控制
0x05Polarity Inversion 1读写P1极性翻转控制
0x06Configuration 0读写P0方向配置,1=输入,0=输出
0x07Configuration 1读写P1方向配置,1=输入,0=输出

上电后配置寄存器默认全1,也就是16个引脚全部是输入状态,输出寄存器默认是0xFF,极性翻转寄存器默认是0x00。这个默认值经常坑人:你明明往输出寄存器写了数据,引脚却没有输出,因为方向没改。配置寄存器的高低位对应每个IO口,1表示输入、0表示输出;极性翻转寄存器写入1会让对应输入引脚的电平在内部取反,如果不是故意做反相输入,保持0就好。

3.3 写操作完整流程:起始、设备地址、寄存器地址、数据、停止

写PCA9555最简单,一条命令发送四个字节就能完成任意一个寄存器的写入。第一步发起始信号,第二步发0xA0,第三步发寄存器地址,第四步发数据,第五步发停止信号。

uint8_t PCA9555_WriteReg(uint8_t reg, uint8_t value) { SoftI2C_Start(); if (SoftI2C_WriteByte(0xA0)) { SoftI2C_Stop(); return 1; } // 设备地址+写 if (SoftI2C_WriteByte(reg)) { SoftI2C_Stop(); return 1; } // 寄存器地址 if (SoftI2C_WriteByte(value)){ SoftI2C_Stop(); return 1; } // 写入数据 SoftI2C_Stop(); return 0; }

函数内每个WriteByte如果返回1,说明PCA9555回的是NACK,这时候直接发停止信号退出,避免总线卡在错误状态。这个函数返回值0表示成功,1表示失败。初始化时最常用的三行调用如下:配置寄存器0x06写0,P0全部输出;0x07写0xFF,P1全部输入;输出寄存器0x02写0x55给P0口置特定电平。

PCA9555_WriteReg(0x06, 0x00); // P0全部配置为输出 PCA9555_WriteReg(0x07, 0xFF); // P1全部配置为输入 PCA9555_WriteReg(0x02, 0x55); // P0输出0x55

3.4 读操作:用重复起始条件读回输入寄存器

读操作比写操作多一步:先写设备地址和寄存器地址,让PCA9555内部的地址指针指向目标寄存器,然后重新发一个起始信号,再发0xA1读方向,最后读取一个字节并回NACK。第二次的起始信号在I2C协议里叫重复起始条件(repeated start),PCA9555支持这种操作。

uint8_t PCA9555_ReadReg(uint8_t reg) { uint8_t value; SoftI2C_Start(); SoftI2C_WriteByte(0xA0); // 先写:选择寄存器 SoftI2C_WriteByte(reg); SoftI2C_Start(); // 重复起始条件 SoftI2C_WriteByte(0xA1); // 切换为读方向 value = SoftI2C_ReadByte(); SoftI2C_SendAck(1); // 最后一个字节回NACK SoftI2C_Stop(); return value; }

读取P1口按键状态时,直接调PCA9555_ReadReg(0x01),拿到的值就是16路输入口中P1那8路的实时电平。如果一次要读16路输入,可以连续读0x00和0x01两个字节,第一字节回ACK、第二字节回NACK,PCA9555内部地址指针会自动递增,不需要重新指定寄存器地址。

uint16_t PCA9555_ReadAllInput(void) { uint16_t val = 0; SoftI2C_Start(); SoftI2C_WriteByte(0xA0); SoftI2C_WriteByte(0x00); // 从输入寄存器0x00开始 SoftI2C_Start(); SoftI2C_WriteByte(0xA1); val = SoftI2C_ReadByte(); // 读0x00 SoftI2C_SendAck(0); // 还要继续读,回ACK val |= (SoftI2C_ReadByte() << 8); // 读0x01,放高字节 SoftI2C_SendAck(1); // 最后一个字节回NACK SoftI2C_Stop(); return val; }

读回组合值时,低8位是P0,高8位是P1,业务代码里可以直接用位操作判断某一路输入。如果读的过程中回ACK策略写错,比如最后一个字节回了ACK,首次读取能得到数据,再次读取时总线状态可能错乱,表现就是数据偶发跳变。

4. 工程落地:初始化顺序、寄存器配置和LED验证

4.1 驱动分层与函数清单

正式工程里建议把时序层和设备层分开。时序层只做GPIO和I2C字节,不关心芯片是什么;设备层只处理PCA9555寄存器,不碰具体延时。这样后续换一颗I2C从设备,设备层重写,时序层原样保留。

函数所属层作用
SoftI2C_GPIO_Init时序层初始化SCL/SDA引脚
SoftI2C_Start/Stop时序层生成起始/停止信号
SoftI2C_WriteByte时序层发送一个字节并返回ACK状态
SoftI2C_ReadByte时序层读取一个字节
SoftI2C_SendAck时序层发送ACK/NACK
PCA9555_WriteReg设备层写PCA9555寄存器
PCA9555_ReadReg设备层读PCA9555寄存器

实际工程中我一般把时序层代码放soft_i2c.c,设备层放pca9555.c,main.c只做初始化和业务逻辑。如果你是在老项目里嵌这段代码,注意把soft_i2c.c里的宏定义和工程的引脚定义放在同一个头文件里,避免两边各写一套导致改引脚时漏改一处。

4.2 初始化顺序:先把时钟和引脚搞定,再写配置

初始化顺序有个容易踩的坑:有人先把PCA9555配置写成输出,再初始化SCL/SDA,结果是总线还没建立,芯片方向却是输出,输出引脚又在乱跳,整个初始化看起来像是偶发故障。正确的顺序是先配MCU GPIO时钟、再配SCL/SDA、然后拉高总线,最后才给PCA9555写配置寄存器。

int main(void) { SystemInit(); // 配置系统时钟,默认72MHz Delay_Init(); // 如果有SysTick延时,先初始化 SoftI2C_GPIO_Init(); // 1. SCL/SDA引脚开漏配置 SoftI2C_Start(); // 2. 总线探测,确认空闲 SoftI2C_Stop(); PCA9555_WriteReg(0x06, 0x00); // 3. P0全部输出 PCA9555_WriteReg(0x07, 0xFF); // 4. P1全部输入 PCA9555_WriteReg(0x02, 0x55); // 5. P0输出测试值 while (1) { // 业务逻辑 } }

上面的第2步不是必须的,但我习惯在初始化时跑一个起始+停止的空操作,目的是确认GPIO引脚已经被正确拉高,避免后续第一个起始信号因为引脚初始化时序来不及而失败。如果第3步之后发现P0口电平不对,不要急着查PCA9555,先用万用表量一下P0.0到P0.7的电压,一般能找到问题是配置没生效还是后级负载短路。

4.3 用LED验证输出:闪烁一个IO口

调输出的判断标准是灯在闪。最简单的主循环写法是翻转P0.0输出状态,每100ms翻一次。写回PCA9555输出寄存器之前,先读当前输出值,异或一个位,再把新值写回。

while (1) { uint8_t out = PCA9555_ReadReg(0x02); // 读当前输出锁存 out ^= 0x01; // 翻转最低位 PCA9555_WriteReg(0x02, out); // 写回 Delay_Ms(100); }

注意PCA9555的输出寄存器是锁存型,读0x02拿到的是当前锁存值,不是引脚上的实时电平。如果某个引脚被配置成了输入,这个锁存值不会出现在引脚上。如果你只是想验证输出而不想读寄存器,也可以定义一个全局变量做缓存,每次翻转变量后直接整字节写寄存器,这样还能少一次I2C读操作,总线压力更小。

4.4 用按键验证输入:读回的原始电平要自己消抖

PCA9555的输入寄存器是电平采样,没有按键消抖功能。把P1口接8个按键后,读0x01得到的是一个字节,按键按下时引脚被拉低,对应位为0。很多初学者以为读取值里按下的键是1,实际是0,这个反逻辑经常让人怀疑芯片坏了。

uint8_t key; if (PCA9555_ReadReg(0x01) != 0xFF) // 有按键被按下 { Delay_Ms(10); // 简单消抖 key = PCA9555_ReadReg(0x01); // key中为0的位就是被按下的按键 }

按键按下后引脚接低电平,如果想按下读1,可以在初始化时把对应输入口的极性翻转寄存器0x05写0xFF,内部自动把输入取反。不过这种反转会掩盖真实的引脚电平,调试时反而容易迷路,建议先用原始电平调通,再考虑反相。还有一个容易忽略的问题:PCA9555的P1口输入如果悬空,读回的电平是不定的,按键接法必须明确是拉低还是拉高,否则读回来的数据里就会掺杂随机抖动。

5. 避坑排查:模拟IIC访问PCA9555的五个典型翻车现场

这一章里记录的都是我在实际板子上遇到过的问题,有些看起来像芯片坏了,有些看起来像代码逻辑错了,最后排查下来基本都是时序或寄存器地址的细节。每一条我会按现象、原因、解决的顺序写清楚。

5.1 ACK一直为1,设备好像不存在

现象:调用PCA9555_WriteReg后返回值一直是1,用示波器看SDA在第九个时钟也有拉低动作,但代码读到的ACK就是高电平。

原因:SDA在第9个时钟之前没有正确释放,或者GPIO配置成了推挽输出,外部从机根本拉不动SDA。再有就是上拉电阻没接,开漏输出释放后SDA悬空,读回来的是噪声电平。

解决:把SCL和SDA都改为开漏输出,并在第9个时钟前把SDA输出置1。如果用的是其它库,确认GPIO模式是GPIO_Mode_Out_OD而不是GPIO_Mode_Out_PP,同时检查SCL和SDA是否都接了上拉电阻,悬空状态下开漏输出读回来的电平是没有意义的。排查顺序是先量SDA对地电阻,再用示波器看第九个时钟处SDA波形,最后再怀疑代码。

5.2 读输入口永远是0xFF,输出口怎么配都没反应

现象:P1口接按键后读0x01始终0xFF,配置寄存器写0后读回还是0xFF。

原因:配置寄存器地址写错了,把0x06/0x07写成了0x04/0x05。0x04/0x05是极性翻转寄存器,写0xFF正好把所有输入极性反转,里外里一起把状态搞乱了。另一个常见原因是上电后只写了输出寄存器,没写配置寄存器,PCA9555保持默认的全输入状态。

解决:对照寄存器表确认配置寄存器是0x06和0x07,极性寄存器保持0x00。另外要记得PCA9555上电默认全输入,如果只写了输出寄存器没写配置寄存器,引脚不会有驱动能力。判断配置是否生效,可以在写完0x06后立刻读回0x06,如果读到的值和你写的一致,说明配置通道已经通了,问题就在业务层的计算逻辑。

5.3 总线上扫描不到任何设备

现象:写一个总线扫描程序,从0x40到0x47全部试了一遍,一个ACK都没有。

原因:地址引脚A0/A1/A2悬空或接错,导致设备实际地址和代码里预期的0x40不一致;或者SCL/SDA两根线在PCB上接反了。

解决:先用万用表量A0/A1/A2到GND的电压,确认三个引脚都是0V,然后确认SDA和SCL有没有接反。最后写一个扫描函数,把0x40到0x47的地址依次发一遍,看哪个地址有ACK,一台设备一个地址,扫描程序最容易定位问题。

uint8_t addr; for (addr = 0x40; addr <= 0x47; addr++) { SoftI2C_Start(); if (SoftI2C_WriteByte(addr << 1) == 0) // 0x40左移一位得到0xA0 { SoftI2C_Stop(); printf("PCA9555 found at 0x%X\r\n", addr); } }

扫描程序的第一个坑是地址左移,第二个坑是找到设备后要立刻发停止信号,不能在NACK状态直接做下一次起始,否则总线会被占住。这个扫描代码在F103上跑通之后,我会保留在工程里,当成一块板子的上电自检工具。

5.4 指示灯不受控,输出引脚电平乱跳

现象:配置寄存器已经写0,输出寄存器写固定值,但个别引脚输出电平不稳定,偶尔翻转。

原因:这几个引脚可能同时被其它外设占用,或者PCB走线过长、上拉电阻过大导致边沿太缓。还有可能是极性翻转寄存器被意外写了值,把输入路径反转了,虽然输出不受极性影响,但读回寄存器时看起来就像数据乱了。

解决:把输出配置值和实际引脚电平对比,确认是不是配置位写错。上拉电阻从10kΩ换成4.7kΩ,SCL/SDA走线控制在5cm以内,可以在SDA上串联一个330Ω电阻限制边沿过冲。如果还有引脚乱跳,用示波器观察SCL/SDA波形,看停止条件之后SDA是否处于释放状态的高电平。

5.5 模拟IIC时序明明对,读回来的数据偶尔多bit

现象:连续读100次,大多数正常,偶尔有一个字节多一位或丢一位,波形上看SCL有毛刺。

原因:SCL上升沿过缓,在PCA9555的输入阈值附近震荡,导致数据位被重复采样;或者主循环里打断了延时函数的执行,让SCL高电平时间忽长忽短。

解决:把SoftI2C_Delay加大到2us以上,SCL频率降到250kHz以下。如果用中断处理其它任务,在I2C读写期间关中断,或者把I2C读写放到临界区保护中,避免时序被中断拉长。还有一个容易被忽略的情况:如果PCB上I2C走线和电机驱动线平行,干扰串进SDA会让采样值出错,这种情况把I2C线包地或者加屏蔽处理。

6. 进阶用法:把软IIC封装成可移植驱动,换板子只改引脚表

6.1 用结构体管理SCL/SDA引脚

如果手头项目不止一块板子,有的用PB6/PB7,有的用PA8/PA9,把引脚写死在宏里,换板子就得改头文件。常见做法是定义一个结构体,把端口、引脚和延时函数全部放进去,所有的时序函数都基于这个结构体工作。

typedef struct { GPIO_TypeDef *SCL_PORT; uint16_t SCL_PIN; GPIO_TypeDef *SDA_PORT; uint16_t SDA_PIN; void (*DelayUs)(uint16_t us); } SoftI2C_Handle; SoftI2C_Handle i2c1 = { GPIOB, GPIO_Pin_6, GPIOB, GPIO_Pin_7, Delay_Us };

初始化函数变成SoftI2C_Init(&i2c1),字节读写函数变成SoftI2C_WriteByte(&i2c1, dat),PCA9555的函数再多加一个句柄参数。这样整套驱动跟具体MCU引脚的耦合就被切开,换到同系列其它型号,只改结构体里的端口和引脚号;换成完全不同的MCU,也只需要重写三个底层宏。

6.2 新板子到手,先跑一遍总线扫描再谈业务

拿到一块带有PCA9555的新板子,我最先做的事情不是直接写业务代码,而是先跑一遍地址扫描,确认芯片应答稳定。扫描程序固定放在初始化后面,输出每次上电的扫描结果,如果ACK不稳定,说明硬件或者时序还有问题,这时候去查原理图比查业务逻辑快得多。

从那以后,我每次拿到新板子都会强制走一遍“上电先扫描、扫描完再配置、配置完再测IO”的流程。模拟IIC的好处是出问题能直接看波形、改延时,不像硬件I2C那样黑匣子还得猜状态机。这套STM32F103模拟IIC访问PCA9555的代码,已经被我复用到好几块板子上,真正踩过的坑也都写在上面了,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 6:59:34

庖丁解牛式拆解SaaS:从业务骨架到增长密码

做了这么多年SaaS产品&#xff0c;有一个场景让我印象特别深&#xff1a;有位创始人在融资遇到挫折后跑来问我&#xff0c;为什么他照搬了一套标准的SaaS打法&#xff0c;产品功能不缺、销售团队也在打&#xff0c;考核了MQL、SQL、转化率、续费率&#xff0c;但增长曲线就是上…

作者头像 李华
网站建设 2026/10/16 6:59:24

SAP MD01 MRP运行参数详解:从处理代码到计划时界的实战指南

1. 走进MD01&#xff1a;MRP运行时&#xff0c;你到底在指挥什么&#xff1f;干SAP物流和计划这行&#xff0c;迟早要跟MRP运行较上劲。只要聊到物料需求计划&#xff0c;绕不开那个经典事务代码MD01。很多新手第一次点开MD01&#xff0c;对着最后一个界面愣住&#xff1a;一堆…

作者头像 李华
网站建设 2026/10/16 6:59:15

RFID赋能制造业:资产与物料智能管控实战指南

前年我陪某制造企业做了一次年底固定资产盘点。财务部、设备部、车间主管三方凑在一起&#xff0c;拿着纸质表核对上千台设备&#xff0c;三天盘下来&#xff0c;账实差异率大约7%。许多小设备早就挪了位置没人登记&#xff0c;几台工装夹具连责任人都说不清楚。这类场景在制造…

作者头像 李华
网站建设 2026/10/16 6:58:53

Report Machine 2.6:规则驱动型报告流水线实践

简介&#xff1a;Report Machine 2.6 是一款专为 Borland Delphi&#xff08;3–7.1 版本&#xff09;开发者打造的轻量级报表生成工具&#xff0c;面向中高级 Delphi 桌面应用开发人员&#xff0c;解决传统报表开发中设计繁琐、集成度低、定制困难等痛点。资源包共 488 个文件…

作者头像 李华
网站建设 2026/10/16 6:58:48

业务逻辑漏洞挖掘实战指南:从参数篡改到竞态条件与状态机绕过

刚入行做安全测试那会儿&#xff0c;我身边不少人都执着于挖SQL注入和XSS&#xff0c;觉得那才叫"技术活"。后来慢慢发现&#xff0c;真正让甲方愿意给高额奖励的&#xff0c;往往是另一种东西——逻辑漏洞。所谓逻辑漏洞&#xff0c;也叫业务逻辑漏洞&#xff0c;指…

作者头像 李华
网站建设 2026/10/16 6:58:39

Tello无人机PC端控制:Win10+VS2019+Qt5.15环境搭建与H.264视频流解码实战

简介&#xff1a;本资源是一套面向嵌入式与无人机开发初学者的PC端大疆Tello控制实战项目&#xff0c;聚焦UDP通信、GUI交互与实时视频处理三大核心技术环节。项目基于Visual Studio Qt构建跨平台图形界面&#xff0c;集成FFmpeg解码H.264视频流、OpenCV实时渲染显示&#xff…

作者头像 李华