Implementacja sprzętowej wielowątkowości w procesorze programowalnym RISC-V dla FPGA
Mikroarchitektura wielowątkowa procesora programowalnego opartego na RISC-V wykorzystuje rejestry cienia do przechowywania stanów hartów, zapewniając szybkie przełączanie kontekstów bez interwencji oprogramowania. Upraszcza to przetwarzanie zdarzeń asynchronicznych w projektach FPGA, minimalizując obciążenie kontrolera przerwań i redukując ilość kodu. Implementacja jest zorientowana na systemy z jednym poziomem uprzywilejowania i wspólną przestrzenią adresową.
Podstawy mikroarchitektury
Kluczowym elementem są tablice rejestrów cienia dla każdego harta (hart w terminologii RISC-V). Każdy hart przechowuje stan w kopiach rejestrów x0-x31 i PC. Wspólne pozostają ALU, pamięć, peryferia i rejestry CSR. Ochrona danych jest realizowana na poziomie oprogramowania.
Obsługiwany jest podstawowy zestaw instrukcji I+Zicsr. Zarządzanie wątkami odbywa się przez rejestry CSR i tabelę hartów (hart_table). Przełączanie hartów następuje cyklicznie według timera systemowego, którego młodsze bity adresują tabelę.
Bity aktywności hartów kontrolują zapis do PC, rejestrów i pamięci, a także resetują potok ALU dla nieaktywnych wątków.
Moduł rejestrów CSR
Blok CSR jest podzielony na niezależne porty odczytu i zapisu dla kompatybilności z wielowątkowością:
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
Plik rejestrów z obsługą hartów
Wielowątkowy plik rejestrów implementuje tablicę RAM dla 8 hartów. Odczyt/zapis są adresowane według numeru hart_in/hart_out:
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, // hart to read out data from
input [2:0] hart_out, // hart 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
Licznik programu dla wątków
PC jest zrealizowany jako tablica rejestrów z inicjalizacją z pliku pc.txt. Inkrementacja i ładowanie odbywają się tylko dla aktywnego harta:
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
assign pc = pc_reg[hart_out];
always @ *
begin
pc_plus <= pc_reg[hart_out] + 3'h4;
end
endmodule
Tabela hartów i zarządzanie
Hart_table to ROM na 8 wpisów (4 bity: 1 bit aktywności + 3 bity numeru harta), adresowany przez timer. Systemowy 64-bitowy timer służy jako licznik cykliczny i systemowy chronometr.
Pełna lista modułów:
- rv_pc — wielowątkowy licznik rozkazów
- rv_mem — unifikowana pamięć
- rv_desh — dekoder z resetem potoku
- rv_reg_file — rejestry cienia
- rv_alu_v — potokowe ALU
- rv_csr — wspólne rejestry specjalne
- rv_hart_reg — status hartów
- rv_timer — 64-bitowy timer
- rv_hart_table — konfiguracja wątków
Zalety implementacji
- Szybkie przełączanie: sprzętowe, bez zapisywania/przywracania rejestrów
- Uproszczenie IRQ: wątki zamiast wektorów przerwań
- Kompatybilność RISC-V: obsługa koncepcji hartów
- Optymalizacja FPGA: wspólne zasoby, minimum logiki
Co jest ważne
- Rejestry cienia przechowują pełny stan architektoniczny każdego harta
- Wspólne CSR wymagają oddzielnych portów R/W dla poprawnej pracy
- Aktywność harta kontroluje wszystkie zapisy i resetuje potok
- Timer cyklicznie adresuje hart_table, zapewniając round-robin
- Ochrona pamięci i synchronizacja — odpowiedzialność OS/RTOS
— Editorial Team
Brak komentarzy.