Best on a larger screen — the animation and controls have more room to breathe.
Inputs

Instruction Execution

Instruction execution is the loop a CPU runs and never leaves: read the word at the address in the program counter, add one to the program counter, cut the word into fields, and do whatever the opcode field says. Then do it again.

Four stages — fetch, decode, execute, write back — in that order every time, with any stage the current opcode has no use for skipped, repeated a few billion times a second and with no memory of having done it before.

The part that takes a while to accept is that the CPU has no idea a program exists. It cannot see functions, loops, or the end of your code. It has an address, and it runs whatever is at that address. Everything structural you have ever written is an arrangement of words in memory that happens to make that dumb loop trace out the shape you wanted.

How Instruction Execution Works

The machine on screen is TinyCPU: four registers, one 16-bit word per instruction, and a memory that holds instructions and data in the same column because there is nowhere else to put them. Every word is cut at the same three places:

op takes the top four bits and picks the operation. rd and rs take two bits each and name a destination and a source register. imm takes the low eight bits and carries a literal number — which the opcode then reads as a value (LDI r1, 2), or as an address (JNZ r1, 2), depending entirely on what the opcode is. The same eight bits, two different meanings, decided by four bits somewhere else in the word.

Decoding is nothing more than that cut. There is no parsing, no lookahead, no error checking beyond "is this a real opcode". A word that arrives with imm set to garbage and an opcode of ADD executes fine, because ADD never looks at imm. Half the word can be meaningless and the machine is perfectly happy.

The program counter is where the interesting part hides. PC is a register — the same kind of thing as r1, a number in a box. It gets +1 during fetch, before the opcode has even been examined, so by the time an instruction starts running, PC is already pointing past it. That ordering is not a detail. It is what makes a branch possible: the branch's job is to overwrite the value fetch already put there, and it can only be the last word on the subject because it runs later.

Instruction Execution Step by Step

The default run loads a seven-instruction program that adds 2 + 1 and stores the answer, with n = 2. Memory looks like this at load time, with the third column being our note to ourselves — the CPU never reads it:

addrwordreads as
00001 01 00 00000010LDI r1, 2
10001 10 00 00000000LDI r2, 0
20010 10 01 00000000ADD r2, r1
30011 01 00 00000001SUBI r1, 1
40100 00 01 00000010JNZ r1, 2
50101 00 10 00000111STORE r2, 7
61111 00 00 00000000HALT
70000 00 00 00000000not an instruction
  1. LDI r1, 2. PC is 0, so the word at address 0 travels into the instruction register. PC becomes 1. The opcode 0001 is LDI, rd is 01, and imm is 00000010 — so r1 = 2. The value was inside the instruction the whole time; eight of its sixteen bits are the number 2. No ALU work, so the execute stage is skipped entirely.
  2. LDI r2, 0. Same shape. r2 = 0. This is the accumulator, starting empty.
  3. ADD r2, r1. PC becomes 3 before anything is decoded. ADD reads two registers and writes one of them: the ALU takes r2 = 0 and r1 = 2, produces 2, and the write-back beat lands it in r2. Note that imm is 00000000 here and means nothing at all — ADD never looks at it.
  4. SUBI r1, 1. The ALU takes r1 = 2 and the literal 1 from the instruction, and r1 becomes 1. This is the instruction that will eventually end the loop, three beats before the loop knows it.
  5. JNZ r1, 2. PC becomes 5. The ALU tests whether r1 is non-zero; r1 is 1, so it is. The write-back beat writes PC = 2. Watch what does not happen here: no special loop machinery, no stack, no marker. The same write path that put a number in r2 two instructions ago puts a number in PC, and the next fetch reads address 2 because that is what PC now says.
  6. ADD r2, r1 again. r2 = 2 + 1 = 3.
  7. SUBI r1, 1 again. r1 = 0.
  8. JNZ r1, 2 again. This is the beat worth pausing on. The identical word, at the identical address, decoded into the identical fields. The ALU tests r1 = 0 and gets false, so the write-back beat writes nothing, and PC keeps the 5 that fetch handed it. Falling through is not an action the CPU takes; it is one it skips.
  9. STORE r2, 7. r2 holds 3, and imm is 7 — read as an address this time. Memory at 7 becomes 3, in the same column the program itself came out of.
  10. HALT. The opcode 1111 breaks the loop, after exactly 10 instructions. In general this program costs 3n + 4: two to set up, three per pass, and two to finish. Drag n to 5 and the counter in the header reads 19.

Push n to 3 and one more identical pass appears between steps 7 and 8. Nothing else about the machine changes, which is the point — the loop is not a feature that scales, it is one instruction being reached more times.

Why a Jump Is Just a Write

Three of these opcodes write a register, one writes memory, and one writes PC. Structurally they are the same beat: the result of the current instruction goes somewhere, and the somewhere is named by the instruction. Give that beat a different destination and you have invented control flow.

