Writes finish in ACCESS. Reads continue through WAIT and end with one RESP cycle.
State | Outputs | Next |
|---|---|---|
|
|
|
|
| write → |
| none |
|
|
|
|
The waveform shows a write of 0xA7 to address 5 followed by a read of the same address.
Write offer: req_valid is high with write set and address 5.
Write accept: req_ready is high, the state moves to ACCESS, and mem_en with mem_we pulses.
Read offer: req_valid is high with address 5 while the controller is idle.
Read wait: ACCESS is followed by WAIT while the RAM latency elapses.
Response: RESP carries rsp_valid high with rsp_rdata of 0xA7.
Read response appears after edge 2, is consumed at edge 3, and ready returns after edge 3; next acceptance is edge 4. A write commits at edge 1, ready returns after edge 1, and next acceptance is edge 2.
The full controller with independent value/order, exact access/response timing and consumer checks. Legal parameters: AW >= 1, DW >= 1, RD_LAT >= 1; initial diagnostics are time-zero simulation checks, not portable elaboration guarantees. The bounded sweep includes one-bit ports, latencies 1 through 4, directed ordering and reset cancellation before random traffic.
module mem_ctrl #(parameter int AW = 6, parameter int DW = 8, parameter int RD_LAT = 2) (
input logic clk, rst_n,
// request side (valid/ready)
input logic req_valid,
output logic req_ready,
input logic req_we,
input logic [AW-1:0] req_addr,
input logic [DW-1:0] req_wdata,
// response side (reads only)
output logic rsp_valid,
output logic [DW-1:0] rsp_rdata,
// synchronous RAM: RD_LAT registered stages, launch edge counts as stage 1
output logic mem_en, mem_we,
output logic [AW-1:0] mem_addr,
output logic [DW-1:0] mem_wdata,
input logic [DW-1:0] mem_rdata
);
typedef enum logic [1:0] {IDLE, ACCESS, WAIT, RESP} state_t;
initial if (AW < 1 || DW < 1 || RD_LAT < 1)
$fatal(1, "mem_ctrl: AW, DW and RD_LAT must be at least 1");
state_t state;
// the request is latched on accept, so the requester may change its inputs afterwards
logic we_r;
logic [AW-1:0] addr_r;
logic [DW-1:0] wdata_r;
logic [$clog2(RD_LAT+1):0] wait_cnt;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
state <= IDLE; we_r <= 1'b0; addr_r <= '0; wdata_r <= '0; wait_cnt <= '0;
end else begin
unique case (state)
IDLE: if (req_valid) begin
we_r <= req_we; addr_r <= req_addr; wdata_r <= req_wdata;
state <= ACCESS;
end
ACCESS: if (we_r) state <= IDLE; // write: done after one access cycle
else begin // read: wait for the RAM latency
wait_cnt <= RD_LAT - 1;
if (RD_LAT == 1) state <= RESP; else state <= WAIT;
end
WAIT: begin
wait_cnt <= wait_cnt - 1'b1;
if (wait_cnt == 1) state <= RESP;
end
RESP: state <= IDLE; // read data is valid in this cycle
default: state <= IDLE;
endcase
end
assign req_ready = (state == IDLE);
assign mem_en = (state == ACCESS);
assign mem_we = (state == ACCESS) && we_r;
assign mem_addr = addr_r;
assign mem_wdata = wdata_r;
assign rsp_valid = (state == RESP);
assign rsp_rdata = mem_rdata;
endmodule
module mc_env #(parameter int AW = 6, DW = 8, LAT = 2)
(input logic clk, output int errors, output bit finished);
timeunit 1ns; timeprecision 1ps;
logic rst_n = 1;
logic req_valid = 0, req_ready, req_we = 0, rsp_valid, mem_en, mem_we;
logic [AW-1:0] req_addr = 0, mem_addr;
logic [DW-1:0] req_wdata = 0, rsp_rdata, mem_wdata, mem_rdata;
mem_ctrl #(AW, DW, LAT) dut (.*);
// synchronous RAM model with LAT cycles of read latency
logic [DW-1:0] ram [2**AW];
logic [DW-1:0] rpipe [LAT];
int ram_reads = 0, ram_writes = 0;
always_ff @(posedge clk) begin
if (!rst_n) begin ram_reads <= 0; ram_writes <= 0; end
else if (mem_en) begin
if (mem_we) begin
ram[mem_addr] <= mem_wdata;
ram_writes <= ram_writes + 1;
end else begin
rpipe[0] <= ram[mem_addr];
ram_reads <= ram_reads + 1;
end
end
for (int i = 1; i < LAT; i++) rpipe[i] <= rpipe[i-1];
end
assign mem_rdata = rpipe[LAT-1];
// scoreboard: a software memory and a queue of expected read results
logic [DW-1:0] model [2**AW];
logic [DW-1:0] expected [$];
int writes = 0, reads = 0, rsp_count = 0;
int due [$];
int cycle = 0, access_due = -1;
logic saved_we;
logic [AW-1:0] saved_addr;
logic [DW-1:0] saved_wdata, consumer_data;
int consumer_count = 0;
logic [DW-1:0] exp_val;
initial for (int i = 0; i < 2**AW; i++) begin ram[i] = '0; model[i] = '0; end
// driver: holds a request until accepted, then immediately presents the next (different) request,
// so any logic that reads the request inputs after accept sees the wrong values
logic took = 1, stop = 1, manual = 1;
logic request_held = 0;
logic [AW+DW:0] held_request;
always @(posedge clk) begin
if (!rst_n) request_held <= 0;
else begin
if (request_held && (req_valid !== 1'b1 || {req_we, req_addr, req_wdata} !== held_request)) errors++;
request_held <= req_valid && !req_ready;
held_request <= {req_we, req_addr, req_wdata};
end
end
always @(negedge clk) begin
if (!rst_n) req_valid = 0;
else if (manual) begin end
else if (req_valid && !took) begin
// Hold every request field until acceptance, including during shutdown.
end
else if (stop) req_valid = 0;
else if (!req_valid || took) begin // previous request was taken: present the next one NOW
if ($urandom_range(0, 9) < 7) begin
req_valid = 1; req_we = $urandom_range(0, 1);
req_addr = AW'($urandom_range(0, (AW < 3) ? 2**AW-1 : 7));
req_wdata = DW'($urandom);
end else begin
req_valid = 0; req_we = $urandom_range(0, 1); req_addr = AW'($urandom); req_wdata = DW'($urandom);
end
end
end
// A real downstream register captures the pulse with no backpressure.
always_ff @(posedge clk) begin
if (!rst_n) begin consumer_data <= '0; consumer_count <= 0; end
else if (rsp_valid) begin consumer_data <= rsp_rdata; consumer_count <= consumer_count + 1; end
end
always @(posedge clk) begin
cycle++;
took <= rst_n && req_valid && req_ready;
if (!rst_n) begin
expected.delete(); due.delete(); access_due = -1;
writes = 0; reads = 0; rsp_count = 0;
// RAM contents survive reset. Only committed writes updated model[].
#1;
if (rsp_valid !== 1'b0 || mem_en !== 1'b0 || req_ready !== 1'b1) errors++;
end else begin
if (mem_en !== (cycle == access_due)) begin
errors++; $display("FAIL LAT=%0d cycle=%0d: access timing", LAT, cycle);
end
if (cycle == access_due) begin
if ({mem_we, mem_addr, mem_wdata} !== {saved_we, saved_addr, saved_wdata}) errors++;
// Update from the independently saved request only when its write commits.
if (saved_we) model[saved_addr] = saved_wdata;
access_due = -1;
end
if (due.size() != 0) begin
if ((cycle == due[0]) && rsp_valid !== 1'b1) begin
errors++; $display("FAIL LAT=%0d: missing response at edge %0d", LAT, cycle);
end
end
if (req_valid && req_ready) begin
saved_we = req_we; saved_addr = req_addr; saved_wdata = req_wdata;
access_due = cycle + 1;
if (req_we) writes++;
else begin expected.push_back(model[req_addr]); due.push_back(cycle + LAT + 1); reads++; end
end
if (rsp_valid) begin
rsp_count++;
if (expected.size() == 0) errors++;
else begin
if (cycle != due.pop_front()) errors++;
exp_val = expected.pop_front();
if (rsp_rdata !== exp_val) begin errors++; if (errors < 5) $display("FAIL: read got %h, expected %h", rsp_rdata, exp_val); end
#1;
if (consumer_data !== exp_val || consumer_count != rsp_count) errors++;
end
end
end
end
task automatic offer(input bit we, input logic [AW-1:0] addr, input logic [DW-1:0] data);
int guard;
@(negedge clk); #1; req_valid = 1; req_we = we; req_addr = addr; req_wdata = data;
guard = 0;
do begin @(posedge clk); guard++; end while (!req_ready && guard < LAT + 5);
if (!req_ready) $fatal(1, "mc_env: offer timeout LAT=%0d", LAT);
@(negedge clk); #1; req_valid = 0; req_addr = ~addr; req_wdata = ~data; req_we = !we;
endtask
task automatic reset_epoch;
rst_n = 0; req_valid = 0;
@(posedge clk); #2;
if (rsp_valid !== 1'b0 || mem_en !== 1'b0 || req_ready !== 1'b1) errors++;
@(negedge clk); #1 rst_n = 1;
repeat (LAT + 3) begin
@(posedge clk); #2;
if (rsp_valid !== 1'b0 || consumer_count != 0) errors++;
end
endtask
initial begin
errors = 0; finished = 0;
#1 rst_n = 0; @(posedge clk); #1 rst_n = 1;
// Explicit write/write/read/read ordering, same address and changed live inputs.
offer(1, AW'(1), DW'('1)); offer(1, AW'(1), DW'(1));
offer(0, AW'(1), '0); offer(0, AW'(0), '0);
repeat (LAT + 3) @(negedge clk);
if (reads != 2 || writes != 2 || rsp_count != 2) errors++;
// Cancel a read in ACCESS, each WAIT slot, and RESP before consumption.
for (int k = 0; k <= LAT; k++) begin
offer(0, AW'(1), '0);
repeat (k) @(negedge clk);
#1; reset_epoch();
offer(0, AW'(1), '0); // old RAM pipeline data must not create an extra pulse
repeat (LAT + 3) @(negedge clk);
if (rsp_count != 1 || consumer_count != 1) errors++;
end
// Cancel a write before its ACCESS commit; model and RAM retain the old value.
offer(1, AW'(1), '0);
reset_epoch();
offer(0, AW'(1), '0);
repeat (LAT + 3) @(negedge clk);
if (consumer_data !== DW'(1)) errors++;
// Start a clean accounting epoch, preserving RAM and reference contents.
reset_epoch();
manual = 0; stop = 0;
repeat (20000) @(posedge clk);
#1 stop = 1; // after the acceptance monitor and NBA updates
// Finish the active request and any held offer; RAM latency is fixed.
repeat (2 * (LAT + 3)) begin @(posedge clk); #1; end
if (req_valid || !req_ready || mem_en || rsp_valid || expected.size() != 0) errors++;
if (rsp_count !== reads) errors++;
if (ram_reads != reads || ram_writes != writes) errors++; // one enabled access per accepted request
if (reads < 1000 || writes < 1000) errors++;
if (due.size() != 0 || access_due != -1 || consumer_count != reads) errors++;
finished = 1;
end
endmodule
module tb_mc;
timeunit 1ns; timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
int e1, e2, e3, e4;
bit f1, f2, f3, f4;
mc_env #(1, 1, 1) u1(clk, e1, f1);
mc_env #(6, 8, 2) u2(clk, e2, f2);
mc_env #(3, 12, 3) u3(clk, e3, f3);
mc_env #(4, 4, 4) u4(clk, e4, f4);
initial begin
wait (f1 && f2 && f3 && f4);
if (e1 + e2 + e3 + e4 != 0) $fatal(1, "Memory sweep FAIL: %0d %0d %0d %0d", e1, e2, e3, e4);
$display("PASS: LAT=1..4, varied widths, exact access/response timing, consumer and reset epochs");
$finish;
end
initial begin
repeat (30000) @(posedge clk);
$fatal(1, "Memory sweep watchdog");
end
endmoduleA software model queues read expectations at acceptance. Writes update it at their independently predicted commit edge, from saved testbench request fields. Reset before commit cancels a write; committed RAM contents survive reset.
Each read queues an exact response edge: accept + LAT + 1 for pre-edge consumption. Missing, early, late or extra responses fail; the downstream register is checked after updates. The same contract detects the formerly invisible late-response mutation.
The RAM launches reads only on mem_en && !mem_we; enabled read and write access counts must match accepted request counts after draining.
A stability monitor requires valid and all request fields to remain unchanged until acceptance. Stop is asserted after the sampling edge, retains any pending offer, and drains both requests within a fixed latency-based bound.
Random addresses use up to eight locations, including the one-bit address boundary. Directed write/write/read/read traffic guarantees ordering cases instead of relying only on random collisions.
Directed offers poison the live inputs immediately after acceptance. Random traffic may present another offer at the next falling edge. Reset cancellation clears expected responses and accounting for that epoch while preserving committed memory contents.
Testbench: legal parameters are AW >= 1, DW >= 1 and RD_LAT >= 1. Initial diagnostics are time-zero simulation checks, not portable elaboration guarantees. The bounded sweep includes one-bit ports and latencies 1 to 4. Run with Icarus Verilog 13.0 (2026-10-16): prints PASS.