Skip to content

VHDL · Chapter 20.5 · Capstone Projects

Capstone: Memory Controller

The fifth capstone is the most timing-aware block yet, a memory controller that sequences an external SRAM-like memory. Unlike the on-chip arrays you have inferred, an external memory has a bus protocol with timing. You must drive address, data, and control signals such as chip-enable, write-enable, and output-enable in the right order and hold them for the memory's required cycles, often inserting wait states for slow devices, and capture read data at exactly the right moment. The controller is a textbook datapath and control design. A command interface accepts read and write requests, and a control state machine steps through idle, setup, access, and capture phases to drive the memory bus. Generics set the address width, data width, and access latency. Verification covers read-after-write ordering, wait-state counts, back-to-back accesses, and boundary addresses.

Intermediate16 min readVHDLCapstoneMemory ControllerFSMTimingBus Protocol

1. Engineering intuition — a state machine that obeys the memory's timing contract

An external memory is a device with a timing contract: to write, you must present the address, present the data, and pulse write-enable, each held for a minimum time; to read, you assert the address and output-enable, wait the memory's access latency, then capture the data on the right edge. The controller's whole job is to honor that contract — drive the bus signals in the correct sequence and hold them long enough, inserting wait states when the memory is slower than your clock. So the natural structure is a control FSM stepping through the phases of each access while a small datapath routes address and data. Picture a state machine that walks setup → access → (wait) → capture for each request, driving the memory's control pins on cue — that is the controller.

2. Formal explanation — the memory controller architecture

mem_ctrl_architecture.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
entity mem_ctrl is
    generic ( ADDR_W : positive := 16; DATA_W : positive := 16; RD_LATENCY : positive := 2 );
    port ( clk, rst : in  std_logic;
           -- command interface (handshake)
           req      : in  std_logic;  we_cmd : in std_logic;  ack : out std_logic;
           addr_in  : in  std_logic_vector(ADDR_W-1 downto 0);
           wdata    : in  std_logic_vector(DATA_W-1 downto 0);
           rdata    : out std_logic_vector(DATA_W-1 downto 0);  rvalid : out std_logic;
           -- external memory bus
           mem_addr : out std_logic_vector(ADDR_W-1 downto 0);
           mem_data_o : out std_logic_vector(DATA_W-1 downto 0);
           mem_data_i : in  std_logic_vector(DATA_W-1 downto 0);
           mem_ce_n, mem_we_n, mem_oe_n : out std_logic );
end entity;
-- CONTROL FSM sequences each access (datapath/control, 18.5):
--   IDLE  -> on req: latch addr/we/wdata, drive mem_addr + mem_ce_n='0'
--   WRITE : drive mem_data_o + pulse mem_we_n low for the required time -> ACK
--   READ  : assert mem_oe_n='0', WAIT RD_LATENCY cycles (wait states), then CAPTURE mem_data_i -> rvalid
--   back to IDLE.

The controller is a command interface (req/ack, we_cmd) + a control FSM (idle/write/read with wait states) + a small datapath (address/data routing) driving the memory bus (ce_n, we_n, oe_n). Generics set the address/data widths and the read latency. The FSM exists to honor the memory's timing.

3. Production usage — write pulse and read capture with wait states

mem_ctrl_cycles.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
process (clk) begin
    if rising_edge(clk) then
        if rst = '1' then state <= IDLE; mem_ce_n <= '1'; mem_we_n <= '1'; mem_oe_n <= '1'; ack <= '0'; rvalid <= '0';
        else
            ack <= '0'; rvalid <= '0';
            case state is
                when IDLE =>
                    mem_ce_n <= '1'; mem_we_n <= '1'; mem_oe_n <= '1';
                    if req = '1' then
                        mem_addr <= addr_in; mem_ce_n <= '0';
                        if we_cmd = '1' then mem_data_o <= wdata; state <= WRITE; else mem_oe_n <= '0'; wcnt <= 0; state <= READ; end if;
                    end if;
                when WRITE =>
                    mem_we_n <= '0';                              -- assert write-enable (hold per timing)
                    state <= WR_DONE;
                when WR_DONE =>
                    mem_we_n <= '1'; mem_ce_n <= '1'; ack <= '1'; state <= IDLE;   -- end the write pulse
                when READ =>
                    if wcnt = RD_LATENCY-1 then                  -- WAIT STATES for access latency
                        rdata <= mem_data_i;                     -- CAPTURE at the right cycle
                        rvalid <= '1'; mem_oe_n <= '1'; mem_ce_n <= '1'; state <= IDLE;
                    else
                        wcnt <= wcnt + 1;
                    end if;
            end case;
        end if;
    end if;
end process;

