00383 利用 PyUVM + cocotbext-axi 验证 AXI DMA 的完整教程


本教程从零开始,面向初学者,带你理解 DMAAXI 总线数字验证 的基本概念,并一步步搭建基于 PyUVMcocotbext-axi VIP 和 Verilator 的仿真环境,实现一个简易的 AXI-Lite DMA IP,编写完整的 Python Testbench 进行功能验证与覆盖率收集。


目录

  1. 什么是 DMA?
  2. 什么是 AXI?
  3. 什么是数字验证?
  4. 项目概述
  5. 环境 Setup
  6. RTL 源码详解
  7. Makefile
  8. PyUVM Testbench 详解
  9. 运行仿真
  10. PyUVM 常见踩坑与教训
  11. 扩展建议
  12. 总结

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 握手:当 validready 同时为高时,数据传输发生。

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)完成后,不能直接把代码烧到芯片里。因为:

  1. 流片成本极高:一次芯片制造(流片)可能需要数百万美元,且耗时数月。
  2. bug 修复困难:芯片制造完成后发现 bug,只能重新设计、重新流片。
  3. 逻辑错误难以肉眼发现:复杂的状态机、时序交互问题,单纯看代码很难发现。

因此,我们需要在流片前,通过仿真来穷尽各种场景,确保设计功能正确。

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 的 rrespbresp,则拉高 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.1cocotbext-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 分支]

状态流转详解:

  1. IDLE:空闲状态。收到 startlength > 0 时,保存配置参数,进入 READ_ADDR
  2. READ_ADDR:发送读地址(araddr = current_srcarvalid = 1)。当 Slave 返回 arready = 1 时,握手成功,进入 READ_DATA
  3. READ_DATA:等待 Slave 返回数据(rvalid = 1)。收到数据后检查 rresp
    • 若为 OKAY(00):保存数据到 data_buffer,进入 WRITE_ADDR
    • 若为错误:置位 errordone,回到 IDLE
  4. WRITE_ADDR:发送写地址(awaddr = current_dstawvalid = 1)。握手成功后进入 WRITE_DATA
  5. WRITE_DATA:发送写数据(wdata = data_bufferwvalid = 1)。握手成功后进入 WRITE_RESP
  6. WRITE_RESP:等待 Slave 返回写响应(bvalid = 1)。检查 bresp
    • 若为 OKAY:更新地址和剩余字节数。若 bytes_left <= 4 则传输完成(done = 1),否则回到 READ_ADDR 继续下一轮。
    • 若为错误:置位 errordone,回到 IDLE

6.5 代码设计要点

  • 复位处理:所有控制信号(validready)都清零,防止复位后出现虚假的总线请求。
  • 握手逻辑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 中最核心的数据流:

  1. Sequence 调用 start_item() 将事务放入 Sequencer 队列。
  2. Driver 调用 get_next_item() 从队列取出事务。
  3. Sequencer 通知 Sequence “item 已就绪”,Sequence 调用 finish_item()
  4. Driver 收到 finish_item 信号后开始执行(驱动 DUT)。
  5. Driver 执行完成后调用 item_done(rsp) 将结果返回给 Sequencer。
  6. 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. 运行仿真

确保你的终端位于项目目录下,且已激活安装了 cocotbcocotbext-axipyuvm 的 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

这会执行以下两步:

  1. 先调用 make coverage_report,生成源码标注文件。
  2. 再调用 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") 的第一个参数 selfuvm_sequence 中是 uvm_object,而不是 uvm_component。ConfigDB 的实现要求 context 必须是 Noneuvm_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. 扩展建议

  1. 增加随机测试:使用 randomcocotb-coverage 库生成随机地址和长度,配合 PyUVM 的 Sequence 随机化机制,进一步提升覆盖率。
  2. 测试延迟场景:在手动 Slave 协程中加入随机 Timer 延迟,验证 DMA 对 AXI ready 信号等待的鲁棒性。
  3. 接入 CI/CD:将 make clean && make SIM=verilator 写入 GitHub Actions 或 GitLab CI,实现自动化回归。
  4. 使用 UVM Agent:将 Sequencer + Driver + Monitor 封装成 uvm_agent,更符合工业标准架构。
  5. 加入 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 进行编译仿真并收集行覆盖率

所有代码均已在本教程中完整给出,复制到对应文件即可直接运行。

结语

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

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


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