嵌入式DMA通道复用与触发机制:eDMA与DMA_MUX实战解析
2026/6/15 14:10:57 网站建设 项目流程

1. 项目概述与核心价值

在嵌入式系统开发中,尤其是面对实时数据流处理、高速通信接口或复杂波形生成这类任务时,CPU如果深陷于频繁的数据搬运工作,整个系统的实时性和效率就会大打折扣。这时候,直接内存访问(DMA)技术就成了工程师手中的“王牌”。它本质上是一个独立的、高度专业化的数据搬运工,能够在内存与外设之间,或者内存的不同区域之间,直接建立高速数据传输通道,完全绕过CPU的干预。想象一下,你正在用SPI接口以10MHz的速率接收来自传感器的数据流,如果没有DMA,CPU需要不断被中断,去读取SPI数据寄存器里那一个字节的数据,然后存到内存里。这个过程本身的开销(保存现场、执行中断服务程序、恢复现场)可能比处理数据本身还要耗时,CPU宝贵的算力被大量浪费在“跑腿”上。

而eDMA(Enhanced DMA)作为第二代DMA控制器,其能力更为强大。它不仅仅是一个简单的搬运工,更像是一个可编程的数据传输引擎。它支持复杂的传输描述符(TCD),允许我们预先定义好一整套传输规则,比如从哪里搬、搬到哪里、一次搬多少、搬完一轮后地址怎么变化、总共搬几轮等等。设置好后,只需一个启动信号,eDMA就能自动、精确地执行整个复杂的传输序列。这对于处理音频缓冲区、图像帧数据、网络数据包等场景是革命性的。

然而,在像PXS20这样集成度高的微控制器中,外设数量(如多个SPI、ADC、定时器、UART)往往远超eDMA控制器提供的物理通道数量(例如16个)。这就引出了本文要深入探讨的核心:eDMA通道复用与触发机制。DMA_MUX(DMA Multiplexer)模块,就是这个问题的“交通调度中心”。它允许我们将多达27个不同的外设DMA请求源(称为“槽位”或Slots),灵活地映射到16个物理DMA通道上。更重要的是,前4个通道还集成了周期性触发能力,可以由一个高精度的定时器(如PIT)来“定时”启动DMA传输,从而实现无需CPU干预的、精准周期性的数据采集或发送。这种机制,是将DMA从“被动响应外设请求”升级为“主动定时执行任务”的关键,对于构建高实时性、低CPU占用的嵌入式系统至关重要。

2. eDMA与DMA_MUX架构深度解析

要玩转eDMA的通道复用,必须先从整体上理解它的架构和工作流程。这不仅仅是配置几个寄存器那么简单,而是理解数据如何在你的系统中“自动”流动起来。

2.1 eDMA核心引擎:TCD与双循环机制

eDMA的强大,根植于其**传输控制描述符(TCD)双循环(Minor Loop & Major Loop)**机制。每个DMA通道都对应一个32字节的TCD数据结构,存储在eDMA模块内部的SRAM中。你可以把它理解为给这个DMA通道下达的一份“作战计划书”。

这份计划书里定义了所有传输细节:

  • 源地址(SADDR)和目的地址(DADDR):数据从哪里来,到哪里去。
  • 传输属性:包括源和目的的数据宽度(8位、16位、32位)、每次传输后的地址调整方式(递增、递减、固定)。
  • 次循环(Minor Loop):这是最基本的传输单元。NBYTES字段定义了一次服务请求所要传输的总字节数。eDMA引擎会根据源/目的数据宽度,自动计算出需要多少次“读-写”操作来完成这NBYTES个字节的传输。完成一次Minor Loop,称为完成一次“Major Loop迭代”。
  • 主循环(Major Loop)CITER(当前迭代计数)和BITER(起始迭代计数)字段共同定义了Major Loop的次数。每完成一次Minor Loop,CITER减1。当CITER减到0时,意味着整个主传输循环结束。此时,可以触发DMA传输完成中断,并应用主循环的地址偏移量(SLASTDLAST_SGA)。

