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! 🚀