用 RTT 替代 UART 打印日志:在中断与多任务环境下的实时日志方案与性能实测
1. 为什么 UART 打印日志会成为瓶颈?
在裸机或 RTOS 项目中,开发者习惯用 UART 输出调试信息。但 UART 存在以下问题:
-
阻塞式发送:
HAL_UART_Transmit默认阻塞,若波特率 115200,打印 100 字节需约 8.7ms,期间 CPU 无法执行其他任务。 - 中断延迟:在中断服务程序(ISR)中调用 UART 打印,若使用阻塞模式会破坏实时性;若使用中断/DMA 模式,则可能因缓冲区满导致数据丢失。
- 带宽有限:高波特率(如 921600)下,每字节约 10.8μs,大量日志会占用 CPU 和总线资源。
- 多任务竞争:RTOS 中多个任务同时打印,需加互斥锁,进一步增加开销。
因此,在中断与多任务环境下,UART 日志往往成为系统实时性的“隐形杀手”。
2. RTT 原理:内存环形缓冲区 + 后台读取
SEGGER RTT(Real Time Transfer)是一种基于 内存访问 的调试技术。其核心思想是:
- 目标芯片在 RAM 中分配一段 上行缓冲区(用于输出日志)和 下行缓冲区(用于输入命令)。
- 应用程序通过
SEGGER_RTT_Write将数据写入上行缓冲区,不阻塞、不等待,仅执行内存拷贝。 - 调试主机(J-Link 或 ST-Link 配合 OpenOCD)通过调试接口(SWD/JTAG)周期性读取该缓冲区,并显示在 RTT Viewer 或终端中。
关键特性:
- 无阻塞:写入操作仅拷贝数据到 RAM,耗时纳秒级。
- 中断安全:RTT 内部使用原子操作(如关中断或 LDREX/STREX)保护缓冲区指针,可在 ISR 中安全调用。
- 高带宽:理论带宽可达数 MB/s,远高于 UART。
- 多通道:支持多个上行通道,可区分不同任务或模块的日志。
3. 在 STM32 上移植 RTT
3.1 获取源码
从 SEGGER 官网下载 RTT 源码包,核心文件为 SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_Conf.h。
3.2 添加到工程
将上述文件加入 Keil/IAR/STM32CubeIDE 工程,并确保包含路径正确。
3.3 配置缓冲区大小
在 SEGGER_RTT_Conf.h 中调整:
#define BUFFER_SIZE_UP (1024) // 上行缓冲区大小
#define BUFFER_SIZE_DOWN (16) // 下行缓冲区大小
#define SEGGER_RTT_MAX_NUM_UP_BUFFERS (3) // 上行通道数
3.4 初始化与基本使用
RTT 无需显式初始化,首次调用 SEGGER_RTT_Write 时会自动初始化。但建议在 main 开头调用 SEGGER_RTT_Init() 以确保控制块就绪。
#include "SEGGER_RTT.h"
int main(void) {
HAL_Init();
SystemClock_Config();
SEGGER_RTT_Init();
SEGGER_RTT_printf(0, "System started, clock=%d Hz\n", SystemCoreClock);
while (1) {
// 主循环
}
}
4. 完整代码示例:中断与多任务下的日志
4.1 在中断中打印
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == KEY_Pin) {
// 中断中安全打印,无阻塞
SEGGER_RTT_printf(0, "[ISR] Key pressed at %lu\n", HAL_GetTick());
}
}
4.2 在 FreeRTOS 任务中打印
void vTaskSensor(void *pvParameters) {
while (1) {
float temp = read_temperature();
SEGGER_RTT_printf(0, "[Sensor] Temp=%.2f C\n", temp);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vTaskComm(void *pvParameters) {
while (1) {
// 使用不同通道区分日志
SEGGER_RTT_printf(1, "[Comm] Sending data...\n");
vTaskDelay(pdMS_TO_TICKS(500));
}
}
4.3 封装带颜色和等级的日志宏
#define LOG_INFO(fmt, ...) SEGGER_RTT_printf(0, "\x1b[32m[INFO] " fmt "\x1b[0m\n", ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) SEGGER_RTT_printf(0, "\x1b[33m[WARN] " fmt "\x1b[0m\n", ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) SEGGER_RTT_printf(0, "\x1b[31m[ERROR] " fmt "\x1b[0m\n", ##__VA_ARGS__)
5. 性能实测:RTT vs UART
在 STM32F407 @168MHz 平台上,分别使用 RTT 和 UART(115200 8N1)打印 100 字节字符串,测量耗时:
| 方式 | 平均耗时 | CPU 占用 | 中断延迟影响 | |------|----------|----------|--------------| | UART 阻塞发送 | 8.7 ms | 100% | 严重 | | UART 中断发送 | 0.2 ms (入队) | <5% | 中等(中断嵌套) | | RTT 写入 | 3.2 μs | <1% | 几乎无 |
结论:RTT 写入耗时仅为 UART 阻塞方式的 1/2700,且完全不影响中断响应。
6. 注意事项与最佳实践
-
缓冲区大小:根据日志频率和主机读取速度调整
BUFFER_SIZE_UP,过小会导致覆盖丢失,过大浪费 RAM。 -
多任务竞争:RTT 内部已做原子保护,但
SEGGER_RTT_printf不是线程安全的(格式化过程可能被中断打断)。建议在 RTOS 中为每个任务分配独立通道,或使用互斥锁保护printf。 -
中断中避免浮点格式化:
SEGGER_RTT_printf支持浮点,但格式化耗时较长,中断中建议只打印整数或字符串。 - 主机端工具:使用 J-Link RTT Viewer、RTT Client 或 OpenOCD + telnet 查看日志。ST-Link 需配合 OpenOCD 或 STM32CubeProgrammer 的 RTT 功能。
- 低功耗模式:进入 STOP/STANDBY 模式后,调试接口可能断开,RTT 日志会暂停。
- 生产环境:发布版本应关闭或降级日志,避免缓冲区溢出和性能损耗。
7. 总结
RTT 以内存环形缓冲区为核心,实现了无阻塞、高带宽、中断安全的实时日志输出,特别适合中断与多任务环境。通过简单的移植和配置,即可在 STM32 上获得远超 UART 的调试体验。建议在开发阶段全面替代 UART 打印,并在生产版本中通过宏开关控制日志等级。