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 styleHDLHardware chosen by VivadoTypical size
constant, combinationalcase / constant arrayLUTs (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 blockblock 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).

hex7seg — a ROM written as a case statement
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
endmodule

For 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.

sine_rom — 64-sample sine table with a synchronous read
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
endmodule
sine64.hex (first 8 of 64 lines)
80
8C
98
A5
B0
BC
C6
D0
rom_tb — checks the decoder and five sine samples
`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
endmodule

Both 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.

ram_sp — single-port block RAM (one address for read and write)
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
endmodule

When 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:

ram_sdp — simple dual-port block RAM (write port A, read port B)
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
endmodule

If 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.

ram_dist — distributed (LUT) RAM: synchronous write, asynchronous read
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
endmodule
Common mistake: a reset on the memory array
Putting the memory array inside an if (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 narrow32K × 1, 16K × 2, 8K × 4
with parity bits used as data4K × 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

\[ N_\text{BRAM} \;\gtrsim\; \left\lceil \frac{\text{depth} \times \text{width}}{36\,864} \right\rceil \]

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)?

emptyaddr 0addr 1addr 2addr 3wptrrptrwptr = 000rptr = 0003 wordsaddr 0addr 1addr 2addr 3wptrrptrwptr = 011rptr = 000fulladdr 0addr 1addr 2addr 3wptrrptrwptr = 110rptr = 010
A 4-word FIFO with pointers one bit wider than the address. The low bits select the RAM word; the top bit counts laps. Equal pointers mean empty; same address on different laps means full.

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:

\[ \text{empty} \iff w = r, \qquad \text{full} \iff w_\text{low} = r_\text{low} \;\wedge\; w_\text{top} \neq r_\text{top}, \qquad \text{count} = (w - r) \bmod 2^{A+1} \]
fifo_sync — single-clock FIFO with block-RAM storage
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;
endmodule

The 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.

Key idea: depth is a power of two
With depth \(2^A\), pointers simply wrap by overflowing their binary counter, and the extra-bit trick works with no comparisons against a maximum. That is why FIFO depths are almost always 16, 512, 1024…

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.

fifo_sync_tb — overflow, underflow and simultaneous access
`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
endmodule
Output (both simulators, at 120 ns)
PASS: 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.

On the board: putting the FIFO between the UART receiver and transmitter

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:

decimal01234567
binary000001010011100101110111
Gray000001011010110111101100

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:

\[ g = b \oplus (b \gg 1), \qquad b_{N-1} = g_{N-1}, \quad b_i = b_{i+1} \oplus g_i \]
gray_conv — binary to Gray and Gray to binary
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
gray_conv_tb — round trip and the one-bit property over a full cycle
`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
endmodule

8. 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.

wclk domainrclk domaindual-port RAMwrite: wclk · read: rclkdindoutwrite ptrbin + Graywaddrread ptrbin + Grayraddr2-FF syncwgrayempty = (rgray == wgray_sync)2-FF syncrgrayfull = one lap apart
Dual-clock FIFO. The RAM is written on wclk and read on rclk. Each pointer crosses to the other domain as a Gray code through a two-flip-flop synchroniser, so only one bit can be in transition.
fifo_async — dual-clock FIFO with Gray-coded pointers
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);
endmodule

The 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.

fifo_async_tb — 125 MHz writer, ~77 MHz reader
`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
endmodule
A simulation pass is not a CDC proof
A logic simulator never shows metastability, so this testbench checks the bookkeeping, not the clock crossing itself. The correctness of the crossing rests on the Gray-code argument and on constraints: in Vivado, the paths from one clock to the other must be told not to be timed as ordinary paths (for instance with set_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.

Python
script.py79 lines

Click Run to execute the Python code

Code will be executed with Python 3 on the server

Key idea: a FIFO absorbs bursts, not rate differences
If the long-run input rate exceeds the output rate, no finite FIFO is deep enough — it only delays the overflow. Size a FIFO for the worst burst; fix the average rate with the design itself.

Practice Problems

Progress: 0/5 (0%)
1
⭐ Easy
How many 36 Kb block RAMs does Vivado need for (a) a 1024 × 16 memory, (b) a 4096 × 32 memory, (c) a 512 × 64 simple dual-port memory?
2
⭐ Easy
Write the 4-bit Gray code for the decimal values 5, 6 and 7, and show that consecutive codes differ in one bit. Then convert the Gray code 1101 back to binary.
3
⭐⭐ Medium
A FIFO of depth 8 uses 3-bit pointers only (no extra bit). After reset both pointers are 0. (a) Why can it not tell full from empty? (b) Name another fix besides the extra bit, and its cost.
4
⭐⭐ Medium
A camera delivers lines of 640 pixels at 1 pixel per cycle with 160 idle cycles between lines. A processing block consumes 1 pixel every 1.2 cycles on average, without pauses. What FIFO depth is needed between them, and is the system sustainable?
5
⭐⭐⭐ Hard
In the dual-clock FIFO, the read side sees the write pointer two read-clock cycles late. Explain why 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?

Problem Set Statistics

Total Problems
5
Attempted
0
Completion
0%
Share:XRedditLinkedIn
Rate this chapter: