本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32是基于ARM Cortex-M内核的主流微控制器,广泛应用于嵌入式系统。本文以STM32F103C8T6为例,详细介绍如何利用DMA(直接存储器访问)实现USART1的高效串口数据接收。通过配置DMA通道与USART外设联动,可在不占用CPU资源的情况下完成数据传输,显著提升系统实时性与稳定性。内容涵盖DMA与USART的初始化配置、内存缓冲区设置、中断处理及调试优化,适用于物联网、遥测等需持续接收串行数据的应用场景。
stm32用dma实现串口接收

1. STM32与DMA技术概述

在嵌入式系统中,CPU频繁处理外设数据会显著增加负荷,影响系统实时性与效率。STM32通过集成 直接存储器访问(DMA) 技术,实现外设与内存间的数据直传,无需CPU干预。以USART1接收为例,启用DMA后,数据可自动从DR寄存器搬运至指定内存缓冲区,避免中断频繁触发带来的上下文切换开销。

// 示例:开启DMA请求(后续章节详述)
USART1->CR3 |= USART_CR3_DMAR;  // 使能DMA接收

本章为理解DMA工作机制奠定理论基础,明确其在提升STM32通信性能中的关键作用。

2. DMA通道选择与配置(DMA1 Channel2/3)

在STM32嵌入式系统中,直接存储器访问(DMA)技术是实现高效数据搬运的核心机制。尤其在处理高速外设如USART、SPI或ADC时,合理选择并精确配置DMA通道,不仅能显著降低CPU负载,还能提升系统的实时性与稳定性。本章将围绕STM32F1系列为代表的主流MCU平台,深入剖析DMA控制器的架构特性,重点聚焦于 DMA1的Channel 2和Channel 3 ,特别是在与 USART1_RX 绑定场景下的实际应用。通过逐层解析硬件资源分布、通道映射逻辑、寄存器配置流程及代码实现细节,帮助开发者构建清晰且可复用的DMA配置体系。

2.1 STM32 DMA控制器架构解析

STM32微控制器通常配备两个独立的DMA控制器:DMA1和DMA2。它们虽然功能相似,但在支持的外设范围、通道数量以及性能定位上存在明显差异。理解这些差异对于合理分配系统资源至关重要。

2.1.1 DMA1与DMA2的区别及资源分布

在STM32F103系列等经典型号中,DMA1提供7个通道(Channel 1~7),而DMA2仅提供5个通道(Channel 1~5)。尽管两者都基于相同的传输控制机制,但其服务的外设集合不同:

DMA控制器 支持外设示例 典型用途
DMA1 USART1_RX/TX, ADC1~3, SPI2/3, I2C1/2 中低速外设数据采集与通信
DMA2 USART1_TX(部分型号)、SDIO、ADC1(高带宽模式) 高吞吐量任务(如图像、音频流)

值得注意的是, USART1_RX通常固定映射到DMA1_Channel3 ,而TX方向可能根据具体芯片支持DMA1_Channel4或DMA2_Channel7。这种不对称设计要求开发者在多外设并发使用DMA时进行细致规划。

从总线连接角度看,DMA1挂接在AHB总线下较低优先级分支,适用于对延迟不敏感的数据流;而DMA2往往具有更高的仲裁优先级,适合需要快速响应的高带宽任务。因此,在设计系统时应遵循“ DMA2用于高性能路径,DMA1用于常规外设 ”的原则。

此外,某些外设请求源只能由特定DMA控制器响应。例如,STM32F103RCT6中,ADC1的DMA输出只能通过DMA1_Channel1完成,即便DMA2空闲也无法接管。这表明资源调度必须依赖芯片参考手册中的《DMA请求映射表》进行严格查证。

// 示例:开启DMA1时钟(RCC配置)
RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 使能DMA1时钟

代码逻辑分析
- RCC->AHBENR 是AHB外设时钟使能寄存器。
- RCC_AHBENR_DMA1EN 是预定义宏,对应位为DMA1EN位(第0位)。
- 使用按位或操作确保不影响其他已启用的AHB设备。
- 若未开启此位,后续对DMA寄存器的写入将无效,导致配置失败。

该步骤是所有DMA操作的前提,体现了“先启时钟再配置”的通用规则。

2.1.2 通道映射机制与外设请求源分配

每个DMA通道都对应一组固定的外设事件作为触发源。以DMA1为例,其7个通道分别服务于不同的外设请求:

graph TD
    A[DMA1 Controller] --> B[Channel 1: ADC1]
    A --> C[Channel 2: SPI1_RX / USART3_RX]
    A --> D[Channel 3: SPI1_TX / USART1_RX]
    A --> E[Channel 4: USART1_TX / TIM1_CH1]
    A --> F[Channel 5: ADC2 / SPI2_RX]
    A --> G[Channel 6: I2C1_TX]
    A --> H[Channel 7: I2C1_RX]

上述流程图展示了典型STM32F1系列中DMA1各通道的默认映射关系。可以看到, Channel 3同时支持SPI1_TX和USART1_RX ,这意味着同一时刻只能有一个外设占用该通道。

当多个外设试图共享同一通道时,会发生 竞争冲突 。解决方法包括:
- 修改外设选择(如改用USART2代替USART1)
- 分时复用通道(需软件协调)
- 切换至另一DMA控制器(若支持)

此外,STM32允许通过AFIO(Alternate Function I/O)重映射部分外设引脚,但DMA通道绑定仍由内部硬连线决定,不可更改。例如,无论USART1的TX/RX引脚是否重映射至PB6/PB7,其DMA请求仍固定关联DMA1_Channel3(RX)。

为避免误配,推荐查阅《STM32F1xx Reference Manual》第11章中的“Table 62. DMA1 request mapping”,获取权威映射信息。

以下表格列出常用外设与DMA通道的对应关系(以STM32F103为例):

外设 方向 DMA控制器 通道编号 请求编号
USART1 RX DMA1 Channel 3 5
USART1 TX DMA1 Channel 4 4
USART2 RX DMA1 Channel 6 6
USART2 TX DMA1 Channel 7 6
ADC1 数据输出 DMA1 Channel 1 1
SPI1 RX DMA1 Channel 2 3
SPI1 TX DMA1 Channel 3 3

注:请求编号用于配置DMA_CCRx中的CHSEL位域,指示具体外设来源。

理解这一映射机制后,开发者可在初始化阶段准确设置DMA通道归属,防止因错误绑定导致传输异常或系统死锁。

2.2 USART1与DMA通道的绑定关系

USART1作为STM32中最常用的异步串行接口之一,常被用于调试输出、协议通信或传感器交互。当其接收端启用DMA时,可极大提升数据吞吐效率,减少中断频率。

2.2.1 USART1_RX对应DMA1 Channel3的硬件连接逻辑

从硬件角度看,USART1模块内部包含一个数据寄存器(DR),每当接收到一帧完整数据时,硬件会自动将其移入DR,并产生一个“接收数据就绪”信号。该信号通过内部总线路由至DMA控制器的请求输入端口。

具体路径如下:

flowchart LR
    A[UART Line] --> B[USART1 Receiver]
    B --> C{Data Ready?}
    C -->|Yes| D[Generate DMA Request]
    D --> E[DMA1 Channel3 Input]
    E --> F[Start Memory Write Transfer]
    F --> G[Store to Buffer in SRAM]

此流程无需CPU干预即可完成整个接收链路。关键在于:
- USART1的RX DMA请求线连接至DMA1的第5号请求输入;
- DMA1_Channel3需配置为“外设到内存”模式(DIR=10b);
- 外设地址指向USART1_DR寄存器(固定地址);
- 内存地址指向用户定义的缓冲区起始位置。

相关地址常量定义如下:

#define USART1_DR_Address    ((uint32_t)&USART1->DR)
uint8_t rx_buffer[256]; // 接收缓冲区

在初始化DMA前,必须确认这些地址的合法性与对齐方式。特别是SRAM区域建议采用4字节对齐,以防总线错误。

2.2.2 多外设竞争时的通道优先级考量

当多个外设共用同一个DMA控制器时,仲裁器将依据优先级决定哪个请求先被响应。STM32的DMA通道优先级分为四级:
- 软件优先级(可编程)
- 硬件优先级(通道编号越小越高)
- 相同级别下按请求顺序轮询

假设系统同时运行ADC采样(DMA1_Channel1)和USART1_RX(DMA1_Channel3),若两者同时发起请求,则Channel1优先执行。

可通过DMA_CCRx寄存器中的PL[1:0]位设置优先级等级:

PL值 描述
00
01
10
11 非常高

示例代码设置USART1_RX通道为“高优先级”:

DMA1_Channel3->CCR |= DMA_CCR_PL_1; // 设置为高优先级 (10b)

参数说明
- DMA_CCR_PL_1 表示第1位(bit10)置1;
- 实际需结合PL[1:0]共同设置,完整写法可为:
c DMA1_Channel3->CCR &= ~DMA_CCR_PL; // 清除原优先级 DMA1_Channel3->CCR |= DMA_CCR_PL_1; // 设置为高

合理设置优先级可避免关键通信被低速采集任务阻塞,保障系统响应及时性。

2.3 DMA寄存器初始化流程

STM32的DMA控制器通过一组专用寄存器实现完全控制。每个通道拥有独立的控制寄存器组,主要包括:

  • DMA_CCRx :控制寄存器(Channel x Control Register)
  • DMA_CNDTRx :数据传输数量寄存器(Number of Data Register)
  • DMA_CPARx :外设地址寄存器(Peripheral Address Register)
  • DMA_CMARx :内存地址寄存器(Memory Address Register)

2.3.1 DMA_CCRx控制寄存器的关键位域设置(如EN、DIR、CIRC、MINC等)

DMA_CCRx 是核心控制寄存器,其位结构如下(以DMA1为例):

位段 名称 功能描述
0 EN 通道使能
1 TCIE 传输完成中断使能
2 HTIE 半传输中断使能
3 TEIE 传输错误中断使能
4 DIR 数据传输方向(0=外设→内存,1=内存→外设)
5 CIRC 循环模式使能
6 PINC 外设地址增量使能
7 MINC 内存地址增量使能
8:9 PSIZE 外设数据宽度(00=8位,01=16位,10=32位)
10:11 MSIZE 内存数据宽度
12 PL 通道优先级
13 MEM2MEM 存储器到存储器模式

