本篇将介绍mdk下99%用户都不知道的万能printf方法。
一、说在前面的话
你听说过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完成部署
除了简单的初始化函数外,无需手动插入代码
可以将你的printf输出直接打印在mdk的debug (printf) view窗口中
你是否心动了呢?
二、部署从未如此简单
2.1rte配置
依次通过菜单 project->manage->run-time environment 打开rte配置窗口:
找到并展开compiler选项卡,勾选event recorder,并确保variant下拉列表选中的是默认的dap。
展开 compiler 下的 i/o ,勾选stdout,并在 variant 下拉列表中选择 evr——这里 evr 是 event recorder 的缩写。单击确定后,我们会在工程管理器中看到以下的内容:
至此,所需的工具都已经成功地加入到工程中了。
虽然这里eventrecorderconf.h 是一个可以编辑的状态,但实践中,我们基本不用去碰他——使用默认配置即可。
2.2服务初始化
在包含 main() 函数的c代码文件中,按照如下的格式添加对头文件的包含:
#include #if defined(rte_compiler_eventrecorder)# include #endif
在 main() 函数中添加对eventrecorder服务的初始化:
void main(void){ ...#if defined(rte_compiler_eventrecorder) && defined(rte_compiler_io_stdout_evr) eventrecorderinitialize(0, 1);#endif ...}
如果你从未使用过eventrecorder也不必惊慌,这段代码的主要作用是为printf专门开启一个数据通道。
理论上,到这里,我们就已经完成了部署,可以在进入调试模式后,通过mdk的 debug (printf) view窗口来观察 printf 的输出结果了。比如,我们在 main() 函数中打印一个 hello world :
#include #include #if defined(rte_compiler_eventrecorder)# include #endifvoid main(void){ ...#if defined(rte_compiler_eventrecorder) && defined(rte_compiler_io_stdout_evr) eventrecorderinitialize(0, 1);#endif ... printf(hello world); ...}
编译,一切顺利的话,进入调试模式后通过菜单 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 内紧接着为 rw和zi的 execution region为 .bss.noinit 提供一个属性为uninit的专属execution region。
在领会精神的情况下,如果你的工程原本就使用了scatter script也可以如法炮制。俗话说解铃还须系铃人,如果你还是不知道怎么处理,那么就去找 你工程中scatter script 的作者吧。
值得强调的是:如果你的mdk版本太老,为了确保最佳的用户体验,还是推荐尽快升级吧。您可以在关注【裸机思维】公众号后发送关键字【mdk】来获取其最新的网盘链接。
四、说在后面的话
总的来说,mdk 通过 eventrecorder 为我们提供了一个通用便捷的方式来重定向 printf——无论你使用什么调试仿真器,甚至是fvp,都可以享受来自“mdk”的阳光普照。
对很多有分发自己工程作为模板的小伙伴来说,使用该方法后将不再限制用户必须使用 j-link 之类的工具,而是可以放开手脚,获得了“开袋即食”的调试体验。
最后强调一下哦,eventrecorder只在调试阶段有意义,如果我们需要在产品的正常工作模式下使用 printf,还是老老实实把 compiler->io->stdout 配置为 user:
实现stdout_putchar() 函数——用它来发送字符到具体的外设吧,比如:
int stdout_putchar(int ch){ if ('' == ch) { int temp = ''; while(driver_usart0.send(&temp, 1) != arm_driver_ok); } if (driver_usart0.send(&ch, 1) == arm_driver_ok) { return ch; } return -1;}
物联网发展的范畴是怎样的
新型冠状肺炎正式进入大流行,全球系统风险将冲击存储器产业
以自主原创为魂,铸造有强大生命力的工业连接器产品
土壤墒情监测系统的工作原理是什么
半导体工艺在纳米级空间提高AI芯片性能就此展开了新一轮竞争
MDK下99%用户都不知道的万能printf方法
IT9121交流功率表,全面提升测试性能
电磁继电器的工作原理及如何进行接线和选用?
虹科资讯 | 虹科工业物联网产品荣获中国自动化产业年会用户信赖产品奖!
NVIDIA DLI教授如何构建异常检测的人工智能应用
木林森宣布与和谐明芯合并 将提高运营效率降低管理成本
部分接插件产品展示
SoC测试技术面临的挑战和发展趋势
LED显示屏和LCD显示屏有什么区别
Rust的 match 语句用法
RS Components备货Tektronix新款示波器 可提供高端测试和测量能力
华为任正非展现世界级企业如何逆转熵增 使寒冬中的生存成为可能
MP3电源适配器电路原理
一文带你彻底了解卷积神经网络
一加真无线耳机定制版正式限量开售