STM32F1 DMA原理与HAL库实战:从寄存器到UART+DMA发送
DMA(直接存储器访问)是嵌入式系统实现高效数据搬运的核心硬件机制,其本质是通过专用控制器解耦CPU与I/O传输任务,从而释放CPU资源、提升实时性。其工作原理基于源地址、目标地址与传输数目三大要素,并严格依赖方向性配置(外设到内存/内存到外设/内存到内存)。在STM32F1中,DMA作为AHB主设备运行,不占用CPU总线带宽,为ADC采样、UART批量通信、SPI高速读写等场景提供零干预数据流支
1. DMA技术原理与硬件架构解析
1.1 DMA的本质:硬件级数据搬运引擎
DMA(Direct Memory Access,直接存储器访问)并非一种“高级传输协议”,而是嵌入式系统中一项基础而关键的硬件机制。其核心价值在于 解除CPU对数据搬移操作的绑定 。在传统CPU主导的数据传输中,每一次字节读取、地址递增、写入外设寄存器的操作,都需CPU执行一条或多条指令。当面对ADC持续采样、SPI高速Flash读写或UART批量串口通信等场景时,这种“指令驱动”的方式会迅速耗尽CPU周期,使其无法响应其他实时任务。
DMA的解决方案是引入一个独立于CPU的专用硬件控制器——DMA控制器。它本质上是一个可编程的状态机,其唯一职责就是:在预设的源地址与目标地址之间,依据配置好的参数,自动完成指定数量的数据单元复制。整个过程完全由硬件逻辑完成,CPU仅需在传输开始前进行一次性的参数配置,并在传输完成后接收一个通知(中断或轮询状态),中间阶段CPU可自由执行其他计算、调度或休眠任务。这并非简单的“效率提升”,而是系统架构层面的解耦,是构建高实时性、多任务嵌入式应用的基石。
1.2 STM32F1系列DMA控制器拓扑结构
理解STM32F1的DMA,必须从其在系统总线架构中的物理位置入手。F1系列拥有两个独立的DMA控制器:DMA1(7个通道)和DMA2(5个通道,仅存在于大容量及互联型产品,如STM32F103ZET6,而入门级的C8T6则无DMA2)。它们并非挂载在APB1或APB2外设总线上,而是作为AHB总线上的一个独立主设备存在。
这一设计至关重要。当DMA控制器需要访问外设(如USART1的DR寄存器)或SRAM时,其数据路径为:DMA控制器 → AHB总线 → 总线矩阵(Bus Matrix)→ AHB-APB桥(Bridge)→ APBx总线 → 目标外设。这条路径与CPU的典型数据路径(CPU → AHB → 总线矩阵 → …)是并行且独立的。这意味着,DMA进行数据传输时, 完全不占用CPU的总线带宽与指令执行周期 。CPU可以同时在SRAM中进行复杂算法运算,而DMA正将ADC采集的数据流式地写入另一块缓冲区,二者互不干扰。这种物理隔离是DMA实现“零CPU干预”传输的根本保障。
1.3 DMA传输的核心三要素与方向性
一次DMA传输的启动,依赖于三个不可分割的要素,它们共同构成了DMA工作的“契约”:
-
源地址(Source Address) :数据从何处读取。它可以是:
- 内存地址 :如
&buffer[0],指向SRAM中的一块数组。 - 外设地址 :如
&(USART1->DR),指向某个外设的数据寄存器(Data Register)。这是外设产生或消费数据的端口。
- 内存地址 :如
-
目标地址(Destination Address) :数据将被写入何处。同样可以是内存地址或外设地址。
-
传输数目(Number of Data Items) :本次传输需要搬运多少个数据单元。这是一个16位寄存器(CNDTRx),最大值为65535。
这三个要素在传输开始前必须被精确配置到DMA控制器的对应寄存器中。尤为关键的是 传输方向(Transfer Direction) ,它由 DMA_CCRx 寄存器的 DIR 位(第14位)定义,且具有严格的单向性:
- 外设到内存(Peripheral to Memory) :例如,ADC规则通道转换完成,将结果自动存入SRAM缓冲区。此时,源地址为
&ADC1->DR,目标地址为&adc_buffer[0]。 - 内存到外设(Memory to Peripheral) :例如,UART发送数据。此时,源地址为
&uart_tx_buffer[0],目标地址为&(USART1->DR)。 - 内存到内存(Memory to Memory) :一种特殊的、仅由软件触发的模式,用于高效地在SRAM的不同区域间拷贝数据。此模式下,源和目标均为内存地址。
方向是绝对固定的,不可逆向。 尝试配置一个“内存到外设”的DMA通道去执行“外设到内存”的操作,只会导致传输失败或数据错乱。这一约束源于DMA通道的硬件连线设计,每个通道的物理信号线(如 DMA_CHx_PERIPH_DATA 和 DMA_CHx_MEM_DATA )在芯片内部已被固化连接至特定的外设或总线段。
2. STM32F1 DMA寄存器深度剖析
2.1 核心控制寄存器(DMA_CCRx)
DMA_CCRx (x为通道号,1-7对应DMA1,1-5对应DMA2)是DMA通道的“大脑”,其每一位都精确控制着传输行为。理解其配置逻辑,是掌握DMA底层的关键。
| 位 (Bit) | 名称 | 功能说明 | 工程配置要点 |
|---|---|---|---|
| 14:13 | DIR |
数据传输方向 | 00 : 外设到内存; 01 : 内存到外设; 10 : 内存到内存。对于UART发送,必须为 01 。 |
| 12:11 | CM |
循环模式(Circular Mode) | 00 : 正常模式(Normal)。传输完成后, CNDTRx 计数器归零,通道自动关闭。 10 : 循环模式。计数器归零后,自动重载初始值,通道持续工作。ADC连续扫描常用此模式。 |
| 10:9 | PL |
通道优先级(Priority Level) | 00 : 低; 01 : 中; 10 : 高; 11 : 极高。当多个通道同时请求时,此软件优先级与通道号共同决定仲裁结果。 |
| 8 | MSIZE |
存储器数据宽度(Memory Data Size) | 0 : 8位(Byte); 1 : 16位(Half-word)。需与源/目标数据类型严格匹配。 |
| 7 | PSIZE |
外设数据宽度(Peripheral Data Size) | 0 : 8位; 1 : 16位。必须与外设寄存器宽度一致。USART_DR为8位,故此处必为 0 。 |
| 6 | MINC |
存储器地址增量模式(Memory Increment Mode) | 0 : 禁用(地址不变); 1 : 启用(每次传输后地址+ MSIZE )。对于数组缓冲区,必须启用。 |
| 5 | PINC |
外设地址增量模式(Peripheral Increment Mode) | 0 : 禁用(地址不变); 1 : 启用。 绝大多数外设DR寄存器是固定地址,必须禁用! |
| 4 | CIRC |
循环模式使能(Circular Mode Enable) | 与 CM 位协同。 CM=10 时,此位决定循环是否生效。 |
| 3 | DIR |
传输方向(同14:13) | 历史兼容位,实际使用高位。 |
| 2 | TEIE |
传输错误中断使能(Transfer Error Interrupt Enable) | 出现总线错误(如非法地址)时触发。调试阶段建议开启。 |
| 1 | HTIE |
半传输中断使能(Half Transfer Interrupt Enable) | 当传输完成一半数据时触发。可用于双缓冲区切换。 |
| 0 | TCIE |
传输完成中断使能(Transfer Complete Interrupt Enable) | 计数器归零时触发。最常用的中断源。 |
关键配置逻辑解释:
* PINC=0 的必然性 :以USART1为例,其数据寄存器地址 &USART1->DR 是恒定不变的物理地址。DMA每次向该地址写入一个字节,硬件UART模块便会将其加入发送移位寄存器。若错误地设置 PINC=1 ,DMA会在第一次写入 &USART1->DR 后,尝试向 &USART1->DR + 1 (即下一个字节地址)写入,这将访问到一个完全无关的寄存器(可能是 USART1->BRR ),导致不可预测的错误。
* MINC=1 的实用性 :当源缓冲区是一个 uint8_t tx_buffer[100] 数组时,我们期望DMA依次将 tx_buffer[0] , tx_buffer[1] , …, tx_buffer[99] 发送出去。启用 MINC 后,DMA在每次成功写入 USART1->DR 后,会自动将源地址指针递增1(因为 MSIZE=0 ,8位),完美匹配我们的需求。若禁用 MINC ,所有100个字节都会被写入到 tx_buffer[0] 这个单一地址,毫无意义。
2.2 状态与控制寄存器组
除了 CCR ,DMA还有一组紧密协作的寄存器,共同构成完整的传输控制闭环。
-
DMA_CNDTRx(通道x传输数目寄存器) :这是一个16位的向下计数器。初始化时写入待传输的数据项总数。DMA每完成一次数据搬运,此寄存器值减1。当其值减至0时,标志着一次传输的完成,并根据CCRx的配置产生相应的中断标志。 它是轮询传输状态的核心 。在非中断模式下,应用程序通过不断读取此寄存器的值,判断其是否为0,来获知传输是否结束。 -
DMA_CPARx(通道x外设地址寄存器) :存放外设数据寄存器(如&USART1->DR)的32位物理地址。此地址在传输过程中保持不变(因PINC=0)。 -
DMA_CMARx(通道x存储器地址寄存器) :存放内存缓冲区(如&tx_buffer[0])的32位物理地址。在MINC=1模式下,DMA硬件会自动更新此寄存器的值。 -
DMA_ISR(中断状态寄存器)与DMA_IFCR(中断标志清除寄存器) :ISR是一个只读寄存器,其各位(如TCIF1表示通道1传输完成)反映了各通道当前的中断状态。IFCR是一个只写寄存器,向其对应位写1,即可清除ISR中相应的标志位。 这是标准的“写1清零”(Write-One-to-Clear)机制 。例如,要清除通道1的传输完成标志,需向DMA_IFCR的CTCIF1位写1。这是中断服务程序(ISR)中必不可少的第一步操作,否则标志位将持续置位,导致中断被反复触发。
3. HAL库DMA驱动接口与工程实践
3.1 HAL库DMA抽象层设计哲学
ST的HAL库并未抛弃对底层寄存器的操控,而是将其封装为一套面向对象的、可移植的API。其核心思想是: 用结构体描述硬件状态,用函数封装初始化与控制逻辑 。开发者不再直接操作 DMA_CCR1 、 DMA_CNDTR1 等寄存器,而是通过填充结构体成员并调用标准函数,由HAL库内部完成寄存器映射与配置。
-
DMA_HandleTypeDef(DMA句柄) :这是HAL库中DMA的“身份证”。它是一个结构体,内部包含了所有与该DMA通道相关的运行时信息,如指向DMA_Channel_TypeDef的指针(标识具体通道)、指向DMA_InitTypeDef的指针(初始化参数)、以及指向相关外设句柄(如UART_HandleTypeDef *huart)的指针。一个DMA通道对应一个唯一的DMA_HandleTypeDef实例。 -
DMA_InitTypeDef(DMA初始化结构体) :这是配置DMA行为的“蓝图”。其成员变量与DMA_CCRx寄存器的各位一一对应,如Direction(对应DIR)、PeriphInc(对应PINC)、MemInc(对应MINC)、PeriphDataAlignment(对应PSIZE)、MemDataAlignment(对应MSIZE)、Mode(对应CM)、Priority(对应PL)等。开发者只需按需填充这些字段,即可完成对DMA通道的完整配置。
3.2 UART+DMA发送的标准化配置流程
基于HAL库,实现UART通过DMA发送数据是一个高度结构化的四步流程,每一步都服务于明确的工程目的。
第一步:使能DMA时钟
__HAL_RCC_DMA1_CLK_ENABLE(); // 对于DMA1通道x
// __HAL_RCC_DMA2_CLK_ENABLE(); // 对于DMA2通道x
- 目的 :为DMA控制器及其寄存器提供时钟源。任何外设在使用前,其对应的RCC时钟门控必须打开,这是硬件复位后的基本要求。
第二步:初始化DMA通道
// 定义DMA句柄
DMA_HandleTypeDef hdma_usart1_tx;
// 配置初始化结构体
hdma_usart1_tx.Instance = DMA1_Channel4; // USART1_TX 固定映射到 DMA1_Channel4
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; // 方向:内存->外设
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不增量
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址增量
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据宽度:字节
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 内存数据宽度:字节
hdma_usart1_tx.Init.Mode = DMA_NORMAL; // 模式:正常(非循环)
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM; // 优先级:中等
// 调用HAL初始化函数
HAL_DMA_Init(&hdma_usart1_tx);
// 关联DMA句柄与UART句柄(关键!)
__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
- 目的与原理 :
HAL_DMA_Init()函数内部会将DMA_InitTypeDef结构体中的所有配置项,逐一映射并写入到DMA1_Channel4的CCR4、CPAR4、CMAR4、CNDTR4等寄存器中。__HAL_LINKDMA()宏则是一个至关重要的软件关联操作,它将huart1结构体中的hdmatx成员(一个DMA_HandleTypeDef *指针)指向我们刚刚初始化好的hdma_usart1_tx实例。这为后续的HAL_UART_Transmit_DMA()函数提供了“去哪里找DMA控制器”的线索。
第三步:启动DMA传输
uint8_t tx_data[] = "Hello, DMA!";
HAL_UART_Transmit_DMA(&huart1, tx_data, sizeof(tx_data)-1);
- 目的与原理 :
HAL_UART_Transmit_DMA()是一个高层封装函数。它内部执行了以下关键操作:- 将
tx_data数组的起始地址写入DMA1_Channel4->CMAR。 - 将
&USART1->DR的地址写入DMA1_Channel4->CPAR。 - 将数据长度
sizeof(tx_data)-1写入DMA1_Channel4->CNDTR。 - 最关键一步 :通过
huart1.hdmatx指针,找到hdma_usart1_tx句柄,并调用HAL_DMA_Start_IT()(或HAL_DMA_Start()),最终向DMA1_Channel4->CCR的EN位写1,正式启动DMA通道。 - 同时,它会配置
USART1->CR3寄存器的TXEIE位(发送寄存器空)为0,并置位DMAT位(DMA发送使能),将UART的发送请求(TXE)与DMA通道4的请求线连接起来。
- 将
第四步:处理传输完成事件
传输完成可通过两种方式感知:
* 轮询方式 :在主循环中调用 HAL_UART_GetState(&huart1) ,检查返回值是否为 HAL_UART_STATE_BUSY_TX (忙于发送)或 HAL_UART_STATE_READY (就绪)。这种方式简单,但占用了CPU资源。
* 中断方式(推荐) :在 stm32f1xx_it.c 中编写 DMA1_Channel4_IRQHandler 函数。 c void DMA1_Channel4_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart1_tx); // HAL库提供的标准处理函数 } HAL_DMA_IRQHandler() 内部会:
1. 读取 DMA_ISR 寄存器,判断是哪个通道的哪个事件(TC, HT, TE)。
2. 执行 DMA_IFCR 的相应位写 1 操作,清除中断标志。
3. 调用用户注册的回调函数 HAL_UART_TxCpltCallback(&huart1) 。开发者在此回调函数中编写业务逻辑,如发送下一批数据、点亮LED、切换状态机等。
3.3 内存到内存(M2M)DMA实战:按键触发的缓冲区拷贝
为了彻底理解DMA的底层机制,脱离外设的束缚,我们实现一个纯粹的内存到内存拷贝实验。此例以按键K0为触发源,按下后,将源缓冲区 src_buf 中的10个字节拷贝到目标缓冲区 dst_buf 。
关键代码与调试洞察:
// 全局缓冲区
uint8_t src_buf[10] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A};
uint8_t dst_buf[10] = {0};
// DMA句柄
DMA_HandleTypeDef hdma_mem2mem;
// M2M初始化
void MX_DMA_MEM2MEM_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_mem2mem.Instance = DMA1_Channel1;
hdma_mem2mem.Init.Direction = DMA_MEMORY_TO_MEMORY;
hdma_mem2mem.Init.PeriphInc = DMA_PINC_ENABLE; // M2M模式下,源和目标都需增量!
hdma_mem2mem.Init.MemInc = DMA_MINC_ENABLE;
hdma_mem2mem.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_mem2mem.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_mem2mem.Init.Mode = DMA_NORMAL;
hdma_mem2mem.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_mem2mem);
}
// 启动M2M传输
void DMA_M2M_Transmit(uint8_t *src, uint8_t *dst, uint16_t size)
{
// 配置源和目标地址
hdma_mem2mem.Instance->CPAR = (uint32_t)src;
hdma_mem2mem.Instance->CMAR = (uint32_t)dst;
hdma_mem2mem.Instance->CNDTR = size;
// 启动通道
hdma_mem2mem.Instance->CCR |= DMA_CCR_EN;
}
调试验证与增量模式的深刻教训:
在Keil MDK的调试界面中,将 src_buf 和 dst_buf 添加到Watch窗口。设置断点于 DMA_M2M_Transmit() 函数末尾。
- 正确配置(
PINC=1,MINC=1) :运行后,观察dst_buf,其值将精确地变为{0x01, 0x02, ..., 0x0A},与src_buf完全一致。这证明DMA成功地、按序地将每个字节从源地址搬运到了目标地址。 - 错误配置(
PINC=0,MINC=1) :将PeriphInc改为DMA_PINC_DISABLE后重新运行。你会发现dst_buf[0]的值是0x0A,而dst_buf[1]到dst_buf[9]的值全部是0x00。这是因为DMA在第一次搬运src_buf[0](0x01)到dst_buf[0]后,由于PINC=0,下一次仍试图从src_buf[0]读取,但MINC=1却将目标地址递增到了dst_buf[1],于是0x02被写入dst_buf[1]…以此类推,最终src_buf[9](0x0A)被写入dst_buf[9]。然而,dst_buf[0]在第一次写入0x01后,再未被覆盖,其值应为0x01,而非0x0A。这个看似矛盾的结果,恰恰暴露了PINC=0在M2M模式下的逻辑谬误——它强制源地址锁定,破坏了数据流的完整性。
这个调试过程生动地诠释了: DMA的每一个配置位都不是孤立的,它们共同编织了一张精确的数据搬运网络。任何一个节点的错误,都会在网络中引发连锁的、可被观测到的故障。
4. DMA通道仲裁与多外设共用策略
4.1 硬件通道映射的刚性约束
STM32F1的DMA通道与外设之间的连接关系是芯片硬件设计时就已固化,无法通过软件更改。这种“硬连线”(Hard-wired)的设计保证了确定性和低延迟,但也要求开发者必须严格遵循参考手册(Reference Manual)中的《DMA Channel Assignment》表格。例如,对于常见的外设:
| 外设 | 功能 | DMA1通道 | DMA2通道 |
|---|---|---|---|
| USART1 | TX | Channel 4 | — |
| USART1 | RX | Channel 5 | — |
| USART2 | TX | Channel 7 | — |
| USART2 | RX | Channel 6 | — |
| SPI1 | TX | Channel 3 | — |
| SPI1 | RX | Channel 2 | — |
| ADC1 | 规则通道 | Channel 1 | — |
| TIM1 | CC1 | Channel 2 | — |
这意味着,如果你的项目同时需要USART1发送和SPI1接收,你必须分别使用DMA1的Channel 4和Channel 2。你无法将USART1的TX请求“重定向”到Channel 2,因为物理上它们之间没有连接线。
4.2 多请求并发下的仲裁机制
当多个外设(如USART1、USART2、TIM3)在同一时刻都向DMA控制器发出传输请求时,DMA控制器内部的仲裁器(Arbiter)必须决定服务的先后顺序。STM32F1采用两级仲裁:
-
软件优先级(Software Priority) :由
DMA_CCRx寄存器的PL[1:0]位(高/中/低/极高)设定。这是第一道筛选。例如,将USART1_TX(Ch4)设为HIGH,而TIM3_CC1(Ch2)设为LOW,则当两者同时请求时,Ch4将被优先服务。 -
硬件优先级(Hardware Priority) :当两个或多个通道被配置为相同的软件优先级时,仲裁器会依据通道的 物理编号 进行裁决。编号越小的通道,硬件优先级越高。例如,Channel 1的硬件优先级高于Channel 2,依此类推。
此外,还有一个全局的优先级规则: DMA1的所有通道,其优先级均高于DMA2的所有通道 。因此,在一个同时使用DMA1和DMA2的系统中,DMA1的任何通道请求,都将优先于DMA2的任何通道请求得到响应。
4.3 实际工程中的冲突规避与资源规划
在复杂的多外设系统中,DMA资源是宝贵的。工程师必须在项目初期就进行周密的资源规划,以避免后期陷入“通道不够用”的困境。
- 评估带宽需求 :首先评估每个外设的数据吞吐量。一个以115200波特率运行的UART,其理论最大发送速率为约11.5KB/s,对DMA带宽要求极低。而一个以10MHz SCLK运行的SPI Flash读取,则可能达到1.25MB/s,对DMA带宽有较高要求。高带宽外设应优先分配高优先级通道。
- 功能降级备选方案 :如果发现DMA通道紧张,可考虑对某些低速、非实时性外设降级为中断或轮询方式。例如,一个用于调试信息输出的UART,完全可以接受轮询发送,从而将DMA通道释放给更关键的ADC或音频接口。
- 利用同一通道的双向能力 :一个DMA通道只能在一个方向上工作,但一个外设(如USART)通常有TX和RX两个独立的DMA请求线。这意味着,虽然USART1_TX固定使用Ch4,但USART1_RX可以使用Ch5,它们是两个独立的、可并行工作的通道。合理规划,可以最大化利用有限的通道资源。
5. DMA常见问题诊断与性能优化技巧
5.1 典型故障现象与根因分析
在实际开发中,DMA问题往往表现为“数据没发出去”、“数据错乱”或“系统卡死”。以下是几个高频问题的诊断路径:
-
现象:传输完全无反应,
CNDTR寄存器值始终为初始值。- 根因1(最常见) :DMA时钟未使能。检查
RCC->AHBENR寄存器的DMA1EN或DMA2EN位是否为1。 - 根因2 :DMA通道未真正启动。检查
DMA_CCRx寄存器的EN位是否被置位。确认HAL_DMA_Start()或直接写CCR |= EN的操作已执行。 - 根因3 :外设的DMA请求未使能。例如,对于USART,检查
USART_CR3寄存器的DMAT(发送)或DMAR(接收)位。
- 根因1(最常见) :DMA时钟未使能。检查
-
现象:传输启动后,
CNDTR立即归零,但目标缓冲区数据全为0或乱码。- 根因1 :源地址或目标地址配置错误。最常见的错误是将
&buffer写成了buffer(缺少取地址符),导致DMA从栈上一个随机地址读取数据。务必使用&buffer[0]或buffer(数组名本身即为首地址)。 - 根因2 :
PINC/MINC配置错误。如前所述,PINC=1用于USART会导致灾难性后果。仔细核对CCR寄存器的PINC和MINC位。
- 根因1 :源地址或目标地址配置错误。最常见的错误是将
-
现象:传输完成后,中断未触发,或触发后立即再次进入。
- 根因1 :NVIC中断未使能或优先级配置不当。检查
NVIC->ISER和NVIC->IP寄存器。 - 根因2 :中断标志未清除。在
DMA1_Channelx_IRQHandler中,必须调用HAL_DMA_IRQHandler()或手动向DMA_IFCR写1。遗漏此步,标志位将持续置位。 - 根因3 :DMA通道在传输完成后被意外关闭。检查是否有其他代码错误地修改了
CCR寄存器的EN位。
- 根因1 :NVIC中断未使能或优先级配置不当。检查
5.2 提升DMA系统性能的实用技巧
-
利用半传输中断(HTI)实现无缝双缓冲 :对于需要连续、不间断数据流的场景(如音频播放),可将缓冲区分为两半(A和B)。配置DMA为循环模式,并使能HTI。当DMA传输完A区时,触发HTI,在HTI Handler中填充B区数据;当传输完B区时,触发TCI,在TCI Handler中填充A区数据。这样,CPU总是在DMA忙于传输一块数据时,准备下一块数据,实现了真正的流水线作业。
-
合理选择数据宽度 :尽可能使用16位(Half-word)传输。对于一个1000字节的缓冲区,使用8位传输需要1000次DMA事务;而使用16位传输,仅需500次。这减少了DMA控制器的状态切换开销,提升了整体吞吐量。当然,这要求源和目标的数据结构也必须是16位对齐的。
-
避免在中断中进行耗时操作 :DMA中断服务程序(ISR)应尽可能短小精悍。其核心任务只有两个:1) 清除中断标志;2) 调用
HAL_xxx_Callback()。所有数据处理、协议解析、状态更新等逻辑,都应在回调函数中完成。将耗时操作放在ISR中,会严重降低系统的实时响应能力,甚至导致后续中断丢失。 -
内存对齐优化 :确保DMA传输的源地址和目标地址都是
MSIZE或PSIZE的整数倍。例如,使用16位传输时,地址应为偶数。现代ARM Cortex-M内核对此要求相对宽松,但良好的对齐习惯能避免潜在的总线错误,并在某些情况下获得最佳性能。
在一次为工业传感器网关开发的项目中,我曾遇到一个棘手的bug:系统在长时间运行后,偶尔会出现UART发送丢包。经过数天的逻辑分析无果,最终通过在 DMA1_Channel4_IRQHandler 中添加一个简单的计数器并打印,发现中断确实被触发了,但 HAL_UART_TxCpltCallback() 回调却没有执行。追踪 HAL_DMA_IRQHandler() 源码,发现其内部有一个 if (__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_TC)) 的判断,而 __HAL_DMA_GET_IT_SOURCE 宏又依赖于 DMA_ISR 的状态。最终定位到,是另一个低优先级的SPI DMA中断服务程序在执行时,意外地将 DMA_ISR 寄存器的值读取并丢弃,导致 HAL_DMA_IRQHandler() 读取到的 ISR 值为空,从而跳过了TCI的处理分支。这个案例深刻地提醒我: 在共享同一DMA控制器的多外设系统中,对 DMA_ISR / DMA_IFCR 的操作必须是原子的、受保护的,任何对这些寄存器的“旁路”访问,都可能成为系统稳定性的定时炸弹。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)