1. μDMA控制器:嵌入式系统性能的“数据搬运工”
在嵌入式系统开发中,尤其是涉及实时数据采集、高速通信(如UART、SPI、I2S)或图形处理的场景,CPU常常被大量重复性的数据搬运任务所拖累。想象一下,CPU就像一个忙碌的厨师,不仅要炒菜(执行核心算法),还要不停地往返于冰箱(内存)和灶台(外设)之间取送食材(数据)。这种“跑腿”工作不仅效率低下,还会让厨师无暇顾及火候。直接内存访问(DMA)技术就是为了解决这个问题而生的,它相当于为系统聘请了一位专职的“传菜员”。
在德州仪器(TI)的Tiva™系列微控制器(如TM4C1294NCPDT)中,这个“传菜员”被称为微直接内存访问(μDMA)控制器。与传统的DMA相比,μDMA的设计更加精巧和灵活。它不仅仅是一个简单的数据搬运工,更像是一个智能的物流调度中心。它拥有多达32个独立的通道,每个通道都可以被配置为连接特定的“供应商”(源,如内存或外设)和“客户”(目的,如另一块内存或外设)。通过一套存储在系统内存中的“物流计划表”(通道控制结构),μDMA控制器能够自主完成复杂的数据传输任务,而无需CPU这个“厨师”的干预。
其核心价值在于解放CPU和提升数据吞吐效率。当ADC需要将采集到的1000个样本存入数组,或者UART需要发送一长串数据时,你只需初始化好μDMA,下达“开始”指令,CPU就可以转身去处理更重要的控制逻辑或算法运算。μDMA会在后台默默完成所有数据传输,并在完成后通过中断通知CPU“任务已完成”。这种机制极大地减少了数据传输的延迟,避免了因CPU忙于搬运数据而错过关键外部事件的情况,对于构建高实时性、高可靠性的嵌入式系统至关重要。
2. μDMA核心架构与工作原理解析
要熟练驾驭μDMA,不能只停留在“配置寄存器”的层面,必须理解其内部的工作逻辑。这就像开车,知道油门和刹车在哪固然重要,但了解发动机和变速箱如何协同工作,才能开得又快又稳。
2.1 通道、仲裁与优先级机制
μDMA控制器内部有32个通道(Channel 0-31)。每个通道都是独立的,可以配置为服务于一个特定的外设(如UART0的发送)或由软件触发(用于内存间拷贝)。通道0-31的默认映射关系是固定的,例如,通道30通常预留给软件触发,而UART0的接收和发送可能分别映射到通道4和5。
当多个外设同时请求DMA服务时,谁先谁后?这就涉及到仲裁。μDMA采用一种基于优先级的轮询仲裁机制。每个通道可以被设置为高优先级(High Priority)或默认优先级(Default Priority)。控制器总是优先服务所有高优先级通道中正在发出请求的通道。在高优先级通道内部或默认优先级通道内部,则采用固定的轮询顺序(通常是通道号从低到高)。
这里有一个关键细节:如果一个高优先级通道正在服务时,同一个外设又发出了一个新的请求,并且此时没有其他更高优先级的请求,那么这个新请求会在当前传输完成后立即被处理。但是,如果在该通道服务期间,触发了两次以上的新请求,那么第三次及以后的请求将会丢失。这提醒我们,在设计使用DMA的流式数据传输时(例如从高速ADC读取数据),必须确保数据处理(或缓冲区切换)的速度跟得上数据产生的速度,否则就会丢数据。
2.2 通道控制结构:μDMA的“任务清单”
这是μDMA最精妙的设计之一。控制器本身并不存储每个通道的详细配置(如源地址、目标地址、传输数量等),这些信息被放在系统RAM中一个称为“通道控制表”的区域里。每个通道在这个表中拥有两个“任务清单”:一个主控制结构和一个备用控制结构。
每个控制结构包含三个关键的32位寄存器:
- 源地址结束指针:指向你要传输的最后一个数据的地址(注意是“结束”指针,且包含该地址)。如果源地址不递增(比如从一个固定的外设数据寄存器读取),那么这个指针就直接指向该寄存器地址。
- 目的地址结束指针:指向数据要存放的最后一个位置的地址。同样,如果目的地址不递增(比如向一个固定的外设数据寄存器写入),这个指针就指向该寄存器。
- 通道控制字:这是整个任务的“大脑”,一个32位的寄存器,定义了传输的所有行为规则。它的每个字段都至关重要:
DSTINC/SRCINC:目的/源地址递增模式。决定每传输一个数据项后,地址指针是增加(0/1/2对应8/16/32位递增)还是保持不变(3)。DSTSIZE/SRCSIZE:目的/源数据大小。必须设置为相同值,可选8、16或32位。ARBSIZE:仲裁大小。定义了μDMA在一次“获得总线权限”的周期内,连续传输多少项数据后,会释放总线并重新参与仲裁。这就像“传菜员”一次端几盘菜。设置过小(如1)会导致频繁仲裁,增加开销;设置过大可能阻塞其他低优先级通道太久。通常,这个值会设置为与外设FIFO的触发深度相匹配。XFERSIZE:传输大小。这里填写的是传输项数减1。如果你想传输256个32位字,这里就填255。XFERMODE:传输模式。这是μDMA灵活性的核心,我们稍后详细展开。
将配置信息存储在RAM中而非固定寄存器里,带来了极大的灵活性。你可以在运行时动态修改这些“任务清单”,实现复杂的传输序列,例如“乒乓缓冲”或“散聚传输”。
2.3 四大传输模式深度剖析
XFERMODE字段定义了数据传输的“节奏”和“剧本”,理解它们是应用μDMA的关键。
2.3.1 基本模式这是最简单直接的模式。外设每产生一次请求(或软件触发一次),μDMA就传输ARBSIZE所指定数量的数据项。传输完XFERSIZE个数据项需要多次请求。这适用于那些数据产生速率不稳定、需要外部事件严格同步的场景。例如,一个ADC只有在转换完成时才产生一个请求,DMA就读取这一个数据。
2.3.2 自动请求模式这是内存到内存传输的标配模式。在这种模式下,只需要一次初始的软件请求,μDMA就会像开了“自动挡”一样,连续不断地传输,直到完成所有XFERSIZE个数据项,期间不再需要任何额外请求。它内部会自动产生后续的请求。这非常适合大块数据的拷贝,效率最高。
2.3.3 乒乓模式这是实现连续无间断数据流的经典模式。它需要同时使用主和备用两个控制结构,分别指向两个缓冲区(例如Buffer A和Buffer B)。
- 初始时,控制器使用主控制结构向Buffer A填充数据。
- 当Buffer A填满(即完成了主控制结构中
XFERSIZE指定的传输量),μDMA会自动切换到备用控制结构,开始向Buffer B填充数据。 - 同时,它会将主控制结构的模式字段
XFERMODE置为停止,并触发一个中断通知CPU:“Buffer A满了,快来处理!” - CPU在中断服务程序中处理Buffer A的数据,并重新配置主控制结构(重置源/目的指针和
XFERSIZE),为下一次接收做准备。 - 当Buffer B填满时,控制器又切换回主控制结构(此时它已被CPU重新武装好),并向Buffer A填充新数据,同时触发中断通知CPU处理Buffer B。
如此循环往复,就像打乒乓球一样在两个缓冲区之间切换,实现���数据生产(DMA填充)和消费(CPU处理)的流水线操作,完美避免了数据覆盖或丢失。
2.3.4 散聚模式这是更高级的“物流计划”模式。它允许你定义一个“任务列表”存放在内存中。主控制结构的源地址指向这个任务列表,模式设置为“内存散聚”。列表中的每一项任务描述(实际上就是一个备用控制结构的副本)会被μDMA自动加载到备用控制结构中并执行。一个任务完成后,自动加载并执行下一个,直到遇到一个非“备用散聚”模式的描述符为止。这可以用于将存储在不连续内存区域的数据,自动搬运到一个连续区域(聚集),或者反过来(分散),非常适合处理复杂的数据包或图像数据块。
3. 从零开始:μDMA的初始化与通道配置实战
理解了原理,我们进入实战环节。以Tiva™ TM4C1294NCPDT为例,配置μDMA需要遵循一个清晰的流程。请务必注意,在访问任何μDMA寄存器之前,必须确保其模块时钟已使能,并且使能后需要等待至少3个系统时钟周期。
3.1 系统级初始化
这是一次性的设置,通常在系统启动时完成。
- 使能μDMA时钟:通过设置系统控制模块中的
RCGCDMA寄存器相应位来完成。没有时钟,一切操作都无效。 - 使能μDMA控制器:向
DMACFG寄存器的MASTEREN位写1,激活整个μDMA模块。 - 设置通道控制表基地址:将你预先在内存中分配好的控制表基地址写入
DMACTLBASE寄存器。这里有一个关键陷阱:这个基地址必须在1024字节(1KB)边界上对齐!这意味着地址的低10位必须为0(例如,0x20001000是合法的,0x20001040则不行)。不对齐会导致不可预知的行为。通常,我们会在链接脚本中定义一个特殊对齐的全局数组,或者使用malloc并手动对齐。
3.2 配置一个内存到内存的传输(软件通道)
这是最基础的练习,我们使用专用的软件通道30。步骤一:配置通道属性通道属性决定了通道的“行为偏好”,通过一系列SET/CLR寄存器来配置。
// 假设我们使用通道30 #define DMA_CH30_MASK (1UL << 30) // 1. 设置通道优先级为默认(或使用DMAPRIOSET设为高优先级) HWREG(UDMA_PRIOCLR) = DMA_CH30_MASK; // 2. 选择使用主控制结构(对于简单传输,我们只用主结构) HWREG(UDMA_ALTCLR) = DMA_CH30_MASK; // 3. 允许控制器响应单次和突发请求(对于软件请求,此设置通常保持允许) HWREG(UDMA_USEBURSTCLR) = DMA_CH30_MASK; // 4. 解除该通道的请求掩蔽,允许其接收请求 HWREG(UDMA_REQMASKCLR) = DMA_CH30_MASK;步骤二:填充通道控制结构这是核心步骤。我们需要计算控制结构在表中的偏移。每个控制结构占12字节(3个32位字)。通道30的主控制结构偏移量是30 * 12 = 360字节,即0x168。但根据数据手册,其偏移是0x1E0,这是因为控制表条目之间有预留空间,务必以手册为准。
// 假设控制表基地址为 pControlTable volatile tDMAControlTable *pControlTable = ...; // 已对齐的基地址 uint32_t *pCh30Ctrl = (uint32_t*)((uint8_t*)pControlTable + 0x1E0); // 定义源和目的缓冲区 uint32_t srcBuffer[256]; uint32_t dstBuffer[256]; // 1. 设置源结束指针:指向最后一个字的地址 pCh30Ctrl[0] = (uint32_t)(&srcBuffer[255]); // 偏移 0x1E0: DMASRCENDP // 2. 设置目的结束指针:指向最后一个字的地址 pCh30Ctrl[1] = (uint32_t)(&dstBuffer[255]); // 偏移 0x1E4: DMADSTENDP // 3. 配置通道控制字 (偏移 0x1E8: DMACHCTL) // 构建控制字:需要仔细对照手册的位域 uint32_t ctrlWord = 0; ctrlWord |= (0x2 << 30); // DSTINC: 目的地址按字(32位)递增 ctrlWord |= (0x2 << 28); // DSTSIZE: 目的数据大小为32位 ctrlWord |= (0x2 << 26); // SRCINC: 源地址按字(32位)递增 ctrlWord |= (0x2 << 24); // SRCSIZE: 源数据大小为32位 ctrlWord |= (0x3 << 14); // ARBSIZE: 仲裁大小设为8(即每次仲裁传输8个数据项) ctrlWord |= (255 << 4); // XFERSIZE: 传输256项,填255 ctrlWord |= (0x2 << 0); // XFERMODE: 自动请求模式 pCh30Ctrl[2] = ctrlWord;注意:
SRCSIZE和DSTSIZE必须相等。地址增量值(SRCINC/DSTINC)必须大于等于数据大小值。例如,传输32位数据,增量可以是2(字)或3(不增),但不能是0或1(字节/半字)。
步骤三:启动传输配置完成后,启动传输只需两步:
// 1. 使能通道30 HWREG(UDMA_ENASET) = DMA_CH30_MASK; // 2. 发出软件请求,启动传输 HWREG(UDMA_SWREQ) = DMA_CH30_MASK;传输开始后,μDMA会自主完成所有256个字的拷贝。你可以通过轮询DMAENASET寄存器的位30(传输完成后硬件自动清零),或检查控制字中的XFERMODE字段是否变为0(停止)来判断传输是否完成。对于自动请求模式,完成后不会产生μDMA软件中断,因为这不是一个外设传输。
3.3 配置外设发送(以UART为例)
假设我们使用通道7将一块数据发送到UART的发送FIFO。UART的发送FIFO深度为8,触发水平设为4(即FIFO空余>=4时请求DMA)。步骤一:配置通道属性与内存传输类似,只是将通道掩码改为(1UL << 7)。步骤二:填充通道控制结构
// 通道7的主控制结构偏移为 0x070 volatile uint32_t *pCh7Ctrl = (uint32_t*)((uint8_t*)pControlTable + 0x070); uint8_t txBuffer[64]; // 要发送的64字节数据 // 1. 源结束指针:指向缓冲区最后一个字节 pCh7Ctrl[0] = (uint32_t)(&txBuffer[63]); // 源地址递增 // 2. 目的结束指针:指向UART数据寄存器(固定地址,不递增) pCh7Ctrl[1] = (uint32_t)(UART0_DR_R); // UART0数据寄存器地址 // 3. 配置控制字 ctrlWord = 0; ctrlWord |= (0x3 << 30); // DSTINC: 目的地址不递增(外设寄存器) ctrlWord |= (0x0 << 28); // DSTSIZE: 目的数据大小8位(UART发送字节) ctrlWord |= (0x0 << 26); // SRCINC: 源地址按字节递增 ctrlWord |= (0x0 << 24); // SRCSIZE: 源数据大小8位 ctrlWord |= (0x2 << 14); // ARBSIZE: 仲裁大小设为4,与FIFO触发水平匹配 ctrlWord |= (63 << 4); // XFERSIZE: 传输64项,填63 ctrlWord |= (0x1 << 0); // XFERMODE: 基本模式(由UART的FIFO空事件触发) pCh7Ctrl[2] = ctrlWord;步骤三:启动与监控
// 使能通道7 HWREG(UDMA_ENASET) = (1UL << 7); // 注意:这里不需要软件请求!传输由UART的发送FIFO空事件自动触发。此后,每当UART的发送FIFO有空位(达到触发水平),就会向μDMA控制器发出请求,μDMA随即搬运ARBSIZE(本例为4)个字节到FIFO中,直到64字节全部发送完毕。完成后,通道会自动禁用,XFERMODE变为0。你可以在UART的发送完成中断中检查此状态,或者重新使能通道以发送下一包数据。
3.4 实现乒乓模式接收数据流
这是最具实用价值的场景之一,用于连续接收数据(如ADC采样流)。我们使用通道8,配置两个64字节的缓冲区进行乒乓接收。步骤一:配置通道属性同上,使用通道8的掩码。步骤二:配置主/备用控制结构乒乓模式需要同时配置主和备用两套结构。
// 偏移量:主结构 0x080, 备用结构 0x280 volatile uint32_t *pCh8PriCtrl = (uint32_t*)((uint8_t*)pControlTable + 0x080); volatile uint32_t *pCh8AltCtrl = (uint32_t*)((uint8_t*)pControlTable + 0x280); uint8_t pingPongBufferA[64]; uint8_t pingPongBufferB[64]; // 1. 配置主控制结构(指向Buffer A) pCh8PriCtrl[0] = (uint32_t)(UART0_DR_R); // 源:UART数据寄存器(固定) pCh8PriCtrl[1] = (uint32_t)(&pingPongBufferA[63]); // 目的:Buffer A末尾 // 控制字:源不递增,目的按字节递增,仲裁大小8,传输64项,乒乓模式 uint32_t pingPongCtrlWord = 0; pingPongCtrlWord |= (0x0 << 30); // DSTINC: 目的地址按字节递增 pingPongCtrlWord |= (0x0 << 28); // DSTSIZE: 8位 pingPongCtrlWord |= (0x3 << 26); // SRCINC: 源地址不递增 pingPongCtrlWord |= (0x0 << 24); // SRCSIZE: 8位 pingPongCtrlWord |= (0x3 << 14); // ARBSIZE: 8(匹配接收FIFO触发深度) pingPongCtrlWord |= (63 << 4); // XFERSIZE: 64 pingPongCtrlWord |= (0x3 << 0); // XFERMODE: 乒乓模式 pCh8PriCtrl[2] = pingPongCtrlWord; // 2. 配置备用控制结构(指向Buffer B) pCh8AltCtrl[0] = (uint32_t)(UART0_DR_R); // 源相同 pCh8AltCtrl[1] = (uint32_t)(&pingPongBufferB[63]); // 目的:Buffer B末尾 pCh8AltCtrl[2] = pingPongCtrlWord; // 控制字相同步骤三:配置外设中断并启用通道
// 配置UART的DMA接收完成中断(具体寄存器请参考UART章节) // ... // 使能通道8 HWREG(UDMA_ENASET) = (1UL << 8);步骤四:编写中断服务程序处理数据传输启动后,μDMA会先用主结构向Buffer A填数据,填满后自动切换到备用结构向Buffer B填数据,并触发UART的DMA接收完成中断。
void UART0_DMA_Rx_Handler(void) { // 1. 清除UART中断标志 // ... // 2. 检查哪个缓冲区已满 volatile uint32_t *pPriCtrlWord = (uint32_t*)((uint8_t*)pControlTable + 0x088); // 主控制字地址 volatile uint32_t *pAltCtrlWord = (uint32_t*)((uint8_t*)pControlTable + 0x288); // 备用控制字地址 if ((*pPriCtrlWord & 0x7) == 0) { // 检查主结构XFERMODE是否为0(停止) // Buffer A 已满 processBuffer(pingPongBufferA, 64); // 处理Buffer A的数据 // 重新武装主控制结构,以便下次使用 pCh8PriCtrl[1] = (uint32_t)(&pingPongBufferA[63]); // 重置目的指针(如果缓冲区复用) pCh8PriCtrl[2] = pingPongCtrlWord; // 重新写入控制字,重启传输 } if ((*pAltCtrlWord & 0x7) == 0) { // 检查备用结构XFERMODE是否为0(停止) // Buffer B 已满 processBuffer(pingPongBufferB, 64); // 处理Buffer B的数据 // 重新武装备用控制结构 pCh8AltCtrl[1] = (uint32_t)(&pingPongBufferB[63]); pCh8AltCtrl[2] = pingPongCtrlWord; } }通过这个机制,数据处理(processBuffer)和DMA数据填充可以并行进行,实现了高效、无丢失的连续数据流处理。
4. 高级配置与调试:通道重映射与错误处理
4.1 通道重映射
Tiva™的μDMA允许通过DMACHMAPn寄存器重映射外设到不同的通道。例如,默认UART1 RX可能在通道22,但你想把它改到通道8。
// 假设我们想将UART1 RX映射到通道8 // 查阅手册,UART1 RX的选择码可能是0x01 HWREG(UDMA_CHMAP1) = (HWREG(UDMA_CHMAP1) & ~0x000000F0) | (0x01 << 4); // DMACHMAP1寄存器的[7:4]位对应通道8的映射选择重要提示:一个外设不能同时映射到两个使能的通道上。如果UART1 RX同时被映射到通道8和通道22,并且两个通道都使能了,那么只有优先级更高的那个通道会实际响应请求。你需要禁用或降低另一个通道的优先级。
4.2 错误检测与处理
μDMA控制器在遇到总线错误或内存保护错误时会自动停止相关通道,并在DMAERRCLR寄存器中置位ERRCLR标志,同时产生一个μDMA错误中断(中断向量45)。 一个健壮的系统应该使能这个错误中断,并在其服务程序中进行处理:
void UDMA_Error_Handler(void) { // 1. 读取错误状态寄存器,确认错误发生 if (HWREG(UDMA_ERRCLR) & 0x1) { // 2. (可选)读取其他系统状态寄存器,定位错误地址和原因 // 3. 清除错误标志,否则中断会持续触发 HWREG(UDMA_ERRCLR) = 0x1; // 4. 进行错误恢复处理,例如重置相关DMA通道,记录错误日志,系统安全恢复等 // 例如,禁用所有DMA通道并重新初始化 // HWREG(UDMA_ENACLR) = 0xFFFFFFFF; // ... 重新初始化流程 } }常见的错误原因包括:访问了无效的内存地址(如未初始化的指针)、试图向只读内存区域写入数据、或者在内存保护单元(MPU)启用的系统中,以错误的权限等级(非特权模式访问特权区域)进行访问。在调试阶段,使能错误中断能帮你快速定位这类隐蔽的硬件配置或软件指针错误。
5. 工程实践中的关键技巧与避坑指南
基于多年的项目经验,这里分享一些在Tiva™平台上使用μDMA时总结出的“干货”和容易踩的“坑”。
5.1 控制表对齐与内存分配
这是新手最容易出错的地方。DMACTLBASE寄存器要求基地址1024字节对齐。在C代码中,你可以使用编译器特性来保证:
// 方法1:使用链接脚本定义绝对地址对齐的段 // 在链接脚本(.ld文件)中定义: // .udma_control_table 0x20000000 (NOLOAD) : ALIGN(1024) { ... } // 方法2:使用C语言属性(GCC/ARMCC等编译器支持) __attribute__((aligned(1024))) static uint8_t s_ui8ControlTable[1024 * 2]; // 分配足够空间 // 或者使用标准库(C11以上) #include <stdalign.h> alignas(1024) static uint8_t s_ui8ControlTable[2048];避坑:不要使用普通的数组或malloc而不检查对齐,否则系统会以静默方式失败,调试起来极其困难。
5.2 传输完成判断的“双保险”
依赖单一方法判断DMA传输完成有时不可靠。
- 轮询法:检查
DMAENASET寄存器中对应通道的使能位。传输完成后硬件会自动清除该位。这是最直接的方法。 - 控制字检查法:读取通道控制结构中的
DMACHCTL寄存器(XFERMODE字段)。完成后会变为0(停止)。 - 中断法:对于外设传输,使能外设的DMA完成中断;对于软件通道的自动请求模式,它不会产生μDMA中断,但你可以使用软件通道完成中断(中断向量44)。
最佳实践:在关键数据传输中,采用“中断为主,状态查询为辅”的策略。在中断服务程序中,除了处理业务数据,还应读取DMAENASET或控制字进行二次确认,确保传输确实已完成,避免因中断嵌套或意外清除导致的状态误判。
5.3 动态缓冲区管理与指针计算
在乒乓或循环缓冲模式下,需要动态更新控制结构中的源/目的结束指针。计算指针时务必小心:
// 正确做法:计算最后一个元素的地址 uint32_t *pBufferEnd = pBufferStart + (bufferSizeInWords - 1); pChCtrl[1] = (uint32_t)pBufferEnd; // DMADSTENDP // 错误做法:传入缓冲区起始地址或超出末尾的地址 // pChCtrl[1] = (uint32_t)pBufferStart; // 这将导致只传输一个数据! // pChCtrl[1] = (uint32_t)(pBufferStart + bufferSizeInWords); // 这将导致缓冲区溢出!牢记:DMASRCENDP和DMADSTENDP是结束指针,指向传输范围的最后一个有效地址。
5.4 仲裁大小与外设FIFO的匹配艺术
ARBSIZE的设置对性能有显著影响。它应该与外设FIFO的触发深度紧密匹配。
- 设置过小:如果FIFO深度为8,触发水平为4,而
ARBSIZE设为1。那么每次FIFO空出4个位置,DMA只会搬1个数据,需要触发4次请求才能填满FIFO的空余,增加了总线仲裁和请求响应的开销。 - 设置过大:如果
ARBSIZE设为16,但FIFO深度只有8。DMA一次尝试搬16个数据,但外设FIFO可能很快就满了,导致部分传输被阻塞,实际效率并未提升,反而可能因为单次占用总线时间过长影响其他低优先级通道。 - 黄金法则:将
ARBSIZE设置为等于或略小于外设FIFO的触发深度。例如,对于触发水平为4的发送FIFO,ARBSIZE设为4是最优的。对于接��FIFO,如果触发水平是“非空时触发”,则ARBSIZE应设为1;如果是“达到某个水平时触发”,则设为该水平值。
5.5 多通道协同与优先级死锁预防
当系统中有多个DMA通道同时活跃时(例如,ADC采集、UART发送、内存拷贝同时进行),需要合理规划优先级。
- 高优先级通道:分配给对实时性要求最高、数据流不能中断的外设,如高速ADC或DAC。
- 默认优先级通道:分配给吞吐量大但对微小延迟不敏感的任务,如大块内存拷贝或SD卡读写。
- 预防死锁:避免设计这样的场景:一个高优先级通道等待的某个资源(例如一块内存总线),正被一个低优先级通道长时间占用(由于
ARBSIZE设置过大)。虽然μDMA的仲裁机制本身不易死锁,但在与CPU或其他总线主设备共享资源时,不合理的配置可能导致性能瓶颈。必要时,可以使用DMAUSEBURSTSET寄存器强制某个通道只使用突发传输,以控制其单次占用总线的时间。
5.6 调试技巧:利用IDE的Memory Viewer
在调试复杂的DMA传输,特别是控制结构配置错误时,肉眼检查代码中的位运算很容易出错。一个非常有效的方法是:在初始化完控制表后,设置一个断点,然后使用IDE(如Keil MDK、IAR Embedded Workbench或CCS)的Memory Viewer功能,直接查看你分配的pControlTable地址开始的内存区域。对照数据手册中控制结构的偏移量和字段定义,逐一核对DMASRCENDP、DMADSTENDP和DMACHCTL的值是否正确。这比单步跟踪代码要直观和高效得多。