实时传输(RTT)是SEGGER基于基本的存储读写实现的目标和主机之间的双向通讯接口。该规范独立于目标架构,只要支持“后台内存访问”,也就是说当目标在运行时,可以读写内存,那么就可以使用该方式。一个通道对应一个TCP/IP连接,可以给某个通道开启多个TCP/IP服务。
当前的OpenOCD版本只支持单目标设备,不支持channel buffer flags,并且TARGET为RISCV时还需要一些修改才能支持。
其中目标到主机为up数据流。
主机到目标为down数据流。
基本命令
先介绍下基本的RTT相关的配置命令。在TARGET为RISCV时,目前的版本默认除了rtt server外的命令都不支持,需要修改源码,后面会介绍。
配置RTT
rtt setup address size [ID]
ID默认为字符串 "SEGGER RTT"。
设置之后OpenOCD就后台从address地址处的size大小范围内搜寻ID对应的控制块。
启动RTT
rtt start
如果控制块的位置未知则会搜寻。
停止RTT
rtt stop
上行查询间隔设置与打印
rtt polling_interval [interval]
设置查询上行数据的间隔为interval单位为ms,不指定interval则为打印当前值。
查看通道
rtt channels
打印所有通道和其属性。
获取通道信息
rtt channellist
和 rtt channels 一样,只是 rtt channellist 的执行结果返回一个tcl的list,可以放在[]中,脚本中其他地方使用。
启动服务
rtt server start port channel [message]
给指定通道启动一个TCP服务端,当指定message不为空时,就会将内容发送给连接到该服务端的客户端。
停止服务
rtt server stop port
停止指定通道对应的TCP服务。
OpenOCD中为RISCV添加RTT支持
在RISCV平台默认输入 rtt 看到只支持以下两个命令。
> rtt
rtt
rtt server
rtt server start <port> <channel> [message]
rtt server stop <port>
>
这是在 src\server\rtt_server.c 中实现的,只有 start 和 stop 俩个命令。
static const struct command_registration rtt_server_subcommand_handlers[] = {
{
.name = "start",
.handler = handle_rtt_start_command,
.mode = COMMAND_ANY,
.help = "Start a RTT server",
.usage = "<port> <channel> [message]"
},
{
.name = "stop",
.handler = handle_rtt_stop_command,
.mode = COMMAND_ANY,
.help = "Stop a RTT server",
.usage = "<port>"
},
COMMAND_REGISTRATION_DONE
};
而其他 setup 等命令是在 src\rtt\tcl.c 中实现的。
static const struct command_registration rtt_subcommand_handlers[] = {
{
.name = "setup",
.handler = handle_rtt_setup_command,
.mode = COMMAND_ANY,
.help = "setup RTT",
.usage = "<address> <size> [ID]"
},
{
.name = "start",
.handler = handle_rtt_start_command,
.mode = COMMAND_EXEC,
.help = "start RTT",
.usage = ""
},
{
.name = "stop",
.handler = handle_rtt_stop_command,
.mode = COMMAND_EXEC,
.help = "stop RTT",
.usage = ""
},
{
.name = "polling_interval",
.handler = handle_rtt_polling_interval_command,
.mode = COMMAND_EXEC,
.help = "show or set polling interval in ms",
.usage = "[interval]"
},
{
.name = "channels",
.handler = handle_rtt_channels_command,
.mode = COMMAND_EXEC,
.help = "list available channels",
.usage = ""
},
{
.name = "channellist",
.handler = handle_channel_list,
.mode = COMMAND_EXEC,
.help = "list available channels",
.usage = ""
},
COMMAND_REGISTRATION_DONE
};
搜索 rtt_target_command_handlers 发现它只被注册到了特定的目标类型中,例如 arc_cmd.c、cortex_m.c 和 hla_target.c。
所以我们需要修改源码,将其也注册到RISC-V的目标命令处理器中。修改位于 src\target\riscv\riscv.c 的 riscv_command_handlers 数组。
首先需要包含头文件:
#include <rtt/rtt.h>
然后在 riscv_command_handlers 数组末尾,COMMAND_REGISTRATION_DONE 之前添加:
{
.chain = rtt_target_command_handlers,
},
这样,RISC-V目标就具备了完整的RTT命令支持。
实例
TARGET移植RTT
添加源码
从以下地址下载代码:
https://github.com/SEGGERMicro/RTT
添加以下文件到自己的工程:
RTT/SEGGER_RTT.c
RTT/SEGGER_RTT.h
RTT/SEGGER_RTT_Printf.c
Syscalls/SEGGER_RTT_Syscalls_GCC.c (按照编译器选择)
Config/SEGGER_RTT_Conf.h
配置
相关配置位于 SEGGER_RTT_Conf.h 中。
LOCK相关实现
SEGGER_RTT_Conf.h 中 SEGGER_RTT_LOCK() 和 SEGGER_RTT_UNLOCK() 需要根据目标平台实现。例如对于RISC-V,可以实现为:
#define SEGGER_RTT_LOCK() { \
unsigned int _SEGGER_RTT__LockState; \
__asm volatile ("csrr %0, mstatus \n\t" \
"csrci mstatus, 8 \n\t" \
"andi %0, %0, 8 \n\t" \
: "=r" (_SEGGER_RTT__LockState) \
: \
: \
);
#define SEGGER_RTT_UNLOCK() __asm volatile ("csrr a1, mstatus \n\t" \
"or %0, %0, a1 \n\t" \
"csrs mstatus, %0 \n\t" \
: \
: "r" (_SEGGER_RTT__LockState) \
: "a1" \
); \
}
测试代码
可以参考官方示例 Main_RTT_InputEchoApp.c,添加简单的测试代码:
#include "SEGGER_RTT.h"
static char r;
SEGGER_RTT_WriteString(0, "SEGGER Real-Time-Terminal Sample\r\n");
SEGGER_RTT_ConfigUpBuffer(0, NULL, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP);
do {
r = SEGGER_RTT_WaitKey();
SEGGER_RTT_Write(0, &r, 1);
r++;
} while (1);
OpenOCD配置
首先通过telnet连接到OpenOCD服务:
telnet localhost 4444
接下来需要找到RTT控制块在内存中的地址。可以通过GDB命令查看全局变量 _SEGGER_RTT 的地址:
(gdb) p /x &_SEGGER_RTT
$1 = 0x28100bfc
或者在生成的map文件中搜索:
.bss._SEGGER_RTT
0x0000000028100bfc 0xa8 build/acore/app/libapp.a(SEGGER_RTT.o)
0x0000000028100bfc _SEGGER_RTT
为了确保RTT控制块已正确初始化,可以在程序运行到RTT初始化之后,通过GDB检查其结构体内容,确认 acID 字段为 "SEGGER RTT”。
如果执行命令时提示 [riscv.cpu0] Failed to read priv register.,则需要在halt条件下执行RTT设置命令。
在OpenOCD的telnet会话中,输入以下命令进行设置和启动:
rtt setup 0x28100bfc 1024
rtt start
成功识别到RTT控制块后,会输出类似信息:
rtt: Searching for control block 'SEGGER RTT'
rtt: Control block found at 0x28100bfc
接着,为通道0启动一个TCP服务器,监听端口19021,并发送初始消息:
rtt server start 19021 0 "Hello RTT"
现在,可以使用客户端连接了。可以使用SEGGER提供的 JLinkRTTClient.exe 工具连接 localhost:19021 端口,也可以自己实现一个TCP客户端进行数据收发。
要停止服务,使用命令:
rtt server stop 19021
OpenOCD中RTT相关的关键代码与数据流
首先,RTT相关命令的实现分为两部分:
server start/stop 的实现位于 src\server\rtt_server.c,对应 rtt_command_handlers。
- 其他RTT相关命令(如
setup, start, stop, polling_interval 等)实现位于 src/rtt/tcl.c 中,对应 rtt_target_command_handlers。
然后是搜寻控制块的实现,位于 src\target\rtt.c 的 target_rtt_find_control_block 函数。它会从 setup 设置的起始地址和大小范围内读取内存,搜寻指定的ID字符串(默认为 ”SEGGER RTT”)。
获取通道信息的逻辑在 read_rtt_channel 函数中。它是根据RTT控制块的结构体定义去解析内存布局。控制块结构体定义大致如下:
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
SEGGER_RTT_BUFFER_DOWN aDown[SEGGER_RTT_MAX_NUM_DOWN_BUFFERS]; // Down buffers
#if SEGGER_RTT__CB_PADDING
unsigned char aDummy[SEGGER_RTT__CB_PADDING];
#endif
} SEGGER_RTT_CB;
控制块开头是16字节ID和两个4字节的整数,共24字节。之后就是上行和下行通道数组,每个通道缓冲区描述符(SEGGER_RTT_BUFFER_UP/DOWN)也是24字节。只要找到这个结构体,就可以按此布局解析出所有通道信息。
写数据到通道的实现位于 write_to_channel 函数,其逻辑类似于环形缓冲区(FIFO)的写入操作。
从通道读数据的实现位于 read_from_channel 函数,其逻辑类似于环形缓冲区(FIFO)的读出操作。
整个数据链路可以分为上行和下行两部分:
下行链路 (Host -> Target)
下行链路的目的是将TCP客户端发送的数据写入到目标的down通道。
setup 命令 (handle_rtt_setup_command) 会注册写回调:source.write = &target_rtt_write_callback;。
rtt server start 命令会添加一个服务,其输入处理器 (input_handler) 被设置为 rtt_input。
- 在OpenOCD的主服务循环 (
server_loop) 中,会调用 service->input(c),即 rtt_input 函数。
rtt_input 函数从TCP连接读取数据,然后调用 rtt_write_channel 写入通道。
rtt_write_channel 最终调用之前注册的 rtt.source.write,也就是 target_rtt_write_callback。
target_rtt_write_callback 调用 write_to_channel 函数,将数据真正写入到目标内存的down通道缓冲区中。
链路总结:openocd_thread -> server_loop -> service->input (rtt_input) -> tcp connection_read/rtt_write_channel -> rtt.source.write (target_rtt_write_callback) -> write_to_channel。
上行链路 (Target -> Host)
上行链路的目的是将目标up通道中的数据读取并发送给TCP客户端。
setup 命令注册读回调:source.read = &target_rtt_read_callback;。
rtt server start 命令在客户端连接时,会通过 rtt_new_connection 为该通道注册一个接收器(sink):rtt_register_sink(service->channel, &read_callback, connection);。这个 read_callback 函数负责将数据通过TCP连接发送出去。
target_rtt_read_callback 函数调用 read_from_channel 从up通道读取数据。读到的数据会遍历所有为该通道注册的sink,并调用 sink->read 回调(即上一步注册的 read_callback)将数据发送出去。
read_callback 函数内部调用 connection_write 将数据写入TCP连接。
- 那么,谁触发了
target_rtt_read_callback 呢?这是在 rtt_set_polling_interval 和 rtt_start 命令中,通过注册一个定时器回调 read_channel_callback 实现的。
read_channel_callback 函数会调用 rtt.source.read,也就是 target_rtt_read_callback,从而启动一次上行数据读取和转发流程。
链路总结:定时器触发 -> read_channel_callback -> rtt.source.read (target_rtt_read_callback) -> read_from_channel -> 回调 sink->read (read_callback, connection) -> connection_write 发送数据到TCP客户端。
总结
以上通过修改OpenOCD的源码,使得TARGET为RISC-V时也支持RTT。请注意,使用RTT有一个关键前提:在芯片未处于HALT(暂停)状态时,调试器也必须能通过“后台内存访问”来读写内存。如果只有在HALT时才能访问内存,那么RTT的“实时”传输意义就大打折扣了。是否支持在未HALT时进行后台内存操作,这个特性依赖于具体调试硬件(如调试探针)和目标芯片本身的调试模块实现。
希望这篇在云栈社区分享的配置指南,能帮助你更高效地进行RISC-V平台的调试工作。如果在实践中遇到问题,欢迎在社区内与其他开发者交流探讨。