Memories & FIFOs
Inferring ROM and block RAM from HDL, initialising memories from files, synchronous FIFOs with full and empty flags, and Gray-code pointers.
1. A memory is an array of registers
Every design so far stored its data in a handful of flip-flops. As soon as you need to keep hundreds or thousands of values — a lookup table, a line of an image, bytes waiting to be sent over the UART of Chapter 16 — you need a memory: an array of words, each selected by an address.
In HDL a memory is simply a two-dimensional array. In Verilog reg [7:0] mem [0:1023] declares 1024 words of 8 bits; in VHDL you first declare an array type and then a signal of that type. Writing is always done on a clock edge. The interesting question is how you read it, because that decides what hardware the synthesiser builds:
| Read style | HDL | Hardware chosen by Vivado | Typical size |
|---|---|---|---|
| constant, combinational | case / constant array | LUTs (logic) | a few dozen words |
| asynchronous (no clock) | assign dout = mem[addr] | distributed RAM (LUT RAM) | ≤ ~256 words |
| synchronous (registered) | dout <= mem[addr] in a clocked block | block RAM (BRAM) | hundreds to many thousands |
Chapter 6 introduced the hardware: the XC7Z020 has 140 block RAMs of 36 Kb each — 4.9 Mb in total — as dedicated memory blocks on the die. Each one has registered inputs (address, data, enables) plus an optional output register, which is why it can only be read synchronously: the address is captured on a clock edge and the data appears after it.
2. Read-only memories
A ROM (read-only memory) is a fixed table: an address goes in, a stored value comes out. The smallest ROMs are just case statements. Here is the classic one, the decoder for a seven-segment display. It is 16 words of 7 bits, and the synthesiser turns it into a few LUTs (each LUT is itself a tiny ROM, Chapter 5).
module hex7seg (
input wire [3:0] digit,
output reg [6:0] seg // segments g..a, 1 = lit
);
// A 16-word x 7-bit ROM written as a case statement
always @(*) begin
case (digit)
4'h0: seg = 7'b0111111;
4'h1: seg = 7'b0000110;
4'h2: seg = 7'b1011011;
4'h3: seg = 7'b1001111;
4'h4: seg = 7'b1100110;
4'h5: seg = 7'b1101101;
4'h6: seg = 7'b1111101;
4'h7: seg = 7'b0000111;
4'h8: seg = 7'b1111111;
4'h9: seg = 7'b1101111;
4'hA: seg = 7'b1110111;
4'hB: seg = 7'b1111100;
4'hC: seg = 7'b0111001;
4'hD: seg = 7'b1011110;
4'hE: seg = 7'b1111001;
default: seg = 7'b1110001; // F
endcase
end
endmoduleFor larger tables, writing a case per entry is impractical. Verilog can load the array from a text file at the start of simulation and synthesis with $readmemh (hexadecimal values, one per line; $readmemb reads binary). Vivado honours it and puts the values into the bitstream. In VHDL the portable way that also works in VHDL-93 is a constant array written out in the source; a short script (like the one that produced sine64.hex) can generate both. The VHDL file can also be read with the textio package in a function, but that is more code and depends on the tool, so we stay with the constant.
module sine_rom (
input wire clk,
input wire [5:0] addr, // 64 samples per period
output reg [7:0] dout = 8'd0 // unsigned, 0x80 = zero crossing
);
reg [7:0] rom [0:63];
// Contents come from a text file: one hex value per line
initial $readmemh("sine64.hex", rom);
always @(posedge clk)
dout <= rom[addr]; // synchronous read -> block RAM
endmodule80 8C 98 A5 B0 BC C6 D0
`timescale 1ns/1ps
module rom_tb;
reg clk = 0;
reg [3:0] digit = 4'h0;
reg [5:0] addr = 6'd0;
wire [6:0] seg;
wire [7:0] sample;
integer errors = 0;
hex7seg u_seg (.digit(digit), .seg(seg));
sine_rom u_sin (.clk(clk), .addr(addr), .dout(sample));
always #4 clk = ~clk;
task check_sine(input [5:0] a, input [7:0] expected);
begin
addr = a;
@(posedge clk); #1; // data appears after the edge
if (sample !== expected) begin
$display("FAIL: sine[%0d] = %h, expected %h", a, sample, expected);
errors = errors + 1;
end
end
endtask
initial begin
digit = 4'h8; #1;
if (seg !== 7'b1111111) begin $display("FAIL: 8 -> %b", seg); errors = errors + 1; end
digit = 4'h1; #1;
if (seg !== 7'b0000110) begin $display("FAIL: 1 -> %b", seg); errors = errors + 1; end
check_sine(0, 8'h80); check_sine(16, 8'hFF);
check_sine(32, 8'h80); check_sine(48, 8'h00); check_sine(5, 8'hBC);
if (errors == 0) $display("PASS: ROM contents as expected");
$finish;
end
endmoduleBoth simulators print PASS: ROM contents as expected at 37 ns. Notice in the testbench that the sine value is checked after the clock edge: a synchronous ROM has one cycle of latency.
3. RAM templates that Vivado recognises
Synthesis tools recognise memories by pattern. If your code matches the pattern, you get a block RAM for free; if it does not, you may get thousands of flip-flops and multiplexers instead. The rule for block RAM is: write and read inside a clocked process, and register the read data.
module ram_sp #(
parameter DW = 16, // data width
parameter AW = 10 // address width: 2**AW words
) (
input wire clk,
input wire we, // write enable
input wire [AW-1:0] addr,
input wire [DW-1:0] din,
output reg [DW-1:0] dout = 0
);
reg [DW-1:0] mem [0:(1<<AW)-1];
always @(posedge clk) begin
if (we)
mem[addr] <= din;
dout <= mem[addr]; // registered read: old data (read-first)
end
endmoduleWhen you write and read the same address in the same cycle, this template returns the old contents (“read-first”), because the read uses the value of mem before the edge. Block RAMs support this mode natively, as well as “write-first” and “no-change”.
Most designs need to write at one address while reading another — a FIFO is the obvious example. Block RAMs have two independent ports, so the simple dual-port template, with one write port and one read port, maps onto a single block:
module ram_sdp #(
parameter DW = 16,
parameter AW = 10
) (
input wire clk,
// write port A
input wire we,
input wire [AW-1:0] waddr,
input wire [DW-1:0] din,
// read port B
input wire re, // read enable
input wire [AW-1:0] raddr,
output reg [DW-1:0] dout = 0
);
reg [DW-1:0] mem [0:(1<<AW)-1];
always @(posedge clk) begin
if (we)
mem[waddr] <= din;
if (re)
dout <= mem[raddr]; // synchronous read on its own address
end
endmoduleIf you read asynchronously — data available in the same cycle, no clock — the block RAM cannot be used. Vivado then builds distributed RAM out of LUTs: in some slices, a 6-input LUT can act as a 64 × 1-bit RAM. That is ideal for small memories such as register files, and wasteful for large ones.
module ram_dist #(
parameter DW = 8,
parameter AW = 5 // 32 words: small, LUT-based
) (
input wire clk,
input wire we,
input wire [AW-1:0] addr,
input wire [DW-1:0] din,
output wire [DW-1:0] dout
);
reg [DW-1:0] mem [0:(1<<AW)-1];
always @(posedge clk)
if (we)
mem[addr] <= din; // write is synchronous
assign dout = mem[addr]; // read is asynchronous -> LUT RAM
endmoduleif (rst) branch that clears all words (a for loop over every address) cannot be mapped to block RAM, which has no way to clear all its contents in one cycle. Vivado falls back to flip-flops — 16 384 of them for a 1K × 16 memory. Reset the pointers and control logic around a memory, never the memory itself. Initial contents (from a file or constant) are fine: they are loaded with the bitstream.4. Sizing memories in block RAM units
A 36 Kb block RAM (36 864 bits, including one parity bit per byte that can store data) can be configured with different aspect ratios — the product of depth and width is roughly constant. It can also be split into two independent 18 Kb halves.
| Configuration (one 36 Kb block) | Depth × width |
|---|---|
| deep and narrow | 32K × 1, 16K × 2, 8K × 4 |
| with parity bits used as data | 4K × 9, 2K × 18, 1K × 36 |
| simple dual-port only (widest) | 512 × 72 |
Vivado tiles a larger memory with several blocks. A quick estimate for the number of 36 Kb blocks is
but the real count depends on the aspect ratio. A 4096 × 32 memory holds 131 072 bits, so the estimate gives 4; using four 4K × 9 blocks side by side indeed gives 4K × 36 and wastes 4 bits per word. A 4096 × 8 memory fits one block exactly. Always check the utilization report after synthesis for the actual number of RAMB36/RAMB18 used.
5. The synchronous FIFO
A FIFO (first in, first out) is a queue. One side writes words when it has them; the other side reads them, in the same order, when it is ready. It decouples a producer from a consumer that do not work in lockstep — exactly what was missing in the UART echo of Chapter 16.
Inside, a FIFO is a dual-port RAM used as a circular buffer with two counters: the write pointer (where the next word goes) and the read pointer (where the next word comes from). Both only count up and wrap around at the end of the memory. The difficulty is the flags: if both pointers are equal, is the FIFO empty (the reader caught up) or full (the writer went round once and caught up from behind)?
The elegant solution is to make each pointer one bit wider than the address. The low bits address the RAM; the extra top bit flips every time the pointer completes a lap. Then:
module fifo_sync #(
parameter DW = 8, // data width
parameter AW = 4 // depth = 2**AW words
) (
input wire clk,
input wire rst,
// write side
input wire wr_en,
input wire [DW-1:0] din,
output wire full,
// read side
input wire rd_en,
output reg [DW-1:0] dout = 0, // valid the cycle after rd_en
output wire empty,
output wire [AW:0] count // words stored, 0 .. 2**AW
);
reg [DW-1:0] mem [0:(1<<AW)-1];
reg [AW:0] wptr = 0, rptr = 0; // one extra bit: the "lap" bit
wire do_wr = wr_en && !full; // refuse writes when full
wire do_rd = rd_en && !empty; // refuse reads when empty
always @(posedge clk) begin
if (do_wr)
mem[wptr[AW-1:0]] <= din;
if (do_rd)
dout <= mem[rptr[AW-1:0]]; // synchronous read -> block RAM
end
always @(posedge clk) begin
if (rst) begin
wptr <= 0;
rptr <= 0;
end else begin
if (do_wr) wptr <= wptr + 1'b1;
if (do_rd) rptr <= rptr + 1'b1;
end
end
// Same address, same lap -> empty. Same address, different lap -> full.
assign empty = (wptr == rptr);
assign full = (wptr[AW-1:0] == rptr[AW-1:0]) && (wptr[AW] != rptr[AW]);
assign count = wptr - rptr;
endmoduleThe FIFO protects itself: a write while full (overflow) and a read while empty (underflow) are simply ignored. Like the block RAM it is built on, dout appears one cycle after rd_en. This style is called a “standard” FIFO; a “first-word fall-through” FIFO instead shows the next word on dout before it is read, at the cost of an extra output register and some logic.
6. Testing the FIFO, including abuse
A good FIFO testbench does not only check the happy path. This one uses a 4-word FIFO and tries to write 6 words (two must be refused), reads 6 times (two must be refused, and dout must keep its last value), and finally reads and writes in the same cycle.
`timescale 1ns/1ps
module fifo_sync_tb;
reg clk = 0, rst = 0, wr_en = 0, rd_en = 0;
reg [7:0] din = 8'd0;
wire [7:0] dout;
wire full, empty;
wire [2:0] count;
integer i, errors = 0;
fifo_sync #(.DW(8), .AW(2)) dut ( // depth 4, so it fills quickly
.clk(clk), .rst(rst), .wr_en(wr_en), .din(din), .full(full),
.rd_en(rd_en), .dout(dout), .empty(empty), .count(count));
always #4 clk = ~clk;
task check(input cond, input [8*24-1:0] what);
if (!cond) begin $display("FAIL: %0s", what); errors = errors + 1; end
endtask
initial begin
@(negedge clk);
check(empty && !full, "empty after start");
// 1. Write 6 words into a 4-deep FIFO: the last 2 must be refused
for (i = 1; i <= 6; i = i + 1) begin
din = 8'h10 + i; wr_en = 1;
@(negedge clk);
end
wr_en = 0;
check(full && count == 4, "full with 4 words");
// 2. Read 6 times: 4 words in order, then 2 refused reads
for (i = 1; i <= 6; i = i + 1) begin
rd_en = 1;
@(negedge clk);
if (i <= 4) check(dout == 8'h10 + i, "data in order");
end
rd_en = 0;
check(empty && count == 0, "empty after underflow");
check(dout == 8'h14, "underflow kept dout");
// 3. Simultaneous read and write with one word stored
din = 8'hA0; wr_en = 1; @(negedge clk);
din = 8'hA1; rd_en = 1; @(negedge clk);
wr_en = 0; rd_en = 0;
check(dout == 8'hA0 && count == 1, "read+write same cycle");
if (errors == 0)
$display("PASS: overflow and underflow refused, order kept, count=%0d", count);
$finish;
end
endmodulePASS: overflow and underflow refused, order kept, count=1
To convince yourself that the test has teeth, remove && !full from do_wr: the fifth write then overwrites the oldest word and the testbench reports FAIL: full with 4 words and FAIL: data in order.
Insert fifo_sync (8 bits wide, depth 16 or more) into the uart_echo design of Chapter 16: connect the receiver's valid and data to wr_en and din; start the transmitter when the FIFO is not empty and the transmitter is not busy, and pulse rd_en at the same time. Because dout arrives one cycle after rd_en, delay the transmitter's start by one cycle. Then paste a long line of text into the terminal: a short burst now comes back complete. For an endless stream, the transmitter must also be at least as fast as the sender (Problem 5 of Chapter 16).
7. Crossing clock domains: why binary pointers fail
Often the producer and consumer run on different clocks — a camera at 74.25 MHz feeding logic at 125 MHz, for example. Then the full flag (computed in the write clock domain) needs the read pointer, and the empty flag (read domain) needs the write pointer. Each pointer must cross into the other clock domain.
Chapter 15 showed that a single bit can be brought across with a two-flip-flop synchroniser. A multi-bit binary counter cannot. When the counter goes from 7 (0111) to 8 (1000), four bits change at once. They never arrive at exactly the same instant, and if the receiving clock samples during the change, each bit can independently be old or new — the received value can be anything from 0 to 15. A FIFO that believes the write pointer is 15 will read garbage.
The fix is the Gray code, a way of numbering in which consecutive values differ in exactly one bit, including the wrap-around from the last value back to 0:
| decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| binary | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 |
| Gray | 000 | 001 | 011 | 010 | 110 | 111 | 101 | 100 |
If only one bit is changing when the other clock samples, the synchroniser delivers either the old value or the new one — both are legal pointer values, merely possibly one count stale. Conversion is cheap:
module gray_conv #(
parameter N = 4
) (
input wire [N-1:0] bin_in,
output wire [N-1:0] gray_out, // binary -> Gray
input wire [N-1:0] gray_in,
output reg [N-1:0] bin_out // Gray -> binary
);
// Binary to Gray: each bit XOR the bit above it
assign gray_out = bin_in ^ (bin_in >> 1);
// Gray to binary: each bit is the XOR of all Gray bits at and above it
integer i;
always @(*) begin
bin_out[N-1] = gray_in[N-1];
for (i = N - 2; i >= 0; i = i - 1)
bin_out[i] = bin_out[i+1] ^ gray_in[i];
end
endmodule`timescale 1ns/1ps
module gray_conv_tb;
reg [3:0] bin = 4'd0, prev_gray = 4'd0;
wire [3:0] gray, back;
integer i, k, ones, errors = 0;
// Feed the Gray output straight back into the Gray-to-binary converter
gray_conv #(.N(4)) dut (.bin_in(bin), .gray_out(gray), .gray_in(gray), .bin_out(back));
initial begin
for (i = 0; i <= 16; i = i + 1) begin
bin = i[3:0]; #1; // i = 16 wraps to 0
if (back !== bin) begin
$display("FAIL: round trip %0d", i); errors = errors + 1;
end
if (i > 0) begin
ones = 0;
for (k = 0; k < 4; k = k + 1) ones = ones + (gray[k] ^ prev_gray[k]);
if (ones != 1) begin
$display("FAIL: %0d bits changed at step %0d", ones, i); errors = errors + 1;
end
end
prev_gray = gray;
end
if (errors == 0) $display("PASS: 16 round trips, exactly 1 bit changes per step (incl. wrap)");
$finish;
end
endmodule8. The dual-clock FIFO
The architecture below is the classic asynchronous FIFO. Each side keeps its own pointer in binary (for addressing and incrementing) and in Gray code (for export). Only the Gray copy crosses to the other domain, through two flip-flops. Full and empty are compared in Gray code directly: empty when the two Gray pointers are equal; full when they are one lap apart, which in Gray code means the top two bits are inverted and the rest equal.
module fifo_async #(
parameter DW = 8,
parameter AW = 4 // depth 2**AW, AW >= 2
) (
// write clock domain
input wire wclk,
input wire wrst,
input wire wr_en,
input wire [DW-1:0] din,
output wire full,
// read clock domain
input wire rclk,
input wire rrst,
input wire rd_en,
output reg [DW-1:0] dout = 0,
output wire empty
);
reg [DW-1:0] mem [0:(1<<AW)-1];
reg [AW:0] wbin = 0, wgray = 0; // write pointer: binary and Gray
reg [AW:0] rbin = 0, rgray = 0; // read pointer: binary and Gray
reg [AW:0] rgray_w1 = 0, rgray_w2 = 0; // read pointer, in write domain
reg [AW:0] wgray_r1 = 0, wgray_r2 = 0; // write pointer, in read domain
// ---------------- write domain ----------------
wire do_wr = wr_en && !full;
wire [AW:0] wbin_next = wbin + 1'b1;
always @(posedge wclk) begin
if (do_wr)
mem[wbin[AW-1:0]] <= din;
if (wrst) begin
wbin <= 0;
wgray <= 0;
end else if (do_wr) begin
wbin <= wbin_next;
wgray <= wbin_next ^ (wbin_next >> 1);
end
rgray_w1 <= rgray; // 2-FF synchroniser: only one
rgray_w2 <= rgray_w1; // bit of rgray changes at a time
end
// Full: the pointers are one lap apart. In Gray code that means the
// top two bits differ and all the others are equal.
assign full = (wgray == {~rgray_w2[AW:AW-1], rgray_w2[AW-2:0]});
// ---------------- read domain ----------------
wire do_rd = rd_en && !empty;
wire [AW:0] rbin_next = rbin + 1'b1;
always @(posedge rclk) begin
if (do_rd)
dout <= mem[rbin[AW-1:0]];
if (rrst) begin
rbin <= 0;
rgray <= 0;
end else if (do_rd) begin
rbin <= rbin_next;
rgray <= rbin_next ^ (rbin_next >> 1);
end
wgray_r1 <= wgray;
wgray_r2 <= wgray_r1;
end
assign empty = (rgray == wgray_r2);
endmoduleThe testbench below writes on an 8 ns clock and reads on a 13 ns clock with no fixed phase relation. The reader starts late, so the 8-word FIFO fills up and full must hold the writer back; then 100 words must arrive in order. Both simulators print PASS: 100 words crossed clock domains in order at 1712 ns.
`timescale 1ns/1ps
module fifo_async_tb;
localparam N = 100; // words to transfer
reg wclk = 0, rclk = 0, wr_en = 0, rd_en = 0;
reg [7:0] din = 0;
wire [7:0] dout;
wire full, empty;
integer sent = 0, got = 0, errors = 0, full_seen = 0, pending = 0;
fifo_async #(.DW(8), .AW(3)) dut (
.wclk(wclk), .wrst(1'b0), .wr_en(wr_en), .din(din), .full(full),
.rclk(rclk), .rrst(1'b0), .rd_en(rd_en), .dout(dout), .empty(empty));
always #4 wclk = ~wclk; // 125 MHz writer
always #6.5 rclk = ~rclk; // ~77 MHz reader, unrelated phase
// Writer: push 0, 1, 2, ... whenever the FIFO is not full
always @(negedge wclk) begin
if (full) full_seen = 1;
wr_en = (sent < N) && !full;
din = sent;
if (wr_en) sent = sent + 1;
end
// Reader: starts late (so the FIFO fills up), then pops every cycle
always @(negedge rclk) begin
if (pending) begin // data from the previous pop
if (dout !== got[7:0]) begin
$display("FAIL: got %0d expected %0d", dout, got); errors = errors + 1;
end
got = got + 1;
end
rd_en = ($time > 300) && !empty;
pending = rd_en;
end
initial begin
wait (got == N);
#100;
if (errors == 0 && full_seen && empty)
$display("PASS: %0d words crossed clock domains in order (full seen, empty at end)", got);
else
$display("FAIL: errors=%0d full_seen=%0d", errors, full_seen);
$finish;
end
endmoduleset_max_delay -datapath_only on the Gray pointer buses, or by declaring the clocks asynchronous). For production designs, Xilinx provides verified FIFO generators and the XPM macros; use the hand-written one to understand them.9. Python: Gray codes and choosing a FIFO depth
The first part generates a 5-bit Gray code and verifies the one-bit-change property and the conversion back to binary. The second part answers the practical question “how deep must my FIFO be?” for a producer that writes in bursts of 64 words at full speed, then pauses, and a consumer that reads steadily at half speed. The average input rate (0.4 words/cycle) is below the read capacity (0.5), so the FIFO only has to absorb the bursts. An isolated burst leaves \(64 \times (1 - 0.5) = 32\) words behind, but bursts that arrive before the FIFO has drained pile up: the simulated peak over 200 000 cycles is 80 words, so a depth of 128 is the right choice.
Click Run to execute the Python code
Code will be executed with Python 3 on the server
Practice Problems
1101 back to binary.empty can stay high for a few cycles after a word was written, but can never go low while the FIFO is really empty. Is the same "safe direction" true for full?