1
\$\begingroup\$

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
\$\endgroup\$
  • \$\begingroup\$ It's helpful to to have the full module, but to know whether the outputs need to be latched or not depends how they're used. Do they get latched by the circuit they connect to? Would adding an extra delay mess up the timing of the higher design? Only you know this...you have to design your module so it works with all the other parts of your design. \$\endgroup\$ – The Photon Jul 14 '16 at 18:37
  • \$\begingroup\$ @ThePhoton No they do not get latched, adding extra delay would mess up the timming (because 'full' would still be low for one clock period when FIFO would be actually full). Does this mean I would need to redesign it in such a way for full and empty signals to be registered by clocks? \$\endgroup\$ – Golaž Jul 14 '16 at 18:44
  • \$\begingroup\$ If extra delay will mess up your system, then you can not register these signals. If you register them, then the downstream logic won't receive the new value until the next clock edge gates the signal through the flip-flop. \$\endgroup\$ – The Photon Jul 14 '16 at 20:06
5
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Yes I thought about that too. But what is the prefered way of doing it? Would you declare these outputs as regs or wires (like I did) ? Is it OK if the output is combinatorial logic, or should I delcare it as reg and thus infer a flip-flop after this combinatorial logic? \$\endgroup\$ – Golaž Jul 14 '16 at 6:23
  • \$\begingroup\$ If output is a wire then it is a combinatorical output, but if I delcare it as a reg output is then synchronous to clock. But if all values (wr_pnt, rd_pnt_sync_w,...) in wire are synchronous to clock, do I really need the output to be registered also, or is it OK like this? \$\endgroup\$ – Golaž Jul 14 '16 at 6:25
  • \$\begingroup\$ I think you need to separate your question into two parts. First, how should you design your logic? Should these signals be the outputs of flip-flops, syncronized to some clock, or should they be the output of some combinatorial logic. Only after you've decided that, worry about the second question, how to describe your design in Verilog. \$\endgroup\$ – The Photon Jul 14 '16 at 15:33
  • 1
    \$\begingroup\$ BTW, when you wrote "if I delcare it as a reg output is then synchronous to clock." This is not true. There are many cases where you declare something as reg but the inferred logic does not syncronize that signal to any clock. This kind of logical confusion is why I am recommending you break your problem in two parts. I can't help with the first part (what you actually want to design) because you haven't given enough information about what the logic is suppposed to do. \$\endgroup\$ – The Photon Jul 14 '16 at 15:35
  • \$\begingroup\$ About the first part. FIFO is arranged as a circular buffer. There are two seperate Gray encoded counters (pointers) which increase after each write (write pointer) or read (read pointer). So when pointers match, FIFO is either full or empty, by using another variable which indicates wether pointer has wraped around back to start it is possible to conclude with certainty wether FIFO is full or empty. Please take a look at this document :sunburst-design.com/papers/CummingsSNUG2002SJ_FIFO1.pdf the relevant part starts at page 9. \$\endgroup\$ – Golaž Jul 14 '16 at 17:40
2
\$\begingroup\$

I think you would want to declare full and empty as registers, since they are memory nodes.

One particular reason (from link below) that it will not work in your case: 4. Wire elements cannot be used as the left-hand side of an = or <= sign in an always@ block.

An easy way to keep track is to use _q at the end of any node which is declared as a register (like full_q and empty_q for the stored bits). That way if you have an exact copy of this node which is not stored, you could call it full and empty.

Signals which are not latched can be left as wires (wr_pnt, rd_pnt_sync_w, etc). These must be continuously driven, unlike regs.

There are other constraints as well, the link below does a good job of describing them all. If this link breaks in the future, just google wire vs reg verilog.

pretty good explanation of difference b/w wire and reg

\$\endgroup\$
1
\$\begingroup\$

A widely-accepted guideline for synthesis and reusability is that all RTL module outputs should be registered. If one or more outputs cannot be registered, then either the module boundary or the location of that combo logic should be reconsidered. For an async module such as this, the synchronizers can serve as the output regs.

Likewise, the RAM read data output should also be registered, unless the RAM has built-in read output registers. The read data does not need to pass through a synchronizer, because the RAM read occurs in the read clock domain. Adding RAM output regs would fix the problem of "messing up the timing" downstream (from adding full/empty regs), since everything would be equally delayed by one cycle.

With this in mind, the read logic outside of this module should be designed to expect a one-cycle delay between re and rdata, or it should be designed with a valid/ready handshake protocol such that it waits for a valid signal, which would also be a reg'd output:

@(posedge clk_r) valid <= re & ~empty
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.