这种机制的精妙之处在于“一次设置,多次执行”。例如,你想把一块内存缓冲区(比如ADC采样数组)通过SPI发送出去。缓冲区有100个32位数据。你可以设置:NBYTES = 4(一次发1个32位数据),BITER = CITER = 100。启动后,每次SPI发送缓冲区空(触发一次服务请求),eDMA就自动完成一次Minor Loop(传输4字节),CITER减1。重复100次后,整个缓冲区发送完毕,自动触发中断通知CPU。整个过程CPU只需初始化并启动DMA,之后就可以去处理其他任务。

2.2 DMA_MUX:灵活的通道路由器

理解了eDMA这个“执行者”,我们再来看DMA_MUX这个“调度员”。它的结构非常清晰,如图18-1所示(概念图)。简单来说,它内部有16个“路由开关”(对应16个DMA通道),每个开关可以从33个输入源(27个外设Slot + 6个Always-on Slot)中选择一个进行连接。

1. 外设请求槽位(Peripheral Slots, 0-26): 这是最常用的模式。每个槽位固定连接到一个具体的外设DMA请求信号。例如,Slot 1对应DSPI_0的发送缓冲区空标志(TFFF),Slot 2对应其接收缓冲区满标志(RFDF)。当外设需要DMA服务时(比如SPI发送缓冲区空了,需要新数据),它就会通过对应的Slot向DMA_MUX发出请求。DMA_MUX根据配置,将这个请求路由到指定的DMA通道,进而触发eDMA传输。

2. 常使能请求源(Always-on Slots, 28-33): 这是6个特殊的“虚拟”请求源。它们不像外设那样有“忙”或“闲”的状态来“节流”请求。一旦使能,它们会持续地、无条件地向关联的DMA通道发出传输请求。这非常适合几种场景:

  • 内存到内存的搬运:你想以最快速度复制一大块数据。使用Always-on Slot,DMA会连续不断地发起请求,直到完成整个传输(由eDMA的TCD中的CITER控制)。
  • 软件触发传输:当你需要手动启动一次或一系列DMA传输时,可以配置使用Always-on Slot。在软件中启动DMA通道后,这个常使能的请求会立刻触发第一次Minor Loop。
  • 与触发功能结合:将Always-on Slot配置到支持触发功能的前4个通道,并启用周期性触发。这样,就能实现纯定时驱动的DMA传输,即使没有外设事件,也能定时从内存搬数据到某个外设(如GPIO),用于生成精确的波形。

3. 触发功能(仅通道0-3): 这是DMA_MUX的“王牌”功能。对于前4个通道,除了路由,还增加了一个“与门”控制。来自外设或Always-on Slot的DMA请求,必须与一个周期性的触发信号(来自PIT定时器)同时有效时,才能真正传递到eDMA通道。这就实现了定时门控

关键理解:触发不是“产生”请求,而是“放行”请求。外设(或Always-on源)必须首先产生请求。如果此时触发信号有效,请求被放行,DMA传输发生。如果触发信号无效,即使外设一直在请求,也会被阻塞。如果触发信号来了但外设没有请求,这次触发会被忽略(见图18-5)。这种机制确保了DMA传输与一个精准的时钟节拍同步。

3. DMA_MUX配置详解与实战步骤

理论清晰后,我们进入实战环节。配置DMA_MUX的核心,就是正确地操作每个通道的通道配置寄存器(CHCONFIGn)

3.1 寄存器位域精讲

每个CHCONFIG寄存器是8位宽,其结构如下:

位域名称描述
7ENBLDMA通道使能位。0=禁用,1=启用。重要:在修改SOURCETRIG位之前,必须先将此位清零。
6TRIG触发使能位(仅通道0-3有效)。0=禁用触发(透明模式),1=启用周期性触发。
5-0SOURCEDMA通��源选择(槽位编号)。写入0-33之间的值,选择对应的请求源。具体映射需查芯片手册的Table 18-4

ENBLTRIG位的组合,决定了通道的三种工作模式,如Table 18-3所示:

ENBLTRIG功能模式
0XDMA通道被禁用。禁用模式
10DMA通道启用,触发功能关闭。外设请求直接路由到DMA通道。普通模式
11DMA通道启用,且启用周期性触发。请求需与PIT触发信号同步。周期触发模式

3.2 标准配置流程与代码实战

