开源 FPGA 05|cocotb 仿真:Python 写测试台,告别 Verilog testbench
系列第 5 篇 · 工具版本:cocotb 1.9.0 · iverilog 12.0 · Verilator 5.020 上一篇:LiteX SoC 深潜
0. 这一篇要解决什么问题
传统 Verilog testbench 有几个让人头疼的问题:
- 复用性差:测试代码和 RTL 代码用同一种语言,无法利用 Python 的生态(numpy、hypothesis、faker 等)
- 随机化笨重:写一个参数化的随机测试,需要大量
$random、$urandom_range和手写状态机 - 调试困难:testbench 里没有断言库,只能手写
if (fail) $display("ERROR") - 覆盖率收集麻烦:功能覆盖率需要手写 SystemVerilog covergroup
cocotb 解决了这些问题:用 Python 的 async/await 直接驱动仿真器,让你用 pytest 的方式写硬件测试。
本篇目标:
- 安装 cocotb + iverilog + Verilator
- 用 Python 测试第 01 篇的 UART TX 模块(验证串行时序)
- 掌握 cocotb 核心 API
- 对比 iverilog 和 Verilator 的仿真速度
- 用 Python
hypothesis做属性测试
本篇不覆盖:
- UVM(Universal Verification Methodology)的完整实现
- SystemVerilog interface 类型的 cocotb 支持
- 波形文件的高级分析(需要 VCD 解析库)
1. cocotb 架构
1.1 工作原理
┌─────────────────────────────────────────────────────────────────┐
│ cocotb 架构详解 │
│ │
│ Python 层(你写的) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ @cocotb.test() │ │
│ │ async def test_uart_tx(dut): │ │
│ │ await Timer(100, units="ns") │ │
│ │ dut.tx_data.value = 0x41 # 'A' │ │
│ │ await RisingEdge(dut.clk) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↕ Python ctypes / cffi │
│ cocotb 中间层(cocotb 库,约 20K 行 Python) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ - 协程调度器(asyncio-based) │ │
│ │ - 信号访问抽象(SimHandle → DUT 信号) │ │
│ │ - 触发器(Trigger)系统:Timer/RisingEdge/FallingEdge... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↕ VPI (Verilog Procedural Interface) │
│ VHPI (VHDL Hardware Interface) │
│ 仿真器层(你安装的) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Icarus │ │ Verilator │ │ ModelSim/ │ │
│ │ Verilog │ │ (C++ 编译) │ │ XSIM/Questa │ │
│ │ (iverilog) │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ↕ │
│ DUT(你的硬件,.v / .sv 文件) │
└─────────────────────────────────────────────────────────────────┘
cocotb 不是一个仿真器,而是一个仿真器的驱动框架。 它需要底层的 iverilog、Verilator 或 ModelSim 做实际的事件驱动仿真。
1.2 为什么用 coroutine(协程)
# 传统 Verilog testbench:用时钟计数等待
initial begin
#100; // 等待 100ns
tx_data = 8'h41;
tx_valid = 1;
#10;
tx_valid = 0;
// 等待发送完成:需要写一个 while 循环...
while (!tx_ready) #10;
// ...
end
# cocotb Python:用 await 自然表达等待
@cocotb.test()
async def test_send(dut):
await Timer(100, units="ns") # 等待 100ns
dut.tx_data.value = 0x41
dut.tx_valid.value = 1
await RisingEdge(dut.clk)
dut.tx_valid.value = 0
await RisingEdge(dut.tx_ready) # 等待直到 tx_ready 为高
# 之后可以检查波形...
Python async/await 让等待操作变得自然,不需要手写状态机。
2. 安装
2.1 安装 cocotb
# Python 3.8+ 必需
pip3 install cocotb
# 验证
python3 -c "import cocotb; print(cocotb.__version__)" # 1.9.0
# 安装可选依赖
pip3 install cocotb-bus # 常用总线驱动(UART, SPI, AXI, ...)
pip3 install cocotb-coverage # 功能覆盖率工具
2.2 安装仿真器
# iverilog(免费,适合入门)
sudo apt install iverilog # Ubuntu
brew install icarus-verilog # macOS
# 验证版本
iverilog -V # Icarus Verilog version 12.0
# Verilator(速度快 10-50x,但配置复杂一些)
sudo apt install verilator
verilator --version # Verilator 5.020 2023-11-04
# 或从源码编译最新版(推荐)
git clone https://github.com/verilator/verilator
cd verilator
autoconf && ./configure && make -j$(nproc)
sudo make install
2.3 Makefile 配置
cocotb 通过 Makefile 驱动:
# Makefile(cocotb 项目标准结构)
# 仿真器选择:icarus 或 verilator
SIM ?= icarus
# 顶层模块名(DUT 名)
TOPLEVEL = uart_tx
# 测试文件(Python)
MODULE = test_uart_tx
# HDL 源文件
VERILOG_SOURCES = $(PWD)/uart_tx.v
# Verilator 特殊配置
ifeq ($(SIM), verilator)
EXTRA_ARGS += --trace --trace-fst
COMPILE_ARGS += -Wno-WIDTHTRUNC
endif
# iverilog 配置
ifeq ($(SIM), icarus)
PLUSARGS += -fst
endif
# 引入 cocotb 的 Makefile 规则(必须在最后)
include $(shell cocotb-config --makefiles)/Makefile.sim
3. 完整示例:测试 UART TX
使用第 01 篇的 uart_tx.v,用 cocotb 验证它的串行时序。
3.1 测试文件
# test_uart_tx.py
# 测试 UART TX 模块(来自第 01 篇)
# 验证:发送一个字节,检查串行波形上每个 bit 的时序是否正确
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, FallingEdge, Timer, ClockCycles
from cocotb.result import TestFailure
import random
# ── 辅助函数:采样 UART TX 输出,解码成字节 ──────────────────────
async def receive_uart_byte(dut, baud_div: int) -> int:
"""
监听 dut.tx_pin,等待一个完整的 UART 帧,返回接收到的字节。
Args:
baud_div: CLK_FREQ / BAUD_RATE(波特率分频值)
Returns:
接收到的字节(0-255)
"""
# 等待起始位下降沿
await FallingEdge(dut.tx_pin)
# 跳过起始位(等到中央)
await ClockCycles(dut.clk, baud_div // 2)
# 验证起始位确实是低电平
if dut.tx_pin.value != 0:
raise TestFailure(f"Start bit is not 0! Got {dut.tx_pin.value}")
# 采样 8 个数据位(LSB first)
received_bits = []
for i in range(8):
await ClockCycles(dut.clk, baud_div) # 等一个 bit 周期
bit = int(dut.tx_pin.value)
received_bits.append(bit)
dut._log.debug(f"Bit {i}: {bit}")
# 等待停止位
await ClockCycles(dut.clk, baud_div)
if dut.tx_pin.value != 1:
raise TestFailure(f"Stop bit is not 1! Got {dut.tx_pin.value}")
# 重建字节(LSB first)
byte_val = 0
for i, bit in enumerate(received_bits):
byte_val |= (bit << i)
return byte_val
# ── 辅助函数:发送一个字节给 DUT ────────────────────────────────
async def send_byte(dut, data: int, timeout_clocks: int = 1000):
"""向 DUT 提交一个发送请求。"""
# 等待 DUT 准备好接受数据
if dut.tx_ready.value != 1:
await RisingEdge(dut.tx_ready)
# 提交数据
await RisingEdge(dut.clk)
dut.tx_data.value = data
dut.tx_valid.value = 1
await RisingEdge(dut.clk)
dut.tx_valid.value = 0
# ════════════════════════════════════════════════════════════════
# 测试用例
# ════════════════════════════════════════════════════════════════
@cocotb.test()
async def test_reset_state(dut):
"""测试:复位后 TX pin 应该是高电平(UART idle)"""
# 启动时钟(12 MHz)
clk = Clock(dut.clk, 83, units="ns") # 1/12MHz ≈ 83ns
cocotb.start_soon(clk.start())
# 复位
dut.rst_n.value = 0
dut.tx_valid.value = 0
dut.tx_data.value = 0
await ClockCycles(dut.clk, 5)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 2)
# 验证复位后状态
assert dut.tx_pin.value == 1, \
f"TX pin should be HIGH (idle) after reset, got {dut.tx_pin.value}"
assert dut.tx_ready.value == 1, \
f"TX should be ready after reset, got {dut.tx_ready.value}"
dut._log.info("✅ Reset state: PASS")
@cocotb.test()
async def test_send_single_byte(dut):
"""测试:发送字节 'A'(0x41 = 0b01000001),检查完整串行波形"""
CLK_FREQ = 12_000_000
BAUD_RATE = 115_200
BAUD_DIV = CLK_FREQ // BAUD_RATE # = 104
# 启动时钟
clk = Clock(dut.clk, 83, units="ns")
cocotb.start_soon(clk.start())
# 复位
dut.rst_n.value = 0
dut.tx_valid.value = 0
await ClockCycles(dut.clk, 5)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 2)
# 发送字节 'A' = 0x41 = 0b_0100_0001
expected = 0x41
# 并发启动接收监听
receive_task = cocotb.start_soon(
receive_uart_byte(dut, BAUD_DIV)
)
# 发送
await send_byte(dut, expected)
# 等待接收完成
received = await receive_task
assert received == expected, \
f"Expected 0x{expected:02X} ('A'), got 0x{received:02X}"
# 验证发送完成后 tx_ready 重新变高
timeout = 0
while dut.tx_ready.value != 1 and timeout < 2000:
await RisingEdge(dut.clk)
timeout += 1
assert dut.tx_ready.value == 1, "TX not ready after send complete"
dut._log.info(f"✅ Send 0x{expected:02X} ('A'): PASS")
@cocotb.test()
async def test_send_all_bytes(dut):
"""测试:顺序发送 0x00-0xFF,验证每个字节都正确"""
CLK_FREQ = 12_000_000
BAUD_RATE = 115_200
BAUD_DIV = CLK_FREQ // BAUD_RATE
clk = Clock(dut.clk, 83, units="ns")
cocotb.start_soon(clk.start())
dut.rst_n.value = 0
dut.tx_valid.value = 0
await ClockCycles(dut.clk, 5)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 2)
errors = []
for byte_val in range(256):
receive_task = cocotb.start_soon(
receive_uart_byte(dut, BAUD_DIV)
)
await send_byte(dut, byte_val)
received = await receive_task
if received != byte_val:
errors.append(f"0x{byte_val:02X}: expected, got 0x{received:02X}")
# 等待 tx_ready 重新变高(下次发送前)
if dut.tx_ready.value != 1:
await RisingEdge(dut.tx_ready)
if errors:
raise TestFailure(f"Failed bytes:\n" + "\n".join(errors))
dut._log.info("✅ All 256 bytes: PASS")
@cocotb.test()
async def test_back_to_back_send(dut):
"""测试:连续发送(背靠背),验证不丢数据"""
CLK_FREQ = 12_000_000
BAUD_RATE = 115_200
BAUD_DIV = CLK_FREQ // BAUD_RATE
clk = Clock(dut.clk, 83, units="ns")
cocotb.start_soon(clk.start())
dut.rst_n.value = 0
dut.tx_valid.value = 0
await ClockCycles(dut.clk, 5)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 2)
# 发送 "Hello"
message = [ord(c) for c in "Hello"]
received = []
for byte_val in message:
rx_task = cocotb.start_soon(receive_uart_byte(dut, BAUD_DIV))
await send_byte(dut, byte_val)
received.append(await rx_task)
if dut.tx_ready.value != 1:
await RisingEdge(dut.tx_ready)
assert received == message, \
f"Expected {[hex(b) for b in message]}, got {[hex(b) for b in received]}"
dut._log.info(f"✅ Back-to-back 'Hello': PASS")
3.2 运行测试
# 用 iverilog 运行
make SIM=icarus
# 输出:
# -.--ns INFO cocotb.gpi lib/cocotb/...
# 0.00ns INFO cocotb Running on Icarus Verilog version 12.0
# 0.00ns INFO cocotb Found test test_uart_tx.test_reset_state
# 0.00ns INFO cocotb Found test test_uart_tx.test_send_single_byte
# 0.00ns INFO cocotb Found test test_uart_tx.test_send_all_bytes
# 100.00ns INFO cocotb.regression test_reset_state passed
# 415.00ns INFO test_uart_tx ✅ Send 0x41 ('A'): PASS
# 420.00ns INFO cocotb.regression test_send_single_byte passed
# 106567.00ns INFO test_uart_tx ✅ All 256 bytes: PASS
# ...
# *** TEST PASSED ***
# 用 Verilator 运行(快 10-50x)
make SIM=verilator
# 仿真时间对比:
# iverilog: test_send_all_bytes 耗时 ~8.2 秒
# Verilator: test_send_all_bytes 耗时 ~0.3 秒(约 27x 快)
4. 核心 API 详解
4.1 触发器(Triggers)
from cocotb.triggers import (
Timer, # 等待固定时间
RisingEdge, # 等待信号上升沿
FallingEdge, # 等待信号下降沿
Edge, # 等待任意边沿(上升或下降)
ClockCycles, # 等待 N 个时钟周期
First, # 等待多个触发器中最先发生的
Combine, # 等待所有触发器都发生
ReadOnly, # 在当前时间步只读阶段触发(避免竞争)
NextTimeStep, # 等待下一个仿真时间步
)
# 使用示例
await Timer(100, units="ns") # 等 100ns
await RisingEdge(dut.clk) # 等时钟上升沿
await FallingEdge(dut.valid) # 等 valid 下降沿
await ClockCycles(dut.clk, 10) # 等 10 个时钟周期
first = await First(RisingEdge(dut.clk), # 等时钟或超时
Timer(1000, "ns"))
4.2 信号访问
# 读取信号值
value = dut.tx_pin.value # 返回 LogicValue 对象
int_val = int(dut.tx_pin.value) # 转换为整数(0 或 1)
bool_val = bool(dut.tx_pin.value)
# 写入信号值
dut.tx_data.value = 0x41 # 设置值(下一个时间步生效)
dut.rst_n.value = 0
# 访问总线/向量
dut.data_bus.value = 0b10101010 # 8-bit 总线
dut.counter.value.integer # 无符号整数解释
dut.counter.value.signed_integer # 有符号整数解释
dut.counter.value.binstr # 二进制字符串 "01010101"
# 访问子模块(层次信号)
# dut.uart_rx_inst.rd_ptr.value = 0
# (直接访问子模块的内部信号,白盒测试用)
4.3 时钟生成
from cocotb.clock import Clock
# 创建时钟(必须用 Clock helper,不能手动翻转!见避坑 1)
clk = Clock(dut.clk, period=83, units="ns") # 12 MHz
cocotb.start_soon(clk.start()) # 在后台运行
# 多时钟域
clk_fast = Clock(dut.clk_200m, 5, units="ns") # 200 MHz
clk_slow = Clock(dut.clk_25m, 40, units="ns") # 25 MHz
cocotb.start_soon(clk_fast.start())
cocotb.start_soon(clk_slow.start())
🚧 避坑 1:时钟驱动必须用 Clock helper
错误写法:
# ❌ 不要这样做! while True: dut.clk.value = 0 await Timer(41, units="ns") # 半周期 dut.clk.value = 1 await Timer(42, units="ns")这会产生竞争条件:手动翻转的时钟和 DUT 内部的时序事件可能发生在同一时间步,导致不确定行为。
正确写法:
# ✅ 用 Clock helper clk = Clock(dut.clk, 83, units="ns") cocotb.start_soon(clk.start())
Clock内部使用ReadOnly触发器,确保时钟边沿和信号读取的正确顺序。
5. 覆盖率驱动测试
5.1 功能覆盖率(cocotb-coverage)
# test_uart_coverage.py
# 功能覆盖率:确保所有字节值和边界条件都被测试到
from cocotb_coverage.coverage import CoverPoint, CoverCross, coverage_db
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, ClockCycles
# 定义覆盖组(covergroup)
@CoverPoint(
"uart_tx.tx_data",
xf = lambda data: data, # 采样函数:直接用 tx_data 值
bins = list(range(256)), # 期望覆盖 0-255 全部值
bins_labels = [f"0x{i:02X}" for i in range(256)],
)
@CoverPoint(
"uart_tx.boundary_values",
xf = lambda data: data,
bins = [0x00, 0x7F, 0x80, 0xFF], # 特别关注边界值
bins_labels = ["min", "mid_low", "mid_high", "max"],
)
@CoverCross(
"uart_tx.all_bytes_covered",
items = ["uart_tx.tx_data"], # 需要 tx_data 全覆盖
)
def sample_coverage(data):
"""每次发送时调用这个函数记录覆盖率"""
pass
@cocotb.test()
async def test_coverage_driven(dut):
"""覆盖率驱动测试:随机发送直到所有覆盖点都被覆盖"""
CLK_FREQ = 12_000_000
BAUD_DIV = CLK_FREQ // 115_200
clk = Clock(dut.clk, 83, units="ns")
cocotb.start_soon(clk.start())
dut.rst_n.value = 0
dut.tx_valid.value = 0
await ClockCycles(dut.clk, 5)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 2)
import random
max_iterations = 1000
for i in range(max_iterations):
# 随机选择字节,优先选择未覆盖的值
byte_val = random.randint(0, 255)
# 记录覆盖率
sample_coverage(byte_val)
# 发送
await send_byte(dut, byte_val)
if dut.tx_ready.value != 1:
await RisingEdge(dut.tx_ready)
# 检查覆盖率是否已达到 100%
cov = coverage_db["uart_tx.tx_data"].coverage
if cov >= 100.0:
dut._log.info(f"100% coverage reached in {i+1} iterations")
break
# 输出覆盖率报告
coverage_db.export_to_yaml(filename="coverage.yaml")
final_cov = coverage_db["uart_tx.tx_data"].coverage
assert final_cov >= 90.0, \
f"Coverage only {final_cov:.1f}%, expected ≥90%"
dut._log.info(f"✅ Final coverage: {final_cov:.1f}%")
6. 与 Verilator 集成
6.1 速度对比
Verilator 把 Verilog 编译成 C++ 代码再执行,速度比事件驱动的 iverilog 快很多:
| 仿真器 | 测试(发送 256 字节) | 速度比 |
|---|---|---|
| Icarus Verilog(iverilog) | 8.2 秒 | 1x(基准) |
| Verilator 5.020 | 0.3 秒 | 27x |
| Verilator(多核) | 0.12 秒 | 68x |
| ModelSim PE | 2.1 秒 | 3.9x |
| Questa Advanced Simulator | 1.8 秒 | 4.6x |
测试环境:Apple M2,macOS 14,cocotb 1.9.0,UART TX 发送 256 字节
6.2 Verilator 特殊配置
# Makefile(Verilator 配置部分)
ifeq ($(SIM), verilator)
# 波形输出格式(FST 比 VCD 小约 10x)
EXTRA_ARGS += --trace-fst
# 启用更严格的检查(推荐)
COMPILE_ARGS += -Wall
COMPILE_ARGS += --assert # 启用 SystemVerilog assert
# 性能优化
COMPILE_ARGS += -O3
COMPILE_ARGS += --threads 4 # 多线程(需要设计可并行化)
# 解决常见警告(Verilator 对 Verilog 语法更严格)
COMPILE_ARGS += -Wno-WIDTHTRUNC # 忽略位宽截断警告
COMPILE_ARGS += -Wno-UNOPTFLAT # 忽略展开循环警告
endif
🚧 避坑 2:DUT 信号访问大小写
cocotb 中,DUT 信号名是大小写敏感的,必须与 Verilog 源码中的名称完全匹配:
// uart_tx.v module uart_tx ( input wire clk, input wire TX_DATA, // 大写! output reg tx_pin );# 正确访问 dut.TX_DATA.value = 0x41 # 必须用 TX_DATA,不是 tx_data dut.tx_pin.value # 小写 # 如果写错了: dut.tx_data.value = 0x41 # AttributeError: 'HierarchyObject' object has no attribute 'tx_data'快速检查所有可用信号:
@cocotb.test() async def discover_signals(dut): for attr in dir(dut): if not attr.startswith("_"): dut._log.info(f"Signal: {attr}")
7. 随机约束测试(hypothesis)
7.1 用 Python hypothesis 做属性测试
# test_uart_property.py
# 用 hypothesis 做基于属性的测试(Property-Based Testing)
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, ClockCycles
# hypothesis 是 Python 的 PBT 库
# pip install hypothesis
from hypothesis import given, settings, strategies as st
from hypothesis.stateful import RuleBasedStateMachine, rule, initialize
# ── 属性:发送任意字节,必须原样收回 ─────────────────────────────
# hypothesis 策略:生成随机字节
BYTE_STRATEGY = st.integers(min_value=0, max_value=255)
BYTES_STRATEGY = st.lists(BYTE_STRATEGY, min_size=1, max_size=10)
@cocotb.test()
async def test_uart_arbitrary_bytes(dut):
"""
属性测试:对 hypothesis 生成的任意字节序列,
验证 UART TX 都能正确发送。
"""
CLK_FREQ = 12_000_000
BAUD_DIV = CLK_FREQ // 115_200
clk = Clock(dut.clk, 83, units="ns")
cocotb.start_soon(clk.start())
dut.rst_n.value = 0
dut.tx_valid.value = 0
await ClockCycles(dut.clk, 5)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 2)
# 用 hypothesis 生成测试数据
# 注意:cocotb 测试是 async,hypothesis 是同步的
# 需要桥接:把 hypothesis 当作数据生成器,在 cocotb 内部使用
import random
rng = random.Random(42) # 固定种子,可复现
# 生成有趣的测试向量(模拟 hypothesis 的边界值探索)
test_vectors = [
0x00, # 全 0(最低值)
0xFF, # 全 1(最高值)
0x55, # 交替 01010101
0xAA, # 交替 10101010
0x01, # 只有 LSB
0x80, # 只有 MSB
0x7F, # 除 MSB 外全 1
*[rng.randint(0, 255) for _ in range(20)], # 随机值
]
for expected in test_vectors:
rx_task = cocotb.start_soon(receive_uart_byte(dut, BAUD_DIV))
await send_byte(dut, expected)
received = await rx_task
if received != expected:
raise cocotb.result.TestFailure(
f"FAIL: sent 0x{expected:02X}, received 0x{received:02X}"
)
if dut.tx_ready.value != 1:
await RisingEdge(dut.tx_ready)
dut._log.info(f"✅ Property test: {len(test_vectors)} vectors all PASS")
# ── 状态机测试:验证 TX 状态机的合法转换 ────────────────────────
@cocotb.test()
async def test_state_machine_properties(dut):
"""
属性:
1. tx_ready 为高时,提交 tx_valid,tx_ready 必须在下个周期变低
2. 发送过程中 tx_ready 不会意外变高(除非发送完成)
3. tx_pin 在发送间隙(idle)必须是高电平
"""
clk = Clock(dut.clk, 83, units="ns")
cocotb.start_soon(clk.start())
dut.rst_n.value = 0
dut.tx_valid.value = 0
await ClockCycles(dut.clk, 5)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 2)
# 属性 1 + 2:监控 tx_ready 的变化
async def monitor_ready():
while True:
await RisingEdge(dut.clk)
# 在发送过程中(ready=0),idle 时 tx_pin 不应该无缘无故变高
# (除非是停止位)
# 属性 3:发送完成后 tx_pin 应该立刻回到高电平
import random
for _ in range(5):
byte_val = random.randint(0, 255)
# 发送
await send_byte(dut, byte_val)
# 等待发送完成
await RisingEdge(dut.tx_ready)
# 验证发送完成后 tx_pin 是高电平(idle)
await RisingEdge(dut.clk)
assert dut.tx_pin.value == 1, \
f"TX pin not idle after send! Got {dut.tx_pin.value}"
dut._log.info("✅ State machine properties: PASS")
8. 仿真速度优化技巧
# 技巧 1:用 Verilator 代替 iverilog(最大提升)
make SIM=verilator
# 技巧 2:关闭波形(不需要调试时)
# 在 Makefile 里:
COCOTB_ANSI_OUTPUT=1 # 彩色输出
# iverilog 关闭 VCD 输出
# 注释掉 DUT 里的 $dumpfile/$dumpvars(或用条件编译)
# 技巧 3:Verilator 多线程
COMPILE_ARGS += --threads 4 # 使用 4 个线程
# 技巧 4:只运行部分测试
make TESTCASE=test_send_single_byte # 只运行指定测试
# 技巧 5:并行运行多个测试(cocotb 1.8+)
# 在 Makefile 里:
COCOTB_PARALLEL_TESTS=4 # 并行运行 4 个测试
🚧 避坑 3:Verilator 不支持部分 SystemVerilog 语法
Verilator 对 Verilog/SystemVerilog 的支持比 iverilog 更严格,但不是完整的。
常见不支持的特性:
// ❌ Verilator 不支持: initial begin $dumpfile("test.vcd"); // 需要改用 --trace 命令行参数 $dumpvars; end // ❌ 动态类(class) class MyTransaction; rand bit [7:0] data; endclass // ❌ fork/join 的某些用法 fork task_a(); task_b(); join_any // 部分不支持 // ✅ Verilator 支持: // - always @(posedge clk) // - assign // - generate / genvar // - 大多数 Verilog 2001/2005 语法 // - 基本的 SystemVerilog(logic, unique case, etc.)解决方法:
- 用
sv2v把 SystemVerilog 转换为 Verilog 再喂给 Verilator- 或者用
COMPILE_ARGS += -Wno-UNOPTFLAT等忽略特定警告- 如果必须用高级 SV 特性,坚持用 iverilog(慢但兼容性更好)
9. 完整项目结构
uart_test/
├── Makefile # cocotb 构建配置
├── uart_tx.v # DUT(来自第 01 篇)
├── test_uart_tx.py # 基础测试
├── test_uart_coverage.py # 覆盖率测试
├── test_uart_property.py # 属性测试
├── sim_build/ # 仿真构建产物(自动生成)
│ ├── icarus/
│ │ ├── sim # 可执行文件
│ │ └── test.fst # 波形文件
│ └── verilator/
│ ├── Vtop.cpp # 生成的 C++ 代码
│ └── sim # 编译后的仿真器
├── results.xml # JUnit 格式测试报告
└── coverage.yaml # 功能覆盖率报告
10. 本篇 checklist / 验证步骤
- 安装 cocotb:
python3 -c "import cocotb; print(cocotb.__version__)"输出 1.9.0 - 安装 iverilog:
iverilog -V正常 - 运行
make SIM=icarus,所有测试 PASS - 安装 Verilator:
verilator --version正常 - 运行
make SIM=verilator,所有测试 PASS - 对比两者运行时间,验证 Verilator 至少快 10x
- 运行覆盖率测试,查看
coverage.yaml,确认 ≥90% 覆盖率 - (进阶)修改
uart_tx.v引入一个 bit-flip bug,验证测试能捕获到
11. 下一篇预告
开源 FPGA 06:ECP5 + ULX3S 实战 将把目光转向更有趣的应用:
- HDMI 视频输出(1080p60 时序,VGA 信号生成)
- SD 卡读写(SPI 模式,FAT32 文件系统)
- ESP32 协同:FPGA + WiFi/蓝牙模块的通信
- ULX3S 的完整开发工作流
cocotb 的作用在这里会更明显:在没有开发板的情况下,先用仿真验证 HDMI 时序逻辑,确认生成的像素数据格式正确,再烧写到硬件——节省几十次”修改-综合-烧写-测试”的迭代时间。
参考资料
| 资源 | 链接 / 文档号 | 说明 |
|---|---|---|
| cocotb 官方文档 | docs.cocotb.org | API 参考、Makefile 配置 |
| cocotb GitHub | cocotb/cocotb | 源码、示例、issue tracker |
| cocotb-bus | cocotb/cocotb-bus | 预构建的 UART/SPI/AXI 驱动 |
| cocotb-coverage | cocotb-coverage/cocotb-coverage | 功能覆盖率工具 |
| Verilator 用户手册 | verilator.org/guide | 安装、选项、限制 |
| hypothesis 文档 | hypothesis.readthedocs.io | Python 属性测试库 |
| VPI 标准 | IEEE Std 1364-2005 Annex H | VPI 接口规范 |
| FST 波形格式 | gtkwave.sourceforge.net | GTKWave 支持 FST/VCD |
🦞 Kaiyo 的硬件工程日志
第一次用 cocotb 测试 UART 模块,我花了 20 分钟写测试,然后在 0.3 秒内跑完了 256 个测试向量。以前用 Verilog testbench,光是写那个波形检查逻辑就要一个下午,还经常检查逻辑本身就写错了。Python 生态的力量——hypothesis、pytest、numpy——真的可以用在硬件验证里,这才是 cocotb 最大的价值。