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.
Difficulty: Beginner | Key Learning: Single clock domain, pointer management
A synchronous FIFO operates on a single clock domain. Both read and write operations are synchronized to the same clock.
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
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
Difficulty: Advanced | Key Learning: Gray code, 2-FF synchronizers, CDC
An asynchronous FIFO bridges two different clock domains. It uses Gray code pointers and dual-flop synchronizers for safe CDC (Clock Domain Crossing).
Gray code changes only one bit at a time, preventing metastability issues when synchronizing multi-bit pointers across clock domains.
// 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
Difficulty: Intermediate | Key Learning: Two independent ports, simultaneous access
Dual-port RAM allows simultaneous read and write operations from two independent ports.
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
| 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 |
Difficulty: Intermediate | Key Learning: Flexible addressing patterns
ROM stores pre-initialized data. Multiple address modes allow flexible access patterns (direct, indexed, indirect).
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
| 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 |
Continue your VLSI learning journey with the complete blog series: