在keil利用指令跟踪宏单元(ITM)重定向printf,并完成scanf实现数据双向交互

文章目录

  • 在keil利用指令跟踪宏单元(ITM)重定向printf,并完成scanf实现数据双向交互
    • 1. 开发环境
    • 2. ITM简介和硬件需求
      • 2.1 技术简介
      • 2.1 硬件支持
        • 2.1.1 首先我们知道,对于使用不一样的调试器或下载器,我们常用的ARM下载方式有三种接口:
        • 2.1.2 调试接口大概分为三种,如图:
        • 2.1.3 简化的下载接口引脚的说明
        • 2.1.3 ITM需要多接一根线
    • 3. 用ST-LINK v2来实现ITM
      • 3.1 KEIL中配置
      • 3.2 mcu内部程序printf重定向-使用标准c库
      • 3.3 通过KEIL的Debug(printf)Viewer查看打印信息
    • 4. 利用JLINK调试器配置ITM输出
      • 4.1 KEIL 配置
      • 4.2 mcu程序
      • 4.3 jlink不能像stlink那样调试成功的缘由
      • 4.4 用jlink来配置mcu寄存器`0xE0042004U `的bit5为1
      • 4.5 解释为什么ST-LINK不需要设置寄存器`0xE0042004U `
    • 5. KEIL 实现scanf,完成双向通信
    • 6. 升级双向通信功能
    • 7. 针对JLINK和ST-LINK仿真器实现ITM的说明
    • 8 利用ITM调试实现逻辑分析仪功能
    • 其他:参考声明

声明:由于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):

  1. TPIU:Trace Port Interface Unit,跟踪端口接口单元。
  2. ITM模块属于Cortex-M内核调试组件中的一部分内容,ITM输出的消息被送往 TPIU(跟踪端口接口单元),这里的TPIU,对应SWO串行线输出。
  3. 这里TPIU要和上面说的【ITM 包含 32 个刺激(Stimulus)端口】区分开来。
  4. ITM的32个刺激(Stimulus)端口并不是要对应32个SWO引脚,32个刺激端口调试信息可通过一个SWO引脚输出,下面详细讲述。

注意:ITM是内核的功能,因此需要处理器带这个组件,比如你的芯片是 Cortex-M0或M0+的ARM内核,是无法不支持ITM,对于这种无法支持ITM的,我们有的可以采用半主机(semihosting)模式进行调试,后面有时间我会讲解半主机调试。

2.1 硬件支持

从上面我们知道,ITM的实现除了处理器支持外,对调试器也有要求。

2.1.1 首先我们知道,对于使用不一样的调试器或下载器,我们常用的ARM下载方式有三种接口:

  1. 串口接口,这种情况一般是开机时候通过boot引脚来控制运行芯片固化的boot loader完成串口下载。只能用来下载,不能debug。因为这个比较简单,本文不讨论。
  2. JTAG接口,可以用来下载和调试,但调试速度和效果不如SW,因为JTAG的最牛功能是借助BSDL来实现边界扫描。
  3. SW接口,可以用来下载和调试,ITM的实现必须借助SW接口。

2.1.2 调试接口大概分为三种,如图:

虽然我们买ARM板上面的调试接口可能五花八门,但是本质上都是从这3种接口删删减减演化而来的,下面我们说的是最标准的接口:

  1. 最常用的20pin调试接口:

  2. 不常见的10pin调试接口:

  3. 几乎不用的一种调试接口:

有关接口的详细说明请参考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);elsereturn 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:链接

其他:参考声明


  1. 本文参考了strongerHuang的文章,他的csdn账号,本文一些图也取自这篇文章,文章地址; ↩︎ ↩︎

