8 min read

SDRAM Controller FPGA Design

SDRAM Controller FPGA Design

Dec. 2023 - Aug. 2024

Introduction

In this project, I designed and implemented an SDRAM controller for interfacing with an FPGA. The goal was to enable efficient external memory access—handling both read and write operations—while ensuring correct timing and refresh behavior for the SDRAM.

Simplified block diagram of the SDRAM controller architecture.

The diagram above shows the high-level architecture of the sdram_ctrl module. It includes several submodules:

  • sdram_init handles the initialization sequence required by SDRAM before any operations can take place.
  • sdram_write and sdram_read manage the data transfers to and from memory.
  • sdram_aref ensures the SDRAM is refreshed periodically to preserve data integrity.
  • sdram_arbit coordinates the access between the different modules to avoid conflicts and ensure smooth operation.

All of this was implemented in Verilog, and I verified the design through simulation using custom test vectors. This project gave me hands-on experience with memory timing, burst access, and hardware control logic—skills that are crucial in digital system design. Now I will go in depth talk about each module.

Initial Module

  • sys_clk: 1-bit input. System clock, frequency 100 MHz.
  • sys_rst_n: 1-bit input. Reset signal, active low.
  • init_cmd: 4-bit output. Command signal output during the initialization phase.
  • init_ba: 2-bit output. L-Bank address used during the initialization phase.
  • init_addr: 13-bit output. Address bus used during initialization to assist with precharge and mode register configuration.
  • init_end: 1-bit output. Flag indicating initialization is complete, allowing SDRAM to proceed with other operations.

The sdram_init module handles the entire power-up and setup sequence required by SDRAM before any read or write operations can happen. SDRAMs aren’t ready to go right after power-on—they require a specific set of commands issued in a strict order with timing delays between them.

Here’s how the module works:

  • When the system starts, the controller waits for 200 microseconds to let the SDRAM stabilize.
  • It then moves through a sequence of state transitions, each responsible for sending a command:
    1. Precharge – preps the SDRAM by closing all open rows.
    2. Auto-refresh – repeated 8 times to refresh internal cells.
    3. Mode Register Set (MRS) – configures SDRAM settings like CAS latency, burst length, and access type.
  • Each state includes built-in wait cycles (e.g., tRP, tRC, tMRD) to satisfy SDRAM timing requirements.
  • Once initialization is complete, the init_end signal goes high, telling the rest of the system that the SDRAM is ready for normal use.

Key outputs like init_cmd, init_ba, and init_addr are used to drive the SDRAM’s command bus during this sequence. Internally, the module uses counters and flags to track wait times and state transitions.

Pseudocode for Initialization FSM

IF reset is active:
  state ← INIT_IDLE
ELSE:
  SWITCH (state):
CASE INIT_IDLE:
  WAIT for 200us
→ INIT_PRE
    CASE INIT_PRE:
        issue PRECHARGE command
        → INIT_TRP
    CASE INIT_TRP:
        WAIT tRP cycles
        → INIT_AR
    CASE INIT_AR:
        issue AUTO-REFRESH command
        → INIT_TRF
    CASE INIT_TRF:
        WAIT tRC cycles
        IF refresh count == 8:
            → INIT_MRS
        ELSE:
            → INIT_AR
    CASE INIT_MRS:
        issue MODE REGISTER SET command
        → INIT_TMRD
    CASE INIT_TMRD:
        WAIT tMRD cycles
        → INIT_END
    CASE INIT_END:
        assert init_end = 1
        stay here

AutoRefresh Module

  • sys_clk: 1-bit input. System clock, frequency 100 MHz.
  • sys_rst_n: 1-bit input. Reset signal, active low.
  • init_end: 1-bit input. Signal indicating initialization is complete.
  • aref_en: 1-bit input. Auto-refresh enable signal.
  • aref_req: 1-bit output. Auto-refresh request signal.
  • aref_cmd: 4-bit output. Auto-refresh phase command signal.
  • aref_ba: 2-bit output. L-Bank address for auto-refresh phase.
  • aref_addr: 13-bit output. Address bus for auto-refresh phase.
  • aref_end: 1-bit output. Signal indicating end of auto-refresh phase.

How it work?

  • After initialization (init_end), the module waits for 7.5 µs (CNT_REF_MAX) before sending a refresh request.
  • When the time is up, it raises aref_req to start a refresh.
  • The module then goes through a small FSM (finite state machine):

States:

  1. AREF_IDLE: Waits for refresh to be enabled.
  2. AREF_PCHA: Issues a precharge command.
  3. AREF_TRP: Waits tRP cycles after precharge.
  4. AUTO_REF: Sends the auto-refresh command.
  5. AREF_TRF: Waits tRC cycles after refresh.
  6. AREF_END: Refresh complete → back to IDLE.

