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) → forlw
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 fromrtregister -
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 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; }
Top comments (0)