Files
retroDE_ps2/sim/tb/top/tb_hdmi_i2c_wake_smoke.sv
T
thejayman77 ec82764bef 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>
2026-06-29 20:10:50 -04:00

527 lines
22 KiB
Systemverilog
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// retroDE_ps2 — tb_hdmi_i2c_wake_smoke (Ch165 Medium + Ch166 NACK watchdog + Ch167 byte-sequence lock)
//
// Focused, accelerated bring-up TB for the Ch165 ADV7513 I²C
// wake-up FSM (`I2C_HDMI_Config` → `I2C_Controller`). The
// existing Ch149 board TB only smoke-tests the wrapper and
// can't observe the LUT walk because at the production I²C
// clock divider (50 MHz / 20 kHz = 2500) the full 38-entry
// walk takes ~125 ms simulated (controller-clock period
// ~100 µs × 33 phases per byte × 38 bytes) — far longer
// than the 5 ms board-TB runtime. This TB instantiates
// `I2C_HDMI_Config` directly with a tiny `CLK_Freq / I2C_Freq`
// ratio so the LUT walks in microseconds, then exercises:
// Phase 1 — `LUT_INDEX` advances 0 → LUT_SIZE1 (38-entry walk).
// Phase 1 — `READY` (= `hdmi_init_done`) rises after the walk.
// Phase 1 — Ch167 byte-sequence lock: a bus-level decoder samples
// SDA on each SCL rising edge between START/STOP and
// assembles all 38 transactions as 24-bit
// {dev_addr, reg, data} tuples; in parallel a snoop on
// `mI2C_GO` rises captures the FSM-intent payload from
// `u_dut.mI2C_DATA[23:0]`. After the walk completes the
// test asserts the captured wire bytes match the
// FSM-intent bytes one-for-one and that every dev_addr
// is 8'h72 (ADV7513 write address).
// Phase 2 — `HDMI_TX_INT` low retriggers the FSM (`LUT_INDEX`
// back to 0; `READY` falls until the second walk).
// Bus capture is disabled here so the retrigger walk
// doesn't overflow the captured/expected logs.
// Phase 3 — Open-drain shape check: SDA is never `'x` (would
// indicate two drivers contending for strong-HIGH).
// Phase 1-3 — `ERROR` (Ch166 NACK watchdog) stays LOW on the
// healthy bus.
// Phase 4 — Force `u_dut.mI2C_ACK = 1` to simulate a slave that
// never ACKs. The FSM retries the same LUT entry; after
// NACK_LIMIT (overridden to 4) consecutive retries
// `ERROR` must latch HIGH. Releasing the force must
// NOT clear `ERROR` (sticky semantics).
//
// Bus model — Ch167 switched the legacy pulldown(sda) to
// pullup(sda) plus a minimal slave-ACK driver: after each START,
// count SCL rising edges; on the 8th, 17th, and 26th edge the
// slave drives SDA strong-LOW for one ACK clock so the master
// FSM sees ACK=0 and advances. Pullup is required because the
// master encodes a 1-bit by *releasing* SDA (1'bz), and a
// pulldown would mask every released bit as 0 — preventing
// byte-level decode.
//
// Slave model: the TB pulls SDA LOW unconditionally (weak
// pulldown). The master drives SDA either strong-LOW (sending
// 0 or releasing to high-Z while the chip ACKs); on
// release-cycles the pulldown wins and the master reads ACK=0
// → the FSM advances every byte. Data correctness on the bus
// is NOT modeled — this TB only verifies FSM progress + the
// open-drain release behavior, which is exactly the audit's
// scope (`Medium — verify the LUT walk + READY + SDA + INT
// retrigger`).
`timescale 1ns/1ps
module tb_hdmi_i2c_wake_smoke;
// 100 MHz TB clock. The wake-up FSM divides this internally,
// but the parameter overrides below collapse the divider to
// ~2 cycles per I²C controller-clock toggle.
logic clk;
initial clk = 1'b0;
always #5 clk = ~clk;
logic rst_n;
logic hdmi_tx_int;
// I²C bus modeling. The bus floats high through external
// pull-ups in real hardware. Ch167 byte-sequence-lock work
// requires the line to read each transmitted bit faithfully:
// master drives strong-LOW for 0 and releases (1'bz) for 1,
// so a pull-up is needed to bring 1's up to 1'b1 (pulldown
// would mask every released bit as 0). The minimal I²C slave
// model below drives SDA strong-LOW during the three ACK
// windows of every transaction so the master FSM sees ACK=0
// and advances to the next LUT entry.
wire scl;
wire sda;
pullup(sda);
// ----------------------------------------------------------
// Bus-edge detection (used by the slave-ACK driver and the
// byte-sequence decoder). Sampled on the TB master clock.
// ----------------------------------------------------------
reg scl_d, sda_d;
always_ff @(posedge clk) begin
scl_d <= scl;
sda_d <= sda;
end
wire scl_rise = scl && !scl_d;
wire sda_fall_during_scl_high = scl && !sda && sda_d;
wire sda_rise_during_scl_high = scl && sda && !sda_d;
// ----------------------------------------------------------
// Minimal I²C slave-ACK driver. The master's I2C_Controller
// FSM lives at u_dut.u0; phase encoding is a 6-bit register
// with PH_ACK0=11, PH_ACK1=20, PH_ACK2=29 (see
// rtl/platform/I2C_Controller.v parameters). Drive SDA
// strong-LOW for the entire ACK phase so the master sees
// ACK=0 regardless of where it samples within the phase.
// Outside ACK phases SDA is 1'bz so the master's data bits
// (released → 1'b1, driven → 1'b0) are visible to the
// byte decoder. This is far more robust than counting SCL
// edges (which races at ACK boundaries when input-clock
// resolution is only 2× the controller-clock period).
// ----------------------------------------------------------
wire slave_in_ack_phase =
(u_dut.u0.phase == 6'd11) || // PH_ACK0
(u_dut.u0.phase == 6'd20) || // PH_ACK1
(u_dut.u0.phase == 6'd29); // PH_ACK2
assign sda = slave_in_ack_phase ? 1'b0 : 1'bz;
// ----------------------------------------------------------
// Ch167 byte-sequence lock. Bus-level decoder: between
// START and STOP, sample SDA on each SCL rising edge and
// assemble three 8-bit data bytes (skipping the ACK windows
// at edges 8, 17, 26). On STOP, append the 24-bit
// {dev_addr, reg, data} payload to a captured[] log. A
// parallel snoop on `mI2C_GO` rising edges captures the
// FSM's intent (`u_dut.mI2C_DATA[23:0]`) into expected[].
// After the first LUT walk completes (Phase 1), the test
// asserts captured[i] === expected[i] for i = 0..LUT_SIZE-1.
// ----------------------------------------------------------
bit bus_capture_enable;
initial bus_capture_enable = 1'b1;
typedef enum logic [0:0] { DEC_IDLE, DEC_TXN } dec_state_t;
dec_state_t dec_state;
int dec_bit_count;
logic [23:0] dec_payload;
int captured_count;
logic [23:0] captured [0:63];
always_ff @(posedge clk) begin
if (!rst_n) begin
dec_state <= DEC_IDLE;
dec_bit_count <= 0;
dec_payload <= 24'h0;
captured_count <= 0;
end else if (bus_capture_enable) begin
case (dec_state)
DEC_IDLE: begin
if (sda_fall_during_scl_high) begin
dec_state <= DEC_TXN;
dec_bit_count <= 0;
dec_payload <= 24'h0;
end
end
DEC_TXN: begin
if (sda_rise_during_scl_high &&
!slave_in_ack_phase &&
dec_bit_count >= 27) begin
// STOP — record the captured payload.
if (captured_count < 64)
captured[captured_count] <= dec_payload;
captured_count <= captured_count + 1;
dec_state <= DEC_IDLE;
end else if (scl_rise && dec_bit_count < 27) begin
// Map SCL-rise position within transaction
// to a bit slot in the 24-bit payload
// (MSB-first per byte). Edges 8/17/26 are
// ACK windows - sample but discard.
case (dec_bit_count)
0: dec_payload[23] <= sda;
1: dec_payload[22] <= sda;
2: dec_payload[21] <= sda;
3: dec_payload[20] <= sda;
4: dec_payload[19] <= sda;
5: dec_payload[18] <= sda;
6: dec_payload[17] <= sda;
7: dec_payload[16] <= sda;
// 8 = ACK0
9: dec_payload[15] <= sda;
10: dec_payload[14] <= sda;
11: dec_payload[13] <= sda;
12: dec_payload[12] <= sda;
13: dec_payload[11] <= sda;
14: dec_payload[10] <= sda;
15: dec_payload[9] <= sda;
16: dec_payload[8] <= sda;
// 17 = ACK1
18: dec_payload[7] <= sda;
19: dec_payload[6] <= sda;
20: dec_payload[5] <= sda;
21: dec_payload[4] <= sda;
22: dec_payload[3] <= sda;
23: dec_payload[2] <= sda;
24: dec_payload[1] <= sda;
25: dec_payload[0] <= sda;
// 26 = ACK2
default: ;
endcase
dec_bit_count <= dec_bit_count + 1;
end
end
endcase
end
end
// FSM-intent snoop: capture u_dut.mI2C_DATA on each rising
// edge of u_dut.mI2C_GO. The FSM raises GO once per LUT
// entry's first attempt (and again on each retry, but
// expected_count is gated to LUT_SIZE so retries don't
// overflow).
reg mi2c_go_d;
int expected_count;
logic [23:0] expected_seq [0:63];
always_ff @(posedge clk) begin
if (!rst_n) begin
mi2c_go_d <= 1'b0;
expected_count <= 0;
end else begin
mi2c_go_d <= u_dut.mI2C_GO;
if (u_dut.mI2C_GO && !mi2c_go_d && bus_capture_enable) begin
if (expected_count < 64)
expected_seq[expected_count] <= u_dut.mI2C_DATA[23:0];
expected_count <= expected_count + 1;
end
end
end
// The DUT. Tiny CLK_Freq/I2C_Freq ratio (≈ 1) collapses the
// controller-clock divider so the LUT walks in microseconds
// instead of tens of milliseconds. NACK_LIMIT shrunk from
// production 16 → 4 so Phase 4 (forced-NACK error trigger)
// finishes in microseconds rather than tens of ms.
logic ready;
logic dut_error;
I2C_HDMI_Config #(
.CLK_Freq (2),
.I2C_Freq (1),
.LUT_SIZE (38),
.NACK_LIMIT (4)
) u_dut (
.iCLK (clk),
.iRST_N (rst_n),
.I2C_SCLK (scl),
.I2C_SDAT (sda),
.HDMI_TX_INT(hdmi_tx_int),
.READY (ready),
.ERROR (dut_error)
);
// ----------------------------------------------------------
// Phase 1: reset, watch the FSM walk LUT_INDEX 0 → 37.
// ----------------------------------------------------------
int errors;
int lut_index_max_seen;
int ready_rise_cycles;
bit saw_lut_full_walk;
bit saw_ready_rise;
bit saw_error_during_happy_path;
// Hoisted out of the Phase-2/Phase-3/Phase-4 initial block below so
// iverilog 12 (which rejects mid-block declarations after executable
// statements) accepts the file. Module-scope `bit`/`int` initialize
// to 0 / 1'b0.
bit saw_restart;
int lut_idx_after_int;
int sda_high_obs;
bit saw_error_rise;
initial begin
errors = 0;
lut_index_max_seen = 0;
ready_rise_cycles = 0;
saw_lut_full_walk = 1'b0;
saw_ready_rise = 1'b0;
rst_n = 1'b0;
hdmi_tx_int = 1'b1; // deasserted (active-low)
repeat (10) @(posedge clk);
rst_n = 1'b1;
end
// Track the highest LUT_INDEX ever observed, and whether the
// ERROR output ever rose during the "happy path" (Phases 1-3,
// before the explicit NACK-injection Phase 4 begins).
bit happy_path_active;
initial happy_path_active = 1'b1;
always_ff @(posedge clk) begin
if (rst_n) begin
if (int'(u_dut.LUT_INDEX) > lut_index_max_seen)
lut_index_max_seen <= int'(u_dut.LUT_INDEX);
if (u_dut.LUT_INDEX == u_dut.LUT_SIZE - 1)
saw_lut_full_walk <= 1'b1;
if (ready && !saw_ready_rise) begin
saw_ready_rise <= 1'b1;
ready_rise_cycles <= 0;
end else if (saw_ready_rise) begin
ready_rise_cycles <= ready_rise_cycles + 1;
end
if (happy_path_active && dut_error)
saw_error_during_happy_path <= 1'b1;
end
end
// ----------------------------------------------------------
// Slave-side ACK behavior is implicit in the SDA pulldown:
// when the master releases SDA at the 9th bit, the bus reads
// LOW, the controller's mI2C_ACK samples 0, and the wake-up
// FSM advances to LUT_INDEX+1. Sanity-check that the bus is
// actually toggling (master drives SCL) — if SCL never
// toggles, the FSM is stuck and we'd time out below.
int scl_edge_count;
initial scl_edge_count = 0;
always @(scl) if (rst_n) scl_edge_count = scl_edge_count + 1;
// ----------------------------------------------------------
// Phase 2: once READY, pull HDMI_TX_INT LOW briefly and
// assert the LUT walk restarts (LUT_INDEX returns to 0;
// READY falls).
// ----------------------------------------------------------
initial begin
// Wait for the first LUT walk to complete.
wait (rst_n);
fork
begin
wait (ready == 1'b1);
end
begin
#5_000_000; // 5 ms wall budget
$error("Phase 1: READY never asserted in 5 ms (LUT walk stuck or too slow)");
errors = errors + 1;
end
join_any
disable fork;
// Phase 1 asserts. LUT_INDEX walks 0..LUT_SIZE-1 inside the
// case() loop, then the post-step increments it to LUT_SIZE
// (38) on the cycle the FSM falls through to the READY-high
// branch — so the max value ever observed is LUT_SIZE, not
// LUT_SIZE-1. The full-walk evidence comes from the separate
// `saw_lut_full_walk` flag (LUT_INDEX == LUT_SIZE-1).
if (lut_index_max_seen != 38) begin
$error("Phase 1: LUT_INDEX max seen = %0d, expected 38 (LUT_SIZE)", lut_index_max_seen);
errors = errors + 1;
end
if (!saw_lut_full_walk) begin
$error("Phase 1: never observed LUT_INDEX == LUT_SIZE-1 (37)");
errors = errors + 1;
end
if (!ready) begin
$error("Phase 1: READY low after wait — fork race");
errors = errors + 1;
end
if (scl_edge_count < 100) begin
$error("Phase 1: only %0d SCL edges seen — bus is stuck", scl_edge_count);
errors = errors + 1;
end
// ------------------------------------------------------
// Ch167 byte-sequence lock. Compare every captured
// 24-bit transaction (assembled from the wire) against
// the FSM-intent snapshot (assembled at mI2C_GO rise).
// Both should be exactly LUT_SIZE entries long; mismatch
// means either the controller miswired the data path or
// the FSM tried to send something unexpected.
// ------------------------------------------------------
// Settle a few extra cycles so the last STOP gets recorded.
repeat (32) @(posedge clk);
if (captured_count != int'(u_dut.LUT_SIZE)) begin
$error("Ch167: captured %0d transactions on the wire, expected %0d",
captured_count, u_dut.LUT_SIZE);
errors = errors + 1;
end
if (expected_count != int'(u_dut.LUT_SIZE)) begin
$error("Ch167: snooped %0d FSM-intent payloads, expected %0d",
expected_count, u_dut.LUT_SIZE);
errors = errors + 1;
end
for (int i = 0; i < int'(u_dut.LUT_SIZE); i++) begin
if (captured[i] !== expected_seq[i]) begin
$error("Ch167: txn %0d mismatch: bus=24'h%06h fsm=24'h%06h",
i, captured[i], expected_seq[i]);
errors = errors + 1;
end
end
// First byte of every transaction must be 8'h72 = ADV7513
// write address (7-bit 0x39 << 1, R/W=0). Cross-check the
// captured stream directly so we lock the dev_addr too.
for (int i = 0; i < int'(u_dut.LUT_SIZE); i++) begin
if (captured[i][23:16] !== 8'h72) begin
$error("Ch167: txn %0d dev_addr=8'h%02h, expected 8'h72",
i, captured[i][23:16]);
errors = errors + 1;
end
end
// Disable bus capture for the rest of the test —
// Phase 2 retriggers the LUT (would overflow captured[])
// and Phase 4 forces NACKs (broken bus by design).
bus_capture_enable = 1'b0;
// Phase 2: HDMI_TX_INT retrigger. Pull INT low while
// READY is high; the FSM should reset LUT_INDEX to 0
// and re-walk.
@(posedge clk); hdmi_tx_int = 1'b0;
// Hold long enough for the FSM to sample it.
repeat (8) @(posedge clk);
hdmi_tx_int = 1'b1;
// Wait for LUT_INDEX to dip back below the max (proves
// the FSM restarted) and then for READY to come back up.
saw_restart = 1'b0;
fork
begin
wait (u_dut.LUT_INDEX == 0);
saw_restart = 1'b1;
end
begin
#5_000_000;
$error("Phase 2: LUT_INDEX never returned to 0 after HDMI_TX_INT pulse");
errors = errors + 1;
end
join_any
disable fork;
if (!saw_restart) begin
// (already errored above)
end else begin
// Wait for the second walk to complete.
fork
begin
wait (ready == 1'b1 && u_dut.LUT_INDEX >= u_dut.LUT_SIZE);
end
begin
#5_000_000;
$error("Phase 2: READY did not reassert after retrigger walk");
errors = errors + 1;
end
join_any
disable fork;
end
// Phase 3: open-drain shape check. With the Ch167 pullup
// + slave-ACK bus model, the line is HIGH at idle and
// LOW only when the master drives a 0 or when the slave
// drives the ACK. The relevant violation is that no two
// drivers ever simultaneously drive strong-HIGH (which
// would resolve to 'x' on a tri-state wire). Sample SDA
// repeatedly and assert it is never 'x'.
// (Reusing `sda_high_obs` as the violation counter.)
sda_high_obs = 0;
repeat (50) begin
@(posedge clk);
if (sda === 1'bx) sda_high_obs = sda_high_obs + 1;
end
if (sda_high_obs > 0) begin
$error("Phase 3: SDA was 'x' on %0d/50 sample cycles (driver conflict / open-drain violation)",
sda_high_obs);
errors = errors + 1;
end
// Happy-path ERROR check: across Phases 1-3 the bus was
// healthy (pulldown → ACK=0), so the NACK watchdog must
// never have fired.
if (saw_error_during_happy_path) begin
$error("Happy path: ERROR rose during normal walk (NACK watchdog false-positive)");
errors = errors + 1;
end
if (dut_error) begin
$error("Happy path: ERROR is HIGH at end of Phase 3 (expected LOW)");
errors = errors + 1;
end
// ----------------------------------------------------------
// Phase 4: forced-NACK error trigger. The healthy bus
// produced ACK=0 every byte (master sees the pulldown
// win the release window). Force `mI2C_ACK = 1'b1` to
// simulate a slave that doesn't ACK — every transaction
// bounces back to mSetup_ST=0 and retries the same LUT
// entry. After NACK_LIMIT consecutive retries (overridden
// to 4 above) the watchdog must latch ERROR.
//
// We also pulse HDMI_TX_INT so the FSM restarts the walk
// at LUT_INDEX=0 → predictable retry counter starting
// point.
happy_path_active = 1'b0;
@(posedge clk); hdmi_tx_int = 1'b0;
repeat (8) @(posedge clk);
hdmi_tx_int = 1'b1;
wait (u_dut.LUT_INDEX == 0);
force u_dut.mI2C_ACK = 1'b1;
saw_error_rise = 1'b0;
fork
begin
wait (dut_error == 1'b1);
saw_error_rise = 1'b1;
end
begin
#5_000_000;
$error("Phase 4: ERROR never asserted within 5 ms after forced NACK");
errors = errors + 1;
end
join_any
disable fork;
// ERROR is sticky. Releasing the force (slave starts ACKing
// again) must NOT clear it — only iRST_N would.
release u_dut.mI2C_ACK;
repeat (200) @(posedge clk);
if (!dut_error) begin
$error("Phase 4: ERROR was not sticky — went LOW after force released");
errors = errors + 1;
end
$display("[tb_hdmi_i2c_wake_smoke] lut_index_max=%0d saw_full_walk=%0b saw_ready_rise=%0b scl_edges=%0d saw_error_rise=%0b errors=%0d",
lut_index_max_seen, saw_lut_full_walk, saw_ready_rise,
scl_edge_count, saw_error_rise, errors);
if (errors == 0) $display("[tb_hdmi_i2c_wake_smoke] PASS");
else $display("[tb_hdmi_i2c_wake_smoke] FAIL");
$finish;
end
// Hard timeout — bound the whole test.
initial begin
#20_000_000;
$error("[tb_hdmi_i2c_wake_smoke] hard timeout");
$finish;
end
endmodule : tb_hdmi_i2c_wake_smoke