简介:这份资源面向使用STM32F103进行嵌入式开发的工程师与学习者,聚焦于用GPIO软件模拟IIC时序来驱动PCA9555这款16位IO扩展芯片,解决单片机IO口不足、需要外扩端口的实际问题。压缩包共127个文件,约2MB,以32个c源文件与33个h头文件为核心,涵盖STM32标准外设库、PCA9555读写驱动及主程序逻辑,另含14个o、14个d编译中间文件、13个crf、1个hex与1个axf等构建产物,以及uvprojx、uvoptx工程配置和sct、lst、map等链接与列表文件,可直接用Keil打开编译。资源已有4838人学习下载,热度较高。读者可从中获得完整的模拟IIC底层时序实现、PCA9555寄存器读写封装、GPIO推挽输出与上拉配置范例,以及可复用的工程模板与排错参考,适合需要快速验证IO扩展方案的中初级开发者。1. 为什么我宁愿用软件模拟 IIC 去驱动 PCA9555手上这块 STM32F103 的板子,硬件 IIC 外设被前人拿去接了别的从机,留给扩展 IO 的只剩两根普通 GPIO。项目又急着要读一组按键和驱动几路继电器,PCA9555 这颗 16 位 IO 扩展芯片正好合适,可总线资源已经没了。翻了一圈资料,发现用软件模拟 IIC 访问 PCA9555 是条稳妥的路子,时序完全由自己掌控,不用跟硬件外设的坑较劲。这篇就把我实际跑通的模拟 IIC 方案拆开讲清楚,从时序原理到寄存器读写,再到踩过的几个坑,新手照着能复现,熟手也能对照看看边界在哪。2. 模拟 IIC 的时序底座与 PCA9555 寄存器模型2.1 为什么模拟 IIC 在这类场景反而更省心STM32F103 的硬件 IIC 外设,用过的人多少都听过一些玄学问题,比如总线锁死、状态机卡在某个标志位出不来。当然这些问题在正确配置下大多能规避,但在一个 IO 资源紧张、又不想花时间调硬件外设的场合,软件模拟的性价比就出来了。模拟 IIC 的本质就是用两根 GPIO 手动翻转电平,去复现 IIC 协议的起始、停止、应答和字节传输。它的好处是时序可见、可打断点、可加延时,出问题一眼能看出是哪一拍不对。代价是占用 CPU 时间,速率上不去,通常跑到 100kHz 左右比较稳,再快就要看主频和延时函数的精度了。PCA9555 这边,它是一颗通过 IIC 挂载的 16 位 IO 扩展器,分成两个 8 位端口 Port0 和 Port1。每个端口有输入寄存器、输出寄存器、极性反转寄存器和配置寄存器。配置寄存器决定某个引脚是输入还是输出,这一点和 STM32 的 GPIO 模式寄存器思路类似。上电后所有引脚默认是输入,这点要记住,不然你会发现自己写输出寄存器没反应,其实是引脚还处在输入态。2.2 模拟 IIC 的四个基本时序单元模拟 IIC 要跑通,核心就是四个动作:起始条件、停止条件、发送一个字节、接收一个字节并回应答。起始条件是 SCL 为高时 SDA 由高变低,停止条件是 SCL 为高时 SDA 由低变高。发送字节时,数据在 SCL 低电平期间变化,在 SCL 高电平期间保持稳定,从机在这期间采样。接收字节类似,只是方向反过来,主机在第八个时钟后拉低 SDA 表示应答。下面这段是起始和停止的代码,我一般把 SDA 和 SCL 都配成开漏输出,外接上拉电阻。开漏的好处是总线可以多设备共享,不会出现推挽对推挽的短路。// 假设 SDA 接 PB7,SCL 接 PB6,均配置为开漏输出 #define IIC_SDA_H() GPIOB->BSRR = GPIO_Pin_7 // SDA 拉高 #define IIC_SDA_L() GPIOB->BRR = GPIO_Pin_7 // SDA 拉低 #define IIC_SCL_H() GPIOB->BSRR = GPIO_Pin_6 #define IIC_SCL_L() GPIOB->BRR = GPIO_Pin_6 #define IIC_SDA_READ() ((GPIOB->IDR & GPIO_Pin_7) ? 1 : 0) // 起始条件:SCL 高时 SDA 由高变低 void IIC_Start(void) { IIC_SDA_H(); IIC_SCL_H(); IIC_Delay(); // 延时约 4~5us,对应 100kHz 左右 IIC_SDA_L(); IIC_Delay(); IIC_SCL_L(); // 钳住总线,准备发送数据 IIC_Delay(); } // 停止条件:SCL 高时 SDA 由低变高 void IIC_Stop(void) { IIC_SDA_L(); IIC_SCL_H(); IIC_Delay(); IIC_SDA_H(); IIC_Delay(); }这里的IIC_Delay()是个简单的空循环或__NOP()组合,具体循环次数要按主频算。72MHz 下我一般用几个__NOP()凑出 4us 左右。延时太短从机可能来不及采样,太长则速率掉得厉害。这个延时是模拟 IIC 最容易被忽略的参数,很多人代码逻辑没错,就是延时不对导致读回来全是 0xFF 或 0x00。2.3 PCA9555 的寄存器地址与读写命令字PCA9555 的 IIC 从机地址由 A2、A1、A0 三个引脚决定,7 位地址是0100 A2 A1 A0。如果三个脚都接地,7 位地址就是 0x20,写命令字是 0x40,读命令字是 0x41。注意这里说的是 8 位命令字,也就是 7 位地址左移一位再或上读写位。它的寄存器命令字节有 8 个,分两组,每组四个:命令字节寄存器说明0x00Port0 输入寄存器读 Port0 引脚电平0x01Port0 输出寄存器写 Port0 输出电平0x02Port0 极性反转反转输入极性0x03Port0 配置寄存器1 为输入,0 为输出0x04Port1 输入寄存器读 Port1 引脚电平0x05Port1 输出寄存器写 Port1 输出电平0x06Port1 极性反转反转输入极性0x07Port1 配置寄存器1 为输入,0 为输出写寄存器时,先发写命令字,再发寄存器命令字节,然后发数据。读寄存器时,先发写命令字,再发寄存器命令字节,然后重新发起始条件,发读命令字,再读数据。这个「写命令字节再重启读」的流程是 PCA9555 读操作的关键,漏掉重启直接读会拿到错误数据。3. 从零把模拟 IIC 读写 PCA9555 跑起来3.1 GPIO 初始化与延时函数先把两个 IO 配好。我习惯用标准库,寄存器操作也行,这里用标准库写法更直观。SDA 和 SCL 都配成开漏输出,速度选 50MHz 够用。注意开漏模式下必须外接上拉,一般 4.7k 到 10k 都行,板子上如果没有就要自己补。void IIC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); IIC_SDA_H(); IIC_SCL_H(); } // 粗略延时,72MHz 下约 4us,按需调整 void IIC_Delay(void) { uint8_t i = 20; while (i--) { __NOP(); } }延时这里给的是个经验值,实际要用示波器或逻辑分析仪看一眼 SCL 频率。没有仪器的话,可以先用保守的大延时把功能跑通,再逐步减小到通信稳定为止。我一般会留一档余量,不追求极限速率。3.2 字节收发与应答处理发送一个字节是高位先发,每发一位拉低 SCL,放好数据,再拉高 SCL 让从机采样。发完八位后释放 SDA,读从机的应答位。接收字节则是主机释放 SDA,从机驱动,主机在 SCL 高电平期间读取。// 发送一个字节,返回从机应答:0 应答,1 非应答 uint8_t IIC_SendByte(uint8_t byte) { uint8_t i, ack; for (i = 0; i < 8; i++) { IIC_SCL_L(); IIC_Delay(); if (byte & 0x80) IIC_SDA_H(); else IIC_SDA_L(); byte <<= 1; IIC_Delay(); IIC_SCL_H(); IIC_Delay(); } IIC_SCL_L(); IIC_Delay(); IIC_SDA_H(); // 释放 SDA,准备读应答 IIC_Delay(); IIC_SCL_H(); IIC_Delay(); ack = IIC_SDA_READ(); // 0 表示从机应答 IIC_SCL_L(); IIC_Delay(); return ack; } // 接收一个字节,ack=0 发送应答,ack=1 发送非应答 uint8_t IIC_ReadByte(uint8_t ack) { uint8_t i, byte = 0; IIC_SDA_H(); // 主机释放 SDA for (i = 0; i < 8; i++) { IIC_SCL_L(); IIC_Delay(); IIC_SCL_H(); IIC_Delay(); byte <<= 1; if (IIC_SDA_READ()) byte |= 0x01; } IIC_SCL_L(); IIC_Delay(); if (ack) IIC_SDA_H(); // 非应答 else IIC_SDA_L(); // 应答 IIC_Delay(); IIC_SCL_H(); IIC_Delay(); IIC_SCL_L(); IIC_Delay(); IIC_SDA_H(); return byte; }IIC_SendByte返回的应答位一定要检查。如果返回 1,说明从机没应答,常见原因是地址不对、接线松了或者上拉没接。我早期调试时就是没看应答,结果后面读出来全是 0xFF,查了半天才发现是从机地址算错了。3.3 写 PCA9555 配置寄存器与输出寄存器配置寄存器决定引脚方向。假设我要把 Port0 的低四位设成输出驱动继电器,高四位设成输入读按键,那 Port0 配置寄存器的值就是 0xF0。写的时候先发写命令字 0x40,再发寄存器命令字节 0x03,再发 0xF0。#define PCA9555_ADDR_W 0x40 #define PCA9555_ADDR_R 0x41 // 写一个寄存器 void PCA9555_WriteReg(uint8_t reg, uint8_t data) { IIC_Start(); IIC_SendByte(PCA9555_ADDR_W); IIC_SendByte(reg); IIC_SendByte(data); IIC_Stop(); } // 初始化:Port0 低四位输出,高四位输入 void PCA9555_Init(void) { PCA9555_WriteReg(0x03, 0xF0); // Port0 配置 PCA9555_WriteReg(0x07, 0xFF); // Port1 全输入 PCA9555_WriteReg(0x01, 0x00); // Port0 输出全低 }写输出寄存器就是往 0x01 写值。比如要点亮接在 P0_0 上的 LED,就写 0x01。这里要注意,输出寄存器的值只有在配置寄存器对应位为 0 时才真正驱动到引脚上,配置为输入的位写输出寄存器不影响引脚。3.4 读 PCA9555 输入寄存器读操作要复杂一点,先写命令字和寄存器命令字节,再重启发读命令字,然后读一个字节,最后发非应答再停止。// 读一个寄存器 uint8_t PCA9555_ReadReg(uint8_t reg) { uint8_t data; IIC_Start(); IIC_SendByte(PCA9555_ADDR_W); IIC_SendByte(reg); IIC_Start(); // 重启 IIC_SendByte(PCA9555_ADDR_R); data = IIC_ReadByte(1); // 读一个字节,发非应答 IIC_Stop(); return data; } // 读 Port0 输入 uint8_t PCA9555_ReadPort0(void) { return PCA9555_ReadReg(0x00); }读回来的值就是引脚当前电平。如果某个输入引脚悬空,读回来可能是随机跳变的,所以输入引脚最好有确定的上拉或下拉。我一般用内部上拉或者外部上拉,避免悬空带来的误判。4. 模拟 IIC 访问 PCA9555 的避坑与排查4.1 从机不应答,读回全是 0xFF现象是发送命令字后IIC_SendByte返回 1,后续读数据全是 0xFF。原因通常是地址算错、SDA/SCL 接反或者上拉缺失。PCA9555 的 7 位地址是0100 A2 A1 A0,如果 A2A1A0 都接地,写命令字是 0x40。有人会直接把 0x20 当写命令字发,那就错了。解决方法是先用万用表确认 A2A1A0 的电平,再核对命令字,同时确认 SDA 和 SCL 没有接反,上拉电阻是否焊上。4.2 写输出寄存器没反应现象是往输出寄存器写了值,但引脚电平不变。原因多半是配置寄存器没设成输出。PCA9555 上电默认所有引脚为输入,配置寄存器对应位是 1。要输出必须先写配置寄存器把对应位清 0。解决方法是先写配置寄存器,再写输出寄存器,顺序不能反。另外确认写的是正确的端口,Port0 和 Port1 的寄存器地址不同。4.3 读输入寄存器数据跳变现象是读回来的输入值不稳定,偶尔跳变。原因是输入引脚悬空,没有确定电平。解决方法是给输入引脚加上拉或下拉,或者启用 PCA9555 的内部上拉(如果有的话,具体看型号手册)。软件上也可以在读之前多读几次取稳定值,但根本办法还是硬件上给确定电平。4.4 通信偶尔失败,重启后恢复现象是跑一段时间后通信失败,断电重启又好了。原因可能是总线被干扰,或者延时太短导致时序临界。解决方法是适当加大IIC_Delay的延时,降低通信速率,同时在 SDA 和 SCL 上并一个小电容滤波。另外检查电源是否稳定,PCA9555 供电波动也会导致通信异常。4.5 多字节读写时数据错位现象是连续读写多个寄存器时,第二个字节开始错位。原因是 PCA9555 支持地址自增,连续读写时寄存器地址会自动加一。如果你不想自增,每次操作都要重新发起始条件并发送寄存器命令字节。解决方法是明确自己的读写意图,要么利用自增连续读写,要么每次单独寻址。5. 把模拟 IIC 封装成可复用驱动与验证方法5.1 封装成独立驱动文件把模拟 IIC 的底层和 PCA9555 的操作分成两个文件,底层只管起始、停止、收发字节,PCA9555 文件只管寄存器和业务逻辑。这样换一个从机,底层不用动。我一般会定义一组函数指针或者宏,方便换 IO 口。// pca9555.h #ifndef __PCA9555_H #define __PCA9555_H #include "stm32f10x.h" #define PCA9555_ADDR_W 0x40 #define PCA9555_ADDR_R 0x41 void PCA9555_Init(void); void PCA9555_WriteReg(uint8_t reg, uint8_t data); uint8_t PCA9555_ReadReg(uint8_t reg); void PCA9555_WritePort0(uint8_t data); uint8_t PCA9555_ReadPort0(void); #endif封装好之后,业务层只调PCA9555_WritePort0和PCA9555_ReadPort0,不用关心 IIC 时序。这样代码可读性和可维护性都好很多。5.2 用回环测试验证时序没有逻辑分析仪的时候,我会用一个简单的回环测试验证模拟 IIC 时序。把 PCA9555 的某个输出引脚和某个输入引脚短接,写输出再读输入,看是否一致。一致说明时序和寄存器操作都对。这个方法虽然土,但很有效,能快速定位是时序问题还是寄存器配置问题。// 回环测试:P0_0 输出,P0_4 输入,短接 P0_0 和 P0_4 void PCA9555_LoopbackTest(void) { PCA9555_WriteReg(0x03, 0xEF); // P0_4 输入,其余输出 PCA9555_WritePort0(0x01); // P0_0 输出高 if (PCA9555_ReadPort0() & 0x10) { // P0_4 读到高,回环成功 } PCA9555_WritePort0(0x00); // P0_0 输出低 if (!(PCA9555_ReadPort0() & 0x10)) { // P0_4 读到低,回环成功 } }这个测试能覆盖起始、停止、发送、接收和应答的完整流程,跑通了基本说明模拟 IIC 没问题。5.3 速率与稳定性的取舍模拟 IIC 的速率和稳定性是一对矛盾。延时越小速率越高,但越容易受干扰和主频波动影响。我一般会把延时设到通信稳定的最小值再留 20% 余量。比如稳定跑 100kHz 需要 5us 延时,我就设 6us。这样温度变化或电源波动时不容易翻车。另外,如果系统里有中断会打断 IIC 时序,最好在 IIC 操作期间关中断,或者保证中断服务程序足够短。从那以后我每次做模拟 IIC,都会先用回环测试把时序跑通,再上真实从机,最后用逻辑分析仪看一眼波形。这个习惯帮我省了很多返工时间。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源面向使用STM32F103进行嵌入式开发的工程师与学习者,聚焦于用GPIO软件模拟IIC时序来驱动PCA9555这款16位IO扩展芯片,解决单片机IO口不足、需要外扩端口的实际问题。压缩包共127个文件,约2MB,以32个c源文件与33个h头文件为核心,涵盖STM32标准外设库、PCA9555读写驱动及主程序逻辑,另含14个o、14个d编译中间文件、13个crf、1个hex与1个axf等构建产物,以及uvprojx、uvoptx工程配置和sct、lst、map等链接与列表文件,可直接用Keil打开编译。资源已有4838人学习下载,热度较高。读者可从中获得完整的模拟IIC底层时序实现、PCA9555寄存器读写封装、GPIO推挽输出与上拉配置范例,以及可复用的工程模板与排错参考,适合需要快速验证IO扩展方案的中初级开发者。1. 为什么我宁愿用软件模拟 IIC 去驱动 PCA9555手上这块 STM32F103 的板子,硬件 IIC 外设被前人拿去接了别的从机,留给扩展 IO 的只剩两根普通 GPIO。项目又急着要读一组按键和驱动几路继电器,PCA9555 这颗 16 位 IO 扩展芯片正好合适,可总线资源已经没了。翻了一圈资料,发现用软件模拟 IIC 访问 PCA9555 是条稳妥的路子,时序完全由自己掌控,不用跟硬件外设的坑较劲。这篇就把我实际跑通的模拟 IIC 方案拆开讲清楚,从时序原理到寄存器读写,再到踩过的几个坑,新手照着能复现,熟手也能对照看看边界在哪。2. 模拟 IIC 的时序底座与 PCA9555 寄存器模型2.1 为什么模拟 IIC 在这类场景反而更省心STM32F103 的硬件 IIC 外设,用过的人多少都听过一些玄学问题,比如总线锁死、状态机卡在某个标志位出不来。当然这些问题在正确配置下大多能规避,但在一个 IO 资源紧张、又不想花时间调硬件外设的场合,软件模拟的性价比就出来了。模拟 IIC 的本质就是用两根 GPIO 手动翻转电平,去复现 IIC 协议的起始、停止、应答和字节传输。它的好处是时序可见、可打断点、可加延时,出问题一眼能看出是哪一拍不对。代价是占用 CPU 时间,速率上不去,通常跑到 100kHz 左右比较稳,再快就要看主频和延时函数的精度了。PCA9555 这边,它是一颗通过 IIC 挂载的 16 位 IO 扩展器,分成两个 8 位端口 Port0 和 Port1。每个端口有输入寄存器、输出寄存器、极性反转寄存器和配置寄存器。配置寄存器决定某个引脚是输入还是输出,这一点和 STM32 的 GPIO 模式寄存器思路类似。上电后所有引脚默认是输入,这点要记住,不然你会发现自己写输出寄存器没反应,其实是引脚还处在输入态。2.2 模拟 IIC 的四个基本时序单元模拟 IIC 要跑通,核心就是四个动作:起始条件、停止条件、发送一个字节、接收一个字节并回应答。起始条件是 SCL 为高时 SDA 由高变低,停止条件是 SCL 为高时 SDA 由低变高。发送字节时,数据在 SCL 低电平期间变化,在 SCL 高电平期间保持稳定,从机在这期间采样。接收字节类似,只是方向反过来,主机在第八个时钟后拉低 SDA 表示应答。下面这段是起始和停止的代码,我一般把 SDA 和 SCL 都配成开漏输出,外接上拉电阻。开漏的好处是总线可以多设备共享,不会出现推挽对推挽的短路。// 假设 SDA 接 PB7,SCL 接 PB6,均配置为开漏输出 #define IIC_SDA_H() GPIOB->BSRR = GPIO_Pin_7 // SDA 拉高 #define IIC_SDA_L() GPIOB->BRR = GPIO_Pin_7 // SDA 拉低 #define IIC_SCL_H() GPIOB->BSRR = GPIO_Pin_6 #define IIC_SCL_L() GPIOB->BRR = GPIO_Pin_6 #define IIC_SDA_READ() ((GPIOB->IDR & GPIO_Pin_7) ? 1 : 0) // 起始条件:SCL 高时 SDA 由高变低 void IIC_Start(void) { IIC_SDA_H(); IIC_SCL_H(); IIC_Delay(); // 延时约 4~5us,对应 100kHz 左右 IIC_SDA_L(); IIC_Delay(); IIC_SCL_L(); // 钳住总线,准备发送数据 IIC_Delay(); } // 停止条件:SCL 高时 SDA 由低变高 void IIC_Stop(void) { IIC_SDA_L(); IIC_SCL_H(); IIC_Delay(); IIC_SDA_H(); IIC_Delay(); }这里的IIC_Delay()是个简单的空循环或__NOP()组合,具体循环次数要按主频算。72MHz 下我一般用几个__NOP()凑出 4us 左右。延时太短从机可能来不及采样,太长则速率掉得厉害。这个延时是模拟 IIC 最容易被忽略的参数,很多人代码逻辑没错,就是延时不对导致读回来全是 0xFF 或 0x00。2.3 PCA9555 的寄存器地址与读写命令字PCA9555 的 IIC 从机地址由 A2、A1、A0 三个引脚决定,7 位地址是0100 A2 A1 A0。如果三个脚都接地,7 位地址就是 0x20,写命令字是 0x40,读命令字是 0x41。注意这里说的是 8 位命令字,也就是 7 位地址左移一位再或上读写位。它的寄存器命令字节有 8 个,分两组,每组四个:命令字节寄存器说明0x00Port0 输入寄存器读 Port0 引脚电平0x01Port0 输出寄存器写 Port0 输出电平0x02Port0 极性反转反转输入极性0x03Port0 配置寄存器1 为输入,0 为输出0x04Port1 输入寄存器读 Port1 引脚电平0x05Port1 输出寄存器写 Port1 输出电平0x06Port1 极性反转反转输入极性0x07Port1 配置寄存器1 为输入,0 为输出写寄存器时,先发写命令字,再发寄存器命令字节,然后发数据。读寄存器时,先发写命令字,再发寄存器命令字节,然后重新发起始条件,发读命令字,再读数据。这个「写命令字节再重启读」的流程是 PCA9555 读操作的关键,漏掉重启直接读会拿到错误数据。3. 从零把模拟 IIC 读写 PCA9555 跑起来3.1 GPIO 初始化与延时函数先把两个 IO 配好。我习惯用标准库,寄存器操作也行,这里用标准库写法更直观。SDA 和 SCL 都配成开漏输出,速度选 50MHz 够用。注意开漏模式下必须外接上拉,一般 4.7k 到 10k 都行,板子上如果没有就要自己补。void IIC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); IIC_SDA_H(); IIC_SCL_H(); } // 粗略延时,72MHz 下约 4us,按需调整 void IIC_Delay(void) { uint8_t i = 20; while (i--) { __NOP(); } }延时这里给的是个经验值,实际要用示波器或逻辑分析仪看一眼 SCL 频率。没有仪器的话,可以先用保守的大延时把功能跑通,再逐步减小到通信稳定为止。我一般会留一档余量,不追求极限速率。3.2 字节收发与应答处理发送一个字节是高位先发,每发一位拉低 SCL,放好数据,再拉高 SCL 让从机采样。发完八位后释放 SDA,读从机的应答位。接收字节则是主机释放 SDA,从机驱动,主机在 SCL 高电平期间读取。// 发送一个字节,返回从机应答:0 应答,1 非应答 uint8_t IIC_SendByte(uint8_t byte) { uint8_t i, ack; for (i = 0; i < 8; i++) { IIC_SCL_L(); IIC_Delay(); if (byte & 0x80) IIC_SDA_H(); else IIC_SDA_L(); byte <<= 1; IIC_Delay(); IIC_SCL_H(); IIC_Delay(); } IIC_SCL_L(); IIC_Delay(); IIC_SDA_H(); // 释放 SDA,准备读应答 IIC_Delay(); IIC_SCL_H(); IIC_Delay(); ack = IIC_SDA_READ(); // 0 表示从机应答 IIC_SCL_L(); IIC_Delay(); return ack; } // 接收一个字节,ack=0 发送应答,ack=1 发送非应答 uint8_t IIC_ReadByte(uint8_t ack) { uint8_t i, byte = 0; IIC_SDA_H(); // 主机释放 SDA for (i = 0; i < 8; i++) { IIC_SCL_L(); IIC_Delay(); IIC_SCL_H(); IIC_Delay(); byte <<= 1; if (IIC_SDA_READ()) byte |= 0x01; } IIC_SCL_L(); IIC_Delay(); if (ack) IIC_SDA_H(); // 非应答 else IIC_SDA_L(); // 应答 IIC_Delay(); IIC_SCL_H(); IIC_Delay(); IIC_SCL_L(); IIC_Delay(); IIC_SDA_H(); return byte; }IIC_SendByte返回的应答位一定要检查。如果返回 1,说明从机没应答,常见原因是地址不对、接线松了或者上拉没接。我早期调试时就是没看应答,结果后面读出来全是 0xFF,查了半天才发现是从机地址算错了。3.3 写 PCA9555 配置寄存器与输出寄存器配置寄存器决定引脚方向。假设我要把 Port0 的低四位设成输出驱动继电器,高四位设成输入读按键,那 Port0 配置寄存器的值就是 0xF0。写的时候先发写命令字 0x40,再发寄存器命令字节 0x03,再发 0xF0。#define PCA9555_ADDR_W 0x40 #define PCA9555_ADDR_R 0x41 // 写一个寄存器 void PCA9555_WriteReg(uint8_t reg, uint8_t data) { IIC_Start(); IIC_SendByte(PCA9555_ADDR_W); IIC_SendByte(reg); IIC_SendByte(data); IIC_Stop(); } // 初始化:Port0 低四位输出,高四位输入 void PCA9555_Init(void) { PCA9555_WriteReg(0x03, 0xF0); // Port0 配置 PCA9555_WriteReg(0x07, 0xFF); // Port1 全输入 PCA9555_WriteReg(0x01, 0x00); // Port0 输出全低 }写输出寄存器就是往 0x01 写值。比如要点亮接在 P0_0 上的 LED,就写 0x01。这里要注意,输出寄存器的值只有在配置寄存器对应位为 0 时才真正驱动到引脚上,配置为输入的位写输出寄存器不影响引脚。3.4 读 PCA9555 输入寄存器读操作要复杂一点,先写命令字和寄存器命令字节,再重启发读命令字,然后读一个字节,最后发非应答再停止。// 读一个寄存器 uint8_t PCA9555_ReadReg(uint8_t reg) { uint8_t data; IIC_Start(); IIC_SendByte(PCA9555_ADDR_W); IIC_SendByte(reg); IIC_Start(); // 重启 IIC_SendByte(PCA9555_ADDR_R); data = IIC_ReadByte(1); // 读一个字节,发非应答 IIC_Stop(); return data; } // 读 Port0 输入 uint8_t PCA9555_ReadPort0(void) { return PCA9555_ReadReg(0x00); }读回来的值就是引脚当前电平。如果某个输入引脚悬空,读回来可能是随机跳变的,所以输入引脚最好有确定的上拉或下拉。我一般用内部上拉或者外部上拉,避免悬空带来的误判。4. 模拟 IIC 访问 PCA9555 的避坑与排查4.1 从机不应答,读回全是 0xFF现象是发送命令字后IIC_SendByte返回 1,后续读数据全是 0xFF。原因通常是地址算错、SDA/SCL 接反或者上拉缺失。PCA9555 的 7 位地址是0100 A2 A1 A0,如果 A2A1A0 都接地,写命令字是 0x40。有人会直接把 0x20 当写命令字发,那就错了。解决方法是先用万用表确认 A2A1A0 的电平,再核对命令字,同时确认 SDA 和 SCL 没有接反,上拉电阻是否焊上。4.2 写输出寄存器没反应现象是往输出寄存器写了值,但引脚电平不变。原因多半是配置寄存器没设成输出。PCA9555 上电默认所有引脚为输入,配置寄存器对应位是 1。要输出必须先写配置寄存器把对应位清 0。解决方法是先写配置寄存器,再写输出寄存器,顺序不能反。另外确认写的是正确的端口,Port0 和 Port1 的寄存器地址不同。4.3 读输入寄存器数据跳变现象是读回来的输入值不稳定,偶尔跳变。原因是输入引脚悬空,没有确定电平。解决方法是给输入引脚加上拉或下拉,或者启用 PCA9555 的内部上拉(如果有的话,具体看型号手册)。软件上也可以在读之前多读几次取稳定值,但根本办法还是硬件上给确定电平。4.4 通信偶尔失败,重启后恢复现象是跑一段时间后通信失败,断电重启又好了。原因可能是总线被干扰,或者延时太短导致时序临界。解决方法是适当加大IIC_Delay的延时,降低通信速率,同时在 SDA 和 SCL 上并一个小电容滤波。另外检查电源是否稳定,PCA9555 供电波动也会导致通信异常。4.5 多字节读写时数据错位现象是连续读写多个寄存器时,第二个字节开始错位。原因是 PCA9555 支持地址自增,连续读写时寄存器地址会自动加一。如果你不想自增,每次操作都要重新发起始条件并发送寄存器命令字节。解决方法是明确自己的读写意图,要么利用自增连续读写,要么每次单独寻址。5. 把模拟 IIC 封装成可复用驱动与验证方法5.1 封装成独立驱动文件把模拟 IIC 的底层和 PCA9555 的操作分成两个文件,底层只管起始、停止、收发字节,PCA9555 文件只管寄存器和业务逻辑。这样换一个从机,底层不用动。我一般会定义一组函数指针或者宏,方便换 IO 口。// pca9555.h #ifndef __PCA9555_H #define __PCA9555_H #include "stm32f10x.h" #define PCA9555_ADDR_W 0x40 #define PCA9555_ADDR_R 0x41 void PCA9555_Init(void); void PCA9555_WriteReg(uint8_t reg, uint8_t data); uint8_t PCA9555_ReadReg(uint8_t reg); void PCA9555_WritePort0(uint8_t data); uint8_t PCA9555_ReadPort0(void); #endif封装好之后,业务层只调PCA9555_WritePort0和PCA9555_ReadPort0,不用关心 IIC 时序。这样代码可读性和可维护性都好很多。5.2 用回环测试验证时序没有逻辑分析仪的时候,我会用一个简单的回环测试验证模拟 IIC 时序。把 PCA9555 的某个输出引脚和某个输入引脚短接,写输出再读输入,看是否一致。一致说明时序和寄存器操作都对。这个方法虽然土,但很有效,能快速定位是时序问题还是寄存器配置问题。// 回环测试:P0_0 输出,P0_4 输入,短接 P0_0 和 P0_4 void PCA9555_LoopbackTest(void) { PCA9555_WriteReg(0x03, 0xEF); // P0_4 输入,其余输出 PCA9555_WritePort0(0x01); // P0_0 输出高 if (PCA9555_ReadPort0() & 0x10) { // P0_4 读到高,回环成功 } PCA9555_WritePort0(0x00); // P0_0 输出低 if (!(PCA9555_ReadPort0() & 0x10)) { // P0_4 读到低,回环成功 } }这个测试能覆盖起始、停止、发送、接收和应答的完整流程,跑通了基本说明模拟 IIC 没问题。5.3 速率与稳定性的取舍模拟 IIC 的速率和稳定性是一对矛盾。延时越小速率越高,但越容易受干扰和主频波动影响。我一般会把延时设到通信稳定的最小值再留 20% 余量。比如稳定跑 100kHz 需要 5us 延时,我就设 6us。这样温度变化或电源波动时不容易翻车。另外,如果系统里有中断会打断 IIC 时序,最好在 IIC 操作期间关中断,或者保证中断服务程序足够短。从那以后我每次做模拟 IIC,都会先用回环测试把时序跑通,再上真实从机,最后用逻辑分析仪看一眼波形。这个习惯帮我省了很多返工时间。希望帮到你。本文还有配套的精品资源,点击获取