返回首页

Zephyr 测试与调试学习笔记

概述

本文介绍 Zephyr RTOS 的测试框架、调试工具和最佳实践,帮助开发者确保代码质量和快速定位问题。

测试框架

1. Ztest 框架

Zephyr 内置测试框架是 ztest

#include <zephyr/ztest.h>

// 测试套件
ZTEST_SUITE(my_suite);

// 测试用例
ZTEST(my_suite, test_example)
{
    int a = 1;
    int b = 2;
    
    zassert_equal(a + b, 3, "Addition failed");
}

// 带前置条件的测试
ZTEST_F(my_suite, test_with_setup)
{
    zassert_not_null(fixture->data, "Data not initialized");
}

2. 单元测试

#include <zephyr/ztest.h>
#include <zephyr/kernel.h>

/* 测试数学函数 */
ZTEST_SUITE(math_tests);

ZTEST(math_tests, test_add)
{
    zassert_equal(add(2, 3), 5, NULL);
    zassert_equal(add(-1, 1), 0, NULL);
    zassert_equal(add(0, 0), 0, NULL);
}

ZTEST(math_tests, test_multiply)
{
    zassert_equal(multiply(3, 4), 12, NULL);
    zassert_equal(multiply(0, 5), 0, NULL);
    zassert_equal(multiply(-2, 3), -6, NULL);
}

/* 测试边界条件 */
ZTEST(math_tests, test_overflow)
{
    zassert_true(overflow_add(INT_MAX, 1) < 0, "Overflow not detected");
}

3. 集成测试

#include <zephyr/ztest.h>
#include <zephyr/kernel.h>

ZTEST_SUITE(integration_tests);

static struct k_sem test_sem;

ZTEST(integration_tests, test_thread_sync)
{
    k_sem_init(&test_sem, 0, 1);
    
    // 启动测试线程
    k_tid_t tid = k_thread_create(&thread_data, stack,
                                  STACK_SIZE, thread_fn,
                                  NULL, NULL, NULL, 5, 0, K_NO_WAIT);
    
    // 等待信号量
    zassert_equal(k_sem_take(&test_sem, K_MSEC(100)), 0, 
                  "Semaphore not given");
    
    // 验证结果
    zassert_true(thread_executed, "Thread did not execute");
}

ZTEST(integration_tests, test_message_queue)
{
    struct k_msgq msgq;
    uint8_t buffer[16 * sizeof(int)];
    k_msgq_init(&msgq, buffer, sizeof(int), 16);
    
    int data = 42;
    zassert_equal(k_msgq_put(&msgq, &data, K_NO_WAIT), 0, 
                  "Failed to send message");
    
    int received;
    zassert_equal(k_msgq_get(&msgq, &received, K_NO_WAIT), 0,
                  "Failed to receive message");
    zassert_equal(received, 42, "Message data mismatch");
}

4. 回归测试

# prj.conf
CONFIG_ZTEST=y
CONFIG_ZTEST_REPEATS=3  # 重复运行 3 次
// 测试失败时自动重试
ZTEST(my_suite, test_flaky)
{
    // 可能不稳定的测试
    zassert_true(random_condition(), "Random condition failed");
}

5. Mock 测试

#include <zephyr/ztest.h>
#include <zephyr/ztest_mock.h>

// Mock 函数
static int (*mock_sensor_read)(struct sensor_value *val) = NULL;

int sensor_read_mock(struct sensor_value *val)
{
    if (mock_sensor_read) {
        return mock_sensor_read(val);
    }
    return -ENOTSUP;
}

// 测试用例
ZTEST(mock_tests, test_sensor_success)
{
    // 设置 mock 返回值
    struct sensor_value expected = { .val1 = 25 };
    ztest_expect_value(sensor_read_mock, return_value, 0);
    ztest_returns_value(sensor_read_mock, 0);
    mock_sensor_read = mock_success;
    
    int ret = read_sensor(&val);
    zassert_equal(ret, 0, "Read failed");
    zassert_equal(val.val1, 25, "Value mismatch");
}

