Приступая к реализации собственно процессорного узла для определенного набора команд, мы ступаем в область создания т.н. микроархитектуры процессора - его уникальной для каждой реализации внутренней структуры. По факту уже, разбирая вариант построения декодера команд в предыдущей части, мы уже начали работу над микроархитектурой.
Вернемся к основным архитектурным блокам процессора:
Дополнительными блоками не входящими в архитектурные блоки процессорного ядра, но необходимые для работы:
Согласно теории - однотактная микроархитектура подразумевает выполнение команды процессора за один такт. Все операции выполняются за один такт, эта микроархитектура не требует никакого неархитектурного состояния. Все исполнительные узлы процессорного ядра (за исключением, конечно же его регистров, определенных или других элементов с памятью состояния) архитектурой представлены комбинационно-логическими схемами. Минимальная длительность такта при этом определяется временем выполнения самой медленной команды (например, временем распространения сигнала переноса в сумматоре).
Файл-регистр данных архитектур устроен довольно просто - особенно для базовых конфигураций архитектур.
Тридцать два - 32-разрядных регистра, два порта на чтение данных (для упрощения делаем их асинхронными), один синхронный порт на запись данных. Регистр файла с адресом 0 - всегда при чтении выдает ноль, на запись не работает.
Файл-регистр принимает адреса регистров-операндов (rs1, rs2), асинхронно выдает содержимое соответствующих регистров на выходные порты. Параллельно при наличии разрешающего сигнала записывает в регистр с адресом rd значение, поданное на вход.
При такой реализации выдача значений регистров производится всегда, безотносительно к операции, соответственно, дальнейшая "судьба" данных регистров, поданных на выходы зависит от последующих узлов и требует аккуратного обращения.
Код файл-регистра:
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
);
// RAM array
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;
// read RAM content from file
//initial
//$readmemh("ram.txt",ram);
always @ (posedge clk)
begin
if (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
Тест-бенч:
`include "rv_reg_file.v"
`timescale 10ns/1ns
/*
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
*/
module testbench; // input and output test signals
reg clk;
reg [4:0] rs1;
reg [4:0] rs2;
reg [4:0] rd;
wire [31:0] Rs1_out;
wire [31:0] Rs2_out;
reg [31:0] Rd_input;
reg we;
rv_reg_file dut ( clk, rs1, rs2, rd, Rs1_out, Rs2_out, Rd_input, we);
integer i;
initial
begin
clk = 1'b0; rs1 = 5'h00; rs2 = 5'h01; rd = 5'h02; we=1'b0;
#10;
i = 0;
rs1 <= #i 5'h00; rs2 <= #i 5'h01; rd <= #i 5'h02; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
rs1 <= #i 5'h01; rs2 <= #i 5'h01; rd <= #i 5'h01; Rd_input <= #i 32'h77; we <= #i 1'b0; i = i + 20; //we = 1'b1;
rs1 <= #i 5'h02; rs2 <= #i 5'h01; rd <= #i 5'h02; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20; //we = 1'b1;
rs1 <= #i 5'h03; rs2 <= #i 5'h02; rd <= #i 5'h03; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
rs1 <= #i 5'h04; rs2 <= #i 5'h04; rd <= #i 5'h04; Rd_input <= #i 32'h01; we <= #i 1'b1; i = i + 20;
rs1 <= #i 5'h05; rs2 <= #i 5'h04; rd <= #i 5'h05; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20; //we = 1'b0;
rs1 <= #i 5'h00; rs2 <= #i 5'h01; rd <= #i 5'h06; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
rs1 <= #i 5'h00; rs2 <= #i 5'h01; rd <= #i 5'h07; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
for(i=0; i < 80; i = i+1)
#10 clk = ~clk;
end
initial
$monitor("Rs1=%h Rs2=%h ",
Rs2_out, Rs2_out);
initial
$dumpvars; //iverilog dump init
endmodule
(рис 5.1) Временные диаграммы тестирования файл-регистра
Арифметико-логическое устройство является одним из центральных узлов любого процессора. Структура АЛУ и его функциональность определяется ISA, поэтому вернемся к еще раз к базовым операциям RISC-V.
Операции с регистрами и константами (immediate/непосредственными значениями) - тип-I - оперируют с регистрами и непосредственными операндами - арифметико-логические операции, сравнения с константами, загрузкой данных в регистры
| funct3 | opcode | I-type | |
|---|---|---|---|
Rd = Rs1+imm (NOP encoded as ADDI x0, x0, 0.) |
000 | 0010011 | ADDI |
Rd= 1 if Rs< imm else 0 |
010 | 0010011 | SLTI |
Rd= 1 if Rs< uimm else 0 |
011 | 0010011 | SLTIU |
Rd = rs1 XOR imm |
100 | 0010011 | XORI |
Rd = rs1 OR imm |
110 | 0010011 | ORI |
Rd = rs1 AND imm |
111 | 0010011 | ANDI |
| opcode | U-type | |
|---|---|---|
| $$Rd = imm[31:12][0_{12}]$$ | 0110111 | LUI |
| $$Rd=PC+ imm[31:12] [0_{12}]$$ | 0010111 | AUIPC |
| funct7 | funct3 | opcode | R-type | |
|---|---|---|---|---|
Rd = Rs1 + Rs2 |
0000000 | 000 | 0110011 | ADD |
Rd = Rs1 - Rs2 |
0100000 | 000 | 0110011 | SUB |
Rd = Rs1 << Rs2 |
0000000 | 001 | 0110011 | SLL |
Rd= 1 if Rs1 < Rs2 else 0 |
0000000 | 010 | 0110011 | SLT |
Rd= 1 if Rs1 < Rs2 else 0(unsign) SLTU rd, x0, rs2 sets rd to 1 if rs2 is not equal to zero |
0000000 | 011 | 0110011 | SLTU |
Rd = Rs1 XOR Rs2 |
0000000 | 100 | 0110011 | XOR |
Rd = Rs1 >> Rs2 |
0000000 | 101 | 0110011 | SRL |
Rd = Rs1 >> Rs2 (sign ext) |
0100000 | 101 | 0110011 | SRA |
Rd = Rs1 OR Rs2 |
0000000 | 110 | 0110011 | OR |
Rd = Rs1 AND Rs2 |
0000000 | 111 | 0110011 | AND |
Rd = Rs1 << imm |
0000000 | 001 | 0010011 | SLLI |
Rd = Rs1 >> imm (0 ext) |
0000000 | 101 | 0010011 | SRLI |
Rd = Rs1 >> imm (sign ext) |
0100000 | 101 | 0010011 | SRAI |
Переходы. Все инструкции условных переходов используют формат команд B-типа (переход в пределах 4КБ).
| funct3 | opcode | B-type | |
|---|---|---|---|
If (Rs1 == Rs2)
PC=PC+imm |
000 | 1100011 | BEQ |
If (Rs1 != Rs2)
PC=PC+imm |
001 | 1100011 | BNE |
If (Rs1 <Rs2)
PC=PC+imm |
100 | 1100011 | BLT |
If (Rs1 >Rs2)
PC=PC+imm |
101 | 1100011 | BGE |
If (Rs1 <Rs2) -usign
PC=PC+imm |
110 | 1100011 | BLTU |
If (Rs1 >Rs2) -usign
PC=PC+imm |
111 | 1100011 | BGEU |
Безусловный переход относительно значения регистра с записью в регистр связи
| funct3 | opcode | I-type | |
|---|---|---|---|
Rd = PC+4 PC= Rs + imm |
000 | 1100111 | JALR |
Команда простого безусловного перехода с сохранением адреса возврата в регистр (переход без сохранения адреса возврата осуществляется, если в качестве регистра указан x0)
| Imm[20|10:1|11|19:12] | rd | opcode | J-type | |
|---|---|---|---|---|
Rd = PC + 4 PC = PC + imm |
imm[20|10:1|11|19:12] | rd | 1101111 | JAL |
Инструкции записи в память - S-тип.
| imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | S-type | |
|---|---|---|---|---|---|---|---|
[Rs1+imm]=Rs2(byte) |
imm[11:5] | rs2 | rs1 | 000 | imm[4:0] | 0100011 | SB |
[Rs1+imm]=Rs2(half) |
imm[11:5] | rs2 | rs1 | 001 | imm[4:0] | 0100011 | SH |
[Rs1+imm]=Rs2(word) |
imm[11:5] | rs2 | rs1 | 010 | imm[4:0] | 0100011 | SW |
Чтение из памяти (I-тип, как бы неожиданно это не выглядело)
| funct3 | opcode | I-type | |
|---|---|---|---|
Rd = @(Rs + imm) 8bit |
000 | 0000011 | LB |
Rd = @(Rs + imm) 16bit |
001 | 0000011 | LH |
Rd = @(Rs + imm) 32bit |
010 | 0000011 | LW |
Rd = @(Rs + imm) 8bit zero ext |
100 | 0000011 | LBU |
Rd = @(Rs + imm) 16bit zero ext |
101 | 0000011 | LHU |
| funct3 | Опкод | I-type | |
|---|---|---|---|
Rd= CSR[imm] CSR = Rs1 |
001 | 1110011 | CSRRW |
Rd= CSR[imm] CSR[imm] = CSR[imm] or Rs1 |
010 | 1110011 | CSRRS |
Rd= CSR[imm] CSR[imm] = CSR[imm] and (~Rs1) |
011 | 1110011 | CSRRC |
Rd= CSR[imm] CSR[imm] = zimm |
101 | 1110011 | CSRRWI |
Rd= CSR[imm] CSR[imm] = CSR[imm] or zimm |
110 | 1110011 | CSRRSI |
Rd= CSR[imm] CSR[imm] = CSR[imm] and (~zimm) |
111 | 1110011 | CSRRCI |
Анализируем далее систему команд:
Программный счётчик в нашей микро архитектуре уже вынесен в отдельный блок с функциями счетчика с опцией параллельной загрузки - при линейном выполнении программного кода PC автоматически инкрементируется (+4 к текущему значению), в случае переходов - инициируется новым значением.
Поскольку в командах перехода со связыванием (jal, jarl) в регистр связи записывается значение инкрементированное значение программного счётчика (PC+4), в его модуле организуем параллельно к выходу значения PC и вывод PC+4. Такое решение, возможно, не слишком каноническое, но оно позволяет уже иметь значение PC+4 "под рукой".
module rv_pc
#(
parameter WIDTH=32
)
(
input clk,
input rst_n,
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(pc_load==1'b1)
pc_reg <= pc_next;
else
pc_reg <= pc_reg + 3'h4;
end
///*
assign pc = pc_reg;
always @ *
begin
pc_plus <= pc_reg + 3'h4;
end
// */
endmodule
Для поддержки операций условных переходов выделим отдельный блок сравнения принимающий на вход значения регистров, код операции сравнения (funct3) и выдающий сигнал, разрешающий или не разрешающий переход. Модуль представляет собой относительно небольшой модуль на case-ах.
В этот же модуль вынесем формирование сигнала разрешения параллельной загрузки в регистр программного счётчика нового значения - при командах переходов и возвратов из обработчиков прерываний (mret, например, или также можно добавить в этот список команды ecall, ebreak, хотя для ядер микроконтроллерного класса их поддержка не сильно актуальна).
module rv_cmp
( input [6:0] opcode,
input [2:0] funct3,
input [31:0] Rs1,
input [31:0] Rs2,
output reg pc_new
);
always @ *
begin
case (opcode)
7'b11011_11 : begin
$display("JAL");
pc_new <= 1'b1;
end
7'b11001_11 : begin
$display("JALR");
pc_new <= 1'b1;
end
7'b11100_11 : begin
$display("MRET/ecall/ect...");
pc_new <= 1'b1;
end
7'b11000_11 : begin // Relative (rs1) jump and link in register
$display("BRANCH");
case (funct3)
3'h0: begin
pc_new <= Rs1 == Rs2;
end
3'h1: begin
pc_new <= Rs1 != Rs2;
end
3'h4: begin
pc_new <= Rs1 < Rs2;
end
3'h5: begin
pc_new <= Rs1 >= Rs2;
end
3'h6: begin
pc_new <= {1'b0,Rs1} < {1'b0,Rs2};
end
3'h7: begin
pc_new <= {1'b0,Rs1} >= {1'b0,Rs2};
end
default: begin
pc_new <= 1'b0;
end
endcase
end
default: begin
$display ("default");
pc_new <= 1'b0;
end
endcase
end
endmodule
Чтобы не усложнять структуру и поведенческое описание АЛУ (не делать его очень многовходовым - как минимум Rs1, Rs2, PC, imm, вынесем функционал выбора операндов на откуп специальному мультиплексору. АЛУ таким образом, имеет два входных порта-операнда, выход результата и вход управления выбором функции (зависит от полей opcode, funct3, funct7), что соответствует тому факту, что все операции в базовом наборе RV32I выполняются исключительно над двумя операндами - трех и более операндных действий не требуется.
Код ALU:
module rv_alu_v
#(
parameter WIDTH=32
)
(
input [6:0] opcode,
input [2:0] funct3,
input [6:0] funct7,
input [31:0] Op1,
input [31:0] Op2,
output reg [31:0] Rez
);
always @ *
begin
case (opcode)
//I-type
7'b00000_11 : begin // load data from mem
$display("LOAD");
Rez <= Op1 + Op2;
end
7'b00011_11 : begin // fence
$display("fence");
Rez <= Op1;
end
7'b00100_11 : begin // reg with immediate operations
$display("OP-IMM");
`include "alu_op_base.v"
end
7'b01100_11 : begin // reg with reg operations
$display("OP-REG");
`include "alu_op_base.v"
end
7'b11100_11 : begin // mret - system return
$display("RET_SYST or CSR");
`include "alu_op_CSR.v"
end
7'b11001_11 : begin // Relative (rs1) jump and link in register
$display("JALR");
Rez <= Op1 + Op2;
end
//J-type
7'b11011_11 : begin // pc relative jump and link in register
$display("JAL");
Rez <= Op1 + Op2;
end
//S-type
7'b01000_11 : begin // store register value in memory
$display("Store");
Rez <= Op1 + Op2;
end
//U-type
7'b01101_11 : begin // load upper immediate
$display("LUI");
Rez <= Op1;
end
7'b00101_11 : begin // add upper immediate to PC
$display("AUIPC");
Rez <= Op1 + Op2;
end
//B-type
7'b11000_11 : begin // conditional PC relative branch - PC+imm
$display("BRANCH");
Rez <= Op1 + Op2;
end
default: begin
$display ("default");
Rez <= Op1;
end
endcase
end
endmodule
Как видно, арифметико-логических операций в базовом варианте не много. Адресные операции (и для подготовки адресов переходов и для адресов обращения к памяти) сводятся к сложению.
Описания арифметических операций с регистрами (включая и операции с регистрами специального назначения) вынесены в отдельные файлы - "alu_op_base.v" и "alu_op_CSR.v". Это несколько сократило само текстовое описание АЛУ и проще их модифицировать при необходимости.
"alu_op_base.v" - набор арифметико-логических операций базового набора команд.
case (funct3)
3'h0: begin
if (funct7 == 7'h20) begin Rez <= Op1 + Op2; end
else begin Rez <= Op1 - Op2; end
end
3'h1: begin Rez <= Op1 << Op2; end
3'h2: begin Rez <= (Op1 < Op2) ? 32'h1 : 32'h0; end
3'h3: begin Rez <= ({1'b0,Op1} < {1'b0,Op2}) ? 32'h1 : 32'h0; end
3'h4: begin Rez <= Op1 << Op2; end
3'h5: begin
if (funct7 == 7'h20) begin Rez <= Op1 >>> Op2; end
else begin Rez <= Op1 >> Op2; end
end
3'h6: begin Rez <= Op1 | Op2; end
3'h7: begin Rez <= Op1 Op2; end
default: begin Rez <= Op2; end
endcase
Как упоминалось ранее, расширить набор математических операций в рамках набора I можно за счет поля funct7. В представленной версии кода достаточно, например, добавить выборы типа "case (funct7)" в вариантах выбора по полю funct3.
Набор операций практически типовой. Также можно видеть возможность расширения набора команд в рамках регистровых за счет дополнительного кодирования операций в поле ' funct7'.
Аналогичная ситуация наблюдается и в операциях с регистрами специального назначения.
case (funct3)
3'h0: begin Rez <= Op1; end
3'h1: begin Rez <= Op1; end
3'h2: begin Rez <= Op1 | Op2; end
3'h3: begin Rez <= Op1 (~Op2); end
3'h4: begin Rez <= Op1; end
3'h5: begin Rez <= Op1; end
3'h6: begin Rez <= Op1 | Op2; end
3'h7: begin Rez <= Op1 (~Op2); end
default: begin Rez <= Op1; end
endcase
Ещё один модуль который будет необходим - мультиплексор данных для записи в файл-регистр по адресу rd - в него будут записаны данные или с выхода АЛУ, или данные из оперативной памяти по вычисленному адресу.
module rv_rez_mux
#(
parameter WIDTH=32
)
(
input [6:0] opcode,
input [2:0] funct3,
input [6:0] funct7,
input [WIDTH-1:0] Rez,
input [WIDTH-1:0] Pc_plus,
input [WIDTH-1:0] Mem_data,
output reg [WIDTH-1:0] Rd
);
always @ *
begin
case (opcode)
//I-type
7'b00000_11 : begin // load data from mem
$display("LOAD");
Rd <= Rez;
end
7'b00011_11 : begin // fence
$display("fence");
Rd <= Rez;
end
7'b00100_11 : begin // reg with immediate operations
$display("OP-IMM");
Rd <= Rez;
end
7'b00100_11 : begin // reg with reg operations
$display("OP-REG");
Rd <= Rez;
end
7'b11100_11 : begin // mret - system return
$display("RET_SYST or CSR");
Rd <= Rez;
end
7'b11001_11 : begin // Relative (rs1) jump and link in register
$display("JALR");
case (funct3)
3'h0: begin Rd <= Pc_plus; end
default: begin Rd <= Pc_plus; end
endcase
end
//J-type
7'b11011_11 : begin // pc relative jump and link in register
$display("JAL");
Rd <= Pc_plus;
end
//S-type
7'b01000_11 : begin // store register value in memory
$display("Store");
Rd <= Rez;
end
//U-type
7'b01101_11 : begin // load upper immediate
$display("LUI");
Rd <= Rez;
end
7'b00101_11 : begin // add upper immediate to PC
$display("AUIPC");
case (funct3)
3'h0: begin Rd <= Rez; end
default: begin Rd <= Rez; end
endcase
end
//B-type
7'b11000_11 : begin // conditional PC relative branch - PC+imm
$display("BRANCH");
case (funct3)
3'h0: begin Rd <= Rez; end
3'h1: begin Rd <= Rez; end
3'h4: begin Rd <= Rez; end
3'h5: begin Rd <= Rez; end
3'h6: begin Rd <= Rez; end
3'h7: begin Rd <= Rez; end
default: begin Rd <= Rez; end
endcase
end
default: begin
$display ("default");
Rd <= Rez;
end
endcase
end
endmodule
И финальный блок формально не входящий в архитектуру процессорного ядра - оперативная память и память программ. С учетом специфики реализации блоков памяти в большинстве семейств FPGA память для процессорного ядра логичнее реализовать в виде (или на основе) двухпортовой памяти - один порт будет использован для чтения данных из памяти - функция памяти программ, второй порт - для чтения/записи данных - функция оперативной памяти.
Вариант кода описания модуля:
module rv_mem
#(
parameter DATA_WIDTH=32,
parameter ADDR_WIDTH=10
)
( input clk,
input [(ADDR_WIDTH-1):0] i_addr,
output reg [(DATA_WIDTH-1):0] code_out,
input [(ADDR_WIDTH-1):0] d_addr,
output reg [(DATA_WIDTH-1):0] d_out,
input [(DATA_WIDTH-1):0] d_in,
input we
);
// ROM array
reg [DATA_WIDTH-1:0] rom [0:2**ADDR_WIDTH-1] ;
// read ROM content from file
initial
$readmemh("rom.txt",rom);
always @ (posedge clk)
begin
if (we) rom[d_addr] <= d_in;
end
always @ (*)
begin
code_out <= rom[i_addr];
end
endmodule
В некоторых случаях удобнее будет реализовать две отдельные памяти - память для кода, память для данных - как некие сильно упрощенные варианты кэша инструкций и кэша данных.
Общая структура процессора (вместе с блоками памятей - и программ и данных) представлена на рис.5.2. Блоки выстроены в условно-линейном порядке прохождения сигнала, сквозные сигналы также проброшены вдоль блоков. Это один из портов блока памяти rv_mem адресуется программным счетчиком (выход РС), соответствующее выходное слово инструкции - iw поступает параллельно на входы модулей rv_desh и rv_imm. Данные модули "разбирают" поступившую инструкцию выделяя из нее адреса регистров-операндов файл-регистра, формируют непосредственное числовое значение (immediate), сопровождая декодированные элементами сигналами стробов.
Значения выбранных регистров регистрового файла, числовое значение (immediate), программный счетчик, значения выбранных регистров специального назначения (опционально) поступают на вход мультиплексора операндов rv_ops_mux, параллельно выхода файл-регистра подаются на вход компаратора (вернее его называть блоков разрешения перехода) совместно с комбинацией опкода с модификаторами (opcode, funct3, funct7).
Парные выходы мультиплексора также вместе с кодами операции поступают на вход АЛУ (модуль rv_alu_v). АЛУ применяется и для выполнения операций над операндами, и для расчета адресов - или адресов переходов, или адресов для обращения к памяти данных (ОЗУ). В зависимости от операции результат АЛУ, инкрементированный программный счетчик (PC+4) или данные из оперативной памяти записывается в регистр с номером rd файл-регистра.
Блок регистров специального назначения добавлен, как опциональный (выделен зелёным цветом).
Выходной сигнал блока сравнения/управления переносом и новое вычисленное значение подаются на соответствующие входы программного счетчика rv_pc.
Возможные недостатки предлагаемого решения:
(рис 5.2) Структурная схема процессора
Код-заглушка для блока CSR-регистров:
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
);
// 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) 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
И, собственно, "код объединяющий всё" (нотации и сигналы сохранены по возможности близко к рис.5.2):
`include "rv_pc.v" `include "rv_mem.v" `include "rv_desh.v" `include "rv_imm.v" `include "rv_reg_file.v" `include "rv_ops_mux.v" `include "rv_cmp.v" `include "rv_alu_v.v" `include "rv_rez_mux.v" `include "rv_csr.v" module rv_cpu_top #( parameter DATA_WIDTH=32, parameter ADDR_WIDTH=32 ) ( input clk, input rst, // input [(ADDR_WIDTH-1):0] Data_In, output[(DATA_WIDTH-1):0] Data_out ); // wires wire [(DATA_WIDTH-1):0] PC; wire [(DATA_WIDTH-1):0] PCplus; wire [(DATA_WIDTH-1):0] iw; wire [(DATA_WIDTH-1):0] imm; wire [(DATA_WIDTH-1):0] Rs1_reg; wire [(DATA_WIDTH-1):0] Rs2_reg; wire [(DATA_WIDTH-1):0] Rd_reg; wire [(DATA_WIDTH-1):0] Op1; wire [(DATA_WIDTH-1):0] Op2; wire [(DATA_WIDTH-1):0] Rez; wire [(DATA_WIDTH-1):0] CSR; wire [(DATA_WIDTH-1):0] Mem_data; wire [6:0] op; wire [2:0] fn3; wire [6:0] fn7; wire [4:0] rs1; wire [4:0] rs2; wire [4:0] rd; wire rd_wr; wire mem_wr; wire csr_wr; wire PC_load; //assign Data_In = CSR; assign Data_out = CSR; // CPU modules rv_pc pc( .clk(clk), .rst_n(rst), .pc_load(PC_load), .pc_next(REZ), .pc(PC), .pc_plus(PCplus) ); rv_mem mem( .clk(clk), .i_addr(PC), .code_out(iw), .d_addr(REZ), .d_out(Mem_data), .d_in(Rs2_reg), .we(mem_wr) ); rv_desh desh( .inst(iw), .opcode(op), .rs1(rs1), .rs1_en(), .rs2(rs2), .rs2_en(), .rd(rd), .rd_wr(rd_wr), .funct3(fn3), .f3_en(), .funct7(fn7), .f7_en(), .mem_en(), .mem_wr(mem_wr), .csr_en(), .csr_wr(crsr_wr), .pc_load() ); rv_imm immed( .inst(iw), .imm(imm) ); rv_reg_file reg_file( .clk(clk), .rs1(rs1), .rs2(rs2), .rd(rd), .Rs1_out(Rs1_reg), .Rs2_out(Rs2_reg), .Rd_input(Rd_reg), .we(rd_wr) ); rv_cmp cmp( .opcode(op), .funct3(fn3), .Rs1(Rs1_reg), .Rs2(Rs2_reg), .pc_new(PC_load) ); wire [31:0] Op1; wire [31:0] Op2; rv_ops_mux ops_mux( .opcode(r3l3_op), .funct3(r3l3_fn3), .funct7(r3l3_fn7), .Rs1(Rs1_reg), .Rs2(Rs2_reg), .imm(r3l3_imm), .PC(r3l3_PC), .CSR(CSR_mux), .Op1(Op1), .Op2(Op2) ); rv_alu_v alu_v( .opcode(op), .funct3(fn3), .funct7(fn7), .Op1(Op1), .Op2(Op2), .Rez(Rez) ); rv_rez_mux rez_mux( .opcode(op), .funct3(fn3), .funct7(fn7), .Rez(REZ), .Pc_plus(PCplus), .Mem_data(Mem_data), .Rd(Rd_reg) ); rv_csr csr( .csr_addr(imm[11:0]), .csr_in(REZ), .csr_out(CSR), .csr_wr(csr_wr) ); endmodule
Приступая к реализации собственно процессорного узла для определенного набора команд, мы ступаем в область создания т.н. микроархитектуры процессора - его уникальной для каждой реализации внутренней структуры. По факту уже, разбирая вариант построения декодера команд в предыдущей части, мы уже начали работу над микроархитектурой.
Вернемся к основным архитектурным блокам процессора:
Дополнительными блоками не входящими в архитектурные блоки процессорного ядра, но необходимые для работы:
Согласно теории - однотактная микроархитектура подразумевает выполнение команды процессора за один такт. Все операции выполняются за один такт, эта микроархитектура не требует никакого неархитектурного состояния. Все исполнительные узлы процессорного ядра (за исключением, конечно же его регистров, определенных или других элементов с памятью состояния) архитектурой представлены комбинационно-логическими схемами. Минимальная длительность такта при этом определяется временем выполнения самой медленной команды (например, временем распространения сигнала переноса в сумматоре).
Файл-регистр данных архитектур устроен довольно просто - особенно для базовых конфигураций архитектур.
Тридцать два - 32-разрядных регистра, два порта на чтение данных (для упрощения делаем их асинхронными), один синхронный порт на запись данных. Регистр файла с адресом 0 - всегда при чтении выдает ноль, на запись не работает.
Файл-регистр принимает адреса регистров-операндов (rs1, rs2), асинхронно выдает содержимое соответствующих регистров на выходные порты. Параллельно при наличии разрешающего сигнала записывает в регистр с адресом rd значение, поданное на вход.
При такой реализации выдача значений регистров производится всегда, безотносительно к операции, соответственно, дальнейшая "судьба" данных регистров, поданных на выходы зависит от последующих узлов и требует аккуратного обращения.
Код файл-регистра:
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
);
// RAM array
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;
// read RAM content from file
//initial
//$readmemh("ram.txt",ram);
always @ (posedge clk)
begin
if (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
Тест-бенч:
`include "rv_reg_file.v"
`timescale 10ns/1ns
/*
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
*/
module testbench; // input and output test signals
reg clk;
reg [4:0] rs1;
reg [4:0] rs2;
reg [4:0] rd;
wire [31:0] Rs1_out;
wire [31:0] Rs2_out;
reg [31:0] Rd_input;
reg we;
rv_reg_file dut ( clk, rs1, rs2, rd, Rs1_out, Rs2_out, Rd_input, we);
integer i;
initial
begin
clk = 1'b0; rs1 = 5'h00; rs2 = 5'h01; rd = 5'h02; we=1'b0;
#10;
i = 0;
rs1 <= #i 5'h00; rs2 <= #i 5'h01; rd <= #i 5'h02; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
rs1 <= #i 5'h01; rs2 <= #i 5'h01; rd <= #i 5'h01; Rd_input <= #i 32'h77; we <= #i 1'b0; i = i + 20; //we = 1'b1;
rs1 <= #i 5'h02; rs2 <= #i 5'h01; rd <= #i 5'h02; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20; //we = 1'b1;
rs1 <= #i 5'h03; rs2 <= #i 5'h02; rd <= #i 5'h03; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
rs1 <= #i 5'h04; rs2 <= #i 5'h04; rd <= #i 5'h04; Rd_input <= #i 32'h01; we <= #i 1'b1; i = i + 20;
rs1 <= #i 5'h05; rs2 <= #i 5'h04; rd <= #i 5'h05; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20; //we = 1'b0;
rs1 <= #i 5'h00; rs2 <= #i 5'h01; rd <= #i 5'h06; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
rs1 <= #i 5'h00; rs2 <= #i 5'h01; rd <= #i 5'h07; Rd_input <= #i 32'h01; we <= #i 1'b0; i = i + 20;
for(i=0; i < 80; i = i+1)
#10 clk = ~clk;
end
initial
$monitor("Rs1=%h Rs2=%h ",
Rs2_out, Rs2_out);
initial
$dumpvars; //iverilog dump init
endmodule
(рис 5.1) Временные диаграммы тестирования файл-регистра
Арифметико-логическое устройство является одним из центральных узлов любого процессора. Структура АЛУ и его функциональность определяется ISA, поэтому вернемся к еще раз к базовым операциям RISC-V.
Операции с регистрами и константами (immediate/непосредственными значениями) - тип-I - оперируют с регистрами и непосредственными операндами - арифметико-логические операции, сравнения с константами, загрузкой данных в регистры
| funct3 | opcode | I-type | |
|---|---|---|---|
Rd = Rs1+imm (NOP encoded as ADDI x0, x0, 0.) |
000 | 0010011 | ADDI |
Rd= 1 if Rs< imm else 0 |
010 | 0010011 | SLTI |
Rd= 1 if Rs< uimm else 0 |
011 | 0010011 | SLTIU |
Rd = rs1 XOR imm |
100 | 0010011 | XORI |
Rd = rs1 OR imm |
110 | 0010011 | ORI |
Rd = rs1 AND imm |
111 | 0010011 | ANDI |
| opcode | U-type | |
|---|---|---|
| $$Rd = imm[31:12][0_{12}]$$ | 0110111 | LUI |
| $$Rd=PC+ imm[31:12] [0_{12}]$$ | 0010111 | AUIPC |
| funct7 | funct3 | opcode | R-type | |
|---|---|---|---|---|
Rd = Rs1 + Rs2 |
0000000 | 000 | 0110011 | ADD |
Rd = Rs1 - Rs2 |
0100000 | 000 | 0110011 | SUB |
Rd = Rs1 << Rs2 |
0000000 | 001 | 0110011 | SLL |
Rd= 1 if Rs1 < Rs2 else 0 |
0000000 | 010 | 0110011 | SLT |
Rd= 1 if Rs1 < Rs2 else 0(unsign) SLTU rd, x0, rs2 sets rd to 1 if rs2 is not equal to zero |
0000000 | 011 | 0110011 | SLTU |
Rd = Rs1 XOR Rs2 |
0000000 | 100 | 0110011 | XOR |
Rd = Rs1 >> Rs2 |
0000000 | 101 | 0110011 | SRL |
Rd = Rs1 >> Rs2 (sign ext) |
0100000 | 101 | 0110011 | SRA |
Rd = Rs1 OR Rs2 |
0000000 | 110 | 0110011 | OR |
Rd = Rs1 AND Rs2 |
0000000 | 111 | 0110011 | AND |
Rd = Rs1 << imm |
0000000 | 001 | 0010011 | SLLI |
Rd = Rs1 >> imm (0 ext) |
0000000 | 101 | 0010011 | SRLI |
Rd = Rs1 >> imm (sign ext) |
0100000 | 101 | 0010011 | SRAI |
Переходы. Все инструкции условных переходов используют формат команд B-типа (переход в пределах 4КБ).
| funct3 | opcode | B-type | |
|---|---|---|---|
If (Rs1 == Rs2)
PC=PC+imm |
000 | 1100011 | BEQ |
If (Rs1 != Rs2)
PC=PC+imm |
001 | 1100011 | BNE |
If (Rs1 <Rs2)
PC=PC+imm |
100 | 1100011 | BLT |
If (Rs1 >Rs2)
PC=PC+imm |
101 | 1100011 | BGE |
If (Rs1 <Rs2) -usign
PC=PC+imm |
110 | 1100011 | BLTU |
If (Rs1 >Rs2) -usign
PC=PC+imm |
111 | 1100011 | BGEU |
Безусловный переход относительно значения регистра с записью в регистр связи
| funct3 | opcode | I-type | |
|---|---|---|---|
Rd = PC+4 PC= Rs + imm |
000 | 1100111 | JALR |
Команда простого безусловного перехода с сохранением адреса возврата в регистр (переход без сохранения адреса возврата осуществляется, если в качестве регистра указан x0)
| Imm[20|10:1|11|19:12] | rd | opcode | J-type | |
|---|---|---|---|---|
Rd = PC + 4 PC = PC + imm |
imm[20|10:1|11|19:12] | rd | 1101111 | JAL |
Инструкции записи в память - S-тип.
| imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | S-type | |
|---|---|---|---|---|---|---|---|
[Rs1+imm]=Rs2(byte) |
imm[11:5] | rs2 | rs1 | 000 | imm[4:0] | 0100011 | SB |
[Rs1+imm]=Rs2(half) |
imm[11:5] | rs2 | rs1 | 001 | imm[4:0] | 0100011 | SH |
[Rs1+imm]=Rs2(word) |
imm[11:5] | rs2 | rs1 | 010 | imm[4:0] | 0100011 | SW |
Чтение из памяти (I-тип, как бы неожиданно это не выглядело)
| funct3 | opcode | I-type | |
|---|---|---|---|
Rd = @(Rs + imm) 8bit |
000 | 0000011 | LB |
Rd = @(Rs + imm) 16bit |
001 | 0000011 | LH |
Rd = @(Rs + imm) 32bit |
010 | 0000011 | LW |
Rd = @(Rs + imm) 8bit zero ext |
100 | 0000011 | LBU |
Rd = @(Rs + imm) 16bit zero ext |
101 | 0000011 | LHU |
| funct3 | Опкод | I-type | |
|---|---|---|---|
Rd= CSR[imm] CSR = Rs1 |
001 | 1110011 | CSRRW |
Rd= CSR[imm] CSR[imm] = CSR[imm] or Rs1 |
010 | 1110011 | CSRRS |
Rd= CSR[imm] CSR[imm] = CSR[imm] and (~Rs1) |
011 | 1110011 | CSRRC |
Rd= CSR[imm] CSR[imm] = zimm |
101 | 1110011 | CSRRWI |
Rd= CSR[imm] CSR[imm] = CSR[imm] or zimm |
110 | 1110011 | CSRRSI |
Rd= CSR[imm] CSR[imm] = CSR[imm] and (~zimm) |
111 | 1110011 | CSRRCI |
Анализируем далее систему команд:
Программный счётчик в нашей микро архитектуре уже вынесен в отдельный блок с функциями счетчика с опцией параллельной загрузки - при линейном выполнении программного кода PC автоматически инкрементируется (+4 к текущему значению), в случае переходов - инициируется новым значением.
Поскольку в командах перехода со связыванием (jal, jarl) в регистр связи записывается значение инкрементированное значение программного счётчика (PC+4), в его модуле организуем параллельно к выходу значения PC и вывод PC+4. Такое решение, возможно, не слишком каноническое, но оно позволяет уже иметь значение PC+4 "под рукой".
module rv_pc
#(
parameter WIDTH=32
)
(
input clk,
input rst_n,
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(pc_load==1'b1)
pc_reg <= pc_next;
else
pc_reg <= pc_reg + 3'h4;
end
///*
assign pc = pc_reg;
always @ *
begin
pc_plus <= pc_reg + 3'h4;
end
// */
endmodule
Для поддержки операций условных переходов выделим отдельный блок сравнения принимающий на вход значения регистров, код операции сравнения (funct3) и выдающий сигнал, разрешающий или не разрешающий переход. Модуль представляет собой относительно небольшой модуль на case-ах.
В этот же модуль вынесем формирование сигнала разрешения параллельной загрузки в регистр программного счётчика нового значения - при командах переходов и возвратов из обработчиков прерываний (mret, например, или также можно добавить в этот список команды ecall, ebreak, хотя для ядер микроконтроллерного класса их поддержка не сильно актуальна).
module rv_cmp
( input [6:0] opcode,
input [2:0] funct3,
input [31:0] Rs1,
input [31:0] Rs2,
output reg pc_new
);
always @ *
begin
case (opcode)
7'b11011_11 : begin
$display("JAL");
pc_new <= 1'b1;
end
7'b11001_11 : begin
$display("JALR");
pc_new <= 1'b1;
end
7'b11100_11 : begin
$display("MRET/ecall/ect...");
pc_new <= 1'b1;
end
7'b11000_11 : begin // Relative (rs1) jump and link in register
$display("BRANCH");
case (funct3)
3'h0: begin
pc_new <= Rs1 == Rs2;
end
3'h1: begin
pc_new <= Rs1 != Rs2;
end
3'h4: begin
pc_new <= Rs1 < Rs2;
end
3'h5: begin
pc_new <= Rs1 >= Rs2;
end
3'h6: begin
pc_new <= {1'b0,Rs1} < {1'b0,Rs2};
end
3'h7: begin
pc_new <= {1'b0,Rs1} >= {1'b0,Rs2};
end
default: begin
pc_new <= 1'b0;
end
endcase
end
default: begin
$display ("default");
pc_new <= 1'b0;
end
endcase
end
endmodule
Чтобы не усложнять структуру и поведенческое описание АЛУ (не делать его очень многовходовым - как минимум Rs1, Rs2, PC, imm, вынесем функционал выбора операндов на откуп специальному мультиплексору. АЛУ таким образом, имеет два входных порта-операнда, выход результата и вход управления выбором функции (зависит от полей opcode, funct3, funct7), что соответствует тому факту, что все операции в базовом наборе RV32I выполняются исключительно над двумя операндами - трех и более операндных действий не требуется.
Код ALU:
module rv_alu_v
#(
parameter WIDTH=32
)
(
input [6:0] opcode,
input [2:0] funct3,
input [6:0] funct7,
input [31:0] Op1,
input [31:0] Op2,
output reg [31:0] Rez
);
always @ *
begin
case (opcode)
//I-type
7'b00000_11 : begin // load data from mem
$display("LOAD");
Rez <= Op1 + Op2;
end
7'b00011_11 : begin // fence
$display("fence");
Rez <= Op1;
end
7'b00100_11 : begin // reg with immediate operations
$display("OP-IMM");
`include "alu_op_base.v"
end
7'b01100_11 : begin // reg with reg operations
$display("OP-REG");
`include "alu_op_base.v"
end
7'b11100_11 : begin // mret - system return
$display("RET_SYST or CSR");
`include "alu_op_CSR.v"
end
7'b11001_11 : begin // Relative (rs1) jump and link in register
$display("JALR");
Rez <= Op1 + Op2;
end
//J-type
7'b11011_11 : begin // pc relative jump and link in register
$display("JAL");
Rez <= Op1 + Op2;
end
//S-type
7'b01000_11 : begin // store register value in memory
$display("Store");
Rez <= Op1 + Op2;
end
//U-type
7'b01101_11 : begin // load upper immediate
$display("LUI");
Rez <= Op1;
end
7'b00101_11 : begin // add upper immediate to PC
$display("AUIPC");
Rez <= Op1 + Op2;
end
//B-type
7'b11000_11 : begin // conditional PC relative branch - PC+imm
$display("BRANCH");
Rez <= Op1 + Op2;
end
default: begin
$display ("default");
Rez <= Op1;
end
endcase
end
endmodule
Как видно, арифметико-логических операций в базовом варианте не много. Адресные операции (и для подготовки адресов переходов и для адресов обращения к памяти) сводятся к сложению.
Описания арифметических операций с регистрами (включая и операции с регистрами специального назначения) вынесены в отдельные файлы - "alu_op_base.v" и "alu_op_CSR.v". Это несколько сократило само текстовое описание АЛУ и проще их модифицировать при необходимости.
"alu_op_base.v" - набор арифметико-логических операций базового набора команд.
case (funct3)
3'h0: begin
if (funct7 == 7'h20) begin Rez <= Op1 + Op2; end
else begin Rez <= Op1 - Op2; end
end
3'h1: begin Rez <= Op1 << Op2; end
3'h2: begin Rez <= (Op1 < Op2) ? 32'h1 : 32'h0; end
3'h3: begin Rez <= ({1'b0,Op1} < {1'b0,Op2}) ? 32'h1 : 32'h0; end
3'h4: begin Rez <= Op1 << Op2; end
3'h5: begin
if (funct7 == 7'h20) begin Rez <= Op1 >>> Op2; end
else begin Rez <= Op1 >> Op2; end
end
3'h6: begin Rez <= Op1 | Op2; end
3'h7: begin Rez <= Op1 Op2; end
default: begin Rez <= Op2; end
endcase
Как упоминалось ранее, расширить набор математических операций в рамках набора I можно за счет поля funct7. В представленной версии кода достаточно, например, добавить выборы типа "case (funct7)" в вариантах выбора по полю funct3.
Набор операций практически типовой. Также можно видеть возможность расширения набора команд в рамках регистровых за счет дополнительного кодирования операций в поле ' funct7'.
Аналогичная ситуация наблюдается и в операциях с регистрами специального назначения.
case (funct3)
3'h0: begin Rez <= Op1; end
3'h1: begin Rez <= Op1; end
3'h2: begin Rez <= Op1 | Op2; end
3'h3: begin Rez <= Op1 (~Op2); end
3'h4: begin Rez <= Op1; end
3'h5: begin Rez <= Op1; end
3'h6: begin Rez <= Op1 | Op2; end
3'h7: begin Rez <= Op1 (~Op2); end
default: begin Rez <= Op1; end
endcase
Ещё один модуль который будет необходим - мультиплексор данных для записи в файл-регистр по адресу rd - в него будут записаны данные или с выхода АЛУ, или данные из оперативной памяти по вычисленному адресу.
module rv_rez_mux
#(
parameter WIDTH=32
)
(
input [6:0] opcode,
input [2:0] funct3,
input [6:0] funct7,
input [WIDTH-1:0] Rez,
input [WIDTH-1:0] Pc_plus,
input [WIDTH-1:0] Mem_data,
output reg [WIDTH-1:0] Rd
);
always @ *
begin
case (opcode)
//I-type
7'b00000_11 : begin // load data from mem
$display("LOAD");
Rd <= Rez;
end
7'b00011_11 : begin // fence
$display("fence");
Rd <= Rez;
end
7'b00100_11 : begin // reg with immediate operations
$display("OP-IMM");
Rd <= Rez;
end
7'b00100_11 : begin // reg with reg operations
$display("OP-REG");
Rd <= Rez;
end
7'b11100_11 : begin // mret - system return
$display("RET_SYST or CSR");
Rd <= Rez;
end
7'b11001_11 : begin // Relative (rs1) jump and link in register
$display("JALR");
case (funct3)
3'h0: begin Rd <= Pc_plus; end
default: begin Rd <= Pc_plus; end
endcase
end
//J-type
7'b11011_11 : begin // pc relative jump and link in register
$display("JAL");
Rd <= Pc_plus;
end
//S-type
7'b01000_11 : begin // store register value in memory
$display("Store");
Rd <= Rez;
end
//U-type
7'b01101_11 : begin // load upper immediate
$display("LUI");
Rd <= Rez;
end
7'b00101_11 : begin // add upper immediate to PC
$display("AUIPC");
case (funct3)
3'h0: begin Rd <= Rez; end
default: begin Rd <= Rez; end
endcase
end
//B-type
7'b11000_11 : begin // conditional PC relative branch - PC+imm
$display("BRANCH");
case (funct3)
3'h0: begin Rd <= Rez; end
3'h1: begin Rd <= Rez; end
3'h4: begin Rd <= Rez; end
3'h5: begin Rd <= Rez; end
3'h6: begin Rd <= Rez; end
3'h7: begin Rd <= Rez; end
default: begin Rd <= Rez; end
endcase
end
default: begin
$display ("default");
Rd <= Rez;
end
endcase
end
endmodule
И финальный блок формально не входящий в архитектуру процессорного ядра - оперативная память и память программ. С учетом специфики реализации блоков памяти в большинстве семейств FPGA память для процессорного ядра логичнее реализовать в виде (или на основе) двухпортовой памяти - один порт будет использован для чтения данных из памяти - функция памяти программ, второй порт - для чтения/записи данных - функция оперативной памяти.
Вариант кода описания модуля:
module rv_mem
#(
parameter DATA_WIDTH=32,
parameter ADDR_WIDTH=10
)
( input clk,
input [(ADDR_WIDTH-1):0] i_addr,
output reg [(DATA_WIDTH-1):0] code_out,
input [(ADDR_WIDTH-1):0] d_addr,
output reg [(DATA_WIDTH-1):0] d_out,
input [(DATA_WIDTH-1):0] d_in,
input we
);
// ROM array
reg [DATA_WIDTH-1:0] rom [0:2**ADDR_WIDTH-1] ;
// read ROM content from file
initial
$readmemh("rom.txt",rom);
always @ (posedge clk)
begin
if (we) rom[d_addr] <= d_in;
end
always @ (*)
begin
code_out <= rom[i_addr];
end
endmodule
В некоторых случаях удобнее будет реализовать две отдельные памяти - память для кода, память для данных - как некие сильно упрощенные варианты кэша инструкций и кэша данных.
Общая структура процессора (вместе с блоками памятей - и программ и данных) представлена на рис.5.2. Блоки выстроены в условно-линейном порядке прохождения сигнала, сквозные сигналы также проброшены вдоль блоков. Это один из портов блока памяти rv_mem адресуется программным счетчиком (выход РС), соответствующее выходное слово инструкции - iw поступает параллельно на входы модулей rv_desh и rv_imm. Данные модули "разбирают" поступившую инструкцию выделяя из нее адреса регистров-операндов файл-регистра, формируют непосредственное числовое значение (immediate), сопровождая декодированные элементами сигналами стробов.
Значения выбранных регистров регистрового файла, числовое значение (immediate), программный счетчик, значения выбранных регистров специального назначения (опционально) поступают на вход мультиплексора операндов rv_ops_mux, параллельно выхода файл-регистра подаются на вход компаратора (вернее его называть блоков разрешения перехода) совместно с комбинацией опкода с модификаторами (opcode, funct3, funct7).
Парные выходы мультиплексора также вместе с кодами операции поступают на вход АЛУ (модуль rv_alu_v). АЛУ применяется и для выполнения операций над операндами, и для расчета адресов - или адресов переходов, или адресов для обращения к памяти данных (ОЗУ). В зависимости от операции результат АЛУ, инкрементированный программный счетчик (PC+4) или данные из оперативной памяти записывается в регистр с номером rd файл-регистра.
Блок регистров специального назначения добавлен, как опциональный (выделен зелёным цветом).
Выходной сигнал блока сравнения/управления переносом и новое вычисленное значение подаются на соответствующие входы программного счетчика rv_pc.
Возможные недостатки предлагаемого решения:
(рис 5.2) Структурная схема процессора
Код-заглушка для блока CSR-регистров:
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
);
// 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) 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
И, собственно, "код объединяющий всё" (нотации и сигналы сохранены по возможности близко к рис.5.2):
`include "rv_pc.v" `include "rv_mem.v" `include "rv_desh.v" `include "rv_imm.v" `include "rv_reg_file.v" `include "rv_ops_mux.v" `include "rv_cmp.v" `include "rv_alu_v.v" `include "rv_rez_mux.v" `include "rv_csr.v" module rv_cpu_top #( parameter DATA_WIDTH=32, parameter ADDR_WIDTH=32 ) ( input clk, input rst, // input [(ADDR_WIDTH-1):0] Data_In, output[(DATA_WIDTH-1):0] Data_out ); // wires wire [(DATA_WIDTH-1):0] PC; wire [(DATA_WIDTH-1):0] PCplus; wire [(DATA_WIDTH-1):0] iw; wire [(DATA_WIDTH-1):0] imm; wire [(DATA_WIDTH-1):0] Rs1_reg; wire [(DATA_WIDTH-1):0] Rs2_reg; wire [(DATA_WIDTH-1):0] Rd_reg; wire [(DATA_WIDTH-1):0] Op1; wire [(DATA_WIDTH-1):0] Op2; wire [(DATA_WIDTH-1):0] Rez; wire [(DATA_WIDTH-1):0] CSR; wire [(DATA_WIDTH-1):0] Mem_data; wire [6:0] op; wire [2:0] fn3; wire [6:0] fn7; wire [4:0] rs1; wire [4:0] rs2; wire [4:0] rd; wire rd_wr; wire mem_wr; wire csr_wr; wire PC_load; //assign Data_In = CSR; assign Data_out = CSR; // CPU modules rv_pc pc( .clk(clk), .rst_n(rst), .pc_load(PC_load), .pc_next(REZ), .pc(PC), .pc_plus(PCplus) ); rv_mem mem( .clk(clk), .i_addr(PC), .code_out(iw), .d_addr(REZ), .d_out(Mem_data), .d_in(Rs2_reg), .we(mem_wr) ); rv_desh desh( .inst(iw), .opcode(op), .rs1(rs1), .rs1_en(), .rs2(rs2), .rs2_en(), .rd(rd), .rd_wr(rd_wr), .funct3(fn3), .f3_en(), .funct7(fn7), .f7_en(), .mem_en(), .mem_wr(mem_wr), .csr_en(), .csr_wr(crsr_wr), .pc_load() ); rv_imm immed( .inst(iw), .imm(imm) ); rv_reg_file reg_file( .clk(clk), .rs1(rs1), .rs2(rs2), .rd(rd), .Rs1_out(Rs1_reg), .Rs2_out(Rs2_reg), .Rd_input(Rd_reg), .we(rd_wr) ); rv_cmp cmp( .opcode(op), .funct3(fn3), .Rs1(Rs1_reg), .Rs2(Rs2_reg), .pc_new(PC_load) ); wire [31:0] Op1; wire [31:0] Op2; rv_ops_mux ops_mux( .opcode(r3l3_op), .funct3(r3l3_fn3), .funct7(r3l3_fn7), .Rs1(Rs1_reg), .Rs2(Rs2_reg), .imm(r3l3_imm), .PC(r3l3_PC), .CSR(CSR_mux), .Op1(Op1), .Op2(Op2) ); rv_alu_v alu_v( .opcode(op), .funct3(fn3), .funct7(fn7), .Op1(Op1), .Op2(Op2), .Rez(Rez) ); rv_rez_mux rez_mux( .opcode(op), .funct3(fn3), .funct7(fn7), .Rez(REZ), .Pc_plus(PCplus), .Mem_data(Mem_data), .Rd(Rd_reg) ); rv_csr csr( .csr_addr(imm[11:0]), .csr_in(REZ), .csr_out(CSR), .csr_wr(csr_wr) ); endmodule
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.