一、构建系统log日志打印功能
在嵌入式设备软件开发、调试、生产、维护过程中,设备运行输出的日志信息对分析问题至关重要。有了日志信息,就能准确的知道当前设备的运行状态,设备出现了什么故障,可以帮助工程师有效的定位问题和解决问题。从在云端穿梭的飞机,到路上奔驰的汽车,再到攥在手里的手机,在它们的控制系统中,都无时无刻不在记录和保存系统运行日志。当飞机失事时,人们第一件要做的事就是寻找黑匣子,因为黑匣子详细记录了飞机的飞行数据,可以供分析事故原因。当我们的手机出现异常拿到维修店维修时,工程师通常也会插入数据线,抓取当前手机的运行日志来定位故障原因。所以一套行之有效的日志输出功能是嵌入式软件中不可或缺的重要组成部分。
对JAVA语言开发有所了解的读者可能知道,使用JAVA 语言做Web 企业级开发时,有很多开源的日志框架可供使用,比如Log4J,可以将系统运行的信息、告警信息、异常信息等打印到控制台或者某个文件中,使开发和维护都很方便。但是对于嵌入式软件开发而言,因为芯片的差异,所采用的C/C++语言开发,不像JAVA 语言有丰富的开源框架可以使用,针对该种情况,本章我们介绍一种基于芯片上自带的串口UART构建的日志打印功能,通过串口UART 输出日志信息,支持格式化打印、设定日志级别和分模块打印,也可以进行动态日志的开关和级别设定。
我们这里提供一个log4m.c/.h文件,用户只需要将自己平台的uart数据输出接口填入log4m_data_output(uint8_t *pData, uint16_t Size)函数即可,就可以开始使用log4m提供的日志打印功能,在串口调试助手上获取到输出的日志信息。当然如果你填入的是CAN、SPI,USB 底层输出接口,那么你也可以通过对应的CAN、SPI、USB等工具能接收到日志打印,这里重点是提供一种LOG输出的框架,拿UART来举例子说明,因为UART目前仍然是非常常用的通信方式,简单可靠,像主流的SOC如RK3568 上跑的Linux 系统,它的系统日志输出和shell功能,还是通过UART串口实现的。通用的MCU 如STM32系列,GD,蓝牙、WIFI等芯片都会有一个或多个UART,串口日志输出是必不可少的调试手段,所以此日志设计模式特别在适合在这些芯片上使用。用户也可以在基础上做些修改使其可以更好的为所开发的系统服务,比如可以在统一的日志输出接口中导入到系统的FatFs文件系统中进行系统LOG的离线存储,非常方便。
二、最佳实践
通过串口实现类似printf的格式化打印函数,并且可以分模块打印,设定log打印等级,可以进行动态log开关,静态log开关功能,方便调试。
log4m.h文件如下:
/**
******************************************************************************
* @file log4m.h
* @author xmq
* @date 25/05/15
* @brief header for log4m.c module
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2025 xmq.
* All rights reserved.</center></h2>
*
* This software component is licensed by xmq
*
******************************************************************************
*/
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef LOG4M_H
#define LOG4M_H
/* Includes ------------------------------------------------------------------*/
#include <stdint.h>
/*
* @brief function log struct
*/
typedef struct
{
uint32_t log_id;
char * log_name;
}log_id_name_map_t;
/*
* @brief function log struct
*/
typedef struct
{
uint32_t level;
char * level_name;
}log_level_t;
/*
* @brief log levels
*/
typedef enum
{
LEVEL_NONE = 0, /**< no level */
LEVEL_DEBUG = 1, /**< debug info */
LEVEL_INFO = 2, /**< run info */
LEVEL_WARN = 3, /**< warnning info */
LEVEL_ERROR = 4 /**< error info */
} log_level_e;
/*
* @brief log ctrl struct
*/
typedef struct
{
uint32_t switch_sta;
log_level_e level;
} log_ctrl_t;
/*
* @brief Disable/Enable states
*
*/
typedef enum
{
LOG_DISABLE=0,
LOG_ENABLE =1,
}log_switch_e;
/* Log ID 定义
如果不需要某个模块的LOG打印,请注释掉下面的对应行的宏定义,
这样该模块的LOG打印接口将编译不进版本,能够节省空间
*/
#define LOG_ID_NULL (0x00000000)
#define LOG_ID_RAW (0x00000001<<0)
#define LOG_ID_ADC (0x00000001<<1)
#define LOG_ID_BSP (0x00000001<<2)
#define LOG_ID_CAN (0x00000001<<3)
#define LOG_ID_DBG (0x00000001<<4)
#define LOG_ID_E2P (0x00000001<<5)
#define LOG_ID_NFC (0x00000001<<6)
#define LOG_ID_GPS (0x00000001<<7)
#define LOG_ID_GUI (0x00000001<<8)
#define LOG_ID_BLE (0x00000001<<9)
#define LOG_ID_MDM (0x00000001<<10)
#define LOG_ID_PMM (0x00000001<<11)
#define LOG_ID_RTC (0x00000001<<12)
#define LOG_ID_SYS (0x00000001<<13)
#define LOG_ID_WDT (0x00000001<<14)
#define LOG_ID_WIFI (0x00000001<<15)
#define LOG_ID_MOTOR (0x00000001<<16)
#define LOG_ID_SPI (0x00000001<<17)
#define LOG_ID_I2C (0x00000001<<18)
/*<用户可以在这里继续添加自定义的LOG ID 或对上面的ID进行调整>*/
#define LOG_ID_ALL 0xFFFFFFFF
void log4m_printf(log_level_e level,uint32_t log_id,const char *fmt, ...);
#ifdef LOG_ID_NULL
#define log4m(level,...) log4m_printf(level,LOG_ID_RAW,__VA_ARGS__)
#else
#define log4m(...)
#endif
#ifdef LOG_ID_ADC
#define log4adc(level,...) log4m_printf(level,LOG_ID_ADC,__VA_ARGS__)
#else
#define log4adc(...)
#endif
#ifdef LOG_ID_BSP
#define log4bsp(level,...) log4m_printf(level,LOG_ID_BSP,__VA_ARGS__)
#else
#define log4bsp(...)
#endif
#ifdef LOG_ID_SPI
#define log4spi(level,...) log4m_printf(level,LOG_ID_SPI,__VA_ARGS__)
#else
#define log4spi(...)
#endif
#ifdef LOG_ID_I2C
#define log4i2c(level,...) log4m_printf(level,LOG_ID_I2C,__VA_ARGS__)
#else
#define log4i2c(...)
#endif
#ifdef LOG_ID_CAN
#define log4can(level,...) log4m_printf(level,LOG_ID_CAN,__VA_ARGS__)
#else
#define log4can(...)
#endif
#ifdef LOG_ID_DBG
#define log4dbg(level,...) log4m_printf(level,LOG_ID_DBG,__VA_ARGS__)
#else
#define log4dbg(...)
#endif
#ifdef LOG_ID_E2P
#define log4e2p(level,...) log4m_printf(level,LOG_ID_E2P,__VA_ARGS__)
#else
#define log4e2p(...)
#endif
#ifdef LOG_ID_NFC
#define log4nfc(level,...) log4m_printf(level,LOG_ID_NFC,__VA_ARGS__)
#else
#define log4nfc(...)
#endif
#ifdef LOG_ID_GPS
#define log4gps(level,...) log4m_printf(level,LOG_ID_GPS,__VA_ARGS__)
#else
#define log4gps(...)
#endif
#ifdef LOG_ID_GUI
#define log4gui(level,...) log4m_printf(level,LOG_ID_GUI,__VA_ARGS__)
#else
#define log4gui(...)
#endif
#ifdef LOG_ID_BLE
#define log4ble(level,...) log4m_printf(level,LOG_ID_BLE,__VA_ARGS__)
#else
#define log4ble(...)
#endif
#ifdef LOG_ID_MDM
#define log4mdm(level,...) log4m_printf(level,LOG_ID_MDM,__VA_ARGS__)
#else
#define log4mdm(...)
#endif
#ifdef LOG_ID_PMM
#define log4pmm(level,...) log4m_printf(level,LOG_ID_PMM,__VA_ARGS__)
#else
#define log4pmm(...)
#endif
#ifdef LOG_ID_RTC
#define log4rtc(level,...) log4m_printf(level,LOG_ID_RTC,__VA_ARGS__)
#else
#define log4rtc(...)
#endif
#ifdef LOG_ID_SYS
#define log4sys(level,...) log4m_printf(level,LOG_ID_SYS,__VA_ARGS__)
#else
#define log4sys(...)
#endif
#ifdef LOG_ID_WDT
#define log4wdt(level,...) log4m_printf(level,LOG_ID_WDT,__VA_ARGS__)
#else
#define log4wdt(...)
#endif
#ifdef LOG_ID_WIFI
#define log4wifi(level,...) log4m_printf(level,LOG_ID_WIFI,__VA_ARGS__)
#else
#define log4wifi(...)
#endif
#ifdef LOG_ID_MOTOR
#define log4motor(level,...) log4m_printf(level,LOG_ID_MOTOR,__VA_ARGS__)
#else
#define log4motor(...)
#endif
/*<用户可以在这里添加自定义的LOG 接口>*/
/* Exported functions ------------------------------------------------------- */
void log4m_init(void);
void log4m_switch(char *func_log_name,log_switch_e sta);
void log4m_test(void);
#endif /* LOG4M_H */
/************************ (C) COPYRIGHT xmq *****END OF FILE****/
log4m.c文件如下:
/**********************************Copyright (c)**********************************
** 版权所有 (C), 2022-2030
**
*********************************************************************************/
/**
* @file log4m.c
* @author xmq
* @version v0.0.1
* @date 22.5.16
* @brief
******************************************************
*
*
*/
/****************************** 免责声明 !!! *******************************
由于芯片类型和编译环境多种多样,所以此代码仅供参考,请使用者自行把控最终代码质量
******************************************************************************/
/*********************************非常重要,一定要看哦!!!*********************************************
移植须知:
0.当使能宏定义后需要用户实现代码的函数内部有#err提示,完成函数后请删除该#err
1:如果用户系统中使用了操作系统,请使能宏定义:USE_RTOS,并实现互斥信号量用于保护公共变量,实现完成后删除#err
2:实现要输出log的串口发送接口;
3:以上两步实现完成后,移植完成;log4m_init() 进行初始化,然后可在所要使用log打印的的文件中,添加#include "log4m.h",调用log4XXX()接口进行log打印。
4:用户可以仿照添加自定义的LOG_ID_XXX和log4XXX接口
******************************************************************************/
/* Includes ------------------------------------------------------------------*/
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include "log4m.h"
#include "cmsis_os.h"
#include "bsp_uart.h"
#undef USE_RTOS
//#define USE_RTOS //是否使用RTOS操作系统,不使用请注释掉
#define LOG_NAME_LEN_LMT 16 //log功能名称最大限制,如果用户自定义的名称更长,可以放大
#define LOG_MSG_LEN_LMT 512 //log buff长度最大限制,如果用户要打印的log数据长度更长,可以放大
char g_log_msg_buff[LOG_MSG_LEN_LMT];
log_ctrl_t g_log_ctrl= //控制各个log模块开关状态和等级
{
.switch_sta=LOG_ID_ALL, //log_switch 用于控制功能模块log开关;
.level =LEVEL_NONE //log_level 用于控制功能模块log的log等级,高于或等于此级别的LOG 才允许输出
};
/*<---------log等级map------------>*/
static log_level_t g_log_level_map[]=
{
{LEVEL_NONE , "" },
{LEVEL_DEBUG, "[DBG]"},
{LEVEL_INFO , "[INF]"},
{LEVEL_WARN , "[WAR]"},
{LEVEL_ERROR, "[ERR]"},
};
/*<---------log名称map------------>*/
static log_id_name_map_t func_log_map[]=
{
#ifdef LOG_ID_RAW
{LOG_ID_RAW, "" },
#endif
#ifdef LOG_ID_ADC
{LOG_ID_ADC, "<ADC>"},
#endif
#ifdef LOG_ID_SPI
{LOG_ID_SPI, "<SPI>"},
#endif
#ifdef LOG_ID_I2C
{LOG_ID_I2C, "<I2C>"},
#endif
#ifdef LOG_ID_CAN
{LOG_ID_CAN, "<CAN>"},
#endif
#ifdef LOG_ID_DBG
{LOG_ID_DBG, "<DBG>"},
#endif
#ifdef LOG_ID_E2P
{LOG_ID_E2P, "<E2P>"},
#endif
#ifdef LOG_ID_NFC
{LOG_ID_NFC, "<NFC>"},
#endif
#ifdef LOG_ID_GPS
{LOG_ID_GPS, "<GPS>"},
#endif
#ifdef LOG_ID_GUI
{LOG_ID_GUI, "<GUI>"},
#endif
#ifdef LOG_ID_BLE
{LOG_ID_BLE, "<BLE>"},
#endif
#ifdef LOG_ID_MDM
{LOG_ID_MDM, "<MDM>"},
#endif
#ifdef LOG_ID_PMM
{LOG_ID_PMM, "<PMM>"},
#endif
#ifdef LOG_ID_RTC
{LOG_ID_RTC, "<RTC>"},
#endif
#ifdef LOG_ID_SYS
{LOG_ID_SYS, "<SYS>"},
#endif
#ifdef LOG_ID_WDT
{LOG_ID_WDT, "<WDT>"},
#endif
#ifdef LOG_ID_WIFI
{LOG_ID_WIFI, "<WIFI>"},
#endif
#ifdef LOG_ID_MOTOR
{LOG_ID_MOTOR,"<MOTOR>"},
#endif
/*-用户可以在这里添加自定义的LOG ID 和名称-*/
};
/***************************************************
说明: 全局变量数组互斥锁定义
Note:如果使用了操作系统,请实现相应互斥信号量:create、lock、unlock操作
****************************************************/
static void log_mutex_create(void)
{
#ifdef USE_RTOS
#error "如果使用了操作系统,请在此处创建互斥信号量用于保护公用资源,避免冲突,实现后注释掉该行"
#endif
}
static void log_mutex_lock(void)
{
#ifdef USE_RTOS
#error "实现互斥上锁,实现后注释掉该行"
#endif
}
static void log_mutex_unlock(void)
{
#ifdef USE_RTOS
#error "实现互斥解锁,实现后注释掉该行"
#endif
}
/*log4m 初始化函数*/
void log4m_init(void)
{
log_mutex_create();
}
static void log4m_data_output(uint8_t *pData, uint16_t Size)
{
//#error "请将串口发送函数填入该函数,并删除该行"
/* 示例如下,请替换成自己的接口函数*/
extern void bsp_uartx_send(UARTx_e uartx,uint8_t *pData, uint16_t Size);
bsp_uartx_send(UART_1,pData,Size);
}
static int get_func_log_name(uint32_t log_Id,char *dest_name)
{
uint16_t func_num=0;
uint16_t len=0;
uint16_t i;
if(log_Id==LOG_ID_NULL||dest_name==NULL)
{
return -1;
}
func_num=sizeof(func_log_map)/sizeof(func_log_map[0]);
for(i=0;i<func_num;i++)
{
if(func_log_map[i].log_id==log_Id)
{
len=strlen(func_log_map[i].log_name);
memcpy(dest_name,func_log_map[i].log_name,len);
return len;
}
}
return 0;
}
/**
* 功能: 按照log_id,log级别,实现类似printf功能打印
* 参数:
* @log_name :要开关的log名称
* @sta :log开关状态
* @return :无
*/
void log4m_printf(log_level_e level,uint32_t log_id,const char *fmt, ...)
{
if(0 != (g_log_ctrl.switch_sta & log_id)) //判断形参log_id对应的log是否在总开关中使能
{
if(g_log_ctrl.level<=level||LEVEL_NONE==level) //判断形参输入的log等级是否大于默认等级,大于等于当前设定的log等级才允许输出
{
va_list argptr;
int name_len=0;
int16_t level_name_len=0;
int format_wr_len = 0;
int16_t log_msg_addr=0;
char log_name[LOG_NAME_LEN_LMT]={'\0'};
log_mutex_lock();
memset(g_log_msg_buff,0,sizeof(g_log_msg_buff));
if(LEVEL_NONE!=level)
{
level_name_len=strlen(g_log_level_map[level].level_name);//获取log level字符串长度
if(0!=level_name_len)
{
memcpy(g_log_msg_buff,g_log_level_map[level].level_name,level_name_len);//将log level字符串拷贝至buff中
}
}
if(LOG_ID_RAW!=log_id)
{
name_len=get_func_log_name(log_id,log_name); //获取log模块名称
if(name_len>0&&name_len<=LOG_NAME_LEN_LMT)
{
memcpy(g_log_msg_buff+level_name_len,log_name,name_len); //将能模块名字符串依次拷贝至buff中
}
}
log_msg_addr=name_len+level_name_len; //更新log信息在buff中的起始地址
va_start(argptr, fmt);
format_wr_len= vsnprintf((char *)(g_log_msg_buff+log_msg_addr),LOG_MSG_LEN_LMT,fmt, argptr);
va_end(argptr);
if(format_wr_len>0)
{
log4m_data_output((uint8_t *)g_log_msg_buff,log_msg_addr+format_wr_len);
}
log_mutex_unlock();
}
}
}
/**
* 功能:开关某个类型的log
* 参数:
* @log_name :要开关的log名称
* @sta :log开关状态
* @return :无
*/
void log4m_switch(char *log_name,log_switch_e sta)
{
uint16_t i;
for(i=0;i<(sizeof(func_log_map)/sizeof(func_log_map[0]));i++)
{
if(strstr(func_log_map[i].log_name,log_name)!=NULL)
{
if(LOG_DISABLE==sta)
{
g_log_ctrl.switch_sta=g_log_ctrl.switch_sta&(~func_log_map[i].log_id);
}
else
{
g_log_ctrl.switch_sta=g_log_ctrl.switch_sta|func_log_map[i].log_id;
}
}
}
}
void log4m_test(void)
{
log4m(LEVEL_NONE,"\r\n");
log4m(LEVEL_NONE,"\r\n");
log4m(LEVEL_NONE,"This is the raw log print,no level,and no log name,just msg\r\n");
log4adc(LEVEL_DEBUG,"adc value:\r\n",256);
log4bsp(LEVEL_WARN,"gpio set high\r\n");
log4spi(LEVEL_DEBUG,"SPI Init Ok\r\n");
log4i2c(LEVEL_INFO ,"i2c master read:%d\r\n",0x55aa);
log4can(LEVEL_WARN ,"can data overload\r\n");
log4dbg(LEVEL_INFO ,"debug test\r\n");
log4e2p(LEVEL_WARN,"eep write complete\r\n");
log4gps(LEVEL_INFO,"gps turn on ok\r\n");
log4gui(LEVEL_INFO,"gui show normal\r\n");
log4ble(LEVEL_ERROR,"ble broad fail\r\n");
log4mdm(LEVEL_INFO,"mdm shut down\r\n");
log4pmm(LEVEL_DEBUG,"system enter sleep\r\n");
log4rtc(LEVEL_WARN,"rtc run ok\r\n");
log4sys(LEVEL_DEBUG,"SPI Init Ok\r\n");
log4wdt(LEVEL_INFO,"feed watch dog\r\n");
log4wifi(LEVEL_ERROR,"wifi disconnect\r\n");
log4motor(LEVEL_DEBUG,"motor stoped\r\n");
}
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
总结:
通过串口实现了一个可以分模块,分等级打印的函数,方便log分类搜索查询,追踪问题,还可以进行静态开关log:
静态开关log:当把对应的LOG ID 宏定义注释掉后,该模块的LOG打印接口将不进行编译,节省代码空间。
动态开关log:可以在代码运行是调整log 总开关的状态将g_log_ctrl.switch_sta状态进行设置,来动态控制log的打印。
可以设定log等级和log名称。
可以选择无log等级输出:此时level使用LEVEL_NONE,此时输出的log中将不显示log等级。
可以选择无log名称输出:此时log id使用LOG_ID_RAW,此时输出的log中将log名称,这样可以打印原始log数据。
测试示例:
void log4m_test(void)
{
log4m(LEVEL_NONE,"\r\n");
log4m(LEVEL_NONE,"\r\n");
log4m(LEVEL_NONE,"This is the raw log print,no level,and no log name,just msg\r\n");
log4adc(LEVEL_DEBUG,"adc value:\r\n",256);
log4bsp(LEVEL_WARN,"gpio set high\r\n");
log4spi(LEVEL_DEBUG,"SPI Init Ok\r\n");
log4i2c(LEVEL_INFO ,"i2c master read:%d\r\n",0x55aa);
log4can(LEVEL_WARN ,"can data overload\r\n");
log4dbg(LEVEL_INFO ,"debug test\r\n");
log4e2p(LEVEL_WARN,"eep write complete\r\n");
log4gps(LEVEL_INFO,"gps turn on ok\r\n");
log4gui(LEVEL_INFO,"gui show normal\r\n");
log4ble(LEVEL_ERROR,"ble broad fail\r\n");
log4mdm(LEVEL_INFO,"mdm shut down\r\n");
log4pmm(LEVEL_DEBUG,"system enter sleep\r\n");
log4rtc(LEVEL_WARN,"rtc run ok\r\n");
log4sys(LEVEL_DEBUG,"SPI Init Ok\r\n");
log4wdt(LEVEL_INFO,"feed watch dog\r\n");
log4wifi(LEVEL_ERROR,"wifi disconnect\r\n");
log4motor(LEVEL_DEBUG,"motor stoped\r\n");
}