RISC-V Single-Cycle Processor: addi Implementation and DPI-C Termination
Understanding addi Execution in Single-Cycle Architecture
The addi instruction belongs to the RISC-V I-type format with the following bit layout:
[31:20] immediate | [19:15] rs1 | [14:12] funct3 | [11:7] rd | [6:0] opcode
Operation: rd = rs1 + immediate
In single-cycle processors, every instruction completes within one clock period. This requires a carefully designed datapath that performs all necessary operations for addi before the next clock edge.
Core Components
- Instruction Memory (IMEM): Stores program instructions, indexed by the program counter
- Register File: Provides data from source registers and accepts results for destination registers
- Immediate Generator: Extracts and sign-extends the 12-bit immediate to 32 bits
- Arithmetic Unit: Performs addition betwean register value and immediate
- PC Increment Logic: Calculates PC+4 for sequential execution (jump support deferred for now)
Execution Flow
- Fetch instruction from memory using current PC value
- Decode instruction fields: opcode, rd, rs1, immediate
- Read source register rs1 from register file
- Generate sign-extended 32-bit immediate value
- Execute addition operation in ALU
- Write result back to destination register rd
- Update PC to PC+4 for next instruction
Datapath Architecture
The synchronous design uses edge-triggered writes and combinational reads:
Program Counter → Instruction Memory → Decode Logic → Register File (Read rs1)
↓
Immediate Generator → ALU (Add) → Register File (Write rd)
↓
PC Increment Logic → Next PC Register
Verilog Implementation
Top-Level Module
module cpu_core(
input wire clock,
input wire reset,
output reg [31:0] result_out
);
// Pipeline registers
reg [31:0] current_pc;
reg [31:0] next_pc_reg;
// Instruction fields
wire [6:0] opcode;
wire [4:0] dest_reg;
wire [4:0] src1_reg;
wire [2:0] func3_field;
wire [31:0] imm_value;
// Register file signals
wire [31:0] src1_data;
wire [31:0] writeback_data;
wire [4:0] writeback_addr;
wire regfile_wen;
// ALU result
wire [31:0] alu_result;
// ebreak detection
wire breakpoint_detected;
initial begin
current_pc = 32'h80000000;
next_pc_reg = current_pc + 4;
result_out = 32'h0;
end
always @(posedge clock or posedge reset) begin
if (reset) begin
current_pc <= 32'h80000000;
next_pc_reg <= current_pc + 4;
result_out <= 32'h0;
end else begin
current_pc <= next_pc_reg;
next_pc_reg <= current_pc + 4;
result_out <= alu_result;
if (breakpoint_detected) begin
$display("Breakpoint at time %0t", $time);
ebreak_handler();
end
end
end
// Module instantiations
instruction_fetch fetch_unit(
.pc_addr(current_pc),
.clk(clock),
.instruction_word(ins)
);
decode_unit decoder(
.instruction_word(ins),
.opcode(opcode),
.dest_reg(dest_reg),
.func3_field(func3_field),
.src1_reg(src1_reg),
.imm_value(imm_value),
.regfile_wen(regfile_wen),
.breakpoint_detected(breakpoint_detected)
);
regfile #(
.ADDR_BITS(5),
.DATA_BITS(32)
) register_bank(
.clk(clock),
.write_data(writeback_data),
.write_addr(writeback_addr),
.write_en(regfile_wen),
.read_addr1(src1_reg),
.read_data1(src1_data)
);
execution_unit alu_block(
.imm_value(imm_value),
.src1_value(src1_data),
.func3_field(func3_field),
.opcode(opcode),
.dest_reg(dest_reg),
.regfile_wen(regfile_wen),
.writeback_data(writeback_data),
.writeback_addr(writeback_addr),
.alu_result(alu_result)
);
endmodule
Register File Module
module regfile #(
parameter ADDR_BITS = 5,
parameter DATA_BITS = 32
)(
input wire clk,
input wire [DATA_BITS-1:0] write_data,
input wire [ADDR_BITS-1:0] write_addr,
input wire write_en,
input wire [ADDR_BITS-1:0] read_addr1,
output reg [DATA_BITS-1:0] read_data1
);
localparam NUM_REGS = 1 << ADDR_BITS;
reg [DATA_BITS-1:0] registers[NUM_REGS-1:0];
integer idx;
initial begin
for (idx = 0; idx < NUM_REGS; idx = idx + 1) begin
registers[idx] = {DATA_BITS{1'b0}};
end
end
always @(*) begin
if (read_addr1 == {ADDR_BITS{1'b0}}) begin
read_data1 = {DATA_BITS{1'b0}};
end else begin
read_data1 = registers[read_addr1];
end
end
always @(posedge clk) begin
if (write_en && (write_addr != {ADDR_BITS{1'b0}})) begin
registers[write_addr] <= write_data;
$display("Time=%0t: Reg[%0d] <= 0x%08x", $time, write_addr, write_data);
end
end
endmodule
Instruction Fetch Unit
module instruction_fetch(
input wire [31:0] pc_addr,
input wire clk,
output reg [31:0] instruction_word
);
reg [31:0] instruction_memory[0:15];
integer i;
initial begin
// addi x5, x0, 5
instruction_memory[0] = 32'h00500293;
// addi x6, x5, 10
instruction_memory[1] = 32'h00a30313;
// addi x7, x0, 1
instruction_memory[2] = 32'h00100393;
// addi x8, x0, 2
instruction_memory[3] = 32'h00200413;
// addi x9, x5, 15
instruction_memory[4] = 32'h00f28593;
// ebreak
instruction_memory[5] = 32'h00100073;
for (i = 6; i < 16; i = i + 1) begin
instruction_memory[i] = 32'h00000013; // nop
end
end
always @(posedge clk) begin
if (pc_addr >= 32'h80000000 && pc_addr < 32'h80000040) begin
instruction_word <= instruction_memory[(pc_addr - 32'h80000000) >> 2];
$display("Fetch: PC=0x%08x, Inst=0x%08x", pc_addr, instruction_word);
end else begin
instruction_word <= 32'h0;
end
end
endmodule
Decode Unit
module decode_unit(
input wire [31:0] instruction_word,
output reg [6:0] opcode,
output reg [4:0] dest_reg,
output reg [4:0] src1_reg,
output reg [2:0] func3_field,
output reg [31:0] imm_value,
output reg regfile_wen,
output reg breakpoint_detected
);
always @(*) begin
opcode = instruction_word[6:0];
dest_reg = instruction_word[11:7];
func3_field = instruction_word[14:12];
src1_reg = instruction_word[19:15];
// ebreak detection
if (opcode == 7'b1110011 && func3_field == 3'b000 && instruction_word[31:20] == 12'h001) begin
breakpoint_detected = 1'b1;
end else begin
breakpoint_detected = 1'b0;
end
// Sign-extend immediate
if (instruction_word[31]) begin
imm_value = {20'hFFFFF, instruction_word[31:20]};
end else begin
imm_value = {20'h0, instruction_word[31:20]};
end
// Decode addi
if (opcode == 7'b0010011 && func3_field == 3'b000) begin
regfile_wen = 1'b1;
end else begin
regfile_wen = 1'b0;
end
end
endmodule
Execution Unit
module execution_unit(
input wire [31:0] imm_value,
input wire [31:0] src1_value,
input wire [2:0] func3_field,
input wire [6:0] opcode,
input wire [4:0] dest_reg,
input wire regfile_wen,
output reg [31:0] writeback_data,
output reg [4:0] writeback_addr,
output reg [31:0] alu_result
);
always @(*) begin
if (opcode == 7'b0010011 && func3_field == 3'b000) begin
writeback_data = src1_value + imm_value;
writeback_addr = dest_reg;
alu_result = writeback_data;
end else begin
writeback_data = 32'h0;
writeback_addr = 5'h0;
alu_result = 32'h0;
end
end
endmodule
Testbench Implementation
#include "Vcpu_core.h"
#include "verilated.h"
#include "verilated_vcd_c.h"
#include <iostream>
extern "C" void ebreak_handler();
int main(int argc, char **argv) {
Verilated::commandArgs(argc, argv);
Vcpu_core *dut = new Vcpu_core;
VerilatedVcdC *trace = new VerilatedVcdC;
Verilated::traceEverOn(true);
dut->trace(trace, 99);
trace->open("simulation.vcd");
// Reset sequence
dut->clock = 0;
dut->reset = 1;
for (int i = 0; i < 4; i++) {
dut->clock = !dut->clock;
dut->eval();
trace->dump(i * 10);
}
dut->reset = 0;
int cycle = 0;
while (cycle < 100 && !Verilated::gotFinish()) {
dut->clock = !dut->clock;
dut->eval();
trace->dump(100 + cycle * 10);
if (dut->clock) {
std::cout << "Cycle " << cycle/2 << ": Result=0x"
<< std::hex << dut->result_out << std::dec << std::endl;
}
cycle++;
}
trace->close();
delete dut;
delete trace;
return 0;
}
</iostream>
DPI-C Integration
DPI-C Interface
// dpi_callback.cpp
#include <cstdlib>
#include <iostream>
extern "C" void ebreak_handler() {
std::cout << "EBREAK encountered - terminating simulation" << std::endl;
std::cout << "Program execution completed successfully" << std::endl;
exit(0);
}
</iostream></cstdlib>
Modified Build Configuration
# Makefile
VERILATOR = verilator
VERILOG_SOURCES = $(wildcard ./rtl/*.v)
CPP_SOURCES = ./tb/main.cpp ./tb/dpi_callback.cpp
SIM_FLAGS = -Wno-fatal --top-module cpu_core --cc --trace --exe
SIM_FLAGS += --build
SIM_FLAGS += -CFLAGS "-I./tb"
SIM_FLAGS += -LDFLAGS "-L./tb"
simulation: $(VERILOG_SOURCES) $(CPP_SOURCES)
$(VERILATOR) $(SIM_FLAGS) $^ -o sim_binary
./obj_dir/sim_binary
clean:
rm -rf obj_dir simulation.vcd
.PHONY: simulation clean
Simulation Results
Typical console output:
Fetch: PC=0x80000000, Inst=0x00500293
Fetch: PC=0x80000004, Inst=0x00a30313
Reg[5] <= 0x00000005
Fetch: PC=0x80000008, Inst=0x00100393
Reg[6] <= 0x0000000f
Fetch: PC=0x8000000c, Inst=0x00200413
Reg[7] <= 0x00000001
Fetch: PC=0x80000010, Inst=0x00f28593
Reg[8] <= 0x00000002
EBREAK encountered - terminating simulation
The processor correctly executes five addi instructions before the ebreak instruction triggers simulation termination through the DPI-C interface.
Key Design Considerations
- Zero Register Handling: Writes to x0 are silently ignored while reads always return zero
- Sign Extension: 12-bit immediates are sign-extended to 32 bits for arithmetic operations
- Timing Model: Register writes occur on clock edges; reads are combinational, enabling single-cycle operation
- Breakpoint Detection: ebreak instruction (0x00100073) triggers DPI-C callback for clean simulation exit
- PC Range Checking: Fetch unit validates PC addresses against memory bounds
This implementation demonstrates a minimal yet functional RISC-V single-cycle processor core that can be extended with additional instructions and control flow mechanisms.