针对USART1_RX接收任务,典型配置为:

DMA1_Channel3->CCR = 0; // 先清零
DMA1_Channel3->CCR |= 
    DMA_CCR_EN      |  // 启动通道
    DMA_CCR_TCIE    |  // 开启传输完成中断
    DMA_CCR_TEIE    |  // 出错时中断
    (0 << 4)        |  // DIR=0: 外设→内存
    (0 << 5)        |  // 不启用循环模式
    (0 << 6)        |  // PINC=0: 外设地址不变(始终读USART1->DR)
    (1 << 7)        |  // MINC=1: 内存地址递增
    (0 << 8)        |  // PSIZE=00: 8位外设宽度
    (0 << 10)       |  // MSIZE=00: 8位内存宽度
    (2 << 12);       // PL=10: 高优先级

逐行解读
- 第一行清零是为了防止残留状态干扰;
- TCIE TEIE 用于在中断服务程序中处理完成或错误事件;
- DIR=0 表明数据从外设流向内存;
- PINC=0 因为每次都是从同一外设寄存器(USART1_DR)读取;
- MINC=1 保证数据依次填入缓冲区;
- PSIZE/MSIZE 设为8位匹配字节型传输;
- PL=2 提升优先级以确保及时响应。

2.3.2 DMA_CNDTRx计数寄存器与传输长度设定

DMA_CNDTRx 决定本次DMA操作的最大传输次数(即字节数)。对于接收缓冲区大小为 BUFFER_SIZE 的应用,应在此寄存器中写入该值:

#define BUFFER_SIZE 256
DMA1_Channel3->CNDTR = BUFFER_SIZE;

一旦DMA启动,每成功传输一个单位数据,该计数值自动减1。当归零时,触发TCIF标志(传输完成中断标志)。

注意:若启用循环模式(CIRC=1),则每次归零后自动重载初始值,适合持续流式接收。但对于不定长协议(如Modbus RTU),一般采用单次模式并在中断中重新加载计数。

2.3.3 DMA_CPARx与DMA_CMARx地址寄存器配置方法

这两个寄存器分别指定源地址和目标地址:

DMA1_Channel3->CPAR = (uint32_t)&USART1->DR;   // 外设地址
DMA1_Channel3->CMAR = (uint32_t)rx_buffer;      // 内存地址

关键点说明
- CPAR 必须指向外设数据寄存器物理地址;
- CMAR 指向SRAM中定义的数组首地址;
- 地址必须为32位无符号整型,不可使用指针变量间接赋值;
- 若使用编译器优化,建议添加 volatile 修饰符防止优化丢失。

若缓冲区未对齐或跨边界访问,可能导致HardFault。为此可强制对齐:

__attribute__((aligned(4))) uint8_t rx_buffer[BUFFER_SIZE];

该属性确保缓冲区起始地址为4字节对齐,符合AHB总线访问规范。

2.4 配置代码实现与常见错误排查

2.4.1 基于标准外设库或HAL库的DMA初始化函数编写

以下是基于STM32标准外设库的手动配置函数示例:

void DMA1_Channel3_USART1_RX_Init(uint8_t *buffer, uint16_t size)
{
    // 1. 使能DMA1时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    // 2. 关闭通道(安全操作)
    DMA_Cmd(DMA1_Channel3, DISABLE);
    while (DMA1_Channel3->CCR & DMA_CCR_EN); // 等待关闭

    // 3. 配置寄存器
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = size;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

    DMA_Init(DMA1_Channel3, &DMA_InitStructure);

    // 4. 清除标志位
    DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_TE3);

    // 5. 开启中断(可选)
    DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE);

    // 6. 最后使能通道
    DMA_Cmd(DMA1_Channel3, ENABLE);
}

扩展说明
- 使用结构体方式提高可读性;
- DMA_DIR_PeripheralSRC 表示外设为源(即接收);
- DMA_Mode_Normal 表示非循环模式;
- 在调用前需确保USART1已配置完毕并开启DMAR位(见第四章)。

若使用HAL库,则更简洁:

hdma_usart1_rx.Instance = DMA1_Channel3;
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_HIGH;

HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);

其中 __HAL_LINKDMA 宏将DMA句柄与UART句柄关联,便于后续调用 HAL_UART_Receive_DMA()

2.4.2 通道使能顺序与时钟同步问题分析

DMA配置中最常见的错误之一是 寄存器写入无效 ,根源往往是时钟未正确开启或使能顺序颠倒。

正确的初始化顺序应为:
1. 开启DMA控制器时钟(RCC_AHBENR)
2. 关闭目标通道(确保处于干净状态)
3. 配置CPAR、CMAR、CNDTR、CCR等寄存器
4. 清除可能存在的中断标志
5. 最后使能通道(EN=1)

若在配置过程中通道已使能,部分寄存器将被锁定,导致修改失败。

另一个常见问题是 DMA与USART时钟异步 。由于DMA挂载于AHB总线,而USART依赖APB2,若APB2频率过低或不稳定,可能导致DMA请求丢失。建议:
- 使用HSE主时钟作为SYSCLK来源;
- 确保PCLK2足够高(至少72MHz);
- 在低功耗模式下禁用DMA操作。

综上所述,DMA通道的选择与配置不仅是寄存器操作的堆砌,更是对系统资源、优先级、时序协同的综合考量。只有深入理解底层机制,才能构建稳定高效的嵌入式通信系统。

3. USART1异步通信参数设置(波特率、数据位、停止位等)

在嵌入式系统中,串行通信是实现设备间低速但高可靠数据交换的重要手段。STM32微控制器内置的通用同步/异步收发器(USART)模块支持多种工作模式,其中 异步全双工通信 因其无需共享时钟线、布线简单、兼容性强而被广泛应用。本章将深入剖析USART1在异步模式下的关键通信参数配置机制,涵盖波特率生成原理、数据帧结构定义、寄存器协同控制逻辑,并结合实际代码与硬件行为进行验证分析,为后续DMA接收功能的稳定运行提供基础保障。

3.1 USART模块工作模式详解

3.1.1 异步全双工通信基本原理

异步通信不依赖外部时钟信号来同步发送端和接收端的数据采样节奏,而是通过预先约定的 波特率 (Baud Rate)以及固定的 数据帧格式 来完成信息传递。一个典型的USART异步数据帧由以下部分组成:

  • 起始位(Start Bit) :逻辑低电平,标志一帧数据开始。
  • 数据位(Data Bits) :通常为8或9位,从最低有效位(LSB)开始传输。
  • 校验位(Parity Bit,可选) :用于简单的错误检测,分为奇校验或偶校验。
  • 停止位(Stop Bits) :逻辑高电平,表示数据帧结束,长度可设为1、1.5或2位。

这种通信方式要求收发双方必须严格保持相同的波特率,否则会导致采样偏移,进而引发数据误读。STM32的USART模块通过内部 分数波特率发生器 精确生成所需频率,确保通信稳定性。

sequenceDiagram
    participant T as 发送端(TX)
    participant R as 接收端(RX)

    Note right of T: 起始位(0)
    T->>R: 0 (Start)
    Note right of T: 数据位 D0~D7
    T->>R: D0
    T->>R: D1
    T->>R: D2
    T->>R: D3
    T->>R: D4
    T->>R: D5
    T->>R: D6
    T->>R: D7
    Note right of T: 偶校验位 P
    T->>R: P
    Note right of T: 停止位(1)
    T->>R: 1 (Stop)

上述流程图展示了标准8N1(8数据位、无校验、1停止位)帧结构的传输过程。接收端根据起始边沿重新对齐采样时钟,在每位中间点进行三次采样以提高抗干扰能力。

在STM32中,USART支持全双工通信,即TX和RX独立工作,允许同时发送和接收数据。这使得其非常适合用于与PC、传感器、GPS模块等外设通信。

3.1.2 波特率生成机制与BRR寄存器计算公式

STM32 USART的波特率由以下公式决定:

\text{Baud Rate} = \frac{f_{CK}}{8 \times (2 - OVER8) \times (\text{DIV_MANTISSA} + \frac{\text{DIV_FRACTION}}{16})}

其中:
- $ f_{CK} $:USART输入时钟频率(通常来自PCLK2)
- OVER8 :过采样模式(0=16倍采样,1=8倍采样),由CR1寄存器中的 OVER8 位控制
- DIV_MANTISSA :整数部分,写入BRR[15:4]
- DIV_FRACTION :小数部分,写入BRR[3:0]

当使用16倍采样(默认)时,分频系数简化为:

\text{DIV} = \frac{f_{CK}}{16 \times \text{Baud Rate}}

例如,若系统主频为72MHz,PCLK2也为72MHz,目标波特率为115200bps,则:

\text{DIV} = \frac{72,000,000}{16 \times 115200} \approx 39.0625

