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