目标:
了解J-Link的常用API函数有哪些
学会J-Link的函数的使用场景
这一节的内容比较偏向于理论讲解,笔者的语气要变得正式起来了(笔者坚信自己一直是一个严肃的人),这一节的内容可以当作一个笔记速查手册,方便大家快速的调用各个API(我是一名快乐的掉包侠~啦啦啦~)咳咳,不小心又偏了(笔者其实是故意的真的(祈求相信中...)),接下来要讲重点了,相信大家都知道对于数据传输和接收的时候,我们希望在传输和接收这些数据不受干扰,那怎么办呢,赶快打开我心爱的浏览器查一查----查到了(如果你没有查到笔者建议便宜卖给笔者,笔者以市场价0.001折的价格购买(这么良心的买家不多了)),接下来继续走向正题:常用的是三种方式Mutex互斥锁,临界区,关中断(当然不是简单除暴的直接关掉,需要先保留之前的中断寄存器的状态,方便后面进行中断系统的恢复---我重生了,这一世我要拿回属于我的一切(当然重生回去的时候要记得自己的经历,要是不记得,嘿嘿就变成了---我重生了,这一世我要继续受折磨哈哈)),RTT的临界区的实现就是关闭中断的方式同时通过SEGGER\RTT\SEGGER_RTT_ConfDefaults.h的#define SEGGER_RTT_MAX_INTERRUPT_PRIORITY (0x20)这个定义来屏蔽--笔者自己封装的日志就是依赖于类似的日志等级实现的(可以狠狠的期待一波了),踩坑指南:在RTT中,SEGGER_RTT官方已经通过SEGGER_RTT_LOCK()和SEGGER_RTT_UNLOCK()实现了遇到并发时的线程安全,所以就不需要自己加保护了(笔者当时就画蛇添足研究了对RTT输出的Mutex保护)
小科普:
Mutex(互斥锁):主要解决多个任务之间争夺共享资源的问题。拿不到锁的任务通常会进入阻塞状态,让其他任务运行。,不会影响其他任务的调度
临界区(Critical Section):指访问共享资源、不能被不安全并发访问打断的代码区域,也可以指进入和退出这类区域的保护机制。此时os的其他任务的调度无法正常运行,同时会屏蔽部分中断(根据阈值进行判断:优先级小于屏蔽阈值的讲被沉默,反之,拥有霸体效果无法屏蔽正常运行)
关中断(Disable Interrupt):是一种底层手段,通过屏蔽中断防止部分中断打断当前代码,此时os的其他任务的调度无法正常运行(调度源于中断)
踩坑指南:
官方的RTT Viewer只支持通道0的终端输出显示,其他的通道不支持,请使用ozone的RTT功能(支持所有的通道但是一次只能看一个)
官方的RTT Viewer不支持中文的显示,会乱码,请使用ozone的RTT功能(支持中文显示)(还是中文有亲切感)
1. API 速查
本文介绍的是在目标 MCU 上调用的 SEGGER RTT API。相关源码来自 SEGGER 官方 RTT 仓库。
| SEGGER_RTT_ConfigDownBuffer() | 配置下行通道 | 指定 Down | 不等待数据 |
| SEGGER_RTT_ConfigUpBuffer() | 配置上行通道 | 指定 Up | 不等待空间 |
| SEGGER_RTT_GetKey() | 取一个输入字节 | Down 0 | 不等待 |
| SEGGER_RTT_HasKey() | 检查是否有输入 | Down 0 | 不等待 |
| SEGGER_RTT_Init() | 初始化控制块 | Up / Down | 不等待 |
| SEGGER_RTT_printf() | 可变参数格式化输出 | 指定 Up | 取决于 Flags |
| SEGGER_RTT_vprintf() | va_list 格式化输出 | 指定 Up | 取决于 Flags |
| SEGGER_RTT_Read() | 读取一批字节 | 指定 Down | 不等待 |
| SEGGER_RTT_SetTerminal() | 切换虚拟终端 | Up 0 | 取决于 Flags |
| SEGGER_RTT_TerminalOut() | 临时向终端输出字符串 | Up 0 | 取决于 Flags |
| SEGGER_RTT_WaitKey() | 等待并取一个输入字节 | Down 0 | 会等待 |
| SEGGER_RTT_Write() | 按长度写数据 | 指定 Up | 取决于 Flags |
| SEGGER_RTT_WriteString() | 写 \0 结尾字符串 | 指定 Up | 取决于 Flags |
| SEGGER_RTT_GetAvailWriteSpace() | 查询剩余空间 | 指定 Up | 不等待 |
2. SEGGER_RTT_ConfigDownBuffer()
通过指定名称、内存、大小和 Flags,配置一个 Down Buffer,用于接收 PC → MCU 的数据。
int SEGGER_RTT_ConfigDownBuffer(unsigned BufferIndex, const char* sName, void* pBuffer, unsigned BufferSize, unsigned Flags);
| BufferIndex | 下行通道索引,必须小于 SEGGER_RTT_MAX_NUM_DOWN_BUFFERS |
| sName | 通道名称,指向以 \0 结尾的字符串 |
| pBuffer | RTT 下行环形缓冲区的内存地址 |
| BufferSize | 内存大小,单位为字节 |
| Flags | 通道工作模式 |
返回值:本地实现成功返回 0,索引超范围返回 -1。此函数需要自己分配内存同时通道总数是固定的
static char DownBuffer1[64]; // 地址必须在通道使用期间一直有效
int RTT_ConfigInput(void) {
return SEGGER_RTT_ConfigDownBuffer(1, "DataIn", DownBuffer1,
sizeof(DownBuffer1),
SEGGER_RTT_MODE_NO_BLOCK_SKIP);
}pBuffer 和 sName 的地址会被保存;缓冲区要求是存在与整个生命周期的数组。对非 0 通道重复调用此函数配置时会重置读写偏移(RTT的环形缓冲区就是用头尾指针实现的我自己的环形缓冲区也是),可能丢掉尚未处理的数据。运行中修改模式或名称,应使用 SEGGER_RTT_SetFlagsDownBuffer() 或 SEGGER_RTT_SetNameDownBuffer()。
通道 0 在RTT由初始化代码已经完成了缓冲区的配置,所以调用此函数通常只更新 Flags(自己传入的数组没有作用不要像小丑一样骗自己哈哈);要改变它的大小,应该修改 BUFFER_SIZE_DOWN 这个变量。
3. SEGGER_RTT_ConfigUpBuffer()
配置一个 Up Buffer,用于发送 MCU → PC 的数据。
int SEGGER_RTT_ConfigUpBuffer(unsigned BufferIndex, const char* sName, void* pBuffer, unsigned BufferSize, unsigned Flags);
参数含义与 ConfigDownBuffer() 相同,但 BufferIndex 必须小于 SEGGER_RTT_MAX_NUM_UP_BUFFERS,内存用于上行方向。成功返回 0,索引无效返回 -1。
static char UpBuffer1[256];
int RTT_ConfigOutput(void) {
return SEGGER_RTT_ConfigUpBuffer(1, "DataOut", UpBuffer1,
sizeof(UpBuffer1),
SEGGER_RTT_MODE_NO_BLOCK_SKIP);
}通道 0 在RTT由初始化代码已经完成了缓冲区的配置,所以调用此函数通常只更新 Flags(自己传入的数组没有作用不要像小丑一样骗自己哈哈);要改变它的大小,应该修改 BUFFER_SIZE_UP 这个变量。
4. SEGGER_RTT_GetKey()
从下行通道 0 取出一个字节,没有输入就立即返回。
int SEGGER_RTT_GetKey(void);
返回 0~255 表示读到的字节,返回 -1 表示没有数据。
int key = SEGGER_RTT_GetKey();
if (key >= 0 && key == 'q') {
SEGGER_RTT_WriteString(0, "Received q\n");
}使用 int 保存返回值,以区分 -1 与有效的 0 字节;一次调用只取一个字节。
5. SEGGER_RTT_HasKey()
检查下行通道 0 是否至少有一个可读字节,不取走数据。
int SEGGER_RTT_HasKey(void);
返回 1 表示有数据,0 表示没有数据。
6. SEGGER_RTT_Init()
初始化 RTT 控制块,建立默认上行通道 0 和下行通道 0。
void SEGGER_RTT_Init(void);
void RTT_Start(void) {
SEGGER_RTT_Init();
SEGGER_RTT_WriteString(0, "RTT ready\n");
// 自定义通道的 Config*Buffer() 放在 Init() 之后
}SEGGER_RTT_Init() 会清空控制块、重建默认通道,使已有配置和待处理数据失效。同时会对通道0进行默认配置。(环形缓冲区的头尾指针也会重置)
7. SEGGER_RTT_printf()
把格式字符串和可变参数转换成文本,再写入指定上行通道。
int SEGGER_RTT_printf(unsigned BufferIndex, const char* sFormat, ...);
BufferIndex 是上行通道索引;sFormat 是以 \0 结尾的格式字符串;... 是与格式说明符对应的参数。非负数表示输出字节数、负数表示错误,
int speed = 120; unsigned state = 0x2Au; SEGGER_RTT_printf(0, "speed=%d, state=0x%02X\n", speed, state);
RTT实现支持 %c、%d、%u、%x、%X、%s、%p 和 %%。格式基本结构为:
%[flags][FieldWidth][.Precision]ConversionSpecifier
支持的常见写法:%05d(补零)、%-5d(左对齐)、%+d(显示正号)、%.4d(至少 4 位数字)、%.3s(最多 3 个字节)。本地 %x 和 %X 都输出大写 A~F。
注意此函数不支持浮点数的输出,需要自己进行转换
常用格式与实际输出
下面只展示正文,不包含换行;方括号用于看清空格和字段边界。
| "%c", 'A' | A | 输出一个字符 |
| "%d", -120 | -120 | 有符号十进制 |
| "%u", 120u | 120 | 无符号十进制 |
| "0x%02X", 10u | 0x0A | 宽度至少 2 位,前缀由格式字符串提供 |
| "[%5d]", 12 | [ 12] | 数字右对齐,宽度不足时补空格 |
| "[%-5d]", 12 | [12 ] | 数字左对齐,末尾补空格 |
| "%05d", -12 | -0012 | 总宽度包含负号 |
| "%+d", 12 | +12 | 正数也显示符号 |
| "%.4d", 12 | 0012 | 整数精度表示最少数字位数 |
| "%.3s", "motor" | mot | 字符串精度表示最多输出的字节数 |
| "%.*s", 3, "motor" | mot | 从额外的 int 参数读取精度 |
| "load=100%%" | load=100% | 输出字面量百分号 |
这就是复杂的格式化知识了,我们使用常用的就可以了
8. SEGGER_RTT_vprintf()
接收已经建立的可变参数列表,把它按格式字符串转换成文本并写入指定上行通道
int SEGGER_RTT_vprintf(unsigned BufferIndex, const char* sFormat, va_list* pParamList);
这里第三个参数是 va_list*,与标准库常见的 vprintf(format, args) 签名不同。参数列表必须先由 va_start() 或 va_copy() 初始化,使用后由调用者 va_end()。
#include <stdarg.h>
#include "SEGGER_RTT.h"
int RTT_Log(const char* format, ...) {
int result;
va_list args;
va_start(args, format);
result = SEGGER_RTT_vprintf(0, format, &args);
va_end(args);
return result;
}
// RTT_Log("motor=%d, speed=%d\n", 1, 120);调用链是:printf() / 自定义日志函数 → vprintf() 解析格式 → _StoreChar() 放入临时缓冲区 → 满块或结束时调用 Write() → 上行环形缓冲区 → J-Link / PC。临时缓存默认 64 字节,是分批写出的缓存,不是整条日志的长度限制;默认上行通道 0 的环形缓冲区是 1024 字节。
9. SEGGER_RTT_Read()
从指定 Down Buffer 取出 PC 已写入的数据,复制到应用接收数组。
unsigned SEGGER_RTT_Read(unsigned BufferIndex, void* pBuffer, unsigned BufferSize);
BufferIndex 是下行通道索引;pBuffer 是应用接收数组;BufferSize 是本次最多读取的字节数。返回实际读取的 0~BufferSize 字节,没有数据时立即返回 0。
char rx[32];
unsigned n = SEGGER_RTT_Read(0, rx, sizeof(rx) - 1u);
if (n > 0u) {
rx[n] = '\0'; // 只有这样才可把本次数据当作 C 字符串
SEGGER_RTT_printf(0, "RX: %s\n", rx);
}RTT 传输的是字节序列,不自动补 \0,二进制协议应按返回长度处理。一次 Read() 也不保证得到一条完整命令;应用要按换行、固定长度或协议帧边界自行累积解析。
10. SEGGER_RTT_SetTerminal()
切换上行通道 0 后续输出所使用的虚拟终端。
int SEGGER_RTT_SetTerminal(unsigned char TerminalId);
本地 _aTerminalId[16] 支持 0~15,编码为 0~9、A~F。成功返回 0;编号无效,或非阻塞模式下没有足够空间写入 2 字节切换命令时,返回 -1。
if (SEGGER_RTT_SetTerminal(1) == 0) {
SEGGER_RTT_WriteString(0, "ERROR: motor timeout\n");
if (SEGGER_RTT_SetTerminal(0) < 0) {
// 恢复失败,后续输出仍可能发往终端 1
}
}切换后仍写通道 0,不要把终端编号当成通道编号。官方的RTT Viewer只支持通道 0,其他的通道需要自己找支持的上位机了,(这设计不合理,我要提issue太可恶了,找了半天就是找不到),笔者这边建议使用ozone的RTT功能,这里面支持,嘿嘿,勉强原谅你了(不可能的,没有一杯我满意的奶茶是哄不好我的)
11. SEGGER_RTT_TerminalOut()
临时向指定虚拟终端输出字符串,结束后恢复原先的终端。(只影响只一次的使用,对于我的日志太合适了)
int SEGGER_RTT_TerminalOut(unsigned char TerminalId, const char* s);
TerminalId 为目标终端,s 为 \0 结尾字符串。非负返回值为正文写入长度(不含控制字节),-1 表示错误;SKIP 模式下空间不足也可能返回 0。
SEGGER_RTT_TerminalOut(1, "ERROR: motor timeout\n"); SEGGER_RTT_WriteString(0, "Continue normal log\n"); // 第二行仍发往调用 TerminalOut() 之前的活动终端
它会额外写入切换和恢复命令,共 4 字节:SKIP 必须能容纳正文加 4 字节,否则整次跳过;TRIM 会为控制命令留空间并截短正文;BLOCK 会等待完整写出。
12. SEGGER_RTT_WaitKey()
等待下行通道 0 出现一个字节,取出后返回。
int SEGGER_RTT_WaitKey(void);
返回读到的 0~255 字节。它通过不断调用 GetKey() 忙等,没有超时,也不会自动让出 RTOS 任务;需要保持任务运行时应使用 GetKey() / Read() 轮询并自行安排延时。
13. SEGGER_RTT_Write()
把指定长度的数据写入 Up Buffer,可用于文本或二进制数据。
unsigned SEGGER_RTT_Write(unsigned BufferIndex, const void* pBuffer, unsigned NumBytes);
pBuffer 不要求以 \0 结尾,NumBytes 严格决定发送长度。返回实际写入 RTT 缓冲区的字节数;返回类型是 unsigned,应与请求长度比较,不要用“小于 0”判断失败。
const unsigned char frame[] = {0xA5, 0x00, 0x12, 0x5A};
unsigned written = SEGGER_RTT_Write(0, frame, sizeof(frame));
if (written != sizeof(frame)) {
// SKIP 可能整次丢弃;TRIM 可能只写入一部分
}数据中有 0x00 不会提前结束。
14. SEGGER_RTT_WriteString()
写入一个 \0 结尾字符串,不发送末尾的结束符。
unsigned SEGGER_RTT_WriteString(unsigned BufferIndex, const char* s);
返回实际写入的字节数,空间不足时按通道 Flags 处理。它内部计算字符串长度后调用 Write(),不会自动添加换行,也不会解析 %d 等格式说明符。
SEGGER_RTT_WriteString(0, "Hello World from STM32!\n");
固定文本用 WriteString();二进制或已知长度数据用 Write();带变量的文本用 printf();自定义可变参数日志用 vprintf()。
15. SEGGER_RTT_GetAvailWriteSpace()
查询指定 Up Buffer 当前还能写入多少字节。
unsigned SEGGER_RTT_GetAvailWriteSpace(unsigned BufferIndex);
返回当前空闲可写字节数。本地函数不会调用 INIT(),应先初始化 RTT,并确保通道已配置。
const char msg[] = "motor ready\n";
const unsigned len = (unsigned)(sizeof(msg) - 1u);
if (SEGGER_RTT_GetAvailWriteSpace(0) >= len) {
unsigned written = SEGGER_RTT_Write(0, msg, len);
if (written != len) {
// 查询之后,空间可能已被其他任务占用
}
}环形缓冲区会保留 1 字节区分空和满,因此大小为 N 时最大可保存 N - 1 字节。查询与写入之间空间可能变化,不能把查询结果当成写入保证;最终仍应检查 Write() 返回值。
16. SEGGER_RTT_printf.c 内部函数讲解
这一部分对应官方源码中的 Static code。三个函数均返回 void,通过修改 SEGGER_RTT_PRINTF_DESC 传递缓存状态和错误状态;应用层继续使用公开的 printf() / vprintf() 即可。
16.1 SEGGER_RTT_PRINTF_DESC:一次格式化输出的状态
它是结构体类型,不是 API。vprintf() 在栈上创建它,并关联本次调用的临时字符数组。
| pBuffer | 指向本次格式化使用的临时数组 acBuffer |
| BufferSize | 临时数组容量,默认 64 字节 |
| Cnt | 当前临时数组中尚待写出的字节数 |
| ReturnValue | 字符累计计数和错误状态;本版本存在 6.8 所述计数问题 |
| RTTBufferIndex | 最终要写入的上行通道索引 |
这里的临时数组与 RTT 上行环形缓冲区是两块不同的内存:前者负责分批收集格式化字符,后者负责保存等待 J-Link 读取的数据。
16.2 _StoreChar():保存一个字符,满块时写出
static void _StoreChar(SEGGER_RTT_PRINTF_DESC* p, char c);
p 指向本次输出状态,c 是待输出的一个字符。执行流程为:
临时数组有空间时,把 c 放到 pBuffer[Cnt],递增 Cnt 和 ReturnValue。
当 Cnt == BufferSize 时,调用 SEGGER_RTT_Write() 写出这一整块。
若实际写入量等于请求量,把 Cnt 清零,继续收集字符;否则设置 ReturnValue = -1,上层检测到错误后结束格式化。
例如默认配置下,一条 70 字节日志会先写出 64 字节,再由 vprintf() 在结束时写出剩余 6 字节。临时数组不会把整条日志截断到 64 字节;最终是否完整写入仍由通道空间与工作模式决定。
16.3 _PrintUnsigned():把无符号整数转换成数字字符
static void _PrintUnsigned(SEGGER_RTT_PRINTF_DESC* pBufferDesc, unsigned v, unsigned Base, unsigned NumDigits, unsigned FieldWidth, unsigned FormatFlags);
| pBufferDesc | 当前格式化输出状态 |
| v | 要转换的无符号整数 |
| Base | 进制;公开格式解析中使用 10 或 16 |
| NumDigits | 最少数字位数,来自整数精度 |
| FieldWidth | 最小字段宽度;数值更长时不会截断 |
| FormatFlags | 左对齐、补零等格式标志 |
它先计算数字位数,再按宽度补空格或补零,随后从高位到低位取出数字,通过 _StoreChar() 逐字输出。十六进制查表使用 A~F,因此 %x 和 %X 的字母都大写。设置左对齐时,数字输出后再补空格。
例如 SEGGER_RTT_printf(0, "%04X", 42u) 会经由此函数按十六进制输出 002A。%.4u 中的 4 是最少数字位数;%4u 中的 4 是最小字段宽度,两者含义不同。
16.4 _PrintInt():处理有符号整数的符号和宽度
static void _PrintInt(SEGGER_RTT_PRINTF_DESC* pBufferDesc, int v, unsigned Base, unsigned NumDigits, unsigned FieldWidth, unsigned FormatFlags);
参数含义与 _PrintUnsigned() 基本相同,但 v 是有符号整数。它计算数字宽度、为符号预留位置,输出负号或 +,最后把数值部分交给 _PrintUnsigned()。
例如 SEGGER_RTT_printf(0, "%+06d", 120) 输出 +00120:总宽度为 6,符号占 1 位,其余位置补零或写数字。
源码通过 -v 取得负数的绝对值,INT_MIN 的相反数无法用 int 表示,存在有符号溢出问题。若需要可靠输出整个 int 取值范围,可先使用标准库 snprintf() 转换,再调用 SEGGER_RTT_Write()。
我要赚赏金
