Rodolfo Azevedo
Institute of Computing, University of Campinas (UNICAMP), Brazil
rodolfo.azevedo@unicamp.br
http://www.ic.unicamp.br/~rodolfo/mo801
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.
enum
always_comb
case
At the end of this class, you should be able to:
typedef enum
always_ff
state <= state_next
state <= IDLE
FETCH
state_next
state
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;
JAL
JALR
LUI
AUIPC
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
instruction_mem[PC]
IR
PC+4
PC
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
logic [31:0] regs[32]
struct
opcode_t'(instr.opcode)
DECODE
ADD
SUB
EXEC_R
WRITEBACK_ALU
rs1 op rs2
rd
ADDI
EXEC_I
rs1 op imm
BEQ
EXEC_BRANCH
rs1
rs2
PC <= PC + imm
LW
MEM_ADDR
MEM_READ
WRITEBACK_MEM
MDR -> rd
SW
MEM_WRITE
rs1 + imm
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_read
mem_write
reg_write
MDR
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
zero
alu1
BNE
BLT
alu_zero
alu_result
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 = 1
rs2_val
data_mem
WRITEBACK
The minimal FSM handles R-type, I-type ALU, loads, stores, and branches. Now add JAL rd, imm:
JAL rd, imm
PC + imm
PC + 4
Exercise (15 min):
alu_src_a
alu_src_b
pc_write
result_src
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.
result_src = PC_PLUS_4
ALUOUT
When your FSM "almost works":
check_result
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.
ebreak