Linux开发讲课38--- 调试程序打印日志

        无论开发何种程序,单片机,电脑客户端,还是服务器,日志都是最基础也是最重要的调试手段。手机APP,电脑客户端和服务器的开发环境往往提供了功能丰富的日志接口。

        比如linux的syslog模块提供如下日志函数:

        其记录的每条日志包含时间,级别,来源和内容,可根据配置过滤低级别的日志。下图中的日志,红框是时间,黄框是来源,绿框是内容。

        单片机的开发则大不相同,其开发环境中往往不会提供日志接口。单片机的驱动库往往只提供基础的操作外设的接口,如uart,i2c,spi,而不提供高层次的SDK。这就需要我们自己来设计。
        单片机日志接口比较简单的实现,是将日志从串口输出,在电脑上可通过诸多串口软件来查看。向串口输出字符嘛,看着简单,其实大有文章。简陋的设计与精心周全的设计将使日后的代码开发和调试产生巨大差异。

        在串口层面添加打印字符串的函数,从而使log_print不需要长度参数。
        log_print在输出日志内容之后自动换行,从而避免每条日志中都要加''。

                ​​​​​​​        

        如果在设计底层接口时偷懒,则日后写应用层代码时将堕入万劫不复之深渊。如果想在调用时舒服些,那就需要在设计底层时多花些心思。

使用printf
        修改过后的log_print不再丑陋,但依然很简陋。如果想打印动态的内容,如数字时,还是比较麻烦。比如打印网络信号质量(rssi为int型变量,值为信号质量):

        简单打印一个数字却需要三行代码,非常不便。这时我们可以呼叫printf函数:

        相信大家肯定对printf并不陌生,这可是学习C语言时经常用到的函数,其将内容打印到标准输出。单片机SDK并没有指定标准输出,若想将printf的内容输出到指定串口,则需要实现printf所依赖的底层函数。在gcc编译环境中(stm32 cube,TRUEStudio,Code Compose Studio等均使用gcc编译器),需要实现_write函数:

        drv_uart_send为笔者封装的串口发送函数,dbg_uart指向调试串口。

        _write为C库中定义的系统函数之一。_write实现的其实是将数据写入文件,fd为文件号。由printf调用_wirte时,传入的fd为1,即标准输出stdout。其他函数,如fprintf,fwrite也会调用_write,它们传入的fd指向相关文件。所以上述_write函数的实现并不严谨,它忽略了fd参数,不管是什么文件的写操作,均输出到调度串口。
严谨的设计应该这样:

添加更多的信息
        使用printf已经比之前的log_print方便许多,不过仍然有改进的空间。
快速打开或关闭日志
        在调试阶段,可能需要打印较多的日志来观察效果和跟踪问题。由于过多的日志可能降低程序的实时性,也暴露了实现的细节,所以在正式运行时需要关闭全部或者大部分日志。如果写了大量的printf语句的话,一个个注释掉显然是件麻烦的事情。需要有一个能够快速打开或关闭日志的功能。
时间信息
        有时不仅想要知道程序运行的结果,还想观察关键步骤执行的时间。比如某步执行了多长时间,某个定时任务执行的周期是否正确。如果日志中包含时间信息(比如从启动到现在所经过的毫秒)就会很方便观察。

来源和级别
        当日志较多时,如果能打印日志的来源(文件名或模块名,函数名,代码行数),则更方便查看和定位问题。在多人合作开发时,这点尤为重要。
        如果根据日志内容的重要性对日志分级,以不同的颜色来显示不同级别的内容,则更容易观察。
下图中:
白色为verbose级别,一般用于打印码流。
蓝色为debug级别,包含了大量的调试信息。
绿色为info级别,通常打印关键步骤的结果。
红色为error级别,打印错误信息。
下图红框中为日志来源,由模块名和代码行数组成。

实现
定义日志级别:

定义宏LOG_D以打印调试级别的日志:

解释几点内容:
        LOG_LEVEL为当前日志级别,其可以控制是否打印调试级别的日志。此宏在用户代码中定义,而不是头文件这中。这样一来,不同的代码文件可以单独控制自己日志的级别。有些开发完毕的文件可调高日志级别(数值越小,级别越高)以精简日志,而开发中的文件可调低日志级别。
...用于接收变长参数,编译器做宏替换时,会将...接收的参数集替换到__VA_ARGS__所在位置。
##为标识连接符,这里是用于处理变长参数集为空的场景。此时,##会吞噬前面的逗号。
        LOG_D在进行宏替换时,会加上日志级别(调试级别),标签名(LOG_TAG,同样在用户代码中定义),代码行数信息。
每一个用户代码在导入日志模块头文件时,需要指定标签名称和日志级别:

使用起来很简单,当printf使用即可,比如:

        如果刚才没有理解...和##的话,这里结合示例再说明下。
        上述示例中,LOG_D("rssi:%d", 30)中的"rssi:%d"是格式化字符串,对应于LOG_D(fmt, ...)中的fmt参数,30则属于变长参数。为了便于理解,笔者给出中间替换结果,即LOG_LEVEL_DEBUG之类的不替换:

至于LOG_D("this is a debug log"),如果没有##的话,将被替换为:

        有没有注意最右边有一个单独的逗号?##的作用就是在这种情况下把逗号给吃掉。
        logger_output的实现如下,先打印时间和来源,之后调用了va_list版本的snprintf,即vsnprintf,以打印用户传递的格式化字符串和变长参数。关于va_list,改天单独出篇文章讲解。

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值