Milestone 1
Overview
This milestone implements a basic 5-stage RISC-V pipeline simulator with the following stages:
- Instruction Fetch (IF): Fetches instructions from memory at the current PC
- Instruction Decode (ID): Decodes the instruction and reads from the register file
- Execute (EX): Performs ALU operations and computes branch/jump targets
- Memory (MEM): Accesses data memory for load/store instructions
- Write Back (WB): Writes results back to the register file
MS1 Implementation Details
1. Control Logic Generation in ID Stage
The control logic generation is implemented in the gen_control() function in stage_helpers.h. This function takes an instruction and generates all necessary control signals based on the opcode:
idex_reg_t gen_control(Instruction instruction)
{
idex_reg_t idex_reg = {0};
switch (instruction.opcode) { case 0x33: // R-type idex_reg.alu_src = 0; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 1; idex_reg.mem_read = 0; idex_reg.mem_write = 0; idex_reg.branch = 0; break;
case 0x13: // I-type arithmetic idex_reg.alu_src = 1; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 1; idex_reg.mem_read = 0; idex_reg.mem_write = 0; idex_reg.branch = 0; break;
case 0x03: // load idex_reg.alu_src = 1; idex_reg.mem_to_reg = 1; idex_reg.reg_write = 1; idex_reg.mem_read = 1; idex_reg.mem_write = 0; idex_reg.branch = 0; break;
case 0x23: // store idex_reg.alu_src = 1; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 0; idex_reg.mem_read = 0; idex_reg.mem_write = 1; idex_reg.branch = 0; break;
case 0x63: // branch (BEQ, BNE, etc.) idex_reg.alu_src = 0; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 0; idex_reg.mem_read = 0; idex_reg.mem_write = 0; idex_reg.branch = 1; break;
case 0x6F: // JAL idex_reg.alu_src = 1; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 1; idex_reg.jal = 1; break;
case 0x67: // JALR idex_reg.alu_src = 1; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 1; idex_reg.jalr = 1; break;
case 0x37: // LUI idex_reg.alu_src = 1; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 1; idex_reg.is_lui = 1; break;
case 0x17: // AUIPC idex_reg.alu_src = 1; idex_reg.mem_to_reg = 0; idex_reg.reg_write = 1; idex_reg.is_auipc = 1; break;
default: // Remaining opcodes break; } return idex_reg;} Control Signals Generated:
alu_src: Selects ALU input 2 (0 = register, 1 = immediate)mem_to_reg: Selects write-back data source (0 = ALU result, 1 = memory data)reg_write: Enables register file writemem_read/mem_write: Controls memory operationsbranch: Indicates branch instructionjal/jalr: Indicates jump instructionsis_lui/is_auipc: Special flags for upper immediate instructions
2. IF Stage Mux Implementation
The multiplexer in the IF stage selects the next PC value between the sequential PC (PC+4) and a branch/jump target. This is implemented in the stage_fetch() function:
ifid_reg_t stage_fetch(pipeline_wires_t* pwires_p, regfile_t* regfile_p, Byte* memory_p)
{
// Pick the fetch PC first (consume redirect for this cycle) bool redirected = pwires_p->pcsrc; uint32_t pc = redirected ? pwires_p->pc_src1 : regfile_p->PC; // ... fetch instruction from memory ...
// Compute default next-PC = pc + 4
pwires_p->next_pc = pc + 4; // Update PC logic handled in cycle_pipeline()
if (redirected) pwires_p->pcsrc = 0; // consume redirect now} Mux Logic:
pwires_p->pcsrcacts as the select signalregfile_p->PCprovides the sequential address (current PC)pwires_p->pc_src1provides the branch/jump target address- The selected PC is used to fetch the instruction for the current cycle
3. EX Stage Mux Implementation
The EX stage has multiple multiplexers for ALU input selection and forwarding. The main ALU input mux is implemented in stage_execute():
exmem_reg_t stage_execute(idex_reg_t idex_reg, pipeline_wires_t* pwires_p, pipeline_regs_t* pregs_p)
{
// Select ALU inputs: rs1 always, rs2 or immediate uint32_t alu_inp1 = idex_reg.rs1_val; uint32_t alu_inp2 = idex_reg.alu_src ? idex_reg.immediate : idex_reg.rs2_val;
// Handle forwarding for ALU input 1 (rs1) if (pwires_p->forwardA == 1) { // Forward from EX/MEM stage alu_inp1 = pregs_p->exmem_preg.out.alu_result; } else if (pwires_p->forwardA == 2) { // Forward from MEM/WB stage uint32_t memwb_result = pregs_p->memwb_preg.out.mem_to_reg ? pregs_p->memwb_preg.out.mem_data :
pregs_p->memwb_preg.out.alu_result;
alu_inp1 = memwb_result; } // Apply forwarding for ALU input 2 (rs2) - only if not using immediate
if (!idex_reg.alu_src) { if (pwires_p->forwardB == 1) { alu_inp2 = pregs_p->exmem_preg.out.alu_result; } else if (pwires_p->forwardB == 2) { uint32_t memwb_result = pregs_p->memwb_preg.out.mem_to_reg ? pregs_p->memwb_preg.out.mem_data :
pregs_p->memwb_preg.out.alu_result;
alu_inp2 = memwb_result; } } // ... perform ALU operation ...
} EX Stage Multiplexers:
- ALU Input 2 Mux: Selects between rs2 register value and immediate value based on
alu_src - Forwarding Mux A: Selects ALU input 1 source (register file, EX/MEM forward, or MEM/WB forward)
- Forwarding Mux B: Selects ALU input 2 source (similar to Mux A, but only when not using immediate)
4. ALU Operation Tables
ALU_OP Values Based on Instruction Type
| Instruction Type | Opcode | ALU_OP Value | Description |
|---|---|---|---|
| R-type | 0x33 | funct7|funct3 | Uses funct fields for operation |
| I-type arithmetic | 0x13 | 000|funct3 | Similar to R-type but with immediate |
| Load | 0x03 | 000|000 | Always ADD for address calculation |
| Store | 0x23 | 000|000 | Always ADD for address calculation |
| Branch | 0x63 | 001|funct3 | Comparison operations |
ALU_Control Values Based on Instruction
| ALU_Control | Value | Operation | Instructions |
|---|---|---|---|
| ALU_ADD | 0x0 | Addition | ADD, ADDI, loads, stores |
| ALU_SUB | 0x2 | Subtraction | SUB |
| ALU_SLL | 0x3 | Shift Left Logical | SLL, SLLI |
| ALU_SLT | 0x5 | Set Less Than | SLT, SLTI |
| ALU_XOR | 0x6 | Exclusive OR | XOR, XORI |
| ALU_SRL | 0x7 | Shift Right Logical | SRL, SRLI |
| ALU_SRA | 0x8 | Shift Right Arithmetic | SRA, SRAI |
| ALU_OR | 0x9 | Logical OR | OR, ORI |
| ALU_AND | 0xC | Logical AND | AND, ANDI |
| ALU_MUL | 0x1 | Multiply | MUL |
| ALU_MULH | 0x4 | Multiply High | MULH |
| ALU_DIV | 0xA | Division | DIV |
| ALU_REM | 0xB | Remainder | REM |
5. Branch Generation Logic
The gen_branch() function implements the branch condition evaluation logic:
bool gen_branch(idex_reg_t idex_reg)
{
if (!idex_reg.branch) return false;
uint32_t v1 = idex_reg.rs1_val; uint32_t v2 = idex_reg.rs2_val; uint8_t funct3 = idex_reg.instr.sbtype.funct3;
switch (funct3) { case 0x0: return v1 == v2; // BEQ case 0x1: return v1 != v2; // BNE case 0x4: return (int32_t)v1 < (int32_t)v2; // BLT case 0x5: return (int32_t)v1 >= (int32_t)v2; // BGE case 0x6: return v1 < v2; // BLTU case 0x7: return v1 >= v2; // BGEU default: return false; }} Logic Behind Implementation:
- Input Validation: First checks if the instruction is actually a branch (
idex_reg.branch) - Operand Extraction: Gets the two register values to compare (
rs1_valandrs2_val) - Operation Decoding: Uses
funct3field to determine the branch condition type - Condition Evaluation: Performs the appropriate comparison:
- BEQ/BNE: Simple equality/inequality comparison
- BLT/BGE: Signed comparison using type casting to
int32_t - BLTU/BGEU: Unsigned comparison using direct
uint32_tvalues
- Result: Returns boolean indicating whether branch should be taken
The function handles both signed and unsigned comparisons correctly by using appropriate type casting, ensuring that negative numbers are handled properly in signed comparisons while maintaining correct behavior for unsigned operations.
Milestone 2
Overview
Milestone 2 extends the basic pipeline with advanced features to handle data hazards and control hazards through:
- Data forwarding mechanisms
- Pipeline stalling for load-use hazards
- Pipeline flushing for control hazards
- Branch prediction and resolution
MS2 Implementation Details
1. EX-Hazard Detection and Forwarding
An EX hazard occurs when an instruction in the execute (EX) stage needs a result from a previous instruction that is still in the execute stage. This is a data dependency that can be resolved by forwarding the result from the ALU of the previous instruction to the current one.
Condition for EX-Hazard Detection:
The condition is implemented in the gen_forward() function in stage_helpers.h:
void gen_forward(pipeline_regs_t* pregs_p, pipeline_wires_t* pwires_p)
{
idex_reg_t idex = pregs_p->idex_preg.out; exmem_reg_t exmem = pregs_p->exmem_preg.out; memwb_reg_t memwb = pregs_p->memwb_preg.out;
// EX hazard (EX/MEM -> ID/EX) if (exmem.reg_write && exmem.rd != 0) { if (uses_rs1 && exmem.rd == idex_rs1) { pwires_p->forwardA = 1; printf("[FWD]: Resolving EX hazard on rs1: x%u\n", exmem.rd); fwd_exex_counter++; } if (uses_rs2 && exmem.rd == idex_rs2) { pwires_p->forwardB = 1; printf("[FWD]: Resolving EX hazard on rs2: x%u\n", exmem.rd); fwd_exex_counter++; } }} EX-Hazard Conditions:
- The instruction in EX/MEM stage must write to a register (
exmem.reg_write == true) - The destination register must not be x0 (
exmem.rd != 0) - The destination register must match a source register of the current instruction (
exmem.rd == idex_rs1orexmem.rd == idex_rs2) - The current instruction must actually use the source register (
uses_rs1oruses_rs2)
Forwarding Implementation:
Forwarding is resolved by setting forwardA = 1 or forwardB = 1. In the execute stage, these signals control multiplexers:
// Handle forwarding for ALU input 1 (rs1)
if (pwires_p->forwardA == 1) {
// Forward from EX/MEM stage alu_inp1 = pregs_p->exmem_preg.out.alu_result;} 2. MEM-Hazard Detection and Forwarding
A MEM hazard occurs when an instruction in the execute stage needs a result from an instruction that is currently in the memory (MEM) stage or has completed memory access.
Condition for MEM-Hazard Detection:
// MEM hazard (MEM/WB -> ID/EX), only if EX didn't already take it
if (memwb.reg_write && memwb.rd != 0) {
if (uses_rs1 && pwires_p->forwardA == 0 && memwb.rd == idex_rs1) { pwires_p->forwardA = 2; printf("[FWD]: Resolving MEM hazard on rs1: x%u\n", memwb.rd); fwd_exmem_counter++; } if (uses_rs2 && pwires_p->forwardB == 0 && memwb.rd == idex_rs2) { pwires_p->forwardB = 2; printf("[FWD]: Resolving MEM hazard on rs2: x%u\n", memwb.rd); fwd_exmem_counter++; }} MEM-Hazard Conditions:
- The instruction in MEM/WB stage must write to a register (
memwb.reg_write == true) - The destination register must not be x0 (
memwb.rd != 0) - No EX hazard is already being forwarded (
pwires_p->forwardA == 0) - The destination register must match a source register (
memwb.rd == idex_rs1ormemwb.rd == idex_rs2) - The current instruction must use the source register
Forwarding Implementation:
MEM hazards set forwardA = 2 or forwardB = 2. The forwarded data comes from either ALU result or memory data:
// Forward from MEM/WB stage
uint32_t memwb_result = pregs_p->memwb_preg.out.mem_to_reg ?
pregs_p->memwb_preg.out.mem_data :
pregs_p->memwb_preg.out.alu_result;
alu_inp1 = memwb_result; 3. Load-Use Hazard Detection
A load-use hazard is a specific data hazard where an instruction needs the result of a load instruction that hasn’t completed the memory stage yet. This cannot be resolved by forwarding and requires a pipeline stall.
Condition for Load-Use Hazard Detection:
The detection is implemented in the detect_hazard() function:
void detect_hazard(pipeline_regs_t* pregs_p, pipeline_wires_t* pwires_p, regfile_t* regfile_p)
{
idex_reg_t idex = pregs_p->idex_preg.out; ifid_reg_t ifid = pregs_p->ifid_preg.out;
// Load-use hazard: ID/EX is a load, and its destination is used in the following instruction if (idex.instr.opcode == 0x03) { // load opcode uint32_t load_rd = get_rd(idex.instr); uint32_t next_rs1 = uses_rs1 ? get_rs1(ifid.instr) : 0xFFFFFFFFu; uint32_t next_rs2 = uses_rs2 ? get_rs2(ifid.instr) : 0xFFFFFFFFu; if (load_rd != 0 && ((uses_rs1 && load_rd == next_rs1) || (uses_rs2 && load_rd == next_rs2))) {
pwires_p->stall = 1; stall_counter++; printf("[HZD]: Stalling and rewriting PC: 0x%08x\n", regfile_p->PC); } }} Load-Use Hazard Conditions:
- Current instruction in ID/EX must be a load (
idex.instr.opcode == 0x03) - Load destination register must not be x0 (
load_rd != 0) - The next instruction (in IF/ID) must use the load’s destination register as a source
- The next instruction must actually read from that source register (
uses_rs1oruses_rs2)
4. Pipeline Flush Implementation (NOP Insertion)
Pipeline flush is used to handle control hazards from branches and jumps. When a branch is taken, subsequent instructions that were fetched must be discarded.
Flush Implementation:
void cycle_pipeline(...)
{
// Handle pipeline flush after MEM stage sets the flush wire if (pwires_p->flush) { // Prefer flush over stall (kill younger instructions) pwires_p->stall = 0;
// Squash EX, ID, IF (preserve PCs for trace) pregs_p->exmem_preg.inp = make_exmem_nop(pregs_p->exmem_preg.inp.instr_addr); pregs_p->idex_preg.inp = make_idex_nop (pregs_p->idex_preg.inp.instr_addr); pregs_p->ifid_preg.inp = make_ifid_nop (pregs_p->ifid_preg.inp.instr_addr); } if (pwires_p->flush) {
printf("[CPL]: Pipeline Flushed\n"); }} NOP Creation Functions:
static inline idex_reg_t make_idex_nop(uint32_t pc) {
idex_reg_t r = {0}; r.instr.bits = 0x00000013; // ADDI x0,x0,0 r.instr_addr = pc; // keep PC for trace return r;} Flush Mechanism:
- Trigger: Set when a branch is taken in the MEM stage
- Scope: Flushes IF/ID, ID/EX, and EX/MEM pipeline registers
- Implementation: Replaces instructions with NOPs (0x00000013 =
addi x0, x0, 0) - PC Preservation: Maintains instruction addresses for debugging/tracing
5. Bubble Insertion for Pipeline Stall
Pipeline stalls are implemented by inserting bubbles (NOPs) while preventing PC advancement, effectively “pausing” the pipeline for one cycle.
Stall Implementation:
if (pwires_p->stall) {
pregs_p->ifid_preg.inp = pregs_p->ifid_preg.out; pregs_p->idex_preg.inp = make_idex_nop(pregs_p->idex_preg.out.instr_addr);} PC Control During Stall:
// In cycle_pipeline()
if (!pwires_p->stall) {
regfile_p->PC = pwires_p->next_pc;} Stall Mechanism:
- IF/ID Register: Keeps the same instruction (
pregs_p->ifid_preg.out) - ID/EX Register: Inserts a NOP bubble
- PC Update: PC is not advanced when
stallis asserted - Duration: Typically one cycle for load-use hazards
6. Additional Pipeline Changes for MS2
1. Branch Target and Taken Branch Fields:
Added to exmem_reg_t to support branch resolution timing:
typedef struct {
// ... existing fields ... uint8_t is_taken_branch; uint32_t branch_target;} exmem_reg_t; 2. Enhanced Branch Resolution:
Branches are now resolved in the execute stage with proper forwarding:
// Apply forwarding to branch inputs
uint32_t branch_rs1 = idex_reg.rs1_val;
uint32_t branch_rs2 = idex_reg.rs2_val;
if (pwires_p->forwardA == 1) {
branch_rs1 = pregs_p->exmem_preg.out.alu_result;} // ... similar for other forwarding cases
// Create a temporary copy of idex_reg with forwarded values for branch evaluation
idex_reg_t branch_reg = idex_reg;
branch_reg.rs1_val = branch_rs1;
branch_reg.rs2_val = branch_rs2;
bool take = gen_branch(branch_reg) || idex_reg.jal || idex_reg.jalr; 3. Instruction Type Detection:
Added helper logic to determine which instructions use which source registers:
uint8_t op = idex.instr.opcode;
bool uses_rs1 = (op==0x33)||(op==0x13)||(op==0x03)||(op==0x23)||(op==0x63)||(op==0x67);
bool uses_rs2 = (op==0x33)||(op==0x23)||(op==0x63); 4. Flush Timing Control:
Moved branch/jump redirection to MEM stage for proper timing:
// In stage_mem()
if (exmem_reg.is_taken_branch) {
pwires_p->pcsrc = 1; pwires_p->pc_src1 = exmem_reg.branch_target; pwires_p->flush = 1; // bubbles go into IF/ID for the *next* cycle} Reasons for MS2 Changes:
- Timing Accuracy: Branch resolution moved to match real pipeline timing
- Forwarding Support: Branch comparisons now support forwarded values
- Hazard Resolution: Proper detection of which instructions use which registers
- Performance: Minimizes pipeline stalls through effective forwarding
- Correctness: Ensures proper instruction sequencing during hazards
Project Summary
This RISC-V pipeline simulator successfully implements:
MS1 Achievements:
- Complete 5-stage pipeline with proper control signal generation
- Correct ALU operations including M-extension (multiply, divide)
- Branch and jump instruction support
- Memory operations (loads and stores)
- Proper PC management and instruction flow
MS2 Achievements:
- Comprehensive hazard detection (EX, MEM, load-use)
- Data forwarding mechanisms to minimize stalls
- Pipeline stalling for unavoidable hazards
- Pipeline flushing for control hazards
- Enhanced branch resolution with forwarding support
The implementation passes all required test cases and correctly handles the complex interactions between instructions in a pipelined processor.