Многопоточное процессорное ядро
На работу над многопоточным вариантом вдохновили процессоры семейства Xcore компании XMOS, специализирующейся на процессорах/микроконтроллерах для встраиваемых систем. Решения XMOS традиционно включают в себя элементы, которые традиционно требовали бы использования компонентов другого класса [1]. Там, где традиционно можно использовать микроконтроллер для управления конструкцией, DSP для обработки сигналов и, возможно, CPLD для подключения к сложному цифровому интерфейсу, процессоры XMOS могут выполнять эти три задачи в одном устройстве, используя единый программный процесс на основе программного обеспечения. Их характерные черты - аппаратная многопоточность, возможность масштабирования количества ядер/процессоров в системе, гибкие программно-конфигурируемые порты ввода-вывода. В ранних версиях своих процессоров XMOS применяли свои RISC ядра, были серии с комбинацией RISC-ядер и ARM-ядра. В 2023м году компания анонсировала вариант процессора с ядрами RISC-V (рис.8.1).
Вторым побуждающим мотивом экспериментов с является желание в решениях софт-ядер для FPGA уйти от необходимости введения в состав софт-процессора контроллера прерываний (кто любит обрабатывать прерывания, да еще и вложенные? Да - мало кто).
Попробуем несколько модифицировать многотактный процессор, добавив ему поддержку многопоточности в виде "теневых" копий основных архитектурных регистров. В терминологии RISC-V аппаратно-поддерживаемый поток называется хартом (hart) [2-4]. Общая структура ядра пока останется почти прежней - рис.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_wr&en) 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