为什么用 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 上移植简单,几乎无需额外硬件,值得在项目中推广。