ZTEST(mock_tests, test_sensor_error)
{
    // Mock 错误返回
    mock_sensor_read = mock_error;
    ztest_returns_value(sensor_read_mock, -EIO);
    
    int ret = read_sensor(&val);
    zassert_equal(ret, -EIO, "Error not propagated");
}

调试工具

1. Shell (Console)

# 启用 Shell
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y

# 启用常用命令模块
CONFIG_KERNEL_SHELL=y
CONFIG_DEVICE_SHELL=y
CONFIG_MEMORY_SHELL=y
CONFIG_THREAD_SHELL=y
#include <zephyr/shell/shell.h>

// 自定义 Shell 命令
static int cmd_led(const struct shell *sh, size_t argc, char **argv)
{
    if (argc < 2) {
        shell_print(sh, "Usage: led <on|off|toggle>");
        return -EINVAL;
    }
    
    if (strcmp(argv[1], "on") == 0) {
        gpio_pin_set_dt(&led, 1);
    } else if (strcmp(argv[1], "off") == 0) {
        gpio_pin_set_dt(&led, 0);
    } else if (strcmp(argv[1], "toggle") == 0) {
        gpio_pin_toggle_dt(&led);
    }
    
    return 0;
}

SHELL_CMD_REGISTER(led, NULL, "Control LED", cmd_led);

2. 日志系统调试

# 启用调试日志
CONFIG_LOG=y
CONFIG_LOG_MAX_LEVEL=4
CONFIG_LOG_DBG=y

# 启用日志后端
CONFIG_LOG_BACKEND_UART=y
CONFIG_LOG_BACKEND_RTT=y

# 运行时过滤
CONFIG_LOG_RUNTIME_FILTERING=y
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(my_module, LOG_LEVEL_DBG);

void debug_function(void)
{
    LOG_DBG("Debug info: var=%d", var);
    LOG_INF("Info: important message");
    LOG_WRN("Warning: unusual condition");
    LOG_ERR("Error: operation failed");
    
    // 条件日志
    if (IS_ENABLED(CONFIG_DEBUG_FEATURE)) {
        LOG_DBG("Expensive debug info");
    }
}

3. 断言和检查

#include <zephyr/kernel.h>

// 运行时断言
__ASSERT(pointer != NULL, "Null pointer");
__ASSERT_NO_MSG(condition);

// 带描述的断言
__ASSERT(cond, "Description: var=%d", var);

// 编译时静态断言
BUILD_ASSERT(CONFIG_VALUE > 0, "Value must be positive");

4. 追踪和分析

# 启用追踪
CONFIG_TRACING=y
CONFIG_TRACING_CORE=y

# 启用性能分析
CONFIG_PERFORMANCE_ANALYSIS=y
#include <zephyr/tracing/tracing.h>

void traced_function(void)
{
    // 追踪入口
    TRACE_FUNC_ENTER();
    
    // 执行操作
    
    // 追踪退出
    TRACE_FUNC_EXIT();
}

常见问题调试

1. 线程堆栈溢出

# 启用堆栈保护
CONFIG_THREAD_STACK_GUARD=y
CONFIG_STACK_SENTINEL=y

# 最小堆栈检查
CONFIG_HW_STACK_PROTECTION=y
// 检测堆栈使用
void check_stack_usage(void)
{
    size_t unused = k_thread_stack_space_get(k_current_get());
    if (unused < 128) {
        LOG_WRN("Low stack space: %zu bytes", unused);
    }
}

2. 内存泄漏

#include <zephyr/kernel.h>

// 跟踪内存分配
void *tracked_malloc(size_t size)
{
    void *ptr = k_malloc(size);
    LOG_INF("Alloc %zu bytes at %p", size, ptr);
    return ptr;
}

void tracked_free(void *ptr)
{
    LOG_INF("Free %p", ptr);
    k_free(ptr);
}

3. 死锁检测

#include <zephyr/kernel.h>

// 简单的死锁超时检测
int safe_mutex_lock(struct k_mutex *mutex, int32_t timeout)
{
    int ret = k_mutex_lock(mutex, timeout);
    if (ret == -EAGAIN) {
        LOG_ERR("Possible deadlock detected!");
    }
    return ret;
}

