STM32 USART串口通信配置与printf重定向实战

1. 从零开始配置USART串口通信

作为一名嵌入式开发者,我清楚地记得第一次在STM32上实现串口打印时的兴奋感。那闪烁的LED灯和终端上跳出的"Hello World"字样,标志着我真正踏入了嵌入式开发的大门。本文将带你完整走一遍USART串口配置的全流程,从CubeMX基础设置到printf重定向的实现细节。

1.1 硬件连接与基础概念

在开始软件配置前,我们需要明确硬件连接。STM32的USART接口通常使用PA9(TX)和PA10(RX)引脚,这是大多数开发板的默认配置。如果你使用的是常见的CH340或FTDI串口转换芯片,连接方式如下:

  • STM32 TX → CH340 RX
  • STM32 RX → CH340 TX
  • 共地连接(GND)

重要提示:务必避免直接将3.3V与5V系统直连,若转换芯片是5V电平,需要在信号线上添加电平转换电路。

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)与UART的主要区别在于时钟线。USART支持同步模式(需要时钟信号),而UART是纯异步的。但在大多数单片机应用中,我们只使用异步模式,这时两者功能几乎相同。

1.2 CubeMX工程创建与时钟配置

启动CubeMX后,按以下步骤操作:

  1. 选择正确的STM32型号(如STM32F103C8T6)
  2. 在Pinout视图中启用USART1:
    • Mode: Asynchronous
    • Hardware Flow Control: Disable

时钟配置是关键环节。以STM32F103为例,外部晶振通常为8MHz,我们需要通过PLL倍频到72MHz系统时钟。在Clock Configuration标签页中:

  1. 在HSE输入框输入8
  2. 设置PLLMUL为9倍频
  3. 选择PLL作为系统时钟源
  4. 确保APB2 Prescaler为1(USART1挂载在APB2上)

正确配置后,USART1的时钟应该是72MHz,这将直接影响后续波特率的计算精度。

1.3 串口参数详细设置

在Configuration标签页中点击USART1,弹出配置窗口需要关注以下参数:

  • Baud Rate: 115200(常用值,与PC端串口调试助手匹配)
  • Word Length: 8 bits
  • Parity: None
  • Stop Bits: 1
  • Over Sampling: 16 samples

这些参数必须与通信对端严格一致。特别是波特率误差,STM32的USART波特率计算公式为:

波特率 = fCK / (8 × (2 - OVER8) × USARTDIV)

其中fCK是USART时钟频率,OVER8对应Over Sampling设置(0表示16倍过采样,1表示8倍)。当使用16倍过采样时,公式简化为:

USARTDIV = fCK / (16 × BaudRate)

以72MHz时钟和115200波特率为例: USARTDIV = 72000000 / (16 × 115200) ≈ 39.0625

这个值会被自动拆分为整数部分(39)和小数部分(0.0625×16=1)写入BRR寄存器。计算出的实际波特率为72000000/(16×39.0625)=115200,完全匹配。

2. 生成代码与基础功能测试

2.1 生成MDK-ARM工程

在Project Manager标签页中:

  1. 设置Toolchain为MDK-ARM V5
  2. 勾选"Generate peripheral initialization as a pair of .c/.h files"
  3. 特别重要:在Code Generator中勾选"Generate peripheral initialization as a pair of .c/.h files"

点击GENERATE CODE生成工程,用Keil打开后,我们可以在main.c中看到HAL库已经帮我们生成了USART初始化代码:

huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); }

2.2 基础发送功能验证

在main函数中添加测试代码:

uint8_t msg[] = "USART Test\r\n"; HAL_UART_Transmit(&huart1, msg, sizeof(msg)-1, HAL_MAX_DELAY);

编译下载后,用串口调试助手(如SSCOM或Putty)应该能看到输出。如果没看到:

  1. 检查开发板供电
  2. 确认串口线连接正确(TX-RX交叉)
  3. 验证PC端串口驱动是否安装(设备管理器查看端口)
  4. 确保波特率等参数设置一致

2.3 接收功能与中断配置

在CubeMX中启用USART1全局中断(NVIC Settings标签页),然后在stm32f1xx_it.c中找到USART1_IRQHandler,添加:

void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); }

在main.c中定义接收缓冲区和回调函数:

uint8_t rx_data; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1){ HAL_UART_Transmit(&huart1, &rx_data, 1, HAL_MAX_DELAY); HAL_UART_Receive_IT(&huart1, &rx_data, 1); } }

在main函数初始化后启动接收:

HAL_UART_Receive_IT(&huart1, &rx_data, 1);

现在可以实现简单的回显功能——发送到串口的任何字符都会被原样返回。

3. printf重定向实现与优化

3.1 基础重定向方法

标准库的printf最终会调用_write函数,我们只需要重定向它。添加以下代码:

