Verilog output reg vs output wire

0

Actualmente estoy diseñando un FIFO asíncrono para fines de aprendizaje. He hecho el módulo pero tengo algunas dudas al respecto.

En primer lugar, he visto algunos artículos que describen cómo abordarlos y diseñarlos de manera aproximada (no he revisado ningún artículo con información detallada, porque quiero aprenderlo yo mismo). Y en todos los indicadores de estado que he visto que indican si FIFO está lleno o vacío se está registrando:

always @(posedge clk_write) begin
    full <= ...
end

always @(posedge clk_read) begin
    empty <= ...
end

Pero en mi módulo he definido 'lleno' y 'vacío' como cables y asignarlos así:

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' y 'wr_pnt_sync_r' son punteros sincronizados a otros dominios de reloj. Esos '_wraped' son solo algunos bits para indicar si un puntero de escritura / lectura se ha desplazado alrededor de FIFO.

Entonces, tengo una pregunta, ¿cuál es la diferencia si registro señales 'llenas' / 'vacías'? En mi módulo, todas las señales 'wr_pnt', 'rd_pnt_sync_w', 'wr_pnt_wraped', ... están registradas en los relojes respectivos. ¿Hay una gran diferencia si lo hago así o debo registrar esas señales?

Módulo completo:

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

    
pregunta Golaž

3 respuestas

4

La única diferencia real entre las declaraciones wire y reg en Verilog es que se puede asignar un reg en un bloque de procedimiento (un bloque que comienza con always o initial ), y un wire se puede asignar en una asignación continua (una declaración assign ) o como una salida de un submódulo instanciado.

Simplemente necesitas declarar cada red como wire o reg dependiendo de cómo le asignes un valor.

Debido a esta diferencia, wire nets son casi siempre la salida de lógica combinatoria o submódulos. Pero reg nets puede ser la salida de lógica secuencial o combinatoria, por ejemplo, cuando se usa una instrucción case en un bloque always para inferir un multiplexor.

    
respondido por el The Photon
1

Creo que querría declararlo lleno y vacío como registros, ya que son nodos de memoria.

Una razón particular (del enlace a continuación) por la cual no funcionará en su caso: 4. Los elementos de cable no se pueden usar como el lado izquierdo de un signo = o < = en un bloque always @.

Una forma fácil de mantener un seguimiento es usar _q al final de cualquier nodo que se declara como registro (como full_q y empty_q para los bits almacenados). De esa manera, si tiene una copia exacta de este nodo que no está almacenado, puede llamarlo lleno y vacío.

Las señales que no están bloqueadas pueden dejarse como cables (wr_pnt, rd_pnt_sync_w, etc.). Estos deben ser impulsados continuamente, a diferencia de los registros.

También existen otras restricciones, el enlace de abajo hace un buen trabajo al describirlas todas. Si este enlace se rompe en el futuro, simplemente google wire vs reg verilog.

explicación bastante buena de la diferencia b / w wire and reg

    
respondido por el jbord39
1

Una guía ampliamente aceptada para la síntesis y la reutilización es que todas las salidas del módulo RTL deben registrarse. Si no se pueden registrar una o más salidas, entonces se debe reconsiderar el límite del módulo o la ubicación de esa lógica combinada. Para un módulo asíncrono como este, los sincronizadores pueden servir como registros de salida.

Del mismo modo, la salida de datos de lectura de RAM también se debe registrar, a menos que la RAM tenga registros de salida de lectura incorporados. Los datos de lectura no necesitan pasar a través de un sincronizador, porque la lectura de la RAM se produce en el dominio del reloj de lectura. La adición de registros de salida de RAM solucionaría el problema de "estropear el tiempo" en sentido descendente (de agregar registros completos / vacíos), ya que todo se retrasaría por igual en un ciclo.

Teniendo esto en cuenta, la lógica de lectura fuera de este módulo debe diseñarse para esperar un retraso de un ciclo entre re y rdata , o debe diseñarse con un protocolo de intercambio válido / listo para que espere para una señal valid , que también sería una salida registrada:

@(posedge clk_r) valid <= re & ~empty
    
respondido por el BHook

Lea otras preguntas en las etiquetas