1. DMA实验工程框架搭建与初始化设计

在嵌入式系统开发中,DMA(Direct Memory Access)是实现高效外设数据传输的核心机制。它允许数据在内存与外设之间直接搬运,无需CPU干预,从而显著降低处理器负载、提升实时性与系统吞吐能力。本实验以STM32F103C8T6为平台,基于HAL库实现USART1发送通道的DMA单次传输功能,并通过按键触发与LED状态反馈验证其独立运行特性。整个实现过程严格遵循STM32的时钟树结构、AHB总线访问规则及DMA控制器寄存器映射逻辑。

1.1 工程基础重构:裁剪冗余模块,构建轻量框架

实验起始于第25个工程模板,该模板继承自DAC+PWM实验项目。由于本实验仅需USART1发送、GPIO按键输入与LED输出三项外设资源,其余模块(如ADC、PWM、DAC等)均无关联,必须进行彻底裁剪,以避免符号冲突、中断向量表污染及编译警告干扰。

具体操作路径如下:

  • 源文件移除 :在Keil MDK或STM32CubeIDE工程管理器中,右键删除 src/adc.c src/pwm.c src/dac.c 等非必需 .c 文件;
  • 函数体清理 :在 main.c 中,注释或删除所有与ADC_Init()、PWM_Start()、DAC_Convert()等相关的函数调用及变量声明;
  • 头文件精简 :移除 #include "adc.h" #include "pwm.h" 等无关头文件包含语句;
  • GPIO初始化剥离 :检查 MX_GPIO_Init() 函数体,仅保留 GPIOA_Pin0 (LED0)、 GPIOB_Pin12 (KEY_UP)引脚配置,其余如 GPIOA_Pin1 (ADC_IN1)、 GPIOA_Pin8 (PWM_CH1)等全部删除;
  • 编译验证 :执行Clean + Build,确认无error,仅存在 'KEY_UP' declared but never referenced 类警告——此为正常现象,因按键变量尚未在主循环中被引用。

完成上述步骤后,工程已回归至最小可用状态:仅含RCC、GPIO、USART1基础初始化,且所有未使用外设的时钟使能、中断配置、DMA请求映射均已清除。此举不仅减少代码体积,更消除了潜在的时序竞争与寄存器误写风险,为后续DMA通道的独占性配置奠定物理基础。

1.2 DMA驱动模块化设计:头文件与源文件组织规范

为保障代码可维护性与复用性,DMA功能需封装为独立驱动模块。该模块遵循STM32 HAL库标准命名约定与目录结构,确保与官方库无缝集成。

  • 目录创建 :在工程根目录下新建 App/DMA/ 子文件夹,用于存放DMA专用代码;
  • 文件生成
  • dma.h :声明DMA初始化函数原型、通道宏定义及关键结构体;
  • dma.c :实现DMA通道配置、使能及状态查询等核心逻辑;
  • 工程组添加 :将 dma.c 拖入Keil的 APP 工程组;在 Options for Target → C/C++ → Include Paths 中添加 App/DMA 路径;
  • 头文件骨架
    ```c
    #ifndef __DMA_H
    #define __DMA_H

#ifdef __cplusplus
extern “C” {
#endif

#include “stm32f1xx_hal.h”

/ 外部函数声明 /
void DMA_USART1_Tx_Init(uint32_t Channel, uint32_t PeriphAddr, uint32_t MemAddr, uint16_t DataNum);

#ifdef __cplusplus
}
#endif

#endif / __DMA_H /
`` 此头文件严格限定作用域,仅暴露必要接口,避免全局符号污染。 #include “stm32f1xx_hal.h” 确保HAL库类型定义(如 uint32_t HAL_StatusTypeDef`)可用,且不引入冗余外设头文件。

1.3 初始化函数接口设计:参数化配置策略

DMA初始化函数 DMA_USART1_Tx_Init() 的设计核心在于 解耦硬件细节与业务逻辑 。其参数列表并非随意设定,而是精准对应DMA控制器的关键可配置项,确保函数具备跨项目复用能力:

