Building the Single-Cycle Datapath: ALU, Register File, Memory, and Buses
Part 12 of 15
Lesson 2, Part 1 nailed down the ISA we're implementing: a 32-bit MIPS-style machine with R0 hardwired to zero, R-type ADD/SUB/AND/OR, and I-type LW/SW/BEQ, decoded from opcode and funct fields. This part builds the hardware that actually executes those seven instructions — one wire, one mux, one memory port at a time — until we have a single labeled diagram that every later part of this lesson can point at.
1. The Rule We're Building Under
Every gate in this datapath has to do useful work for every instruction it's wired into, because in a single-cycle design there is exactly one piece of hardware per function and it's shared by whichever instructions pass through it. There's one ALU, not seven. One register file, not one per instruction type. The entire art of single-cycle datapath design is finding the union of what all seven instructions need, building shared hardware for that union, and then inserting a multiplexer wherever two instruction types disagree about what should flow down a shared wire.
That's the whole story, actually. Every mux in this post exists because two (or more) instructions want different data on the same physical wire in the same cycle. Keep that lens on as we go — it turns "why is there a mux here" from a memorization question into a derivation.
We'll build the datapath in the same order data actually flows through it: fetch, decode/read, execute, memory, writeback, and finally the PC-update logic that closes the loop back to fetch. Each section adds one piece to a running diagram, and Section 9 assembles the complete thing.
2. Fetch: PC and Instruction Memory
At the start of every cycle, the Program Counter holds a 32-bit address. That address goes straight into instruction memory, which is just a big combinational read-only lookup table for this design (no writes, no clock dependency on the read path):
Two things to notice already:
- The bus carrying
PCand the bus carryingInstructionare both 32 bits wide — full machine-word width, because both an address and an encoded instruction are 32-bit quantities in this ISA. - Nothing here depends on the opcode yet. Every one of the seven instructions is fetched by exactly this same hardware, with exactly this same 32-bit address-in, 32-bit instruction-out behavior. Fetch is the one stage with zero muxes, because there is nothing yet to choose between.
Recall from Part 1 that PC starts at 0x00400000. That value lives in the PC register and is loaded into instruction memory's address port every cycle, combinationally — the read happens continuously, not on a clock edge, exactly like the register-file reads from the earlier register-file post in this series.
3. Field Splitting: Slicing the Instruction
The 32-bit instruction word coming out of instruction memory isn't opaque — it's several fixed-position bit fields, and different instructions use different subsets of them. This "splitting" isn't a circuit at all; it's just labeled taps into the same 32 wires, wired to whatever needs each field:
R-type: [ opcode 6 | rs 5 | rt 5 | rd 5 | shamt 5 | funct 6 ]
31 26 25 21 20 16 15 11 10 6 5 0
I-type: [ opcode 6 | rs 5 | rt 5 | immediate 16 ]
31 26 25 21 20 16 15 0Concretely, off the 32-bit Instruction bus:
| Field | Bits | Width | Feeds |
|---|---|---|---|
opcode | [31:26] | 6 | Control unit (Part 3) |
rs | [25:21] | 5 | Register file ReadReg1 |
rt | [20:16] | 5 | Register file ReadReg2, and one input of the RegDst mux |
rd | [15:11] | 5 (R-type only) | Other input of the RegDst mux |
funct | [5:0] | 6 (R-type only) | ALU-control decoder (Part 3) |
immediate | [15:0] | 16 (I-type only) | Sign-extend unit |
Every one of these taps is a plain wire bundle, not a piece of active logic — opcode, rs, rt, rd, funct, and immediate are just different-width slices of the same 32 bits, routed to wherever downstream hardware needs them. rs and rt are always valid regardless of instruction format (both formats put them in the same two 5-bit slots — this is a deliberate MIPS encoding regularity that avoids needing a mux just to find the source register fields). rd and funct are only meaningful for R-type; immediate is only meaningful for I-type. The datapath doesn't need to "know" which fields are meaningless for a given instruction — it just needs the right value to end up on each shared wire when it matters, which is exactly what the muxes in the next few sections handle.
4. Register Read: The Register File's Two Read Ports
rs and rt go directly into the register file's two read-address ports:
Both reads happen combinationally and simultaneously — no instruction in this ISA needs to read one register, wait, then read a second one. ADD/SUB/AND/OR read rs and rt as its two operands. LW reads rs as the base address (it doesn't use ReadData2 for computing the address, but it does read rt's register — wait, actually for LW, rt is the destination, not a source; more on that in Section 5). SW reads rs as the base address and rt as the value to store. BEQ reads rs and rt as the two operands to compare.
So ReadData1 is always fed by rs, for every one of the seven instructions — that wire never needs a mux in front of it. ReadData2 is always fed by rt — also no mux needed on the read side. The interesting mux shows up on the write side, because rt means something different depending on instruction type: for LW it's the destination register, but for R-type instructions the destination is rd, not rt.
R0 deserves a callout here: it's not a normal storage cell. Reading register address 00000 always returns the constant 0, and writes to address 00000 are architecturally discarded — the register file's write logic simply refuses to update storage when WriteReg == 0, regardless of RegWrite. Every ALU op, load, and address calculation in this ISA can therefore use R0 as a free source of the constant zero without a single dedicated "load immediate zero" instruction.
4.1 The RegDst Mux — Choosing the Write-Back Register Address
- What it chooses between: the
rtfield (bits[20:16]) vs. therdfield (bits[15:11]) as the 5-bit address of the register that gets written this cycle. - Who needs which input:
RegDst = 1→ selectrd. Used byADD, SUB, AND, OR— R-type instructions put their destination in therdfield by encoding convention (rd ← rs OP rt).RegDst = 0→ selectrt. Used byLW— I-type instructions have nordfield at all (those bits are the top of the 16-bit immediate instead), so the destination register for a load is encoded inrt(rt ← Mem[rs + signext(imm)]).SWandBEQdon't write any register, soRegDst's value is irrelevant for them —RegWritewill be0and the mux output is simply ignored downstream.
- Bus width: both inputs and the output are 5 bits — a register address, not a register value.
This is a textbook-style pattern that appears identically in Patterson and Hennessy's Computer Organization and Design, which is the canonical reference for this entire datapath shape (details in Further Reading).
5. The Immediate Path: Sign-Extend
I-type instructions (LW, SW, BEQ) carry a 16-bit immediate field. R-type instructions don't use it at all — but the hardware doesn't get to skip building the sign-extend unit for R-type instructions; it's simply unused (its output feeds a mux input that R-type ignores).
Sign extension replicates bit 15 (the sign bit of the 16-bit field) into the upper 16 bits of the 32-bit result:
immediate = 1111 1111 1111 0100 (-12 as a 16-bit two's-complement value)
↓ sign-extend (bit 15 = 1 replicated upward)
SignExtImm = 1111 1111 1111 1111 1111 1111 1111 0100 (-12 as 32-bit two's-complement)If bit 15 had been 0 (a positive or zero immediate), the upper 16 bits would all become 0 instead — the unit is a pure combinational replication circuit, one wire fanning out to sixteen positions, not an adder or anything stateful. Note this is genuinely a 16-bit-in, 32-bit-out circuit — the bus width changes here, which is worth flagging explicitly since most of this datapath keeps buses at a constant width end to end.
Why sign-extend rather than zero-extend? Because LW/SW use the immediate as a signed byte offset from a base register (Mem[rs + signext(imm)]) — negative offsets need to work exactly the same as positive ones for address arithmetic to be correct, and BEQ's branch offset (Section 8) is signed for the same reason: branching backward (a loop) is exactly as common as branching forward.
6. Execute: The ALU and the ALUSrc Mux
The ALU always takes its first operand from ReadData1 (i.e., from register rs) — no instruction in this ISA needs anything else on that side. The second operand is where instructions disagree, and that disagreement is exactly what ALUSrc resolves.
6.1 The ALUSrc Mux — Choosing the ALU's Second Operand
- What it chooses between: the raw value read from register
rt(ReadData2) vs. the sign-extended 16-bit immediate (SignExtImm). - Who needs which input:
ALUSrc = 0→ selectReadData2. Used byADD, SUB, AND, OR(both operands are registers) and byBEQ(comparing two registers means both ALU inputs are register values — see Section 8).ALUSrc = 1→ selectSignExtImm. Used byLWandSW— both compute an effective address asrs + signext(imm), so the immediate has to reach the ALU's second input.
- Bus width: 32 bits on every wire here —
ReadData2,SignExtImm, and the mux output are all full register width, even though the immediate started life as only 16 bits (that's exactly why Section 5's sign-extend unit exists: to widen it to 32 bits before it reaches this mux, so the mux itself never has to deal with mismatched widths).
6.2 The ALU Itself
The ALU is a single shared piece of combinational hardware performing whichever operation ALUCtrl selects (ADD, SUB, AND, OR — the same four functional units from the ALU built in an earlier post in this series, muxed together internally by ALUCtrl). Two of its outputs matter to the rest of the datapath:
Result(32 bits) — the arithmetic/logic answer for R-type instructions, or the computed memory address forLW/SW, or the "are these equal" difference forBEQ(computed asrs − rt; equality shows up as theZeroflag, not asResultitself).Zero(1 bit) — asserted whenResult == 0. ForADD/SUB/AND/ORthis flag is simply unused downstream. ForBEQ, this is the entire point of running the ALU at all:BEQuses the ALU as a comparator by computingrs − rtand checking whether the answer is zero, rather than needing separate comparator hardware.
This reuse is worth sitting with, because it's the same design principle as the whole datapath: rather than build a dedicated equality comparator for BEQ, the design reuses the subtractor that already exists inside the ALU and taps its zero-detect output. One piece of hardware, three uses (add/subtract results, address arithmetic, and now equality testing).
7. Memory: Data Memory and the MemToReg Mux
The ALU's Result becomes the address into data memory for LW and SW — the same Result bus, no mux needed, because in this ISA the ALU is the only source of memory addresses (there's no separate address-generation unit).
Addresscomes straight from the ALUResult— forLW/SWthis isrs + signext(imm); forADD/SUB/AND/OR/BEQthis input is simply unused becauseMemReadandMemWriteare both0for them.- Write-data into memory is
ReadData2— the value read from registerrt— used only bySW(Mem[rs + signext(imm)] ← rt). MemReadis asserted only forLW.MemWriteis asserted only forSW.ReadData(32 bits) is the value memory returns on a read — meaningful only whenMemRead = 1, i.e., only forLW.
Every instruction's Address, write-data, and ReadData wires are physically connected to this same memory every cycle — data memory doesn't get to "know" it's being talked to by an ADD instruction and switch itself off. It's MemRead/MemWrite that determine whether anything observable actually happens.
7.1 The MemToReg Mux — Choosing the Write-Back Value
- What it chooses between: the ALU's
Resultvs. the value just read from data memory. - Who needs which input:
MemToReg = 0→ selectALU Result. Used byADD, SUB, AND, OR(the ALU result is the answer to write back).MemToReg = 1→ selectMem ReadData. Used byLW(the loaded value from memory is what belongs in the destination register, not the address that was used to fetch it).SWandBEQwrite no register at all (RegWrite = 0), soMemToReg's value is irrelevant for them, exactly likeRegDstwas for them in Section 4.1.
- Bus width: 32 bits on all three wires — this mux picks between two full register-width values, unlike
RegDst, which picked between two 5-bit addresses. Don't confuse the two — they're both "the write-back mux family" conceptually, but one selects an address and the other selects data.
The MemToReg mux's output feeds straight back into the register file's WriteData port from Section 4 — closing the loop from ALU/memory back into storage.
8. PC Update: Sequential Flow, Branch Target, and PCSrc
Every cycle, the CPU needs a next-PC value, and — mirroring every other decision point in this datapath — there are exactly two candidates and a signal that picks between them.
8.1 The Sequential Adder: PC + 4
Every instruction is 4 bytes wide in this fixed-width ISA, so "the next instruction, assuming no branch" is always PC + 4. This adder runs every single cycle, for every instruction, unconditionally — it's the default path.
8.2 The Branch-Target Adder
BEQ's target isn't an absolute address — it's an offset relative to PC + 4, scaled by 4 because branch offsets in this ISA count instructions, not bytes, and every instruction is 4 bytes. SignExtImm << 2 is a pure wiring shift (append two zero bits at the low end, or equivalently, shift the 32-bit bus left by two positions with zeros shifted in) — it costs no gates, just relabeled wires, since a left-shift-by-2 is nothing more than reading the same bits at different bit positions.
Concretely, from Part 1's running example: BEQ R6, R2, 1 at instruction index 4 has immediate = 1 (signext(1) = 0x00000001). Shifted left by 2, that's 0x00000004. If PC at that instruction is 0x00400010 (instruction 4, four words past the program start at 0x00400000), then PC + 4 = 0x00400014, and Branch Target = 0x00400014 + 0x00000004 = 0x00400018 — which is exactly instruction index 6, skipping over instruction 5 as the ISA semantics specify (PC ← PC + 4 + (signext(imm) << 2)).
This adder, like the sign-extend unit and the ALU, runs unconditionally every cycle regardless of whether the current instruction is actually a branch — the datapath computes a branch target for ADD instructions too, it just never gets used. That's the price of a single-cycle design: every functional unit that any instruction might need is present and active on every cycle; only the muxes decide what actually matters.
8.3 The Branch Decision: Branch AND Zero, and the PCSrc Mux
PCSrc is not a direct control-unit output — it's a derived signal, computed by ANDing two things together:
Branch— asserted by the control unit if and only if the current instruction isBEQ. This alone doesn't mean the branch is taken; it just means "this instruction is even capable of branching."Zero— asserted by the ALU (Section 6.2) if and only ifrs − rt == 0, i.e.,rs == rt.
Only when both are true — the instruction is BEQ and the two registers are equal — does PCSrc = 1, selecting Branch Target as the next PC. For every non-branch instruction, Branch = 0 unconditionally forces PCSrc = 0 regardless of what Zero happens to be (and Zero genuinely can be 1 for a non-branch instruction — e.g., SUB R6, R1, R1 produces Result = 0 and Zero = 1, but since Branch = 0 for SUB, the AND gate output stays 0 and the sequential PC + 4 path is selected anyway). For BEQ with unequal registers, Branch = 1 but Zero = 0, so the AND still outputs 0, and PC + 4 — sequential fall-through — is correctly selected.
This is the same technique from Section 6.2 taken one level further: rather than build dedicated "is this a taken branch" hardware, the design reuses the ALU's existing zero-detect output and combines it with one control bit via a single 2-input AND gate. MIT's 6.004 course materials and Patterson and Hennessy's textbook both describe this exact Branch AND Zero → PCSrc structure as the standard mechanism for deriving the branch-taken signal in a single-cycle MIPS datapath (see Further Reading).
- Bus width note:
Branch,Zero, andPCSrcare all single bits — this is the one place in the whole datapath where two 1-bit control-ish signals combine through a logic gate (not a mux) to produce a third 1-bit signal that then drives a mux select line. Every other mux in this design is selected directly by a control-unit output;PCSrcis the sole exception, because it's the one decision that depends on data (are the two registers actually equal) rather than purely on instruction type.
9. The Complete Single-Cycle Datapath
Everything above assembles into one picture. This is the centerpiece diagram of this lesson — every instruction this CPU executes flows through exactly this hardware, differing only in which muxes route which values and which of RegWrite/MemRead/MemWrite/Branch are asserted (that decoding is Part 3's job; this part only builds the pipes the water flows through).
This ASCII rendering is drawn in my own layout to convey the same well-known design taught in Patterson and Hennessy's Computer Organization and Design — it is not a reproduction of their textbook figure, just a from-scratch depiction of the identical, decades-standard set of connections (PC → instruction memory → register file/sign-extend → ALU → data memory → writeback, plus the branch-adder/AND-gate/PCSrc loop back into the PC). If the linearized diagram above is hard to trace visually, walk it stage by stage using Sections 2 through 8 — every wire in the picture is named and explained there.
10. Mux Summary Table
Five multiplexers appear in this datapath. Here's the complete reference:
| Mux | Input 0 | Input 1 | Selected by | Width | Instructions using Input 0 | Instructions using Input 1 |
|---|---|---|---|---|---|---|
RegDst mux | rt field | rd field | RegDst | 5 bits | LW | ADD, SUB, AND, OR |
ALUSrc mux | ReadData2 (register rt) | SignExtImm | ALUSrc | 32 bits | ADD, SUB, AND, OR, BEQ | LW, SW |
MemToReg mux | ALU Result | Mem ReadData | MemToReg | 32 bits | ADD, SUB, AND, OR | LW |
PCSrc mux | PC + 4 | Branch Target | PCSrc (= Branch AND Zero) | 32 bits | ADD, SUB, AND, OR, LW, SW, and BEQ not-taken | BEQ taken |
PC-input mux (implicit — same mux as PCSrc row above) | — | — | — | — | — | — |
(The last row is intentionally the same mux as the PCSrc row — it's listed as "PC-input mux" in some textbook treatments and as "PCSrc mux" in others; they name the identical piece of hardware.)
Note what's not in this table: there's no mux selecting the ALU's first input (ReadData1/rs always feeds it), no mux on the register file's read-address ports (rs/rt always feed ReadReg1/ReadReg2 directly), and no mux on the memory address port (the ALU Result always feeds it). Single-cycle datapath design is exactly this exercise — cataloging every wire, and inserting a mux only where the catalog reveals genuine disagreement between instructions.
11. Wire-Width Cheat Sheet
Pulling every bus width mentioned above into one place, since getting these wrong is the single most common bug when actually building this circuit (e.g., in Verilog for a class lab):
| Signal | Width | Why |
|---|---|---|
PC, PC + 4, Branch Target, Next PC | 32 | Full address width |
Instruction | 32 | Fixed instruction width |
opcode | 6 | Encoding field |
rs, rt, rd, RegDst/ALUSrc mux address-type outputs | 5 | 2^5 = 32 addressable registers |
funct | 6 | Encoding field |
shamt | 5 | Encoding field (unused by this 7-instruction subset) |
immediate (raw, from instruction) | 16 | Encoding field |
SignExtImm | 32 | Widened from 16 bits for ALU/address use |
SignExtImm << 2 | 32 | Shift doesn't change width, only value |
ReadData1, ReadData2, WriteData (register file) | 32 | Register width |
ALU input A, ALU input B, Result | 32 | Register/address width |
Zero | 1 | Single-bit flag |
Address, memory write-data, ReadData (data memory) | 32 | Byte-addressed, word-sized accesses in this ISA |
RegDst, ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, PCSrc | 1 | Each is a single control bit |
ALUOp | 2 | Feeds the secondary ALU-control decoder (Part 3) — 2 bits is enough to distinguish "R-type, look at funct" from "always add" (LW/SW) from "always subtract" (BEQ) |
The pattern to internalize: data buses are 32 bits, register addresses are 5 bits, control signals are 1 bit (except ALUOp at 2 bits), and the immediate field is the only place a 16-to-32-bit widening happens. Every wire in Section 9's diagram falls into exactly one of these categories.
Takeaway: The single-cycle datapath is not a pile of special-purpose circuits — it's a small, fixed set of shared hardware (one register file, one ALU, one data memory, two adders) wired together with exactly five multiplexers, each mux existing precisely because two or more of the seven instructions disagree about what value belongs on some shared wire.
RegDstandMemToRegresolve register-file disagreements (which address to write, which value to write).ALUSrcresolves what the ALU's second operand should be.PCSrc— the only mux driven by data rather than pure instruction type — resolves whether execution continues sequentially or jumps to a branch target, by ANDing the control unit'sBranchbit with the ALU'sZeroflag.
12. Per-Instruction Walkthrough: What Every Mux Selects
Part 3 will derive where each control bit's value comes from (the opcode-to-control-signal decoder). Here, treating the control values as given, is the complete map of what every one of the five muxes actually selects for each of the seven instructions — useful as a reference while reading Section 9's diagram, and worth checking your own understanding against before moving on.
ADD, SUB, AND, OR (R-type)
RegDst = 1→ write address comes fromrd.ALUSrc = 0→ ALU's second input isReadData2(registerrt); both operands are registers.MemToReg = 0→ the value written back is the ALUResultdirectly.PCSrc = 0always, becauseBranch = 0regardless of whatZerohappens to be — soNext PC = PC + 4unconditionally, even forSUBinstructions whose result happens to be zero.- Data memory is untouched:
MemRead = MemWrite = 0. TheAddress, write-data, andReadDatawires all carry values, but nothing observable happens because the enables are off.
LW
RegDst = 0→ write address comes fromrt(I-type has nordfield).ALUSrc = 1→ ALU's second input isSignExtImm; the ALU computesrs + signext(imm)as an address, not a "real" arithmetic result.MemRead = 1→ data memory actually performs a read atAddress = ALU Result.MemToReg = 1→ the value written back isMem ReadData, not the ALU result — this is the one instruction where the address computed by the ALU is not what ends up in the destination register.PCSrc = 0, same reasoning as R-type.
SW
ALUSrc = 1→ same address computation asLW:rs + signext(imm).MemWrite = 1→ data memory performs a write atAddress = ALU Result, usingReadData2(registerrt) as the write-data.RegWrite = 0→ no register is written this cycle, soRegDstandMemToRegare both don't-cares; their mux outputs exist but feed nothing that matters.PCSrc = 0, same reasoning as R-type.
BEQ
ALUSrc = 0→ both ALU inputs are registers (rsandrt), exactly like R-type, becauseBEQis comparing two register values, not computing an address.- The ALU performs a subtraction internally regardless of the surface-level "compare" semantics; only
Zerois consulted,Resultitself is discarded. RegWrite = 0→ same asSW, no register write, soRegDst/MemToRegare don't-cares.MemRead = MemWrite = 0→ data memory is untouched.Branch = 1→ this is the one instruction where the AND gate's first input is asserted, makingPCSrcactually depend onZero:PCSrc = 1(branch target taken) ifrs == rt, elsePCSrc = 0(fall through toPC + 4).
Notice the pattern across all seven: only BEQ ever sets Branch = 1, only LW ever sets MemRead = 1, only SW ever sets MemWrite = 1, and RegWrite = 0 for exactly the two instructions that write no register (SW, BEQ). Every other control combination reduces to a choice between "R-type shape" (RegDst=1, ALUSrc=0, MemToReg=0) and "I-type memory shape" (RegDst=0, ALUSrc=1, MemToReg=1 for LW; ALUSrc=1 alone for SW). That regularity — few distinct control patterns covering seven instructions — is exactly what makes the opcode-driven control unit in Part 3 tractable as a small truth table rather than seven hand-written special cases.
Further Reading
- David A. Patterson and John L. Hennessy, Computer Organization and Design (MIPS/RISC-V editions) — the canonical textbook treatment of this exact single-cycle datapath, including the
RegDst/ALUSrc/MemToReg/PCSrccontrol-signal set used throughout this post. - MIT OpenCourseWare, 6.004 Computation Structures, Lecture 13 ("Datapath and Control Logic"): ocw.mit.edu/courses/6-004-computation-structures-spring-2017/pages/c13/c13s1/
- GeeksforGeeks, Differences between Single Cycle and Multiple Cycle Datapath: geeksforgeeks.org/computer-organization-architecture/differences-between-single-cycle-and-multiple-cycle-datapath/
- Sarah L. Harris and David Money Harris, Digital Design and Computer Architecture — a worked single-cycle datapath build with the same mux-per-disagreement structure used in this post, including the
Branch AND Zero → PCSrcderivation. - UC Berkeley CS61C, Single-Cycle CPU Datapath Design lecture and course notes: notes.cs61c.org/content/datapath/
- EE457 (USC), Single-Cycle CPU Datapath and Control slides — another independent worked example of the same MIPS-style datapath and control-signal naming: ee.usc.edu/~redekopp/ee457/slides/EE457Unit5_SingleCycleCPU.pdf
With the pipes built, the next question is where the actual 0s and 1s driving RegDst, ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, and ALUOp come from. That's Lesson 2, Part 3: The Control Unit — Generating Every Control Signal from the Opcode.