目录

1、DMA介绍

2、UART介绍

3、UART_DMA模式配置

3.1 UART参数配置

3.2 DMA参数配置

3.3 NVIC配置

4、UART串口DMA收发代码

4.1 sys.c

4.2 sys.h

4.3 usart.c部分代码

4.4 usart.h 部分代码

4.5 代码使用

5、拓展


1、DMA介绍

MCU的DMA(Direct Memory Access)是一种无需CPU参与即可在内存与外设之间、内存与内存之间完成大批量数据搬运的硬件机制。

一句话:让“数据搬运”这苦力活由专用硬件完成,CPU只负责“指挥”,从而大幅提升效率。

DMA工作的三大步骤:

  1. CPU配置:告诉 DMA「从哪搬、搬到哪、搬多少、怎么搬」
  2. DMA干活:拿到总线控制权后,完全硬件自动搬运;CPU可并行做其他事
  3. 完成后通知:搬运结束或出错时,可选中断唤醒 CPU 做后续处理

典型配置参数(以 STM32 为例):

  1. 源地址/目标地址:可固定(寄存器)或递增(内存Buffer)
  2. 传输方向:外设→内存、内存→外设、内存→内存、外设→外设
  3. 数据宽度:Byte/Half-Word/Word
  4. 传输模式:一次性(Normal)或循环自动重载(Circular)
  5. 触发源:软件触发、定时器、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 接收事件同步给应用线程”,好处有三点:

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