// retroDE_ps2 — iop_exec_stub // // First RAM-backed IOP execution primitive. Micro-op fetch now comes // from IOP RAM through the real `iop_memory_map_stub` CPU-side port — // the same way a future MIPS-class CPU would fetch instructions. The // control program is no longer RTL-resident; it lives as data in RAM // that someone (a TB, eventually a BIOS / loader path) preloads before // pulsing `go_i`. // // NOT a MIPS core, NOT an ISA decoder. A tiny FSM sequencer over a // five-opcode micro-op ISA, designed as the bridge between "testbench // choreographs everything" and a real instruction-fetching CPU. When // the real CPU arrives, it replaces this module but keeps the same // map / DMA / INTC hookup verbatim. // // Contract refs: // docs/contracts/iop.md (IOP-local programming model) // // Opcodes (encoded in word 0 low nibble): // OP_HALT 0x0 — terminal; halt_o rises, no further accesses. // OP_WRITE 0x1 — pulse map CPU write with (addr, data). pc++ // OP_READ 0x2 — pulse map CPU read; latch into last_read_data. // pc++ // OP_WAIT_IRQ 0x3 — block until cpu_irq==1. pc++ // OP_BNE 0x4 — if last_read_data != expected, pc <= target; // else pc++. // target is in word1[7:0]; expected is in word2. // // Micro-op layout in RAM (16 bytes per op, little-endian word order): // +0 word 0: {28'd0, opcode[3:0]} // +4 word 1: addr (for WRITE/READ) or target_pc in low 8 bits (for BNE) // +8 word 2: data (for WRITE) or expected value (for BNE); unused for // READ/WAIT_IRQ/HALT // +12 word 3: reserved for future opcodes // // Fetch sequence: three map reads per op (words 0/1/2). Word 3 is // skipped to save a cycle. Each read has one-cycle latency via the // map — so a full fetch is ~6 cycles, after which dispatch takes one // more cycle. Negligible in the current scope; swap the engine for a // real CPU later and the instruction width stops mattering. // // Trace payload (SUBSYS_IOP, EV_IFETCH, emitted on each op completion): // arg0 = pc value of the op that just completed // arg1 = opcode // arg2 = addr (0 for WAIT_IRQ/HALT) // arg3 = data written, data read back, expected (for BNE), or 0 // flags bit 0 = 1 → write-flavour op // flags bit 1 = 1 → WAIT_IRQ just exited (IRQ observed) // flags bit 2 = 1 → HALT entered // flags bit 3 = 1 → BNE branch taken (pc changed to target, not +1) `timescale 1ns/1ps module iop_exec_stub import trace_pkg::*; #( parameter logic [31:0] SCRIPT_BASE = 32'h0000_0400 ) ( input logic clk, input logic rst_n, input logic go_i, // Drive the IOP memory map's CPU-side port. Both ifetch reads and // the script's own WRITE/READ ops flow through here. output logic map_rd_en, output logic [31:0] map_rd_addr, input logic [31:0] map_rd_data, input logic map_rd_valid, output logic map_wr_en, output logic [31:0] map_wr_addr, output logic [31:0] map_wr_data, output logic [3:0] map_wr_be, input logic cpu_irq, output logic halt_o, output logic [7:0] pc_o, output logic ev_valid, output subsys_e ev_subsys, output event_e ev_event, output logic [63:0] ev_arg0, output logic [63:0] ev_arg1, output logic [63:0] ev_arg2, output logic [63:0] ev_arg3, output logic [31:0] ev_flags ); localparam logic [3:0] OP_HALT = 4'h0; localparam logic [3:0] OP_WRITE = 4'h1; localparam logic [3:0] OP_READ = 4'h2; localparam logic [3:0] OP_WAIT_IRQ = 4'h3; localparam logic [3:0] OP_BNE = 4'h4; typedef enum logic [3:0] { S_IDLE = 4'd0, S_IF0_REQ = 4'd1, S_IF0_WAIT = 4'd2, S_IF1_REQ = 4'd3, S_IF1_WAIT = 4'd4, S_IF2_REQ = 4'd5, S_IF2_WAIT = 4'd6, S_DECODE = 4'd7, S_WRITE = 4'd8, S_READ_REQ = 4'd9, S_READ_WAIT = 4'd10, S_WAIT_IRQ = 4'd11, S_BNE = 4'd12, S_HALT = 4'd13 } state_e; state_e state; logic [7:0] pc; logic [3:0] cur_opcode; logic [31:0] cur_addr; logic [31:0] cur_data; logic [31:0] last_read_data; // Op-completion event triggers (one-cycle pulses) logic ev_op_done; logic ev_wait_irq_exit; logic ev_enter_halt; logic ev_bne_taken; // Address for the next ifetch word: SCRIPT_BASE + pc*16 + word_offset logic [31:0] ifetch_base; assign ifetch_base = SCRIPT_BASE + {20'd0, pc, 4'd0}; // pc << 4 // ------------------------------------------------------------------ // Map-port drive (combinational on state) // ------------------------------------------------------------------ always_comb begin map_wr_en = 1'b0; map_wr_addr = 32'd0; map_wr_data = 32'd0; map_wr_be = 4'd0; map_rd_en = 1'b0; map_rd_addr = 32'd0; case (state) S_IF0_REQ: begin map_rd_en = 1'b1; map_rd_addr = ifetch_base + 32'd0; end S_IF1_REQ: begin map_rd_en = 1'b1; map_rd_addr = ifetch_base + 32'd4; end S_IF2_REQ: begin map_rd_en = 1'b1; map_rd_addr = ifetch_base + 32'd8; end S_WRITE: begin map_wr_en = 1'b1; map_wr_addr = cur_addr; map_wr_data = cur_data; map_wr_be = 4'b1111; end S_READ_REQ: begin map_rd_en = 1'b1; map_rd_addr = cur_addr; end default: ; endcase end // ------------------------------------------------------------------ // State machine // ------------------------------------------------------------------ always_ff @(posedge clk) begin if (!rst_n) begin state <= S_IDLE; pc <= 8'd0; cur_opcode <= 4'd0; cur_addr <= 32'd0; cur_data <= 32'd0; last_read_data <= 32'd0; ev_op_done <= 1'b0; ev_wait_irq_exit <= 1'b0; ev_enter_halt <= 1'b0; ev_bne_taken <= 1'b0; end else begin ev_op_done <= 1'b0; ev_wait_irq_exit <= 1'b0; ev_enter_halt <= 1'b0; ev_bne_taken <= 1'b0; case (state) S_IDLE: begin if (go_i) begin pc <= 8'd0; state <= S_IF0_REQ; end end S_IF0_REQ: state <= S_IF0_WAIT; S_IF0_WAIT: if (map_rd_valid) begin cur_opcode <= map_rd_data[3:0]; state <= S_IF1_REQ; end S_IF1_REQ: state <= S_IF1_WAIT; S_IF1_WAIT: if (map_rd_valid) begin cur_addr <= map_rd_data; state <= S_IF2_REQ; end S_IF2_REQ: state <= S_IF2_WAIT; S_IF2_WAIT: if (map_rd_valid) begin cur_data <= map_rd_data; state <= S_DECODE; end S_DECODE: begin case (cur_opcode) OP_HALT: begin state <= S_HALT; ev_enter_halt <= 1'b1; end OP_WRITE: state <= S_WRITE; OP_READ: state <= S_READ_REQ; OP_WAIT_IRQ: state <= S_WAIT_IRQ; OP_BNE: state <= S_BNE; default: state <= S_HALT; // unknown opcode → safe stop endcase end S_WRITE: begin pc <= pc + 8'd1; state <= S_IF0_REQ; ev_op_done <= 1'b1; end S_READ_REQ: state <= S_READ_WAIT; S_READ_WAIT: if (map_rd_valid) begin last_read_data <= map_rd_data; pc <= pc + 8'd1; state <= S_IF0_REQ; ev_op_done <= 1'b1; end S_WAIT_IRQ: begin if (cpu_irq) begin pc <= pc + 8'd1; state <= S_IF0_REQ; ev_op_done <= 1'b1; ev_wait_irq_exit <= 1'b1; end end S_BNE: begin // target_pc = cur_addr[7:0], expected = cur_data if (last_read_data != cur_data) begin pc <= cur_addr[7:0]; ev_bne_taken <= 1'b1; end else begin pc <= pc + 8'd1; end state <= S_IF0_REQ; ev_op_done <= 1'b1; end S_HALT: state <= S_HALT; default: state <= S_IDLE; endcase end end assign halt_o = (state == S_HALT); assign pc_o = pc; // ------------------------------------------------------------------ // Trace emission. One event per op completion + one on HALT entry. // ------------------------------------------------------------------ always_ff @(posedge clk) begin if (!rst_n) begin ev_valid <= 1'b0; ev_subsys <= SUBSYS_IOP; ev_event <= EV_IFETCH; ev_arg0 <= 64'd0; ev_arg1 <= 64'd0; ev_arg2 <= 64'd0; ev_arg3 <= 64'd0; ev_flags <= 32'd0; end else if (ev_enter_halt) begin ev_valid <= 1'b1; ev_subsys <= SUBSYS_IOP; ev_event <= EV_IFETCH; ev_arg0 <= {56'd0, pc}; ev_arg1 <= {60'd0, cur_opcode}; ev_arg2 <= 64'd0; ev_arg3 <= 64'd0; ev_flags <= 32'h0000_0004; // halt marker end else if (ev_op_done) begin ev_valid <= 1'b1; ev_subsys <= SUBSYS_IOP; ev_event <= EV_IFETCH; ev_arg0 <= {56'd0, pc}; ev_arg1 <= {60'd0, cur_opcode}; ev_arg2 <= {32'd0, cur_addr}; ev_arg3 <= (cur_opcode == OP_READ) ? {32'd0, map_rd_data} : {32'd0, cur_data}; ev_flags <= {28'd0, ev_bne_taken, 1'b0, // (was halt; halt has its own path above) ev_wait_irq_exit, (cur_opcode == OP_WRITE)}; end else begin ev_valid <= 1'b0; end end endmodule : iop_exec_stub