找回密码
立即注册
搜索
发回帖 发新帖

5237

积分

0

好友

675

主题
发表于 2 小时前 | 查看: 8| 回复: 0

给音频接口接入 DMA,配置里经常会出现这样的两行:

.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.src_maxburst   = 8,

一个是 4,一个是 8。它们到底表示一次搬 4 字节、8 字节,还是 32 字节?外设 FIFO 明明只有一个数据寄存器地址,为什么重复读取它,拿到的却是后续数据?设备树里写了 DMA 请求号,为什么实际分配到的通道编号又不一样?

这些问题不是 API 记忆问题,而是硬件模型还没有接起来。上一篇讨论软件怎样组织 DMA 任务;这一篇往下走一层,看看外设、控制器和总线之间,究竟怎样完成一次搬运。

本文仍以 drivers/dma/dw-axi-dmac/ 的实现为主线,在 Burst 配置处用 pl330.c 做一个小范围对照。示意图中的 FIFO 深度、请求编号和音频参数都是明确给出的教学假设,不对应某一块开发板。寄存器结论以文末列出的源码位置为边界。

一、先把五个名字放进同一条硬件链路

假设一个 I2S 接收接口不断收到音频数据,采用独立 DMA 控制器把数据写入 RAM。先不考虑描述符队列,只看硬件协作。

外设内部的接收逻辑把数据送进 FIFO。FIFO 积累到满足条件时,外设向 DMA 控制器提出请求;控制器中已经配置好的通道获得服务机会,通过总线读取 FIFO 数据端口,再把数据写到内存。

外设请求与通道执行机制架构图

图 1:虚线是请求或配置关系,粗实线是有效载荷路径。控制器内的暂存说明只表示可能存在内部缓冲,不表示所有控制器都采用相同的独立 FIFO 实现。

这五个名词分别回答不同的问题:

名词 回答的问题 先记住的含义
通道 Channel 由哪条执行资源承担传输? 保存和执行一组传输状态,不是一根数据线
请求线 Request 哪个外设端点现在需要服务? 提供请求或握手信息,不承载音频样本
FIFO 数据为什么可以暂时等一等? 先到先出的有限缓冲队列
位宽 Transfer width 某一侧一次数据访问按多大单位进行? 必须带单位,并区分源端和目标端
Burst 多次访问怎样成组组织? 要说明是接口参数、控制器分组,还是总线事务

通道与请求的区分、传输位宽、地址递增方式,都属于 DMA 控制器驱动必须处理的基本硬件属性。但把几个术语放进表格还不够,下面逐个拆开。

二、通道是执行资源,请求线是服务对象

2.1 一条通道不是"一次只能搬一个字节的管子"

从驱动视角看,一条硬件通道至少需要维护当前传输使用的地址、控制参数、计数和运行状态;支持链表的控制器还需要知道去哪里读取下一项硬件描述符。

DW AXI DMAC 的私有通道结构中,下面几个成员就把这些关系分开了。代码摘录自 dw-axi-dmac.h,省略了其他成员:

struct axi_dma_chan {
    struct axi_dma_chip *chip;
    void __iomem *chan_regs;
    u8 id;
    u8 hw_handshake_num;
    /* 其他成员省略 */
    struct dma_slave_config config;
    enum dma_transfer_direction direction;
};

id 标识这里的硬件通道;chan_regs 指向这条通道的寄存器窗口;hw_handshake_num 记录外设握手请求编号。config 和 direction 则保存后续构造任务时需要使用的配置。

初始化时,dw_probe() 为通道设置 id = i,并按通道步长计算 chan_regs。这说明本实现确实有按硬件通道划分的寄存器窗口;不能把这个结论直接推广为所有 DMAEngine 驱动的软件通道都与物理通道一一对应。

2.2 请求号为什么不等于通道号?

可以把通道理解为可分配的执行席位,把请求号理解为外设端点的编号。假设一台控制器有 4 条通道,SoC 给它接入 8 路外设请求,并提供相应的选择或路由能力:请求 5 可以交给通道 1 服务,并不要求必须存在"通道 5"。

请求号与通道号映射路由示意图

图 2:仅示意一种允许路由的控制器。哪些请求能连到哪些通道、是否存在固定连接,需要查目标 SoC 的连接关系,不能由此推断成任意全互连。

