
1. 项目概述为什么嵌入式调试离不开SEGGER RTT如果你在嵌入式开发领域摸爬滚打过几年一定对调试的“痛”深有体会。传统的调试手段比如串口打印UART几乎是每个工程师的“启蒙老师”。它简单、直接但也伴随着一堆麻烦需要占用宝贵的硬件UART外设和引脚波特率设置不当会导致乱码高速打印时可能丢数据最要命的是输出大量日志时会显著拖慢程序运行速度让你在分析实时性问题时束手束脚。而SEGGER RTTReal Time Transfer实时传输的出现就像给嵌入式调试世界打开了一扇新的大门。我第一次接触RTT是在一个对实时性要求极高的电机控制项目上当时用串口打印FOC算法中的电流环数据波形都失真了根本没法分析。换上RTT后那种“丝滑”的数据流和几乎零侵入的体验让我瞬间决定把它列为后续项目的标配调试工具。简单来说SEGGER RTT是一种通过调试器如J-Link在目标MCU和PC主机之间进行高速数据通信的技术。它不需要额外的硬件接口仅利用芯片已有的调试模块如ARM CoreSight在RAM中开辟一小块缓冲区就能实现双向的、极低延迟的打印输出和输入。对于开发者而言你可以在代码中像使用printf一样调用SEGGER_RTT_printf()日志信息就会近乎实时地显示在PC端的RTT Viewer工具上完全不影响程序的正常执行时序。它特别适合谁呢首先是所有使用ARM Cortex-M/R/A系列芯片的开发者因为J-Link和RTT对其支持最为完善。其次是那些苦于串口资源紧张、调试信息量大、或对系统实时性有苛刻要求的项目。无论是做物联网设备、工业控制、还是消费电子当你需要洞察程序运行的每一个细节而又不想打扰它时RTT就是你工具箱里的“瑞士军刀”。2. RTT核心原理与架构深度拆解要真正用好RTT不能只停留在调通API的层面理解其内部工作原理能帮助你在遇到复杂问题时快速定位并发挥其最大效能。2.1 “通道”与“缓冲区”RTT的数据高速公路RTT的核心设计思想是围绕“通道Channel”和“环形缓冲区Ring Buffer”构建的。你可以把整个RTT系统想象成一个高效物流中心。上行缓冲区Up Buffer 这是从目标MCU发送到PC如RTT Viewer的数据通道主要用于打印输出。你的SEGGER_RTT_Write()或SEGGER_RTT_printf()函数调用就是将数据打包放进这个上行缓冲区的“发货区”。下行缓冲区Down Buffer 这是从PC发送到目标MCU的数据通道主要用于终端输入。当你在RTT Viewer里键入命令数据就被放入下行缓冲区等待MCU端的SEGGER_RTT_Read()函数来“取货”。每个方向上行/下行都可以有多个通道默认通道0用于终端I/O类似printf/scanf但你完全可以自定义通道1、2、3...用于传输不同类型的数据比如通道1专用于传输高速的传感器原始数据通道2用于传输事件日志实现数据分流。缓冲区采用环形队列FIFO结构。这意味着当缓冲区写满时如果PC端没有及时读取消费新的数据会覆盖最旧的数据。这听起来是个缺点但实际上在追求极致实时性的场景下这保证了总是能读到“最新”的数据状态避免了因为历史数据堆积导致缓冲区阻塞进而影响程序运行。当然你可以通过增大缓冲区大小来降低覆盖发生的概率。2.2 调试器如何“魔法般”地访问内存这是RTT最精妙的部分也是其零硬件依赖的关键。它并不通过任何外设如UART、USB进行物理通信。整个过程依赖于调试接口控制块Control Block 在你的代码中通过SEGGER_RTT_Init()初始化后会在RAM中定义一个固定的数据结构即RTT控制块。这个控制块里包含了所有通道缓冲区的地址、大小、读写指针等关键信息。它的符号名是固定的默认为_SEGGER_RTT并且其结构对SEGGER的工具链是公开的。调试探针的“窥视” J-Link、J-Trace或支持RTT的DAPLink调试器在连接目标板时不仅能够下载程序、控制CPU运行还能通过调试访问端口DAP直接读写目标芯片的RAM内存。自动发现与通信 上电后PC端的RTT客户端软件如J-Link RTT Viewer会通过调试器在目标芯片的RAM地址空间中自动扫描寻找那个具有特定标识符SEGGER RTT的控制块。一旦找到它就知道了所有缓冲区的位置。轮询式读写 客户端软件以极高的频率可配置通常为1kHz以上通过调试器轮询检查上行缓冲区的读写指针。如果发现写指针移动了说明MCU写入了新数据它就立刻通过调试接口将那段新数据从RAM中读取出来显示在PC屏幕上。下行方向同理客户端将输入数据写入下行缓冲区MCU端轮询读取。整个过程完全在后台通过调试链路完成不占用CPU的任何计算资源除了你调用RTT API的那一瞬间也不需要使用UART等外设实现了真正的“零开销”通信。其延迟极低通常仅在微秒级别这是串口通信毫秒级无法比拟的。注意 正因为RTT依赖调试接口所以它的一个前提是调试接口必须保持连接且功能正常。如果你的芯片进入了深度睡眠模式并关闭了调试模块或者调试引脚被复用为其他功能RTT将无法工作。此外某些带安全启动或TrustZone的芯片可能需要特殊配置才能允许调试器访问特定内存区域。3. 从零开始集成与配置SEGGER RTT理论懂了接下来我们动手把它集成到你的项目中。这里以在ARM Cortex-M平台的裸机或RTOS如FreeRTOS环境下集成为例。3.1 获取源码与工程集成SEGGER官方将RTT源码随其J-Link软件包一起分发你也可以从其官网单独下载。定位源码 安装J-Link软件包后RTT源码通常位于J-Link安装目录\Samples\RTT。核心文件只有几个SEGGER_RTT.c 实现文件包含所有API。SEGGER_RTT.h 头文件包含API声明和配置宏。SEGGER_RTT_Conf.h最重要的配置文件你需要根据项目定制它。SEGGER_RTT_printf.c 可选提供了SEGGER_RTT_printf函数需要标准库stdio.h的支持或使用SEGGER自家的emCompress软件包用于浮点数格式化。集成到工程将上述.c和.h文件拷贝到你的项目源码目录下例如Middlewares/SEGGER/RTT。在IDE如Keil MDK、IAR EWARM、STM32CubeIDE中将这些文件添加到你的工程。在需要使用的源文件中包含头文件#include SEGGER_RTT.h。确保你的工程链接脚本为RTT控制块和缓冲区分配了足够的RAM空间。通常不需要特殊设置因为它们是全局变量编译器会自动分配在数据段。3.2 关键配置详解SEGGER_RTT_Conf.h这个文件是RTT性能和行为的总开关。下面解析几个最关键的配置项// SEGGER_RTT_Conf.h /********************************************************************* * Configuration of RTT */ #define SEGGER_RTT_MAX_NUM_UP_BUFFERS (3) // 最大上行缓冲区数量。至少为1通道0。如果需要多个通道传输不同数据就增加。 #define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (1) // 最大下行缓冲区数量。通常1个通道0用于输入就够了。 #define BUFFER_SIZE_UP (1024) // 上行缓冲区默认大小字节。通道0使用此大小。根据你的打印数据量调整。如果打印很频繁建议设为2048或更大。 #define BUFFER_SIZE_DOWN (16) // 下行缓冲区默认大小字节。用于终端输入通常16-32字节足够。 #define SEGGER_RTT_MODE_DEFAULT SEGGER_RTT_MODE_NO_BLOCK_SKIP // 默认缓冲模式缓冲模式Buffer Mode 这是行为核心决定了当缓冲区满时写入函数的行为。SEGGER_RTT_MODE_NO_BLOCK_SKIP默认且最常用的模式。缓冲区满时直接丢弃新数据跳过函数立即返回。这是保证实时性的关键确保写操作不会阻塞你的应用程序。适用于高速日志输出你接受丢失部分旧数据以换取程序持续运行。SEGGER_RTT_MODE_NO_BLOCK_TRIM 缓冲区满时丢弃缓冲区开头的旧数据以腾出空间写入新数据。保证你能看到最新的数据。SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL阻塞模式。缓冲区满时写入函数会等待直到有空间为止。这会严重破坏实时性除非你非常确定数据流不会爆缓冲区否则慎用SEGGER_RTT_PRINTF_BUFFER_SIZE 如果你使用SEGGER_RTT_printf这个宏定义了内部用于格式化字符串的临时缓冲区大小。如果打印的字符串很长比如打印很长的JSON需要调大这个值否则会被截断。默认值64可能偏小建议设置为256或512。3.3 初始化与基础API调用集成完成后使用非常简单。初始化 理论上全局变量会在启动时自动初始化。但为了确保控制块标识符正确写入最好在main()函数早期显式调用一次#include SEGGER_RTT.h int main(void) { // 硬件初始化... SEGGER_RTT_Init(); // 初始化RTT控制块 SEGGER_RTT_printf(0, 系统启动成功\n); // 在通道0打印 // ... 主循环 }对于RTOS环境确保在任务调度器启动vTaskStartScheduler()之前调用初始化。核心APISEGGER_RTT_Write(unsigned BufferIndex, const char* pBuffer, unsigned NumBytes) 向指定缓冲区索引写入原始字节数据。SEGGER_RTT_printf(unsigned BufferIndex, const char* sFormat, ...) 最常用的函数像printf一样格式化输出到指定通道。注意 默认实现不支持浮点数%f如需支持需集成SEGGER_RTT_printf.c并启用相关宏或使用SEGGER的软件浮点库。int SEGGER_RTT_GetKey(void) 从下行缓冲区通道0读取一个字符非阻塞如果没有数据则返回-1。适合实现简单的交互。unsigned SEGGER_RTT_HasKey(void) 检查下行缓冲区是否有可读字符。int SEGGER_RTT_Read(unsigned BufferIndex, char* pBuffer, unsigned BufferSize) 从指定下行缓冲区读取数据。4. 高级应用与多场景实战技巧掌握了基础我们来看看如何把RTT玩出花来应对各种复杂场景。4.1 多通道分流让调试信息井井有条在大型系统中所有日志都挤在通道0会显得混乱。我们可以利用多通道进行分类。// 定义通道索引可放在头文件中 #define RTT_CHANNEL_TERMINAL 0 // 默认终端用于普通打印和交互 #define RTT_CHANNEL_SENSOR_DATA 1 // 传感器原始数据流 #define RTT_CHANNEL_EVENT_LOG 2 // 关键事件日志 #define RTT_CHANNEL_TRACE 3 // 函数调用跟踪 // 在系统初始化时配置额外的上行通道 SEGGER_RTT_ConfigUpBuffer(RTT_CHANNEL_SENSOR_DATA, SensorData, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); SEGGER_RTT_ConfigUpBuffer(RTT_CHANNEL_EVENT_LOG, EventLog, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); // 在不同的模块中使用不同的通道 void Sensor_Task(void) { float temp read_temperature(); // 高速数据流使用专用通道避免影响终端输出 SEGGER_RTT_printf(RTT_CHANNEL_SENSOR_DATA, %.2f,, temp); } void Error_Handler(int code) { // 关键事件使用事件日志通道 SEGGER_RTT_printf(RTT_CHANNEL_EVENT_LOG, [ERR] Code: %d at %lu ms\n, code, HAL_GetTick()); }在PC端的RTT Viewer或J-Link RTT Logger中你可以选择只监听SensorData通道并将其数据保存为CSV文件直接导入MATLAB或Excel进行分析同时在另一个窗口监听EventLog通道只看系统关键事件。这样实现了调试信息的解耦和专业化处理。4.2 与RTOS深度结合任务级调试在FreeRTOS、RT-Thread等系统中RTT可以成为任务监控的利器。// 示例在FreeRTOS中为每个任务创建独立的RTT输出上下文利用任务标签或自定义 void vApplicationTickHook(void) { // 在滴答钩子中检查任务堆栈使用情况并输出到RTT专用通道 TaskStatus_t *pxTaskStatusArray; volatile UBaseType_t uxArraySize, x; uxArraySize uxTaskGetNumberOfTasks(); pxTaskStatusArray pvPortMalloc(uxArraySize * sizeof(TaskStatus_t)); if (pxTaskStatusArray ! NULL) { uxArraySize uxTaskGetSystemState(pxTaskStatusArray, uxArraySize, NULL); SEGGER_RTT_printf(RTT_CHANNEL_TRACE, \n Task Snapshot %lu ms \n, xTaskGetTickCount()); for (x 0; x uxArraySize; x) { SEGGER_RTT_printf(RTT_CHANNEL_TRACE, Task: %s, State: %d, Prio: %lu, Stack: %lu\n, pxTaskStatusArray[x].pcTaskName, pxTaskStatusArray[x].eCurrentState, pxTaskStatusArray[x].uxCurrentPriority, pxTaskStatusArray[x].usStackHighWaterMark); } vPortFree(pxTaskStatusArray); } }你甚至可以创建一个低优先级的“日志聚合任务”其他任务通过队列、邮箱等RTOS通信机制将日志信息发送给这个聚合任务由它统一通过RTT输出。这样可以避免在关键任务或中断中直接调用RTT_printf虽然RTT本身很快但格式化函数printf可能较慢进一步保证实时性。4.3 实现交互式调试终端RTT的下行通道让你可以打造一个简单的交互式命令行界面CLI用于动态查询或修改系统状态。void CLI_Task(void *argument) { char cmd_buf[64]; int len; SEGGER_RTT_printf(0, CLI Ready. Type help for commands.\n ); for (;;) { if (SEGGER_RTT_HasKey()) { len SEGGER_RTT_Read(0, cmd_buf, sizeof(cmd_buf)-1); if (len 0) { cmd_buf[len] \0; // 确保字符串结束 // 简单处理回车换行 if (cmd_buf[len-1] \n || cmd_buf[len-1] \r) { cmd_buf[len-1] \0; if (len 1 (cmd_buf[len-2] \r || cmd_buf[len-2] \n)) { cmd_buf[len-2] \0; } process_command(cmd_buf); // 处理命令 SEGGER_RTT_printf(0, \n ); } } } vTaskDelay(pdMS_TO_TICKS(10)); // 让出CPU } } void process_command(const char* cmd) { if (strcmp(cmd, help) 0) { SEGGER_RTT_printf(0, Commands: help, read_temp, set_led [on|off], sysinfo\n); } else if (strcmp(cmd, sysinfo) 0) { SEGGER_RTT_printf(0, Heap Free: %lu bytes\n, xPortGetFreeHeapSize()); SEGGER_RTT_printf(0, Uptime: %lu ticks\n, xTaskGetTickCount()); } else if (strncmp(cmd, set_led , 8) 0) { // 解析参数控制LED... SEGGER_RTT_printf(0, LED command processed.\n); } else { SEGGER_RTT_printf(0, Unknown command: %s\n, cmd); } }这样你无需停止程序、无需重新编译就能通过RTT Viewer的输入框实时查询内存、任务状态甚至控制外设极大提升了调试效率。5. 性能优化与深度调优指南要让RTT跑得既快又稳需要一些精细的调整。5.1 缓冲区大小与模式的权衡选择这是一个典型的空间换时间和可靠性的问题。场景一高频、小数据量状态打印如电机控制电流环每1ms打印几个浮点数。挑战 数据产生速率极高~1kHz但每次数据量小几十字节。策略 使用较小的上行缓冲区如512字节配合NO_BLOCK_SKIP模式。因为数据更新极快我们更关心最新数据可以接受偶尔的覆盖。小缓冲区能减少调试器轮询时的内存访问量理论上响应更快。配置示例BUFFER_SIZE_UP 512,SEGGER_RTT_MODE_DEFAULT SEGGER_RTT_MODE_NO_BLOCK_SKIP场景二低频、大数据块传输如每10秒传输一幅小图片或一段音频数据。挑战 单次数据量大几KB需要保证完整性。策略 使用专用的上行通道并配置足够大的缓冲区大于单次数据块模式可以设为NO_BLOCK_TRIM或甚至BLOCK_IF_FIFO_FULL如果你能确保PC端消费速度跟得上。同时在PC端使用J-Link RTT Logger而非Viewer因为Logger是为连续记录大数据设计的。配置示例#define IMAGE_BUFFER_SIZE (8*1024) // 8KB SEGGER_RTT_ConfigUpBuffer(1, ImageData, myImageBuffer, IMAGE_BUFFER_SIZE, SEGGER_RTT_MODE_NO_BLOCK_TRIM);场景三关键事件日志不容丢失。挑战 事件发生频率不确定但每条信息都至关重要。策略 使用中等大小的缓冲区如2KB模式设为NO_BLOCK_TRIM。同时在代码层面实现一个简单的日志缓存队列当SEGGER_RTT_Write返回0表示缓冲区满数据被跳过时将日志暂存在一个由自己管理的备份RAM队列中然后在一个低优先级任务里尝试重新发送。这实现了应用层的重传机制。5.2 中断服务程序ISR中使用RTT的禁忌与正确姿势在ISR中直接调用SEGGER_RTT_printf是非常危险的行为因为printf类函数内部可能使用动态内存、或本身执行时间较长这会导致中断执行时间不可控可能引发其他中断丢失或系统异常。安全做法仅使用SEGGER_RTT_Write 在ISR中只调用最底层的SEGGER_RTT_Write函数它只是内存拷贝速度极快。提前将固定的消息字符串定义好。// 预定义消息 static const char ISR_MSG_UART_RX[] [ISR] UART RX Ready\n; void USART1_IRQHandler(void) { if (USART1-SR USART_SR_RXNE) { // ... 处理数据 // 安全地记录ISR发生 SEGGER_RTT_Write(0, ISR_MSG_UART_RX, sizeof(ISR_MSG_UART_RX)-1); } }使用无锁缓冲区或标志位 在ISR中设置一个标志位或者将一个简短的整数ID写入一个无锁的环形缓冲区。在主循环或一个专用的日志任务中轮询这个标志或缓冲区然后进行格式化和完整的RTT输出。这是最推荐的方式将ISR的负担降到最低。5.3 与DAPLink等非J-Link调试器的适配除了原厂J-Link很多开源调试器如DAPLink常见于ST-Link V2-1、PyOCD等也通过实现RTT协议支持了此功能。使用时需要注意固件版本 确保你的DAPLink固件是比较新的版本并且编译时启用了RTT支持DAP_RTT_ENABLE宏。客户端工具 你可能无法使用SEGGER官方的RTT Viewer它通常只认J-Link。替代方案是使用PyOCD命令行工具或OpenOCD配合Telnet或者使用一些第三方集成了RTT功能的IDE插件如VSCode的Cortex-Debug插件。连接稳定性 非官方调试器在高速RTT通信时稳定性可能不如J-Link。如果发现数据丢失严重尝试降低RTT客户端的轮询频率或者检查调试器与目标板之间的连接质量线缆、速度设置。6. 实战问题排查与经验实录即使配置正确在实际项目中还是会遇到各种“坑”。下面是我和同事们踩过的一些典型问题及解决方案。6.1 连接与通信失败排查表现象可能原因排查步骤与解决方案RTT Viewer显示“RTT Control Block not found”1. RTT源码未正确链接到工程。2. 芯片未正确连接或调试接口被禁用。3. 控制块被编译器优化掉。4. 目标芯片RAM地址范围不对。1. 检查SEGGER_RTT.c是否被编译_SEGGER_RTT符号是否存在于生成的map文件中。2. 确保调试器连接正常芯片已复位并运行。检查芯片的调试配置如DBGMCU寄存器确保调试接口使能。3. 在SEGGER_RTT_Conf.h中将控制块定义为volatile通常已是并检查编译器优化等级尝试在调试时使用-O0。4. 在RTT Viewer中手动输入正确的RAM起始地址和大小通常不需要但某些非标准芯片需要。能连接但无任何输出1. 程序未调用SEGGER_RTT_Init()或输出API。2. 程序卡在某个地方如HardFault未执行到输出语句。3. 缓冲区模式设置不当数据被立即跳过。1. 在main函数开头添加一个简单的SEGGER_RTT_WriteString(0, START\n);进行测试。2. 使用调试器单步运行检查程序流。或者先使用一个简单的LED闪烁程序测试RTT。3. 将缓冲区模式临时改为SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL如果此时有输出说明之前是缓冲区太小或PC端未及时读取导致数据被丢弃。输出乱码或字符缺失1. PC端RTT Viewer的编码设置与MCU端不匹配。2. 缓冲区溢出数据被覆盖。3. 在中断中调用不安全的输出函数导致数据错乱。1. 确保RTT Viewer的编码设置为“UTF-8”或“ANSI”与你的源码编码一致。2. 增大BUFFER_SIZE_UP并检查PC端是否及时连接和读取。3. 严格遵守ISR中只使用SEGGER_RTT_Write的规则避免在中断中使用printf。输出速度慢有明显延迟1. RTT Viewer的刷新率设置过低。2. 调试器连接速度JTAG/SWD频率太低。3. MCU端频繁输出大量数据超出调试链路带宽。1. 在RTT Viewer的设置中提高“刷新周期Refresh Period”例如从100ms改为10ms。2. 在J-Link Commander或IDE调试配置中提高JTAG/SWD时钟频率如从1MHz提高到4MHz。3. 优化代码减少不必要的打印或使用专用通道和更大的缓冲区采用NO_BLOCK_SKIP模式保证程序运行。使用printf格式化浮点数失败默认的SEGGER_RTT_printf实现未启用浮点数支持。1. 确保工程包含了SEGGER_RTT_printf.c。2. 在SEGGER_RTT_Conf.h中定义SEGGER_RTT_PRINTF_BUFFER_SIZE为一个足够大的值如256。3.关键启用浮点支持。通常需要定义SEGGER_RTT_PRINTF_ENABLE_FLOAT宏并且链接了相应的库如libc的浮点格式化函数或SEGGER的emCompress库。对于Newlib Nano等精简库可能需要额外实现_write等系统调用。6.2 性能瓶颈分析与优化心得瓶颈往往在PC端而非MCU端 RTT的MCU端API效率极高一次Write操作只是内存拷贝。真正的瓶颈通常是调试链路的带宽和PC端软件的消费速度。如果发现数据丢失首先考虑增大缓冲区和提高PC端读取频率而不是优化MCU代码。格式化是性能杀手SEGGER_RTT_printf中耗时的不是RTT本身而是sprintf类的格式化过程尤其是浮点数格式化。在性能敏感的循环或中断中避免使用printf。可以预先格式化好字符串到静态缓冲区再用Write发送或者直接发送二进制数据在PC端用脚本或工具解析。善用“静默”模式 在SEGGER_RTT_Conf.h中可以定义一个SEGGER_RTT_DISABLE宏在发布版本或性能测试时通过条件编译完全禁用RTT功能做到零开销。或者实现一个动态的日志级别控制在运行时关闭低级别日志的输出。6.3 一个真实的“坑”缓存一致性Cache Coherency问题这个问题在使用了数据缓存D-Cache的高性能Cortex-M7/M33等芯片上尤为突出。现象 你在代码中调用了SEGGER_RTT_Write数据也确实写入了RAM中的缓冲区。但通过调试器J-Link去读取时看到的却是旧数据或者全零。程序逻辑看起来完全正确。根源 CPU核心写入数据时是写到了自己的数据缓存D-Cache里并没有立即写回到真正的RAM主存中。而调试器是通过调试访问端口DAP直接读取RAM它绕过了CPU的缓存系统因此读不到缓存中尚未刷新的新数据。解决方案 需要确保在调用RTT写入函数后强制将缓存行写回内存并让调试器访问的区域不被缓存。对于缓冲区内存区域配置为“非缓存Non-Cacheable”或“写通Write-Through”。这通常在MPU内存保护单元或芯片的启动文件/链接脚本中配置。例如在STM32CubeIDE中你可以通过MPU配置将RTT控制块和缓冲区所在的RAM区域如0x20000000开始的一段属性设置为Device或Normal Non-Cacheable。在写入操作后手动执行缓存清理Clean操作。对于CMSIS可以使用SCB_CleanDCache_by_Addr()函数。但这会增加开销方法1是更根本的解决方案。// 方法2示例不推荐作为首选仅作备用 #include core_cm7.h // 包含CMSIS头文件 void RTT_Write_WithCache(uint8_t ch, const void* p, uint32_t len) { SEGGER_RTT_Write(ch, p, len); // 清理数据缓存确保写入的数据对调试器可见 SCB_CleanDCache_by_Addr((uint32_t*)p, len); }第一次遇到这个问题时我花了整整一天时间排查所有代码逻辑都对就是看不到输出。最后用内存观察窗口对比CPU视角和调试器视角的RAM值才恍然大悟。所以当你使用带缓存的高端MCU时请务必把这一点列入检查清单。