2、日志系统配置:LVGL日志等级设置、日志输出目标配置、日志格式自定义

调试嵌入式GUI,说白了就是跟看不见摸不着的东西打交道。

你画了一个按钮,它没显示出来。你改了颜色,它还是老样子。这时候怎么办?

我个人的习惯是——先看日志。

LVGL的日志系统,其实是被很多人忽略的调试利器。今天我们就把它彻底讲透。

2.1 日志等级设置:从“闭嘴”到“话痨”

LVGL的日志分五个等级,从低到高分别是:

宏定义 等级 说明
LV_LOG_LEVEL_TRACE 0 最详细,连函数进出都打印
LV_LOG_LEVEL_INFO 1 常规信息,比如初始化完成
LV_LOG_LEVEL_WARN 2 警告,不影响运行但值得注意
LV_LOG_LEVEL_ERROR 3 错误,功能可能异常
LV_LOG_LEVEL_USER 4 用户自定义,一般不用
LV_LOG_LEVEL_NONE 5 关闭所有日志

怎么设置?很简单,在 lv_conf.h 里改一个宏:

/* 在 lv_conf.h 中设置 */
#define LV_LOG_LEVEL    LV_LOG_LEVEL_WARN

我建议你调试阶段开到 LV_LOG_LEVEL_INFO 甚至 LV_LOG_LEVEL_TRACE。等产品稳定了,再改成 LV_LOG_LEVEL_WARNLV_LOG_LEVEL_ERROR

我的经验: 曾经有个客户说他的屏幕刷新的时候会闪一下。我让他把日志开到 TRACE 级别,结果发现是某个定时器回调里调了 lv_task_handler() 两次。日志里清清楚楚地打印了重复调用的堆栈。你说,这要是靠肉眼盯代码,得盯到什么时候?

2.2 日志输出目标配置:把话说给谁听?

日志等级设好了,但输出到哪儿呢?

LVGL默认是往 printf 里吐。但嵌入式环境千奇百怪——有的板子没有串口,有的只能用SEGGER RTT,有的需要写到文件系统里。

这时候就要靠 LV_LOG_PRINTF 这个宏了。它的原型是:

/* 默认实现,直接调用 printf */
#define LV_LOG_PRINTF(fmt, ...)    printf(fmt, ##__VA_ARGS__)

你可以把它重定向到任何地方。比如重定向到SEGGER RTT:

/* 在 lv_conf.h 或你的头文件里 */
#define LV_LOG_PRINTF(fmt, ...)    SEGGER_RTT_printf(0, fmt, ##__VA_ARGS__)

或者你想加个时间戳?没问题:

/* 自定义日志输出,带上时间戳 */
#define LV_LOG_PRINTF(fmt, ...) do { \
    printf("[%lu] ", (unsigned long)get_system_tick_ms()); \
    printf(fmt, ##__VA_ARGS__); \
} while(0)
注意: 别在日志输出函数里调用 LVGL 的 API!我曾经见过有人把日志输出到 lv_label_set_text() 里,结果日志一多,直接死循环了。日志系统本身不能依赖 LVGL 的图形资源。

2.3 日志格式自定义:让信息一眼看懂

默认的日志格式长这样:

[INFO] lv_disp.c: 123 - Display driver registered

格式是:[等级] 文件名: 行号 - 消息

说实话,这个格式够用了。但如果你觉得不够,LVGL 提供了 LV_LOG_USE_FILE_LINELV_LOG_USE_FUNC 两个宏来控制细节:

/* 在 lv_conf.h 中 */
#define LV_LOG_USE_FILE_LINE    1   /* 显示文件名和行号,默认开启 */
#define LV_LOG_USE_FUNC         1   /* 显示函数名,默认关闭 */

打开函数名后,日志会变成:

[WARN] lv_obj.c: 456 (lv_obj_set_pos) - Position out of screen bounds

我个人习惯把函数名打开。为什么呢?因为有时候同一个文件里好几个函数都在调同一个API,光看文件名和行号,你还得去翻代码。有了函数名,一眼就知道是哪个函数出了问题。

避坑指南: 我曾经在一个项目里把 LV_LOG_USE_FILE_LINE 关了,想着省点Flash。结果后来出bug了,日志里全是 [ERROR] - Buffer overflow detected,根本不知道是哪个文件哪一行。那叫一个抓狂。从那以后,我再也不敢关这个选项了。

2.4 实战:一套完整的日志配置方案

说了这么多,给出一套我常用的配置吧:

/* lv_conf.h 中的日志配置 */
#define LV_LOG_LEVEL           LV_LOG_LEVEL_WARN   /* 发布版用 WARN,调试版用 INFO */
#define LV_LOG_USE_FILE_LINE   1                   /* 必须开,不然查bug累死 */
#define LV_LOG_USE_FUNC        1                   /* 建议开,省得翻代码 */
#define LV_LOG_PRINTF          my_custom_log       /* 重定向到自定义函数 */

/* 自定义日志函数 */
void my_custom_log(const char *buf)
{
    /* 加个时间戳,再通过串口输出 */
    char timestamp[32];
    snprintf(timestamp, sizeof(timestamp), "[%u]", (unsigned int)(HAL_GetTick() / 1000));
    uart_send_string(timestamp);
    uart_send_string(buf);
    uart_send_string("\r\n");
}

这套配置的好处是:

  • 发布版只打印警告和错误,不影响性能
  • 每条日志都带文件名、行号、函数名
  • 有统一的时间戳,方便分析时序问题
  • 输出目标可以随时切换,不改LVGL源码
小技巧: 如果你用的是ESP32或者STM32,可以把日志通过蓝牙或者WiFi发到手机上。我在做可穿戴设备时就这么干过——设备戴在手上,日志实时显示在手机APP里。调试起来特别方便。

嗯,日志系统就讲到这里。说白了,它就是你的第二双眼睛。配置好了,能帮你省下大把抓头发的时间。

下一章我们聊聊LVGL的内存管理——那个让无数人翻车的地方。


公众号:蓝海资料掘金营,微信deep3321