【更新】MDK下99%用户都不知道的万能printf方法

文摘   科技   2024-08-22 23:30   英国  

【说在前面的话】

你听说过J-LinkRTT么?官方的宣传是这样的:

简单来说,只要拥有了J-Link,你就可以享受以下的便利:
  • 无需占用USART或者USB转串口工具
    ,将
    printf
    重定位到一个由J-LINK提供的虚拟串口上;


  • 支持任何J-LINK声称支持的芯片


  • 高速通信,不影响芯片的实时响应

它的缺点也是明显的:
  • 你必须拥有一个J-Link
    ,如果你使用的是 
    CMSIS-DAP
    或者
    ST-Link
    之类的第三方调试工具,就无法享受这一福利;
  • 你必须在工程中手动插入一段代码



曾几何时,J-Link的这一福利让多少非J-Link用户羡慕嫉妒恨,看看手中的ST-LinkULINKpro和各类廉价的CMSIS-DAP板载调试器——“隔壁邻居的小孩都馋哭了”

如果我告诉你,其实MDK中内置了一种非常简单廉价的方式,可以让你实现类似的功能,并具有以下特点:
  • 支持所有的调试仿真器,哪怕自己手搓的CMSIS-DAP都行

  • MDK

    原生功能,连CMSIS-Pack都不用安装

  • 点几下鼠标就可以通过
    RTE
    完成部署
  • 除了简单的初始化函数外,
    无需手动插入代码
  • 可以将你的printf输出直接打印在MDK的Debug (printf) View窗口中


你是否心动了呢?


【部署从未如此简单】

步骤一:RTE配置

依次通过菜单 Project->Manage->Run-Time Environment 打开RTE配置窗口:

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

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

此时,只要简单的单机窗口左下角的Resolve的按钮就可以解决。

单击确定后,我们会在工程管理器中看到以下的内容:

至此,所需的工具都已经成功地加入到工程中了。

这里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

https://www.keil.arm.com/packs/cmsis-compiler-arm/
https://www.keil.arm.com/packs/cmsis-view-arm/


步骤二:服务初始化

在包含 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专门开启一个数据通道。

理论上,到这里,我们就已经完成了部署,可以在进入调试模式后,通过MDKDebug (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”,它的内容就会立即被覆盖掉——意味着我们在后续步骤中所做的改就会付诸东流。


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

选择“Open Container Folder”来打开文件所在目录:

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

重新打开工程配置窗口:

确保我们“没有”选中“Use Memory Layout from Target Dialog”选项,并在Scatter File文本框中直接填写我们刚刚拷贝出来的脚本文件名(由于我们直接放在工程目录下,因此这里直接用相对路径"./example.scat"或者"example.scat"就行)。单击OK保存配置。

打开example.sct,在 RW_IRAM1 后面追加如下的代码:

    ZI_RAM_UNINIT +0 UNINIT {        .ANY (.bss.noinit)    }

效果大约类似这样:

保存后重新编译,再次进入 Debug 模式,问题就应该解决了。

这里步骤的核心思想是在 scatter script 内紧接着为 RWZIexecution region.bss.noinit 提供一个属性为UNINIT的专属execution region

在领会精神的情况下,如果你的工程原本就使用了scatter script也可以如法炮制。俗话说解铃还须系铃人,如果你还是不知道怎么处理,那么就去找 你工程中scatter script 的作者吧。



值得强调的是:如果你的MDK版本太老,为了确保最佳的用户体验,还是推荐尽快升级吧。您可以在关注【裸机思维】公众号后发送关键字【MDK】来获取其最新的网盘链接


【说在后面的话】

总的来说,MDK 通过 EventRecorder 为我们提供了一个通用便捷的方式来重定向 printf——无论你使用什么调试仿真器,甚至是FVP,都可以享受来自“MDK”的阳光普照。
对很多有分发自己工程作为模板的小伙伴来说,使用该方法后将不再限制用户必须使用 J-Link 之类的工具,而是可以放开手脚,获得了“开袋即食”的调试体验。
最后强调一下哦,EventRecorder只在调试阶段有意义,如果我们需要在产品的正常工作模式下使用 printf,还是老老实实在 CMSIS-Compiler->STDOUT(API) 中勾选 Custom

实现 stdout_putchar() 函数——用它来发送字符到具体的外设吧,比如:
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;}


原创不易,


如果你喜欢我的思维、觉得我的文章对你有所启发,

请务必 “点赞、收藏、转发” 三连,这对我很重要!谢谢!


欢迎订阅 裸机思维


裸机思维
傻孩子图书工作室。探讨嵌入式系统开发的相关思维、方法、技巧。
 最新文章