4.2 Single-Cycle CPU Data Path
Master Chen connects scattered components from the engine room into a single diagram, challenging you to design a data path that completes one instruction within a single cycle.
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 cycle means "one instruction, one long cycle"
At the heart of the engine lies a register file, an ALU, two memory interfaces, and a row of multiplexers. The goal is to complete a single instruction (from fetch to producing all final results) within one clock cycle. At the end of the cycle, the program counter (PC), destination registers, or memory state is updated 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, this 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. This behavior is not enforced by software.
add: Let one instruction pass through the engine first
Put add x3, x1, x2 at the entry point. 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, 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: Selecting next PC Based on Comparison
beq x1, x2, target Compare 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 least significant bit of a B-type offset is implicitly zero, and the target address is relative to the current branch instruction, not to the address already incremented by 4.
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)This represents a software decoding model; in hardware, it corresponds to fixed wiring, sign extension, and multiplexing, no individual shift instructions are executed.
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:
assert -2048 <= value <= 2047
return (value & 0xFFF) << 20
def encode_s_immediate(value: int) -> int:
assert -2048 <= value <= 2047
encoded = value & 0xFFF
return ((encoded >> 5) << 25) | ((encoded & 0x1F) << 7)
def encode_b_immediate(value: int) -> int:
assert -4096 <= value <= 4094 and value % 2 == 0
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 it cannot alone verify that opcode, register fields, or illegal instruction handling are correctly implemented. 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.
Multi-cycle and Pipeline Approaches to Addressing Different Forms of Waste
| Organization Style | Instructions Per Cycle | 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 that an individual instruction has lower latency. Instead, it operates like an assembly line, with different instructions occupying different stages simultaneously. In steady state, this approach improves instruction throughput. Chapter 15 will delve into hazards, forwarding, stalls, and prediction.
What the Teaching Pipeline Hides
- Cache misses and main memory latency are not fixed combinations; 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, whereasrddoes not. - 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 delves into Caching and Memory Hierarchy, beginning with locality, cache lines, and miss types.