文件下载:log.zip
目标:
了解 bsp_log 在原生 RTT 之上补齐了哪些能力。
理解日志的发送链路、并发保护和上下文限制。
学会用标签/颜色开关与 Float2StrN() 调整日志显示。
之前的教程介绍了 RTT 的相关知识和 RTT 的 API,这一章讲述笔者自己对 RTT 的日志封装(感谢 @Charliechen114514 的帮助,对我丑陋的代码封装提供了宝贵的建议,辛苦了哈哈)。本章会介绍日志的详细细节:bsp_log 就是为 RTT 库搭建的一间方便自己使用的小房子(户型一般,笔者水平有限嘿嘿)。
本章的日志发送依赖 RTOS 和 RTT。原生 SEGGER_RTT_printf() 只负责把字节塞进环形缓冲区,它不关心这条日志属于哪个任务、该不该输出、会不会被别的任务拦腰截断。实际开发中不同信息的优先级并不相同,需要按实际情况过滤,同时还要能精准定位任务,因此在 RTT 之上又加了一层简单日志(由于 C 语言的接收代码需要打很多补丁,准备后期有时间用 C++ 开发;目前只有发送的 C 语言版本,但已经可以满足大部分需求)。
至于并发,还是老三位:Mutex 互斥锁、临界区、关中断。日志这东西有个特点——它不是业务数据,丢了顶多骂两句,卡住就是事故。所以这里选互斥锁,而且是只试一次、绝不等待的那种(xSemaphoreTake(log_mutex, 0u)),拿不到锁直接返回 BUSY,本条日志放弃。用临界区或者关中断?那会把整个调度器和中断系统拖下水,为了一行字,不值,真的不值(笔者已经替你们试过了,别问,问就是泪)。
小科普:
TLS(任务本地存储):FreeRTOS 给每个任务准备的一排指针槽位。本模块独占一个槽位保存错误码,任务之间互不覆盖(注意:CubeMX 不支持这项功能,需要自己进行宏定义)。详见 FreeRTOS TLS 文档。
踩坑指南:
SEGGER_RTT_Init() 会重建整个控制块并恢复默认通道 0。BSPLogInit() 内部会调用它,所以它必须是第一个碰 RTT 的模块;别的模块要是提前 Config*Buffer() 配好了通道,转头就被清空(别问笔者怎么知道的)。
官方的 RTT Viewer 只显示通道 0 的终端文本,通道 1 的 J-Scope 数据请用 J-Scope,通道 2 的预留通道……自己写上位机吧,笔者也没写。
通道 0 必须保持 SEGGER_RTT_MODE_NO_BLOCK_SKIP。改成 BLOCK 模式后,空间不足时 SEGGER_RTT_Write() 会原地等主机读走数据,而 BSPLogWrite() 本身是不等待语义,这俩一组合就是“日志把任务挂死”的经典现场。
开启 BSP_LOG_COLOR_ENABLED 后,每条记录会多出颜色前缀和复位序列,通道空间紧张时 SKIP 模式更容易整条丢弃;终端不支持 ANSI 时,这些字节会变成一串 [2;32m 之类的鬼画符(这不是编码坏了,是终端不认)。
BSP_LOG_TAG_ENABLED 为 0 只是不显示标签,tag 仍然必须非 NULL——接口契约没变,传空指针照样 PTR_NULL。
不要在 LOG_* 的参数里写 i++ 之类的副作用。Release 构建下 BSP_LOG_ENABLED 为 0,宏直接展开成 ((void)0),参数根本不会求值(Debug 下求值一次,于是你还得同时兼容“求值”和“不求值”两种行为,图啥呢)。
日志正文不支持 %f。想输出浮点请先用 Float2StrN() 转成字符串,再用 %s。
如果你的日志在使用时经常丢到一些信息,这时说明你的日志太多了,缓冲区放不下了(臣妾做不到啊),此时可以禁用tag和color,也可以采用简单粗爆的方式:直接增大发送缓冲区,在SEGGER\Config\SEGGER_RTT_Conf.h中添加
#ifndef BUFFER_SIZE_UP #define BUFFER_SIZE_UP 4096 #endif
1. API 速查
本文介绍的是 bsp/log/bsp_log.h 对外声明的封装接口,实现位于 bsp_log.c,底层仍然是 SEGGER RTT。所有接口都不分配堆内存,也不等待主机。
函数 / 宏作用通道 / 上下文是否等待
| BSPLogInit() | 初始化 RTT 通道、互斥锁和默认等级 | 通道 0/1/2,仅启动阶段 | 不等待 |
| BSPLogWrite() | 按等级输出一条带标签的记录 | 上行 0,任务 / 启动阶段 | 不等待 |
| LOG_TRACE() … LOG_FATAL() | 固定等级的日志快捷宏 | 上行 0,任务 / 启动阶段 | 不等待 |
| BSPLogClear() | 提交终端清屏控制序列 | 上行 0,任务 / 启动阶段 | 不等待 |
| BSPLogSetLevel() | 设置共享最低等级 | 无通道,任意上下文 | 不等待 |
| BSPLogGetLevel() | 读取共享最低等级 | 无通道,任意上下文 | 不等待 |
| BSPLogGetLastError() | 读取任务本地最后状态 | 无通道,仅任务上下文 | 不等待 |
| Float2StrN() | 浮点数转三位小数字符串 | 无通道,独立工具 | 不等待 |
等级宏与数值一一对应,数值越大越严重:
宏等级枚举数值典型用途
| LOG_TRACE() | BSP_LOG_LEVEL_TRACE | 0 | 最细粒度的流程跟踪 |
| LOG_DEBUG() | BSP_LOG_LEVEL_DEBUG | 1 | 初始化细节、单次调试数据 |
| LOG_INFO() | BSP_LOG_LEVEL_INFO | 2 | 任务就绪、模式切换、通信恢复 |
| LOG_WARN() | BSP_LOG_LEVEL_WARN | 3 | 可恢复的异常 |
| LOG_ERROR() | BSP_LOG_LEVEL_ERROR | 4 | 操作失败 |
| LOG_FATAL() | BSP_LOG_LEVEL_FATAL | 5 | 严重故障;只记录,不会停止运行 |
另外还有两个只影响显示、不影响等级的编译开关:
宏默认值作用
| BSP_LOG_TAG_ENABLED | 1 | 前缀里是否输出 [tag] |
| BSP_LOG_COLOR_ENABLED | 0 | 是否按等级输出 ANSI 颜色并在行尾复位 |
2. BSPLogInit()
在调度器启动前初始化 RTT 终端通道、数据通道、预留通道、静态日志互斥锁和默认等级。
err_t BSPLogInit(void);
参数含义
| 无 | 通道号、缓冲区大小和默认等级全部由 bsp_log.h 中的宏固定 |
返回值:成功或重复初始化成功返回 OK;不在启动阶段线程上下文返回 STATE_ERR;RTT 通道配置或静态互斥锁创建失败返回 INIT_ERR;编译时关闭日志返回 NOT_SUPPORT。
#include "bsp_log.h"
volatile err_t bsp_log_init_result = PENDING;
int main(void) {
HAL_Init();
SystemClock_Config();
/* 外设初始化 ... */
bsp_log_init_result = BSPLogInit(); /* 必须在 osKernelStart() 之前 */
osKernelInitialize();
MX_FREERTOS_Init();
osKernelStart();
}3. BSPLogWrite()
校验参数、等级和上下文,把一条带等级前缀与标签的日志提交到 RTT 上行通道 0。
err_t BSPLogWrite(bsp_log_level_t level, const char* tag, const char* format, ...);
参数含义
| level | 日志等级,TRACE(0)至 FATAL(5),越界返回 ARG_ERR |
| tag | 借用的 NUL 结尾标签,不能为 NULL,允许空串;不注册、不复制 |
| format | 借用的 RTT 原生格式串,不能为 NULL |
| ... | 按 format 顺序和类型提供的可变参数 |
返回值:OK 表示前缀、正文和行尾都已完整写入 RTT;PENDING 表示被等级过滤;ARG_ERR、PTR_NULL、INIT_ERR、STATE_ERR、BUSY、NO_BUFF、NOT_SUPPORT 的含义见第 11 节。
判定顺序为“等级 → 参数 → 初始化 → 等级过滤 → 上下文 → 加锁 → 发送 → 解锁”。过滤发生在上下文检查之前,因此在 ISR 里调用一条被过滤掉的记录会返回 PENDING 而不是 STATE_ERR。
LOG_INFO("CAN", "CAN%u 发送恢复正常", can_number);
LOG_DEBUG("CAN", "CAN%u 发送已提交 帧ID=0x%03X 长度=%u字节", can_number,
id, length);
/* 需要拿到状态时直接调用,不要用宏 */
err_t result = BSPLogWrite(BSP_LOG_LEVEL_ERROR, "CAN", "发送失败 错误码=%d",
(int)status);
if (result == BUSY) {
/* 互斥锁忙,本条记录被跳过,没有排队也没有补发 */
}默认记录格式为 [LEVEL][tag] 正文\r\n;关闭标签后为 [LEVEL] 正文\r\n;开启颜色后,前缀之前多一段等级颜色码,换行之前多一段颜色复位码。例如:
[INFO][CAN] CAN2 发送恢复正常\r\n [INFO] CAN2 发送恢复正常\r\n ← BSP_LOG_TAG_ENABLED = 0 \033[2;32m[INFO][CAN] CAN2 发送恢复正常\033[0m\r\n ← BSP_LOG_COLOR_ENABLED = 1
4. 显示开关与发送缓冲区
两个开关都在 bsp_log.h 里用 #ifndef 给默认值,可以在编译选项里用 -D 覆盖,不占运行期开销。
#ifndef BSP_LOG_TAG_ENABLED #define BSP_LOG_TAG_ENABLED 1 /* 0:不显示 [tag];非零:显示(默认) */ #endif #ifndef BSP_LOG_COLOR_ENABLED #define BSP_LOG_COLOR_ENABLED 0 /* 0:不上色(默认);非零:按等级上色 */ #endif
4.1 BSP_LOG_TAG_ENABLED
取值前缀说明
| 非零(默认 1) | [INFO][CAN] | 等级和标签都输出 |
| 0 | [INFO] | 只输出等级,标签被省略 |
关闭标签后 tag 仍要传给 BSPLogWrite(),仍然不能为 NULL(tag/format 的非空检查照旧),只是不参与前缀拼装。这个开关适合“标签太多、屏幕太窄”的场合——但代价是排查时少了一个筛选维度,建议只在空间真的紧张时关。
4.2 BSP_LOG_COLOR_ENABLED
启用后,六个等级对应六种 ANSI 颜色:
等级颜色宏实际序列观感
| TRACE | RTT_CTRL_TEXT_WHITE | \x1B[2;37m | 白色 |
| DEBUG | RTT_CTRL_TEXT_CYAN | \x1B[2;36m | 青色 |
| INFO | RTT_CTRL_TEXT_GREEN | \x1B[2;32m | 绿色 |
| WARN | RTT_CTRL_TEXT_YELLOW | \x1B[2;33m | 黄色 |
| ERROR | RTT_CTRL_TEXT_RED | \x1B[2;31m | 红色 |
| FATAL | RTT_CTRL_TEXT_BRIGHT_RED | \x1B[1;31m | 亮红色 |
颜色码写在等级前缀之前,RTT_CTRL_RESET(\x1B[0m)写在 CRLF 之前。即使前缀或正文发送失败,行尾仍会尝试发送复位和换行——否则终端会一直用上一条日志的颜色继续打印,后面的输出全变成“血红一片”,排查体验直接归零。
颜色是纯显示层的东西,只在主机终端里生效;RTT Viewer 和 Ozone 的终端支持 ANSI,自己撸的纯文本串口工具就只能看到转义字符。另外颜色码本身也占通道空间,开启后缓冲吃紧时更容易整条 SKIP,属于“好看是要付出代价的”典型案例。(但是笔者就喜欢好看的)
4.3 日志丢失与发送缓冲区
如果日志在运行中经常少几条,通常不是打印语句没执行,而是通道 0 的环形缓冲区满了:SEGGER_RTT_MODE_NO_BLOCK_SKIP 不会等待主机,放不下就整条丢弃,也不会补发(臣妾做不到啊)。可以按下面的顺序处理:
先确认主机在持续读取通道 0。主机不读,缓冲区加多大都没用。
关闭 BSP_LOG_TAG_ENABLED 和 BSP_LOG_COLOR_ENABLED,每条记录能省下标签和 ANSI 序列的开销。
仍然不够,就直接增大发送缓冲区:在 SEGGER/Config/SEGGER_RTT_Conf.h 中把通道 0 的 BUFFER_SIZE_UP 调大。
#ifndef BUFFER_SIZE_UP #define BUFFER_SIZE_UP 4096 #endif
BUFFER_SIZE_UP 是通道 0 上行缓冲区的静态数组大小,改大后 RAM 占用同步增加;它是编译期配置,修改后需要重新编译,运行时改不了。工程当前已经使用 4096 字节。通道 1、2 的容量不走这个宏,它们分别由 bsp_log.h 里的 BSP_LOG_DATA_BUFFER_SIZE 和 BSP_LOG_RESERVED_BUFFER_SIZE 控制。
5. LOG_TRACE() 等快捷宏
按固定等级调用 BSPLogWrite(),省掉每次手写等级枚举。
#define LOG_TRACE(...) BSPLogWrite(BSP_LOG_LEVEL_TRACE, __VA_ARGS__) #define LOG_DEBUG(...) BSPLogWrite(BSP_LOG_LEVEL_DEBUG, __VA_ARGS__) #define LOG_INFO(...) BSPLogWrite(BSP_LOG_LEVEL_INFO, __VA_ARGS__) #define LOG_WARN(...) BSPLogWrite(BSP_LOG_LEVEL_WARN, __VA_ARGS__) #define LOG_ERROR(...) BSPLogWrite(BSP_LOG_LEVEL_ERROR, __VA_ARGS__) #define LOG_FATAL(...) BSPLogWrite(BSP_LOG_LEVEL_FATAL, __VA_ARGS__)
宏第一个参数第二个参数后续参数
| LOG_* | tag | format | 与格式串匹配的变参 |
启用时,宏展开为 BSPLogWrite(固定等级, __VA_ARGS__),每个参数求值一次——即使这条记录随后被等级过滤,参数也已经求值过了。关闭时,六个宏全部展开为 ((void)0):参数不求值、不输出、不更新状态,连编译警告都不会有(未使用的变量该警告还是警告,别赖宏)。
float angle = 1.25f;
char text[16];
LOG_DEBUG("motor", "目标角度=%s",
Float2StrN(text, sizeof(text), angle) >= 0 ? text : "<invalid>");宏本身丢弃返回值,所以它适合“记一笔就走”的场景。要判断是否真的写出去了(比如 BUSY 或 NO_BUFF),请直接调用 BSPLogWrite()。
编译开关 BSP_LOG_ENABLED 默认是 0,工程里由 CMakeLists.txt 的 $<$<CONFIG:Debug>:BSP_LOG_ENABLED> 只在 Debug 配置注入为 1。也就是说 Release 下这些日志是编译期消失的,不是运行期过滤,二进制里连格式串都没有,省 Flash 省得相当彻底。而 BSP_LOG_TAG_ENABLED / BSP_LOG_COLOR_ENABLED 只影响 Debug 下长什么样,不改变这条“消失”规则。
6. BSPLogClear()
通过 RTT 通道 0 提交终端清屏控制序列。
err_t BSPLogClear(void);
参数含义
| 无 | 无参数 |
返回值:OK 表示序列提交成功;INIT_ERR、STATE_ERR、BUSY、NO_BUFF、NOT_SUPPORT 见第 11 节。
if (BSPLogClear() != OK) {
LOG_WARN("log", "清屏序列提交失败");
}实际写入的内容是 " " RTT_CTRL_CLEAR,即两个空格加 ESC[2J(\x1B[2J),共 6 字节,不发送末尾的 NUL。它只让主机终端清屏并把光标移回左上角,不会丢弃 RTT 缓冲区里尚未被主机读走的数据——这两件事经常被混为一谈,实际上一个在主机侧,一个在目标机侧。清屏和日志共用同一把锁,所以不会插到某条记录的中间去(想象一下一条日志被清屏劈成两半,画面太美)。
清屏是独立控制序列,不受 BSP_LOG_TAG_ENABLED / BSP_LOG_COLOR_ENABLED 影响,也不要求先开启颜色。主机终端必须支持 ANSI 转义序列;不支持时你只会看到两个空格后面跟着一串 [2J,这不是 bug,是终端不认。
7. BSPLogSetLevel()
设置后续日志记录使用的共享最低等级。
err_t BSPLogSetLevel(bsp_log_level_t level);
参数含义
| level | 最低等级,TRACE(0)至 FATAL(5) |
返回值:成功返回 OK;超出范围返回 ARG_ERR,并保留原等级不变。
BSPLogSetLevel(BSP_LOG_LEVEL_DEBUG); /* 打开调试输出 */ BSPLogSetLevel(BSP_LOG_LEVEL_INFO); /* 恢复精简输出 */
等级保存在 _Atomic uint8_t 中,读写都用 memory_order_release/acquire,所以在任务里改、在别的任务里读不会读到“半个等级”。这个接口不检查初始化状态,也不检查上下文:即使日志被编译关闭,等级仍然可以配置(只是没人用它过滤而已);在 ISR 里调用也能成功,只是不会更新任务状态。
BSPLogSetLevel() 成功时会把当前任务的最后状态清成 OK——这一点和 BSPLogGetLevel() 不同,读取成功是保留旧状态的。
8. BSPLogGetLevel()
把共享最低等级读到调用方提供的存储位置。
err_t BSPLogGetLevel(bsp_log_level_t* level);
参数含义
| level | 借用的非空输出指针,接收 TRACE 至 FATAL 范围内的等级 |
返回值:成功返回 OK;level 为 NULL 返回 PTR_NULL,不修改任何存储。
bsp_log_level_t current;
if (BSPLogGetLevel(¤t) == OK) {
LOG_INFO("log", "当前等级=%d", (int)current);
}成功读取不改变已保存的错误状态,方便你在错误处理路径里“顺便看一眼等级”而不破坏现场。空指针错误会更新状态(ISR 中除外),因为那属于调用方写错了。
9. BSPLogGetLastError()
读取启动阶段或当前任务保存的最后一个日志状态,不修改该状态。
err_t BSPLogGetLastError(void);
参数含义
| 无 | 调度器启动前读启动状态,启动后读当前任务的 TLS 槽位 |
返回值:线程模式下返回已保存的 err_t;ISR 中固定返回 STATE_ERR,且不访问 TLS。
(void)LOG_ERROR("motor", "初始化失败");
if (BSPLogGetLastError() != OK) {
/* 这里读到的是“当前任务”最近一次状态 */
}状态保存规则:
PENDING 不更新状态。
BSPLogInit()、BSPLogWrite()、BSPLogClear()、BSPLogSetLevel() 返回 OK 时清除当前任务状态。
其他失败状态写入当前任务 TLS;ISR 结果一律不写。
调度器启动前写入独立的 startup_error,新建任务不会继承它。
全新任务的槽位初值为 NULL,解码后恰好是 OK,所以任务函数里不需要“初始化错误状态”这一步。
槽位由 Core/Inc/FreeRTOSConfig.h 指定,本模块独占:
#define configBSP_LOG_TLS_ERROR_INDEX 0 #define configNUM_THREAD_LOCAL_STORAGE_POINTERS 1
缺编号、编号为负、槽位数量不够,bsp_log.c 都会在编译期 #error,不会留到运行期才爆炸(这是笔者为数不多的好习惯之一)。别的模块要用 TLS,请分配新编号并把总数加一,不要复用 0 号槽。
10. Float2StrN()
把有限浮点数转换成固定三位小数的字符串,供 %s 输出使用。
int Float2StrN(char* str, size_t size, float va);
参数含义
| str | 借用的非空调用方缓冲区,最多接收 size 字节(含 NUL) |
| size | 缓冲区容量,单位为字节,至少为 1 |
| va | 有限输入值,取整后的 va * 1000 必须落在 [-2^63, 2^63) 内 |
返回值:成功返回完整结果字符数(不含 NUL);返回值大于等于 size 表示发生截断;str 为 NULL、size 为 0、输入非有限、越界或 snprintf 失败时返回 -1。
char text[16];
int len = Float2StrN(text, sizeof(text), 1.25f);
if (len < 0) {
LOG_WARN("motor", "角度转换失败");
} else if ((size_t)len >= sizeof(text)) {
LOG_WARN("motor", "角度文本被截断");
} else {
LOG_INFO("motor", "角度=%s", text); /* 输出 角度=1.250 */
}转换规则:
先按 FRACTION_SCALE(1000)缩放并 round(),中点向远离零方向舍入。
取整后为零的负数输出 0.000,不会出现 -0.000(负零这种反人类的东西,能少一个是一个)。
小数部分按 FRACTION_DIGITS(3)补零,所以整数 2 会输出 2.000,而不是 2。
整数部分按 INTEGER_GROUP_BASE(10^9)拆成两个 uint32_t 段,用 PRIu32 输出,绕开 newlib-nano 不支持 long long 格式的问题。
失败时只要 str != NULL && size > 0,缓冲区一定以 NUL 结尾,可以安全打印。
Float2StrN() 不参与日志等级、互斥锁或错误状态,纯工具函数。它的返回值是 int 而不是 err_t:因为这里要表达的是“长度”和“截断”,不是错误码。别把它塞进 if (result < 0) 就完事——len >= size 同样是需要处理的截断情况(笔者见过有人只判 -1,然后日志里印出半截数字,还以为是电机的问题)。
11. err_t 返回值速查
err_t为公共类型,可以自己放到合适的位置
typedef enum {
PENDING = 1, ///< 等待中 | Pending
OK = 0, ///< 操作成功 | Operation successful
FAILED = -1, ///< 操作失败 | Operation failed
INIT_ERR = -2, ///< 初始化错误 | Initialization error
ARG_ERR = -3, ///< 参数错误 | Argument error
STATE_ERR = -4, ///< 状态错误 | State error
SIZE_ERR = -5, ///< 尺寸错误 | Size error
CHECK_ERR = -6, ///< 校验错误 | Check error
NOT_SUPPORT = -7, ///< 不支持 | Not supported
NOT_FOUND = -8, ///< 未找到 | Not found
NO_RESPONSE = -9, ///< 无响应 | No response
NO_MEM = -10, ///< 内存不足 | Insufficient memory
NO_BUFF = -11, ///< 缓冲区不足 | Insufficient buffer
TIMEOUT = -12, ///< 超时 | Timeout
EMPTY = -13, ///< 为空 | Empty
FULL = -14, ///< 已满 | Full
BUSY = -15, ///< 忙碌 | Busy
PTR_NULL = -16, ///< 空指针 | Null pointer
OUT_OF_RANGE = -17 ///< 超出范围 | Out of range
} err_t;全部取值与日志接口的使用情况:
状态数值含义日志接口
| PENDING | 1 | 等待中 | 使用:等级过滤,正常跳过,不更新最后错误 |
| OK | 0 | 操作成功 | 使用:写日志时表示整条记录(前缀 + 正文 + 行尾)已写入 RTT |
| FAILED | -1 | 操作失败 | 不使用:日志失败都有更细的状态,不用笼统的 FAILED |
| INIT_ERR | -2 | 初始化错误 | 使用:尚未初始化,或 RTT 通道 / 静态互斥锁创建失败 |
| ARG_ERR | -3 | 参数错误 | 使用:日志等级非法 |
| STATE_ERR | -4 | 状态错误 | 使用:上下文非法,或释放日志互斥锁失败 |
| SIZE_ERR | -5 | 尺寸错误 | 不使用 |
| CHECK_ERR | -6 | 校验错误 | 不使用 |
| NOT_SUPPORT | -7 | 不支持 | 使用:编译时关闭日志后,直接调用初始化 / 输出 / 清屏接口 |
| NOT_FOUND | -8 | 未找到 | 不使用 |
| NO_RESPONSE | -9 | 无响应 | 不使用 |
| NO_MEM | -10 | 内存不足 | 不使用:日志层不分配堆内存,压根没有这个机会 |
| NO_BUFF | -11 | 缓冲区不足 | 使用:RTT 空间不足,可能已写入部分记录 |
| TIMEOUT | -12 | 超时 | 不使用:整条链路不等待,没资格超时 |
| EMPTY | -13 | 为空 | 不使用 |
| FULL | -14 | 已满 | 不使用:缓冲区满表现为 NO_BUFF,不是 FULL |
| BUSY | -15 | 忙碌 | 使用:日志互斥锁忙,本条记录或清屏序列未输出 |
| PTR_NULL | -16 | 空指针 | 使用:标签、格式串或等级输出指针为空 |
| OUT_OF_RANGE | -17 | 超出范围 | 不使用:等级越界统一报 ARG_ERR |
“日志接口”一列写“不使用”的成员,只是 bsp_log 不会返回它,不代表这个值在工程里不存在——err_t 是共享类型,其他模块完全可能用它表示总线超时、队列已满等状态。另外注意 PENDING 的值是 1,if (result < 0) 判不出“被过滤”,要显式比较 result == PENDING。
12. 后续计划
关于本模块的使用,可参考 bsp/log/bsp_log.md 和后续的使用讲解。
下一章讲解它在 J-Link RTT Viewer 与 Ozone 终端中的使用方法。

我要赚赏金