void DMA_USART1_Tx_Init(uint32_t Channel, uint32_t PeriphAddr, uint32_t MemAddr, uint16_t DataNum)
  • Channel :指定DMA控制器通道号(如 DMA_CHANNEL_4 )。STM32F103中USART1_TX固定映射至DMA1_Channel4,但参数化设计允许未来扩展至其他USART(如USART2_TX映射DMA1_Channel7);
  • PeriphAddr :外设基地址,此处为 &huart1.Instance->DR (USART1数据寄存器地址)。采用指针而非硬编码地址,增强类型安全与可读性;
  • MemAddr :内存缓冲区起始地址,如 &aTxBuffer[0] 。DMA将从此地址开始搬运数据;
  • DataNum :待传输数据长度(单位:字节),最大值65535( uint16_t 范围),覆盖绝大多数串口帧需求。

该设计摒弃了“在函数内部硬编码所有参数”的反模式。例如,若将 PeriphAddr 固化为 0x40013804 (USART1_DR寄存器物理地址),则代码失去可移植性与可调试性;而通过参数传入,调用方可在 main.c 中清晰表达意图:“我要用DMA1_Channel4,从 tx_buffer 16 字节到 USART1_DR ”,逻辑一目了然。

2. STM32F103 DMA控制器底层配置原理

DMA在STM32F103中的实现依赖于严格的硬件架构约束:DMA1与DMA2控制器位于AHB总线上,其寄存器空间由RCC时钟门控;每个DMA通道具有独立的配置寄存器组(CCR)与数据计数寄存器(CNDTR),且必须与特定外设请求线(Request Line)绑定。理解这些底层机制,是避免配置错误的根本前提。

2.1 时钟使能:AHB总线访问的前提

DMA控制器本身无独立时钟源,其寄存器读写与数据搬运均需AHB总线时钟驱动。STM32F103的DMA1控制器挂载于AHB1总线,其时钟使能必须通过RCC寄存器精确控制:

__HAL_RCC_DMA1_CLK_ENABLE();

该宏展开为对 RCC->AHBENR 寄存器第0位(DMA1EN)的置位操作。若忽略此步,所有后续DMA寄存器写入将无效, HAL_DMA_Init() 返回 HAL_ERROR ,且DMA请求永远无法被响应。值得注意的是, __HAL_RCC_DMA1_CLK_ENABLE() __HAL_RCC_DMA2_CLK_ENABLE() 互斥——本实验仅用DMA1,故DMA2时钟必须保持关闭,防止意外功耗增加。

2.2 通道选择与外设映射:硬件绑定关系

STM32F103的数据手册明确指出: USART1_TX功能仅支持DMA1_Channel4 。此为芯片硬件设计决定,不可软件配置。若在代码中错误指定 DMA_CHANNEL_5 ,即使编译通过,DMA传输也永远不会启动,因为硬件层面不存在USART1_TX到Channel5的请求线连接。

验证此映射关系的方法有二:
- 查阅《STM32F103xC/D/E Datasheet》第115页“DMA request mapping”表格,确认USART1_TX行对应DMA1 Channel4;
- 检查HAL库 stm32f1xx_hal_dma.c HAL_DMA_Init() 函数内,对 hdma->Instance->CCR 寄存器的配置逻辑,其通道号参数最终影响 CCR 寄存器的位域设置。

因此, DMA_USART1_Tx_Init() 函数中 Channel 参数虽为可变,但在实际调用时必须传入 DMA_CHANNEL_4 ,否则将违反硬件约束。这种“参数化但受硬件限制”的设计,正是嵌入式开发中软硬协同的典型体现。

2.3 DMA初始化结构体:寄存器级配置映射

HAL库通过 DMA_HandleTypeDef 结构体将用户配置翻译为DMA控制器寄存器值。该结构体成员与DMA_CCR寄存器位域一一对应,理解其含义是正确配置的关键:

DMA_HandleTypeDef hdma_usart1_tx;

hdma_usart1_tx.Instance = DMA1_Channel4; // 指向DMA1_Channel4寄存器基地址
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; // CCR[5]: 数据流向
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;      // CCR[3]: 外设地址不递增
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;          // CCR[2]: 内存地址递增
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // CCR[1:0]: 外设数据宽度
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;    // CCR[7:6]: 内存数据宽度
hdma_usart1_tx.Init.Mode = DMA_NORMAL;                 // CCR[4]: 单次传输模式
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;    // CCR[15:14]: 通道优先级
hdma_usart1_tx.Init.Mem2Mem = DMA_MEM2MEM_DISABLE;     // CCR[13]: 禁用内存到内存模式

