资讯详情

资讯详情

嵌入式软件单元测试(二十六)——IAR C-SPY调试器也能跑UT?一种混合调试+测试的奇技淫巧

❄️ 我的个人专栏《智能软件工程AI4SE》《嵌入式面试总结》《嵌入式处理器架构解析》《嵌入式与虚拟化》《嵌入式软件测试》 Simplicity is the ultimate sophistication摘要本文介绍一种介于纯主机测试与硬件在环测试之间的嵌入式单元测试思路——借助 IAR C-SPY 调试器直接驱动测试用例执行。文章先梳理寄存器依赖、中断时序和启动流程耦合三类痛点再说明 C-SPY 与 Unity 等测试框架的结合方式并以 LED 驱动模块为例演示从准备被测模块、编写测试用例、配置 C-SPY 自动化脚本到用 Python 串联编译、调试与结果解析的完整流程最后分析该方案的优缺点、适用场景与落地建议。1. 引言在嵌入式软件单元测试的实践中很多团队会遇到一个尴尬的处境单元测试框架已经搭好但被测代码依赖硬件寄存器、中断或外设导致测试用例在纯主机环境下根本无法运行。传统的做法是引入模拟层Mock或硬件在环HIL测试但这两者都有各自的成本。本文介绍一种介于两者之间的思路——借助 IAR C-SPY 调试器直接驱动单元测试执行把调试器和测试框架结合起来形成一种混合调试测试的实用方法。2. 为什么需要 C-SPY 参与单元测试先梳理一下嵌入式单元测试常见的三类痛点寄存器依赖被测函数直接读写硬件寄存器主机环境下没有对应地址空间。中断与时序部分逻辑依赖中断触发或精确时序纯软件模拟难以还原。启动流程耦合某些模块的初始化依赖芯片启动代码脱离硬件无法单独执行。针对这些问题C-SPY 调试器提供了一条中间路径它既能像主机测试一样控制执行流、设置断点、观察变量又能访问真实或模拟的硬件资源。换句话说C-SPY 可以充当测试用例的“执行引擎”让单元测试跑在调试会话里。3. C-SPY 与单元测试框架的结合方式这种混合方案的核心思路并不复杂把单元测试框架例如 Unity、CMock 或自定义的断言宏编译进目标工程然后通过 C-SPY 的脚本接口控制测试用例的启动、执行和结果收集。整体结构可以概括为三层flowchart TD A[测试用例源码] -- B[单元测试框架] B -- C[目标工程固件] C -- D[C-SPY 调试器] D -- E[测试结果输出] E -- F[自动化脚本解析]其中C-SPY 承担两个关键职责一是提供可控的执行环境二是通过宏或脚本把测试结果导出到主机端。4. 具体实施步骤下面以一个简单的寄存器读写模块为例演示如何在 IAR 工程中跑起单元测试。4.1 准备被测模块假设被测模块是一个 LED 控制驱动它直接操作 GPIO 寄存器。为了便于测试我们把寄存器地址抽象成宏并保留真实的寄存器映射。/* led_driver.h */ #ifndef LED_DRIVER_H #define LED_DRIVER_H #include stdint.h #define LED_GPIO_BASE 0x40021000u #define LED_GPIO_ODR (*(volatile uint32_t *)(LED_GPIO_BASE 0x14u)) void led_init(void); void led_on(uint8_t index); void led_off(uint8_t index); #endif/* led_driver.c */ #include led_driver.h void led_init(void) { LED_GPIO_ODR 0x00u; } void led_on(uint8_t index) { LED_GPIO_ODR | (1u index); } void led_off(uint8_t index) { LED_GPIO_ODR ~(1u index); }4.2 编写测试用例测试用例使用 Unity 框架编写。这里的关键点是测试用例并不需要真正操作硬件而是通过 C-SPY 的“内存监视”能力来验证寄存器写入是否符合预期。/* test_led_driver.c */ #include unity.h #include led_driver.h void setUp(void) { led_init(); } void tearDown(void) { } void test_led_on_sets_correct_bit(void) { led_on(3u); uint32_t odr LED_GPIO_ODR; TEST_ASSERT_BITS_HIGH(0x08u, odr); } void test_led_off_clears_correct_bit(void) { led_on(3u); led_off(3u); uint32_t odr LED_GPIO_ODR; TEST_ASSERT_BITS_LOW(0x08u, odr); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_led_on_sets_correct_bit); RUN_TEST(test_led_off_clears_correct_bit); return UNITY_END(); }4.3 配置 C-SPY 自动化脚本C-SPY 支持通过命令行或宏文件.mac控制调试会话。我们可以编写一个简单的宏让调试器自动加载固件、运行到 main 函数、执行测试并导出结果。/* run_ut.mac */ execUserReset() { } execUserRun() { __message C-SPY UT: start; __writeMemory32(0x00, 0x40021014, MEMORY); __go(); __message C-SPY UT: finished; }在实际项目中更推荐的做法是把测试结果通过串口或文件输出再由主机端的脚本例如 Python解析并生成报告。4.4 用 Python 脚本串联编译、调试与结果解析为了让整个流程可重复、可接入 CI推荐用 Python 脚本把编译、启动调试会话、运行测试和解析结果串起来。下面这个脚本演示了如何调用 IAR 命令行工具iarbuild和cspybat完成从编译到生成测试报告的全过程。#!/usr/bin/env python3 # -*- coding: utf-8 -*- run_ut.py - 调用 IAR 命令行工具自动编译并运行单元测试。 import subprocess import sys import re from pathlib import Path 按实际安装路径修改 IARBUILD rC:\Program Files\IAR Systems\Embedded Workbench 9.0\common\bin\iarbuild.exe CSPYBAT rC:\Program Files\IAR Systems\Embedded Workbench 9.0\common\bin\cspybat.exe PROJECT rD:\projects\led_ut\led_ut.ewp CONFIG Debug MAC_FILE rD:\projects\led_ut\run_ut.mac OUTPUT_LOG Path(rD:\projects\led_ut\ut_output.txt) def build_project(): 调用 iarbuild 编译工程返回是否成功。 cmd [IARBUILD, PROJECT, -build, CONFIG] print( 开始编译工程) result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: print(编译失败) print(result.stdout) print(result.stderr) return False print(编译成功) return True def run_cspy(): 调用 cspybat 启动 C-SPY 调试会话并执行测试。 cmd [ CSPYBAT, -f, rD:\projects\led_ut\settings\led_ut.Debug.general.xcl, --backend, -f, rD:\projects\led_ut\settings\led_ut.Debug.driver.xcl, --mac, MAC_FILE, --log, file, str(OUTPUT_LOG), ] print( 启动 C-SPY 调试会话) result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: print(C-SPY 执行失败) print(result.stdout) print(result.stderr) return False return True def parse_report(): 解析 C-SPY 输出日志统计测试结果并生成简洁报告。 if not OUTPUT_LOG.exists(): print(未找到输出日志文件) return text OUTPUT_LOG.read_text(encodingutf-8, errorsignore) passed len(re.findall(rPASS, text)) failed len(re.findall(rFAIL, text)) total passed failed print( * 40) print(单元测试报告) print( * 40) print(f通过用例{passed}) print(f失败用例{failed}) print(f用例总数{total}) if failed 0: print(结论全部通过) else: print(结论存在失败用例请检查日志) print( * 40) def main(): if not build_project(): sys.exit(1) if not run_cspy(): sys.exit(1) parse_report() if name main: main()脚本的核心逻辑分为三步先用iarbuild编译工程再用cspybat加载调试配置并执行宏文件最后解析 C-SPY 输出的日志文件统计通过和失败的用例数量。实际使用时需要根据本机的 IAR 安装路径、工程路径以及调试会话的配置文件.xcl路径做相应调整。5. 这种方案的优缺点维度优势局限硬件依赖可直接访问真实寄存器减少模拟层维护成本仍需目标板或模拟器无法完全脱离硬件执行速度比硬件在环测试快比纯主机测试慢调试会话启动和下载固件需要时间可观测性可实时查看寄存器、内存和变量定位问题直观自动化程度依赖脚本编写质量集成成本复用现有 IAR 工程无需额外搭建测试平台对 CI 环境配置有一定要求6. 适用场景与建议这种混合调试测试的方法并不适合所有项目但在以下场景中尤其有价值驱动层或 BSP 层代码寄存器操作密集难以在主机端模拟。团队已经统一使用 IAR 工具链不希望引入额外的测试框架。需要快速验证某个硬件相关模块的正确性但又不想搭建完整的 HIL 环境。建议在项目初期先小范围试点把 C-SPY 脚本和测试用例的目录结构约定好再逐步推广到更多模块。同时仍然保留纯主机端的单元测试作为快速回归手段两者互补才能形成完整的测试策略。7. 总结IAR C-SPY 调试器参与单元测试本质上是在“纯主机测试”和“硬件在环测试”之间找到了一条折中路线。它让测试用例能够运行在接近真实硬件的环境中同时保留了调试器的可观测性优势。虽然这种方案在自动化和执行效率上还有提升空间但对于寄存器密集型的嵌入式模块来说确实是一种值得尝试的“奇技淫巧”。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →