本教程从零开始,面向初学者,带你理解 DMA、AXI 总线、数字验证 的基本概念,并一步步搭建基于 PyUVM、cocotbext-axi VIP 和 Verilator 的仿真环境,实现一个简易的 AXI-Lite DMA IP,编写完整的 Python Testbench 进行功能验证与覆盖率收集。
目录
- 什么是 DMA?
- 什么是 AXI?
- 什么是数字验证?
- 项目概述
- 环境 Setup
- RTL 源码详解
- Makefile
- PyUVM Testbench 详解
- 运行仿真
- PyUVM 常见踩坑与教训
- 扩展建议
- 总结
1. 什么是 DMA?
DMA(Direct Memory Access,直接存储器访问) 是一种让外设不经过 CPU 直接读写内存的技术。
1.1 为什么需要 DMA?
想象一下,你要把硬盘上的 1GB 文件拷贝到内存中:
- 没有 DMA:CPU 必须一个字节一个字节地从硬盘读,再写入内存。这期间 CPU 被完全占用,无法做其他事情。
- 有了 DMA:CPU 只需要告诉 DMA 控制器”源地址、目标地址、长度”,DMA 就会自动完成拷贝。CPU 可以去执行其他任务,等 DMA 完成后通过中断通知 CPU。
flowchart LR
subgraph 无DMA["❌ 无 DMA"]
CPU1["CPU"] -->|逐字节搬运| MEM1["内存"]
CPU1 -->|占用100%| DISK1["硬盘"]
end
subgraph 有DMA["✅ 有 DMA"]
CPU2["CPU"] -->|配置参数| DMA["DMA 控制器"]
DMA -->|自动搬运| MEM2["内存"]
DMA -->|自动搬运| DISK2["硬盘"]
CPU2 -->|空闲做其他事| OTHER["其他任务"]
end
1.2 DMA 的基本工作流程
sequenceDiagram
participant CPU as CPU
participant DMA as DMA 控制器
participant SRC as 源设备/内存
participant DST as 目标设备/内存
CPU->>DMA: 1. 配置 src_addr
CPU->>DMA: 2. 配置 dst_addr
CPU->>DMA: 3. 配置 length
CPU->>DMA: 4. 启动 start
Note over CPU: CPU 去干别的了
loop 每次搬运 4 字节
DMA->>SRC: 5. 发送读请求
SRC-->>DMA: 6. 返回数据
DMA->>DST: 7. 发送写请求
DST-->>DMA: 8. 返回写完成
end
DMA-->>CPU: 9. done = 1 (完成)
1.3 本教程的 DMA 特点
本教程实现的是一个简化版 AXI-Lite DMA:
- 只支持 Memory-to-Memory(内存到内存)传输
- 通过 AXI-Lite 总线协议访问内存
- 每次传输固定 4 字节(32位数据宽度)
- 支持总线错误检测(非 OKAY 响应时报告 error)
2. 什么是 AXI?
AXI(Advanced eXtensible Interface) 是 ARM 公司设计的一种高性能片上总线协议,广泛应用于 SoC(System on Chip)设计中。
2.1 AXI 家族成员
| 协议 | 特点 | 适用场景 |
|---|---|---|
| AXI4-Full | 支持突发传输(Burst)、乱序完成 | 高性能数据传输(如 DDR 控制器) |
| AXI4-Lite | 简单、每次传输 1 个数据 | 寄存器配置、低速外设 |
| AXI4-Stream | 无地址,只有数据流 | 音视频流、流水线数据处理 |
本教程使用 AXI4-Lite,因为它足够简单,适合初学者理解。
2.2 AXI-Lite 写通道信号
AXI-Lite 写操作使用三个独立通道:
| 通道 | 信号 | 方向(对 Master) | 说明 |
|---|---|---|---|
| 写地址 | awaddr |
Output | 要写入的内存地址 |
| 写地址 | awvalid |
Output | Master 地址有效 |
| 写地址 | awready |
Input | Slave 准备好接收地址 |
| 写数据 | wdata |
Output | 要写入的数据 |
| 写数据 | wstrb |
Output | 字节使能(本例固定 0xF) |
| 写数据 | wvalid |
Output | Master 数据有效 |
| 写数据 | wready |
Input | Slave 准备好接收数据 |
| 写响应 | bresp |
Input | 响应状态(00=OKAY) |
| 写响应 | bvalid |
Input | Slave 响应有效 |
| 写响应 | bready |
Output | Master 准备好接收响应 |
2.3 AXI-Lite 读通道信号
| 通道 | 信号 | 方向(对 Master) | 说明 |
|---|---|---|---|
| 读地址 | araddr |
Output | 要读取的内存地址 |
| 读地址 | arvalid |
Output | Master 地址有效 |
| 读地址 | arready |
Input | Slave 准备好接收地址 |
| 读数据 | rdata |
Input | 返回的数据 |
| 读数据 | rresp |
Input | 响应状态(00=OKAY) |
| 读数据 | rvalid |
Input | Slave 数据有效 |
| 读数据 | rready |
Output | Master 准备好接收数据 |
2.4 AXI-Lite 写时序(关键握手)
AXI 的核心机制是 VALID/READY 握手:当 valid 和 ready 同时为高时,数据传输发生。
sequenceDiagram
participant M as DMA (Master)
participant S as AXI-Lite Slave (内存)
Note over M,S: 写地址通道握手
M->>S: awaddr = 0x100, awvalid = 1
S-->>M: awready = 1
Note over M,S: 时钟上升沿:地址传输完成
Note over M,S: 写数据通道握手
M->>S: wdata = 0x11223344, wvalid = 1
S-->>M: wready = 1
Note over M,S: 时钟上升沿:数据传输完成
Note over M,S: 写响应通道握手
S-->>M: bresp = 00 (OKAY), bvalid = 1
M->>S: bready = 1
Note over M,S: 时钟上升沿:响应传输完成
2.5 AXI-Lite 读时序
sequenceDiagram
participant M as DMA (Master)
participant S as AXI-Lite Slave (内存)
Note over M,S: 读地址通道握手
M->>S: araddr = 0x100, arvalid = 1
S-->>M: arready = 1
Note over M,S: 时钟上升沿:地址传输完成
Note over M,S: 读数据通道握手
S-->>M: rdata = 0xAABBCCDD, rresp = 00, rvalid = 1
M->>S: rready = 1
Note over M,S: 时钟上升沿:数据传输完成
2.6 响应编码
| rresp/bresp 值 | 含义 | 说明 |
|---|---|---|
00 |
OKAY | 正常完成 |
01 |
EXOKAY | 独占访问成功(Lite 不支持) |
10 |
SLVERR | Slave 错误 |
11 |
DECERR | 解码错误(地址不存在) |
3. 什么是数字验证?
3.1 为什么需要验证?
数字电路设计(RTL)完成后,不能直接把代码烧到芯片里。因为:
- 流片成本极高:一次芯片制造(流片)可能需要数百万美元,且耗时数月。
- bug 修复困难:芯片制造完成后发现 bug,只能重新设计、重新流片。
- 逻辑错误难以肉眼发现:复杂的状态机、时序交互问题,单纯看代码很难发现。
因此,我们需要在流片前,通过仿真来穷尽各种场景,确保设计功能正确。
3.2 验证 = 找 bug
验证工程师的核心目标是:证明设计有 bug,或者在有限时间内找不到 bug。
flowchart TD
A[RTL 设计代码] --> B[Testbench 测试平台]
B --> C{仿真运行}
C -->|发现输出不匹配| D[发现 BUG]
C -->|所有测试通过| E[暂时未发现 BUG]
D --> A
E --> F[流片 / 上板]
3.3 传统验证 vs Cocotb vs PyUVM
| 方式 | 语言 | 优点 | 缺点 |
|---|---|---|---|
| 传统 SystemVerilog | SystemVerilog + UVM | 行业标准,功能强大 | 学习曲线陡峭,代码冗长 |
| Cocotb | Python | 简洁易读,生态丰富,适合快速验证 | 缺乏标准验证架构,代码难以复用 |
| PyUVM | Python | 基于 IEEE 1800.2 UVM 标准,提供 Sequence/Driver/Sequencer/Env 等经典架构 | 基于 cocotb,需额外了解 UVM 概念 |
3.4 什么是 PyUVM?
PyUVM 是 UVM(Universal Verification Methodology)的 Python 实现,基于 IEEE 1800.2 标准。它构建在 cocotb 之上,提供了一套标准化的验证组件和流程:
- uvm_component:所有验证组件的基类,支持层次化构建(build_phase、connect_phase、run_phase 等)。
- uvm_sequence_item:事务(Transaction),描述一次总线传输的参数。
- uvm_sequence:序列,生成一系列事务并交给 Sequencer。
- uvm_sequencer:仲裁器,连接 Sequence 和 Driver。
- uvm_driver:驱动器,从 Sequencer 获取事务,驱动到 DUT 接口。
- uvm_env:环境,包含 Sequencer、Driver、Monitor 等组件。
- uvm_test:测试,顶层容器,负责配置环境、启动 Sequence。
- ConfigDB:配置数据库,用于在不同组件之间传递 DUT 句柄等共享资源。
- Objection 机制:通过
raise_objection()/drop_objection()控制run_phase的生命周期。
3.5 本教程的验证架构
flowchart TB
subgraph PYUVM_TB["Testbench Python + PyUVM"]
direction TB
TEST["cocotb.test entry"]
UVM["uvm_test DmaBaseTest"]
ENV["uvm_env DmaEnv"]
SEQR["uvm_sequencer"]
DRV["uvm_driver DmaDriver"]
SEQ1["Sequence BasicTransferSeq"]
SEQ2["Sequence ReadErrorSeq"]
VIP1["cocotbext-axi AxiLiteRam"]
VIP2["manual Slave coroutine"]
end
subgraph SIM["Simulator Verilator"]
DUT["DUT unique_simple_axi_dma"]
end
TEST --> UVM
UVM --> ENV
ENV --> SEQR
ENV --> DRV
SEQR <--> DRV
SEQ1 --> SEQR
SEQ2 --> SEQR
DRV --> DUT
DUT <--> VIP1
DUT <--> VIP2
3.6 什么是覆盖率?
覆盖率(Coverage)是衡量验证完备性的核心指标。它回答了一个关键问题:
“我们的测试用例到底跑过了设计中的多少代码?”
行覆盖率(Line Coverage)
本教程使用的是最直观的覆盖率指标——行覆盖率:
- 定义:仿真过程中,RTL 源代码里每一行被执行的次数。
- 意义:如果某一行代码从未被执行(
%000000),说明当前测试集没有覆盖到这个场景,对应的功能可能存在未被发现的 bug。 - 目标:通常追求尽可能高的行覆盖率(如 >95%),但 100% 行覆盖率不等于没有 bug——它只说明代码被”跑到过”,不代表所有边界条件都被验证。
举个例子:
if (m_axi_rresp != 2'b00) begin
error <= 1'b1; // 这行只有总线返回错误时才会执行
end
如果测试用例从不注入错误响应,那么 error <= 1'b1 这行就永远是 %000000,意味着错误处理逻辑从未被验证——而这恰恰是 DMA 控制器鲁棒性的关键部分。
本教程通过 --coverage 参数让 Verilator 自动插桩,仿真结束后生成带执行次数标注的源码报告(详见 9.2 生成覆盖率报告)。
4. 项目概述
我们要实现的是一个 Memory-to-Memory 的 AXI-Lite DMA 控制器:
- 用户配置
src_addr(源地址)、dst_addr(目标地址)和length(传输字节数,需为 4 的倍数)。 - 拉高
start信号启动传输。 - DMA 通过 AXI-Lite 总线从源地址读取数据,再写入目标地址。
- 传输完成后拉高
done信号。 - 若总线返回非 OKAY 的
rresp或bresp,则拉高error信号并停止。
验证方面,我们使用:
- PyUVM 提供标准 UVM 验证架构(Sequence/Driver/Sequencer/Env/Test)。
- cocotbext-axi 提供的
AxiLiteRam作为 AXI-Lite Slave VIP,模拟内存。 - 手动编写的 AXI Slave 协程,用于注入错误响应(SLVERR / DECERR)。
- Verilator 作为仿真器,并开启
--coverage收集行覆盖率。
4.1 项目目录结构
axi_dma_pyuvm/
├── unique_simple_axi_dma.v # RTL 源码
├── test_dma_pyuvm.py # PyUVM Testbench
├── Makefile # Cocotb 编译规则
└── tutorial.md # 本教程
5. 环境 Setup
5.1 系统要求
- Linux (Ubuntu / WSL 均可)
- Python 3.10+
- GCC / G++ (用于编译 Verilator 生成的 C++ 代码)
- git
5.2 安装 Verilator
# 安装依赖
sudo apt update
sudo apt install -y git help2man perl python3 make autoconf g++ flex bison libfl2 libfl-dev zlib1g-dev
# 从源码编译安装 Verilator
git clone https://github.com/verilator/verilator
cd verilator
autoconf
./configure --prefix=/usr/local
make -j$(nproc)
sudo make install
# 验证安装
verilator --version
注意:确保安装的是 **Verilator 5.0+**,因为 Cocotb 2.x 需要较新的 VPI 支持。本教程使用
Verilator 5.038。
5.3 安装 lcov(可选,用于生成 HTML 覆盖率报告)
sudo apt install -y lcov
lcov 是 Linux 下的覆盖率可视化工具,能将 Verilator 生成的 .dat 覆盖率数据转换成美观的 HTML 网页报告,方便在浏览器中查看。
5.4 安装 Python 依赖
推荐使用 Conda 或 venv 创建虚拟环境:
# 创建虚拟环境(示例使用 miniconda)
conda create -n cocotb_env python=3.12 -y
conda activate cocotb_env
# 安装核心包
pip install cocotb cocotbext-axi pyuvm
# 验证安装
python -c "import cocotb; print(cocotb.__version__)"
python -c "from cocotbext.axi import AxiLiteBus, AxiLiteRam; print('cocotbext-axi OK')"
python -c "import pyuvm; print(pyuvm.__version__)"
pyuvm是 UVM 标准的 Python 实现,本教程使用版本4.0.1;cocotbext-axi是 Alex Forencich 开发的 AXI VIP 库,本教程使用版本0.1.28。
6. RTL 源码详解
下面是完整的 DMA Verilog 代码。它实现了基于状态机的 AXI-Lite Master 读写流程,并支持错误检测。
创建文件 unique_simple_axi_dma.v:
`timescale 1ns / 1ps
module unique_simple_axi_dma (
input wire clk,
input wire rst_n,
// 控制接口
input wire start,
input wire [31:0] src_addr,
input wire [31:0] dst_addr,
input wire [31:0] length, // 字节数,需为 4 的倍数
output reg done,
output reg error, // 传输过程中收到非 OKAY 响应
// AXI-Lite 主机接口
output reg [31:0] m_axi_awaddr,
output reg m_axi_awvalid,
input wire m_axi_awready,
output reg [31:0] m_axi_wdata,
output wire [3:0] m_axi_wstrb,
output reg m_axi_wvalid,
input wire m_axi_wready,
input wire [1:0] m_axi_bresp,
input wire m_axi_bvalid,
output reg m_axi_bready,
output reg [31:0] m_axi_araddr,
output reg m_axi_arvalid,
input wire m_axi_arready,
input wire [31:0] m_axi_rdata,
input wire [1:0] m_axi_rresp,
input wire m_axi_rvalid,
output reg m_axi_rready
);
assign m_axi_wstrb = 4'b1111;
localparam IDLE = 3'd0;
localparam READ_ADDR = 3'd1;
localparam READ_DATA = 3'd2;
localparam WRITE_ADDR = 3'd3;
localparam WRITE_DATA = 3'd4;
localparam WRITE_RESP = 3'd5;
localparam ERROR = 3'd6;
reg [2:0] state;
reg [31:0] current_src;
reg [31:0] current_dst;
reg [31:0] bytes_left;
reg [31:0] data_buffer;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
done <= 1'b0;
error <= 1'b0;
m_axi_awvalid <= 1'b0;
m_axi_wvalid <= 1'b0;
m_axi_bready <= 1'b0;
m_axi_arvalid <= 1'b0;
m_axi_rready <= 1'b0;
end else begin
case (state)
IDLE: begin
done <= 1'b0;
error <= 1'b0;
if (start) begin
current_src <= src_addr;
current_dst <= dst_addr;
bytes_left <= length;
state <= (length > 0) ? READ_ADDR : IDLE;
end
end
READ_ADDR: begin
m_axi_araddr <= current_src;
m_axi_arvalid <= 1'b1;
if (m_axi_arvalid && m_axi_arready) begin
m_axi_arvalid <= 1'b0;
m_axi_rready <= 1'b1;
state <= READ_DATA;
end
end
READ_DATA: begin
if (m_axi_rvalid && m_axi_rready) begin
if (m_axi_rresp != 2'b00) begin
error <= 1'b1;
done <= 1'b1;
state <= IDLE;
end else begin
data_buffer <= m_axi_rdata;
m_axi_rready <= 1'b0;
state <= WRITE_ADDR;
end
end
end
WRITE_ADDR: begin
m_axi_awaddr <= current_dst;
m_axi_awvalid <= 1'b1;
if (m_axi_awvalid && m_axi_awready) begin
m_axi_awvalid <= 1'b0;
state <= WRITE_DATA;
end
end
WRITE_DATA: begin
m_axi_wdata <= data_buffer;
m_axi_wvalid <= 1'b1;
if (m_axi_wvalid && m_axi_wready) begin
m_axi_wvalid <= 1'b0;
m_axi_bready <= 1'b1;
state <= WRITE_RESP;
end
end
WRITE_RESP: begin
if (m_axi_bvalid && m_axi_bready) begin
m_axi_bready <= 1'b0;
if (m_axi_bresp != 2'b00) begin
error <= 1'b1;
done <= 1'b1;
state <= IDLE;
end else begin
bytes_left <= bytes_left - 4;
current_src <= current_src + 4;
current_dst <= current_dst + 4;
if (bytes_left <= 4) begin
done <= 1'b1;
state <= IDLE;
end else begin
state <= READ_ADDR;
end
end
end
end
default: state <= IDLE;
endcase
end
end
endmodule
6.1 模块接口解析
graph TB
subgraph DMA["unique_simple_axi_dma"]
CTRL["控制接口"]
AXI["AXI-Lite Master 接口"]
end
subgraph EXTERNAL["外部"]
CPU["CPU / Testbench"]
MEM["AXI-Lite Slave<br/>(内存)"]
end
CPU -->|start, src_addr,<br/>dst_addr, length| CTRL
CTRL -->|done, error| CPU
AXI -->|awaddr, awvalid...| MEM
MEM -->|awready, bresp...| AXI
| 信号 | 方向(对本模块) | 说明 |
|---|---|---|
clk |
Input | 系统时钟,上升沿触发 |
rst_n |
Input | 低电平有效的异步复位 |
start |
Input | 单次脉冲启动 DMA 传输 |
src_addr |
Input | 源内存地址(4字节对齐) |
dst_addr |
Input | 目标内存地址(4字节对齐) |
length |
Input | 要传输的字节数,必须是 4 的倍数 |
done |
Output | 传输完成标志,高电平有效 |
error |
Output | 传输中收到 AXI 错误响应 |
6.2 AXI-Lite 接口信号
所有 m_axi_* 信号构成一个完整的 AXI-Lite Master 接口。Master 主动发起读写请求,Slave(本例中是内存模型)被动响应。
aw*信号:写地址通道(Address Write)w*信号:写数据通道(Write Data)b*信号:写响应通道(Write Response)ar*信号:读地址通道(Address Read)r*信号:读数据通道(Read Data)
6.3 内部寄存器
| 寄存器 | 位宽 | 用途 |
|---|---|---|
state |
3 bit | 状态机当前状态 |
current_src |
32 bit | 当前读地址(会随着传输递增) |
current_dst |
32 bit | 当前写地址(会随着传输递增) |
bytes_left |
32 bit | 剩余待传输字节数 |
data_buffer |
32 bit | 从总线读到的数据暂存 |
6.4 状态机详解
stateDiagram-v2
[*] --> IDLE : 复位 rst_n=0
IDLE --> READ_ADDR : start=1 && length>0
IDLE --> IDLE : start=1 && length=0
READ_ADDR --> READ_DATA : arvalid && arready
READ_DATA --> WRITE_ADDR : rvalid && rready && rresp==OKAY
READ_DATA --> IDLE : rvalid && rready && rresp!=OKAY<br/>[error=1, done=1]
WRITE_ADDR --> WRITE_DATA : awvalid && awready
WRITE_DATA --> WRITE_RESP : wvalid && wready
WRITE_RESP --> READ_ADDR : bvalid && bready && bresp==OKAY<br/>&& bytes_left > 4
WRITE_RESP --> IDLE : bvalid && bready && bresp==OKAY<br/>&& bytes_left <= 4<br/>[done=1]
WRITE_RESP --> IDLE : bvalid && bready && bresp!=OKAY<br/>[error=1, done=1]
ERROR --> IDLE : [通过 error 分支]
状态流转详解:
- IDLE:空闲状态。收到
start且length > 0时,保存配置参数,进入READ_ADDR。 - READ_ADDR:发送读地址(
araddr = current_src,arvalid = 1)。当 Slave 返回arready = 1时,握手成功,进入READ_DATA。 - READ_DATA:等待 Slave 返回数据(
rvalid = 1)。收到数据后检查rresp:- 若为 OKAY(00):保存数据到
data_buffer,进入WRITE_ADDR。 - 若为错误:置位
error和done,回到IDLE。
- 若为 OKAY(00):保存数据到
- WRITE_ADDR:发送写地址(
awaddr = current_dst,awvalid = 1)。握手成功后进入WRITE_DATA。 - WRITE_DATA:发送写数据(
wdata = data_buffer,wvalid = 1)。握手成功后进入WRITE_RESP。 - WRITE_RESP:等待 Slave 返回写响应(
bvalid = 1)。检查bresp:- 若为 OKAY:更新地址和剩余字节数。若
bytes_left <= 4则传输完成(done = 1),否则回到READ_ADDR继续下一轮。 - 若为错误:置位
error和done,回到IDLE。
- 若为 OKAY:更新地址和剩余字节数。若
6.5 代码设计要点
- 复位处理:所有控制信号(
valid、ready)都清零,防止复位后出现虚假的总线请求。 - 握手逻辑:
if (valid && ready)是 AXI 协议的标准写法,表示一次成功的传输。 - 字节对齐:由于数据宽度是 32 位(4 字节),每次传输固定 4 字节,因此地址每次递增 4。
- 错误处理:一旦收到非 OKAY 响应,立即停止当前传输,报告错误,不再继续。
7. Makefile
Cocotb 使用 Makefile 驱动仿真流程。下面是针对 Verilator 的完整 Makefile。
创建文件 Makefile:
# 仿真器设置
SIM ?= verilator
TOPLEVEL_LANG ?= verilog
PWD=$(shell pwd)
# RTL 源代码文件
VERILOG_SOURCES += $(PWD)/unique_simple_axi_dma.v
# 顶层模块与 Testbench 名称
TOPLEVEL = unique_simple_axi_dma
MODULE = test_dma_pyuvm
# 开启 Verilator 覆盖率和波形生成
EXTRA_ARGS += --coverage --trace
# 引入 Cocotb 默认规则
include $(shell cocotb-config --makefiles)/Makefile.sim
# 自定义目标:生成带有源码标注的覆盖率报告
coverage_report:
mkdir -p logs/annotated
verilator_coverage --annotate logs/annotated coverage.dat
# 自定义目标:使用 lcov 生成 HTML 覆盖率报告
lcov_report: coverage_report
verilator_coverage --write-info coverage.info coverage.dat
genhtml coverage.info --output-directory logs/html
7.1 关键配置说明
| 变量 | 说明 |
|---|---|
SIM ?= verilator |
指定仿真器为 Verilator |
TOPLEVEL |
DUT 顶层模块名,必须和 Verilog 的 module 名一致 |
MODULE |
Python Testbench 文件名(不含 .py) |
EXTRA_ARGS |
传给 Verilator 的额外参数,--coverage 开启覆盖率,--trace 生成 VCD 波形 |
coverage_report |
自定义目标,调用 verilator_coverage 生成源码标注报告 |
8. PyUVM Testbench 详解
下面是完整的 PyUVM Testbench。它包含 8 个测试用例,覆盖了正常传输、大长度传输、零长度、多次传输、复位、非法状态恢复,以及读写错误响应注入。
创建文件 test_dma_pyuvm.py:
"""
基于 PyUVM 的 AXI DMA 验证平台
使用 PyUVM (Python UVM) 框架对 unique_simple_axi_dma 进行功能验证。
PyUVM 基于 IEEE 1800.2 UVM 标准,提供 sequence/driver/sequencer/env/test 等经典 UVM 组件。
依赖:
pip install pyuvm cocotbext-axi
"""
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer, ClockCycles
from cocotbext.axi import AxiLiteBus, AxiLiteRam
from pyuvm import *
# ============================================================
# 1. Sequence Items(事务)
# ============================================================
class DmaTransaction(uvm_sequence_item):
"""
DMA 控制事务:描述一次 DMA 传输的参数与期望结果
"""
def __init__(self, name,
src_addr=0,
dst_addr=0,
length=0,
test_data=b'',
expect_error=False,
use_manual_slave=False):
super().__init__(name)
self.src_addr = src_addr
self.dst_addr = dst_addr
self.length = length
self.test_data = test_data
self.expect_error = expect_error
self.use_manual_slave = use_manual_slave
def __str__(self):
return (f"DmaTransaction(src=0x{self.src_addr:08X}, "
f"dst=0x{self.dst_addr:08X}, len={self.length})")
class DmaResult(uvm_sequence_item):
"""
DMA 传输结果:由 Driver 返回给 Sequence
"""
def __init__(self, name, success=True, error=0, read_back_data=b''):
super().__init__(name)
self.success = success
self.error = error
self.read_back_data = read_back_data
# ============================================================
# 2. Driver
# ============================================================
class DmaDriver(uvm_driver):
"""
DMA 控制接口 Driver
- 从 Sequencer 获取 DmaTransaction
- 驱动 DUT 控制信号 (src_addr, dst_addr, length, start)
- 通过 AxiLiteRam 写入/读出测试数据(正常模式)
- 或配合手动 Slave 协程完成错误注入测试
"""
def build_phase(self):
# 从 ConfigDB 获取 DUT 和可选的 AXI RAM
self.dut = ConfigDB().get(self, "", "DUT")
try:
self.axi_ram = ConfigDB().get(self, "", "AXI_RAM")
except UVMConfigItemNotFound:
self.axi_ram = None
async def run_phase(self):
"""
Driver 主循环:不断从 Sequencer 获取事务并执行
"""
while True:
item = await self.seq_item_port.get_next_item()
result = await self.execute_transfer(item)
# 必须将 response 的 transaction_id 与 request 对齐
result.transaction_id = item.transaction_id
self.seq_item_port.item_done(result)
async def execute_transfer(self, item: DmaTransaction) -> DmaResult:
"""
执行一次 DMA 传输:驱动控制信号、等待完成、校验结果
"""
dut = self.dut
# 正常模式下,预先将测试数据写入源地址
if self.axi_ram is not None and not item.use_manual_slave and item.length > 0:
self.axi_ram.write(item.src_addr, item.test_data)
# 驱动 DMA 控制接口
dut.src_addr.value = item.src_addr
dut.dst_addr.value = item.dst_addr
dut.length.value = item.length
dut.start.value = 1
await RisingEdge(dut.clk)
dut.start.value = 0
# 零长度特殊情况:DMA 不应启动,直接采样状态
if item.length == 0:
await ClockCycles(dut.clk, 2)
success = (dut.done.value == 0 and dut.state.value == 0)
return DmaResult("rsp", success=success, error=int(dut.error.value))
# 等待传输完成(带超时保护)
done = await self.wait_done(timeout=2000)
# 结果校验
if self.axi_ram is not None and not item.use_manual_slave:
read_back = self.axi_ram.read(item.dst_addr, item.length)
success = (done and dut.error.value == 0 and read_back == item.test_data)
return DmaResult("rsp", success=success, error=int(dut.error.value),
read_back_data=read_back)
else:
# 手动 Slave 模式(错误注入):只需检查 done 和 error 是否符合预期
success = (done and dut.error.value == int(item.expect_error))
return DmaResult("rsp", success=success, error=int(dut.error.value))
async def wait_done(self, timeout=1000):
"""等待 DMA 完成,带超时保护"""
for _ in range(timeout):
if self.dut.done.value == 1:
return True
await RisingEdge(self.dut.clk)
return False
# ============================================================
# 3. Environment
# ============================================================
class DmaEnv(uvm_env):
"""
DMA 验证环境:包含 Sequencer 和 Driver
"""
def build_phase(self):
self.seqr = uvm_sequencer("seqr", self)
self.driver = DmaDriver("driver", self)
def connect_phase(self):
# 将 Driver 的 seq_item_port 连接到 Sequencer 的 export
self.driver.seq_item_port.connect(self.seqr.seq_item_export)
# ============================================================
# 4. Base Test
# ============================================================
class DmaBaseTest(uvm_test):
"""
所有 DMA 测试的基类,提供公共的 env 实例化和 DUT 复位
"""
def build_phase(self):
self.env = DmaEnv("env", self)
async def run_phase(self):
self.raise_objection()
try:
await self.run_test_body()
finally:
self.drop_objection()
async def run_test_body(self):
"""子类重写此方法来定义具体测试流程"""
raise NotImplementedError("Subclasses must implement run_test_body()")
async def reset_dut(self):
"""复位 DUT"""
dut = ConfigDB().get(None, "", "DUT")
dut.rst_n.value = 0
dut.start.value = 0
dut.src_addr.value = 0
dut.dst_addr.value = 0
dut.length.value = 0
await Timer(50, unit="ns")
dut.rst_n.value = 1
await RisingEdge(dut.clk)
# ============================================================
# 5. Sequences & Tests
# ============================================================
# ---------- 5.1 基本传输 ----------
class BasicTransferSeq(uvm_sequence):
async def body(self):
item = DmaTransaction("item",
src_addr=0x100, dst_addr=0x200, length=8,
test_data=b'\x11\x22\x33\x44\xAA\xBB\xCC\xDD')
await self.start_item(item)
await self.finish_item(item)
rsp = await self.get_response()
assert rsp.success, f"基本传输失败: error={rsp.error}"
class DmaBasicTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
seq = BasicTransferSeq("seq")
await seq.start(self.env.seqr)
# ---------- 5.2 大长度传输 ----------
class LargeTransferSeq(uvm_sequence):
async def body(self):
length = 32
data = bytes([i % 256 for i in range(length)])
item = DmaTransaction("item",
src_addr=0x100, dst_addr=0x400, length=length,
test_data=data)
await self.start_item(item)
await self.finish_item(item)
rsp = await self.get_response()
assert rsp.success, f"大长度传输失败: error={rsp.error}"
class DmaLargeTransferTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
seq = LargeTransferSeq("seq")
await seq.start(self.env.seqr)
# ---------- 5.3 零长度传输 ----------
class ZeroLengthSeq(uvm_sequence):
async def body(self):
item = DmaTransaction("item",
src_addr=0x100, dst_addr=0x200, length=0,
test_data=b'')
await self.start_item(item)
await self.finish_item(item)
rsp = await self.get_response()
assert rsp.success, "零长度传输测试失败"
class DmaZeroLengthTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
seq = ZeroLengthSeq("seq")
await seq.start(self.env.seqr)
# ---------- 5.4 连续多次传输 ----------
class MultipleTransfersSeq(uvm_sequence):
async def body(self):
for i in range(3):
src = 0x100 + i * 0x40
dst = 0x400 + i * 0x40
data = bytes([0xAA + i, 0xBB + i, 0xCC + i, 0xDD + i])
item = DmaTransaction("item",
src_addr=src, dst_addr=dst, length=4,
test_data=data)
await self.start_item(item)
await self.finish_item(item)
rsp = await self.get_response()
assert rsp.success, f"第 {i+1} 次传输失败"
class DmaMultipleTransfersTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
seq = MultipleTransfersSeq("seq")
await seq.start(self.env.seqr)
# ---------- 5.5 复位状态检查 ----------
class ResetCheckSeq(uvm_sequence):
async def body(self):
dut = ConfigDB().get(None, "", "DUT")
assert dut.state.value == 0, "复位后状态应为 IDLE"
assert dut.done.value == 0, "复位后 done 应为 0"
assert dut.error.value == 0, "复位后 error 应为 0"
class DmaResetTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
seq = ResetCheckSeq("seq")
await seq.start(self.env.seqr)
# ---------- 5.6 非法状态恢复 ----------
class IllegalStateSeq(uvm_sequence):
async def body(self):
dut = ConfigDB().get(None, "", "DUT")
# 强制注入非法状态 7
dut.state.value = 7
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
assert dut.state.value == 0, "非法状态应在 default 分支回到 IDLE"
class DmaIllegalStateTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
seq = IllegalStateSeq("seq")
await seq.start(self.env.seqr)
# ---------- 5.7 读错误响应注入 ----------
class ReadErrorSeq(uvm_sequence):
async def body(self):
item = DmaTransaction("item",
src_addr=0x100, dst_addr=0x200, length=4,
expect_error=True, use_manual_slave=True)
await self.start_item(item)
await self.finish_item(item)
rsp = await self.get_response()
assert rsp.error == 1, "收到 SLVERR 应置位 error"
class DmaReadErrorTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
dut = ConfigDB().get(None, "", "DUT")
cocotb.start_soon(axi_lite_slave_read_error(dut, rresp_err=2))
seq = ReadErrorSeq("seq")
await seq.start(self.env.seqr)
# ---------- 5.8 写错误响应注入 ----------
class WriteErrorSeq(uvm_sequence):
async def body(self):
item = DmaTransaction("item",
src_addr=0x100, dst_addr=0x200, length=4,
expect_error=True, use_manual_slave=True)
await self.start_item(item)
await self.finish_item(item)
rsp = await self.get_response()
assert rsp.error == 1, "收到 DECERR 应置位 error"
class DmaWriteErrorTest(DmaBaseTest):
async def run_test_body(self):
await self.reset_dut()
dut = ConfigDB().get(None, "", "DUT")
cocotb.start_soon(axi_lite_slave_read_ok(dut))
cocotb.start_soon(axi_lite_slave_write_error(dut, bresp_err=3))
seq = WriteErrorSeq("seq")
await seq.start(self.env.seqr)
# ============================================================
# 6. 手动 AXI-Lite Slave 协程(用于错误注入)
# ============================================================
async def axi_lite_slave_read_error(dut, rresp_err=2):
"""手动驱动 AXI-Lite 读通道返回错误响应 (SLVERR)"""
while dut.m_axi_arvalid.value != 1:
await RisingEdge(dut.clk)
dut.m_axi_arready.value = 1
await RisingEdge(dut.clk)
dut.m_axi_arready.value = 0
dut.m_axi_rdata.value = 0xDEADBEEF
dut.m_axi_rresp.value = rresp_err
dut.m_axi_rvalid.value = 1
while dut.m_axi_rready.value != 1:
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
dut.m_axi_rvalid.value = 0
async def axi_lite_slave_read_ok(dut):
"""手动驱动 AXI-Lite 读通道返回正常响应"""
while dut.m_axi_arvalid.value != 1:
await RisingEdge(dut.clk)
dut.m_axi_arready.value = 1
await RisingEdge(dut.clk)
dut.m_axi_arready.value = 0
dut.m_axi_rdata.value = 0xCAFEBABE
dut.m_axi_rresp.value = 0
dut.m_axi_rvalid.value = 1
while dut.m_axi_rready.value != 1:
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
dut.m_axi_rvalid.value = 0
async def axi_lite_slave_write_error(dut, bresp_err=3):
"""手动驱动 AXI-Lite 写通道返回错误响应 (DECERR)"""
while dut.m_axi_awvalid.value != 1:
await RisingEdge(dut.clk)
dut.m_axi_awready.value = 1
await RisingEdge(dut.clk)
dut.m_axi_awready.value = 0
while dut.m_axi_wvalid.value != 1:
await RisingEdge(dut.clk)
dut.m_axi_wready.value = 1
await RisingEdge(dut.clk)
dut.m_axi_wready.value = 0
dut.m_axi_bresp.value = bresp_err
dut.m_axi_bvalid.value = 1
while dut.m_axi_bready.value != 1:
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
dut.m_axi_bvalid.value = 0
# ============================================================
# 7. Cocotb 测试入口
# ============================================================
async def do_reset(dut):
"""模块级复位辅助函数"""
dut.rst_n.value = 0
dut.start.value = 0
dut.src_addr.value = 0
dut.dst_addr.value = 0
dut.length.value = 0
await Timer(50, unit="ns")
dut.rst_n.value = 1
await RisingEdge(dut.clk)
async def run_dma_test(dut, test_name, has_axi_ram=True):
"""
公共的 PyUVM 测试启动函数:生成时钟、配置 ConfigDB、运行 UVM Test
复位统一由 DmaBaseTest.reset_dut() 完成,避免 AxiLiteRam 经历多重复位
"""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
ConfigDB().set(None, "*", "DUT", dut)
if has_axi_ram:
axi_bus = AxiLiteBus.from_prefix(dut, "m_axi")
axi_ram = AxiLiteRam(axi_bus, dut.clk, dut.rst_n,
reset_active_level=False, size=0x2000)
ConfigDB().set(None, "*", "AXI_RAM", axi_ram)
# 保留 ConfigDB 单例,防止 run_test 清除已设置的 DUT 和 RAM
await uvm_root().run_test(test_name, keep_set={ConfigDB})
@cocotb.test()
async def test_dma_basic_transfer(dut):
"""基本 DMA 传输测试 (PyUVM)"""
await run_dma_test(dut, "DmaBasicTest")
@cocotb.test()
async def test_dma_large_transfer(dut):
"""大长度 DMA 传输测试 (PyUVM)"""
await run_dma_test(dut, "DmaLargeTransferTest")
@cocotb.test()
async def test_dma_zero_length(dut):
"""零长度 DMA 测试 (PyUVM)"""
await run_dma_test(dut, "DmaZeroLengthTest", has_axi_ram=False)
@cocotb.test()
async def test_dma_multiple_transfers(dut):
"""连续多次 DMA 传输测试 (PyUVM)"""
await run_dma_test(dut, "DmaMultipleTransfersTest")
@cocotb.test()
async def test_dma_reset(dut):
"""复位状态检查测试 (PyUVM)"""
await run_dma_test(dut, "DmaResetTest", has_axi_ram=False)
@cocotb.test()
async def test_dma_illegal_state(dut):
"""非法状态恢复测试 (PyUVM)"""
await run_dma_test(dut, "DmaIllegalStateTest", has_axi_ram=False)
@cocotb.test()
async def test_dma_read_error(dut):
"""读通道错误响应测试 (PyUVM)"""
await run_dma_test(dut, "DmaReadErrorTest", has_axi_ram=False)
@cocotb.test()
async def test_dma_write_error(dut):
"""写通道错误响应测试 (PyUVM)"""
await run_dma_test(dut, "DmaWriteErrorTest", has_axi_ram=False)
8.1 Testbench 整体架构
flowchart TB
subgraph TB_PYTHON["Testbench (Python + PyUVM)"]
direction TB
subgraph UVM_HIER["UVM 组件层次"]
TEST["uvm_test<br/>DmaBaseTest"]
ENV["uvm_env<br/>DmaEnv"]
SEQR["uvm_sequencer"]
DRV["uvm_driver<br/>DmaDriver"]
end
subgraph SEQUENCES["Sequences"]
S1["BasicTransferSeq"]
S2["LargeTransferSeq"]
S3["ZeroLengthSeq"]
S4["MultipleTransfersSeq"]
S5["ResetCheckSeq"]
S6["IllegalStateSeq"]
S7["ReadErrorSeq"]
S8["WriteErrorSeq"]
end
subgraph VIP["VIP / Slave 模型"]
RAM["AxiLiteRam<br/>正常内存响应"]
SLAVE_R["axi_lite_slave_read_error<br/>返回 SLVERR"]
SLAVE_W["axi_lite_slave_write_error<br/>返回 DECERR"]
SLAVE_OK["axi_lite_slave_read_ok<br/>返回正常数据"]
end
end
DUT["DUT: unique_simple_axi_dma"]
TEST --> ENV
ENV --> SEQR
ENV --> DRV
SEQR <--> DRV
S1 --> SEQR
S2 --> SEQR
S3 --> SEQR
S4 --> SEQR
S5 --> SEQR
S6 --> SEQR
S7 --> SEQR
S8 --> SEQR
DRV --> DUT
DUT <-->|AXI-Lite| RAM
DUT <-->|AXI-Lite| SLAVE_R
DUT <-->|AXI-Lite| SLAVE_W
DUT <-->|AXI-Lite| SLAVE_OK
8.2 PyUVM 核心概念速查
| 概念 | 代码示例 | 说明 |
|---|---|---|
| 定义测试 | @cocotb.test() |
装饰器标记这是一个 cocotb 测试用例 |
| 启动 UVM 测试 | await uvm_root().run_test("TestName", keep_set={ConfigDB}) |
创建并运行 UVM 测试树 |
| 事务基类 | class DmaTransaction(uvm_sequence_item) |
定义 Sequence 与 Driver 之间传递的数据结构 |
| Sequence | class BasicTransferSeq(uvm_sequence) |
生成事务序列,通过 start_item/finish_item 发送 |
| Driver | class DmaDriver(uvm_driver) |
从 Sequencer 拉取事务,驱动到 DUT |
| 连接 Port | self.driver.seq_item_port.connect(self.seqr.seq_item_export) |
在 connect_phase 中完成 TLM 连接 |
| ConfigDB 写 | ConfigDB().set(None, "*", "DUT", dut) |
全局设置共享资源(第一个参数为 context) |
| ConfigDB 读 | ConfigDB().get(self, "", "DUT") |
在组件中读取(参数为 uvm_component 实例) |
| Objection | self.raise_objection() / self.drop_objection() |
控制 run_phase 生命周期 |
| 获取响应 | rsp = await self.get_response() |
Sequence 中等待 Driver 返回的结果 |
| 启动时钟 | cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) |
自动生成时钟 |
8.3 Sequence / Driver / Sequencer 数据流
sequenceDiagram
participant SEQ as Sequence
participant SEQR as Sequencer
participant DRV as Driver
SEQ->>SEQR: start_item(item)
SEQR->>DRV: put_req(item)
DRV->>SEQR: get_next_item()
SEQR->>SEQ: start_condition.set() (item 就绪)
SEQ->>SEQR: finish_item(item)
SEQR->>DRV: item_ready.set()
DRV->>DRV: 执行事务,驱动 DUT
DRV->>SEQR: item_done(rsp)
SEQR->>SEQ: finish_condition.set()
SEQ->>SEQR: get_response()
SEQR->>SEQ: rsp
这是 PyUVM 中最核心的数据流:
- Sequence 调用
start_item()将事务放入 Sequencer 队列。 - Driver 调用
get_next_item()从队列取出事务。 - Sequencer 通知 Sequence “item 已就绪”,Sequence 调用
finish_item()。 - Driver 收到
finish_item信号后开始执行(驱动 DUT)。 - Driver 执行完成后调用
item_done(rsp)将结果返回给 Sequencer。 - Sequence 调用
get_response()获取结果。
8.4 测试用例详解
test_dma_basic_transfer — 基本功能测试
sequenceDiagram
participant PY as Python TB
participant RAM as AxiLiteRam
participant DUT as DMA
PY->>RAM: write(0x100, b'\x11\x22...')
PY->>DUT: src_addr=0x100, dst=0x200, len=8
PY->>DUT: start=1
DUT->>RAM: AXI Read addr=0x100
RAM-->>DUT: AXI Read data=0x11223344
DUT->>RAM: AXI Write addr=0x200, data=0x11223344
RAM-->>DUT: AXI Write OK
DUT->>RAM: AXI Read addr=0x104
RAM-->>DUT: AXI Read data=0xAABBCCDD
DUT->>RAM: AXI Write addr=0x204
RAM-->>DUT: AXI Write OK
DUT-->>PY: done=1
PY->>RAM: read(0x200, 8)
RAM-->>PY: b'\x11\x22\x33\x44\xAA\xBB\xCC\xDD'
PY->>PY: assert 数据匹配
这是最基本的” happy path “测试:先通过 VIP 把数据写入源地址,然后启动 DMA,最后从目标地址读回数据并比对。
test_dma_large_transfer — 大长度循环测试
32 字节数据需要 32 / 4 = 8 轮读写循环。这个测试验证状态机能否正确循环。
test_dma_zero_length — 边界条件测试
length = 0 是一个典型的边界条件。好的设计应该在这种情况下什么都不做,保持在 IDLE。
为什么边界条件很重要? 因为许多 bug 都发生在”正常情况”和”异常情况”的交界处。
test_dma_multiple_transfers — 连续传输测试
验证 DMA 在完成一次传输后,能否立即接受下一次启动,而不需要重新复位。
test_dma_default_state_recovery — 容错测试
通过 dut.state.value = 7 强制把状态机注入一个非法值(本设计只有 0~6 是合法状态),验证 default 分支能否正确恢复。
sequenceDiagram
participant TB as Testbench
participant DUT as DMA 状态机
TB->>DUT: force state = 7 (非法)
Note over DUT: 时钟上升沿采样
DUT->>DUT: case 进入 default 分支
DUT-->>TB: state = 0 (IDLE)
TB->>TB: assert state == 0
test_dma_read_error_response — 错误注入测试
使用手动 Slave 协程代替 AxiLiteRam,在收到读请求后返回 rresp = SLVERR (2'b10)。
async def axi_lite_slave_read_error(dut, rresp_err=2):
while dut.m_axi_arvalid.value != 1: # 等待 DMA 发起读地址
await RisingEdge(dut.clk)
dut.m_axi_arready.value = 1 # 立即响应地址
await RisingEdge(dut.clk)
dut.m_axi_arready.value = 0
dut.m_axi_rdata.value = 0xDEADBEEF # 随便给个数据
dut.m_axi_rresp.value = rresp_err # 返回错误响应!
dut.m_axi_rvalid.value = 1
while dut.m_axi_rready.value != 1: # 等待 DMA 接收数据
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
dut.m_axi_rvalid.value = 0
这个协程就是一个最简单的 AXI-Lite Slave 行为模型:等待请求 -> 握手 -> 返回响应。
test_dma_write_error_response — 写错误注入测试
与读错误类似,但需要同时运行一个正常读 Slave 协程和一个错误写 Slave 协程:
cocotb.start_soon(axi_lite_slave_read_ok(dut)) # 后台协程:处理读请求
await axi_lite_slave_write_error(dut, bresp_err=3) # 前台协程:处理写请求
cocotb.start_soon()启动一个后台协程,它会和主测试并行执行。这模拟了真实 SoC 中多个通道同时工作的场景。
9. 运行仿真
确保你的终端位于项目目录下,且已激活安装了 cocotb、cocotbext-axi 和 pyuvm 的 Python 环境。
9.1 编译与仿真
make clean
make SIM=verilator
如果一切正常,你会看到如下输出:
** TESTS=8 PASS=8 FAIL=0 SKIP=0 2030.01 0.02 96581.89 **
9.2 生成覆盖率报告
make coverage_report
这会生成 logs/annotated/unique_simple_axi_dma.v,其中每一行前面都标注了执行次数。
例如:
000269 always @(posedge clk or negedge rst_n) begin
000229 if (!rst_n) begin
000040 state <= IDLE;
...
%000001 default: state <= IDLE;
- 左侧数字表示该行被执行的次数。
%000001表示该行只执行了 1 次(对应default分支的非法状态恢复测试)。- 如果看到
%000000,说明该行从未被执行,需要补充测试用例。
9.3 生成 HTML 覆盖率报告(需 lcov)
如果你已经安装了 lcov(见 5.3 安装 lcov),可以运行:
make lcov_report
这会执行以下两步:
- 先调用
make coverage_report,生成源码标注文件。 - 再调用
verilator_coverage --write-info将 Verilator 覆盖率数据转换为 lcov 格式的.info文件,然后用genhtml生成可在浏览器中查看的 HTML 报告。
生成完成后,打开 logs/html/index.html:
# Ubuntu / WSL
xdg-open logs/html/index.html
# macOS
open logs/html/index.html
你将看到一个带有颜色标注的网页:
- 绿色:已执行的代码行
- 红色:未执行的代码行
- 黄色:部分执行的分支
这种方式比纯文本的 --annotate 输出更直观,尤其适合向团队或评审展示验证完备性。
10. PyUVM 常见踩坑与教训
以下是在将本教程从纯 Cocotb 迁移到 PyUVM 过程中遇到的真实踩坑记录,强烈建议仔细阅读,可以帮你节省大量调试时间。
10.1 ConfigDB 被 run_test() 清空
现象:
UVMConfigItemNotFound: "uvm_test_top.env.driver" is not in ConfigDB()
原因:uvm_root().run_test() 在启动测试前会默认调用 clear_singletons(),这会清空包括 ConfigDB 在内的所有单例。如果你在 run_test() 之前用 ConfigDB().set() 设置了 DUT 句柄,进入 build_phase 后就找不到了。
解决:在调用 run_test() 时传入 keep_set={ConfigDB}:
await uvm_root().run_test("DmaBasicTest", keep_set={ConfigDB})
教训:只要跨
run_test()调用使用 ConfigDB,就必须保留 ConfigDB 单例。
10.2 Sequence 中不能用 self 作为 ConfigDB 的 context
现象:
AssertionError: config_db context must be None or a uvm_component
原因:ConfigDB().get(self, "", "DUT") 的第一个参数 self 在 uvm_sequence 中是 uvm_object,而不是 uvm_component。ConfigDB 的实现要求 context 必须是 None 或 uvm_component 实例。
解决:在 Sequence 中访问 ConfigDB 时,第一个参数传 None:
# 正确
dut = ConfigDB().get(None, "", "DUT")
# 错误(只在 uvm_component 子类中可用)
dut = ConfigDB().get(self, "", "DUT")
教训:
ConfigDB().get(None, "*", "KEY")是最安全的跨组件访问方式;带self的写法只适用于uvm_component子类(如 Driver、Env、Test)。
10.3 AxiLiteRam 复位极性不匹配导致挂死
现象:仿真运行到 50ns100ns 时,AxiLiteRam 内部协程被杀掉,仿真挂死或后续读写无响应。
原因:AxiLiteRam 的构造函数有一个 reset_active_level 参数,默认值为 True(高电平有效复位)。而我们的 DUT 使用的是 rst_n(低电平有效)。当 rst_n 从 0 变为 1(释放复位)时,AxiLiteRam 识别为高电平复位有效,会杀掉内部所有处理协程。
解决:显式传入 reset_active_level=False:
axi_ram = AxiLiteRam(axi_bus, dut.clk, dut.rst_n,
reset_active_level=False, size=0x2000)
教训:使用任何带复位参数的 VIP 时,一定要核对
reset_active_level与 DUT 的复位极性是否一致。
10.4 get_response() 因 transaction_id 不匹配而挂死(最严重!)
现象:仿真在 DMA 传输完成后(如 240ns)突然挂死,CPU 占用 95%,永不结束。测试用例如果不调用 get_response() 则正常,一旦调用就挂死。
原因:PyUVM 4.0.1 + cocotb 2.0 中,uvm_sequence.get_response() 的默认行为是通过 transaction_id 匹配来从响应队列中取出结果。如果 Driver 在 item_done(rsp) 时没有将 rsp.transaction_id 设为与 item 一致,get_response() 会永远找不到匹配项,陷入 while True 循环等待一个永远不会到来的 Event。
解决:在 Driver 中调用 item_done() 前,显式同步 transaction_id:
async def run_phase(self):
while True:
item = await self.seq_item_port.get_next_item()
result = await self.execute_transfer(item)
result.transaction_id = item.transaction_id # <-- 关键!
self.seq_item_port.item_done(result)
教训:PyUVM 的
get_response()默认按transaction_id精确匹配。如果不需要精确匹配,可以传transaction_id=None,但显式同步 ID 是最规范的做法。这个坑在纯 cocotb 中不存在,是迁移到 PyUVM 时最容易踩的”静默挂死”问题。
10.5 cocotb.start_soon() 与 UVM phasing 的交互
现象:uvm_run_phase 完成后,后台的 Clock 协程和 Driver 的 while True 循环仍在运行,但不会影响测试结束。
说明:PyUVM 的 uvm_run_phase 通过 ObjectionHandler 管理生命周期。当 Test 中所有 objection 都被 drop 后,run_phase_complete() 返回,UVM 继续执行后续 phase(extract/check/report/final),然后 run_test() 结束。后台残留的 cocotb.start_soon() 协程(如 Clock、Driver while True)虽然还在事件循环中,但 cocotb 的 regression manager 会正常结束当前测试并进入下一个。
教训:PyUVM 测试的正常结束不依赖于”杀死所有后台协程”,而依赖于 Objection 机制。只要确保
raise_objection()和drop_objection()成对出现即可。
11. 扩展建议
- 增加随机测试:使用
random或cocotb-coverage库生成随机地址和长度,配合 PyUVM 的 Sequence 随机化机制,进一步提升覆盖率。 - 测试延迟场景:在手动 Slave 协程中加入随机
Timer延迟,验证 DMA 对 AXI ready 信号等待的鲁棒性。 - 接入 CI/CD:将
make clean && make SIM=verilator写入 GitHub Actions 或 GitLab CI,实现自动化回归。 - 使用 UVM Agent:将 Sequencer + Driver + Monitor 封装成
uvm_agent,更符合工业标准架构。 - 加入 Scoreboard:实现一个
uvm_scoreboard,自动比对预期数据和实际读回数据,替代 Sequence 中的手动assert。
12. 总结
本项目完整展示了:
- DMA 的基本概念:为什么需要 DMA、Memory-to-Memory 传输流程。
- AXI-Lite 协议基础:VALID/READY 握手机制、读写通道分离、响应编码。
- 数字验证的核心思想:通过 Testbench 模拟各种场景,在流片前发现 bug。
- 如何用 Verilog 实现一个简单的 AXI-Lite Master DMA:状态机设计、错误处理、复位策略。
- 如何用 PyUVM + cocotbext-axi 编写标准 UVM 验证平台:Sequence、Driver、Sequencer、Env、Test 的经典架构,ConfigDB 与 Objection 的使用。
- 如何用 Verilator 进行编译仿真并收集行覆盖率。
所有代码均已在本教程中完整给出,复制到对应文件即可直接运行。
结语
第三百八十三篇博文写完,开心!!!!
今天,也是充满希望的一天。