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。 - **看代码行覆盖率 (极度推荐)**:
- 在 VS Code 扩展市场搜索并安装官方插件 Live Preview (Microsoft 出品)。
- 在左侧文件树中找到
cov_html/index.html。 - 右键点击该文件,选择 **”Show Preview”**。
- VS Code 右侧会直接打开网页,点击进入
counter.v,蓝色代码行表示已被执行,红色表示存在覆盖漏洞。
全流程测试无死角,告别环境玄学,助你快速上手现代敏捷芯片验证!
结语
第三百八十一篇博文写完,开心!!!!
今天,也是充满希望的一天。