RISC-V для FPGA - архитектура, микроархитектурные реализации

Многопоточное процессорное ядро

Разбить на страницы
Показывать лекцию целиком

На работу над многопоточным вариантом вдохновили процессоры семейства Xcore компании XMOS, специализирующейся на процессорах/микроконтроллерах для встраиваемых систем. Решения XMOS традиционно включают в себя элементы, которые традиционно требовали бы использования компонентов другого класса [1]. Там, где традиционно можно использовать микроконтроллер для управления конструкцией, DSP для обработки сигналов и, возможно, CPLD для подключения к сложному цифровому интерфейсу, процессоры XMOS могут выполнять эти три задачи в одном устройстве, используя единый программный процесс на основе программного обеспечения. Их характерные черты - аппаратная многопоточность, возможность масштабирования количества ядер/процессоров в системе, гибкие программно-конфигурируемые порты ввода-вывода. В ранних версиях своих процессоров XMOS применяли свои RISC ядра, были серии с комбинацией RISC-ядер и ARM-ядра. В 2023м году компания анонсировала вариант процессора с ядрами RISC-V (рис.8.1).

Вторым побуждающим мотивом экспериментов с является желание в решениях софт-ядер для FPGA уйти от необходимости введения в состав софт-процессора контроллера прерываний (кто любит обрабатывать прерывания, да еще и вложенные? Да - мало кто).

(рис 8.1) Двухядерный процессор XMOS с восьмипоточными RISC-V ядрами

Попробуем несколько модифицировать многотактный процессор, добавив ему поддержку многопоточности в виде "теневых" копий основных архитектурных регистров. В терминологии RISC-V аппаратно-поддерживаемый поток называется хартом (hart) [2-4]. Общая структура ядра пока останется почти прежней - рис.8.2.

(рис 8.2) Базовая структура ядра

Микроархитектурные блоки:

rv_pc - программный счетчик;

rv_mem - блок памяти (программная и оперативная;

rv_desh - дешифратор команд (слова-инструкции);

rv_imm - формирователь непосредственного значения из слова-инструкции;

rv_reg_file - файл-регистр

rv_ops_mux - коммутатор операндов для АЛУ;

rv_cmp - формирователь сигнала разрешения перехода;

rv_alu_v - АЛУ;

rv_rez_mux - коммутатор результатов;

rv_csr - блок регистров специального назначения.

Ключевая идея - для каждого из хартов хранение состояния потока(нити) в массиве теневых регистров. При этом каждая из копий регистров хранит архитектурное состояние нити. Общими ресурсами остаются логические блоки, память. Также имеет смысл сохранить общим для всех хартов блок регистров специальных функций (как правило, это системные таймеры, счетчики производительности и тп).

Блок 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_in,
  input [(DATA_WIDTH-1):0] csr_in,
  input [(ADDR_WIDTH-1):0] csr_addr_out,
  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_in)
    32'h0 : begin
      if (csr_wren) begin
        csr_reg[0] <= csr_in;
      end 
    end
    default: begin
      csr_reg[1] <= 32'h555;
    end
  endcase
  case (csr_addr_out)
    32'h0 : begin
      csr_out <= csr_reg[0];
    end
    default: begin
      csr_out<=32'hAAA;
    end
  endcase
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 [2:0] hart_in, // hard to read out data from
  input [2:0] hart_out, // hard to write data to
  input we,
  input en,
  output reg [(DATA_WIDTH-1):0] x1_out,
  output reg [(DATA_WIDTH-1):0] x2_out,
  output reg [(DATA_WIDTH-1):0] x3_out,
  output reg [(DATA_WIDTH-1):0] x4_out,
  output reg [(DATA_WIDTH-1):0] x5_out
);
// RAM array
  reg [DATA_WIDTH-1:0] ram[0:2**ADDR_WIDTH-1][0:7];
  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][hart_in] <= Rd_input;
  end

always @ (*)
  begin
    Rs1_out <= rs1_nonzero ? ram[rs1][hart_out] : 32'h0;
    Rs2_out <= rs2_nonzero ? ram[rs2][hart_out] : 32'h0;
    x1_out <= ram[1][hart_out];
    x2_out <= ram[2][hart_out];
    x3_out <= ram[3][hart_out];
    x4_out <= ram[4][hart_out];
    x5_out <= ram[5][hart_out];
  end
// */
Endmodule

Программный счетчик также будет массивом регистров.

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,
  input [2:0] hart_in,
  input [2:0] hart_out
);

reg [WIDTH-1:0] pc_reg[0:7];
// read initial PCs data values
initial
$readmemh("pc.txt",pc_reg);

always@(posedge clk or negedge rst_n)
  begin
    if (en) begin
      if(pc_load==1'b1)
        pc_reg[hart_in] <= pc_next;
      else
        pc_reg[hart_in] <= pc_reg[hart_in] + 3'h4;
      end
    end
  //end///*
assign pc = pc_reg[hart_out];

always @ *
  begin
    pc_plus <= pc_reg[hart_out] + 3'h4;
  end
// */    
endmodule

В файле "pc.txt" - начальные значения регистров.

Как и ожидалось, "ленивый" вариант преобразования многотактного процессора в многопоточный простой заменой файл-регистра и счетчика не работоспособен, требуются некоторый механизм управления.

Для управления выборкой данных хартов введём группу регистров - hart-reg, объединенных в линейку. Выходные сигналы - номер харта и бит активности харта. При сбросе регистры инициируются нулями и биты активности хартов сброшены.

На линейку в процессе функционирования должна подаваться последовательность хартов с кодами активности. Сама последовательность может, например, хранится в отдельной небольшой памяти - назовем ее hart_table. Hart_table адресуется циклическим счетчиком.

Для текущего примера создадим простенький "системный таймер" разрядностью 64-бита (на самом деле можно любой приемлемой разрядности, в данном случае 64 бита выбрано по аналогии с традиционными таймерами-счетчиками в реализациях RISC-V архитектуры). Младшие разряды счетчика будут адресовать память таблицы хартов. Сам счетчик потом можно будет использовать в качестве счетчика тактов работы с момента старта системы, например.

Биты активности харта также должны будут использованы для управления записью в такие модули процессорного ядра, как программный счетчик, файл-регистр, запись в память (если харт не активен - запись не будет произведена). Также инверсный сигнал активности харта подается на вход "сброса конвейера" АЛУ (да, для данной реализации будет взята версия АЛУ конвейеризированного процессорного ядра).

Уровни "разделяются" промежуточными регистрами (обычные регистры хранения), которые фиксируют данные (регистры R0 - R6). Ряд сигналов просто "пробрасываются" по этапам (напрямую между регистрами) в том случае, если они не задействованы в них, но требуются на последующих - всё как и было в простом многотактном варианте.

Общая структура многопоточного процессора представлена на рис.8.3.

(рис 8.3) Структура многопоточного процессора

Описание памяти таблицы хартов - в текущем варианте - память на 8 записей, выходная разрядность 4 бита, старший бит - бит активности харта, младшие три - номер харта:

module rv_hart_table
#(
  parameter DATA_WIDTH=4, 
  parameter ADDR_WIDTH=3
  )
( input clk,
  input [(ADDR_WIDTH-1):0] h_addr,
  output reg [(DATA_WIDTH-1):0] h_out,
  input we,
  input en
);
// ROM array
reg [DATA_WIDTH-1:0] rom [0:2**ADDR_WIDTH-1] ;
// read ROM content from file
initial
$readmemh("hart_table.txt",rom);
always @ (*)
  begin
    h_out <= rom[h_addr];
  end
endmodule

Системный таймер:

module rv_timer
#(
  parameter DATA_WIDTH=64
  )
( input clk,
  input rst,
  output [(DATA_WIDTH-1):0] timer
);
reg [(DATA_WIDTH-1):0] cnt;
always@(posedge clk or negedge rst)
  begin
    if(!rst)
      cnt <= 64'h0;
    else
      cnt <= cnt + 1'b1;
  end
assign timer = cnt;
endmodule

Соединение таблицы хартов со счетчиком - рис.8.4.

(рис 8.4) Соединение таблицы хартов со счетчиком
wire [63:0] tmr_out;
rv_timer tmr(
  .clk(clk),
  .rst(rst),
  .timer(tmr_out)
);
//==========================================================
rv_hart_table hart_table(
  .clk(clk),
  .h_addr(tmr_out[2:0]),
  .h_out(hart0_out)
);
//==========================================================
wire [3:0] hart0_out;

Соединение линейки регистров для хранения хартов:

wire [3:0] hart1_out;
rv_hart_reg H1(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart0_out),
  .h_out(hart1_out)
);
wire [3:0] hart2_out;
rv_hart_reg H2(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart1_out),
  .h_out(hart2_out)
);
wire [3:0] hart3_out;
rv_hart_reg H3(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart2_out),
  .h_out(hart3_out)
);
wire [3:0] hart4_out;
rv_hart_reg H4(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart3_out),
  .h_out(hart4_out)
);
wire [3:0] hart5_out;
rv_hart_reg H5(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart4_out),
  .h_out(hart5_out)
);
wire [3:0] hart6_out;
rv_hart_reg H6(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart5_out),
  .h_out(hart6_out)
);

Общий код процессора:

`include "rv_pc.v"
`include "rv_mem.v"
`include "rv_desh_pipe.v"
`include "rv_imm.v"
`include "rv_reg_file_tst.v"
`include "rv_ops_mux.v"
`include "rv_cmp.v"
`include "rv_alu_v.v"
`include "rv_rez_mux.v"
`include "rv_csr.v"
`include "rv_r_reg.v"
`include "rv_hart_reg.v"
`include "rv_timer.v"
`include "rv_hart_table.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
);

// CPU modules 
//===============================================================
wire [63:0] tmr_out;
rv_timer tmr(
  .clk(clk),
  .rst(rst),
  .timer(tmr_out)
);
//===============================================================
rv_hart_table hart_table(
  .clk(clk),
  .h_addr(tmr_out[2:0]),
  .h_out(hart0_out)
);
// ==============================================================
wire [3:0] hart0_out;
wire [3:0] hart1_out;
rv_hart_reg H1(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart0_out),
  .h_out(hart1_out)
);
wire [3:0] hart2_out;
rv_hart_reg H2(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart1_out),
  .h_out(hart2_out)
);
wire [3:0] hart3_out;
rv_hart_reg H3(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart2_out),
  .h_out(hart3_out)
);
wire [3:0] hart4_out;
rv_hart_reg H4(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart3_out),
  .h_out(hart4_out)
);
wire [3:0] hart5_out;
rv_hart_reg H5(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart4_out),
  .h_out(hart5_out)
);
wire [3:0] hart6_out;
rv_hart_reg H6(
  .clk(clk),
  .rst(rst),
  .en(1'b1),
  .h_in(hart5_out),
  .h_out(hart6_out)
);
// ==============================================================
// L0_R1
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(hart6_out[3]),
  .pc_load(r6l6_PC_load),
  .pc_next(r6l6_Rez),
  .pc(l0r1_PC),
  .pc_plus(l0r1_PCplus),
  .hart_in(hart6_out[2:0]),
  .hart_out(hart0_out[2:0])
);
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(1'b1),
  .r_in(r1_in),
  .r_out(r1_out)
);
// R1_L1
wire [31:0] l1r2_iw;
wire [31:0] l1r2_PC;
wire [31:0] l1r2_PCplus;

rv_mem mem( // system memory
  .clk(clk),
  .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(hart5_out[3])
);
// 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(1'b1),
  .r_in(r2_in),
  .r_out(r2_out)
);
// L2_R3
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(),
  .pipe_rst(~hart2_out[3])
);
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(1'b1),
  .r_in(r3_in),
  .r_out(r3_out)
);
//L3_R4
wire [31:0] l3r4_Rs1_reg;
wire [31:0] l3r4_Rs2_reg;
wire [31:0] l3r4_CSR;

rv_reg_file reg_file(   // register's file
  .clk(~clk),
  .rs1(r3l3_rs1),
  .rs2(r3l3_rs2),
  .rd(r6l6_rd),
  .Rs1_out(l3r4_Rs1_reg),
  .Rs2_out(l3r4_Rs2_reg),
  .Rd_input(Rd_reg),
  .we(r6l6_rd_wr),
  .en(hart6_out[3]),
  .hart_in(hart6_out[2:0]),
  .hart_out(hart3_out[2:0])
);
rv_csr csr(
  .clk(~clk),
  .csr_addr_out(r3l3_imm[11:0]),
  .csr_addr_in(r6l6_imm[11:0]),
  .csr_in(r6l6_Rez),
  .csr_out(l3r4_CSR),
  .csr_wr(r6l6_csr_wr),
  .en(hart6_out[3])
);
assign Data_out = l3r4_CSR; //CSR;
wire [31:0] r4l4_Rs1_reg;
wire [31:0] r4l4_Rs2_reg;
wire [6:0] r4l4_op;
wire [2:0] r4l4_fn3;
wire [6:0] r4l4_fn7;
wire [4:0] r4l4_rd;
wire r4l4_rd_wr;
wire r4l4_mem_wr;
wire r4l4_csr_wr;
wire [31:0] r4l4_imm;
wire [31:0] r4l4_PC;
wire [31:0] r4l4_PCplus;
wire [31:0] r4l4_CSR;

wire [255:0] r4_in;
wire [255:0] r4_out;
assign r4_in = {l3r4_Rs1_reg, l3r4_Rs2_reg, r3l3_op, r3l3_fn3, r3l3_fn7, r3l3_rd, r3l3_rd_wr,
    r3l3_mem_wr, r3l3_csr_wr, r3l3_imm,  r3l3_PC, r3l3_PCplus, l3r4_CSR, 39'h0};
wire [38:0] r4_null;
assign {r4l4_Rs1_reg, r4l4_Rs2_reg, r4l4_op, r4l4_fn3, r4l4_fn7, r4l4_rd, r4l4_rd_wr,
    r4l4_mem_wr, r4l4_csr_wr, r4l4_imm,  r4l4_PC, r4l4_PCplus, r4l4_CSR, r4_null} = r4_out;

rv_r_reg R4(
  .clk(clk),
  .rst_n(rst),
  .en(1'b1),
  .r_in(r4_in),
  .r_out(r4_out)
);
// L4

wire [31:0] Op1;
wire [31:0] Op2;

rv_ops_mux ops_mux(
  .opcode(r4l4_op),
  .funct3(r4l4_fn3),
  .funct7(r4l4_fn7),
  .Rs1(r4l4_Rs1_reg),
  .Rs2(r4l4_Rs2_reg),
  .imm(r4l4_imm),
  .PC(r4l4_PC),
  .CSR(r4l4_CSR),
  .Op1(Op1),
  .Op2(Op2)
);

rv_cmp cmp(
  .opcode(r4l4_op),
  .funct3(r4l4_fn3),
  .Rs1(r4l4_Rs1_reg),
  .Rs2(r4l4_Rs2_reg),
  .pc_new(l4r5_PC_load)
);

rv_alu_v alu_v(
  .opcode(r4l4_op),
  .funct3(r4l4_fn3),
  .funct7(r4l4_fn7),
  .Op1(Op1),
  .Op2(Op2),
  .Rez(l4r5_Rez)
);
wire [31:0] l4r5_Rez;
wire l4r5_PC_load;
wire [31:0] r5l5_Rez;
wire [31:0] r5l5_Rs2_reg;
wire [6:0] r5l5_op;
wire [2:0] r5l5_fn3;
wire [6:0] r5l5_fn7;
wire [4:0] r5l5_rd;
wire r5l5_rd_wr;
wire r5l5_mem_wr; 
wire r5l5_csr_wr;
wire [31:0] r5l5_imm;
wire [31:0] r5l5_PC;
wire [31:0] r5l5_PCplus;
wire r5l5_PC_load;
//L5
wire [255:0] r5_in;
wire [255:0] r5_out;
assign r5_in = {l4r5_Rez, r4l4_Rs2_reg, r4l4_op, r4l4_fn3, r4l4_fn7, r4l4_rd, r4l4_rd_wr,
    r4l4_mem_wr, r4l4_csr_wr, r4l4_imm,  r4l4_PC, r4l4_PCplus, l4r5_PC_load, 70'h0};
wire [69:0] r5_null;
assign {r5l5_Rez, r5l5_Rs2_reg, r5l5_op, r5l5_fn3, r5l5_fn7, r5l5_rd, r5l5_rd_wr,
    r5l5_mem_wr, r5l5_csr_wr, r5l5_imm,  r5l5_PC, r5l5_PCplus, r5l5_PC_load, r5_null} = r5_out;

rv_r_reg R5(
  .clk(clk),
  .rst_n(rst),
  .en(1'b1),
  .r_in(r5_in),
  .r_out(r5_out)
);
// L6
wire [31:0] l5r6_Mem_data;
wire [31:0] r6l6_Mem_data;
wire [31:0] r6l6_Rez;
wire [31:0] r6l6_Rs2_reg;
wire [6:0] r6l6_op;
wire [2:0] r6l6_fn3;
wire [6:0] r6l6_fn7;
wire [4:0] r6l6_rd;
wire r6l6_rd_wr;
wire r6l6_mem_wr;
wire r6l6_csr_wr; 
wire [31:0] r6l6_imm;
wire [31:0] r6l6_PC;
wire [31:0] r6l6_PCplus;
wire r6l6_PC_load;

wire [255:0] r6_in;
wire [255:0] r6_out;
assign r6_in = {l5r6_Mem_data, r5l5_Rez,  r5l5_op, r5l5_fn3, r5l5_fn7, r5l5_rd, r5l5_rd_wr,
    r5l5_mem_wr, r5l5_csr_wr, r5l5_imm,  r5l5_PC, r5l5_PCplus, r5l5_PC_load, 70'h0};
wire [69:0] r6_null;
assign {r6l6_Mem_data, r6l6_Rez, r6l6_op, r6l6_fn3, r6l6_fn7, r6l6_rd, r6l6_rd_wr,
    r6l6_mem_wr, r6l6_csr_wr, r6l6_imm,  r6l6_PC, r6l6_PCplus, r6l6_PC_load, r6_null} = r6_out;

rv_r_reg R6(
  .clk(clk),
  .rst_n(rst),
  .en(1'b1),
  .r_in(r6_in),
  .r_out(r6_out)
);
wire [31:0] Rd_reg;
rv_rez_mux rez_mux(
  .opcode(r6l6_op),
  .funct3(r6l6_fn3),
  .funct7(r6l6_fn7),
  .Rez(r6l6_Rez),
  .Pc_plus(r6l6_PCplus),
  .Mem_data(r6l6_Mem_data),
  .Imm(r6l6_imm),
  .Rd(Rd_reg)
);
Endmodule

Традиционно небольшая, тестовая программа:

lui x1,0x1
srli x1,x1,12
l1:
addi x1, x1, 1
jal x2,l1
lui x1,0x99

lui x1,0x2
srli x1,x1,12
l2:
addi x1, x1, 2
jal x2,l2
lui x1,0x99

lui x1,0x3
srli x1,x1,12
l3:
addi x1, x1, 3
jal x2,l3
lui x1,0x99

lui x1,0x4
srli x1,x1,12
l4:
addi x1, x1, 4
jal x2,l4
lui x1,0x99

lui x1,0x5
srli x1,x1,12
l5:
addi x1, x1, 5
jal x2,l5
lui x1,0x99

lui x1,0x6
srli x1,x1,12
l6:
addi x1, x1, 6
jal x2,l6
lui x1,0x99

lui x1,0x7
srli x1,x1,12
l7:
addi x1, x1, 7
jal x2,l7
lui x1,0x99

lui x1,0x8
srli x1,x1,12
l8:
addi x1, x1, 8
jal x2,l8
lui x1,0x99

На рис.8.5 - она же в симуляторе RARS.

(рис 8.5)

В программе заданы несколько коротких бесконечных цикла, работающих с регистрами х1, х2. Регистр х1 инициируется определенным значением, регистр х2 будет использован, как регистр связи. При простом линейном исполнении программа зациклится на метке l1.

Дамп программы:

Address     Code        Basic                        Line Source

0x00400000  0x000010b7  lui x1,1                     1    lui x1,0x1
0x00400004  0x00c0d093  srli x1,x1,12                2    srli x1,x1,12
0x00400008  0x00108093  addi x1,x1,1                 4    addi x1, x1, 1
0x0040000c  0xffdff16f  jal x2,0xfffffffc            5    jal x2,l1
0x00400010  0x000990b7  lui x1,0x00000099            6    lui x1,0x99
0x00400014  0x000020b7  lui x1,2                     8    lui x1,0x2
0x00400018  0x00c0d093  srli x1,x1,12                9    srli x1,x1,12
0x0040001c  0x00208093  addi x1,x1,2                 11   addi x1, x1, 2
0x00400020  0xffdff16f  jal x2,0xfffffffc            12   jal x2,l2
0x00400024  0x000990b7  lui x1,0x00000099            13   lui x1,0x99
0x00400028  0x000030b7  lui x1,3                     15   lui x1,0x3
0x0040002c  0x00c0d093  srli x1,x1,12                16   srli x1,x1,12
0x00400030  0x00308093  addi x1,x1,3                 18   addi x1, x1, 3
0x00400034  0xffdff16f  jal x2,0xfffffffc            19   jal x2,l3
0x00400038  0x000990b7  lui x1,0x00000099            20   lui x1,0x99
0x0040003c  0x000040b7  lui x1,4                     22   lui x1,0x4
0x00400040  0x00c0d093  srli x1,x1,12                23   srli x1,x1,12
0x00400044  0x00408093  addi x1,x1,4                 25   addi x1, x1, 4
0x00400048  0xffdff16f  jal x2,0xfffffffc            26   jal x2,l4
0x0040004c  0x000990b7  lui x1,0x00000099            27   lui x1,0x99
0x00400050  0x000050b7  lui x1,5                     29   lui x1,0x5
0x00400054  0x00c0d093  srli x1,x1,12                30   srli x1,x1,12
0x00400058  0x00508093  addi x1,x1,5                 32   addi x1, x1, 5
0x0040005c  0xffdff16f  jal x2,0xfffffffc            33   jal x2,l5
0x00400060  0x000990b7  lui x1,0x00000099            34   lui x1,0x99
0x00400064  0x000060b7  lui x1,6                     36   lui x1,0x6
0x00400068  0x00c0d093  srli x1,x1,12                37   srli x1,x1,12
0x0040006c  0x00608093  addi x1,x1,6                 39   addi x1, x1, 6
0x00400070  0xffdff16f  jal x2,0xfffffffc            40   jal x2,l6
0x00400074  0x000990b7  lui x1,0x00000099            41   lui x1,0x99
0x00400078  0x000070b7  lui x1,7                     43   lui x1,0x7
0x0040007c  0x00c0d093  srli x1,x1,12                44   srli x1,x1,12
0x00400080  0x00708093  addi x1,x1,7                 46   addi x1, x1, 7
0x00400084  0xffdff16f  jal x2,0xfffffffc            47   jal x2,l7
0x00400088  0x000990b7  lui x1,0x00000099            48   lui x1,0x99
0x0040008c  0x000080b7  lui x1,8                     50   lui x1,0x8
0x00400090  0x00c0d093  srli x1,x1,12                51   srli x1,x1,12
0x00400094  0x00808093  addi x1,x1,8                 53   addi x1, x1, 8
0x00400098  0xffdff16f  jal x2,0xfffffffc            54   jal x2,l8
0x0040009c  0x000990b7  lui x1,0x00000099            55   lui x1,0x99

Для исполнения на многопоточном процессоре массив программных счетчиков инициируется значениями адресов начала каждого из циклов - в данном примере - адреса:

0x0, 0x14, 0x28, 0x3С, 0x50, 0x64, 0x78, 0x8C.

Таблица хартов - перечисление номеров хартов с 0 по 7 в 4-битном представлении. Например, если активен должен быть харт с номером 0, то в таблице он будет представлен цифрой 8 (4`b1000), если с номером 3, то 0xB (4`b1011) и так далее.

(рис 8.5) Тестовая программа в симуляторе RARS

Тест 1:

PC.txt Hart_table.txt

@00

0

14

28

3c

50

64

78

8c

@00

8

1

2

3

4

5

6

7

Рис. 8.6-8.7 - временные диаграммы исполнения кода.

На ней представлены значения:

  • сигналы сброса и тактовый;
  • номера хартов (с битом активности);
  • значение программного счетчика и выбранное из памяти слово инструкции;
  • для АЛУ - опкод команды, операнды и результат;
  • для файл-регистра текущие выходы и вход с разрешением записи;
  • тестовые выходы первых 4 регистров файл-регистра с указанием, к какому харту они относятся.
  • Для регистра Х1 0-го активного харта видна сначала его загрузка значением 0х1000, потом сдвиг его до 0х1.

    (рис 8.6) Временные диаграммы исполнения программы в многопоточном ядре - активен харт0 (рис 8.7) Временные диаграммы исполнения программы в многопоточном ядре - активен харт0

    Тест 3 - распределение адресов тоже самое, активируем 2й харт (номер 1):

    PC.txt Hart_table.txt

    @00

    0

    14

    28

    3c

    50

    64

    78

    8c

    @00

    8

    9

    2

    3

    4

    5

    6

    7

    (рис 8.8) Временные диаграммы исполнения программы в многопоточном ядре - активены харт0 и харт1

    Итоговые микроархитектурные блоки многопоточного варианта процессора:

  • rv_pc - программный счетчик (массив из нескольких регистров);
  • rv_mem - блок памяти (программная и оперативная;
  • rv_desh - дешифратор команд (слова-инструкции) с входом "сброса конвейера";
  • rv_imm - формирователь непосредственного значения из слова-инструкции;
  • rv_reg_file - файл-регистр (массив из нескольких файл-регистров);
  • rv_ops_mux - коммутатор операндов для АЛУ;
  • rv_cmp - формирователь сигнала разрешения перехода;
  • rv_alu_v - АЛУ;
  • rv_rez_mux - коммутатор результатов;
  • rv_csr - блок регистров специального назначения;
  • rv_r_reg - регистры хранения результатов промежуточных этапов;
  • rv_hart_reg - регистры хранения номеров и состояния активности хартов;
  • rv_timer - системный таймер;
  • rv_hart_table - память хранения списка и состояния хартов.
  • Литература

  • Мультиядерные микроконтроллеры семейства XCore от XMOS // http://cyberleninka.ru/article/n/multiyadernye-mikrokontrollery-semeystva-xcore-ot-xmos
  • Using RISC-V to define SoCs in software _ XMOS // http://www.xmos.com/using-risc-v-to-define-socs-in-software/
  • The RISC-V Instruction Set Manual. Volume I: User-Level ISA Document Version 2.2 // Editors: Andrew Waterman1, Krste Asanov
  • http://electronics.stackexchange.com/questions/580645/what-is-a-hardware-thread-in-risc-v
  • http://www.computer.org/csdl/proceedings-article/trustcom/2022/942500b587/1LFM9tik1IQ
  • Страницы:

    На работу над многопоточным вариантом вдохновили процессоры семейства Xcore компании XMOS, специализирующейся на процессорах/микроконтроллерах для встраиваемых систем. Решения XMOS традиционно включают в себя элементы, которые традиционно требовали бы использования компонентов другого класса [1]. Там, где традиционно можно использовать микроконтроллер для управления конструкцией, DSP для обработки сигналов и, возможно, CPLD для подключения к сложному цифровому интерфейсу, процессоры XMOS могут выполнять эти три задачи в одном устройстве, используя единый программный процесс на основе программного обеспечения. Их характерные черты - аппаратная многопоточность, возможность масштабирования количества ядер/процессоров в системе, гибкие программно-конфигурируемые порты ввода-вывода. В ранних версиях своих процессоров XMOS применяли свои RISC ядра, были серии с комбинацией RISC-ядер и ARM-ядра. В 2023м году компания анонсировала вариант процессора с ядрами RISC-V (рис.8.1).

    Вторым побуждающим мотивом экспериментов с является желание в решениях софт-ядер для FPGA уйти от необходимости введения в состав софт-процессора контроллера прерываний (кто любит обрабатывать прерывания, да еще и вложенные? Да - мало кто).

    (рис 8.1) Двухядерный процессор XMOS с восьмипоточными RISC-V ядрами

    Попробуем несколько модифицировать многотактный процессор, добавив ему поддержку многопоточности в виде "теневых" копий основных архитектурных регистров. В терминологии RISC-V аппаратно-поддерживаемый поток называется хартом (hart) [2-4]. Общая структура ядра пока останется почти прежней - рис.8.2.

    (рис 8.2) Базовая структура ядра

    Микроархитектурные блоки:

    rv_pc - программный счетчик;

    rv_mem - блок памяти (программная и оперативная;

    rv_desh - дешифратор команд (слова-инструкции);

    rv_imm - формирователь непосредственного значения из слова-инструкции;

    rv_reg_file - файл-регистр

    rv_ops_mux - коммутатор операндов для АЛУ;

    rv_cmp - формирователь сигнала разрешения перехода;

    rv_alu_v - АЛУ;

    rv_rez_mux - коммутатор результатов;

    rv_csr - блок регистров специального назначения.

    Ключевая идея - для каждого из хартов хранение состояния потока(нити) в массиве теневых регистров. При этом каждая из копий регистров хранит архитектурное состояние нити. Общими ресурсами остаются логические блоки, память. Также имеет смысл сохранить общим для всех хартов блок регистров специальных функций (как правило, это системные таймеры, счетчики производительности и тп).

    Блок 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_in,
      input [(DATA_WIDTH-1):0] csr_in,
      input [(ADDR_WIDTH-1):0] csr_addr_out,
      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_in)
        32'h0 : begin
          if (csr_wren) begin
            csr_reg[0] <= csr_in;
          end 
        end
        default: begin
          csr_reg[1] <= 32'h555;
        end
      endcase
      case (csr_addr_out)
        32'h0 : begin
          csr_out <= csr_reg[0];
        end
        default: begin
          csr_out<=32'hAAA;
        end
      endcase
    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 [2:0] hart_in, // hard to read out data from
      input [2:0] hart_out, // hard to write data to
      input we,
      input en,
      output reg [(DATA_WIDTH-1):0] x1_out,
      output reg [(DATA_WIDTH-1):0] x2_out,
      output reg [(DATA_WIDTH-1):0] x3_out,
      output reg [(DATA_WIDTH-1):0] x4_out,
      output reg [(DATA_WIDTH-1):0] x5_out
    );
    // RAM array
      reg [DATA_WIDTH-1:0] ram[0:2**ADDR_WIDTH-1][0:7];
      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][hart_in] <= Rd_input;
      end
    
    always @ (*)
      begin
        Rs1_out <= rs1_nonzero ? ram[rs1][hart_out] : 32'h0;
        Rs2_out <= rs2_nonzero ? ram[rs2][hart_out] : 32'h0;
        x1_out <= ram[1][hart_out];
        x2_out <= ram[2][hart_out];
        x3_out <= ram[3][hart_out];
        x4_out <= ram[4][hart_out];
        x5_out <= ram[5][hart_out];
      end
    // */
    Endmodule
    

    Программный счетчик также будет массивом регистров.

    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,
      input [2:0] hart_in,
      input [2:0] hart_out
    );
    
    reg [WIDTH-1:0] pc_reg[0:7];
    // read initial PCs data values
    initial
    $readmemh("pc.txt",pc_reg);
    
    always@(posedge clk or negedge rst_n)
      begin
        if (en) begin
          if(pc_load==1'b1)
            pc_reg[hart_in] <= pc_next;
          else
            pc_reg[hart_in] <= pc_reg[hart_in] + 3'h4;
          end
        end
      //end///*
    assign pc = pc_reg[hart_out];
    
    always @ *
      begin
        pc_plus <= pc_reg[hart_out] + 3'h4;
      end
    // */    
    endmodule
    

    В файле "pc.txt" - начальные значения регистров.

    Как и ожидалось, "ленивый" вариант преобразования многотактного процессора в многопоточный простой заменой файл-регистра и счетчика не работоспособен, требуются некоторый механизм управления.

    Для управления выборкой данных хартов введём группу регистров - hart-reg, объединенных в линейку. Выходные сигналы - номер харта и бит активности харта. При сбросе регистры инициируются нулями и биты активности хартов сброшены.

    На линейку в процессе функционирования должна подаваться последовательность хартов с кодами активности. Сама последовательность может, например, хранится в отдельной небольшой памяти - назовем ее hart_table. Hart_table адресуется циклическим счетчиком.

    Для текущего примера создадим простенький "системный таймер" разрядностью 64-бита (на самом деле можно любой приемлемой разрядности, в данном случае 64 бита выбрано по аналогии с традиционными таймерами-счетчиками в реализациях RISC-V архитектуры). Младшие разряды счетчика будут адресовать память таблицы хартов. Сам счетчик потом можно будет использовать в качестве счетчика тактов работы с момента старта системы, например.

    Биты активности харта также должны будут использованы для управления записью в такие модули процессорного ядра, как программный счетчик, файл-регистр, запись в память (если харт не активен - запись не будет произведена). Также инверсный сигнал активности харта подается на вход "сброса конвейера" АЛУ (да, для данной реализации будет взята версия АЛУ конвейеризированного процессорного ядра).

    Уровни "разделяются" промежуточными регистрами (обычные регистры хранения), которые фиксируют данные (регистры R0 - R6). Ряд сигналов просто "пробрасываются" по этапам (напрямую между регистрами) в том случае, если они не задействованы в них, но требуются на последующих - всё как и было в простом многотактном варианте.

    Общая структура многопоточного процессора представлена на рис.8.3.

    (рис 8.3) Структура многопоточного процессора

    Описание памяти таблицы хартов - в текущем варианте - память на 8 записей, выходная разрядность 4 бита, старший бит - бит активности харта, младшие три - номер харта:

    module rv_hart_table
    #(
      parameter DATA_WIDTH=4, 
      parameter ADDR_WIDTH=3
      )
    ( input clk,
      input [(ADDR_WIDTH-1):0] h_addr,
      output reg [(DATA_WIDTH-1):0] h_out,
      input we,
      input en
    );
    // ROM array
    reg [DATA_WIDTH-1:0] rom [0:2**ADDR_WIDTH-1] ;
    // read ROM content from file
    initial
    $readmemh("hart_table.txt",rom);
    always @ (*)
      begin
        h_out <= rom[h_addr];
      end
    endmodule
    

    Системный таймер:

    module rv_timer
    #(
      parameter DATA_WIDTH=64
      )
    ( input clk,
      input rst,
      output [(DATA_WIDTH-1):0] timer
    );
    reg [(DATA_WIDTH-1):0] cnt;
    always@(posedge clk or negedge rst)
      begin
        if(!rst)
          cnt <= 64'h0;
        else
          cnt <= cnt + 1'b1;
      end
    assign timer = cnt;
    endmodule
    

    Соединение таблицы хартов со счетчиком - рис.8.4.

    (рис 8.4) Соединение таблицы хартов со счетчиком
    wire [63:0] tmr_out;
    rv_timer tmr(
      .clk(clk),
      .rst(rst),
      .timer(tmr_out)
    );
    //==========================================================
    rv_hart_table hart_table(
      .clk(clk),
      .h_addr(tmr_out[2:0]),
      .h_out(hart0_out)
    );
    //==========================================================
    wire [3:0] hart0_out;

    Соединение линейки регистров для хранения хартов:

    wire [3:0] hart1_out;
    rv_hart_reg H1(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart0_out),
      .h_out(hart1_out)
    );
    wire [3:0] hart2_out;
    rv_hart_reg H2(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart1_out),
      .h_out(hart2_out)
    );
    wire [3:0] hart3_out;
    rv_hart_reg H3(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart2_out),
      .h_out(hart3_out)
    );
    wire [3:0] hart4_out;
    rv_hart_reg H4(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart3_out),
      .h_out(hart4_out)
    );
    wire [3:0] hart5_out;
    rv_hart_reg H5(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart4_out),
      .h_out(hart5_out)
    );
    wire [3:0] hart6_out;
    rv_hart_reg H6(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart5_out),
      .h_out(hart6_out)
    );
    

    Общий код процессора:

    `include "rv_pc.v"
    `include "rv_mem.v"
    `include "rv_desh_pipe.v"
    `include "rv_imm.v"
    `include "rv_reg_file_tst.v"
    `include "rv_ops_mux.v"
    `include "rv_cmp.v"
    `include "rv_alu_v.v"
    `include "rv_rez_mux.v"
    `include "rv_csr.v"
    `include "rv_r_reg.v"
    `include "rv_hart_reg.v"
    `include "rv_timer.v"
    `include "rv_hart_table.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
    );
    
    // CPU modules 
    //===============================================================
    wire [63:0] tmr_out;
    rv_timer tmr(
      .clk(clk),
      .rst(rst),
      .timer(tmr_out)
    );
    //===============================================================
    rv_hart_table hart_table(
      .clk(clk),
      .h_addr(tmr_out[2:0]),
      .h_out(hart0_out)
    );
    // ==============================================================
    wire [3:0] hart0_out;
    wire [3:0] hart1_out;
    rv_hart_reg H1(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart0_out),
      .h_out(hart1_out)
    );
    wire [3:0] hart2_out;
    rv_hart_reg H2(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart1_out),
      .h_out(hart2_out)
    );
    wire [3:0] hart3_out;
    rv_hart_reg H3(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart2_out),
      .h_out(hart3_out)
    );
    wire [3:0] hart4_out;
    rv_hart_reg H4(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart3_out),
      .h_out(hart4_out)
    );
    wire [3:0] hart5_out;
    rv_hart_reg H5(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart4_out),
      .h_out(hart5_out)
    );
    wire [3:0] hart6_out;
    rv_hart_reg H6(
      .clk(clk),
      .rst(rst),
      .en(1'b1),
      .h_in(hart5_out),
      .h_out(hart6_out)
    );
    // ==============================================================
    // L0_R1
    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(hart6_out[3]),
      .pc_load(r6l6_PC_load),
      .pc_next(r6l6_Rez),
      .pc(l0r1_PC),
      .pc_plus(l0r1_PCplus),
      .hart_in(hart6_out[2:0]),
      .hart_out(hart0_out[2:0])
    );
    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(1'b1),
      .r_in(r1_in),
      .r_out(r1_out)
    );
    // R1_L1
    wire [31:0] l1r2_iw;
    wire [31:0] l1r2_PC;
    wire [31:0] l1r2_PCplus;
    
    rv_mem mem( // system memory
      .clk(clk),
      .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(hart5_out[3])
    );
    // 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(1'b1),
      .r_in(r2_in),
      .r_out(r2_out)
    );
    // L2_R3
    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(),
      .pipe_rst(~hart2_out[3])
    );
    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(1'b1),
      .r_in(r3_in),
      .r_out(r3_out)
    );
    //L3_R4
    wire [31:0] l3r4_Rs1_reg;
    wire [31:0] l3r4_Rs2_reg;
    wire [31:0] l3r4_CSR;
    
    rv_reg_file reg_file(   // register's file
      .clk(~clk),
      .rs1(r3l3_rs1),
      .rs2(r3l3_rs2),
      .rd(r6l6_rd),
      .Rs1_out(l3r4_Rs1_reg),
      .Rs2_out(l3r4_Rs2_reg),
      .Rd_input(Rd_reg),
      .we(r6l6_rd_wr),
      .en(hart6_out[3]),
      .hart_in(hart6_out[2:0]),
      .hart_out(hart3_out[2:0])
    );
    rv_csr csr(
      .clk(~clk),
      .csr_addr_out(r3l3_imm[11:0]),
      .csr_addr_in(r6l6_imm[11:0]),
      .csr_in(r6l6_Rez),
      .csr_out(l3r4_CSR),
      .csr_wr(r6l6_csr_wr),
      .en(hart6_out[3])
    );
    assign Data_out = l3r4_CSR; //CSR;
    wire [31:0] r4l4_Rs1_reg;
    wire [31:0] r4l4_Rs2_reg;
    wire [6:0] r4l4_op;
    wire [2:0] r4l4_fn3;
    wire [6:0] r4l4_fn7;
    wire [4:0] r4l4_rd;
    wire r4l4_rd_wr;
    wire r4l4_mem_wr;
    wire r4l4_csr_wr;
    wire [31:0] r4l4_imm;
    wire [31:0] r4l4_PC;
    wire [31:0] r4l4_PCplus;
    wire [31:0] r4l4_CSR;
    
    wire [255:0] r4_in;
    wire [255:0] r4_out;
    assign r4_in = {l3r4_Rs1_reg, l3r4_Rs2_reg, r3l3_op, r3l3_fn3, r3l3_fn7, r3l3_rd, r3l3_rd_wr,
        r3l3_mem_wr, r3l3_csr_wr, r3l3_imm,  r3l3_PC, r3l3_PCplus, l3r4_CSR, 39'h0};
    wire [38:0] r4_null;
    assign {r4l4_Rs1_reg, r4l4_Rs2_reg, r4l4_op, r4l4_fn3, r4l4_fn7, r4l4_rd, r4l4_rd_wr,
        r4l4_mem_wr, r4l4_csr_wr, r4l4_imm,  r4l4_PC, r4l4_PCplus, r4l4_CSR, r4_null} = r4_out;
    
    rv_r_reg R4(
      .clk(clk),
      .rst_n(rst),
      .en(1'b1),
      .r_in(r4_in),
      .r_out(r4_out)
    );
    // L4
    
    wire [31:0] Op1;
    wire [31:0] Op2;
    
    rv_ops_mux ops_mux(
      .opcode(r4l4_op),
      .funct3(r4l4_fn3),
      .funct7(r4l4_fn7),
      .Rs1(r4l4_Rs1_reg),
      .Rs2(r4l4_Rs2_reg),
      .imm(r4l4_imm),
      .PC(r4l4_PC),
      .CSR(r4l4_CSR),
      .Op1(Op1),
      .Op2(Op2)
    );
    
    rv_cmp cmp(
      .opcode(r4l4_op),
      .funct3(r4l4_fn3),
      .Rs1(r4l4_Rs1_reg),
      .Rs2(r4l4_Rs2_reg),
      .pc_new(l4r5_PC_load)
    );
    
    rv_alu_v alu_v(
      .opcode(r4l4_op),
      .funct3(r4l4_fn3),
      .funct7(r4l4_fn7),
      .Op1(Op1),
      .Op2(Op2),
      .Rez(l4r5_Rez)
    );
    wire [31:0] l4r5_Rez;
    wire l4r5_PC_load;
    wire [31:0] r5l5_Rez;
    wire [31:0] r5l5_Rs2_reg;
    wire [6:0] r5l5_op;
    wire [2:0] r5l5_fn3;
    wire [6:0] r5l5_fn7;
    wire [4:0] r5l5_rd;
    wire r5l5_rd_wr;
    wire r5l5_mem_wr; 
    wire r5l5_csr_wr;
    wire [31:0] r5l5_imm;
    wire [31:0] r5l5_PC;
    wire [31:0] r5l5_PCplus;
    wire r5l5_PC_load;
    //L5
    wire [255:0] r5_in;
    wire [255:0] r5_out;
    assign r5_in = {l4r5_Rez, r4l4_Rs2_reg, r4l4_op, r4l4_fn3, r4l4_fn7, r4l4_rd, r4l4_rd_wr,
        r4l4_mem_wr, r4l4_csr_wr, r4l4_imm,  r4l4_PC, r4l4_PCplus, l4r5_PC_load, 70'h0};
    wire [69:0] r5_null;
    assign {r5l5_Rez, r5l5_Rs2_reg, r5l5_op, r5l5_fn3, r5l5_fn7, r5l5_rd, r5l5_rd_wr,
        r5l5_mem_wr, r5l5_csr_wr, r5l5_imm,  r5l5_PC, r5l5_PCplus, r5l5_PC_load, r5_null} = r5_out;
    
    rv_r_reg R5(
      .clk(clk),
      .rst_n(rst),
      .en(1'b1),
      .r_in(r5_in),
      .r_out(r5_out)
    );
    // L6
    wire [31:0] l5r6_Mem_data;
    wire [31:0] r6l6_Mem_data;
    wire [31:0] r6l6_Rez;
    wire [31:0] r6l6_Rs2_reg;
    wire [6:0] r6l6_op;
    wire [2:0] r6l6_fn3;
    wire [6:0] r6l6_fn7;
    wire [4:0] r6l6_rd;
    wire r6l6_rd_wr;
    wire r6l6_mem_wr;
    wire r6l6_csr_wr; 
    wire [31:0] r6l6_imm;
    wire [31:0] r6l6_PC;
    wire [31:0] r6l6_PCplus;
    wire r6l6_PC_load;
    
    wire [255:0] r6_in;
    wire [255:0] r6_out;
    assign r6_in = {l5r6_Mem_data, r5l5_Rez,  r5l5_op, r5l5_fn3, r5l5_fn7, r5l5_rd, r5l5_rd_wr,
        r5l5_mem_wr, r5l5_csr_wr, r5l5_imm,  r5l5_PC, r5l5_PCplus, r5l5_PC_load, 70'h0};
    wire [69:0] r6_null;
    assign {r6l6_Mem_data, r6l6_Rez, r6l6_op, r6l6_fn3, r6l6_fn7, r6l6_rd, r6l6_rd_wr,
        r6l6_mem_wr, r6l6_csr_wr, r6l6_imm,  r6l6_PC, r6l6_PCplus, r6l6_PC_load, r6_null} = r6_out;
    
    rv_r_reg R6(
      .clk(clk),
      .rst_n(rst),
      .en(1'b1),
      .r_in(r6_in),
      .r_out(r6_out)
    );
    wire [31:0] Rd_reg;
    rv_rez_mux rez_mux(
      .opcode(r6l6_op),
      .funct3(r6l6_fn3),
      .funct7(r6l6_fn7),
      .Rez(r6l6_Rez),
      .Pc_plus(r6l6_PCplus),
      .Mem_data(r6l6_Mem_data),
      .Imm(r6l6_imm),
      .Rd(Rd_reg)
    );
    Endmodule
    

    Традиционно небольшая, тестовая программа:

    lui x1,0x1
    srli x1,x1,12
    l1:
    addi x1, x1, 1
    jal x2,l1
    lui x1,0x99
    
    lui x1,0x2
    srli x1,x1,12
    l2:
    addi x1, x1, 2
    jal x2,l2
    lui x1,0x99
    
    lui x1,0x3
    srli x1,x1,12
    l3:
    addi x1, x1, 3
    jal x2,l3
    lui x1,0x99
    
    lui x1,0x4
    srli x1,x1,12
    l4:
    addi x1, x1, 4
    jal x2,l4
    lui x1,0x99
    
    lui x1,0x5
    srli x1,x1,12
    l5:
    addi x1, x1, 5
    jal x2,l5
    lui x1,0x99
    
    lui x1,0x6
    srli x1,x1,12
    l6:
    addi x1, x1, 6
    jal x2,l6
    lui x1,0x99
    
    lui x1,0x7
    srli x1,x1,12
    l7:
    addi x1, x1, 7
    jal x2,l7
    lui x1,0x99
    
    lui x1,0x8
    srli x1,x1,12
    l8:
    addi x1, x1, 8
    jal x2,l8
    lui x1,0x99
    

    На рис.8.5 - она же в симуляторе RARS.

    (рис 8.5)

    В программе заданы несколько коротких бесконечных цикла, работающих с регистрами х1, х2. Регистр х1 инициируется определенным значением, регистр х2 будет использован, как регистр связи. При простом линейном исполнении программа зациклится на метке l1.

    Дамп программы:

    Address     Code        Basic                        Line Source
    
    0x00400000  0x000010b7  lui x1,1                     1    lui x1,0x1
    0x00400004  0x00c0d093  srli x1,x1,12                2    srli x1,x1,12
    0x00400008  0x00108093  addi x1,x1,1                 4    addi x1, x1, 1
    0x0040000c  0xffdff16f  jal x2,0xfffffffc            5    jal x2,l1
    0x00400010  0x000990b7  lui x1,0x00000099            6    lui x1,0x99
    0x00400014  0x000020b7  lui x1,2                     8    lui x1,0x2
    0x00400018  0x00c0d093  srli x1,x1,12                9    srli x1,x1,12
    0x0040001c  0x00208093  addi x1,x1,2                 11   addi x1, x1, 2
    0x00400020  0xffdff16f  jal x2,0xfffffffc            12   jal x2,l2
    0x00400024  0x000990b7  lui x1,0x00000099            13   lui x1,0x99
    0x00400028  0x000030b7  lui x1,3                     15   lui x1,0x3
    0x0040002c  0x00c0d093  srli x1,x1,12                16   srli x1,x1,12
    0x00400030  0x00308093  addi x1,x1,3                 18   addi x1, x1, 3
    0x00400034  0xffdff16f  jal x2,0xfffffffc            19   jal x2,l3
    0x00400038  0x000990b7  lui x1,0x00000099            20   lui x1,0x99
    0x0040003c  0x000040b7  lui x1,4                     22   lui x1,0x4
    0x00400040  0x00c0d093  srli x1,x1,12                23   srli x1,x1,12
    0x00400044  0x00408093  addi x1,x1,4                 25   addi x1, x1, 4
    0x00400048  0xffdff16f  jal x2,0xfffffffc            26   jal x2,l4
    0x0040004c  0x000990b7  lui x1,0x00000099            27   lui x1,0x99
    0x00400050  0x000050b7  lui x1,5                     29   lui x1,0x5
    0x00400054  0x00c0d093  srli x1,x1,12                30   srli x1,x1,12
    0x00400058  0x00508093  addi x1,x1,5                 32   addi x1, x1, 5
    0x0040005c  0xffdff16f  jal x2,0xfffffffc            33   jal x2,l5
    0x00400060  0x000990b7  lui x1,0x00000099            34   lui x1,0x99
    0x00400064  0x000060b7  lui x1,6                     36   lui x1,0x6
    0x00400068  0x00c0d093  srli x1,x1,12                37   srli x1,x1,12
    0x0040006c  0x00608093  addi x1,x1,6                 39   addi x1, x1, 6
    0x00400070  0xffdff16f  jal x2,0xfffffffc            40   jal x2,l6
    0x00400074  0x000990b7  lui x1,0x00000099            41   lui x1,0x99
    0x00400078  0x000070b7  lui x1,7                     43   lui x1,0x7
    0x0040007c  0x00c0d093  srli x1,x1,12                44   srli x1,x1,12
    0x00400080  0x00708093  addi x1,x1,7                 46   addi x1, x1, 7
    0x00400084  0xffdff16f  jal x2,0xfffffffc            47   jal x2,l7
    0x00400088  0x000990b7  lui x1,0x00000099            48   lui x1,0x99
    0x0040008c  0x000080b7  lui x1,8                     50   lui x1,0x8
    0x00400090  0x00c0d093  srli x1,x1,12                51   srli x1,x1,12
    0x00400094  0x00808093  addi x1,x1,8                 53   addi x1, x1, 8
    0x00400098  0xffdff16f  jal x2,0xfffffffc            54   jal x2,l8
    0x0040009c  0x000990b7  lui x1,0x00000099            55   lui x1,0x99
    

    Для исполнения на многопоточном процессоре массив программных счетчиков инициируется значениями адресов начала каждого из циклов - в данном примере - адреса:

    0x0, 0x14, 0x28, 0x3С, 0x50, 0x64, 0x78, 0x8C.

    Таблица хартов - перечисление номеров хартов с 0 по 7 в 4-битном представлении. Например, если активен должен быть харт с номером 0, то в таблице он будет представлен цифрой 8 (4`b1000), если с номером 3, то 0xB (4`b1011) и так далее.

    (рис 8.5) Тестовая программа в симуляторе RARS

    Тест 1:

    PC.txt Hart_table.txt

    @00

    0

    14

    28

    3c

    50

    64

    78

    8c

    @00

    8

    1

    2

    3

    4

    5

    6

    7

    Рис. 8.6-8.7 - временные диаграммы исполнения кода.

    На ней представлены значения:

  • сигналы сброса и тактовый;
  • номера хартов (с битом активности);
  • значение программного счетчика и выбранное из памяти слово инструкции;
  • для АЛУ - опкод команды, операнды и результат;
  • для файл-регистра текущие выходы и вход с разрешением записи;
  • тестовые выходы первых 4 регистров файл-регистра с указанием, к какому харту они относятся.
  • Для регистра Х1 0-го активного харта видна сначала его загрузка значением 0х1000, потом сдвиг его до 0х1.

    (рис 8.6) Временные диаграммы исполнения программы в многопоточном ядре - активен харт0 (рис 8.7) Временные диаграммы исполнения программы в многопоточном ядре - активен харт0

    Тест 3 - распределение адресов тоже самое, активируем 2й харт (номер 1):

    PC.txt Hart_table.txt

    @00

    0

    14

    28

    3c

    50

    64

    78

    8c

    @00

    8

    9

    2

    3

    4

    5

    6

    7

    (рис 8.8) Временные диаграммы исполнения программы в многопоточном ядре - активены харт0 и харт1

    Итоговые микроархитектурные блоки многопоточного варианта процессора:

  • rv_pc - программный счетчик (массив из нескольких регистров);
  • rv_mem - блок памяти (программная и оперативная;
  • rv_desh - дешифратор команд (слова-инструкции) с входом "сброса конвейера";
  • rv_imm - формирователь непосредственного значения из слова-инструкции;
  • rv_reg_file - файл-регистр (массив из нескольких файл-регистров);
  • rv_ops_mux - коммутатор операндов для АЛУ;
  • rv_cmp - формирователь сигнала разрешения перехода;
  • rv_alu_v - АЛУ;
  • rv_rez_mux - коммутатор результатов;
  • rv_csr - блок регистров специального назначения;
  • rv_r_reg - регистры хранения результатов промежуточных этапов;
  • rv_hart_reg - регистры хранения номеров и состояния активности хартов;
  • rv_timer - системный таймер;
  • rv_hart_table - память хранения списка и состояния хартов.
  • Литература

  • Мультиядерные микроконтроллеры семейства XCore от XMOS // http://cyberleninka.ru/article/n/multiyadernye-mikrokontrollery-semeystva-xcore-ot-xmos
  • Using RISC-V to define SoCs in software _ XMOS // http://www.xmos.com/using-risc-v-to-define-socs-in-software/
  • The RISC-V Instruction Set Manual. Volume I: User-Level ISA Document Version 2.2 // Editors: Andrew Waterman1, Krste Asanov
  • http://electronics.stackexchange.com/questions/580645/what-is-a-hardware-thread-in-risc-v
  • http://www.computer.org/csdl/proceedings-article/trustcom/2022/942500b587/1LFM9tik1IQ
  • Вернуться к учебному плану