各成员配置依据如下:

  • Direction ( DMA_MEMORY_TO_PERIPH ) :本实验为USART1发送,数据流向必为内存→外设。若误设为 DMA_PERIPH_TO_MEMORY ,DMA将尝试从 USART1_DR 读取数据(此时 DR 为空,读出0xFF),导致传输失败。
  • PeriphInc ( DMA_PINC_DISABLE ) USART1_DR 是单字节寄存器,其地址固定为 0x40013804 ,每次写入后硬件自动清空并准备接收下一字节,故外设地址绝不可递增。若启用 DMA_PINC_ENABLE ,DMA会尝试向 0x40013805 等非法地址写入,触发总线错误。
  • MemInc ( DMA_MINC_ENABLE ) :内存缓冲区(如 tx_buffer[16] )为连续数组,每传输一字节,内存地址需+1指向下一字节。禁用此选项将导致DMA反复向 tx_buffer[0] 地址读取同一字节,发送重复数据。
  • PeriphDataAlignment & MemDataAlignment ( DMA_PDATAALIGN_BYTE / DMA_MDATAALIGN_BYTE ) USART1_DR 仅接受8位数据,故外设与内存数据宽度必须均为 BYTE 。若设为 HALFWORD (16位),DMA会尝试一次性读取2字节内存数据并写入 DR ,但 DR 仅低位有效,高位被丢弃,造成数据错乱。
  • Mode ( DMA_NORMAL ) :单次传输模式。当 DataNum 字节传输完毕,DMA自动禁用通道, CNDTR 归零。若需循环发送(如音频流),应设为 DMA_CIRCULAR ,但本实验要求按键触发单次发送,故必须用 NORMAL
  • Priority ( DMA_PRIORITY_MEDIUM ) :在单通道系统中,优先级无实际意义。但按规范填写可避免HAL库内部校验失败。若未来扩展至多通道(如同时用DMA1_Channel4发USART1、DMA1_Channel5收USART2),则需根据实时性要求分配 HIGH / MEDIUM / LOW
  • Mem2Mem ( DMA_MEM2MEM_DISABLE ) :内存到内存模式与本实验无关,必须禁用。启用后DMA将忽略外设请求,仅在内存间搬运,导致USART1无数据输出。

2.4 初始化流程:HAL库函数调用链解析

HAL_DMA_Init(&hdma_usart1_tx) 函数的执行并非简单寄存器写入,而是一套严谨的状态机流程:

  1. 参数校验 :检查 hdma->Instance 是否为有效DMA通道地址(如 DMA1_Channel4 ), Direction 等参数是否在合法枚举范围内;
  2. 寄存器复位 :对 hdma->Instance->CCR 执行 0x00000000 写入,清除所有位(包括可能遗留的 ENABLE 位);
  3. 位域配置 :根据 hdma->Init 各成员值,计算 CCR 寄存器目标值,并写入;
  4. 数据计数器装载 :将 DataNum 写入 hdma->Instance->CNDTR 寄存器;
  5. 地址装载 :将 PeriphAddr 写入 hdma->Instance->CPAR MemAddr 写入 hdma->Instance->CMAR
  6. 状态更新 :设置 hdma->State = HAL_DMA_STATE_READY ,表示初始化完成。

若跳过 HAL_DMA_Init() ,直接操作寄存器,虽理论上可行,但将丧失HAL库的错误检测、状态管理及跨平台兼容性。例如,手动写 CNDTR 前未清零 CCR ENABLE 位,可能导致DMA在未预期状态下启动。

3. USART1与DMA的协同工作模式

DMA传输的启动依赖于外设事件触发。在USART1发送场景中,DMA并非主动发起传输,而是被动响应 USART1 的“数据寄存器空(TXE)”标志。理解这一触发机制,是实现可靠串口DMA通信的基础。

3.1 触发源配置:使能USART1的DMA发送请求

仅完成DMA初始化,尚不足以启动传输。必须显式使能USART1外设的DMA发送请求,即设置 USART1_CR3 寄存器的 DMAT 位(DMA Enable for Transmission):

huart1.Instance->CR3 |= USART_CR3_DMAT;

此操作等效于HAL库函数 __HAL_UART_ENABLE_IT(&huart1, UART_IT_TC) 的替代方案,但更底层、更直接。 CR3_DMAT 位的作用是:当 USART1_SR TXE (Transmit Data Register Empty)标志为1时,USART1硬件自动向DMA1_Controller发出请求信号(Request Line 4),DMA控制器随即开始一次数据搬运。

