STM32 USART 通讯原理与三种模式详解

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 被完全占用,无法执行其他任务。

发送流程

  1. 等待TXE位(发送数据寄存器空)置 1
  2. 将数据写入USART_DR寄存器
  3. 重复步骤 1-2 直到所有数据发送完成
  4. 可选:等待TC位(发送完成)置 1,确保最后一个字节发送完毕

接收流程

  1. 等待RXNE位(接收数据寄存器非空)置 1
  2. USART_DR寄存器读取数据
  3. 重复步骤 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 暂停当前任务,跳转到中断服务函数中处理数据传输。

发送中断流程

  1. 使能TXE中断(发送数据寄存器空中断)
  2. USART_DR为空时,触发中断
  3. 在中断服务函数中写入下一个字节
  4. 所有数据发送完成后,关闭TXE中断
  5. 可选:使能TC中断,在最后一个字节发送完成后执行后续操作

接收中断流程

  1. 使能RXNE中断(接收数据寄存器非空中断)
  2. 当接收到一个字节时,触发中断
  3. 在中断服务函数中读取USART_DR中的数据
  4. 将数据存入缓冲区,供主程序处理

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 发送流程

  1. 配置 DMA 通道,设置源地址(内存缓冲区)、目的地址(USART_DR)、数据长度
  2. 使能 USART 的 DMA 发送请求(DMAT位)
  3. 使能 DMA 通道,开始传输
  4. DMA 控制器自动将数据从内存发送到 USART
  5. 传输完成后,DMA 触发传输完成中断
  6. 在中断服务函数中执行后续操作

DMA 接收流程

  1. 配置 DMA 通道,设置源地址(USART_DR)、目的地址(内存缓冲区)、数据长度
  2. 使能 USART 的 DMA 接收请求(DMAR位)
  3. 使能 DMA 通道,开始接收
  4. DMA 控制器自动将接收到的数据存入内存
  5. 传输完成后,DMA 触发传输完成中断
  6. 在中断服务函数中处理接收到的数据

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 占用要求严格的场合

六、进阶技巧与注意事项

  1. 空闲中断 + DMA 接收:解决 DMA 模式下必须接收固定长度数据的问题。当 USART 总线空闲时触发中断,在中断中读取 DMA 剩余传输长度,从而知道实际接收了多少数据。

  2. 双缓冲区 DMA:使用两个缓冲区交替接收数据,避免数据覆盖,实现无缝数据传输。

  3. 错误处理:在实际应用中,需要处理 USART 的错误中断,如奇偶校验错误、帧错误、溢出错误等。

  4. HAL 库注意事项 :HAL 库的HAL_UART_Transmit_ITHAL_UART_Transmit_DMA函数会自动使能相应的中断和 DMA,不需要手动配置。

相关推荐
资深流水灯工程师1 小时前
STM32 单片机 SPI 通讯原理详解
stm32·单片机·嵌入式硬件
EMTime2 小时前
玲珑GUI-工程设置
单片机·mcu·ui·用户界面
不做无法实现的梦~2 小时前
MAVLink 协议教程
linux·stm32·嵌入式硬件·算法
QiLinkOS3 小时前
【用呼吸重构创造价值关系——QiLink生态】
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
sxstj3 小时前
STM32F103 串口数量 + 对应 GPIO
单片机·嵌入式硬件
嵌入式ZYXC3 小时前
第4章:MCU最小系统设计——从一颗光杆芯片到它能跑起来
stm32·单片机·嵌入式硬件·物联网
czhaii3 小时前
ABB变频器 ACS510 传动故障诊断的故障队列
嵌入式硬件·硬件工程
嵌入式小站3 小时前
STM32 零基础可移植教程 14:ADC 单通道采样,不接电位器也能读电压
chrome·stm32·嵌入式硬件
记帖5 小时前
STM32C542开发(1)----点亮LED
嵌入式硬件·stm32cubemx·stm32cubeide·stm32cubemx2·stm32c542cct6