因此:
- DIV_MANTISSA = 39 (二进制 100111
- DIV_FRACTION = 0.0625 × 16 = 1

最终写入USART_BRR寄存器的值为:

BRR = (39 << 4) | 1;  // 即 0x271
参数
输入时钟 $ f_{CK} $ 72 MHz
目标波特率 115200 bps
计算分频值 39.0625
MANTISSA 39
FRACTION 1
BRR 写入值 0x271

该计算可通过STM32CubeMX自动完成,但在手动编程时需注意浮点精度误差可能导致实际波特率偏差超过接收容忍范围(一般允许±3%)。建议在关键应用中使用更高精度晶振或启用超频模式优化时钟源。

3.2 关键通信参数配置

3.2.1 数据位(8/9位)、停止位(1/1.5/2)与校验位(奇偶校验)设定

STM32 USART支持灵活的数据帧配置,主要通过三个寄存器位组合实现:

配置项 寄存器 控制位 可选值
数据位 CR1 M[1:0] 00=8位, 10=9位
停止位 CR2 STOP[1:0] 00=1位, 01=0.5位, 10=2位, 11=1.5位
校验使能 CR1 PCE 0=禁用, 1=启用
校验类型 CR1 PS 0=偶校验, 1=奇校验
示例:配置9位数据+偶校验+2停止位
// 设置CR1
USART1->CR1 &= ~(USART_CR1_M_Msk);     // 清除M位
USART1->CR1 |= USART_CR1_M_1;          // M[1:0]=10 → 9位数据
USART1->CR1 |= USART_CR1_PCE;          // 启用校验
USART1->CR1 &= ~(USART_CR1_PS);        // PS=0 → 偶校验

// 设置CR2
USART1->CR2 &= ~(USART_CR2_STOP_Msk);
USART1->CR2 |= (2 << USART_CR2_STOP_Pos); // STOP=10 → 2停止位

逐行解读:
- 第1行:清除 M 字段,防止旧值干扰。
- 第2行:设置 M=10 ,选择9位数据模式(常用于地址/数据复用场景,如Modbus RTU)。
- 第3行:启用校验功能(PCE=1),激活PS位作用。
- 第4行:清零PS位,选择偶校验(Even Parity)。
- 第6–7行:配置2个停止位,增强长距离通信的可靠性。

此配置适用于工业现场总线协议,如某些定制Modbus变种,其中第9位作为地址/命令标识。

3.2.2 硬件流控(RTS/CTS)是否启用的选择依据

硬件流控通过RTS(Request to Send)和CTS(Clear to Send)信号线实现流量控制,防止接收缓冲区溢出。是否启用取决于应用场景:

场景 是否启用流控 理由
高速连续传输(>115200bps) 推荐启用 避免因CPU响应延迟导致丢包
低速间歇通信(如调试打印) 可禁用 简化接线,降低成本
使用RS485总线 不适用 半双工需方向控制而非流控

启用流程如下:

// 使能CTS和RTS功能
USART1->CR3 |= USART_CR3_CTSE | USART_CR3_RTSE;

// 对应GPIO需配置为AF输出
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12;       // CTS=PA11, RTS=PA12
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &gpio);

参数说明:
- CTSE :使能CTS输入监测,仅当CTS为低时才允许发送。
- RTSE :使能RTS输出,当接收准备就绪时拉低通知对方。
- GPIO配置必须匹配AF映射表(参考数据手册Table 10),否则功能无效。

graph TD
    A[发送方准备数据] --> B{RTS是否为低?}
    B -- 是 --> C[启动发送]
    B -- 否 --> D[等待直到RTS变低]
    E[接收方缓冲区满] --> F[拉高RTS]
    F --> G[发送方暂停]

该机制可在DMA尚未完成当前缓冲区处理时及时“背压”上游设备,显著提升系统鲁棒性。

3.3 USART_CR1~CR3寄存器组协同配置

3.3.1 USART_CR1中UE、TE、RE、M等关键位的作用

USART_CR1是核心控制寄存器,包含多个功能开关位:

位名 位置 功能描述
UE bit13 USART Enable,启用整个模块
RE bit2 Receiver Enable,开启接收功能
TE bit3 Transmitter Enable,开启发送功能
M bits[12:12], M0/M1* 数据位长度(配合CR1[12]和CR3[M1])
PCE bit10 Parity Control Enable
PS bit9 Parity Selection(0=Even, 1=Odd)
OVER8 bit15 Oversampling mode(0=16x, 1=8x)

典型初始化顺序:

// 1. 禁用USART(修改前先关闭)
USART1->CR1 &= ~USART_CR1_UE;

// 2. 配置基本功能
USART1->CR1 |= USART_CR1_TE | USART_CR1_RE;      // 启用收发
USART1->CR1 |= USART_CR1_UE;                     // 最后使能模块

⚠️ 注意: 必须在所有参数配置完成后才置位UE ,否则可能触发非法状态机转移。

3.3.2 USART_CR2中STOP位与CLKEN位的影响

USART_CR2主要用于高级控制与时钟管理:

位名 位置 功能
STOP[1:0] [13:12] 设置停止位长度
CLKEN bit11 时钟输出使能(仅同步模式有用)
LINEN bit14 LIN模式使能

即使在异步模式下, STOP 位仍至关重要。例如,在噪声较大的环境中,使用2个停止位可增加帧间隔时间,降低误判风险。

// 设置1.5个停止位(适用于某些老旧设备)
USART1->CR2 &= ~USART_CR2_STOP;
USART1->CR2 |= (1 << USART_CR2_STOP_Pos);  // STOP[1:0] = 01 → 0.5位?
// 实际上:01=0.5, 10=2, 11=1.5 —— 注意编码非线性!
USART1->CR2 |= (3 << USART_CR2_STOP_Pos);  // 正确:1.5停止位

说明: STM32的STOP位编码如下:
- 00 : 1 停止位
- 01 : 0.5 停止位
- 10 : 2 停止位
- 11 : 1.5 停止位

这一设计源于历史兼容性考虑,开发者需特别留意避免误配。

3.3.3 USART_CR3中DMAR与DMAT位与DMA联动机制

USART_CR3负责高级功能扩展,尤其与DMA密切相关:

位名 位置 功能
DMAT bit7 DMA Enable for Transmission
DMAR bit6 DMA Enable for Reception
IRLP bit2 IrDA低功耗模式
SCEN bit5 智能卡模式使能(占用TX/RX引脚)

要实现DMA接收,必须设置 DMAR=1 ;若还需DMA发送,则额外开启 DMAT

// 启用DMA接收
USART1->CR3 |= USART_CR3_DMAR;

// 若使用DMA发送
// USART1->CR3 |= USART_CR3_DMAT;

重要规则:
- 必须在 DMA通道已配置并使能后 再设置 DMAR 位,否则可能产生一次虚假请求。
- 若同时启用TX/RX DMA,应确保DMA优先级合理分配,避免总线竞争。

flowchart LR
    A[USART DR寄存器] -->|数据到达| B{CR3.DMAR=1?}
    B -->|Yes| C[触发DMA请求]
    C --> D[DMA控制器读取DR]
    D --> E[内存缓冲区]

该流程表明,一旦 DMAR 置位,每当USART接收到新字节且RXNE标志置起,就会立即向DMA发出请求,无需CPU干预。

3.4 初始化代码设计与调试验证

3.4.1 使用CubeMX自动生成配置与手动编码对比

CubeMX生成代码(HAL库)
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);

优点:
- 自动处理时钟树、GPIO、NVIC配置
- 可视化界面减少出错概率

缺点:
- 抽象层次高,不利于理解底层机制
- 编译体积大,实时性略差

手动寄存器配置(推荐学习用)
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN;

// 2. PA9(TX), PA10(RX) 复用推挽
GPIOA->CRL &= ~(0xFF << 4);  // 清除CNF9/Mode9 和 CNF10/Mode10
GPIOA->CRL |= (0xB4 << 4);   // PA9: AF PP, 50MHz; PA10: FLOATING INPUT

// 3. 配置USART
USART1->BRR = 0x271;         // 72MHz @115200
USART1->CR1 = USART_CR1_TE | USART_CR1_RE;
USART1->CR2 = 0;
USART1->CR3 = 0;
USART1->CR1 |= USART_CR1_UE; // 最后使能

优势:
- 完全掌控每一步操作
- 可针对特定芯片优化
- 更适合裸机开发或RTOS环境

3.4.2 通过示波器或逻辑分析仪验证波特率准确性

使用逻辑分析仪捕获实际波形是验证配置正确性的黄金标准。

操作步骤:
  1. 连接探头至USART1_TX(PA9)
  2. 发送固定字符 'U' (ASCII 0x55,二进制 01010101
  3. 观察波形是否呈现交替高低电平

预期结果:
- 起始位:低电平(1 bit time)
- 数据位:LSB先行 → 1,0,1,0,1,0,1,0
- 因此整体呈现近似方波形态

测量单bit宽度:
- 理论值:$ \frac{1}{115200} \approx 8.68\mu s $
- 实测值应在 ±3% 范围内(8.42~8.94μs)

若超出容限,检查:
- PLL是否锁定?
- PCLK2是否真为72MHz?
- BRR是否四舍五入误差过大?

表格总结常见波特率对应BRR值(f_PCLK=72MHz):

波特率 DIV 计算值 BRR 十六进制 误差
9600 468.75 0x1D4.C → 0x1D5 <0.1%
19200 234.375 0xEA.6 → 0xEA ~0.8%
115200 39.0625 0x27.1 → 0x271 ~0.4%
921600 4.8828 0x4.E → 0x4E >3% ❌

⚠️ 注意:921600bps在72MHz下误差较大,建议改用更高主频(如120MHz)或启用OVER8=1提升精度。

综上所述,USART1通信参数的精准配置是构建稳定串口链路的前提。唯有深入理解各寄存器之间的协作关系,并辅以实测手段验证,才能为后续DMA高效接收奠定坚实基础。

4. USART1 DMA接收功能使能(USART_CR3寄存器配置)

在STM32嵌入式系统中,串口通信的高效性直接决定了系统的实时响应能力与数据吞吐性能。当采用DMA技术进行数据接收时,核心在于正确配置USART外设与DMA控制器之间的协同机制。其中, USART_CR3 寄存器作为控制高级功能的关键枢纽,其 DMAR 位的设置是实现DMA接收使能的核心步骤。本章将深入剖析该寄存器的作用机制、配置逻辑及工程实践中的关键细节,确保开发者能够构建出稳定、高效的DMA接收通道。

4.1 DMA与USART接口的耦合机制

在理解如何使能DMA接收之前,必须先厘清DMA与USART之间是如何建立物理和逻辑连接的。这种耦合不仅涉及硬件信号路径的设计,还包括寄存器层面的触发条件设定与数据流向控制。

4.1.1 发送与接收DMA请求触发条件分析

STM32的USART模块通过内部逻辑生成DMA请求信号,这些信号被送往DMA控制器以启动数据传输。对于接收方向而言,DMA请求的触发依赖于 数据寄存器非空 状态(即RDR标志置位)。一旦接收到一个字节的数据并移入RDR寄存器,若DMA接收已使能,则会立即向DMA控制器发出读取请求。

该过程遵循如下流程:

graph TD
    A[USART引脚接收到串行数据] --> B[数据完成采样并移入移位寄存器]
    B --> C[移位寄存器数据搬移到RDR接收数据寄存器]
    C --> D[RDR标志位置1]
    D --> E{是否使能DMAR?}
    E -- 是 --> F[产生DMA读请求]
    F --> G[DMA从USART_DR地址读取数据]
    G --> H[存储到指定内存缓冲区]
    E -- 否 --> I[等待CPU轮询或中断处理]

值得注意的是,DMA请求并非在每个比特到达时就触发,而是在整个字符帧(包括起始位、数据位、校验位、停止位)完整接收后才生效。这意味着DMA操作是以“字符”为单位进行搬运的,而非按位操作。

此外,DMA请求信号由硬件自动管理,无需软件干预。只要满足以下两个前提:
- USART处于启用状态(UE=1)
- 对应的DMA使能位(如DMAR)已被置位

此时,每接收到一个有效字节,就会自动触发一次DMA读操作,从而实现“零CPU参与”的连续接收。

4.1.2 DR寄存器与DMA数据通路的物理连接路径

从系统架构角度看,USART的数据寄存器(DR)实际上是一个双端口寄存器:一端面向CPU总线(可通过读写 USART_DR 寄存器访问),另一端则连接至DMA控制器的AHB主接口。

其物理通路可简化为:

模块 接口类型 地址映射 访问权限
USART1_DR AHB Slave 0x40013804 CPU可读写
DMA Controller AHB Master —— 可发起对0x40013804的读操作

当DMA通道配置为从外设地址 &USART1->DR 读取数据时,DMA控制器会在每次DMA请求到来时,通过AHB总线发起一次 单字节读事务 (通常为8位或9位模式下),目标地址固定不变(即指向DR寄存器)。

为了验证这一机制,可以查看STM32F4xx参考手册中关于DMA请求映射表的内容:

DMA请求源 触发事件 数据宽度 方向
USART1_RX RDR不为空 8/9 bits 外设→内存

代码示例如下(使用HAL库风格初始化DMA接收):

// 配置DMA通道用于USART1接收
hdma_usart1_rx.Instance = DMA1_Stream3;
hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
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_CIRCULAR;                   // 循环模式
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;

if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK) {
    Error_Handler();
}

