STM32F103 USART1 DMA发送实战:从初始化到硬件协同
DMA(直接内存访问)是一种绕过CPU、由硬件自主完成内存与外设间数据搬运的关键技术,其核心原理在于通过AHB总线控制器接管地址/数据传输,实现零CPU干预的高吞吐通信。在嵌入式实时系统中,DMA显著降低中断负载、保障确定性时序,并提升系统整体能效。典型应用场景包括串口大数据量发送、ADC高速采样、音频流传输等。本文以STM32F103C8T6平台的USART1+DMA单次发送为切入点,深入解析D
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) 函数的执行并非简单寄存器写入,而是一套严谨的状态机流程:
- 参数校验 :检查
hdma->Instance是否为有效DMA通道地址(如DMA1_Channel4),Direction等参数是否在合法枚举范围内; - 寄存器复位 :对
hdma->Instance->CCR执行0x00000000写入,清除所有位(包括可能遗留的ENABLE位); - 位域配置 :根据
hdma->Init各成员值,计算CCR寄存器目标值,并写入; - 数据计数器装载 :将
DataNum写入hdma->Instance->CNDTR寄存器; - 地址装载 :将
PeriphAddr写入hdma->Instance->CPAR,MemAddr写入hdma->Instance->CMAR; - 状态更新 :设置
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 功能验证三步法
- 硬件环回测试 :将USART1的TX引脚(PA9)与RX引脚(PA10)短接,PC端串口助手发送指令,观察是否收到回显。此步验证USART1硬件连接与基础收发功能正常,排除物理层故障;
- DMA触发验证 :使用逻辑分析仪或示波器捕获PA9(TX)引脚波形。按下KEY_UP后,应观测到一串连续的UART帧(起始位+8数据位+停止位),帧间隔均匀。若波形断续或缺失,说明DMA未成功启动或数据缓冲区异常;
- 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发送实验已从工程搭建、原理剖析、代码实现到故障排查形成闭环。其价值不仅在于掌握一个外设配置,更在于建立起对嵌入式系统“软硬协同、时序敏感、资源受限”本质的深刻认知。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)