Pseudocode for AutoRefresh

On power-up:
    Wait until init_end and aref_en are both high

Every 7.5 µs (based on cnt_aref):
    Trigger aref_req to start refresh

Refresh Sequence:
    → Precharge             // closes any open rows before refresh
    → Wait tRP cycles       // tRP = Row Precharge time (minimum delay before next command)

    → Auto-Refresh command  // triggers internal SDRAM refresh
    → Wait tRC cycles       // tRC = Row Cycle time (minimum time between refresh commands)

    → Repeat Auto-Refresh (total 2 times per cycle)
    → Set aref_end = 1      // marks end of the refresh operation

Write Module

  • sys_clk: 1-bit input. System clock, frequency 100 MHz.
  • sys_rst_n: 1-bit input. Reset signal, active low.
  • init_end: 1-bit input. Signal indicating initialization is complete.
  • wr_en: 1-bit input. Write enable signal for data.
  • wr_addr: 24-bit input. Address input for data write stage.
  • wr_data: 16-bit input. Data input for data write stage.
  • wr_burst_len: 10-bit input. Burst length for write operation.
  • wr_ack: 1-bit output. Acknowledge signal for write operation.
  • wr_end: 1-bit output. Signal indicating end of a single write burst.
  • write_cmd: 4-bit output. Command signal for data write stage.
  • write_ba: 2-bit output. Logical bank address for data write stage.
  • write_addr: 13-bit output. Address output for data write stage.
  • wr_sdram_en: 1-bit output. SDRAM output enable for data write stage.
  • wr_sdram_data: 16-bit output. SDRAM data output for data write stage.

This module handles writing data from the FPGA to SDRAM in burst mode. It's triggered when wr_en is asserted and init_end confirms the SDRAM is ready.

The module walks through a finite state machine that performs all necessary steps for a safe and valid SDRAM write transaction, including activation, writing, and precharge.

FSM Main States:

WR_IDLE → Wait for write enable (wr_en) and init_end
WR_ACTIVE → Activate the target row (ACTIVE command)
WR_TRCD → Wait tRCD (row-to-column delay)
WR_WRITE → Issue WRITE command to begin burst write
WR_DATA → Output actual data for N clock cycles
WR_PRE → Issue PRECHARGE command to close the row
WR_TRP → Wait tRP (precharge time)
WR_END → Write complete, go back to IDLE

Outputs:

write_cmd: Command sent to SDRAM (ACTIVE, WRITE, P_CHARGE, etc.)

write_ba: Bank address of the SDRAM

write_addr: Row/column address (derived from wr_addr)

wr_ack: Indicates write is acknowledged (ready to send data)

wr_end: Pulse that signals the burst write has finished

wr_sdram_en: Enables SDRAM data output

wr_sdram_data: The data being sent to SDRAM (driven by wr_data)

Pseudocode for Write

IF reset:
    state ← WR_IDLE

FSM:
    WR_IDLE:
        IF wr_en && init_end → WR_ACTIVE

    WR_ACTIVE:
        Send ACTIVE command
        → WR_TRCD

    WR_TRCD:
        Wait tRCD cycles
        → WR_WRITE

    WR_WRITE:
        Send WRITE command
        → WR_DATA

    WR_DATA:
        FOR wr_burst_len cycles:
            Output wr_data to SDRAM
        After burst ends → WR_PRE

    WR_PRE:
        Send PRECHARGE command
        → WR_TRP

    WR_TRP:
        Wait tRP cycles
        → WR_END

    WR_END:
        → Return to WR_IDLE

Read Module

  • sys_clk: 1-bit input. System clock, frequency 100 MHz.
  • sys_rst_n: 1-bit input. Reset signal, active low.
  • init_end: 1-bit input. Signal indicating initialization is complete.
  • rd_en: 1-bit input. Data read enable signal.
  • rd_addr: 24-bit input. Address input for the data read stage.
  • rd_data: 16-bit input. Data input for the read stage (usually from FIFO or intermediate logic).
  • rd_burst_len: 10-bit input. Burst length for the read operation.
  • rd_ack: 1-bit output. Acknowledge signal for read operation.
  • rd_end: 1-bit output. Signal indicating end of a burst read.
  • read_cmd: 4-bit output. Command signal during data read stage.
  • read_ba: 2-bit output. Logical L-Bank address during data read stage.
  • read_addr: 13-bit output. Address output for data read stage.
  • rd_sdram_data: 16-bit output. Data output from SDRAM during the read phase.

This module manages burst read operations from SDRAM. It waits until the SDRAM is initialized, then responds to a read enable (rd_en) signal, fetches data from SDRAM, and outputs it via rd_sdram_data.

FSM Flow:

