第2章:测试固件架构设计:模块化固件设计原则、分层架构与测试框架解耦

做嵌入式可靠性测试这么多年,我见过太多测试固件写得像一锅粥。一个函数上千行,全局变量满天飞,换个测试用例就得改底层代码。这种固件,说白了就是给自己挖坑。今天咱们聊聊怎么把测试固件架构搭好,让它既好写又好维护。

2.1 模块化固件设计原则

模块化不是什么新鲜概念,但在测试固件里,它的意义更特殊。测试固件本身也是固件,但它要服务于测试,而不是产品功能。我个人的习惯是,把测试固件当成一个独立的嵌入式项目来设计,而不是产品代码的附属品。

2.1.1 单一职责原则

每个模块只做一件事。比如,一个模块只负责GPIO读写,另一个只负责串口通信,第三个只负责测试流程控制。别把什么都揉在一起。

核心要点:模块的职责边界要清晰。一个模块如果同时处理硬件操作和测试逻辑,那它既不好测也不好改。

我在项目中遇到过这样的案例:一个测试固件里,同一个函数既操作I2C总线,又解析测试数据,还负责打印日志。后来要换一个I2C器件,结果改了三天,还引入了新bug。这就是典型的职责不清。

2.1.2 接口隔离原则

模块之间的交互通过接口完成,而不是直接访问内部数据。接口要小而精,不要搞一个大而全的万能接口。

/* 好的接口设计:小而专 */
typedef struct {
    int32_t (*init)(void);
    int32_t (*read)(uint8_t reg, uint8_t *buf, uint16_t len);
    int32_t (*write)(uint8_t reg, const uint8_t *buf, uint16_t len);
} i2c_bus_t;

/* 不好的接口设计:大而全 */
typedef struct {
    int32_t (*do_everything)(uint32_t cmd, void *param1, void *param2);
} universal_driver_t;

你想想看,do_everything这种接口,调用方得传什么参数?文档写不清楚,代码也看不懂。我建议每个接口只做一件事,参数明确,返回值清晰。

2.1.3 依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖抽象。在测试固件里,这意味着测试逻辑不应该直接调用硬件寄存器,而是通过抽象层来操作。

个人经验:我曾经在一个项目中,测试用例直接操作GPIO寄存器。后来换了MCU,所有测试用例都得重写。如果当时用了HAL抽象层,只需要改底层驱动,测试用例完全不用动。

2.2 分层架构:HAL/驱动/应用层

分层架构是嵌入式固件的经典做法,测试固件也不例外。我习惯把测试固件分成三层:硬件抽象层(HAL)、驱动层、应用层。每一层各司其职,层与层之间通过接口通信。

测试固件分层架构 应用层(Application Layer) 测试用例调度、测试流程控制、结果判定、日志管理 不直接操作硬件,只调用驱动层接口 驱动层(Driver Layer) 外设驱动:GPIO、UART、I2C、SPI、ADC、PWM 封装硬件操作细节,提供标准化接口 硬件抽象层(HAL Layer) 寄存器操作、中断处理、时钟配置、DMA管理 直接与MCU硬件交互,屏蔽芯片差异 各层之间通过结构体函数指针或抽象接口通信,不直接调用

2.2.1 硬件抽象层(HAL)

HAL层是最底层,直接跟MCU寄存器打交道。它的职责是屏蔽不同MCU之间的差异。比如,STM32的GPIO配置和GD32的GPIO配置不一样,但通过HAL层,上层看到的接口是一样的。

注意:HAL层不要包含任何测试逻辑。它只负责把硬件操作封装成函数。我曾经见过有人在HAL层里加测试计数和日志打印,结果导致时序偏差,测试结果不准。

2.2.2 驱动层

驱动层在HAL层之上,封装具体外设的操作。比如,一个I2C驱动,它调用HAL层的I2C读写函数,但增加了超时处理、错误重试、数据校验等逻辑。

/* 驱动层示例:I2C驱动 */
typedef struct {
    hal_i2c_t *hal;          /* 依赖HAL层接口 */
    uint32_t    timeout_ms;  /* 超时时间 */
    uint8_t     retry_cnt;   /* 重试次数 */
} i2c_driver_t;

int32_t i2c_driver_read(i2c_driver_t *drv, uint8_t dev_addr,
                        uint8_t reg, uint8_t *data, uint16_t len)
{
    int32_t ret;
    for (uint8_t i = 0; i < drv->retry_cnt; i++) {
        ret = drv->hal->read(dev_addr, reg, data, len, drv->timeout_ms);
        if (ret == 0) return 0;  /* 成功 */
    }
    return -1;  /* 重试失败 */
}

