使用C语言,基于STM32实现一个串口打印日志口,要求日志按照等级打印,每个打印内容要包含当前输出的函数名
时间: 2025-07-24 20:37:01 浏览: 1
<think>我们参考引用[3]和引用[4]中的UART初始化以及ESP8266初始化的代码结构,但我们的目标是实现一个串口日志打印功能,因此重点在如何格式化日志信息并通过串口输出。
需求分析:
1. 支持日志等级(如DEBUG, INFO, WARN, ERROR等)
2. 包含函数名信息
3. 通过串口输出
设计思路:
1. 定义日志等级枚举,并为每个等级设置一个字符串标签。
2. 使用宏定义来简化日志调用,并自动获取函数名(使用预定义宏__FUNCTION__或__func__)。
3. 初始化UART(参考引用[3]的UART初始化代码)。
4. 实现一个日志打印函数,该函数将日志等级、函数名和用户格式化的消息组合成一条日志,并通过串口发送。
注意:由于在嵌入式环境中,我们通常希望避免使用标准库中耗资源的函数(如printf),但为了简化,我们可以使用printf重定向到串口(如果资源允许)。或者使用自己的串口发送函数。
步骤:
1. 初始化UART(使用HAL库,如引用[3]所示)。
2. 重定向printf(可选,如果使用printf的话)或者实现一个轻量级的打印函数。
3. 定义日志宏。
考虑到资源,我们可以不使用printf重定向,而是自己实现一个格式化的函数(但这样比较复杂)。另一种方法是使用sprintf将格式化后的字符串输出到一个缓冲区,然后用串口发送该缓冲区。但需要注意缓冲区大小。
由于STM32资源有限,我们建议使用一个固定大小的缓冲区,并注意防止溢出。
实现方案:
步骤1:UART初始化(参考引用[3])
步骤2:定义日志等级
步骤3:定义日志宏,该宏将展开为调用一个日志输出函数,传递日志等级、函数名和格式字符串以及可变参数。
注意:__FUNCTION__在编译时会被替换为当前函数的名称(字符串)。
示例代码结构:
1. 定义日志等级枚举和标签数组:
typedef enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARN,
LOG_ERROR
} LogLevel;
static const char *log_level_strings[] = {
"DEBUG",
"INFO",
"WARN",
"ERROR"
};
2. 实现日志输出函数:
void log_output(LogLevel level, const char *func, const char *format, ...) {
char buffer[256]; // 固定大小的缓冲区,注意防止溢出
va_list args;
va_start(args, format);
// 先打印日志等级和函数名
int pos = snprintf(buffer, sizeof(buffer), "[%s] <%s>: ", log_level_strings[level], func);
if (pos < sizeof(buffer)) {
vsnprintf(buffer + pos, sizeof(buffer) - pos, format, args);
}
va_end(args);
// 通过串口发送buffer
HAL_UART_Transmit(&huart2, (uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY);
// 如果需要换行,可以加上\r\n
HAL_UART_Transmit(&huart2, (uint8_t *)"\r\n", 2, HAL_MAX_DELAY);
}
3. 定义宏:
#define LOG(level, format, ...) log_output(level, __FUNCTION__, format, ##__VA_ARGS__)
4. 使用宏进行日志打印:
LOG(LOG_DEBUG, "This is a debug message: %d", 123);
但是,我们可能希望不同等级有不同的宏,便于使用和条件编译(例如在发布时关闭调试信息):
#define LOG_DEBUG(format, ...) LOG(LOG_DEBUG, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...) LOG(LOG_INFO, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...) LOG(LOG_WARN, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) LOG(LOG_ERROR, format, ##__VA_ARGS__)
5. 条件编译:我们可以通过定义日志级别阈值,在编译时过滤掉低于该阈值的日志。例如:
#define LOG_LEVEL_THRESHOLD LOG_INFO
然后修改宏:
#if LOG_LEVEL_THRESHOLD <= LOG_DEBUG
#define LOG_DEBUG(format, ...) log_output(LOG_DEBUG, __FUNCTION__, format, ##__VA_ARGS__)
#else
#define LOG_DEBUG(format, ...)
#endif
同样处理其他级别。
6. 注意:使用可变参数函数(va_list)会占用一定的栈空间,并且snprintf和vsnprintf可能会消耗较多资源(如果使用标准库,且没有使用微库(microLIB)的话)。在资源紧张的系统中,可以考虑简化格式化功能,或者使用更轻量级的实现。
7. 重定向printf(可选):
如果已经重定向了printf,那么我们可以直接使用printf,但这里我们为了加入日志等级和函数名,还是使用自定义的日志函数。
8. 关于串口发送:我们使用HAL_UART_Transmit,注意这个函数是阻塞的。在实时性要求高的场合,可以考虑使用中断或DMA方式。
9. 考虑到引用[4]中使用了DMA接收,但发送我们暂时使用阻塞方式,因为日志打印通常不需要非常高的实时性,且阻塞发送简单可靠。
10. 如果使用DMA发送,则需要注意缓冲区管理和发送完成回调,避免日志覆盖。
综合考虑,我们给出一个基础版本(阻塞发送)的实现。
代码示例:
注意:以下代码需要根据具体硬件调整UART句柄(如huart2),并确保已经初始化了该UART。
步骤:
1. 将日志模块的头文件和源文件分别创建。
2. 在头文件中声明日志函数和宏。
3. 在源文件中实现。
日志模块头文件(log.h)示例:
```
#ifndef __LOG_H
#define __LOG_H
#include "stm32f1xx_hal.h" // 根据实际使用的STM32系列包含对应的HAL头文件
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
typedef enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARN,
LOG_ERROR
} LogLevel;
// 外部定义的UART句柄,用于发送日志
extern UART_HandleTypeDef huart2; // 假设使用USART2
// 条件编译的日志等级阈值,可以在编译选项中定义,也可以在这里定义
#ifndef LOG_LEVEL_THRESHOLD
#define LOG_LEVEL_THRESHOLD LOG_DEBUG // 默认所有日志都输出
#endif
void log_output(LogLevel level, const char *func, const char *format, ...);
// 根据日志等级阈值定义宏
#if LOG_LEVEL_THRESHOLD <= LOG_DEBUG
#define LOG_DEBUG(format, ...) log_output(LOG_DEBUG, __FUNCTION__, format, ##__VA_ARGS__)
#else
#define LOG_DEBUG(format, ...)
#endif
#if LOG_LEVEL_THRESHOLD <= LOG_INFO
#define LOG_INFO(format, ...) log_output(LOG_INFO, __FUNCTION__, format, ##__VA_ARGS__)
#else
#define LOG_INFO(format, ...)
#endif
#if LOG_LEVEL_THRESHOLD <= LOG_WARN
#define LOG_WARN(format, ...) log_output(LOG_WARN, __FUNCTION__, format, ##__VA_ARGS__)
#else
#define LOG_WARN(format, ...)
#endif
#if LOG_LEVEL_THRESHOLD <= LOG_ERROR
#define LOG_ERROR(format, ...) log_output(LOG_ERROR, __FUNCTION__, format, ##__VA_ARGS__)
#else
#define LOG_ERROR(format, ...)
#endif
#endif /* __LOG_H */
```
日志模块源文件(log.c)示例:
```
#include "log.h"
static const char *log_level_strings[] = {
"DEBUG",
"INFO",
"WARN",
"ERROR"
};
// 注意:huart2需要在外部定义(例如在main.c中定义并初始化)
extern UART_HandleTypeDef huart2;
void log_output(LogLevel level, const char *func, const char *format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
// 打印日志头:等级和函数名
int head_len = snprintf(buffer, sizeof(buffer), "[%s] <%s>: ", log_level_strings[level], func);
if (head_len < 0 || head_len >= sizeof(buffer)) {
va_end(args);
return;
}
// 打印日志内容
int content_len = vsnprintf(buffer + head_len, sizeof(buffer) - head_len, format, args);
va_end(args);
// 检查总长度
int total_len = head_len + content_len;
if (total_len < 0) {
return;
} else if (total_len >= sizeof(buffer)) {
total_len = sizeof(buffer) - 1;
}
// 添加换行符(覆盖缓冲区末尾,确保有空间)
if (total_len <= sizeof(buffer) - 2) {
buffer[total_len] = '\r';
buffer[total_len+1] = '\n';
total_len += 2;
} else {
// 如果已经满了,则覆盖最后两个字符为换行(或者不换行?这里选择覆盖最后两个)
buffer[sizeof(buffer)-2] = '\r';
buffer[sizeof(buffer)-1] = '\n';
total_len = sizeof(buffer);
}
// 通过串口发送
HAL_UART_Transmit(&huart2, (uint8_t *)buffer, total_len, HAL_MAX_DELAY);
}
```
注意:
1. 缓冲区大小固定为256,如果日志信息过长会被截断,并确保最后有换行符。
2. 换行符使用"\r\n"(Windows风格),因为很多串口终端需要。
3. 使用vsnprintf和snprintf需要标准库支持,如果使用微库(在Keil中可以选择),则可以使用。如果资源非常紧张,可以考虑自己实现简单的整数和字符串格式化。
使用示例:
在main.c中,初始化UART(如引用[3]所示),然后包含log.h,就可以使用:
LOG_DEBUG("System start, version: %s", "1.0");
LOG_INFO("Initialization completed.");
条件编译:如果在编译前定义LOG_LEVEL_THRESHOLD为LOG_INFO,则LOG_DEBUG不会输出。
优化方向:
1. 使用DMA发送日志,避免阻塞。
2. 使用环形缓冲区和后台发送(例如在中断中发送),实现非阻塞日志。
3. 添加时间戳(需要RTC或系统时钟支持)。
根据引用[4]中,他们配置了SNTP服务器,如果系统需要网络时间,可以添加时间戳。但这里我们先实现基本功能。
注意:在中断中使用日志函数要小心,因为HAL_UART_Transmit是阻塞的,且如果使用标准库函数(如snprintf)可能不可重入。因此,建议在中断中不要直接使用日志宏,而是将日志信息放入队列,由主循环处理。
以上实现为一个基础版本,可以满足基本需求。</think>### 带日志等级和函数名标识的串口日志模块实现
在STM32上实现带日志等级和函数名标识的串口日志输出模块,可通过以下步骤完成:
#### 1. **日志等级定义**
```c
// log_level.h
typedef enum {
LOG_DEBUG, // 调试信息
LOG_INFO, // 普通信息
LOG_WARN, // 警告信息
LOG_ERROR, // 错误信息
LOG_CRITICAL // 严重错误
} LogLevel;
```
#### 2. **核心日志函数实现**
```c
// logger.c
#include "logger.h"
#include <stdarg.h>
#include <string.h>
// 外部声明的UART句柄(根据实际硬件修改)
extern UART_HandleTypeDef huart2;
void log_output(LogLevel level, const char* func, const char* format, ...) {
// 日志等级标签
const char* level_str[] = {
"[DEBUG]", "[INFO]", "[WARN]", "[ERROR]", "[CRIT]"
};
char buffer[256];
int pos = 0;
// 添加等级标签
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s ", level_str[level]);
// 添加函数名
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "<%s> ", func);
// 添加用户消息
va_list args;
va_start(args, format);
pos += vsnprintf(buffer + pos, sizeof(buffer) - pos, format, args);
va_end(args);
// 添加换行符
if (pos < sizeof(buffer) - 2) {
buffer[pos++] = '\r';
buffer[pos++] = '\n';
}
// 通过串口发送
HAL_UART_Transmit(&huart2, (uint8_t*)buffer, pos, HAL_MAX_DELAY);
}
```
#### 3. **日志宏定义**
```c
// logger.h
#pragma once
#include "log_level.h"
// 使用编译器内置宏获取函数名
#define LOG(level, format, ...) \
log_output(level, __FUNCTION__, format, ##__VA_ARGS__)
// 快捷宏定义
#define LOG_DEBUG(format, ...) LOG(LOG_DEBUG, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...) LOG(LOG_INFO, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...) LOG(LOG_WARN, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) LOG(LOG_ERROR, format, ##__VA_ARGS__)
```
#### 4. **UART初始化**
参考引用[3]初始化UART外设:
```c
// main.c
#include "stm32f1xx_hal.h"
UART_HandleTypeDef huart2;
void UART_Init() {
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&huart2);
}
```
#### 5. **使用示例**
```c
void system_init() {
LOG_DEBUG("Initializing peripherals");
if (HAL_TIM_Base_Start(&htim) != HAL_OK) {
LOG_ERROR("Timer initialization failed!");
}
LOG_INFO("System initialized successfully");
}
int main() {
UART_Init();
system_init();
while(1) {
LOG_DEBUG("Main loop iteration");
HAL_Delay(1000);
}
}
```
#### 6. **输出示例**
```
[DEBUG] <system_init> Initializing peripherals
[ERROR] <system_init> Timer initialization failed!
[INFO] <system_init> System initialized successfully
[DEBUG] <main> Main loop iteration
```
### 关键优化点
1. **条件编译控制**:
```c
// 在logger.h中添加
#ifdef DISABLE_DEBUG_LOGS
#undef LOG_DEBUG
#define LOG_DEBUG(...)
#endif
```
2. **异步发送优化**:
```c
// 使用DMA发送避免阻塞
HAL_UART_Transmit_DMA(&huart2, (uint8_t*)buffer, pos);
```
3. **时间戳支持**:
```c
// 添加RTC时间戳
char timestamp[12];
snprintf(timestamp, sizeof(timestamp), "[%02d:%02d:%02d]", hours, mins, secs);
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s ", timestamp);
```
4. **线程安全保护**:
```c
// 在RTOS环境中添加互斥锁
osMutexAcquire(log_mutex, osWaitForever);
HAL_UART_Transmit(...);
osMutexRelease(log_mutex);
```
### 注意事项
1. 缓冲区大小(256字节)可根据芯片RAM调整
2. `__FUNCTION__` 宏在IAR/Keil/GCC中均支持
3. 使用 `snprintf` 防止缓冲区溢出
4. 确保UART初始化正确(波特率、停止位等)[^3]
此实现提供了可扩展的日志框架,通过宏定义简化调用,自动捕获函数名,并支持日志分级过滤,适用于各类STM32项目开发[^4]。
阅读全文
相关推荐



















