Module 10: Capstone Projects ยท Project 5 โ€” AXI4-Lite Slave Interface

Project 5, Part 3: The Complete Slave, and the End of the Course

Every piece is built: the five-channel contract from Part 1, the register file and handshake logic from Part 2. This article assembles them, verifies the result three ways (directed, randomized and assertion-based, deliberately drawing on the whole course’s verification toolkit), and closes both the project and the course.

The complete slave

module axi4_lite_slave #(parameter NUM_REGS = 4) (
    input  logic        clk, rst_n,
    // Write address channel
    input  logic [31:0] awaddr, input logic awvalid, output logic awready,
    // Write data channel
    input  logic [31:0] wdata, input logic wvalid, output logic wready,
    // Write response channel
    output logic [1:0]  bresp, output logic bvalid, input logic bready,
    // Read address channel
    input  logic [31:0] araddr, input logic arvalid, output logic arready,
    // Read data channel
    output logic [31:0] rdata, output logic [1:0] rresp, output logic rvalid, input logic rready
);
    // AW/W independent capture and commit logic, and register file: Part 2
    logic wr_commit;
    logic awaddr_valid_flag, wdata_valid_flag;
    logic [$clog2(NUM_REGS)-1:0] waddr_internal, raddr_internal;
    logic [31:0] wdata_internal, rdata_internal;

    // Accept each half only while it has not already been captured, and
    // not while a response is still outstanding
    assign awready = !awaddr_valid_flag && !bvalid;
    assign wready  = !wdata_valid_flag  && !bvalid;

    // ... AW/W capture flags and wr_commit generation, exactly per Part 2 ...

    axi_registers #(.NUM_REGS(NUM_REGS)) regs (
        .clk(clk), .rst_n(rst_n), .waddr(waddr_internal), .raddr(raddr_internal),
        .wdata(wdata_internal), .wr_commit(wr_commit), .rdata(rdata_internal)
    );

    // Write response: issue the B channel response the cycle AFTER wr_commit,
    // per Part 1's rule that B must not precede both AW and W completing
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) bvalid <= 1'b0;
        else if (wr_commit) begin
            bvalid <= 1'b1;
            bresp  <= 2'b00;   // OKAY response
        end else if (bvalid && bready) begin
            bvalid <= 1'b0;
        end
    end

    // Read address/data FSM: exactly Part 2
    // ... AXI_R_IDLE / AXI_R_DATA states ...
    assign rdata = rdata_internal;
    assign rresp = 2'b00;   // OKAY response
endmodule

Directed test: the basic read/write contract

task automatic axi_write(bit [31:0] addr, bit [31:0] data);
    awaddr = addr; awvalid = 1'b1;
    wdata_drv = data; wvalid = 1'b1;
    fork
        begin @(posedge clk iff awready); awvalid = 1'b0; end
        begin @(posedge clk iff wready);  wvalid  = 1'b0; end
    join
    bready = 1'b1;
    @(posedge clk iff bvalid); bready = 1'b0;
endtask

task automatic axi_read(bit [31:0] addr, output bit [31:0] data);
    araddr = addr; arvalid = 1'b1;
    @(posedge clk iff arready); arvalid = 1'b0;
    rready = 1'b1;
    @(posedge clk iff rvalid); data = rdata; rready = 1'b0;
endtask

initial begin
    bit [31:0] readback;
    rst_n = 0; awvalid=0; wvalid=0; arvalid=0; bready=0; rready=0;
    repeat(3) @(posedge clk); rst_n = 1;

    axi_write(32'd0, 32'hDEAD_BEEF);
    axi_read(32'd0, readback);
    if (readback == 32'hDEAD_BEEF) $display("PASS: basic write/read");
    else $display("FAIL: basic write/read, got %h", readback);
end

@(posedge clk iff awready) waits for a clock edge on which awready is high, which is exactly the handshake completion: VALID is held high by the task, and the transfer completes on the first edge where READY is also high.

Randomized test: independent AW/W timing

Part 1’s whole point was that AW and W can arrive in any order. A test that only drives them together has verified almost nothing about that claim. This is Section 9.2’s randomization applied to timing rather than data:

class axi_timing_txn;
    rand int aw_delay;   // cycles to wait before asserting AWVALID
    rand int w_delay;    // cycles to wait before asserting WVALID
    constraint reasonable_delay { aw_delay inside {[0:5]}; w_delay inside {[0:5]}; }
endclass

task automatic axi_write_with_delay(bit [31:0] addr, data, int aw_delay, w_delay);
    fork
        begin
            repeat (aw_delay) @(posedge clk);
            awaddr = addr; awvalid = 1'b1;
            @(posedge clk iff awready); awvalid = 1'b0;
        end
        begin
            repeat (w_delay) @(posedge clk);
            wdata_drv = data; wvalid = 1'b1;
            @(posedge clk iff wready); wvalid = 1'b0;
        end
    join
    bready = 1'b1;
    @(posedge clk iff bvalid); bready = 1'b0;
endtask

fork...join runs both channels’ stimulus concurrently, each with its own randomized delay, exercising “AW before W”, “W before AW” and “both together” across repeated runs, which is the coverage Part 1’s design rationale depends on.

The assertion: checking the ordering rule directly

Here is where Module 9 pays off. Rather than hoping the directed and randomized tests happen to expose an ordering violation if one exists, state the rule as a concurrent assertion, checked continuously:

// The rule from Part 1: BVALID must never rise until BOTH the AW and W
// handshakes for that transaction have completed, i.e. until wr_commit fired.
property write_response_ordering;
    @(posedge clk) disable iff (!rst_n)
    $rose(bvalid) |-> $past(wr_commit);   // bvalid can only rise the cycle AFTER wr_commit
endproperty

assert property (write_response_ordering)
else $error("AXI ordering violation: BVALID rose without a preceding wr_commit");

$past(wr_commit) is another SVA built-in, the value a signal held one cycle earlier, which encodes “the cycle before this one” as a checkable condition and matches the design’s own bvalid <= 1'b1 the cycle after wr_commit. The assertion runs against every test in the testbench, directed, randomized and any added later, catching an ordering violation the instant it occurs, which is the Section 9.3 point about assertions travelling with the design. This is how real AXI verification IP works: a standardized assertion library checks every protocol rule continuously against any DUT it is bound to, because the assertions check the specification itself rather than any one test’s scenario.

Looking back over the course

Ten modules, and this is where they met. Module 1’s Boolean algebra became Module 2’s ALU, which became this project’s register write logic. Module 3’s flip-flops and FSMs became every controller in this module. Modules 4 and 5 gave the language to describe all of it, and Module 6 the discipline to prove it correct. Module 7’s pipelining, FIFOs and CDC awareness underpinned the SRAM controller’s timing discipline and would underpin any of these projects the moment it crossed a clock boundary. Module 8’s coding quality and debugging method are what get a design from “seems to work” to verified and reviewable. Module 9’s SystemVerilog gave this article its randomized timing test and its concurrent assertion, capability that did not exist earlier in the course.

None of the five projects introduced a technique you had not already met. That was deliberate. If a processor core, a UART, an SPI master, a memory controller and a bus protocol slave all felt like assemblies of things you already knew, rather than five new subjects needing five new toolkits, that is the measure of what the course built. You did not only learn Verilog. You learned to think in hardware, one module at a time, until a whole system stopped looking like anything more than the sum of parts you had already made your own.

That is the course. Go and build something real with it.

โ† Project 5, Part 2: A Register File Behind Five Handshakes