J-Link RTT 移植和运行机制详解

2024-01-08 19:36:30

问题背景

什么情况下需要用到J-Link RTT打印输出调试信息?主要是当你的项目硬件设计当前没有引出debug用途的串口或是没有多余的引脚可以用于串口,但引出了SWD烧录口,并且需要输出一些log进行调试验证等,这时我们可以通过SWD口,用Jlink工具读取log,具体过程参考下文进行操作即可。

如果能用串口输出,笔者个人认为还是串口输出log好用一点,主要是因为Jlink输出需要用到一个配套软件JLinkRTTViewer,这个软件说实话有一些缺陷。比如最基本的时间戳功能就没有,无法判断log的时间间隔,除非你在程序里用RTC时间打印出来。

简介

**RTT全称是Real Time Transmit(实时传输)**是Segger公司推出的调试手段之一。它是一种用于嵌入式中与用户进行交互的技术。

使用RTT可以从MCU快速输出调试信息和数据,且不影响MCU的实时性。只要支持J-Link的MCU就可使用RTT功能,兼容性非常强。

RTT支持两个方向的多个通道,上到主机,下到目标,它可以用于不同的目的,为用户提供尽可能多的自由。默认实现每个方向使用一个通道,用户可在在调试终端输入和输出。

使用J-Link RTT Viewer,可用于“虚拟”终端,允许打印到多个窗口(例如,一个用于标准输出,一个对于错误输出,一个用于调试输出)。

工作原理

一句话简述: 固件代码将要输出的log数据按照RTT的格式写到确定地址的内存中去,然后RTT通过SWD口读取对应内存地址的数据,并显示到PC终端上。

RTT在MCU的存储器中使用Segger RTT控制块结构管理数据的读写操作。控制块对于每个可用的信道都在内存中包含了一个ID,用于描述通道缓冲区及其状态,通过J-Link或者环形缓冲结构区(链表)都可以通过ID找到对应的控制块。

可用信道的最大数目可以在编译时配置,并且每个缓冲区都可以在MCU运行时配置和使用。上下缓冲区可以分开处理。每个通道都可以配置为阻塞或非阻塞。

阻塞模式下,应用程序将等待缓冲区写满,直到可以写入所有内存为止,这将导致应用程序处于阻塞状态,但可以防止数据丢失。

非阻塞模式下,只会写入适合缓冲区的数据,或完全不写入缓冲区,其余的数据将被丢弃。这样即使没有连接调试器,也可以实时运行。开发人员不必创建特殊的调试版本,并且代码可以保留在发布应用程序中。

在程序中有共定义了3种模式,非阻塞模式细分了两种情况(缓冲区满了后是否丢弃数据)。

#define SEGGER_RTT_MODE_NO_BLOCK_SKIP         (0)     // Skip. Do not block, output nothing. (Default)
#define SEGGER_RTT_MODE_NO_BLOCK_TRIM         (1)     // Trim: Do not block, output as much as fits.
#define SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL    (2)     // Block: Wait until there is space in the buffer.

当 RTT处于活动状态时,无论是通过 RTT Viewer 等应用程序直接使用 RTT,还是通过 Telnet 连接到使用 J-Link 的应用程序(如调试器),J-Link 都会在目标的已知 RAM区域中自动搜索 Segger RTT 控制块。RAM区域或控制块的特定地址也可以通过主机应用程序设置以加快检测速度,否则无法自动找到控制块。
即:Segger RTT 控制块的地址可以自动搜索,也可以通过主机应用程序设置

下图显示了Segger RTT 控制块的内部结构:
在这里插入图片描述
截取程序中对SEGGER_RTT_CB控制块的定义加深理解:

//
// Description for a circular buffer (also called "ring buffer")
// which is used as up-buffer (T->H)
//
typedef struct {
  const     char*    sName;         // Optional name. Standard names so far are: "Terminal", "SysView", "J-Scope_t4i4"
            char*    pBuffer;       // Pointer to start of buffer
            unsigned SizeOfBuffer;  // Buffer size in bytes. Note that one byte is lost, as this implementation does not fill up the buffer in order to avoid the problem of being unable to distinguish between full and empty.
            unsigned WrOff;         // Position of next item to be written by either target.
  volatile  unsigned RdOff;         // Position of next item to be read by host. Must be volatile since it may be modified by host.
            unsigned Flags;         // Contains configuration flags
} SEGGER_RTT_BUFFER_UP;

