STM32-UART串口DMA模式收发讲解
使用DMA优化UART串口进行数据收发
目录
1、DMA介绍
MCU的DMA(Direct Memory Access)是一种无需CPU参与即可在内存与外设之间、内存与内存之间完成大批量数据搬运的硬件机制。
一句话:让“数据搬运”这苦力活由专用硬件完成,CPU只负责“指挥”,从而大幅提升效率。
DMA工作的三大步骤:
- CPU配置:告诉 DMA「从哪搬、搬到哪、搬多少、怎么搬」
- DMA干活:拿到总线控制权后,完全硬件自动搬运;CPU可并行做其他事
- 完成后通知:搬运结束或出错时,可选中断唤醒 CPU 做后续处理
典型配置参数(以 STM32 为例):
- 源地址/目标地址:可固定(寄存器)或递增(内存Buffer)
- 传输方向:外设→内存、内存→外设、内存→内存、外设→外设
- 数据宽度:Byte/Half-Word/Word
- 传输模式:一次性(Normal)或循环自动重载(Circular)
- 触发源:软件触发、定时器、ADC、UART、SPI、I²S 等外设事件
问题1:为什么DMA可以代替CPU进行“数据搬运”?
在MCU内部有一组专门的DMA控制器,它能在总线上像CPU一样“读/写”存储器和外设寄存器;CPU只是把“搬运路线”告诉DMA,之后DMA独占总线完成搬运,CPU被解放出来去做运算或控制。
问题2:数据搬运本质上是什么?
“从总线地址A读,再把值写到总线地址B”的循环。
CPU能做:while(len--) *dst++ = *src++;
DMA也能做:它内部有地址寄存器(源/目的)+计数器+控制逻辑,硬件自动完成同样的循环。
问题3:为什么DMA能“独占总线”?
MCU总线矩阵是多主仲裁结构(CPU、DMA、USB、以太网…)。DMA控制器也是主设备(bus master),一旦启动,它会向仲裁器申请总线。
仲裁器把总线暂时交给DMA,CPU被“挂起”若干周期(可配置优先级和突发长度),DMA完成word/half-word/byte搬运后再归还总线。
因此不需要CPU逐条指令读/写。
DMA和CPU关系图表:
|
阶段 |
CPU角色 |
DMA角色 |
|
配置 |
设定源/目的地址、长度、触发源 |
待命 |
|
运行 |
可能被挂起或并行执行其他指令 |
独占总线,高速搬运 |
|
结束 |
可选中断唤醒 CPU,处理后续逻辑 |
停止或循环重载 |
形象比喻:
CPU:项目经理,告诉DMA“把仓库A的1000箱货搬到仓库B”。
DMA:叉车司机,拿到路线后就开叉车自己搬,项目经理可以去做报表、打电话,不用一箱一箱搬。
2、UART介绍
UART(Universal Asynchronous Receiver & Transmitter),一种两根线就能完成“点对点”串行通信的极简协议。
特点:“异步 + 全双工 + 帧格式固定”。
只需三根线即可通信(TX、RX、GND),无需时钟线(异步)。
1.物理连接
|
引脚 |
方向 |
说明 |
|
TX |
输出 |
本机发送端 → 对方RX |
|
RX |
输入 |
本机接收端 ← 对方TX |
|
GND |
公共 |
必须共地 |
2.工作模式
|
模式 |
描述 |
|
全双工 |
TX、RX 各用一条线,可同时收发 |
|
半双工 |
单线分时收发(如RS-485差分扩展) |
|
单工 |
只能单向发或收(极少) |
3.帧格式
起始位(0) + 数据位(5~9位) + [可选校验位] + 停止位(1位或2位)
起始位通知“数据来了”、停止位给线路恢复时间、校验位可选奇/偶/无、波特率决定每位时长,双方必须一致。
4.UART原理图

