简介这份资源是面向数字IC验证工程师与PCIe协议学习者的Cocotb仿真框架聚焦用Python驱动Verilog硬件模型完成PCI Express验证。包内共55个文件以38个Python脚本为核心承担测试序列生成、协议层校验与结果分析5个Verilog文件实现物理层、链路层与事务层硬件模型另有6个Makefile及cfg、yml等配置用于组织仿真流程与回归测试。压缩包约181KB结构紧凑便于快速上手。资源围绕cocotbext-pcie扩展展开涵盖Endpoint、Root Complex等模型与示例测试脚本展示Python与Verilog协同的并发仿真方式。已有378人学习下载适合希望掌握PCIe验证方法、理解事务层包处理与自动化测试流程的读者参考。1. 从一次 PCIe 链路训练仿真说起做 PCIe 验证的人大多经历过这样的场景DUT 是一段 Verilog 写的 PCIe 控制器链路训练状态机在 LTSSM 的 Detect、Polling、Configuration 之间来回跳你想在波形里抓一个 TS1 有序集结果仿真跑了三十分钟还没进 L0。传统做法是写一个 Verilog testbench用 task 拼 TLP 包用#delay卡时序改一个字段就要重编译整条链路。Cocotb 的 PCIexpress 仿真框架解决的正是这件事——它把 Python 作为测试激励层通过 VPI/VHPI 挂到 Verilog 仿真器上让你用 Python 协程描述事务、用 Python 断言检查协议字段而 DUT 仍然是原汁原味的 Verilog RTL。这个组合的价值在于分工Verilog 负责时序精确的物理层与链路层逻辑Python 负责灵活可变的激励生成、覆盖率收集和结果比对。对做 IC 验证、尤其是 PCIe 这类协议栈深、状态多的模块Python 的字典、列表、随机库比 Verilog 的 task 好用太多。适合已经会写 Verilog、但被 testbench 维护成本拖住的工程师也适合刚入门验证、想用 Python 快速搭起 PCIe 事务级激励的新手。下面从环境搭建讲到 TLP 构造再到覆盖率与排错把这条链路走通。2. Cocotb 与 PCIe 仿真环境搭建2.1 Cocotb 安装与仿真器对接方式Cocotb 本身是一个 Python 库它不仿真仿真靠底层 EDA 工具。常见搭配是 Icarus Verilog开源、轻量或 Verilator快、但只支持可综合子集商业侧则是 VCS、Questa、Xcelium。安装 Cocotb 用 pip 即可但要注意 Python 版本与仿真器的 ABI 匹配。# 建议用虚拟环境隔离避免污染系统 Python python -m venv cocotb_pcie_env source cocotb_pcie_env/bin/activate # 安装 cocotb指定版本避免 API 变动 pip install cocotb1.8.0 # 安装 Icarus VerilogUbuntu 示例 sudo apt-get install iverilog # 验证安装 cocotb-config --version iverilog -V逻辑说明cocotb-config是 Cocotb 自带的配置查询工具能打印当前绑定的仿真器路径和 Python 版本。参数上cocotb1.8.0是较稳定的版本2.x 改了部分 API新手先用 1.8 系列减少踩坑。Icarus 对 SystemVerilog 支持有限如果 DUT 里用了interface或class要换 Verilator 或商业仿真器。提示Cocotb 通过MODULE环境变量找测试模块通过TOPLEVEL指定顶层通过TOPLEVEL_LANG指定 HDL 语言。这三个变量配错仿真会直接报找不到模块。2.2 用 Makefile 组织 PCIe DUT 与 Python 测试Cocotb 官方推荐 Makefile 流程核心是把 Verilog 源文件、顶层名、测试模块名串起来。下面是一个 PCIe 控制器仿真的最小 Makefile。# 仿真器选择 SIM ? icarus TOPLEVEL_LANG ? verilog # DUT 源文件PCIe 控制器拆成多个 .v VERILOG_SOURCES $(PWD)/rtl/pcie_ltssm.v VERILOG_SOURCES $(PWD)/rtl/pcie_tlp_rx.v VERILOG_SOURCES $(PWD)/rtl/pcie_tlp_tx.v # 顶层模块名必须与 Verilog 里 module 名一致 TOPLEVEL pcie_top # Python 测试模块名不带 .py MODULE test_pcie_tlp # 把当前目录加入 Python 路径 export PYTHONPATH : $(PWD):$(PYTHONPATH) include $(shell cocotb-config --makefiles)/Makefile.sim逻辑说明VERILOG_SOURCES列出所有参与编译的 RTL顺序不影响综合但影响include查找。TOPLEVEL是仿真顶层Cocotb 会从这里找时钟和复位。MODULE指向test_pcie_tlp.py里面用cocotb.test()装饰器定义测试用例。执行make就会编译 RTL、启动仿真器、加载 Python。参数上SIM可换成verilator或vcsTOPLEVEL_LANG在混合语言仿真时改成vhdl或mixed。如果 DUT 有参数化位宽可以在 Makefile 里用COMPILE_ARGS -P pcie_top.DATA_WIDTH64传入。2.3 时钟、复位与 PCIe 参考时钟的 Python 驱动PCIe 的参考时钟通常是 100MHz 差分仿真里简化为单端时钟。Cocotb 用cocotb.clock.Clock生成复位用cocotb.triggers.Timer控制。import cocotb from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer async def reset_dut(dut): 拉低复位并保持模拟 PCIe 上电复位时序 dut.rst_n.value 0 dut.ref_clk.value 0 await Timer(100, unitsns) # 保持 100ns 复位 dut.rst_n.value 1 await RisingEdge(dut.ref_clk) cocotb.test() async def test_link_up(dut): # 100MHz 参考时钟周期 10ns clock Clock(dut.ref_clk, 10, unitsns) cocotb.start_soon(clock.start()) await reset_dut(dut) # 等待 LTSSM 进入 L0最多等 100us for _ in range(10000): await RisingEdge(dut.ref_clk) if int(dut.ltssm_state.value) 0x10: # L0 状态编码 break assert int(dut.ltssm_state.value) 0x10, 链路未进入 L0逻辑说明Clock以 10ns 周期翻转ref_clk对应 100MHz。reset_dut先拉低rst_n保持 100ns再释放并等一个时钟沿确保复位同步。测试里轮询ltssm_state0x10 是 L0 的常见编码具体值要看 DUT 定义。assert失败时 Cocotb 会打印波形时间点方便定位。参数上Timer(100, unitsns)的 100ns 要大于 DUT 复位滤波要求轮询次数 10000 乘以 10ns 等于 100us是链路训练的超时上限。如果 DUT 训练慢加大这个值。3. 用 Python 构造 PCIe TLP 事务3.1 TLP 头部字段的 Python 建模PCIe TLP 头分 3DW 和 4DW 两种字段包括 Fmt、Type、TC、Length、Requester ID、Tag、Address 等。用 Python 的dataclass建模比 Verilog 的reg [127:0]拼接可读得多。from dataclasses import dataclass dataclass class TLPHeader: fmt: int # 2bit格式 type_: int # 5bit类型 tc: int # 3bit流量类别 length: int # 10bit以 DW 为单位的长度 requester_id: int # 16bit tag: int # 8bit address: int # 64bit仅 Mem 请求有效 def to_3dw(self) - int: 打包成 3DW 头返回 96bit 整数 word0 (self.fmt 29) | (self.type_ 24) | (self.tc 21) | self.length word1 (self.requester_id 16) | (self.tag 8) word2 self.address 0xFFFFFFFF return (word0 64) | (word1 32) | word2逻辑说明to_3dw按 PCIe 规范把字段移位拼接。fmt占 bit30:29type_占 bit28:24tc占 bit23:21length占 bit9:0。requester_id和tag拼成第二个 DW。地址低 32 位是第三个 DW。返回 96bit 整数后续可以拆成字节流驱动 DUT。参数上length是 DW 数量不是字节数Mem Read 请求里它表示要读多少 DW。tag用于区分未完成事务同一 Requester 下不能重复。address在 3DW 头里只有低 32 位高 32 位要用 4DW 头。3.2 把 TLP 灌进 Verilog DUT 的驱动协程DUT 的 TLP 接收接口通常是 valid/ready 握手。Cocotb 用协程在每个时钟沿驱动信号模拟背压。async def send_tlp(dut, header: TLPHeader, payload: list): 通过 valid/ready 接口发送一个 TLP data_words [header.to_3dw() 0xFFFFFFFF, (header.to_3dw() 32) 0xFFFFFFFF, (header.to_3dw() 64) 0xFFFFFFFF] data_words payload for word in data_words: dut.tlp_valid.value 1 dut.tlp_data.value word await RisingEdge(dut.ref_clk) # 等待 DUT 拉高 ready处理背压 while int(dut.tlp_ready.value) 0: await RisingEdge(dut.ref_clk) dut.tlp_valid.value 0逻辑说明to_3dw返回 96bit拆成三个 32bit 字依次发送。每个字先拉高tlp_valid等一个时钟沿再检查tlp_ready。如果 DUT 没准备好继续等这就是背压处理。发完所有字后拉低valid。参数上payload是 DW 列表Mem Write 时带上数据Mem Read 时为空。tlp_data位宽要和 DUT 接口一致32bit 还是 64bit 决定每次发几个字。如果 DUT 是 64bit 接口要把两个字拼成一个 64bit 值。3.3 用 Python 断言检查完成包与协议字段发完请求后DUT 会返回 Completion TLP。用 Python 解析并断言比在波形里肉眼找高效。async def recv_completion(dut): 接收一个 Completion TLP 并解析 words [] while True: await RisingEdge(dut.ref_clk) if int(dut.cpl_valid.value) 1: words.append(int(dut.cpl_data.value)) if int(dut.cpl_last.value) 1: break # 解析头部 fmt (words[0] 29) 0x3 type_ (words[0] 24) 0x1F assert fmt 0x0, fCompletion Fmt 错误: {fmt} assert type_ 0x0A, fCompletion Type 错误: {type_} return words逻辑说明循环等cpl_valid收集cpl_data直到cpl_last拉高。然后从第一个字里提取fmt和type_。Completion 的fmt通常是 0x03DW 无数据或 0x23DW 带数据type_是 0x0A。断言失败会打印实际值方便对照规范。参数上cpl_last是包结束标志有些 DUT 用cpl_valid拉低表示结束要看接口定义。words[0]是头部第一个 DW字段位置和请求头一致。4. PCIe 仿真中的覆盖率与回归4.1 用 Python 字典收集 TLP 类型覆盖率PCIe 验证要求覆盖所有 TLP 类型和边界长度。Cocotb 里用 Python 字典做覆盖率收集比 Verilog 的covergroup灵活。from collections import defaultdict class TLPCoverage: def __init__(self): self.type_hit defaultdict(int) self.length_hit defaultdict(int) def sample(self, header: TLPHeader): self.type_hit[header.type_] 1 # 长度分桶1DW、2-4DW、5-16DW、16DW if header.length 1: self.length_hit[1DW] 1 elif header.length 4: self.length_hit[2-4DW] 1 elif header.length 16: self.length_hit[5-16DW] 1 else: self.length_hit[16DW] 1 def report(self): print(TLP 类型覆盖:, dict(self.type_hit)) print(长度覆盖:, dict(self.length_hit))逻辑说明defaultdict(int)让未出现的键自动初始化为 0。sample在每次发 TLP 时调用累加类型计数长度按区间分桶。report在测试结束时打印人工检查是否所有类型都非零。参数上长度分桶的边界 1、4、16 对应 PCIe 常见的最长 payload 和 header 组合。如果 DUT 支持 256DW 大包要加一个16DW以上的桶。覆盖率报告可以写进文件用 CI 脚本判断是否达标。4.2 回归脚本与随机种子管理PCIe 仿真跑一次几分钟回归要跑几十个种子。用 Python 脚本调make把种子传进 Cocotb。#!/bin/bash # run_regression.sh for seed in $(seq 1 20); do echo 种子 $seed make clean make MODULEtest_pcie_tlp SEED$seed 21 | tee log_seed_$seed.txt if grep -q FAIL log_seed_$seed.txt; then echo 种子 $seed 失败 fi done逻辑说明循环 20 个种子每次make clean清掉上次编译产物SEED传给 Cocotb 的随机数生成器。日志按种子编号保存grep FAIL快速定位失败用例。参数上SEED是 Cocotb 内置变量影响random模块的种子。种子数量看 DUT 复杂度PCIe 控制器一般 50 到 100 个种子能覆盖主要状态。make clean不能省否则 RTL 改动不会重新编译。注意Cocotb 的随机种子只影响 Python 侧Verilog 里的$random要单独用ntb_random_seed或仿真器参数控制否则两边随机不同步复现困难。4.3 常见失败模式与波形定位PCIe 仿真失败集中在几类链路训练超时、TLP 握手死锁、Completion 超时、字段断言失败。Cocotb 失败时会打印仿真时间配合波形查看器定位。失败现象可能原因排查手段LTSSM 卡在 Polling参考时钟频率不对检查Clock周期与 DUT 期望tlp_ready 一直为 0DUT 接收 FIFO 满看 DUT 内部 FIFO 指针Completion 超时Tag 不匹配或地址越界打印请求 Tag 和 DUT 返回 Tag字段断言失败头部打包移位错误对照规范逐位检查to_3dw逻辑说明表格里每行对应一类失败排查手段是具体动作。比如tlp_ready为 0先看 DUT 的 FIFO 深度和写指针可能是激励发太快。参数上波形文件用make WAVES1生成Icarus 输出 VCDVerilator 输出 FST。VCD 文件大长仿真建议只 dump 关键信号在 Makefile 里加COMPILE_ARGS -DDUMP_ON控制。5. 让 PCIe 仿真跑得更快的几个技巧Cocotb 加 Verilog 的仿真速度瓶颈通常在 Python 与仿真器的跨语言调用。每次dut.signal.value读写都要过 VPI频繁访问会拖慢。一个直接优化是把多个信号打包成一次读用dut.signal.value读整个向量再在 Python 里拆位而不是逐位读。# 慢逐位读 # for i in range(32): # bit (int(dut.tlp_data.value) i) 1 # 快一次读整个向量 data int(dut.tlp_data.value) bits [(data i) 1 for i in range(32)]逻辑说明int(dut.tlp_data.value)只触发一次 VPI 调用后续位操作在 Python 内存里完成。逐位读会触发 32 次 VPI差距在长仿真里很明显。参数上tlp_data位宽越大收益越明显。64bit 接口一次读比逐位读快一个数量级。如果 DUT 信号是logic类型Cocotb 返回LogicArray用int()转换即可。另一个技巧是减少RisingEdge等待。如果测试逻辑不依赖每个时钟沿可以用Timer等更长时间再采样减少协程切换。但 PCIe 握手必须逐沿检查不能省。折中做法是把轮询间隔从 1 个时钟改成 4 个时钟只在关键状态切换时逐沿。覆盖率收集也有开销。sample每次发 TLP 都调如果 TLP 数量上万字典操作会累积。可以每 100 个 TLP 采样一次或者只对边界长度采样。回归时用SEED控制随机失败种子单独重跑不用全量重来。最后仿真器选择影响最大。Icarus 免费但慢Verilator 快但只支持可综合子集PCIe 控制器如果全是可综合 RTLVerilator 能快 5 到 10 倍。切换只需改 Makefile 里SIM verilator但要注意 Verilator 对initial块和延迟的支持有限DUT 里如果有#delay要改成时钟同步。本文还有配套的精品资源点击获取
