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