4. 固件测试框架:嵌入式测试点设计、日志输出与调试接口、看门狗与异常复位测试、固件版本管理与校验

体温计的固件,说白了就是它的「大脑」。硬件再扎实,固件一崩,全白搭。我见过太多项目,硬件改了三版没问题,结果固件里一个看门狗喂狗时机不对,量产时批量死机。所以这一章,咱们聊聊固件测试框架怎么搭才靠谱。

4.1 嵌入式测试点设计

测试点设计,不是让你在PCB上焊几个探针。这里说的是固件里的「软件测试点」——在关键路径上埋下钩子,方便产线或调试时抓取状态。

我的习惯是分三类:

  • 功能测试点:比如测温启动、ADC采样完成、数据滤波结束。每个关键函数入口和出口,都留一个测试点。
  • 边界测试点:比如电池电压低于2.8V、温度超过42℃、存储空间剩余不足10%。这些地方最容易出幺蛾子。
  • 时序测试点:比如看门狗喂狗间隔、I2C通信超时、LCD刷新周期。时序问题最难查,提前埋点能救命。

重要原则:测试点不能影响正常功能。我一般用条件编译控制,量产固件里默认关闭,调试版本才打开。

举个例子,我在ADC采样函数里会这样埋点:

// 测试点宏定义
#ifdef ENABLE_TEST_POINT
    #define TEST_POINT(id, val) do { \
        test_point_buffer[id] = val; \
        test_point_flag |= (1 << id); \
    } while(0)
#else
    #define TEST_PINT(id, val) /* 空操作 */
#endif

uint16_t adc_sample(void)
{
    uint16_t raw = adc_read();
    TEST_POINT(TP_ADC_RAW, raw);  // 记录原始值
    
    uint16_t filtered = median_filter(raw);
    TEST_POINT(TP_ADC_FILTERED, filtered);  // 记录滤波后值
    
    return filtered;
}

你看,这样既不增加额外开销,又能随时打开调试。我曾经在产线上遇到一批体温计读数跳变,就是靠这个测试点抓到是滤波算法边界条件没处理好。

4.2 日志输出与调试接口

日志这东西,平时觉得没用,出问题的时候才知道有多香。但体温计这种小设备,资源有限,不能像PC那样随便打log。

我建议的日志分级:

级别标签用途产线是否开启
0ERROR致命错误,比如看门狗复位、存储写入失败
1WARN异常但不致命,比如通信重试、电池低压
2INFO关键流程,比如测温完成、数据上传成功建议关闭
3DEBUG详细调试信息,比如ADC原始值、滤波中间结果

调试接口方面,我推荐用UART。成本低,实现简单。但要注意——量产时一定要把UART引脚复用成GPIO,或者干脆不焊连接器。不然用户拿个串口工具就能乱发指令,你想想看,多危险。

小技巧:日志缓冲区用环形队列。我习惯分配256字节,满了就覆盖最旧的。这样即使死机前一刻的日志也能保留下来。

代码实现大概这样:

void log_printf(uint8_t level, const char *fmt, ...)
{
    if (level > g_log_level) return;  // 级别过滤
    
    char buf[128];
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    
    // 写入环形缓冲区
    ring_buffer_write(g_log_buf, buf, strlen(buf));
    
    // 如果UART已初始化,直接输出
    if (g_uart_ready) {
        uart_send_string(buf);
    }
}

嗯,这里要注意:vsnprintf在MCU上可能比较占资源。如果ROM紧张,可以自己写个轻量级的格式化函数,只支持%d、%x、%s就够了。

4.3 看门狗与异常复位测试

看门狗,体温计的「最后一道防线」。但很多人用错了——要么喂狗太频繁,看门狗形同虚设;要么喂狗间隔太长,死机后半天才复位。

我的经验是:

  • 看门狗超时时间设为主循环周期的3-5倍。比如主循环10ms跑一圈,看门狗设30-50ms。
  • 只在主循环末尾喂一次狗。不要在中断里喂,否则主循环卡死了,中断还在喂,看门狗永远不会复位。
  • 异常复位后,要记录复位原因。大部分MCU都有RCC_CSR寄存器,能读出是上电复位、看门狗复位还是外部复位。

警告:我曾经遇到过一个案例,工程师在ADC中断里喂狗,结果ADC触发频率太高,主循环死锁了看门狗也不复位。产线测试时10台有3台死机,查了三天才找到原因。

异常复位测试怎么做?我一般写个专门的测试用例:

void test_watchdog_reset(void)
{
    // 记录当前复位次数
    uint8_t reset_count = eeprom_read(RESET_COUNT_ADDR);
    reset_count++;
    eeprom_write(RESET_COUNT_ADDR, reset_count);
    
    if (reset_count < 3) {
        // 故意卡死,触发看门狗复位
        while(1);
    } else {
        // 测试通过,清除计数
        eeprom_write(RESET_COUNT_ADDR, 0);
        log_printf(LOG_INFO, "Watchdog test passed");
    }
}

这个用例会在产线测试时自动运行。三次复位后如果系统能正常恢复,说明看门狗机制没问题。

4.4 固件版本管理与校验

体温计固件版本管理,看着简单,但坑不少。我见过产线上烧录了旧版本固件,结果测温偏差0.3℃,整批返工。

版本信息结构体:

typedef struct {
    uint8_t  major;      // 主版本号,重大功能变更
    uint8_t  minor;      // 次版本号,功能新增或修改
    uint8_t  patch;      // 修订号,bug修复
    uint8_t  build;      // 构建号,每天递增
    uint32_t timestamp;  // 编译时间戳
    uint32_t crc32;      // 固件CRC校验值
} firmware_version_t;

我把这个结构体放在固件的固定地址(比如0x0800FFF0),这样上位机工具可以直接读取,不用解析整个bin文件。

校验机制分两层:

  1. 启动校验:上电时计算固件区的CRC32,跟存储的CRC值比对。不一致就报错,拒绝运行。
  2. 运行时校验:每1000次测温循环,随机抽查几个关键函数的CRC。防止Flash比特翻转导致代码损坏。

核心要点:版本号一定要在编译时自动生成,不要手动改。我习惯在Makefile里用git describe生成版本号,确保每个固件都是可追溯的。

产线测试时,上位机会读取体温计的版本信息,跟数据库里的「合格版本列表」比对。如果版本不对,直接NG。这个流程虽然简单,但能挡住90%的烧录错误。

最后说一句:固件测试框架不是一次性搭完就完事的。每次产品迭代,都要同步更新测试点、日志策略和校验逻辑。我见过太多团队,硬件改了三版,固件测试代码还是第一版的那一套——不出问题才怪。