ARTICLE DETAIL

资讯详情

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

用Cocotb搭建AXI验证环境:从零开始的事务级测试平台

用Cocotb搭建AXI验证环境:从零开始的事务级测试平台 1. 为什么建议把第一个AXI验证环境直接搭在Cocotb上之前有同学问我入门AXI总线验证是老老实实啃UVM还是先用Verilog写一套简单的testbench就够了。我的回答是如果你是想快速验证一个模块、验证一个想法或者想把精力留在“业务逻辑”而不是“平台脚手架”上Cocotb加cocotbext-axi是目前最值得尝试的组合。Cocotb全称是Coroutine-based Cosimulation Test Bench本质是用Python写测试让仿真器负责RTL仿真两边通过VPI直接交互。这跟传统UVM那套SystemVerilog类库工厂机制的思路完全不同。传统的Verilog testbench不是不能用但你会发现一旦要处理复杂的AXI协议交互代码量会成倍增加。AXI有独立的写地址、写数据、写响应、读地址、读数据五组通道每一组都要维护VALID/READY握手和时序。用纯Verilog写一个支持突发、乱序返回、优先级调度的主设备模型可能要几百行而且调试过程非常痛苦。UVM当然功能强大但它的学习曲线也摆在那里为了“第一个环境”就引入UVM很多时间是花在理解组件树和sequence机制上而不是花在验证AXI总线本身。Cocotb的另一个好处是测试代码和RTL代码分离得非常干净。RTL该用Verilog还是Verilog测试平台则用Python组织。你可以直接在测试里写for循环、写断言、用pytest风格的日志甚至可以把验证环境和CI流水线直接接起来。配合cocotbext-axi这个库AXI协议的主设备、从设备模型都已经是现成的了你不需要去手动驱动每一根信号只需要调用类似await master.write(addr, data)这样的事务级接口。它解决的问题非常明确解放验证人员让测试代码聚焦在“验证什么”而不是“怎么把信号拉起来”。所以如果你打算入门AXI总线验证或者手头已经有一个AXI接口的RTL模块要快速验证这篇文章给到的环境、代码和踩坑经验能帮你把第一套环境搭起来而不是停留在“看着波形图猜信号”的阶段。2. 动手前的环境确认版本、仿真器和Cocotb的一处不兼容先别急着写代码环境这关不过后面会浪费大量时间。我第一次搭建时就是栽在cocotbext-axi和cocotb版本不匹配上安装完import直接报错查了半天才发现是Python版本太老。2.1 推荐安装组合我测试时用的是Python 3.11、Cocotb 2.x、cocotbext-axi0.3.x。如果你是从零开始用下面这套流程python3 -m venv .venv source .venv/bin/activate pip install cocotb cocotbext-axi为什么一定要用虚拟环境因为Cocotb会依赖特定版本的cocotb-config、cocotb库和Python头文件如果你系统里已经有其他Python项目全局安装很容易出现版本互相干扰。虚拟环境是最省心的做法。仿真器方面我建议至少准备一个。常见的选择是Icarus Verilog和Verilator。Icarus安装简单大部分Linux发行版直接包管理就能装上适合第一次跑通流程。Verilator仿真速度快很多但现在Cocotb配合Verilator时对SystemVerilog的有些语法支持不够直观如果你的RTL写得比较简单选哪个都行。# 以Ubuntu/Debian系为例需要root权限时加上sudo apt install iverilog apt install verilator2.2 仿真器选型参考仿真器Cocotb兼容性适用场景注意事项Icarus Verilog很好小型模块、教学、快速验证对SystemVerilog的interface支持有限建议RTL保持Verilog-2001风格Verilator很好中大型模块、回归测试较多需要关注timing选项编译耗时比Icarus长VCS/Xcelium商业环境支持公司内大型芯片验证许可成本高但Cocotb也支持通过SIMvcs等方式接入安装完成后用python -m pip show cocotb确认版本号再运行cocotb-config --version确认cocotb-config能被找到。这一步很多人会漏掉因为某些环境下Python脚本目录没加入PATH导致后面Makefile里include $(shell cocotb-config --makefiles)直接报错找不到命令。这里额外说一个容易踩的点如果你的RTL代码里用了logic、always_ff、interface这些SystemVerilog特性Icarus可能编译不过去。不是因为Cocotb不支持而是Icarus的SystemVerilog支持并不完整。我的建议是第一套环境先用Verilog-2001风格写DUT先把链路打通再逐步引入更复杂的语法。3. 第一个AXI4-Lite验证环境的完整搭建与代码拆解下面这套环境我拆成了三部分RTL待测模块、Python测试平台、Makefile仿真入口。目标很简单用AXI4-Lite总线对一个寄存器堆做写操作再把它读出来比对。3.1 DUT设计一个带读回通路的AXI4-Lite从设备这里我不想拿一个空壳模块演示那样没意义。我们用一个简单的寄存器堆通过AXI4-Lite从接口写入数据再通过AXI4-Lite从接口读回来。这个场景几乎覆盖了所有AXI从设备验证的基础需求。文件路径rtl/axil_regs.vmodule axil_regs #( parameter ADDR_W 4, parameter DATA_W 32 )( input wire s_axi_aclk, input wire s_axi_aresetn, input wire [ADDR_W-1:0] s_axi_awaddr, input wire s_axi_awvalid, output reg s_axi_awready, input wire [DATA_W-1:0] s_axi_wdata, input wire [DATA_W/8-1:0] s_axi_wstrb, input wire s_axi_wvalid, output reg s_axi_wready, output reg [1:0] s_axi_bresp, output reg s_axi_bvalid, input wire s_axi_bready, input wire [ADDR_W-1:0] s_axi_araddr, input wire s_axi_arvalid, output reg s_axi_arready, output reg [DATA_W-1:0] s_axi_rdata, output reg [1:0] s_axi_rresp, output reg s_axi_rvalid, input wire s_axi_rready ); reg [DATA_W-1:0] mem [0:(1ADDR_W)-1]; reg [ADDR_W-1:0] aw_addr; reg [DATA_W-1:0] wdata_buf; reg [DATA_W/8-1:0] wstrb_buf; reg aw_pending; reg w_pending; integer i; initial begin for (i 0; i (1ADDR_W); i i 1) mem[i] {DATA_W{1b0}}; aw_addr 0; wdata_buf 0; wstrb_buf 0; aw_pending 0; w_pending 0; end always (posedge s_axi_aclk) begin if (!s_axi_aresetn) begin s_axi_awready 1b0; s_axi_wready 1b0; s_axi_bvalid 1b0; s_axi_bresp 2b00; s_axi_arready 1b0; s_axi_rvalid 1b0; s_axi_rresp 2b00; s_axi_rdata {DATA_W{1b0}}; aw_pending 1b0; w_pending 1b0; end else begin s_axi_awready 1b0; s_axi_wready 1b0; s_axi_arready 1b0; if (s_axi_awvalid !aw_pending) begin aw_addr s_axi_awaddr; aw_pending 1b1; s_axi_awready 1b1; end if (s_axi_wvalid !w_pending) begin wdata_buf s_axi_wdata; wstrb_buf s_axi_wstrb; w_pending 1b1; s_axi_wready 1b1; end if (aw_pending w_pending) begin for (i 0; i DATA_W/8; i i 1) begin if (wstrb_buf[i]) mem[aw_addr][i*8 : 8] wdata_buf[i*8 : 8]; end s_axi_bresp 2b00; s_axi_bvalid 1b1; aw_pending 1b0; w_pending 1b0; end if (s_axi_bvalid s_axi_bready) begin s_axi_bvalid 1b0; end if (s_axi_arvalid !s_axi_rvalid) begin s_axi_rdata mem[s_axi_araddr]; s_axi_rresp 2b00; s_axi_rvalid 1b1; s_axi_arready 1b1; end if (s_axi_rvalid s_axi_rready) begin s_axi_rvalid 1b0; end end end endmodule这个DUT故意没有做得太复杂但它体现了一个关键设计写地址通道和写数据通道是独立握手的。AXI协议允许AW先到、W先到或者同时到主设备发来的时序并不固定。所以我在内部用aw_pending和w_pending分别记录两个通道的到达状态只有两个都到了才真正把数据写入寄存器堆然后返回BVALID响应。如果你第一次设计AXI从设备这个结构值得抄下来因为它避免了“先到通道被后续事务覆盖”的问题。读通道相对简单只要没有正在返回的数据就可以接收新的读地址下一拍把数据放到RDATA上同时拉高RVALID。这里只支持单笔未完成事务对于第一套环境来说够了等后面需要提升吞吐再扩展。3.2 Python测试平台用AxiLiteMaster发起事务cocotbext-axi提供了非常方便的AxiLiteBus和AxiLiteMaster。Bus类会自动从DUT端口里按前缀收集信号Master类会替你把VALID/READY握手、数据排列这些底层细节处理好。文件路径sim/test_axil.pyimport cocotb from cocotb.clock import Clock from cocotb.triggers import ClockCycles, RisingEdge from cocotbext.axi import AxiLiteBus, AxiLiteMaster async def reset_dut(dut): dut.s_axi_aresetn.value 0 for _ in range(4): await ClockCycles(dut.s_axi_aclk, 1) dut.s_axi_aresetn.value 1 await ClockCycles(dut.s_axi_aclk, 1) cocotb.test() async def test_axil_write_read(dut): AXI4-Lite 寄存器写读回环测试 cocotb.start_soon(Clock(dut.s_axi_aclk, 10, unitsns).start()) await reset_dut(dut) bus AxiLiteBus.from_prefix(dut, s_axi) master AxiLiteMaster(bus, dut.s_axi_aclk, dut.s_axi_aresetn, reset_active_levelFalse) # 连续写两个地址 await master.write(0x00, 0x12345678) await master.write(0x04, 0xDEADBEEF) # 回读并比较 data, resp await master.read(0x00, 4) dut._log.info(read 0x00: data0x%08X, resp0x%02X, data, resp) assert data 0x12345678 assert resp 0 data, resp await master.read(0x04, 4) dut._log.info(read 0x04: data0x%08X, resp0x%02X, data, resp) assert data 0xDEADBEEF assert resp 0你发现没有测试里面没有一行手动拉awvalid或等待awready的代码。AxiLiteMaster把所有通道按事务打包成了一个协程write(addr, data)会一直等到写响应返回read(addr, 4)会一直等到读数据返回。这就是事务级建模的意义验证人员只关心“往地址0x00写一个32位数据”而不是关心具体几个周期能写完。关于reset_active_levelFalse这里说明一下。cocotbext-axi的构造函数里有一个reset_active_level参数默认认为复位是高有效。但我们的RTL端口叫aresetn是低有效复位所以必须显式传False。如果不传库内部可能会在错误的状态下等待复位释放导致后续事务一直超时。这个参数是新手最容易漏掉的地方。3.3 Makefile一行命令跑起来Cocotb的仿真入口通常用Makefile管理。Cocotb官方提供了共同的makefile片段只要我们设置几个变量再include它就行。文件路径sim/MakefileSIM ? icarus TOPLEVEL_LANG ? verilog VERILOG_SOURCES $(PWD)/rtl/axil_regs.v TOPLEVEL axil_regs MODULE test_axil include $(shell cocotb-config --makefiles)TOPLEVEL是仿真顶层模块名也就是DUT模块名。MODULE是我们写的Python测试文件的名字去掉.py后缀。Cocotb会在仿真启动后加载这个Python模块并执行里面所有带cocotb.test()装饰器的函数。VERILOG_SOURCES指向DUT源码。运行方式很简单cd sim make如果要用Verilator就执行make SIMverilator第一次跑通时你应该能在日志里看到类似test_axil_write_read PASSED的输出。我们还可以故意改一个期望值比如把assert data 0x12345678改成assert data 0x12345679再看看日志里如何报错这样能直观理解Cocotb的断言和仿真器日志是怎么串起来的。3.4 这段代码到底做了什么从时序角度看整个流程是测试开始时创建10ns周期的时钟并持续运行。将aresetn拉低4个时钟周期再拉高确保DUT内部状态被彻底复位。通过AxiLiteBus.from_prefix把DUT上所有以s_axi开头的信号收集成一个总线对象。创建AxiLiteMaster它会在后续发起事务时自动驱动AW、W、B、AR、R这些通道。写入两个地址的数据然后逐一读回比对。跑完这个用例你其实已经覆盖了AXI4-Lite协议的核心内容写地址握手、写数据握手、写响应、读地址握手、读数据握手。第一套环境能做到这一步已经不“小”了。4. AXI总线越用越熟的几个关键点事务模型、握手和突发如果你之前只写过简单的SPI或UART验证第一次接触AXI可能会被它的通道数量吓到。其实不需要怕AXI的通道再多核心就一个思想每一笔传输都通过“地址、数据、响应”三个元素来描述只是读和写各走各的通道。4.1 VALID/READY握手一个请求要双方都“点头”才算完成AXI协议里任何一次传输都遵循VALID/READY握手规则。发送方拉高VALID表示数据已经准备好接收方拉高READY表示自己可以接收。只有VALID和READY同时为1的那个上升沿数据才算真正被采样。很多第一次写AXI从设备的人会犯一个错误他们认为只要自己拉高了READY事务就完成了。实际上如果发送方没有拉高VALIDREADY再高也没用反过来发送方拉高VALID后如果接收方一直不拉READY发送方就必须一直保持数据不变。这就叫“握手”双方都点头事情才算完成。在cocotbext-axi里你基本不用关心这些细节但理解握手对排查问题非常重要。比如仿真挂死十有八九是某个通道的VALID等了很久没等到READY或者是两个通道互相等待形成了死锁。4.2 AXI4-Lite和AXI4的差异没有突发但要会扩展这套验证环境用的是AXI4-Lite它的特点是协议简单不支持突发传输地址宽度和数据宽度也可配置。很多寄存器配置类模块用AXI4-Lite就够了。但如果你要验证内存控制器、DMA或者高性能互联总线就需要完整AXI4。AXI4在地址通道上多了突发长度、突发大小、突发类型这些控制信号数据通道上还多了多拍传输。下表是一个直观对比特性AXI4-LiteAXI4写地址通道AWADDR、AWVALID/AWREADY支持BURST_LEN/BURST_SIZE写数据通道WDATA、WSTRB、WVALID/WREADY支持多拍数据传输写响应通道BRESP、BVALID/BREADY基本相同读地址通道ARADDR、ARVALID/ARREADY支持突发控制读数据通道RDATA、RVALID/RREADY支持多拍数据返回ID机制无支持多个ID乱序返回从AXI4-Lite切到AXI4验证环境并不需要推翻重来。cocotbext-axi里对应的类名从AxiLiteMaster换成Axi4Master总线类从AxiLiteBus换成Axi4Bus。测试代码的主体结构基本不变。4.3 事务模型把传输看成对象而不是信号翻转Cocotb和cocotbext-axi带来的最大思维转变是把“传输”抽象成对象。你在Python里调用master.write(0x00, 0x12345678)背后对应着一组完整的总线事务调用master.read(0x04, 4)背后对应一组读事务并返回数据和响应码。这种抽象的直接收益是测试用例可以写成接近自然语言的序列。你不需要知道某条数据在第几个时钟周期出现在总线上只需要知道结果对不对。这对后期写随机测试、定向测试、回归测试都很有帮助。如果每个用例都从手动拉信号开始写那你的验证环境会越来越难维护最终陷入“改一个RTL就要重写一大半测试”的困境。5. 把环境升级成AXI4全总线cocotbext-axi还提供了哪些现成组件很多人问第一套环境跑通AXI4-Lite之后下一步该做什么。我的建议是去试着接入Axi4Ram和Axi4Slave因为这两个组件能帮你验证完整的AXI4主设备或互联模块。5.1 当DUT是AXI4主设备时用Axi4Ram当从设备模型如果DUT里有AXI4主接口比如一个DMA控制器或者一个总线读取引擎那么你需要一个从设备模型来响应它的请求。手写一个完整AXI4从设备是很痛苦的要处理突发、WSTRB、ID标记、乱序返回。Axi4Ram就是为此设计的它本质上是一个AXI4从设备模型加一个内存模型可以直接挂在DUT的AXI4主接口上。典型写法是from cocotbext.axi import Axi4Bus, Axi4Ram axi_bus Axi4Bus.from_prefix(dut, m_axi) axi_ram Axi4Ram(axi_bus, dut.axi_aclk, dut.axi_aresetn, reset_active_levelFalse) cocotb.start_soon(axi_ram.run())from_prefix会把DUT里所有以m_axi开头的信号收集成总线。Axi4Ram的run()协程会持续监听总线自动处理地址、数据、响应。你在测试里可以随时往这块“内存”里写入期望值也可以读取DUT写入的内容用来做数据比对。我在实际项目里最喜欢用Axi4Ram做的一件事是先往特定地址预置一段数据然后让DUT去读再通过DUT的输出判断读到的数据是否正确。这样等于把从设备的行为完全接管过来了DUT只要规规矩矩按照AXI协议发起请求就行。5.2 当DUT是AXI4从设备时用Axi4Master发突发事务当DUT是AXI4从设备时则是反过来的用Axi4Master去发起写事务和读事务。和Lite版本相比Axi4Master增加了很多突发能力代码风格却变化不大。大概结构是这样from cocotbext.axi import Axi4Bus, Axi4Master axi_bus Axi4Bus.from_prefix(dut, s_axi) master Axi4Master(axi_bus, dut.axi_aclk, dut.axi_aresetn, reset_active_levelFalse) # 发起一笔记带16字节数据的写突发 await master.write(0x1000, some_data, size16) # 从某个地址读32字节 data, resp await master.read(0x1000, 32)这里size的单位是字节具体数值要对应总线的位宽和突发长度。如果总线是32位宽size16意味着4拍突发如果总线是64位宽size16则意味着2拍突发。这一块需要你根据RTL的实际位宽去换算cocotbext-axi不会替你判断你的总线是多宽。5.3 组件选型表场景推荐组件说明DUT是AXI4-Lite从设备AxiLiteMaster发起配置寄存器读写DUT是AXI4-Lite主设备AxiLiteSlave / AxiLiteRam响应DUT发起的配置事务DUT是AXI4从设备Axi4Master发起突发读写验证从设备行为DUT是AXI4主设备Axi4Ram / Axi4Slave响应DUT的读写请求需要模拟真实从设备延迟Axi4Slave 自定义driver可在从设备侧加入反压、错误响应等掌握了这张表你就不会再纠结“这个接口到底该用Master还是Slave了”——先看DUT的角色再看是Lite还是完整AXI4两两组合就得到答案。6. 搭建第一套环境时最常见的报错与排查思路这部分我把实践中真正遇到过、也帮别人排查过的几类问题整理出来。如果某天你的仿真突然不动了可以直接对表查。6.1 仿真一直跑不完日志卡在总线等待上表现是终端一直不出结果CtrlC之后能看到traceback停在await master.read或者await master.write附近。出现这种情况大多数原因是握手条件一直不满足。排查思路先看DUT是否处于复位状态。cocotbext-axi在构造Master时会按照reset_active_level去等待复位释放。如果复位信号拉错了极性Master内部状态永远不对事务自然发不出去。第二步看时钟是否真的在跑如果忘了cocotb.start_soon(Clock(...).start())仿真时间就停住了。第三步在DUT内部加一些$display观察握手信号看是AW通道卡住还是W通道卡住。6.2 报错“No matching pins found for prefix”这是AxiLiteBus.from_prefix的典型报错意思是DUT顶层找不到以指定前缀开头的信号。最常见原因是端口名用的前缀和总线名对不上。比如RTL里信号叫m_axi_awaddr测试里却写成from_prefix(dut, s_axi)那一定找不到。解决办法很简单先把RTL顶层端口列出来确认所有AXI信号都用了同一个前缀再把这个前缀传给from_prefix。还有一种情况是综合后的RTL端口名带上了数组下标或者被重命名这时可以手写一个AxiLiteBus对象显式把每个信号名映射进去但第一套环境不建议这么做尽量保持端口命名干净。6.3 Verilator编译报Unsupported或时间控制相关错误如果你选择make SIMverilator有时会遇到类似Unsupported: ...的编译错误。原因比较复杂但常见的诱因有两个一是DUT里用了Icarus和Verilator支持程度不一致的SystemVerilog语法二是当前Cocotb版本和Verilator版本之间的兼容性问题。遇到这种问题时我的建议是先换回Icarus把逻辑调通等RTL语法稳定后再尝试Verilator。不要在一开始就同时处理协议调试、Python调试和仿真器兼容性三件事那会非常消耗耐心。6.4 回读数据全是0但写事务看起来成功了这是寄存器类DUT很容易出现的问题。写事务返回了正常的B响应读事务也没有超时但读回来的数据就是复位值0。原因往往在于读写操作的是同一块地址空间吗不一定。更常见的是DUT里写数据通道和写地址通道的数据并不是按照你想象的方式对齐例如写地址使用的是字节地址而寄存器堆索引使用的是字地址导致你写的是地址0x04实际却落在0x01的寄存器位置。解决办法在Python测试里多打印几组地址和数据比如写0x00、0x04、0x08、0x0C再读回来看规律是整体偏移还是只有某一个地址错位能很快判断出是地址映射问题还是写数据掩码问题。6.5 加了自定义延迟后时序错乱有些同学会把Axi4Slave或自己的从设备模型加上随机等待周期用来模拟真实从设备反压。加完之后发现某些用例偶尔对某些用例固定错。这通常不是算法问题而是复位和事务启动之间没有做好同步。如果DUT和总线模型都在同一个时钟沿被复位但Python测试在复位释放后第一拍就开始发起事务就会产生竞态。我的做法是在所有测试的复位模块里复位释放后统一等2到3个时钟沿再开始发事务这样能避开复位释放那一拍的亚稳态窗口也让主从模型的内部状态充分建立起来。最后再分享一个小习惯我每次新建AXI验证环境时不会急着写复杂用例而是先做一个最原始的“写一个地址、读一个地址、比对结果”的冒烟用例。这个用例一旦跑通说明时钟、复位、总线绑定、仿真器入口这些基础设施都没有问题了。之后再逐步叠加burst、backpressure、错误响应这些高级场景。这样做的好处是后面每次出问题都能尽快把范围缩小到“是协议交互问题”还是“是验证平台本身的问题”。这套思路也适用于你后续搭建其他总线环境比如APB、AXI-Stream。工具可以换环境可以换但“先用最小闭环打通链路再层层加码”的原则基本不会变。你现在跟着上面的代码把环境跑起来就已经迈出了最实在的第一步。
返回列表