The Single-Cycle Datapath
Imagine building a processor from scratch. It must execute every instruction — a load, an add, a branch — in exactly one clock tick. No instruction may straddle two cycles. No shortcuts are allowed for the fast cases. This constraint sounds wasteful. Yet it forces a beautifully disciplined design. Every functional unit must be wired together so that data flows from instruction fetch to register writeback. This happens in a single, deterministic pass through combinational logic. This is the single-cycle datapath, the wiring skeleton that connects the Register File, the ALU, instruction and data memories, sign-extension logic, and a collection of multiplexers. All of this is governed by control signals derived from the instruction's opcode and function fields. It is the concrete machine that realizes the abstract Fetch-Decode-Execute Cycle as a physical circuit. It is a circuit rather than a sequence of steps performed over time. Understanding the single-cycle datapath matters because it is the conceptual seed. Every modern high-performance processor grows from it. Real CPUs no longer execute one instruction per (long) cycle. They pipeline, they execute out of order, they issue multiple instructions per cycle. But every one of those designs began as an engineer's mental model. That model asks "what hardware must exist and how must it connect for one instruction to complete." The single-cycle machine is that model in its purest, most legible form. It is uncomplicated by pipeline hazards or multi-cycle finite-state control. This article builds the datapath from its components upward. It starts with the register file and ALU as the computational core. Then come the muxes that route data for different instruction formats. It covers the memories that hold code and data, and the adder that computes the next program counter. We derive the clock period from the slowest instruction's critical path. We argue correctness instruction-class by instruction-class. We trace a real add, load, and branch through the wires. This shows exactly what happens on the rising edge of the clock.
- The single-cycle datapath executes one complete instruction every clock cycle using purely combinational logic between state elements.
- Core components: PC, instruction memory, register file, ALU, data memory, sign-extension unit, and control multiplexers.
- Multiplexers select between competing data sources so the same physical hardware serves R-type, load/store, and branch instructions.
- The clock period is fixed by the longest instruction's critical path — typically a load — forcing every instruction to run at that slow pace.
- Correctness follows from an invariant: after each rising clock edge, PC and register file contents equal the architectural state after executing exactly one instruction.
- The design is simple and easy to reason about, but wastes time on fast instructions and motivates Introduction to Pipelining.
A single-cycle datapath wires fixed hardware — register file, ALU, memories, and muxes — so that combinational logic alone carries every instruction from fetch to writeback within one clock period.
The idea
A datapath is the network of functional units, registers, and interconnecting wires that data flows through as a processor executes instructions. In a single-cycle implementation, this entire network is built from combinational logic (logic with no internal clocked storage) sandwiched between just a few clocked state elements: the program counter (PC), the register file, and the data memory. On every rising clock edge, these state elements simultaneously update, and then the combinational logic has exactly one full clock period to compute the next instruction's effects before the next edge arrives. The key design principle is hardware reuse through multiplexing. Different instruction formats need different things from the same units — an R-type instruction reads two registers and writes a third; a load instruction reads one register, computes an address, and writes back memory data; a branch instruction reads two registers only to compare them and never writes anything. Rather than building separate circuits for each instruction type, the datapath builds one general-purpose ALU, one register file with two read ports and one write port, and one memory, then uses multiplexers (muxes) driven by control signals to select, for each instruction, which values flow into and out of these shared units. This is the same idea as Addressing Modes applied at the microarchitectural level: one set of hardware, many behavioral configurations. The datapath is single-cycle in the strict sense that the clock period is long enough for the slowest instruction to complete entirely — fetch, decode, register read, ALU operation, memory access, and register writeback — within one tick. This is the crucial tradeoff to internalize: simplicity of control (no multi-cycle state machine, no hazard detection) is bought at the price of a clock period dictated by the worst-case instruction, even though most instructions (like simple ALU operations) could finish much faster.
How it works
The datapath is best understood by building it up instruction class by instruction class, then merging the pieces with muxes. We use a MIPS/RISC-V–style architecture with 32-bit instructions as the running example, since its regular encoding (fixed instruction length, fields in fixed positions) is exactly what makes a clean single-cycle datapath possible — a property explored in Instruction Formats and Encoding.
Step 1 — Instruction fetch. The PC holds the address of the current instruction. It feeds directly into instruction memory as a read address (instruction memory here is treated as combinational read-only for the datapath's purposes; writes happen only via a separate loader). Concurrently, the PC is fed into an adder that computes `PC + 4`, the address of the sequentially next instruction.
Step 2 — Instruction decode and register read. The fetched 32-bit instruction is split into fields: opcode, register specifiers (`rs`, `rt`, `rd`), a function code, and/or an immediate. The register specifier fields feed directly into the register file's two read-address ports as wires — no clock edge is needed to "decode" since decoding is just picking off fixed bit positions. The register file simultaneously outputs the values of both specified registers.
Step 3 — ALU operation. The ALU takes one operand directly from the register file's first read port. Its second operand is chosen by a mux: either the register file's second read port (for R-type instructions) or a sign-extended immediate (for loads, stores, and immediate arithmetic). The ALU control signal, derived from the opcode and function fields, selects add, subtract, AND, OR, set-less-than, and so on.
Step 4 — Memory access. For loads and stores, the ALU result is used as a memory address into data memory. Stores write the second register's value into memory; loads read a value out.
Step 5 — Register writeback. A mux selects what gets written back into the register file's write port: the ALU result (for R-type/immediate arithmetic) or the data memory's output (for loads). Another mux selects the write-register specifier itself, since R-type instructions write to `rd` while I-type instructions (like loads) write to `rt`.
Step 6 — PC update. A final mux chooses the next PC: `PC + 4` for sequential execution, or `PC + 4 + (sign-extended immediate << 2)` for a taken branch. A branch is "taken" if a comparison — typically computed by having the ALU subtract the two register operands and expose a zero flag — succeeds. All of this happens in one pass through combinational logic per cycle; nothing here is sequenced across multiple cycles. ```text // Single-cycle datapath — one execution per invocation = one clock cycle function CLOCK_TICK(PC, RegFile, InstrMem, DataMem): // ---- Fetch ---- instr = InstrMem[PC] PCplus4 = PC + 4 // ---- Decode (pure field extraction, no delay in the "cycle" sense) ---- opcode = instr[31:26] rs = instr[25:21] rt = instr[20:16] rd = instr[15:11] funct = instr[5:0] imm16 = instr[15:0] immExt = SignExtend(imm16) // 16 -> 32 bits // ---- Control (combinational decode of opcode/funct) ---- ctrl = ControlUnit(opcode, funct) // ctrl fields: RegDst, ALUSrc, MemToReg, RegWrite, // MemRead, MemWrite, Branch, ALUOp // ---- Register read ---- readData1 = RegFile.read(rs) readData2 = RegFile.read(rt) // ---- ALU input selection ---- aluOperandB = ctrl.ALUSrc ? immExt : readData2 aluControl = ALUControlUnit(ctrl.ALUOp, funct) aluResult, zero = ALU(readData1, aluOperandB, aluControl) // ---- Memory stage ---- memReadData = ctrl.MemRead ? DataMem.read(aluResult) : undefined if ctrl.MemWrite: DataMem.write(aluResult, readData2) // ---- Writeback selection ---- writeData = ctrl.MemToReg ? memReadData : aluResult writeReg = ctrl.RegDst ? rd : rt if ctrl.RegWrite: RegFile.write(writeReg, writeData) // ---- Next-PC computation ---- branchTarget = PCplus4 + (immExt << 2) PCnext = (ctrl.Branch AND zero) ? branchTarget : PCplus4 return PCnext // becomes PC at the next rising clock edge ``` Notice that this function has no internal loops or clock steps — it models exactly one combinational pass. The only state that survives from one call to the next is `PC`, the contents of `RegFile`, and the contents of `DataMem`; everything else (`instr`, `readData1`, `aluResult`, ...) is transient combinational-logic output that is recomputed from scratch every cycle.
Complexity & correctness
Time complexity — the clock period. The single-cycle datapath has no notion of "steps"; instead its cost is measured as the length of the critical combinational path, which fixes the clock period . For a load instruction, the path is:
summing the delay through the PC register, instruction memory, register file read, the ALUSrc mux, the ALU itself, data memory, the MemToReg mux, and the register file's setup time before the next edge. This is the longest path among all instruction classes — R-type instructions skip the data memory access, and branches skip the writeback mux, so their true critical paths are shorter. But because there is only one global clock, every instruction is forced to take as long as the slowest one. This is the central complexity fact about single-cycle design: throughput is one instruction per cycle by construction, but the cycle itself is as slow as the worst case, so overall performance is
with set by the load instruction's path — typically the longest because it touches instruction memory, the register file, the ALU, and data memory in series.
Space / hardware cost. The datapath uses functional units regardless of program size: one ALU, one register file, two memories, a small constant number of muxes and adders. This is a design feature: hardware area does not grow with instruction count or program complexity, only with word width (e.g., a 32-bit vs. 64-bit ALU), which is a constant multiplicative factor, not asymptotic growth.
Correctness argument. We argue correctness via an invariant maintained across clock edges: > Invariant: Immediately after the -th rising clock edge, the pair equals the architectural machine state that would result from an Fetch-Decode-Execute Cycle interpreter having executed exactly instructions starting from the initial state. Base case (): before any clock edges, PC, register file, and memory are initialized to the program's starting state by construction — trivially true. Inductive step: assume the invariant holds after edges, i.e., state reflects instructions executed. During cycle , the combinational logic reads `InstrMem[PC]` — by the invariant this is exactly the -th instruction the interpreter would fetch next. Case analysis on instruction class shows the combinational network computes precisely the semantics the interpreter defines:
- R-type (`add`, `sub`, `and`, ...): `RegDst=1` routes `rd` to the write-address port; `ALUSrc=0` selects the second register operand; `RegWrite=1` and `MemToReg=0` write the ALU result into `rd`. This matches "register-register op" semantics exactly.
- Load word: `ALUSrc=1` computes `base + offset` via the sign-extended immediate; `MemRead=1` fetches the value; `MemToReg=1, RegDst=0` write that value into `rt`. This matches load semantics.
- Store word: `ALUSrc=1` computes the same address; `MemWrite=1` writes `readData2` (the value in `rt`) to that address; `RegWrite=0` — no register is modified, matching store semantics.
- Branch-on-equal: the ALU performs a subtraction to expose `zero`; `Branch=1` combined with `zero=1` steers the next-PC mux to the branch target `PC+4+(imm<<2)`; otherwise `PC+4` is chosen. This matches branch semantics under both taken and not-taken cases.
In every case, the newly computed `writeData`/`writeReg`/`DataMem` write and the computed `PCnext` are exactly the effects the interpreter's step function would apply. Because register file and data memory writes, and the PC update, all occur synchronously on the same clock edge, the old values persist throughout the combinational computation of the current instruction (this is precisely why reads and writes to the register file within one cycle don't race: reads use the pre-edge state, and the computed write happens only at the next edge). Hence after the -th edge, the state equals the interpreter's state after steps, completing the induction.
Tradeoff, stated honestly. The single-cycle datapath trades time efficiency for conceptual and control simplicity: there is no hazard detection, no forwarding, no stalling logic, because each instruction fully completes — including all register writes — before the next one begins reading. This clean semantics is exactly what Introduction to Pipelining must give up (and then painstakingly recover via forwarding and hazard detection) in exchange for a much shorter clock period and higher throughput.
Worked example
Consider a tiny fragment of MIPS-style machine code, with registers `t1=3` initially, memory word at address `100` holding value `50`, and PC starting at `0x00400000`.
| # | Instruction | Encoding fields (relevant) | Effect |
|---|---|---|---|
| 1 | `add t0, $t1` | opcode=0(R-type), rs=t1, rd=$t2, funct=add | `$t2 ← 8+3 = 11` |
| 2 | `lw t0)` | opcode=lw, rs=t3, imm=100 | `$t3 ← Mem[8+100]` |
| 3 | `beq t3, LABEL` | opcode=beq, rs=t3, imm=offset | branch if `t3` |
Cycle 1 — `add t0, $t1`:
| Signal | Value | Why |
|---|---|---|
| `RegDst` | 1 | R-type writes to `rd` |
| `ALUSrc` | 0 | second operand is a register, not immediate |
| `readData1, readData2` | 8, 3 | register file outputs for `rs=t1` |
| ALU op | add | funct field decodes to addition |
| `aluResult` | 11 | 8 + 3 |
| `MemRead/MemWrite` | 0 / 0 | no memory access for R-type |
| `MemToReg` | 0 | writeback source is ALU result |
| `RegWrite` | 1 | write 11 into `$t2` |
| `PCnext` | `PC+4` | no branch signal asserted |
At the clock edge: `$t2 = 11`, `PC = 0x00400004`.
Cycle 2 — `lw t0)`:
| Signal | Value | Why |
|---|---|---|
| `RegDst` | 0 | I-type writes to `rt`, not `rd` |
| `ALUSrc` | 1 | second ALU operand is the sign-extended immediate |
| `readData1` | 8 | `$t0` |
| `immExt` | 100 | sign-extended 16-bit immediate |
| `aluResult` | 108 | 8 + 100, the effective address |
| `MemRead` | 1 | this is a load |
| `memReadData` | 50 | `Mem[108]` — suppose this holds 50 (address arithmetic from `Mem[100]` example simplified) |
| `MemToReg` | 1 | writeback source is memory data, not ALU result |
| `RegWrite` | 1 | write 50 into `$t3` |
| `PCnext` | `PC+4` | no branch |
At the clock edge: `$t3 = 50`, `PC = 0x00400008`.
Cycle 3 — `beq t3, LABEL`:
| Signal | Value | Why |
|---|---|---|
| `ALUSrc` | 0 | beq compares two registers |
| `readData1, readData2` | 11, 50 | `t3` |
| ALU op | subtract | beq's ALUOp forces subtraction to test equality |
| `aluResult, zero` | −39, `zero=0` | 11 − 50 ≠ 0 |
| `RegWrite` | 0 | branches never write a register |
| `Branch` | 1 | this is a beq instruction |
| `PCnext` | `PC+4` | `Branch AND zero = 1 AND 0 = 0`, so not taken |
Since `t3`, the branch mux selects the fall-through address, and execution continues sequentially to `0x0040000C`. Notice how the same ALU computed an address addition in cycle 2 and an equality-testing subtraction in cycle 3 — this is hardware reuse via muxed control signals, the central efficiency idea of the design.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Forgetting `RegDst` mux | R-type writes `rd`, I-type writes `rt` — using the wrong field corrupts the wrong register | Always mux the write-register address, not just the write data |
| Sign-extension omitted or wrong width | Negative offsets (e.g., branch backwards) silently become huge positive numbers | Sign-extend, don't zero-extend, immediates used in arithmetic/addressing |
| Assuming register writes are visible same-cycle | Combinational logic reads register file before the current write completes | Reads always see pre-edge (old) values; new values appear only next cycle |
: | | | | Setting clock period from an "average" instruction | Some instructions (loads) have longer critical paths than others | Clock period must accommodate the worst-case (slowest) instruction path | | Forgetting to gate `Branch AND zero` | Every `beq` would branch or never branch regardless of comparison | Next-PC mux must combine both the `Branch` control bit and the ALU's `zero` flag |
Pioneers & history
The single-cycle datapath as a teaching and design artifact crystallized alongside the broader RISC philosophy: build simple, regular, fixed-format instructions specifically so that a clean, mux-based, single-cycle (or few-cycle) hardware realization becomes possible. Its lineage runs through the stored-program concept and decades of digital logic design that made expressing a CPU as a wiring diagram of ALUs, registers, and multiplexers both tractable and teachable.
Origins · 1 minJohn von Neumann and how this was discovered
Von Neumann's 1945 First Draft of a Report on the EDVAC articulated the stored-program architecture: instructions and data share memory, and a central processing unit fetches, decodes, and executes them in sequence. The single-cycle datapath is a direct hardware embodiment of this abstraction — the PC, instruction memory, and control logic are precisely the mechanism von Neumann described, rendered as combinational circuitry with clocked state elements.
Where this matters
Single-cycle datapaths are rarely deployed as-is in commercial high-performance CPUs — real chips pipeline aggressively for higher clock rates and throughput — but the datapath skeleton described here is the literal starting point from which pipelined designs are derived (by inserting pipeline registers between stages, exactly the stages this article names: fetch, decode, execute, memory, writeback). Microcontrollers and small embedded cores, where simplicity and low design cost outweigh raw performance, sometimes do use single-cycle or near-single-cycle designs directly. Educational architectures used in university courses and open hardware projects (including many RISC-V teaching cores) present the single-cycle datapath as the canonical first implementation before layering in Single-Cycle Control and then pipelining. The mux-based, control-signal-driven pattern shown here — one shared ALU, one shared register file, selection logic for different instruction formats — recurs throughout computer architecture, from simple classroom CPUs to the execution units of modern superscalar processors.
Ask the tutor: How would you modify this datapath's muxes and control signals to add support for a jump-and-link instruction?
Why is the single-cycle datapath called "single-cycle" if some instructions could logically finish faster?
The name refers to the clock discipline, not to any instruction's intrinsic speed. Every instruction — whether a fast R-type add or a slower load — occupies exactly one clock cycle, because there is only one global clock period and no mechanism to let some instructions finish early and free up the hardware for the next one. The clock period itself is fixed by the worst-case critical path (typically a load), so simpler instructions like `add` finish their actual computation well before the cycle ends but simply wait, idle, until the next rising edge. This wasted slack is the central inefficiency that pipelined and multi-cycle designs are built to reclaim.
How does the datapath know which ALU operation to perform for a given instruction?
The opcode field alone doesn't fully specify the operation for R-type instructions, since all R-type instructions typically share one opcode (e.g., `0`) and differ only in their `funct` field. An ALU control unit takes a coarser `ALUOp` signal from the main control unit (indicating "R-type," "load/store address," or "branch compare") together with the `funct` field, and produces the specific ALU operation code (add, subtract, AND, OR, set-less-than, etc.). This two-level decoding — main control then ALU control — keeps the main control logic simple while still supporting many distinct arithmetic and logical operations.
Why does the same ALU handle both arithmetic and address computation?
Reusing the ALU for both purposes is a deliberate hardware-economy decision, not a coincidence. Computing a load/store effective address (`base register + offset`) is structurally identical to computing `a + b` for an `add` instruction — both are 32-bit additions. By routing the same adder circuit through the ALUSrc multiplexer (choosing a register or a sign-extended immediate as the second operand), the datapath avoids building a second, redundant adder purely for address computation, saving substantial chip area at essentially no cost in cycle time, since the ALU was already on the critical path.
What happens to the datapath during a store instruction, and why doesn't it write to the register file?
For a store (e.g., `sw t0)`), the ALU computes the effective address exactly as in a load, using `ALUSrc=1`. But the control unit sets `MemWrite=1` and `RegWrite=0`: the value read from the register file's second port (`$t1`) is written into data memory at that address, and no register is modified at all. This matches the instruction's semantics — a store moves data from a register into memory, never the reverse — and the datapath's control-signal design ensures the shared register-write hardware simply stays inactive for this instruction class.
Could a single-cycle datapath support a variable-length instruction set, like x86?
Not cleanly, which is precisely why RISC-style fixed-length, fixed-field instruction encodings (see Instruction Formats and Encoding) are the natural partner for single-cycle design. A variable-length instruction requires first determining its length before you even know which fields to extract, introducing a serial decode step that resists the "everything decodes in parallel, combinationally" structure this datapath relies on. Complex variable-length ISAs are typically implemented with microcode or multi-cycle/pipelined front-ends specifically to absorb this variability, rather than a strict single-cycle model.
How does this datapath relate to control signals and the control unit itself?
This article deliberately treats control signals (RegDst, ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, ALUOp) as given inputs generated by a black-box control unit, focusing on how the datapath uses them to route data. The actual logic that derives these signals from the opcode and funct fields — whether as a truth table, PLA, or small combinational circuit — is the subject of Single-Cycle Control, which pairs directly with this datapath to form a complete single-cycle processor.
Why does pipelining eventually replace the single-cycle design if it's simpler and easier to reason about?
Simplicity and speed are in direct tension here. The single-cycle datapath's clock period must accommodate the slowest instruction's entire fetch-through-writeback path, so if a load takes, say, 800 picoseconds end-to-end, every instruction — even a one-cycle-worth-of-work `add` — must wait that full 800 picoseconds. Pipelining breaks this single long combinational path into several shorter stages separated by pipeline registers, shrinking the clock period to roughly the slowest individual stage rather than the slowest whole instruction, at the cost of needing hazard detection and forwarding logic, which Introduction to Pipelining introduces.