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,不需要手动配置。

相关推荐
蓝斯4977 小时前
从.到实现一个Windows上的虚拟hid键盘设备
windows·单片机·计算机外设
熙芯XiChip8 小时前
硬实时与软实时RTOS核心区别
嵌入式硬件
伟大的前程8 小时前
unsigned int 定义的变量 在 电脑的 64 位系统中 和 32 位 cortex-m0 和 8 位的 51 单片机
单片机·嵌入式硬件
小渔村的拉线工9 小时前
1.HPM6E80 解析芯片整体工作原理和存储架构
单片机·嵌入式硬件·mcu·架构·hpm6e80
熙芯XiChip10 小时前
CAN与CANopen的本质区别
单片机
lilihewo10 小时前
Altium Designer 封装库的创建(分享pcb 3D封装库)
嵌入式硬件·51单片机
神罚天下ET11 小时前
典型arm位单片机启动流程(从上电到main.c)
c语言·arm开发·单片机
碧海银沙音频科技研究院12 小时前
蓝牙耳机使用WSDF5361锂保芯片进入船运模式实现方法
嵌入式硬件·算法
Zevalin爱灰灰12 小时前
模拟信号链电路——基于TL431的电压基准电路
单片机·嵌入式硬件
csdn杰哥13 小时前
瑞萨单片机开发教程[二十七] DTC 外部中断触发传输实验
单片机·嵌入式硬件·mongodb·瑞萨·ra6m5