Module 10: Capstone Projects · Project 5 — AXI4-Lite Slave Interface

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

The previous article specified the five channels. This one builds the logic behind them.

The storage is the least novel part of the project: a small register file, structurally the same as Section 3.6’s. What is new is managing five separate channels’ handshake state at the same time, and implementing Part 1’s rule that a write commits only once both AW and W have completed.

The register file

module axi_registers #(parameter NUM_REGS = 4) (
    input  logic                    clk, rst_n,
    input  logic [$clog2(NUM_REGS)-1:0] waddr, raddr,
    input  logic [31:0]             wdata,
    input  logic                    wr_commit,   // pulses HIGH exactly when a write should commit
    output logic [31:0]             rdata
);
    logic [31:0] regs [0:NUM_REGS-1];

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            for (int i = 0; i < NUM_REGS; i++) regs[i] <= 32'b0;
        end else if (wr_commit) begin
            regs[waddr] <= wdata;
        end
    end

    assign rdata = regs[raddr];   // combinational read, the Section 3.6 pattern
endmodule

Nothing here is unfamiliar. The whole challenge of this article is generating that one wr_commit pulse at the right moment, honouring the rule that it can fire only once both address and data have arrived.

Capturing write address and write data independently

Because AW and W can arrive in either order, the slave latches whichever comes first and waits for its partner:

logic        awaddr_valid_flag, wdata_valid_flag;
logic [1:0]  waddr_captured;
logic [31:0] wdata_captured;

assign wr_commit = awaddr_valid_flag && wdata_valid_flag;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        awaddr_valid_flag <= 1'b0;
        wdata_valid_flag  <= 1'b0;
    end else begin
        // Capture AW the moment it completes its own handshake
        if (awvalid && awready) begin
            waddr_captured    <= awaddr[3:2];   // word address, per the register map
            awaddr_valid_flag <= 1'b1;
        end
        // Capture W the moment IT completes its own, independent handshake
        if (wvalid && wready) begin
            wdata_captured   <= wdata;
            wdata_valid_flag <= 1'b1;
        end
        // Once BOTH have arrived the write commits this cycle; clear both
        // flags for the next transaction
        if (wr_commit) begin
            awaddr_valid_flag <= 1'b0;
            wdata_valid_flag  <= 1'b0;
        end
    end
end

assign waddr = waddr_captured;
assign wdata_reg_in = wdata_captured;

Two independent “have I received my half” flags, each set by its own channel’s completion, and the commit fires for one cycle only when both are set. That is Part 1’s ordering rule as hardware: the write cannot commit on the cycle AW alone completes, nor W alone, only on the later of the two. The slave must also hold awready and wready low while a flag is already set, so a second transaction cannot overwrite a captured half before the first has committed; the full implementation in the next article does that.

Read address to read data

The read side is simpler, since a read involves two channels (AR in, R out) rather than the write side’s three:

typedef enum logic [1:0] {AXI_R_IDLE, AXI_R_DATA} axi_r_state_t;
axi_r_state_t r_state, r_next_state;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) r_state <= AXI_R_IDLE;
    else        r_state <= r_next_state;
end

always_comb begin
    r_next_state = r_state;
    arready = 1'b0; rvalid = 1'b0;
    unique case (r_state)
        AXI_R_IDLE: begin
            arready = 1'b1;
            if (arvalid) r_next_state = AXI_R_DATA;
        end
        AXI_R_DATA: begin
            rvalid = 1'b1;
            if (rready) r_next_state = AXI_R_IDLE;
        end
        default: r_next_state = AXI_R_IDLE;
    endcase
end

always_ff @(posedge clk) begin
    if (r_state == AXI_R_IDLE && arvalid && arready)
        raddr <= araddr[3:2];   // latch the read address the moment AR completes
end

Read FSM: idle accepts the address handshake, data state presents the response
Figure 1: The read-side FSM

This is the two-block FSM style from every other project in the module. AXI_R_IDLE accepts the address handshake, AXI_R_DATA presents the response and waits for the master’s RREADY, the same ready/valid discipline from Part 1 on a single channel pair.

Practice Exercise

Trace the write-side flags for a case where WVALID/WREADY complete on cycle 2 but AWVALID/AWREADY do not complete until cycle 5. On which cycle does wr_commit assert?

Solution

Cycle 2: wvalid && wready is true, so wdata_valid_flag is 1 from cycle 3. awaddr_valid_flag is still 0, so wr_commit stays 0. Cycles 3 and 4: wdata_valid_flag stays 1 (nothing has cleared it), awaddr_valid_flag still 0, wr_commit still 0, correctly waiting. Cycle 5: awvalid && awready completes, so awaddr_valid_flag is 1 from cycle 6. On cycle 6 both flags are 1 for the first time, wr_commit asserts, the register is written at the end of cycle 6, and both flags clear from cycle 7. The commit waited for the later of the two completions, AW on cycle 5, matching the rule from Part 1.

The storage behind this slave is Section 3.6’s register file, unchanged. The real design work was the independent capture of write address and write data, each set by its own handshake, with the write committing only when both flags are set, a direct implementation of the ordering rule. The read side reuses the two-block FSM from every earlier project. In the next article, the last of the project and the course, we assemble the complete slave and verify it, including a concurrent assertion checking the ordering rule directly.

← Project 5, Part 1: AXI4-Lite, Five Independent Handshakes Project 5, Part 3: The Complete Slave, and the End of the Course →