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..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 @@ -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; @@ -152,15 +149,20 @@ 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 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 @@ -219,19 +221,21 @@ 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 - -- 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 + 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_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; + v.bit_count := 0; end case; sample_rin <= v; @@ -305,10 +309,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 @@ -331,22 +336,13 @@ 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); - 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'; @@ -359,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; @@ -406,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'; 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..1fd475a6 --- /dev/null +++ b/hdl/projects/cosmo_seq/dimms_subsystem/sims/dimm_arb_mux_tb.vhd @@ -0,0 +1,339 @@ +-- 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. +-- +-- 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. + +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 + -- 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; + 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) -> + -- 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). + 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 https://github.com/oxidecomputer/quartz/issues/497. + -- + -- 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 + -- 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 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 + -- 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'; + + -- 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"); + + bus_grant <= '0'; + wait for 500 ns; + + elsif run("arb_bug_sample_error_stale_bitcount") then + -- 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 + -- 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..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 @@ -32,18 +32,26 @@ 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 >>; + -- 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 + -- 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; 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); @@ -284,6 +292,51 @@ 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 + -- + -- 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 + -- 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 + -- 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). + -- + -- 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; + 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; + 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"); + -- 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 write_word(memory(I2C_DIMM1F_TGT_VC), 16#80#, X"AA");