1. 项目概述与核心价值
在嵌入式系统开发,尤其是涉及高速数据流处理的应用中,CPU的资源是极其宝贵的。无论是音频编解码、图像传感器数据采集,还是高速通信接口的数据搬移,如果让CPU亲自去处理每一个字节的传输,其计算能力将被大量消耗在简单的数据搬运上,导致核心业务逻辑(如算法处理)无法及时响应,系统实时性大打折扣。这时,直接内存访问(DMA)技术就成了我们的“救星”。它就像一个专职的“数据搬运工”,能够在内存与外设、或者内存与内存之间,独立地、高效地完成大批量数据的传输,而CPU只需在开始时下达指令,结束时验收成果,期间可以完全解放出来去处理其他任务。
然而,基础的DMA传输模式在面对连续、实时的数据流时,依然会面临“等待”的瓶颈。例如,当DMA正在将A缓冲区的数据搬移到外设时,CPU无法处理这份数据,必须等待传输完成;反之,当CPU在处理A缓冲区的数据时,DMA也无法向其中写入新数据。这种串行操作会引入不可避免的延迟。为了解决这个问题,Ping-Pong缓冲和传输链这两种高级DMA技术应运而生。它们不是简单的“搬运”,而是实现了数据流水线作业,让数据的“搬入”、“处理”、“搬出”这几个环节能够并行进行,从而将系统吞吐量推向极限。
本文将以德州仪器(TI)的EDMA3控制器为例,深入剖析这两种技术的实现原理、配置细节和实战技巧。EDMA3是TI C6000系列DSP等高性能处理器中的核心外设,其功能强大且配置灵活,是理解高级DMA概念的绝佳样板。我们将结合一个具体的McBSP(多通道缓冲串行端口)音频流处理案例,手把手拆解Ping-Pong缓冲如何实现零等待的数据交替处理,以及传输链如何将一个大块传输“化整为零”,避免阻塞系统。无论你是正在调试音频接口的嵌入式工程师,还是希望优化图像处理流水线的开发者,理解这些技术都能让你对系统性能的掌控力提升一个档次。
2. EDMA3核心机制与Ping-Pong缓冲原理
2.1 EDMA3的传输模型与PaRAM集
要玩转EDMA3的高级功能,必须先理解它的基本工作模型。EDMA3的每一次传输行为,都由一个称为参数集(PaRAM Set)的数据结构完全定义。你可以把它想象成发给“搬运工”DMA的一张详细“工作单”。一个完整的PaRAM集包含8个关键参数,它们共同描述了一次传输的方方面面:
- OPT(选项参数):传输的“模式开关”,决定同步方式、地址模式、是否启用中断或链式触发等。
- SRC(源地址):数据从哪里开始搬。
- DST(目的地址):数据要搬到哪里去。
- ACNT(第一维数量):一个数组(Array)里有多少个连续字节。这是传输的最小单位。
- BCNT(第二维数量):一个帧(Frame)里有多少个这样的数组(Array)。
- SRCBIDX / DSTBIDX(B维索引):每完成一个数组传输后,源地址和目的地址需要跳过的字节数,用于在帧内定位下一个数组。
- LINK(链接地址):当前参数集用完后,下一个要使用的PaRAM集在参数RAM中的地址。这是实现“传输链”和“Ping-Pong”自动切换的关键。
- BCNTRLD(BCNT重载值):当一帧传输完成(BCNT减到0)后,重新装载到BCNT的值,用于循环传输。
- SRCCIDX / DSTCIDX(C维索引):每完成一帧传输后,源地址和目的地址需要跳过的字节数,用于在块(Block)内定位下一帧。
- CCNT(第三维数量):一个块(Block)里有多少帧。
EDMA3支持三维传输(ACNT -> BCNT -> CCNT),这提供了极大的灵活性。但对于理解Ping-Pong,我们通常关注A同步和AB同步这两种模式,由OPT中的SYNCDIM位决定。
- A同步:一个外部事件(Event)触发传输一个数组(ACNT个字节)。传输完ACNT个字节后,本次传输完成,等待下一个事件。
- AB同步:一个外部事件触发传输一整帧数据(即 BCNT 个数组,总共 ACNT * BCNT 个字节)。传输完这一帧后,本次传输完成。
2.2 Ping-Pong缓冲:消除处理空窗期
Ping-Pong缓冲的核心思想非常简单:准备两个缓冲区,一个叫Ping,一个叫Pong。让DMA和CPU在这两个缓冲区上“打乒乓球”。
工作流程如下:
- 初始状态:DMA正在向Ping缓冲区写入新数据。CPU处于空闲或处理其他任务。
- DMA写满Ping:DMA完成对Ping缓冲区的写入。
- 切换与并行:
- DMA收到“Ping满”的信号,自动切换到Pong缓冲区,开始向Pong写入下一批数据。
- 同时,DMA通过中断或标志位通知CPU:“Ping缓冲区数据已就绪,可以处理了”。
- CPU开始处理Ping缓冲区中的数据。
- 循环往复:
- 当CPU处理完Ping,而DMA可能还在写Pong(或已写完)。
- 一旦DMA写满Pong,它又切换回Ping写入,并通知CPU去处理Pong。
- 如此循环,实现了数据“写入”和“处理”的完全并行,理论上消除了CPU的等待时间。
在EDMA3中,这种“自动切换”是如何实现的?秘密就在于PaRAM集的链接(LINK)功能。我们可以为同一个DMA通道配置两套PaRAM集:一套指向Ping缓冲区,其LINK字段指向指向Pong缓冲区的PaRAM集地址;另一套指向Pong缓冲区,其LINK字段指回Ping的PaRAM集地址。当DMA用当前参数集完成一次传输后,它会自动从LINK指向的地址加载下一套参数,从而在Ping和Pong之间循环切换。
2.3 传输链:化整为零的智慧
传输链技术解决的是另一个问题:大块数据传输的“霸道”行为。假设你需要通过DMA搬运一个16MB的图像数据到外部存储器。如果配置成一次性的三维传输,这个DMA通道将会长时间占据总线资源和传输控制器(TC),导致同优先级甚至更高优先级的其他DMA请求被“饿死”,系统实时性遭到破坏。
传输链,特别是中间传输完成链(ITCCHEN),提供了解决方案。它允许你将一个大的传输任务(例如,BCNT=16, ACNT=1024, 共16KB)分解为多个小的、独立的传输子任务(例如,16次ACNT=1024的传输)。关键配置在于OPT寄存器中的两个位:
ITCCHEN(中间传输完成链使能):设置为1。TCC(传输完成码):设置为一个特定的值,比如本通道的通道号。
当ITCCHEN=1时,每完成一个中间传输(即每传输完一个ACNT数组),EDMA3就会根据TCC值产生一个链式事件。这个链式事件可以触发另一个DMA通道开始工作(用于服务另一个外设),更巧妙的是,它也可以触发本通道自己,从而启动下一个数组的传输。
这样,一个16KB的传输就被分解成了16次1KB的传输。每次1KB传输结束后,都会释放总线资源一小段时间,让其他等待的传输请求得以插入。这就像在一条繁忙的单行道上,一辆长卡车主动在路口分批通过,而不是一次性堵死整条路,显著提升了系统的整体吞吐量和响应性。
3. McBSP音频流处理的Ping-Pong实战解析
现在,我们结合输入材料中TI官方手册提供的McBSP Ping-Pong缓冲示例,来具体看看这些概念是如何落地的。这个例子非常经典,描述了如何用两个EDMA3通道(通道2和通道3)服务一个全双工McBSP,实现音频数据的连续输入输出。
3.1 系统框架与数据流
在这���例子中,我们假设:
- McBSP接收(RX):数据从外部设备(如音频编解码器)通过McBSP的DRR(数据接收寄存器)流入。我们需要一个DMA通道(例如通道3)将数据从
DRR (地址 0x01D0 0000)搬运到内存中的Ping/Pong缓冲区。 - McBSP发送(TX):处理后的数据需要从内存中的Ping/Pong缓冲区,通过DMA搬运到McBSP的DXR(数据发送寄存器)(地址 0x01D0 0004),再发送出去。这需要另一个DMA通道(例如通道2)。
- 内存缓冲区:我们为输入和输出分别准备了两组Ping-Pong缓冲区,位于内存的不同区域(例如输入缓冲区在
0x1180 0000和0x1180 0800,输出缓冲区在0x1180 1000和0x1180 1800)。
数据流如下图所示(概念示意):
外部音频数据 -> McBSP.DRR -> EDMA3 Ch3 -> 内存输入缓冲区(Ping/Pong) CPU处理 <- 内存输入缓冲区(Ping/Pong) 内存输出缓冲区(Ping/Pong) -> EDMA3 Ch2 -> McBSP.DXR -> 外部设备输入材料中那些看似杂乱的A1i, B2o等符号,正是描述了在某个时刻,各个缓冲区(A区、B区分别代表Ping和Pong)的“输入(i)”和“输出(o)”状态,勾勒出了一幅动态的、交替工作的画面。
3.2 PaRAM配置详解
手册中的图16-29、16-30、16-31提供了完整的参数配置表,我们来解读关键部分:
对于接收通道(Channel 3, PaRAM Set 3 链接到 Pong Set 64):
SRC = 0x01D0 0000:源地址是McBSP的DRR寄存器。DST = 0x1180 0000:目的地址是Ping输入缓冲区的起始地址(初始状态)。ACNT = 1:每个数组1个字节?这里需要根据音频数据格式调整,例如对于16位音频,ACNT=2。示例中可能简化了。BCNT = 0x0080(128):一帧包含128个数组。这意味着一次DMA传输会从DRR搬移128个数据单元到连续的内存。SRCBIDX = 0:源地址(DRR寄存器)是固定的,不递增。DSTBIDX = 1:每传输一个数组后,目的内存地址递增1字节(或ACNT字节),以在缓冲区中顺序存放。LINK = 0x4800:这是一个关键!它指向PaRAM Set 64的地址偏移。Set 64中配置的DST = 0x1180 0800,即Pong输入缓冲区。当本次传输(写Ping)完成后,EDMA3会自动加载Set 64的参数,下一次传输就会写向Pong缓冲区。
对于发送通道(Channel 2, PaRAM Set 2 链接到 Pong Set 65):
SRC = 0x1180 1000:源地址是Ping输出缓冲区。DST = 0x01D0 0004:目的地址是McBSP的DXR寄存器。- 其他参数如BCNT、索引等与接收通道类似,方向相反。
LINK = 0x4840:指向PaRAM Set 65,其中SRC = 0x1180 1800,即Pong输出缓冲区。
Ping和Pong参数集(Set 64/65, 66/67)形成了闭环链接。Set 64(Pong输入)的LINK指向Set 65(Ping输入),而Set 65的LINK又指回Set 64。发送端同理。这就构建了一个永动的“8”字型链接环路,实现了Ping和Pong的自动交替。
3.3 同步与CPU交互
配置好DMA自己循环还不够,关键是让CPU知道什么时候该处理哪个缓冲区。这就是传输完成中断的作用。
在OPT参数中,我们设置:
TCINTEN = 1:使能传输完成中断。TCC = N:设置传输完成码,例如通道3设为3,通道2设为2。
当通道3完成一次传输(即写满一个输入缓冲区)时,它会将中断挂起寄存器(IPR)中的对应位(E3)置1。同样,通道2清空一个输出缓冲区时,会置位E2。CPU可以通过轮询IPR寄存器或者配置中断服务程序(ISR)来感知这些事件。
典型的CPU工作流程如下:
- 初始化所有PaRAM集,启动McBSP和EDMA3通道。
- EDMA3通道3开始将数据从DRR搬至Ping缓冲区(
0x1180 0000)。 - 当Ping缓冲区被填满,通道3传输完成,触发中断(IPR.E3=1)。同时,其PaRAM自动通过
LINK切换到指向Pong缓冲区(0x1180 0800),并准备下一次传输。 - CPU在中断服务程序或主循环中检测到IPR.E3被置位,知道Ping输入缓冲区数据已就绪。
- CPU开始处理位于
0x1180 0000的Ping缓冲区数据。与此同时,EDMA3通道3已经在向Pong缓冲区(0x1180 0800)写入新的数据了,实现了并行。 - CPU处理完Ping数据后,可能需要将结果放入输出缓冲区。它会检查输出状态。假设初始时EDMA2正在从Ping输出缓冲区(
0x1180 1000)读取数据发送。当发送完成(IPR.E2=1)后,CPU可以将处理好的数据填入这个已腾空的Ping输出缓冲区。 - 如此循环,CPU始终处理“刚刚被DMA填满”的那个输入缓冲区,并将结果填入“刚刚被DMA清空”的那个输出缓冲区。DMA则永远在操作另一个缓冲区,两者互不干扰。
注意:中断的清除需要手动进行。CPU在响应中断后,必须向中断清除寄存器(ICR)的相应位写1,以清除IPR中的标志位,否则会持续产生中断。
4. 传输链(Transfer Chaining)高级应用与配置
Ping-Pong解决了并行性问题,而传输链则解决了公平性和实时性问题。输入材料中给出了两个非常实用的传输链例子。
4.1 单事件服务多个FIFO(AB同步与链式事件)
第一个例子(16.3.4.5.1)描述了一个常见场景:系统有两个外部FIFO,一个输入,一个输出,需要以相同的速率进行服务。理想情况下,一个外部事件(如一个GPIO引脚跳变)能同时触发对这两个FIFO的DMA服务。
如果没有传输链,这需要两个独立的事件源。但利用中间传输完成链(ITCCHEN),我们可以用单个事件实现。配置如下:
- 使用一个DMA通道(如通道16)来服务主FIFO(例如输入FIFO),配置为AB同步传输(
SYNCDIM=1),BCNT设为2(代表两个FIFO都需要服务?这里更合理的解释是,该通道负责一个数据流,但其完成事件用于链式触发)。 - 关键设置:
ITCCHEN=1,TCCHEN=1, 且TCC=31(假设通道31用于服务另一个FIFO)。 - 当通道16的传输启动后,每完成一个中间数组传输(A同步维度),由于
ITCCHEN=1,EDMA3会设置链式事件寄存器(CER)的E31位,并产生一个对通道31的同步事件。 - 通道31被配置为由事件31触发,执行对第二个FIFO的服务。
- 当通道16完成整个传输(最后一个数组),
TCCHEN=1会再次触发通道31。
这样,仅需一个外部GPIO事件触发通道16,就能规律性地、交替地触发通道31,完美同步了两个FIFO的服务。图16-32清晰地展示了这一过程。
4.2 拆分大块传输(A同步与自链)
第二个例子(16.3.4.5.2)是传输链的经典应用:拆分大块传输。假设我们需要从内部内存搬运16KB数据到外部内存(EMIF),这是一个高优先级但耗时的操作。
不推荐的粗暴方式:设置ACNT=16384, BCNT=1, CCNT=1,一次搬完。在这期间,同优先级队列的其他传输请求将被阻塞。
推荐的智能方式(使用ITCCHEN自链):
- 配置通道25:
ACNT=1024(1KB),BCNT=16,CCNT=1,SYNCDIM=A-sync。这表示总共16个数组,每个数组1KB,需要16个事件触发。 - 关键设置:
ITCCHEN=1,TCC=25(自身通道号)。 - 启动方式:CPU手动写事件置位寄存器(
ESR.E25=1)来触发第一次传输。 - 过程:
- 第一次传输(第一个1KB数组)完成���
- 由于
ITCCHEN=1且TCC=25,EDMA3产生一个链式事件到通道25自身(CER.E25被置位并触发事件25)。 - 这个自触发的事件立刻启动第二次传输(第二个1KB数组)。
- 如此反复,直到第16次传输完成。因为这是最后一次传输���
TCCHEN也会生效(如果使能),可以产生一个最终完成中断给CPU。
- 优势:每次只传输1KB,传输间隙(从传输结束到链式事件触发下一次传输有极短延迟)为同队列的其他DMA请求提供了可乘之机,避免了长时间的资源独占。图16-34的时间线直观展示了这种“化整为零”带来的时间空隙。
实操心得:在设置自链拆分大传输时,
ACNT(即每次拆分的“小块”大小)的选择至关重要。太小会导致链式事件过于频繁,增加额外开销;太大则起不到“让出资源”的作用。这个值需要根据总线带宽、存储器延迟以及系统中其他实时任务的周期来权衡。一个实用的起点是将其设置为系统主要等时数据流(如音频帧、视频行)大小的整数倍。
5. 关键寄存器深度解析与配置陷阱
理解了原理和流程,最终都要落实到寄存器配置上。输入材料中16.4.1节详细列出了PaRAM各字段,这里我们挑出最易出错和最关键的几个点进行深度解析。
5.1 OPT寄存器:控制逻辑的核心
OPT寄存器是大脑,每一个bit都至关重要:
ITCCHEN/TCCHEN:如前所述,控制中间和最终传输完成的链式触发。常见错误是只设置了TCINTEN想中断CPU,却忘了如果要用链式触发,必须同时使能TCCHEN。ITCINTEN/TCINTEN:控制是否在中间/最终传输完成时产生中断。注意:即使这里使能了,还必须确保中断使能寄存器(IER)中对应的TCC位也被置1,中断才能真正到达CPU。TCC(Bit 17-12):6位传输完成码。它有两个作用:1) 作为中断号,对应IPR和IER中的位;2) 作为链式事件码,对应CER中的位。关键陷阱:在有多核或者复杂事件路由的系统中,需要查清手册,确认你使用的TCC值映射到的具体中断线是否正确。TCCMODE:选择是“正常完成”(数据完全搬完)还是“早期完成”(EDMA3CC提交传输请求后即认为完成)。对于需要精确控制数据完整性的场景,务必使用正常完成模式。SYNCDIM:A同步 vs AB同步。这是概念理解的关键,配置错误会导致传输量不符合预期。DAM/SAM:地址模式。绝大多数情况都使用增量模式(INCR,值为0)。常量地址模式(CONST,值为1)仅用于模拟FIFO行为,且需要外设支持。TI明确指出在C674x等处理器上,没有外设或存储器控制器支持常量模式,用错会导致数据错乱。如果需要FIFO行为,应通过精心计算ACNT和BIDX在增量模式下模拟。
5.2 索引与计数:数据布局的画笔
SRCBIDX,DSTBIDX,SRCCIDX,DSTCIDX这些索引值定义了数据在内存中是如何“摆放”的。
- 一维线性传输:
ACNT=数据总长,BCNT=1,CCNT=1,所有索引设为0。 - 二维数组传输(例如图像行):
ACNT=一行的字节数,BCNT=行数。SRCBIDX和DSTBIDX通常设为ACNT,表示每传输完一行,地址跳到下一行的开头。 - 三维块传输(例如图像帧):在二维基础上增加
CCNT(帧数)和SRCCIDX/DSTCIDX(帧间偏移)。
计算示例:假设要将一个100x100的16位灰度图像(每像素2字节)从连续存储的源地址SrcAddr,搬运到目的地址DstAddr,且目的内存布局需要每行末尾有10字节的间隔(Stride)。
ACNT = 100 * 2 = 200(一行数据字节数)BCNT = 100(行数)CCNT = 1(只有一帧图像)SRCBIDX = 200(源是连续的,所以偏移就是一行字节数)DSTBIDX = 200 + 10 = 210(目的每行后需要跳过10字节的间隔)SRCCIDX和DSTCIDX为0(因为只有一帧)
5.3 LINK与BCNTRLD:实现循环与切换的魔法
LINK:链接地址。必须指向一个32字节对齐的PaRAM集起始地址。通常我们会把用于Ping-Pong或传输链的几组PaRAM集在内存中连续排列,这样LINK值就是当前集的地址加上0x20(32字节)的整数倍。设置为0xFFFF表示空链接,传输完成后停止。BCNTRLD:仅用于A同步传输。当BCNT在传输过程中递减到0后,会用这个值重新装载BCNT。在Ping-Pong这种需要固定帧大小的场景中,BCNTRLD通常就等于初始的BCNT值。
配置陷阱排查清单:
- 传输不动:首先检查事件是否已使能(EER寄存器对应位),事件是否被正确触发(检查外设或手动写ESR)。其次检查PaRAM集是否已正确加载到对应的通道参数RAM中。
- 传输地址错乱:复查
SRC/DST地址是否有效,SAM/DAM模式是否正确(99%的情况是INCR模式)。检查BIDX/CIDX计算是否正确,特别是符号(有符号数)。 - Ping-Pong不切换:检查两组PaRAM的
LINK地址是否形成了闭环,并且指向正确的、已配置好的参数集。确认OPT中的STATIC位是否为0(非静态),允许链接更新。 - 中断不产生:确认OPT中
TCINTEN或ITCINTEN已使能,TCC值设置正确,并且IER寄存器中对应的TCC位也已使能。CPU侧的中断控制器(INTC)配置也需要正确。 - 链式触发不工作:确认OPT中
ITCCHEN或TCCHEN已使能,TCC值指向的目标通道事件已使能(EER)。对于自链,确保目标通道就是自己。
6. 工程实践:从零构建一个McBSP音频Ping-Pong系统
理论最终要服务于实践。下面我将概述在TI C6000系列DSP上,为一个McBSP音频接口配置EDMA3 Ping-Pong传输的具体步骤和代码片段思路。
6.1 系统初始化与内存规划
首先,在内存中明确划分出缓冲区。通常使用.section或#pragma DATA_SECTION指令将缓冲区定位到特定的高速内存段(如L2 SRAM)。
#define BUFFER_SIZE 128 // 例如,128个采样点 #define NUM_BUFFERS 2 #pragma DATA_SECTION(inputBuffers, ".edma_buffer") int16_t inputBuffers[NUM_BUFFERS][BUFFER_SIZE]; // Ping-Pong输入缓冲区 #pragma DATA_SECTION(outputBuffers, ".edma_buffer") int16_t outputBuffers[NUM_BUFFERS][BUFFER_SIZE]; // Ping-Pong输出缓冲区 // 在链接命令文件(.cmd)中,确保.edma_buffer段被映射到合适的物理地址(如0x11800000)接着,初始化McBSP:设置采样率、数据格式(字长、压扩、帧同步)、时钟等,并使能McBSP的发送和接收事件(XEVT和REVT)以触发DMA。
6.2 PaRAM表配置与装载
这是最核心的一步。我们需要在内存中定义一个PaRAM表结构体数组,并填充四组参数(Rx Ping, Rx Pong, Tx Ping, Tx Pong)。
typedef struct { volatile uint32_t OPT; volatile uint32_t SRC; volatile uint32_t A_B_CNT; // ACNT在低16位,BCNT在高16位 volatile uint32_t DST; volatile uint32_t SRC_DST_BIDX; // SRCBIDX在低16位,DSTBIDX在高16位 volatile uint32_t LINK_BCNTRLD; // LINK在低16位,BCNTRLD在高16位 volatile uint32_t SRC_DST_CIDX; volatile uint32_t CCNT; } EdmaParamSet; EdmaParamSet myParamTable[4] __attribute__((aligned(32))); // 对齐到32字节 void configureEdmaParamTable(void) { // 假设 McBSP0 DRR 地址为 0x01D00000, DXR 为 0x01D00004 uint32_t mcbsp_drr = 0x01D00000; uint32_t mcbsp_dxr = 0x01D00004; uint32_t buf_in_ping = (uint32_t)&inputBuffers[0][0]; uint32_t buf_in_pong = (uint32_t)&inputBuffers[1][0]; uint32_t buf_out_ping = (uint32_t)&outputBuffers[0][0]; uint32_t buf_out_pong = (uint32_t)&outputBuffers[1][0]; // 1. 接收通道 - Ping参数集 (假设使用PaRAM Set 0) myParamTable[0].OPT = ...; // 配置TCC=3, TCINTEN=1, SYNCDIM=AB-sync等 myParamTable[0].SRC = mcbsp_drr; myParamTable[0].A_B_CNT = (BUFFER_SIZE << 16) | (2); // BCNT=BUFFER_SIZE, ACNT=2字节(16位音频) myParamTable[0].DST = buf_in_ping; myParamTable[0].SRC_DST_BIDX = (2 << 16) | (0); // DSTBIDX=2, SRCBIDX=0 (DRR地址不变) myParamTable[0].LINK_BCNTRLD = (BUFFER_SIZE << 16) | (0x20); // LINK指向下一个PaRAM Set (偏移0x20) // ... 设置其他字段 // 2. 接收通道 - Pong参数集 (PaRAM Set 1) myParamTable[1].OPT = ...; // OPT可与Set 0相同 myParamTable[1].SRC = mcbsp_drr; myParamTable[1].DST = buf_in_pong; // 目的地址改为Pong缓冲区 myParamTable[1].LINK_BCNTRLD = (BUFFER_SIZE << 16) | (0x00); // LINK指回Set 0,形成环路 // ... 复制其他字段 // 3. 发送通道 - Ping参数集 (PaRAM Set 2) myParamTable[2].OPT = ...; // 配置TCC=2, TCINTEN=1, SYNCDIM=AB-sync myParamTable[2].SRC = buf_out_ping; myParamTable[2].DST = mcbsp_dxr; myParamTable[2].A_B_CNT = (BUFFER_SIZE << 16) | (2); myParamTable[2].SRC_DST_BIDX = (0 << 16) | (2); // SRCBIDX=2, DSTBIDX=0 (DXR地址不变) myParamTable[2].LINK_BCNTRLD = (BUFFER_SIZE << 16) | (0x60); // LINK指向Set 3 // ... // 4. 发送通道 - Pong参数集 (PaRAM Set 3) myParamTable[3].OPT = ...; myParamTable[3].SRC = buf_out_pong; myParamTable[3].LINK_BCNTRLD = (BUFFER_SIZE << 16) | (0x40); // LINK指回Set 2 // ... // 将配置好的PaRAM表内容,通过EDMA3的API或直接写内存,搬运到实际的EDMA3参数RAM区域 // 例如使用CSL库函数:EDMA3_setPaRAM(edmaHandle, paramSetId, &myParamTable[setId]); }6.3 通道使能与中断处理
配置好PaRAM后,需要使能对应的事件,并挂接中断服务程序。
void startAudioTransfer(void) { // 1. 使能EDMA3通道事件(假设接收用事件3,发送用事件2,需与McBSP事件映射一致) EDMA3_enableChannel(edmaHandle, EDMA3_CHANNEL_TYPE_DMA, 3); // 使能通道3(接收) EDMA3_enableChannel(edmaHandle, EDMA3_CHANNEL_TYPE_DMA, 2); // 使能通道2(发送) // 2. 在事件使能寄存器(EER)中使能对应事件 // 通常McBSP的REVT会映射到某个EDMA3事件,例如事件3 // 同样,XEVT映射到事件2。需要根据具体芯片的交叉开关配置。 // 3. 使能CPU中断 // 清除可能存在的旧中断标志 EDMA3_clearInterrupt(edmaHandle, 3); // 清除TCC=3对应的中断 EDMA3_clearInterrupt(edmaHandle, 2); // 清除TCC=2对应的中断 // 在中断使能寄存器(IER)中使能中断 EDMA3_enableInterrupt(edmaHandle, 3); // 使能TCC=3中断 EDMA3_enableInterrupt(edmaHandle, 2); // 使能TCC=2中断 // 4. 配置CPU中断控制器,将EDMA3中断线连接到对应的CPU中断,并注册ISR。 // 5. 启动McBSP收发。 mcbsp_start(); } // 中断服务程序示例 interrupt void edmaIsr(void) { uint32_t ipr = EDMA3_getInterruptStatus(edmaHandle); // 读取IPR if (ipr & (1 << 3)) { // 通道3(接收)完成中断 // 确定当前哪个输入缓冲区刚被填满(可通过一个标志变量或检查当前PaRAM的DST地址来判断) volatile int16_t *readyInputBuffer = getCurrentFullInputBuffer(); // 处理 readyInputBuffer 中的数据... processAudioData(readyInputBuffer, BUFFER_SIZE); // 将处理后的数据填入下一个可用的输出缓冲区... fillNextOutputBuffer(processedData); // 清除中断标志 EDMA3_clearInterrupt(edmaHandle, 3); } if (ipr & (1 << 2)) { // 通道2(发送)完成中断 // 一个输出缓冲区已发送完毕,可以标记为空闲,准备接收新的待发送数据。 markOutputBufferFree(); // 清除中断标志 EDMA3_clearInterrupt(edmaHandle, 2); } // ... 可能还需要清除中断控制器中的标志 }6.4 调试技巧与性能优化
- 使用EDMA3调试工具:TI的CCS集成开发环境提供了EDMA3可视化工具,可以实时查看PaRAM内容、事件状态、队列状态和传输请求状态,是调试的利器。
- 内存对齐与缓存一致性:确保DMA缓冲区地址按Cache行对齐(如32字节或128字节),并在CPU访问DMA缓冲区前,使用
CACHE_invL1d或CACHE_invL2等函数无效化缓存;在CPU写完后、DMA读取前,使用CACHE_wbL1d或CACHE_wbL2写回缓存。否则会出现数据不一致的幽灵问题。 - 优先级与队列分配:合理分配DMA通道到不同的传输队列(Queue)。将高实时性要求的通道(如音频RX/TX)分配到高优先级队列,将后台大数据搬运分配到低优先级队列。避免所有高带宽通道挤在同一个队列。
- 避免中断延迟:如果中断处理函数过于复杂,可能无法及时响应,导致缓冲区覆盖。可以考虑使用双缓冲+Polling模式:关闭DMA完成中断,CPU在主循环中轮询IPR寄存器。这在处理周期固定且对延迟不极度敏感的应用中,可以减少中断上下文切换的开销。
- 参数重载时机:理解“静态”与“非静态”参数集(OPT.STATIC位)。对于Ping-Pong这种动态链接的场景,必须设置为非静态(0)。对于一次性触发的QDMA传输,可以设置为静态(1)。
在我经手的多个音频处理项目中,EDMA3的Ping-Pong缓冲是保证低延迟、零丢失音频流的关键。有一次调试,发现偶尔会有音频毛刺,最终定位到缓存一致性问题——CPU在处理缓冲区时,由于缓存未及时更新,读到的是旧数据。加上缓存维护操作后问题立刻消失。另一个常见的坑是LINK地址计算错误,没有32字节对齐,导致DMA在链接时读取到错误的参数,引发内存访问错误。这些细节,手册上可能只是一笔带过,但却是工程稳定性的生命线。