配置DMA_MUX必须遵循严格的步骤,否则可能导致不可预测的行为(如数据损坏)。以下是通用的配置流程,我们结合手册中的示例代码进行解析。

步骤一:确定硬件连接与地址首先,你需要从芯片参考手册中找到DMA_MUX模块的基地址(DMAMUX_BASE_ADDR)。在PXS20中,这个地址是0xFC084000。每个CHCONFIG寄存器是8位宽,从基地址开始连续排列。

// registers.h #define DMAMUX_BASE_ADDR 0xFC084000 volatile uint8_t * const DMAMUX_CHCONFIG0 = (volatile uint8_t *)(DMAMUX_BASE_ADDR + 0x00); volatile uint8_t * const DMAMUX_CHCONFIG1 = (volatile uint8_t *)(DMAMUX_BASE_ADDR + 0x01); // ... 以此类推,直到 CHCONFIG15 volatile uint8_t * const DMAMUX_CHCONFIG15 = (volatile uint8_t *)(DMAMUX_BASE_ADDR + 0x0F);

注意:手册示例中使用了unsigned char *和直接指针运算。在实际工程中,更推荐使用volatile uint8_t *以明确类型,或者直接使用芯片厂商提供的SDK中的寄存器定义结构体,这样更安全、可读性更好。

步骤二:配置eDMA通道本身在配置DMA_MUX之前,必须先完成对应eDMA通道的TCD配置。这包括设置源/目的地址、传输属性、循环次数等。并且,通常建议在DMA_MUX使能通道前,先在eDMA模块中禁用该通道(清除DMA_ERQL寄存器中对应的位),防止配置过程中产生意外的传输。

步骤三:配置DMA_MUX通道这是核心步骤。假设我们要将DSPI_0的发送请求(Slot 1)路由到DMA通道2,并希望使用PIT定时器进行周期性触发。

  1. 禁用目标通道:向CHCONFIG2写入0x00,确保ENBLTRIG位为0。

    *DMAMUX_CHCONFIG2 = 0x00; // 禁用通道2
  2. 配置PIT定时器:去配置PIT模块,设置你想要的触发周期。例如,设置PIT通道0产生一个10kHz的触发信号。这一步必须在使能DMA_MUX触发前完成。

  3. 使能并配置通道:计算要写入CHCONFIG2的值。

    • ENBL (bit7) = 1
    • TRIG (bit6) = 1(启用触发)
    • SOURCE (bit5-0) = 1(对应Slot 1,即DSPI_0_TFFF)
    • 因此,写入的值为:(1<<7) | (1<<6) | 1 = 0x80 | 0x40 | 0x01 = 0xC1
    *DMAMUX_CHCONFIG2 = 0xC1; // 使能通道2,启用触发,源为Slot 1

    手册示例勘误与理解:手册Example 18-1中写的是0xC5,这对应的是Source #5。你需要根据自己实际使用的外设Slot号来计算。0xC5ENBL=1,TRIG=1,SOURCE=5的结果。

步骤四:启动传输完成以上配置后,你需要:

  1. 在eDMA模块中使能通道2(设置DMA_ERQL对应位)。
  2. 启动PIT定时器。
  3. 确保DSPI_0的发送缓冲区为空(或手动写入第一个数据启动发送流程)。

一旦PIT的触发信号到来,并且SPI发送缓冲区为空(产生DMA请求),DMA传输就会被触发,eDMA将根据TCD的设置,自动从源地址(通常是内存中的数组)搬运数据到SPI数据寄存器。

3.3 模式切换与通道重配

在实际系统中,可能需要动态改变DMA通道的配置。绝对禁止在通道使能时直接修改SOURCETRIG位。必须遵循以下安全流程:

  1. 在eDMA侧禁用该通道:清除DMA_ERQL对应位,停止接受新的服务请求。
  2. 等待当前传输完成:可以通过检查eDMA的完成状态位(DMA_INTL)或TCD中的CITER是否为0,确保通道空闲。
  3. 在DMA_MUX侧禁用通道:将对应CHCONFIGn寄存器的ENBL位清零。
  4. 重新配置:修改CHCONFIGnSOURCETRIG位。
  5. 重新使能:先使能DMA_MUX通道(设置ENBL),再使能eDMA通道。

