One trip through the machine
In the previous guide you met the datapath as a wiring diagram — the program counter, instruction memory, the register file, the ALU, data memory, the sign-extender, and the muxes that choose between routes. That guide showed where everything sits. This one makes it move. We will run a single-cycle processor, where one whole instruction completes in one clock cycle, and watch the bits actually flow.
Every instruction, no matter what it does, begins the same way. This shared opening is the fetch half of the instruction cycle: read the instruction the PC points at, and figure out the address of the next one. Only after the bits of the instruction are in hand does the machine diverge, because only then does it know whether it is holding an add, a load, a store, or a branch. Think of it like a kitchen reading the next ticket off the rail: the act of pulling the ticket is identical every time; what you do next depends on what the ticket says.
- Fetch: the current PC value is sent to instruction memory, which returns the 32-bit instruction word; in parallel an adder computes PC + 4, the address of the next instruction.
- Decode: the instruction's bits are split into fields — the opcode, the register numbers, and any immediate — and the register file is read using those register numbers.
- Execute: the ALU computes — an arithmetic result, or a memory address, or a comparison for a branch — depending on what the instruction is.
- Memory and write-back: optionally touch data memory (for a load or store), then optionally write a result back into a register, and update the PC for the next cycle.
An arithmetic op: the shortest path
Start with the simplest case: a register-to-register arithmetic instruction such as add x5, x6, x7 (put the sum of x6 and x7 into x5). In RISC-V this is an R-type instruction, and walking it through the datapath is the cleanest demonstration of R-type execution. After fetch, the decoder reads the two source register numbers (6 and 7) and the destination number (5) straight out of the instruction's fixed fields.
The register file reads x6 and x7 simultaneously — it has two read ports for exactly this reason — and presents their values to the ALU's two inputs. The ALU is told to add (we will see in the next guide where that 'add' command comes from), and it produces the sum. That sum travels back to the register file's write port, where it is written into x5. Meanwhile PC + 4 becomes the next PC. Notice what was never touched: data memory was idle, the sign-extender's output went nowhere, and the branch adder's result was ignored. The hardware for those paths still exists — it just sat unused this cycle.
add x5, x6, x7 (R-type)
PC --> instr.mem --> instruction bits
| reg numbers 6,7,5
v
reg[6] --+
+--> ALU(add) --> result --> write into reg[5]
reg[7] --+
data memory: not used
sign-extender: not used
next PC: PC + 4A load and a store: reaching into memory
Now a load: lw x5, 8(x6) means 'fetch the word at the address (value in x6) + 8 and put it in x5'. This is load execution, and it uses two parts the add ignored. First, the offset 8 lives inside the instruction as a small immediate; the sign-extender widens it to a full 32 (or 64) bits so it can be added to a register that wide — sign-extension because the offset can be negative. Then the ALU adds the base register x6 to that extended offset to compute the effective address.
Here is the key difference from the add: the ALU's output is not the final answer — it is an address. It feeds the address port of data memory, which reads that location and returns the word. A mux at the register file's write port now chooses the memory's output (instead of the ALU's) as the value written into x5. That mux is the whole reason a load and an add can share one set of wires: the same ALU result line exists in both, but a load overrides it at the last moment with the value pulled from memory.
A store, sw x5, 8(x6), computes its address the very same way — sign-extend the offset, ALU-add it to x6 — but the data flows the other direction. Instead of reading memory, it writes the value of x5 into that address, and nothing is written back to any register. So a store and a load are almost mirror images: identical address arithmetic, opposite memory direction, and a store leaves the register file untouched. The control signals that flip those directions are exactly what guide 3 is about.
A branch: changing what comes next
The first three instructions all let PC + 4 win — they flow straight ahead. A conditional branch like beq x5, x6, label is different: it may make the next instruction somewhere else entirely. This is branch execution, and it splits the ALU's job from the address job. The ALU here is used not to produce a value but to compare: it subtracts x6 from x5 and checks whether the result is zero, which answers 'are they equal?'.
In parallel, a second adder computes the branch target: it sign-extends the branch's immediate offset and adds it to the PC, giving the address of label. Now a mux chooses the next PC between two candidates — PC + 4 (fall through) and the branch target (take the branch) — and the ALU's equality result is the select line for that mux. If equal, the target wins; if not, PC + 4 wins. That single mux is where a program's control flow lives: it is the hardware that decides whether a loop spins again or a function returns.
Why one cycle starts to hurt
In a single-cycle machine, every instruction finishes in one tick of the clock — beautifully simple, and a great way to learn the datapath. But the clock can only tick as fast as the slowest instruction allows, because the cycle must be long enough for that instruction's whole chain of work to settle. That chain is the critical path: for a load it runs fetch, then register read, then ALU add for the address, then a full data-memory read, then the write-back — the longest journey any instruction makes.
Here is the waste: an add finishes in maybe half that time, yet it is forced to wait out the same long cycle, because the clock period is fixed to the worst case. By the iron law — run time = instruction count x CPI x cycle time — a single-cycle design pins CPI at a tidy 1 but pays for it with an enormous cycle time, so the product is poor. This is the single-cycle limitation, and it is honest to admit no real high-performance CPU is built this way.
Two escapes lead out of this. One is the multi-cycle datapath: chop each instruction into several short steps and give each step its own fast clock tick, so an add can finish in three ticks while a load takes five — nobody waits for the worst case anymore. The other, which the next rung builds toward, is pipelining: keep one cycle per stage but overlap different instructions in different stages, like a laundry line washing one load while drying another. Both rest on the same realisation: the parts of an instruction can be separated in time, not just in space.
Who tells the wires what to do
Throughout this guide one thing stayed in the shadows: who set the muxes, told the ALU to add versus subtract, and decided whether memory was read, written, or left alone? That is the control unit. It looks only at the opcode (and a few extra bits) and emits a bundle of control signals — one wire per switch — that configure the datapath for this instruction. The datapath moves the data; the control unit steers it. Guide 3 opens that box.
There are two classic ways to build that controller, and they preview a theme you will meet again. Hardwired control bakes the opcode-to-signals mapping into fixed logic gates — fast and lean, the natural fit for a clean RISC ISA. Microprogramming instead stores the signals as tiny words in a small memory and looks them up, trading a little speed for the flexibility to express very complex instructions — historically how baroque CISC machines coped. This split between datapath and control, and the choice of how to implement control, is the seam where the simple picture you now hold opens into the real engineering of a processor.