Building SystemVerilog AXI VIP for Fast Bring-Up

Task-based AXI4 drivers for fast testbench bring-up

Posted by Nelson Campos on March 21, 2026

A previous post introduced the AMBA AXI handshake at a conceptual level. This post goes further: it shows how to build a practical AXI4 verification environment using task-based SystemVerilog drivers and simulate it with Verilator. All examples are taken from verilaxi, an open-source AXI verification library.

Why a task-based environment?

UVM is excellent at what it is designed for: building large reusable verification environments with factory-driven composition, configuration databases, sequencers, scoreboards, and coverage infrastructure. That is the right long-term answer for many production verification projects.

Verilaxi makes a different trade-off. The goal here is very fast testbench bring-up: get interfaces plumbed quickly, express protocol handshakes directly, and start running meaningful stress tests before a full UVM environment exists. Task-based drivers are a practical fit for that stage. They express the same handshake sequences — address phase, data phase, response phase — as plain SystemVerilog tasks inside classes, and they are straightforward to read, debug, and extend as the environment evolves.

The AXI4 interface

An AXI4 interface in SystemVerilog groups all five channels (AW, W, B, AR, R) and their handshake signals into a single interface, parameterised by address width, data width, and ID width. All channels use the same VALID/READY handshake described in the AMBA post: a transfer occurs on the rising edge of the clock when both VALID and READY are high.

interface axi_if #(
    parameter ADDR_WIDTH = 32,
    parameter DATA_WIDTH = 64,
    parameter ID_WIDTH   = 4
)(
    input logic ACLK,
    input logic ARESETn
);
    // Write address channel
    logic [ID_WIDTH-1:0]   awid;
    logic [ADDR_WIDTH-1:0] awaddr;
    logic [7:0]            awlen;
    logic [2:0]            awsize;
    logic [1:0]            awburst;
    logic                  awvalid;
    logic                  awready;

    // Write data channel
    logic [DATA_WIDTH-1:0]   wdata;
    logic [DATA_WIDTH/8-1:0] wstrb;
    logic                    wlast;
    logic                    wvalid;
    logic                    wready;

    // Write response channel
    logic [ID_WIDTH-1:0] bid;
    logic [1:0]          bresp;
    logic                bvalid;
    logic                bready;

    // Read address channel
    logic [ID_WIDTH-1:0]   arid;
    logic [ADDR_WIDTH-1:0] araddr;
    logic [7:0]            arlen;
    logic [2:0]            arsize;
    logic [1:0]            arburst;
    logic                  arvalid;
    logic                  arready;

    // Read data channel
    logic [ID_WIDTH-1:0]   rid;
    logic [DATA_WIDTH-1:0] rdata;
    logic [1:0]            rresp;
    logic                  rlast;
    logic                  rvalid;
    logic                  rready;

    modport master (output awid, awaddr, awlen, awsize, awburst, awvalid,
                           wdata, wstrb, wlast, wvalid, bready,
                           arid, araddr, arlen, arsize, arburst, arvalid, rready,
                    input  awready, wready, bid, bresp, bvalid,
                           arready, rid, rdata, rresp, rlast, rvalid);

    modport slave  (input  awid, awaddr, awlen, awsize, awburst, awvalid,
                           wdata, wstrb, wlast, wvalid, bready,
                           arid, araddr, arlen, arsize, arburst, arvalid, rready,
                    output awready, wready, bid, bresp, bvalid,
                           arready, rid, rdata, rresp, rlast, rvalid);
endinterface

AXI4 interface definition (tb/interfaces/axi_if.sv)

The AXI master driver

The axi_master class holds a virtual interface handle and provides tasks for write and read bursts. Each task drives the appropriate channels and waits for the handshake to complete before returning.

