USART(通用同步异步收发器)是 STM32 单片机最常用的串行通信外设,支持全双工异步通信、同步通信、半双工单线通信、IrDA 红外和 LIN 总线等多种模式。本文重点讲解异步串行通信 下的阻塞(轮询)、中断、DMA三种数据传输模式,从底层寄存器到上层应用全面解析。
一、USART 异步通信基本原理
1.1 异步通信帧格式
异步通信不需要时钟线,收发双方通过预先约定的波特率同步数据。标准帧格式如下:
[起始位(1位低电平)] + [数据位(5-8位)] + [奇偶校验位(0-1位)] + [停止位(0.5-2位高电平)]
- 起始位:标志一帧数据的开始
- 数据位:实际传输的数据,通常为 8 位
- 奇偶校验位:可选,用于错误检测
- 停止位:标志一帧数据的结束
1.2 波特率计算
波特率是每秒传输的二进制位数。STM32 的 USART 波特率由波特率寄存器USART_BRR决定:
USARTDIV = fCK / (16 × 波特率)
其中fCK是 USART 的时钟源(通常为 APB1 或 APB2 时钟)。USART_BRR寄存器分为两部分:
- 整数部分 (12 位):DIV_Mantissa
- 小数部分 (4 位):DIV_Fraction
例如:fCK=72MHz,波特率 = 115200
USARTDIV = 72000000 / (16 × 115200) = 39.0625
DIV_Mantissa = 39 (0x27)
DIV_Fraction = 0.0625 × 16 = 1 (0x1)
USART_BRR = 0x271
1.3 USART 核心寄存器概览
所有模式都会用到以下核心寄存器:
| 寄存器 | 主要功能 | 关键位 |
|---|---|---|
USART_CR1 |
控制寄存器 1 | UE (USART 使能)、TE (发送使能)、RE (接收使能)、RXNEIE (接收中断使能)、TCIE (发送完成中断使能) |
USART_CR2 |
控制寄存器 2 | STOP (停止位个数) |
USART_CR3 |
控制寄存器 3 | DMAT (发送 DMA 使能)、DMAR (接收 DMA 使能) |
USART_SR |
状态寄存器 | RXNE (接收数据寄存器非空)、TXE (发送数据寄存器空)、TC (发送完成) |
USART_DR |
数据寄存器 | 低 8 位存储发送 / 接收的数据 |
USART_BRR |
波特率寄存器 | 波特率设置 |
二、阻塞模式(轮询模式)
2.1 工作原理
阻塞模式是最简单的 USART 通信方式,CPU 不断查询USART_SR寄存器的状态标志,直到满足条件才进行数据读写。在数据传输过程中,CPU 被完全占用,无法执行其他任务。
发送流程:
- 等待
TXE位(发送数据寄存器空)置 1 - 将数据写入
USART_DR寄存器 - 重复步骤 1-2 直到所有数据发送完成
- 可选:等待
TC位(发送完成)置 1,确保最后一个字节发送完毕
接收流程:
- 等待
RXNE位(接收数据寄存器非空)置 1 - 从
USART_DR寄存器读取数据 - 重复步骤 1-2 直到所有数据接收完成
2.2 涉及的寄存器与状态
- 控制寄存器 :
USART_CR1(UE、TE、RE)、USART_CR2(STOP)、USART_BRR - 状态寄存器 :
USART_SR(TXE、TC、RXNE) - 数据寄存器 :
USART_DR
2.3 优缺点
- 优点:实现简单,代码量少,不需要配置中断和 DMA
- 缺点:CPU 利用率极低,长时间等待会导致系统卡顿;无法处理突发数据,容易丢失数据
2.4 应用示例(基于 HAL 库)
cpp
#include "stm32f1xx_hal.h"
UART_HandleTypeDef huart1;
// USART1 初始化函数
void MX_USART1_UART_Init(void) {
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;
HAL_UART_Init(&huart1);
}
// 阻塞式发送字符串
void UART_SendString_Blocking(char *str) {
while (*str) {
// 等待发送数据寄存器空
while (!(USART1->SR & USART_SR_TXE));
// 发送一个字节
USART1->DR = *str++;
}
// 等待最后一个字节发送完成
while (!(USART1->SR & USART_SR_TC));
}
// 阻塞式接收一个字节
uint8_t UART_ReceiveByte_Blocking(void) {
// 等待接收数据寄存器非空
while (!(USART1->SR & USART_SR_RXNE));
// 读取数据(会自动清除RXNE位)
return USART1->DR;
}
// 阻塞式接收指定长度的数据
void UART_ReceiveBuffer_Blocking(uint8_t *buf, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
buf[i] = UART_ReceiveByte_Blocking();
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
uint8_t recv_buf[10];
while (1) {
// 发送提示信息
UART_SendString_Blocking("请输入10个字符:\r\n");
// 阻塞接收10个字节
UART_ReceiveBuffer_Blocking(recv_buf, 10);
// 回显接收到的数据
UART_SendString_Blocking("你输入的是:");
HAL_UART_Transmit(&huart1, recv_buf, 10, HAL_MAX_DELAY); // HAL库阻塞发送
UART_SendString_Blocking("\r\n");
}
}
三、中断模式
3.1 工作原理
中断模式通过配置 USART 的中断使能位,当特定事件发生时(如接收到数据、发送完成),USART 会向 NVIC 发送中断请求,CPU 暂停当前任务,跳转到中断服务函数中处理数据传输。
发送中断流程:
- 使能
TXE中断(发送数据寄存器空中断) - 当
USART_DR为空时,触发中断 - 在中断服务函数中写入下一个字节
- 所有数据发送完成后,关闭
TXE中断 - 可选:使能
TC中断,在最后一个字节发送完成后执行后续操作
接收中断流程:
- 使能
RXNE中断(接收数据寄存器非空中断) - 当接收到一个字节时,触发中断
- 在中断服务函数中读取
USART_DR中的数据 - 将数据存入缓冲区,供主程序处理
3.2 涉及的寄存器与状态
- 控制寄存器 :
USART_CR1(RXNEIE、TXEIE、TCIE)、USART_CR3 - 状态寄存器 :
USART_SR(RXNE、TXE、TC) - 中断相关:NVIC 寄存器(设置中断优先级和使能)
3.3 优缺点
- 优点:CPU 利用率高,只有在数据传输时才会被打断;可以及时处理突发数据,避免丢失
- 缺点:频繁的中断会产生一定的开销;对于大量数据传输,中断次数过多会影响系统实时性
3.4 应用示例(基于 HAL 库)
cpp
#include "stm32f1xx_hal.h"
UART_HandleTypeDef huart1;
// 接收缓冲区
#define RX_BUF_SIZE 128
uint8_t rx_buf[RX_BUF_SIZE];
uint16_t rx_len = 0;
uint8_t rx_complete = 0;
// 发送缓冲区
#define TX_BUF_SIZE 128
uint8_t tx_buf[TX_BUF_SIZE];
uint16_t tx_len = 0;
uint16_t tx_ptr = 0;
// USART1 初始化函数
void MX_USART1_UART_Init(void) {
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;
HAL_UART_Init(&huart1);
// 使能接收中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
}
// HAL库MSP初始化回调函数(配置GPIO和NVIC)
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (uartHandle->Instance == USART1) {
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// PA9(TX) 复用推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10(RX) 浮空输入
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// USART1 中断配置
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
}
// USART1 中断服务函数
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
}
// HAL库接收中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef* uartHandle) {
if (uartHandle->Instance == USART1) {
if (rx_len < RX_BUF_SIZE) {
// 读取接收到的字节
rx_buf[rx_len++] = huart1.Instance->DR;
// 检测换行符作为接收完成标志
if (rx_buf[rx_len-1] == '\n') {
rx_complete = 1;
}
} else {
// 缓冲区满,重置
rx_len = 0;
}
// 重新使能接收中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
}
}
// HAL库发送中断回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef* uartHandle) {
if (uartHandle->Instance == USART1) {
// 发送完成,重置发送指针
tx_ptr = 0;
}
}
// 中断式发送字符串
void UART_SendString_Interrupt(char *str) {
// 等待上一次发送完成
while (tx_ptr != 0);
// 复制字符串到发送缓冲区
tx_len = strlen(str);
memcpy(tx_buf, str, tx_len);
// 启动中断发送
HAL_UART_Transmit_IT(&huart1, tx_buf, tx_len);
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
UART_SendString_Interrupt("USART 中断模式测试\r\n");
while (1) {
if (rx_complete) {
// 处理接收到的数据
rx_buf[rx_len] = '\0'; // 添加字符串结束符
UART_SendString_Interrupt("收到:");
UART_SendString_Interrupt((char*)rx_buf);
// 重置接收标志和长度
rx_complete = 0;
rx_len = 0;
}
// 主程序可以执行其他任务
HAL_Delay(10);
}
}
四、DMA 模式
4.1 工作原理
DMA(直接存储器访问)模式允许外设直接与内存进行数据传输,不需要 CPU 干预。当 USART 需要发送或接收数据时,DMA 控制器会自动将数据从内存搬运到 USART_DR 寄存器(发送)或从 USART_DR 寄存器搬运到内存(接收),传输完成后可以触发 DMA 中断通知 CPU。
DMA 发送流程:
- 配置 DMA 通道,设置源地址(内存缓冲区)、目的地址(USART_DR)、数据长度
- 使能 USART 的 DMA 发送请求(
DMAT位) - 使能 DMA 通道,开始传输
- DMA 控制器自动将数据从内存发送到 USART
- 传输完成后,DMA 触发传输完成中断
- 在中断服务函数中执行后续操作
DMA 接收流程:
- 配置 DMA 通道,设置源地址(USART_DR)、目的地址(内存缓冲区)、数据长度
- 使能 USART 的 DMA 接收请求(
DMAR位) - 使能 DMA 通道,开始接收
- DMA 控制器自动将接收到的数据存入内存
- 传输完成后,DMA 触发传输完成中断
- 在中断服务函数中处理接收到的数据
4.2 涉及的寄存器与状态
- USART 寄存器 :
USART_CR3(DMAT、DMAR) - DMA 寄存器 :
DMA_CCRx:通道配置寄存器(使能、传输方向、数据宽度、中断使能)DMA_CNDTRx:通道数据数量寄存器(传输长度)DMA_CPARx:通道外设地址寄存器(USART_DR 地址)DMA_CMARx:通道内存地址寄存器(缓冲区地址)DMA_ISR:中断状态寄存器(传输完成、半传输、传输错误)
- 状态标志:DMA_TCIFx(传输完成中断标志)、DMA_HTIFx(半传输中断标志)
4.3 优缺点
- 优点:CPU 利用率最高,数据传输过程中 CPU 完全空闲;适合大量数据的高速传输;传输速度快,没有中断开销
- 缺点:配置相对复杂;DMA 通道数量有限;对于小数据量传输,优势不明显
4.4 应用示例(基于 HAL 库)
cpp
#include "stm32f1xx_hal.h"
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
// 接收缓冲区
#define RX_BUF_SIZE 128
uint8_t rx_buf[RX_BUF_SIZE];
uint8_t rx_complete = 0;
// 发送缓冲区
#define TX_BUF_SIZE 128
uint8_t tx_buf[TX_BUF_SIZE];
uint8_t tx_complete = 1;
// USART1 初始化函数
void MX_USART1_UART_Init(void) {
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;
HAL_UART_Init(&huart1);
}
// DMA 初始化函数
void MX_DMA_Init(void) {
__HAL_RCC_DMA1_CLK_ENABLE();
// USART1 TX DMA 配置(DMA1 通道4)
hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_usart1_tx);
__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
// USART1 RX DMA 配置(DMA1 通道5)
hdma_usart1_rx.Instance = DMA1_Channel5;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_NORMAL;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
// DMA 中断配置
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}
// HAL库MSP初始化回调函数
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (uartHandle->Instance == USART1) {
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// PA9(TX) 复用推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10(RX) 浮空输入
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// USART1 DMA 初始化
MX_DMA_Init();
}
}
// DMA1 通道4 中断服务函数(USART1 TX)
void DMA1_Channel4_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_usart1_tx);
}
// DMA1 通道5 中断服务函数(USART1 RX)
void DMA1_Channel5_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_usart1_rx);
}
// HAL库发送完成回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef* uartHandle) {
if (uartHandle->Instance == USART1) {
tx_complete = 1;
}
}
// HAL库接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef* uartHandle) {
if (uartHandle->Instance == USART1) {
rx_complete = 1;
}
}
// DMA式发送字符串
void UART_SendString_DMA(char *str) {
// 等待上一次发送完成
while (!tx_complete);
// 复制字符串到发送缓冲区
uint16_t len = strlen(str);
memcpy(tx_buf, str, len);
// 标记发送未完成
tx_complete = 0;
// 启动DMA发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
// 启动DMA接收
void UART_StartReceive_DMA(uint16_t len) {
rx_complete = 0;
HAL_UART_Receive_DMA(&huart1, rx_buf, len);
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
UART_SendString_DMA("USART DMA模式测试\r\n");
UART_SendString_DMA("请输入10个字符:\r\n");
// 启动DMA接收10个字节
UART_StartReceive_DMA(10);
while (1) {
if (rx_complete) {
// 处理接收到的数据
UART_SendString_DMA("你输入的是:");
HAL_UART_Transmit_DMA(&huart1, rx_buf, 10);
UART_SendString_DMA("\r\n");
// 重新启动DMA接收
UART_StartReceive_DMA(10);
}
// 主程序可以执行其他任务
HAL_Delay(10);
}
}
五、三种模式对比与适用场景
| 特性 | 阻塞模式 | 中断模式 | DMA 模式 |
|---|---|---|---|
| CPU 利用率 | 极低(100% 占用) | 较高(仅中断时占用) | 最高(几乎不占用) |
| 实现复杂度 | 最简单 | 中等 | 较复杂 |
| 传输速度 | 慢 | 中等 | 最快 |
| 数据丢失风险 | 高(无法处理突发数据) | 低(及时响应) | 极低(自动缓存) |
| 适用场景 | 简单调试、小数据量传输、对实时性要求不高的场合 | 中等数据量传输、需要及时响应的场合 | 大量数据传输、高速通信、对 CPU 占用要求严格的场合 |
六、进阶技巧与注意事项
-
空闲中断 + DMA 接收:解决 DMA 模式下必须接收固定长度数据的问题。当 USART 总线空闲时触发中断,在中断中读取 DMA 剩余传输长度,从而知道实际接收了多少数据。
-
双缓冲区 DMA:使用两个缓冲区交替接收数据,避免数据覆盖,实现无缝数据传输。
-
错误处理:在实际应用中,需要处理 USART 的错误中断,如奇偶校验错误、帧错误、溢出错误等。
-
HAL 库注意事项 :HAL 库的
HAL_UART_Transmit_IT和HAL_UART_Transmit_DMA函数会自动使能相应的中断和 DMA,不需要手动配置。