☰
STM32F4 Unity 单元测试:Mock 与 I2C 模拟详细指南
2026/10/10 6:04:02 网站建设 项目流程

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#include<stdint.h>#include<stdbool.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);#endif

2.3 提供真实的 STM32 实现

新建i2c_hal.c:

#include"i2c_hal.h"#include"main.h"// 包含 hi2c1 等externI2C_HandleTypeDef hi2c1;staticboolreal_i2c_write(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen){returnHAL_I2C_Mem_Write(&hi2c1,dev_addr<<1,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_addr<<1,reg,I2C_MEMADD_SIZE_8BIT,data,len,100)==HAL_OK;}i2c_write_fn i2c_write=real_i2c_write;i2c_read_fn i2c_read=real_i2c_read;voidi2c_hal_init(void){i2c_write=real_i2c_write;i2c_read=real_i2c_read;}

2.4 业务代码使用函数指针

例如传感器驱动sensor.c:

#include"i2c_hal.h"boolsensor_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中:

#include"unity.h"#include"i2c_hal.h"#include"sensor.h"// --- Mock 实现 ---staticuint8_tmock_temp_data[2];staticbool mock_i2c_read_result=true;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_read=mock_i2c_read;// 替换为 Mockmock_i2c_read_result=true;}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 result=sensor_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_result=false;floattemp;bool result=sensor_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:

#include"stm32f4xx_hal.h"#include<string.h>// 模拟数据缓冲区,供测试设置uint8_tmock_i2c_rx_buffer[256];uint16_tmock_i2c_rx_len=0;HAL_StatusTypeDef mock_i2c_return=HAL_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_return==HAL_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#include<stdint.h>#include<stdbool.h>booli2c_read(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen);booli2c_write(uint8_tdev_addr,uint8_treg,uint8_t*data,uint16_tlen);#endif

4.3 在测试中使用自动生成的 Mock

#include"unity.h"#include"mock_i2c_interface.h"#include"sensor.h"voidtest_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 result=sensor_read_temperature(0x48,&temp);TEST_ASSERT_TRUE(result);TEST_ASSERT_FLOAT_WITHIN(0.1f,25.0f,temp);}

优点:自动化程度高,支持参数校验、调用次数验证。
缺点:需要搭建 Ceedling 环境,学习曲线较陡。


5. 方案四:StubWithCallback(CMock 高级用法)

当 Mock 行为复杂(如模拟完整 I2C 设备寄存器映射)时,可用StubWithCallback完全接管函数行为。

HAL_StatusTypeDefmy_i2c_stub(I2C_HandleTypeDef*hi2c,uint16_tDevAddress,uint32_tTrials,uint32_tTimeout,intnum_calls){if(DevAddress==(0x50<<1)){returnHAL_OK;// 模拟地址 0x50 设备存在}returnHAL_ERROR;}voidtest_scanner_should_find_device_at_0x50(void){HAL_I2C_IsDeviceReady_StubWithCallback(my_i2c_stub);uint8_tfound=i2c_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 方案与之前文档中的“多次测试失败率统计”结合:

  1. 在setUp()中安装 Mock 函数指针。
  2. 在tearDown()中恢复真实实现(如需在硬件上继续运行)。
  3. 在测试用例中设置 Mock 返回值。
  4. 多次循环运行测试,统计失败率。

示例:

voidsetUp(void){i2c_read=mock_i2c_read;i2c_write=mock_i2c_write;mock_i2c_read_result=true;}voidtearDown(void){i2c_hal_init();// 恢复真实实现}

9. 总结

通过本文档,你可以在 STM32F4 Unity 测试中灵活加入 Mock 来模拟 I2C:

  • 函数指针:最易上手,推荐首选。
  • 链接时替换:零侵入,适合已有代码。
  • CMock + Ceedling:自动化程度最高,适合工程化。
  • StubWithCallback:应对复杂设备模拟。
  • 影子寄存器:底层寄存器驱动测试。

结合多次测试与失败率统计,可构建稳定、可重复的嵌入式单元测试体系。


保存本文档为stm32f4_unity_mock_i2c.md,即可作为项目测试指南。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询