1. UART 基础原理
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是嵌入式开发中最常用的通信接口之一。 它采用异步串行方式传输数据,无需额外时钟线,仅需 TX(发送)、RX(接收)两根信号线,加上共地即可实现全双工通信。

图1:UART 数据帧格式(1个起始位 + 8位数据 + 可选校验位 + 1/2个停止位)
波特率决定每秒传输的符号数,常用值为 9600、115200、921600 等。 通信双方必须预先约定相同的波特率、数据位、校验位、停止位(即"串口参数")。
| 参数 | 常用值 | 说明 |
|---|---|---|
| 波特率 | 9600 / 115200 / 921600 | 每秒传输比特数 |
| 数据位 | 7 / 8 / 9 bit | 最常用 8 bit |
| 校验位 | None / Even / Odd | None 最常用,对可靠性有要求时选 Even |
| 停止位 | 0.5 / 1 / 1.5 / 2 | 一般选 1,多机通信有时用 2 |
**UART vs USART:**USART(Universal Synchronous/Asynchronous)在 UART 基础上增加了同步模式(CK 时钟线), 但平时用的都是异步(UART)模式。
2.HAL 库初始化
使用 HAL 库初始化 USART5(115200, 8N1),核心是配置 UART_HandleTypeDef 结构体。 以下代码以 USART5 为例。
cpp
void UART5_Init(void)
{
/* USER CODE BEGIN UART5_Init 0 */
/* USER CODE END UART5_Init 0 */
/* USER CODE BEGIN UART5_Init 1 */
/* USER CODE END UART5_Init 1 */
huart5.Instance = UART5;
huart5.Init.BaudRate = 115200;
huart5.Init.WordLength = UART_WORDLENGTH_8B;
huart5.Init.StopBits = UART_STOPBITS_1;
huart5.Init.Parity = UART_PARITY_NONE;
huart5.Init.Mode = UART_MODE_TX_RX;
huart5.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart5.Init.OverSampling = UART_OVERSAMPLING_16;
huart5.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart5.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart5.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart5) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetTxFifoThreshold(&huart5, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetRxFifoThreshold(&huart5, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_DisableFifoMode(&huart5) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN UART5_Init 2 */
/* USER CODE END UART5_Init 2 */
}
关键参数解析
| 参数 | 可选值 | 说明 |
|---|---|---|
WordLength |
8B 9B | 8位模式需 Parity=None;9位模式可配合 Even/Odd 校验(实际数据仍8位+1位校验) |
OverSampling |
16x 8x | 16x 精度更高;8x 可支持更高波特率(可达 pclk/8) |
HwFlowCtl |
NONE RTS CTS RTS_CTS | 启用 RTS/CTS 需额外连接两根信号线 |
HAL_UARTEx_DisableFifoMode(&huart5)可以使用HAL_UARTEx_EnableFifoMode开启FIFO模式。
HAL_UARTEx_SetTxFifoThreshold和HAL_UARTEx_SetRxFifoThreshold可以设置FIFO 的触发阈值。如果使用中断模式接收数据可以减少中断触发的次数。
3.轮询收发
轮询模式最简单:调用 HAL_UART_Transmit / HAL_UART_Receive, 函数会阻塞等待直到收发完成或超时。这种模式不常用,一般只调试阶段使用。
cpp
uint8_t tx_buf[] = "Hello STM32H7!\r\n";
uint8_t rx_buf[64];
/* ===== 发送 ===== */
// 参数:串口句柄 / 数据指针 / 数据长度 / 超时(ms)
HAL_UART_Transmit(&huart1, tx_buf, sizeof(tx_buf) - 1, HAL_MAX_DELAY);
/* ===== 接收 ===== */
// 阻塞等待接收到 10 字节或超时 1000ms
HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, rx_buf, 10, 1000);
if (status == HAL_OK) {
// 接收成功,处理 rx_buf
} else if (status == HAL_TIMEOUT) {
// 超时,未收齐数据
} else {
// 其他错误
}
4. 中断模式收发
中断模式下,UART 收发由硬件中断驱动:TXE(发送寄存器空) 触发发送中断, **RXNE(接收寄存器非空)**触发接收中断。CPU 只在需要时介入,不再阻塞等待。
4.1 开启中断接收
cpp
#include "stm32h7xx_hal.h"
extern UART_HandleTypeDef huart1;
#define RX_BUF_SIZE 256
static uint8_t rx_data;
static uint8_t rx_buffer[RX_BUF_SIZE];
static volatile uint16_t rx_len = 0;
/* ── 启动中断接收(接收1字节,完成后在中断中重新启动)── */
void UART_Start_IT_Receive(void)
{
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
/* ── HAL 接收完成回调(弱函数覆盖)── */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
if (rx_len < RX_BUF_SIZE) {
rx_buffer[rx_len++] = rx_data; // 存入缓冲区
}
// 重新启动接收,实现连续接收
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
}
4.2 中断发送 --- HAL_UART_Transmit_IT()
HAL 库提供了专用的中断发送函数 HAL_UART_Transmit_IT(), 其内部自动管理 TXE 中断使能/禁用、逐字节写入 TDR、以及 TC(发送完成)回调, 无需手动操作寄存器。调用后函数立即返回,发送在后台中断中完成。
cpp
#include "stm32h7xx_hal.h"
extern UART_HandleTypeDef huart1;
static uint8_t tx_buf[] = "Hello from IT!\r\n";
static volatile uint8_t tx_busy = 0; // 发送忙标志
/* ── 启动中断发送 ── */
void UART_IT_Send(uint8_t *data, uint16_t len)
{
if (tx_busy) return; // 上一次发送未完成,拒绝新请求
tx_busy = 1;
HAL_UART_Transmit_IT(&huart1, data, len); // 非阻塞,立即返回
}
/* ── HAL 发送完成回调(弱函数覆盖)── */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
tx_busy = 0; // 发送完成,允许下一次发送
}
}
/* ── 使用示例 ── */
void Some_Function(void)
{
uint8_t msg[] = "CMD:START\r\n";
UART_IT_Send(msg, sizeof(msg) - 1);
// 函数立即返回,CPU 继续执行其他任务
// 发送完成后自动进入 HAL_UART_TxCpltCallback
}
5. DMA 高速传输
DMA(Direct Memory Access)让数据在内存与 USART 外设之间直接搬运,全程无需 CPU 介入 。 在高波特率下,DMA 是唯一能保证不丢数据的方案。STM32H7 的 DMA 支持循环模式 (Circular) 和普通模式(Normal)。
5.1 DMA 接收代码
cpp
#include "stm32h7xx_hal.h"
extern UART_HandleTypeDef huart1;
#define DMA_RX_LEN 256
static uint8_t dma_rx_buf[DMA_RX_LEN];
/* ── 启动 DMA 循环接收 ── */
void UART_DMA_Start_Receive(void)
{
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_RX_LEN);
}
/* ── DMA 接收完成回调(循环模式下缓冲区满时触发)── */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 循环模式下,DMA 自动从头继续接收
// 此处处理 dma_rx_buf 中后半段数据
Process_Rx_Data(dma_rx_buf + DMA_RX_LEN/2, DMA_RX_LEN/2);
}
}
/* ── DMA 半接收回调(收到一半时触发)── */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 处理前半段数据
Process_Rx_Data(dma_rx_buf, DMA_RX_LEN/2);
}
}
/* ── DMA 发送 ── */
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
HAL_UART_Transmit_DMA(&huart1, data, len);
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 发送完成,可启动下一次发送
}
}
H7 的 DMA 缓存一致性问题: STM32H7 有 D-Cache,DMA 读取的内存区域可能和 Cache 不一致。 解决方案:① 将 DMA 缓冲区放在 DTCM(0x20000000 起,默认不经过 Cache);② 使用 SCB_InvalidateDCache_by_Addr() 手动失效 Cache;③ 在 MPU 中将该区域配置为 Non-Cacheable。
6.空闲中断 + DMA 实战
这是实际工程中最常用、最推荐 的串口接收方案。核心思路: DMA 循环接收持续搬运数据到缓冲区,同时开启 IDLE(空闲线检测)中断 ------ 当总线空闲一个字节时间后,硬件自动触发中断,此时通过 DMA 传输计数器算出已接收的数据长度。 这样无需预先知道数据长度,也不怕数据被截断。
cpp
#include "stm32h7xx_hal.h"
#include <string.h>
extern UART_HandleTypeDef huart1;
/* ─── 配置区 ─── */
#define RX_BUF_SIZE 512
static uint8_t rx_buf[RX_BUF_SIZE]; // DMA 接收缓冲区
static volatile uint16_t rx_len = 0; // 已接收数据长度
static volatile uint8_t rx_flag = 0; // 接收完成标志
/* ─── 初始化:启动 DMA + IDLE 中断 ─── */
void UART_IDLE_DMA_Init(void)
{
// 1. 启动 DMA 循环接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart == &huart8)
{
rx_len = Size;
rx_flag = 1;
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, RX_BUF_SIZE);//继续接收数据
__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT);//屏蔽半传输完成中断
}
}
上面是屏蔽了半完成中断的,下面可以根据接收类型来判断是半完成还是完成。
cpp
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->RxEventType == HAL_UART_RXEVENT_HT)
{
// 半完成
}
else if (huart->RxEventType == HAL_UART_RXEVENT_TC)
{
// 完成
}
}
7.printf 重定向
在嵌入式开发中,printf() 是最方便的调试手段。只需重写 fputc() 或 _write(),将标准输出重定向到 USART,即可在串口助手中看到打印信息。
7.1 MicroLib 方式
在 Keil MDK 中勾选 "Use MicroLib" 后,只需重写 fputc():
cpp
#include "stdio.h"
#include "stm32h7xx_hal.h"
extern UART_HandleTypeDef huart1;
/* 重写 fputc,将字符通过 USART1 发送 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
7.2 GCC / 标准库方式
使用 GCC 工具链(STM32CubeIDE 等)时,需重写 _write():
cpp
#include "stm32h7xx_hal.h"
#include <unistd.h>
extern UART_HandleTypeDef huart1;
int _write(int fd, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
return len;
}