给RT-Thread的RT_ASSERT加点‘料’:手把手教你定制断言处理,让死机问题自己‘开口说话’

在嵌入式开发中,断言(Assertion)是确保代码健壮性的重要防线。当系统运行出现异常时,断言能够及时捕获问题并终止程序,防止错误扩散。然而,传统的断言机制往往只提供简单的文件名和行号信息,这对于复杂系统中的问题定位远远不够。本文将带你深入RT-Thread内核,定制一个会"说话"的断言处理机制,让死机问题自己暴露根源。

1. RT-Thread断言机制深度解析

RT-Thread作为一款优秀的实时操作系统,其内核中的 RT_ASSERT 宏是开发者最常用的调试工具之一。默认情况下,当断言触发时,系统会调用 rt_assert_handler 函数,打印出触发断言的文件名和行号,然后进入死循环。

让我们先看看RT-Thread中默认的断言处理实现:

void rt_assert_handler(const char *ex_string, const char *func, rt_size_t line)
{
    rt_kprintf("(%s) assertion failed at function:%s, line number:%d\n", ex_string, func, line);
    while (1);
}

这种实现虽然简单直接,但在实际产品中却存在明显不足:

  • 信息量有限 :仅提供文件名和行号,缺乏上下文信息
  • 难以远程诊断 :产品部署后,控制台输出可能无法获取
  • 无历史记录 :无法追踪断言触发前的系统状态

2. 定制断言处理的核心思路

要让断言"开口说话",我们需要从多个维度增强其信息收集和输出能力。以下是几个关键改进方向:

2.1 信息维度扩展

一个完善的断言处理应该包含以下信息:

  1. 基础信息

    • 断言表达式内容
    • 触发位置(文件名、函数名、行号)
    • 系统时间戳
  2. 上下文信息

    • 当前线程名称和状态
    • 调用栈回溯
    • CPU寄存器状态
  3. 系统状态

    • 内存使用情况
    • 关键变量值
    • 任务调度状态

2.2 输出渠道多样化

根据产品形态不同,可以选择以下一种或多种输出方式:

输出方式 优点 缺点 适用场景
串口输出 简单可靠 需要物理连接 开发调试阶段
网络传输 支持远程 依赖网络模块 物联网设备
Flash存储 断电保存 需要额外空间 无网络环境
LED指示 直观快速 信息量有限 极简系统

3. 实现增强版断言处理

下面我们一步步实现一个功能丰富的断言处理模块。

3.1 基础框架搭建

首先创建一个新的头文件 enhanced_assert.h

#ifndef __ENHANCED_ASSERT_H__
#define __ENHANCED_ASSERT_H__

#include <rtthread.h>

void enhanced_assert_handler(const char *ex, const char *func, rt_size_t line);

