深入解析μDMA控制器:中断机制、寄存器配置与AES加密实战
1. μDMA控制器:嵌入式系统数据搬运的“隐形管家”
在嵌入式系统开发中,尤其是面对音频流处理、图像采集、无线通信或加密解密这类需要处理大量连续数据的场景时,一个高效的“数据搬运工”至关重要。这个角色就是DMA(Direct Memory Access,直接内存访问)。想象一下,CPU是公司里负责核心业务逻辑的经理,而DMA则是一位训练有素、沉默寡言的物流主管。当需要将成吨的原材料(数据)从仓库(内存)搬到生产线(外设),或者反过来时,如果每次都让经理(CPU)亲自去搬,他不仅会累得筋疲力尽,更会耽误核心的战略决策(程序执行)。DMA的出现,就是让这位物流主管全权负责这些重复性的搬运工作,经理只需在任务开始前下达指令(配置源地址、目标地址、数据量),并在任务完成或出错时接收一个简短的汇报(中断),期间可以完全专注于其他工作。
德州仪器(TI)在其CC27xx系列无线微控制器中集成的微DMA(μDMA)控制器,便是这种理念的极致体现。它不仅仅是简单的“搬运工”,更是一个高度可配置、智能化的数据传输引擎。与传统的DMA相比,μDMA引入了更精细的通道控制、灵活的中断管理以及与外设(如AES加密模块)深度集成的能力。它通过一个集中式的控制表和一系列功能寄存器,让开发者能够以编程方式定义复杂的数据传输序列,包括数据宽度、地址增量模式、传输模式(基本、自动、乒乓)等。其核心价值在于最大化总线利用率和最小化CPU干预,从而在电池供电的物联网设备中,实现高性能与低功耗的完美平衡。接下来,我们将深入其内部机制,从最核心的中断处理逻辑开始。
2. μDMA中断处理机制:从“喋喋不休”到“一击汇报”
中断是CPU与DMA控制器协同工作的关键信号。如果没有合理的中断机制,CPU要么需要不断轮询传输状态(浪费资源),要么会在数据搬运过程中被频繁打断。μDMA的中断设计哲学是“化繁为简”,旨在将多次外设数据就绪中断,合并为一次高效的传输完成中断。
2.1 中断流的接管与合并
当一个外设(例如UART、SPI或AES模块)启用μDMA进行数据传输后,其数据传输的“话语权”就发生了根本性变化。在传统无DMA模式下,外设每准备好一个数据单元(比如一个字节),就会产生一个中断请求给中断控制器(INTC),CPU必须立即响应,读取或写入数据。这就像快递员每送一个包裹就给你打一次电话。
启用μDMA后,情况变了。μDMA控制器会“拦截”原本由该外设发出的、用于通知数据已就绪的常规传输中断。这些中断信号不再送达INTC。请注意,外设本身的中断状态寄存器仍然会记录这些事件,但通知CPU的路径被μDMA接管了。取而代之的是,在整个由μDMA管理的大块数据传输(比如传输256字节)全部完成后,μDMA控制器才会代表该外设,向INTC产生一个“传输完成”中断。
这种设计的优势显而易见:
- 极大降低CPU中断负载:对于大量连续数据传输,CPU从中断的“汪洋大海”中解放出来,可能从数百次中断减少到一次。
- 提升系统实时性:CPU可以更长时间地连续执行主程序或处理其他高优先级任务,减少因频繁中断响应导致的上下文切换开销。
- 简化中断服务程序(ISR)逻辑:ISR无需处理单个数据单元,只需在传输完成后,进行批量数据的后处理(如校验、通知上层应用)。
2.2 完成中断的识别与处理
那么,当CPU进入一个外设的中断服务程序时,它如何判断这次中断是来自μDMA的“传输完成”,还是外设本身报告的“错误事件”(如溢出、帧错误)呢?μDMA提供了一个非常清晰的硬件标志位机制。
每个μDMA通道都对应一个独立的完成状态位,这些位集中记录在DMA Channel Request Done寄存器(DMA.REQDONE)中。这是一个关键的“任务完成看板”。当某个通道的传输任务完成时,μDMA控制器会自动将该通道对应的CHNLS位置1。
因此,一个健壮的外设中断服务程序应该遵循以下流程:
void UART_DMA_IRQHandler(void) { // 1. 检查μDMA完成状态 uint32_t dmaDoneStatus = HWREG(DMA_BASE + DMA_O_REQDONE); if (dmaDoneStatus & (1 << UART_RX_DMA_CHANNEL)) { // 这是μDMA接收完成中断 // 清除μDMA完成标志(写1清零) HWREG(DMA_BASE + DMA_O_REQDONE) = (1 << UART_RX_DMA_CHANNEL); // 处理接收到的批量数据(例如,存入环形缓冲区,设置数据就绪标志) process_received_data(); // 可以在此重新配置并启动下一次DMA传输(循环模式) setup_next_dma_transfer(); } // 2. 检查外设自身的错误中断标志(例如UART的OE, FE, PE, BE错误) uint32_t uartErrorStatus = HWREG(UART0_BASE + UART_O_RSR) & UART_RSR_ANY; if (uartErrorStatus) { // 处理错误(如清除错误标志,记录日志,复位接收状态) handle_uart_errors(uartErrorStatus); } // 注意:μDMA的完成中断请求在中断处理程序被激活时会被硬件自动清除, // 但DMA.REQDONE中的状态位是“粘滞”的,需要软件写1清零,如上所示。 }这个流程确保了中断原因的精准判别和高效处理。对于软件触发的μDMA通道(通常是内存到内存传输),其完成中断会走专用的软件μDMA中断向量,逻辑更为简单。
2.3 错误中断的处理
数据传输并非总是顺利。如果μDMA控制器在尝试执行数据传输时遇到了总线错误(例如访问了非法地址或受保护的内存区域)或内存保护错误,它会立即采取以下措施:
- 禁用故障通道:自动禁用引发错误的那个μDMA通道,防止错误操作继续。
- 触发错误中断:在专用的μDMA错误中断向量上产生一个错误中断。
在错误中断服务程序中,软件需要读取DMA Error Status and Clear寄存器(DMA.ERROR)的STATUS位。如果该位为1,则表示有未决的错误。处理错误(例如记录错误地址、进行系统恢复)后,必须通过向该STATUS位写1来清除错误标志,否则该中断将持续触发。
重要提示:μDMA错误事件通过芯片内部的事件总线(Event Fabric)连接到Arm Cortex-M33处理器的中断系统。这意味着你需要确保在NVIC(嵌套向量中断控制器)中正确启用对应的μDMA错误中断,并设置好优先级。
3. μDMA寄存器详解:操控数据传输的“控制面板”
要熟练驾驭μDMA,必须理解其寄存器地图。这些寄存器是软件与μDMA硬件对话的唯一接口。CC27xx的μDMA寄存器组设计清晰,主要分为全局控制、通道属性控制和状态查询三大类。下面我们挑选最核心、最常用的寄存器进行深入解析,并解释其背后的设计逻辑。
3.1 全局配置与控制寄存器
这类寄存器用于控制μDMA控制器的整体行为,是所有通道操作的基础。
DMA Configuration Register (DMA.CFG)这是μDMA的总开关。其最低位MASTERENABLE是控制器的使能位。在给任何通道分配任务前,必须先置位此位。此外,PRTOCTRL字段(位[7:5])用于设置AHB-Lite总线访问的保护属性,控制HProt[3:1]信号,这对于在具有内存保护单元(MPU)或缓存系统的复杂SoC中确保访问安全至关重要。例如,你可以通过它标记DMA传输是否为缓存访问、缓冲访问或特权访问。
DMA Channel Control Base Pointer Register (DMA.CTRL)这是μDMA架构的核心之一。它指向一个在系统内存中分配的通道控制表(Channel Control Table)的基地址。这个表存储了每个通道的传输描述符(或称控制数据结构),包括源地址指针、目的地址指针、传输数据量、传输模式等。控制器在执行时,会从这个表中读取配置。基地址必须256字节对齐,这通常是出于硬件设计中对地址解码和访问效率的考虑。
DMA Status Register (DMA.STATUS)这是一个只读寄存器,用于查询控制器的状态。MASTERENABLE位反映CFG寄存器的设置。STATE字段(位[7:4])是一个非常有用的调试工具,它实时显示μDMA内部状态机的当前状态,例如:空闲(Idle)、正在读取通道控制数据、正在读写数据、等待请求清除、已完成等。TOTALCHANNELS字段则告诉你硬件实际实现了多少个DMA通道(值是通道数减一)。
3.2 通道属性控制寄存器(Set/Clear 对)
μDMA为许多通道控制功能设计了一对“置位”和“清零”寄存器。这种设计简化了原子操作:你想设置某个通道的某个属性,就向对应的SETxxx寄存器的对应位写1;想清除该属性,就向对应的CLEARxxx寄存器的对应位写1。向这些寄存器的位写0是无效操作。这种模式避免了软件需要“读-修改-写”寄存器,在多任务或中断环境下更安全。
DMA Set/Clear Channel Enable (DMA.SETCHANNELEN / DMA.CLEARCHANNELEN)这是通道的开关。只有使能的通道才会响应传输请求。在启动传输前使能通道,在传输完成或需要禁用时清除使能。
DMA Set/Clear Channel Priority (DMA.SETCHNLPRIORITY / DMA.CLEARCHNLPRIORITY)用于设置通道优先级。当多个通道同时发出请求时,高优先级通道会优先获得总线仲裁权。在实时性要求高的场景(如音频播放),可以将相关通道设为高优先级。
DMA Set/Clear UseBurst (DMA.SETBURST / DMA.CLEARBURST)控制通道对“突发(Burst)”请求的响应模式。当该位被设置(即禁用单次请求)时,通道只响应外设发出的突发传输请求,每次执行固定长度(由仲裁大小决定)的连续传输。这能最大化总线带宽利用率,适合大数据量、连续地址的传输。当该位被清除时,通道既响应突发请求也响应单次请求,灵活性更高。
DMA Set/Clear Request Mask (DMA.SETREQMASK / DMA.CLEARREQMASK)这是一个“请求屏蔽”开关。如果屏蔽(SET)了某个通道,那么该通道将完全忽略来自外部的任何传输请求(无论是硬件外设请求还是软件请求)。这常用于在配置通道或处理关键任务时,临时暂停该通道的传输活动。
DMA Set/Clear Primary-Alternate (DMA.SETCHNLPRIALT / DMA.CLEARCHNLPRIALT)这是实现“乒乓(Ping-Pong)”或“散射-聚集(Scatter-Gather)”高级传输模式的关键。每个通道有两套控制数据结构:主(Primary)和备用(Alternate)。当一次传输完成后,可以通过硬件或软件自动切换到另一套数据结构,从而实现不间断的连续数据传输。此寄存器用于选择当前使用哪一套结构。
3.3 传输请求与状态寄存器
DMA Channel Software Request Register (DMA.SOFTREQ)对于软件通道(如内存到内存传输),没有硬件外设来触发请求。此时,软件通过向该寄存器的对应通道位写1,来手动发起一次传输请求。这是启动软件通道传输的唯一方式。
DMA Channel Request Done Register (DMA.REQDONE)如前所述,这是传输完成的“粘滞”状态寄存器。每个通道完成一次请求后,其对应位会被硬件置1。该位不会自动清除,必须由软件写1来清零。这确保了即使CPU暂时没来得及处理中断,完成状态也不会丢失。在查询方式(非中断)使用DMA时,轮询此寄存器是判断传输是否完成的标准方法。
DMA Channel Wait On Request Status Register (DMA.WAITONREQ)这个寄存器控制通道的“请求握手”行为。当某通道的CHNLSTATUS位为1时,μDMA会在收到该通道的请求后,即使完成了当前仲裁大小的传输,只要外部请求信号仍然有效,就会保持通道为“活跃”状态,继续等待数据传输。这对于时钟域不同步或速度较慢的外设(请求者)是必要的,确保μDMA不会在对方还没准备好下一个数据时就断开连接。通常,对于大多数片上外设,此位应保持为0。
DMA Error Status and Clear Register (DMA.ERROR)错误状态寄存器。当发生总线错误时,STATUS位被置1并产生中断。软件读取确认后,必须通过写1来清除此位。
通过合理配置上述寄存器,你可以精细地控制每一个DMA通道的行为,构建出高效、可靠的数据传输流水线。
4. μDMA的初始化与配置实战:以内存到内存传输为例
理解了寄存器之后,我们通过一个完整的、软件发起的“内存到内存”传输示例,将理论转化为实践。我们假设使用通道6(这是一个典型的软件通道),将256个32位字(Word)从源缓冲区srcBuffer搬运到目标缓冲区dstBuffer。
4.1 模块初始化:搭建舞台
在配置任何通道之前,必须确保μDMA控制器本身已经上电并启用。这通常涉及电源管理域和时钟的使能。
使能控制器时钟与电源:首先,需要确保μDMA控制器所在的电源域已上电,并且其时钟被使能。在CC27xx中,可以通过设置
CLKCTL.CLKENSET0寄存器的第17位(DMA位),或者直接调用TI驱动库提供的便捷函数:#include <ti/drivers/Power.h> #include <ti/drivers/dma/UDMACC26XX.h> // 使能DMA电源域 Power_enable_DMA();这一步是硬件依赖的,确保控制器有“能量”工作。
全局使能μDMA控制器:然后,我们需要打开控制器的总开关。通过设置
DMA.CFG寄存器的MASTERENABLE位为1。// 假设 DMA_BASE 是μDMA控制器的基地址 #define DMA_BASE 0x40021000 #define DMA_O_CFG 0x004 #define DMA_CFG_MASTER_ENABLE 0x00000001 HWREG(DMA_BASE + DMA_O_CFG) |= DMA_CFG_MASTER_ENABLE;配置通道控制表基址:这是最关键的一步。我们需要在内存中分配一块对齐的区域,用于存放各个通道的控制数据结构,并将其基地址告知μDMA控制器。
// 分配一个对齐在256字节边界的内存区域作为控制表。 // 通常我们会将其定义在链接脚本中的一个特定段(如 .dma_table),并设置对齐属性。 // 这里以静态数组为例(编译器需支持对齐属性,如GCC的 __attribute__((aligned(256))) ) #define DMA_CHANNEL_COUNT 32 // 假设有32个通道 #define DMA_CTRL_STRUCT_SIZE 16 // 每个通道的控制结构大小(4个32位字) // 通道控制表,必须256字节对齐 static uint32_t g_pui32ControlTable[DMA_CHANNEL_COUNT * DMA_CTRL_STRUCT_SIZE] __attribute__((aligned(256))); // 将基地址写入 DMA.CTRL 寄存器。注意:只取高23位([31:9]),低9位硬件强制为0。 HWREG(DMA_BASE + DMA_O_CTRL) = (uint32_t)g_pui32ControlTable;控制表中的每个条目(对应一个通道)包含4个32位字,分别定义了传输模式、数据大小、源/目的地址指针、传输数量等。具体布局需要参考芯片手册。
4.2 配置通道属性:定义搬运工的性格
接下来,我们针对要使用的通道6进行个性化设置。我们需要清除其所有属性,因为对于软件传输,通常使用默认配置。
设置通道优先级(可选):我们可以选择将其设为高优先级或默认优先级。这里我们选择默认优先级。
// 使用 CLEARCHNLPRIORITY 寄存器将通道6设为默认优先级 // 向 CLEARCHNLPRIORITY[6] 写1 HWREG(DMA_BASE + DMA_O_CLEARCHNLPRIORITY) = (1 << 6);选择主控制结构:我们使用主控制结构(Primary)而非备用结构(Alternate)。
// 使用 CLEARCHNLPRIALT 寄存器选择主结构 HWREG(DMA_BASE + DMA_O_CLEARCHNLPRIALT) = (1 << 6);允许响应单次和突发请求:对于软件通道,我们通常希望它能响应单次请求(因为我们用软件触发一次就是一次传输)。
// 使用 CLEARBURST 寄存器允许单次请求 HWREG(DMA_BASE + DMA_O_CLEARBURST) = (1 << 6);允许通道识别请求:我们必须取消对通道请求的屏蔽,否则它不会理会我们的软件启动命令。
// 使用 CLEARREQMASK 寄存器使能通道6的请求 HWREG(DMA_BASE + DMA_O_CLEARREQMASK) = (1 << 6);以上步骤也可以通过TI驱动库函数
uDMAChannelAttributeDisable()一次性完成,它会将指定属性清除(即设为默认状态)。
4.3 配置通道控制结构:绘制搬运路线图
这是配置的核心,我们直接在之前分配的控制表g_pui32ControlTable中,填写通道6对应的条目。控制结构通常包含以下几个字段(具体位域定义请查阅手册):
- 控制字(Control Word):定义传输模式(如基本模式、自动模式)、数据大小(8/16/32位)、源和目的地址的增量方式(递增、递减、不变)、仲裁大小等。
- 源地址结束指针(Source End Pointer):指向源数据区的最后一个地址。
- 目的地址结束指针(Destination End Pointer):指向目的数据区的最后一个地址。
- 备用控制字/未使用。
对于我们的任务:传输256个32位字,源和目的地址都按字(4字节)递增。
// 计算通道6在控制表中的索引。假设每个通道的控制结构是4个连续的32位字。 uint32_t *pui32ControlEntry = &g_pui32ControlTable[6 * 4]; // 通道6,索引为6 // 1. 配置控制字 // 假设我们使用“基本模式”(完成一次传输后停止),仲裁大小设为8(即每完成8个字的传输,释放一次总线)。 // 数据大小:32位 (0x3) // 源地址增量:32位 (0x3) // 目的地址增量:32位 (0x3) // 传输模式:基本模式 (0x0) // 仲裁大小:8 (0x7,因为仲裁大小 = 2^(ARSIZE+1),0x7对应256? 这里需要根据手册公式计算,假设为8) // 总传输数量:256-1 = 255 (因为硬件计数值是N-1) uint32_t ui32Control = 0; ui32Control |= (0x3 << 26); // 目的数据大小: 32-bit ui32Control |= (0x3 << 24); // 源数据大小: 32-bit ui32Control |= (0x0 << 22); // 保留 ui32Control |= (0x3 << 18); // 目的地址增量: 32-bit ui32Control |= (0x3 << 14); // 源地址增量: 32-bit ui32Control |= (0x0 << 12); // 保留 // 假设手册定义:传输模式[11:8], 0x0=基本模式 ui32Control |= (0x0 << 8); // 传输模式: 基本模式 // 假设手册定义:仲裁大小[7:4], 0x3 对应 2^(3+1)=16? 这里需要精确计算。我们假设配置为8次传输后仲裁。 // 假设我们找到宏定义 UDMA_ARB_8 的值为 0x00000030 (位[7:4]=3) ui32Control |= (0x3 << 4); // 仲裁大小: 8 // 传输数量[3:0] 和 [15:12] (对于32位传输,数量是 (XFSIZE+1) * (2^(ARSIZE+1))? 这里非常依赖手册公式) // 更常见的做法是使用驱动库函数来设置这些复杂的位域。 // 此处仅为示意,实际开发强烈建议使用驱动库API。 pui32ControlEntry[0] = ui32Control; // 控制字写入控制表第一个字 // 2. 配置源地址结束指针 // 源缓冲区起始地址 srcBuffer,传输256个字,地址按字递增。 // 结束指针 = 起始地址 + (传输数量 - 1) * 数据大小(字节) // 假设 srcBuffer 是 uint32_t 数组 extern uint32_t srcBuffer[256]; pui32ControlEntry[1] = (uint32_t)&srcBuffer[255]; // 指向最后一个元素 // 3. 配置目的地址结束指针 extern uint32_t dstBuffer[256]; pui32ControlEntry[2] = (uint32_t)&dstBuffer[255]; // 指向最后一个元素 // 4. 第四个字段在基本模式下未使用,可以置0 pui32ControlEntry[3] = 0x00000000;重要提示:手动计算控制字极易出错,且不同厂商、不同系列的DMA控制器位域定义差异巨大。在实际项目中,务必使用芯片厂商提供的驱动库函数(如TI的
uDMAChannelControlSet)来配置控制结构,它们封装了所有底层细节,并经过充分测试。
4.4 启动传输:按下启动按钮
一切准备就绪,现在可以启动传输了。对于软件通道,启动分为两步:使能通道,然后发出软件请求。
(可选)使能中断:如果我们希望传输完成后通过中断通知CPU,需要配置中断。
#include <ti/drivers/dpl/HwiP.h> // 假设使用TI的HwiP抽象层 // 使能全局中断 IntMasterEnable(); // 使能μDMA的传输完成中断(中断号需查手册) IntEnable(INT_DMA_SW); // 假设 INT_DMA_SW 是软件通道完成中断号同时,需要编写对应的中断服务程序(ISR),并在其中检查
DMA.REQDONE寄存器并处理。使能通道:告诉μDMA,通道6现在可以工作了。
// 向 SETCHANNELEN 寄存器的第6位写1 HWREG(DMA_BASE + DMA_O_SETCHANNELEN) = (1 << 6);发出软件传输请求:这是启动传输的“发令枪”。
// 向 SOFTREQ 寄存器的第6位写1,为通道6生成一个软件请求 HWREG(DMA_BASE + DMA_O_SOFTREQ) = (1 << 6);一旦请求发出,μDMA控制器会立即从通道6的控制表中读取配置,并开始数据搬运。
等待传输完成(查询方式):如果不使用中断,可以通过轮询
DMA.SETCHANNELEN寄存器或DMA.REQDONE寄存器来判断传输是否完成。// 轮询通道使能位,传输完成后硬件会自动清除该位 while (HWREG(DMA_BASE + DMA_O_SETCHANNELEN) & (1 << 6)) { // 可以在此处执行低优先级任务或进入低功耗模式 } // 或者轮询完成标志位 // while (!(HWREG(DMA_BASE + DMA_O_REQDONE) & (1 << 6))); // 传输完成后,清除完成标志(如果使用REQDONE轮询) HWREG(DMA_BASE + DMA_O_REQDONE) = (1 << 6); // 此时,数据已从 srcBuffer 完整复制到 dstBuffer
至此,一个完整的内存到内存DMA传输流程就完成了。这个过程清晰地展示了从硬件初始化、通道配置到最终启动的每一步,以及其中关键的寄存器操作。
5. μDMA与AES加速器的协同作战:硬件加密流水线
在安全至上的物联网设备中,加密解密是常见操作。CC27xx的AES加速器模块与μDMA的深度集成,为我们构建一个高效的硬件加密数据流水线提供了可能。其核心思想是:让μDMA负责在内存和AES模块之间自动搬运明文和密文数据,AES模块则专注于加密/解密运算,CPU几乎不参与数据传输过程。
5.1 AES模块的μDMA接口设计
AES模块为数据传输分配了两个专用的μDMA通道:通道4(Channel A)和通道5(Channel B)。这是一个典型的生产者-消费者模型优化:
- 通道A(高优先级):通常用于将待加密的明文从内存(如SRAM)搬运到AES模块的输入缓冲区。
- 通道B:通常用于将加密完成的密文从AES模块的输出缓冲区搬运回内存。
每个通道都有独立的“请求(Request)”和“完成(Done)”信号与μDMA控制器交互。AES模块在输入缓冲区有空闲或输出缓冲区有数据时,会向μDMA发出请求;μDMA完成一次搬运后,会向AES模块回送完成信号。这种硬件级的握手协议,使得数据流可以无缝衔接。
5.2 一个完整的AES-CBC加密DMA配置实例
假设我们需要使用AES-CBC模式,加密4个连续的数据块(每个块128位,即16字节)。我们将使用通道A输入明文,通道B输出密文。
第一步:配置AES模块首先,需要配置AES的工作模式、密钥、初始化向量(IV)等。这通常通过写AES模块的寄存器完成(AES.KEY0-3,AES.TXT0-3(IV),AES.AUTOCFG等)。关键在于AUTOCFG寄存器,我们需要设置触发条件。例如,可以配置为“写入BUF3寄存器时触发AES加密”(TRGAES = WRBUF3),并可能启用“总线暂停”选项(BUSHALT = EN)以确保数据一致性。
第二步:配置μDMA通道A(输入通道)我们需要设置通道4(AES Channel A)的控制结构,用于从内存搬运明文到AES的DMA.DMACHA地址(这是一个固定的硬件地址,指向AES的输入缓冲区)。
// 伪代码,使用驱动库概念 uDMAChannelControlSet(UDMA_CHANNEL_AES_A, // 通道号 UDMA_SIZE_32, // 传输数据大小:32位 UDMA_ARB_4, // 仲裁大小:4个数据单元后释放总线 UDMA_SRC_INC_32, // 源地址(内存)递增:32位(4字节) UDMA_DST_INC_NONE, // 目的地址(AES模块)不递增 UDMA_MODE_BASIC); // 传输模式:基本模式 // 设置传输参数 uint32_t plaintext[4][4]; // 假设明文是4个块,每个块4个32位字 uDMAChannelTransferSet(UDMA_CHANNEL_AES_A, UDMA_MODE_BASIC, plaintext, // 源地址:内存中的明文数组 (void *)AES_DMACHA_ADDR, // 目的地址:AES模块的DMA输入地址 4 * 4); // 传输数量:4块 * 4字/块 = 16个字注意目的地址AES_DMACHA_ADDR是固定值,由硬件映射。由于AES模块内部会自动管理其缓冲区的写入指针,所以这里目的地址增量设为UDMA_DST_INC_NONE。
第三步:配置μDMA通道B(输出通道)类似地,配置通道5(AES Channel B),用于从AES模块的DMA.DMACHB地址搬运密文到内存。
uDMAChannelControlSet(UDMA_CHANNEL_AES_B, UDMA_SIZE_32, UDMA_ARB_4, UDMA_SRC_INC_NONE, // 源地址(AES模块)不递增 UDMA_DST_INC_32, // 目的地址(内存)递增:32位 UDMA_MODE_BASIC); uint32_t ciphertext[4][4]; // 密文存储区 uDMAChannelTransferSet(UDMA_CHANNEL_AES_B, UDMA_MODE_BASIC, (void *)AES_DMACHB_ADDR, // 源地址:AES模块的DMA输出地址 ciphertext, // 目的地址:内存中的密文数组 4 * 4);第四步:启动传输并联动
- 使能AES模块和两个DMA通道。
- 通过软件向AES模块的触发寄存器写值(例如,写
TRG.DMACHA),启动整个流程。 - AES模块开始工作后,当其输入缓冲区需要数据时,会通过事件触发μDMA通道A进行传输;当其输出缓冲区有数据时,会触发通道B进行传输。
- 两个DMA通道可以并行工作,形成流水线:当AES正在加密第N块数据时,通道A可以搬运第N+1块明文,通道B可以搬运第N-1块密文。
- 所有4个数据块处理完毕后,AES模块和两个DMA通道都会产生完成中断或标志,通知CPU任务完成。
通过这种紧密的硬件协作,数据加密的吞吐量得到极大提升,CPU仅在开始和结束时进行干预,特别适合无线通信中需要实时加密/解密大量数据包的应用。
6. 软件设计注意事项与常见问题排查
即使硬件配置正确,软件逻辑上的疏忽也可能导致DMA工作异常。以下是一些从实际项目中总结出的关键注意事项和排查技巧。
6.1 关键软件守则
勿在活跃传输中禁用μDMA:绝对不要在μDMA通道还有活跃传输任务时,禁用整个μDMA控制器(清除
DMA.CFG[0] MASTERENABLE)或关闭其时钟。手册明确指出,此时的行为是未定义的。重新使能后,外设状态和内存内容可能已改变,导致系统崩溃或数据错误。正确的做法是,先停止所有通道(使用CLEARCHANNELEN),等待所有传输完成(检查REQDONE或通道使能位),然后再禁用控制器。进入低功耗模式前的清理:在让系统进入深度睡眠(Standby)等低功耗模式前,必须确保所有来自外设的μDMA通道都被禁用,并且没有新的传输请求产生。否则,当μDMA时钟被关闭时,正在进行的传输序列会被强制终止(虽然总线不会挂死,但当前事务可能不完整),这可能导致数据丢失或外设状态错误。
内存对齐与边界:源地址和目的地址,特别是控制表基地址(
DMA.CTRL),必须严格遵守硬件要求的对齐边界(如256字节)。非对齐访问可能引发硬件错误或静默的数据错误。使用编译器属性(如__attribute__((aligned(256))))或动态内存分配函数(如memalign)来保证。数据一致性(Cache Coherency):如果CPU的缓存(Cache)被启用,需要特别注意DMA操作内存的数据一致性问题。DMA控制器直接访问物理内存,不经过CPU缓存。如果CPU缓存了某块即将被DMA写入或读取的内存,就会导致数据不同步。
- DMA写入的内存(CPU读取):在启动DMA写入之前,如果CPU曾写过这块内存,需要确保数据从Cache写回内存(Clean);在DMA写入完成后、CPU读取之前,需要使CPU Cache中该区域的缓存失效(Invalidate)。
- DMA读取的内存(CPU写入):在CPU写入数据后、启动DMA读取之前,必须确保数据从Cache写回内存(Clean)。 许多MCU提供硬件维护操作或库函数来处理Cache一致性,务必在DMA传输前后调用它们。
6.2 常见问题排查速查表
当DMA传输不工作或出现异常时,可以按照以下步骤进行排查:
| 现象 | 可能原因 | 排查步骤与解决方法 |
|---|---|---|
| 传输完全无法启动 | 1. μDMA控制器未全局使能。 2. 通道未使能。 3. 通道请求被屏蔽。 4. 控制表基址未配置或未对齐。 5. 软件请求未发出(针对软件通道)。 | 1. 检查DMA.CFG[0] MASTERENABLE是否为1。2. 检查 DMA.SETCHANNELEN对应通道位是否为1。3. 检查 DMA.SETREQMASK对应通道位是否为0(未屏蔽)。4. 确认 DMA.CTRL寄存器值是否正确且对齐。5. 对于软件通道,确认已向 DMA.SOFTREQ对应位写1。 |
| 传输启动但未完成 | 1. 传输数量配置错误(例如,配置为0)。 2. 源/目的地址指针错误(如空指针、非法地址)。 3. 外设未正确发出请求(硬件通道)。 4. 总线错误导致通道被禁用。 | 1. 仔细核对控制结构中的传输数量字段,硬件通常要求编程N-1。2. 检查源和目的地址指针是否有效,并符合对齐要求。 3. 使用逻辑分析仪或调试器检查外设的DMA请求信号。 4. 检查 DMA.ERROR寄存器的STATUS位,并查看是否产生错误中断。 |
| 传输完成但数据错误 | 1. 数据大小(8/16/32位)配置错误。 2. 地址增量模式配置错误。 3. Cache一致性问题。 4. 源/目的缓冲区在传输过程中被意外修改。 | 1. 确认控制字中源和目的的数据大小位与外设及内存数据类型匹配。 2. 确认地址增量与缓冲区布局匹配(例如,数组用递增,外设固定寄存器用不变)。 3. 在DMA传输前后执行必要的Cache Clean和Invalidate操作。 4. 确保在DMA传输期间,没有其他中断或任务访问同一块缓冲区。 |
| 中断未触发 | 1. 中断未在NVIC中使能。 2. 中断服务程序(ISR)未正确清除中断标志。 3. 对于μDMA完成中断, DONEMASK寄存器可能屏蔽了信号。 | 1. 确认NVIC中对应的μDMA或外设中断已使能,且优先级设置正确。 2. 在ISR中,检查并清除 DMA.REQDONE寄存器位(写1清零)以及外设的中断标志。3. 检查 DMA.DONEMASK寄存器,确保对应通道位为0,允许完成状态传播。 |
| 系统不稳定或偶尔卡死 | 1. 内存访问冲突(DMA与CPU访问同一区域)。 2. 在DMA传输过程中修改了控制表或配置寄存器。 3. 仲裁大小设置过大,长期占用总线。 | 1. 使用互斥锁(Mutex)或标志位来保护共享缓冲区,确保CPU和DMA不同时访问。 2. 确保在一次传输配置完成后、启动前,不再修改其控制结构。如需修改,先停止通道。 3. 适当减小仲裁大小,让总线有机会响应其他主设备(如CPU)的请求。 |
掌握这些原则和排查方法,能帮助你在嵌入式开发中更自信、更高效地运用μDMA这一强大工具,构建出响应迅速、资源利用率高的固件系统。