
说在前面的话
你听说过 J-Link 的 RTT 吗?官方是这样宣传的:

简单来说,只要拥有一个 J-Link,你就能享受以下便利:
- 无需占用 USART 或者 USB 转串口工具,将
printf 重定向到由 J-Link 提供的虚拟串口上。
- 支持任何 J-Link 声称支持的芯片。
- 高速通信,不影响芯片的实时响应。
它的缺点也很明显:
- 你必须拥有一个 J-Link。如果你使用的是 CMSIS-DAP 或者 ST-Link 之类的第三方调试工具,就无法享受这一福利。
- 你必须在工程中手动插入一段代码。
曾几何时,J-Link 的这一“特权”让多少非 J-Link 用户羡慕不已。看看手中的 ST-Link、ULINKpro 和各类廉价的板载 CMSIS-DAP 调试器,真是“隔壁邻居的小孩都馋哭了”。

如果我告诉你,其实 MDK 中内置了一种非常简单的方式,可以实现类似的功能,并且具有以下特点,你会不会心动?
- 支持所有的调试仿真器,哪怕是自己手搓的 CMSIS-DAP 也行。
- MDK 原生功能,连 CMSIS-Pack 都不用额外安装。
- 点几下鼠标就可以通过 RTE(Run-Time Environment) 完成部署。
- 除了简单的初始化函数外,无需手动插入代码。
- 可以将你的
printf 输出直接打印在 MDK 的 Debug (printf) View 窗口中。

部署从未如此简单
步骤一:RTE 配置
依次通过菜单 Project -> Manage -> Run-Time Environment 打开 RTE 配置窗口:

找到并展开 CMSIS-Compiler 选项卡,勾选 CORE:

展开 STDOUT(API) 下的 I/O,勾选 Event Recorder。如果此前没有勾选过 CMSIS-View 下的 EventRecorder,RTE 会出现黄色的警告:

此时,只需单击窗口左下角的 Resolve 按钮,MDK 会自动解决依赖问题。
单击确定后,我们会在工程管理器中看到以下内容:

至此,所需的工具都已经成功地加入到工程中了。
这里的 EventRecorderConf.h 是一个可编辑的配置文件,实践中,我们基本不用修改它——使用默认配置即可。如果你还部署了 perf_counter,则可以在上述头文件的尾部添加如下代码:
#ifdef __PERF_COUNTER__
# undef EVENT_TIMESTAMP_SOURCE
# undef EVENT_TIMESTAMP_FREQ
# define EVENT_TIMESTAMP_SOURCE 3
# define EVENT_TIMESTAMP_FREQ 0
#endif
如果你在 RTE 中找不到 CMSIS-Compiler 和 CMSIS-View,说明你的 MDK 版本较低。如果不想升级 MDK,则可以在 Pack Installer 中手动安装:

如果你的列表中看不到上述组件,可以通过顶部菜单的 Packs -> Check For Updates 来刷新列表:

如果 Pack Installer 无法正确访问网络,你也可以通过下面的链接从官方直接下载对应的 cmsis-pack:
步骤二:服务初始化
在包含 main() 函数的 C 代码文件中,按照如下格式添加对头文件的包含:
#include <RTE_Components.h>
#undef __USE_EVENT_RECORDER__
#if defined(RTE_Compiler_EventRecorder) || defined(RTE_CMSIS_View_EventRecorder)
# define __USE_EVENT_RECORDER__ 1
#endif
#if __USE_EVENT_RECORDER__
# include <EventRecorder.h>
# include "EventRecorderConf.h"
#endif
在 main() 函数中添加对 EventRecorder 服务的初始化:
void main(void)
{
...
#if __USE_EVENT_RECORDER__
EventRecorderInitialize(0, 1);
#endif
...
}
如果你从未使用过 EventRecorder 也不必担心,这段代码的主要作用是为 printf 专门开启一个数据通道。
理论上,到这里我们就完成了部署。可以在进入调试模式后,通过 MDK 的 Debug (printf) View 窗口来观察 printf 的输出结果。例如,我们在 main() 函数中打印一个 “Hello World\r\n”:
#include <stdio.h>
#include <RTE_Components.h>
#undef __USE_EVENT_RECORDER__
#if defined(RTE_Compiler_EventRecorder) || defined(RTE_CMSIS_View_EventRecorder)
# define __USE_EVENT_RECORDER__ 1
#endif
#if __USE_EVENT_RECORDER__
# include <EventRecorder.h>
# include "EventRecorderConf.h"
#endif
void main(void)
{
...
#if __USE_EVENT_RECORDER__
EventRecorderInitialize(0, 1);
#endif
...
printf("Hello World\r\n");
...
}
编译并进入调试模式后,通过菜单 View -> Serial Windows -> Debug (printf) View 打开窗口:

