前言 在学习嵌入式开发以及参与RoboMaster的过程中,调试一直是一件繁琐而又不得不去做的事。
初学的时候,我们学习过UART串口,但在实际使用中,串口的速率实在捉襟见肘,并且在发送过快时,前面的数据尚未发出,后面的数据又接踵而至,不仅数据没发出来,少量接收到的数据也完全损坏不能使用。
除此以外,部分开发版根本没有预留用来调试的串口,在这种情况下调试更是一种折磨。此刻我们迫切地希望有工具能解决这个问题,而这个工具就是RTT(Real Time Transfer)。
RTT的介绍RTT是SEGGER公司开发的一种技术,它允许在嵌入式应用程序中进行系统监控和交互式用户输入/输出。
相对于其他方案,RTT的优势很大:
无需额外的接口,仅仅使用最基础的SWDIO和SWCLK便能使用。
速度十分快,几乎没有任何性能开销,因为其工作方式就是读写内存。
双向通信,主机与MCU之间可以互相收发数据。
多通道支持
RTT的原理就是RTT库在RAM中创建了一个名为SEGGER RTT Control Block的控制块结构。在发送和读取数据时,本质上是对这块区域进行写和读。
RTT的使用RTT库的导入首先我们需要到JLink驱动的安装目录下的Samples文件夹中,在我的电脑上,这一路径为C:\Program Files\SEGGER\JLink\Samples。
在这个文件夹内部,有一个名为RTT的文件夹。新版的JLink驱动中没有这个文件夹,可以卸载然后安装旧版驱动来使用。亦或者是到官方GitHub仓库 中下载使用。
RTT文件夹中有一个压缩包,解压,然后将其中的Config文件夹和RTT文件夹复制到自己的项目下,并添加到项目资源。此时便可以使用了。
RTT库的用法在需要使用到RTT的地方,导入SEGGER_RTT.h。在调用RTT的API时,其会自动检测RTT是否初始化并进行初始化,所以不用显性的调用SEGGER_RTT_Init函数进行初始化。
发送 RTT一共有三种向终端发送数据的方式,分别为SEGGER_RTT_Write、SEGGER_RTT_WriteString、SEGGER_RTT_printf。
说是发送其实并不准确,实际上是将数据写入到上行缓冲区。
如同函数名,这三者分别为将字节、字符串、以及格式化字符串写入到缓冲区。示例代码如下:
1 2 3 4 uint8_t bytes[] = "Hello, World from SEGGER_RTT_Write!\n" ; SEGGER_RTT_Write(0 , bytes, strlen (bytes)); SEGGER_RTT_WriteString(0 , "Hello, World from SEGGER_RTT_WriteString!\n" ); SEGGER_RTT_printf(0 , "Hello, World from SEGGER_RTT_printf on %d/%d/%d!\n" , year, month, day);
将数据写入到缓冲区后,主机会读取缓冲区数据。
添加SEGGER_RTT_printf对浮点数的输出支持 RTT库提供的SEGGER_RTT_printf函数默认并不支持浮点数,我们可以手动为其加上相关支持。
我们在库文件中找到SEGGER_RTT_printf.c文件。在其中的SEGGER_RTT_vprintf函数中的switch-case语句块的末尾,加上如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 case 'f' :case 'F' :{ float fv; fv = (float )va_arg(*pParamList, double ); if (fv < 0 ) _StoreChar(&BufferDesc, '-' ); v = abs ((int )fv); _PrintInt(&BufferDesc, v, 10u , NumDigits, FieldWidth, FormatFlags); _StoreChar(&BufferDesc, '.' ); v = abs ((int )(fv * 1000 )); v = v % 1000 ; _PrintInt(&BufferDesc, v, 10u , 3 , FieldWidth, FormatFlags); break ; }
不过,除了资源紧张以外,建议直接使用printf函数转发RTT输出。
printf转发RTT在JLink驱动下的RTT文件夹内,还有一个名为Syscalls的文件夹。其中有四个文件,需要根据自身编译器类型选择合适的文件。
以GCC编译器为例,我们选择SEGGER_RTT_Syscalls_GCC.c文件复制到项目中合适路径下,并添加为项目资源。
当然,如果观察文件中内容就会发现,四个文件会根据编译环境自行决定是否被编译,也就是说,为了省事直接把整个文件夹直接加入到项目中也可以使用。
然后在文件中调用printf函数并编译,我们就能观察到效果了。注意printf的内容后面一定要加上\n,防止内容卡在缓冲区无法输出。
此时我们会发现,printf的大部分功能我们都能使用,但还是不能输出浮点数。这一点我们只需要在编译选项后面加上
即可。
带有样式的字符串输出 在SEGGER_RTT.h的文件末尾,可以看到许多基于ANSI的转义代码的宏。
要输出样式,我们可以直接用ANSI转义代码实现,也可以用预先设置好的宏。
1 printf (RTT_CTRL_TEXT_RED RTT_CTRL_BG_YELLOW "Text color red, background blue\n" );
接收 接收和发送类似,也存在一个下行缓冲区。与接收相关的函数分别为SEGGER_RTT_Read()、SEGGER_RTT_HasKey()、SEGGER_RTT_GetKey()。
如同函数名,其作用分别为将下行缓冲区的数据读入、检测下行缓冲区是否有数据以及获取其中的一位数据。 可以从下面的例子中了解其用法。
1 2 3 4 5 6 char acIn[4 ];uint8_t NumBytes = sizeof (acIn); NumBytes = SEGGER_RTT_Read(0 , &acIn[0 ], NumBytes); if (NumBytes) { AnalyzeInput(acIn); }
1 2 3 if (SEGGER_RTT_HasKey()) { int c = SEGGER_RTT_GetKey(); }
一个完整的接收示例如下:
首先定义缓冲数组。
1 char RTT_BUFFER_DOWN[BUFFER_SIZE_DOWN];
然后声明并实现接收函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void RTT_ProcessDown () ;void RTT_ProcessDown () { uint8_t NumBytes = BUFFER_SIZE_DOWN; NumBytes = SEGGER_RTT_Read(0 , RTT_BUFFER_DOWN, NumBytes); if (NumBytes) { RTT_BUFFER_DOWN[NumBytes] = '\0' ; printf (RTT_CTRL_RESET "%d\n" , atoi(RTT_BUFFER_DOWN)); } }
最后在主循环中调用。
1 2 3 4 while (1 ) { RTT_ProcessDown(); }
终端切换 调用SEGGER_RTT_SetTerminal函数即可切换终端。
其他 实例——基于RTT的日志系统 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #ifndef __LOG_H #define __LOG_H #include <stdio.h> #include <SEGGER_RTT.h> #define LOG_LEVEL_DEBUG 0 #define LOG_LEVEL_INFO 1 #define LOG_LEVEL_WARN 2 #define LOG_LEVEL_ERROR 3 static int current_log_level = LOG_LEVEL_DEBUG;#define LOG_COLOR(level) \ (level == LOG_LEVEL_DEBUG) ? RTT_CTRL_TEXT_CYAN : \ (level == LOG_LEVEL_INFO) ? RTT_CTRL_TEXT_GREEN : \ (level == LOG_LEVEL_WARN) ? RTT_CTRL_TEXT_YELLOW : \ (level == LOG_LEVEL_ERROR) ? RTT_CTRL_TEXT_RED : RTT_CTRL_RESET #define LOG_LEVEL_NAME(level) \ (level == LOG_LEVEL_DEBUG) ? "DEBUG" : \ (level == LOG_LEVEL_INFO) ? "INFO" : \ (level == LOG_LEVEL_WARN) ? "WARN" : \ (level == LOG_LEVEL_ERROR) ? "ERROR" : "UNKNOWN" #define LOG_PRINT(level, fmt, ...) do { \ if (level >= current_log_level) { \ printf("%s[%s] " fmt "%s\n" , \ LOG_COLOR(level), \ LOG_LEVEL_NAME(level), \ ##__VA_ARGS__, \ RTT_CTRL_RESET); \ } \ } while(0) #define LOG_DEBUG(fmt, ...) LOG_PRINT(LOG_LEVEL_DEBUG, fmt, ##__VA_ARGS__) #define LOG_INFO(fmt, ...) LOG_PRINT(LOG_LEVEL_INFO, fmt, ##__VA_ARGS__) #define LOG_WARN(fmt, ...) LOG_PRINT(LOG_LEVEL_WARN, fmt, ##__VA_ARGS__) #define LOG_ERROR(fmt, ...) LOG_PRINT(LOG_LEVEL_ERROR, fmt, ##__VA_ARGS__) #endif