In the previous post, we explored the von Neumann
architecture, which stores programs as data in memory. We learned that a binary
number like 01100011 can mean store 3 in R0. That was the concept of an
Instruction Set Architecture (ISA) — a contract between the CPU designer and
the programmer.
But we haven’t actually seen the list of instructions yet. What specific instructions can we give the CPU? In this post, let’s walk through the ten instructions in this series’ 8-bit ISA one by one.
Just as a cook takes ingredients from the fridge and works on the counter, a CPU pulls data from memory and places it nearby. That counter is a register.
Why not work directly from memory? Memory is large but physically distant from the CPU, making access slow. Registers live inside the CPU, so access is extremely fast — but they’re expensive, so there are only a few. This series’ ISA has four registers: R0, R1, R2, R3.
There are four instructions for moving data between registers and memory:
LOAD Addr — Read a value from a memory address into R0STORE Addr — Write R0’s value to a memory addressLDI Rd, Imm — Load an immediate value directly into a registerMOVE Rd, Rs — Copy a value between registersLOAD and STORE only transfer values between memory and R0. To put a value into another register, use MOVE to copy from R0, or use LDI (Load Immediate) to put a value directly into any register. Here, Immediate means a number embedded in the instruction itself — no need to go to memory, the value is right there in the instruction.
We can move data around now. But moving alone is meaningless. What can we actually do with the data?
Earlier in the series, we combined logic gates to build an adder. The device containing that adder is the ALU (Arithmetic Logic Unit). The ALU handles arithmetic operations like addition and subtraction.
There are four arithmetic instructions that use the ALU:
ADD Rd, Rs — Add Rd and Rs, store the result in RdSUB Rd, Rs — Subtract Rs from Rd, store in RdADDI Rd, Imm — Add an immediate value to RdSUBI Rd, Imm — Subtract an immediate value from RdHere Rd is the destination register where the result is stored, and Rs is the source register to read from. ADDI and SUBI are versions that use an immediate value instead of a register.
The Zero Flag is a 1-bit indicator that records whether the last operation’s result was zero. Why do we need it? Because it answers the question are these two values equal? Subtract two register values — if the result is 0, the values are the same. This indicator plays a decisive role in the control-flow instructions coming next.
With only the instructions so far, a program runs top to bottom, one line at a time. But real programs aren’t like that. They need flow control like go this way if the condition is met (if) and repeat until true (while).
Two instructions handle this:
JUMP Addr — Change the next instruction’s address to the specified addressJZ Addr — Jump only if the last operation’s result was 0JUMP unconditionally moves to the specified address. For example, putting
JUMP 0 at the end of a program loops back to the start, repeating the same
code forever.
But infinite loops alone aren’t enough. To express stop repeating when a certain condition is met, we need a conditional jump. That’s JZ (Jump if Zero). JZ jumps only when the Zero Flag is 1 — that is, when the last operation’s result was 0.
Here’s a concrete example. Consider a countdown program that subtracts 1 from R0 and stops when it hits 0:
Addr 0: SUBI R0, 1 ← Subtract 1 from R0
Addr 1: JZ 3 ← If result is 0, jump to address 3
Addr 2: JUMP 0 ← Otherwise, go back to address 0
Addr 3: (next task)
If R0 was 3, SUBI runs three times until the result hits 0, JZ fires, and the loop ends. JUMP handles the repetition and JZ handles the exit condition. With just these two instructions, we can express both loops and conditional branches.
Here is a complete list of the instructions we’ve covered. Click each one to examine its structure and behavior.
Copies the value of Rs to Rd.
Four data-movement instructions, four arithmetic instructions, and two control-flow instructions. Only ten in total. That may seem few, but they’re enough to build programs like countdowns, multiplication of two numbers, and conditional branching. Try them out in the simulator we’ll build in the next post.
We’ve learned the language for talking to a CPU. Moving data, performing arithmetic, and controlling flow — these three categories of instructions are everything a CPU understands.
But knowing the instructions doesn’t mean we know how the CPU works internally. How does the CPU take these instructions lined up in memory and execute them one by one? Let’s follow that process in the next post.