Instruction Cache
Controller Architecture
The instruction cache controller (instruction_cache_controller.sv) manages the fetch path between the CPU and the DDR2 memory via the shared 128-bit Wishbone arbiter. It wraps the cache_memory_subsystem module and adds the hit/miss evaluation, miss handling, and pipeline handshake logic.
flowchart LR
subgraph CPU
FETCH["Fetch Stage"]
end
subgraph ICACHE["ICache Controller"]
EVAL["Hit/Miss Eval"]
MEM["Cache Memory Arrays"]
FSM["FSM<br/>IDLE→EVAL→FETCH"]
end
subgraph DDR2_PATH["Memory Subsystem"]
ARB["128-bit Arbiter"]
BRIDGE["MIG Bridge"]
DDR2["DDR2 SDRAM"]
end
FETCH --> EVAL
EVAL --> MEM
MEM --> FETCH
EVAL --> FSM
FSM --> ARB
ARB --> BRIDGE
BRIDGE --> DDR2
DDR2 --> BRIDGE
BRIDGE --> ARB
ARB --> FSM
State Machine
The controller operates in three states:
stateDiagram-v2
IDLE --> EVALUATE: cpu_bus.cyc && cpu_bus.stb
EVALUATE --> IDLE: !cpu_bus.cyc (CPU abort)
EVALUATE --> IDLE: cache_hit (update LRU)
EVALUATE --> FETCH_DDR: cache_miss
FETCH_DDR --> EVALUATE: mem_bus.ack (line fetched)
FETCH_DDR --> IDLE: mem_bus.err (error)
IDLE
Waits for a valid CPU request. The Wishbone signals cyc and stb indicate the fetch stage is requesting an instruction at the current adr. On receiving both, the controller transitions to EVALUATE.
EVALUATE
In this state, the memory arrays have already been read (they are combinational/inferred-BRAM on the previous cycle). The hit/miss logic compares the tag from cpu_bus.adr[31:11] against both ways’ stored tags:
assign current_tag = { {(32-TAG_BITS){1'b0}}, cpu_bus.adr[31 : 32-TAG_BITS] };
assign hit_way0 = valid_out_way0 && (tag_out_way0 == current_tag);
assign hit_way1 = valid_out_way1 && (tag_out_way1 == current_tag);
assign cache_hit = hit_way0 | hit_way1;
On hit: The controller selects the correct 32-bit word from the 128-bit line based on cpu_bus.adr[1:0], asserts cpu_bus.ack, updates the LRU state, and returns to IDLE for the next request:
case (cpu_bus.adr[1:0])
2'b00: cpu_bus.dat_miso = active_line[31:0];
2'b01: cpu_bus.dat_miso = active_line[63:32];
2'b10: cpu_bus.dat_miso = active_line[95:64];
2'b11: cpu_bus.dat_miso = active_line[127:96];
endcase
On miss: The controller initiates a 128-bit read on the memory bus, aligned to the 16-byte boundary of the requested address:
mem_bus.adr <= {cpu_bus.adr[31:2], 2'b00};
mem_bus.cyc <= 1'b1;
mem_bus.stb <= 1'b1;
mem_bus.we <= 1'b0;
mem_bus.sel <= 16'hFFFF;
FETCH_DDR
Waits for the DDR2 access to complete. The memory bus is monitored for ack (success) or err (error):
- On
ack: The 128-bit data from DDR2 is written into the evicted cache way (determined bylru_evict_way), the tag is updated, and the valid bit is set. The controller returns to EVALUATE to re-read from cache (which now contains the requested line). - On
err: The error is forwarded to the CPU, and the controller returns to IDLE.
The fill writes the full 128-bit line:
write_data <= mem_bus.dat_miso;
if (lru_evict_way == 1'b0) we_way0 <= 1'b1;
else we_way1 <= 1'b1;
state <= STATE_EVALUATE;
Interrupt Handling
Between IDLE and EVALUATE, if the CPU de-asserts cpu_bus.cyc (indicating an interrupt or pipeline flush), the controller aborts the pending access and returns to IDLE. This prevents stale cache operations after a control flow change, which otherwise led to hard faults, as the first instruction of the context switch code within the interrupt handler was being dropped.
Hit Latency
A cache hit completes in a single cycle: the address is presented in EVALUATE, the data is available at the end of the same cycle, and ack is returned to the CPU.