说明:记录 RT-Thread: ulog 日志功能和使用流程。
官网资料链接:
https://docs.rt-thread.org/#/rt-thread-version/rt-thread-standard/programming-manual/ulog/ulog
日志的定义:日志是将软件运行的状态、过程等信息,输出到不同的介质中(例如:文件、控制台、显示屏等),并进行显示和保存。为软件调试、维护过程中的问题追溯、性能分析、系统监控、故障预警等功能,提供参考依据。可以说,日志的使用,几乎占用的软件生命周期的至少 80% 的时间。
1.日志输出的后端多样化,可支持例如:串口、网络,文件、闪存等后端形式。
2.日志输出被设计为线程安全的方式,并支持异步输出模式。
3.日志系统高可靠,在中断 ISR 、Hardfault 等复杂环境下依旧可用。
4.日志支持运行期 / 编译期设置输出级别。
5.日志内容支持按关键词及标签方式进行全局过滤。
6.API 和日志格式可兼容 linux syslog。
7.支持以 hex 格式 dump 调试数据到日志中。
8.兼容 rtdbg (RTT 早期的日志头文件)及 EasyLogger 的日志输出 API。
点开 settings 菜单
点亮,ulog 日志 图标,
注意:没有开启这个图标框架功能时, LOG_D 这些函数的基本 串口打印输出 功能还是有的,只是一些丰富的信息没有了。
右键点击 详细配置 有更丰富的配置选项
设置好后点击保存。
日志级别代表了日志的重要性,在 ulog 中由高到低,有如下几个日志级别:
级别 |
名称 |
描述 |
LOG_LVL_ASSERT |
断言 |
发生无法处理、致命性的的错误,以至于系统无法继续运行的断言日志 |
LOG_LVL_ERROR |
错误 |
发生严重的、不可修复的错误时输出的日志属于错误级别日志 |
LOG_LVL_WARNING |
警告 |
出现一些不太重要的、具有可修复性的错误时,会输出这些警告日志 |
LOG_LVL_INFO |
信息 |
给本模块上层使用人员查看的重要提示信息日志,例如:初始化成功,当前工作状态等。该级别日志一般在量产时依旧保留 |
LOG_LVL_DBG |
调试 |
给本模块开发人员查看的调试日志,该级别日志一般在量产时关闭 |
由于日志输出量的不断增大,为了避免日志被杂乱无章的输出出来,就需要使用标签(tag)给每条日志进行分类。标签的定义是按照模块化的方式,例如:Wi-Fi 组件包括设备驱动(wifi_driver)、设备管理(wifi_mgnt)等模块,则 Wi-Fi 组件内部模块可以使用 wifi.driver、wifi.mgnt 等作为标签,进行日志的分类输出。
每条日志的标签属性也可以被输出并显示出来,同时 ulog 还可以设置每个标签(模块)对应日志的输出级别,当前不重要模块的日志可以选择性关闭,不仅降低 ROM 资源,还能帮助开发者过滤无关日志。
参见 rt-thread\examples\ulog_example.c ulog 例程文件,在文件顶部有定义 LOG_TAG 宏:
#define LOG_TAG "example" // 该模块对应的标签。不定义时,默认:NO_TAG
#define LOG_LVL LOG_LVL_DBG // 该模块对应的日志输出级别。不定义时,默认:调试级别
#include // 必须在 LOG_TAG 与 LOG_LVL 下面复制错误复制成功
需要注意的,定义日志标签必须位于 #include 的上方,否则会使用默认的 NO_TAG(不推荐定义在头文件中定义这些宏)。
日志标签的作用域是当前源码文件,项目源代码通常也会按照模块进行文件分类。所以在定义标签时,可以指定模块名、子模块名作为标签名称,这样不仅在日志输出显示时清晰直观,也能方便后续按标签方式动态调整级别或过滤。
初始化
int ulog_init(void)复制错误复制成功
返回 |
描述 |
>=0 |
成功 |
-5 |
失败,内存不足 |
在使用 ulog 前必须调用该函数完成 ulog 初始化。如果开启了组件自动初始化,该函数也将被自动调用。
去初始化
void ulog_deinit(void)复制错误复制成功
当 ulog 不再使用时,可以执行该 deinit 释放资源。
ulog 主要有两种日志输出宏 API,源代码中定义如下所示:
#define LOG_E(...) ulog_e(LOG_TAG, __VA_ARGS__)
#define LOG_W(...) ulog_w(LOG_TAG, __VA_ARGS__)
#define LOG_I(...) ulog_i(LOG_TAG, __VA_ARGS__)
#define LOG_D(...) ulog_d(LOG_TAG, __VA_ARGS__)
#define LOG_RAW(...) ulog_raw(__VA_ARGS__)
#define LOG_HEX(name, width, buf, size) ulog_hex(name, width, buf, size)
宏 LOG_X(...):X 对应的是不同级别的第一个字母大写。参数 ... 为日志内容,格式与 printf 一致。这种方式是首选,一方面因为其 API 格式简单,入参只有一个即日志信息,再者还支持按模块静态日志级别过滤。
宏 ulog_x(LOG_TAG, __VA_ARGS__):x对应的是不同级别的简写。参数LOG_TAG为日志标签,参数...` 为日志内容,格式与 printf 一致。这个 API 适用于在一个文件中使用不同 tag 输出日志的情况。
API |
描述 |
LOG_E(...) |
错误级别日志 |
LOG_W(...) |
错误级别日志 |
LOG_I(...) |
提示级别日志 |
LOG_D(...) |
调试级别日志 |
LOG_RAW(...) |
输出 raw 日志 |
LOG_HEX(name, width, buf, size) |
输出 16 进制格式数据到日志 |
LOG_X 及 ulog_x 这类 API 输出都是带格式日志,有些时候需要输出不带任何格式的日志时,可以使用 LOG_RAW 或 ulog_raw() 。例如:
LOG_RAW("\r");
ulog_raw("\033[2A");
以 16 进制 hex 格式 dump 数据到日志中可使用可以使用 LOG_HEX() 或 ulog_hex 。函数参数及描述如下所示:
参数 |
描述 |
tag |
日志标签 |
width |
一行 hex 内容的宽度(数量) |
buf |
待输出的数据内容 |
size |
数据大小 |
hexdump 日志为 DEBUG 级别,支持运行期的级别过滤,hexdump 日志对应的 tag ,支持运行期的标签过滤。
ulog 也提供里断言 API :ASSERT(表达式) ,当断言触发时,系统会停止运行,内部也会执行 ulog_flush() ,所有日志后端将执行 flush 。如果开启了异步模式,缓冲区中所有的日志也将被 flush 。断言的使用示例如下:
void show_string(const char *str)
{
ASSERT(str);
...
}
API |
描述 |
LOG_E(...) |
错误级别日志 |
LOG_W(...) |
错误级别日志 |
LOG_I(...) |
提示级别日志 |
LOG_D(...) |
调试级别日志 |
LOG_RAW(...) |
输出 raw 日志 |
LOG_HEX(name, width, buf, size) |
输出 16 进制格式数据到日志 |
下面将以 ulog 例程进行介绍,打开 rt-thread\examples\ulog_example.c 可以看到,顶部有定义该文件的标签及静态优先级。
#define LOG_TAG "example"
#define LOG_LVL LOG_LVL_DBG
#include 复制错误复制成功
在 void ulog_example(void) 函数中有使用 LOG_X API ,大致如下:
/* output different level log by LOG_X API */
LOG_D("LOG_D(%d): RT-Thread is an open source IoT operating system from China.", count);
LOG_I("LOG_I(%d): RT-Thread is an open source IoT operating system from China.", count);
LOG_W("LOG_W(%d): RT-Thread is an open source IoT operating system from China.", count);
LOG_E("LOG_E(%d): RT-Thread is an open source IoT operating system from China.", count);复制错误复制成功
这些日志输出 API 均支持 printf 格式,并且会在日志末尾自动换行。
以下截图是 如下代码放在 main 函数中运行后的效果,截图是 MobaXterm1_CHS1.exe 串口调试软件的显示效果
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#define LED_2 GET_PIN(C,3)
#include
int main(void)
{
int count = 1;
while (count++)
{
rt_thread_mdelay(2000);
LOG_D("LOG_D(%d): RT-Thread is an open source IoT operating system from China.", count);
LOG_I("LOG_I(%d): RT-Thread is an open source IoT operating system from China.", count);
LOG_W("LOG_W(%d): RT-Thread is an open source IoT operating system from China.", count);
LOG_E("LOG_E(%d): RT-Thread is an open source IoT operating system from China.", count);
}
return RT_EOK;
}
可以看到每条日志都是按行显示,不同级别日志也有着不同的颜色。在日志最前面有当前系统的 tick ,中间有显示日志级别及标签,最后面是具体的日志内容。
在本文后面也会重点介绍这些日志格式及配置说明。
很多时候需要在中断 ISR 中输出日志,但是中断 ISR 可能会打断正在进行日志输出的线程。要保证中断日志与线程日志互不干涉,就得针对于中断情况进行特殊处理。
ulog 已集成中断日志的功能,但是默认没有开启,使用时打开 Enable ISR log 选项即可,日志的 API 与线程中使用的方式一致,例如:
#define LOG_TAG "driver.timer"
#define LOG_LVL LOG_LVL_DBG
#include
void Timer2_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
LOG_D("I'm in timer2 ISR");
/* leave interrupt */
rt_interrupt_leave();
}
这里说明下中断日志在 ulog 处于同步模式与异步模式下的不同策略:
-同步模式下:如果线程此时正在输出日志时来了中断,此时如果中断里也有日志要输出,会直接输出到控制台上,不支持输出到其他后端;
-异步模式下:如果发生上面的情况,中断里的日志会先放入缓冲区中,最终和线程日志一起交给日志输出线程来处理。
hexdump 也是日志输出时较为常用的功能,通过 hexdump 可以将一段数据以 hex 格式输出出来,对应的 API 为:void ulog_hexdump(const char *tag, rt_size_t width, rt_uint8_t *buf, rt_size_t size) ,
下面看下具体的使用方法及运行效果:
参数 |
描述 |
tag |
日志标签 |
width |
一行 hex 内容的宽度(数量) |
buf |
待输出的数据内容 |
size |
数据大小 |
/* 定义一个 128 个字节长度的数组 */
uint8_t i, buf[128];
/* 在数组内填充上数字 */
for (i = 0; i < sizeof(buf); i++)
{
buf[i] = i;
}
/* 以 hex 格式 dump 数组内的数据,宽度为 16 */
ulog_hexdump("buf_dump_test", 16, buf, sizeof(buf));
可以将上面的代码拷贝到 ulog 例程中运行,然后再看下实际运行结果:
点开 settings 菜单
点亮,ulog 日志 图标,
注意:没有开启这个图标框架功能时, LOG_D 这些函数的基本 串口打印输出 功能还是有的,只是一些丰富的信息没有了。
右键点击 详细配置 有更丰富的配置选项
具体设置内容
分别可以配置:
浮点型数字的支持(传统的 rtdbg/rt_kprintf 均不支持浮点数日志)、
带颜色的日志、
时间信息(包括时间戳)、
级别信息、
标签信息、
线程信息。
设置完成后点击保存