Implementing the Control Unit of a RISC-V 32I Processor on FPGA
We continue our series on building a RISC-V 32I processor using the Tang Nano 20K FPGA. Today, we’ll discuss the Control Unit, a key component that brings our processor to life. We’ll explain it clearly and simply, then implement the code together in Verilog, detailing each step for clarity.
What Exactly is the Control Unit?
The Control Unit is like the conductor of an orchestra—it ensures all parts of the processor work in harmony. Its primary job is to read (decode) each instruction from memory, understand what it means, and then send the correct signals to other processor components to execute that instruction properly.
Its main functions are:
- Decoding instructions: Determines what type of instruction has arrived and how to execute it.
- Generating control signals: Activates or deactivates internal components like the ALU, memory, or registers.
- Synchronization: Coordinates timings to ensure orderly operation.
Step-by-Step Implementation of the Control Unit in Verilog
Now, let’s dive into the Verilog code, explaining each part in detail.
Module Definition
First, we declare the inputs and outputs for our control unit:
module ControlUnit (
input wire [6:0] opcode, // Identifies the instruction type
output reg RegWrite, // Allows writing to registers
output reg ALUSrc, // Selects the second operand for the ALU
output reg MemRead, // Reads data from memory
output reg MemWrite, // Writes data to memory
output reg MemtoReg, // Moves data from memory to register
output reg Branch, // Indicates conditional jump instruction
output reg Jump, // Indicates unconditional jump instruction
output reg [1:0] ALUOp // Specifies the type of ALU operation
);
Understanding those Binary Codes (Opcodes)
Clearly defining what each opcode does helps readability and maintainability:
localparam R_TYPE = 7'b0110011; // Arithmetic and logical operations
localparam I_LOAD = 7'b0000011; // Load data from memory
localparam I_TYPE = 7'b0010011; // Immediate value operations
localparam S_TYPE = 7'b0100011; // Store data in memory
localparam B_TYPE = 7'b1100011; // Conditional branches
localparam JAL = 7'b1101111; // Unconditional jump and link
localparam JALR = 7'b1100111; // Jump to register location
localparam LUI = 7'b0110111; // Load upper immediate
localparam AUIPC = 7'b0010111; // Add upper immediate to PC
The Core of the Module: Combinational Block
Here, the Control Unit makes decisions based on the received instruction:
always @(*) begin
// First, set default values to prevent errors
ALUSrc = 0;
MemRead = 0;
MemWrite = 0;
RegWrite = 0;
Branch = 0;
Jump = 0;
MemtoReg = 0;
ALUOp = 2'b00;
// Adjust signals according to the opcode
case(opcode)
R_TYPE: begin // Arithmetic and logical instructions
RegWrite = 1;
ALUSrc = 0;
ALUOp = 2'b10;
end
I_LOAD: begin // Load data from memory (e.g., LW)
RegWrite = 1;
ALUSrc = 1;
MemRead = 1;
MemtoReg = 1;
ALUOp = 2'b00;
end
I_TYPE: begin // Immediate operations (e.g., ADDI)
RegWrite = 1;
ALUSrc = 1;
ALUOp = 2'b11;
end
S_TYPE: begin // Store data in memory (e.g., SW)
MemWrite = 1;
ALUSrc = 1;
ALUOp = 2'b00;
end
B_TYPE: begin // Conditional branches (e.g., BEQ)
Branch = 1;
ALUOp = 2'b01;
end
JAL: begin // Unconditional jumps
Jump = 1;
RegWrite = 1;
end
JALR: begin // Jump to register location
Jump = 1;
ALUSrc = 1;
RegWrite = 1;
end
LUI: begin // Load upper immediate
RegWrite = 1;
end
AUIPC: begin // Add upper immediate to PC
RegWrite = 1;
ALUOp = 2'b00;
end
default: begin // By default, no operation
RegWrite = 0;
ALUSrc = 0;
MemRead = 0;
MemWrite = 0;
ALUOp = 2'b00;
end
endcase
end
Complete Code
Here is the complete and final Verilog code:
module control_unit(
input [6:0] opcode,
output reg ALUSrc,
output reg MemtoReg,
output reg RegWrite,
output reg MemRead,
output reg MemWrite,
output reg Branch,
output reg Jump,
output reg [1:0] ALUOp
);
// Definición de los opcodes RV32I
localparam R_TYPE = 7'b0110011;
localparam I_LOAD = 7'b0000011;
localparam I_ALU = 7'b0010011;
localparam S_TYPE = 7'b0100011;
localparam B_TYPE = 7'b1100011;
localparam JAL = 7'b1101111;
localparam JALR = 7'b1100111;
localparam LUI = 7'b0110111;
localparam AUIPC = 7'b0010111;
always @(*) begin
// Valores predeterminados (evita latch)
ALUSrc = 1'b0;
MemtoReg = 0;
RegWrite = 0;
MemRead = 0;
MemWrite = 0;
Branch = 0;
Jump = 0;
ALUOp = 2'b00;
case(opcode)
R_TYPE: begin // R-Type
ALUSrc = 0;
RegWrite = 1;
ALUOp = 2'b10;
end
I_ALU: begin // I-Type inmediato (addi, andi, ori, etc.)
ALUSrc = 1;
RegWrite = 1;
ALUOp = 2'b11;
end
I_LOAD: begin // Load (lw, lb, lh)
ALUSrc = 1;
MemRead = 1;
MemtoReg = 1;
RegWrite = 1;
ALUOp = 2'b00;
end
S_TYPE: begin // Store (sw, sb, sh)
ALUSrc = 1;
MemWrite = 1;
ALUOp = 2'b00;
end
B_TYPE: begin // Branch (beq, bne, etc.)
Branch = 1;
ALUOp = 2'b01;
end
JAL: begin // Jump and Link (JAL)
Jump = 1;
RegWrite = 1;
ALUOp = 2'b00;
end
JALR: begin // Jump and Link Register (JALR)
ALUSrc = 1;
Jump = 1;
RegWrite = 1;
ALUOp = 2'b00;
end
LUI: begin // Load Upper Immediate
RegWrite = 1;
ALUOp = 2'b00;
end
AUIPC: begin // Add Upper Immediate to PC
RegWrite = 1;
ALUOp = 2'b00;
end
default: begin // Instrucción no reconocida
ALUSrc = 0;
MemtoReg = 0;
RegWrite = 0;
MemRead = 0;
MemWrite = 0;
Branch = 0;
Jump = 0;
ALUOp = 2'b00;
end
endcase
end
endmodule
Conclusion: The Control Unit Making Magic
With this Control Unit, our RISC-V 32I processor can now interpret instructions and perform coordinated, precise tasks. This module is essential to bring our system to life and ensure correct operation.
In the next article, we’ll cover the Arithmetic Logic Unit (ALU), responsible for performing calculations and logical operations. Stay tuned and let’s keep building together! 🚀