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.