The Z-Core implements a 5-stage in-order stalling pipeline for the RV32IM instruction set. This document describes the complete pipeline architecture, data flow, hazard handling, memory interface, and implementation details.
graph LR
IF["IF\nInstruction Fetch"] --> ID["ID\nDecode"]
ID --> EX["EX\nExecute"]
EX --> MEM["MEM\nMemory"]
MEM --> WB["WB\nWriteback"]
style IF fill:#e1f5fe
style ID fill:#fff3e0
style EX fill:#e8f5e9
style MEM fill:#fce4ec
style WB fill:#f3e5f5
| Stage | Function | Key Operations | Latency |
|---|---|---|---|
| IF | Instruction Fetch | Read instruction via AXI-Lite, manage PC, handle fetch buffer | 10-11 cycles (AXI) |
| ID | Instruction Decode | Decode opcode, read register file, generate immediates, detect hazards | 1 cycle |
| EX | Execute | ALU operations, branch condition evaluation, target calculation | 1 cycle |
| MEM | Memory Access | Load/Store via AXI-Lite, byte/halfword extraction | 10-11 cycles (AXI) |
| WB | Writeback | Write results to register file | 1 cycle |
Important
Some of the diagrams may not be up to date with the current implementation and the instruction cache.
The IF stage manages instruction fetching with these key components:
graph TD
PC[Program Counter] --> ARBITER[Memory Arbiter]
ARBITER --> AXI[AXI Master]
AXI --> |mem_rdata| FETCH_CTRL[Fetch Controller]
FETCH_CTRL --> |Direct| IF_ID[IF/ID Register]
FETCH_CTRL --> |Buffered| BUFFER[Fetch Buffer]
BUFFER --> IF_ID
FLUSH[Flush Signal] --> |"PC ← target"| PC
FLUSH --> |invalidate| IF_ID
FLUSH --> |invalidate| BUFFER
Key Features:
- Fetch Buffer: Decouples fetch from pipeline stalls. If IF/ID is busy, fetched instruction goes to buffer.
- PC Tracking:
fetch_pcregister captures PC when fetch starts, ensuring correct PC association even if flush occurs mid-fetch. - Flush Handling: On flush, both IF/ID and fetch buffer are invalidated, PC is redirected to target.
Key Signals:
| Signal | Description |
|---|---|
fetch_wait |
Fetch operation in progress |
fetch_pc |
PC value for current fetch (captured at start) |
fetch_buffer_valid |
Fetch buffer contains valid instruction |
if_id_valid |
IF/ID register contains valid instruction |
The ID stage decodes instructions and handles data hazard detection:
graph LR
IF_ID[IF/ID Register] --> DECODER[Decoder]
DECODER --> IMM[Immediate Generator]
DECODER --> CTRL[Control Signals]
IF_ID --> REG[Register File Read]
REG --> FWD[Forwarding Mux]
EX_MEM[EX/MEM] --> FWD
MEM_WB[MEM/WB] --> FWD
FWD --> ID_EX[ID/EX Register]
IMM --> ID_EX
CTRL --> ID_EX
Forwarding to Decode: The decode stage can receive forwarded data for immediate use in ID/EX:
dec_fwd_rs1 = (ex_mem_valid && ex_mem_reg_write && ex_mem_rd == dec_rs1 && dec_rs1 != 0)
? ex_mem_alu_result :
(mem_wb_valid && mem_wb_reg_write && mem_wb_rd == dec_rs1 && mem_wb_rd != 0)
? mem_wb_result :
rf_rs1_data;The EX stage performs ALU operations, evaluates branches, and handles division:
graph TD
ID_EX[ID/EX Register] --> ALU_MUX1[ALU Input 1 Mux]
ID_EX --> ALU_MUX2[ALU Input 2 Mux]
ALU_MUX1 --> ALU[ALU]
ALU_MUX2 --> ALU
ALU_MUX1 --> DIV[Division Unit]
ALU_MUX2 --> DIV
ALU --> RESULT_MUX[Result Mux]
DIV --> |div_result| RESULT_MUX
ID_EX --> |PC+4| RESULT_MUX
ID_EX --> |Immediate| RESULT_MUX
ID_EX --> BRANCH[Branch Evaluator]
BRANCH --> |taken?| FLUSH[Flush Control]
RESULT_MUX --> EX_MEM[EX/MEM Register]
ID_EX --> |Target| EX_MEM
ALU Input Selection:
alu_in1 = (LUI) ? 0
: (AUIPC) ? id_ex_pc
: fwd_rs1_data
alu_in2 = (I-type|Load|Store|LUI|AUIPC|Jump) ? immediate
: fwd_rs2_data
Result Selection:
ex_result = (LUI) ? immediate
: (AUIPC) ? pc + immediate
: (JAL|JALR) ? pc + 4 (return address)
: (DIV/REM) ? div_result (division/remainder)
: alu_out
The MEM stage handles load/store operations with byte/halfword support:
graph TD
EX_MEM[EX/MEM Register] --> ADDR[Address]
EX_MEM --> WDATA[Write Data]
ADDR --> ARBITER[Memory Arbiter]
WDATA --> AXI[AXI Master]
ARBITER --> AXI
AXI --> |mem_rdata| EXTRACT[Byte/Halfword Extraction]
EXTRACT --> |Combinational| MEM_WB[MEM/WB Register]
EX_MEM --> |ALU Result| MEM_WB
Load Data Extraction (Combinational):
always @* begin
case (ex_mem_funct3)
3'b000: mem_load_data = {{24{mem_rdata[byte_pos+7]}}, mem_rdata[...]}; // LB
3'b001: mem_load_data = {{16{mem_rdata[hw_pos+15]}}, mem_rdata[...]}; // LH
3'b010: mem_load_data = mem_rdata; // LW
3'b100: mem_load_data = {24'b0, mem_rdata[...]}; // LBU
3'b101: mem_load_data = {16'b0, mem_rdata[...]}; // LHU
endcase
endImportant:
mem_load_datais combinational (not registered) to ensure the correct data is available to WB on the same cyclemem_readygoes high.
The WB stage writes results back to the register file:
// Register file write condition
assign write_enable = mem_wb_valid && mem_wb_reg_write &&
mem_wb_rd != 5'b0;
assign write_data = mem_wb_result;
assign write_addr = mem_wb_rd;graph TD
subgraph MEM_WB["MEM/WB Register"]
D1["result: 32-bit"]
D2["pc: 32-bit (for debug)"]
D3["rd: 5-bit"]
D4["reg_write: 1-bit"]
D5["valid: 1-bit"]
end
subgraph EX_MEM["EX/MEM Register"]
C1["pc: 32-bit"]
C2["alu_result: 32-bit"]
C3["rs2_data: 32-bit (for stores)"]
C4["rd: 5-bit"]
C5["funct3: 3-bit"]
C6["is_load, is_store, is_div, reg_write"]
C7["valid: 1-bit"]
end
subgraph ID_EX["ID/EX Register"]
B1["pc: 32-bit"]
B2["rs1_data, rs2_data: 32-bit"]
B3["imm: 32-bit"]
B4["rd, rs1_addr, rs2_addr: 5-bit"]
B5["alu_op: 5-bit"]
B6["funct3: 3-bit"]
B7["is_load, is_store, is_branch, is_jal, is_jalr"]
B8["is_lui, is_auipc, is_r_type, is_i_alu, is_div"]
B9["reg_write: 1-bit"]
B10["valid: 1-bit"]
end
subgraph IF_ID["IF/ID Register"]
A1["pc: 32-bit"]
A2["ir: 32-bit instruction"]
A3["valid: 1-bit"]
end
The pipeline implements full forwarding from EX/MEM and MEM/WB stages:
graph LR
subgraph "EX Stage"
EX_RS1[rs1 needed]
EX_RS2[rs2 needed]
end
subgraph "Forward Sources"
EX_MEM[EX/MEM result]
MEM_WB[MEM/WB result]
end
EX_MEM --> |Priority 1| EX_RS1
MEM_WB --> |Priority 2| EX_RS1
EX_MEM --> |Priority 1| EX_RS2
MEM_WB --> |Priority 2| EX_RS2
Forwarding Logic (EX Stage):
fwd_rs1_data = (ex_mem_valid && ex_mem_reg_write && ex_mem_rd == id_ex_rs1_addr && ex_mem_rd != 0)
? ex_mem_alu_result :
(mem_wb_valid && mem_wb_reg_write && mem_wb_rd == id_ex_rs1_addr && mem_wb_rd != 0)
? mem_wb_result :
id_ex_rs1_data;Critical Fix:
mem_wb_reg_writemust be explicitly cleared when the pipeline stalls (inserting a bubble). Otherwise, stalled, invalid instructions in the pipeline could be interpreted as valid writes, causing incorrect forwarding of "ghost" data.
When a load is followed by an instruction that uses the loaded value:
LW x2, 0(x1) ; Load into x2
ADD x3, x2, x4 ; Uses x2 - STALL required!
Detection:
load_use_hazard = id_ex_valid && id_ex_is_load && if_id_valid &&
((id_ex_rd == dec_rs1 && dec_rs1 != 0) ||
(id_ex_rd == dec_rs2 && dec_rs2 != 0 && (dec_is_r_type || dec_is_store || dec_is_branch)));Action: Stall IF, ID for 1 cycle; insert bubble in ID/EX.
When a branch is taken or jump executes:
sequenceDiagram
participant IF
participant ID
participant EX
Note over EX: Branch evaluated
EX->>IF: Flush! PC ← target
EX->>ID: Insert bubble
Note over IF: Start fetch from target
Flush Actions:
if_id_valid <= 0(invalidate delay slot)if_id_ir <= NOP(safe instruction)fetch_buffer_valid <= 0(clear buffer)PC <= next_pc(redirect to target)fetch_wait <= 0(cancel in-flight fetch)- Insert bubble in ID/EX stage
The pipeline uses a single AXI-Lite master for both instruction fetch and data access:
graph TD
subgraph "Memory Arbiter"
direction TB
FETCH[Fetch Request] --> |Priority 2| ARB{Arbiter}
DATA[Load/Store Request] --> |Priority 1| ARB
end
ARB --> AXI[AXI-Lite Master]
AXI --> INTERCONNECT[AXI Interconnect]
INTERCONNECT --> RAM[RAM]
INTERCONNECT --> UART[UART]
INTERCONNECT --> GPIO[GPIO]
INTERCONNECT --> TIMER[TIMER]
Arbiter Priority:
always @* begin
if (mem_op_pending && !mem_ready) begin
// Data access has priority
mem_req = 1; mem_addr = ex_mem_alu_result;
end else if (fetch_wait && !mem_ready) begin
// Instruction fetch
mem_req = 1; mem_addr = fetch_pc;
end else begin
mem_req = 0;
end
endstateDiagram-v2
[*] --> IDLE
IDLE --> READ_ADDR: mem_req && !mem_wen
IDLE --> WRITE_ADDR: mem_req && mem_wen
READ_ADDR --> READ_DATA: arready
READ_DATA --> READ_DONE: rvalid
READ_DONE --> IDLE: (assert mem_ready)
WRITE_ADDR --> WRITE_DATA: awready
WRITE_DATA --> WRITE_RESP: wready
WRITE_RESP --> IDLE: bvalid
Note: The
READ_DONEstate ensuresmem_rdatais stable beforemem_readyis asserted. This prevents timing issues where the consumer reads stale data.
Cycle: 1 2 3 4 5 6 7 8 9 10 11 12
|----AXI Read Transaction (10 cycles)-----|
IDLE ADDR DATA ........................... DONE IDLE
↑
mem_ready
mem_rdata stable
Cycle: 1 2 3 4 5 6 7 ...
I1: IF ID EX MEM WB
I2: IF ID EX MEM WB
I3: IF ID EX MEM WB
Cycle: 1----11 12 13 14 15 16----23 24 25 26 27
I1: IF ID EX MEM WB
I2: IF ID EX MEM WB
| Signal | Description |
|---|---|
stall |
Freezes IF, ID stages (load-use hazard or EX stall) |
flush |
Invalidates IF/ID, redirects PC (branch taken / jump) |
squash_now |
Set for one cycle after JAL/JALR enters ID/EX |
mem_stall |
Stalls when load/store in progress |
div_stall |
Stalls when division instruction is executing |
ex_stall |
Stalls EX when MEM is busy, waiting for AXI, or division in progress |
| Signal | Description |
|---|---|
fetch_wait |
Fetch operation in progress |
fetch_pc |
Captured PC for current fetch |
mem_op_pending |
Load/Store operation in progress |
mem_busy |
AXI master not in IDLE state |
mem_ready |
Memory operation complete, data valid |
| Signal | Description |
|---|---|
*_valid |
Stage contains valid instruction to process |
- CPI (ideal): 1 cycle per instruction (ideally, depends on code locality and memory access patterns)
- Branch Penalty: 2-11 cycles (flush + re-fetch from cache or memory)
- Load Penalty: 10 cycles for memory access
- Forwarding: Zero-cycle penalty for RAW hazards (except load-use)
- RISC-V Specification
- Patterson & Hennessy, "Computer Organization and Design: RISC-V Edition"