What hardware does this become? A control FSM plus bus-driver logic: for a write, it presents address and data and pulses we_n low for the required duration; for a read, it asserts oe_n, counts RD_LATENCY wait states, then captures mem_data_i on the correct cycle and pulses rvalid. The datapath is just registers routing address/data; the timing is everything — the FSM holds each signal long enough and captures read data at exactly the right moment. Generics make it serve different memory widths and a faster or slower device (more or fewer wait states). This is the datapath/control pattern applied to an external timing contract.

4. Structural interpretation — the memory controller

memory controller: command interface into a control FSM driving the external memory bus with timingrequestdrive + timeread datacommand interfacereq/ack, we_cmd, addr,wdata/rdatacontrol FSMidle -> setup -> access ->capture (+wait states)external memory busaddr, data, ce_n/we_n/oe_n12
A memory controller accepts read and write requests on a command interface and drives an external memory bus through a control FSM that honors the memory's timing. On a request it latches the address, write flag, and write data, then sequences the access: a write presents data and pulses write-enable for the required time; a read asserts output-enable, counts the memory's access latency as wait states, and captures the read data on the correct cycle, returning it with a valid pulse. A small datapath routes address and data; generics set the address and data widths and the read latency. This is the integrated controller structure; the waveform below shows a write cycle's enable pulse and a read cycle with wait states and the capture point.

5. Simulation interpretation — a write cycle and a read cycle with wait states

Write (we_n pulse) then read (oe_n + wait states + capture)

10 cycles
Write (we_n pulse) then read (oe_n + wait states + capture)WRITE: address A0 stable, mem_we_n pulsed low to write wdataWRITE: address A0 stab…READ: address A1, mem_oe_n asserted; controller counts wait statesREAD: address A1, mem_…after RD_LATENCY wait states, capture mem_data_i -> rdata, pulse rvalidafter RD_LATENCY wait …clkmem_addr0A0A0A0A1A1A1A1A1A1mem_we_n1101111111mem_oe_n1111000111rdata000000D1D1D1D1rvalid0000001000t0t1t2t3t4t5t6t7t8t9
The write cycle presents the address and pulses mem_we_n low long enough to commit the data. The read cycle asserts mem_oe_n, then the controller waits the memory's access latency (the wait states) before capturing the data on the correct cycle and returning it with rvalid. Honoring the hold times and capturing read data at exactly the right moment — neither too early nor too late — is the entire correctness question for an external memory controller.

6. Debugging example — capturing read data too early (and a short write pulse)

Expected: reliable reads and writes to the external memory. Observed: reads return stale or wrong data (especially on a slower memory), and some writes do not commit, with both worsening as the memory's timing tightens. Root cause: the controller captured read data before the memory's access latency elapsed (too few wait states), so it latched the bus before the data was valid; and/or the write-enable pulse was too short, not held for the memory's required write time, so the write did not commit. Fix: count the memory's read latency as wait states and capture mem_data_i only on the cycle the data is guaranteed valid, and hold the write-enable pulse (and address/data) for the device's required write time — both parameterized by generics (RD_LATENCY, write hold). Engineering takeaway: an external memory controller must obey the device's timing — insert enough wait states to capture read data when it is valid, and hold the write pulse long enough to commit; capturing early or pulsing short corrupts reads and writes.

mem_ctrl_timing.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- BUG: capture read data immediately (no wait states) + one-cycle we pulse -> stale reads, lost writes.
-- FIX: count RD_LATENCY wait states before capture; hold we_n low for the required write time.
-- when READ => if wcnt = RD_LATENCY-1 then rdata <= mem_data_i; rvalid <= '1'; ... else wcnt <= wcnt+1; end if;

7. Common mistakes & what to watch for

  • Capturing read data too early. Count the memory's access latency as wait states and capture only when the data is valid; early capture latches garbage.
  • Write pulse too short. Hold write-enable (and address/data) for the device's required write time, or the write does not commit.
  • Hard-coded latency. Parameterize timing with generics (read latency, hold times) so one controller serves faster and slower memories.
  • Glitchy control signals. Drive ce_n/we_n/oe_n cleanly from the FSM (registered), respecting setup/hold — glitches can corrupt accesses.
  • Verifying only single accesses. Test read-after-write ordering, varying wait-state counts, back-to-back accesses, and boundary addresses.

8. Engineering insight & continuity

The memory controller capstone is datapath/control built around an external timing contract: a command interface, a control FSM that sequences read/write cycles with the right signal ordering and wait states, correct read-data capture and write-enable pulses, and generics for widths and latency — verified across read-after-write, wait-state, and boundary cases. It is the model for any block that talks to a timed external device. The final capstone brings all the blocks together into a working subsystem — the closing lesson, Capstone: Integrating a Small SoC Block, which composes these components into one verified design and completes the curriculum.