嵌入式调试LOG日志输出(以STM32为例)
引言
在嵌入式系统开发中,调试是贯穿整个生命周期的关键环节。与传统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 < s_ulog_level) return;// 2. 缓冲区初始化char log_buf[CONFIG_ULOG_BUF_SIZE] = {0};va_list args;int idx = 0;// 3. 拼接级别字符串(如[TRACE])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]);}// 4. 拼接时间戳(如[14:23:45.678])if (s_ulog_fmt & LOG_FMT_TIME_STAMP) {char time_buf[32];uint16_t ms = 0;Get_SystemTime(time_buf, sizeof(time_buf), &ms); // 基于HAL_GetTick获取时间idx += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[%s.%03d] ", time_buf, ms);}// 5. 拼接函数名+行号(如[main:45])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);}// 6. 填充日志内容(可变参数)va_start(args, fmt);int len = vsnprintf(log_buf + idx, sizeof(log_buf)-idx, fmt, args); // 格式化消息va_end(args);if (len > 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 <stdarg.h>
#include <stdint.h>
#include <string.h>/* 配置宏定义 */
#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 += snprintf(log_buf + idx, sizeof(log_buf)-idx, "[LOG FORMAT ERROR]");} else 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**********************/