//
// Description for a circular buffer (also called "ring buffer")
// which is used as down-buffer (H->T)
//
typedef struct {
  const     char*    sName;         // Optional name. Standard names so far are: "Terminal", "SysView", "J-Scope_t4i4"
            char*    pBuffer;       // Pointer to start of buffer
            unsigned SizeOfBuffer;  // Buffer size in bytes. Note that one byte is lost, as this implementation does not fill up the buffer in order to avoid the problem of being unable to distinguish between full and empty.
  volatile  unsigned WrOff;         // Position of next item to be written by host. Must be volatile since it may be modified by host.
            unsigned RdOff;         // Position of next item to be read by target (down-buffer).
            unsigned Flags;         // Contains configuration flags
} SEGGER_RTT_BUFFER_DOWN;

//
// RTT control block which describes the number of buffers available
// as well as the configuration for each buffer
//
//
typedef struct {
  char                    acID[16];                                 // Initialized to "SEGGER RTT"
  int                     MaxNumUpBuffers;                          // Initialized to SEGGER_RTT_MAX_NUM_UP_BUFFERS (type. 2)
  int                     MaxNumDownBuffers;                        // Initialized to SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (type. 2)
  SEGGER_RTT_BUFFER_UP    aUp[SEGGER_RTT_MAX_NUM_UP_BUFFERS];       // Up buffers, transferring information up from target via debug probe to host
  SEGGER_RTT_BUFFER_DOWN  aDown[SEGGER_RTT_MAX_NUM_DOWN_BUFFERS];   // Down buffers, transferring information down from host via debug probe to target
} SEGGER_RTT_CB;

移植RTT代码

1.安装Jlink软件包,相关的下载以及安装方式就不做过多的介绍。

2.在安装完成J-Link全家桶以后,在电脑安装路径下的C:\Program Files (x86)\SEGGER\JLink\Samples\RTT文件夹下面存放的就是RTT_Viewer的源代码。将此文件夹下面的SEGGER_RTT_Vxxxx.zip文件解压。(有时候也可能在C:\Program Files,具体情况可以自己去找找)

得到如下文件:
在这里插入图片描述
其中RTT文件夹下面存放的即为我们需要的源码,将RTT整个文件夹拷贝到我们的工程路径下面。
在这里插入图片描述

3.在工程中创建一个RTT的分组,添加两个源文件SEGGER_RTT.c和SEGGER_RTT_printf.c
在这里插入图片描述

4.将工程目录中的RTT文件夹包含进来:
在这里插入图片描述

5.在用到了RTT函数的文件添加头文件:
#include “SEGGER_RTT.h”

6.在main函数添加SEGGER_RTT_printf(0, “hello world\r\n”);
如果打印某个变量:SEGGER_RTT_printf(0, “n=%d\r\n”, n);
第一个参数0表示终端号。
编译成功后烧录。

连接RTT Viewer

在文件路径C:\Program Files (x86)\SEGGER\JLink找到JLinkRTTViewer.exe打开
(也可能在C:\Program Files,具体情况可以自己去找找)
在这里插入图片描述
注意要先用Jlink连接MCU,打开File->Connect
在这里插入图片描述
选择对应的MCU型号,RTT控制块默认选择自动检测即可
在这里插入图片描述
如果没有对应型号选择,可以选择对应内核,然后手动填写控制块的起始地址
(为什么只有选择型号才能自动检测,因为选择型号就等于选择了芯片RAM的大小和起始地址,RTT Viewer就能在RAM区域自动搜索RTT控制块)
在这里插入图片描述
打开map文件,找到如下字段,该地址就是RTT控制块的地址,填入上面的地址栏中。
在这里插入图片描述
在这里插入图片描述
连接成功后:
在这里插入图片描述
这里要注意RTT与串口的不同,前面已经了解到RTT是MCU先把log缓存在一段RAM区,然后当我们用RTT Viewer连接上时,才会一次性读出来全部的数据,因为必须MCU先上电才能用jlink连接上,如果有一些上电立即打印的log也不会丢失,当连接后还是能读取到。
一般我们用串口打印都是先打开串口助手,然后MCU上电才能抓取上电log。

