AXI总线入门:一份能直接跑仿真和综合的源码工程
发布时间:2026/10/28 17:04:56 锦皓数字建站

简介AXI总线源码与验证工程包面向ZYNQ/FPGA开发者和嵌入式系统学习者围绕AXI4-Lite与AXI4-Full协议展开旨在帮助读者理解高性能、低延迟的SoC互连规范及Vivado中的实现流程。压缩包约37.73MBRAR格式内含五个主要工程模块AXI4-Lite LED驱动、AXI4-Full仿真验证、Vivado构建工程、Lite接口模拟以及板级LED测试覆盖从基础控制到高速数据交互的典型场景。目前已有3128人学习适合具备一定HDL/FPGA基础的中高级工程师入门或复盘AXI总线设计。通过源码与仿真工程可系统观察地址、数据、控制信号的交互过程对比轻量级与全功能接口的读写流水线差异并借助物理板级验证将理论知识落地为实际运行结果为后续自定义IP或SoC系统开发提供参考。1. AXI总线入门没有捷径一份能直接跑仿真和综合的源码工程AXI总线这个名词做ZYNQ和FPGA开发的迟早要正面撞上。PS端和PL端传数据、给DMA或VDMA挂缓冲Vivado里各种IP核内部几乎都靠AXI4协议通信。新手看ARM的AMBA规范几百页看得头晕而项目里真正要的往往只是一个能用的AXI4从机、一段能发起读写的master、一个能看到握手过程的testbench。这份“AXI总线轻松搞定AXI总线源码和验证工程AXI4.rar”打包的正是这三样。对想在最短时间内跑通AXI4仿真的人特别合适也适合调板子时怀疑总线握手异常的熟手做参照对比。2. AXI4协议核心读懂五个通道、握手机制和突发参数再动手2.1 五个通道各管一摊读写分离是AXI4的设计底色打开AXI4协议图第一眼看到的不是信号列表而是五条平行通道。很多人被绕晕是因为把AXI4误当成传统总线理解——地址线和数据线复用总线要切换方向。AXI4不是这个逻辑它把读和写拆成两套独立链路各自有地址通道和数据通道写事务还多一条响应通道。看下面这张通道分工表基本就把骨架记住了通道方向干什么用关键信号写地址通道 AW主→从发送写地址、突发长度、突发大小、突发类型AWADDR、AWLEN、AWSIZE、AWBURST、AWVALID、AWREADY写数据通道 W主→从承载写数据按字节选通WDATA、WSTRB、WLAST、WVALID、WREADY写响应通道 B从→主一次写突发结束后回报OKAY或ERRORBRESP、BVALID、BREADY读地址通道 AR主→从发送读地址和突发控制信息ARADDR、ARLEN、ARSIZE、ARBURST、ARVALID、ARREADY读数据通道 R从→主返回读数据和响应状态RDATA、RRESP、RLAST、RVALID、RREADY一个完整的写动作是AW通道发一次地址W通道按突发参数发多拍数据最后B通道回报一个响应。读动作更直白AR发一次地址R返回长度固定的数据拍。因为读写物理上分开主设备完全可以一边等写响应、一边发读请求这就是outstanding并发的基础。做协议分析时先分清每根信号属于哪个通道再去看时序图基本就不会迷路。2.2 握手协议VALID和READY同时为高才算一拍每个通道的两端靠VALID和READY两个信号握手。协议规定得比较死的主要是三条规则这份源码里对应位置基本都能看到注释。第一条源端一旦把VALID拉高就要保持到握手成功不允许因为没看到READY就把它拉低。很多第一次写AXI4的人会写出“if(awready) awvalid1; else awvalid0;”这种组合逻辑这在仿真里会表现为波形抖动在综合后就是时序收敛难。第二条目标端的READY可以早拉也可以等VALID来了再拉但如果本拍没有发生握手VALID和READY没有同时为高READY不能随意拉低除非接口本身处于空闲等特殊情况。第三条也是唯一的数据完成判据VALID与READY在同一拍同时为高这一拍的数据才算真正被传输。我用一个简单的寄存器写地址示例来说明// 握手判据只有当awvalid与awready同时为高地址才被从机锁存 wire aw_fire awvalid awready; reg [31:0] awaddr_r; always (posedge clk or negedge rst_n) begin if (!rst_n) awaddr_r 32h0; else if (aw_fire) awaddr_r awaddr; // 握手成功拍锁存本次写地址 end代码逻辑很简单但注意一点所有数据搬移逻辑都应该基于aw_fire这类一拍脉冲而不是基于awvalid单独判断。我见过不少从机源码里用awvalid做条件、却不关心awready结果对端READY没拉高时地址数据被过早锁存属于低级但高发的错误。真正从协议层面理解握手是把这个判据贯穿到五个通道里去。2.3 突发三要素AxLEN、AxSIZE、AxBURST决定一次传输的边界AXI4之所以快靠的是突发传输地址只听一次后面的数据连续跟上不用每次单独发地址。突发行为由三个参数共同决定这份源码里的从机也是围绕这三个数写状态机的。参数宽度含义与约束常见取值AxLEN8bit突发长度实际拍数AxLEN1AXI4最大256拍单拍用8h00批量用8h0F16拍AxSIZE3bit每拍传输字节数2^AxSIZE不能超过数据总线字节宽度AXI4从机数据位宽64bit时取38字节AxBURST2bit0FIXED1INCR2WRAP访问连续内存用INCRFIFO用FIXEDINCR模式最常用地址每拍递增2^AxSIZE个字节。比如AxSIZE38字节AWADDR0x40那数据在0x40、0x48、0x50……依此类推直到突发结束。WRAP的地址会在一个对齐块内回绕主要用于cache line做普通外设访问时基本用不到。FIXED则不管突发多长地址一直保持不变适合对FIFO口反复写。协议还要求起始地址对齐到本次突发传输宽度即AWADDR[AxSIZE-1:0]必须为0。这个坑很隐蔽很多master发出的地址不对齐却期待从机自己兜底从机一旦不支持读出来的数据就是错位的。这个问题我会在第5章专门给一个避坑项。2.4 从机源码的骨架通道状态机与WLAST递减计数器这份RAR里的核心源码通常由一个AXI4从机接口的RAM或寄存器模型组成。理解它的关键在于看W通道怎么配合AW通道完成突发。给出WLAST生成最常见的写法reg [7:0] wlen_r; // 突发拍数计数器AW握手时装载AxLEN每完成一拍W传输减1 always (posedge clk or negedge rst_n) begin if (!rst_n) wlen_r 8h0; else if (aw_fire) wlen_r awlen; else if (w_fire (wlen_r ! 8h0)) wlen_r wlen_r - 8h1; end // WLAST信号与最后一拍数据对齐 wire wlast_out (w_fire (wlen_r 8h0)) ? 1b1 : 1b0;计数器在aw_fire时装载awlen之后每完成一拍写数据递减一次减到0时这一拍就是突发最后一拍。从机可以根据wlast_out去锁存写数据到BRAM并在下一拍拉高BVALID给出响应。读通道的RLAST同理都是跟最后一拍数据对齐。这里有一点必须提醒AxLEN是“拍数减一”8h00表示单拍8hFF表示256拍。很多人在写burst计数时直接拿AxLEN当拍数去循环结果总是少传一拍仿真里表现为写数据少了一拍、响应来得很勤快波形一对比就能发现问题。这个细节属于一看就懂、一写就错的那种建议拿到源码后先在TB里把单拍、16拍、256拍三种长度各跑一遍。3. 把RAR里的工程点亮Vivado建工程、跑仿真、看波形的完整操作3.1 文件结构先分清rtl、tb、脚本各是干什么的拿到资源压缩包别急着双击xpr工程先在目录树里点一遍看清楚每一份文件的作用。以这套AXI4源码和验证工程为例解压后目录通常是这么组织的路径或文件类型作用rtl/axi4_slave.v可综合源码AXI4从机接口内部是双口RAM或寄存器堆rtl/axi4_master.v可综合源码产生读写激励的master突发参数可配置rtl/axi4_defines.vh头文件参数宏定义比如数据宽度、地址宽度sim/tb_axi4_ram.vtestbench例化master和slave发握手激励并检查读回数据sim/tb_axi4_ram_behav.wcfg波形配置Vivado波形窗口预设信号分组scripts/create_project.tcl脚本一键自动建工程、加文件、跑仿真testbench先例化master再例化slave做成一个闭环master往slave的地址空间写一串数据再读回来比对。刚上手的人会以为testbench只是把接口信号拉一拉实际上验证工程里最值得看的是master内部那套burst计数和读回比较逻辑那是写出正确AXI4主控的参照模板。我一般会先打开TB文件搜索$error和$display出现的位置那些就是验证的检查点跑完仿真后log里所有的PASS/FAIL都来自这些打印。3.2 用TCL脚本一键建工程并启动仿真Vivado里手动New Project点起来并不难难的是把文件路径、库属性、top设置一遍遍重复。我拿到这类工程的第一件事是看有没有现成的TCL脚本。这套资源里的create_project.tcl大概就是这么一段set part xc7z020clg400-1 create_project axi4_demo ./axi4_demo -part $part -force add_files -norecurse [glob ./rtl/*.v] add_files -fileset sim_1 -norecurse [glob ./sim/*.v] set_property top axi4_ram_top [current_fileset] update_compile_order -fileset sources_1 set_property top tb_axi4_ram [get_filesets sim_1] launch_simulation -mode behavioral这段脚本做的事情按顺序拆开看第一行指定器件ZYNQ的芯片可以用xc7z020clg400-1如果你的板卡是xc7z010或xc7z035改这一行part即可create_project第二个参数是工程目录-force表示目录存在时删除重建add_files把rtl目录和sim目录里的.v文件分别加到综合文件和仿真文件集这一步最容易出错的是路径写错导致文件没加进去最后一行直接启动行为仿真不手动开GUI点那么多菜单。我一般会在命令行里执行vivado -mode batch -source scripts/create_project.tcl一条命令把工程建出来并打开波形。对需要反复换工程做的场景比手点快得多也比记菜单路径可靠。如果你拿到手的压缩包里没有这个脚本按上面的逻辑自己拼一个也就十分钟的事。3.3 跑仿真先看这四组信号握手、数据、last、响应仿真启动后波形窗口默认可能只显示testbench顶层信号看不到内部从机的通道信号。先手动把需要观察的信号加进来按通道分组最直观add_wave -group AW {/tb_axi4_ram/u_slave/awaddr} add_wave -group AW {/tb_axi4_ram/u_slave/awvalid} add_wave -group AW {/tb_axi4_ram/u_slave/awready} add_wave -group W {/tb_axi4_ram/u_slave/wdata} add_wave -group W {/tb_axi4_ram/u_slave/wstrb} add_wave -group W {/tb_axi4_ram/u_slave/wlast} add_wave -group B {/tb_axi4_ram/u_slave/bresp} add_wave -group B {/tb_axi4_ram/u_slave/bvalid} add_wave -group B {/tb_axi4_ram/u_slave/bready}看波形时我习惯按顺序扫四块先是复位释放后第一个通道的握手确认没有任何一拍VALID在等待READY时被拉低再核对WDATA和WSTRB是否对齐32位总线下写完4字节时WSTRB应该是1111然后找WLAST、RLAST的位置确认它们和突发最后一拍是同一拍最后看BVALID和BRESPOKAY响应不出现在错误时机。如果仿真结果和预期不符不要整个testbench重新跑先把波形光标点在手握手信号上测量各通道的响应延迟。大多数死锁问题从波形图上找第一个没人响应的VALID就能定位比我当初对着代码干瞪眼快得多。另外一个常见情况是波形窗口里信号全是X态先检查TB里复位释放时间是不是在仿真结束之后这种时序错位会让整个总线从头到尾都没起来。4. 挂到ZYNQ PS端三种AXI变体选型、IP封装与SDK读写实测4.1 三种变体别选错Full、Lite、Stream各管一片AXI4在Xilinx体系里演化出三个变体三者的接口长得很像但特性差异极大。选错变体是项目里最常见的架构级失误。变体特性适合场景典型IPAXI4Full支持突发、outstanding、ID乱序DMA、DDR控制器、批量搬运AXI DMA、MIGAXI4-Lite单拍传输无突发地址直接访问寄存器控制寄存器、状态读取、配置GPIO、UART、自定义寄存器AXI4-Stream无地址无响应纯粹数据流视频流、音频流、FIFO接口AXI Stream FIFO、Video In/Out这套RAR里的源码通常默认实现的是AXI4 Full接口因为它要演示最完整的burst逻辑实际项目里真要挂在ZYNQ PS端做控制寄存器我会建议把接口降成AXI4-Lite代码面积小一半时序压力也小。数据通路才用Full不要一个接口从头用到尾。判断依据很简单需要连续搬数据的用Full或Stream需要CPU配置寄存器的用Lite三者共存没有任何问题。4.2 把从机源码封装成自定义IP在Vivado里把写好的AXI4从机挂到ZYNQ PS正规做法是走IP封装。菜单路径是Tools - Create and Package New IP - Next - Create AXI4 Peripheral填好IP名字后界面会让你选接口类型这时选AXI4还是AXI4-Lite完全取决于外设定位寄存器配置型就选Lite。模板生成后不要直接拿模板里那套多通道状态机去改先找到user logic区域的注释把模板自带的寄存器读写逻辑换成你自己模块的桥接逻辑。常见做法是保留模板的握手和地址译码把自己的从机模块或RAM模型放进user logic里两个模块之间的数据通道用aw_fire、w_fire这些拍脉冲对接。封装完成后在Address Editor给这个IP分配地址段ZYNQ PS的M_AXI_GP0默认能看到0x40000000左右的高地址空间具体以xparameters.h导出结果为准上板前不要凭印象写地址。注意IP封装时只加可综合的rtl文件sim目录里的testbench和wcfg波形配置不要混进IP工程。把验证文件加进IP核综合时会报出一堆不可综合的语法错误还容易把仿真库带乱。4.3 在SDK里用PS端发起一次读写实测产业界通常不会只做仿真上板才是最终验收。用Vivado导出硬件后打开Vitis或SDK写一段最直接的读写测试来验证AXI4通道和地址译码#include xil_io.h #include xil_printf.h #define MY_AXI_BASE 0x80000000U void axi4_rw_test(void) { unsigned int rd_val; Xil_Out32(MY_AXI_BASE 0x00, 0x5A5A1234); rd_val Xil_In32(MY_AXI_BASE 0x00); if (rd_val ! 0x5A5A1234) xil_printf(AXI4 mismatch: addr0 got %08x\r\n, rd_val); else xil_printf(AXI4 readback pass\r\n); }Xil_Out32和Xil_In32是SDK里推荐的带volatile语义的访问宏编译后就是普通的单次写和读操作地址MY_AXI_BASE必须来自Vivado导出的地址映射而不是拍脑袋定的值。第一次上板时我吃过一次亏——直接在代码里写了0x40000000结果对应的是另一个GP端口数据全部写到了不存在的地址上读回来的全是垃圾值。用xparameters.h里的宏替代硬编码是后来养成的习惯。4.4 综合布局后的时序检查AXI接口的路径约束与常见报警AXI接口在ZYNQ里经常跨时钟域综合后report_timing_summary会报一组setup或hold违例尤其是WLAST和RLAST的生成路径。原因大多是RTL里把WLAST逻辑串了太长的组合链比如同时判断wlen等于0和wstrb等于全1组合深度一上去时序就难收敛。我一般会在自定义从机的XDC里给输入总线加一组参考约束set_input_delay -clock [get_clocks clk] -max 4.0 [get_ports {awaddr[*]}] set_input_delay -clock [get_clocks clk] -min 1.0 [get_ports {awaddr[*]}]这组约束只是一个模板真正数值要以PS端输出的时钟关系为准。加了之后看时序报告里awaddr到内部寄存器的路径如果还有余量说明从机的输入级寄存器做得没问题如果violation很大就该回去检查是不是在握手还没完成时就把地址送入了组合逻辑。时序问题在仿真阶段看不出来只能通过约束和报告来暴露所以这部分也是改用AXI4后比普通逻辑设计多出来的工作量。5. 避坑AXI4仿真死锁和上板异常的五个高频雷区5.1 仿真时间卡住不动波形停在某一拍现象launch_simulation后时间光标一直往前走但所有信号从某个点开始再也不变化仿真像死了一样。原因AXI4握手形成死锁。典型场景是master已经拉高AWVALID等从机就绪但从机的状态机还在等另一个通道的条件或者W通道的数据没有推送从机却等在W通道完成才肯发起下一次AW。解决把波形放大到停滞点找到第一个VALID为高且长时间等不到READY的信号直接反查驱动它的状态机。最实用的保护是在testbench里加握手超时断言比如等100拍还没握手就$error把死锁点暴露在log里这个手法我会在最后一章专门展开。5.2 WVALID只维持了一拍突发的后续数据全丢现象突发长度为16拍但波形里WVALID只在第一拍拉高后面变成了低电平从机写进RAM的数据只有半截。原因主设备的WVALID生成逻辑错误。协议要求VALID一旦拉高必须保持到与READY握手成功这是为了防止从机按READY时序采样时出现不确定。很多第一次写master的人会把WVALID设计成“看到WREADY才发下一拍”导致它被READY反向控制。解决WVALID应该在突发开始一拍时拉高之后无条件保持直到WVALID与WREADY握手完成并且本拍就是WLAST才拉低结束本次突发。我一般会用突发计数器在非零状态强制保持WVALID这样逻辑上不存在被READY回压的路径。5.3 读出来的数据错位地址没对齐是元凶现象仿真里地址、数据看起来都对但读回的第N拍数据实际上是对应地址N-1拍的内容整体错位。原因AXI4要求读写地址必须对齐到突发的传输宽度。比如AxSIZE3每拍8字节地址最低3位必须为0如果从机只接受对齐访问而你给的地址低bit非0从机可能按自己的规则解析最终数据和地址对应关系就对不上。解决在master侧对存储地址做对齐处理或者在从机侧把非对齐访问拆成多拍。最省事的办法是在接口参数里固定AxSIZE等于数据总线字节宽度并约束起始地址低bit为0不给非对齐留退路。功能验证阶段这么做完全够用性能测试里才需要具体计算非对齐访问的开销。5.4 WSTRB全0但写数据还在传RAM进了一堆垃圾现象波形上W通道的WDATA确实有值但RAM里写进去的数据是乱的单步调试发现WSTRB一直是0。原因WSTRB是字节选通对应数据总线每个字节。32位数据总线WSTRB[0]对应WDATA[7:0]要写满4字节必须让WSTRB4b1111。代码里如果只推动WDATA而忘记推动WSTRB仿真器不会报错但从机按字节选通写入时全部被屏蔽。解决最简单的做法是设置WSTRB与WDATA同步生成突发开始时置为全1。只有做非对齐写时才需要精确到字节的选通其他场合全1最省心。我把这条写进过团队代码规范从那以后这种低级bug就没再出现过。5.5 上板偶发数据错仿真却一次过现象仿真怎么跑都对下载到ZYNQ上之后长时间跑会出现偶发的数据不一致而且频率不高很难抓。原因仿真没覆盖到的两个点——ID乱序返回和跨时钟域。如果从机不支持乱序读数据必须严格按照AR通道发出顺序返回一旦master同时挂起多个读事务从机返回顺序错乱数据就串了。另外AXI时钟和用户逻辑时钟不一致时没有异步FIFO隔离也会导致偶发。解决先把从机的返回顺序强制改为按地址FIFO顺序即in-order模型跨时钟域用双口RAM或异步FIFO做缓冲不要依赖仿真环境里的“刚好一个周期对齐”。上板调试时用ILA抓RVALID和ARVALID的间隔看返回顺序是否与请求顺序一致。6. 小技巧在testbench里给握手加上超时看门狗死锁立刻现形6.1 计数器式握手看门狗AXI4仿真里最恼人的是死锁原因千奇百怪但表现都一样信号停在那仿真器不走。与其盯着波形一根根找不如在testbench里加一个最原始的看门狗每个通道都盯着超过门限还不握手就打印错误。integer aw_timeout_cnt; wire aw_pending awvalid !awready; always (posedge clk) begin if (!rst_n) begin aw_timeout_cnt 0; end else if (aw_pending) begin if (aw_timeout_cnt 32) $error([%0t] AW timeout, awaddr%h, awlen%d, $time, awaddr, awlen); aw_timeout_cnt aw_timeout_cnt 1; end else begin aw_timeout_cnt 0; end end逻辑说明aw_pending表示地址已经过来但从机一直没就绪计数器在挂起期间累加超过32拍就$error。32拍这个门限不是拍脑袋定的它比正常响应延迟大一个数量级又不会长到掩盖逻辑错误。同理可复制出AR、W、B四个通道的看门狗。6.2 把看门狗收成task复用如果一个testbench里有多个master要激励挨个复制太啰嗦。我通常把上面的逻辑包成一个task用参数传入对应通道的VALID和READY信号这样每个master例化一份代码量不涨。如果你的资源里没有这个自己加上很快改起来也不伤其他验证逻辑。6.3 把门限调成随burst长度自适应对长突发场景固定32拍可能误报。常见做法是把门限设成AwLEN32挂起时间超出一个完整突发应有的时间才报错这样既能抓住死锁又不会因为数据量大而误报。改法很简单把比较条件从常数32换成wlen_r 32即可。从那以后我每次新增AXI4外设或者改从机逻辑第一件事不是综合而是先加这套看门狗再跑一遍验证确保握手不卡死才敢进布局布线。如果你手头正好在调AXI4这套带验证工程的源码可以直接拿来当参照把看门狗逻辑加进去很多“仿真莫名不动”的玄学问题第一次跑log就能定位。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。