Tiva微控制器μDMA外设散聚模式:原理、配置与实战指南
1. 项目概述为什么需要外设散聚模式在嵌入式开发中尤其是面对像Tiva™ TM4C123GH6ZRB这类基于ARM Cortex-M内核的微控制器时我们常常需要处理外设与内存之间频繁且不规则的数据交换。比如一个图形LCD需要从内存中多个不连续的地址区域可能是不同的字体库、图标缓存读取数据来刷新屏幕或者一个音频DAC需要从多个音频片段缓冲区如不同的音效文件读取数据流进行混合播放。如果这些操作都让CPU通过软件循环来搬运数据CPU将被牢牢绑死在简单的数据搬运上无法处理更重要的逻辑计算系统实时性会大打折扣。这就是直接存储器访问DMA技术大显身手的地方。而TI Tiva系列微控制器内置的微型直接存储器访问μDMA控制器更是将DMA的灵活性和效率提升到了一个新的高度。它不仅仅支持简单的一块内存到另一块内存或外设的传输更提供了多种高级传输模式其中外设散聚模式就是为处理上述“多源对一目的”或“一源对多目的”的复杂场景而设计的。简单来说外设散聚模式允许你预先在内存中定义一个“任务清单”。这个清单里包含了多个独立的传输任务描述每个任务有自己的源地址、目的地址和传输数据量。当外设比如UART的发送缓冲区空、ADC转换完成发出一个DMA请求信号时μDMA控制器不是执行一次简单的传输就结束而是从这个清单中取出下一个任务配置执行一次完整的子传输。外设请求一次它就完成清单中的一项任务直到所有任务都执行完毕。这就像有一个非常专业的快递分拣员μDMA面前有一张包含多个包裹取件地址的订单任务列表。每当有货车外设到达仓库门口发出装货请求分拣员就根据订单去一个指定的地址内存地址A取一批包裹装上车。货车走了分拣员就等待下一辆货车的请求然后再去订单上的下一个地址内存地址B取货。整个过程完全自动化CPU只需要在开始时写好“订单”就可以去忙别的事情了。2. 核心机制深度解析主副控制结构体与任务列表要理解外设散聚模式必须吃透三个核心概念主控制结构体、副控制结构体和任务列表。这是μDMA实现复杂流程控制的精髓所在。2.1 控制结构体μDMA的“工作指令单”μDMA的每个通道都关联着一对控制结构体一个主控制结构体和一个副控制结构体。你可以把它们想象成两份“工作指令单”。副控制结构体这是当前正在执行的传输任务的指令单。它直接告诉μDMA控制器这次传输的源地址末尾指针SRCENDP在哪里、目的地址末尾指针DSTENDP在哪里、要传输多少数据单元XFERSIZE、数据宽度是多少SRCSIZE/DSTSIZE、地址如何变化SRCINC/DSTINC以及传输模式XFERMODE等。在简单传输模式下我们通常只配置和使用这个副控制结构体。主控制结构体这是管理任务列表的指令单。在外设散聚模式下它的角色发生了根本变化。它不再描述一次具体的传输而是描述一个“任务列表”在内存中的位置。具体来说主控制结构体中的SRCENDP指向任务列表在内存中的起始地址DSTENDP指向通道副控制结构体在μDMA控制表中的地址而XFERMODE则被设置为“外设散聚模式”。2.2 任务列表待办事项的详细清单任务列表是应用程序在系统内存如SRAM中开辟的一块连续区域里面按顺序存放着若干个“任务项”。每个任务项的数据结构恰好和副控制结构体需要的信息一致通常包含三个32位字任务源末尾指针该子任务数据源的结束地址。任务目的末尾指针该子任务目的地的结束地址在外设散聚的“汇集”操作中这个地址通常是同一个外设数据寄存器。任务控制字该子任务的传输参数如数据量、数据宽度、地址增量等。这个列表定义了完整的传输序列。例如一个包含3个任务项的列表就对应着3次独立的DMA传输操作。2.3 协同工作流程一次请求一项任务理解了上述两个概念整个外设散聚模式的工作流程就清晰了初始化应用程序配置通道的主控制结构体将其SRCENDP指向内存中创建好的任务列表DSTENDP指向本通道的副控制结构体位置并将XFERMODE设置为外设散聚模式。然后使能该DMA通道。等待请求μDMA控制器进入等待状态监听来自指定外设的请求信号可能是单次请求也可能是突发请求。任务加载当外设产生一个有效的DMA请求时μDMA控制器首先根据主控制结构体的配置执行一次“内部加载”传输。这次传输的源地址是主结构体SRCENDP指向的任务列表当前项目的地址是主结构体DSTENDP指向的副控制结构体。效果就是将任务列表中的当前任务项三个字拷贝到副控制结构体中覆盖其原有内容。注意这个加载过程本身也是一次DMA传输但它是在μDMA控制器内部完成的不占用系统总线带宽速度极快。任务执行副控制结构体被更新为新的任务配置后μDMA控制器立即基于这个新配置执行一次从内存到外设或反向的“实际”数据传输。传输的数据量、地址等完全由刚刚载入的任务项决定。迭代与结束一次“请求-加载-执行”的循环结束后主控制结构体中的地址指针由SRCINC决定会自动指向任务列表中的下一项为下一次外设请求做好准备。控制器再次回到等待请求的状态。只有当最后一个任务项被执行完毕并且对应的实际数据传输完成后μDMA通道才会产生传输完成中断并自动禁用自身如果配置为自动停止模式。这个过程完美实现了由外设节奏控制的、复杂的多批次数据传输自动化。CPU的参与度被降至最低仅需初始创建任务列表和配置主结构体并在最终完成时处理一个中断即可。3. 外设散聚模式实战配置详解理论讲透了我们来看如何动手配置。这里我们以Tiva TM4C123GH6ZRB的UART0发送数据为例假设我们需要将存储在内存中三个不同区域的字符串比如日志头、时间戳、实际消息自动依次发送出去。我们将使用UART0的发送DMA请求通常在其发送FIFO有空位时产生来触发散聚传输。3.1 硬件与软件准备首先确保你的开发环境已就绪。你需要硬件一块基于TM4C123GH6ZRB的开发板如EK-TM4C123GXL。软件TI的Code Composer Studio (CCS) 或Keil MDK以及TivaWare固件库。使用固件库可以极大简化寄存器操作。在开始前通过系统控制模块的RCGCDMA寄存器使能μDMA时钟并通过DMACFG寄存器使能μDMA控制器主设备。同时需要为DMA控制表分配一块1024字节对齐的内存空间并将其基地址写入DMACTLBASE寄存器。TivaWare库中的uDMAEnable()和uDMAChannelAttributeEnable()等函数封装了这些操作。3.2 创建任务列表这是应用程序层面的核心工作。我们需要在全局区或静态区定义我们的任务列表。#include stdint.h #include “inc/hw_memmap.h” #include “driverlib/udma.h” // 假设的三个数据源 const char g_pucHeader[] “[LOG] “; // 6字节 const char g_pucTimestamp[] “2023-10-27 14:30:00 “; // 20字节 const char g_pucMessage[] “Sensor reading: 25.5C\r\n”; // 24字节 // 定义任务项的结构体必须严格对齐 typedef struct { void *pvSrcEndAddr; // 源地址末尾指针 void *pvDstEndAddr; // 目的地址末尾指针 uint32_t ui32Control; // 控制字 } tDMATaskItem; // 分配任务列表假设我们使用通道0 // 控制表基地址假设为0x20004000通道0的副控制结构体在偏移0x000处。 // 但实际上任务列表可以放在任何可用的RAM中。 __attribute__((aligned(4))) // 确保4字节对齐虽然不是严格必须但是个好习惯 tDMATaskItem g_sPeripheralScatterTaskList[3]; // 外设目的地址UART0数据寄存器 #define UART0_DATA_BASE (UART0_BASE 0x0000)接下来初始化这个列表。关键点在于指针的计算pvSrcEndAddr必须指向最后一次传输的源地址。对于递增的源地址计算公式是源起始地址 (传输数据量 - 1) * 数据宽度对应的字节数。void InitScatterTaskList(void) { // 任务项1传输头部字符串 g_sPeripheralScatterTaskList[0].pvSrcEndAddr (void *)(g_pucHeader sizeof(g_pucHeader) - 1); // 末尾地址 g_sPeripheralScatterTaskList[0].pvDstEndAddr (void *)UART0_DATA_BASE; // 目的地址固定为UART0数据寄存器 // 构建控制字传输6个8位数据源地址字节递增目的地址不递增使用基本模式由外设请求控制 // ARBSIZE仲裁大小设置为1即每传输1个数据单元就释放总线并重新仲裁。 // 对于UART这种低速外设ARBSIZE1是合适的避免长时间占用总线。 g_sPeripheralScatterTaskList[0].ui32Control UDMA_CHCTL_DSTINC_NONE | // 目的不递增 UDMA_CHCTL_DSTSIZE_8 | // 目的数据宽度8位 UDMA_CHCTL_SRCINC_8 | // 源地址按8位字节递增 UDMA_CHCTL_SRCSIZE_8 | // 源数据宽度8位 UDMA_CHCTL_ARBSIZE_1 | // 仲裁大小1 (5 4) | // XFERSIZE 6-1 5 UDMA_CHCTL_XFERMODE_BASIC; // 基本传输模式 // 任务项2传输时间戳字符串 g_sPeripheralScatterTaskList[1].pvSrcEndAddr (void *)(g_pucTimestamp sizeof(g_pucTimestamp) - 1); g_sPeripheralScatterTaskList[1].pvDstEndAddr (void *)UART0_DATA_BASE; g_sPeripheralScatterTaskList[1].ui32Control UDMA_CHCTL_DSTINC_NONE | UDMA_CHCTL_DSTSIZE_8 | UDMA_CHCTL_SRCINC_8 | UDMA_CHCTL_SRCSIZE_8 | UDMA_CHCTL_ARBSIZE_1 | (19 4) | // 传输20个字节20-119 UDMA_CHCTL_XFERMODE_BASIC; // 任务项3传输消息字符串 g_sPeripheralScatterTaskList[2].pvSrcEndAddr (void *)(g_pucMessage sizeof(g_pucMessage) - 1); g_sPeripheralScatterTaskList[2].pvDstEndAddr (void *)UART0_DATA_BASE; g_sPeripheralScatterTaskList[2].ui32Control UDMA_CHCTL_DSTINC_NONE | UDMA_CHCTL_DSTSIZE_8 | UDMA_CHCTL_SRCINC_8 | UDMA_CHCTL_SRCSIZE_8 | UDMA_CHCTL_ARBSIZE_1 | (23 4) | // 传输24个字节24-123 UDMA_CHCTL_XFERMODE_BASIC; }注意XFERSIZE字段在控制字中占据的位数是10位bit13:4它表示的是传输次数减1。所以传输N个数据单元此处应填写N-1。上面的代码中(5 4)即是因为XFERSIZE从bit4开始且需要填入56-1。3.3 配置μDMA通道现在配置μDMA通道这里我们假设使用通道0分配给UART0发送具体通道映射需查阅芯片数据手册。void ConfigureUDMAChannelForScatter(void) { // 1. 设置通道属性使用主控制结构体允许单次和突发请求使能请求 uDMAChannelAttributeDisable(UDMA_CHANNEL_UART0_TX, UDMA_ATTR_ALTSELECT); // 使用主结构体 uDMAChannelAttributeDisable(UDMA_CHANNEL_UART0_TX, UDMA_ATTR_USEBURST); // 允许单次请求 uDMAChannelAttributeEnable(UDMA_CHANNEL_UART0_TX, UDMA_ATTR_REQMASK); // 实际上这个函数是“清除请求屏蔽”即使能请求 // 2. 配置主控制结构体 // 主结构体的源地址是任务列表的地址目的地址是通道0副控制结构体的地址。 // 传输模式设置为外设散聚模式Peripheral Scatter-Gather。 uint32_t ui32Control; ui32Control UDMA_CHCTL_DSTINC_32 | // 目的地址副控制结构体按32位字递增因为我们每次拷贝一个完整的任务项3个字 UDMA_CHCTL_DSTSIZE_32 | // 目的数据宽度32位 UDMA_CHCTL_SRCINC_32 | // 源地址任务列表按32位字递增 UDMA_CHCTL_SRCSIZE_32 | // 源数据宽度32位 UDMA_CHCTL_ARBSIZE_1 | // 仲裁大小加载任务项时设为1 (2 4) | // XFERSIZE: 每个任务项3个字加载次数3-12 UDMA_CHCTL_XFERMODE_PER_SCATTER_GATHER; // 关键设置为外设散聚模式 // 使用TivaWare库函数设置控制结构体。 // 注意库函数uDMAChannelControlSet通常用于设置副控制结构体。 // 但在散聚模式下我们需要直接操作主控制结构体。库函数可能未直接提供此高级操作的封装。 // 因此我们需要直接访问DMA控制表的内存映射区域。 // 首先获取控制表基地址 uint32_t *pui32ControlTable (uint32_t *)uDMAControlTableBaseGet(); // 计算通道0主控制结构体的偏移量。根据手册通道N的主结构体偏移量为 N * 0x20。 uint32_t ui32ChanOffset UDMA_CHANNEL_UART0_TX * 0x20; // 每个通道主结构体占32字节8个字*4字节 uint32_t *pui32ChanCtrl pui32ControlTable (ui32ChanOffset / 4); // 转换为字指针 // pui32ChanCtrl[0] 对应 SRCENDP // pui32ChanCtrl[1] 对应 DSTENDP // pui32ChanCtrl[2] 对应 CHCTL (控制字) pui32ChanCtrl[0] (uint32_t)g_sPeripheralScatterTaskList; // 主结构体源地址任务列表起始地址 pui32ChanCtrl[1] (uint32_t)(pui32ControlTable (UDMA_CHANNEL_UART0_TX * 0x10 / 4)); // 主结构体目的地址通道0副控制结构体地址 // 通道N的副控制结构体在控制表中的偏移量为 N * 0x10。 pui32ChanCtrl[2] ui32Control; // 主结构体控制字 // 3. 使能UART0的DMA发送功能此步骤通过UART本身的寄存器配置非μDMA配置 // 使用TivaWare库UARTDMAEnable(UART0_BASE, UART_DMA_TX); }关键点解析主结构体目的地址它指向的是副控制结构体在控制表中的位置而不是外设寄存器。这是加载操作的目标。主结构体控制字SRCINC和DSTINC都设置为32位递增因为任务列表项和副控制结构体在内存中都是32位字对齐的。XFERSIZE设置为2因为每个任务项有3个字源指针、目的指针、控制字需要传输3次所以是3-12。最重要的是XFERMODE必须设置为UDMA_CHCTL_XFERMODE_PER_SCATTER_GATHER。库函数的局限标准TivaWare库函数如uDMAChannelControlSet主要针对简单传输模式设计。对于散聚模式这种需要直接操作主控制结构体的高级功能往往需要开发者直接读写DMA控制表的内存区域这要求对内存布局有清晰的理解。3.4 启动传输与中断处理配置完成后使能μDMA通道并确保UART0已配置好并开始产生发送请求例如使能UART0发送FIFO的DMA触发。void StartScatterTransfer(void) { // 使能μDMA通道 uDMAChannelEnable(UDMA_CHANNEL_UART0_TX); // 此时μDMA通道已就绪等待UART0发送FIFO空时产生的DMA请求。 // 一旦UART0发出请求散聚传输过程便会自动开始。 } // UART0中断服务函数需要处理DMA完成中断 void UART0_Handler(void) { uint32_t ui32Status UARTIntStatus(UART0_BASE, true); UARTIntClear(UART0_BASE, ui32Status); // 检查是否是DMA发送完成中断 if(ui32Status UART_INT_DMATX) { // 三个任务项全部发送完毕 // 可以进行后续操作例如点亮一个LED或者准备下一个任务列表 GPIO_PIN_3 ^ 1; // 翻转一个GPIO引脚用于示波器观察 // 如果需要再次传输必须重新配置主控制结构体特别是源地址指针可能需重置并重新使能通道。 } }重要提醒在外设散聚模式下整个任务列表传输完成后μDMA通道会产生一个完成中断具体是外设的中断向量如UART0中断。你需要在对应的外设中断服务程序中检查并清除DMA完成中断标志。同时传输完成后通道通常会自动禁用CHCTL中的XFERMODE变为STOP。如果你想再次启动一轮相同的传输必须重新初始化主控制结构体的源地址指针将其指回任务列表的起始位置然后再使能通道。因为上一轮传输结束后主结构体的源地址指针已经递增到了任务列表的末尾。4. 关键参数设计与避坑指南在实际项目中应用外设散聚模式以下几个参数和细节需要格外关注它们直接决定了传输的稳定性和效率。4.1 仲裁大小ARBSIZE的权衡ARBSIZE决定了μDMA控制器在一次总线仲裁获胜后连续传输多少个数据单元才会释放总线。设置较大的ARBSIZE如8或16可以提高突发传输效率减少总线仲裁开销在传输大块连续数据时优势明显。但在外设散聚模式下尤其是与外设交互时需要谨慎。对于低速外设如UART、I2C建议将ARBSIZE设置为1。因为这些外设的数据吞吐率很低设置大的突发量没有意义反而会长时间独占总线阻塞其他总线主设备如CPU、另一个DMA通道的访问影响系统整体响应性。对于高速外设或内存到内存的散聚如果任务项本身描述的就是一大块连续数据的传输例如一个任务项传输1024字节到DAC那么可以适当增大ARBSIZE如4或8以提升这块数据内部的传输效率。外设FIFO深度ARBSIZE的值最好与外设FIFO的触发深度相匹配或为其整数倍。例如一个SPI发送FIFO的触发深度为8那么设置ARBSIZE8可以确保每次DMA请求都能高效地填满FIFO减少请求次数。4.2 地址增量SRCINC/DSTINC与指针计算这是最容易出错的地方之一。源地址增量在“汇集”操作中源地址通常需要递增SRCINC_8/16/32以遍历内存缓冲区。务必确保SRCENDP指向的是最后一次传输的地址计算公式为起始地址 (传输次数 - 1) * 增量字节数。如果设置错误DMA会从错误的位置开始或结束传输导致数据错乱或内存访问越界。目的地址增量在“汇集”操作中目的地址是固定的外设数据寄存器必须设置为DSTINC_NONE。如果是“分散”操作从单一外设读取数据到多个内存缓冲区则目的地址需要递增源地址不递增。数据宽度对齐SRCSIZE和DSTSIZE必须一致。地址增量SRCINC/DSTINC必须大于或等于数据宽度。例如传输16位数据地址增量可以设置为16位_16或32位_32但不能设置为8位_8。4.3 任务列表的内存对齐与放置虽然μDMA控制器对任务列表所在的内存区域没有强制性的对齐要求不像控制表需要1024字节对齐但出于性能和避免总线错误考虑强烈建议将任务列表按32位字边界对齐。因为主控制结构体在加载任务项时默认是按字32位进行传输的SRCSIZE和DSTSIZE通常设置为32位。使用编译器的对齐属性如GCC的__attribute__((aligned(4)))可以轻松实现。任务列表应该放置在稳定的内存区域例如全局变量区或静态分配的大数组中。绝对避免将其分配在栈局部变量上因为函数返回后栈内存可能被覆盖导致DMA读取到错误的任务配置引发不可预知的行为甚至系统崩溃。4.4 外设端的配置要点禁用外设中断手册中明确强调“当使用 μDMA 与外设进行数据通信时外设必须禁用所有到 NVIC 的中断”。这意味着你需要禁用该外设的普通传输完成中断、空中断、满中断等。因为DMA接管了数据传输这些中断不再需要且可能产生冲突。只保留DMA完成中断或错误中断即可。正确使能DMA请求每个支持μDMA的外设都有独立的DMA请求使能位。例如对于UART需要设置UARTDMACTL寄存器中的TXDMAE发送DMA使能或RXDMAE接收DMA使能位。忘记这一步外设将永远不会产生DMA请求传输无法启动。请求信号类型了解你的外设产生的是单次请求每准备好一个数据单元就请求一次还是突发请求FIFO达到一定深度后请求一次。这会影响你配置DMAUSEBURST通道属性以及评估ARBSIZE的设置是否合理。5. 调试技巧与常见问题排查即使配置看起来完美第一次使用散聚模式时也难免遇到问题。以下是一些实用的调试方法和常见问题的排查思路。5.1 问题现象传输完全没有发生检查清单时钟与使能确认RCGCDMA和DMACFG.MASTEREN已正确设置μDMA控制器全局使能。通道使能确认已调用uDMAChannelEnable使能了特定通道。外设DMA请求使能确认外设模块本身的DMA请求功能已开启如UART的TXDMAE位。请求屏蔽确认DMAREQMASK寄存器对应通道位已清零请求未被屏蔽。使用uDMAChannelAttributeEnable(chan, UDMA_ATTR_REQMASK)。外设是否就绪对于发送UART的发送FIFO是否为空或者你是否手动触发了一次发送例如先写一个数据来启动请求链有时需要一点“火花”来启动进程。软件请求测试为了隔离问题可以暂时将通道配置为“软件请求”模式XFERMODE AUTO或BASIC然后通过写DMASWREQ寄存器来手动触发一次传输。如果软件请求能工作说明μDMA基础配置和通道是好的问题出在外设请求链路或外设配置上。5.2 问题现象只传输了第一个任务项就停止排查重点主控制结构体模式百分之百确认主控制结构体的XFERMODE字段被设置成了外设散聚模式UDMA_CHCTL_XFERMODE_PER_SCATTER_GATHER。如果误设为基本模式它只会在第一次外设请求时将任务列表的第一项加载到副结构体并执行然后就结束了。主结构体传输次数检查主结构体的XFERSIZE。它应该等于每个任务项的字数减1。通常一个任务项是3个字源、目的、控制所以XFERSIZE应设为2。如果设成了0它只会加载第一个字源地址的一部分就停止导致后续行为异常。中断过早触发检查是否错误地使能了外设的“单次传输完成”类中断并在中断中错误地禁用了DMA通道。在外设散聚模式下应在所有任务项完成后才产生一次完成中断。5.3 问题现象数据传输错乱或地址飞了排查重点指针计算错误反复核对任务列表中每个任务项的pvSrcEndAddr和pvDstEndAddr。这是最高发的错误源。使用调试器查看这些指针的值确认它们指向了你预期的内存区域末尾。控制字字段错位手动构建控制字时务必确认每个字段DSTINC,DSTSIZE,SRCINC,SRCSIZE,ARBSIZE,XFERSIZE,XFERMODE都移位到了正确的比特位。参考数据手册中的寄存器定义图或者直接使用芯片厂商提供的位定义宏如UDMA_CHCTL_SRCINC_8它们能最大程度避免移位错误。内存覆盖确保任务列表和源数据缓冲区所在的区域没有被其他代码特别是栈、堆覆盖。尤其是使用动态内存分配或大型局部数组时风险很高。5.4 利用调试器进行诊断现代IDE如CCS的调试视图非常强大内存视图直接查看DMA控制表所在的内存区域地址由DMACTLBASE指定。找到你所用通道对应的主、副控制结构体位置检查其中的值是否符合预期。在传输过程中观察副控制结构体的内容是否随着外设请求而改变被主结构体加载的新任务项覆盖。外设寄存器视图监控外设的DMA相关状态寄存器确认DMA请求标志是否被置起、清除。总线分析仪如果条件允许使用硬件总线分析仪如逻辑分析仪连接系统总线可以最直观地看到DMA请求信号、总线访问地址和数据的时序是解决复杂问题的终极武器。外设散聚模式是μDMA控制器提供的强大工具它将DMA从简单的数据搬运工升级为可编程的数据流管理器。初次接触时其配置过程确实比简单模式繁琐但一旦掌握它能优雅地解决许多实际工程中的难题例如多缓冲区数据流处理、非连续数据包的组装与发送等。理解其“主结构体管理任务列表副结构体执行当前任务”的核心思想仔细计算地址和控制参数再结合充分的调试你就能让这个强大的硬件加速器为你所用极大提升嵌入式系统的数据处理能力。