// 将DMA句柄关联到USART1的接收通道
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
逐行解析与参数说明
  • Instance : 指定使用DMA1的Stream3(对应Channel3),这是STM32F4系列中绑定USART1_RX的标准通道。
  • Direction = DMA_PERIPH_TO_MEMORY : 明确数据流方向为从外设(USART_DR)流向内存缓冲区。
  • PeriphInc = DISABLE : 因为始终从同一个外设寄存器(DR)读取,所以地址不可变。
  • MemInc = ENABLE : 接收缓冲区需要顺序写入多个字节,因此内存地址需自增。
  • Mode = DMA_CIRCULAR : 设置为循环模式,允许DMA在缓冲区满后自动重置指针继续填充,适用于持续监听场景。
  • __HAL_LINKDMA(...) : 宏定义用于将DMA句柄与UART结构体绑定,便于后续调用 HAL_UART_Receive_DMA()

该配置完成后,DMA便具备了从USART1_DR自动读取数据的能力,但前提是 必须打开USART_CR3中的DMAR位 ,否则DMA请求不会被释放。

4.2 USART_CR3寄存器关键位操作

USART_CR3 是STM32 USART模块的扩展控制寄存器,位于基地址偏移 0x14 处,共32位,主要用于启用高级功能,如DMA支持、智能卡模式、LIN模式等。其中,与DMA接收最相关的是第6位—— DMAR

4.2.1 DMAR位(DMA Enable Receiver)的置位时机与影响

DMAR 位(bit 6)用于使能 接收器的DMA模式 。当此位置1时,USART在每次接收到有效数据并将其放入RDR寄存器后,会自动向DMA控制器发送一个请求信号,促使DMA从 USART_DR 读走数据。

寄存器位定义如下:

名称 功能描述
6 DMAR 1: 使能接收DMA;0: 禁止

重要特性包括:

  • 只影响接收方向 :即使设置了 DMAT (发送DMA使能),也不影响 DMAR 的行为。
  • 不影响中断机制 :即便开启了DMA,如果同时使能了RXNEIE中断,则仍可能触发中断。推荐关闭此类中断以避免冲突。
  • 必须在DMA通道使能前设置 :虽然硬件允许动态修改,但最佳实践是在DMA启动前完成配置,防止出现竞争条件。

典型配置方式如下(基于LL库):

// 使用LL库配置USART1 CR3寄存器
LL_USART_Enable(USART1);                          // 先使能USART
LL_USART_ConfigAsyncMode(USART1);                 // 异步模式

// 开启DMA接收使能
SET_BIT(USART1->CR3, USART_CR3_DMAR);

// 此时可安全启动DMA接收
LL_DMA_ConfigAddresses(DMA1, LL_DMA_STREAM_3,
                       (uint32_t)&(USART1->DR),           // 源地址:USART1_DR
                       (uint32_t)rx_buffer,               // 目标地址:用户缓冲区
                       LL_DMA_DIRECTION_PERIPH_TO_MEMORY);

LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_3, BUFFER_SIZE);
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_3);
执行逻辑分析
  1. LL_USART_Enable(USART1) :激活USART模块,使其开始监听RX引脚。
  2. SET_BIT(...DMAR) :通过位操作宏直接置位CR3寄存器第6位,开启DMA接收请求输出。
  3. LL_DMA_ConfigAddresses :设置DMA传输三要素——源地址、目标地址、方向。
  4. LL_DMA_EnableStream :最终使能DMA流,开始监听来自USART的请求。

⚠️ 注意:若在DMA已运行后再设置 DMAR ,可能导致第一个字节丢失,因为RDR中已有数据但尚未触发DMA请求。

4.2.2 其他相关控制位(如IRDA_MODE、SCEN)的干扰排除

尽管 DMAR 是主要关注点,但 USART_CR3 中其他位也可能间接影响DMA行为:

位域 名称 是否影响DMA接收 建议设置
bit 0 EIE 错误中断使能 若使用DMA,建议关闭以减少中断负载
bit 1 IREN IrDA模式使能 若未使用红外通信,应清零
bit 2 IRLP IrDA低功耗模式 同上
bit 3 HDSEL 半双工选择 若全双工通信,应清零
bit 5 SCEN 智能卡使能 若非智能卡应用,必须禁用
bit 7 DMAT 发送DMA使能 若仅接收,可不设置

特别需要注意的是 SCEN (Smartcard mode enable)位。一旦启用,USART会进入特殊协议模式,改变数据帧格式,并可能屏蔽常规DMA行为。因此,在普通异步通信中务必确保该位为0。

可以通过以下代码统一清理无关功能:

// 清除所有非必要的高级功能
CLEAR_BIT(USART1->CR3, 
          USART_CR3_SCEN   |  // 禁用智能卡
          USART_CR3_IREN   |  // 禁用IrDA
          USART_CR3_HDSEL  |  // 禁用半双工
          USART_CR3_EIE);     // 关闭错误中断(除非需要)

这一步有助于提高系统的可预测性和稳定性。

4.3 双向DMA传输的潜在冲突处理

在某些应用场景中,用户希望同时使用DMA进行串口的 发送与接收 ,即双向DMA传输。虽然STM32支持此功能,但在实际部署中存在资源竞争与同步问题,需谨慎设计。

4.3.1 同时启用TX/RX DMA的风险评估

DMAT=1 DMAR=1 时,USART会分别为发送和接收创建独立的DMA请求通道。这两个请求分别连接到不同的DMA通道(如DMA1_Stream3用于RX,Stream2用于TX),理论上互不干扰。

然而,潜在风险包括:

  1. 总线带宽竞争 :若频繁收发大数据包,AHB总线上可能出现拥塞,导致DMA延迟增加。
  2. 优先级倒置 :若TX DMA优先级高于RX,可能导致接收缓冲区溢出(ORE标志置位)。
  3. 缓冲区管理复杂化 :双缓冲或多缓冲策略需精心设计,否则易引发数据覆盖或访问越界。
  4. 调试困难 :错误往往表现为间歇性丢包,难以复现。

例如,以下配置虽合法但需注意:

// 同时使能发送与接收DMA
SET_BIT(USART1->CR3, USART_CR3_DMAT | USART_CR3_DMAR);

// 分别配置TX和RX的DMA流
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_2);  // TX
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_3);  // RX
表格:DMA流资源分配建议(STM32F407为例)
USART RX DMA Stream TX DMA Stream 通道号 最佳优先级设置
USART1 DMA1_Stream3 DMA1_Stream2 Ch4 RX: High, TX: Medium
USART2 DMA1_Stream5 DMA1_Stream6 Ch4 RX: High, TX: Medium
USART6 DMA2_Stream1 DMA2_Stream6 Ch5 RX: High, TX: Low

建议在高可靠性系统中优先保证 接收通道具有更高DMA优先级 ,以防数据丢失。

4.3.2 单向接收模式下的最优配置策略

对于大多数监控类应用(如传感器采集、Modbus从机),仅需DMA接收即可。此时应采取以下优化措施:

  1. 禁用DMAT位 :减少不必要的DMA请求生成。
  2. 使用循环模式(Circular Mode) :让DMA持续填充同一缓冲区,适合长期监听。
  3. 结合IDLE中断检测不定长帧 :利用USART的IDLE线空闲中断判断一帧结束。
  4. 关闭RXNE中断 :避免重复通知,仅依靠DMA完成中断或IDLE中断处理。

示例配置片段:

// 仅启用DMA接收,禁用发送DMA
SET_BIT(USART1->CR3, USART_CR3_DMAR);
CLEAR_BIT(USART1->CR3, USART_CR3_DMAT);

// 配置DMA为循环模式
LL_DMA_SetMode(DMA1, LL_DMA_STREAM_3, LL_DMA_MODE_CIRCULAR);

// 启用IDLE中断来检测帧结束
LL_USART_EnableIT_IDLE(USART1);
NVIC_EnableIRQ(USART1_IRQn);

随后在中断服务程序中判断是否发生IDLE事件,并计算当前已接收字节数:

void USART1_IRQHandler(void)
{
    if (LL_USART_IsActiveFlag_IDLE(USART1)) {
        uint16_t received_len = BUFFER_SIZE - LL_DMA_GetDataLength(DMA1, LL_DMA_STREAM_3);
        // 处理接收到的数据帧
        ProcessReceivedFrame(rx_buffer, received_len);
        // 清除IDLE标志
        LL_USART_ClearFlag_IDLE(USART1);
    }
}

这种方式极大提升了对变长协议的支持能力,如JSON、AT指令集等。

4.4 实际工程中的使能顺序与异常恢复

在真实项目开发中,DMA接收的稳定性不仅取决于配置正确,更依赖于合理的启动顺序与容错机制。

4.4.1 必须先开启DMA再使能USART_DR接收的依赖关系

一个常见的误区是:先使能USART接收中断或等待数据,再配置DMA。这会导致 首个字节丢失 ,因为RDR中已有数据但DMA尚未准备好。

正确的启动顺序应为:

  1. 初始化GPIO、USART基本参数(波特率、数据位等)
  2. 配置DMA通道(地址、长度、方向、模式)
  3. 使能DMA流
  4. 设置USART_CR3.DMAR = 1
  5. 最后使能USART(UE=1)

顺序不可颠倒,尤其是第3、4步必须在UE之前完成。

// ✅ 正确顺序
MX_GPIO_Init();
MX_USART1_UART_Init();            // 包含波特率等设置

// 配置DMA
HAL_DMA_Start(&hdma_usart1_rx, 
              (uint32_t)&USART1->DR, 
              (uint32_t)rx_buffer, 
              BUFFER_SIZE);

// 使能DMA接收
SET_BIT(USART1->CR3, USART_CR3_DMAR);

// 最后开启USART
LL_USART_Enable(USART1);

若顺序错误,可能造成:
- 第一个字节滞留在RDR中,无法触发DMA
- 后续字节正常传输,但首字丢失导致协议解析失败

4.4.2 错误状态检测与重新启动机制设计

即使配置无误,运行中仍可能发生DMA传输错误(如总线故障、地址不对齐、流冲突等)。为此应设计健壮的恢复机制。

常见错误来源及应对策略:

错误类型 检测方法 恢复动作
传输错误(TEIF) 查询DMA_ISR.TEIFi 停止流 → 清标志 → 重启DMA
缓冲区溢出(ORE) 查看USART_SR.ORE 清除ORE标志,重启DMA
总线错误(BEIF) DMA中断标志 检查地址对齐与权限
流禁用(TEIF + FEIF组合) 中断上下文判断 重新配置并启动

提供一个通用的DMA错误处理函数框架:

void DMA_Recovery_Routine(void)
{
    if (__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_TEIF3)) {
        // 传输错误发生
        __HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_TEIF3);
        // 停止当前传输
        LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_3);
        // 重新加载参数
        LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_3, BUFFER_SIZE);
        LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_3);
        // 可选:记录错误日志
        ErrorLog_Add(ERROR_DMA_TRANSFER_FAIL, __LINE__);
    }
}

此外,建议定期检查DMA运行状态:

// 在主循环中监控DMA健康状况
if (!LL_DMA_IsEnabledStream(DMA1, LL_DMA_STREAM_3)) {
    // 意外关闭?尝试重启
    Reinitialize_USART_DMA_Receive();
}

综上所述,只有在严格遵守配置顺序、合理处理异常的前提下,才能确保USART1 DMA接收长期稳定运行。

5. 内存缓冲区定义与DMA传输大小设置

在嵌入式系统中,高效的数据采集依赖于合理的内存管理机制。当使用STM32的DMA技术实现USART1异步串行通信的接收功能时,核心环节之一便是对接收数据的存储空间进行科学规划——即 内存缓冲区的定义 以及 DMA传输长度的设定 。这两个参数不仅直接影响系统的稳定性、吞吐能力,还决定了能否支持复杂协议(如不定长帧、变长报文)下的可靠接收。

本章将深入探讨静态缓冲区与环形缓冲区的设计差异,分析如何根据应用层协议特征预估缓冲区大小,并通过实际代码展示对齐属性的应用技巧。同时,结合DMA_CNDTRx寄存器的工作机制,详细解析初始传输计数的配置逻辑,讨论溢出保护策略和自动重载机制的选择依据。最终目标是构建一个既能满足高性能需求,又具备良好容错性的接收架构。

5.1 缓冲区设计原则与选型策略

在嵌入式通信场景中,数据往往以“流”的形式连续到达,而处理器处理这些数据的速度通常无法做到实时响应每一个字节。因此,必须引入中间缓存来桥接外设高速输入与CPU低频处理之间的速度差。对于DMA接收而言,这一中间载体就是 内存缓冲区

5.1.1 静态缓冲区 vs 环形缓冲区

最常见的两种缓冲结构为 静态线性缓冲区 环形缓冲区(Circular Buffer) 。它们各有优劣,适用于不同的应用场景。

特性 静态缓冲区 环形缓冲区
实现复杂度 极低,仅需数组声明 中等,需维护读写指针
内存利用率 固定,易浪费或溢出 动态复用,高利用率
是否支持持续接收 否,满后停止 是,可循环覆盖
数据完整性保障 强(完整帧保存) 弱(可能被覆盖)
适用协议类型 固定包长、短报文 不定长、高频数据流

从上表可以看出,若应用要求接收Modbus RTU这类固定格式的协议包,且每帧不超过256字节,则采用静态缓冲即可;但若用于GPS模块、传感器阵列等持续输出数据的设备,则推荐使用环形缓冲配合中断回调机制,避免频繁重启DMA。

示例:静态缓冲区定义
#define RX_BUFFER_SIZE  256
uint8_t rx_buffer[RX_BUFFER_SIZE]; // 静态分配,位于SRAM

该方式简单直接,但存在明显缺陷:一旦DMA完成一次传输(CNDTR归零),就必须由软件重新加载计数值并重置地址,否则后续数据将丢失。这对于需要不间断监听的应用来说显然不可接受。

改进方案:启用DMA循环模式
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; // 关键设置
DMA_InitStruct.DMA_BufferSize = RX_BUFFER_SIZE;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;

此时DMA通道进入循环模式(CIRC位使能),每当完成一轮传输,硬件自动重载 DMA_CNDTRx 寄存器,无需CPU干预,从而实现无缝接收。然而,这也带来新问题:旧数据未被及时读取前即可能被新数据覆盖。

为此,引入更高级的 双缓冲机制(Double Buffer Mode) 成为必要选择。

5.1.2 双缓冲机制详解与mermaid流程图说明

双缓冲模式允许DMA在两个独立的内存区域之间交替传输。当一个缓冲正在被DMA填充时,另一个可以安全地被CPU读取,极大提升了并发处理能力。

graph TD
    A[DMA Start] --> B{Current Buffer: BUF_A}
    B --> C[DMA Writes Data to BUF_A]
    C --> D[Half Transfer Interrupt?]
    D --> E[Notify CPU: BUF_B Ready for Read]
    C --> F[Full Transfer Complete?]
    F --> G[Switch to BUF_B, Trigger TCIF]
    G --> H[CPU Processes BUF_A]
    H --> I[DMA Begins Writing to BUF_B]
    I --> J[Repeat Alternately]

上述流程清晰展示了双缓冲的交替工作机制。其关键优势在于:
- 无锁访问 :CPU与DMA操作不同物理缓冲,避免竞争。
- 低延迟响应 :可在半传输(HTIF)和全传输(TCIF)时分别触发中断,提前准备处理。
- 高吞吐保障 :理论上可实现无限连续接收。

要启用此模式,在HAL库中需如下配置:

DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; // 注意:双缓冲下不能设为Circular
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)buffer_a;
DMA_InitStruct.DMA_Memory1BaseAddr = (uint32_t)buffer_b;
DMA_InitStruct.DMA_DoubleBufferMode = ENABLE;

底层寄存器层面, DMA_CCRx 中的 DBM 位被置1,同时 CT 位用于指示当前活跃缓冲(0=Memory0, 1=Memory1)。每次传输结束后,硬件自动切换 CT 状态。

5.1.3 地址对齐与总线异常防护

ARM Cortex-M内核(如STM32F4/F7/H7系列)普遍采用AMBA AHB总线架构,要求DMA访问的内存地址满足自然对齐条件。例如,若传输宽度为 Word (32位),则源/目的地址必须为4字节对齐。

未对齐可能导致以下后果:
- 总线错误(Bus Fault)
- DMA挂起或传输失败
- 系统HardFault崩溃

因此,建议使用编译器扩展关键字强制对齐:

__attribute__((aligned(4))) uint8_t rx_buffer[256];

或使用静态分配确保边界:

uint8_t __attribute__((section(".dma_buffer"))) rx_buffer_aligned[256] 
    __attribute__((aligned(32))); // 按32字节对齐,优化DMA性能

此外,某些高端MCU(如STM32H7)支持D-Cache一致性管理,若缓冲区位于可缓存区域,还需执行如下操作防止脏数据:

SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

这确保了DMA写入的内容能被CPU正确读取,而非从Cache中获取过期副本。

5.1.4 边界检查与溢出保护策略

即使采用了环形或双缓冲机制,仍需防范因数据速率过高导致的处理滞后问题。常见的防护手段包括:

  1. 定时扫描机制 :主循环定期检查缓冲区占用率,超过阈值则丢弃陈旧数据或上报警告。
  2. 硬件流量控制 :启用RTS信号反压上游设备,减缓发送速率。
  3. 软件协议级确认 :如接收端回传ACK,控制发送频率。

此外,可在中断服务程序中加入统计逻辑:

volatile uint32_t dma_overrun_count = 0;

void DMA1_Channel3_IRQHandler(void) {
    if (DMA1->ISR & DMA_ISR_TEIF3) { // 传输错误中断
        dma_overrun_count++;
        DMA1->IFCR |= DMA_IFCR_CTEIF3; // 清除标志
        // 可在此处软重启DMA或通知上层
    }
}

通过长期监控 dma_overrun_count ,可评估系统负载压力,进而调整缓冲区大小或升级主频。

5.1.5 缓冲区大小估算方法论

合理设定缓冲区长度是平衡资源占用与性能的关键。过大浪费RAM,过小则频繁中断影响效率。

通用估算公式如下:

\text{Buffer Size} = \frac{\text{Max Data Rate (bps)} \times \text{Max Latency Tolerance (s)}}{8}

举例:某设备以115200 bps速率发送JSON数据包,最大容忍延迟为100ms:

\frac{115200 \times 0.1}{8} = 1440 \, \text{bytes}

