Многотактное процессорное ядро
Попробуем немного улучшить предыдущий вариант процессорного ядра, проведя повторную ревизию модулей ядра, проанализировав систему команд и возможные преобразования сигналов и данных, которые необходимо будет сделать.
Подход к построению многотактного процессорного ядра выбран не совсем классический, введем изначально некоторое количество т.н. "неархитектурных регистров" которые будут хранить промежуточные состояния.
И так, еще раз наши микроархитектурные блоки однотактного варианта:
- rv_pc - программный счетчик;
- rv_mem - блок памяти (программная и оперативная;
- rv_desh - дешифратор команд (слова-инструкции);
- rv_imm - формирователь непосредственного значения из слова-интсрукции;
- rv_reg_file - файл-регистр
- rv_ops_mux - коммутатор операндов для АЛУ;
- rv_cmp - формирователь сигнала разрешения перехода;
- rv_alu_v - АЛУ;
- rv_rez_mux - коммутатор результатов;
- rv_csr - блок регистров специального назначения.
Пробуем рассмотреть возможные сигналы и действия на каждом из этапов. Промежуточные результаты каждого из этапов фиксируются в неархитектурных регистрах. Да, таким образом количество задействованных ресурсов возрастает, но в качестве упражнения такой подход может иметь место.
Предлагается разбиение функциональности на несколько уровней:
- L0 - выборка текущего значения программного счетчика (точнее пары значений - PC и РС+4).
- L1 - выборка слова-инструкции из памяти.
- L2 - дешифрация инструкции - выделение адресов операндов, типа инструкции, формирование управляющих сигналов.
- L3 - выборка операндов из файл-регистров (в рассматриваемой микроархитектуре - это файл-регистр и блок регистров специальных функций).
- L4 - работа АЛУ.
- L5 - чтение/запись данных из оперативной памяти.
- L6 - запись данных в файл-регистры и нового значения в программный счётчик.
Вся структура управляется кольцевым счетчиком поочередно разрешающим работу модулей каждого из уровней. Для этого модули процессорного ядра должны быть дополнены сигналами разрешения работы (особенно модули с элементами памяти). Такой модификации подвергнутся - программный счетчик, добавим разрешение на запись в файл-регистр (запись результата в порт Rd/регистра по адресу rd), разрешение на запись в блок регистров специальных функций.
"Новый" программный счетчик:
module rv_pc
#(
parameter WIDTH=32
)
(
input clk,
input rst_n,
input en,
input pc_load,
input [WIDTH-1:0] pc_next,
output [WIDTH-1:0] pc,
output reg [WIDTH-1:0] pc_plus
);
reg [WIDTH-1:0] pc_reg;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
pc_reg <= 32'h00;
else
if (en) begin
if(pc_load==1'b1)
pc_reg <= pc_next;
else
pc_reg <= pc_reg + 3'h4;
end
end
//end///*
assign pc = pc_reg;
always @ *
begin
pc_plus <= pc_reg + 3'h4;
end
// */
Endmodule
Модификация файл-регистра:
module rv_reg_file
#(
parameter DATA_WIDTH=32,
parameter ADDR_WIDTH=5
)
( input clk,
input [(ADDR_WIDTH-1):0] rs1,
input [(ADDR_WIDTH-1):0] rs2,
input [(ADDR_WIDTH-1):0] rd,
output reg [(DATA_WIDTH-1):0] Rs1_out,
output reg [(DATA_WIDTH-1):0] Rs2_out,
input [(DATA_WIDTH-1):0] Rd_input,
input we,
input en
);
reg [DATA_WIDTH-1:0] ram[0:2**ADDR_WIDTH-1];
wire rd_nonzero;
wire rs1_nonzero;
wire rs2_nonzero;
assign rd_nonzero = |rd;
assign rs1_nonzero = |rs1;
assign rs2_nonzero = |rs2;
always @ (posedge clk)
begin
if (en & we & rd_nonzero) ram[rd] <= Rd_input;
end
always @ (*)
begin
Rs1_out <= rs1_nonzero ? ram[rs1] : 32'h0;
Rs2_out <= rs2_nonzero ? ram[rs2] : 32'h0;
end
endmodule
И блок регистров специального назначения (пока это просто код-заглушка):
module rv_csr
#(
parameter DATA_WIDTH=32,
parameter ADDR_WIDTH=12,
parameter CSR_size = 32
)
( input clk,
input [(ADDR_WIDTH-1):0] csr_addr,
input [(DATA_WIDTH-1):0] csr_in,
output reg [(DATA_WIDTH-1):0] csr_out,
input csr_wr,
input en
);
// csr register file
reg [ADDR_WIDTH-1:0] csr_reg[0:CSR_size-1];
always @ (posedge clk)
begin
case (csr_addr)
32'h0 : begin
if (csr_wr&en) begin
csr_reg[0] <= csr_in;
end
else begin
csr_out <= csr_reg[0];
end
end
default: begin
csr_out<=32'h0;
end
endcase
end
endmodule
Уровни "разделяются" промежуточными регистрами (обычные регистры хранения), которые фиксируют данные (регистры R0 - R7). Ряд сигналов просто "пробрасываются" по этапам (напрямую между регистрами) в том случае, если они не задействованы в них, но требуются на последующих.
L0 - выборка текущего значения программного счетчика
Текущее значение программного счетчика и его инкремента (PC+4) запоминаются в регистре R1. Выходными линиями, соответствующими PC, адресуется память (память программ). Также пара PC, PC+4 с выхода R1 будет подана на вход регистра R2. Программному счетчику добавлен вход разрешения работы (en).
L1 - выборка слова-инструкции из памяти
В регистре R2 фиксируется слово инструкции из памяти и текущие значения пары PC, PC+4. Выход регистра подается на декодер инструкции и на схемы формирования непосредственного значения (immediate). Значения счетчиков передаются на вход следующего регистра (R3).
wire [31:0]l0r1_PC;
wire [31:0]l0r1_PCplus;
wire [31:0]r1l1_PC;
wire [31:0]r1l1_PCplus;
rv_pc pc( // programm counter
.clk(clk),
.rst_n(rst),
.en(en_level[6]),
.pc_load(r6l6_PC_load),
.pc_next(r6l6_Rez),
.pc(l0r1_PC),
.pc_plus(l0r1_PCplus)
);
wire [255:0] r1_in;
assign r1_in = {l0r1_PC, l0r1_PCplus, 192'h0};
wire [255:0] r1_out;
wire [191:0] r1_null;
assign {r1l1_PC, r1l1_PCplus, r1_null} = r1_out;
rv_r_reg R1(
.clk(clk),
.rst_n(rst),
.en(en_level[0]),
.r_in(r1_in),
.r_out(r1_out)
);
|
Адрес инструкции на память подается со сдвигом на два бита - таким образом учитывается, что реализованная в примере микроархитектура использует 32-битную память.
wire [31:0] l1r2_iw;
wire [31:0] l1r2_PC;
wire [31:0] l1r2_PCplus;
rv_mem mem( // system memory
.clk(clk),
//.i_addr(32'h0), //(r1l1_PC),
.i_addr(r1l1_PC>>2),
.code_out(l1r2_iw),
.d_addr(r5l5_Rez>>2),
.d_out(l5r6_Mem_data),
.d_in(r5l5_Rs2_reg),
.we(r5l5_mem_wr),
.en(en_level[5])
);
// L1_R2
wire [31:0]r2l2_iw;
wire [31:0]r2l2_PC;
wire [31:0]r2l2_PCplus;
wire [255:0] r2_in;
wire [255:0] r2_out;
assign r2_in = {l1r2_iw, r1l1_PC, r1l1_PCplus, 160'h0};
wire [159:0] r2_null;
assign {r2l2_iw, r2l2_PC, r2l2_PCplus, r2_null} = r2_out;
rv_r_reg R2(
.clk(clk),
.rst_n(rst),
.en(en_level[1]),
.r_in(r2_in),
.r_out(r2_out)
);
|
L2 - дешифрация инструкции
Из слова-инструкции идет выделение адресов регистров-операндов, типа (опкодов и функциональных полей) инструкции, формирование управляющих сигналов на запись в файл-регистры (регистров общего назначения и специальных регистров).
wire [4:0] l2r3_rs1;
wire [4:0] l2r3_rs2;
wire [6:0] l2r3_op;
wire [2:0] l2r3_fn3;
wire [6:0] l2r3_fn7;
wire [4:0] l2r3_rd;
wire l2r3_rd_wr;
wire l2r3_mem_wr;
wire l2r3_csr_wr;
wire [31:0] l2r3_imm;
wire [31:0] l2r3_PC;
wire [31:0] l2r3_PCplus;
rv_desh desh( // instruction decoder
.inst(r2l2_iw),
.opcode(l2r3_op),
.rs1(l2r3_rs1),
.rs1_en(),
.rs2(l2r3_rs2),
.rs2_en(),
.rd(l2r3_rd),
.rd_wr(l2r3_rd_wr),
.funct3(l2r3_fn3),
.f3_en(),
.funct7(l2r3_fn7),
.f7_en(),
.mem_en(),
.mem_wr(l2r3_mem_wr),
.csr_en(),
.csr_wr(l2r3_csr_wr),
.pc_load()
);
rv_imm immed( // immediate decoder
.inst(r2l2_iw),
.imm(l2r3_imm)
);
wire [4:0] r3l3_rs1;
wire [4:0] r3l3_rs2;
wire [6:0] r3l3_op;
wire [2:0] r3l3_fn3;
wire [6:0] r3l3_fn7;
wire [4:0] r3l3_rd;
wire r3l3_rd_wr;
wire r3l3_mem_wr;
wire r3l3_csr_wr;
wire [31:0] r3l3_imm;
wire [31:0] r3l3_PC;
wire [31:0] r3l3_PCplus;
wire [255:0] r3_in;
wire [255:0] r3_out;
assign r3_in = {l2r3_rs1, l2r3_rs2, l2r3_op, l2r3_fn3, l2r3_fn7, l2r3_rd, l2r3_rd_wr,
l2r3_mem_wr, l2r3_csr_wr, l2r3_imm, r2l2_PC, r2l2_PCplus, 125'h0};
wire [124:0] r3_null;
assign {r3l3_rs1, r3l3_rs2, r3l3_op, r3l3_fn3, r3l3_fn7, r3l3_rd, r3l3_rd_wr,
r3l3_mem_wr, r3l3_csr_wr, r3l3_imm, r3l3_PC, r3l3_PCplus, r3_null} = r3_out;
rv_r_reg R3(
.clk(clk),
.rst_n(rst),
.en(en_level[2]),
.r_in(r3_in),
.r_out(r3_out)
);
|
L3 - выборка операндов из файл-регистров
Адреса регистров-операндов подаются на адресные входы файл-регистров (основного и регистров специальных функций). Оставшиеся адрес регистра-приёмника, числовая константа, программный счетчик, сигналы разрешения записью поступают на входы регистра R4.