4. 竞争条件

# Zephyr 主线没有通用的 ThreadSanitizer 工作流
# 更常见的是通过原子操作、锁顺序检查和日志定位竞争问题
// 使用原子操作避免竞争
#include <zephyr/sys/atomic.h>

static atomic_t counter = ATOMIC_INIT(0);

void increment(void)
{
    atomic_inc(&counter);  // 原子操作
}

int get_count(void)
{
    return atomic_get(&counter);
}

调试技巧

1. GDB 调试

# 启动调试
west debug

# 或使用 J-Link
JLinkGDBServer -device <SoC> -if swd -speed 4000

# 在 GDB 中
(gdb) break main.c:100
(gdb) continue
(gdb) print variable_name
(gdb) backtrace
(gdb) thread apply all bt  # 所有线程堆栈

2. Segger RTT

# 使用 RTT Viewer
JLinkRTTViewer -Device <SoC> -RTTChannel 0
// RTT 日志
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(app, LOG_LEVEL_INF);

// 通过 RTT 查看日志

3. 崩溃分析

#include <zephyr/sys/reboot.h>

/*
 * Zephyr 崩溃时通常会由异常处理和日志后端打印 fault 信息。
 * 如果需要应用侧兜底处理,更常见的是记录上下文后主动重启。
 */

4. 仿真测试

# 使用 native_sim 板卡进行仿真测试
west build -b native_sim tests/kernel/mem_slab/mslab
west build -b native_sim tests/kernel/queue

持续集成

1. 测试配置

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          pip install west
          west init -l .
          west update
      - name: Run tests
        run: |
          west build -b native_sim tests/kernel
          west build -b <board> tests/bluetooth

2. 代码覆盖率

# prj.conf
CONFIG_COVERAGE=y
CONFIG_COVERAGE_DUMP=y
# 生成覆盖率报告
west build -b native_sim tests/kernel -- -DCMAKE_C_FLAGS="-coverage"
west build -t gcov
lcov --capture --derive-func-data --directory build --output-file coverage.info
genhtml coverage.info --output-directory coverage_html

最佳实践

1. 测试策略

测试金字塔:
       /\
      /  \    端到端测试 (少量)
     /----\
    /      \   集成测试 (中等)
   /--------\
  /          \  单元测试 (大量)
 /------------\

2. 测试命名

// 好的命名
ZTEST(sensor_tests, test_bme280_read_temperature_normal_range)
ZTEST(sensor_tests, test_bme280_read_humidity_high_humidity)
ZTEST(sensor_tests, test_bme280_read_pressure_at_sea_level)

// 避免
ZTEST(sensor_tests, test1)
ZTEST(sensor_tests, test_read)

3. 测试数据

// 使用结构化测试数据
struct test_case {
    int input;
    int expected;
    const char *description;
};

static const struct test_case test_cases[] = {
    {0, 0, "Zero input"},
    {1, 1, "One input"},
    {-1, -1, "Negative input"},
    {INT_MAX, INT_MAX, "Maximum value"},
};

ZTEST_F(my_tests, test_cases)
{
    for (int i = 0; i < ARRAY_SIZE(test_cases); i++) {
        int result = calculate(test_cases[i].input);
        zassert_equal(result, test_cases[i].expected,
                      "Failed: %s", test_cases[i].description);
    }
}

总结

测试要点

  1. 单元测试:测试每个函数/模块的正确性
  2. 集成测试:测试模块间的交互
  3. 回归测试:防止新代码破坏旧功能
  4. Mock 测试:隔离依赖,精确测试

调试工具选择

场景工具
运行时日志LOG + Shell
崩溃分析GDB + console
性能分析Perf + tracing
内存问题Valgrind (native) + built-in

最佳实践

  1. 先写测试:测试驱动开发 (TDD)
  2. 覆盖率目标:核心代码 > 80%
  3. 自动化:CI/CD 集成测试
  4. 持续运行:回归测试套件

*学习日期: 2026-03-21* *小白 🤖*