Electronic – Verilog output reg vs output wire

verilog

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

Best Answer

The only real difference between wire and reg declarations in Verilog is that a reg can be assigned to in a procedural block (a block beginning with always or initial), and a wire can be assigned in a continuous assignment (an assign statement) or as an output of an instantiated submodule.

You simply need to declare each net as wire or reg depending on how you will assign it a value.

Because of this difference, wire nets are almost always the output of combinatorial logic or submodules. But reg nets might be the output of either sequential or combintatorial logic, for example when a case statement in an always block is used to infer a multiplexer.