Memory & FIFO Projects for VLSI Engineers

Master CDC-Safe Designs with Complete Verilog Implementations

Praveen Kumar Vagala | 12 min read

1000

Introduction

Memory and FIFO designs are critical components in any digital system. FIFOs handle data buffering between different clock domains or processing units, while RAM/ROM provide storage. This blog covers synchronous and asynchronous FIFOs with CDC-safe implementations.

Table of Contents

  1. Synchronous FIFO Design
  2. Asynchronous FIFO (CDC-Safe)
  3. Dual-Port RAM Controller
  4. ROM with Multiple Address Modes

1. Synchronous FIFO Design

Difficulty: Beginner | Key Learning: Single clock domain, pointer management

Concept

A synchronous FIFO operates on a single clock domain. Both read and write operations are synchronized to the same clock.

Block Diagram

+---------------------------+ wr_en -------->| | wr_data[N] --->| Synchronous FIFO |-------> rd_data[N] | | rd_en -------->| +---------------+ |-------> empty | | Memory | |-------> full clk ---------->| | Array | |-------> almost_empty rst_n -------->| +---------------+ |-------> almost_full | | +---------------------------+

Verilog Code

module sync_fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 16,
    parameter ADDR_WIDTH = $clog2(DEPTH)
)(
    input  wire                    clk,
    input  wire                    rst_n,
    
    // Write interface
    input  wire                    wr_en,
    input  wire [DATA_WIDTH-1:0]   wr_data,
    
    // Read interface
    input  wire                    rd_en,
    output wire [DATA_WIDTH-1:0]   rd_data,
    
    // Status flags
    output wire                    empty,
    output wire                    full,
    output wire                    almost_empty,
    output wire                    almost_full,
    output wire [ADDR_WIDTH:0]     count
);

    // Memory array
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    
    // Pointers
    reg [ADDR_WIDTH:0] wr_ptr;
    reg [ADDR_WIDTH:0] rd_ptr;
    
    // FIFO count
    wire [ADDR_WIDTH:0] fifo_count;
    assign fifo_count = wr_ptr - rd_ptr;
    assign count = fifo_count;
    
    // Status flags
    assign empty        = (fifo_count == 0);
    assign full         = (fifo_count == DEPTH);
    assign almost_empty = (fifo_count <= 1);
    assign almost_full  = (fifo_count >= DEPTH - 1);
    
    // Write operation
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr <= 0;
        end else if (wr_en && !full) begin
            mem[wr_ptr[ADDR_WIDTH-1:0]] <= wr_data;
            wr_ptr <= wr_ptr + 1;
        end
    end
    
    // Read operation
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rd_ptr <= 0;
        end else if (rd_en && !empty) begin
            rd_ptr <= rd_ptr + 1;
        end
    end
    
    // Read data (combinational)
    assign rd_data = mem[rd_ptr[ADDR_WIDTH-1:0]];

endmodule

Testbench

module tb_sync_fifo;
    parameter DATA_WIDTH = 8;
    parameter DEPTH = 8;
    
    reg                    clk, rst_n;
    reg                    wr_en, rd_en;
    reg  [DATA_WIDTH-1:0]  wr_data;
    wire [DATA_WIDTH-1:0]  rd_data;
    wire                   empty, full;
    
    sync_fifo #(.DATA_WIDTH(DATA_WIDTH), .DEPTH(DEPTH)) uut (.*);
    
    always #5 clk = ~clk;
    
    initial begin
        clk = 0; rst_n = 0; wr_en = 0; rd_en = 0;
        #20 rst_n = 1;
        
        // Write data
        repeat(5) begin
            @(posedge clk);
            wr_en = 1;
            wr_data = $random;
        end
        @(posedge clk);
        wr_en = 0;
        
        // Read data
        repeat(5) begin
            @(posedge clk);
            rd_en = 1;
            $display("Read: %h", rd_data);
        end
        @(posedge clk);
        rd_en = 0;
        
        #50 $finish;
    end
endmodule