因此至少应配置1500字节以上缓冲区。若协议本身有明确帧头帧尾(如$…*checksum\r\n),还可结合状态机实现动态截断,进一步降低需求。

5.1.6 小结:缓冲区设计决策树

综合以上因素,形成如下设计决策路径:

graph LR
    A[是否连续高频数据?] -- 是 --> B[启用环形或双缓冲]
    A -- 否 --> C[使用静态缓冲+单次传输]
    B --> D[是否允许数据覆盖?]
    D -- 否 --> E[采用双缓冲+中断处理]
    D -- 是 --> F[环形缓冲+用户轮询]
    C --> G[是否固定包长?]
    G -- 是 --> H[设置CNDTR=包长]
    G -- 否 --> I[配合空闲中断IDLE检测]

该模型可指导开发者快速定位最优方案。

5.2 DMA传输长度配置与运行时管理

DMA控制器通过 DMA_CNDTRx 寄存器控制本次传输的字节数(或字数)。该值一经设置,便在每次启动后递减直至归零,触发传输完成事件。因此,正确初始化并适时更新该寄存器至关重要。

5.2.1 DMA_CNDTRx 寄存器工作机制解析

每个DMA通道对应一个 DMA_CNDTRx 寄存器(x为通道号),其位宽决定最大传输长度。例如在STM32F4系列中,该寄存器为16位,故最大值为65535。

关键行为特性如下:
- 初始值由软件写入。
- 每完成一次数据传输(按 PSIZE MSIZE 单位),寄存器自动减1。
- 归零后产生中断(若使能),并可自动重载(仅限Circular模式)。
- 不支持运行时动态修改(除非通道关闭)。

示例代码:

DMA1_Channel3->CNDTR = RX_BUFFER_SIZE; // 设置传输长度

若使用HAL库:

HAL_DMA_Start(&hdma_usart1_rx, 
              (uint32_t)&USART1->DR, 
              (uint32_t)rx_buffer, 
              RX_BUFFER_SIZE);

此处第三个参数即传输项数,HAL内部会将其写入 CNDTR

5.2.2 固定长度 vs 动态长度接收策略对比

方式 优点 缺点 适用场景
固定长度(CNDTR预设) 实现简单,中断明确 浪费带宽,不适应变长帧 Modbus TCP、CAN FD
结合IDLE中断检测 支持任意长度帧 需额外使能USART中断 JSON、NMEA-0183
双缓冲+半传输中断 高效流水处理 占用更多RAM 音频流、图像传输

其中, IDLE中断 是一种极为实用的技术:当USART线路连续一段时间无活动(即“空闲”)时,硬件触发中断,表明一帧数据已结束。此时可通过查询 DMA_CNDTRx 值得知实际接收字节数:

uint16_t bytes_received = RX_BUFFER_SIZE - DMA1_Channel3->CNDTR;

然后调用解析函数处理 rx_buffer[0..bytes_received-1] 区间数据。

5.2.3 运行时重配置流程与注意事项

在非循环模式下,DMA传输完成后通道自动禁用。若需继续接收,必须重新配置 CNDTR 并再次使能通道。

典型重启步骤如下:
1. 等待当前传输完成(检查 GIF TCIF 标志)
2. 停止DMA通道(清 EN 位)
3. 重置 CNDTR
4. 若使用相同缓冲区,无需重设 CMAR
5. 重新置位 EN 启动下一轮

if (__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_TCIF3)) {
    __HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_TCIF3);

    // 重启DMA接收
    hdma_usart1_rx.Instance->CNDTR = RX_BUFFER_SIZE;
    __HAL_DMA_ENABLE(&hdma_usart1_rx);
}

注意: 不能在DMA运行期间修改 CNDTR ,否则会导致不可预测行为。必须先关闭通道。

5.2.4 使用环形缓冲时的长度管理陷阱

尽管循环模式简化了配置,但也隐藏风险。由于 CNDTR 始终处于递减-重载循环中,无法直接反映“最新接收位置”。若CPU想获取当前已写入的数据量,需借助辅助变量或外部触发机制。

一种解决方案是结合定时器采样 CNDTR 值:

uint16_t last_pos = 0;
uint16_t current_pos = RX_BUFFER_SIZE - DMA1_Channel3->CNDTR;
if (current_pos >= last_pos) {
    memcpy(app_buffer, &rx_buffer[last_pos], current_pos - last_pos);
} else {
    memcpy(app_buffer, &rx_buffer[last_pos], RX_BUFFER_SIZE - last_pos);
    memcpy(app_buffer + (RX_BUFFER_SIZE - last_pos), rx_buffer, current_pos);
}
last_pos = current_pos;

此即典型的“环形到线性”拷贝逻辑,常用于日志记录或调试输出。

5.2.5 多通道协同下的传输长度协调

在复杂系统中,多个外设共享DMA资源(如ADC+USART+SPI),需统一规划各通道的传输规模,防止总线拥堵。

建议遵循以下原则:
- 高优先级通道(如实时控制)设置较小 CNDTR ,提升响应速度。
- 低优先级大批量传输(如文件下载)可分段进行,避免独占总线。
- 合理分配通道优先级(Low/Medium/High/Very High)

例如:

hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_adc1.Init.Priority = DMA_PRIORITY_VERY_HIGH;

并通过 DMA_SetConfig() 确保顺序注册,避免冲突。

5.2.6 表格总结:常见应用场景下的配置建议

应用类型 缓冲区类型 DMA模式 CNDTR设置 中断策略
Modbus主机轮询 静态缓冲 Normal 固定长度(256) TCIF完成中断
GPS模块NMEA语句 环形缓冲 Circular 全长(512) IDLE+DMA双中断
蓝牙透传模块 双缓冲 Double Buffer 每块256 HTIF+TCIF
固件OTA升级 静态缓冲 Normal 包长(1024) TCIF+校验
音频PCM流采集 双缓冲 Double Buffer 128 samples HTIF实时处理

此表可作为项目初期选型参考,减少试错成本。

5.3 编译器属性与内存布局优化技巧

现代嵌入式开发中,编译器提供的扩展属性可用于精细控制变量的内存分布,这对DMA稳定运行具有重要意义。

5.3.1 __attribute__((aligned)) 的深度应用

如前所述,DMA传输要求地址对齐。虽然多数情况下栈或全局数组默认对齐,但在涉及DMA时仍建议显式声明:

uint8_t rx_buf_unaligned[256];           // ❌ 存在风险
uint8_t __attribute__((aligned(4))) rx_buf_aligned[256]; // ✅ 推荐

aligned(N) 确保变量起始地址是N的倍数。对于32位传输,N≥4;对于DMA Burst模式,建议按8或16字节对齐以提升总线效率。

5.3.2 自定义内存段放置(Linker Script干预)

为进一步优化性能,可将DMA专用缓冲区放置于特定SRAM区域(如SRAM1而非CCM RAM),或排除Cache区域。

示例链接脚本片段( .ld 文件):

MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
  SRAM1 (rwx) : ORIGIN = 0x20000000, LENGTH = 112K
  SRAM2 (rwx) : ORIGIN = 0x2001C000, LENGTH = 16K
}

SECTIONS
{
  .dma_buffer ALIGN(4) : 
  {
    *(.dma_buffer)
  } > SRAM1
}

结合C代码:

uint8_t __attribute__((section(".dma_buffer"))) dma_rx_buf[512];

这样可确保缓冲区位于高速SRAM1中,且不会与其他关键任务争抢资源。

5.3.3 volatile关键字的必要性

DMA缓冲区若被中断和服务程序共同访问,必须声明为 volatile ,防止编译器优化导致数据不可见:

volatile uint8_t rx_buffer[256]; // ✅ 必须加volatile

否则可能出现:CPU认为缓冲区未变,跳过处理逻辑,造成数据丢失。

5.3.4 零拷贝设计思想与应用延伸

理想状态下,DMA接收的数据应尽可能减少中间复制。例如:
- 直接将缓冲区指向协议解析器输入区
- 使用 union 封装不同类型访问视图
- 在RTOS中传递缓冲区句柄而非内容

typedef union {
    uint8_t raw[256];
    struct {
        uint16_t cmd;
        uint8_t data[254];
    } pkt;
} RxPacket_t;

volatile RxPacket_t __attribute__((aligned(4))) rx_pkt;

这种方式实现“零拷贝”,显著降低CPU开销。

5.3.5 安全性增强:MPU保护机制引入(可选)

在安全敏感系统中,可通过内存保护单元(MPU)限制DMA只能访问指定区域,防止越界写入破坏代码或其他数据区。

示例(Cortex-M4 MPU配置):

MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)rx_buffer;
MPU_InitStruct.Size = MPU_REGION_SIZE_1KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);

此举可在硬件层面拦截非法DMA写操作,提升系统鲁棒性。

5.3.6 综合代码示例:完整缓冲区配置模板

// 定义双缓冲区,4字节对齐
__attribute__((aligned(4)))
uint8_t buffer_a[256], buffer_b[256];

// DMA句柄(HAL库)
DMA_HandleTypeDef hdma_usart1_rx;

void MX_DMA_Init(void) {
    __HAL_RCC_DMA1_CLK_ENABLE();

    hdma_usart1_rx.Instance = DMA1_Channel3;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeripheralInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeripheralDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL; // 双缓冲禁用Circular
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    hdma_usart1_rx.Init.DoubleBufferMode = ENABLE;
    hdma_usart1_rx.Init.SecondaryMemoryBuffer = (uint32_t)buffer_b;

    HAL_DMA_Init(&hdma_usart1_rx);

    // 启动DMA接收
    HAL_DMA_Start(&hdma_usart1_rx, 
                  (uint32_t)&USART1->DR, 
                  (uint32_t)buffer_a, 
                  256);

    // 使能中断
    __HAL_DMA_ENABLE_IT(&hdma_usart1_rx, DMA_IT_TC | DMA_IT_HT);
    HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
}

逐行逻辑分析:

行号 说明
__attribute__((aligned(4))) 确保两缓冲区均4字节对齐,符合总线要求
hdma_usart1_rx.Init.Mode = DMA_NORMAL 双缓冲模式下不可使用Circular,否则行为未定义
Init.DoubleBufferMode = ENABLE 开启双缓冲功能
SecondaryMemoryBuffer = buffer_b 指定第二缓冲区地址
HAL_DMA_Start(...) 初始化DMA,将buffer_a设为主缓冲
__HAL_DMA_ENABLE_IT(...) 使能半传输与全传输中断,实现交替通知

此模板可直接集成至CubeMX生成工程中,具备高度复用性。

6. DMA中断服务程序编写与传输完成回调处理

在嵌入式系统中,直接存储器访问(DMA)技术通过将数据搬运任务从CPU卸载至专用硬件模块,显著提升了外设通信的效率。然而,当DMA传输完成、半程或发生错误时,系统仍需具备及时响应的能力,以确保数据处理的实时性和可靠性。这就引出了 中断机制的重要性 ——它是连接底层硬件事件与上层应用逻辑的关键桥梁。本章聚焦于如何编写高效、可维护的DMA中断服务程序(ISR),并设计灵活的回调处理机制,实现传输状态的精准捕获与应用层解耦。

6.1 DMA中断类型与标志位识别

DMA控制器在运行过程中会根据预设条件触发不同类型的中断信号,这些中断为开发者提供了对传输生命周期的细粒度控制能力。理解各类中断的本质及其对应的标志位是构建健壮中断处理逻辑的前提。

6.1.1 传输完成(TCIF)、半传输(HTIF)、传输错误(TEIF)中断的区别

STM32的DMA通道支持三种主要中断源:

中断类型 标志位名称 触发条件 应用场景
传输完成中断 TCIF (Transfer Complete Interrupt Flag) 当DMA_CNDTRx计数器减至0时触发 表示一整块数据已成功搬运完毕,常用于通知应用层取数
半传输中断 HTIF (Half Transfer Interrupt Flag) 当计数器达到初始值的一半时触发 适用于双缓冲或多阶段处理场景,提前准备下一阶段资源
传输错误中断 TEIF (Transfer Error Interrupt Flag) 发生地址不对齐、访问冲突或总线错误等异常时触发 故障诊断与恢复机制的核心依据

这三类中断可在DMA通道的控制寄存器 DMA_CCRx 中独立使能。例如,在接收串口数据时启用 TCIE TEIE 位即可同时监听完成和错误事件。

Mermaid 流程图:DMA中断触发逻辑
graph TD
    A[DMA Start] --> B{Data Remaining?}
    B -- Yes --> C[Continue Transfer]
    B -- No --> D[Set TCIF = 1]
    C --> E{Count == Half Initial?}
    E -- Yes --> F[Set HTIF = 1]
    E -- No --> G[Next Cycle]
    H[Bus Error / Misalignment] --> I[Set TEIF = 1]
    D --> J[Generate IRQ if TCIE=1]
    F --> K[Generate IRQ if HTIE=1]
    I --> L[Generate IRQ if TEIE=1]

该流程清晰展示了DMA在每次传输周期中如何动态判断是否应置位相应中断标志。

6.1.2 中断标志清除的正确时序与寄存器操作

尽管中断标志由硬件自动置位,但其清除必须由软件显式执行,且顺序极为关键。若处理不当,可能导致中断重复触发或丢失。

每个DMA通道的状态由 DMA_ISR 寄存器统一管理,而清除操作则需写入 DMA_IFCR 寄存器对应的位置“1”来清零。以DMA1 Channel3为例:

  • TCIF3 : 对应 DMA_IFCR 的 bit 5
  • HTIF3 : bit 4
  • TEIF3 : bit 3
示例代码:安全清除DMA中断标志
void DMA1_Channel3_IRQHandler(void) {
    // 检查是否为传输完成中断
    if (DMA1->ISR & DMA_ISR_TCIF3) {
        // 处理完后必须先清除标志再退出
        DMA1->IFCR |= DMA_IFCR_CTCIF3;  // 写1清零TCIF3
        // 调用用户回调函数(后续章节详述)
        USART_DMA_RxComplete_Callback();
    }

    // 检查是否有传输错误
    if (DMA1->ISR & DMA_ISR_TEIF3) {
        DMA1->IFCR |= DMA_IFCR_CTEIF3;  // 清除错误标志
        USART_DMA_Error_Callback();     // 错误处理回调
    }

    // 可选:处理半传输中断
    if (DMA1->ISR & DMA_ISR_HTIF3) {
        DMA1->IFCR |= DMA_IFCR_CHTIF3;
        USART_DMA_HalfTransfer_Callback();
    }
}
参数说明与逻辑分析:
  • DMA1->ISR : 只读状态寄存器,反映当前所有通道的中断状态。
  • DMA_ISR_TCIF3 : 宏定义为 (1 << 5) ,表示第3通道的传输完成标志。
  • DMA1->IFCR |= DMA_IFCR_CTCIF3 : 使用“或等于”确保不影响其他通道的清除操作。
  • 重要原则 :必须在处理完业务逻辑后再清除标志,否则可能遗漏中断;同时避免只读不写导致标志残留。

⚠️ 常见误区:使用 &=~ 来清除标志是无效的,因为 IFCR 是“写1清零”机制,不能通过清零位来关闭它。

6.2 中断服务例程(ISR)结构设计

中断服务例程的设计质量直接影响系统的稳定性与可扩展性。一个良好的ISR应当具备快速响应、低延迟、高可读性以及易于维护的特点。

6.2.1 在stm32fxxx_it.c中编写DMA1_Channel3_IRQHandler

标准工程模板中,中断向量表映射决定了具体函数名。对于STM32F1系列,DMA1 Channel3的中断处理函数位于 stm32f1xx_it.c 文件中,函数名为 DMA1_Channel3_IRQHandler

典型ISR结构如下:
void DMA1_Channel3_IRQHandler(void) {
    uint32_t isr_reg = DMA1->ISR;

    // 判断是否属于Channel3的中断
    if (isr_reg & (DMA_ISR_TEIF3 | DMA_ISR_HTIF3 | DMA_ISR_TCIF3)) {

        // 优先处理错误(最紧急)
        if (isr_reg & DMA_ISR_TEIF3) {
            DMA1->IFCR |= DMA_IFCR_CTEIF3;
            HAL_DMA_ErrorCallback(&hdma_usart1_rx);  // 若使用HAL库
        }

        // 其次处理半传输
        if (isr_reg & DMA_ISR_HTIF3) {
            DMA1->IFCR |= DMA_IFCR_CHTIF3;
            HAL_DMA_HalfCpltCallback(&hdma_usart1_rx);
        }

        // 最后处理完成中断
        if (isr_reg & DMA_ISR_TCIF3) {
            DMA1->IFCR |= DMA_IFCR_CTCIF3;
            HAL_DMA_CpltCallback(&hdma_usart1_rx);
        }
    }
}
逐行解读:
  1. uint32_t isr_reg = DMA1->ISR;
    → 缓存一次状态寄存器值,防止在判断过程中被外部修改,提高原子性。

  2. if (isr_reg & ...)
    → 批量检查多个标志位,减少多次读取寄存器带来的开销。

  3. 错误优先级最高,第一时间响应异常情况。

  4. 回调函数调用采用指针方式传递句柄(如 hdma_usart1_rx ),便于与HAL库兼容。

  5. 每次清除标志后立即调用对应处理函数,保证状态一致性。

6.2.2 判断中断来源并调用对应处理函数

为了增强可移植性与模块化程度,建议将中断源判断封装为独立函数或宏。

使用宏定义简化判断逻辑
#define IS_DMA_TRANSFER_COMPLETE(ch)  (DMA1->ISR & (DMA_ISR_TCIF3))
#define IS_DMA_HALF_TRANSFER(ch)      (DMA1->ISR & (DMA_ISR_HTIF3))
#define IS_DMA_ERROR(ch)              (DMA1->ISR & (DMA_ISR_TEIF3))

void DMA1_Channel3_IRQHandler(void) {
    if (IS_DMA_ERROR(3)) {
        DMA1->IFCR |= DMA_IFCR_CTEIF3;
        Handle_DMA_Error();
    }

    if (IS_DMA_HALF_TRANSFER(3)) {
        DMA1->IFCR |= DMA_IFCR_CHTIF3;
        Handle_DMA_HalfComplete();
    }

    if (IS_DMA_TRANSFER_COMPLETE(3)) {
        DMA1->IFCR |= DMA_IFCR_CTCIF3;
        Handle_DMA_Complete();
    }
}

此方法不仅提升代码可读性,也方便后期迁移到其他通道或其他型号MCU。

6.3 回调机制封装与应用层解耦

传统做法是在ISR中直接处理数据,如解析协议、点亮LED等,但这会导致中断上下文过重,影响系统实时性。更优方案是引入 回调机制 ,实现硬件驱动与应用逻辑的彻底分离。

6.3.1 定义用户回调函数指针(如void (*DMA_RxCpltCallback)(void))

在驱动层定义函数指针变量,允许用户注册自定义行为。

自定义DMA管理结构体
typedef struct {
    DMA_Channel_TypeDef *channel;
    uint8_t *buffer;
    uint32_t length;
    void (*RxCompleteCallback)(uint8_t*, uint32_t);  // 新增参数:缓冲区与长度
    void (*ErrorCallback)(void);
} DMA_UartHandle;
注册回调函数接口
void DMA_RegisterCallbacks(DMA_UartHandle *hdma,
                           void (*rx_cplt_cb)(uint8_t*, uint32_t),
                           void (*err_cb)(void)) {
    if (rx_cplt_cb) hdma->RxCompleteCallback = rx_cplt_cb;
    if (err_cb)    hdma->ErrorCallback = err_cb;
}
ISR中调用注册的回调
if (DMA1->ISR & DMA_ISR_TCIF3) {
    DMA1->IFCR |= DMA_IFCR_CTCIF3;
    if (hdma_uart.RxCompleteCallback != NULL) {
        hdma_uart.RxCompleteCallback(hdma_uart.buffer, hdma_uart.length);
    }
}

这样,主程序可以自由决定收到数据后的行为:

void On_UART_Receive_Complete(uint8_t *buf, uint32_t len) {
    // 解析Modbus帧
    Parse_Modbus_Frame(buf, len);
    // 反馈接收成功
    LED_Toggle();
}
优势分析:
  • 实现 非阻塞式编程模型
  • 提升代码复用率
  • 支持多任务环境下的事件驱动架构

