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.