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

4

LatticeMico32 (LM32) es una CPU sin royalties que utilizo para estudiar cómo se puede implementar una CPU en orden canalizada.

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:

  • Inferencia de RAM de bloque donde las lecturas / escrituras tienen lógica adicional para evitar lecturas / escrituras paralelas.
  • Inferencia de RAM de bloque con relojes fuera de fase que no requieren lógica adicional.
  • Inferencia de RAM distribuida.

En la práctica, incluso con inferencia de RAM distribuida, he visto que tanto Xilinx ise como yosys infieren un bloque RAM con los 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 lm32 incluye explícitamente para un archivo de registro de RAM de bloque de borde positivo.

La lógica extra inferida habilita lecturas transparentes . He pegado el código aquí para la implementación explícita de lm32, pero sé por experiencia que yosys genera efectivamente el mismo código para colocar el archivo de registro en el bloque 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 aseguran que las escrituras en la misma dirección que una lectura desde otro puerto también aparezcan 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 de lm32 se basa en los puertos de lectura que reflejan inmediatamente el valor del registro de devolución por escrito.

Sin embargo, hay una lógica de pegamento adicional para lidiar con un bloqueo de la tubería y no estoy seguro de qué este código se cumple, incluso después de estudiar en detalle la implementación de la CPU. He comentado el siguiente código 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 los relojes de lectura / escritura de en fase ? ¿Es este código similar a cualquier otro lenguaje común para tratar con la lectura de los datos correctos de la RAM del bloque implementado en los FPGA (es decir, similar a cómo los sintetizadores deducirán el código de lectura / escritura transparente)?

Me hubiera imaginado que durante un bloqueo de la etapa de decodificación de una CPU RISC, la lógica que asegura que las lecturas transparentes sería suficiente para garantizar que los puertos de lectura tengan la salida de datos correcta cuando finalice el bloqueo. En el momento en que haya transcurrido 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 haber llegado al nuevo valor? ¿Necesita almacenar los datos más inmediatos escritos en el puerto de escritura?

He sintetizado esta CPU muchas veces utilizando solo la inferencia de RAM distribuida (deducida como RAM de bloque), por lo que no se requiere esta lógica, o ise y yosys son capaces de inferir la lógica de pegamento adicional requerida.

    
pregunta cr1901

1 respuesta

2

Esto ha estado sin respuesta por un día y creo que sé por qué. Si el código de 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 en (Usted dijo que agregó los comentarios para Supongo que no fue el caso aquí) encuentra que tiene que ejecutar la simulación para ver cómo todo se une.
Para averiguar por qué se necesita ese código, elimínelo y vea dónde van las cosas mal.

Habiendo dicho eso, puedo pensar en un escenario posible .

  • 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 de decodificador.
  • Los datos que salen se pierden durante el bloqueo, por lo que deben capturarse.

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

Enelciclo2sedetectalanecesidaddeunbloqueo.Poralgunarazón,lasdireccionesnosepuedendetener.
Elciclo3esnuestrocicloadicionaldepérdida.Ahoraelpuestohallegadoalalógicadeladirecciónporloquesedetendrá.
EnelCiclo4queremoscontinuarperolosdatos'M1'sepierden.Amenosqueloalmacenemosdurantelaparada,utilíceloenelciclo4yenelciclo5todoestábienotravez.

Tengaencuentaqueconunarchivoderegistrosincrónicoelproblemanoseproduce.

Comonotaalmargen:noestoydeacuerdocontucomentario"Almacenar incondicionalmente los valores del archivo de registro" No es "incondicional" porque el código seguido "if (reg_write_enable_q_w ..." tiene prioridad. Eso significa que hay una condición implícita de "si no hay escritura").

    
respondido por el Oldfart

Lea otras preguntas en las etiquetas