Designing the Control FSM

Rodolfo Azevedo

Institute of Computing, University of Campinas (UNICAMP), Brazil

rodolfo.azevedo@unicamp.br

http://www.ic.unicamp.br/~rodolfo/mo801

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Goal of this class

Module 2, Class 3: the state machine that drives every signal in the datapath, cycle by cycle.

Last class introduced the datapath's components and the control signals that steer them. Today we build the FSM that sets those signals - the enum + always_comb + case pattern from M01A02, scaled up to a real processor.

At the end of this class, you should be able to:

  • Design the control FSM that drives every datapath signal across fetch, decode, execute, memory, and writeback states.
  • Write the two-process control FSM in SystemVerilog using typedef enum and a case statement over the current state.
  • Map each FSM state to the correct set of control signal values for the corresponding datapath operation.
  • Handle instruction-type branching (R, I, load, store, branch, JAL, LUI, AUIPC) within the control FSM.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

The shape of the FSM

  • One state per cycle type - not per instruction. Many instructions share the same early states (every instruction fetches and decodes the same way) and diverge only in their last 1-3 states.
  • Two processes, exactly as in M01A02's FSM example:
    • always_ff: state <= state_next (and state <= IDLE/FETCH on reset).
    • always_comb: state_next and every control signal from M02A02's table, as a function of state (and, in later states, opcode/funct3).
typedef enum logic [3:0] {
    FETCH, DECODE,
    EXEC_R, EXEC_I, EXEC_BRANCH,
    MEM_ADDR, MEM_READ, MEM_WRITE,
    WRITEBACK_ALU, WRITEBACK_MEM
} state_t;
  • This is one reasonable state set for a minimal RV32I subset - your Project 1 FSM will likely need a few more (e.g., for JAL/JALR, LUI/AUIPC), but the shape - shared early states, diverging late states - stays the same.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

FETCH state — datapath activated

FETCH step active in the datapath

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

State 1: FETCH

FETCH: begin
    state_next = DECODE;
    -- control signals --
    mem_read    = 1'b1;   // read instruction memory at PC
    ir_write    = 1'b1;   // latch the result into IR
    alu_src_a   = ALU_SRC_A_PC;
    alu_src_b   = ALU_SRC_B_4;
    alu_op      = ALU_ADD;  // compute PC+4 ...
    pc_write    = 1'b1;     // ... and store it back into PC
end
  • Every instruction starts here, identically: read instruction_mem[PC] into IR, and simultaneously compute PC+4 using the ALU (reusing the mux pattern from M02A02) and write it back to PC.
  • Computing PC+4 speculatively in the fetch cycle - before we even know what the instruction is - is a classic multicycle trick: branches/jumps will overwrite PC again later if needed, but the common case (sequential execution) is ready immediately.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

State 2: DECODE

DECODE: begin
    -- read rs1/rs2 from the register file (combinational, no control bit needed) --
    -- reassemble immediate from IR (M02A01) --
    case (opcode)
        OPCODE_OP:      state_next = EXEC_R;
        OPCODE_OP_IMM:  state_next = EXEC_I;
        OPCODE_BRANCH:  state_next = EXEC_BRANCH;
        OPCODE_LOAD,
        OPCODE_STORE:   state_next = MEM_ADDR;
        default:        state_next = FETCH;   // unimplemented - or trap
    endcase
end
  • Register reads are typically combinational (no clock edge needed to read a register file modeled as logic [31:0] regs[32]) - so "decode" mostly means computing state_next from opcode.
  • This is where the struct/enum machinery from M01A02's "tiny instruction decoder" example earns its keep: opcode_t'(instr.opcode) feeds directly into this case.
  • This is the fan-out point: every instruction passes through FETCH -> DECODE identically, then diverges based on opcode.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Complete multicycle FSM

Complete multicycle control FSM

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

States 3+: divergence by instruction type

Path States What happens
R-type (ADD, SUB, ...) EXEC_R -> WRITEBACK_ALU ALU computes rs1 op rs2; result -> rd
I-type ALU (ADDI, ...) EXEC_I -> WRITEBACK_ALU ALU computes rs1 op imm; result -> rd
Branch (BEQ, ...) EXEC_BRANCH ALU compares rs1, rs2; if taken, PC <= PC + imm (computed in DECODE or here)
Load (LW) MEM_ADDR -> MEM_READ -> WRITEBACK_MEM ALU computes address; memory read; MDR -> rd
Store (SW) MEM_ADDR -> MEM_WRITE ALU computes address; memory write from rs2
  • EXEC_BRANCH needs no write-back state - it either updates PC or doesn't, and returns to FETCH.
  • MEM_ADDR is shared by loads and stores - both need rs1 + imm computed by the ALU - and only then diverge into read vs. write.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Worked example: LW rd, imm(rs1)

The 5-cycle load path — the longest in the base ISA:

MEM_ADDR: begin
    state_next = MEM_READ;    // (for load) or MEM_WRITE (for store)
    alu_src_a  = ALU_SRC_A_RS1;
    alu_src_b  = ALU_SRC_B_IMM;
    alu_op     = ALU_ADD;     // compute address: rs1 + imm
end

