ARTICLE DETAIL

资讯详情

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

基于Cocotb和cocotbext-axi的AXI总线最小验证环境搭建

基于Cocotb和cocotbext-axi的AXI总线最小验证环境搭建 很多做验证的朋友第一次接触AXI总线模块时脑子里蹦出来的方案基本就是两套要么开一套UVM环境要么直接上商用的AXI VIP。这两条路本身没毛病但对一个刚起步的IP验证、或者只是想快速看接口时序的场景来说成本确实有点高。我自己前几年开始尝试Cocotb配合cocotbext-axi这个现成的AXI驱动库搭环境的体感一下就不一样了——不用再写一整套agent、driver、monitor测试用例直接用Python协程组织读写事务像调用函数一样简单。这篇文章我会把一套最小可用的AXI总线验证环境完整拆开讲从为什么用Cocotb、AXI协议里哪些点必须先吃透到目录结构、DUT代码、测试用例和Makefile所有代码都是我本地跑过验证的版本。你只要装好依赖、按文章里的结构复制一遍就能在自己机器上看到波形和读写比对结果。如果你是刚接触AXI验证的新人或者正在评估轻量级验证方案的工程师这篇应该能帮你省掉不少摸索时间。1. 为什么用Cocotb搭AXI验证环境1.1 传统UVM方案的两座大山UVM验证AXI的问题从来不是能力不够而是启动成本太高。你要搭一个能用的UVM环境至少得有sequencer、driver、monitor、scoreboard、寄存器模型和一大堆sequence然后还要处理SV里的参数化、宏定义和类型转换。这些东西对于芯片公司的平台团队来说不是问题但对单个IP验证、算法模块联调、或者学校实验室里跑实验的人来说就是两座大山学习曲线陡环境搭建慢。商用VIP就更不用说了授权费用、安装配置、文档阅读每一步都在消耗时间。我从实际接触过的项目里得到的感受是UVM和商用VIP更适合Put in一个成熟的大团队流程里因为它们要解决的是可复用性、覆盖率闭环和大规模回归问题。如果你只是要验证一个AXI Slave接口、一个DMA控制器、或者一组带AXI接口的加速器模块用UVM就像开着集装箱卡车去买菜——不是不行是没必要。Cocotb的价值恰好在这个位置。它把仿真环境的组织方式从“类继承工厂创建TLM通信”变成了“Python协程事件等待对象调用”这就把验证环境的搭建门槛从几周降到了几小时。你不需要精通SystemVerilog的OOP语法不需要处理复杂的uvm_config_db参数传递甚至不需要整套VIP的授权文件。一个Python文件里写几个await就能完成对AXI接口的驱动和采样。1.2 Cocotb的异步模型让我少写了什么Cocotb的核心是Python的原生异步机制。你的测试用例写成一个协程然后用await去等待时钟上升沿、信号值变化或者某个函数返回。这个模型和硬件仿真天然贴合因为验证的本质就是“等事件、做操作、再等待、再校验”。用一个具体例子说明。如果我想向AXI Slave发起一次写事务然后在写响应返回后再发起读在Cocotb里逻辑是await axi.write(0x100, b\x11\x22\x33\x44) resp await axi.read(0x100, 4)这两行代码已经把地址、数据、握手、响应处理全部包住了。你不必手动去写“拉高awvalid等awready拉低awvalid再等wready……”这些逻辑。对于一个有多年Verilog Testbench经验的人第一次看到这种写法会有点不真实但它确实是Cocotb生态里cocotbext-axi库的日常操作。对比UVM同等功能至少要写几十行甚至上百行代码而且这些代码都是验证环境的“管道”跟你要验证的功能关系不大。Cocotb真正做到把注意力从“怎么驱动信号”转移到“验证什么功能”上这对快速迭代非常有价值。1.3 cocotbext-axi面向AXI的现成drivercocotbext-axi是Cocotb社区里比较成熟的一个总线验证库。它提供了一组开箱即用的组件包括AxiMaster、AxiSlave、AxiMemory、AxiRam这些类。AxiMaster可以直接发起读写事务AxiSlave能模拟AXI从机的行为AxiRam则可以当做一个内存模型挂在总线上做数据比对用。我第一次用这个库的时候惊讶的不仅是它的API简洁还有它对AXI协议细节的处理。AW通道和W通道的独立握手、WLAST的生成、B通道的响应时序、AR和R通道的读数据返回逻辑这些都是库内部封装好的。你不需要重新写driver去处理这些繁琐逻辑只需要告诉它“写哪个地址、给什么数据、读多少字节”就可以了。另外它还支持burst类型和长度的配置。你可以在调用write或read时指定burst是INCR还是FIXED也可以让库自动根据数据长度切分beat。这些功能在验证总线带宽和地址递增逻辑时特别有用后面我会在测试用例部分细讲。1.4 什么场景下不建议硬上CocotbCocotb不是银弹有些场景我还是会老老实实回到UVM或商用VIP。最典型的是大型SoC级验证涉及几十个引号、复杂中断、多主多从仲裁、功耗域控制这时候UVM的标准化组件和成熟IP库仍然是效率最高且风险最低的选择。另一个场景是团队协作如果团队所有人都只写SystemVerilog突然引入Python技术栈人员培训和代码维护成本反而更高。还有一点要注意Cocotb本身不替代覆盖率工具。如果你要收集功能覆盖率、做formal verification联动或者需要和某个商用EDA工具的VIP深度集成那么Cocotb的生态可能还不够。我的经验是Cocotb适合算法验证、IP级验证、快速原型环境、跨语言协同验证这些场景在这些场景下它能发挥出最大的灵活性。2. AXI协议中验证环境必须先吃透的部分2.1 五个通道先说清楚谁和谁握AXI4一共有五个通道写地址AW、写数据W、写响应B、读地址AR、读数据R。很多新手刚接触的时候会被这五个通道绕晕我的记忆方式是分两组看。写传输需要单向三个通道AW负责下单W负责送货B负责回执读传输只需要两个通道AR负责询问R负责把数据送回来。每个通道核心的握手信号只有两个VALID和READY。发送方拉高VALID表示“我现在有数据/地址要发”接收方拉高READY表示“我现在可以收”。一次传输成功的条件是VALID和READY同时为高然后在时钟边沿采样完成。验证环境中经常踩的一个坑是握手信号的依赖关系。简单说发送方不能等VALID被接收方响应之后再撤掉也不能依赖READY拉高才拉高VALID。协议要求VALID一旦拉高必须保持到握手成功。反过来接收方可以提前拉高READY等数据也可以在VALID有效后再给出READY。这些规则在cocotbext-axi内部已经处理好但你自己写monitor或者对信号做断言检查时一定要按照这个依赖关系去判断否则容易误报。2.2 Burst、size和地址推进AXI的Burst机制是协议里最核心也最容易出问题的地方。AW和AR通道上有三个关键信号AxLEN表示突发传输的拍数减1AxSIZE表示每一拍传输的字节数AxBURST表示突发类型。BURST类型分三种FIXED是每一拍都用同一个地址INCR是每一拍地址递增WRAP是在固定边界内回卷。验证中需要跟DUT进行比对的时候最关键的就是地址推进逻辑。以INCR类型为例如果起始地址是0x100SIZE是4字节即AxSIZE3d2LEN是3表示实际4拍那么四个beat的地址就是0x100、0x104、0x108、0x10C。这个计算公式看起来简单但一旦地址不是按SIZE对齐的时候情况就复杂了很多DUT在地址递增逻辑上出bug恰恰就出在地址低位的翻转处理上。cocotbext-axi的API在设计上已经考虑到了这个问题你不需要显式地计算每个beat的地址只需要给出起始地址和完整数据库会自动按字节地址做切分。但作为验证工程师你必须清楚这个计算过程否则当你自己写reference model或者检查DUT的地址更新时很难定位到问题根因。2.3 读响应与ID乱序的约定AXI的ID机制是另一块容易让新手懵的内容。协议规定不同ID的事务可以乱序完成但同一ID的事务必须按顺序完成。这意味着在验证环境里如果你同时发起多个outstanding读请求你收到的R通道响应可能和你发出的AR请求顺序不一致你要靠ID字段把它们对应起来。cocotbext-axi的AxiMaster在默认情况下是顺序发起事务的也就是说前一个事务完成后才会发起后一个。这降低了验证环境的复杂度但也意味着你可能测不到DUT的乱序处理能力。如果你需要验证DUT的乱序特性我的建议是在掌握基础API之后自己写一个outstanding读事务发生器用多个协程并行发起读请求然后根据返回的ID做数据比对。这一步是Cocotb异步模型的优势区域UVM里要折腾sequence和sequencer交互才能实现的事在Cocotb里开几个cocotb.start_soon就能完成。2.4 关于VIP的transaction打印顺手聊聊日志控制网上搜索AXI验证经常会看到类似“synopsys axi vip如何关闭transaction打印”的问题。用过商用VIP的朋友应该都有体会仿真跑起来之后终端里每小时刷几百条transaction记录是常事。要关掉它们不同VIP有不同接口但核心原理都是调整report级别或者重定向日志输出。Cocotb体系下这个问题其实更简单。cocotbext-axi的日志也是基于Python的logging模块你可以直接调整logger级别比如只输出ERROR不输出INFO或者在指定的测试阶段临时屏蔽某个logger。例如import logging logging.getLogger(cocotb).setLevel(logging.WARNING) logging.getLogger(cocotbext.axi).setLevel(logging.ERROR)这种灵活控制是Python生态带来的天然优势。商用VIP的report系统通常要翻文档找接口而Cocotb的日志和Python标准库完全一致几乎没有学习成本。如果你以前被VIP的transaction刷屏折磨过换到Cocotb之后应该会感到心情舒畅。3. 环境搭建DUT、测试文件和Makefile3.1 工具链和目录规划搭建环境之前先确认你的机器上有Python 3.8以上版本、cocotb和cocotbext-axi两个库以及一个支持cocotb的仿真器。我自己常用的是Icarus Verilogiverilog做快速仿真原因只有一个免费、安装方便、对cocotb支持得很好。如果你有Vivado的xsim或者商业仿真器配置方式类似核心就是把仿真器路径和参数传给cocotb的Makefile。安装依赖直接用pippip install cocotb cocotbext-axi为了便于复现我建议在项目目录下建虚拟环境避免污染全局Python。目录结构我习惯这样组织axi_cocotb_env/ ├── rtl/ │ └── axi_ram.v ├── tb/ │ └── test_axi_ram.py ├── Makefile └── requirements.txtrtl放待测的Verilog模块tb放cocotb测试文件Makefile负责编译和仿真调度。这个结构虽然小但已经能体现“DUT和验证环境分离”的思想后续加入脚本、回归列表、覆盖率收集都方便。3.2 准备一个能跑的AXI Slave DUT巧妇难为无米之炊验证环境再好也得有个DUT来跑。我这里准备了一个最简单的AXI4 Slave RAM模块支持写通道和读通道支持INCR和FIXED两种burst类型内存大小256个word。为了保证代码可读性我故意把outstanding和WRAP这些高级特性去掉了目的就是让你把注意力放在验证环境本身。module axi_ram #( parameter ADDR_WIDTH 12, parameter DATA_WIDTH 32, parameter MEM_DEPTH 256 )( input wire clk, input wire rst_n, input wire s_axi_awvalid, output reg s_axi_awready, input wire [ADDR_WIDTH-1:0] s_axi_awaddr, input wire [7:0] s_axi_awlen, input wire [2:0] s_axi_awsize, input wire [1:0] s_axi_awburst, input wire s_axi_wvalid, output reg s_axi_wready, input wire [DATA_WIDTH-1:0] s_axi_wdata, input wire [DATA_WIDTH/8-1:0] s_axi_wstrb, input wire s_axi_wlast, output reg s_axi_bvalid, input wire s_axi_bready, output reg [1:0] s_axi_bresp, input wire s_axi_arvalid, output reg s_axi_arready, input wire [ADDR_WIDTH-1:0] s_axi_araddr, input wire [7:0] s_axi_arlen, input wire [2:0] s_axi_arsize, input wire [1:0] s_axi_arburst, output reg s_axi_rvalid, input wire s_axi_rready, output reg [DATA_WIDTH-1:0] s_axi_rdata, output reg [1:0] s_axi_rresp, output reg s_axi_rlast ); reg [DATA_WIDTH-1:0] mem [0:MEM_DEPTH-1]; function [7:0] beat_bytes; input [2:0] size; begin case (size) 3d0: beat_bytes 8d1; 3d1: beat_bytes 8d2; 3d2: beat_bytes 8d4; 3d3: beat_bytes 8d8; default: beat_bytes 8d4; endcase end endfunction // write state machine localparam W_IDLE 2d0; localparam W_RESP 2d1; reg [ADDR_WIDTH-1:0] aw_addr; reg [7:0] aw_len; reg [2:0] aw_size; reg [1:0] aw_burst; reg [7:0] w_cnt; reg [1:0] w_state; assign s_axi_awready (w_state W_IDLE); assign s_axi_wready (w_state W_IDLE); always (posedge clk or negedge rst_n) begin if (!rst_n) begin w_state W_IDLE; w_cnt 8d0; s_axi_bvalid 1b0; s_axi_bresp 2b00; end else begin case (w_state) W_IDLE: begin if (s_axi_awvalid s_axi_awready) begin aw_addr s_axi_awaddr; aw_len s_axi_awlen; aw_size s_axi_awsize; aw_burst s_axi_awburst; w_cnt 8d0; end if (s_axi_wvalid s_axi_wready) begin // word select if (aw_burst 2b01) begin // INCR aw_addr aw_addr beat_bytes(aw_size); end // write with strobe if (s_axi_wstrb[0]) mem[aw_addr[ADDR_WIDTH-1:2]][7:0] s_axi_wdata[7:0]; if (s_axi_wstrb[1]) mem[aw_addr[ADDR_WIDTH-1:2]][15:8] s_axi_wdata[15:8]; if (s_axi_wstrb[2]) mem[aw_addr[ADDR_WIDTH-1:2]][23:16] s_axi_wdata[23:16]; if (s_axi_wstrb[3]) mem[aw_addr[ADDR_WIDTH-1:2]][31:24] s_axi_wdata[31:24]; if (s_axi_wlast) begin w_state W_RESP; end else begin w_cnt w_cnt 8d1; end end end W_RESP: begin s_axi_bvalid 1b1; s_axi_bresp 2b00; if (s_axi_bvalid s_axi_bready) begin s_axi_bvalid 1b0; w_state W_IDLE; end end endcase end end // read state machine localparam R_IDLE 1d0; reg [ADDR_WIDTH-1:0] ar_addr; reg [7:0] ar_len; reg [2:0] ar_size; reg [1:0] ar_burst; reg [7:0] r_cnt; reg r_state; assign s_axi_arready (r_state R_IDLE); always (posedge clk or negedge rst_n) begin if (!rst_n) begin r_state R_IDLE; s_axi_rvalid 1b0; s_axi_rresp 2b00; s_axi_rlast 1b0; r_cnt 8d0; end else begin case (r_state) R_IDLE: begin if (s_axi_arvalid s_axi_arready) begin r_state 1b1; ar_addr s_axi_araddr; ar_len s_axi_arlen; ar_size s_axi_arsize; ar_burst s_axi_arburst; r_cnt 8d0; end end 1b1: begin s_axi_rvalid 1b1; s_axi_rresp 2b00; s_axi_rlast (r_cnt ar_len); s_axi_rdata mem[ar_addr[ADDR_WIDTH-1:2]]; if (s_axi_rvalid s_axi_rready) begin if (r_cnt ar_len) begin s_axi_rvalid 1b0; s_axi_rlast 1b0; r_state R_IDLE; end else begin r_cnt r_cnt 8d1; if (ar_burst 2b01) begin ar_addr ar_addr beat_bytes(ar_size); end end end end endcase end end endmodule这个DUT看起来不复杂但已经包含了AXI Slave最基本的通道逻辑。写通道里AW和W是同时接收的也就是说AW握手成功和W数据到达必须在同一个周期这样设计简化了状态机但和真实复杂Slave相比少了一点灵活性。如果你用cocotbext-axi去驱动它大部分情况下没有问题因为库默认的写事务会把AW和W的时序协调好。3.3 用cocotbext-axi完成第一版testbenchDUT准备好后testbench的核心就是实例化AxiMaster并让它去驱动DUT的从机接口。cocotbext-axi提供了一个非常方便的信号绑定方法你可以用AxiBus.from_prefix(dut, s_axi)把DUT顶层接口里所有带s_axi前缀的信号自动关联成一个AxiBus对象。完整的测试文件如下import cocotb from cocotb.clock import Clock from cocotb.triggers import ClockCycles, FallingEdge from cocotbext.axi import AxiBus, AxiMaster async def setup_dut(dut): 启动时钟、释放复位并返回配置好的AxiMaster实例 cocotb.start_soon(Clock(dut.clk, 10, unitsns).start()) dut.rst_n.value 0 await ClockCycles(dut.clk, 5) dut.rst_n.value 1 await ClockCycles(dut.clk, 2) axi AxiMaster(AxiBus.from_prefix(dut, s_axi), dut.clk, dut.rst_n, reset_active_levelFalse) return axi cocotb.test() async def test_write_read_basic(dut): 基础读写向0x100写入4字节再读回来比对 axi await setup_dut(dut) addr 0x100 data bytes([0x11, 0x22, 0x33, 0x44]) await axi.write(addr, data) resp await axi.read(addr, len(data)) assert resp.data data, fread back mismatch: {resp.data.hex()} vs {data.hex()} dut._log.info(basic write/read test passed) cocotb.test() async def test_incr_burst_64b(dut): INCR burst连续写入64字节并读回 axi await setup_dut(dut) addr 0x200 data bytes((i % 251 for i in range(64))) await axi.write(addr, data) resp await axi.read(addr, len(data)) assert resp.data data, fburst data mismatch at {[i for i in range(len(data)) if resp.data[i] ! data[i]][:8]} dut._log.info(64-byte incremental burst test passed)这里有几个细节值得说一下。AxiMaster构造时的reset_active_levelFalse表示复位信号是低有效和DUT里rst_n命名对应。如果你用的是高有效复位要跟着DUT信号调整。另一个细节是读函数的返回值。axi.read返回的是一个读响应对象它的data字段是bytes类型可以直接和Python原生的bytes对象做比较。这一点非常方便因为我在UVM里做数据比对时经常要处理队列、动态数组和位宽对齐的转换而Python的bytes天然就适合做这种逐字节比对。3.4 Makefile运行与波形开关cocotb的仿真运行依赖一套Makefile配置。你需要告诉它使用哪个仿真器、DUT的Verilog文件在哪、顶层模块名是什么、测试模块名是什么。我这里的Makefile如下SIM ? icarus TOPLEVEL_LANG ? verilog VERILOG_SOURCES $(shell pwd)/rtl/axi_ram.v TOPLEVEL axi_ram MODULE test_axi_ram include $(shell cocotb-config --makefiles)/Makefile.sim保存后在项目根目录直接执行makecocotb会自动完成Verilog编译、Python测试加载和仿真运行。如果没有意外你会看到类似这样的输出-!-- test_write_read_basic passed -!-- test_incr_burst_64b passed如果装了GTKWave这类波形工具可以在Makefile里加一个变量把仿真波形导出来。cocotb本身支持通过WAVES1参数让Icarus直接产生VCD文件执行时这样写make WAVES1默认情况下波形文件会生成在你的仿真运行目录里文件名一般是dump.vcd或类似。打开GTKWave后拉入s_axi的通道信号就能直观地看到AW握手、W数据对齐、B响应、AR和R通道的时序。我第一次跑通这个环境的时候第一件事就是开波形检查每个通道的VALID/READY时序确认和协议一致后后续的功能迭代就踏实多了。4. 测试用例怎么写从读写到异常路径4.1 基础读改写第一个回归用例最基本的回归用例就是前面那个test_write_read_basic它验证了“写入的数据能够被正确存储读出来的内容和写入内容一致”。这个用例看着简单但它是所有后续测试的基石因为一旦这个用例失败说明验证环境本身有问题后面的burst测试和错误注入也就不用看了。在实际项目中我通常会把基础读写用例再拆细一点增加几个不同的地址边界。比如偏移为0的地址、一个非对齐地址、接近内存末尾的地址。你可以这样写cocotb.test() async def test_write_read_boundaries(dut): axi await setup_dut(dut) test_addrs [0x000, 0x010, 0x0fc, 0x100, 0x3fc] for addr in test_addrs: data bytes([addr 0xff, (addr 8) 0xff, 0xaa, 0x55]) await axi.write(addr, data) resp await axi.read(addr, len(data)) assert resp.data data, fboundary test failed at 0x{addr:03x}边界测试的价值在于它能很快暴露地址译码的高位错误或者内存越界问题。我在验证DMA模块的时候好几次都是靠边界测试抓到了地址位宽截断的bug。4.2 burst和地址递增覆盖协议重点burst是AXI协议里最容易出错的部分也是验证的重心之一。cocotbext-axi的write和read方法默认会根据数据长度自动选择burst传输你不需要手动关心每一拍怎么切分。但如果要针对特定的burst长度做测试可以通过参数来指定。cocotb.test() async def test_burst_beat_count(dut): axi await setup_dut(dut) addr 0x100 pattern bytes(range(128)) # 写128字节automatically split into multiple beats await axi.write(addr, pattern) resp await axi.read(addr, len(pattern)) assert resp.data pattern这里的核心校验点其实是“DUT是否按协议正确处理了LEN和地址递增”。比如一个数据宽度32位的Slave如果Master发一个8字节的INCR burstLEN应该是1两个beat的地址应该分别是addr和addr4。cocotbext-axi会把这些时序都生成好你只管关心DUT的响应。如果你希望进一步控制burst length比如强制每次传输4拍可以在API参数上做调整。这一点不同版本的cocotbext-axi接口略有差异我的建议是查看你安装库里的docstring重点确认三个参数burst类型、beat长度和数据宽度。测试的关键思路是覆盖“单拍传输”“多拍传输”“跨地址边界传输”三类场景。4.3 背压与延迟注入验证握手健壮性很多AXI模块的bug不在正常收发而在握手路径被拉长、或者响应延迟较大的时候。比如从机忙碌时wready拉低几个周期Master是否还能稳住数据不丢。cocotbext-axi的Master本身可以配置等待延迟但用这个来测DUT意义不大因为延迟注入要加在DUT的响应路径上。我的做法分两种。如果DUT代码可以改我在RTL里增加一个delay寄存器让s_axi_wready、s_axi_awready或s_axi_rvalid可以人为插入等待周期然后通过总线配置寄存器改变延迟值。这样Cocotb测试里只要往这个寄存器写不同的值就能对握手做压力测试。如果DUT不能改就在验证环境侧搭一个可配置的AXI Slave模型。cocotbext-axi自带的AxiRam/AxiMemory模型就可以承担类似功能你可以把一个Master挂到它上面做参考再把DUT用两套模型包起来做对比测试。这个方案稍微重一点但能实现类似VIP的延迟注入效果。我不建议直接用dut.s_axi_wready.value 0这种信号级强赋因为在RTL内部wready本来是reg驱动从外部再驱动会产生多驱动冲突仿真结果不可信。4.4 多事务冲突与数据一致性检查真实系统里AXI接口很少是单主单从多事务并发更常见。Cocotb的强项在于可以用协程模拟多事务并发。比如同时发起两个写事务和两个读事务看DUT是否会出现数据覆盖或响应错乱。cocotb.test() async def test_concurrent_write_read(dut): axi await setup_dut(dut) async def writer(addr, val): await axi.write(addr, (val * 4).to_bytes(4, little)) await cocotb.start_soon(writer(0x100, 1)) await cocotb.start_soon(writer(0x200, 2)) await ClockCycles(dut.clk, 10) resp1 await axi.read(0x100, 4) resp2 await axi.read(0x200, 4) assert int.from_bytes(resp1.data, little) 4 assert int.from_bytes(resp2.data, little) 8这种测试在验证仲裁器、带缓存的AXI接口、或者多个DMA通道竞争的场景下特别有用。如果你用过Vivado的AXI Traffic Generator工具应该对它的激励设置界面有印象——它可以设置发起几个事务、地址从哪开始、burst长度固定多少。Cocotb里的多协程并发其实就是在做类似的事只不过你不用打开一个图形界面直接写协程就行。5. 常见问题与排查速查表5.1 环境跑不起来的5个原因我见过很多初次尝试这个环境组合的人卡在同一个地方测试文件已经写好Makefile也复制了但运行时报一堆奇怪错误。这里我把最常见的几种问题列出来方便你对照。现象常见原因解决办法cocotb提示找不到cocotbext模块没有安装或安装到了错误的Python环境pip install cocotbext-axi并确认使用的解释器路径仿真器报找不到顶层模块TOPLEVEL写错或Verilog文件路径不对检查Makefile里的TOPLEVEL和VERILOG_SOURCES测试模块找不到MODULE名字和python文件名不一致确保MODULE test_axi_ram对应tb/test_axi_ram.py波形没有生成没有开启WAVES参数使用make WAVES1或配置cocotb的wave选项运行时卡死直到超时握手信号一直没握手成功在波形里检查DUT的ready/valid是否一直为低5.2 AXI死锁的三种典型现场死锁是AXI验证中最难查的问题之一因为表面现象就是“仿真卡住不动”。我自己碰到过三种典型情况。第一种是AW和W通道的乱序等待死锁。比如DUT先接收了AW请求然后等待W数据但Master侧因为某种原因迟迟不发W数据两边就僵住了。这种问题在简化DUT里很少出现但在真实IP里很常见排查手段是看波形里awvalid和wvalid是不是一直在等对方。第二种是B通道的响应卡住。DUT已经完成了写数据接收也在等bready但Master如果因为某个bug没有及时拉高bready整个写事务就无法结束。cocotbext-axi的Master默认是正确处理的但如果你的DUT自己生成bvalid的逻辑有问题这个死锁就很容易出现。第三种是读数据和rready的配合问题。R通道要求rvalid和rready同时拉高才算一个beat完成如果DUT在发送最后一拍rlast之后没有正确撤掉rvalid或者Master侧因为数据缓冲满暂时不拉高rready都会导致读事务无法收尾。排查AXI死锁我的经验是不要靠读代码去猜直接把波形窗口打开把五个通道的VALID/READY信号全拉出来盯住“谁都拉高了、谁没响应”就行。只要能找到那个“只有VALID没有READY”的信号组合问题就定位了一半。5.3 数据比对失败时怎么定位数据比对失败是验证报告里最常见的一类失败但它背后的原因千奇百怪。我在测试这个简单AXI Ram时也遇到过写入正常、读出来却全是0的情况后来发现是DUT读状态机里mem[ar_addr[ADDR_WIDTH-1:2]]的地址位宽取错了。碰到数据比对失败时我会先做三件事。第一确认是固定的几个字节出错还是整个数据全都错固定字节错大概率是WSTRB或字节序号映射有问题全错则可能是地址位宽或字节序问题。第二在波形里查看RVALID和RDATA的时序确认读出来的数据是DUT的mem值而不是未初始化的寄存器。第三检查cocotbext-axi的burst拆包逻辑和你DUT的地址递增逻辑是否一致尤其是起始地址不是SIZE对齐倍数时很容易出现“两边都按自己的思路算地址”的错位。我把这些问题和排查建议汇总成一张速查表贴在我工位旁边已经一年多了每次排查AXI数据错误都能用上一两条。失败模式重点检查项常用的波形观察点返回全0内存初始化、地址位宽、读状态机ar_addr、mem索引、rdata固定字节错位WSTRB字节掩码、字节序转换wstrb、wdata、mem写入位置连续地址错位burst地址递增逻辑、SIZE对齐aw_addr/ar_addr每个beat的变化部分beat丢失wlast/rlast生成逻辑wlast、rlast、beat计数最后一点个人经验我在实际项目中用Cocotb搭验证环境最深的体会是这个技术栈把“验证环境的搭建成本”大幅降低了但它没有降低你对AXI协议本身的理解成本。cocotbext-axi帮你搞定了driver层帮你处理了繁琐的握手时序但burst地址推进、ID乱序、WSTRB这些协议细节你必须心里有数否则连测试用例该写什么都想不清楚。建议你从这一套最小环境开始跑通之后试着加两个东西一个是给DUT增加WSTRB的非对齐写入逻辑另一个是用多协程发起outstanding读事务。这两个方向能把AXI协议里最坑人的角落都碰到到时候你再看商用VIP和UVM环境的时候就不会觉得它们神秘了。
返回列表