¿Cuál es el propósito de este código Verilog para implementar Block RAM de 3 puertos?

LatticeMico32 (LM32) es una CPU libre de regalías que utilizo para estudiar cómo se puede implementar una CPU en orden segmentada.

Un punto problemático en particular con el que tengo problemas es cómo se implementa el archivo de registro. En una CPU canalizada, normalmente tendrá al menos tres accesos de memoria al archivo de registro en un ciclo de reloj determinado:

  • 2 lecturas para ambos operandos para las unidades de ejecución.
  • 1 escritura desde la etapa de reescritura

LM32 proporciona tres formas de implementar el archivo de registro:

  • Bloquee la inferencia de RAM donde las lecturas/escrituras tienen lógica adicional para evitar lecturas/escrituras paralelas.
  • Bloquee la inferencia de RAM con relojes desfasados ​​que no requieren lógica adicional.
  • Inferencia de RAM distribuida.

En la práctica, incluso con la inferencia de RAM distribuida, he visto tanto a Xilinx isecomo yosysa inferir un bloque de RAM con relojes de lectura y escritura en fase. Además, he visto que ambos sintetizadores infieren y al menos parte de la lógica adicional que el lm32 incluye explícitamente para un archivo de registro de RAM de bloque de borde positivo.

La lógica adicional inferida permite lecturas transparentes . Pegué el código aquí para la implementación explícita de lm32, pero sé por experimentación que yosysgenera efectivamente el mismo código para colocar el archivo de registro en el bloque de RAM en iCE40:

// Register file
`ifdef CFG_EBR_POSEDGE_REGISTER_FILE
   /*----------------------------------------------------------------------
    Register File is implemented using EBRs. There can be three accesses to
    the register file in each cycle: two reads and one write. On-chip block
    RAM has two read/write ports. To accomodate three accesses, two on-chip
    block RAMs are used (each register file "write" is made to both block
    RAMs).
    One limitation of the on-chip block RAMs is that one cannot perform a
    read and write to same location in a cycle (if this is done, then the
    data read out is indeterminate).
    ----------------------------------------------------------------------*/
   wire [31:0] regfile_data_0, regfile_data_1;
   reg [31:0]  w_result_d;
   reg         regfile_raw_0, regfile_raw_0_nxt;
   reg         regfile_raw_1, regfile_raw_1_nxt;

   /*----------------------------------------------------------------------
    Check if read and write is being performed to same register in current
    cycle? This is done by comparing the read and write IDXs.
    ----------------------------------------------------------------------*/
   always @(reg_write_enable_q_w or write_idx_w or instruction_f)
     begin
        if (reg_write_enable_q_w
            && (write_idx_w == instruction_f[25:21]))
          regfile_raw_0_nxt = 1'b1;
        else
          regfile_raw_0_nxt = 1'b0;

        if (reg_write_enable_q_w
            && (write_idx_w == instruction_f[20:16]))
          regfile_raw_1_nxt = 1'b1;
        else
          regfile_raw_1_nxt = 1'b0;
     end

   /*----------------------------------------------------------------------
    Select latched (delayed) write value or data from register file. If
    read in previous cycle was performed to register written to in same
    cycle, then latched (delayed) write value is selected.
    ----------------------------------------------------------------------*/
   always @(regfile_raw_0 or w_result_d or regfile_data_0)
     if (regfile_raw_0)
       reg_data_live_0 = w_result_d;
     else
       reg_data_live_0 = regfile_data_0;

   /*----------------------------------------------------------------------
    Select latched (delayed) write value or data from register file. If
    read in previous cycle was performed to register written to in same
    cycle, then latched (delayed) write value is selected.
    ----------------------------------------------------------------------*/
   always @(regfile_raw_1 or w_result_d or regfile_data_1)
     if (regfile_raw_1)
       reg_data_live_1 = w_result_d;
     else
       reg_data_live_1 = regfile_data_1;

   /*----------------------------------------------------------------------
    Latch value written to register file
    ----------------------------------------------------------------------*/
   always @(posedge clk_i `CFG_RESET_SENSITIVITY)
     if (rst_i == `TRUE)
       begin
          regfile_raw_0 <= 1'b0;
          regfile_raw_1 <= 1'b0;
          w_result_d <= 32'b0;
       end
     else
       begin
          regfile_raw_0 <= regfile_raw_0_nxt;
          regfile_raw_1 <= regfile_raw_1_nxt;
          w_result_d <= w_result;
       end

// Two Block RAM instantiations follow to get 2 read/1 write port.

Las lecturas transparentes garantizan que las escrituras en la misma dirección que una lectura de otro puerto también aparecen en el puerto de lectura en el mismo borde del reloj (suponga que los relojes de lectura y escritura son síncronos). La canalización lm32 se basa en los puertos de lectura que reflejan inmediatamente el valor de registro reescrito.

Sin embargo, hay una lógica adicional de pegamento para lidiar con un estancamiento de la tubería y no estoy seguro de lo que logra este código , incluso después de estudiar la implementación de la CPU en detalle. He comentado el código a continuación para mayor comodidad:

 ifdef CFG_EBR_POSEDGE_REGISTER_FILE
 // Buffer data read from register file, in case a stall occurs, and watch for
 // any writes to the modified registers
 always @(posedge clk_i `CFG_RESET_SENSITIVITY)
 begin
    if (rst_i == `TRUE)
    begin
        use_buf <= `FALSE;
        reg_data_buf_0 <= {`LM32_WORD_WIDTH{1'b0}};
        reg_data_buf_1 <= {`LM32_WORD_WIDTH{1'b0}};
    end
    else
    begin
        if (stall_d == `FALSE)
            use_buf <= `FALSE;
        else if (use_buf == `FALSE)
        begin
            // If we stall in the decode stage, unconditionally
            // buffer the register file values from the read ports.
            // They will be used instead when the stall ends.
            reg_data_buf_0 <= reg_data_live_0;
            reg_data_buf_1 <= reg_data_live_1;
            use_buf <= `TRUE;
        end
        if (reg_write_enable_q_w == `TRUE)
        // If either register's address matches the register
        // to be written back, replace the buffered read values.
        begin
            if (write_idx_w == read_idx_0_d)
                reg_data_buf_0 <= w_result;
            if (write_idx_w == read_idx_1_d)
                reg_data_buf_1 <= w_result;
        end
    end
end
endif

¿Por qué se requiere esta lógica, y solo para relojes de lectura/escritura en fase ? ¿Es este código similar a cualquier otro idioma común para lidiar con la lectura de datos correctos de la RAM de bloque implementada en FPGA (es decir, similar a cómo los sintetizadores inferirán un código de lectura/escritura transparente)?

Me habría imaginado que durante una parada de la etapa de decodificación de una CPU RISC, la lógica que garantiza lecturas transparentes sería suficiente para garantizar que los puertos de lectura tengan la salida de datos correcta cuando finalice la parada. En el momento en que haya pasado un ciclo de reloj completo después de que se haya producido una lectura/escritura simultánea en la misma dirección en diferentes puertos, ¿no deberían las salidas de datos de los puertos de lectura haberse asentado en el nuevo valor, por lo que solo necesitamos almacenar en búfer? los datos más inmediatos escritos en el puerto de escritura?

He sintetizado esta CPU muchas veces utilizando solo la inferencia de RAM distribuida (inferida como RAM de bloque), por lo que esta lógica no es necesaria o isees yosyscapaz de inferir la lógica de pegamento adicional requerida.

Respuestas (1)

Esto ha estado sin respuesta durante un día y creo que sé por qué. Si el código Verilog se vuelve un poco más grande y complejo, es muy difícil ver todas las relaciones temporales. Incluso si el usuario pone muchos comentarios (dijiste que agregaste los comentarios, así que supongo que no fue el caso aquí) encuentras que tienes que ejecutar la simulación para ver cómo encaja todo.
Para averiguar por qué se necesita ese código, elimínelo y vea dónde fallan las cosas.

Habiendo dicho eso, puedo pensar en un posible escenario.

  • Si el archivo de registro es una memoria síncrona, la salida de datos se retrasa un ciclo.
  • Las direcciones del archivo de registro no se detienen inmediatamente en una parada del decodificador.
  • Los datos que salen se pierden durante la parada, por lo que deben capturarse.

Esto no es fácil de describir con palabras, así que aquí hay un diagrama de tiempo de ese posible escenario:

ingrese la descripción de la imagen aquí

En el ciclo 2 se detecta la necesidad de una parada. Por alguna razón, las direcciones no se pueden detener.
El ciclo 3 es nuestro ciclo de pérdida adicional. Ahora el puesto ha llegado a la lógica de dirección, por lo que se detendrá.
En el Ciclo 4 queremos continuar pero se pierde el dato 'M1'. A menos que lo almacenemos durante la parada, utilícelo en el ciclo 4 y en el ciclo 5 todo vuelve a estar bien.

Tenga en cuenta que con un archivo de registro asíncrono, el problema no ocurre.


Como nota al margen: no estoy de acuerdo con su comentario "amortiguar incondicionalmente los valores del archivo de registro". No es 'incondicionalmente' porque el código seguido "if (reg_write_enable_q_w ..." tiene prioridad. Eso significa que hay un no hay condición de escritura".

No he tenido tiempo de examinar en detalle apreciable, o crear un caso de prueba con un comportamiento equivalente y un bloque de RAM inferido, pero su respuesta es correcta en este caso (excepto que M0 debe conservarse durante la parada; no es posible detener Adrs se actualice a A1 incluso cuando se recibe la señal de bloqueo). Acepto y agregaré mi propia respuesta elaborando cuando tenga la oportunidad.