1. DMA控制器核心概念与价值
如果你在嵌入式系统开发中处理过UART、SPI或者ADC的数据流,大概率会碰到一个场景:CPU被频繁的中断和内存拷贝操作拖累,系统响应变慢,功耗上升,而数据吞吐量却上不去。这时候,DMA(Direct Memory Access,直接内存访问)控制器就是你的救星。简单来说,DMA就像一个专门负责“搬家”的硬件助手,它能在不打扰CPU这个“大管家”的情况下,独立完成内存和外设之间,或者内存不同区域之间的大量数据搬运工作。
它的核心价值在于解放CPU。想象一下,CPU原本需要亲自从UART的接收缓冲区一个字节一个字节地读数据,再存到内存的某个数组里。这个过程每搬运一个字节就可能产生一次中断,CPU需要保存现场、处理中断、恢复现场,效率极低。而DMA控制器接管后,CPU只需要告诉DMA:“从UART的地址A,搬N个字节到内存地址B”,然后就可以去处理其他更重要的计算任务了。DMA会默默完成所有数据传输,并在完成后通过一个中断通知CPU“活儿干完了”。这种异步操作模式,对于需要高带宽、低延迟数据流的应用(如音频处理、图像采集、网络通信)至关重要。
以德州仪器(TI)的CC35xx系列无线MCU中的HOST_DMA模块为例,它是一个相当典型和功能丰富的DMA控制器实现。它支持多达14个独立通道,每个通道都可以绑定到特定的外设(如UART0/1/2, SPI0/1, I2C0/1等),并拥有精细的配置项来控制传输行为。理解它的工作机制,特别是块传输(Block Transfer)、**地址对齐(Address Alignment)和通道优先级(Channel Priority)**这三个核心机制,是进行高效、可靠嵌入式编程的关键。这不仅仅是配置几个寄存器那么简单,而是关乎到如何设计你的数据缓冲区、如何预估系统带宽、以及如何确保关键数据流不被阻塞的系统级思考。
2. 块传输机制深度解析
块传输是DMA提升效率的核心手段。它背后的思想是“批量处理”,即DMA控制器不是每收到一个外设请求就搬一个数据单元(比如一个字节),而是积累一定数量的请求,然后以“块”为单位进行一次性的高效传输。这个“块”的大小,就是我们需要配置的BLKSIZE(块大小,以字为单位)。
2.1 块传输的基本流程
在CC35xx的HOST_DMA中,每个通道的作业控制寄存器(例如CHxJCTL)中的BLKSIZE字段定义了块大小。当DMA通道被使能并开始传输时,它会尝试以块为单位进行数据搬运。每个块传输对应一次总线仲裁和一次突发(Burst)传输,这比单次传输(Single Transfer)具有更高的总线利用率和更低的延迟。
关键在于理解**触发(Trigger)与传输(Transaction)**的关系。外设(如UART接收缓冲区满)会产生一个DMA请求(Trigger)。DMA控制器并不会为每个请求立即行动,而是会等待,直到累积的待传输数据量达到一个块的大小(BLKSIZE*WORDSIZE),然后启动一次块传输。如果直到作业(Job)结束,剩余的数据量也不足一个块,DMA会如何处理呢?这就是块传输的边界情况。
2.2 块传输的边界处理与BURSTREQ配置
输入材料中给出了非常清晰的例子。假设DMA字长(WORDSIZE)配置为8位(1字节),我们需要从UART搬运30字节数据,而块大小(BLKSIZE)配置为4个字(即4字节)。
- 理想情况:30字节正好是7个整块(4字节/块 * 7 = 28字节)加上2个零头字节。
- DMA的实际操作:
- 前7次传输,每次传输都是一个完整的4字节块。
- 最后剩下2字节,不足一个块(4字节)。此时,DMA的行为取决于
CHxTCTL.BURSTREQ(突发请求)位的配置。- 如果
BURSTREQ = 0(默认):DMA将采用单次请求模式。对于最后不足一块的数据,它会退化为每次触发只传输一个字(Word)。在这个例子中,最后2个字节会分成2次独立的单字传输。因此总事务数 = 7(块传输) + 2(单字传输) = 9次。 - 如果
BURSTREQ = 1:此选项用于已知外设会持续请求的场景。即使剩余数据不足一个块,DMA也会等待一个“块请求”信号,然后将所有剩余数据在一次传输中完成。这可以减少总线仲裁次数,但要求外设能配合发出块请求。
- 如果
另一个例子是字长为16位(2字节),搬运40字节(即20个字),块大小为8个字(16字节)。那么,40字节可以分成2个完整的块(16字节 * 2 = 32字节),剩余8字节(即4个字)。若BURSTREQ=0,则这4个字会进行4次单字传输。总事务数 = 2 + 4 = 6次。
实操心得:设置
BLKSIZE需要权衡。较大的块尺寸能提升总线效率,减少仲裁开销,适合大数据量连续传输(如SD卡读写)。但过大的块尺寸会延长单个通道占用总线的时间,可能影响其他通道的实时性。对于低速或零散数据(如偶发的UART命令),较小的块尺寸或保持BURSTREQ=0的默认单字模式可能响应更及时。通常,我会将高速、连续流通道(如ADC采样流)的块大小设大,而将事件驱动型通道(如I2C命令)的块大小设小,甚至设为1。
2.3 传输计数与状态追踪
每个通道的CHxTCTL.TRANSB字段定义了本次作业需要传输的总字节数。在传输过程中,CHxTSTA.REMAINB寄存器会动态更新,指示还剩多少字节需要传输。同时,CHxTSTA.OFFSET字段指示了在当前块内已经传输了多少个字,这对于调试和了解DMA内部状态非常有用。
CHxSTA.RUN位指示通道是否处于运行(或等待仲裁)状态,而CHxSTA.FSMSTATE则展示了通道内部有限状态机的具体状态(如IDLE, COPY, DONE等),是诊断DMA卡死或异常的高级工具。
3. 地址对齐支持与实战影响
地址对齐是计算机体系结构中的一个基础概念,指的是数据在内存中的起始地址是否是某个值(通常是数据本身大小的整数倍)的整数倍。例如,一个32位(4字节)的整数,如果其内存地址是4的倍数(如0x0, 0x4, 0x8),就是对齐访问;如果是0x1, 0x2, 0x3,就是非对齐访问。非对齐访问在某些架构上会导致性能下降甚至硬件异常。
CC35xx的HOST_DMA在字长为32位时,提供了对非对齐地址和作业大小的硬件支持,这极大地增加了软件设计的灵活性。输入材料中的表格详细描述了四种情况,我们来解读并转化为实战场景。
3.1 四种对齐组合场景分析
假设DMA字长为32位(4字节),我们传输数据的基本单位就是一个4字节的“字”。
| 作业起始地址 | 作业大小(字节数) | DMA内部操作解析 |
|---|---|---|
| 对齐 | 对齐 | 最理想情况。所有传输都是地址对齐且完整的字传输。效率最高。 |
| 对齐 | 非对齐 | 起始地址是字对齐的,但总字节数不是4的倍数。DMA会进行多次对齐的字传输,但最后一次传输将是不足一个字的。例如,从0x0搬运13字节。DMA会进行3次完整的4字节传输(0x0, 0x4, 0x8),最后一次从0xC开始,只传输1个字节(第13字节)。 |
| 非对齐 | 对齐 | 起始地址不对齐,但总字节数是4的倍数。第一次传输将是非对齐且不足一个字的,最后一次传输是对齐但可能不足一个字的,中间的都是对齐的完整字传输。例如,从0x1搬运12字节。第一次访问0x0地址(因为总线按字访问),但只取后3字节(地址0x1,0x2,0x3)。中间一次完整传输0x4地址的4字节。最后一次传输0x8地址的4字节,但只使用其第一个字节(凑齐总数12字节)。 |
| 非对齐 | 非对齐 | 起始地址和总字节数都不对齐。第一次传输是非对齐且不足字的,最后一次传输可能对齐也可能不对齐,且不足字,中间的是对齐的完整字传输。这是最复杂的情况,但硬件帮我们处理了所有细节。 |
3.2 对齐处理的硬件逻辑与软件考量
DMA控制器内部通过拆分和合并总线访问来实现非对齐传输。例如,从0x1读一个32位数据,硬件实际上会执行一次对齐到0x0的32位读取,然后通过内部移位逻辑提取出0x1,0x2,0x3这三个有效字节。这个过程对软件透明,但开发者必须意识到其存在。
注意事项:虽然硬件支持非对齐,但强烈建议在软件层面尽量保证对齐。原因有三:第一,非对齐访问通常需要多个内部总线周期,实际传输带宽会下降。第二,在涉及缓存(Cache)的系统中,非对齐访问可能跨越缓存行(Cache Line),导致更复杂的缓存操作。第三,确保对齐是写出高性能、可移植代码的好习惯。在定义DMA源/目标缓冲区时,使用编译器对齐指令(如GCC的
__attribute__((aligned(4))))是标准做法。
输入材料中的例子“Address un-aligned, Job Size Un-Aligned: Start address 0x1, Job size 14 bytes”很好地说明了硬件行为。它会从0x0地址读取第一个字,提取后3字节;然后完整读取0x4, 0x8地址的字;最后从0xC地址读取一个字,但只使用前2个字节(因为总共14字节,0xD和0xE)。这一切都由DMA控制器自动完成。
4. 通道优先级与仲裁机制详解
当一个DMA控制器拥有多个通道,且多个通道同时有传输请求时,就需要一个仲裁器来决定谁先使用总线。CC35xx的HOST_DMA采用了一种混合优先级轮询(Hybrid Priority Round-Robin)的仲裁策略,兼顾了实时性和公平性。
4.1 仲裁策略解析
仲裁逻辑主要由PRIOCFG寄存器控制:
PRIOEN:全局优先级使能位。CH1ST:第一优先级通道编号。CH2ND:第二优先级通道编号。MAXBLOCKS:高优先级通道最大连续传输块数限制。
其工作流程如下:
- 高优先级通道优先:如果
PRIOEN使能,且通道1(CH1ST)有请求,它总是最先获得服务。只有在它没有请求时,才会考虑通道2(CH2ND)。 - 防止饥饿(Starvation):为了防止高优先级通道霸占总线,导致低优先级通道永远得不到服务,
MAXBLOCKS参数至关重要。它限定了高优先级通道可以连续赢得的仲裁次数(以块为单位)。一旦一个高优先级通道连续传输达到了MAXBLOCKS个块,仲裁器会强制切换到轮询模式。 - 轮询(Round-Robin)仲裁:所有未被设置为高优先级的通道(包括普通优先级和第三优先级),以及在高优先级通道达到
MAXBLOCKS限制后,仲裁器会按照通道编号顺序(0, 1, 2, …)依次服务有请求的通道。这是一种公平调度算法,确保每个通道都有机会。
4.2 仲裁实例推演
输入材料给出了两个生动的例子,我们结合寄存器来解读:
场景一:MAXBLOCKS = 4, Channel 1为第一优先级,Channel 4为第二优先级。
- 初始:Channel 1连续请求3次,Channel 4请求1次,其余通道(2,3,5,6,7,8,9,10,11)也有请求。
- 仲裁序列:1, 1, 1, 4, 2, 3, 5, 1, 1, 1, 4, 6, 7, 8, 1, 1, 1, 4, 9, 10, 11
- 解读:Channel 1先连续传输3个块(未超
MAXBLOCKS=4),然后Channel 4传输1个块。接着,由于高优先级通道已服务过,仲裁器进入轮询,依次服务Channel 2, 3, 5。轮询一圈后,再次检查高优先级,Channel 1又传输3个块,Channel 4传输1个块,然后继续轮询6,7,8…如此循环。
场景二:MAXBLOCKS = 3, 其他条件不变。
- 仲裁序列:1, 1, 1, 2, 4, 3, 5, 1, 1, 1, 6, 4, 7, 8, 1, 1, 1, 9, 4, 10, 11
- 解读:Channel 1传输3个块后立即达到上限,因此仲裁器必须离开高优先级模式。它先服务轮询队列中的下一个通道(Channel 2),然后服务第二优先级Channel 4,再继续轮询Channel 3, 5。之后Channel 1再次获得3个块,接着轮询到Channel 6,然后服务Channel 4,再轮询7,8…
配置心得:
MAXBLOCKS的配置是平衡系统实时性的关键。对于音频播放这类对延迟极其敏感、数据流不能中断的通道,应设置为高优先级,并给予较大的MAXBLOCKS值(甚至接近最大值31),以确保其带宽。对于后台的数据备份、日志写入等不紧急的任务,应使用默认轮询优先级。切忌将所有通道都设为高优先级,那将失去优先级的意义,并可能因为MAXBLOCKS限制导致复杂的、不可预测的调度延迟。
5. 寄存器精讲与实战配置流程
理解了原理,最终要落实到寄存器配置上。CC35xx的HOST_DMA寄存器映射清晰,每个通道都有一套完全独立的寄存器集,从CHxSTA到CHxJCTL,偏移地址以0x1000为间隔递增。下面我们以一个典型的“从UART1接收数据到内存缓冲区”为例,拆解配置步骤和关键寄存器。
5.1 通道与外设绑定
首先,需要将DMA通道与具体的外设关联起来。这是通过通道控制寄存器CHCTL0和CHCTL1完成的。
CHCTL0:控制通道0-7的映射。每个通道用4个位(如CH0占用bit[3:0])来选择一个外设。例如,将CHCTL0.CH0设置为0x1,表示将通道0分配给UART1。CHCTL1:控制通道8-13的映射,格式同CHCTL0。
这里有一个进阶特性:CHCTLx.CHx字段可以与对应通道的CHxJCTL.SRCDSTCFG位拼接,实现更灵活的外设映射。但通常,我们直接配置CHCTLx即可。
5.2 作业控制寄存器(CHxJCTL)配置
这是配置传输特性的核心寄存器。
WORDSIZE[1:0]:设置传输字长。00=32位,01=16位,10=8位。这决定了每次总线访问的基本数据宽度,必须与外设的数据宽度匹配。例如,UART通常是8位,这里应设为10。BLKSIZE[21:16]:设置块大小(以WORDSIZE为单位)。如前所述,需要根据数据流特性权衡设置。BLKMODESRC/BLKMODEDST:源/目标地址环绕模式。如果使能(设为1),当指针增加到超过块边界时,会自动回绕到块起始地址。这对于实现循环缓冲区(Circular Buffer)极其有用,例如用于音频的乒乓缓冲区。SRCDSTCFG:方向配置。0表示源是外设,目标是内存(外设->内存,如UART接收);1表示源是内存,目标是外设(内存->外设,如UART发送)。FIFOMODS/FIFOMODD:源/目标FIFO模式。用于当一端是FIFO类型的外设时,保持地址指针不变,仅内部计数器递增。DMASIGBPS:此位置高可将DMA请求信号拉高,常用于内存到内存的传输测试。
5.3 传输控制与地址指针设置
CHxTIPTR:输入(源)指针寄存器。写入传输源头的32位物理地址。对于外设->内存传输,这里应写入外设的数据寄存器地址(如UART的接收数据寄存器地址)。CHxTOPTR:输出(目标)指针寄存器。写入传输目标的32位物理地址。即内存中缓冲区的地址。CHxTCTL.TRANSB[13:0]:本次作���要传输的总字节数。注意,即使WORDSIZE是16或32位,这里也以字节为单位。CHxTCTL.BURSTREQ:如前所述,控制不足块数据时的传输模式。CHxTCTL.ENDIANESS:字节序设置,用于处理大小端转换,在与不同字节序的系统或外设交互时使用。
5.4 启动、停止与状态查询
CHxTCTL2.CMD[2:0]:命令寄存器。这是一个只写寄存器。- 写入
1(RUN):启动传输。 - 写入
2(ABORT):停止当前传输。DMA会在完成当前块传输后停止,通道状态保持不变,便于后续恢复。 - 写入
4(INIT):在ABORT或发生错误(进入EXCEPTION状态)后,初始化通道,使其回到就绪状态。
- 写入
CHxSTA:状态寄存器。RUN位指示是否在运行,FSMSTATE可以查看详细状态机,HWEVENT显示硬件事件(处理中、传输完成、中止、异常)。CHxTSTA:传输状态寄存器。REMAINB显示剩余字节数,OFFSET显示块内偏移,STA位指示是否因错误进入异常状态。
5.5 一个完整的UART接收DMA配置示例(伪代码)
假设使用通道0,从UART1接收100字节数据到数组uart_rx_buffer。
// 1. 确保缓冲区地址对齐(4字节对齐) uint8_t uart_rx_buffer[100] __attribute__((aligned(4))); // 2. 将通道0映射到UART1外设 (CHCTL0.CH0 = 0x1) HOST_DMA->CHCTL0 = (HOST_DMA->CHCTL0 & ~0xF) | (0x1 << 0); // 3. 配置通道0的作业控制寄存器 (CH0JCTL) // WORDSIZE=8位 (0x2), BLKSIZE=16个字, 源为外设(SRCDSTCFG=0), 目标内存非环绕模式 HOST_DMA->CH0JCTL = (0x2 << 0) | // WORDSIZE = 8-bit (16 << 16) | // BLKSIZE = 16 words (0 << 29); // SRCDSTCFG = 0 (Periph -> Mem) // 4. 设置源地址(UART1数据寄存器地址)和目标地址(内存缓冲区) HOST_DMA->CH0TIPTR = (uint32_t)&(UART1->DR); // 假设UART数据寄存器地址 HOST_DMA->CH0TOPTR = (uint32_t)uart_rx_buffer; // 5. 设置传输总字节数,并选择单请求模式(BURSTREQ=0) HOST_DMA->CH0TCTL = (100 & 0x3FFF) | // TRANSB = 100 bytes (0 << 16); // BURSTREQ = 0 // 6. (可选)配置通道优先级,假设此为普通通道,使用默认轮询即可 // PRIOCFG寄存器保持默认值。 // 7. 启动传输 HOST_DMA->CH0TCTL2 = 0x1; // CMD = RUN // 8. 等待传输完成(可通过查询状态或中断) while ((HOST_DMA->CH0STA & (1<<16)) != 0) { // 等待RUN位变为0,或检查HWEVENT事件位 } // 或者,使能DMA传输完成中断,在中断服务程序中处理数据...6. 常见问题排查与调试技巧
即使配置正确,DMA传输也可能遇到各种问题。以下是我在实际项目中总结的一些排查经验和技巧。
6.1 传输卡住或无法启动
- 检查时钟与电源域:确保DMA控制器所在的总线时钟(如AHB)已经使能,并且DMA模块未处于低功耗休眠状态。这是最容易被忽略的一点。
- 验证地址与对齐:确认
TIPTR和TOPTR写入的地址是有效的物理地址,并且符合外设和内存的访问权限。对于不支持非对齐访问的外设,确保地址按字长对齐。 - 确认外设DMA请求:DMA传输通常由外设触发。检查外设的DMA请求是否已使能(例如,UART的DMA接收使能位)。DMA控制器只是响应方。
- 检查通道状态机:读取
CHxSTA.FSMSTATE。如果停留在IDLE,说明启动命令未生效或条件不满足。如果卡在PENDING_ARB,说明该通道优先级低,正在等待总线仲裁。如果进入EXCEPTION,说明传输过程中发生了总线错误(如访问了非法地址),需要检查地址并发送INIT命令复位通道。 - 命令寄存器写入:
CHxTCTL2.CMD是只写寄存器。确保你的写入操作是有效的(例如,不是读-修改-写,而是直接写入0x1)。
6.2 数据传输错误或数据错位
- 字长不匹配:这是最常见的原因。如果外设数据寄存器是8位的(如UART),但DMA
WORDSIZE配置为32位,那么DMA每次会尝试读32位,导致数据错乱和地址偏移。务必确保字长匹配。 - 字节序问题:如果源和目标系统的字节序不同(大端 vs 小端),需要正确设置
CHxTCTL.ENDIANESS字段。在纯ARM Cortex-M内核(小端)系统中,通常保持默认值0即可。 - 缓冲区溢出:DMA不会自动检查目标缓冲区边界。如果
TRANSB设置的大小超过了缓冲区实际大小,会导致内存越界,破坏其他数据。务必仔细计算。 - 环绕模式误用:如果使能了
BLKMODEDST(目标环绕),但你的软件逻辑是按线性缓冲区处理的,就会导致新数据覆盖旧数据,造成数据丢失或重复。理解你的缓冲区管理策略,选择正确的模式。
6.3 性能不达预期
- 仲裁竞争:如果多个高带宽DMA通道同时工作,且优先级配置不当,会导致某些通道等待时间过长。使用逻辑分析仪或系统跟踪器(如ARM的ITM)查看总线利用率,并调整通道优先级和
MAXBLOCKS值。 - 块大小过小:对于大数据量传输,过小的
BLKSIZE会导致频繁的仲裁和命令开销,降低有效带宽。尝试增大块大小,观察性能变化。 - 非对齐访问:如前所述,非对齐访问会降低性能。使用工具(如编译器输出map文件)检查缓冲区地址是否对齐。
- 内存访问速度:如果DMA的目标内存是低速的Flash或通过总线桥接的外部RAM,其访问速度可能成为瓶颈。考虑将缓冲区放在核心紧耦合的SRAM中。
6.4 中断与事件管理
DMA传输完成或出错会产生中断。CC35xx将其分为安全和非安全DMA事件,汇聚到CPU的NVIC。你需要:
- 在NVIC中使能对应的DMA中断。
- 在中断服务程序(ISR)中,查询事件管理器或DMA状态寄存器(
CHxSTA.HWEVENT)来确定是哪个通道、什么事件(完成、异常)触发了中断。 - 处理数据(如从缓冲区读取)或错误。
- 清除中断标志(通常通过向事件管理器的特定位写1来完成)。特别注意:
CHxTSTA.STA(错误状态位)只能通过发送INIT命令清除,而不是通过中断清除寄存器。
掌握DMA控制器,尤其是像CC35xx HOST_DMA这样功能丰富的模块,是嵌入式开发从“能用”到“高效、可靠”的关键一步。它要求开发者不仅了解寄存器配置,更要理解其背后的总线架构、仲裁策略和对系统整体性能的影响。最好的学习方式就是在实际项目中反复实践、调试和优化,积累属于自己的“避坑”经验。希望这篇深入的解析能成为你探索DMA世界的一块坚实垫脚石。