4. 高级应用场景与设计模式

理解了基本配置,我们来看看如何利用DMA_MUX和eDMA的双循环机制,构建一些高效的应用模式。

4.1 场景一:周期性SPI数据采集与发送

这是触发功能最典型的应用。假设我们需要用SPI1以固定的1ms周期,向外设发送一组命令字,并同时读取响应数据。

  • 硬件连接SPI1的发送(TFFF, Slot 3)和接收(RFDF, Slot 4)请求。
  • DMA通道分配
    • 通道0:用于发送,配置为周期触发模式,源为Slot 3,触发源为PIT通道0(1ms)。
    • 通道1:用于接收,配置为普通模式,源为Slot 4。
  • eDMA TCD配置
    • 发送通道 (Ch0)
      • SADDR: 指向内存中命令字数组。
      • DADDR: 指向SPI1数据发送寄存器。
      • SOFF/DOFF: 每次传输后,源地址递增(命令字大小),目的地址固定。
      • NBYTES: 每个命令字的大小(如2字节)。
      • CITER/BITER: 命令字数组的长度。当CITER减到0,主循环结束,可以触发中断,并用SLAST将源地址重新调整到数组开头,为下一轮发送做准备。
    • 接收通道 (Ch1)
      • SADDR: 指向SPI1数据接收寄存器。
      • DADDR: 指向内存中接收数据缓冲区。
      • SOFF/DOFF: 源地址固定,目的地址递增。
      • NBYTES: 每个响应数据的大小。
      • CITER/BITER: 与发送数组长度相同(一问一答)。
  • 工作流程
    1. 初始化所有配置,使能通道。
    2. PIT定时器每1ms产生一个触发信号。
    3. 触发信号到来时,若SPI发送缓冲区空,则触发发送通道的DMA传输,将一个命令字送入SPI。
    4. SPI硬件在发送命令字的同时,也会接收从机数据。当接收缓冲区满时,自动触发接收通道的DMA传输,将数据搬入内存缓冲区。
    5. 当主循环计数结束时,触发中断,CPU可以处理接收到的完整一帧数据,并准备下一轮命令字。

这样,CPU只在每帧数据收发完成后才被中断一次,期间可以处理其他任务,SPI通信的时序由硬件和PIT精确保证。

4.2 场景二:使用Always-on Slot与触发生成复杂GPIO波形

如果你想用GPIO口模拟一个复杂的协议波形(如WS2812B LED的时序),DMA_MUX的触发模式配合Always-on Slot是绝佳选择。

  • 目标:用GPIO的某个引脚,输出一个精确的、由一系列高低电平持续时间定义的波形。
  • 设计
    1. 内存中的波形表:在内存中创建一个数组(waveform_buffer[]),数组中的每个元素对应GPIO输出寄存器需要被设置的值(例如,1为高,0为低)。更重要的是,这个数组的排列顺序,要使得每次DMA传输改变GPIO状态后,能产生所需的波形。
    2. DMA配置
      • 使用一个支持触发的DMA通道(如通道2)。
      • 在DMA_MUX中,将该通道的SOURCE配置为一个Always-on Slot(例如Slot 28)。因为GPIO本身不产生DMA请求,我们需要一个永不间断的请求源。
      • 使能TRIG,并配置PIT定时器产生一个高分辨率的触发信号(例如1MHz,即1us周期)。这个周期决定了波形时间的最小分辨率。
    3. eDMA TCD配置
      • SADDR: 指向waveform_buffer
      • DADDR: 指向GPIO数据输出寄存器(GPIOx_PDOR)。
      • SOFF: 每次传输后递增(指向数组下一个元素)。
      • DOFF: 0(目的地址始终是同一个GPIO寄存器)。
      • NBYTES: 4(假设是32位写入)。
      • CITER/BITER: 等于波形表中元素的数量。
      • DLAST_SGA:-4 * CITER(主循环结束后,将目的地址回退到GPIO寄存器,其实DOFF=0时这里通常设0即可,因为地址没变,但为了严谨可以设置)。
      • SLAST:-4 * CITER(主循环结束后,将源地址重置回波形表开头,实现循环播放)。
    4. 工作流程
      • 启动后,PIT每1us产生一个触发。
      • 由于Always-on Slot始终有请求,因此每个触发都会导致一次DMA传输。
      • DMA将波形表中的下一个值写入GPIO寄存器,从而改变引脚电平。
      • 通过精心设计波形表(每个值代表一个时间片段的电平),就能合成出任意复杂的数字波形。CPU仅在需要更换波形表时才需要介入。

