嵌入式调试LOG日志输出(以STM32为例)_嵌入式日志log开发
引言
在嵌入式系统开发中,调试是贯穿整个生命周期的关键环节。与传统PC端程序不同,嵌入式设备资源受限(如内存、存储、处理器性能),且运行环境复杂(无显示器、键盘),传统的断点调试或打印到控制台的方式往往难以满足实时性、便捷性需求。此时,日志系统(LOG) 成为嵌入式调试的核心工具——它通过将关键运行信息输出到外部设备(如串口),帮助开发者快速定位问题、跟踪程序状态。
本文将以STM32F103系列单片机为例,结合实际工程实践,介绍一款轻量、灵活、易集成的日志系统设计与实现,涵盖日志分级、格式控制、串口输出等核心功能,并通过示例演示其在嵌入式调试中的具体应用。
嵌入式日志系统的核心需求
嵌入式场景下,日志系统需满足以下核心需求:
1. 资源友好性
STM32的内存(如STM32F103C8T6仅有20KB SRAM)和Flash空间有限,日志系统需避免占用过多资源。例如,日志缓冲区需固定大小(如256字节),避免动态内存分配;输出函数需轻量(如直接调用串口发送)。
2. 分级控制
不同调试阶段需要关注不同详细程度的信息。例如:
- 开发阶段:需要详细的函数调用、变量值(TRACE/DEBUG级别);
- 测试阶段:关注关键流程状态(INFO/WARN级别);
- 发布阶段:仅保留错误信息(ERROR/FATAL级别)。
因此,日志系统需支持级别过滤,通过配置只输出高于设定级别的日志。
3. 格式灵活性
日志需包含足够的上下文信息以辅助调试,但冗余信息会干扰阅读。常见的日志要素包括:
- 级别标识(如[TRACE]/[ERROR]):快速区分日志严重程度;
- 时间戳(如[14:23:45.678]):定位问题发生时刻;
- 函数名+行号(如[main:45]):追踪代码执行路径;
- 原始消息(如“文件打开失败”):具体问题描述。
日志系统需支持格式配置,允许用户按需组合上述要素。
4. 高效输出
嵌入式系统的串口带宽有限(如常见的115200bps,约11.5KB/s),日志输出需避免阻塞主程序。例如,采用非阻塞发送(或短时间阻塞)、控制单次输出数据量(不超过串口发送缓冲区)。
日志系统设计实现
基于上述需求,我们设计了一款基于STM32 HAL库的日志系统,核心功能包括日志分级、格式控制、串口输出,以下是关键模块的实现细节。
1. 日志级别定义
日志级别采用枚举类型定义,从低到高依次为TRACE
→DEBUG
→INFO
→WARN
→ERROR
→FATAL
,数值越小优先级越高。通过级别过滤,可灵活控制日志输出范围:
typedef enum { LOG_LEVEL_TRACE = 0, // 最低级别,用于最详细的跟踪信息 LOG_LEVEL_DEBUG, // 调试信息,开发阶段使用 LOG_LEVEL_INFO, // 重要状态信息,测试阶段使用 LOG_LEVEL_WARN, // 警告信息,提示潜在问题 LOG_LEVEL_ERROR, // 错误信息,功能异常但可恢复 LOG_LEVEL_FATAL, // 严重错误,系统可能崩溃 LOG_LEVEL_MAX // 枚举结束标志} log_level_t;
2. 日志格式控制
日志格式通过宏定义控制,支持按位或组合多种要素:
#define LOG_FMT_RAW (0u) // 仅原始消息(无额外信息)#define LOG_FMT_LEVEL_STR (1u << 0) // 级别字符串(如[TRACE])#define LOG_FMT_TIME_STAMP (1u << 1) // 时间戳(如[14:23:45.678])#define LOG_FMT_FUNC_LINE (1u << 2) // 函数名+行号(如[main:45])
用户可通过Ulog_SetFmt()
函数动态配置格式(例如LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP
表示输出级别和时间戳)。
3. 核心日志函数实现
日志系统的核心是Ulog()
函数,负责格式化日志内容并输出。其流程如下:
(1)级别过滤
首先检查当前日志级别是否低于设定的最低输出级别(如设置为LOG_LEVEL_INFO
时,TRACE
和DEBUG
日志会被过滤)。
(2)缓冲区初始化
使用固定大小的缓冲区(如256字节)存储日志内容,避免动态内存分配带来的风险。
(3)格式化要素拼接
根据配置的格式,依次拼接级别字符串、时间戳、函数名+行号等信息。例如:
- 级别字符串通过
level_str
数组映射(如LOG_LEVEL_TRACE
对应\"[TRACE]\"); - 时间戳基于
HAL_GetTick()
获取系统运行时间(毫秒级),格式化为[HH:MM:SS.xxx]
; - 函数名+行号通过
__func__
(编译器内置宏)和__LINE__
(行号宏)获取,并截断过长函数名(避免缓冲区溢出)。
(4)日志内容填充
使用va_list
处理可变参数,将用户输入的日志消息格式化到缓冲区中。
(5)输出日志
通过注册的输出函数(默认使用串口)将缓冲区内容发送到外部设备。
void Ulog(uint32_t level, const char *func, uint32_t line, const char *fmt, ...) { // 1. 级别过滤 if (level >= LOG_LEVEL_MAX || level 0) idx += len; // 有效内容则更新索引 // 7. 添加换行符(STM32串口常用\\r\\n) if (idx < CONFIG_ULOG_BUF_SIZE - 2) { snprintf(log_buf + idx, sizeof(log_buf)-idx, \"%s\", ULOG_NEWLINE_SIGN); idx += strlen(ULOG_NEWLINE_SIGN); } // 8. 输出日志(调用注册的串口发送函数) ulog_output((uint8_t *)log_buf, (uint16_t)idx);}
4. 串口输出实现
STM32的串口输出通过HAL库实现,核心是Uart_SendData()
函数,利用HAL_UART_Transmit()
发送数据。为避免阻塞,设置超时时间(如100ms):
// 串口句柄(需在stm32f1xx_hal_conf.h中启用USART1)extern UART_HandleTypeDef UartHandle;// 串口数据发送函数static void Uart_SendData(uint8_t *data, uint16_t size) { if (huart1.Instance != NULL) { HAL_UART_Transmit(&UartHandle, data, size, 100); // 超时100ms }}
5. 全局配置与接口
通过全局变量管理日志配置(如当前级别、格式、输出函数),并提供接口供用户动态修改:
// 全局配置static uint32_t s_ulog_fmt = LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE; // 默认格式static uint32_t s_ulog_level = CONFIG_ULOG_DEF_LEVEL; // 默认级别(TRACE)static UlogOutputFunc ulog_output = NULL; // 默认输出函数// 注册输出函数(默认使用串口)void Ulog_RegisterOutput(UlogOutputFunc func) { ulog_output = func ? func : Uart_SendData; // 未注册时使用串口}// 设置日志级别int Ulog_SetLevel(uint32_t level) { if (level >= LOG_LEVEL_MAX) return -1; s_ulog_level = level; return 0;}// 设置日志格式void Ulog_SetFmt(uint32_t fmt) { s_ulog_fmt = fmt;}
日志系统使用示例
以下通过一个完整的测试用例,演示日志系统的实际效果。
1. 工程配置
- 硬件连接:STM32F103 USART1(PA9-TX,PA10-RX)接USB转串口模块(波特率115200,8-N-1);
- 软件配置:在
main.c
中初始化HAL库、系统时钟、USART1,并注册串口输出函数。
2. 测试代码
/* Includes ------------------------------------------------------------------*/#include \"main.h\"#include \"stm32f1xx.h\"#include \"./usart/bsp_debug_usart.h\"#include \"./log/log.h\" /* 测试函数 */void Test_LogFunctions(void) { LOG_TRACE(\"开始测试日志功能\"); LOG_DEBUG(\"调试信息 - 变量值: %d\", 100); LOG_INFO(\"系统初始化完成\"); LOG_WARN(\"内存使用率高达85%%\"); LOG_ERROR(\"文件打开失败: %s\", \"test.log\"); LOG_FATAL(\"核心模块初始化失败,系统即将终止\");}void Test_LogRunTimeDebug(void){static uint32_t u32Cnt = 0;u32Cnt++;LOG_DEBUG(\"系统运行中,%04d\", u32Cnt);}int main(void) { HAL_Init(); /* 配置系统时钟为72 MHz */ SystemClock_Config(); /*初始化USART 配置模式为 115200 8-N-1,中断接收*/DEBUG_USART_Config(); /* 注册串口输出函数 */ Ulog_RegisterOutput(Uart_SendData); /* 测试完整格式日志 (级别+时间+行号) */Ulog_SetFmt(LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE); printf(\"=== 测试完整格式日志 (级别+时间+行号) ===\\r\\n\"); Test_LogFunctions(); /* 测试基本格式(级别+时间) */ Ulog_SetFmt(LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP); printf(\"\\r\\n=== 测试基本格式(级别+时间) ===\\r\\n\"); Test_LogFunctions();/* 测试基本格式(级别) */ Ulog_SetFmt(LOG_FMT_LEVEL_STR); printf(\"\\r\\n=== 测试基本格式(级别) ===\\r\\n\"); Test_LogFunctions(); /* 测试原始格式(仅消息内容) */ Ulog_SetFmt(LOG_FMT_RAW); printf(\"\\r\\n=== 测试原始格式(仅消息内容) ===\\r\\n\"); Test_LogFunctions();//显示运行时Debug数据Ulog_SetFmt(LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE);printf(\"\\r\\n=== 显示运行时Debug数据 ===\\r\\n\"); while(1) { HAL_Delay(1000); // 主循环保持运行Test_LogRunTimeDebug(); }}
完整代码
log.h
#ifndef __LOG_H#define__LOG_H#include \"stm32f1xx.h\"#include \"stm32f1xx_hal.h\"#include #include #include /* 配置宏定义 */#define CONFIG_ULOG_BUF_SIZE 256u#define CONFIG_ULOG_DEF_LEVEL LOG_LEVEL_TRACE#define ULOG_NEWLINE_SIGN \"\\r\\n\" // STM32串口常用换行符/* 日志级别枚举 */typedef enum { LOG_LEVEL_TRACE = 0, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARN, LOG_LEVEL_ERROR, LOG_LEVEL_FATAL, LOG_LEVEL_MAX} log_level_t;/* 格式控制宏 */#define LOG_FMT_RAW (0u)#define LOG_FMT_LEVEL_STR (1u << 0)#define LOG_FMT_TIME_STAMP (1u << 1)#define LOG_FMT_FUNC_LINE (1u << 2)/* 启用日志级别开关 */#define LOG_TRACE_EN 1#define LOG_DEBUG_EN 1#define LOG_INFO_EN 1#define LOG_WARN_EN 1#define LOG_ERROR_EN 1#define LOG_FATAL_EN 1/* 日志宏定义 */#if LOG_TRACE_EN#define LOG_TRACE(fmt, ...) Ulog(LOG_LEVEL_TRACE, __func__, __LINE__, fmt, ##__VA_ARGS__)#else#define LOG_TRACE(fmt, ...)#endif#if LOG_DEBUG_EN#define LOG_DEBUG(fmt, ...) Ulog(LOG_LEVEL_DEBUG, __func__, __LINE__, fmt, ##__VA_ARGS__)#else#define LOG_DEBUG(fmt, ...)#endif#if LOG_INFO_EN#define LOG_INFO(fmt, ...) Ulog(LOG_LEVEL_INFO, __func__, __LINE__, fmt, ##__VA_ARGS__)#else#define LOG_INFO(fmt, ...)#endif#if LOG_WARN_EN#define LOG_WARN(fmt, ...) Ulog(LOG_LEVEL_WARN, __func__, __LINE__, fmt, ##__VA_ARGS__)#else#define LOG_WARN(fmt, ...)#endif#if LOG_ERROR_EN#define LOG_ERROR(fmt, ...) Ulog(LOG_LEVEL_ERROR, __func__, __LINE__, fmt, ##__VA_ARGS__)#else#define LOG_ERROR(fmt, ...)#endif#if LOG_FATAL_EN#define LOG_FATAL(fmt, ...) Ulog(LOG_LEVEL_FATAL, __func__, __LINE__, fmt, ##__VA_ARGS__)#else#define LOG_FATAL(fmt, ...)#endif/* 日志输出函数类型 */typedef void (*UlogOutputFunc)(uint8_t *data, uint16_t size);extern void Ulog_RegisterOutput(UlogOutputFunc func);extern void Ulog_SetFmt(uint32_t fmt);extern void Ulog(uint32_t level, const char *func, uint32_t line, const char *fmt, ...);#endif /* __LOG_H */
log.c
#include \"./usart/bsp_debug_usart.h\"#include \"./log/log.h\" /* 全局配置 */static uint32_t s_ulog_fmt = LOG_FMT_LEVEL_STR | LOG_FMT_TIME_STAMP | LOG_FMT_FUNC_LINE;static uint32_t s_ulog_level = CONFIG_ULOG_DEF_LEVEL;static UlogOutputFunc ulog_output = NULL;static void Get_SystemTime(char *time_buf, uint16_t buf_size, uint16_t *ms);/* 注册输出函数(默认使用串口) */void Ulog_RegisterOutput(UlogOutputFunc func) { ulog_output = func ? func : Uart_SendData;}/* 设置日志格式 */void Ulog_SetFmt(uint32_t fmt) { s_ulog_fmt = fmt;}/* 系统时间获取(基于HAL_GetTick) */static void Get_SystemTime(char *time_buf, uint16_t buf_size, uint16_t *ms) { uint32_t tick = HAL_GetTick(); // 获取系统运行时间(毫秒) *ms = tick % 1000; uint32_t sec = tick / 1000; uint32_t hour = sec / 3600; uint32_t min = (sec % 3600) / 60; sec = sec % 60; snprintf(time_buf, buf_size, \"%02d:%02d:%02d\", (int)(hour % 24), (int)min, (int)sec);}/* 核心日志函数 */void Ulog(uint32_t level, const char *func, uint32_t line, const char *fmt, ...) { /* 级别过滤 */ if (level >= LOG_LEVEL_MAX || level < s_ulog_level) return; /* 缓冲区初始化 */ char log_buf[CONFIG_ULOG_BUF_SIZE] = {0}; va_list args; int idx = 0; /* 级别字符串 */ if (s_ulog_fmt & LOG_FMT_LEVEL_STR) { static const char *level_str[] = {\"TRACE\", \"DEBUG\", \"INFO \", \"WARN \", \"ERROR\", \"FATAL\"}; idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, \"[%s] \", level_str[level]); } /* 时间戳 */ if (s_ulog_fmt & LOG_FMT_TIME_STAMP) { char time_buf[32]; uint16_t ms = 0; Get_SystemTime(time_buf, sizeof(time_buf), &ms); idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, \"[%s.%03d] \", time_buf, ms); } /* 函数名+行号 */ if (s_ulog_fmt & LOG_FMT_FUNC_LINE) { char short_func[20] = {0}; strncpy(short_func, func, sizeof(short_func)-1); idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, \"[%s:%d] \", short_func, (int)line); } /* 日志内容 */ va_start(args, fmt); int len = vsnprintf(log_buf + idx, sizeof(log_buf)-idx, fmt, args); va_end(args); /* 处理格式化错误 */ if (len 0) { idx += len; } /* 添加换行符 */ if (idx < CONFIG_ULOG_BUF_SIZE - 2) { snprintf(log_buf + idx, sizeof(log_buf)-idx, \"%s\", ULOG_NEWLINE_SIGN); idx += strlen(ULOG_NEWLINE_SIGN); } /* 输出日志 */ ulog_output((uint8_t *)log_buf, (uint16_t)idx);}/*********************************************END OF FILE**********************/