4. 可靠性验证测试体系

好,咱们进入第四章。这一章是测试固件的“心脏”。你想想看,一个测试固件跑起来,如果没有一个清晰的调度逻辑,没有超时保护,没有日志记录,那跟一盘散沙有什么区别?我早年吃过这个亏,测试用例跑飞了,整个系统死在那,连个死因都查不到。所以,核心测试引擎必须稳、准、狠。

这一章,我们聚焦三个核心模块:状态机驱动的测试调度器、超时与看门狗管理、测试日志环形缓冲区设计。这三个东西组合起来,就是一个健壮的测试执行框架。

4.1 状态机驱动的测试调度器

为什么用状态机?因为测试流程本质上是顺序的,但中间会有分支、重试、异常处理。用状态机来管理,逻辑清晰,可读性强,不容易出乱子。我个人习惯把测试调度器设计成一个有限状态机,每个状态对应一个测试阶段。

先看一个典型的状态定义:

typedef enum {
    TEST_STATE_IDLE = 0,
    TEST_STATE_INIT,
    TEST_STATE_RUNNING,
    TEST_STATE_CHECK,
    TEST_STATE_PASS,
    TEST_STATE_FAIL,
    TEST_STATE_RETRY,
    TEST_STATE_TIMEOUT,
    TEST_STATE_ERROR,
    TEST_STATE_DONE
} TestState_t;

调度器的核心就是一个 switch-case 或者函数指针表。我推荐用函数指针表,扩展性好,维护方便。下面是一个简化版的调度循环:

void TestScheduler_Run(void) {
    while (g_testState != TEST_STATE_DONE) {
        switch (g_testState) {
            case TEST_STATE_IDLE:
                // 等待启动命令
                break;
            case TEST_STATE_INIT:
                Test_Init();
                g_testState = TEST_STATE_RUNNING;
                break;
            case TEST_STATE_RUNNING:
                Test_Execute();
                g_testState = TEST_STATE_CHECK;
                break;
            case TEST_STATE_CHECK:
                if (Test_CheckResult() == PASS) {
                    g_testState = TEST_STATE_PASS;
                } else {
                    g_testState = TEST_STATE_FAIL;
                }
                break;
            case TEST_STATE_PASS:
                Test_ReportPass();
                g_testState = TEST_STATE_DONE;
                break;
            case TEST_STATE_FAIL:
                if (g_retryCount < MAX_RETRY) {
                    g_testState = TEST_STATE_RETRY;
                } else {
                    Test_ReportFail();
                    g_testState = TEST_STATE_DONE;
                }
                break;
            case TEST_STATE_RETRY:
                g_retryCount++;
                g_testState = TEST_STATE_RUNNING;
                break;
            case TEST_STATE_TIMEOUT:
                Test_HandleTimeout();
                g_testState = TEST_STATE_ERROR;
                break;
            case TEST_STATE_ERROR:
                Test_ReportError();
                g_testState = TEST_STATE_DONE;
                break;
            default:
                break;
        }
    }
}

这里有个关键点:状态切换不能阻塞。每个状态执行的动作要快,不能在里面做延时等待。如果测试本身需要等待硬件响应,那就要配合后面的超时管理机制。

我的经验: 状态机里的每个状态,最好都加一个“进入次数”计数器。我在调试一个复杂的多通道测试时,发现某个状态反复进入,就是靠这个计数器定位到的。调试利器,强烈推荐。

4.2 超时与看门狗管理

测试固件最怕什么?死等。硬件没响应、总线卡住、外设异常,任何一个都能让你的测试永远停在那。所以,超时管理是必须的。

我一般设计两级超时:

  • 单步超时:针对单个测试步骤,比如等待一个寄存器置位,超时时间通常很短(毫秒级)。
  • 用例超时:针对整个测试用例,防止用例陷入死循环,超时时间较长(秒级)。

实现方式很简单,用一个硬件定时器作为时间基准,每个测试步骤开始时记录起始时间,然后在调度循环里不断检查:

uint32_t g_stepTimeoutMs = 1000;  // 单步超时1秒
uint32_t g_stepStartTick;

void Test_StepStart(void) {
    g_stepStartTick = GetSysTick();
}

bool Test_IsStepTimeout(void) {
    return (GetSysTick() - g_stepStartTick) > g_stepTimeoutMs;
}

配合看门狗,效果更好。看门狗是最后一道防线。当系统完全死掉时,看门狗能强制复位。但要注意,看门狗的喂狗时机要选对。我见过有人在整个测试循环里喂狗,结果系统卡死在某个子函数里,看门狗照样被喂,根本起不到保护作用。

避坑指南: 我曾经在一个项目中,把喂狗放在主循环的末尾。结果一个测试用例里的 while 循环因为条件永远不满足而卡死,但主循环还在跑,看门狗被正常喂,系统一直不复位。后来我改成在每个状态机状态切换时喂狗,并且只在非阻塞状态下喂。这样,一旦某个状态卡住,看门狗就会超时复位。

看门狗的超时时间设置也有讲究。一般设置为用例超时的 2-3 倍,给系统留出足够的复位和恢复时间。

4.3 测试日志环形缓冲区设计

日志是事后分析的关键。但嵌入式系统资源有限,不能无限制地存日志。环形缓冲区(Ring Buffer)是最常用的方案。它固定大小,新数据覆盖旧数据,永远不会溢出。

一个标准的环形缓冲区设计如下:

#define LOG_BUFFER_SIZE 256

typedef struct {
    uint8_t buffer[LOG_BUFFER_SIZE];
    uint32_t head;  // 写入位置
    uint32_t tail;  // 读取位置
    uint32_t count; // 有效数据个数
} RingBuffer_t;

RingBuffer_t g_logBuffer;

bool RingBuffer_Write(uint8_t data) {
    if (g_logBuffer.count >= LOG_BUFFER_SIZE) {
        // 缓冲区满,覆盖最旧的数据
        g_logBuffer.tail = (g_logBuffer.tail + 1) % LOG_BUFFER_SIZE;
        g_logBuffer.count--;
    }
    g_logBuffer.buffer[g_logBuffer.head] = data;
    g_logBuffer.head = (g_logBuffer.head + 1) % LOG_BUFFER_SIZE;
    g_logBuffer.count++;
    return true;
}

bool RingBuffer_Read(uint8_t *data) {
    if (g_logBuffer.count == 0) {
        return false;
    }
    *data = g_logBuffer.buffer[g_logBuffer.tail];
    g_logBuffer.tail = (g_logBuffer.tail + 1) % LOG_BUFFER_SIZE;
    g_logBuffer.count--;
    return true;
}

这里要注意几个细节:

  • 缓冲区大小:根据日志量和系统 RAM 决定。我一般用 2 的幂次方大小,这样取模运算可以用位与操作优化。
  • 原子操作:如果日志写入和读取可能发生在中断和主循环中,必须保证操作是原子的,或者加临界区保护。
  • 日志格式:建议每条日志包含时间戳、日志级别、模块标识和内容。这样后期分析时一目了然。
核心要点: 环形缓冲区不是万能的。如果系统崩溃前产生了大量日志,而缓冲区太小,关键信息可能被覆盖。我建议在系统复位后,将环形缓冲区的内容通过串口或其它接口导出到上位机,避免丢失。

下面这张图展示了本章三个核心模块的关系:

核心测试引擎架构 状态机调度器 超时管理 看门狗管理 日志环形缓冲区 调度器驱动测试流程,超时和看门狗提供保护,日志记录全过程。 三者协同,构成一个健壮的测试执行框架。

最后,把这三个模块整合到一起,就是一个完整的测试引擎。调度器负责流程,超时和看门狗负责安全,环形缓冲区负责记录。这三板斧下去,测试固件的可靠性就能上一个台阶。

我的习惯: 在开发初期,我会把环形缓冲区的大小设得大一些,方便调试。等系统稳定后,再根据实际日志量调整到合适大小。另外,日志的写入函数一定要快,不能影响测试的实时性。