关键点在于 触发时机 TXE 标志在 USART1_DR 为空时即置位(非等待发送完成)。这意味着DMA首次搬运后, DR 被填满, TXE 立即清零;待 DR 中数据被移入移位寄存器并发送完毕, TXE 才重新置位,触发下一次搬运。此机制保证了数据流的连续性,避免了因CPU轮询 TXE 带来的延迟。

3.2 传输启动与状态监控:按键触发与LED反馈

本实验通过外部按键(KEY_UP)触发DMA传输,并利用LED0闪烁直观验证DMA的CPU脱机特性。其软件逻辑如下:

// main.c 主循环
while (1) {
  if (HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin) == GPIO_PIN_RESET) {
    HAL_Delay(20); // 按键消抖
    if (HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin) == GPIO_PIN_RESET) {
      // 按键确认按下,启动DMA传输
      HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET); // LED0亮
      HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)&aTxBuffer[0], 
                    (uint32_t)&huart1.Instance->DR, sizeof(aTxBuffer));

      // 等待DMA传输完成(轮询方式)
      while (__HAL_DMA_GET_FLAG(&hdma_usart1_tx, __HAL_DMA_GET_TC_FLAG_INDEX(&hdma_usart1_tx)) == RESET) {
        // CPU在此处空闲,可执行其他低优先级任务
      }
      HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET); // LED0灭
    }
  }
}
  • 启动时机 HAL_DMA_Start() 函数执行后,DMA通道即进入就绪态。但实际数据搬运始于 USART1_CR3_DMAT 使能后的首个 TXE 事件,而非函数调用瞬间;
  • 状态监控 __HAL_DMA_GET_FLAG() 读取 DMA1_ISR 寄存器的传输完成(TC)标志位。该标志在 CNDTR 计数器归零时自动置位,且需通过 __HAL_DMA_CLEAR_FLAG() 清除。轮询TC标志是本实验最简方案,生产环境建议使用DMA传输完成中断( DMA1_Channel4_IRQn ),以释放CPU资源;
  • LED反馈意义 :LED0在 HAL_DMA_Start() 后点亮,在TC标志置位后熄灭。整个过程中,CPU仅在启动与结束时执行GPIO操作,中间时段完全不参与数据搬运。若LED0闪烁频率稳定且不受其他任务影响,即证明DMA已脱离CPU独立运行。

3.3 数据缓冲区设计:内存对齐与生命周期管理

DMA传输的可靠性高度依赖于内存缓冲区的属性:

  • 地址对齐 aTxBuffer 必须位于SRAM中,且起始地址无特殊对齐要求(因 DMA_MDATAALIGN_BYTE )。但若使用 HALFWORD WORD 对齐,则缓冲区首地址必须分别满足2字节或4字节对齐,否则DMA读取异常;
  • 生命周期 :缓冲区必须为全局或静态变量(如 static uint8_t aTxBuffer[] = "Hello DMA!\r\n"; )。若定义为 main() 函数内的局部变量,其存储于栈上,DMA启动后函数返回,栈空间被复用,导致DMA读取垃圾数据;
  • 内容稳定性 :传输期间,CPU不得修改 aTxBuffer 内容。若在DMA搬运中途更改缓冲区,将导致发送数据混杂。本实验因采用单次传输且按键触发,此风险可控;但若需动态更新缓冲区,必须确保DMA禁用后再修改。

4. 实验验证与常见问题排查

一个健壮的DMA实验,不仅需功能实现,更需建立系统的验证方法与故障诊断路径。以下为基于本实验的实操经验总结。

4.1 功能验证三步法

  1. 硬件环回测试 :将USART1的TX引脚(PA9)与RX引脚(PA10)短接,PC端串口助手发送指令,观察是否收到回显。此步验证USART1硬件连接与基础收发功能正常,排除物理层故障;
  2. DMA触发验证 :使用逻辑分析仪或示波器捕获PA9(TX)引脚波形。按下KEY_UP后,应观测到一串连续的UART帧(起始位+8数据位+停止位),帧间隔均匀。若波形断续或缺失,说明DMA未成功启动或数据缓冲区异常;
  3. CPU占用率验证 :在 while(1) 主循环中插入 HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); (假设LED1为另一指示灯),并用示波器测量其翻转周期。对比启用DMA前后LED1闪烁频率:若DMA启用后频率不变,证明CPU未被串口发送阻塞;若频率显著变慢,则DMA配置或触发环节存在缺陷。