运行程序,即可在 Debug (printf) View 窗口中看到输出结果:


常见问题
如果你的工程从未提供过对 .bss.noinit 数据段的处理,那么可能会发现通过上述方法实现的 printf 输出不太稳定——时有时无,处于一种“薛定谔”的状态。
这是因为 EventRecorder 有一段数据被放置在了 .bss.noinit section 中,目的是在芯片复位后不会破坏其中原有的内容。如果你的工程没有专门处理 .bss.noinit,就会在进入调试模式后,从 Command 窗口中看到类似如下的警告:

即:
Warning: Event Recorder not located in uninitialized memory!
如果遇到这种情况应该怎么办呢?
打开工程配置窗口 Options for Target,切换到 Linker 选项卡:

首先,确保勾选了图中的 Use Memory Layout from Target Dialog 选项。然后,取消对它的勾选:

此时,MDK 会基于当前的 Memory Layout,在 Out 目录下生成一个与工程同名的链接脚本(例如工程叫 example,则生成 example.sct)。
单击 Edit 按钮,可以查看脚本内容:

注意:该文件是系统自动生成的。如果我们不移动它的位置,那么只要之后再次勾选 Use Memory Layout from Target Dialog,其内容就会被覆盖,我们在后续步骤中所做的修改将丢失。

为了避免这个问题,应该将它从 Out 目录移动到工程根目录下。具体步骤为:在编辑器中右键单击脚本文件名:

选择 Open Containing Folder 打开文件所在目录:

找到 Scatter Script 脚本文件后,将其拷贝到上一级目录(即工程根目录):

重新打开工程配置窗口:

确保没有选中 Use Memory Layout from Target Dialog 选项,并在 Scatter File 文本框中直接填写我们刚刚拷贝出来的脚本文件名(由于直接放在工程目录下,这里用相对路径 ./example.sct 或 example.sct 即可)。单击 OK 保存配置。
打开 example.sct 文件,在 RW_IRAM1 定义的后面追加如下代码:
ZI_RAM_UNINIT +0 UNINIT {
.ANY (.bss.noinit)
}
效果类似这样:

保存后重新编译,再次进入 Debug 模式,问题就应该解决了。
这段步骤的核心思想是:在 scatter script 中,紧跟着为 RW 和 ZI 数据定义的 execution region 之后,为 .bss.noinit 专门提供一个属性为 UNINIT 的 execution region。
如果你的工程原本就使用了自定义的 scatter script,也可以参照此方法修改。如果你的项目比较复杂,建议咨询 scatter script 的编写者。
说在后面的话
总的来说,MDK 通过 EventRecorder 为我们提供了一个通用且便捷的方式来重定向 printf。无论你使用什么调试仿真器,甚至是 ARM 的 FVP(Fixed Virtual Platform),都可以享受这一便利。
对于需要分发自己工程作为模板的开发者来说,使用该方法后将不再限制用户必须使用 J-Link 等特定工具,能提供“开袋即食”的调试体验。
最后强调一下,EventRecorder 仅在调试阶段有意义。如果我们需要在产品正常工作模式下使用 printf,还是需要在 CMSIS-Compiler -> STDOUT(API) 中勾选 Custom:

然后实现 stdout_putchar() 函数,用它来发送字符到具体的外设,例如 USART:
int stdout_putchar(int ch)
{
if ('\n' == ch) {
int temp = '\r';
while(Driver_USART0.Send(&temp, 1) != ARM_DRIVER_OK);
}
if (Driver_USART0.Send(&ch, 1) == ARM_DRIVER_OK) {
return ch;
}
return -1;
}
希望这篇关于在 MDK 中利用 EventRecorder 实现高效 printf 调试的教程对你有所帮助。如果你在实践中遇到了其他嵌入式和底层开发相关的问题,欢迎到 云栈社区 的 C/C++ 或 计算机基础 板块与大家交流探讨。