1. 项目概述与μDMA核心价值
在嵌入式系统开发中,尤其是面对Tiva™这类基于ARM Cortex-M内核的微控制器,我们常常需要处理高速、连续的数据流,比如从ADC采集传感器数据、通过UART收发大量信息,或是将摄像头捕捉的图像数据搬运到显示缓冲区。如果这些数据搬移工作全部交由CPU通过软件循环来完成,CPU的绝大部分时间都将被“搬运工”这类低效任务占据,无法及时响应更重要的计算、逻辑判断或系统调度事件,整个系统的实时性和吞吐量会大打折扣。
这时,直接存储器访问(DMA)技术就成了我们的“救星”。它的核心思想非常直观:在存储器和外设之间开辟一条“数据高速公路”,并设置一个“交通指挥中心”(DMA控制器)。当外设准备好数据或需要数据时,它向DMA控制器发出一个“运输请求”(DMA请求),DMA控制器便直接接管总线,在源地址和目的地址之间进行数据搬运,整个过程完全不需要CPU介入。CPU只需要在初始化时告诉DMA控制器“从哪里搬、搬到哪里、搬多少”,就可以去处理其他任务,等DMA搬运完成后再通过中断等方式通知CPU即可。
TI Tiva™系列微控制器集成的微型DMA(μDMA)模块,更是将这种高效性推向了新的高度。它不仅支持基本的存储器到存储器、存储器到外设等传输,更提供了诸如乒乓(Ping-Pong)、散聚(Scatter-Gather)等高级传输模式。今天我们要深入探讨的,就是散聚模式中较为特殊的一种——外设散聚模式。简单来说,常规的存储器散聚模式是从一个外设读取数据,然后分散写入到内存的多个不连续区域。而外设散聚模式则相反,它是将内存中多个不连续区域的数据,汇聚起来连续地写入到一个外设中。这种模式对于需要将多个数据缓冲区的内容顺序发送出去的应用(如网络数据包组装、复杂波形合成输出)极为有用。
要玩转μDMA,特别是这些高级模式,仅仅知道概念是远远不够的,我们必须深入到寄存器配置的层面。芯片手册上那些密密麻麻的寄存器位域描述,就是指挥这台“数据搬运机器人”的指令集。理解并正确配置DMASTAT、DMACFG、DMACTLBASE、DMAALTSET等关键寄存器,是确保μDMA按照我们预期高效、可靠工作的基石。接下来,我将结合多年的实际项目经验,为你拆解外设散聚模式的运作机制,并逐一详解相关寄存器的配置要点与避坑指南。
2. μDMA外设散聚模式深度解析
2.1 散聚模式的基本概念与工作流程
在深入外设散聚之前,我们必须先建立对μDMA散聚模式整体的认知。散聚模式的核心思想是通过一个“任务列表”(即控制结构体链表)来描述一段复杂的、非连续的数据传输任务。μDMA控制器会按照这个列表,自动地、顺序地执行多个传输子任务。
整个流程依赖于两个核心数据结构:主控制结构体(Primary Control Structure)和副控制结构体(Alternate Control Structure)。你可以把它们想象成两份任务清单。
- 初始化阶段:程序员在系统内存中预先定义好一个或多个副控制结构体。每个副结构体描述了一个独立的传输子任务,包括源地址、目的地址、传输数据量(
XFERSIZE)等。然后,设置主控制结构体,其关键字段指向第一个副控制结构体的地址,并设置总传输模式为散聚模式。 - 触发与执行:当外设发出DMA请求(或软件触发)时,μDMA控制器首先读取主控制结构体,得知当前处于散聚模式,并找到第一个副控制结构体。
- 链表遍历:控制器读取第一个副结构体,按照其配置执行一次数据传输。这次传输的数据量由该副结构体的
XFERSIZE决定。 - 任务切换:第一个子任务完成后,控制器会检查当前副结构体中的一个特殊标志位(通常是某个保留位或模式位,用于指示链表结束)。如果未结束,控制器会自动加载下一个副控制结构体(其地址通常由当前副结构体中的
NEXT指针字段指定),并开始执行下一个子任务。 - 循环与完成:此过程持续进行,直到遇到标识链表结束的副控制结构体。此时,一次完整的散聚传输完成,DMA通道可能自动禁用或产生完成中断。
关键点:在散聚模式下,一次DMA请求会触发整个链表的执行,直到所有子任务完成。这非常适合处理那些数据源或目的地分散在内存多处,但逻辑上属于一个完整事务的场景。
2.2 外设散聚模式的特殊之处与配置要点
理解了通用散聚模式,外设散聚模式就很好理解了。根据输入资料,外设散聚模式与存储器散聚模式“工作流程基本相同”。它们的核心区别在于每次传输所依据的数据量定义不同。
- 存储器散聚模式:每次传输的数据量由当前正在执行的副控制结构体中的
XFERSIZE位域定义。XFERSIZE定义了该次“子传输”要搬多少数据单元。 - 外设散聚模式:每次传输的数据量不由
XFERSIZE决定,而是每次触发时,按照ARBSIZE位域定义的数目执行。ARBSIZE我们通常称之为“仲裁大小”或“突发大小”,它定义了DMA控制器在一次总线占用期内,连续传输的数据单元数量。
这个区别带来了一个非常重要的配置差异和潜在陷阱:
配置要点一:主/副控制结构体的模式位输入资料明确指出:“当μDMA控制器工作于外设散聚模式时,必须将主控制结构体的工作模式配置为此种模式。” 同时,“当μDMA控制器工作于外设散聚模式时,必须将副控制结构体的工作模式配置为此种模式。” 这意味着,在初始化控制结构体时,你需要同时设置主结构体和每一个副结构体中的模式选择位,明确指定为“外设散聚模式”。芯片的硬件逻辑会检查这个配置,如果模式不匹配,可能导致无法预料的行为或传输错误。
配置要点二:XFERSIZE与ARBSIZE的角色重定义在外设散聚模式下,XFERSIZE在副控制结构体中依然存在,但它不再直接决定单次传输量。它的作用可能转变为指示“该子任务总共需要被触发多少次”。例如,假设一个副任务需要传输1024字节,ARBSIZE设置为8(即每次突发传输8字节)。那么,完成这个子任务需要外设触发1024 / 8 = 128次DMA请求。XFERSIZE可能用于内部递减计数器,直到为0时该子任务才算完成,然后切换到下一个副结构体。 而ARBSIZE则成为了每次请求实际搬运量的控制者。这要求软件工程师必须根据外设的特性(比如UART的FIFO深度、SPI的数据寄存器宽度)来合理设置ARBSIZE,以匹配外设的数据吞吐节奏,实现最高效的传输。
配置要点三:触发方式的考量外设散聚模式的传输是由外设请求驱动的。因此,你需要确保外设已正确配置为在需要数据时(例如发送缓冲区空)能产生DMA请求。同时,由于传输量由ARBSIZE和请求次数决定,你必须确保外设产生的请求次数足以完成所有副结构体定义的XFERSIZE总量,否则传输会挂起。
实操心得:在实际调试外设散聚模式时,最容易出现的问题就是“数据传了一部分就停了”。这时候,第一要检查外设的DMA请求是否持续产生;第二要核对
ARBSIZE的设置是否与外设特性匹配(例如,对于字节宽度的UART,ARBSIZE通常设为1);第三,要确认所有副控制结构体的模式位都已正确设置为外设散聚模式。我曾经在一个使用DAC输出波形的项目中,因为一个副结构体的模式位配置错误,导致波形输出到一半发生错乱,排查了很久才发现是这个基础配置问题。
3. 关键寄存器详解与配置实战
了解了模式原理,我们来看看如何通过寄存器来指挥μDMA。以下配置均基于Tiva TM4C123GH6ZRB微控制器,其μDMA模块基地址为0x400FF000。我们将以配置一个UART发送通道使用外设散聚模式为例,串联起关键寄存器的操作。
3.1 全局控制与状态寄存器
3.1.1 DMA配置寄存器 (DMACFG) - 偏移量 0x004
这是一个只写寄存器,只有一个有效位:MASTEN(位0)。
- 功能:μDMA控制器的主使能开关。必须将此位置1,整个μDMA模块才能工作。在配置任何通道之前,应先启用控制器。
- 操作:
// 假设已定义宏 DMA_BASE 为 0x400FF000 HWREG(DMA_BASE + DMA_O_CFG) = 0x00000001; // 置位MASTEN,使能控制器注意:在系统初始化早期,可能需要在使能μDMA前确保系统时钟和总线时钟已稳定。有些项目会在外设初始化完成后再统一使能DMA。
3.1.2 DMA状态寄存器 (DMASTAT) - 偏移量 0x000
这是一个只读寄存器,用于查看控制器状态。
DMACHANS(位20:16):只读。显示芯片支持的DMA通道数量减1。对于TM4C123GH6ZRB,复位值是0x1F,表示有32个通道(0-31)。STATE(位7:4):只读。反映DMA控制器的状态机状态。在调试时非常有用,可以判断控制器是空闲(0x0)、正在读写数据(0x4,0x5),还是等待请求(0x6)等。例如,如果通道使能后一直处于STATE=0x6(等待请求),说明外设没有产生请求或请求被屏蔽。MASTEN(位0):只读。反映DMACFG中主使能位的状态。
3.2 通道控制表与指针寄存器
这是配置散聚模式的核心,涉及到内存中数据结构的布局。
3.2.1 DMA通道控制基指针寄存器 (DMACTLBASE) - 偏移量 0x008
- 功能:设置通道控制表(Channel Control Table)在系统内存中的基地址。这个表包含了所有32个通道的主控制结构体。
- 关键要求:基地址必须1024字节对齐(即地址的低10位为0)。通常我们会在链接脚本中定义一个特殊的段(例如
.dma_table),并将其对齐到1024字节边界,然后将这个段的起始地址赋值给DMACTLBASE。 - 操作:
// 假设 control_table 是一个在1024字节对齐地址的数组 extern uint32_t control_table[256]; // 大小至少为 32通道 * 8字/通道 = 256字 // 设置基地址指针,右移10位是因为寄存器[31:10]存储地址[31:10] HWREG(DMA_BASE + DMA_O_CTLBASE) = (uint32_t)control_table;
3.2.2 DMA副通道控制基指针寄存器 (DMAALTBASE) - 偏移量 0x00C
- 功能:这是一个只读寄存器,返回副控制结构体区域的基地址。这个地址是硬件根据
DMACTLBASE自动计算出来的(通常是DMACTLBASE + 0x200)。软件可以直接读取此寄存器来获取副结构体的基地址,无需手动计算,方便编程。 - 操作:
uint32_t alt_base = HWREG(DMA_BASE + DMA_O_ALTBASE); // alt_base 现在指向副控制结构体表的起始地址
控制结构体内存布局详解: 每个通道的主控制结构体占用8个字(32字节)。32个通道的主结构体连续存放,构成了“主控制表”。紧接着主控制表之后,就是“副控制表”,每个通道的副结构体也占用8个字。因此,整个控制表的大小至少是32通道 * 8字/通道 * 2(主副) = 512字 = 2048字节。DMAALTBASE正好指向这2048字节区域的后半部分(即副表)的起始点。
3.3 通道属性配置寄存器组
这些寄存器以位图形式管理32个通道的各类属性。
3.3.1 DMA通道启用置位/清除寄存器 (DMAENASET / DMAENACLR) - 偏移量 0x028 / 0x02C
- 功能:使能或禁用特定通道。通道必须在使能状态下才能响应请求。
- 操作:
#define UART_TX_CHANNEL 4 // 假设UART0 TX使用通道4 // 使能通道4 HWREG(DMA_BASE + DMA_O_ENASET) = (1 << UART_TX_CHANNEL); // 禁用通道4 (通常传输完成后硬件自动禁用,也可软件禁用) HWREG(DMA_BASE + DMA_O_ENACLR) = (1 << UART_TX_CHANNEL);注意:
DMAENASET是可读写的,你可以读取它来查询哪些通道已使能。而DMAENACLR是只写的,写1清除对应位。
3.3.2 DMA通道请求屏蔽置位/清除寄存器 (DMAREQMASKSET / DMAREQMASKCLR) - 偏移量 0x020 / 0x024
- 功能:屏蔽或允许外设向特定通道发出硬件DMA请求。
- 应用场景:
- 当你想进行软件触发的DMA传输时,需要先屏蔽该通道的硬件请求(
SET对应位),然后使能通道,再通过软件请求寄存器DMASWREQ来启动传输。 - 在配置通道期间,为避免意外触发,可以先屏蔽请求,配置完成后再打开。
- 当你想进行软件触发的DMA传输时,需要先屏蔽该通道的硬件请求(
- 操作:
// 屏蔽通道4的硬件请求(例如,在初始化阶段) HWREG(DMA_BASE + DMA_O_REQMASKSET) = (1 << UART_TX_CHANNEL); // ... 配置控制结构体 ... // 配置完成后,允许硬件请求 HWREG(DMA_BASE + DMA_O_REQMASKCLR) = (1 << UART_TX_CHANNEL);
3.3.3 DMA通道主副置位/清除寄存器 (DMAALTSET / DMAALTCLR) - 偏移量 0x030 / 0x034
- 功能:强制指定某个通道使用其主控制结构体还是副控制结构体。
- 与外设散聚模式的关系:资料中特别注明:“对于乒乓模式和散聚模式,μDMA控制器会自动将相应位置位,选择副控制结构体。” 这意味着,当你将某个通道的主控制结构体中的模式配置为散聚模式并启动传输后,硬件会自动将该通道对应的
DMAALTSET位置1,指示当前正在使用副控制结构体链表。通常我们不需要手动操作这两个寄存器,除非有特殊需求要强制切换。 - 操作:读取
DMAALTSET可以判断通道当前使用的是主还是副结构体,这在调试时很有用。
3.3.4 DMA通道采用猝发置位/清除寄存器 (DMAUSEBURSTSET / DMAUSEBURSTCLR) - 偏移量 0x018 / 0x01C
- 功能:配置通道只响应猝发(Burst)请求,还是同时响应单次(Single)和猝发请求。
- 关键理解:
ARBSIZE定义了猝发传输的大小。如果使能了“只响应猝发”(SET对应位置1),则外设必须产生猝发请求,DMA才会以ARBSIZE为单位进行传输。如果剩余数据量小于ARBSIZE,硬件会自动清除该位,并以单次请求方式传输剩余数据。对于外设散聚模式,由于传输量由ARBSIZE和请求次数决定,通常需要外设支持并产生猝发请求。 - 操作:
// 设置通道4只使用猝发请求 HWREG(DMA_BASE + DMA_O_USEBURSTSET) = (1 << UART_TX_CHANNEL);
3.3.5 DMA通道软件请求寄存器 (DMASWREQ) - 偏移量 0x014
- 功能:向指定通道发出软件DMA请求。写1到对应位产生一次请求,请求完成后硬件自动清零该位。
- 应用:可用于测试DMA通道功能,或在屏蔽硬件请求后手动启动传输。
// 向通道4发起一次软件请求 HWREG(DMA_BASE + DMA_O_SWREQ) = (1 << UART_TX_CHANNEL);
3.4 外设散聚模式配置实战代码示例
假设我们需要使用UART0的发送通道(假设映射到μDMA通道4),以外设散聚模式发送三段分散在内存中的数据。
步骤1:内存中定义控制结构体
#include <stdint.h> #include <stdbool.h> #include "inc/hw_memmap.h" #include "inc/hw_types.h" #include "driverlib/sysctl.h" #include "driverlib/udma.h" // 1. 确保控制表1024字节对齐。通常使用编译器属性或链接脚本。 // 这里使用GCC/ARMCC风格的属性示例 __attribute__((aligned(1024))) static uint32_t sDMAControlTable[256]; // 主控制表 (32通道 * 8字) // 2. 定义我们的数据缓冲区 uint8_t gDataBuffer1[128]; uint8_t gDataBuffer2[256]; uint8_t gDataBuffer3[64]; // 3. 定义副控制结构体 (需要3个) // 每个副结构体是8个uint32_t的数组 uint32_t sAltControlStruct1[8]; uint32_t sAltControlStruct2[8]; uint32_t sAltControlStruct3[8];步骤2:初始化μDMA控制器与通道控制表
void InitMicroDMA(void) { // 使能μDMA外设时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_UDMA); // 等待外设就绪(非必须,但建议) while(!SysCtlPeripheralReady(SYSCTL_PERIPH_UDMA)) {} // 启用μDMA控制器主开关 HWREG(UDMA_BASE + UDMA_O_CFG) = UDMA_CFG_MASTEN; // 设置通道控制表基地址 HWREG(UDMA_BASE + UDMA_O_CTLBASE) = (uint32_t)sDMAControlTable; // 获取副控制表基地址(可选,用于验证) uint32_t altBase = HWREG(UDMA_BASE + UDMA_O_ALTBASE); // altBase 应等于 ((uint32_t)sDMAControlTable + 0x200) }步骤3:配置外设散聚模式的副控制结构体这是最关键的一步。我们需要按照μDMA手册中控制结构体的格式来填充数组。每个结构体包含8个字(字0-字7),定义传输参数。
void SetupScatterGatherAltStructures(void) { // 假设我们使用通道4,其副结构体在表中的偏移量是固定的。 // 通道N的副结构体地址 = DMAALTBASE + (N * 32字节) // 但我们直接操作我们定义的数组更清晰。 // 配置副控制结构体1 // 字0: 源地址 - 数据缓冲区1的地址 sAltControlStruct1[0] = (uint32_t)gDataBuffer1; // 字1: 目的地址 - UART0数据寄存器地址 (假设为0x4000C000) sAltControlStruct1[1] = 0x4000C000; // 字2: 控制字 // 包含:XFERSIZE, ARBSIZE, 源/目的增量,数据宽度,模式等 // 假设:传输128字节,ARBSIZE=8,源地址递增,目的地址不变(外设),数据宽度8位,模式=外设散聚模式 uint32_t controlWord1 = 0; controlWord1 |= (127 << 4); // XFERSIZE = 128-1 (注意是传输次数-1) controlWord1 |= (7 << 8); // ARBSIZE = 8-1 (突发大小-1) controlWord1 |= (0x1 << 14); // 源地址递增 (每次+1) controlWord1 |= (0x0 << 15); // 目的地址不变 controlWord1 |= (0x0 << 18); // 源数据大小 8位 controlWord1 |= (0x0 << 21); // 目的数据大小 8位 controlWord1 |= (0x3 << 26); // 模式:0x3 通常代表外设散聚模式 (请查具体手册确认!) sAltControlStruct1[2] = controlWord1; // 字3: 保留(通常为0) sAltControlStruct1[3] = 0; // 字4: 指向下一个副结构体的指针 (结构体2) sAltControlStruct1[4] = (uint32_t)&sAltControlStruct2[0]; // 字5-7: 保留 sAltControlStruct1[5] = 0; sAltControlStruct1[6] = 0; sAltControlStruct1[7] = 0; // 配置副控制结构体2 (类似,指向缓冲区2,下一个指向结构体3) sAltControlStruct2[0] = (uint32_t)gDataBuffer2; sAltControlStruct2[1] = 0x4000C000; // UART0 DR uint32_t controlWord2 = 0; controlWord2 |= (255 << 4); // XFERSIZE = 256-1 controlWord2 |= (7 << 8); // ARBSIZE = 8-1 controlWord2 |= (0x1 << 14); // 源地址递增 controlWord2 |= (0x0 << 15); // 目的地址不变 controlWord2 |= (0x0 << 18); // 源大小 8位 controlWord2 |= (0x0 << 21); // 目的大小 8位 controlWord2 |= (0x3 << 26); // 模式:外设散聚 sAltControlStruct2[2] = controlWord2; sAltControlStruct2[3] = 0; sAltControlStruct2[4] = (uint32_t)&sAltControlStruct3[0]; // 指向结构体3 sAltControlStruct2[5] = 0; sAltControlStruct2[6] = 0; sAltControlStruct2[7] = 0; // 配置副控制结构体3 (最后一个) sAltControlStruct3[0] = (uint32_t)gDataBuffer3; sAltControlStruct3[1] = 0x4000C000; // UART0 DR uint32_t controlWord3 = 0; controlWord3 |= (63 << 4); // XFERSIZE = 64-1 controlWord3 |= (7 << 8); // ARBSIZE = 8-1 controlWord3 |= (0x1 << 14); // 源地址递增 controlWord3 |= (0x0 << 15); // 目的地址不变 controlWord3 |= (0x0 << 18); // 源大小 8位 controlWord3 |= (0x0 << 21); // 目的大小 8位 controlWord3 |= (0x3 << 26); // 模式:外设散聚 // 关键:最后一个结构体的“下一个指针”需要设置为一个特殊值(如0xFFFFFFFF)或模式位中的结束标志,以告知DMA链表结束。 // 具体值需查阅芯片手册。这里假设将下一个指针设为0表示结束。 sAltControlStruct3[2] = controlWord3; sAltControlStruct3[3] = 0; sAltControlStruct3[4] = 0; // 链表结束 sAltControlStruct3[5] = 0; sAltControlStruct3[6] = 0; sAltControlStruct3[7] = 0; }步骤4:配置通道的主控制结构体并启动
void SetupAndStartDMATransfer(void) { // 1. 获取通道4的主控制结构体指针 // 通道N的主结构体地址 = DMACTLBASE + (N * 32字节) uint32_t *pPrimaryCtrl = (uint32_t*)((uint32_t)sDMAControlTable + (4 * 32)); // 2. 填充主控制结构体 // 字0: 源地址 - 对于散聚模式,通常忽略或设为0(因为源在副结构体中定义) pPrimaryCtrl[0] = 0; // 字1: 目的地址 - 对于外设散聚模式,通常也忽略或设为外设地址(副结构体会覆盖) pPrimaryCtrl[1] = 0x4000C000; // 字2: 控制字 - 这是关键! uint32_t primaryControlWord = 0; primaryControlWord |= (0x3 << 26); // 模式:外设散聚模式 (必须与手册一致) // 其他位如XFERSIZE, ARBSIZE在主结构体中可能被忽略,但安全起见可设 primaryControlWord |= (0x1 << 14); // 源递增(虽然源在副结构体) primaryControlWord |= (0x0 << 15); // 目的不变 pPrimaryCtrl[2] = primaryControlWord; // 字3: 保留 pPrimaryCtrl[3] = 0; // 字4: 指向第一个副控制结构体的指针 pPrimaryCtrl[4] = (uint32_t)&sAltControlStruct1[0]; // 字5-7: 保留 pPrimaryCtrl[5] = 0; pPrimaryCtrl[6] = 0; pPrimaryCtrl[7] = 0; // 3. 配置通道属性(使用TI DriverLib库函数示例,更安全便捷) uDMAChannelControlSet(UDMA_CH4_UART0TX | UDMA_PRI_SELECT, UDMA_SIZE_8 | UDMA_SRC_INC_8 | UDMA_DST_INC_NONE | UDMA_ARB_8); // 设置通道传输模式为外设散聚模式(需要根据DriverLib具体API) // uDMAChannelScatterGatherSet(...); // 假设有此函数 // 4. 使能通道(在屏蔽硬件请求后) HWREG(UDMA_BASE + UDMA_O_REQMASKSET) = (1 << 4); // 先屏蔽请求 HWREG(UDMA_BASE + UDMA_O_ENASET) = (1 << 4); // 使能通道 // 5. 允许硬件请求,传输开始(当UART发送缓冲区空时触发) HWREG(UDMA_BASE + UDMA_O_REQMASKCLR) = (1 << 4); }4. 调试技巧与常见问题排查
配置μDMA,尤其是散聚模式,容易出错。以下是一些实战中总结的排查思路和技巧。
4.1 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| DMA传输完全没启动 | 1. μDMA控制器未使能 (DMACFG.MASTEN)。2. 通道未使能 ( DMAENASET)。3. 外设的DMA请求未使能或未产生。 4. 通道请求被屏蔽 ( DMAREQMASKSET)。 | 1. 检查DMASTAT.MASTEN位。2. 读取 DMAENASET确认通道位已置1。3. 检查外设寄存器(如UART的DMACTL)。 4. 读取 DMAREQMASKSET确认对应位为0。 |
| 传输中途停止,数据不完整 | 1. 控制结构体链表未正确终止(最后一个副结构体的NEXT指针错误)。2. XFERSIZE或ARBSIZE计算错误。3. 外设请求中断(如FIFO满)。 4. 总线错误(访问非法地址)。 | 1. 检查最后一个副结构体的NEXT指针是否为结束标志。2. 复核 XFERSIZE(传输次数-1)和ARBSIZE(突发大小-1)。3. 检查外设状态寄存器。 4. 检查 DMAERRCLR寄存器是否有错误标志。 |
| 数据传输地址错乱 | 1. 源/目的地址未正确赋值。 2. 源/目的地址增量模式配置错误。 3. 数据宽度(8/16/32位)配置不匹配。 | 1. 在调试器中查看控制结构体中的地址值。 2. 确认 SRCINC和DSTINC位设置是否符合预期。3. 确认 SRCSIZE和DSTSIZE与外设数据寄存器宽度一致。 |
| 只能传输一次,不能循环 | 1. 模式未配置为自动重载或乒乓模式。 2. 对于基本模式,传输完成后通道自动禁用。 | 1. 检查主控制结构体中的模式位,散聚模式本身不支持自动循环整个链表,需要软件重新配置或使用乒乓模式实现类似效果。 2. 如需连续传输,需在传输完成中断中重新使能通道或配置链表。 |
| 使用库函数配置后不工作 | 库函数可能未正确设置所有位,或与自定义控制结构体冲突。 | 1. 尝试使用纯寄存器操作验证基本功能。 2. 仔细阅读库函数源码,看它如何填充控制结构体。 3. 确保没有混合使用库函数和直接操作控制表导致配置被覆盖。 |
4.2 高级调试技巧
利用
DMASTAT寄存器:当传输卡住时,读取DMASTAT.STATE字段。如果状态一直停留在0x6(等待请求),说明DMA控制器在等外设信号,重点查外设和请求屏蔽。如果状态在0x4或0x5(读写数据)之间循环但数据没动,可能是地址或控制字配置错误。内存查看器(Memory Viewer)是你的好朋友:在IDE的调试模式下,直接查看你定义的
sDMAControlTable和sAltControlStructX数组的内存内容。对照芯片手册中控制结构体的格式,逐个字段检查是否正确。这是定位配置错误最直接的方法。软件请求测试法:在复杂的外设散聚配置之前,先配置一个简单的“存储器到存储器”基本模式传输,并使用
DMASWREQ寄存器进行软件触发。如果基本传输能成功,说明DMA控制器全局配置和通道基础功能是好的,问题可能出在散聚模式特定的配置(如链表指针、模式位)或外设接口上。分步验证法:不要一次性配置完整的散聚链表。先只配置一个副结构体,测试单次“外设散聚”传输是否成功。成功后再添加第二个副结构体,并检查链表指针。逐步增加复杂度,能有效隔离问题。
关注对齐与边界:确保控制表地址1024字节对齐。确保数据缓冲区的地址和长度符合
ARBSIZE和数据宽度的对齐要求(例如,32位传输时地址最好4字节对齐)。非对齐访问在某些架构上会导致性能下降或总线错误。
踩坑实录:在一次项目中使用ADC双缓冲(乒乓模式)采集数据,并通过DMA传输到内存。初期测试正常,但长时间运行后偶尔会数据错位。最终发现是计算下一个缓冲区的地址时,使用了
uint8_t*指针进行加法,结果在指针超过255时发生了意外的截断(由于代码逻辑瑕疵),导致DMA写入了错误的内存区域。教训:对DMA的地址指针进行计算时,务必使用足够宽的类型(如uint32_t),并在计算后检查地址值是否在预期范围内。