十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AXI-Stream sg模式详解:从描述符机制到DMA调试实战

AXI-Stream sg模式详解:从描述符机制到DMA调试实战 做DMA设计或者接DMA外设的时候AXI-stream的sg模式迟早会遇到。我第一次接触它是在调一个网卡控制器的驱动CPU要不断地把收到的报文从Buffer复制到连续内存再交给协议栈效率非常难看。后来换成支持sg模式的DMACPU只负责填描述符、点一下门铃剩下的事情全交给硬件性能直接上了一个台阶。这篇东西就围绕AMBA总线里的AXI-stream接口把sg模式从概念到落地聊清楚适合正在看协议手册、写RTL验证或者被驱动和DMA调试折磨的朋友参考。1. AXI-stream总线基础与sg模式定位1.1 AXI4-Stream本质上是一条单向数据高铁很多第一次接触AMBA总线的同学容易被AXI4、AXI4-Lite、AXI4-Stream这三个名字搞混。先快速过一遍AXI4是有地址读写通道的完整协议适合CPU访问外设寄存器、DDR这种随机访问场景AXI4-Lite是AXI4的瘦身版去掉突发传输一次只传一笔数据主要用来配寄存器AXI4-Stream则干脆连地址都不要了只在主从之间单向传输连续的数据流。AXI4-Stream的特点可以理解成一条单行道高铁数据从上游一站站往下游送没有回头路也不需要告诉对方“我要访问哪个地址”。它的核心信号就那几个TVALID和TREADY负责握手主设备拉高TVALID表示“这拍数据有效”从设备拉高TREADY表示“我这拍能收”两边同时拉高才算完成一拍传输。除此之外还有TDATA传数据TKEEP标记每个字节是否有效TLAST标记这一组数据的最后一拍TUSER、TID、TDEST则是用来携带用户自定义信息、数据流ID和路由信息的扩展信号。AXI-stream最巧妙的地方在于它不关心数据是什么意义只保证数据以字节流的形式准确送达。正因为这种简洁它成了高速外设和DMA引擎之间的标准接口不管是视频像素流、以太网报文还是PCIe的TLP包都可以套在AXI-stream上搬运。sg模式就是在这条“单行道”上解决一个关键问题如何高效地把内存中分散的数据块组合成流或者把流拆散到内存的多个位置。1.2 固定地址DMA的痛点与sg模式的定位传统的DMA控制器比如简单的内存到内存搬运引擎操作起来很直接你告诉它“从地址A搬N字节到地址B”它就一直搬搬完通知你。这在连续内存块之间搬运时很高效但现实里数据往往不是连续存放的。以网络报文为例驱动可能把缓冲区拆成头部区和数据区甚至一个报文分布在多个页上如果每个不连续的小块都要CPU先拷贝合并再交给DMA搬一次CPU开销就白省了。sg模式就是为这种“数据散落在内存各处”的场景设计的。它的核心思路是让DMA自己通过一张描述符链表找到每一块数据的地址和长度一块搬完自动跳到下一块全部搬完再告诉CPU。这个机制的学名叫Scatter-Gather中文常叫分散聚合scatter指把流数据写到多个不连续的内存块gather指从多个不连续的内存块把数据收集成流。驱动程序只需要在内存里维护好这张“地图”DMA凭地图就能干活CPU的参与频率从一个字节一次降到一个描述符链表一次甚至一批描述符一次。把这套机制放到AXI-stream环境下DMA的Memory-Mapped一侧连接的是系统内存控制器Stream一侧就是AXI-stream接口。CPU把要发送的数据地址和长度填入描述符DMA按列表读取数据并从AXI-stream主端口发出去反过来AXI-stream从端口收到数据DMA按描述符里的地址写到内存对应位置。理解sg模式的关键就是理解这张“地图”的组织形式和DMA遍历地图的方式下面展开聊。2. sg模式的组成结构与搬运流程2.1 描述符和地址链表的“长相”sg模式的核心数据结构是描述符英文叫Descriptor或者BDBuffer Descriptor它是一个固定大小、按规定格式放在内存里的结构体。不同的IP对描述符字段的定义会有细微差别但内核结构基本一致大致长这样struct sg_descriptor { uint32_t src_addr_l; // 源地址低32位 uint32_t src_addr_h; // 源地址高32位用于64位系统 uint32_t dst_addr_l; // 目的地址低32位 uint32_t dst_addr_h; // 目的地址高32位 uint32_t length; // 本段数据字节数 uint32_t control; // 控制字段比如使能、中断使能、TLAST使能 uint32_t status; // 状态字段硬件执行完后回写 uint32_t next_desc; // 下一个描述符地址 };描述符里最重要的几个信息本段数据的源地址、目的地址、传输长度以及下一个描述符在哪。地址和长度决定了DMA“搬哪块、搬多大”next字段决定了搬完这块去哪找下一块。控制字段里通常会有一位是“所有权位”owner bit软件填好描述符后把这位写成1表示“这个描述符现在归硬件所有”DMA处理完了回写状态时把这位清掉归还给软件。这个owner机制避免了CPU和DMA同时修改同一个描述符造成冲突。实际芯片里有的DMA要求描述符按链表组织有的要求固定放在一个环形队列里。链表的好处是灵活描述符可以分散在内存各处坏处是DMA需要额外总线访问去读next指针环形队列则要求描述符在内存里连续摆放DMA处理到队列末尾时自动回到开头硬件实现简单也便于软件批量提交。选择哪种取决于系统需求但理解的时候可以把它们都看成“用数组或链表记录的搬运任务清单”。2.2 从CPU配置到DMA完成的完整流程sg模式的一次完整搬运可以拆成下面几步我在调试时习惯把每一步对应到信号或寄存器上方便定位问题CPU在内存中分配描述符缓冲区填好每个描述符的地址、长度和next指针并把owner位置位。CPU把第一个描述符的地址写入DMA的“当前描述符指针”寄存器或者直接写入一个指向描述符链表头的寄存器。CPU往DMA的门铃寄存器写入一个“启动”值这一步相当于按下了启动按钮。DMA看到门铃信号后从内存读取第一个描述符解析出源地址、目的地址和长度开始搬运。当前描述符搬完后DMA把状态比如搬运了多少字节、有没有出错回写到描述符的状态字段并清除owner位。DMA检查next字段如果next有效就继续读下一个描述符如果next为空或者到达环形队列尾部就停止本轮搬运并发中断通知CPU。CPU收到中断后遍历描述符通过owner位和状态字段判断哪些描述符已完成回收缓冲区或提交下一次传输。这里面“写门铃”的动作很关键。DMA不会自动发现你更新了描述符必须由CPU主动告诉它“列表就绪开始干活”。有些IP还会把门铃分成TX和RX两个方向网络接口里发数据写TX门铃收数据则由DMA检测到描述符准备好后自动启动或者也需要软件先写RX门铃取决于具体设计。流程看起来不复杂但工程实现里有大量细节决定它能不能稳定高效跑起来。描述符放在内存里CPU写的字段DMA不一定马上能看到这不是DMA“偷懒”而是Cache一致性问题在作祟DMA读描述符的速度也直接影响整个链路能跑多快。下一节就说这些必须在设计阶段抠死的点。3. 设计sg模式时必须抠死的几个细节3.1 描述符对齐和Cache Line很多DMA IP对描述符有对齐要求常见的是32字节对齐高端一些的会给到64字节对齐。这个对齐不是协议强制的但硬件设计为了方便地址解码和突发传输通常会要求描述符起始地址落在某个对齐边界内。如果软件不小心填了一个未对齐的地址轻则性能下降重则DMA直接不工作状态寄存器里报一个地址错误。更麻烦的是Cache一致性问题。现代CPU访问内存会经过CacheDMA访问内存是直接走总线的。CPU刚写好的描述符如果还躺在Cache里没被写回DDRDMA去内存读到的就是旧数据反过来DMA回写了状态字段CPU如果从Cache里读看到的是旧值。一个经验做法是把描述符所在内存区域标记为Non-Cacheable或者每次写完后显式做Cache clean读完描述符前做Cache invalidate。驱动代码里常见的dma_map_single、dma_alloc_coherent这类接口本质就是在帮你处理这些事情。从硬件设计角度看CPU的Cache Line一般是64字节如果描述符大小是32字节两个描述符刚好占一个Cache Line是理想状态。如果描述符大小和Cache Line错配一个描述符跨两个Cache Line软件做Cache维护的代价会变大硬件读描述符也可能需要拆成两次总线访问。所以定义描述符结构体的时候我建议在草稿阶段就算清楚尽量让单个描述符大小整除64避免设计完再返工。3.2 地址映射与跨页问题的处理CPU和DMA眼中的“地址”不是一回事。CPU跑在虚拟地址空间DMA访问的是物理地址。Linux驱动里用kmalloc分配的缓冲区拿到的是虚拟地址要交给DMA用必须转换成物理地址或者IOMMU/设备地址。这个转换如果做错了DMA会搬到一个完全错误的位置。在最开始调DMA的时候我踩过一个坑把虚拟地址直接写进描述符结果DMA搬运的数据全写进了错误的物理页面系统跑一会儿就崩了。跨页是另一个高频坑。很多DMA控制器为了简化地址递增逻辑单次传输的长度有限制比如不能超过一个页4KB或者描述符里的源地址和目的地址不能在传输过程中“跨越”页边界。一旦缓冲区起始地址在页中间数据又超过了页边界就必须把一次传输拆成两个描述符第一个从页中位置搬到页尾第二个从下一页开头继续。这个逻辑如果交给驱动做驱动得自己判断是否跨页如果交给硬件做DMA内部要有跨页拆分状态机。无论做在哪边设计文档里都该写明跨页规则否则调试时数据“莫名丢失”会让人抓狂。针对有SMMU/IOMMU的系统还有一个优势DMA可以通过IOMMU直接拿到连续的“设备地址”软件侧不需要处理和物理页的纠缠硬件侧也能用更大的突发长度去访问内存。但代价是每次DMA访问都多了一级地址翻译延迟和带宽会有一定折损。是否启用IOMMU需要结合系统的实时性要求和地址空间大小来做平衡。3.3 TLAST、突发和包边界的配合AXI-stream传输本身是字节流没有自动的“包”概念。是TLAST告诉下游“这一拍是最后一个数据”下游才知道一个数据包结束。sg模式里一个描述符往往对应一个数据包但一个数据包也可能由多个描述符拼成。设计时要想清楚TLAST在每个描述符边界上怎么拉如果每个描述符就是独立包很简单搬完这个描述符的最后拍拉TLAST如果多个描述符组成一个包TLAST只能由最后一个描述符触发。另一个容易出问题的点是TLAST和TKEEP的配合。当总线位宽是64位、即8字节而数据包长度不是8的倍数时最后一拍TLAST拉高的同时TKEEP要准确指出哪几个字节是有效数据。比如一个63字节的包总线一次能传8字节前7拍都是满8字节最后一拍只有7个字节有效TKEEP就应该是0x7F。软件填描述符长度和硬件产生TKEEP的逻辑必须一致否则接收方会多收或少收字节这在网络协议栈里直接表现为校验和错误。突发长度Burst的选择也直接影响性能。AXI-stream主设备发数据时如果每拍都跟从设备握手一次效率很低一次发一组连续的突发数据总线利用率会高很多。sg DMA通过描述符拿到一整块数据的地址和长度后应该尽量把访问内存的突发配置成比较大的值比如16拍或32拍而不是每8拍就断一次。我在实际环境里测过同样搬4KB数据突发长度从8提到32DDR带宽利用率能提升两三成。3.4 中断频率与性能取舍sg模式一个很容易做错的地方是中断策略。如果每个描述符搬完都发一次中断CPU在大量数据包场景下会被中断淹没忙于处理中断而没时间干正事。更合理的做法是“批处理”软件一次性提交几十个描述符DMA全部搬完才发一次中断或者DMA支持“描述符完成计数”寄存器软件定期去查计数而不是靠中断。中断合并Interrupt Coalescing也是常用手段。让DMA支持一个延迟计时器比如收到完成事件后等10微秒再发中断如果这段时间内又有新的完成事件刷新计时器继续等直到空闲才统一上报。这个策略在网卡里很常见能把收包中断从每秒几十万次降到几万次CPU占用率大幅下降。但代价是延迟变高对实时性要求高的场景要把延迟窗口调小或者干脆关闭合并。还有个容易忽略的小点中断状态寄存器是“写1清0”还是“写0清1”各IP定义不一样。驱动的中断处理函数里清错标志位会导致中断风暴或者丢中断尤其要仔细看手册。我见过不止一个同事在这种小地方浪费一下午最后发现就是清了不对的位。4. 调试sg模式时的高频问题记录4.1 DMA不启动卡死的排查顺序sg模式最常见的故障现象是软件配了半天DMA纹丝不动状态寄存器停在IDLE。按照下面的顺序排查绝大多数情况能快速定位第一检查描述符地址是否写入正确。用总线跟踪工具或者仿真波形看寄存器的值确认CPU写入DMA的描述符指针寄存器与内存里的实际地址一致特别是64位系统的高低32位有没有拼接错误。第二检查门铃是否真的写进去了。有些DMA要求门铃寄存器必须写特定值比如“写入当前描述符索引”随便写个1可能不会触发。第三检查描述符的owner位和使能位。软件填完描述符后必须把控制字段里的owner位置1硬件只认这个标志没置位一律当无效描述符跳过。第四关掉Cache再试一次。如果关了Cache就正常说明前面说的Cache一致性问题软件侧需要做clean/invalidate。如果以上都查完仍然卡死就要回到RTL验证层面了。用仿真把DMA读描述符的总线交易抓出来看看它读到的第一个字实际是什么。很多时候问题是描述符在内存里的物理布局和硬件的预期不一致比如硬件默认描述符是64字节一个软件按32字节一个排DMA读出来的字段全是错的。4.2 数据错位和TLAST异常数据错位的表现比较典型接收端收到的数据和发送端发的对不上但总长度是对的。问题大概率出在字节通道映射上。AXI-stream总线的TDATA是并行多字节硬件内部通常会把第一个字节对准最低8位或者把字节按“小端”排列到总线向量上。如果发送DMA按大端处理接收DMA按小端处理每8个字节就倒一次序接收端看起来就是“字节错位”而不是“位错乱”。TLAST异常则更多体现在网络接口上对端收到的报文尺寸和预期不符经常多几个字节或少几个字节。排查时先把TKEEP的波形拉出来看最后一拍确认硬件有没有正确标记有效字节。如果软件填的描述符长度是63而硬件在TLAST那一拍把8个字节全部置为有效接收方就会多收到1个字节。反过来如果软件长度是64硬件却在第63字节就拉TLAST接收方少1个字节。这个匹配逻辑要写死在设计文档里驱动和RTL两边各管一半最容易出现理解偏差。4.3 带宽跑不满的原因sg DMA带宽上不去的根因往往不在数据传输本身而在描述符处理路径上。DMA每处理完一个描述符都要回写状态、读取下一个描述符这些操作都占用总线带宽。如果描述符间距很大比如每个描述符都放在不同的Cache Line甚至不同的页DMA读描述符的开销会非常可观。性能分析时可以把总线上发给描述符读的数据量和实际搬数据的数据量做个对比如果描述符读取占了超过一成的带宽就该考虑优化描述符布局了。另外还有一个常见因素是AXI-stream侧的反压。如果下游从设备数据消费速度慢TREADY频繁拉低DMA就会暂时阻塞这个阻塞会一路传导到内存侧让DDR访问模式变得碎片化带宽自然上不去。解决办法是给Stream侧加上足够的FIFO缓冲让DMA能够预取下一段数据减少来回等待。还有一个小技巧是预取描述符。支持sg模式的DMA一般会有描述符缓冲或者预取机制在处理当前描述符的同时提前把下一个描述符读进内部寄存器。如果硬件不支持预取可以考虑让描述符排列成预取友好的结构比如把多个描述符放在连续内存里让DMA可以用一次突发读连续拿到多个描述符。4.4 缓存一致性问题缓存一致性在带Cache的嵌入式处理器平台上是绕不开的。现象很典型CPU填好描述符DMA跑完也更新了状态但CPU读取时看到的始终是旧值。这个问题的本质在前面说过数据躺在Cache里没写回或者CPU读到了Cache里的旧数据。处理方式有两种一种是软件维护在合适的时机调用Cache clean或invalidate API另一种是硬件维护系统总线支持Cache Coherent比如ACE或CHI协议DMA访问和CPU访问会通过硬件一致性机制自动同步。软件维护看起来简单但很考验工程师对时机的把握。发数据时CPU要确保描述符里的数据都写回内存后再写门铃否则DMA可能读到半新半旧的数据收数据时CPU要等DMA完成中断后再做Cache invalidate否则描述符状态字段读回来还是旧的。我在项目里遇到过收数据连续性错误查到最后发现是一个必要的位置漏了Cache invalidate补上之后问题立刻消失。5. 从验证到落地的一点建议5.1 用简单的FPGA环境快速跑通sg如果你正在写支持sg的DMA RTL或者在做验证环境我建议先不要一上来就搭全系统。第一步先把物料准备好一个AXI-stream主端口、一个AXI-stream从端口、一组描述符存储空间、一颗能模拟CPU总线写操作的寄存器模型。验证用例从最简单的开始一个描述符搬运64字节回写状态结束。这个用例跑通说明握手、描述符读取、数据搬运的基本路径是通的。第二步再上多描述符链表两个或多个不连续的缓冲区通过next指针串起来验证DMA搬完第一个能不能正确跳到第二个。这一步最容易暴露next字段解析、地址递增、跨页处理的问题。第三步再上环回场景DMA从Stream端口收数据写到内存的一组描述符缓冲区再从另一组描述符缓冲区读出来发出去。跑环回时重点检查数据内容是否逐字节一致TLAST的位置对不对。等这三步都稳定了再接入真实的总线互联和DDR此时遇到的问题基本就是系统级而非DMA本身的问题了。5.2 落到真实项目里的扩展思路sg模式的设计思路放到真实项目里选择要结合器件的特性和系统需求。以FPGA里常见的DMA方案为例Xilinx的AXI DMA IP天生支持SG模式硬件上通过一个SG引擎模块来管理描述符软件驱动也相对成熟项目初期如果时间紧直接用这个IP是最稳妥的路径。Intel平台也类似有对应的DMA IP和驱动参考。但如果场景特殊比如要求极低延迟、或者要把描述符格式改成公司内部协议格式自己做RTL也完全可行只是验证和调试成本要计算进去。另一个值得留意的方向是描述符与多队列的结合。很多高性能网络设备会为每个CPU队列维护独立的描述符环形队列DMA根据包的哈希结果分发到不同队列CPU之间互不干扰性能扩展性比单队列好很多。sg模式天然支持这种结构因为每个队列就是一组独立的描述符链表驱动和硬件只要约定好队列与描述符基地址的映射关系就可以。最后说一个个人体会sg模式本质上是用一条“任务链表”把CPU从繁重的数据搬运中解放出来它并不神秘但工程上处处是细节。描述符格式、对齐、Cache一致性、TLAST语义、中断策略每一处都值得在项目初期就确定清楚。尤其是协议用户手册里那些看似不起眼的注释比如“描述符必须32字节对齐”“状态字段由硬件清零”在实际调试时往往就是决定成败的那一行。我在做以太网DMA那段时间白天查波形晚上看驱动最终把问题收敛到一两个信号上之后才发现很多所谓疑难杂症其实都藏在最基础的定义里。先把这些底层机制吃透再难的sg设计也能一步步拆开。
返回列表