6.3.2 实现非阻塞式数据通知机制

基于回调机制,可进一步构建完整的异步通信框架。

状态机辅助设计
typedef enum {
    IDLE,
    RX_IN_PROGRESS,
    RX_COMPLETE,
    ERROR_STATE
} RxState;

RxState uart_state = IDLE;

在回调中更新状态,并唤醒主循环处理:

void UART_RxComplete_Callback(uint8_t *buf, uint32_t len) {
    received_length = len;
    uart_state = RX_COMPLETE;
}

主循环中轮询状态:

while (1) {
    if (uart_state == RX_COMPLETE) {
        ProcessReceivedData(rx_buffer, received_length);
        Restart_DMA_Receive();  // 重启接收
        uart_state = IDLE;
    }
}

这种模式下,中断仅负责“通知”,主流程负责“决策”,形成典型的生产者-消费者模型。

6.4 数据一致性与二次接收启动

DMA接收完成后,若不及时重启,将导致后续数据丢失。因此,“何时重启”、“如何保证缓冲区安全”成为关键问题。

6.4.1 如何安全地重启下一轮DMA接收

重启DMA接收涉及三个步骤:

  1. 禁用DMA通道(CCR.EN = 0)
  2. 重置计数器(CNDTRx)
  3. 重新启用通道(CCR.EN = 1)
示例函数:重启USART1 DMA接收
void Restart_USART1_DMA_Receive(uint8_t *buffer, uint32_t size) {
    // 1. 停止当前传输
    DMA1_Channel3->CCR &= ~DMA_CCR_EN;
    // 等待真正关闭(可加超时)
    while (DMA1_Channel3->CCR & DMA_CCR_EN);

    // 2. 重置传输计数
    DMA1_Channel3->CNDTR = size;

    // 3. 设置目标内存地址(通常不变)
    DMA1_Channel3->CMAR = (uint32_t)buffer;

    // 4. 重新使能DMA
    DMA1_Channel3->CCR |= DMA_CCR_EN;
}
关键点说明:
  • 必须等待 EN 位真正清零后再操作,防止寄存器竞争。
  • 若使用循环缓冲区, CMAR 可固定;若为单次接收,则每次指向新缓冲区。
  • 不需要重新配置CPAR(外设地址固定为USART1_DR)。

6.4.2 使用双缓冲机制提升连续接收效率

对于高速、不间断的数据流(如音频、传感器流),普通单缓冲DMA存在“传输间隙”风险。此时应采用 双缓冲模式(Double Buffer Mode)

启用双缓冲的配置步骤
  1. DMA_CCRx 中设置 DBM 位(Double Buffer Mode)
  2. 分别配置 CMAR1 CMAR2 (通过 DMA_CMARx 和特定偏移)
  3. 每次传输完成后自动切换缓冲区
STM32寄存器布局(以DMA1 Channel3为例)
寄存器 地址偏移 功能
DMA_CPAR3 +0x0C 外设地址
DMA_CMAR3 +0x10 主缓冲区地址(Memory 0)
DMA_CMR3 +0x14 辅缓冲区地址(Memory 1)

注意: CMAR3 CMR3 并非标准命名,实际访问需通过基址+偏移计算。

初始化双缓冲DMA
// 开启双缓冲模式
DMA1_Channel3->CCR |= DMA_CCR_DBM;

// 配置两个缓冲区地址
DMA1_Channel3->CMAR = (uint32_t)primary_buf;   // Memory 0
*(volatile uint32_t*)(&(DMA1_Channel3->CPAR) + 4) = (uint32_t)secondary_buf; // Memory 1

📌 注:第二个缓冲区地址写入位置为 &DMA_CPAR + 4*2 = +0x14

中断中的缓冲区识别

ISR 中可通过 CT 位(Current Target)判断当前使用的缓冲区:

if (DMA1_Channel3->CCR & DMA_CCR_CT) {
    // 正在使用 Memory 1 (secondary_buf)
} else {
    // 正在使用 Memory 0 (primary_buf)
}
应用优势:
  • 实现无缝接收,无数据丢失窗口
  • 减少CPU干预频率
  • 特别适合音频流、视频采集等实时场景

综上所述,DMA中断处理不仅是简单的“打个招呼”,更是整个数据链路闭环中的核心环节。从标志识别到ISR设计,再到回调封装与双缓冲优化,每一层都体现了嵌入式系统对性能与可靠性的极致追求。掌握这些技能,开发者便能在复杂项目中游刃有余地驾驭DMA这一强大工具。

7. STM32串口DMA接收完整实现流程与实战应用

7.1 工程项目结构设计与模块化划分

在构建一个稳定可靠的STM32串口DMA接收系统时,合理的工程项目架构是成功的关键。我们采用模块化设计思想,将功能划分为以下核心组件:

  • main.c :主程序入口,负责系统初始化与任务调度
  • usart1_dma.h/.c :封装USART1与DMA1的初始化及控制接口
  • ring_buffer.h/.c :实现环形缓冲区管理,支持动态数据存取
  • modbus_parser.h/.c :协议解析层,处理接收到的数据帧
  • stm32f4xx_it.c :中断服务例程注册与分发
  • led_feedback.c :外设反馈逻辑(如LED闪烁提示)

该结构确保各层职责清晰,便于后期维护和移植。

7.2 完整初始化流程与代码实现

以下是基于HAL库的完整初始化序列,涵盖从RCC到DMA中断使能的全过程:

// main.c
#include "usart1_dma.h"
#include "ring_buffer.h"
#include "stm32f4xx_hal.h"

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

uint8_t rx_buffer[256];  // 接收缓冲区
RingBuffer_t rb;         // 环形缓冲区实例

int main(void)
{
    HAL_Init();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_DMA1_CLK_ENABLE();

    MX_USART1_UART_Init();           // 初始化USART1
    MX_DMA_Init();                   // 配置DMA1 Channel3
    RingBuffer_Init(&rb, rx_buffer, 256);  // 初始化环形缓冲区

    // 启动DMA接收(单次传输256字节)
    HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);

    while (1)
    {
        // 主循环中非阻塞处理数据
        ProcessReceivedData(&rb);
        HAL_Delay(10);
    }
}

其中, MX_USART1_UART_Init() MX_DMA_Init() 是由CubeMX生成或手动编写的底层配置函数。

7.3 USART与DMA联合配置参数表

参数项 配置值 说明
USART1 波特率 115200 常用高速通信速率
数据位 8 bit 标准配置
停止位 1 bit 异步模式常用
校验位 减少开销
DMA方向 外设→内存 接收方向
缓冲区大小 256 bytes 支持Modbus RTU帧
传输模式 Normal(非循环) 每次重置计数器
中断使能 TCIE + TEIE 完成与错误中断
地址对齐 32-bit aligned 使用__ALIGN_BEGIN宏
优先级 DMA Channel Priority = High 避免丢包

7.4 DMA中断回调处理与数据移交

stm32fxxx_it.c 中注册中断并调用HAL默认ISR:

// stm32f4xx_it.c
void DMA1_Channel3_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_usart1_rx);
}

通过重写回调函数实现应用层解耦:

// usart1_dma.c
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        // 将DMA接收到的256字节全部送入环形缓冲区
        for (int i = 0; i < 256; i++)
        {
            RingBuffer_Put(&rb, rx_buffer[i]);
        }

        // 清空原缓冲区并重启DMA接收
        memset(rx_buffer, 0, 256);
        HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);

        // 触发协议解析任务
        ParseProtocolAsync();
    }
}

7.5 不定长数据帧处理策略与实战案例

针对Modbus RTU这类不定长协议(最小6字节,最长256字节),我们采用“DMA+IDLE中断”组合方案提升效率:

__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        // IDLE中断触发:表示一帧数据结束
        for (uint16_t i = 0; i < Size; i++)
        {
            RingBuffer_Put(&rb, rx_buffer[i]);
        }

        // 解析完整帧
        Modbus_ParseFrame(RingBuffer_GetAll(&rb));

        // 重启DMA
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, 256);
    }
}

此方式避免轮询,可在数据流暂停时立即响应,适用于命令应答类场景。

7.6 实战应用场景:上位机指令响应系统

假设上位机发送如下格式指令:

[CMD][LEN][DATA...][CRC]

例如: 0x81 0x03 0x00 0x01 0x02 0xABCD

在回调中进行分类处理:

void ParseProtocolAsync(void)
{
    uint8_t frame[64];
    int len = RingBuffer_GetFrame(&rb, frame, sizeof(frame), IsFrameValid);

    if (len > 0)
    {
        switch (frame[0])
        {
            case 0x81:
                HandleSensorRead(frame);
                break;
            case 0x82:
                HandleControlCmd(frame);
                break;
            default:
                LogError("Unknown CMD");
                break;
        }
        ToggleLED();  // 反馈执行结果
    }
}

7.7 调试技巧与常见问题排查清单

使用以下方法提升调试效率:

flowchart TD
    A[串口无数据] --> B{DMA是否使能?}
    B -->|否| C[检查HAL_UART_Receive_DMA调用]
    B -->|是| D{USART_CR3.DMAR=1?}
    D -->|否| E[确认先后顺序: 先DMA后USART]
    D -->|是| F{缓冲区地址合法?}
    F -->|否| G[使用__attribute__((aligned(4)))]
    F -->|是| H[使用ST-Link Logic Analyzer抓波形]

常见故障点包括:
1. DMA时钟未开启
2. 缓冲区未对齐导致总线错误
3. 中断向量未正确映射
4. 重复启动DMA前未停止旧通道
5. 协议解析未考虑字节序(CRC高低位)

提供可复用模板仓库链接: https://github.com/embedded-template/stm32-usart-dma
支持STM32F4/F7/H7系列芯片一键移植。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32是基于ARM Cortex-M内核的主流微控制器,广泛应用于嵌入式系统。本文以STM32F103C8T6为例,详细介绍如何利用DMA(直接存储器访问)实现USART1的高效串口数据接收。通过配置DMA通道与USART外设联动,可在不占用CPU资源的情况下完成数据传输,显著提升系统实时性与稳定性。内容涵盖DMA与USART的初始化配置、内存缓冲区设置、中断处理及调试优化,适用于物联网、遥测等需持续接收串行数据的应用场景。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

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

更多推荐