ARTICLE DETAIL

资讯详情

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

FPGA实战:AXI总线协议设计、时序关键点与调试技巧详解

FPGA实战:AXI总线协议设计、时序关键点与调试技巧详解 干FPGA时间久了你会发现凡是跟高性能、多模块打交道的地方绕不开AXI总线协议。无论是Zynq里ARM和PL的通信还是自己搭一个多主多从的片上网络AXI这套东西基本上就是事实标准。很多朋友一开始看协议文档会觉得头大满屏的通道、握手、突发好像很玄乎但实际上把它拆成Master和Slave两边来看再用状态机去落地并没有想象中那么难。这篇文章我就从实战角度把AXI总线协议的完整FPGA设计思路、时序关键点、以及调试中踩过的坑一次性讲透适合刚接触AXI、想自己动手写Master或Slave模块的开发者参考。1. 为什么我劝你用AXI总线而不是自己造轮子做FPGA设计的时候很多人最开始习惯了自己定义接口比如一个valid、data、ready点对点传数据简单粗暴。等到模块一多A要跟B通信C也要跟B通信D还要跟A通信这时候你就会发现自己随便定的接口根本没有统一规则每个模块都要单独写一套握手逻辑光是适配对方时序就够折腾半天。AXI总线协议最大的价值就是它把通信这件事给标准化了大家按照同一套规则来接口一接就能跑省掉了大量重复劳动。1.1 AXI总线到底是什么三个通道与一次握手AXI是ARM AMBA协议家族里的一员主打高性能、高频率、低延迟。它跟传统总线最大的区别是采用了多通道机制读地址通道、读数据通道、写地址通道、写数据通道、写响应通道。读和写相互独立地址和数据相互分离这意味着读写并行、流水线操作变得非常自然。一个事务的发起本质上就是一次手握手的交互发起方拉高VALID接收方准备好后拉高READY在两者同时为高的时钟上升沿数据就算正式传输完成。很多教材上来就是一堆信号表容易把人劝退。我觉得理解AXI最笨也最有效的方法是先把Master和Slave的角色分清。Master是发起请求的一方比如CPU、DMA、我们自己的自定义逻辑Slave是被动响应的一方比如BRAM控制器、寄存器组、DDR控制器。两边签好协议Master说我要读地址0x1000Slave就把数据送回来这个过程完全靠约定好的信号接口完成不需要关心对方内部怎么实现。1.2 这套设计的核心价值与应用场景AXI总线协议的FPGA设计常见应用场景非常广。首先是Zynq / MPSoC这类ARMFPGA异构平台ARM侧跑操作系统FPGA侧做自定义外设两者通过AXI接口互通其次是多模块实时数据流比如图像采集、预处理、后处理、显示输出每条链路都是高带宽数据流AXI的突发传输能把效率拉满还有一些接口桥接场景比如把SPI、I2C、UART这类低速接口封装成AXI Slave统一给处理器访问整个系统结构会清晰很多。我见过不少项目问题不是在算法逻辑上而是死在接口上。明明DMA写的数据也没错就是总感觉哪儿不对劲最后查下来是时序违例或者握手条件没写对。所以说AXI不仅仅是协议更是一个系统工程需要从Master端设计、Slave端实现、时序约束、仿真验证四个方面同时下手。这篇文章我会按这个顺序把每个环节的关键点拆开讲。2. AXI总线协议的关键机制理解透才能动手写写代码之前先把协议的几个核心机制吃透。AXI协议说复杂也确实复杂因为后面还有AXI-Stream、AXI-Lite、ACE缓存一致性、AXI-ATB追踪总线等一堆变种。但对于入门和实战我们只需要把标准AXI4或AXI4-Lite的通道握手、突发、读写流程搞明白就足够应对绝大多数场景了。2.1 握手信号与VALID/READY通道机制AXI每个通道都遵循VALID/READY握手规则。发送方拉VALID表示本周期数据/地址有效接收方拉READY表示本周期可以接收。规范文档里给了三种握手场景第一种是双方同时准备好VALID和READY在同一周期都为高立即传输第二种是发送方先准备好VALID先拉高接收方过几个周期才拉READY第三种是接收方先把READY拉高发送方过几个周期才拉VALID。协议允许这两种情况交叉出现但有一个硬性要求一旦VALID拉高在握手完成也就是两者同时为高之前VALID不能拉低。换句话说发送方一旦承诺这次数据有效就必须等着接收方绝不能把数据悄悄撤回去。这个机制看着简单实际写代码时最容易出问题。比如有的同学在状态机里为了省一个周期看到READY为高就立刻改变DATA但这时候VALID还没配对那上一拍的数据就被悄无声息地覆盖了接收方收到错误数据。正确做法是VALID拉高后数据必须保持稳定直到握手成功为止。2.2 突发传输地址与数据的节奏控制AXI4支持突发Burst传输也就是一次地址请求后连续传多拍数据。突发长度由ARLEN/AWLEN决定0表示传1拍7表示传8拍255表示传256拍突发大小由ARSIZE/AWSIZE决定0表示1字节2表示4字节3表示8字节。地址的增长规则和突发类型BURST也有关系FIXED为固定地址INCR为递增地址WRAP为回卷地址。最常用的是INCR比如读DDR里的连续数据块。这里要特别提醒突发长度和数据拍数必须对得上。比如你发了一个AWLEN7、AWSIZE2的写突发按4字节×8拍计算总数据量是32字节。在写数据通道上必须严格送出8拍数据WLAST在最后一拍拉高。如果中间数据通道还经常插入空闲WLAST的位置必须跟着有效拍走不能按绝对周期数算。很多第一次写Slave的人就在这里翻车数了8个时钟周期就拉WLAST结果中间有握手等待真正传的数据没够8拍对端就以为突发提前结束了。2.3 读事务与写事务的完整流程读事务相对简单Master先在读地址通道发一个地址ARVALID握手Slave收到后在读数据通道返回数据。第一拍数据可能和地址握手相隔若干周期因为Slave需要时间去访问内部存储。对于多拍突发数据通道会连续返回多拍最后一拍拉RLASTMaster在收到RVALID RREADY且RLAST1时认定这次读事务结束。写事务稍微复杂因为多了一个写响应通道。Master先发写地址AW通道然后发写数据W通道Slave把数据真正写入后在写响应通道回一个BRESP响应OKAY表示成功SLVERR表示失败。这里需要注意写响应返回的时机并不是在最后一拍写数据握手的同时而是在Slave内部真正把数据落位之后。比如你写一个BRAM需要一拍写使能那响应就要在那一拍之后返回。如果写地址、写数据还没完全处理好就急着回BVALIDMaster那边可能提前开始下一个事务导致数据还没写入就被覆盖。3. Master侧设计与时序拆解从状态机到代码落地接下来进入真正的代码设计环节。先讲Master侧因为Master是整个总线事务的发起者也是时序里最复杂、最容易写出死锁的一方。我以一个简单的寄存器和BRAM读写控制器为例目标是在FPGA里用AXI Master去反复读写一组地址用来验证链路完整性。3.1 Master接口架构与内部信号规划设计AXI Master时首先要明确它要接什么样的Slave。如果只是访问寄存器用AXI4-Lite就够地址和数据都是单拍没有突发如果要搬大块数据必须用完整的AXI4地址通道、数据通道都支持突发。为了通用性我习惯把两类接口都预留出来内部统一做状态机控制。模块的信号规划大致如下全局的axi_aclk和axi_aresetn读地址通道的axi_araddr、axi_arvalid、axi_arready、axi_arlen、axi_arsize读数据通道的axi_rdata、axi_rvalid、axi_rready、axi_rlast、axi_rresp写地址通道的axi_awaddr、axi_awvalid、axi_awready、axi_awlen、axi_awsize写数据通道的axi_wdata、axi_wstrb、axi_wvalid、axi_wready、axi_wlast写响应通道的axi_bresp、axi_bvalid、axi_bready。很多人抱怨信号太多记不住其实你把它们按通道一块一块归好类每个通道只有一个核心互动关系谁发VALID、谁回READY、通道上跑什么内容一下就看明白了。3.2 状态机设计IDLE、读写地址、读写数据、写响应我常用的Master状态机分这么几段IDLE空闲、READ_ADDR发读地址、READ_DATA收读数据、WRITE_ADDR发写地址、WRITE_DATA发写数据、WRITE_RESP收写响应。读写可以并行设计但初学时建议先把读写分开用同一个状态机串行处理等跑通了再考虑读写独立并行。在READ_ADDR状态下ARVALID拉高等待ARREADY握手成功后如果突发长度是1拍直接跳到READ_DATA等数据。READ_DATA状态下RREADY拉高当RVALID为高时接收数据如果RLAST为高说明这是最后一拍读完就回IDLE或者开始下一个事务。这里有个细节RREADY应该拉多早协议允许RREADY一直拉高只要Master随时能接收数据。如果你的Master内部有FIFO或者寄存器能暂存数据建议一直拉高RREADY这样Slave返回数据时不用等待效率最高。如果数据通路下游处理不过来那就要用FIFO的almost_full信号去门控RREADY。写事务的状态机需要多留一个心眼。在WRITE_ADDR成功握手之后不一定要等到地址通道处理完才能发数据。AXI协议允许写地址和写数据通道独立甚至可以先发数据再发地址。但为了代码清晰我习惯先发地址然后立即进入WRITE_DATA。数据通道要根据突发长度逐拍发送每一拍都在WVALID WREADY握手有效时递减一个计数器到最后一拍时拉高WLAST。等数据全部发完再进WRITE_RESP等待BVALID。如果Slave因为没有正确写入而返回SLVERRMaster要有能力感知这个错误至少拉一个错误标志方便调试。3.3 关键时序案例连续写操作的背靠背处理做DMA或数据搬移时经常需要连续发起多个写突发这时候如果每个突发之间都回IDLE再重来会有几个周期的气泡吞吐量上不去。AXI协议允许连续的突发之间不间隔上一个事务的最后一拍写数据握手完成的同时下一个事务的写地址可以先发出来甚至写数据通道可以紧接着发下一段数据只要不违反WLAST和WVALID的组合规则。实际操作上我用一个pending_burst计数器来管理每个通道上还有多少个未完成的事务。比如发起一个写突发后不等BVALID回来就继续发下一个写突发。因为写地址和写数据通道可以流水线化只有写响应通道需要一个一个匹配。这样做的代价是如果Slave处理能力有限内部缓冲满了AWREADY或WREADY会主动拉低我的Master就会在对应状态停下等待这是完全合规的。所以背靠背优化的核心不是把状态机写得多花哨而是懂得什么时候该等、什么时候该冲。3.4 Master侧代码示例Verilog核心片段下面给出一段简单的AXI4-Lite Master读地址发送部分方便大家对照理解// AXI4-Lite Master read request reg arvalid_r; reg [31:0] araddr_r; wire arready_comb axi_arready; always (posedge axi_aclk or negedge axi_aresetn) begin if (!axi_aresetn) begin arvalid_r 1b0; araddr_r 32h0; end else begin if (state S_READ_ADDR start_read) begin arvalid_r 1b1; araddr_r read_addr_q; end else if (arvalid_r arready_comb) begin arvalid_r 1b0; end end end assign axi_araddr araddr_r; assign axi_arvalid arvalid_r;上面这段代码就是把arvalid拉高同时锁存目标地址。当arready返回时握手完成撤销arvalid。这里面最关键的一点是arvalid_r一旦为高如果没有握手成功下一周期必须保持不能用其他状态机分支去把它清零。很多人喜欢用一个组合逻辑next_state直接赋值结果把arvalid在握手前清掉了这是很典型的错误。读数据接收部分类似重点是RREADY的赋值。如果是AXI4-Lite没有RLAST每笔请求只回一拍数据。只要Master内部有接收空间rready可以直接拉高assign axi_rready (state S_READ_DATA);握手时用axi_rvalid axi_rready作为数据有效标志采样即可。这里的精细点是如果rready只在S_READ_DATA状态为高那状态机切换到S_READ_DATA的周期和rvalid到达的周期可能有一个相位差需要仔细核对仿真波形确保没有漏采数据。4. Slave侧设计与时序拆解让Master的建议都能落地说完Master再看Slave。Slave是被动方它不能主动发起事务只能老老实实响应。但Slave设计的难点不是逻辑复杂度而是要在各种突发、各种等待中精准完成响应。我经常打比方Master是外卖小哥Slave是餐厅后厨。外卖小哥可以随时来取餐但后厨得保证菜都做好了、做得对不能前一个订单还没做完就接下一个更不能做完了不给小哥。4.1 Slave接口架构与地址译码方案设计AXI Slave时接口信号和Master是镜像的区别在于握手信号的方向例如AWVALID是输入AWREADY是输出WVALID是输入WREADY是输出BVALID是输出BREADY是输入。读数据通道上RVALID是Slave输出RREADY是Master输入。地址译码是Slave的主要逻辑之一。最简单的方式是用高几位比较如果地址落在某个区间就选对应的内部寄存器或BRAM。对于寄存器组每个寄存器对应一个偏移地址读操作时用ARADDR的低位做索引写操作时用AWADDR做索引。要注意的是AXI地址是字节地址而寄存器可能是32位宽所以最低两位通常不能直接用来索引寄存器编号右移两位才是寄存器序号。4.2 读Slave时序地址请求与数据返回读Slave的响应流程检测到ARVALID并且地址译码已准备好就可以拉高ARREADY完成握手。地址锁存后如果是简单的寄存器读下一拍就可以把数据放到RVALID和RDATA上。这里有个权衡如果寄存器的读取组合延迟太大就必须像流水线一样在地址握手完成后再用一个周期去读否则RDATA可能出现毛刺或延迟。如果Slave内部是BRAMBRAM本身有读延迟通常1~3拍需要根据延迟去规划RVLD的生成时机。举个例子我在某个项目里用Xilinx BRAM做Slave存储BRAM读地址在地址握手时写入数据在2拍后出现在读数据线上那么就需要一个延迟计数器在第2拍拉高RVALID并输出RDATA。如果Master端RREADY没有拉高RVALID要保持住同时RDATA也要保持。这里特别容易踩坑很多人会用一个组合逻辑把RVALID和某个内部read_data_valid信号直接赋值但这个信号如果在握手等待时被清掉了那Master就会一直等不到数据。解决这个问题有一个通用技巧用一个内部FIFO做读数据缓冲。把BRAM读出来的数据先写进FIFO然后由Slave状态机按握手规则读出到AXI接口上。这样即使Master偶尔不准备好数据也不会丢而且逻辑清晰很多。4.3 写Slave时序地址、数据、响应的协作写Slave的流程稍微绕一点。首先是AW和W两个通道可能独立到达甚至W数据先于AW地址到达。AXI协议允许这种情况所以Slave不能简单地先等待地址、再处理数据而是要把地址和数据的到达独立记录下来。我常用的方案是把地址先锁存到寄存器把写数据也先暂存下来当两者都齐了再执行实际写内部的寄存器或BRAM。以单拍写为例当AWVALID AWREADY握手完成后保存AWADDR当WVALID WREADY握手完成后保存WDATA和WSTRB。然后用一个状态标志位write_start当地址有效且数据有效时拉高一个周期执行写操作。执行完之后在写响应通道拉高BVALID返回BRESPOKAY。如果BREADY没有及时拉高BVALID必须保持住直到握手成功。有一点值得注意写响应返回之后地址锁存和写标志要及时清掉避免同一个地址被错误地写两次。另外如果Master连续发两个突发地址通道和数据通道的匹配关系要严格按发送顺序来。如果内部同时只能处理一笔写事务那么在返回BVALID之前应该把后续的AWVALID或WVALID用AWREADY0或WREADY0挡住防止数据覆盖。4.4 让Slave支持突发计数器与数据缓冲如果Slave要支持AXI4突发复杂度会上升一个级别。读突发相对好做关键是按ARLEN计数返回数据每返回一拍计数器加1到最后一拍拉高RLAST。写突发的关键则是正确解析WLAST信号和WVALID配合不能自己内部数周期。我的做法是在Slave内部维护一个write_burst_len寄存器从AWLEN取得突发长度再维护一个write_burst_cnt每个写数据通道握手有效拍自增一次当WVALID WREADY且WLAST为高时说明整个突发数据已经全部到达此时如果地址也已经锁存就可以执行批量写入。如果内部存储不支持一周期写多拍那就需要把数据先缓存到内部FIFO再逐拍写入。注意突发模式下WSTRB是每一拍独立的所以写逻辑要逐拍处理字节使能不能整包一起写。读突发也是一样ARLEN锁存后每发一拍数据RLAST就在最后一拍为高。有些Slave为了提升突发效率会做成一次读一个地址然后在内部线性递增地址。如果内部存储是双端口BRAM可以做到连续读不中断性能很好如果是单端口寄存器组请务必计算好读写冲突。5. 仿真与上板调试用逻辑分析仪和约束文件排查问题写完代码不代表完事仿真和调试才是AXI实战的重头戏。只要时序有一丁点不对连跑几万个周期之后才会露馅。我调试AXI接口的经验是先用仿真脚本把主流程跑通再用约束文件把接口时序约束好最后上板用ILA或逻辑分析仪抓波形。5.1 激励搭建与关键波形观测点测试AXI Master时需要例化一个简单的Slave模型通常用always块模拟响应检测到ARVALID时下一拍返回有效读数据检测到AWVALID和WVALID后返回写响应。测试AXI Slave时则要例化一个Master模型可以写成任务task一键发起读或写事务。仿真中要重点监控的信号包括所有通道的VALID和READY握手事件、RLAST/WLAST的位置、BRESP的值以及和内部状态机的对应关系。我习惯在关键握手处打$display日志例如if (arvalid arready) $display([%0t] AR handshake, addr%h, $time, araddr);这样跑完仿真后直接看打印日志就能知道事务走到了哪一步。如果发现日志中某个READY一直没有拉高基本可以锁定死锁卡在哪个通道上。5.2 时序约束的添加与关键路径分析AXI接口工作频率高时序约束必须做对。在Vivado里通常需要给axi_aclk创建时钟约束然后针对跨时钟域或其他特殊路径做约束。如果Master和Slave都在同一个时钟域只是普通同步逻辑一般不需要额外管脚约束。但如果接口要接到IP核或者外部引脚比如MIG的AXI接口就要注意MIG本身会输出对应的约束你只需要保证自己的逻辑符合这些约束的时序预期。使用report_timing_summary检查时序是必备步骤。AXI接口常见的时序违例出现在地址译码逻辑、数据路径的宽位宽信号上。比如RDATA是256位或512位时数据路径上组合逻辑容易变长。对策通常是增加流水线寄存器把大的组合逻辑切开代价是多一个周期的读延迟。AXI协议对延迟没有硬性要求只要Master能等多一个周期完全没关系。这也是AXI的灵活之处。5.3 常见死锁、数据错位问题的排查实录我在项目里遇到的一个典型问题Master发起写突发后给了AWADDR和AWLEN3但在写数据通道只发了两拍就拉高WLAST然后等待BVALID。Slave那边严格按WLAST判断风暴结束就认为这次突发只有2拍写了2个数据就返回响应。结果Master认为自己写了4拍后续数据全部错位。这个问题就是Master的WLAST生成逻辑写错了没有按握手有效拍数计数而是按绝对计数。排查的时候在仿真波形里看WVALID WREADY的有效次数和WLAST位置一眼就能发现问题。另一个常见问题时序握手死锁Master在等待BVALIDSlave却在等待BREADY返回响应两个信号互相等系统卡死。根因通常是Slave提前把BVALID拉高但Master状态机还没进入WRITE_RESP状态所以BREADY为0而Slave头脑简单死等BREADY为1才撤销BVALID两者磕上了。解决方式很简单Slave返回BVALID后如果BREADY为0就必须持续保持BVALID而Master必须在最后数据拍之后的周期无条件进入等待响应状态不要把其他处理插在中间。双方规则都遵守不可能死锁。再有一个数据错位问题Master在发写地址的同时数据通道还在传上一笔突发如果Slave只是简单地把地址和第一个数据配对就会把上一笔的数据写到新地址里。这是经典的跨突发错位。正确做法是在Slave收到AW握手时记录一个aw_pending标志在收到WLAST之前新的AW握手即使发生也不要覆盖写地址而是先缓存到队列。等当前突发结束后再处理下一个。这种问题在仿真中很容易被漏掉除非你特意做连续背靠背突发的激励。6. 从“能跑”到“跑得稳”的经验建议最后说点个人体会。AXI总线协议的FPGA设计很多人以为写完状态机、仿真通过就万事大吉实际不是。把它接到真实系统里往往还要考虑性能、资源、死锁恢复、错误处理这些问题。我在实际项目中踩过几次坑之后形成了一个习惯每个AXI组件都加上一个超时计数器。具体做法是当某个状态停留超过比如1024个周期还没有握手成功就拉一个超时中断要么复位状态机要么打印日志。这个机制在调试时能救命因为很多死锁一旦发生单纯看波形未必能快速找到是哪个环节有了超时定位会快很多。性能方面如果你追求高吞吐推荐把Master和Slave的接口都用FIFO缓冲起来。地址通道用一个小的aw_fifo数据通道用独立的w_fifo和r_fifo。这样即使上下游处理速度不一致也不会因为一两个周期的等待拖慢整体带宽。代价是资源占用和延迟增加但对大多数中高性能系统来说这个牺牲非常划算。另外AXI协议文档很长不用死记硬背。把常用的通道握手机制、突发规则、写响应流程记住剩下遇到特殊情况再去翻ARM官方规范。我建议每个做FPGA的人手里都留一份AXI4规范PDF遇到拿不准的信号语义直接查原文不要凭感觉写代码。毕竟时序这种东西感觉是会骗人的只有波形和规范不会。
返回列表