RD_IDLE → Wait for rd_en + init_end
RD_ACTIVE → Send ACTIVE command to open the row
RD_TRCD → Wait tRCD (Row to Column Delay)
RD_READ → Issue READ command
RD_CL → Wait tCL (CAS Latency)
RD_DATA → Read data out for rd_burst_len cycles
RD_PRE → Issue PRECHARGE command to close the row
RD_TRP → Wait tRP (Precharge time)
RD_END → Done; return to IDLE

Pseudocode for Read

IF reset:
    state ← RD_IDLE

FSM:
    RD_IDLE:
        IF rd_en AND init_end → RD_ACTIVE

    RD_ACTIVE:
        Send ACTIVE command to select row
        → RD_TRCD

    RD_TRCD:
        Wait tRCD cycles
        → RD_READ

    RD_READ:
        Send READ command
        → RD_CL

    RD_CL:
        Wait tCL cycles (CAS Latency)
        → RD_DATA

    RD_DATA:
        Output rd_data to rd_sdram_data
        FOR rd_burst_len cycles
        → RD_PRE

    RD_PRE:
        Send PRECHARGE to close row
        → RD_TRP

    RD_TRP:
        Wait tRP
        → RD_END

    RD_END:
        → Return to RD_IDLE

Arbit Module

Signal Width Direction Description
sys_clk 1 Bit Input System clock, 100 MHz
sys_rst_n 1 Bit Input Reset signal, active low
init_cmd 4 Bit Input Initialization command
init_ba 2 Bit Input Bank address during initialization
init_addr 13 Bit Input Address bus during initialization
init_end 1 Bit Input Initialization complete signal
aref_req 1 Bit Input Auto-refresh request signal
aref_cmd 4 Bit Input Auto-refresh command
aref_ba 2 Bit Input Bank address during auto-refresh
aref_addr 13 Bit Input Address bus during auto-refresh
aref_end 1 Bit Input Auto-refresh complete signal
wr_req 1 Bit Input Write request signal
wr_cmd 4 Bit Input Write command
wr_ba 2 Bit Input Write bank address
wr_addr 13 Bit Input Write address bus
wr_sdram_en 1 Bit Input SDRAM data output enable (for write)
wr_data 16 Bit Input Data to write to SDRAM
wr_end 1 Bit Input Write burst complete signal
rd_req 1 Bit Input Read request signal
rd_cmd 4 Bit Input Read command
rd_ba 2 Bit Input Read bank address
rd_addr 13 Bit Input Read address bus
rd_end 1 Bit Input Read burst complete signal
aref_en 1 Bit Output Auto-refresh enable
wr_en 1 Bit Output Write enable
rd_en 1 Bit Output Read enable
sdram_cke 1 Bit Output SDRAM clock enable
sdram_cs_n 1 Bit Output SDRAM chip select (active low)
sdram_cas_n 1 Bit Output SDRAM column address strobe (active low)
sdram_ras_n 1 Bit Output SDRAM row address strobe (active low)
sdram_we_n 1 Bit Output SDRAM write enable (active low)
sdram_ba 2 Bit Output SDRAM bank address
sdram_addr 13 Bit Output SDRAM address bus
sdram_dq 16 Bit Output SDRAM data bus

This is the arbiter module that controls access to the SDRAM by coordinating which module (initialization, refresh, write, or read) can drive the SDRAM command/address/data lines at any moment. Only one can access it at a time.


It implements a priority-based FSM to decide who gets access to the SDRAM:

Priority order:

  1. Initialization
  2. Auto-refresh
  3. Write
  4. Read
StateMeaning
IDLEWaiting for initialization to finish
ARBITArbitration decision state
AREFHandles auto-refresh
WRITEHandles write burst
READHandles read burst
Initial State:
    If init_end == 0 → Stay in IDLE
    If init_end == 1 → Go to ARBIT

ARBITRATION:
    If aref_req → AREF
    Else if wr_req → WRITE
    Else if rd_req → READ
    Else → Stay in ARBIT

AREF:
    If aref_end → Return to ARBIT

WRITE:
    If wr_end → Return to ARBIT

READ:
    If rd_end → Return to ARBIT

Always drive SDRAM outputs with:
    - current state’s command (init_cmd / aref_cmd / wr_cmd / rd_cmd)
    - bank address and row/col address

This module ensures conflict-free SDRAM access. For example:

  • You don't want a write to interrupt an auto-refresh.
  • You don't want both read and write trying to drive sdram_dq.

It simplifies the system by putting access control logic in one place and makes sure the SDRAM always receives valid timing-respecting commands.

Conclusion

Through this project, I successfully designed and implemented a fully functional SDRAM controller capable of handling initialization, auto-refresh, read, and write operations on an FPGA platform. By breaking down the controller into modular components—such as sdram_init, sdram_a_ref, sdram_write, sdram_read, and sdram_arbit—I was able to manage complex timing and command sequences required by SDRAM protocols.