在本文的 dw_axi_dma_of_xlate() 中,先申请可用通道,再记录设备树参数:

dchan = dma_get_any_slave_channel(&dw->dma);
if (!dchan)
    return NULL;

chan = dchan_to_axi_dma_chan(dchan);
chan->hw_handshake_num = dma_spec->args[0];
return dchan;

例如,把 <&dmac 5> 当作一个纯示意配置:在这条转换路径中,5 被保存为握手请求号,不是"必须选择物理通道 5"。这与"先挑一个能工作的通道,再让它响应指定请求"的组织方式一致。

启动时还要继续跟踪请求号写到哪里。axi_chan_block_xfer_start() 在普通路径中,根据方向把 hw_handshake_num 放入 src_per 或 dst_per。带额外 APB 寄存器的分支有所不同:通道配置使用 chan->id,另由 dw_axi_dma_set_hw_channel() 把请求号写进握手选择寄存器。

因此,看到某个字段最终写了通道号,不能马上认定设备树参数就是通道号;中间可能还有一层硬件路由。

2.3 通道多,不等于带宽按数量翻倍

多条通道可以分别维护多组任务,但它们可能共享总线主接口、片上互连和内存控制器。执行资源的数量与最终可用带宽不是同一个量。

这里的硬件配置也把 nr_channels 和 nr_masters 分成了两个成员,分别解析 dma-channels 与 snps,dma-masters。即使请求分配到了不同通道,也仍需考虑共享路径上的仲裁。

对驱动开发来说,先回答"任务是否能分配到执行资源",再回答"这些任务同时工作时能否得到足够服务"。不要用通道数直接估算吞吐量。

三、FIFO:地址不动,数据为什么还能向前走?

3.1 数据端口是队列的入口或出口,不是整块队列的地址表

FIFO 是 First In, First Out,即先进先出。可以把它看成一个有限长度的排队区:最先进入的数据,最先被取走。

对采用单一数据端口的外设,DMA 看到的是一个固定寄存器地址。每次合法地读取 RX 数据端口,外设内部读指针向前推进,返回下一项数据;写入 TX 数据端口时,新数据则进入发送队列。

变化的是 FIFO 内部的队列状态,不是外部访问地址。 AXI 规范也把 FIFO 列为读访问可能产生副作用的设备:多读一次,就可能多取走一项,而不是"多读了也没关系"。

固定地址反复访问FIFO示意图

图 3:假设每次数据端口访问恰好取走一项 4 字节数据。F 表示固定的 FIFO 数据端口地址,M 表示已建立访问条件的内存起始地址;不是具体平台地址。

假设 RX FIFO 中依次是 A、B、C、D,每项 4 字节,传入内存时的逻辑效果为:

次数 读取地址 取出的数据 写入地址
1 F A M
2 F B M + 4
3 F C M + 8
4 F D M + 12

若错误地让外设地址递增,下一次可能访问到状态寄存器、控制寄存器或保留地址;若错误地让目标内存地址固定,则多项数据可能反复写在同一位置。具体后果取决于外设寄存器布局,但两种配置都已经偏离了"把队列连续保存到内存"的目标。

3.2 三种常见方向,地址策略不同

传输方向 源地址 目标地址 典型任务
DMA_DEV_TO_MEM 外设数据端口固定 内存递增 录音、串口接收
DMA_MEM_TO_DEV 内存递增 外设数据端口固定 播放、串口发送
DMA_MEM_TO_MEM 内存递增 内存递增 普通连续块复制

这是单数据端口、连续内存的常见模型,不覆盖带寄存器窗口、二维步长或其他特殊访问规则的设备。本文控制器的 Slave 和 memcpy 准备路径正好提供了这三种策略的实例。

其中,RX 分支在 dw_axi_dma_set_hw_desc() 中这样组织控制位:

case DMA_DEV_TO_MEM:
    reg_width = __ffs(chan->config.src_addr_width);
    device_addr = chan->config.src_addr;
    ctllo = reg_width << CH_CTL_L_SRC_WIDTH_POS |
        mem_width << CH_CTL_L_DST_WIDTH_POS |
        DWAXIDMAC_CH_CTL_L_INC << CH_CTL_L_DST_INC_POS |
        DWAXIDMAC_CH_CTL_L_NOINC << CH_CTL_L_SRC_INC_POS;
    block_ts = len >> reg_width;
    break;