MEM_READ: begin
    state_next = WRITEBACK_MEM;
    mem_read   = 1'b1;        // read data_mem at ALUOUT address
    // MDR latches the result automatically (see datapath)
end

WRITEBACK_MEM: begin
    state_next = FETCH;
    reg_write  = 1'b1;
    result_src = RESULT_MDR;  // write MDR (loaded word) into rd
end

Key observations:

  • MEM_ADDR does not set mem_read or mem_write — it only computes the address.
  • MEM_READ does not set reg_write — it only triggers the memory read into MDR.
  • Separating address computation from memory access from write-back ensures each cycle does exactly one unit of work and the signals are clean.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

A worked example: BEQ rs1, rs2, imm

EXEC_BRANCH: begin
    state_next  = FETCH;
    alu_src_a   = ALU_SRC_A_RS1;
    alu_src_b   = ALU_SRC_B_RS2;
    alu_op      = ALU_SUB;          // rs1 - rs2; zero flag <=> equal
    if (funct3 == 3'b000 && alu_zero) begin   // BEQ, and rs1 == rs2
        pc_write   = 1'b1;
        -- PC <= branch target (computed from imm in DECODE) --
    end
end
  • The ALU's zero output (recall alu1's zero signal from M01A01!) is exactly what a BEQ needs: subtract, then check if the result is zero.
  • funct3 distinguishes BEQ/BNE/BLT/... - all branches share EXEC_BRANCH, just with different conditions on alu_zero/alu_result and funct3.
  • No rd is written - branches don't produce a register result, only (conditionally) a new PC.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Worked example: SW rs2, imm(rs1)

Store shares MEM_ADDR with load, then diverges:

MEM_ADDR: begin
    // same as LW up to here — compute address rs1 + imm
    state_next = (opcode == OPCODE_STORE) ? MEM_WRITE : MEM_READ;
    alu_src_a  = ALU_SRC_A_RS1;
    alu_src_b  = ALU_SRC_B_IMM;
    alu_op     = ALU_ADD;
end

MEM_WRITE: begin
    state_next = FETCH;
    mem_write  = 1'b1;          // write rs2 to data_mem[ALUOUT]
    // reg_write stays 0 — stores never write to the register file
end
  • MEM_WRITE is the only state where mem_write = 1. Writing it there (not in MEM_ADDR) keeps each state's semantics clean.
  • rs2_val must be routed to data_mem write data — this is a separate datapath connection, not controlled by the ALU mux.
  • SW takes 4 cycles, one fewer than LW, because there is no WRITEBACK state — memory is the destination, not a register.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

In-class exercise — extending the FSM

The minimal FSM handles R-type, I-type ALU, loads, stores, and branches. Now add JAL rd, imm:

  • JAL computes PC + imm (the jump target) and writes PC + 4 (the return address) into rd.
  • It needs the ALU twice — or the PC+4 value that was already computed in FETCH.

Exercise (15 min):

  1. How many cycles does JAL need? Sketch the state sequence.
  2. Which existing states can be reused, and which need a new state?
  3. Fill in the control signals for the new state(s): what does alu_src_a, alu_src_b, pc_write, reg_write, result_src each hold?
  4. Why does JALR (jump-and-link register, target = rs1 + imm) need slightly different handling than JAL?

Hint: JAL can reuse DECODE to compute PC + imm (the ALU is free in DECODE — registers are read combinationally). The write-back can then look like a WRITEBACK_ALU variant where result_src = PC_PLUS_4 rather than ALUOUT.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Cycle count per instruction

Instruction Cycles States visited
ADDI 4 FETCH, DECODE, EXEC_I, WRITEBACK_ALU
ADD 4 FETCH, DECODE, EXEC_R, WRITEBACK_ALU
BEQ 3 FETCH, DECODE, EXEC_BRANCH
LW 5 FETCH, DECODE, MEM_ADDR, MEM_READ, WRITEBACK_MEM
SW 4 FETCH, DECODE, MEM_ADDR, MEM_WRITE
  • This table is exactly what you'll measure (and report) in Project 1 - and it's the starting point for the cycle-count baselines in the reference implementation (System v0).
  • Notice the trade-off from the start of M02A02: BEQ finishes in 3 cycles because it needs no write-back; LW needs 5 because it chains address computation, memory access, and write-back.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Debugging the FSM: a checklist

When your FSM "almost works":

  • Every state assigns every control signal - use the same "default at the top of always_comb" discipline from M01A02's accidental-latch warning, applied to a dozen+ signals instead of one.
  • state_next has a path out of every state - a state that never transitions is an infinite loop (often visible in GTKWave as state stuck at one value forever).
  • Trace one instruction at a time in GTKWave (M01A03): does state visit exactly the states in the table above, in order, for a single ADDI?
  • Directed tests first (M01A03): one test per row of the cycle-count table, checking both the result (check_result) and, optionally, the cycle count (compare against the table).
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Next class

Verification, Toolchain & Project 1 Kickoff: putting together a full Verilator testbench for the FSM + datapath, compiling C programs to RV32I with ebreak as a termination convention, synthesis/timing on the Tang Nano 9K, and Project 1's milestones.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0