I am currently designing an asynchronous FIFO for learning purposes. I have the module done but I am having some second thoughts about it.
Firstly I've been thru some articles describing how to approach and roughly design it (I haven't looked at any articles with detailed designs, because I want to learn it myself). And in all I've seen status flags which indicate if FIFO is full or empty being registered:
always @(posedge clk_write) begin
full <= ...
end
always @(posedge clk_read) begin
empty <= ...
end
But in my module I've defined 'full' and 'empty' as wires and assign them like so:
assign full = (wr_pnt == rd_pnt_sync_w) && (wr_pnt_wraped != rd_pnt_wraped_sync_w);
assign empty = (rd_pnt == wr_pnt_sync_r) && (rd_pnt_wraped == wr_pnt_wraped_sync_r);
'rd_pnt_sync_w' and 'wr_pnt_sync_r' are synchronized pointers to other clock domains. Those '_wraped' are just some bits to indicate wether a write/read pointer has wraped around FIFO.
So I have a question, whats the difference if I register 'full'/'empty' signals? In my module all of the signals 'wr_pnt', 'rd_pnt_sync_w', 'wr_pnt_wraped', ... are registered to respective clocks. Does it make any huge difference if I do it like so or should I really register those signals?
Full module:
module dual_port_fifo_dc #(
parameter DATA_WIDTH = 8, // Data bus width in bits.
parameter ADDR_WIDTH = 8 // Address width in bits. 2 ^ 'ADDR_WIDTH' locations 'DATA_WIDTH' wide in FIFO.
)(
input clk_r, // Clock for read port.
input clk_w, // Clock for write port.
input reset, // Active high reset.
input we, // Write enable.
input re, // Read enable.
input [DATA_WIDTH - 1:0] data_w, // Data to write to FIFO.
output [DATA_WIDTH - 1:0] data_r, // Data read from FIFO.
output full, // FIFO is full and can not be written to.
output empty, // FIFO is empty and can not be read from.
output almost_full, // When FIFO is half or more full.
output almost_empty // When FIFO is half or more empty.
);
// Gray encoding is used for pointers because at maximum only one bit changes simultaneously where as
// with binary encoding going from 3 (3'b011) to 4 (3'b100) all bits change. This one bit change is
// wanted for synchronizations to other clock domains as no special care is needed (just a general
// 2-stage synchronizer with D flip-flops). While with binary encoding there could be problems with
// the same approach, if value changes from 3->4 close to positive clock edge, some bit values may
// not get captured correctly.
// Write address/pointer counter.
wire [ADDR_WIDTH - 1:0] wr_pnt; // Write pointer value (Gray).
wire [ADDR_WIDTH - 1:0] wr_addr; // Write address value (Binary).
wire wr_pnt_wraped; // Aditional bit indicating if write address has wraped around.
fifo_addr_counter #(
.WIDTH (ADDR_WIDTH)
) write_counter (
.clk (clk_w),
.ce (we & ~full),
.reset (reset),
.gray_cnt (wr_pnt),
.binary_cnt (wr_addr),
.carry (wr_pnt_wraped)
);
// Read address/pointer counter.
wire [ADDR_WIDTH - 1:0] rd_pnt; // Read pointer value (Gray).
wire [ADDR_WIDTH - 1:0] rd_addr; // Read address value (Binary).
wire rd_pnt_wraped; // Aditional bit indicating if write address has wraped around.
fifo_addr_counter #(
.WIDTH (ADDR_WIDTH)
) read_counter (
.clk (clk_r),
.ce (re & ~empty),
.reset (reset),
.gray_cnt (rd_pnt),
.binary_cnt (rd_addr),
.carry (rd_pnt_wraped)
);
// Synchronize read pointer to write clock ('clk_w').
wire [ADDR_WIDTH - 1:0] rd_pnt_sync_w;
wire rd_pnt_wraped_sync_w;
nbit_synchronizer #(
.STAGE (2),
.WIDTH (DATA_WIDTH)
) rd_pnt_synch (
.clk (clk_w),
.reset (reset),
.d (rd_pnt),
.q (rd_pnt_sync_w)
);
synchronizer #(
.STAGE (2)
) rd_pnt_wraped_synch (
.clk (clk_w),
.reset (reset),
.d (rd_pnt_wraped),
.q (rd_pnt_wraped_sync_w)
);
// Synchronize write pointer to read clock ('clk_r').
wire [ADDR_WIDTH - 1:0] wr_pnt_sync_r;
wire wr_pnt_wraped_sync_r;
nbit_synchronizer #(
.STAGE (2),
.WIDTH (DATA_WIDTH)
) wr_pnt_synch (
.clk (clk_r),
.reset (reset),
.d (wr_pnt),
.q (wr_pnt_sync_r)
);
synchronizer #(
.STAGE (2)
) wr_pnt_wraped_synch (
.clk (clk_r),
.reset (reset),
.d (wr_pnt_wraped),
.q (wr_pnt_wraped_sync_r)
);
// FIFO full flag generation. 'full' signal gets asserted immediately as
// write pointer changes. But because of read pointer synchronization it will
// get de-asserted two 'clk_w' ticks after FIFO isnt actually full anymore.
// Full when wraped bits dont match. Meaning write pointer has wraped around FIFO.
assign full = (wr_pnt == rd_pnt_sync_w) && (wr_pnt_wraped != rd_pnt_wraped_sync_w);
// FIFO empty flag generation. 'empty' signal gets asserted immediately as
// read pointer changes. But because of write pointer synchronization it will
// get de-asserted two 'clk_r' ticks after FIFO isnt actually empty anymore.
assign empty = (rd_pnt == wr_pnt_sync_r) && (rd_pnt_wraped == wr_pnt_wraped_sync_r);
// Dual port RAM with seperate asynchronous clocks for reading and writting.
dp_ram_block #(
.DATA_WIDTH (DATA_WIDTH),
.ADDR_WIDTH (ADDR_WIDTH)
) dpram (
.clk_w (clk_w),
.clk_r (clk_r),
.we (we),
.addr_w (wr_addr),
.addr_r (rd_addr),
.data_w (data_w),
.data_r (data_r)
);
endmodule