Silicon From Scratch

    The Control Unit

    A control unit is the CPU’s decision-maker. It interprets each instruction and generates the control signals needed to execute it. These signals tell the processor’s components what to do and when to do it. Without the control unit, the CPU is nothing but a tangle of wires.

    Think you got this already? Skip to Check Yourself
    The single-cycle datapath drawn in gray with the control unit and its output signals highlighted in blue: Branch, MemRead, MemToReg, ALUOp, MemWrite, ALUSrc, and RegWrite fan out from the Control bubble, while an ALU Ctrl block sets the ALU operation.

    The ALU Control

    Before we can dive into how the main control unit works, it’s important that we have our ALU operating smoothly. Luckily for us, we’ve already constructed the ALU’s datapath, but for it to actually perform the tasks we want, we need to control it. While we could theoretically build a single control unit that decodes every instruction bit directly into ALU commands, it’s better to abstract the ALU’s logic away from the rest of the datapath so each control unit answers just one question, potentially making it easier to catch bugs in the future.

    The CPU datapath we built in the last lesson can handle a variety of instructions. Instead of asking the ALU Control Unit to decode an entire 32-bit instruction, the main control unit gives it a simple two-bit signal called ALUOp. This signal tells the ALU Control Unit what class of instruction it’s dealing with:

    A table mapping the two-bit ALUOp signal to the ALU's action: 00 for load/store address calculation (add), 01 for branch comparison (subtract), and 10 for arithmetic and logical instructions, whose exact operation is decoded from the funct3 and funct7 fields.
    • 00 → Memory instructions (lw/sw) always require an address calculation, so perform addition.
    • 01 → Branch instructions (beq/bne) compare two registers by subtracting them.
    • 10 → Arithmetic and logical instructions require a closer inspection of the instruction’s funct3 (and sometimes funct7) fields to determine the exact operation.
    Verilog HDL
    module alu_control (
        input  wire [2:0] funct3,
        input  wire       funct7Bit5,
        input  wire [6:0] opcode,
        input  wire [1:0] ALUOp,
        output reg  [3:0] ALUControl
    );
      wire isRtype;
      assign isRtype = (opcode == 7'b0110011);
    
      always @(*) begin
        case (ALUOp)
          2'b00:   ALUControl = 4'b0010;  // addition (lw/sw)
          2'b01:   ALUControl = 4'b0110;  // subtraction (beq/bne)
          2'b10: begin  // R-type and I-type decode (and blt)
            case (funct3)
              3'b000:  ALUControl = (funct7Bit5 && isRtype) ? 4'b0110 :  // sub
                                                              4'b0010;  // add/addi
              3'b111:  ALUControl = 4'b0000;  // and/andi
              3'b110:  ALUControl = 4'b0001;  // or/ori
              3'b010:  ALUControl = 4'b0111;  // slt
              3'b100:  ALUControl = 4'b0111;  // blt
              default: ALUControl = 4'b0000;
            endcase
          end
          default: ALUControl = 4'b0000;
        endcase
      end
    
    endmodule

    When ALUOp equals 10, the ALU Control Unit becomes a small decoder. It examines the instruction’s function fields and translates them into the four-bit control signal that drives the ALU. For example, a funct3 value of 111 selects a bitwise AND operation, 110 selects OR, 010 selects set-less-than, and 000 could mean either ADD or SUB. In that final case, the control unit checks bit 5 of funct7; if it’s an R-type instruction with that bit set, the ALU performs subtraction. Otherwise, it performs addition.

    This layered approach keeps responsibilities clean. The main control unit decides what kind of instruction is executing, while the ALU Control Unit decides which arithmetic or logical operation the ALU should perform. Each module stays small, focused, and much easier to test than one giant decoder.

    Go back Forgot how the ALU works? See how the ALUControl bits decide which operation it performs.

    The Main Control Unit

    In the last section we saw how the ALU learns which operation to perform. The main control classifies each instruction into a coarse ALUOp category, and a dedicated ALU control module refines that category into an exact operation code. But that raises a natural question: where does ALUOp come from in the first place, and who is steering everything else? After all, the ALU is only one piece of the datapath. Memory needs to know whether it is being read or written, the register file needs to know whether to save a result, and the multiplexers need to know which inputs to pass along.

    Verilog HDL
    module sc_cpu_control (
        input  wire [6:0] opcode,
        input  wire [2:0] funct3,
        output reg  [2:0] branch,
        output reg        memRead,
        output reg        memToReg,
        output reg  [1:0] ALUOp,
        output reg        memWrite,
        output reg        ALUsrc,
        output reg        regWrite
    );
    
      always @(*) begin
    
        branch   = 3'b000;
        memRead  = 1'b0;
        memToReg = 1'b0;
        ALUOp    = 2'b00;
        memWrite = 1'b0;
        ALUsrc   = 1'b0;
        regWrite = 1'b0;
    
        case (opcode)
          7'b0000011: begin   // lw
            memRead = 1'b1;   // read word from memory
            memToReg = 1'b1;  // send word to register
            ALUOp = 2'b00;    // addition
            ALUsrc = 1'b1;    // use immediate as offset
            regWrite = 1'b1;  // write word to register
          end
          7'b0100011: begin  // sw
            memWrite = 1'b1; // write word to memory
            ALUOp = 2'b00;   // addition
            ALUsrc = 1'b1;   // use immediate as offset
          end
        endcase
      end
    
    endmodule
    The Verilog above shows how the lw and sw instructions are decoded in the control unit.

    The main control is the unit that answers all of these questions at once. By examining nothing more than the instruction’s opcode, it produces the complete set of signals that configure the datapath for a single instruction: memRead and memWrite for memory access, ALUsrc to choose between a register and the immediate, memToReg to choose what gets written back, regWrite to permit that writeback, ALUOp for the ALU, and branch for anything that might redirect the program counter.

    Conceptually, the main control works like a lookup table. It holds no state and performs no computation of its own. Given an instruction class, it simply recalls the recipe of signals that class requires and asserts them, leaving everything else quiet. A helpful way to think about it is that the datapath is a machine full of switches, and each instruction comes with a wiring diagram for which switches should be flipped. The main control is the component that reads the diagram and flips them. Because every signal starts from a safe, do-nothing baseline and is only raised when an instruction calls for it, no instruction can inherit stray behavior from another, and anything the unit does not recognize passes through harmlessly.

    A table listing each instruction type and the main control signals it asserts — memRead, memWrite, ALUsrc, memToReg, regWrite, ALUOp, and branch — showing which are set to 1 and which stay 0 for each instruction class.

    The Branch Logic

    In the table above, we hinted at some of the ideas behind the branch logic block in our datapath. It turns out this logic is actually quite simple, and doesn’t need more than a Verilog case statement to work.

    In this CPU, we want to be able to handle the beq, bne, and blt instructions, all of which use the same opcode, 1100011. Our control unit differentiates these instructions by taking a closer look at their funct3 bits. Together, the opcode and funct3 fields can uniquely identify which branch is being executed. Each instruction carries its own funct3 code: beq uses 000, bne uses 001, and blt uses 100.

    Since there are three different branch instructions, one simple way to encode which operation to perform is with a one-hot encoded signal. In our design, this signal is called branch, and it helps the datapath choose whether it should increment the PC by 4, or increment it by an immediate specified in a branch instruction.

    Each branch instruction lights up its own bit of branch, while every non-branch instruction leaves it at its default of 000 — telling the datapath to simply move on to the next instruction at PC + 4. When one of those bits is set, the datapath checks the matching condition against the ALU’s result, and if it holds, redirects the program counter by the branch’s immediate instead.

    Verilog HDL
      always @(*) begin
    
        branch   = 3'b000;
        case (opcode)
          7'b1100011: begin
            case (funct3)
              3'b000: begin      // beq
                branch = 3'b100;
                ALUOp  = 2'b01;  // subtraction
              end
              3'b001: begin      // bne
                branch = 3'b010;
                ALUOp  = 2'b01;  // subtraction
              end
              3'b100: begin      // blt
                branch = 3'b001;
                ALUOp  = 2'b10;  // later decoded to slt
              end
              default: branch = 3'b000;
            endcase
          end
        endcase
      end

    Check Yourself

    Given the one-hot coding for the branch signal above, what would be the correct Verilog assign statement for the Sel signal below?

    A multiplexer in the datapath whose select line, Sel, chooses whether the program counter advances by 4 or takes the branch target; its Sel input is driven by the branch one-hot signal together with the ALU's zero and lessThan status outputs.