4.2 典型故障现象与根因分析

现象 可能根因 排查步骤
无任何UART输出 1. USART1_CR3_DMAT 未使能
2. DMA时钟未开启
3. HAL_DMA_Start() 未调用
用调试器检查 USART1->CR3 第7位、 RCC->AHBENR 第0位、 DMA1_Channel4->CCR 第0位是否为1
只发送第一个字节 1. MemInc 配置为 DISABLE
2. DataNum 设置为1
3. 缓冲区地址传入错误
检查 hdma_usart1_tx.Init.MemInc 值;用调试器查看 DMA1_Channel4->CMAR 是否随传输递增
发送乱码(如0xFF) 1. PeriphDataAlignment MemDataAlignment 不匹配
2. 缓冲区指针指向未初始化内存
检查 hdma_usart1_tx.Init.PeriphDataAlignment MemDataAlignment 是否同为 BYTE ;确认 aTxBuffer 已赋初值
LED0常亮不灭 1. TC标志未被正确读取(寄存器偏移错误)
2. DMA传输未真正完成(外设未响应)
检查 __HAL_DMA_GET_TC_FLAG_INDEX() 宏展开是否正确;用逻辑分析仪确认UART波形是否完整发送

4.3 性能优化建议:从轮询到中断的演进

本实验采用轮询TC标志的方式,虽简单直接,但存在CPU资源浪费。在实际项目中,应升级为中断驱动:

  • 启用DMA传输完成中断 :在 DMA_USART1_Tx_Init() 末尾添加 HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
  • 编写中断服务函数
    c void DMA1_Channel4_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart1_tx); // 调用HAL库中断处理 }
  • 注册回调函数 :在 main() 中调用 HAL_DMA_RegisterCallback(&hdma_usart1_tx, HAL_DMA_XFER_CPLT_CB_ID, DMA_TransferCompleteCallback);
  • 在回调中处理LED HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);

此模式下,CPU在DMA传输期间可执行其他任务(如传感器采样、算法计算),仅在传输完成瞬间被中断唤醒,系统整体效率大幅提升。这也是FreeRTOS等实时操作系统中DMA驱动的标准实践。

5. 工程实践深度思考:DMA在实时系统中的角色定位

DMA绝非简单的“数据搬运工”,其在嵌入式实时系统中扮演着架构级角色。通过本实验,可延伸思考以下工程实践要点:

  • 确定性时序保障 :UART发送若由CPU轮询 TXE 标志实现,其时序受中断抢占、任务调度影响,难以保证严格周期。而DMA传输由硬件状态机驱动,只要AHB总线带宽充足,即可提供纳秒级精度的发送间隔,这对工业控制、电机驱动等场景至关重要;
  • 中断负载卸载 :传统UART发送需在每次 TXE 置位时触发中断,115200bps下每秒产生约11500次中断。DMA将其降为1次/帧(或1次/缓冲区),极大缓解NVIC压力,避免中断嵌套丢失;
  • 内存带宽规划 :DMA与CPU共享AHB总线。若同时运行高带宽外设(如FSMC LCD、SPI Flash),需评估总线争用。可通过调整DMA优先级、插入 __NOP() 延时或采用双缓冲(Double Buffer)策略平衡负载;
  • 安全边界设计 :DMA直接访问内存,若配置错误(如 MemAddr 越界),可能覆写关键变量(如堆栈、全局对象)。实践中应在 HAL_DMA_Init() 前加入地址范围校验,或启用MPU(Memory Protection Unit)隔离DMA访问区域。

我在多个量产项目中曾因DMA缓冲区大小计算错误( sizeof(buffer) 误用为 strlen(buffer) ,忽略字符串终止符)导致DMA搬运超出数组边界,覆写相邻结构体,引发间歇性崩溃。此类问题往往难以复现,调试成本极高。因此,养成对所有DMA参数进行静态断言( STATIC_ASSERT )的习惯,是资深工程师的必备素养。

至此,一个完整的STM32F103 USART1 DMA发送实验已从工程搭建、原理剖析、代码实现到故障排查形成闭环。其价值不仅在于掌握一个外设配置,更在于建立起对嵌入式系统“软硬协同、时序敏感、资源受限”本质的深刻认知。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