class axi_master #(
    parameter ADDR_WIDTH = 32,
    parameter DATA_WIDTH = 64,
    parameter ID_WIDTH   = 4
);
    virtual axi_if #(ADDR_WIDTH, DATA_WIDTH, ID_WIDTH) vif;

    function new(virtual axi_if #(ADDR_WIDTH, DATA_WIDTH, ID_WIDTH) vif);
        this.vif = vif;
    endfunction

    // Single write burst: awlen+1 beats starting at addr
    task write_burst(
        input logic [ADDR_WIDTH-1:0]   addr,
        input logic [7:0]              awlen,
        input logic [2:0]              awsize,
        input logic [DATA_WIDTH-1:0]   data[]
    );
        // AW channel
        @(posedge vif.ACLK);
        vif.awaddr  <= addr;
        vif.awlen   <= awlen;
        vif.awsize  <= awsize;
        vif.awburst <= 2'b01;   // INCR
        vif.awvalid <= 1;
        @(posedge vif.ACLK iff vif.awready);
        vif.awvalid <= 0;

        // W channel
        for (int i = 0; i <= awlen; i++) begin
            vif.wdata  <= data[i];
            vif.wstrb  <= '1;
            vif.wlast  <= (i == awlen);
            vif.wvalid <= 1;
            @(posedge vif.ACLK iff vif.wready);
        end
        vif.wvalid <= 0;
        vif.wlast  <= 0;

        // B channel
        vif.bready <= 1;
        @(posedge vif.ACLK iff vif.bvalid);
        vif.bready <= 0;
    endtask

    // Single read burst: arlen+1 beats starting at addr
    task read_burst(
        input  logic [ADDR_WIDTH-1:0] addr,
        input  logic [7:0]            arlen,
        input  logic [2:0]            arsize,
        output logic [DATA_WIDTH-1:0] data[]
    );
        // AR channel
        @(posedge vif.ACLK);
        vif.araddr  <= addr;
        vif.arlen   <= arlen;
        vif.arsize  <= arsize;
        vif.arburst <= 2'b01;
        vif.arvalid <= 1;
        @(posedge vif.ACLK iff vif.arready);
        vif.arvalid <= 0;

        // R channel
        vif.rready <= 1;
        for (int i = 0; i <= arlen; i++) begin
            @(posedge vif.ACLK iff vif.rvalid);
            data[i] = vif.rdata;
        end
        vif.rready <= 0;
    endtask

endclass

AXI4 master driver class (simplified from tb/classes/axi/axi_master.sv)

Backpressure: the AXI slave model

A real memory controller does not always assert AWREADY, WREADY, or ARREADY immediately. The axi_slave VIP models this with a configurable ready_prob property: on each clock cycle, READY is asserted with probability ready_prob percent and withheld otherwise. This stresses the DUT FSMs under realistic memory-controller latency without changing any test logic.

The important detail is that this is generated independently on a cycle-by-cycle basis, not from a fixed repeating stall pattern. In the VIP, the helper function below is called whenever the slave decides whether a READY signal should be asserted:

// Returns 1 with probability ready_prob%
function automatic bit rand_ready();
    if (ready_prob >= 100) return 1'b1;
    if (ready_prob <= 0)   return 1'b0;
    return ($urandom_range(99, 0) < ready_prob);
endfunction

Random READY generation in tb/classes/axi/axi_slave.sv

If ready_prob = 70, then on each eligible clock edge the slave draws a fresh random integer between 0 and 99 and asserts READY only when the result is less than 70. That means READY is high on roughly 70% of cycles and low on roughly 30%, but the exact pattern changes from run to run. The resulting waveform looks much more like realistic backpressure from a memory subsystem than a deterministic every-third-cycle stall would.

The AW, W, and AR channels are sampled separately, so each channel can stall independently. In particular, the write-data channel uses

// Assert WREADY when data is expected; randomly deassert when ready_prob < 100
vif.wready <= (wr_beats_left > 0) && rand_ready();

W-channel backpressure generation in axi_slave.sv

That detail matters because it exercises exactly the corner cases we care about in a DMA or burst-capable master: address accepted but data stalled, data advancing unevenly across beats, and read commands that wait several cycles before the slave becomes ready. In other words, ready_prob is not just cosmetic randomness; it is a compact way to stress handshake robustness and state-machine correctness.

// 70% ready probability — slave stalls the master 30% of cycles
axi_slave mem_slave;
mem_slave = new(axi_if_u0.slave);
mem_slave.ready_prob = 70;
fork mem_slave.run(); join_none

// Or via plusarg:
make run TESTNAME=dma READY_PROB=70

Enabling AXI slave backpressure via the VIP property or a Makefile plusarg

Figure (2) shows a DMA simulation waveform with READY_PROB=80. The gaps in AWREADY and WREADY are the slave withholding its ready signal.

DMA waveform with backpressure

Figure (2): DMA waveform with AXI slave backpressure (READY_PROB=80). The DUT S2MM engine stalls correctly while waiting for AWREADY and WREADY.

Running a write and read burst

The following example drives a write burst followed by a read burst using the master driver directly. In practice, the high-level axi_dma_driver wraps these calls behind CSR configuration and stream tasks, but the underlying mechanism is the same.

module test_axi_rw (input logic clk, input logic rst_n);
    import axi_pkg::*;

    axi_if #(.ADDR_WIDTH(32), .DATA_WIDTH(64)) axi_if_u0 (.ACLK(clk), .ARESETn(rst_n));
    axi_master #(32, 64) m;

    logic [63:0] wr_data[] = new[4];
    logic [63:0] rd_data[] = new[4];

    initial begin
        m = new(axi_if_u0.master);
        @(posedge rst_n);

        wr_data[0] = 64'h1111_0001_0000_0000;
        wr_data[1] = 64'h2222_0002_0000_0000;
        wr_data[2] = 64'h3333_0003_0000_0000;
        wr_data[3] = 64'h4444_0004_0000_0000;

        // 4-beat write burst at address 0x100, awsize=3 (8 bytes/beat)
        m.write_burst(32'h100, 8'd3, 3'd3, wr_data);

        // 4-beat read burst at address 0x100
        m.read_burst(32'h100, 8'd3, 3'd3, rd_data);

        foreach (rd_data[i])
            assert (rd_data[i] === wr_data[i]) else $error("Mismatch at beat %0d", i);

        $finish;
    end
endmodule

Write burst followed by read burst using the AXI master driver

AXI-Lite and AXI-Stream

Verilaxi includes separate driver classes for AXI-Lite and AXI-Stream. axil_master provides write(addr, data) and read(addr, data) tasks for single-beat register accesses. axis_source and axis_sink drive and capture AXI-Stream packets respectively, with optional backpressure on TVALID and TREADY. These are described in detail in the posts on CSR design and DMA engines.

Summary

Task-based AXI drivers in plain SystemVerilog are a practical and Verilator-compatible alternative to UVM agents. An interface groups all channel signals; a class holds the virtual interface and provides write and read burst tasks; a configurable slave model injects backpressure. The same pattern applies to AXI-Lite and AXI-Stream. The full VIP source is available in the verilaxi repository.

Read next:
Checking AXI Protocol with SystemVerilog Assertions for the lightweight protocol checkers layered on top of the task-based environment.
Building a Verilator Testbench for AXI Designs for how the C++ harness and plusargs drive these classes at runtime.
AXI DMA — Moving Data Without the CPU for a full design that uses the same VIP style.
Implementation pointers in verilaxi: tb/classes/axi/axi_master.sv, tb/classes/axi/axi_slave.sv, and tb/interfaces/axi_if.sv.

References:
[1] ARM. AMBA AXI and ACE Protocol Specification. 2011
[2] AMBA AXI Protocol — sistenix.com


Also available in GitHub.