- My Contributions
- Mistakes I've made
- Special Design Decisions and test results
- What I've Learned in This Project
- Future Improvements
My contributions focus on the PC module, F1 assembly code, Pipelining, Hazard Unit and Testing for F1 program & Reference program on Vbuddy.
This module is central to controlling the program counter (PC) in the architecture.
Relavent commits:
(pc, pc_offset, pc_plus)
(top_fetch)
(Pipelined top_fetch)
| Control Signal | pc_next Value | Description |
|---|---|---|
PCSrc = 0 |
PCPlus4 |
Select next sequential address. |
PCSrc = 1 |
PCTarget |
Selects address for branch or jump. |
Inputs to note:
PCSrc: I used control signal to select the next value of thepc_next. When there is a branch or jump instruction, it setsPCSrc = 1, which determines that next pc address is jump address or branch target address. Whenbranch_neg = 0, it is applicable forBEQinstruction, where the branch condition is met only ifbranch_l = 1andBranch = 1. Whenbranch_neg = 1, it is applicable forBNEinstruction, where the branch condition is met only ifbranch_l = 0andBranch = 1. The logic is described as follows:PCSrc = (Branch && (branch_l ^ branch_neg)) || Jump;PCTarget: I usedPcOpto either selectrd1+ ImmExtas target address for instruction likeJALR, whichrd1provides the base adress orpc + ImmExtfor branch instructions. Thus, it determines the address of the instruction to be executed when the program performs a jump or branch. The logic is described as follows:PCTarget = PcOp ? rd1 + ImmExt : pc + ImmExt;
Relavent commits: (F1 assembly code)
In the F1 program, I designed an algorithm structured as Initialisation - Generation Light Up - Countdown. The process is as follows:
- In the
Initsection, vairables are established, including the fixed countdown number. - During the
mainloop, a 7-bit LFSR(initialized at 0x7F) is generated for random delay. - In the
light_uploop, the output registera0incrementally increases from0000_0000to0000_0001, and ultimately to1111_1111. Following each increment, the program enters thelightdelayloop to perform a fixed-number countdown. - Upon completion of the countdown in
final_random, the program returns to therstsection to reinitialize all values.
The algorithm is in the diagram below, with white straight lines illustrating unconditional jumps, and blue curves indicating conditional branches.
Relavent commits: (stage 1)
(stage1-4)
(stage 3 fix signal)
Pipelining improves a processor's performance by allowing multiple instructions to overlap in execution. We divided the single-cycle processor into five stages, enabling simultaneous execution of different parts:
-
F: Fetch Instruction
-
D: Decode
-
E: Execute ALU
-
M: Memory Read and Write
-
W: Write Register
-
Pipeline registersare used to separate each stage. Thus, I built registers for each pipeline, which stores the output of the current stage and serves as the input to the next stage and operates synchronously with the clock.InstrD,pc_next, andPC_PlusDinFetch_regisolate fetch-stage data before passing it to the decode stage.RegWriteE,ALUctrlE, andRD1EinDecode_regisolate control signals and register data for the execute stage.
-
Control signals(e.g.RegWrite,MemWrite) are generated in the decode stage and passed through the pipeline to ensure instruction execution in later stages. So, I propagate control signals by:RegWriteE(Decode_reg → Execute_reg) andRegWriteM(Execute_reg → Memory_reg) ensure the write-back behavior is preserved.ResultSrcEandResultSrcMdictate the data source (ALU result, memory data, or PC).
The hazard unit manages data and control hazards in a pipelined processor to ensure correct execution.
Relavent commits: (Hazard_unit)
Hazard unit deal with 3 situations that leads to error output in pipelined processor.
-
- Data hazards are addressed by redirecting data from later pipeline stages to earlier ones. Instead of waiting for a result written back to a register, I desinged the logic which makes the result forward to where it's needed:
if (((Rs1E == RdM) & RegWriteM) & (Rs1E != 0)) ForwardAE = 2'b10; // Forward from Memory_stage
else if(((Rs1E == RdW) & RegWriteW) & (Rs1E != 0)) ForwardAE = 2'b01// Forward from Writeback_stage
else ForwardAE = 2'b00; // No forwarding (use Register file output)
-
- Another data hazard occurs when a subsequent instruction depends on data from a previous instruction that has not been produced yet. To support stalls, I wrote the logic below which when the current instruction is a Load instruction (indicated by
ResultSrcE0) and the next instruction's source registers (Rs1D or Rs2D) depend on the destination register (RdE) of the Load instruction, then it sets thelwStallsignal. This signal is then assigned toStallFandStallD, which are used to stall the Fetch and Decode stages in pipelining. This also explained why I added inputsStallFto the Fetch and Decode pipeline registers:
- Another data hazard occurs when a subsequent instruction depends on data from a previous instruction that has not been produced yet. To support stalls, I wrote the logic below which when the current instruction is a Load instruction (indicated by
//stall
assign lwStall = ResultSrcE0 & ((Rs1D == RdE)|(Rs2D == RdE));
assign StallF = lwStall;
assign StallD = lwStall;-
- Control hazards arise from branch instructions when the branch outcome is uncertain until the execution stage. I designed the logic below which when a branch or jump instruction is resolved (
PCSrcEis high), the current instruction in the Decode stage becomes invalid and is cleared usingFlushD. Similarly, when a data hazard occurs(lwStallis high) or a branch/jump occurs (PCSrcEis high), the instruction in the Execute stage becomes invalid and is cleared usingFlushE. Thus, in pipeline control, I added inputsFlushDandFlushEsignals to clear invalid instructions in the Decode and Execute stages.
- Control hazards arise from branch instructions when the branch outcome is uncertain until the execution stage. I designed the logic below which when a branch or jump instruction is resolved (
//flush
assign FlushD = PCSrcE;
assign FlushE = lwStall | PCSrcE;- In the initial design, the program counter (
pc_module) was divided into overly granular components likepc_plusandpc_mux. While this approach clarified individual functionalities, it significantly complicated integration, making system connectivity cumbersome and less efficient.
- In the F1 program, I initially adopted the algorithm from reference program which uses
loadandstoreto generate random numbers. I expect it to pre-generate all random numbers and storing them in the data memory. Thus, the program could map thePCvalue to a specific location in the data memory to read the random number. However, I found thenPCwould be a constant value at the point where a random number to be loaded. In this case, this approach would transform the random number into a constant, which doesn't make sense. Finally, I designed a method which links to the way of pseudo-random number generation, which is Before thetriggersignal is asserted, the program iterates through a loop generating random numbers. Oncetriggeris asserted, a random number is generated. In this way, no data being written to or read from the Data Memory.
- In the
regfilemodule, I forgot to connect the trigger signal to register 5 (registers[5]), which results in no output on output register. This was found when I observe the test result from Gtkwave, there is always0in output (registers[10]). So, I added this line for representation of trigger signal inreg_file. Furthermore, In physical testing, the push-button switch on the Vbuddy functions as thetrigger.
registers[5] <= {31'b0, Trigger};
top->trigger = vbdFlag();
- In Pipelining design, since we restructured the cpu after single cycle processor,
Inst_memhas to be in fetch stage andreg_filesin decode stage. But I designed pipelining which matches to the old control unit. So, I mistaknly putreg_filesin fetch stage, which make the Fetch stage does not know the opcode and register addresses of the current instruction, so it cannot accurately read the registers.
- I tried to use the input
clrandento achieve the function ofStallandFlushin pipeling. However, both therstsignal and theclrsignal can reset the registers to zero, since I didn't clearly defined their priority, which led to confusion. Besides, the clearing logic ofclrmay override the holding logic ofen. Later, We changed it to inputStallFandFlushF, which makes sure that two signals do not interfere with each other.
if (!StallF) begin
if (FlushF) begin
instrD <= 0;
pcD <= 0;
PCPlus4D <= 0;
end
- In data forwarding logic, I omitted to check for whether Rs1E is 0, which result in incorrect data forwarding. Specifically,when
Rs1Eis register 0 (R0), the logic might mistakenly trigger data forwarding, leading to invalid data being fetched from the Memory or Writeback stage. This invalid data could overwrite the correct value of the "zero register" (which is always 0), causing logical errors in the program.
& (Rs1E != 0)
- Each component, such as PC or Control Unit, has its functionality encapsulated in its own top module.
- Advantages: Modules focus on single tasks (e.g. PC updates program counter, Control Unit generates signals), making the design easier to understand and test.
- In traditional instruction fetch stages, the address calculations for sequential execution and branch/jump operations are handled by separate units, namely
pc_plusandpc_offset. So, I used Top-Fetch module, which combines their functionality into a single, streamlined design using a (mux).
mux pc_sel(
.in0 (PCPlus4),
.in1 (PCTarget),
.sel (PCSrc),
.out (pc_next)
);
- We split the Control Unit into Main Decoder, the ALU Decoder. The Main Decoder is responsible for decoding the opcode, generating basic global control signals such as register read/write, memory access, and branch signals, and passing ALU-related information to the ALU Decoder. The ALU Decoder further decodes the opcode and function code to generate signals that control the ALU operations. The top-level Control Unit integrates and coordinates the outputs of both decoders, ensuring collaboration between modules and fulfilling the requirements of the pipelined architecture.
- We thought that we should test each module independently before integrating them to avoid potential errors. Thus, we created our own testbenches to test each module and eventually used the testbench provided to test the entire processor. (tb_unit)
Relavent commits: (F1 test)
- I made the testbench for F1 code on Vbuddy:
// Initialize top Verilog instance
Vtop *top = new Vtop;
// a0 is the output from F1 assembly code
vbdBar(top->a0 & 0xFF);
- Manually tested result on Gtkwave:
- Test result from Vbuddy
Single cycle F1:
Pipelined_F1: 
Relavent commits: (ref test)
- I built a testbench for Reference Program on vbuddy.
- Special designs 1: When analyzing the reference code for the probability density function, I noticed an interesting behavior of
vbuddy. The outputa0becomes valid only after cleaning, building, and displaying data, meaning the initial output might be zero for multiple cycles. To improve efficiency, I optimized thetestbenchby plotting graphs only whena0 = 1, significantly reducing overhead.
if (plot == 0 && top->a0 != 0) {
plot = 1;
}
- Special designs 2: The final loop generating the PDF graph often repeats itself. To resolve this, I added a maximum plotting limit in the
testbench, ensuring only one complete and meaningful graph is displayed. This prevents redundant visual outputs, and the limit dynamically adjusts based on different memory contents.
if (plot > 1920) {
break;
}
Through the process of building the processor during this project, I have gained a substantial practical understanding to RISC-V. This coursework has been instrumental in deepening my comprehension of the various modules within the CPU and their interrelations. I learned to use a lot of tools like Git, which is essential for version control and managing repository structure. I got more opportunities to write SystemVerilog language and use Verilator. Additionally, I am more familiar with sequential and combinational logic, and I learned how to use Gtest combined with test on Vbuddy to fully prove the functionality. If I have time, I would keep an detailed logbook which records what I did and how I did it, it also serves as a reference for reporting.
In future works, I would add more pipelining stages by spitting existing stages(decode_reg, execute_reg) into finer sub-stages, which allow higher clock speeds and make execution of inistructions faster. Secondly, I would have liked to work on the data cache and try to apply state machine on cache design. Futhermore, I am also eager to integrate multiple cores within the processor, allowing each core to execute tasks independently. To conclude, I enjoyed the time I spent working with my team on the project and I feel satisfied with what we achieved and learned from it.