问题:为什么要共地?
GND(Ground)就是所有电路的公共参考“零电位”点。没有共同的地,TX和RX之间的电压就“各说各话”,通信必然失败。
形象理解:
TX端:把自身GND设为0V参考,输出高 = TX_GND + 3.3V。
RX端:把自身GND设为0V参考,判决高,需要 ≥ RX_GND + 2V。
如果两块板子的GND之间有ΔV = 2V的电位差(例如TX_GND比RX_GND高2V),则:TX线上的实际电压 = TX_GND + 3.3 V = RX_GND + 1.3 V,而1.3V < 2 V ⇒ RX把它判成低电平 → 逻辑错误。
类比:像两个人用卷尺量距离,必须以同一把尺子的0cm 为基准;如果各用各的尺子,读数就会对不上。
实际接线:用一根杜邦线把两板的 GND 连起来即可,线越短越可靠。差分接口(如 RS-485、CAN)虽然信号线成对传输,仍需共地,只是要求宽松些。
GND 让“0 V”成为双方公认的基准,确保逻辑电平一致,是 UART(乃至任何数字通信)正常工作的前提。
3、UART_DMA模式配置
3.1 UART参数配置

Over Sampling(过采样):在每一位(bit)的时间里,USART 用比“必需”更多的时钟周期去多次采样这条线上的电平,然后用“投票”或滤波算法挑出最可信的值,从而提高抗噪声能力和波特率精度。
16 Samples:对应STM32的16倍过采样(Over Sampling by 16,简称OS16)。具体含义:当波特率为115200bps时,每一位的时长 ≈ 8.68µs。
在16倍过采样模式下,USART会把这8.68µs再细分成16个小窗口(每个0.54µs),然后在中间几个窗口(通常是第 7、8、9 次)采样,取多数值作为最终电平。即使某次采样被噪声干扰,也不会导致误判。
简单来讲就是:把1bit时间切成N份去多次采样,用“多数表决”提高UART的可靠性和容错率。
3.2 DMA参数配置
1、Use FIFO(FIFO 模式):打开后,DMA先把数据攒在内部4×4字节FIFO里,等攒够阈值(1/4、1/2、3/4、Full)再一次性写进目标地址,减少总线访问次数,降低CPU干扰。
|
开关 |
FIFO 深度 |
优点 |
适用场景 |
|
Use FIFO开 |
4×4 byte = 16 byte |
带宽高、CPU 打断少 |
高速流(音频、USB、SDIO) |
|
Use FIFO关 |
无 |
实时性高、延迟低 |
实时串口、低功耗 |
CubeMX默认OFF;想要高吞吐再勾选,并选阈值 Half-FIFO 或 Full-FIFO。
2、Circular Mode(回环/循环模式):DMA搬运完最后一个字节后,自动回到首地址继续搬,形成“环形缓冲区”,无需CPU重启,适合持续流式接收。
|
模式 |
传输完成动作 |
典型用法 |
|
Normal |
停止,等待CPU重新启动 |
一次性发送文件 |
|
Circular |
自动回到起点继续 |
串口 DMA 持续接收、音频播放 |
在CubeMX的“Mode”下拉框里选Circular即可;配合双缓冲(Double Buffer)可做到“零拷贝”。

Half Word(半字):数据宽度设置为16位(2字节),每次传输2字节数据
Byte(字节):数据宽度设置为8位(1字节),每次传输1字节的数据。
3.3 NVIC配置
由于我的代码启用了FreeRTOS,所以DMA的中断默认打开了,裸机的情况下应该也需要开启。但是USART的全局中断一定要打开,要不然使用不了UART相关的中断函数。