这一段里,源端使用外设寄存器位宽并禁止递增,目标端使用内存侧位宽并允许递增。后续代码再把 device_addr 写入源地址字段,把 mem_addr 写入目标地址字段。

反向发送时,对应关系翻转:源内存递增,目标数据端口固定。并不是把 RX 配置里的地址对调一下就必然足够,源端与目标端的位宽、握手选择和流控方向也要一起核对。

3.3 FIFO 深度不等于数据寄存器宽度

"32 位数据端口"和"32 项 FIFO"描述的是两件事。前者说一次端口访问的宽度,后者说内部最多能存多少项。

若每项占 4 字节,16 项 FIFO 的容量就是 64 字节。深度也可能以字节、样本或其他单元给出,读取手册时必须确认单位,不能只记一个数字。

还要分开外设 FIFO、DMA 控制器内部暂存区、RAM 缓冲区与 CPU Cache。它们可能同时存在,但容量、访问方式和承担的任务不同。外设 FIFO 装不下的输入数据,不会因为 RAM 中分配了一个更大的缓冲区,就自动获得额外等待空间。

四、请求线与 FIFO 水位:什么时候该搬,来不来得及?

4.1 RX 等数据积累,TX 等空间腾出来

先采用一种常见的水位模型:RX FIFO 的数据项数达到门限时提出接收请求;TX FIFO 剩余数据降到门限时提出补数请求。门限的精确比较方式、是否使用"空闲项数"表示,以及请求的撤销条件,都由具体外设定义。

假设 FIFO 深度为 16 项,每项 4 字节。RX 在已有 8 项时请求搬出,TX 在剩余 8 项时请求补入。暂时忽略这段时间内新数据到达或旧数据消耗,一次搬 4 项后,RX 从 8 项降到 4 项,TX 从 8 项升到 12 项。

FIFO给DMA留下时间余量示意图

图 4:假定两种模式都在 8 项水位提出请求。图中瞬时搬动 4 项只是帮助计算占用变化,不代表真实传输没有时间开销。

FIFO 的价值不是把外设变成无限快,而是容纳短时间的服务延迟。接收端怕迟迟无人读取而溢出,发送端怕迟迟没有补数而耗尽。

4.2 请求不等于完成中断,也不是"CPU 收到通知才开始搬"

DMA 请求进入的是控制器的请求或握手机制;DMA 完成或出错中断进入 CPU 的中断处理路径。二者的对象和时机不同。

请求可能持续有效,也可能通过控制器规定的握手协议交互;不能把它统一画成"一个脉冲对应一个字节",更不能认为一次请求就是整笔任务已经完成。本文启动路径选择硬件握手,同时设置传输完成与错误中断掩码,这两类配置在代码中也是分开的。

这带来一个很实用的判断:降低 FIFO 请求门限可能增加 DMA 服务频次,却不一定按相同比例增加 CPU 完成中断次数。 中断粒度还要看块、描述符、循环周期以及具体驱动的设置,而不是只看请求线跳变。

4.3 用容量除以速率,先估算"还能等多久"

设 FIFO 总深度为 D 项,每项 W 字节;RX 请求时已有 H 项,输入速率为 R 字节/秒。忽略离散项边界、请求传播及已在途数据,若 DMA 暂时完全不读取,则到 FIFO 填满的名义时间为:

RX 名义等待窗口 = (D - H) × W / R

TX 请求时若还剩 L 项,输出速率为 R 字节/秒,且 DMA 暂时完全不补数,则到数据耗尽的名义时间为:

TX 名义等待窗口 = L × W / R

两式只是静态预算,工程上必须留余量;"第一次读写赶上了"也不够,后续持续服务还要跟得上。

沿用上面的假设,再设音频为 48 kHz、双声道,每个声道样本在 FIFO/内存中占 4 字节,则搬运速率为:

R = 48,000 × 2 × 4 = 384,000 B/s
RX 空闲空间 = (16 - 8) × 4 = 32 B
RX 名义等待窗口 = 32 / 384,000 ≈ 83.3 μs

TX 在剩余 8 项时也有相同的名义窗口。若把 RX 门限提高到 12 项,剩余空间只有 16 字节,名义窗口缩短为约 41.7 μs。