#include <stdio.h> int _write(int file, char *ptr, int len) { HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY); return len; }

但这样实现有几个问题:

  1. 没有处理换行符(\n)到回车换行(\r\n)的转换
  2. 每次调用都使用阻塞式传输
  3. 缺乏错误处理

3.2 增强版重定向实现

更完善的版本应该考虑以下方面:

int _write(int file, char *ptr, int len) { if(file != STDOUT_FILENO && file != STDERR_FILENO){ errno = EBADF; return -1; } HAL_StatusTypeDef status; int remaining = len; while(remaining > 0){ // 处理换行转换 if(*ptr == '\n'){ uint8_t cr = '\r'; status = HAL_UART_Transmit(&huart1, &cr, 1, HAL_MAX_DELAY); if(status != HAL_OK) return -1; } status = HAL_UART_Transmit(&huart1, (uint8_t*)ptr, 1, HAL_MAX_DELAY); if(status != HAL_OK) return -1; ptr++; remaining--; } return len; }

3.3 使用DMA提升性能

对于大量输出,可以使用DMA减轻CPU负担:

  1. 在CubeMX中启用USART1_TX的DMA通道
  2. 定义发送缓冲区和管理结构体:
#define PRINTF_BUF_SIZE 128 uint8_t printf_buf[PRINTF_BUF_SIZE]; volatile uint8_t printf_busy = 0; int _write(int file, char *ptr, int len) { if(len > PRINTF_BUF_SIZE) len = PRINTF_BUF_SIZE; while(printf_busy); // 等待上次传输完成 printf_busy = 1; memcpy(printf_buf, ptr, len); // 启动DMA传输 HAL_UART_Transmit_DMA(&huart1, printf_buf, len); return len; } // 在传输完成回调中清除busy标志 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1){ printf_busy = 0; } }

4. 实际应用中的问题排查与优化

4.1 常见问题排查指南

问题1:无输出或乱码

  • 检查时钟配置是否正确(特别是HSE_VALUE定义)
  • 验证波特率计算(使用示波器测量实际波形)
  • 确认串口线连接(TX/RX是否接反)

问题2:printf导致程序卡死

  • 检查是否链接了标准库(在Keil的Target选项中勾选"Use MicroLIB")
  • 确保_write函数实现正确
  • 验证堆栈大小是否足够(启动文件中Stack_Size至少设为0x400)

问题3:DMA传输不完整

  • 检查DMA通道优先级设置
  • 确保缓冲区在传输期间不被修改
  • 验证DMA中断是否启用

4.2 性能优化技巧

  1. 缓冲机制:实现环形缓冲区减少频繁调用
#define BUF_SIZE 256 typedef struct { uint8_t buffer[BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } ring_buf_t; void buf_put(ring_buf_t *buf, uint8_t data) { buf->buffer[buf->head++] = data; if(buf->head >= BUF_SIZE) buf->head = 0; } uint8_t buf_get(ring_buf_t *buf) { uint8_t data = buf->buffer[buf->tail++]; if(buf->tail >= BUF_SIZE) buf->tail = 0; return data; }
  1. 格式化优化:避免浮点运算(特别在没有FPU的芯片上)
// 不推荐 printf("Value: %f", 3.14f); // 推荐:使用定点数代替 printf("Value: %d.%02d", 3, 14);
  1. 异步日志系统:结合RTOS实现非阻塞日志
void log_task(void const *argument) { while(1){ if(osMessageAvailable(log_queue)){ log_msg_t msg = osMessageGet(log_queue); HAL_UART_Transmit(&huart1, msg.data, msg.len, 100); } osDelay(1); } }

4.3 跨平台兼容性设计

为了实现代码在不同STM32系列间的可移植性:

  1. 抽象硬件依赖:
// uart_hal.h typedef struct { void (*init)(uint32_t baudrate); int (*write)(const uint8_t *data, uint16_t len); } uart_driver_t; // 在具体平台实现 #ifdef STM32F1 #include "stm32f1xx_hal.h" extern UART_HandleTypeDef huart1; void uart_init(uint32_t baudrate) { huart1.Instance = USART1; // ...其他初始化 } #endif
  1. 使用条件编译处理差异:
#if defined(STM32F1) #define USART_INSTANCE USART1 #elif defined(STM32F4) #define USART_INSTANCE USART2 #endif
  1. 统一接口封装:
void debug_printf(const char *fmt, ...) { va_list args; va_start(args, fmt); char buf[128]; vsnprintf(buf, sizeof(buf), fmt, args); uart_write((uint8_t*)buf, strlen(buf)); va_end(args); }

通过以上方法,我们可以构建一个健壮的、可移植的串口打印系统,满足从简单调试到复杂日志记录的各种需求。在实际项目中,根据具体需求选择合适的实现方式,平衡功能、性能和资源消耗。