Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)

RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 20:10:50 -04:00
commit ec82764bef
2462 changed files with 2174303 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# rtl/dmac
EE DMAC. Matches `docs/contracts/dmac.md`.
## Wave 2 / Wave 2.5 contents
- `dmac_reg_stub.sv` — channel-2-focused register shell + single-transfer
state machine. Wave 2.5 revision is memory-backed: DMAC now issues real
memory reads via the `mem_rd_*` port (connected directly to
`ee_ram_stub` in the current topology; routing through
`ee_memory_map_stub` is deferred). State flow: IDLE → FETCH_WAIT →
ACTIVE_SEND → DONE. MADR is the real fetch source address.
See `docs/wave25_memory_backed_dma_plan.md`.
**EE-core chapter 3** added a CPU write path: the EE memory map's
new `ee_dmac_ch2_wr_*` port drives `reg_wr_en` / `reg_offset` /
`reg_wr_data`, so the EE core can program MADR/QWC/CHCR from a
MIPS bootstrap via `SW`.
**EE-core chapter 4** added a CPU read path (`reg_rd_en` /
`reg_rd_data` / `reg_rd_valid`, 1-cycle latency) plus a DONE_COUNT
monotonic counter at offset 0x40. CHCR/MADR/QWC/TADR read back
their stored values; DONE_COUNT increments each time the state
machine enters S_DONE. The EE map forwards CPU reads in the same
DMAC window through a new `ee_dmac_ch2_rd_*` pair, so software
can now poll CHCR.start or compare DONE_COUNT before/after a
transfer without needing INTC.
## Explicit non-goals (Wave 2 / 2.5)
- Multi-channel arbitration or fairness.
- Chain mode (normal / chain / interleaved transfer modes).
- Stall / ring / suspend semantics.
- Interrupt routing to INTC.
- QWC > 1 multi-beat transfers (state machine is shaped for it; initial
signoff is QWC == 1 per Wave 2.5 plan).
- Routing through `ee_memory_map_stub` (current topology is direct to
`ee_ram_stub`).
Each of these is a future-wave concern, not a stub-plan shortcut.
+355
View File
@@ -0,0 +1,355 @@
// retroDE_ps2 — dmac_reg_stub
//
// EE DMAC stub. Channel-agnostic: the module's behaviour is generic across
// PS2 DMA channels and downstream endpoints. The specific channel and path
// id are set via parameters; the downstream endpoint wires (ep_*) are
// valid/data/last/ready regardless of what consumer is connected. Current
// uses: CHANNEL=2 (GIF path), CHANNEL=5 (SIF0 path).
//
// Payload source: memory-backed via the `mem_rd_*` master port, typically
// routed through `ee_memory_map_stub` to `ee_ram_stub`. MADR is the real
// fetch source address.
//
// Contract refs:
// docs/stub_module_plan.md (Wave 2, item 8)
// docs/wave2_dma_gif_plan.md (Wave 2 scope)
// docs/wave25_memory_backed_dma_plan.md (Wave 2.5 scope — THIS REVISION)
// docs/contracts/dmac.md
//
// Register surface (single channel, selected by CHANNEL parameter):
// offset 0x00 CHCR — start bit at [0], other bits recorded
// offset 0x10 MADR — real fetch source address (Wave 2.5)
// offset 0x20 QWC — transfer length in 128-bit qwords (first sign-off
// path requires QWC == 1; state machine is QWC-
// generic for a future Wave 2.6 extension)
// offset 0x30 TADR — recorded for future chain-mode use
// offset 0x40 DONE_COUNT — monotonic completion counter (read-only;
// writes are accepted but ignored). Software reads
// this to distinguish "nth completion" without
// counting interrupts externally. EE-core chapter 4
// addition; mirrors iop_dmac_reg_stub's DONE_COUNT
// but at a new slot (0x0C is occupied on the IOP
// stub; EE stub's 16-byte register spacing puts
// DONE_COUNT at 0x40).
//
// Register reads (EE-core chapter 4, added alongside the original write
// surface): reg_rd_en / reg_rd_data / reg_rd_valid with 1-cycle latency,
// matching the rest of the stub ecosystem. All four config registers plus
// DONE_COUNT are readable; all other offsets return 0.
//
// Memory master interface (to ee_ram_stub in Wave 2.5):
// mem_rd_en / mem_rd_addr drive the request
// mem_rd_valid / mem_rd_data return data one cycle later
//
// Downstream endpoint: ep_{valid,data,last,ready}. The port names are
// channel-agnostic because the DMAC's behaviour is generic across PS2
// channels (ch2 = GIF, ch5 = SIF0, etc.). Connect the endpoint side to
// whichever consumer matches the instantiated CHANNEL/PATH_ID.
//
// State machine:
// IDLE → FETCH_WAIT on CHCR start
// FETCH_WAIT → ACTIVE_SEND on mem_rd_valid (data latched)
// ACTIVE_SEND → FETCH_WAIT on endpoint accept with more beats pending
// → DONE on endpoint accept for the final beat
// DONE → IDLE next cycle (clears CHCR.start)
//
// Trace payload schemas (per wave25_memory_backed_dma_plan.md):
// DMAC DMA_CFG arg0=channel arg1=chcr arg2=madr arg3=qwc
// flags=reg_offset (which reg was written)
// DMAC DMA_START arg0=channel arg1=qwc arg2=MADR arg3=path_id
// DMAC DMA_BEAT arg0=channel arg1=beat arg2=src_addr arg3=remaining
// DMAC DMA_DONE arg0=channel arg1=beats arg2=completion arg3=path_id
// completion code: 0 = OK
`timescale 1ns/1ps
module dmac_reg_stub
import trace_pkg::*;
#(
parameter logic [3:0] CHANNEL = 4'd2,
parameter logic [3:0] PATH_ID = 4'd2
) (
input logic clk,
input logic rst_n,
// CPU / testbench register write port (single-channel, see CHANNEL).
// reg_offset is shared by read and write; callers must not assert both
// enables in the same cycle (the map ensures this because the EE CPU
// emits either rd or wr per transaction, never both).
input logic reg_wr_en,
input logic [7:0] reg_offset,
input logic [31:0] reg_wr_data,
// Register read port (EE-core chapter 4). 1-cycle latency.
input logic reg_rd_en,
output logic [31:0] reg_rd_data,
output logic reg_rd_valid,
// Memory master (Wave 2.5) — direct link to ee_ram_stub in this phase.
// Future waves will route this through ee_memory_map_stub.
output logic mem_rd_en,
output logic [31:0] mem_rd_addr,
input logic [127:0] mem_rd_data,
input logic mem_rd_valid,
// Downstream to gif_path_stub
output logic ep_valid,
output logic [127:0] ep_data,
output logic ep_last,
input logic ep_ready,
// Completion pulse — one cycle high when the transfer reaches S_DONE.
// Intended as an INTC source; level-held bit latching happens in the
// interrupt controller, not here.
output logic irq_completion_o,
// Trace
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 [7:0] CHCR_OFFSET = 8'h00;
localparam logic [7:0] MADR_OFFSET = 8'h10;
localparam logic [7:0] QWC_OFFSET = 8'h20;
localparam logic [7:0] TADR_OFFSET = 8'h30;
localparam logic [7:0] DONE_COUNT_OFFSET = 8'h40;
// ------------------------------------------------------------------
// Register file (ch2 only)
// ------------------------------------------------------------------
logic [31:0] chcr;
logic [31:0] madr;
logic [31:0] qwc;
logic [31:0] tadr;
logic [31:0] done_count;
logic start_pulse;
assign start_pulse = reg_wr_en && (reg_offset == CHCR_OFFSET) &&
reg_wr_data[0] && !chcr[0];
// Single owner for the config regs: software writes win over the
// S_DONE auto-clear on CHCR[0] in the unlikely same-cycle case
// (the NBA queue lets the case-statement full-width assign
// override the partial bit-0 clear). Software writing CHCR while
// the DMA is completing is not part of any sane flow, so this
// ordering is defensive — the point is: chcr has one procedural
// driver, not two.
always_ff @(posedge clk) begin
if (!rst_n) begin
chcr <= 32'd0;
madr <= 32'd0;
qwc <= 32'd0;
tadr <= 32'd0;
end else begin
if (state == S_DONE) chcr[0] <= 1'b0;
if (reg_wr_en) begin
case (reg_offset)
CHCR_OFFSET: chcr <= reg_wr_data;
MADR_OFFSET: madr <= reg_wr_data;
QWC_OFFSET: qwc <= reg_wr_data;
TADR_OFFSET: tadr <= reg_wr_data;
default: ;
endcase
end
end
end
// DONE_COUNT: monotonic completion counter. Increments on S_DONE
// entry. Reset-only clear path; writes at the DONE_COUNT offset are
// silently dropped by the write always_ff above (read-only register).
always_ff @(posedge clk) begin
if (!rst_n) done_count <= 32'd0;
else if (state == S_DONE) done_count <= done_count + 32'd1;
end
// Register read (1-cycle latency, matches rest of stub ecosystem).
always_ff @(posedge clk) begin
if (!rst_n) begin
reg_rd_data <= 32'd0;
reg_rd_valid <= 1'b0;
end else begin
reg_rd_valid <= reg_rd_en;
if (reg_rd_en) begin
case (reg_offset)
CHCR_OFFSET: reg_rd_data <= chcr;
MADR_OFFSET: reg_rd_data <= madr;
QWC_OFFSET: reg_rd_data <= qwc;
TADR_OFFSET: reg_rd_data <= tadr;
DONE_COUNT_OFFSET: reg_rd_data <= done_count;
default: reg_rd_data <= 32'd0;
endcase
end
end
end
// ------------------------------------------------------------------
// Transfer state machine
// ------------------------------------------------------------------
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_FETCH_WAIT = 2'd1,
S_ACTIVE_SEND = 2'd2,
S_DONE = 2'd3
} state_e;
state_e state;
logic [31:0] madr_latched;
logic [31:0] qwc_latched;
logic [31:0] beat_index;
logic [127:0] beat_payload;
logic [31:0] src_addr;
assign src_addr = madr_latched + (beat_index << 4); // beat * 16 bytes
logic beat_accepted;
assign beat_accepted = ep_valid && ep_ready;
// Pulse mem_rd_en for one cycle whenever we first enter FETCH_WAIT.
logic prev_state_fw;
always_ff @(posedge clk) begin
if (!rst_n) prev_state_fw <= 1'b0;
else prev_state_fw <= (state == S_FETCH_WAIT);
end
logic entering_fw;
assign entering_fw = (state == S_FETCH_WAIT) && !prev_state_fw;
assign mem_rd_en = entering_fw;
assign mem_rd_addr = src_addr;
// Drive endpoint only in ACTIVE_SEND with the latched payload.
assign ep_valid = (state == S_ACTIVE_SEND);
assign ep_data = beat_payload;
assign ep_last = (state == S_ACTIVE_SEND) &&
(beat_index + 32'd1 == qwc_latched);
assign irq_completion_o = (state == S_DONE);
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= S_IDLE;
madr_latched <= 32'd0;
qwc_latched <= 32'd0;
beat_index <= 32'd0;
beat_payload <= 128'd0;
end else begin
unique case (state)
S_IDLE: begin
if (start_pulse) begin
// start_pulse is gated by reg_wr_en && reg_offset ==
// CHCR_OFFSET, so a same-cycle QWC write is
// structurally impossible through this interface.
// Latch the currently-visible register state.
state <= S_FETCH_WAIT;
madr_latched <= madr;
qwc_latched <= qwc;
beat_index <= 32'd0;
end
end
S_FETCH_WAIT: begin
if (mem_rd_valid) begin
beat_payload <= mem_rd_data;
state <= S_ACTIVE_SEND;
end
end
S_ACTIVE_SEND: begin
if (beat_accepted) begin
if (beat_index + 32'd1 == qwc_latched) begin
state <= S_DONE;
end else begin
beat_index <= beat_index + 32'd1;
state <= S_FETCH_WAIT;
end
end
end
S_DONE: begin
state <= S_IDLE;
// chcr[0] auto-clear on S_DONE now lives in the
// register-ownership always_ff above (single
// procedural driver for chcr).
end
default: state <= S_IDLE;
endcase
end
end
// ------------------------------------------------------------------
// Trace emission — one event per cycle; priority:
// DONE pulse > BEAT accept > START on transition > CFG on write
// ------------------------------------------------------------------
logic prev_state_fetch_or_later;
always_ff @(posedge clk) begin
if (!rst_n) prev_state_fetch_or_later <= 1'b0;
else prev_state_fetch_or_later <= (state != S_IDLE);
end
logic enter_start; // transitioning from IDLE into the transfer
assign enter_start = (state == S_FETCH_WAIT) && !prev_state_fetch_or_later;
logic enter_done;
assign enter_done = (state == S_DONE);
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_CFG;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (enter_done) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_DONE;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, beat_index + 32'd1}; // beats completed
ev_arg2 <= 64'd0; // completion: OK
ev_arg3 <= {60'd0, PATH_ID};
ev_flags <= 32'd0;
end else if (beat_accepted) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_BEAT;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, beat_index};
ev_arg2 <= {32'd0, src_addr}; // this beat's source
ev_arg3 <= {32'd0, qwc_latched - beat_index - 32'd1};
ev_flags <= 32'd0;
end else if (enter_start) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_START;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, qwc_latched};
ev_arg2 <= {32'd0, madr_latched}; // MADR is the source
ev_arg3 <= {60'd0, PATH_ID};
ev_flags <= 32'd0;
end else if (reg_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_DMA_CFG;
ev_arg0 <= {60'd0, CHANNEL};
ev_arg1 <= {32'd0, (reg_offset == CHCR_OFFSET) ? reg_wr_data : chcr};
ev_arg2 <= {32'd0, (reg_offset == MADR_OFFSET) ? reg_wr_data : madr};
ev_arg3 <= {32'd0, (reg_offset == QWC_OFFSET) ? reg_wr_data : qwc};
ev_flags <= {24'd0, reg_offset};
end else begin
ev_valid <= 1'b0;
end
end
endmodule : dmac_reg_stub
+177
View File
@@ -0,0 +1,177 @@
// retroDE_ps2 — ee_dmac_ctrl_stub
//
// Ch287 — EE DMAC global control/status registers at
// 0x1000_E000..0x1000_E0FF (256 bytes). NOT the per-channel registers
// (those live in dmac_reg_stub at 0x1000_A000+ for channel 2; per-
// channel registers for other channels are not modelled yet).
//
// Surface modelled here (R5900 DMAC global):
// offset 0x00 D_CTRL — DMAC enable / cycle-stealing / RELE / etc.
// Latched write, read returns last-written.
// offset 0x10 D_STAT — Per-channel interrupt status (CIS) + per-
// channel interrupt mask (CIM) + stall / MEIS.
// Read returns current latch (reset = 0 = no
// pending interrupts). Writes are W1C against
// the CIS/MEIS half (bits where write_data has
// a 1 are cleared); CIM half is NOT W1C — bits
// are unconditionally written. Real R5900
// splits the word: bits[15:0] = CIS (W1C), bits
// [31:16] = CIM (write). With nothing in the
// stub yet setting bits, qbert sees "no
// interrupts pending" on every read, which is
// exactly the wait-for-quiet pattern its init
// loop polls for.
// offset 0x20 D_PCR — Per-channel priority + W1C enables. Latched
// write, read returns last-written.
// offset 0x30 D_SQWC — Stall/skip cycles. Latched.
// offset 0x40 D_RBSR — Ring-buffer size. Latched.
// offset 0x50 D_RBOR — Ring-buffer base. Latched.
// any other offset — write traced + dropped; read returns 0.
//
// Codex framing: "If the hot PC is truly a D_STAT poll, read-as-zero
// may or may not be the right 'ready' value. Let the next run tell us.
// If it still loops, the next chapter should decode the branch
// condition and choose the exact D_STAT bit semantics, not guess the
// whole region." The implementation honors that — every offset has
// minimal-sufficient behavior; future chapters can refine specific
// bits once a real ELF surfaces a divergence.
//
// Port interface mirrors the dmac_reg_stub / intc_stub conventions:
// reg_wr_en / reg_offset / reg_wr_data : write port
// reg_rd_en / reg_offset / reg_rd_data / reg_rd_valid : read port,
// 1-cycle latency
// trace_pkg::* : ev_* events tagged SUBSYS_DMAC + EV_READ/EV_WRITE
// with arg0 = offset, arg1 = data.
`timescale 1ns/1ps
module ee_dmac_ctrl_stub
import trace_pkg::*;
(
input logic clk,
input logic rst_n,
// Write port (single-cycle, shared offset with read).
input logic reg_wr_en,
input logic [7:0] reg_offset,
input logic [31:0] reg_wr_data,
// Read port (1-cycle latency).
input logic reg_rd_en,
output logic [31:0] reg_rd_data,
output logic reg_rd_valid,
// Trace
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 [7:0] D_CTRL_OFFSET = 8'h00;
localparam logic [7:0] D_STAT_OFFSET = 8'h10;
localparam logic [7:0] D_PCR_OFFSET = 8'h20;
localparam logic [7:0] D_SQWC_OFFSET = 8'h30;
localparam logic [7:0] D_RBSR_OFFSET = 8'h40;
localparam logic [7:0] D_RBOR_OFFSET = 8'h50;
// ------------------------------------------------------------------
// Register file
// ------------------------------------------------------------------
logic [31:0] d_ctrl;
logic [31:0] d_stat; // CIS in low half (W1C), CIM in high half (W)
logic [31:0] d_pcr;
logic [31:0] d_sqwc;
logic [31:0] d_rbsr;
logic [31:0] d_rbor;
always_ff @(posedge clk) begin
if (!rst_n) begin
d_ctrl <= 32'd0;
d_stat <= 32'd0;
d_pcr <= 32'd0;
d_sqwc <= 32'd0;
d_rbsr <= 32'd0;
d_rbor <= 32'd0;
end else if (reg_wr_en) begin
unique case (reg_offset)
D_CTRL_OFFSET: d_ctrl <= reg_wr_data;
D_STAT_OFFSET: begin
// W1C on the low half (interrupt-status bits): a 1
// in reg_wr_data clears that bit; a 0 leaves it.
// Direct-write on the high half (mask bits).
d_stat[15:0] <= d_stat[15:0] & ~reg_wr_data[15:0];
d_stat[31:16] <= reg_wr_data[31:16];
end
D_PCR_OFFSET: d_pcr <= reg_wr_data;
D_SQWC_OFFSET: d_sqwc <= reg_wr_data;
D_RBSR_OFFSET: d_rbsr <= reg_wr_data;
D_RBOR_OFFSET: d_rbor <= reg_wr_data;
default: ; // unknown offsets: write dropped (traced)
endcase
end
end
// Read mux (1-cycle latency to match the stub ecosystem).
always_ff @(posedge clk) begin
if (!rst_n) begin
reg_rd_data <= 32'd0;
reg_rd_valid <= 1'b0;
end else begin
reg_rd_valid <= reg_rd_en;
if (reg_rd_en) begin
unique case (reg_offset)
D_CTRL_OFFSET: reg_rd_data <= d_ctrl;
D_STAT_OFFSET: reg_rd_data <= d_stat;
D_PCR_OFFSET: reg_rd_data <= d_pcr;
D_SQWC_OFFSET: reg_rd_data <= d_sqwc;
D_RBSR_OFFSET: reg_rd_data <= d_rbsr;
D_RBOR_OFFSET: reg_rd_data <= d_rbor;
default: reg_rd_data <= 32'd0;
endcase
end
end
end
// ------------------------------------------------------------------
// Trace — one event per cycle, write priority over read (consistent
// with the rest of the stub ecosystem).
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_WRITE;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (reg_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_WRITE;
ev_arg0 <= {56'd0, reg_offset};
ev_arg1 <= {32'd0, reg_wr_data};
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'h0000_0001; // write
end else if (reg_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_READ;
ev_arg0 <= {56'd0, reg_offset};
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : ee_dmac_ctrl_stub
+181
View File
@@ -0,0 +1,181 @@
// retroDE_ps2 — ee_dmac_passive_chan_stub
//
// Ch288 — Lightweight per-channel register surface for the EE DMAC
// channels NOT covered by a dedicated transfer-FSM stub. Hosts the
// four standard per-channel registers (CHCR/MADR/QWC/TADR) for each
// covered channel; reset to zero, writes latch, reads return the
// latched value. NO transfer FSM, NO start-bit side effects, NO
// D_STAT interaction. This is the "init-time channel-clear / quiet
// register surface" Codex framed for Ch288.
//
// Channels covered (4 KiB window each, starting at 0x1000_8000):
// ch0 (VIF0) 0x1000_8000-0x1000_8FFF
// ch1 (VIF1) 0x1000_9000-0x1000_9FFF
// ch3 (IPU_FROM) 0x1000_B000-0x1000_BFFF
// ch4 (IPU_TO) 0x1000_C000-0x1000_CFFF ← qbert's first hit
// ch5 (SIF0) 0x1000_D000-0x1000_DFFF
//
// SKIPPED:
// ch2 (GIF) 0x1000_A000-0x1000_AFFF — already routed
// externally to dmac_reg_stub via the map's
// ee_dmac_ch2_* ports. Do NOT shadow it here.
//
// Channel index extracted from chan_addr[15:12]:
// 0x8 → ch0, 0x9 → ch1, 0xB → ch3, 0xC → ch4, 0xD → ch5
// (0xA / ch2 is filtered by the caller; if chan_addr[15:12]==0xA
// arrives here the module silently drops it.)
//
// Register offsets (chan_addr[11:0], matches dmac_reg_stub layout):
// 0x00 CHCR — control (start bit at [0]); latched, no FSM
// 0x10 MADR — main address
// 0x20 QWC — quadword count
// 0x30 TADR — tag address
// any other offset: read = 0, write dropped + traced
`timescale 1ns/1ps
module ee_dmac_passive_chan_stub
import trace_pkg::*;
(
input logic clk,
input logic rst_n,
// Write port. chan_addr is the 16-bit offset into the entire
// 0x1000_8000-base window: chan_addr[15:12] = channel selector,
// chan_addr[11:0] = register offset within that channel.
input logic reg_wr_en,
input logic [15:0] chan_addr,
input logic [31:0] reg_wr_data,
// Read port (1-cycle latency).
input logic reg_rd_en,
output logic [31:0] reg_rd_data,
output logic reg_rd_valid,
// Trace
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 [11:0] CHCR_OFFSET = 12'h000;
localparam logic [11:0] MADR_OFFSET = 12'h010;
localparam logic [11:0] QWC_OFFSET = 12'h020;
localparam logic [11:0] TADR_OFFSET = 12'h030;
// Channel index from the high nibble of chan_addr. Out-of-range
// nibbles (anything outside 0x8/0x9/0xB/0xC/0xD) get
// chan_valid=0 and the access is dropped.
logic [3:0] chan_nibble;
logic [2:0] chan_idx; // 0..4 packed: 0=ch0, 1=ch1, 2=ch3, 3=ch4, 4=ch5
logic chan_valid;
always_comb begin
chan_nibble = chan_addr[15:12];
chan_idx = 3'd0;
chan_valid = 1'b0;
unique case (chan_nibble)
4'h8: begin chan_idx = 3'd0; chan_valid = 1'b1; end // ch0
4'h9: begin chan_idx = 3'd1; chan_valid = 1'b1; end // ch1
4'hB: begin chan_idx = 3'd2; chan_valid = 1'b1; end // ch3
4'hC: begin chan_idx = 3'd3; chan_valid = 1'b1; end // ch4
4'hD: begin chan_idx = 3'd4; chan_valid = 1'b1; end // ch5
default: ;
endcase
end
logic [11:0] reg_offset;
assign reg_offset = chan_addr[11:0];
// ------------------------------------------------------------------
// Register file: 5 channels × 4 registers
// ------------------------------------------------------------------
logic [31:0] chcr [0:4];
logic [31:0] madr [0:4];
logic [31:0] qwc [0:4];
logic [31:0] tadr [0:4];
always_ff @(posedge clk) begin
if (!rst_n) begin
for (int i = 0; i < 5; i++) begin
chcr[i] <= 32'd0;
madr[i] <= 32'd0;
qwc[i] <= 32'd0;
tadr[i] <= 32'd0;
end
end else if (reg_wr_en && chan_valid) begin
unique case (reg_offset)
CHCR_OFFSET: chcr[chan_idx] <= reg_wr_data;
MADR_OFFSET: madr[chan_idx] <= reg_wr_data;
QWC_OFFSET: qwc[chan_idx] <= reg_wr_data;
TADR_OFFSET: tadr[chan_idx] <= reg_wr_data;
default: ;
endcase
end
end
// Read mux (1-cycle latency). Returns 0 for invalid channel /
// unknown offset.
always_ff @(posedge clk) begin
if (!rst_n) begin
reg_rd_data <= 32'd0;
reg_rd_valid <= 1'b0;
end else begin
reg_rd_valid <= reg_rd_en;
if (reg_rd_en && chan_valid) begin
unique case (reg_offset)
CHCR_OFFSET: reg_rd_data <= chcr[chan_idx];
MADR_OFFSET: reg_rd_data <= madr[chan_idx];
QWC_OFFSET: reg_rd_data <= qwc[chan_idx];
TADR_OFFSET: reg_rd_data <= tadr[chan_idx];
default: reg_rd_data <= 32'd0;
endcase
end else if (reg_rd_en) begin
reg_rd_data <= 32'd0; // invalid channel
end
end
end
// ------------------------------------------------------------------
// Trace — write priority over read; tagged SUBSYS_DMAC with
// arg0 = chan_nibble (0x8/0x9/0xB/0xC/0xD = phys channel), arg1
// = data, arg2 = reg_offset, arg3 = chan_idx (packed 0..4).
// ------------------------------------------------------------------
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_WRITE;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (reg_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_WRITE;
ev_arg0 <= {60'd0, chan_nibble};
ev_arg1 <= {32'd0, reg_wr_data};
ev_arg2 <= {52'd0, reg_offset};
ev_arg3 <= {61'd0, chan_idx};
ev_flags <= {31'd0, chan_valid};
end else if (reg_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_DMAC;
ev_event <= EV_READ;
ev_arg0 <= {60'd0, chan_nibble};
ev_arg1 <= 64'd0;
ev_arg2 <= {52'd0, reg_offset};
ev_arg3 <= {61'd0, chan_idx};
ev_flags <= {31'd0, chan_valid};
end else begin
ev_valid <= 1'b0;
end
end
endmodule : ee_dmac_passive_chan_stub