为什么用 RTT 替代串口日志?

串口(UART)日志是嵌入式开发中最常用的调试手段,但在以下场景中力不从心:

  • 中断上下文中:printf 重定向到 UART 通常是阻塞发送,若在中断里调用,会拉长中断响应时间,甚至导致数据丢失或系统卡死。
  • 低功耗模式:进入 STOP/STANDBY 前若 UART 还有数据未发完,必须等待发送完成,否则休眠会打断传输;而等待发送又浪费功耗。
  • 速度瓶颈:115200 波特率下每秒仅约 11KB,大量日志时严重拖慢系统。

SEGGER RTT(Real Time Transfer)通过调试接口(SWD/JTAG)直接读写目标芯片的内存缓冲区,不占用任何外设,速度可达 MB/s 级,且写入操作是非阻塞的(缓冲区满时直接丢弃或覆盖),非常适合中断和低功耗场景。

RTT 工作原理简述

RTT 在目标芯片的 RAM 中开辟一块控制块(_SEGGER_RTT),内部包含多个上行(Target→Host)和下行(Host→Target)通道。每个通道是一个环形缓冲区。

  • 目标端写日志:调用 SEGGER_RTT_Write(),将数据拷贝到上行缓冲区,更新写指针,不等待主机读取。
  • 主机端读日志:J-Link 调试器通过 SWD 周期性地读取控制块和缓冲区内容,显示在 J-Link RTT Viewer 或 RTT Logger 中。

因此,目标端几乎不耗时,且完全异步。

移植与基础配置(STM32 + MDK/IAR/GCC)

1. 添加 RTT 源码

从 SEGGER 官网下载 RTT 包,将以下文件加入工程:

  • SEGGER_RTT.c
  • SEGGER_RTT.h
  • SEGGER_RTT_Conf.h(可选,用于配置缓冲区大小)

2. 配置缓冲区大小

在 SEGGER_RTT_Conf.h 中调整:

#define BUFFER_SIZE_UP        (1024)   // 上行缓冲区,根据日志量调整
#define BUFFER_SIZE_DOWN      (16)     // 下行缓冲区,一般不用改
#define SEGGER_RTT_MAX_NUM_UP_BUFFERS   (3)
#define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (3)

注意:缓冲区位于 RAM,增大可减少丢数据概率,但会占用更多内存。

3. 初始化

RTT 无需显式初始化,首次调用 SEGGER_RTT_Write() 时会自动初始化控制块。但建议在 main() 开头调用一次:

SEGGER_RTT_Init();

4. 重定向 printf(可选)

若想继续使用 printf,可重写 _write(GCC)或 fputc(MDK):

// GCC
int _write(int fd, char *ptr, int len) {
    (void)fd;
    SEGGER_RTT_Write(0, ptr, len);
    return len;
}

// MDK
int fputc(int ch, FILE *f) {
    SEGGER_RTT_Write(0, &ch, 1);
    return ch;
}

但注意:printf 本身可能不是中断安全的,建议在中断中直接使用 SEGGER_RTT_WriteString() 或 SEGGER_RTT_printf()。

中断上下文中的安全使用

RTT 的写操作是非阻塞的,但多个中断同时写同一通道可能破坏环形缓冲区指针。SEGGER 提供了两种模式:

  • SEGGER_RTT_MODE_NO_BLOCK_SKIP:缓冲区满时跳过本次写入(默认)。
  • SEGGER_RTT_MODE_NO_BLOCK_TRIM:缓冲区满时截断,只写入部分数据。

在中断中,建议使用 SEGGER_RTT_WriteString(0, "...") 或 SEGGER_RTT_printf(0, ...),并确保:

  • 不要在高优先级中断中调用 SEGGER_RTT_printf(其内部可能使用较多栈)。
  • 若多个中断可能同时写,使用 SEGGER_RTT_LOCK() 和 SEGGER_RTT_UNLOCK() 进行临界区保护(需在 SEGGER_RTT_Conf.h 中实现)。

示例:在 SysTick 中断中打印计数:

void SysTick_Handler(void) {
    static uint32_t tick = 0;
    tick++;
    if (tick % 1000 == 0) {
        SEGGER_RTT_WriteString(0, "Tick 1000\n");
    }
}

低功耗模式下的落地配置

低功耗模式(如 STM32 的 STOP 模式)会关闭内核时钟,但 RTT 缓冲区位于 RAM,数据不会丢失。主机端 J-Link 仍可通过 SWD 读取(需保持调试器连接)。

关键点:

  • 进入低功耗前,无需等待 RTT 发送完成,因为写入是异步的。
  • 若使用 SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL 模式,则可能阻塞,务必避免。
  • 唤醒后,RTT 继续工作,主机端会看到休眠期间写入的日志(只要 RAM 保持供电)。

示例:在进入 STOP 模式前打印一条日志:

SEGGER_RTT_WriteString(0, "Entering STOP mode...\n");
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后
SEGGER_RTT_WriteString(0, "Woke up!\n");

注意:若使用 STANDBY 模式,RAM 内容丢失,RTT 缓冲区也会丢失,此时日志无法保留。

完整代码示例(STM32 + GCC)

#include "SEGGER_RTT.h"
#include "stm32f4xx_hal.h"

void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    SEGGER_RTT_Init();
    SEGGER_RTT_WriteString(0, "RTT Log Start\n");

    uint32_t counter = 0;
    while (1) {
        SEGGER_RTT_printf(0, "Counter: %lu\n", counter++);
        HAL_Delay(1000);

        if (counter % 5 == 0) {
            SEGGER_RTT_WriteString(0, "Entering STOP mode for 2s...\n");
            HAL_SuspendTick();
            HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
            SystemClock_Config(); // 唤醒后重新配置时钟
            HAL_ResumeTick();
            SEGGER_RTT_WriteString(0, "Woke up!\n");
        }
    }
}

// 重定向 _write 以支持 printf
int _write(int fd, char *ptr, int len) {
    (void)fd;
    SEGGER_RTT_Write(0, ptr, len);
    return len;
}

注意事项与常见坑

  • 缓冲区大小:默认上行 1KB,若日志量大且主机读取慢,会丢数据。可增大至 4KB 或使用多通道。
  • 中断优先级:RTT 写操作本身很快,但 SEGGER_RTT_printf 涉及格式化,可能耗时较长,避免在最高优先级中断中使用。
  • 低功耗调试:进入 STOP 模式后,SWD 可能断开,需在调试器中设置“Connect under reset”或保持调试器连接。
  • 多任务环境:RTOS 中多个任务写同一通道,建议使用互斥锁或为每个任务分配独立通道。
  • 主机端工具:使用 J-Link RTT Viewer 或 RTT Logger,确保 J-Link 固件为最新。
  • 不要滥用:RTT 虽快,但大量日志仍会占用 CPU 时间,生产版本应关闭或降级。

总结

SEGGER RTT 是替代串口日志的利器,尤其适合中断上下文和低功耗场景。通过合理配置缓冲区、选择非阻塞模式、注意临界区保护,即可实现高效、非阻塞的日志系统。在 STM32 上移植简单,几乎无需额外硬件,值得在项目中推广。