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:
LM32 proporciona tres formas de implementar el archivo de registro:
En la práctica, incluso con la inferencia de RAM distribuida, he visto tanto a Xilinx ise
como yosys
a 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 yosys
genera 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 ise
es yosys
capaz de inferir la lógica de pegamento adicional requerida.
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.
Esto no es fácil de describir con palabras, así que aquí hay un diagrama de tiempo de ese posible escenario:
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".
cr1901