重新写一遍单片机程序(分层与接口)------第四篇 把IIC驱动重做一遍 基于stm32f103 前言‌ 上一篇结尾说下一篇写 IIC这篇就来写 IIC。‌ 动手前翻旧代码发现了一件挺尴尬的事旧 IIC 驱动里硬件 I2C 那条路是个空壳。‌ 设备层的 Init、Read、Write 里都有这么一段分支if (self-i2c I2C_3_ANALOG) { /* 什么都没写 */ } else if (self-i2c I2C_3_ANALOG self-i2c MAX_I2C) { AnalogI2cWriteMemory(self-i2c, slaveAddr, regAddr, data, length, I2C_TIMEOUT_TIMES); } return result;‌result一直是I2C_ERROR_NONE。也就是说凡是配成I2C_1、I2C_2的设备读写调用都会直接返回成功一个字节都没发出去。‌ 而仓库里三个用 IIC 的模块EEPROM、LIS2DH12TR 加速度计、AHT20 温湿度配的全是I2C_1。按这份代码看它们从来没跟芯片通过一次话。‌ 真正有实现的是软件模拟那一条I2C_3_ANALOGPD14 / PD15 两个脚来回拉。这次重做把两条路都补成真的硬件 I2C 用标准外设库那套事件轮询软件模拟沿用旧代码的位翻转两条路给同一个设备类当后端。‌ 板子还是野火指南者STM32F103VE板载 AT24C02 挂在 PB6 / PB7 上7 位地址 0x50。‌ 这篇的文字也是 AI 帮忙整理的。1 旧 IIC 驱动里的三个问题1.1 一张表里混着两种实现‌ 旧板级表把硬件 I2C 和软件模拟塞进了同一个数组#define I2C_HARDWARE_MAPPING_TABLE \ {\ {{RCC_APB1PeriphClockCmd,RCC_APB1Periph_I2C1,I2C1,(100*1000),0x56,I2C_AcknowledgedAddress_7bit},\ {RCC_APB2PeriphClockCmd,RCC_APB2Periph_GPIOB,GPIOB,GPIO_Pin_6}, \ {RCC_APB2PeriphClockCmd,RCC_APB2Periph_GPIOB,GPIOB,GPIO_Pin_7}}, \ {{RCC_APB1PeriphClockCmd,RCC_APB1Periph_I2C2,I2C2,(100*1000),0x56,I2C_AcknowledgedAddress_7bit},\ {RCC_APB2PeriphClockCmd,RCC_APB2Periph_GPIOB,GPIOB,GPIO_Pin_10},\ {RCC_APB2PeriphClockCmd,RCC_APB2Periph_GPIOB,GPIOB,GPIO_Pin_11}},\ {{NULL,0,NULL,0,0,0},\ {RCC_APB2PeriphClockCmd,RCC_APB2Periph_GPIOD,GPIOD,GPIO_Pin_15}, \ {RCC_APB2PeriphClockCmd,RCC_APB2Periph_GPIOD,GPIOD,GPIO_Pin_14}}, \ }‌ 前两项是硬件外设第三项是两条普通 GPIO。表项类型却是一模一样的I2cParmConfig_t软件模拟那一路把硬件字段全填了 0 和 NULL。‌ 上层按i2c I2C_3_ANALOG判断走哪条分支。这个枚举值本身成了分派依据加一路模拟 I2C 就得改设备层。1.2 硬件分支是空的‌ 除了分派分支为空硬件初始化的返回值也没人看。I2CHardWareInit配好了时钟、引脚、外设但设备层的I2CInit根本没调用它只在硬件分支里留了个空括号。‌ 旧代码里还有一个I2CHardWareWriteMemory函数体是I2cErrorCode_t I2CHardWareWriteMemory(I2cType_t i2c) { if (i2c I2C_3_ANALOG) { return I2C_ERROR_TYPE; } return I2C_ERROR_NONE; }‌ 参数表里连数据指针和长度都没有直接返回成功。‌ 原工程AT32F435 那一版的情况更有意思。它把雅特力官方的 I2C 应用层库i2c_application.c整份引进来了两千四百多行i2c_memory_read/i2c_memory_write的轮询、中断、DMA 三套实现都齐。但设备层里这几个调用全被注释掉了那两千多行从头到尾没执行过一行。移植到 F103 的时候这份文件没带过来空分支就留在了原地。1.3 运算符优先级‌ 模拟 I2C 里处理 10 位地址的代码if (slaveAddr 0xff00) { AnalogI2cSendByte(i2c, (slaveAddr 0xff00 8)); AnalogI2cSendByte(i2c, (slaveAddr 0xff) | 0x00); }‌的优先级比高slaveAddr 0xff00 8实际算的是slaveAddr 0xff。高字节永远取不出来10 位地址设备发出去的第一个字节是错的。‌ 同样的写法在AnalogI2cReadMemory里也有。‌ 另外三个模块的从机地址约定也不统一#define SLAVE_EEPROM_ADDR (0xA0) #define SLAVE_LIS2DH12TR_ADDR (0x30) #define SLAVE_AHT20_ADDR 0x38‌ 0xA0 和 0x30 是含读写位的 8 位形式0x38 是 7 位形式。驱动的slaveAddr | 0x01只会对前两个碰巧成立第三个会在总线上发一个错误的地址。‌ 这次新接口只说 7 位地址读写位由实现拼。2 接口一次传输到底是什么‌ 新的 ops_i2c 表放在ops/ops_i2c.h四个方法初始化、反初始化、写、读。typedef struct { OpsStatus_t (*Init)(uint8_t busId, const OpsI2cPin_t* pins, uint8_t pinCount, uint32_t speedHz); void (*DeInit)(uint8_t busId); OpsStatus_t (*Write)(uint8_t busId, uint8_t slaveAddr, const uint8_t* prefix, uint16_t prefixLength, const uint8_t* data, uint16_t length); OpsStatus_t (*Read)(uint8_t busId, uint8_t slaveAddr, const uint8_t* txData, uint16_t txLength, uint8_t* rxData, uint16_t rxLength); }OpsI2cOps_t; const OpsI2cOps_t* OpsI2cGetOps(void);‌Write比常见的 I2C 接口多一个前缀段。原因是寄存器式写要发「从机地址 寄存器地址 数据」这三段必须在同一次总线事务里完成中间不能有停止条件。如果接口只收一段数据设备层就得在栈上拼一个临时缓冲或者调两次写第二次会重新发起始条件AT24C02 会把数据当成新的地址字节。‌ 前缀段把这件事交给实现/* 设备层 */ return self-Hw-Write(self-BusId, slaveAddr, regBuffer, regLength, data, length); /* 芯片层按顺序发起始、地址(写)、前缀段、数据段、停止 */‌ 没有临时缓冲也没有长度上限。‌Read的语义是「起始 地址(写) 发送段 重复起始 地址(读) 接收段 停止」。发送段就是寄存器地址可以是 NULL / 0此时就是直接读。重复起始放在芯片实现里完成中间不产生停止条件。‌ 接口约定从机地址一律 7 位addr 1和读写位都由实现拼。‌ 头文件里还写死了几条总线级超时由实现兜底失败时总线必须回到空闲业务级重试和器件时序属于上层比如 EEPROM 的写周期实现不等待四个方法都只能在任务上下文调用。3 硬件实现‌chip/ops_i2c.c用 STM32F10x 标准外设库。两路总线先做成一张小表typedef struct { I2C_TypeDef* Instance; ChipClock_t Clock; }ChipI2cHw_t; static const ChipI2cHw_t ChipI2cHw[OPS_CHIP_I2C_COUNT] { { I2C1, { CHIP_CLOCK_BUS_APB1, RCC_APB1Periph_I2C1 } }, { I2C2, { CHIP_CLOCK_BUS_APB1, RCC_APB1Periph_I2C2 } }, };3.1 等事件顺便看 AF‌ 标准库的 I2C 是事件驱动的每一步都要等对应的状态组合。等事件的函数长这样static OpsStatus_t ChipI2cWaitEvent(I2C_TypeDef* i2c, uint32_t event) { uint32_t timeout CHIP_I2C_TIMEOUT; while (I2C_CheckEvent(i2c, event) ! SUCCESS) { if (I2C_GetFlagStatus(i2c, I2C_FLAG_AF) ! RESET) { return OPS_ERR_HW; } if (0u timeout) { return OPS_ERR_TIMEOUT; } timeout--; } return OPS_OK; }‌ 从机不应答的时候地址字节发完会置 AF 标志而期望的事件永远不会来。如果只靠超时计数从机没接、地址写错这几种情况每次都要等满几十毫秒。这里每次循环顺手看一眼 AF直接返回硬件错误快得多。‌ 超时值是个常量200000次循环。它只跟 CPU 主频有关不涉及任何调度器芯片层也就不需要知道有没有 RTOS。3.2 出错恢复‌ F1 的 I2C 有个出了名的脾气一旦状态机卡在半路BUSY 位可能一直不清之后的传输全部失败。每次出错都做三件事static void ChipI2cAbort(uint8_t busId) { const ChipI2cHw_t* hw ChipI2cHw[busId]; I2C_GenerateSTOP(hw-Instance, ENABLE); I2C_SoftwareResetCmd(hw-Instance, ENABLE); I2C_SoftwareResetCmd(hw-Instance, DISABLE); ChipI2cConfigure(hw, ChipI2cSpeed[busId]); }‌ 先补一个停止条件再软件复位外设状态机然后按原来的速度重新配置。每路当前的速度存在文件私有的数组里Abort 的时候不用再问上层。‌ 这一步只把外设拉回可用状态。如果从机真的把 SDA 拉死不放软件复位也救不了得用 GPIO 打九个时钟让它释放——这次没做留给以后真碰上再说。3.3 写‌ 写的流程就是标准的四步起始、地址、数据、停止。static OpsStatus_t ChipI2cWrite(uint8_t busId, uint8_t slaveAddr, const uint8_t* prefix, uint16_t prefixLength, const uint8_t* data, uint16_t length) { I2C_TypeDef* i2c; OpsStatus_t ret; if (busId OPS_CHIP_I2C_COUNT) { return OPS_ERR_PARAM; } if (slaveAddr 0x7Fu) { return OPS_ERR_PARAM; } if ((NULL prefix) (0u ! prefixLength)) { return OPS_ERR_NULL; } if ((NULL data) (0u ! length)) { return OPS_ERR_NULL; } i2c ChipI2cHw[busId].Instance; I2C_GenerateSTART(i2c, ENABLE); ret ChipI2cWaitEvent(i2c, I2C_EVENT_MASTER_MODE_SELECT); if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } I2C_Send7bitAddress(i2c, (uint8_t)(slaveAddr 1), I2C_Direction_Transmitter); ret ChipI2cWaitEvent(i2c, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } ret ChipI2cSendBytes(i2c, prefix, prefixLength); if (OPS_OK ret) { ret ChipI2cSendBytes(i2c, data, length); } if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } I2C_GenerateSTOP(i2c, ENABLE); return OPS_OK; }‌ChipI2cSendBytes是一个字节一个字节发、每个字节等 EV8static OpsStatus_t ChipI2cSendBytes(I2C_TypeDef* i2c, const uint8_t* data, uint16_t length) { OpsStatus_t ret; uint16_t i; for (i 0u; i length; i) { I2C_SendData(i2c, data[i]); ret ChipI2cWaitEvent(i2c, I2C_EVENT_MASTER_BYTE_TRANSMITTED); if (OPS_OK ! ret) { return ret; } } return OPS_OK; }3.4 读尾巴要提前处理‌ 读比写麻烦第一版还写错了上板跑出来是超时。先把两个坑说清楚。‌ 接收不能用事件检查。标准库的I2C_EVENT_MASTER_BYTE_RECEIVED里包含 BUSY 位最后一个字节收完之后 STOP 生效、BUSY 清零最后一次检查必然失败。接收段只等 RXNE 就够了static OpsStatus_t ChipI2cWaitFlag(I2C_TypeDef* i2c, uint32_t flag) { uint32_t timeout CHIP_I2C_TIMEOUT; while (I2C_GetFlagStatus(i2c, flag) RESET) { if (0u timeout) { return OPS_ERR_TIMEOUT; } timeout--; } return OPS_OK; }‌ 第二个坑是停止条件的时机。第一版在还剩两个字节的时候就把 ACK 关了、STOP 预约上想的是最后一个字节收 NACK 后自然停止。实际跑起来STOP 会在倒数第二个字节的 ACK 位之后生效最后一个字节根本发不出来等 RXNE 等到超时。正确的顺序是读完倒数第二个字节后关 ACK最后一个字节收完读走再生成停止条件。‌ 完整的读static OpsStatus_t ChipI2cRead(uint8_t busId, uint8_t slaveAddr, const uint8_t* txData, uint16_t txLength, uint8_t* rxData, uint16_t rxLength) { I2C_TypeDef* i2c; OpsStatus_t ret; uint16_t remaining; if (busId OPS_CHIP_I2C_COUNT) { return OPS_ERR_PARAM; } if (slaveAddr 0x7Fu) { return OPS_ERR_PARAM; } if (NULL rxData) { return OPS_ERR_NULL; } if (0u rxLength) { return OPS_ERR_PARAM; } if ((NULL txData) (0u ! txLength)) { return OPS_ERR_NULL; } i2c ChipI2cHw[busId].Instance; /* 阶段一起始 从机地址(写) 发送段 */ I2C_GenerateSTART(i2c, ENABLE); ret ChipI2cWaitEvent(i2c, I2C_EVENT_MASTER_MODE_SELECT); if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } I2C_Send7bitAddress(i2c, (uint8_t)(slaveAddr 1), I2C_Direction_Transmitter); ret ChipI2cWaitEvent(i2c, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } ret ChipI2cSendBytes(i2c, txData, txLength); if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } /* 阶段二重复起始 从机地址(读) 接收段 */ I2C_GenerateSTART(i2c, ENABLE); ret ChipI2cWaitEvent(i2c, I2C_EVENT_MASTER_MODE_SELECT); if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } I2C_Send7bitAddress(i2c, (uint8_t)(slaveAddr 1), I2C_Direction_Receiver); ret ChipI2cWaitEvent(i2c, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); if (OPS_OK ! ret) { ChipI2cAbort(busId); return ret; } if (1u rxLength) { /* 只收一个字节读之前就要关 ACK 并预约 STOP */ I2C_AcknowledgeConfig(i2c, DISABLE); I2C_GenerateSTOP(i2c, ENABLE); ret ChipI2cWaitFlag(i2c, I2C_FLAG_RXNE); if (OPS_OK ret) { rxData[0] I2C_ReceiveData(i2c); } } else { remaining rxLength; while (remaining 0u) { /* 读完倒数第二个字节后给最后一个字节关 ACK让它回 NACK 最后一个字节读走之后再生成停止条件 */ if (1u remaining) { I2C_AcknowledgeConfig(i2c, DISABLE); } ret ChipI2cWaitFlag(i2c, I2C_FLAG_RXNE); if (OPS_OK ! ret) { break; } *rxData I2C_ReceiveData(i2c); rxData; remaining--; if (0u remaining) { I2C_GenerateSTOP(i2c, ENABLE); } } } /* 恢复 ACK 供下次使用 */ I2C_AcknowledgeConfig(i2c, ENABLE); if (OPS_OK ! ret) { ChipI2cAbort(busId); } return ret; }‌ 收一个字节的分支要单独处理。只收一个的时候没有「倒数第二个字节」这个时机只能在读之前就关 ACK、预约 STOP这个顺序反倒是对的上板验证过。3.5 初始化‌ 初始化按一整路入参总线号、引脚数组、速度。引脚角色是OPS_I2C_ROLE_SCL/OPS_I2C_ROLE_SDA复用编号留给 GPIO 实现static OpsStatus_t ChipI2cInit(uint8_t busId, const OpsI2cPin_t* pins, uint8_t pinCount, uint32_t speedHz) { /* ...参数与引脚角色检查... */ gpio OpsGpioGetOps(); if (NULL gpio) { return OPS_ERR_UNSUPPORTED; } ret gpio-InitI2c(busId, pins, pinCount); if (OPS_OK ! ret) { return ret; } if (0u speedHz) { speedHz CHIP_I2C_DEFAULT_SPEED; } ChipI2cSpeed[busId] speedHz; hw ChipI2cHw[busId]; ChipClockEnable(hw-Clock); I2C_SoftwareResetCmd(hw-Instance, ENABLE); I2C_SoftwareResetCmd(hw-Instance, DISABLE); ChipI2cConfigure(hw, speedHz); return OPS_OK; }‌ 上电或者上次异常之后可能残留 BUSY所以配置之前先软件复位一次。4 设备类和两种后端4.1 板级表‌ 板级表只写「板子上怎么接」typedef enum { I2C_EEPROM 0, /* 板载 AT24C02SCL PB6 / SDA PB7 */ I2C_MAX, }BoardI2cId_t; typedef struct { OpsPin_t Scl; OpsPin_t Sda; uint8_t BusId; uint32_t SpeedHz; }BoardI2cConfig_t; extern const BoardI2cConfig_t BoardI2cConfig[I2C_MAX];const BoardI2cConfig_t BoardI2cConfig[I2C_MAX] { [I2C_EEPROM] { { OPS_PORT_B, OPS_PIN(6) }, { OPS_PORT_B, OPS_PIN(7) }, 0u, 100000u }, };‌ 从机地址不进板级表。一条总线可以挂好几个从机地址是调用的参数。4.2 一个设备类两张方法表‌ 设备类对外只有三个方法typedef struct { OpsStatus_t (*Read)(DevI2c_t* self, uint8_t slaveAddr, uint16_t regAddr, uint8_t* data, uint16_t length); OpsStatus_t (*Write)(DevI2c_t* self, uint8_t slaveAddr, uint16_t regAddr, const uint8_t* data, uint16_t length); OpsStatus_t (*Delete)(DevI2c_t* self); }DevI2cOps_t;‌ 后端在创建时选typedef enum { DEV_I2C_BACKEND_HARDWARE 0, /* 硬件外设后端 */ DEV_I2C_BACKEND_SOFT, /* GPIO 位翻转后端 */ DEV_I2C_BACKEND_MAX, }DevI2cBackend_t;‌ 创建时按后端挂不同的方法表DevI2c_t* I2cCreate(I2cId_t id, DevI2cBackend_t backend) { const BoardI2cConfig_t* config; const OpsGpioOps_t* gpio; const OpsI2cOps_t* hw NULL; const OsMemOps_t* mem; DevI2c_t* self; OpsStatus_t ret OPS_OK; if (id (I2cId_t)I2C_MAX) { return NULL; } if (backend DEV_I2C_BACKEND_MAX) { return NULL; } /* 设备层自己取能力上层不需要装配 */ mem OsMemGetOps(); if (NULL mem) { return NULL; } gpio OpsGpioGetOps(); if (NULL gpio) { return NULL; } if (DEV_I2C_BACKEND_HARDWARE backend) { hw OpsI2cGetOps(); if (NULL hw) { return NULL; } } config BoardI2cConfig[id]; self (DevI2c_t*)mem-Alloc((uint32_t)sizeof(DevI2c_t)); if (NULL self) { return NULL; } self-Id id; self-Backend backend; self-Scl config-Scl; self-Sda config-Sda; self-BusId config-BusId; self-SpeedHz config-SpeedHz; self-Gpio gpio; self-Hw hw; if (DEV_I2C_BACKEND_HARDWARE backend) { OpsI2cPin_t pins[2]; self-Ops I2cHardwareOps; pins[0].Role OPS_I2C_ROLE_SCL; pins[0].Pin config-Scl; pins[1].Role OPS_I2C_ROLE_SDA; pins[1].Pin config-Sda; ret hw-Init(config-BusId, pins, 2u, config-SpeedHz); } else { self-Ops I2cSoftOps; ret gpio-Init(config-Scl, OPS_GPIO_OUT_OD); if (OPS_OK ret) { ret gpio-Init(config-Sda, OPS_GPIO_OUT_OD); } if (OPS_OK ret) { /* 总线回空闲两条线都放高 */ (void)gpio-Write(config-Scl, 1u); (void)gpio-Write(config-Sda, 1u); } } if (OPS_OK ! ret) { mem-Free(self); return NULL; } return self; }‌ 能力表都是设备层自己取的上层拿到的就是一句I2cCreate(I2C_EEPROM, DEV_I2C_BACKEND_HARDWARE)。‌ 硬件后端的方法只是把寄存器地址拼好再交给芯片层static uint8_t BuildRegAddr(uint16_t regAddr, uint8_t buffer[2]) { if (regAddr 0xFFu) { buffer[0] (uint8_t)(regAddr 8); buffer[1] (uint8_t)(regAddr 0xFFu); return 2u; } buffer[0] (uint8_t)regAddr; return 1u; } static OpsStatus_t I2cHardwareWrite(DevI2c_t* self, uint8_t slaveAddr, uint16_t regAddr, const uint8_t* data, uint16_t length) { uint8_t regBuffer[2]; uint8_t regLength; if (NULL self) { return OPS_ERR_STATE; } if ((NULL data) (0u ! length)) { return OPS_ERR_NULL; } regLength BuildRegAddr(regAddr, regBuffer); return self-Hw-Write(self-BusId, slaveAddr, regBuffer, regLength, data, length); }‌ 16 位寄存器地址发两个字节高字节在前。这套拼接硬件和软件两个后端共用。4.3 软件模拟那一条‌ 软件后端的全部实现都在设备层里用的是 GPIO 接口不碰任何芯片符号。‌ 延时沿用旧代码的空循环变量加了volatile防止编译器在优化的时候把整个循环删掉static void I2cSoftDelay(void) { volatile uint32_t i; for (i 0u; i 10u; i) { } }‌ 起始条件和停止条件static void I2cSoftStart(DevI2c_t* self) { I2cSoftSdaHigh(self); I2cSoftSclHigh(self); I2cSoftDelay(); I2cSoftSdaLow(self); I2cSoftDelay(); I2cSoftSclLow(self); I2cSoftDelay(); } static void I2cSoftStop(DevI2c_t* self) { I2cSoftSdaLow(self); I2cSoftDelay(); I2cSoftSclHigh(self); I2cSoftDelay(); I2cSoftSdaHigh(self); I2cSoftDelay(); }‌ 发一个字节第九个时钟读应答static uint8_t I2cSoftWriteByte(DevI2c_t* self, uint8_t data) { uint8_t i; uint8_t ack; for (i 0u; i 8u; i) { if (0u ! (data 0x80u)) { I2cSoftSdaHigh(self); } else { I2cSoftSdaLow(self); } I2cSoftDelay(); I2cSoftSclHigh(self); I2cSoftDelay(); I2cSoftSclLow(self); I2cSoftDelay(); data 1; } /* 第 9 个时钟释放 SDA由从机拉低表示应答 */ I2cSoftSdaHigh(self); I2cSoftDelay(); I2cSoftSclHigh(self); I2cSoftDelay(); ack I2cSoftSdaRead(self); I2cSoftSclLow(self); I2cSoftDelay(); return ack; }‌ 读一个字节static uint8_t I2cSoftReadByte(DevI2c_t* self) { uint8_t i; uint8_t data 0u; I2cSoftSdaHigh(self); /* 释放 SDA交给从机驱动 */ for (i 0u; i 8u; i) { data 1; I2cSoftSclHigh(self); I2cSoftDelay(); if (0u ! I2cSoftSdaRead(self)) { data | 0x01u; } I2cSoftSclLow(self); I2cSoftDelay(); } return data; }‌ 这里有个偷懒但正确的做法SDA 一直配成开漏输出读的时候直接读引脚电平。F1 的输入数据寄存器在开漏输出模式下仍然反映引脚的实际电压所以不用来回切输入输出方向。旧代码就是这么干的这次照搬。‌ 完整的一次写static OpsStatus_t I2cSoftWrite(DevI2c_t* self, uint8_t slaveAddr, uint16_t regAddr, const uint8_t* data, uint16_t length) { uint8_t regBuffer[2]; uint8_t regLength; uint16_t i; if (NULL self) { return OPS_ERR_STATE; } if ((NULL data) (0u ! length)) { return OPS_ERR_NULL; } regLength BuildRegAddr(regAddr, regBuffer); I2cSoftStart(self); if (0u ! I2cSoftWriteByte(self, (uint8_t)(slaveAddr 1))) { I2cSoftStop(self); return OPS_ERR_HW; } for (i 0u; i regLength; i) { if (0u ! I2cSoftWriteByte(self, regBuffer[i])) { I2cSoftStop(self); return OPS_ERR_HW; } } for (i 0u; i length; i) { if (0u ! I2cSoftWriteByte(self, data[i])) { I2cSoftStop(self); return OPS_ERR_HW; } } I2cSoftStop(self); return OPS_OK; }‌ 完整的一次读中间有一次重复起始static OpsStatus_t I2cSoftRead(DevI2c_t* self, uint8_t slaveAddr, uint16_t regAddr, uint8_t* data, uint16_t length) { uint8_t regBuffer[2]; uint8_t regLength; uint16_t i; if (NULL self) { return OPS_ERR_STATE; } if (NULL data) { return OPS_ERR_NULL; } if (0u length) { return OPS_ERR_PARAM; } regLength BuildRegAddr(regAddr, regBuffer); I2cSoftStart(self); if (0u ! I2cSoftWriteByte(self, (uint8_t)(slaveAddr 1))) { I2cSoftStop(self); return OPS_ERR_HW; } for (i 0u; i regLength; i) { if (0u ! I2cSoftWriteByte(self, regBuffer[i])) { I2cSoftStop(self); return OPS_ERR_HW; } } /* 重复起始切到读方向 */ I2cSoftStart(self); if (0u ! I2cSoftWriteByte(self, (uint8_t)((slaveAddr 1) | 0x01u))) { I2cSoftStop(self); return OPS_ERR_HW; } for (i 0u; i length; i) { data[i] I2cSoftReadByte(self); if ((uint16_t)(i 1u) length) { I2cSoftSendAck(self, 0u); /* 还要继续读应答 */ } else { I2cSoftSendAck(self, 1u); /* 最后一个字节不应答 */ } } I2cSoftStop(self); return OPS_OK; }5 PC 测试‌ 设备层在 PC 上测硬件侧全部换成假表。这次多了一张假的 I2C 表和之前的假 GPIO、假内存表一起链接{ name: dev_i2c, source: tools/tests/test_dev_i2c.c, mocks: [tools/tests/mocks/mock_ops_i2c.c, tools/tests/mocks/mock_ops_gpio.c, tools/tests/mocks/mock_os_mem.c], impl: [Demo/User/device/dev_i2c.c, Demo/User/board/board_i2c.c], include: [Demo/User, tools/tests/mocks], },‌ 假 I2C 表记录每次调用的参数还能预设返回值typedef struct { unsigned InitCount; unsigned DeInitCount; unsigned WriteCount; unsigned ReadCount; uint8_t LastBusId; uint32_t LastSpeedHz; uint8_t LastPinCount; OpsI2cPin_t LastPins[2]; uint8_t LastPrefix[MOCK_I2C_MAX_BYTES]; uint16_t LastPrefixLength; uint8_t LastData[MOCK_I2C_MAX_BYTES]; uint16_t LastDataLength; /* ... */ uint8_t Available; /* 0 时 OpsI2cGetOps() 返回 NULL */ OpsStatus_t InitResult; OpsStatus_t WriteResult; OpsStatus_t ReadResult; }MockI2c_t;‌Available这个字段让「实现层没提供能力」也能测OpsI2cGetOps()返回 NULL创建必须失败。板子上造不出这种场景。‌ 用例覆盖了这些硬件后端的创建与 Init 参数、8 位和 16 位寄存器地址的拼接、读写透传、错误码原样返回、软件后端创建时两次开漏初始化、软件后端的写和读假 GPIO 返回应答、从机不应答时返回硬件错误、参数校验空指针、长度 0、只写地址、板级 id 与后端越界、内存申请失败、初始化失败回滚、两种对象的删除。‌ 软件后端在 PC 上跑位翻转从机应答是假的把假 GPIO 的读值设成 0所有读都是应答。这样至少能证明状态机不会卡死、错误路径能返回。‌ 加上之前的三套现在一共 5 个套件、265 项断言。6 上板验证‌ 验证任务做四件事硬件后端写读、软件后端交叉读、软件后端写读、无效地址的错误处理。static void I2cVerifyTask(void* parameter) { DevI2c_t* i2c; uint8_t writeData[8] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }; uint8_t readData[8]; uint8_t mismatch; uint8_t i; (void)parameter; ModifySystemLogLevel(LOG_LEVEL_INFO); i2c I2cCreate(I2C_EEPROM, DEV_I2C_BACKEND_HARDWARE); if (NULL i2c) { LogPrintf(LOG_LEVEL_ERROR, I2C HW create failed); } else { if (OPS_OK i2c-Ops-Write(i2c, 0x50, 0x0000, writeData, 8)) { LogPrintf(LOG_LEVEL_INFO, I2C HW write ok); } else { LogPrintf(LOG_LEVEL_ERROR, I2C HW write FAIL); } vTaskDelay(pdMS_TO_TICKS(10)); memset(readData, 0, sizeof(readData)); if (OPS_OK ! i2c-Ops-Read(i2c, 0x50, 0x0000, readData, 8)) { LogPrintf(LOG_LEVEL_ERROR, I2C HW read FAIL); } else { mismatch 0; for (i 0; i 8; i) { if (readData[i] ! writeData[i]) { mismatch; } } LogPrintf(LOG_LEVEL_INFO, I2C HW read mismatch%d, (int)mismatch); } /* ...无效地址 0x51 的错误处理与总线恢复... */ (void)i2c-Ops-Delete(i2c); } /* 软件后端交叉读硬件刚写的 8 个字节再写另一组数据读回 */ /* ... */ LogPrintf(LOG_LEVEL_INFO, I2C verify done); vTaskDelete(NULL); }‌ 硬件写完之后要延时 10 毫秒再读。AT24C02 收到停止条件后开始内部写周期这段时间它不应答驱动不等这个等不等由调用方决定。‌ 验证里还专门读了一次 0x51。板载 EEPROM 的地址是 0x500x51 上没有器件读它必然收不到应答。这一步看两件事驱动返回错误而不是卡死以及出错之后总线还能不能继续用。‌ 两条后端用的是同一组引脚 PB6 / PB7。硬件后端用完删掉软件后端重新把这两个脚配成普通开漏输出再跑一遍同样的读写。‌ 第一次上板的结果是这样I2C HW write ok I2C HW read FAIL I2C HW NACK returned error ok I2C HW bus stuck after NACK I2C SOFT cross mismatch0 I2C SOFT write ok I2C SOFT read mismatch0 I2C SOFT NACK returned error ok I2C SOFT bus still alive‌ 软件那条路全对交叉读也一致说明数据确实写进了 EEPROM硬件写的那条路也是通的。硬件读失败了。‌ 为了定位在验证任务里加了两条打印一条直接调芯片层读一个字节另一条把设备层读 8 字节的返回码打出来。返回码是统一的OpsStatus_t3 表示超时4 表示从机没应答。改动之后I2C HW read1 ret0 val11 I2C HW read FAIL ret3‌ 读一个字节成功值是 0x11正是写进去的第一个字节。起始、地址、重复起始、收字节、NACK、停止这一整套时序都没问题。超时只出现在多字节那一段。‌ 原因就是前面说的第二个坑STOP 预约得太早落在倒数第二个字节的 ACK 之后最后一个字节没发出来。改成读完倒数第二个字节再关 ACK、最后一个字节读走再设 STOP重新下载I2C HW write ok I2C HW read1 ret0 val11 I2C HW read mismatch0 I2C HW NACK returned error ok I2C HW bus still alive I2C SOFT cross mismatch0 I2C SOFT write ok I2C SOFT read mismatch0 I2C SOFT NACK returned error ok I2C SOFT bus still alive I2C verify done‌ 两条后端都通过。硬件写的数据用软件后端读出来是mismatch0反过来软件写的数据硬件也能读。无效地址返回错误之后总线还能继续用软件复位那一步有效。结语‌ 这一路的代码量比串口小麻烦在两条后端要维持一致的行为同样的寄存器地址拼接、同样的错误码、同样的 7 位地址约定。硬件那条还有 F1 的勘误要照顾出错之后必须把外设拉回空闲不然下一次调用直接挂死。‌ 代价是每个操作都要在芯片层和软件层各维护一份时序。好在两边的接口收得很窄读写的语义完全一样上层换后端只改一个枚举值。软件模拟的位延时是空转速度比硬件慢一截只适合低速器件或者引脚被占用的场景。‌ 总线恢复只做到了软件复位这一层。NACK 之后外设能拉回空闲这个验证过了如果从机把 SDA 彻底拉死不放软件复位也救不了得用 GPIO 打九个时钟这次没做。等真碰上再加。‌ 下一篇写 SPI。