Implementing Hardware Multithreading in a RISC-V Soft-Core Processor for FPGA
A multithreaded microarchitecture for a RISC-V-based soft-core processor uses shadow registers to store hart states, enabling fast context switching without software intervention. This simplifies handling asynchronous events in FPGA projects, minimizing interrupt controller load and reducing code volume. The implementation targets systems with a single privilege level and shared address space.
Microarchitecture Fundamentals
The key element is arrays of shadow registers for each hart (using RISC-V terminology). Each hart saves its state in copies of registers x0-x31 and the PC. The ALU, memory, peripherals, and CSR registers remain shared. Data protection is implemented at the software level.
The base instruction set I+Zicsr is supported. Thread management is handled via CSR registers and a hart table (hart_table). Hart switching occurs cyclically based on the system timer, with its lower bits addressing the table.
Hart activity bits control writes to the PC, registers, and memory, and reset the ALU pipeline for inactive threads.
CSR Register Module
The CSR block is divided into independent read and write ports for multithreading compatibility:
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
Register File with Hart Support
The multithreaded register file implements a RAM array for 8 harts. Read/write operations are addressed by hart_in/hart_out numbers:
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
Program Counter for Threads
The PC is implemented as a register array initialized from a pc.txt file. Increment and load operations occur only for the active hart:
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
Hart Table and Management
The hart_table is a ROM with 8 entries (4 bits: 1 activity bit + 3 hart number bits), addressed by the timer. The 64-bit system timer serves as both a cyclic counter and a system chronometer.
Full module list:
- rv_pc — multithreaded program counter
- rv_mem — unified memory
- rv_desh — decoder with pipeline reset
- rv_reg_file — shadow registers
- rv_alu_v — pipelined ALU
- rv_csr — shared special registers
- rv_hart_reg — hart status
- rv_timer — 64-bit timer
- rv_hart_table — thread configuration
Implementation Advantages
- Fast Switching: hardware-based, without saving/restoring registers
- IRQ Simplification: threads instead of interrupt vectors
- RISC-V Compatibility: support for hart concept
- FPGA Optimization: shared resources, minimal logic
Key Points
- Shadow registers store the full architectural state of each hart
- Shared CSRs require separate R/W ports for correct operation
- Hart activity controls all writes and resets the pipeline
- The timer addresses the hart_table cyclically, ensuring round-robin scheduling
- Memory protection and synchronization are the responsibility of the OS/RTOS
— Editorial Team
No comments yet.