1. STM32L475串口打印模块封装实战
在嵌入式开发中,串口打印是最基础也最常用的调试手段。但直接使用标准库的printf会导致代码体积暴增,这对于资源有限的STM32L475(Flash仅512KB)来说是不可接受的。本文将分享如何基于HAL库封装一个精简高效的串口打印模块,从硬件初始化到软件实现,完整呈现一个可用于生产环境的解决方案。
2. 硬件层串口初始化
2.1 硬件初始化代码解析
首先我们需要完成USART1的硬件初始化。在bsp_uart.h中定义硬件相关参数:
c复制#ifndef BSP_UART_H
#define BSP_UART_H
#include "stm32l4xx_hal.h"
#include <stdint.h>
// 串口波特率
#define BR_115200 ((uint32_t)115200)
// 调试串口配置
#define DEBUG_UART USART1
#define DEBUG_UART_CLK_ENABLE() __HAL_RCC_USART1_CLK_ENABLE()
#define DEBUG_UART_CLK_DISABLE() __HAL_RCC_USART1_CLK_DISABLE()
// GPIO配置
#define DEBUG_UART_GPIO_PORT GPIOA
#define DEBUG_UART_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define DEBUG_UART_GPIO_CLK_DISABLE() __HAL_RCC_GPIOA_CLK_DISABLE()
#define DEBUG_UART_TX_PIN GPIO_PIN_9
#define DEBUG_UART_RX_PIN GPIO_PIN_10
#define DEBUG_UART_AF GPIO_AF7_USART1
void BSP_UART_Init(uint32_t baudrate);
void BSP_UART_DeInit(void);
#endif
这里有几个关键点需要注意:
- USART1使用GPIOA的PA9(TX)和PA10(RX)引脚
- 复用功能选择AF7(STM32L4系列USART1的专用AF编号)
- 波特率作为参数传入,方便后期调整
2.2 初始化函数实现
在bsp_uart.c中实现初始化函数:
c复制#include "bsp_uart.h"
void BSP_UART_Init(uint32_t baudrate) {
GPIO_InitTypeDef GPIO_InitData = {0};
// 1. 开启时钟
DEBUG_UART_GPIO_CLK_ENABLE();
DEBUG_UART_CLK_ENABLE();
// 2. 配置TX引脚
GPIO_InitData.Pin = DEBUG_UART_TX_PIN;
GPIO_InitData.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_InitData.Pull = GPIO_PULLUP; // 默认拉高
GPIO_InitData.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 高速模式
GPIO_InitData.Alternate = DEBUG_UART_AF;
HAL_GPIO_Init(DEBUG_UART_GPIO_PORT, &GPIO_InitData);
// 3. 配置RX引脚
GPIO_InitData.Pin = DEBUG_UART_RX_PIN;
HAL_GPIO_Init(DEBUG_UART_GPIO_PORT, &GPIO_InitData);
// 4. 配置UART参数
DEBUG_UART->CR1 &= ~USART_CR1_UE; // 禁用串口以配置
// 波特率计算
uint32_t pclk = HAL_RCC_GetPCLK2Freq();
DEBUG_UART->BRR = (pclk + (baudrate / 2)) / baudrate; // 带四舍五入
// 8N1配置
DEBUG_UART->CR1 = USART_CR1_TE | USART_CR1_RE; // 使能发送接收
DEBUG_UART->CR2 = 0x0000; // 1个停止位
DEBUG_UART->CR3 = 0x0000; // 无硬件流控
DEBUG_UART->CR1 |= USART_CR1_UE; // 使能串口
}
注意:这里直接操作寄存器而非使用HAL库函数,效率更高。但需要确保理解每个寄存器的含义。
2.3 反初始化函数实现
产品发布时需要彻底关闭串口以节省功耗:
c复制void BSP_UART_DeInit(void) {
GPIO_InitTypeDef GPIO_InitData = {0};
// 1. 禁用串口
if(READ_BIT(DEBUG_UART->CR1, USART_CR1_UE) != RESET) {
DEBUG_UART->CR1 &= ~(USART_CR1_UE | USART_CR1_TE | USART_CR1_RE);
DEBUG_UART->CR2 = 0x0000;
DEBUG_UART->CR3 = 0x0000;
DEBUG_UART->BRR = 0x0000;
}
// 2. 配置GPIO为高阻态
GPIO_InitData.Mode = GPIO_MODE_INPUT;
GPIO_InitData.Pull = GPIO_NOPULL;
GPIO_InitData.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitData.Pin = DEBUG_UART_TX_PIN;
HAL_GPIO_Init(DEBUG_UART_GPIO_PORT, &GPIO_InitData);
GPIO_InitData.Pin = DEBUG_UART_RX_PIN;
HAL_GPIO_Init(DEBUG_UART_GPIO_PORT, &GPIO_InitData);
// 3. 关闭时钟
DEBUG_UART_GPIO_CLK_DISABLE();
DEBUG_UART_CLK_DISABLE();
}
3. 精简版printf实现
3.1 为什么需要精简printf
标准库的printf会带来约40KB的代码体积增长,这对于只有512KB Flash的STM32L475来说太过奢侈。我们选用第三方开源实现mpaland/printf,通过裁剪不需要的功能,可以将其体积控制在2KB左右。
3.2 printf功能裁剪
在printf.h中添加以下定义来禁用不常用功能:
c复制// 禁用浮点数支持
#define PRINTF_DISABLE_SUPPORT_FLOAT
// 禁用科学计数法
#define PRINTF_DISABLE_SUPPORT_EXPONENTIAL
// 禁用long long支持
#define PRINTF_DISABLE_SUPPORT_LONG_LONG
// 禁用ptrdiff_t支持
#define PRINTF_DISABLE_SUPPORT_PTRDIFF_T
3.3 实现字符发送接口
printf库需要我们自己实现_putchar函数:
c复制void BSP_UART_SendByte(uint8_t byte) {
// 自动将\n转换为\r\n
if (byte == '\n') {
while (!(DEBUG_UART->ISR & USART_ISR_TXE));
DEBUG_UART->TDR = '\r';
}
while (!(DEBUG_UART->ISR & USART_ISR_TXE));
DEBUG_UART->TDR = byte;
}
void _putchar(char character) {
BSP_UART_SendByte((uint8_t)character);
}
这里有两个关键点:
- 自动将\n转换为\r\n,确保终端显示正常
- 使用寄存器直接操作而非HAL库函数,提高效率
4. 分级日志系统实现
4.1 日志等级配置
在trace_cfg.h中定义日志等级:
c复制#ifndef TRACE_CFG_H
#define TRACE_CFG_H
// 全局开关
#define TRACE_ENABLE 1
// 日志等级
#define TRACE_LEVEL_ERROR 1
#define TRACE_LEVEL_WARN 2
#define TRACE_LEVEL_INFO 3
#define TRACE_LEVEL_DEBUG 4
// 当前编译等级
#define TRACE_LEVEL TRACE_LEVEL_DEBUG
#endif
4.2 日志宏定义
在trace.h中实现分级日志宏:
c复制#ifndef TRACE_H
#define TRACE_H
#include "trace_cfg.h"
#include "stm32l4xx_hal.h"
#if TRACE_ENABLE
void trace_printf(const char *fmt, ...);
uint32_t trace_get_timestamp_ms(void);
#define TRACE_ERROR(fmt, ...) \
do { \
if (TRACE_LEVEL >= TRACE_LEVEL_ERROR) \
trace_printf("[E] " fmt "\r\n", ##__VA_ARGS__); \
} while (0)
#define TRACE_WARN(fmt, ...) \
do { \
if (TRACE_LEVEL >= TRACE_LEVEL_WARN) \
trace_printf("[W] " fmt "\r\n", ##__VA_ARGS__); \
} while (0)
#define TRACE_INFO(fmt, ...) \
do { \
if (TRACE_LEVEL >= TRACE_LEVEL_INFO) \
trace_printf("[I] " fmt "\r\n", ##__VA_ARGS__); \
} while (0)
#define TRACE_DEBUG(fmt, ...) \
do { \
if (TRACE_LEVEL >= TRACE_LEVEL_DEBUG) { \
trace_printf("[D][%lums][%s:%d] " fmt "\r\n", \
trace_get_timestamp_ms(), \
__FILE__, \
__LINE__, \
##__VA_ARGS__); \
} \
} while (0)
#else // TRACE_ENABLE == 0
#define TRACE_ERROR(...) ((void)0)
#define TRACE_WARN(...) ((void)0)
#define TRACE_INFO(...) ((void)0)
#define TRACE_DEBUG(...) ((void)0)
#endif // TRACE_ENABLE
#endif // TRACE_H
4.3 日志函数实现
在trace.c中实现底层函数:
c复制#include "trace.h"
#include "printf.h"
#include <stdarg.h>
#if TRACE_ENABLE
void trace_printf(const char *fmt, ...) {
va_list va;
va_start(va, fmt);
vprintf(fmt, va);
va_end(va);
}
uint32_t trace_get_timestamp_ms(void) {
return HAL_GetTick();
}
#endif
5. 实际使用示例
5.1 初始化与使用
c复制#include "main.h"
#include "bsp_clock.h"
#include "bsp_uart.h"
#include "trace.h"
int main(void) {
HAL_Init();
SystemClock_Config();
BSP_UART_Init(BR_115200);
int counter = 0;
while (1) {
counter++;
TRACE_DEBUG("this is debug working %d", counter);
TRACE_INFO("this is info working %d", counter);
TRACE_ERROR("this is error working %d", counter);
TRACE_WARN("this is warning working %d", counter);
HAL_Delay(1000);
}
}
5.2 输出效果
code复制[D][7093ms][..\Src\main.c:16] this is debug working 8
[I] this is info working 8
[E] this is error working 8
[W] this is warning working 8
[D][8106ms][..\Src\main.c:16] this is debug working 9
[I] this is info working 9
[E] this is error working 9
[W] this is warning working 9
6. 性能优化与注意事项
6.1 性能优化技巧
- 中断发送:当前实现是轮询方式,可以改为DMA或中断方式提高效率
- 缓冲机制:添加环形缓冲区减少等待时间
- 时间戳精度:使用定时器获取更高精度的时间戳
6.2 常见问题排查
-
无输出:
- 检查时钟是否使能
- 确认GPIO模式配置正确(AF_PP)
- 测量TX引脚是否有波形
-
乱码:
- 检查波特率计算是否正确
- 确认两端波特率一致
- 检查时钟源配置
-
调试完成后:
- 发布版本时通过TRACE_ENABLE宏完全禁用日志
- 调用BSP_UART_DeInit彻底关闭串口硬件
7. 生产环境建议
- 日志分级管理:发布版本设置为TRACE_LEVEL_ERROR,只保留关键错误信息
- 功耗优化:在低功耗应用中,调试完成后彻底关闭串口外设
- 安全性:避免在生产代码中打印敏感信息
- 内存占用:定期检查.map文件,确认printf库的体积符合预期
这套封装已经在多个STM32L4系列产品中实际应用,在保持调试便利性的同时,将代码体积增加控制在3KB以内,是资源受限嵌入式系统的理想选择。