ARM调试(2):在keil利用指令跟踪宏单元(ITM)重定向printf,并完成scanf实现数据双向交互相关推荐

  1. 最低成本的ARM调试解决方案——有关于Wiggler、H-Jtag、OpenOCD、GDB

    origin: http://blog.sina.com.cn/s/blog_70bb32080100lx1u.html 又是一个多月没有动这个Blog嘿嘿,我发现一个有趣的现象,我的Blog在每年的 ...

  2. STM32 ARM调试问题总结

    文章转载自:http://xfjane.spaces.eepw.com.cn/articles/article/item/77908 基于ADS的ARM调试有关问题总结 1.  在添加文件的过程中你可 ...

  3. 苹果m1可以虚拟服务器,苹果M1芯片可以运行ARM版win10 但需要利用虚拟机

    原标题:苹果M1芯片可以运行ARM版win10 但需要利用虚拟机 得益于ARM架构的低功耗小型化,苹果已经开发出了基于ARM版本的MacBook芯片--M1.不过有不少人喜欢将购买的MacBook 重 ...

  4. iOS开发:沙盒机制以及利用沙盒存储字符串、数组、字典等数据

    iOS开发:沙盒机制以及利用沙盒存储字符串.数组.字典等数据 1.初识沙盒:(1).存储在内存中的数据,程序关闭,内存释放,数据就会丢失,这种数据是临时的.要想数据永久保存,将数据保存成文件,存储到程 ...

  5. hive sqoop 分区导入_利用oozie,执行sqoop action将DB2中的数据导入到hive分区表中

    测试:利用oozie,执行sqoop action将DB2中的数据导入到hive分区表中. 需要注意的地方: 1,要添加hive.metastore.uris这个参数.否则无法将数据加载到hive表中 ...

  6. 利用unison+inotify 实现数据双向实时同步

    利用unison+inotify 实现数据双向实时同步 环境:Centos 6.5 64位 server1 :192.168.1.201 server2 :192.168.1.250 需求软件:oca ...

  7. python根据频率画出词云_利用pandas+python制作100G亚马逊用户评论数据词云

    原标题:利用pandas+python制作100G亚马逊用户评论数据词云 数据挖掘入门与实战 公众号: datadw 我们手里面有一个差不多100G的亚马逊用户在购买商品后留下的评论数据(数据格式为j ...

  8. keil MDK AC5向AC6迁移后如何重定向printf

    从AC5向AC6迁移,已经有很多人给出了迁移的教程,AC6的编译速度着实非常有吸引力,我也尝试根据网上的教程从AC5向AC6迁移,但程序执行到printf的时候便会有问题. 有问题的重定向代码: /* ...

  9. 如何利用python将excel表格中筛选出来的每一份数据各自另存为新的excel文件?

    如何利用python将excel表格中筛选出来的每一份数据各自另存为新的excel文件? 1.问题描述 2.解决过程 2.1 问题分析: 2.2 解决思路 3.运行结果 1.问题描述 最近在处理一堆工 ...

最新文章

  1. Windows使用MSVC,命令行编译,链接64位dll,Python调用
  2. CodeSmith生成SQL Server视图的实体类脚本/对应的生成模板
  3. python类属性初始化_Python:如何模拟类属性初始化函数
  4. phpstorm如何回滚。并取消本地提交
  5. datetime方法
  6. Pytorch:矩阵乘法总结
  7. MySQL中select * for update锁表的范围
  8. java sort 字符串_java字符串怎么排序
  9. 3D旋转相册代码及详细使用教程
  10. 使用arcgis修改行政区划图边界
  11. 组卷积和深度可分离卷积
  12. uniapp发行为小程序分享转发功能
  13. setSingleChoiceItems和setPositiveButton两者触发时期
  14. java程序设计六大原则
  15. opencv中 画六边形
  16. SQL 修改表的常用命令
  17. n*n蛇形方阵的输出
  18. 如何快速把多个excel表合并成一个excel表(不熟悉vba及公式的人)
  19. R语言丨根据VCF文件自动填充对其变异位点并生成序列fa文件
  20. 通讯录怎么恢复?在 手机上检索找回已删除的电话号码的3种方式

热门文章

  1. python读取文件,读文件的前几行
  2. python中hashlib_python的hashlib模块
  3. 预防培训班害人和架构介绍(理论)
  4. el-input添加默认前置值,不可删除,只能在默认值之后增加
  5. 市场调研报告-全球与中国渠道可视化工具市场现状及未来发展趋势
  6. python做三维图片挑战眼力_腾讯实习挑战赛30强WriteUp
  7. Linux基础教程1
  8. SATA硬盘 IDE硬盘混用设置
  9. java super关键字的作用_详解Java编程中super关键字的用法
  10. 机器学习--从K近邻算法、距离度量谈到KD树、SIFT+BBF算法