引言
在嵌入式系统开发中,调试是贯穿整个生命周期的关键环节。与传统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
,数值越小优先级越高。通过级别过滤,可灵活控制日志输出范围:
cpp
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. 日志格式控制
日志格式通过宏定义控制,支持按位或组合多种要素:
cpp
#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)输出日志
通过注册的输出函数(默认使用串口)将缓冲区内容发送到外部设备。
cpp
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):
cpp
// 串口句柄(需在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. 全局配置与接口
通过全局变量管理日志配置(如当前级别、格式、输出函数),并提供接口供用户动态修改:
cpp
// 全局配置
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. 测试代码
cpp
/* 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
cpp
#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
cpp
#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**********************/