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
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.