这个算例说明了一个容易与直觉相反的事实:接收门限设得更高,虽然每次可以积累更多数据,却压缩了 DMA 来不及服务时的缓冲余量。 这不是"门限越大越高效"就能决定的参数。

4.4 硬件握手不等于外设担任流控制器

在本文启动函数中,硬件握手选择与 device_fc 是不同配置:

config.hs_sel_dst = DWAXIDMAC_HS_SEL_HW;
config.hs_sel_src = DWAXIDMAC_HS_SEL_HW;

对于 RX,后面的代码还会根据 chan->config.device_fc 在 PER_TO_MEM_SRC 和 PER_TO_MEM_DMAC 两种模式之间选择。前者对应源外设控制,后者对应 DMA 控制器控制。device_fc 的通用含义也正是选择外设是否充当流控制器。

不要因此把 device_fc = false 解释成"不再响应外设请求"或"忽略 FIFO 水位"。它决定的是控制器提供的流控模式,不是外设 DMA 请求的总开关。

外设自身还需要正确配置数据端口、FIFO 门限与 DMA 请求使能;这些不由一次 dmaengine_slave_config() 自动代办。外设握手怎样表示最后一批、余数和结束条件,需要继续查对应硬件定义。

五、位宽:先问是哪一侧,再问单位是什么

5.1 24 位音频,不一定要求 3 字节 DMA 访问

"音频是 24 位"通常只说明有效采样精度。一个具体设计完全可能把 24 位有效样本放在 32 位容器中,通过 32 位 FIFO 数据端口访问,再连接到 64 位片上总线。

这几个数字可以同时成立,因为它们描述的对象不同。

同一份数据不同宽度的层级示意图

图 5:示例采用 24 位有效数据、32 位容器、4 字节端口访问和 64 位物理总线。填充或符号扩展的位置、左右对齐和字节序由音频格式及外设定义;图中不替代具体格式约定。

层次 示例 决定什么
有效样本精度 24 bit 哪些位代表有效音频数值
FIFO/内存容器 32 bit 每项数据怎样排列、占多少空间
外设端口访问宽度 4 byte 一次合法数据端口访问的单位
内存侧传输宽度 可能与外设侧相同,也可能不同 DMA 如何组织另一侧访问
物理数据总线宽度 64 bit 接口具有多少数据位,不保证每次全部用满

因此不能直接按样本精度填 src_addr_width = 3,也不能因为总线是 64 位就填 8。应该从外设数据端口的合法访问方式和数据布局出发,再与控制器支持的位宽组合核对。

位宽转换、数据打包也不是免费的通用保证。控制器是否支持源窄目标宽、怎样处理尾部和字节顺序,需要看硬件能力及驱动路径。

5.2 API 用字节数,寄存器可能用编码

dma_slave_config.src_addr_width 和 dst_addr_width 的单位是字节。对采用 2 的幂字节宽度的这条 DW 路径,__ffs() 把字节数变成对应的位宽编码。

合法的幂次宽度示例 二进制字节数 __ffs() 结果 本控制器枚举含义
1 byte 0001 0 8 bit
2 byte 0010 1 16 bit
4 byte 0100 2 32 bit
8 byte 1000 3 64 bit

__ffs(4) = 2,所以把编码 2 写进宽度位域,表达的是每次 4 字节,不是 2 字节。这里的转换不是对任意整数都求数学对数:传入 3,最低置位的位置是 0,绝不等价于支持 24 位传输;零值也不能交给这段逻辑当作普通位宽。

API 参数、寄存器编码和最终硬件含义,需要分成三列来看。 位宽如此,后面的 Burst 长度和块计数也一样。

5.3 源码还会根据地址和长度选择内存侧位宽

dw_axi_dma_set_hw_desc() 的 Slave 路径先计算内存侧位宽:

mem_width = __ffs(data_width | mem_addr | len);
if (mem_width > DWAXIDMAC_TRANS_WIDTH_32)
    mem_width = DWAXIDMAC_TRANS_WIDTH_32;

if (!IS_ALIGNED(mem_addr, 4)) {
    dev_err(chan->chip->dev, "invalid buffer alignment\n");
    return -EINVAL;
}

这里的按位或把硬件上限、地址对齐和长度对齐一起纳入选择:只要其中一个因素要求较小的单位,就不能仅凭总线宽度采用更大的访问单位。

还应读出两条仅限这份实现的约束:这条 Slave 路径把内存侧传输宽度编码限制到 32 位,并显式要求内存起始地址按 4 字节对齐。不能据此声称所有 DMA 缓冲区都必须 4 字节对齐,也不能用"地址对齐了"替代对长度、端口访问宽度和尾部处理的检查。

相邻的 memcpy 路径使用另一套宽度选择过程,并在注释中说明为了简化而把源、目标位宽取成相同值。同一控制器的不同传输路径,也未必具有完全相同的位宽策略。

六、Burst:不是位宽,不是总长度,也不是时钟周期数

6.1 先定义 beat:一次被接受的数据传输

讨论总线时,beat 通常指一次数据传输。在 AXI 数据通道上,只有有效与就绪握手同时满足,才真正完成这一拍的数据交接;如果接收方暂时不接受,数据可以在若干周期内等待。

所以"4 beats"表示完成 4 次数据交接,不保证只用了 4 个时钟周期。图中用时钟边沿采样表表示一个带等待的例子,而不是把等待时重复呈现的数据算成新数据。

4 beats与时钟周期关系示意图

图 6:每 beat 4 字节,4 beats 共 16 字节;中间等待两个周期,总共跨越 6 个采样时刻。此图只讨论数据交接,省略地址阶段及响应。

在起始地址对齐、每拍各字节均有效等简化条件下,可以这样换算:

单个 burst 的数据量 = 每 beat 字节数 × beat 数

每 beat 4 字节、8 beats,对应 32 字节。它表示这一组的数据量,并不表示整笔 DMA 任务只有 32 字节,也不意味着每次都必须组织出同样大小的组。

遇到窄传输、非对齐访问或写字节使能时,实际有效字节数还要按对应总线规则计算,不能只拿物理总线宽度乘长度。

6.2 maxburst 的单位是"宽度单位的个数"

src_maxburst 与 dst_maxburst 的 API 语义是对应一侧的最大成组传输数量,计数单位取自对应的 addr_width,不是字节。

回到开头的例子:

.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.src_maxburst   = 8,

它表达的是源端每个单位 4 字节,最多按 8 个这样的单位组织一次 burst,也就是上限按 32 字节理解。前提是控制器驱动确实支持并落实这个配置。

把 src_maxburst = 32 当作"我想每组搬 32 字节",在宽度为 4 字节时就把请求写成了 128 字节。这个单位错误经常比算法本身更难发现。

同时,maxburst 不是整笔任务长度。任务长度由准备接口及其缓冲区描述提供;一个较大的任务通常需要多组成组传输。

6.3 三种"Burst"不能直接画等号

所在层次 本文出现的字段 应该怎样理解
DMAEngine 配置 src_maxburst、dst_maxburst 客户端提出的端点成组传输限制
DW 控制器配置 SRC_MSIZE、DST_MSIZE 控制器源、目标侧的成组传输参数
AXI 总线事务 ARLEN、AWLEN,以及 ARSIZE、AWSIZE 总线读写事务的 beat 数与每 beat 字节数

这张表不是说它们彼此无关,而是说关系需要由具体控制器的实现建立。DMA 内部缓存、位宽转换、总线桥及长度限制,都可能让端点访问分组与内存侧看到的 AXI 事务边界不同。

还有一个命名陷阱:AXI 规范里的 AxSIZE 描述每个 beat 的字节数编码,不是一个 burst 里有多少 beats。AxLEN 才描述长度,标准编码是 beat 数 - 1。

6.4 Burst 大一些,为什么可能更高效,又为什么不能无限增大?

把多次访问放进同一组,有机会摊薄控制开销,减少过多的小事务。若把一次组传输简化为固定开销 C 加 N 次数据交接,且忽略等待,则控制开销占比为 C / (C + N)。在这个模型里,N 增大时占比下降。

但这只是解释"为什么有优化空间"的模型,不是实际带宽公式。真实传输会受到端点可接收量、总线竞争、转换桥、在途事务数量和尾部长度的影响。

尤其对于 FIFO:若某次服务只保证能读出 8 项或容纳 8 项,就不能在没有额外流控保证的情况下要求连续访问 16 项。也不能把硬件握手当成万能防护,假设它一定会替任何不合法的批量配置自动拆分。