驱动层的接口要稳定。一旦确定,尽量不要改。因为应用层和测试用例都依赖它。

2.2.3 应用层

应用层是测试固件的核心。它负责测试用例的调度、测试流程的控制、结果的判定和日志的输出。应用层不直接操作硬件,它只调用驱动层的接口。

我习惯把应用层再细分成几个子模块:

  • 测试调度器:管理测试用例的执行顺序、超时、异常处理
  • 测试用例库:存放具体的测试用例函数
  • 结果管理器:收集测试结果,生成报告
  • 日志系统:记录测试过程中的关键信息

2.3 固件与测试框架解耦

这是很多团队容易忽略的点。测试固件和测试框架(比如PC端的测试管理软件)之间,应该有清晰的接口,而不是耦合在一起。

2.3.1 为什么需要解耦?

我举个例子。你写了一个测试固件,它通过串口跟PC端的测试软件通信。测试软件发命令,固件执行并返回结果。如果固件和测试软件耦合太紧,比如固件里直接解析测试软件的特定协议格式,那换一个测试软件,固件就得重写。

解耦的核心思路:固件只提供标准化的命令接口,不关心上层是谁在调用。测试框架只负责发送命令和接收结果,不关心固件内部怎么实现。

2.3.2 接口设计原则

固件与测试框架之间的接口,我建议采用以下设计:

接口类型 说明 示例
命令接口 测试框架发送命令,固件执行 CMD_READ_ADC CH1
数据接口 固件返回测试数据 DATA 1234 mV
状态接口 固件报告当前状态 STATUS IDLE
错误接口 固件报告错误信息 ERR TIMEOUT

接口协议要简单、可扩展。我推荐用文本协议,而不是二进制协议。文本协议调试方便,人眼可读,而且容易扩展。

/* 文本协议示例 */
/* 测试框架发送: */
TEST START
TEST ADC_CH1
TEST ADC_CH2
TEST STOP

/* 固件返回: */
OK
DATA ADC_CH1: 1234 mV
DATA ADC_CH2: 1256 mV
DONE

2.3.3 解耦的具体做法

在固件内部,我建议把通信模块和测试逻辑模块分开。通信模块只负责收发数据,不解析测试命令。测试逻辑模块只负责执行测试,不关心数据怎么来的。

避坑指南:我曾经在一个项目里,把命令解析和测试执行写在同一个函数里。结果测试框架升级了协议,我不得不改固件里的解析逻辑,还差点引入新bug。后来我把通信模块独立出来,用回调函数把命令传给测试模块,再也没出过类似问题。

/* 解耦后的通信模块接口 */
typedef int32_t (*cmd_handler_t)(const char *cmd, char *resp, uint16_t resp_len);

int32_t comm_init(uart_t *uart, cmd_handler_t handler);
int32_t comm_process(void);  /* 在主循环中调用 */

/* 测试模块注册命令处理函数 */
int32_t test_cmd_handler(const char *cmd, char *resp, uint16_t resp_len)
{
    if (strcmp(cmd, "TEST START") == 0) {
        return test_start(resp, resp_len);
    } else if (strcmp(cmd, "TEST STOP") == 0) {
        return test_stop(resp, resp_len);
    }
    return -1;  /* 未知命令 */
}

这样设计之后,通信模块和测试模块完全解耦。换通信方式(比如从串口换成USB),只需要改通信模块,测试模块不用动。换测试逻辑,也只需要改测试模块,通信模块不受影响。

2.4 架构设计的避坑总结

做测试固件架构设计,有几个坑我踩过,分享给大家:

  • 别过早优化:先把架构搭起来,跑通基本流程,再考虑性能优化。我见过有人花了两周设计一个完美的架构,结果发现根本跑不通。
  • 接口文档要写:模块之间的接口一定要文档化。别指望别人看代码就能理解你的设计意图。
  • 预留测试点:在关键模块的接口处,预留测试点(比如hook函数),方便后续调试和验证。
  • 版本管理:接口变更要通知所有相关方。我曾经因为改了HAL层接口,没通知驱动层同事,结果联调时出了大问题。

最后提醒:架构设计不是一蹴而就的。随着测试需求的变化,架构也需要迭代。关键是保持模块化、分层、解耦这三个原则,这样后续改起来才不会太痛苦。