其他用法了解

上面是常规应用,基本可以满足日常使用了。
下面讲的是一些深入技巧,没有兴趣的可以到此为止了。

1、控制台发送指令到MCU

用SEGGER_RTT_GetKey()函数可以获取一个输入字节,下面这种方式基本可以满足常规调试需求了(比如发1触发一个动作,发2触发一个动作)

char c;
while(1)
{
	c = SEGGER_RTT_GetKey();
	if(c == '1')
	{
	
	}
	else if(c == '2')
	{
	
	}
}

如果想识别一串指令比如“AT+LED=1”,那么就做个接收缓冲区接收SEGGER_RTT_GetKey()获取的单个字节,组合后就能判断长串指令(就跟串口的处理很像了),这里不做示例。

发送数据在这里:
在这里插入图片描述
注意Input有个发送的选项,Send on Input指的是一输入就自动发出去了,Send on Enter是输入完后点击Enter才会发出去。默认是Send on Input,想发长串指令的话这里得改成Send on Enter
在这里插入图片描述

2、输出彩色log

颜色定义:

#define RTT_CTRL_TEXT_BLACK           "\x1B[2;30m"
#define RTT_CTRL_TEXT_RED             "\x1B[2;31m"
#define RTT_CTRL_TEXT_GREEN           "\x1B[2;32m"
#define RTT_CTRL_TEXT_YELLOW          "\x1B[2;33m"
#define RTT_CTRL_TEXT_BLUE            "\x1B[2;34m"
#define RTT_CTRL_TEXT_MAGENTA         "\x1B[2;35m"
#define RTT_CTRL_TEXT_CYAN            "\x1B[2;36m"
#define RTT_CTRL_TEXT_WHITE           "\x1B[2;37m"

#define RTT_CTRL_TEXT_BRIGHT_BLACK    "\x1B[1;30m"
#define RTT_CTRL_TEXT_BRIGHT_RED      "\x1B[1;31m"
#define RTT_CTRL_TEXT_BRIGHT_GREEN    "\x1B[1;32m"
#define RTT_CTRL_TEXT_BRIGHT_YELLOW   "\x1B[1;33m"
#define RTT_CTRL_TEXT_BRIGHT_BLUE     "\x1B[1;34m"
#define RTT_CTRL_TEXT_BRIGHT_MAGENTA  "\x1B[1;35m"
#define RTT_CTRL_TEXT_BRIGHT_CYAN     "\x1B[1;36m"
#define RTT_CTRL_TEXT_BRIGHT_WHITE    "\x1B[1;37m"

在这里插入图片描述
只需在要打印的数据前加一个颜色就行了,比如:

SEGGER_RTT_printf(0, RTT_CTRL_TEXT_GREEN"hello world RTT\r\n");

SEGGER_RTT_printf(0, RTT_CTRL_TEXT_RED"hello world RTT\r\n");

我们看到其实原理就是在数据前加一个特殊指示颜色的字段,RTT Viewer软件能识别这个字段显示不同的颜色。
(但是RTT Viewer软件会记忆这个颜色,当你没有加这个颜色字段,也会显示最后一个文字颜色)

3、打印浮点数

SEGGER_RTT_printf函数不支持打印浮点数,想打印浮点数需要对其改造一下。
编写如下函数:

int rtt_printf(const char *fmt,...) 
{
  int     n;
  char    aBuffer[256]; //根据应用需求调整大小
  va_list args;

  va_start (args, fmt);
  n = vsnprintf(aBuffer, sizeof(aBuffer), fmt, args);
  if (n > (int)sizeof(aBuffer)) {
    SEGGER_RTT_Write(0, aBuffer, sizeof(aBuffer));
  } else if (n > 0) {
    SEGGER_RTT_Write(0, aBuffer, n);
  }
  va_end(args);
  return n;
}

测试:

rtt_printf("flaot=%f\r\n",0.988787);

在这里插入图片描述

https://zhuanlan.zhihu.com/p/466586992?utm_id=0&wd=&eqid=b3adb913000055080000000664659b26 【优质】

https://blog.csdn.net/xingqingly/article/details/132268753

https://aijishu.com/a/1060000000195415

文章来源:https://blog.csdn.net/weixin_44788542/article/details/134730536
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。