AXI 的单个 burst 还不能跨越 4 KiB 地址边界。这是单个总线事务的约束,不表示 DMA 任务或硬件链表项最多只有 4 KiB。更大的搬运需要被组织成满足总线约束的事务;具体由控制器和互连怎样实现,不能脱离硬件推断。

七、回到源码:参数填写之后,真的生效了吗?

7.1 slave_config() 返回成功,不代表所有字段已经写入硬件

本文 DW AXI DMAC 的配置回调很短:

static int dw_axi_dma_chan_slave_config(struct dma_chan *dchan,
                                       struct dma_slave_config *config)
{
    struct axi_dma_chan *chan = dchan_to_axi_dma_chan(dchan);

    memcpy(&chan->config, config, sizeof(*config));
    return 0;
}

它先保存配置,后续准备任务、启动通道时才使用相关字段。这里没有为每个参数执行完整的合法性验证,也没有立即把每一项配置写进寄存器。

所以看到返回 0,只能按这段实现理解为配置结构已经保存,不能得出"传输一定支持""所有字段一定生效"或"硬件已经开始搬运"的结论。

7.2 这份 DW AXI DMAC 把 MSIZE 固定为 4 次传输

继续跟到 dw_axi_dma_set_hw_desc(),可以看到:

ctllo |= DWAXIDMAC_BURST_TRANS_LEN_4 << CH_CTL_L_DST_MSIZE_POS |
         DWAXIDMAC_BURST_TRANS_LEN_4 << CH_CTL_L_SRC_MSIZE_POS;

头文件对应的枚举从 0 起依次编码 1、4、8、16 等传输长度。也就是说,DWAXIDMAC_BURST_TRANS_LEN_4 的枚举值为 1,硬件含义却是 4 次传输,不是 1 次。

检查这份 dw-axi-dmac/ 的代码,没有发现直接读取 src_maxburst 或 dst_maxburst 的位置;配置被整体保存,但这里的源、目标 MSIZE 使用固定常量。memcpy 准备路径也采用相同的 MSIZE 常量。

因此,单独把客户端的 src_maxburst 从 4 改成 8,不能据此宣称这份 DW 驱动就会把 SRC_MSIZE 改成 8。 这是本实现的结论,不是 DMAEngine 的统一规则,也不是对所有版本 DW AXI DMAC 的断言。

7.3 PL330 给出了一条不同的落实路径

pl330_config_write() 对 RX 明确读取了端口位宽与 Burst 参数:

if (slave_config->src_addr_width)
    pch->burst_sz = __ffs(slave_config->src_addr_width);
pch->burst_len = fixup_burst_len(slave_config->src_maxburst,
                               pch->dmac->quirks);

在这份实现中,fixup_burst_len() 将输入限制到 1~16,后续 _prepare_ccr() 再把长度减一后编码进 CCR 的源、目标 Burst 长度字段。这说明客户端字段确实进入了这条控制器配置路径;但最终总线波形仍受任务剩余长度和控制器执行过程约束,不能只看一个字段就断言每笔总线事务的形态。

两种实现并排看,才容易形成正确的阅读习惯:

接口定义给出"参数想表达什么",控制器源码说明"这一版究竟怎样实现"。两者缺一不可。

7.4 AXI 长度还有一个需要单独复核的编码边界

这里的 parse_device_properties() 读取 snps,axi-max-burst-len,检查值在 1~256 之间,随后直接保存;准备描述符时,又把保存值直接移入 ARLEN/AWLEN 控制位域。

但头文件中的 DWAXIDMAC_ARWLEN_16 = 15 等枚举,以及 AXI 的长度编码,都采用"实际数量减一"的表达。外部 binding 也把这个属性描述为长度限制,而不是直接暴露寄存器编码。

这构成了源码里值得继续核对的长度值与编码值边界,不能在讲解中悄悄补上源码没有的减一操作。 如果对应硬件字段使用这些枚举所指示的编码,直接移位就存在偏一和上限位域风险;是否需要修正,应结合准确的 IP 手册、目标分支变更和寄存器验证确认。

本篇不把某个设备树数值当作已经验证的性能调优建议,也不把这个静态疑点写成已经复现的硬件故障。先把"配置值 → 内部值 → 位域编码 → 总线含义"逐段核对,才有资格判断实际行为。

八、把五个概念接起来:一次 1024 字节的接收

8.1 先列条件,不直接套用开发板参数

