引言
在嵌入式系統開發中,調試是貫穿整個生命周期的關鍵環節。與傳統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**********************/