Common Mistakes to Avoid

  1. Not handling simultaneous read/write - Define behavior clearly
  2. Overflow/underflow conditions - Check full/empty before operations
  3. Incorrect pointer width - Use $clog2 for address width

2. Asynchronous FIFO (CDC-Safe)

Difficulty: Advanced | Key Learning: Gray code, 2-FF synchronizers, CDC

Concept

An asynchronous FIFO bridges two different clock domains. It uses Gray code pointers and dual-flop synchronizers for safe CDC (Clock Domain Crossing).

Why Gray Code?

Gray code changes only one bit at a time, preventing metastability issues when synchronizing multi-bit pointers across clock domains.

Binary: 000 -> 001 -> 010 -> 011 -> 100 -> 101 -> 110 -> 111 Gray: 000 -> 001 -> 011 -> 010 -> 110 -> 111 -> 101 -> 100 ^ ^ ^ ^ ^ ^ ^ (only 1 bit changes at each transition)

Block Diagram

Write Clock Domain | Read Clock Domain | wr_clk wr_en wr_data | rd_clk rd_en | | | | | | v v v | v v +----------------------+ | +----------------------+ | Write Pointer | | | Read Pointer | | (Binary -> Gray) | | | (Binary -> Gray) | +----------+-----------+ | +----------+-----------+ | | | | Gray Ptr | | Gray Ptr v | v +----------------------+ | +----------------------+ | 2-FF Synchronizer |<---| | 2-FF Synchronizer | | (rd_ptr to wr_clk) | |--->| (wr_ptr to rd_clk) | +----------------------+ | +----------------------+ | | | v | v full flag | empty flag | +--------Memory--------+ | (Dual-Port) | +----------------------+

Verilog Code

// Gray code converter
module bin2gray #(parameter WIDTH = 4) (
    input  wire [WIDTH-1:0] bin,
    output wire [WIDTH-1:0] gray
);
    assign gray = bin ^ (bin >> 1);
endmodule

module gray2bin #(parameter WIDTH = 4) (
    input  wire [WIDTH-1:0] gray,
    output wire [WIDTH-1:0] bin
);
    genvar i;
    generate
        for (i = 0; i < WIDTH; i = i + 1) begin : gen_bin
            assign bin[i] = ^gray[WIDTH-1:i];
        end
    endgenerate
endmodule

// 2-FF Synchronizer
module sync_2ff #(parameter WIDTH = 4) (
    input  wire             clk,
    input  wire             rst_n,
    input  wire [WIDTH-1:0] din,
    output reg  [WIDTH-1:0] dout
);
    reg [WIDTH-1:0] meta;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            meta <= 0;
            dout <= 0;
        end else begin
            meta <= din;
            dout <= meta;
        end
    end
endmodule