构造一个有限长度的 RX 示例:FIFO 每项 4 字节,深度 16 项,在 8 项水位提出请求;使用固定 RX 数据端口,目标内存满足映射、对齐和长度约束;源、目标传输宽度均为 4 字节,端点服务分组按 4 次访问考虑;本次搬运 1024 字节,且单个硬件块允许容纳这段数据。

若先写一个配置辅助函数,它只能负责描述端口访问条件,并不是完整的接收驱动:

#include <linux/dmaengine.h>
#include <linux/errno.h>
#include <linux/err.h>

/* 假设:硬件端口必须用 4 字节访问,服务分组允许 4 项。 */
static int configure_rx_dma(struct dma_chan *chan,
                            phys_addr_t rx_fifo_phys)
{
    struct dma_slave_config cfg = {
        .src_addr       = rx_fifo_phys,
        .src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
        .src_maxburst   = 4,
        .device_fc      = false,
    };

    if (IS_ERR(chan))
        return PTR_ERR(chan);
    if (!chan)
        return -EINVAL;

    return dmaengine_slave_config(chan, &cfg);
}

这里的 rx_fifo_phys 是按平台资源及该驱动约定得到的外设数据端口地址,不是 ioremap() 返回的 CPU 虚拟指针。PL330 中还存在对这类端口资源调用 dma_map_resource() 的处理;不能假设所有地址域、所有控制器都可以直接套用同一地址数值。

外设门限和 DMA 请求使能要由外设驱动负责;内存的 DMA 映射、准备接口里的 DMA_DEV_TO_MEM、提交、启动和完成后的资源处理也都还没包含。尤其不应只填配置结构里的 direction 就认为后续准备方向已经自动确定。

8.2 1024 字节里有多少次访问、多少组?

在上面的对齐、完整数据单位和统一位宽假设下:

总数据量:             1024 B
源端每次访问:           4 B
源端访问次数:    1024 / 4 = 256
每组访问次数:           4
等量分组数:       256 / 4 = 64
每组数据量:          4 × 4 = 16 B

1024字节DMA搬运分组示意图

图 7:展示这个示例的源端计数关系。64 组是按 4 次端点访问分组的算术结果,不等于已经测得 64 笔 AXI 事务,更不等于 64 次 CPU 中断。一个任务也可以因硬件限制拆成多个块。

在本文 RX 描述符构造路径中,block_ts = len >> reg_width。4 字节对应宽度编码 2,因而 1024 >> 2 = 256;写入 block_ts_lo 的值是 256 - 1 = 255。

注意这个寄存器计数使用的是这里的源端传输单位,而不是字节,也不是 Burst 组数。把 1024、256、64、255 都叫"传输长度",调试时就很容易读错状态。

对于 TX,同一函数用 len >> mem_width 计算块传输次数,因为源端此时是内存。这进一步说明:当两侧位宽不同时,判断硬件块计数必须先明确按哪一侧计数,不能固定套用外设宽度。

8.3 运行时,不是请求一次就把全部 1024 字节强行读完

DMA 通道先持有足够执行任务的配置。外设不断接收数据,FIFO 满足请求条件;控制器结合握手、通道调度与总线条件推进读写;数据不足或暂时不能继续服务时,按硬件协议等待。任务计数与握手条件共同约束传输,而不是拿到第一次请求后就无条件读空整个目标长度。

如果整段音频按前面的 384,000 B/s 连续输入,1024 字节对应约 2.67 ms 的数据;它仍可能要求 DMA 在约 83.3 μs 的名义 FIFO 余量内开始并持续提供足够服务。任务总体持续多久,与 FIFO 每次最多能等多久,是两种不同的时间尺度。

九、调参前,先能解释这几种故障

9.1 通道已经启动,计数却不前进

先沿服务链检查:外设是否真的产生数据,DMA 请求是否使能,门限条件是否达到,请求号及路由是否正确,通道源目标方向是否匹配。然后再看控制器错误状态、访问地址及总线响应。

"申请通道成功"只说明资源申请完成;"配置返回 0"也不证明外设请求已经接通。尤其不应一看到没有完成中断,就立刻把问题归为 CPU 中断系统故障:也可能搬运根本没有推进到完成点。

9.2 数据总是重复、错位,或一组数据只有最后一项

固定 FIFO 地址本来就是正确的,不能为了让数据"向前走"而改成递增。应该分别核对端口访问的副作用、内存地址递增、源目标位宽,以及 24 位有效数据在 32 位容器中的排列。

