4.2 Single-Cycle CPU Data Path
ISA defines what results an instruction must produce. This lesson builds a simplified teaching CPU supporting only a few RV32I instructions, focusing on control signals and data paths, rather than replicating modern processor architectures.
One Instruction Uses One Long Cycle
A single-cycle datapath contains a register file, an ALU, instruction and data-memory interfaces, and multiplexers that select each path. The goal is to complete one instruction, from fetch through its final result, within a single clock cycle. At the end of the cycle, the PC, destination register, or memory state updates according to the instruction's semantics.
The conceptual flow can be represented as:
+---------------- control ----------------+
| |
PC -> instruction memory -> fields -> register file -> ALU -> result MUX
| | | | |
+-> PC + 4 / branch target | immediate data memory |
+------------------------------->|The instruction memory and data memory are separated in this diagram to allow fetching an instruction and accessing data memory to happen simultaneously within the same cycle. In practice, this behavior can be achieved through independent cache ports that feed into a unified memory hierarchy; the instruction set architecture (ISA) does not require physical separation of two distinct memory chips.
A single instruction progressing through fetch, decode, execute, memory access, and write-back represents a data-dependency order; it does not imply that a single-cycle CPU requires five separate clock cycles. Combinational logic propagates across the circuit during a cycle, and all state is committed at the rising edge of the clock.
The Core Components of the Data Path
| Component | Reads | Produces |
|---|---|---|
| PC register | Current instruction address | Fetch address |
| Instruction memory | PC | 32-bit instruction |
| Main control and ALU control | opcode, funct fields | MUX selection and write enable signals |
| Immediate generator | Dispersed immediate fields | Sign-extended 32-bit value |
| Register file | rs1, rs2, rd | Two read values; one write value available at end of cycle |
| ALU | Two selected operands | Arithmetic result and comparison condition |
| Data memory interface | Address, write data, byte enable | Load data or write effect |
| next-PC MUX | PC+4, branch/jump target | Next PC |
| result MUX | ALU result, load data, PC+4, etc. | Value written back to rd |
The RV32I x0 requires special handling: register file read ports return zero for register 0, and writes to address zero must be discarded. Hardware enforces this. However, lw x0, ... discards only the register result: the access, exceptions, and side effects still occur. rd=0 is not permission to cancel the instruction.
add: Trace the Register-to-ALU Path
Start with add x1, x2, x3. It doesn't access data memory and doesn't require complex immediate values; it's ideal to walk through the fetch, register read, ALU, and write-back stages first, before gradually adding lw, sw, and branch paths.
The path of add x1, x2, x3 is:
PC -> Fetch instruction -> Read x2/x3 -> ALU addition -> result MUX -> Write x1
PC -> Add 4 -> next-PC MUX -> Write back PCControl logic identifies the OP opcode and, in combination with funct3=000, funct7=0000000, selects ADD. RegWrite=1, the ALU second operand comes from rs2, and the write-back source is the ALU.
Register file reads are typically viewed as combinational reads, while writes occur on the cycle edge. Actual port timing in real implementations is determined by process and microarchitecture.
lw: After Address Computation, Read Data
lw x1, 8(x2) depends on two combined operations:
Read x2 + generate sign-extended immediate 8
-> ALU computes effective address x2+8
-> Read 32-bit data from memory
-> result MUX
-> Write x1The second input to the ALU is switched to an immediate value, and the write source is changed to memory data. In RV32I, lw writes a 32-bit value into a 32-bit register; in RV64I, lw sign-extends the 32-bit result to XLEN, and lwu performs zero extension.
Address access may also trigger unaligned access, permission violations, page faults, or bus errors. Simplified one-cycle diagrams often depict memory as a fixed-delay combinational block, hiding exceptions and variable delays for simplicity. In reality, a cache miss cannot be modeled by stretching a single physical clock cycle indefinitely.
sw: No rd and No Register Write
sw x3, 8(x2) uses rs1 to store the base address and rs2 to hold the data to be written. The S-type immediate value is split across the high and low portions of the instruction:
x2 + sign_extend(8) -> write address
x3 -> write data
MemWrite -> permits corresponding byte lanes to update at the end of the cycleRegWrite=0, the result MUX value is irrelevant. The "don't care" entry in the control table at X does not mean that the hardware input can float; the synthesizer may choose a convenient value as long as the observable behavior remains unchanged.
beq: Select the Next PC from a Comparison
beq x1, x2, target compares two registers. If they are equal:
next_pc = current_pc + sign_extend(branch_immediate)Otherwise, execute instructions sequentially per next_pc = current_pc + 4. The B-type offset has an implicit zero low bit and is relative to the current branch instruction, not PC+4; RV32 address addition wraps at 32 bits. Encodable offsets are multiples of two bytes, but this model has no compressed instructions and requires four-byte instruction alignment. A taken branch to a misaligned target raises an instruction-address-misaligned exception on the branch; a branch not taken does not raise that exception for its unused target.
A simple datapath can perform the subtraction and check the zero flag using the ALU, or it can use a dedicated comparator. The implementation choice is not part of the ISA specification.
In an ideal single-cycle model, a taken branch does not consume an additional cycle because all instructions inherently occupy one long cycle. In a pipelined processor, however, a control hazard arises, and whether the branch is correctly predicted determines whether instructions already fetched at the front end are valid or need to be flushed.
A Deliberately Simplified Control Table
The following table covers only add, lw, sw, and beq:
| Instruction | RegWrite | ALUSrc | MemWrite | ResultSrc | Branch | ALUControl |
|---|---|---|---|---|---|---|
add | 1 | rs2 | 0 | ALU | 0 | ADD |
lw | 1 | imm | 0 | memory | 0 | ADD |
sw | 0 | imm | 1 | X | 0 | ADD |
beq | 0 | rs2 | 0 | X | 1 | compare equal |
In reality, the control logic must also account for load/store width, signedness, branch conditions, exceptions, CSR accesses, extended instructions, and write masks. The simpler the table, the more important it is to clearly define the subset of instructions it supports.
The controller can be layered: a main decoder generates broad signal categories based on the opcode, while an ALU decoder further selects the specific operation by combining funct3, funct7, and the instruction category. This approach enables logic reuse and provides a centralized location for handling invalid encodings.
Why Immediate Value Generators Need Reordering
RISC-V places register fields in fixed positions, enabling early instruction decoding and register file access. As a result, the immediate number bits from different instructions are scattered across the instruction stream. The decoder uses fixed wiring to recombine these bits and performs sign extension starting from bit 31.
def sign_extend(value: int, width: int) -> int:
sign = 1 << (width - 1)
return (value ^ sign) - sign
def decode_i_immediate(instruction: int) -> int:
return sign_extend((instruction >> 20) & 0xFFF, 12)
def decode_s_immediate(instruction: int) -> int:
encoded = ((instruction >> 25) << 5) | ((instruction >> 7) & 0x1F)
return sign_extend(encoded & 0xFFF, 12)
def decode_b_immediate(instruction: int) -> int:
encoded = (
(((instruction >> 31) & 0x1) << 12)
| (((instruction >> 7) & 0x1) << 11)
| (((instruction >> 25) & 0x3F) << 5)
| (((instruction >> 8) & 0xF) << 1)
)
return sign_extend(encoded, 13)Put both Python blocks in this section into one immediates.py file and run it. Decoders assume an unsigned 32-bit instruction word; sign_extend assumes positive width and a value already limited to that width. These helpers extract fields without validating the opcode. Hardware uses wiring, sign extension, and multiplexers rather than executing software shifts.
Round-Trip Testing of Decoder Against Encoder
When testing immediate values, positive, negative, and boundary cases must all be covered:
def encode_i_immediate(value: int) -> int:
if not -2048 <= value <= 2047:
raise ValueError("immediate outside signed 12-bit range")
return (value & 0xFFF) << 20
def encode_s_immediate(value: int) -> int:
if not -2048 <= value <= 2047:
raise ValueError("immediate outside signed 12-bit range")
encoded = value & 0xFFF
return ((encoded >> 5) << 25) | ((encoded & 0x1F) << 7)
def encode_b_immediate(value: int) -> int:
if not (-4096 <= value <= 4094 and value % 2 == 0):
raise ValueError("branch offset must be even and in [-4096, 4094]")
encoded = value & 0x1FFF
return (
(((encoded >> 12) & 0x1) << 31)
| (((encoded >> 11) & 0x1) << 7)
| (((encoded >> 5) & 0x3F) << 25)
| (((encoded >> 1) & 0xF) << 8)
)
for immediate in (-2048, -1, 0, 1, 2047):
assert decode_i_immediate(encode_i_immediate(immediate)) == immediate
assert decode_s_immediate(encode_s_immediate(immediate)) == immediate
for immediate in (-4096, -2, 0, 2, 4094):
assert decode_b_immediate(encode_b_immediate(immediate)) == immediateRound-trip testing can reveal wiring errors such as bit 11 and bit 12 being swapped, but matching mistakes in both directions can cancel out. Also compare with the specification’s bit layouts or an independent assembler. Immediate round trips cannot establish correct opcode checks, register fields, or illegal-instruction behavior. A complete processor validation also employs an ISA reference model, random instruction streams, formal properties, and compliance tests.
Clock Cycle Limited by the Slowest Legal Path
A single-cycle CPU must complete all supported instructions within the same clock cycle budget. If lw passes sequentially through the instruction fetch memory, register file, ALU, data memory, and write-back MUX, it often becomes a long candidate for the critical path; however, "load is necessarily the slowest" ultimately depends on the multiplier, branch logic, memory implementation, and physical routing.
The lower bound of the cycle is roughly composed of:
source state clock-to-Q
+ slowest combinational/memory path
+ destination state setup time
+ clock uncertaintyEven short instructions must wait until this unified cycle completes, and hardware resources are typically not reusable across different stages within the same cycle. This is precisely why single-cycle designs are easy to understand but unsuitable for high-performance general-purpose processors.
Multicycle and Pipelined Designs Address Different Costs
| Organization Style | Time per Instruction | Number of Instructions in Flight | Key Trade-offs |
|---|---|---|---|
| Single-cycle | 1 long cycle | 1 | Intuitive control flow; cycle time determined by the slowest path |
| Multi-cycle | Multiple shorter cycles | Typically 1 | Reusable ALU and memory ports; different instructions take varying numbers of cycles |
| Pipeline | Multiple stages | Multiple instructions | Higher throughput, but introduces data, control, and structural hazards |
A pipeline does not guarantee lower latency for one instruction. It overlaps different instructions in different stages to improve ideal steady-state throughput; stalls, dependencies, and branches reduce that gain. Chapter 15 examines those effects.
What the Teaching Diagram Leaves Out
- Cache misses and main memory latency are not fixed combinational delays; they vary based on system conditions and access patterns.
- Exceptions must prevent unintended writebacks and preserve recoverable architectural state.
- Peripheral memory accesses can have side effects and cannot be safely replayed like ordinary RAM.
- Multiply, divide, floating-point, and vector units may take multiple cycles or be pipelined.
- Modern CPUs perform prediction, out-of-order execution, and register renaming, yet must still maintain the software-visible behavior allowed by the instruction set architecture (ISA).
- Self-modifying code, memory consistency, and privilege state require additional synchronization rules.
The value of a single-cycle processor model is not to represent the "true essence" of a CPU, but to provide a clear, linear path for explaining every effect defined by the ISA.
Hands-on Trace Four Paths
- On the diagram, mark which components are shared between
addandlw, and identify which MUX selects differently. - Derive the fields of the S-type immediate value
-16and verify them using a decoding function. - Explain why
swneeds to read two registers, yet has nordfield. - Add
PC+4write-back and jump target paths forjal, and list the new MUX selections introduced. - Design an "illegal instruction exception" for the controller when encountering an unknown opcode, rather than silently executing the default ADD operation.
- Assuming component delays, compute the paths for
add,lw,sw, andbeq, and identify the critical path.
The Next Bottleneck Is Not in the ALU
The data path can now execute the minimal instruction set. The next challenge (fetching instructions and loading data) will face the same reality: the speed of processor computation vastly outpaces access to large-capacity storage. The next chapter examines Caching and Memory Hierarchy, beginning with locality, cache lines, and miss types.