【经验篇】嵌入式,日志调试法的一些规则


大家好,我是杂烩君。

在我们嵌入式开发中,打印日志是最常用的一种调试手段。合理地打印日志,可以帮助我们快速地分析问题。

本篇文章我们来汇总一些嵌入式打log的一些规则。

1、什么操作下加日志?

(1)错误处理

对于不能恢复的严重错误,日志内容应详细到足以帮助定位问题,但同时不应该包含敏感信息。比如申请内存失败时使用错误(Error)级别加上日志信息。

(2)一些关键性的操作

一些很关键地处理,无论是正常情况或者异常情况都要打印日志。比如wifi打开时要有对应的日志信息。

(3)系统的打开、关闭

记录系统启动和关闭过程中的关键步骤有助于分析系统初始化是否正确,或者系统是否正常关闭。

(4)性能监控

日志可以记录系统运行的关键性能指标,如CPU和内存使用率、IO操作等,以便进行系统性能分析和优化。

(5)关键数据

一些关键数据需要打印,很多功能上的问题大多直接与数据进行挂钩。

(6)通信日志

对于需要与外部设备或网络通信的嵌入式系统,记录通信日志可以帮助分析和调试通信协议或数据交换的问题。

(7)记录用户行为

在需要分析用户如何与嵌入式设备交互的情况下,记录用户行为的日志会非常有帮助。

(8)if、switch

分支判断中,各执行分支需要加上对应的日志信息,可以帮助我们准确地知道程序执行的走向。

(9)程序崩溃时的信息

比如,Linxu下应用进程崩溃时的调用堆栈信息。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <execinfo.h>

void func0(void)
{
    printf("This is func0\n");
    int *p = NULL;
    *p = 1234;
}

void func1(void)
{
    printf("This is func1\n");
    func0();
}

void func2(void)
{
    printf("This is func2\n");
    func1();
}

void dump(int signo)
{
    void *array[100];
    size_t size;
    char **strings;

    size = backtrace(array, 100);
    strings = backtrace_symbols(array, size);

    printf("Obtained %zd stacks.\n", size);
    for(int i = 0; i < size; i++)
    {
        printf("%s\n", strings[i]);
    }
        
    free(strings);
    exit(0);
}

int main(int argc, char **argv)
{
    printf("==================segmentation fault test5==================\n");
    signal(SIGSEGV, &dump);
    func2();

    return 0;
}

2、功能模块标签

项目中肯定会划分有多个模块,可以给各个模块标记一个模块标签字符串,包含在日志条目里。这样我们就可以在日志文件里通过模块标签来筛选某个模块的日志,提高我们定位问题的效率。

比如:

// app_wifi.c

#define LOG_TAG    "[wifi_module]"
#define LOG_D(fmt, arg...) LOG_D_TAG(LOG_TAG, fmt, ##arg)
LOG_D("hello wifi module");

输出:

[wifi_module]hello wifi module

3、模块日志开关

设置模块日志开关,可以方便我们调试、分析问题时,缩小分析范围。当我们的函数设计有多个功能函数模块的时候,当某个模块出现问题时,这个时候我们只是关心此模块,那么可以先把其他模块的日志功能关闭掉,只是打开关心模块的日志。

如:

// module1.c
#include "module1.h"

#if MODULE1_LOG_SWITCH
#define LOG_MODULE1(fmt, args...)   DBG_PRINTF(fmt, ##args)  
#else
#define LOG_MODULE1(fmt, args...)	
#endif

// module2.c
#include "module2.h"

#if MODULE2_LOG_SWITCH
#define LOG_MODULE2(fmt, args...)   DBG_PRINTF(fmt, ##args)  
#else
#define LOG_MODULE2(fmt, args...)	
#endif

// config.h
#define  MODULE1_LOG_SWITCH  0
#define  MODULE2_LOG_SWITCH  1

4、时间戳

日志应包含时间戳,可以方便地查看某段代码的执行时间、确定问题发生的具体时间。时间戳最好能精确到微秒/毫秒。

5、日志级别

使用不同的日志级别可以帮助筛选和控制输出的信息量。

常见的日志级别包括:

  • 错误(Error):程序无法运行或严重问题。
  • 警告(Warning):可能的问题,程序可以继续运行。
  • 信息(Info):程序运行状态信息。
  • 调试(Debug):详细的调试信息,包括变量值和程序流程。
  • 详细(Verbose):非常详细的信息,用于深入调试。

6、格式统一

为了使日志易于阅读,所有日志应保持一致的格式。

日志里常包含的固定信息有:

  • 文件名
  • 行号
  • 时间日期/时间戳
  • 函数名
  • 模块名称
  • 进程ID
  • 线程ID

可根据需要进行组合。如:

[2024-01-14 11:12:30.666][wifi_module][func:wifi_init]

7、过滤控制

使用日志动态过滤控制功能可以动态地调整日志地输出,但前提是项目使用地日志组件具备这样的能力。比如EasyLogger(https://github.com/armink/EasyLogger)。

8、Release/Debug开关

由于日志打印可能占用不少系统资源,应当注意其对性能的影响。在Release版本中,可能需要减少日志输出或者去掉一些不必要的日志,需要一个开关来进行切换。

9、封装日志接口

嵌入式中,日志的输出大多数情况下会输出到串口终端。但也有一些场景,需要把日志输出到屏幕、网络设备、定制化的上位机等场景。需要留出对应接口,方便灵活切换。

以上就是本次的分享,欢迎收藏、转发!



文章作者: 杂烩君
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杂烩君 !
  目录