1. 从零开始理解AXI/AXIS数据搬运与DMA缓存架构
FPGA开发走到一定阶段,你一定会遇到一个绕不开的坎:数据怎么高效地从外设搬进内存,又怎么从内存搬到计算单元。我刚开始接触这块的时候,脑子里全是问号——AXI和AXIS到底有什么区别,DMA为什么需要它,DDR缓存又该怎么设计。如果你也在近似零基础的状态下啃FPGA,并且已经学到了数据搬运这一环,那这篇内容就是写给你的。我会把AXI/AXIS协议的核心逻辑、DMA控制器的设计思路、以及DDR缓存架构的搭建方法,用从业者的视角拆开讲清楚,让你看完能直接动手搭一个可用的数据通路。
先说说这个内容能解决什么问题。在FPGA项目里,数据搬运的效率往往决定了整个系统的吞吐上限。你写了一个图像处理模块,算法本身跑得飞快,但数据从外部芯片进来要等、处理完写回内存也要等,最后整体性能被搬运环节卡死。AXI/AXIS就是解决芯片内部各模块之间高速数据传输的协议标准,DMA是帮你把CPU从繁琐的数据搬运中解放出来的控制器,DDR缓存则是给大数据量提供临时存放空间的仓库。这三者组合起来,就是一套完整的数据搬运与缓存架构。
适合谁来参考?如果你已经写过简单的FPGA逻辑,知道什么是时钟、什么是状态机,但对AXI总线协议只有模糊印象,对DMA和DDR的配合方式不太清楚,那这篇内容正好补上这块。我会从协议的基本握手讲起,一直讲到缓存架构的参数计算,中间穿插我实际踩过的坑和调试技巧。
2. AXI与AXIS协议的核心差异与选型逻辑
2.1 为什么会有两种协议
AXI的全称是Advanced eXtensible Interface,AXIS是AXI Stream的缩写。很多人第一次看到这两个名字会懵,觉得都是AXI,为什么还要分两种。其实它们的定位完全不同,就像快递和自来水的区别。
AXI是面向地址的传输协议。你要读数据,得先告诉它读哪个地址;你要写数据,得先告诉它写到哪个地址。它有一套完整的地址通道、数据通道、响应通道,适合处理器访问内存、访问寄存器这类场景。AXIS是面向流的传输协议,它没有地址的概念,数据就像水流一样从上游流到下游,适合视频像素流、ADC采样数据流这类连续不断的场景。
我刚开始学的时候犯过一个错误:用AXI去传视频流。结果每次传一帧都要算地址、发地址、等响应,效率极低。后来换成AXIS,数据直接连着走,吞吐量翻了好几倍。这个教训让我明白,选协议之前先想清楚数据是“有地址的块”还是“无地址的流”。
2.2 握手机制:VALID与READY的博弈
AXI和AXIS都依赖一套握手机制来保证数据传输的可靠性。核心信号就两个:VALID和READY。发送方把VALID拉高表示数据有效,接收方把READY拉高表示准备好接收。只有当VALID和READY同时为高的那个时钟周期,数据才真正完成传输。
这个机制看起来简单,但实际写代码的时候很容易出问题。我见过最常见的错误是:发送方把VALID拉高之后,看到READY没来就马上把VALID拉低,然后下一个周期再拉高。这种“脉冲式”的VALID会导致接收方漏掉数据。正确的做法是:VALID一旦拉高,就必须保持到READY到来为止,中间不能撤销。
还有一个坑是组合逻辑路径太长。有些人喜欢把READY信号用组合逻辑直接连到VALID的使能上,结果时序报告一片红。我的经验是,VALID和READY最好都经过寄存器输出,虽然会多一个周期的延迟,但时序容易收敛,系统更稳定。
2.3 突发传输:一次握手搬一批数据
AXI支持突发传输,也就是一次地址握手可以连续搬多个数据。这个特性对DDR访问特别重要,因为DDR的读写本身就有突发长度的概念,如果每次只搬一个数据,效率会低得可怜。
突发传输有两个关键参数:突发长度和突发类型。突发长度指的是一次传输搬多少个数据,常见的有1、2、4、8、16。突发类型有FIXED、INCR、WRAP三种。FIXED是地址不变,适合访问FIFO;INCR是地址递增,适合访问内存;WRAP是地址回绕,适合访问缓存行。
我在设计DMA的时候,通常会把突发长度设成8或16。太短了效率低,太长了占用总线时间久,影响其他模块访问。具体设多少,要看你的数据位宽和DDR的颗粒特性。比如数据位宽是128位,DDR的突发长度是8,那AXI的突发长度也设成8,这样正好匹配,效率最高。
2.4 AXIS的流控与背压
AXIS虽然没有地址,但它有一套流控机制。除了VALID和READY,还有TLAST和TKEEP。TLAST表示一帧数据的结束,TKEEP表示哪些字节是有效的。这两个信号在处理视频流和网络包的时候特别有用。
背压是AXIS里一个很重要的概念。当下游处理不过来的时候,它会把READY拉低,上游就必须暂停发送。这个机制保证了数据不会丢失,但也要求上游模块能够正确响应背压。我见过有人在上游用FIFO缓存数据,但FIFO的快满信号没有正确连接到AXIS的READY逻辑上,结果FIFO溢出,数据丢了都不知道。
处理背压的诀窍是:在数据通路上多放几级FIFO,每级FIFO的快满信号控制上一级的READY。这样即使下游突然卡住,也有足够的缓冲空间吸收上游的数据,不会立刻产生背压。但FIFO也不能放太多,否则面积和延迟都会上去。一般两级就够了,一级在数据源附近,一级在数据目的地附近。
3. DMA控制器的设计思路与关键实现
3.1 DMA到底解决了什么问题
DMA的全称是Direct Memory Access,直接内存访问。它的核心价值是让数据在内存和外设之间直接搬运,不需要CPU参与。没有DMA的时候,CPU要一个字节一个字节地从外设读数据,再一个字节一个字节地写到内存,搬完之后CPU才能去干别的事。有了DMA,CPU只需要配置好源地址、目的地址、数据长度,然后说一声“开始搬”,DMA控制器就自己把活干了,搬完了再通知CPU。
在FPGA项目里,DMA通常用在几个场景:高速ADC采样数据写入DDR、图像传感器数据写入DDR、DDR里的数据送到DAC输出、处理器和FPGA逻辑之间的数据交换。这些场景的共同特点是数据量大、实时性要求高,CPU亲自搬根本忙不过来。
3.2 DMA控制器的基本架构
一个典型的DMA控制器包含这几个部分:寄存器配置模块、读通道、写通道、地址生成模块、长度计数模块、中断生成模块。
寄存器配置模块负责接收CPU发来的配置信息,包括源地址、目的地址、传输长度、突发长度、传输方向等。读通道负责从源地址读数据,写通道负责把数据写到目的地址。地址生成模块根据突发类型和突发长度计算下一个地址。长度计数模块记录还剩多少数据没搬。中断生成模块在搬完之后产生中断通知CPU。
我设计DMA的时候,喜欢把读通道和写通道做成独立的,中间用一个异步FIFO连接。这样做的好处是读和写可以并行进行,读通道在搬第N个数据的时候,写通道可以同时在写第N-1个数据,吞吐量直接翻倍。如果读和写串行进行,那效率就减半了。
3.3 描述符链表:让DMA自己找活干
简单的DMA一次只能搬一块连续的内存。但实际项目里,数据往往分散在内存的各个位置,比如视频的一帧可能分成好几块存储。这时候就需要描述符链表。
描述符是一个数据结构,里面记录了这一块数据的源地址、目的地址、长度、下一块描述符的地址。DMA控制器先读第一个描述符,搬完这块数据之后,根据描述符里的“下一块地址”找到第二个描述符,继续搬,直到遇到结束标志。
描述符链表的好处是DMA可以连续搬很多块不连续的内存,不需要CPU每次都重新配置。我在做视频采集项目的时候,用描述符链表把一帧图像的多个区域串起来,DMA一次配置就能搬完整帧,CPU只需要在帧开始时配置一次,帧结束时处理一次中断,中间完全不用管。
设计描述符链表的时候要注意对齐问题。描述符本身在内存里要按一定的字节对齐,比如16字节或32字节对齐,这样DMA读描述符的时候可以用突发传输,效率更高。描述符里的地址和长度也要对齐,否则DMA搬数据的时候会产生非对齐访问,影响性能。
3.4 中断与轮询的取舍
DMA搬完数据之后,怎么通知CPU?有两种方式:中断和轮询。
中断方式是DMA搬完之后拉一根中断线,CPU收到中断后进入中断服务程序处理数据。这种方式实时性好,CPU不用一直盯着,适合对延迟敏感的场景。但中断也有开销,每次中断都要保存现场、跳转、恢复现场,如果数据块很小、中断很频繁,中断开销可能比搬数据本身还大。
轮询方式是CPU定期读DMA的状态寄存器,看搬完了没有。这种方式没有中断开销,但CPU要浪费时间去查,适合对实时性要求不高、数据块比较大的场景。
我的经验是:数据块大于4KB用中断,小于4KB用轮询。当然这不是绝对的,还要看CPU的负载和系统的实时性要求。有些项目里我会把两种方式结合起来,小数据块攒一攒,攒到一定量再触发一次中断,这样既减少了中断次数,又保证了实时性。
4. DDR缓存架构的设计与参数计算
4.1 DDR在数据通路中的角色
DDR是Double Data Rate的缩写,也就是双倍数据速率内存。它在FPGA数据通路里扮演的是“大容量临时仓库”的角色。FPGA内部的BRAM容量有限,通常只有几兆字节,存不下视频帧、雷达采样数据这种大块数据。DDR的容量可以到几百兆甚至几个G,足够存放这些数据。
但DDR有个特点:它的读写延迟比较大,而且读写切换的时候需要等待。如果数据通路设计得不好,DDR的带宽利用率可能只有理论值的百分之二三十。我见过一个项目,DDR理论带宽是12.8GB/s,实际跑下来只有3GB/s,就是因为读写频繁切换,效率被拉低了。
4.2 缓存架构的基本模型
DDR缓存架构的核心思想是:在DDR和计算单元之间加一级或多级缓存,把随机访问变成顺序访问,把小块访问变成大块访问。
最基本的模型是“乒乓缓存”。把DDR分成两块区域,一块用来写入,一块用来读出。写入端把数据写满一块之后,切换到另一块继续写,同时读出端从刚才写满的那块开始读。这样读写不会冲突,DDR的带宽利用率最高。
乒乓缓存的容量计算很简单:每块的大小要能存下一帧数据。比如视频是1920x1080,每个像素16位,那一帧就是1920x1080x2=4,147,200字节,约4MB。两块就是8MB。DDR的容量要大于8MB,还要留出其他用途的空间。
4.3 带宽计算与瓶颈分析
设计DDR缓存架构的时候,带宽计算是必须做的功课。假设你要处理4K视频,每秒60帧,每个像素24位,那数据率就是3840x2160x60x3=1,492,992,000字节/秒,约1.5GB/s。这是写入带宽,读出带宽也是1.5GB/s,加起来就是3GB/s。
DDR的带宽怎么算?假设DDR时钟频率是400MHz,数据位宽是32位,双倍数据速率,那理论带宽就是400M x 32 x 2 = 25.6Gbps = 3.2GB/s。看起来刚好够用,但实际上DDR的效率只有70%左右,实际带宽只有2.24GB/s,不够用。
这时候就要想办法优化。要么提高DDR的频率,要么增加数据位宽,要么优化访问模式减少读写切换。我通常会先优化访问模式,把读写分开,用乒乓缓存,这样效率能提到85%以上。如果还不够,再考虑提高频率或位宽。
4.4 缓存行与预取
DDR的访问粒度是缓存行,通常是64字节或128字节。如果你每次只读4个字节,DDR也会把整个缓存行读出来,剩下的字节就浪费了。所以设计缓存架构的时候,要尽量让访问对齐到缓存行,并且一次读满一个缓存行。
预取是另一个提高效率的技巧。在计算单元需要数据之前,提前把数据从DDR读到片上缓存。这样计算单元需要数据的时候,直接从片上缓存拿,不用等DDR的延迟。预取的深度要根据计算单元的消耗速度和DDR的延迟来定。比如DDR延迟是100个时钟周期,计算单元每个周期消耗4字节,那预取深度至少要400字节,才能保证计算单元不断流。
5. 实操过程:搭建一个完整的数据搬运与缓存系统
5.1 系统框图与数据流
假设我们要做一个视频采集与处理系统。数据流是这样的:图像传感器输出AXIS视频流,经过格式转换模块,通过AXI DMA写入DDR。处理器从DDR读取数据,做算法处理,处理完再通过DMA写回DDR。最后DMA从DDR读出处理后的数据,通过AXIS送到显示接口。
这个系统里有三个DMA通道:采集通道、处理通道、显示通道。采集通道把AXIS流转换成AXI写事务,写入DDR。处理通道从DDR读数据,送给处理器,处理器处理完再写回DDR。显示通道从DDR读数据,转换成AXIS流,送给显示接口。
5.2 关键模块的Verilog实现要点
先看AXIS到AXI的转换模块。这个模块的输入是AXIS流,输出是AXI写事务。核心逻辑是一个状态机:IDLE状态等待AXIS的VALID,收到VALID后进入WRITE状态,把数据写入AXI的写数据通道,同时拉高写地址通道的VALID。写地址通道的地址每次递增,递增的步长是数据位宽除以8。
这里有个细节要注意:AXI的写地址通道和写数据通道是独立的,可以并行进行。也就是说,你可以在发第一个数据的写地址的同时,发第二个数据。这样效率更高。但写响应通道要等所有数据写完才能收到响应,所以状态机要等写响应回来才能回到IDLE。
再看DMA控制器的读通道。读通道的状态机更复杂一些,因为要处理突发传输。状态机先发读地址,然后等读数据通道的VALID。每收到一个数据,就把它写入FIFO,同时地址递增。当收到的数据数量等于突发长度时,如果还有数据要读,就发下一个读地址;如果没有了,就等最后一个读数据回来,然后产生中断。
5.3 DDR控制器的配置与校准
DDR控制器通常由FPGA厂商提供IP核,你只需要配置参数。关键参数包括:DDR类型、数据位宽、时钟频率、时序参数、容量。
时序参数是最容易出问题的。不同的DDR颗粒有不同的时序要求,比如CAS延迟、RAS到CAS延迟、行预充电时间等。这些参数如果设错了,DDR要么不工作,要么工作不稳定。我的经验是:先查DDR颗粒的数据手册,找到对应的时序参数,然后按照IP核的配置界面填进去。如果不确定,就用IP核的默认值,通常默认值是保守的,能工作但性能不是最优。
DDR控制器上电后需要校准。校准的过程是控制器自动调整内部延迟,找到最佳的采样点。校准期间DDR不能访问,所以系统启动时要等校准完成才能开始搬数据。校准时间通常是几百个时钟周期,对系统启动时间影响不大。
5.4 中断服务程序的设计
DMA搬完数据后产生中断,CPU进入中断服务程序。中断服务程序要做几件事:清除中断标志、处理数据、重新配置DMA。
清除中断标志很重要,如果不清楚,中断会一直触发,CPU会卡在中断里出不来。处理数据要看具体应用,比如视频采集就是标记一帧就绪,通知处理线程。重新配置DMA是让DMA准备好搬下一块数据。
中断服务程序要尽量短,不要在中断里做耗时操作。我见过有人在中断里做图像处理,结果中断响应时间太长,丢了后续的中断。正确的做法是在中断里只做标记,把处理放到主循环或单独的任务里。
6. 常见问题与排查技巧实录
6.1 AXI握手死锁
症状:系统跑起来之后卡死,波形上VALID和READY都是高,但数据不动。
原因:发送方和接收方的握手逻辑有冲突。比如发送方等READY拉高才拉VALID,接收方等VALID拉高才拉READY,两边互相等,死锁。
排查:用逻辑分析仪抓波形,看VALID和READY的时序。如果两个信号同时为高但数据不变,那就是死锁。解决方法是打破循环依赖,让其中一方先动。通常是发送方先拉VALID,接收方看到VALID后再拉READY。
6.2 DMA搬数据搬了一半停了
症状:DMA配置了搬1000个数据,实际只搬了500个就停了,中断也没产生。
原因:可能是DDR访问出错,DMA收到了错误响应,进入了错误状态。也可能是FIFO满了,DMA在等FIFO有空位,但FIFO一直没空。
排查:先看DMA的状态寄存器,有没有错误标志。如果有错误标志,看错误类型,是读错误还是写错误。如果是FIFO满,看FIFO的占用情况,是不是下游模块没在读。如果是下游模块的问题,检查下游模块的READY信号是不是一直低。
6.3 DDR带宽不够
症状:系统跑起来之后,数据吞吐量上不去,DDR的带宽利用率只有30%。
原因:访问模式不好,读写频繁切换,或者访问粒度太小,没有利用突发传输。
排查:用DDR控制器的性能计数器,看读写切换的次数和突发传输的效率。如果读写切换太频繁,改成乒乓缓存,把读写分开。如果突发传输效率低,检查AXI的突发长度是不是设得太小,改成8或16。
6.4 缓存溢出
症状:系统跑一段时间后数据出错,或者DMA报溢出错误。
原因:缓存深度不够,或者生产者的速度大于消费者的速度,缓存被写满后继续写,导致溢出。
排查:看缓存的满信号和空信号,如果满信号经常拉高,说明缓存深度不够。解决办法是增加缓存深度,或者降低生产者的速度。如果是消费者太慢,优化消费者的处理逻辑,提高消费速度。
| 问题 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| AXI握手死锁 | VALID和READY互相等待 | 抓波形看时序 | 发送方先拉VALID |
| DMA搬一半停了 | DDR错误或FIFO满 | 看状态寄存器和FIFO占用 | 修复DDR或加快下游读取 |
| DDR带宽不够 | 读写切换频繁或突发太短 | 看性能计数器 | 乒乓缓存或增大突发长度 |
| 缓存溢出 | 缓存深度不够或消费者太慢 | 看满信号和空信号 | 增加深度或优化消费者 |
6.5 实操心得
第一个心得:仿真的时候一定要加超时机制。AXI和AXIS的握手如果出问题,仿真会一直跑下去,不报错也不结束。加一个超时计数器,超过一定周期就报错停止,能省很多调试时间。
第二个心得:DDR的校准状态一定要检查。有些项目里DDR校准失败,但系统还在跑,只是数据全是错的。在系统启动的时候读一下DDR控制器的校准完成寄存器,没完成就报错,别让系统带病运行。
第三个心得:DMA的描述符要放在DDR里,不要放在BRAM里。BRAM容量小,放不下几个描述符。DDR容量大,可以放很多描述符,支持更长的链表。但描述符在DDR里,DMA读描述符也要走DDR,会增加一点延迟。这个延迟通常可以接受,因为描述符的读取可以和数据的搬运重叠。
第四个心得:中断服务程序里不要用printf。printf是阻塞的,在中断里用会导致中断响应时间不可控。如果一定要打印调试信息,用环形缓冲区,在中断里写缓冲区,在主循环里打印。
第五个心得:DDR的带宽要留余量。计算带宽的时候,不要按理论值算,要按实际值的70%算。比如理论带宽是3.2GB/s,实际能用的是2.24GB/s。如果你的需求是2GB/s,那刚好够用,但系统一忙起来就可能不够。留20%的余量,需求不超过1.8GB/s,系统才稳。
7. 架构扩展与性能优化方向
7.1 多通道DMA并行
单通道DMA的带宽有限,如果系统需要更高的吞吐量,可以用多通道DMA并行。比如视频采集用通道0,视频显示用通道1,处理器通信用通道2。每个通道独立配置、独立中断,互不干扰。
多通道DMA的关键是仲裁。多个通道同时访问DDR的时候,需要一个仲裁器决定谁先访问。仲裁策略有轮询、优先级、加权轮询等。轮询最公平,但实时性差;优先级实时性好,但低优先级通道可能饿死。我通常用加权轮询,给实时性要求高的通道更高的权重。
7.2 片上缓存分级
DDR的延迟大,如果计算单元每次都直接访问DDR,效率会很低。可以在DDR和计算单元之间加一级片上缓存,用BRAM实现。计算单元先访问片上缓存,缓存不命中再访问DDR。
片上缓存的容量有限,通常只有几百KB。所以缓存的管理策略很重要。我通常用行缓存,把DDR里连续的一段数据读到片上缓存,计算单元访问的时候先查缓存。如果访问的地址在缓存里,直接读;不在,再从DDR读。
7.3 数据压缩与解压缩
如果DDR带宽实在不够,可以考虑数据压缩。在写入DDR之前压缩,读出DDR之后解压缩。压缩率越高,DDR带宽需求越低。但压缩和解压缩需要额外的计算资源,而且会引入延迟。
常用的压缩算法有游程编码、差分编码、哈夫曼编码等。选择哪种算法要看数据的特点。图像数据通常用差分编码,因为相邻像素的相关性高。雷达数据通常用游程编码,因为有很多零值。
7.4 性能优化的优先级
性能优化要有优先级,不要一上来就做最复杂的优化。我的优先级是这样的:先优化访问模式,把随机访问变成顺序访问,把读写分开;再优化突发长度,让每次传输搬更多数据;然后优化缓存深度,减少DDR访问次数;最后才考虑多通道、压缩这些复杂方案。
这个优先级的原因是:访问模式的优化成本最低、效果最明显。很多时候,仅仅把读写分开,带宽利用率就能从30%提到70%。突发长度的优化也很简单,改一个参数就行。缓存深度的优化需要改架构,成本高一些。多通道和压缩的成本最高,除非前面的优化都做了还不够,否则不建议轻易上。
8. 调试工具与验证方法
8.1 逻辑分析仪的使用
逻辑分析仪是调试AXI和AXIS的利器。把VALID、READY、DATA、LAST这些信号接到逻辑分析仪的探针上,抓一段波形,就能看清楚握手的过程。
用逻辑分析仪的时候,触发条件很关键。我通常把触发条件设成“VALID为高且READY为低超过10个周期”,这样能抓到背压的场景。或者设成“LAST为高”,这样能抓到一帧数据的结束。
抓到的波形要会看。先看VALID和READY的握手,确认每个数据都完成了传输。再看DATA的变化,确认数据没有重复或丢失。最后看LAST,确认帧边界正确。
8.2 仿真验证的要点
仿真是调试的第一道防线。在写代码之前,先写测试平台,模拟上游和下游的行为。上游产生激励,下游响应握手。
仿真的时候要覆盖几种场景:正常传输、背压、错误响应、超时。正常传输验证基本功能,背压验证流控逻辑,错误响应验证错误处理,超时验证超时机制。
仿真的覆盖率要关注。代码覆盖率看有没有遗漏的分支,功能覆盖率看有没有遗漏的场景。覆盖率到90%以上,基本可以认为验证充分了。
8.3 在线调试的方法
在线调试是在FPGA上跑起来之后,用集成逻辑分析仪抓信号。把关键信号接到集成逻辑分析仪的探针上,设置触发条件,跑一段时间,抓波形。
在线调试的难点是触发条件不好设。因为系统跑得很快,问题可能很久才出现一次。我的经验是:先用逻辑分析仪抓长时间波形,找到问题的规律,再设精确的触发条件。比如问题是每1000帧出现一次,那就设一个计数器,计到1000的时候触发。
在线调试还要注意资源占用。集成逻辑分析仪会占用BRAM和逻辑资源,如果占用太多,可能影响系统的正常工作。调试的时候可以只抓关键信号,减少探针数量。调试完了把集成逻辑分析仪去掉,释放资源。
8.4 性能评估的方法
性能评估是验证架构是否达标的关键。评估的指标包括:吞吐量、延迟、资源占用、功耗。
吞吐量的评估方法是:在系统跑起来之后,统计单位时间内搬运的数据量。可以用计数器统计DMA搬了多少个数据,除以时间,就是吞吐量。
延迟的评估方法是:在数据通路的入口和出口各放一个时间戳,算时间差。或者用逻辑分析仪抓入口和出口的信号,看时间差。
资源占用的评估方法是:看FPGA的综合报告,看LUT、FF、BRAM、DSP的占用率。占用率太高会影响时序收敛,一般不要超过80%。
功耗的评估方法是:用FPGA厂商的功耗估算工具,输入时钟频率、资源占用、翻转率,估算功耗。或者用板子上的功耗测量芯片,实测功耗。
9. 从项目实践中积累的经验
9.1 架构设计要留余量
我做过一个项目,DDR带宽算下来刚好够用,结果系统跑起来之后,偶尔会丢数据。查了很久才发现,DDR的带宽在温度高的时候会下降,刚好够用的设计在高温下就不够了。后来把DDR的频率提高了一档,留出20%的余量,系统就稳了。
这个教训让我明白:架构设计不能卡着理论值来,要留余量。带宽留20%,容量留30%,缓存深度留50%。余量看起来是浪费,实际上是给系统留了稳定运行的空间。
9.2 接口协议要吃透
AXI和AXIS的协议文档有几百页,很多人只看个大概就开始写代码。结果遇到问题的时候,不知道是协议理解错了还是代码写错了。我的经验是:协议文档至少要精读一遍,把握手、突发、响应这些关键机制搞清楚。遇到问题的时候,再回去查协议文档,往往能找到答案。
9.3 调试要有耐心
数据搬运和缓存架构的调试往往很耗时。问题可能出在任何一个环节:AXI握手、DMA状态机、DDR控制器、缓存管理。排查的时候要有耐心,一个环节一个环节地查,不要跳步。
我的排查顺序是:先查AXI握手,确认数据能正常传输;再查DMA状态机,确认状态跳转正确;再查DDR控制器,确认读写正常;最后查缓存管理,确认没有溢出。这个顺序是从底层到上层,底层没问题了再查上层,避免底层的问题干扰上层的判断。
9.4 文档和注释要写好
数据搬运和缓存架构的代码往往很复杂,状态机多、信号多。如果文档和注释写不好,过几个月自己都看不懂。我的习惯是:每个模块写一个头注释,说明模块的功能、接口、参数;每个状态机写一个状态说明,说明每个状态的作用和跳转条件;每个关键信号写一个注释,说明信号的含义和时序。
文档和注释看起来是额外的工作,实际上是省时间的。调试的时候,有文档和注释,能快速定位问题。交接的时候,有文档和注释,别人能快速上手。
9.5 版本管理要规范
数据搬运和缓存架构的代码改动频繁,今天改个参数,明天改个逻辑。如果没有版本管理,改乱了都回不去。我的习惯是用Git管理代码,每次改动都提交,提交信息写清楚改了什么、为什么改。这样出问题的时候,可以回退到上一个版本,对比改动,快速定位问题。
版本管理还有一个好处是分支管理。主分支保持稳定,开发新功能的时候开一个分支,开发完了合并回主分支。这样主分支始终是可用的,不会因为开发新功能而变得不稳定。
10. 写在最后的实操建议
如果你正在学FPGA的数据搬运和缓存架构,我的建议是先跑通一个最简单的例子。找一个现成的AXI DMA IP核,配置成最简单的模式,从BRAM搬数据到BRAM,跑通仿真,再上板测试。跑通之后,再把源和目的改成DDR,再跑一遍。最后再加上AXIS接口,把视频流或ADC数据接进来。
这个循序渐进的过程很重要。不要一上来就搭一个复杂的系统,那样出了问题都不知道从哪查。从最简单的例子开始,每加一个模块就验证一次,确保每一步都是对的。这样即使出了问题,也能快速定位到是哪个模块的问题。
还有一点是善用厂商的例程。FPGA厂商通常会提供AXI DMA和DDR控制器的例程,这些例程是经过验证的,可以直接拿来用。在例程的基础上改,比从零开始写要快得多,也稳得多。改的时候要理解例程的逻辑,不要盲目复制,否则出了问题还是不知道怎么查。
最后,数据搬运和缓存架构是FPGA开发里比较难的部分,但也是最有价值的部分。掌握了这块,你就能做高速数据采集、视频处理、雷达信号处理这些高端应用。刚开始可能会觉得难,但坚持下来,把每个环节都搞懂,你会发现其实没有那么复杂。关键是要动手,要仿真,要上板,要调试。光看书是学不会的,只有实际做过,才能真正掌握。