在keil利用指令跟踪宏单元(ITM)重定向printf,并完成scanf实现数据双向交互
文章目录
声明:由于STM32有很好的配套软件,如cubeMX,因此软件替我们做了很多事,这将导致不能对ITM进行一个全面的了解,当我们需要移植到其它ARM上难以成功,因此本文选取了GD32来实现ITM,完整的展现配置的原理和过程,同时对JLINK和ST-LINK的不同配置方法进行叙述,由于能力有限,难免会有理解的错误,若发现错误请留言,让我在分享中得到收获,谢谢。
1. 开发环境
- 系统:win10
- IDE:keil5
- 开发板:GD32F450
- 调试器:J-LINK V10和st-link v2两种方案分别实现
2. ITM简介和硬件需求
平时调试代码的时候大家都喜欢用printf函数来输出一些打印信息,来提示自己代码的执行情况,而最常用的方法就是将printf映射到串口等外设资源上,可是当串口被占用的时候,就显得无能为例了,本文通过GD32来介绍通过调试口,只需多利用ARM芯片的一个引脚,借助仿真器,不使用其他任何芯片外设达到printf输出的一种方法-ITM。
2.1 技术简介
ITM:Instrumentation Trace Macrocell,指令跟踪宏单元。ITM 的一个主要用途,就是支持调试消息。
在《ARM Cortex-M3与Cortex-M4权威指南》的这本书的第18章2节有对ITM的介绍和实现的完整叙述,本节总结如下:
在Cortex-M3、Cortex-M4、Cortex-M7系列MCU中,内核的调试组件有一个仪器跟踪宏单元(ITM) 。ITM是处理器中非常有用的调试特性,ITM中存在32个激励端口寄存器,而对于这些寄存器的写操作会产生通过单针串行线(SWV)接口或多针跟踪接口输出的跟踪包。利用ITM这个特性,将printf重定向到ITM,就可以通过SWV接口输出。而SWV接口输出对应的引脚就是SWO引脚。
ITM 包含 32 个刺激(Stimulus)端口,允许不同的软件把数据输出到不同的端口,从而让调试主机可以把它们的消息分离开,这样的好处是可以将printf分组,通过上位机来监听不一样的端口来实现过滤。与基于 UART 的文字输出不同,使用 ITM 输出不会对应用程序造成很大的延迟,在 ITM 内部有一个 FIFO,它使写入的输出消息得到缓冲。
为了让更多人理解ITM模块,怎样输出调试信息,这里再深入说明一下(声明:下面这部分知识和图片取自别的文章1):
- TPIU:Trace Port Interface Unit,跟踪端口接口单元。
- ITM模块属于Cortex-M内核调试组件中的一部分内容,ITM输出的消息被送往 TPIU(跟踪端口接口单元),这里的TPIU,对应SWO串行线输出。
- 这里TPIU要和上面说的【ITM 包含 32 个刺激(Stimulus)端口】区分开来。
- ITM的32个刺激(Stimulus)端口并不是要对应32个SWO引脚,32个刺激端口调试信息可通过一个SWO引脚输出,下面详细讲述。
注意:ITM是内核的功能,因此需要处理器带这个组件,比如你的芯片是 Cortex-M0或M0+的ARM内核,是无法不支持ITM,对于这种无法支持ITM的,我们有的可以采用半主机(semihosting)模式进行调试,后面有时间我会讲解半主机调试。
2.1 硬件支持
从上面我们知道,ITM的实现除了处理器支持外,对调试器也有要求。
2.1.1 首先我们知道,对于使用不一样的调试器或下载器,我们常用的ARM下载方式有三种接口:
- 串口接口,这种情况一般是开机时候通过boot引脚来控制运行芯片固化的boot loader完成串口下载。只能用来下载,不能debug。因为这个比较简单,本文不讨论。
- JTAG接口,可以用来下载和调试,但调试速度和效果不如SW,因为JTAG的最牛功能是借助BSDL来实现边界扫描。
- SW接口,可以用来下载和调试,ITM的实现必须借助SW接口。
2.1.2 调试接口大概分为三种,如图:
虽然我们买ARM板上面的调试接口可能五花八门,但是本质上都是从这3种接口删删减减演化而来的,下面我们说的是最标准的接口:
-
最常用的20pin调试接口:
-
不常见的10pin调试接口:
-
几乎不用的一种调试接口:
有关接口的详细说明请参考ARM KEIL官网介绍;图上黄色和黑色的字表示不同协议下载方式的不同名字和功能。因为SW接口和JTAG接口是复用的,所以可以将sw调试器直接插在jtag接口上使用,上图中黄色字体代表这个引脚在SW接口中的含义。
通过上面我们知道,这些标准接口有些引脚是重复的(如GND),有些引脚平时debug又用不着,因此为了节约pcb空间和提高引脚利用率,我们往往都是设计简化的下载接口。
2.1.3 简化的下载接口引脚的说明
-
JTAG 调试提供五个引脚的接口:JTAG 时钟引脚(JTCK),JTAG 模式选择引脚(JTMS),JTAG 数据输入引脚(JTDI),JTAG 数据输出引脚(JTDO),JTAG 复位引脚(NJTRST,低电平有效)。
-
串行调试(SWD)提供两个引脚的接口:数据输入输出引脚(SWDIO)和时钟引脚(SWCLK),两个引脚与 JTAG 调试接口的两个引脚复用,SWDIO 和 JTMS复用,SWCLK 和 JTCK 复用。
-
当异步跟踪功能开启时,JTDO 引脚也用作异步跟踪数据输出(TRACESWO)。
名词解释:
SWD:Serial Wire Debug,串行线调试.
SWO:Serial Wire Output,串行线输出.
SWV:Serial Wire Viewer,串行线查看器.
SW是Serial Wire,是一种下载接口。SWD全称Serial Wire Debug,是利用SW接口调试的一种方式.
GD32F450调试接口对应引脚如图:
2.1.3 ITM需要多接一根线
若想使用ITM,调试方式必须设置为SWD方式
和传统的SWD下载相比,实现ITM必须在多接一根线,如下图1:
刚才我们有说ITM包含了32个端口,它们就是通过SWO引脚,将打印的信息输出到keil,keil在通过选择监听端口,我们可以直接用它来输出一些调试信息。
SWO引脚可以类比为UART的Tx引脚,如果不连接此引脚,则(SWV)终端不会接收打印信息。
针对STM32使用cubeMAX配置,网上有很多,如参考strongerHuang的文章,写的很好:文章地址;我就不在叙述了。
本文介绍的是更为通用的方法,使用GD32F450进行介绍,因为JLINK和stlink配置方法是不同的,最后我会详细讲解两者区别。
3. 用ST-LINK v2来实现ITM
因为GD32可以在keil上来使用ST-LINK进行下载和调试,配置比较简单,那我们由浅入深,先介绍ST-LINK来实现ITM。后面我们在介绍较难的jlink配置实现ITM。
3.1 KEIL中配置
本人使用的是st官方发行的STLINK V2,接好GD32开发板,将ST-LINK插入电脑上,在keil配置如下:
不同的调试器这个界面略有区别,这个是st-link的(后面有讲jlink如何配置),时钟要设置的和芯片core时钟一致(GD32F450内核时钟为200M)。
开启Trace,勾选Autodetect。
端口选择需要和ITM_SendChar
函数选择的一致,这里选择端口0,下面程序中关于端口设置会详细介绍。
3.2 mcu内部程序printf重定向-使用标准c库
printf的重定向有使用标准ARM C库和keil的MicroLIB库两种方案,本章介绍较为复杂的标准ARM C库方法,这两种库的区别请参考我的另一篇文章,对照那篇文章很容易将标准ARM C库方案改为keil的MicroLIB库方案。
//在KEIL MDK 用的重定向函数
#include <stdio.h>
#include "gd32f4xx.h"
#pragma import(__use_no_semihosting_swi) //确保没有从 C 库链接使用半主机的函数
//因为禁止了半主机模式,需要重写一个半主机模式下的接口,如下
int _ttywrch(int ch)
{
ch=ch;
return ch;
}
//标准库需要的支持函数
struct __FILE
{
int handle; /* 在此处增加自己需要的内容 */
};
FILE __stdout;
FILE __stdin;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
lable: goto lable; /* 死循环 */
}
int fputc(int ch, FILE *f){
//调取core_cm4中的函数
return (ITM_SendChar(ch));
}
这个ITM_SendChar
函数来自core_cm4.h
,因为这个文件来自ARM,因此不管您用的谁家的处理器,只要是Cortex-M3、Cortex-M4、Cortex-M7之一,那就一定能在对应的core_cmX.h中找到这些函数,如图:
通过上图我们知道,ITM不止支持发送,还支持接收,通过接收来实现scanf的设置我们在后面讲,现在先配置发送。
我们已经知道ITM有32个刺激(Stimulus)端口,printf可以映射到任何端口,在通过keil中的配置,就可以实现不同端口的printf,若要映射到为其他端口,参考下图配置为映射到端口1:
那么你有没有这样的想法,那就是通过修改ITM_SendChar函数,来实现多个端口的printf端口,这时候上位机就可以订阅他感兴趣的端口输出,可以实现printf分组。请各位自己去验证吧。
下面就可以直接在main函数里面写printf就可以输出了:
int main(void)
{
while(1){
printf("\nITM test out\n");
delay_1ms(400);
}
}
注意:根据mcu芯片用户手册可知,芯片上电后调试引脚默认就是调试功能,所以不需要在程序里面初始化SWO引脚,这一就是为什么没有在main中初始化引脚的原因,
3.3 通过KEIL的Debug(printf)Viewer查看打印信息
确定链接调试器并接上mcu后,就可以进行debug调试了:点击:这个调试按钮,进入调试界面,然后点击
View->Serial Windows->Debug(printf)Viewer
就会显示printf打印的信息了,如图:
4. 利用JLINK调试器配置ITM输出
当你仿照ST-LINK配置的时候,用JLINK进行调试会发现不行,具体的现象是调用printf会在fputc调取ITM_SendChar(ch)卡住,这是因为JLINK还需要添加额外的配置。下面我们详细叙述。
4.1 KEIL 配置
先插上JLINK。配置主要就是使能跟踪Trace,配置CPU时钟,以及ITM端口,和使用stlink类似配置。
同样端口选择需要和ITM_SendChar
函数选择的一致,不同的调试器这个界面略有区别,这个是jlink的,设置时钟为core时钟(GD32F450内核时钟为200M),几乎跟用st-link的时候没有区别,勾上Autodetect选项。
4.2 mcu程序
和使用ST-link调试一样,mcu内部程序不变,上面有,我就不再写一次了。
4.3 jlink不能像stlink那样调试成功的缘由
通过上面的步骤使用jlink调试,你会发现不能用,printf会在ITM_SendChar里面卡主,我开始就在这卡了很久,后来查芯片数据手册发现如下:
手册里面说若要使用跟踪引脚,需要使能,需要设置这个寄存器0xE0042004U
的bit5为1,那下面我们就配置一下这个寄存器吧。(后面我会解释为啥前面用ST-LINK不需要设置这个)。
4.4 用jlink来配置mcu寄存器0xE0042004U
的bit5为1
首先新建一个文件,叫XXX.ini,本文就起名为JLINK_ITM_CONFING.ini,在这个文件中添加以下内容:
FUNC void DebugSetup (void) {
_WDWORD(0xE0042004, 0x00000020); // DBGMCU_CR
}
DebugSetup(); // Debugger Setup
然后将这个文件添加到keil中,如图:
你可能疑惑这个文件有什么用,这里解释以下,当你启动debug的时候,这个文件里面的程序将会最先执行,这里可以放一些函数,来实现一些配置和初始化,利用这个你似乎可以完成很多事情。而这个_WDWORD(0xE0042004, 0x00000020);
的意思就是通过调试接口,将mcu的0xE0042004
的寄存器地址赋值为0x00000020
,正好是bit5为1;这个时候,你的jlink就可以实现ITM功能了。
当你仔细阅读用户手册关于调试相关寄存器你会发现,除了这个bit5外,其他的bit位也很有用,比如调试的时候可以关闭看门狗,关闭定时器等。同时你可以在.ini里面设置多个寄存器,如:
FUNC void DebugSetup (void) {
_WDWORD(0xE0042004, 0x00000027); // DBGMCU_CR
_WDWORD(0xE000ED08, 0x20000000); // Setup Vector Table Offset Register
}
DebugSetup(); // Debugger Setup
是不是可玩性极强呢。
这时候你可能会思考,既然是设置mcu的寄存器,那我也可以不使用这个.ini文件,我直接在mcu的程序里面,在main函数里面添加*((int*)0xE0042004) = 0x00000020
给这个寄存器赋值不就可以了吗,当然可以,你可以去验证这个方法。
4.5 解释为什么ST-LINK不需要设置寄存器0xE0042004U
为了验证这件事我通过在程序中添加代码,打印0xE0042004里面的值,调试发现,当我使用stlink在debug的时候,默认会将bit5设置为1,而用jlink就不会,那这是不是说明stlink的固件里有程序替我们实现了那个.ini文件的功能呢。通过这件事我突然想到,我曾和人发生争执,那就是debug的时候看门狗会不会运行,当时我俩各执一词,现在想来,恐怕是我俩用了不一样的调试器和配置影响到了mcu的debug寄存器的配置,才会导致我俩看到的不一样吧。
5. KEIL 实现scanf,完成双向通信
printf完成了,要是还可以scanf,那不就实现双向通信了吗,下面我们就介绍scanf的实现。
printf是借助SWO引脚实现的,我们说过SWO是输出,是单向的。那scanf咋办,是不是要添加引脚,答案是不需要。我猜测scanf的传输是借助SWDIO引脚来实现的,因为我们现在都知道ITM的printf必须在SW调试模式下才有效,而我发现scanf不管是在JTAG还是SW模式下都可以实现。这就说明printf和scanf在实现原理可能是不同的。懒得再查资料了,就此打住,请知道的直接留言给我吧。
直接在mcu里面再添加程序如下:
volatile int32_t ITM_RxBuffer = 0x5AA55AA5; //初始化为EMPTY,在ITM_CheckChar函数内部和其他使用
int fgetc(FILE *f)
{
char tmp;
while(ITM_CheckChar() == 0);//会在这卡主,等待用户输入
tmp = ITM_ReceiveChar();
if(tmp == 13) tmp = 10;//当接收到回车键,就替换成换行键
return (ITM_SendChar(tmp));//回显
}
根据**ASCII **可以知道13是回车键CR,10是换行键LF,这样就可以实现友好的回显功能。
下面就可以直接在main函数里面写scanf和printf来实现mcu数据接收发送。例子:
int main(void)
{
char textbuffer[40];
//SCB->CCR |= SCB_CCR_STKALING_Msk;//使能双字栈对齐
printf("\nhello word!\n");
while(1){
printf("\nplease enter text:\n");
fgets(textbuffer, (sizeof(textbuffer)-1), stdin);
printf("\nyou enteren:%s\n", textbuffer);
}
}
fgets 读取用户输入(stdin)并存入textbuffer中,当然还可以用scanf来替换,scanf实现如下:
scanf("%s", &textbuffer[0]);
但是我们不推荐这样写,因为scanf这个函数不会检测缓冲溢出,也可以使用其他方法,如直接改造fgetc,来实现类似串口接收的效果,很容易,就不再讨论了。
这样就可以实现数据双向传输了,利用fgets和printf实现了发送返回,如下图:
6. 升级双向通信功能
上面的方法,有两个非常严重的问题,第一就是若想实现printf就要启动debug,但是已启动debug,程序就会复位,针对这个问题,请参考我的另一篇文章:ARM调试(3):在keil中不复位调试MCU 。
还有一个问题那就是当不在调试的时候,断电重启,printf会被程序忽略,不会有任何影响,可是接收函数fgets
函数不会被忽略,导致会在这里面卡住,卡住的原因是fgets会调用fgetc,重写的fgetc函数里面的while是等待用户输入,然而因为用户没调试就无法输入,程序就会死在这里。如下:
那么就需要一件事,那就是让输入只在调试的时候有效,不调试的时候就忽略scanf,当然我们可以添加宏来解决这件事,调试的时候打开宏编译一下下载进去,不调试的时候在关闭宏,但是这样很麻烦,还要不断的编译程序。
有一种办法可以很好的解决这件事,根据上面的知识我们知道,当启动debug的时候,mcu的0xE0042004
的寄存器地址赋值为0x00000020
,而不调试的时候,0xE0042004的值是0,那么通过判断0xE0042004
的寄存器的bit5位,就可以知道是不是在调试了.
因此可以封装一下scanf函数,下面的程序是我盲写的,没验证对错,但是我感觉差不多应该是对的,可能需要小改,请自己去实验,同时也可以根据自己的想法,改成类似串口那样的方式。
//封装fgets
char* itm_fgets(char *string,int n, FILE *stream)
{
if(*((int*)0xE0042004) == 0x00000020)
return scanf(string, n, stream);
else
return null;
}
//主函数测试
int main(void)
{
char textbuffer[40];
printf("\nhello word!\n");
while(1){
printf("\nplease enter text:\n");
if(itm_fgets(textbuffer, (sizeof(textbuffer)-1)) != null){
//判空操作,因为不调试的时候,这些变量是不会收到数据的
}
printf("\nyou enteren:%s\n", textbuffer);
}
}
7. 针对JLINK和ST-LINK仿真器实现ITM的说明
本例程都是借助keil来实现不同仿真器的ITM,这样做有一个不好之处,就是keil必须点击debug调试才可以进行交互.但其实每种仿真器都有自己的ide,利用这些ide就可以直接和mcu进行交互,这对于不需要源码的人来讲非常的方便。
jlink利用J-Link Commaner,官网下载地址,可以实现ITM调试。
stlink的STM32 ST-LINK Utility,可以实现ITM调试:
具体方法网上有很多教程,本文就不赘述了。
注意:GD32F450虽然能在keil中用st-link下载程序,但是使用不了STM32 ST-LINK Utility,根据网上的文章,stm32芯片是可以使用STM32 ST-LINK Utility进行调试的。
8 利用ITM调试实现逻辑分析仪功能
使用keil调试还有许多实用的功能,比如对全局变量进行逻辑分析等:
首先创建以后个全局变量,如:int a
;并在程序中改变它的值
开启debug,点击下图设置,调出分析仪窗口:
右击变量a,将其添加到分析仪器中。
这时候这个变量a的变化就实时绘制到这个界面中了
观察其他变量的方法怎样设置请看下面两篇文章,感觉讲的挺好的,我没仔细研究。
文章1:链接
文章2:链接