#define ENHANCED_ASSERT(expr) \
    if (!(expr)) \
        enhanced_assert_handler(#expr, __FUNCTION__, __LINE__)

#endif

3.2 核心处理函数实现

创建 enhanced_assert.c 文件,实现核心处理逻辑:

#include "enhanced_assert.h"
#include <rthw.h>

// 断言信息结构体
typedef struct {
    const char *expression;
    const char *function;
    rt_size_t line;
    rt_tick_t tick;
    char thread_name[RT_NAME_MAX];
    rt_uint32_t stack_usage;
} assert_info_t;

void enhanced_assert_handler(const char *ex, const char *func, rt_size_t line)
{
    assert_info_t info;
    
    // 填充基础信息
    info.expression = ex;
    info.function = func;
    info.line = line;
    info.tick = rt_tick_get();
    
    // 获取当前线程信息
    rt_thread_t current = rt_thread_self();
    if (current)
    {
        rt_strncpy(info.thread_name, current->name, RT_NAME_MAX);
        info.stack_usage = current->stack_size - current->stack_usage;
    }
    else
    {
        rt_strncpy(info.thread_name, "N/A", RT_NAME_MAX);
        info.stack_usage = 0;
    }
    
    // 输出信息到串口
    rt_kprintf("\n!!! ASSERTION FAILED !!!\n");
    rt_kprintf("Expression: %s\n", info.expression);
    rt_kprintf("Location: %s() at line %d\n", info.function, info.line);
    rt_kprintf("Thread: %s\n", info.thread_name);
    rt_kprintf("Stack left: %d bytes\n", info.stack_usage);
    rt_kprintf("System tick: %d\n", info.tick);
    
    // 这里可以添加更多输出方式,如保存到Flash等
    
    while (1);
}

3.3 调用栈回溯实现

为了获取更有价值的调试信息,我们可以实现调用栈回溯功能。这需要针对不同CPU架构进行适配,以下是ARM Cortex-M系列的实现示例:

#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM Compiler */
void print_backtrace(void)
{
    rt_uint32_t *frame;
    rt_uint32_t *stack_ptr;
    
    asm volatile ("mov %0, fp" : "=r" (frame));
    
    rt_kprintf("Call stack:\n");
    while (frame)
    {
        rt_uint32_t pc = *(frame + 1) - 4;
        rt_kprintf("0x%08x\n", pc);
        frame = (rt_uint32_t *)*frame;
    }
}
#else
void print_backtrace(void)
{
    rt_kprintf("Backtrace not supported for this arch\n");
}
#endif

4. 高级功能扩展

4.1 关键变量自动记录

我们可以扩展断言处理,自动记录关键变量的值:

typedef struct {
    const char *name;
    void *addr;
    rt_uint8_t size;
} watch_var_t;

#define MAX_WATCH_VARS 10
static watch_var_t watch_list[MAX_WATCH_VARS];
static rt_uint8_t watch_count = 0;

void assert_add_watch_var(const char *name, void *addr, rt_uint8_t size)
{
    if (watch_count < MAX_WATCH_VARS)
    {
        watch_list[watch_count].name = name;
        watch_list[watch_count].addr = addr;
        watch_list[watch_count].size = size;
        watch_count++;
    }
}

static void dump_watch_vars(void)
{
    rt_uint8_t i;
    rt_kprintf("Watched variables:\n");
    
    for (i = 0; i < watch_count; i++)
    {
        rt_kprintf("%s: ", watch_list[i].name);
        
        switch (watch_list[i].size)
        {
        case 1:
            rt_kprintf("0x%02x\n", *(rt_uint8_t *)watch_list[i].addr);
            break;
        case 2:
            rt_kprintf("0x%04x\n", *(rt_uint16_t *)watch_list[i].addr);
            break;
        case 4:
            rt_kprintf("0x%08x\n", *(rt_uint32_t *)watch_list[i].addr);
            break;
        default:
            rt_kprintf("(size %d)\n", watch_list[i].size);
            break;
        }
    }
}

4.2 Flash存储实现

对于需要长期保存断言信息的产品,可以实现Flash存储功能:

#include <fal.h>

#define ASSERT_LOG_SIZE 512
#define ASSERT_LOG_ADDR 0x080E0000 // 根据实际Flash布局调整

static void save_to_flash(assert_info_t *info)
{
    char buffer[ASSERT_LOG_SIZE];
    int len;
    struct fal_blk_device *blk_dev;
    
    blk_dev = fal_blk_device_create("assert_log");
    if (!blk_dev)
        return;
        
    len = rt_snprintf(buffer, ASSERT_LOG_SIZE,
                     "ASSERT: %s\n"
                     "Func: %s Line: %d\n"
                     "Thread: %s\n"
                     "Tick: %d\n",
                     info->expression, info->function, info->line,
                     info->thread_name, info->tick);
                     
    fal_blk_write(blk_dev, 0, (rt_uint8_t *)buffer, len);
}

5. 实际应用与优化建议

5.1 在项目中使用增强断言

要在项目中使用这个增强版断言,只需做简单替换:

  1. 将原来的 RT_ASSERT 替换为 ENHANCED_ASSERT
  2. 在系统初始化时注册关键变量:
int main(void)
{
    // 初始化系统...
    
    // 监控关键变量
    assert_add_watch_var("sensor_value", &sensor_value, sizeof(sensor_value));
    assert_add_watch_var("system_mode", &system_mode, sizeof(system_mode));
    
    // ...
}

5.2 性能与资源平衡

增强断言功能会带来一定的资源开销,需要根据实际情况进行权衡:

  • 内存占用 :调用栈回溯和变量监控会消耗额外内存
  • 执行时间 :信息收集和处理会增加断言触发后的处理时间
  • Flash磨损 :频繁写入Flash可能影响寿命

建议的优化策略:

  1. 在开发阶段启用所有功能
  2. 量产版本根据需求裁剪功能
  3. 对Flash写入实现磨损均衡
  4. 设置信息收集的深度限制

5.3 常见问题排查

在实际使用中可能会遇到以下问题:

  • 调用栈信息不完整

    • 检查编译优化级别,过高优化可能导致栈帧破坏
    • 确认CPU架构支持情况
  • Flash写入失败

    • 检查Flash分区是否正确
    • 确认写入地址是否擦除
  • 系统资源不足

    • 减少监控变量数量
    • 简化输出信息格式

通过本文介绍的方法,你可以为RT-Thread打造一个功能强大的断言系统,让死机问题不再神秘。在实际项目中,这种增强的断言机制可以显著缩短问题定位时间,特别是在远程维护和现场故障分析场景中价值尤为突出。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