IDE:CLion
MCU: STM32F407VET6
工具:OpenOCD Telnet
一、简介
调试单片机时,如果要打印数据往往需要另接一根线通过USB转TTL接到电脑上。但这样做往往并不方便,尤其是身边没有USB转TTL工具时。这时可以使用单片机自带的ITM单元,仅靠ST-Link仿真器即可实现串口打印,并且可以轻易把波特率设置到M以上的级别。
使用ITM进行串口打印并不是什么新方法,IAR、STM32CubeIDE等都集成了相应的调试工具窗口,或者直接使用ST-Link的客户端(可参考博客【嵌入式】CLion & CubeMX )。
本篇介绍的是如何使用 OpenOCD 配合 ITM(Instrumentation Trace Macrocell)来实现串口打印功能,充了在 CLion 开发环境中如何配置和使用 ITM 进行调试信息输出,从而弥补 CLion 在串口打印支持方面的不足。
二、准备
1,前置
首先,需要大致了解 OpenOCD 这个工具。OpenOCD 是一个开源的调试工具,主要作用是直接与硬件(如 ST-Link、J-Link 等仿真器)进行交互。我们可以用相应的命令,让OpenOCD完成对应操作,比如
复位:reset
暂停:halt
烧录:flash write_image firmware.elf
擦除:flash erase_sector 0 0 last
……
与 OpenOCD 配合使用的是 GDB(GNU 调试器),它是一个更高级的调试工具。通过 GDB,我们可以执行复杂的调试操作,例如设置断点、查看变量值、单步执行等。GDB 通过 GDB 远程协议 与 OpenOCD 通信,OpenOCD 则将这些请求转换为具体的硬件操作(如读写寄存器、访问内存等),从而实现对嵌入式系统的调试。
更为通俗的解释是:你可以把OpenOCD当成是你写的ADC驱动,你提供了如下接口:
ADC初始化:ADC1_init()
ADC启动:ADC1_start()
ADC读取:get_ADC_value()
……
而GDB可以当成是你写的业务,比如获取温度值这个操作,你需要启动ADC,读取ADC,并把ADC读取的值根据公式转为对应的温度,最终完成了这个操作。
不过本篇没有用到GDB,虽然可以使用GDB的monitor命令
除此之外,我们还会用到Telnet客户端,这个Telnet的作用就是与OpenOCD进行通信,比如向OpenOCD发送命令、OpenOCD把数据发送到Telnet。字面意义上,就是微信QQ那样的通信。不过一般这个功能需要手动打开,我们可以打开控制面板,在里面选择程序,再选择【启用或关闭Windows功能】
从中找到Telnet客户端这个功能,并启用。
2,思路
前置知识大致了解完了,现在实现的思路就很简单了。我们只需要让OpenOCD开通SWO这个端口(ITM发送数据的引脚),然后让OpenOCD接收数据就行了。
不过由于我没在CLion中找到OpenOCD的控制台,所以就使用Telnet来向OpenOCD发送这些命令,让其完成前面的的操作。那么流程如下:
- 单片机→启用ITM
- 上位机→通过Telnet向OpenOCD发送命令
三、正式开始
1,启用ITM
首先我们要对齐颗粒度,我指的是SWO的波特率。现在我们可以把波特率配置为2M,下面的函数里给了波特率的公式,CPU时钟除以对应的分频系数+1,此处为168M/(83+1)=2M。由于有些宏CMSIS头文件里不一定有,这里就定义了相关的宏
#define ITM_LAR_ACCESS_KEY 0xC5ACCE55 // ITM 解锁密钥 //#define ITM_TCR_ITMENA_Pos 0 // ITM 启用位 //#define ITM_TCR_SYNCENA_Pos 2 // 同步数据包启用位 #define ITM_TCR_TXENA_Pos 3 // TXENA 启用位 #define ITM_TER_PORTENA_Pos 0 // ITM 端口启用位 // 初始化 ITM 和 SWO void ITM_Init() { // 启用 ITM 和 TPIU DBGMCU->CR |= DBGMCU_CR_TRACE_IOEN; // 启用跟踪引脚 CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 启用 ITM 和 DWT // 配置 TPIU TPI->ACPR = 83; // 设置 SWO 波特率 = CPU 时钟 / (ACPR + 1) TPI->SPPR = 2; // 设置 SWO 协议为异步模式(UART) TPI->FFCR = 0x00; // 禁用格式化器 // 启用 ITM ITM->LAR = ITM_LAR_ACCESS_KEY; // 解锁 ITM ITM->TCR = (1 << ITM_TCR_ITMENA_Pos) | // 启用 ITM (1 << ITM_TCR_SYNCENA_Pos) | // 启用同步数据包 (1 << ITM_TCR_TXENA_Pos); // 启用 TXENA ITM->TER = (1 << ITM_TER_PORTENA_Pos); // 启用 ITM 端口 0 }
在初始化ITM之后,就可以直接调用ITM_SendChar()来发送数据了,不过此时往往会重定向printf
如果是GCC工具链,那么需要重写_write函数
// 重写 _write 函数 int _write(int file, char *ptr, int len) { for (int i = 0; i < len; i++) { ITM_SendChar(*ptr++); // 使用 ITM_SendChar 发送字符 } return len; }
如果是Keil的工具链,那么就需要重写fputc函数
// 重写 fputc 函数 int fputc(int ch, FILE *f) { ITM_SendChar(ch); // 使用 ITM_SendChar 发送字符 return ch; }
如果是IAR工具链,那么也需要重定向到_write函数
// 重写 __write 函数 size_t __write(int handle, const unsigned char *buffer, size_t size) { for (size_t i = 0; i < size; i++) { ITM_SendChar(buffer[i]); // 使用 ITM_SendChar 发送字符 } return size; }
一般可以使用宏来自动重定向,不过要注意,使用printf时如果不加换行,那么默认会积攒到1024个后,才会把缓冲区里的数据一次性发送过去。
// 判断编译器并定义对应的重定向函数 #if defined(__GNUC__) // GCC 工具链 #include <unistd.h> // 提供 _write 函数的声明 int _write(int file, char *ptr, int len) { __BKPT(0); for (int i = 0; i < len; ++i) { ITM_SendChar(ptr[i]); // 使用 ITM_SendChar 发送字符 } return len; } #elif defined(__CC_ARM) // Keil (ARMCC) 工具链 int fputc(int ch, FILE *f) { ITM_SendChar(ch); // 使用 ITM_SendChar 发送字符 return ch; } #elif defined(__IAR_SYSTEMS_ICC__) // IAR 工具链 #include <LowLevelIOInterface.h> // IAR 提供的低级 I/O 接口头文件 size_t __write(int handle, const unsigned char *buffer, size_t size) { for (size_t i = 0; i < size; i++) { ITM_SendChar(buffer[i]); // 使用 ITM_SendChar 发送字符 } return size; } #else #error "Unsupported compiler!" // 不支持其他编译器 #endif
2,启用OpenOCD
首先我们先进入调试状态,下面代码设置了三个按键,分别发送A、C、E
然后打开终端
OpenOCD默认监听的Telnet端口为4444,所以这时候我们使用Telnet连接到远程端口4444。连接成功后,会有下面提示
telnet localhost 4444
接下来就可以向OpenOCD输入命令了,这里我们先输入下面tpiu命令,这句的命令是把输出连接到端口3444。因为找不到OpenOCD的控制台,那么只好让OpenOCD把它接收的数据放到另一个端口,比如3444,而端口4444就作为向OpenOCD发送命令的端口即可。
tpiu config internal :3444 uart off 168000000 2000000
此外,还需要打开ITM的端口(这个是引脚端口),一般默认是端口0,这个命令没有任何提示
itm port 0 on
接着,我们再打开一个终端,使用Telnet连接到端口3444。等待一会就能连接上了
Telnet localhost 3444
3,开始测试
能用到的命令都在下面
telnet localhost 4444 tpiu config internal :3444 uart off 168000000 2000000 tpiu config internal swo.log uart off 168000000 2000000 itm port 0 on
最后,可以通过按下按键或者你可以加个延时放进主循环里,使用ITM发送数据,可以看到下图的打印结果。
回头看命令窗口,可以看到这两个窗口已经勾搭上了(使用Telnet连接端口3444时就已经握手了)
接下来是使用printf来打印大量数据,这里测试用的是发送CPU温度
.
不过结果有些感人,发送字符串时呈阶梯状,调试时观察发送的数据长度没有问题,应该是终端窗口解析数据时没有回车导致的。这个与Windows有关,Linux/Unix的换行是\n
把换行换成符合Windows标准的形式\r\n后就可以正常显示了,看起来效果还不错
printf("%f\r\n", get_adc1_temperature());
4,输出到文件中
前面介绍的方法是实时串口打印,现在提供另一种方法,把打印的数据保存到文件中。不过需要注意的是保存的文件类型是二进制,使用记事本打开会看到有很多“口”,可以使用python脚本来处理。
操作与前面类似,只不过现在要输入下面命令,这段命令会把数据输出到swo.log文件中
tpiu config internal swo.log uart off 168000000 2000000
如果直接输入命令,会有下面提示,因为tpiu已经启用了,不能再重新开始
此时要么使用关闭命令,要么关闭调试再重新打开。
为了方便演示,这里就重新打开了
然后开启ITM端口
接着,使用单片机发送数据。等一会,我们就能看到工程目录下会有一个新文件
打开后就能看到里面的内容,这些奇怪的SOH就是前面在终端看到的“空格”
此时如果选择删除swo.log文件,那么就会提示该文件正在被OpenOCD使用,这是合乎情理的
四、Tips
不过使用终端来显示这个串口打印结果有些不方便,如果后面有时间的话,我应该会开发一个插件或者一个桌面程序,既能显示打印结果,也能把接收到的数据显示在图表上。我个人更倾向于开发桌面应用程序,不过估计能鸽很久很久
脚本试了好几种,包括在OpenOCD的配置文件里添加那两个命令,但没啥用,最后发现还得是GDB,简单好用。
先在项目根目录创建一个后缀名为gdb的文件,比如tpiu_itm_commands.gdb。monitor命令相当于传参,这里把后面的命令传递给OpenOCD了
monitor tpiu config internal :3444 uart off 168000000 2000000 monitor itm port 0 on
接着进入调试状态,进入这个GDB控制台
然后按暂停,不然无法在GDB控制台里输入命令
输入source命令,后面写tp字母,再按一下Tab键,脚本名称就自动补全了,如果没补全就再多输入几个字母
回车执行会闪一下,然后没有任何提示
接着全速运行,再开一个终端窗口,连接端口3444
Telnet localhost 3444
此时就能接收到数据了。
相较于之前只少了使用Telnet这个步骤,其实还可以更进一步,直接打开终端。不过这个终端并非是CLion里的,而是新的终端窗口,黑乎乎的那种
monitor tpiu config internal :3444 uart off 168000000 2000000 monitor itm port 0 on shell start cmd.exe /k telnet localhost 3444
丑是丑了一点,但只有执行GDB脚本这一个步骤,比较适合两块电脑屏幕去看。话说cmd这个窗口,接收一段时间数据后,变得有些搞笑