文件下载:log.zip
目标: 之前的教程里面介绍了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不支持此功能需要自己进行宏定义)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。
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);返回值:成功或重复初始化成功返回 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 节。
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 = 14. 标签与颜色编译开关
两个开关都在 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:不上色(默认);非零:按等级上色 */ #endif4.1 BSP_LOG_TAG_ENABLED
取值前缀说明 非零(默认 1) [INFO][CAN] 等级和标签都输出 0 [INFO] 只输出等级,标签被省略 关闭标签后 tag 仍要传给 BSPLogWrite(),仍然不能为 NULL(第 2 步检查照旧),只是不参与前缀拼装。这个开关适合“标签太多、屏幕太窄”的场合——但代价是排查时少了一个筛选维度,建议只在空间真的紧张时关。
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,属于“好看是要付出代价的”典型案例。(但是笔者就喜欢好看的)
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。
err_t result = LOG_ERROR("motor", "初始化失败"); if (result < 0 && BSPLogGetLastError() != OK) { /* 这里读到的是“当前任务”最近一次状态 */ } - `PENDING` 不更新状态。 - `BSPLogInit()`、`BSPLogWrite()`、`BSPLogClear()`、`BSPLogSetLevel()` 返回 `OK` 时清除当前任务状态。 - 其他失败状态写入当前任务 TLS;ISR 结果一律不写。 - 调度器启动前写入独立的 `startup_error`,新建任务不会继承它。 - 全新任务的槽位初值为 `NULL`,解码后恰好是 `OK`,所以任务函数里不需要“初始化错误状态”这一步。 槽位由 `Core/Inc/FreeRTOSConfig.h` 指定,本模块独占: ```c #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 返回值速查
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 这个err_t不止在此日志中可以使用在其他的地方也可以使用方便快速定位错误
12.后续计划
关于此文件的使用可以参考bsp_md和后续的使用讲解
下一章讲解此代码在jlink的RTT Viewer和ozone的终端的使用方法和教程
共6条
1/1 1 跳转至页
bsp_log:RTT分级日志封装
2楼
关于有关日志的优秀github有很多,我这里只是以我自己使用的以RTT为基础的log日志作为参考,小弟代码水平不高,勿喷,谢谢各位

共6条
1/1 1 跳转至页
回复
我要赚赏金打赏帖 |
|
|---|---|
| 一种有效完成脚本程序测试的方法被打赏¥28元 | |
| WorkBuddy搭配SolidCraft给开发板做两件式外壳被打赏¥33元 | |
| 开箱一块k230被打赏¥33元 | |
| 【瑞萨RA8D1测评】RA8D1+lwIP网络验证与TCPOTA的实验记录被打赏¥33元 | |
| 28BYJ-48机械臂实现人脸跟随被打赏¥24元 | |
| 基于RK3506B的片温识读功能的实现被打赏¥25元 | |
| 【瑞萨RA8D1测评】基于FreeRTOS的lvgl移植教程被打赏¥29元 | |
| 电子价签墨水屏开发被打赏¥35元 | |
| 【Ozone】RTT 打印终端使用被打赏¥29元 | |
| BMS保护板绝缘检测功能-不平衡电桥技术绝缘电阻检测设计与实现被打赏¥34元 | |


我要赚赏金
