Skip to content

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:

text
                  +---------------- 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

ComponentReadsProduces
PC registerCurrent instruction addressFetch address
Instruction memoryPC32-bit instruction
Main control and ALU controlopcode, funct fieldsMUX selection and write enable signals
Immediate generatorDispersed immediate fieldsSign-extended 32-bit value
Register filers1, rs2, rdTwo read values; one write value available at end of cycle
ALUTwo selected operandsArithmetic result and comparison condition
Data memory interfaceAddress, write data, byte enableLoad data or write effect
next-PC MUXPC+4, branch/jump targetNext PC
result MUXALU 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:

text
PC -> Fetch instruction -> Read x2/x3 -> ALU addition -> result MUX -> Write x1
PC -> Add 4 -> next-PC MUX -> Write back PC

Control 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:

text
Read x2 + generate sign-extended immediate 8
        -> ALU computes effective address x2+8
        -> Read 32-bit data from memory
        -> result MUX
        -> Write x1

The 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:

text
x2 + sign_extend(8) -> write address
x3                  -> write data
MemWrite            -> permits corresponding byte lanes to update at the end of the cycle

RegWrite=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:

text
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:

InstructionRegWriteALUSrcMemWriteResultSrcBranchALUControl
add1rs20ALU0ADD
lw1imm0memory0ADD
sw0imm1X0ADD
beq0rs20X1compare 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.

python
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:

python
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)) == immediate

Round-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.

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:

text
source state clock-to-Q
+ slowest combinational/memory path
+ destination state setup time
+ clock uncertainty

Even 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 StyleInstructions Per CycleNumber of Instructions in FlightKey Trade-offs
Single-cycle1 long cycle1Intuitive control flow; cycle time determined by the slowest path
Multi-cycleMultiple shorter cyclesTypically 1Reusable ALU and memory ports; different instructions take varying numbers of cycles
PipelineMultiple stagesMultiple instructionsHigher 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

  1. On the diagram, mark which components are shared between add and lw, and identify which MUX selects differently.
  2. Derive the fields of the S-type immediate value -16 and verify them using a decoding function.
  3. Explain why sw needs to read two registers, whereas rd does not.
  4. Add PC+4 write-back and jump target paths for jal, and list the new MUX selections introduced.
  5. Design an "illegal instruction exception" for the controller when encountering an unknown opcode, rather than silently executing the default ADD operation.
  6. Assuming component delays, compute the paths for add, lw, sw, and beq, 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.

Built with VitePress | Software Systems Atlas