00381 芯片验证新姿势:Cocotb 从入门到精通全记录(含行覆盖率全流程)


0. 前言

传统的 Verilog Testbench 编写繁琐,而 Cocotb (Coroutine based cosimulation library) 允许我们使用 Python 进行协程驱动的仿真验证。本文将记录从零搭建环境、基础验证、高级协程特性、功能覆盖率收集,直到使用 Verilator 5.x 导出 HTML 代码行覆盖率报告的全过程。所有的代码均提供完整可运行版。

cocotb docs: https://www.cocotb.org/


1. 环境搭建(避坑指南)

为了避免不同 Python 环境(如 Conda 与系统自带环境)冲突,建议直接在 Conda 环境下操作,并使用系统级的仿真器。

1.1 基础依赖安装

打开终端(确保处于你常用的 Python 虚拟环境中),执行:

# 1. 安装基础仿真器 iverilog 与波形查看器
sudo apt update
sudo apt install iverilog gtkwave

# 2. 安装 Python 核心包与覆盖率扩展包
pip install cocotb cocotb-coverage cocotb-bus

1.2 高级仿真器 Verilator 5.x 安装 (用于行覆盖率)

由于 Ubuntu 默认仓库版本过低(无法支持最新 Cocotb),必须从源码编译安装 **Verilator 5.036+**。

# 安装编译依赖
sudo apt-get install git help2man perl python3 make autoconf g++ flex bison ccache \
libgoogle-perftools-dev numactl perl-doc libfl2 libfl-dev zlib1g zlib1g-dev

# 源码编译安装
git clone https://github.com/verilator/verilator
cd verilator
git checkout v5.038  # 选择稳定高版本
autoconf
./configure
make -j `nproc`
sudo make install

# 验证版本
verilator --version  # 应显示 5.038

2. 被测设计 (DUT)

请创建一个空文件夹存放项目。首先创建被测的 8 位带有使能端的同步计数器 counter.v

// counter.v
`timescale 1ns/1ps

module counter #(
    parameter WIDTH = 8
)(
    input  wire             clk,
    input  wire             rst,
    input  wire             en,
    output reg  [WIDTH-1:0] count
);

    always @(posedge clk) begin
        if (rst) begin
            count <= {WIDTH{1'b0}};
        end else if (en) begin
            count <= count + 1'b1;
        end
    end

    // 用于在仿真器中导出波形文件
    initial begin
        $dumpfile("waveform.vcd");
        $dumpvars(0, counter);
    end

endmodule

3. 基础与进阶验证 (iverilog)

在这个阶段,我们将测试 Cocotb 的时钟生成、协程并发 (start_soon)、组合触发器 (Combine) 和超时机制 (with_timeout)。

3.1 测试脚本 test_counter.py

创建 Python 测试文件:

# test_counter.py
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, FallingEdge, Timer, Combine, with_timeout

# ==============================================================================
# 辅助协程:模拟复位过程
# ==============================================================================
async def reset_dut(dut, duration_ns):
    dut._log.info("开始复位...")
    dut.rst.value = 1
    dut.en.value = 0
    await Timer(duration_ns, units="ns")
    dut.rst.value = 0
    dut._log.info("复位完成!")

# ==============================================================================
# 测试用例 1:基础功能验证
# ==============================================================================
@cocotb.test()
async def test_basic_counting(dut):
    """测试基本的计数器累加功能"""
    # 启动后台时钟 (10ns 周期)
    cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())

    await reset_dut(dut, 25)

    # 驱动使能信号
    dut.en.value = 1
    for i in range(5):
        await RisingEdge(dut.clk)
        
    # 在下降沿检查数据,避免竞争冒险
    await FallingEdge(dut.clk)
    expected_val = 5
    assert int(dut.count.value) == expected_val, f"计数值错误!预期:{expected_val}, 实际:{int(dut.count.value)}"
    dut._log.info(f"基础测试通过!当前计数值: {int(dut.count.value)}")

# ==============================================================================
# 辅助协程:后台监控器
# ==============================================================================
async def count_monitor(dut):
    """持续监控计数值变化的后台任务"""
    prev_val = -1
    while True:
        await RisingEdge(dut.clk)
        await Timer(1, units="ns") 
        current_val = int(dut.count.value) if dut.count.value.is_resolvable else -1
        if current_val != prev_val and current_val != -1:
            dut._log.info(f"[Monitor] 计数器值更新为: {current_val}")
            prev_val = current_val

# ==============================================================================
# 测试用例 2:高级特性验证 (并发、组合、超时)
# ==============================================================================
@cocotb.test()
async def test_advanced_features(dut):
    """测试并发协程、组合触发器以及超时处理"""
    cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
    monitor_task = cocotb.start_soon(count_monitor(dut))

    await reset_dut(dut, 15)
    dut.en.value = 1

    # 1. 组合触发器 (Combine): 等待多个协程同时完成
    async def task_a():
        await Timer(30, units="ns")
        dut._log.info("任务 A 完成")
        
    async def task_b():
        for _ in range(5):
            await RisingEdge(dut.clk)
        dut._log.info("任务 B 完成")

    await Combine(cocotb.start_soon(task_a()), cocotb.start_soon(task_b()))
    
    # 2. 超时机制 (with_timeout): 防止死锁
    dut.en.value = 0 # 故意关闭使能
    try:
        async def wait_for_100():
            while int(dut.count.value) != 100:
                await RisingEdge(dut.clk)
                
        dut._log.info("尝试等待计数器到达 100,带有超时限制...")
        await with_timeout(wait_for_100(), 50, 'ns')
    except cocotb.result.SimTimeoutError:
        dut._log.info("成功捕获到预期的超时异常!")

    monitor_task.kill()

3.2 基础 Makefile

# Makefile
SIM ?= icarus
TOPLEVEL_LANG ?= verilog

VERILOG_SOURCES += $(PWD)/counter.v
TOPLEVEL = counter
MODULE = test_counter

include $(shell cocotb-config --makefiles)/Makefile.sim

运行测试: 在终端直接输入 make 即可看到所有测试通过的日志。


4. 终极实战:功能覆盖率与行覆盖率并进

接下来我们将底层仿真器切换为 Verilator,并在 Python 层用 cocotb-coverage 收集功能覆盖率,在 C++ 仿真层收集代码行覆盖率。

4.1 覆盖率测试脚本 test_counter_cov.py

创建这个包含覆盖率定义的新文件:

# test_counter_cov.py
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
from cocotb_coverage.coverage import CoverPoint, coverage_db

# ==============================================================================
# 1. 定义功能覆盖点 (Functional Coverage)
# ==============================================================================
@CoverPoint(
    "counter.value_bins",
    xf=lambda current_val: current_val,
    bins=[0, 1, 2, 3, 4, 5],
    bins_labels=["val_0", "val_1", "val_2", "val_3", "val_4", "val_5"]
)
def sample_counter(current_val):
    """采样钩子函数"""
    pass

# ==============================================================================
# 2. 带有采样的后台监控协程
# ==============================================================================
async def coverage_monitor(dut):
    prev_val = -1
    while True:
        await RisingEdge(dut.clk)
        await Timer(1, units="ns") 
        if dut.count.value.is_resolvable:
            current_val = int(dut.count.value)
            if current_val != prev_val:
                sample_counter(current_val)
                dut._log.info(f"[Monitor] 采样到计数器值: {current_val}")
                prev_val = current_val

# ==============================================================================
# 3. 测试主体
# ==============================================================================
@cocotb.test()
async def test_coverage_collection(dut):
    """运行计数器并收集双重覆盖率"""
    cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
    cocotb.start_soon(coverage_monitor(dut))

    # 复位
    dut.rst.value = 1
    dut.en.value = 0
    await Timer(25, units="ns")
    dut.rst.value = 0

    # 计数 (足以覆盖 0 到 5)
    dut.en.value = 1
    for _ in range(8):
        await RisingEdge(dut.clk)

    dut.en.value = 0
    await Timer(20, units="ns")

    # 4. 导出功能覆盖率报告
    coverage_db.report_coverage(dut._log.info, bins=True)
    coverage_db.export_to_xml("functional_coverage.xml")
    dut._log.info("功能覆盖率报告已导出至 functional_coverage.xml")

4.2 Verilator 覆盖率 Makefile

修改之前的 Makefile(注意替换内容):

# Makefile
SIM ?= verilator
TOPLEVEL_LANG ?= verilog

VERILOG_SOURCES += $(PWD)/counter.v
TOPLEVEL = counter
MODULE = test_counter_cov

# 【关键】向 Verilator 传递开启代码行/翻转覆盖率的参数,必须放在 include 前
EXTRA_ARGS += --coverage --coverage-line --coverage-toggle

include $(shell cocotb-config --makefiles)/Makefile.sim

4.3 运行并生成 HTML 报告

在终端执行以下流程:

# 1. 强制清理旧的编译缓存,并运行测试
make clean
make

# 2. 安装可视化转换工具 (如未安装)
sudo apt install lcov

# 3. 将 Verilator 二进制覆盖率文件转为 LCOV 格式
# (Verilator 5.x 必须使用 --write-info)
verilator_coverage --write-info coverage.info coverage.dat

# 4. 生成 HTML 网页报告
genhtml coverage.info --output-directory cov_html

5. 在 VS Code 中高效查看结果

  • 看波形:终端输入 gtkwave waveform.vcd 即可。
  • 看功能覆盖率:直接在终端日志中查看打印输出,或者打开生成的 functional_coverage.xml
  • **看代码行覆盖率 (极度推荐)**:
    1. 在 VS Code 扩展市场搜索并安装官方插件 Live Preview (Microsoft 出品)。
    2. 在左侧文件树中找到 cov_html/index.html
    3. 右键点击该文件,选择 **”Show Preview”**。
    4. VS Code 右侧会直接打开网页,点击进入 counter.v蓝色代码行表示已被执行,红色表示存在覆盖漏洞。

全流程测试无死角,告别环境玄学,助你快速上手现代敏捷芯片验证!

结语

第三百八十一篇博文写完,开心!!!!

今天,也是充满希望的一天。


文章作者: LuYF-Lemon-love
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LuYF-Lemon-love !
  目录