// Asynchronous FIFO
module async_fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 16,
    parameter ADDR_WIDTH = $clog2(DEPTH)
)(
    // Write clock domain
    input  wire                    wr_clk,
    input  wire                    wr_rst_n,
    input  wire                    wr_en,
    input  wire [DATA_WIDTH-1:0]   wr_data,
    output wire                    full,
    
    // Read clock domain
    input  wire                    rd_clk,
    input  wire                    rd_rst_n,
    input  wire                    rd_en,
    output wire [DATA_WIDTH-1:0]   rd_data,
    output wire                    empty
);

    localparam PTR_WIDTH = ADDR_WIDTH + 1;
    
    // Dual-port memory
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    
    // Write domain signals
    reg  [PTR_WIDTH-1:0] wr_ptr_bin;
    wire [PTR_WIDTH-1:0] wr_ptr_gray;
    wire [PTR_WIDTH-1:0] rd_ptr_gray_sync;
    
    // Read domain signals
    reg  [PTR_WIDTH-1:0] rd_ptr_bin;
    wire [PTR_WIDTH-1:0] rd_ptr_gray;
    wire [PTR_WIDTH-1:0] wr_ptr_gray_sync;
    
    // Binary to Gray conversion
    bin2gray #(PTR_WIDTH) wr_b2g (.bin(wr_ptr_bin), .gray(wr_ptr_gray));
    bin2gray #(PTR_WIDTH) rd_b2g (.bin(rd_ptr_bin), .gray(rd_ptr_gray));
    
    // Synchronizers
    sync_2ff #(PTR_WIDTH) sync_rd_ptr (
        .clk(wr_clk), .rst_n(wr_rst_n),
        .din(rd_ptr_gray), .dout(rd_ptr_gray_sync)
    );
    
    sync_2ff #(PTR_WIDTH) sync_wr_ptr (
        .clk(rd_clk), .rst_n(rd_rst_n),
        .din(wr_ptr_gray), .dout(wr_ptr_gray_sync)
    );
    
    // Full flag generation (write domain)
    assign full = (wr_ptr_gray == {~rd_ptr_gray_sync[PTR_WIDTH-1:PTR_WIDTH-2], 
                                    rd_ptr_gray_sync[PTR_WIDTH-3:0]});
    
    // Empty flag generation (read domain)
    assign empty = (rd_ptr_gray == wr_ptr_gray_sync);
    
    // Write logic
    always @(posedge wr_clk or negedge wr_rst_n) begin
        if (!wr_rst_n) begin
            wr_ptr_bin <= 0;
        end else if (wr_en && !full) begin
            mem[wr_ptr_bin[ADDR_WIDTH-1:0]] <= wr_data;
            wr_ptr_bin <= wr_ptr_bin + 1;
        end
    end
    
    // Read logic
    always @(posedge rd_clk or negedge rd_rst_n) begin
        if (!rd_rst_n) begin
            rd_ptr_bin <= 0;
        end else if (rd_en && !empty) begin
            rd_ptr_bin <= rd_ptr_bin + 1;
        end
    end
    
    assign rd_data = mem[rd_ptr_bin[ADDR_WIDTH-1:0]];

endmodule

✅ CDC Verification Checklist

  1. Gray code pointers (single bit change)
  2. 2-FF synchronizers for pointer crossing
  3. Full generated in write domain
  4. Empty generated in read domain
  5. FIFO depth must be power of 2

Interview Questions

  1. Why Gray code for async FIFO? - Only one bit changes at a time, preventing intermediate wrong values during synchronization
  2. Why 2-FF synchronizer? - Reduces probability of metastability to acceptable levels
  3. Can full/empty flags be wrong? - They can be pessimistic (show full when not quite full) but never optimistic - this is safe behavior

3. Dual-Port RAM Controller

Difficulty: Intermediate | Key Learning: Two independent ports, simultaneous access

Concept

Dual-port RAM allows simultaneous read and write operations from two independent ports.

Verilog Code

module dual_port_ram #(
    parameter DATA_WIDTH = 32,
    parameter ADDR_WIDTH = 10,
    parameter DEPTH      = 1024
)(
    // Port A
    input  wire                    clk_a,
    input  wire                    en_a,
    input  wire                    wr_en_a,
    input  wire [ADDR_WIDTH-1:0]   addr_a,
    input  wire [DATA_WIDTH-1:0]   wr_data_a,
    output reg  [DATA_WIDTH-1:0]   rd_data_a,
    
    // Port B
    input  wire                    clk_b,
    input  wire                    en_b,
    input  wire                    wr_en_b,
    input  wire [ADDR_WIDTH-1:0]   addr_b,
    input  wire [DATA_WIDTH-1:0]   wr_data_b,
    output reg  [DATA_WIDTH-1:0]   rd_data_b
);

    // Memory array
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    
    // Port A operations
    always @(posedge clk_a) begin
        if (en_a) begin
            if (wr_en_a)
                mem[addr_a] <= wr_data_a;
            rd_data_a <= mem[addr_a];
        end
    end
    
    // Port B operations
    always @(posedge clk_b) begin
        if (en_b) begin
            if (wr_en_b)
                mem[addr_b] <= wr_data_b;
            rd_data_b <= mem[addr_b];
        end
    end

endmodule

True Dual-Port vs Simple Dual-Port

Feature True Dual-Port Simple Dual-Port
Port A Read/Write Write only
Port B Read/Write Read only
Complexity Higher Lower
Use case Shared memory FIFO backing store

4. ROM with Multiple Address Modes

