1. 项目概述
在嵌入式系统开发中,尤其是面对高速数据流处理时,CPU如果被频繁的数据搬运任务所拖累,整个系统的实时性和效率就会大打折扣。想象一下,你正在用微控制器处理一个摄像头采集的图像数据流,或者高速ADC转换的音频采样点,如果每个字节的搬运都需要CPU亲自“跑腿”去读写内存和外设寄存器,那CPU就几乎没时间干“思考”和“计算”这类核心工作了。这时,直接内存访问(DMA)技术就成了解放CPU、提升系统吞吐量的关键角色。它就像一个专门负责“搬砖”的得力助手,一旦你告诉它从哪里搬(源地址)、搬到哪里去(目标地址)、搬多少(传输量)以及怎么搬(传输模式),它就能独立、高效地完成大批量数据的转移,而CPU则可以抽身去处理更复杂的算法或响应其他事件。
德州仪器(TI)的Tiva™ C系列微控制器,特别是像TM4C129XNCZAD这样的高性能型号,集成了一个高度灵活且功能强大的微直接内存访问(μDMA)控制器。与一些基础DMA控制器不同,μDMA提供了更为精细的控制能力,例如可编程的仲裁大小、多种传输模式(基础、自动请求、乒乓、散点-聚集等),以及独立的通道优先级管理。然而,其强大的灵活性也带来了配置上的复杂性。寄存器位域的含义、通道控制表的结构、不同传输模式下的配置流程,这些细节如果理解不透彻,很容易在调试时陷入困境。
本文将聚焦于Tiva微控制器的μDMA模块,抛开晦涩的理论堆砌,直接从工程实践角度出发。我会结合自己多次在数据采集和通信项目中配置μDMA的经验,详细拆解其核心工作机制,并通过三个最典型的实例:内存到内存传输、外设简单发送和外设乒乓接收,一步步展示如何从零开始,正确配置寄存器与通道控制结构,让μDMA真正为你所用。过程中,我会穿插那些数据手册不会明说,但实际调试中至关重要的“坑点”和技巧,帮助你快速上手,避免走弯路。
2. μDMA核心架构与配置流程解析
在动手写代码之前,我们必须先理解Tiva μDMA的“工作蓝图”。它不是一个简单的、只能执行单一搬运指令的模块,而是一个拥有完整状态机和可编程任务列表的智能数据搬运引擎。整个配置过程可以看作是在为这个引擎编写“工作说明书”。
2.1 全局控制器使能与通道控制表
μDMA控制器本身是一个外设,在使用前需要先“上电”并告诉它“工作手册”放在哪里。
第一步:使能μDMA控制器。这是通过设置DMA配置寄存器(DMACFG)的MASTEREN位完成的。这个操作通常放在系统外设时钟使能之后。需要特别注意的是,根据数据手册,在使能μDMA模块时钟后,必须等待至少3个系统时钟周期,才能对μDMA的任何寄存器进行读写操作。这是一个容易被忽略的硬件时序要求。
// 假设已使能μDMA模块的时钟(例如在SysCtlPeripheralEnable中) // 等待至少3个系统时钟周期。一种简单但不够精确的方法是执行几条空指令或短延时。 __asm(“ NOP”); __asm(“ NOP”); __asm(“ NOP”); // 使能μDMA主控制器 HWREG(UDMA_BASE + UDMA_CFG) = UDMA_CFG_MASTER_ENABLE;第二步:设置通道控制表基地址。这是μDMA配置中最核心的概念之一。通道控制表(Channel Control Table)是一块在系统RAM中预定义的数据结构,μDMA控制器通过读取这块内存来获取每个通道的传输参数(源地址、目标地址、控制字)。你需要通过DMACTLBASE寄存器告诉控制器这张表在内存中的起始位置。
关键细节与避坑指南:
DMACTLBASE寄存器写入的地址必须1024字节对齐(即地址的低10位必须为0)。例如,0x20001000是合法的,而0x20001040则不行。通常,我们会在链接脚本中定义一个专用的、对齐的RAM段,或者在代码中动态分配一块对齐的内存。不满足对齐要求会导致不可预知的行为。
// 在全局变量区域定义一个对齐到1024字节的结构体或数组作为控制表 // 使用编译器扩展属性确保对齐 #if defined(__TI_ARM__) || defined(__GNUC__) #pragma DATA_ALIGN(g_sDMAControlTable, 1024) #elif defined(__ICCARM__) #pragma data_alignment=1024 #endif static tDMAControlTable g_sDMAControlTable[64]; // 通常分配足够条目,例如对应32个通道的主备结构 // 将控制表基地址写入寄存器 HWREG(UDMA_BASE + UDMA_CTLBASE) = (uint32_t)g_sDMAControlTable;2.2 通道属性配置:设定搬运工的“性格”
每个μDMA通道(共32个)都可以独立配置其属性,这决定了该通道如何响应请求以及如何处理数据流。
优先级设置:通过
DMAPRIOSET(置位)或DMAPRIOCLR(清零)寄存器来设定通道是高优先级(High)还是默认优先级(Default)。高优先级通道可以打断正在进行的低优先级传输。这在处理实时性要求极高的数据(如USB端点数据、关键传感器中断)时非常有用。例如,设置通道7为高优先级:HWREG(UDMA_BASE + UDMA_PRIOSET) = (1 << 7);选择控制结构:每个通道有两套控制结构:主(Primary)和备(Alternate)。通过
DMAALTCLR(选择主结构)或DMAALTSET(选择备结构)寄存器来指定当前使用哪一套。在乒乓(Ping-Pong)或散点-聚集(Scatter-Gather)模式下,控制器会自动在两套结构间切换。对于简单的单次传输,我们通常使用主结构,因此需要清除对应的DMAALTCLR位。突发请求使能:
DMAUSEBURSTCLR和DMAUSEBURSTSET寄存器控制通道是否响应突发(Burst)请求。外设可以发出单次(Single)或突发(Burst)传输请求。如果清除该位(即设置为0),通道会响应两种请求;如果置位,则只响应突发请求。这需要与外设的FIFO深度和触发阈值配合考虑。例如,一个UART的接收FIFO深度为8,触发阈值设为4,那么当FIFO中有4个数据时,它可能发出一个突发请求。如果你的应用希望每次只搬一个数据,则应配置为只响应单次请求。请求掩码:
DMAREQMASKCLR用于允许通道识别请求(即“取消屏蔽”)。在配置初期或需要暂停通道时,可以通过DMAREQMASKSET来屏蔽请求。配置传输前,务必确保对应通道的请求是允许的。
2.3 通道控制结构详解:编写“搬运工任务单”
通道控制结构是存储在RAM中的通道控制表里的三个关键字段,它直接定义了单次传输任务的所有参数。理解每个字段的含义是正确配置的基石。
字段(在DMACHCTL寄存器中) | 位域 | 功能描述 | 配置要点与常见值 |
|---|---|---|---|
| DSTINC | 31:30 | 目标地址增量模式。 | 0x0: 字节增量,0x1: 半字增量,0x2: 字增量,0x3: 不增量(用于外设寄存器)。 |
| DSTSIZE | 29:28 | 目标数据大小。 | 0x0: 8位,0x1: 16位,0x2: 32位。必须与SRCSIZE相同。 |
| SRCINC | 27:26 | 源地址增量模式。 | 同DSTINC,0x3表示源地址固定(如从外设数据寄存器读)。 |
| SRCSIZE | 25:24 | 源数据大小。 | 同DSTSIZE,必须与DSTSIZE匹配。 |
| ARBSIZE | 17:14 | 仲裁大小。 | 定义在一次仲裁周期内连续传输的最大项数。必须为2的幂(1,2,4,...,1024)。需与外设FIFO深度匹配。 |
| XFERSIZE | 13:4 | 传输项数(总传输量-1)。 | 要传输的数据项数减1。例如,传输256个字,此处填255。注意是“项”数,不是字��数。 |
| XFERMODE | 2:0 | 传输模式。 | 0x0: 停止,0x1: 基础模式,0x2: 自动请求模式,0x3: 乒乓模式,0x4/0x5/0x6/0x7: 散点-聚集模式。 |
源和目标地址指针:DMASRCENDP和DMADSTENDP。这里有一个非常重要的概念:它们指向的是传输的最后一个地址(包含),而不是起始地址。例如,你要从一个起始地址为SrcAddr的缓冲区传输N个数据项(每个项大小为DataSize字节),那么源结束指针应设置为SrcAddr + (N-1) * DataSize。如果地址不增量(如访问外设寄存器),则直接填入该寄存器的地址。
实操心得:计算结束指针时,务必注意数据项的大小。对于32位字传输,
DataSize为4。一个常见的错误是直接使用字节数进行计算,导致地址错位。在编程时,利用指针类型转换可以避免手动计算偏移的麻烦,并提高代码可读性。
3. 典型传输模式实战配置
理解了基本原理后,我们通过三个具体场景,将理论转化为代码。我会以TivaWare驱动库风格的代码进行演示,并解释每一步的意图。
3.1 实例一:内存到内存传输(软件触发)
这是最基础的场景,常用于初始化大块内存(如清零)、缓冲区拷贝或数据预处理。我们使用专用的软件触发通道(通道30)。
步骤1:配置通道属性我们选择通道30,并将其配置为默认优先级、使用主控制结构、允许响应单次和突发请求、并允许请求。
// 假设使用TivaWare库 #include “driverlib/udma.h” // 设置通道30为默认优先级(即清除高优先级位) HWREG(UDMA_BASE + UDMA_PRIOCLR) = (1 << 30); // 选择主控制结构 HWREG(UDMA_BASE + UDMA_ALTCLR) = (1 << 30); // 允许响应单次和突发请求 HWREG(UDMA_BASE + UDMA_USEBURSTCLR) = (1 << 30); // 允许通道识别请求(取消屏蔽) HWREG(UDMA_BASE + UDMA_REQMASKCLR) = (1 << 30);步骤2:填充通道控制结构假设我们要将sourceBuffer中的256个32位字(即1024字节)拷贝到destBuffer。
// 计算控制表条目偏移量:通道号 * 0x10(每个主结构16字节) // 通道30的偏移量是 30 * 0x10 = 0x1E0 uint32_t ui32ControlTableBase = (uint32_t)g_sDMAControlTable; tDMAControlTable *psControlTable = (tDMAControlTable *)(ui32ControlTableBase + 0x1E0); // 设置源结束指针:最后一个字的地址 psControlTable->pvSrcEndAddr = (void *)((uint32_t)sourceBuffer + (256 - 1) * sizeof(uint32_t)); // 设置目标结束指针 psControlTable->pvDstEndAddr = (void *)((uint32_t)destBuffer + (256 - 1) * sizeof(uint32_t)); // 配置控制字 psControlTable->ui32Control = UDMA_CHCTL_DSTINC_WORD | // 目标地址按字递增 UDMA_CHCTL_DSTSIZE_WORD | // 目标数据大小为32位 UDMA_CHCTL_SRCINC_WORD | // 源地址按字递增 UDMA_CHCTL_SRCSIZE_WORD | // 源数据大小为32位 UDMA_CHCTL_ARBSIZE_256 | // 仲裁大小256(对于内存传输,可设为最大以提升效率) ((256 - 1) << UDMA_CHCTL_XFERSIZE_S) | // 传输项数255 UDMA_CHCTL_XFERMODE_AUTO; // 自动请求模式(软件触发一次,完成全部传输)为什么选择自动请求模式?对于纯内存拷贝,没有外设持续发出请求。自动请求模式(UDMA_CHCTL_XFERMODE_AUTO)意味着只需要一次软件请求,μDMA就会自动完成XFERSIZE指定的所有数据传输,非常适合这种场景。
步骤3:启动传输
// 1. 使能通道30 HWREG(UDMA_BASE + UDMA_ENASET) = (1 << 30); // 2. 发出软件传输请求 HWREG(UDMA_BASE + UDMA_SWREQ) = (1 << 30);启动后,μDMA控制器开始工作。在自动请求模式下,它会一次性搬完256个字。完成后,通道会自动禁用(DMAENASET对应位清零),控制字的XFERMODE字段也会被硬件清零。
3.2 实例二:外设简单发送(UART TX)
假设我们通过UART1发送一段数据,使用μDMA通道7(根据数据手册映射,UART1 TX可能默认在通道7,具体需查DMACHMAP寄存器)。
步骤1:配置通道属性与外设关联的通道属性配置与内存传输类似,但优先级和突发请求的配置需要结合外设特性。
// 设置通道7为默认优先级 HWREG(UDMA_BASE + UDMA_PRIOCLR) = (1 << 7); // 选择主控制结构 HWREG(UDMA_BASE + UDMA_ALTCLR) = (1 << 7); // 允许响应单次和突发请求(UART FIFO非空时可能发出请求) HWREG(UDMA_BASE + UDMA_USEBURSTCLR) = (1 << 7); // 允许通道识别UART的DMA请求 HWREG(UDMA_BASE + UDMA_REQMASKCLR) = (1 << 7);步骤2:填充通道控制结构我们要发送64个字节的数据。UART数据寄存器(UART1_DR_R)地址固定,目标地址不增量;源数据缓冲区地址递增。
// 通道7主结构偏移:7 * 0x10 = 0x070 tDMAControlTable *psControlTable = (tDMAControlTable *)((uint32_t)g_sDMAControlTable + 0x070); // 设置源结束指针(最后一个字节地址) psControlTable->pvSrcEndAddr = (void *)((uint32_t)txBuffer + 64 - 1); // 设置目标结束指针(UART数据寄存器地址,固定) psControlTable->pvDstEndAddr = (void *)(UART1_BASE + UART_O_DR); // 配置控制字 psControlTable->ui32Control = UDMA_CHCTL_DSTINC_NONE | // 目标地址不增量(外设寄存器) UDMA_CHCTL_DSTSIZE_8 | // 目标数据大小8位 UDMA_CHCTL_SRCINC_8 | // 源地址按字节递增 UDMA_CHCTL_SRCSIZE_8 | // 源数据大小8位 UDMA_CHCTL_ARBSIZE_4 | // 假设UART TX FIFO深度为4,仲裁大小设为4 ((64 - 1) << UDMA_CHCTL_XFERSIZE_S) | // 传输63个字节(64-1) UDMA_CHCTL_XFERMODE_BASIC; // 基础模式(每收到一个外设请求,传输ARBSIZE个数据)传输模式选择:基础模式 vs 自动请求模式对于外设发送,通常使用基础模式(UDMA_CHCTL_XFERMODE_BASIC)。因为UART的发送请求是基于其FIFO空余空间的。每当FIFO有空位(例如空出4个位置),UART会发出一个请求,μDMA响应并传输ARBSIZE(这里为4)个数据到FIFO。如此反复,直到所有数据发送完毕。自动请求模式在此场景不适用,因为它会试图一次性搬完所有数据,可能压满甚至溢出外设FIFO。
步骤3:配置外设以启用DMA请求仅仅配置μDMA还不够,必须告诉外设(UART):“请在有数据需求时,向DMA控制器发出请求”。
#include “driverlib/uart.h” // 使能UART1的发送DMA功能 UARTDMAEnable(UART1_BASE, UART_DMA_TX); // 可能需要根据具体UART型号设置FIFO触发级别 UARTFIFOLevelSet(UART1_BASE, UART_FIFO_TX1_8, UART_FIFO_RX1_8); // 示例:TX FIFO <=1/8时触发步骤4:启动传输
// 使能通道7 HWREG(UDMA_BASE + UDMA_ENASET) = (1 << 7); // 注意:这里不需要软件请求!传输由UART的硬件请求自动触发。此后,每当UART的TX FIFO有空位并达到触发条件,就会向μDMA通道7发出请求,μDMA随即搬运数据。传输完成后,通道自动禁用。
3.3 实例三:外设乒乓接收(UART RX)
这是处理连续数据流的经典模式,比如接收一个持续的串口���据流。它使用两个缓冲区(Ping和Pong),当一个缓冲区正在被DMA填充时,CPU可以处理另一个已满的缓冲区,实现数据接收和处理的并行,几乎无丢失数据风险。
步骤1:配置通道属性假设使用通道8进行UART1接收。
HWREG(UDMA_BASE + UDMA_PRIOCLR) = (1 << 8); HWREG(UDMA_BASE + UDMA_ALTCLR) = (1 << 8); HWREG(UDMA_BASE + UDMA_USEBURSTCLR) = (1 << 8); HWREG(UDMA_BASE + UDMA_REQMASKCLR) = (1 << 8);步骤2:配置主、备两套控制结构这是乒乓模式的关键。我们需要为通道8的主结构(对应缓冲区A)和备结构(对应缓冲区B)分别配置参数。
// 主结构偏移:8 * 0x10 = 0x080 tDMAControlTable *psPrimary = (tDMAControlTable *)((uint32_t)g_sDMAControlTable + 0x080); // 备结构偏移:0x200 + 8 * 0x10 = 0x280 tDMAControlTable *psAlternate = (tDMAControlTable *)((uint32_t)g_sDMAControlTable + 0x280); // 配置主结构(缓冲区A) psPrimary->pvSrcEndAddr = (void *)(UART1_BASE + UART_O_DR); // 源固定为UART数据寄存器 psPrimary->pvDstEndAddr = (void *)((uint32_t)pingBuffer + 64 - 1); // 目标指向缓冲区A末尾 psPrimary->ui32Control = UDMA_CHCTL_DSTINC_8 | // 目标缓冲区地址按字节递增 UDMA_CHCTL_DSTSIZE_8 | UDMA_CHCTL_SRCINC_NONE | // 源地址固定 UDMA_CHCTL_SRCSIZE_8 | UDMA_CHCTL_ARBSIZE_8 | // 假设UART RX FIFO深度为8,触发级别为8 ((64 - 1) << UDMA_CHCTL_XFERSIZE_S) | UDMA_CHCTL_XFERMODE_PINGPONG; // 传输模式:乒乓 // 配置备结构(缓冲区B) - 参数与主结构对称 psAlternate->pvSrcEndAddr = (void *)(UART1_BASE + UART_O_DR); psAlternate->pvDstEndAddr = (void *)((uint32_t)pongBuffer + 64 - 1); psAlternate->ui32Control = psPrimary->ui32Control; // 控制字相同步骤3:配置外设与中断启用UART的接收DMA,并必须启用UART的接收完成中断(或DMA完成中断,取决于芯片支持)。在乒乓模式下,每次一个缓冲区填满(即一次XFERSIZE传输完成),μDMA不仅会切换控制结构,还会触发外设的中断。
// 使能UART1的接收DMA UARTDMAEnable(UART1_BASE, UART_DMA_RX); // 使能UART1的接收中断(用于处理缓冲区满事件) UARTIntEnable(UART1_BASE, UART_INT_RX | UART_INT_RT); // RT是接收超时中断,有时也需使能 IntEnable(INT_UART1); // 使能UART1的NVIC中断步骤4:使能DMA通道并启动
// 使能通道8 HWREG(UDMA_BASE + UDMA_ENASET) = (1 << 8); // UART开始接收数据,DMA请求自动开始步骤5:编写中断服务程序(ISR)这是乒乓模式的核心处理逻辑。当UART中断触发时,我们需要判断是哪个缓冲区满了,并重新“武装”(re-arm)该缓冲区对应的控制结构。
void UART1_IRQHandler(void) { uint32_t ui32Status = UARTIntStatus(UART1_BASE, true); UARTIntClear(UART1_BASE, ui32Status); // 清除中断标志 // 检查是否是接收完成中断(或DMA完成中断) if(ui32Status & (UART_INT_RX | UART_INT_DMARX)) // 具体标志位请查阅数据手册 { // 1. 检查主控制结构传输是否完成(XFERMODE被硬件清零) tDMAControlTable *psPrimary = ...; // 获取主结构地址 if((psPrimary->ui32Control & UDMA_CHCTL_XFERMODE_M) == 0) { // 缓冲区A已满 processBufferA(); // 处理缓冲区A的数据 // 重新武装主结构,使其指向缓冲区A起始处,准备下次接收 psPrimary->pvDstEndAddr = (void *)((uint32_t)pingBuffer + 64 - 1); psPrimary->ui32Control |= UDMA_CHCTL_XFERMODE_PINGPONG; // 重新设置为乒乓模式 } // 2. 检查备控制结构传输是否完成 tDMAControlTable *psAlternate = ...; // 获取备结构地址 if((psAlternate->ui32Control & UDMA_CHCTL_XFERMODE_M) == 0) { // 缓冲区B已满 processBufferB(); // 处理缓冲区B的数据 // 重新武装备结构,使其指向缓冲区B起始处 psAlternate->pvDstEndAddr = (void *)((uint32_t)pongBuffer + 64 - 1); psAlternate->ui32Control |= UDMA_CHCTL_XFERMODE_PINGPONG; } } }关键技巧:在ISR中重新武装控制结构时,除了重置目标结束指针,必须重新设置
XFERMODE字段为乒乓模式。因为当一次传输完成后,硬件会将该字段清零(表示停止)。如果不重新设置,该套控制结构将不会再次被使用,导致乒乓模式失效,数据只会在两个缓冲区间轮流填充一次后停止。
4. 调试技巧与常见问题排查
即使按照手册配置,在实际项目中依然可能遇到μDMA不工作、数据错误或中断不触发的问题。以下是我在多个项目中总结的排查清单和经验。
4.1 DMA传输未启动或数据错误
时钟与使能检查:
- 首要检查:是否在系统控制模块中使能了μDMA控制器的外设时钟?
SysCtlPeripheralEnable(SYSCTL_PERIPH_UDMA); - 延时检查:使能时钟后,是否等待了至少3个系统时钟周期才访问μDMA寄存器?这是数据手册明确要求的。
- 主使能检查:
DMACFG寄存器的MASTEREN位是否已置1?
- 首要检查:是否在系统控制模块中使能了μDMA控制器的外设时钟?
通道控制表(CCT)地址:
- 对齐问题:
DMACTLBASE寄存器写入的地址是否1024字节对齐?可以用(address & 0x3FF) == 0来验证。 - 内存区域:控制表是否位于有效的、可被DMA控制器访问的RAM区域?有些微控制器的DMA无法访问所有内存区域。
- 对齐问题:
通道属性配置顺序:
- 一个稳健的配置顺序是:先配置通道控制结构(源/目标指针、控制字),再配置通道属性(优先级、突发等),最后使能通道并(如果是软件或外设触发)发出请求。避免在通道使能后修改控制结构。
源/目标指针与传输大小:
- 结束指针计算:这是最常见的错误源。务必确认
DMASRCENDP和DMADSTENDP设置的是最后一个有效数据的地址。公式:EndPtr = StartAddr + (TransferCount - 1) * DataSize。 - 数据大小匹配:
SRCSIZE和DSTSIZE必须设置为相同的值。试图将8位数据存入16位目标地址(不匹配的增量)会导致数据错位。 - 传输项数:
XFERSIZE字段是“传输项数减1”。要传输N项,就填N-1。
- 结束指针计算:这是最常见的错误源。务必确认
4.2 外设DMA传输不触发
外设DMA请求使能:
- 你配置了μDMA,但外设本身是否被设置为产生DMA请求?例如,对于UART,需要使用
UARTDMAEnable()函数。对于ADC,可能需要配置ADC的DMA控制寄存器。
- 你配置了μDMA,但外设本身是否被设置为产生DMA请求?例如,对于UART,需要使用
通道映射:
- 外设的DMA请求默认映射到哪个μDMA通道?这由
DMACHMAPn寄存器决定。务必查阅芯片数据手册的“DMA Channel Assignments”表格。例如,UART1 RX可能默认在通道8,但也可以重映射到其他通道。如果你修改了映射,两边的配置必须一致。
- 外设的DMA请求默认映射到哪个μDMA通道?这由
外设FIFO与仲裁大小:
ARBSIZE(仲裁大小)需要与外设的FIFO触发深度合理匹配。如果UART RX FIFO深度为8,触发级别设为7,那么ARBSIZE设为8是合适的。如果ARBSIZE设得太大(如128),而FIFO很快被填满,可能导致外设无法及时发出新的请求,影响效率。设得太小(如1)则可能增加仲裁开销。
请求掩码:
- 确认对应通道的
DMAREQMASKCLR位已被清除(允许请求)。在调试初期,可以尝试通过DMASWREQ寄存器手动发起一个软件请求,测试通道本身是否工作,以隔离是μDMA配置问题还是外设请求问题。
- 确认对应通道的
4.3 乒乓模式中断不触发或数据覆盖
中断使能:
- 乒乓模式下,必须使能外设相应���中断(通常是接收完成中断)。μDMA在完成一个缓冲区的传输后,会通过外设向CPU发出中断。仅配置μDMA而不使能外设中断,CPU将无法知晓缓冲区已满。
控制字未重装:
- 在中断服务程序中,处理完一个满缓冲区后,必须将该缓冲区对应的控制结构中的
XFERMODE字段重新设置为乒乓模式(UDMA_CHCTL_XFERMODE_PINGPONG),并更新目标结束指针到缓冲区的起始位置。这是最容易被遗忘的一步,会导致第二次传输无法启动。
- 在中断服务程序中,处理完一个满缓冲区后,必须将该缓冲区对应的控制结构中的
缓冲区大小与指针:
- 确保主、备控制结构中的目标结束指针分别正确指向两个不同的缓冲区,且缓冲区大小足够容纳
XFERSIZE指定的数据量。指针计算错误会导致数据写入到未知内存区域,造成程序崩溃。
- 确保主、备控制结构中的目标结束指针分别正确指向两个不同的缓冲区,且缓冲区大小足够容纳
传输模式设置:
- 主、备控制结构的
XFERMODE在初始化时都必须设置为乒乓模式。如果其中一个被错误地设置为停止(0)或其他模式,乒乓循环会在那里中断。
- 主、备控制结构的
4.4 性能优化与高级考量
仲裁大小优化:增大
ARBSIZE可以减少仲裁次数,在传输大块连续数据时提升总线利用率和整体吞吐量。但需要平衡外设FIFO的深度和实时性要求。数据对齐:虽然μDMA支持非对齐访问,但访问32位对齐的地址(地址是4的倍数)通常能获得最佳性能。在定义源和目标缓冲区时,可以考虑使用编译器属性(如
__attribute__((aligned(4))))进行对齐。缓存一致性:如果微控制器带有数据缓存(DCache),且DMA的目标或源地址位于缓存内存区域,必须在DMA操作前后执行缓存维护操作(Clean或Invalidate),以确保CPU和DMA控制器看到一致的数据。这是Cortex-M7等带缓存内核上的常见痛点。
通道优先级管理:在多个DMA通道同时工作的系统中,合理设置通道优先级可以确保高实时性数据流(如音频、电机控制PWM)不被低优先级的大数据块传输(如LCD刷新)所阻塞。