Skip to content

VOL 03 / CH 04 / LESSON 02

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:

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; 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 ​

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

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

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: Select the Next PC from a Comparison ​

beq x1, x2, target compares 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 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:

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)

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:

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

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

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.

Multicycle and Pipelined Designs Address Different Costs ​

Organization StyleTime per InstructionNumber 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 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 ​

  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, yet has no rd field.
  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 examines Caching and Memory Hierarchy, beginning with locality, cache lines, and miss types.

References ​

Built with VitePress | Software Systems Atlas