简介:AXI总线源码与验证工程包面向ZYNQ/FPGA开发者和嵌入式系统学习者,围绕AXI4-Lite与AXI4-Full协议展开,旨在帮助读者理解高性能、低延迟的SoC互连规范及Vivado中的实现流程。压缩包约37.73MB,RAR格式,内含五个主要工程模块: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或ERROR | BRESP、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) awvalid=1; else awvalid=0;”这种组合逻辑,这在仿真里会表现为波形抖动,在综合后就是时序收敛难。
第二条,目标端的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 <= 32'h0; else if (aw_fire) awaddr_r <= awaddr; // 握手成功拍,锁存本次写地址 end代码逻辑很简单,但注意一点:所有数据搬移逻辑都应该基于aw_fire这类一拍脉冲,而不是基于awvalid单独判断。我见过不少从机源码里用awvalid做条件、却不关心awready,结果对端READY没拉高时,地址数据被过早锁存,属于低级但高发的错误。真正从协议层面理解握手,是把这个判据贯穿到五个通道里去。
2.3 突发三要素:AxLEN、AxSIZE、AxBURST决定一次传输的边界
AXI4之所以快,靠的是突发传输:地址只听一次,后面的数据连续跟上,不用每次单独发地址。突发行为由三个参数共同决定,这份源码里的从机也是围绕这三个数写状态机的。
| 参数 | 宽度 | 含义与约束 | 常见取值 |
|---|---|---|---|
| AxLEN | 8bit | 突发长度,实际拍数=AxLEN+1,AXI4最大256拍 | 单拍用8'h00,批量用8'h0F(16拍) |
| AxSIZE | 3bit | 每拍传输字节数=2^AxSIZE,不能超过数据总线字节宽度 | AXI4从机数据位宽64bit时取3(8字节) |
| AxBURST | 2bit | 0=FIXED,1=INCR,2=WRAP | 访问连续内存用INCR,FIFO用FIXED |
INCR模式最常用,地址每拍递增2^AxSIZE个字节。比如AxSIZE=3(8字节),AWADDR=0x40,那数据在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 <= 8'h0; else if (aw_fire) wlen_r <= awlen; else if (w_fire && (wlen_r != 8'h0)) wlen_r <= wlen_r - 8'h1; end // WLAST信号与最后一拍数据对齐 wire wlast_out = (w_fire && (wlen_r == 8'h0)) ? 1'b1 : 1'b0;计数器在aw_fire时装载awlen,之后每完成一拍写数据,递减一次,减到0时这一拍就是突发最后一拍。从机可以根据wlast_out去锁存写数据到BRAM,并在下一拍拉高BVALID给出响应。读通道的RLAST同理,都是跟最后一拍数据对齐。
这里有一点必须提醒:AxLEN是“拍数减一”,8'h00表示单拍,8'hFF表示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.v | testbench | 例化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和BRESP,OKAY响应不出现在错误时机。
如果仿真结果和预期不符,不要整个testbench重新跑,先把波形光标点在手握手信号上,测量各通道的响应延迟。大多数死锁问题,从波形图上找第一个没人响应的VALID就能定位,比我当初对着代码干瞪眼快得多。另外一个常见情况是波形窗口里信号全是X态,先检查TB里复位释放时间是不是在仿真结束之后,这种时序错位会让整个总线从头到尾都没起来。
4. 挂到ZYNQ PS端:三种AXI变体选型、IP封装与SDK读写实测
4.1 三种变体别选错:Full、Lite、Stream各管一片
AXI4在Xilinx体系里演化出三个变体,三者的接口长得很像,但特性差异极大。选错变体是项目里最常见的架构级失误。
| 变体 | 特性 | 适合场景 | 典型IP |
|---|---|---|---|
| AXI4(Full) | 支持突发、outstanding、ID乱序 | DMA、DDR控制器、批量搬运 | AXI DMA、MIG |
| AXI4-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要求读写地址必须对齐到突发的传输宽度。比如AxSIZE=3(每拍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字节必须让WSTRB=4'b1111。代码里如果只推动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拍可能误报。常见做法是把门限设成AwLEN+32,挂起时间超出一个完整突发应有的时间才报错;这样既能抓住死锁,又不会因为数据量大而误报。改法很简单,把比较条件从常数32换成wlen_r + 32即可。
从那以后,我每次新增AXI4外设或者改从机逻辑,第一件事不是综合,而是先加这套看门狗再跑一遍验证,确保握手不卡死才敢进布局布线。如果你手头正好在调AXI4,这套带验证工程的源码可以直接拿来当参照,把看门狗逻辑加进去,很多“仿真莫名不动”的玄学问题,第一次跑log就能定位。希望帮到你。
本文还有配套的精品资源,点击获取