1. μDMA控制器:嵌入式系统数据搬运的“隐形管家”
在嵌入式系统开发中,尤其是面对UART、ADC、I2C这类需要频繁进行数据交换的外设时,一个永恒的矛盾是:CPU既要处理复杂的应用逻辑,又要被琐碎的字节搬运任务所拖累。想象一下,CPU就像一个忙碌的厨师,他不仅要设计菜单、烹饪主菜,还得不停地跑到仓库(内存)去取每一颗葱花、每一勺酱油(外设数据),效率自然低下。而直接内存访问,正是为了解决这个矛盾而生的“专职配菜员”。它允许数据在外设和内存之间直接流动,无需CPU这个“大厨”亲自跑腿。
但DMA的实现千差万别,从简单的单次搬运到复杂的流式处理,其能力天壤之别。今天,我们就来深入剖析一种在诸多高性能微控制器中常见的DMA实现——μDMA控制器。它绝不仅仅是一个简单的“数据搬运工”,而是一个配备了智能调度算法、支持多种工作模式的“隐形数据管家”。理解它的通道配置、仲裁机制和传输模式,是解锁外设极限性能、构建高效、实时嵌入式系统的关键。无论你是正在调试一个高速ADC采样数据流,还是想优化UART通信的吞吐量,掌握μDMA的运作细节,都能让你从“能用”走向“精通”。
2. 通道配置:为每个外设分配专属“车道”
μDMA控制器的核心资源是一组通道。你可以把这些通道想象成连接内存和外设的“专用车道”。每个通道在物理上是独立的,可以同时配置不同的传输任务。但硬件通道的数量是有限的,而系统可能需要服务的外设却很多,这就引出了第一个核心概念:通道映射。
2.1 通道映射表:灵活的硬件资源分配
根据提供的资料,每个DMA通道最多有九种可能的映射选项,通过DMACHMAPn寄存器进行选择。这就像一个大型交通枢纽的调度屏,可以将有限的物理车道(通道)动态分配给不同的车辆(外设)。
关键设计解析:为什么需要如此灵活的映射?答案在于芯片设计的复杂性与应用场景的多样性。一颗微控制器可能集成了数十个外设,但出于成本、面积和功耗的考虑,不可能为每个外设配备一个物理上完全独立的DMA通道。因此,通过映射寄存器,允许用户根据实际应用需求,将可用的物理通道“分配”给当前需要使用的特定外设。例如,在你的应用中可能同时使用了UART0和I2C0,但它们可能无法同时使用通道0,你需要查阅映射表,为它们分配两个互不冲突的物理通道。
实操要点与避坑指南:
- 务必查阅数据手册中的映射表:这是配置的起点。表中会明确列出每个物理通道(Channel 0-31)在不同编码(Encoding 0-8)下对应的外设。例如,编码0可能将通道0映射到UART0 RX,而编码1可能将其映射到ADC0。
- 理解“类型”字段:表中“Type”一栏至关重要,它标识了外设的请求类型:
- S (Single): 仅支持单次请求。外设每次准备好一个数据单元(如一个字节)时发出请求。
- B (Burst): 仅支持突发请求。外设积累了一定量的数据(如FIFO半满)后一次性发出请求。
- SB (Single/Burst): 同时支持单次和突发请求。这是最灵活的类型,也是UART、SPI等常见外设的模式。
- 警惕“Reserved”和“Software”:
- Reserved(保留):标记为保留的通道或编码绝对不能用于μDMA传输,尝试使用会导致未定义行为。
- Software(软件):这是专为软件触发传输而设计的通道。当你需要从内存的一个区域搬运大量数据到另一个区域(内存到内存复制)时,就可以配置一个软件通道,并通过写特定寄存器来触发传输。如果应用中只需要一个软件请求,可以使用专用的软件通道;如果需要多个,则应使用映射给外设的通道但配置为软件请求,以确保能获得正确的传输完成确认。
一个常见的配置错误:开发者只配置了使能寄存器,却忽略了通道映射寄存器,导致DMA通道实际并未连接到预期的外设,传输永远无法启动。正确的流程是:先通过DMACHMAPn设定通道的服务对象(外设),再配置该通道的控制参数,最后使能通道。
2.2 优先级仲裁:确保关键数据流不被阻塞
当多个外设同时需要DMA服务时,谁先谁后?这就由优先级仲裁机制决定。μDMA采用了一个简单而有效的两级优先级方案。
核心原理:每个通道的优先级由两个因素共同决定:
- 通道编号:编号越小,优先级越高。这是一个固定硬件属性,通道0拥有最高优先级。
- 优先级位:每个通道都有一个可编程的优先级位。如果该位被置位,则该通道被提升为“高优先级”。所有高优先级通道的排序,仍然按照其通道编号进行。
这意味着,你可以通过DMAPRIOSET和DMAPRIOCLR寄存器,临时提升某个关键外设(如实时性要求最高的ADC)所在通道的优先级,让它能打断正在进行中的、低优先级的传输(如后台的UART日志发送)。
经验之谈:优先级设置是一把双刃剑。滥用高优先级会导致低优先级通道“饿死”。一个良好的实践是,只为真正对延迟敏感的数据流(如音频DAC的填充、高速ADC的连续采样)设置高优先级。对于UART打印调试信息这类任务,使用默认优先级即可。同时,要特别注意资料中的警告:如果一个外设被映射到两个不同的通道,应用应使用其默认映射,或将默认映射更改为其他源。这是因为硬件内部可能存在关联逻辑,错误的映射可能导致冲突或不可预知的行为。
3. 仲裁大小:平衡吞吐量与实时性的艺术
仲裁大小(Arbitration Size)是μDMA中一个极其重要却又常被误解的概念。它直接决定了单次仲裁周期内,一个通道能连续传输多少数据项。
3.1 仲裁大小的本质与配置
当某个通道发出传输请求并被仲裁器选中后,μDMA控制器并不会只传输一个数据项就立刻重新仲裁。相反,它会连续传输由“仲裁大小”所指定数量的数据项,之后才会释放总线,重新评估所有通道的请求状态。
你可以把仲裁大小理解为“一次服务时长”。假设通道1(UART TX)的仲裁大小设为8,通道0(高优先级ADC)的仲裁大小设为1。当两者同时请求时:
- 仲裁器优先服务通道0(ADC),传输1个数据项后结束。
- 重新仲裁,由于通道1(UART)仍在请求,且此时通道0无请求,则服务通道1。
- 通道1开始传输,并且会连续传输8个数据项,在此期间即使ADC再次准备好数据并发出请求,也必须等待这8个传输全部完成。
- 8个传输完成后,重新仲裁,此时通道0(ADC)的请求得到响应。
配置建议与计算:仲裁大小的配置需要在外设能力和系统实时性之间取得平衡。
- 对于FIFO型外设(如UART):理想的仲裁大小应等于其FIFO的触发深度。例如,UART的TX FIFO深度为16,触发水平设为8(半满),那么仲裁大小设为8是最经济的。这样,当FIFO半满触发突发请求时,DMA能一次性搬空8个数据,最大化总线利用率,减少仲裁开销。
- 对于高实时性要求的触发型外设(如定时器触发ADC):应将仲裁大小设为1。这确保了每次触发都能被尽快响应,将高优先级通道的延迟降至最低。
- 对于软件触发的大��内存拷贝:可以设置一个较大的仲裁大小(如32或64),以减少仲裁次数,提升整体拷贝吞吐量。
关键寄存器:仲裁大小通过DMACHCTL寄存器中的ARBSIZE字段配置,范围是1到1024。这里的“项”指的是数据大小(8/16/32位)定义的数据单元。
3.2 突发请求与单次请求的协同
仲裁机制与请求类型紧密相关。外设可以产生两种请求:
- 单次请求:表示外设准备好传输一个数据项。
- 突发请求:表示外设准备好传输多个数据项(通常对应FIFO达到一定水平)。
μDMA控制器对这两种请求的响应策略不同:
- 如果只检测到单次请求,则传输一个数据项后停止。
- 如果检测到突发请求(或两者同时),且通道配置为允许突发,则控制器会启动一次突发传输,传输数量为
min(仲裁大小, 剩余传输项数)。
一个高级技巧:你可以通过设置DMAUSEBURSTSET寄存器的对应位,强制某个通道仅响应突发请求。这在处理那些数据必须成块传输才有意义的场景时非常有用。例如,一个图像传感器通过并行接口送来数据,每个像素16位,但你需要以32位为单位写入内存进行对齐。此时,你可以配置仲裁大小为2(即每次传输2个16位数据,构成一个32位字),并禁用单次请求,确保数据传输的原子性。
4. 传输模式详解:从基础搬运到智能流水线
μDMA提供了多种传输模式,从简单的一次性搬运到支持复杂数据流的智能模式,适应不同场景的需求。理解这些模式是发挥DMA威力的关键。
4.1 基础模式与自动模式:简单任务的基石
- 基础模式:在此模式下,DMA传输完全由外设请求“驱动”。有请求则传输(数量受仲裁大小限制),无请求则停止。它适用于外设请求信号会持续保持直到数据传输完成的场景。一个典型的反面教材是软件触发:软件触发产生的是一个脉冲请求。如果在基础模式下使用软件触发,DMA只会执行仲裁大小指定次数的传输,即使你配置的总传输项数远大于此,剩下的数据也不会被搬运。
- 自动模式:与基础模式的关键区别在于,一旦传输被启动(由请求触发),它将无视后续请求信号的变化,一直运行到配置的所有数据项传输完毕。这完美契合了软件触发的需求:你配置好源、目标和总数,写一下软件触发寄存器,DMA就会自动搬完全部数据,期间不需要CPU再干预。
模式选择心法:问自己一个问题——“我的数据传输是由一个持续的条件(如FIFO非空)驱动,还是一个瞬间的事件(如定时器溢出、软件命令)驱动?”前者用基础模式,后者用自动模式。
4.2 乒乓模式:实现零延迟的连续数据流
乒乓模式是构建高效、连续数据流处理的利器。它需要用到通道控制表中的主控结构和备用控制结构。
运作机制:
- 你准备两个缓冲区:缓冲区A和缓冲区B。
- 初始化主控结构指向缓冲区A,备用控制结构指向缓冲区B,并启动传输(通常从主控结构开始)。
- DMA开始使用主控结构(缓冲区A)进行数据传输。
- 当缓冲区A的数据传输完成后,DMA控制器自动切换到备用控制结构(缓冲区B)继续传输,同时产生一个中断给CPU。
- CPU在中断服务程序中,处理刚刚填满的缓冲区A的数据,并重新加载主控结构,使其指向下一个准备好的缓冲区(例如,再次指向A,或一个新的缓冲区C)。
- 当缓冲区B传输完成时,DMA又切换回主控结构(此时已被CPU更新),同时再次产生中断。
- 如此循环往复,形成“乒乓”操作。
核心优势:实现了数据处理与数据采集的并行化。当DMA在向缓冲区B填充数据时,CPU可以同时处理缓冲区A的数据,几乎没有空闲等待时间。这对于音频流、高速数据采集等场景至关重要。
配置关键点:
- 内存对齐:整个控制表必须在内存中连续,且起始地址对齐到1024字节边界。
- 缓冲区管理:中断服务程序中的缓冲区重载操作必须及时、准确,否则会导致数据覆盖或丢失。通常需要维护一个缓冲区队列。
4.3 散点-聚集模式:处理非连续内存的终极武器
这是μDMA提供的最强大的模式,分为内存散点-聚集和外围设备散点-聚集两种。它们解决了数据源或目的地地址不连续这一经典难题。
内存散点-聚集模式:
- 场景:你需要从内存中多个不连续的区域收集数据,拼接到一个连续的缓冲区中(聚集),或者将一个连续缓冲区的数据分发到内存中多个不连续的区域(散点)。
- 原理:CPU先在内存中创建一个“任务列表”。列表中的每一项都是一个完整的DMA控制结构(包含源/目标地址、控制字),但其模式字段被设置为“散点-聚集模式”。通道的主控结构被配置为:执行“内存到控制表”的传输,其目标地址就是通道的备用控制结构所在位置。也就是说,主控结构的任务是把“任务列表”中的条目,逐个拷贝到备用控制结构中。
- 流程:
- DMA使用主控结构,将任务列表中的第一个任务描述符拷贝到备用控制结构。
- 然后,DMA使用这个刚刚加载的备用控制结构,执行真正的数据搬运任务(比如从地址A拷贝N个数据到地址X)。
- 该任务完成后,DMA再次使用主控结构,从任务列表中拷贝下一个任务描述符到备用控制结构,并执行。
- 如此循环,直到遇到一个模式字段被设置为“自动模式”的任务描述符。执行完这个自动模式的任务后,整个散点-聚集传输完成,并产生一个中断。
外围设备散点-聚集模式:
- 场景:外围设备(如UART)需要从多个不连续的内存缓冲区发送数据,或者将接收到的数据存放到多个不连续的缓冲区。
- 原理:与内存模式类似,但任务的启动是由外围设备的请求触发的。每次外设发出请求,DMA就使用主控结构从任务列表加载下一个任务到备用结构,然后执行该任务定义的数据传输。如果下一个任务是内存到内存类型的,它会立即执行;如果是外设类型的,则会等待外设的下一个请求。
实战价值:假设你有一个通信协议栈,需要发送一个数据包,这个包由固定头部(在地址0x20001000)、可变长度的载荷(在地址0x20002000)和校验和(在地址0x20003000)三部分组成。使用散点-聚集模式,你只需创建一个包含这三个任务的任务列表,然后启动一次DMA传输(或由UART的发送请求驱动),DMA就会自动从这三个不连续的地方取出数据,依次送入UART的发送数据寄存器,无需CPU参与拼接。这极大地减轻了CPU负担,并提高了发送效率。
5. 控制结构与传输配置:工程师的调优面板
所有强大的功能,最终都通过配置通道控制结构来实现。这个结构位于系统内存中一个1024字节对齐的表格里,每个通道占32字节(主控和备用各16字节)。
5.1 控制结构详解
每个控制结构包含四个32位字:
- 源结束指针:指向传输源地址范围的最后一个字节的地址。如果源地址不递增(如外设寄存器),则直接指向该寄存器地址。
- 目标结束指针:指向传输目标地址范围的最后一个字节的地址。规则同上。
- 控制字:这是配置的精华所在,包含以下字段:
SRCSIZE,DSTSIZE: 源和目标数据大小(8/16/32位)。两者必须相同。SRCINC,DSTINC: 源和目标地址增量(0/1/2/4字节,代表不递增/字节/半字/字)。增量可以独立设置,且可以大于数据大小。例如,你可以用8位数据大小,但地址按32位(4字节)递增,这在处理某些打包数据时很有用。ARBSIZE: 仲裁大小,如前所述。XFERSIZE: 总传输项数。这是需要传输的数据项总数,DMA每完成一项会递减此值。NXTUSEBURST: 下次使用突发模式。一个高级控制位。MODE: 传输模式(停止、基础、自动、乒乓、散点-聚集)。
- 未使用:保留字,可供应用程序使用。
重要提示:控制字在传输过程中会被DMA控制器修改(主要是XFERSIZE和MODE)。因此,在每次启动新传输前,必须重新初始化控制字。而源和目标结束指针如果地址不变,则可以保持不变。
5.2 外设接口适配:FIFO与触发型外设
μDMA主要与两类外设交互:
- FIFO型外设:如UART、SPI、I2C。它们通过FIFO的填充水平来产生单次或突发请求。配置时,关键是将仲裁大小与外设FIFO的触发深度匹配。例如,将UART的接收FIFO触发深度设为4,并将对应DMA通道的仲裁大小也设为4,可以实现最高效的批量搬运。
- 触发型外设:如通用定时器、ADC。它们在特定事件(如定时器匹配、ADC转换完成)发生时,产生一个触发信号来请求DMA传输。对于这类外设,通常使用自动模式,并将仲裁大小设为1(如果每次触发只传输一个数据)或设为需要的突发数量(如果一次触发需要传输一组数据)。
一个关于USEBURST的深度建议:对于UART这类外设,资料中明确提到,不建议轻易设置USEBURST位来强制只使用突发请求。因为如果强制只响应突发请求,当FIFO中数据量不足触发水平时,DMA将不会搬运数据。这可能导致传输末尾的少量数据一直滞留在FIFO中无法发出,除非你通过其他方式(如中断)清空。通常,保持默认(同时允许单次和突发请求)是更稳健的选择。
6. 实战配置流程与常见问题排查
理解了原理,最终要落到代码上。下面以一个典型的UART DMA发送配置为例,梳理流程并分享避坑经验。
6.1 UART DMA发送配置步骤
假设我们要使用UART0的TX通道(假设映射到物理通道8),以DMA方式发送一段存储在数组tx_buffer中的100个字节数据。
内存分配与对齐:
// 分配控制表,必须1024字节对齐。许多编译器提供对齐属性。 __attribute__((aligned(1024))) uint32_t dma_control_table[256]; // 32通道 * 2结构 * 4字在初始化函数中,将DMA控制表基地址寄存器指向这个数组。
通道映射:
// 查阅手册,设置DMACHMAP2寄存器(因为通道8属于DMACHMAP2的字段),将通道8映射到UART0 TX。 // 假设编码5对应UART0 TX。 HWREG(DMA_BASE + DMA_O_CHMAP2) = (HWREG(DMA_BASE + DMA_O_CHMAP2) & ~0xF0) | (5 << 4);配置控制结构(主控):
// 计算通道8主控结构的偏移量:通道号 * 0x10 = 8 * 16 = 128字节。 volatile tDMAControlTable *pControl = (tDMAControlTable *)((uint32_t)dma_control_table + 128); pControl->srcEndPtr = (uint32_t)&tx_buffer[99]; // 源结束地址,最后一个字节地址 pControl->dstEndPtr = (uint32_t)(UART0_BASE + UART_O_DR); // 目标地址,UART数据寄存器,不递增 pControl->controlWord = (0x00 << 26) | // SRCINC: 源地址递增,1字节(因为数据是uint8_t数组) (0x03 << 30) | // DSTINC: 目标地址不递增(外设寄存器) (0x00 << 24) | // SRCSIZE: 源数据大小8位 (0x00 << 28) | // DSTSIZE: 目标数据大小8位 (0x07 << 8) | // ARBSIZE: 仲裁大小设为8(假设UART FIFO触发深度为8) (100 << 4) | // XFERSIZE: 总传输100项 (0x02 << 0); // MODE: 自动模式(因为我们要一次性发完)注意:控制字各字段的偏移位需根据具体芯片手册确定,此处为示例。
使能通道:
HWREG(DMA_BASE + DMA_O_ENASET) = 1 << 8; // 使能通道8配置UART以产生DMA请求:
// 使能UART0的DMA发送请求 HWREG(UART0_BASE + UART_O_DMACTL) |= UART_DMACTL_TXDMAE; // 设置UART发送FIFO触发水平,与DMA仲裁大小匹配 HWREG(UART0_BASE + UART_O_IFLS) = (HWREG(UART0_BASE + UART_O_IFLS) & ~UART_IFLS_TX_M) | UART_IFLS_TX_1_8; // 例如设为1/8,即2字节(假设FIFO深度16)对于自动模式,一旦UART TX FIFO有空位(或低于触发水平),就会请求DMA,DMA会持续传输直到100字节完成。
6.2 常见问题排查速查表
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| DMA传输完全无法启动 | 1. 通道未使能。 2. 通道映射错误,未连接到目标外设。 3. 控制表地址未正确设置或未对齐。 4. 外设的DMA请求未使能。 | 1. 检查DMAENASET寄存器对应位。2. 核对 DMACHMAPn寄存器配置与数据手册映射表。3. 检查控制表基地址寄存器 DMA控制表基地址,并确认地址是1024字节对齐的。4. 检查外设模块内的DMA控制寄存器(如UART的 DMACTL)。 |
| DMA只传输了一次(仲裁大小个数据)就停止 | 1. 错误地使用了基础模式处理软件或瞬时触发。 2. 总传输项数 XFERSIZE设置成了仲裁大小。 | 1. 对于需要一次性完成的传输,将模式改为自动模式。 2. 确认 XFERSIZE设置的是总数据项数,而非单次突发数。 |
| 传输完成后,通道自动禁用,但无法再次启动 | 这是正常现象。在自动或基础模式下,当XFERSIZE减为0,DMA会自动将模式设为停止并禁用通道。 | 每次重新启动传输前,必须重新配置控制字(至少重置XFERSIZE和MODE),然后重新使能通道。 |
| 高优先级传输延迟仍然很大 | 低优先级通道的仲裁大小设置过大。低优先级通道长时间占用总线,阻塞了高优先级请求。 | 减小低优先级、非实时通道的仲裁大小。确保高优先级通道的仲裁大小为1。 |
| 乒乓模式或散点-聚集模式中断混乱 | 1. 中断服务程序中未及时重载正确的控制结构。 2. 主控和备用结构在传输过程中被意外修改。 | 1. 确保在DMA中断中,在切换缓冲区后,立即为刚使用完的控制结构重新加载新的传输任务。 2. 使用 volatile关键字声明控制结构指针,防止编译器优化。考虑在关键配置步骤禁用全局中断。 |
| 数据错位或损坏 | 1. 源/目标数据大小不匹配。 2. 地址增量设置错误。例如,源是字节数组,但 SRCINC设为了4(字递增)。3. 源或目标指针未指向缓冲区末端。 | 1. 确认SRCSIZE和DSTSIZE设置相同。2. 核对 SRCINC/DSTINC与缓冲区数据类型是否匹配。对于外设寄存器,通常设为0(不递增)。3. 牢记结束指针指向的是最后一次传输操作的地址,对于递增传输,应指向缓冲区最后一个元素。 |
6.3 调试心得与高级技巧
- 利用调试器观察控制表:在调试时,直接查看内存中DMA控制表区域的内容非常有效。你可以看到
XFERSIZE和MODE字段��传输过程中的动态变化,这比任何打印信息都直观。 - 优先使用“自动模式”进行内存拷贝:对于单纯的、由软件触发的大块内存搬运,自动模式是最简单可靠的选择。配置好源、目标、总数和仲裁大小,触发后即可等待完成中断。
- 谨慎使用高优先级:除非必要,否则让所有通道使用默认优先级。过多的“高优先级”会破坏优先级的意义,增加系统调度复杂度。
- 计算总线带宽:当多个高带宽外设(如高速ADC、DAC、摄像头接口)同时使用DMA时,需要粗略估算总线带宽占用率,避免总线成为瓶颈。考虑数据宽度、频率和仲裁开销。
- 初始化时清零控制表:在系统初始化时,将整个DMA控制表内存区域清零是一个好习惯。这可以确保所有未使用的通道都处于明确的“停止”模式,防止误触发。
通过深入理解通道配置的灵活性、仲裁机制对实时性的影响,以及灵活运用从基础到散点-聚集的各种传输模式,你就能将μDMA从一个简单的数据搬运模块,转变为构建高效、可靠嵌入式系统数据流架构的核心引擎。它让你能够精心设计数据在芯片内的流动路径,把CPU的资源真正解放出来,去处理那些它最擅长的决策与控制任务。