From 37839811a01e9a0fdf2debe62df07fe6471ee679 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 6 May 2026 09:18:37 -0400 Subject: [PATCH 1/8] Add testbenches that fail for 4 possible bugs --- hdl/projects/cosmo_seq/dimms_subsystem/BUCK | 9 +- .../proxy_channel/dimm_arb_mux.vhd | 6 + .../dimms_subsystem/sims/dimm_arb_mux_tb.vhd | 325 ++++++++++++++++++ .../dimms_subsystem/sims/spd_proxy_top_tb.vhd | 43 ++- 4 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/BUCK b/hdl/projects/cosmo_seq/dimms_subsystem/BUCK index f8e645ef..51b5942f 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/BUCK +++ b/hdl/projects/cosmo_seq/dimms_subsystem/BUCK @@ -56,7 +56,7 @@ vunit_sim( vunit_sim( name = "spd_proxy_top_tb", - srcs = glob(["sims/*.vhd"], exclude = ["sims/*tb_pkg.vhd"]), + srcs = glob(["sims/*.vhd"], exclude = ["sims/*tb_pkg.vhd", "sims/dimm_arb_mux_tb.vhd"]), deps = [ ":dimms_subsystem_top", ":spd_shared_sim_pkg", @@ -66,4 +66,11 @@ vunit_sim( "//hdl/ip/vhd/vunit_components:i2c_controller_vc" ], visibility = ['PUBLIC'], +) + +vunit_sim( + name = "dimm_arb_mux_tb", + srcs = ["sims/dimm_arb_mux_tb.vhd"], + deps = [":dimms_subsystem_top"], + visibility = ['PUBLIC'], ) \ No newline at end of file diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd index 98f4203f..f814cc04 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd @@ -152,8 +152,14 @@ architecture rtl of dimm_arb_mux is signal playback_scl_fedge : std_logic; signal dimm_i2c_idle_cnts : integer range 0 to BUS_IDLE_MIN := 0; + -- Observability hook for unit-level testbench: NVC external names cannot + -- traverse record fields, so mirror sample_r.bit_count onto a scalar signal. + signal dbg_sample_bit_count : integer range 0 to 7 := 0; + begin + dbg_sample_bit_count <= sample_r.bit_count; + fpga_i2c_has_bus <= '1' when fpga_i2c_grant else '0'; sp5_playback_i2c_has_bus <= '1' when mux_r.state = PLAY_STORED_START or mux_r.state = PLAY_STORED_DATA or diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd new file mode 100644 index 00000000..268ab52b --- /dev/null +++ b/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd @@ -0,0 +1,325 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- Copyright 2025 Oxide Computer Company + +-- Unit-level testbench for dimm_arb_mux. +-- +-- Bug 1 cannot be triggered at the integration level: the FPGA abort always finishes +-- before the CPU's first SCL falling edge (~2-3 µs vs the 5 µs STANDARD half-period), +-- so PLAY_STORED_START is entered while sample_r.state=SAMPLE_START and Bug 3 fires +-- instead. Direct stimulus control lets us reach PLAY_STORED_DATA with bit_count=1 +-- while keeping cpu_scl_in='1' through the catch-up rising edge. + +library ieee; +use ieee.std_logic_1164.all; + +library vunit_lib; + context vunit_lib.vunit_context; + +use work.i2c_common_pkg.all; +use work.tristate_if_pkg.all; + +entity dimm_arb_mux_tb is + generic (runner_cfg : string); +end entity; + +architecture tb of dimm_arb_mux_tb is + constant CLK_PER_NS : positive := 8; + constant I2C_MODE : mode_t := FAST_PLUS; + + signal clk : std_logic := '0'; + signal reset : std_logic := '1'; + + signal cpu_scl_in : std_logic := '1'; + signal cpu_scl_fedge : std_logic := '0'; + signal cpu_scl_redge : std_logic := '0'; + signal cpu_sda_in : std_logic := '1'; + signal cpu_sda_fedge : std_logic := '0'; + signal cpu_sda_redge : std_logic := '0'; + + -- dimm_scl/sda are the inputs the bus_idle_monitor watches. + -- Keep them both high so dimm_i2c_idle asserts quickly. + signal dimm_sda : std_logic := '1'; + signal dimm_scl : std_logic := '1'; + + signal bus_request : std_logic; + signal bus_grant : std_logic := '0'; + signal fpga_i2c_grant : std_logic := '0'; + signal fpga_i2c_abort_or_finish : std_logic; + + signal playback_sda_if : tristate; + signal playback_scl_if : tristate; + + signal fpga_i2c_has_bus : std_logic; + signal sp5_playback_i2c_has_bus : std_logic; + signal sp5_i2c_has_bus : std_logic; +begin + + clk <= not clk after 4 ns; + + -- Provide idle bus feedback to the DUT (not functionally used by dimm_arb_mux + -- for these ports, but must be driven to avoid 'U' in the model). + playback_sda_if.i <= '1'; + playback_scl_if.i <= '1'; + + DUT: entity work.dimm_arb_mux + generic map ( + CLK_PER_NS => CLK_PER_NS, + I2C_MODE => I2C_MODE + ) + port map ( + clk => clk, + reset => reset, + in_a0 => '1', + cpu_scl_in => cpu_scl_in, + cpu_scl_fedge => cpu_scl_fedge, + cpu_scl_redge => cpu_scl_redge, + cpu_sda_in => cpu_sda_in, + cpu_sda_fedge => cpu_sda_fedge, + cpu_sda_redge => cpu_sda_redge, + dimm_sda => dimm_sda, + dimm_scl => dimm_scl, + bus_request => bus_request, + bus_grant => bus_grant, + fpga_i2c_grant => fpga_i2c_grant, + fpga_i2c_abort_or_finish => fpga_i2c_abort_or_finish, + playback_sda => playback_sda_if, + playback_scl => playback_scl_if, + fpga_i2c_has_bus => fpga_i2c_has_bus, + sp5_playback_i2c_has_bus => sp5_playback_i2c_has_bus, + sp5_i2c_has_bus => sp5_i2c_has_bus + ); + + -- Once the intentional start condition has been generated (the first SDA change + -- while the playback SCL is high), no further SDA transition is legal while SCL + -- is still high. Any such change is a spurious start or stop on the DIMM bus. + sda_glitch_monitor: process + variable start_seen : boolean := false; + begin + loop + wait on playback_sda_if.oe; + if sp5_playback_i2c_has_bus = '1' and playback_scl_if.o = '1' then + if not start_seen then + start_seen := true; + else + check_false(true, + "Bug 1: SDA changed while playback SCL was high -- " & + "spurious start/stop condition on DIMM bus"); + end if; + end if; + end loop; + wait; + end process; + + bench: process + procedure clk_tick is begin wait until rising_edge(clk); end procedure; + begin + test_runner_setup(runner, runner_cfg); + + while test_suite loop + if run("arb_bug_sda_glitch_during_playback") then + -- GH # 469 + -- Release reset, wait for POWER_UP_CLEAR (~9 *� 1000 ns at FAST_PLUS) + -- and for dimm_i2c_idle to assert (~504 ns with both dimm lines held high). + reset <= '1'; + clk_tick; + reset <= '0'; + wait for 10 us; + + -- Step 1: CPU start condition: SDA falls while SCL is high. + -- cpu_start_detected fires� -> mux: IDLE ->�� CPU_REQ_GRANT. + -- sample: SAMPLE_IDLE -> SAMPLE_START. + cpu_scl_in <= '1'; + cpu_sda_in <= '0'; + cpu_sda_fedge <= '1'; + clk_tick; + cpu_sda_fedge <= '0'; + clk_tick; + + -- Step 2: CPU SCL falling edge. + -- sample: SAMPLE_START ->�� SAMPLE_DATA (bit_count still 0). + cpu_scl_in <= '0'; + cpu_scl_fedge <= '1'; + clk_tick; + cpu_scl_fedge <= '0'; + clk_tick; + + -- Step 3: CPU SCL rising edge; sample first bit (sda_in='0'). + -- sample: bit_count becomes 1, data_bits(0)='0'. + cpu_scl_in <= '1'; + cpu_scl_redge <= '1'; + clk_tick; + cpu_scl_redge <= '0'; + clk_tick; + + -- Step 4: Grant the bus. + -- sample_r.state=SAMPLE_DATA (not SAMPLE_START) �->� GH #498 cannot fire + -- even though cpu_scl_in='1'. + -- mux: CPU_REQ_GRANT ->�� PLAY_STORED_START (dimm_i2c_idle='1' already). + bus_grant <= '1'; + wait until sp5_playback_i2c_has_bus = '1'; + + -- Step 5: Hold cpu_scl_in='1' and set cpu_sda_in='1' so the catch-up + -- rising edge produces a visible SDA change. + -- + -- data_bits(0)='0' playback currently drives SDA low (oe='1'). + -- Bug 1 will set oe := not(cpu_sda_in) = not('1') = '0' on the catch-up + -- rising edge, releasing SDA while the playback SCL is still high. + -- + -- Timeline from PLAY_STORED_START entry (playback SCL starts at '1'): + -- T + ~500 ns : first playback FEDGE → enter PLAY_STORED_DATA, + -- dimm_sda_oe set to not(data_bits(0))='1' (no change) + -- T + ~1000 ns: first playback REDGE, playback_bits=1=bit_count, + -- cpu_scl_in='1' → Bug 1 exits to ENSURE_PLAYBACK_HOLD, + -- schedules dimm_sda_oe='0' for next cycle + -- T + ~1008 ns: playback_sda_if.oe '1'→'0' while playback_scl_if.o='1' + -- → sda_glitch_monitor fires check_false → test fails + cpu_sda_in <= '1'; + wait for 2 us; + + bus_grant <= '0'; + wait for 500 ns; + + elsif run("arb_bug_playback_stall") then + -- Direct stimulus reproduction of Bug 2. + -- + -- Bug GH # 497: in PLAY_STORED_DATA, when playback_bits catches up to bit_count + -- at a falling playback SCL edge while cpu_scl_in='1', the exit condition + -- `playback_scl_fedge='1' and playback_bits=bit_count and cpu_scl_in='0'` + -- is FALSE because cpu_scl_in is '1'. The following REDGE overshoots to + -- playback_bits=2 > bit_count=1, and no subsequent condition ever matches, + -- so the machine spins in PLAY_STORED_DATA forever. + -- + -- The setup is identical to the Bug GH # 496 test (SAMPLE_DATA, bit_count=1) but + -- the check monitors sp5_i2c_has_bus rather than SDA. With Bug GH # 497 fixed the + -- mux exits to ENSURE_PLAYBACK_HOLD → CPU_HAS_BUS within ~2 us. + reset <= '1'; + clk_tick; + reset <= '0'; + wait for 10 us; + + -- CPU start condition: SAMPLE_IDLE ->�� SAMPLE_START, mux: IDLE ->�� CPU_REQ_GRANT. + cpu_scl_in <= '1'; + cpu_sda_in <= '0'; + cpu_sda_fedge <= '1'; + clk_tick; + cpu_sda_fedge <= '0'; + clk_tick; + + -- CPU SCL falling edge: SAMPLE_START �-> SAMPLE_DATA (bit_count=0). + cpu_scl_in <= '0'; + cpu_scl_fedge <= '1'; + clk_tick; + cpu_scl_fedge <= '0'; + clk_tick; + + -- CPU SCL rising edge: sample first bit ('0'), bit_count becomes 1. + cpu_scl_in <= '1'; + cpu_scl_redge <= '1'; + clk_tick; + cpu_scl_redge <= '0'; + clk_tick; + + -- Lower cpu_scl_in='0' so Bug 1's REDGE exit (which requires cpu_scl_in='1') + -- cannot fire at the catch-up REDGE and mask Bug 2's stall. + cpu_scl_in <= '0'; + clk_tick; + + -- Grant bus with sample_r.state=SAMPLE_DATA (not SAMPLE_START) so Bug GH # 498 + -- cannot fire. Mux enters PLAY_STORED_START → PLAY_STORED_DATA. + bus_grant <= '1'; + wait until sp5_playback_i2c_has_bus = '1'; + + -- Timeline from PLAY_STORED_START entry (playback SCL starts at '1'): + -- T + ~504 ns : FEDGE 1 → PLAY_STORED_DATA, playback_bits=0 + -- T + ~1008 ns: REDGE 1 → playback_bits=1; cpu_scl_in='0' blocks Bug 1 + -- T + ~1512 ns: FEDGE 2 (catch-up FEDGE for bit_count=1) + -- Wait past REDGE 1 but before FEDGE 2, then raise cpu_scl_in='1' so the + -- Bug 2 exit condition (cpu_scl_in='0') is blocked at FEDGE 2. With Bug 2 + -- the mux then stalls in PLAY_STORED_DATA forever. + wait for 1100 ns; + cpu_scl_in <= '1'; + + -- With Bug GH # 497 fixed, the FEDGE exit fires regardless of cpu_scl_in + -- when playback_bits=bit_count=1, and sp5_i2c_has_bus asserts within ~2 us. + wait until sp5_i2c_has_bus = '1' for 50 us; + check_true(sp5_i2c_has_bus = '1', + "Bug GH # 497: mux stalled in PLAY_STORED_DATA -- never reached CPU_HAS_BUS"); + + bus_grant <= '0'; + wait for 500 ns; + + elsif run("arb_bug_sample_error_stale_bitcount") then + -- Direct stimulus reproduction of Bug 4 (GH # 499). + -- + -- SAMPLE_ERROR is entered when bit_count reaches 7 (the bit_count subtype + -- saturates). The buggy SAMPLE_ERROR → SAMPLE_IDLE transition does not + -- reset bit_count, so the next sample_r.bit_count read returns the stale + -- value 7. The fix sets v.bit_count := 0 in SAMPLE_ERROR. + -- + -- This is a white-box check: we drive seven CPU SCL rising edges to push + -- the sampler through SAMPLE_DATA → SAMPLE_ERROR → SAMPLE_IDLE, then read + -- the internal bit_count via an external name. bus_grant stays low so + -- the mux remains in CPU_REQ_GRANT (playback_done='0') and does not + -- short-circuit the sampler via mux_r.playback_done. + reset <= '1'; + clk_tick; + reset <= '0'; + wait for 10 us; + + -- CPU start: SAMPLE_IDLE → SAMPLE_START (resets bit_count to 0). + cpu_scl_in <= '1'; + cpu_sda_in <= '0'; + cpu_sda_fedge <= '1'; + clk_tick; + cpu_sda_fedge <= '0'; + clk_tick; + + -- First SCL falling edge: SAMPLE_START → SAMPLE_DATA. + cpu_scl_in <= '0'; + cpu_scl_fedge <= '1'; + clk_tick; + cpu_scl_fedge <= '0'; + clk_tick; + + -- Seven SCL rising/falling edges; the 7th REDGE drives bit_count to 7 + -- and v.state to SAMPLE_ERROR. Next cycle SAMPLE_IDLE. + for i in 0 to 6 loop + cpu_scl_in <= '1'; + cpu_scl_redge <= '1'; + clk_tick; + cpu_scl_redge <= '0'; + clk_tick; + if i < 6 then + cpu_scl_in <= '0'; + cpu_scl_fedge <= '1'; + clk_tick; + cpu_scl_fedge <= '0'; + clk_tick; + end if; + end loop; + + -- Allow SAMPLE_ERROR → SAMPLE_IDLE to settle. + for i in 0 to 3 loop + clk_tick; + end loop; + + check_equal( + << signal .dimm_arb_mux_tb.DUT.dbg_sample_bit_count : integer >>, + 0, + "Bug GH # 499: sample_r.bit_count not reset after SAMPLE_ERROR -> SAMPLE_IDLE"); + + wait for 500 ns; + end if; + end loop; + + wait for 500 ns; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 1 ms); + +end tb; diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd index 14210c82..2967bc12 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd @@ -32,13 +32,19 @@ entity spd_proxy_top_tb is end entity; architecture tb of spd_proxy_top_tb is - begin th: entity work.spd_proxy_top_th; bench: process alias reset is << signal th.reset : std_logic >>; + -- Bug 3 (PLAY_STORED_START hold-time) is the only dimm_arb_mux bug that is + -- observable at the integration level: STANDARD-mode CPU SCL is too slow + -- relative to the FAST_PLUS playback machine for Bugs 1/2/4 to manifest + -- before the start-hold-time branch fires, so those bugs are covered by + -- the unit-level testbench instead. + alias playback_active is + << signal th.DUT.proxy_channel_top_bus0.sp5_playback_i2c_has_bus : std_logic >>; variable cmd : cmd_type; variable data32 : std_logic_vector(31 downto 0); variable rnd : RandomPType; @@ -284,6 +290,41 @@ begin read_bus(net, bus_handle, To_StdLogicVector(SPD_RDATA_OFFSET, bus_handle.p_address_length), data32); check_match(data32, std_logic_vector'(X"DDCCBBAA"), "Read data mismatch"); + -- ----------------------------------------------------------------------- + -- Bug regression tests for dimm_arb_mux + -- + -- Only Bug 3 (start-condition hold time) is observable at the integration + -- level here. With STANDARD-mode CPU SCL the FPGA abort completes long + -- before the CPU's first SCL falling edge, so PLAY_STORED_START always + -- fires the SAMPLE_START hold-time branch and Bugs 1/2/4 never get a + -- chance to manifest. Those bugs are covered by the unit-level testbench + -- in dimm_arb_mux_tb.vhd, which drives sample_r.state directly. + -- ----------------------------------------------------------------------- + + elsif run("arb_bug_start_hold_time") then + -- Bug GH # 498: when the CPU starts a transaction while the bus is idle, the mux + -- reaches PLAY_STORED_START while sample_r.state=SAMPLE_START and + -- cpu_scl_in='1' (CPU still in start hold-time). The immediate branch + -- goes directly to CPU_HAS_BUS, skipping ENSURE_PLAYBACK_HOLD entirely. + -- The DIMM bus sees the FPGA drive SDA low for only one system clock (~8 ns) + -- before releasing it, far below the t_HD;STA minimum of 260 ns (FAST_PLUS). + -- Observable: playback_active drops after a single cycle instead of persisting + -- through ENSURE_PLAYBACK_HOLD (~500 ns for FAST_PLUS at 125 MHz). + wait for 15 us; + push_byte(cpu_tx_q, to_integer(std_logic_vector'(X"AA"))); + i2c_write_txn(net, address(I2C_DIMM1F_TGT_VC), cpu_tx_q, cpu_ack_q, I2C_CTRL_VC0.p_actor); + -- Wait for the mux to enter the playback window (PLAY_STORED_START asserts + -- sp5_playback_i2c_has_bus = '1'). + wait until playback_active = '1' for 100 us; + check_true(playback_active = '1', "Bug GH # 498: playback never activated"); + -- ENSURE_PLAYBACK_HOLD keeps playback_active='1' for FAST_SCL_PERIOD/2 cycles + -- = 62 * 8 ns = ~496 ns. With Bug GH # 498 it drops in a single cycle (~8 ns). + -- Checking at 200 ns reliably distinguishes the two cases. + wait for 200 ns; + check_true(playback_active = '1', + "Bug GH # 498: playback_active dropped in under 200 ns -- " & + "ENSURE_PLAYBACK_HOLD was bypassed, start-condition hold time violated"); + elsif run("spd_sm_prefetch_again") then wait for 15 us; --allow power up clear write_word(memory(I2C_DIMM1F_TGT_VC), 16#80#, X"AA"); From 22ca007603c4bbc501aedd94d2144c5b56a053bc Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 6 May 2026 09:23:08 -0400 Subject: [PATCH 2/8] Fix Bug 1: remove REDGE exit from PLAY_STORED_DATA The REDGE-exit branch (cpu_scl_in='1' catch-up) set dimm_sda_oe one registered cycle after the rising edge, while the playback SCL was still high -- producing a spurious start or stop condition on the DIMM bus. The FEDGE path already handles catch-up exit correctly, so just remove the REDGE branch and let playback_bits increment cleanly. --- .../dimms_subsystem/proxy_channel/dimm_arb_mux.vhd | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd index f814cc04..1841b3c0 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd @@ -337,18 +337,9 @@ begin -- We're going to play back one bit per synthesized scl rising edge until we catch -- up with the sampled bits. We'll to CPU_HAS_BUS only if we've caught up -- and CPU's SCL is low so that the SDA handoff is clean. - + if playback_scl_redge = '1' then v.playback_bits := mux_r.playback_bits + 1; - if v.playback_bits = sample_r.bit_count and cpu_scl_in = '1' then - v.state := ENSURE_PLAYBACK_HOLD; - v.playback_done := '1'; - v.playback_bits := 0; - v.hold_timer := 0; - -- prevent small glitches or arbitration oddities. Since we've caught up - -- and are at a scl fedge, match the CPU's sda line for a smooth transition - v.dimm_sda_oe := not cpu_sda_in; - end if; elsif playback_scl_fedge = '1' and mux_r.playback_bits < sample_r.bit_count then -- oe = 1 is output = 0 so there's an inversion here. v.dimm_sda_oe := not sample_r.data_bits(mux_r.playback_bits); From 3c0ffa77d9f0910260259ff326f5605b9f866c66 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 6 May 2026 15:10:50 -0400 Subject: [PATCH 3/8] Fix Bug 2: PLAY_STORED_DATA FEDGE exit must not gate on cpu_scl_in --- .../cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd index 1841b3c0..fc31a270 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd @@ -343,7 +343,7 @@ begin elsif playback_scl_fedge = '1' and mux_r.playback_bits < sample_r.bit_count then -- oe = 1 is output = 0 so there's an inversion here. v.dimm_sda_oe := not sample_r.data_bits(mux_r.playback_bits); - elsif playback_scl_fedge = '1' and mux_r.playback_bits = sample_r.bit_count and cpu_scl_in = '0' then + elsif playback_scl_fedge = '1' and mux_r.playback_bits = sample_r.bit_count then -- we've played all the bits we have, just float sda until we can hand off v.state := ENSURE_PLAYBACK_HOLD; v.playback_done := '1'; From 61ab3d06369001e055852ebb4afa40cbc97843d1 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 6 May 2026 15:11:49 -0400 Subject: [PATCH 4/8] Fix Bug 3: PLAY_STORED_START must hold start condition via ENSURE_PLAYBACK_HOLD --- .../proxy_channel/dimm_arb_mux.vhd | 5 +++-- .../dimms_subsystem/sims/spd_proxy_top_tb.vhd | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd index fc31a270..91b10834 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd @@ -311,10 +311,11 @@ begin when PLAY_STORED_START => -- play back the stored start condition -- if the CPU is still in the start condition but SCL is still high - -- just hand over the bus immediately. We'll have generated start already here. + -- go through ENSURE_PLAYBACK_HOLD to satisfy I2C t_HD;STA hold time. v.dimm_sda_oe := '1'; --generate a start by pulling sda low if sample_r.state = SAMPLE_START and cpu_scl_in = '1' then - v.state := CPU_HAS_BUS; + v.state := ENSURE_PLAYBACK_HOLD; + v.hold_timer := 0; v.playback_done := '1'; elsif playback_scl_fedge = '1' then if sample_r.state = SAMPLE_DATA and sample_r.bit_count > 0 then diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd index 2967bc12..6321ab9b 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd @@ -50,6 +50,7 @@ begin variable rnd : RandomPType; variable cpu_tx_q : queue_t := new_queue; variable cpu_ack_q : queue_t := new_queue; + variable request_msg : msg_t; begin -- Always the first thing in the process, set up things for the VUnit test runner test_runner_setup(runner, runner_cfg); @@ -310,9 +311,13 @@ begin -- before releasing it, far below the t_HD;STA minimum of 260 ns (FAST_PLUS). -- Observable: playback_active drops after a single cycle instead of persisting -- through ENSURE_PLAYBACK_HOLD (~500 ns for FAST_PLUS at 125 MHz). + -- + -- Send the I2C START non-blocking so the bench can immediately wait for + -- the playback_active rising edge rather than blocking in i2c_write_txn + -- for ~200 us while the playback window opens and closes. wait for 15 us; - push_byte(cpu_tx_q, to_integer(std_logic_vector'(X"AA"))); - i2c_write_txn(net, address(I2C_DIMM1F_TGT_VC), cpu_tx_q, cpu_ack_q, I2C_CTRL_VC0.p_actor); + request_msg := new_msg(i2c_send_start); + send(net, I2C_CTRL_VC0.p_actor, request_msg); -- Wait for the mux to enter the playback window (PLAY_STORED_START asserts -- sp5_playback_i2c_has_bus = '1'). wait until playback_active = '1' for 100 us; @@ -324,6 +329,12 @@ begin check_true(playback_active = '1', "Bug GH # 498: playback_active dropped in under 200 ns -- " & "ENSURE_PLAYBACK_HOLD was bypassed, start-condition hold time violated"); + -- Clean up: let the start finish, then stop the transaction. + wait_until_idle(net, I2C_CTRL_VC0.p_actor); + request_msg := new_msg(i2c_send_stop); + send(net, I2C_CTRL_VC0.p_actor, request_msg); + wait_until_idle(net, I2C_CTRL_VC0.p_actor); + wait for 100 us; elsif run("spd_sm_prefetch_again") then wait for 15 us; --allow power up clear From 5c379c27e42ad2dc32b2bfb4d59a6e7649e00d2b Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 6 May 2026 15:12:27 -0400 Subject: [PATCH 5/8] Fix Bug 4: SAMPLE_ERROR must reset bit_count before returning to SAMPLE_IDLE --- .../cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd | 1 + 1 file changed, 1 insertion(+) diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd index 91b10834..2f1f13fe 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd @@ -238,6 +238,7 @@ begin when SAMPLE_ERROR => -- Unexpected state, go back to idle v.state := SAMPLE_IDLE; + v.bit_count := 0; end case; sample_rin <= v; From 678adf05f0ea847fed265bfd770068587181d1bc Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 6 May 2026 17:38:27 -0400 Subject: [PATCH 6/8] cleanup unused stuff --- .../dimms_subsystem/proxy_channel/dimm_arb_mux.vhd | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd index 2f1f13fe..8e8671ca 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd @@ -66,7 +66,6 @@ entity dimm_arb_mux is fpga_i2c_has_bus : out std_logic; sp5_playback_i2c_has_bus : out std_logic; - forced_idle_delay : out std_logic; sp5_i2c_has_bus : out std_logic; ); @@ -125,13 +124,11 @@ architecture rtl of dimm_arb_mux is state : sample_state_t; data_bits : unsigned(6 downto 0); bit_count : integer range 0 to 7; - allowed_to_handoff : std_logic; end record; constant SAMPLE_REG_RESET : sample_reg_t := ( state => SAMPLE_IDLE, data_bits => (others => '0'), - bit_count => 0, - allowed_to_handoff => '0' + bit_count => 0 ); signal sample_r, sample_rin : sample_reg_t; signal dimm_i2c_idle : std_logic; @@ -166,7 +163,6 @@ begin mux_r.state = POWER_UP_CLEAR or mux_r.state = ENSURE_PLAYBACK_HOLD else '0'; sp5_i2c_has_bus <= '1' when mux_r.state = CPU_HAS_BUS else '0'; - forced_idle_delay <= '1' when mux_r.state = BUS_IDLE_DELAY else '0'; fpga_i2c_abort_or_finish <= mux_r.fpga_abort_or_finish; -- need to enforce minimum idle time on the bus before we can From 8fe1745d29c7f525614bc973dd2ca8f4d0d14977 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 6 May 2026 17:38:27 -0400 Subject: [PATCH 7/8] another_bug --- .../proxy_channel/dimm_arb_mux.vhd | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd index 8e8671ca..5e809ea3 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/proxy_channel/dimm_arb_mux.vhd @@ -221,16 +221,17 @@ begin -- as we could move the data on the other block. v.data_bits(v.bit_count) := cpu_sda_in; v.bit_count := v.bit_count + 1; - if mux_r.playback_done = '1' or cpu_stop_detected = '1' then + if v.bit_count = 7 then -- all bits sampled, go back to idle -- we should *always* get to playback done before we hit 7 bits. - v.state := SAMPLE_IDLE; - elsif v.bit_count = 7 then - -- all bits sampled, go back to idle - -- we should *always* get to playback done before we hit 7 bits. - v.state := SAMPLE_ERROR; + v.state := SAMPLE_ERROR; end if; end if; + if mux_r.playback_done = '1' or cpu_stop_detected = '1' then + -- all bits sampled, go back to idle + -- we should *always* get to playback done before we hit 7 bits. + v.state := SAMPLE_IDLE; + end if; when SAMPLE_ERROR => -- Unexpected state, go back to idle v.state := SAMPLE_IDLE; @@ -354,7 +355,7 @@ begin when ENSURE_PLAYBACK_HOLD => if mux_r.hold_timer < (FAST_SCL_PERIOD / 2) then v.hold_timer := mux_r.hold_timer + 1; - else + elsif cpu_scl_in = '0' then -- Need to make sure we transition on an SCL low after hold v.hold_timer := 0; v.state := CPU_HAS_BUS; end if; @@ -401,6 +402,9 @@ begin -- out of the playback states. We could end up in a scenario where we are transitioning -- out of playback *right* on or near an CPU scl edge which can cause runt pulses and make -- things unhappy as we'll have possibly missed a bit due to i2c glitch filtering. + elsif mux_r.state = ENSURE_PLAYBACK_HOLD then + v.scl_cnts := 0; + -- scl will keep current state during the hold. elsif mux_r.state /= ENSURE_PLAYBACK_HOLD then v.scl_cnts := 0; v.scl_out := '1'; From 2319f5c668c8817a001013d1b1c1aeff0b13e785 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Fri, 15 May 2026 13:39:39 -0400 Subject: [PATCH 8/8] Fix review feedback --- .../dimms_subsystem/sims/dimm_arb_mux_tb.vhd | 76 +++++++++++-------- .../dimms_subsystem/sims/spd_proxy_top_tb.vhd | 9 ++- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd index 268ab52b..1fd475a6 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd @@ -6,9 +6,11 @@ -- Unit-level testbench for dimm_arb_mux. -- --- Bug 1 cannot be triggered at the integration level: the FPGA abort always finishes --- before the CPU's first SCL falling edge (~2-3 µs vs the 5 µs STANDARD half-period), --- so PLAY_STORED_START is entered while sample_r.state=SAMPLE_START and Bug 3 fires +-- https://github.com/oxidecomputer/quartz/issues/496 cannot be triggered at the integration +-- level: the FPGA abort always finishes +-- before the CPU's first SCL falling edge (~2-3 us vs the 5 us STANDARD half-period), +-- so PLAY_STORED_START is entered while sample_r.state=SAMPLE_START and +-- https://github.com/oxidecomputer/quartz/issues/498 fires -- instead. Direct stimulus control lets us reach PLAY_STORED_DATA with bit_count=1 -- while keeping cpu_scl_in='1' through the catch-up rising edge. @@ -120,8 +122,8 @@ begin while test_suite loop if run("arb_bug_sda_glitch_during_playback") then - -- GH # 469 - -- Release reset, wait for POWER_UP_CLEAR (~9 *� 1000 ns at FAST_PLUS) + -- Issue https://github.com/oxidecomputer/quartz/issues/469 + -- Release reset, wait for POWER_UP_CLEAR (~9 x 1000 ns at FAST_PLUS) -- and for dimm_i2c_idle to assert (~504 ns with both dimm lines held high). reset <= '1'; clk_tick; @@ -129,7 +131,7 @@ begin wait for 10 us; -- Step 1: CPU start condition: SDA falls while SCL is high. - -- cpu_start_detected fires� -> mux: IDLE ->�� CPU_REQ_GRANT. + -- cpu_start_detected fires -> mux: IDLE -> CPU_REQ_GRANT. -- sample: SAMPLE_IDLE -> SAMPLE_START. cpu_scl_in <= '1'; cpu_sda_in <= '0'; @@ -139,7 +141,7 @@ begin clk_tick; -- Step 2: CPU SCL falling edge. - -- sample: SAMPLE_START ->�� SAMPLE_DATA (bit_count still 0). + -- sample: SAMPLE_START -> SAMPLE_DATA (bit_count still 0). cpu_scl_in <= '0'; cpu_scl_fedge <= '1'; clk_tick; @@ -155,9 +157,10 @@ begin clk_tick; -- Step 4: Grant the bus. - -- sample_r.state=SAMPLE_DATA (not SAMPLE_START) �->� GH #498 cannot fire + -- sample_r.state=SAMPLE_DATA (not SAMPLE_START) -> + -- Issue https://github.com/oxidecomputer/quartz/issues/498 cannot fire -- even though cpu_scl_in='1'. - -- mux: CPU_REQ_GRANT ->�� PLAY_STORED_START (dimm_i2c_idle='1' already). + -- mux: CPU_REQ_GRANT -> PLAY_STORED_START (dimm_i2c_idle='1' already). bus_grant <= '1'; wait until sp5_playback_i2c_has_bus = '1'; @@ -169,13 +172,13 @@ begin -- rising edge, releasing SDA while the playback SCL is still high. -- -- Timeline from PLAY_STORED_START entry (playback SCL starts at '1'): - -- T + ~500 ns : first playback FEDGE → enter PLAY_STORED_DATA, + -- T + ~500 ns : first playback FEDGE -> enter PLAY_STORED_DATA, -- dimm_sda_oe set to not(data_bits(0))='1' (no change) -- T + ~1000 ns: first playback REDGE, playback_bits=1=bit_count, - -- cpu_scl_in='1' → Bug 1 exits to ENSURE_PLAYBACK_HOLD, + -- cpu_scl_in='1' -> Bug 1 exits to ENSURE_PLAYBACK_HOLD, -- schedules dimm_sda_oe='0' for next cycle - -- T + ~1008 ns: playback_sda_if.oe '1'→'0' while playback_scl_if.o='1' - -- → sda_glitch_monitor fires check_false → test fails + -- T + ~1008 ns: playback_sda_if.oe '1'->'0' while playback_scl_if.o='1' + -- -> sda_glitch_monitor fires check_false -> test fails cpu_sda_in <= '1'; wait for 2 us; @@ -183,9 +186,9 @@ begin wait for 500 ns; elsif run("arb_bug_playback_stall") then - -- Direct stimulus reproduction of Bug 2. + -- Direct stimulus reproduction of https://github.com/oxidecomputer/quartz/issues/497. -- - -- Bug GH # 497: in PLAY_STORED_DATA, when playback_bits catches up to bit_count + -- Bug: in PLAY_STORED_DATA, when playback_bits catches up to bit_count -- at a falling playback SCL edge while cpu_scl_in='1', the exit condition -- `playback_scl_fedge='1' and playback_bits=bit_count and cpu_scl_in='0'` -- is FALSE because cpu_scl_in is '1'. The following REDGE overshoots to @@ -193,14 +196,14 @@ begin -- so the machine spins in PLAY_STORED_DATA forever. -- -- The setup is identical to the Bug GH # 496 test (SAMPLE_DATA, bit_count=1) but - -- the check monitors sp5_i2c_has_bus rather than SDA. With Bug GH # 497 fixed the - -- mux exits to ENSURE_PLAYBACK_HOLD → CPU_HAS_BUS within ~2 us. + -- the check monitors sp5_i2c_has_bus rather than SDA. With GH # 497 fixed the + -- mux exits to ENSURE_PLAYBACK_HOLD -> CPU_HAS_BUS within ~2 us. reset <= '1'; clk_tick; reset <= '0'; wait for 10 us; - -- CPU start condition: SAMPLE_IDLE ->�� SAMPLE_START, mux: IDLE ->�� CPU_REQ_GRANT. + -- CPU start condition: SAMPLE_IDLE -> SAMPLE_START, mux: IDLE -> CPU_REQ_GRANT. cpu_scl_in <= '1'; cpu_sda_in <= '0'; cpu_sda_fedge <= '1'; @@ -208,7 +211,7 @@ begin cpu_sda_fedge <= '0'; clk_tick; - -- CPU SCL falling edge: SAMPLE_START �-> SAMPLE_DATA (bit_count=0). + -- CPU SCL falling edge: SAMPLE_START -> SAMPLE_DATA (bit_count=0). cpu_scl_in <= '0'; cpu_scl_fedge <= '1'; clk_tick; @@ -228,22 +231,33 @@ begin clk_tick; -- Grant bus with sample_r.state=SAMPLE_DATA (not SAMPLE_START) so Bug GH # 498 - -- cannot fire. Mux enters PLAY_STORED_START → PLAY_STORED_DATA. + -- cannot fire. Mux enters PLAY_STORED_START -> PLAY_STORED_DATA. bus_grant <= '1'; wait until sp5_playback_i2c_has_bus = '1'; -- Timeline from PLAY_STORED_START entry (playback SCL starts at '1'): - -- T + ~504 ns : FEDGE 1 → PLAY_STORED_DATA, playback_bits=0 - -- T + ~1008 ns: REDGE 1 → playback_bits=1; cpu_scl_in='0' blocks Bug 1 + -- T + ~504 ns : FEDGE 1 -> PLAY_STORED_DATA, playback_bits=0 + -- T + ~1008 ns: REDGE 1 -> playback_bits=1; cpu_scl_in='0' blocks Bug 1 -- T + ~1512 ns: FEDGE 2 (catch-up FEDGE for bit_count=1) -- Wait past REDGE 1 but before FEDGE 2, then raise cpu_scl_in='1' so the - -- Bug 2 exit condition (cpu_scl_in='0') is blocked at FEDGE 2. With Bug 2 + -- this bug's exit condition (cpu_scl_in='0') is blocked at FEDGE 2. With Bug 2 -- the mux then stalls in PLAY_STORED_DATA forever. wait for 1100 ns; cpu_scl_in <= '1'; - -- With Bug GH # 497 fixed, the FEDGE exit fires regardless of cpu_scl_in - -- when playback_bits=bit_count=1, and sp5_i2c_has_bus asserts within ~2 us. + -- Hold cpu_scl_in='1' past FEDGE 2 (~1512 ns) so we exercise the + -- PLAY_STORED_DATA FEDGE-exit-without-cpu_scl_in path, then drop + -- cpu_scl_in='0' so the ENSURE_PLAYBACK_HOLD handoff (gated on + -- cpu_scl_in='0' after the hold timer) can fire. + wait for 1 us; + cpu_scl_in <= '0'; + cpu_scl_fedge <= '1'; + clk_tick; + cpu_scl_fedge <= '0'; + + -- With this bug fixed, the FEDGE exit fires regardless of cpu_scl_in + -- when playback_bits=bit_count=1; once cpu_scl_in drops low the + -- ENSURE_PLAYBACK_HOLD handoff completes and sp5_i2c_has_bus asserts. wait until sp5_i2c_has_bus = '1' for 50 us; check_true(sp5_i2c_has_bus = '1', "Bug GH # 497: mux stalled in PLAY_STORED_DATA -- never reached CPU_HAS_BUS"); @@ -252,15 +266,15 @@ begin wait for 500 ns; elsif run("arb_bug_sample_error_stale_bitcount") then - -- Direct stimulus reproduction of Bug 4 (GH # 499). + -- Direct stimulus reproduction of https://github.com/oxidecomputer/quartz/issues/499. -- -- SAMPLE_ERROR is entered when bit_count reaches 7 (the bit_count subtype - -- saturates). The buggy SAMPLE_ERROR → SAMPLE_IDLE transition does not + -- saturates). The buggy SAMPLE_ERROR -> SAMPLE_IDLE transition does not -- reset bit_count, so the next sample_r.bit_count read returns the stale -- value 7. The fix sets v.bit_count := 0 in SAMPLE_ERROR. -- -- This is a white-box check: we drive seven CPU SCL rising edges to push - -- the sampler through SAMPLE_DATA → SAMPLE_ERROR → SAMPLE_IDLE, then read + -- the sampler through SAMPLE_DATA -> SAMPLE_ERROR -> SAMPLE_IDLE, then read -- the internal bit_count via an external name. bus_grant stays low so -- the mux remains in CPU_REQ_GRANT (playback_done='0') and does not -- short-circuit the sampler via mux_r.playback_done. @@ -269,7 +283,7 @@ begin reset <= '0'; wait for 10 us; - -- CPU start: SAMPLE_IDLE → SAMPLE_START (resets bit_count to 0). + -- CPU start: SAMPLE_IDLE -> SAMPLE_START (resets bit_count to 0). cpu_scl_in <= '1'; cpu_sda_in <= '0'; cpu_sda_fedge <= '1'; @@ -277,7 +291,7 @@ begin cpu_sda_fedge <= '0'; clk_tick; - -- First SCL falling edge: SAMPLE_START → SAMPLE_DATA. + -- First SCL falling edge: SAMPLE_START -> SAMPLE_DATA. cpu_scl_in <= '0'; cpu_scl_fedge <= '1'; clk_tick; @@ -301,7 +315,7 @@ begin end if; end loop; - -- Allow SAMPLE_ERROR → SAMPLE_IDLE to settle. + -- Allow SAMPLE_ERROR -> SAMPLE_IDLE to settle. for i in 0 to 3 loop clk_tick; end loop; diff --git a/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd b/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd index 6321ab9b..6866843b 100644 --- a/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd +++ b/hdl/projects/cosmo_seq/dimms_subsystem/sims/spd_proxy_top_tb.vhd @@ -38,7 +38,8 @@ begin bench: process alias reset is << signal th.reset : std_logic >>; - -- Bug 3 (PLAY_STORED_START hold-time) is the only dimm_arb_mux bug that is + -- Issue https://github.com/oxidecomputer/quartz/issues/498 + -- (PLAY_STORED_START hold-time) is the only dimm_arb_mux bug that is -- observable at the integration level: STANDARD-mode CPU SCL is too slow -- relative to the FAST_PLUS playback machine for Bugs 1/2/4 to manifest -- before the start-hold-time branch fires, so those bugs are covered by @@ -294,7 +295,7 @@ begin -- ----------------------------------------------------------------------- -- Bug regression tests for dimm_arb_mux -- - -- Only Bug 3 (start-condition hold time) is observable at the integration + -- Issue https://github.com/oxidecomputer/quartz/issues/498(start-condition hold time) is observable at the integration -- level here. With STANDARD-mode CPU SCL the FPGA abort completes long -- before the CPU's first SCL falling edge, so PLAY_STORED_START always -- fires the SAMPLE_START hold-time branch and Bugs 1/2/4 never get a @@ -303,12 +304,12 @@ begin -- ----------------------------------------------------------------------- elsif run("arb_bug_start_hold_time") then - -- Bug GH # 498: when the CPU starts a transaction while the bus is idle, the mux + -- when the CPU starts a transaction while the bus is idle, the mux -- reaches PLAY_STORED_START while sample_r.state=SAMPLE_START and -- cpu_scl_in='1' (CPU still in start hold-time). The immediate branch -- goes directly to CPU_HAS_BUS, skipping ENSURE_PLAYBACK_HOLD entirely. -- The DIMM bus sees the FPGA drive SDA low for only one system clock (~8 ns) - -- before releasing it, far below the t_HD;STA minimum of 260 ns (FAST_PLUS). + -- before releasing it, far below the t_HD_STA minimum of 260 ns (FAST_PLUS). -- Observable: playback_active drops after a single cycle instead of persisting -- through ENSURE_PLAYBACK_HOLD (~500 ns for FAST_PLUS at 125 MHz). --