Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
    • Controller and Datapath
    • Moore vs Mealy
    • State Encoding
    • Multi-Cycle Sequencers
    • UART Transmitter
    • Memory Controller
      • Objective and Interface
      • State Machine and Bus Behavior
      • Request Latching
      • Design Closure Checklist
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Memory Controller
  1. Topics
  2. RTL Design
  3. FSM & Control
  4. Memory Controller
  5. State Machine and Bus Behavior

Part 4 · FSM & Control · Intermediate

State Machine and Bus Behavior

Four states, a write-read waveform, and the full controller with a scoreboard test.

On this page

    Why this matters

    The RAM's output holds an old word between reads, and only the value in the RESP cycle is the one you asked for. A requester that uses rsp_rdata without rsp_valid reads stale data.

    After this lesson you can: draw the four-state controller (IDLE, ACCESS, WAIT, RESP), follow a write and a read on the waveform, and say when rsp_rdata is valid.

    The four states

    Writes finish in ACCESS. Reads continue through WAIT and end with one RESP cycle.

    State

    Outputs

    Next

    IDLE

    req_ready = 1

    ACCESS when req_valid (latch the request)

    ACCESS

    mem_en = 1, mem_we = we_r

    write → IDLE; read → WAIT (or RESP if latency is 1)

    WAIT

    none

    RESP after RD_LAT − 1 cycles

    RESP

    rsp_valid = 1

    IDLE

    State diagram of the memory controller.
    Writes take one trip through ACCESS; reads add WAIT cycles and a RESP cycle.

    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.

    Common mistakes

    Tip

    Look at how rsp_rdata is a plain wire from mem_rdata. The data is valid only in the RESP cycle, which is exactly when rsp_valid is high. If the requester needs it later, it must capture it on rsp_valid.

    Common pitfall

    Only an enabled read (mem_en && !mem_we) launches data into the RAM model's output pipeline. Between reads the output may hold an old word; only the value in the RESP cycle is meaningful. Never use rsp_rdata without rsp_valid.

    Check yourself For RD_LAT = 2 and acceptance at edge 0, when are the response, consumption and next acceptance? And a write?

    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.

    Testbench

    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.

    mem_ctrl.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    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
    endmodule

    How the test works

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

    Remember

    • Writes finish in ACCESS. Reads go on through WAIT and end with one RESP cycle.

    • rsp_rdata is a plain wire from mem_rdata. It is valid only in the RESP cycle, so capture it on rsp_valid.

    • Only an enabled read (mem_en and not mem_we) launches data into the RAM's output pipeline.

    Rule of thumb: use rsp_rdata only when rsp_valid is high.

    How this was checked

    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.

    Keep going

    • Interview: run a mock interview on this topic

    Related topics

    • Request latching

    Previous

    113 / 350

    Next: Request Latching

    Up next

    Up nextRequest Latching

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Design Closure Checklist
    Write 0xA7 to address 5, then read address 5 (RAM read latency 2)clkreq_validreq_readyreq_wereq_addr05050stateIDLEACCESSIDLEACCESSWAITRESPIDLEmem_enmem_wersp_validrsp_rdataxa7
    Write 0xA7 to address 5 (accepted in cycle 1, ACCESS in cycle 2). Read of address 5 offered in cycle 4: ACCESS in cycle 5, WAIT in cycle 6, RESP in cycle 7, where rsp_valid is high and rsp_rdata is 0xA7. The requester's address inputs return to 0 after each handshake; the controller keeps using its own copy.