ARTICLE DETAIL

资讯详情

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

用Python写Verilog验证:cocotb协仿真框架实战指南

用Python写Verilog验证:cocotb协仿真框架实战指南 做FPGA或数字IC验证的朋友应该都经历过这种场景RTL写完了想快速验一下功能要么打开Vivado建工程要么写一个巨大的SystemVerilog testbench里边全是#10这种延时和$display几轮迭代下来测试代码比设计代码还难维护。后来我在一个项目里接触了cocotb才真正体会到什么叫“用写Python的姿势写验证”。cocotb是一个基于Python的协仿真测试框架它不改变你的Verilog设计流程只把testbench从HDL换成了Python让你能直接调用cocotb.test()装饰器定义用例用await Timer或者await RisingEdge控制时序。这篇文章我会从头讲怎么搭建一个可用的Verilogcocotb测试环境从装工具到跑通第一个用例再讲到参数化、波形导出和常见坑适合刚接触验证、或者从FPGA开发转验证的工程师参考。我假设你至少会写一点点Verilog但完全不懂cocotb也没关系接下来每个文件我都会贴出来你跟着抄就能跑。1. 背景与选型思考为什么用cocotb做Verilog测试1.1 Verilog测试环境的传统手法和痛点传统的Verilog testbench一般长这样module tb_counter; reg clk 0; reg rst_n 0; reg en 1; wire [7:0] count; counter u_dut(.clk(clk), .rst_n(rst_n), .en(en), .count(count)); always #5 clk ~clk; initial begin #20 rst_n 1; #1000 en 0; #200 $finish; end always (posedge clk) if (count 8d3) $display(PASS count3); endmodule这段代码写起来不复杂可一旦用例多了问题就暴露出来验证代码和设计代码混在同一个仿真流程里控制用例的跳转全靠#延时和disable想对数据做复杂判断还得自己写函数。更麻烦的是当你想引用Python生态里的numpy、scipy做参考模型或者用matplotlib画波形对比时传统testbench完全帮不上忙。最后是维护性一个完整项目往往有几十个测试每个测试都要独立的initial块失败信息通过$display打到日志里没有统一的断言和报告机制等仿真log几百MB的时候想定位一个失败点真的会让人心态崩掉。1.2 cocotb名字的由来和核心机制cocotb的全称是COroutine based COsimulation TestBench直译过来是“基于协程的协同仿真测试平台”。它解决的正是上面这些问题。cocotb通过仿真器提供的VPI/DPI接口让Python代码和HDL仿真器跑在同一个进程里。你在Python脚本里读写Verilog信号仿真器负责推进仿真时间当Python需要等待某个事件时就把当前协程挂起等仿真事件到来再恢复执行。这里“协程”可以理解成一种轻量级任务它不会阻塞仿真器而是随时可以暂停、可以恢复。用生活一点的话说传统testbench像“拿着对讲机喊话”你让信号变化仿真器照做但两边没有真正融合。cocotb则是“Python直接坐到驾驶舱里”信号变化、事件等待、断言检查都在同一套代码里完成。因为这个机制cocotb可以用Python的async/await语法去表达时序逻辑比如await RisingEdge(dut.clk)就是“等时钟上升沿再来跑下一步”。这套逻辑和Python的asyncio很像如果你写过爬虫或者网络服务上手cocotb几乎不需要额外理解成本。1.3 和SystemVerilog/UVM比值不值得换很多人一听cocotb就会问那SystemVerilog和UVM怎么办其实两者定位不一样。SystemVerilog/UVM是完整的方法学适合大型SoC级、芯片级验证环境项目里需要建立agent、scoreboard、sequence体系庞大但结构化强。cocotb更轻特别适合做模块级、IP级验证尤其是算法模块、通信接口这类需要快速迭代逻辑的场景。我个人的感受是如果团队已经积累了很多UVM VIP没必要为了新鲜感换cocotb但如果你是在做FPGA原型验证、科研项目里的硬件模块或者想给RTL写一个能快速跑的单元测试cocotb的上手成本和表达灵活度都明显更友好。下面这张表可以帮你快速判断对比点传统Verilog testbenchSystemVerilog/UVMcocotb开发语言Verilog/SVSystemVerilogPython学习成本低高中低复杂数据类型支持弱强强可用Python全部生态用例组织靠initial/module组件层次Python函数装饰器参考模型自己写HDL自己写SVnumpy/scipy/sklearn都能用适用场景极简测试大规模UVM环境模块/IP级、算法验证明白了这些再去看cocotb就顺理成章了它不是来终结UVM的而是给“想高效验证Verilog模块”的人多了一个很趁手的工具。2. 从零准备cocotb运行环境2.1 需要哪些软件工具在开始写任何测试之前先把环境准备清楚。cocotb本身只是一个Python库它要真正驱动HDL仿真还需要一个第三方仿真器。我日常用得最多的是Icarus Verilog也就是iverilog原因很简单开源、跨平台、支持VPIcocotb官方示例基本都能直接跑。如果你想用Verilator它也能配合cocotb但Verilator对Verilog语法支持有一定限制对仿真精度和流程也有要求新手更容易在VPI编译上踩坑。硬件描述语言这边需要准备三样东西Python 3.8及以上版本建议3.10或3.11一个支持VPI的仿真器cocotb库本身。操作系统上Linux和macOS最顺利Windows建议用WSL因为很多cocotb相关工具链在Windows原生环境的路径处理上会有稀奇古怪的问题。我后面的命令以Ubuntu/Debian为主其他系统差别不大。2.2 安装Icarus Verilog和cocotb安装分两步第一步装仿真器。Ubuntu/Debian直接执行sudo apt update sudo apt install iverilogmacOS用户可以用Homebrewbrew install icarus-verilogWindows用户建议装WSL在Ubuntu子系统里按上面命令操作。装完可以验证一下iverilog -v能输出版本号就说明基础环境OK。第二步装cocotbpip install cocotb装好后检查python -c import cocotb; print(cocotb.__version__)这里要特别注意版本匹配cocotb 1.7及以上要求Python 3.8个别功能在3.10上支持得更好iverilog的版本太老比如早于10.x可能在编译VPI时出现兼容问题。我有一次在CentOS上装了一个很老的iverilogcocotb跑起来直接报undefined symbol换成官方仓库最新版就好了。另外建议用虚拟环境别把cocotb直接装到系统Python里否则后续项目依赖多了容易乱套。2.3 一个最小工程目录应该怎么摆很多刚接触cocotb的人会问到底要不要建工程cocotb没有强制要求目录结构但按照惯例建议把RTL、测试脚本、构建脚本分开。我一般用这样的布局counter_demo/ ├── hdl/ │ └── counter.v ├── test/ │ └── test_counter.py ├── Makefile └── sim_build/hdl目录放Verilog设计文件test目录放cocotb测试脚本Makefile负责驱动仿真流程sim_build是仿真生成的临时目录可以忽略或清理。为什么要这样分首先RTL和testbench从物理上隔离版本管理时能清晰区分设计代码与验证代码其次cocotb通过Makefile里的变量把仿真器、顶模块、测试模块串起来目录清晰能减少很多路径问题。等你以后工程变大了还可以在这个基础上加scripts目录放公共工具加results目录放回归日志。3. 实战第一轮给计数器写一个可跑的测试3.1 准备一个最小Verilog DUT为了不啰嗦我们直接用一个同步清零计数器作为被测对象。这个DUT足够简单但已经包含时钟、复位、使能、输出这些基本要素足够演示cocotb的用法。新建hdl/counter.vmodule counter ( input wire clk, input wire rst_n, input wire en, output reg [7:0] count ); always (posedge clk or negedge rst_n) begin if (!rst_n) count 8d0; else if (en) count count 1b1; end endmodule这里的复位是异步低复位使能拉高时每个时钟上升沿加18位计数器溢出自动回卷。功能上没有任何花哨非常适合做第一个cocotb目标。你可以先用iverilog单独编译这个文件确认语法没问题也可以等cocotb集成跑的时候一起报错我建议先手动编译一次能少一个变量。3.2 写第一个cocotb测试脚本接下来在test/test_counter.py里写测试脚本。直接用代码来定义用例import cocotb from cocotb.clock import Clock from cocotb.triggers import Timer, RisingEdge cocotb.test() async def test_counter_increment(dut): cocotb.start_soon(Clock(dut.clk, 10, unitsns).start()) dut.rst_n.value 0 dut.en.value 0 await Timer(25, unitsns) dut.rst_n.value 1 dut.en.value 1 for i in range(1, 5): await RisingEdge(dut.clk) assert dut.count.value i, fcount should be {i}, got {dut.count.value}这个测试的逻辑很简单先启动一个周期为10ns的时钟信号start_soon把时钟任务放到后台然后复位20多ns释放复位并拉高使能接着在每个时钟上升沿后检查计数器的值是否与预期一致。await RisingEdge(dut.clk)会等到时钟上升沿所以每次循环都精确地在上升沿之后采样。跑测试时如果断言失败cocotb会把这个用例标记为Fail并打印出你的提示信息。我第一次写时直接用dut.count.value去比较结果类型不匹配后来加了个.value.integer属性才拿到整数。另外你可以在一个测试文件里写多个cocotb.test()函数cocotb会按顺序依次执行每个函数都算一个独立用例。3.3 时钟、复位和随机激励的基本套路看完上面的例子你已经掌握了cocotb最核心的几件事Clock创建时钟start_soon启动后台任务Timer做固定延时RisingEdge/FallingEdge等待边沿。这里展开讲一下背后的协同调度逻辑。Clock(dut.clk, 10, unitsns)表示创建一个周期为10ns的方波但调用start()后时钟源不会立刻占用主流程而是作为独立协程在后台运行。所以你在测试函数里可以随时await RisingEdge(dut.clk)后台时钟任务会通过仿真器把信号翻转RisingEdge触发器等到了上升沿就恢复。这个机制像什么呢就像你开了一个后台闹钟每隔10ns响一下而你的主任务一直在处理别的事情只在需要的时候等下一次闹钟。随机激励也是cocotb的日常用法比如可以这样import random cocotb.test() async def test_counter_random(dut): cocotb.start_soon(Clock(dut.clk, 10, unitsns).start()) dut.rst_n.value 0 await Timer(20, unitsns) dut.rst_n.value 1 for _ in range(20): dut.en.value random.randint(0, 1) await RisingEdge(dut.clk)因为Python自带random你可以非常自然地在验证里加入随机因子。这比在SV里写约束要直观得多尤其当你需要产生复杂的随机数据包时Python的优势更明显。不过要注意随机测试必须和断言配合否则跑完什么都没检查那是“假回归”。4. 跑仿真、看波形和套件集成4.1 用make命令把测试跑起来cocotb官方提供了一套Makefile模板你只要在工程根目录写一个Makefile把变量指定好就能用make命令一键跑仿真。我的Makefile内容如下SIM ? icarus TOPLEVEL_LANG ? verilog VERILOG_SOURCES $(PWD)/hdl/counter.v TOPLEVEL counter MODULE test_counter include $(shell cocotb-config --makefiles)/Makefile.sim解释一下几个变量的含义SIM选择仿真器这里用icarusTOPLEVEL_LANG告诉cocotb我们顶层语言是VerilogVERILOG_SOURCES列出所有RTL文件TOPLEVEL是顶层模块名要和hdl/counter.v里的模块名一致MODULE是测试Python模块名cocotb会去导入这个模块并执行里面所有带cocotb.test()的函数。include那一行会引入cocotb自带的Makefile逻辑它负责把Python测试和仿真器连接起来。然后在工程根目录执行make你应该会看到cocotb打印出加载Python模块、启动仿真器、执行测试用例的日志。最终结果类似test_counter_increment passed Passed 1 tests如果测试失败日志里会明确告诉你哪个文件名、哪一行断言失败以及期望值和实际值。这个格式比纯Verilog的$display要友好得多因为它天生就带了测试用例名和结果汇总。如果你有多个cocotb.test()函数就会看到多条passed/failed记录非常适合直接当回归报告用。4.2 导出VCD波形并用GTKWave排查写验证不是光看断言过没过有时候分析时序需要看波形。cocotb对Icarus的支持里可以用WAVES1来打开波形导出make WAVES1跑完之后在sim_build目录下会生成VCD格式的波形文件。你可以直接用GTKWave打开gtkwave sim_build/counter.vcdGTKWave是开源波形查看器安装很简单Ubuntu下执行sudo apt install gtkwave。打开后把counter模块的信号拖到波形窗就能看到时钟、复位、使能、计数变化。有时候你会觉得波形文件很大那是因为VCD保存了所有可见信号。对于复杂设计我倾向于在测试里只关心需要观测的信号或者用仿真器的层次过滤功能。这里提个小技巧如果你的DUT内部有寄存器想观测可以把信号名写全例如dut.u_submodule.reg_x这样在波形里也能定位到。cocotb对信号名访问非常灵活只要能通过Python属性链找得到波形里一般也能看到。4.3 把cocotb测试接入pytest和CI项目一旦跑起来就不能总在本地手动make。cocotb提供了和pytest集成的能力你可以把验证用例纳入自动化测试流程。在cocotb里测试函数本身是异步函数pytest不能直接执行通常还是通过Makefile跑不过你可以在CI里加一个job来执行make把cocotb的测试结果当作CI结果。一个简化版的GitHub Actions workflow可以这样写name: cocotb-test on: [push] jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-pythonv4 with: python-version: 3.10 - run: sudo apt install iverilog - run: pip install cocotb - run: make这只是一个最小示例但原理已经够了统一的入口就是make只要环境里装好iverilog和cocotbCI就能复现本地结果。对于多人协作的团队我建议把make target细分成regression和smoke比如一个跑全量用例一个只跑冒烟用例这样平时提交代码能快速反馈。5. 避坑实录与进阶方向5.1 新手最容易踩的五个坑第一个坑是TOPLEVEL_LANG忘记设置。很多新手从网上复制Makefile只改了模块名和测试名没注意这个变量。cocotb默认可能把它当成VHDL结果报Failed to evaluate之类的错误。解决办法很简单显式写上TOPLEVEL_LANG ? verilog。第二个坑是信号赋值后的采样时机。我见过不少人用Timer(10, unitsns)来等待而不是RisingEdge结果采样到的信号是刚变化前还是变化后完全靠运气。cocotb里正确的做法是如果是同步设计一律用时钟边沿触发只有对组合逻辑或固定延时场景才用Timer。第三个坑是拿LogicArray对象直接和整数比较。Verilog信号的value在cocotb里通常是LogicArray对象直接和整数比较可能因为类型不匹配出问题。稳妥的做法是使用dut.signal.value.integer拿到整数值。有些场景还需要关注value.is_resolvable避免X态误判。第四个坑是iverilog版本过老。老版本对VPI支持不完善cocotb编译DLL时会报链接错误。遇到这种问题不要浪费时间调代码先去升级iverilog到最新版再清掉sim_build目录重新make。第五个坑是在testbench里试图写入input端口。cocotb的信号赋值对象是双向的但如果你不小心对DUT的output端口赋值仿真器会报驱动冲突。写之前先明确哪些是输入、哪些是输出输出信号只能读。5.2 参数化、覆盖率与回归测试的延伸当测试用例数量增多你会希望用更少代码覆盖更多场景。cocotb支持在测试函数里循环处理不同条件但更优雅的是利用Python的循环生成多个场景或者在用例里通过环境变量传入配置。比如我写一个测试函数通过num_cycles控制随机测试的轮数cocotb.test() async def test_counter_loop(dut): cocotb.start_soon(Clock(dut.clk, 10, unitsns).start()) dut.rst_n.value 0 await Timer(20, unitsns) dut.rst_n.value 1 for _ in range(100): dut.en.value random.randint(0, 1) await RisingEdge(dut.clk) assert dut.count.value 0这种写法在验证协议时序时特别有用你可以把循环变成等待数据、发送数据、检查响应的组合。覆盖率方面cocotb本身不会强制生成功能覆盖率但你可以结合Verilator等工具打开覆盖率选项或者通过Python脚本统计测试路径。回归测试则是把多个用例组合起来用Makefile里不同的target控制测试范围再配合CI跑定时回归。5.3 cocotb后续可以怎么玩当你跑通了第一个用例后面就可以把cocotb真正用到你的项目里。我个人最常做的几类场景第一给常见总线接口写BFM比如用cocotb写一个AXI Master的驱动在用例里直接发起读请求然后用await等待响应第二算法验证把Python的numpy参考模型直接放在testbench里和RTL结果对比这个在滤波器、FFT、通信基带处理里尤其方便第三结合现有的pytest测试体系把硬件验证和软件测试统一到一套CI流水线里。关于工具链如果项目规模变大建议换用Verilator跑快速仿真或者用商业仿真器支持更完整的功能覆盖。cocotb在这些工具上都能工作但需要额外配置VPI路径。等你熟练之后还可以研究cocotb的Task对象和邮件风格的事件调度不过那是后话了。就我个人而言从第一次用cocotb跑通计数器测试开始我就再也不想回到纯Verilog testbench里做模块验证了。它的学习曲线不算陡却能把“写验证代码”这件事从HDL的拘束里解放出来。如果你正卡在RTL验证的效率问题上不妨按这篇文章的步骤搭个最小环境亲手跑一个用例相信你会感受到这种“用Python写验证”的爽快感。真遇到问题时cocotb的官方文档和GitHub issue是最值得翻的资料很多坑大家都踩过解决方案也都在那里。
返回列表