若每一项都写到同一内存地址,结果可能只剩最后一次写入;若端口访问单位不匹配,可能出现采样错位或总线错误。这些现象只是定位线索,不是唯一诊断。修正配置后仍需用可预测数据验证,不能只凭波形"看起来像音频"。

9.3 单独运行正常,摄像头或编码器一忙就溢出

这时值得测量的是最坏服务间隔,而不仅是平均 MB/s。若其他主设备的访问让 DMA 在某个窗口内得不到足够服务,即使长期平均带宽看起来充足,小 FIFO 也可能先装满。

按顺序核对输入速率、FIFO 实际容量、门限、请求到服务的延迟分布和错误计数,再考虑平台支持的优先级、互连服务保证及 Burst 配置。降低 RX 门限可能换来更多等待余量;TX 则要注意在耗尽前提早补数。两者不能简单照抄同一个"调大门限"操作。

增加 RAM 缓冲区可以改善软件消费不及时的问题,但未必改变外设 FIFO 到 DMA 之间的硬件服务期限。先判断丢数据发生在哪一段,再决定扩大哪一个缓冲区。

9.4 把 Burst 改大,速度和寄存器却没变化

先验证参数有没有被使用。在本文 DW 路径中,客户端 maxburst 并没有直接控制 MSIZE;而 PL330 存在明确的读取、限制和编码过程。此时反复修改同一个字段,不如沿配置使用点追一遍。

性能记录至少需要同时保存:传输大小、两侧位宽、外设门限、实际写入的控制值、有效数据速率,以及溢出/欠载/总线错误计数。平均吞吐量改善但最坏服务间隔变差,不一定是实时音视频链路想要的结果。

十、留一个小练习:能算清楚,也要知道不能推断什么

假设仍使用 16 项 FIFO、每项 4 字节,48 kHz 双声道、每个声道样本占 4 字节。RX 门限为 12 项;准备搬运 1024 字节;客户端填写 src_maxburst = 8;目标控制器采用本文分析的 DW AXI DMAC 路径。

问题一:名义 RX 等待窗口是多少?

剩余空间是 (16 - 12) × 4 = 16 B,除以 384,000 B/s,结果约为 41.7 μs。这个数需要再扣除实际实现的延迟和安全余量,不能当作保证不会溢出的边界。

问题二:src_maxburst = 8 能不能证明 SRC_MSIZE 已经是 8?

不能。要看字段是否进入硬件配置;本文源码写入的仍是固定 DWAXIDMAC_BURST_TRANS_LEN_4。接口参数的意图与实际实现要分别核对。

问题三:这段数据对应多少次源端 4 字节访问?应该写多少块计数?

源端访问数为 256。若单块限制允许容纳整段,按本文 RX 路径写入的 block_ts_lo 为 255;如果硬件块限制更小,需要先拆块,再分别编码。不能把 maxburst、任务总长度或分组数直接写成块计数。

问题四:能不能据此算出 AXI 事务数量和 CPU 中断数量?

不能。前面的信息足够计算逻辑访问量,却不足以推导实际总线事务边界和通知粒度。还需要控制器的事务组织、AXI 长度限制及描述符/中断设置。

结语

读 DMA 硬件配置,最怕的不是寄存器多,而是几个数字混用了单位和层次。

通道决定由哪条资源执行,请求线决定响应哪个外设端点,FIFO 提供有限等待空间;位宽定义某一侧一次访问的单位,Burst 定义若干访问怎样组织,而任务总长度定义这次总共要搬多少。

落实到代码时,再加一道检查:参数有没有被这份驱动使用?从字节数、访问次数到寄存器编码,中间做了什么转换?

这几条关系理清之后,src_addr_width、SRC_MSIZE、ARLEN 和 BLOCK_TS 就不再是一堆相似的名字。下一篇进入地址与缓存,继续讨论:即使这些硬件参数都配对了,为什么一块普通 CPU 缓冲区仍不能直接交给 DMA。




上一篇:对话网易首曝FPS新作制作人:95分才能上牌桌,下一代3A想从极限运动射击开始
下一篇:Rust 终端 ASCII 宠物:用 Git 提交喂食,吃透 trait 与所有权
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-11 05:43 , Processed in 0.087873 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表