4、UART串口DMA收发代码
4.1 sys.c
#include <stdio.h>
#include "sys.h"
osSemaphoreId_t txSemaphore; // 发送信号量ID
osSemaphoreId_t rxSemaphore; // 接收信号量ID
/**
* @brief UART信号量初始化
* @param none
* @return none
*/
void UART_Semaphore_Init(void)
{
/* 创建二值信号量 */
txSemaphore = osSemaphoreNew(1, 1, NULL); // 发送信号量,初始为1(空闲)
rxSemaphore = osSemaphoreNew(1, 0, NULL); // 接收信号量,初始为0(等待数据)
if (txSemaphore == NULL || rxSemaphore == NULL)
printf("[UART_Semaphore_Init] Failed to create semaphore!\n");
}
4.2 sys.h
#ifndef __SYS_H
#define __SYS_H
#include "cmsis_os2.h"
extern osSemaphoreId_t txSemaphore;
extern osSemaphoreId_t rxSemaphore;
void UART_Semaphore_Init(void);
#endif /* __SYS_H */
4.3 usart.c部分代码
/* USER CODE BEGIN 0 */
#include <string.h>
#include <stdio.h>
#include "sys.h"
#define TX_DMA_BUF_LEN 1024
#define RX_DMA_BUF_LEN 1024
static uint8_t txBuf[TX_DMA_BUF_LEN]; // 发送缓冲区
static uint8_t rxBuf[RX_DMA_BUF_LEN]; // 接收缓冲区
static volatile uint16_t rxLen = 0; // 接收字符长度
/* ---------- 串口发送函数(DMA) ---------- */
/* 重定向发送函数 */
static void _flush_dma(const uint8_t *data, uint16_t len)
{
if (len == 0 || len > TX_DMA_BUF_LEN)
return;
// 获取信号量 - 阻塞等待
osSemaphoreAcquire(txSemaphore, osWaitForever);
memcpy(txBuf, data, len); // 源缓冲可能随时被库改写,需拷贝
HAL_UART_Transmit_DMA(&huart1, txBuf, len);
}
#ifdef __GNUC__
/* 适用于 GCC/ARMCC 6 及以上版本 */
int _write(int file, char *ptr, int len)
{
(void)file; // 避免未使用参数警告
_flush_dma((uint8_t *)ptr, len);
return len;
}
#else
/* 适用于 Keil/ARMCC 5 及以下版本 */
int fputc(int ch, FILE *f)
{
(void)f; // 避免未使用参数警告
uint8_t c = (uint8_t)ch;
_flush_dma(&c, 1);
return ch;
}
#endif
/* USER CODE END 0 */
/* USER CODE BEGIN 1 */
/* -------------------- 串口接收函数(DMA) -------------------- */
/**
* @brief 初始化UART接收
* @param none
* @return none
*/
void UART_Receive_Init(void)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t *)rxBuf, sizeof(rxBuf));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 禁用半传输中断
}
/**
* @brief UART接收到的数据
* @param buffer 接收数据数组
* @return rxLen 接收字符长度
*/
uint16_t UART_GetReceivedData(uint8_t *buffer)
{
if (osSemaphoreAcquire(rxSemaphore, osWaitForever) != osOK)
return 0;
memcpy(buffer, rxBuf, rxLen);
buffer[rxLen] = '\0';
return rxLen;
}
/* -------------------- 回调函数 -------------------- */
/* 发送中断回调 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
osSemaphoreRelease(txSemaphore); // 释放发送信号量
}
}
/* 接收事件回调 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1)
{
// 安全截断数据
rxLen = (Size >= RX_DMA_BUF_LEN) ? (RX_DMA_BUF_LEN - 1) : Size;
rxBuf[rxLen] = '\0';
osSemaphoreRelease(rxSemaphore); // 释放信号量
// 重启接收(自动禁用HT中断)
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rxBuf, RX_DMA_BUF_LEN);
}
}
/* 错误回调 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 自动恢复接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rxBuf, RX_DMA_BUF_LEN);
}
}
/* USER CODE END 1 */
4.4 usart.h 部分代码
/* USER CODE END Header */
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __USART_H__
#define __USART_H__
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
extern UART_HandleTypeDef huart1;
/* USER CODE BEGIN Private defines */
/* USER CODE END Private defines */
void MX_USART1_UART_Init(void);
/* USER CODE BEGIN Prototypes */
void UART_Receive_Init(void);
uint16_t UART_GetReceivedData(uint8_t *buffer);
/* USER CODE END Prototypes */
#ifdef __cplusplus
}
#endif
#endif /* __USART_H__ */
4.5 代码使用
发送:串口发送是通过printf重定向的,使用printf就可以发送数据了
接收:
1、调用初始化函数:UART_Receive_Init(); // 初始化UART接收模式
2、调用接收函数
uint8_t rxData[] = {0};
uint16_t len = UART_GetReceivedData(rxData);
if (len > 0)
{
printf("RECEIVE <<<<<<<<<< %s\n", rxData);
}
5、拓展
DMA串口接收的代码,我上面使用的是HAL库自带的解包函数。下面还有一种方式,使用中断模式进行接收。
Serial_RxFlag = 1; 是接收标志位,用于告诉CPU我已经收到数据了,你可以处理其它信息了。

同样你也可以不设置标志位(信号量),但不设就要自己“轮询”或者“死等”,CPU 会被拖死,RTOS 的多任务优势也废了。
二值信号量在这里只做一件事——“把 DMA 接收事件同步给应用线程”,好处有三点:

更多推荐




所有评论(0)