Difficulty: Intermediate | Key Learning: Flexible addressing patterns

Concept

ROM stores pre-initialized data. Multiple address modes allow flexible access patterns (direct, indexed, indirect).

Verilog Code

module rom_multi_mode #(
    parameter DATA_WIDTH = 16,
    parameter ADDR_WIDTH = 8,
    parameter DEPTH      = 256
)(
    input  wire                    clk,
    input  wire                    en,
    input  wire [1:0]              addr_mode,
    input  wire [ADDR_WIDTH-1:0]   base_addr,
    input  wire [ADDR_WIDTH-1:0]   offset,
    input  wire [ADDR_WIDTH-1:0]   indirect_addr,
    output reg  [DATA_WIDTH-1:0]   data_out,
    output reg                     valid
);

    // Address modes
    localparam DIRECT   = 2'b00;  // Use base_addr directly
    localparam INDEXED  = 2'b01;  // base_addr + offset
    localparam INDIRECT = 2'b10;  // Address stored at indirect_addr
    localparam AUTO_INC = 2'b11;  // Auto-increment mode
    
    // ROM memory (initialized)
    reg [DATA_WIDTH-1:0] rom [0:DEPTH-1];
    
    // Auto-increment counter
    reg [ADDR_WIDTH-1:0] auto_ptr;
    
    // Effective address
    reg [ADDR_WIDTH-1:0] eff_addr;
    wire [ADDR_WIDTH-1:0] indirect_data;
    
    // Initialize ROM with sample data
    initial begin
        integer i;
        for (i = 0; i < DEPTH; i = i + 1) begin
            rom[i] = i * 2;  // Sample initialization
        end
    end
    
    // For indirect mode, first read gives address
    assign indirect_data = rom[indirect_addr][ADDR_WIDTH-1:0];
    
    // Address calculation
    always @(*) begin
        case (addr_mode)
            DIRECT:   eff_addr = base_addr;
            INDEXED:  eff_addr = base_addr + offset;
            INDIRECT: eff_addr = indirect_data;
            AUTO_INC: eff_addr = auto_ptr;
            default:  eff_addr = base_addr;
        endcase
    end
    
    // Read operation
    always @(posedge clk) begin
        if (en) begin
            data_out <= rom[eff_addr];
            valid <= 1'b1;
            
            // Auto-increment pointer
            if (addr_mode == AUTO_INC)
                auto_ptr <= auto_ptr + 1;
        end else begin
            valid <= 1'b0;
        end
    end
    
    // Reset auto pointer
    always @(posedge clk) begin
        if (addr_mode != AUTO_INC)
            auto_ptr <= base_addr;
    end

endmodule

Address Mode Examples

DIRECT Mode: base_addr = 0x10 --> Read from ROM[0x10] INDEXED Mode: base_addr = 0x10, offset = 0x05 --> Read from ROM[0x15] INDIRECT Mode: indirect_addr = 0x20 ROM[0x20] = 0x50 --> Read from ROM[0x50] AUTO_INC Mode: First read: ROM[base_addr] Next read: ROM[base_addr + 1] Next read: ROM[base_addr + 2] ...

Summary

Design Key Concept CDC Safe?
Sync FIFO Single clock, simple pointers N/A
Async FIFO Gray code, 2-FF sync Yes
Dual-Port RAM Two independent ports Depends
Multi-Mode ROM Flexible addressing N/A

More Interview Questions

  1. Why is FIFO depth always power of 2? - For proper Gray code wrapping and pointer comparison
  2. What happens if 2-FF sync captures metastable value? - Second FF has full clock period to resolve; probability of failure is extremely low (MTBF in years)
  3. How to handle simultaneous read/write to same address in dual-port RAM? - Define priority (write-first, read-first, or no-change mode)
  4. What is CDC? - Clock Domain Crossing - transferring signals between different clock domains

Next Steps

Continue your VLSI learning journey with the complete blog series:

← Previous: Core RTL Design Next: Communication & Peripherals →
#Verilog #FIFO #CDC #Memory #Gray Code #VLSI #FPGA #ASIC