STM32F4 Unity 单元测试Mock 与 I2C 模拟详细指南本文档介绍在 STM32F4 Unity 单元测试框架中如何加入 Mock 来模拟 I2C 设备从而在不依赖真实硬件的情况下测试业务逻辑。文档包含多种方案从轻量级函数指针到自动化 CMock可根据项目需求选择。摘要本文介绍在 STM32F4 Unity 单元测试框架中通过 Mock 技术模拟 I2C 设备实现不依赖真实硬件的业务逻辑测试。文章系统梳理了五种方案函数指针推荐入门、链接时替换零侵入、CMock Ceedling自动化、StubWithCallback复杂设备模拟与影子寄存器寄存器级驱动并给出方案对比与选择建议最后说明如何与多次测试失败率统计集成帮助构建稳定、可重复的嵌入式单元测试体系。1. 为什么需要 Mock I2C在嵌入式单元测试中直接操作硬件如 I2C 外设会导致测试必须在真实硬件上运行无法在 PC 上快速执行。测试结果受硬件状态、时序、外部器件影响不稳定。无法模拟异常场景如 NACK、超时、数据错误。通过 Mock I2C可以将业务逻辑与硬件访问解耦使测试在 PC 或 STM32 上均可运行。完全可控可模拟任意返回值。快速、可重复、易于集成 CI。2. 方案一函数指针推荐入门2.1 设计思路在业务代码与 STM32 HAL 之间插入一层函数指针接口。真实运行时指向 HAL 封装函数测试时替换为 Mock 函数。2.2 定义 I2C 操作接口新建i2c_hal.h#ifndefI2C_HAL_H#defineI2C_HAL_H#includestdint.h#includestdbool.h// 定义 I2C 操作函数的指针类型typedefbool(*i2c_write_fn)(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen);typedefbool(*i2c_read_fn)(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen);// 声明全局函数指针externi2c_write_fn i2c_write;externi2c_read_fn i2c_read;// 初始化函数用于绑定真实的 HAL 实现voidi2c_hal_init(void);#endif2.3 提供真实的 STM32 实现新建i2c_hal.c#includei2c_hal.h#includemain.h// 包含 hi2c1 等externI2C_HandleTypeDef hi2c1;staticboolreal_i2c_write(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen){returnHAL_I2C_Mem_Write(hi2c1,dev_addr1,reg,I2C_MEMADD_SIZE_8BIT,data,len,100)HAL_OK;}staticboolreal_i2c_read(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen){returnHAL_I2C_Mem_Read(hi2c1,dev_addr1,reg,I2C_MEMADD_SIZE_8BIT,data,len,100)HAL_OK;}i2c_write_fn i2c_writereal_i2c_write;i2c_read_fn i2c_readreal_i2c_read;voidi2c_hal_init(void){i2c_writereal_i2c_write;i2c_readreal_i2c_read;}2.4 业务代码使用函数指针例如传感器驱动sensor.c#includei2c_hal.hboolsensor_read_temperature(uint8_tdev_addr,float*temperature){uint8_traw_data[2];if(!i2c_read(dev_addr,0x00,raw_data,2)){returnfalse;}// 假设转换公式temp raw / 16.0*temperature((int16_t)((raw_data[0]8)|raw_data[1]))/16.0f;returntrue;}2.5 在测试中替换为 Mock在test_sensor.c中#includeunity.h#includei2c_hal.h#includesensor.h// --- Mock 实现 ---staticuint8_tmock_temp_data[2];staticbool mock_i2c_read_resulttrue;staticboolmock_i2c_read(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen){if(mock_i2c_read_result){data[0]mock_temp_data[0];data[1]mock_temp_data[1];}returnmock_i2c_read_result;}voidsetUp(void){i2c_readmock_i2c_read;// 替换为 Mockmock_i2c_read_resulttrue;}voidtearDown(void){// 可恢复为真实实现i2c_hal_init();}voidtest_sensor_read_temperature_should_return_correct_value(void){mock_temp_data[0]0x01;mock_temp_data[1]0x90;// 原始值 0x0190 400温度 25.0floattemp;bool resultsensor_read_temperature(0x48,temp);TEST_ASSERT_TRUE(result);TEST_ASSERT_FLOAT_WITHIN(0.1f,25.0f,temp);}voidtest_sensor_read_temperature_should_fail_on_i2c_error(void){mock_i2c_read_resultfalse;floattemp;bool resultsensor_read_temperature(0x48,temp);TEST_ASSERT_FALSE(result);}优点简单、无需额外工具、业务代码仅需改为通过函数指针调用。缺点需要稍微调整业务代码架构。3. 方案二链接时替换零侵入3.1 设计思路业务代码仍直接调用HAL_I2C_Mem_Read。在测试构建中不编译真实的 HAL I2C 源文件而是链接一个自己实现的mock_hal_i2c.c其中提供同名函数的模拟版本。3.2 创建 Mock 文件mock_hal_i2c.c#includestm32f4xx_hal.h#includestring.h// 模拟数据缓冲区供测试设置uint8_tmock_i2c_rx_buffer[256];uint16_tmock_i2c_rx_len0;HAL_StatusTypeDef mock_i2c_returnHAL_OK;HAL_StatusTypeDefHAL_I2C_Mem_Read(I2C_HandleTypeDef*hi2c,uint16_tDevAddress,uint16_tMemAddress,uint16_tMemAddSize,uint8_t*pData,uint16_tSize,uint32_tTimeout){if(mock_i2c_returnHAL_OK){memcpy(pData,mock_i2c_rx_buffer,Size);}returnmock_i2c_return;}HAL_StatusTypeDefHAL_I2C_Mem_Write(I2C_HandleTypeDef*hi2c,uint16_tDevAddress,uint16_tMemAddress,uint16_tMemAddSize,uint8_t*pData,uint16_tSize,uint32_tTimeout){returnmock_i2c_return;}3.3 配置构建系统在测试构建中排除stm32f4xx_hal_i2c.c。将mock_hal_i2c.c加入编译。确保链接器优先解析 Mock 符号。优点业务代码零修改。缺点需要掌控构建系统配置较复杂。4. 方案三CMock Ceedling自动化4.1 简介CMock 可根据头文件自动生成 Mock 函数Ceedling 负责管理 Unity、CMock 和构建流程。4.2 定义接口头文件i2c_interface.h#ifndefI2C_INTERFACE_H#defineI2C_INTERFACE_H#includestdint.h#includestdbool.hbooli2c_read(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen);booli2c_write(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen);#endif4.3 在测试中使用自动生成的 Mock#includeunity.h#includemock_i2c_interface.h#includesensor.hvoidtest_sensor_read_temperature_should_return_correct_value(void){uint8_texpected_data[]{0x01,0x90};// 期望 i2c_read 被调用一次返回 true并通过指针输出数据i2c_read_ExpectAndReturn(0x48,0x00,NULL,2,true);i2c_read_ReturnArrayThruPtr_data(expected_data,2);floattemp;bool resultsensor_read_temperature(0x48,temp);TEST_ASSERT_TRUE(result);TEST_ASSERT_FLOAT_WITHIN(0.1f,25.0f,temp);}优点自动化程度高支持参数校验、调用次数验证。缺点需要搭建 Ceedling 环境学习曲线较陡。5. 方案四StubWithCallbackCMock 高级用法当 Mock 行为复杂如模拟完整 I2C 设备寄存器映射时可用StubWithCallback完全接管函数行为。HAL_StatusTypeDefmy_i2c_stub(I2C_HandleTypeDef*hi2c,uint16_tDevAddress,uint32_tTrials,uint32_tTimeout,intnum_calls){if(DevAddress(0x501)){returnHAL_OK;// 模拟地址 0x50 设备存在}returnHAL_ERROR;}voidtest_scanner_should_find_device_at_0x50(void){HAL_I2C_IsDeviceReady_StubWithCallback(my_i2c_stub);uint8_tfoundi2c_scanner_scan_first();TEST_ASSERT_EQUAL_HEX8(0x50,found);}6. 方案五影子寄存器寄存器级驱动若直接操作寄存器而非 HAL 库可在测试环境中用全局变量模拟寄存器。// 测试专用头文件#ifdefUNIT_TESTexternuint32_tV_I2C1_CR1;#defineI2C1_CR1V_I2C1_CR1#else#defineI2C1_CR1(*(volatileuint32_t*)0x40005400)#endif// 测试用例voidtest_i2c_enable_sets_pe_bit(void){i2c_enable(I2C1);TEST_ASSERT_BITS_HIGH(0x01,V_I2C1_CR1);}7. 方案对比与选择方案优点缺点适用场景函数指针简单无需工具逻辑清晰需调整业务代码架构中小项目快速引入 Mock链接时替换对业务代码零侵入需掌控构建系统已有代码不想改动CMock Ceedling自动化功能强大环境搭建成本高中大型项目工程化测试StubWithCallback灵活可模拟复杂设备需 CMock 环境复杂外设行为模拟影子寄存器直接验证寄存器操作仅适用寄存器级代码手写底层驱动验证推荐从**方案一函数指针**开始快速让 I2C 业务逻辑可测试。后续需要自动化时再引入 CMock。8. 与现有 Unity 多次测试方案集成将 Mock 方案与之前文档中的“多次测试失败率统计”结合在setUp()中安装 Mock 函数指针。在tearDown()中恢复真实实现如需在硬件上继续运行。在测试用例中设置 Mock 返回值。多次循环运行测试统计失败率。示例voidsetUp(void){i2c_readmock_i2c_read;i2c_writemock_i2c_write;mock_i2c_read_resulttrue;}voidtearDown(void){i2c_hal_init();// 恢复真实实现}9. 总结通过本文档你可以在 STM32F4 Unity 测试中灵活加入 Mock 来模拟 I2C函数指针最易上手推荐首选。链接时替换零侵入适合已有代码。CMock Ceedling自动化程度最高适合工程化。StubWithCallback应对复杂设备模拟。影子寄存器底层寄存器驱动测试。结合多次测试与失败率统计可构建稳定、可重复的嵌入式单元测试体系。保存本文档为stm32f4_unity_mock_i2c.md即可作为项目测试指南。