Everything higher-level is built out of that write. A while loop is a conditional backward write to PC. An if is a conditional forward write, skipping over the words you did not want. A function call is a write to PC that first stashes the old value so a later instruction can write it back — which is exactly what a return address on the stack is for. C's goto gets a bad reputation, but at this level goto is the only control flow there is, and every structured construct compiles into it.

This is also why the branch is where a modern CPU hurts. If it takes several cycles to discover which way the branch went, the machine has to either wait or guess — which is where branch prediction and pipelining start, and both are downstream of this one write.

Edge Cases

  • The branch that is never taken. Set n = 1 and the loop body runs once, the test finds r1 = 0 immediately, and the program is a straight line with a useless instruction in it. Nothing distinguishes a loop from straight-line code except how often PC gets rewritten.
  • A program with no branch at all. The No branch scenario removes the jump, so PC only counts up, one word per instruction, from 0 to HALT. It runs the same setup and one pass through ADD and SUBI, then stores immediately. The missing branch also removes every later loop pass, so the final registers and stored sum differ whenever n > 1.
  • A program that edits itself. In the Program edits itself scenario, a STORE writes the number 3 into address 4, which is where the HALT lives. The write is unremarkable — memory has no idea what it is holding. Three instructions later PC reaches address 4, fetches 0000 0000 0000 0011, and finds an opcode of 0000, which is not an opcode. The machine stops with an illegal instruction, and the complaint arrives at execution time, not at write time. Real operating systems mark code pages read-only precisely because this works.
  • A program that never ends. In Never returns, a JNZ at address 1 jumps to address 1, testing a register nothing ever decrements. The machine is not stuck in any sense it could detect — it is executing correctly, forever. The trace stops after six instructions because the animation has to stop somewhere; the CPU has no such limit.
  • Running off the end. Nothing marks where a program stops. If PC counts past the last instruction it fetches whatever is there — data, zeros, the remains of something else — and runs it. Ending a program is an instruction you have to write, not a boundary the machine respects.

Common Mistakes

  • Thinking the CPU knows where a program starts and ends. It knows one address. main is a convention enforced by a loader that sets PC to the right place; nothing below that level recognizes a function.
  • Believing instructions and data live in different places. In this unified-memory model they live in the same memory and format, and PC determines which words are fetched as instructions. Real machines may separate instruction and data paths or address spaces. The separation you rely on daily is enforced by page permissions in an OS, not by the machine's idea of what a word is.
  • Thinking a branch "jumps over" instructions. It writes a number. Instructions are not skipped so much as never addressed. A not-taken branch does not "continue" either — it declines to write, and PC's earlier increment stands.
  • Reading imm as one thing. The same eight bits are a literal in LDI, a subtrahend in SUBI, and an address in JNZ and STORE. If you find yourself asking what a field "is", the answer is always "whatever the opcode reads it as".
  • Assuming an unused field must be zero. ADD ignores imm completely. Some real ISAs formally require unused bits to be zero and trap otherwise; many do not, and assemblers happily leave junk there. Do not infer meaning from a field the opcode never reads.
  • Expecting the count of instructions to predict the time. This machine completes one instruction before starting the next, but each instruction has several teaching beats. 3n + 4 counts executed instructions, not animation steps or hardware clock cycles. On a real CPU the same count can take wildly different wall-clock times depending on cache misses, branch mispredictions, and how much of the work overlaps.

A Note on Simplification

This lesson is an explainer, not a substitute for a computer architecture course. TinyCPU executes one instruction at a time, start to finish, straight out of memory. A fast processor overlaps its stages so several instructions are in flight at once, predicts which way a branch will go before it can know, reorders instructions that do not depend on each other, and reaches a cache rather than DRAM on almost every access — most of the reason a modern chip is fast is machinery this animation does not contain.

The instruction set is also deliberately tiny. Real ISAs have condition flags, variable-length or multi-word instructions, addressing modes that compute an address instead of naming one, interrupts that redirect PC without any instruction asking, memory-mapped I/O, and privilege levels. Registers here start at 0 as a convenience; real initial values depend on the architecture, reset state, operating system, and calling convention. Do not assume all registers start at zero.

The general fetch, decode, execute, and result-update cycle connects the model to real CPUs. Advancing PC by one during fetch is TinyCPU's rule; real instruction lengths, PC timing, and speculative front ends differ. The packaging differs: most real machines keep PC out of the general-purpose register file — Arm's A64 will not let an instruction name it as an operand — and form a branch target by adding an offset to the current PC rather than reading an absolute address out of the instruction, as TinyCPU's JNZ does. What survives intact is the mechanism: a taken branch is a different value arriving in the register the next fetch reads, and nothing about it is more magical than the write on screen.