4.3 场景三:链式DMA与分散/聚集(Scatter/Gather)

eDMA支持通道链接(Channel Linking),即一个通道传输完成后,可以自动启动另一个通道。这在处理非连续内存块的数据时非常有用。DMA_MUX的Always-on Slot可以在此扮演“软件触发链”的角色。

例如,你需要将数据从三个不连续的内存区域(buf1,buf2,buf3)搬运到外设。你可以配置三个DMA通道(Ch4, Ch5, Ch6),每个通道负责搬运一个缓冲区。

  • 将Ch4的SOURCE配置为软件启动或一个事件启动。
  • 在Ch4的TCD中,设置主循环完成后,启用通道链接(ELINK),并链接到Ch5。
  • 同样,在Ch5的TCD中链接到Ch6。
  • 在Ch6的TCD中,设置传输完成后产生中断。

启动Ch4后,它会自动搬运buf1,完成后自动启动Ch5搬运buf2,再自动启动Ch6搬运buf3,全部完成后产生一个中断通知CPU。整个过程只需要一次初始启动,实现了多个非连续传输的自动化流水。

5. 调试技巧与常见问题排查

在实际开发中,DMA相关的问题往往比较隐蔽,因为数据传输在后台进行。以下是一些实用的调试方法和常见坑点。

5.1 调试技巧

  1. “先CPU,后DMA”验证法:在启用DMA之前,先用CPU轮询或中断的方式,让外设(如SPI、ADC)工作起来,确保底层外设驱动和硬件连接是正确的。然后再将数据传输部分切换到DMA模式。
  2. 利用完成中断:务必为DMA通道使能主循环完成中断。在中断服务程序(ISR)里设置标志位、增加计数器或切换缓冲区。这是判断DMA是否正常工作的最直接证据。
  3. 内存内容检查:对于接收DMA,在预期数据应该到达的内存区域设置断点,或者定期通过调试器查看该内存区域的内容。确认数据是否被正确写入,有无错位、丢失或覆盖。
  4. 寄存器状态监控:在调试器中密切关注以下寄存器:
    • eDMA状态寄存器DMA_ERRL(错误)、DMA_INTL(完成中断请求)、DMA_HRSL(硬件请求状态)。看看是否有错误标志被置起,或请求是否被正确识别。
    • DMA_MUX配置寄存器:确认CHCONFIGnENBLTRIGSOURCE值是否正确。
    • TCD内存:直接查看对应通道的TCD结构体内容,特别是CITER的值是否在递减,SADDR/DADDR是否按预期变化。
  5. 逻辑分析仪/示波器:这是最强大的工具。直接抓取外设的通信引脚(如SPI的SCK、MOSI)和可能的DMA请求/应答信号(如果引脚复用),可以直观地看到DMA传输的时机、数据内容以及与触发信号的同步关系。

5.2 常见问题与解决方案

下表列出了一些典型问题及其排查思路:

问题现象可能原因排查步骤与解决方案
DMA传输完全没发生1. DMA通道未使能。
2. DMA_MUX通道未使能或源配置错误。
3. 外设的DMA请求未产生。
4. TCD配置错误,如NBYTES为0。
1. 检查DMA_ERQL寄存器对应位。
2. 检查DMAMUX_CHCONFIGnENBLSOURCE
3. 检查外设配置:是否使能了DMA请求(如SPI的TFFF/RFDFDMA使能位)?外设是否处于活动状态(如SPI已使能)?
4. 仔细核对TCD所有字段,特别是CITER/BITERNBYTES
DMA只传输一次就停止1.CITER初始值设为1。
2. 主循环完成后未重新加载CITERELINKDONE位处理不当)。
3. 触发模式或请求源问题。
1. 确认BITERCITER为期望的循环次数。
2. 如果希望连续循环,需配置ELINK链接到自身,或在中断中手动重装CITER并清除DONE位。
3. 在触发模式下,确认PIT定时器是否持续产生触发信号。对于外设源,确认请求是否持续产生(如SPI发送是否持续进行)。
DMA传输数据错乱(地址/数据不对)1.SADDR/DADDR初始值错误。
2.SOFF/DOFF地址偏移设置错误。
3.SSIZE/DSIZE数据宽度设置与外设或内存不匹配。
4. 字节序(Endianness)问题。
1. 检查并打印SADDR/DADDR的初始值。
2. 根据数据宽度计算SOFF/DOFF。例如,传输uint16_t数组,SSIZE为16位,则SOFF应设为2。
3. 确保外设数据寄存器宽度与DSIZE匹配,内存访问宽度与SSIZE匹配。
4. 对于涉及字节交换的传输,检查是否需启用eDMA的字节交换功能,或手动处理。
周期性触发模式不工作1.TRIG位未使能。
2. PIT定时器未配置或未启动。
3. DMA请求源(外设或Always-on)无效。
4. 通道编号错误(触发仅通道0-3支持)。
1. 确认CHCONFIGnTRIG=1
2. 确认PIT对应通道已配置、使能,且触发周期合理。
3. 对于外设源,确保外设能产生请求。对于Always-on源,确认SOURCE选择正确(28-33)。
4. 确认使用的DMA通道编号是0,1,2,3之一。
系统进入硬件错误(HardFault)1. 访问了非法内存地址(SADDR/DADDR指向无效区域)。
2. TCD结构体所在内存被意外修改(如栈溢出)。
3. DMA传输期间访问了被DMA和CPU共同访问的临界资源(如同一内存区)而未加保护。
1. 使用调试器检查SADDR/DADDR值是否在有效的物理地址范围内。
2. 将TCD结构体或DMA缓冲区放在不会被栈或其它代码覆盖的区域(如全局变量区或专用内存段)。
3. 对共享数据区使用互斥锁(Mutex)、关中断或使用双缓冲区机制。

5.3 性能优化与注意事项

  • 总线仲裁与带宽:eDMA作为总线主设备,与CPU和其他主设备共享系统总线。频繁的、大数据量的DMA传输可能会阻塞CPU对内存的访问,导致CPU性能下降。在设计时,需要评估总线带宽,必要时调整DMA的优先级(通过DCHPRIn寄存器),或使用更宽的数据宽度(如32位替代8位)来减少传输次数。
  • 数据一致性:对于Cache使能的系统,必须注意DMA缓冲区所在内存区域的数据一致性。如果CPU写了数据到缓冲区(Cache),而DMA直接从内存(可能还是旧数据)读取,就会出错。通常需要将DMA缓冲区定义为非缓存(Non-cacheable)区域,或者在DMA操作前后执行缓存清洗(Clean)或无效化(Invalidate)操作。
  • 中断风暴:如果DMA传输非常频繁且每次主循环结束都产生中断,可能会导致系统被中断淹没。对于高速流式数据,考虑使用“双缓冲区”或“环形缓冲区”模式。DMA在后台循环填充缓冲区,CPU在另一个缓冲区处理数据,通过半满或全满标志进行同步,可以大幅减少中断频率。
  • TCD动态更新:在DMA传输过程中,如果CPU需要修改正在使用的TCD,必须极其小心。安全的做法是:先禁用该DMA通道(在eDMA和DMA_MUX侧),等待当前传输完成,然后修改TCD,最后重新使能通道。更高级的做法是使用eDMA的“散射/聚集”功能,提前准备好下一个TCD,让DMA在完成当前传输后自动加载。

配置eDMA和DMA_MUX就像在给一个强大的协处理器编写微程序。初看寄存器众多、概念复杂,但一旦掌握其“描述-触发-执行”的核心逻辑,就能极大地解放CPU,构建出响应迅速、效率卓越的嵌入式系统。从简单的内存搬运,到复杂的定时、触发、链式传输,这套机制为处理实时数据流提供了几乎无限的灵活性。关键在于多实践,从简单的例子开始,逐步增加复杂度,并善用调试工具观察其内部状态,你就能越来越得心应手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询