DEV Community

olayiwolaosho
olayiwolaosho

Posted on

Part 3 — Building the Control Unit: The Brain of the Datapath

This is the third component in our emulator build. Like the ALU, the Control Unit is part of the Execute stage in the classic 5-stage MIPS instruction flow:

Fetch → Decode → Execute → Memory → Write Back

We're still working with the single-cycle processor model, which isn’t used in modern CPUs due to performance limitations, but it's incredibly useful for understanding how pipelined designs work later on. every instruction completes in one clock cycle. This means all components—memory, ALU, registers—must be ready to do their job in the same moment.


What Does the Control Unit Do?

The Control Unit takes the 6-bit opcode (bits [31–26] of the instruction) and uses it to generate 9 control signals. These signals determine:

  • Which data paths should be enabled or blocked
  • Which parts of the datapath (register file, ALU, memory, etc.) should be active
  • What kind of operation the ALU should perform

it basically orchestrates the data flow across the CPU.


Understanding the Control Logic

All instructions (R-type, lw, sw, beq, etc.) are fetched in the same way. After fetch, we extract common fields from the 32-bit instruction:

  • [31–26]: opcode
  • [25–21]: rs
  • [20–16]: rt
  • [15–11]: rd
  • [15–0]: immediate

Although instruction formats differ, we parse all instructions into these common slices, and rely on the Control Unit to make sense of what parts matter.


Example: RegDst for R-type vs lw

In an R-type instruction like add $t1, $t2, $t3, we are writing the result to the rd field ([15–11]). In contrast, for a lw instruction, the destination register is in the rt field ([20–16]).

So how do we know which register field to use?

This is where the control signal RegDst comes in:

  • RegDst = 1 → use [15–11] (rd) → for R-type
  • RegDst = 0 → use [20–16] (rt) → for lw

The control unit activates the correct data path using this signal.


Another Example: ALUSrc for R-type vs lw

The ALU takes two operands. But the source of the second operand depends on the instruction type:

  • R-type → both operands come from registers
  • lw, sw → second operand comes from the immediate field

To handle this, the control signal ALUSrc determines the ALU input source:

  • ALUSrc = 0 → second ALU input comes from rt register
  • ALUSrc = 1 → second ALU input comes from the sign-extended immediate

This lets us reuse the same ALU circuit for both instruction types, just by rerouting the inputs.


Image description
image from the book computer organisation and design detailing the R-type instructions flow

Control Signal Summary

Here are the 9 control signals and what they control:

Signal Purpose
RegDst Selects between rd and rt for write reg
ALUSrc Selects ALU second input (reg vs immediate)
MemtoReg Selects memory output or ALU result to write
RegWrite Enables register write
MemRead Enables memory read
MemWrite Enables memory write
Branch Enables PC update on beq
ALUOp1/0 Encodes ALU operation intent

The Code

Here's the full control unit code:

#include <stdint.h>

// Opcodes
#define R_FORMAT 0b000000
#define LW       0b100011
#define SW       0b101011
#define BEQ      0b000100

// Control values
#define ZERO     0b01
#define ONE      0b11
#define DONT_CARE 0b00

// Control signals stored here
static struct Control_Signals {
    uint8_t RegDst;
    uint8_t ALUSrc;
    uint8_t MemtoReg;
    uint8_t RegWrite;
    uint8_t MemRead;
    uint8_t MemWrite;
    uint8_t Branch;
    uint8_t ALUOp1;
    uint8_t ALUOp0;
} control_signals;

// R-type instruction setup
static void set_r_format() {
    control_signals.RegDst   = ONE;
    control_signals.ALUSrc   = ZERO;
    control_signals.MemtoReg = ZERO;
    control_signals.RegWrite = ONE;
    control_signals.MemRead  = ZERO;
    control_signals.MemWrite = ZERO;
    control_signals.Branch   = ZERO;
    control_signals.ALUOp1   = ONE;
    control_signals.ALUOp0   = ZERO;
}

// Load Word
static void set_lw() {
    control_signals.RegDst   = ZERO;
    control_signals.ALUSrc   = ONE;
    control_signals.MemtoReg = ONE;
    control_signals.RegWrite = ONE;
    control_signals.MemRead  = ONE;
    control_signals.MemWrite = ZERO;
    control_signals.Branch   = ZERO;
    control_signals.ALUOp1   = ZERO;
    control_signals.ALUOp0   = ZERO;
}

// Store Word
static void set_sw() {
    control_signals.RegDst   = DONT_CARE;
    control_signals.ALUSrc   = ONE;
    control_signals.MemtoReg = DONT_CARE;
    control_signals.RegWrite = ZERO;
    control_signals.MemRead  = ZERO;
    control_signals.MemWrite = ONE;
    control_signals.Branch   = ZERO;
    control_signals.ALUOp1   = ZERO;
    control_signals.ALUOp0   = ZERO;
}

// Branch if Equal
static void set_beq() {
    control_signals.RegDst   = DONT_CARE;
    control_signals.ALUSrc   = ZERO;
    control_signals.MemtoReg = DONT_CARE;
    control_signals.RegWrite = ZERO;
    control_signals.MemRead  = ZERO;
    control_signals.MemWrite = ZERO;
    control_signals.Branch   = ONE;
    control_signals.ALUOp1   = ZERO;
    control_signals.ALUOp0   = ONE;
}

// Main control function
void control_unit(int8_t op_code) {
    switch (op_code) {
        case R_FORMAT: set_r_format(); break;
        case LW:       set_lw();       break;
        case SW:       set_sw();       break;
        case BEQ:      set_beq();      break;
        default:
            // Leave as is for unsupported ops
            break;
    }
}

// Accessor functions
uint8_t get_RegDst()     { return control_signals.RegDst; }
uint8_t get_ALUSrc()     { return control_signals.ALUSrc; }
uint8_t get_MemtoReg()   { return control_signals.MemtoReg; }
uint8_t get_RegWrite()   { return control_signals.RegWrite; }
uint8_t get_MemRead()    { return control_signals.MemRead; }
uint8_t get_MemWrite()   { return control_signals.MemWrite; }
uint8_t get_Branch()     { return control_signals.Branch; }
uint8_t get_ALUOp1()     { return control_signals.ALUOp1; }
uint8_t get_ALUOp0()     { return control_signals.ALUOp0; }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)