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;
}
配合看门狗,效果更好。看门狗是最后一道防线。当系统完全死掉时,看门狗能强制复位。但要注意,看门狗的喂狗时机要选对。我见过有人在整个测试循环里喂狗,结果系统卡死在某个子函数里,看门狗照样被喂,根本起不到保护作用。
看门狗的超时时间设置也有讲究。一般设置为用例超时的 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 的幂次方大小,这样取模运算可以用位与操作优化。
- 原子操作:如果日志写入和读取可能发生在中断和主循环中,必须保证操作是原子的,或者加临界区保护。
- 日志格式:建议每条日志包含时间戳、日志级别、模块标识和内容。这样后期分析时一目了然。
下面这张图展示了本章三个核心模块的关系:
最后,把这三个模块整合到一起,就是一个完整的测试引擎。调度器负责流程,超时和看门狗负责安全,环形缓冲区负责记录。这三板斧下去,测试固件的可靠性就能上一个台阶。