← ブログ一覧へ
FPGAcocotb仿真PythonVerilatoriverilog测试UARTRTL验证

开源 FPGA 05|cocotb 仿真:Python 写测试台,告别 Verilog testbench

この記事は中国語で書かれ、Google 翻訳で自動翻訳されています。
中国語の原文を見る →
cocotb 仿真调用栈 Python 协程通过 VPI 接口驱动 RTL 仿真器 Layer 1 · Python Test (your code) 用户测试用例 · pytest 风格 · async/await 协程 @cocotb.test() async def test_uart(dut): await RisingEdge(dut.clk); dut.tx_data.value = 42 schedule coroutine Layer 2 · cocotb Framework 事件循环 (scheduler) · trigger 系统 · BinaryValue 转换 Triggers · Edges Coroutine scheduler Handle / Signal proxy C ABI · libcocotb.so Layer 3 · VPI / VHPI (IEEE 1800/1076 标准接口) cb_data_s 回调 · vpiHandle 句柄 · 信号读写 / 时间推进 / 事件订阅 vpi_register_cb() · vpi_get_value() · vpi_put_value() drive signals Layer 4 · RTL Simulator + DUT Icarus Verilog Verilator ModelSim DUT (uart_tx.v)
图 0 · cocotb 调用栈:Python 协程 → cocotb 调度器 → VPI → 仿真器内核

系列第 5 篇 · 工具版本:cocotb 1.9.0 · iverilog 12.0 · Verilator 5.020 上一篇:LiteX SoC 深潜


0. 这一篇要解决什么问题

传统 Verilog testbench 有几个让人头疼的问题:

  1. 复用性差:测试代码和 RTL 代码用同一种语言,无法利用 Python 的生态(numpy、hypothesis、faker 等)
  2. 随机化笨重:写一个参数化的随机测试,需要大量 $random$urandom_range 和手写状态机
  3. 调试困难:testbench 里没有断言库,只能手写 if (fail) $display("ERROR")
  4. 覆盖率收集麻烦:功能覆盖率需要手写 SystemVerilog covergroup

cocotb 解决了这些问题:用 Python 的 async/await 直接驱动仿真器,让你用 pytest 的方式写硬件测试。

本篇目标:

  1. 安装 cocotb + iverilog + Verilator
  2. 用 Python 测试第 01 篇的 UART TX 模块(验证串行时序)
  3. 掌握 cocotb 核心 API
  4. 对比 iverilog 和 Verilator 的仿真速度
  5. 用 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.0200.3 秒27x
Verilator(多核)0.12 秒68x
ModelSim PE2.1 秒3.9x
Questa Advanced Simulator1.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.)

解决方法:

  1. sv2v 把 SystemVerilog 转换为 Verilog 再喂给 Verilator
  2. 或者用 COMPILE_ARGS += -Wno-UNOPTFLAT 等忽略特定警告
  3. 如果必须用高级 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.orgAPI 参考、Makefile 配置
cocotb GitHubcocotb/cocotb源码、示例、issue tracker
cocotb-buscocotb/cocotb-bus预构建的 UART/SPI/AXI 驱动
cocotb-coveragecocotb-coverage/cocotb-coverage功能覆盖率工具
Verilator 用户手册verilator.org/guide安装、选项、限制
hypothesis 文档hypothesis.readthedocs.ioPython 属性测试库
VPI 标准IEEE Std 1364-2005 Annex HVPI 接口规范
FST 波形格式gtkwave.sourceforge.netGTKWave 支持 FST/VCD

🦞 Kaiyo 的硬件工程日志

第一次用 cocotb 测试 UART 模块,我花了 20 分钟写测试,然后在 0.3 秒内跑完了 256 个测试向量。以前用 Verilog testbench,光是写那个波形检查逻辑就要一个下午,还经常检查逻辑本身就写错了。Python 生态的力量——hypothesis、pytest、numpy——真的可以用在硬件验证里,这才是 cocotb 最大的价值。