From ea0bd66d5c6eab9f5c617a30193ba6ca70987028 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 5 Aug 2026 14:53:28 -0700 Subject: [PATCH 01/57] devices: add shared ADC DDR readout RTL --- .../UltraScale/rtl/Ad9249ReadoutGroup2.vhd | 580 ---------------- .../ad9249/core/Ad9249Readout.vhd | 190 ++++++ .../ad9249/core/Ad9249ReadoutBank.vhd | 152 +++++ devices/AnalogDevices/ad9249/ruckus.tcl | 5 +- .../ad9252/core/Ad9252Config.vhd | 206 ++++++ .../AnalogDevices/ad9252/core/Ad9252Pkg.vhd | 35 + .../ad9252/core/Ad9252Readout.vhd | 156 +++++ devices/AnalogDevices/ad9252/ruckus.tcl | 14 + .../ad9681/7Series/rtl/Ad9681Deserializer.vhd | 121 ---- .../ad9681/7Series/rtl/Ad9681Readout.vhd | 639 ------------------ .../7Series/rtl/Ad9681ReadoutManual.vhd | 600 ---------------- .../AnalogDevices/ad9681/7Series/ruckus.tcl | 5 - .../ad9681/core/Ad9681Readout.vhd | 207 ++++++ devices/AnalogDevices/ad9681/ruckus.tcl | 23 +- .../7Series/rtl/AdcDdrDeserializer7Series.vhd | 180 +++++ .../adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd | 220 ++++++ .../AnalogDevices/adcDdr/7Series/ruckus.tcl | 13 + .../rtl/AdcDdrDeserializerUltraScale.vhd | 134 ++++ .../UltraScale/rtl/AdcDdrPhyUltraScale.vhd | 182 +++++ .../adcDdr/UltraScale/ruckus.tcl | 13 + .../AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd | 608 +++++++++++++++++ .../adcDdr/rtl/AdcDdrPatternTester.vhd | 389 +++++++++++ .../AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd | 191 ++++++ .../AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd | 123 ++++ devices/AnalogDevices/adcDdr/ruckus.tcl | 34 + devices/AnalogDevices/ruckus.tcl | 2 + 26 files changed, 3053 insertions(+), 1969 deletions(-) delete mode 100644 devices/AnalogDevices/ad9249/UltraScale/rtl/Ad9249ReadoutGroup2.vhd create mode 100644 devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd create mode 100644 devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd create mode 100644 devices/AnalogDevices/ad9252/core/Ad9252Config.vhd create mode 100644 devices/AnalogDevices/ad9252/core/Ad9252Pkg.vhd create mode 100644 devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd create mode 100644 devices/AnalogDevices/ad9252/ruckus.tcl delete mode 100755 devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Deserializer.vhd delete mode 100644 devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Readout.vhd delete mode 100644 devices/AnalogDevices/ad9681/7Series/rtl/Ad9681ReadoutManual.vhd delete mode 100644 devices/AnalogDevices/ad9681/7Series/ruckus.tcl create mode 100644 devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd create mode 100644 devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrDeserializer7Series.vhd create mode 100644 devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd create mode 100644 devices/AnalogDevices/adcDdr/7Series/ruckus.tcl create mode 100644 devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd create mode 100644 devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrPhyUltraScale.vhd create mode 100644 devices/AnalogDevices/adcDdr/UltraScale/ruckus.tcl create mode 100644 devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd create mode 100644 devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd create mode 100644 devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd create mode 100644 devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd create mode 100644 devices/AnalogDevices/adcDdr/ruckus.tcl diff --git a/devices/AnalogDevices/ad9249/UltraScale/rtl/Ad9249ReadoutGroup2.vhd b/devices/AnalogDevices/ad9249/UltraScale/rtl/Ad9249ReadoutGroup2.vhd deleted file mode 100644 index e9daaf4ddf..0000000000 --- a/devices/AnalogDevices/ad9249/UltraScale/rtl/Ad9249ReadoutGroup2.vhd +++ /dev/null @@ -1,580 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: --- ADC Readout Controller --- Receives ADC Data from an AD9592 chip. --- Designed specifically for Xilinx 7 series FPGAs -------------------------------------------------------------------------------- --- This file is part of 'SLAC Firmware Standard Library'. --- It is subject to the license terms in the LICENSE.txt file found in the --- top-level directory of this distribution and at: --- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. --- No part of 'SLAC Firmware Standard Library', including this file, --- may be copied, modified, propagated, or distributed except according to --- the terms contained in the LICENSE.txt file. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_arith.all; -use ieee.std_logic_unsigned.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.AxiLitePkg.all; -use surf.AxiStreamPkg.all; -use surf.Ad9249Pkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9249ReadoutGroup2 is - generic ( - TPD_G : time := 1 ns; - SIM_DEVICE_G : string := "ULTRASCALE"; - NUM_CHANNELS_G : natural := 8; - SIMULATION_G : boolean := false; - DEFAULT_DELAY_G : slv(8 downto 0) := "000000000"; - ADC_INVERT_CH_G : slv(7 downto 0) := "00000000"); - port ( - -- AXI-Lite clock - axilClk : in sl; - axilRst : in sl; - - -- Axi Interface - axilWriteMaster : in AxiLiteWriteMasterType; - axilWriteSlave : out AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; - axilReadMaster : in AxiLiteReadMasterType; - axilReadSlave : out AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; - - -- Asynchronous reset for adc deserializer - adcClkRst : in sl; - - -- DDR Serial Data from ADC - adcSerial : in Ad9249SerialGroupType; - - -- Deserialized ADC Data - adcStreamClk : in sl; - adcStreams : out AxiStreamMasterArray(NUM_CHANNELS_G-1 downto 0) := (others => axiStreamMasterInit(AD9249_AXIS_CFG_G))); -end Ad9249ReadoutGroup2; - -architecture rtl of Ad9249ReadoutGroup2 is - - ------------------------------------------------------------------------------------------------- - -- AXIL Registers - ------------------------------------------------------------------------------------------------- - type AxilRegType is record - axilWriteSlave : AxiLiteWriteSlaveType; - axilReadSlave : AxiLiteReadSlaveType; - delay : slv(8 downto 0); - delaySet : sl; - freezeDebug : sl; - readoutDebug0 : slv16Array(7 downto 0); - readoutDebug1 : slv16Array(7 downto 0); - lockedCountRst : sl; - invert : sl; - realign : sl; - minEyeWidth : slv(7 downto 0); - end record; - - constant AXIL_REG_INIT_C : AxilRegType := ( - axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, - axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, - delay => DEFAULT_DELAY_G, - delaySet => '0', - freezeDebug => '0', - readoutDebug0 => (others => (others => '0')), - readoutDebug1 => (others => (others => '0')), - lockedCountRst => '0', - invert => '0', - realign => '1', - minEyeWidth => X"50"); - - signal lockedSync : sl; - signal lockedFallCount : slv(15 downto 0); - - signal axilR : AxilRegType := AXIL_REG_INIT_C; - signal axilRin : AxilRegType; - - ------------------------------------------------------------------------------------------------- - -- ADC Readout Clocked Registers - ------------------------------------------------------------------------------------------------- - type AdcRegType is record - errorDet : sl; - end record; - - constant ADC_REG_INIT_C : AdcRegType := ( - errorDet => '1'); - - - signal adcR : AdcRegType := ADC_REG_INIT_C; - signal adcRin : AdcRegType; - - - -- Local Signals - signal adcBitClk : sl; - signal adcBitRst : sl; - signal adcBitClkDiv4 : sl; - signal adcBitRstDiv4 : sl; - - signal adcFrame : slv(13 downto 0); - signal adcFrameValid : sl; - signal adcFrameSync : slv(13 downto 0); - signal adcFrameSyncValid : sl; - signal adcData : slv14Array(NUM_CHANNELS_G-1 downto 0); - signal adcDataValid : slv(NUM_CHANNELS_G-1 downto 0); - - signal fifoWrData : slv16Array(NUM_CHANNELS_G-1 downto 0); - signal fifoDataValid : sl; - signal fifoDataOut : slv(NUM_CHANNELS_G*16-1 downto 0); - signal fifoDataIn : slv(NUM_CHANNELS_G*16-1 downto 0); - signal fifoDataTmp : slv16Array(NUM_CHANNELS_G-1 downto 0); - - signal debugDataValid : sl; - signal debugDataOut : slv(NUM_CHANNELS_G*16-1 downto 0); - signal debugDataTmp : slv16Array(7 downto 0) := (others => (others => '0')); - - signal invertSync : sl; - signal bitSlip : sl; - signal dlyLoad : sl; - signal dlyCfg : slv(8 downto 0); - signal enUsrDlyCfg : sl; - signal usrDlyCfg : slv(8 downto 0) := (others => '0'); - signal minEyeWidthSync : slv(7 downto 0); - signal lockingCntCfg : slv(23 downto 0) := ite(SIMULATION_G, X"000008", X"00FFFF"); - signal locked : sl; - signal realignSync : sl; - signal curDelay : slv(8 downto 0); - signal errorDetCount : slv(15 downto 0); - signal errorDet : sl; - - -begin - - ------------------------------------------------------------------------------------------------- - -- Synchronize adcR.locked across to axil clock domain and count falling edges on it - ------------------------------------------------------------------------------------------------- - Synchronizer_locked : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => locked, - dataOut => lockedSync); - - SynchronizerOneShotCnt_locked_fall : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '0', - CNT_RST_EDGE_G => true, - CNT_WIDTH_G => 16) - port map ( - dataIn => locked, - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => lockedFallCount, - wrClk => adcBitClkDiv4, - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - SynchronizerOneShotCnt_2 : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '1', - OUT_POLARITY_G => '1', - CNT_RST_EDGE_G => false, - CNT_WIDTH_G => 16) - port map ( - dataIn => errorDet, - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => errorDetCount, - wrClk => adcBitClkDiv4, - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - - SynchronizerVector_FRAME : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => 14, - ADDR_WIDTH_G => 4) - port map ( - rst => axilRst, - wr_clk => adcBitClkDiv4, - wr_en => adcFrameValid, - din => adcFrame, - rd_clk => axilClk, - rd_en => adcFrameSyncValid, - valid => adcFrameSyncValid, - dout => adcFrameSync); - - U_SynchronizerVector_CUR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 9) - port map ( - clk => axilClk, -- [in] - rst => axilRst, -- [in] - dataIn => dlyCfg, -- [in] - dataOut => curDelay); -- [out] - - - -- AXIL to ADC clock - Synchronizer_INVERT : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkDiv4, - dataIn => axilR.invert, - dataOut => invertSync); - - Synchronizer_REALIGN : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 3) - port map ( - clk => adcBitClkDiv4, - asyncRst => axilR.realign, - syncRst => realignSync); - - Synchronizer_USR_DELAY_SET : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 3) - port map ( - clk => adcBitClkDiv4, - rst => adcBitRstDiv4, - dataIn => axilR.delaySet, - dataOut => enUsrDlyCfg); - - U_SynchronizerVector_USR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 9) - port map ( - clk => adcBitClkDiv4, -- [in] - rst => adcBitRstDiv4, -- [in] - dataIn => axilR.delay, -- [in] - dataOut => usrDlyCfg); -- [out] - - U_SynchronizerVector_EYE_WIDTH : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => adcBitClkDiv4, -- [in] - rst => adcBitRstDiv4, -- [in] - dataIn => axilR.minEyeWidth, -- [in] - dataOut => minEyeWidthSync); -- [out] - - - -------------------------------------------------------------------------------------------------- --- AXIL Interface -------------------------------------------------------------------------------------------------- - axilComb : process (adcFrameSync, axilR, axilReadMaster, axilRst, - axilWriteMaster, curDelay, debugDataTmp, debugDataValid, - errorDetCount, lockedFallCount, lockedSync) is - variable v : AxilRegType; - variable axilEp : AxiLiteEndpointType; - begin - v := axilR; - - v.delaySet := '0'; - - -- Store last two samples read from ADC - if (debugDataValid = '1' and axilR.freezeDebug = '0') then - v.readoutDebug0 := debugDataTmp; - v.readoutDebug1 := axilR.readoutDebug0; - end if; - - axiSlaveWaitTxn(axilEp, axilWriteMaster, axilReadMaster, v.axilWriteSlave, v.axilReadSlave); - - -- Write delay values to IDELAY primitives - -- Overriding gearbox aligner - -- All writes go to same r.delay register, - axiSlaveRegister(axilEp, X"00", 0, v.delay); - axiWrDetect(axilEp, X"00", v.delaySet); - axiSlaveRegisterR(axilEp, X"00", 0, curDelay); - - v.realign := '0'; - axiSlaveRegister(axilEp, X"20", 0, v.realign); - axiSlaveRegisterR(axilEp, X"30", 0, errorDetCount); - - -- Debug output to see how many times the shift has needed a relock - axiSlaveRegisterR(axilEp, X"50", 0, lockedFallCount); - axiSlaveRegisterR(axilEp, X"50", 16, lockedSync); - - axiSlaveRegisterR(axilEp, X"58", 0, adcFrameSync); - - axiSlaveRegister(axilEp, X"5C", 0, v.lockedCountRst); - - axiSlaveRegister(axilEp, X"60", 0, v.invert); - - -- Debug registers. Output the last 2 words received - for ch in 0 to 7 loop - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 0, axilR.readoutDebug0(ch)); - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 16, axilR.readoutDebug1(ch)); - end loop; - - axiSlaveRegister(axilEp, X"A0", 0, v.freezeDebug); - - axiSlaveDefault(axilEp, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); - - if (axilRst = '1') then - v := AXIL_REG_INIT_C; - end if; - - axilRin <= v; - axilWriteSlave <= axilR.axilWriteSlave; - axilReadSlave <= axilR.axilReadSlave; - - end process; - - axilSeq : process (axilClk) is - begin - if (rising_edge(axilClk)) then - axilR <= axilRin after TPD_G; - end if; - end process axilSeq; - - -------------------------------------------------------------------------------------------------- --- Create Clocks -------------------------------------------------------------------------------------------------- - - AdcClk_I_Ibufds : IBUFGDS - port map ( - I => adcSerial.dClkP, - IB => adcSerial.dClkN, - O => adcBitClk); - - - ADC_BITCLK_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClk, - asyncRst => adcClkRst, - syncRst => adcBitRst); - - - U_AdcBitClkRD4 : BUFGCE_DIV - generic map ( - BUFGCE_DIVIDE => 4, -- 1-8 - -- Programmable Inversion Attributes: Specifies built-in programmable inversion on specific pins - IS_CE_INVERTED => '0', -- Optional inversion for CE - IS_CLR_INVERTED => '0', -- Optional inversion for CLR - IS_I_INVERTED => '0') -- Optional inversion for I - port map ( - I => adcBitClk, - O => adcBitClkDiv4, - CE => '1', - CLR => '0'); - - - ADC_BITCLK_DIV4_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClkDiv4, - asyncRst => adcClkRst, - syncRst => adcBitRstDiv4); - - -------------------------------------------------------------------------------------------------- --- Deserializers -------------------------------------------------------------------------------------------------- - - U_FRAME_DESERIALIZER : entity surf.Ad9249Deserializer - generic map ( - TPD_G => TPD_G, - SIM_DEVICE_G => SIM_DEVICE_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IDELAYCTRL_FREQ_G => 350.0, -- Check this - ADC_INVERT_CH_G => '0', - BIT_REV_G => '1') - port map ( - dClk => adcBitClk, - dRst => adcBitRst, - dClkDiv4 => adcBitClkDiv4, - dRstDiv4 => realignSync, - sDataP => adcSerial.fClkP, - sDataN => adcSerial.fClkN, - loadDelay => dlyLoad, - delay => dlyCfg, - bitSlip => bitSlip, - delayValueOut => open, - adcData => adcFrame, - adcValid => adcFrameValid); - - --------------------------------- --- Data Input, 8 channels --------------------------------- - GenData : for ch in NUM_CHANNELS_G-1 downto 0 generate - U_DATA_DESERIALIZER : entity surf.Ad9249Deserializer - generic map ( - TPD_G => TPD_G, - SIM_DEVICE_G => SIM_DEVICE_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IDELAYCTRL_FREQ_G => 350.0, -- Check this - ADC_INVERT_CH_G => ADC_INVERT_CH_G(ch), - BIT_REV_G => '1') -- Should maybe be '1' - port map ( - dClk => adcBitClk, - dRst => adcBitRst, - dClkDiv4 => adcBitClkDiv4, - dRstDiv4 => realignSync, - sDataP => adcSerial.chP(ch), - sDataN => adcSerial.chN(ch), - loadDelay => dlyLoad, - delay => dlyCfg, - bitSlip => bitSlip, - delayValueOut => open, - adcData => adcData(ch), - adcValid => adcDataValid(ch)); - end generate; - - - ---------------------------------------------------------------------------------------------- - -- Aligner - ---------------------------------------------------------------------------------------------- - U_SelectIoRxGearboxAligner_1 : entity surf.SelectIoRxGearboxAligner - generic map ( - TPD_G => TPD_G, - SIMULATION_G => SIMULATION_G, - CODE_TYPE_G => "LINE_CODE", - DLY_STEP_SIZE_G => ite(SIMULATION_G, 16, 1)) - port map ( - clk => adcBitClkDiv4, -- [in] - rst => adcBitRstDiv4, -- [in] - lineCodeValid => '1', -- [in] - lineCodeErr => adcR.errorDet, -- [in] - lineCodeDispErr => realignSync, -- [in] - linkOutOfSync => '0', -- [in] - rxHeaderValid => '0', -- [in] - rxHeader => (others => '0'), -- [in] - bitSlip => bitSlip, -- [out] - dlyLoad => dlyLoad, -- [out] - dlyCfg => dlyCfg, -- [out] - enUsrDlyCfg => enUsrDlyCfg, -- [in] - usrDlyCfg => usrDlyCfg, -- [in] - bypFirstBerDet => '1', -- [in] - minEyeWidth => minEyeWidthSync, -- [in] - lockingCntCfg => lockingCntCfg, -- [in] - errorDet => errorDet, -- [out] - locked => locked); -- [out] - - - ------------------------------------------------------------------------------------------------- - -- ADC Bit Clocked Logic - ------------------------------------------------------------------------------------------------- - adcComb : process (adcFrame, adcFrameValid, adcR) is - variable v : AdcRegType; - begin - v := adcR; - - if (adcFrameValid = '1') then - v.errorDet := toSl(adcFrame /= "11111110000000"); - end if; - - adcRin <= v; - - end process adcComb; - - adcSeq : process (adcBitClkDiv4, adcBitRstDiv4) is - begin - if (adcBitRstDiv4 = '1') then - adcR <= ADC_REG_INIT_C after TPD_G; - elsif (rising_edge(adcBitClkDiv4)) then - adcR <= adcRin after TPD_G; - end if; - end process adcSeq; - - - GLUE_COMB : process (adcData, invertSync, locked) is - begin - for ch in NUM_CHANNELS_G-1 downto 0 loop - if (locked = '1') then - -- Locked, output adc data - if invertSync = '1' then - -- Invert all bits but keep 2 LSBs clear - fifoWrData(ch) <= "00" & ("11111111111111" - adcData(ch)); - else - fifoWrData(ch) <= "00" & adcData(ch); - end if; - else - -- Not locked - fifoWrData(ch) <= (others => '1'); --"10" & "00000000000000"; - end if; - end loop; - end process GLUE_COMB; - - --- Flatten fifoWrData onto fifoDataIn for FIFO --- Regroup fifoDataOut by channel into fifoDataTmp --- Format fifoDataTmp into AxiStream channels - glue : for i in NUM_CHANNELS_G-1 downto 0 generate - fifoDataIn(i*16+15 downto i*16) <= fifoWrData(i); - fifoDataTmp(i) <= fifoDataOut(i*16+15 downto i*16); - debugDataTmp(i) <= debugDataOut(i*16+15 downto i*16); - adcStreams(i).tdata(15 downto 0) <= fifoDataTmp(i); - adcStreams(i).tDest <= toSlv(i, 8); - adcStreams(i).tValid <= fifoDataValid; - end generate; - - -- Single fifo to synchronize adc data to the Stream clock - U_DataFifo : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_G*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRstDiv4, - wr_clk => adcBitClkDiv4, - wr_en => adcFrameValid, --Always write data - din => fifoDataIn, - rd_clk => adcStreamClk, - rd_en => fifoDataValid, - valid => fifoDataValid, - dout => fifoDataOut); - - U_DataFifoDebug : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_G*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRstDiv4, - wr_clk => adcBitClkDiv4, - wr_en => adcFrameValid, --Always write data - din => fifoDataIn, - rd_clk => axilClk, - rd_en => debugDataValid, - valid => debugDataValid, - dout => debugDataOut); - - -end rtl; - diff --git a/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd b/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd new file mode 100644 index 0000000000..39ef347835 --- /dev/null +++ b/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd @@ -0,0 +1,190 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Full-device AD9249 serialized readout +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9249Pkg.all; + +entity Ad9249Readout is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + IODELAY_GROUP_0_G : string := "DEFAULT_GROUP_0"; + IODELAY_GROUP_1_G : string := "DEFAULT_GROUP_1"; + IDELAYCTRL_FREQ_G : real := 200.0; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + DATA_DELAY_INIT_G : NaturalArray(15 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(1 downto 0) := (others => 0); + ADC_INVERT_CH_G : slv(15 downto 0) := (others => '0'); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in slv(1 downto 0) := (others => '1'); + adcSerial : in Ad9249SerialGroupArray(1 downto 0); + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(15 downto 0)); +end entity Ad9249Readout; + +architecture rtl of Ad9249Readout is + + constant AXIL_CONFIG_C : AxiLiteCrossbarMasterConfigArray(1 downto 0) := + genAxiLiteConfig(2, AXIL_BASE_ADDR_G, 13, 12); + + function bankDataDelay ( + values : NaturalArray; + bank : natural) + return NaturalArray is + variable result : NaturalArray(7 downto 0); + begin + for ch in 7 downto 0 loop + result(ch) := values((8*bank)+ch); + end loop; + return result; + end function bankDataDelay; + + function bankFcoDelay ( + values : NaturalArray; + bank : natural) + return NaturalArray is + variable result : NaturalArray(0 downto 0); + begin + result(0) := values(bank); + return result; + end function bankFcoDelay; + + function bankInvert (values : slv; bank : natural) return slv is + variable result : slv(7 downto 0); + begin + for ch in 7 downto 0 loop + result(ch) := values((8*bank)+ch); + end loop; + return result; + end function bankInvert; + + signal bankWriteMasters : AxiLiteWriteMasterArray(1 downto 0); + signal bankWriteSlaves : AxiLiteWriteSlaveArray(1 downto 0); + signal bankReadMasters : AxiLiteReadMasterArray(1 downto 0); + signal bankReadSlaves : AxiLiteReadSlaveArray(1 downto 0); + signal bank0Streams : AxiStreamMasterArray(7 downto 0); + signal bank1Streams : AxiStreamMasterArray(7 downto 0); + +begin + + ------------------------------------------------------------------------------------------------- + -- Decode one 8-KiB device window into independent 4-KiB bank register + -- regions. Each bank keeps its own DCO/FCO capture domain and AdcDdrCore. + ------------------------------------------------------------------------------------------------- + U_AxiLiteCrossbar : entity surf.AxiLiteCrossbar + generic map ( + TPD_G => TPD_G, + NUM_SLAVE_SLOTS_G => 1, + NUM_MASTER_SLOTS_G => 2, + MASTERS_CONFIG_G => AXIL_CONFIG_C) + port map ( + axiClk => axilClk, -- [in] + axiClkRst => axilRst, -- [in] + sAxiWriteMasters(0) => axilWriteMaster, -- [in] + sAxiWriteSlaves(0) => axilWriteSlave, -- [out] + sAxiReadMasters(0) => axilReadMaster, -- [in] + sAxiReadSlaves(0) => axilReadSlave, -- [out] + mAxiWriteMasters => bankWriteMasters, -- [out] + mAxiWriteSlaves => bankWriteSlaves, -- [in] + mAxiReadMasters => bankReadMasters, -- [out] + mAxiReadSlaves => bankReadSlaves); -- [in] + + U_Bank0 : entity surf.Ad9249ReadoutBank + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_CONFIG_C(0).baseAddr, + NUM_CHANNELS_G => 8, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_0_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_DELAY_INIT_G => bankDataDelay(DATA_DELAY_INIT_G, 0), + FCO_DELAY_INIT_G => bankFcoDelay(FCO_DELAY_INIT_G, 0), + ADC_INVERT_CH_G => bankInvert(ADC_INVERT_CH_G, 0), + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilWriteMaster => bankWriteMasters(0), -- [in] + axilWriteSlave => bankWriteSlaves(0), -- [out] + axilReadMaster => bankReadMasters(0), -- [in] + axilReadSlave => bankReadSlaves(0), -- [out] + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy(0), -- [in] + adcSerial => adcSerial(0), -- [in] + adcStreamClk => adcStreamClk, -- [in] + adcStreamRst => adcStreamRst, -- [in] + adcStreams => bank0Streams); -- [out] + + U_Bank1 : entity surf.Ad9249ReadoutBank + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_CONFIG_C(1).baseAddr, + NUM_CHANNELS_G => 8, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_1_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_DELAY_INIT_G => bankDataDelay(DATA_DELAY_INIT_G, 1), + FCO_DELAY_INIT_G => bankFcoDelay(FCO_DELAY_INIT_G, 1), + ADC_INVERT_CH_G => bankInvert(ADC_INVERT_CH_G, 1), + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilWriteMaster => bankWriteMasters(1), -- [in] + axilWriteSlave => bankWriteSlaves(1), -- [out] + axilReadMaster => bankReadMasters(1), -- [in] + axilReadSlave => bankReadSlaves(1), -- [out] + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy(1), -- [in] + adcSerial => adcSerial(1), -- [in] + adcStreamClk => adcStreamClk, -- [in] + adcStreamRst => adcStreamRst, -- [in] + adcStreams => bank1Streams); -- [out] + + mapStreams : process (bank0Streams, bank1Streams) is + variable v : AxiStreamMasterArray(15 downto 0); + begin + for ch in 7 downto 0 loop + v(ch) := bank0Streams(ch); + v(ch).tDest := toSlv(ch, 8); + v(8+ch) := bank1Streams(ch); + v(8+ch).tDest := toSlv(8+ch, 8); + end loop; + adcStreams <= v; + end process mapStreams; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd new file mode 100644 index 0000000000..7db9a08b97 --- /dev/null +++ b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd @@ -0,0 +1,152 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Readout for one eight-channel AD9249 output bank +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9249Pkg.all; +use surf.AdcDdrPkg.all; + +entity Ad9249ReadoutBank is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + NUM_CHANNELS_G : natural range 1 to 8 := 8; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_DELAY_INIT_G : NaturalArray(NUM_CHANNELS_G-1 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(0 downto 0) := (others => 0); + ADC_INVERT_CH_G : slv(7 downto 0) := (others => '0'); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in sl := '1'; + adcSerial : in Ad9249SerialGroupType; + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(NUM_CHANNELS_G-1 downto 0)); +end entity Ad9249ReadoutBank; + +architecture rtl of Ad9249ReadoutBank is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant DELAY_BITS_C : positive := adcDdrDelayBits(DEVICE_FAMILY_G); + + signal adcWordClk : sl; + signal adcWordRst : sl; + signal phyReset : sl; + signal delayReady : sl; + + signal dataWord : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal dataValid : slv(NUM_CHANNELS_G-1 downto 0); + signal sampleIn : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal fcoWord : Slv16Array(0 downto 0); + signal fcoValid : slv(0 downto 0); + + signal bitSlip : slv(0 downto 0); + signal dataDelay : AdcDdrDelayArray(NUM_CHANNELS_G-1 downto 0); + signal frameDelay : AdcDdrDelayArray(0 downto 0); + +begin + + U_Phy : entity surf.AdcDdrPhy + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + SERIALIZATION_FACTOR_G => 14, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_FCO_MAP_G => (others => 0)) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => adcSerial.dClkP, -- [in] + dClkN => adcSerial.dClkN, -- [in] + fcoP => (0 => adcSerial.fClkP), -- [in] + fcoN => (0 => adcSerial.fClkN), -- [in] + dataP => adcSerial.chP(NUM_CHANNELS_G-1 downto 0), -- [in] + dataN => adcSerial.chN(NUM_CHANNELS_G-1 downto 0), -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelay, -- [in] + fcoDelayWrite => frameDelay, -- [in] + captureClk => adcWordClk, -- [out] + captureRst => adcWordRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + + GEN_CHANNEL : for i in NUM_CHANNELS_G-1 downto 0 generate + sampleIn(i) <= "00" & ite(ADC_INVERT_CH_G(i) = '1', + not dataWord(i)(13 downto 0), dataWord(i)(13 downto 0)); + end generate GEN_CHANNEL; + + U_Core : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + CHANNELS_G => NUM_CHANNELS_G, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + DELAY_BITS_G => DELAY_BITS_C, + DATA_DELAY_INIT_G => DATA_DELAY_INIT_G, + FCO_DELAY_INIT_G => FCO_DELAY_INIT_G, + FRAME_PATTERN_G => FRAME_PATTERN_C, + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + captureClk => adcWordClk, -- [in] + captureRst => adcWordRst, -- [in] + delayReady => delayReady, -- [in] + fcoWord => fcoWord, -- [in] + fcoValid => fcoValid, -- [in] + sampleValid => uAnd(dataValid), -- [in] + sampleIn => sampleIn, -- [in] + phyReset => phyReset, -- [out] + bitSlip => bitSlip, -- [out] + dataDelayWrite => dataDelay, -- [out] + fcoDelayWrite => frameDelay, -- [out] + streamClk => adcStreamClk, -- [in] + streamRst => adcStreamRst, -- [in] + streams => adcStreams); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/ruckus.tcl b/devices/AnalogDevices/ad9249/ruckus.tcl index 35ed2898d2..3b236cbf5a 100644 --- a/devices/AnalogDevices/ad9249/ruckus.tcl +++ b/devices/AnalogDevices/ad9249/ruckus.tcl @@ -2,8 +2,9 @@ source $::env(RUCKUS_PROC_TCL) # Load Source Code -loadSource -lib surf -dir "$::DIR_PATH/core" +loadSource -lib surf -dir "$::DIR_PATH/core" -fileType "VHDL 2008" loadSource -lib surf -sim_only -dir "$::DIR_PATH/tb" +loadRuckusTcl "$::DIR_PATH/sim" # Get the family type set family [getFpgaArch] @@ -23,4 +24,4 @@ if { ${family} eq {kintexu} || ${family} eq {zynquplus} || ${family} eq {zynquplusRFSOC} } { loadRuckusTcl "$::DIR_PATH/UltraScale" -} \ No newline at end of file +} diff --git a/devices/AnalogDevices/ad9252/core/Ad9252Config.vhd b/devices/AnalogDevices/ad9252/core/Ad9252Config.vhd new file mode 100644 index 0000000000..d049d2c9d4 --- /dev/null +++ b/devices/AnalogDevices/ad9252/core/Ad9252Config.vhd @@ -0,0 +1,206 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Imported AD9252 support. +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.std_logic_arith.all; +use ieee.std_logic_unsigned.all; + +library unisim; +use unisim.vcomponents.all; + + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; + +entity Ad9252Config is + + generic ( + TPD_G : time := 1 ns; + SIMULATION_G : boolean := false); + + port ( + axiClk : in sl; + axiRst : in sl; + + axiReadMaster : in AxiLiteReadMasterType; + axiReadSlave : out AxiLiteReadSlaveType; + axiWriteMaster : in AxiLiteWriteMasterType; + axiWriteSlave : out AxiLiteWriteSlaveType; + + adcSclk : out sl; + adcSdio : inout sl; + adcCsb : out sl + ); + +end entity Ad9252Config; + +architecture rtl of Ad9252Config is + + -- AdcCore Outputs + signal rdData : slv(23 downto 0); + signal rdEn : sl; + + -- Adc Core Chip IO + signal coreSclk : sl; + signal coreSDin : sl; + signal coreSDout : sl; + signal coreCsb : sl; + + type StateType is (WAIT_AXI_TXN_S, WAIT_CYCLE_S, WAIT_SPI_TXN_DONE_S); + + -- Registers + type RegType is record + state : StateType; + axiReadSlave : AxiLiteReadSlaveType; + axiWriteSlave : AxiLiteWriteSlaveType; + -- Adc Core Inputs + wrData : slv(23 downto 0); + wrEn : sl; + end record RegType; + + constant REG_INIT_C : RegType := ( + state => WAIT_AXI_TXN_S, + axiReadSlave => AXI_LITE_READ_SLAVE_INIT_C, + axiWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, + wrData => (others => '0'), + wrEn => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + comb : process (axiRst, axiReadMaster, axiWriteMaster, r, rdData, rdEn) is + variable v : RegType; + variable axiStatus : AxiLiteStatusType; + begin + v := r; + + axiSlaveWaitTxn(axiWriteMaster, axiReadMaster, v.axiWriteSlave, v.axiReadSlave, axiStatus); + + case (r.state) is + when WAIT_AXI_TXN_S => + + if (axiStatus.writeEnable = '1') then + v.wrData(23) := '0'; -- Write bit + v.wrData(22 downto 21) := "00"; -- Number of bytes (1) + v.wrData(20 downto 16) := "00000"; -- Unused address bits + v.wrData(15 downto 8) := axiWriteMaster.awaddr(9 downto 2); -- Address + v.wrData(7 downto 0) := axiWriteMaster.wdata(7 downto 0); -- Data + v.wrEn := '1'; + v.state := WAIT_CYCLE_S; + end if; + + if (axiStatus.readEnable = '1') then + v.wrData(23) := '1'; -- read bit + v.wrData(22 downto 21) := "00"; -- Number of bytes (1) + v.wrData(20 downto 16) := "00000"; -- Unused address bits + v.wrData(15 downto 8) := axiReadMaster.araddr(9 downto 2); -- Address + v.wrData(7 downto 0) := (others => '1'); -- Make bus float to Z so slave can + -- drive during data segment + v.wrEn := '1'; + v.state := WAIT_CYCLE_S; + end if; + + when WAIT_CYCLE_S => + -- Wait 1 cycle for rdEn to drop + v.wrEn := '0'; + v.state := WAIT_SPI_TXN_DONE_S; + + when WAIT_SPI_TXN_DONE_S => + + if (rdEn = '1') then + v.state := WAIT_AXI_TXN_S; + if (r.wrData(23) = '0') then + -- Finish write + axiSlaveWriteResponse(v.axiWriteSlave); + else + -- Finish read + v.axiReadSlave.rdata := (others => '0'); + v.axiReadSlave.rdata(7 downto 0) := rdData(7 downto 0); + axiSlaveReadResponse(v.axiReadSlave); + end if; + end if; + + when others => null; + end case; + + if (axiRst = '1') then + v := REG_INIT_C; + end if; + + rin <= v; + + axiWriteSlave <= r.axiWriteSlave; + axiReadSlave <= r.axiReadSlave; + + end process comb; + + seq : process (axiClk) is + begin + if (rising_edge(axiClk)) then + r <= rin after TPD_G; + end if; + end process seq; + + SpiMaster_1 : entity surf.SpiMaster + generic map ( + TPD_G => TPD_G, + NUM_CHIPS_G => 1, + DATA_SIZE_G => 24, + CPHA_G => '0', -- Sample on leading edge + CPOL_G => '0', -- Sample on rising edge + CLK_PERIOD_G => 8.0E-9, + SPI_SCLK_PERIOD_G => ite(SIMULATION_G, 100.0E-9, 100.0E-6)) + port map ( + clk => axiClk, + sRst => axiRst, + chipSel => "0", + wrEn => r.wrEn, + wrData => r.wrData, + rdEn => rdEn, + rdData => rdData, + spiCsL(0) => coreCsb, + spiSclk => coreSclk, + spiSdi => coreSDout, + spiSdo => coreSDin); + + -- Bus lines float to Z when not being driven to '0'. + -- Lines should all have resistor pullups off chip + SCLK_OBUFT : OBUFT + port map ( + I => '0', + O => adcSclk, + T => coreSclk); + + SDIO_IOBUFT : IOBUF + port map ( + I => '0', + O => coreSDin, + IO => adcSdio, + T => coreSDout); + + CSB_OBUFT : OBUFT + port map ( + I => '0', + O => adcCsb, + T => coreCsb); + +-- adcSclk <= '0' when coreSclk = '0' else 'Z'; +-- adcSdio <= '0' when coreSDout = '0' else 'Z'; +-- coreSDin <= to_x01z(adcSdio); +-- adcCsb <= '0' when coreCsb = '0' else 'Z'; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/core/Ad9252Pkg.vhd b/devices/AnalogDevices/ad9252/core/Ad9252Pkg.vhd new file mode 100644 index 0000000000..756ee3579c --- /dev/null +++ b/devices/AnalogDevices/ad9252/core/Ad9252Pkg.vhd @@ -0,0 +1,35 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: AD9252 serialized pin-interface types +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +package Ad9252Pkg is + + -- One AD9252 bank has a shared differential DCO/FCO pair and eight + -- differential serialized data lanes. Array index zero corresponds to ADC channel zero. + type Ad9252SerialType is record + fClkP : sl; -- Frame clock, positive input + fClkN : sl; + dClkP : sl; -- Data clock, positive input + dClkN : sl; + chP : slv(7 downto 0); -- Serialized data, positive inputs + chN : slv(7 downto 0); + end record Ad9252SerialType; + + type Ad9252SerialArray is array (natural range <>) of Ad9252SerialType; + +end package Ad9252Pkg; diff --git a/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd b/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd new file mode 100644 index 0000000000..83b7869b3a --- /dev/null +++ b/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd @@ -0,0 +1,156 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: AD9252 serialized readout +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9252Pkg.all; +use surf.AdcDdrPkg.all; + +entity Ad9252Readout is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + NUM_CHANNELS_G : natural range 1 to 8 := 8; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + IODELAY_GROUP_G : string; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_DELAY_INIT_G : NaturalArray(NUM_CHANNELS_G-1 downto 0) := (others => 5); + FCO_DELAY_INIT_G : NaturalArray(0 downto 0) := (others => 5); + ADC_INVERT_CH_G : slv(7 downto 0) := (others => '0'); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in sl := '1'; + adc : in Ad9252SerialType; + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(NUM_CHANNELS_G-1 downto 0)); +end entity Ad9252Readout; + +architecture rtl of Ad9252Readout is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant DELAY_BITS_C : positive := adcDdrDelayBits(DEVICE_FAMILY_G); + + signal adcWordClk : sl; + signal adcWordRst : sl; + signal phyReset : sl; + signal delayReady : sl; + + signal dataWord : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal dataValid : slv(NUM_CHANNELS_G-1 downto 0); + signal sampleIn : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal fcoWord : Slv16Array(0 downto 0); + signal fcoValid : slv(0 downto 0); + + signal bitSlip : slv(0 downto 0); + signal dataDelay : AdcDdrDelayArray(NUM_CHANNELS_G-1 downto 0); + signal frameDelay : AdcDdrDelayArray(0 downto 0); + +begin + + U_Phy : entity surf.AdcDdrPhy + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + SERIALIZATION_FACTOR_G => 14, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_FCO_MAP_G => (others => 0)) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => adc.dClkP, -- [in] + dClkN => adc.dClkN, -- [in] + fcoP => (0 => adc.fClkP), -- [in] + fcoN => (0 => adc.fClkN), -- [in] + dataP => adc.chP(NUM_CHANNELS_G-1 downto 0), -- [in] + dataN => adc.chN(NUM_CHANNELS_G-1 downto 0), -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelay, -- [in] + fcoDelayWrite => frameDelay, -- [in] + captureClk => adcWordClk, -- [out] + captureRst => adcWordRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + + GEN_CHANNEL : for i in NUM_CHANNELS_G-1 downto 0 generate + sampleIn(i) <= "00" & ite(ADC_INVERT_CH_G(i) = '1', + not dataWord(i)(13 downto 0), dataWord(i)(13 downto 0)); + end generate GEN_CHANNEL; + + ------------------------------------------------------------------------------------------------- + -- Common alignment, register map, monitoring, and stream clock crossing + ------------------------------------------------------------------------------------------------- + U_Core : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + CHANNELS_G => NUM_CHANNELS_G, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + DELAY_BITS_G => DELAY_BITS_C, + DATA_DELAY_INIT_G => DATA_DELAY_INIT_G, + FCO_DELAY_INIT_G => FCO_DELAY_INIT_G, + FRAME_PATTERN_G => FRAME_PATTERN_C, + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + captureClk => adcWordClk, -- [in] + captureRst => adcWordRst, -- [in] + delayReady => delayReady, -- [in] + fcoWord => fcoWord, -- [in] + fcoValid => fcoValid, -- [in] + sampleValid => uAnd(dataValid), -- [in] + sampleIn => sampleIn, -- [in] + phyReset => phyReset, -- [out] + bitSlip => bitSlip, -- [out] + dataDelayWrite => dataDelay, -- [out] + fcoDelayWrite => frameDelay, -- [out] + streamClk => adcStreamClk, -- [in] + streamRst => adcStreamRst, -- [in] + streams => adcStreams); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/ruckus.tcl b/devices/AnalogDevices/ad9252/ruckus.tcl new file mode 100644 index 0000000000..d9faa7daf1 --- /dev/null +++ b/devices/AnalogDevices/ad9252/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# This file is part of the 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/core" -fileType "VHDL 2008" +loadRuckusTcl "$::DIR_PATH/sim" diff --git a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Deserializer.vhd b/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Deserializer.vhd deleted file mode 100755 index 9094af1b53..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Deserializer.vhd +++ /dev/null @@ -1,121 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: 14 bit DDR deserializer using 7 series IDELAYE2 and ISERDESE2. -------------------------------------------------------------------------------- --- This file is part of 'SLAC Firmware Standard Library'. --- It is subject to the license terms in the LICENSE.txt file found in the --- top-level directory of this distribution and at: --- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. --- No part of 'SLAC Firmware Standard Library', including this file, --- may be copied, modified, propagated, or distributed except according to --- the terms contained in the LICENSE.txt file. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; - -library surf; -use surf.StdRtlPkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681Deserializer is - generic ( - TPD_G : time := 1 ns; - DEFAULT_DELAY_G : natural := 0; - IODELAY_GROUP_G : string; - IDELAYCTRL_FREQ_G : real := 200.0); - port ( - clkIo : in sl; - clkIoInv : in sl; - clkR : in sl; - rst : in sl; - slip : in sl; - - sysClk : in sl; - curDelay : out slv(4 downto 0); - setDelay : in slv(4 downto 0); - setValid : in sl; - - iData : in sl; - oData : out slv(7 downto 0)); -end entity Ad9681Deserializer; - -architecture rtl of Ad9681Deserializer is - - signal dlyData : sl; - - attribute IODELAY_GROUP : string; - attribute IODELAY_GROUP of U_DELAY : label is IODELAY_GROUP_G; - -begin - - -- ADC frame delay - U_DELAY : IDELAYE2 - generic map ( - DELAY_SRC => "IDATAIN", - HIGH_PERFORMANCE_MODE => "TRUE", - IDELAY_TYPE => "VAR_LOAD", - IDELAY_VALUE => DEFAULT_DELAY_G, -- Here - REFCLK_FREQUENCY => IDELAYCTRL_FREQ_G, - SIGNAL_PATTERN => "DATA") - port map ( - C => sysClk, - REGRST => '0', - LD => setValid, - CE => '0', - INC => '1', - CINVCTRL => '0', - CNTVALUEIN => setDelay, - IDATAIN => iData, - DATAIN => '0', - LDPIPEEN => '0', - DATAOUT => dlyData, - CNTVALUEOUT => curDelay); - - U_ISERDES_MASTER : ISERDESE2 - generic map ( - DATA_RATE => "DDR", - DATA_WIDTH => 8, - INTERFACE_TYPE => "NETWORKING", - DYN_CLKDIV_INV_EN => "FALSE", - DYN_CLK_INV_EN => "FALSE", - NUM_CE => 1, - OFB_USED => "FALSE", - IOBDELAY => "IFD", -- Use input at DDLY to output the data on Q1-Q6 - SERDES_MODE => "MASTER") - port map ( - Q1 => oData(0), - Q2 => oData(1), - Q3 => oData(2), - Q4 => oData(3), - Q5 => oData(4), - Q6 => oData(5), - Q7 => oData(6), - Q8 => oData(7), - SHIFTOUT1 => open, -- Cascade connection to Slave ISERDES - SHIFTOUT2 => open, -- Cascade connection to Slave ISERDES - BITSLIP => slip, -- 1-bit Invoke Bitslip. This can be used with any - -- DATA_WIDTH, cascaded or not. - CE1 => '1', -- 1-bit Clock enable input - CE2 => '1', -- 1-bit Clock enable input - CLK => clkIo, -- Fast Source Synchronous SERDES clock from BUFIO - CLKB => clkIoInv, -- Locally inverted clock - CLKDIV => clkR, -- Slow clock driven by BUFR - CLKDIVP => '0', - D => '0', - DDLY => dlyData, -- 1-bit Input signal from IODELAYE1. - RST => rst, -- 1-bit Asynchronous reset only. - SHIFTIN1 => '0', - SHIFTIN2 => '0', - -- unused connections - DYNCLKDIVSEL => '0', - DYNCLKSEL => '0', - OFB => '0', - OCLK => '0', - OCLKB => '0', - O => open); -- unregistered output of ISERDESE1 - -end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Readout.vhd b/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Readout.vhd deleted file mode 100644 index f408b53626..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Readout.vhd +++ /dev/null @@ -1,639 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: --- ADC Readout Controller --- Receives ADC Data from an AD9592 chip. --- Designed specifically for Xilinx 7 series FPGAs -------------------------------------------------------------------------------- --- This file is part of 'SLAC Firmware Standard Library'. --- It is subject to the license terms in the LICENSE.txt file found in the --- top-level directory of this distribution and at: --- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. --- No part of 'SLAC Firmware Standard Library', including this file, --- may be copied, modified, propagated, or distributed except according to --- the terms contained in the LICENSE.txt file. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_arith.all; -use ieee.std_logic_unsigned.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.AxiLitePkg.all; -use surf.AxiStreamPkg.all; -use surf.Ad9681Pkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681Readout is - generic ( - TPD_G : time := 1 ns; - SIMULATION_G : boolean := false; - IODELAY_GROUP_G : string := "DEFAULT_GROUP"; - IDELAYCTRL_FREQ_G : real := 200.0; - DEFAULT_DELAY_G : integer range 0 to 2**5-1 := 0; - INVERT_G : boolean := false; - NEGATE_G : boolean := false); - port ( - -- Master system clock, 125Mhz - axilClk : in sl; - axilRst : in sl; - - -- Axi Interface - axilWriteMaster : in AxiLiteWriteMasterType; - axilWriteSlave : out AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; - axilReadMaster : in AxiLiteReadMasterType; - axilReadSlave : out AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; - - -- Reset for adc deserializer - adcClkRst : in sl; - - -- Serial Data from ADC - adcSerial : in Ad9681SerialType; - - -- Deserialized ADC Data - adcStreamClk : in sl; - adcStreams : out AxiStreamMasterArray(7 downto 0) := (others => axiStreamMasterInit(AD9681_AXIS_CFG_G))); -end Ad9681Readout; - -architecture rtl of Ad9681Readout is - - constant NUM_CHANNELS_C : natural := 8; - - type AdcDataArray is array (natural range <>) of slv8Array(7 downto 0); - type DelayDataArray is array (natural range <>) of slv5Array(7 downto 0); - - ------------------------------------------------------------------------------------------------- - -- AXIL Registers - ------------------------------------------------------------------------------------------------- - type AxilRegType is record - axilWriteSlave : AxiLiteWriteSlaveType; - axilReadSlave : AxiLiteReadSlaveType; - usrDly : slv5Array(1 downto 0); - enUsrDly : sl; - freezeDebug : sl; - readoutDebug0 : slv16Array(NUM_CHANNELS_C-1 downto 0); - readoutDebug1 : slv16Array(NUM_CHANNELS_C-1 downto 0); - lockedCountRst : sl; - invert : sl; - negate : sl; - realign : sl; - minEyeWidth : slv(7 downto 0); - end record; - - constant AXIL_REG_INIT_C : AxilRegType := ( - axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, - axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, - usrDly => (others => toSlv(DEFAULT_DELAY_G, 5)), - enUsrDly => '0', - freezeDebug => '0', - readoutDebug0 => (others => (others => '0')), - readoutDebug1 => (others => (others => '0')), - lockedCountRst => '0', - invert => toSl(INVERT_G), - negate => toSl(NEGATE_G), - realign => '1', - minEyeWidth => X"50"); - - signal lockedSync : slv(1 downto 0); - signal lockedFallCount : slv16Array(1 downto 0); - - signal axilR : AxilRegType := AXIL_REG_INIT_C; - signal axilRin : AxilRegType; - - ------------------------------------------------------------------------------------------------- - -- ADC Readout Clocked Registers - ------------------------------------------------------------------------------------------------- - type AdcRegType is record - errorDet : sl; - end record; - - type AdcRegArray is array (natural range <>) of AdcRegType; - - constant ADC_REG_INIT_C : AdcRegType := ( - errorDet => '0'); - - signal adcR : AdcRegArray(1 downto 0) := (others => ADC_REG_INIT_C); - signal adcRin : AdcRegArray(1 downto 0); - signal adcValid : slv(1 downto 0); - - - -- Local Signals - signal tmpAdcClk : slv(1 downto 0); - signal adcBitClkIo : slv(1 downto 0); - signal adcBitClkIoInv : slv(1 downto 0); - signal adcBitClkR : slv(1 downto 0); - signal adcBitRst : slv(1 downto 0); - - signal adcFramePad : slv(1 downto 0); - signal adcFrame : slv8Array(1 downto 0); - signal adcFrameSync : slv8Array(1 downto 0); - signal adcDataPad : slv8Array(1 downto 0); - signal adcData : AdcDataArray(1 downto 0); - - signal fifoWrData : slv16Array(NUM_CHANNELS_C-1 downto 0); - signal fifoDataValid : sl; - signal fifoDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataIn : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal debugDataValid : sl; - signal debugDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal debugDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal invertSync : slv(1 downto 0); - signal negateSync : slv(1 downto 0); - signal bitSlip : slv(1 downto 0); - signal dlyLoad : slv(1 downto 0); - signal dlyCfg : Slv9Array(1 downto 0); - signal enUsrDlyCfg : slv(1 downto 0); - signal usrDlyCfg : slv9Array(1 downto 0) := (others => (others => '0')); - signal minEyeWidthSync : slv8Array(1 downto 0); - signal lockingCntCfg : slv(23 downto 0) := ite(SIMULATION_G, X"000008", X"00FFFF"); - signal locked : slv(1 downto 0); - signal realignSync : slv(1 downto 0); - signal curDelay : slv5Array(1 downto 0); - signal errorDetCount : slv16Array(1 downto 0); - signal errorDet : slv(1 downto 0); - -begin - - ------------------------------------------------------------------------------------------------- - -- Synchronize adcR.locked across to axil clock domain and count falling edges on it - ------------------------------------------------------------------------------------------------- - SYNC_GEN : for i in 1 downto 0 generate - - Synchronizer_locked : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => locked(i), - dataOut => lockedSync(i)); - - - SynchronizerOneShotCnt_locked_fall : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '0', - CNT_RST_EDGE_G => true, - CNT_WIDTH_G => 16) - port map ( - dataIn => locked(i), - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => lockedFallCount(i), - wrClk => adcBitClkR(i), - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - SynchronizerOneShotCnt_2 : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '1', - OUT_POLARITY_G => '1', - CNT_RST_EDGE_G => false, - CNT_WIDTH_G => 16) - port map ( - dataIn => errorDet(i), - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => errorDetCount(i), - wrClk => adcBitClkR(i), - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - - - SynchronizerVector_FRAME : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => adcFrame(i), - dataOut => adcFrameSync(i)); - - U_SynchronizerVector_CUR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 5) - port map ( - clk => axilClk, -- [in] - rst => axilRst, -- [in] - dataIn => dlyCfg(i)(8 downto 4), -- [in] - dataOut => curDelay(i)); -- [out] - - - - -- AXIL to ADC clock - Synchronizer_INVERT : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.invert, - dataOut => invertSync(i)); - - Synchronizer_NEGATE : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.negate, - dataOut => negateSync(i)); - - - Synchronizer_REALIGN : entity surf.SynchronizerEdge - generic map ( - TPD_G => TPD_G, - STAGES_G => 3) - port map ( - clk => adcBitClkR(i), - rst => adcBitRst(i), - dataIn => axilR.realign, - risingEdge => realignSync(i)); - - Synchronizer_USR_DELAY_SET : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 3) - port map ( - clk => adcBitClkR(i), - rst => adcBitRst(i), - dataIn => axilR.enUsrDly, - dataOut => enUsrDlyCfg(i)); - - U_SynchronizerVector_USR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 5) - port map ( - clk => adcBitClkR(i), -- [in] - rst => adcBitRst(i), -- [in] - dataIn => axilR.usrDly(i), -- [in] - dataOut => usrDlyCfg(i)(8 downto 4)); -- [out] - - U_SynchronizerVector_EYE_WIDTH : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => adcBitClkR(i), -- [in] - rst => adcBitRst(i), -- [in] - dataIn => axilR.minEyeWidth, -- [in] - dataOut => minEyeWidthSync(i)); -- [out] - - - - end generate SYNC_GEN; - - ------------------------------------------------------------------------------------------------- - -- AXIL Interface - ------------------------------------------------------------------------------------------------- - axilComb : process (adcFrameSync, axilR, axilReadMaster, axilRst, - axilWriteMaster, curDelay, debugDataTmp, debugDataValid, - errorDetCount, lockedFallCount, lockedSync) is - variable v : AxilRegType; - variable axilEp : AxiLiteEndpointType; - begin - v := axilR; - - - -- Store last two samples read from ADC - if (debugDataValid = '1' and axilR.freezeDebug = '0') then - v.readoutDebug0 := debugDataTmp; - v.readoutDebug1 := axilR.readoutDebug0; - end if; - - axiSlaveWaitTxn(axilEp, axilWriteMaster, axilReadMaster, v.axilWriteSlave, v.axilReadSlave); - - -- Overriding gearbox aligner - if (axilR.enUsrDly = '0') then - v.usrDly := curDelay; - end if; - - axiSlaveRegister(axilEp, X"00", 0, v.usrDly(0)); - axiSlaveRegisterR(axilEp, X"00", 0, curDelay(0)); - - axiSlaveRegister(axilEp, X"04", 0, v.usrDly(1)); - axiSlaveRegisterR(axilEp, X"04", 0, curDelay(1)); - - axiSlaveRegister(axilEp, X"20", 0, v.enUsrDly); - - axiSlaveRegister(axilEp, X"70", 0, v.realign); - axiSlaveRegisterR(axilEp, X"30", 0, errorDetCount(0)); - axiSlaveRegisterR(axilEp, X"34", 0, errorDetCount(1)); - - -- Debug output to see how many times the shift has needed a relock - axiSlaveRegisterR(axilEp, X"50", 0, lockedFallCount(0)); - axiSlaveRegisterR(axilEp, X"50", 16, lockedSync(0)); - axiSlaveRegisterR(axilEp, X"54", 0, lockedFallCount(1)); - axiSlaveRegisterR(axilEp, X"54", 16, lockedSync(1)); - - axiSlaveRegisterR(axilEp, X"58", 0, adcFrameSync(0)); - axiSlaveRegisterR(axilEp, X"58", 8, adcFrameSync(1)); - - axiSlaveRegister(axilEp, X"5C", 0, v.lockedCountRst); - - axiSlaveRegister(axilEp, X"60", 0, v.invert); - axiSlaveRegister(axilEp, X"60", 1, v.negate); - - -- Debug registers. Output the last 2 words received - for ch in 0 to NUM_CHANNELS_C-1 loop - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 0, axilR.readoutDebug0(ch)); - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 16, axilR.readoutDebug1(ch)); - end loop; - - axiSlaveRegister(axilEp, X"A0", 0, v.freezeDebug); - - axiSlaveDefault(axilEp, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); - - if (axilRst = '1') then - v := AXIL_REG_INIT_C; - end if; - - axilRin <= v; - axilWriteSlave <= axilR.axilWriteSlave; - axilReadSlave <= axilR.axilReadSlave; - - end process; - - axilSeq : process (axilClk) is - begin - if (rising_edge(axilClk)) then - axilR <= axilRin after TPD_G; - end if; - end process axilSeq; - - - GEN_PARTS : for i in 1 downto 0 generate - - ------------------------------------------------------------------------------------------------- - -- Create Clocks - ------------------------------------------------------------------------------------------------- - - AdcClk_I_Ibufds : IBUFDS - generic map ( - DIFF_TERM => true, - IOSTANDARD => "LVDS_25") - port map ( - I => adcSerial.dClkP(i), - IB => adcSerial.dClkN(i), - O => tmpAdcClk(i)); - - -- IO Clock - U_BUFIO : BUFIO - port map ( - I => tmpAdcClk(i), - O => adcBitClkIo(i)); - - adcBitClkIoInv(i) <= not adcBitClkIo(i); - - -- Regional clock - U_AdcBitClkR : BUFR - generic map ( - SIM_DEVICE => "7SERIES", - BUFR_DIVIDE => "4") - port map ( - I => tmpAdcClk(i), - O => adcBitClkR(i), - CE => '1', - CLR => '0'); - - -- Regional clock reset - ADC_BITCLK_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClkR(i), - asyncRst => adcClkRst, - syncRst => adcBitRst(i)); - - - ------------------------------------------------------------------------------------------------- - -- Deserializers - ------------------------------------------------------------------------------------------------- - - -- Frame signal input - U_FrameIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.fClkP(i), - IB => adcSerial.fClkN(i), - O => adcFramePad(i)); - - U_FRAME_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => realignSync(0), - slip => bitSlip(i), - sysClk => adcBitClkR(0), - curDelay => open, --curDelayFrame(i), - setDelay => dlyCfg(i)(8 downto 4), - setValid => dlyLoad(i), --axilR.frameDelaySet(i), - iData => adcFramePad(i), - oData => adcFrame(i)); - - - - -------------------------------- - -- Data Input, 8 channels - -------------------------------- - GenData : for ch in NUM_CHANNELS_C-1 downto 0 generate - - -- Frame signal input - U_DataIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.chP(i)(ch), - IB => adcSerial.chN(i)(ch), - O => adcDataPad(i)(ch)); - - -- Optionally invert the pad input --- adcDataPad(i)(ch) <= adcDataPadOut(i)(ch) when ADC_INVERT_CH_G(i)(ch) = '0' else (not adcDataPadOut(i)(ch)); - - U_DATA_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => realignSync(0), - slip => bitSlip(i), - sysClk => adcBitClkR(0), - curDelay => open, --curDelayData(i)(ch), - setDelay => dlyCfg(i)(8 downto 4), - setValid => dlyLoad(i), --axilR.dataDelaySet(i)(ch), - iData => adcDataPad(i)(ch), - oData => adcData(i)(ch)); - end generate; - - - ---------------------------------------------------------------------------------------------- - -- Aligner - ---------------------------------------------------------------------------------------------- - U_SelectIoRxGearboxAligner_1 : entity surf.SelectIoRxGearboxAligner - generic map ( - TPD_G => TPD_G, - SIMULATION_G => SIMULATION_G, - CODE_TYPE_G => "LINE_CODE", - DLY_STEP_SIZE_G => 16) - port map ( - clk => adcBitClkR(0), -- [in] - rst => adcBitRst(0), -- [in] - lineCodeValid => '1', -- [in] - lineCodeErr => adcR(i).errorDet, -- [in] - lineCodeDispErr => realignSync(0), -- [in] - linkOutOfSync => '0', -- [in] - rxHeaderValid => '0', -- [in] - rxHeader => (others => '0'), -- [in] - bitSlip => bitSlip(i), -- [out] - dlyLoad => dlyLoad(i), -- [out] - dlyCfg => dlyCfg(i), -- [out] - enUsrDlyCfg => enUsrDlyCfg(i), -- [in] - usrDlyCfg => usrDlyCfg(i), -- [in] - bypFirstBerDet => '1', -- [in] - minEyeWidth => minEyeWidthSync(i), -- [in] - lockingCntCfg => lockingCntCfg, -- [in] - errorDet => errorDet(i), -- [out] - locked => locked(i)); -- [out] - - - ------------------------------------------------------------------------------------------------- - -- ADC Bit Clocked Logic - ------------------------------------------------------------------------------------------------- - adcComb : process (adcFrame, adcR) is - variable v : AdcRegType; - begin - v := adcR(i); --- v.adcValid := '0'; - ---------------------------------------------------------------------------------------------- - -- Slip bits until correct alignment seen - ---------------------------------------------------------------------------------------------- - v.errorDet := toSl(adcFrame(i) /= "11110000"); - - adcRin(i) <= v; - - end process adcComb; - - adcSeq : process (adcBitClkR, adcBitRst) is - begin - if (adcBitRst(0) = '1') then - adcR(i) <= ADC_REG_INIT_C after TPD_G; - elsif (rising_edge(adcBitClkR(0))) then - adcR(i) <= adcRin(i) after TPD_G; - end if; - end process adcSeq; - - end generate; - - GLUE_COMB : process (adcData, invertSync, locked, negateSync) is - variable tmp : slv16Array(7 downto 0); - begin - for ch in NUM_CHANNELS_C-1 downto 0 loop - if (locked = "11") then - tmp(ch) := adcData(1)(ch) & adcData(0)(ch); - -- Locked, output adc data - if invertSync(0) = '1' then - -- Invert all bits but keep 2 LSBs clear - tmp(ch) := (X"FFFF" - tmp(ch)) and X"FFFC"; - elsif (negateSync(0) = '1') then - if (tmp(ch) = X"8000") then - -- Negative 1 case - tmp(ch) := X"7FFC"; - else - tmp(ch) := (not(tmp(ch)(15 downto 2)) + 1) & "00"; - end if; - end if; - else - -- Not locked - tmp(ch) := (others => '1'); --"10" & "00000000000000"; - end if; - end loop; - fifoWrData <= tmp; - end process GLUE_COMB; - - --- Flatten fifoWrData onto fifoDataIn for FIFO --- Regroup fifoDataOut by channel into fifoDataTmp --- Format fifoDataTmp into AxiStream channels - glue : for i in NUM_CHANNELS_C-1 downto 0 generate - fifoDataIn(i*16+15 downto i*16) <= fifoWrData(i); - fifoDataTmp(i) <= fifoDataOut(i*16+15 downto i*16); - debugDataTmp(i) <= debugDataOut(i*16+15 downto i*16); - adcStreams(i).tdata(15 downto 0) <= fifoDataTmp(i); - adcStreams(i).tDest <= toSlv(i, 8); - adcStreams(i).tValid <= fifoDataValid; - end generate; - - -- Single fifo to synchronize adc data to the Stream clock - U_DataFifo : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => adcStreamClk, - rd_en => fifoDataValid, - valid => fifoDataValid, - dout => fifoDataOut); - - U_DataFifoDebug : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => axilClk, - rd_en => debugDataValid, - valid => debugDataValid, - dout => debugDataOut); - - -end rtl; - diff --git a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681ReadoutManual.vhd b/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681ReadoutManual.vhd deleted file mode 100644 index b55e527d19..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681ReadoutManual.vhd +++ /dev/null @@ -1,600 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: --- ADC ReadoutManual Controller --- Receives ADC Data from an AD9592 chip. --- Designed specifically for Xilinx 7 series FPGAs -------------------------------------------------------------------------------- --- This file is part of 'SLAC Firmware Standard Library'. --- It is subject to the license terms in the LICENSE.txt file found in the --- top-level directory of this distribution and at: --- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. --- No part of 'SLAC Firmware Standard Library', including this file, --- may be copied, modified, propagated, or distributed except according to --- the terms contained in the LICENSE.txt file. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_arith.all; -use ieee.std_logic_unsigned.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.AxiLitePkg.all; -use surf.AxiStreamPkg.all; -use surf.Ad9681Pkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681ReadoutManual is - generic ( - TPD_G : time := 1 ns; - - IODELAY_GROUP_G : string := "DEFAULT_GROUP"; - IDELAYCTRL_FREQ_G : real := 200.0; - DEFAULT_DELAY_G : integer range 0 to 2**5-1 := 0; - INVERT_G : boolean := false; - NEGATE_G : boolean := false); - port ( - -- Master system clock, 125Mhz - axilClk : in sl; - axilRst : in sl; - - -- Axi Interface - axilWriteMaster : in AxiLiteWriteMasterType; - axilWriteSlave : out AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; - axilReadMaster : in AxiLiteReadMasterType; - axilReadSlave : out AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; - - -- Reset for adc deserializer - adcClkRst : in sl; - - -- Serial Data from ADC - adcSerial : in Ad9681SerialType; - - -- Deserialized ADC Data - adcStreamClk : in sl; - adcStreams : out AxiStreamMasterArray(7 downto 0) := (others => axiStreamMasterInit(AD9681_AXIS_CFG_G))); -end Ad9681ReadoutManual; - -architecture rtl of Ad9681ReadoutManual is - - constant NUM_CHANNELS_C : natural := 8; - - type AdcDataArray is array (natural range <>) of slv8Array(7 downto 0); - type DelayDataArray is array (natural range <>) of slv5Array(7 downto 0); - - ------------------------------------------------------------------------------------------------- - -- AXIL Registers - ------------------------------------------------------------------------------------------------- - type AxilRegType is record - axilWriteSlave : AxiLiteWriteSlaveType; - axilReadSlave : AxiLiteReadSlaveType; - delay : slv(4 downto 0); - dataDelaySet : slv8Array(1 downto 0); - frameDelaySet : slv(1 downto 0); - freezeDebug : sl; - readoutDebug0 : slv16Array(NUM_CHANNELS_C-1 downto 0); - readoutDebug1 : slv16Array(NUM_CHANNELS_C-1 downto 0); - lockedCountRst : sl; - invert : sl; - negate : sl; - relock : slv(1 downto 0); - curDelayFrame : slv5Array(1 downto 0); - curDelayData : DelayDataArray(1 downto 0); - end record; - - constant AXIL_REG_INIT_C : AxilRegType := ( - axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, - axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, - delay => toSlv(DEFAULT_DELAY_G, 5), - dataDelaySet => (others => (others => '1')), - frameDelaySet => "11", - freezeDebug => '0', - readoutDebug0 => (others => (others => '0')), - readoutDebug1 => (others => (others => '0')), - lockedCountRst => '0', - invert => toSl(INVERT_G), - negate => toSl(NEGATE_G), - relock => "00", - curDelayFrame => (others => (others => '0')), - curDelayData => (others => (others => (others => '0')))); - - signal lockedSync : slv(1 downto 0); - signal lockedFallCount : slv16Array(1 downto 0); - - signal axilR : AxilRegType := AXIL_REG_INIT_C; - signal axilRin : AxilRegType; - - ------------------------------------------------------------------------------------------------- - -- ADC Readout Clocked Registers - ------------------------------------------------------------------------------------------------- - type SyncStateType is (RESET_S, SYNCING_S, SYNCED_S); - - type AdcRegType is record - state : SyncStateType; - slip : sl; - count : slv(5 downto 0); - locked : sl; - reset : sl; - end record; - - type AdcRegArray is array (natural range <>) of AdcRegType; - - constant ADC_REG_INIT_C : AdcRegType := ( - state => RESET_S, - slip => '0', - count => (others => '0'), - locked => '0', - reset => '1'); - - signal adcR : AdcRegArray(1 downto 0) := (others => ADC_REG_INIT_C); - signal adcRin : AdcRegArray(1 downto 0); - signal adcValid : slv(1 downto 0); - - - -- Local Signals - signal tmpAdcClk : slv(1 downto 0); - signal adcBitClkIo : slv(1 downto 0); - signal adcBitClkIoInv : slv(1 downto 0); - signal adcBitClkR : slv(1 downto 0); - signal adcBitRst : slv(1 downto 0); - - signal adcFramePad : slv(1 downto 0); - signal adcFrame : slv8Array(1 downto 0); - signal adcFrameSync : slv8Array(1 downto 0); - signal adcDataPad : slv8Array(1 downto 0); - signal adcData : AdcDataArray(1 downto 0); - - signal curDelayFrame : slv5Array(1 downto 0); - signal curDelayData : DelayDataArray(1 downto 0); - - signal fifoWrData : slv16Array(NUM_CHANNELS_C-1 downto 0); - signal fifoDataValid : sl; - signal fifoDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataIn : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal debugDataValid : sl; - signal debugDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal debugDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal invertSync : slv(1 downto 0); - signal negateSync : slv(1 downto 0); - signal relockSync : slv(1 downto 0); - -begin - - ------------------------------------------------------------------------------------------------- - -- Synchronize adcR.locked across to axil clock domain and count falling edges on it - ------------------------------------------------------------------------------------------------- - SYNC_GEN : for i in 1 downto 0 generate - - SynchronizerOneShotCnt_1 : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '0', - CNT_RST_EDGE_G => true, - CNT_WIDTH_G => 16) - port map ( - dataIn => adcR(i).locked, - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => lockedFallCount(i), - wrClk => adcBitClkR(i), - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - Synchronizer_1 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => adcR(i).locked, - dataOut => lockedSync(i)); - - SynchronizerVec_1 : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => adcFrame(i), - dataOut => adcFrameSync(i)); - - Synchronizer_2 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.invert, - dataOut => invertSync(i)); - - Synchronizer_4 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.negate, - dataOut => negateSync(i)); - - - Synchronizer_3 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), --- rst => axilRst, - dataIn => axilR.relock(i), - dataOut => relockSync(i)); - - - end generate SYNC_GEN; - - - - ------------------------------------------------------------------------------------------------- - -- AXIL Interface - ------------------------------------------------------------------------------------------------- - axilComb : process (adcFrameSync, axilR, axilReadMaster, axilRst, - axilWriteMaster, curDelayData, curDelayFrame, - debugDataTmp, debugDataValid, lockedFallCount, - lockedSync) is - variable v : AxilRegType; - variable axilEp : AxiLiteEndpointType; - begin - v := axilR; - - v.dataDelaySet := (others => (others => '0')); - v.frameDelaySet := "00"; - - v.curDelayFrame := curDelayFrame; - v.curDelayData := curDelayData; - - -- Store last two samples read from ADC - if (debugDataValid = '1' and axilR.freezeDebug = '0') then - v.readoutDebug0 := debugDataTmp; - v.readoutDebug1 := axilR.readoutDebug0; - end if; - - axiSlaveWaitTxn(axilEp, axilWriteMaster, axilReadMaster, v.axilWriteSlave, v.axilReadSlave); - - -- Up to 8 delay registers - -- Write delay values to IDELAY primitives - -- All writes go to same r.delay register, - -- dataDelaySet(ch) or frameDelaySet enables the primitive write - for i in 1 downto 0 loop - for ch in 0 to NUM_CHANNELS_C-1 loop - axiSlaveRegister(axilEp, X"00"+toSlv((ch*8+i*4), 8), 0, v.delay); - axiWrDetect(axilEp, X"00"+toSlv((ch*8+i*4), 8), v.dataDelaySet(i)(ch)); - - axiSlaveRegisterR(axilEp, X"00"+toSlv((ch*8+i*4), 8), 0, axilR.curDelayData(i)(ch)); - end loop; - - axiSlaveRegister(axilEp, X"40"+toSlv(i*4, 8), 0, v.delay); - axiWrDetect(axilEp, X"40"+toSlv(i*4, 8), v.frameDelaySet(i)); - - axiSlaveRegisterR(axilEp, X"40"+toSlv(i*4, 8), 0, axilR.curDelayFrame(i)); - end loop; - - - - - -- Debug output to see how many times the shift has needed a relock - axiSlaveRegisterR(axilEp, X"50", 0, lockedFallCount(0)); - axiSlaveRegisterR(axilEp, X"50", 16, lockedSync(0)); - axiSlaveRegisterR(axilEp, X"54", 0, lockedFallCount(1)); - axiSlaveRegisterR(axilEp, X"54", 16, lockedSync(1)); - - axiSlaveRegisterR(axilEp, X"58", 0, adcFrameSync(0)); - axiSlaveRegisterR(axilEp, X"58", 8, adcFrameSync(1)); - - axiSlaveRegister(axilEp, X"5C", 0, v.lockedCountRst); - - axiSlaveRegister(axilEp, X"60", 0, v.invert); - axiSlaveRegister(axilEp, X"60", 1, v.negate); - - axiSlaveRegister(axilEp, X"70", 0, v.relock); - - -- Debug registers. Output the last 2 words received - for ch in 0 to NUM_CHANNELS_C-1 loop - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 0, axilR.readoutDebug0(ch)); - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 16, axilR.readoutDebug1(ch)); - end loop; - - axiSlaveRegister(axilEp, X"A0", 0, v.freezeDebug); - - axiSlaveDefault(axilEp, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); - - if (axilRst = '1') then - v := AXIL_REG_INIT_C; - end if; - - axilRin <= v; - axilWriteSlave <= axilR.axilWriteSlave; - axilReadSlave <= axilR.axilReadSlave; - - end process; - - axilSeq : process (axilClk) is - begin - if (rising_edge(axilClk)) then - axilR <= axilRin after TPD_G; - end if; - end process axilSeq; - - - - - GEN_PARTS : for i in 1 downto 0 generate - - ------------------------------------------------------------------------------------------------- - -- Create Clocks - ------------------------------------------------------------------------------------------------- - - AdcClk_I_Ibufds : IBUFDS - generic map ( - DIFF_TERM => true, - IOSTANDARD => "LVDS_25") - port map ( - I => adcSerial.dClkP(i), - IB => adcSerial.dClkN(i), - O => tmpAdcClk(i)); - - -- IO Clock - U_BUFIO : BUFIO - port map ( - I => tmpAdcClk(i), - O => adcBitClkIo(i)); - - adcBitClkIoInv(i) <= not adcBitClkIo(i); - - -- Regional clock - U_AdcBitClkR : BUFR - generic map ( - SIM_DEVICE => "7SERIES", - BUFR_DIVIDE => "4") - port map ( - I => tmpAdcClk(i), - O => adcBitClkR(i), - CE => '1', - CLR => '0'); - - -- Regional clock reset - ADC_BITCLK_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClkR(i), - asyncRst => adcClkRst, - syncRst => adcBitRst(i)); - - - ------------------------------------------------------------------------------------------------- - -- Deserializers - ------------------------------------------------------------------------------------------------- - - -- Frame signal input - U_FrameIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.fClkP(i), - IB => adcSerial.fClkN(i), - O => adcFramePad(i)); - - U_FRAME_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => adcR(i).reset, --adcBitRst(i), - slip => adcR(i).slip, - sysClk => axilClk, - curDelay => curDelayFrame(i), - setDelay => axilR.delay, - setValid => axilR.frameDelaySet(i), - iData => adcFramePad(i), - oData => adcFrame(i)); - - -------------------------------- - -- Data Input, 8 channels - -------------------------------- - GenData : for ch in NUM_CHANNELS_C-1 downto 0 generate - - -- Frame signal input - U_DataIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.chP(i)(ch), - IB => adcSerial.chN(i)(ch), - O => adcDataPad(i)(ch)); - - -- Optionally invert the pad input --- adcDataPad(i)(ch) <= adcDataPadOut(i)(ch) when ADC_INVERT_CH_G(i)(ch) = '0' else (not adcDataPadOut(i)(ch)); - - U_DATA_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => adcR(i).reset, --adcBitRst(i), - slip => adcR(i).slip, - sysClk => axilClk, - curDelay => curDelayData(i)(ch), - setDelay => axilR.delay, - setValid => axilR.dataDelaySet(i)(ch), - iData => adcDataPad(i)(ch), - oData => adcData(i)(ch)); - end generate; - - - - ------------------------------------------------------------------------------------------------- - -- ADC Bit Clocked Logic - ------------------------------------------------------------------------------------------------- - adcComb : process (adcFrame, adcR, relockSync) is - variable v : AdcRegType; - begin - v := adcR(i); - - ---------------------------------------------------------------------------------------------- - -- Slip bits until correct alignment seen - ---------------------------------------------------------------------------------------------- - v.slip := '0'; - v.reset := '0'; - v.locked := '0'; - - v.count := adcR(i).count + 1; - - case adcR(i).state is - when RESET_S => - v.reset := '1'; - if (adcR(i).count = "111111") then - v.state := SYNCING_S; - end if; - - when SYNCING_S => - if (adcR(i).count = "111111") then - if (adcFrame(i) = "11110000") then - v.state := SYNCED_S; - else - v.slip := '1'; - end if; - end if; - - when SYNCED_S => - v.locked := '1'; - v.count := (others => '0'); - if (adcFrame(i) /= "11110000") then - v.state := RESET_S; - end if; - - when others => null; - end case; - - if (relockSync(i) = '1') then - v.state := RESET_S; - end if; - - ---------------------------------------------------------------------------------------------- - -- Look for Frame rising edges and write data to fifos - ---------------------------------------------------------------------------------------------- - adcRin(i) <= v; - adcValid(i) <= adcR(i).locked; - - end process adcComb; - - adcSeq : process (adcBitClkR, adcBitRst) is - begin - if (adcBitRst(0) = '1') then - adcR(i) <= ADC_REG_INIT_C after TPD_G; - elsif (rising_edge(adcBitClkR(0))) then - adcR(i) <= adcRin(i) after TPD_G; - end if; - end process adcSeq; - - end generate; - - GLUE_COMB : process (adcData, adcValid, invertSync, negateSync) is - variable tmp : slv16Array(7 downto 0); - begin - for ch in NUM_CHANNELS_C-1 downto 0 loop - if (adcValid = "11") then - tmp(ch) := adcData(1)(ch) & adcData(0)(ch); - -- Locked, output adc data - if invertSync(0) = '1' then - -- Invert all bits but keep 2 LSBs clear - tmp(ch) := (X"FFFF" - tmp(ch)) and X"FFFC"; - elsif (negateSync(0) = '1') then - if (tmp(ch) = X"8000") then - -- Negative 1 case - tmp(Ch) := X"7FFC"; - else - tmp(ch) := (not(tmp(ch)(15 downto 2)) + 1) & "00"; - end if; - end if; - else - -- Not locked - tmp(ch) := (others => '1'); --"10" & "00000000000000"; - end if; - end loop; - fifoWrData <= tmp; - end process GLUE_COMB; - - --- Flatten fifoWrData onto fifoDataIn for FIFO --- Regroup fifoDataOut by channel into fifoDataTmp --- Format fifoDataTmp into AxiStream channels - glue : for i in NUM_CHANNELS_C-1 downto 0 generate - fifoDataIn(i*16+15 downto i*16) <= fifoWrData(i); - fifoDataTmp(i) <= fifoDataOut(i*16+15 downto i*16); - debugDataTmp(i) <= debugDataOut(i*16+15 downto i*16); - adcStreams(i).tdata(15 downto 0) <= fifoDataTmp(i); - adcStreams(i).tDest <= toSlv(i, 8); - adcStreams(i).tValid <= fifoDataValid; - end generate; - - -- Single fifo to synchronize adc data to the Stream clock - U_DataFifo : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => adcStreamClk, - rd_en => fifoDataValid, - valid => fifoDataValid, - dout => fifoDataOut); - - U_DataFifoDebug : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => axilClk, - rd_en => debugDataValid, - valid => debugDataValid, - dout => debugDataOut); - - -end rtl; - diff --git a/devices/AnalogDevices/ad9681/7Series/ruckus.tcl b/devices/AnalogDevices/ad9681/7Series/ruckus.tcl deleted file mode 100644 index e3e79e21f3..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/ruckus.tcl +++ /dev/null @@ -1,5 +0,0 @@ -# Load RUCKUS library -source $::env(RUCKUS_PROC_TCL) - -# Load Source Code -loadSource -lib surf -dir "$::DIR_PATH/rtl" diff --git a/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd b/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd new file mode 100644 index 0000000000..12db52f022 --- /dev/null +++ b/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd @@ -0,0 +1,207 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: AD9681 serialized readout +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9681Pkg.all; +use surf.AdcDdrPkg.all; + +entity Ad9681Readout is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + CAPTURE_DCLK_IDX_G : natural range 0 to 1 := 0; + DATA_DELAY_INIT_G : NaturalArray(15 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(1 downto 0) := (others => 0); + ADC_INVERT_CH_G : Slv8Array(1 downto 0) := (others => (others => '0')); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false; + LEFT_JUSTIFY_G : boolean := true); + port ( + axilClk : in sl; + axilRst : in sl; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in sl := '1'; + adcSerial : in Ad9681SerialType; + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(7 downto 0)); +end entity Ad9681Readout; + +architecture rtl of Ad9681Readout is + + constant FRAME_PATTERN_C : slv(7 downto 0) := "11110000"; + constant DELAY_BITS_C : positive := adcDdrDelayBits(DEVICE_FAMILY_G); + constant DATA_FCO_MAP_C : NaturalArray(15 downto 0) := ( + 15 downto 8 => 1, + 7 downto 0 => 0); + + signal adcWordClk : sl; + signal adcWordRst : sl; + signal phyReset : sl; + signal delayReady : sl; + + signal dataP : slv(15 downto 0); + signal dataN : slv(15 downto 0); + signal dataWord : Slv16Array(15 downto 0); + signal dataValid : slv(15 downto 0); + signal sampleIn : Slv16Array(7 downto 0); + signal fcoWord : Slv16Array(1 downto 0); + signal fcoValid : slv(1 downto 0); + + signal bitSlip : slv(1 downto 0); + signal dataDelay : AdcDdrDelayArray(15 downto 0); + signal frameDelay : AdcDdrDelayArray(1 downto 0); + + -- Right-justified [13:0] streams as produced by AdcDdrCore. + signal coreStreams : AxiStreamMasterArray(7 downto 0); + +begin + + ------------------------------------------------------------------------------------------------- + -- Capture both serialized halves with one selected DCLK. The two DCLKs are + -- related ADC outputs, but combining their FPGA clock domains would make + -- sample coherence depend on an implicit inter-clock relationship. + ------------------------------------------------------------------------------------------------- + GEN_HALF : for i in 1 downto 0 generate + GEN_CHANNEL : for ch in 7 downto 0 generate + constant LANE_C : natural := (8*i)+ch; + begin + dataP(LANE_C) <= adcSerial.chP(i)(ch); + dataN(LANE_C) <= adcSerial.chN(i)(ch); + end generate GEN_CHANNEL; + end generate GEN_HALF; + + U_Phy : entity surf.AdcDdrPhy + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => 16, + FCO_LANES_G => 2, + SERIALIZATION_FACTOR_G => 8, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_FCO_MAP_G => DATA_FCO_MAP_C) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => adcSerial.dClkP(CAPTURE_DCLK_IDX_G), -- [in] + dClkN => adcSerial.dClkN(CAPTURE_DCLK_IDX_G), -- [in] + fcoP => adcSerial.fClkP, -- [in] + fcoN => adcSerial.fClkN, -- [in] + dataP => dataP, -- [in] + dataN => dataN, -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelay, -- [in] + fcoDelayWrite => frameDelay, -- [in] + captureClk => adcWordClk, -- [out] + captureRst => adcWordRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + + ------------------------------------------------------------------------------------------------- + -- In default two-lane mode, half 1 contains the upper eight serialized bits + -- and half 0 contains the lower six sample bits followed by two pad bits. + -- Strip those pad bits so the core receives the 14-bit ADC code in bits 13:0. + ------------------------------------------------------------------------------------------------- + GEN_ASSEMBLE : for ch in 7 downto 0 generate + sampleIn(ch) <= "00" & + ite(ADC_INVERT_CH_G(1)(ch) = '1', not dataWord(8+ch)(7 downto 0), + dataWord(8+ch)(7 downto 0)) & + ite(ADC_INVERT_CH_G(0)(ch) = '1', not dataWord(ch)(7 downto 2), + dataWord(ch)(7 downto 2)); + end generate GEN_ASSEMBLE; + + U_Core : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => 16, + FCO_LANES_G => 2, + CHANNELS_G => 8, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 8, + DELAY_BITS_G => DELAY_BITS_C, + DATA_DELAY_INIT_G => DATA_DELAY_INIT_G, + FCO_DELAY_INIT_G => FCO_DELAY_INIT_G, + FRAME_PATTERN_G => FRAME_PATTERN_C, + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + captureClk => adcWordClk, -- [in] + captureRst => adcWordRst, -- [in] + delayReady => delayReady, -- [in] + fcoWord => fcoWord, -- [in] + fcoValid => fcoValid, -- [in] + sampleValid => uAnd(dataValid), -- [in] + sampleIn => sampleIn, -- [in] + phyReset => phyReset, -- [out] + bitSlip => bitSlip, -- [out] + dataDelayWrite => dataDelay, -- [out] + fcoDelayWrite => frameDelay, -- [out] + streamClk => adcStreamClk, -- [in] + streamRst => adcStreamRst, -- [in] + streams => coreStreams); -- [out] + + ------------------------------------------------------------------------------------------------- + -- Sample justification + -- + -- AdcDdrCore emits the 14-bit ADC code right-justified in bits 13:0. The + -- previous Ad9681Readout and the warm-tdm DSP chain expect the sample + -- left-justified into bits 15:2 with the two LSBs cleared. Reproduce that + -- layout by default; keep the raw right-justified stream when disabled. + ------------------------------------------------------------------------------------------------- + GEN_JUSTIFY : if LEFT_JUSTIFY_G generate + GEN_CH : for ch in 7 downto 0 generate + justify : process (coreStreams) is + variable v : AxiStreamMasterType; + begin + v := coreStreams(ch); + v.tData(15 downto 0) := coreStreams(ch).tData(13 downto 0) & "00"; + adcStreams(ch) <= v; + end process justify; + end generate GEN_CH; + end generate GEN_JUSTIFY; + + GEN_RAW : if not LEFT_JUSTIFY_G generate + adcStreams <= coreStreams; + end generate GEN_RAW; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/ruckus.tcl b/devices/AnalogDevices/ad9681/ruckus.tcl index ec7c6fe333..f1d7eb4658 100644 --- a/devices/AnalogDevices/ad9681/ruckus.tcl +++ b/devices/AnalogDevices/ad9681/ruckus.tcl @@ -3,25 +3,4 @@ source $::env(RUCKUS_PROC_TCL) # Load Source Code loadSource -lib surf -dir "$::DIR_PATH/core" -loadSource -lib surf -sim_only -dir "$::DIR_PATH/sim" - -# Get the family type -set family [getFpgaFamily] - -if { ${family} eq {artix7} || - ${family} eq {kintex7} || - ${family} eq {virtex7} || - ${family} eq {zynq} } { - loadRuckusTcl "$::DIR_PATH/7Series" -} - -# if { ${family} eq {kintexu} || - # ${family} eq {virtexu} || - # ${family} eq {kintexuplus} || - # ${family} eq {virtexuplus} || - # ${family} eq {virtexuplusHBM} || - # ${family} eq {zynquplus} || - # ${family} eq {zynquplusRFSOC} || - # ${family} eq {qzynquplusRFSOC} } { - # loadRuckusTcl "$::DIR_PATH/UltraScale" -# } +loadRuckusTcl "$::DIR_PATH/sim" diff --git a/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrDeserializer7Series.vhd b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrDeserializer7Series.vhd new file mode 100644 index 0000000000..bfcf3c4841 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrDeserializer7Series.vhd @@ -0,0 +1,180 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: One delayed DDR input lane for AMD 7 Series FPGAs +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrDeserializer7Series is + generic ( + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + SERIALIZATION_FACTOR_G : positive := 14); + port ( + bitClk : in sl; + bitClkInv : in sl; + wordClk : in sl; + rst : in sl; + bitSlip : in sl; + + delayClk : in sl; + delayValue : in slv(4 downto 0); + delayLoad : in sl; + + serialData : in sl; + dataWord : out slv(SERIALIZATION_FACTOR_G-1 downto 0)); +end entity AdcDdrDeserializer7Series; + +architecture rtl of AdcDdrDeserializer7Series is + + signal delayedData : sl; + signal rawWord : slv(13 downto 0); + signal shift1 : sl; + signal shift2 : sl; + + attribute IODELAY_GROUP : string; + attribute IODELAY_GROUP of U_Delay : label is IODELAY_GROUP_G; + +begin + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 14 + report "AdcDdrDeserializer7Series supports only native DDR widths 4, 6, 8, 10, and 14" + severity failure; + + U_Delay : IDELAYE2 + generic map ( + DELAY_SRC => "IDATAIN", + HIGH_PERFORMANCE_MODE => "TRUE", + IDELAY_TYPE => "VAR_LOAD", + IDELAY_VALUE => 0, + REFCLK_FREQUENCY => IDELAYCTRL_FREQ_G, + SIGNAL_PATTERN => "DATA") + port map ( + C => delayClk, -- [in] + REGRST => '0', -- [in] + LD => delayLoad, -- [in] + CE => '0', -- [in] + INC => '1', -- [in] + CINVCTRL => '0', -- [in] + CNTVALUEIN => delayValue, -- [in] + IDATAIN => serialData, -- [in] + DATAIN => '0', -- [in] + LDPIPEEN => '0', -- [in] + DATAOUT => delayedData, -- [out] + CNTVALUEOUT => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Every supported width uses the same master. Q outputs above the selected + -- width and width-expansion shift outputs are ignored when no slave exists. + ------------------------------------------------------------------------------------------------ + U_Master : ISERDESE2 + generic map ( + DATA_RATE => "DDR", + DATA_WIDTH => SERIALIZATION_FACTOR_G, + INTERFACE_TYPE => "NETWORKING", + DYN_CLKDIV_INV_EN => "FALSE", + DYN_CLK_INV_EN => "FALSE", + NUM_CE => 1, + OFB_USED => "FALSE", + IOBDELAY => "IFD", + SERDES_MODE => "MASTER") + port map ( + Q1 => rawWord(0), -- [out] + Q2 => rawWord(1), -- [out] + Q3 => rawWord(2), -- [out] + Q4 => rawWord(3), -- [out] + Q5 => rawWord(4), -- [out] + Q6 => rawWord(5), -- [out] + Q7 => rawWord(6), -- [out] + Q8 => rawWord(7), -- [out] + SHIFTOUT1 => shift1, -- [out] + SHIFTOUT2 => shift2, -- [out] + BITSLIP => bitSlip, -- [in] + CE1 => '1', -- [in] + CE2 => '1', -- [in] + CLK => bitClk, -- [in] + CLKB => bitClkInv, -- [in] + CLKDIV => wordClk, -- [in] + CLKDIVP => '0', -- [in] + D => '0', -- [in] + DDLY => delayedData, -- [in] + RST => rst, -- [in] + SHIFTIN1 => '0', -- [in] + SHIFTIN2 => '0', -- [in] + DYNCLKDIVSEL => '0', -- [in] + DYNCLKSEL => '0', -- [in] + OFB => '0', -- [in] + OCLK => '0', -- [in] + OCLKB => '0', -- [in] + O => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- The native 10- and 14-bit DDR modes use the dedicated master/slave width + -- expansion path. For width 10 only slave Q3/Q4 are meaningful; width 14 + -- additionally uses Q5 through Q8. + ------------------------------------------------------------------------------------------------ + GEN_WIDTH_WIDE : if SERIALIZATION_FACTOR_G = 10 or SERIALIZATION_FACTOR_G = 14 generate + begin + U_Slave : ISERDESE2 + generic map ( + DATA_RATE => "DDR", + DATA_WIDTH => SERIALIZATION_FACTOR_G, + INTERFACE_TYPE => "NETWORKING", + DYN_CLKDIV_INV_EN => "FALSE", + DYN_CLK_INV_EN => "FALSE", + NUM_CE => 1, + OFB_USED => "FALSE", + IOBDELAY => "IFD", + SERDES_MODE => "SLAVE") + port map ( + Q1 => open, -- [out] + Q2 => open, -- [out] + Q3 => rawWord(8), -- [out] + Q4 => rawWord(9), -- [out] + Q5 => rawWord(10), -- [out] + Q6 => rawWord(11), -- [out] + Q7 => rawWord(12), -- [out] + Q8 => rawWord(13), -- [out] + SHIFTOUT1 => open, -- [out] + SHIFTOUT2 => open, -- [out] + BITSLIP => bitSlip, -- [in] + CE1 => '1', -- [in] + CE2 => '1', -- [in] + CLK => bitClk, -- [in] + CLKB => bitClkInv, -- [in] + CLKDIV => wordClk, -- [in] + CLKDIVP => '0', -- [in] + D => '0', -- [in] + DDLY => '0', -- [in] + RST => rst, -- [in] + SHIFTIN1 => shift1, -- [in] + SHIFTIN2 => shift2, -- [in] + DYNCLKDIVSEL => '0', -- [in] + DYNCLKSEL => '0', -- [in] + OFB => '0', -- [in] + OCLK => '0', -- [in] + OCLKB => '0', -- [in] + O => open); -- [out] + end generate GEN_WIDTH_WIDE; + + dataWord <= rawWord(SERIALIZATION_FACTOR_G-1 downto 0); + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd new file mode 100644 index 0000000000..1dc0d83774 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd @@ -0,0 +1,220 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Serialized DDR ADC physical input for AMD 7 Series FPGAs +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrPhy7Series is + generic ( + TPD_G : time := 1 ns; + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + SERIALIZATION_FACTOR_G : positive := 14; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); + port ( + adcClkRst : in sl; + idelayCtrlRdy : in sl := '1'; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPhy7Series; + +architecture rtl of AdcDdrPhy7Series is + + function bufrDivide (value : positive) return string is + begin + case value is + when 1 => return "1"; + when 2 => return "2"; + when 3 => return "3"; + when 4 => return "4"; + when 5 => return "5"; + when 6 => return "6"; + when 7 => return "7"; + when others => return "8"; + end case; + end function bufrDivide; + + signal dClkPad : sl; + signal bitClk : sl; + signal bitClkInv : sl; + signal wordClk : sl; + signal wordRst : sl; + signal deserReset : sl; + signal fcoPad : slv(FCO_LANES_G-1 downto 0); + signal dataPad : slv(DATA_LANES_G-1 downto 0); + +begin + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 14 + report "AdcDdrPhy7Series supports only native DDR widths 4, 6, 8, 10, and 14" + severity failure; + + GEN_MAP_CHECK : for i in DATA_LANES_G-1 downto 0 generate + assert DATA_FCO_MAP_G(i) < FCO_LANES_G + report "AdcDdrPhy7Series data-to-FCO mapping index is out of range" + severity failure; + end generate GEN_MAP_CHECK; + + U_DelayReadySync : entity surf.Synchronizer + generic map ( + TPD_G => TPD_G) + port map ( + clk => wordClk, -- [in] + rst => wordRst, -- [in] + dataIn => idelayCtrlRdy, -- [in] + dataOut => delayReady); -- [out] + + U_DcoInput : IBUFDS + generic map ( + DIFF_TERM => true, + IOSTANDARD => "LVDS_25") + port map ( + I => dClkP, -- [in] + IB => dClkN, -- [in] + O => dClkPad); -- [out] + + U_BitClock : BUFIO + port map ( + I => dClkPad, -- [in] + O => bitClk); -- [out] + + bitClkInv <= not bitClk; + + U_WordClock : BUFR + generic map ( + SIM_DEVICE => "7SERIES", + BUFR_DIVIDE => bufrDivide(SERIALIZATION_FACTOR_G/2)) + port map ( + I => dClkPad, -- [in] + O => wordClk, -- [out] + CE => '1', -- [in] + CLR => '0'); -- [in] + + U_WordReset : entity surf.RstSync + generic map ( + TPD_G => TPD_G, + RELEASE_DELAY_G => 5) + port map ( + clk => wordClk, -- [in] + asyncRst => adcClkRst, -- [in] + syncRst => wordRst); -- [out] + + deserReset <= wordRst or phyReset; + + -- The common PHY command carries the widest supported nine-bit value. Fail + -- on an invalid 7-Series load instead of silently discarding its upper bits. + GEN_FCO : for i in FCO_LANES_G-1 downto 0 generate + begin + assert fcoDelayWrite(i).load /= '1' or fcoDelayWrite(i).value(8 downto 5) = X"0" + report "AdcDdrPhy7Series FCO delay value exceeds the native five-bit range; " & + "the value will be truncated" + severity warning; + + U_Input : IBUFDS + generic map ( + DIFF_TERM => true) + port map ( + I => fcoP(i), -- [in] + IB => fcoN(i), -- [in] + O => fcoPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializer7Series + generic map ( + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(i), -- [in] + delayClk => wordClk, -- [in] + delayValue => fcoDelayWrite(i).value(4 downto 0), -- [in] + delayLoad => fcoDelayWrite(i).load, -- [in] + serialData => fcoPad(i), -- [in] + dataWord => fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0)); -- [out] + + fcoWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_FCO; + + GEN_DATA : for i in DATA_LANES_G-1 downto 0 generate + begin + assert dataDelayWrite(i).load /= '1' or dataDelayWrite(i).value(8 downto 5) = X"0" + report "AdcDdrPhy7Series data delay value exceeds the native five-bit range; " & + "the value will be truncated" + severity warning; + + U_Input : IBUFDS + generic map ( + DIFF_TERM => true) + port map ( + I => dataP(i), -- [in] + IB => dataN(i), -- [in] + O => dataPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializer7Series + generic map ( + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(DATA_FCO_MAP_G(i)), -- [in] + delayClk => wordClk, -- [in] + delayValue => dataDelayWrite(i).value(4 downto 0), -- [in] + delayLoad => dataDelayWrite(i).load, -- [in] + serialData => dataPad(i), -- [in] + dataWord => dataWord(i)(SERIALIZATION_FACTOR_G-1 downto 0)); -- [out] + + dataWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_DATA; + + captureClk <= wordClk; + captureRst <= wordRst; + dataValid <= (others => not deserReset); + fcoValid <= (others => not deserReset); + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/7Series/ruckus.tcl b/devices/AnalogDevices/adcDdr/7Series/ruckus.tcl new file mode 100644 index 0000000000..5c74f6b308 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/7Series/ruckus.tcl @@ -0,0 +1,13 @@ +#----------------------------------------------------------------------------- +# This file is part of 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/rtl" -fileType "VHDL 2008" diff --git a/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd new file mode 100644 index 0000000000..d6f967ecfd --- /dev/null +++ b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd @@ -0,0 +1,134 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: One delayed DDR input lane for AMD UltraScale FPGAs +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrDeserializerUltraScale is + generic ( + TPD_G : time := 1 ns; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + SERIALIZATION_FACTOR_G : positive := 14); + port ( + bitClk : in sl; + bitClkInv : in sl; + wordClk : in sl; + rst : in sl; + bitSlip : in sl; + + delayClk : in sl; + delayValue : in slv(8 downto 0); + delayLoad : in sl; + + serialData : in sl; + dataWord : out slv(SERIALIZATION_FACTOR_G-1 downto 0); + dataValid : out sl); +end entity AdcDdrDeserializerUltraScale; + +architecture rtl of AdcDdrDeserializerUltraScale is + + constant ISERDES_WIDTH_C : positive := ite(SERIALIZATION_FACTOR_G <= 6, 4, 8); + + signal delayedData : sl; + signal rawData : slv(7 downto 0); + signal gearboxData : slv(SERIALIZATION_FACTOR_G-1 downto 0); + + attribute IODELAY_GROUP : string; + attribute IODELAY_GROUP of U_Delay : label is IODELAY_GROUP_G; + +begin + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 12 or SERIALIZATION_FACTOR_G = 14 + report "AdcDdrDeserializerUltraScale supports only DDR widths 4, 6, 8, 10, 12, and 14" + severity failure; + + U_Delay : entity surf.Idelaye3Wrapper + generic map ( + CASCADE => "NONE", + DELAY_FORMAT => "COUNT", + DELAY_SRC => "IDATAIN", + DELAY_TYPE => "VAR_LOAD", + DELAY_VALUE => 0, + IS_CLK_INVERTED => '0', + IS_RST_INVERTED => '0', + SIM_DEVICE => DEVICE_FAMILY_G, + UPDATE_MODE => "ASYNC") + port map ( + BUSY => open, -- [out] + CASC_IN => '0', -- [in] + CASC_OUT => open, -- [out] + CASC_RETURN => '0', -- [in] + CNTVALUEOUT => open, -- [out] + DATAOUT => delayedData, -- [out] + CE => '0', -- [in] + CLK => delayClk, -- [in] + CNTVALUEIN => delayValue, -- [in] + DATAIN => '0', -- [in] + EN_VTC => '0', -- [in] + IDATAIN => serialData, -- [in] + INC => '0', -- [in] + LOAD => delayLoad, -- [in] + RST => rst); -- [in] + + U_Deserializer : ISERDESE3 + generic map ( + DATA_WIDTH => ISERDES_WIDTH_C, + FIFO_ENABLE => "FALSE", + FIFO_SYNC_MODE => "FALSE", + IS_CLK_B_INVERTED => '0', + IS_CLK_INVERTED => '0', + IS_RST_INVERTED => '0', + SIM_DEVICE => DEVICE_FAMILY_G) + port map ( + FIFO_EMPTY => open, -- [out] + INTERNAL_DIVCLK => open, -- [out] + Q => rawData, -- [out] + CLK => bitClk, -- [in] + CLKDIV => wordClk, -- [in] + CLK_B => bitClkInv, -- [in] + D => delayedData, -- [in] + FIFO_RD_CLK => '0', -- [in] + FIFO_RD_EN => '0', -- [in] + RST => rst); -- [in] + + U_Gearbox : entity surf.Gearbox + generic map ( + TPD_G => TPD_G, + SLAVE_WIDTH_G => ISERDES_WIDTH_C, + MASTER_WIDTH_G => SERIALIZATION_FACTOR_G, + MASTER_BIT_REVERSE_G => true) + port map ( + clk => wordClk, -- [in] + rst => rst, -- [in] + slaveData => rawData(ISERDES_WIDTH_C-1 downto 0), -- [in] + slaveValid => '1', -- [in] + slaveReady => open, -- [out] + startOfSeq => '0', -- [in] + slip => bitSlip, -- [in] + masterData => gearboxData, -- [out] + masterValid => dataValid, -- [out] + masterReady => '1'); -- [in] + + dataWord <= gearboxData; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrPhyUltraScale.vhd b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrPhyUltraScale.vhd new file mode 100644 index 0000000000..3206d68960 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrPhyUltraScale.vhd @@ -0,0 +1,182 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Serialized DDR ADC physical input for AMD UltraScale FPGAs +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrPhyUltraScale is + generic ( + TPD_G : time := 1 ns; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + SERIALIZATION_FACTOR_G : positive := 14; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); + port ( + adcClkRst : in sl; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPhyUltraScale; + +architecture rtl of AdcDdrPhyUltraScale is + + constant ISERDES_WIDTH_C : positive := ite(SERIALIZATION_FACTOR_G <= 6, 4, 8); + + signal bitClk : sl; + signal bitClkInv : sl; + signal wordClk : sl; + signal wordRst : sl; + signal deserReset : sl; + signal fcoPad : slv(FCO_LANES_G-1 downto 0); + signal dataPad : slv(DATA_LANES_G-1 downto 0); + +begin + + assert DEVICE_FAMILY_G = "ULTRASCALE" or DEVICE_FAMILY_G = "ULTRASCALE_PLUS" + report "AdcDdrPhyUltraScale DEVICE_FAMILY_G must be ULTRASCALE or ULTRASCALE_PLUS" + severity failure; + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 12 or SERIALIZATION_FACTOR_G = 14 + report "AdcDdrPhyUltraScale supports only DDR widths 4, 6, 8, 10, 12, and 14" + severity failure; + + GEN_MAP_CHECK : for i in DATA_LANES_G-1 downto 0 generate + assert DATA_FCO_MAP_G(i) < FCO_LANES_G + report "AdcDdrPhyUltraScale data-to-FCO mapping index is out of range" + severity failure; + end generate GEN_MAP_CHECK; + + U_DcoInput : IBUFGDS + port map ( + I => dClkP, -- [in] + IB => dClkN, -- [in] + O => bitClk); -- [out] + + bitClkInv <= not bitClk; + + U_WordClock : BUFGCE_DIV + generic map ( + BUFGCE_DIVIDE => ISERDES_WIDTH_C/2, + IS_CE_INVERTED => '0', + IS_CLR_INVERTED => '0', + IS_I_INVERTED => '0') + port map ( + I => bitClk, -- [in] + O => wordClk, -- [out] + CE => '1', -- [in] + CLR => '0'); -- [in] + + U_WordReset : entity surf.RstSync + generic map ( + TPD_G => TPD_G, + RELEASE_DELAY_G => 5) + port map ( + clk => wordClk, -- [in] + asyncRst => adcClkRst, -- [in] + syncRst => wordRst); -- [out] + + deserReset <= wordRst or phyReset; + + GEN_FCO : for i in FCO_LANES_G-1 downto 0 generate + begin + U_Input : IBUFDS + port map ( + I => fcoP(i), -- [in] + IB => fcoN(i), -- [in] + O => fcoPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializerUltraScale + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(i), -- [in] + delayClk => wordClk, -- [in] + delayValue => fcoDelayWrite(i).value, -- [in] + delayLoad => fcoDelayWrite(i).load, -- [in] + serialData => fcoPad(i), -- [in] + dataWord => fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0), -- [out] + dataValid => fcoValid(i)); -- [out] + + fcoWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_FCO; + + GEN_DATA : for i in DATA_LANES_G-1 downto 0 generate + begin + U_Input : IBUFDS + port map ( + I => dataP(i), -- [in] + IB => dataN(i), -- [in] + O => dataPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializerUltraScale + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(DATA_FCO_MAP_G(i)), -- [in] + delayClk => wordClk, -- [in] + delayValue => dataDelayWrite(i).value, -- [in] + delayLoad => dataDelayWrite(i).load, -- [in] + serialData => dataPad(i), -- [in] + dataWord => dataWord(i)(SERIALIZATION_FACTOR_G-1 downto 0), -- [out] + dataValid => dataValid(i)); -- [out] + + dataWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_DATA; + + captureClk <= wordClk; + captureRst <= wordRst; + delayReady <= not wordRst; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/UltraScale/ruckus.tcl b/devices/AnalogDevices/adcDdr/UltraScale/ruckus.tcl new file mode 100644 index 0000000000..5c74f6b308 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/UltraScale/ruckus.tcl @@ -0,0 +1,13 @@ +#----------------------------------------------------------------------------- +# This file is part of 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/rtl" -fileType "VHDL 2008" diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd new file mode 100644 index 0000000000..73f07473a7 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd @@ -0,0 +1,608 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Common control and readout core for serialized DDR ADCs +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.std_logic_unsigned.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrCore is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + CHANNELS_G : positive := 8; + SAMPLE_WIDTH_G : positive range 2 to 16 := 14; + SERIALIZATION_FACTOR_G : positive := 14; + DELAY_BITS_G : positive range 1 to 9 := 5; + DATA_DELAY_INIT_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(FCO_LANES_G-1 downto 0) := (others => 0); + FIFO_ADDR_WIDTH_G : positive := 4; + FRAME_PATTERN_G : slv := "11111110000000"; + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + + captureClk : in sl; + captureRst : in sl; + delayReady : in sl; + fcoWord : in Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : in slv(FCO_LANES_G-1 downto 0) := (others => '1'); + sampleValid : in sl; + sampleIn : in Slv16Array(CHANNELS_G-1 downto 0); + phyReset : out sl; + bitSlip : out slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : out AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : out AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + streamClk : in sl; + streamRst : in sl; + streams : out AxiStreamMasterArray(CHANNELS_G-1 downto 0)); +end entity AdcDdrCore; + +architecture rtl of AdcDdrCore is + + constant FIFO_WIDTH_C : positive := (16*CHANNELS_G)+1; + constant AXIS_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => 2, + TDEST_BITS_C => 8, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_FIXED_C, + TUSER_BITS_C => 1, + TUSER_MODE_C => TUSER_NORMAL_C); + constant AXIL_CONFIG_C : AxiLiteCrossbarMasterConfigArray(1 downto 0) := ( + 0 => ( + baseAddr => AXIL_BASE_ADDR_G, + addrBits => 11, + connectivity => x"0001"), + 1 => ( + baseAddr => AXIL_BASE_ADDR_G + resize(ADC_DDR_PATTERN_BASE_ADDR_C, 32), + addrBits => 8, + connectivity => x"0001")); + + type FcoCountArray is array (natural range <>) of natural range 0 to ADC_DDR_LOCK_MATCHES_C; + type FcoErrorArray is array (natural range <>) of natural range 0 to ADC_DDR_UNLOCK_ERRORS_C; + type FcoWaitArray is array (natural range <>) of natural range 0 to ADC_DDR_BITSLIP_INTERVAL_C-1; + + function delayValuesToSlv ( + values : NaturalArray; + delayClass : string) + return Slv9Array is + variable result : Slv9Array(values'range); + begin + for i in values'range loop + assert values(i) < 2**DELAY_BITS_G + report "AdcDdrCore " & delayClass & " initial delay exceeds DELAY_BITS_G" + severity failure; + result(i) := toSlv(values(i), 9); + end loop; + return result; + end function delayValuesToSlv; + + type RegType is record + phyReset : sl; + startupPending : sl; + relock : sl; + clearCounters : sl; + bitSlip : slv(FCO_LANES_G-1 downto 0); + locked : slv(FCO_LANES_G-1 downto 0); + matchCount : FcoCountArray(FCO_LANES_G-1 downto 0); + errorCount : FcoErrorArray(FCO_LANES_G-1 downto 0); + slipWait : FcoWaitArray(FCO_LANES_G-1 downto 0); + lostLockCount : Slv32Array(FCO_LANES_G-1 downto 0); + overflowCount : slv(31 downto 0); + overflow : sl; + dataDelay : Slv9Array(DATA_LANES_G-1 downto 0); + fcoDelay : Slv9Array(FCO_LANES_G-1 downto 0); + dataDelayLoad : slv(DATA_LANES_G-1 downto 0); + fcoDelayLoad : slv(FCO_LANES_G-1 downto 0); + debugBusy : sl; + debugIndex : natural range 0 to 3; + snapshotSeq : slv(31 downto 0); + debugWorking : Slv16Array((CHANNELS_G*4)-1 downto 0); + debugSample : Slv16Array((CHANNELS_G*4)-1 downto 0); + formatted : Slv16Array(CHANNELS_G-1 downto 0); + sampleValid : sl; + axilReadSlave : AxiLiteReadSlaveType; + axilWriteSlave : AxiLiteWriteSlaveType; + end record RegType; + + constant REG_INIT_C : RegType := ( + phyReset => '0', + startupPending => '1', + relock => '0', + clearCounters => '0', + bitSlip => (others => '0'), + locked => (others => '0'), + matchCount => (others => 0), + errorCount => (others => 0), + slipWait => (others => 0), + lostLockCount => (others => (others => '0')), + overflowCount => (others => '0'), + overflow => '0', + dataDelay => delayValuesToSlv(DATA_DELAY_INIT_G, "data"), + fcoDelay => delayValuesToSlv(FCO_DELAY_INIT_G, "FCO"), + dataDelayLoad => (others => '0'), + fcoDelayLoad => (others => '0'), + debugBusy => '0', + debugIndex => 0, + snapshotSeq => (others => '0'), + debugWorking => (others => (others => '0')), + debugSample => (others => (others => '0')), + formatted => (others => (others => '0')), + sampleValid => '0', + axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, + axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + + signal syncAxilReadMaster : AxiLiteReadMasterType; + signal syncAxilReadSlave : AxiLiteReadSlaveType; + signal syncAxilWriteMaster : AxiLiteWriteMasterType; + signal syncAxilWriteSlave : AxiLiteWriteSlaveType; + + signal xbarMasterReadMasters : AxiLiteReadMasterArray(1 downto 0); + signal xbarMasterReadSlaves : AxiLiteReadSlaveArray(1 downto 0); + signal xbarMasterWriteMasters : AxiLiteWriteMasterArray(1 downto 0); + signal xbarMasterWriteSlaves : AxiLiteWriteSlaveArray(1 downto 0); + + signal fifoIn : slv(FIFO_WIDTH_C-1 downto 0); + signal fifoOut : slv(FIFO_WIDTH_C-1 downto 0); + signal fifoValid : sl; + signal fifoOverflow : sl; + +begin + + -- The frame-pattern width is part of the logical PHY contract. Catch a + -- device-wrapper mismatch at elaboration instead of silently truncating it. + assert FRAME_PATTERN_G'length = SERIALIZATION_FACTOR_G + report "FRAME_PATTERN_G length must equal SERIALIZATION_FACTOR_G" + severity failure; + + assert DATA_LANES_G <= 64 + report "Data-delay register window supports at most 64 lanes" + severity failure; + + assert FCO_LANES_G <= 16 + report "FCO register windows support at most 16 lanes" + severity failure; + + assert CHANNELS_G <= 32 + report "Debug register window supports at most 32 channels" + severity failure; + + assert not PATTERN_CHECK_G or CHANNELS_G <= 16 + report "Pattern result windows support at most 16 channels" + severity failure; + + assert not PATTERN_CHECK_G or FCO_LANES_G <= 16 + report "Pattern result windows support at most 16 FCO lanes" + severity failure; + + ------------------------------------------------------------------------------------------------- + -- Cross the complete AXI-Lite bus into the ADC word-clock domain. Keeping + -- the endpoint beside the capture state eliminates individual command and + -- status CDC paths and makes multi-bit register reads inherently coherent. + ------------------------------------------------------------------------------------------------- + U_AxiLiteAsync : entity surf.AxiLiteAsync + generic map ( + TPD_G => TPD_G) + port map ( + sAxiClk => axilClk, -- [in] + sAxiClkRst => axilRst, -- [in] + sAxiReadMaster => axilReadMaster, -- [in] + sAxiReadSlave => axilReadSlave, -- [out] + sAxiWriteMaster => axilWriteMaster, -- [in] + sAxiWriteSlave => axilWriteSlave, -- [out] + mAxiClk => captureClk, -- [in] + mAxiClkRst => captureRst, -- [in] + mAxiReadMaster => syncAxilReadMaster, -- [out] + mAxiReadSlave => syncAxilReadSlave, -- [in] + mAxiWriteMaster => syncAxilWriteMaster, -- [out] + mAxiWriteSlave => syncAxilWriteSlave); -- [in] + + U_AxiLiteCrossbar : entity surf.AxiLiteCrossbar + generic map ( + TPD_G => TPD_G, + NUM_SLAVE_SLOTS_G => 1, + NUM_MASTER_SLOTS_G => 2, + MASTERS_CONFIG_G => AXIL_CONFIG_C) + port map ( + axiClk => captureClk, -- [in] + axiClkRst => captureRst, -- [in] + sAxiReadMasters(0) => syncAxilReadMaster, -- [in] + sAxiReadSlaves(0) => syncAxilReadSlave, -- [out] + sAxiWriteMasters(0) => syncAxilWriteMaster, -- [in] + sAxiWriteSlaves(0) => syncAxilWriteSlave, -- [out] + mAxiReadMasters => xbarMasterReadMasters, -- [out] + mAxiReadSlaves => xbarMasterReadSlaves, -- [in] + mAxiWriteMasters => xbarMasterWriteMasters, -- [out] + mAxiWriteSlaves => xbarMasterWriteSlaves); -- [in] + + comb : process (captureRst, delayReady, fcoValid, fcoWord, fifoOverflow, r, + sampleIn, sampleValid, xbarMasterReadMasters, + xbarMasterWriteMasters) is + variable v : RegType; + variable ep : AxiLiteEndpointType; + variable word : slv(SAMPLE_WIDTH_G-1 downto 0); + variable snapshotTxn : sl; + begin + v := r; + + ---------------------------------------------------------------------------------------------- + -- Default all command-like outputs low. AXI writes below assert these + -- fields for exactly one captureClk cycle; sampleValid is pipelined with + -- the formatted sample written into the output FIFO. + ---------------------------------------------------------------------------------------------- + v.relock := '0'; + v.clearCounters := '0'; + v.bitSlip := (others => '0'); + v.dataDelayLoad := (others => '0'); + v.fcoDelayLoad := (others => '0'); + v.sampleValid := sampleValid; + + ---------------------------------------------------------------------------------------------- + -- FCO word alignment and continuous lock monitoring + -- + -- While unlocked, require consecutive matching frame words before + -- declaring lock. A mismatch requests bitslip only after the enforced + -- quiet interval. Once locked, consecutive errors drop lock and increment + -- the saturating lost-lock counter. + ---------------------------------------------------------------------------------------------- + for i in FCO_LANES_G-1 downto 0 loop + if (r.relock = '1' or r.phyReset = '1' or r.startupPending = '1') then + v.locked(i) := '0'; + v.matchCount(i) := 0; + v.errorCount(i) := 0; + v.slipWait(i) := 0; + elsif (fcoValid(i) = '1') then + if (r.locked(i) = '0') then + v.errorCount(i) := 0; + if (r.slipWait(i) /= 0) then + -- Do not evaluate the ISERDES output until the DDR BITSLIP + -- operation has propagated through its CLKDIV pipeline. + v.matchCount(i) := 0; + v.slipWait(i) := r.slipWait(i) - 1; + elsif (fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0) = FRAME_PATTERN_G) then + if (r.matchCount(i) = ADC_DDR_LOCK_MATCHES_C-1) then + v.matchCount(i) := ADC_DDR_LOCK_MATCHES_C; + v.locked(i) := '1'; + else + v.matchCount(i) := r.matchCount(i) + 1; + end if; + else + v.matchCount(i) := 0; + v.bitSlip(i) := '1'; + v.slipWait(i) := ADC_DDR_BITSLIP_INTERVAL_C-1; + end if; + else + v.matchCount(i) := ADC_DDR_LOCK_MATCHES_C; + v.slipWait(i) := 0; + if (fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0) = FRAME_PATTERN_G) then + v.errorCount(i) := 0; + elsif (r.errorCount(i) = ADC_DDR_UNLOCK_ERRORS_C-1) then + v.locked(i) := '0'; + v.matchCount(i) := 0; + v.errorCount(i) := 0; + if (r.lostLockCount(i) /= x"FFFFFFFF") then + v.lostLockCount(i) := r.lostLockCount(i) + 1; + end if; + else + v.errorCount(i) := r.errorCount(i) + 1; + end if; + end if; + end if; + end loop; + + ---------------------------------------------------------------------------------------------- + -- Event accounting + -- + -- The wide sample FIFO represents one coherent channel group, so one + -- sticky flag and counter account for every dropped sample group. + ---------------------------------------------------------------------------------------------- + if (fifoOverflow = '1') then + v.overflow := '1'; + if (r.overflowCount /= x"FFFFFFFF") then + v.overflowCount := r.overflowCount + 1; + end if; + end if; + + -- Clear is deliberately applied after event accumulation so software gets + -- a deterministic all-zero result even when an event is present that cycle. + if (r.clearCounters = '1') then + v.lostLockCount := (others => (others => '0')); + v.overflowCount := (others => '0'); + v.overflow := '0'; + end if; + + ---------------------------------------------------------------------------------------------- + -- Numeric sample formatting + -- + -- Physical lane polarity and word ordering are resolved by the device + -- wrapper. Here offset-binary conversion and optional arithmetic negation + -- operate only across the meaningful ADC width; upper transport bits stay zero. + ---------------------------------------------------------------------------------------------- + if (sampleValid = '1') then + for i in CHANNELS_G-1 downto 0 loop + word := sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0); + if (OFFSET_BINARY_G) then + word(SAMPLE_WIDTH_G-1) := not word(SAMPLE_WIDTH_G-1); + end if; + if (NEGATE_G) then + word := (not word) + 1; + end if; + v.formatted(i) := (others => '0'); + v.formatted(i)(SAMPLE_WIDTH_G-1 downto 0) := word; + end loop; + end if; + + ---------------------------------------------------------------------------------------------- + -- Atomic debug snapshot + -- + -- Four raw, assembled channel groups are collected before numeric format + -- conversion so physical-lane calibration is independent of offset-binary + -- conversion and arithmetic negation. Publishing the whole bank only + -- after the fourth group prevents AXI software from observing a snapshot + -- assembled from different requests. + ---------------------------------------------------------------------------------------------- + if (r.debugBusy = '1' and sampleValid = '1') then + for i in CHANNELS_G-1 downto 0 loop + v.debugWorking((r.debugIndex*CHANNELS_G)+i) := + resize(sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0), 16); + end loop; + if (r.debugIndex = 3) then + v.debugBusy := '0'; + v.snapshotSeq := r.snapshotSeq + 1; + v.debugSample := v.debugWorking; + else + v.debugIndex := r.debugIndex + 1; + end if; + end if; + + ---------------------------------------------------------------------------------------------- + -- Normalized AXI-Lite register map + -- + -- Delay writes update the retained programmed value and create a one-cycle + -- load strobe for the corresponding PHY lane. Status and debug windows are + -- read directly in this clock domain, so no secondary status image exists. + ---------------------------------------------------------------------------------------------- + axiSlaveWaitTxn(ep, xbarMasterWriteMasters(0), xbarMasterReadMasters(0), + v.axilWriteSlave, v.axilReadSlave); + + -- A snapshot write remains outstanding until the complete four-sample + -- bank has been published. Holding both write-channel ready signals low + -- keeps the request stable without adding a second command/status CDC. + -- Reads remain available while the capture is active. + snapshotTxn := r.debugBusy; + if (r.debugBusy = '1') then + ep.axiStatus.writeEnable := '0'; + if (sampleValid = '1' and r.debugIndex = 3) then + axiSlaveWriteResponse(ep.axiWriteSlave); + end if; + elsif (ep.axiStatus.writeEnable = '1' and + ep.axiWriteMaster.awaddr(11 downto 0) = ADC_DDR_SNAPSHOT_ADDR_C and + ep.axiWriteMaster.wstrb(0) = '1' and + ep.axiWriteMaster.wdata(0) = '1') then + snapshotTxn := '1'; + ep.axiStatus.writeEnable := '0'; + if (r.phyReset = '1' or r.startupPending = '1') then + axiSlaveWriteResponse(ep.axiWriteSlave, AXI_RESP_SLVERR_C); + else + v.debugBusy := '1'; + v.debugIndex := 0; + end if; + end if; + + axiSlaveRegisterR(ep, ADC_DDR_VERSION_ADDR_C, 0, ADC_DDR_VERSION_C); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 0, toSlv(DATA_LANES_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 8, toSlv(FCO_LANES_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 16, toSlv(CHANNELS_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 24, toSlv(SAMPLE_WIDTH_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES1_ADDR_C, 0, toSlv(DELAY_BITS_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES1_ADDR_C, 8, toSlv(SERIALIZATION_FACTOR_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES1_ADDR_C, + ADC_DDR_CAP_PATTERN_CHECK_BIT_C, toSl(PATTERN_CHECK_G)); + axiSlaveRegister(ep, ADC_DDR_CAPTURE_RESET_ADDR_C, 0, v.phyReset); + axiSlaveRegister(ep, ADC_DDR_RELOCK_ADDR_C, 0, v.relock); + axiSlaveRegister(ep, ADC_DDR_CLEAR_COUNTERS_ADDR_C, 0, v.clearCounters); + axiSlaveRegisterR(ep, ADC_DDR_STATUS_ADDR_C, + ADC_DDR_STATUS_DELAY_READY_BIT_C, delayReady); + axiSlaveRegisterR(ep, ADC_DDR_STATUS_ADDR_C, + ADC_DDR_STATUS_ALL_LOCKED_BIT_C, uAnd(r.locked)); + axiSlaveRegisterR(ep, ADC_DDR_STATUS_ADDR_C, + ADC_DDR_STATUS_ANY_OVERFLOW_BIT_C, r.overflow); + axiSlaveRegisterR(ep, ADC_DDR_LOCKED_MASK_ADDR_C, 0, r.locked); + axiSlaveRegisterR(ep, ADC_DDR_SNAPSHOT_SEQUENCE_ADDR_C, 0, r.snapshotSeq); + + for i in DATA_LANES_G-1 downto 0 loop + axiSlaveRegister(ep, ADC_DDR_DATA_DELAY_ADDR_C+(4*i), 0, + v.dataDelay(i)(DELAY_BITS_G-1 downto 0)); + if (ep.axiStatus.writeEnable = '1' and + xbarMasterWriteMasters(0).awaddr(11 downto 0) = + ADC_DDR_DATA_DELAY_ADDR_C+(4*i)) then + v.dataDelayLoad(i) := '1'; + end if; + end loop; + for i in FCO_LANES_G-1 downto 0 loop + axiSlaveRegister(ep, ADC_DDR_FCO_DELAY_ADDR_C+(4*i), 0, + v.fcoDelay(i)(DELAY_BITS_G-1 downto 0)); + if (ep.axiStatus.writeEnable = '1' and + xbarMasterWriteMasters(0).awaddr(11 downto 0) = + ADC_DDR_FCO_DELAY_ADDR_C+(4*i)) then + v.fcoDelayLoad(i) := '1'; + end if; + axiSlaveRegisterR(ep, ADC_DDR_FCO_WORD_ADDR_C+(4*i), 0, fcoWord(i)); + axiSlaveRegisterR(ep, ADC_DDR_LOST_LOCK_COUNT_ADDR_C+(4*i), 0, r.lostLockCount(i)); + end loop; + axiSlaveRegisterR(ep, ADC_DDR_OVERFLOW_COUNT_ADDR_C, 0, r.overflowCount); + for i in CHANNELS_G-1 downto 0 loop + for j in 3 downto 0 loop + axiSlaveRegisterR(ep, ADC_DDR_DEBUG_ADDR_C+(16*i)+(4*j), 0, + r.debugSample((j*CHANNELS_G)+i)); + end loop; + end loop; + axiSlaveDefault(ep, v.axilWriteSlave, v.axilReadSlave, + AXI_RESP_DECERR_C, snapshotTxn); + + ---------------------------------------------------------------------------------------------- + -- Hold the PHY after capture reset until its delay resources are ready, + -- then apply every configured delay and release alignment automatically. + -- A manual reset rearms the same sequence, so releasing it also waits for + -- delay readiness and reapplies all retained values. + ---------------------------------------------------------------------------------------------- + if (v.phyReset = '1') then + v.startupPending := '1'; + elsif (r.startupPending = '1' and delayReady = '1') then + v.startupPending := '0'; + v.relock := '1'; + v.dataDelayLoad := (others => '1'); + v.fcoDelayLoad := (others => '1'); + end if; + + ---------------------------------------------------------------------------------------------- + -- Capture-domain reset and registered outputs + -- + -- phyReset is a software-controlled manual hold and must not reset this + -- endpoint; captureRst is the only reset for the core state and AXI + -- slave records. startupPending provides the hardware-owned startup hold. + ---------------------------------------------------------------------------------------------- + if (captureRst = '1') then + v := REG_INIT_C; + end if; + + rin <= v; + + xbarMasterReadSlaves(0) <= r.axilReadSlave; + xbarMasterWriteSlaves(0) <= r.axilWriteSlave; + phyReset <= r.phyReset or r.startupPending; + bitSlip <= r.bitSlip; + for i in DATA_LANES_G-1 downto 0 loop + dataDelayWrite(i).value <= r.dataDelay(i); + dataDelayWrite(i).load <= r.dataDelayLoad(i); + end loop; + for i in FCO_LANES_G-1 downto 0 loop + fcoDelayWrite(i).value <= r.fcoDelay(i); + fcoDelayWrite(i).load <= r.fcoDelayLoad(i); + end loop; + end process comb; + + seq : process (captureClk) is + begin + if rising_edge(captureClk) then + r <= rin after TPD_G; + end if; + end process seq; + + GEN_PATTERN_CHECK : if (PATTERN_CHECK_G) generate + U_PatternTester : entity surf.AdcDdrPatternTester + generic map ( + TPD_G => TPD_G, + CHANNELS_G => CHANNELS_G, + FCO_LANES_G => FCO_LANES_G, + SAMPLE_WIDTH_G => SAMPLE_WIDTH_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G, + FRAME_PATTERN_G => FRAME_PATTERN_G) + port map ( + clk => captureClk, -- [in] + rst => captureRst, -- [in] + axilReadMaster => xbarMasterReadMasters(1), -- [in] + axilReadSlave => xbarMasterReadSlaves(1), -- [out] + axilWriteMaster => xbarMasterWriteMasters(1), -- [in] + axilWriteSlave => xbarMasterWriteSlaves(1), -- [out] + sampleValid => sampleValid, -- [in] + sampleIn => sampleIn, -- [in] + fcoValid => fcoValid, -- [in] + fcoWord => fcoWord); -- [in] + end generate GEN_PATTERN_CHECK; + + GEN_NO_PATTERN_CHECK : if (not PATTERN_CHECK_G) generate + xbarMasterReadSlaves(1) <= AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; + xbarMasterWriteSlaves(1) <= AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; + end generate GEN_NO_PATTERN_CHECK; + + ------------------------------------------------------------------------------------------------- + -- Coherent sample clock crossing + -- + -- Pack all channels plus one common alignment-error bit into a single FIFO + -- word. This preserves channel-to-channel sample association across unrelated + -- capture and stream clocks. + ------------------------------------------------------------------------------------------------- + fifoIn(FIFO_WIDTH_C-1) <= not uAnd(r.locked); + GEN_FIFO_IN : for i in CHANNELS_G-1 downto 0 generate + fifoIn((16*i)+15 downto 16*i) <= r.formatted(i); + end generate GEN_FIFO_IN; + + U_DataFifo : entity surf.FifoAsync + generic map ( + TPD_G => TPD_G, + MEMORY_TYPE_G => "distributed", + FWFT_EN_G => true, + DATA_WIDTH_G => FIFO_WIDTH_C, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_G) + port map ( + rst => captureRst or streamRst, -- [in] + wr_clk => captureClk, -- [in] + wr_en => r.sampleValid and not r.phyReset and not r.startupPending, -- [in] + din => fifoIn, -- [in] + wr_data_count => open, -- [out] + wr_ack => open, -- [out] + overflow => fifoOverflow, -- [out] + prog_full => open, -- [out] + almost_full => open, -- [out] + full => open, -- [out] + not_full => open, -- [out] + rd_clk => streamClk, -- [in] + rd_en => fifoValid, -- [in] + dout => fifoOut, -- [out] + rd_data_count => open, -- [out] + valid => fifoValid, -- [out] + underflow => open, -- [out] + prog_empty => open, -- [out] + almost_empty => open, -- [out] + empty => open); -- [out] + + ------------------------------------------------------------------------------------------------- + -- Reconstruct one always-consumed AXI Stream output per logical channel. + -- tUser(0) reports that the capture group was not aligned without disturbing + -- sample cadence or imposing SSI framing on the stream. + ------------------------------------------------------------------------------------------------- + GEN_STREAM : for i in CHANNELS_G-1 downto 0 generate + format : process (fifoOut, fifoValid) is + variable v : AxiStreamMasterType; + begin + v := axiStreamMasterInit(AXIS_CONFIG_C); + v.tValid := fifoValid; + v.tData(15 downto 0) := fifoOut((16*i)+15 downto 16*i); + v.tDest := toSlv(i, 8); + v.tUser(0) := fifoOut(FIFO_WIDTH_C-1); + streams(i) <= v; + end process format; + end generate GEN_STREAM; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd new file mode 100644 index 0000000000..8c048165d9 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd @@ -0,0 +1,389 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Parallel finite-window pattern checker for serialized ADC data +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrPatternTester is + generic ( + TPD_G : time := 1 ns; + CHANNELS_G : positive := 8; + FCO_LANES_G : positive := 1; + SAMPLE_WIDTH_G : positive range 2 to 16 := 14; + SERIALIZATION_FACTOR_G : positive := 14; + FRAME_PATTERN_G : slv := "11111110000000"); + port ( + clk : in sl; + rst : in sl; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + sampleValid : in sl; + sampleIn : in Slv16Array(CHANNELS_G-1 downto 0); + fcoValid : in slv(FCO_LANES_G-1 downto 0); + fcoWord : in Slv16Array(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPatternTester; + +architecture rtl of AdcDdrPatternTester is + + type RegType is record + start : sl; + abort : sl; + cfgAlternating : sl; + cfgReference : slv(7 downto 0); + cfgChannelMask : slv(CHANNELS_G-1 downto 0); + cfgFcoMask : slv(FCO_LANES_G-1 downto 0); + cfgDataMask : slv(SAMPLE_WIDTH_G-1 downto 0); + cfgPatternA : slv(SAMPLE_WIDTH_G-1 downto 0); + cfgPatternB : slv(SAMPLE_WIDTH_G-1 downto 0); + cfgSamples : slv(31 downto 0); + cfgTimeout : slv(31 downto 0); + busy : sl; + done : sl; + timedOut : sl; + configError : sl; + aborted : sl; + phaseAcquired : sl; + expectedPhase : sl; + allChannelsPass : sl; + allFcoPass : sl; + alternating : sl; + referenceChannel : slv(7 downto 0); + channelMask : slv(CHANNELS_G-1 downto 0); + fcoMask : slv(FCO_LANES_G-1 downto 0); + dataMask : slv(SAMPLE_WIDTH_G-1 downto 0); + patternA : slv(SAMPLE_WIDTH_G-1 downto 0); + patternB : slv(SAMPLE_WIDTH_G-1 downto 0); + requestedSamples : slv(31 downto 0); + noValidTimeout : slv(31 downto 0); + noValidCount : slv(31 downto 0); + completionSeq : slv(31 downto 0); + checkedSamples : slv(31 downto 0); + channelPassed : slv(CHANNELS_G-1 downto 0); + fcoPassed : slv(FCO_LANES_G-1 downto 0); + fcoSeen : slv(FCO_LANES_G-1 downto 0); + wordErrorCount : Slv32Array(CHANNELS_G-1 downto 0); + bitErrorMask : Slv16Array(CHANNELS_G-1 downto 0); + fcoErrorCount : Slv32Array(FCO_LANES_G-1 downto 0); + axilReadSlave : AxiLiteReadSlaveType; + axilWriteSlave : AxiLiteWriteSlaveType; + end record RegType; + + constant REG_INIT_C : RegType := ( + start => '0', + abort => '0', + cfgAlternating => '0', + cfgReference => (others => '0'), + cfgChannelMask => (others => '1'), + cfgFcoMask => (others => '1'), + cfgDataMask => (others => '1'), + cfgPatternA => (others => '0'), + cfgPatternB => (others => '1'), + cfgSamples => x"00000100", + cfgTimeout => x"00000100", + busy => '0', + done => '0', + timedOut => '0', + configError => '0', + aborted => '0', + phaseAcquired => '0', + expectedPhase => '0', + allChannelsPass => '0', + allFcoPass => '0', + alternating => '0', + referenceChannel => (others => '0'), + channelMask => (others => '0'), + fcoMask => (others => '0'), + dataMask => (others => '0'), + patternA => (others => '0'), + patternB => (others => '0'), + requestedSamples => (others => '0'), + noValidTimeout => (others => '0'), + noValidCount => (others => '0'), + completionSeq => (others => '0'), + checkedSamples => (others => '0'), + channelPassed => (others => '0'), + fcoPassed => (others => '0'), + fcoSeen => (others => '0'), + wordErrorCount => (others => (others => '0')), + bitErrorMask => (others => (others => '0')), + fcoErrorCount => (others => (others => '0')), + axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, + axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + assert FRAME_PATTERN_G'length = SERIALIZATION_FACTOR_G + report "FRAME_PATTERN_G length must equal SERIALIZATION_FACTOR_G" + severity failure; + + comb : process (axilReadMaster, axilWriteMaster, fcoValid, fcoWord, r, rst, + sampleIn, sampleValid) is + variable v : RegType; + variable ep : AxiLiteEndpointType; + variable actual : slv(SAMPLE_WIDTH_G-1 downto 0); + variable expected : slv(SAMPLE_WIDTH_G-1 downto 0); + variable errorBits : slv(SAMPLE_WIDTH_G-1 downto 0); + variable referenceIndex : natural range 0 to 255; + variable selectedPhase : sl; + variable phaseValid : sl; + variable terminal : sl; + variable invalidConfig : sl; + variable channelsPassing : sl; + variable fcoPassing : sl; + begin + v := r; + v.start := '0'; + v.abort := '0'; + v.done := '0'; + terminal := '0'; + + if (r.busy = '1') then + for i in FCO_LANES_G-1 downto 0 loop + if (r.fcoMask(i) = '1' and fcoValid(i) = '1') then + v.fcoSeen(i) := '1'; + if (fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0) /= FRAME_PATTERN_G) then + if (r.fcoErrorCount(i) /= x"FFFFFFFF") then + v.fcoErrorCount(i) := std_logic_vector(unsigned(r.fcoErrorCount(i)) + 1); + end if; + end if; + end if; + end loop; + + if (r.abort = '1') then + v.busy := '0'; + v.done := '1'; + v.aborted := '1'; + elsif (sampleValid = '1') then + v.noValidCount := (others => '0'); + phaseValid := '1'; + selectedPhase := '0'; + + if (r.alternating = '1') then + if (r.phaseAcquired = '1') then + selectedPhase := r.expectedPhase; + else + referenceIndex := to_integer(unsigned(r.referenceChannel)); + actual := sampleIn(referenceIndex)(SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; + if (actual = (r.patternA and r.dataMask)) then + selectedPhase := '0'; + v.phaseAcquired := '1'; + v.expectedPhase := '1'; + elsif (actual = (r.patternB and r.dataMask)) then + selectedPhase := '1'; + v.phaseAcquired := '1'; + v.expectedPhase := '0'; + else + phaseValid := '0'; + end if; + end if; + end if; + + for i in CHANNELS_G-1 downto 0 loop + if (r.channelMask(i) = '1') then + if (phaseValid = '0') then + errorBits := r.dataMask; + else + if (selectedPhase = '0') then + expected := r.patternA and r.dataMask; + else + expected := r.patternB and r.dataMask; + end if; + actual := sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; + errorBits := actual xor expected; + end if; + if (uOr(errorBits) = '1') then + if (r.wordErrorCount(i) /= x"FFFFFFFF") then + v.wordErrorCount(i) := std_logic_vector(unsigned(r.wordErrorCount(i)) + 1); + end if; + v.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) := + r.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) or errorBits; + end if; + end if; + end loop; + + if (r.alternating = '1' and r.phaseAcquired = '1') then + v.expectedPhase := not r.expectedPhase; + end if; + v.checkedSamples := std_logic_vector(unsigned(r.checkedSamples) + 1); + if (unsigned(r.checkedSamples) + 1 >= unsigned(r.requestedSamples)) then + terminal := '1'; + end if; + elsif (r.noValidTimeout /= x"00000000") then + if (unsigned(r.noValidCount) + 1 >= unsigned(r.noValidTimeout)) then + v.busy := '0'; + v.done := '1'; + v.timedOut := '1'; + else + v.noValidCount := std_logic_vector(unsigned(r.noValidCount) + 1); + end if; + end if; + + if (terminal = '1') then + v.busy := '0'; + v.done := '1'; + channelsPassing := '1'; + for i in CHANNELS_G-1 downto 0 loop + if (r.channelMask(i) = '1') then + if (v.wordErrorCount(i) = x"00000000") then + v.channelPassed(i) := '1'; + else + v.channelPassed(i) := '0'; + channelsPassing := '0'; + end if; + end if; + end loop; + fcoPassing := '1'; + for i in FCO_LANES_G-1 downto 0 loop + if (r.fcoMask(i) = '1') then + if (v.fcoSeen(i) = '1' and v.fcoErrorCount(i) = x"00000000") then + v.fcoPassed(i) := '1'; + else + v.fcoPassed(i) := '0'; + fcoPassing := '0'; + end if; + end if; + end loop; + v.allChannelsPass := channelsPassing; + v.allFcoPass := fcoPassing; + end if; + + elsif (r.start = '1') then + v.busy := '0'; + v.timedOut := '0'; + v.configError := '0'; + v.aborted := '0'; + v.phaseAcquired := '0'; + v.expectedPhase := '0'; + v.allChannelsPass := '0'; + v.allFcoPass := '0'; + v.alternating := r.cfgAlternating; + v.referenceChannel := r.cfgReference; + v.channelMask := r.cfgChannelMask; + v.fcoMask := r.cfgFcoMask; + v.dataMask := r.cfgDataMask; + v.patternA := r.cfgPatternA; + v.patternB := r.cfgPatternB; + v.requestedSamples := r.cfgSamples; + v.noValidTimeout := r.cfgTimeout; + v.noValidCount := (others => '0'); + v.checkedSamples := (others => '0'); + v.channelPassed := (others => '0'); + v.fcoPassed := (others => '0'); + v.fcoSeen := (others => '0'); + v.wordErrorCount := (others => (others => '0')); + v.bitErrorMask := (others => (others => '0')); + v.fcoErrorCount := (others => (others => '0')); + invalidConfig := '0'; + if (r.cfgSamples = x"00000000" or uOr(r.cfgChannelMask) = '0' or + uOr(r.cfgDataMask) = '0') then + invalidConfig := '1'; + end if; + if (r.cfgAlternating = '1') then + referenceIndex := to_integer(unsigned(r.cfgReference)); + if (referenceIndex >= CHANNELS_G) then + invalidConfig := '1'; + elsif (r.cfgChannelMask(referenceIndex) = '0') then + invalidConfig := '1'; + end if; + if ((r.cfgPatternA and r.cfgDataMask) = (r.cfgPatternB and r.cfgDataMask)) then + invalidConfig := '1'; + end if; + end if; + if (invalidConfig = '1') then + v.done := '1'; + v.configError := '1'; + else + v.busy := '1'; + v.phaseAcquired := not r.cfgAlternating; + end if; + end if; + + if (v.done = '1') then + v.completionSeq := std_logic_vector(unsigned(r.completionSeq) + 1); + end if; + + axiSlaveWaitTxn(ep, axilWriteMaster, axilReadMaster, + v.axilWriteSlave, v.axilReadSlave); + axiSlaveRegister(ep, ADC_DDR_PATTERN_START_ADDR_C, 0, v.start); + axiSlaveRegister(ep, ADC_DDR_PATTERN_ABORT_ADDR_C, 0, v.abort); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, + ADC_DDR_PATTERN_ALTERNATING_BIT_C, v.cfgAlternating); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, + ADC_DDR_PATTERN_REFERENCE_OFFSET_C, v.cfgReference); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CHANNEL_MASK_ADDR_C, 0, v.cfgChannelMask); + axiSlaveRegister(ep, ADC_DDR_PATTERN_FCO_MASK_ADDR_C, 0, v.cfgFcoMask); + axiSlaveRegister(ep, ADC_DDR_PATTERN_DATA_MASK_ADDR_C, 0, v.cfgDataMask); + axiSlaveRegister(ep, ADC_DDR_PATTERN_A_ADDR_C, 0, v.cfgPatternA); + axiSlaveRegister(ep, ADC_DDR_PATTERN_B_ADDR_C, 0, v.cfgPatternB); + axiSlaveRegister(ep, ADC_DDR_PATTERN_SAMPLES_ADDR_C, 0, v.cfgSamples); + axiSlaveRegister(ep, ADC_DDR_PATTERN_TIMEOUT_ADDR_C, 0, v.cfgTimeout); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_BUSY_BIT_C, r.busy); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_TIMEOUT_BIT_C, r.timedOut); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_CONFIG_ERROR_BIT_C, r.configError); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_ABORTED_BIT_C, r.aborted); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_PHASE_ACQUIRED_BIT_C, r.phaseAcquired); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_CHANNEL_PASS_BIT_C, r.allChannelsPass); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_FCO_PASS_BIT_C, r.allFcoPass); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_SEQUENCE_ADDR_C, 0, r.completionSeq); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_CHECKED_ADDR_C, 0, r.checkedSamples); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_CHANNEL_PASS_ADDR_C, 0, r.channelPassed); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_FCO_PASS_ADDR_C, 0, r.fcoPassed); + for i in CHANNELS_G-1 downto 0 loop + axiSlaveRegisterR(ep, std_logic_vector(unsigned(ADC_DDR_PATTERN_WORD_ERROR_ADDR_C)+(4*i)), + 0, r.wordErrorCount(i)); + axiSlaveRegisterR(ep, std_logic_vector(unsigned(ADC_DDR_PATTERN_BIT_ERROR_ADDR_C)+(4*i)), + 0, r.bitErrorMask(i)); + end loop; + for i in FCO_LANES_G-1 downto 0 loop + axiSlaveRegisterR(ep, std_logic_vector(unsigned(ADC_DDR_PATTERN_FCO_ERROR_ADDR_C)+(4*i)), + 0, r.fcoErrorCount(i)); + end loop; + axiSlaveDefault(ep, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); + + if (rst = '1') then + v := REG_INIT_C; + end if; + + rin <= v; + + axilReadSlave <= r.axilReadSlave; + axilWriteSlave <= r.axilWriteSlave; + end process comb; + + seq : process (clk) is + begin + if rising_edge(clk) then + r <= rin after TPD_G; + end if; + end process seq; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd new file mode 100644 index 0000000000..39bb15dbc9 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd @@ -0,0 +1,191 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Device-family selector for the serialized DDR ADC PHY +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrPhy is + generic ( + TPD_G : time := 1 ns; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + SERIALIZATION_FACTOR_G : positive := 14; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); + port ( + adcClkRst : in sl; + idelayCtrlRdy : in sl := '1'; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPhy; + +architecture rtl of AdcDdrPhy is + + component AdcDdrPhy7Series is + generic ( + TPD_G : time; + DATA_LANES_G : positive; + FCO_LANES_G : positive; + SERIALIZATION_FACTOR_G : positive; + IODELAY_GROUP_G : string; + IDELAYCTRL_FREQ_G : real; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0)); + port ( + adcClkRst : in sl; + idelayCtrlRdy : in sl; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); + end component AdcDdrPhy7Series; + + component AdcDdrPhyUltraScale is + generic ( + TPD_G : time; + DEVICE_FAMILY_G : string; + DATA_LANES_G : positive; + FCO_LANES_G : positive; + SERIALIZATION_FACTOR_G : positive; + IODELAY_GROUP_G : string; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0)); + port ( + adcClkRst : in sl; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); + end component AdcDdrPhyUltraScale; + + constant ULTRASCALE_C : boolean := + DEVICE_FAMILY_G = "ULTRASCALE" or DEVICE_FAMILY_G = "ULTRASCALE_PLUS"; + +begin + + assert DEVICE_FAMILY_G = "7SERIES" or ULTRASCALE_C + report "AdcDdrPhy DEVICE_FAMILY_G must be 7SERIES, ULTRASCALE, or ULTRASCALE_PLUS" + severity failure; + + GEN_7SERIES : if DEVICE_FAMILY_G = "7SERIES" generate + U_Phy : AdcDdrPhy7Series + generic map ( + TPD_G => TPD_G, + DATA_LANES_G => DATA_LANES_G, + FCO_LANES_G => FCO_LANES_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_FCO_MAP_G => DATA_FCO_MAP_G) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => dClkP, -- [in] + dClkN => dClkN, -- [in] + fcoP => fcoP, -- [in] + fcoN => fcoN, -- [in] + dataP => dataP, -- [in] + dataN => dataN, -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelayWrite, -- [in] + fcoDelayWrite => fcoDelayWrite, -- [in] + captureClk => captureClk, -- [out] + captureRst => captureRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + end generate GEN_7SERIES; + + GEN_ULTRASCALE : if ULTRASCALE_C generate + U_Phy : AdcDdrPhyUltraScale + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => DATA_LANES_G, + FCO_LANES_G => FCO_LANES_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + DATA_FCO_MAP_G => DATA_FCO_MAP_G) + port map ( + adcClkRst => adcClkRst, -- [in] + phyReset => phyReset, -- [in] + dClkP => dClkP, -- [in] + dClkN => dClkN, -- [in] + fcoP => fcoP, -- [in] + fcoN => fcoN, -- [in] + dataP => dataP, -- [in] + dataN => dataN, -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelayWrite, -- [in] + fcoDelayWrite => fcoDelayWrite, -- [in] + captureClk => captureClk, -- [out] + captureRst => captureRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + end generate GEN_ULTRASCALE; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd new file mode 100644 index 0000000000..d592d1bf52 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd @@ -0,0 +1,123 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Common constants and types for serialized DDR ADC receivers +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +package AdcDdrPkg is + + -- Register-map version and fixed alignment policy. Alignment thresholds are + -- shared because all supported devices expose a deterministic repeating FCO word. + constant ADC_DDR_VERSION_C : slv(31 downto 0) := x"00010000"; + constant ADC_DDR_LOCK_MATCHES_C : positive := 4; + constant ADC_DDR_UNLOCK_ERRORS_C : positive := 2; + -- ISERDESE2 DDR operation requires three quiet CLKDIV cycles after a + -- one-cycle BITSLIP request before the shifted word is evaluated. + constant ADC_DDR_BITSLIP_INTERVAL_C : positive := 8; + + -- Return the native programmable input-delay width for an FPGA family. + function adcDdrDelayBits (deviceFamily : string) return positive; + + -- Base offsets for the normalized register windows. Per-lane and per-channel + -- entries use four-byte strides from these bases. + constant ADC_DDR_VERSION_ADDR_C : slv(11 downto 0) := X"000"; + constant ADC_DDR_CAPABILITIES0_ADDR_C : slv(11 downto 0) := X"004"; + constant ADC_DDR_CAPABILITIES1_ADDR_C : slv(11 downto 0) := X"008"; + constant ADC_DDR_CAPTURE_RESET_ADDR_C : slv(11 downto 0) := X"00C"; + constant ADC_DDR_RELOCK_ADDR_C : slv(11 downto 0) := X"010"; + constant ADC_DDR_SNAPSHOT_ADDR_C : slv(11 downto 0) := X"014"; + constant ADC_DDR_CLEAR_COUNTERS_ADDR_C : slv(11 downto 0) := X"018"; + constant ADC_DDR_STATUS_ADDR_C : slv(11 downto 0) := X"01C"; + constant ADC_DDR_LOCKED_MASK_ADDR_C : slv(11 downto 0) := X"020"; + constant ADC_DDR_SNAPSHOT_SEQUENCE_ADDR_C : slv(11 downto 0) := X"024"; + constant ADC_DDR_DATA_DELAY_ADDR_C : slv(11 downto 0) := X"100"; + constant ADC_DDR_FCO_DELAY_ADDR_C : slv(11 downto 0) := X"200"; + constant ADC_DDR_FCO_WORD_ADDR_C : slv(11 downto 0) := X"300"; + constant ADC_DDR_LOST_LOCK_COUNT_ADDR_C : slv(11 downto 0) := X"340"; + constant ADC_DDR_PATTERN_BASE_ADDR_C : slv(11 downto 0) := X"800"; + constant ADC_DDR_PATTERN_START_ADDR_C : slv(7 downto 0) := X"00"; + constant ADC_DDR_PATTERN_ABORT_ADDR_C : slv(7 downto 0) := X"04"; + constant ADC_DDR_PATTERN_CONFIG_ADDR_C : slv(7 downto 0) := X"08"; + constant ADC_DDR_PATTERN_CHANNEL_MASK_ADDR_C : slv(7 downto 0) := X"0C"; + constant ADC_DDR_PATTERN_FCO_MASK_ADDR_C : slv(7 downto 0) := X"10"; + constant ADC_DDR_PATTERN_DATA_MASK_ADDR_C : slv(7 downto 0) := X"14"; + constant ADC_DDR_PATTERN_A_ADDR_C : slv(7 downto 0) := X"18"; + constant ADC_DDR_PATTERN_B_ADDR_C : slv(7 downto 0) := X"1C"; + constant ADC_DDR_PATTERN_SAMPLES_ADDR_C : slv(7 downto 0) := X"20"; + constant ADC_DDR_PATTERN_TIMEOUT_ADDR_C : slv(7 downto 0) := X"24"; + constant ADC_DDR_PATTERN_STATUS_ADDR_C : slv(7 downto 0) := X"28"; + constant ADC_DDR_PATTERN_SEQUENCE_ADDR_C : slv(7 downto 0) := X"2C"; + constant ADC_DDR_PATTERN_CHECKED_ADDR_C : slv(7 downto 0) := X"30"; + constant ADC_DDR_PATTERN_CHANNEL_PASS_ADDR_C : slv(7 downto 0) := X"34"; + constant ADC_DDR_PATTERN_FCO_PASS_ADDR_C : slv(7 downto 0) := X"38"; + constant ADC_DDR_PATTERN_WORD_ERROR_ADDR_C : slv(7 downto 0) := X"40"; + constant ADC_DDR_PATTERN_BIT_ERROR_ADDR_C : slv(7 downto 0) := X"80"; + constant ADC_DDR_PATTERN_FCO_ERROR_ADDR_C : slv(7 downto 0) := X"C0"; + constant ADC_DDR_OVERFLOW_COUNT_ADDR_C : slv(11 downto 0) := X"500"; + constant ADC_DDR_DEBUG_ADDR_C : slv(11 downto 0) := X"600"; + + -- Capture capability feature-bit assignments. + constant ADC_DDR_CAP_PATTERN_CHECK_BIT_C : natural := 16; + + -- Status register bit assignments. + constant ADC_DDR_STATUS_DELAY_READY_BIT_C : natural := 1; + constant ADC_DDR_STATUS_ALL_LOCKED_BIT_C : natural := 2; + constant ADC_DDR_STATUS_ANY_OVERFLOW_BIT_C : natural := 3; + + -- Pattern measurement command and status bit assignments. + constant ADC_DDR_PATTERN_ALTERNATING_BIT_C : natural := 0; + constant ADC_DDR_PATTERN_REFERENCE_OFFSET_C : natural := 8; + constant ADC_DDR_PATTERN_BUSY_BIT_C : natural := 0; + constant ADC_DDR_PATTERN_TIMEOUT_BIT_C : natural := 1; + constant ADC_DDR_PATTERN_CONFIG_ERROR_BIT_C : natural := 2; + constant ADC_DDR_PATTERN_ABORTED_BIT_C : natural := 3; + constant ADC_DDR_PATTERN_PHASE_ACQUIRED_BIT_C : natural := 4; + constant ADC_DDR_PATTERN_CHANNEL_PASS_BIT_C : natural := 5; + constant ADC_DDR_PATTERN_FCO_PASS_BIT_C : natural := 6; + + -- Runtime PHY delay command. The value width matches the widest supported + -- Xilinx input delay; load is a one-cycle capture-domain write strobe. + type AdcDdrDelayType is record + value : slv(8 downto 0); + load : sl; + end record AdcDdrDelayType; + + constant ADC_DDR_DELAY_INIT_C : AdcDdrDelayType := ( + value => (others => '0'), + load => '0'); + + type AdcDdrDelayArray is array (natural range <>) of AdcDdrDelayType; + +end package AdcDdrPkg; + +package body AdcDdrPkg is + + function adcDdrDelayBits (deviceFamily : string) return positive is + begin + if deviceFamily = "7SERIES" then + return 5; + elsif deviceFamily = "ULTRASCALE" or deviceFamily = "ULTRASCALE_PLUS" then + return 9; + else + assert false + report "Unsupported AdcDdr FPGA device family " & deviceFamily + severity failure; + return 5; + end if; + end function adcDdrDelayBits; + +end package body AdcDdrPkg; diff --git a/devices/AnalogDevices/adcDdr/ruckus.tcl b/devices/AnalogDevices/adcDdr/ruckus.tcl new file mode 100644 index 0000000000..7678ba1f07 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/ruckus.tcl @@ -0,0 +1,34 @@ +#----------------------------------------------------------------------------- +# This file is part of 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/rtl" -fileType "VHDL 2008" +loadSource -lib surf -sim_only -dir "$::DIR_PATH/sim" + +set family [getFpgaArch] + +if { ${family} eq {artix7} || + ${family} eq {kintex7} || + ${family} eq {virtex7} || + ${family} eq {zynq} } { + loadRuckusTcl "$::DIR_PATH/7Series" +} + +if { ${family} eq {kintexu} || + ${family} eq {virtexu} || + ${family} eq {kintexuplus} || + ${family} eq {zynquplus} || + ${family} eq {zynquplusRFSOC} || + ${family} eq {qzynquplusRFSOC} || + ${family} eq {virtexuplus} || + ${family} eq {virtexuplusHBM} } { + loadRuckusTcl "$::DIR_PATH/UltraScale" +} diff --git a/devices/AnalogDevices/ruckus.tcl b/devices/AnalogDevices/ruckus.tcl index d4d6e53eee..e13dfead08 100644 --- a/devices/AnalogDevices/ruckus.tcl +++ b/devices/AnalogDevices/ruckus.tcl @@ -6,10 +6,12 @@ loadRuckusTcl "$::DIR_PATH/ad5541" loadRuckusTcl "$::DIR_PATH/ad5780" loadRuckusTcl "$::DIR_PATH/general" loadRuckusTcl "$::DIR_PATH/ltm4664" +loadRuckusTcl "$::DIR_PATH/adcDdr" # Check for non-zero Vivado version (in-case non-Vivado project) if { $::env(VIVADO_VERSION) > 0.0} { loadRuckusTcl "$::DIR_PATH/ad9467" + loadRuckusTcl "$::DIR_PATH/ad9252" loadRuckusTcl "$::DIR_PATH/ad9249" loadRuckusTcl "$::DIR_PATH/ad9681" From cba39bca0bf80713b25fe6e72e1c33dc391e2d9d Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 5 Aug 2026 14:53:41 -0700 Subject: [PATCH 02/57] python: add ADC DDR models and calibration --- python/surf/devices/analog_devices/_Ad9249.py | 689 ++++----- .../devices/analog_devices/_Ad9249Legacy.py | 284 ++++ python/surf/devices/analog_devices/_Ad9252.py | 393 +++++ python/surf/devices/analog_devices/_Ad9681.py | 515 ++----- python/surf/devices/analog_devices/_AdcDdr.py | 472 ++++++ .../analog_devices/_AdcDdrCalibration.py | 1288 +++++++++++++++++ .../analog_devices/_AdcDdrPatternTester.py | 288 ++++ .../surf/devices/analog_devices/__init__.py | 19 +- setup.py | 2 +- 9 files changed, 3139 insertions(+), 811 deletions(-) create mode 100644 python/surf/devices/analog_devices/_Ad9249Legacy.py create mode 100644 python/surf/devices/analog_devices/_Ad9252.py create mode 100644 python/surf/devices/analog_devices/_AdcDdr.py create mode 100644 python/surf/devices/analog_devices/_AdcDdrCalibration.py create mode 100644 python/surf/devices/analog_devices/_AdcDdrPatternTester.py diff --git a/python/surf/devices/analog_devices/_Ad9249.py b/python/surf/devices/analog_devices/_Ad9249.py index 67bffc451e..7b265a79b9 100644 --- a/python/surf/devices/analog_devices/_Ad9249.py +++ b/python/surf/devices/analog_devices/_Ad9249.py @@ -1,8 +1,8 @@ #----------------------------------------------------------------------------- -# Title : PyRogue _ad9249 Module +# Title : PyRogue AD9249 model #----------------------------------------------------------------------------- # Description: -# PyRogue _ad9249 Module +# AD9249 configuration, readout, and calibration models. #----------------------------------------------------------------------------- # This file is part of 'SLAC Firmware Standard Library'. # It is subject to the license terms in the LICENSE.txt file found in the @@ -13,14 +13,31 @@ # the terms contained in the LICENSE.txt file. #----------------------------------------------------------------------------- -import pyrogue as pr -import rogue.interfaces.memory as rim +from __future__ import annotations + import math +from typing import Any + +import pyrogue as pr + +import surf.devices.analog_devices as analog_devices class Ad9249ConfigGroup(pr.Device): - def __init__(self, - description = 'Configure one side of an AD9249 ADC', - **kwargs): + """Configuration registers for one eight-channel AD9249 bank. + + Parameters + ---------- + description : str, optional + PyRogue device description. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + description: str = 'Configure one side of an AD9249 ADC', + **kwargs: Any) -> None: + """Create one AD9249 bank configuration model.""" super().__init__(description=description, **kwargs) @@ -180,6 +197,26 @@ def __init__(self, }, )) + self.add(pr.RemoteVariable( + name = 'ResetPNShort', + description = 'Reset the PN9 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 4, + base = pr.Bool, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'ResetPNLong', + description = 'Reset the PN23 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 5, + base = pr.Bool, + mode = 'RW', + )) + self.add(pr.RemoteVariable( name = 'OffsetAdjust', description = 'Output offset adjustment in LSB steps', @@ -250,21 +287,100 @@ def __init__(self, base = pr.Bool, )) + self.add(pr.RemoteCommand( + name = 'DeviceUpdate', + description = 'Transfers the resolution/sample-rate override into the ADC', + offset = (0xFF*4), + function = pr.BaseCommand.touchOne, + )) + + self.add(pr.RemoteVariable( + name = 'ResolutionSampleRateOverride', + description = 'Enables the resolution and maximum sample-rate override', + offset = (0x100*4), + bitSize = 1, + bitOffset = 6, + base = pr.Bool, + )) + + self.add(pr.RemoteVariable( + name = 'Resolution', + description = 'Selects ADC resolution when the override is enabled', + offset = (0x100*4), + bitSize = 2, + bitOffset = 4, + enum = { + 0b01: '14 bits', + 0b10: '12 bits', + }, + )) + + self.add(pr.RemoteVariable( + name = 'SampleRate', + description = 'Selects maximum ADC sample rate when the override is enabled', + offset = (0x100*4), + bitSize = 3, + bitOffset = 0, + enum = { + 0b000: '20 MSPS', + 0b001: '40 MSPS', + 0b010: '50 MSPS', + 0b011: '65 MSPS', + }, + )) + + def writeBlocks(self, **kwargs: Any) -> None: + """Write pending blocks and transfer them into the ADC.""" + super().writeBlocks(**kwargs) + self.DeviceUpdate() + class Ad9249ChipConfig(pr.Device): - def __init__(self, - name = 'Ad9249ChipConfig', - description = 'Configure one side of an AD9249 ADC', - **kwargs): + """Configuration model containing both banks of one AD9249. + + Parameters + ---------- + name : str, optional + PyRogue device name. + description : str, optional + PyRogue device description. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + name: str = 'Ad9249ChipConfig', + description: str = 'Configure one side of an AD9249 ADC', + **kwargs: Any) -> None: + """Create the two-bank configuration model.""" + super().__init__(name=name, description=description, **kwargs) - self.add(Ad9249ConfigGroup('BankConfig[0]', 0x0000)) - self.add(Ad9249ConfigGroup('BankConfig[1]', 0x0200)) + self.add(Ad9249ConfigGroup(name='BankConfig[0]', offset=0x0000)) + self.add(Ad9249ConfigGroup(name='BankConfig[1]', offset=0x0200)) class Ad9249Config(pr.Device): - def __init__(self, - name = 'Ad9249Config', - description = 'Configuration of Ad9249 AD', - chips = 1, - **kwargs): + """Configuration model for one or more AD9249 devices. + + Parameters + ---------- + name : str, optional + PyRogue device name. + description : str, optional + PyRogue device description. + chips : int, optional + Number of AD9249 devices represented by the firmware register map. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + name: str = 'Ad9249Config', + description: str = 'Configuration of Ad9249 ADC', + chips: int = 1, + **kwargs: Any) -> None: + """Create the AD9249 configuration model.""" + super().__init__(name=name, description=description, **kwargs) PDWN_ADDR = int(pow(2,11+math.log(chips*2,2))) @@ -296,415 +412,128 @@ def __init__(self, self.add(Ad9249ConfigGroup(name=f'Ad9249ChipBankConfig0[{i}]', offset=i*0x1000)) self.add(Ad9249ConfigGroup(name=f'Ad9249ChipBankConfig1[{i}]', offset=i*0x1000+0x0800)) -class Ad9249ReadoutGroup(pr.Device): - def __init__(self, - name = 'Ad9249ReadoutGroup', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 10 - else: - delayBits = 6 - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'ChannelDelay[{i}]', - description = f'IDELAY value for serial channel {i}', - offset = i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - self.add(pr.RemoteVariable( - name = 'FrameDelay', - description = 'IDELAY value for FCO', - offset = 0x20, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - self.add(pr.RemoteVariable( - name = 'LostLockCount', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x30, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Locked', - description = 'Readout has locked on to the frame boundary', - offset = 0x30, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'AdcFrame', - description = 'Last deserialized FCO value for debug', - offset = 0x34, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x40, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:_x}', - mode = 'RO', - )) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x38, - bitSize = 1, - bitOffset = 0, - )) - - self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - - @staticmethod - def setDelay(var, value, write): - iValue = value + 512 - var.dependencies[0].set(iValue, write) - var.dependencies[0].set(value, write) - - @staticmethod - def getDelay(var, read): - return var.dependencies[0].get(read=read) - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads - """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - freeze = isinstance(variable, list) and any(v.name.startswith('AdcChannel') for v in variable) - if freeze: - self.FreezeDebug(1) - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - if freeze: - self.FreezeDebug(0) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) - - -class Ad9249ReadoutGroup2(pr.Device): - def __init__(self, - name = 'Ad9249Readout', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 9 - else: - delayBits = 6 - - - self.add(pr.RemoteVariable( - name = 'Delay', - description = 'IDELAY value', - offset = 0x00, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - groups = ['NoConfig'], - )) - - self.add(pr.RemoteCommand( - name='Relock', - description='Triggers ADC readout relock sequence', - hidden=False, - offset=0x20, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.toggle)) - - self.add(pr.RemoteVariable( - name = 'ErrorDetCount', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x30, - disp = '{:d}', - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'LostLockCount', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x50, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Locked', - description = 'Readout has locked on to the frame boundary', - offset = 0x50, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'AdcFrameSync', - description = 'Last deserialized FCO value for debug', - offset = 0x58, - bitSize = 14, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x60, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:09_x}', - mode = 'RO', - )) - - for i in range(channels): - self.add(pr.LinkVariable( - name = f'AdcVoltage[{i}]', - description = f'Converted voltage for ADC channel {i}', - mode = 'RO', - disp = '{:1.9f}', - variable = self.AdcChannel[i], - linkedGet = lambda read, check, r=self.AdcChannel[i]: 2*pr.twosComplement(r.get(read=read, check=check)>>18, 14)/2**14, - units = 'V')) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x5C, - bitSize = 1, - bitOffset = 0, - )) - - self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads - """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) - - -class AdcTester(pr.Device): - def __init__(self, description='ADC Pattern Tester Registers', **kwargs): - """Create AdcTester""" - super().__init__(description=description, **kwargs) - - # Creation. memBase is either the register bus server (srp, rce mapped memory, etc) or the device which - # contains this object. In most cases the parent and memBase are the same but they can be - # different in more complex bus structures. They will also be different for the top most node. - # The setMemBase call can be used to update the memBase for this Device. All sub-devices and local - # blocks will be updated. - - ############################################# - # Create block / variable combinations - ############################################# - - - #Setup registers & variables - self.add(pr.RemoteVariable( - name = 'TestChannel', - description = 'Test Channel Select', - offset = 0x00000000, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestDataMask', - description = 'Test Data Mask', - offset = 0x00000004, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestPattern', - description = 'Test Pattern', - offset = 0x00000008, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestSamples', - description = 'Test Samples Number', - offset = 0x0000000C, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestTimeout', - description = 'Test Timeout', - offset = 0x00000010, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestRequest', - description = 'Test Request', - offset = 0x00000014, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestPassed', - description = 'Test Passed Flag', - offset = 0x00000018, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'TestFailed', - description = 'Test Failed Flag', - offset = 0x0000001C, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RO', - )) - - ##################################### - # Create commands - ##################################### - - # A command has an associated function. The function can be a series of - # python commands in a string. Function calls are executed in the command scope - # the passed arg is available as 'arg'. Use 'dev' to get to device scope. - # A command can also be a call to a local function with local scope. - # The command object and the arg are passed - - @staticmethod - def frequencyConverter(self): - def func(dev, var): - return '{:.3f} kHz'.format(1/(self.clkPeriod * self._count(var.dependencies)) * 1e-3) - return func +class Ad9249ReadoutBank(analog_devices.AdcDdr): + """Normalized register model for one eight-channel AD9249 output bank. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + **kwargs : Any + Additional arguments forwarded to ``AdcDdr``. + """ + + def __init__( + self, + *, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + **kwargs: Any) -> None: + """Create one normalized AD9249 bank readout.""" + + delayBits = analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'One normalized eight-channel AD9249 output bank') + super().__init__( + dataLanes = 8, + fcoLanes = 1, + channels = 8, + sampleBits = 14, + serializationFactor = 14, + delayBits = delayBits, + **kwargs) + + +class Ad9249ReadoutBankCalibration(analog_devices.AdcDdrCalibration): + """Calibration process for one normalized AD9249 output bank. + + Parameters + ---------- + config : Ad9249ConfigGroup + Configuration device for the corresponding ADC bank. + readout : Ad9249ReadoutBank + Normalized readout to calibrate. + **kwargs : Any + Additional arguments forwarded to ``AdcDdrCalibration``. + """ + + def __init__( + self, + *, + config: Ad9249ConfigGroup, + readout: Ad9249ReadoutBank, + **kwargs: Any) -> None: + """Create one AD9249 bank calibration process.""" + + if not isinstance(config, Ad9249ConfigGroup): + raise TypeError('config must be an Ad9249ConfigGroup') + if not isinstance(readout, Ad9249ReadoutBank): + raise TypeError('readout must be an Ad9249ReadoutBank') + super().__init__( + config = config, + readout = readout, + dataLaneToChannel = tuple(range(8)), + pn23Reset = config.ResetPNLong, + **kwargs) + + +class Ad9249Readout(pr.Device): + """Full AD9249 readout containing two independent normalized banks. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + *, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + **kwargs: Any) -> None: + """Create the complete normalized AD9249 readout.""" + + analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'Complete normalized 16-channel AD9249 readout') + super().__init__(**kwargs) + + for i in range(2): + self.add(Ad9249ReadoutBank( + name = f'Bank[{i}]', + offset = 0x1000*i, + deviceFamily = deviceFamily)) + + +class Ad9249ReadoutCalibration(pr.Device): + """Container for both AD9249 bank calibration processes. + + Parameters + ---------- + config : Ad9249Config + Configuration device containing both ADC banks. + readout : Ad9249Readout + Complete normalized AD9249 readout. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + *, + config: Ad9249Config, + readout: Ad9249Readout, + **kwargs: Any) -> None: + """Create both AD9249 bank calibration processes.""" + + if not isinstance(config, Ad9249Config): + raise TypeError('config must be an Ad9249Config') + if not isinstance(readout, Ad9249Readout): + raise TypeError('readout must be an Ad9249Readout') + kwargs.setdefault('description', 'Calibration processes for both AD9249 output banks') + super().__init__(**kwargs) + + for bank in range(2): + self.add(Ad9249ReadoutBankCalibration( + name = f'Bank[{bank}]', + config = config.BankConfig[bank], + readout = readout.Bank[bank])) diff --git a/python/surf/devices/analog_devices/_Ad9249Legacy.py b/python/surf/devices/analog_devices/_Ad9249Legacy.py new file mode 100644 index 0000000000..83611c9a6c --- /dev/null +++ b/python/surf/devices/analog_devices/_Ad9249Legacy.py @@ -0,0 +1,284 @@ +#----------------------------------------------------------------------------- +# Title : Legacy AD9249 readout interfaces +#----------------------------------------------------------------------------- +# Description: +# PyRogue register maps retained for the legacy Ad9249ReadoutGroup RTL. +#----------------------------------------------------------------------------- +# This file is part of the 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +import pyrogue as pr +import rogue.interfaces.memory as rim + + +__all__ = [ + 'Ad9249ReadoutGroup', + 'AdcTester', +] + + +class Ad9249ReadoutGroup(pr.Device): + def __init__(self, + name = 'Ad9249ReadoutGroup', + description = 'Configure readout of 1 bank of an AD9249', + fpga = '7series', + channels = 8, + **kwargs): + assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' + super().__init__(name=name, description=description, **kwargs) + + if fpga == '7series': + delayBits = 6 + elif fpga == 'ultrascale': + delayBits = 10 + else: + delayBits = 6 + + for i in range(channels): + self.add(pr.RemoteVariable( + name = f'ChannelDelay[{i}]', + description = f'IDELAY value for serial channel {i}', + offset = i*4, + bitSize = delayBits, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + verify = False, + )) + + self.add(pr.RemoteVariable( + name = 'FrameDelay', + description = 'IDELAY value for FCO', + offset = 0x20, + bitSize = delayBits, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + verify = False, + )) + + self.add(pr.RemoteVariable( + name = 'LostLockCount', + description = 'Number of times that frame lock has been lost since reset', + offset = 0x30, + bitSize = 16, + bitOffset = 0, + base = pr.UInt, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'Locked', + description = 'Readout has locked on to the frame boundary', + offset = 0x30, + bitSize = 1, + bitOffset = 16, + base = pr.Bool, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'AdcFrame', + description = 'Last deserialized FCO value for debug', + offset = 0x34, + bitSize = 16, + bitOffset = 0, + base = pr.UInt, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'Invert', + description = 'Optional ADC data inversion (offset binary only)', + offset = 0x40, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RW', + )) + + for i in range(channels): + self.add(pr.RemoteVariable( + name = f'AdcChannel[{i:d}]', + description = f'Last deserialized channel {i:d} ADC value for debug', + offset = 0x80 + (i*4), + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + disp = '{:_x}', + mode = 'RO', + )) + + self.add(pr.RemoteCommand( + name = 'LostLockCountReset', + description = 'Reset LostLockCount', + function = pr.BaseCommand.toggle, + offset = 0x38, + bitSize = 1, + bitOffset = 0, + )) + + self.add(pr.RemoteCommand( + name='FreezeDebug', + description='Freeze all of the AdcChannel registers', + hidden=True, + offset=0xA0, + bitSize=1, + bitOffset=0, + base=pr.UInt, + function=pr.RemoteCommand.touch)) + + @staticmethod + def setDelay(var, value, write): + iValue = value + 512 + var.dependencies[0].set(iValue, write) + var.dependencies[0].set(value, write) + + @staticmethod + def getDelay(var, read): + return var.dependencies[0].get(read=read) + + def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): + """ + Perform background reads + """ + checkEach = checkEach or self.forceCheckEach + + if variable is not None: + freeze = isinstance(variable, list) and any(v.name.startswith('AdcChannel') for v in variable) + if freeze: + self.FreezeDebug(1) + pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) + if freeze: + self.FreezeDebug(0) + + else: + self.FreezeDebug(1) + for block in self._blocks: + if block.bulkOpEn: + pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) + self.FreezeDebug(0) + + if recurse: + for key,value in self.devices.items(): + value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) + + +class AdcTester(pr.Device): + def __init__(self, description='ADC Pattern Tester Registers', **kwargs): + """Create AdcTester""" + super().__init__(description=description, **kwargs) + + # Creation. memBase is either the register bus server (srp, rce mapped memory, etc) or the device which + # contains this object. In most cases the parent and memBase are the same but they can be + # different in more complex bus structures. They will also be different for the top most node. + # The setMemBase call can be used to update the memBase for this Device. All sub-devices and local + # blocks will be updated. + + ############################################# + # Create block / variable combinations + ############################################# + + + #Setup registers & variables + self.add(pr.RemoteVariable( + name = 'TestChannel', + description = 'Test Channel Select', + offset = 0x00000000, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestDataMask', + description = 'Test Data Mask', + offset = 0x00000004, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestPattern', + description = 'Test Pattern', + offset = 0x00000008, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestSamples', + description = 'Test Samples Number', + offset = 0x0000000C, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestTimeout', + description = 'Test Timeout', + offset = 0x00000010, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestRequest', + description = 'Test Request', + offset = 0x00000014, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestPassed', + description = 'Test Passed Flag', + offset = 0x00000018, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'TestFailed', + description = 'Test Failed Flag', + offset = 0x0000001C, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RO', + )) + + ##################################### + # Create commands + ##################################### + + # A command has an associated function. The function can be a series of + # python commands in a string. Function calls are executed in the command scope + # the passed arg is available as 'arg'. Use 'dev' to get to device scope. + # A command can also be a call to a local function with local scope. + # The command object and the arg are passed + + @staticmethod + def frequencyConverter(self): + def func(dev, var): + return '{:.3f} kHz'.format(1/(self.clkPeriod * self._count(var.dependencies)) * 1e-3) + return func diff --git a/python/surf/devices/analog_devices/_Ad9252.py b/python/surf/devices/analog_devices/_Ad9252.py new file mode 100644 index 0000000000..cb2a878198 --- /dev/null +++ b/python/surf/devices/analog_devices/_Ad9252.py @@ -0,0 +1,393 @@ +#----------------------------------------------------------------------------- +# This file is part of the 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any + +import pyrogue as pr + +import surf.devices.analog_devices as analog_devices + +class Ad9252Config(pr.Device): + """PyRogue configuration model for the AD9252 ADC. + + Parameters + ---------- + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__(self, **kwargs: Any) -> None: + """Create the AD9252 configuration model.""" + + super().__init__(description="AD9252 ADC object.",**kwargs) + + +# self.add(pr.RemoteVariable( +# name = "ConfigEn", +# description='Set to ''True'' to enable register writes to ADC.', +# offset=0x00, +# bitSize=1, +# bitOffset=0, +# base =pr.Bool)) + + self.add(pr.RemoteVariable( + name = "ChipId", + description='Read only chip ID value.', + offset=0x04, + bitSize=8, + bitOffset=0, + mode = "RO")) + + self.add(pr.RemoteVariable( + name="ChipGrade", + description='Read only chip grade value.', + offset=0x08, + bitSize=3, + bitOffset=4, + mode="RO")) + + + self.add(pr.RemoteVariable( + name="PowerDownMode", + description='Set power mode of device.', + offset=0x20, + bitSize=2, + bitOffset=0, + base=pr.UInt, + enum = { + 0: "Chip Run", + 1: "Full Power Down", + 2: "Standby", + 3: "Digital Reset"})) + + self.add(pr.RemoteVariable( + name = "DutyCycleStabilizer", + description='Turns on internal duty cycle stabilizer. (default=True).', + offset=0x24, + bitSize=1, + bitOffset=0, + base =pr.Bool)) + + + self.add(pr.RemoteVariable( + name="DevIndexMask[7:4]", + offset=0x10, + bitSize=4, + bitOffset=0, + base=pr.UInt, + disp='{:#b}')) + + self.add(pr.RemoteVariable( + name="DevIndexMask[3:0]", + offset=0x14, + bitSize=4, + bitOffset=0, + base=pr.UInt, + disp='{:#b}')) + + self.add(pr.RemoteVariable( + name="DevIndexMask[DCO:FCO]", + offset=0x14, + bitSize=2, + bitOffset=0x4, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="OutputTestMode", + description='Set output test mode.', + offset=0x34, + bitSize=4, + bitOffset=0, + base=pr.UInt, + enum={ + 0: "Off", + 1: "Midscale Short", + 2: "Positive FS", + 3: "Negative FS", + 4: "Alternating checkerboard", + 5: "PN23", + 6: "PN9", + 7: "1/0-word toggle", + 8: "User Input", + 9: "1/0-bit Toggle", + 10: "1x sync", + 11: "One bit high", + 12: "mixed bit frequency"})) + + self.add(pr.RemoteVariable( + name='ResetPNShort', + description='Reset PN short gen test mode', + offset=0x34, + bitSize=1, + bitOffset=4, + base=pr.Bool)) + + self.add(pr.RemoteVariable( + name='ResetPNLong', + description='Reset PN long gen test mode', + offset=0x34, + bitSize=1, + bitOffset=5, + base=pr.Bool)) + + + self.add(pr.RemoteVariable( + name='UserTestMode', + description='Sets user test mode of all channels', + offset=0x34, + bitSize=2, + bitOffset=6, + base=pr.UInt, + enum={ + 0: 'Off', + 1: 'OnSingAlternate', + 2: 'OnSingleOnce', + 3: 'OnAlternateOnce'})) + + + self.add(pr.RemoteVariable( + name='OutputFormat', + description='Set output format. binary or twos complement.', + offset=0x50, + bitSize=2, + bitOffset=0, + base=pr.UInt, + enum={ + 1: 'Twos Compliment', + 0: 'Offset Binary'})) + + self.add(pr.RemoteVariable( + name='OutputInvert', + description='Enable output inversion.', + offset=0x50, + bitSize=1, + bitOffset=2, + base=pr.Bool)) + + self.add(pr.RemoteVariable( + name='OutputMode', + description='Set output mode of device. Default=LVDS.', + offset=0x50, + bitSize=1, + bitOffset=6, + base=pr.UInt, + enum={ + 0:'LVDS ANSI-644', + 1:'LVDS Low Power'})) + + self.add(pr.RemoteVariable( + name='DcoFcoDrive2x', + description='Set DCO and FCO output drive strength.', + offset=0x54, + bitSize=1, + bitOffset=0, + base = pr.Bool)) + + + self.add(pr.RemoteVariable( + name='OutputTermDrive', + description='Set output driver termination.', + offset=0x54, + bitSize=2, + bitOffset=4, + base = pr.UInt, + enum={ + 0:'none', + 1:'200 Ohms', + 2:'100 Ohms', + 3:'100 Ohms'})) + + self.add(pr.RemoteVariable( + name='OutputPhase', + description='Set output phase adjustment.', + offset=0x58, + bitSize=4, + bitOffset=0, + base = pr.UInt, + enum={ + 0:'0 deg to edge', + 1:'60 deg to edge', + 2:'120 deg to edge', + 3:'180 deg to edge', + 4:'unused1', + 5:'300 deg to edge', + 6:'360 deg to edge', + 7:'unused2', + 8:'480 deg to edge', + 9:'540 deg to edge', + 10:'600 deg to edge', + 11:'660 deg to edge'})) + +# def convPattern(raw): +# def convert(): +# return raw.value() +# return convert + + self.add(pr.RemoteVariable( + name="UserPattern1Lsb", + offset=0x64, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="UserPattern1Msb", + offset=0x68, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="UserPattern2Lsb", + offset=0x6C, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="UserPattern2Msb", + offset=0x70, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + # self.add(pr.LinkVariable( +# name='UserPattern1', +# description='Set user test pattern 1 data.', +# linkedGet=convPattern(self.UserPattern1Raw), +# dependencies=[self.UserPattern1Raw])) + +# self.add(pr.LinkVariable( +# name='UserPattern2', +# description='Set user test pattern 2 data.', +# linkedGet=convPattern(self.UserPattern2Raw), +# dependencies=[self.UserPattern2Raw])) + + self.add(pr.RemoteVariable( + name='SerialBits', + description='Set number of serial bits.', + offset=0x84, + bitSize=3, + bitOffset=0, + base = pr.UInt, + enum={ + 0:'14 bits', + 1:'8 bits', + 2:'10 bits', + 3:'12 bits', + 4:'14 bits'})) + + self.add(pr.RemoteVariable( + name='LowEncodeRate', + description='Set low rate less than 10mbs mode.', + offset=0x84, + bitSize=1, + bitOffset=3, + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name='SerialLsbFirst', + description='Set LSB first mode of device.', + offset=0x84, + bitSize=1, + bitOffset=7, + base = pr.Bool)) + + + self.add(pr.RemoteVariable( + name='ChPowerDown', + description='Set channel power down.', + offset=0x88, + bitSize=1, + bitOffset=0, + base = pr.Bool)) + + self.add(pr.RemoteCommand( + name='DeviceUpdate', + description='Transfers buffered SPI register values into the ADC', + offset=0x3FC, + function=pr.BaseCommand.touchOne, + )) + + def writeBlocks(self, **kwargs: Any) -> None: + """Write pending blocks and transfer them into the ADC.""" + super().writeBlocks(**kwargs) + self.DeviceUpdate() + + +class Ad9252Readout(analog_devices.AdcDdr): + """Normalized AD9252 readout with one data lane per enabled channel. + + Parameters + ---------- + channels : int, optional + Number of ADC channels exposed by the firmware. + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + **kwargs : Any + Additional arguments forwarded to ``AdcDdr``. + """ + + def __init__( + self, + *, + channels: int = 8, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + **kwargs: Any) -> None: + """Create the normalized AD9252 readout.""" + + if not 1 <= channels <= 8: + raise ValueError('channels must be from 1 through 8') + delayBits = analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'AD9252 serialized DDR readout') + super().__init__( + dataLanes = channels, + fcoLanes = 1, + channels = channels, + sampleBits = 14, + serializationFactor = 14, + delayBits = delayBits, + **kwargs) + + +class Ad9252ReadoutCalibration(analog_devices.AdcDdrCalibration): + """Calibration process for a normalized AD9252 readout. + + Parameters + ---------- + config : Ad9252Config + ADC configuration device. + readout : Ad9252Readout + Normalized readout to calibrate. + **kwargs : Any + Additional arguments forwarded to ``AdcDdrCalibration``. + """ + + def __init__( + self, + *, + config: Ad9252Config, + readout: Ad9252Readout, + **kwargs: Any) -> None: + """Create the AD9252 calibration process.""" + + if not isinstance(config, Ad9252Config): + raise TypeError('config must be an Ad9252Config') + if not isinstance(readout, Ad9252Readout): + raise TypeError('readout must be an Ad9252Readout') + super().__init__( + config = config, + readout = readout, + dataLaneToChannel = tuple(range(readout._channels)), + configUpdate = config.DeviceUpdate, + pn23Reset = config.ResetPNLong, + **kwargs) diff --git a/python/surf/devices/analog_devices/_Ad9681.py b/python/surf/devices/analog_devices/_Ad9681.py index 7a0618e299..0b78996ed7 100644 --- a/python/surf/devices/analog_devices/_Ad9681.py +++ b/python/surf/devices/analog_devices/_Ad9681.py @@ -1,8 +1,8 @@ #----------------------------------------------------------------------------- -# Title : PyRogue _ad9249 Module +# Title : PyRogue AD9681 model #----------------------------------------------------------------------------- # Description: -# PyRogue _ad9249 Module +# AD9681 configuration, normalized readout, and calibration models. #----------------------------------------------------------------------------- # This file is part of 'SLAC Firmware Standard Library'. # It is subject to the license terms in the LICENSE.txt file found in the @@ -13,18 +13,34 @@ # the terms contained in the LICENSE.txt file. #----------------------------------------------------------------------------- +from __future__ import annotations + +from typing import Any + import pyrogue as pr -import rogue.interfaces.memory as rim -# import math + +import surf.devices.analog_devices as analog_devices class Ad9681Config(pr.Device): - def __init__(self, - description = 'Configure one side of an AD9249 ADC', - **kwargs): + """PyRogue configuration model for the AD9681 ADC. + + Parameters + ---------- + description : str, optional + PyRogue device description. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + description: str = 'Configure an AD9681 ADC', + **kwargs: Any) -> None: + """Create the AD9681 configuration model.""" super().__init__(description=description, **kwargs) - # AD9249 bank configuration registers + # AD9681 configuration registers self.add(pr.RemoteVariable( name = 'ChipId', description = 'ADC chip identification register', @@ -171,6 +187,26 @@ def __init__(self, }, )) + self.add(pr.RemoteVariable( + name = 'ResetPNShort', + description = 'Reset the PN9 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 4, + base = pr.Bool, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'ResetPNLong', + description = 'Reset the PN23 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 5, + base = pr.Bool, + mode = 'RW', + )) + self.add(pr.RemoteVariable( name = 'OffsetAdjust', description = 'Output offset adjustment in LSB steps', @@ -347,383 +383,116 @@ def __init__(self, }, )) - self.add(pr.RemoteCommand( - name='DeviceUpdate', - description='Transfers SPI register values to internal device shadow registers', - offset=0x3FC, - function=pr.BaseCommand.touchZero, - )) - - def writeBlocks(self, force=False, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - pr.Device.writeBlocks(self, force=force, recurse=True, variable=variable, checkEach=checkEach, index=index) - self.DeviceUpdate() - - - - -class Ad9681ReadoutManual(pr.Device): - def __init__(self, - name = 'Ad9249Readout', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 10 - else: - delayBits = 6 - - - for ch in range(channels): - for i in range(2): - self.add(pr.RemoteVariable( - name = f'ChannelDelay[{ch}][{i}]', - description = f'IDELAY value for serial channel {ch}_{i}', - offset = ch*8 + i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'FrameDelay[{i}]', - description = f'IDELAY value for FCO_{i}', - offset = 0x40 + i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - @self.command() - def AllDelay0(arg): - self.FrameDelay[0].set(arg) - for ch in range(8): - self.ChannelDelay[ch][0].set(arg) - - @self.command() - def AllDelay1(arg): - self.FrameDelay[1].set(arg) - for ch in range(8): - self.ChannelDelay[ch][1].set(arg) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'LostLockCount[{i}]', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x50+ 4*i, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'Locked[{i}]', - description = 'Readout has locked on to the frame boundary', - offset = 0x50+ 4*i, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'AdcFrameSync[{i}]', - description = 'Last deserialized FCO value for debug', - offset = 0x58, - bitSize = 8, - bitOffset = i*8, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x60, + self.add(pr.RemoteVariable( + name = 'ResolutionSampleRateOverride', + description = 'Enables the resolution and maximum sample-rate override', + offset = (0x100*4), bitSize = 1, - bitOffset = 0, + bitOffset = 6, base = pr.Bool, - mode = 'RW', )) self.add(pr.RemoteVariable( - name = 'Negate', - description = "Optional ADC data negation (two's complement)", - offset = 0x60, - bitSize = 1, - bitOffset = 1, - base = pr.Bool, - mode = 'RW', + name = 'Resolution', + description = 'Selects ADC resolution when the override is enabled', + offset = (0x100*4), + bitSize = 2, + bitOffset = 4, + enum = { + 0b01: '14 bits', + 0b10: '12 bits', + }, )) - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:09_x}', - mode = 'RO', - )) - - for i in range(channels): - self.add(pr.LinkVariable( - name = f'AdcVoltage[{i}]', - description = f'Converted voltage for ADC channel {i}', - mode = 'RO', - disp = '{:1.9f}', - variable = self.AdcChannel[i], - linkedGet = lambda read, check, r=self.AdcChannel[i]: 2*pr.twosComplement(r.get(read=read, check=check)>>18, 14)/2**14, - units = 'V')) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x5C, - bitSize = 1, + self.add(pr.RemoteVariable( + name = 'SampleRate', + description = 'Selects maximum ADC sample rate when the override is enabled', + offset = (0x100*4), + bitSize = 3, bitOffset = 0, + enum = { + 0b000: '20 MSPS', + 0b001: '40 MSPS', + 0b010: '50 MSPS', + 0b011: '65 MSPS', + 0b100: '80 MSPS', + 0b101: '105 MSPS', + 0b110: '125 MSPS', + }, )) self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - - self.add(pr.RemoteCommand( - name='Relock', - description='Triggers ADC readout relock sequence', - hidden=False, - offset=0x70, - bitSize=2, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.createToggle([0, 3, 0]))) - - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads - """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - freeze = False #isinstance(variable, list) and any(v.name.startswith('AdcChannel') for v in variable) - if freeze: - self.FreezeDebug(1) - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - if freeze: - self.FreezeDebug(0) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) - -class Ad9681Readout(pr.Device): - def __init__(self, - name = 'Ad9249Readout', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 10 - else: - delayBits = 6 - - self.add(pr.RemoteVariable( - name = 'EnUsrDelay', - description = 'Enable manual delay value', - offset = 0x20, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - verify = True, - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'Delay[{i}]', - description = f'IDELAY value for serial channel {i}', - offset = i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'ErrorDetCount[{i}]', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x30+ 4*i, - disp = '{:d}', - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'LostLockCount[{i}]', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x50+ 4*i, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'Locked[{i}]', - description = 'Readout has locked on to the frame boundary', - offset = 0x50+ 4*i, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'AdcFrameSync[{i}]', - description = 'Last deserialized FCO value for debug', - offset = 0x58, - bitSize = 8, - bitOffset = i*8, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x60, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'Negate', - description = "Optional ADC data negation (two's complement)", - offset = 0x60, - bitSize = 1, - bitOffset = 1, - base = pr.Bool, - mode = 'RW', + name='DeviceUpdate', + description='Transfers the resolution/sample-rate override into the ADC', + offset=0x3FC, + function=pr.BaseCommand.touchOne, )) + def writeBlocks(self, **kwargs: Any) -> None: + """Write pending blocks and transfer them into the ADC.""" + super().writeBlocks(**kwargs) + self.DeviceUpdate() - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:09_x}', - mode = 'RO', - )) - - for i in range(channels): - self.add(pr.LinkVariable( - name = f'AdcVoltage[{i}]', - description = f'Converted voltage for ADC channel {i}', - mode = 'RO', - disp = '{:1.9f}', - variable = self.AdcChannel[i], - linkedGet = lambda read, check, r=self.AdcChannel[i]: 2*pr.twosComplement(r.get(read=read, check=check)>>18, 14)/2**14, - units = 'V')) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x5C, - bitSize = 1, - bitOffset = 0, - )) - - self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - self.add(pr.RemoteCommand( - name='Relock', - description='Triggers ADC readout relock sequence', - hidden=False, - offset=0x70, - bitSize=2, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.createToggle([0, 3, 0]))) - - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads - """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) +class Ad9681Readout(analog_devices.AdcDdr): + """Normalized register model for the eight-channel AD9681 readout. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + **kwargs : Any + Additional arguments forwarded to ``AdcDdr``. + """ + + def __init__( + self, + *, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + **kwargs: Any) -> None: + """Create the normalized AD9681 readout.""" + + delayBits = analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'AD9681 serialized DDR readout') + super().__init__( + dataLanes = 16, + fcoLanes = 2, + channels = 8, + sampleBits = 14, + serializationFactor = 8, + delayBits = delayBits, + **kwargs) + + +class Ad9681ReadoutCalibration(analog_devices.AdcDdrCalibration): + """Calibration process for a normalized AD9681 readout. + + Parameters + ---------- + config : Ad9681Config + ADC configuration device. + readout : Ad9681Readout + Normalized readout to calibrate. + **kwargs : Any + Additional arguments forwarded to ``AdcDdrCalibration``. + """ + + def __init__( + self, + *, + config: Ad9681Config, + readout: Ad9681Readout, + **kwargs: Any) -> None: + """Create the AD9681 calibration process.""" + + if not isinstance(config, Ad9681Config): + raise TypeError('config must be an Ad9681Config') + if not isinstance(readout, Ad9681Readout): + raise TypeError('readout must be an Ad9681Readout') + super().__init__( + config = config, + readout = readout, + dataLaneToChannel = tuple(range(8))+tuple(range(8)), + dataLaneMasks = (0x003F,)*8+(0x3FC0,)*8, + pn23Reset = config.ResetPNLong, + **kwargs) diff --git a/python/surf/devices/analog_devices/_AdcDdr.py b/python/surf/devices/analog_devices/_AdcDdr.py new file mode 100644 index 0000000000..0b88359f0c --- /dev/null +++ b/python/surf/devices/analog_devices/_AdcDdr.py @@ -0,0 +1,472 @@ +#----------------------------------------------------------------------------- +# Title : Serialized DDR ADC readout +#----------------------------------------------------------------------------- +# Description: +# PyRogue model for the normalized AdcDdr capture and monitoring register map. +#----------------------------------------------------------------------------- +# This file is part of the 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any, Literal, Mapping, Sequence + +import pyrogue as pr + +import surf.devices.analog_devices as analog_devices + + +AdcDdrDeviceFamily = Literal['7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'] + + +def adcDdrDelayBits(deviceFamily: AdcDdrDeviceFamily) -> int: + """Return the native input-delay width for an FPGA device family. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'} + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + + Returns + ------- + int + Five for 7-Series or nine for UltraScale and UltraScale+. + + Raises + ------ + ValueError + If ``deviceFamily`` is not supported by the AdcDdr PHY. + """ + + try: + return { + '7SERIES': 5, + 'ULTRASCALE': 9, + 'ULTRASCALE_PLUS': 9, + }[deviceFamily] + except KeyError as exc: + raise ValueError( + 'deviceFamily must be 7SERIES, ULTRASCALE, or ULTRASCALE_PLUS') from exc + + +def _formatDebugSnapshot(samples: Sequence[int], sampleBits: int) -> str: + """Format one debug snapshot as grouped hexadecimal ADC samples.""" + + width = (sampleBits+3)//4 + mask = (1 << sampleBits)-1 + return '0x' + '_'.join(f'{sample & mask:0{width}X}' for sample in samples) + + +def _convertDebugVoltage( + sample: int, + sampleBits: int, + inputRange: float, + offsetBinary: bool) -> float: + """Convert one ADC code to a signed voltage.""" + + modulus = 1 << sampleBits + sign = 1 << (sampleBits-1) + mask = modulus-1 + code = sample & mask + if offsetBinary: + signed = code-sign + elif code & sign: + signed = code-modulus + else: + signed = code + return inputRange*signed/modulus + + +class AdcDdr(pr.Device): + """PyRogue model for a normalized serialized DDR ADC readout. + + Parameters + ---------- + dataLanes : int, optional + Number of serialized ADC data lanes. + fcoLanes : int, optional + Number of serialized frame-clock lanes. + channels : int, optional + Number of logical ADC channels. + sampleBits : int, optional + Number of meaningful bits in each ADC sample. + serializationFactor : int, optional + Number of serialized bits captured per data lane. + delayBits : int, optional + Width of each programmable input-delay value. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def _getDataDelays(self, read: bool) -> list[int]: + """Return all data-lane delay values.""" + + return [int(delay) for delay in self.DataDelayBulk.get(read=read)] + + def _setDataDelays(self, values: Mapping[int, int]) -> None: + """Set and verify a collection of data-lane delay values.""" + + for lane, value in sorted(values.items()): + self.DataDelayBulk.set(value, index=lane, write=False) + self.writeAndVerifyBlocks( + recurse = False, + variable = self.DataDelayBulk) + + def _getDebugSamples(self, read: bool) -> list[list[int]]: + """Return four debug samples for every logical channel.""" + + flat = self.DebugSampleRaw.get(read=read) + return [ + [int(sample) for sample in flat[4*channel:4*(channel+1)]] + for channel in range(self._channels) + ] + + def _snapshot(self, cmd: Any) -> list[str]: + """Trigger and format one coherent debug snapshot.""" + + cmd.set(1) + self._getDebugSamples(read=True) + return [ + self.nodes[f'DebugSample[{channel}]'].get(read=False) + for channel in range(self._channels) + ] + + def __init__(self, *, + dataLanes: int = 8, + fcoLanes: int = 1, + channels: int = 8, + sampleBits: int = 14, + serializationFactor: int = 14, + delayBits: int = 5, + **kwargs: Any) -> None: + """Create the normalized ADC readout model.""" + + for name, value, minimum, maximum in ( + ('dataLanes', dataLanes, 1, 64), + ('fcoLanes', fcoLanes, 1, 16), + ('channels', channels, 1, 16), + ('sampleBits', sampleBits, 2, 16), + ('serializationFactor', serializationFactor, 1, 16), + ('delayBits', delayBits, 1, 9)): + if not minimum <= value <= maximum: + raise ValueError(f'{name} must be from {minimum} through {maximum}') + + kwargs.setdefault('description', 'Serialized DDR ADC capture and monitoring') + super().__init__(**kwargs) + + self._dataLanes = dataLanes + self._fcoLanes = fcoLanes + self._channels = channels + self._sampleBits = sampleBits + self._serializationFactor = serializationFactor + self._delayBits = delayBits + + self.add(pr.RemoteVariable( + name = 'Version', + description = 'Normalized AdcDdr register-map version', + offset = 0x000, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:#010x}')) + + self.add(pr.RemoteVariable( + name = 'DataLanes', + description = 'Number of serialized ADC data lanes', + offset = 0x004, + bitSize = 8, + bitOffset = 0, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'FcoLanes', + description = 'Number of serialized ADC frame-clock lanes', + offset = 0x004, + bitSize = 8, + bitOffset = 8, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'Channels', + description = 'Number of logical ADC channels', + offset = 0x004, + bitSize = 8, + bitOffset = 16, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'SampleBits', + description = 'Number of meaningful bits in each ADC sample', + offset = 0x004, + bitSize = 8, + bitOffset = 24, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'DelayBits', + description = 'Width of each programmable input-delay value', + offset = 0x008, + bitSize = 8, + bitOffset = 0, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'SerializationFactor', + description = 'Number of serialized bits captured per data lane', + offset = 0x008, + bitSize = 8, + bitOffset = 8, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'PatternCheck', + description = 'Hardware pattern measurement engine is present', + offset = 0x008, + bitSize = 1, + bitOffset = 16, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'CaptureReset', + description = 'Manually hold the ADC PHY and capture behavior in reset', + offset = 0x00C, + bitSize = 1, + bitOffset = 0, + mode = 'RW', + base = pr.Bool)) + + self.add(pr.RemoteCommand( + name = 'Relock', + description = 'Restart FCO word alignment without changing delays', + offset = 0x010, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.LocalVariable( + name = 'DebugVoltageRange', + description = 'Differential full-scale input range used for debug voltage conversion', + value = 2.0, + minimum = 0.0, + units = 'V', + disp = '{:1.6f}', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'DebugVoltageFormat', + description = 'ADC output coding used for debug voltage conversion', + value = 1, + enum = { + 0: 'Offset Binary', + 1: 'Twos Complement', + }, + mode = 'RW')) + + self.add(pr.RemoteCommand( + name = 'Snapshot', + description = 'Block until four coherent debug samples are captured, then read them', + offset = 0x014, + bitSize = 1, + bitOffset = 0, + function = self._snapshot)) + + self.add(pr.RemoteCommand( + name = 'ClearCounters', + description = 'Clear event counters and sticky overflow status', + offset = 0x018, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.RemoteVariable( + name = 'DelayReady', + description = 'Input-delay controller is ready', + offset = 0x01C, + bitSize = 1, + bitOffset = 1, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AllLocked', + description = 'All FCO lanes are word aligned', + offset = 0x01C, + bitSize = 1, + bitOffset = 2, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AnyOverflow', + description = 'One or more coherent samples were dropped', + offset = 0x01C, + bitSize = 1, + bitOffset = 3, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'LockedMask', + offset = 0x020, + bitSize = fcoLanes, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'SnapshotSequence', + description = 'Completed atomic debug-snapshot count', + offset = 0x024, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(analog_devices.AdcDdrPatternTester( + name = 'PatternTester', + offset = 0x800, + channels = channels, + fcoLanes = fcoLanes, + sampleBits = sampleBits, + expand = False)) + + self.add(pr.RemoteVariable( + name = 'DataDelayBulk', + description = 'Array of programmed input-delay values for all serialized data lanes', + offset = 0x100, + bitSize = 32*dataLanes, + valueBits = delayBits, + numValues = dataLanes, + valueStride = 32, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 0, + maximum = (1 << delayBits) - 1, + hidden = True)) + + for lane in range(dataLanes): + self.add(pr.LinkVariable( + name = f'DataDelay[{lane}]', + description = f'Programmed input-delay value for serialized data lane {lane}', + variable = self.DataDelayBulk, + linkedGet = lambda read, check, lane=lane: self.DataDelayBulk.get( + index=lane, read=read, check=check), + linkedSet = lambda value, write, verify, check, lane=lane: + self.DataDelayBulk.set( + value, + index=lane, + write=write, + verify=verify, + check=check))) + + self.addRemoteVariables( + name = 'FcoDelay', + description = 'Programmed input-delay value for each FCO lane', + number = fcoLanes, + stride = 4, + offset = 0x200, + bitSize = delayBits, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 0, + maximum = (1 << delayBits) - 1) + + self.addRemoteVariables( + name = 'FcoWord', + description = 'Most recent deserialized FCO word', + number = fcoLanes, + stride = 4, + offset = 0x300, + bitSize = serializationFactor, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}') + + self.addRemoteVariables( + name = 'LostLockCount', + number = fcoLanes, + stride = 4, + offset = 0x340, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}') + + self.add(pr.RemoteVariable( + name = 'OverflowCount', + description = 'Saturating coherent sample-group FIFO overflow count', + offset = 0x500, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'DebugSampleRaw', + description = ( + 'Flattened channel-major raw pre-format atomic samples; ' + 'four oldest-to-newest samples per logical channel'), + offset = 0x600, + bitSize = 32*4*channels, + valueBits = sampleBits, + numValues = 4*channels, + valueStride = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}', + hidden = True)) + + for channel in range(channels): + self.add(pr.LinkVariable( + name = f'DebugSample[{channel}]', + description = ( + f'Raw pre-format atomic samples for logical channel {channel}; ' + 'oldest to newest'), + mode = 'RO', + dependencies = [self.DebugSampleRaw], + linkedGet = lambda read, channel=channel: _formatDebugSnapshot( + self._getDebugSamples(read=read)[channel], sampleBits))) + + for channel in range(channels): + self.add(pr.LinkVariable( + name = f'DebugVoltage[{channel}]', + description = f'Differential input voltage for logical channel {channel}', + mode = 'RO', + units = 'V', + disp = '{:1.6f}', + dependencies = [ + self.DebugSampleRaw, + self.DebugVoltageRange, + self.DebugVoltageFormat, + ], + linkedGet = lambda read, channel=channel: _convertDebugVoltage( + self._getDebugSamples(read=read)[channel][0], + sampleBits, + self.DebugVoltageRange.get(read=read), + self.DebugVoltageFormat.get(read=read) == 0))) diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py new file mode 100644 index 0000000000..b6983b7a36 --- /dev/null +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -0,0 +1,1288 @@ +#----------------------------------------------------------------------------- +# Title : Serialized DDR ADC calibration helpers +#----------------------------------------------------------------------------- +# Description: +# Device-neutral eye analysis and calibration result types for AdcDdr. +#----------------------------------------------------------------------------- +# This file is part of the 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +import copy +from dataclasses import asdict, dataclass +import itertools +import threading +import time +from typing import Any, Callable, Iterable, Mapping, Sequence + +import pyrogue as pr + + +class _AdcDdrCalibrationStopped(Exception): + """Internal control flow for a user-requested process stop.""" + + +@dataclass(frozen=True) +class AdcDdrEye: + """One contiguous passing delay window and its chosen sampling tap. + + Parameters + ---------- + start : int + First passing tap in the window. + end : int + Last passing tap in the window. + width : int + Number of passing taps in the window. + selected : int + Center tap selected for sampling. + leftMargin : int + Passing taps between ``selected`` and ``start``. + rightMargin : int + Passing taps between ``selected`` and ``end``. + wraps : bool + Whether the window crosses the end of a circular delay range. + leftBounded : bool + Whether a failing tap was observed beyond the left edge. + rightBounded : bool + Whether a failing tap was observed beyond the right edge. + """ + + start: int + end: int + width: int + selected: int + leftMargin: int + rightMargin: int + wraps: bool + leftBounded: bool + rightBounded: bool + + def contains(self, tap: int) -> bool: + """Return whether ``tap`` lies inside this eye, including wraparound.""" + + if self.wraps: + return tap >= self.start or tap <= self.end + return self.start <= tap <= self.end + + def asDict(self) -> dict[str, Any]: + """Return a PyRogue-friendly dictionary representation.""" + + return asdict(self) + + +def findAdcDdrEyes( + passing: Mapping[int, bool], + *, + minimumWidth: int = 1, + guardBand: int = 0, + circular: bool = False) -> tuple[AdcDdrEye, ...]: + """Return every qualifying window from a consecutive tap scan. + + Parameters + ---------- + passing : Mapping[int, bool] + Pass/fail result for each consecutively scanned delay tap. + minimumWidth : int, optional + Minimum accepted eye width in taps. + guardBand : int, optional + Required passing taps on both sides of the selected center. + circular : bool, optional + Merge passing runs at the high and low scan boundaries. + + Returns + ------- + tuple[AdcDdrEye, ...] + Qualifying eyes ordered by decreasing width and increasing center tap. + + Raises + ------ + ValueError + If the scan controls or tap domain are invalid. + TypeError + If a delay tap index is not an integer. + RuntimeError + If the scan contains no qualifying passing window. + + Notes + ----- + For an even-width window, the lower of its two center positions is used. + """ + + if minimumWidth < 1: + raise ValueError('minimumWidth must be at least one tap') + if guardBand < 0: + raise ValueError('guardBand must not be negative') + if not passing: + raise ValueError('passing scan must not be empty') + + # A contiguous numeric domain makes physical edges and margins meaningful. + # Reject sparse maps rather than accidentally treating an untested gap as + # adjacent delay taps. + taps = sorted(passing) + if any(not isinstance(tap, int) for tap in taps): + raise TypeError('delay tap indices must be integers') + if any(right != left+1 for left, right in zip(taps, taps[1:])): + raise ValueError('passing scan tap indices must be consecutive') + + # Collapse the boolean scan into maximal passing runs. Keeping every run is + # important for FCO calibration because different unit intervals can each + # contain a valid eye even though only one can be the initial selection. + runs = [] + run = [] + for tap in taps: + if passing[tap]: + run.append(tap) + elif run: + runs.append(run) + run = [] + if run: + runs.append(run) + + if not runs: + raise RuntimeError('scan contains no passing delay window') + + scanStart = taps[0] + scanEnd = taps[-1] + # On circular delay elements, the high-end and low-end runs are two pieces + # of one physical eye. Preserve their high-to-low ordering so start > end is + # the explicit wrapped-eye representation used by AdcDdrEye.contains(). + if circular and len(runs) > 1 and runs[0][0] == scanStart and runs[-1][-1] == scanEnd: + runs = runs[1:-1] + [runs[-1] + runs[0]] + + # GuardBand is a center-margin requirement, so it implies a minimum total + # width even when MinimumEyeWidth is configured to a smaller value. + requiredWidth = max(minimumWidth, (2*guardBand)+1) + eyes = [] + for candidate in runs: + if len(candidate) < requiredWidth: + continue + centerIndex = (len(candidate)-1)//2 + start = candidate[0] + end = candidate[-1] + wraps = start > end + eyes.append(AdcDdrEye( + start = start, + end = end, + width = len(candidate), + selected = candidate[centerIndex], + leftMargin = centerIndex, + rightMargin = len(candidate)-centerIndex-1, + wraps = wraps, + leftBounded = wraps or start != scanStart, + rightBounded = wraps or end != scanEnd)) + + if not eyes: + raise RuntimeError( + f'scan contains no passing delay window at least {requiredWidth} taps wide') + + # This ordering makes itertools.product() try the historical preferred eye + # combination first, followed by progressively less-preferred alternatives. + return tuple(sorted(eyes, key=lambda eye: (-eye.width, eye.selected))) + + +def findAdcDdrEye( + passing: Mapping[int, bool], + *, + minimumWidth: int = 1, + guardBand: int = 0, + circular: bool = False) -> AdcDdrEye: + """Select the highest-priority qualifying eye from a tap scan. + + Parameters + ---------- + passing : Mapping[int, bool] + Pass/fail result for each consecutively scanned delay tap. + minimumWidth : int, optional + Minimum accepted eye width in taps. + guardBand : int, optional + Required passing taps on both sides of the selected center. + circular : bool, optional + Merge passing runs at the high and low scan boundaries. + + Returns + ------- + AdcDdrEye + Highest-priority qualifying eye. + """ + + return findAdcDdrEyes( + passing, + minimumWidth=minimumWidth, + guardBand=guardBand, + circular=circular)[0] + + +def checkAdcDdrPn23(samples: Sequence[int], sampleBits: int) -> dict[str, Any]: + """Check an arbitrary-phase, MSB-first PN23 sample sequence. + + Parameters + ---------- + samples : Sequence[int] + Complete logical ADC samples in capture order. + sampleBits : int + Number of meaningful bits in each sample. + + Returns + ------- + dict[str, Any] + Pass status and diagnostics for each tested output transformation. + + Raises + ------ + ValueError + If ``sampleBits`` is invalid or too few bits were captured. + + Notes + ----- + The first 23 captured bits establish the unknown LFSR phase. Remaining + bits must satisfy ``x^23 + x^18 + 1``. All four combinations of output and + format inversion are considered explicitly. + """ + + if sampleBits < 1: + raise ValueError('sampleBits must be positive') + if len(samples)*sampleBits <= 23: + raise ValueError('PN23 verification requires more than 23 captured bits') + + sampleMask = (1 << sampleBits)-1 + transformations = ( + ('asCaptured', 0), + ('formatMsbInverted', 1 << (sampleBits-1)), + ('outputInverted', sampleMask), + ('outputAndFormatInverted', sampleMask ^ (1 << (sampleBits-1))), + ) + attempts = [] + for name, xorMask in transformations: + # ADC PN words are serialized MSB first. Flatten complete logical words + # in that same order so word boundaries disappear for the recurrence. + bits = [ + ((sample ^ xorMask) >> bit) & 1 + for sample in samples + for bit in range(sampleBits-1, -1, -1) + ] + initialState = sum(bit << (22-index) for index, bit in enumerate(bits[:23])) + errorBits = [ + index + for index in range(23, len(bits)) + if bits[index] != (bits[index-23] ^ bits[index-18]) + ] + # The all-zero state satisfies the linear recurrence but is not part of + # the maximal-length PN23 sequence and must not qualify a dead data bus. + passed = initialState != 0 and not errorBits + attempt = { + 'name': name, + 'xorMask': xorMask, + 'initialState': initialState, + 'checkedBits': len(bits)-23, + 'errorBits': errorBits, + 'passed': passed, + } + attempts.append(attempt) + if passed: + return { + 'passed': True, + 'selected': copy.deepcopy(attempt), + 'attempts': attempts, + } + + return { + 'passed': False, + 'selected': None, + 'attempts': attempts, + } + + +class AdcDdrCalibration(pr.Process): + """Software-driven FCO and per-data-lane delay calibration. + + The process scans FCO eyes first, centers compatible data lanes in parallel, + and finally qualifies the complete logical sample with a shared checkerboard + phase plus optional PN23 coherence and recurrence. Device adapters describe + how physical lanes map into logical channels and which sample bits each lane + contributes. + + Parameters + ---------- + config : Any + ADC configuration device containing ``OutputTestMode``. + readout : Any + Normalized ``AdcDdr`` readout device. + dataLaneToChannel : Sequence[int], optional + Logical channel contributed by each physical data lane. + dataLaneMasks : Sequence[int], optional + Logical sample-bit mask contributed by each physical data lane. + testMode : int, optional + ADC checkerboard test-mode value. + expectedPatterns : Iterable[int], optional + Expected checkerboard sample values. + configUpdate : Callable[[], None], optional + Callback that transfers staged ADC configuration writes. + pn23Mode : int, optional + ADC PN23 test-mode value. + pn23Reset : Any, optional + PyRogue variable or command used to restart the PN23 generator. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Process``. + """ + + FULL_C = 0 + VERIFY_CURRENT_C = 1 + VERIFY_GUARD_BAND_C = 2 + RUN_TIME_UPDATE_INTERVAL_C = 1.0 + + def __init__( + self, + *, + config: Any, + readout: Any, + dataLaneToChannel: Sequence[int] | None = None, + dataLaneMasks: Sequence[int] | None = None, + testMode: int = 4, + expectedPatterns: Iterable[int] | None = None, + configUpdate: Callable[[], None] | None = None, + pn23Mode: int = 5, + pn23Reset: Any | None = None, + **kwargs: Any) -> None: + """Construct a calibration process for one normalized ADC readout.""" + + self._config = config + self._readout = readout + self._dataLanes = readout._dataLanes + self._fcoLanes = readout._fcoLanes + self._channels = readout._channels + + # One-lane ADCs use the natural lane-to-channel mapping. Multi-lane ADC + # adapters, such as AD9681, explicitly map both physical halves back to + # the same set of logical channels. + if dataLaneToChannel is None: + dataLaneToChannel = [lane % self._channels for lane in range(self._dataLanes)] + if len(dataLaneToChannel) != self._dataLanes: + raise ValueError('dataLaneToChannel must contain one entry per data lane') + if any(channel < 0 or channel >= self._channels for channel in dataLaneToChannel): + raise ValueError('dataLaneToChannel contains an out-of-range channel') + self._dataLaneToChannel = tuple(dataLaneToChannel) + + # The mask identifies which logical sample bits are actually carried by + # a physical lane. It lets a lane be judged while its partner half is + # still outside its eye. + if dataLaneMasks is None: + dataLaneMasks = [(1 << readout._sampleBits)-1] * self._dataLanes + if len(dataLaneMasks) != self._dataLanes: + raise ValueError('dataLaneMasks must contain one entry per data lane') + sampleMask = (1 << readout._sampleBits)-1 + if any(mask <= 0 or mask & ~sampleMask for mask in dataLaneMasks): + raise ValueError('dataLaneMasks contains an invalid sample-bit mask') + self._dataLaneMasks = tuple(dataLaneMasks) + # Greedily build the largest safe parallel sweep groups. Lanes on + # different channels never interfere. Lanes on the same channel can + # also move together when their masks are disjoint, as with AD9681's + # lower-six-bit and upper-eight-bit halves. Overlapping contributors + # remain in separate groups so an error can be attributed to one lane. + groups = [] + for lane, (channel, mask) in enumerate( + zip(self._dataLaneToChannel, self._dataLaneMasks)): + for group in groups: + if all( + self._dataLaneToChannel[other] != channel or + not (self._dataLaneMasks[other] & mask) + for other in group): + group.append(lane) + break + else: + groups.append([lane]) + self._dataLaneGroups = tuple(tuple(group) for group in groups) + + # Default to the ADC checkerboard mode. A frozenset intentionally drops + # phase ordering here; per-lane eye scans need only observe both values. + # The final full-sample check restores strict shared phase ordering. + if expectedPatterns is None: + checkerA = sum(1 << bit for bit in range(readout._sampleBits) if bit % 2) + checkerB = ((1 << readout._sampleBits)-1) ^ checkerA + expectedPatterns = (checkerA, checkerB) + self._expectedPatterns = frozenset(expectedPatterns) + if not self._expectedPatterns: + raise ValueError('expectedPatterns must not be empty') + self._testMode = testMode + self._configUpdate = configUpdate + self._pn23Mode = pn23Mode + self._pn23Reset = pn23Reset + self._runDiagnostics = {} + self._lastCaptureDiagnostics = {} + self._patternTesterActive = False + + kwargs.setdefault( + 'description', + 'Measure, verify, and apply normalized AdcDdr input-delay settings') + super().__init__(function=self._runCalibration, **kwargs) + + # User controls, retained results, and diagnostics are LocalVariables; + # all hardware access remains behind the normalized readout/config APIs. + self.add(pr.LocalVariable( + name = 'Operation', + description = 'Calibration or verification operation', + value = self.FULL_C, + enum = { + self.FULL_C: 'Full calibration', + self.VERIFY_CURRENT_C: 'Verify current', + self.VERIFY_GUARD_BAND_C: 'Verify guard band', + }, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'DelayStart', + description = 'First delay tap included in a full scan', + value = 0, + minimum = 0, + maximum = (1 << readout._delayBits)-1, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'DelayStop', + description = 'Last delay tap included in a full scan', + value = (1 << readout._delayBits)-1, + minimum = 0, + maximum = (1 << readout._delayBits)-1, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'UsePatternTester', + description = 'Use the hardware pattern tester instead of debug snapshots', + value = False, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'SampleCount', + description = 'Four-sample groups checked at each data tap', + value = 2, + minimum = 1, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'VerifyPn23', + description = ( + 'After checkerboard alignment, compare one four-sample PN23 ' + 'snapshot across channels and verify its recurrence'), + value = pn23Reset is not None, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'RunTime', + description = 'Elapsed wall-clock duration of the current or last operation', + value = 0.0, + units = 's', + disp = '{:1.6f}', + mode = 'RO')) + + self.add(pr.LocalVariable( + name = 'SettleTime', + description = 'Delay after changing a tap, relocking, or selecting test mode', + value = 0.001, + minimum = 0.0, + units = 's', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'MinimumEyeWidth', + description = 'Minimum passing width accepted by a full scan', + value = 8, + minimum = 1, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'GuardBand', + description = 'Passing taps required on both sides of a selected tap', + value = 2, + minimum = 0, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'CircularDelays', + description = 'Merge passing windows at the delay scan boundaries', + value = False, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'Debug', + description = 'Publish detailed per-tap calibration diagnostics', + value = True, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'Diagnostics', + description = 'Per-tap FCO words, lock state, and raw/masked data samples', + value = {}, + mode = 'RO')) + + self.add(pr.LocalVariable( + name = 'Results', + description = 'FCO and data-lane results from the last operation', + value = {}, + mode = 'RO')) + + self.add(pr.LocalCommand( + name = 'ApplyResults', + description = 'Reapply selected taps from the last full calibration', + function = self.applyResults)) + + def _process(self) -> None: + """Translate a user-requested stop into a normal process outcome.""" + + try: + super()._process() + except _AdcDdrCalibrationStopped: + # Process.Stop is an expected user action. The calibration + # function has already restored the test mode and original delays + # in its finally block, so retain the partial progress without + # reporting the stop as an execution error. + self.Message.set('Calibration stopped') + + def _checkRun(self) -> None: + """Raise the private stop exception at cooperative cancellation points.""" + + if not self._runEn: + raise _AdcDdrCalibrationStopped + + def _recordDiagnostic( + self, + kind: str, + lane: int, + tap: int, + detail: dict[str, Any], + publish: bool = False) -> None: + """Retain one per-tap diagnostic and optionally publish a deep copy.""" + + # The working dictionary is mutated throughout a run. Publish a copy so + # PyRogue readers never observe later mutations through a shared object. + self._runDiagnostics[kind].setdefault(lane, {})[tap] = detail + self._runDiagnostics['Current'] = { + 'kind': kind, + 'lane': lane, + 'tap': tap, + } + if publish or self.Debug.value(): + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) + + def _configurePatternTester(self, channels: Sequence[int], mask: int) -> None: + """Program one hardware measurement window for a channel/mask set.""" + + patterns = sorted(self._expectedPatterns) + tester = self._readout.PatternTester + # Alternating patterns share one phase reference across every enabled + # channel. This prevents each logical channel from independently calling + # an opposite checkerboard phase valid. + tester.Alternating.set(len(patterns) == 2, write=True) + tester.ReferenceChannel.set(channels[0], write=True) + tester.ChannelMask.set(sum(1 << channel for channel in channels), write=True) + tester.FcoMask.set((1 << self._fcoLanes)-1, write=True) + tester.DataMask.set(mask, write=True) + tester.PatternA.set(patterns[0], write=True) + tester.PatternB.set(patterns[-1], write=True) + # One software SampleCount unit represents the same four ADC samples as + # one atomic debug snapshot, keeping the two backends comparable. + tester.Samples.set(4*self.SampleCount.value(), write=True) + + def _runPatternTester(self) -> dict[str, Any]: + """Start one hardware window and return its stable retained results.""" + + tester = self._readout.PatternTester + # Sequence is the completion handshake. It avoids depending on the exact + # cycle in which Busy asserts or clears across the AXI-Lite boundary. + sequence = int(tester.Sequence.get(read=True)) + tester.Start() + while True: + if not self._runEn: + tester.Abort() + self._checkRun() + current = int(tester.Sequence.get(read=True)) + if current != sequence: + break + + # Force one final hardware transaction before reading the cached status + # fields populated by the completed measurement. + tester.Busy.get(read=True) + status = { + 'busy': bool(tester.Busy.get(read=False)), + 'timedOut': bool(tester.TimedOut.get(read=False)), + 'configError': bool(tester.ConfigError.get(read=False)), + 'aborted': bool(tester.Aborted.get(read=False)), + 'phaseAcquired': bool(tester.PhaseAcquired.get(read=False)), + 'allChannelsPass': bool(tester.AllChannelsPass.get(read=False)), + 'allFcoPass': bool(tester.AllFcoPass.get(read=False)), + } + if status['timedOut']: + raise RuntimeError('ADC pattern tester timed out waiting for valid samples') + if status['configError']: + raise RuntimeError('ADC pattern tester rejected its measurement configuration') + if status['aborted']: + raise RuntimeError('ADC pattern tester measurement was aborted') + + return { + 'sequence': current, + 'checkedSamples': int(tester.CheckedSamples.get(read=True)), + 'channelPassed': int(tester.ChannelPassed.get(read=True)), + 'fcoPassed': int(tester.FcoPassed.get(read=True)), + 'status': status, + } + + def _captureGroupPassesPattern( + self, + lanes: Sequence[int]) -> dict[int, dict[str, Any]]: + """Measure a parallel lane group with the hardware pattern tester.""" + + # The hardware checker has one DataMask per window. A mixed-mask group + # still shares one programmed delay tap, but requires one measurement + # for each distinct mask to preserve independent lane verdicts. + lanesByMask = {} + for lane in lanes: + lanesByMask.setdefault(self._dataLaneMasks[lane], []).append(lane) + + details = {} + for mask, maskedLanes in lanesByMask.items(): + channels = sorted({self._dataLaneToChannel[lane] for lane in maskedLanes}) + self._configurePatternTester(channels, mask) + measurement = self._runPatternTester() + fcoMask = (1 << self._fcoLanes)-1 + fcoPassed = (measurement['fcoPassed'] & fcoMask) == fcoMask + # ChannelPassed is reported per logical channel. Map that verdict + # back to every physical lane contributing the mask being measured. + for lane in maskedLanes: + details[lane] = { + 'channel': self._dataLaneToChannel[lane], + 'mask': mask, + 'expected': sorted({ + pattern & mask + for pattern in self._expectedPatterns}), + 'captures': [copy.deepcopy(measurement)], + 'passed': ( + bool(measurement['channelPassed'] & + (1 << self._dataLaneToChannel[lane])) and + fcoPassed), + } + return details + + def _captureGroupPassesSnapshot( + self, + lanes: Sequence[int]) -> dict[int, dict[str, Any]]: + """Measure a parallel lane group from atomic four-sample snapshots.""" + + details = { + lane: { + 'channel': self._dataLaneToChannel[lane], + 'mask': self._dataLaneMasks[lane], + 'expected': sorted({ + pattern & self._dataLaneMasks[lane] + for pattern in self._expectedPatterns}), + 'captures': [], + 'passed': True, + } + for lane in lanes + } + channels = sorted({self._dataLaneToChannel[lane] for lane in lanes}) + for _ in range(self.SampleCount.value()): + # Snapshot publishes every logical channel atomically. Read it once + # and reuse that coherent capture for every lane-specific mask. + self._readout.Snapshot() + snapshotSequence = self._readout.SnapshotSequence.get(read=False) + snapshot = self._readout._getDebugSamples(read=False) + rawByChannel = { + channel: snapshot[channel] + for channel in channels + } + for lane, detail in details.items(): + raw = rawByChannel[detail['channel']] + masked = [sample & detail['mask'] for sample in raw] + # Eye discovery only needs both expected checkerboard values; + # strict temporal phase is deferred until all lanes are centered. + capturePassed = sorted(set(masked)) == detail['expected'] + detail['captures'].append({ + 'sequence': snapshotSequence, + 'raw': raw, + 'masked': masked, + 'passed': capturePassed, + }) + detail['passed'] &= capturePassed + return details + + def _captureGroupPasses( + self, + lanes: Sequence[int]) -> dict[int, dict[str, Any]]: + """Dispatch one grouped measurement to the selected backend.""" + + if not self._patternTesterActive: + return self._captureGroupPassesSnapshot(lanes) + return self._captureGroupPassesPattern(lanes) + + def _capturePasses(self, lane: int) -> bool: + """Compatibility helper for verifying one physical data lane.""" + + detail = self._captureGroupPasses((lane,))[lane] + self._lastCaptureDiagnostics = detail + return detail['passed'] + + def _captureFinalPasses(self) -> dict[str, Any]: + """Qualify complete logical samples with one shared pattern phase.""" + + mask = (1 << self._readout._sampleBits)-1 + patterns = sorted(self._expectedPatterns) + if self._patternTesterActive: + self._configurePatternTester(list(range(self._channels)), mask) + measurement = self._runPatternTester() + channels = { + channel: { + 'passed': bool(measurement['channelPassed'] & (1 << channel)), + } + for channel in range(self._channels) + } + capture = copy.deepcopy(measurement) + capture['channels'] = channels + return { + 'mask': mask, + 'expected': patterns, + 'referenceChannel': 0, + 'captures': [capture], + 'passed': ( + all(channel['passed'] for channel in channels.values()) and + measurement['status']['allFcoPass']), + } + + detail = { + 'mask': mask, + 'expected': patterns, + 'referenceChannel': 0, + 'captures': [], + 'passed': True, + } + + for _ in range(self.SampleCount.value()): + self._checkRun() + self._readout.Snapshot() + snapshotSequence = self._readout.SnapshotSequence.get(read=False) + snapshot = self._readout._getDebugSamples(read=False) + rawByChannel = { + channel: snapshot[channel] + for channel in range(self._channels) + } + maskedByChannel = { + channel: [sample & mask for sample in raw] + for channel, raw in rawByChannel.items() + } + + # Acquire the A/B phase once from channel zero, then require every + # channel—and therefore every assembled physical half—to match the + # same ordered sequence. This detects split-lane sample epochs that + # an FCO pattern cannot distinguish by itself. + reference = maskedByChannel[detail['referenceChannel']] + try: + phase = patterns.index(reference[0]) + except ValueError: + phase = None + expected = [] + else: + expected = [ + patterns[(phase+index) % len(patterns)] + for index in range(len(reference)) + ] + + channels = { + channel: { + 'raw': rawByChannel[channel], + 'masked': masked, + 'passed': phase is not None and masked == expected, + } + for channel, masked in maskedByChannel.items() + } + capturePassed = all(channel['passed'] for channel in channels.values()) + detail['captures'].append({ + 'sequence': snapshotSequence, + 'phase': phase, + 'expectedSequence': expected, + 'channels': channels, + 'passed': capturePassed, + }) + detail['passed'] &= capturePassed + + return detail + + def _setPn23Mode(self) -> None: + """Select PN23 and synchronously restart every selected generator.""" + + if self._pn23Reset is None: + raise RuntimeError('PN23 verification requires a PN-long reset control') + + # Publish mode and asserted reset together, then explicitly clear reset. + # This avoids depending on whether a particular ADC implements the reset + # field as a pulse, a level, or a self-clearing staged register bit. + self._config.OutputTestMode.set(self._pn23Mode, write=True) + self._pn23Reset.set(True, write=True) + if self._configUpdate is not None: + self._configUpdate() + self._pn23Reset.set(False, write=True) + if self._configUpdate is not None: + self._configUpdate() + + def _capturePn23Passes(self) -> dict[str, Any]: + """Check channel coherence and PN23 recurrence in one atomic snapshot.""" + + self._checkRun() + self._readout.Snapshot() + snapshotSequence = self._readout.SnapshotSequence.get(read=False) + snapshot = self._readout._getDebugSamples(read=False) + channels = { + channel: [int(sample) for sample in snapshot[channel]] + for channel in range(self._channels) + } + referenceChannel = 0 + reference = channels[referenceChannel] + recurrence = checkAdcDdrPn23(reference, self._readout._sampleBits) + channelResults = { + channel: { + 'samples': samples, + 'matchesReference': samples == reference, + } + for channel, samples in channels.items() + } + coherencePassed = all( + result['matchesReference'] + for result in channelResults.values()) + return { + 'sequence': snapshotSequence, + 'referenceChannel': referenceChannel, + 'channels': channelResults, + 'coherencePassed': coherencePassed, + 'recurrence': recurrence, + 'passed': coherencePassed and recurrence['passed'], + } + + def _setTestMode(self, value: int) -> None: + """Select an ADC output pattern and issue any required device update.""" + + self._config.OutputTestMode.set(value, write=True) + if self._configUpdate is not None: + # Some ADC register interfaces shadow writes until a transfer/update + # command is issued. Device adapters provide that command here. + self._configUpdate() + + def _scanFco( + self, + lane: int, + taps: Sequence[int], + settle: float) -> dict[int, bool]: + """Return the lock verdict at every requested tap for one FCO lane.""" + + passing = {} + for tap in taps: + self._checkRun() + self._readout.FcoDelay[lane].set(tap, write=True) + # Changing FCO timing invalidates the frame-word lock. Reacquire it + # at each tap before treating LockedMask as the measurement result. + self._readout.Relock() + time.sleep(settle) + lockedMask = self._readout.LockedMask.get(read=True) + passing[tap] = bool(lockedMask & (1 << lane)) + if self.Debug.value(): + word = self._readout.FcoWord[lane].get(read=True) + self._recordDiagnostic('Fco', lane, tap, { + 'passed': passing[tap], + 'lockedMask': lockedMask, + 'word': word, + }) + self.Message.set( + f'FCO lane {lane}, tap {tap}: ' + f'{"pass" if passing[tap] else "fail"}, ' + f'word=0x{word:X}, lockedMask=0x{lockedMask:X}') + self.incrementSteps() + return passing + + def _scanDataGroup( + self, + lanes: Sequence[int], + taps: Sequence[int], + settle: float) -> dict[int, dict[int, bool]]: + """Sweep a compatible physical-lane group through one common tap range.""" + + passing = {lane: {} for lane in lanes} + for tap in taps: + self._checkRun() + # All group members move before the capture, so every lane verdict + # in this iteration describes the same physical sampling instant. + self._readout._setDataDelays({lane: tap for lane in lanes}) + time.sleep(settle) + details = self._captureGroupPasses(lanes) + for lane, detail in details.items(): + passing[lane][tap] = detail['passed'] + self._runDiagnostics['Data'].setdefault(lane, {})[tap] = detail + self._runDiagnostics['Current'] = { + 'kind': 'Data', + 'lanes': list(lanes), + 'tap': tap, + } + if self.Debug.value(): + passed = [lane for lane in lanes if details[lane]['passed']] + failed = [lane for lane in lanes if not details[lane]['passed']] + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) + self.Message.set( + f'Data lanes {list(lanes)}, tap {tap}: ' + f'passing={passed}, failing={failed}') + self.incrementSteps() + return passing + + def _fullCalibration( + self, + taps: Sequence[int], + settle: float) -> dict[str, Any]: + """Scan, center, and jointly qualify every FCO and data lane.""" + + results = {'Fco': {}, 'Data': {}} + fcoEyes = {} + minimum = self.MinimumEyeWidth.value() + guard = self.GuardBand.value() + circular = self.CircularDelays.value() + + # FCO lanes and compatible data groups each consume one tap scan. FCO + # combination retries are data-dependent, so they are intentionally not + # included in the deterministic progress-bar total. + self.setTotalSteps(len(taps)*(self._fcoLanes+len(self._dataLaneGroups))+1) + self.setStep(0) + + # Locate all qualifying FCO windows. The preferred (widest) eye is + # installed immediately, while alternatives are retained for the final + # cross-lane phase qualification below. + for lane in range(self._fcoLanes): + self.Message.set(f'Scanning FCO lane {lane}') + passing = self._scanFco(lane, taps, settle) + try: + eyes = findAdcDdrEyes( + passing, + minimumWidth=minimum, + guardBand=guard, + circular=circular) + except RuntimeError as exc: + results['Fco'][lane] = { + 'passing': passing, + 'diagnostics': copy.deepcopy(self._runDiagnostics['Fco'].get(lane, {})), + } + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) + self.Results.set(results) + raise RuntimeError(f'FCO lane {lane}: {exc}') from exc + fcoEyes[lane] = eyes + results['Fco'][lane] = { + 'eye': eyes[0].asDict(), + 'eyes': [eye.asDict() for eye in eyes], + 'passing': passing, + } + self._readout.FcoDelay[lane].set(eyes[0].selected, write=True) + + self._readout.Relock() + time.sleep(settle) + + # Put the ADC into a known alternating pattern only after FCO lock is + # established. Each overlap-safe lane group is swept once and centered + # before the next group is measured. + self._setTestMode(self._testMode) + time.sleep(settle) + for lanes in self._dataLaneGroups: + self.Message.set(f'Scanning data lanes {list(lanes)}') + groupPassing = self._scanDataGroup(lanes, taps, settle) + groupEyes = {} + for lane in lanes: + passing = groupPassing[lane] + try: + eye = findAdcDdrEye( + passing, + minimumWidth=minimum, + guardBand=guard, + circular=circular) + except RuntimeError as exc: + results['Data'][lane] = { + 'passing': passing, + 'diagnostics': copy.deepcopy( + self._runDiagnostics['Data'].get(lane, {})), + } + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) + self.Results.set(results) + raise RuntimeError(f'Data lane {lane}: {exc}') from exc + groupEyes[lane] = eye + results['Data'][lane] = {'eye': eye.asDict(), 'passing': passing} + # Center the complete group before scanning any remaining lanes + # whose overlapping sample masks require a separate pass. + self._readout._setDataDelays({ + lane: eye.selected + for lane, eye in groupEyes.items() + }) + + # Per-lane FCO lock cannot distinguish equivalent frame windows that + # imply different sample epochs. Try the Cartesian product of retained + # eyes and accept the first combination whose fully assembled channel + # data shares one ordered checkerboard phase. + self.Message.set('Checking final full-channel pattern alignment') + combinations = itertools.product(*( + fcoEyes[lane] + for lane in range(self._fcoLanes) + )) + attempts = [] + final = None + for index, combination in enumerate(combinations): + self._checkRun() + if index != 0: + # Data-eye locations are independent of which equivalent FCO + # window establishes frame phase, so retries only move FCO taps + # and relock; the expensive data sweeps do not need repeating. + selected = { + lane: eye.selected + for lane, eye in enumerate(combination) + } + self.Message.set( + f'Retrying FCO eye combination ' + f'{[selected[lane] for lane in range(self._fcoLanes)]}') + for lane, tap in selected.items(): + self._readout.FcoDelay[lane].set(tap, write=True) + self._readout.Relock() + time.sleep(settle) + # A previous combination may have reached PN23 qualification. + # Restore checkerboard before judging this combination so every + # FCO candidate is subject to the same two-stage test. + self._setTestMode(self._testMode) + time.sleep(settle) + + checkerboard = self._captureFinalPasses() + candidate = copy.deepcopy(checkerboard) + candidate['checkerboardPassed'] = checkerboard['passed'] + candidate['pn23'] = { + 'enabled': bool(self.VerifyPn23.value()), + 'performed': False, + 'passed': not bool(self.VerifyPn23.value()), + } + if checkerboard['passed'] and self.VerifyPn23.value(): + self.Message.set('Checking final PN23 channel coherence and recurrence') + self._setPn23Mode() + time.sleep(settle) + pn23 = self._capturePn23Passes() + pn23['enabled'] = True + pn23['performed'] = True + candidate['pn23'] = pn23 + candidate['passed'] = pn23['passed'] + else: + candidate['passed'] = checkerboard['passed'] and candidate['pn23']['passed'] + + # Retain every attempted combination and its raw qualification so a + # failed calibration explains exactly which phase choices were tried. + attempts.append({ + 'fcoDelays': [eye.selected for eye in combination], + 'fcoEyes': [eye.asDict() for eye in combination], + 'passed': candidate['passed'], + 'qualification': copy.deepcopy(candidate), + }) + if candidate['passed']: + final = candidate + # Publish the eyes actually left in hardware, which may differ + # from the individually preferred eyes selected above. + for lane, eye in enumerate(combination): + results['Fco'][lane]['eye'] = eye.asDict() + break + + if final is None: + final = copy.deepcopy(attempts[-1]['qualification']) + final['attempts'] = attempts + results['Final'] = final + self._runDiagnostics['Final'] = copy.deepcopy(final) + self._runDiagnostics['Current'] = {'kind': 'Final'} + self.incrementSteps() + if not final['passed']: + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) + self.Results.set(results) + raise RuntimeError('Final full-channel pattern qualification failed') + return results + + def _verifyCurrent( + self, + originalFco: Sequence[int], + originalData: Sequence[int], + settle: float, + guard: int) -> tuple[dict[str, Any], bool]: + """Check installed taps, optionally including symmetric guard points.""" + + start = self.DelayStart.value() + stop = self.DelayStop.value() + results = {'Fco': {}, 'Data': {}} + passed = True + candidatesPerLane = 1 if guard == 0 else 3 + + # Verification checks every lane independently. Unlike a full scan, it + # previously did not initialize or advance the Process progress fields, + # which left the UI parked at step one for the entire operation. + self.setTotalSteps(candidatesPerLane*(len(originalFco)+len(originalData))) + self.setStep(0) + + self._setTestMode(self._testMode) + time.sleep(settle) + for kind, currentValues in ( + ('Fco', originalFco), + ('Data', originalData)): + for lane, selected in enumerate(currentValues): + checked = {} + # Guard-band verification samples the selected point and the + # requested distance on either side. A point outside the user's + # allowed scan domain is an explicit failure, not a skipped test. + candidates = [selected] if guard == 0 else [selected-guard, selected, selected+guard] + for tap in candidates: + self._checkRun() + self.Message.set( + f'Verifying {kind} lane {lane}, tap {tap} ' + f'(selected {selected})') + if tap < start or tap > stop: + checked[tap] = False + elif kind == 'Fco': + self._readout.FcoDelay[lane].set(tap, write=True) + self._readout.Relock() + time.sleep(settle) + checked[tap] = bool( + self._readout.LockedMask.get(read=True) & (1 << lane)) + else: + self._readout._setDataDelays({lane: tap}) + time.sleep(settle) + checked[tap] = self._capturePasses(lane) + self.incrementSteps() + lanePassed = all(checked.values()) + results[kind][lane] = { + 'selected': selected, + 'guardBand': guard, + 'checked': checked, + 'passed': lanePassed, + } + passed &= lanePassed + # Restore this lane before testing the next one. Multiple physical + # lanes can contribute to the same logical sample, so leaving one + # at its final guard candidate can invalidate the next lane's test. + if kind == 'Fco': + self._readout.FcoDelay[lane].set(selected, write=True) + self._readout.Relock() + else: + self._readout._setDataDelays({lane: selected}) + time.sleep(settle) + return results, passed + + def _updateRunTime(self, runStart: float, stopEvent: threading.Event) -> None: + """Publish elapsed wall time without burdening individual scan loops.""" + + while not stopEvent.wait(self.RUN_TIME_UPDATE_INTERVAL_C): + self.RunTime.set(time.monotonic()-runStart) + + def _runCalibration(self, *, dev: Any) -> dict[str, Any]: + """Wrap the operation with a live elapsed-time monitor.""" + + runStart = time.monotonic() + timerStop = threading.Event() + timerThread = threading.Thread( + target = self._updateRunTime, + args = (runStart, timerStop), + daemon = True) + self.RunTime.set(0.0) + timerThread.start() + try: + return self._runCalibrationImpl(dev=dev) + finally: + runTime = time.monotonic()-runStart + timerStop.set() + timerThread.join() + self.RunTime.set(runTime) + + def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: + """Validate controls, execute the selected operation, and restore state.""" + + start = self.DelayStart.value() + stop = self.DelayStop.value() + settle = self.SettleTime.value() + if start > stop: + raise ValueError('DelayStart must not be greater than DelayStop') + + self._patternTesterActive = bool(self.UsePatternTester.value()) + if self._patternTesterActive: + # Fail before changing ADC state if the selected readout cannot + # perform the requested hardware-backed measurement. + if not bool(self._readout.PatternCheck.get(read=True)): + raise RuntimeError('ADC pattern tester is not present in this readout') + if len(self._expectedPatterns) > 2: + raise RuntimeError( + 'ADC pattern tester supports at most two expected patterns') + + # Snapshot every mutable hardware setting before the operation. Verify, + # failure, and stop paths restore these values in the common finally + # block; only a successful full calibration retains newly selected taps. + originalMode = self._config.OutputTestMode.get(read=True) + originalFco = [variable.get(read=True) for variable in self._readout.FcoDelay.values()] + originalData = self._readout._getDataDelays(read=True) + operation = self.Operation.value() + results = {} + retainResults = False + self._runDiagnostics = { + 'TestMode': self._testMode, + 'ExpectedPatterns': sorted(self._expectedPatterns), + 'MeasurementBackend': ( + 'PatternTester' if self._patternTesterActive else 'Snapshot'), + 'VerifyPn23': bool(self.VerifyPn23.value()), + 'Fco': {}, + 'Data': {}, + 'Final': {}, + 'Current': {}, + } + self.Diagnostics.set({}) + self.Results.set({}) + + try: + if operation == self.FULL_C: + results = self._fullCalibration(list(range(start, stop+1)), settle) + elif operation == self.VERIFY_CURRENT_C: + results, passed = self._verifyCurrent(originalFco, originalData, settle, 0) + if not passed: + self.Results.set(results) + raise RuntimeError('Current ADC delay verification failed') + elif operation == self.VERIFY_GUARD_BAND_C: + results, passed = self._verifyCurrent( + originalFco, originalData, settle, self.GuardBand.value()) + if not passed: + self.Results.set(results) + raise RuntimeError('ADC delay guard-band verification failed') + else: + raise ValueError(f'Unsupported calibration operation {operation}') + self.Results.set(results) + if self.Debug.value(): + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) + self.Message.set('Calibration operation complete') + retainResults = operation == self.FULL_C + return results + finally: + # Test mode is always temporary. Delay settings remain installed + # only after a successful full calibration so ApplyResults and + # downstream software see a coherent, qualified configuration. + self._setTestMode(originalMode) + if not retainResults: + for variable, value in zip(self._readout.FcoDelay.values(), originalFco): + variable.set(value, write=True) + self._readout._setDataDelays(dict(enumerate(originalData))) + self._readout.Relock() + + def applyResults(self) -> None: + """Reinstall the selected taps from the last complete calibration.""" + + results = self.Results.value() + # Partial scans and failed final qualifications can contain useful + # diagnostics, but must never be applied as a hardware configuration. + if (not results or not results.get('Final', {}).get('passed', False) or + any('eye' not in lane for kind in ('Fco', 'Data') + for lane in results.get(kind, {}).values())): + raise RuntimeError('No complete full-calibration result is available') + for lane, result in results['Fco'].items(): + self._readout.FcoDelay[int(lane)].set(result['eye']['selected'], write=True) + self._readout._setDataDelays({ + int(lane): result['eye']['selected'] + for lane, result in results['Data'].items() + }) + self._readout.Relock() diff --git a/python/surf/devices/analog_devices/_AdcDdrPatternTester.py b/python/surf/devices/analog_devices/_AdcDdrPatternTester.py new file mode 100644 index 0000000000..d329a38a21 --- /dev/null +++ b/python/surf/devices/analog_devices/_AdcDdrPatternTester.py @@ -0,0 +1,288 @@ +#----------------------------------------------------------------------------- +# Title : Serialized DDR ADC pattern tester +#----------------------------------------------------------------------------- +# Description: +# PyRogue model for the AdcDdr finite-window pattern measurement engine. +#----------------------------------------------------------------------------- +# This file is part of the 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any + +import pyrogue as pr + + +class AdcDdrPatternTester(pr.Device): + """PyRogue model for the finite-window ADC pattern tester. + + Parameters + ---------- + channels : int + Number of logical ADC channels. + fcoLanes : int + Number of frame-clock lanes. + sampleBits : int + Number of meaningful bits in each ADC sample. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + *, + channels: int, + fcoLanes: int, + sampleBits: int, + **kwargs: Any) -> None: + """Create the ADC pattern-tester model.""" + + for name, value, minimum, maximum in ( + ('channels', channels, 1, 16), + ('fcoLanes', fcoLanes, 1, 16), + ('sampleBits', sampleBits, 2, 16)): + if not minimum <= value <= maximum: + raise ValueError(f'{name} must be from {minimum} through {maximum}') + + kwargs.setdefault('description', 'Parallel finite-window ADC pattern measurement') + super().__init__(**kwargs) + + self.add(pr.RemoteCommand( + name = 'Start', + description = 'Start a finite pattern measurement window', + offset = 0x000, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.RemoteCommand( + name = 'Abort', + description = 'Abort the active pattern measurement window', + offset = 0x004, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.RemoteVariable( + name = 'Alternating', + description = 'Compare a shared-phase alternating A/B pattern instead of constant A', + offset = 0x008, + bitSize = 1, + bitOffset = 0, + mode = 'RW', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'ReferenceChannel', + description = 'Enabled logical channel used to acquire shared A/B phase', + offset = 0x008, + bitSize = 8, + bitOffset = 8, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 0, + maximum = channels-1)) + + self.add(pr.RemoteVariable( + name = 'ChannelMask', + description = 'Logical channels enabled for parallel pattern comparison', + offset = 0x00C, + bitSize = channels, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'FcoMask', + description = 'FCO lanes enabled for mismatch counting', + offset = 0x010, + bitSize = fcoLanes, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'DataMask', + description = 'Sample bits included in each comparison', + offset = 0x014, + bitSize = sampleBits, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'PatternA', + description = 'Constant pattern or first alternating pattern', + offset = 0x018, + bitSize = sampleBits, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'PatternB', + description = 'Second alternating pattern', + offset = 0x01C, + bitSize = sampleBits, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'Samples', + description = 'Number of valid sample groups requested for the measurement', + offset = 0x020, + bitSize = 32, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 1)) + + self.add(pr.RemoteVariable( + name = 'Timeout', + description = 'Consecutive capture clocks without sampleValid; zero disables timeout', + offset = 0x024, + bitSize = 32, + mode = 'RW', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'Busy', + description = 'A pattern measurement window is active', + offset = 0x028, + bitSize = 1, + bitOffset = 0, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'TimedOut', + description = 'The last window ended without sampleValid', + offset = 0x028, + bitSize = 1, + bitOffset = 1, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'ConfigError', + description = 'The last start had invalid configuration', + offset = 0x028, + bitSize = 1, + bitOffset = 2, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'Aborted', + description = 'The last window was aborted', + offset = 0x028, + bitSize = 1, + bitOffset = 3, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'PhaseAcquired', + description = 'Shared alternating-pattern phase was acquired', + offset = 0x028, + bitSize = 1, + bitOffset = 4, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AllChannelsPass', + description = 'Every enabled logical channel passed', + offset = 0x028, + bitSize = 1, + bitOffset = 5, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AllFcoPass', + description = 'Every enabled FCO lane was observed and had no mismatches', + offset = 0x028, + bitSize = 1, + bitOffset = 6, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'Sequence', + description = 'Completed pattern-window count including error and abort outcomes', + offset = 0x02C, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'CheckedSamples', + description = 'Valid sample groups checked by the completed or active window', + offset = 0x030, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'ChannelPassed', + description = 'One pass bit per enabled logical channel', + offset = 0x034, + bitSize = channels, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'FcoPassed', + description = 'One pass bit per observed, mismatch-free enabled FCO lane', + offset = 0x038, + bitSize = fcoLanes, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}')) + + self.addRemoteVariables( + name = 'WordErrorCount', + description = 'Saturating pattern word-error count for each logical channel', + number = channels, + stride = 4, + offset = 0x040, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}') + + self.addRemoteVariables( + name = 'BitErrorMask', + description = 'Accumulated failing sample-bit mask for each logical channel', + number = channels, + stride = 4, + offset = 0x080, + bitSize = sampleBits, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}') + + self.addRemoteVariables( + name = 'FcoErrorCount', + description = 'Saturating FCO mismatch count for each enabled FCO lane', + number = fcoLanes, + stride = 4, + offset = 0x0C0, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}') diff --git a/python/surf/devices/analog_devices/__init__.py b/python/surf/devices/analog_devices/__init__.py index b4f2e34ec7..102023b200 100644 --- a/python/surf/devices/analog_devices/__init__.py +++ b/python/surf/devices/analog_devices/__init__.py @@ -7,10 +7,15 @@ ## may be copied, modified, propagated, or distributed except according to ## the terms contained in the LICENSE.txt file. ############################################################################## -from surf.devices.analog_devices._Ad9249 import * -from surf.devices.analog_devices._Ad5780 import * -from surf.devices.analog_devices._AttHmc624 import * -from surf.devices.analog_devices._Adt7420 import * -from surf.devices.analog_devices._Ad9681 import * -from surf.devices.analog_devices._Ad5541 import * -from surf.devices.analog_devices._Ltm4664 import * +from surf.devices.analog_devices._AdcDdrPatternTester import * +from surf.devices.analog_devices._AdcDdr import * +from surf.devices.analog_devices._AdcDdrCalibration import * +from surf.devices.analog_devices._Ad9249 import * +from surf.devices.analog_devices._Ad9249Legacy import * +from surf.devices.analog_devices._Ad9252 import * +from surf.devices.analog_devices._Ad5780 import * +from surf.devices.analog_devices._AttHmc624 import * +from surf.devices.analog_devices._Adt7420 import * +from surf.devices.analog_devices._Ad9681 import * +from surf.devices.analog_devices._Ad5541 import * +from surf.devices.analog_devices._Ltm4664 import * diff --git a/setup.py b/setup.py index d8767c7aba..c4629eba3f 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ setup ( name='surf', version=pyVer, + python_requires='>=3.10', packages=['surf', 'surf/axi', 'surf/devices', @@ -53,4 +54,3 @@ 'surf/protocols/ssi', ], package_dir={'':'python'}, ) - From 33a422736e33466087944994bb29cc712dbb23b5 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 5 Aug 2026 14:53:57 -0700 Subject: [PATCH 03/57] test: add serialized ADC simulation coverage --- .../AnalogDevices/ad9249/sim/Ad9249Sim.vhd | 259 +++++ .../ad9249/sim/Ad9249SimCore.vhd | 323 ++++++ devices/AnalogDevices/ad9249/sim/ruckus.tcl | 14 + .../AnalogDevices/ad9249/tb/Ad9249Group.vhd | 2 +- .../ad9249/wrappers/Ad9249SimCoreWrapper.vhd | 60 ++ .../ad9249/wrappers/Ad9249SimWrapper.vhd | 85 ++ .../AnalogDevices/ad9252/sim/Ad9252Sim.vhd | 234 +++++ .../ad9252/sim/Ad9252SimCore.vhd | 283 +++++ devices/AnalogDevices/ad9252/sim/ruckus.tcl | 14 + .../ad9252/wrappers/Ad9252SimCoreWrapper.vhd | 60 ++ .../ad9252/wrappers/Ad9252SimWrapper.vhd | 85 ++ devices/AnalogDevices/ad9681/sim/Ad9681.vhd | 628 ----------- .../ad9681/sim/Ad9681Serializer.vhd | 74 -- .../AnalogDevices/ad9681/sim/Ad9681Sim.vhd | 272 +++++ .../ad9681/sim/Ad9681SimCore.vhd | 326 ++++++ devices/AnalogDevices/ad9681/sim/ruckus.tcl | 14 + .../ad9681/wrappers/Ad9681SimCoreWrapper.vhd | 62 ++ .../ad9681/wrappers/Ad9681SimWrapper.vhd | 90 ++ .../adcDdr/sim/AdcDdrPatternPkg.vhd | 97 ++ .../adcDdr/wrappers/AdcDdrCoreWrapper.vhd | 177 ++++ .../adcDdr/wrappers/AdcDdrPatternPkgTb.vhd | 43 + .../wrappers/AdcDdrPatternTesterWrapper.vhd | 126 +++ tests/devices/__init__.py | 0 tests/devices/analog_devices/__init__.py | 0 .../devices/analog_devices/test_Ad9249Sim.py | 151 +++ .../analog_devices/test_Ad9249SimCore.py | 188 ++++ .../analog_devices/test_Ad9249SimTiming.py | 129 +++ .../devices/analog_devices/test_Ad9252Sim.py | 143 +++ .../analog_devices/test_Ad9252SimCore.py | 174 ++++ .../analog_devices/test_Ad9252SimTiming.py | 129 +++ .../devices/analog_devices/test_Ad9681Sim.py | 144 +++ .../analog_devices/test_Ad9681SimCore.py | 201 ++++ .../analog_devices/test_Ad9681SimTiming.py | 133 +++ .../analog_devices/test_AdcDdrCalibration.py | 976 ++++++++++++++++++ .../devices/analog_devices/test_AdcDdrCore.py | 313 ++++++ .../analog_devices/test_AdcDdrModel.py | 533 ++++++++++ .../analog_devices/test_AdcDdrPatternPkg.py | 87 ++ .../test_AdcDdrPatternTester.py | 209 ++++ 38 files changed, 6135 insertions(+), 703 deletions(-) create mode 100644 devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd create mode 100644 devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd create mode 100644 devices/AnalogDevices/ad9249/sim/ruckus.tcl create mode 100644 devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd create mode 100644 devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd create mode 100644 devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd create mode 100644 devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd create mode 100644 devices/AnalogDevices/ad9252/sim/ruckus.tcl create mode 100644 devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd create mode 100644 devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd delete mode 100644 devices/AnalogDevices/ad9681/sim/Ad9681.vhd delete mode 100644 devices/AnalogDevices/ad9681/sim/Ad9681Serializer.vhd create mode 100644 devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd create mode 100644 devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd create mode 100644 devices/AnalogDevices/ad9681/sim/ruckus.tcl create mode 100644 devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd create mode 100644 devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd create mode 100644 devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd create mode 100644 devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd create mode 100644 devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd create mode 100644 devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd create mode 100644 tests/devices/__init__.py create mode 100644 tests/devices/analog_devices/__init__.py create mode 100644 tests/devices/analog_devices/test_Ad9249Sim.py create mode 100644 tests/devices/analog_devices/test_Ad9249SimCore.py create mode 100644 tests/devices/analog_devices/test_Ad9249SimTiming.py create mode 100644 tests/devices/analog_devices/test_Ad9252Sim.py create mode 100644 tests/devices/analog_devices/test_Ad9252SimCore.py create mode 100644 tests/devices/analog_devices/test_Ad9252SimTiming.py create mode 100644 tests/devices/analog_devices/test_Ad9681Sim.py create mode 100644 tests/devices/analog_devices/test_Ad9681SimCore.py create mode 100644 tests/devices/analog_devices/test_Ad9681SimTiming.py create mode 100644 tests/devices/analog_devices/test_AdcDdrCalibration.py create mode 100644 tests/devices/analog_devices/test_AdcDdrCore.py create mode 100644 tests/devices/analog_devices/test_AdcDdrModel.py create mode 100644 tests/devices/analog_devices/test_AdcDdrPatternPkg.py create mode 100644 tests/devices/analog_devices/test_AdcDdrPatternTester.py diff --git a/devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd b/devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd new file mode 100644 index 0000000000..45df47bef2 --- /dev/null +++ b/devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd @@ -0,0 +1,259 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free pin-level AD9249 device simulation +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9249Sim is + generic ( + TPD_G : time := 1 ns; + CLK_PERIOD_G : time := 24 ns; + DIVCLK_DIVIDE_G : integer := 1; + CLKFBOUT_MULT_G : integer := 49; + CLK_DCO_DIVIDE_G : integer := 49; + CLK_FCO_DIVIDE_G : integer := 7; + DATA_PHASE_G : time := 0 ns; + FCO_PHASE_G : time := 0 ns; + DATA_SKEW_G : TimeArray(15 downto 0) := (others => 0 ns); + FCO_SKEW_G : TimeArray(1 downto 0) := (others => 0 ns); + JITTER_G : time := 0 ns; + TIMING_BIAS_G : time := 0 ns); + port ( + clkP : in sl; + clkN : in sl; + vin : in RealArray(15 downto 0); + dP : out slv(15 downto 0); + dN : out slv(15 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + sclk : in sl; + sdio : inout sl; + csb : in slv(1 downto 0)); +end entity Ad9249Sim; + +architecture behavioral of Ad9249Sim is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant HALF_BIT_TIME_C : time := CLK_PERIOD_G/28; + constant CONVERSION_LATENCY_C : positive := 16; + + type Slv13Array is array (natural range <>) of slv(12 downto 0); + type NormalDataPipelineType is array (CONVERSION_LATENCY_C-2 downto 0) of + Slv16Array(15 downto 0); + + signal cfgWrEn : slv(1 downto 0); + signal cfgAddr : Slv13Array(1 downto 0); + signal cfgWrData : Slv32Array(1 downto 0); + signal cfgByteValid : Slv4Array(1 downto 0); + signal cfgRdByte : Slv8Array(1 downto 0); + signal cfgRdData : Slv32Array(1 downto 0); + signal normalData : Slv16Array(15 downto 0) := (others => (others => '0')); + signal normalDataPipeline : NormalDataPipelineType := (others => (others => (others => '0'))); + signal delayedNormalData : Slv16Array(15 downto 0); + signal sampleData : Slv16Array(15 downto 0); + signal sampleValid : slv(1 downto 0); + signal serialData : Slv8Array(1 downto 0); + signal serialDco : slv(1 downto 0); + signal serialFco : slv(1 downto 0); + +begin + + assert HALF_BIT_TIME_C > 0 ns + report "Ad9249Sim requires CLK_PERIOD_G >= 28 simulator time units" + severity failure; + + -- These clock-manager generics are retained for source compatibility with + -- the legacy model. Device timing is derived directly from CLK_PERIOD_G. + assert DIVCLK_DIVIDE_G > 0 and CLKFBOUT_MULT_G > 0 and + CLK_DCO_DIVIDE_G > 0 and CLK_FCO_DIVIDE_G > 0 + report "Ad9249Sim clock compatibility generics must be positive" + severity failure; + + assert JITTER_G >= 0 ns + report "Ad9249Sim requires nonnegative JITTER_G" + severity failure; + + assert TIMING_BIAS_G >= 0 ns + report "Ad9249Sim requires nonnegative TIMING_BIAS_G" + severity failure; + + GEN_DATA_TIMING_CHECK : for i in 15 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := DATA_PHASE_G+DATA_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9249Sim data timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_DATA_TIMING_CHECK; + + GEN_FCO_TIMING_CHECK : for i in 1 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := FCO_PHASE_G+FCO_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9249Sim FCO timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_FCO_TIMING_CHECK; + + GEN_NORMAL_DATA : for i in 15 downto 0 generate + adcConvert : process (vin(i)) is + variable analogInput : real; + begin + -- Real-valued board models can briefly produce NaN at time zero. + -- Substitute low scale because adcConversion() cannot clamp NaN. + if (vin(i) < 0.0) or (vin(i) >= 0.0) then + analogInput := vin(i); + else + analogInput := 0.0; + end if; + normalData(i) <= "00" & adcConversion(analogInput, 0.0, 2.0, 14, false); + end process adcConvert; + end generate GEN_NORMAL_DATA; + + ------------------------------------------------------------------------------------------------ + -- The AD9249 specifies 16 sample clocks of conversion latency. Fifteen + -- stages are explicit here; the coherent serializer-frame capture below + -- contributes the final sample clock at the output pins. Test patterns are + -- generated after this normal-conversion pipeline. + ------------------------------------------------------------------------------------------------ + conversionPipeline : process (clkP) is + begin + if rising_edge(clkP) then + normalDataPipeline(0) <= normalData after TPD_G; + for i in 1 to CONVERSION_LATENCY_C-2 loop + normalDataPipeline(i) <= normalDataPipeline(i-1) after TPD_G; + end loop; + end if; + end process conversionPipeline; + + delayedNormalData <= normalDataPipeline(CONVERSION_LATENCY_C-2); + + GEN_GROUP : for g in 1 downto 0 generate + constant LOW_CH_C : natural := 8*g; + constant HIGH_CH_C : natural := LOW_CH_C+7; + begin + + cfgRdData(g) <= x"000000" & cfgRdByte(g); + + U_Config : entity surf.AdiConfigSlave + generic map ( + TPD_G => TPD_G) + port map ( + clk => clkP, -- [in] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb(g), -- [in] + wrEn => cfgWrEn(g), -- [out] + rdEn => open, -- [out] + addr => cfgAddr(g), -- [out] + wrData => cfgWrData(g), -- [out] + byteValid => cfgByteValid(g), -- [out] + rdData => cfgRdData(g)); -- [in] + + U_Core : entity surf.Ad9249SimCore + generic map ( + TPD_G => TPD_G) + port map ( + sampleClk => clkP, -- [in] + sampleRst => '0', -- [in] + sampleEnable => '1', -- [in] + normalData => delayedNormalData(HIGH_CH_C downto LOW_CH_C), -- [in] + cfgWrEn => cfgWrEn(g), -- [in] + cfgAddr => cfgAddr(g)(8 downto 0), -- [in] + cfgWrData => cfgWrData(g)(7 downto 0), -- [in] + cfgRdData => cfgRdByte(g), -- [out] + sampleData => sampleData(HIGH_CH_C downto LOW_CH_C), -- [out] + sampleValid => sampleValid(g)); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Latch one coherent bank word per frame before applying transition-only + -- static timing and bounded alternating jitter. DCO remains binary and + -- jitter-free; the common bias makes negative jitter schedulable. + ------------------------------------------------------------------------------------------------ + serializer : process is + variable dco : sl := '0'; + variable frameData : Slv16Array(7 downto 0) := (others => (others => '0')); + variable dataCurrent : slv(7 downto 0) := (others => '0'); + variable fcoCurrent : sl := '0'; + variable dataJitterPositive : BooleanArray(7 downto 0) := (others => false); + variable fcoJitterPositive : boolean := false; + variable nextData : sl; + variable nextFco : sl; + variable edgeJitter : time; + begin + serialData(g) <= (others => '0'); + serialDco(g) <= '0'; + serialFco(g) <= '0'; + wait until rising_edge(clkP); + loop + -- sampleData updates after the encode edge. Capturing it here both + -- prevents checkerboard tearing and supplies the last latency cycle. + for ch in 7 downto 0 loop + frameData(ch) := sampleData(LOW_CH_C+ch); + end loop; + for bitindex in 13 downto 0 loop + for ch in 7 downto 0 loop + nextData := frameData(ch)(bitindex); + if (nextData /= dataCurrent(ch)) then + if (dataJitterPositive(ch)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + dataJitterPositive(ch) := not dataJitterPositive(ch); + serialData(g)(ch) <= transport nextData after + TIMING_BIAS_G+DATA_PHASE_G+ + DATA_SKEW_G(LOW_CH_C+ch)+edgeJitter; + dataCurrent(ch) := nextData; + end if; + end loop; + + nextFco := FRAME_PATTERN_C(bitindex); + if (nextFco /= fcoCurrent) then + if (fcoJitterPositive) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + fcoJitterPositive := not fcoJitterPositive; + serialFco(g) <= transport nextFco after + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(g)+edgeJitter; + fcoCurrent := nextFco; + end if; + + wait for HALF_BIT_TIME_C; + dco := not dco; + serialDco(g) <= transport dco after TIMING_BIAS_G; + wait for HALF_BIT_TIME_C; + end loop; + end loop; + end process serializer; + + end generate GEN_GROUP; + + dP <= serialData(1) & serialData(0); + dN <= not (serialData(1) & serialData(0)); + dcoP <= serialDco; + dcoN <= not serialDco; + fcoP <= serialFco; + fcoN <= not serialFco; + +end architecture behavioral; diff --git a/devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd b/devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd new file mode 100644 index 0000000000..1e2bf21f47 --- /dev/null +++ b/devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd @@ -0,0 +1,323 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free AD9249 output-bank simulation core +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity Ad9249SimCore is + generic ( + TPD_G : time := 1 ns); + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in Slv16Array(7 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out Slv16Array(7 downto 0); + sampleValid : out sl); +end entity Ad9249SimCore; + +architecture rtl of Ad9249SimCore is + + constant PN9_SEED_C : slv(8 downto 0) := "011011111"; + constant PN23_SEED_C : slv(22 downto 0) := "01001101110000000101000"; + + constant SPI_CONFIG_ADDR_C : slv(8 downto 0) := '0' & X"00"; + constant CHIP_ID_ADDR_C : slv(8 downto 0) := '0' & X"01"; + constant CHIP_GRADE_ADDR_C : slv(8 downto 0) := '0' & X"02"; + constant DEVICE_INDEX2_ADDR_C : slv(8 downto 0) := '0' & X"04"; + constant DEVICE_INDEX1_ADDR_C : slv(8 downto 0) := '0' & X"05"; + constant POWER_MODE_ADDR_C : slv(8 downto 0) := '0' & X"08"; + constant TEST_MODE_ADDR_C : slv(8 downto 0) := '0' & X"0D"; + constant OUTPUT_MODE_ADDR_C : slv(8 downto 0) := '0' & X"14"; + constant OUTPUT_PHASE_ADDR_C : slv(8 downto 0) := '0' & X"16"; + constant USER_PATTERN1_LSB_C : slv(8 downto 0) := '0' & X"19"; + constant USER_PATTERN1_MSB_C : slv(8 downto 0) := '0' & X"1A"; + constant USER_PATTERN2_LSB_C : slv(8 downto 0) := '0' & X"1B"; + constant USER_PATTERN2_MSB_C : slv(8 downto 0) := '0' & X"1C"; + constant SERIAL_OUTPUT_ADDR_C : slv(8 downto 0) := '0' & X"21"; + constant CHANNEL_STATUS_ADDR_C : slv(8 downto 0) := '0' & X"22"; + constant TRANSFER_ADDR_C : slv(8 downto 0) := '0' & X"FF"; + constant RESOLUTION_RATE_ADDR_C : slv(8 downto 0) := '1' & X"00"; + + type ChannelType is record + testMode : slv(3 downto 0); + userMode : slv(1 downto 0); + userPatternA : slv(13 downto 0); + userPatternB : slv(13 downto 0); + outputPhase : slv(3 downto 0); + outputReset : sl; + powerDown : sl; + resetPn9 : sl; + resetPn23 : sl; + pn9 : slv(8 downto 0); + pn23 : slv(22 downto 0); + end record ChannelType; + + constant CHANNEL_INIT_C : ChannelType := ( + testMode => "0000", + userMode => "00", + userPatternA => (others => '0'), + userPatternB => (others => '0'), + outputPhase => "0011", + outputReset => '0', + powerDown => '0', + resetPn9 => '0', + resetPn23 => '0', + pn9 => PN9_SEED_C, + pn23 => PN23_SEED_C); + + type ChannelArray is array (natural range <>) of ChannelType; + + type RegType is record + rdData : slv(7 downto 0); + selectMask : slv(9 downto 0); + channel : ChannelArray(7 downto 0); + outputInvert : sl; + outputFormat : sl; + lsbFirst : sl; + powerMode : slv(2 downto 0); + resolution : slv(6 downto 0); + tmpResolution : slv(6 downto 0); + toggle : sl; + data : Slv16Array(7 downto 0); + valid : sl; + end record RegType; + + constant REG_INIT_C : RegType := ( + rdData => (others => '0'), + selectMask => (others => '1'), + channel => (others => CHANNEL_INIT_C), + outputInvert => '0', + outputFormat => '0', + lsbFirst => '0', + powerMode => "000", + resolution => (others => '0'), + tmpResolution => (others => '0'), + toggle => '0', + data => (others => (others => '0')), + valid => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + ------------------------------------------------------------------------------------------------- + -- AD9249 writes take effect immediately except for the resolution/sample-rate + -- override at 0x100. The model represents one independently selected output + -- bank; a full device uses two instances. + ------------------------------------------------------------------------------------------------- + comb : process (cfgAddr, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is + variable active : ChannelType; + variable v : RegType; + variable word : slv(13 downto 0); + begin + v := r; + v.valid := '0'; + + if (sampleRst = '1') then + v := REG_INIT_C; + else + if (cfgWrEn = '1') then + case cfgAddr is + when SPI_CONFIG_ADDR_C => + if (cfgWrData(5) = '1' or cfgWrData(2) = '1') then + v := REG_INIT_C; + end if; + when DEVICE_INDEX2_ADDR_C => + v.selectMask(7 downto 4) := cfgWrData(3 downto 0); + when DEVICE_INDEX1_ADDR_C => + v.selectMask(3 downto 0) := cfgWrData(3 downto 0); + v.selectMask(9 downto 8) := cfgWrData(5 downto 4); + when POWER_MODE_ADDR_C => + v.powerMode := cfgWrData(2 downto 0); + when TEST_MODE_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userMode := cfgWrData(7 downto 6); + v.channel(i).resetPn23 := cfgWrData(5); + v.channel(i).resetPn9 := cfgWrData(4); + v.channel(i).testMode := cfgWrData(3 downto 0); + if (cfgWrData(4) = '1') then + v.channel(i).pn9 := PN9_SEED_C; + end if; + if (cfgWrData(5) = '1') then + v.channel(i).pn23 := PN23_SEED_C; + end if; + end if; + end loop; + when OUTPUT_MODE_ADDR_C => + v.outputInvert := cfgWrData(2); + v.outputFormat := cfgWrData(0); + when OUTPUT_PHASE_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).outputPhase := cfgWrData(3 downto 0); + end if; + end loop; + when USER_PATTERN1_LSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternA(7 downto 0) := cfgWrData; + end if; + end loop; + when USER_PATTERN1_MSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternA(13 downto 8) := cfgWrData(5 downto 0); + end if; + end loop; + when USER_PATTERN2_LSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternB(7 downto 0) := cfgWrData; + end if; + end loop; + when USER_PATTERN2_MSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternB(13 downto 8) := cfgWrData(5 downto 0); + end if; + end loop; + when SERIAL_OUTPUT_ADDR_C => + assert cfgWrData(2 downto 0) = "000" + report "Ad9249SimCore supports only 14-bit serial output" + severity failure; + v.lsbFirst := cfgWrData(7); + when CHANNEL_STATUS_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).outputReset := cfgWrData(1); + v.channel(i).powerDown := cfgWrData(0); + end if; + end loop; + when TRANSFER_ADDR_C => + if (cfgWrData(0) = '1') then + v.resolution := r.tmpResolution; + end if; + when RESOLUTION_RATE_ADDR_C => + v.tmpResolution := cfgWrData(6 downto 0); + when others => null; + end case; + end if; + + if (sampleEnable = '1') then + v.toggle := not r.toggle; + v.valid := '1'; + for i in 7 downto 0 loop + case r.channel(i).testMode is + when "0000" => + word := normalData(i)(13 downto 0); + if (r.outputFormat = '1') then + word(13) := not word(13); + end if; + when "0001" => word := "10000000000000"; + when "0010" => word := (others => '1'); + when "0011" => word := (others => '0'); + when "0100" => + for j in 13 downto 0 loop + word(j) := ite((j mod 2) = 0, r.toggle, not r.toggle); + end loop; + when "0101" => + word := adcDdrPn23Word(r.channel(i).pn23, 14); + if (r.channel(i).resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + else + v.channel(i).pn23 := adcDdrPn23Advance(r.channel(i).pn23, 14); + end if; + when "0110" => + word := adcDdrPn9Word(r.channel(i).pn9, 14); + if (r.channel(i).resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + else + v.channel(i).pn9 := adcDdrPn9Advance(r.channel(i).pn9, 14); + end if; + when "0111" => word := (others => r.toggle); + when "1000" => word := ite(r.toggle = '0', r.channel(i).userPatternA, + r.channel(i).userPatternB); + when "1001" => word := "10101010101010"; + when "1010" => word := "00000001111111"; + when "1011" => word := "10000000000000"; + when "1100" => word := "10100001100111"; + when others => word := (others => '0'); + end case; + if (r.outputInvert = '1') then + word := not word; + end if; + if (r.lsbFirst = '1') then + word := bitReverse(word); + end if; + if (r.powerMode /= "000" or r.channel(i).powerDown = '1' or + r.channel(i).outputReset = '1') then + word := (others => '0'); + end if; + v.data(i) := "00" & word; + end loop; + end if; + end if; + + -- Local-register reads return the lowest-numbered selected channel. This + -- is Channel A when the default mask selects all channels. + active := r.channel(0); + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + active := r.channel(i); + end if; + end loop; + v.rdData := (others => '0'); + case cfgAddr is + when SPI_CONFIG_ADDR_C => v.rdData := "00011000"; + when CHIP_ID_ADDR_C => v.rdData := X"92"; + when CHIP_GRADE_ADDR_C => v.rdData := X"30"; + when DEVICE_INDEX2_ADDR_C => v.rdData(3 downto 0) := r.selectMask(7 downto 4); + when DEVICE_INDEX1_ADDR_C => v.rdData(5 downto 0) := r.selectMask(9 downto 8) & r.selectMask(3 downto 0); + when POWER_MODE_ADDR_C => v.rdData(2 downto 0) := r.powerMode; + when TEST_MODE_ADDR_C => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; + when OUTPUT_MODE_ADDR_C => + v.rdData(2) := r.outputInvert; + v.rdData(0) := r.outputFormat; + when OUTPUT_PHASE_ADDR_C => v.rdData(3 downto 0) := active.outputPhase; + when USER_PATTERN1_LSB_C => v.rdData := active.userPatternA(7 downto 0); + when USER_PATTERN1_MSB_C => v.rdData(5 downto 0) := active.userPatternA(13 downto 8); + when USER_PATTERN2_LSB_C => v.rdData := active.userPatternB(7 downto 0); + when USER_PATTERN2_MSB_C => v.rdData(5 downto 0) := active.userPatternB(13 downto 8); + when SERIAL_OUTPUT_ADDR_C => v.rdData(7) := r.lsbFirst; + when CHANNEL_STATUS_ADDR_C => v.rdData(1 downto 0) := active.outputReset & active.powerDown; + when TRANSFER_ADDR_C => v.rdData := (others => '0'); + when RESOLUTION_RATE_ADDR_C => v.rdData(6 downto 0) := r.resolution; + when others => v.rdData := (others => '1'); + end case; + rin <= v; + end process comb; + + seq : process (sampleClk) is + begin + if rising_edge(sampleClk) then + r <= rin after TPD_G; + end if; + end process seq; + + cfgRdData <= rin.rdData; + sampleData <= r.data; + sampleValid <= r.valid; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/sim/ruckus.tcl b/devices/AnalogDevices/ad9249/sim/ruckus.tcl new file mode 100644 index 0000000000..876384e050 --- /dev/null +++ b/devices/AnalogDevices/ad9249/sim/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# This file is part of 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9249SimCore.vhd" +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9249Sim.vhd" diff --git a/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd b/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd index 3883ba05af..f4e4809fb9 100644 --- a/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd +++ b/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd @@ -93,7 +93,7 @@ architecture behavioral of Ad9249Group is clockDivRatio => "000", outputLvds => '0', outputInvert => '0', - binFormat => "00", + binFormat => "01", termination => "00", driveStrength => '0', lsbFirst => '0', diff --git a/devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd new file mode 100644 index 0000000000..9b3a169469 --- /dev/null +++ b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd @@ -0,0 +1,60 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.Ad9249SimCore +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9249SimCoreWrapper is + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in slv(127 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out slv(127 downto 0); + sampleValid : out sl); +end entity Ad9249SimCoreWrapper; + +architecture rtl of Ad9249SimCoreWrapper is + + signal normalArray : Slv16Array(7 downto 0); + signal sampleArray : Slv16Array(7 downto 0); + +begin + + GEN_FLATTEN : for i in 7 downto 0 generate + normalArray(i) <= normalData((i*16)+15 downto i*16); + sampleData((i*16)+15 downto i*16) <= sampleArray(i); + end generate GEN_FLATTEN; + + U_DUT : entity surf.Ad9249SimCore + port map ( + sampleClk => sampleClk, -- [in] + sampleRst => sampleRst, -- [in] + sampleEnable => sampleEnable, -- [in] + normalData => normalArray, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr, -- [in] + cfgWrData => cfgWrData, -- [in] + cfgRdData => cfgRdData, -- [out] + sampleData => sampleArray, -- [out] + sampleValid => sampleValid); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd new file mode 100644 index 0000000000..d1f9972edf --- /dev/null +++ b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd @@ -0,0 +1,85 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened cocotb wrapper for surf.Ad9249Sim +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9249SimWrapper is + generic ( + CLK_PERIOD_G : time := 24 ns; + DATA_PHASE_PS_G : natural := 0; + FCO_PHASE_PS_G : natural := 0; + DATA_LANE0_SKEW_PS_G : natural := 0; + FCO_LANE0_SKEW_PS_G : natural := 0; + JITTER_PS_G : natural := 0; + TIMING_BIAS_PS_G : natural := 0); + port ( + clkP : in sl; + clkN : in sl; + normalData : in slv(255 downto 0); + dP : out slv(15 downto 0); + dN : out slv(15 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + sclk : in sl; + sdioDrive : in sl; + sdioEnable : in sl; + sdioRead : out sl; + csb : in slv(1 downto 0)); +end entity Ad9249SimWrapper; + +architecture rtl of Ad9249SimWrapper is + + signal vin : RealArray(15 downto 0); + signal sdio : sl; + +begin + + GEN_INPUT : for i in 15 downto 0 generate + vin(i) <= real(to_integer(unsigned(normalData((16*i)+13 downto 16*i))))*(2.0/16384.0); + end generate GEN_INPUT; + + sdio <= sdioDrive when sdioEnable = '1' else 'Z'; + sdioRead <= to_x01z(sdio); + + U_DUT : entity surf.Ad9249Sim + generic map ( + CLK_PERIOD_G => CLK_PERIOD_G, + DATA_PHASE_G => DATA_PHASE_PS_G*1 ps, + FCO_PHASE_G => FCO_PHASE_PS_G*1 ps, + DATA_SKEW_G => (0 => DATA_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + FCO_SKEW_G => (0 => FCO_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + JITTER_G => JITTER_PS_G*1 ps, + TIMING_BIAS_G => TIMING_BIAS_PS_G*1 ps) + port map ( + clkP => clkP, -- [in] + clkN => clkN, -- [in] + vin => vin, -- [in] + dP => dP, -- [out] + dN => dN, -- [out] + dcoP => dcoP, -- [out] + dcoN => dcoN, -- [out] + fcoP => fcoP, -- [out] + fcoN => fcoN, -- [out] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb); -- [in] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd b/devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd new file mode 100644 index 0000000000..98ce78b155 --- /dev/null +++ b/devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd @@ -0,0 +1,234 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free pin-level AD9252 device simulation +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9252Sim is + generic ( + TPD_G : time := 1 ns; + CLK_PERIOD_G : time := 24 ns; + DATA_PHASE_G : time := 0 ns; + FCO_PHASE_G : time := 0 ns; + DATA_SKEW_G : TimeArray(7 downto 0) := (others => 0 ns); + FCO_SKEW_G : time := 0 ns; + JITTER_G : time := 0 ns; + TIMING_BIAS_G : time := 0 ns); + port ( + clkP : in sl; + clkN : in sl; + + vin : in RealArray(7 downto 0); + + dP : out slv(7 downto 0); + dN : out slv(7 downto 0); + dcoP : out sl; + dcoN : out sl; + fcoP : out sl; + fcoN : out sl; + + sclk : in sl; + sdio : inout sl; + csb : in sl); +end entity Ad9252Sim; + +architecture behavioral of Ad9252Sim is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant HALF_BIT_TIME_C : time := CLK_PERIOD_G/28; + constant CONVERSION_LATENCY_C : positive := 8; + + type NormalDataPipelineType is array (CONVERSION_LATENCY_C-2 downto 0) of + Slv16Array(7 downto 0); + + signal cfgWrEn : sl; + signal cfgAddr : slv(12 downto 0); + signal cfgWrData : slv(31 downto 0); + signal cfgByteValid : slv(3 downto 0); + signal cfgRdByte : slv(7 downto 0); + signal cfgRdData : slv(31 downto 0); + signal normalData : Slv16Array(7 downto 0) := (others => (others => '0')); + signal normalDataPipeline : NormalDataPipelineType := (others => (others => (others => '0'))); + signal delayedNormalData : Slv16Array(7 downto 0); + signal sampleData : Slv16Array(7 downto 0); + signal serialData : slv(7 downto 0); + signal serialDco : sl; + signal serialFco : sl; + +begin + + assert HALF_BIT_TIME_C > 0 ns + report "Ad9252Sim requires CLK_PERIOD_G >= 28 simulator time units" + severity failure; + + assert JITTER_G >= 0 ns + report "Ad9252Sim requires nonnegative JITTER_G" + severity failure; + + assert TIMING_BIAS_G >= 0 ns + report "Ad9252Sim requires nonnegative TIMING_BIAS_G" + severity failure; + + GEN_DATA_TIMING_CHECK : for i in 7 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := DATA_PHASE_G+DATA_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9252Sim data timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_DATA_TIMING_CHECK; + + assert TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G-JITTER_G >= 0 ns and + FCO_PHASE_G+FCO_SKEW_G+JITTER_G < HALF_BIT_TIME_C + report "Ad9252Sim FCO timing must be schedulable and precede the DCO edge" + severity failure; + + GEN_NORMAL_DATA : for i in 7 downto 0 generate + adcConvert : process (vin(i)) is + variable analogInput : real; + begin + -- Real-valued board models can briefly produce NaN at time zero. + -- Substitute low scale because adcConversion() cannot clamp NaN. + if (vin(i) < 0.0) or (vin(i) >= 0.0) then + analogInput := vin(i); + else + analogInput := 0.0; + end if; + normalData(i) <= "00" & adcConversion(analogInput, 0.0, 2.0, 14, false); + end process adcConvert; + end generate GEN_NORMAL_DATA; + + ------------------------------------------------------------------------------------------------ + -- The AD9252 specifies eight sample clocks of conversion latency. Seven + -- stages are explicit here; the coherent serializer-frame capture below + -- contributes the final sample clock at the output pins. Test patterns are + -- generated after this normal-conversion pipeline. + ------------------------------------------------------------------------------------------------ + conversionPipeline : process (clkP) is + begin + if rising_edge(clkP) then + normalDataPipeline(0) <= normalData after TPD_G; + for i in 1 to CONVERSION_LATENCY_C-2 loop + normalDataPipeline(i) <= normalDataPipeline(i-1) after TPD_G; + end loop; + end if; + end process conversionPipeline; + + delayedNormalData <= normalDataPipeline(CONVERSION_LATENCY_C-2); + + cfgRdData <= x"000000" & cfgRdByte; + + U_Config : entity surf.AdiConfigSlave + generic map ( + TPD_G => TPD_G) + port map ( + clk => clkP, -- [in] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb, -- [in] + wrEn => cfgWrEn, -- [out] + rdEn => open, -- [out] + addr => cfgAddr, -- [out] + wrData => cfgWrData, -- [out] + byteValid => cfgByteValid, -- [out] + rdData => cfgRdData); -- [in] + + U_Core : entity surf.Ad9252SimCore + generic map ( + TPD_G => TPD_G) + port map ( + sampleClk => clkP, -- [in] + sampleRst => '0', -- [in] + sampleEnable => '1', -- [in] + normalData => delayedNormalData, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr(7 downto 0), -- [in] + cfgWrData => cfgWrData(7 downto 0), -- [in] + cfgRdData => cfgRdByte, -- [out] + sampleData => sampleData, -- [out] + sampleValid => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Latch one coherent word per frame before applying transition-only static + -- timing and bounded alternating jitter. DCO remains binary and jitter-free; + -- the common bias makes negative jitter schedulable. + ------------------------------------------------------------------------------------------------ + serializer : process is + variable dco : sl := '0'; + variable frameData : Slv16Array(7 downto 0) := (others => (others => '0')); + variable dataCurrent : slv(7 downto 0) := (others => '0'); + variable fcoCurrent : sl := '0'; + variable dataJitterPositive : BooleanArray(7 downto 0) := (others => false); + variable fcoJitterPositive : boolean := false; + variable nextData : sl; + variable nextFco : sl; + variable edgeJitter : time; + begin + serialData <= (others => '0'); + serialDco <= '0'; + serialFco <= '0'; + wait until rising_edge(clkP); + loop + -- sampleData updates after the encode edge. Capturing it here both + -- prevents checkerboard tearing and supplies the last latency cycle. + frameData := sampleData; + for bitindex in 13 downto 0 loop + for ch in 7 downto 0 loop + nextData := frameData(ch)(bitindex); + if (nextData /= dataCurrent(ch)) then + if (dataJitterPositive(ch)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + dataJitterPositive(ch) := not dataJitterPositive(ch); + serialData(ch) <= transport nextData after + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(ch)+edgeJitter; + dataCurrent(ch) := nextData; + end if; + end loop; + + nextFco := FRAME_PATTERN_C(bitindex); + if (nextFco /= fcoCurrent) then + if (fcoJitterPositive) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + fcoJitterPositive := not fcoJitterPositive; + serialFco <= transport nextFco after + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G+edgeJitter; + fcoCurrent := nextFco; + end if; + + wait for HALF_BIT_TIME_C; + dco := not dco; + serialDco <= transport dco after TIMING_BIAS_G; + wait for HALF_BIT_TIME_C; + end loop; + end loop; + end process serializer; + + dP <= serialData; + dN <= not serialData; + dcoP <= serialDco; + dcoN <= not serialDco; + fcoP <= serialFco; + fcoN <= not serialFco; + +end architecture behavioral; diff --git a/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd b/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd new file mode 100644 index 0000000000..664b9cfdb2 --- /dev/null +++ b/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd @@ -0,0 +1,283 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free AD9252 register and digital output model +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity Ad9252SimCore is + generic ( + TPD_G : time := 1 ns); + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in Slv16Array(7 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(7 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out Slv16Array(7 downto 0); + sampleValid : out sl); +end entity Ad9252SimCore; + +architecture rtl of Ad9252SimCore is + + constant PN9_SEED_C : slv(8 downto 0) := "011011111"; + constant PN23_SEED_C : slv(22 downto 0) := "01001101110000000101000"; + + type ChannelType is record + testMode : slv(3 downto 0); + userMode : slv(1 downto 0); + userPatternA : slv(13 downto 0); + userPatternB : slv(13 downto 0); + outputPhase : slv(3 downto 0); + outputReset : sl; + powerDown : sl; + resetPn9 : sl; + resetPn23 : sl; + pn9 : slv(8 downto 0); + pn23 : slv(22 downto 0); + end record ChannelType; + + constant CHANNEL_INIT_C : ChannelType := ( + testMode => "0000", + userMode => "00", + userPatternA => (others => '0'), + userPatternB => (others => '0'), + outputPhase => "0011", + outputReset => '0', + powerDown => '0', + resetPn9 => '0', + resetPn23 => '0', + pn9 => PN9_SEED_C, + pn23 => PN23_SEED_C); + + type ChannelArray is array (natural range <>) of ChannelType; + + type RegType is record + rdData : slv(7 downto 0); + selectMask : slv(9 downto 0); + staged : ChannelType; + channel : ChannelArray(7 downto 0); + outputInvert : sl; + stagedInvert : sl; + lsbFirst : sl; + stagedLsb : sl; + toggle : sl; + data : Slv16Array(7 downto 0); + valid : sl; + end record RegType; + + constant REG_INIT_C : RegType := ( + rdData => (others => '0'), + selectMask => (others => '0'), + staged => CHANNEL_INIT_C, + channel => (others => CHANNEL_INIT_C), + outputInvert => '0', + stagedInvert => '0', + lsbFirst => '0', + stagedLsb => '0', + toggle => '0', + data => (others => (others => '0')), + valid => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + ------------------------------------------------------------------------------------------------- + -- Configuration writes and sample generation share sampleClk in this + -- logical model. Register writes update staging state; only device-update + -- register 0xFF publishes buffered settings to selected channels. + ------------------------------------------------------------------------------------------------- + comb : process (cfgAddr, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is + variable active : ChannelType; + variable v : RegType; + variable word : slv(13 downto 0); + begin + v := r; + v.valid := '0'; + + if (sampleRst = '1') then + v := REG_INIT_C; + else + ------------------------------------------------------------------------------------------- + -- AD9252 SPI-register behavior represented as a byte write port. + -- Device-index writes take effect immediately. Functional settings + -- are staged, matching the device's transfer/update requirement. + ------------------------------------------------------------------------------------------- + if (cfgWrEn = '1') then + case cfgAddr is + when X"00" => + if (cfgWrData(5) = '1' or cfgWrData(2) = '1') then + v := REG_INIT_C; + end if; + when X"04" => + v.selectMask(7 downto 4) := cfgWrData(3 downto 0); + when X"05" => + v.selectMask(3 downto 0) := cfgWrData(3 downto 0); + v.selectMask(9 downto 8) := cfgWrData(5 downto 4); + when X"0D" => + v.staged.userMode := cfgWrData(7 downto 6); + v.staged.resetPn23 := cfgWrData(5); + v.staged.resetPn9 := cfgWrData(4); + v.staged.testMode := cfgWrData(3 downto 0); + when X"14" => + v.stagedInvert := cfgWrData(2); + when X"16" => + v.staged.outputPhase := cfgWrData(3 downto 0); + when X"19" => + v.staged.userPatternA(7 downto 0) := cfgWrData; + when X"1A" => + v.staged.userPatternA(13 downto 8) := cfgWrData(5 downto 0); + when X"1B" => + v.staged.userPatternB(7 downto 0) := cfgWrData; + when X"1C" => + v.staged.userPatternB(13 downto 8) := cfgWrData(5 downto 0); + when X"21" => + assert cfgWrData(2 downto 0) = "000" + report "Ad9252SimCore supports only 14-bit serial output" + severity failure; + v.stagedLsb := cfgWrData(7); + when X"22" => + v.staged.outputReset := cfgWrData(1); + v.staged.powerDown := cfgWrData(0); + when X"FF" => + -- Atomically publish global settings and the staged + -- channel image to every channel selected by 0x04/0x05. + if (cfgWrData(0) = '1') then + v.outputInvert := r.stagedInvert; + v.lsbFirst := r.stagedLsb; + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i) := r.staged; + -- Preserve live PN state across ordinary + -- transfers. An asserted reset instead installs + -- and holds the documented seed value. + v.channel(i).pn9 := r.channel(i).pn9; + v.channel(i).pn23 := r.channel(i).pn23; + if (r.staged.resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + end if; + if (r.staged.resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + end if; + end if; + end loop; + end if; + when others => null; + end case; + end if; + + ------------------------------------------------------------------------------------------- + -- Generate one parallel ADC word per enabled sample. Pattern state + -- advances independently per channel, then global inversion/bit + -- order and per-channel suppression are applied in pin-data order. + ------------------------------------------------------------------------------------------- + if (sampleEnable = '1') then + v.toggle := not r.toggle; + v.valid := '1'; + for i in 7 downto 0 loop + case r.channel(i).testMode is + when "0000" => word := normalData(i)(13 downto 0); + when "0001" => word := "10000000000000"; + when "0010" => word := (others => '1'); + when "0011" => word := (others => '0'); + when "0100" => + for j in 13 downto 0 loop + word(j) := ite((j mod 2) = 0, r.toggle, not r.toggle); + end loop; + when "0101" => + word := adcDdrPn23Word(r.channel(i).pn23, 14); + if (r.channel(i).resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + else + v.channel(i).pn23 := adcDdrPn23Advance(r.channel(i).pn23, 14); + end if; + when "0110" => + word := adcDdrPn9Word(r.channel(i).pn9, 14); + if (r.channel(i).resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + else + v.channel(i).pn9 := adcDdrPn9Advance(r.channel(i).pn9, 14); + end if; + when "0111" => word := (others => r.toggle); + when "1000" => word := ite(r.toggle = '0', r.channel(i).userPatternA, + r.channel(i).userPatternB); + when "1001" => word := "10101010101010"; + when "1010" => word := "00000001111111"; + when "1011" => word := "10000000000000"; + when "1100" => word := "10100001100111"; + when others => word := (others => '0'); + end case; + if (r.outputInvert = '1') then + word := not word; + end if; + if (r.lsbFirst = '1') then + word := bitReverse(word); + end if; + if (r.channel(i).powerDown = '1' or r.channel(i).outputReset = '1') then + word := (others => '0'); + end if; + v.data(i) := "00" & word; + end loop; + end if; + end if; + + -- Local-register reads return the lowest-numbered selected channel. This + -- is Channel A when all channels are selected. + active := r.channel(0); + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + active := r.channel(i); + end if; + end loop; + v.rdData := (others => '0'); + case cfgAddr is + when X"00" => v.rdData := "00011000"; + when X"01" => v.rdData := X"09"; + when X"02" => v.rdData := X"30"; + when X"04" => v.rdData(3 downto 0) := r.selectMask(7 downto 4); + when X"05" => v.rdData(5 downto 0) := r.selectMask(9 downto 8) & r.selectMask(3 downto 0); + when X"0D" => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; + when X"14" => v.rdData(2) := r.outputInvert; + when X"16" => v.rdData(3 downto 0) := active.outputPhase; + when X"19" => v.rdData := active.userPatternA(7 downto 0); + when X"1A" => v.rdData(5 downto 0) := active.userPatternA(13 downto 8); + when X"1B" => v.rdData := active.userPatternB(7 downto 0); + when X"1C" => v.rdData(5 downto 0) := active.userPatternB(13 downto 8); + when X"21" => v.rdData(7) := r.lsbFirst; + when X"22" => v.rdData(1 downto 0) := active.outputReset & active.powerDown; + when others => v.rdData := (others => '1'); + end case; + rin <= v; + end process comb; + + seq : process (sampleClk) is + begin + if rising_edge(sampleClk) then + r <= rin after TPD_G; + end if; + end process seq; + + cfgRdData <= rin.rdData; + sampleData <= r.data; + sampleValid <= r.valid; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/sim/ruckus.tcl b/devices/AnalogDevices/ad9252/sim/ruckus.tcl new file mode 100644 index 0000000000..3079db2061 --- /dev/null +++ b/devices/AnalogDevices/ad9252/sim/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# This file is part of 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9252SimCore.vhd" +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9252Sim.vhd" diff --git a/devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd new file mode 100644 index 0000000000..b5d259bd1f --- /dev/null +++ b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd @@ -0,0 +1,60 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.Ad9252SimCore +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9252SimCoreWrapper is + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in slv(127 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(7 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out slv(127 downto 0); + sampleValid : out sl); +end entity Ad9252SimCoreWrapper; + +architecture rtl of Ad9252SimCoreWrapper is + + signal normalArray : Slv16Array(7 downto 0); + signal sampleArray : Slv16Array(7 downto 0); + +begin + + GEN_FLATTEN : for i in 7 downto 0 generate + normalArray(i) <= normalData((i*16)+15 downto i*16); + sampleData((i*16)+15 downto i*16) <= sampleArray(i); + end generate; + + U_DUT : entity surf.Ad9252SimCore + port map ( + sampleClk => sampleClk, -- [in] + sampleRst => sampleRst, -- [in] + sampleEnable => sampleEnable, -- [in] + normalData => normalArray, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr, -- [in] + cfgWrData => cfgWrData, -- [in] + cfgRdData => cfgRdData, -- [out] + sampleData => sampleArray, -- [out] + sampleValid => sampleValid); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd new file mode 100644 index 0000000000..ba6b15094a --- /dev/null +++ b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd @@ -0,0 +1,85 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened cocotb wrapper for surf.Ad9252Sim +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9252SimWrapper is + generic ( + CLK_PERIOD_G : time := 24 ns; + DATA_PHASE_PS_G : natural := 0; + FCO_PHASE_PS_G : natural := 0; + DATA_LANE0_SKEW_PS_G : natural := 0; + FCO_SKEW_PS_G : natural := 0; + JITTER_PS_G : natural := 0; + TIMING_BIAS_PS_G : natural := 0); + port ( + clkP : in sl; + clkN : in sl; + normalData : in slv(127 downto 0); + dP : out slv(7 downto 0); + dN : out slv(7 downto 0); + dcoP : out sl; + dcoN : out sl; + fcoP : out sl; + fcoN : out sl; + sclk : in sl; + sdioDrive : in sl; + sdioEnable : in sl; + sdioRead : out sl; + csb : in sl); +end entity Ad9252SimWrapper; + +architecture rtl of Ad9252SimWrapper is + + signal vin : RealArray(7 downto 0); + signal sdio : sl; + +begin + + GEN_INPUT : for i in 7 downto 0 generate + vin(i) <= real(to_integer(unsigned(normalData((16*i)+13 downto 16*i))))*(2.0/16384.0); + end generate GEN_INPUT; + + sdio <= sdioDrive when sdioEnable = '1' else 'Z'; + sdioRead <= sdio; + + U_DUT : entity surf.Ad9252Sim + generic map ( + CLK_PERIOD_G => CLK_PERIOD_G, + DATA_PHASE_G => DATA_PHASE_PS_G*1 ps, + FCO_PHASE_G => FCO_PHASE_PS_G*1 ps, + DATA_SKEW_G => (0 => DATA_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + FCO_SKEW_G => FCO_SKEW_PS_G*1 ps, + JITTER_G => JITTER_PS_G*1 ps, + TIMING_BIAS_G => TIMING_BIAS_PS_G*1 ps) + port map ( + clkP => clkP, -- [in] + clkN => clkN, -- [in] + vin => vin, -- [in] + dP => dP, -- [out] + dN => dN, -- [out] + dcoP => dcoP, -- [out] + dcoN => dcoN, -- [out] + fcoP => fcoP, -- [out] + fcoN => fcoN, -- [out] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb); -- [in] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681.vhd deleted file mode 100644 index 35ff84735c..0000000000 --- a/devices/AnalogDevices/ad9681/sim/Ad9681.vhd +++ /dev/null @@ -1,628 +0,0 @@ -------------------------------------------------------------------------------- --- Title : AD9681 Simulation Module -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory --- Platform : --- Standard : VHDL'93/02 -------------------------------------------------------------------------------- --- Description: -------------------------------------------------------------------------------- --- This file is part of SLAC Firmware Standard Library. It is subject to --- the license terms in the LICENSE.txt file found in the top-level directory --- of this distribution and at: --- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. --- No part of SLAC Firmware Standard Library, including this file, may be --- copied, modified, propagated, or distributed except according to the terms --- contained in the LICENSE.txt file. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_unsigned.all; -use ieee.std_logic_arith.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.TextUtilPkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681 is - - generic ( - TPD_G : time := 1 ns); - - port ( - clkP : in sl; - clkN : in sl; - - vin : in RealArray(7 downto 0); - - dP : out slv8array(1 downto 0); - dN : out slv8array(1 downto 0); - dcoP : out slv(1 downto 0); - dcoN : out slv(1 downto 0); - fcoP : out slv(1 downto 0); - fcoN : out slv(1 downto 0); - - sclk : in sl; - sdio : inout sl; - csb : in sl); -end entity Ad9681; - -architecture behavioral of Ad9681 is - - ------------------------------------------------------------------------------------------------- - -- Config and Sampling constant and signals - ------------------------------------------------------------------------------------------------- - constant PN_SHORT_TAPS_C : NaturalArray := (0 => 4, 1 => 8); -- X9+X5+1 - constant PN_SHORT_INIT_C : slv(8 downto 0) := "011011111"; - constant PN_LONG_TAPS_C : NaturalArray := (0 => 16, 1 => 22); -- X23+X18+1 - constant PN_LONG_INIT_C : slv(22 downto 0) := "01001101110000000101000"; - - -- ConfigSlave signals - signal wrEn : sl; - signal addr : slv(12 downto 0); - signal wrData : slv(31 downto 0); - signal byteValid : slv(3 downto 0); - - type GlobalConfigType is record - mode : slv(2 downto 0); - stabilizer : sl; - clockDivRatio : slv(2 downto 0); - outputLvds : sl; - outputInvert : sl; - termination : slv(1 downto 0); - driveStrength : sl; - lsbFirst : sl; - outputMode : slv(2 downto 0); - pllLowRateMode : sl; - sel2xFrame : sl; - bits : slv(1 downto 0); - binFormat : sl; - digitalFsAdj : slv(2 downto 0); - end record GlobalConfigType; - - constant GLOBAL_CONFIG_INIT_C : GlobalConfigType := ( - mode => "000", - stabilizer => '1', - clockDivRatio => "000", - outputLvds => '0', - outputInvert => '0', - termination => "00", - driveStrength => '0', - lsbFirst => '0', - outputMode => "011", - pllLowRateMode => '0', - sel2xFrame => '0', - bits => "00", - binFormat => '1', - digitalFsAdj => "100"); - - type ChannelConfigType is record - chopMode : sl; - pn23 : slv(22 downto 0); - resetPnLongGen : sl; - pn9 : slv(8 downto 0); - resetPnShortGen : sl; - userTestMode : slv(1 downto 0); - outputTestMode : slv(3 downto 0); - outputPhase : slv(3 downto 0); - inputPhase : slv(2 downto 0); - userPattern1 : slv(15 downto 0); - userPattern2 : slv(15 downto 0); - offsetAdjust : slv(7 downto 0); - outputReset : sl; - powerDown : sl; - end record ChannelConfigType; - - constant CHANNEL_CONFIG_INIT_C : ChannelConfigType := ( - chopMode => '0', - pn23 => PN_LONG_INIT_C, - resetPnLongGen => '0', - pn9 => PN_SHORT_INIT_C, - resetPnShortGen => '0', - userTestMode => "00", - outputTestMode => "0000", - outputPhase => "0011", - inputPhase => "000", - userPattern1 => X"0000", - userPattern2 => X"0000", - offsetAdjust => X"00", - outputReset => '0', - powerDown => '0'); - - type ChannelConfigArray is array (natural range <>) of ChannelConfigType; - - type DelayArray is array (15 downto 0) of RealArray(7 downto 0); - - type ConfigRegType is record - vinDelay : DelayArray; - sample : Slv16Array(7 downto 0); -- slv(13 downto 0); - rdData : slv(31 downto 0); - lsbFirst : sl; - softReset : sl; - channelConfigEn : slv(9 downto 0); - tmpGlobal : GlobalConfigType; - tmpChannel : ChannelConfigType; - global : GlobalConfigType; - channel : ChannelConfigArray(9 downto 0); - word : sl; - end record ConfigRegType; - - constant CONFIG_REG_INIT_C : ConfigRegType := ( - vinDelay => (others => (others => 0.0)), - sample => (others => "0000000000000000"), - rdData => X"00000000", - lsbFirst => '0', - softReset => '0', - channelConfigEn => "1111111111", - tmpGlobal => GLOBAL_CONFIG_INIT_C, - tmpChannel => CHANNEL_CONFIG_INIT_C, - global => GLOBAL_CONFIG_INIT_C, - channel => (others => CHANNEL_CONFIG_INIT_C), - word => '0'); - - signal r : ConfigRegType := CONFIG_REG_INIT_C; - signal rin : ConfigRegType; - - ------------------------------------------------------------------------------------------------- - -- Output constants and signals - ------------------------------------------------------------------------------------------------- --- constant DCLK_PERIOD_C : time := CLK_PERIOD_G / 7.0; - - signal pllRst : sl; - signal clk : sl; - signal locked : sl; - signal rst : sl; - signal clkFbOut : sl; - signal clkFbIn : sl; - signal dClkInt : sl; - signal dClk : sl; - signal fClkInt : sl; - signal fClk : sl; - signal dcoInt : sl; - signal dco : sl; - signal fcoInt : sl; - signal fco : sl; - signal serData : slv8array(1 downto 0); - -begin - - ------------------------------------------------------------------------------------------------- - -- Create local clocks - ------------------------------------------------------------------------------------------------- --- ClkRst_1 : entity surf.ClkRst --- generic map ( --- RST_HOLD_TIME_G => 50 us) --- port map ( --- rst => pllRst); - - process is - begin - pllRst <= '1'; - wait for 15 us; - pllRst <= '0'; - wait until locked = '0'; - end process; - - - CLK_BUFG : IBUFGDS - port map ( - I => clkP, - IB => clkN, - O => clk); - - plle2_adv_inst : PLLE2_ADV - generic map ( - BANDWIDTH => "HIGH", - COMPENSATION => "ZHOLD", - DIVCLK_DIVIDE => 1, - CLKFBOUT_MULT => 8, - CLKFBOUT_PHASE => 0.000, - CLKOUT0_DIVIDE => 8, - CLKOUT0_PHASE => 0.000, - CLKOUT0_DUTY_CYCLE => 0.500, - CLKOUT1_DIVIDE => 2, - CLKOUT1_PHASE => 0.000, - CLKOUT1_DUTY_CYCLE => 0.500, - CLKOUT2_DIVIDE => 2, - CLKOUT2_PHASE => 180.000, - CLKOUT2_DUTY_CYCLE => 0.500, - CLKOUT3_DIVIDE => 8, - CLKOUT3_PHASE => 0.000, - CLKOUT3_DUTY_CYCLE => 0.500, - CLKIN1_PERIOD => 8.0, - REF_JITTER1 => 0.010) - port map ( - -- Output clocks - CLKFBOUT => clkFbOut, - CLKOUT0 => fClkInt, - CLKOUT1 => dClkInt, - CLKOUT2 => dcoInt, -- Shifted serial clock for output - CLKOUT3 => fcoInt, - CLKOUT4 => open, - CLKOUT5 => open, - -- Input clock control - CLKFBIN => clkFbIn, - CLKIN1 => clk, - CLKIN2 => '0', - -- Tied to always select the primary input clock - CLKINSEL => '1', - -- Ports for dynamic reconfiguration - DADDR => (others => '0'), - DCLK => '0', - DEN => '0', - DI => (others => '0'), - DO => open, - DRDY => open, - DWE => '0', - -- Other control and status signals - LOCKED => locked, - PWRDWN => '0', - RST => pllRst); - - FB_BUFG : BUFG - port map ( - I => clkFbOut, - O => clkFbIn); - - FCLK_BUFG : BUFG - port map ( - I => fClkInt, - O => fClk); - - DCLK_BUFG : BUFG - port map ( - I => dClkInt, - O => dClk); - - DCO_BUFG : BUFG - port map ( - I => dcoInt, - O => dco); - - FCO_BUFG : BUFG - port map ( - I => fcoInt, - O => fco); - - RstSync_1 : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '1', - RELEASE_DELAY_G => 10) - port map ( - clk => fClk, - asyncRst => locked, - syncRst => rst); - - ------------------------------------------------------------------------------------------------- - -- Instantiate configuration interface - ------------------------------------------------------------------------------------------------- - AdiConfigSlave_1 : entity surf.AdiConfigSlave - generic map ( - TPD_G => TPD_G) - port map ( - clk => fClk, - sclk => sclk, - sdio => sdio, - csb => csb, - wrEn => wrEn, - rdEn => open, - addr => addr, - wrData => wrData, - byteValid => byteValid, - rdData => r.rdData); - - ------------------------------------------------------------------------------------------------- - -- Configuration register logic - ------------------------------------------------------------------------------------------------- - comb : process (addr, r, vin, wrData, wrEn) is - variable v : ConfigRegType; - variable activeChannel : ChannelConfigType; - variable zero : slv(13 downto 0) := (others => '0'); - begin - v := r; - - for i in 7 downto 0 loop - v.vinDelay(0)(i) := vin(i); - for j in 15 downto 1 loop - v.vinDelay(j)(i) := r.vinDelay(j-1)(i); - end loop; - end loop; - - ---------------------------------------------------------------------------------------------- - -- Configuration Registers - ---------------------------------------------------------------------------------------------- - activeChannel := r.channel(0); - for i in 9 downto 0 loop - if (r.channelConfigEn(i) = '1') then - activeChannel := r.channel(i); - end if; - end loop; - - - v.rdData := (others => '0'); - case (addr(7 downto 0)) is - - when X"00" => -- chip_port_config - v.rdData(6) := r.lsbFirst; - v.rdData(5) := r.softReset; - v.rdData(4) := '1'; - v.rdData(3) := '1'; - v.rdData(2) := r.softReset; - v.rdData(1) := r.lsbFirst; - if (wrEn = '1') then - v.lsbFirst := wrData(6) or wrData(1); - v.softReset := wrData(5) or wrData(2); - end if; - - when X"01" => -- chip_id - v.rdData(7 downto 0) := X"8F"; - - when X"02" => -- chip_grade - v.rdData(6 downto 4) := "110"; - - ------------------------------------------------------------------------------------------- --- when X"04" => -- device_index_2 --- v.rdData(3 downto 0) := r.channelConfigEn(7 downto 4); --- if (wrEn = '1') then --- v.channelConfigEn(7 downto 4) := wrData(3 downto 0); --- end if; - - when X"05" => -- device_index_1 - v.rdData(3 downto 0) := r.channelConfigEn(3 downto 0); - v.rdData(4) := r.channelConfigEn(8); - v.rdData(5) := r.channelConfigEn(9); - if (wrEn = '1') then - v.channelConfigEn(3 downto 0) := wrData(3 downto 0); - v.channelConfigEn(7 downto 4) := wrData(3 downto 0); -- Check this - v.channelConfigEn(8) := wrData(4); - v.channelConfigEn(9) := wrData(5); - end if; - - when X"FF" => -- device update - if (wrEn = '1') then - v.global := r.tmpGlobal; - for i in 9 downto 0 loop - if (r.channelConfigEn(i) = '1') then - v.channel(i) := r.tmpChannel; - if (r.tmpChannel.resetPnLongGen = '1') then - v.channel(i).pn23 := PN_LONG_INIT_C; - end if; - if (r.tmpChannel.resetPnShortGen = '1') then - v.channel(i).pn9 := PN_SHORT_INIT_C; - end if; - end if; - end loop; - end if; - - ------------------------------------------------------------------------------------------- - when X"08" => -- modes - v.rdData(1 downto 0) := r.global.mode(1 downto 0); - v.rdData(5) := r.global.mode(2); - if (wrEn = '1') then - v.tmpGlobal.mode(1 downto 0) := wrData(1 downto 0); - v.tmpGlobal.mode(2) := wrData(5); - end if; - - when X"09" => -- clock - v.rdData(0) := r.global.stabilizer; - if (wrEn = '1') then - v.tmpGlobal.stabilizer := wrData(0); - end if; - - when X"0B" => - v.rdData(2 downto 0) := r.global.clockDivRatio; - if (wrEn = '1') then - v.tmpGlobal.clockDivRatio := wrData(2 downto 0); - end if; - - when X"0C" => - v.rdData(2) := activeChannel.chopMode; - if (wrEn = '1') then - v.tmpChannel.chopMode := wrData(2); - end if; - - - when X"0D" => -- test_io - v.rdData(7 downto 6) := activeChannel.userTestMode; - v.rdData(5) := activeChannel.resetPnLongGen; - v.rdData(4) := activeChannel.resetPnShortGen; - v.rdData(3 downto 0) := activeChannel.outputTestMode; - if (wrEn = '1') then - v.tmpChannel.userTestMode := wrData(7 downto 6); - v.tmpChannel.resetPnLongGen := wrData(5); - v.tmpChannel.resetPnShortGen := wrData(4); - v.tmpChannel.outputTestMode := wrData(3 downto 0); - end if; - - when X"10" => - v.rdData(7 downto 0) := activeChannel.offsetAdjust; - if (wrEn = '1') then - v.tmpChannel.offsetAdjust := wrData(7 downto 0); - end if; - - - when X"14" => -- output_mode - v.rdData(6) := r.global.outputLvds; - v.rdData(2) := r.global.outputInvert; - v.rdData(0) := r.global.binFormat; - if (wrEn = '1') then - v.tmpGlobal.outputLvds := wrData(6); - v.tmpGlobal.outputInvert := wrData(2); - v.tmpGlobal.binFormat := wrData(0); - end if; - - when X"15" => -- output_adjust - -- Not sure if this is global - v.rdData(5 downto 4) := r.global.termination; - v.rdData(0) := r.global.driveStrength; - if (wrEn = '1') then - v.tmpGlobal.termination := wrData(5 downto 4); - v.tmpGlobal.driveStrength := wrData(0); - end if; - - when X"16" => -- output_phase - v.rdData(6 downto 4) := activeChannel.inputPhase; - v.rdData(3 downto 0) := activeChannel.outputPhase; - if (wrEn = '1') then - v.tmpChannel.inputPhase := wrData(6 downto 4); - v.tmpChannel.outputPhase := wrData(3 downto 0); - end if; - - when X"18" => - v.rdData(2 downto 0) := r.global.digitalFsAdj; - if (wrEn = '1') then - v.tmpGlobal.digitalFsAdj := wrData(2 downto 0); - end if; - - - when X"19" => -- user_patt1_lsb - v.rdData(7 downto 0) := activeChannel.userPattern1(7 downto 0); - if (wrEn = '1') then - v.tmpChannel.userPattern1(7 downto 0) := wrData(7 downto 0); - end if; - - when X"1A" => -- user_patt1_msb - v.rdData(7 downto 0) := activeChannel.userPattern1(15 downto 8); - if (wrEn = '1') then - v.tmpChannel.userPattern1(15 downto 8) := wrData(7 downto 0); - end if; - - when X"1B" => -- user_patt2_lsb - v.rdData(7 downto 0) := activeChannel.userPattern2(7 downto 0); - if (wrEn = '1') then - v.tmpChannel.userPattern2(7 downto 0) := wrData(7 downto 0); - end if; - - when X"1C" => -- user_patt2_msb - v.rdData(7 downto 0) := activeChannel.userPattern2(15 downto 8); - if (wrEn = '1') then - v.tmpChannel.userPattern2(15 downto 8) := wrData(7 downto 0); - end if; - - when X"21" => -- serial_control - v.rdData(7) := r.global.lsbFirst; - v.rdData(6 downto 4) := r.global.outputMode; - v.rdData(3) := r.global.pllLowRateMode; - v.rdData(2) := r.global.sel2xFrame; - v.rdData(1 downto 0) := r.global.bits; - if (wrEn = '1') then - v.tmpGlobal.lsbFirst := wrData(7); - v.tmpGlobal.outputMode := wrData(6 downto 4); - v.tmpGlobal.pllLowRateMode := wrData(3); - v.tmpGlobal.sel2xFrame := wrData(2); - v.tmpGlobal.bits := wrData(1 downto 0); - end if; - - when X"22" => -- serial_ch_stat - v.rdData(1) := activeChannel.outputReset; - v.rdData(0) := activeChannel.powerDown; - if (wrEn = '1') then - v.tmpChannel.outputReset := wrData(1); - v.tmpChannel.powerDown := wrData(0); - end if; - - when others => - v.rdData := (others => '1'); - - end case; - - ---------------------------------------------------------------------------------------------- - -- ADC Sampling - ---------------------------------------------------------------------------------------------- - v.word := not r.word; - for i in 7 downto 0 loop - if (r.channel(i).powerDown = '0') then - case (r.channel(i).outputTestMode) is - when "0000" => -- normal - v.sample(i) := adcConversion(r.vinDelay(15)(i), -1.0, 1.0, 14, toBoolean(r.global.binFormat)) & "00"; - - -- Emulate chip behavior at -1 - if (r.vinDelay(15)(i) >= 1.0 or r.vinDelay(15)(i) <= -1.0) then - v.sample(i) := X"8000"; - end if; - when "0001" => -- midscale short - v.sample(i) := "1000000000000000"; - when "0010" => -- +FS short - v.sample(i) := "1111111111111100"; - when "0011" => -- -FS short - v.sample(i) := "0000000000000000"; - when "0100" => -- checkerboard - v.sample(i) := ite(r.word = '0', "1010101010101000", "0101010101010100"); - when "0101" => -- pn23 (not implemented) - v.sample(i) := (others => '0'); --(scrambler(zero, r.pn23, PN_LONG_TAPS_C, v.pn23, v.sample(i)); - when "0110" => -- pn9 (not implemented) - v.sample(i) := (others => '0'); --scrambler(zero, r.pn9, PN_SHORT_TAPS_C, v.pn9, v.sample(i)); - when "0111" => -- one/zero toggle - v.sample(i) := ite(r.word = '0', "1111111111111100", "0000000000000000"); - when "1000" => -- user input - v.sample(i) := ite(r.word = '0', r.channel(i).userPattern1, r.channel(i).userPattern2); - when "1001" => -- 1/0 bit toggle - v.sample(i) := "1010101010101000"; - when "1010" => -- 1x sync - v.sample(i) := "0000000111111100"; - when "1011" => -- one bit high - v.sample(i) := "1000000000000000"; - when "1100" => -- mixed bit frequency - v.sample(i) := "1010000110011100"; - when others => - v.sample(i) := (others => '0'); - end case; - - else - v.sample(i) := (others => '0'); - end if; - end loop; - - rin <= v; - - end process comb; - - seq : process (fClk) is - begin - if (rising_edge(fClk)) then - r <= rin after TPD_G; - end if; - end process seq; - - ------------------------------------------------------------------------------------------------- - -- Output - ------------------------------------------------------------------------------------------------- - BYTE_GEN : for i in 1 downto 0 generate - DATA_SERIALIZER_GEN : for ch in 7 downto 0 generate - Ad9681Serializer_1 : entity surf.Ad9681Serializer - port map ( - clk => dClk, - clkDiv => fClk, - rst => rst, - iData => r.sample(ch)(i*8+7 downto i*8), - oData => serData(i)(ch)); - - DATA_OUT_BUFF : OBUFDS - port map ( - I => serData(i)(ch), - O => dP(i)(ch), - OB => dN(i)(ch)); - end generate DATA_SERIALIZER_GEN; - - - FCLK_OUT_BUFF : entity surf.ClkOutBufDiff - port map ( - clkIn => fco, - clkOutP => fcoP(i), - clkOutN => fcoN(i)); - - DCLK_OUT_BUFF : entity surf.ClkOutBufDiff - port map ( - clkIn => dco, - clkOutP => dcoP(i), - clkOutN => dcoN(i)); - end generate; - - -end architecture behavioral; diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681Serializer.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681Serializer.vhd deleted file mode 100644 index c04585b9c0..0000000000 --- a/devices/AnalogDevices/ad9681/sim/Ad9681Serializer.vhd +++ /dev/null @@ -1,74 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: 14 bit DDR deserializer using 7 series IDELAYE2 and ISERDESE2. -------------------------------------------------------------------------------- --- This file is part of 'SLAC Firmware Standard Library'. --- It is subject to the license terms in the LICENSE.txt file found in the --- top-level directory of this distribution and at: --- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. --- No part of 'SLAC Firmware Standard Library', including this file, --- may be copied, modified, propagated, or distributed except according to --- the terms contained in the LICENSE.txt file. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; - -library surf; -use surf.StdRtlPkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681Serializer is - port ( - clk : in sl; -- Serial High speed clock - clkDiv : in sl; -- Parallel low speed clock - rst : in sl; -- Reset - - iData : in slv(7 downto 0); - oData : out sl); -end entity Ad9681Serializer; - -architecture rtl of Ad9681Serializer is - -begin - - oserdese2_master : OSERDESE2 - generic map ( - DATA_RATE_OQ => "DDR", - DATA_RATE_TQ => "SDR", - DATA_WIDTH => 8, - TRISTATE_WIDTH => 1, - SERDES_MODE => "MASTER") - port map ( - D1 => iData(7), - D2 => iData(6), - D3 => iData(5), - D4 => iData(4), - D5 => iData(3), - D6 => iData(2), - D7 => iData(1), - D8 => iData(0), - T1 => '0', - T2 => '0', - T3 => '0', - T4 => '0', - SHIFTIN1 => '0', - SHIFTIN2 => '0', - SHIFTOUT1 => open, - SHIFTOUT2 => open, - OCE => '1', - CLK => clk, - CLKDIV => clkDiv, - OQ => oData, - TQ => open, - OFB => open, - TBYTEIN => '0', - TBYTEOUT => open, - TFB => open, - TCE => '0', - RST => rst); - -end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd new file mode 100644 index 0000000000..b5600a03e0 --- /dev/null +++ b/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd @@ -0,0 +1,272 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free pin-level AD9681 device simulation +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9681Sim is + generic ( + TPD_G : time := 1 ns; + CLK_PERIOD_G : time := 8 ns; + DATA_PHASE_G : time := 0 ns; + FCO_PHASE_G : time := 0 ns; + DATA_SKEW_G : TimeArray(15 downto 0) := (others => 0 ns); + FCO_SKEW_G : TimeArray(1 downto 0) := (others => 0 ns); + JITTER_G : time := 0 ns; + TIMING_BIAS_G : time := 0 ns); + port ( + clkP : in sl; + clkN : in sl; + + vin : in RealArray(7 downto 0); + + dP : out Slv8Array(1 downto 0); + dN : out Slv8Array(1 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + + sclk : in sl; + sdio : inout sl; + csb : in sl); +end entity Ad9681Sim; + +architecture behavioral of Ad9681Sim is + + constant FRAME_PATTERN_C : slv(7 downto 0) := "11110000"; + constant HALF_BIT_TIME_C : time := CLK_PERIOD_G/16; + constant CONVERSION_LATENCY_C : positive := 16; + + -- The coherent serializer-frame handoff contributes the final sample clock + -- of the ADC's specified 16-clock conversion latency. + type NormalDataPipelineType is array (CONVERSION_LATENCY_C-2 downto 0) of Slv16Array(7 downto 0); + + signal spiWrEn : sl; + signal cfgAddr : slv(12 downto 0); + signal cfgWrData : slv(31 downto 0); + signal cfgByteValid : slv(3 downto 0); + signal cfgRdByte : slv(7 downto 0); + signal cfgRdData : slv(31 downto 0); + signal cfgPending : sl := '0'; + signal coreBank : sl; + signal coreWrEn : sl; + signal normalData : Slv16Array(7 downto 0) := (others => (others => '0')); + signal normalDataPipeline : NormalDataPipelineType := (others => (others => (others => '0'))); + signal delayedNormalData : Slv16Array(7 downto 0); + signal sampleData : Slv16Array(7 downto 0); + signal serialData : Slv8Array(1 downto 0); + signal serialDco : sl; + signal serialFco : slv(1 downto 0); + +begin + + assert HALF_BIT_TIME_C > 0 ns + report "Ad9681Sim requires CLK_PERIOD_G >= 16 simulator time units" + severity failure; + + assert JITTER_G >= 0 ns + report "Ad9681Sim requires nonnegative JITTER_G" + severity failure; + + assert TIMING_BIAS_G >= 0 ns + report "Ad9681Sim requires nonnegative TIMING_BIAS_G" + severity failure; + + GEN_DATA_TIMING_CHECK : for i in 15 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := DATA_PHASE_G+DATA_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9681Sim data timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_DATA_TIMING_CHECK; + + GEN_FCO_TIMING_CHECK : for i in 1 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := FCO_PHASE_G+FCO_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9681Sim FCO timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_FCO_TIMING_CHECK; + + GEN_NORMAL_DATA : for i in 7 downto 0 generate + adcConvert : process (vin(i)) is + variable analogInput : real; + begin + -- Real-valued board models can produce a transient NaN while their + -- concurrent amplifier stages settle at time zero. NaN is unordered, + -- so both comparisons are false. Substitute the low-scale input before + -- calling adcConversion(), whose math_real clamp does not handle NaN. + if (vin(i) < 0.0) or (vin(i) >= 0.0) then + analogInput := vin(i); + else + analogInput := 0.0; + end if; + normalData(i) <= "00" & adcConversion(analogInput, 0.0, 2.0, 14, false); + end process adcConvert; + end generate GEN_NORMAL_DATA; + + ------------------------------------------------------------------------------------------------ + -- The AD9681 specifies 16 sample clocks of conversion latency. Fifteen + -- stages are explicit here; sampleData is then captured once per complete + -- serializer frame below, providing the sixteenth sample-clock delay at the + -- pins. Test patterns are generated after this analog conversion pipeline. + ------------------------------------------------------------------------------------------------ + conversionPipeline : process (clkP) is + begin + if rising_edge(clkP) then + normalDataPipeline(0) <= normalData after TPD_G; + for i in 1 to CONVERSION_LATENCY_C-2 loop + normalDataPipeline(i) <= normalDataPipeline(i-1) after TPD_G; + end loop; + end if; + end process conversionPipeline; + + delayedNormalData <= normalDataPipeline(CONVERSION_LATENCY_C-2); + + cfgRdData <= x"000000" & cfgRdByte; + + U_Config : entity surf.AdiConfigSlave + generic map ( + TPD_G => TPD_G) + port map ( + clk => clkP, -- [in] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb, -- [in] + wrEn => spiWrEn, -- [out] + rdEn => open, -- [out] + addr => cfgAddr, -- [out] + wrData => cfgWrData, -- [out] + byteValid => cfgByteValid, -- [out] + rdData => cfgRdData); -- [in] + + ------------------------------------------------------------------------------------------------ + -- The previous single-SPI model applies channel-index writes to both + -- four-channel halves. Replay each parsed write once per internal bank. + ------------------------------------------------------------------------------------------------ + cfgReplay : process (clkP) is + begin + if rising_edge(clkP) then + cfgPending <= spiWrEn after TPD_G; + end if; + end process cfgReplay; + + coreWrEn <= spiWrEn or cfgPending; + coreBank <= '0' when spiWrEn = '1' else '1'; + + U_Core : entity surf.Ad9681SimCore + generic map ( + TPD_G => TPD_G) + port map ( + sampleClk => clkP, -- [in] + sampleRst => '0', -- [in] + sampleEnable => '1', -- [in] + normalData => delayedNormalData, -- [in] + cfgBank => coreBank, -- [in] + cfgWrEn => coreWrEn, -- [in] + cfgAddr => cfgAddr(8 downto 0), -- [in] + cfgWrData => cfgWrData(7 downto 0), -- [in] + cfgRdData => cfgRdByte, -- [out] + sampleData => sampleData, -- [out] + sampleValid => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Each physical output group serializes one byte of every channel. DCO + -- remains binary and jitter-free. A common timing bias delays DCO, data, and + -- FCO equally so negative jitter remains schedulable without moving the + -- nominal sampling point. Data and FCO transitions also receive their + -- configured static phase/skew plus bounded deterministic jitter that + -- alternates between negative and positive displacement on each actual + -- transition. All pins remain binary so unknown values cannot escape through + -- system interfaces. + ------------------------------------------------------------------------------------------------ + serializer : process is + variable dco : sl := '0'; + variable frameData : Slv16Array(7 downto 0) := (others => (others => '0')); + variable dataCurrent : Slv8Array(1 downto 0) := (others => (others => '0')); + variable fcoCurrent : slv(1 downto 0) := (others => '0'); + variable dataJitterPositive : BooleanArray(15 downto 0) := (others => false); + variable fcoJitterPositive : BooleanArray(1 downto 0) := (others => false); + variable nextData : sl; + variable nextFco : sl; + variable edgeJitter : time; + begin + serialData <= (others => (others => '0')); + serialDco <= '0'; + serialFco <= (others => '0'); + wait until rising_edge(clkP); + loop + -- Capture one coherent output word per channel. sampleData updates + -- after the encode edge, so this also provides the last cycle of the + -- specified conversion latency without tearing alternating patterns. + frameData := sampleData; + for bitindex in 7 downto 0 loop + for grp in 1 downto 0 loop + for ch in 7 downto 0 loop + nextData := frameData(ch)(bitindex+(8*grp)); + if (nextData /= dataCurrent(grp)(ch)) then + if (dataJitterPositive((8*grp)+ch)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + dataJitterPositive((8*grp)+ch) := + not dataJitterPositive((8*grp)+ch); + serialData(grp)(ch) <= transport nextData after + TIMING_BIAS_G+DATA_PHASE_G+ + DATA_SKEW_G((8*grp)+ch)+edgeJitter; + dataCurrent(grp)(ch) := nextData; + end if; + end loop; + end loop; + + nextFco := FRAME_PATTERN_C(bitindex); + for grp in 1 downto 0 loop + if (nextFco /= fcoCurrent(grp)) then + if (fcoJitterPositive(grp)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + fcoJitterPositive(grp) := not fcoJitterPositive(grp); + serialFco(grp) <= transport nextFco after + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(grp)+edgeJitter; + fcoCurrent(grp) := nextFco; + end if; + end loop; + + wait for HALF_BIT_TIME_C; + dco := not dco; + serialDco <= transport dco after TIMING_BIAS_G; + wait for HALF_BIT_TIME_C; + end loop; + end loop; + end process serializer; + + dP <= serialData; + dN <= (not serialData(1), not serialData(0)); + dcoP <= (others => serialDco); + dcoN <= (others => not serialDco); + fcoP <= serialFco; + fcoN <= not serialFco; + +end architecture behavioral; diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd new file mode 100644 index 0000000000..9aa9ca8e64 --- /dev/null +++ b/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd @@ -0,0 +1,326 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free AD9681 digital output model +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity Ad9681SimCore is + generic ( + TPD_G : time := 1 ns); + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in Slv16Array(7 downto 0); + cfgBank : in sl; + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out Slv16Array(7 downto 0); + sampleValid : out sl); +end entity Ad9681SimCore; + +architecture rtl of Ad9681SimCore is + + constant PN9_SEED_C : slv(8 downto 0) := "011011111"; + constant PN23_SEED_C : slv(22 downto 0) := "01001101110000000101000"; + + constant SPI_CONFIG_ADDR_C : slv(8 downto 0) := '0' & X"00"; + constant CHIP_ID_ADDR_C : slv(8 downto 0) := '0' & X"01"; + constant CHIP_GRADE_ADDR_C : slv(8 downto 0) := '0' & X"02"; + constant DEVICE_INDEX_ADDR_C : slv(8 downto 0) := '0' & X"05"; + constant POWER_MODE_ADDR_C : slv(8 downto 0) := '0' & X"08"; + constant TEST_MODE_ADDR_C : slv(8 downto 0) := '0' & X"0D"; + constant OUTPUT_MODE_ADDR_C : slv(8 downto 0) := '0' & X"14"; + constant USER_PATTERN1_LSB_C : slv(8 downto 0) := '0' & X"19"; + constant USER_PATTERN1_MSB_C : slv(8 downto 0) := '0' & X"1A"; + constant USER_PATTERN2_LSB_C : slv(8 downto 0) := '0' & X"1B"; + constant USER_PATTERN2_MSB_C : slv(8 downto 0) := '0' & X"1C"; + constant SERIAL_OUTPUT_ADDR_C : slv(8 downto 0) := '0' & X"21"; + constant CHANNEL_STATUS_ADDR_C : slv(8 downto 0) := '0' & X"22"; + constant TRANSFER_ADDR_C : slv(8 downto 0) := '0' & X"FF"; + constant RESOLUTION_RATE_ADDR_C : slv(8 downto 0) := '1' & X"00"; + + type GlobalType is record + powerMode : slv(2 downto 0); + outputInvert : sl; + outputFormat : sl; + lsbFirst : sl; + outputMode : slv(2 downto 0); + select2x : sl; + outputBits : slv(1 downto 0); + end record GlobalType; + + constant GLOBAL_INIT_C : GlobalType := ( + powerMode => "000", + outputInvert => '0', + outputFormat => '0', + lsbFirst => '0', + outputMode => "011", + select2x => '0', + outputBits => "00"); + + type ChannelType is record + testMode : slv(3 downto 0); + userMode : slv(1 downto 0); + userPatternA : slv(15 downto 0); + userPatternB : slv(15 downto 0); + outputReset : sl; + powerDown : sl; + resetPn9 : sl; + resetPn23 : sl; + pn9 : slv(8 downto 0); + pn23 : slv(22 downto 0); + end record ChannelType; + + constant CHANNEL_INIT_C : ChannelType := ( + testMode => "0000", + userMode => "00", + userPatternA => (others => '0'), + userPatternB => (others => '0'), + outputReset => '0', + powerDown => '0', + resetPn9 => '0', + resetPn23 => '0', + pn9 => PN9_SEED_C, + pn23 => PN23_SEED_C); + + type ChannelArray is array (natural range <>) of ChannelType; + type SelectArray is array (natural range <>) of slv(3 downto 0); + + type RegType is record + rdData : slv(7 downto 0); + selectMask : SelectArray(1 downto 0); + global : GlobalType; + channel : ChannelArray(7 downto 0); + resolution : slv(6 downto 0); + tmpResolution : slv(6 downto 0); + toggle : sl; + data : Slv16Array(7 downto 0); + valid : sl; + end record RegType; + + constant REG_INIT_C : RegType := ( + rdData => (others => '0'), + selectMask => (others => (others => '1')), + global => GLOBAL_INIT_C, + channel => (others => CHANNEL_INIT_C), + resolution => (others => '0'), + tmpResolution => (others => '0'), + toggle => '0', + data => (others => (others => '0')), + valid => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + ------------------------------------------------------------------------------------------------- + -- Each configuration bank owns four channels. AD9681 writes take effect + -- immediately except for the resolution/sample-rate override at 0x100. + ------------------------------------------------------------------------------------------------- + comb : process (cfgAddr, cfgBank, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is + variable active : ChannelType; + variable bankBase : natural range 0 to 4; + variable bankIndex : natural range 0 to 1; + variable v : RegType; + variable word : slv(15 downto 0); + variable code : slv(13 downto 0); + begin + v := r; + v.valid := '0'; + bankBase := ite(cfgBank = '1', 4, 0); + bankIndex := ite(cfgBank = '1', 1, 0); + + if (sampleRst = '1') then + v := REG_INIT_C; + else + if (cfgWrEn = '1') then + case cfgAddr is + when SPI_CONFIG_ADDR_C => + if (cfgWrData(5) = '1' or cfgWrData(2) = '1') then + v := REG_INIT_C; + end if; + when DEVICE_INDEX_ADDR_C => + v.selectMask(bankIndex) := cfgWrData(3 downto 0); + when POWER_MODE_ADDR_C => + v.global.powerMode(1 downto 0) := cfgWrData(1 downto 0); + v.global.powerMode(2) := cfgWrData(5); + when TEST_MODE_ADDR_C => + for i in 3 downto 0 loop + if (r.selectMask(bankIndex)(i) = '1') then + v.channel(bankBase+i).userMode := cfgWrData(7 downto 6); + v.channel(bankBase+i).resetPn23 := cfgWrData(5); + v.channel(bankBase+i).resetPn9 := cfgWrData(4); + v.channel(bankBase+i).testMode := cfgWrData(3 downto 0); + if (cfgWrData(4) = '1') then + v.channel(bankBase+i).pn9 := PN9_SEED_C; + end if; + if (cfgWrData(5) = '1') then + v.channel(bankBase+i).pn23 := PN23_SEED_C; + end if; + end if; + end loop; + when OUTPUT_MODE_ADDR_C => + v.global.outputInvert := cfgWrData(2); + v.global.outputFormat := cfgWrData(0); + when USER_PATTERN1_LSB_C | USER_PATTERN1_MSB_C | + USER_PATTERN2_LSB_C | USER_PATTERN2_MSB_C => + for i in 3 downto 0 loop + if (r.selectMask(bankIndex)(i) = '1') then + case cfgAddr is + when USER_PATTERN1_LSB_C => v.channel(bankBase+i).userPatternA(7 downto 0) := cfgWrData; + when USER_PATTERN1_MSB_C => v.channel(bankBase+i).userPatternA(15 downto 8) := cfgWrData; + when USER_PATTERN2_LSB_C => v.channel(bankBase+i).userPatternB(7 downto 0) := cfgWrData; + when others => v.channel(bankBase+i).userPatternB(15 downto 8) := cfgWrData; + end case; + end if; + end loop; + when SERIAL_OUTPUT_ADDR_C => + assert cfgWrData(6 downto 4) = "011" and cfgWrData(2) = '0' and + cfgWrData(1 downto 0) = "00" + report "Ad9681SimCore does not model this output format; continuing with " & + "16-bit DDR two-lane bytewise output" + severity warning; + v.global.lsbFirst := cfgWrData(7); + v.global.outputMode := cfgWrData(6 downto 4); + v.global.select2x := cfgWrData(2); + v.global.outputBits := cfgWrData(1 downto 0); + when CHANNEL_STATUS_ADDR_C => + for i in 3 downto 0 loop + if (r.selectMask(bankIndex)(i) = '1') then + v.channel(bankBase+i).outputReset := cfgWrData(1); + v.channel(bankBase+i).powerDown := cfgWrData(0); + end if; + end loop; + when TRANSFER_ADDR_C => + if (cfgWrData(0) = '1') then + v.resolution := r.tmpResolution; + end if; + when RESOLUTION_RATE_ADDR_C => + v.tmpResolution := cfgWrData(6 downto 0); + when others => null; + end case; + end if; + + if (sampleEnable = '1') then + v.toggle := not r.toggle; + v.valid := '1'; + for i in 7 downto 0 loop + case r.channel(i).testMode is + when "0000" => + code := normalData(i)(13 downto 0); + if (r.global.outputFormat = '1') then + code(13) := not code(13); + end if; + word := code & "00"; + when "0001" => word := "1000000000000000"; + when "0010" => word := "1111111111111100"; + when "0011" => word := (others => '0'); + when "0100" => word := ite(r.toggle = '0', "1010101010101000", "0101010101010100"); + when "0101" => + word := adcDdrPn23Word(r.channel(i).pn23, 14) & "00"; + if (r.channel(i).resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + else + v.channel(i).pn23 := adcDdrPn23Advance(r.channel(i).pn23, 14); + end if; + when "0110" => + word := adcDdrPn9Word(r.channel(i).pn9, 14) & "00"; + if (r.channel(i).resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + else + v.channel(i).pn9 := adcDdrPn9Advance(r.channel(i).pn9, 14); + end if; + when "0111" => word := ite(r.toggle = '0', "1111111111111100", "0000000000000000"); + when "1000" => word := ite(r.toggle = '0', r.channel(i).userPatternA, + r.channel(i).userPatternB); + when "1001" => word := "1010101010101000"; + when "1010" => word := "0000000111111100"; + when "1011" => word := "1000000000000000"; + when "1100" => word := "1010000110011100"; + when others => word := (others => '0'); + end case; + if (r.global.outputInvert = '1') then + word := not word; + end if; + if (r.global.lsbFirst = '1') then + word(15 downto 8) := bitReverse(word(15 downto 8)); + word(7 downto 0) := bitReverse(word(7 downto 0)); + end if; + if (r.global.powerMode /= "000" or r.channel(i).powerDown = '1' or + r.channel(i).outputReset = '1') then + word := (others => '0'); + end if; + v.data(i) := word; + end loop; + end if; + end if; + + -- Local-register reads return the lowest-numbered selected channel. This + -- is Channel A when the default mask selects all four channels. + active := r.channel(bankBase); + for i in 3 downto 0 loop + if (r.selectMask(bankIndex)(i) = '1') then + active := r.channel(bankBase+i); + end if; + end loop; + v.rdData := (others => '0'); + case cfgAddr is + when SPI_CONFIG_ADDR_C => v.rdData := "00011000"; + when CHIP_ID_ADDR_C => v.rdData := X"8F"; + when CHIP_GRADE_ADDR_C => v.rdData(6 downto 4) := "110"; + when DEVICE_INDEX_ADDR_C => v.rdData(3 downto 0) := r.selectMask(bankIndex); + when POWER_MODE_ADDR_C => + v.rdData(1 downto 0) := r.global.powerMode(1 downto 0); + v.rdData(5) := r.global.powerMode(2); + when TEST_MODE_ADDR_C => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; + when OUTPUT_MODE_ADDR_C => + v.rdData(2) := r.global.outputInvert; + v.rdData(0) := r.global.outputFormat; + when USER_PATTERN1_LSB_C => v.rdData := active.userPatternA(7 downto 0); + when USER_PATTERN1_MSB_C => v.rdData := active.userPatternA(15 downto 8); + when USER_PATTERN2_LSB_C => v.rdData := active.userPatternB(7 downto 0); + when USER_PATTERN2_MSB_C => v.rdData := active.userPatternB(15 downto 8); + when SERIAL_OUTPUT_ADDR_C => + v.rdData(7) := r.global.lsbFirst; + v.rdData(6 downto 4) := r.global.outputMode; + v.rdData(2) := r.global.select2x; + v.rdData(1 downto 0) := r.global.outputBits; + when CHANNEL_STATUS_ADDR_C => v.rdData(1 downto 0) := active.outputReset & active.powerDown; + when TRANSFER_ADDR_C => v.rdData := (others => '0'); + when RESOLUTION_RATE_ADDR_C => v.rdData(6 downto 0) := r.resolution; + when others => v.rdData := (others => '1'); + end case; + rin <= v; + end process comb; + + seq : process (sampleClk) is + begin + if rising_edge(sampleClk) then + r <= rin after TPD_G; + end if; + end process seq; + + cfgRdData <= rin.rdData; + sampleData <= r.data; + sampleValid <= r.valid; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/sim/ruckus.tcl b/devices/AnalogDevices/ad9681/sim/ruckus.tcl new file mode 100644 index 0000000000..961d78c6dd --- /dev/null +++ b/devices/AnalogDevices/ad9681/sim/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# This file is part of 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9681SimCore.vhd" +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9681Sim.vhd" diff --git a/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd new file mode 100644 index 0000000000..7a0df5b4d4 --- /dev/null +++ b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd @@ -0,0 +1,62 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.Ad9681SimCore +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9681SimCoreWrapper is + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in slv(127 downto 0); + cfgBank : in sl; + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out slv(127 downto 0); + sampleValid : out sl); +end entity Ad9681SimCoreWrapper; + +architecture rtl of Ad9681SimCoreWrapper is + + signal normalArray : Slv16Array(7 downto 0); + signal sampleArray : Slv16Array(7 downto 0); + +begin + + GEN_FLATTEN : for i in 7 downto 0 generate + normalArray(i) <= normalData((i*16)+15 downto i*16); + sampleData((i*16)+15 downto i*16) <= sampleArray(i); + end generate GEN_FLATTEN; + + U_DUT : entity surf.Ad9681SimCore + port map ( + sampleClk => sampleClk, -- [in] + sampleRst => sampleRst, -- [in] + sampleEnable => sampleEnable, -- [in] + normalData => normalArray, -- [in] + cfgBank => cfgBank, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr, -- [in] + cfgWrData => cfgWrData, -- [in] + cfgRdData => cfgRdData, -- [out] + sampleData => sampleArray, -- [out] + sampleValid => sampleValid); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd new file mode 100644 index 0000000000..63db988dc9 --- /dev/null +++ b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd @@ -0,0 +1,90 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened cocotb wrapper for surf.Ad9681Sim +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9681SimWrapper is + generic ( + CLK_PERIOD_G : time := 8 ns; + DATA_PHASE_PS_G : natural := 0; + FCO_PHASE_PS_G : natural := 0; + DATA_LANE0_SKEW_PS_G : natural := 0; + FCO_LANE0_SKEW_PS_G : natural := 0; + JITTER_PS_G : natural := 0; + TIMING_BIAS_PS_G : natural := 0); + port ( + clkP : in sl; + clkN : in sl; + normalData : in slv(127 downto 0); + dP : out slv(15 downto 0); + dN : out slv(15 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + sclk : in sl; + sdioDrive : in sl; + sdioEnable : in sl; + sdioRead : out sl; + csb : in sl); +end entity Ad9681SimWrapper; + +architecture rtl of Ad9681SimWrapper is + + signal vin : RealArray(7 downto 0); + signal dataP : Slv8Array(1 downto 0); + signal dataN : Slv8Array(1 downto 0); + signal sdio : sl; + +begin + + GEN_INPUT : for i in 7 downto 0 generate + vin(i) <= real(to_integer(unsigned(normalData((16*i)+13 downto 16*i))))*(2.0/16384.0); + end generate GEN_INPUT; + + dP <= dataP(1) & dataP(0); + dN <= dataN(1) & dataN(0); + + sdio <= sdioDrive when sdioEnable = '1' else 'Z'; + sdioRead <= sdio; + + U_DUT : entity surf.Ad9681Sim + generic map ( + CLK_PERIOD_G => CLK_PERIOD_G, + DATA_PHASE_G => DATA_PHASE_PS_G*1 ps, + FCO_PHASE_G => FCO_PHASE_PS_G*1 ps, + DATA_SKEW_G => (0 => DATA_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + FCO_SKEW_G => (0 => FCO_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + JITTER_G => JITTER_PS_G*1 ps, + TIMING_BIAS_G => TIMING_BIAS_PS_G*1 ps) + port map ( + clkP => clkP, -- [in] + clkN => clkN, -- [in] + vin => vin, -- [in] + dP => dataP, -- [out] + dN => dataN, -- [out] + dcoP => dcoP, -- [out] + dcoN => dcoN, -- [out] + fcoP => fcoP, -- [out] + fcoN => fcoN, -- [out] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb); -- [in] + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd b/devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd new file mode 100644 index 0000000000..eb2640b16f --- /dev/null +++ b/devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd @@ -0,0 +1,97 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Shared PN-sequence helpers for serialized DDR ADC models +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +package AdcDdrPatternPkg is + + function adcDdrPn9Next (state : slv(8 downto 0)) return slv; + function adcDdrPn23Next (state : slv(22 downto 0)) return slv; + function adcDdrPn9Advance (state : slv(8 downto 0); count : positive) return slv; + function adcDdrPn23Advance (state : slv(22 downto 0); count : positive) return slv; + function adcDdrPn9Word (state : slv(8 downto 0); width : positive) return slv; + function adcDdrPn23Word (state : slv(22 downto 0); width : positive) return slv; + +end package AdcDdrPatternPkg; + +package body AdcDdrPatternPkg is + + -- The PN helpers use an MSB-first Fibonacci representation throughout. A + -- caller can therefore generate the word visible on serialized ADC pins and + -- obtain the next state by advancing exactly the same number of bits. + function adcDdrPn9Next (state : slv(8 downto 0)) return slv is + variable result : slv(8 downto 0); + begin + -- Fibonacci realization of x^9 + x^5 + 1. The current MSB is the + -- serialized output bit and the register shifts toward the MSB. + result := state(7 downto 0) & (state(8) xor state(4)); + return result; + end function adcDdrPn9Next; + + function adcDdrPn23Next (state : slv(22 downto 0)) return slv is + variable result : slv(22 downto 0); + begin + -- Fibonacci realization of x^23 + x^18 + 1. + result := state(21 downto 0) & (state(22) xor state(17)); + return result; + end function adcDdrPn23Next; + + function adcDdrPn9Advance (state : slv(8 downto 0); count : positive) return slv is + variable result : slv(8 downto 0) := state; + begin + for i in 1 to count loop + result := adcDdrPn9Next(result); + end loop; + return result; + end function adcDdrPn9Advance; + + function adcDdrPn23Advance (state : slv(22 downto 0); count : positive) return slv is + variable result : slv(22 downto 0) := state; + begin + for i in 1 to count loop + result := adcDdrPn23Next(result); + end loop; + return result; + end function adcDdrPn23Advance; + + function adcDdrPn9Word (state : slv(8 downto 0); width : positive) return slv is + variable current : slv(8 downto 0) := state; + variable result : slv(width-1 downto 0); + begin + -- Fill from the result MSB downward so bit width-1 is the first bit that + -- would appear on an MSB-first serialized lane. + for i in width-1 downto 0 loop + result(i) := current(8); + current := adcDdrPn9Next(current); + end loop; + return result; + end function adcDdrPn9Word; + + function adcDdrPn23Word (state : slv(22 downto 0); width : positive) return slv is + variable current : slv(22 downto 0) := state; + variable result : slv(width-1 downto 0); + begin + -- Keep PN23 packing identical to PN9; only the polynomial/order differs. + for i in width-1 downto 0 loop + result(i) := current(22); + current := adcDdrPn23Next(current); + end loop; + return result; + end function adcDdrPn23Word; + +end package body AdcDdrPatternPkg; diff --git a/devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd new file mode 100644 index 0000000000..0500db1ae5 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd @@ -0,0 +1,177 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.AdcDdrCore +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrCoreWrapper is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := x"00000000"; + PATTERN_CHECK_G : boolean := true; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + S_AXI_AWADDR : in slv(31 downto 0); + S_AXI_AWPROT : in slv(2 downto 0); + S_AXI_AWVALID : in sl; + S_AXI_AWREADY : out sl; + S_AXI_WDATA : in slv(31 downto 0); + S_AXI_WSTRB : in slv(3 downto 0); + S_AXI_WVALID : in sl; + S_AXI_WREADY : out sl; + S_AXI_BRESP : out slv(1 downto 0); + S_AXI_BVALID : out sl; + S_AXI_BREADY : in sl; + S_AXI_ARADDR : in slv(31 downto 0); + S_AXI_ARPROT : in slv(2 downto 0); + S_AXI_ARVALID : in sl; + S_AXI_ARREADY : out sl; + S_AXI_RDATA : out slv(31 downto 0); + S_AXI_RRESP : out slv(1 downto 0); + S_AXI_RVALID : out sl; + S_AXI_RREADY : in sl; + captureClk : in sl; + captureRst : in sl; + delayReady : in sl := '0'; + fcoWord : in slv(13 downto 0); + fcoValid : in sl; + sampleValid : in sl; + sampleIn : in slv(31 downto 0); + bitSlip : out sl; + dataDelayValue : out slv(31 downto 0); + dataDelayLoad : out slv(1 downto 0); + fcoDelayValue : out slv(15 downto 0); + fcoDelayLoad : out sl; + phyReset : out sl; + streamClk : in sl; + streamRst : in sl; + streamValid : out slv(1 downto 0); + streamData : out slv(31 downto 0); + streamKeep : out slv(3 downto 0); + streamDest : out slv(15 downto 0); + streamLast : out slv(1 downto 0); + streamUser : out slv(15 downto 0)); +end entity AdcDdrCoreWrapper; + +architecture rtl of AdcDdrCoreWrapper is + + signal axilReadMaster : AxiLiteReadMasterType := AXI_LITE_READ_MASTER_INIT_C; + signal axilReadSlave : AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_INIT_C; + signal axilWriteMaster : AxiLiteWriteMasterType := AXI_LITE_WRITE_MASTER_INIT_C; + signal axilWriteSlave : AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_INIT_C; + signal sAxiAResetN : sl := '1'; + signal dataDelayWrite : AdcDdrDelayArray(1 downto 0); + signal fcoDelayWrite : AdcDdrDelayArray(0 downto 0); + signal sampleArray : Slv16Array(1 downto 0); + signal streamArray : AxiStreamMasterArray(1 downto 0); + signal bitSlipInt : slv(0 downto 0); + +begin + + sAxiAResetN <= not axilRst; + + U_ShimLayer : entity surf.SlaveAxiLiteIpIntegrator + generic map ( + EN_ERROR_RESP => true, + HAS_PROT => 1, + HAS_WSTRB => 1, + ADDR_WIDTH => 32) + port map ( + S_AXI_ACLK => axilClk, + S_AXI_ARESETN => sAxiAResetN, + S_AXI_AWADDR => S_AXI_AWADDR, + S_AXI_AWPROT => S_AXI_AWPROT, + S_AXI_AWVALID => S_AXI_AWVALID, + S_AXI_AWREADY => S_AXI_AWREADY, + S_AXI_WDATA => S_AXI_WDATA, + S_AXI_WSTRB => S_AXI_WSTRB, + S_AXI_WVALID => S_AXI_WVALID, + S_AXI_WREADY => S_AXI_WREADY, + S_AXI_BRESP => S_AXI_BRESP, + S_AXI_BVALID => S_AXI_BVALID, + S_AXI_BREADY => S_AXI_BREADY, + S_AXI_ARADDR => S_AXI_ARADDR, + S_AXI_ARPROT => S_AXI_ARPROT, + S_AXI_ARVALID => S_AXI_ARVALID, + S_AXI_ARREADY => S_AXI_ARREADY, + S_AXI_RDATA => S_AXI_RDATA, + S_AXI_RRESP => S_AXI_RRESP, + S_AXI_RVALID => S_AXI_RVALID, + S_AXI_RREADY => S_AXI_RREADY, + axilClk => open, + axilRst => open, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave); + + sampleArray(0) <= sampleIn(15 downto 0); + sampleArray(1) <= sampleIn(31 downto 16); + + U_DUT : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => 2, + FCO_LANES_G => 1, + CHANNELS_G => 2, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + PATTERN_CHECK_G => PATTERN_CHECK_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, + axilRst => axilRst, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave, + captureClk => captureClk, + captureRst => captureRst, + delayReady => delayReady, + fcoWord => (0 => "00" & fcoWord), + fcoValid => (0 => fcoValid), + sampleValid => sampleValid, + sampleIn => sampleArray, + phyReset => phyReset, + bitSlip => bitSlipInt, + dataDelayWrite => dataDelayWrite, + fcoDelayWrite => fcoDelayWrite, + streamClk => streamClk, + streamRst => streamRst, + streams => streamArray); + + bitSlip <= bitSlipInt(0); + GEN_FLATTEN : for i in 1 downto 0 generate + dataDelayValue((i*16)+15 downto i*16) <= resize(dataDelayWrite(i).value, 16); + dataDelayLoad(i) <= dataDelayWrite(i).load; + streamValid(i) <= streamArray(i).tValid; + streamData((i*16)+15 downto i*16) <= streamArray(i).tData(15 downto 0); + streamKeep((i*2)+1 downto i*2) <= streamArray(i).tKeep(1 downto 0); + streamDest((i*8)+7 downto i*8) <= streamArray(i).tDest; + streamLast(i) <= streamArray(i).tLast; + streamUser((i*8)+7 downto i*8) <= streamArray(i).tUser(7 downto 0); + end generate; + fcoDelayValue <= resize(fcoDelayWrite(0).value, 16); + fcoDelayLoad <= fcoDelayWrite(0).load; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd new file mode 100644 index 0000000000..0e56905797 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd @@ -0,0 +1,43 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Simulation wrapper for surf.AdcDdrPatternPkg +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity AdcDdrPatternPkgTb is + generic ( + WORD_WIDTH_G : positive := 14); + port ( + pn9State : in slv(8 downto 0); + pn23State : in slv(22 downto 0); + pn9Next : out slv(8 downto 0); + pn23Next : out slv(22 downto 0); + pn9Word : out slv(WORD_WIDTH_G-1 downto 0); + pn23Word : out slv(WORD_WIDTH_G-1 downto 0)); +end entity AdcDdrPatternPkgTb; + +architecture rtl of AdcDdrPatternPkgTb is + +begin + + pn9Next <= adcDdrPn9Advance(pn9State, WORD_WIDTH_G); + pn23Next <= adcDdrPn23Advance(pn23State, WORD_WIDTH_G); + pn9Word <= adcDdrPn9Word(pn9State, WORD_WIDTH_G); + pn23Word <= adcDdrPn23Word(pn23State, WORD_WIDTH_G); + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd new file mode 100644 index 0000000000..98dc7eda68 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd @@ -0,0 +1,126 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.AdcDdrPatternTester +------------------------------------------------------------------------------- +-- This file is part of 'SLAC Firmware Standard Library'. +-- It is subject to the license terms in the LICENSE.txt file found in the +-- top-level directory of this distribution and at: +-- https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +-- No part of 'SLAC Firmware Standard Library', including this file, +-- may be copied, modified, propagated, or distributed except according to +-- the terms contained in the LICENSE.txt file. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; + +entity AdcDdrPatternTesterWrapper is + generic ( + TPD_G : time := 1 ns); + port ( + clk : in sl; + rst : in sl; + S_AXI_AWADDR : in slv(11 downto 0); + S_AXI_AWPROT : in slv(2 downto 0); + S_AXI_AWVALID : in sl; + S_AXI_AWREADY : out sl; + S_AXI_WDATA : in slv(31 downto 0); + S_AXI_WSTRB : in slv(3 downto 0); + S_AXI_WVALID : in sl; + S_AXI_WREADY : out sl; + S_AXI_BRESP : out slv(1 downto 0); + S_AXI_BVALID : out sl; + S_AXI_BREADY : in sl; + S_AXI_ARADDR : in slv(11 downto 0); + S_AXI_ARPROT : in slv(2 downto 0); + S_AXI_ARVALID : in sl; + S_AXI_ARREADY : out sl; + S_AXI_RDATA : out slv(31 downto 0); + S_AXI_RRESP : out slv(1 downto 0); + S_AXI_RVALID : out sl; + S_AXI_RREADY : in sl; + sampleValid : in sl; + sampleIn : in slv(31 downto 0); + fcoValid : in sl; + fcoWord : in slv(13 downto 0)); +end entity AdcDdrPatternTesterWrapper; + +architecture rtl of AdcDdrPatternTesterWrapper is + + signal axilReadMaster : AxiLiteReadMasterType := AXI_LITE_READ_MASTER_INIT_C; + signal axilReadSlave : AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_INIT_C; + signal axilWriteMaster : AxiLiteWriteMasterType := AXI_LITE_WRITE_MASTER_INIT_C; + signal axilWriteSlave : AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_INIT_C; + signal axiAResetN : sl := '1'; + signal sampleArray : Slv16Array(1 downto 0); + signal fcoArray : Slv16Array(0 downto 0); + +begin + + axiAResetN <= not rst; + + U_ShimLayer : entity surf.SlaveAxiLiteIpIntegrator + generic map ( + EN_ERROR_RESP => true, + HAS_PROT => 1, + HAS_WSTRB => 1, + ADDR_WIDTH => 12) + port map ( + S_AXI_ACLK => clk, -- [in] + S_AXI_ARESETN => axiAResetN, -- [in] + S_AXI_AWADDR => S_AXI_AWADDR, -- [in] + S_AXI_AWPROT => S_AXI_AWPROT, -- [in] + S_AXI_AWVALID => S_AXI_AWVALID, -- [in] + S_AXI_AWREADY => S_AXI_AWREADY, -- [out] + S_AXI_WDATA => S_AXI_WDATA, -- [in] + S_AXI_WSTRB => S_AXI_WSTRB, -- [in] + S_AXI_WVALID => S_AXI_WVALID, -- [in] + S_AXI_WREADY => S_AXI_WREADY, -- [out] + S_AXI_BRESP => S_AXI_BRESP, -- [out] + S_AXI_BVALID => S_AXI_BVALID, -- [out] + S_AXI_BREADY => S_AXI_BREADY, -- [in] + S_AXI_ARADDR => S_AXI_ARADDR, -- [in] + S_AXI_ARPROT => S_AXI_ARPROT, -- [in] + S_AXI_ARVALID => S_AXI_ARVALID, -- [in] + S_AXI_ARREADY => S_AXI_ARREADY, -- [out] + S_AXI_RDATA => S_AXI_RDATA, -- [out] + S_AXI_RRESP => S_AXI_RRESP, -- [out] + S_AXI_RVALID => S_AXI_RVALID, -- [out] + S_AXI_RREADY => S_AXI_RREADY, -- [in] + axilClk => open, -- [out] + axilRst => open, -- [out] + axilReadMaster => axilReadMaster, -- [out] + axilReadSlave => axilReadSlave, -- [in] + axilWriteMaster => axilWriteMaster, -- [out] + axilWriteSlave => axilWriteSlave); -- [in] + + sampleArray(0) <= sampleIn(15 downto 0); + sampleArray(1) <= sampleIn(31 downto 16); + fcoArray(0) <= "00" & fcoWord; + + U_DUT : entity surf.AdcDdrPatternTester + generic map ( + TPD_G => TPD_G, + CHANNELS_G => 2, + FCO_LANES_G => 1, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + FRAME_PATTERN_G => "11111110000000") + port map ( + clk => clk, -- [in] + rst => rst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + sampleValid => sampleValid, -- [in] + sampleIn => sampleArray, -- [in] + fcoValid => (0 => fcoValid), -- [in] + fcoWord => fcoArray); -- [in] + +end architecture rtl; diff --git a/tests/devices/__init__.py b/tests/devices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/analog_devices/__init__.py b/tests/devices/analog_devices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/analog_devices/test_Ad9249Sim.py b/tests/devices/analog_devices/test_Ad9249Sim.py new file mode 100644 index 0000000000..228cc43df3 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9249Sim.py @@ -0,0 +1,151 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise conversion latency and the system-facing AD9249 shape with +# two serialized banks, differential DCO/FCO/data pins, real conversion, and +# a sample-alternating checkerboard pattern. +# - Stimulus: Drive a differential encode clock and independent channel codes, +# then program only the second CSB bank for a deterministic full-scale pattern. +# - Checks: Recovered words, coherent checkerboard frames, FCO framing, +# complementary pins, and bank-isolated SPI are checked without Xilinx models. +# - Timing: Normal conversions appear exactly 16 sample clocks after capture; +# both banks otherwise use ideal centered DCO/FCO timing in this test. + +import cocotb +from cocotb.triggers import Edge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def spi_write(dut, bank, address, value): + header = address & 0x1FFF + dut.sclk.value = 0 + dut.sdioEnable.value = 1 + dut.csb.value = 0b10 if bank == 0 else 0b01 + await Timer(100, unit="ns") + for bit in range(15, -1, -1): + dut.sdioDrive.value = (header >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + for bit in range(7, -1, -1): + dut.sdioDrive.value = (value >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + await Timer(100, unit="ns") + dut.csb.value = 0b11 + dut.sdioEnable.value = 0 + await Timer(200, unit="ns") + + +async def wait_fco_rise(dut, bank): + previous = (int(dut.fcoP.value) >> bank) & 1 + while True: + await Edge(dut.fcoP) + current = (int(dut.fcoP.value) >> bank) & 1 + if previous == 0 and current == 1: + return + previous = current + + +async def capture_bank(dut, bank): + await wait_fco_rise(dut, bank) + words = [0] * 8 + frame = 0 + for _ in range(14): + await Edge(dut.dcoP) + data = int(dut.dP.value) >> (8 * bank) + frame = (frame << 1) | ((int(dut.fcoP.value) >> bank) & 1) + for channel in range(8): + words[channel] = (words[channel] << 1) | ((data >> channel) & 1) + return words, frame + + +@cocotb.test() +async def ad9249_pin_level_device_sim_test(dut): + normal = [0x100 + i for i in range(16)] + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 0b11 + cocotb.start_soon(differential_clock(dut)) + # Allow the model's time-zero output initialization to settle before any + # std_logic_vector is converted to an integer by the frame collector. + await Timer(1, unit="ns") + + # A normal conversion captured with the first frame must remain absent for + # 16 complete output frames, then appear on the seventeenth frame. + for _ in range(16): + bank0, frame0 = await capture_bank(dut, 0) + assert bank0 == [0] * 8 + assert frame0 == 0b11111110000000 + + bank0, frame0 = await capture_bank(dut, 0) + bank1, frame1 = await capture_bank(dut, 1) + assert bank0 == normal[:8] + assert bank1 == normal[8:] + assert frame0 == 0b11111110000000 + assert frame1 == 0b11111110000000 + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + + # Both groups share SCLK/SDIO, but their independent CSB pins must isolate + # configuration. Program only bank 1 for positive full-scale output. + await spi_write(dut, 1, 0x0D, 0x02) + bank0, _ = await capture_bank(dut, 0) + bank1, _ = await capture_bank(dut, 1) + assert bank0 == normal[:8] + assert bank1 == [0x3FFF] * 8 + + # Alternating test words must be captured once per frame rather than read + # live across serialization, which would combine both checkerboard phases. + await spi_write(dut, 1, 0x0D, 0x04) + checkerboard = [] + for _ in range(4): + bank1, _ = await capture_bank(dut, 1) + assert len(set(bank1)) == 1 + assert bank1[0] in (0x2AAA, 0x1555) + checkerboard.append(bank1[0]) + assert checkerboard[0] == checkerboard[2] + assert checkerboard[1] == checkerboard[3] + assert checkerboard[0] != checkerboard[1] + + +def test_Ad9249Sim(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9249simwrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd", + "devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9249SimCore.py b/tests/devices/analog_devices/test_Ad9249SimCore.py new file mode 100644 index 0000000000..1bc0a46743 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9249SimCore.py @@ -0,0 +1,188 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise one eight-channel AD9249 bank's defaults, device indexing, +# immediate writes, transferred resolution/rate override, user patterns, PN9, +# inversion, bit order, and power-down. +# - Stimulus: Write the primitive-free byte-register interface while sampling +# independent normal words for every bank channel. +# - Checks: Identity/default readback, selected-channel isolation, pattern +# values, PN reset hold/release, global transforms, soft reset, and +# suppression and the staged 0x100 update are checked. +# - Timing: Configuration writes and samples share the model sample clock; +# only the AD9249 resolution/rate override requires device update. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def write(dut, addr, data): + await FallingEdge(dut.sampleClk) + dut.cfgAddr.value = addr + dut.cfgWrData.value = data + dut.cfgWrEn.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.cfgWrEn.value = 0 + + +async def read(dut, addr): + dut.cfgAddr.value = addr + await Timer(1, unit="ns") + return int(dut.cfgRdData.value) + + +async def sample(dut): + await FallingEdge(dut.sampleClk) + dut.sampleEnable.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.sampleEnable.value = 0 + return int(dut.sampleData.value) + + +def channel(data, index): + return (data >> (16 * index)) & 0xFFFF + + +def pn_word(state, order=9, tap=5, width=14): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return word + + +def pn_advance(state, order=9, tap=5, width=14): + for _ in range(width): + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return state + + +@cocotb.test() +async def ad9249_bank_register_and_pattern_test(dut): + dut.sampleRst.value = 1 + dut.sampleEnable.value = 0 + dut.cfgWrEn.value = 0 + dut.cfgAddr.value = 0 + dut.cfgWrData.value = 0 + dut.normalData.value = sum((0x200 + i) << (16 * i) for i in range(8)) + cocotb.start_soon(Clock(dut.sampleClk, 8, unit="ns").start()) + for _ in range(2): + await RisingEdge(dut.sampleClk) + dut.sampleRst.value = 0 + + assert await read(dut, 0x01) == 0x92 + assert await read(dut, 0x02) == 0x30 + assert await read(dut, 0x04) == 0x0F + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [0x200 + i for i in range(8)] + + # Register 0x100 is staged until the transfer strobe at register 0xFF. + await write(dut, 0x100, 0x63) + assert await read(dut, 0x100) == 0x00 + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x100) == 0x63 + + # Two's-complement normal-data mode flips the 14-bit code MSB. Test + # patterns are checked separately because their format applicability differs. + await write(dut, 0x14, 0x01) + data = await sample(dut) + assert channel(data, 1) == (0x201 ^ 0x2000) + await write(dut, 0x00, 0x04) + + # Select only channels 0 and 3. AD9249 writes become visible immediately. + await write(dut, 0x04, 0x00) + await write(dut, 0x05, 0x09) + await write(dut, 0x0D, 0x04) + data = await sample(dut) + assert channel(data, 0) in (0x2AAA, 0x1555) + assert channel(data, 3) == channel(data, 0) + assert channel(data, 1) == 0x201 + + await write(dut, 0x19, 0x23) + await write(dut, 0x1A, 0x01) + await write(dut, 0x1B, 0x56) + await write(dut, 0x1C, 0x04) + await write(dut, 0x0D, 0x08) + first = channel(await sample(dut), 0) + second = channel(await sample(dut), 0) + assert {first, second} == {0x0123, 0x0456} + + # PN23 reset remains readable and holds the generator at its seed until + # software clears the level. + pn23_seed = 0b01001101110000000101000 + pn23_first = pn_word(pn23_seed, order=23, tap=18) + pn23_second = pn_word( + pn_advance(pn23_seed, order=23, tap=18), order=23, tap=18) + await write(dut, 0x0D, 0x25) + assert await read(dut, 0x0D) == 0x25 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_first + await write(dut, 0x0D, 0x05) + assert await read(dut, 0x0D) == 0x05 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_second + + # Apply the same hold/release check to PN9, then retain the existing global + # output inversion and LSB-first serialization coverage. + await write(dut, 0x0D, 0x16) + await write(dut, 0x14, 0x04) + await write(dut, 0x21, 0x80) + assert await read(dut, 0x0D) == 0x16 + pn9_seed = 0b011011111 + pn9_first = int(f"{pn_word(pn9_seed):014b}"[::-1], 2) ^ 0x3FFF + pn9_second = int( + f"{pn_word(pn_advance(pn9_seed)):014b}"[::-1], 2) ^ 0x3FFF + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_first + await write(dut, 0x0D, 0x06) + assert await read(dut, 0x0D) == 0x06 + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_second + + await write(dut, 0x22, 0x01) + data = await sample(dut) + assert channel(data, 0) == 0 + assert channel(data, 3) == 0 + normal_reversed = int(f"{0x201:014b}"[::-1], 2) + assert channel(data, 1) == (normal_reversed ^ 0x3FFF) + + # Global power modes suppress every output bank channel. + await write(dut, 0x08, 0x01) + assert await sample(dut) == 0 + + # Soft reset restores selection, normal data, inversion, and bit order. + await write(dut, 0x00, 0x04) + assert await read(dut, 0x04) == 0x0F + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [0x200 + i for i in range(8)] + + +def test_Ad9249SimCore(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9249simcorewrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd", + "devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9249SimTiming.py b/tests/devices/analog_devices/test_Ad9249SimTiming.py new file mode 100644 index 0000000000..ae3e15e7de --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9249SimTiming.py @@ -0,0 +1,129 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise per-lane/per-bank static skew and bounded deterministic +# jitter in both source-synchronous AD9249 output banks. +# - Stimulus: Serialize transition-rich normal samples after the 16-clock +# conversion pipeline; skew data lane 0 and FCO bank 0 relative to bank 1. +# - Checks: DCO remains ideal, skew is preserved, jitter alternates around each +# nominal transition cadence, and every differential output remains binary. +# - Timing: Bias/jitter are 50 ps, data skew is 100 ps, and FCO skew is 150 ps. + +import cocotb +from cocotb.triggers import Edge, Timer +from cocotb.utils import get_sim_time + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def collect_bit_edges(signal, bit, count): + previous = (int(signal.value) >> bit) & 1 + edges = [] + while len(edges) < count: + await Edge(signal) + current = (int(signal.value) >> bit) & 1 + if current != previous: + edges.append(int(get_sim_time(unit="ps"))) + previous = current + return edges + + +def intervals(edges): + return [current-previous for previous, current in zip(edges, edges[1:])] + + +def setup_times(edges, dco_edges): + return [next(dco-edge for dco in dco_edges if dco > edge) for edge in edges] + + +def contains_near(values, expected, tolerance=2): + return any(abs(value-expected) <= tolerance for value in values) + + +@cocotb.test() +async def ad9249_binary_skew_and_jitter_test(dut): + dut.normalData.value = sum(0x2AAA << (16*channel) for channel in range(16)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 0b11 + cocotb.start_soon(differential_clock(dut)) + + await Timer(480, unit="ns") + + lane0_task = cocotb.start_soon(collect_bit_edges(dut.dP, 0, 16)) + lane1_task = cocotb.start_soon(collect_bit_edges(dut.dP, 1, 16)) + fco0_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 0, 6)) + fco1_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 1, 6)) + # Include a DCO edge after the last, much slower FCO transition so every + # measured source edge has a following sampling edge for setup calculation. + dco_task = cocotb.start_soon(collect_bit_edges(dut.dcoP, 0, 48)) + + lane0_edges = await lane0_task + lane1_edges = await lane1_task + fco0_edges = await fco0_task + fco1_edges = await fco1_task + dco_edges = await dco_task + + assert all(abs((a-b)-100) <= 1 for a, b in zip(lane0_edges, lane1_edges)) + assert all(abs((a-b)-150) <= 1 for a, b in zip(fco0_edges, fco1_edges)) + assert all(abs(interval-1714) <= 1 for interval in intervals(dco_edges)) + + data_setup = setup_times(lane1_edges, dco_edges) + assert contains_near(data_setup, 807) + assert contains_near(data_setup, 907) + assert contains_near(intervals(lane1_edges), 1614) + assert contains_near(intervals(lane1_edges), 1814) + assert contains_near(intervals(fco1_edges), 11900) + assert contains_near(intervals(fco1_edges), 12100) + + assert dut.dP.value.is_resolvable + assert dut.dN.value.is_resolvable + assert dut.dcoP.value.is_resolvable + assert dut.dcoN.value.is_resolvable + assert dut.fcoP.value.is_resolvable + assert dut.fcoN.value.is_resolvable + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + + +def test_Ad9249SimTiming(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9249simwrapper", + parameters={ + "DATA_LANE0_SKEW_PS_G": 100, + "FCO_LANE0_SKEW_PS_G": 150, + "JITTER_PS_G": 50, + "TIMING_BIAS_PS_G": 50, + }, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd", + "devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9252Sim.py b/tests/devices/analog_devices/test_Ad9252Sim.py new file mode 100644 index 0000000000..4bc1bb3113 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9252Sim.py @@ -0,0 +1,143 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise conversion latency and the system-facing AD9252 pin shape in +# normal, deterministic, and sample-alternating checkerboard modes. +# - Stimulus: Drive differential encode clock and real-valued channel inputs, +# then configure one channel through the actual shared SDIO interface. +# - Checks: Recover coherent serialized words and framing, verify differential +# complements, and confirm staged SPI configuration reaches the output pins. +# - Timing: Normal conversions appear exactly eight sample clocks after capture; +# data and FCO otherwise use ideal centered timing in this test. + +import cocotb +from cocotb.triggers import Edge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def spi_write(dut, address, value): + header = address & 0x1FFF + dut.sclk.value = 0 + dut.sdioEnable.value = 1 + dut.csb.value = 0 + await Timer(100, unit="ns") + for bit in range(15, -1, -1): + dut.sdioDrive.value = (header >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + for bit in range(7, -1, -1): + dut.sdioDrive.value = (value >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + await Timer(100, unit="ns") + dut.csb.value = 1 + dut.sdioEnable.value = 0 + await Timer(200, unit="ns") + + +async def capture_frame(dut): + previous = int(dut.fcoP.value) + while True: + await Edge(dut.fcoP) + current = int(dut.fcoP.value) + if previous == 0 and current == 1: + break + previous = current + words = [0] * 8 + frame = 0 + for _ in range(14): + await Edge(dut.dcoP) + data = int(dut.dP.value) + frame = (frame << 1) | int(dut.fcoP.value) + for channel in range(8): + words[channel] = (words[channel] << 1) | ((data >> channel) & 1) + return words, frame + + +@cocotb.test() +async def ad9252_pin_level_device_sim_test(dut): + normal = [0x100 + i for i in range(8)] + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + cocotb.start_soon(differential_clock(dut)) + # Allow the model's time-zero output initialization to settle before any + # std_logic value is converted to an integer by the frame collector. + await Timer(1, unit="ns") + + # Seven explicit conversion registers plus the coherent frame handoff model + # the specified eight-clock latency at the device pins. + for _ in range(8): + words, frame = await capture_frame(dut) + assert words == [0] * 8 + assert frame == 0b11111110000000 + + words, frame = await capture_frame(dut) + assert words == normal + assert frame == 0b11111110000000 + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFF) + assert int(dut.dcoN.value) == (not int(dut.dcoP.value)) + assert int(dut.fcoN.value) == (not int(dut.fcoP.value)) + + await spi_write(dut, 0x05, 0x01) + await spi_write(dut, 0x0D, 0x02) + await spi_write(dut, 0xFF, 0x01) + words, _ = await capture_frame(dut) + assert words[0] == 0x3FFF + assert words[1:] == normal[1:] + + # Alternating output words must remain coherent across each serialized + # frame even though the core toggles the pattern every sample clock. + await spi_write(dut, 0x0D, 0x04) + await spi_write(dut, 0xFF, 0x01) + checkerboard = [] + for _ in range(4): + words, _ = await capture_frame(dut) + assert words[0] in (0x2AAA, 0x1555) + assert words[1:] == normal[1:] + checkerboard.append(words[0]) + assert checkerboard[0] == checkerboard[2] + assert checkerboard[1] == checkerboard[3] + assert checkerboard[0] != checkerboard[1] + + +def test_Ad9252Sim(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9252simwrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd", + "devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9252SimCore.py b/tests/devices/analog_devices/test_Ad9252SimCore.py new file mode 100644 index 0000000000..d92b21eb45 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9252SimCore.py @@ -0,0 +1,174 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise AD9252 defaults, channel indexing, buffered transfer, +# user patterns, PN9, inversion, bit order, and channel power controls. +# - Stimulus: Write the logical register interface and issue device-update +# transfers while sampling eight independent normal input words. +# - Checks: Reads expose identity/defaults, writes have no effect before +# transfer, selected channels update together, PN resets hold/release, and +# unselected channels do not change. +# - Timing: Register writes and each output sample occur on the sample clock. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def write(dut, addr, data): + await FallingEdge(dut.sampleClk) + dut.cfgAddr.value = addr + dut.cfgWrData.value = data + dut.cfgWrEn.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.cfgWrEn.value = 0 + + +async def read(dut, addr): + dut.cfgAddr.value = addr + await Timer(1, unit="ns") + return int(dut.cfgRdData.value) + + +async def sample(dut): + await FallingEdge(dut.sampleClk) + dut.sampleEnable.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.sampleEnable.value = 0 + return int(dut.sampleData.value) + + +def channel(data, index): + return (data >> (16 * index)) & 0xFFFF + + +def pn_word(state, order=9, tap=5, width=14): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return word + + +def pn_advance(state, order=9, tap=5, width=14): + for _ in range(width): + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return state + + +@cocotb.test() +async def ad9252_register_and_pattern_test(dut): + dut.sampleRst.value = 1 + dut.sampleEnable.value = 0 + dut.cfgWrEn.value = 0 + dut.cfgAddr.value = 0 + dut.cfgWrData.value = 0 + dut.normalData.value = sum((0x100 + i) << (16 * i) for i in range(8)) + cocotb.start_soon(Clock(dut.sampleClk, 8, unit="ns").start()) + for _ in range(2): + await RisingEdge(dut.sampleClk) + dut.sampleRst.value = 0 + + assert await read(dut, 0x01) == 0x09 + assert await read(dut, 0x02) == 0x30 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [0x100 + i for i in range(8)] + + # Select channels 0 and 3, stage checkerboard, and prove transfer is required. + await write(dut, 0x05, 0x09) + await write(dut, 0x0D, 0x04) + data = await sample(dut) + assert channel(data, 0) == 0x100 + await write(dut, 0xFF, 0x01) + data = await sample(dut) + assert channel(data, 0) == 0x2AAA + assert channel(data, 3) == 0x2AAA + assert channel(data, 1) == 0x101 + + # User pattern updates selected channels atomically. + await write(dut, 0x19, 0x23) + await write(dut, 0x1A, 0x01) + await write(dut, 0x1B, 0x56) + await write(dut, 0x1C, 0x04) + await write(dut, 0x0D, 0x08) + await write(dut, 0xFF, 0x01) + first = channel(await sample(dut), 0) + second = channel(await sample(dut), 0) + assert {first, second} == {0x0123, 0x0456} + + # PN23 reset is published by device update and remains asserted until a + # second staged write/update releases the generator from its seed. + pn23_seed = 0b01001101110000000101000 + pn23_first = pn_word(pn23_seed, order=23, tap=18) + pn23_state_1 = pn_advance(pn23_seed, order=23, tap=18) + pn23_state_2 = pn_advance(pn23_state_1, order=23, tap=18) + pn23_second = pn_word(pn23_state_1, order=23, tap=18) + pn23_third = pn_word(pn23_state_2, order=23, tap=18) + await write(dut, 0x0D, 0x25) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x25 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_first + await write(dut, 0x0D, 0x05) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x05 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_second + # Re-publishing unchanged shadow configuration must not rewind live PN + # state maintained independently by each selected channel. + await write(dut, 0xFF, 0x01) + assert channel(await sample(dut), 0) == pn23_third + + # Apply the same hold/release check to PN9 together with global inversion + # and LSB-first bit reversal. + await write(dut, 0x0D, 0x16) + await write(dut, 0x14, 0x04) + await write(dut, 0x21, 0x80) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x16 + pn9_seed = 0b011011111 + pn9_first = int(f"{pn_word(pn9_seed):014b}"[::-1], 2) ^ 0x3FFF + pn9_second = int( + f"{pn_word(pn_advance(pn9_seed)):014b}"[::-1], 2) ^ 0x3FFF + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_first + await write(dut, 0x0D, 0x06) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x06 + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_second + + # Power-down is per selected channel and leaves other channels running. + await write(dut, 0x22, 0x01) + await write(dut, 0xFF, 0x01) + data = await sample(dut) + assert channel(data, 0) == 0 + assert channel(data, 3) == 0 + normal_reversed = int(f"{0x101:014b}"[::-1], 2) + assert channel(data, 1) == (normal_reversed ^ 0x3FFF) + + +def test_Ad9252SimCore(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9252simcorewrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd", + "devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9252SimTiming.py b/tests/devices/analog_devices/test_Ad9252SimTiming.py new file mode 100644 index 0000000000..e8d7b3388b --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9252SimTiming.py @@ -0,0 +1,129 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise per-lane/FCO static skew and bounded deterministic jitter in +# the primitive-free AD9252 pin-level model. +# - Stimulus: Serialize transition-rich normal samples after the eight-clock +# conversion pipeline; skew data lane 0 and the single FCO output. +# - Checks: DCO remains ideal, programmed skew changes setup time, jitter +# alternates around nominal transition cadence, and differential pins resolve. +# - Timing: Bias/jitter are 50 ps, data skew is 100 ps, and FCO skew is 150 ps. + +import cocotb +from cocotb.triggers import Edge, Timer +from cocotb.utils import get_sim_time + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def collect_bit_edges(signal, bit, count): + previous = (int(signal.value) >> bit) & 1 + edges = [] + while len(edges) < count: + await Edge(signal) + current = (int(signal.value) >> bit) & 1 + if current != previous: + edges.append(int(get_sim_time(unit="ps"))) + previous = current + return edges + + +def intervals(edges): + return [current-previous for previous, current in zip(edges, edges[1:])] + + +def setup_times(edges, dco_edges): + return [next(dco-edge for dco in dco_edges if dco > edge) for edge in edges] + + +def contains_near(values, expected, tolerance=2): + return any(abs(value-expected) <= tolerance for value in values) + + +@cocotb.test() +async def ad9252_binary_skew_and_jitter_test(dut): + dut.normalData.value = sum(0x2AAA << (16*channel) for channel in range(8)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + cocotb.start_soon(differential_clock(dut)) + + await Timer(288, unit="ns") + + lane0_task = cocotb.start_soon(collect_bit_edges(dut.dP, 0, 16)) + lane1_task = cocotb.start_soon(collect_bit_edges(dut.dP, 1, 16)) + fco_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 0, 6)) + # Include a DCO edge after the last, much slower FCO transition so every + # measured source edge has a following sampling edge for setup calculation. + dco_task = cocotb.start_soon(collect_bit_edges(dut.dcoP, 0, 48)) + + lane0_edges = await lane0_task + lane1_edges = await lane1_task + fco_edges = await fco_task + dco_edges = await dco_task + + assert all(abs((a-b)-100) <= 1 for a, b in zip(lane0_edges, lane1_edges)) + assert all(abs(interval-1714) <= 1 for interval in intervals(dco_edges)) + + data_setup = setup_times(lane1_edges, dco_edges) + fco_setup = setup_times(fco_edges, dco_edges) + assert contains_near(data_setup, 807) + assert contains_near(data_setup, 907) + assert contains_near(fco_setup, 657) + assert contains_near(fco_setup, 757) + assert contains_near(intervals(lane1_edges), 1614) + assert contains_near(intervals(lane1_edges), 1814) + assert contains_near(intervals(fco_edges), 11900) + assert contains_near(intervals(fco_edges), 12100) + + assert dut.dP.value.is_resolvable + assert dut.dN.value.is_resolvable + assert dut.dcoP.value.is_resolvable + assert dut.dcoN.value.is_resolvable + assert dut.fcoP.value.is_resolvable + assert dut.fcoN.value.is_resolvable + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFF) + assert int(dut.dcoN.value) == (not int(dut.dcoP.value)) + assert int(dut.fcoN.value) == (not int(dut.fcoP.value)) + + +def test_Ad9252SimTiming(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9252simwrapper", + parameters={ + "DATA_LANE0_SKEW_PS_G": 100, + "FCO_SKEW_PS_G": 150, + "JITTER_PS_G": 50, + "TIMING_BIAS_PS_G": 50, + }, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd", + "devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9681Sim.py b/tests/devices/analog_devices/test_Ad9681Sim.py new file mode 100644 index 0000000000..33a40ddc5d --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9681Sim.py @@ -0,0 +1,144 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise conversion latency and the system-facing AD9681 two-byte-lane +# pin shape, including a sample-alternating checkerboard pattern. +# - Stimulus: Drive differential encode clock and independent real-valued +# channels, then issue immediate writes through the actual shared SDIO pins. +# - Checks: Recombine both serialized bytes, verify coherent checkerboard words, +# framing and differential complements, and confirm SPI updates are applied +# across both channel halves. +# - Timing: Normal conversions appear exactly 16 sample clocks after capture; +# both byte groups share an ideal centered DCO/FCO waveform. + +import cocotb +from cocotb.triggers import Edge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=8): + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def spi_write(dut, address, value): + header = address & 0x1FFF + dut.sclk.value = 0 + dut.sdioEnable.value = 1 + dut.csb.value = 0 + await Timer(50, unit="ns") + for bit in range(15, -1, -1): + dut.sdioDrive.value = (header >> bit) & 1 + await Timer(30, unit="ns") + dut.sclk.value = 1 + await Timer(30, unit="ns") + dut.sclk.value = 0 + for bit in range(7, -1, -1): + dut.sdioDrive.value = (value >> bit) & 1 + await Timer(30, unit="ns") + dut.sclk.value = 1 + await Timer(30, unit="ns") + dut.sclk.value = 0 + await Timer(50, unit="ns") + dut.csb.value = 1 + dut.sdioEnable.value = 0 + await Timer(100, unit="ns") + + +async def capture_frame(dut): + previous = int(dut.fcoP.value) & 1 + while True: + await Edge(dut.fcoP) + current = int(dut.fcoP.value) & 1 + if previous == 0 and current == 1: + break + previous = current + low = [0] * 8 + high = [0] * 8 + frame = 0 + for _ in range(8): + await Edge(dut.dcoP) + data = int(dut.dP.value) + frame = (frame << 1) | (int(dut.fcoP.value) & 1) + for channel in range(8): + low[channel] = (low[channel] << 1) | ((data >> channel) & 1) + high[channel] = (high[channel] << 1) | ((data >> (8 + channel)) & 1) + return [(high[i] << 8) | low[i] for i in range(8)], frame + + +@cocotb.test() +async def ad9681_pin_level_device_sim_test(dut): + normal = [0x200 + i for i in range(8)] + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + cocotb.start_soon(differential_clock(dut)) + await Timer(1, unit="ns") + + # A normal conversion captured with the first frame must remain absent for + # 16 complete output frames, then appear on the seventeenth frame. + for _ in range(16): + words, frame = await capture_frame(dut) + assert words == [0] * 8 + assert frame == 0b11110000 + + words, frame = await capture_frame(dut) + assert words == [value << 2 for value in normal] + assert frame == 0b11110000 + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + + await spi_write(dut, 0x05, 0x01) + await spi_write(dut, 0x0D, 0x02) + words, _ = await capture_frame(dut) + assert words[0] == 0xFFFC + assert words[4] == 0xFFFC + assert words[1:4] == [value << 2 for value in normal[1:4]] + assert words[5:] == [value << 2 for value in normal[5:]] + + # Alternating output words must be latched once per frame. Reading + # sampleData live for each serialized bit tears these two patterns together. + await spi_write(dut, 0x0D, 0x04) + checkerboard = [] + for _ in range(4): + words, _ = await capture_frame(dut) + assert words[0] == words[4] + assert words[0] in (0xAAA8, 0x5554) + checkerboard.append(words[0]) + assert checkerboard[0] == checkerboard[2] + assert checkerboard[1] == checkerboard[3] + assert checkerboard[0] != checkerboard[1] + + +def test_Ad9681Sim(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9681simwrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd", + "devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9681SimCore.py b/tests/devices/analog_devices/test_Ad9681SimCore.py new file mode 100644 index 0000000000..7e981ebb70 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9681SimCore.py @@ -0,0 +1,201 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise default two-lane words, both four-channel configuration +# banks, the staged resolution/rate override, user and PN9 patterns, +# inversion, bit order, and power. +# - Stimulus: Write the primitive-free byte-register interface while sampling +# independent normal codes for all eight channels. +# - Checks: Padding placement, identity, immediate ordinary-register updates, +# 0x100 transfer semantics, unsupported-format fallback, pattern values, PN reset +# hold/release, transforms, soft reset, and suppression are checked. +# - Timing: Configuration and samples share one clock; only the resolution/rate +# override remains hidden until the Register 0xFF transfer write. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def write(dut, bank, addr, data): + await FallingEdge(dut.sampleClk) + dut.cfgBank.value = bank + dut.cfgAddr.value = addr + dut.cfgWrData.value = data + dut.cfgWrEn.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.cfgWrEn.value = 0 + + +async def read(dut, bank, addr): + dut.cfgBank.value = bank + dut.cfgAddr.value = addr + await Timer(1, unit="ns") + return int(dut.cfgRdData.value) + + +async def sample(dut): + await FallingEdge(dut.sampleClk) + dut.sampleEnable.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.sampleEnable.value = 0 + return int(dut.sampleData.value) + + +def channel(data, index): + return (data >> (16 * index)) & 0xFFFF + + +def pn_word(state, order=9, tap=5, width=14): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return word + + +def pn_advance(state, order=9, tap=5, width=14): + for _ in range(width): + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return state + + +def reverse_bytes(word): + upper = int(f"{(word >> 8) & 0xFF:08b}"[::-1], 2) + lower = int(f"{word & 0xFF:08b}"[::-1], 2) + return (upper << 8) | lower + + +@cocotb.test() +async def ad9681_register_bank_and_pattern_test(dut): + normal = [0x200 + i for i in range(8)] + dut.sampleRst.value = 1 + dut.sampleEnable.value = 0 + dut.cfgBank.value = 0 + dut.cfgWrEn.value = 0 + dut.cfgAddr.value = 0 + dut.cfgWrData.value = 0 + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + cocotb.start_soon(Clock(dut.sampleClk, 8, unit="ns").start()) + for _ in range(2): + await RisingEdge(dut.sampleClk) + dut.sampleRst.value = 0 + + assert await read(dut, 0, 0x01) == 0x8F + assert await read(dut, 1, 0x02) == 0x60 + assert await read(dut, 0, 0x05) == 0x0F + assert await read(dut, 1, 0x05) == 0x0F + assert await read(dut, 0, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] + assert all((channel(data, i) & 0x3) == 0 for i in range(8)) + + # Register 0x100 is staged until the transfer strobe at Register 0xFF. + await write(dut, 0, 0x100, 0x66) + assert await read(dut, 0, 0x100) == 0x00 + await write(dut, 0, 0xFF, 0x00) + assert await read(dut, 0, 0x100) == 0x00 + await write(dut, 0, 0xFF, 0x01) + assert await read(dut, 0, 0x100) == 0x66 + + # Unsupported output formats warn immediately but retain register + # readback and continue using the model's fixed two-lane bytewise format. + await write(dut, 0, 0x21, 0x00) + assert await read(dut, 0, 0x21) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] + await write(dut, 0, 0x21, 0x30) + + # Select physical configuration bank 1/channel 4. The new mode applies + # immediately and must not affect bank 0. + await write(dut, 1, 0x05, 0x01) + await write(dut, 1, 0x0D, 0x02) + data = await sample(dut) + assert channel(data, 4) == 0xFFFC + assert channel(data, 0) == normal[0] << 2 + assert channel(data, 5) == normal[5] << 2 + + # Program a selected channel in bank 0 for alternating user words. + await write(dut, 0, 0x05, 0x01) + await write(dut, 0, 0x19, 0x23) + await write(dut, 0, 0x1A, 0x01) + await write(dut, 0, 0x1B, 0x56) + await write(dut, 0, 0x1C, 0x04) + await write(dut, 0, 0x0D, 0x48) + first = channel(await sample(dut), 0) + second = channel(await sample(dut), 0) + assert {first, second} == {0x0123, 0x0456} + + # PN23 reset is a retained level, not a self-clearing command. While it is + # asserted, the selected generator stays at its seed; clearing it releases + # a repeatable sequence from that seed. + pn23_seed = 0b01001101110000000101000 + pn23_first = pn_word(pn23_seed, order=23, tap=18) << 2 + pn23_state_1 = pn_advance(pn23_seed, order=23, tap=18) + pn23_state_2 = pn_advance(pn23_state_1, order=23, tap=18) + pn23_second = pn_word(pn23_state_1, order=23, tap=18) << 2 + pn23_third = pn_word(pn23_state_2, order=23, tap=18) << 2 + await write(dut, 0, 0x0D, 0x25) + assert await read(dut, 0, 0x0D) == 0x25 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_first + await write(dut, 0, 0x0D, 0x05) + assert await read(dut, 0, 0x0D) == 0x05 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_second + # Transferring unchanged resolution/rate configuration must not rewind the + # live PN state maintained independently by each channel. + await write(dut, 0, 0xFF, 0x01) + assert channel(await sample(dut), 0) == pn23_third + + # Apply the same hold/release check to bank 1/channel 4's PN9 generator, + # together with global inversion and byte-local LSB-first ordering. + await write(dut, 1, 0x0D, 0x16) + await write(dut, 1, 0x14, 0x04) + await write(dut, 1, 0x21, 0xB0) + assert await read(dut, 1, 0x0D) == 0x16 + pn9_seed = 0b011011111 + pn9_first = reverse_bytes((pn_word(pn9_seed) << 2) ^ 0xFFFF) + pn9_second = reverse_bytes( + (pn_word(pn_advance(pn9_seed)) << 2) ^ 0xFFFF) + assert channel(await sample(dut), 4) == pn9_first + assert channel(await sample(dut), 4) == pn9_first + await write(dut, 1, 0x0D, 0x06) + assert await read(dut, 1, 0x0D) == 0x06 + assert channel(await sample(dut), 4) == pn9_first + assert channel(await sample(dut), 4) == pn9_second + + # A power-mode write immediately suppresses all channels. Soft reset + # immediately restores default two-lane normal output and override state. + await write(dut, 0, 0x08, 0x01) + assert await sample(dut) == 0 + await write(dut, 0, 0x00, 0x04) + assert await read(dut, 0, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] + + +def test_Ad9681SimCore(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9681simcorewrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd", + "devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9681SimTiming.py b/tests/devices/analog_devices/test_Ad9681SimTiming.py new file mode 100644 index 0000000000..dc17bd3724 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9681SimTiming.py @@ -0,0 +1,133 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise static per-lane/FCO transition skew together with bounded +# deterministic jitter in the primitive-free AD9681 pin-level model. +# - Stimulus: Serialize a repeating transition-rich normal sample with a common +# timing bias; one data lane and one FCO lane receive additional static skew. +# - Checks: DCO retains its exact period, the unskewed data/FCO setup times are +# centered at 500 ps, paired lanes retain their programmed skew, transition +# intervals alternate around their nominal cadence, and every differential +# output remains binary and complementary. +# - Timing: Common bias and jitter are both 50 ps, data lane 0 adds 100 ps, and +# FCO lane 0 adds 150 ps relative to their corresponding lane 1 outputs. + +import cocotb +from cocotb.triggers import Edge, Timer +from cocotb.utils import get_sim_time + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=8): + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def collect_bit_edges(signal, bit, count): + previous = (int(signal.value) >> bit) & 1 + edges = [] + while len(edges) < count: + await Edge(signal) + current = (int(signal.value) >> bit) & 1 + if current != previous: + edges.append(int(get_sim_time(unit="ps"))) + previous = current + return edges + + +def short_intervals(edges, maximum_ps): + return [ + current-previous + for previous, current in zip(edges, edges[1:]) + if current-previous < maximum_ps + ] + + +def setup_times(edges, dco_edges): + return [next(dco-edge for dco in dco_edges if dco > edge) for edge in edges] + + +@cocotb.test() +async def ad9681_binary_skew_and_jitter_test(dut): + # 0x2AAA becomes 0xAAA8 on each 16-bit serialized channel, providing + # repeated transitions in both physical byte groups without SPI setup. + dut.normalData.value = sum(0x2AAA << (16*channel) for channel in range(8)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + cocotb.start_soon(differential_clock(dut)) + + # Allow the modeled 16-sample ADC conversion pipeline to fill before + # collecting data edges and their corresponding DCO sampling edges. + await Timer(160, unit="ns") + + lane0_task = cocotb.start_soon(collect_bit_edges(dut.dP, 0, 16)) + lane1_task = cocotb.start_soon(collect_bit_edges(dut.dP, 1, 16)) + fco0_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 0, 6)) + fco1_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 1, 6)) + dco_task = cocotb.start_soon(collect_bit_edges(dut.dcoP, 0, 32)) + + lane0_edges = await lane0_task + lane1_edges = await lane1_task + fco0_edges = await fco0_task + fco1_edges = await fco1_task + dco_edges = await dco_task + + assert all(a-b == 100 for a, b in zip(lane0_edges, lane1_edges)) + assert all(a-b == 150 for a, b in zip(fco0_edges, fco1_edges)) + assert all(interval == 1000 for interval in short_intervals(dco_edges, 1500)) + assert set(setup_times(lane1_edges, dco_edges)) == {450, 550} + assert set(setup_times(fco1_edges, dco_edges)) == {450, 550} + + # Alternating -50/+50 ps displacement creates 900/1100 ps data intervals + # and 3900/4100 ps FCO intervals around their nominal cadence. + assert {900, 1100}.issubset(set(short_intervals(lane1_edges, 1500))) + assert {3900, 4100}.issubset(set(short_intervals(fco1_edges, 5000))) + + assert dut.dP.value.is_resolvable + assert dut.dN.value.is_resolvable + assert dut.dcoP.value.is_resolvable + assert dut.dcoN.value.is_resolvable + assert dut.fcoP.value.is_resolvable + assert dut.fcoN.value.is_resolvable + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + + +def test_Ad9681SimTiming(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9681simwrapper", + parameters={ + "DATA_LANE0_SKEW_PS_G": 100, + "FCO_LANE0_SKEW_PS_G": 150, + "JITTER_PS_G": 50, + "TIMING_BIAS_PS_G": 50, + }, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd", + "devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py new file mode 100644 index 0000000000..ef1f44021f --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -0,0 +1,976 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise ordinary, tied, boundary, full-range, narrow, guarded, and +# circular passing windows over small representative delay ranges. +# - Stimulus: Supply deterministic tap-to-pass maps directly to the common eye +# selector without PyRogue hardware access. +# - Checks: Verify selected taps, physical margins, boundary visibility, +# wraparound reporting, deterministic ties, invalid-input rejection, and +# topology-aware, overlap-safe parallel grouping plus restoration between +# guard checks for coupled physical lanes. Exercise snapshot and pattern-tester +# measurement backends plus verification progress, final qualification, and +# live run-time reporting. +# - Timing: Fake register access and zero settling keep process tests short; the +# live timer regression temporarily reduces its update interval. + +import threading +import time + +import pytest +import pyrogue as pr + +from surf.devices.analog_devices import ( + AdcDdrCalibration, + checkAdcDdrPn23, + findAdcDdrEye, + findAdcDdrEyes, +) + + +class FakeVariable: + def __init__(self, value=0, getter=None): + self._value = value + self._getter = getter + + def get(self, read=False): + return self._getter() if self._getter is not None else self._value + + def set(self, value, write=False): + self._value = value + + def value(self): + return self._value + + +class FakeConfig: + def __init__(self): + self.OutputTestMode = FakeVariable(0) + self.ResetPNLong = FakeVariable(False) + self.updateCount = 0 + + def update(self): + self.updateCount += 1 + + +class FakePatternTester: + def __init__(self, readout): + self._readout = readout + self.Alternating = FakeVariable(False) + self.ReferenceChannel = FakeVariable(0) + self.ChannelMask = FakeVariable(0) + self.FcoMask = FakeVariable(0) + self.DataMask = FakeVariable(0) + self.PatternA = FakeVariable(0) + self.PatternB = FakeVariable(0) + self.Samples = FakeVariable(0) + self.Sequence = FakeVariable(0) + self.CheckedSamples = FakeVariable(0) + self.ChannelPassed = FakeVariable(0) + self.FcoPassed = FakeVariable(0) + self.Busy = FakeVariable(False) + self.TimedOut = FakeVariable(False) + self.ConfigError = FakeVariable(False) + self.Aborted = FakeVariable(False) + self.PhaseAcquired = FakeVariable(False) + self.AllChannelsPass = FakeVariable(False) + self.AllFcoPass = FakeVariable(False) + self.measurements = [] + + def Start(self): + alternating = bool(self.Alternating.value()) + mask = self.DataMask.value() + patternA = self.PatternA.value() & mask + patternB = self.PatternB.value() & mask + sampleCount = self.Samples.value() + channelMask = self.ChannelMask.value() + channelPassed = 0 + for channel in range(self._readout._channels): + if not channelMask & (1 << channel): + continue + samples = [ + self._readout._debugSample(channel, index) & mask + for index in range(sampleCount) + ] + if alternating and samples: + if samples[0] == patternA: + phase = 0 + elif samples[0] == patternB: + phase = 1 + else: + phase = None + passed = phase is not None and all( + sample == (patternA, patternB)[(phase+index) % 2] + for index, sample in enumerate(samples) + ) + else: + passed = all(sample == patternA for sample in samples) + if passed: + channelPassed |= 1 << channel + + fcoMask = self.FcoMask.value() + fcoPassed = self._readout._lockedMask() & fcoMask + self.ChannelPassed.set(channelPassed) + self.FcoPassed.set(fcoPassed) + self.CheckedSamples.set(sampleCount) + self.Busy.set(False) + self.TimedOut.set(False) + self.ConfigError.set(False) + self.Aborted.set(False) + self.PhaseAcquired.set(not alternating or bool(channelPassed)) + self.AllChannelsPass.set((channelPassed & channelMask) == channelMask) + self.AllFcoPass.set((fcoPassed & fcoMask) == fcoMask) + self.Sequence.set(self.Sequence.value()+1) + self.measurements.append({ + 'delays': tuple( + variable.value() for variable in self._readout.DataDelay.values()), + 'channelMask': channelMask, + 'dataMask': mask, + 'samples': sampleCount, + }) + + def Abort(self): + self.Busy.set(False) + self.Aborted.set(True) + self.Sequence.set(self.Sequence.value()+1) + + +class FakeReadout: + def __init__(self): + self._dataLanes = 2 + self._fcoLanes = 1 + self._channels = 2 + self._sampleBits = 14 + self._delayBits = 3 + self.FcoDelay = {0: FakeVariable(3)} + self.FcoWord = {0: FakeVariable(0x3F00)} + self.DataDelay = {0: FakeVariable(2), 1: FakeVariable(4)} + self.PatternCheck = FakeVariable(True) + self.SnapshotSequence = FakeVariable(0) + self.LockedMask = FakeVariable(getter=self._lockedMask) + self.nodes = { + f'DebugSampleRaw[{channel}][{index}]': FakeVariable( + getter=lambda channel=channel, index=index: self._debugSample(channel, index)) + for channel in range(2) + for index in range(4) + } + self.relockCount = 0 + self.snapshotDelays = [] + self.dataDelayWrites = [] + self.PatternTester = FakePatternTester(self) + + def _lockedMask(self): + return int(2 <= self.FcoDelay[0].value() <= 5) + + def _debugSample(self, channel, index): + tap = self.DataDelay[channel].value() + passing = (1 <= tap <= 4) if channel == 0 else (3 <= tap <= 7) + if not passing: + return 0 + return 0x2AAA if index % 2 == 0 else 0x1555 + + def _getDebugSamples(self, read=False): + return [ + [ + self.nodes[f'DebugSampleRaw[{channel}][{index}]'].get(read=read) + for index in range(4) + ] + for channel in range(self._channels) + ] + + def _getDataDelays(self, read=False): + return [variable.get(read=read) for variable in self.DataDelay.values()] + + def _setDataDelays(self, values): + self.dataDelayWrites.append(dict(values)) + for lane, value in values.items(): + self.DataDelay[lane].set(value, write=True) + + def Snapshot(self): + self.snapshotDelays.append(tuple( + variable.value() for variable in self.DataDelay.values())) + self.SnapshotSequence.set(self.SnapshotSequence.value()+1) + + def Relock(self): + self.relockCount += 1 + + +class CoupledFakeReadout(FakeReadout): + def __init__(self): + super().__init__() + self._channels = 1 + self.nodes = { + f'DebugSampleRaw[0][{index}]': FakeVariable( + getter=lambda index=index: self._debugSample(0, index)) + for index in range(4) + } + + def _debugSample(self, channel, index): + # Both physical lanes form one logical sample. Each lane's guard points + # pass against the selected value of the other lane, but the combination + # of both upper guard points does not. + passing = not ( + self.DataDelay[0].value() == 3 and + self.DataDelay[1].value() == 5) + if not passing: + return 0 + return 0x2AAA if index % 2 == 0 else 0x1555 + + +class MaskedCoupledFakeReadout(FakeReadout): + def __init__(self): + super().__init__() + self._channels = 1 + self.DataDelay = {0: FakeVariable(0), 1: FakeVariable(0)} + self.nodes = { + f'DebugSampleRaw[0][{index}]': FakeVariable( + getter=lambda index=index: self._debugSample(0, index)) + for index in range(4) + } + + def _debugSample(self, channel, index): + expected = 0x2AAA if index % 2 == 0 else 0x1555 + sample = 0 + if 1 <= self.DataDelay[0].value() <= 3: + sample |= expected & 0x003F + if 4 <= self.DataDelay[1].value() <= 6: + sample |= expected & 0x3FC0 + return sample + + +class PhaseMismatchedCoupledFakeReadout(MaskedCoupledFakeReadout): + def _debugSample(self, channel, index): + expected = 0x2AAA if index % 2 == 0 else 0x1555 + opposite = 0x1555 if index % 2 == 0 else 0x2AAA + sample = 0 + if 1 <= self.DataDelay[0].value() <= 3: + sample |= expected & 0x003F + if 4 <= self.DataDelay[1].value() <= 6: + sample |= opposite & 0x3FC0 + return sample + + +class MultiWindowFcoFakeReadout(MaskedCoupledFakeReadout): + def __init__(self): + super().__init__() + self._fcoLanes = 2 + self.FcoDelay = {0: FakeVariable(0), 1: FakeVariable(0)} + self.FcoWord = {0: FakeVariable(0xF0), 1: FakeVariable(0xF0)} + + def _lockedMask(self): + mask = 0 + if self.FcoDelay[0].value() in (1, 2, 3, 5, 6): + mask |= 0x1 + if self.FcoDelay[1].value() in (1, 2, 4, 5, 6): + mask |= 0x2 + return mask + + def _fcoPhaseAligned(self): + lowWindow = ( + self.FcoDelay[0].value() in (1, 2, 3) and + self.FcoDelay[1].value() in (1, 2)) + highWindow = ( + self.FcoDelay[0].value() in (5, 6) and + self.FcoDelay[1].value() in (4, 5, 6)) + return lowWindow or highWindow + + def _debugSample(self, channel, index): + expected = 0x2AAA if index % 2 == 0 else 0x1555 + opposite = 0x1555 if index % 2 == 0 else 0x2AAA + sample = 0 + if 1 <= self.DataDelay[0].value() <= 3: + sample |= expected & 0x003F + if 4 <= self.DataDelay[1].value() <= 6: + upper = expected if self._fcoPhaseAligned() else opposite + sample |= upper & 0x3FC0 + return sample + + +def _pn23Words(state=0x654321, count=4, width=14): + mask = (1 << 23)-1 + words = [] + for _ in range(count): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> 22) & 1) + state = ( + ((state << 1) & mask) | + (((state >> 22) ^ (state >> 17)) & 1)) + words.append(word) + return words + + +class Pn23FakeReadout(FakeReadout): + def __init__(self, config, fault=None): + super().__init__() + self._config = config + self._fault = fault + self._pn23 = _pn23Words(count=5) + + def _debugSample(self, channel, index): + if self._config.OutputTestMode.value() != 5: + return super()._debugSample(channel, index) + if self._fault == 'channelShift' and channel == 1: + return self._pn23[index+1] + sample = self._pn23[index] + if self._fault == 'commonCorruption' and index == 2: + sample ^= 0x1 + return sample + + +class Pn23MultiWindowFcoFakeReadout(MultiWindowFcoFakeReadout): + def __init__(self, config): + super().__init__() + self._config = config + self._pn23 = _pn23Words() + + def _fcoPhaseAligned(self): + # Both preferred and alternate FCO combinations look valid under the + # repetitive checkerboard; PN23 must disambiguate them. + return True + + def _debugSample(self, channel, index): + if self._config.OutputTestMode.value() != 5: + return super()._debugSample(channel, index) + sample = self._pn23[index] + if self.FcoDelay[1].value() == 5 and index == 2: + sample ^= 0x1 + return sample + + +def test_selects_center_and_reports_margins(): + eye = findAdcDdrEye({0: False, 1: True, 2: True, 3: True, 4: True, 5: False}) + + assert eye.start == 1 + assert eye.end == 4 + assert eye.width == 4 + assert eye.selected == 2 + assert eye.leftMargin == 1 + assert eye.rightMargin == 2 + assert eye.leftBounded + assert eye.rightBounded + assert eye.contains(3) + assert not eye.contains(5) + + +def test_tie_selects_lowest_center(): + eye = findAdcDdrEye({ + 0: False, + 1: True, + 2: True, + 3: False, + 4: False, + 5: True, + 6: True, + 7: False, + }) + + assert (eye.start, eye.end, eye.selected) == (1, 2, 1) + + +def test_returns_all_qualifying_eyes_in_priority_order(): + eyes = findAdcDdrEyes({ + 0: False, + 1: True, + 2: True, + 3: False, + 4: True, + 5: True, + 6: True, + 7: False, + }) + + assert [(eye.start, eye.end, eye.selected) for eye in eyes] == [ + (4, 6, 5), + (1, 2, 1), + ] + assert findAdcDdrEye({tap: True for tap in range(4)}) == findAdcDdrEyes( + {tap: True for tap in range(4)})[0] + + +def test_pn23_recurrence_acquires_arbitrary_phase_and_handles_formatting(): + words = _pn23Words() + + result = checkAdcDdrPn23(words, 14) + assert result['passed'] + assert result['selected']['name'] == 'asCaptured' + assert result['selected']['checkedBits'] == 33 + + # Datasheet PN23 values are shown after two's-complement formatting, which + # flips the logical sample MSB relative to the underlying PN bit stream. + formatted = [0x1FFF, 0x1FE0, 0x2001, 0x1C00] + result = checkAdcDdrPn23(formatted, 14) + assert result['passed'] + assert result['selected']['name'] == 'formatMsbInverted' + + corrupted = list(words) + corrupted[2] ^= 0x1 + assert not checkAdcDdrPn23(corrupted, 14)['passed'] + assert not checkAdcDdrPn23([0, 0, 0, 0], 14)['passed'] + + +def test_minimum_width_and_guard_band_reject_narrow_windows(): + passing = {tap: 1 <= tap <= 3 for tap in range(5)} + + assert findAdcDdrEye(passing, minimumWidth=3).width == 3 + assert findAdcDdrEye(passing, guardBand=1).selected == 2 + with pytest.raises(RuntimeError, match='at least 4 taps'): + findAdcDdrEye(passing, minimumWidth=4) + with pytest.raises(RuntimeError, match='at least 5 taps'): + findAdcDdrEye(passing, guardBand=2) + + +def test_scan_boundary_visibility(): + left = findAdcDdrEye({0: True, 1: True, 2: False, 3: False}) + right = findAdcDdrEye({0: False, 1: False, 2: True, 3: True}) + full = findAdcDdrEye({tap: True for tap in range(4)}) + + assert not left.leftBounded and left.rightBounded + assert right.leftBounded and not right.rightBounded + assert not full.leftBounded and not full.rightBounded + + +def test_circular_window_merges_scan_boundaries(): + passing = {tap: tap in (0, 1, 6, 7) for tap in range(8)} + linear = findAdcDdrEye(passing) + circular = findAdcDdrEye(passing, circular=True) + + assert linear.width == 2 + assert (circular.start, circular.end) == (6, 1) + assert circular.width == 4 + assert circular.selected == 7 + assert circular.leftMargin == 1 + assert circular.rightMargin == 2 + assert circular.wraps + assert circular.leftBounded and circular.rightBounded + assert circular.contains(0) + assert circular.contains(7) + assert not circular.contains(3) + + +@pytest.mark.parametrize( + ('passing', 'kwargs', 'exception', 'message'), + [ + ({}, {}, ValueError, 'must not be empty'), + ({0: False, 1: False}, {}, RuntimeError, 'no passing'), + ({0: True, 2: True}, {}, ValueError, 'consecutive'), + ({0: True}, {'minimumWidth': 0}, ValueError, 'at least one'), + ({0: True}, {'guardBand': -1}, ValueError, 'must not be negative'), + ]) +def test_rejects_invalid_scans(passing, kwargs, exception, message): + with pytest.raises(exception, match=message): + findAdcDdrEye(passing, **kwargs) + + +def make_calibration(): + config = FakeConfig() + readout = FakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + calibration._testRoot = root + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + return calibration, config, readout, root + + +def test_ad9681_topology_builds_one_parallel_lane_group(): + config = FakeConfig() + readout = FakeReadout() + readout._dataLanes = 16 + readout._channels = 8 + + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=tuple(range(8))+tuple(range(8)), + dataLaneMasks=(0x003F,)*8+(0x3FC0,)*8) + + assert calibration._dataLaneGroups == (tuple(range(16)),) + + +@pytest.fixture +def calibration_fixture(): + calibration, config, readout, root = make_calibration() + try: + yield calibration, config, readout + finally: + calibration._runEn = False + root.stop() + + +def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): + calibration, config, readout = calibration_fixture + + assert calibration.Debug.value() is True + assert calibration.UsePatternTester.value() is False + results = calibration._runCalibration(dev=calibration) + + assert results['Fco'][0]['eye']['selected'] == 3 + assert results['Data'][0]['eye']['selected'] == 2 + assert results['Data'][1]['eye']['selected'] == 5 + assert results['Final']['passed'] + assert config.OutputTestMode.value() == 0 + assert config.updateCount == 2 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + assert calibration._dataLaneGroups == ((0, 1),) + assert readout.dataDelayWrites[:8] == [ + {0: tap, 1: tap} + for tap in range(8) + ] + assert readout.dataDelayWrites[8] == {0: 2, 1: 5} + assert readout.snapshotDelays == [(tap, tap) for tap in range(8)] + [(2, 5)] + assert calibration.RunTime.value() > 0.0 + + readout.FcoDelay[0].set(0) + readout.DataDelay[0].set(0) + readout.DataDelay[1].set(0) + calibration.applyResults() + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + + +def test_full_calibration_can_use_pattern_tester(calibration_fixture): + calibration, _, readout = calibration_fixture + calibration.UsePatternTester.set(True) + + results = calibration._runCalibration(dev=calibration) + + assert results['Data'][0]['eye']['selected'] == 2 + assert results['Data'][1]['eye']['selected'] == 5 + assert results['Final']['passed'] + assert readout.snapshotDelays == [] + assert len(readout.PatternTester.measurements) == 9 + assert all( + measurement['samples'] == 4 + for measurement in readout.PatternTester.measurements) + assert calibration.Diagnostics.value()['MeasurementBackend'] == 'PatternTester' + assert calibration.RunTime.value() > 0.0 + + +@pytest.mark.parametrize('usePatternTester', [False, True]) +def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): + config = FakeConfig() + readout = Pn23FakeReadout(config) + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update, + pn23Reset=config.ResetPNLong) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(usePatternTester) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + assert calibration.VerifyPn23.value() + assert results['Final']['checkerboardPassed'] + assert results['Final']['pn23']['enabled'] + assert results['Final']['pn23']['performed'] + assert results['Final']['pn23']['coherencePassed'] + assert results['Final']['pn23']['recurrence']['passed'] + assert results['Final']['pn23']['passed'] + assert results['Final']['passed'] + assert config.OutputTestMode.value() == 0 + assert not config.ResetPNLong.value() + # PN23 always uses one atomic debug snapshot, independently of the data + # eye and checkerboard backend selected for the rest of calibration. + assert readout.snapshotDelays[-1] == (2, 5) + finally: + calibration._runEn = False + root.stop() + + +@pytest.mark.parametrize( + ('fault', 'coherencePassed', 'recurrencePassed'), + [ + ('channelShift', False, True), + ('commonCorruption', True, False), + ]) +def test_pn23_qualification_rejects_relative_and_common_errors( + fault, coherencePassed, recurrencePassed): + config = FakeConfig() + readout = Pn23FakeReadout(config, fault=fault) + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update, + pn23Reset=config.ResetPNLong) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + pn23 = calibration.Results.value()['Final']['pn23'] + assert pn23['coherencePassed'] is coherencePassed + assert pn23['recurrence']['passed'] is recurrencePassed + assert not pn23['passed'] + assert config.OutputTestMode.value() == 0 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + finally: + calibration._runEn = False + root.stop() + + +def test_pattern_tester_selection_requires_hardware_capability(calibration_fixture): + calibration, _, readout = calibration_fixture + calibration.UsePatternTester.set(True) + readout.PatternCheck.set(False) + + with pytest.raises(RuntimeError, match='pattern tester is not present'): + calibration._runCalibration(dev=calibration) + + assert calibration.RunTime.value() > 0.0 + + +def test_run_time_updates_while_calibration_is_running(calibration_fixture): + calibration, _, _ = calibration_fixture + started = threading.Event() + release = threading.Event() + calibration.RUN_TIME_UPDATE_INTERVAL_C = 0.01 + + def operation(*, dev): + started.set() + assert release.wait(1.0) + + calibration._runCalibrationImpl = operation + worker = threading.Thread( + target=calibration._runCalibration, + kwargs={'dev': calibration}) + worker.start() + try: + assert started.wait(1.0) + deadline = time.monotonic()+1.0 + while calibration.RunTime.value() == 0.0 and time.monotonic() < deadline: + time.sleep(0.005) + runningTime = calibration.RunTime.value() + assert runningTime > 0.0 + finally: + release.set() + worker.join(1.0) + + assert not worker.is_alive() + assert calibration.RunTime.value() >= runningTime + + +def test_failed_calibration_restores_mode_and_all_delays(calibration_fixture): + calibration, config, readout = calibration_fixture + calibration.MinimumEyeWidth.set(6) + config.OutputTestMode.set(7) + + with pytest.raises(RuntimeError, match='at least 6 taps'): + calibration._runCalibration(dev=calibration) + + assert config.OutputTestMode.value() == 7 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + assert calibration.RunTime.value() > 0.0 + + +def test_failed_data_scan_reports_lane_and_retains_partial_results(calibration_fixture): + calibration, _, readout = calibration_fixture + calibration.Debug.set(True) + for node in readout.nodes.values(): + node._getter = lambda: 0 + + with pytest.raises(RuntimeError, match='Data lane 0: scan contains no passing'): + calibration._runCalibration(dev=calibration) + + results = calibration.Results.value() + assert results['Fco'][0]['eye']['selected'] == 3 + assert not any(results['Data'][0]['passing'].values()) + assert results['Data'][0]['diagnostics'][0]['expected'] == [0x1555, 0x2AAA] + assert results['Data'][0]['diagnostics'][0]['captures'][0]['raw'] == [0, 0, 0, 0] + diagnostics = calibration.Diagnostics.value() + assert diagnostics['Fco'][0][0]['word'] == 0x3F00 + assert diagnostics['Current'] == {'kind': 'Data', 'lanes': [0, 1], 'tap': 7} + + +def test_verify_current_and_guard_band_restore_hardware_state(calibration_fixture): + calibration, config, readout = calibration_fixture + calibration.Operation.set(calibration.VERIFY_CURRENT_C) + + results = calibration._runCalibration(dev=calibration) + assert all(result['passed'] for lanes in results.values() for result in lanes.values()) + assert calibration.TotalSteps.value() == 3 + assert calibration.Step.value() == 3 + assert calibration.Progress.value() == 1.0 + + calibration.Operation.set(calibration.VERIFY_GUARD_BAND_C) + calibration.GuardBand.set(2) + with pytest.raises(RuntimeError, match='guard-band verification failed'): + calibration._runCalibration(dev=calibration) + + assert not calibration.Results.value()['Data'][0]['passed'] + assert config.OutputTestMode.value() == 0 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + assert calibration.TotalSteps.value() == 9 + assert calibration.Step.value() == 9 + assert calibration.Progress.value() == 1.0 + assert calibration.RunTime.value() > 0.0 + + +def test_guard_verification_restores_each_coupled_lane_before_advancing(): + config = FakeConfig() + readout = CoupledFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + assert calibration._dataLaneGroups == ((0,), (1,)) + results, passed = calibration._verifyCurrent([3], [2, 4], 0.0, 1) + + assert passed + assert all(result['passed'] for lanes in results.values() for result in lanes.values()) + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + finally: + calibration._runEn = False + root.stop() + + +@pytest.mark.parametrize('usePatternTester', [False, True]) +def test_full_calibration_masks_unaligned_partner_lane(usePatternTester): + config = FakeConfig() + readout = MaskedCoupledFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(usePatternTester) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + assert calibration._dataLaneGroups == ((0, 1),) + assert results['Data'][0]['eye']['selected'] == 2 + assert results['Data'][1]['eye']['selected'] == 5 + assert results['Final']['passed'] + if usePatternTester: + assert readout.snapshotDelays == [] + assert [measurement['delays'] for measurement in readout.PatternTester.measurements] == ( + [delays for tap in range(8) for delays in ((tap, tap), (tap, tap))] + + [(2, 5)]) + assert [measurement['dataMask'] for measurement in + readout.PatternTester.measurements[:-1]] == [0x003F, 0x3FC0]*8 + else: + assert readout.snapshotDelays == ( + [(tap, tap) for tap in range(8)] + + [(2, 5)]) + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + finally: + calibration._runEn = False + root.stop() + + +def test_final_qualification_rejects_split_lane_phase_mismatch(): + config = FakeConfig() + readout = PhaseMismatchedCoupledFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + results = calibration.Results.value() + assert all('eye' in results['Data'][lane] for lane in range(2)) + assert not results['Final']['passed'] + assert not results['Final']['captures'][0]['channels'][0]['passed'] + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 0 + assert readout.DataDelay[1].value() == 0 + with pytest.raises(RuntimeError, match='No complete full-calibration result'): + calibration.applyResults() + finally: + calibration._runEn = False + root.stop() + + +@pytest.mark.parametrize('usePatternTester', [False, True]) +def test_final_qualification_retries_alternate_fco_eye_combinations(usePatternTester): + config = FakeConfig() + readout = MultiWindowFcoFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(2) + calibration.GuardBand.set(0) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(usePatternTester) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + assert len(results['Fco'][0]['eyes']) == 2 + assert len(results['Fco'][1]['eyes']) == 2 + assert results['Final']['passed'] + assert [attempt['fcoDelays'] for attempt in results['Final']['attempts']] == [ + [2, 5], + [2, 1], + ] + assert not results['Final']['attempts'][0]['passed'] + assert results['Final']['attempts'][1]['passed'] + assert results['Fco'][0]['eye']['selected'] == 2 + assert results['Fco'][1]['eye']['selected'] == 1 + assert readout.FcoDelay[0].value() == 2 + assert readout.FcoDelay[1].value() == 1 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + finally: + calibration._runEn = False + root.stop() + + +def test_pn23_failure_retries_alternate_fco_eye_combination_in_checkerboard_mode(): + config = FakeConfig() + readout = Pn23MultiWindowFcoFakeReadout(config) + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update, + pn23Reset=config.ResetPNLong) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(2) + calibration.GuardBand.set(0) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + attempts = results['Final']['attempts'] + assert [attempt['fcoDelays'] for attempt in attempts] == [[2, 5], [2, 1]] + assert all( + attempt['qualification']['checkerboardPassed'] + for attempt in attempts) + assert not attempts[0]['qualification']['pn23']['passed'] + assert attempts[1]['qualification']['pn23']['passed'] + assert attempts[1]['passed'] + assert results['Fco'][1]['eye']['selected'] == 1 + finally: + calibration._runEn = False + root.stop() + + +def test_cancellation_is_graceful_and_restores_hardware(calibration_fixture): + calibration, config, readout = calibration_fixture + originalCheck = calibration._checkRun + checks = 0 + + def stop_during_fco_scan(): + nonlocal checks + checks += 1 + if checks == 3: + calibration._runEn = False + originalCheck() + + calibration._checkRun = stop_during_fco_scan + calibration._process() + + assert calibration.Message.value() == 'Calibration stopped' + assert calibration.Step.value() == 2 + assert calibration.Progress.value() < 1.0 + assert config.OutputTestMode.value() == 0 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + assert calibration.RunTime.value() > 0.0 diff --git a/tests/devices/analog_devices/test_AdcDdrCore.py b/tests/devices/analog_devices/test_AdcDdrCore.py new file mode 100644 index 0000000000..0101729fb5 --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrCore.py @@ -0,0 +1,313 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Integrate two data lanes/channels and one 14-bit FCO lane with the +# optional pattern engine both enabled and disabled. +# - Stimulus: Access the relocatable block at a nonzero absolute AXI address, +# verify hardware-owned startup waits for delay readiness and loads every +# configured delay, exercise the manual reset/reload path and required DDR +# bitslip quiet interval, acquire lock, stream samples, load every delay +# class, and snapshot sample history. +# - Checks: AXI status follows alignment, streams preserve both channels and +# sidebands, delay requests are width-limited and read back, snapshot writes +# reject reset state and block until publication, and AXI transactions +# execute coherently in the capture domain; the pattern window is +# capability-gated and reports its shared-phase result through AXI-Lite. +# - Timing: AXI-Lite is crossed as a complete bus into the capture clock domain; +# one wide FIFO crosses coherent channel samples to the stream clock. + +import os + +import cocotb +import pytest +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp + +from tests.axi.utils import axil_read_u32 as _axil_read_u32 +from tests.axi.utils import axil_write_u32 as _axil_write_u32 +from tests.common.regression_utils import run_surf_vhdl_test + + +AXIL_BASE_ADDR = 0xC100_0000 + + +def negate14(value): + return (-value) & 0x3FFF + + +async def axil_read_u32(axil, address): + return await _axil_read_u32(axil, AXIL_BASE_ADDR + address) + + +async def axil_write_u32(axil, address, value): + await _axil_write_u32(axil, AXIL_BASE_ADDR + address, value) + + +async def axil_poll(axil, address, predicate, limit=128): + for _ in range(limit): + value = await axil_read_u32(axil, address) + if predicate(value): + return value + assert False, f"AXI register 0x{address:03X} did not reach expected state" + + +async def wait_load(clock, signal, mask, limit=80): + for _ in range(limit): + await RisingEdge(clock) + await Timer(2, unit="ns") + if int(signal.value) & mask: + return + assert False, "delay-load pulse was not observed" + + +@cocotb.test() +async def core_integration_test(dut): + dut.axilRst.value = 1 + dut.captureRst.value = 1 + dut.streamRst.value = 1 + dut.delayReady.value = 0 + dut.fcoWord.value = 0b11111110000000 + dut.fcoValid.value = 0 + dut.sampleValid.value = 0 + dut.sampleIn.value = 0 + cocotb.start_soon(Clock(dut.axilClk, 5, unit="ns").start()) + cocotb.start_soon(Clock(dut.captureClk, 8, unit="ns").start()) + cocotb.start_soon(Clock(dut.streamClk, 11, unit="ns").start()) + + for _ in range(5): + await RisingEdge(dut.axilClk) + dut.axilRst.value = 0 + dut.captureRst.value = 0 + dut.streamRst.value = 0 + await Timer(500, unit="ns") + axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "S_AXI"), dut.axilClk, dut.axilRst) + await RisingEdge(dut.axilClk) + assert int(dut.S_AXI_BVALID.value) == 0 + await Timer(200, unit="ns") + status = await axil_read_u32(axil, 0x01C) + assert status & 0x07 == 0 + assert await axil_read_u32(axil, 0x00C) & 0x01 == 0 + assert int(dut.phyReset.value) == 1 + response = await axil.write(AXIL_BASE_ADDR + 0x014, (0x01).to_bytes(4, 'little')) + assert response.resp == AxiResp.SLVERR + assert await axil_read_u32(axil, 0x024) == 0 + + data_load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 0x3)) + fco_load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 0x1)) + await FallingEdge(dut.captureClk) + dut.delayReady.value = 1 + await data_load + await fco_load + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 0 + + status = await axil_read_u32(axil, 0x01C) + assert status & 0x07 == 0x02 + + # A 7-series DDR ISERDES needs three quiet CLKDIV cycles after each + # one-cycle BITSLIP request before another request can be issued. + await FallingEdge(dut.captureClk) + dut.fcoWord.value = 0 + dut.fcoValid.value = 1 + slip_cycles = [] + for cycle in range(20): + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + if int(dut.bitSlip.value): + slip_cycles.append(cycle) + if len(slip_cycles) == 1: + # A transient pre-settle match must not end the quiet interval. + dut.fcoWord.value = 0b11111110000000 + if len(slip_cycles) == 3: + break + if len(slip_cycles) == 1 and cycle == slip_cycles[0]+1: + dut.fcoWord.value = 0 + assert len(slip_cycles) == 3 + assert all(b-a >= 4 for a, b in zip(slip_cycles, slip_cycles[1:])) + + await FallingEdge(dut.captureClk) + dut.fcoWord.value = 0b11111110000000 + status = await axil_poll(axil, 0x01C, lambda value: (value & 0x07) == 0x06) + assert status & 0x07 == 0x06 + assert await axil_read_u32(axil, 0x300) == 0b11111110000000 + + # Manual reset remains available, but release is also hardware sequenced + # and reapplies every retained delay without any additional software step. + await axil_write_u32(axil, 0x00C, 0x01) + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0) + assert int(dut.phyReset.value) == 1 + dut.delayReady.value = 0 + data_load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 0x3)) + fco_load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 0x1)) + await axil_write_u32(axil, 0x00C, 0) + for _ in range(5): + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 1 + assert int(dut.dataDelayLoad.value) == 0 + assert int(dut.fcoDelayLoad.value) == 0 + await FallingEdge(dut.captureClk) + dut.delayReady.value = 1 + await data_load + await fco_load + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 0 + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0x04) + + await FallingEdge(dut.captureClk) + dut.sampleIn.value = 0x2345_1234 + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + for _ in range(40): + await FallingEdge(dut.streamClk) + await Timer(1, unit="ns") + if int(dut.streamValid.value) == 3: + assert int(dut.streamData.value) == (negate14(0x2345) << 16) | negate14(0x1234) + assert int(dut.streamKeep.value) == 0xF + assert int(dut.streamDest.value) == 0x0100 + assert int(dut.streamLast.value) == 0 + assert int(dut.streamUser.value) == 0 + break + else: + assert False, "sample did not cross to stream clock" + + # Preserve sample cadence during loss of alignment and mark each affected + # channel with ordinary AXI Stream tUser(0), without SSI framing. + dut.fcoWord.value = 0 + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0) + await FallingEdge(dut.captureClk) + dut.sampleIn.value = 0x2567_3456 + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + for _ in range(40): + await FallingEdge(dut.streamClk) + await Timer(1, unit="ns") + if int(dut.streamValid.value) == 3: + assert int(dut.streamData.value) == (negate14(0x2567) << 16) | negate14(0x3456) + assert int(dut.streamLast.value) == 0 + assert int(dut.streamUser.value) == 0x0101 + break + else: + assert False, "unaligned sample did not cross to stream clock" + + dut.fcoWord.value = 0b11111110000000 + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0x04) + + # Observe each acknowledged load and feed the applied value back as the + # logical PHY's current-delay status. + load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 1)) + await axil_write_u32(axil, 0x100, 0x12) + await load + assert int(dut.dataDelayValue.value) & 0xFFFF == 0x12 + + # The default five-bit core must never forward discarded AXI write bits to + # the shared nine-bit PHY command. + load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 1)) + await axil_write_u32(axil, 0x100, 0x1FF) + await load + assert int(dut.dataDelayValue.value) & 0xFFFF == 0x1F + assert await axil_read_u32(axil, 0x100) == 0x1F + + load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 2)) + await axil_write_u32(axil, 0x104, 0x13) + await load + + load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 1)) + await axil_write_u32(axil, 0x200, 0x14) + await load + await axil_poll(axil, 0x104, lambda value: value == 0x13) + await axil_poll(axil, 0x200, lambda value: value == 0x14) + + dut.sampleIn.value = 0x2000_1000 + snapshot = cocotb.start_soon(axil_write_u32(axil, 0x014, 0x01)) + await Timer(500, unit="ns") + assert not snapshot.done() + for index in range(4): + await FallingEdge(dut.captureClk) + dut.sampleIn.value = ((0x2000 + index) << 16) | (0x1000 + index) + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + if index != 3: + assert not snapshot.done() + await snapshot + assert await axil_read_u32(axil, 0x024) == 1 + assert await axil_read_u32(axil, 0x600) == 0x1000 + assert await axil_read_u32(axil, 0x610) == 0x2000 + assert await axil_read_u32(axil, 0x000) == 0x00010000 + assert await axil_read_u32(axil, 0x004) == 0x0E020102 + assert await axil_read_u32(axil, 0x500) == 0 + response = await axil.read(AXIL_BASE_ADDR + 0x028, 4) + assert response.resp == AxiResp.DECERR + pattern_check = os.getenv('PATTERN_CHECK_G', 'true').lower() == 'true' + assert await axil_read_u32(axil, 0x008) == (0x00010E05 if pattern_check else 0x00000E05) + + if not pattern_check: + response = await axil.read(AXIL_BASE_ADDR + 0x800, 4) + assert response.resp == AxiResp.DECERR + return + + # Run one shared-phase alternating-pattern window through the integrated + # AXI-Lite register map. Both channels acquire B first and then A. + await axil_write_u32(axil, 0x808, 0x00000001) + await axil_write_u32(axil, 0x80C, 0x00000003) + await axil_write_u32(axil, 0x810, 0x00000001) + await axil_write_u32(axil, 0x814, 0x00003FFF) + await axil_write_u32(axil, 0x818, 0x00001555) + await axil_write_u32(axil, 0x81C, 0x00002AAA) + await axil_write_u32(axil, 0x820, 0x00000002) + await axil_write_u32(axil, 0x824, 0x00000020) + await axil_write_u32(axil, 0x800, 0x00000001) + await axil_poll(axil, 0x828, lambda value: value & 0x01) + for sample in (0x2AAA, 0x1555): + await FallingEdge(dut.captureClk) + dut.sampleIn.value = (sample << 16) | sample + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + await axil_poll(axil, 0x82C, lambda value: value == 1) + assert await axil_read_u32(axil, 0x828) == 0x70 + assert await axil_read_u32(axil, 0x830) == 2 + assert await axil_read_u32(axil, 0x834) == 3 + assert await axil_read_u32(axil, 0x838) == 1 + assert await axil_read_u32(axil, 0x840) == 0 + assert await axil_read_u32(axil, 0x844) == 0 + assert await axil_read_u32(axil, 0x8C0) == 0 + + +@pytest.mark.parametrize('pattern_check', (True, False)) +def test_AdcDdrCore(pattern_check): + sources = [ + "devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd", + "devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd", + "devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd", + "devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd", + "devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd", + ] + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.adcddrcorewrapper", + parameters={ + 'AXIL_BASE_ADDR_G': f'{AXIL_BASE_ADDR:032b}', + 'NEGATE_G': True, + 'PATTERN_CHECK_G': pattern_check, + }, + extra_vhdl_sources={"surf": sources}, + ) diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py new file mode 100644 index 0000000000..f44ff16d06 --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -0,0 +1,533 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Construct a non-default normalized map and all three device-specific +# calibration adapters, including the two-lane AD9681 topology. +# - Stimulus: Use Rogue's memory emulator to write control and every delay lane; +# construct calibration adapters with real device configuration models. +# - Checks: Verify register offsets, field widths/modes, memory readback, range +# enforcement, blocking-command snapshot transaction count, bulk raw-snapshot +# reads and grouped delay writes, staged-update callbacks, and physical-lane +# channel mappings. +# - Timing: Memory transactions are synchronous software operations; ADC mode, +# snapshot, and settling behavior is covered by the calibration mock test. + +import pytest +import pyrogue as pr +import rogue +import rogue.interfaces.memory as rim + +from surf.devices.analog_devices import ( + Ad9249Config, + Ad9249ConfigGroup, + Ad9249ChipConfig, + Ad9249Readout, + Ad9249ReadoutCalibration, + Ad9249ReadoutBank, + Ad9249ReadoutBankCalibration, + Ad9252Config, + Ad9252Readout, + Ad9252ReadoutCalibration, + Ad9681Config, + Ad9681Readout, + Ad9681ReadoutCalibration, + AdcDdr, + adcDdrDelayBits, +) + + +class CountingMemory(rim.Slave): + def __init__(self, size=0x10000): + super().__init__(4, size) + self._memory = bytearray(size) + self.transactions = [] + + def _doTransaction(self, transaction): + address = transaction.address() + size = transaction.size() + transactionType = transaction.type() + self.transactions.append((transactionType, address, size)) + with transaction.lock(): + if transactionType in (rim.Write, rim.Post): + data = bytearray(size) + transaction.getData(data, 0) + self._memory[address:address+size] = data + else: + transaction.setData(self._memory[address:address+size], 0) + transaction.done() + + +@pytest.fixture +def memory_backed_readout(): + memory = rim.Emulate(4, 0x10000) + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name = 'Readout', + memBase = memory, + dataLanes = 3, + fcoLanes = 2, + channels = 2, + sampleBits = 14, + serializationFactor = 8, + delayBits = 9) + root.add(readout) + root.start() + try: + yield readout + finally: + root.stop() + + +def test_normalized_map_geometry_and_memory_readback(memory_backed_readout): + readout = memory_backed_readout + + assert readout.Version.offset == 0x000 + for name, offset, bitOffset, bitSize in ( + ('DataLanes', 0x004, 0, 8), + ('FcoLanes', 0x004, 8, 8), + ('Channels', 0x004, 16, 8), + ('SampleBits', 0x004, 24, 8), + ('DelayBits', 0x008, 0, 8), + ('SerializationFactor', 0x008, 8, 8), + ('PatternCheck', 0x008, 16, 1)): + variable = readout.nodes[name] + assert variable.offset == offset + assert variable.bitOffset == [bitOffset] + assert variable.bitSize == [bitSize] + assert variable.mode == 'RO' + for name in ('DataLanes', 'FcoLanes', 'Channels', 'SampleBits', 'SerializationFactor'): + assert readout.nodes[name].hidden + assert not readout.DelayBits.hidden + assert 'Capabilities0' not in readout.nodes + assert 'Capabilities1' not in readout.nodes + assert readout.CaptureReset.offset == 0x00C + assert readout.Relock.offset == 0x010 + assert readout.Snapshot.offset == 0x014 + assert readout.ClearCounters.offset == 0x018 + assert readout.DelayReady.offset == 0x01C + assert readout.LockedMask.offset == 0x020 + assert readout.SnapshotSequence.offset == 0x024 + assert readout.Relock.bitOffset == [0] + assert readout.Snapshot.bitOffset == [0] + assert readout.ClearCounters.bitOffset == [0] + pattern = readout.PatternTester + assert pattern.offset == 0x800 + assert pattern.Start.offset == 0x000 + assert pattern.Abort.offset == 0x004 + assert pattern.Start.bitOffset == [0] + assert pattern.Abort.bitOffset == [0] + assert pattern.Alternating.address == 0x808 + assert pattern.ReferenceChannel.bitOffset == [8] + assert pattern.ChannelMask.bitSize == [2] + assert pattern.FcoMask.bitSize == [2] + assert pattern.DataMask.bitSize == [14] + assert pattern.Samples.address == 0x820 + assert pattern.Busy.address == 0x828 + assert pattern.AllFcoPass.bitOffset == [6] + assert pattern.Sequence.address == 0x82C + assert pattern.ChannelPassed.address == 0x834 + assert pattern.WordErrorCount[1].address == 0x844 + assert pattern.BitErrorMask[1].address == 0x884 + assert pattern.FcoErrorCount[1].address == 0x8C4 + assert not any(name.startswith('Pattern') and + name not in ('PatternCheck', 'PatternTester') + for name in readout.nodes) + assert readout.LockedMask.bitSize == [2] + assert 'ClockPresent' not in readout.nodes + assert 'OverflowMask' not in readout.nodes + assert readout.DataDelayBulk.offset == 0x100 + assert readout.DataDelayBulk.numValues == 3 + assert readout.DataDelayBulk.valueBits == 9 + assert readout.DataDelayBulk.valueStride == 32 + assert readout.DataDelayBulk.hidden + assert readout.DataDelay[2].mode == 'RW' + assert readout.DataDelay[2].minimum == 0 + assert readout.DataDelay[2].maximum == 511 + assert readout.FcoDelay[1].offset == 0x204 + assert readout.FcoWord[1].offset == 0x304 + assert readout.FcoWord[1].bitSize == [8] + assert readout.LostLockCount[1].offset == 0x344 + assert readout.OverflowCount.offset == 0x500 + assert readout.DebugSampleRaw.offset == 0x600 + assert readout.DebugSampleRaw.numValues == 8 + assert readout.DebugSampleRaw.valueBits == 14 + assert readout.DebugSampleRaw.valueStride == 32 + assert readout.DebugSampleRaw.varBytes == 32 + assert readout.nodes['DebugVoltage[1]'].units == 'V' + assert readout.DebugVoltageRange.get() == 2.0 + assert readout.DebugVoltageFormat.get() == 1 + assert isinstance(readout.nodes['DebugVoltage[0]'].get(read=True), float) + nodeNames = list(readout.nodes) + assert max(nodeNames.index(f'DebugSample[{channel}]') for channel in range(2)) < ( + min(nodeNames.index(f'DebugVoltage[{channel}]') for channel in range(2))) + assert readout.Version.mode == 'RO' + assert isinstance(readout.DataDelay[0], pr.LinkVariable) + + readout.CaptureReset.set(True) + assert readout.CaptureReset.get(read=True) is True + pattern.Alternating.set(True) + pattern.ChannelMask.set(3) + pattern.DataMask.set(0x3FFF) + assert pattern.Alternating.get(read=True) is True + assert pattern.ChannelMask.get(read=True) == 3 + assert pattern.DataMask.get(read=True) == 0x3FFF + for lane, value in enumerate((17, 255, 511)): + readout.DataDelay[lane].set(value) + assert readout.DataDelay[lane].get(read=True) == value + for lane, value in enumerate((31, 300)): + readout.FcoDelay[lane].set(value) + assert readout.FcoDelay[lane].get(read=True) == value + + with pytest.raises(rogue.GeneralError, match='Value range error'): + readout.DataDelay[0].set(512) + + +def test_debug_sample_array_uses_one_bulk_transaction(): + memory = CountingMemory() + expected = [0x100+index for index in range(32)] + for index, sample in enumerate(expected): + offset = 0x600 + 4*index + memory._memory[offset:offset+4] = sample.to_bytes(4, 'little') + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + channels=8, + sampleBits=14) + root.add(readout) + root.start() + try: + memory.transactions.clear() + samples = readout.DebugSampleRaw.get(read=True) + + assert samples.shape == (32,) + assert samples.tolist() == expected + assert readout._getDebugSamples(read=False) == [ + expected[4*channel:4*(channel+1)] + for channel in range(8) + ] + assert readout.nodes['DebugSample[1]'].get(read=False) == ( + '0x0104_0105_0106_0107') + assert readout.nodes['DebugVoltage[1]'].get(read=False) == 0.03173828125 + assert memory.transactions == [(rim.Read, 0x600, 128)] + assert readout.DebugSampleRaw._block.size == 128 + finally: + root.stop() + + +@pytest.mark.parametrize( + ('sample', 'inputRange', 'offsetBinary', 'expected'), + ( + (0x0000, 2.0, False, 0.0), + (0x1FFF, 2.0, False, 0.9998779296875), + (0x2000, 2.0, False, -1.0), + (0x0000, 2.0, True, -1.0), + (0x3FFF, 2.0, True, 0.9998779296875), + (0x0800, 1.0, False, 0.125), + ), +) +def test_debug_voltage_conversion_through_public_model( + sample, inputRange, offsetBinary, expected): + memory = CountingMemory() + memory._memory[0x600:0x604] = sample.to_bytes(4, 'little') + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + channels=1, + sampleBits=14) + root.add(readout) + root.start() + try: + readout.DebugVoltageRange.set(inputRange) + readout.DebugVoltageFormat.set(0 if offsetBinary else 1) + + assert readout.nodes['DebugVoltage[0]'].get(read=True) == expected + finally: + root.stop() + + +def test_snapshot_uses_one_command_and_one_bulk_read(): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + channels=8, + sampleBits=14) + root.add(readout) + root.start() + try: + memory.transactions.clear() + snapshots = readout.Snapshot() + + assert snapshots == ['0x0000_0000_0000_0000']*8 + assert memory.transactions == [ + (rim.Write, 0x014, 4), + (rim.Read, 0x600, 128), + ] + finally: + root.stop() + + +def test_data_delay_bulk_view_preserves_scalar_access_and_groups_transactions(): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + dataLanes=16, + channels=8, + sampleBits=14) + root.add(readout) + root.start() + try: + memory.transactions.clear() + readout.DataDelay[3].set(7, write=True) + + assert memory.transactions == [ + (rim.Write, 0x10C, 4), + (rim.Verify, 0x10C, 4), + ] + assert readout.DataDelayBulk.get(read=False, index=3) == 7 + + memory.transactions.clear() + readout._setDataDelays({lane: 11 for lane in range(8)}) + + assert memory.transactions == [ + (rim.Write, 0x100, 32), + (rim.Verify, 0x100, 32), + ] + assert readout._getDataDelays(read=False) == [11]*8 + [0]*8 + assert [readout.DataDelay[lane].get(read=False) for lane in range(16)] == ( + [11]*8 + [0]*8) + finally: + root.stop() + + +def test_device_specific_calibration_adapters(): + ad9249Config = Ad9249ConfigGroup(name='Ad9249Config') + ad9249Readout = Ad9249ReadoutBank(name='Ad9249Readout') + ad9249 = Ad9249ReadoutBankCalibration( + name='Ad9249Calibration', + config=ad9249Config, + readout=ad9249Readout) + assert ad9249._dataLaneToChannel == tuple(range(8)) + assert ad9249._configUpdate is None + + ad9249Full = Ad9249ReadoutCalibration( + name='Ad9249FullCalibration', + config=Ad9249Config(name='Ad9249FullConfig'), + readout=Ad9249Readout(name='Ad9249FullReadout')) + assert len(ad9249Full.Bank) == 2 + assert all(isinstance(process, Ad9249ReadoutBankCalibration) + for process in ad9249Full.Bank.values()) + + ad9252Config = Ad9252Config(name='Ad9252Config') + ad9252Readout = Ad9252Readout(name='Ad9252Readout') + ad9252 = Ad9252ReadoutCalibration( + name='Ad9252Calibration', + config=ad9252Config, + readout=ad9252Readout) + assert ad9252._dataLaneToChannel == tuple(range(8)) + assert ad9252._configUpdate == ad9252Config.DeviceUpdate + + ad9681Config = Ad9681Config(name='Ad9681Config') + ad9681Readout = Ad9681Readout(name='Ad9681Readout') + ad9681 = Ad9681ReadoutCalibration( + name='Ad9681Calibration', + config=ad9681Config, + readout=ad9681Readout) + assert ad9681._dataLaneToChannel == tuple(range(8))+tuple(range(8)) + assert ad9681._configUpdate is None + assert ad9681.UsePatternTester.value() is False + assert ad9681.UsePatternTester.mode == 'RW' + assert ad9681.RunTime.value() == 0.0 + assert ad9681.RunTime.mode == 'RO' + assert ad9681.RunTime.units == 's' + + +def test_ad9252_config_register_fields(): + config = Ad9252Config(name='Config') + + assert config.UserTestMode.offset == 0x34 + assert config.UserTestMode.bitOffset == [6] + assert config.UserTestMode.bitSize == [2] + assert config.DcoFcoDrive2x.description == 'Set DCO and FCO output drive strength.' + + +def test_ad9249_chip_config_constructs_both_banks(): + config = Ad9249ChipConfig() + + assert config.BankConfig[0].offset == 0x0000 + assert config.BankConfig[1].offset == 0x0200 + + +def test_ad9249_resolution_sample_rate_override_register_fields(): + config = Ad9249ConfigGroup(name='Config') + + assert config.DeviceUpdate.offset == 0x3FC + assert config.ResolutionSampleRateOverride.offset == 0x400 + assert config.ResolutionSampleRateOverride.bitOffset == [6] + assert config.ResolutionSampleRateOverride.bitSize == [1] + assert config.Resolution.offset == 0x400 + assert config.Resolution.bitOffset == [4] + assert config.Resolution.bitSize == [2] + assert config.SampleRate.offset == 0x400 + assert config.SampleRate.bitOffset == [0] + assert config.SampleRate.bitSize == [3] + + +def test_ad9681_resolution_sample_rate_override_register_fields(): + config = Ad9681Config(name='Config') + + assert config.DeviceUpdate.offset == 0x3FC + assert config.ResolutionSampleRateOverride.offset == 0x400 + assert config.ResolutionSampleRateOverride.bitOffset == [6] + assert config.ResolutionSampleRateOverride.bitSize == [1] + assert config.Resolution.offset == 0x400 + assert config.Resolution.bitOffset == [4] + assert config.Resolution.bitSize == [2] + assert config.SampleRate.offset == 0x400 + assert config.SampleRate.bitOffset == [0] + assert config.SampleRate.bitSize == [3] + + +@pytest.mark.parametrize('configType', (Ad9249ConfigGroup, Ad9252Config, Ad9681Config)) +def test_config_write_blocks_forwards_arguments(monkeypatch, configType): + calls = [] + + def writeBlocks(self, **kwargs): + calls.append(kwargs) + + monkeypatch.setattr(pr.Device, 'writeBlocks', writeBlocks) + config = configType(name='Config') + updates = [] + config.DeviceUpdate = lambda: updates.append(True) + variable = object() + + config.writeBlocks( + force=True, + recurse=False, + variable=variable, + checkEach=True, + index=3, + testOption='forwarded') + + assert calls == [{ + 'force': True, + 'recurse': False, + 'variable': variable, + 'checkEach': True, + 'index': 3, + 'testOption': 'forwarded', + }] + assert updates == [True] + + +@pytest.mark.parametrize('configType', (Ad9249ConfigGroup, Ad9252Config, Ad9681Config)) +def test_device_update_writes_transfer_strobe(configType): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + config = configType(name='Config', memBase=memory) + root.add(config) + root.start() + try: + memory.transactions.clear() + config.DeviceUpdate() + + assert int.from_bytes(memory._memory[0x3FC:0x400], 'little') == 1 + assert memory.transactions == [(rim.Write, 0x3FC, 4)] + finally: + root.stop() + + +def test_device_specific_readout_geometry(): + ad9249 = Ad9249Readout(name='Ad9249') + assert len(ad9249.Bank) == 2 + for bank, readout in ad9249.Bank.items(): + assert readout.offset == 0x1000*bank + assert ( + readout._dataLanes, + readout._fcoLanes, + readout._channels, + readout._sampleBits, + readout._serializationFactor, + readout._delayBits, + ) == (8, 1, 8, 14, 14, 9) + + ad9252 = Ad9252Readout(name='Ad9252') + assert ( + ad9252._dataLanes, + ad9252._fcoLanes, + ad9252._channels, + ad9252._sampleBits, + ad9252._serializationFactor, + ad9252._delayBits, + ) == (8, 1, 8, 14, 14, 9) + + ad9681 = Ad9681Readout(name='Ad9681') + assert ( + ad9681._dataLanes, + ad9681._fcoLanes, + ad9681._channels, + ad9681._sampleBits, + ad9681._serializationFactor, + ad9681._delayBits, + ) == (16, 2, 8, 14, 8, 9) + + +@pytest.mark.parametrize( + ('deviceFamily', 'delayBits'), + ( + ('7SERIES', 5), + ('ULTRASCALE', 9), + ('ULTRASCALE_PLUS', 9), + ), +) +def test_device_family_selects_native_delay_width(deviceFamily, delayBits): + assert adcDdrDelayBits(deviceFamily) == delayBits + assert Ad9249ReadoutBank( + name='Ad9249', deviceFamily=deviceFamily)._delayBits == delayBits + assert Ad9252Readout( + name='Ad9252', deviceFamily=deviceFamily)._delayBits == delayBits + assert Ad9681Readout( + name='Ad9681', deviceFamily=deviceFamily)._delayBits == delayBits + + +@pytest.mark.parametrize( + ('constructor', 'kwargs', 'message'), + ( + (AdcDdr, {'dataLanes': 65}, 'dataLanes'), + (AdcDdr, {'channels': 0}, 'channels'), + (AdcDdr, {'sampleBits': 17}, 'sampleBits'), + (AdcDdr, {'serializationFactor': 17}, 'serializationFactor'), + (Ad9249ReadoutBank, {'deviceFamily': 'SPARTAN6'}, 'deviceFamily'), + (Ad9252Readout, {'channels': 9}, 'channels'), + (Ad9252Readout, {'deviceFamily': 'SPARTAN6'}, 'deviceFamily'), + (Ad9681Readout, {'deviceFamily': 'SPARTAN6'}, 'deviceFamily'), + ), +) +def test_readout_geometry_rejects_values_outside_rtl_limits( + constructor, kwargs, message): + with pytest.raises(ValueError, match=message): + constructor(name='Readout', **kwargs) + + +def test_ad9681_adapter_requires_device_specific_readout(): + config = Ad9681Config(name='Config') + wrong = AdcDdr(name='Wrong', dataLanes=8, fcoLanes=1, channels=8) + + with pytest.raises(TypeError, match='must be an Ad9681Readout'): + Ad9681ReadoutCalibration( + name='Calibration', + config=config, + readout=wrong) diff --git a/tests/devices/analog_devices/test_AdcDdrPatternPkg.py b/tests/devices/analog_devices/test_AdcDdrPatternPkg.py new file mode 100644 index 0000000000..a4cc0f0175 --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrPatternPkg.py @@ -0,0 +1,87 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, may be +## copied, modified, propagated, or distributed except according to the terms +## contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Generate 8-, 14-, and 16-bit words used by supported ADC modes. +# - Stimulus: Feed all-ones and asymmetric nonzero PN9/PN23 states through many +# consecutive word advances. +# - Checks: Every word and next state matches independent polynomial models; +# the PN9 one-bit recurrence also proves its full 511-state nonzero period. +# - Timing: Package functions are combinational and checked after settling. + +import os + +import cocotb +import pytest +from cocotb.triggers import Timer + +from tests.common.regression_utils import hdl_parameters_from, parameter_case, run_surf_vhdl_test + + +def pn_next(state, order, tap): + return ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + + +def pn_word(state, order, tap, width): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = pn_next(state, order, tap) + return word, state + + +@cocotb.test() +async def pn_pattern_test(dut): + width = int(os.environ["WORD_WIDTH_G"]) + states = [(0x1FF, 0x7FFFFF), (0x12D, 0x654321)] + + for pn9, pn23 in states: + for _ in range(64): + dut.pn9State.value = pn9 + dut.pn23State.value = pn23 + await Timer(1, unit="ns") + expected9, next9 = pn_word(pn9, 9, 5, width) + expected23, next23 = pn_word(pn23, 23, 18, width) + assert int(dut.pn9Word.value) == expected9 + assert int(dut.pn23Word.value) == expected23 + assert int(dut.pn9Next.value) == next9 + assert int(dut.pn23Next.value) == next23 + pn9, pn23 = next9, next23 + + state = 0x1FF + visited = set() + for _ in range(511): + assert state not in visited and state != 0 + visited.add(state) + state = pn_next(state, 9, 5) + assert state == 0x1FF + + +PARAMETER_SWEEP = [ + parameter_case("word_8", WORD_WIDTH_G="8"), + parameter_case("word_14", WORD_WIDTH_G="14"), + parameter_case("word_16", WORD_WIDTH_G="16"), +] + + +@pytest.mark.parametrize("parameters", PARAMETER_SWEEP) +def test_AdcDdrPatternPkg(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.adcddrpatternpkgtb", + parameters=hdl_parameters_from(parameters), + extra_env=parameters, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_AdcDdrPatternTester.py b/tests/devices/analog_devices/test_AdcDdrPatternTester.py new file mode 100644 index 0000000000..e577f9ca55 --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrPatternTester.py @@ -0,0 +1,209 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Exercise two logical channels and one FCO lane in constant and +# alternating pattern modes, followed by every terminal error class. +# - Stimulus: Configure and start finite windows through the module's AXI-Lite +# interface, provide valid sample groups with shared A/B phase, inject one +# data-bit error and one FCO mismatch, suppress FCO validity, then withhold +# valid samples or abort. +# - Checks: Verify configuration readback, completion sequencing, common sample +# counts, phase acquisition, channel/FCO pass masks, error counters, +# accumulated bit-error masks, configuration errors, timeout, and abort. +# - Timing: AXI-Lite and the primitive-free checker share the capture clock; +# gaps in sampleValid exercise the programmable no-valid timeout. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotbext.axi import AxiLiteBus, AxiLiteMaster + +from tests.axi.utils import axil_read_u32, axil_write_u32 +from tests.common.regression_utils import run_surf_vhdl_test + + +FRAME_PATTERN = 0b11111110000000 + +START_ADDR = 0x00 +ABORT_ADDR = 0x04 +CONFIG_ADDR = 0x08 +CHANNEL_MASK_ADDR = 0x0C +FCO_MASK_ADDR = 0x10 +DATA_MASK_ADDR = 0x14 +PATTERN_A_ADDR = 0x18 +PATTERN_B_ADDR = 0x1C +SAMPLES_ADDR = 0x20 +TIMEOUT_ADDR = 0x24 +STATUS_ADDR = 0x28 +SEQUENCE_ADDR = 0x2C +CHECKED_ADDR = 0x30 +CHANNEL_PASS_ADDR = 0x34 +FCO_PASS_ADDR = 0x38 +WORD_ERROR_ADDR = 0x40 +BIT_ERROR_ADDR = 0x80 +FCO_ERROR_ADDR = 0xC0 + + +async def axil_poll(axil, address, predicate, limit=128): + for _ in range(limit): + value = await axil_read_u32(axil, address) + if predicate(value): + return value + assert False, f'AXI register 0x{address:03X} did not reach expected state' + + +async def send_sample(dut, channel0, channel1): + await FallingEdge(dut.clk) + dut.sampleIn.value = (channel1 << 16) | channel0 + dut.sampleValid.value = 1 + await RisingEdge(dut.clk) + await Timer(2, unit='ns') + dut.sampleValid.value = 0 + + +async def configure(axil, *, alternating, samples, timeout): + await axil_write_u32(axil, CONFIG_ADDR, int(alternating)) + await axil_write_u32(axil, CHANNEL_MASK_ADDR, 3) + await axil_write_u32(axil, FCO_MASK_ADDR, 1) + await axil_write_u32(axil, DATA_MASK_ADDR, 0x3FFF) + await axil_write_u32(axil, PATTERN_A_ADDR, 0x1555) + await axil_write_u32(axil, PATTERN_B_ADDR, 0x2AAA) + await axil_write_u32(axil, SAMPLES_ADDR, samples) + await axil_write_u32(axil, TIMEOUT_ADDR, timeout) + + +async def start_window(axil): + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + await axil_poll(axil, STATUS_ADDR, lambda value: value & 1) + return sequence + + +async def wait_done(axil, sequence): + await axil_poll(axil, SEQUENCE_ADDR, lambda value: value != sequence) + return await axil_read_u32(axil, STATUS_ADDR) + + +@cocotb.test() +async def pattern_tester_test(dut): + dut.rst.value = 1 + dut.sampleValid.value = 0 + dut.sampleIn.value = 0 + dut.fcoValid.value = 1 + dut.fcoWord.value = FRAME_PATTERN + cocotb.start_soon(Clock(dut.clk, 8, unit='ns').start()) + + for _ in range(5): + await RisingEdge(dut.clk) + dut.rst.value = 0 + await RisingEdge(dut.clk) + axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, 'S_AXI'), dut.clk, dut.rst) + + # Constant mode ignores unmasked transport bits and checks both channels. + await configure(axil, alternating=False, samples=3, timeout=100) + await axil_write_u32(axil, DATA_MASK_ADDR, 0x3F) + await axil_write_u32(axil, PATTERN_A_ADDR, 0x15) + assert await axil_read_u32(axil, DATA_MASK_ADDR) == 0x3F + sequence = await start_window(axil) + for _ in range(3): + await send_sample(dut, 0x3015, 0x2015) + status = await wait_done(axil, sequence) + assert status == 0x70 + assert await axil_read_u32(axil, CHECKED_ADDR) == 3 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 3 + assert await axil_read_u32(axil, FCO_PASS_ADDR) == 1 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, BIT_ERROR_ADDR) == 0 + + # A reference word matching neither A nor B fails every enabled channel + # without acquiring phase. A later match acquires one group-wide phase but + # does not erase the first sample's errors. + await configure(axil, alternating=True, samples=2, timeout=100) + sequence = await start_window(axil) + await send_sample(dut, 0, 0) + assert await axil_read_u32(axil, STATUS_ADDR) & 0x10 == 0 + await send_sample(dut, 0x2AAA, 0x2AAA) + status = await wait_done(axil, sequence) + assert status & 0x10 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 1 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR) == 0x3FFF + assert await axil_read_u32(axil, BIT_ERROR_ADDR+4) == 0x3FFF + + # Channel 1 accumulates one exact failing bit and the FCO lane records one + # mismatch without disturbing the shared alternating phase. + await configure(axil, alternating=True, samples=3, timeout=100) + sequence = await start_window(axil) + await send_sample(dut, 0x2AAA, 0x2AAA) + dut.fcoWord.value = FRAME_PATTERN ^ 1 + await send_sample(dut, 0x1555, 0x1551) + dut.fcoWord.value = FRAME_PATTERN + await send_sample(dut, 0x2AAA, 0x2AAA) + status = await wait_done(axil, sequence) + assert status == 0x10 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 1 + assert await axil_read_u32(axil, FCO_PASS_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR+4) == 4 + assert await axil_read_u32(axil, FCO_ERROR_ADDR) == 1 + + # A selected FCO lane must be observed during the window to pass. Missing + # fcoValid is distinct from a mismatch and therefore leaves its error count + # at zero while clearing the FCO pass result. + await configure(axil, alternating=False, samples=2, timeout=100) + dut.fcoValid.value = 0 + sequence = await start_window(axil) + for _ in range(2): + await send_sample(dut, 0x1555, 0x1555) + status = await wait_done(axil, sequence) + assert status == 0x30 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 3 + assert await axil_read_u32(axil, FCO_PASS_ADDR) == 0 + assert await axil_read_u32(axil, FCO_ERROR_ADDR) == 0 + dut.fcoValid.value = 1 + + # A zero sample request is rejected without entering busy. + await configure(axil, alternating=False, samples=0, timeout=100) + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x04 + + # Three consecutive capture clocks without sampleValid terminate a window. + await configure(axil, alternating=False, samples=2, timeout=3) + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x12 + assert await axil_read_u32(axil, CHECKED_ADDR) == 0 + + # Abort is a distinct terminal result and leaves partial counts readable. + await configure(axil, alternating=False, samples=2, timeout=0) + sequence = await start_window(axil) + await send_sample(dut, 0x1555, 0x1555) + await axil_write_u32(axil, ABORT_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x18 + assert await axil_read_u32(axil, CHECKED_ADDR) == 1 + + +def test_AdcDdrPatternTester(): + sources = [ + 'devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd', + 'devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd', + 'devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd', + ] + run_surf_vhdl_test( + test_file=__file__, + toplevel='surf.adcddrpatterntesterwrapper', + extra_vhdl_sources={'surf': sources}, + ) From a66f8c61fc4beaf0d47e42b29674ab5679f7536b Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 5 Aug 2026 14:54:09 -0700 Subject: [PATCH 04/57] docs: document ADC DDR integration and migration --- devices/AnalogDevices/README.md | 93 ++++ devices/AnalogDevices/ad9249/README.md | 504 ++++++++++++++++++ devices/AnalogDevices/adcDdr/README.md | 210 ++++++++ .../AnalogDevices/adcDdr/migration-guide.md | 259 +++++++++ devices/README.md | 6 +- python/README.md | 11 +- 6 files changed, 1081 insertions(+), 2 deletions(-) create mode 100644 devices/AnalogDevices/README.md create mode 100644 devices/AnalogDevices/ad9249/README.md create mode 100644 devices/AnalogDevices/adcDdr/README.md create mode 100644 devices/AnalogDevices/adcDdr/migration-guide.md diff --git a/devices/AnalogDevices/README.md b/devices/AnalogDevices/README.md new file mode 100644 index 0000000000..ae9b81e9f8 --- /dev/null +++ b/devices/AnalogDevices/README.md @@ -0,0 +1,93 @@ +# Analog Devices + +This tree contains register interfaces, readout RTL, simulation models, and +FPGA-family wrappers for supported Analog Devices parts. + +## Serialized DDR ADCs + +AD9249, AD9252, and AD9681 use the shared [`adcDdr`](adcDdr/) infrastructure +for their normalized readout implementations. The device directories retain +the part-specific pin topology, sample assembly, configuration interface, and +simulation model: + +- [`ad9249/`](ad9249/) provides two independent eight-channel readout banks + and a detailed guide for migrating the retained legacy + `Ad9249ReadoutGroup` firmware and software. +- [`ad9252/`](ad9252/) provides one eight-channel readout. +- [`ad9681/`](ad9681/) assembles two physical serialized lanes per logical + channel and captures both halves with one selected DCLK. + +The shared [`adcDdr` README](adcDdr/README.md) describes the RTL boundary, +software calibration algorithm, measurement backends, and target-owned timing +responsibilities. +The matching PyRogue models are under +[`python/surf/devices/analog_devices`](../../python/surf/devices/analog_devices/). +AD9249 configuration/readout classes live in `_Ad9249.py`; the +unchanged `Ad9249ReadoutGroup` map and related legacy helper are isolated in +`_Ad9249Legacy.py` while remaining available through the package namespace. + +### PyRogue Readout Classes + +Application code should instantiate the device-specific readout class instead +of repeating normalized `AdcDdr` geometry. For example, the AD9681 +map is: + +```python +self.add(surf.devices.analog_devices.Ad9681Readout( + enabled = True, + name = 'Ad9681Readout', + offset = 0x00000000, + deviceFamily = '7SERIES')) +``` + +The device classes supply these mappings: + +| Class | Data lanes | FCO lanes | Channels | Sample bits | Serialization | +|---|---:|---:|---:|---:|---:| +| `Ad9249ReadoutBank` | 8 | 1 | 8 | 14 | 14 | +| `Ad9252Readout` | 8 by default | 1 | 8 by default | 14 | 14 | +| `Ad9681Readout` | 16 | 2 | 8 | 14 | 8 | + +`Ad9249Readout` represents the full converter and contains two independent +`Ad9249ReadoutBank` children at offsets `0x0000` and `0x1000`. AD9252 exposes a +`channels` argument because its RTL supports `NUM_CHANNELS_G` from +one through eight. All three classes expose `deviceFamily`, matching RTL +`DEVICE_FAMILY_G`; the models derive five delay bits for 7-Series and nine for +UltraScale/UltraScale+. Migration from the +removed AD9681 and AD9249 Group2 interfaces is documented in the +[serialized DDR ADC migration guide](adcDdr/migration-guide.md). +Migration from the retained legacy `Ad9249ReadoutGroup` interface is documented +in the [AD9249 readout migration guide](ad9249/README.md). + +### Device Simulation Timing + +The three serialized-DDR ADC directories provide primitive-free `*Sim` models +with the device pin topology, a binary ideal DCO, coherent frame serialization, +and datasheet conversion latency. Normal conversion data is delayed by 16 +sample clocks for AD9249 and AD9681 and by eight sample clocks for AD9252. +Digitally generated test patterns are selected after that conversion pipeline. + +Their serializer timing controls have the same meaning: + +- `DATA_PHASE_G` and `FCO_PHASE_G` apply a common static displacement relative + to DCO to all data or FCO transitions. +- `DATA_SKEW_G` applies independent physical-data-lane displacement. + `FCO_SKEW_G` is per FCO for AD9249 and AD9681 and scalar for AD9252. +- `JITTER_G` alternates each actual data/FCO transition between `-JITTER_G` + and `+JITTER_G`. It does not introduce `X` values or jitter DCO. +- `TIMING_BIAS_G` delays data, FCO, and DCO together. Set it to at least + `JITTER_G` when no positive phase/skew already makes the early transition + schedulable. Because it is common-mode, it does not change setup time. + +All timing controls default to zero, preserving an ideal centered eye. A +useful nonzero regression starting point is 50 ps of jitter with 50 ps of +timing bias, then measured or deliberately stressed per-lane skew. The models +assert if an early edge would fall before simulation time or a late data/FCO +edge would cross its following DCO sampling edge. The flattened cocotb wrappers +express timing controls in integer picoseconds and expose representative lane +zero/FCO-zero skew; instantiate the `*Sim` entity directly to provide complete +`TimeArray` lane maps. + +Other part directories contain independent control or datapath cores and do +not use the serialized-DDR calibration flow unless their readout is explicitly +built on the normalized `AdcDdr` map. diff --git a/devices/AnalogDevices/ad9249/README.md b/devices/AnalogDevices/ad9249/README.md new file mode 100644 index 0000000000..ed462d2073 --- /dev/null +++ b/devices/AnalogDevices/ad9249/README.md @@ -0,0 +1,504 @@ +# AD9249 Readout Migration Guide + +## Purpose + +This guide explains how to move a downstream design from the retained legacy +`Ad9249ReadoutGroup` RTL/PyRogue interface to the AdcDdr-based serialized-DDR ADC +readout. It is written for firmware and software repositories that consume SURF +as a submodule and may need to schedule the migration independently. + +The legacy implementation is not removed by this migration effort. Both +family-specific legacy RTL files and the matching PyRogue class remain available +for existing projects: + +- [7-Series legacy RTL](7Series/rtl/Ad9249ReadoutGroup.vhd) +- [UltraScale legacy RTL](UltraScale/rtl/Ad9249ReadoutGroup.vhd) +- [legacy PyRogue map](../../../python/surf/devices/analog_devices/_Ad9249Legacy.py) + +The replacements are: + +- [Ad9249ReadoutBank](core/Ad9249ReadoutBank.vhd) for one independently clocked + eight-channel DCO/FCO bank; +- [Ad9249Readout](core/Ad9249Readout.vhd) for a complete 16-channel ADC with two + independent banks; +- `Ad9249ReadoutBank` and `Ad9249Readout` in the + [PyRogue module](../../../python/surf/devices/analog_devices/_Ad9249.py); +- `Ad9249ReadoutBankCalibration` and `Ad9249ReadoutCalibration` for software + eye calibration and verification. + +This is not a drop-in entity rename. The register map, address-window size, +clock/reset boundary, stream behavior, and software operations change. + +## Choose the Replacement + +Use `Ad9249ReadoutBank` when the existing `Ad9249ReadoutGroup` represents one +physical DCO/FCO bank. This is the normal migration and preserves the existing +one-bank integration boundary. + +Use full `Ad9249Readout` only when one module owns both physical banks and can +provide: + +- both `Ad9249SerialGroupType` records; +- one `0x2000`-byte AXI-Lite device window; +- separate initial data/FCO delays and `IODELAY_GROUP` values for each bank; +- one common stream clock/reset for all 16 logical channel streams. + +The full wrapper places bank 0 at offset `0x0000`, bank 1 at `0x1000`, and maps +stream destinations to `0..15`. Two separate `Ad9249ReadoutBank` instances are +equally valid when banks belong to different modules, AXI crossbars, resets, or +software devices. + +## Migration Gates + +Review these conditions before editing the instantiation. A project that hits a +gate should retain the legacy wrapper until the required target or SURF support +is designed and validated. + +| Legacy behavior | AdcDdr behavior | Required decision | +|---|---|---| +| `NUM_CHANNELS_G` may be `1..8`. | RTL bank supports `1..8`, but the typed `Ad9249ReadoutBank` PyRogue model and calibration adapter currently describe eight channels. | Eight-channel banks migrate directly. For reduced-channel software, retain legacy support or add and test a matching typed model before migration. | +| UltraScale `USE_MMCME_G=false` accepts shared external bit/divided clocks and resets. | The AdcDdr PHY derives its capture clocks internally from the bank DCO and has no external-clock ports. | Confirm per-bank clock-resource use is acceptable. If shared external clocking is required, retain legacy support or extend the AdcDdr PHY boundary first. | +| UltraScale `adcReady[ch]` can stall each channel FIFO independently. | AdcDdr output has no ready input and crosses all channels coherently through one wide FIFO. | Remove `adcReady` only when the consumer can accept every asserted `tValid`. Otherwise add an explicit downstream buffering/flow-control adapter. | +| Runtime `Invert` register complements every sample. | AdcDdr inversion/coding choices are compile-time generics or ADC configuration settings. | Replace runtime changes with a fixed `ADC_INVERT_CH_G`, `OFFSET_BINARY_G`, `NEGATE_G`, or ADC output-format policy. | +| Legacy AXI-Lite remains responsive from `axilClk` without DCO. | The normalized endpoint is crossed into the capture domain. | Do not access the readout while its DCO is absent. Sequence ADC clock startup before readout software access. | +| Legacy unlocked behavior differs by family. | AdcDdr output preserves cadence and marks unlocked samples with `tUser(0)`. | Consumers must inspect or intentionally ignore `tUser(0)`; do not assume the old all-ones or suppressed-valid behavior. | + +## Firmware Migration + +### Entity replacement + +Replace: + +```vhdl +U_AdcReadout : entity surf.Ad9249ReadoutGroup +``` + +with: + +```vhdl +U_AdcReadout : entity surf.Ad9249ReadoutBank +``` + +`Ad9249ReadoutBank` is family-neutral. `DEVICE_FAMILY_G` selects exactly one +PHY implementation and its native delay width: + +| Target | `DEVICE_FAMILY_G` | Delay bits | +|---|---|---:| +| 7-Series | `"7SERIES"` | `5` | +| UltraScale | `"ULTRASCALE"` | `9` | +| UltraScale+ | `"ULTRASCALE_PLUS"` | `9` | + +The legacy Python `fpga` choice exposed six or ten bits because its register +definition included the adjacent legacy load-control bit. AdcDdr delay +values contain only the physical tap count. RTL and software derive five bits +for 7-Series and nine bits for UltraScale/UltraScale+ from the family. + +### Generic mapping + +| Legacy generic | AdcDdr bank generic | Migration | +|---|---|---| +| `TPD_G` | `TPD_G` | Copy unchanged. | +| `NUM_CHANNELS_G` | `NUM_CHANNELS_G` | Copy for RTL; review the reduced-channel software gate above. | +| none | `AXIL_BASE_ADDR_G` | Set to the absolute base address visible on the incoming AXI-Lite master. | +| none | `DEVICE_FAMILY_G` | Select `7SERIES`, `ULTRASCALE`, or `ULTRASCALE_PLUS`. | +| `IODELAY_GROUP_G` | `IODELAY_GROUP_G` | Copy, then verify the target constraints and controller ownership. | +| `IDELAYCTRL_FREQ_G` | `IDELAYCTRL_FREQ_G` | Copy for 7-Series; it describes the target-owned `IDELAYCTRL` reference clock. | +| `DEFAULT_DELAY_G` | `DATA_DELAY_INIT_G`, `FCO_DELAY_INIT_G` | Expand the common legacy default into independent numeric tap values. | +| `ADC_INVERT_CH_G` | `ADC_INVERT_CH_G` | Copy the active channel bits. | +| none | `PATTERN_CHECK_G` | Enable to include hardware-assisted bounded pattern measurements. | +| legacy runtime `Invert` | `OFFSET_BINARY_G`, `NEGATE_G`, or `ADC_INVERT_CH_G` | Choose fixed numeric/polarity behavior deliberately; there is no runtime register equivalent. | + +UltraScale-only legacy generics do not have direct AdcDdr equivalents: + +| UltraScale legacy generic | AdcDdr handling | +|---|---| +| `SIM_DEVICE_G` | Replace with `DEVICE_FAMILY_G`. | +| `D_DELAY_CASCADE_G`, `F_DELAY_CASCADE_G` | No wrapper-level equivalent; the AdcDdr PHY delay topology is fixed. Revalidate timing/range. | +| `USE_MMCME_G` | Removed; the AdcDdr PHY owns DCO-derived capture clocking. | +| `SIM_SPEEDUP_G` | Removed; simulation selection belongs to the selected PHY/build environment. | + +### Initial delay conversion + +The legacy wrapper already has independent runtime data and frame delays, but +only one scalar compile-time default. `Ad9249ReadoutBank` has an independent +compile-time value for every data lane and FCO lane. + +For an initial, behavior-preserving seed: + +```vhdl +constant LEGACY_DELAY_TAPS_C : natural := 12; + +constant ADC_DATA_DELAY_INIT_C : NaturalArray(NUM_CHANNELS_C-1 downto 0) := + (others => LEGACY_DELAY_TAPS_C); + +constant ADC_FCO_DELAY_INIT_C : NaturalArray(0 downto 0) := + (0 => LEGACY_DELAY_TAPS_C); +``` + +Replace the replicated seed with characterized per-lane values after running +AdcDdr calibration. Each array value must fit the width selected by +`DEVICE_FAMILY_G`; elaboration asserts otherwise. + +If the old software saved `ChannelDelay[ch]` and `FrameDelay`, copy those actual +tap values—not the legacy Python field width—into `DATA_DELAY_INIT_G(ch)` and +`FCO_DELAY_INIT_G(0)`. + +### Port mapping + +The following ports retain their roles: + +- `axilClk`, `axilRst`, and all four AXI-Lite records; +- `adcClkRst`; +- `adcSerial : Ad9249SerialGroupType`; +- `adcStreamClk`; +- `adcStreams` with the active logical channel range. + +AdcDdr bank integration adds: + +```vhdl +idelayCtrlRdy => adcIdelayCtrlRdy, -- [in] +adcStreamRst => adcStreamRst, -- [in] +``` + +For 7-Series, instantiate or reuse a target-owned `IDELAYCTRL`, match its +`IODELAY_GROUP` to `IODELAY_GROUP_G`, constrain its reference clock at +`IDELAYCTRL_FREQ_G`, and connect `RDY` to `idelayCtrlRdy`. Do not tie readiness +high merely to bypass startup sequencing in hardware. + +UltraScale/UltraScale+ uses `IDELAYE3` count mode and does not use +`IDELAYCTRL`; tie `idelayCtrlRdy` high explicitly or accept its default. + +`adcStreamRst` resets the coherent stream-domain FIFO/output state and must be +valid in the `adcStreamClk` domain. + +Remove the UltraScale legacy ports `adcBitClkIn`, `adcBitClkDiv4In`, +`adcBitRstIn`, `adcBitRstDiv4In`, and `adcReady`. Before doing so, resolve the +shared-clock and backpressure migration gates described above. + +### One-bank example + +```vhdl +U_AdcReadout : entity surf.Ad9249ReadoutBank + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => ADC_READOUT_BASE_ADDR_C, + NUM_CHANNELS_G => 8, + DEVICE_FAMILY_G => ADC_DEVICE_FAMILY_C, + IODELAY_GROUP_G => "ADC_BANK_0", + IDELAYCTRL_FREQ_G => 200.0, + DATA_DELAY_INIT_G => ADC_DATA_DELAY_INIT_C, + FCO_DELAY_INIT_G => ADC_FCO_DELAY_INIT_C, + ADC_INVERT_CH_G => ADC_INVERT_CH_C, + PATTERN_CHECK_G => true, + OFFSET_BINARY_G => false, + NEGATE_G => false) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilWriteMaster => adcWriteMaster, -- [in] + axilWriteSlave => adcWriteSlave, -- [out] + axilReadMaster => adcReadMaster, -- [in] + axilReadSlave => adcReadSlave, -- [out] + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => adcIdelayCtrlRdy, -- [in] + adcSerial => adcSerial, -- [in] + adcStreamClk => adcStreamClk, -- [in] + adcStreamRst => adcStreamRst, -- [in] + adcStreams => adcStreams); -- [out] +``` + +Set `ADC_DELAY_BITS_C` and `ADC_DEVICE_FAMILY_C` from the target family table; +do not infer them from the old PyRogue `fpga` field width. + +### Full-device example boundary + +`Ad9249Readout` has two fixed eight-channel banks. Its important differences +from one bank are: + +- `adcSerial` is `Ad9249SerialGroupArray(1 downto 0)`; +- `idelayCtrlRdy` is a two-bit vector; +- `IODELAY_GROUP_0_G` and `IODELAY_GROUP_1_G` are independent; +- `DATA_DELAY_INIT_G(7 downto 0)` belongs to bank 0 and + `(15 downto 8)` belongs to bank 1; +- `FCO_DELAY_INIT_G(0)` and `(1)` belong to banks 0 and 1; +- AXI offsets are `0x0000` and `0x1000` relative to `AXIL_BASE_ADDR_G`; +- output streams/destinations are channels `0..7` for bank 0 and `8..15` for + bank 1. + +Do not wrap two existing one-bank AXI regions with full `Ad9249Readout` unless +the surrounding crossbar and Python hierarchy are changed to the single +`0x2000`-byte device layout at the same time. + +## AXI-Lite Register Migration + +One legacy bank typically fits inside a small `0x100`-byte region. One +AdcDdr bank requires a non-overlapping `0x1000`-byte region because the +normalized map includes delay, counter, debug snapshot, and pattern-test +windows. Expand both the firmware crossbar allocation and the PyRogue stride. + +| Legacy offset/path | AdcDdr offset/path | Notes | +|---|---|---| +| `0x00+4*ch`, `ChannelDelay[ch]` | `0x100+4*ch`, `DataDelay[ch]` | Same physical data-lane tap concept; AdcDdr readback is the retained programmed setting. | +| `0x20`, `FrameDelay` | `0x200`, `FcoDelay[0]` | Same physical FCO tap concept. | +| `0x30[15:0]`, `LostLockCount` | `0x340`, `LostLockCount[0]` | The AdcDdr counter is 32-bit saturating. | +| `0x30[16]`, `Locked` | `0x020[0]`, `LockedMask`; `0x01C[2]`, `AllLocked` | Use the mask for per-FCO state or aggregate status for startup. | +| `0x34`, `AdcFrame` | `0x300`, `FcoWord[0]` | Most recent deserialized FCO word. | +| `0x38`, `LostLockCountReset()` | `0x018`, `ClearCounters()` | Also clears overflow count/sticky overflow. | +| `0x40`, `Invert` | no runtime register | Select compile-time wrapper/ADC coding behavior. | +| `0x80+4*ch`, `AdcChannel[ch]` | `0x600+0x10*ch`, four `DebugSample` words | Legacy packs two rolling samples; AdcDdr publishes four coherent samples after `Snapshot()`. | +| `0xA0`, `FreezeDebug` | `0x014`, `Snapshot()` | Snapshot is explicit, atomic across every channel, and blocking. | + +AdcDdr-specific controls/status include: + +- `Version`, geometry, delay-width, and pattern-check capabilities; +- `CaptureReset` and explicit `Relock`; +- `DelayReady`, `AllLocked`, and `AnyOverflow`; +- `SnapshotSequence`; +- `OverflowCount`; +- optional `PatternTester` configuration and result windows. + +Unmapped AdcDdr accesses return `DECERR`. Do not retain hard-coded legacy +offsets in C++, Python, YAML-generated maps, notebooks, or command scripts. + +## Stream Behavior Migration + +Payload placement remains compatible: the 14-bit ADC code is right-justified +in `tData(13 downto 0)` and bits `15:14` are zero during locked, normal +operation. `tDest` remains the logical channel number. + +The following timing-visible behavior changes: + +- AdcDdr capture moves all active channels through one wide asynchronous + FIFO, preserving channel coherence for each sample epoch. +- AdcDdr output has no ready/backpressure input. A full internal FIFO drops + the newest complete channel group, sets `AnyOverflow`, and increments + `OverflowCount`; it does not stall the ADC. +- AdcDdr output does not suppress cadence when FCO alignment is lost. + `tUser(0)` marks affected samples. +- `tLast` remains deasserted; this is not SSI framing. +- `adcStreamRst` explicitly resets the stream crossing. + +Legacy 7-Series emits all-ones data while unlocked. Legacy UltraScale +suppresses channel `tValid` while unlocked and can independently stall channels +with `adcReady`. Any consumer depending on either behavior must be updated to +use `tValid`, `tUser(0)`, and overflow status deliberately. + +## PyRogue Migration + +### One bank + +Replace: + +```python +self.add(surf.devices.analog_devices.Ad9249ReadoutGroup( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + fpga = '7series', + channels = 8)) +``` + +with: + +```python +self.add(surf.devices.analog_devices.Ad9249ReadoutBank( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + deviceFamily = '7SERIES')) +``` + +Use `deviceFamily='ULTRASCALE'` or `'ULTRASCALE_PLUS'` for those FPGA families. +The Python and RTL models derive the same native delay width from this value. + +Ensure the next device offset is at least `ADC_READOUT_OFFSET + 0x1000`. If +several banks are created in a loop, change any legacy `0x100` stride to +`0x1000` or larger. + +### Full device + +For the full RTL wrapper: + +```python +self.add(surf.devices.analog_devices.Ad9249Readout( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + deviceFamily = ADC_DEVICE_FAMILY)) +``` + +Software then accesses `Ad9249Readout.Bank[0]` and `.Bank[1]`, matching RTL +offsets `0x0000` and `0x1000` inside the device. + +### Operational path mapping + +| Legacy software | AdcDdr software | +|---|---| +| `ChannelDelay[ch]` | `DataDelay[ch]` | +| `FrameDelay` | `FcoDelay[0]` | +| `Locked` | `AllLocked` or `LockedMask & 0x1` | +| `LostLockCount` | `LostLockCount[0]` | +| `LostLockCountReset()` | `ClearCounters()` | +| `AdcFrame` | `FcoWord[0]` | +| background/explicit `AdcChannel[ch]` read | call `Snapshot()`, then read `DebugSample[ch]` or the raw `DebugSampleRaw` array | +| `FreezeDebug` | remove; `Snapshot()` owns atomic publication | +| `Invert` | no runtime path; use the selected RTL/ADC coding policy | + +`Snapshot()` holds its AXI-Lite write response until four valid coherent sample +groups are captured. It can return `SLVERR` during reset/startup and cannot +complete without a running DCO and valid samples. Do not call it from a generic +startup `ReadAll` path. + +## Calibration Integration + +AdcDdr calibration uses the ADC checkerboard pattern to measure FCO and +every data-lane eye independently, performs full-channel qualification, and can +verify the selected settings at startup. + +For one bank: + +```python +readout = surf.devices.analog_devices.Ad9249ReadoutBank( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + deviceFamily = ADC_DEVICE_FAMILY) +self.add(readout) + +calibration = surf.devices.analog_devices.Ad9249ReadoutBankCalibration( + name = 'Ad9249Calibration', + config = self.Ad9249Config.BankConfig[0], + readout = readout) +self.add(calibration) +``` + +For full-device RTL/Python, use `Ad9249ReadoutCalibration` with the complete +`Ad9249Config` and `Ad9249Readout`; it creates `Bank[0]` and `Bank[1]` +calibration processes. + +Recommended bring-up flow: + +1. Configure the ADC output format and enable its DCO/FCO/data outputs. +2. Wait for target resets and 7-Series `IDELAYCTRL.RDY` where applicable. +3. Read `Version`, geometry, `DelayBits`, and `DelayReady`; verify they match + the software/target configuration. +4. Run full calibration per bank with the complete delay range. +5. Review every FCO/data eye, selected tap, and left/right margin. +6. Copy stable characterized centers into `DATA_DELAY_INIT_G` and + `FCO_DELAY_INIT_G` for that board/target. +7. On later startups, run verify-current or guard-band verification rather than + assuming a successful lock bit proves adequate margin. + +Calibration is disruptive: it changes ADC test mode, delay taps, and alignment +while running. Schedule it before normal data taking and prevent concurrent +readout-control access. + +The shared [adcDdr README](../adcDdr/README.md) documents calibration controls, +measurement backends, results, cleanup behavior, and limitations. + +## Constraints and Timing + +Keep the AD9249 constraints in the board target's top-level XDC. Update its pin +queries and timing values using the selected ADC mode, datasheet `tCO`, PCB +flight times, board skew, DCO frequency, and actual target hierarchy. + +For a complete ADC, apply the bank constraint independently to both DCO/FCO/data +groups. Calibration does not replace `create_clock`, DDR rise/fall input delays, +clock-placement review, or min/max timing analysis. + +At minimum, inspect: + +```tcl +report_clocks +check_timing -verbose +report_timing -from [get_ports ] -max_paths 50 +report_timing -from [get_ports ] -delay_type min -max_paths 50 +report_methodology +report_cdc -details +``` + +Correlate implemented timing margin with measured eyes. Do not treat a wide +simulation or hardware eye as evidence that an unconstrained path is safe. + +## Suggested Repository Migration Sequence + +For firmware maintained outside the SURF repository: + +1. **Pin the current working revision.** Record the existing SURF commit, + target, FPGA part, ADC mode, delay settings, and known-good bitstream. +2. **Inventory dependencies.** Search RTL, Python, generated maps, scripts, + YAML, notebooks, and constraints for legacy entity names and register paths. +3. **Evaluate migration gates.** Resolve reduced channels, external UltraScale + clocks, `adcReady`, runtime inversion, and DCO startup ordering first. +4. **Expand address windows.** Reserve `0x1000` per bank or `0x2000` per full + ADC in RTL and software before replacing register paths. +5. **Replace RTL and constraints.** Add stream reset and delay readiness, + select the PHY family, and seed initial delays. +6. **Replace PyRogue and scripts.** Update constructor arguments, paths, + snapshot use, counter clear, status, and calibration. +7. **Compile and simulate.** Check source selection, entity uniqueness, reset, + lock/relock, channel order, `tUser(0)`, overflow, and stopped-clock behavior. +8. **Implement and review timing.** Check clocks, placement, min/max input + timing, CDC, and constraints for every bank. +9. **Validate on hardware.** Compare samples with the known-good revision, run + calibration, power-cycle, and verify repeatability before deployment. +10. **Keep rollback available.** Do not remove the known-good SURF pin or saved + delay configuration until production validation is complete. + +Useful initial searches include: + +```bash +rg -n 'Ad9249ReadoutGroup|ChannelDelay|FrameDelay|LostLockCountReset|FreezeDebug' +rg -n 'USE_MMCME_G|adcBitClkIn|adcBitClkDiv4In|adcReady' +rg -n '0x20|0x30|0x34|0x38|0x40|0x80|0xA0' +``` + +## Validation Checklist + +Do not consider the migration complete until the applicable checks pass. + +### Build and static checks + +- Exactly one family implementation of `AdcDdrPhy` is loaded. +- `DEVICE_FAMILY_G` names a supported FPGA family and selects the expected delay width. +- Every bank has a non-overlapping `0x1000` AXI-Lite window. +- RTL `AXIL_BASE_ADDR_G` matches the address observed by the endpoint. +- PyRogue `deviceFamily` matches RTL `DEVICE_FAMILY_G`. +- All DCO, FCO, and data input paths are constrained for rising and falling + capture edges. +- 7-Series `IODELAY_GROUP` and `IDELAYCTRL.RDY` are correctly connected. +- Clock, methodology, CDC, and min/max timing reports have no unexplained ADC + paths or broad false-path exceptions. + +### Functional simulation + +- Reset and startup work with unrelated AXI, capture, and stream clocks. +- FCO locks from every bitslip phase and relocks after injected errors. +- Data/FCO delay writes affect exactly the requested lane and read back the + programmed setting. +- Channel numbering, `tDest`, right-justified sample bits, and physical lane + inversion match the old design. +- Unlocked samples assert `tUser(0)` and do not masquerade as valid aligned + data. +- Stream reset and overflow behavior are checked with the stream clock stopped + or slowed. +- Four-sample snapshots are coherent across every channel. + +### Hardware + +- Device identity/configuration and output coding are verified before capture. +- All FCO and data lanes produce bounded eyes with acceptable guard bands. +- Selected taps remain valid after relock and power cycle. +- Stream samples match an independent stimulus/reference for every channel. +- Lost-lock recovery, counter clearing, stopped-DCO behavior, and overflow + diagnostics work as documented. +- Representative voltage/temperature conditions are checked when required by + the application. + +## Compatibility Summary + +The public legacy class remains available as +`surf.devices.analog_devices.Ad9249ReadoutGroup`, and the legacy RTL remains in +its family directories. Projects do not need to migrate merely because they +update other SURF modules. + +Projects that do migrate should switch firmware, address allocation, PyRogue, +scripts, constraints, and characterized delay settings as one reviewed change. +Mixing the legacy software map with AdcDdr RTL, or AdcDdr software with +legacy RTL, is unsupported and will access the wrong registers. diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md new file mode 100644 index 0000000000..cc1c4e764b --- /dev/null +++ b/devices/AnalogDevices/adcDdr/README.md @@ -0,0 +1,210 @@ +# Serialized DDR ADC Readout + +`adcDdr` is the device-neutral receive and monitoring layer for Analog Devices +ADCs that send source-synchronous serialized DDR data with a forwarded data +clock (DCO) and frame clock (FCO). AD9249, AD9252, and AD9681 provide the +part-specific physical-lane topology around this common layer. + +## Layout + +- [`rtl/AdcDdrPhy.vhd`](rtl/AdcDdrPhy.vhd) selects the FPGA-family input PHY. +- [`rtl/AdcDdrCore.vhd`](rtl/AdcDdrCore.vhd) owns FCO word alignment, delay + controls, coherent samples, debug snapshots, counters, and the normalized + AXI-Lite map. +- [`rtl/AdcDdrPatternTester.vhd`](rtl/AdcDdrPatternTester.vhd) optionally checks + ADC test patterns in hardware over a bounded sample window. +- [`7Series/`](7Series/) and [`UltraScale/`](UltraScale/) contain the family + implementations of the programmable input delays and deserializers. +- [`sim/`](sim/) contains simulation-only pattern helpers. +- [`migration-guide.md`](migration-guide.md) maps the removed AD9681 and + AD9249 Group2 interfaces to their replacements. + +The PyRogue register model is +[`_AdcDdr.py`](../../../python/surf/devices/analog_devices/_AdcDdr.py), and the +device-neutral software calibration process is +[`_AdcDdrCalibration.py`](../../../python/surf/devices/analog_devices/_AdcDdrCalibration.py). +Device adapters in `_Ad9249.py`, `_Ad9252.py`, and `_Ad9681.py` supply the +physical-lane mapping and ADC configuration behavior. + +## Calibration Model + +Calibration finds a stable sampling point for every FCO and physical data lane. +It uses the ADC's alternating checkerboard test mode by default. For a 14-bit +sample the two expected logical values are `0x2AAA` and `0x1555`; their order is +not important during individual lane scans, but it is enforced during final +full-channel qualification. + +The distinction between a physical lane and a logical channel matters. AD9249 +and AD9252 use one physical data lane per channel. AD9681 uses two lanes per +channel: one contributes logical bits `[5:0]`, and the other contributes bits +`[13:6]`. The calibration adapter supplies a channel number and meaningful-bit +mask for every physical lane so each contribution can be measured separately. + +### Full calibration algorithm + +The `Full calibration` operation performs these phases: + +1. **Preserve state.** Software reads the current ADC test mode and all FCO and + data delay settings. These values can be restored after a stop or failure. +2. **Scan each FCO lane.** For every tap from `DelayStart` through `DelayStop`, + inclusive, software programs that FCO delay, commands `Relock`, waits for + `SettleTime`, and tests the corresponding `LockedMask` bit. +3. **Extract FCO eyes.** Consecutive passing taps form candidate eyes. An eye + must be at least `MinimumEyeWidth` taps wide and must provide at least + `GuardBand` passing taps on both sides of its selected center. For an + even-width eye, the lower center tap is selected. When `CircularDelays` is + enabled, passing runs at the two ends of the scan are merged. All qualifying + FCO eyes are retained, ordered by widest eye and then lowest center tap; the + first eye is the initial choice. +4. **Enable the checkerboard pattern.** The ADC is placed in its device-specific + test mode after the initial FCO delays have been selected and frame lock has + been reacquired. +5. **Build safe data-lane sweep groups.** Lanes on different logical channels + can move together. Lanes on the same channel can also move together when + their meaningful-bit masks do not overlap. Lanes with overlapping masks are + put in separate groups so a failure remains attributable to one physical + lane. Consequently, all sixteen AD9681 lanes form one sweep group even + though each pair contributes to the same logical channel. +6. **Scan each data group.** At a given tap, all lanes in the group are updated + in one bulk transaction and then measured from the same settled sampling + point. A per-lane verdict uses only that lane's logical sample-bit mask. + Each lane gets its own passing map and centered eye even though compatible + lanes are swept together. +7. **Qualify the assembled checkerboard samples.** With every data lane centered, software + checks the complete sample width. Channel zero establishes the checkerboard + A/B phase, and every logical channel must match the same ordered sequence. + The FCO lanes must also pass. This catches half-word or sample-epoch errors + that individual masked lane tests and a repetitive FCO word cannot detect. +8. **Qualify PN23 coherence and recurrence.** When `VerifyPn23` is enabled, + software switches the ADC to PN23, pulses the PN-long reset, and reads one + atomic four-sample debug snapshot. All logical channels must contain the + same four words. The reference channel's 56 captured bits must also satisfy + the `x^23 + x^18 + 1` recurrence after the first 23 bits establish its + arbitrary phase. This second condition rejects a common malformed sequence + that channel equality alone would accept. The checker considers the ADC's + possible sample-MSB format conversion and full output inversion; it does not + require the snapshot to begin at the PN seed. +9. **Try alternate FCO windows if necessary.** More than one FCO eye can appear + valid because the repetitive frame pattern may lock in equivalent-looking + unit intervals. If either checkerboard or PN23 qualification fails, + calibration tries the Cartesian product of the retained FCO eyes, relocking + and restoring checkerboard mode before each attempt. Data delays are not + rescanned because the FCO choice establishes frame/sample phase without + changing the data-eye centers. The first combination that passes both final + checks is retained. +10. **Publish or restore.** A successful full calibration restores the normal + ADC output mode but leaves the qualified FCO and data delays installed. A + stop, exception, or failed final qualification restores the original test + mode and delays. `ApplyResults` can later reinstall only a complete result + whose final qualification passed. + +This is an eye-centering algorithm, not a bit-error-rate characterization. A +passing tap means every bounded measurement requested by `SampleCount` passed; +it does not prove an arbitrarily low error rate. + +### Measurement backends + +`UsePatternTester` selects how data taps and final alignment are judged: + +- **Debug snapshots (`False`, default):** each `Snapshot` atomically publishes + four oldest-to-newest samples for every logical channel. One software + `SampleCount` unit requests one four-sample snapshot. The same coherent + snapshot is reused for every lane mask in a sweep group. +- **Hardware pattern tester (`True`):** the bounded checker in + `AdcDdrPatternTester` evaluates the requested channels and sample-bit mask. + One software `SampleCount` unit requests four hardware samples. A group with + more than one distinct lane mask requires one hardware measurement window per + mask. Alternating mode uses a single reference channel so channels cannot + independently accept opposite checkerboard phases. Every selected FCO lane + must produce at least one valid frame word during the window and every + observed word must match the configured frame pattern. + +The pattern-tester backend requires `PatternCheck` to report that the RTL block +is present. Both backends use the same eye extraction and final FCO-combination +logic. PN23 final qualification always uses the atomic debug snapshot, even +when the hardware pattern tester handles the checkerboard measurements; the +current hardware tester implements only constant and alternating patterns. + +### Verification operations + +`Verify current` checks each installed FCO and data delay without retaining any +temporary changes. `Verify guard band` also checks the taps at `selected - +GuardBand` and `selected + GuardBand`. A requested guard point outside +`DelayStart` through `DelayStop` is a failure. Each lane is restored before the +next lane is tested because multiple physical lanes can contribute to one +logical sample; the complete original configuration is restored when the +verification operation ends. + +## Calibration Controls + +| Control | Default | Meaning | +|---|---:|---| +| `Operation` | `Full calibration` | Full scan, current-tap verification, or guard-band verification. | +| `DelayStart` | `0` | First delay tap included in a scan or allowed verification range. | +| `DelayStop` | Maximum tap | Last delay tap included in a scan or allowed verification range. | +| `MinimumEyeWidth` | `8` taps | Minimum physical passing-window width. | +| `GuardBand` | `2` taps | Required passing margin on each side of the selected center. | +| `CircularDelays` | `False` | Whether the first and last passing scan runs are one wrapped eye. | +| `SampleCount` | `2` | Number of four-sample measurement groups checked at each data tap. | +| `VerifyPn23` | Device dependent | Enable the four-sample PN23 coherence and recurrence check when the adapter provides a PN-long reset control. | +| `SettleTime` | `1 ms` | Wall-clock wait after delay, relock, or ADC test-mode changes. | +| `UsePatternTester` | `False` | Select hardware checking instead of coherent debug snapshots. | +| `Debug` | `True` | Publish detailed per-tap diagnostics while the operation runs. | + +The full delay range is the safest initial scan. Narrow `DelayStart` and +`DelayStop` only when the board has a characterized region and the entire +expected eye, including failing boundary taps, remains visible. An eye touching +a scan boundary is reported as unbounded on that side unless circular scanning +merges it with the opposite boundary. + +## Results and Diagnostics + +`Results` contains the selected eye, complete passing map, and margins for each +FCO and data lane. FCO entries also contain every retained candidate eye. The +`Final` entry contains full-channel qualification captures plus every attempted +FCO-eye combination. When enabled, `Final.pn23` reports the four samples from +every channel, channel-coherence results, each recurrence transformation tried, +and the selected valid transformation. A failed scan may publish partial +results for the lane that could not produce a qualifying eye. + +`Diagnostics` contains the measurement backend, expected patterns, raw and +masked data captures, FCO words and lock masks, and the currently active scan +point. `RunTime`, process progress, and `Message` provide coarse operational +monitoring. Detailed diagnostics can be large because they retain every tap and +capture; disable `Debug` when live publication is unnecessary. + +## Device-Specific Behavior + +- **AD9249:** calibration is exposed per eight-channel bank because each bank + has its own DCO, FCO, delays, and capture domain. +- **AD9252:** one calibration process covers the configured logical channels; + ADC test-mode changes are committed with `DeviceUpdate`. +- **AD9681:** one process covers sixteen physical data lanes, eight assembled + logical channels, and two FCO lanes. Disjoint lower/upper sample masks permit + one common data sweep, while final shared-phase qualification verifies that + both serialized halves represent the same sample epoch. ADC test-mode changes + are committed with `DeviceUpdate`. + +## Relationship to Static Timing + +Runtime calibration compensates supported input-delay variation and chooses a +robust point inside the observed eye. It does not create timing constraints, +prove PCB skew limits, or guarantee that the FPGA DCO routing and I/O resources +are legal. + +SURF does not install target XDC for these readouts. The integrating project +owns the package-pin assignments, DCO clock definition, DDR rise/fall input +delays, clock-placement constraints, `IODELAY_GROUP` assignments, and any +board-specific skew budgets in its top-level constraints. Derive timing values +from the selected ADC mode, datasheet timing, PCB flight times, and actual +target hierarchy, then review both min and max timing. + +## Validation Status + +The common RTL, register model, calibration process, pattern tester, and +primitive-free device simulations have focused GHDL/PyRogue regressions. A full +32-tap AD9681 snapshot-based calibration has also completed in VCS +co-simulation: both FCO lanes and all sixteen physical data lanes produced +bounded eyes, and final coherent eight-channel checkerboard qualification +passed. This simulation evidence does not replace target implementation timing +or hardware validation. diff --git a/devices/AnalogDevices/adcDdr/migration-guide.md b/devices/AnalogDevices/adcDdr/migration-guide.md new file mode 100644 index 0000000000..04d93edc34 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/migration-guide.md @@ -0,0 +1,259 @@ +# Serialized DDR ADC Migration Guide + +## Scope + +This guide covers the coordinated migrations required by removal of: + +- the old 7-Series `Ad9681Readout` RTL and its corresponding PyRogue map; +- the transitional `Ad9681Readout2` development name; +- `Ad9249ReadoutGroup2` RTL and its PyRogue map. + +The replacements use the normalized `AdcDdr` register map. They are +not register-compatible aliases. Update RTL, AXI-Lite address allocation, +PyRogue construction, command paths, constraints, and characterized delay +values together. + +These downstream migrations are expected after the SURF release containing the +AdcDdr-based interfaces. A consumer that still uses a removed interface must stay +pinned to its previous SURF revision until its firmware and software are +migrated together. + +The permanently retained 7-Series and UltraScale `Ad9249ReadoutGroup` +interfaces are unaffected. Projects choosing to move those widely deployed +legacy interfaces to the AdcDdr-based readout should use the dedicated +[AD9249 readout migration guide](../ad9249/README.md). + +## Common Integration Changes + +Each AdcDdr readout region occupies `0x1000` bytes. Its normalized registers +extend through the pattern-tester window beginning at `0x800`. Do not retain an +old `0x100`-byte AXI-Lite slot or place the next endpoint inside this window. + +AdcDdr-based wrappers add two target-facing inputs: + +- `idelayCtrlRdy` reports that the target-owned input-delay controller is ready. + Drive the real ready indication in hardware. A constant `'1'` is appropriate + only when readiness is guaranteed externally or in focused simulation. +- `adcStreamRst` resets the coherent stream-domain FIFO and output state. Drive + the reset synchronous to `adcStreamClk` according to the target reset plan. + +The target must also provide an `IDELAYCTRL` with an `IODELAY_GROUP` matching +`IODELAY_GROUP_G` where the selected FPGA family requires it. Source-synchronous +pin and timing constraints remain target-owned; update the project's top-level +XDC for its actual pinout, ADC mode, DCO frequency, datasheet timing, PCB skew, +and clocking hierarchy. + +The normalized stream contract is: + +- one fixed two-byte transfer per logical channel and sample epoch; +- `tDest` equals the logical channel number; +- `tUser(0)` is asserted when the sample was captured while FCO alignment was + not locked; +- `tLast` remains deasserted; +- a stopped or slow stream clock can cause coherent group drops, reported by + `AnyOverflow` and `OverflowCount`; asserting `adcStreamRst` instead flushes + any samples queued in the stream-domain FIFO. + +AXI-Lite state is owned in the ADC capture domain. Do not access a readout while +its selected DCO is absent. + +## warm-tdm: AD9681 + +### RTL entity and generics + +The entity remains `surf.Ad9681Readout`, but it now refers to the family-neutral +wrapper in `ad9681/core/Ad9681Readout.vhd`. + +| Old generic | Replacement | +|---|---| +| `TPD_G` | `TPD_G` | +| `SIMULATION_G` | Remove; simulation selection belongs to the build/PHY environment. | +| `IODELAY_GROUP_G` | `IODELAY_GROUP_G` | +| `IDELAYCTRL_FREQ_G` | `IDELAYCTRL_FREQ_G` | +| `DEFAULT_DELAY_G` | Expand into all entries of `DATA_DELAY_INIT_G` and `FCO_DELAY_INIT_G`, then characterize independently. | +| `INVERT_G` | No blind one-to-one mapping. Use `ADC_INVERT_CH_G` for physical P/N lane inversion and `OFFSET_BINARY_G` for ADC numeric coding. | +| `NEGATE_G` | `NEGATE_G` | + +Set these new generics deliberately: + +- `DEVICE_FAMILY_G => "7SERIES"` for the current warm-tdm 7-Series target; +- `CAPTURE_DCLK_IDX_G => 0` to preserve the old DCLK0 single-domain behavior, + or `1` only after separately validating DCLK1; +- `PATTERN_CHECK_G => true` when hardware-assisted calibration is desired; +- `LEFT_JUSTIFY_G => true` to preserve warm-tdm's previous sample placement in + stream bits `[15:2]`. + +The old shared delay for half `i` affected its FCO and all eight data lanes. +For an initial like-for-like conversion, copy it to: + +```vhdl +DATA_DELAY_INIT_G => ( + 15 downto 8 => OLD_DELAY_1_C, + 7 downto 0 => OLD_DELAY_0_C), +FCO_DELAY_INIT_G => ( + 1 => OLD_DELAY_1_C, + 0 => OLD_DELAY_0_C), +``` + +Then replace those seed values with per-lane calibration results. Physical +data lane `(8*half)+channel` maps to `DATA_DELAY_INIT_G((8*half)+channel)`. + +Add the new ports to the instantiation: + +```vhdl +idelayCtrlRdy => adcIdelayCtrlRdy, -- [in] +adcStreamRst => adcStreamRst, -- [in] +``` + +All existing AXI-Lite, `adcClkRst`, `adcSerial`, `adcStreamClk`, and +eight-channel `adcStreams` connections retain their roles. + +### PyRogue and scripts + +Continue constructing `surf.devices.analog_devices.Ad9681Readout`, but replace +the old `fpga` and `channels` arguments with the RTL device-family selection: + +```python +self.add(surf.devices.analog_devices.Ad9681Readout( + name = 'Ad9681Readout', + offset = 0x00000000, + deviceFamily = '7SERIES', + enabled = True)) +``` + +The RTL and PyRogue models derive a five-bit delay value for `7SERIES` and a +nine-bit delay value for `ULTRASCALE` or `ULTRASCALE_PLUS`. + +Update script paths as follows: + +| Removed node or command | AdcDdr node or command | +|---|---| +| `Delay[i]` | `FcoDelay[i]` plus `DataDelay[(8*i)+ch]` for channels `0..7` | +| `EnUsrDelay` | Remove; programmed delays are always authoritative after startup. | +| `Relock()` | `Relock()` | +| `LostLockCountReset()` | `ClearCounters()` | +| `Locked[i]` | bit `i` of `LockedMask`; use `AllLocked` for the aggregate condition | +| `LostLockCount[i]` | `LostLockCount[i]` | +| `AdcFrameSync[i]` | `FcoWord[i]` | +| `AdcChannel[ch]` | call `Snapshot()`, then read the four coherent values in `DebugSample[ch]` | +| `AdcVoltage[ch]` | call `Snapshot()`, then use `DebugVoltage[ch]` for the oldest captured sample after configuring its range/format | +| `FreezeDebug` | Remove; `Snapshot()` publishes one atomic four-sample bank. | +| `Invert`, `Negate` | Set the corresponding compile-time wrapper semantics and ADC output format; there are no runtime normalized-map equivalents. | +| `ErrorDetCount[i]` | No direct equivalent; use `LostLockCount[i]`, lock state, and calibration/pattern-test diagnostics. | + +In warm-tdm initialization, the existing `Relock()` call can remain and the +immediately following `LostLockCountReset()` call should become +`ClearCounters()`. + +### AD9681 validation + +Before accepting the migration: + +1. Confirm the AXI crossbar reserves at least `0x1000` bytes. +2. Elaborate the target with exactly one `Ad9681Readout` entity declaration. +3. Check `idelayCtrlRdy`, DCLK selection, generated clocks, CDC, and both + source-synchronous input halves in Vivado reports. +4. Verify stream samples remain left-justified and preserve channel ordering. +5. Run full calibration, save per-lane/FCO centers, then run verify-current. +6. Exercise relock, counter clear, stopped-DCO diagnostics, and overflow status. + +## ldmx-firmware: AD9249 + +### RTL entity and generics + +Replace each one-bank `surf.Ad9249ReadoutGroup2` instantiation with +`surf.Ad9249ReadoutBank`. Do not use full `Ad9249Readout` unless one integration +owns both independent DCO/FCO banks and can allocate its `0x2000`-byte, +two-region map. + +| Group2 generic | AdcDdr bank replacement | +|---|---| +| `TPD_G` | `TPD_G` | +| `SIM_DEVICE_G` | `DEVICE_FAMILY_G`; use `"ULTRASCALE_PLUS"` for the current tracker target | +| `NUM_CHANNELS_G` | `NUM_CHANNELS_G` | +| `SIMULATION_G` | Remove; simulation selection belongs to the build/PHY environment. | +| `DEFAULT_DELAY_G` | Expand into every active `DATA_DELAY_INIT_G` entry and `FCO_DELAY_INIT_G(0)`. | +| `ADC_INVERT_CH_G` | `ADC_INVERT_CH_G` | + +The selected `DEVICE_FAMILY_G` also selects the native delay width. Provide +`IODELAY_GROUP_G`/`IDELAYCTRL_FREQ_G`, and decide `OFFSET_BINARY_G`, +`NEGATE_G`, and `PATTERN_CHECK_G` explicitly. + +For a like-for-like starting point from the old common delay: + +```vhdl +DATA_DELAY_INIT_G => (others => DEFAULT_DELAY_TAPS_C), +FCO_DELAY_INIT_G => (0 => DEFAULT_DELAY_TAPS_C), +``` + +Declare `DEFAULT_DELAY_TAPS_C` as a `natural` when converting the old +`slv(8 downto 0)` generic value. The readout checks that each natural tap count +fits the five- or nine-bit width selected by `DEVICE_FAMILY_G`. Characterized +per-lane values should replace the replicated seed after calibration. + +Add `idelayCtrlRdy` and `adcStreamRst` connections as described above. The +`Ad9249SerialGroupType`, AXI-Lite ports, stream clock, and per-channel stream +array retain their roles. AD9249 samples remain right-justified in +stream bits `[13:0]`, matching Group2's placement. + +### PyRogue and address layout + +Replace: + +```python +surf.devices.analog_devices.Ad9249ReadoutGroup2(...) +``` + +with: + +```python +surf.devices.analog_devices.Ad9249ReadoutBank( + name = f'Ad9249Readout[{i}]', + offset = i * 0x1000, + deviceFamily = 'ULTRASCALE_PLUS', + enabled = True) +``` + +The old tracker code used `i*0x100`; that stride must become at least +`i*0x1000`, and the containing AXI crossbar must expose the same expanded +windows. + +Update nodes and commands as follows: + +| Group2 node or command | AdcDdr bank node or command | +|---|---| +| `Delay` | `FcoDelay[0]` and each active `DataDelay[ch]` | +| `Relock()` | `Relock()` | +| `LostLockCountReset()` | `ClearCounters()` | +| `Locked` | `AllLocked` or bit 0 of `LockedMask` | +| `LostLockCount` | `LostLockCount[0]` | +| `AdcFrameSync` | `FcoWord[0]` | +| `AdcChannel[ch]` | call `Snapshot()`, then read `DebugSample[ch]` | +| `AdcVoltage[ch]` | call `Snapshot()`, then read `DebugVoltage[ch]` for the oldest captured sample | +| `FreezeDebug` | Remove; use atomic `Snapshot()`. | +| `Invert` | Configure `ADC_INVERT_CH_G`, `OFFSET_BINARY_G`, and the ADC output format deliberately. | +| `ErrorDetCount` | No direct equivalent; use `LostLockCount[0]` and pattern/calibration diagnostics. | + +### AD9249 validation + +Before accepting the LDMX migration: + +1. Confirm every bank receives a non-overlapping `0x1000` AXI-Lite window. +2. Confirm each physical bank still uses its own DCO/FCO capture domain. +3. Elaborate the UltraScale+ target and review delay-controller, clock, CDC, + and input timing reports. +4. Verify sample channel order, right justification, `tDest`, and unlock marking. +5. Run full calibration per bank, save independent FCO/data centers, then run + verify-current. +6. Exercise relock, counter clear, overflow, and stopped-clock diagnostics. + +## Removed and Retained Interfaces + +After this coordinated change, SURF no longer supplies +`Ad9249ReadoutGroup2`, `Ad9681Readout2`, or `Ad9681ReadoutManual`. The old +AD9681 7-Series implementation has been replaced by the family-neutral +`Ad9681Readout`. + +The existing 7-Series and UltraScale `Ad9249ReadoutGroup` RTL entities and +matching PyRogue class remain supported and unchanged. The public +names are final; no additional readout rename is planned as part of this work. diff --git a/devices/README.md b/devices/README.md index b4b2deee2b..22fe4f13e1 100644 --- a/devices/README.md +++ b/devices/README.md @@ -4,8 +4,12 @@ This tree contains vendor and component-specific RTL support. It is organized pr ## Layout -- Manufacturer folders such as `AnalogDevices/`, `Microchip/`, `Micron/`, `Silabs/`, `Ti/`, and `Xilinx/` hold individual device cores. +- Manufacturer folders such as [`AnalogDevices/`](AnalogDevices/), `Microchip/`, `Micron/`, `Silabs/`, `Ti/`, and `Xilinx/` hold individual device cores. - Device folders usually contain `rtl/` for synthesizable register/control logic, optional `sim/` models, optional FPGA-family implementation directories, and a local `ruckus.tcl`. - `transceivers/` holds generic pluggable transceiver support such as SFP/QSFP control and status blocks. Keep register maps and control names aligned with vendor data sheets and with the matching PyRogue modules under `python/surf/devices` when they exist. Add new device sources to the nearest `ruckus.tcl`. + +The Analog Devices tree includes the shared [`adcDdr`](AnalogDevices/adcDdr/) +source-synchronous readout and software-calibration infrastructure used by the +normalized AD9249, AD9252, and AD9681 implementations. diff --git a/python/README.md b/python/README.md index 58e78350e8..fe894473aa 100644 --- a/python/README.md +++ b/python/README.md @@ -1,6 +1,6 @@ # Python -The Python package lives under `python/surf` and is installed as `surf`. It primarily contains PyRogue device descriptions and small support utilities that mirror SURF RTL register maps. +The Python package lives under `python/surf` and is installed as `surf`. It primarily contains PyRogue device descriptions and small support utilities that mirror SURF RTL register maps. Python 3.10 or newer is required, matching the supported range of current Rogue releases. ## Layout @@ -12,3 +12,12 @@ The Python package lives under `python/surf` and is installed as `surf`. It prim - `surf/misc/` and `surf/dsp/`: smaller utilities and DSP-related support. Implementation modules usually use private filenames such as `_AxiVersion.py` and are re-exported from package `__init__.py` files. Keep register names, offsets, bit offsets, modes, and descriptions synchronized with the corresponding RTL packages and user-facing hardware documentation. + +## PyRogue API Style + +New and substantially revised PyRogue modules should follow the current Rogue Python style: + +- Add `from __future__ import annotations` and type all function and method parameters and return values, including private helpers. Use Python 3.10 union syntax such as `Node | None`, and use `Any` where PyRogue's dynamic node plumbing makes a more specific annotation misleading. +- Document public classes and functions with NumPy-style docstrings. Put constructor arguments in the class-level `Parameters` section and add `Returns`, `Raises`, and `Notes` sections when they clarify the interface. +- Keep private-method docstrings concise unless the method has a non-obvious contract. +- Use annotations to state accepted Python types. Runtime validation should focus on meaningful hardware constraints, such as supported lane counts and delay widths, instead of duplicating the type system. From 250169a2e41a24611048238133efabcf2cd121d8 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 6 Aug 2026 14:12:19 -0700 Subject: [PATCH 05/57] devices: enhance Ad9252SimCore with additional register constants and address mappings --- .../ad9252/sim/Ad9252SimCore.vhd | 70 ++++++++++++------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd b/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd index 664b9cfdb2..5121eef57a 100644 --- a/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd +++ b/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd @@ -40,6 +40,22 @@ architecture rtl of Ad9252SimCore is constant PN9_SEED_C : slv(8 downto 0) := "011011111"; constant PN23_SEED_C : slv(22 downto 0) := "01001101110000000101000"; + constant SPI_CONFIG_ADDR_C : slv(7 downto 0) := X"00"; + constant CHIP_ID_ADDR_C : slv(7 downto 0) := X"01"; + constant CHIP_GRADE_ADDR_C : slv(7 downto 0) := X"02"; + constant DEVICE_INDEX2_ADDR_C : slv(7 downto 0) := X"04"; + constant DEVICE_INDEX1_ADDR_C : slv(7 downto 0) := X"05"; + constant TEST_MODE_ADDR_C : slv(7 downto 0) := X"0D"; + constant OUTPUT_MODE_ADDR_C : slv(7 downto 0) := X"14"; + constant OUTPUT_PHASE_ADDR_C : slv(7 downto 0) := X"16"; + constant USER_PATTERN1_LSB_C : slv(7 downto 0) := X"19"; + constant USER_PATTERN1_MSB_C : slv(7 downto 0) := X"1A"; + constant USER_PATTERN2_LSB_C : slv(7 downto 0) := X"1B"; + constant USER_PATTERN2_MSB_C : slv(7 downto 0) := X"1C"; + constant SERIAL_OUTPUT_ADDR_C : slv(7 downto 0) := X"21"; + constant CHANNEL_STATUS_ADDR_C : slv(7 downto 0) := X"22"; + constant TRANSFER_ADDR_C : slv(7 downto 0) := X"FF"; + type ChannelType is record testMode : slv(3 downto 0); userMode : slv(1 downto 0); @@ -124,41 +140,41 @@ begin ------------------------------------------------------------------------------------------- if (cfgWrEn = '1') then case cfgAddr is - when X"00" => + when SPI_CONFIG_ADDR_C => if (cfgWrData(5) = '1' or cfgWrData(2) = '1') then v := REG_INIT_C; end if; - when X"04" => + when DEVICE_INDEX2_ADDR_C => v.selectMask(7 downto 4) := cfgWrData(3 downto 0); - when X"05" => + when DEVICE_INDEX1_ADDR_C => v.selectMask(3 downto 0) := cfgWrData(3 downto 0); v.selectMask(9 downto 8) := cfgWrData(5 downto 4); - when X"0D" => + when TEST_MODE_ADDR_C => v.staged.userMode := cfgWrData(7 downto 6); v.staged.resetPn23 := cfgWrData(5); v.staged.resetPn9 := cfgWrData(4); v.staged.testMode := cfgWrData(3 downto 0); - when X"14" => + when OUTPUT_MODE_ADDR_C => v.stagedInvert := cfgWrData(2); - when X"16" => + when OUTPUT_PHASE_ADDR_C => v.staged.outputPhase := cfgWrData(3 downto 0); - when X"19" => + when USER_PATTERN1_LSB_C => v.staged.userPatternA(7 downto 0) := cfgWrData; - when X"1A" => + when USER_PATTERN1_MSB_C => v.staged.userPatternA(13 downto 8) := cfgWrData(5 downto 0); - when X"1B" => + when USER_PATTERN2_LSB_C => v.staged.userPatternB(7 downto 0) := cfgWrData; - when X"1C" => + when USER_PATTERN2_MSB_C => v.staged.userPatternB(13 downto 8) := cfgWrData(5 downto 0); - when X"21" => + when SERIAL_OUTPUT_ADDR_C => assert cfgWrData(2 downto 0) = "000" report "Ad9252SimCore supports only 14-bit serial output" severity failure; v.stagedLsb := cfgWrData(7); - when X"22" => + when CHANNEL_STATUS_ADDR_C => v.staged.outputReset := cfgWrData(1); v.staged.powerDown := cfgWrData(0); - when X"FF" => + when TRANSFER_ADDR_C => -- Atomically publish global settings and the staged -- channel image to every channel selected by 0x04/0x05. if (cfgWrData(0) = '1') then @@ -250,20 +266,20 @@ begin end loop; v.rdData := (others => '0'); case cfgAddr is - when X"00" => v.rdData := "00011000"; - when X"01" => v.rdData := X"09"; - when X"02" => v.rdData := X"30"; - when X"04" => v.rdData(3 downto 0) := r.selectMask(7 downto 4); - when X"05" => v.rdData(5 downto 0) := r.selectMask(9 downto 8) & r.selectMask(3 downto 0); - when X"0D" => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; - when X"14" => v.rdData(2) := r.outputInvert; - when X"16" => v.rdData(3 downto 0) := active.outputPhase; - when X"19" => v.rdData := active.userPatternA(7 downto 0); - when X"1A" => v.rdData(5 downto 0) := active.userPatternA(13 downto 8); - when X"1B" => v.rdData := active.userPatternB(7 downto 0); - when X"1C" => v.rdData(5 downto 0) := active.userPatternB(13 downto 8); - when X"21" => v.rdData(7) := r.lsbFirst; - when X"22" => v.rdData(1 downto 0) := active.outputReset & active.powerDown; + when SPI_CONFIG_ADDR_C => v.rdData := "00011000"; + when CHIP_ID_ADDR_C => v.rdData := X"09"; + when CHIP_GRADE_ADDR_C => v.rdData := X"30"; + when DEVICE_INDEX2_ADDR_C => v.rdData(3 downto 0) := r.selectMask(7 downto 4); + when DEVICE_INDEX1_ADDR_C => v.rdData(5 downto 0) := r.selectMask(9 downto 8) & r.selectMask(3 downto 0); + when TEST_MODE_ADDR_C => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; + when OUTPUT_MODE_ADDR_C => v.rdData(2) := r.outputInvert; + when OUTPUT_PHASE_ADDR_C => v.rdData(3 downto 0) := active.outputPhase; + when USER_PATTERN1_LSB_C => v.rdData := active.userPatternA(7 downto 0); + when USER_PATTERN1_MSB_C => v.rdData(5 downto 0) := active.userPatternA(13 downto 8); + when USER_PATTERN2_LSB_C => v.rdData := active.userPatternB(7 downto 0); + when USER_PATTERN2_MSB_C => v.rdData(5 downto 0) := active.userPatternB(13 downto 8); + when SERIAL_OUTPUT_ADDR_C => v.rdData(7) := r.lsbFirst; + when CHANNEL_STATUS_ADDR_C => v.rdData(1 downto 0) := active.outputReset & active.powerDown; when others => v.rdData := (others => '1'); end case; rin <= v; From 2abf08d6595e4e752f9e6f980f62d5ea6a86c588 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 12 Aug 2026 11:34:32 -0700 Subject: [PATCH 06/57] Remove unnecessary attribute bind. --- .../adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd | 3 --- 1 file changed, 3 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd index d6f967ecfd..998e0c226e 100644 --- a/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd +++ b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd @@ -51,9 +51,6 @@ architecture rtl of AdcDdrDeserializerUltraScale is signal rawData : slv(7 downto 0); signal gearboxData : slv(SERIALIZATION_FACTOR_G-1 downto 0); - attribute IODELAY_GROUP : string; - attribute IODELAY_GROUP of U_Delay : label is IODELAY_GROUP_G; - begin assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or From be9d2b00a78a42fb66cf88fbcc7cbfbf4b00ce7a Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 12 Aug 2026 11:35:20 -0700 Subject: [PATCH 07/57] Fix structure that crashes VCS elaboration. --- devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd index 7db9a08b97..8dbf6140b7 100644 --- a/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd +++ b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd @@ -84,7 +84,9 @@ begin SERIALIZATION_FACTOR_G => 14, IODELAY_GROUP_G => IODELAY_GROUP_G, IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, - DATA_FCO_MAP_G => (others => 0)) + -- Ranged-choice (not "others =>") avoids a VCS elaborator segfault when + -- driving a generic-sized NaturalArray generic (DATA_LANES_G-1 downto 0). + DATA_FCO_MAP_G => (NUM_CHANNELS_G-1 downto 0 => 0)) port map ( adcClkRst => adcClkRst, -- [in] idelayCtrlRdy => idelayCtrlRdy, -- [in] From 2292b933883f19095e50f4c953d444ca032b54f8 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 13 Aug 2026 14:24:04 -0700 Subject: [PATCH 08/57] Pass number of channels properly and add a default enum to avoid warnings. --- python/surf/devices/analog_devices/_Ad9249.py | 17 ++++++++++++----- python/surf/devices/analog_devices/_Ad9681.py | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/python/surf/devices/analog_devices/_Ad9249.py b/python/surf/devices/analog_devices/_Ad9249.py index 7b265a79b9..7cf136173b 100644 --- a/python/surf/devices/analog_devices/_Ad9249.py +++ b/python/surf/devices/analog_devices/_Ad9249.py @@ -310,6 +310,7 @@ def __init__( bitSize = 2, bitOffset = 4, enum = { + 0b00: 'Default (14 bits)', # power-up/reset value; effective 14-bit 0b01: '14 bits', 0b10: '12 bits', }, @@ -427,15 +428,21 @@ def __init__( self, *, deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + numChannels: int = 8, **kwargs: Any) -> None: - """Create one normalized AD9249 bank readout.""" + """Create one normalized AD9249 bank readout. + + ``numChannels`` must match the RTL ``NUM_CHANNELS_G`` generic; the + firmware only implements delay/data registers for the instantiated + lanes, so reading a wider map returns an AXI decode error. + """ delayBits = analog_devices.adcDdrDelayBits(deviceFamily) - kwargs.setdefault('description', 'One normalized eight-channel AD9249 output bank') + kwargs.setdefault('description', 'One normalized AD9249 output bank') super().__init__( - dataLanes = 8, + dataLanes = numChannels, fcoLanes = 1, - channels = 8, + channels = numChannels, sampleBits = 14, serializationFactor = 14, delayBits = delayBits, @@ -470,7 +477,7 @@ def __init__( super().__init__( config = config, readout = readout, - dataLaneToChannel = tuple(range(8)), + dataLaneToChannel = tuple(range(readout._dataLanes)), pn23Reset = config.ResetPNLong, **kwargs) diff --git a/python/surf/devices/analog_devices/_Ad9681.py b/python/surf/devices/analog_devices/_Ad9681.py index 0b78996ed7..89925b1e91 100644 --- a/python/surf/devices/analog_devices/_Ad9681.py +++ b/python/surf/devices/analog_devices/_Ad9681.py @@ -399,6 +399,7 @@ def __init__( bitSize = 2, bitOffset = 4, enum = { + 0b00: 'Default (14 bits)', # power-up/reset value; effective 14-bit 0b01: '14 bits', 0b10: '12 bits', }, From 2b9a3dd19910ab520db8509bb3bfb7f94adaae2f Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 13 Aug 2026 17:36:55 -0700 Subject: [PATCH 09/57] Fix assert message. --- python/surf/devices/analog_devices/_Ad9249Legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/surf/devices/analog_devices/_Ad9249Legacy.py b/python/surf/devices/analog_devices/_Ad9249Legacy.py index 83611c9a6c..6e915cd32e 100644 --- a/python/surf/devices/analog_devices/_Ad9249Legacy.py +++ b/python/surf/devices/analog_devices/_Ad9249Legacy.py @@ -30,7 +30,7 @@ def __init__(self, fpga = '7series', channels = 8, **kwargs): - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' + assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 1 and 8' super().__init__(name=name, description=description, **kwargs) if fpga == '7series': From 1f58948f6311e2049c61383f633d3f99d79dabb7 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 13 Aug 2026 17:47:14 -0700 Subject: [PATCH 10/57] Rogue CI support. --- .github/workflows/surf_ci.yml | 39 ++++++++++++++++++- conda-rogue.yml | 18 +++++++++ .../analog_devices/test_AdcDdrCalibration.py | 7 +++- .../analog_devices/test_AdcDdrModel.py | 15 +++++-- 4 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 conda-rogue.yml diff --git a/.github/workflows/surf_ci.yml b/.github/workflows/surf_ci.yml index 2a9d1070b7..6d034f9b8f 100644 --- a/.github/workflows/surf_ci.yml +++ b/.github/workflows/surf_ci.yml @@ -222,6 +222,41 @@ jobs: codecov coverage report -m +# ---------------------------------------------------------------------------- + + adc_ddr_rogue: + name: ADC DDR Rogue Tests + runs-on: ubuntu-24.04 + timeout-minutes: 15 + defaults: + run: + shell: bash -el {0} + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Setup Miniforge + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 + with: + miniforge-variant: Miniforge3 + miniforge-version: latest + activate-environment: surf-rogue-test + environment-file: conda-rogue.yml + auto-activate: false + conda-remove-defaults: true + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r pip_requirements.txt + + - name: Run ADC DDR Rogue tests + run: | + python -c "import rogue, pyrogue; print(rogue.Version.current())" + PYTHONPATH="$PWD/python" python -m pytest -q -n 0 \ + tests/devices/analog_devices/test_AdcDdrCalibration.py \ + tests/devices/analog_devices/test_AdcDdrModel.py + # ---------------------------------------------------------------------------- docs: @@ -250,7 +285,7 @@ jobs: # ---------------------------------------------------------------------------- gen_release: - needs: [lint, test, docs] + needs: [lint, test, adc_ddr_rogue, docs] if: startsWith(github.ref, 'refs/tags/') uses: slaclab/ruckus/.github/workflows/gen_release.yml@main with: @@ -261,7 +296,7 @@ jobs: # ---------------------------------------------------------------------------- conda_build_lib: - needs: [lint, test, docs] + needs: [lint, test, adc_ddr_rogue, docs] if: startsWith(github.ref, 'refs/tags/') uses: slaclab/ruckus/.github/workflows/conda_build_lib.yml@main with: diff --git a/conda-rogue.yml b/conda-rogue.yml new file mode 100644 index 0000000000..292d0156ef --- /dev/null +++ b/conda-rogue.yml @@ -0,0 +1,18 @@ +#----------------------------------------------------------------------------- +# This file is part of the 'SLAC Firmware Standard Library'. It is subject to +# the license terms in the LICENSE.txt file found in the top-level directory +# of this distribution and at: +# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +# No part of 'SLAC Firmware Standard Library', including this file, may be +# copied, modified, propagated, or distributed except according to the terms +# contained in the LICENSE.txt file. +#----------------------------------------------------------------------------- + +name: surf-rogue-test +channels: + - tidair-tag + - conda-forge +dependencies: + - python=3.12 + - pip + - rogue=v6.15.0 diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index ef1f44021f..5a9d826551 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -26,9 +26,12 @@ import time import pytest -import pyrogue as pr -from surf.devices.analog_devices import ( +pr = pytest.importorskip( + 'pyrogue', + reason='ADC DDR calibration tests require Rogue/PyRogue') + +from surf.devices.analog_devices import ( # noqa: E402 AdcDdrCalibration, checkAdcDdrPn23, findAdcDdrEye, diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py index f44ff16d06..0483cbaff4 100644 --- a/tests/devices/analog_devices/test_AdcDdrModel.py +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -21,11 +21,18 @@ # snapshot, and settling behavior is covered by the calibration mock test. import pytest -import pyrogue as pr -import rogue -import rogue.interfaces.memory as rim -from surf.devices.analog_devices import ( +pr = pytest.importorskip( + 'pyrogue', + reason='ADC DDR model tests require Rogue/PyRogue') +rogue = pytest.importorskip( + 'rogue', + reason='ADC DDR model tests require Rogue/PyRogue') +rim = pytest.importorskip( + 'rogue.interfaces.memory', + reason='ADC DDR model tests require Rogue/PyRogue') + +from surf.devices.analog_devices import ( # noqa: E402 Ad9249Config, Ad9249ConfigGroup, Ad9249ChipConfig, From 84c1ed4ef3f575545b4a65b2350b2c40875fa440 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 13 Aug 2026 22:16:31 -0700 Subject: [PATCH 11/57] Update idelayCtrlRdy default to low and enhance documentation for delay readiness handling --- devices/AnalogDevices/ad9249/README.md | 7 +- .../ad9249/core/Ad9249Readout.vhd | 2 +- .../ad9249/core/Ad9249ReadoutBank.vhd | 2 +- .../ad9252/core/Ad9252Readout.vhd | 2 +- .../ad9681/core/Ad9681Readout.vhd | 2 +- .../adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd | 2 +- devices/AnalogDevices/adcDdr/README.md | 19 +++++ .../AnalogDevices/adcDdr/migration-guide.md | 12 +++- .../AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd | 25 ++++--- .../AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd | 2 +- python/surf/devices/analog_devices/_AdcDdr.py | 2 +- .../devices/analog_devices/test_AdcDdrCore.py | 72 +++++++++++++++++-- 12 files changed, 120 insertions(+), 29 deletions(-) diff --git a/devices/AnalogDevices/ad9249/README.md b/devices/AnalogDevices/ad9249/README.md index ed462d2073..b38aaab2ce 100644 --- a/devices/AnalogDevices/ad9249/README.md +++ b/devices/AnalogDevices/ad9249/README.md @@ -163,10 +163,13 @@ adcStreamRst => adcStreamRst, -- [in] For 7-Series, instantiate or reuse a target-owned `IDELAYCTRL`, match its `IODELAY_GROUP` to `IODELAY_GROUP_G`, constrain its reference clock at `IDELAYCTRL_FREQ_G`, and connect `RDY` to `idelayCtrlRdy`. Do not tie readiness -high merely to bypass startup sequencing in hardware. +high merely to bypass startup sequencing in hardware. The input defaults low, +and any later loss of `RDY` holds the readout PHY in reset until readiness +returns. The target must reset `IDELAYCTRL` after its reference clock is stable; +the readout does not generate that target-owned reset. UltraScale/UltraScale+ uses `IDELAYE3` count mode and does not use -`IDELAYCTRL`; tie `idelayCtrlRdy` high explicitly or accept its default. +`IDELAYCTRL`; the selected PHY ignores the low `idelayCtrlRdy` default. `adcStreamRst` resets the coherent stream-domain FIFO/output state and must be valid in the `adcStreamClk` domain. diff --git a/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd b/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd index 39ef347835..5ecdcdf37b 100644 --- a/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd +++ b/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd @@ -44,7 +44,7 @@ entity Ad9249Readout is axilReadSlave : out AxiLiteReadSlaveType; adcClkRst : in sl; - idelayCtrlRdy : in slv(1 downto 0) := (others => '1'); + idelayCtrlRdy : in slv(1 downto 0) := (others => '0'); adcSerial : in Ad9249SerialGroupArray(1 downto 0); adcStreamClk : in sl; diff --git a/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd index 8dbf6140b7..d53ddff0a7 100644 --- a/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd +++ b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd @@ -45,7 +45,7 @@ entity Ad9249ReadoutBank is axilReadSlave : out AxiLiteReadSlaveType; adcClkRst : in sl; - idelayCtrlRdy : in sl := '1'; + idelayCtrlRdy : in sl := '0'; adcSerial : in Ad9249SerialGroupType; adcStreamClk : in sl; diff --git a/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd b/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd index 83b7869b3a..638e7b7ed9 100644 --- a/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd +++ b/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd @@ -46,7 +46,7 @@ entity Ad9252Readout is axilReadSlave : out AxiLiteReadSlaveType; adcClkRst : in sl; - idelayCtrlRdy : in sl := '1'; + idelayCtrlRdy : in sl := '0'; adc : in Ad9252SerialType; adcStreamClk : in sl; diff --git a/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd b/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd index 12db52f022..a4e0474c80 100644 --- a/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd +++ b/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd @@ -46,7 +46,7 @@ entity Ad9681Readout is axilReadSlave : out AxiLiteReadSlaveType; adcClkRst : in sl; - idelayCtrlRdy : in sl := '1'; + idelayCtrlRdy : in sl := '0'; adcSerial : in Ad9681SerialType; adcStreamClk : in sl; diff --git a/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd index 1dc0d83774..9ddda80da7 100644 --- a/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd +++ b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd @@ -33,7 +33,7 @@ entity AdcDdrPhy7Series is DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); port ( adcClkRst : in sl; - idelayCtrlRdy : in sl := '1'; + idelayCtrlRdy : in sl := '0'; phyReset : in sl; dClkP : in sl; dClkN : in sl; diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index cc1c4e764b..2f67f824e9 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -26,6 +26,25 @@ device-neutral software calibration process is Device adapters in `_Ad9249.py`, `_Ad9252.py`, and `_Ad9681.py` supply the physical-lane mapping and ADC configuration behavior. +## Delay Controller Readiness + +The `idelayCtrlRdy` input defaults low so an omitted 7-Series connection holds +the capture PHY safely in reset. A 7-Series target must instantiate or reuse an +`IDELAYCTRL`, match its `IODELAY_GROUP`, and connect the controller's `RDY` +output. UltraScale and UltraScale+ use `IDELAYE3` count mode; their selected PHY +does not use `idelayCtrlRdy`. + +If readiness drops after startup, the common core clears frame alignment, +resets the deserializers, stops sample writes, and aborts an outstanding debug +snapshot with `SLVERR`. Programmed data and FCO delays remain retained. When +readiness returns, the core reloads every retained delay before releasing the +PHY and reacquiring alignment. + +The target owns `IDELAYCTRL` reset generation because it also owns the reference +clock and knows when that clock is stable. If `RDY` deasserts, the target must +reset the controller according to the AMD primitive requirements; the common +readout does not generate that reset request. + ## Calibration Model Calibration finds a stable sampling point for every FCO and physical data lane. diff --git a/devices/AnalogDevices/adcDdr/migration-guide.md b/devices/AnalogDevices/adcDdr/migration-guide.md index 04d93edc34..82044b8149 100644 --- a/devices/AnalogDevices/adcDdr/migration-guide.md +++ b/devices/AnalogDevices/adcDdr/migration-guide.md @@ -31,9 +31,9 @@ old `0x100`-byte AXI-Lite slot or place the next endpoint inside this window. AdcDdr-based wrappers add two target-facing inputs: -- `idelayCtrlRdy` reports that the target-owned input-delay controller is ready. - Drive the real ready indication in hardware. A constant `'1'` is appropriate - only when readiness is guaranteed externally or in focused simulation. +- `idelayCtrlRdy` reports that the target-owned input-delay controller is ready + and defaults low. Drive the real ready indication in 7-Series hardware. The + UltraScale/UltraScale+ PHY ignores this input. - `adcStreamRst` resets the coherent stream-domain FIFO and output state. Drive the reset synchronous to `adcStreamClk` according to the target reset plan. @@ -43,6 +43,12 @@ pin and timing constraints remain target-owned; update the project's top-level XDC for its actual pinout, ADC mode, DCO frequency, datasheet timing, PCB skew, and clocking hierarchy. +If `IDELAYCTRL.RDY` deasserts after startup, the readout holds its PHY in reset, +clears alignment, stops sample writes, aborts an outstanding snapshot with +`SLVERR`, and waits for readiness to return before reloading every retained +delay. The target must reset `IDELAYCTRL` after its reference clock is stable; +the readout intentionally does not generate the target-owned controller reset. + The normalized stream contract is: - one fixed two-byte transfer per logical channel and sample epoch; diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd index 73f07473a7..9d6b25f900 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd @@ -276,7 +276,8 @@ begin -- the saturating lost-lock counter. ---------------------------------------------------------------------------------------------- for i in FCO_LANES_G-1 downto 0 loop - if (r.relock = '1' or r.phyReset = '1' or r.startupPending = '1') then + if (r.relock = '1' or r.phyReset = '1' or r.startupPending = '1' or + delayReady = '0') then v.locked(i) := '0'; v.matchCount(i) := 0; v.errorCount(i) := 0; @@ -371,7 +372,7 @@ begin -- after the fourth group prevents AXI software from observing a snapshot -- assembled from different requests. ---------------------------------------------------------------------------------------------- - if (r.debugBusy = '1' and sampleValid = '1') then + if (r.debugBusy = '1' and sampleValid = '1' and delayReady = '1') then for i in CHANNELS_G-1 downto 0 loop v.debugWorking((r.debugIndex*CHANNELS_G)+i) := resize(sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0), 16); @@ -402,7 +403,11 @@ begin snapshotTxn := r.debugBusy; if (r.debugBusy = '1') then ep.axiStatus.writeEnable := '0'; - if (sampleValid = '1' and r.debugIndex = 3) then + if (delayReady = '0') then + v.debugBusy := '0'; + v.debugIndex := 0; + axiSlaveWriteResponse(ep.axiWriteSlave, AXI_RESP_SLVERR_C); + elsif (sampleValid = '1' and r.debugIndex = 3) then axiSlaveWriteResponse(ep.axiWriteSlave); end if; elsif (ep.axiStatus.writeEnable = '1' and @@ -411,7 +416,7 @@ begin ep.axiWriteMaster.wdata(0) = '1') then snapshotTxn := '1'; ep.axiStatus.writeEnable := '0'; - if (r.phyReset = '1' or r.startupPending = '1') then + if (r.phyReset = '1' or r.startupPending = '1' or delayReady = '0') then axiSlaveWriteResponse(ep.axiWriteSlave, AXI_RESP_SLVERR_C); else v.debugBusy := '1'; @@ -471,12 +476,12 @@ begin AXI_RESP_DECERR_C, snapshotTxn); ---------------------------------------------------------------------------------------------- - -- Hold the PHY after capture reset until its delay resources are ready, - -- then apply every configured delay and release alignment automatically. - -- A manual reset rearms the same sequence, so releasing it also waits for - -- delay readiness and reapplies all retained values. + -- Hold the PHY whenever its delay resources are not ready, then apply + -- every retained delay and release alignment automatically. A manual + -- reset rearms the same sequence. The target owns IDELAYCTRL reset and + -- must restore delayReady after any loss of readiness. ---------------------------------------------------------------------------------------------- - if (v.phyReset = '1') then + if (delayReady = '0' or v.phyReset = '1') then v.startupPending := '1'; elsif (r.startupPending = '1' and delayReady = '1') then v.startupPending := '0'; @@ -568,7 +573,7 @@ begin port map ( rst => captureRst or streamRst, -- [in] wr_clk => captureClk, -- [in] - wr_en => r.sampleValid and not r.phyReset and not r.startupPending, -- [in] + wr_en => r.sampleValid and delayReady and not r.phyReset and not r.startupPending, -- [in] din => fifoIn, -- [in] wr_data_count => open, -- [out] wr_ack => open, -- [out] diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd index 39bb15dbc9..63cf9974ec 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd @@ -31,7 +31,7 @@ entity AdcDdrPhy is DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); port ( adcClkRst : in sl; - idelayCtrlRdy : in sl := '1'; + idelayCtrlRdy : in sl := '0'; phyReset : in sl; dClkP : in sl; dClkN : in sl; diff --git a/python/surf/devices/analog_devices/_AdcDdr.py b/python/surf/devices/analog_devices/_AdcDdr.py index 0b88359f0c..86c3f27729 100644 --- a/python/surf/devices/analog_devices/_AdcDdr.py +++ b/python/surf/devices/analog_devices/_AdcDdr.py @@ -304,7 +304,7 @@ def __init__(self, *, self.add(pr.RemoteVariable( name = 'DelayReady', - description = 'Input-delay controller is ready', + description = 'Input-delay controller is ready; low holds the capture PHY in reset', offset = 0x01C, bitSize = 1, bitOffset = 1, diff --git a/tests/devices/analog_devices/test_AdcDdrCore.py b/tests/devices/analog_devices/test_AdcDdrCore.py index 0101729fb5..02aaa30bdb 100644 --- a/tests/devices/analog_devices/test_AdcDdrCore.py +++ b/tests/devices/analog_devices/test_AdcDdrCore.py @@ -13,14 +13,15 @@ # optional pattern engine both enabled and disabled. # - Stimulus: Access the relocatable block at a nonzero absolute AXI address, # verify hardware-owned startup waits for delay readiness and loads every -# configured delay, exercise the manual reset/reload path and required DDR -# bitslip quiet interval, acquire lock, stream samples, load every delay -# class, and snapshot sample history. +# configured delay, exercise manual and readiness-loss reset/reload paths and +# the required DDR bitslip quiet interval, acquire lock, stream samples, load +# every delay class, and snapshot sample history. # - Checks: AXI status follows alignment, streams preserve both channels and -# sidebands, delay requests are width-limited and read back, snapshot writes -# reject reset state and block until publication, and AXI transactions -# execute coherently in the capture domain; the pattern window is -# capability-gated and reports its shared-phase result through AXI-Lite. +# sidebands, delay requests are width-limited and retained across readiness +# loss, an in-flight snapshot aborts with SLVERR, snapshot writes reject reset +# state and otherwise block until publication, and AXI transactions execute +# coherently in the capture domain; the pattern window is capability-gated +# and reports its shared-phase result through AXI-Lite. # - Timing: AXI-Lite is crossed as a complete bus into the capture clock domain; # one wide FIFO crosses coherent channel samples to the stream clock. @@ -233,6 +234,63 @@ async def core_integration_test(dut): await axil_poll(axil, 0x104, lambda value: value == 0x13) await axil_poll(axil, 0x200, lambda value: value == 0x14) + # Loss of delay readiness is a hardware-owned reset request. Abort an + # outstanding snapshot, clear alignment, suppress samples, and retain the + # programmed delays for automatic reload when readiness returns. + snapshot = cocotb.start_soon( + axil.write(AXIL_BASE_ADDR + 0x014, (0x01).to_bytes(4, 'little'))) + await Timer(500, unit="ns") + assert not snapshot.done() + for index in range(3): + await FallingEdge(dut.captureClk) + dut.sampleIn.value = ((0x0300 + index) << 16) | (0x0200 + index) + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + assert not snapshot.done() + + # Drop readiness with what would otherwise be the publishing fourth sample. + # Readiness loss takes priority, so the snapshot sequence must not advance. + await FallingEdge(dut.captureClk) + dut.delayReady.value = 0 + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + response = await snapshot + assert response.resp == AxiResp.SLVERR + assert int(dut.phyReset.value) == 1 + assert await axil_read_u32(axil, 0x01C) & 0x07 == 0 + assert await axil_read_u32(axil, 0x024) == 0 + assert await axil_read_u32(axil, 0x100) == 0x1F + assert await axil_read_u32(axil, 0x104) == 0x13 + assert await axil_read_u32(axil, 0x200) == 0x14 + + await FallingEdge(dut.captureClk) + dut.sampleIn.value = 0x0123_0234 + dut.sampleValid.value = 1 + for _ in range(4): + await RisingEdge(dut.captureClk) + dut.sampleValid.value = 0 + for _ in range(20): + await RisingEdge(dut.streamClk) + await Timer(1, unit="ns") + assert int(dut.streamValid.value) == 0 + + data_load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 0x3)) + fco_load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 0x1)) + await FallingEdge(dut.captureClk) + dut.delayReady.value = 1 + await data_load + await fco_load + assert int(dut.dataDelayValue.value) == 0x0013_001F + assert int(dut.fcoDelayValue.value) == 0x0014 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 0 + await axil_poll(axil, 0x01C, lambda value: (value & 0x07) == 0x06) + dut.sampleIn.value = 0x2000_1000 snapshot = cocotb.start_soon(axil_write_u32(axil, 0x014, 0x01)) await Timer(500, unit="ns") From 446819fafed27aa465506a7954219609b0eaf2a0 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Tue, 18 Aug 2026 15:42:04 -0700 Subject: [PATCH 12/57] fix(i2c): saturate I2cMaster timeout counter The timer field is declared as integer range 0 to TIMEOUT_C, but the WAIT_ADDR_ACK_S, WAIT_READ_DATA_S, and WAIT_WRITE_ACK_S states incremented it unconditionally. Because the timeout check compares r.timer against TIMEOUT_C, the increment evaluates TIMEOUT_C+1 one cycle before the timeout is detected, which trips a range violation in VCS simulation (SIMERR_CONSTERR_INDEXEDRANGE). Guard each increment so the counter saturates at TIMEOUT_C. The registered value is unchanged: when the guard blocks the increment, v.timer keeps its default of 0 and the timeout logic also forces 0, so the timeout error still fires on the same clock cycle as before. --- protocols/i2c/rtl/I2cMaster.vhd | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/protocols/i2c/rtl/I2cMaster.vhd b/protocols/i2c/rtl/I2cMaster.vhd index 026e84cbe0..6507eac4c5 100644 --- a/protocols/i2c/rtl/I2cMaster.vhd +++ b/protocols/i2c/rtl/I2cMaster.vhd @@ -260,7 +260,10 @@ begin when WAIT_ADDR_ACK_S => - v.timer := r.timer + 1; + -- Saturate the timer to prevent an out of range value + if (r.timer /= TIMEOUT_C) then + v.timer := r.timer + 1; + end if; if (byteCtrlOut.cmdAck = '1') then -- Master sent the command if (byteCtrlOut.ackOut = '0') then -- Slave ack'd the transfer @@ -302,7 +305,10 @@ begin when WAIT_READ_DATA_S => - v.timer := r.timer + 1; + -- Saturate the timer to prevent an out of range value + if (r.timer /= TIMEOUT_C) then + v.timer := r.timer + 1; + end if; v.byteCtrlIn.stop := r.byteCtrlIn.stop; -- Hold stop or it wont get seen v.byteCtrlIn.ackIn := r.byteCtrlIn.ackIn; -- This too @@ -331,7 +337,10 @@ begin end if; when WAIT_WRITE_ACK_S => - v.timer := r.timer + 1; + -- Saturate the timer to prevent an out of range value + if (r.timer /= TIMEOUT_C) then + v.timer := r.timer + 1; + end if; v.byteCtrlIn.stop := r.byteCtrlIn.stop; if (byteCtrlOut.cmdAck = '1') then -- Master sent the command From 88f6989025013f149234438c0160cdca7e4ea484 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Fri, 21 Aug 2026 13:06:28 -0700 Subject: [PATCH 13/57] fix(AxiLiteAsync): stop replaying errored transactions after remote reset With COMMON_CLK_G = false, an access issued while the master domain was held in reset was answered with AXI_ERROR_RESP_G on the slave side and queued at the same time, so it executed on the master side once mAxiClk and mAxiClkRst recovered. A write could modify hardware after software had been told the access failed, and a register implemented as a command strobe could fire long after its caller gave up. Five defects in the GEN_ASYNC branch: - The request FIFO write enables were not gated by the remote reset, so a rejected request was queued anyway. - The request FIFOs were reset only by s2mRst, which carries sAxiClkRst, so a request queued before mAxiClkRst asserted also survived. The response FIFOs had the mirror-image problem and were not flushed by a slave-domain reset. - AW and W cross in separate FIFOs, so a write straddling the reset boundary left an address queued with no data behind it, and the next write's data was then committed to that abandoned address. - RVALID and BVALID were forced to a constant '1', so a response could be presented with no matching request and ahead of its request handshake. - Every reset comparison was written against '0' rather than RST_POLARITY_G, which inverted the fail-fast sense for active LOW resets. All five FIFOs now share one reset, asserted while either domain is in reset or while the bridge is still draining an abandoned transaction, so the bridge is empty whenever either side is reset. Each rejected access is answered from per-channel pending state, so exactly one response follows each accepted request and a write response waits for both AW and W. The bridge now also holds one transaction in flight per channel, matching AxiLiteCrossbar, whose per-slot state machine does not release a slot until rvalid and rready, and AxiLiteMaster, which runs one transaction at a time. The bound is enforced by the ready outputs rather than assumed, because the bridge otherwise accepts four or more outstanding and one pending flag per channel could answer only one of them. Sizing the tracking to the FIFO capacity instead would have needed three 7-bit counters, costing 45 LUTs and 22 flip-flops against the 4 flip-flops this uses. No port or generic changes. --- axi/axi-lite/rtl/AxiLiteAsync.vhd | 187 ++++++++++++++++++++++++++---- 1 file changed, 163 insertions(+), 24 deletions(-) diff --git a/axi/axi-lite/rtl/AxiLiteAsync.vhd b/axi/axi-lite/rtl/AxiLiteAsync.vhd index 4fbf5edb8c..01d2ed15c7 100755 --- a/axi/axi-lite/rtl/AxiLiteAsync.vhd +++ b/axi/axi-lite/rtl/AxiLiteAsync.vhd @@ -89,6 +89,40 @@ architecture STRUCTURE of AxiLiteAsync is signal writeMastToSlaveRead : sl; signal writeMastToSlaveWrite : sl; + -- Depth of every channel FIFO instantiated below + constant FIFO_ADDR_WIDTH_C : positive := 4; + + -- Reset terms normalized to active HIGH, independent of RST_POLARITY_G + signal s2mRstActive : sl; + signal m2sRstActive : sl; + signal mAxiRstActive : sl; + + -- Asserted while every FIFO in the bridge must be held empty + signal fifoRst : sl; + + -- Slave side handshakes, kept local so the error responder can observe them + signal sArReady : sl; + signal sRValid : sl; + signal sAwReady : sl; + signal sWReady : sl; + signal sBValid : sl; + + type RegType is record + errMode : sl; -- Answering locally with AXI_ERROR_RESP_G + rPend : sl; -- Read accepted, not yet answered + awPend : sl; -- Write address accepted, not yet answered + wPend : sl; -- Write data accepted, not yet answered + end record RegType; + + constant REG_INIT_C : RegType := ( + errMode => '0', + rPend => '0', + awPend => '0', + wPend => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + begin GEN_SYNC : if (COMMON_CLK_G = true) generate @@ -125,6 +159,102 @@ begin asyncRst => mAxiClkRst, syncRst => m2sRst); + -- Normalize both synchronized resets to active HIGH so everything below is + -- correct for either RST_POLARITY_G setting + s2mRstActive <= '1' when (s2mRst = RST_POLARITY_G) else '0'; + m2sRstActive <= '1' when (m2sRst = RST_POLARITY_G) else '0'; + mAxiRstActive <= '1' when (mAxiClkRst = RST_POLARITY_G) else '0'; + + -- Hold every FIFO empty while either domain is in reset, and while the + -- bridge is still answering transactions it has abandoned. Without this a + -- request accepted before the remote reset stays queued and is replayed + -- downstream once the remote domain recovers, even though the slave side + -- was already told the access failed. FifoAsync resynchronizes this reset + -- into both of its clock domains, so one assertion clears the read and + -- write pointers even while mAxiClk is stopped. + fifoRst <= RST_POLARITY_G when (s2mRstActive = '1') or (m2sRstActive = '1') or (r.errMode = '1') else not RST_POLARITY_G; + + -- Transaction tracking and local error responder. + -- + -- One transaction per channel is in flight at a time, matching + -- AxiLiteCrossbar, whose per-slot state machine does not release a slot + -- until the response completes. The ready outputs below enforce that bound + -- rather than assuming the master honours it. + -- + -- The same state decides what the bridge owes the slave side while the + -- remote domain is in reset, when each access is answered locally instead + -- of being forwarded. That keeps AXI-Lite ordering intact, because a read + -- response only follows an accepted AR and a write response only follows + -- both an accepted AW and W, and it covers the transaction discarded by + -- fifoRst above, which still owes the slave side a response. + comb : process (m2sRstActive, r, sArReady, sAwReady, sAxiClkRst, + sAxiReadMaster, sAxiWriteMaster, sBValid, sRValid, sWReady) is + variable v : RegType; + variable arTxn : sl; + variable rTxn : sl; + variable awTxn : sl; + variable wTxn : sl; + variable bTxn : sl; + begin + -- Latch the current value + v := r; + + -- Slave side handshakes + arTxn := sAxiReadMaster.arvalid and sArReady; + rTxn := sRValid and sAxiReadMaster.rready; + awTxn := sAxiWriteMaster.awvalid and sAwReady; + wTxn := sAxiWriteMaster.wvalid and sWReady; + bTxn := sBValid and sAxiWriteMaster.bready; + + -- Read accepted but not yet answered. Set and clear are mutually + -- exclusive because ARREADY is held low while the read is pending. + if (arTxn = '1') then + v.rPend := '1'; + elsif (rTxn = '1') then + v.rPend := '0'; + end if; + + -- Write address accepted but not yet answered + if (awTxn = '1') then + v.awPend := '1'; + elsif (bTxn = '1') then + v.awPend := '0'; + end if; + + -- Write data accepted but not yet answered + if (wTxn = '1') then + v.wPend := '1'; + elsif (bTxn = '1') then + v.wPend := '0'; + end if; + + -- Enter error mode when the remote domain resets and stay there until + -- the abandoned transaction has been answered + if (m2sRstActive = '1') then + v.errMode := '1'; + elsif (v.rPend = '0') and (v.awPend = '0') and (v.wPend = '0') then + v.errMode := '0'; + end if; + + -- Synchronous Reset + if (RST_ASYNC_G = false) and (sAxiClkRst = RST_POLARITY_G) then + v := REG_INIT_C; + end if; + + -- Register the variable for the next clock cycle + rin <= v; + + end process comb; + + seq : process (sAxiClk, sAxiClkRst) is + begin + if (RST_ASYNC_G) and (sAxiClkRst = RST_POLARITY_G) then + r <= REG_INIT_C after TPD_G; + elsif rising_edge(sAxiClk) then + r <= rin after TPD_G; + end if; + end process seq; + ------------------------------------ -- Read: Slave to Master ------------------------------------ @@ -140,12 +270,12 @@ begin SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => NUM_ADDR_BITS_G+3, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => s2mRst, + rst => fifoRst, wr_clk => sAxiClk, wr_en => readSlaveToMastWrite, din => readSlaveToMastDin, @@ -171,9 +301,12 @@ begin readSlaveToMastDin(2 downto 0) <= sAxiReadMaster.arprot; readSlaveToMastDin(NUM_ADDR_BITS_G+2 downto 3) <= sAxiReadMaster.araddr(NUM_ADDR_BITS_G-1 downto 0); - -- Write control and ready generation - sAxiReadSlave.arready <= ite(m2sRst = '0', not readSlaveToMastFull, '1'); - readSlaveToMastWrite <= sAxiReadMaster.arvalid and (not readSlaveToMastFull); + -- Write control and ready generation. The request is never queued while the + -- bridge is answering locally, otherwise an access already reported as + -- failed would still reach the master side. + sArReady <= (not r.rPend) when (r.errMode = '1') else ((not readSlaveToMastFull) and (not r.rPend)); + sAxiReadSlave.arready <= sArReady; + readSlaveToMastWrite <= sAxiReadMaster.arvalid and (not readSlaveToMastFull) and (not r.errMode); -- Data Out mAxiReadMaster.arprot <= readSlaveToMastDout(2 downto 0); @@ -203,12 +336,12 @@ begin SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => 34, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => m2sRst, + rst => fifoRst, wr_clk => mAxiClk, wr_en => readMastToSlaveWrite, din => readMastToSlaveDin, @@ -235,15 +368,17 @@ begin readMastToSlaveDin(33 downto 2) <= mAxiReadSlave.rdata; -- Write control and ready generation - mAxiReadMaster.rready <= ite(mAxiClkRst = '0', not readMastToSlaveFull, '1'); + mAxiReadMaster.rready <= '1' when (mAxiRstActive = '1') else (not readMastToSlaveFull); readMastToSlaveWrite <= mAxiReadSlave.rvalid and (not readMastToSlaveFull); -- Data Out - sAxiReadSlave.rresp <= ite(m2sRst = '0', readMastToSlaveDout(1 downto 0), AXI_ERROR_RESP_G); + sAxiReadSlave.rresp <= AXI_ERROR_RESP_G when (r.errMode = '1') else readMastToSlaveDout(1 downto 0); sAxiReadSlave.rdata <= readMastToSlaveDout(33 downto 2); - -- Read control and valid - sAxiReadSlave.rvalid <= ite(m2sRst = '0', readMastToSlaveValid, '1'); + -- Read control and valid. Answering locally requires an accepted AR, so the + -- response can never arrive ahead of its request. + sRValid <= r.rPend when (r.errMode = '1') else readMastToSlaveValid; + sAxiReadSlave.rvalid <= sRValid; readMastToSlaveRead <= sAxiReadMaster.rready; ------------------------------------ @@ -261,12 +396,12 @@ begin SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => NUM_ADDR_BITS_G+3, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => s2mRst, + rst => fifoRst, wr_clk => sAxiClk, wr_en => writeAddrSlaveToMastWrite, din => writeAddrSlaveToMastDin, @@ -293,8 +428,9 @@ begin writeAddrSlaveToMastDin(NUM_ADDR_BITS_G+2 downto 3) <= sAxiWriteMaster.awaddr(NUM_ADDR_BITS_G-1 downto 0); -- Write control and ready generation - sAxiWriteSlave.awready <= ite(m2sRst = '0', not writeAddrSlaveToMastFull, '1'); - writeAddrSlaveToMastWrite <= sAxiWriteMaster.awvalid and (not writeAddrSlaveToMastFull); + sAwReady <= (not r.awPend) when (r.errMode = '1') else ((not writeAddrSlaveToMastFull) and (not r.awPend)); + sAxiWriteSlave.awready <= sAwReady; + writeAddrSlaveToMastWrite <= sAxiWriteMaster.awvalid and (not writeAddrSlaveToMastFull) and (not r.errMode); -- Data Out mAxiWriteMaster.awprot <= writeAddrSlaveToMastDout(2 downto 0); @@ -324,12 +460,12 @@ begin SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => 36, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => s2mRst, + rst => fifoRst, wr_clk => sAxiClk, wr_en => writeDataSlaveToMastWrite, din => writeDataSlaveToMastDin, @@ -356,8 +492,9 @@ begin writeDataSlaveToMastDin(35 downto 4) <= sAxiWriteMaster.wdata; -- Write control and ready generation - sAxiWriteSlave.wready <= ite(m2sRst = '0', not writeDataSlaveToMastFull, '1'); - writeDataSlaveToMastWrite <= sAxiWriteMaster.wvalid and (not writeDataSlaveToMastFull); + sWReady <= (not r.wPend) when (r.errMode = '1') else ((not writeDataSlaveToMastFull) and (not r.wPend)); + sAxiWriteSlave.wready <= sWReady; + writeDataSlaveToMastWrite <= sAxiWriteMaster.wvalid and (not writeDataSlaveToMastFull) and (not r.errMode); -- Data Out mAxiWriteMaster.wstrb <= writeDataSlaveToMastDout(3 downto 0); @@ -382,12 +519,12 @@ begin SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => 2, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => m2sRst, + rst => fifoRst, wr_clk => mAxiClk, wr_en => writeMastToSlaveWrite, din => writeMastToSlaveDin, @@ -417,10 +554,12 @@ begin writeMastToSlaveWrite <= mAxiWriteSlave.bvalid and (not writeMastToSlaveFull); -- Data Out - sAxiWriteSlave.bresp <= ite(m2sRst = '0', writeMastToSlaveDout, AXI_ERROR_RESP_G); + sAxiWriteSlave.bresp <= AXI_ERROR_RESP_G when (r.errMode = '1') else writeMastToSlaveDout; - -- Read control and valid - sAxiWriteSlave.bvalid <= ite(m2sRst = '0', writeMastToSlaveValid, '1'); + -- Read control and valid. Answering locally requires both an accepted AW and + -- an accepted W, so the two channels can still arrive in either order. + sBValid <= (r.awPend and r.wPend) when (r.errMode = '1') else writeMastToSlaveValid; + sAxiWriteSlave.bvalid <= sBValid; writeMastToSlaveRead <= sAxiWriteMaster.bready; end generate; From e2df92d4527c8186f20869916306634bfcdea060 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Fri, 21 Aug 2026 13:06:29 -0700 Subject: [PATCH 14/57] test(AxiLiteAsync): cover remote reset recovery, reset polarity and the bound The sweep previously ran COMMON_CLK_G = true only, so the asynchronous branch of the bridge had no coverage at all. It now runs five cases: common-clock plus asynchronous active-high, active-low, RST_ASYNC_G = true and PIPE_STAGES_G = 2. The bench drives mAxiClk from a gateable coroutine so a test can hold the master domain still while the slave domain keeps running, records an ordered log of the handshakes the downstream slave accepts, counts slave-side handshakes so every response can be matched to an accepted request, and bounds each slave-side access so a missing fail-fast response fails the test instead of hanging the regression. Some tests drive the slave port directly rather than through cocotbext-axi, because the channel ordering cases need AW and W presented independently. New tests: - remote_reset_ghost_test: an access rejected while the remote domain is reset is never replayed downstream after recovery. - remote_reset_write_order_test: a local write response waits for both AW and W, a read response waits for an accepted AR, and neither repeats. - remote_reset_inflight_flush_test: a request queued before the remote reset is discarded rather than replayed. - remote_reset_orphan_pairing_test: an abandoned write address does not pair with a later write data beat and misdirect it. - source_reset_stale_response_test: a response queued when the slave domain resets is discarded instead of being consumed by the next read. - source_reset_clears_outstanding_test: a slave-domain reset clears the pending state, so a later remote reset does not answer an abandoned transaction. This is what exercises the reset path of the registered logic, since a RST_ASYNC_G = true case on its own compiled the branch without checking it. - single_outstanding_bound_test: one transaction per channel is accepted, in normal operation and during a remote reset. Each was confirmed to fail when only its corresponding RTL fix is reverted. reset_behavior_test now waits out the cross-domain reset release before expecting a successful access, which the asynchronous cases require. --- tests/axi/axi_lite/test_AxiLiteAsync.py | 798 +++++++++++++++++++++++- 1 file changed, 772 insertions(+), 26 deletions(-) diff --git a/tests/axi/axi_lite/test_AxiLiteAsync.py b/tests/axi/axi_lite/test_AxiLiteAsync.py index 5e31bc9880..c75c00588c 100644 --- a/tests/axi/axi_lite/test_AxiLiteAsync.py +++ b/tests/axi/axi_lite/test_AxiLiteAsync.py @@ -9,25 +9,28 @@ ############################################################################## # Test methodology: -# - Sweep: Keep a narrow common-clock wrapper-focused case that proves the -# cocotb-facing bridge topology and stable pass-through behavior without -# trying to force the less simulator-stable asynchronous reset branches into -# the initial regression batch. -# - Stimulus: Drive AXI-Lite writes and reads through the slave-side port into -# a cocotb RAM attached to the master-side port, then assert only the master +# - Sweep: Keep the narrow common-clock wrapper case that proves the cocotb-facing +# bridge topology and stable pass-through behavior, and add one asynchronous +# active-high case so the remote-reset path is covered without pulling the +# less simulator-stable reset permutations into the regression batch. +# - Stimulus: Drive AXI-Lite writes and reads through the slave-side port into a +# cocotb RAM attached to the master-side port, then assert only the master # reset while the slave side remains live in the asynchronous case. # - Checks: Successful transactions must round-trip through the bridge into the -# backing RAM, common-clock reset must restart the path cleanly, and -# post-reset traffic must recover without stale responses. +# backing RAM, common-clock reset must restart the path cleanly, post-reset +# traffic must recover without stale responses, and a transaction rejected +# while the master domain is reset must never execute downstream afterwards. # - Timing: The bench drives both bridge clocks from one lockstep coroutine so -# `COMMON_CLK_G=true` is exercised as a true shared-clock configuration. +# `COMMON_CLK_G=true` is exercised as a true shared-clock configuration. The +# asynchronous case drives mAxiClk from a gateable coroutine so the test can +# hold the master domain still while the slave domain keeps running. import os import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import RisingEdge, Timer, with_timeout from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import ( @@ -39,29 +42,133 @@ ) -class TB: +# cocotb resolves `skip` when the decorator runs, so the configuration has to be +# read at import time rather than from inside a test body. +COMMON_CLK = env_flag("COMMON_CLK_G", default=False) + +# Bound every slave-side transaction so a missing fail-fast response is reported +# as a test failure instead of hanging the regression. +TXN_TIMEOUT_US = 20 + +# Distinct addresses keep the baseline, rejected, and recovery accesses from +# aliasing each other in the backing RAM. +BASELINE_ADDR = 0x040 +REJECTED_WRITE_ADDR = 0x044 +REJECTED_READ_ADDR = 0x048 +RECOVERY_ADDR = 0x04C + + +class GatedClock: + """Free-running clock that the test can stop and restart. + + `cocotb.clock.Clock` cannot be paused, and the remote-reset scenario needs + mAxiClk held low while sAxiClk keeps running. + """ + + def __init__(self, signal, period_ns): + self.signal = signal + self.half_period_ns = period_ns / 2 + self.enabled = True + signal.setimmediatevalue(0) + cocotb.start_soon(self._drive()) + + async def _drive(self): + while True: + await Timer(self.half_period_ns, unit="ns") + if not self.enabled: + self.signal.value = 0 + continue + self.signal.value = 1 + await Timer(self.half_period_ns, unit="ns") + self.signal.value = 0 + + def stop(self): + self.enabled = False + self.signal.value = 0 + + def start(self): + self.enabled = True + + +class SourcePortMonitor: + """Counts handshakes on the slave-side AXI-Lite port. + + The bridge must never present a response that the slave side did not ask + for, so the test compares accepted requests against completed responses. + """ + def __init__(self, dut): self.dut = dut - self.common_clk = env_flag("COMMON_CLK_G", default=False) + self.counts = {"AR": 0, "R": 0, "AW": 0, "W": 0, "B": 0} + cocotb.start_soon(self._run()) + + @staticmethod + def _high(signal) -> bool: + try: + return int(signal.value) == 1 + except ValueError: + return False + + def _handshake(self, valid, ready) -> bool: + return self._high(valid) and self._high(ready) + + async def _run(self): + dut = self.dut + channels = ( + ("AR", dut.S_AXI_ARVALID, dut.S_AXI_ARREADY), + ("R", dut.S_AXI_RVALID, dut.S_AXI_RREADY), + ("AW", dut.S_AXI_AWVALID, dut.S_AXI_AWREADY), + ("W", dut.S_AXI_WVALID, dut.S_AXI_WREADY), + ("B", dut.S_AXI_BVALID, dut.S_AXI_BREADY), + ) + while True: + # Sample at the clock edge, before combinational logic reacts to it. + await RisingEdge(dut.sAxiClk) + for name, valid, ready in channels: + if self._handshake(valid, ready): + self.counts[name] += 1 + + +class TB: + def __init__(self, dut, drive_master=True): + self.dut = dut + self.common_clk = COMMON_CLK self.pipe_stages = int(os.environ["PIPE_STAGES_G"]) self.reset_active = env_sl("RST_POLARITY_G", default=1) + self.m_clk = None if self.common_clk: start_lockstep_clocks(dut.sAxiClk, dut.mAxiClk, period_ns=6.0) else: cocotb.start_soon(Clock(dut.sAxiClk, 8.0, unit="ns").start()) - cocotb.start_soon(Clock(dut.mAxiClk, 5.0, unit="ns").start()) + # Gateable so the remote-reset test can hold the master domain still. + self.m_clk = GatedClock(dut.mAxiClk, 5.0) dut.sAxiClkRst.setimmediatevalue(self.reset_active_value()) dut.mAxiClkRst.setimmediatevalue(self.reset_active_value()) - self.axil = AxiLiteMaster( - bus=AxiLiteBus.from_prefix(dut, "S_AXI"), - clock=dut.sAxiClk, - reset=dut.sAxiClkRst, - reset_active_level=bool(self.reset_active), - ) + if drive_master: + self.axil = AxiLiteMaster( + bus=AxiLiteBus.from_prefix(dut, "S_AXI"), + clock=dut.sAxiClk, + reset=dut.sAxiClkRst, + reset_active_level=bool(self.reset_active), + ) + else: + # Channel level tests drive the slave side port by hand, so the + # cocotbext master must not be driving the same signals. + self.axil = None + for signal in ( + dut.S_AXI_AWADDR, dut.S_AXI_AWPROT, dut.S_AXI_AWVALID, + dut.S_AXI_WDATA, dut.S_AXI_WSTRB, dut.S_AXI_WVALID, + dut.S_AXI_BREADY, + dut.S_AXI_ARADDR, dut.S_AXI_ARPROT, dut.S_AXI_ARVALID, + dut.S_AXI_RREADY, + ): + signal.setimmediatevalue(0) + self.slave = SimpleAxiLiteSlave(dut, self.reset_active) + self.source = SourcePortMonitor(dut) def reset_active_value(self) -> int: return self.reset_active @@ -82,6 +189,54 @@ async def m_cycle(self, count=1): await RisingEdge(self.dut.mAxiClk) await self.settle() + async def write(self, addr, payload): + # Every slave-side access is bounded; a bridge that never answers is a + # failure, not a reason to stall the regression. + return await with_timeout( + self.axil.write(addr, payload), TXN_TIMEOUT_US, "us" + ) + + async def read(self, addr, length): + return await with_timeout( + self.axil.read(addr, length), TXN_TIMEOUT_US, "us" + ) + + async def drive_handshake(self, valid, ready, what, limit=64): + # Hold valid until the edge where ready is also high, sampling ready + # before the clock edge so the check matches the transfer itself. + valid.value = 1 + for _ in range(limit): + await RisingEdge(self.dut.sAxiClk) + accepted = int(ready.value) == 1 + await self.settle() + if accepted: + valid.value = 0 + return + valid.value = 0 + raise AssertionError(f"{what} was never accepted") + + async def await_high(self, signal, what, limit=64): + for _ in range(limit): + await self.s_cycle() + if int(signal.value) == 1: + return + raise AssertionError(f"{what} never asserted") + + async def consume(self, valid, ready, what, limit=128): + # Mirror of drive_handshake for the response direction: raise ready until + # the edge where valid is also high, then drop it again. + ready.value = 1 + for _ in range(limit): + await RisingEdge(self.dut.sAxiClk) + taken = int(valid.value) == 1 + await self.settle() + if taken: + ready.value = 0 + await self.settle() + return + ready.value = 0 + raise AssertionError(f"{what} was never returned") + async def reset(self): # Hold both domains in reset together so the bridge and RAM start from # a known empty state before each scenario. @@ -108,6 +263,9 @@ def __init__(self, dut, reset_active): self.dut = dut self.reset_active = reset_active self.mem = {} + # Ordered record of everything this slave accepted, so the test can + # prove a rejected request never reached the far side of the bridge. + self.handshakes = [] dut.M_AXI_AWREADY.setimmediatevalue(0) dut.M_AXI_WREADY.setimmediatevalue(0) @@ -127,6 +285,9 @@ def in_reset(self) -> bool: except ValueError: return True + def addresses_seen(self, channel) -> list: + return [value for kind, value in self.handshakes if kind == channel] + async def cycle(self, count=1): for _ in range(count): await RisingEdge(self.dut.mAxiClk) @@ -154,6 +315,7 @@ async def _run_write(self): self.dut.M_AXI_AWREADY.value = 1 await self.cycle(1) self.dut.M_AXI_AWREADY.value = 0 + self.handshakes.append(("AW", awaddr)) while not int(self.dut.M_AXI_WVALID.value): await self._wait_while_reset() @@ -173,6 +335,7 @@ async def _run_write(self): self.dut.M_AXI_WREADY.value = 1 await self.cycle(1) self.dut.M_AXI_WREADY.value = 0 + self.handshakes.append(("W", wdata)) self.dut.M_AXI_BRESP.value = int(AxiResp.OKAY) self.dut.M_AXI_BVALID.value = 1 @@ -196,6 +359,7 @@ async def _run_read(self): self.dut.M_AXI_ARREADY.value = 1 await self.cycle(1) self.dut.M_AXI_ARREADY.value = 0 + self.handshakes.append(("AR", araddr)) self.dut.M_AXI_RDATA.value = self.mem.get(araddr, 0) self.dut.M_AXI_RRESP.value = int(AxiResp.OKAY) @@ -222,11 +386,11 @@ async def bridge_round_trip_test(dut): # Sweep a few aligned accesses so the test proves the slave-side bus can # drive data through the bridge into the master-side backing RAM. for addr, payload in transactions: - wr_txn = await tb.axil.write(addr, payload) + wr_txn = await tb.write(addr, payload) assert wr_txn.resp == AxiResp.OKAY assert tb.slave.mem[addr].to_bytes(4, "little")[: len(payload)] == payload - rd_txn = await tb.axil.read(addr, len(payload)) + rd_txn = await tb.read(addr, len(payload)) assert rd_txn.resp == AxiResp.OKAY assert rd_txn.data == payload @@ -237,9 +401,9 @@ async def reset_behavior_test(dut): await tb.reset() baseline = b"\x5A\xA5\xC3\x3C" - wr_txn = await tb.axil.write(0x020, baseline) + wr_txn = await tb.write(0x020, baseline) assert wr_txn.resp == AxiResp.OKAY - rd_txn = await tb.axil.read(0x020, len(baseline)) + rd_txn = await tb.read(0x020, len(baseline)) assert rd_txn.resp == AxiResp.OKAY assert rd_txn.data == baseline @@ -253,14 +417,557 @@ async def reset_behavior_test(dut): await tb.s_cycle(3) tb.dut.sAxiClkRst.value = self_release tb.dut.mAxiClkRst.value = self_release - await tb.s_cycle(3) + # Both resets are released together here, so in the asynchronous case each + # one still has to cross into the opposite domain before the bridge stops + # reporting the remote side as reset. Wait out that release on both clocks, + # otherwise the next access is legitimately answered with AXI_ERROR_RESP_G. + await tb.s_cycle(16) + await tb.m_cycle(16) recovery = b"\x89\x67\x45\x23" - wr_txn = await tb.axil.write(0x024, recovery) + wr_txn = await tb.write(0x024, recovery) + assert wr_txn.resp == AxiResp.OKAY + rd_txn = await tb.read(0x024, len(recovery)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == recovery + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_ghost_test(dut): + """A transaction rejected while the remote domain is reset must never run. + + `COMMON_CLK_G=true` reduces the bridge to direct pass-through with no request + FIFOs, so this scenario only exists in the asynchronous configuration. + """ + tb = TB(dut) + await tb.reset() + + # Prove the bridge is healthy before the fault is injected. + baseline = b"\x01\x02\x03\x04" + wr_txn = await tb.write(BASELINE_ADDR, baseline) assert wr_txn.resp == AxiResp.OKAY - rd_txn = await tb.axil.read(0x024, len(recovery)) + rd_txn = await tb.read(BASELINE_ADDR, len(baseline)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == baseline + + # Let the downstream slave model return to its idle state before its clock + # is taken away. + await tb.m_cycle(4) + + # Stop the master clock, then assert only the master reset. The slave domain + # keeps running, so the bridge has to fail these accesses locally. + tb.m_clk.stop() + tb.dut.mAxiClkRst.value = tb.reset_active_value() + + # Give mAxiClkRst time to synchronize into the slave domain. + await tb.s_cycle(16) + + downstream_before = list(tb.slave.handshakes) + mem_before = dict(tb.slave.mem) + + # Both accesses must fail fast, inside the bounded transaction timeout. + rejected = b"\xDE\xAD\xBE\xEF" + wr_txn = await tb.write(REJECTED_WRITE_ADDR, rejected) + assert wr_txn.resp == AxiResp.SLVERR, ( + f"write during remote reset returned {wr_txn.resp!r}, expected SLVERR" + ) + rd_txn = await tb.read(REJECTED_READ_ADDR, 4) + assert rd_txn.resp == AxiResp.SLVERR, ( + f"read during remote reset returned {rd_txn.resp!r}, expected SLVERR" + ) + + # Nothing can have reached the downstream slave yet: its clock is stopped. + assert tb.slave.handshakes == downstream_before, ( + "downstream slave saw activity while mAxiClk was stopped" + ) + assert tb.slave.mem == mem_before, ( + "downstream memory changed while mAxiClk was stopped" + ) + + # Restart the master clock while the master reset is still asserted. + tb.m_clk.start() + await tb.m_cycle(8) + + # Release the master reset and let both sides finish coming out of reset. + tb.dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + # The rejected requests must not have been replayed downstream. + replayed = [ + entry + for entry in tb.slave.handshakes[len(downstream_before):] + if entry in (("AW", REJECTED_WRITE_ADDR), ("AR", REJECTED_READ_ADDR)) + ] + assert not replayed, ( + f"request rejected with SLVERR was replayed downstream after recovery: {replayed}" + ) + assert REJECTED_WRITE_ADDR not in tb.slave.mem, ( + "write rejected with SLVERR modified downstream memory after recovery" + ) + assert tb.slave.mem == mem_before, ( + "downstream memory changed after recovery without a new transaction" + ) + + # A fresh access must still work, and must not consume a stale response left + # over from the rejected pair. + recovery = b"\x0F\x1E\x2D\x3C" + wr_txn = await tb.write(RECOVERY_ADDR, recovery) + assert wr_txn.resp == AxiResp.OKAY + rd_txn = await tb.read(RECOVERY_ADDR, len(recovery)) assert rd_txn.resp == AxiResp.OKAY assert rd_txn.data == recovery + assert tb.slave.mem[RECOVERY_ADDR].to_bytes(4, "little") == recovery + + # Every response the bridge produced has to map to a request it accepted. + counts = tb.source.counts + assert counts["R"] == counts["AR"], ( + f"bridge returned {counts['R']} read responses for {counts['AR']} accepted " + "read requests" + ) + assert counts["B"] == counts["AW"], ( + f"bridge returned {counts['B']} write responses for {counts['AW']} accepted " + "write addresses" + ) + assert counts["W"] == counts["AW"], ( + f"bridge accepted {counts['W']} write data beats for {counts['AW']} accepted " + "write addresses" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_write_order_test(dut): + """A local write response must wait for both AW and W, arriving separately. + + The bridge carries the write address and the write data in separate FIFOs, so + the error response has to be paired explicitly instead of being asserted as + soon as the remote domain resets. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Hold the remote domain still and in reset. + tb.m_clk.stop() + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # Present the write address on its own. + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW during remote reset" + ) + + # No write data has been accepted yet, so there must be no write response. + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_BVALID.value) == 0, ( + "write response asserted before any write data was accepted" + ) + + # Now present the write data. + dut.S_AXI_WDATA.value = 0xA5A5A5A5 + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W during remote reset" + ) + + # The paired response must appear, and must carry the error code. + await tb.await_high(dut.S_AXI_BVALID, "write response during remote reset") + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.SLVERR), ( + f"write response during remote reset carried {int(dut.S_AXI_BRESP.value)}, " + f"expected {int(AxiResp.SLVERR)}" + ) + + # Accept it, then confirm a single write produced a single response. + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_BVALID.value) == 0, ( + "write response repeated for a single accepted write" + ) + + # The same pairing rule applies to reads: no response without an accepted AR. + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "read response asserted with no read address accepted" + ) + + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + await tb.drive_handshake( + dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "AR during remote reset" + ) + await tb.await_high(dut.S_AXI_RVALID, "read response during remote reset") + assert int(dut.S_AXI_RRESP.value) == int(AxiResp.SLVERR) + + dut.S_AXI_RREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_RREADY.value = 0 + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "read response repeated for a single accepted read" + ) + + # Nothing may have reached the master side, and nothing may be replayed once + # the remote domain recovers. + assert tb.slave.handshakes == [], ( + f"master side saw activity while held in reset: {tb.slave.handshakes}" + ) + + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + assert tb.slave.handshakes == [], ( + f"rejected request replayed downstream after recovery: {tb.slave.handshakes}" + ) + assert tb.slave.mem == {}, ( + f"rejected write reached downstream memory: {tb.slave.mem}" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_inflight_flush_test(dut): + """A request queued before the remote reset must not survive it. + + The request FIFOs are written from the slave domain but drained from the + master domain, so a transaction can already be sitting in them when + mAxiClkRst asserts. Gating new requests is not enough; the queued one has to + be discarded rather than replayed once the remote domain recovers. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Freeze the master domain while its reset is still released, so the bridge + # accepts and queues the write but cannot forward it yet. + tb.m_clk.stop() + await tb.s_cycle(4) + + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW before remote reset" + ) + dut.S_AXI_WDATA.value = 0xDEADBEEF + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W before remote reset" + ) + + # The write is queued and still unanswered, and cannot have reached the far + # side because that clock is stopped. + assert int(dut.S_AXI_BVALID.value) == 0, ( + "write answered while the master domain was still expected to handle it" + ) + assert tb.slave.handshakes == [], ( + f"master side saw activity with its clock stopped: {tb.slave.handshakes}" + ) + + # Now reset the remote domain underneath the queued write. + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # The bridge still owes a response for it, and it must be the error response. + await tb.await_high(dut.S_AXI_BVALID, "write response after remote reset") + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.SLVERR), ( + f"queued write answered with {int(dut.S_AXI_BRESP.value)}, " + f"expected {int(AxiResp.SLVERR)}" + ) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + # Recover and confirm the queued write was discarded, not replayed. + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + assert tb.slave.handshakes == [], ( + f"write queued before the remote reset was replayed downstream: " + f"{tb.slave.handshakes}" + ) + assert tb.slave.mem == {}, ( + f"write queued before the remote reset reached memory: {tb.slave.mem}" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_orphan_pairing_test(dut): + """An orphaned write address must not pair with a later write data beat. + + The write address and the write data cross the bridge in separate FIFOs, so a + write that straddles the remote reset can leave an address queued with no + data behind it. If that address is not discarded, the next write's data is + committed to the wrong location. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Queue the address only, with the master domain frozen. + tb.m_clk.stop() + await tb.s_cycle(4) + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "orphan AW") + + # Reset the remote domain with that address queued and no data sent yet. + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # Supply the data now. The bridge answers locally and both halves are dropped. + dut.S_AXI_WDATA.value = 0xDEADBEEF + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W during remote reset" + ) + await tb.await_high(dut.S_AXI_BVALID, "write response during remote reset") + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.SLVERR) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + # Recover. + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + # Issue a fresh write to a different address. + dut.S_AXI_AWADDR.value = RECOVERY_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "recovery AW") + dut.S_AXI_WDATA.value = 0x0F1E2D3C + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake(dut.S_AXI_WVALID, dut.S_AXI_WREADY, "recovery W") + + # Let the master side act before checking, so a misdirected write is reported + # as exactly that rather than as a missing response. + await tb.m_cycle(64) + + assert ("AW", REJECTED_WRITE_ADDR) not in tb.slave.handshakes, ( + f"abandoned write address reached the master side: {tb.slave.handshakes}" + ) + assert REJECTED_WRITE_ADDR not in tb.slave.mem, ( + f"recovery write data was committed to the abandoned address: {tb.slave.mem}" + ) + assert tb.slave.mem == {RECOVERY_ADDR: 0x0F1E2D3C}, ( + f"recovery write did not land correctly: {tb.slave.mem}" + ) + + await tb.await_high(dut.S_AXI_BVALID, "recovery write response", limit=128) + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.OKAY) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + +@cocotb.test(skip=COMMON_CLK) +async def source_reset_stale_response_test(dut): + """A response queued when the slave domain resets must be discarded. + + The response FIFOs are written from the master domain, so a completed + response can still be queued when sAxiClkRst asserts. If it survives the + reset, the next read after recovery consumes it and returns another + address's data. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Seed two addresses with distinct data. + seeds = ((BASELINE_ADDR, 0x11111111), (RECOVERY_ADDR, 0x22222222)) + for addr, data in seeds: + dut.S_AXI_AWADDR.value = addr + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, f"seed AW {addr:#05x}" + ) + dut.S_AXI_WDATA.value = data + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, f"seed W {addr:#05x}" + ) + await tb.await_high(dut.S_AXI_BVALID, f"seed B {addr:#05x}", limit=128) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + # Read one address but never take the response, so it sits in the response + # FIFO on the slave side. + dut.S_AXI_ARADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "stale AR") + await tb.await_high(dut.S_AXI_RVALID, "stale read response", limit=128) + + # Reset the slave domain with that response still queued. + dut.sAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(8) + dut.sAxiClkRst.value = tb.reset_inactive_value() + await tb.s_cycle(16) + await tb.m_cycle(16) + + # A fresh read must return its own data, not the abandoned response. + dut.S_AXI_ARADDR.value = RECOVERY_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "recovery AR") + await tb.await_high(dut.S_AXI_RVALID, "recovery read response", limit=128) + assert int(dut.S_AXI_RDATA.value) == 0x22222222, ( + f"read after a slave-domain reset returned " + f"{int(dut.S_AXI_RDATA.value):#010x}, expected 0x22222222 for its own " + "address" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def source_reset_clears_outstanding_test(dut): + """A slave-domain reset must clear the bridge's outstanding transaction state. + + The outstanding counts decide whether the bridge owes a local error response. + If a slave-domain reset leaves them stale, the next remote reset answers a + transaction the slave side already abandoned, which is a response with no + request behind it. + + This is the case that actually exercises the reset path of the registered + logic, through the sequential process when RST_ASYNC_G is true and through + the combinational next-state path when it is false. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Freeze the master domain so no real response can ever be produced, then + # leave a read accepted and unanswered. + tb.m_clk.stop() + await tb.s_cycle(4) + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + await tb.drive_handshake( + dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "AR before slave reset" + ) + assert int(dut.S_AXI_RVALID.value) == 0, ( + "read answered while the master domain was frozen" + ) + + # Reset the slave domain. That abandons the read, so the bridge no longer + # owes anything for it. + dut.sAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(8) + dut.sAxiClkRst.value = tb.reset_inactive_value() + await tb.s_cycle(16) + + # Now reset the remote domain, which puts the bridge into local-answer mode. + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # With the outstanding state cleared there is nothing to answer. + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "bridge answered a read that was abandoned by the slave-domain reset" + ) + assert int(dut.S_AXI_BVALID.value) == 0, ( + "bridge produced a write response with no write outstanding" + ) + + # And nothing may reach the master side once it recovers. + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + assert tb.slave.handshakes == [], ( + f"abandoned read reached the master side after recovery: " + f"{tb.slave.handshakes}" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def single_outstanding_bound_test(dut): + """The bridge allows one transaction per channel in flight, like the crossbar. + + AxiLiteCrossbar does not release a slave slot until the response completes, + so AxiLiteAsync matches that bound. It enforces the bound with its own ready + outputs rather than trusting the master to honour it, which is what lets the + remote-reset responder be a single flag per channel and still answer exactly + once per accepted request. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Normal operation: a second read must not be accepted while the first is + # still unanswered. + dut.S_AXI_RREADY.value = 0 + dut.S_AXI_ARADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "first AR") + + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + dut.S_AXI_ARVALID.value = 1 + for _ in range(32): + await tb.s_cycle() + assert int(dut.S_AXI_ARREADY.value) == 0, ( + "bridge accepted a second read while the first was still unanswered" + ) + dut.S_AXI_ARVALID.value = 0 + await tb.settle() + + # Answer the first read, after which the next one is accepted normally. + await tb.await_high(dut.S_AXI_RVALID, "first read response", limit=128) + await tb.consume(dut.S_AXI_RVALID, dut.S_AXI_RREADY, "first read response") + + dut.S_AXI_ARADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "second AR") + await tb.await_high(dut.S_AXI_RVALID, "second read response", limit=128) + await tb.consume(dut.S_AXI_RVALID, dut.S_AXI_RREADY, "second read response") + + # The same bound applies to writes: no second address while one is pending. + dut.S_AXI_AWADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "first AW") + dut.S_AXI_AWADDR.value = RECOVERY_ADDR + dut.S_AXI_AWVALID.value = 1 + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_AWREADY.value) == 0, ( + "bridge accepted a second write address while one was still pending" + ) + dut.S_AXI_AWVALID.value = 0 + await tb.settle() + dut.S_AXI_WDATA.value = 0x5A5A5A5A + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake(dut.S_AXI_WVALID, dut.S_AXI_WREADY, "first W") + await tb.await_high(dut.S_AXI_BVALID, "first write response", limit=128) + await tb.consume(dut.S_AXI_BVALID, dut.S_AXI_BREADY, "first write response") + + # Error mode: the same bound holds, and the accepted read is answered once. + tb.m_clk.stop() + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + await tb.drive_handshake( + dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "AR during remote reset" + ) + + dut.S_AXI_ARVALID.value = 1 + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_ARREADY.value) == 0, ( + "bridge accepted a second read during remote reset while the first " + "was still unanswered" + ) + dut.S_AXI_ARVALID.value = 0 + await tb.settle() + + await tb.await_high(dut.S_AXI_RVALID, "error response during remote reset") + assert int(dut.S_AXI_RRESP.value) == int(AxiResp.SLVERR), ( + f"remote-reset read answered with {int(dut.S_AXI_RRESP.value)}, " + f"expected {int(AxiResp.SLVERR)}" + ) + await tb.consume(dut.S_AXI_RVALID, dut.S_AXI_RREADY, "error response") + + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "error response repeated for a single accepted read" + ) PARAMETER_SWEEP = [ @@ -272,6 +979,45 @@ async def reset_behavior_test(dut): RST_ASYNC_G="false", RST_POLARITY_G="'1'", ), + parameter_case( + "async_active_high", + COMMON_CLK_G="false", + PIPE_STAGES_G="0", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="false", + RST_POLARITY_G="'1'", + ), + # Active LOW covers the reset-polarity handling in the bridge; the remote + # reset comparisons are only exercised for one sense per case. + parameter_case( + "async_active_low", + COMMON_CLK_G="false", + PIPE_STAGES_G="0", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="false", + RST_POLARITY_G="'0'", + ), + # Asynchronous reset reaches the registered logic through the sequential + # process instead of the combinational next-state path, so it needs its own + # case to be executed at all. + parameter_case( + "async_rst_async", + COMMON_CLK_G="false", + PIPE_STAGES_G="0", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="true", + RST_POLARITY_G="'1'", + ), + # A non-zero PIPE_STAGES_G adds output registers to every channel FIFO and + # widens the worst-case outstanding transaction count. + parameter_case( + "async_pipelined", + COMMON_CLK_G="false", + PIPE_STAGES_G="2", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="false", + RST_POLARITY_G="'1'", + ), ] From 31dbced5ba8a44b582cf87084c4f934e08026480 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Fri, 21 Aug 2026 13:06:30 -0700 Subject: [PATCH 15/57] docs(plans): add AxiLiteAsync remote reset notes Capture the goal, the defects found, the design decisions and their rejected alternatives, the measured Vivado resource and timing comparison, the defect-to-test mapping, the validation run, and the remaining gaps. --- .../axilite-async-remote-reset/README.md | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 docs/plans/axilite-async-remote-reset/README.md diff --git a/docs/plans/axilite-async-remote-reset/README.md b/docs/plans/axilite-async-remote-reset/README.md new file mode 100644 index 0000000000..42a17daa5c --- /dev/null +++ b/docs/plans/axilite-async-remote-reset/README.md @@ -0,0 +1,142 @@ +# AxiLiteAsync Remote Reset Recovery + +## Goal And Scope + +Stop `AxiLiteAsync` replaying transactions it already rejected. With +`COMMON_CLK_G = false`, an access issued while the master domain was reset was +answered with `AXI_ERROR_RESP_G` on the slave side but queued at the same time, +so it executed once `mAxiClk` and `mAxiClkRst` recovered. + +Limited to `axi/axi-lite/rtl/AxiLiteAsync.vhd` and its cocotb regression. No port +or generic changes. GitHub issue #1467. Requested constraints: minimal added +logic, no impact on timing closure. + +## Current Status + +Complete. Every defect below has a test that fails when only its own fix is +reverted, with one documented exception. + +## Defects + +All in the `GEN_ASYNC` branch. + +1. FIFO write enables were not gated by the remote reset, so a rejected request + was queued anyway. +2. Request FIFOs were reset only by `s2mRst`, which carries `sAxiClkRst`, so a + request queued before `mAxiClkRst` also survived. Mirror image: response FIFOs + were not flushed by a slave-domain reset. +3. `AW` and `W` cross in separate FIFOs, so a write straddling the reset left an + orphaned address that misdirected the next write's data. +4. `RVALID` and `BVALID` were forced to `'1'`, so a response could appear with no + matching request. +5. Reset comparisons were written against `'0'`, inverting fail-fast when + `RST_POLARITY_G = '0'`. + +## Design Decisions + +**One shared `fifoRst` for all five FIFOs**, asserted while either domain is in +reset or while draining an abandoned transaction. Contract: the bridge is empty +whenever either side is reset. Covers defects 2 and 3 and the mirror image, and +collapses two reset nets into one. Works with `mAxiClk` stopped because +`FifoAsync` treats `rst` as asynchronous and resynchronizes it into both domains. + +**One transaction in flight per channel, matching `AxiLiteCrossbar`** (maintainer +decision). The bridge does not need the bound: measured on unmodified RTL it +accepts 4 outstanding reads and returns all 4 in order. But `AxiLiteCrossbar` +does not release a slave slot until `rvalid and rready`, and `AxiLiteMaster` runs +one at a time, so no SURF topology exceeds one. + +Tracking must exist regardless, because `fifoRst` discards in-flight requests +that still owe a response. Matching the crossbar makes that one flag per channel +instead of a counter, which is the whole cost of the fix. The bound is therefore +enforced by the ready outputs, in normal operation as well as during reset: +measured, a flag responder without the bound answers 1 of 4 abandoned reads and +hangs the master. Consequence, a master that pipelines is throttled to one +transaction, which matters because `AxiLiteAsyncIpIntegrator` exposes the slave +port to non-SURF masters. + +**Rejected: size tracking to FIFO capacity.** Three 7-bit counters, correct for +any AXI4-Lite master and preserving throughput, but +45 LUTs and +22 flip-flops +on a primitive used in more than 40 modules. + +Reset polarity is normalized once into active-high terms. + +## Resource And Timing + +Vivado 2025.2, out-of-context synthesis and implementation, +`xcku040-ffva1156-2-e`, `sAxiClk` at 2.5 ns and `mAxiClk` at 2.0 ns declared +asynchronous. + +| | Baseline | Counters (rejected) | Bounded (shipped) | +| --- | --- | --- | --- | +| CLB LUTs | 312 | 357 | 309 | +| CLB registers | 473 | 495 | 477 | +| WNS `sAxiClk` | +0.485 ns | +0.397 ns | +0.527 ns | +| WNS `mAxiClk` | +0.293 ns | +0.215 ns | +0.184 ns | + +Shipped version is 3 LUTs smaller than the unfixed baseline for 4 flip-flops. All +variants meet timing and no handshake output gained a logic level. The `mAxiClk` +worst path reports 0 logic levels, so its roughly 100 ps movement is routing +noise at this scale. + +## Files And Tests + +- `axi/axi-lite/rtl/AxiLiteAsync.vhd` +- `tests/axi/axi_lite/test_AxiLiteAsync.py` + +`AxiLiteAsyncIpIntegrator.vhd` needed no change; it already exposes both clocks +and resets and passes real response codes through. + +Sweep: 5 cases (common-clock, plus asynchronous active-high, active-low, +`RST_ASYNC_G = true`, `PIPE_STAGES_G = 2`), 9 cocotb tests each. + +| Defect or behaviour | Test | +| --- | --- | +| 2 | `remote_reset_inflight_flush_test` | +| 3 | `remote_reset_orphan_pairing_test` | +| 4 | `reset_behavior_test`, `remote_reset_write_order_test` | +| 5 | `async_active_low` case | +| Rejected request replayed | `remote_reset_ghost_test` | +| Mirror image of 2 | `source_reset_stale_response_test` | +| Reset of the new registered state | `source_reset_clears_outstanding_test` | +| One-transaction bound | `single_outstanding_bound_test` | + +`source_reset_clears_outstanding_test` is what gives the reset cases teeth. +Adding a `RST_ASYNC_G = true` case alone compiled the asynchronous reset branch +without checking it, and disabling that branch still passed. With this test, +breaking only the sequential asynchronous reset fails just that case and breaking +only the combinational synchronous reset fails just the others. + +**Exception:** defect 1 is not independently covered. Removing the write-enable +gating passes the whole regression, because `fifoRst` holds the FIFOs in reset +for the entire local-answer window so the write pointer cannot advance. Kept +anyway: one LUT input, it states the invariant explicitly instead of depending on +`FifoAsync` ignoring `wr_en` while reset, and it stays correct if `fifoRst` is +ever narrowed. + +## Validation Run + +- Reproduced first: unmodified RTL replayed the rejected write and read + downstream after recovery. +- `pytest tests/axi/axi_lite/test_AxiLiteAsync.py`, 5 cases. +- `pytest -n auto tests/`, 1017 passed and 29 skipped. Run in full because + `AxiLiteAsync` is instantiated in `axi`, `dsp` and every GigE and 10GigE core. +- `make MODULES="$PWD" analysis` clean library wide. +- `vsg -c vsg-linter.yml` clean; `flake8 --count python/ scripts/ tests/` 0. +- Vivado numbers above: all SURF sources into library `surf` and ruckus into + `ruckus`, testbenches excluded, `synth_design -mode out_of_context` then + `opt_design`, `place_design`, `phys_opt_design`, `route_design`. + +Do not run `pytest -n auto` while Vivado implementation is running. The +`tests/simlink` tests are timing sensitive and fail from CPU starvation. + +## Open Risks And Next Steps + +- The bound is now enforced, so a previously pipelining external master is + throttled. Correctness unaffected, throughput not. +- Synthesis numbers are out-of-context on one part; absolute slack will differ + inside a real design. +- `NUM_ADDR_BITS_G` exercised only at 12 in simulation and 32 in synthesis. +- With one transaction in flight the 16-deep FIFOs are oversized and their 96 + distributed-RAM LUTs now dominate the module. Shrinking `FIFO_ADDR_WIDTH_C` is + the obvious next saving, left out as an optimisation rather than part of the fix. From 20116083fedc8a37477532ee3ac92e225f75063b Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Fri, 21 Aug 2026 17:10:58 -0700 Subject: [PATCH 16/57] fix(AxiLiteAsync): harden reset CDC and handshakes --- axi/axi-lite/rtl/AxiLiteAsync.vhd | 68 ++++++++++-------- .../axilite-async-remote-reset/README.md | 35 ++++++++-- tests/axi/axi_lite/test_AxiLiteAsync.py | 69 +++++++++++++++++-- 3 files changed, 131 insertions(+), 41 deletions(-) diff --git a/axi/axi-lite/rtl/AxiLiteAsync.vhd b/axi/axi-lite/rtl/AxiLiteAsync.vhd index 01d2ed15c7..507ae675fa 100755 --- a/axi/axi-lite/rtl/AxiLiteAsync.vhd +++ b/axi/axi-lite/rtl/AxiLiteAsync.vhd @@ -51,7 +51,7 @@ end AxiLiteAsync; architecture STRUCTURE of AxiLiteAsync is - signal s2mRst : sl; -- Slave rst sync'd to master clk + signal sRst : sl; -- Slave rst sync'd to slave clk signal m2sRst : sl; -- Master rst sync'd to slave clk signal readSlaveToMastDin : slv(NUM_ADDR_BITS_G+2 downto 0); @@ -93,12 +93,12 @@ architecture STRUCTURE of AxiLiteAsync is constant FIFO_ADDR_WIDTH_C : positive := 4; -- Reset terms normalized to active HIGH, independent of RST_POLARITY_G - signal s2mRstActive : sl; signal m2sRstActive : sl; signal mAxiRstActive : sl; - -- Asserted while every FIFO in the bridge must be held empty - signal fifoRst : sl; + -- Registered active-HIGH reset request for every FIFO in the bridge + signal fifoRstReq : sl; + signal fifoRst : sl; -- Slave side handshakes, kept local so the error responder can observe them signal sArReady : sl; @@ -136,18 +136,18 @@ begin GEN_ASYNC : if (COMMON_CLK_G = false) generate - -- Synchronize each reset across to the other clock domain - LOC_S2M_RstSync : entity surf.RstSync + -- Synchronize the local reset release before it controls fifoRst + LOC_S_RstSync : entity surf.RstSync generic map ( TPD_G => TPD_G, IN_POLARITY_G => RST_POLARITY_G, - OUT_POLARITY_G => RST_POLARITY_G, - OUT_REG_RST_G => false) + OUT_POLARITY_G => RST_POLARITY_G) port map ( - clk => mAxiClk, + clk => sAxiClk, asyncRst => sAxiClkRst, - syncRst => s2mRst); + syncRst => sRst); + -- Synchronize the remote reset into the slave/control clock domain LOC_M2S_RstSync : entity surf.RstSync generic map ( TPD_G => TPD_G, @@ -159,20 +159,30 @@ begin asyncRst => mAxiClkRst, syncRst => m2sRst); - -- Normalize both synchronized resets to active HIGH so everything below is - -- correct for either RST_POLARITY_G setting - s2mRstActive <= '1' when (s2mRst = RST_POLARITY_G) else '0'; + -- Normalize reset indications to active HIGH m2sRstActive <= '1' when (m2sRst = RST_POLARITY_G) else '0'; mAxiRstActive <= '1' when (mAxiClkRst = RST_POLARITY_G) else '0'; - -- Hold every FIFO empty while either domain is in reset, and while the - -- bridge is still answering transactions it has abandoned. Without this a - -- request accepted before the remote reset stays queued and is replayed - -- downstream once the remote domain recovers, even though the slave side - -- was already told the access failed. FifoAsync resynchronizes this reset - -- into both of its clock domains, so one assertion clears the read and - -- write pointers even while mAxiClk is stopped. - fifoRst <= RST_POLARITY_G when (s2mRstActive = '1') or (m2sRstActive = '1') or (r.errMode = '1') else not RST_POLARITY_G; + -- Build one glitch-free FIFO reset request in the slave/control domain. + -- The local reset asserts it asynchronously, so the FIFOs are reset even + -- if sAxiClk is stopped. The remote reset is synchronized above before it + -- sets this register. Deassertion is synchronous and delayed until error + -- mode has drained every abandoned transaction. FifoAsync then + -- resynchronizes this single registered request into both FIFO domains. + fifoRstReq <= m2sRstActive or r.errMode; + + U_FifoRstReg : entity surf.RegisterVector + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => true, + WIDTH_G => 1, + INIT_G => "1") + port map ( + clk => sAxiClk, -- [in] + rst => sRst, -- [in] + sig_i(0) => fifoRstReq, -- [in] + reg_o(0) => fifoRst); -- [out] -- Transaction tracking and local error responder. -- @@ -263,7 +273,7 @@ begin U_ReadSlaveToMastFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, @@ -306,7 +316,7 @@ begin -- failed would still reach the master side. sArReady <= (not r.rPend) when (r.errMode = '1') else ((not readSlaveToMastFull) and (not r.rPend)); sAxiReadSlave.arready <= sArReady; - readSlaveToMastWrite <= sAxiReadMaster.arvalid and (not readSlaveToMastFull) and (not r.errMode); + readSlaveToMastWrite <= sAxiReadMaster.arvalid and sArReady and (not r.errMode); -- Data Out mAxiReadMaster.arprot <= readSlaveToMastDout(2 downto 0); @@ -329,7 +339,7 @@ begin U_ReadMastToSlaveFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, @@ -389,7 +399,7 @@ begin U_WriteAddrSlaveToMastFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, @@ -430,7 +440,7 @@ begin -- Write control and ready generation sAwReady <= (not r.awPend) when (r.errMode = '1') else ((not writeAddrSlaveToMastFull) and (not r.awPend)); sAxiWriteSlave.awready <= sAwReady; - writeAddrSlaveToMastWrite <= sAxiWriteMaster.awvalid and (not writeAddrSlaveToMastFull) and (not r.errMode); + writeAddrSlaveToMastWrite <= sAxiWriteMaster.awvalid and sAwReady and (not r.errMode); -- Data Out mAxiWriteMaster.awprot <= writeAddrSlaveToMastDout(2 downto 0); @@ -453,7 +463,7 @@ begin U_WriteDataSlaveToMastFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, @@ -494,7 +504,7 @@ begin -- Write control and ready generation sWReady <= (not r.wPend) when (r.errMode = '1') else ((not writeDataSlaveToMastFull) and (not r.wPend)); sAxiWriteSlave.wready <= sWReady; - writeDataSlaveToMastWrite <= sAxiWriteMaster.wvalid and (not writeDataSlaveToMastFull) and (not r.errMode); + writeDataSlaveToMastWrite <= sAxiWriteMaster.wvalid and sWReady and (not r.errMode); -- Data Out mAxiWriteMaster.wstrb <= writeDataSlaveToMastDout(3 downto 0); @@ -512,7 +522,7 @@ begin U_WriteMastToSlaveFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, diff --git a/docs/plans/axilite-async-remote-reset/README.md b/docs/plans/axilite-async-remote-reset/README.md index 42a17daa5c..62cbad11b8 100644 --- a/docs/plans/axilite-async-remote-reset/README.md +++ b/docs/plans/axilite-async-remote-reset/README.md @@ -34,11 +34,23 @@ All in the `GEN_ASYNC` branch. ## Design Decisions -**One shared `fifoRst` for all five FIFOs**, asserted while either domain is in -reset or while draining an abandoned transaction. Contract: the bridge is empty -whenever either side is reset. Covers defects 2 and 3 and the mirror image, and -collapses two reset nets into one. Works with `mAxiClk` stopped because -`FifoAsync` treats `rst` as asynchronous and resynchronizes it into both domains. +**One shared, registered, active-high `fifoRst` for all five FIFOs**, generated +in the `sAxiClk` control domain by a one-bit `RegisterVector`. `sAxiClkRst` +first passes through a local `RstSync`, then asynchronously asserts the +reset-request register; `mAxiClkRst` is synchronized into `sAxiClk` before +entering its synchronous data input. Release is synchronous and held until the +local error responder drains the abandoned transaction. `FifoAsync` then +resynchronizes the single registered request into both FIFO domains. The FIFO +instances use active-high reset internally regardless of the bridge's external +reset polarity, so the register directly drives every reset synchronizer +without an intervening polarity-select LUT. This avoids both combinational +logic and multi-clock fan-in ahead of the synchronizers. + +Clock/reset contract: if either AXI clock is unavailable, its corresponding +reset must remain asserted. The clock must be stable before reset release, and +traffic must remain inactive until synchronized release completes. A reset of +the slave/source domain abandons its outstanding transactions, so they require +no response. **One transaction in flight per channel, matching `AxiLiteCrossbar`** (maintainer decision). The bridge does not need the bound: measured on unmodified RTL it @@ -49,7 +61,8 @@ one at a time, so no SURF topology exceeds one. Tracking must exist regardless, because `fifoRst` discards in-flight requests that still owe a response. Matching the crossbar makes that one flag per channel instead of a counter, which is the whole cost of the fix. The bound is therefore -enforced by the ready outputs, in normal operation as well as during reset: +enforced by the ready outputs and by making every FIFO write conditional on its +source-side handshake, in normal operation as well as during reset: measured, a flag responder without the bound answers 1 of 4 abandoned reads and hangs the master. Consequence, a master that pipelines is throttled to one transaction, which matters because `AxiLiteAsyncIpIntegrator` exposes the slave @@ -114,6 +127,10 @@ anyway: one LUT input, it states the invariant explicitly instead of depending o `FifoAsync` ignoring `wr_en` while reset, and it stays correct if `fifoRst` is ever narrowed. +`single_outstanding_bound_test` also holds a second `AR`, `AW`, and `W` while +the corresponding ready output is low and verifies that none of those +unaccepted beats crosses to the downstream slave. + ## Validation Run - Reproduced first: unmodified RTL replayed the rejected write and read @@ -136,6 +153,12 @@ Do not run `pytest -n auto` while Vivado implementation is running. The throttled. Correctness unaffected, throughput not. - Synthesis numbers are out-of-context on one part; absolute slack will differ inside a real design. +- The resource and timing table predates the registered reset-request review + revision and must be remeasured before using the exact numbers in release + notes. +- Rerun Vivado `report_cdc -details -all_checks_per_endpoint` on the revised + reset topology and confirm that the former FIFO-reset CDC-10/CDC-12 paths are + gone. Vivado is not available in the current validation environment. - `NUM_ADDR_BITS_G` exercised only at 12 in simulation and 32 in synthesis. - With one transaction in flight the 16-deep FIFOs are oversized and their 96 distributed-RAM LUTs now dominate the module. Shrinking `FIFO_ADDR_WIDTH_C` is diff --git a/tests/axi/axi_lite/test_AxiLiteAsync.py b/tests/axi/axi_lite/test_AxiLiteAsync.py index c75c00588c..ea8ed9ae71 100644 --- a/tests/axi/axi_lite/test_AxiLiteAsync.py +++ b/tests/axi/axi_lite/test_AxiLiteAsync.py @@ -9,17 +9,16 @@ ############################################################################## # Test methodology: -# - Sweep: Keep the narrow common-clock wrapper case that proves the cocotb-facing -# bridge topology and stable pass-through behavior, and add one asynchronous -# active-high case so the remote-reset path is covered without pulling the -# less simulator-stable reset permutations into the regression batch. +# - Sweep: Cover common-clock pass-through plus asynchronous active-high, +# active-low, asynchronous-reset, and pipelined FIFO configurations. # - Stimulus: Drive AXI-Lite writes and reads through the slave-side port into a # cocotb RAM attached to the master-side port, then assert only the master # reset while the slave side remains live in the asynchronous case. # - Checks: Successful transactions must round-trip through the bridge into the # backing RAM, common-clock reset must restart the path cleanly, post-reset -# traffic must recover without stale responses, and a transaction rejected -# while the master domain is reset must never execute downstream afterwards. +# traffic must recover without stale responses, requests held while READY is +# low must not cross the bridge, and a transaction rejected while the master +# domain is reset must never execute downstream afterwards. # - Timing: The bench drives both bridge clocks from one lockstep coroutine so # `COMMON_CLK_G=true` is exercised as a true shared-clock configuration. The # asynchronous case drives mAxiClk from a gateable coroutine so the test can @@ -908,6 +907,10 @@ async def single_outstanding_bound_test(dut): ) dut.S_AXI_ARVALID.value = 0 await tb.settle() + assert REJECTED_READ_ADDR not in tb.slave.addresses_seen("AR"), ( + "read held while ARREADY was low crossed to the master side: " + f"{tb.slave.handshakes}" + ) # Answer the first read, after which the next one is accepted normally. await tb.await_high(dut.S_AXI_RVALID, "first read response", limit=128) @@ -936,6 +939,60 @@ async def single_outstanding_bound_test(dut): await tb.await_high(dut.S_AXI_BVALID, "first write response", limit=128) await tb.consume(dut.S_AXI_BVALID, dut.S_AXI_BREADY, "first write response") + # Once the first response releases the downstream slave, none of the address + # beats held above while AWREADY was low may appear there. + await tb.m_cycle(16) + assert RECOVERY_ADDR not in tb.slave.addresses_seen("AW"), ( + "write address held while AWREADY was low crossed to the master side: " + f"{tb.slave.handshakes}" + ) + + # Exercise the same rule on W independently. Queue one accepted data beat + # before its address, then hold a different beat while WREADY is low. + first_wdata = 0x11223344 + blocked_wdata = 0x55667788 + dut.S_AXI_WDATA.value = first_wdata + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake(dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W before AW") + + dut.S_AXI_WDATA.value = blocked_wdata + dut.S_AXI_WVALID.value = 1 + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_WREADY.value) == 0, ( + "bridge accepted a second write data beat while one was still pending" + ) + dut.S_AXI_WVALID.value = 0 + await tb.settle() + + # Complete the first W with its address and consume its response. + dut.S_AXI_AWADDR.value = RECOVERY_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW after W") + await tb.await_high(dut.S_AXI_BVALID, "W-before-AW response", limit=128) + await tb.consume(dut.S_AXI_BVALID, dut.S_AXI_BREADY, "W-before-AW response") + assert tb.slave.mem[RECOVERY_ADDR] == first_wdata + + # A fresh address must wait for a fresh W; an unaccepted data beat left in + # the FIFO would instead pair with this address and modify memory. + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW after blocked W" + ) + await tb.m_cycle(32) + assert REJECTED_WRITE_ADDR not in tb.slave.mem, ( + "write data held while WREADY was low crossed to the master side: " + f"{tb.slave.handshakes}" + ) + + # Finish the legitimate write so the following remote-reset scenario starts + # with no partial transaction in either interface. + dut.S_AXI_WDATA.value = 0x99AABBCC + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "fresh W after blocked W" + ) + await tb.await_high(dut.S_AXI_BVALID, "fresh write response", limit=128) + await tb.consume(dut.S_AXI_BVALID, dut.S_AXI_BREADY, "fresh write response") + # Error mode: the same bound holds, and the accepted read is answered once. tb.m_clk.stop() dut.mAxiClkRst.value = tb.reset_active_value() From 5430277b37d1cbf1f9d2257eb8ea3468489129da Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Sun, 23 Aug 2026 16:24:19 -0700 Subject: [PATCH 17/57] feat(gtx7): add cPllRefClkLostOut and WAIT_TIME_CDRLOCK_G to Gtx7Core Two independent Gtx7Core additions, both backward compatible. cPllRefClkLostOut exposes CPLLREFCLKLOST from the GTXE2_CHANNEL, which the core already consumed internally but never brought out to a caller. WAIT_TIME_CDRLOCK_G decouples the CDR-lock wait from the primitive's simulation speedup. SIM_GTRESET_SPEEDUP_G previously fed both the WAIT_TIME_CDRLOCK_C fabric constant and the GTXE2_CHANNEL SIM_RESET_SPEEDUP attribute, so a simulation could not select the CDR-lock wait independently of the primitive's own reset speedup. The new integer generic defaults to -1, which reproduces the previous derivation exactly and changes no elaborated behavior for any instance that leaves it at the default. Any value of 0 or greater is used directly as the stable-clock cycle count for WAIT_TIME_CDRLOCK_C. SIM_RESET_SPEEDUP continues to consume SIM_GTRESET_SPEEDUP_G unmodified. --- xilinx/7Series/gtx7/rtl/Gtx7Core.vhd | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd index 4a0a48dc26..eede82ad49 100755 --- a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd +++ b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd @@ -27,8 +27,9 @@ entity Gtx7Core is TPD_G : time := 1 ns; -- Sim Generics -- - SIM_GTRESET_SPEEDUP_G : string := "FALSE"; - SIM_VERSION_G : string := "4.0"; + SIM_GTRESET_SPEEDUP_G : string := "FALSE"; + SIM_VERSION_G : string := "4.0"; + WAIT_TIME_CDRLOCK_G : integer := -1; -- -1: use the legacy SIM_GTRESET_SPEEDUP_G derivation; >=0: stable-clock cycle count used directly SIMULATION_G : boolean := false; @@ -157,8 +158,9 @@ entity Gtx7Core is port ( stableClkIn : in sl; -- Freerunning clock needed to drive reset logic - cPllRefClkIn : in sl := '0'; -- Drives CPLL if used - cPllLockOut : out sl; + cPllRefClkIn : in sl := '0'; -- Drives CPLL if used + cPllLockOut : out sl; + cPllRefClkLostOut : out sl; -- CPLLREFCLKLOST from the GTXE2_CHANNEL qPllRefClkIn : in sl := '0'; -- Signals from QPLL if used qPllClkIn : in sl := '0'; @@ -276,7 +278,7 @@ architecture rtl of Gtx7Core is constant RX_DATA_WIDTH_C : integer := getDataWidth(RX_8B10B_EN_G, RX_EXT_DATA_WIDTH_G); constant TX_DATA_WIDTH_C : integer := getDataWidth(TX_8B10B_EN_G, TX_EXT_DATA_WIDTH_G); - constant WAIT_TIME_CDRLOCK_C : integer := ite(SIM_GTRESET_SPEEDUP_G = "TRUE", 16, 65520); + constant WAIT_TIME_CDRLOCK_C : integer := ite(WAIT_TIME_CDRLOCK_G >= 0, WAIT_TIME_CDRLOCK_G, ite(SIM_GTRESET_SPEEDUP_G = "TRUE", 16, 65520)); constant RX_INT_DATAWIDTH_C : integer := (RX_INT_DATA_WIDTH_G/32); constant TX_INT_DATAWIDTH_C : integer := (TX_INT_DATA_WIDTH_G/32); @@ -390,7 +392,8 @@ begin rxOutClkOut <= rxOutClkBufg; - cPllLockOut <= cPllLock; + cPllLockOut <= cPllLock; + cPllRefClkLostOut <= cPllRefClkLost; -------------------------------------------------------------------------------------------------- -- PLL Resets. Driven from TX Rst if both use same PLL From 47d9c457fe9e6fc6a5d79492e424593e8599a5e8 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Sun, 23 Aug 2026 16:24:45 -0700 Subject: [PATCH 18/57] feat(gtx7): resolve odd comma landings in fabric behind RX_ODD_ALIGN_MODE_G Gtx7RxFixedLatPhaseAligner unilaterally forced Gtx7RxRst back to INIT on every odd comma landing, unbounded and unobservable. Add RX_ODD_ALIGN_MODE_G ("RESET" or "BITSLIP") to gate a fabric escape from that relock loop. Under BITSLIP, an odd landed offset i issues i-2 rxSlide pulses, which SLIDE_WAIT_S turns into i-1 actual pulses (always even, so the recovered clock phase is never disturbed), reducing the residue to 1. The aligned word is then presented as a 1-bit-shifted slice of the aligner's own r.last history through two new output ports, rxDataAligned and rxDataAlignedSel, muxed onto Gtx7Core's rxDataOut only when RX_ODD_ALIGN_MODE_G = "BITSLIP". Both terminal states are sourced one register stage deep so that the resulting latency does not depend on where the comma landed: ALIGNED_S from the previous GT word, ALIGNED_SLIP_S from that word shifted up one bit with its missing MSB taken live. One stage is the floor, since at offset 1 the aligned word's last bit only arrives with the next GT word. The select therefore asserts in both terminal states, and BITSLIP always costs exactly one rxUsrClk of fiber-to-rxDataOut latency relative to RESET, whose RX_DATA_OUT_RESET_GEN drives rxDataOut combinationally off RXDATA and adds no fabric stage. That is 5.385 ns on an LCLS-II link at 3.714 Gbps and 8.403 ns on an LCLS-I link at 2.380 Gbps. It is constant across bring-ups, so it costs a caller one re-calibration rather than run-to-run jitter, but it is a real change to the absolute number. rxDataAligned and rxDataAlignedSel are driven exclusively inside RX_FIX_LAT_ALIGN_GEN, so BITSLIP asserts that generate's full condition, RX_BUF_EN_G = false and RX_ALIGN_MODE_G = "FIXED_LAT". RX_BUF_EN_G defaults to true, which makes the unguarded combination the likeliest caller mistake: it would elaborate RX_NO_ALIGN_GEN instead of the aligner, tie rxPhaseAlignmentDone high, leave rxDataAlignedSel at its declared '0', and fall through to the raw rxDataInt path, giving no comma alignment at all while reporting alignment done. No caller in surf selects BITSLIP, and the one external caller that does (lcls-timing-core TimingGtCoreWrapper) already sets RX_BUF_EN_G => false, so nothing that elaborates today starts failing. BITSLIP never asserts the aligner rxReset, which removes one of the two paths that previously restarted alignment. The remaining path is Gtx7RxRst MONITOR_DATA_VALID/FSM_DONE, fed by Gtx7Core rxDataValidIn, which defaults to '1'. Left at that default the supervision loop is permanently satisfied and a lost alignment is never re-attempted; DO_PHASE_ALIGNMENT time_out_wait_bypass only covers an alignment that never completes, not one that completes and is later lost. Several in-tree callers hardwire rxDataValidIn => '1' while already being RX_BUF_EN_G => false with FIXED_LAT, so this is a live constraint. Both properties are documented at the generic and at the mux. A dont_touch'd signal inside a BITSLIP_MODE_C-gated generate exposes the first resolved odd offset and an odd-landing count for hardware observability. Only the first odd offset is latched, since every odd landing above 1 re-scans at 1 and would otherwise overwrite the offset the CDR actually landed on. Scoping the state to the generate keeps it out of the netlist entirely under RESET; the earlier null-range record form did not, and Vivado tied four degenerate bits low on lanes that use neither field. RX_ODD_ALIGN_MODE_G defaults to "RESET", reproducing today's behavior exactly. Every change is behind BITSLIP_MODE_C or inside the gated generate, and RX_DATA_OUT_RESET_GEN is untouched. Verified by co-simulating the pre-change and post-change aligners from identical stimulus: cycle-identical on rxReset, rxSlide and rxPhaseAlignmentDone over 12000 cycles across 5 aligner-driven RX resets. Not addressed here: even landings settle at GT offset 0 and odd landings at offset 1. If an odd final offset costs a sub-UI recovered-clock phase step, a <=1 UI difference between the two classes remains that no fabric logic can remove, since crossing parity classes requires a fresh CDR lock. That is a property of the silicon and is being measured on hardware, binned on landing parity. --- xilinx/7Series/gtx7/rtl/Gtx7Core.vhd | 74 +++++++-- .../gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd | 157 ++++++++++++++++-- 2 files changed, 206 insertions(+), 25 deletions(-) diff --git a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd index eede82ad49..d964f006c5 100755 --- a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd +++ b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd @@ -81,6 +81,19 @@ entity Gtx7Core is -- Configure RX comma alignment RX_ALIGN_MODE_G : string := "GT"; -- Or "FIXED_LAT" or "NONE" + RX_ODD_ALIGN_MODE_G : string := "RESET"; -- "RESET": legacy behavior, resets the RX on + -- an odd comma landing; "BITSLIP": resolves + -- the odd residue in fabric. Requires + -- RX_ALIGN_MODE_G = "FIXED_LAT" and + -- RX_BUF_EN_G = false. "BITSLIP" adds one + -- rxUsrClk of latency versus "RESET" and + -- never asserts the aligner's rxReset, so + -- recovery from a LOST alignment rests + -- entirely on Gtx7RxRst's DATA_VALID + -- supervision. Drive rxDataValidIn from a + -- decoder when selecting "BITSLIP"; its + -- default of '1' leaves that loop + -- permanently satisfied. ALIGN_COMMA_DOUBLE_G : string := "FALSE"; ALIGN_COMMA_ENABLE_G : bit_vector := "1111111111"; ALIGN_COMMA_WORD_G : integer := 2; @@ -340,11 +353,13 @@ architecture rtl of Gtx7Core is signal rxLpmHfHold : sl; -- Rx Data - signal rxDataInt : slv(RX_EXT_DATA_WIDTH_G-1 downto 0); - signal rxDataFull : slv(63 downto 0); -- GT RXDATA - signal rxCharIsKFull : slv(7 downto 0); -- GT RXCHARISK - signal rxDispErrFull : slv(7 downto 0); -- GT RXDISPERR - signal rxDecErrFull : slv(7 downto 0); + signal rxDataInt : slv(RX_EXT_DATA_WIDTH_G-1 downto 0); + signal rxDataAligned : slv(RX_EXT_DATA_WIDTH_G-1 downto 0) := (others => '0'); + signal rxDataAlignedSel : sl := '0'; + signal rxDataFull : slv(63 downto 0); -- GT RXDATA + signal rxCharIsKFull : slv(7 downto 0); -- GT RXCHARISK + signal rxDispErrFull : slv(7 downto 0); -- GT RXDISPERR + signal rxDecErrFull : slv(7 downto 0); ---------------------------- @@ -390,6 +405,23 @@ architecture rtl of Gtx7Core is begin + -- RX_ODD_ALIGN_MODE_G is a string generic so it cannot carry a constrained range; this assert + -- enforces the two-member enumeration explicitly instead. + assert (RX_ODD_ALIGN_MODE_G = "RESET") or (RX_ODD_ALIGN_MODE_G = "BITSLIP") + report "Gtx7Core: RX_ODD_ALIGN_MODE_G must be RESET or BITSLIP" + severity failure; + + -- rxDataAligned/rxDataAlignedSel are driven only inside RX_FIX_LAT_ALIGN_GEN, so this assert + -- must repeat that generate's FULL condition, RX_BUF_EN_G = false AND RX_ALIGN_MODE_G = + -- "FIXED_LAT". RX_BUF_EN_G defaults to true, so checking only the align mode would let the + -- likeliest caller mistake through: FIXED_LAT plus BITSLIP with RX_BUF_EN_G left at its + -- default elaborates RX_NO_ALIGN_GEN instead, which ties rxPhaseAlignmentDone high and leaves + -- rxDataAlignedSel at its declared '0', so RX_DATA_OUT_BITSLIP_GEN silently degenerates to the + -- raw rxDataInt path while reporting alignment done. + assert (RX_ODD_ALIGN_MODE_G /= "BITSLIP") or (RX_ALIGN_MODE_G = "FIXED_LAT" and RX_BUF_EN_G = false) + report "Gtx7Core: RX_ODD_ALIGN_MODE_G = BITSLIP requires RX_ALIGN_MODE_G = FIXED_LAT and RX_BUF_EN_G = false" + severity failure; + rxOutClkOut <= rxOutClkBufg; cPllLockOut <= cPllLock; @@ -417,7 +449,18 @@ begin -- Rx Logic -------------------------------------------------------------------------------------------------- -- Fit GTX port sizes to selected rx external interface size - rxDataOut <= rxDataInt; + -- rxDataAlignedSel asserts in both of the aligner's terminal states, so once alignment is + -- reached this path is taken regardless of where the comma landed. That is what keeps the + -- fiber-to-rxDataOut latency identical on every bring-up; selecting rxDataInt for the + -- even-landing case would reintroduce a landing-dependent parallel-clock period. + RX_DATA_OUT_BITSLIP_GEN : if (RX_ODD_ALIGN_MODE_G = "BITSLIP") generate + rxDataOut <= rxDataAligned when (rxDataAlignedSel = '1') else rxDataInt; + end generate; + + RX_DATA_OUT_RESET_GEN : if (RX_ODD_ALIGN_MODE_G /= "BITSLIP") generate + rxDataOut <= rxDataInt; + end generate; + RX_DATA_8B10B_GLUE : process (rxCharIsKFull, rxDataFull, rxDecErrFull, rxDispErrFull) is begin @@ -584,20 +627,23 @@ begin RX_FIX_LAT_ALIGN_GEN : if (RX_BUF_EN_G = false and RX_ALIGN_MODE_G = "FIXED_LAT") generate Gtx7RxFixedLatPhaseAligner_Inst : entity surf.Gtx7RxFixedLatPhaseAligner generic map ( - TPD_G => TPD_G, - WORD_SIZE_G => RX_EXT_DATA_WIDTH_G, - COMMA_EN_G => FIXED_COMMA_EN_G, - COMMA_0_G => FIXED_ALIGN_COMMA_0_G, - COMMA_1_G => FIXED_ALIGN_COMMA_1_G, - COMMA_2_G => FIXED_ALIGN_COMMA_2_G, - COMMA_3_G => FIXED_ALIGN_COMMA_3_G) + TPD_G => TPD_G, + WORD_SIZE_G => RX_EXT_DATA_WIDTH_G, + COMMA_EN_G => FIXED_COMMA_EN_G, + COMMA_0_G => FIXED_ALIGN_COMMA_0_G, + COMMA_1_G => FIXED_ALIGN_COMMA_1_G, + COMMA_2_G => FIXED_ALIGN_COMMA_2_G, + COMMA_3_G => FIXED_ALIGN_COMMA_3_G, + RX_ODD_ALIGN_MODE_G => RX_ODD_ALIGN_MODE_G) port map ( rxUsrClk => rxUsrClkIn, rxRunPhAlignment => rxRunPhAlignment, rxData => rxDataInt, rxReset => rxAlignReset, rxSlide => rxSlide, - rxPhaseAlignmentDone => rxPhaseAlignmentDone); + rxPhaseAlignmentDone => rxPhaseAlignmentDone, + rxDataAligned => rxDataAligned, + rxDataAlignedSel => rxDataAlignedSel); rxDlySReset <= '0'; end generate; diff --git a/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd b/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd index e3f508a4a8..2345142afc 100755 --- a/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd +++ b/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd @@ -12,6 +12,17 @@ -- the phase of the output clock only every other slide. This module's -- purpose is to obtain an output clock that exactly matches the phase of the -- commas. +-- +-- That reset-and-retry is RX_ODD_ALIGN_MODE_G = "RESET", the default, and it +-- can loop without bound on a link whose CDR keeps landing odd. "BITSLIP" +-- resolves an odd landing in fabric instead and never resets; see the generic +-- below for what it costs. +-- +-- Because "BITSLIP" never asserts rxReset, returning to SEARCH_S after an +-- alignment is LOST depends entirely on the enclosing Gtx7RxRst deasserting +-- rxRunPhAlignment, which it only does when its own DATA_VALID supervision +-- fails. A caller selecting "BITSLIP" must therefore drive Gtx7Core's +-- rxDataValidIn from a decoder rather than leave it at its default of '1'. ------------------------------------------------------------------------------- -- This file is part of 'SLAC Firmware Standard Library'. -- It is subject to the license terms in the LICENSE.txt file found in the @@ -31,27 +42,42 @@ use surf.StdRtlPkg.all; entity Gtx7RxFixedLatPhaseAligner is generic ( - TPD_G : time := 1 ns; - WORD_SIZE_G : integer := 20; - COMMA_EN_G : slv(3 downto 0) := "0011"; - COMMA_0_G : slv := "----------0101111100"; - COMMA_1_G : slv := "----------1010000011"; - COMMA_2_G : slv := "XXXXXXXXXXXXXXXXXXXX"; - COMMA_3_G : slv := "XXXXXXXXXXXXXXXXXXXX"); + TPD_G : time := 1 ns; + WORD_SIZE_G : integer := 20; + COMMA_EN_G : slv(3 downto 0) := "0011"; + COMMA_0_G : slv := "----------0101111100"; + COMMA_1_G : slv := "----------1010000011"; + COMMA_2_G : slv := "XXXXXXXXXXXXXXXXXXXX"; + COMMA_3_G : slv := "XXXXXXXXXXXXXXXXXXXX"; + RX_ODD_ALIGN_MODE_G : string := "RESET"); -- "RESET": legacy behavior, resets the GTX RX on + -- an odd comma landing and hopes for an even + -- relock; "BITSLIP": resolves an odd landing in + -- fabric using only even rxSlide counts, then a + -- constant 1-bit fabric slice. Both terminal + -- states present the aligned word one rxUsrClk + -- after the GT, so the latency is the same for + -- every landing. port ( rxUsrClk : in sl; rxRunPhAlignment : in sl; -- From RxRst, active low reset, not clocked by rxUsrClk rxData : in slv(WORD_SIZE_G-1 downto 0); -- Encoded raw rx data rxReset : out sl; rxSlide : out sl; -- RXSLIDE input to GTX - rxPhaseAlignmentDone : out sl); -- Alignment has been achieved. + rxPhaseAlignmentDone : out sl; -- Alignment has been achieved. + rxDataAligned : out slv(WORD_SIZE_G-1 downto 0); -- Valid only when rxDataAlignedSel='1' + rxDataAlignedSel : out sl); -- '1': downstream must select rxDataAligned over rxData end entity Gtx7RxFixedLatPhaseAligner; architecture rtl of Gtx7RxFixedLatPhaseAligner is constant SLIDE_WAIT_C : integer := 32; -- Dictated by UG476 GTX Transceiver Guide - type StateType is (SEARCH_S, RESET_S, SLIDE_S, SLIDE_WAIT_S, ALIGNED_S); + constant BITSLIP_MODE_C : boolean := (RX_ODD_ALIGN_MODE_G = "BITSLIP"); + + constant ODD_OBS_WIDTH_C : positive := bitSize(WORD_SIZE_G); + constant ODD_CNT_WIDTH_C : positive := 8; + + type StateType is (SEARCH_S, RESET_S, SLIDE_S, SLIDE_WAIT_S, ALIGNED_S, ALIGNED_SLIP_S); type RegType is record state : StateType; @@ -74,11 +100,30 @@ architecture rtl of Gtx7RxFixedLatPhaseAligner is rxSlide => '0', rxPhaseAlignmentDone => '0'); + subtype OddOffsetType is natural range 0 to WORD_SIZE_G-1; + + type OddObsType is record + landedOffset : slv(ODD_OBS_WIDTH_C-1 downto 0); + landedValid : sl; + oddLandingCount : slv(ODD_CNT_WIDTH_C-1 downto 0); + end record OddObsType; + + constant ODD_OBS_INIT_C : OddObsType := ( + landedOffset => (others => '0'), + landedValid => '0', + oddLandingCount => (others => '0')); + signal r : RegType := REG_RESET_C; signal rin : RegType; signal rxRunPhAlignmentSync : sl; + -- Combinational, not part of r/rin: gated by the elaboration-time BITSLIP_MODE_C constant, so + -- under "RESET" this elaborates to a constant '0' drive with no added mux, and dont_touch on r + -- does not preserve any register for it. + signal rxDataAlignedInt : slv(WORD_SIZE_G-1 downto 0); + signal rxDataAlignedSelInt : sl; + attribute dont_touch : string; attribute dont_touch of r : signal is "TRUE"; @@ -87,6 +132,12 @@ architecture rtl of Gtx7RxFixedLatPhaseAligner is begin + -- RX_ODD_ALIGN_MODE_G is a string generic so it cannot carry a constrained range; this assert + -- enforces the two-member enumeration explicitly instead. + assert (RX_ODD_ALIGN_MODE_G = "RESET") or (RX_ODD_ALIGN_MODE_G = "BITSLIP") + report "Gtx7RxFixedLatPhaseAligner: RX_ODD_ALIGN_MODE_G must be RESET or BITSLIP" + severity failure; + -- Must use async resets since rxUsrClk can drop out RstSync_1 : entity surf.RstSync generic map ( @@ -132,8 +183,27 @@ begin else -- Latch the Alignment Value v.alignmentValue := i; - -- Reset the rx and hope for a new lock requiring an even number of slides - v.state := RESET_S; + if BITSLIP_MODE_C then + if (i = 1) then + -- Zero slides needed: the residue resolves through the fabric slice + -- alone + v.state := ALIGNED_SLIP_S; + else + -- Reduce the residue to 1 using the i mod 2 = 0 branch's own slide + -- sequencer (SLIDE_S/SLIDE_WAIT_S, unmodified). That sequencer issues + -- slideCount+1 pulses, as the even branch above notes, so slideCount + -- must be i-2 to issue i-1 pulses, which is even for odd i. Setting it + -- to i-1 would issue i pulses, an odd count landing on offset 0, which + -- is exactly what this mode exists to avoid. SLIDE_WAIT_S returns to + -- SEARCH_S, which re-scans and re-enters this branch at i = 1, + -- resolving with no further slides. + v.slideCount := to_unsigned(i-2, bitSize(WORD_SIZE_G)); + v.state := SLIDE_S; + end if; + else + -- Reset the rx and hope for a new lock requiring an even number of slides + v.state := RESET_S; + end if; end if; end if; end loop; @@ -162,6 +232,12 @@ begin v.rxPhaseAlignmentDone := '1'; -- Gtx7RxRst module will reset this module back to SEARCH_S if alignment is lost + when ALIGNED_SLIP_S => + v.rxPhaseAlignmentDone := '1'; + -- Gtx7RxRst module will reset this module back to SEARCH_S if alignment is lost. + -- rxDataAlignedSelInt (driven below, combinationally, from r.state) tells Gtx7Core to + -- select the fabric-sliced word instead of rxData while this state holds. + end case; rin <= v; @@ -172,6 +248,65 @@ begin rxPhaseAlignmentDone <= r.rxPhaseAlignmentDone; end process comb; + -- Aligned-word output, valid only when BITSLIP_MODE_C. Every odd landing resolves to a fixed + -- residue of 1 before the slice is taken, so the slice is a constant bit range of the history, + -- not an offset-dependent one. + -- + -- Both terminal states source the word one rxUsrClk after the GT presented it, so the latency + -- from fiber to rxDataOut does not depend on where the comma landed: + -- + -- ALIGNED_S (offset 0) -> the previous GT word, unshifted + -- ALIGNED_SLIP_S (offset 1) -> the previous GT word shifted up one bit, its missing MSB taken + -- from the live word + -- + -- One stage is the floor here, not a convenience: at offset 1 the aligned word's last bit only + -- arrives with the next GT word, so it cannot be presented combinationally. Sourcing ALIGNED_S + -- from rxData instead would make the two states differ by a full parallel-clock period, which + -- is the determinism this mode is supposed to provide. + -- + -- Under "RESET" both drives elaborate to constants with no mux, since BITSLIP_MODE_C is an + -- elaboration-time constant. + rxDataAlignedInt <= + (rxData(0) & r.last(WORD_SIZE_G*2-1 downto WORD_SIZE_G+1)) when (BITSLIP_MODE_C and (r.state = ALIGNED_SLIP_S)) else + r.last(WORD_SIZE_G*2-1 downto WORD_SIZE_G) when (BITSLIP_MODE_C and (r.state = ALIGNED_S)) else + (others => '0'); + + rxDataAlignedSelInt <= '1' when (BITSLIP_MODE_C and ((r.state = ALIGNED_S) or (r.state = ALIGNED_SLIP_S))) else '0'; + + rxDataAligned <= rxDataAlignedInt; + rxDataAlignedSel <= rxDataAlignedSelInt; + + ODD_OBS_GEN : if BITSLIP_MODE_C generate + + signal obs : OddObsType := ODD_OBS_INIT_C; + + attribute dont_touch of obs : signal is "TRUE"; + + begin + + obsSeq : process (rxRunPhAlignmentSync, rxUsrClk) is + begin + if (rising_edge(rxUsrClk)) then + -- Latch only the FIRST odd offset seen since reset. Every odd landing above 1 slides + -- down to a residue of 1 and re-scans, so without this guard the offset the CDR + -- actually landed on would always be overwritten by that terminal 1. + if (r.state = SEARCH_S) and ((rin.alignmentValue mod 2) = 1) and (obs.landedValid = '0') then + obs.landedOffset <= std_logic_vector( + to_unsigned(OddOffsetType'(rin.alignmentValue), ODD_OBS_WIDTH_C)) after TPD_G; + obs.landedValid <= '1' after TPD_G; + end if; + if (rin.state = ALIGNED_SLIP_S) and (r.state /= ALIGNED_SLIP_S) then + obs.oddLandingCount <= std_logic_vector( + unsigned(obs.oddLandingCount) + 1) after TPD_G; + end if; + end if; + if (rxRunPhAlignmentSync = '0') then + obs <= ODD_OBS_INIT_C after TPD_G; + end if; + end process obsSeq; + + end generate ODD_OBS_GEN; + seq : process (rxRunPhAlignmentSync, rxUsrClk) is begin if (rising_edge(rxUsrClk)) then From 134e40ce82944c8a4e32caf034f800f7c2f8e36b Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Sun, 23 Aug 2026 16:25:01 -0700 Subject: [PATCH 19/57] test(gtx7): add a Gtx7RxFixedLatPhaseAligner cocotb regression Add the aligner to the non-Vivado GHDL source list and a pytest/cocotb suite sweeping all 20 comma landings in both RX_ODD_ALIGN_MODE_G modes. Per-landing invariants. Every landing settles with an even slide count, so the recovered clock phase is never disturbed; BITSLIP never asserts rxReset; and every landing presents the same frame on the same cycle as every other landing in its mode. Absolute latency, not just landing-invariance. EXPECTED_TRAIL carries one entry per mode, in whole rxUsrClk of fiber-to-rxDataOut latency, so what switching a link from RESET to BITSLIP costs is stated rather than recoverable only by reading Gtx7Core's two output-mux legs against the aligner's pipeline depth. RESET adds no fabric stage; BITSLIP always adds the aligner's one stage. The delta is one rxUsrClk. Mutation-tested per property. Restoring the odd-branch off-by-one fails the 9 odd BITSLIP landings; restoring a combinational even path fails the 10 even ones. Moving the RESET trail entry alone fails exactly the ten even RESET landings and nothing else; moving the BITSLIP entry alone fails all twenty BITSLIP landings and nothing else. Deepening both aligner terminal states by one stage fails all twenty BITSLIP landings, confirming the trail entry tracks the RTL rather than a constant. Scope is recorded in the file. The aligner's end of the contract is elaborated, but Gtx7Core's output mux is reproduced by Harness.data_out() rather than built, because Gtx7Core needs GTXE2_CHANNEL and does not compile under GHDL. An edit to the mux expressions themselves is out of reach of this suite. --- tests/xilinx/gtx7/__init__.py | 0 .../gtx7/test_Gtx7RxFixedLatPhaseAligner.py | 298 ++++++++++++++++++ xilinx/ruckus.tcl | 1 + 3 files changed, 299 insertions(+) create mode 100644 tests/xilinx/gtx7/__init__.py create mode 100644 tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py diff --git a/tests/xilinx/gtx7/__init__.py b/tests/xilinx/gtx7/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py new file mode 100644 index 0000000000..fb73db1b45 --- /dev/null +++ b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py @@ -0,0 +1,298 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Two elaborations, one per RX_ODD_ALIGN_MODE_G value. The generic +# gates a constant and a generate, so it cannot be varied inside one build. +# - Stimulus: A bit-accurate serial stream of tagged 20-bit frames feeds a GT +# model that presents word k as stream[20k + b - d] for a landing offset d and +# decrements d on every rxSlide pulse. Every one of the 20 possible comma +# landings is driven, one per test, so the aligner is exercised across its +# whole input space rather than at a sampled subset. +# - Checks: The property under test is that the fiber-to-rxDataOut latency does +# not depend on where the CDR happened to land. Each landing must reach +# alignment, present a correctly comma-aligned word, and -- the part that +# matters -- present the SAME frame on the SAME cycle as every other landing. +# Under BITSLIP each landing must also settle using an EVEN rxSlide count and +# must never assert rxReset. Under RESET the legacy contract is pinned +# instead: odd landings assert rxReset, even landings do not. +# - Cross-mode: each mode's ABSOLUTE latency is pinned against EXPECTED_TRAIL, +# so the cost of choosing BITSLIP over RESET (one rxUsrClk) is itself a +# regression target rather than an unstated consequence of two independent +# within-mode checks. That check reaches the aligner's boundary contract only; +# Gtx7Core's mux is reproduced by the harness, not elaborated. +# - Timing: Latency is compared in exact frame indices at a common cycle, which +# is a stricter statement than comparing rxPhaseAlignmentDone timing. The +# comparison is held over several cycles so a one-sample coincidence cannot +# pass. Frames carry a 6-bit sequence tag for exactly this purpose. +# - Does not prove: Anything about the GTX PMA itself. Whether a final offset of +# 1 costs a sub-UI recovered-clock phase step relative to a final offset of 0 +# is a property of the silicon, not of this RTL, and no fabric simulation can +# settle it. This test proves the fabric contributes no landing-dependent +# latency of its own; the residual sub-UI term must be measured on hardware. + +import os + +import cocotb +import pytest +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import ( + env_int, + parameter_case, + run_surf_vhdl_test, +) + +WORD_SIZE = 20 +CLK_PERIOD_NS = 5.384 # lane 1's measured rxUsrClk period +COMMA = "0101111100" # K28.5, bits 9:0 of every frame +SLIDE_SETTLE_CYCLES = 70 # SLIDE_WAIT_S burns 64; give it margin +N_FRAMES = 4096 + +# Whole rxUsrClk of fiber-to-rxDataOut latency each mode adds once aligned, and +# therefore the cost of choosing BITSLIP over RESET. Both entries are asserted +# below, so the delta between them is a checked contract and not just a comment. +# +# RESET takes Gtx7Core's RX_DATA_OUT_RESET_GEN leg (rxDataOut <= rxDataInt), +# combinational off RXDATA through RX_DATA_8B10B_GLUE, so it adds no fabric +# stage. BITSLIP takes RX_DATA_OUT_BITSLIP_GEN, whose select asserts in BOTH +# terminal states, so once aligned it always adds the aligner's one stage. One +# stage is the floor, not a convenience: at final offset 1 the aligned word's +# MSB only arrives with the next GT word. +# +# The delta is therefore one rxUsrClk: 5.385 ns on an LCLS-II link at 3.714 +# Gbps, 8.403 ns on an LCLS-I link at 2.380 Gbps. It is constant across +# bring-ups, so it costs a caller one re-calibration rather than introducing +# run-to-run jitter, but it is a real change to the absolute number and a +# reviewer switching a link to BITSLIP will ask for it by name. +# +# Scope: the aligner drives one end of this (rxDataAligned one stage deep, and +# rxDataAlignedSel telling Gtx7Core which leg to take), and that end is +# elaborated. Gtx7Core's mux itself is reproduced by Harness.data_out(), since +# Gtx7Core needs GTXE2_CHANNEL and does not build under GHDL, so an edit to the +# mux expressions is out of reach of this file. +EXPECTED_TRAIL = {"RESET": 0, "BITSLIP": 1} + +ODD_ALIGN_MODE = os.environ.get("RX_ODD_ALIGN_MODE_G", "RESET").strip().strip("'") +LANDING = env_int("LANDING", default=0) + + +def frame_of(m: int) -> int: + """Frame m: comma in bits 9:0, '1111' in bits 19:16, 6-bit tag in bits 15:10. + + The tag nibble is pinned to all ones so the comma pattern (and its inverse) + can only match at the true frame boundary; a run of four ones cannot occur + inside either comma code, which rules out a false landing. + """ + return (0xF << 16) | ((m % 64) << 10) | int(COMMA, 2) + + +def bit_at(p: int) -> int: + if p < 0: + return 0 + return (frame_of(p // WORD_SIZE) >> (p % WORD_SIZE)) & 1 + + +def gt_word(k: int, d: int) -> int: + """The GT's parallel word k when the comma lands d bits into the word.""" + word = 0 + for b in range(WORD_SIZE): + word |= bit_at(WORD_SIZE * k + b - d) << b + return word + + +def is_frame(word: int) -> bool: + return (word & 0x3FF) == int(COMMA, 2) and ((word >> 16) & 0xF) == 0xF + + +def tag_of(word: int) -> int: + return (word >> 10) & 0x3F + + +def expected_tag_at(cycle: int) -> int: + """Tag this mode must be presenting at `cycle`, per EXPECTED_TRAIL. + + Harness.step() drives GT word index (cycle-1) before clocking, so at `cycle` + the model is sourcing index cycle-1 and the output must sit EXPECTED_TRAIL + cycles behind it. + """ + return (cycle - 1 - EXPECTED_TRAIL[ODD_ALIGN_MODE]) % 64 + + +class Harness: + """GT model plus Gtx7Core's output mux, wrapped around one aligner.""" + + def __init__(self, dut, landing): + self.dut = dut + self.offset = landing + self.landing = landing + self.slides = 0 + self.saw_reset = False + self.cycle = 0 + + dut.rxRunPhAlignment.value = 0 + dut.rxData.value = 0 + cocotb.start_soon(Clock(dut.rxUsrClk, CLK_PERIOD_NS, unit="ns").start()) + + async def release_reset(self): + for _ in range(10): + await RisingEdge(self.dut.rxUsrClk) + self.dut.rxRunPhAlignment.value = 1 + + async def step(self): + """Advance one cycle, presenting the GT word and consuming rxSlide.""" + self.dut.rxData.value = gt_word(self.cycle, self.offset) + await RisingEdge(self.dut.rxUsrClk) + await Timer(1, unit="ns") + self.cycle += 1 + if self.dut.rxReset.value == 1: + self.saw_reset = True + if self.dut.rxSlide.value == 1: + self.offset -= 1 + self.slides += 1 + + def data_out(self) -> int: + """Reproduce Gtx7Core's RX_DATA_OUT_BITSLIP_GEN mux.""" + if ODD_ALIGN_MODE == "BITSLIP" and self.dut.rxDataAlignedSel.value == 1: + return int(self.dut.rxDataAligned.value) + return int(self.dut.rxData.value) + + def aligned(self) -> bool: + return self.dut.rxPhaseAlignmentDone.value == 1 + + +async def run_landing(dut, landing): + tb = Harness(dut, landing) + await tb.release_reset() + + # Worst case is landing 19: 18 slides, each costing SLIDE_WAIT_S's full wait. + budget = 20 * SLIDE_SETTLE_CYCLES + for _ in range(budget): + await tb.step() + if tb.aligned(): + break + return tb + + +@cocotb.test() +async def bitslip_landing_is_latency_invariant(dut): + """Every landing must align with an even slide count and no RX reset.""" + if ODD_ALIGN_MODE != "BITSLIP": + return + + tb = await run_landing(dut, LANDING) + + assert tb.aligned(), f"landing {LANDING}: never reached alignment" + assert not tb.saw_reset, ( + f"landing {LANDING}: asserted rxReset in BITSLIP mode, which is the " + f"unbounded-relock behavior this mode exists to remove" + ) + assert tb.slides % 2 == 0, ( + f"landing {LANDING}: settled with an ODD slide count ({tb.slides}). " + f"An odd count moves the recovered sampling phase off the grid that " + f"even counts preserve, which is what RESET mode refuses to do." + ) + assert tb.offset in (0, 1), ( + f"landing {LANDING}: settled at offset {tb.offset}, expected 0 or 1" + ) + assert tb.offset == LANDING % 2, ( + f"landing {LANDING}: parity is not conserved by sliding " + f"(settled at {tb.offset})" + ) + + word = tb.data_out() + assert is_frame(word), ( + f"landing {LANDING}: output 0x{word:05X} is not a comma-aligned frame" + ) + + # Latency in exact frames: the aligned word must trail the GT word the + # model is presenting by the same amount for every landing, and that amount + # is EXPECTED_TRAIL's BITSLIP entry, so the mode's absolute cost is pinned + # here rather than left as a bare offset. One stage is the floor at offset + # 1, so the contract is exactly one. + expected_tag = expected_tag_at(tb.cycle) + assert tag_of(word) == expected_tag, ( + f"landing {LANDING}: presented frame tag {tag_of(word)} at cycle " + f"{tb.cycle}, expected {expected_tag}. The fabric added a " + f"landing-dependent delay." + ) + + # Hold it: a single sample could coincide by luck. + for _ in range(8): + await tb.step() + word = tb.data_out() + assert is_frame(word), f"landing {LANDING}: lost alignment at cycle {tb.cycle}" + assert tag_of(word) == expected_tag_at(tb.cycle), ( + f"landing {LANDING}: frame tag slipped at cycle {tb.cycle}" + ) + + +@cocotb.test() +async def reset_mode_rejects_odd_landings(dut): + """RESET mode's legacy contract, pinned against regression.""" + if ODD_ALIGN_MODE != "RESET": + return + + tb = await run_landing(dut, LANDING) + + if LANDING % 2 == 1: + assert tb.saw_reset, ( + f"landing {LANDING} is odd: RESET mode must demand a fresh CDR lock" + ) + else: + assert not tb.saw_reset, ( + f"landing {LANDING} is even: RESET mode must not reset" + ) + assert tb.aligned(), f"landing {LANDING}: never reached alignment" + assert tb.offset == 0, ( + f"landing {LANDING}: RESET mode must settle at offset 0, " + f"got {tb.offset}" + ) + assert tb.slides == LANDING, ( + f"landing {LANDING}: expected {LANDING} slides, got {tb.slides}" + ) + # RESET mode must keep the fabric path out of the way entirely, so + # Gtx7Core's RX_DATA_OUT_RESET_GEN branch stays bit-identical. + assert dut.rxDataAlignedSel.value == 0, ( + "rxDataAlignedSel asserted under RESET mode" + ) + assert is_frame(int(dut.rxData.value)), ( + f"landing {LANDING}: GT word is not comma-aligned after sliding" + ) + # The other end of EXPECTED_TRAIL. RESET adds no stage, so the delta + # against BITSLIP is one rxUsrClk; see EXPECTED_TRAIL for what that + # costs a caller. Stated as a trail rather than left implicit in + # offset == 0 so both modes are pinned in the same terms. + assert tag_of(int(dut.rxData.value)) == expected_tag_at(tb.cycle), ( + f"landing {LANDING}: RESET mode presented frame tag " + f"{tag_of(int(dut.rxData.value))} at cycle {tb.cycle}, expected " + f"{expected_tag_at(tb.cycle)}. RESET's fiber-to-rxDataOut latency " + f"moved, so the BITSLIP delta is no longer one rxUsrClk." + ) + + +PARAMETER_SWEEP = [ + parameter_case(f"{mode.lower()}_landing{landing:02d}", + RX_ODD_ALIGN_MODE_G=mode, + LANDING=str(landing)) + for mode in ("BITSLIP", "RESET") + for landing in range(WORD_SIZE) +] + + +@pytest.mark.parametrize("parameters", PARAMETER_SWEEP) +def test_Gtx7RxFixedLatPhaseAligner(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.gtx7rxfixedlatphasealigner", + parameters={"RX_ODD_ALIGN_MODE_G": parameters["RX_ODD_ALIGN_MODE_G"]}, + extra_env=parameters, + ) diff --git a/xilinx/ruckus.tcl b/xilinx/ruckus.tcl index db4a5e86dc..c9875a582f 100644 --- a/xilinx/ruckus.tcl +++ b/xilinx/ruckus.tcl @@ -9,6 +9,7 @@ if { $::env(VIVADO_VERSION) > 0.0} { } else { loadSource -lib surf -path "$::DIR_PATH/general/rtl/SelectIoRxGearboxAligner.vhd" loadSource -lib surf -path "$::DIR_PATH/general/rtl/GtRxAlignCheck.vhd" + loadSource -lib surf -path "$::DIR_PATH/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd" loadSource -lib surf -dir "$::DIR_PATH/dummy" } From 16b6fd1272281ebd6ae6c5fc6e1ece465da4cd83 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 7 Jul 2026 14:09:58 -0700 Subject: [PATCH 20/57] Harden RSSI connection negotiation --- protocols/rssi/v1/rtl/RssiConnFsm.vhd | 123 +++++++++++++++-------- tests/protocols/rssi/test_RssiConnFsm.py | 5 +- 2 files changed, 84 insertions(+), 44 deletions(-) diff --git a/protocols/rssi/v1/rtl/RssiConnFsm.vhd b/protocols/rssi/v1/rtl/RssiConnFsm.vhd index b00b28b38b..421d9d8f47 100644 --- a/protocols/rssi/v1/rtl/RssiConnFsm.vhd +++ b/protocols/rssi/v1/rtl/RssiConnFsm.vhd @@ -40,7 +40,7 @@ entity RssiConnFsm is -- WINDOW_ADDR_SIZE_G : positive := 3; SEGMENT_ADDR_SIZE_G : positive := 7 -- 2^SEGMENT_ADDR_SIZE_G = Number of 64 bit wide data words - ); + ); port ( clk_i : in sl; rst_i : in sl; @@ -95,10 +95,11 @@ entity RssiConnFsm is -- Status signals peerTout_o : out sl; paramReject_o : out sl - ); + ); end entity RssiConnFsm; architecture rtl of RssiConnFsm is + -- constant SAMPLES_PER_TIME_C : integer := integer(TIMEOUT_UNIT_G * CLK_FREQUENCY_G); -- @@ -112,7 +113,7 @@ architecture rtl of RssiConnFsm is WAIT_ACK_S, SEND_RST_S, OPEN_S - ); + ); type RegType is record connActive : sl; @@ -136,7 +137,6 @@ architecture rtl of RssiConnFsm is --- state : StateType; connState : slv(3 downto 0); - end record RegType; constant REG_INIT_C : RegType := ( @@ -165,6 +165,42 @@ architecture rtl of RssiConnFsm is signal r : RegType := REG_INIT_C; signal rin : RegType; + function validPeerParams(param : RssiParamType) return boolean is + begin + return ( + param.maxOutsSeg /= 0 and + conv_integer(param.maxSegSize) >= 8 and + param.retransTout /= 0 and + param.cumulAckTout /= 0 and + param.nullSegTout /= 0); + end function validPeerParams; + + function clampWindowSize(value : slv(7 downto 0)) return integer is + variable size : integer; + begin + size := conv_integer(value); + if (size < 1) then + return 1; + elsif (size > 2 ** WINDOW_ADDR_SIZE_G) then + return 2 ** WINDOW_ADDR_SIZE_G; + else + return size; + end if; + end function clampWindowSize; + + function clampBufferSize(value : slv(15 downto 0)) return integer is + variable size : integer; + begin + size := conv_integer(value(15 downto 3)); -- Divide by 8 + if (size < 1) then + return 1; + elsif (size > 2 ** SEGMENT_ADDR_SIZE_G) then + return 2 ** SEGMENT_ADDR_SIZE_G; + else + return size; + end if; + end function clampBufferSize; + begin @@ -234,14 +270,17 @@ begin v.sndAck := '0'; v.sndRst := '0'; v.txAckF := '0'; - v.timeoutCntr := r.timeoutCntr + 1; + if (r.timeoutCntr /= RETRANS_TOUT_G * SAMPLES_PER_TIME_C) then + v.timeoutCntr := r.timeoutCntr + 1; + end if; -- if (rxValid_i = '1' and rxFlags_i.syn = '1' and rxFlags_i.ack = '1') then -- Check parameters if ( rxRssiParam_i.version = appRssiParam_i.version and -- Version match rxRssiParam_i.chksumEn = appRssiParam_i.chksumEn and -- Checksum match - rxRssiParam_i.timeoutUnit = appRssiParam_i.timeoutUnit -- Timeout unit match + rxRssiParam_i.timeoutUnit = appRssiParam_i.timeoutUnit and -- Timeout unit match + validPeerParams(rxRssiParam_i) = true ) then -- Accept the parameters from the server @@ -249,19 +288,19 @@ begin -- Autoneg the maxOutsSeg if (rxRssiParam_i.maxOutsSeg > appRssiParam_i.maxOutsSeg) then - v.txWindowSize := conv_integer(appRssiParam_i.maxOutsSeg); + v.txWindowSize := clampWindowSize(appRssiParam_i.maxOutsSeg); v.rssiParam.maxOutsSeg := appRssiParam_i.maxOutsSeg; else - v.txWindowSize := conv_integer(rxRssiParam_i.maxOutsSeg); + v.txWindowSize := clampWindowSize(rxRssiParam_i.maxOutsSeg); v.rssiParam.maxOutsSeg := rxRssiParam_i.maxOutsSeg; end if; -- Autoneg the maxSegSize if (rxRssiParam_i.maxSegSize > appRssiParam_i.maxSegSize) then - v.txBufferSize := conv_integer(appRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 + v.txBufferSize := clampBufferSize(appRssiParam_i.maxSegSize); v.rssiParam.maxSegSize := appRssiParam_i.maxSegSize; else - v.txBufferSize := conv_integer(rxRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 + v.txBufferSize := clampBufferSize(rxRssiParam_i.maxSegSize); v.rssiParam.maxSegSize := rxRssiParam_i.maxSegSize; end if; @@ -327,43 +366,43 @@ begin -- if (rxValid_i = '1' and rxFlags_i.syn = '1') then - -- Accept the parameters from the client - v.rssiParam := rxRssiParam_i; - - -- Autoneg the maxOutsSeg - if (rxRssiParam_i.maxOutsSeg > appRssiParam_i.maxOutsSeg) then - v.txWindowSize := conv_integer(appRssiParam_i.maxOutsSeg); - v.rssiParam.maxOutsSeg := appRssiParam_i.maxOutsSeg; - else - v.txWindowSize := conv_integer(rxRssiParam_i.maxOutsSeg); - v.rssiParam.maxOutsSeg := rxRssiParam_i.maxOutsSeg; - end if; - - -- Autoneg the maxSegSize - if (rxRssiParam_i.maxSegSize > appRssiParam_i.maxSegSize) then - v.txBufferSize := conv_integer(appRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 - v.rssiParam.maxSegSize := appRssiParam_i.maxSegSize; - else - v.txBufferSize := conv_integer(rxRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 - v.rssiParam.maxSegSize := rxRssiParam_i.maxSegSize; - end if; - - -- - v.paramReject := '0'; - -- Check parameters that have to match if ( rxRssiParam_i.version /= appRssiParam_i.version or -- Version equality rxRssiParam_i.chksumEn /= appRssiParam_i.chksumEn or -- Checksum match - rxRssiParam_i.timeoutUnit /= appRssiParam_i.timeoutUnit -- Timeout unit match + rxRssiParam_i.timeoutUnit /= appRssiParam_i.timeoutUnit or -- Timeout unit match + validPeerParams(rxRssiParam_i) = false ) then -- Propose different parameters (overwrite) - v.rssiParam.version := appRssiParam_i.version; - v.rssiParam.timeoutUnit := appRssiParam_i.timeoutUnit; - v.rssiParam.chksumEn := appRssiParam_i.chksumEn; + v.rssiParam := appRssiParam_i; + v.txWindowSize := clampWindowSize(appRssiParam_i.maxOutsSeg); + v.txBufferSize := clampBufferSize(appRssiParam_i.maxSegSize); -- - v.paramReject := '1'; + v.paramReject := '1'; + else + -- Accept the parameters from the client + v.rssiParam := rxRssiParam_i; + + -- Autoneg the maxOutsSeg + if (rxRssiParam_i.maxOutsSeg > appRssiParam_i.maxOutsSeg) then + v.txWindowSize := clampWindowSize(appRssiParam_i.maxOutsSeg); + v.rssiParam.maxOutsSeg := appRssiParam_i.maxOutsSeg; + else + v.txWindowSize := clampWindowSize(rxRssiParam_i.maxOutsSeg); + v.rssiParam.maxOutsSeg := rxRssiParam_i.maxOutsSeg; + end if; + + -- Autoneg the maxSegSize + if (rxRssiParam_i.maxSegSize > appRssiParam_i.maxSegSize) then + v.txBufferSize := clampBufferSize(appRssiParam_i.maxSegSize); + v.rssiParam.maxSegSize := appRssiParam_i.maxSegSize; + else + v.txBufferSize := clampBufferSize(rxRssiParam_i.maxSegSize); + v.rssiParam.maxSegSize := rxRssiParam_i.maxSegSize; + end if; + + v.paramReject := '0'; end if; -- Go to ACK state v.state := SEND_SYN_ACK_S; @@ -398,7 +437,9 @@ begin v.txAckF := '0'; v.paramReject := '0'; -- - v.timeoutCntr := r.timeoutCntr+1; + if (r.timeoutCntr /= RETRANS_TOUT_G * SAMPLES_PER_TIME_C) then + v.timeoutCntr := r.timeoutCntr + 1; + end if; -- v.rssiParam := r.rssiParam; @@ -507,5 +548,7 @@ begin connState_o <= r.connState; peerTout_o <= r.peerTout; paramReject_o <= r.paramReject; + --------------------------------------------------------------------- + end architecture rtl; diff --git a/tests/protocols/rssi/test_RssiConnFsm.py b/tests/protocols/rssi/test_RssiConnFsm.py index 41834586a9..e31818c32c 100644 --- a/tests/protocols/rssi/test_RssiConnFsm.py +++ b/tests/protocols/rssi/test_RssiConnFsm.py @@ -37,7 +37,7 @@ import pytest from cocotb.triggers import Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import RssiParams from tests.protocols.ssi.ssi_test_utils import ( cycle as ssi_cycle, @@ -422,10 +422,7 @@ async def client_retries_syn_then_times_out_waiting_for_syn_ack_test(dut): pytest.param({"SERVER_G": False}, id="client"), ] -KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" - -@pytest.mark.skipif(not env_flag("RUN_RSSI_KNOWN_ISSUE_TESTS", default=False), reason=KNOWN_ISSUE_REASON) @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_RssiConnFsm(parameters): run_surf_vhdl_test( From 34f78ff61f47be4cd665965e9a0612b5ad21a099 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 7 Jul 2026 12:56:08 -0700 Subject: [PATCH 21/57] fix(rssi): correct monitor busy and liveness timing --- protocols/rssi/v1/rtl/RssiMonitor.vhd | 21 +++++++++++++++------ tests/protocols/rssi/test_RssiMonitor.py | 5 +---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/protocols/rssi/v1/rtl/RssiMonitor.vhd b/protocols/rssi/v1/rtl/RssiMonitor.vhd index 5a4ffe6167..6aba795975 100644 --- a/protocols/rssi/v1/rtl/RssiMonitor.vhd +++ b/protocols/rssi/v1/rtl/RssiMonitor.vhd @@ -52,8 +52,8 @@ entity RssiMonitor is STATUS_WIDTH_G : positive := 8; CNT_WIDTH_G : positive := 32; RETRANSMIT_ENABLE_G : boolean := true - -- - ); + -- + ); port ( clk_i : in sl; rst_i : in sl; @@ -185,7 +185,9 @@ architecture rtl of RssiMonitor is signal rin : RegType; signal s_status : slv(STATUS_WIDTH_G - 1 downto 0); -- + begin + -- Status assignment s_status(0) <= r.retransMax and r.sndResend and not r.sndResendD1; s_status(1) <= r.nullTout; @@ -312,8 +314,6 @@ begin if (connActive_i = '0' or (rxValid_i = '1' and rxFlags_i.data = '1') or (rxValid_i = '1' and rxFlags_i.nul = '1') or - (rxValid_i = '1' and rxFlags_i.ack = '1') or - (rxValid_i = '1' and rxFlags_i.busy = '1') or RETRANSMIT_ENABLE_G = false -- Disable null timeout ) then v.nullToutCnt := (others => '0'); @@ -356,7 +356,7 @@ begin dataHeadSt_i = '1' or rstHeadSt_i = '1' or nullHeadSt_i = '1' or - (rxLastSeqN_i - r.lastAckSeqN) = 0 + ((rxLastSeqN_i - r.lastAckSeqN) = 0 and localBusy_i = '0') ) then v.ackToutCnt := (others => '0'); elsif ((rxLastSeqN_i - r.lastAckSeqN) > 0 and (rxLastSeqN_i - r.lastAckSeqN) <= rxWindowSize_i) or (localBusy_i = '1') then @@ -374,8 +374,16 @@ begin ) then v.sndAck := '0'; + -- Periodic BUSY acknowledgment request. The RSSI page recommends + -- Retransmission Timeout/2 so the peer keeps its retransmission timer + -- reset while this receiver remains busy. + elsif (localBusy_i = '1' and (rxLastSeqN_i - r.lastAckSeqN) = 0 and + r.ackToutCnt >= ((conv_integer(rssiParam_i.retransTout)*SAMPLES_PER_TIME_C)/2)) then + v.sndAck := '1'; + -- Timeout acknowledgment request - elsif (r.ackToutCnt >= (conv_integer(rssiParam_i.cumulAckTout)* SAMPLES_PER_TIME_C)) then + elsif ((rxLastSeqN_i - r.lastAckSeqN) > 0 and + r.ackToutCnt >= (conv_integer(rssiParam_i.cumulAckTout)* SAMPLES_PER_TIME_C)) then v.sndAck := '1'; -- Cumulative acknowledgment request @@ -460,4 +468,5 @@ begin resendCnt_o <= r.resendCnt; reconCnt_o <= r.reconCnt; --------------------------------------------------------------------- + end architecture rtl; diff --git a/tests/protocols/rssi/test_RssiMonitor.py b/tests/protocols/rssi/test_RssiMonitor.py index 8103ff2e61..3ca7b1721d 100644 --- a/tests/protocols/rssi/test_RssiMonitor.py +++ b/tests/protocols/rssi/test_RssiMonitor.py @@ -34,7 +34,7 @@ from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import run_surf_vhdl_test class TB: @@ -222,10 +222,7 @@ async def local_busy_generates_periodic_ack_after_cumulative_timeout_test(dut): PARAMETER_SWEEP = [pytest.param({}, id="server_monitor")] -KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" - -@pytest.mark.skipif(not env_flag("RUN_RSSI_KNOWN_ISSUE_TESTS", default=False), reason=KNOWN_ISSUE_REASON) @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_RssiMonitor(parameters): run_surf_vhdl_test( From 3e65a780dcb4ffaecfaa749423636142c0328907 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 7 Jul 2026 13:17:46 -0700 Subject: [PATCH 22/57] fix(rssi): correct TX NULL ordering and checksum injection --- protocols/rssi/v1/rtl/RssiTxFsm.vhd | 50 +++++++++++++++++--------- tests/protocols/rssi/test_RssiTxFsm.py | 5 +-- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/protocols/rssi/v1/rtl/RssiTxFsm.vhd b/protocols/rssi/v1/rtl/RssiTxFsm.vhd index 61449bf991..3554dacf3a 100644 --- a/protocols/rssi/v1/rtl/RssiTxFsm.vhd +++ b/protocols/rssi/v1/rtl/RssiTxFsm.vhd @@ -64,7 +64,7 @@ entity RssiTxFsm is DATA_HEADER_SIZE_G : natural := 8; HEADER_CHKSUM_EN_G : boolean := true - ); + ); port ( clk_i : in sl; rst_i : in sl; @@ -153,7 +153,7 @@ entity RssiTxFsm is -- Segment buffer indicator bufferEmpty_o : out sl - ); + ); end entity RssiTxFsm; architecture rtl of RssiTxFsm is @@ -185,7 +185,7 @@ architecture rtl of RssiTxFsm is RESEND_H_S, RESEND_DATA_S, RESEND_PP_S - ); + ); type AppStateType is ( IDLE_S, @@ -193,17 +193,16 @@ architecture rtl of RssiTxFsm is SEG_RCV_S, SEG_RDY_S, SEG_LEN_ERR - ); + ); type AckStateType is ( IDLE_S, ERR_S, ACK_S --EACK_S, - ); + ); type RegType is record - -- Buffer window handling and acknowledgment control ----------------------------------------- windowArray : WindowTypeArray(0 to 2 ** WINDOW_ADDR_SIZE_G-1); @@ -275,7 +274,7 @@ architecture rtl of RssiTxFsm is tspSsiSlave : SsiSlaveType; -- State Machine - tspState : tspStateType; + tspState : TspStateType; txTspState : slv(7 downto 0); end record RegType; @@ -358,6 +357,7 @@ architecture rtl of RssiTxFsm is signal rin : RegType; signal s_chksum : slv(chksum_i'range); signal s_headerAndChksum : slv(RSSI_WORD_WIDTH_C*8-1 downto 0); + signal s_corruptHeader : slv(RSSI_WORD_WIDTH_C*8-1 downto 0); -- attribute dont_touch : string; -- attribute dont_touch of r : signal is "TRUE"; @@ -369,13 +369,15 @@ begin -- Send all 0 if checksum disabled s_chksum <= ite(HEADER_CHKSUM_EN_G, chksum_i, (chksum_i'range => '0')); s_headerAndChksum <= rdHeaderData_i(63 downto 16) & s_chksum(15 downto 0); + s_corruptHeader <= s_headerAndChksum xor x"000000000000FFFF"; ----------------------------------------------------------------------------------------------- comb : process (ackN_i, ack_i, appSsiMaster_i, bufferSize_i, chksumValid_i, closed_i, connActive_i, headerLength_i, headerRdy_i, initSeqN_i, injectFault_i, r, rdBuffData_i, rdHeaderData_i, - rst_i, s_headerAndChksum, sndAck_i, sndNull_i, sndResend_i, - sndRst_i, sndSyn_i, tspSsiSlave_i, windowSize_i) is + rst_i, s_corruptHeader, s_headerAndChksum, sndAck_i, + sndNull_i, sndResend_i, sndRst_i, sndSyn_i, tspSsiSlave_i, + windowSize_i) is variable v : RegType; @@ -892,7 +894,7 @@ begin v.tspState := RESEND_INIT_S; elsif (sndAck_i = '1') then v.tspState := ACK_H_S; - elsif (sndNull_i = '1' and r.bufferFull = '0' and r.appBusy = '0') then + elsif (sndNull_i = '1' and r.bufferEmpty = '1' and r.appBusy = '0') then v.tspState := NULL_WE_S; elsif (connActive_i = '0') then v.tspState := INIT_S; @@ -943,6 +945,7 @@ begin -- Add checksum v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); v.tspSsiMaster.valid := '1'; + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.eof := '1'; v.tspSsiMaster.eofe := '0'; @@ -1007,8 +1010,13 @@ begin v.tspSsiMaster.eof := '1'; v.tspSsiMaster.eofe := '0'; - -- Add checksum to last two bytes - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + if (r.injectFaultReg = '1') then + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); + else + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + end if; + + v.injectFaultReg := '0'; -- if connActive_i = '0' then @@ -1178,8 +1186,13 @@ begin v.tspSsiMaster.eof := '1'; v.tspSsiMaster.eofe := '0'; - -- Add checksum to last two bytes - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + if (r.injectFaultReg = '1') then + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); + else + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + end if; + + v.injectFaultReg := '0'; -- Increment seqN v.nextSeqN := r.nextSeqN+1; -- Increment SEQ number at the end of segment transmission @@ -1261,13 +1274,14 @@ begin v.tspSsiMaster.valid := '1'; v.tspSsiMaster.sof := '1'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); v.tspSsiMaster.eof := '0'; v.tspSsiMaster.eofe := '0'; -- Inject fault into checksum if (r.injectFaultReg = '1') then - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum) xor (s_headerAndChksum'range => '1'); -- Flip bits in checksum! Point of fault injection! + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); else v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); -- Add checksum to last two bytes end if; @@ -1307,6 +1321,7 @@ begin -- Other SSI parameters v.tspSsiMaster.sof := '0'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := rdBuffData_i; @@ -1440,11 +1455,12 @@ begin v.tspSsiMaster.sof := '1'; v.tspSsiMaster.valid := '1'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); -- Inject fault into checksum if (r.injectFaultReg = '1') then - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum) xor (s_headerAndChksum'range => '1'); -- Flip bits in checksum! Point of fault injection! + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); else v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); -- Add checksum to last two bytes end if; @@ -1501,6 +1517,7 @@ begin -- Other SSI parameters v.tspSsiMaster.sof := '0'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := rdBuffData_i; @@ -1639,4 +1656,5 @@ begin end process seq; --------------------------------------------------------------------- + end architecture rtl; diff --git a/tests/protocols/rssi/test_RssiTxFsm.py b/tests/protocols/rssi/test_RssiTxFsm.py index 60517d60dc..afdd36084a 100644 --- a/tests/protocols/rssi/test_RssiTxFsm.py +++ b/tests/protocols/rssi/test_RssiTxFsm.py @@ -39,7 +39,7 @@ import pytest from cocotb.triggers import FallingEdge, RisingEdge, Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import ( RssiParams, build_ack_header, @@ -815,10 +815,7 @@ async def rst_segment_emits_header_and_consumes_sequence_without_buffering_test( PARAMETER_SWEEP = [pytest.param({}, id="small_window")] -KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" - -@pytest.mark.skipif(not env_flag("RUN_RSSI_KNOWN_ISSUE_TESTS", default=False), reason=KNOWN_ISSUE_REASON) @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_RssiTxFsm(parameters): run_surf_vhdl_test( From 16aac145113c8e1be8452a7427ef670651935cc0 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Mon, 24 Aug 2026 12:23:23 -0700 Subject: [PATCH 23/57] Check that software instantiation matches hardware. --- python/surf/devices/analog_devices/_AdcDdr.py | 38 +++++++++++++++++++ .../analog_devices/_AdcDdrCalibration.py | 6 +++ 2 files changed, 44 insertions(+) diff --git a/python/surf/devices/analog_devices/_AdcDdr.py b/python/surf/devices/analog_devices/_AdcDdr.py index 86c3f27729..2b69529022 100644 --- a/python/surf/devices/analog_devices/_AdcDdr.py +++ b/python/surf/devices/analog_devices/_AdcDdr.py @@ -104,6 +104,44 @@ class AdcDdr(pr.Device): Additional arguments forwarded to ``pyrogue.Device``. """ + def checkGeometry(self) -> None: + """Verify the model's construction matches the RTL capability registers. + + The register map's field widths and scan ranges are fixed at + construction from generics such as ``deviceFamily``. If those do not + match the geometry the RTL actually built, individual accesses fail far + from the cause -- a too-wide ``delayBits`` model, for example, lets a + scan program a tap the hardware register cannot hold, and only the + readback verify deep inside a calibration reports it. Reading the + normalized read-only capability registers here turns that into one + explicit, early error that names the mismatched field. + + Raises + ------ + RuntimeError + If any advertised geometry field disagrees with this model. + """ + + expected = { + 'DataLanes': self._dataLanes, + 'FcoLanes': self._fcoLanes, + 'Channels': self._channels, + 'SampleBits': self._sampleBits, + 'SerializationFactor': self._serializationFactor, + 'DelayBits': self._delayBits, + } + mismatches = [] + for name, modelValue in expected.items(): + hwValue = int(self.node(name).get(read=True)) + if hwValue != modelValue: + mismatches.append(f'{name}: model={modelValue}, hardware={hwValue}') + if mismatches: + raise RuntimeError( + f'{self.path}: AdcDdr register model does not match the RTL ' + 'capability registers; check the deviceFamily and other ' + 'construction generics against the firmware. Mismatches -- ' + + '; '.join(mismatches)) + def _getDataDelays(self, read: bool) -> list[int]: """Return all data-lane delay values.""" diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index b6983b7a36..4376c2b195 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -1197,6 +1197,12 @@ def _runCalibration(self, *, dev: Any) -> dict[str, Any]: def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: """Validate controls, execute the selected operation, and restore state.""" + # Fail early and clearly if the register model was constructed with a + # geometry that disagrees with the RTL (for example a deviceFamily that + # sets the wrong delay width). Otherwise a mismatch only surfaces as a + # cryptic readback verify error partway through the scan. + self._readout.checkGeometry() + start = self.DelayStart.value() stop = self.DelayStop.value() settle = self.SettleTime.value() From be3c401a2621489b87748a7021207c4830855b1c Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Mon, 24 Aug 2026 13:40:23 -0700 Subject: [PATCH 24/57] Fix Ad9681Sim - proper bank addressing. --- .../AnalogDevices/ad9681/sim/Ad9681Sim.vhd | 22 +-- .../ad9681/sim/Ad9681SimCore.vhd | 86 +++++----- .../ad9681/wrappers/Ad9681SimCoreWrapper.vhd | 2 - .../devices/analog_devices/test_Ad9681Sim.py | 17 +- .../analog_devices/test_Ad9681SimCore.py | 159 ++++++++++-------- .../analog_devices/test_AdcDdrCalibration.py | 5 + 6 files changed, 154 insertions(+), 137 deletions(-) diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd index b5600a03e0..3f1c736ac6 100644 --- a/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd +++ b/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd @@ -62,9 +62,6 @@ architecture behavioral of Ad9681Sim is signal cfgByteValid : slv(3 downto 0); signal cfgRdByte : slv(7 downto 0); signal cfgRdData : slv(31 downto 0); - signal cfgPending : sl := '0'; - signal coreBank : sl; - signal coreWrEn : sl; signal normalData : Slv16Array(7 downto 0) := (others => (others => '0')); signal normalDataPipeline : NormalDataPipelineType := (others => (others => (others => '0'))); signal delayedNormalData : Slv16Array(7 downto 0); @@ -160,19 +157,11 @@ begin rdData => cfgRdData); -- [in] ------------------------------------------------------------------------------------------------ - -- The previous single-SPI model applies channel-index writes to both - -- four-channel halves. Replay each parsed write once per internal bank. + -- The physical AD9681 exposes a single SPI port that addresses both internal + -- four-channel banks. Applying each write to both banks in the same sample + -- clock keeps their per-channel PN generators reseeded coherently, matching + -- the real device; staggering the two banks would offset them by one sample. ------------------------------------------------------------------------------------------------ - cfgReplay : process (clkP) is - begin - if rising_edge(clkP) then - cfgPending <= spiWrEn after TPD_G; - end if; - end process cfgReplay; - - coreWrEn <= spiWrEn or cfgPending; - coreBank <= '0' when spiWrEn = '1' else '1'; - U_Core : entity surf.Ad9681SimCore generic map ( TPD_G => TPD_G) @@ -181,8 +170,7 @@ begin sampleRst => '0', -- [in] sampleEnable => '1', -- [in] normalData => delayedNormalData, -- [in] - cfgBank => coreBank, -- [in] - cfgWrEn => coreWrEn, -- [in] + cfgWrEn => spiWrEn, -- [in] cfgAddr => cfgAddr(8 downto 0), -- [in] cfgWrData => cfgWrData(7 downto 0), -- [in] cfgRdData => cfgRdByte, -- [out] diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd index 9aa9ca8e64..0d9368fca0 100644 --- a/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd +++ b/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd @@ -27,7 +27,6 @@ entity Ad9681SimCore is sampleRst : in sl; sampleEnable : in sl; normalData : in Slv16Array(7 downto 0); - cfgBank : in sl; cfgWrEn : in sl; cfgAddr : in slv(8 downto 0); cfgWrData : in slv(7 downto 0); @@ -102,11 +101,10 @@ architecture rtl of Ad9681SimCore is pn23 => PN23_SEED_C); type ChannelArray is array (natural range <>) of ChannelType; - type SelectArray is array (natural range <>) of slv(3 downto 0); type RegType is record rdData : slv(7 downto 0); - selectMask : SelectArray(1 downto 0); + selectMask : slv(5 downto 0); global : GlobalType; channel : ChannelArray(7 downto 0); resolution : slv(6 downto 0); @@ -116,9 +114,11 @@ architecture rtl of Ad9681SimCore is valid : sl; end record RegType; + -- Register 0x05 (device index) powers up at 0x3F, selecting every data + -- channel so the first local-register write reaches all channels at once. constant REG_INIT_C : RegType := ( rdData => (others => '0'), - selectMask => (others => (others => '1')), + selectMask => (others => '1'), global => GLOBAL_INIT_C, channel => (others => CHANNEL_INIT_C), resolution => (others => '0'), @@ -133,21 +133,22 @@ architecture rtl of Ad9681SimCore is begin ------------------------------------------------------------------------------------------------- - -- Each configuration bank owns four channels. AD9681 writes take effect - -- immediately except for the resolution/sample-rate override at 0x100. + -- One SPI register file addresses all eight channels. Register 0x05 (device + -- index) selects which data channels a subsequent local-register write + -- affects; its four select bits each cover a channel pair (A1/A2 .. D1/D2), + -- so channel ch is selected by selectMask(ch/2). A single write reaches + -- every selected channel in the same cycle, matching the real device. All + -- writes take effect immediately except the 0x100 override, which is staged + -- until the 0xFF transfer strobe. ------------------------------------------------------------------------------------------------- - comb : process (cfgAddr, cfgBank, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is - variable active : ChannelType; - variable bankBase : natural range 0 to 4; - variable bankIndex : natural range 0 to 1; - variable v : RegType; - variable word : slv(15 downto 0); - variable code : slv(13 downto 0); + comb : process (cfgAddr, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is + variable active : ChannelType; + variable v : RegType; + variable word : slv(15 downto 0); + variable code : slv(13 downto 0); begin v := r; v.valid := '0'; - bankBase := ite(cfgBank = '1', 4, 0); - bankIndex := ite(cfgBank = '1', 1, 0); if (sampleRst = '1') then v := REG_INIT_C; @@ -159,22 +160,25 @@ begin v := REG_INIT_C; end if; when DEVICE_INDEX_ADDR_C => - v.selectMask(bankIndex) := cfgWrData(3 downto 0); + -- Bits[3:0] select data channels A..D (each a pair); bits[5:4] + -- select the DCO/FCO clock channels and are retained only for + -- readback since this model has no separately timed clocks. + v.selectMask := cfgWrData(5 downto 0); when POWER_MODE_ADDR_C => v.global.powerMode(1 downto 0) := cfgWrData(1 downto 0); v.global.powerMode(2) := cfgWrData(5); when TEST_MODE_ADDR_C => - for i in 3 downto 0 loop - if (r.selectMask(bankIndex)(i) = '1') then - v.channel(bankBase+i).userMode := cfgWrData(7 downto 6); - v.channel(bankBase+i).resetPn23 := cfgWrData(5); - v.channel(bankBase+i).resetPn9 := cfgWrData(4); - v.channel(bankBase+i).testMode := cfgWrData(3 downto 0); + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then + v.channel(i).userMode := cfgWrData(7 downto 6); + v.channel(i).resetPn23 := cfgWrData(5); + v.channel(i).resetPn9 := cfgWrData(4); + v.channel(i).testMode := cfgWrData(3 downto 0); if (cfgWrData(4) = '1') then - v.channel(bankBase+i).pn9 := PN9_SEED_C; + v.channel(i).pn9 := PN9_SEED_C; end if; if (cfgWrData(5) = '1') then - v.channel(bankBase+i).pn23 := PN23_SEED_C; + v.channel(i).pn23 := PN23_SEED_C; end if; end if; end loop; @@ -183,13 +187,13 @@ begin v.global.outputFormat := cfgWrData(0); when USER_PATTERN1_LSB_C | USER_PATTERN1_MSB_C | USER_PATTERN2_LSB_C | USER_PATTERN2_MSB_C => - for i in 3 downto 0 loop - if (r.selectMask(bankIndex)(i) = '1') then + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then case cfgAddr is - when USER_PATTERN1_LSB_C => v.channel(bankBase+i).userPatternA(7 downto 0) := cfgWrData; - when USER_PATTERN1_MSB_C => v.channel(bankBase+i).userPatternA(15 downto 8) := cfgWrData; - when USER_PATTERN2_LSB_C => v.channel(bankBase+i).userPatternB(7 downto 0) := cfgWrData; - when others => v.channel(bankBase+i).userPatternB(15 downto 8) := cfgWrData; + when USER_PATTERN1_LSB_C => v.channel(i).userPatternA(7 downto 0) := cfgWrData; + when USER_PATTERN1_MSB_C => v.channel(i).userPatternA(15 downto 8) := cfgWrData; + when USER_PATTERN2_LSB_C => v.channel(i).userPatternB(7 downto 0) := cfgWrData; + when others => v.channel(i).userPatternB(15 downto 8) := cfgWrData; end case; end if; end loop; @@ -204,10 +208,10 @@ begin v.global.select2x := cfgWrData(2); v.global.outputBits := cfgWrData(1 downto 0); when CHANNEL_STATUS_ADDR_C => - for i in 3 downto 0 loop - if (r.selectMask(bankIndex)(i) = '1') then - v.channel(bankBase+i).outputReset := cfgWrData(1); - v.channel(bankBase+i).powerDown := cfgWrData(0); + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then + v.channel(i).outputReset := cfgWrData(1); + v.channel(i).powerDown := cfgWrData(0); end if; end loop; when TRANSFER_ADDR_C => @@ -274,12 +278,12 @@ begin end if; end if; - -- Local-register reads return the lowest-numbered selected channel. This - -- is Channel A when the default mask selects all four channels. - active := r.channel(bankBase); - for i in 3 downto 0 loop - if (r.selectMask(bankIndex)(i) = '1') then - active := r.channel(bankBase+i); + -- A local-register read returns the lowest-numbered selected channel; the + -- datasheet specifies Channel A1 when every device-index bit is set. + active := r.channel(0); + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then + active := r.channel(i); end if; end loop; v.rdData := (others => '0'); @@ -287,7 +291,7 @@ begin when SPI_CONFIG_ADDR_C => v.rdData := "00011000"; when CHIP_ID_ADDR_C => v.rdData := X"8F"; when CHIP_GRADE_ADDR_C => v.rdData(6 downto 4) := "110"; - when DEVICE_INDEX_ADDR_C => v.rdData(3 downto 0) := r.selectMask(bankIndex); + when DEVICE_INDEX_ADDR_C => v.rdData(5 downto 0) := r.selectMask; when POWER_MODE_ADDR_C => v.rdData(1 downto 0) := r.global.powerMode(1 downto 0); v.rdData(5) := r.global.powerMode(2); diff --git a/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd index 7a0df5b4d4..a9b48c8bb8 100644 --- a/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd +++ b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd @@ -24,7 +24,6 @@ entity Ad9681SimCoreWrapper is sampleRst : in sl; sampleEnable : in sl; normalData : in slv(127 downto 0); - cfgBank : in sl; cfgWrEn : in sl; cfgAddr : in slv(8 downto 0); cfgWrData : in slv(7 downto 0); @@ -51,7 +50,6 @@ begin sampleRst => sampleRst, -- [in] sampleEnable => sampleEnable, -- [in] normalData => normalArray, -- [in] - cfgBank => cfgBank, -- [in] cfgWrEn => cfgWrEn, -- [in] cfgAddr => cfgAddr, -- [in] cfgWrData => cfgWrData, -- [in] diff --git a/tests/devices/analog_devices/test_Ad9681Sim.py b/tests/devices/analog_devices/test_Ad9681Sim.py index 33a40ddc5d..6b110475b2 100644 --- a/tests/devices/analog_devices/test_Ad9681Sim.py +++ b/tests/devices/analog_devices/test_Ad9681Sim.py @@ -106,16 +106,21 @@ async def ad9681_pin_level_device_sim_test(dut): assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + # Device index bit 0 selects data channel pair A (physical channels 0 and + # 1). The test mode applies to exactly that pair and leaves the rest on + # normal data, confirming per-channel selection through the actual SPI pins. await spi_write(dut, 0x05, 0x01) await spi_write(dut, 0x0D, 0x02) words, _ = await capture_frame(dut) assert words[0] == 0xFFFC - assert words[4] == 0xFFFC - assert words[1:4] == [value << 2 for value in normal[1:4]] - assert words[5:] == [value << 2 for value in normal[5:]] - - # Alternating output words must be latched once per frame. Reading - # sampleData live for each serialized bit tears these two patterns together. + assert words[1] == 0xFFFC + assert words[2:] == [value << 2 for value in normal[2:]] + + # Reselect every channel, then apply the alternating checkerboard. Words are + # latched once per frame; reading sampleData live for each serialized bit + # would tear the two patterns together. A single broadcast write keeps every + # channel coherent, so channels 0 and 4 must show the same phase each frame. + await spi_write(dut, 0x05, 0x3F) await spi_write(dut, 0x0D, 0x04) checkerboard = [] for _ in range(4): diff --git a/tests/devices/analog_devices/test_Ad9681SimCore.py b/tests/devices/analog_devices/test_Ad9681SimCore.py index 7e981ebb70..bbf3c4d8d8 100644 --- a/tests/devices/analog_devices/test_Ad9681SimCore.py +++ b/tests/devices/analog_devices/test_Ad9681SimCore.py @@ -9,16 +9,17 @@ ############################################################################## # Test methodology: -# - Sweep: Exercise default two-lane words, both four-channel configuration -# banks, the staged resolution/rate override, user and PN9 patterns, -# inversion, bit order, and power. +# - Sweep: Exercise default two-lane words, the register 0x05 device index that +# selects data channels, the staged resolution/rate override, user and PN +# patterns, inversion, bit order, and power for all eight channels. # - Stimulus: Write the primitive-free byte-register interface while sampling # independent normal codes for all eight channels. # - Checks: Padding placement, identity, immediate ordinary-register updates, -# 0x100 transfer semantics, unsupported-format fallback, pattern values, PN reset -# hold/release, transforms, soft reset, and suppression are checked. +# device-index channel isolation, 0x100 transfer semantics, unsupported-format +# fallback, pattern values, PN reset hold/release, transforms, soft reset, and +# suppression are checked. # - Timing: Configuration and samples share one clock; only the resolution/rate -# override remains hidden until the Register 0xFF transfer write. +# override remains hidden until the register 0xFF transfer write. import cocotb from cocotb.clock import Clock @@ -27,9 +28,8 @@ from tests.common.regression_utils import run_surf_vhdl_test -async def write(dut, bank, addr, data): +async def write(dut, addr, data): await FallingEdge(dut.sampleClk) - dut.cfgBank.value = bank dut.cfgAddr.value = addr dut.cfgWrData.value = data dut.cfgWrEn.value = 1 @@ -38,8 +38,7 @@ async def write(dut, bank, addr, data): dut.cfgWrEn.value = 0 -async def read(dut, bank, addr): - dut.cfgBank.value = bank +async def read(dut, addr): dut.cfgAddr.value = addr await Timer(1, unit="ns") return int(dut.cfgRdData.value) @@ -79,11 +78,10 @@ def reverse_bytes(word): @cocotb.test() -async def ad9681_register_bank_and_pattern_test(dut): +async def ad9681_register_index_and_pattern_test(dut): normal = [0x200 + i for i in range(8)] dut.sampleRst.value = 1 dut.sampleEnable.value = 0 - dut.cfgBank.value = 0 dut.cfgWrEn.value = 0 dut.cfgAddr.value = 0 dut.cfgWrData.value = 0 @@ -93,96 +91,115 @@ async def ad9681_register_bank_and_pattern_test(dut): await RisingEdge(dut.sampleClk) dut.sampleRst.value = 0 - assert await read(dut, 0, 0x01) == 0x8F - assert await read(dut, 1, 0x02) == 0x60 - assert await read(dut, 0, 0x05) == 0x0F - assert await read(dut, 1, 0x05) == 0x0F - assert await read(dut, 0, 0x100) == 0x00 + # Identity and defaults. Register 0x05 powers up at 0x3F so every data and + # clock channel receives the next write. + assert await read(dut, 0x01) == 0x8F + assert await read(dut, 0x02) == 0x60 + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 data = await sample(dut) assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] assert all((channel(data, i) & 0x3) == 0 for i in range(8)) # Register 0x100 is staged until the transfer strobe at Register 0xFF. - await write(dut, 0, 0x100, 0x66) - assert await read(dut, 0, 0x100) == 0x00 - await write(dut, 0, 0xFF, 0x00) - assert await read(dut, 0, 0x100) == 0x00 - await write(dut, 0, 0xFF, 0x01) - assert await read(dut, 0, 0x100) == 0x66 + await write(dut, 0x100, 0x66) + assert await read(dut, 0x100) == 0x00 + await write(dut, 0xFF, 0x00) + assert await read(dut, 0x100) == 0x00 + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x100) == 0x66 # Unsupported output formats warn immediately but retain register # readback and continue using the model's fixed two-lane bytewise format. - await write(dut, 0, 0x21, 0x00) - assert await read(dut, 0, 0x21) == 0x00 + await write(dut, 0x21, 0x00) + assert await read(dut, 0x21) == 0x00 data = await sample(dut) assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] - await write(dut, 0, 0x21, 0x30) + await write(dut, 0x21, 0x30) - # Select physical configuration bank 1/channel 4. The new mode applies - # immediately and must not affect bank 0. - await write(dut, 1, 0x05, 0x01) - await write(dut, 1, 0x0D, 0x02) + # Device index bits[3:0] select data channels A..D, each a pair (channel ch + # is gated by bit ch/2). Selecting only channel B (bit 1) applies a test + # mode to channels 2 and 3 and leaves the others on normal data. + await write(dut, 0x05, 0x02) + await write(dut, 0x0D, 0x02) data = await sample(dut) - assert channel(data, 4) == 0xFFFC + assert channel(data, 2) == 0xFFFC + assert channel(data, 3) == 0xFFFC assert channel(data, 0) == normal[0] << 2 - assert channel(data, 5) == normal[5] << 2 - - # Program a selected channel in bank 0 for alternating user words. - await write(dut, 0, 0x05, 0x01) - await write(dut, 0, 0x19, 0x23) - await write(dut, 0, 0x1A, 0x01) - await write(dut, 0, 0x1B, 0x56) - await write(dut, 0, 0x1C, 0x04) - await write(dut, 0, 0x0D, 0x48) - first = channel(await sample(dut), 0) - second = channel(await sample(dut), 0) - assert {first, second} == {0x0123, 0x0456} + assert channel(data, 4) == normal[4] << 2 + + # A local read with a single channel selected returns that channel's copy. + assert await read(dut, 0x0D) == 0x02 + # Restore full selection; the datasheet returns Channel A1 for an all-set + # read, so 0x0D reads back channel 0's (still default) test mode. + await write(dut, 0x05, 0x3F) + await write(dut, 0x0D, 0x00) + + # Program every channel for alternating user words in one broadcast write. + # A single snapshot holds one shared toggle phase, so all channels match; + # consecutive snapshots alternate. Capturing one sample and indexing every + # channel keeps the shared state at a single instant. + await write(dut, 0x19, 0x23) + await write(dut, 0x1A, 0x01) + await write(dut, 0x1B, 0x56) + await write(dut, 0x1C, 0x04) + await write(dut, 0x0D, 0x48) + first = await sample(dut) + second = await sample(dut) + assert all(channel(first, i) == channel(first, 0) for i in range(8)) + assert all(channel(second, i) == channel(second, 0) for i in range(8)) + assert {channel(first, 0), channel(second, 0)} == {0x0123, 0x0456} # PN23 reset is a retained level, not a self-clearing command. While it is - # asserted, the selected generator stays at its seed; clearing it releases - # a repeatable sequence from that seed. + # asserted, the selected generators stay at their seed; clearing it releases + # a repeatable sequence from that seed. Because one broadcast write reseeds + # all channels in the same cycle, every channel stays coherent. pn23_seed = 0b01001101110000000101000 pn23_first = pn_word(pn23_seed, order=23, tap=18) << 2 pn23_state_1 = pn_advance(pn23_seed, order=23, tap=18) pn23_state_2 = pn_advance(pn23_state_1, order=23, tap=18) pn23_second = pn_word(pn23_state_1, order=23, tap=18) << 2 pn23_third = pn_word(pn23_state_2, order=23, tap=18) << 2 - await write(dut, 0, 0x0D, 0x25) - assert await read(dut, 0, 0x0D) == 0x25 - assert channel(await sample(dut), 0) == pn23_first - assert channel(await sample(dut), 0) == pn23_first - await write(dut, 0, 0x0D, 0x05) - assert await read(dut, 0, 0x0D) == 0x05 - assert channel(await sample(dut), 0) == pn23_first - assert channel(await sample(dut), 0) == pn23_second + def all_channels(data): + return [channel(data, i) for i in range(8)] + + await write(dut, 0x0D, 0x25) + assert await read(dut, 0x0D) == 0x25 + assert all_channels(await sample(dut)) == [pn23_first] * 8 + assert all_channels(await sample(dut)) == [pn23_first] * 8 + await write(dut, 0x0D, 0x05) + assert await read(dut, 0x0D) == 0x05 + assert all_channels(await sample(dut)) == [pn23_first] * 8 + assert all_channels(await sample(dut)) == [pn23_second] * 8 # Transferring unchanged resolution/rate configuration must not rewind the # live PN state maintained independently by each channel. - await write(dut, 0, 0xFF, 0x01) - assert channel(await sample(dut), 0) == pn23_third - - # Apply the same hold/release check to bank 1/channel 4's PN9 generator, - # together with global inversion and byte-local LSB-first ordering. - await write(dut, 1, 0x0D, 0x16) - await write(dut, 1, 0x14, 0x04) - await write(dut, 1, 0x21, 0xB0) - assert await read(dut, 1, 0x0D) == 0x16 + await write(dut, 0xFF, 0x01) + assert all_channels(await sample(dut)) == [pn23_third] * 8 + + # Apply the same hold/release check to the PN9 generator, together with + # global inversion and byte-local LSB-first ordering. + await write(dut, 0x0D, 0x16) + await write(dut, 0x14, 0x04) + await write(dut, 0x21, 0xB0) + assert await read(dut, 0x0D) == 0x16 pn9_seed = 0b011011111 pn9_first = reverse_bytes((pn_word(pn9_seed) << 2) ^ 0xFFFF) pn9_second = reverse_bytes( (pn_word(pn_advance(pn9_seed)) << 2) ^ 0xFFFF) - assert channel(await sample(dut), 4) == pn9_first - assert channel(await sample(dut), 4) == pn9_first - await write(dut, 1, 0x0D, 0x06) - assert await read(dut, 1, 0x0D) == 0x06 - assert channel(await sample(dut), 4) == pn9_first - assert channel(await sample(dut), 4) == pn9_second + assert all_channels(await sample(dut)) == [pn9_first] * 8 + assert all_channels(await sample(dut)) == [pn9_first] * 8 + await write(dut, 0x0D, 0x06) + assert await read(dut, 0x0D) == 0x06 + assert all_channels(await sample(dut)) == [pn9_first] * 8 + assert all_channels(await sample(dut)) == [pn9_second] * 8 # A power-mode write immediately suppresses all channels. Soft reset # immediately restores default two-lane normal output and override state. - await write(dut, 0, 0x08, 0x01) + await write(dut, 0x08, 0x01) assert await sample(dut) == 0 - await write(dut, 0, 0x00, 0x04) - assert await read(dut, 0, 0x100) == 0x00 + await write(dut, 0x00, 0x04) + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 data = await sample(dut) assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 5a9d826551..0dab1ac84c 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -205,6 +205,11 @@ def Snapshot(self): def Relock(self): self.relockCount += 1 + def checkGeometry(self): + # These fakes are constructed consistent with their own geometry, so the + # model/RTL capability cross-check is vacuously satisfied here. + pass + class CoupledFakeReadout(FakeReadout): def __init__(self): From df19585ae3e58f40e3c8f959bbcb16c682f0f739 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 10:40:15 -0700 Subject: [PATCH 25/57] Comment out tricky 0x100 register. --- python/surf/devices/analog_devices/_Ad9681.py | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/python/surf/devices/analog_devices/_Ad9681.py b/python/surf/devices/analog_devices/_Ad9681.py index 89925b1e91..f6090125d6 100644 --- a/python/surf/devices/analog_devices/_Ad9681.py +++ b/python/surf/devices/analog_devices/_Ad9681.py @@ -383,44 +383,51 @@ def __init__( }, )) - self.add(pr.RemoteVariable( - name = 'ResolutionSampleRateOverride', - description = 'Enables the resolution and maximum sample-rate override', - offset = (0x100*4), - bitSize = 1, - bitOffset = 6, - base = pr.Bool, - )) - - self.add(pr.RemoteVariable( - name = 'Resolution', - description = 'Selects ADC resolution when the override is enabled', - offset = (0x100*4), - bitSize = 2, - bitOffset = 4, - enum = { - 0b00: 'Default (14 bits)', # power-up/reset value; effective 14-bit - 0b01: '14 bits', - 0b10: '12 bits', - }, - )) - - self.add(pr.RemoteVariable( - name = 'SampleRate', - description = 'Selects maximum ADC sample rate when the override is enabled', - offset = (0x100*4), - bitSize = 3, - bitOffset = 0, - enum = { - 0b000: '20 MSPS', - 0b001: '40 MSPS', - 0b010: '50 MSPS', - 0b011: '65 MSPS', - 0b100: '80 MSPS', - 0b101: '105 MSPS', - 0b110: '125 MSPS', - }, - )) + # Register 0x100 (resolution/sample-rate override) is a transfer-staged + # register: the datasheet specifies it is not applied until the 0xFF + # transfer strobe, and a read returns the old value until then. That + # breaks pyrogue's immediate write-verify, and the override is not needed + # for normal full-rate operation, so these fields are disabled for now. + # Re-enable with verify=False (and route through DeviceUpdate) if the + # override is ever required. + # self.add(pr.RemoteVariable( + # name = 'ResolutionSampleRateOverride', + # description = 'Enables the resolution and maximum sample-rate override', + # offset = (0x100*4), + # bitSize = 1, + # bitOffset = 6, + # base = pr.Bool, + # )) + + # self.add(pr.RemoteVariable( + # name = 'Resolution', + # description = 'Selects ADC resolution when the override is enabled', + # offset = (0x100*4), + # bitSize = 2, + # bitOffset = 4, + # enum = { + # 0b00: 'Default (14 bits)', # power-up/reset value; effective 14-bit + # 0b01: '14 bits', + # 0b10: '12 bits', + # }, + # )) + + # self.add(pr.RemoteVariable( + # name = 'SampleRate', + # description = 'Selects maximum ADC sample rate when the override is enabled', + # offset = (0x100*4), + # bitSize = 3, + # bitOffset = 0, + # enum = { + # 0b000: '20 MSPS', + # 0b001: '40 MSPS', + # 0b010: '50 MSPS', + # 0b011: '65 MSPS', + # 0b100: '80 MSPS', + # 0b101: '105 MSPS', + # 0b110: '125 MSPS', + # }, + # )) self.add(pr.RemoteCommand( name='DeviceUpdate', From 7d77e6d35b2702b21f3c599bce5a5fb08c75f4bc Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 10:41:26 -0700 Subject: [PATCH 26/57] Fix reset sequence to get deterministic delays at start of alignment sequence. --- .../AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd | 43 ++++++++++++++----- .../AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd | 4 ++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd index 9d6b25f900..88aae31bae 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd @@ -107,6 +107,7 @@ architecture rtl of AdcDdrCore is type RegType is record phyReset : sl; startupPending : sl; + resetHold : natural range 0 to ADC_DDR_RESET_HOLD_C; relock : sl; clearCounters : sl; bitSlip : slv(FCO_LANES_G-1 downto 0); @@ -135,6 +136,7 @@ architecture rtl of AdcDdrCore is constant REG_INIT_C : RegType := ( phyReset => '0', startupPending => '1', + resetHold => ADC_DDR_RESET_HOLD_C, relock => '0', clearCounters => '0', bitSlip => (others => '0'), @@ -476,18 +478,39 @@ begin AXI_RESP_DECERR_C, snapshotTxn); ---------------------------------------------------------------------------------------------- - -- Hold the PHY whenever its delay resources are not ready, then apply - -- every retained delay and release alignment automatically. A manual - -- reset rearms the same sequence. The target owns IDELAYCTRL reset and - -- must restore delayReady after any loss of readiness. + -- Deserializer reset and alignment restart + -- + -- Every alignment attempt -- power-up, a lost delay controller, a manual + -- CaptureReset, or a Relock command -- runs the same sequence: hold the + -- PHY (deserializer) reset for a fixed number of captureClk/CLKDIV cycles + -- so a group's FCO and data deserializers all leave reset on the same + -- edge, then reload every retained delay and let alignment restart. A bare + -- Relock that only cleared the lock bits would re-run the bitslip search + -- from whatever independent phase each deserializer happened to hold, + -- aligning the FCO onto data lanes at an arbitrary relative offset. + -- + -- The FCO FSM above already holds its counters cleared and issues no + -- BITSLIP while startupPending is set, and delayReady/startupPending force + -- sampleValid low, so FIFO writes and snapshots stay suppressed until the + -- reset is released. Alignment therefore starts naturally on release from + -- cleared counters; no separate relock strobe is asserted here (that would + -- re-arm this sequence and loop). ---------------------------------------------------------------------------------------------- - if (delayReady = '0' or v.phyReset = '1') then + if (delayReady = '0' or v.phyReset = '1' or r.relock = '1') then + -- (Re)arm the sequence and keep the reset counter charged while any + -- trigger persists. v.startupPending := '1'; - elsif (r.startupPending = '1' and delayReady = '1') then - v.startupPending := '0'; - v.relock := '1'; - v.dataDelayLoad := (others => '1'); - v.fcoDelayLoad := (others => '1'); + v.resetHold := ADC_DDR_RESET_HOLD_C; + elsif (r.startupPending = '1') then + if (r.resetHold /= 0) then + v.resetHold := r.resetHold - 1; + else + -- Reset held long enough with delays ready: release the PHY and + -- reload every retained delay on the same edge. + v.startupPending := '0'; + v.dataDelayLoad := (others => '1'); + v.fcoDelayLoad := (others => '1'); + end if; end if; ---------------------------------------------------------------------------------------------- diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd index d592d1bf52..86ecbb7f94 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd @@ -28,6 +28,10 @@ package AdcDdrPkg is -- ISERDESE2 DDR operation requires three quiet CLKDIV cycles after a -- one-cycle BITSLIP request before the shifted word is evaluated. constant ADC_DDR_BITSLIP_INTERVAL_C : positive := 8; + -- captureClk (CLKDIV) cycles the deserializer reset is held on every relock + -- so that a group's FCO and data deserializers all leave reset on the same + -- edge. Comfortably exceeds the minimum ISERDES/IDELAY reset pulse width. + constant ADC_DDR_RESET_HOLD_C : positive := 8; -- Return the native programmable input-delay width for an FPGA family. function adcDdrDelayBits (deviceFamily : string) return positive; From fa2932cda76ed68988fb4713b4cb5b90901d982c Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 13:00:26 -0700 Subject: [PATCH 27/57] Add pattern check tests. --- devices/AnalogDevices/adcDdr/README.md | 84 +++--- .../adcDdr/rtl/AdcDdrPatternTester.vhd | 82 +++++- .../AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd | 1 + .../serialized-ddr-adc-cleanup/README.md | 53 ++++ .../analog_devices/_AdcDdrCalibration.py | 260 ++++++++++++------ .../analog_devices/_AdcDdrPatternTester.py | 13 +- .../analog_devices/test_AdcDdrCalibration.py | 240 +++++++++++++--- .../analog_devices/test_AdcDdrModel.py | 18 +- .../test_AdcDdrPatternTester.py | 126 ++++++++- 9 files changed, 688 insertions(+), 189 deletions(-) create mode 100644 docs/plans/serialized-ddr-adc-cleanup/README.md diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index 2f67f824e9..1f3a51706b 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -86,15 +86,24 @@ The `Full calibration` operation performs these phases: though each pair contributes to the same logical channel. 6. **Scan each data group.** At a given tap, all lanes in the group are updated in one bulk transaction and then measured from the same settled sampling - point. A per-lane verdict uses only that lane's logical sample-bit mask. + point. Ordered four-sample snapshots acquire phase independently per lane so + one channel leaving its eye cannot truncate another channel's passing window. + A per-lane verdict uses only its logical sample-bit mask, so an unaligned + partner half does not contaminate it. Each lane gets its own passing map and centered eye even though compatible lanes are swept together. 7. **Qualify the assembled checkerboard samples.** With every data lane centered, software checks the complete sample width. Channel zero establishes the checkerboard A/B phase, and every logical channel must match the same ordered sequence. - The FCO lanes must also pass. This catches half-word or sample-epoch errors - that individual masked lane tests and a repetitive FCO word cannot detect. -8. **Qualify PN23 coherence and recurrence.** When `VerifyPn23` is enabled, + This catches half-word or sample-epoch errors that individual masked lane + tests and a repetitive FCO word cannot detect. +8. **Run the optional deep checkerboard window.** When `UsePatternTester` is + enabled, the hardware pattern tester checks `PatternTesterSamples` complete + samples at line rate. Every channel must maintain one shared alternating + phase, and every selected FCO word observed during the window must match. + Per-channel word-error counts and accumulated bit-error masks, plus per-FCO + error counts, are retained in the final result. +9. **Qualify PN23 coherence and recurrence.** When `VerifyPn23` is enabled, software switches the ADC to PN23, pulses the PN-long reset, and reads one atomic four-sample debug snapshot. All logical channels must contain the same four words. The reference channel's 56 captured bits must also satisfy @@ -102,16 +111,20 @@ The `Full calibration` operation performs these phases: arbitrary phase. This second condition rejects a common malformed sequence that channel equality alone would accept. The checker considers the ADC's possible sample-MSB format conversion and full output inversion; it does not - require the snapshot to begin at the PN seed. -9. **Try alternate FCO windows if necessary.** More than one FCO eye can appear + require the snapshot to begin at the PN seed. When `UsePatternTester` is + enabled, the selected transformation is then applied to a deep hardware + window. The tester acquires an arbitrary nonzero 23-bit history, checks all + subsequent reference bits against the recurrence, and verifies every other + channel remains word-for-word coherent with the reference. +10. **Try alternate FCO windows if necessary.** More than one FCO eye can appear valid because the repetitive frame pattern may lock in equivalent-looking - unit intervals. If either checkerboard or PN23 qualification fails, + unit intervals. If the snapshot, deep checkerboard, or PN23 qualification fails, calibration tries the Cartesian product of the retained FCO eyes, relocking and restoring checkerboard mode before each attempt. Data delays are not rescanned because the FCO choice establishes frame/sample phase without changing the data-eye centers. The first combination that passes both final checks is retained. -10. **Publish or restore.** A successful full calibration restores the normal +11. **Publish or restore.** A successful full calibration restores the normal ADC output mode but leaves the qualified FCO and data delays installed. A stop, exception, or failed final qualification restores the original test mode and delays. `ApplyResults` can later reinstall only a complete result @@ -121,28 +134,23 @@ This is an eye-centering algorithm, not a bit-error-rate characterization. A passing tap means every bounded measurement requested by `SampleCount` passed; it does not prove an arbitrarily low error rate. -### Measurement backends - -`UsePatternTester` selects how data taps and final alignment are judged: - -- **Debug snapshots (`False`, default):** each `Snapshot` atomically publishes - four oldest-to-newest samples for every logical channel. One software - `SampleCount` unit requests one four-sample snapshot. The same coherent - snapshot is reused for every lane mask in a sweep group. -- **Hardware pattern tester (`True`):** the bounded checker in - `AdcDdrPatternTester` evaluates the requested channels and sample-bit mask. - One software `SampleCount` unit requests four hardware samples. A group with - more than one distinct lane mask requires one hardware measurement window per - mask. Alternating mode uses a single reference channel so channels cannot - independently accept opposite checkerboard phases. Every selected FCO lane - must produce at least one valid frame word during the window and every - observed word must match the configured frame pattern. - -The pattern-tester backend requires `PatternCheck` to report that the RTL block -is present. Both backends use the same eye extraction and final FCO-combination -logic. PN23 final qualification always uses the atomic debug snapshot, even -when the hardware pattern tester handles the checkerboard measurements; the -current hardware tester implements only constant and alternating patterns. +### Snapshot and deep measurements + +Every data tap and the initial full-channel qualification use atomic debug +snapshots. One `SampleCount` unit requests one four-sample snapshot, and the +same coherent samples are reused for every lane mask in a sweep group. The +checker enforces an ordered pattern independently per lane instead of merely +requiring both checkerboard values to appear. The final assembled snapshot and +deep hardware window then require one shared phase across all channels. + +`UsePatternTester` adds deep full-channel measurements after the centered +snapshots pass; it does not replace the tap-scan backend. The bounded checker +in `AdcDdrPatternTester` evaluates `PatternTesterSamples` consecutive valid +samples without transferring them to software. Alternating mode uses one +reference channel for the complete channel group. PN23 mode checks the +reference recurrence from an arbitrary stream phase and checks every other +channel for coherence. Every selected FCO lane must also be observed and remain +error-free. The deep checker requires the `PatternCheck` capability. ### Verification operations @@ -165,9 +173,10 @@ verification operation ends. | `GuardBand` | `2` taps | Required passing margin on each side of the selected center. | | `CircularDelays` | `False` | Whether the first and last passing scan runs are one wrapped eye. | | `SampleCount` | `2` | Number of four-sample measurement groups checked at each data tap. | -| `VerifyPn23` | Device dependent | Enable the four-sample PN23 coherence and recurrence check when the adapter provides a PN-long reset control. | +| `UsePatternTester` | `False` | Add deep hardware checkerboard and, when enabled, PN23 qualification after centering. | +| `PatternTesterSamples` | `4096` | Valid samples checked by each deep hardware window. | +| `VerifyPn23` | Device dependent | Enable PN23 coherence and recurrence qualification when the adapter provides a PN-long reset control. | | `SettleTime` | `1 ms` | Wall-clock wait after delay, relock, or ADC test-mode changes. | -| `UsePatternTester` | `False` | Select hardware checking instead of coherent debug snapshots. | | `Debug` | `True` | Publish detailed per-tap diagnostics while the operation runs. | The full delay range is the safest initial scan. Narrow `DelayStart` and @@ -181,10 +190,13 @@ merges it with the opposite boundary. `Results` contains the selected eye, complete passing map, and margins for each FCO and data lane. FCO entries also contain every retained candidate eye. The `Final` entry contains full-channel qualification captures plus every attempted -FCO-eye combination. When enabled, `Final.pn23` reports the four samples from -every channel, channel-coherence results, each recurrence transformation tried, -and the selected valid transformation. A failed scan may publish partial -results for the lane that could not produce a qualifying eye. +FCO-eye combination. `Final.patternTester` reports whether the optional deep +window ran, its requested and checked sample counts, channel/FCO pass masks, +and detailed error counts and masks. When enabled, `Final.pn23` reports the four +samples from every channel, channel-coherence results, each recurrence +transformation tried, the selected valid transformation, and the nested deep +pattern-tester result. A failed scan may publish partial results for the lane +that could not produce a qualifying eye. `Diagnostics` contains the measurement backend, expected patterns, raw and masked data captures, FCO words and lock masks, and the currently active scan diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd index 8c048165d9..f170465603 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd @@ -44,10 +44,13 @@ end entity AdcDdrPatternTester; architecture rtl of AdcDdrPatternTester is + constant PN23_MIN_SAMPLES_C : positive := (23/SAMPLE_WIDTH_G)+1; + type RegType is record start : sl; abort : sl; cfgAlternating : sl; + cfgPn23 : sl; cfgReference : slv(7 downto 0); cfgChannelMask : slv(CHANNELS_G-1 downto 0); cfgFcoMask : slv(FCO_LANES_G-1 downto 0); @@ -66,6 +69,7 @@ architecture rtl of AdcDdrPatternTester is allChannelsPass : sl; allFcoPass : sl; alternating : sl; + pn23 : sl; referenceChannel : slv(7 downto 0); channelMask : slv(CHANNELS_G-1 downto 0); fcoMask : slv(FCO_LANES_G-1 downto 0); @@ -75,6 +79,8 @@ architecture rtl of AdcDdrPatternTester is requestedSamples : slv(31 downto 0); noValidTimeout : slv(31 downto 0); noValidCount : slv(31 downto 0); + pnHistory : slv(22 downto 0); + pnHistoryCount : natural range 0 to 23; completionSeq : slv(31 downto 0); checkedSamples : slv(31 downto 0); channelPassed : slv(CHANNELS_G-1 downto 0); @@ -91,6 +97,7 @@ architecture rtl of AdcDdrPatternTester is start => '0', abort => '0', cfgAlternating => '0', + cfgPn23 => '0', cfgReference => (others => '0'), cfgChannelMask => (others => '1'), cfgFcoMask => (others => '1'), @@ -109,6 +116,7 @@ architecture rtl of AdcDdrPatternTester is allChannelsPass => '0', allFcoPass => '0', alternating => '0', + pn23 => '0', referenceChannel => (others => '0'), channelMask => (others => '0'), fcoMask => (others => '0'), @@ -118,6 +126,8 @@ architecture rtl of AdcDdrPatternTester is requestedSamples => (others => '0'), noValidTimeout => (others => '0'), noValidCount => (others => '0'), + pnHistory => (others => '0'), + pnHistoryCount => 0, completionSeq => (others => '0'), checkedSamples => (others => '0'), channelPassed => (others => '0'), @@ -145,6 +155,11 @@ begin variable actual : slv(SAMPLE_WIDTH_G-1 downto 0); variable expected : slv(SAMPLE_WIDTH_G-1 downto 0); variable errorBits : slv(SAMPLE_WIDTH_G-1 downto 0); + variable referenceWord : slv(SAMPLE_WIDTH_G-1 downto 0); + variable pnErrorBits : slv(SAMPLE_WIDTH_G-1 downto 0); + variable pnHistory : slv(22 downto 0); + variable pnHistoryCount : natural range 0 to 23; + variable pnExpected : sl; variable referenceIndex : natural range 0 to 255; variable selectedPhase : sl; variable phaseValid : sl; @@ -180,7 +195,38 @@ begin phaseValid := '1'; selectedPhase := '0'; - if (r.alternating = '1') then + if (r.pn23 = '1') then + referenceIndex := to_integer(unsigned(r.referenceChannel)); + referenceWord := (sampleIn(referenceIndex)(SAMPLE_WIDTH_G-1 downto 0) xor + r.patternA) and r.dataMask; + pnHistory := r.pnHistory; + pnHistoryCount := r.pnHistoryCount; + pnErrorBits := (others => '0'); + for bitindex in SAMPLE_WIDTH_G-1 downto 0 loop + if (pnHistoryCount < 23) then + pnHistory := pnHistory(21 downto 0) & referenceWord(bitindex); + pnHistoryCount := pnHistoryCount + 1; + if (pnHistoryCount = 23) then + if uOr(pnHistory) = '1' then + v.phaseAcquired := '1'; + else + -- The all-zero state satisfies the recurrence but is + -- not part of the maximal-length PN23 sequence. + pnErrorBits := (others => '1'); + end if; + end if; + else + pnExpected := pnHistory(22) xor pnHistory(17); + if (referenceWord(bitindex) /= pnExpected) then + pnErrorBits(bitindex) := '1'; + end if; + pnHistory := pnHistory(21 downto 0) & referenceWord(bitindex); + end if; + end loop; + v.pnHistory := pnHistory; + v.pnHistoryCount := pnHistoryCount; + + elsif (r.alternating = '1') then if (r.phaseAcquired = '1') then selectedPhase := r.expectedPhase; else @@ -202,7 +248,15 @@ begin for i in CHANNELS_G-1 downto 0 loop if (r.channelMask(i) = '1') then - if (phaseValid = '0') then + if (r.pn23 = '1') then + if (i = referenceIndex) then + errorBits := pnErrorBits; + else + actual := (sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) xor + r.patternA) and r.dataMask; + errorBits := actual xor referenceWord; + end if; + elsif (phaseValid = '0') then errorBits := r.dataMask; else if (selectedPhase = '0') then @@ -267,6 +321,9 @@ begin end loop; v.allChannelsPass := channelsPassing; v.allFcoPass := fcoPassing; + if (r.pn23 = '1' and v.phaseAcquired = '0') then + v.allChannelsPass := '0'; + end if; end if; elsif (r.start = '1') then @@ -279,6 +336,7 @@ begin v.allChannelsPass := '0'; v.allFcoPass := '0'; v.alternating := r.cfgAlternating; + v.pn23 := r.cfgPn23; v.referenceChannel := r.cfgReference; v.channelMask := r.cfgChannelMask; v.fcoMask := r.cfgFcoMask; @@ -288,6 +346,8 @@ begin v.requestedSamples := r.cfgSamples; v.noValidTimeout := r.cfgTimeout; v.noValidCount := (others => '0'); + v.pnHistory := (others => '0'); + v.pnHistoryCount := 0; v.checkedSamples := (others => '0'); v.channelPassed := (others => '0'); v.fcoPassed := (others => '0'); @@ -300,14 +360,24 @@ begin uOr(r.cfgDataMask) = '0') then invalidConfig := '1'; end if; - if (r.cfgAlternating = '1') then + if (r.cfgAlternating = '1' and r.cfgPn23 = '1') then + invalidConfig := '1'; + end if; + if (r.cfgAlternating = '1' or r.cfgPn23 = '1') then referenceIndex := to_integer(unsigned(r.cfgReference)); if (referenceIndex >= CHANNELS_G) then invalidConfig := '1'; elsif (r.cfgChannelMask(referenceIndex) = '0') then invalidConfig := '1'; end if; - if ((r.cfgPatternA and r.cfgDataMask) = (r.cfgPatternB and r.cfgDataMask)) then + if (r.cfgAlternating = '1' and + (r.cfgPatternA and r.cfgDataMask) = (r.cfgPatternB and r.cfgDataMask)) then + invalidConfig := '1'; + end if; + end if; + if (r.cfgPn23 = '1') then + if uAnd(r.cfgDataMask) = '0' or + unsigned(r.cfgSamples) < to_unsigned(PN23_MIN_SAMPLES_C, 32) then invalidConfig := '1'; end if; end if; @@ -316,7 +386,7 @@ begin v.configError := '1'; else v.busy := '1'; - v.phaseAcquired := not r.cfgAlternating; + v.phaseAcquired := not r.cfgAlternating and not r.cfgPn23; end if; end if; @@ -330,6 +400,8 @@ begin axiSlaveRegister(ep, ADC_DDR_PATTERN_ABORT_ADDR_C, 0, v.abort); axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, ADC_DDR_PATTERN_ALTERNATING_BIT_C, v.cfgAlternating); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, + ADC_DDR_PATTERN_PN23_BIT_C, v.cfgPn23); axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, ADC_DDR_PATTERN_REFERENCE_OFFSET_C, v.cfgReference); axiSlaveRegister(ep, ADC_DDR_PATTERN_CHANNEL_MASK_ADDR_C, 0, v.cfgChannelMask); diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd index 86ecbb7f94..1f26b9a3fd 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd @@ -84,6 +84,7 @@ package AdcDdrPkg is -- Pattern measurement command and status bit assignments. constant ADC_DDR_PATTERN_ALTERNATING_BIT_C : natural := 0; + constant ADC_DDR_PATTERN_PN23_BIT_C : natural := 1; constant ADC_DDR_PATTERN_REFERENCE_OFFSET_C : natural := 8; constant ADC_DDR_PATTERN_BUSY_BIT_C : natural := 0; constant ADC_DDR_PATTERN_TIMEOUT_BIT_C : natural := 1; diff --git a/docs/plans/serialized-ddr-adc-cleanup/README.md b/docs/plans/serialized-ddr-adc-cleanup/README.md new file mode 100644 index 0000000000..ed69aef25d --- /dev/null +++ b/docs/plans/serialized-ddr-adc-cleanup/README.md @@ -0,0 +1,53 @@ +# Serialized DDR ADC Cleanup Follow-Up + +## Goal + +Complete real-hardware validation of the normalized ADC DDR readout and keep +the alignment/calibration path deterministic, fast, and diagnostically useful. + +## Current Status + +- AD9681 register `0x100` fields remain intentionally absent from the PyRogue + model because hardware readback did not support verified writes. +- Relock now resets the deserializers for a bounded interval before reloading + retained delays and restarting FCO alignment. +- Hardware AD9681 calibration produces bounded eyes for all sixteen physical + data lanes and completes successfully. +- Calibration tap scans use coherent ordered snapshots. `UsePatternTester` + enables one deep final checkerboard window instead of replacing every tap + measurement with repeated hardware windows. + +## Deep Qualification Contract + +- `SampleCount` controls the shallow four-sample snapshot depth at each tap. +- `PatternTesterSamples` controls the final hardware checkerboard depth and + defaults to 4096 valid samples per deep window. +- The deep result retains per-channel word-error counts and accumulated + bit-error masks plus per-FCO error counts. +- A deep-check failure participates in the existing alternate-FCO-eye retry. +- PN23 first uses the four-sample snapshot to select the ADC output/format + transformation. The hardware tester then acquires an arbitrary nonzero + 23-bit history and deeply checks reference-channel recurrence plus + word-for-word coherence across every other enabled channel. + +## Validation + +Focused PyRogue model and calibration regressions cover strict snapshot order, +deep-check success and failure, detailed error reporting, capability rejection, +alternate FCO-eye retries, shallow and deep PN23 qualification, and state +restoration. + +- `test_AdcDdrCalibration.py` and `test_AdcDdrModel.py`: 69 passed. +- `test_AdcDdrPatternTester.py`: 1 passed with GHDL/cocotb. +- VSG: no violations in `AdcDdrPkg.vhd` or `AdcDdrPatternTester.vhd`. + +## Open Hardware Checks + +- Compare normal calibration run time with deep qualification disabled and + enabled. +- Confirm a 4096-sample deep window reports all-zero channel and FCO errors on + the target board. +- Confirm the 4096-sample PN23 window acquires phase and reports all-zero + reference recurrence and cross-channel coherence errors. +- Increase `PatternTesterSamples` if a longer bounded checkerboard stress test + is useful; this remains qualification, not a claimed BER measurement. diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index 4376c2b195..e53b6e6cae 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -337,6 +337,7 @@ class AdcDdrCalibration(pr.Process): VERIFY_CURRENT_C = 1 VERIFY_GUARD_BAND_C = 2 RUN_TIME_UPDATE_INTERVAL_C = 1.0 + PATTERN_TESTER_TIMEOUT_C = 256 def __init__( self, @@ -416,7 +417,7 @@ def __init__( self._pn23Reset = pn23Reset self._runDiagnostics = {} self._lastCaptureDiagnostics = {} - self._patternTesterActive = False + self._deepPatternTesterActive = False kwargs.setdefault( 'description', @@ -454,10 +455,20 @@ def __init__( self.add(pr.LocalVariable( name = 'UsePatternTester', - description = 'Use the hardware pattern tester instead of debug snapshots', + description = ( + 'Run deep hardware checkerboard and optional PN23 qualification ' + 'after centering'), value = False, mode = 'RW')) + self.add(pr.LocalVariable( + name = 'PatternTesterSamples', + description = 'Valid samples checked by each deep hardware qualification window', + value = 4096, + minimum = 1, + maximum = 0xFFFFFFFF, + mode = 'RW')) + self.add(pr.LocalVariable( name = 'SampleCount', description = 'Four-sample groups checked at each data tap', @@ -570,7 +581,14 @@ def _recordDiagnostic( if publish or self.Debug.value(): self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) - def _configurePatternTester(self, channels: Sequence[int], mask: int) -> None: + def _configurePatternTester( + self, + channels: Sequence[int], + mask: int, + samples: int, + *, + pn23: bool = False, + xorMask: int = 0) -> None: """Program one hardware measurement window for a channel/mask set.""" patterns = sorted(self._expectedPatterns) @@ -578,16 +596,19 @@ def _configurePatternTester(self, channels: Sequence[int], mask: int) -> None: # Alternating patterns share one phase reference across every enabled # channel. This prevents each logical channel from independently calling # an opposite checkerboard phase valid. - tester.Alternating.set(len(patterns) == 2, write=True) + tester.Alternating.set(not pn23 and len(patterns) == 2, write=True) + tester.Pn23.set(pn23, write=True) tester.ReferenceChannel.set(channels[0], write=True) tester.ChannelMask.set(sum(1 << channel for channel in channels), write=True) tester.FcoMask.set((1 << self._fcoLanes)-1, write=True) tester.DataMask.set(mask, write=True) - tester.PatternA.set(patterns[0], write=True) - tester.PatternB.set(patterns[-1], write=True) - # One software SampleCount unit represents the same four ADC samples as - # one atomic debug snapshot, keeping the two backends comparable. - tester.Samples.set(4*self.SampleCount.value(), write=True) + tester.PatternA.set(xorMask if pn23 else patterns[0], write=True) + tester.PatternB.set(0 if pn23 else patterns[-1], write=True) + tester.Samples.set(samples, write=True) + # Do not inherit mutable state from manual uses of the child device. + # This timeout detects a stopped producer without limiting a healthy + # continuous sample window. + tester.Timeout.set(self.PATTERN_TESTER_TIMEOUT_C, write=True) def _runPatternTester(self) -> dict[str, Any]: """Start one hardware window and return its stable retained results.""" @@ -624,62 +645,45 @@ def _runPatternTester(self) -> dict[str, Any]: if status['aborted']: raise RuntimeError('ADC pattern tester measurement was aborted') + channelPassed = int(tester.ChannelPassed.get(read=True)) + fcoPassed = int(tester.FcoPassed.get(read=True)) + channels = { + channel: { + 'passed': bool(channelPassed & (1 << channel)), + 'wordErrorCount': int(tester.WordErrorCount[channel].get(read=True)), + 'bitErrorMask': int(tester.BitErrorMask[channel].get(read=True)), + } + for channel in range(self._channels) + } + fco = { + lane: { + 'passed': bool(fcoPassed & (1 << lane)), + 'errorCount': int(tester.FcoErrorCount[lane].get(read=True)), + } + for lane in range(self._fcoLanes) + } return { 'sequence': current, 'checkedSamples': int(tester.CheckedSamples.get(read=True)), - 'channelPassed': int(tester.ChannelPassed.get(read=True)), - 'fcoPassed': int(tester.FcoPassed.get(read=True)), + 'channelPassed': channelPassed, + 'fcoPassed': fcoPassed, + 'channels': channels, + 'fco': fco, 'status': status, } - def _captureGroupPassesPattern( - self, - lanes: Sequence[int]) -> dict[int, dict[str, Any]]: - """Measure a parallel lane group with the hardware pattern tester.""" - - # The hardware checker has one DataMask per window. A mixed-mask group - # still shares one programmed delay tap, but requires one measurement - # for each distinct mask to preserve independent lane verdicts. - lanesByMask = {} - for lane in lanes: - lanesByMask.setdefault(self._dataLaneMasks[lane], []).append(lane) - - details = {} - for mask, maskedLanes in lanesByMask.items(): - channels = sorted({self._dataLaneToChannel[lane] for lane in maskedLanes}) - self._configurePatternTester(channels, mask) - measurement = self._runPatternTester() - fcoMask = (1 << self._fcoLanes)-1 - fcoPassed = (measurement['fcoPassed'] & fcoMask) == fcoMask - # ChannelPassed is reported per logical channel. Map that verdict - # back to every physical lane contributing the mask being measured. - for lane in maskedLanes: - details[lane] = { - 'channel': self._dataLaneToChannel[lane], - 'mask': mask, - 'expected': sorted({ - pattern & mask - for pattern in self._expectedPatterns}), - 'captures': [copy.deepcopy(measurement)], - 'passed': ( - bool(measurement['channelPassed'] & - (1 << self._dataLaneToChannel[lane])) and - fcoPassed), - } - return details - def _captureGroupPassesSnapshot( self, lanes: Sequence[int]) -> dict[int, dict[str, Any]]: - """Measure a parallel lane group from atomic four-sample snapshots.""" + """Measure ordered per-lane patterns from atomic snapshots.""" + patterns = sorted(self._expectedPatterns) details = { lane: { 'channel': self._dataLaneToChannel[lane], 'mask': self._dataLaneMasks[lane], - 'expected': sorted({ - pattern & self._dataLaneMasks[lane] - for pattern in self._expectedPatterns}), + 'expected': sorted({pattern & self._dataLaneMasks[lane] + for pattern in patterns}), 'captures': [], 'passed': True, } @@ -697,13 +701,30 @@ def _captureGroupPassesSnapshot( for channel in channels } for lane, detail in details.items(): + mask = detail['mask'] + maskedPatterns = detail['expected'] raw = rawByChannel[detail['channel']] - masked = [sample & detail['mask'] for sample in raw] - # Eye discovery only needs both expected checkerboard values; - # strict temporal phase is deferred until all lanes are centered. - capturePassed = sorted(set(masked)) == detail['expected'] + masked = [sample & mask for sample in raw] + try: + phase = maskedPatterns.index(masked[0]) + except ValueError: + phase = None + expected = [] + else: + expected = [ + maskedPatterns[(phase+index) % len(maskedPatterns)] + for index in range(len(masked)) + ] + + # Each physical lane acquires phase independently during the + # scan so one channel leaving its eye cannot truncate another + # channel's passing window. The assembled final checks impose + # one shared phase after every lane has been centered. + capturePassed = phase is not None and masked == expected detail['captures'].append({ 'sequence': snapshotSequence, + 'phase': phase, + 'expectedSequence': expected, 'raw': raw, 'masked': masked, 'passed': capturePassed, @@ -714,11 +735,9 @@ def _captureGroupPassesSnapshot( def _captureGroupPasses( self, lanes: Sequence[int]) -> dict[int, dict[str, Any]]: - """Dispatch one grouped measurement to the selected backend.""" + """Measure one grouped data tap using coherent debug snapshots.""" - if not self._patternTesterActive: - return self._captureGroupPassesSnapshot(lanes) - return self._captureGroupPassesPattern(lanes) + return self._captureGroupPassesSnapshot(lanes) def _capturePasses(self, lane: int) -> bool: """Compatibility helper for verifying one physical data lane.""" @@ -732,27 +751,6 @@ def _captureFinalPasses(self) -> dict[str, Any]: mask = (1 << self._readout._sampleBits)-1 patterns = sorted(self._expectedPatterns) - if self._patternTesterActive: - self._configurePatternTester(list(range(self._channels)), mask) - measurement = self._runPatternTester() - channels = { - channel: { - 'passed': bool(measurement['channelPassed'] & (1 << channel)), - } - for channel in range(self._channels) - } - capture = copy.deepcopy(measurement) - capture['channels'] = channels - return { - 'mask': mask, - 'expected': patterns, - 'referenceChannel': 0, - 'captures': [capture], - 'passed': ( - all(channel['passed'] for channel in channels.values()) and - measurement['status']['allFcoPass']), - } - detail = { 'mask': mask, 'expected': patterns, @@ -811,6 +809,49 @@ def _captureFinalPasses(self) -> dict[str, Any]: return detail + def _captureDeepPatternPasses(self) -> dict[str, Any]: + """Run one deep full-channel checkerboard measurement in hardware.""" + + mask = (1 << self._readout._sampleBits)-1 + channels = list(range(self._channels)) + samples = int(self.PatternTesterSamples.value()) + self._configurePatternTester(channels, mask, samples) + measurement = self._runPatternTester() + measurement['enabled'] = True + measurement['performed'] = True + measurement['mode'] = 'Checkerboard' + measurement['requestedSamples'] = samples + measurement['passed'] = ( + measurement['checkedSamples'] == samples and + measurement['status']['allChannelsPass'] and + measurement['status']['allFcoPass']) + return measurement + + def _captureDeepPn23Passes(self, xorMask: int) -> dict[str, Any]: + """Run one arbitrary-phase PN23 recurrence/coherence window.""" + + mask = (1 << self._readout._sampleBits)-1 + channels = list(range(self._channels)) + samples = int(self.PatternTesterSamples.value()) + self._configurePatternTester( + channels, + mask, + samples, + pn23=True, + xorMask=xorMask) + measurement = self._runPatternTester() + measurement['enabled'] = True + measurement['performed'] = True + measurement['mode'] = 'Pn23' + measurement['xorMask'] = xorMask + measurement['requestedSamples'] = samples + measurement['passed'] = ( + measurement['checkedSamples'] == samples and + measurement['status']['phaseAcquired'] and + measurement['status']['allChannelsPass'] and + measurement['status']['allFcoPass']) + return measurement + def _setPn23Mode(self) -> None: """Select PN23 and synchronously restart every selected generator.""" @@ -1054,23 +1095,52 @@ def _fullCalibration( checkerboard = self._captureFinalPasses() candidate = copy.deepcopy(checkerboard) - candidate['checkerboardPassed'] = checkerboard['passed'] + candidate['snapshotPassed'] = checkerboard['passed'] + candidate['patternTester'] = { + 'enabled': self._deepPatternTesterActive, + 'performed': False, + 'requestedSamples': ( + int(self.PatternTesterSamples.value()) + if self._deepPatternTesterActive else 0), + 'passed': not self._deepPatternTesterActive, + } + if checkerboard['passed'] and self._deepPatternTesterActive: + self.Message.set('Running deep hardware checkerboard qualification') + candidate['patternTester'] = self._captureDeepPatternPasses() + candidate['checkerboardPassed'] = ( + checkerboard['passed'] and candidate['patternTester']['passed']) candidate['pn23'] = { 'enabled': bool(self.VerifyPn23.value()), 'performed': False, 'passed': not bool(self.VerifyPn23.value()), } - if checkerboard['passed'] and self.VerifyPn23.value(): + if candidate['checkerboardPassed'] and self.VerifyPn23.value(): self.Message.set('Checking final PN23 channel coherence and recurrence') self._setPn23Mode() time.sleep(settle) pn23 = self._capturePn23Passes() pn23['enabled'] = True pn23['performed'] = True + pn23['snapshotPassed'] = pn23['passed'] + pn23['patternTester'] = { + 'enabled': self._deepPatternTesterActive, + 'performed': False, + 'requestedSamples': ( + int(self.PatternTesterSamples.value()) + if self._deepPatternTesterActive else 0), + 'passed': not self._deepPatternTesterActive, + } + if pn23['snapshotPassed'] and self._deepPatternTesterActive: + self.Message.set('Running deep hardware PN23 qualification') + pn23['patternTester'] = self._captureDeepPn23Passes( + int(pn23['recurrence']['selected']['xorMask'])) + pn23['passed'] = ( + pn23['snapshotPassed'] and pn23['patternTester']['passed']) candidate['pn23'] = pn23 candidate['passed'] = pn23['passed'] else: - candidate['passed'] = checkerboard['passed'] and candidate['pn23']['passed'] + candidate['passed'] = ( + candidate['checkerboardPassed'] and candidate['pn23']['passed']) # Retain every attempted combination and its raw qualification so a # failed calibration explains exactly which phase choices were tried. @@ -1209,15 +1279,23 @@ def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: if start > stop: raise ValueError('DelayStart must not be greater than DelayStop') - self._patternTesterActive = bool(self.UsePatternTester.value()) - if self._patternTesterActive: + operation = self.Operation.value() + self._deepPatternTesterActive = ( + bool(self.UsePatternTester.value()) and operation == self.FULL_C) + if self._deepPatternTesterActive: # Fail before changing ADC state if the selected readout cannot - # perform the requested hardware-backed measurement. + # perform the requested deep hardware qualification. if not bool(self._readout.PatternCheck.get(read=True)): raise RuntimeError('ADC pattern tester is not present in this readout') if len(self._expectedPatterns) > 2: raise RuntimeError( 'ADC pattern tester supports at most two expected patterns') + pn23Minimum = (23//self._readout._sampleBits)+1 + if (self.VerifyPn23.value() and + self.PatternTesterSamples.value() < pn23Minimum): + raise ValueError( + f'PatternTesterSamples must be at least {pn23Minimum} ' + f'for {self._readout._sampleBits}-bit PN23 verification') # Snapshot every mutable hardware setting before the operation. Verify, # failure, and stop paths restore these values in the common finally @@ -1225,14 +1303,18 @@ def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: originalMode = self._config.OutputTestMode.get(read=True) originalFco = [variable.get(read=True) for variable in self._readout.FcoDelay.values()] originalData = self._readout._getDataDelays(read=True) - operation = self.Operation.value() results = {} retainResults = False self._runDiagnostics = { 'TestMode': self._testMode, 'ExpectedPatterns': sorted(self._expectedPatterns), - 'MeasurementBackend': ( - 'PatternTester' if self._patternTesterActive else 'Snapshot'), + 'MeasurementBackend': 'Snapshot', + 'DeepPatternTester': { + 'enabled': self._deepPatternTesterActive, + 'samples': ( + int(self.PatternTesterSamples.value()) + if self._deepPatternTesterActive else 0), + }, 'VerifyPn23': bool(self.VerifyPn23.value()), 'Fco': {}, 'Data': {}, diff --git a/python/surf/devices/analog_devices/_AdcDdrPatternTester.py b/python/surf/devices/analog_devices/_AdcDdrPatternTester.py index d329a38a21..e622222434 100644 --- a/python/surf/devices/analog_devices/_AdcDdrPatternTester.py +++ b/python/surf/devices/analog_devices/_AdcDdrPatternTester.py @@ -79,9 +79,18 @@ def __init__( mode = 'RW', base = pr.Bool)) + self.add(pr.RemoteVariable( + name = 'Pn23', + description = 'Check arbitrary-phase PN23 recurrence and cross-channel coherence', + offset = 0x008, + bitSize = 1, + bitOffset = 1, + mode = 'RW', + base = pr.Bool)) + self.add(pr.RemoteVariable( name = 'ReferenceChannel', - description = 'Enabled logical channel used to acquire shared A/B phase', + description = 'Enabled channel used for shared A/B phase or PN23 recurrence', offset = 0x008, bitSize = 8, bitOffset = 8, @@ -120,7 +129,7 @@ def __init__( self.add(pr.RemoteVariable( name = 'PatternA', - description = 'Constant pattern or first alternating pattern', + description = 'Constant/first alternating pattern, or PN23 sample XOR mask', offset = 0x018, bitSize = sampleBits, mode = 'RW', diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 0dab1ac84c..99fc32711b 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -16,9 +16,9 @@ # - Checks: Verify selected taps, physical margins, boundary visibility, # wraparound reporting, deterministic ties, invalid-input rejection, and # topology-aware, overlap-safe parallel grouping plus restoration between -# guard checks for coupled physical lanes. Exercise snapshot and pattern-tester -# measurement backends plus verification progress, final qualification, and -# live run-time reporting. +# guard checks for coupled physical lanes. Exercise strict ordered snapshot +# scanning, deep hardware qualification, verification progress, final +# qualification, and live run-time reporting. # - Timing: Fake register access and zero settling keep process tests short; the # live timer regression temporarily reduces its update interval. @@ -68,6 +68,7 @@ class FakePatternTester: def __init__(self, readout): self._readout = readout self.Alternating = FakeVariable(False) + self.Pn23 = FakeVariable(False) self.ReferenceChannel = FakeVariable(0) self.ChannelMask = FakeVariable(0) self.FcoMask = FakeVariable(0) @@ -75,10 +76,14 @@ def __init__(self, readout): self.PatternA = FakeVariable(0) self.PatternB = FakeVariable(0) self.Samples = FakeVariable(0) + self.Timeout = FakeVariable(0) self.Sequence = FakeVariable(0) self.CheckedSamples = FakeVariable(0) self.ChannelPassed = FakeVariable(0) self.FcoPassed = FakeVariable(0) + self.WordErrorCount = {index: FakeVariable(0) for index in range(16)} + self.BitErrorMask = {index: FakeVariable(0) for index in range(16)} + self.FcoErrorCount = {index: FakeVariable(0) for index in range(16)} self.Busy = FakeVariable(False) self.TimedOut = FakeVariable(False) self.ConfigError = FakeVariable(False) @@ -90,37 +95,84 @@ def __init__(self, readout): def Start(self): alternating = bool(self.Alternating.value()) + pn23 = bool(self.Pn23.value()) mask = self.DataMask.value() patternA = self.PatternA.value() & mask patternB = self.PatternB.value() & mask sampleCount = self.Samples.value() channelMask = self.ChannelMask.value() + referenceChannel = self.ReferenceChannel.value() + rawReference = [ + self._readout._debugSample(referenceChannel, index) & mask + for index in range(sampleCount) + ] + reference = [sample ^ patternA for sample in rawReference] if pn23 else rawReference + pnErrorBits = [0] * sampleCount + if pn23: + bits = [ + (sample >> bitIndex) & 1 + for sample in reference + for bitIndex in range(self._readout._sampleBits-1, -1, -1) + ] + phase = len(bits) >= 23 and any(bits[:23]) + for index in range(23, len(bits)): + if bits[index] != (bits[index-23] ^ bits[index-18]): + wordIndex = index // self._readout._sampleBits + bitIndex = self._readout._sampleBits-1-(index % self._readout._sampleBits) + pnErrorBits[wordIndex] |= 1 << bitIndex + if len(bits) >= 23 and not any(bits[:23]): + firstCompleteWord = 22 // self._readout._sampleBits + pnErrorBits[firstCompleteWord] = mask + elif not alternating: + phase = 0 + elif reference and reference[0] == patternA: + phase = 0 + elif reference and reference[0] == patternB: + phase = 1 + else: + phase = None channelPassed = 0 for channel in range(self._readout._channels): + self.WordErrorCount[channel].set(0) + self.BitErrorMask[channel].set(0) if not channelMask & (1 << channel): continue samples = [ self._readout._debugSample(channel, index) & mask for index in range(sampleCount) ] - if alternating and samples: - if samples[0] == patternA: - phase = 0 - elif samples[0] == patternB: - phase = 1 + if pn23: + if channel == referenceChannel: + errorBits = pnErrorBits else: - phase = None - passed = phase is not None and all( - sample == (patternA, patternB)[(phase+index) % 2] - for index, sample in enumerate(samples) - ) + transformed = [sample ^ patternA for sample in samples] + errorBits = [ + sample ^ wanted + for sample, wanted in zip(transformed, reference) + ] else: - passed = all(sample == patternA for sample in samples) - if passed: + expected = [ + patternA if not alternating else (patternA, patternB)[(phase+index) % 2] + for index in range(sampleCount) + ] if phase is not None else [None] * sampleCount + errorBits = [ + mask if wanted is None else sample ^ wanted + for sample, wanted in zip(samples, expected) + ] + wordErrors = sum(bool(bits) for bits in errorBits) + bitErrors = 0 + for bits in errorBits: + bitErrors |= bits + self.WordErrorCount[channel].set(wordErrors) + self.BitErrorMask[channel].set(bitErrors) + if wordErrors == 0: channelPassed |= 1 << channel fcoMask = self.FcoMask.value() fcoPassed = self._readout._lockedMask() & fcoMask + for lane in range(self._readout._fcoLanes): + self.FcoErrorCount[lane].set( + 0 if fcoPassed & (1 << lane) else 1) self.ChannelPassed.set(channelPassed) self.FcoPassed.set(fcoPassed) self.CheckedSamples.set(sampleCount) @@ -128,7 +180,7 @@ def Start(self): self.TimedOut.set(False) self.ConfigError.set(False) self.Aborted.set(False) - self.PhaseAcquired.set(not alternating or bool(channelPassed)) + self.PhaseAcquired.set(phase is not None) self.AllChannelsPass.set((channelPassed & channelMask) == channelMask) self.AllFcoPass.set((fcoPassed & fcoMask) == fcoMask) self.Sequence.set(self.Sequence.value()+1) @@ -137,7 +189,9 @@ def Start(self): variable.value() for variable in self._readout.DataDelay.values()), 'channelMask': channelMask, 'dataMask': mask, + 'pn23': pn23, 'samples': sampleCount, + 'timeout': self.Timeout.value(), }) def Abort(self): @@ -266,6 +320,14 @@ def _debugSample(self, channel, index): return sample +class DeepPatternFaultReadout(FakeReadout): + def _debugSample(self, channel, index): + sample = super()._debugSample(channel, index) + if channel == 1 and index == 20: + sample ^= 0x4 + return sample + + class MultiWindowFcoFakeReadout(MaskedCoupledFakeReadout): def __init__(self): super().__init__() @@ -321,7 +383,7 @@ def __init__(self, config, fault=None): super().__init__() self._config = config self._fault = fault - self._pn23 = _pn23Words(count=5) + self._pn23 = _pn23Words(count=5000) def _debugSample(self, channel, index): if self._config.OutputTestMode.value() != 5: @@ -331,6 +393,8 @@ def _debugSample(self, channel, index): sample = self._pn23[index] if self._fault == 'commonCorruption' and index == 2: sample ^= 0x1 + if self._fault == 'deepCommonCorruption' and index == 20: + sample ^= 0x4 return sample @@ -338,7 +402,7 @@ class Pn23MultiWindowFcoFakeReadout(MultiWindowFcoFakeReadout): def __init__(self, config): super().__init__() self._config = config - self._pn23 = _pn23Words() + self._pn23 = _pn23Words(count=5000) def _fcoPhaseAligned(self): # Both preferred and alternate FCO combinations look valid under the @@ -559,24 +623,85 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert readout.DataDelay[1].value() == 5 -def test_full_calibration_can_use_pattern_tester(calibration_fixture): +def test_full_calibration_can_add_deep_pattern_qualification(calibration_fixture): calibration, _, readout = calibration_fixture calibration.UsePatternTester.set(True) + calibration.PatternTesterSamples.set(32) results = calibration._runCalibration(dev=calibration) assert results['Data'][0]['eye']['selected'] == 2 assert results['Data'][1]['eye']['selected'] == 5 assert results['Final']['passed'] - assert readout.snapshotDelays == [] - assert len(readout.PatternTester.measurements) == 9 - assert all( - measurement['samples'] == 4 - for measurement in readout.PatternTester.measurements) - assert calibration.Diagnostics.value()['MeasurementBackend'] == 'PatternTester' + assert readout.snapshotDelays == [(tap, tap) for tap in range(8)] + [(2, 5)] + assert len(readout.PatternTester.measurements) == 1 + assert readout.PatternTester.measurements[0] == { + 'delays': (2, 5), + 'channelMask': 0x3, + 'dataMask': 0x3FFF, + 'pn23': False, + 'samples': 32, + 'timeout': calibration.PATTERN_TESTER_TIMEOUT_C, + } + deep = results['Final']['patternTester'] + assert deep['enabled'] and deep['performed'] and deep['passed'] + assert deep['requestedSamples'] == 32 + assert deep['checkedSamples'] == 32 + assert deep['channels'][0]['wordErrorCount'] == 0 + diagnostics = calibration.Diagnostics.value() + assert diagnostics['MeasurementBackend'] == 'Snapshot' + assert diagnostics['DeepPatternTester'] == {'enabled': True, 'samples': 32} assert calibration.RunTime.value() > 0.0 +def test_snapshot_data_scan_requires_ordered_shared_phase(calibration_fixture): + calibration, _, readout = calibration_fixture + sequence = (0x2AAA, 0x2AAA, 0x1555, 0x1555) + readout._debugSample = lambda channel, index: sequence[index] + + details = calibration._captureGroupPasses((0, 1)) + + assert not details[0]['passed'] + assert not details[1]['passed'] + assert details[0]['captures'][0]['expectedSequence'] == [ + 0x2AAA, 0x1555, 0x2AAA, 0x1555] + + +def test_deep_pattern_error_is_reported_and_fails_final_qualification(): + config = FakeConfig() + readout = DeepPatternFaultReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(True) + calibration.PatternTesterSamples.set(32) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + deep = calibration.Results.value()['Final']['patternTester'] + assert deep['performed'] and not deep['passed'] + assert deep['channels'][0]['wordErrorCount'] == 0 + assert deep['channels'][1]['wordErrorCount'] == 1 + assert deep['channels'][1]['bitErrorMask'] == 0x4 + assert deep['fco'][0] == {'passed': True, 'errorCount': 0} + finally: + calibration._runEn = False + root.stop() + + @pytest.mark.parametrize('usePatternTester', [False, True]) def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): config = FakeConfig() @@ -611,9 +736,54 @@ def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): assert results['Final']['passed'] assert config.OutputTestMode.value() == 0 assert not config.ResetPNLong.value() - # PN23 always uses one atomic debug snapshot, independently of the data - # eye and checkerboard backend selected for the rest of calibration. + # PN23 always starts with one atomic snapshot. Deep qualification adds + # one checkerboard and one PN23 tester window after centering. assert readout.snapshotDelays[-1] == (2, 5) + assert len(readout.PatternTester.measurements) == 2*usePatternTester + if usePatternTester: + deep = results['Final']['pn23']['patternTester'] + assert deep['mode'] == 'Pn23' + assert deep['checkedSamples'] == 4096 + assert deep['status']['phaseAcquired'] + assert readout.PatternTester.measurements[-1]['pn23'] + finally: + calibration._runEn = False + root.stop() + + +def test_deep_pn23_error_beyond_snapshot_is_reported(): + config = FakeConfig() + readout = Pn23FakeReadout(config, fault='deepCommonCorruption') + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update, + pn23Reset=config.ResetPNLong) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(True) + calibration.PatternTesterSamples.set(32) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + pn23 = calibration.Results.value()['Final']['pn23'] + assert pn23['snapshotPassed'] + deep = pn23['patternTester'] + assert deep['performed'] and not deep['passed'] + assert deep['status']['phaseAcquired'] + assert deep['channels'][0]['wordErrorCount'] >= 1 + assert deep['channels'][0]['bitErrorMask'] & 0x4 + assert deep['channels'][1]['wordErrorCount'] == 0 finally: calibration._runEn = False root.stop() @@ -821,16 +991,12 @@ def test_full_calibration_masks_unaligned_partner_lane(usePatternTester): assert results['Data'][1]['eye']['selected'] == 5 assert results['Final']['passed'] if usePatternTester: - assert readout.snapshotDelays == [] - assert [measurement['delays'] for measurement in readout.PatternTester.measurements] == ( - [delays for tap in range(8) for delays in ((tap, tap), (tap, tap))] + - [(2, 5)]) - assert [measurement['dataMask'] for measurement in - readout.PatternTester.measurements[:-1]] == [0x003F, 0x3FC0]*8 - else: - assert readout.snapshotDelays == ( - [(tap, tap) for tap in range(8)] + - [(2, 5)]) + assert [measurement['delays'] for measurement in + readout.PatternTester.measurements] == [(2, 5)] + assert readout.PatternTester.measurements[0]['dataMask'] == 0x3FFF + assert readout.snapshotDelays == ( + [(tap, tap) for tap in range(8)] + + [(2, 5)]) assert readout.DataDelay[0].value() == 2 assert readout.DataDelay[1].value() == 5 finally: diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py index 0483cbaff4..b8a7244d97 100644 --- a/tests/devices/analog_devices/test_AdcDdrModel.py +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -132,6 +132,8 @@ def test_normalized_map_geometry_and_memory_readback(memory_backed_readout): assert pattern.Start.bitOffset == [0] assert pattern.Abort.bitOffset == [0] assert pattern.Alternating.address == 0x808 + assert pattern.Pn23.address == 0x808 + assert pattern.Pn23.bitOffset == [1] assert pattern.ReferenceChannel.bitOffset == [8] assert pattern.ChannelMask.bitSize == [2] assert pattern.FcoMask.bitSize == [2] @@ -357,6 +359,8 @@ def test_device_specific_calibration_adapters(): assert ad9681._configUpdate is None assert ad9681.UsePatternTester.value() is False assert ad9681.UsePatternTester.mode == 'RW' + assert ad9681.PatternTesterSamples.value() == 4096 + assert ad9681.PatternTesterSamples.mode == 'RW' assert ad9681.RunTime.value() == 0.0 assert ad9681.RunTime.mode == 'RO' assert ad9681.RunTime.units == 's' @@ -393,19 +397,13 @@ def test_ad9249_resolution_sample_rate_override_register_fields(): assert config.SampleRate.bitSize == [3] -def test_ad9681_resolution_sample_rate_override_register_fields(): +def test_ad9681_resolution_sample_rate_override_register_is_disabled(): config = Ad9681Config(name='Config') assert config.DeviceUpdate.offset == 0x3FC - assert config.ResolutionSampleRateOverride.offset == 0x400 - assert config.ResolutionSampleRateOverride.bitOffset == [6] - assert config.ResolutionSampleRateOverride.bitSize == [1] - assert config.Resolution.offset == 0x400 - assert config.Resolution.bitOffset == [4] - assert config.Resolution.bitSize == [2] - assert config.SampleRate.offset == 0x400 - assert config.SampleRate.bitOffset == [0] - assert config.SampleRate.bitSize == [3] + assert 'ResolutionSampleRateOverride' not in config.nodes + assert 'Resolution' not in config.nodes + assert 'SampleRate' not in config.nodes @pytest.mark.parametrize('configType', (Ad9249ConfigGroup, Ad9252Config, Ad9681Config)) diff --git a/tests/devices/analog_devices/test_AdcDdrPatternTester.py b/tests/devices/analog_devices/test_AdcDdrPatternTester.py index e577f9ca55..6d78b4c986 100644 --- a/tests/devices/analog_devices/test_AdcDdrPatternTester.py +++ b/tests/devices/analog_devices/test_AdcDdrPatternTester.py @@ -9,12 +9,13 @@ ############################################################################## # Test methodology: -# - Sweep: Exercise two logical channels and one FCO lane in constant and -# alternating pattern modes, followed by every terminal error class. +# - Sweep: Exercise two logical channels and one FCO lane in constant, +# alternating, and arbitrary-phase PN23 modes, followed by every terminal +# error class. # - Stimulus: Configure and start finite windows through the module's AXI-Lite -# interface, provide valid sample groups with shared A/B phase, inject one -# data-bit error and one FCO mismatch, suppress FCO validity, then withhold -# valid samples or abort. +# interface, provide valid sample groups with shared A/B phase and PN23 +# recurrence, inject recurrence, channel-coherence, and FCO errors, suppress +# FCO validity, then withhold valid samples or abort. # - Checks: Verify configuration readback, completion sequencing, common sample # counts, phase acquisition, channel/FCO pass masks, error counters, # accumulated bit-error masks, configuration errors, timeout, and abort. @@ -69,13 +70,36 @@ async def send_sample(dut, channel0, channel1): dut.sampleValid.value = 0 -async def configure(axil, *, alternating, samples, timeout): - await axil_write_u32(axil, CONFIG_ADDR, int(alternating)) +def pn23_words(state=0x654321, count=8, width=14): + mask = (1 << 23)-1 + words = [] + for _ in range(count): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> 22) & 1) + state = ( + ((state << 1) & mask) | + (((state >> 22) ^ (state >> 17)) & 1)) + words.append(word) + return words + + +async def configure( + axil, + *, + alternating, + samples, + timeout, + pn23=False, + dataMask=0x3FFF, + patternA=0x1555, + patternB=0x2AAA): + await axil_write_u32(axil, CONFIG_ADDR, int(alternating) | (int(pn23) << 1)) await axil_write_u32(axil, CHANNEL_MASK_ADDR, 3) await axil_write_u32(axil, FCO_MASK_ADDR, 1) - await axil_write_u32(axil, DATA_MASK_ADDR, 0x3FFF) - await axil_write_u32(axil, PATTERN_A_ADDR, 0x1555) - await axil_write_u32(axil, PATTERN_B_ADDR, 0x2AAA) + await axil_write_u32(axil, DATA_MASK_ADDR, dataMask) + await axil_write_u32(axil, PATTERN_A_ADDR, patternA) + await axil_write_u32(axil, PATTERN_B_ADDR, patternB) await axil_write_u32(axil, SAMPLES_ADDR, samples) await axil_write_u32(axil, TIMEOUT_ADDR, timeout) @@ -156,6 +180,70 @@ async def pattern_tester_test(dut): assert await axil_read_u32(axil, BIT_ERROR_ADDR+4) == 4 assert await axil_read_u32(axil, FCO_ERROR_ADDR) == 1 + # PN23 mode acquires an arbitrary nonzero 23-bit prefix, applies PatternA + # as an input XOR mask, and then checks recurrence plus channel coherence. + words = pn23_words() + xorMask = 0x2000 + await configure( + axil, + alternating=False, + pn23=True, + samples=len(words), + timeout=100, + patternA=xorMask, + patternB=0) + assert await axil_read_u32(axil, CONFIG_ADDR) & 0x3 == 0x2 + sequence = await start_window(axil) + for word in words: + await send_sample(dut, word ^ xorMask, word ^ xorMask) + status = await wait_done(axil, sequence) + assert status == 0x70 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 3 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 0 + + # A common corruption preserves channel coherence but violates the PN23 + # recurrence on the reference channel and reports its physical bit lane. + await configure( + axil, + alternating=False, + pn23=True, + samples=len(words), + timeout=100, + patternA=xorMask, + patternB=0) + sequence = await start_window(axil) + for index, word in enumerate(words): + sample = (word ^ xorMask) ^ (4 if index == 4 else 0) + await send_sample(dut, sample, sample) + status = await wait_done(axil, sequence) + assert status == 0x50 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 2 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) >= 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR) & 4 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 0 + + # A relative error leaves the reference recurrence valid while identifying + # the non-reference channel and failing bit. + await configure( + axil, + alternating=False, + pn23=True, + samples=len(words), + timeout=100, + patternA=xorMask, + patternB=0) + sequence = await start_window(axil) + for index, word in enumerate(words): + sample = word ^ xorMask + await send_sample(dut, sample, sample ^ (8 if index == 5 else 0)) + status = await wait_done(axil, sequence) + assert status == 0x50 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 1 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR+4) == 8 + # A selected FCO lane must be observed during the window to pass. Missing # fcoValid is distinct from a mismatch and therefore leaves its error count # at zero while clearing the FCO pass result. @@ -178,6 +266,24 @@ async def pattern_tester_test(dut): status = await wait_done(axil, sequence) assert status == 0x04 + # PN23 requires exclusive mode selection, every sample bit, and enough + # samples to acquire its 23-bit history. + for alternating, dataMask, samples in ( + (True, 0x3FFF, 3), + (False, 0x3FFE, 3), + (False, 0x3FFF, 1)): + await configure( + axil, + alternating=alternating, + pn23=True, + samples=samples, + timeout=100, + dataMask=dataMask) + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x04 + # Three consecutive capture clocks without sampleValid terminate a window. await configure(axil, alternating=False, samples=2, timeout=3) sequence = await axil_read_u32(axil, SEQUENCE_ADDR) From 435d7a987ff8a1503beea4fe6effbd535d20340a Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 13:28:30 -0700 Subject: [PATCH 28/57] Fix inferred latch. --- .../adcDdr/rtl/AdcDdrPatternTester.vhd | 72 ++++++++++--------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd index f170465603..f833bb2279 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd @@ -79,6 +79,8 @@ architecture rtl of AdcDdrPatternTester is requestedSamples : slv(31 downto 0); noValidTimeout : slv(31 downto 0); noValidCount : slv(31 downto 0); + pnReferenceWord : slv(SAMPLE_WIDTH_G-1 downto 0); + pnErrorBits : slv(SAMPLE_WIDTH_G-1 downto 0); pnHistory : slv(22 downto 0); pnHistoryCount : natural range 0 to 23; completionSeq : slv(31 downto 0); @@ -126,6 +128,8 @@ architecture rtl of AdcDdrPatternTester is requestedSamples => (others => '0'), noValidTimeout => (others => '0'), noValidCount => (others => '0'), + pnReferenceWord => (others => '0'), + pnErrorBits => (others => '0'), pnHistory => (others => '0'), pnHistoryCount => 0, completionSeq => (others => '0'), @@ -155,12 +159,6 @@ begin variable actual : slv(SAMPLE_WIDTH_G-1 downto 0); variable expected : slv(SAMPLE_WIDTH_G-1 downto 0); variable errorBits : slv(SAMPLE_WIDTH_G-1 downto 0); - variable referenceWord : slv(SAMPLE_WIDTH_G-1 downto 0); - variable pnErrorBits : slv(SAMPLE_WIDTH_G-1 downto 0); - variable pnHistory : slv(22 downto 0); - variable pnHistoryCount : natural range 0 to 23; - variable pnExpected : sl; - variable referenceIndex : natural range 0 to 255; variable selectedPhase : sl; variable phaseValid : sl; variable terminal : sl; @@ -169,10 +167,21 @@ begin variable fcoPassing : sl; begin v := r; + + -- Default all combinational scratch variables before conditional logic. + actual := (others => '0'); + expected := (others => '0'); + errorBits := (others => '0'); + selectedPhase := '0'; + phaseValid := '0'; + terminal := '0'; + invalidConfig := '0'; + channelsPassing := '0'; + fcoPassing := '0'; + v.start := '0'; v.abort := '0'; v.done := '0'; - terminal := '0'; if (r.busy = '1') then for i in FCO_LANES_G-1 downto 0 loop @@ -196,42 +205,38 @@ begin selectedPhase := '0'; if (r.pn23 = '1') then - referenceIndex := to_integer(unsigned(r.referenceChannel)); - referenceWord := (sampleIn(referenceIndex)(SAMPLE_WIDTH_G-1 downto 0) xor - r.patternA) and r.dataMask; - pnHistory := r.pnHistory; - pnHistoryCount := r.pnHistoryCount; - pnErrorBits := (others => '0'); + v.pnReferenceWord := + (sampleIn(to_integer(unsigned(r.referenceChannel)))(SAMPLE_WIDTH_G-1 downto 0) xor + r.patternA) and r.dataMask; + v.pnErrorBits := (others => '0'); for bitindex in SAMPLE_WIDTH_G-1 downto 0 loop - if (pnHistoryCount < 23) then - pnHistory := pnHistory(21 downto 0) & referenceWord(bitindex); - pnHistoryCount := pnHistoryCount + 1; - if (pnHistoryCount = 23) then - if uOr(pnHistory) = '1' then + if (v.pnHistoryCount < 23) then + v.pnHistory := v.pnHistory(21 downto 0) & v.pnReferenceWord(bitindex); + v.pnHistoryCount := v.pnHistoryCount + 1; + if (v.pnHistoryCount = 23) then + if uOr(v.pnHistory) = '1' then v.phaseAcquired := '1'; else -- The all-zero state satisfies the recurrence but is -- not part of the maximal-length PN23 sequence. - pnErrorBits := (others => '1'); + v.pnErrorBits := (others => '1'); end if; end if; else - pnExpected := pnHistory(22) xor pnHistory(17); - if (referenceWord(bitindex) /= pnExpected) then - pnErrorBits(bitindex) := '1'; + if (v.pnReferenceWord(bitindex) /= + (v.pnHistory(22) xor v.pnHistory(17))) then + v.pnErrorBits(bitindex) := '1'; end if; - pnHistory := pnHistory(21 downto 0) & referenceWord(bitindex); + v.pnHistory := v.pnHistory(21 downto 0) & v.pnReferenceWord(bitindex); end if; end loop; - v.pnHistory := pnHistory; - v.pnHistoryCount := pnHistoryCount; elsif (r.alternating = '1') then if (r.phaseAcquired = '1') then selectedPhase := r.expectedPhase; else - referenceIndex := to_integer(unsigned(r.referenceChannel)); - actual := sampleIn(referenceIndex)(SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; + actual := sampleIn(to_integer(unsigned(r.referenceChannel))) + (SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; if (actual = (r.patternA and r.dataMask)) then selectedPhase := '0'; v.phaseAcquired := '1'; @@ -249,12 +254,12 @@ begin for i in CHANNELS_G-1 downto 0 loop if (r.channelMask(i) = '1') then if (r.pn23 = '1') then - if (i = referenceIndex) then - errorBits := pnErrorBits; + if (i = to_integer(unsigned(r.referenceChannel))) then + errorBits := v.pnErrorBits; else actual := (sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) xor r.patternA) and r.dataMask; - errorBits := actual xor referenceWord; + errorBits := actual xor v.pnReferenceWord; end if; elsif (phaseValid = '0') then errorBits := r.dataMask; @@ -346,6 +351,8 @@ begin v.requestedSamples := r.cfgSamples; v.noValidTimeout := r.cfgTimeout; v.noValidCount := (others => '0'); + v.pnReferenceWord := (others => '0'); + v.pnErrorBits := (others => '0'); v.pnHistory := (others => '0'); v.pnHistoryCount := 0; v.checkedSamples := (others => '0'); @@ -364,10 +371,9 @@ begin invalidConfig := '1'; end if; if (r.cfgAlternating = '1' or r.cfgPn23 = '1') then - referenceIndex := to_integer(unsigned(r.cfgReference)); - if (referenceIndex >= CHANNELS_G) then + if (to_integer(unsigned(r.cfgReference)) >= CHANNELS_G) then invalidConfig := '1'; - elsif (r.cfgChannelMask(referenceIndex) = '0') then + elsif (r.cfgChannelMask(to_integer(unsigned(r.cfgReference))) = '0') then invalidConfig := '1'; end if; if (r.cfgAlternating = '1' and From 2782c452c662846e7670599c4ec23e067e13ec4a Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 13:34:52 -0700 Subject: [PATCH 29/57] Refactor. --- .../adcDdr/rtl/AdcDdrPatternTester.vhd | 193 ++++++++---------- 1 file changed, 80 insertions(+), 113 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd index f833bb2279..bb97c38805 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd @@ -79,8 +79,9 @@ architecture rtl of AdcDdrPatternTester is requestedSamples : slv(31 downto 0); noValidTimeout : slv(31 downto 0); noValidCount : slv(31 downto 0); - pnReferenceWord : slv(SAMPLE_WIDTH_G-1 downto 0); - pnErrorBits : slv(SAMPLE_WIDTH_G-1 downto 0); + referenceWord : slv(SAMPLE_WIDTH_G-1 downto 0); + expectedWord : slv(SAMPLE_WIDTH_G-1 downto 0); + sampleErrorBits : Slv16Array(CHANNELS_G-1 downto 0); pnHistory : slv(22 downto 0); pnHistoryCount : natural range 0 to 23; completionSeq : slv(31 downto 0); @@ -128,8 +129,9 @@ architecture rtl of AdcDdrPatternTester is requestedSamples => (others => '0'), noValidTimeout => (others => '0'), noValidCount => (others => '0'), - pnReferenceWord => (others => '0'), - pnErrorBits => (others => '0'), + referenceWord => (others => '0'), + expectedWord => (others => '0'), + sampleErrorBits => (others => (others => '0')), pnHistory => (others => '0'), pnHistoryCount => 0, completionSeq => (others => '0'), @@ -154,31 +156,10 @@ begin comb : process (axilReadMaster, axilWriteMaster, fcoValid, fcoWord, r, rst, sampleIn, sampleValid) is - variable v : RegType; - variable ep : AxiLiteEndpointType; - variable actual : slv(SAMPLE_WIDTH_G-1 downto 0); - variable expected : slv(SAMPLE_WIDTH_G-1 downto 0); - variable errorBits : slv(SAMPLE_WIDTH_G-1 downto 0); - variable selectedPhase : sl; - variable phaseValid : sl; - variable terminal : sl; - variable invalidConfig : sl; - variable channelsPassing : sl; - variable fcoPassing : sl; + variable v : RegType; + variable ep : AxiLiteEndpointType; begin v := r; - - -- Default all combinational scratch variables before conditional logic. - actual := (others => '0'); - expected := (others => '0'); - errorBits := (others => '0'); - selectedPhase := '0'; - phaseValid := '0'; - terminal := '0'; - invalidConfig := '0'; - channelsPassing := '0'; - fcoPassing := '0'; - v.start := '0'; v.abort := '0'; v.done := '0'; @@ -200,18 +181,17 @@ begin v.done := '1'; v.aborted := '1'; elsif (sampleValid = '1') then - v.noValidCount := (others => '0'); - phaseValid := '1'; - selectedPhase := '0'; + v.noValidCount := (others => '0'); + v.referenceWord := sampleIn(to_integer(unsigned(r.referenceChannel))) + (SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; + v.expectedWord := r.patternA and r.dataMask; + v.sampleErrorBits := (others => (others => '0')); if (r.pn23 = '1') then - v.pnReferenceWord := - (sampleIn(to_integer(unsigned(r.referenceChannel)))(SAMPLE_WIDTH_G-1 downto 0) xor - r.patternA) and r.dataMask; - v.pnErrorBits := (others => '0'); + v.referenceWord := v.referenceWord xor r.patternA; for bitindex in SAMPLE_WIDTH_G-1 downto 0 loop if (v.pnHistoryCount < 23) then - v.pnHistory := v.pnHistory(21 downto 0) & v.pnReferenceWord(bitindex); + v.pnHistory := v.pnHistory(21 downto 0) & v.referenceWord(bitindex); v.pnHistoryCount := v.pnHistoryCount + 1; if (v.pnHistoryCount = 23) then if uOr(v.pnHistory) = '1' then @@ -219,34 +199,36 @@ begin else -- The all-zero state satisfies the recurrence but is -- not part of the maximal-length PN23 sequence. - v.pnErrorBits := (others => '1'); + v.sampleErrorBits(to_integer(unsigned(r.referenceChannel))) + (SAMPLE_WIDTH_G-1 downto 0) := (others => '1'); end if; end if; else - if (v.pnReferenceWord(bitindex) /= + if (v.referenceWord(bitindex) /= (v.pnHistory(22) xor v.pnHistory(17))) then - v.pnErrorBits(bitindex) := '1'; + v.sampleErrorBits(to_integer(unsigned(r.referenceChannel)))(bitindex) := '1'; end if; - v.pnHistory := v.pnHistory(21 downto 0) & v.pnReferenceWord(bitindex); + v.pnHistory := v.pnHistory(21 downto 0) & v.referenceWord(bitindex); end if; end loop; elsif (r.alternating = '1') then if (r.phaseAcquired = '1') then - selectedPhase := r.expectedPhase; + if (r.expectedPhase = '0') then + v.expectedWord := r.patternA and r.dataMask; + else + v.expectedWord := r.patternB and r.dataMask; + end if; + v.expectedPhase := not r.expectedPhase; else - actual := sampleIn(to_integer(unsigned(r.referenceChannel))) - (SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; - if (actual = (r.patternA and r.dataMask)) then - selectedPhase := '0'; + if (v.referenceWord = (r.patternA and r.dataMask)) then + v.expectedWord := r.patternA and r.dataMask; v.phaseAcquired := '1'; v.expectedPhase := '1'; - elsif (actual = (r.patternB and r.dataMask)) then - selectedPhase := '1'; + elsif (v.referenceWord = (r.patternB and r.dataMask)) then + v.expectedWord := r.patternB and r.dataMask; v.phaseAcquired := '1'; v.expectedPhase := '0'; - else - phaseValid := '0'; end if; end if; end if; @@ -254,40 +236,58 @@ begin for i in CHANNELS_G-1 downto 0 loop if (r.channelMask(i) = '1') then if (r.pn23 = '1') then - if (i = to_integer(unsigned(r.referenceChannel))) then - errorBits := v.pnErrorBits; - else - actual := (sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) xor - r.patternA) and r.dataMask; - errorBits := actual xor v.pnReferenceWord; + if (i /= to_integer(unsigned(r.referenceChannel))) then + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0) := + (((sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) xor r.patternA) and + r.dataMask) xor v.referenceWord); end if; - elsif (phaseValid = '0') then - errorBits := r.dataMask; + elsif (v.phaseAcquired = '0') then + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0) := r.dataMask; else - if (selectedPhase = '0') then - expected := r.patternA and r.dataMask; - else - expected := r.patternB and r.dataMask; - end if; - actual := sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; - errorBits := actual xor expected; + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0) := + (sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) and r.dataMask) xor + v.expectedWord; end if; - if (uOr(errorBits) = '1') then + if (uOr(v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0)) = '1') then if (r.wordErrorCount(i) /= x"FFFFFFFF") then v.wordErrorCount(i) := std_logic_vector(unsigned(r.wordErrorCount(i)) + 1); end if; v.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) := - r.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) or errorBits; + r.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) or + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0); end if; end if; end loop; - if (r.alternating = '1' and r.phaseAcquired = '1') then - v.expectedPhase := not r.expectedPhase; - end if; v.checkedSamples := std_logic_vector(unsigned(r.checkedSamples) + 1); if (unsigned(r.checkedSamples) + 1 >= unsigned(r.requestedSamples)) then - terminal := '1'; + v.busy := '0'; + v.done := '1'; + v.allChannelsPass := '1'; + for i in CHANNELS_G-1 downto 0 loop + if (r.channelMask(i) = '1') then + if (v.wordErrorCount(i) = x"00000000") then + v.channelPassed(i) := '1'; + else + v.channelPassed(i) := '0'; + v.allChannelsPass := '0'; + end if; + end if; + end loop; + v.allFcoPass := '1'; + for i in FCO_LANES_G-1 downto 0 loop + if (r.fcoMask(i) = '1') then + if (v.fcoSeen(i) = '1' and v.fcoErrorCount(i) = x"00000000") then + v.fcoPassed(i) := '1'; + else + v.fcoPassed(i) := '0'; + v.allFcoPass := '0'; + end if; + end if; + end loop; + if (r.pn23 = '1' and v.phaseAcquired = '0') then + v.allChannelsPass := '0'; + end if; end if; elsif (r.noValidTimeout /= x"00000000") then if (unsigned(r.noValidCount) + 1 >= unsigned(r.noValidTimeout)) then @@ -299,38 +299,6 @@ begin end if; end if; - if (terminal = '1') then - v.busy := '0'; - v.done := '1'; - channelsPassing := '1'; - for i in CHANNELS_G-1 downto 0 loop - if (r.channelMask(i) = '1') then - if (v.wordErrorCount(i) = x"00000000") then - v.channelPassed(i) := '1'; - else - v.channelPassed(i) := '0'; - channelsPassing := '0'; - end if; - end if; - end loop; - fcoPassing := '1'; - for i in FCO_LANES_G-1 downto 0 loop - if (r.fcoMask(i) = '1') then - if (v.fcoSeen(i) = '1' and v.fcoErrorCount(i) = x"00000000") then - v.fcoPassed(i) := '1'; - else - v.fcoPassed(i) := '0'; - fcoPassing := '0'; - end if; - end if; - end loop; - v.allChannelsPass := channelsPassing; - v.allFcoPass := fcoPassing; - if (r.pn23 = '1' and v.phaseAcquired = '0') then - v.allChannelsPass := '0'; - end if; - end if; - elsif (r.start = '1') then v.busy := '0'; v.timedOut := '0'; @@ -351,8 +319,9 @@ begin v.requestedSamples := r.cfgSamples; v.noValidTimeout := r.cfgTimeout; v.noValidCount := (others => '0'); - v.pnReferenceWord := (others => '0'); - v.pnErrorBits := (others => '0'); + v.referenceWord := (others => '0'); + v.expectedWord := (others => '0'); + v.sampleErrorBits := (others => (others => '0')); v.pnHistory := (others => '0'); v.pnHistoryCount := 0; v.checkedSamples := (others => '0'); @@ -362,34 +331,32 @@ begin v.wordErrorCount := (others => (others => '0')); v.bitErrorMask := (others => (others => '0')); v.fcoErrorCount := (others => (others => '0')); - invalidConfig := '0'; if (r.cfgSamples = x"00000000" or uOr(r.cfgChannelMask) = '0' or uOr(r.cfgDataMask) = '0') then - invalidConfig := '1'; + v.configError := '1'; end if; if (r.cfgAlternating = '1' and r.cfgPn23 = '1') then - invalidConfig := '1'; + v.configError := '1'; end if; if (r.cfgAlternating = '1' or r.cfgPn23 = '1') then if (to_integer(unsigned(r.cfgReference)) >= CHANNELS_G) then - invalidConfig := '1'; + v.configError := '1'; elsif (r.cfgChannelMask(to_integer(unsigned(r.cfgReference))) = '0') then - invalidConfig := '1'; + v.configError := '1'; end if; if (r.cfgAlternating = '1' and (r.cfgPatternA and r.cfgDataMask) = (r.cfgPatternB and r.cfgDataMask)) then - invalidConfig := '1'; + v.configError := '1'; end if; end if; if (r.cfgPn23 = '1') then if uAnd(r.cfgDataMask) = '0' or unsigned(r.cfgSamples) < to_unsigned(PN23_MIN_SAMPLES_C, 32) then - invalidConfig := '1'; + v.configError := '1'; end if; end if; - if (invalidConfig = '1') then - v.done := '1'; - v.configError := '1'; + if (v.configError = '1') then + v.done := '1'; else v.busy := '1'; v.phaseAcquired := not r.cfgAlternating and not r.cfgPn23; From 9efd23c1af8cd763d133ff0074942daf3f6bab8e Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 13:35:08 -0700 Subject: [PATCH 30/57] Additional style directives. --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 14b4b6d882..8edeb50485 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,8 @@ SURF RTL generally follows the two-process style popularized by Gaisler: one com - Put registered state in a `RegType` record. Use a `REG_INIT_C` constant for reset/default state, and declare `r` and `rin` signals for current and next state. - Name the combinational process `comb` and the sequential process `seq` unless the surrounding file has a stronger local convention. - At the top of `comb`, declare `variable v : RegType;` and immediately assign `v := r;`. Make all next-state updates to `v`. +- Minimize additional process variables. Prefer adding intermediate state or diagnostic values to `RegType` and operating on `v.` directly, even when the registered value is not currently consumed. This keeps the next-state path uniform and makes a useful value straightforward to expose through AXI-Lite later. +- Use a process-local variable only when it is genuinely clearer or required by a helper, such as `AxiLiteEndpointType`. Give every such scratch variable an unconditional default immediately after `v := r;` before any conditional logic, unless the called helper initializes the complete object before its first use. Never depend on mutually exclusive branches to imply a combinational default; incomplete assignment can infer a latch in synthesis even when simulation and lint pass. - Assign `rin <= v;` near the end of `comb`. Drive module outputs from `r` for registered outputs and from `v` only when the local design intentionally exposes next-cycle/combinational behavior. - Include all combinational inputs read by the process in the sensitivity list. Existing files often use explicit lists rather than `process(all)`; match nearby style. - Apply synchronous reset in `comb` by assigning `v := REG_INIT_C` when `RST_ASYNC_G = false` and reset is asserted. From 3a83ea6805601f10150fe0ccf438d85282675d14 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 16:42:15 -0700 Subject: [PATCH 31/57] Enhance ADC calibration diagnostics and outcomes reporting - Update debug diagnostics to retain detailed per-tap information and publish at operation termination. - Introduce explicit outcome states (IDLE, RUNNING, PASSED, FAILED, STOPPED) for better process tracking. - Ensure delay thresholds and results use native 'tap' units for consistency across hardware. - Modify tests to validate new outcome reporting and diagnostic publishing behavior. --- devices/AnalogDevices/adcDdr/README.md | 21 ++++-- .../serialized-ddr-adc-cleanup/README.md | 18 ++++- .../analog_devices/_AdcDdrCalibration.py | 71 ++++++++++++++----- .../analog_devices/test_AdcDdrCalibration.py | 38 +++++++++- .../analog_devices/test_AdcDdrModel.py | 13 ++++ 5 files changed, 138 insertions(+), 23 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index 1f3a51706b..f79a6a7be2 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -177,7 +177,7 @@ verification operation ends. | `PatternTesterSamples` | `4096` | Valid samples checked by each deep hardware window. | | `VerifyPn23` | Device dependent | Enable PN23 coherence and recurrence qualification when the adapter provides a PN-long reset control. | | `SettleTime` | `1 ms` | Wall-clock wait after delay, relock, or ADC test-mode changes. | -| `Debug` | `True` | Publish detailed per-tap diagnostics while the operation runs. | +| `Debug` | `True` | Retain detailed per-tap diagnostics and publish one completed tree when the operation ends. | The full delay range is the safest initial scan. Narrow `DelayStart` and `DelayStop` only when the board has a characterized region and the entire @@ -185,6 +185,12 @@ expected eye, including failing boundary taps, remains visible. An eye touching a scan boundary is reported as unbounded on that side unless circular scanning merges it with the opposite boundary. +Delay controls are reported in native `tap` units. A tap is not the same +physical interval on every FPGA family. In particular, the UltraScale PHY uses +`IDELAYE3` in uncalibrated `COUNT` mode, so software must not present those tap +counts as a portable time value. ADC sample rate describes the unit interval +but is not sufficient to convert an uncalibrated count into picoseconds. + ## Results and Diagnostics `Results` contains the selected eye, complete passing map, and margins for each @@ -200,9 +206,16 @@ that could not produce a qualifying eye. `Diagnostics` contains the measurement backend, expected patterns, raw and masked data captures, FCO words and lock masks, and the currently active scan -point. `RunTime`, process progress, and `Message` provide coarse operational -monitoring. Detailed diagnostics can be large because they retain every tap and -capture; disable `Debug` when live publication is unnecessary. +point. The private working tree is not repeatedly copied into the public +variable during a scan. A successful run publishes it once at termination when +`Debug` is enabled; a failed or stopped run publishes it regardless of the +debug setting so failure evidence is retained. + +`Outcome` reports `IDLE`, `RUNNING`, `PASSED`, `FAILED`, or `STOPPED` +independently of PyRogue's generic process state. Successful GUI-driven runs +also finish with `Message` set to `PASSED` rather than the ambiguous generic +`Done`. `RunTime` and process progress provide the remaining operational +monitoring. ## Device-Specific Behavior diff --git a/docs/plans/serialized-ddr-adc-cleanup/README.md b/docs/plans/serialized-ddr-adc-cleanup/README.md index ed69aef25d..a44e41dd6e 100644 --- a/docs/plans/serialized-ddr-adc-cleanup/README.md +++ b/docs/plans/serialized-ddr-adc-cleanup/README.md @@ -16,6 +16,22 @@ the alignment/calibration path deterministic, fast, and diagnostically useful. - Calibration tap scans use coherent ordered snapshots. `UsePatternTester` enables one deep final checkerboard window instead of replacing every tap measurement with repeated hardware windows. +- AD9249 UltraScale hardware initially took about 60 seconds per bank with + debug diagnostics enabled, but was quick with debug disabled. Calibration + therefore retains the deterministic exhaustive scan and avoids repeatedly + publishing the growing diagnostics tree. + +## Calibration UI Contract + +- Delay thresholds and results explicitly use native `tap` units. UltraScale + uses `IDELAYE3` uncalibrated `COUNT` mode, so ADC sample rate alone cannot + turn those counts into a portable picosecond value. +- `Debug=True` retains the per-tap diagnostics privately and publishes one + completed copy at process termination. Failed and stopped operations still + publish their evidence when debug is disabled. +- Calibration exposes an explicit terminal `Outcome` (`PASSED`, `FAILED`, or + `STOPPED`) and replaces PyRogue's generic successful `Message=Done` with + `Message=PASSED`. ## Deep Qualification Contract @@ -37,7 +53,7 @@ deep-check success and failure, detailed error reporting, capability rejection, alternate FCO-eye retries, shallow and deep PN23 qualification, and state restoration. -- `test_AdcDdrCalibration.py` and `test_AdcDdrModel.py`: 69 passed. +- `test_AdcDdrCalibration.py` and `test_AdcDdrModel.py`: 71 passed. - `test_AdcDdrPatternTester.py`: 1 passed with GHDL/cocotb. - VSG: no violations in `AdcDdrPkg.vhd` or `AdcDdrPatternTester.vhd`. diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index e53b6e6cae..24834f17c1 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -336,6 +336,11 @@ class AdcDdrCalibration(pr.Process): FULL_C = 0 VERIFY_CURRENT_C = 1 VERIFY_GUARD_BAND_C = 2 + OUTCOME_IDLE_C = 0 + OUTCOME_RUNNING_C = 1 + OUTCOME_PASSED_C = 2 + OUTCOME_FAILED_C = 3 + OUTCOME_STOPPED_C = 4 RUN_TIME_UPDATE_INTERVAL_C = 1.0 PATTERN_TESTER_TIMEOUT_C = 256 @@ -443,6 +448,7 @@ def __init__( value = 0, minimum = 0, maximum = (1 << readout._delayBits)-1, + units = 'tap', mode = 'RW')) self.add(pr.LocalVariable( @@ -451,6 +457,7 @@ def __init__( value = (1 << readout._delayBits)-1, minimum = 0, maximum = (1 << readout._delayBits)-1, + units = 'tap', mode = 'RW')) self.add(pr.LocalVariable( @@ -484,6 +491,19 @@ def __init__( value = pn23Reset is not None, mode = 'RW')) + self.add(pr.LocalVariable( + name = 'Outcome', + description = 'Explicit result of the current or most recent operation', + value = self.OUTCOME_IDLE_C, + enum = { + self.OUTCOME_IDLE_C: 'IDLE', + self.OUTCOME_RUNNING_C: 'RUNNING', + self.OUTCOME_PASSED_C: 'PASSED', + self.OUTCOME_FAILED_C: 'FAILED', + self.OUTCOME_STOPPED_C: 'STOPPED', + }, + mode = 'RO')) + self.add(pr.LocalVariable( name = 'RunTime', description = 'Elapsed wall-clock duration of the current or last operation', @@ -505,6 +525,7 @@ def __init__( description = 'Minimum passing width accepted by a full scan', value = 8, minimum = 1, + units = 'tap', mode = 'RW')) self.add(pr.LocalVariable( @@ -512,6 +533,7 @@ def __init__( description = 'Passing taps required on both sides of a selected tap', value = 2, minimum = 0, + units = 'tap', mode = 'RW')) self.add(pr.LocalVariable( @@ -522,7 +544,7 @@ def __init__( self.add(pr.LocalVariable( name = 'Debug', - description = 'Publish detailed per-tap calibration diagnostics', + description = 'Retain and publish detailed diagnostics when the operation ends', value = True, mode = 'RW')) @@ -554,6 +576,12 @@ def _process(self) -> None: # in its finally block, so retain the partial progress without # reporting the stop as an execution error. self.Message.set('Calibration stopped') + else: + # PyRogue's generic Process implementation sets Message to "Done" + # after the callback returns. Replace that ambiguous terminal text + # with the explicit successful outcome used by this process. + if self.Outcome.value() == self.OUTCOME_PASSED_C: + self.Message.set('PASSED') def _checkRun(self) -> None: """Raise the private stop exception at cooperative cancellation points.""" @@ -566,20 +594,18 @@ def _recordDiagnostic( kind: str, lane: int, tap: int, - detail: dict[str, Any], - publish: bool = False) -> None: - """Retain one per-tap diagnostic and optionally publish a deep copy.""" + detail: dict[str, Any]) -> None: + """Retain one per-tap diagnostic in the private working tree.""" - # The working dictionary is mutated throughout a run. Publish a copy so - # PyRogue readers never observe later mutations through a shared object. + # Publishing this growing tree at every tap makes a long scan + # increasingly expensive. The process wrapper publishes one immutable + # copy after success, failure, or cancellation instead. self._runDiagnostics[kind].setdefault(lane, {})[tap] = detail self._runDiagnostics['Current'] = { 'kind': kind, 'lane': lane, 'tap': tap, } - if publish or self.Debug.value(): - self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) def _configurePatternTester( self, @@ -968,7 +994,6 @@ def _scanDataGroup( if self.Debug.value(): passed = [lane for lane in lanes if details[lane]['passed']] failed = [lane for lane in lanes if not details[lane]['passed']] - self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) self.Message.set( f'Data lanes {list(lanes)}, tap {tap}: ' f'passing={passed}, failing={failed}') @@ -1010,7 +1035,6 @@ def _fullCalibration( 'passing': passing, 'diagnostics': copy.deepcopy(self._runDiagnostics['Fco'].get(lane, {})), } - self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) self.Results.set(results) raise RuntimeError(f'FCO lane {lane}: {exc}') from exc fcoEyes[lane] = eyes @@ -1047,7 +1071,6 @@ def _fullCalibration( 'diagnostics': copy.deepcopy( self._runDiagnostics['Data'].get(lane, {})), } - self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) self.Results.set(results) raise RuntimeError(f'Data lane {lane}: {exc}') from exc groupEyes[lane] = eye @@ -1166,7 +1189,6 @@ def _fullCalibration( self._runDiagnostics['Current'] = {'kind': 'Final'} self.incrementSteps() if not final['passed']: - self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) self.Results.set(results) raise RuntimeError('Final full-channel pattern qualification failed') return results @@ -1250,19 +1272,38 @@ def _runCalibration(self, *, dev: Any) -> dict[str, Any]: runStart = time.monotonic() timerStop = threading.Event() + publishDiagnostics = bool(self.Debug.value()) + self._runDiagnostics = {} + self.Diagnostics.set({}) timerThread = threading.Thread( target = self._updateRunTime, args = (runStart, timerStop), daemon = True) self.RunTime.set(0.0) + self.Outcome.set(self.OUTCOME_RUNNING_C) timerThread.start() try: - return self._runCalibrationImpl(dev=dev) + results = self._runCalibrationImpl(dev=dev) + except _AdcDdrCalibrationStopped: + self.Outcome.set(self.OUTCOME_STOPPED_C) + publishDiagnostics = True + raise + except Exception as exc: + self.Outcome.set(self.OUTCOME_FAILED_C) + self.Message.set(f'FAILED: {exc}') + publishDiagnostics = True + raise + else: + self.Outcome.set(self.OUTCOME_PASSED_C) + self.Message.set('PASSED') + return results finally: runTime = time.monotonic()-runStart timerStop.set() timerThread.join() self.RunTime.set(runTime) + if publishDiagnostics: + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: """Validate controls, execute the selected operation, and restore state.""" @@ -1321,7 +1362,6 @@ def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: 'Final': {}, 'Current': {}, } - self.Diagnostics.set({}) self.Results.set({}) try: @@ -1341,9 +1381,6 @@ def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: else: raise ValueError(f'Unsupported calibration operation {operation}') self.Results.set(results) - if self.Debug.value(): - self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) - self.Message.set('Calibration operation complete') retainResults = operation == self.FULL_C return results finally: diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 99fc32711b..7a5dc4af7a 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -18,10 +18,11 @@ # topology-aware, overlap-safe parallel grouping plus restoration between # guard checks for coupled physical lanes. Exercise strict ordered snapshot # scanning, deep hardware qualification, verification progress, final -# qualification, and live run-time reporting. +# qualification and live run-time reporting. # - Timing: Fake register access and zero settling keep process tests short; the # live timer regression temporarily reduces its update interval. +import copy import threading import time @@ -594,6 +595,7 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert calibration.Debug.value() is True assert calibration.UsePatternTester.value() is False + assert calibration.Outcome.value() == calibration.OUTCOME_IDLE_C results = calibration._runCalibration(dev=calibration) assert results['Fco'][0]['eye']['selected'] == 3 @@ -613,6 +615,8 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert readout.dataDelayWrites[8] == {0: 2, 1: 5} assert readout.snapshotDelays == [(tap, tap) for tap in range(8)] + [(2, 5)] assert calibration.RunTime.value() > 0.0 + assert calibration.Outcome.value() == calibration.OUTCOME_PASSED_C + assert calibration.Message.value() == 'PASSED' readout.FcoDelay[0].set(0) readout.DataDelay[0].set(0) @@ -623,6 +627,35 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert readout.DataDelay[1].value() == 5 +def test_diagnostics_publish_only_at_process_boundaries( + calibration_fixture, monkeypatch): + calibration, _, _ = calibration_fixture + publications = [] + originalSet = calibration.Diagnostics.set + + def record(value, *args, **kwargs): + publications.append(copy.deepcopy(value)) + return originalSet(value, *args, **kwargs) + + monkeypatch.setattr(calibration.Diagnostics, 'set', record) + + calibration._runCalibration(dev=calibration) + + assert len(publications) == 2 + assert publications[0] == {} + assert publications[1]['Current'] == {'kind': 'Final'} + assert publications[1]['Final']['passed'] + + +def test_process_gui_message_reports_passed(calibration_fixture): + calibration, _, _ = calibration_fixture + + calibration._process() + + assert calibration.Outcome.value() == calibration.OUTCOME_PASSED_C + assert calibration.Message.value() == 'PASSED' + + def test_full_calibration_can_add_deep_pattern_qualification(calibration_fixture): calibration, _, readout = calibration_fixture calibration.UsePatternTester.set(True) @@ -886,6 +919,8 @@ def test_failed_calibration_restores_mode_and_all_delays(calibration_fixture): assert readout.DataDelay[0].value() == 2 assert readout.DataDelay[1].value() == 4 assert calibration.RunTime.value() > 0.0 + assert calibration.Outcome.value() == calibration.OUTCOME_FAILED_C + assert calibration.Message.value().startswith('FAILED:') def test_failed_data_scan_reports_lane_and_retains_partial_results(calibration_fixture): @@ -1141,6 +1176,7 @@ def stop_during_fco_scan(): calibration._process() assert calibration.Message.value() == 'Calibration stopped' + assert calibration.Outcome.value() == calibration.OUTCOME_STOPPED_C assert calibration.Step.value() == 2 assert calibration.Progress.value() < 1.0 assert config.OutputTestMode.value() == 0 diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py index b8a7244d97..40018cd718 100644 --- a/tests/devices/analog_devices/test_AdcDdrModel.py +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -357,10 +357,23 @@ def test_device_specific_calibration_adapters(): readout=ad9681Readout) assert ad9681._dataLaneToChannel == tuple(range(8))+tuple(range(8)) assert ad9681._configUpdate is None + assert ad9681.DelayStart.units == 'tap' + assert ad9681.DelayStop.units == 'tap' + assert ad9681.MinimumEyeWidth.units == 'tap' + assert ad9681.GuardBand.units == 'tap' assert ad9681.UsePatternTester.value() is False assert ad9681.UsePatternTester.mode == 'RW' assert ad9681.PatternTesterSamples.value() == 4096 assert ad9681.PatternTesterSamples.mode == 'RW' + assert ad9681.Outcome.value() == ad9681.OUTCOME_IDLE_C + assert ad9681.Outcome.enum == { + 0: 'IDLE', + 1: 'RUNNING', + 2: 'PASSED', + 3: 'FAILED', + 4: 'STOPPED', + } + assert ad9681.Outcome.mode == 'RO' assert ad9681.RunTime.value() == 0.0 assert ad9681.RunTime.mode == 'RO' assert ad9681.RunTime.units == 's' From 9f144e0f83458d103adefa45c53ff51f0d5b4ed0 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 22:14:12 -0700 Subject: [PATCH 32/57] docs(tests): publish regression methodology guide --- tests/README.md | 99 +++++++++++++++++++++++++++++++ tests/common/README.md | 87 +++++++++++++++++++++++++++ tests/protocols/README.md | 84 ++++++++++++++++++++++++++ tests/protocols/batcher/README.md | 31 ++++++++++ tests/protocols/rssi/README.md | 62 +++++++++++++++++++ 5 files changed, 363 insertions(+) create mode 100644 tests/common/README.md create mode 100644 tests/protocols/README.md create mode 100644 tests/protocols/batcher/README.md create mode 100644 tests/protocols/rssi/README.md diff --git a/tests/README.md b/tests/README.md index bd4d8f84cd..95d3ccae85 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,6 +4,40 @@ This directory holds Python-authored regressions for synthesizable SURF RTL. The default stack is `pytest + cocotb + GHDL + ruckus`; VHDL should only be used for thin wrappers, shims, or required simulation models. +This README is the authoritative guide for new SURF regression work. Historical +task plans and module queues are not prerequisites and do not define the next +module that must be tested. Add or deepen coverage when a subsystem is being +changed, when a bug needs a permanent reproducer, or when a contributor chooses +an uncovered module to improve. + +## Quick Start + +1. Read this README and the nearest subsystem README, if one exists. +2. Search the surrounding tests and helper modules before writing new drivers + or protocol models. +3. Identify the externally visible contract and the smallest useful DUT or + wrapper boundary. +4. Write the module-specific `Test methodology` block before implementing the + test. It should make the intended sweep, stimulus, checks, and timing clear. +5. Import the HDL sources and run the narrowest useful pytest target: + + ```bash + make MODULES="$PWD" import + ./.venv/bin/python -m pytest -n 0 -q tests//test_.py + ``` + +6. Lint every edited VHDL file, run the relevant subsystem regression, and + check for stale simulator processes before handing the change off. + +Additional references: + +- [`tests/common/README.md`](common/README.md) documents the shared runner, + parameter cases, environment parsing, and clock helpers. +- [`tests/protocols/README.md`](protocols/README.md) documents protocol-oracle, + layering, malformed-frame, and integration-test practices. +- Subsystem READMEs may define protocol- or simulator-specific commands, but + they should extend rather than replace this guide. + ## Layout - Keep executable tests under subsystem packages, such as `tests/base/fifo/`, @@ -110,6 +144,45 @@ Common structure: - A final pytest wrapper named for the RTL target, calling `run_surf_vhdl_test(test_file=__file__, ...)`. +Keep the cocotb entrypoints and the pytest wrapper in the same file unless a +subsystem has a documented reason to separate them. Pytest owns build +parameters and simulator launches; cocotb owns cycle-level stimulus and checks. + +## Designing The Test + +Start from the public contract, not the current implementation. Read the entity +ports and generics, the nearest package and README, and any applicable protocol +or register-map specification. Then choose the smallest boundary that can prove +the behavior: + +- Test reusable leaves directly when their behavior is observable without a + large integration topology. +- Use an integration test when arbitration, CDC, configuration propagation, or + interaction between already-tested leaves is the actual contract. +- Do not replay a leaf's complete packet grammar or parameter matrix through + every higher-level wrapper. Higher-level tests should focus on what that layer + adds. +- Treat compile/elaboration-only smoke coverage as useful but distinct from a + functional regression. A functional test needs meaningful stimulus and + assertions. +- Treat package declarations as transitively covered unless an important + function or procedure needs a small wrapper and a direct behavioral test. + +A focused regression normally covers the relevant subset of: + +- reset assertion, release, and recovery; +- nominal data or control flow; +- backpressure and accepted-handshake timing; +- frame, burst, or transaction boundaries; +- payload ordering and byte enables such as `TKEEP`/`TSTRB`; +- sidebands such as `TLAST`, `TDEST`, `TID`, SOF, and EOFE; +- invalid inputs, error responses, overflow, timeout, or recovery behavior; +- representative generic or clock-domain configurations. + +Use deterministic directed cases for protocol rules and boundary conditions. +Randomized cases are valuable after a trustworthy reference model exists, but +they do not replace readable directed regressions for known contracts and bugs. + ## Parameter Sweeps Prefer curated matrices over broad Cartesian products. A good sweep covers @@ -224,3 +297,29 @@ command to confirm the file is clean. VHDL packages are usually covered transitively through modules that use them. Add a dedicated package wrapper only when a behavioral function or procedure is important and not reached naturally through existing DUT coverage. + +There is no active repository-wide queue of modules that must be completed in a +fixed order. When selecting new work, prefer high-reuse modules, code being +modified, untested bug fixes, and simulator-friendly leaves that establish +helpers useful to later integration tests. Vendor-heavy or mixed-language +blocks may be deferred when the standard GHDL flow cannot exercise their real +dependencies; document that limitation near the subsystem rather than adding a +test double that changes the DUT boundary. + +## Completion Checklist + +Before considering a new regression ready: + +- The Python file has the standard license header, a specific methodology + block, and comments around non-obvious cocotb sequencing. +- The test asserts behavior rather than merely reaching the end of simulation. +- Every wait is bounded directly or by a helper with a cycle/time limit. +- Reset, backpressure, sidebands, and error/boundary cases relevant to the DUT + are covered or explicitly documented as out of scope. +- Shared helpers were reused or extended instead of duplicated. +- Any retained VHDL wrapper is thin, locally documented, present in the right + ruckus manifest, and clean under `vsg-linter.yml`. +- The focused test and the nearest practical subsystem suite pass. +- `git diff --check` is clean and no stale simulator process remains. +- The nearest README is updated if the test introduces a new layout, helper, + simulator requirement, deferred dependency, or non-obvious invocation. diff --git a/tests/common/README.md b/tests/common/README.md new file mode 100644 index 0000000000..96a127afaf --- /dev/null +++ b/tests/common/README.md @@ -0,0 +1,87 @@ +# Common Regression Infrastructure + +The helpers in this directory provide the shared pytest/cocotb launch path for +SURF regressions. Start with the repository-wide [regression style +guide](../README.md) and use this page when wiring a new Python test into GHDL. + +## Standard Runner + +`run_surf_vhdl_test()` in `regression_utils.py` launches GHDL through +`cocotb-test`, loads the ruckus-imported SURF libraries, selects the cocotb +module from `test_file`, and gives each parameter case its own simulation build +directory. + +A normal pytest wrapper looks like this: + +```python +PARAMETER_SWEEP = [ + parameter_case("default", DATA_WIDTH_G=16, RST_ASYNC_G=False), + parameter_case("async_reset", DATA_WIDTH_G=16, RST_ASYNC_G=True), +] + + +@pytest.mark.parametrize("parameters", PARAMETER_SWEEP) +def test_my_target(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="MyTargetWrapper", + parameters=hdl_parameters_from(parameters), + extra_env=parameters, + ) +``` + +Use `parameters` only for VHDL generics. Use `extra_env` for Python-side case +metadata. When one dictionary contains both, pass it through +`hdl_parameters_from()` before giving it to the simulator. + +Use `extra_vhdl_sources` for a wrapper or simulation model that is intentionally +outside the imported SURF source list. Production RTL belongs in the nearest +`ruckus.tcl`; do not use this argument to hide a missing build-manifest entry. + +Use `sim_build_key` only when normal parameter-derived names would be too long +or when a subsystem requires a deliberately shared/stable build location. + +## Shared Helpers + +- `parameter_case()` creates readable pytest IDs for curated cases. +- `hdl_parameters_from()` filters mixed case dictionaries to keys ending in + `_G`. +- `env_flag()`, `env_sl()`, `env_int()`, `env_hex()`, and `env_float()` parse + simulator environment values consistently inside cocotb coroutines. +- `start_lockstep_clocks()` drives multiple logically common clocks from one + coroutine so their edges cannot drift. +- `build_vhdl_sources()` and `merge_vhdl_sources()` are runner plumbing; tests + should normally reach them only through `run_surf_vhdl_test()`. + +Bus and protocol transaction helpers live closer to their users: + +- `tests/axi/utils.py` contains shared AXI-family handshake utilities. +- `tests/protocols//*_test_utils.py` contains protocol frames, + reference models, sources, sinks, and scoreboards. +- Subsystem helpers should implement mechanics, while policy assertions remain + visible in the module test. + +## Build And Debug Workflow + +Run the ruckus import after source-list changes or when `build/SRC_VHDL` is +missing or stale: + +```bash +make MODULES="$PWD" import +``` + +Use a serial focused run for readable simulator logs: + +```bash +./.venv/bin/python -m pytest -n 0 -q tests//test_.py +``` + +Use parallel execution for a stable subsystem suite: + +```bash +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/ +``` + +Do not call the runner directly from a cocotb coroutine. Pytest launches the +simulator; cocotb code runs inside it. Keep every protocol-progress wait bounded +so a broken handshake becomes a useful failure instead of a hung worker. diff --git a/tests/protocols/README.md b/tests/protocols/README.md new file mode 100644 index 0000000000..f3e34659fd --- /dev/null +++ b/tests/protocols/README.md @@ -0,0 +1,84 @@ +# Protocol Regression Guidance + +Protocol tests follow the repository-wide [cocotb regression style +guide](../README.md). This page captures the additional practices that apply to +framed, reliable, layered, or specification-defined protocols. + +## Build One Shared Protocol Oracle + +Put packet constants, field encoders/decoders, checksum or CRC reference code, +and mechanical source/sink helpers in the subsystem's `*_test_utils.py` module. +Derive them from the normative protocol specification or the established SURF +package definitions, not from copied literals in individual tests. + +Keep policy assertions in the test that names the behavior. A helper may build +a DATA frame or calculate its checksum; the test should still say that an +out-of-order frame must be dropped, an EOFE marker must propagate, or a timeout +must increment a particular counter. This keeps helpers from becoming an +unreviewed second implementation of the DUT. + +When the specification and current RTL disagree, make the distinction explicit: + +- A clear specification requirement should become a directed regression and, + when necessary, an RTL fix. +- An ambiguous behavior should first be a characterization test with its scope + documented. +- A deliberately narrower SURF profile should be described in the subsystem + README and test methodology rather than presented as full protocol + compliance. + +## Test By Layer + +Prefer a progression that establishes trustworthy lower-level behavior before +large integration scenarios: + +1. Field packing, checksums/CRCs, encoders, and decoders. +2. Leaf transmit and receive state machines. +3. Flow control, retries, timeouts, register interfaces, and CDC boundaries. +4. Core or wrapper integration, routing, and multi-stream interaction. + +An integration test should focus on what the integrated layer adds. Reuse the +same protocol oracle, but do not duplicate every leaf permutation at the top +level. + +## Required Protocol Cases + +Choose the cases relevant to the DUT, including: + +- minimum, typical, maximum, and non-word-aligned payload sizes; +- exact and partial final beats with correct `TKEEP`/`TSTRB`; +- SOF, EOF, EOFE, `TLAST`, `TDEST`, `TID`, and per-byte `TUSER` propagation; +- output backpressure and input idle gaps; +- malformed headers, lengths, flags, checksums/CRCs, and trailers; +- truncated frames, early/late termination, and recovery on the next frame; +- retry, acknowledgment, busy, timeout, overflow, and drop behavior; +- reset while idle and, where meaningful, reset with a partial transaction; +- sequence/tag wraparound and representative parameter boundaries; +- multi-lane or multi-stream ordering and arbitration when supported. + +Assert both positive and negative behavior. For invalid traffic, verify not only +that an error is reported but also that forbidden payload or control output is +not emitted and that subsequent valid traffic recovers. + +## Ready/Valid Discipline + +A source must hold data and all sidebands stable until an accepting clock edge. +A sink applying backpressure should verify that the DUT does the same. Use the +shared sampled-ready helper or a suitable `cocotbext.axi` endpoint instead of +open-coding subtly different handshake loops. + +Monitor accepted handshakes when timing, arbitration, or frame boundaries are +part of the contract. Comparing only final payload bytes can miss duplicated +beats, dropped sidebands, premature `TLAST`, or incorrect ordering. + +## Wrappers And Integration Models + +Keep wrapper HDL limited to record flattening, deterministic tie-offs, +simulator-friendly generics, or the smallest required topology. Packet +generation, retry peers, scoreboards, and assertions belong in Python. + +Use real protocol dependencies at the chosen DUT boundary. Do not replace a +generated core or lower protocol layer with a permissive test double and then +claim coverage of the full assembly. If the standard GHDL flow cannot compile +the real mixed-language or vendor dependency, document the deferral in the +subsystem README and test the accessible leaves directly. diff --git a/tests/protocols/batcher/README.md b/tests/protocols/batcher/README.md new file mode 100644 index 0000000000..98ada89b3e --- /dev/null +++ b/tests/protocols/batcher/README.md @@ -0,0 +1,31 @@ +# Batcher Regressions + +These tests follow the repository-wide [regression style guide](../../README.md) +and [protocol guidance](../README.md). Shared AXI Stream beats, source/sink +drivers, byte compaction, and V2 superframe reference helpers live in +`batcher_test_utils.py`. + +The suite is layered deliberately: + +- `test_AxiStreamBatcher.py` proves the leaf V2 byte-stream contract, subframe + metadata, termination controls, and output stability under backpressure. +- `test_AxiStreamBatcherAxil.py` proves reset values, register readback, CDC + behavior, and the stream-side effects of threshold, gap, soft-reset, and + blowoff controls. It reuses the leaf oracle instead of repeating the full + packet matrix. +- `test_AxiStreamBatcherEventBuilder.py` focuses on indexed/routed source + selection, TDEST remapping, transition frames, alignment checks, timeout and + bypass/drop policy, counters, and multi-input progress. + +Keep future additions at the narrowest layer that owns the behavior. Packet +grammar belongs in the leaf test; register behavior belongs in the AXI-Lite +test; arbitration, routing, and cross-source policy belong in the event-builder +test. Extend `batcher_test_utils.py` for reusable mechanics, but leave the +policy being asserted visible in the individual test. + +Run the suite with: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/batcher +``` diff --git a/tests/protocols/rssi/README.md b/tests/protocols/rssi/README.md new file mode 100644 index 0000000000..5354924812 --- /dev/null +++ b/tests/protocols/rssi/README.md @@ -0,0 +1,62 @@ +# RSSI Regressions + +These tests follow the repository-wide [regression style guide](../../README.md) +and [protocol guidance](../README.md). The implementation and sizing guidance +is documented in [`protocols/rssi/README.md`](../../../protocols/rssi/README.md). + +## Protocol Oracle And Layers + +`rssi_test_utils.py` is the shared oracle for RSSI flags, header encoding, +checksum calculation, frame construction/parsing, SSI transport mechanics, and +common client/server setup. Keep protocol constants and mechanical helpers +there; keep assertions about RSSI policy in the test that names the behavior. + +The suite progresses from leaves to integration: + +- `test_RssiChksum.py` and `test_RssiHeaderReg.py` cover checksum and wire-header + formatting. +- `test_RssiRxFsm.py`, `test_RssiTxFsm.py`, `test_RssiMonitor.py`, and + `test_RssiConnFsm.py` cover receive/transmit legality, ACK/NULL/BUSY timing, + retransmission, connection negotiation, close, and recovery. +- `test_RssiAxiLiteRegItf.py` covers the register map, range clamping, + negotiated/current readback, counters, and visible controls/status. +- `test_RssiCore.py` covers direct client/server negotiation, payload transfer, + backpressure, loss/retransmission, checksums, keepalive, close/reopen, BUSY, + and AXI-Lite-controlled behavior. +- `test_RssiCoreWrapper.py` and `test_RssiCoreWrapperMultiStream.py` cover the + packetizer/chunker boundary, segment/window configurations, routing, + multi-stream loss recovery, and application-side sidebands. + +Default CI runs the currently stable RSSI cases. Focused cases that still expose +unresolved RTL behavior are opt-in behind `RUN_RSSI_KNOWN_ISSUE_TESTS=1`, and a +smaller group of long-running integration cases additionally uses +`RUN_RSSI_EXTENDED_TESTS=1`. Keep the skip reason beside each gated pytest entry +and promote the case to default coverage when its blocking RTL issue is fixed. + +## RSSI-Specific Expectations + +The SURF RSSI profile uses 8-byte non-SYN headers, 24-byte SYN headers, 8-bit +sequence numbers, cumulative ACKs, ordered delivery, retransmission, NULL +keepalives, and BUSY flow control. Current hardware does not implement EACK +out-of-sequence delivery. Tests should use the SURF/Rogue profile as the +concrete contract and consult the RUDP lineage only where the profile leaves a +behavior unspecified. + +Directed negative cases should verify that illegal flag combinations, malformed +headers, bad checksums, and out-of-order frames do not leak application payload. +Recovery cases should then send valid traffic and prove that the endpoint makes +forward progress without duplicate delivery. + +Run the default suite with: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/rssi +``` + +Run known-issue and extended cases explicitly with: + +```bash +RUN_RSSI_KNOWN_ISSUE_TESTS=1 RUN_RSSI_EXTENDED_TESTS=1 \ + ./.venv/bin/python -m pytest -n 0 -q tests/protocols/rssi +``` From d08d799ae2967fd0e7722d5bb2d0e1891cdefbd0 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 22:34:10 -0700 Subject: [PATCH 33/57] docs(tests): enhance README files with detailed regression methodology and guidelines --- AGENTS.md | 32 +++++++++++++++++++--- tests/README.md | 62 ++++++++++++++++++++++++++++++++++++------ tests/common/README.md | 25 +++++++++++++++-- 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 14b4b6d882..50b845fc67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,18 @@ Start with [README.md](README.md) for user-facing links and the source tree inde - [protocols/README.md](protocols/README.md) for PGP, SSI, SRP, RSSI, CoaXPress, JESD204B, I2C/SPI/UART, and related protocol cores. - [xilinx/README.md](xilinx/README.md) for Xilinx-family primitives, wrappers, and XVC UDP support. - [python/README.md](python/README.md) for the PyRogue package under `python/surf`. -- [tests/README.md](tests/README.md) for cocotb regression layout, methodology comments, helper reuse, and simulator conventions. +- [tests/README.md](tests/README.md) for the authoritative cocotb regression + methodology, coding style, coverage expectations, layout, and simulator + conventions. +- [tests/common/README.md](tests/common/README.md) for the shared pytest/GHDL + runner, parameter and environment handling, build isolation, and reusable + regression helpers. +- [tests/protocols/README.md](tests/protocols/README.md) for protocol-oracle, + layering, malformed-traffic, ready/valid, and integration-test guidance; then + read the nearest subsystem README, such as + [tests/protocols/batcher/README.md](tests/protocols/batcher/README.md) or + [tests/protocols/rssi/README.md](tests/protocols/rssi/README.md), when working + in that area. - [docs/plans/README.md](docs/plans/README.md) for substantial task planning, progress notes, and handoff conventions. Top-level `ruckus.tcl` loads `axi`, `base`, `dsp`, `devices`, `ethernet`, `protocols`, and `xilinx`. Module-level `ruckus.tcl` files should continue to be the source of truth for which HDL files and submodules are part of a build. @@ -169,7 +180,9 @@ C, C++, and C header files should use the same license text with `//` comment de Tcl, shell, YAML, and other hash-comment files should use the Python-style `#-----------------------------------------------------------------------------` license block when they are maintained SURF source. For executable scripts with a shebang, keep the shebang first and place the license block immediately after it. -Checked-in cocotb regression files must also include the `Test methodology` block described in [tests/README.md](tests/README.md), immediately after the license header. +New or substantially edited cocotb regression files must also include the +module-specific `Test methodology` block described in +[tests/README.md](tests/README.md), immediately after the license header. ## Python Conventions @@ -200,7 +213,11 @@ Checked-in cocotb regression files must also include the `Test methodology` bloc ## Tests And Verification -- For RTL regressions, use the guidance in [tests/README.md](tests/README.md). The expected stack is `pytest + cocotb + GHDL + ruckus`. +- For RTL regressions, start with [tests/README.md](tests/README.md). Use + [tests/common/README.md](tests/common/README.md) for runner/build mechanics, + [tests/protocols/README.md](tests/protocols/README.md) for protocol tests, and + the nearest test-subsystem README for local commands or exceptions. The + expected default stack is `pytest + cocotb + GHDL + ruckus`. - For docs-only changes, no RTL or Python tests are required, but check links and headings if the edit adds navigation. - For ruckus or source-list changes, run `make MODULES="$PWD" import` when practical. - For edited VHDL, run `./.venv/bin/vsg -c vsg-linter.yml path/to/file.vhd` and the most focused relevant cocotb/pytest target when practical. @@ -225,7 +242,14 @@ Before considering an RTL change done, check: ## Documentation Updates -When adding a new subsystem, add or update the closest `README.md` if the layout or usage is not obvious. Keep README files short and navigational: describe what belongs in the folder, important subdirectories, and any local build/test conventions, then link upward through the parent README chain. +When adding a new subsystem, add or update the closest `README.md` if the layout +or usage is not obvious. Keep README files short and navigational: describe what +belongs in the folder, important subdirectories, and any local build/test +conventions, then link upward through the parent README chain. Test-subsystem +READMEs should link to [tests/README.md](tests/README.md), and protocol-test +READMEs should also link to +[tests/protocols/README.md](tests/protocols/README.md), so local instructions +extend rather than duplicate the shared methodology. Add deeper README files as substantial areas are touched, especially in high-traffic module families such as `axi/axi-stream`, `axi/axi-lite`, `protocols/pgp`, `protocols/coaxpress`, `protocols/ssi`, `protocols/srp`, `ethernet/IpV4Engine`, `ethernet/UdpEngine`, and `ethernet/EthMacCore`. Prefer adding the README in the same change that introduces new layout or conventions for that area. diff --git a/tests/README.md b/tests/README.md index 95d3ccae85..a910af6845 100644 --- a/tests/README.md +++ b/tests/README.md @@ -105,9 +105,9 @@ falls back to running the complete `tests/` tree. ## Python Test Files -Every checked-in cocotb test file should start with the standard SLAC/SURF -license header followed immediately by a module-specific `Test methodology` -block: +Every new or substantially edited cocotb test file should start with the +standard SLAC/SURF license header followed immediately by a module-specific +`Test methodology` block. The concise form is: ```python ############################################################################## @@ -124,7 +124,12 @@ block: ``` Do not use generic methodology text. The block should tell a reader what this -specific bench proves and what it intentionally does not prove. +specific bench proves and what it intentionally does not prove. Complex +protocol or integration tests may expand the headings (`Purpose`, `DUT shape`, +`Protocol checks`, `Parameter strategy`, and similar), but must still make the +scope/configuration, DUT boundary, stimulus, checks, and timing assumptions +easy to find. A prose methodology with the same information is acceptable in a +legacy file; use the labeled form for new work. Use in-body comments at the major coroutine steps: clock startup, reset, stimulus phases, backpressure, trigger waits, and result checks. Keep comments @@ -182,6 +187,9 @@ A focused regression normally covers the relevant subset of: Use deterministic directed cases for protocol rules and boundary conditions. Randomized cases are valuable after a trustworthy reference model exists, but they do not replace readable directed regressions for known contracts and bugs. +Seed every randomized case explicitly. Keep the seed fixed or pass it through a +named environment value, and include the effective seed in a failure message or +log so the exact stimulus can be reproduced. ## Parameter Sweeps @@ -199,6 +207,13 @@ Pass only HDL generics as `parameters`. Put Python-only case metadata in `extra_env`, or use `hdl_parameters_from(parameters)` when a case dictionary contains both. +Normally let cocotb run all entrypoints in the module for each pytest build. +When separate pytest nodes intentionally select one cocotb scenario, pass a +selector through `extra_env` (for example `COCOTB_TESTCASE` or a documented +subsystem-specific variable), give the selector a deterministic default, and +make sure it participates in the simulation-build identity. A focused pytest +node should not silently rerun unrelated cocotb scenarios. + ## Reuse And Helpers Before writing transaction code, search nearby helpers and related subsystems. @@ -231,6 +246,13 @@ Assert externally visible behavior, not implementation accidents. Good checks usually include payload bytes, `TKEEP`, `TLAST`, `TUSER`/SOF/EOFE bits, address or ID sidebands, response codes, counters, or accepted-handshake timing. +Initialize reset and every testbench-driven control/data input before the first +active clock edge, preferably with `setimmediatevalue()` during bench setup. +Hold reset for an explicit number of clock edges, release it on the intended +edge, and allow any documented pipeline settling time before sampling outputs. +This prevents unresolved startup values from turning into simulator-dependent +stimulus. + Use bounded waits and explicit timeouts for protocol progress. Avoid open-ended `while True` loops unless they are wrapped by `with_timeout()` or a helper that has a cycle limit. @@ -243,9 +265,19 @@ Account for `TPD_G`, registered outputs, and GHDL scheduling. Sampling exactly on a clock edge can create false failures; most helpers settle with a short `Timer` after `RisingEdge()`. -Known RTL issues or intentionally open coverage should be explicit. If a bench -is checked in skipped or opt-in, document the condition and gate it with a clear -environment variable such as `RUN_KNOWN_ISSUE_TESTS`. +Keep skip reasons and opt-in coverage explicit, and distinguish why a case is +not in the default run: + +- Gate a regression for an unresolved RTL defect with a clear variable such as + `RUN_KNOWN_ISSUE_TESTS`, keep the issue visible in the methodology or local + README, and promote the case to default coverage with the fix. +- Gate an unusually long soak or stress matrix with a separately named + `RUN_*_EXTENDED_TESTS` variable; do not label stable-but-slow coverage as a + known issue. +- Use `pytest.skip()` or `pytest.mark.skipif()` for genuinely optional external + tools, licenses, platforms, or production libraries, and state the exact + missing prerequisite. A required CI job should provision that prerequisite + and treat an unexpected skip as a failure. ## Running Tests @@ -259,6 +291,12 @@ make MODULES="$PWD" import Run `make ... import` when the imported HDL source cache is missing or stale. Use `-n 0` for focused debug runs when serial simulator logs matter. +Assume the suite will run under pytest-xdist. Each case must have an isolated +simulation build directory and must not compete for a fixed port, ready file, +result file, or other process-global resource. Tests that launch peer processes +must use bounded startup/shutdown waits and clean them up in a `finally` block +or shared teardown helper, including after an assertion fails. + After any command that launches pytest, cocotb, GHDL, or another simulator runner, check for stale simulator child processes before starting another run. @@ -317,8 +355,14 @@ Before considering a new regression ready: - Reset, backpressure, sidebands, and error/boundary cases relevant to the DUT are covered or explicitly documented as out of scope. - Shared helpers were reused or extended instead of duplicated. -- Any retained VHDL wrapper is thin, locally documented, present in the right - ruckus manifest, and clean under `vsg-linter.yml`. +- Random stimulus is seeded and failures report enough information to replay + the case. +- The case is safe under pytest-xdist: build artifacts and external resources + are isolated, and child processes are cleaned up on failure. +- Any retained VHDL wrapper is thin, locally documented, clean under + `vsg-linter.yml`, and reachable through its intended source path: the nearest + ruckus manifest for build-facing HDL or `extra_vhdl_sources` for a + cocotb-only wrapper. - The focused test and the nearest practical subsystem suite pass. - `git diff --check` is clean and no stale simulator process remains. - The nearest README is updated if the test introduces a new layout, helper, diff --git a/tests/common/README.md b/tests/common/README.md index 96a127afaf..1d2cadf029 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -36,10 +36,27 @@ metadata. When one dictionary contains both, pass it through Use `extra_vhdl_sources` for a wrapper or simulation model that is intentionally outside the imported SURF source list. Production RTL belongs in the nearest -`ruckus.tcl`; do not use this argument to hide a missing build-manifest entry. +`ruckus.tcl`; do not use this argument to hide a missing build-manifest entry or +repeat a unit already supplied by the import, which can produce a library +redefinition error. Use `sim_build_key` only when normal parameter-derived names would be too long -or when a subsystem requires a deliberately shared/stable build location. +or when a subsystem requires a deliberately stable build location. The default +path already includes `parameters` and `extra_env`, which isolates parallel +cases. A custom key must preserve that isolation; never point concurrently +runnable variants at the same build directory. + +Leave `force_compile=False` for normal regressions. Set it only when a +documented source-topology or simulator-cache limitation makes reuse unsafe; +it is not a substitute for a unique build identity. + +The shared runner is the default for VHDL/GHDL regressions. A direct +`cocotb-test` or simulator-specific runner is justified only when the flow +needs capabilities the shared path cannot express, such as a mixed-language +top, a vendor simulator, precompiled libraries, or explicit external-process +lifecycle control. Document the exception beside the custom runner or in the +subsystem README, and retain the common source, compile-option, result-file, +and build-isolation conventions where they apply. ## Shared Helpers @@ -85,3 +102,7 @@ Use parallel execution for a stable subsystem suite: Do not call the runner directly from a cocotb coroutine. Pytest launches the simulator; cocotb code runs inside it. Keep every protocol-progress wait bounded so a broken handshake becomes a useful failure instead of a hung worker. + +When a pytest wrapper selects one of several cocotb entrypoints, pass the +selector in `extra_env`. Because the shared runner includes `extra_env` in the +default build path, selected scenarios remain isolated under pytest-xdist. From 4005e7ffe3c6d21953e53441fe8d9efae2b43309 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Tue, 25 Aug 2026 22:50:35 -0700 Subject: [PATCH 34/57] Refine the test guidance. --- AGENTS.md | 3 ++ tests/README.md | 86 ++++++++++++++++++++++++++----- tests/common/README.md | 39 +++++++++----- tests/protocols/README.md | 12 +++++ tests/protocols/batcher/README.md | 6 +++ tests/protocols/rssi/README.md | 20 ++++++- 6 files changed, 138 insertions(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 50b845fc67..c51009b4f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -223,6 +223,9 @@ module-specific `Test methodology` block described in - For edited VHDL, run `./.venv/bin/vsg -c vsg-linter.yml path/to/file.vhd` and the most focused relevant cocotb/pytest target when practical. - For Python/PyRogue changes, run a focused import or pytest that exercises the changed module. Avoid packaging commands unless the task specifically requires packaging validation. - For cocotb tests, prefer `./.venv/bin/python -m pytest -q tests/`. Use `-n 0` when serial simulator logs are needed. +- Select or explicitly skip cocotb scenarios that do not apply to a parameter case; do not return early and record an unexercised scenario as a pass. +- Use `extra_vhdl_sources` only for design units absent from the ruckus import, and keep finite cocotb tasks awaited or lifetime agents explicitly owned by the bench. +- For bug regressions, demonstrate failure on the known-bad RTL when practical, or document the defect-catching assertion and why the comparison could not be run. - For protocol or bus behavior changes, include tests or a clear verification note covering sidebands, backpressure, reset behavior, and boundary/error cases relevant to the change. - Avoid hand-editing generated or cache directories such as `build/`, `tests/sim_build/`, `.pytest_cache/`, `docs/_build/`, and `docs/_generated/`. diff --git a/tests/README.md b/tests/README.md index a910af6845..7de7b8f04f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -12,6 +12,17 @@ an uncovered module to improve. ## Quick Start +If the local Python/GHDL/ruckus environment has not been prepared, run the +repository setup helper first: + +```bash +./scripts/setup_regression_env.sh +``` + +The script checks the required host tools, creates `.venv`, installs the Python +requirements, and locates or clones ruckus. Review its output, activate the +environment if desired, and then follow the workflow below. + 1. Read this README and the nearest subsystem README, if one exists. 2. Search the surrounding tests and helper modules before writing new drivers or protocol models. @@ -26,8 +37,8 @@ an uncovered module to improve. ./.venv/bin/python -m pytest -n 0 -q tests//test_.py ``` -6. Lint every edited VHDL file, run the relevant subsystem regression, and - check for stale simulator processes before handing the change off. +6. Lint every edited VHDL file and run the relevant subsystem regression before + handing the change off. Additional references: @@ -173,6 +184,12 @@ the behavior: - Treat package declarations as transitively covered unless an important function or procedure needs a small wrapper and a direct behavioral test. +For a bug regression, verify when practical that the new test fails against the +known-bad RTL and passes with the fix. If that comparison cannot be run, +document why and identify the assertion that would catch the original defect. +Reaching the formerly failing code path without checking its externally visible +effect is not sufficient regression coverage. + A focused regression normally covers the relevant subset of: - reset assertion, release, and recovery; @@ -207,13 +224,19 @@ Pass only HDL generics as `parameters`. Put Python-only case metadata in `extra_env`, or use `hdl_parameters_from(parameters)` when a case dictionary contains both. -Normally let cocotb run all entrypoints in the module for each pytest build. -When separate pytest nodes intentionally select one cocotb scenario, pass a -selector through `extra_env` (for example `COCOTB_TESTCASE` or a documented +Prefer a pytest node that names one cocotb scenario or one coherent scenario +group. Normally let cocotb run all applicable entrypoints in that group. When +separate pytest nodes intentionally select one cocotb scenario, pass a selector +through `extra_env` (for example `COCOTB_TESTCASE` or a documented subsystem-specific variable), give the selector a deterministic default, and make sure it participates in the simulation-build identity. A focused pytest node should not silently rerun unrelated cocotb scenarios. +An entrypoint that is inapplicable to the current parameter set must be +explicitly skipped or excluded by pytest/cocotb selection. Do not return +successfully before exercising the behavior and assertions named by the test; +that records a no-op as a pass and obscures what the regression actually ran. + ## Reuse And Helpers Before writing transaction code, search nearby helpers and related subsystems. @@ -240,11 +263,34 @@ Use `start_lockstep_clocks()` for `COMMON_CLK_G` or similar wrappers that expect truly shared clock edges. Do not start two independent same-period clock coroutines when the DUT contract is common-clock behavior. +## Isolation And Coroutine Lifecycle + +Each cocotb entrypoint must establish its own defined starting state. Initialize +every testbench-driven input, reset the DUT when it has a meaningful reset, and +clear Python-side queues, scoreboards, and monitor state. Do not depend on the +execution order of cocotb entrypoints or on state left by an earlier test. + +Every finite transaction task started with `cocotb.start_soon()` must be awaited +before the test completes. A monitor, protocol peer, or other task intended to +run for the lifetime of the test should be retained by the bench, named for its +purpose, and documented as a lifetime agent. Give benches that own several such +agents an explicit cleanup method when they need orderly cancellation or can +hold an external resource. External processes, sockets, ports, and files always +require bounded setup/teardown and cleanup on assertion failure. + +Use operation-specific cycle limits or `with_timeout()` for protocol progress. +Add `timeout_time`/`timeout_unit` to complex concurrent or integration +entrypoints as a final deadlock watchdog. Small finite leaf tests do not need a +decorator timeout when every possible wait is already bounded. + ## Assertions And Timing Assert externally visible behavior, not implementation accidents. Good checks usually include payload bytes, `TKEEP`, `TLAST`, `TUSER`/SOF/EOFE bits, address or ID sidebands, response codes, counters, or accepted-handshake timing. +For a complex or parameterized check, include enough context in the failure to +identify the case, transaction or beat index, expected value, observed value, +and random seed when applicable. Initialize reset and every testbench-driven control/data input before the first active clock edge, preferably with `setimmediatevalue()` during bench setup. @@ -261,16 +307,19 @@ When a contract includes backpressure, burst length, sideband propagation, or arbitration order, monitor accepted handshakes directly. Final memory contents alone are not enough for timing-visible behavior. -Account for `TPD_G`, registered outputs, and GHDL scheduling. Sampling exactly -on a clock edge can create false failures; most helpers settle with a short -`Timer` after `RisingEdge()`. +Account for `TPD_G`, registered outputs, and GHDL scheduling. After an edge, use +`ReadOnly()` when only delta-cycle settling is required. When the RTL schedules +a real nonzero `after TPD_G`, wait for that configured propagation delay and +then sample the stable value. Keep this distinction visible in a shared helper; +do not add an unexplained fixed delay merely to make a race disappear. Keep skip reasons and opt-in coverage explicit, and distinguish why a case is not in the default run: - Gate a regression for an unresolved RTL defect with a clear variable such as - `RUN_KNOWN_ISSUE_TESTS`, keep the issue visible in the methodology or local - README, and promote the case to default coverage with the fix. + `RUN_KNOWN_ISSUE_TESTS`. Name the tracked defect, expected failure, and + condition for restoring the case to default coverage in the methodology or + local README, and promote the case with the fix. - Gate an unusually long soak or stress matrix with a separately named `RUN_*_EXTENDED_TESTS` variable; do not label stable-but-slow coverage as a known issue. @@ -297,8 +346,10 @@ result file, or other process-global resource. Tests that launch peer processes must use bounded startup/shutdown waits and clean them up in a `finally` block or shared teardown helper, including after an assertion fails. -After any command that launches pytest, cocotb, GHDL, or another simulator -runner, check for stale simulator child processes before starting another run. +The runner or test fixture should clean up simulator children and external +peers during normal execution. After an interrupted or hung run, check for stale +processes before retrying so they cannot retain a build directory, port, or +license. ## VHDL Wrappers @@ -351,10 +402,16 @@ Before considering a new regression ready: - The Python file has the standard license header, a specific methodology block, and comments around non-obvious cocotb sequencing. - The test asserts behavior rather than merely reaching the end of simulation. +- A bug regression was shown to fail on known-bad RTL when practical, or the + limitation and defect-catching assertion are documented. +- Inapplicable scenarios are selected out or reported as skipped; no entrypoint + silently returns before exercising its named behavior. - Every wait is bounded directly or by a helper with a cycle/time limit. - Reset, backpressure, sidebands, and error/boundary cases relevant to the DUT are covered or explicitly documented as out of scope. - Shared helpers were reused or extended instead of duplicated. +- Finite background tasks are awaited, lifetime agents have explicit ownership, + and external resources are cleaned up on failure. - Random stimulus is seeded and failures report enough information to replay the case. - The case is safe under pytest-xdist: build artifacts and external resources @@ -363,7 +420,10 @@ Before considering a new regression ready: `vsg-linter.yml`, and reachable through its intended source path: the nearest ruckus manifest for build-facing HDL or `extra_vhdl_sources` for a cocotb-only wrapper. +- `extra_vhdl_sources` does not repeat production HDL already supplied by the + ruckus import. - The focused test and the nearest practical subsystem suite pass. -- `git diff --check` is clean and no stale simulator process remains. +- `git diff --check` is clean; after an interrupted run, no stale simulator or + peer process remains. - The nearest README is updated if the test introduces a new layout, helper, simulator requirement, deferred dependency, or non-obvious invocation. diff --git a/tests/common/README.md b/tests/common/README.md index 1d2cadf029..c377a91ff2 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -24,7 +24,7 @@ PARAMETER_SWEEP = [ def test_my_target(parameters): run_surf_vhdl_test( test_file=__file__, - toplevel="MyTargetWrapper", + toplevel="surf.MyTargetWrapper", parameters=hdl_parameters_from(parameters), extra_env=parameters, ) @@ -34,17 +34,20 @@ Use `parameters` only for VHDL generics. Use `extra_env` for Python-side case metadata. When one dictionary contains both, pass it through `hdl_parameters_from()` before giving it to the simulator. -Use `extra_vhdl_sources` for a wrapper or simulation model that is intentionally -outside the imported SURF source list. Production RTL belongs in the nearest -`ruckus.tcl`; do not use this argument to hide a missing build-manifest entry or -repeat a unit already supplied by the import, which can produce a library -redefinition error. - -Use `sim_build_key` only when normal parameter-derived names would be too long -or when a subsystem requires a deliberately stable build location. The default -path already includes `parameters` and `extra_env`, which isolates parallel -cases. A custom key must preserve that isolation; never point concurrently -runnable variants at the same build directory. +Use `extra_vhdl_sources` only for a cocotb-only wrapper or simulation model whose +design unit is absent from the ruckus import. Check the imported source tree +under `build/SRC_VHDL/` before adding a path. Production and reusable wrapper RTL +belongs in the nearest `ruckus.tcl`; do not use this argument to hide a missing +build-manifest entry or repeat an imported design unit. Compiling the same unit +from both paths can redefine it, make compile order significant, or leave a +cached build using a different source than the reviewer expects. + +The default build path includes `parameters` and `extra_env`, and the shared +runner hashes path components that would be unsafe or excessively long. Use +`sim_build_key` only when a subsystem requires a deliberately stable or more +meaningful build identity. A custom key must still distinguish every +concurrently runnable compile configuration and selected cocotb scenario; never +point incompatible variants at the same build directory. Leave `force_compile=False` for normal regressions. Set it only when a documented source-topology or simulator-cache limitation makes reuse unsafe; @@ -105,4 +108,14 @@ so a broken handshake becomes a useful failure instead of a hung worker. When a pytest wrapper selects one of several cocotb entrypoints, pass the selector in `extra_env`. Because the shared runner includes `extra_env` in the -default build path, selected scenarios remain isolated under pytest-xdist. +default build path, selected scenarios remain isolated under pytest-xdist. Give +the selector a deterministic default and make each pytest node run only the +scenario or coherent scenario group named by that node; do not use a bare return +inside an otherwise selected cocotb test to turn an inapplicable case into a +pass. + +Retain every task returned by `cocotb.start_soon()`. Await finite producers, +consumers, and transactions before the entrypoint completes. Store monitors or +protocol peers intended to run for the whole test on the bench, document them as +lifetime agents, and provide cleanup when cancellation order matters or an +agent owns a socket, process, file, or other external resource. diff --git a/tests/protocols/README.md b/tests/protocols/README.md index f3e34659fd..a557cd2dd8 100644 --- a/tests/protocols/README.md +++ b/tests/protocols/README.md @@ -11,6 +11,12 @@ and mechanical source/sink helpers in the subsystem's `*_test_utils.py` module. Derive them from the normative protocol specification or the established SURF package definitions, not from copied literals in individual tests. +Anchor the oracle with small published or hand-worked known-answer vectors +before trusting it for randomized or integration traffic. Keep at least one +vector independent of the DUT implementation and, where practical, independent +of the helper's encoder path as well. A Python model that transliterates the RTL +line by line can reproduce the same defect and is not an independent oracle. + Keep policy assertions in the test that names the behavior. A helper may build a DATA frame or calculate its checksum; the test should still say that an out-of-order frame must be dropped, an EOFE marker must propagate, or a timeout @@ -41,6 +47,12 @@ An integration test should focus on what the integrated layer adds. Reuse the same protocol oracle, but do not duplicate every leaf permutation at the top level. +As a suite grows, split it by coherent behavior rather than by an arbitrary +line limit: encoding, transmit, receive, flow control/recovery, register +control, and integration are useful boundaries. Keep shared frames and +mechanics in the oracle module, while each test module owns the assertions and +methodology for the behavior named by that file. + ## Required Protocol Cases Choose the cases relevant to the DUT, including: diff --git a/tests/protocols/batcher/README.md b/tests/protocols/batcher/README.md index 98ada89b3e..de3a396d4d 100644 --- a/tests/protocols/batcher/README.md +++ b/tests/protocols/batcher/README.md @@ -23,6 +23,12 @@ test; arbitration, routing, and cross-source policy belong in the event-builder test. Extend `batcher_test_utils.py` for reusable mechanics, but leave the policy being asserted visible in the individual test. +Routed and unrouted configurations do not make every scenario applicable. Make +that relationship explicit in the pytest parameter/selector matrix, or report +the scenario as skipped with the configuration in the reason. Do not enter a +cocotb test and return successfully before its named routing, remapping, or +transition behavior has been exercised. + Run the suite with: ```bash diff --git a/tests/protocols/rssi/README.md b/tests/protocols/rssi/README.md index 5354924812..c2246e46c4 100644 --- a/tests/protocols/rssi/README.md +++ b/tests/protocols/rssi/README.md @@ -30,8 +30,17 @@ The suite progresses from leaves to integration: Default CI runs the currently stable RSSI cases. Focused cases that still expose unresolved RTL behavior are opt-in behind `RUN_RSSI_KNOWN_ISSUE_TESTS=1`, and a smaller group of long-running integration cases additionally uses -`RUN_RSSI_EXTENDED_TESTS=1`. Keep the skip reason beside each gated pytest entry -and promote the case to default coverage when its blocking RTL issue is fixed. +`RUN_RSSI_EXTENDED_TESTS=1`. A `COCOTB_TESTCASE` selector chooses a named +cocotb scenario; the `RUN_*` gates decide whether the corresponding pytest node +is eligible to launch it. Keep these roles separate so an enabled node cannot +silently run unrelated scenarios. + +Keep the skip reason beside each gated pytest entry. A known-issue case must +identify a durable defect reference or documented local issue, state the +expected failure, and say what change allows the gate to be removed. Promote the +case to default coverage in the same change that fixes the blocking RTL. Keep +stable-but-long coverage under the extended gate rather than calling it a known +issue. ## RSSI-Specific Expectations @@ -47,6 +56,13 @@ headers, bad checksums, and out-of-order frames do not leak application payload. Recovery cases should then send valid traffic and prove that the endpoint makes forward progress without duplicate delivery. +When one methodology block can no longer describe a coherent set of scenarios, +split the integration suite by behavior while continuing to share the RSSI +oracle. Useful boundaries are negotiation and close, data and retransmission, +flow control and keepalive, connection lifecycle, AXI-Lite control, and +multi-stream integration. Preserve the existing pytest case names and gate +semantics during such a split so coverage does not disappear unnoticed. + Run the default suite with: ```bash From 304d5b49a3bedddcf9a7b6499313c5420159d178 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 08:12:36 -0700 Subject: [PATCH 35/57] Fix RSSI segment FIFO pause threshold for small buffers RssiCore passed (2**SEGMENT_ADDR_SIZE_G)-16 directly into the FIFO_PAUSE_THRESH_G generic of both the application and transport AxiStreamFifoV2 instances. That generic is declared as "integer range 1 to (2**24)", so the expression has to stay positive. It does not once the segment buffer is 16 words or smaller: SEGMENT_ADDR_SIZE_G = 3 -> -8 SEGMENT_ADDR_SIZE_G = 4 -> 0 Nothing rejects that range up front. SEGMENT_ADDR_SIZE_G carries no range constraint, and the existing MAX_SEG_SIZE_G assertion explicitly permits it, so the only symptom is a bare "value out of range" elaboration error pointing at the instantiation rather than at the cause. RssiCoreWrapper reaches the range through ordinary use because it derives the generic as bitSize(MAX_SEG_SIZE_G/RSSI_WORD_WIDTH_C-1), so MAX_SEG_SIZE_G of 128 yields 4 and MAX_SEG_SIZE_G of 64 yields 3. Cap the 16 word padding at half a segment instead of flooring the result. That keeps the "one segment early, minus padding" intent proportional for small buffers rather than collapsing to a threshold of 1, which would pause the TX and RX FSMs after only two buffered words. Thresholds for SEGMENT_ADDR_SIZE_G of 5 or more, including the default of 7, are unchanged. --- protocols/rssi/v1/rtl/RssiCore.vhd | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/protocols/rssi/v1/rtl/RssiCore.vhd b/protocols/rssi/v1/rtl/RssiCore.vhd index faa7ef90b0..a06c6bdfba 100644 --- a/protocols/rssi/v1/rtl/RssiCore.vhd +++ b/protocols/rssi/v1/rtl/RssiCore.vhd @@ -89,7 +89,7 @@ entity RssiCore is -- Counters MAX_RETRANS_CNT_G : positive := 2; MAX_CUM_ACK_CNT_G : positive := 3 - ); + ); port ( clk_i : in sl; rst_i : in sl; @@ -128,6 +128,10 @@ end entity RssiCore; architecture rtl of RssiCore is constant BUFFER_ADDR_WIDTH_C : positive := (SEGMENT_ADDR_SIZE_G+WINDOW_ADDR_SIZE_G); + -- Pause one segment early, padded by 16 words. The padding is capped at half + -- a segment so that segment buffers smaller than 32 words keep a valid, + -- proportional threshold instead of going non-positive. + constant FIFO_PAUSE_THRESH_C : positive := (2**SEGMENT_ADDR_SIZE_G) - minimum(16, 2**(SEGMENT_ADDR_SIZE_G-1)); -- Busy Flags signal s_localBusy : sl; @@ -291,8 +295,8 @@ architecture rtl of RssiCore is -- attribute dont_touch of s_mAppAxisCtrl : signal is "TRUE"; -- attribute dont_touch of s_mTspAxisCtrl : signal is "TRUE"; ----------------------------------------------------------------------- begin + -- Assertions to check generics assert (1 <= MAX_NUM_OUTS_SEG_G and MAX_NUM_OUTS_SEG_G <= (2**WINDOW_ADDR_SIZE_G)) report "MAX_NUM_OUTS_SEG_G should be less or equal to 2**WINDOW_ADDR_SIZE_G" severity failure; assert (8 <= MAX_SEG_SIZE_G and MAX_SEG_SIZE_G <= (2**SEGMENT_ADDR_SIZE_G)*8) report "MAX_SEG_SIZE_G should be less or equal to (2**SEGMENT_ADDR_SIZE_G)*8" severity failure; @@ -696,7 +700,7 @@ begin TPD_G => TPD_G, DATA_WIDTH_G => 64, CSUM_WIDTH_G => 16 - ) + ) port map ( clk_i => clk_i, rst_i => rst_i, @@ -784,7 +788,7 @@ begin TPD_G => TPD_G, DATA_WIDTH_G => 64, CSUM_WIDTH_G => 16 - ) + ) port map ( clk_i => clk_i, rst_i => rst_i, @@ -828,7 +832,7 @@ begin MEMORY_TYPE_G => "block", FIFO_ADDR_WIDTH_G => SEGMENT_ADDR_SIZE_G+1, -- Enough to store 2 segments FIFO_FIXED_THRESH_G => true, - FIFO_PAUSE_THRESH_G => (2**SEGMENT_ADDR_SIZE_G) - 16, -- Threshold at 1 segment minus padding + FIFO_PAUSE_THRESH_G => FIFO_PAUSE_THRESH_C, -- Threshold at 1 segment minus padding INT_WIDTH_SELECT_G => "CUSTOM", INT_DATA_WIDTH_G => RSSI_WORD_WIDTH_C, SLAVE_AXI_CONFIG_G => RSSI_AXIS_CONFIG_C, @@ -860,7 +864,7 @@ begin MEMORY_TYPE_G => "block", FIFO_ADDR_WIDTH_G => SEGMENT_ADDR_SIZE_G+1, -- Enough to store 2 segments FIFO_FIXED_THRESH_G => true, - FIFO_PAUSE_THRESH_G => (2**SEGMENT_ADDR_SIZE_G) - 16, -- Threshold at 1 segment minus padding + FIFO_PAUSE_THRESH_G => FIFO_PAUSE_THRESH_C, -- Threshold at 1 segment minus padding INT_WIDTH_SELECT_G => "CUSTOM", INT_DATA_WIDTH_G => RSSI_WORD_WIDTH_C, SLAVE_AXI_CONFIG_G => RSSI_AXIS_CONFIG_C, From 9458e3e1a7af9da967988fae01490d9f47df3304 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 10:11:43 -0700 Subject: [PATCH 36/57] test: establish regression compliance framework --- .../regression-test-compliance/README.md | 287 +++++++ tests/common/README.md | 48 ++ tests/common/compliance_audit.py | 811 ++++++++++++++++++ tests/common/compliance_baseline.json | 86 ++ tests/common/test_compliance_audit.py | 367 ++++++++ 5 files changed, 1599 insertions(+) create mode 100644 docs/plans/regression-test-compliance/README.md create mode 100644 tests/common/compliance_audit.py create mode 100644 tests/common/compliance_baseline.json create mode 100644 tests/common/test_compliance_audit.py diff --git a/docs/plans/regression-test-compliance/README.md b/docs/plans/regression-test-compliance/README.md new file mode 100644 index 0000000000..71eb279eee --- /dev/null +++ b/docs/plans/regression-test-compliance/README.md @@ -0,0 +1,287 @@ +# Regression Test Compliance + +## Goal And Status + +Bring the existing cocotb regression code into closer alignment with the +authoritative [test guidance](../../../tests/README.md) without deleting +coverage, hiding scenarios, or attempting a disruptive whole-tree rewrite. + +Status: implementation is in progress on `verification-2-test-compliance`. +Phase 0 delivered the structural audit, preservation-report comparison, and +checked-in baseline ratchet under `tests/common/`. This branch was created from +`verification-2` so the implementation can use the new guidance while it is +reviewed. Test-code commits from this effort must not be merged back into +`verification-2`. Once the documentation branch lands, rebase or retarget the +implementation onto `pre-release`. + +This effort improves the structure and reliability of existing tests. It does +not reinstate the historical module-coverage queue or require new functional +tests for unrelated RTL. + +## Non-Negotiable Preservation Rules + +- Do not remove an existing test, parameter case, opt-in gate, or behavioral + assertion merely to make the suite conform to the style guide. +- Before changing a subsystem, record its pytest nodes, cocotb entrypoints, + parameter IDs, skips, known-issue gates, and extended-test gates. Afterward, + account explicitly for every removed, renamed, split, or consolidated case. +- A moved or split test must retain equivalent DUT stimulus and assertions. + Preserve public pytest node names and gate semantics when practical; otherwise + provide a before/after mapping in the commit or pull-request description. +- Treat a reduced test count, an unexpected skip, or a formerly exercised + cocotb entrypoint becoming unreachable as a regression until explained. +- Keep compliance cleanup separate from unrelated RTL behavior changes. A + discovered RTL defect should receive a focused reproducer/fix change rather + than being masked by relaxing an assertion. +- Make changes one subsystem or one shared mechanism at a time, and validate + both serial execution and the parallel mode used by CI. + +## Audit Baseline + +The initial source scan produced the following triage baseline. These are +screening signals, not confirmed violations; every site needs semantic review. + +- 726 cocotb entrypoints were found across 320 Python files. +- The AST audit reports 99 entrypoints in 33 files with a bare return within 12 + lines of the function declaration, plus 14 later bare returns in nine files. + Early returns are the highest priority because an inapplicable scenario can + be recorded as a pass. +- At least 119 literal paths passed through `extra_vhdl_sources`, across 70 + files, resolve to design units already present in the current ruckus import. + Variable-generated lists mean the true count may be higher. +- 447 non-clock `cocotb.start_soon()` calls were found. Of those, 61 calls in 23 + test modules and two helpers do not retain the returned task. Many may be + legitimate lifetime agents, but their ownership is currently implicit. +- The AST audit reports 304 directly adjacent edge-then-delay pairs across 129 + files. The initial broader text scan found 574 candidates. Some correctly + model a real nonzero `TPD_G`; others may only be compensating for delta-cycle + scheduling. +- 66 open-ended `while True` loop sites occur across 38 files, while 42 files + use `with_timeout()`. The counts do not establish whether an enclosing wait + is bounded. +- Two cocotb entrypoints use decorator-level timeouts. This is not a target + count: bounded operation waits are preferable, with decorator watchdogs + reserved for complex concurrent tests. +- One ordinary GHDL regression, `test_AxiStreamDmaV2Read.py`, bypasses the + shared runner without an obvious simulator-specific requirement. +- Four cocotb files lack the labeled methodology marker used by the structural + ratchet; they remain in the initial legacy baseline pending focused review. + +The largest reviewed test modules are useful structural-cleanup candidates: + +| Test module | Approximate lines | Cocotb tests | +| --- | ---: | ---: | +| `test_RssiCore.py` | 1,453 | 26 | +| `test_Jesd204bLoopback.py` | 1,254 | 4 | +| `test_CoaXPressRx.py` | 1,174 | 8 | +| `test_RoCEv2AxiStreamRdmaCore.py` | 848 | 16 | +| `test_AxiStreamFifoV2IpIntegrator.py` | 843 | 6 | + +Re-run the audit after material tree changes and before declaring the effort +complete. Record both the raw signal count and the number confirmed to require +change so the metrics are not mistaken for defect counts. + +## Implementation Plan + +### Phase 0: Capture And Ratchet The Baseline + +Create a lightweight, read-only audit under `tests/common/` that can report the +screening categories above. Where a static rule is reliable, add a checked-in +legacy allowlist so new or substantially edited tests cannot introduce another +violation while existing cases are migrated gradually. + +The first enforcement candidates are methodology-header presence, selected +tests that silently return before stimulus, ordinary direct-runner exceptions, +and literal duplicate imported sources. Coroutine and timing scans should begin +as reports because static syntax cannot determine intent reliably. + +Add a repeatable preservation report for each migrated subsystem. At minimum it +should capture pytest collection and the discovered cocotb test names. For gated +suites, capture the default, known-issue, and extended configurations +separately. Do not encode one expected repository-wide test count that changes +whenever legitimate coverage is added. + +### Phase 1: Eliminate No-Op Passes + +Review the 97 early-return candidates first, one subsystem at a time. + +- Move configuration applicability into the pytest parameter or selector + matrix when the scenario should not launch. +- Use an explicit skip with a configuration-specific reason when collection is + valuable but execution is unavailable. +- Keep an early return only when it follows meaningful assertions and represents + an intentional successful terminal path; add a short comment where that is + not self-evident. +- Start with batcher and RSSI because their routed/unrouted and gated scenarios + now have explicit local documentation. Continue with configuration-heavy AXI + and protocol suites. + +For each migrated subsystem, compare the preservation report before and after, +then run the focused suite serially and with pytest-xdist. + +### Phase 2: Normalize Runner And Source Handling + +Remove `extra_vhdl_sources` entries that duplicate units supplied by the ruckus +import. Audit exceptions rather than deleting every matching path mechanically; +a test may intentionally compile a different implementation or library, which +must be documented and isolated explicitly. + +After cleaning the callers, enhance `run_surf_vhdl_test()` or its source-merge +path to diagnose duplicate resolved files/design units before compilation. Use +an explicit, reviewed escape hatch only if a legitimate override exists. + +Migrate `test_AxiStreamDmaV2Read.py` to the shared runner if the focused audit +confirms that it needs no special lifecycle or simulator capability. Review +`force_compile=True` callers at the same time, but remove that setting only +after clean and cached builds prove reuse is safe. + +Validate source changes with a fresh `make MODULES="$PWD" import`, a clean +simulation build, a repeated cached run, and an xdist run. Source cleanup must +not change the elaborated DUT boundary. + +### Phase 3: Make Coroutine And Resource Ownership Explicit + +Classify each unretained non-clock task as finite work, a lifetime agent, or an +actual leak. + +- Await finite producers, consumers, transactions, and checks before the test + completes. +- Store monitors and protocol peers on the bench with names that communicate + their purpose. +- Add a small shared task registry/cleanup helper only if repeated lifecycle + mechanics justify it; avoid forcing simple leaf tests into a large framework. +- Put process, socket, port, temporary-file, and native-library cleanup in a + `finally` block or fixture teardown that also runs after assertion failure. +- Add bounded operation waits first. Add a decorator-level deadlock watchdog to + complex integration entrypoints only after their normal progress waits are + bounded. + +Prioritize RSSI core/wrappers, CoaXPress, RoCEv2, JESD204B, SimLink, and other +tests with several concurrent agents or external resources. + +### Phase 4: Clarify Sampling And Propagation Timing + +Classify edge-plus-delay sites into two semantic categories: + +1. delta-cycle settling after a clock edge, which should use `ReadOnly()`; and +2. real modeled propagation delay from a nonzero VHDL `after TPD_G`, which + should wait for the configured delay and then sample a stable value. + +Introduce narrowly named shared helpers for these two cases if they remove +repeated mechanics without hiding intent. Migrate high-reuse helper modules +before leaf tests. Do not perform a global textual replacement: changing a real +propagation wait to `ReadOnly()` can create a race, while retaining an arbitrary +fixed timer can conceal one. + +Timing cleanup must retain accepted-handshake behavior and should be validated +under both serial and parallel execution. For parameterized `TPD_G`, include at +least the default and a representative nonzero value when practical. + +### Phase 5: Improve Oracles, Diagnostics, And Structure + +Anchor protocol reference models with published or hand-worked known-answer +vectors independent of both the DUT and the main encoder path. Add these while +touching a protocol suite rather than launching a broad rewrite. + +Improve assertion context where pytest's rewritten expression is insufficient, +especially for parameter cases, frames, beats, sequence numbers, expected versus +observed sidebands, and randomized seeds. Do not mechanically add messages to +simple assertions that are already diagnostic. + +Split oversized modules by coherent behavior, not a line-count threshold. The +first candidates and suggested boundaries are: + +- RSSI core: negotiation/close, data/retransmission, flow control/keepalive, + connection lifecycle, and AXI-Lite control. +- JESD204B loopback: configuration/bring-up, data-path checking, error/recovery, + and long-running integration behavior. +- CoaXPress RX: framing/decoding, trigger/control behavior, error/recovery, and + integration scenarios. +- RoCEv2 RDMA: packet/oracle mechanics, operation classes, errors, and + multi-transaction integration. +- AXI Stream FIFO integration: configuration families, traffic/backpressure, + sideband behavior, and reset/status behavior. + +Keep shared mechanics in `*_test_utils.py`; keep the policy assertions and a +specific methodology block in the test module that owns each behavior. Preserve +case names and gates through every split. + +### Phase 6: Make The Ratchet Blocking + +Once each audit rule has low false-positive risk, make it blocking for new and +modified tests. Retain reviewed legacy exceptions and remove them as migration +PRs land. The final policy should reject: + +- a newly introduced no-op passing scenario; +- an unexplained duplicate ruckus/extra-source design unit; +- a new ordinary direct runner without a documented need; +- an unowned external resource or finite background transaction; and +- a methodology or local invocation that contradicts `tests/README.md`. + +Update `scripts/setup_regression_env.sh` separately so its suggested pytest +commands match the current directory layout and authoritative README. Keep this +small operational cleanup out of behavior-heavy migration commits. + +## Pull Request Slicing + +Prefer a sequence of independently reviewable pull requests rather than one +repository-wide compliance change: + +1. Audit/report infrastructure with nonblocking legacy baselines. +2. No-op-pass cleanup, one subsystem per PR. +3. Duplicate-source cleanup and runner enforcement, grouped by related source + manifests. +4. Coroutine/resource ownership, grouped by integration family. +5. Timing-helper introduction followed by subsystem migrations. +6. Large-suite splits, one suite per PR. +7. CI ratchet promotion as each rule becomes trustworthy. + +Every implementation PR should ultimately target `pre-release`, identify the +baseline commit used for comparison, include the before/after preservation +report, list focused and parallel validation, and state any remaining legacy +exceptions. Do not merge implementation commits into `verification-2`. + +## Validation Expectations + +Apply validation in proportion to each change, with the following minimums: + +- `git diff --check` and focused Python lint for edited test code. +- `make MODULES="$PWD" import` for source-list or runner-source changes. +- A serial focused pytest run with `-n 0` for readable simulator evidence. +- The nearest practical subsystem suite under pytest-xdist. +- Default and explicitly gated configurations when selection/gating changes. +- Fresh and cached simulation builds when build identity or source merging + changes. +- Before/after pytest/cocotb case accounting for every move, split, skip, or + selector change. +- VSG lint for any edited wrapper VHDL. + +No phase is complete solely because its screening count reaches zero. The +behavioral preservation report and focused regressions are the acceptance +criteria. + +## Risks And Mitigations + +- **Coverage loss during cleanup:** require before/after case accounting and + treat unexplained count reductions as failures. +- **False positives from static scans:** begin ambiguous rules as reports, use + reviewed allowlists, and enforce only semantically reliable checks. +- **Simulator cache changes hiding source mistakes:** validate both clean and + cached builds and reject duplicate design units before compilation. +- **Timing cleanup introducing races:** distinguish delta settling from real + `TPD_G` waits and migrate shared helpers incrementally. +- **Large diffs becoming unreviewable:** keep subsystem and mechanism changes in + separate PRs and avoid unrelated RTL/style cleanup. +- **Branch topology violating the documentation-only contract:** never merge + this implementation branch into `verification-2`; rebase or retarget onto + `pre-release` once the documentation is available there. + +## Immediate Next Steps + +1. Use batcher as the first no-op-pass migration pilot and remove its reduced + baseline entries in the same change. +2. Use RSSI as the first gated integration and coroutine-lifecycle pilot. +3. Review the first two migrations before extending the blocking ratchet to an + additional structural rule. +4. Begin duplicate-source cleanup by subsystem after the no-op-pass selection + pattern is established. diff --git a/tests/common/README.md b/tests/common/README.md index c377a91ff2..c0eb7125d0 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -119,3 +119,51 @@ consumers, and transactions before the entrypoint completes. Store monitors or protocol peers intended to run for the whole test on the bench, document them as lifetime agents, and provide cleanup when cancellation order matters or an agent owns a socket, process, file, or other external resource. + +## Compliance Audit And Preservation Reports + +`compliance_audit.py` provides a read-only structural audit and a reproducible +inventory for cleanup work. The audit reports screening signals; ambiguous +items such as lifetime tasks, open-ended agent loops, and post-edge delays still +require review rather than mechanical replacement. + +Run an audit for the whole active test tree or one subsystem: + +```bash +./.venv/bin/python -m tests.common.compliance_audit audit tests +./.venv/bin/python -m tests.common.compliance_audit audit tests/protocols/batcher +``` + +Capture a preservation report before changing a subsystem, capture it again +afterward, and compare the two: + +```bash +./.venv/bin/python -m tests.common.compliance_audit \ + inventory tests/protocols/batcher --output /tmp/batcher-before.json +./.venv/bin/python -m tests.common.compliance_audit \ + inventory tests/protocols/batcher --output /tmp/batcher-after.json +./.venv/bin/python -m tests.common.compliance_audit \ + compare /tmp/batcher-before.json /tmp/batcher-after.json +``` + +The comparison exits unsuccessfully when a pytest function, cocotb entrypoint, +parameter ID, environment gate/selector, skip, or decorator timeout disappears. +An intentional rename, move, split, or consolidation therefore remains visible +and needs an explicit before/after mapping in the change description. Added +coverage is reported but does not make the command fail. + +The checked-in `compliance_baseline.json` ratchets the rules that are reliable +enough to enforce structurally: methodology presence, ordinary direct-runner +exceptions, and literal VHDL sources duplicated from the ruckus import. Run the +check after importing the HDL tree: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m tests.common.compliance_audit check tests +``` + +The check fails when a file introduces a new finding or exceeds its existing +per-rule count. Removing a legacy finding is allowed and reported as a baseline +reduction; update the baseline in the same cleanup change so the violation +cannot return. Do not regenerate the whole baseline to accept an unrelated new +finding. diff --git a/tests/common/compliance_audit.py b/tests/common/compliance_audit.py new file mode 100644 index 0000000000..69787eb1cd --- /dev/null +++ b/tests/common/compliance_audit.py @@ -0,0 +1,811 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +from __future__ import annotations + +import argparse +import ast +from collections import Counter +from dataclasses import asdict, dataclass +import json +from pathlib import Path +import re +import sys +from typing import Iterable, Iterator + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCHEMA_VERSION = 1 +DEFAULT_BASELINE = Path(__file__).with_name("compliance_baseline.json") + +ENFORCED_RULES = { + "direct-runner", + "duplicate-imported-source", + "missing-methodology", +} + +EXCLUDED_DIRECTORY_NAMES = { + "__pycache__", + "legacy", + "sim_build", +} + +METHODOLOGY_MARKER = "Test methodology:" +ENVIRONMENT_CONTROL_RE = re.compile( + r"^(?:RUN_[A-Z0-9_]*_TESTS|COCOTB_TESTCASE|[A-Z0-9_]*TESTCASE)$" +) + +DOCUMENTED_DIRECT_RUNNERS = { + "tests/common/regression_utils.py", + "tests/simlink/ghdl/simlink_test_utils.py", +} + + +@dataclass(frozen=True, order=True) +class Finding: + rule: str + path: str + line: int + symbol: str + detail: str + + def to_dict(self) -> dict[str, object]: + return asdict(self) + + +@dataclass(frozen=True) +class FileInventory: + cocotb_entrypoints: tuple[str, ...] + pytest_functions: tuple[str, ...] + parameter_ids: tuple[str, ...] + environment_controls: tuple[str, ...] + skipped_functions: tuple[str, ...] + timeout_entrypoints: tuple[str, ...] + + def to_dict(self) -> dict[str, object]: + return { + key: list(value) + for key, value in asdict(self).items() + } + + +@dataclass(frozen=True) +class PreservationDelta: + removed: dict[str, dict[str, tuple[str, ...]]] + added: dict[str, dict[str, tuple[str, ...]]] + + @property + def has_removals(self) -> bool: + return bool(self.removed) + + def to_dict(self) -> dict[str, object]: + return { + "removed": _nested_tuples_to_lists(self.removed), + "added": _nested_tuples_to_lists(self.added), + } + + +@dataclass(frozen=True) +class BaselineDelta: + new: dict[str, dict[str, int]] + reduced: dict[str, dict[str, int]] + + @property + def has_new(self) -> bool: + return bool(self.new) + + def to_dict(self) -> dict[str, object]: + return { + "new": self.new, + "reduced": self.reduced, + } + + +def _nested_tuples_to_lists( + values: dict[str, dict[str, tuple[str, ...]]], +) -> dict[str, dict[str, list[str]]]: + return { + path: { + category: list(items) + for category, items in categories.items() + } + for path, categories in values.items() + } + + +def _qualified_name(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + prefix = _qualified_name(node.value) + return f"{prefix}.{node.attr}" if prefix else node.attr + if isinstance(node, ast.Call): + return _qualified_name(node.func) + return "" + + +def _is_cocotb_test(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + for decorator in function.decorator_list: + target = decorator.func if isinstance(decorator, ast.Call) else decorator + if _qualified_name(target) == "cocotb.test": + return True + return False + + +def _has_timeout(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + for decorator in function.decorator_list: + if not isinstance(decorator, ast.Call): + continue + if _qualified_name(decorator.func) != "cocotb.test": + continue + if any(keyword.arg == "timeout_time" for keyword in decorator.keywords): + return True + return False + + +def _has_skip(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + for decorator in function.decorator_list: + name = _qualified_name(decorator) + if any(part.startswith("skip") for part in name.split(".")): + return True + + for node in _walk_function(function): + if isinstance(node, ast.Call) and _qualified_name(node.func) == "pytest.skip": + return True + return False + + +class _FunctionWalker(ast.NodeVisitor): + def __init__(self, root: ast.FunctionDef | ast.AsyncFunctionDef): + self.root = root + self.nodes: list[ast.AST] = [] + + def generic_visit(self, node: ast.AST) -> None: + self.nodes.append(node) + super().generic_visit(node) + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + if node is self.root: + self.generic_visit(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + if node is self.root: + self.generic_visit(node) + + def visit_Lambda(self, node: ast.Lambda) -> None: + self.nodes.append(node) + + +def _walk_function( + function: ast.FunctionDef | ast.AsyncFunctionDef, +) -> tuple[ast.AST, ...]: + walker = _FunctionWalker(function) + walker.visit(function) + return tuple(walker.nodes) + + +def _top_level_functions( + tree: ast.Module, +) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ...]: + return tuple( + node + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + ) + + +def _all_functions( + tree: ast.Module, +) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ...]: + return tuple( + sorted( + ( + node + for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + ), + key=lambda node: (node.lineno, node.name), + ) + ) + + +def _string_constants(node: ast.AST) -> Iterator[tuple[str, int]]: + for child in ast.walk(node): + if isinstance(child, ast.Constant) and isinstance(child.value, str): + yield child.value, child.lineno + + +def _parameter_ids(tree: ast.Module) -> tuple[str, ...]: + result = set() + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + + name = _qualified_name(node.func) + if name == "pytest.param": + for keyword in node.keywords: + if keyword.arg == "id" and isinstance(keyword.value, ast.Constant): + if isinstance(keyword.value.value, str): + result.add(keyword.value.value) + elif name.endswith("parameter_case") and node.args: + first = node.args[0] + if isinstance(first, ast.Constant) and isinstance(first.value, str): + result.add(first.value) + return tuple(sorted(result)) + + +def _environment_controls(tree: ast.Module) -> tuple[str, ...]: + controls = { + value + for value, _ in _string_constants(tree) + if ENVIRONMENT_CONTROL_RE.fullmatch(value) + } + return tuple(sorted(controls)) + + +def inventory_source(source: str, path: str = "test.py") -> FileInventory: + tree = ast.parse(source, filename=path) + functions = _top_level_functions(tree) + cocotb_functions = tuple(function for function in functions if _is_cocotb_test(function)) + + return FileInventory( + cocotb_entrypoints=tuple(sorted(function.name for function in cocotb_functions)), + pytest_functions=tuple( + sorted( + function.name + for function in functions + if function.name.startswith("test_") and not _is_cocotb_test(function) + ) + ), + parameter_ids=_parameter_ids(tree), + environment_controls=_environment_controls(tree), + skipped_functions=tuple( + sorted(function.name for function in functions if _has_skip(function)) + ), + timeout_entrypoints=tuple( + sorted(function.name for function in cocotb_functions if _has_timeout(function)) + ), + ) + + +def _repo_relative(path: Path, repo_root: Path) -> str: + try: + return path.resolve().relative_to(repo_root.resolve()).as_posix() + except ValueError: + return path.resolve().as_posix() + + +def discover_python_files( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> tuple[Path, ...]: + result = set() + requested = tuple(paths) or ("tests",) + + for raw_path in requested: + path = Path(raw_path) + if not path.is_absolute(): + path = repo_root / path + + if path.is_file(): + if path.suffix == ".py": + result.add(path.resolve()) + continue + + if not path.exists(): + raise FileNotFoundError(path) + + for candidate in path.rglob("*.py"): + relative_parts = candidate.relative_to(path).parts + if any(part in EXCLUDED_DIRECTORY_NAMES for part in relative_parts): + continue + result.add(candidate.resolve()) + + return tuple(sorted(result)) + + +def inventory_paths( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> dict[str, FileInventory]: + report = {} + for path in discover_python_files(paths, repo_root): + source = path.read_text(encoding="utf-8") + inventory = inventory_source(source, _repo_relative(path, repo_root)) + if any(asdict(inventory).values()): + report[_repo_relative(path, repo_root)] = inventory + return dict(sorted(report.items())) + + +def _parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST]: + return { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + + +def _is_clock_start(call: ast.Call) -> bool: + if not call.args: + return False + try: + expression = ast.unparse(call.args[0]) + except AttributeError: + return False + return "Clock(" in expression or "start_lockstep_clocks" in expression + + +def _await_call_name(statement: ast.stmt) -> str: + if not isinstance(statement, ast.Expr) or not isinstance(statement.value, ast.Await): + return "" + awaited = statement.value.value + if not isinstance(awaited, ast.Call): + return "" + return _qualified_name(awaited.func) + + +def _statement_lists(node: ast.AST) -> Iterator[list[ast.stmt]]: + for _, value in ast.iter_fields(node): + if isinstance(value, list) and value and all(isinstance(item, ast.stmt) for item in value): + yield value + for statement in value: + yield from _statement_lists(statement) + elif isinstance(value, ast.AST): + yield from _statement_lists(value) + + +def _direct_runner_aliases(tree: ast.Module) -> set[str]: + aliases = set() + for node in tree.body: + if not isinstance(node, ast.ImportFrom) or node.module != "cocotb_test.simulator": + continue + for name in node.names: + if name.name == "run": + aliases.add(name.asname or name.name) + return aliases + + +def _imported_source_paths(repo_root: Path) -> frozenset[Path]: + paths = set() + build_root = repo_root / "build" / "SRC_VHDL" + if not build_root.exists(): + return frozenset() + for library in build_root.iterdir(): + if not library.is_dir(): + continue + for path in library.iterdir(): + if path.is_file(): + paths.add(path.resolve()) + return frozenset(paths) + + +def audit_source( + source: str, + path: str = "test.py", + *, + repo_root: Path = REPO_ROOT, + imported_sources: frozenset[Path] | None = None, +) -> tuple[Finding, ...]: + tree = ast.parse(source, filename=path) + findings = [] + functions = _top_level_functions(tree) + cocotb_functions = tuple(function for function in functions if _is_cocotb_test(function)) + + if cocotb_functions: + first_entrypoint_line = min(function.lineno for function in cocotb_functions) + header = "\n".join(source.splitlines()[:first_entrypoint_line]) + if METHODOLOGY_MARKER not in header: + findings.append( + Finding( + "missing-methodology", + path, + 1, + "", + "cocotb test file has no Test methodology block before its first entrypoint", + ) + ) + + for function in cocotb_functions: + for node in _walk_function(function): + if isinstance(node, ast.Return) and node.value is None: + rule = "early-bare-return" if node.lineno - function.lineno <= 12 else "bare-return" + findings.append( + Finding( + rule, + path, + node.lineno, + function.name, + "bare return in cocotb entrypoint; confirm that the scenario cannot pass without its named checks", + ) + ) + + for function in _all_functions(tree): + parents = _parent_map(function) + for node in _walk_function(function): + if isinstance(node, ast.While) and isinstance(node.test, ast.Constant): + if node.test.value is True: + findings.append( + Finding( + "open-ended-loop", + path, + node.lineno, + function.name, + "while True requires a bounded enclosing timeout or lifetime-agent classification", + ) + ) + + if not isinstance(node, ast.Call) or _qualified_name(node.func) != "cocotb.start_soon": + continue + if _is_clock_start(node): + continue + if isinstance(parents.get(node), ast.Expr): + findings.append( + Finding( + "unretained-start-soon", + path, + node.lineno, + function.name, + "classify as finite work or a lifetime agent and make ownership explicit", + ) + ) + + for statements in _statement_lists(function): + for first, second in zip(statements, statements[1:]): + if _await_call_name(first).endswith("RisingEdge") and _await_call_name(second).endswith("Timer"): + findings.append( + Finding( + "edge-then-timer", + path, + second.lineno, + function.name, + "classify the delay as delta-cycle settling or a real modeled propagation delay", + ) + ) + + relative_path = path.replace("\\", "/") + direct_aliases = _direct_runner_aliases(tree) + if direct_aliases and relative_path not in DOCUMENTED_DIRECT_RUNNERS: + for node in ast.walk(tree): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + if node.func.id in direct_aliases: + findings.append( + Finding( + "direct-runner", + path, + node.lineno, + "", + "use run_surf_vhdl_test() or document the capability the shared runner cannot express", + ) + ) + + if imported_sources is None: + imported_sources = _imported_source_paths(repo_root) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + for keyword in node.keywords: + if keyword.arg != "extra_vhdl_sources": + continue + for literal, line in _string_constants(keyword.value): + if not literal.lower().endswith((".vhd", ".vhdl")): + continue + source_path = Path(literal) + if not source_path.is_absolute(): + source_path = repo_root / source_path + if source_path.exists() and source_path.resolve() in imported_sources: + findings.append( + Finding( + "duplicate-imported-source", + path, + line, + "", + f"{literal} is already supplied by build/SRC_VHDL", + ) + ) + + return tuple(sorted(set(findings))) + + +def audit_paths( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> tuple[Finding, ...]: + imported_sources = _imported_source_paths(repo_root) + findings = [] + for source_path in discover_python_files(paths, repo_root): + relative_path = _repo_relative(source_path, repo_root) + try: + findings.extend( + audit_source( + source_path.read_text(encoding="utf-8"), + relative_path, + repo_root=repo_root, + imported_sources=imported_sources, + ) + ) + except SyntaxError as exc: + findings.append( + Finding( + "syntax-error", + relative_path, + exc.lineno or 1, + "", + exc.msg, + ) + ) + return tuple(sorted(findings)) + + +def preservation_report( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> dict[str, object]: + inventories = inventory_paths(paths, repo_root) + return { + "schema_version": SCHEMA_VERSION, + "files": { + path: inventory.to_dict() + for path, inventory in inventories.items() + }, + } + + +def compare_preservation_reports( + before: dict[str, object], + after: dict[str, object], +) -> PreservationDelta: + if before.get("schema_version") != SCHEMA_VERSION: + raise ValueError("unsupported before-report schema") + if after.get("schema_version") != SCHEMA_VERSION: + raise ValueError("unsupported after-report schema") + + before_files = before.get("files") + after_files = after.get("files") + if not isinstance(before_files, dict) or not isinstance(after_files, dict): + raise ValueError("preservation reports must contain a files object") + + removed: dict[str, dict[str, tuple[str, ...]]] = {} + added: dict[str, dict[str, tuple[str, ...]]] = {} + all_paths = sorted(set(before_files) | set(after_files)) + for path in all_paths: + before_inventory = before_files.get(path, {}) + after_inventory = after_files.get(path, {}) + if not isinstance(before_inventory, dict) or not isinstance(after_inventory, dict): + raise ValueError(f"invalid inventory for {path}") + + categories = sorted(set(before_inventory) | set(after_inventory)) + for category in categories: + before_values = set(before_inventory.get(category, [])) + after_values = set(after_inventory.get(category, [])) + removed_values = tuple(sorted(before_values - after_values)) + added_values = tuple(sorted(after_values - before_values)) + if removed_values: + removed.setdefault(path, {})[category] = removed_values + if added_values: + added.setdefault(path, {})[category] = added_values + + return PreservationDelta(removed=removed, added=added) + + +def compliance_baseline(findings: Iterable[Finding]) -> dict[str, object]: + counts: dict[str, Counter[str]] = { + rule: Counter() + for rule in sorted(ENFORCED_RULES) + } + for finding in findings: + if finding.rule in ENFORCED_RULES: + counts[finding.rule][finding.path] += 1 + + return { + "schema_version": SCHEMA_VERSION, + "rules": { + rule: dict(sorted(paths.items())) + for rule, paths in counts.items() + }, + } + + +def compare_compliance_baseline( + baseline: dict[str, object], + findings: Iterable[Finding], +) -> BaselineDelta: + if baseline.get("schema_version") != SCHEMA_VERSION: + raise ValueError("unsupported compliance-baseline schema") + baseline_rules = baseline.get("rules") + if not isinstance(baseline_rules, dict): + raise ValueError("compliance baseline must contain a rules object") + + current = compliance_baseline(findings)["rules"] + new: dict[str, dict[str, int]] = {} + reduced: dict[str, dict[str, int]] = {} + for rule in sorted(ENFORCED_RULES): + allowed_paths = baseline_rules.get(rule, {}) + current_paths = current.get(rule, {}) + if not isinstance(allowed_paths, dict) or not isinstance(current_paths, dict): + raise ValueError(f"invalid baseline counts for {rule}") + for path in sorted(set(allowed_paths) | set(current_paths)): + allowed_count = allowed_paths.get(path, 0) + current_count = current_paths.get(path, 0) + if not isinstance(allowed_count, int) or not isinstance(current_count, int): + raise ValueError(f"invalid baseline count for {rule}:{path}") + if current_count > allowed_count: + new.setdefault(rule, {})[path] = current_count - allowed_count + elif current_count < allowed_count: + reduced.setdefault(rule, {})[path] = allowed_count - current_count + + return BaselineDelta(new=new, reduced=reduced) + + +def _write_output(content: str, output: str | None) -> None: + if output is None: + print(content) + else: + Path(output).write_text(f"{content}\n", encoding="utf-8") + + +def _audit_text(findings: tuple[Finding, ...]) -> str: + counts = Counter(finding.rule for finding in findings) + lines = [ + f"{finding.path}:{finding.line}: {finding.rule}: {finding.symbol}: {finding.detail}" + for finding in findings + ] + lines.append("") + lines.append(f"{len(findings)} finding(s) across {len(counts)} rule(s)") + for rule, count in sorted(counts.items()): + lines.append(f" {rule}: {count}") + return "\n".join(lines) + + +def _delta_text(delta: PreservationDelta) -> str: + lines = [] + for label, values in (("removed", delta.removed), ("added", delta.added)): + for path, categories in values.items(): + for category, items in categories.items(): + for item in items: + lines.append(f"{label}: {path}: {category}: {item}") + if not lines: + return "No preservation-report differences." + return "\n".join(lines) + + +def _baseline_delta_text(delta: BaselineDelta) -> str: + lines = [] + for label, values in (("new", delta.new), ("reduced", delta.reduced)): + for rule, paths in values.items(): + for path, count in paths.items(): + lines.append(f"{label}: {rule}: {path}: {count}") + if not lines: + return "No enforced compliance-baseline differences." + return "\n".join(lines) + + +def _require_imported_source_tree(repo_root: Path) -> None: + if not (repo_root / "build" / "SRC_VHDL").exists(): + raise FileNotFoundError( + "missing build/SRC_VHDL; run `make MODULES=\"$PWD\" import` before baseline checks" + ) + + +def main(argv: list[str] | None = None, repo_root: Path = REPO_ROOT) -> int: + parser = argparse.ArgumentParser( + description="Audit SURF regression structure and compare test-preservation reports." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + audit_parser = subparsers.add_parser("audit", help="report compliance screening signals") + audit_parser.add_argument("paths", nargs="*", default=["tests"]) + audit_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") + audit_parser.add_argument("--output", help="write the report to this path") + + inventory_parser = subparsers.add_parser( + "inventory", + help="record pytest/cocotb names, parameters, gates, skips, and timeouts", + ) + inventory_parser.add_argument("paths", nargs="*", default=["tests"]) + inventory_parser.add_argument("--output", help="write the JSON report to this path") + + baseline_parser = subparsers.add_parser( + "baseline", + help="record current counts for reliable, enforceable audit rules", + ) + baseline_parser.add_argument("paths", nargs="*", default=["tests"]) + baseline_parser.add_argument("--output", required=True, help="write the JSON baseline here") + + check_parser = subparsers.add_parser( + "check", + help="fail when enforced findings exceed the checked-in legacy baseline", + ) + check_parser.add_argument("paths", nargs="*", default=["tests"]) + check_parser.add_argument( + "--baseline", + default=str(DEFAULT_BASELINE), + help="legacy baseline JSON path", + ) + check_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") + check_parser.add_argument("--output", help="write the comparison to this path") + + compare_parser = subparsers.add_parser( + "compare", + help="compare two preservation reports and fail when coverage identifiers disappear", + ) + compare_parser.add_argument("before") + compare_parser.add_argument("after") + compare_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") + compare_parser.add_argument("--output", help="write the comparison to this path") + + args = parser.parse_args(argv) + try: + if args.command == "audit": + findings = audit_paths(args.paths, repo_root) + if args.json: + content = json.dumps( + { + "schema_version": SCHEMA_VERSION, + "findings": [finding.to_dict() for finding in findings], + }, + indent=2, + sort_keys=True, + ) + else: + content = _audit_text(findings) + _write_output(content, args.output) + return 0 + + if args.command == "inventory": + content = json.dumps( + preservation_report(args.paths, repo_root), + indent=2, + sort_keys=True, + ) + _write_output(content, args.output) + return 0 + + if args.command == "baseline": + _require_imported_source_tree(repo_root) + content = json.dumps( + compliance_baseline(audit_paths(args.paths, repo_root)), + indent=2, + sort_keys=True, + ) + _write_output(content, args.output) + return 0 + + if args.command == "check": + _require_imported_source_tree(repo_root) + baseline = json.loads(Path(args.baseline).read_text(encoding="utf-8")) + delta = compare_compliance_baseline( + baseline, + audit_paths(args.paths, repo_root), + ) + content = ( + json.dumps(delta.to_dict(), indent=2, sort_keys=True) + if args.json + else _baseline_delta_text(delta) + ) + _write_output(content, args.output) + return 1 if delta.has_new else 0 + + before = json.loads(Path(args.before).read_text(encoding="utf-8")) + after = json.loads(Path(args.after).read_text(encoding="utf-8")) + delta = compare_preservation_reports(before, after) + content = ( + json.dumps(delta.to_dict(), indent=2, sort_keys=True) + if args.json + else _delta_text(delta) + ) + _write_output(content, args.output) + return 1 if delta.has_removals else 0 + except (FileNotFoundError, SyntaxError, ValueError, json.JSONDecodeError) as exc: + parser.error(str(exc)) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/common/compliance_baseline.json b/tests/common/compliance_baseline.json new file mode 100644 index 0000000000..dd2cb74433 --- /dev/null +++ b/tests/common/compliance_baseline.json @@ -0,0 +1,86 @@ +{ + "rules": { + "direct-runner": { + "tests/axi/dma/test_AxiStreamDmaV2Read.py": 1 + }, + "duplicate-imported-source": { + "tests/axi/axi4/test_AxiMemTester.py": 1, + "tests/axi/axi4/test_AxiMonAxiL.py": 1, + "tests/axi/axi4/test_AxiRateGen.py": 1, + "tests/axi/axi4/test_AxiRingBuffer.py": 1, + "tests/axi/axi_lite/test_AxiLiteAsync.py": 1, + "tests/axi/axi_lite/test_AxiLiteCrossbar.py": 1, + "tests/axi/axi_lite/test_AxiLiteFifoPop.py": 1, + "tests/axi/axi_lite/test_AxiLiteFifoPush.py": 1, + "tests/axi/axi_lite/test_AxiLiteFifoPushPop.py": 1, + "tests/axi/axi_lite/test_AxiLiteMaster.py": 1, + "tests/axi/axi_lite/test_AxiLiteMasterProxy.py": 2, + "tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py": 1, + "tests/axi/axi_lite/test_AxiLiteRegs.py": 1, + "tests/axi/axi_lite/test_AxiLiteRespTimer.py": 1, + "tests/axi/axi_lite/test_AxiLiteRingBuffer.py": 1, + "tests/axi/axi_lite/test_AxiLiteSequencerRam.py": 2, + "tests/axi/axi_lite/test_AxiLiteSlave.py": 1, + "tests/axi/axi_lite/test_AxiLiteWriteFilter.py": 1, + "tests/axi/axi_stream/test_AxiStreamBatchingFifo.py": 4, + "tests/axi/axi_stream/test_AxiStreamCombiner.py": 1, + "tests/axi/axi_stream/test_AxiStreamCompact.py": 1, + "tests/axi/axi_stream/test_AxiStreamConcat.py": 1, + "tests/axi/axi_stream/test_AxiStreamDeMux.py": 1, + "tests/axi/axi_stream/test_AxiStreamFlush.py": 1, + "tests/axi/axi_stream/test_AxiStreamFrameBuffer.py": 3, + "tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py": 1, + "tests/axi/axi_stream/test_AxiStreamGearbox.py": 1, + "tests/axi/axi_stream/test_AxiStreamGearboxPack.py": 1, + "tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py": 1, + "tests/axi/axi_stream/test_AxiStreamMon.py": 2, + "tests/axi/axi_stream/test_AxiStreamMonAxiL.py": 3, + "tests/axi/axi_stream/test_AxiStreamMux.py": 1, + "tests/axi/axi_stream/test_AxiStreamPipeline.py": 1, + "tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py": 1, + "tests/axi/axi_stream/test_AxiStreamRepeater.py": 1, + "tests/axi/axi_stream/test_AxiStreamResize.py": 1, + "tests/axi/axi_stream/test_AxiStreamRingBuffer.py": 3, + "tests/axi/axi_stream/test_AxiStreamScatterGather.py": 4, + "tests/axi/axi_stream/test_AxiStreamShift.py": 1, + "tests/axi/axi_stream/test_AxiStreamSplitter.py": 1, + "tests/axi/axi_stream/test_AxiStreamTap.py": 1, + "tests/axi/axi_stream/test_AxiStreamTimer.py": 1, + "tests/axi/axi_stream/test_AxiStreamTrailerAppend.py": 1, + "tests/axi/axi_stream/test_AxiStreamTrailerRemove.py": 1, + "tests/axi/dma/test_AxiStreamDma.py": 1, + "tests/axi/dma/test_AxiStreamDmaFifo.py": 1, + "tests/axi/dma/test_AxiStreamDmaRingRead.py": 3, + "tests/axi/dma/test_AxiStreamDmaRingWrite.py": 3, + "tests/axi/dma/test_AxiStreamDmaV2.py": 1, + "tests/axi/dma/test_AxiStreamDmaV2Desc.py": 1, + "tests/axi/dma/test_AxiStreamDmaV2Fifo.py": 3, + "tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py": 3, + "tests/axi/dma/test_AxiStreamDmaWrite.py": 1, + "tests/dsp/generic/test_BoxcarIntegrator.py": 1, + "tests/dsp/generic/test_FirFilterMultiChannel.py": 6, + "tests/dsp/generic/test_FirFilterSingleChannel.py": 3, + "tests/dsp/generic/test_FirFilterSingleChannelLowPass.py": 3, + "tests/dsp/generic/test_FirFilterSingleChannelTiming.py": 3, + "tests/dsp/generic/test_FirFilterTap.py": 1, + "tests/protocols/batcher/test_AxiStreamBatcherAxil.py": 1, + "tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py": 1, + "tests/protocols/rssi/test_RssiAxiLiteRegItf.py": 2, + "tests/protocols/rssi/test_RssiConnFsm.py": 2, + "tests/protocols/rssi/test_RssiCore.py": 6, + "tests/protocols/rssi/test_RssiCoreWrapper.py": 2, + "tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py": 3, + "tests/protocols/rssi/test_RssiHeaderReg.py": 1, + "tests/protocols/rssi/test_RssiMonitor.py": 2, + "tests/protocols/rssi/test_RssiRxFsm.py": 4, + "tests/protocols/rssi/test_RssiTxFsm.py": 2 + }, + "missing-methodology": { + "tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py": 1, + "tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py": 1, + "tests/simlink/rogue/test_RogueSideBandRogue.py": 1, + "tests/simlink/rogue/test_RogueStreamRogue.py": 1 + } + }, + "schema_version": 1 +} diff --git a/tests/common/test_compliance_audit.py b/tests/common/test_compliance_audit.py new file mode 100644 index 0000000000..987cf406dc --- /dev/null +++ b/tests/common/test_compliance_audit.py @@ -0,0 +1,367 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +from __future__ import annotations + +import json + +from tests.common import compliance_audit + + +LICENSE_AND_METHODOLOGY = """\ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +############################################################################## + +# Test methodology: +# - Sweep: Example. +# - Stimulus: Example. +# - Checks: Example. +# - Timing: Example. +""" + + +def _rules(findings): + return [finding.rule for finding in findings] + + +def test_inventory_records_public_test_identifiers_and_controls(): + source = LICENSE_AND_METHODOLOGY + """ +import cocotb +import pytest + +RUN = "RUN_SAMPLE_KNOWN_ISSUE_TESTS" +SELECTOR = "COCOTB_TESTCASE" + +@cocotb.test(timeout_time=2, timeout_unit="us") +async def transfer_test(dut): + pass + +@pytest.mark.skipif(True, reason="example") +@pytest.mark.parametrize("parameters", [pytest.param({}, id="default")]) +def test_wrapper(parameters): + pass +""" + + inventory = compliance_audit.inventory_source(source) + + assert inventory.cocotb_entrypoints == ("transfer_test",) + assert inventory.pytest_functions == ("test_wrapper",) + assert inventory.parameter_ids == ("default",) + assert inventory.environment_controls == ( + "COCOTB_TESTCASE", + "RUN_SAMPLE_KNOWN_ISSUE_TESTS", + ) + assert inventory.skipped_functions == ("test_wrapper",) + assert inventory.timeout_entrypoints == ("transfer_test",) + + +def test_audit_reports_high_and_low_confidence_screening_signals(): + source = """ +import cocotb +from cocotb.triggers import RisingEdge, Timer + +@cocotb.test() +async def routed_case(dut): + if int(dut.mode.value) == 0: + return + cocotb.start_soon(run_monitor()) + while True: + await RisingEdge(dut.clk) + await Timer(1, unit="ns") +""" + + findings = compliance_audit.audit_source(source) + + assert set(_rules(findings)) == { + "early-bare-return", + "missing-methodology", + "unretained-start-soon", + "open-ended-loop", + "edge-then-timer", + } + assert len(findings) == 5 + assert {finding.symbol for finding in findings} == {"", "routed_case"} + + +def test_audit_ignores_retained_tasks_and_clock_tasks(): + source = LICENSE_AND_METHODOLOGY + """ +import cocotb +from cocotb.clock import Clock + +@cocotb.test() +async def task_case(dut): + monitor_task = cocotb.start_soon(run_monitor()) + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await monitor_task +""" + + findings = compliance_audit.audit_source(source) + + assert "unretained-start-soon" not in _rules(findings) + + +def test_audit_reports_direct_runner_except_documented_simlink_helper(): + source = """ +from cocotb_test.simulator import run + +def test_wrapper(): + run(toplevel="surf.target") +""" + + ordinary = compliance_audit.audit_source( + source, + "tests/axi/test_target.py", + ) + simlink = compliance_audit.audit_source( + source, + "tests/simlink/ghdl/simlink_test_utils.py", + ) + + assert _rules(ordinary) == ["direct-runner"] + assert simlink == () + + +def test_audit_detects_literal_source_already_in_import(tmp_path): + source_path = tmp_path / "base" / "rtl" / "Target.vhd" + source_path.parent.mkdir(parents=True) + source_path.write_text("entity Target is end entity;\n", encoding="utf-8") + + source = """ +def test_wrapper(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.target", + extra_vhdl_sources={"surf": ["base/rtl/Target.vhd"]}, + ) +""" + + findings = compliance_audit.audit_source( + source, + "tests/base/test_Target.py", + repo_root=tmp_path, + imported_sources=frozenset({source_path.resolve()}), + ) + + assert _rules(findings) == ["duplicate-imported-source"] + assert "base/rtl/Target.vhd" in findings[0].detail + + +def test_discovery_excludes_legacy_build_and_cache_directories(tmp_path): + included = tmp_path / "tests" / "base" / "test_included.py" + included.parent.mkdir(parents=True) + included.write_text("def test_included(): pass\n", encoding="utf-8") + + for directory in ("legacy", "sim_build", "__pycache__"): + excluded = tmp_path / "tests" / directory / "test_excluded.py" + excluded.parent.mkdir(parents=True) + excluded.write_text("def test_excluded(): pass\n", encoding="utf-8") + + discovered = compliance_audit.discover_python_files(("tests",), tmp_path) + + assert discovered == (included.resolve(),) + + +def test_preservation_comparison_reports_removed_and_added_identifiers(): + before = { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": { + "tests/test_target.py": { + "cocotb_entrypoints": ["old_case"], + "pytest_functions": ["test_target"], + }, + }, + } + after = { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": { + "tests/test_target.py": { + "cocotb_entrypoints": ["new_case"], + "pytest_functions": ["test_target"], + }, + }, + } + + delta = compliance_audit.compare_preservation_reports(before, after) + + assert delta.removed == { + "tests/test_target.py": {"cocotb_entrypoints": ("old_case",)}, + } + assert delta.added == { + "tests/test_target.py": {"cocotb_entrypoints": ("new_case",)}, + } + assert delta.has_removals is True + + +def test_compliance_baseline_allows_cleanup_but_rejects_new_findings(): + baseline = { + "schema_version": compliance_audit.SCHEMA_VERSION, + "rules": { + "direct-runner": {"tests/old.py": 1}, + "duplicate-imported-source": {"tests/sources.py": 2}, + "missing-methodology": {}, + }, + } + findings = ( + compliance_audit.Finding( + "duplicate-imported-source", + "tests/sources.py", + 10, + "", + "existing", + ), + compliance_audit.Finding( + "missing-methodology", + "tests/new.py", + 1, + "", + "new", + ), + compliance_audit.Finding( + "early-bare-return", + "tests/ambiguous.py", + 20, + "scenario", + "not enforced yet", + ), + ) + + delta = compliance_audit.compare_compliance_baseline(baseline, findings) + + assert delta.new == {"missing-methodology": {"tests/new.py": 1}} + assert delta.reduced == { + "direct-runner": {"tests/old.py": 1}, + "duplicate-imported-source": {"tests/sources.py": 1}, + } + assert delta.has_new is True + + +def test_compliance_baseline_counts_findings_by_rule_and_path(): + findings = ( + compliance_audit.Finding( + "duplicate-imported-source", + "tests/source.py", + 10, + "", + "one", + ), + compliance_audit.Finding( + "duplicate-imported-source", + "tests/source.py", + 11, + "", + "two", + ), + compliance_audit.Finding( + "edge-then-timer", + "tests/timing.py", + 12, + "scenario", + "reported only", + ), + ) + + baseline = compliance_audit.compliance_baseline(findings) + + assert baseline == { + "schema_version": compliance_audit.SCHEMA_VERSION, + "rules": { + "direct-runner": {}, + "duplicate-imported-source": {"tests/source.py": 2}, + "missing-methodology": {}, + }, + } + + +def test_repository_does_not_exceed_compliance_baseline(): + baseline = json.loads( + compliance_audit.DEFAULT_BASELINE.read_text(encoding="utf-8") + ) + findings = compliance_audit.audit_paths(("tests",)) + + delta = compliance_audit.compare_compliance_baseline(baseline, findings) + + assert not delta.has_new, compliance_audit._baseline_delta_text(delta) + + +def test_inventory_cli_writes_reproducible_json(tmp_path): + test_file = tmp_path / "tests" / "test_target.py" + test_file.parent.mkdir() + test_file.write_text( + LICENSE_AND_METHODOLOGY + + """ +import cocotb + +@cocotb.test() +async def target_case(dut): + pass + +def test_target(): + pass +""", + encoding="utf-8", + ) + output = tmp_path / "inventory.json" + + result = compliance_audit.main( + ["inventory", "tests", "--output", str(output)], + repo_root=tmp_path, + ) + + assert result == 0 + report = json.loads(output.read_text(encoding="utf-8")) + assert report == { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": { + "tests/test_target.py": { + "cocotb_entrypoints": ["target_case"], + "environment_controls": [], + "parameter_ids": [], + "pytest_functions": ["test_target"], + "skipped_functions": [], + "timeout_entrypoints": [], + }, + }, + } + + +def test_compare_cli_fails_when_identifiers_are_removed(tmp_path): + before = tmp_path / "before.json" + after = tmp_path / "after.json" + output = tmp_path / "delta.json" + before.write_text( + json.dumps( + { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": {"tests/test.py": {"pytest_functions": ["test_one"]}}, + } + ), + encoding="utf-8", + ) + after.write_text( + json.dumps( + { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": {"tests/test.py": {"pytest_functions": []}}, + } + ), + encoding="utf-8", + ) + + result = compliance_audit.main( + ["compare", str(before), str(after), "--json", "--output", str(output)], + repo_root=tmp_path, + ) + + assert result == 1 + delta = json.loads(output.read_text(encoding="utf-8")) + assert delta["removed"] == { + "tests/test.py": {"pytest_functions": ["test_one"]}, + } From c16ed980734b430e57f71e4098a7fcc3e819ded3 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 10:11:44 -0700 Subject: [PATCH 37/57] test: make cocotb scenario applicability explicit --- .../regression-test-compliance/README.md | 65 ++++++++++++++-- tests/axi/axi_lite/test_AxiDualPortRam.py | 5 +- .../test_AxiStreamFifoV2IpIntegrator.py | 25 ++----- .../axi/axi_stream/test_AxiStreamPipeline.py | 8 +- tests/base/delay/test_SlvDelay.py | 8 +- tests/base/delay/test_SlvDelayFifo.py | 5 +- tests/base/delay/test_SlvDelayRam.py | 5 +- tests/base/fifo/test_Fifo.py | 5 +- tests/base/fifo/test_FifoAsync.py | 20 +---- tests/base/fifo/test_FifoCascade.py | 5 +- tests/base/fifo/test_FifoMux.py | 10 ++- tests/base/fifo/test_FifoOutputPipeline.py | 20 +---- tests/base/fifo/test_FifoRdFsm.py | 18 +---- tests/base/fifo/test_FifoSync.py | 24 ++---- tests/base/general/test_Arbiter.py | 5 +- tests/base/ram/test_DualPortRam.py | 10 +-- tests/base/ram/test_LutRam.py | 15 +--- tests/base/ram/test_SimpleDualPortRam.py | 25 +++---- tests/base/ram/test_TrueDualPortRam.py | 34 ++++----- tests/base/sync/test_Synchronizer.py | 17 +---- tests/base/sync/test_SynchronizerFifo.py | 15 +--- tests/base/sync/test_SynchronizerVector.py | 16 +--- tests/common/README.md | 10 ++- tests/common/compliance_audit.py | 18 ++++- tests/common/compliance_baseline.json | 1 + tests/common/regression_utils.py | 34 +++++++++ tests/common/test_compliance_audit.py | 47 ++++++++++++ tests/common/test_regression_utils.py | 75 +++++++++++++++++++ tests/protocols/batcher/README.md | 6 ++ .../test_AxiStreamBatcherEventBuilder.py | 13 +++- tests/protocols/coaxpress/test_CoaXPressRx.py | 69 +++++++++-------- .../coaxpress/test_CoaXPressRxHsFsm.py | 33 +++----- .../coaxpress/test_CoaXPressRxWordPacker.py | 26 ++----- .../jesd204b/test_Jesd204bLoopback.py | 16 +--- .../jesd204b/test_JesdAlignFrRepCh.py | 14 +--- .../protocols/jesd204b/test_JesdSyncFsmTx.py | 12 +-- tests/protocols/jesd204b/test_JesdTxLane.py | 14 +--- tests/protocols/pgp/pgp4/test_Pgp4RxEb.py | 33 ++++---- tests/protocols/rssi/README.md | 9 ++- tests/protocols/rssi/test_RssiConnFsm.py | 39 ++-------- tests/protocols/rssi/test_RssiCore.py | 61 ++++++++------- tests/protocols/rssi/test_RssiCoreWrapper.py | 21 ++++-- .../rssi/test_RssiCoreWrapperMultiStream.py | 21 +----- tests/protocols/rssi/test_RssiRxFsm.py | 20 +++-- tests/protocols/srp/test_SrpV0AxiLite.py | 5 +- 45 files changed, 482 insertions(+), 475 deletions(-) create mode 100644 tests/common/test_regression_utils.py diff --git a/docs/plans/regression-test-compliance/README.md b/docs/plans/regression-test-compliance/README.md index 71eb279eee..76d08f86ba 100644 --- a/docs/plans/regression-test-compliance/README.md +++ b/docs/plans/regression-test-compliance/README.md @@ -18,6 +18,57 @@ This effort improves the structure and reliability of existing tests. It does not reinstate the historical module-coverage queue or require new functional tests for unrelated RTL. +Phase 1 has completed its batcher pilot. The event-builder transition-frame +scenario is now excluded explicitly from the INDEXED configuration with +`COCOTB_TEST_FILTER` instead of returning a no-op pass. Preservation comparison +kept all 13 cocotb names, the pytest wrapper, and all three parameter IDs; the +INDEXED result reports one explicit skip while both ROUTED configurations run +all 13 scenarios. + +The RSSI pilot removed 19 additional no-op entrypoint returns across the +connection FSM, core, wrapper, multi-stream wrapper, and receive FSM suites. +Pytest now selects client/server role groups, excludes focused checksum/BUSY/ +loss/AXI-Lite/sequence cases from unrelated simulations, and retains the +existing known-issue and extended gates. The preservation comparison reports no +removed identifiers and four newly explicit `COCOTB_TEST_FILTER` controls. +Client/server FSM simulation passes in serial and xdist modes. Enabling the +known-issue RxFSM nodes still exposes their pre-existing behavioral failures; +their XUnit results confirm that the normal node skips only the disabled- +checksum case and the focused node skips the other 13 scenarios. + +The base-library migration converted 45 configuration and opt-in guards across +18 delay, FIFO, arbiter, RAM, and synchronizer modules into decorator-level +cocotb skips. The only remaining base early return is the intentional +common-clock terminal path in `SynchronizerFifo`: it first exercises and +asserts every passthrough value, then avoids the inapplicable dual-clock FIFO +path. All 171 base pytest nodes and all cocotb entrypoint names are preserved. +A representative 33-node serial run and the complete 94-node edited subset +under pytest-xdist both pass; the generated cocotb XUnit files report +configuration-inapplicable entrypoints as skips rather than passes. + +The AXI migration removed the remaining seven early no-op returns from +`AxiDualPortRam`, `AxiStreamPipeline`, and the AXI Stream FIFO integration +matrix. System-write, registered-pipeline reset, metadata truncation, +frame-ready, threshold, burst, and dynamic-pause scenarios now declare their +applicability in the cocotb decorator. The later return in +`AxiStreamResize.backpressure_and_reset_test` is retained because it follows a +completed equal-width handshake/clear check and intentionally terminates before +the inapplicable stateful-reset path. Preservation comparison reports no +removed nodes or entrypoints; all 17 edited parameter cases pass both serially +and under pytest-xdist. + +Phase 1 is complete. The protocol pass converted 36 remaining lane-, mode-, +known-issue-, overflow-, address-width-, subclass-, and scrambling-specific +no-op branches in CoaXPress, PGP4, SRPv0, and JESD204B into explicit cocotb +applicability skips. The audit now defines an early return by semantics—a bare +return before any awaited simulator activity or assertion—rather than by an +arbitrary line-distance threshold, and that rule is blocking with a zero-entry +baseline. Seven post-activity returns remain report-only and were reviewed as +intentional successful terminal paths. Protocol preservation comparison found +no removed pytest nodes or cocotb entrypoints. The 14 CoaXPress/PGP4/SRP +parameter nodes pass both serially and under pytest-xdist; the JESD-specific +24-node matrix also passes in both modes. + ## Non-Negotiable Preservation Rules - Do not remove an existing test, parameter case, opt-in gate, or behavioral @@ -278,10 +329,10 @@ criteria. ## Immediate Next Steps -1. Use batcher as the first no-op-pass migration pilot and remove its reduced - baseline entries in the same change. -2. Use RSSI as the first gated integration and coroutine-lifecycle pilot. -3. Review the first two migrations before extending the blocking ratchet to an - additional structural rule. -4. Begin duplicate-source cleanup by subsystem after the no-op-pass selection - pattern is established. +1. Begin duplicate-source cleanup by subsystem, starting with the shared AXI + wrapper paths and a fresh ruckus import. +2. Migrate `test_AxiStreamDmaV2Read.py` to the shared runner and add duplicate + source diagnostics after caller cleanup establishes the legitimate + exception set. +3. Classify `force_compile=True` callers while touching their source lists, + retaining only cases with a documented cache or source-topology need. diff --git a/tests/axi/axi_lite/test_AxiDualPortRam.py b/tests/axi/axi_lite/test_AxiDualPortRam.py index f9f4d4500a..d6ac35b8a1 100644 --- a/tests/axi/axi_lite/test_AxiDualPortRam.py +++ b/tests/axi/axi_lite/test_AxiDualPortRam.py @@ -138,14 +138,11 @@ async def axi_round_trip_and_sys_read_test(dut): assert sys_data == int.from_bytes(expected_data, "little") -@cocotb.test() +@cocotb.test(skip=not env_flag("SYS_WR_EN", default=False)) async def sys_write_visibility_test(dut): tb = TB(dut) await tb.reset() - if not tb.sys_wr_en: - return - await tb.sys_write(addr=6, data=0xAABBCCDD, we_mask=(1 << tb.byte_count) - 1) rd_txn = await tb.axil.read(6 << 2, 4) assert rd_txn.resp == AxiResp.OKAY diff --git a/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py b/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py index 626bd4b6ce..36edd81ad4 100644 --- a/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py +++ b/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py @@ -333,12 +333,9 @@ async def stream_round_trip_test(dut): assert int(tb.dut.sAxisOverflow.value) == 0 -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_METADATA_TRUNCATION", default=False)) async def metadata_truncation_test(dut): tb = TB(dut) - if not tb.test_metadata_truncation: - return - await tb.reset() tb.start_agents() @@ -356,12 +353,9 @@ async def metadata_truncation_test(dut): assert scalar_tuser(rx_frame.tuser) == (scalar_tuser(frame.tuser) & mask(tb.m_user_width)) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_FRAME_READY", default=False)) async def frame_ready_release_and_last_user_test(dut): tb = TB(dut) - if not tb.test_frame_ready: - return - await tb.reset() tb.clear_samples() tb.dut.M_AXIS_TREADY.value = 1 @@ -397,12 +391,9 @@ async def frame_ready_release_and_last_user_test(dut): assert received["last_sideband"] == [beat_users[-1]] * len(payload) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_THRESHOLD_PREFILL", default=False)) async def threshold_prefill_release_test(dut): tb = TB(dut) - if not tb.test_threshold_prefill: - return - await tb.reset() tb.dut.M_AXIS_TREADY.value = 0 @@ -437,12 +428,9 @@ async def threshold_prefill_release_test(dut): await tb.cycle_sink(2) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_BURST_BEHAVIOR", default=False)) async def burst_mode_release_test(dut): tb = TB(dut) - if not tb.test_burst_behavior: - return - await tb.reset() tb.clear_samples() tb.dut.M_AXIS_TREADY.value = 1 @@ -468,12 +456,9 @@ async def burst_mode_release_test(dut): assert any(later > earlier + 1 for earlier, later in zip(tb.rx_cycles, tb.rx_cycles[1:])) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_DYNAMIC_PAUSE", default=False)) async def dynamic_pause_threshold_test(dut): tb = TB(dut) - if not tb.test_dynamic_pause: - return - await tb.reset() tb.dut.fifoPauseThresh.value = 1 tb.dut.M_AXIS_TREADY.value = 0 diff --git a/tests/axi/axi_stream/test_AxiStreamPipeline.py b/tests/axi/axi_stream/test_AxiStreamPipeline.py index 996a8c5271..773184ef9a 100644 --- a/tests/axi/axi_stream/test_AxiStreamPipeline.py +++ b/tests/axi/axi_stream/test_AxiStreamPipeline.py @@ -273,17 +273,11 @@ async def latency_and_backpressure_test(dut): tb.drive_source_idle() -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) == 0) async def reset_behavior_test(dut): tb = TB(dut) await tb.reset() - # The zero-stage generate path is purely combinational, so reset should not - # be treated as a stateful flush path in this bench. Only the registered - # cases below are expected to clear buffered data on reset. - if tb.pipe_stages == 0: - return - dut.M_AXIS_TREADY.value = 0 tb.drive_source( valid=1, diff --git a/tests/base/delay/test_SlvDelay.py b/tests/base/delay/test_SlvDelay.py index 434c0eafff..0649e12dfc 100644 --- a/tests/base/delay/test_SlvDelay.py +++ b/tests/base/delay/test_SlvDelay.py @@ -127,17 +127,11 @@ async def reset(self) -> None: await self.cycle() -@cocotb.test() +@cocotb.test(skip=env_flag("REG_OUTPUT_G", default=False)) async def programmable_delay_test(dut): tb = TB(dut) await tb.reset() - # REG_OUTPUT_G adds an extra registered stage on top of the programmable - # delay line. The behavioral coverage for that option is handled by the - # hold and reset tests, while this test stays focused on the selectable tap. - if tb.reg_output: - return - # Change the selected tap while feeding distinct words so the test proves # the mux picks the requested historical sample, not just the newest word. await tb.cycle(din=0x11, delay=0) diff --git a/tests/base/delay/test_SlvDelayFifo.py b/tests/base/delay/test_SlvDelayFifo.py index 45be4259e2..35b2588a00 100644 --- a/tests/base/delay/test_SlvDelayFifo.py +++ b/tests/base/delay/test_SlvDelayFifo.py @@ -120,11 +120,8 @@ async def timestamped_outputs_preserve_programmed_order_test(dut): assert await tb.wait_for_output() == 0x22 -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_RESET_FLUSH", default=False)) async def reset_flushes_pending_entries_test(dut): - if not env_flag("CHECK_RESET_FLUSH", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/delay/test_SlvDelayRam.py b/tests/base/delay/test_SlvDelayRam.py index dc91b1d776..b8b4c73785 100644 --- a/tests/base/delay/test_SlvDelayRam.py +++ b/tests/base/delay/test_SlvDelayRam.py @@ -149,11 +149,8 @@ async def enable_hold_test(dut): assert int(dut.dout.value) == held -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_DYNAMIC_DELAY_CHANGE", default=False)) async def dynamic_delay_change_requires_reset_test(dut): - if not env_flag("CHECK_DYNAMIC_DELAY_CHANGE", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/fifo/test_Fifo.py b/tests/base/fifo/test_Fifo.py index b1a0f4cf40..838bfbabf3 100644 --- a/tests/base/fifo/test_Fifo.py +++ b/tests/base/fifo/test_Fifo.py @@ -141,12 +141,9 @@ async def wrapper_branch_ordering_test(dut): assert observed == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("GEN_SYNC_FIFO_G", default=False)) async def sync_count_alias_test(dut): tb = TB(dut) - if not tb.sync_fifo: - return - await tb.reset() # The sync wrapper aliases both public count ports to the same internal diff --git a/tests/base/fifo/test_FifoAsync.py b/tests/base/fifo/test_FifoAsync.py index 9b96ecd66d..374089e237 100644 --- a/tests/base/fifo/test_FifoAsync.py +++ b/tests/base/fifo/test_FifoAsync.py @@ -174,7 +174,7 @@ async def basic_ordering_test(dut): assert received == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_FULL_EMPTY", default=True)) async def full_empty_flag_test(dut): tb = TB( dut, @@ -183,9 +183,6 @@ async def full_empty_flag_test(dut): ) await tb.reset() - if not env_flag("CHECK_FULL_EMPTY", default=True): - return - # Standard mode exposes one slot less than the raw address space, while # FWFT can present one prefetched word beyond the underlying storage count. entry_capacity = (2 ** int(os.environ["ADDR_WIDTH_G"])) - (0 if tb.fwft_enabled else 1) @@ -200,11 +197,8 @@ async def full_empty_flag_test(dut): await with_timeout(tb._wait_empty(), 5, "us") -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_THRESHOLD_FLAGS", default=False)) async def threshold_flag_test(dut): - if not env_flag("CHECK_THRESHOLD_FLAGS", default=False): - return - tb = TB( dut, wr_clk_period_ns=float(os.environ["WR_CLK_PERIOD_NS"]), @@ -241,11 +235,8 @@ async def threshold_flag_test(dut): await with_timeout(tb._wait_prog_empty(1), 5, "us") -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_STRESS_BEHAVIOR", default=False)) async def burst_backpressure_and_reset_test(dut): - if not env_flag("CHECK_STRESS_BEHAVIOR", default=False): - return - tb = TB( dut, wr_clk_period_ns=float(os.environ["WR_CLK_PERIOD_NS"]), @@ -287,11 +278,8 @@ async def burst_backpressure_and_reset_test(dut): assert await tb.read_word() == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_NEAR_FULL_TURNOVER", default=False)) async def near_full_turnover_test(dut): - if not env_flag("CHECK_NEAR_FULL_TURNOVER", default=False): - return - tb = TB( dut, wr_clk_period_ns=float(os.environ["WR_CLK_PERIOD_NS"]), diff --git a/tests/base/fifo/test_FifoCascade.py b/tests/base/fifo/test_FifoCascade.py index 7cd9b9948c..3e2923a89f 100644 --- a/tests/base/fifo/test_FifoCascade.py +++ b/tests/base/fifo/test_FifoCascade.py @@ -166,11 +166,8 @@ async def stage_vector_mapping_test(dut): assert top_prog_full == int(dut.prog_full.value) -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_STAGE_PRESSURE", default=False)) async def staged_pressure_recovery_test(dut): - if not env_flag("CHECK_STAGE_PRESSURE", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/fifo/test_FifoMux.py b/tests/base/fifo/test_FifoMux.py index e5038c1f54..91dd95b5cf 100644 --- a/tests/base/fifo/test_FifoMux.py +++ b/tests/base/fifo/test_FifoMux.py @@ -179,14 +179,16 @@ async def width_conversion_test(dut): assert observed == expected -@cocotb.test() +@cocotb.test( + skip=( + int(os.environ.get("RD_DATA_WIDTH_G", "1")) + <= int(os.environ.get("WR_DATA_WIDTH_G", "1")) + ), +) async def write_packer_reset_test(dut): tb = TB(dut) await tb.reset() - if tb.rd_width <= tb.wr_width: - return - # When the wrapper is packing several narrow writes into one wider FIFO # word, a reset must discard the partial aggregate rather than letting stale # pre-reset fragments leak into the next output word. diff --git a/tests/base/fifo/test_FifoOutputPipeline.py b/tests/base/fifo/test_FifoOutputPipeline.py index 88c60a7b1f..da31f5c5e7 100644 --- a/tests/base/fifo/test_FifoOutputPipeline.py +++ b/tests/base/fifo/test_FifoOutputPipeline.py @@ -143,12 +143,9 @@ async def collect_words(self, expected_count: int, *, timeout_cycles: int = 80) raise AssertionError(f"Timed out collecting {expected_count} output words") -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) != 0) async def zero_latency_passthrough_test(dut): tb = TB(dut) - if tb.pipe_stages != 0: - return - # With zero stages, this block is combinational: data and handshaking should # pass straight through without waiting for a clock edge. dut.rst.value = tb.reset_inactive_value() @@ -162,12 +159,9 @@ async def zero_latency_passthrough_test(dut): assert int(dut.sRdEn.value) == 1 -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) == 0) async def ordering_test(dut): tb = TB(dut) - if tb.pipe_stages == 0: - return - await tb.reset() # Preload a short stream and then let the downstream consumer read @@ -179,12 +173,9 @@ async def ordering_test(dut): assert await tb.collect_words(len(expected)) == expected -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) < 2) async def backpressure_test(dut): tb = TB(dut) - if tb.pipe_stages < 2: - return - await tb.reset() # This test toggles downstream readiness to make sure the DUT can hold data @@ -231,12 +222,9 @@ async def backpressure_test(dut): assert received == expected -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) == 0) async def reset_behavior_test(dut): tb = TB(dut) - if tb.pipe_stages == 0: - return - await tb.reset() # Fill the pipeline with a few words and consume one so we know the DUT has diff --git a/tests/base/fifo/test_FifoRdFsm.py b/tests/base/fifo/test_FifoRdFsm.py index 2479fb742a..52ea7fbe7b 100644 --- a/tests/base/fifo/test_FifoRdFsm.py +++ b/tests/base/fifo/test_FifoRdFsm.py @@ -108,17 +108,11 @@ async def read_pulse(self) -> tuple[int, int]: return sampled_valid, sampled_underflow -@cocotb.test() +@cocotb.test(skip=env_flag("FWFT_EN_G", default=False)) async def count_and_flag_test(dut): tb = TB(dut) await tb.reset() - if tb.fwft_enabled: - # FWFT mode has a different visible contract because occupancy moves - # through an internal prefetch pipeline before the consumer sees data. - # The dedicated FWFT test below covers that behavior more directly. - return - await tb.initialize_writer(3) assert int(dut.empty.value) == 0 assert int(dut.almost_empty.value) == 0 @@ -131,12 +125,9 @@ async def count_and_flag_test(dut): assert int(dut.prog_empty.value) == (1 if 1 < tb.empty_threshold else 0) -@cocotb.test() +@cocotb.test(skip=env_flag("FWFT_EN_G", default=False)) async def standard_read_and_underflow_test(dut): tb = TB(dut) - if tb.fwft_enabled: - return - await tb.reset() await tb.initialize_writer(2) @@ -158,12 +149,9 @@ async def standard_read_and_underflow_test(dut): assert int(dut.empty.value) == 1 -@cocotb.test() +@cocotb.test(skip=not env_flag("FWFT_EN_G", default=False)) async def fwft_prefetch_test(dut): tb = TB(dut) - if not tb.fwft_enabled: - return - await tb.reset() await tb.initialize_writer(1) diff --git a/tests/base/fifo/test_FifoSync.py b/tests/base/fifo/test_FifoSync.py index aa3222471c..4bcf9408d9 100644 --- a/tests/base/fifo/test_FifoSync.py +++ b/tests/base/fifo/test_FifoSync.py @@ -163,14 +163,11 @@ async def basic_ordering_test(dut): assert received == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_FULL_EMPTY", default=True)) async def full_empty_flag_test(dut): tb = TB(dut, clk_period_ns=float(os.environ["CLK_PERIOD_NS"])) await tb.reset() - if not env_flag("CHECK_FULL_EMPTY", default=True): - return - # FifoSync uses the same user-visible capacity convention as FifoAsync: # standard mode exposes N-1 entries, FWFT mode exposes N entries. entry_capacity = (2 ** int(os.environ["ADDR_WIDTH_G"])) - (0 if tb.fwft_enabled else 1) @@ -185,11 +182,8 @@ async def full_empty_flag_test(dut): await with_timeout(tb._wait_empty(), 5, "us") -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_THRESHOLD_FLAGS", default=False)) async def threshold_flag_test(dut): - if not env_flag("CHECK_THRESHOLD_FLAGS", default=False): - return - tb = TB(dut, clk_period_ns=float(os.environ["CLK_PERIOD_NS"])) await tb.reset() @@ -225,17 +219,15 @@ async def threshold_flag_test(dut): await with_timeout(tb._wait_prog_empty(1), 5, "us") -@cocotb.test() +@cocotb.test( + skip=( + not env_flag("CHECK_SIMULTANEOUS_BOUNDARY", default=False) + or not env_flag("FWFT_EN_G", default=False) + ), +) async def simultaneous_boundary_test(dut): - if not env_flag("CHECK_SIMULTANEOUS_BOUNDARY", default=False): - return - tb = TB(dut, clk_period_ns=float(os.environ["CLK_PERIOD_NS"])) await tb.reset() - - if not tb.fwft_enabled: - return - capacity = 2 ** int(os.environ["ADDR_WIDTH_G"]) seed_values = [0x30 + index for index in range(capacity - 1)] for value in seed_values: diff --git a/tests/base/general/test_Arbiter.py b/tests/base/general/test_Arbiter.py index 700034fb72..5723def5e7 100644 --- a/tests/base/general/test_Arbiter.py +++ b/tests/base/general/test_Arbiter.py @@ -163,11 +163,8 @@ async def reset_behavior_test(dut): assert int(dut.valid.value) == 0 -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_STARVATION_ROTATION", default=False)) async def starvation_rotation_test(dut): - if not env_flag("CHECK_STARVATION_ROTATION", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/ram/test_DualPortRam.py b/tests/base/ram/test_DualPortRam.py index 26b1ceed43..cd172bb7b1 100644 --- a/tests/base/ram/test_DualPortRam.py +++ b/tests/base/ram/test_DualPortRam.py @@ -105,14 +105,11 @@ async def port_a_and_b_readback_test(dut): assert await tb.read_b(1) == 0x1234 -@cocotb.test() +@cocotb.test(skip=env_flag("DOA_REG_G", default=False)) async def port_a_mode_semantics_test(dut): tb = TB(dut) await tb.warmup() - if tb.doa_reg_enabled: - return - # Seed address 1 so `douta` is already showing a different value before # the write-under-test. That makes the three read-during-write modes # visibly distinguishable on port A. @@ -175,11 +172,8 @@ async def byte_write_and_reset_test(dut): assert await tb.read_b(4) == 0xCAFE -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_CROSS_PORT_COLLISION", default=False)) async def cross_port_collision_test(dut): - if not env_flag("CHECK_CROSS_PORT_COLLISION", default=False): - return - tb = TB(dut) await tb.warmup() diff --git a/tests/base/ram/test_LutRam.py b/tests/base/ram/test_LutRam.py index a2a8403b0d..5d72a41a5a 100644 --- a/tests/base/ram/test_LutRam.py +++ b/tests/base/ram/test_LutRam.py @@ -168,13 +168,10 @@ async def multiport_read_visibility_test(dut): assert await tb.read_c(1) == 0x1234 -@cocotb.test() +@cocotb.test(skip=not env_flag("REG_EN_G", default=True)) async def mode_semantics_test(dut): tb = TB(dut) await tb.warmup() - if not tb.reg_enabled: - return - await tb.write_a(0, 0x1111) await tb.write_a(1, 0xAAAA) assert await tb.read_a(1) == 0xAAAA @@ -198,13 +195,10 @@ async def mode_semantics_test(dut): assert await tb.read_b(0) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not env_flag("BYTE_WR_EN_G", default=False)) async def byte_write_enable_test(dut): tb = TB(dut) await tb.warmup() - if not tb.byte_write_enabled: - return - await tb.write_a(3, 0xABCD) await tb.write_a(3, 0x00EF, byte_mask=0b01) assert await tb.read_b(3) == 0xABEF @@ -213,13 +207,10 @@ async def byte_write_enable_test(dut): assert await tb.read_b(3) == 0x12EF -@cocotb.test() +@cocotb.test(skip=not env_flag("REG_EN_G", default=True)) async def reset_behavior_test(dut): tb = TB(dut) await tb.warmup() - if not tb.reg_enabled: - return - await tb.write_a(4, 0xCAFE) assert await tb.read_b(4) == 0xCAFE diff --git a/tests/base/ram/test_SimpleDualPortRam.py b/tests/base/ram/test_SimpleDualPortRam.py index 1075b71a70..b3a7ee9159 100644 --- a/tests/base/ram/test_SimpleDualPortRam.py +++ b/tests/base/ram/test_SimpleDualPortRam.py @@ -103,12 +103,9 @@ async def basic_read_write_test(dut): assert await tb.read_word(1) == 0x1234 -@cocotb.test() +@cocotb.test(skip=not env_flag("BYTE_WR_EN_G", default=False)) async def byte_write_enable_test(dut): tb = TB(dut) - if not tb.byte_write_enabled: - return - # First write a full word, then overwrite only one byte at a time to prove # the byte-mask wiring is being honored. await tb.write_word(2, 0xABCD) @@ -119,11 +116,8 @@ async def byte_write_enable_test(dut): assert await tb.read_word(2) == 0x12EF -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_READ_ENABLE_HOLD", default=False)) async def read_enable_hold_test(dut): - if not env_flag("CHECK_READ_ENABLE_HOLD", default=False): - return - tb = TB(dut) await tb.cycle_a(1) await tb.cycle_b(1) @@ -143,11 +137,8 @@ async def read_enable_hold_test(dut): assert await tb.read_word(1) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_REGISTERED_ENB_HOLD", default=False)) async def registered_read_enable_hold_test(dut): - if not env_flag("CHECK_REGISTERED_ENB_HOLD", default=False): - return - tb = TB(dut) await tb.cycle_a(1) await tb.cycle_b(1) @@ -169,12 +160,14 @@ async def registered_read_enable_hold_test(dut): assert await tb.read_word(1) == 0x3333 -@cocotb.test() +@cocotb.test( + skip=( + not env_flag("DOB_REG_G", default=False) + and int(os.environ.get("READ_LATENCY_G", "1")) != 2 + ), +) async def registered_output_hold_test(dut): tb = TB(dut) - if not tb.dob_reg_enabled: - return - # Seed two addresses so we can prove the output register can hold its old # value even after the read address changes. await tb.write_word(0, 0x1111) diff --git a/tests/base/ram/test_TrueDualPortRam.py b/tests/base/ram/test_TrueDualPortRam.py index b38c2c7a3d..8c22993125 100644 --- a/tests/base/ram/test_TrueDualPortRam.py +++ b/tests/base/ram/test_TrueDualPortRam.py @@ -39,6 +39,15 @@ ) +def output_register_enabled(port: str) -> bool: + latency = int(os.environ.get(f"READ_LATENCY_{port}_G", "-1")) + if latency < 0: + latency = int(os.environ.get("READ_LATENCY_G", "1")) + if env_flag(f"DO{port}_REG_G", default=False) and latency == 1: + latency = 2 + return latency == 2 + + class TB(DualClockRamTB): def __init__(self, dut): super().__init__(dut) @@ -135,13 +144,10 @@ async def cross_port_read_write_test(dut): assert await tb.read_b(2) == 0x5678 -@cocotb.test() +@cocotb.test(skip=output_register_enabled("A")) async def mode_semantics_test(dut): tb = TB(dut) await tb.warmup() - if tb.doa_reg_enabled: - return - # Seed two locations and intentionally leave `douta` showing the value from # address 1 before writing address 0. That setup makes the three RAM modes # produce visibly different output behavior on the write cycle. @@ -170,13 +176,10 @@ async def mode_semantics_test(dut): assert await tb.read_b(0) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not env_flag("BYTE_WR_EN_G", default=False)) async def byte_write_enable_test(dut): tb = TB(dut) await tb.warmup() - if not tb.byte_write_enabled: - return - await tb.write_a(3, 0xABCD) await tb.write_b(3, 0x00EF, byte_mask=0b01) assert await tb.read_a(3) == 0xABEF @@ -185,11 +188,8 @@ async def byte_write_enable_test(dut): assert await tb.read_a(3) == 0x12EF -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_DUAL_WRITE_COLLISION", default=False)) async def dual_write_collision_test(dut): - if not env_flag("CHECK_DUAL_WRITE_COLLISION", default=False): - return - tb = TB(dut) await tb.warmup() @@ -225,13 +225,10 @@ async def dual_write_collision_test(dut): assert await tb.read_b(5) == 0x7777 -@cocotb.test() +@cocotb.test(skip=not output_register_enabled("B")) async def registered_output_hold_test(dut): tb = TB(dut) await tb.warmup() - if not tb.dob_reg_enabled: - return - await tb.write_a(0, 0x1111) await tb.write_a(1, 0x2222) assert await tb.read_b(0) == 0x1111 @@ -248,13 +245,10 @@ async def registered_output_hold_test(dut): assert int(dut.doutb.value) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not output_register_enabled("A")) async def registered_output_hold_a_test(dut): tb = TB(dut) await tb.warmup() - if not tb.doa_reg_enabled: - return - await tb.write_b(0, 0x1111) await tb.write_b(1, 0x2222) assert await tb.read_a(0) == 0x1111 diff --git a/tests/base/sync/test_Synchronizer.py b/tests/base/sync/test_Synchronizer.py index 01b7e47480..1d7b5564e5 100644 --- a/tests/base/sync/test_Synchronizer.py +++ b/tests/base/sync/test_Synchronizer.py @@ -28,21 +28,18 @@ from tests.base.sync.sync_test_utils import SynchronizerLikeTB from tests.common.regression_utils import ( + env_flag, hdl_parameters_from, parameter_case, run_surf_vhdl_test, ) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def propagation_latency_test(dut): # Each `@cocotb.test()` function is an async coroutine. cocotb starts it, # gives it the HDL `dut`, and advances simulation time whenever we `await`. tb = SynchronizerLikeTB(dut, width=1) - if tb.bypass_enabled: - # The bypass case has different behavior and is covered by its own test. - return - await tb.reset() # After reset, the synchronizer should present the reset/default value. @@ -54,12 +51,9 @@ async def propagation_latency_test(dut): await tb.drive_and_expect_after_latency(0) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def reset_behavior_test(dut): tb = SynchronizerLikeTB(dut, width=1) - if tb.bypass_enabled: - return - # First prove the DUT can leave reset and pass a non-default value through. await tb.reset() await tb.drive_and_expect_after_latency(1) @@ -89,12 +83,9 @@ async def reset_behavior_test(dut): assert int(dut.dataOut.value) == tb.expected_output(1) -@cocotb.test() +@cocotb.test(skip=not env_flag("BYPASS_SYNC_G", default=False)) async def bypass_mode_test(dut): tb = SynchronizerLikeTB(dut, width=1) - if not tb.bypass_enabled: - return - # In bypass mode, the DUT is combinational from input to output, so no # clock edge is needed to observe each new value. dut.rst.value = tb.reset_inactive_value() diff --git a/tests/base/sync/test_SynchronizerFifo.py b/tests/base/sync/test_SynchronizerFifo.py index 6e6919cf9c..20f1596cac 100644 --- a/tests/base/sync/test_SynchronizerFifo.py +++ b/tests/base/sync/test_SynchronizerFifo.py @@ -182,12 +182,9 @@ async def data_order_test(dut): assert observed == values -@cocotb.test() +@cocotb.test(skip=not env_flag("COMMON_CLK_G", default=False)) async def common_clock_bypass_test(dut): tb = TB(dut) - if not tb.common_clk: - return - await tb.reset() await tb.expect_common_clock_passthrough(0x5A) @@ -204,11 +201,8 @@ async def reset_value_test(dut): assert observed == tb.init_value -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_ASYNC_BURST_GAPS", default=False)) async def async_burst_read_gap_test(dut): - if not env_flag("CHECK_ASYNC_BURST_GAPS", default=False): - return - tb = TB(dut) await tb.reset() assert not tb.common_clk @@ -235,11 +229,8 @@ async def async_burst_read_gap_test(dut): assert await tb.read_with_pause() == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_RESET_WHILE_PREFETCHED", default=False)) async def reset_while_prefetched_test(dut): - if not env_flag("CHECK_RESET_WHILE_PREFETCHED", default=False): - return - tb = TB(dut) await tb.reset() assert not tb.common_clk diff --git a/tests/base/sync/test_SynchronizerVector.py b/tests/base/sync/test_SynchronizerVector.py index a9d8c1795e..ea26259db2 100644 --- a/tests/base/sync/test_SynchronizerVector.py +++ b/tests/base/sync/test_SynchronizerVector.py @@ -29,20 +29,18 @@ from tests.base.sync.sync_test_utils import SynchronizerLikeTB from tests.common.regression_utils import ( + env_flag, hdl_parameters_from, parameter_case, run_surf_vhdl_test, ) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def propagation_latency_test(dut): # This test is structurally the same as the scalar synchronizer test; the # difference is that the data path now carries a whole vector at once. tb = SynchronizerLikeTB(dut, width=int(os.environ["WIDTH_G"])) - if tb.bypass_enabled: - return - await tb.reset() assert int(dut.dataOut.value) == tb.expected_output(0) @@ -52,12 +50,9 @@ async def propagation_latency_test(dut): await tb.drive_and_expect_after_latency(0b010101 & tb.mask) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def reset_behavior_test(dut): tb = SynchronizerLikeTB(dut, width=int(os.environ["WIDTH_G"])) - if tb.bypass_enabled: - return - # Fill every bit with 1s so reset behavior is obvious on the output. await tb.reset() await tb.drive_and_expect_after_latency(tb.mask) @@ -84,12 +79,9 @@ async def reset_behavior_test(dut): assert int(dut.dataOut.value) == tb.expected_output(tb.mask) -@cocotb.test() +@cocotb.test(skip=not env_flag("BYPASS_SYNC_G", default=False)) async def bypass_mode_test(dut): tb = SynchronizerLikeTB(dut, width=int(os.environ["WIDTH_G"])) - if not tb.bypass_enabled: - return - # In bypass mode, just sample a few representative vector values directly. dut.rst.value = tb.reset_inactive_value() for value in (0, 0b101001 & tb.mask, tb.mask): diff --git a/tests/common/README.md b/tests/common/README.md index c0eb7125d0..43f35c0278 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -68,6 +68,9 @@ and build-isolation conventions where they apply. `_G`. - `env_flag()`, `env_sl()`, `env_int()`, `env_hex()`, and `env_float()` parse simulator environment values consistently inside cocotb coroutines. +- `cocotb_test_filter()`, `cocotb_test_filter_excluding()`, and + `cocotb_filtered_env()` build explicit scenario groups while preserving an + externally requested focused selector in the simulation-build identity. - `start_lockstep_clocks()` drives multiple logically common clocks from one coroutine so their edges cannot drift. - `build_vhdl_sources()` and `merge_vhdl_sources()` are runner plumbing; tests @@ -154,8 +157,11 @@ coverage is reported but does not make the command fail. The checked-in `compliance_baseline.json` ratchets the rules that are reliable enough to enforce structurally: methodology presence, ordinary direct-runner -exceptions, and literal VHDL sources duplicated from the ruckus import. Run the -check after importing the HDL tree: +exceptions, literal VHDL sources duplicated from the ruckus import, and a bare +entrypoint return reached before any awaited simulator activity or assertion. +Returns after test activity remain report-only because they can be intentional +successful terminal paths and require semantic review. Run the check after +importing the HDL tree: ```bash make MODULES="$PWD" import diff --git a/tests/common/compliance_audit.py b/tests/common/compliance_audit.py index 69787eb1cd..12c82f867a 100644 --- a/tests/common/compliance_audit.py +++ b/tests/common/compliance_audit.py @@ -28,6 +28,7 @@ ENFORCED_RULES = { "direct-runner", "duplicate-imported-source", + "early-bare-return", "missing-methodology", } @@ -39,7 +40,7 @@ METHODOLOGY_MARKER = "Test methodology:" ENVIRONMENT_CONTROL_RE = re.compile( - r"^(?:RUN_[A-Z0-9_]*_TESTS|COCOTB_TESTCASE|[A-Z0-9_]*TESTCASE)$" + r"^(?:RUN_[A-Z0-9_]*_TESTS|COCOTB_TEST_FILTER|COCOTB_TESTCASE|[A-Z0-9_]*TESTCASE)$" ) DOCUMENTED_DIRECT_RUNNERS = { @@ -247,6 +248,11 @@ def _environment_controls(tree: ast.Module) -> tuple[str, ...]: for value, _ in _string_constants(tree) if ENVIRONMENT_CONTROL_RE.fullmatch(value) } + if any( + isinstance(node, ast.Call) and _qualified_name(node.func).endswith("cocotb_filtered_env") + for node in ast.walk(tree) + ): + controls.add("COCOTB_TEST_FILTER") return tuple(sorted(controls)) @@ -413,9 +419,15 @@ def audit_source( ) for function in cocotb_functions: - for node in _walk_function(function): + function_nodes = _walk_function(function) + for node in function_nodes: if isinstance(node, ast.Return) and node.value is None: - rule = "early-bare-return" if node.lineno - function.lineno <= 12 else "bare-return" + has_prior_test_activity = any( + isinstance(prior, (ast.Assert, ast.Await)) + and prior.lineno < node.lineno + for prior in function_nodes + ) + rule = "bare-return" if has_prior_test_activity else "early-bare-return" findings.append( Finding( rule, diff --git a/tests/common/compliance_baseline.json b/tests/common/compliance_baseline.json index dd2cb74433..14aa90a60c 100644 --- a/tests/common/compliance_baseline.json +++ b/tests/common/compliance_baseline.json @@ -75,6 +75,7 @@ "tests/protocols/rssi/test_RssiRxFsm.py": 4, "tests/protocols/rssi/test_RssiTxFsm.py": 2 }, + "early-bare-return": {}, "missing-methodology": { "tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py": 1, "tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py": 1, diff --git a/tests/common/regression_utils.py b/tests/common/regression_utils.py index 40cce48bb3..5dcb9bd81b 100644 --- a/tests/common/regression_utils.py +++ b/tests/common/regression_utils.py @@ -14,6 +14,7 @@ import hashlib import os from pathlib import Path +import re import shlex import subprocess @@ -158,6 +159,39 @@ def hdl_parameters_from(parameters: dict[str, object]) -> dict[str, object]: } +def cocotb_test_filter(*test_names: str) -> str: + if not test_names: + raise ValueError("At least one cocotb test name is required") + alternatives = "|".join(re.escape(name) for name in test_names) + return rf"(?:{alternatives})$" + + +def cocotb_test_filter_excluding(*test_names: str) -> str: + if not test_names: + raise ValueError("At least one cocotb test name is required") + alternatives = "|".join(re.escape(name) for name in test_names) + return rf"^(?!.*(?:{alternatives})$).*" + + +def cocotb_filtered_env( + extra_env: dict[str, object], + test_filter: str, +) -> dict[str, object]: + result = dict(extra_env) + external_selectors = { + name: os.environ[name] + for name in ("COCOTB_TESTCASE", "COCOTB_TEST_FILTER") + if name in os.environ + } + if len(external_selectors) > 1: + raise ValueError("Specify only one of COCOTB_TESTCASE or COCOTB_TEST_FILTER") + if external_selectors: + result.update(external_selectors) + else: + result["COCOTB_TEST_FILTER"] = test_filter + return result + + def build_vhdl_sources() -> dict[str, list[str]]: surf_dir = BUILD_SRC_ROOT / "surf" ruckus_dir = BUILD_SRC_ROOT / "ruckus" diff --git a/tests/common/test_compliance_audit.py b/tests/common/test_compliance_audit.py index 987cf406dc..fecceb56cb 100644 --- a/tests/common/test_compliance_audit.py +++ b/tests/common/test_compliance_audit.py @@ -39,6 +39,7 @@ def test_inventory_records_public_test_identifiers_and_controls(): RUN = "RUN_SAMPLE_KNOWN_ISSUE_TESTS" SELECTOR = "COCOTB_TESTCASE" +FILTER = "COCOTB_TEST_FILTER" @cocotb.test(timeout_time=2, timeout_unit="us") async def transfer_test(dut): @@ -57,12 +58,24 @@ def test_wrapper(parameters): assert inventory.parameter_ids == ("default",) assert inventory.environment_controls == ( "COCOTB_TESTCASE", + "COCOTB_TEST_FILTER", "RUN_SAMPLE_KNOWN_ISSUE_TESTS", ) assert inventory.skipped_functions == ("test_wrapper",) assert inventory.timeout_entrypoints == ("transfer_test",) +def test_inventory_recognizes_shared_filter_helper_as_selector_control(): + source = """ +def test_wrapper(parameters): + run(extra_env=cocotb_filtered_env(parameters, "default_test$")) +""" + + inventory = compliance_audit.inventory_source(source) + + assert inventory.environment_controls == ("COCOTB_TEST_FILTER",) + + def test_audit_reports_high_and_low_confidence_screening_signals(): source = """ import cocotb @@ -91,6 +104,38 @@ async def routed_case(dut): assert {finding.symbol for finding in findings} == {"", "routed_case"} +def test_audit_classifies_returns_by_prior_test_activity_not_line_distance(): + source = LICENSE_AND_METHODOLOGY + ''' +import cocotb + +@cocotb.test() +async def prestimulus_return(dut): + """A long description can move a no-op return far below the decorator. + + The rule must still recognize that the function has performed no awaited + simulator operation and made no assertion before taking this branch. + """ + mode = 1 + if mode: + return + +@cocotb.test() +async def successful_terminal_path(dut): + await exercise_named_behavior(dut) + assert dut.done.value + if dut.short_path.value: + return +''' + + findings = compliance_audit.audit_source(source) + returns = [finding for finding in findings if "bare-return" in finding.rule] + + assert {(finding.rule, finding.symbol) for finding in returns} == { + ("early-bare-return", "prestimulus_return"), + ("bare-return", "successful_terminal_path"), + } + + def test_audit_ignores_retained_tasks_and_clock_tasks(): source = LICENSE_AND_METHODOLOGY + """ import cocotb @@ -206,6 +251,7 @@ def test_compliance_baseline_allows_cleanup_but_rejects_new_findings(): "rules": { "direct-runner": {"tests/old.py": 1}, "duplicate-imported-source": {"tests/sources.py": 2}, + "early-bare-return": {"tests/ambiguous.py": 1}, "missing-methodology": {}, }, } @@ -275,6 +321,7 @@ def test_compliance_baseline_counts_findings_by_rule_and_path(): "rules": { "direct-runner": {}, "duplicate-imported-source": {"tests/source.py": 2}, + "early-bare-return": {}, "missing-methodology": {}, }, } diff --git a/tests/common/test_regression_utils.py b/tests/common/test_regression_utils.py new file mode 100644 index 0000000000..434e15fed2 --- /dev/null +++ b/tests/common/test_regression_utils.py @@ -0,0 +1,75 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +import re + +import pytest + +from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter, + cocotb_test_filter_excluding, +) + + +def test_cocotb_test_filter_matches_only_named_entrypoints(): + pattern = cocotb_test_filter("server_opens_test", "server_closes_test") + + assert re.search(pattern, "tests.protocol.server_opens_test") + assert re.search(pattern, "tests.protocol.server_closes_test") + assert not re.search(pattern, "tests.protocol.client_opens_test") + assert not re.search(pattern, "tests.protocol.server_opens_test_extra") + + +def test_cocotb_test_filter_excluding_rejects_only_named_entrypoints(): + pattern = cocotb_test_filter_excluding("routed_only_test", "extended_test") + + assert re.search(pattern, "tests.protocol.default_test") + assert not re.search(pattern, "tests.protocol.routed_only_test") + assert not re.search(pattern, "tests.protocol.extended_test") + assert re.search(pattern, "tests.protocol.extended_test_extra") + + +@pytest.mark.parametrize("builder", (cocotb_test_filter, cocotb_test_filter_excluding)) +def test_cocotb_filter_builders_require_a_test_name(builder): + with pytest.raises(ValueError, match="At least one"): + builder() + + +def test_cocotb_filtered_env_adds_filter_without_mutating_input(monkeypatch): + monkeypatch.delenv("COCOTB_TESTCASE", raising=False) + monkeypatch.delenv("COCOTB_TEST_FILTER", raising=False) + original = {"MODE_G": "ROUTED"} + + result = cocotb_filtered_env(original, "routed_.*_test$") + + assert result == { + "MODE_G": "ROUTED", + "COCOTB_TEST_FILTER": "routed_.*_test$", + } + assert original == {"MODE_G": "ROUTED"} + + +@pytest.mark.parametrize("selector", ("COCOTB_TESTCASE", "COCOTB_TEST_FILTER")) +def test_cocotb_filtered_env_preserves_external_selection(monkeypatch, selector): + monkeypatch.setenv(selector, "focused_test") + + assert cocotb_filtered_env({"MODE_G": "ROUTED"}, "default_.*") == { + "MODE_G": "ROUTED", + selector: "focused_test", + } + + +def test_cocotb_filtered_env_rejects_conflicting_external_selectors(monkeypatch): + monkeypatch.setenv("COCOTB_TESTCASE", "one_test") + monkeypatch.setenv("COCOTB_TEST_FILTER", "other_test") + + with pytest.raises(ValueError, match="Specify only one"): + cocotb_filtered_env({}, "default_.*") diff --git a/tests/protocols/batcher/README.md b/tests/protocols/batcher/README.md index de3a396d4d..c22adc76d8 100644 --- a/tests/protocols/batcher/README.md +++ b/tests/protocols/batcher/README.md @@ -29,6 +29,12 @@ the scenario as skipped with the configuration in the reason. Do not enter a cocotb test and return successfully before its named routing, remapping, or transition behavior has been exercised. +The event-builder pytest wrapper uses `COCOTB_TEST_FILTER` to exclude the routed +transition-frame scenario from the INDEXED configuration before simulation. +All other event-builder scenarios apply to both modes. Add future mode-specific +cases to that explicit selection policy instead of branching out of the cocotb +entrypoint. + Run the suite with: ```bash diff --git a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py index 2fd9a23b86..24908f8062 100644 --- a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py +++ b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py @@ -362,8 +362,6 @@ async def bypass_skips_source_and_recovers_test(dut): async def routed_transition_frame_preempts_event_test(dut): tb = TB(dut) await tb.reset() - if tb.mode != "ROUTED": - return transition = _frame(bytes(range(0x80, 0x85)), dest=tb.trans_tdest, first_user=0x91, last_user=0xF1) blocked = AxisBeat( @@ -660,11 +658,20 @@ async def align_check_survives_missing_reference_source_with_timeout_test(dut): ], ) def test_AxiStreamBatcherEventBuilder(parameters): + extra_env = dict(parameters) + if parameters["MODE_G"] != "ROUTED": + # This scenario exercises the routed transition-frame policy and has no + # contract in INDEXED mode. Exclude it before simulation rather than + # recording a cocotb pass that performed no checks. + extra_env["COCOTB_TEST_FILTER"] = ( + r"^(?!.*routed_transition_frame_preempts_event_test$).*" + ) + run_surf_vhdl_test( test_file=__file__, toplevel="surf.axistreambatchereventbuilderwrapper", parameters=parameters, - extra_env=parameters, + extra_env=extra_env, extra_vhdl_sources={ "surf": [ "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", diff --git a/tests/protocols/coaxpress/test_CoaXPressRx.py b/tests/protocols/coaxpress/test_CoaXPressRx.py index fcef05d0e0..f0adb4a72b 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRx.py +++ b/tests/protocols/coaxpress/test_CoaXPressRx.py @@ -438,10 +438,8 @@ async def _pulse_rx_fsm_reset(dut, *, cycles: int = 4) -> None: dut.rxFsmRst.value = 0 -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_one_lane_integration_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -550,11 +548,8 @@ async def coaxpress_rx_one_lane_integration_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 2) async def coaxpress_rx_two_lane_mux_rotation_test(dut): - if env_int("NUM_LANES_G", default=1) != 2: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -669,11 +664,13 @@ async def capture(cycle_index: int) -> None: # Opt-in investigation benches. These stay behind RUN_KNOWN_ISSUE_TESTS until # the remaining 4-lane short-frame boundary issue in CoaXPressRxHsFsm is fixed. # -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_fsm_error_reset_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -739,11 +736,13 @@ async def coaxpress_rx_four_lane_fsm_error_reset_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_clean_rotation_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -797,11 +796,13 @@ async def coaxpress_rx_four_lane_clean_rotation_known_issue_test(dut): assert [beat[2] for beat in data_beats] == [0, 0, 1] * 4, data_beats -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_fsm_error_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -887,11 +888,13 @@ async def coaxpress_rx_four_lane_fsm_error_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_overflow_reset_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -982,11 +985,13 @@ async def coaxpress_rx_four_lane_overflow_reset_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, (signal_counts, observed_recovery_last) -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -1079,11 +1084,13 @@ async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 1 + ), +) async def coaxpress_rx_repeated_single_line_frame_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, diff --git a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py index 07d11be1c8..87e3742a83 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py @@ -214,10 +214,8 @@ def _expected_header_data_from_fields( ) -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -266,10 +264,8 @@ async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -334,11 +330,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -414,11 +407,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 2) async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): - if env_int("NUM_LANES_G", default=1) != 2: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -526,11 +516,8 @@ async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -669,11 +656,13 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): ], trace -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 1 + ), +) async def coaxpress_rx_hs_fsm_repeated_single_line_frame_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) diff --git a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py index b3e3e6d69f..de253cc33e 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py @@ -123,9 +123,8 @@ async def coaxpress_rx_word_packer_repack_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) == 1) async def coaxpress_rx_word_packer_reset_flush_test(dut): - num_lanes = env_int("NUM_LANES_G", default=1) start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -134,9 +133,6 @@ async def coaxpress_rx_word_packer_reset_flush_test(dut): dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut) - if num_lanes == 1: - return - # Leave a half-full packed word buffered, then reset and confirm the next # frame starts cleanly rather than draining stale payload. await send_axis_beats_no_ready( @@ -177,11 +173,8 @@ async def coaxpress_rx_word_packer_reset_flush_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_three_word_last_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -224,11 +217,8 @@ async def coaxpress_rx_word_packer_three_word_last_beat_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_two_plus_one_last_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -276,11 +266,8 @@ async def coaxpress_rx_word_packer_two_plus_one_last_beat_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_offset_two_plus_one_last_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -328,11 +315,8 @@ async def coaxpress_rx_word_packer_offset_two_plus_one_last_beat_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_back_to_back_offset_short_frames_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) diff --git a/tests/protocols/jesd204b/test_Jesd204bLoopback.py b/tests/protocols/jesd204b/test_Jesd204bLoopback.py index 51b472fe09..1af88e354a 100644 --- a/tests/protocols/jesd204b/test_Jesd204bLoopback.py +++ b/tests/protocols/jesd204b/test_Jesd204bLoopback.py @@ -524,7 +524,7 @@ async def check_latency_step(axil_rx, *, lane: int = 0) -> int: # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_jesd204b_dlat_sweep(dut): """Full-LMFC-wrap arrival-phase sweep. @@ -554,10 +554,6 @@ async def test_jesd204b_dlat_sweep(dut): subclass = env_int("SUBCLASS", default=1) scr_enable = env_sl("SCR_ENABLE", default=0) - if subclass != 1: - # Sweep requires Subclass 1 LMFC alignment; SC0 has no deterministic latency - return - tb = Jesd204bLoopbackTB(dut, l_g) await tb.reset() @@ -737,7 +733,7 @@ async def _wait_data_valid_drop(tb: Jesd204bLoopbackTB, l_g: int, timeout: int = # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_jesd204b_resync_matrix(dut): """Marked-sample latency invariant across all four resync paths. @@ -760,9 +756,6 @@ async def test_jesd204b_resync_matrix(dut): subclass = env_int("SUBCLASS", default=1) scr_enable = env_sl("SCR_ENABLE", default=0) - if subclass != 1: - return # requires SC1 deterministic latency - tb = Jesd204bLoopbackTB(dut, l_g) await tb.reset() @@ -925,7 +918,7 @@ async def _restart_link(se): # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_jesd204b_rst02(dut): """Behavior contract: sticky error latches, counter resume, status tracking. @@ -943,9 +936,6 @@ async def test_jesd204b_rst02(dut): subclass = env_int("SUBCLASS", default=1) scr_enable = env_sl("SCR_ENABLE", default=0) - if subclass != 1: - return # requires SC1 for deterministic behavior - tb = Jesd204bLoopbackTB(dut, l_g) await tb.reset() diff --git a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py index eb1401ec73..f0ff3de808 100644 --- a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py +++ b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py @@ -98,7 +98,7 @@ def _init_dut(dut, *, scr_enable: int) -> JesdTB: # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 0) async def test_char03_restore_nonscrambled(dut): """Non-scrambled: original data recovered, charisk cleared (§5.3.3.4.2). @@ -110,11 +110,6 @@ async def test_char03_restore_nonscrambled(dut): Original data recovered, no residual control chars. """ f = env_int("F_G", default=2) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # Non-scrambled path only for scr=0 - if scr_enable != 0: - return tb = _init_dut(dut, scr_enable=0) await tb.reset() @@ -177,7 +172,7 @@ async def test_char03_restore_nonscrambled(dut): # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 1) async def test_char03_restore_scrambled(dut): """Scrambled: original data recovered, charisk cleared (§5.3.3.4.3). @@ -192,11 +187,6 @@ async def test_char03_restore_scrambled(dut): Original data recovered, no residual control chars. """ f = env_int("F_G", default=2) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # Scrambled path only for scr=1 - if scr_enable != 1: - return tb = _init_dut(dut, scr_enable=1) await tb.reset() diff --git a/tests/protocols/jesd204b/test_JesdSyncFsmTx.py b/tests/protocols/jesd204b/test_JesdSyncFsmTx.py index 3099df4245..64114eed9a 100644 --- a/tests/protocols/jesd204b/test_JesdSyncFsmTx.py +++ b/tests/protocols/jesd204b/test_JesdSyncFsmTx.py @@ -131,7 +131,7 @@ async def startup_sc0(dut, tb): # Test 1 (SC1): Subclass-1 startup and E3 exit # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_cgs01_subclass1(dut): """Code-group-sync (SC1): IDLE->SYNC->ILA->DATA for Subclass 1 with SYSREF gate. @@ -146,10 +146,6 @@ async def test_cgs01_subclass1(dut): checks use wait_for_signal or are done after bounded settling. """ num_mf = env_int("NUM_ILAS_MF_G", default=4) - subclass = env_int("SUBCLASS", default=1) - - if subclass != 1: - return dut.enable_i.setimmediatevalue(0) dut.nSync_i.setimmediatevalue(0) @@ -203,7 +199,7 @@ async def test_cgs01_subclass1(dut): # Test 2 (SC0): Subclass-0 startup and E3 exit # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 0) async def test_cgs01_subclass0(dut): """Code-group-sync (SC0): IDLE->SYNC->ILA->DATA for Subclass 0 (no SYSREF). @@ -214,10 +210,6 @@ async def test_cgs01_subclass0(dut): - NUM_ILAS_MF_G-1 pulses leaves FSM in ILA_S (off-by-one guard). """ num_mf = env_int("NUM_ILAS_MF_G", default=4) - subclass = env_int("SUBCLASS", default=1) - - if subclass != 0: - return dut.enable_i.setimmediatevalue(0) dut.nSync_i.setimmediatevalue(0) diff --git a/tests/protocols/jesd204b/test_JesdTxLane.py b/tests/protocols/jesd204b/test_JesdTxLane.py index a7e1321bfe..eed54a4590 100644 --- a/tests/protocols/jesd204b/test_JesdTxLane.py +++ b/tests/protocols/jesd204b/test_JesdTxLane.py @@ -588,7 +588,7 @@ async def _enter_data_phase( # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 0) async def test_char01_nonscrambled(dut): """Char replacement: /F/ and /A/ substitution for non-scrambled links. @@ -607,11 +607,6 @@ async def test_char01_nonscrambled(dut): k = env_int("K_G", default=32) f = env_int("F_G", default=2) subclass = env_int("SUBCLASS", default=1) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # char replacement only meaningful for non-scrambled cases - if scr_enable != 0: - return tb = TxLaneTB(dut) await tb.reset() @@ -699,7 +694,7 @@ async def test_char01_nonscrambled(dut): # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 1) async def test_char02_scrambled(dut): """Char replacement: /F/ and /A/ substitution for scrambled links. @@ -715,11 +710,6 @@ async def test_char02_scrambled(dut): k = env_int("K_G", default=32) f = env_int("F_G", default=2) subclass = env_int("SUBCLASS", default=1) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # char replacement only meaningful for scrambled cases - if scr_enable != 1: - return tb = TxLaneTB(dut) await tb.reset() diff --git a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py index 611a962aad..5590e01264 100644 --- a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py +++ b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py @@ -204,12 +204,14 @@ async def assert_no_output_words(tb: Pgp4RxEbTB, *, cycles: int): assert signal_int(tb.dut, "pgpRxValid") == 0 -@cocotb.test() +@cocotb.test( + skip=( + env_flag("EXPECT_SKIP_DISABLED", default=False) + or env_flag("EXPECT_OVERFLOW", default=False) + ), +) async def pgp4_rx_eb_filters_skip_and_preserves_stream_order(dut): tb = Pgp4RxEbTB(dut) - if env_flag("EXPECT_SKIP_DISABLED", default=False) or env_flag("EXPECT_OVERFLOW", default=False): - return - initialize_phy_inputs(dut) await tb.reset() @@ -247,12 +249,9 @@ async def pgp4_rx_eb_filters_skip_and_preserves_stream_order(dut): ] await wait_for_signal_in_domain(tb, "remLinkData", value=skip_data, cycles=64) -@cocotb.test() +@cocotb.test(skip=env_flag("EXPECT_SKIP_DISABLED", default=False)) async def pgp4_rx_eb_reset_flushes_buffered_words(dut): tb = Pgp4RxEbTB(dut) - if env_flag("EXPECT_SKIP_DISABLED", default=False): - return - initialize_phy_inputs(dut) await tb.reset() @@ -272,21 +271,20 @@ async def pgp4_rx_eb_reset_flushes_buffered_words(dut): assert words == [(PGP4_D_HEADER, marker_word)] -@cocotb.test() +@cocotb.test( + skip=( + env_flag("EXPECT_SKIP_DISABLED", default=False) + or not env_flag("EXPECT_OVERFLOW", default=False) + ), +) async def pgp4_rx_eb_overflow_pulses_when_phy_outpaces_local_clock(dut): tb = Pgp4RxEbTB(dut) - if env_flag("EXPECT_SKIP_DISABLED", default=False): - return - initialize_phy_inputs(dut) await tb.reset() # Overflow is not expected under the slight-drift case, so keep the normal # regression realistic and only execute the deep fill test when the pytest # parameter sweep requests the explicit overflow-stress clock ratio. - if not env_flag("EXPECT_OVERFLOW", default=False): - return - overflow_monitor = PulseMonitor(dut, "overflow", step=tb.cycle_pgp) cocotb.start_soon(overflow_monitor.run()) @@ -300,12 +298,9 @@ async def pgp4_rx_eb_overflow_pulses_when_phy_outpaces_local_clock(dut): assert overflow_monitor.seen -@cocotb.test() +@cocotb.test(skip=not env_flag("EXPECT_SKIP_DISABLED", default=False)) async def pgp4_rx_eb_skip_disabled_passes_stream_and_link_error(dut): tb = Pgp4RxEbTB(dut) - if not env_flag("EXPECT_SKIP_DISABLED", default=False): - return - initialize_phy_inputs(dut) await tb.reset() diff --git a/tests/protocols/rssi/README.md b/tests/protocols/rssi/README.md index c2246e46c4..aafda44d22 100644 --- a/tests/protocols/rssi/README.md +++ b/tests/protocols/rssi/README.md @@ -30,10 +30,11 @@ The suite progresses from leaves to integration: Default CI runs the currently stable RSSI cases. Focused cases that still expose unresolved RTL behavior are opt-in behind `RUN_RSSI_KNOWN_ISSUE_TESTS=1`, and a smaller group of long-running integration cases additionally uses -`RUN_RSSI_EXTENDED_TESTS=1`. A `COCOTB_TESTCASE` selector chooses a named -cocotb scenario; the `RUN_*` gates decide whether the corresponding pytest node -is eligible to launch it. Keep these roles separate so an enabled node cannot -silently run unrelated scenarios. +`RUN_RSSI_EXTENDED_TESTS=1`. `COCOTB_TESTCASE` selects one named scenario, while +`COCOTB_TEST_FILTER` selects an applicable scenario group such as the client or +server connection-FSM cases. The `RUN_*` gates decide whether the corresponding +pytest node is eligible to launch a simulation. Keep these roles separate so an +enabled node cannot silently run unrelated scenarios. Keep the skip reason beside each gated pytest entry. A known-issue case must identify a durable defect reference or documented local issue, state the diff --git a/tests/protocols/rssi/test_RssiConnFsm.py b/tests/protocols/rssi/test_RssiConnFsm.py index e31818c32c..f8cafed2cb 100644 --- a/tests/protocols/rssi/test_RssiConnFsm.py +++ b/tests/protocols/rssi/test_RssiConnFsm.py @@ -29,15 +29,15 @@ # by close rather than counter overflow. # - Timing: Status checks wait past the default `TPD_G` output delay after each # clock edge. Timeout generics are kept small so retries and peer-timeout -# closure remain deterministic within a directed cocotb test. - -import os +# closure remain deterministic within a directed cocotb test. The pytest +# wrapper selects only server-named scenarios for `SERVER_G=true` and only +# client-named scenarios for `SERVER_G=false`. import cocotb import pytest from cocotb.triggers import Timer -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import cocotb_filtered_env, run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import RssiParams from tests.protocols.ssi.ssi_test_utils import ( cycle as ssi_cycle, @@ -163,15 +163,8 @@ async def wait_state(self, expected: int, *, cycles: int = 16) -> None: raise AssertionError(f"Timed out waiting for connState_o={expected:#x}") -def server_mode() -> bool: - return os.environ.get("SERVER_G", "true").lower() == "true" - - @cocotb.test() async def server_accepts_syn_ack_and_opens_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) tb.dut.connRq_i.value = 1 await tb.cycle() @@ -195,9 +188,6 @@ async def server_accepts_syn_ack_and_opens_test(dut): @cocotb.test() async def server_proposes_local_required_parameters_on_mismatch_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) app_params = RssiParams( version=1, @@ -233,9 +223,6 @@ async def server_proposes_local_required_parameters_on_mismatch_test(dut): @cocotb.test() async def server_rejects_out_of_range_syn_parameters_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) app_params = RssiParams( version=1, @@ -278,9 +265,6 @@ async def server_rejects_out_of_range_syn_parameters_test(dut): @cocotb.test() async def server_retries_syn_ack_then_times_out_waiting_for_ack_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) tb.dut.connRq_i.value = 1 await tb.cycle() @@ -306,9 +290,6 @@ async def server_retries_syn_ack_then_times_out_waiting_for_ack_test(dut): @cocotb.test() async def client_accepts_syn_ack_clamps_and_opens_test(dut): - if server_mode(): - return - tb = await TB.create(dut) app_params = RssiParams(max_outs_seg=4, max_seg_size=64) peer_params = RssiParams(max_outs_seg=8, max_seg_size=128) @@ -336,9 +317,6 @@ async def client_accepts_syn_ack_clamps_and_opens_test(dut): @cocotb.test() async def client_rejects_mismatched_syn_ack_with_rst_test(dut): - if server_mode(): - return - tb = await TB.create(dut) tb.set_app_params(RssiParams(version=1, chksum_en=1, timeout_unit=1)) tb.set_rx_params(RssiParams(version=2, chksum_en=1, timeout_unit=1)) @@ -360,9 +338,6 @@ async def client_rejects_mismatched_syn_ack_with_rst_test(dut): @cocotb.test() async def client_rejects_out_of_range_syn_ack_with_rst_test(dut): - if server_mode(): - return - tb = await TB.create(dut) tb.set_app_params(RssiParams(version=1, chksum_en=1, timeout_unit=1)) tb.set_rx_params( @@ -394,9 +369,6 @@ async def client_rejects_out_of_range_syn_ack_with_rst_test(dut): @cocotb.test() async def client_retries_syn_then_times_out_waiting_for_syn_ack_test(dut): - if server_mode(): - return - tb = await TB.create(dut) tb.dut.connRq_i.value = 1 await tb.wait_high("sndSyn_o") @@ -425,11 +397,12 @@ async def client_retries_syn_then_times_out_waiting_for_syn_ack_test(dut): @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_RssiConnFsm(parameters): + role = "server" if parameters["SERVER_G"] else "client" run_surf_vhdl_test( test_file=__file__, toplevel="surf.rssiconnfsmwrapper", parameters=parameters, - extra_env=parameters, + extra_env=cocotb_filtered_env(parameters, rf"{role}_.*_test$"), extra_vhdl_sources={ "surf": [ "protocols/rssi/v1/rtl/RssiConnFsm.vhd", diff --git a/tests/protocols/rssi/test_RssiCore.py b/tests/protocols/rssi/test_RssiCore.py index 16348826ba..c624857561 100644 --- a/tests/protocols/rssi/test_RssiCore.py +++ b/tests/protocols/rssi/test_RssiCore.py @@ -39,23 +39,26 @@ # generics so the full integration batch remains bounded. Separate pytest # entries enable narrower cocotb tests for sequence wrap, bidirectional DATA # loss in one connection, and out-of-order recovery with longer retransmit -# spacing. Environment flags keep those specialized cocotb tests inert -# during unrelated parameter runs. +# spacing. The default pytest entry filters out those specialized cocotb +# scenarios before simulation; each focused entry selects its named scenario. # - Timing and scoreboarding: Transport monitors sample accepted RSSI frames at # the source side before optional loopback drops. Application scoreboards # assert both absence of premature output and exact recovered frames. Quiet # output drains account for reset-release FIFO behavior so payload assertions # are about RSSI DATA delivery rather than wrapper initialization. -import os - import cocotb import pytest from cocotb.triggers import FallingEdge, RisingEdge, Timer from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter_excluding, + env_flag, + run_surf_vhdl_test, +) from tests.protocols.rssi.rssi_test_utils import ( RSSI_CORE_VHDL_SOURCES, RSSI_FLAG_ACK, @@ -87,13 +90,6 @@ REG_RESEND_CNT = 0x4C -def _run_extended_case(case_name: str) -> bool: - return ( - env_flag("RUN_RSSI_EXTENDED_TESTS", default=False) - or os.environ.get("COCOTB_TESTCASE") == case_name - ) - - class TB: def __init__(self, dut): self.dut = dut @@ -258,6 +254,9 @@ async def send_app_frame(self, endpoint: FlatSsiEndpoint, beats: list[SsiBeat]) endpoint.set_idle() def start_transport_loopbacks(self) -> None: + # These retained coroutines are lifetime agents for one cocotb + # entrypoint. They own no external resources and cocotb cancels them + # when that entrypoint finishes. self.loopback_tasks = [ cocotb.start_soon( self.loopback_transport( @@ -610,9 +609,6 @@ async def dropped_client_data_retransmits_and_recovers_payload_test(dut): @cocotb.test() async def bidirectional_data_losses_recover_without_duplicate_delivery_test(dut): - if not env_flag("RSSI_REPEATED_LOSS_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -676,9 +672,6 @@ async def bidirectional_data_losses_recover_without_duplicate_delivery_test(dut) @cocotb.test() async def lost_first_data_drops_later_data_until_retransmit_test(dut): - if not env_flag("RSSI_OUT_OF_ORDER_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -833,9 +826,6 @@ async def server_backpressure_advertises_busy_to_client_test(dut): @cocotb.test() async def server_backpressure_recovers_without_lost_or_duplicate_frames_test(dut): - if not _run_extended_case("server_backpressure_recovers_without_lost_or_duplicate_frames_test"): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -1039,9 +1029,6 @@ async def close_then_reopen_clears_state_and_delivers_new_payload_test(dut): @cocotb.test() async def client_axil_control_path_opens_injects_reads_and_closes_test(dut): - if not env_flag("RSSI_AXIL_CONTROL_CASE", default=False): - return - tb = await TB.create(dut, direct_client_open=0) await tb.axil_write(REG_CONTROL, 0x0C) @@ -1099,9 +1086,6 @@ async def client_axil_control_path_opens_injects_reads_and_closes_test(dut): @cocotb.test() async def checksum_disabled_connection_and_payload_test(dut): - if not env_flag("RSSI_CHECKSUM_DISABLED_CORE_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -1222,9 +1206,6 @@ async def bidirectional_multi_frame_stress_test(dut): @cocotb.test() async def client_sequence_wraparound_delivers_frame_test(dut): - if not env_flag("RSSI_SEQUENCE_WRAP_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -1270,6 +1251,24 @@ async def client_sequence_wraparound_delivers_frame_test(dut): ) ] +SPECIALIZED_TESTS = ( + "bidirectional_data_losses_recover_without_duplicate_delivery_test", + "checksum_disabled_connection_and_payload_test", + "client_axil_control_path_opens_injects_reads_and_closes_test", + "client_sequence_wraparound_delivers_frame_test", + "lost_first_data_drops_later_data_until_retransmit_test", +) + + +def _default_extra_env(parameters: dict[str, object]) -> dict[str, object]: + excluded = list(SPECIALIZED_TESTS) + if not env_flag("RUN_RSSI_EXTENDED_TESTS", default=False): + excluded.append("server_backpressure_recovers_without_lost_or_duplicate_frames_test") + return cocotb_filtered_env( + parameters, + cocotb_test_filter_excluding(*excluded), + ) + KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" @@ -1280,7 +1279,7 @@ def test_RssiCore(parameters): test_file=__file__, toplevel="surf.rssicoreintegrationwrapper", parameters=parameters, - extra_env=parameters, + extra_env=_default_extra_env(parameters), extra_vhdl_sources={ "surf": [ *RSSI_CORE_VHDL_SOURCES, diff --git a/tests/protocols/rssi/test_RssiCoreWrapper.py b/tests/protocols/rssi/test_RssiCoreWrapper.py index 176c151c13..13acb7938e 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapper.py +++ b/tests/protocols/rssi/test_RssiCoreWrapper.py @@ -32,7 +32,9 @@ # - Parameter strategy: Sweep bypass-chunker and packetizer modes across # multiple `WINDOW_ADDR_SIZE_G` and `MAX_SEG_SIZE_G` values. This catches # wrapper elaboration and derived RSSI FIFO/pause-threshold issues without -# requiring an exhaustive Cartesian product. +# requiring an exhaustive Cartesian product. The default sweep filters out +# the focused BUSY/backpressure scenario; its dedicated pytest node selects +# it explicitly. # - Timing: Small timeout generics keep the wrapper checks bounded. If a # protocol-level failure appears here, reproduce it in `test_RssiCore.py` # unless the failure is clearly caused by wrapper-only packetizer, chunker, or @@ -42,7 +44,12 @@ import pytest from cocotb.triggers import RisingEdge, Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter_excluding, + env_flag, + run_surf_vhdl_test, +) from tests.protocols.rssi.rssi_test_utils import RSSI_CORE_WRAPPER_VHDL_SOURCES from tests.protocols.ssi.ssi_test_utils import ( FlatSsiEndpoint, @@ -208,9 +215,6 @@ async def wrapper_partial_keep_and_eofe_payload_test(dut): @cocotb.test() async def wrapper_server_backpressure_advertises_busy_test(dut): - if not env_flag("RSSI_WRAPPER_BACKPRESSURE_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -286,7 +290,12 @@ def test_RssiCoreWrapper(parameters): test_file=__file__, toplevel="surf.rssicorewrapperintegrationwrapper", parameters=parameters, - extra_env=parameters, + extra_env=cocotb_filtered_env( + parameters, + cocotb_test_filter_excluding( + "wrapper_server_backpressure_advertises_busy_test" + ), + ), extra_vhdl_sources={ "surf": [ *RSSI_CORE_WRAPPER_VHDL_SOURCES, diff --git a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py index 056b993052..bb9fc23d31 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py +++ b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py @@ -42,8 +42,6 @@ # frames after a test arms the hook, leaving ACK/NULL control traffic free to # maintain the connection. -import os - import cocotb import pytest from cocotb.triggers import FallingEdge, RisingEdge, Timer @@ -73,13 +71,6 @@ DEPACKETIZER2_INIT_WAIT_CYCLES = 1024 -def _run_extended_case(case_name: str) -> bool: - return ( - env_flag("RUN_RSSI_EXTENDED_TESTS", default=False) - or os.environ.get("COCOTB_TESTCASE") == case_name - ) - - def _default_extra_env(parameters: dict[str, object]) -> dict[str, object]: if env_flag("RUN_RSSI_EXTENDED_TESTS", default=False): return parameters @@ -200,6 +191,9 @@ async def send_app_frame(self, endpoint: FlatSsiEndpoint, beats: list[SsiBeat]) endpoint.set_idle() def start_transport_loopbacks(self) -> None: + # These retained coroutines are lifetime agents for one cocotb + # entrypoint. They own no external resources and cocotb cancels them + # when that entrypoint finishes. self.loopback_tasks = [ cocotb.start_soon( self.loopback_transport( @@ -334,9 +328,6 @@ async def multi_stream_client_to_server_payload_routes_test(dut): @cocotb.test() async def multi_stream_bidirectional_payload_routes_test(dut): - if not _run_extended_case("multi_stream_bidirectional_payload_routes_test"): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -413,9 +404,6 @@ async def multi_stream_bidirectional_payload_routes_test(dut): @cocotb.test() async def multi_stream_partial_keep_and_eofe_routes_test(dut): - if not _run_extended_case("multi_stream_partial_keep_and_eofe_routes_test"): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -441,9 +429,6 @@ async def multi_stream_partial_keep_and_eofe_routes_test(dut): @cocotb.test() async def multi_stream_dropped_client_data_retransmits_to_route_test(dut): - if not _run_extended_case("multi_stream_dropped_client_data_retransmits_to_route_test"): - return - tb = await TB.create(dut) await tb.wait_connected() diff --git a/tests/protocols/rssi/test_RssiRxFsm.py b/tests/protocols/rssi/test_RssiRxFsm.py index 0c24f635ef..8c947d5bd8 100644 --- a/tests/protocols/rssi/test_RssiRxFsm.py +++ b/tests/protocols/rssi/test_RssiRxFsm.py @@ -32,13 +32,19 @@ # - Timing: Transport input waits for sampled ready before changing beats. # Status checks wait past the default `TPD_G` output delay, and app-output # checks account for the registered segment RAM read latency used by the real -# `RssiCore` path. +# `RssiCore` path. The checksum-enabled sweep filters out the checksum- +# disabled scenario; its dedicated pytest node selects that case explicitly. import cocotb import pytest from cocotb.triggers import Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter_excluding, + env_flag, + run_surf_vhdl_test, +) from tests.protocols.rssi.rssi_test_utils import ( RssiParams, RSSI_FLAG_BUSY, @@ -326,9 +332,6 @@ async def checksum_failed_data_payload_is_flushed_before_retransmit_test(dut): @cocotb.test() async def checksum_disabled_accepts_data_when_checksum_status_is_bad_test(dut): - if not env_flag("RSSI_CHECKSUM_DISABLED_CASE", default=False): - return - tb = await TB.create(dut) payload = 0xCAFE_0000_0000_BEEF @@ -579,7 +582,12 @@ def test_RssiRxFsm(parameters): test_file=__file__, toplevel="surf.rssirxfsmwrapper", parameters=parameters, - extra_env=parameters, + extra_env=cocotb_filtered_env( + parameters, + cocotb_test_filter_excluding( + "checksum_disabled_accepts_data_when_checksum_status_is_bad_test" + ), + ), extra_vhdl_sources={ "surf": [ "protocols/rssi/v1/rtl/RssiRxFsm.vhd", diff --git a/tests/protocols/srp/test_SrpV0AxiLite.py b/tests/protocols/srp/test_SrpV0AxiLite.py index 7bfab9b071..1550777ddf 100644 --- a/tests/protocols/srp/test_SrpV0AxiLite.py +++ b/tests/protocols/srp/test_SrpV0AxiLite.py @@ -240,11 +240,8 @@ async def srpv0_axilite_downstream_error_status_test(dut): assert await read_task == {"address": read_address} -@cocotb.test() +@cocotb.test(skip=os.environ.get("EN_32BIT_ADDR_G", "false").lower() != "true") async def srpv0_axilite_32bit_address_decode_test(dut): - if os.environ.get("EN_32BIT_ADDR_G", "false").lower() != "true": - return - tb = TB(dut, use_ram=False) await tb.reset() From 0f3a8fbfc5fa7a9dbee3199ae02a82efa534b2b7 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 10:11:45 -0700 Subject: [PATCH 38/57] test: normalize regression runner and source handling --- .../regression-test-compliance/README.md | 46 +++++++++++ tests/axi/axi4/test_AxiMemTester.py | 1 - tests/axi/axi4/test_AxiMonAxiL.py | 1 - tests/axi/axi4/test_AxiRateGen.py | 1 - tests/axi/axi4/test_AxiRingBuffer.py | 1 - tests/axi/axi_lite/test_AxiLiteAsync.py | 3 - tests/axi/axi_lite/test_AxiLiteCrossbar.py | 3 - tests/axi/axi_lite/test_AxiLiteFifoPop.py | 1 - tests/axi/axi_lite/test_AxiLiteFifoPush.py | 1 - tests/axi/axi_lite/test_AxiLiteFifoPushPop.py | 1 - tests/axi/axi_lite/test_AxiLiteMaster.py | 3 - tests/axi/axi_lite/test_AxiLiteMasterProxy.py | 6 -- .../test_AxiLiteRamSyncStatusVector.py | 5 -- tests/axi/axi_lite/test_AxiLiteRegs.py | 3 - tests/axi/axi_lite/test_AxiLiteRespTimer.py | 3 - tests/axi/axi_lite/test_AxiLiteRingBuffer.py | 1 - .../axi/axi_lite/test_AxiLiteSequencerRam.py | 6 -- tests/axi/axi_lite/test_AxiLiteSlave.py | 3 - tests/axi/axi_lite/test_AxiLiteWriteFilter.py | 3 - .../axi_stream/test_AxiStreamBatchingFifo.py | 8 -- .../axi/axi_stream/test_AxiStreamCombiner.py | 3 - tests/axi/axi_stream/test_AxiStreamCompact.py | 3 - tests/axi/axi_stream/test_AxiStreamConcat.py | 3 - tests/axi/axi_stream/test_AxiStreamDeMux.py | 3 - tests/axi/axi_stream/test_AxiStreamFlush.py | 3 - .../axi_stream/test_AxiStreamFrameBuffer.py | 7 -- .../test_AxiStreamFrameRateLimiter.py | 3 - tests/axi/axi_stream/test_AxiStreamGearbox.py | 3 - .../axi_stream/test_AxiStreamGearboxPack.py | 3 - .../axi_stream/test_AxiStreamGearboxUnpack.py | 3 - tests/axi/axi_stream/test_AxiStreamMon.py | 6 -- tests/axi/axi_stream/test_AxiStreamMonAxiL.py | 7 -- tests/axi/axi_stream/test_AxiStreamMux.py | 3 - .../axi/axi_stream/test_AxiStreamPipeline.py | 3 - .../axi_stream/test_AxiStreamPrbsFlowCtrl.py | 3 - .../axi/axi_stream/test_AxiStreamRepeater.py | 3 - tests/axi/axi_stream/test_AxiStreamResize.py | 3 - .../axi_stream/test_AxiStreamRingBuffer.py | 7 -- .../axi_stream/test_AxiStreamScatterGather.py | 8 -- tests/axi/axi_stream/test_AxiStreamShift.py | 3 - .../axi/axi_stream/test_AxiStreamSplitter.py | 3 - tests/axi/axi_stream/test_AxiStreamTap.py | 3 - tests/axi/axi_stream/test_AxiStreamTimer.py | 3 - .../axi_stream/test_AxiStreamTrailerAppend.py | 3 - .../axi_stream/test_AxiStreamTrailerRemove.py | 3 - tests/axi/dma/test_AxiStreamDma.py | 1 - tests/axi/dma/test_AxiStreamDmaFifo.py | 1 - tests/axi/dma/test_AxiStreamDmaRingRead.py | 3 - tests/axi/dma/test_AxiStreamDmaRingWrite.py | 3 - tests/axi/dma/test_AxiStreamDmaV2.py | 1 - tests/axi/dma/test_AxiStreamDmaV2Desc.py | 1 - tests/axi/dma/test_AxiStreamDmaV2Fifo.py | 3 - .../dma/test_AxiStreamDmaV2FifoLoopback.py | 3 - tests/axi/dma/test_AxiStreamDmaV2Read.py | 42 +++------- tests/axi/dma/test_AxiStreamDmaWrite.py | 1 - tests/common/README.md | 7 ++ tests/common/compliance_baseline.json | 77 +------------------ tests/common/regression_utils.py | 74 +++++++++++++++++- tests/common/test_regression_utils.py | 57 ++++++++++++++ tests/dsp/generic/test_BoxcarIntegrator.py | 1 - .../dsp/generic/test_FirFilterMultiChannel.py | 6 -- .../generic/test_FirFilterSingleChannel.py | 3 - .../test_FirFilterSingleChannelLowPass.py | 3 - .../test_FirFilterSingleChannelTiming.py | 3 - tests/dsp/generic/test_FirFilterTap.py | 1 - .../batcher/test_AxiStreamBatcherAxil.py | 1 - .../test_AxiStreamBatcherEventBuilder.py | 1 - .../line_codes/line_code_test_utils.py | 2 - .../test_LineCode10b12bIntegration.py | 1 - .../test_LineCode12b14bIntegration.py | 1 - .../test_LineCode8b10bIntegration.py | 1 - tests/protocols/pgp/pgp4/test_Pgp4TxLite.py | 1 - tests/protocols/pgp/pgp_test_utils.py | 6 +- tests/protocols/rssi/rssi_test_utils.py | 13 ---- .../protocols/rssi/test_RssiAxiLiteRegItf.py | 7 -- tests/protocols/rssi/test_RssiConnFsm.py | 7 -- tests/protocols/rssi/test_RssiCore.py | 43 ----------- tests/protocols/rssi/test_RssiCoreWrapper.py | 15 ---- .../rssi/test_RssiCoreWrapperMultiStream.py | 22 ------ tests/protocols/rssi/test_RssiHeaderReg.py | 1 - tests/protocols/rssi/test_RssiMonitor.py | 7 -- tests/protocols/rssi/test_RssiRxFsm.py | 14 ---- tests/protocols/rssi/test_RssiTxFsm.py | 7 -- 83 files changed, 199 insertions(+), 437 deletions(-) diff --git a/docs/plans/regression-test-compliance/README.md b/docs/plans/regression-test-compliance/README.md index 76d08f86ba..46180fa1f6 100644 --- a/docs/plans/regression-test-compliance/README.md +++ b/docs/plans/regression-test-compliance/README.md @@ -69,6 +69,52 @@ no removed pytest nodes or cocotb entrypoints. The 14 CoaXPress/PGP4/SRP parameter nodes pass both serially and under pytest-xdist; the JESD-specific 24-node matrix also passes in both modes. +Phase 2 has completed its AXI source-list pass. A fresh ruckus import confirmed +76 literal AXI paths were exact duplicates of design units already supplied by +`build/SRC_VHDL`; those entries were removed from 53 tests while the AXI4 and +DMA wrappers absent from the import were retained in their mixed source lists. +The AXI preservation report found no removed test identifiers. After deleting +only the ignored AXI simulator cache, the complete 117-node AXI suite passed a +fresh xdist build in 227.12 seconds and then passed again from cache in 17.74 +seconds. The checked-in duplicate-source baseline now contains no AXI entries. + +The remaining source-list pass removed 43 literal duplicate entries from DSP, +batcher, and RSSI tests, plus the dynamically expanded RSSI core source bundles +that the literal audit did not count. Test-only DSP and batcher wrappers absent +from ruckus remain explicit; every RSSI integration wrapper is already imported +and now uses that authoritative copy. The repository duplicate-source count and +blocking baseline are both zero, with no preservation-report changes. From +clean simulator caches, the combined suites pass with 37 passed and 17 gated +skips in 53.42 seconds; the cached repeat reports the same result in 15.51 +seconds. + +`test_AxiStreamDmaV2Read.py` now uses `run_surf_vhdl_test()` and keeps only its +two AXI4/DMA wrappers that are absent from the ruckus import; the production +read engine is supplied by ruckus. Both public parameter IDs are preserved. A +fresh serial build passes both cases in 16.95 seconds and the cached xdist +repeat passes in 1.10 seconds. The ordinary direct-runner finding and its +blocking baseline entry are now both zero. + +The shared source merge now rejects both an extra path resolving to an already +imported file and a different extra file redeclaring an entity, package, or +configuration in the same library. A dynamic-list sweep exercised 153 source +expressions without finding an undisclosed collision, then identified four +helper-mediated duplicates outside the literal audit: three line-code +integration testbenches and `Pgp4TxLiteWrapper`. Those tests now use the ruckus +copies. Their five focused cases pass from clean caches in serial mode and on a +cached parallel repeat; a 16-case Ethernet suite with a legitimate dynamic +source bundle also passes with the runtime guard enabled. + +Phase 2 is complete. The 17 RSSI `force_compile=True` arguments were blanket +settings rather than documented exceptions; the shared GHDL runner already +invalidates an elaborated image when any imported source is newer. After +removing them, the full default RSSI suite passed from a deleted simulator +cache with 6 passed and 17 gated skips in 33.68 seconds, then repeated from the +cache under pytest-xdist with the same results in 1.75 seconds. The remaining +VCS SimLink use is retained because that helper deliberately pre-analyzes its +mixed-language topology before asking cocotb-test to elaborate without source +arguments. + ## Non-Negotiable Preservation Rules - Do not remove an existing test, parameter case, opt-in gate, or behavioral diff --git a/tests/axi/axi4/test_AxiMemTester.py b/tests/axi/axi4/test_AxiMemTester.py index d5672c0d85..7f31a5dfad 100644 --- a/tests/axi/axi4/test_AxiMemTester.py +++ b/tests/axi/axi4/test_AxiMemTester.py @@ -116,7 +116,6 @@ def test_AxiMemTester(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiMemTesterIpIntegrator.vhd", ], diff --git a/tests/axi/axi4/test_AxiMonAxiL.py b/tests/axi/axi4/test_AxiMonAxiL.py index 25cc7aff7f..f6af0758aa 100644 --- a/tests/axi/axi4/test_AxiMonAxiL.py +++ b/tests/axi/axi4/test_AxiMonAxiL.py @@ -89,7 +89,6 @@ def test_AxiMonAxiL(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiMonAxiLIpIntegrator.vhd", ], }, diff --git a/tests/axi/axi4/test_AxiRateGen.py b/tests/axi/axi4/test_AxiRateGen.py index 87e9403a8e..e8f754a5e2 100644 --- a/tests/axi/axi4/test_AxiRateGen.py +++ b/tests/axi/axi4/test_AxiRateGen.py @@ -235,7 +235,6 @@ def test_AxiRateGen(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiRateGenIpIntegrator.vhd", ], diff --git a/tests/axi/axi4/test_AxiRingBuffer.py b/tests/axi/axi4/test_AxiRingBuffer.py index 2c7134a3e9..5f16b7668d 100644 --- a/tests/axi/axi4/test_AxiRingBuffer.py +++ b/tests/axi/axi4/test_AxiRingBuffer.py @@ -106,7 +106,6 @@ def test_AxiRingBuffer(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiRingBufferIpIntegrator.vhd", ], diff --git a/tests/axi/axi_lite/test_AxiLiteAsync.py b/tests/axi/axi_lite/test_AxiLiteAsync.py index 5e31bc9880..183509de76 100644 --- a/tests/axi/axi_lite/test_AxiLiteAsync.py +++ b/tests/axi/axi_lite/test_AxiLiteAsync.py @@ -282,7 +282,4 @@ def test_AxiLiteAsync(parameters): toplevel="surf.axiliteasyncipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteAsyncIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteCrossbar.py b/tests/axi/axi_lite/test_AxiLiteCrossbar.py index 8f7ac3f485..a2ff328288 100644 --- a/tests/axi/axi_lite/test_AxiLiteCrossbar.py +++ b/tests/axi/axi_lite/test_AxiLiteCrossbar.py @@ -176,7 +176,4 @@ def test_AxiLiteCrossbar(): run_surf_vhdl_test( test_file=__file__, toplevel="surf.axilitecrossbaripintegrator", - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteCrossbarIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPop.py b/tests/axi/axi_lite/test_AxiLiteFifoPop.py index a8877ee9dd..96d9c8c39c 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPop.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPop.py @@ -108,5 +108,4 @@ def test_AxiLiteFifoPop(parameters): toplevel="surf.axilitefifopopipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteFifoPopIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPush.py b/tests/axi/axi_lite/test_AxiLiteFifoPush.py index d56f84d2e7..e54864f999 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPush.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPush.py @@ -108,5 +108,4 @@ def test_AxiLiteFifoPush(parameters): toplevel="surf.axilitefifopushipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteFifoPushIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py b/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py index 179e4c0769..dc3e81c897 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py @@ -137,5 +137,4 @@ def test_AxiLiteFifoPushPop(parameters): toplevel="surf.axilitefifopushpopipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteFifoPushPopIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteMaster.py b/tests/axi/axi_lite/test_AxiLiteMaster.py index d75a22482e..edb8697515 100644 --- a/tests/axi/axi_lite/test_AxiLiteMaster.py +++ b/tests/axi/axi_lite/test_AxiLiteMaster.py @@ -277,7 +277,4 @@ def test_AxiLiteMaster(parameters): toplevel="surf.axilitemasteripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteMasterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteMasterProxy.py b/tests/axi/axi_lite/test_AxiLiteMasterProxy.py index aada53d232..69745dff67 100644 --- a/tests/axi/axi_lite/test_AxiLiteMasterProxy.py +++ b/tests/axi/axi_lite/test_AxiLiteMasterProxy.py @@ -92,10 +92,4 @@ def test_AxiLiteMasterProxy(parameters): toplevel="surf.axilitemasterproxyipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/MasterAxiLiteIpIntegrator.vhd", - "axi/axi-lite/ip_integrator/AxiLiteMasterProxyIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py b/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py index 737939ff7d..7350d3d597 100644 --- a/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py +++ b/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py @@ -87,9 +87,4 @@ def test_AxiLiteRamSyncStatusVector(parameters): toplevel="surf.axiliteramsyncstatusvectoripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/AxiLiteRamSyncStatusVectorIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRegs.py b/tests/axi/axi_lite/test_AxiLiteRegs.py index 41ece05c7b..336ed1c219 100644 --- a/tests/axi/axi_lite/test_AxiLiteRegs.py +++ b/tests/axi/axi_lite/test_AxiLiteRegs.py @@ -159,7 +159,4 @@ def test_AxiLiteRegs(parameters): toplevel="surf.axiliteregsipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteRegsIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRespTimer.py b/tests/axi/axi_lite/test_AxiLiteRespTimer.py index dd680e8763..bdb625ef69 100644 --- a/tests/axi/axi_lite/test_AxiLiteRespTimer.py +++ b/tests/axi/axi_lite/test_AxiLiteRespTimer.py @@ -121,7 +121,4 @@ def test_AxiLiteRespTimer(parameters): toplevel="surf.axiliteresptimeripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteRespTimerIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRingBuffer.py b/tests/axi/axi_lite/test_AxiLiteRingBuffer.py index e743d30a7d..e567fc4f69 100644 --- a/tests/axi/axi_lite/test_AxiLiteRingBuffer.py +++ b/tests/axi/axi_lite/test_AxiLiteRingBuffer.py @@ -129,5 +129,4 @@ def test_AxiLiteRingBuffer(parameters): toplevel="surf.axiliteringbufferipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteRingBufferIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteSequencerRam.py b/tests/axi/axi_lite/test_AxiLiteSequencerRam.py index 38cddb2de4..d1097b2b08 100644 --- a/tests/axi/axi_lite/test_AxiLiteSequencerRam.py +++ b/tests/axi/axi_lite/test_AxiLiteSequencerRam.py @@ -94,10 +94,4 @@ def test_AxiLiteSequencerRam(parameters): toplevel="surf.axilitesequencerramipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/MasterAxiLiteIpIntegrator.vhd", - "axi/axi-lite/ip_integrator/AxiLiteSequencerRamIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteSlave.py b/tests/axi/axi_lite/test_AxiLiteSlave.py index 2a51a11ea8..a49ecaea5d 100644 --- a/tests/axi/axi_lite/test_AxiLiteSlave.py +++ b/tests/axi/axi_lite/test_AxiLiteSlave.py @@ -140,7 +140,4 @@ def test_AxiLiteSlave(parameters): toplevel="surf.axiliteslaveipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteSlaveIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py index 63dc108fbd..0a41170af0 100644 --- a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py +++ b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py @@ -193,7 +193,4 @@ def test_AxiLiteWriteFilter(parameters): "FILTER_ADDR_0_G": "416", }, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteWriteFilterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py b/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py index 95604324e1..6e648d7ef1 100644 --- a/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py +++ b/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py @@ -92,12 +92,4 @@ def test_AxiStreamBatchingFifo(parameters): toplevel="surf.axistreambatchingfifoipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamBatchingFifoIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamCombiner.py b/tests/axi/axi_stream/test_AxiStreamCombiner.py index 50f6117b4d..414fcd5340 100644 --- a/tests/axi/axi_stream/test_AxiStreamCombiner.py +++ b/tests/axi/axi_stream/test_AxiStreamCombiner.py @@ -105,7 +105,4 @@ def test_AxiStreamCombiner(parameters): toplevel="surf.axistreamcombineripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamCombinerIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamCompact.py b/tests/axi/axi_stream/test_AxiStreamCompact.py index c409f2d297..0e3ab3c5b9 100644 --- a/tests/axi/axi_stream/test_AxiStreamCompact.py +++ b/tests/axi/axi_stream/test_AxiStreamCompact.py @@ -185,7 +185,4 @@ def test_AxiStreamCompact(parameters): toplevel="surf.axistreamcompactipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamCompactIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamConcat.py b/tests/axi/axi_stream/test_AxiStreamConcat.py index 81bf16f0be..d4539edbcd 100644 --- a/tests/axi/axi_stream/test_AxiStreamConcat.py +++ b/tests/axi/axi_stream/test_AxiStreamConcat.py @@ -80,7 +80,4 @@ def test_AxiStreamConcat(parameters): toplevel="surf.axistreamconcatipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamConcatIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamDeMux.py b/tests/axi/axi_stream/test_AxiStreamDeMux.py index e30024a99c..9d46cfe145 100644 --- a/tests/axi/axi_stream/test_AxiStreamDeMux.py +++ b/tests/axi/axi_stream/test_AxiStreamDeMux.py @@ -364,7 +364,4 @@ def test_AxiStreamDeMux(case): toplevel="surf.axistreamdemuxipintegrator", parameters=case["parameters"], extra_env=extra_env, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamDeMuxIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamFlush.py b/tests/axi/axi_stream/test_AxiStreamFlush.py index c26edda82c..4a9cb012c3 100644 --- a/tests/axi/axi_stream/test_AxiStreamFlush.py +++ b/tests/axi/axi_stream/test_AxiStreamFlush.py @@ -108,7 +108,4 @@ def test_AxiStreamFlush(parameters): toplevel="surf.axistreamflushipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamFlushIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py b/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py index 76e71b8cb7..57f82f3220 100644 --- a/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py +++ b/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py @@ -298,11 +298,4 @@ def test_AxiStreamFrameBuffer(parameters): toplevel="surf.axistreamframebufferipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamFrameBufferIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py b/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py index 4455a550a3..95facc67e9 100644 --- a/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py +++ b/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py @@ -87,7 +87,4 @@ def test_AxiStreamFrameRateLimiter(parameters): toplevel="surf.axistreamframeratelimiteripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamFrameRateLimiterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamGearbox.py b/tests/axi/axi_stream/test_AxiStreamGearbox.py index 54c83810f2..f82d026955 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearbox.py +++ b/tests/axi/axi_stream/test_AxiStreamGearbox.py @@ -80,7 +80,4 @@ def test_AxiStreamGearbox(parameters): toplevel="surf.axistreamgearboxipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamGearboxIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamGearboxPack.py b/tests/axi/axi_stream/test_AxiStreamGearboxPack.py index 80ffa85c3d..554b538e6a 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearboxPack.py +++ b/tests/axi/axi_stream/test_AxiStreamGearboxPack.py @@ -75,7 +75,4 @@ def test_AxiStreamGearboxPack(parameters): toplevel="surf.axistreamgearboxpackipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamGearboxPackIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py b/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py index d2831c64db..96439d0c9c 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py +++ b/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py @@ -76,7 +76,4 @@ def test_AxiStreamGearboxUnpack(parameters): toplevel="surf.axistreamgearboxunpackipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamGearboxUnpackIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamMon.py b/tests/axi/axi_stream/test_AxiStreamMon.py index 8368a80497..0c5405f426 100644 --- a/tests/axi/axi_stream/test_AxiStreamMon.py +++ b/tests/axi/axi_stream/test_AxiStreamMon.py @@ -78,10 +78,4 @@ def test_AxiStreamMon(parameters): toplevel="surf.axistreammonipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamMonIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamMonAxiL.py b/tests/axi/axi_stream/test_AxiStreamMonAxiL.py index d6af03fcab..f9ca24e262 100644 --- a/tests/axi/axi_stream/test_AxiStreamMonAxiL.py +++ b/tests/axi/axi_stream/test_AxiStreamMonAxiL.py @@ -87,11 +87,4 @@ def test_AxiStreamMonAxiL(parameters): toplevel="surf.axistreammonaxilipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamMonAxiLIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamMux.py b/tests/axi/axi_stream/test_AxiStreamMux.py index 33f4d5c567..c57c67cfa8 100644 --- a/tests/axi/axi_stream/test_AxiStreamMux.py +++ b/tests/axi/axi_stream/test_AxiStreamMux.py @@ -451,7 +451,4 @@ def test_AxiStreamMux(case): toplevel="surf.axistreammuxipintegrator", parameters=case["parameters"], extra_env=extra_env, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamMuxIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamPipeline.py b/tests/axi/axi_stream/test_AxiStreamPipeline.py index 773184ef9a..96cbdf9887 100644 --- a/tests/axi/axi_stream/test_AxiStreamPipeline.py +++ b/tests/axi/axi_stream/test_AxiStreamPipeline.py @@ -347,7 +347,4 @@ def test_AxiStreamPipeline(parameters): toplevel="surf.axistreampipelineipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamPipelineIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py b/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py index 032c49c1f7..71a7c4b9c9 100644 --- a/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py +++ b/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py @@ -81,7 +81,4 @@ def test_AxiStreamPrbsFlowCtrl(parameters): toplevel="surf.axistreamprbsflowctrlipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamPrbsFlowCtrlIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamRepeater.py b/tests/axi/axi_stream/test_AxiStreamRepeater.py index 56cdfb383e..68f6b701b2 100644 --- a/tests/axi/axi_stream/test_AxiStreamRepeater.py +++ b/tests/axi/axi_stream/test_AxiStreamRepeater.py @@ -82,7 +82,4 @@ def test_AxiStreamRepeater(parameters): toplevel="surf.axistreamrepeateripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamRepeaterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamResize.py b/tests/axi/axi_stream/test_AxiStreamResize.py index 09ba9fc1cc..f5c2cd7feb 100644 --- a/tests/axi/axi_stream/test_AxiStreamResize.py +++ b/tests/axi/axi_stream/test_AxiStreamResize.py @@ -237,7 +237,4 @@ def test_AxiStreamResize(parameters): toplevel="surf.axistreamresizeipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamResizeIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamRingBuffer.py b/tests/axi/axi_stream/test_AxiStreamRingBuffer.py index 66fa11e503..2d1c96aa81 100644 --- a/tests/axi/axi_stream/test_AxiStreamRingBuffer.py +++ b/tests/axi/axi_stream/test_AxiStreamRingBuffer.py @@ -109,11 +109,4 @@ def test_AxiStreamRingBuffer(parameters): toplevel="surf.axistreamringbufferipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamRingBufferIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamScatterGather.py b/tests/axi/axi_stream/test_AxiStreamScatterGather.py index 6768ac38f6..606e1dca11 100644 --- a/tests/axi/axi_stream/test_AxiStreamScatterGather.py +++ b/tests/axi/axi_stream/test_AxiStreamScatterGather.py @@ -133,12 +133,4 @@ def test_AxiStreamScatterGather(parameters): toplevel="surf.axistreamscattergatheripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamScatterGatherIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamShift.py b/tests/axi/axi_stream/test_AxiStreamShift.py index b7c4575039..3ce2f27214 100644 --- a/tests/axi/axi_stream/test_AxiStreamShift.py +++ b/tests/axi/axi_stream/test_AxiStreamShift.py @@ -84,7 +84,4 @@ def test_AxiStreamShift(parameters): toplevel="surf.axistreamshiftipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamShiftIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamSplitter.py b/tests/axi/axi_stream/test_AxiStreamSplitter.py index ae114a2396..d1634f0a29 100644 --- a/tests/axi/axi_stream/test_AxiStreamSplitter.py +++ b/tests/axi/axi_stream/test_AxiStreamSplitter.py @@ -84,7 +84,4 @@ def test_AxiStreamSplitter(parameters): toplevel="surf.axistreamsplitteripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamSplitterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTap.py b/tests/axi/axi_stream/test_AxiStreamTap.py index 611055127c..41cd87142a 100644 --- a/tests/axi/axi_stream/test_AxiStreamTap.py +++ b/tests/axi/axi_stream/test_AxiStreamTap.py @@ -96,7 +96,4 @@ def test_AxiStreamTap(parameters): toplevel="surf.axistreamtapipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTapIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTimer.py b/tests/axi/axi_stream/test_AxiStreamTimer.py index 45fe2667c3..96837148b0 100644 --- a/tests/axi/axi_stream/test_AxiStreamTimer.py +++ b/tests/axi/axi_stream/test_AxiStreamTimer.py @@ -102,7 +102,4 @@ def test_AxiStreamTimer(parameters): toplevel="surf.axistreamtimeripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTimerIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py b/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py index 24dda10397..1e239455fa 100644 --- a/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py +++ b/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py @@ -76,7 +76,4 @@ def test_AxiStreamTrailerAppend(parameters): toplevel="surf.axistreamtrailerappendipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTrailerAppendIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py b/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py index 3551745fbd..98ba294264 100644 --- a/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py +++ b/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py @@ -72,7 +72,4 @@ def test_AxiStreamTrailerRemove(parameters): toplevel="surf.axistreamtrailerremoveipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTrailerRemoveIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/dma/test_AxiStreamDma.py b/tests/axi/dma/test_AxiStreamDma.py index 104ba133ae..ea51d9c727 100644 --- a/tests/axi/dma/test_AxiStreamDma.py +++ b/tests/axi/dma/test_AxiStreamDma.py @@ -89,7 +89,6 @@ def test_AxiStreamDma(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaIpIntegrator.vhd", ], }, diff --git a/tests/axi/dma/test_AxiStreamDmaFifo.py b/tests/axi/dma/test_AxiStreamDmaFifo.py index 0f5676beea..2d682ea706 100644 --- a/tests/axi/dma/test_AxiStreamDmaFifo.py +++ b/tests/axi/dma/test_AxiStreamDmaFifo.py @@ -84,7 +84,6 @@ def test_AxiStreamDmaFifo(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaFifoIpIntegrator.vhd", ], }, diff --git a/tests/axi/dma/test_AxiStreamDmaRingRead.py b/tests/axi/dma/test_AxiStreamDmaRingRead.py index 4df421fc04..3bea0551e3 100644 --- a/tests/axi/dma/test_AxiStreamDmaRingRead.py +++ b/tests/axi/dma/test_AxiStreamDmaRingRead.py @@ -136,9 +136,6 @@ def test_AxiStreamDmaRingRead(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/MasterAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaRingReadIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaRingWrite.py b/tests/axi/dma/test_AxiStreamDmaRingWrite.py index ceccee2033..0f5e83f340 100644 --- a/tests/axi/dma/test_AxiStreamDmaRingWrite.py +++ b/tests/axi/dma/test_AxiStreamDmaRingWrite.py @@ -139,9 +139,6 @@ def test_AxiStreamDmaRingWrite(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaRingWriteIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2.py b/tests/axi/dma/test_AxiStreamDmaV2.py index 951c655c04..db96860f3d 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2.py +++ b/tests/axi/dma/test_AxiStreamDmaV2.py @@ -98,7 +98,6 @@ def test_AxiStreamDmaV2(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2IpIntegrator.vhd", ], }, diff --git a/tests/axi/dma/test_AxiStreamDmaV2Desc.py b/tests/axi/dma/test_AxiStreamDmaV2Desc.py index eb1389e044..48fc0d47bb 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Desc.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Desc.py @@ -111,7 +111,6 @@ def test_AxiStreamDmaV2Desc(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2DescIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2Fifo.py b/tests/axi/dma/test_AxiStreamDmaV2Fifo.py index adf747a452..77cf57578f 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Fifo.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Fifo.py @@ -132,9 +132,6 @@ def test_AxiStreamDmaV2Fifo(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2FifoIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py b/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py index deee5ac728..4211a31acb 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py +++ b/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py @@ -112,9 +112,6 @@ def test_AxiStreamDmaV2FifoLoopback(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2FifoIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2Read.py b/tests/axi/dma/test_AxiStreamDmaV2Read.py index 12a33d0f1c..14cd29bb5b 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Read.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Read.py @@ -19,22 +19,14 @@ # forced internal state advance. import os -from pathlib import Path import cocotb import pytest -from cocotb_test.simulator import run from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer from cocotbext.axi import AxiRamRead, AxiReadBus, AxiStreamBus, AxiStreamSink -from tests.common.regression_utils import ( - COMMON_VHDL_COMPILE_ARGS, - TESTS_ROOT, - build_vhdl_sources, - merge_vhdl_sources, - cocotb_module_name_from_test_file, -) +from tests.common.regression_utils import run_surf_vhdl_test class TB: @@ -150,27 +142,15 @@ async def single_descriptor_read_test(dut): ), ], ) -def test_AxiStreamDmaV2Read(case_env, request): - test_file = Path(__file__) - rel_parent = test_file.resolve().relative_to(TESTS_ROOT).parent - - run( - module=cocotb_module_name_from_test_file(test_file), +def test_AxiStreamDmaV2Read(case_env): + run_surf_vhdl_test( + test_file=__file__, toplevel="surf.axistreamdmav2readipintegrator", - toplevel_lang="vhdl", - vhdl_sources=merge_vhdl_sources( - build_vhdl_sources(), - { - "surf": [ - "axi/dma/rtl/v2/AxiStreamDmaV2Read.vhd", - "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", - "axi/dma/ip_integrator/AxiStreamDmaV2ReadIpIntegrator.vhd", - ], - }, - ), - parameters={}, - sim_build=str((TESTS_ROOT / "sim_build" / rel_parent / f"{test_file.stem}.{request.node.callspec.id}")), - extra_env={key: str(value) for key, value in case_env.items()}, - simulator="ghdl", - vhdl_compile_args=COMMON_VHDL_COMPILE_ARGS, + extra_env=case_env, + extra_vhdl_sources={ + "surf": [ + "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", + "axi/dma/ip_integrator/AxiStreamDmaV2ReadIpIntegrator.vhd", + ], + }, ) diff --git a/tests/axi/dma/test_AxiStreamDmaWrite.py b/tests/axi/dma/test_AxiStreamDmaWrite.py index 16dc78d950..ad23610a43 100644 --- a/tests/axi/dma/test_AxiStreamDmaWrite.py +++ b/tests/axi/dma/test_AxiStreamDmaWrite.py @@ -96,7 +96,6 @@ def test_AxiStreamDmaWrite(parameters): extra_vhdl_sources={ "surf": [ "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaWriteIpIntegrator.vhd", ], }, diff --git a/tests/common/README.md b/tests/common/README.md index 43f35c0278..c7496db71c 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -42,6 +42,13 @@ build-manifest entry or repeat an imported design unit. Compiling the same unit from both paths can redefine it, make compile order significant, or leave a cached build using a different source than the reviewer expects. +The shared source merge rejects an extra path that resolves to a file already +present in the same library. It also rejects a different extra file that +redeclares an imported entity, package, or configuration in that library, so +variable-generated lists cannot bypass the literal-source audit. There is no +override: give a test-only unit a distinct name, or fix the ruckus/source-list +boundary instead of relying on compile order. + The default build path includes `parameters` and `extra_env`, and the shared runner hashes path components that would be unsafe or excessively long. Use `sim_build_key` only when a subsystem requires a deliberately stable or more diff --git a/tests/common/compliance_baseline.json b/tests/common/compliance_baseline.json index 14aa90a60c..3561dfb4a9 100644 --- a/tests/common/compliance_baseline.json +++ b/tests/common/compliance_baseline.json @@ -1,80 +1,7 @@ { "rules": { - "direct-runner": { - "tests/axi/dma/test_AxiStreamDmaV2Read.py": 1 - }, - "duplicate-imported-source": { - "tests/axi/axi4/test_AxiMemTester.py": 1, - "tests/axi/axi4/test_AxiMonAxiL.py": 1, - "tests/axi/axi4/test_AxiRateGen.py": 1, - "tests/axi/axi4/test_AxiRingBuffer.py": 1, - "tests/axi/axi_lite/test_AxiLiteAsync.py": 1, - "tests/axi/axi_lite/test_AxiLiteCrossbar.py": 1, - "tests/axi/axi_lite/test_AxiLiteFifoPop.py": 1, - "tests/axi/axi_lite/test_AxiLiteFifoPush.py": 1, - "tests/axi/axi_lite/test_AxiLiteFifoPushPop.py": 1, - "tests/axi/axi_lite/test_AxiLiteMaster.py": 1, - "tests/axi/axi_lite/test_AxiLiteMasterProxy.py": 2, - "tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py": 1, - "tests/axi/axi_lite/test_AxiLiteRegs.py": 1, - "tests/axi/axi_lite/test_AxiLiteRespTimer.py": 1, - "tests/axi/axi_lite/test_AxiLiteRingBuffer.py": 1, - "tests/axi/axi_lite/test_AxiLiteSequencerRam.py": 2, - "tests/axi/axi_lite/test_AxiLiteSlave.py": 1, - "tests/axi/axi_lite/test_AxiLiteWriteFilter.py": 1, - "tests/axi/axi_stream/test_AxiStreamBatchingFifo.py": 4, - "tests/axi/axi_stream/test_AxiStreamCombiner.py": 1, - "tests/axi/axi_stream/test_AxiStreamCompact.py": 1, - "tests/axi/axi_stream/test_AxiStreamConcat.py": 1, - "tests/axi/axi_stream/test_AxiStreamDeMux.py": 1, - "tests/axi/axi_stream/test_AxiStreamFlush.py": 1, - "tests/axi/axi_stream/test_AxiStreamFrameBuffer.py": 3, - "tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py": 1, - "tests/axi/axi_stream/test_AxiStreamGearbox.py": 1, - "tests/axi/axi_stream/test_AxiStreamGearboxPack.py": 1, - "tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py": 1, - "tests/axi/axi_stream/test_AxiStreamMon.py": 2, - "tests/axi/axi_stream/test_AxiStreamMonAxiL.py": 3, - "tests/axi/axi_stream/test_AxiStreamMux.py": 1, - "tests/axi/axi_stream/test_AxiStreamPipeline.py": 1, - "tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py": 1, - "tests/axi/axi_stream/test_AxiStreamRepeater.py": 1, - "tests/axi/axi_stream/test_AxiStreamResize.py": 1, - "tests/axi/axi_stream/test_AxiStreamRingBuffer.py": 3, - "tests/axi/axi_stream/test_AxiStreamScatterGather.py": 4, - "tests/axi/axi_stream/test_AxiStreamShift.py": 1, - "tests/axi/axi_stream/test_AxiStreamSplitter.py": 1, - "tests/axi/axi_stream/test_AxiStreamTap.py": 1, - "tests/axi/axi_stream/test_AxiStreamTimer.py": 1, - "tests/axi/axi_stream/test_AxiStreamTrailerAppend.py": 1, - "tests/axi/axi_stream/test_AxiStreamTrailerRemove.py": 1, - "tests/axi/dma/test_AxiStreamDma.py": 1, - "tests/axi/dma/test_AxiStreamDmaFifo.py": 1, - "tests/axi/dma/test_AxiStreamDmaRingRead.py": 3, - "tests/axi/dma/test_AxiStreamDmaRingWrite.py": 3, - "tests/axi/dma/test_AxiStreamDmaV2.py": 1, - "tests/axi/dma/test_AxiStreamDmaV2Desc.py": 1, - "tests/axi/dma/test_AxiStreamDmaV2Fifo.py": 3, - "tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py": 3, - "tests/axi/dma/test_AxiStreamDmaWrite.py": 1, - "tests/dsp/generic/test_BoxcarIntegrator.py": 1, - "tests/dsp/generic/test_FirFilterMultiChannel.py": 6, - "tests/dsp/generic/test_FirFilterSingleChannel.py": 3, - "tests/dsp/generic/test_FirFilterSingleChannelLowPass.py": 3, - "tests/dsp/generic/test_FirFilterSingleChannelTiming.py": 3, - "tests/dsp/generic/test_FirFilterTap.py": 1, - "tests/protocols/batcher/test_AxiStreamBatcherAxil.py": 1, - "tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py": 1, - "tests/protocols/rssi/test_RssiAxiLiteRegItf.py": 2, - "tests/protocols/rssi/test_RssiConnFsm.py": 2, - "tests/protocols/rssi/test_RssiCore.py": 6, - "tests/protocols/rssi/test_RssiCoreWrapper.py": 2, - "tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py": 3, - "tests/protocols/rssi/test_RssiHeaderReg.py": 1, - "tests/protocols/rssi/test_RssiMonitor.py": 2, - "tests/protocols/rssi/test_RssiRxFsm.py": 4, - "tests/protocols/rssi/test_RssiTxFsm.py": 2 - }, + "direct-runner": {}, + "duplicate-imported-source": {}, "early-bare-return": {}, "missing-methodology": { "tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py": 1, diff --git a/tests/common/regression_utils.py b/tests/common/regression_utils.py index 5dcb9bd81b..8d2aa9a575 100644 --- a/tests/common/regression_utils.py +++ b/tests/common/regression_utils.py @@ -37,6 +37,14 @@ OPTIONAL_GHDL_WARNINGS = ("elaboration", "hide", "specs") +PRIMARY_VHDL_UNIT_RE = re.compile( + r"(?im)^(?:" + r"entity\s+(?P[a-z][a-z0-9_]*)\s+is\b|" + r"package\s+(?!body\b)(?P[a-z][a-z0-9_]*)\s+is\b|" + r"configuration\s+(?P[a-z][a-z0-9_]*)\s+of\b" + r")" +) + @lru_cache(maxsize=1) def _supported_ghdl_warning_names() -> frozenset[str]: @@ -207,6 +215,35 @@ def build_vhdl_sources() -> dict[str, list[str]]: } +def _resolved_source_path(path: str | Path) -> Path: + source = Path(path) + if not source.is_absolute(): + source = REPO_ROOT / source + return source.resolve() + + +@lru_cache(maxsize=None) +def _primary_vhdl_units(path: Path) -> frozenset[str]: + if path.suffix.lower() not in {".vhd", ".vhdl"}: + return frozenset() + + try: + source = path.read_text(encoding="utf-8") + except (OSError, UnicodeError): + return frozenset() + + source_without_comments = "\n".join( + line.split("--", 1)[0] + for line in source.splitlines() + ) + return frozenset( + name.lower() + for match in PRIMARY_VHDL_UNIT_RE.finditer(source_without_comments) + for name in match.groupdict().values() + if name is not None + ) + + def merge_vhdl_sources( base_sources: dict[str, list[str]], extra_sources: dict[str, list[str]] | None, @@ -215,11 +252,46 @@ def merge_vhdl_sources( return base_sources merged = {library: list(paths) for library, paths in base_sources.items()} + resolved_by_library = { + library: {_resolved_source_path(path) for path in paths} + for library, paths in base_sources.items() + } + units_by_library = { + library: { + unit: _resolved_source_path(path) + for path in paths + for unit in _primary_vhdl_units(_resolved_source_path(path)) + } + for library, paths in base_sources.items() + } + for library, paths in extra_sources.items(): merged.setdefault(library, []) + resolved_by_library.setdefault(library, set()) + units_by_library.setdefault(library, {}) # Append test-local sources after the imported SURF library so wrappers # can instantiate the real RTL that was already compiled above. - merged[library].extend(str(Path(path)) for path in paths) + for path in paths: + resolved = _resolved_source_path(path) + if resolved in resolved_by_library[library]: + raise ValueError( + f"Extra VHDL source {path} duplicates {resolved} " + f"already present in library {library}" + ) + + units = _primary_vhdl_units(resolved) + duplicate_units = sorted(units & units_by_library[library].keys()) + if duplicate_units: + unit = duplicate_units[0] + previous = units_by_library[library][unit] + raise ValueError( + f"Extra VHDL source {path} declares {unit}, already declared " + f"by {previous} in library {library}" + ) + + merged[library].append(str(Path(path))) + resolved_by_library[library].add(resolved) + units_by_library[library].update({unit: resolved for unit in units}) return merged diff --git a/tests/common/test_regression_utils.py b/tests/common/test_regression_utils.py index 434e15fed2..b0decf2908 100644 --- a/tests/common/test_regression_utils.py +++ b/tests/common/test_regression_utils.py @@ -16,6 +16,7 @@ cocotb_filtered_env, cocotb_test_filter, cocotb_test_filter_excluding, + merge_vhdl_sources, ) @@ -73,3 +74,59 @@ def test_cocotb_filtered_env_rejects_conflicting_external_selectors(monkeypatch) with pytest.raises(ValueError, match="Specify only one"): cocotb_filtered_env({}, "default_.*") + + +def test_merge_vhdl_sources_rejects_same_resolved_file(tmp_path): + source = tmp_path / "Imported.vhd" + source.write_text("entity Imported is end entity;\n", encoding="utf-8") + alias = tmp_path / "Alias.vhd" + alias.symlink_to(source) + + with pytest.raises(ValueError, match="duplicates.*already present"): + merge_vhdl_sources( + {"surf": [str(source)]}, + {"surf": [str(alias)]}, + ) + + +def test_merge_vhdl_sources_rejects_duplicate_primary_unit(tmp_path): + imported = tmp_path / "Imported.vhd" + imported.write_text("entity SharedUnit is end entity;\n", encoding="utf-8") + extra = tmp_path / "Extra.vhd" + extra.write_text( + "-- entity CommentOnly is end entity;\n" + "entity SharedUnit is end entity;\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="declares sharedunit, already declared"): + merge_vhdl_sources( + {"surf": [str(imported)]}, + {"surf": [str(extra)]}, + ) + + +def test_merge_vhdl_sources_keeps_unique_units_and_library_boundaries(tmp_path): + imported = tmp_path / "Imported.vhd" + imported.write_text( + "package SharedPkg is end package;\n" + "entity Imported is end entity;\n", + encoding="utf-8", + ) + extra = tmp_path / "Extra.vhd" + extra.write_text("entity Extra is end entity;\n", encoding="utf-8") + other_library = tmp_path / "OtherLibrary.vhd" + other_library.write_text("entity Imported is end entity;\n", encoding="utf-8") + + merged = merge_vhdl_sources( + {"surf": [str(imported)]}, + { + "surf": [str(extra)], + "testlib": [str(other_library)], + }, + ) + + assert merged == { + "surf": [str(imported), str(extra)], + "testlib": [str(other_library)], + } diff --git a/tests/dsp/generic/test_BoxcarIntegrator.py b/tests/dsp/generic/test_BoxcarIntegrator.py index 190227bb6b..bb1b345ad6 100644 --- a/tests/dsp/generic/test_BoxcarIntegrator.py +++ b/tests/dsp/generic/test_BoxcarIntegrator.py @@ -211,5 +211,4 @@ def test_BoxcarIntegrator(parameters): toplevel="surf.boxcarintegrator", parameters=hdl_parameters_from(parameters), extra_env=parameters, - extra_vhdl_sources={"surf": ["dsp/generic/fixed/BoxcarIntegrator.vhd"]}, ) diff --git a/tests/dsp/generic/test_FirFilterMultiChannel.py b/tests/dsp/generic/test_FirFilterMultiChannel.py index f1ecb11676..e3d8aab016 100644 --- a/tests/dsp/generic/test_FirFilterMultiChannel.py +++ b/tests/dsp/generic/test_FirFilterMultiChannel.py @@ -195,12 +195,6 @@ def test_FirFilterMultiChannel(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", - "dsp/generic/fixed/FirFilterMultiChannel.vhd", str(parameters["WRAPPER_PATH"]), ] }, diff --git a/tests/dsp/generic/test_FirFilterSingleChannel.py b/tests/dsp/generic/test_FirFilterSingleChannel.py index 9081cb3281..8b7f9dd7ae 100644 --- a/tests/dsp/generic/test_FirFilterSingleChannel.py +++ b/tests/dsp/generic/test_FirFilterSingleChannel.py @@ -192,9 +192,6 @@ def test_FirFilterSingleChannel(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", "dsp/generic/wrappers/FirFilterSingleChannelTestWrapper.vhd", ] }, diff --git a/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py b/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py index 1132d9ac97..47a615d0fc 100644 --- a/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py +++ b/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py @@ -195,9 +195,6 @@ def test_FirFilterSingleChannelLowPass(): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", "dsp/generic/wrappers/FirFilterSingleChannelWrapper.vhd", ] }, diff --git a/tests/dsp/generic/test_FirFilterSingleChannelTiming.py b/tests/dsp/generic/test_FirFilterSingleChannelTiming.py index 67060e2399..479cfbdd6a 100644 --- a/tests/dsp/generic/test_FirFilterSingleChannelTiming.py +++ b/tests/dsp/generic/test_FirFilterSingleChannelTiming.py @@ -261,9 +261,6 @@ def test_FirFilterSingleChannelTiming(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", "dsp/generic/wrappers/FirFilterSingleChannelWrapper.vhd", ] }, diff --git a/tests/dsp/generic/test_FirFilterTap.py b/tests/dsp/generic/test_FirFilterTap.py index d1684bcf2e..23903b3abd 100644 --- a/tests/dsp/generic/test_FirFilterTap.py +++ b/tests/dsp/generic/test_FirFilterTap.py @@ -134,7 +134,6 @@ def test_FirFilterTap(parameters): }, extra_vhdl_sources={ "surf": [ - "dsp/generic/fixed/FirFilterTap.vhd", "dsp/generic/wrappers/FirFilterTapTestWrapper.vhd", ] }, diff --git a/tests/protocols/batcher/test_AxiStreamBatcherAxil.py b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py index 92587dd30a..f863708fc6 100644 --- a/tests/protocols/batcher/test_AxiStreamBatcherAxil.py +++ b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py @@ -334,7 +334,6 @@ def test_AxiStreamBatcherAxil(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd", ], }, diff --git a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py index 24908f8062..f20b47941a 100644 --- a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py +++ b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py @@ -674,7 +674,6 @@ def test_AxiStreamBatcherEventBuilder(parameters): extra_env=extra_env, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd", ], }, diff --git a/tests/protocols/line_codes/line_code_test_utils.py b/tests/protocols/line_codes/line_code_test_utils.py index a47b111134..05dc46d055 100644 --- a/tests/protocols/line_codes/line_code_test_utils.py +++ b/tests/protocols/line_codes/line_code_test_utils.py @@ -100,7 +100,6 @@ def run_line_code_integration_test( *, test_file: str, toplevel: str, - tb_source: str, parameters: dict[str, object], ) -> None: run_surf_vhdl_test( @@ -108,7 +107,6 @@ def run_line_code_integration_test( toplevel=toplevel, parameters=hdl_parameters_from(parameters), extra_env=parameters, - extra_vhdl_sources={"surf": [tb_source]}, ) diff --git a/tests/protocols/line_codes/test_LineCode10b12bIntegration.py b/tests/protocols/line_codes/test_LineCode10b12bIntegration.py index c4bcfd8a91..fd4f4f6730 100644 --- a/tests/protocols/line_codes/test_LineCode10b12bIntegration.py +++ b/tests/protocols/line_codes/test_LineCode10b12bIntegration.py @@ -47,6 +47,5 @@ def test_LineCode10b12bIntegration(parameters): run_line_code_integration_test( test_file=__file__, toplevel="surf.linecode10b12btb", - tb_source="protocols/line-codes/tb/LineCode10b12bTb.vhd", parameters=parameters, ) diff --git a/tests/protocols/line_codes/test_LineCode12b14bIntegration.py b/tests/protocols/line_codes/test_LineCode12b14bIntegration.py index 6ab523f55d..233bd744d1 100644 --- a/tests/protocols/line_codes/test_LineCode12b14bIntegration.py +++ b/tests/protocols/line_codes/test_LineCode12b14bIntegration.py @@ -50,6 +50,5 @@ def test_LineCode12b14bIntegration(parameters): run_line_code_integration_test( test_file=__file__, toplevel="surf.linecode12b14btb", - tb_source="protocols/line-codes/tb/LineCode12b14bTb.vhd", parameters=parameters, ) diff --git a/tests/protocols/line_codes/test_LineCode8b10bIntegration.py b/tests/protocols/line_codes/test_LineCode8b10bIntegration.py index e10eeb64e0..8bb08447f6 100644 --- a/tests/protocols/line_codes/test_LineCode8b10bIntegration.py +++ b/tests/protocols/line_codes/test_LineCode8b10bIntegration.py @@ -59,6 +59,5 @@ def test_LineCode8b10bIntegration(parameters): run_line_code_integration_test( test_file=__file__, toplevel="surf.linecode8b10btb", - tb_source="protocols/line-codes/tb/LineCode8b10bTb.vhd", parameters=parameters, ) diff --git a/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py b/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py index c0834d3201..97fe8e0cb4 100644 --- a/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py +++ b/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py @@ -50,6 +50,5 @@ def test_Pgp4TxLite(parameters): run_pgp_wrapper_test( test_file=__file__, toplevel="surf.pgp4txlitewrapper", - wrapper_source="protocols/pgp/pgp4/core/rtl/Pgp4TxLiteWrapper.vhd", extra_env=parameters, ) diff --git a/tests/protocols/pgp/pgp_test_utils.py b/tests/protocols/pgp/pgp_test_utils.py index 3e4ee49b86..37bf3f0f38 100644 --- a/tests/protocols/pgp/pgp_test_utils.py +++ b/tests/protocols/pgp/pgp_test_utils.py @@ -111,12 +111,12 @@ def run_pgp_wrapper_test( *, test_file: str, toplevel: str, - wrapper_source: str, + wrapper_source: str | None = None, parameters: dict[str, object] | None = None, extra_env: dict[str, str] | None = None, extra_sources: list[str] | None = None, ) -> None: - surf_sources = [wrapper_source] + surf_sources = [] if wrapper_source is None else [wrapper_source] if extra_sources is not None: surf_sources.extend(extra_sources) @@ -125,7 +125,7 @@ def run_pgp_wrapper_test( toplevel=toplevel, parameters={} if parameters is None else parameters, extra_env=extra_env, - extra_vhdl_sources={"surf": surf_sources}, + extra_vhdl_sources={"surf": surf_sources} if surf_sources else None, ) diff --git a/tests/protocols/rssi/rssi_test_utils.py b/tests/protocols/rssi/rssi_test_utils.py index d69bfd5948..7e183acd23 100644 --- a/tests/protocols/rssi/rssi_test_utils.py +++ b/tests/protocols/rssi/rssi_test_utils.py @@ -26,19 +26,6 @@ RSSI_VERSION = 0x1 -RSSI_CORE_VHDL_SOURCES = [ - "protocols/rssi/v1/rtl/RssiConnFsm.vhd", - "protocols/rssi/v1/rtl/RssiMonitor.vhd", - "protocols/rssi/v1/rtl/RssiRxFsm.vhd", - "protocols/rssi/v1/rtl/RssiTxFsm.vhd", - "protocols/rssi/v1/rtl/RssiCore.vhd", -] - -RSSI_CORE_WRAPPER_VHDL_SOURCES = RSSI_CORE_VHDL_SOURCES + [ - "protocols/rssi/v1/rtl/RssiCoreWrapper.vhd", -] - - @dataclass(frozen=True) class RssiParams: # Defaults are ordinary valid negotiation values, not reset values. Tests diff --git a/tests/protocols/rssi/test_RssiAxiLiteRegItf.py b/tests/protocols/rssi/test_RssiAxiLiteRegItf.py index 298f232f57..5d70a7589b 100644 --- a/tests/protocols/rssi/test_RssiAxiLiteRegItf.py +++ b/tests/protocols/rssi/test_RssiAxiLiteRegItf.py @@ -333,11 +333,4 @@ def test_RssiAxiLiteRegItf(parameters): test_file=__file__, toplevel="surf.rssiaxiliteregitfwrapper", parameters=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiAxiLiteRegItf.vhd", - "protocols/rssi/v1/wrappers/RssiAxiLiteRegItfWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiConnFsm.py b/tests/protocols/rssi/test_RssiConnFsm.py index f8cafed2cb..a27ffc42d5 100644 --- a/tests/protocols/rssi/test_RssiConnFsm.py +++ b/tests/protocols/rssi/test_RssiConnFsm.py @@ -403,11 +403,4 @@ def test_RssiConnFsm(parameters): toplevel="surf.rssiconnfsmwrapper", parameters=parameters, extra_env=cocotb_filtered_env(parameters, rf"{role}_.*_test$"), - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiConnFsm.vhd", - "protocols/rssi/v1/wrappers/RssiConnFsmWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiCore.py b/tests/protocols/rssi/test_RssiCore.py index c624857561..357b1f7243 100644 --- a/tests/protocols/rssi/test_RssiCore.py +++ b/tests/protocols/rssi/test_RssiCore.py @@ -60,7 +60,6 @@ run_surf_vhdl_test, ) from tests.protocols.rssi.rssi_test_utils import ( - RSSI_CORE_VHDL_SOURCES, RSSI_FLAG_ACK, RSSI_FLAG_NULL, RSSI_FLAG_RST, @@ -1280,13 +1279,6 @@ def test_RssiCore(parameters): toplevel="surf.rssicoreintegrationwrapper", parameters=parameters, extra_env=_default_extra_env(parameters), - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1313,13 +1305,6 @@ def test_RssiCore_sequence_wraparound(): "COCOTB_TESTCASE": "client_sequence_wraparound_delivers_frame_test", "RSSI_SEQUENCE_WRAP_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1345,13 +1330,6 @@ def test_RssiCore_repeated_data_loss(): "COCOTB_TESTCASE": "bidirectional_data_losses_recover_without_duplicate_delivery_test", "RSSI_REPEATED_LOSS_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1377,13 +1355,6 @@ def test_RssiCore_out_of_order_recovery(): "COCOTB_TESTCASE": "lost_first_data_drops_later_data_until_retransmit_test", "RSSI_OUT_OF_ORDER_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1409,13 +1380,6 @@ def test_RssiCore_axil_control_path(): "COCOTB_TESTCASE": "client_axil_control_path_opens_injects_reads_and_closes_test", "RSSI_AXIL_CONTROL_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1442,11 +1406,4 @@ def test_RssiCore_checksum_disabled(): "COCOTB_TESTCASE": "checksum_disabled_connection_and_payload_test", "RSSI_CHECKSUM_DISABLED_CORE_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiCoreWrapper.py b/tests/protocols/rssi/test_RssiCoreWrapper.py index 13acb7938e..9bda302c2a 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapper.py +++ b/tests/protocols/rssi/test_RssiCoreWrapper.py @@ -50,7 +50,6 @@ env_flag, run_surf_vhdl_test, ) -from tests.protocols.rssi.rssi_test_utils import RSSI_CORE_WRAPPER_VHDL_SOURCES from tests.protocols.ssi.ssi_test_utils import ( FlatSsiEndpoint, SsiBeat, @@ -296,13 +295,6 @@ def test_RssiCoreWrapper(parameters): "wrapper_server_backpressure_advertises_busy_test" ), ), - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -323,11 +315,4 @@ def test_RssiCoreWrapper_backpressure(): "COCOTB_TESTCASE": "wrapper_server_backpressure_advertises_busy_test", "RSSI_WRAPPER_BACKPRESSURE_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py index bb9fc23d31..8b87e209c8 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py +++ b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py @@ -48,7 +48,6 @@ from tests.common.regression_utils import env_flag, run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import ( - RSSI_CORE_WRAPPER_VHDL_SOURCES, format_transport_frame, parse_header, protocol_bytes_from_stream_word, @@ -517,13 +516,6 @@ def test_RssiCoreWrapperMultiStream(parameters): toplevel="surf.rssicorewrappermultistreamintegrationwrapper", parameters=parameters, extra_env=_default_extra_env(parameters), - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperMultiStreamIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -542,13 +534,6 @@ def test_RssiCoreWrapperMultiStream_extended(parameters): **parameters, "RUN_RSSI_EXTENDED_TESTS": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperMultiStreamIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -585,11 +570,4 @@ def test_RssiCoreWrapperMultiStream_bidirectional_packetizer2(request): **parameters, "COCOTB_TESTCASE": "multi_stream_bidirectional_payload_routes_test", }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperMultiStreamIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiHeaderReg.py b/tests/protocols/rssi/test_RssiHeaderReg.py index dec46284b8..b6a588c44a 100644 --- a/tests/protocols/rssi/test_RssiHeaderReg.py +++ b/tests/protocols/rssi/test_RssiHeaderReg.py @@ -267,5 +267,4 @@ def test_RssiHeaderReg(parameters): toplevel="surf.rssiheaderregwrapper", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["protocols/rssi/v1/wrappers/RssiHeaderRegWrapper.vhd"]}, ) diff --git a/tests/protocols/rssi/test_RssiMonitor.py b/tests/protocols/rssi/test_RssiMonitor.py index 3ca7b1721d..e2e78631a7 100644 --- a/tests/protocols/rssi/test_RssiMonitor.py +++ b/tests/protocols/rssi/test_RssiMonitor.py @@ -230,11 +230,4 @@ def test_RssiMonitor(parameters): toplevel="surf.rssimonitorwrapper", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiMonitor.vhd", - "protocols/rssi/v1/wrappers/RssiMonitorWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiRxFsm.py b/tests/protocols/rssi/test_RssiRxFsm.py index 8c947d5bd8..f16309b0a0 100644 --- a/tests/protocols/rssi/test_RssiRxFsm.py +++ b/tests/protocols/rssi/test_RssiRxFsm.py @@ -588,13 +588,6 @@ def test_RssiRxFsm(parameters): "checksum_disabled_accepts_data_when_checksum_status_is_bad_test" ), ), - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiRxFsm.vhd", - "protocols/rssi/v1/wrappers/RssiRxFsmWrapper.vhd", - ], - }, - force_compile=True, ) @@ -610,11 +603,4 @@ def test_RssiRxFsm_checksum_disabled(): "COCOTB_TESTCASE": "checksum_disabled_accepts_data_when_checksum_status_is_bad_test", "RSSI_CHECKSUM_DISABLED_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiRxFsm.vhd", - "protocols/rssi/v1/wrappers/RssiRxFsmWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiTxFsm.py b/tests/protocols/rssi/test_RssiTxFsm.py index afdd36084a..21aef9f41d 100644 --- a/tests/protocols/rssi/test_RssiTxFsm.py +++ b/tests/protocols/rssi/test_RssiTxFsm.py @@ -823,11 +823,4 @@ def test_RssiTxFsm(parameters): toplevel="surf.rssitxfsmwrapper", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiTxFsm.vhd", - "protocols/rssi/v1/wrappers/RssiTxFsmWrapper.vhd", - ], - }, - force_compile=True, ) From ec38e64e204ec2208f575bedf0134a9f9d43e136 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 10:11:46 -0700 Subject: [PATCH 39/57] test: enforce coroutine ownership and bounded waits --- .../regression-test-compliance/README.md | 83 +++++++++ tests/README.md | 4 +- tests/axi/axi4/test_AxiRateGen.py | 21 ++- tests/axi/axi4/test_AxiReadPathMux.py | 4 +- tests/axi/axi4/test_AxiResize.py | 9 +- tests/axi/axi4/test_AxiWritePathMux.py | 4 +- tests/axi/axi_lite/test_AxiLiteAsync.py | 11 +- tests/axi/axi_lite/test_AxiLiteMaster.py | 9 +- tests/axi/axi_lite/test_AxiLiteWriteFilter.py | 4 +- tests/axi/axi_stream/test_AxiStreamCompact.py | 11 +- .../test_AxiStreamFifoV2IpIntegrator.py | 55 +++--- .../axi/axi_stream/test_AxiStreamPipeline.py | 3 +- tests/axi/axi_stream/test_AxiStreamResize.py | 4 +- tests/axi/bridge/test_AxiLiteToIpBus.py | 4 +- tests/axi/bridge/test_IpBusToAxiLite.py | 6 +- tests/axi/dma/test_AxiStreamDmaV2Write.py | 9 +- .../dma/test_AxiStreamDmaV2WriteContinue.py | 9 +- tests/axi/dma/test_AxiStreamDmaV2WriteMux.py | 4 +- tests/base/fifo/test_FifoOutputPipeline.py | 5 +- tests/common/README.md | 12 +- tests/common/compliance_audit.py | 11 +- tests/common/compliance_baseline.json | 4 +- tests/common/regression_utils.py | 5 +- tests/common/test_compliance_audit.py | 26 +++ .../RoCEv2/test_RoCEv2AxiStreamRdmaCore.py | 161 +++++++++++------- tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py | 17 +- .../ethernet/RoCEv2/test_RoceConfigurator.py | 10 +- .../ethernet/RoCEv2/test_RoceResizeAndSwap.py | 4 +- .../protocols/coaxpress/test_CoaXPressCore.py | 2 + tests/protocols/coaxpress/test_CoaXPressRx.py | 1 + .../coaxpress/test_CoaXPressRxHsFsm.py | 7 +- .../coaxpress/test_CoaXPressTxLsFsm.py | 7 +- .../jesd204b/test_Jesd204bLoopback.py | 83 +++++---- .../pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py | 6 +- tests/protocols/pgp/pgp4/test_Pgp4RxEb.py | 16 +- .../pgp/shared/vc_fifo_test_utils.py | 14 +- tests/protocols/rssi/test_RssiCore.py | 1 + .../rssi/test_RssiCoreWrapperMultiStream.py | 1 + tests/protocols/srp/srp_test_utils.py | 11 +- tests/protocols/srp/test_SrpV0AxiLite.py | 19 ++- tests/protocols/srp/test_SrpV3AxiLite.py | 8 +- tests/protocols/ssi/test_SsiAxiLiteMaster.py | 8 +- tests/protocols/ssi/test_SsiResizeFifoEofe.py | 5 +- .../common/simlink_multi_instance_cocotb.py | 20 ++- .../native/simlink_stream_overload_probe.py | 9 +- .../general/gt_rx_align_check_test_utils.py | 12 +- 46 files changed, 515 insertions(+), 224 deletions(-) diff --git a/docs/plans/regression-test-compliance/README.md b/docs/plans/regression-test-compliance/README.md index 46180fa1f6..d0ca336fc1 100644 --- a/docs/plans/regression-test-compliance/README.md +++ b/docs/plans/regression-test-compliance/README.md @@ -115,6 +115,89 @@ VCS SimLink use is retained because that helper deliberately pre-analyzes its mixed-language topology before asking cocotb-test to elaborate without source arguments. +Phase 3 has completed its first ownership batch. All 25 unretained non-clock +task findings in `tests/axi/` were lifetime monitors or protocol responders; +the owning benches now retain and name them, while the existing finite AXI +transaction tasks remain awaited. `start_lockstep_clocks()` now returns its +lifetime task so benches can retain that ownership as well. The common helper +suite passes all 84 tests, a representative 14-node serial AXI run passes, and +the full 117-node AXI suite passes under pytest-xdist. No pytest node, cocotb +entrypoint, parameter ID, selector, or gate changed. + +The next Phase 3 batch removed 12 more unretained-task findings across the base +FIFO, SSI, Xilinx GT alignment, CoaXPress low-speed TX, and PGP4 elastic-buffer +suites. The two CoaXPress stimulus coroutines are finite and are now awaited; +the remaining responders, source models, monitors, and collectors are named +lifetime agents retained by their owning model. All 14 affected pytest nodes +pass serially in 95.17 seconds and repeat under pytest-xdist in 6.22 seconds. + +The RoCEv2 lifecycle batch removed another 16 findings. Finite RDMA packet +producers are awaited, long-running engine/source peers are retained and +cancelled in `finally`, and the resize sideband monitor is bench-owned. This is +especially important on RDMA watchdog failures, which now clean up both the +engine and any incomplete producer before propagating the timeout. All five +affected pytest nodes pass serially in 149.54 seconds and under pytest-xdist in +96.92 seconds. + +The JESD204B loopback cleanup removed the final seven unretained-task findings. +GT-forwarding agents now travel with both their stop event and task handle; +every link restart signals and joins the old agent before a replacement starts, +while preserving the original two-cycle handoff margin. All nine loopback +pytest nodes pass serially in 156.58 seconds and under pytest-xdist in 21.40 +seconds. The repository unretained-task screening count is now zero. Phase 3 +continues with bounded-wait and external-resource review before that structural +result is promoted to a blocking ratchet. + +The open-loop audit now recognizes only functions whose docstring explicitly +contains `Lifetime agent:`. Forty-four monitor, responder, clock, source, +transport, and protocol-peer loops have been reviewed and annotated with that +ownership contract. This reduced the ambiguous open-loop report from 66 to 22 +finite operations, without allowlisting paths or function names. The remaining +22 are being converted to direct cycle/time bounds; the marker is not permitted +as a substitute for bounding a transaction. + +Eight AXI finite loops now use `wait_sampled_ready()` with its 1024-cycle limit +or an equivalent explicit IPbus acknowledgement limit. This removes ad hoc +ready/valid polling from the compact and FIFO integration benches while keeping +the accepting edge semantics unchanged. All 15 affected nodes pass both +serially and under pytest-xdist; 14 finite open-loop findings remain. + +Six more RoCEv2, CoaXPress, and PGP waits are bounded. DMA drains now fail with +the number of beats received after a conservative 65,536-cycle deadline; the +stream-facing cases use the shared 1024-cycle accepted-handshake primitive or +an equivalent diagnostic limit. All eight affected nodes pass serially in +94.57 seconds and under pytest-xdist in 37.05 seconds. Eight finite open-loop +findings remain, confined to SRP, SSI, and SimLink. + +The SRP and SSI batch removes six more finite open-loop findings. SRP response +collection remains protected by its existing 20-us `with_timeout()` and now +uses condition-driven completion internally; its direct AXI-Lite responder +waits also have 1024-cycle diagnostic limits. The SSI resize waiter retains its +existing 1-ms operation timeout while making terminal-beat completion explicit. +All 175 affected parameter cases pass serially in 1837.27 seconds and from the +cache under pytest-xdist in 76.68 seconds. + +The final two finite `while True` findings were SimLink traffic operations. +Multi-instance traffic now carries its existing wall-clock deadline directly +in the loop, and the native receive probe has its own four-second diagnostic +deadline in addition to the parent-process watchdog. Both retain unconditional +task/process/native-context cleanup. The focused native and GHDL SimLink suite +passes all nine nodes in 4.60 seconds with localhost socket access. + +The repository reports zero unretained non-clock `start_soon()` calls and zero +unclassified `while True` loops. Both rules now participate in the checked-in +zero-entry compliance baseline, so new implicit task ownership or open-ended +transaction loops fail the blocking repository check. Conditional polling +loops remain a distinct bounded-wait review and are not represented by this +structural zero. + +The first conditional-wait batch covers the RoCEv2 RDMA core's ten direct +ready/valid polls. They now use one bench-local 65,536-cycle helper that names +the stalled interface in its failure, while scoreboard/liveness waits retain +their existing operation-level `with_timeout()` watchdogs and fixed-length +CoaXPress capture loops remain directly cycle-bounded by their requested +sample count. The RDMA-core pytest node passes serially in 35.55 seconds. + ## Non-Negotiable Preservation Rules - Do not remove an existing test, parameter case, opt-in gate, or behavioral diff --git a/tests/README.md b/tests/README.md index 7de7b8f04f..786879abb3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -261,7 +261,9 @@ deassert the source immediately. Use `start_lockstep_clocks()` for `COMMON_CLK_G` or similar wrappers that expect truly shared clock edges. Do not start two independent same-period clock -coroutines when the DUT contract is common-clock behavior. +coroutines when the DUT contract is common-clock behavior. Retain its returned +task on the bench (for example, `self._clock_task = start_lockstep_clocks(...)`) +so ownership remains explicit just like any other lifetime agent. ## Isolation And Coroutine Lifecycle diff --git a/tests/axi/axi4/test_AxiRateGen.py b/tests/axi/axi4/test_AxiRateGen.py index e8f754a5e2..8da972cc79 100644 --- a/tests/axi/axi4/test_AxiRateGen.py +++ b/tests/axi/axi4/test_AxiRateGen.py @@ -48,7 +48,7 @@ def __init__(self, dut): self.ar_handshakes = [] self.r_handshakes = [] - start_lockstep_clocks(dut.axiClk, dut.axilClk, period_ns=5.0) + self._clock_task = start_lockstep_clocks(dut.axiClk, dut.axilClk, period_ns=5.0) dut.axiRst.setimmediatevalue(1) dut.axilRst.setimmediatevalue(1) @@ -60,11 +60,15 @@ def __init__(self, dut): ) self.axi_ram = AxiRam(AxiBus.from_prefix(dut, "M_AXI"), dut.axiClk, dut.axiRst, size=2**16) - cocotb.start_soon(self._track_cycles()) - cocotb.start_soon(self._monitor_aw()) - cocotb.start_soon(self._monitor_w()) - cocotb.start_soon(self._monitor_ar()) - cocotb.start_soon(self._monitor_r()) + # These monitors are lifetime agents; cocotb cancels them when the + # entrypoint ends, and the bench retains them for explicit ownership. + self._monitor_tasks = ( + cocotb.start_soon(self._track_cycles()), + cocotb.start_soon(self._monitor_aw()), + cocotb.start_soon(self._monitor_w()), + cocotb.start_soon(self._monitor_ar()), + cocotb.start_soon(self._monitor_r()), + ) async def cycle(self, count=1): for _ in range(count): @@ -95,11 +99,13 @@ async def wait_for_count(self, store, expected: int, *, limit_cycles: int, label raise AssertionError(f"Timed out waiting for {label}: expected {expected}, saw {len(store)}") async def _track_cycles(self): + """Lifetime agent: count AXI cycles until cocotb ends the test.""" while True: await RisingEdge(self.dut.axiClk) self.cycle_count += 1 async def _monitor_aw(self): + """Lifetime agent: record accepted write addresses for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") @@ -115,6 +121,7 @@ async def _monitor_aw(self): ) async def _monitor_w(self): + """Lifetime agent: record accepted write data for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") @@ -128,6 +135,7 @@ async def _monitor_w(self): ) async def _monitor_ar(self): + """Lifetime agent: record accepted read addresses for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") @@ -143,6 +151,7 @@ async def _monitor_ar(self): ) async def _monitor_r(self): + """Lifetime agent: record accepted read data for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") diff --git a/tests/axi/axi4/test_AxiReadPathMux.py b/tests/axi/axi4/test_AxiReadPathMux.py index 5a2af0e2ef..0fc08acf26 100644 --- a/tests/axi/axi4/test_AxiReadPathMux.py +++ b/tests/axi/axi4/test_AxiReadPathMux.py @@ -40,7 +40,8 @@ def __init__(self, dut): self.s1 = SourcePort(dut, "S1_AXI") self.ram = None - cocotb.start_soon(self._monitor_ar()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_ar()) async def cycle(self, count=1): for _ in range(count): @@ -60,6 +61,7 @@ def start_agents(self): self.ram = AxiRamRead(AxiReadBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _monitor_ar(self): + """Lifetime agent: record accepted read addresses for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") diff --git a/tests/axi/axi4/test_AxiResize.py b/tests/axi/axi4/test_AxiResize.py index c3b926dffa..5609e94718 100644 --- a/tests/axi/axi4/test_AxiResize.py +++ b/tests/axi/axi4/test_AxiResize.py @@ -53,8 +53,11 @@ def __init__(self, dut): self.master = None self.ram = None - cocotb.start_soon(self._monitor_aw()) - cocotb.start_soon(self._monitor_ar()) + # Lifetime monitors retained by the bench until cocotb ends the test. + self._monitor_tasks = ( + cocotb.start_soon(self._monitor_aw()), + cocotb.start_soon(self._monitor_ar()), + ) async def cycle(self, count=1): for _ in range(count): @@ -76,6 +79,7 @@ def start_agents(self): self.ram = AxiRam(AxiBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _monitor_aw(self): + """Lifetime agent: record resized write metadata for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") @@ -89,6 +93,7 @@ async def _monitor_aw(self): ) async def _monitor_ar(self): + """Lifetime agent: record resized read metadata for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") diff --git a/tests/axi/axi4/test_AxiWritePathMux.py b/tests/axi/axi4/test_AxiWritePathMux.py index a51c9cce31..edcc0de1aa 100644 --- a/tests/axi/axi4/test_AxiWritePathMux.py +++ b/tests/axi/axi4/test_AxiWritePathMux.py @@ -47,7 +47,8 @@ def __init__(self, dut): self.s1 = SourcePort(dut, "S1_AXI") self.ram = None - cocotb.start_soon(self._monitor_aw()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_aw()) async def cycle(self, count=1): for _ in range(count): @@ -67,6 +68,7 @@ def start_agents(self): self.ram = AxiRamWrite(AxiWriteBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _monitor_aw(self): + """Lifetime agent: record accepted write addresses for this test.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") diff --git a/tests/axi/axi_lite/test_AxiLiteAsync.py b/tests/axi/axi_lite/test_AxiLiteAsync.py index 183509de76..b9f734b971 100644 --- a/tests/axi/axi_lite/test_AxiLiteAsync.py +++ b/tests/axi/axi_lite/test_AxiLiteAsync.py @@ -47,7 +47,7 @@ def __init__(self, dut): self.reset_active = env_sl("RST_POLARITY_G", default=1) if self.common_clk: - start_lockstep_clocks(dut.sAxiClk, dut.mAxiClk, period_ns=6.0) + self._clock_task = start_lockstep_clocks(dut.sAxiClk, dut.mAxiClk, period_ns=6.0) else: cocotb.start_soon(Clock(dut.sAxiClk, 8.0, unit="ns").start()) cocotb.start_soon(Clock(dut.mAxiClk, 5.0, unit="ns").start()) @@ -118,8 +118,11 @@ def __init__(self, dut, reset_active): dut.M_AXI_RRESP.setimmediatevalue(0) dut.M_AXI_RDATA.setimmediatevalue(0) - cocotb.start_soon(self._run_write()) - cocotb.start_soon(self._run_read()) + # The read/write responders are lifetime protocol peers owned by TB. + self._responder_tasks = ( + cocotb.start_soon(self._run_write()), + cocotb.start_soon(self._run_read()), + ) def in_reset(self) -> bool: try: @@ -142,6 +145,7 @@ async def _wait_while_reset(self): await self.cycle(1) async def _run_write(self): + """Lifetime agent: respond to AXI-Lite writes until the test ends.""" while True: await self._wait_while_reset() @@ -184,6 +188,7 @@ async def _run_write(self): self.dut.M_AXI_BVALID.value = 0 async def _run_read(self): + """Lifetime agent: respond to AXI-Lite reads until the test ends.""" while True: await self._wait_while_reset() diff --git a/tests/axi/axi_lite/test_AxiLiteMaster.py b/tests/axi/axi_lite/test_AxiLiteMaster.py index edb8697515..157ba0de3c 100644 --- a/tests/axi/axi_lite/test_AxiLiteMaster.py +++ b/tests/axi/axi_lite/test_AxiLiteMaster.py @@ -51,8 +51,11 @@ def __init__(self, dut, reset_active): dut.M_AXI_RRESP.setimmediatevalue(0) dut.M_AXI_RDATA.setimmediatevalue(0) - cocotb.start_soon(self._run_write()) - cocotb.start_soon(self._run_read()) + # The read/write responders are lifetime protocol peers owned by TB. + self._responder_tasks = ( + cocotb.start_soon(self._run_write()), + cocotb.start_soon(self._run_read()), + ) def in_reset(self) -> bool: try: @@ -75,6 +78,7 @@ async def _wait_while_reset(self): await self.cycle(1) async def _run_write(self): + """Lifetime agent: respond to AXI-Lite writes until the test ends.""" while True: await self._wait_while_reset() @@ -124,6 +128,7 @@ async def _run_write(self): self.dut.M_AXI_BVALID.value = 0 async def _run_read(self): + """Lifetime agent: respond to AXI-Lite reads until the test ends.""" while True: await self._wait_while_reset() diff --git a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py index 0a41170af0..11326b7ebb 100644 --- a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py +++ b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py @@ -45,7 +45,8 @@ def __init__(self, dut, reset_active): dut.M_AXI_BVALID.setimmediatevalue(0) dut.M_AXI_BRESP.setimmediatevalue(0) - cocotb.start_soon(self._run()) + # Lifetime AXI-Lite responder retained by the bus model. + self._responder_task = cocotb.start_soon(self._run()) def in_reset(self) -> bool: try: @@ -66,6 +67,7 @@ async def _wait_while_reset(self): await self.cycle(1) async def _run(self): + """Lifetime agent: respond to downstream writes until the test ends.""" while True: await self._wait_while_reset() diff --git a/tests/axi/axi_stream/test_AxiStreamCompact.py b/tests/axi/axi_stream/test_AxiStreamCompact.py index 0e3ab3c5b9..6870c4c0bd 100644 --- a/tests/axi/axi_stream/test_AxiStreamCompact.py +++ b/tests/axi/axi_stream/test_AxiStreamCompact.py @@ -27,6 +27,7 @@ from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer +from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test @@ -48,7 +49,8 @@ def __init__(self, dut): dut.S_AXIS_TID.setimmediatevalue(0) dut.S_AXIS_TUSER.setimmediatevalue(0) dut.M_AXIS_TREADY.setimmediatevalue(1) - cocotb.start_soon(self._monitor()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor()) async def cycle(self, count=1): for _ in range(count): @@ -62,6 +64,7 @@ async def reset(self): await self.cycle(3) async def _monitor(self): + """Lifetime agent: collect compacted output beats until the test ends.""" while True: await RisingEdge(self.dut.axisClk) await Timer(1, unit="ns") @@ -85,11 +88,7 @@ async def drive_beat(self, *, data: int, keep: int, last: int, dest: int, tid: i self.dut.S_AXIS_TID.value = tid self.dut.S_AXIS_TUSER.value = user & self.user_mask self.dut.S_AXIS_TVALID.value = 1 - while True: - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") - if int(self.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(self.dut.S_AXIS_TREADY, clk=self.dut.axisClk) self.dut.S_AXIS_TVALID.value = 0 async def drive_payload(self, payload: bytes, *, chunk_size: int, dest: int, tid: int, user: int): diff --git a/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py b/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py index 36edd81ad4..af5fb526ab 100644 --- a/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py +++ b/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py @@ -39,6 +39,7 @@ from cocotb.triggers import RisingEdge, Timer, with_timeout from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource +from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import env_flag, run_surf_vhdl_test, start_lockstep_clocks @@ -103,7 +104,11 @@ def __init__(self, dut): self.rx_beat_users = [] if self.clock_mode == "lockstep": - start_lockstep_clocks(dut.S_AXIS_ACLK, dut.M_AXIS_ACLK, period_ns=5.0) + self._clock_task = start_lockstep_clocks( + dut.S_AXIS_ACLK, + dut.M_AXIS_ACLK, + period_ns=5.0, + ) else: cocotb.start_soon(Clock(dut.S_AXIS_ACLK, 5.0, unit="ns").start()) cocotb.start_soon(Clock(dut.M_AXIS_ACLK, 7.0, unit="ns").start()) @@ -120,8 +125,11 @@ def __init__(self, dut): dut.M_AXIS_TREADY.setimmediatevalue(0) dut.fifoPauseThresh.setimmediatevalue((1 << self.fifo_addr_width) - 1) - cocotb.start_soon(self._monitor_source_handshakes()) - cocotb.start_soon(self._monitor_sink_handshakes()) + # Lifetime handshake monitors retained by the bench. + self._monitor_tasks = ( + cocotb.start_soon(self._monitor_source_handshakes()), + cocotb.start_soon(self._monitor_sink_handshakes()), + ) async def settle(self): await Timer(1, unit="ns") @@ -137,6 +145,7 @@ async def cycle_sink(self, count=1): await self.settle() async def _monitor_source_handshakes(self): + """Lifetime agent: record source handshakes until the test ends.""" cycle = 0 while True: await RisingEdge(self.dut.S_AXIS_ACLK) @@ -146,6 +155,7 @@ async def _monitor_source_handshakes(self): self.tx_cycles.append(cycle) async def _monitor_sink_handshakes(self): + """Lifetime agent: record sink handshakes until the test ends.""" cycle = 0 while True: await RisingEdge(self.dut.M_AXIS_ACLK) @@ -230,11 +240,10 @@ async def send_manual_frame(self, payload: bytes, *, tid: int, tdest: int, tuser tdest=tdest, tuser=tuser_values[index], ) - while True: - await RisingEdge(self.dut.S_AXIS_ACLK) - await self.settle() - if int(self.dut.S_AXIS_TVALID.value) and int(self.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready( + self.dut.S_AXIS_TREADY, + clk=self.dut.S_AXIS_ACLK, + ) self.clear_source() await self.cycle_source(1) @@ -366,20 +375,12 @@ async def frame_ready_release_and_last_user_test(dut): for index, data_byte in enumerate(payload[:-1]): tb.drive_source_beat(data_byte, last=False, tid=0x1, tdest=0x2, tuser=beat_users[index]) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) assert tb.rx_cycles == [] assert int(tb.dut.M_AXIS_TVALID.value) == 0 tb.drive_source_beat(payload[-1], last=True, tid=0x1, tdest=0x2, tuser=beat_users[-1]) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) tb.clear_source() received = await with_timeout(capture_task, 2, "us") @@ -400,19 +401,11 @@ async def threshold_prefill_release_test(dut): payload = b"\x21\x22\x23" for data_byte in payload[: tb.valid_thold - 1]: tb.drive_source_beat(data_byte, last=False, tid=0x1, tdest=0x1, tuser=0) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) assert int(tb.dut.M_AXIS_TVALID.value) == 0 tb.drive_source_beat(payload[tb.valid_thold - 1], last=False, tid=0x1, tdest=0x1, tuser=0) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) await tb.wait_for_output_valid(timeout_cycles=4) assert int(tb.dut.M_AXIS_TVALID.value) == 1 @@ -465,11 +458,7 @@ async def dynamic_pause_threshold_test(dut): for data_byte in [0x40, 0x41, 0x42]: tb.drive_source_beat(data_byte, last=True, tid=0x1, tdest=0x1, tuser=0) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) if int(tb.dut.sAxisPause.value): break diff --git a/tests/axi/axi_stream/test_AxiStreamPipeline.py b/tests/axi/axi_stream/test_AxiStreamPipeline.py index 96cbdf9887..0282690174 100644 --- a/tests/axi/axi_stream/test_AxiStreamPipeline.py +++ b/tests/axi/axi_stream/test_AxiStreamPipeline.py @@ -66,9 +66,10 @@ def __init__(self, dut): # Record the cycle where source and sink handshakes complete so the # tests can talk about pipeline latency in exact clock cycles. - cocotb.start_soon(self._monitor_handshakes()) + self._monitor_task = cocotb.start_soon(self._monitor_handshakes()) async def _monitor_handshakes(self): + """Lifetime agent: record pipeline handshakes until the test ends.""" cycle = 0 while True: await RisingEdge(self.dut.axisClk) diff --git a/tests/axi/axi_stream/test_AxiStreamResize.py b/tests/axi/axi_stream/test_AxiStreamResize.py index f5c2cd7feb..719f865bcb 100644 --- a/tests/axi/axi_stream/test_AxiStreamResize.py +++ b/tests/axi/axi_stream/test_AxiStreamResize.py @@ -60,7 +60,8 @@ def __init__(self, dut): dut.S_SIDE_BAND.setimmediatevalue(0) dut.M_AXIS_TREADY.setimmediatevalue(0) - cocotb.start_soon(self._monitor_sideband()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_sideband()) def reset_active_value(self) -> int: return self.reset_active @@ -77,6 +78,7 @@ async def cycle(self, count=1): await self.settle() async def _monitor_sideband(self): + """Lifetime agent: collect resized sidebands until the test ends.""" while True: await RisingEdge(self.dut.axisClk) await self.settle() diff --git a/tests/axi/bridge/test_AxiLiteToIpBus.py b/tests/axi/bridge/test_AxiLiteToIpBus.py index 55f98e5825..81f050c548 100644 --- a/tests/axi/bridge/test_AxiLiteToIpBus.py +++ b/tests/axi/bridge/test_AxiLiteToIpBus.py @@ -37,7 +37,8 @@ def __init__(self, dut): dut.ipbRdata.setimmediatevalue(0) dut.ipbAck.setimmediatevalue(0) dut.ipbErr.setimmediatevalue(0) - cocotb.start_soon(self._ipb_model()) + # Lifetime IPbus protocol peer retained by the bench. + self._ipb_task = cocotb.start_soon(self._ipb_model()) async def cycle(self, count=1): for _ in range(count): @@ -55,6 +56,7 @@ def start_agents(self): self.axi = AxiLiteMaster(AxiLiteBus.from_prefix(self.dut, "S_AXI"), self.dut.axiClk, self.dut.axiRst) async def _ipb_model(self): + """Lifetime agent: serve IPbus requests until the test ends.""" pending = None delay = 0 while True: diff --git a/tests/axi/bridge/test_IpBusToAxiLite.py b/tests/axi/bridge/test_IpBusToAxiLite.py index fa5faebf1a..ae27cbd65c 100644 --- a/tests/axi/bridge/test_IpBusToAxiLite.py +++ b/tests/axi/bridge/test_IpBusToAxiLite.py @@ -59,11 +59,15 @@ async def ipb_request(self, *, address: int, write: bool, data: int = 0): self.dut.ipbWdata.value = data self.dut.ipbWrite.value = int(write) self.dut.ipbStrobe.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") if int(self.dut.ipbAck.value): break + else: + raise AssertionError( + f"Timed out waiting for IPbus acknowledgement at address 0x{address:08x}" + ) result = (int(self.dut.ipbRdata.value), int(self.dut.ipbErr.value)) self.dut.ipbStrobe.value = 0 self.dut.ipbWrite.value = 0 diff --git a/tests/axi/dma/test_AxiStreamDmaV2Write.py b/tests/axi/dma/test_AxiStreamDmaV2Write.py index 1ce6fbfc4d..86b7edc038 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Write.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Write.py @@ -50,8 +50,11 @@ def __init__(self, dut): dut.axiWriteCtrlOver.setimmediatevalue(0) dut.dmaWrDescAckValid.setimmediatevalue(0) dut.dmaWrDescRetAck.setimmediatevalue(0) - cocotb.start_soon(self._descriptor_responder()) - cocotb.start_soon(self._monitor_aw()) + # Lifetime descriptor peer and handshake monitor owned by the bench. + self._lifetime_tasks = ( + cocotb.start_soon(self._descriptor_responder()), + cocotb.start_soon(self._monitor_aw()), + ) async def cycle(self, count=1): for _ in range(count): @@ -71,6 +74,7 @@ def start_agents(self): self.ram = AxiRamWrite(AxiWriteBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _descriptor_responder(self): + """Lifetime agent: acknowledge DMA descriptors until the test ends.""" max_size = int(os.environ.get("DESC_MAX_SIZE", "32"), 0) timeout = int(os.environ.get("DESC_TIMEOUT", "32"), 0) while True: @@ -89,6 +93,7 @@ async def _descriptor_responder(self): self.dut.dmaWrDescAckValid.value = 1 async def _monitor_aw(self): + """Lifetime agent: record DMA write addresses until the test ends.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") diff --git a/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py b/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py index c3e30cafed..662814b3e6 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py +++ b/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py @@ -63,8 +63,11 @@ def __init__(self, dut): dut.axiWriteCtrlOver.setimmediatevalue(0) dut.dmaWrDescAckValid.setimmediatevalue(0) dut.dmaWrDescRetAck.setimmediatevalue(0) - cocotb.start_soon(self._descriptor_responder()) - cocotb.start_soon(self._monitor_aw()) + # Lifetime descriptor peer and handshake monitor owned by the bench. + self._lifetime_tasks = ( + cocotb.start_soon(self._descriptor_responder()), + cocotb.start_soon(self._monitor_aw()), + ) def buf_addr(self, i): return self.base + i * self.stride @@ -87,6 +90,7 @@ def start_agents(self): self.ram = AxiRamWrite(AxiWriteBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2 ** 16) async def _descriptor_responder(self): + """Lifetime agent: acknowledge DMA descriptors until the test ends.""" acked = False while True: await RisingEdge(self.dut.axiClk) @@ -110,6 +114,7 @@ async def _descriptor_responder(self): self.dut.dmaWrDescAckValid.value = 1 async def _monitor_aw(self): + """Lifetime agent: record DMA write addresses until the test ends.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") diff --git a/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py b/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py index 5dd9fc06f7..5d77dcbbd3 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py +++ b/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py @@ -113,7 +113,8 @@ def __init__(self, dut): dut.axiRst.setimmediatevalue(1) dut.mAxiWriteCtrlPause.setimmediatevalue(0) dut.mAxiWriteCtrlOver.setimmediatevalue(0) - cocotb.start_soon(self._monitor_aw()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_aw()) async def cycle(self, count=1): for _ in range(count): @@ -136,6 +137,7 @@ def start_agents(self): ) async def _monitor_aw(self): + """Lifetime agent: record muxed write addresses until the test ends.""" while True: await RisingEdge(self.dut.axiClk) await Timer(1, unit="ns") diff --git a/tests/base/fifo/test_FifoOutputPipeline.py b/tests/base/fifo/test_FifoOutputPipeline.py index da31f5c5e7..7a8511e9a7 100644 --- a/tests/base/fifo/test_FifoOutputPipeline.py +++ b/tests/base/fifo/test_FifoOutputPipeline.py @@ -59,8 +59,10 @@ def __init__(self, dut): # Start the main DUT clock. For pipelined cases, also start the little # Python coroutine that emulates the upstream FWFT source. cocotb.start_soon(Clock(dut.clk, self.clk_period_ns, unit="ns").start()) + self._source_task = None if self.pipe_stages > 0: - cocotb.start_soon(self._drive_source()) + # Lifetime source model retained by the bench. + self._source_task = cocotb.start_soon(self._drive_source()) def reset_active_value(self) -> int: return self.rst_polarity @@ -98,6 +100,7 @@ def feed_words(self, words: list[int]) -> None: self.source_words.extend(words) async def _drive_source(self) -> None: + """Lifetime agent: emulate the upstream FWFT source for this test.""" # This coroutine emulates the upstream FIFO/source interface. It keeps # `sValid/sData` aligned to the DUT's `sRdEn` requests. next_valid = 0 diff --git a/tests/common/README.md b/tests/common/README.md index c7496db71c..ea0d052552 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -79,7 +79,8 @@ and build-isolation conventions where they apply. `cocotb_filtered_env()` build explicit scenario groups while preserving an externally requested focused selector in the simulation-build identity. - `start_lockstep_clocks()` drives multiple logically common clocks from one - coroutine so their edges cannot drift. + coroutine so their edges cannot drift. It returns the lifetime task; retain + that task on the bench that owns the clock domains. - `build_vhdl_sources()` and `merge_vhdl_sources()` are runner plumbing; tests should normally reach them only through `run_surf_vhdl_test()`. @@ -130,6 +131,11 @@ protocol peers intended to run for the whole test on the bench, document them as lifetime agents, and provide cleanup when cancellation order matters or an agent owns a socket, process, file, or other external resource. +Give an intentional open-ended agent a function docstring containing +`Lifetime agent:` followed by its purpose and termination owner. The compliance +audit recognizes that explicit classification; do not use the marker on a +finite handshake, receive loop, or transaction that needs a cycle/time limit. + ## Compliance Audit And Preservation Reports `compliance_audit.py` provides a read-only structural audit and a reproducible @@ -166,6 +172,10 @@ The checked-in `compliance_baseline.json` ratchets the rules that are reliable enough to enforce structurally: methodology presence, ordinary direct-runner exceptions, literal VHDL sources duplicated from the ruckus import, and a bare entrypoint return reached before any awaited simulator activity or assertion. +It also rejects an unretained non-clock `cocotb.start_soon()` call and an +unclassified `while True` loop. Intentional lifetime loops must carry the +`Lifetime agent:` docstring contract described above; finite operations must +use a direct cycle/time bound instead. Returns after test activity remain report-only because they can be intentional successful terminal paths and require semantic review. Run the check after importing the HDL tree: diff --git a/tests/common/compliance_audit.py b/tests/common/compliance_audit.py index 12c82f867a..a05b0b56ee 100644 --- a/tests/common/compliance_audit.py +++ b/tests/common/compliance_audit.py @@ -30,6 +30,8 @@ "duplicate-imported-source", "early-bare-return", "missing-methodology", + "open-ended-loop", + "unretained-start-soon", } EXCLUDED_DIRECTORY_NAMES = { @@ -39,6 +41,7 @@ } METHODOLOGY_MARKER = "Test methodology:" +LIFETIME_AGENT_MARKER = "Lifetime agent:" ENVIRONMENT_CONTROL_RE = re.compile( r"^(?:RUN_[A-Z0-9_]*_TESTS|COCOTB_TEST_FILTER|COCOTB_TESTCASE|[A-Z0-9_]*TESTCASE)$" ) @@ -163,6 +166,11 @@ def _has_skip(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: return False +def _is_lifetime_agent(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + docstring = ast.get_docstring(function, clean=False) + return docstring is not None and LIFETIME_AGENT_MARKER in docstring + + class _FunctionWalker(ast.NodeVisitor): def __init__(self, root: ast.FunctionDef | ast.AsyncFunctionDef): self.root = root @@ -440,9 +448,10 @@ def audit_source( for function in _all_functions(tree): parents = _parent_map(function) + lifetime_agent = _is_lifetime_agent(function) for node in _walk_function(function): if isinstance(node, ast.While) and isinstance(node.test, ast.Constant): - if node.test.value is True: + if node.test.value is True and not lifetime_agent: findings.append( Finding( "open-ended-loop", diff --git a/tests/common/compliance_baseline.json b/tests/common/compliance_baseline.json index 3561dfb4a9..9121980a13 100644 --- a/tests/common/compliance_baseline.json +++ b/tests/common/compliance_baseline.json @@ -8,7 +8,9 @@ "tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py": 1, "tests/simlink/rogue/test_RogueSideBandRogue.py": 1, "tests/simlink/rogue/test_RogueStreamRogue.py": 1 - } + }, + "open-ended-loop": {}, + "unretained-start-soon": {} }, "schema_version": 1 } diff --git a/tests/common/regression_utils.py b/tests/common/regression_utils.py index 8d2aa9a575..47999f3f19 100644 --- a/tests/common/regression_utils.py +++ b/tests/common/regression_utils.py @@ -79,11 +79,12 @@ def _optional_ghdl_warning_flags() -> list[str]: ] -def start_lockstep_clocks(*signals, period_ns: float) -> None: +def start_lockstep_clocks(*signals, period_ns: float): import cocotb from cocotb.triggers import Timer async def drive() -> None: + """Lifetime agent: drive all requested clocks until the test ends.""" half_period_ns = period_ns / 2 for signal in signals: signal.value = 0 @@ -99,7 +100,7 @@ async def drive() -> None: # Drive logically-common clocks from one coroutine so COMMON_CLK_G tests # really exercise a shared clock, not two same-period oscillators that can # drift in phase relative to each other. - cocotb.start_soon(drive()) + return cocotb.start_soon(drive()) def env_flag(name: str, *, default: bool) -> bool: diff --git a/tests/common/test_compliance_audit.py b/tests/common/test_compliance_audit.py index fecceb56cb..cb377192c5 100644 --- a/tests/common/test_compliance_audit.py +++ b/tests/common/test_compliance_audit.py @@ -153,6 +153,28 @@ async def task_case(dut): assert "unretained-start-soon" not in _rules(findings) +def test_audit_distinguishes_documented_lifetime_agents_from_unbounded_operations(): + source = ''' +async def monitor(): + """Lifetime agent: observe accepted transfers until the test ends.""" + while True: + await sample_transfer() + +async def receive_transaction(): + while True: + await sample_transfer() + if complete(): + break +''' + + findings = compliance_audit.audit_source(source) + loops = [finding for finding in findings if finding.rule == "open-ended-loop"] + + assert [(finding.symbol, finding.line) for finding in loops] == [ + ("receive_transaction", 8), + ] + + def test_audit_reports_direct_runner_except_documented_simlink_helper(): source = """ from cocotb_test.simulator import run @@ -253,6 +275,8 @@ def test_compliance_baseline_allows_cleanup_but_rejects_new_findings(): "duplicate-imported-source": {"tests/sources.py": 2}, "early-bare-return": {"tests/ambiguous.py": 1}, "missing-methodology": {}, + "open-ended-loop": {}, + "unretained-start-soon": {}, }, } findings = ( @@ -323,6 +347,8 @@ def test_compliance_baseline_counts_findings_by_rule_and_path(): "duplicate-imported-source": {"tests/source.py": 2}, "early-bare-return": {}, "missing-methodology": {}, + "open-ended-loop": {}, + "unretained-start-soon": {}, }, } diff --git a/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py b/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py index d732bf50a2..7c9faa4e2e 100644 --- a/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py +++ b/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py @@ -57,6 +57,8 @@ BEAT_BYTES = 32 CLK_NS = 6.4 DATA_MASK = (1 << 256) - 1 +DMA_RESPONSE_TIMEOUT_CYCLES = 65_536 +PROGRESS_TIMEOUT_CYCLES = 65_536 def beat_pattern(counter: int) -> bytes: @@ -135,6 +137,15 @@ async def _edge(self): await RisingEdge(self.dut.clk) await Timer(1, unit="ns") + async def _wait_asserted(self, signal, name: str) -> None: + for _ in range(PROGRESS_TIMEOUT_CYCLES): + if int(signal.value): + return + await self._edge() + raise AssertionError( + f"Timed out after {PROGRESS_TIMEOUT_CYCLES} cycles waiting for {name}" + ) + async def reset(self): self.dut.rst.value = 1 for _ in range(8): @@ -171,8 +182,7 @@ async def push_packets(self, num_packets: int, beats_per_packet: int): dut.S_AXIS_TVALID.value = 1 ctr += 1 await self._edge() - while int(dut.S_AXIS_TREADY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_AXIS_TREADY, "S_AXIS_TREADY") dut.S_AXIS_TVALID.value = 0 dut.S_AXIS_TLAST.value = 0 @@ -187,8 +197,7 @@ async def push_partial(self, nbeats: int): dut.S_AXIS_TLAST.value = 0 dut.S_AXIS_TVALID.value = 1 await self._edge() - while int(dut.S_AXIS_TREADY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_AXIS_TREADY, "S_AXIS_TREADY") dut.S_AXIS_TVALID.value = 0 # --- engine emulator (single, in-order) ------------------------------------- @@ -200,8 +209,7 @@ async def _accept_workreq(self, backpressure=0): await RisingEdge(dut.clk) dut.M_WORKREQ_READY.value = 1 await self._edge() - while int(dut.M_WORKREQ_VALID.value) == 0: - await self._edge() + await self._wait_asserted(dut.M_WORKREQ_VALID, "M_WORKREQ_VALID") wr = { "id": int(dut.M_WORKREQ_ID.value), "opcode": int(dut.M_WORKREQ_OPCODE.value), @@ -220,22 +228,19 @@ async def _issue_dmareadreq(self, wr): dut.S_DMAREADREQ_WRID.value = wr["id"] dut.S_DMAREADREQ_SQPN.value = 0x10 await self._edge() - while int(dut.S_DMAREADREQ_READY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_DMAREADREQ_READY, "S_DMAREADREQ_READY") dut.S_DMAREADREQ_VALID.value = 0 async def _drain_resp(self, cfg): dut = self.dut beats = [] is_err = 0 - n = 0 - while True: + for n in range(DMA_RESPONSE_TIMEOUT_CYCLES): # optional backpressure if cfg.resp_backpressure and (n % cfg.resp_backpressure == cfg.resp_backpressure - 1): dut.M_DMAREADRESP_READY.value = 0 else: dut.M_DMAREADRESP_READY.value = 1 - n += 1 await self._edge() if int(dut.M_DMAREADRESP_VALID.value) == 1 and int(dut.M_DMAREADRESP_READY.value) == 1: ds = int(dut.M_DMAREADRESP_DATASTREAM.value) @@ -246,6 +251,12 @@ async def _drain_resp(self, cfg): beats.append((data, is_first, is_last)) if is_last: break + else: + dut.M_DMAREADRESP_READY.value = 0 + raise AssertionError( + "Timed out waiting for final DMA read response beat; " + f"received {len(beats)} beats" + ) dut.M_DMAREADRESP_READY.value = 0 return beats, is_err @@ -255,11 +266,11 @@ async def _issue_workcomp(self, wr): dut.S_WORKCOMP_STATUS.value = 0 dut.S_WORKCOMP_ID.value = wr["id"] await self._edge() - while int(dut.S_WORKCOMP_READY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_WORKCOMP_READY, "S_WORKCOMP_READY") dut.S_WORKCOMP_VALID.value = 0 async def engine(self, cfg, sb=None): + """Lifetime agent: service RDMA work until its owning test cancels it.""" idx = 0 while True: wr = await self._accept_workreq(cfg.workreq_backpressure) @@ -285,25 +296,33 @@ async def _run(dut, *, cfg, num_packets, beats_per_packet, watchdog_ns): await tb.reset() await tb.configure(beats_per_packet * BEAT_BYTES) sb = Scoreboard(beats_per_packet) - cocotb.start_soon(tb.engine(cfg, sb)) - cocotb.start_soon(tb.push_packets(num_packets, beats_per_packet)) + engine_task = cocotb.start_soon(tb.engine(cfg, sb)) + producer_task = cocotb.start_soon(tb.push_packets(num_packets, beats_per_packet)) async def wait_done(): while sb.packets < num_packets: await RisingEdge(dut.clk) # A wedge in the DUT stalls dmaReadResp -> wait_done never completes -> timeout. - await with_timeout(wait_done(), watchdog_ns, "ns") + try: + await with_timeout(wait_done(), watchdog_ns, "ns") + await producer_task - for _ in range(64): # let the final workComp settle - await RisingEdge(dut.clk) - succ = await axil_read_u32(tb.axil, REG_SUCCESS) - unsucc = await axil_read_u32(tb.axil, REG_UNSUCCESS) + for _ in range(64): # let the final workComp settle + await RisingEdge(dut.clk) + succ = await axil_read_u32(tb.axil, REG_SUCCESS) + unsucc = await axil_read_u32(tb.axil, REG_UNSUCCESS) - assert not sb.errors, f"scoreboard errors (first 10): {sb.errors[:10]}" - assert sb.packets == num_packets, f"only {sb.packets}/{num_packets} packets drained" - assert succ == num_packets, f"SuccessCounter={succ} != {num_packets}" - assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + assert not sb.errors, f"scoreboard errors (first 10): {sb.errors[:10]}" + assert sb.packets == num_packets, f"only {sb.packets}/{num_packets} packets drained" + assert succ == num_packets, f"SuccessCounter={succ} != {num_packets}" + assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + finally: + # The engine is a lifetime protocol peer; the producer is finite but + # must also be cancelled if the watchdog aborts its transaction. + engine_task.cancel() + if not producer_task.done(): + producer_task.cancel() @cocotb.test() @@ -393,7 +412,7 @@ async def engine_teardown_then_restart(dut): # disarm window drains/drops whatever the source was pushing, so kill the old # flood and idle the bus before re-arming. await axil_write_u32(tb.axil, REG_DISPATCH_ENABLE, 0) - flood.kill() + flood.cancel() dut.S_AXIS_TVALID.value = 0 dut.S_AXIS_TLAST.value = 0 for _ in range(20): @@ -403,15 +422,20 @@ async def engine_teardown_then_restart(dut): await axil_write_u32(tb.axil, REG_DISPATCH_ENABLE, 1) # A fresh, healthy engine takes over, fed by a fresh packet flood. - cocotb.start_soon(tb.engine(Cfg(readreq_latency=10, workcomp_latency=4))) - cocotb.start_soon(tb.push_packets(32, bpp)) + engine_task = cocotb.start_soon(tb.engine(Cfg(readreq_latency=10, workcomp_latency=4))) + refill_task = cocotb.start_soon(tb.push_packets(32, bpp)) async def wait_live(n): while int(await axil_read_u32(tb.axil, REG_SUCCESS)) < n: await RisingEdge(dut.clk) # Liveness: at least 4 completions after the restart, or the wedge stands. - await with_timeout(wait_live(4), 600_000, "ns") + try: + await with_timeout(wait_live(4), 600_000, "ns") + finally: + # Both agents intentionally run only for the liveness window. + engine_task.cancel() + refill_task.cancel() @cocotb.test() @@ -442,20 +466,26 @@ async def partial_packet_then_rearm(dut): # The clean stream must validate exactly — the stale partial is gone. n = 8 sb = Scoreboard(bpp) - cocotb.start_soon(tb.engine(Cfg(readreq_latency=8, workcomp_latency=4), sb)) - cocotb.start_soon(tb.push_packets(n, bpp)) + engine_task = cocotb.start_soon(tb.engine(Cfg(readreq_latency=8, workcomp_latency=4), sb)) + producer_task = cocotb.start_soon(tb.push_packets(n, bpp)) async def wait_done(): while sb.packets < n: await RisingEdge(dut.clk) - await with_timeout(wait_done(), 600_000, "ns") - for _ in range(64): - await RisingEdge(dut.clk) - unsucc = int(await axil_read_u32(tb.axil, REG_UNSUCCESS)) - assert not sb.errors, f"partial packet fused into the stream: {sb.errors[:10]}" - assert sb.packets == n, f"only {sb.packets}/{n} packets drained" - assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + try: + await with_timeout(wait_done(), 600_000, "ns") + await producer_task + for _ in range(64): + await RisingEdge(dut.clk) + unsucc = int(await axil_read_u32(tb.axil, REG_UNSUCCESS)) + assert not sb.errors, f"partial packet fused into the stream: {sb.errors[:10]}" + assert sb.packets == n, f"only {sb.packets}/{n} packets drained" + assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + finally: + engine_task.cancel() + if not producer_task.done(): + producer_task.cancel() @cocotb.test() @@ -470,7 +500,7 @@ async def send_opcode_and_zeroed_reth(dut): n = 16 await tb.configure(bpp * BEAT_BYTES) - cocotb.start_soon(tb.push_packets(n, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(n, bpp)) for k in range(n): wr = await tb._accept_workreq() assert wr["opcode"] == 0x3, f"pkt {k}: opCode 0x{wr['opcode']:x} != 0x3 (SEND_WITH_IMM)" @@ -484,6 +514,7 @@ async def send_opcode_and_zeroed_reth(dut): for _ in range(2): await RisingEdge(dut.clk) await tb._issue_workcomp(wr) + await producer_task @cocotb.test() @@ -501,7 +532,7 @@ async def immediate_carries_channel_and_slot(dut): length = bpp * BEAT_BYTES await tb.configure(length, addrwrap=wrap) - cocotb.start_soon(tb.push_packets(n, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(n, bpp)) for k in range(n): wr = await tb._accept_workreq() immdt = wr["immdt"] @@ -517,6 +548,7 @@ async def immediate_carries_channel_and_slot(dut): for _ in range(2): await RisingEdge(dut.clk) await tb._issue_workcomp(wr) + await producer_task @cocotb.test() @@ -530,7 +562,7 @@ async def retry_rereads_same_payload(dut): await tb.reset() bpp = 4 await tb.configure(bpp * BEAT_BYTES) - cocotb.start_soon(tb.push_packets(8, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(8, bpp)) wr = await tb._accept_workreq() # First read of this wr_id. @@ -547,6 +579,7 @@ async def retry_rereads_same_payload(dut): # Complete it (frees the slot) and confirm one success completion was counted. await tb._issue_workcomp(wr) + await producer_task for _ in range(64): await RisingEdge(dut.clk) assert int(await axil_read_u32(tb.axil, REG_SUCCESS)) == 1 @@ -566,7 +599,7 @@ async def ring_backpressure(dut): bpp = 2 total = RING_SLOTS + 6 await tb.configure(bpp * BEAT_BYTES) - cocotb.start_soon(tb.push_packets(total, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(total, bpp)) # Accept WRs WITHOUT completing them (freePtr frozen) -> the ring fills to # RING_SLOTS and the dispatcher must then stall. @@ -582,12 +615,14 @@ async def watch_extra(): await tb._accept_workreq() extra_seen["hit"] = True - t = cocotb.start_soon(watch_extra()) - for _ in range(3000): - await RisingEdge(dut.clk) - assert not extra_seen["hit"], \ - f"dispatch exceeded the ring bound: a {RING_SLOTS + 1}th WR issued with the ring full" - t.kill() + extra_watch_task = cocotb.start_soon(watch_extra()) + try: + for _ in range(3000): + await RisingEdge(dut.clk) + assert not extra_seen["hit"], \ + f"dispatch exceeded the ring bound: a {RING_SLOTS + 1}th WR issued with the ring full" + finally: + extra_watch_task.cancel() dut.M_WORKREQ_READY.value = 0 # Release the gate: complete the held WRs -> freePtr advances -> ring drains. @@ -599,6 +634,8 @@ async def watch_extra(): wr = await with_timeout(tb._accept_workreq(), 400_000, "ns") await tb._issue_workcomp(wr) + await producer_task + for _ in range(64): await RisingEdge(dut.clk) assert int(await axil_read_u32(tb.axil, REG_SUCCESS)) == total, \ @@ -632,12 +669,11 @@ async def push_seq(): d.S_AXIS_TVALID.value = 1 ctr += 1 await tb._edge() - while int(d.S_AXIS_TREADY.value) == 0: - await tb._edge() + await tb._wait_asserted(d.S_AXIS_TREADY, "S_AXIS_TREADY") d.S_AXIS_TVALID.value = 0 d.S_AXIS_TLAST.value = 0 - cocotb.start_soon(push_seq()) + producer_task = cocotb.start_soon(push_seq()) # The over-cap packet produces NO workReq (dropped). The first (and only) workReq is # the FOLLOWING normal packet -- it must dispatch cleanly carrying ITS bytes (proving @@ -654,6 +690,7 @@ async def push_seq(): exp = endian_swap_32(beat_pattern(over_beats + i)) assert data == exp, f"following pkt beat {i}: 0x{data:064x} != 0x{exp:064x}" await tb._issue_workcomp(wr) + await producer_task for _ in range(64): await RisingEdge(dut.clk) @@ -699,12 +736,11 @@ async def push_seq(): d.S_AXIS_TVALID.value = 1 ctr += 1 await tb._edge() - while int(d.S_AXIS_TREADY.value) == 0: - await tb._edge() + await tb._wait_asserted(d.S_AXIS_TREADY, "S_AXIS_TREADY") d.S_AXIS_TVALID.value = 0 d.S_AXIS_TLAST.value = 0 - cocotb.start_soon(push_seq()) + producer_task = cocotb.start_soon(push_seq()) ctr = 0 for k, nbeats in enumerate(sizes): @@ -722,6 +758,8 @@ async def push_seq(): assert data == exp, f"pkt {k} beat {i}: 0x{data:064x} != 0x{exp:064x}" await tb._issue_workcomp(wr) + await producer_task + for _ in range(64): await RisingEdge(dut.clk) succ = int(await axil_read_u32(tb.axil, REG_SUCCESS)) @@ -756,12 +794,11 @@ async def push_frame(): d.S_AXIS_TLAST.value = 1 if last else 0 d.S_AXIS_TVALID.value = 1 await tb._edge() - while int(d.S_AXIS_TREADY.value) == 0: - await tb._edge() + await tb._wait_asserted(d.S_AXIS_TREADY, "S_AXIS_TREADY") d.S_AXIS_TVALID.value = 0 d.S_AXIS_TLAST.value = 0 - cocotb.start_soon(push_frame()) + producer_task = cocotb.start_soon(push_frame()) wr = await with_timeout(tb._accept_workreq(), 200_000, "ns") assert wr["len"] == total_bytes, f"workReq.len {wr['len']} != {total_bytes} (byte-exact)" @@ -772,13 +809,12 @@ async def push_frame(): dut.S_DMAREADREQ_WRID.value = wr["id"] dut.S_DMAREADREQ_SQPN.value = 0x10 await tb._edge() - while int(dut.S_DMAREADREQ_READY.value) == 0: - await tb._edge() + await tb._wait_asserted(dut.S_DMAREADREQ_READY, "S_DMAREADREQ_READY") dut.S_DMAREADREQ_VALID.value = 0 beats = [] is_err = 0 - while True: + for _ in range(DMA_RESPONSE_TIMEOUT_CYCLES): dut.M_DMAREADRESP_READY.value = 1 await tb._edge() if int(dut.M_DMAREADRESP_VALID.value) and int(dut.M_DMAREADRESP_READY.value): @@ -787,7 +823,14 @@ async def push_frame(): beats.append(((ds >> 34) & DATA_MASK, (ds >> 2) & 0xFFFFFFFF, ds & 1)) if ds & 1: break + else: + dut.M_DMAREADRESP_READY.value = 0 + raise AssertionError( + "Timed out waiting for partial-frame DMA response; " + f"received {len(beats)} beats" + ) dut.M_DMAREADRESP_READY.value = 0 + await producer_task def bitrev32(x): return int(f"{x:032b}"[::-1], 2) diff --git a/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py b/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py index da7327bbb4..27cd1288a0 100644 --- a/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py +++ b/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py @@ -132,6 +132,7 @@ async def configure_intervals(self, rate_inc_interval): # --- ingress source: 32-byte beats full-rate, honoring S_AXIS_TREADY ----- async def drive_beats(self): + """Lifetime agent: drive full-rate ingress until its test cancels it.""" dut = self.dut ctr = 0 while True: @@ -179,7 +180,8 @@ async def baseline_no_cnp(dut): await tb.reset() await tb.configure_intervals(RATE_INC_INTERVAL_HOLD) - cocotb.start_soon(tb.drive_beats()) + # The full-rate source is a lifetime agent for this measurement window. + source_task = cocotb.start_soon(tb.drive_beats()) async def body(): # Let the FIFO prime and the TokenBucket reach steady state. @@ -194,7 +196,10 @@ async def body(): assert 0.20 <= rate <= 0.30, \ f"baseline egress {rate:.4f} b/clk outside the ~0.25 token-bucket band ({beats}/{COUNT_WINDOW})" - await with_timeout(body(), 200_000, "ns") + try: + await with_timeout(body(), 200_000, "ns") + finally: + source_task.cancel() @cocotb.test() @@ -211,7 +216,8 @@ async def single_cnp_halves(dut): # measurement window, isolating the single 50% cut from the slow recovery. await tb.configure_intervals(RATE_INC_INTERVAL_HOLD) - cocotb.start_soon(tb.drive_beats()) + # The full-rate source is a lifetime agent for this measurement window. + source_task = cocotb.start_soon(tb.drive_beats()) async def body(): # Establish the LINE_RATE baseline. @@ -243,7 +249,10 @@ async def body(): assert after >= base * 0.35, \ f"post-CNP egress {after} collapsed below the expected half-rate {base} (over-throttled)" - await with_timeout(body(), 300_000, "ns") + try: + await with_timeout(body(), 300_000, "ns") + finally: + source_task.cancel() @pytest.mark.parametrize("parameters", [pytest.param({}, id="rocev2_dcqcn")]) diff --git a/tests/ethernet/RoCEv2/test_RoceConfigurator.py b/tests/ethernet/RoCEv2/test_RoceConfigurator.py index 0014a1e146..1de72ce663 100644 --- a/tests/ethernet/RoCEv2/test_RoceConfigurator.py +++ b/tests/ethernet/RoCEv2/test_RoceConfigurator.py @@ -30,7 +30,7 @@ from cocotb.triggers import RisingEdge, Timer from cocotbext.axi import AxiLiteBus, AxiLiteMaster -from tests.axi.utils import axil_read_u32, axil_write_u32 +from tests.axi.utils import axil_read_u32, axil_write_u32, wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test from tests.ethernet.RoCEv2.roce_test_utils import axil_read_wide, axil_write_wide, roce_rtl_sources @@ -103,12 +103,8 @@ async def roce_configurator_axil_to_metadata_stream_test(dut): # read-only response register bank update as expected. dut.S_META_RESP_TDATA.value = first_response dut.S_META_RESP_TVALID.value = 1 - while True: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") - if int(dut.S_META_RESP_TREADY.value) == 1: - dut.S_META_RESP_TVALID.value = 0 - break + await wait_sampled_ready(dut.S_META_RESP_TREADY, clk=dut.clk) + dut.S_META_RESP_TVALID.value = 0 await tb.cycle(2) assert (await axil_read_u32(tb.axil, 0xF00) >> 1) & 0x1 == 1 diff --git a/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py b/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py index b833e2c639..03ad8cc58f 100644 --- a/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py +++ b/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py @@ -65,7 +65,8 @@ def __init__(self, dut): dut.S_SIDE_BAND.setimmediatevalue(0) dut.M_AXIS_TREADY.setimmediatevalue(0) - cocotb.start_soon(self._monitor_sideband()) + # Lifetime sideband monitor retained by the bench. + self._monitor_task = cocotb.start_soon(self._monitor_sideband()) async def cycle(self, count: int = 1): for _ in range(count): @@ -73,6 +74,7 @@ async def cycle(self, count: int = 1): await Timer(1, unit="ns") async def _monitor_sideband(self): + """Lifetime agent: collect RoCE sidebands until the test ends.""" while True: await RisingEdge(self.dut.axisClk) await Timer(1, unit="ns") diff --git a/tests/protocols/coaxpress/test_CoaXPressCore.py b/tests/protocols/coaxpress/test_CoaXPressCore.py index 3770cb0b80..66fd3833c9 100644 --- a/tests/protocols/coaxpress/test_CoaXPressCore.py +++ b/tests/protocols/coaxpress/test_CoaXPressCore.py @@ -195,6 +195,7 @@ async def _send_image_frame( async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict[str, int], key: str) -> None: + """Lifetime agent: count asserted cycles until the owner sets stop_event.""" while True: await RisingEdge(clk) await Timer(2, unit="ns") @@ -204,6 +205,7 @@ async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict async def _trace_first_signal_high(signal, clk, stop_event: Event, trace: dict[str, object], capture) -> None: + """Lifetime agent: observe a signal until the owner sets stop_event.""" while True: await RisingEdge(clk) await Timer(2, unit="ns") diff --git a/tests/protocols/coaxpress/test_CoaXPressRx.py b/tests/protocols/coaxpress/test_CoaXPressRx.py index f0adb4a72b..0ae16869e5 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRx.py +++ b/tests/protocols/coaxpress/test_CoaXPressRx.py @@ -378,6 +378,7 @@ async def _send_one_lane_frame( async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict[str, int], key: str) -> None: + """Lifetime agent: count asserted cycles until the owner sets stop_event.""" while True: await RisingEdge(clk) await Timer(2, unit="ns") diff --git a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py index 87e3742a83..9f0a14ce73 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py @@ -611,7 +611,7 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): dut.sAxisTKeep.value = lane_keep_mask([0, 1, 2, 3]) dut.sAxisTLast.value = 0 shared_beat_cycles = 0 - while True: + for _ in range(1024): await RisingEdge(dut.rxClk) await Timer(1, unit="ns") shared_beat_cycles += 1 @@ -623,6 +623,11 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) if int(dut.sAxisTReady.value) == 1: break + else: + raise AssertionError( + "Timed out waiting for shared tail/marker beat acceptance; " + f"trace tail={trace[-8:]}" + ) dut.sAxisTValid.value = 0 dut.sAxisTData.value = 0 dut.sAxisTKeep.value = 0 diff --git a/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py b/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py index cc8918333a..dd04174ceb 100644 --- a/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py @@ -108,8 +108,9 @@ async def coaxpress_tx_ls_fsm_idle_and_config_cadence_test(dut): dut.txRate.setimmediatevalue(1) await reset_dut(dut, clk_name="txClk", reset_names=("txRst",)) - cocotb.start_soon(_drive_cfg_bytes(dut, [(0x33, 0), (0xDC, 1)])) + cfg_task = cocotb.start_soon(_drive_cfg_bytes(dut, [(0x33, 0), (0xDC, 1)])) observed = await _collect_strobes(dut, count=6, timeout_cycles=600) + await cfg_task assert [(data, is_k) for _, data, is_k in observed[:4]] == IDLE_SEQUENCE assert [(data, is_k) for _, data, is_k in observed[4:]] == [(0x33, 0), (0xDC, 1)] @@ -137,7 +138,7 @@ async def pulse_again_mid_message() -> None: await cycle(dut.txClk, 200) await _pulse_trigger(dut) - cocotb.start_soon(pulse_again_mid_message()) + retrigger_task = cocotb.start_soon(pulse_again_mid_message()) strobes: list[tuple[int, int, int]] = [] tx_trig_drop_seen = False @@ -151,6 +152,8 @@ async def pulse_again_mid_message() -> None: if len(strobes) >= 14 and tx_trig_drop_seen: break + await retrigger_task + first_trigger = None second_trigger = None for start in range(len(strobes) - 5): diff --git a/tests/protocols/jesd204b/test_Jesd204bLoopback.py b/tests/protocols/jesd204b/test_Jesd204bLoopback.py index 1af88e354a..f4eda70be7 100644 --- a/tests/protocols/jesd204b/test_Jesd204bLoopback.py +++ b/tests/protocols/jesd204b/test_Jesd204bLoopback.py @@ -213,7 +213,8 @@ async def drive_loopback_link_up( the arrival-phase sweep). Default 0 for the data-integrity test. Returns: - (stop_event, golden_capture) so callers can stop coroutine and tap wire. + (stop_event, loopback_task, golden_capture) so callers can stop and + await the lifetime forwarding agent and tap the captured wire data. """ dut = tb.dut @@ -251,7 +252,7 @@ async def drive_loopback_link_up( if golden_capture is None: golden_capture = [] stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback( dut, l_g, @@ -275,7 +276,15 @@ async def drive_loopback_link_up( # Step 6: wait for all lanes to reach DATA state await wait_data_valid_all(dut, l_g, clk=dut.devClk_i, timeout_cycles=4096) - return stop_event, golden_capture + return stop_event, loopback_task, golden_capture + + +async def stop_loopback_agent(tb, stop_event, loopback_task) -> None: + """Stop and join a forwarding agent while preserving link settle time.""" + + stop_event.set() + await loopback_task + await tb.dev_cycle(1) # --------------------------------------------------------------------------- @@ -314,7 +323,7 @@ async def test_jesd204b_loopback(dut): # Link-up phase # ----------------------------------------------------------------------- golden_capture = [] - stop_event, golden_capture = await drive_loopback_link_up( + stop_event, loopback_task, golden_capture = await drive_loopback_link_up( tb, l_g, subclass, scr_enable, golden_capture=golden_capture ) @@ -491,8 +500,7 @@ def _endianswap4(x: int) -> int: ) # Stop forwarding coroutine - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # --------------------------------------------------------------------------- @@ -565,7 +573,7 @@ async def test_jesd204b_dlat_sweep(dut): # Each iteration starts from a clean elastic buffer (tb.reset() re-asserts # devRst_i which drives s_bufRst via JesdRxLane.vhd:165). # ----------------------------------------------------------------------- - stop_event, _ = await drive_loopback_link_up( + stop_event, loopback_task, _ = await drive_loopback_link_up( tb, l_g, subclass, scr_enable, delay_cycles=d ) @@ -612,8 +620,7 @@ async def test_jesd204b_dlat_sweep(dut): # forwarding coroutine can cause premature SYNC_S → HOLD_S transitions # when the FSM restarts after devRst deasserts. # ----------------------------------------------------------------------- - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # Drive K28.5 (CGS) to all RX GT inputs before reset. This prevents the RX # FSM from seeing stale non-K data during the next drive_loopback_link_up's # setup phase (before the new forwarding coroutine starts), which would @@ -769,7 +776,12 @@ async def test_jesd204b_resync_matrix(dut): # ----------------------------------------------------------------------- # Step 1: Initial link-up + baseline latency measurement # ----------------------------------------------------------------------- - stop_event, _ = await drive_loopback_link_up(tb, l_g, subclass, scr_enable) + stop_event, loopback_task, _ = await drive_loopback_link_up( + tb, + l_g, + subclass, + scr_enable, + ) baseline = await measure_marked_latency(tb, l_g) # ----------------------------------------------------------------------- @@ -778,22 +790,21 @@ async def test_jesd204b_resync_matrix(dut): # ----------------------------------------------------------------------- k28_5_word = 0xBCBCBCBC - async def _restart_link(se): + async def _restart_link(se, task): """Stop coroutine se, drive K28.5 to RX GT inputs, restart coroutine, pulse sysRef.""" - se.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, se, task) for _ln in range(l_g): getattr(dut, f"gtRxData_{_ln}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{_ln}_i").value = 0xF new_se = Event() - cocotb.start_soon( + new_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=new_se) ) await tb.dev_cycle(2) dut.sysRef_i.value = 1 await tb.dev_cycle(16) dut.sysRef_i.value = 0 - return new_se + return new_se, new_task # ----------------------------------------------------------------------- # Step 2: Four resync paths, each at a different LMFC offset @@ -808,8 +819,7 @@ async def _restart_link(se): # devClk edge (forwarding loop writes nSync_TX_i each cycle from nSync_RX_o). await tb.dev_cycle(lmfc_offsets[0]) # Step a1: stop coroutine and drive K28.5 (RX sees K28.5 -> exits DATA_S). - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for _ln in range(l_g): getattr(dut, f"gtRxData_{_ln}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{_ln}_i").value = 0xF @@ -819,7 +829,7 @@ async def _restart_link(se): await tb.dev_cycle(8) # synchronizer latency (~3-4 cycles) # Step a3: restart forwarding coroutine + pulse sysRef to re-link. stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(2) @@ -842,7 +852,7 @@ async def _restart_link(se): await write_rx_cdc(tb, RX_ENABLE_ADDR, enable_mask) # re-enable RX # Restart link: stop/restart coroutine + pulse sysRef (SC1 requires sysRef to advance # from IDLE_S; JesdSyncFsmRx.vhd:189-191). - stop_event = await _restart_link(stop_event) + stop_event, loopback_task = await _restart_link(stop_event, loopback_task) await wait_data_valid_all(dut, l_g, clk=dut.devClk_i, timeout_cycles=4096) latency_b = await measure_marked_latency(tb, l_g) assert latency_b == baseline, ( @@ -857,7 +867,7 @@ async def _restart_link(se): await _wait_data_valid_drop(tb, l_g) await write_tx_cdc(tb, TX_ENABLE_ADDR, enable_mask) # re-enable TX # SC1 requires sysRef for IDLE_S -> SYSREF_S on both tops. - stop_event = await _restart_link(stop_event) + stop_event, loopback_task = await _restart_link(stop_event, loopback_task) await wait_data_valid_all(dut, l_g, clk=dut.devClk_i, timeout_cycles=4096) latency_c = await measure_marked_latency(tb, l_g) assert latency_c == baseline, ( @@ -875,8 +885,7 @@ async def _restart_link(se): # Step d1: write gtReset bit (CommonCtrl bit 2) to signal the GT reset intent. await write_rx_cdc(tb, RX_COMMON_ADDR, rx_ctrl | (1 << 2)) # Step d2: stop forwarding coroutine so we can manually control rstDone. - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # Step d3: deassert rstDone on all RX GT inputs (simulate GT transceiver reset). # RX FSM DATA_S: gtReady_i=0 -> IDLE_S. for lane in range(l_g): @@ -894,7 +903,7 @@ async def _restart_link(se): # to exit DATA_S and start emitting K28.5, then 4 more cycles for kStable to recover. # Wait 16 cycles before sysRef to ensure kStable=1 when sysRef fires. stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(16) # wait for TX to exit DATA_S and kStable to recover @@ -909,8 +918,7 @@ async def _restart_link(se): ) # Stop forwarding coroutine - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # --------------------------------------------------------------------------- @@ -995,14 +1003,18 @@ def disp_err_injection_fn(cycle, lane, data, datak): # drive_loopback_link_up starts a coroutine WITHOUT injection_fn; we stop # and restart it so the injection_fn is active for subsequent cycles. # Use the _restart_link helper pattern: stop → K28.5 → new coroutine → sysRef. - stop_event, _ = await drive_loopback_link_up(tb, l_g, subclass, scr_enable) + stop_event, loopback_task, _ = await drive_loopback_link_up( + tb, + l_g, + subclass, + scr_enable, + ) # Stop the existing coroutine; restart with injection_fn (armed=False initially). k28_5_word = 0xBCBCBCBC # STOP coroutine first (so it cannot override the manual nSync_TX_i write). # Then set nSync_TX_i=0 so TX exits DATA_S. Then drive K28.5 so RX exits # DATA_S via kStable. This matches the pattern from the resync matrix path (a). - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{lane}_i").value = 0xF @@ -1010,7 +1022,7 @@ def disp_err_injection_fn(cycle, lane, data, datak): dut.nSync_TX_i.value = 0 # TX DATA_S sees nSync=0 → exits to IDLE_S await tb.dev_cycle(8) # synchronizer latency ~4 cycles, extra margin stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback( dut, l_g, clk=dut.devClk_i, injection_fn=disp_err_injection_fn, @@ -1076,13 +1088,12 @@ def disp_err_injection_fn(cycle, lane, data, datak): # SC1 requires sysRef pulse: stop current coroutine, drive K28.5, restart, pulse sysRef. k28_5_word = 0xBCBCBCBC await write_rx_cdc(tb, RX_ENABLE_ADDR, enable_mask) - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{lane}_i").value = 0xF stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(2) @@ -1127,13 +1138,12 @@ def disp_err_injection_fn(cycle, lane, data, datak): await _wait_data_valid_drop(tb, l_g) # Re-enable and pulse sysRef for SC1 re-link. await write_rx_cdc(tb, RX_ENABLE_ADDR, enable_mask) - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{lane}_i").value = 0xF stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(2) @@ -1171,8 +1181,7 @@ def disp_err_injection_fn(cycle, lane, data, datak): ) # Stop forwarding coroutine - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # --------------------------------------------------------------------------- diff --git a/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py b/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py index 2e1b3239d4..ddebbd47b8 100644 --- a/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py +++ b/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py @@ -23,6 +23,7 @@ import cocotb +from tests.axi.utils import wait_sampled_ready from tests.protocols.pgp.pgp2_test_utils import K_FCD, PgpModuleTB, signal_int, wait_for_signal from tests.protocols.pgp.pgp_test_utils import pgp_family_sources, run_pgp_wrapper_test @@ -48,10 +49,7 @@ async def drive_frame_word( tb.dut.vc0FrameEofe.value = eofe tb.dut.vc0FrameValid.value = 1 - while True: - await tb.cycle() - if signal_int(tb.dut, "vc0FrameReady") == 1: - break + await wait_sampled_ready(tb.dut.vc0FrameReady, clk=tb.clk) tb.dut.vc0FrameValid.value = 0 tb.dut.vc0FrameSof.value = 0 diff --git a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py index 5590e01264..9e14119f55 100644 --- a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py +++ b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py @@ -59,7 +59,11 @@ def __init__(self, dut): self.phy_period_ns = env_float("PHY_CLK_PERIOD_NS", default=4.0) self.pgp_period_ns = env_float("PGP_CLK_PERIOD_NS", default=4.125) if env_flag("COMMON_CLK", default=False): - start_lockstep_clocks(dut.phyClk, dut.pgpClk, period_ns=self.phy_period_ns) + self._clock_task = start_lockstep_clocks( + dut.phyClk, + dut.pgpClk, + period_ns=self.phy_period_ns, + ) else: cocotb.start_soon(Clock(dut.phyClk, self.phy_period_ns, unit="ns").start()) cocotb.start_soon(Clock(dut.pgpClk, self.pgp_period_ns, unit="ns").start()) @@ -95,8 +99,11 @@ def __init__(self, dut, signal_name: str, *, step): self.signal_name = signal_name self.step = step self.seen = False + # Lifetime observer retained by its monitor owner. + self.task = cocotb.start_soon(self.run()) async def run(self): + """Lifetime agent: observe pulses until cocotb ends the test.""" while True: await self.step() if signal_int(self.dut, self.signal_name) == 1: @@ -112,8 +119,11 @@ def __init__(self, dut, *, step, valid_name: str, field_names: tuple[str, ...]): self.valid_name = valid_name self.field_names = field_names self.beats: list[tuple[int, ...]] = [] + # Lifetime observer retained by its collector owner. + self.task = cocotb.start_soon(self.run()) async def run(self): + """Lifetime agent: collect valid beats until cocotb ends the test.""" while True: await self.step() if signal_int(self.dut, self.valid_name) == 1: @@ -224,7 +234,6 @@ async def pgp4_rx_eb_filters_skip_and_preserves_stream_order(dut): valid_name="pgpRxValid", field_names=("pgpRxHeader", "pgpRxData"), ) - cocotb.start_soon(collector.run()) data_word_a = 0x0123456789ABCDEF idle_word = pgp4_idle_word(rem_link_ready=1, pause_mask=0x1234, overflow_mask=0x00A5) @@ -286,7 +295,6 @@ async def pgp4_rx_eb_overflow_pulses_when_phy_outpaces_local_clock(dut): # regression realistic and only execute the deep fill test when the pytest # parameter sweep requests the explicit overflow-stress clock ratio. overflow_monitor = PulseMonitor(dut, "overflow", step=tb.cycle_pgp) - cocotb.start_soon(overflow_monitor.run()) # The DUT uses a 512-entry async FIFO. With the write clock much faster # than the read clock, a sustained burst should eventually overrun that @@ -311,8 +319,6 @@ async def pgp4_rx_eb_skip_disabled_passes_stream_and_link_error(dut): field_names=("pgpRxHeader", "pgpRxData"), ) link_error_monitor = PulseMonitor(dut, "linkError", step=tb.sample_pgp_cycle) - cocotb.start_soon(collector.run()) - cocotb.start_soon(link_error_monitor.run()) data_word = 0x123456789ABCDEF0 await send_phy_word(tb, header=PGP4_D_HEADER, data=data_word, link_error=1) diff --git a/tests/protocols/pgp/shared/vc_fifo_test_utils.py b/tests/protocols/pgp/shared/vc_fifo_test_utils.py index 37cd844c7a..b4b360e5fa 100644 --- a/tests/protocols/pgp/shared/vc_fifo_test_utils.py +++ b/tests/protocols/pgp/shared/vc_fifo_test_utils.py @@ -15,6 +15,7 @@ from cocotb.triggers import RisingEdge, Timer from cocotbext.axi import AxiStreamBus, AxiStreamSink +from tests.axi.utils import wait_sampled_ready def pack_bytes(data: bytes, width_bytes: int = 8) -> int: """Pack a short little-endian byte string into one AXI Stream beat.""" @@ -140,13 +141,12 @@ async def send_frame( self.dut.S_AXIS_TID.value = tid self.dut.S_AXIS_TUSER.value = tuser_last if index == len(beats) - 1 else 0 - while True: - await RisingEdge(self.source_clk) - await self.settle() - if int(self.dut.S_AXIS_TREADY.value) == 1: - if on_handshake is not None: - await on_handshake(index) - break + await wait_sampled_ready( + self.dut.S_AXIS_TREADY, + clk=self.source_clk, + ) + if on_handshake is not None: + await on_handshake(index) self.drive_source_idle() await self.cycle_source(1) diff --git a/tests/protocols/rssi/test_RssiCore.py b/tests/protocols/rssi/test_RssiCore.py index 357b1f7243..46b80cb767 100644 --- a/tests/protocols/rssi/test_RssiCore.py +++ b/tests/protocols/rssi/test_RssiCore.py @@ -321,6 +321,7 @@ async def loopback_transport( *, side: str, ) -> None: + """Lifetime agent: relay RSSI traffic until cocotb ends the test.""" dropping = False destination.set_idle() source_ready.value = 0 diff --git a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py index 8b87e209c8..c979755045 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py +++ b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py @@ -220,6 +220,7 @@ async def loopback_transport( *, drop_attr: str, ) -> None: + """Lifetime agent: relay RSSI traffic until cocotb ends the test.""" dropping = False destination.set_idle() source_ready.value = 0 diff --git a/tests/protocols/srp/srp_test_utils.py b/tests/protocols/srp/srp_test_utils.py index 5fd3384054..4e784fd876 100644 --- a/tests/protocols/srp/srp_test_utils.py +++ b/tests/protocols/srp/srp_test_utils.py @@ -146,7 +146,7 @@ async def send_packed_words(self, words: list[int], *, tdest: int = 0, prefix: s self._sig(prefix, "TLAST").value = 0 self._sig(prefix, "TUSER").value = 0 - async def _recv_response_unbounded(self, *, prefix: str) -> AxisResponse: + async def _recv_response_until_last(self, *, prefix: str) -> AxisResponse: prefix = self.sink_prefix if prefix is None else prefix self._sig(prefix, "TREADY").value = 1 words = [] @@ -154,7 +154,8 @@ async def _recv_response_unbounded(self, *, prefix: str) -> AxisResponse: tuser = [] tkeep = [] - while True: + complete = False + while not complete: await RisingEdge(self.clk) if int(self._sig(prefix, "TVALID").value) != 1: continue @@ -174,12 +175,14 @@ async def _recv_response_unbounded(self, *, prefix: str) -> AxisResponse: if hasattr(self.dut, f"{prefix}_TUSER"): tuser.append(int(self._sig(prefix, "TUSER").value)) if int(self._sig(prefix, "TLAST").value) == 1: - return AxisResponse(words=words, tdest=tdest, tuser=tuser, tkeep=tkeep) + complete = True + + return AxisResponse(words=words, tdest=tdest, tuser=tuser, tkeep=tkeep) async def recv_response(self, *, prefix: str | None = None, timeout_time: int = 20) -> AxisResponse: prefix = self.sink_prefix if prefix is None else prefix return await with_timeout( - self._recv_response_unbounded(prefix=prefix), + self._recv_response_until_last(prefix=prefix), timeout_time, "us", ) diff --git a/tests/protocols/srp/test_SrpV0AxiLite.py b/tests/protocols/srp/test_SrpV0AxiLite.py index 1550777ddf..503de73f68 100644 --- a/tests/protocols/srp/test_SrpV0AxiLite.py +++ b/tests/protocols/srp/test_SrpV0AxiLite.py @@ -87,22 +87,28 @@ async def accept_one_write(self, *, resp: AxiResp = AxiResp.OKAY) -> dict[str, i self.dut.M_AXIL_AWREADY.value = 1 self.dut.M_AXIL_WREADY.value = 1 - while "address" not in record or "data" not in record: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_AWVALID.value) and int(self.dut.M_AXIL_AWREADY.value): record["address"] = int(self.dut.M_AXIL_AWADDR.value) if int(self.dut.M_AXIL_WVALID.value) and int(self.dut.M_AXIL_WREADY.value): record["data"] = int(self.dut.M_AXIL_WDATA.value) record["strobe"] = int(self.dut.M_AXIL_WSTRB.value) + if "address" in record and "data" in record: + break + else: + raise AssertionError(f"Timed out waiting for AXI-Lite write request: {record}") self.dut.M_AXIL_AWREADY.value = 0 self.dut.M_AXIL_WREADY.value = 0 self.dut.M_AXIL_BRESP.value = int(resp) self.dut.M_AXIL_BVALID.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_BREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite write response acceptance") self.dut.M_AXIL_BVALID.value = 0 self.dut.M_AXIL_BRESP.value = 0 return record @@ -110,19 +116,24 @@ async def accept_one_write(self, *, resp: AxiResp = AxiResp.OKAY) -> dict[str, i async def accept_one_read(self, *, data: int, resp: AxiResp = AxiResp.OKAY) -> dict[str, int]: record = {} self.dut.M_AXIL_ARREADY.value = 1 - while "address" not in record: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_ARVALID.value) and int(self.dut.M_AXIL_ARREADY.value): record["address"] = int(self.dut.M_AXIL_ARADDR.value) + break + else: + raise AssertionError("Timed out waiting for AXI-Lite read request") self.dut.M_AXIL_ARREADY.value = 0 self.dut.M_AXIL_RDATA.value = data self.dut.M_AXIL_RRESP.value = int(resp) self.dut.M_AXIL_RVALID.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_RREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite read response acceptance") self.dut.M_AXIL_RVALID.value = 0 self.dut.M_AXIL_RRESP.value = 0 return record diff --git a/tests/protocols/srp/test_SrpV3AxiLite.py b/tests/protocols/srp/test_SrpV3AxiLite.py index b939c4d1bd..d65ddae6ed 100644 --- a/tests/protocols/srp/test_SrpV3AxiLite.py +++ b/tests/protocols/srp/test_SrpV3AxiLite.py @@ -85,18 +85,22 @@ async def reset(self): async def respond_one_read(self, *, data: int, resp: AxiResp = AxiResp.OKAY): self.dut.M_AXIL_ARREADY.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_ARVALID.value) and int(self.dut.M_AXIL_ARREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite read request") self.dut.M_AXIL_ARREADY.value = 0 self.dut.M_AXIL_RDATA.value = data self.dut.M_AXIL_RRESP.value = int(resp) self.dut.M_AXIL_RVALID.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_RREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite read response acceptance") self.dut.M_AXIL_RVALID.value = 0 self.dut.M_AXIL_RRESP.value = 0 diff --git a/tests/protocols/ssi/test_SsiAxiLiteMaster.py b/tests/protocols/ssi/test_SsiAxiLiteMaster.py index 5b661d707e..eb8ad6dcf3 100644 --- a/tests/protocols/ssi/test_SsiAxiLiteMaster.py +++ b/tests/protocols/ssi/test_SsiAxiLiteMaster.py @@ -62,8 +62,10 @@ def __init__(self, dut): # Run independent write and read responders so the DUT sees a realistic # AXI-Lite target rather than zero-delay combinational acks. - cocotb.start_soon(self._run_write()) - cocotb.start_soon(self._run_read()) + self._responder_tasks = ( + cocotb.start_soon(self._run_write()), + cocotb.start_soon(self._run_read()), + ) def in_reset(self) -> bool: try: @@ -87,6 +89,7 @@ async def _wait_while_reset(self): await self.cycle() async def _run_write(self): + """Lifetime agent: respond to AXI-Lite writes until the test ends.""" while True: await self._wait_while_reset() @@ -143,6 +146,7 @@ async def _run_write(self): self.dut.M_AXIL_BVALID.value = 0 async def _run_read(self): + """Lifetime agent: respond to AXI-Lite reads until the test ends.""" while True: await self._wait_while_reset() diff --git a/tests/protocols/ssi/test_SsiResizeFifoEofe.py b/tests/protocols/ssi/test_SsiResizeFifoEofe.py index b9d395bf65..1c2c854ee2 100644 --- a/tests/protocols/ssi/test_SsiResizeFifoEofe.py +++ b/tests/protocols/ssi/test_SsiResizeFifoEofe.py @@ -124,10 +124,11 @@ async def ssi_resize_fifo_eofe_test(dut): await tb.send_eofe_frame() async def wait_for_output_terminal_beat(): - while True: + terminal_seen = False + while not terminal_seen: await RisingEdge(dut.AXIS_ACLK) if int(dut.M_AXIS_TVALID.value) == 1 and int(dut.M_AXIS_TLAST.value) == 1: - return + terminal_seen = True # Wait until the wrapper presents the outgoing terminal beat. Apply the # timeout to the full condition wait so the test cannot hang indefinitely diff --git a/tests/simlink/common/simlink_multi_instance_cocotb.py b/tests/simlink/common/simlink_multi_instance_cocotb.py index f8f7e968ec..ef5b5f301d 100644 --- a/tests/simlink/common/simlink_multi_instance_cocotb.py +++ b/tests/simlink/common/simlink_multi_instance_cocotb.py @@ -79,6 +79,7 @@ def _pack(values, width): async def _memory_slaves(dut): + """Lifetime agent: serve memory requests until the owner cancels it.""" stores = [{}, {}] read_countdown = [0, 0] read_data = [0, 0] @@ -127,6 +128,7 @@ async def _memory_slaves(dut): async def _receive_monitor(dut, stream_received, sideband_opcodes, sideband_remdata): + """Lifetime agent: collect transport outputs until the owner cancels it.""" while True: await RisingEdge(dut.clock) await ReadOnly() @@ -256,7 +258,9 @@ def pending_inbound(): return missing deadline = time.monotonic() + MAX_TRAFFIC_SECONDS - while True: + running = list(peers) + inbound = pending_inbound() + while time.monotonic() < deadline: await RisingEdge(dut.clock) await ReadOnly() @@ -264,13 +268,13 @@ def pending_inbound(): inbound = pending_inbound() if not running and not inbound: break - if time.monotonic() >= deadline: - raise TimeoutError( - f"multi-instance traffic did not finish within " - f"{MAX_TRAFFIC_SECONDS}s: peers still running " - f"{[(peer.mode, peer.tag, peer.port) for peer in running]}, " - f"DUT still missing {inbound}" - ) + else: + raise TimeoutError( + f"multi-instance traffic did not finish within " + f"{MAX_TRAFFIC_SECONDS}s: peers still running " + f"{[(peer.mode, peer.tag, peer.port) for peer in running]}, " + f"DUT still missing {inbound}" + ) for peer in peers: assert peer.returncode == 0, f"peer exited with code {peer.returncode}" diff --git a/tests/simlink/native/simlink_stream_overload_probe.py b/tests/simlink/native/simlink_stream_overload_probe.py index 4ce14beb54..4dbbcade22 100644 --- a/tests/simlink/native/simlink_stream_overload_probe.py +++ b/tests/simlink/native/simlink_stream_overload_probe.py @@ -30,6 +30,7 @@ NULL_FD = os.open(os.devnull, os.O_WRONLY) os.dup2(NULL_FD, sys.stdout.fileno()) os.close(NULL_FD) +RECEIVE_TIMEOUT_SECONDS = 4.0 def _emit(event, operation, completed_messages, **fields): @@ -121,8 +122,9 @@ def _receive_probe(lib, port, cycle_sleep): context = _create(lib, port) _emit("ready", "socket_bind", 0) cycles = 0 + deadline = time.monotonic() + RECEIVE_TIMEOUT_SECONDS try: - while True: + while time.monotonic() < deadline: result, valid, _ = stream_cycle(lib, context, port, ob_ready=1) if result != 1: raise RuntimeError("Stream update failed") @@ -131,6 +133,11 @@ def _receive_probe(lib, port, cycle_sleep): _emit("received", "nonblocking_receive", 1, cycles=cycles) break time.sleep(cycle_sleep) + else: + raise TimeoutError( + f"Stream receive did not complete within {RECEIVE_TIMEOUT_SECONDS}s " + f"after {cycles} cycles" + ) finally: lib.rogueTcpStreamDestroy(context) _emit("destroyed", "socket_cleanup", 1, cycles=cycles) diff --git a/tests/xilinx/general/gt_rx_align_check_test_utils.py b/tests/xilinx/general/gt_rx_align_check_test_utils.py index 7cba9472f6..49229803de 100644 --- a/tests/xilinx/general/gt_rx_align_check_test_utils.py +++ b/tests/xilinx/general/gt_rx_align_check_test_utils.py @@ -104,7 +104,8 @@ def __init__(self, dut, *, phases): dut.M_AXI_RRESP.setimmediatevalue(0) dut.M_AXI_RDATA.setimmediatevalue(0) - cocotb.start_soon(self._run_read()) + # Lifetime DRP responder retained by its bus-model owner. + self._responder_task = cocotb.start_soon(self._run_read()) def set_phases(self, phases) -> None: """Replace the scripted phase sequence and restart from its head.""" @@ -139,6 +140,7 @@ async def _wait_while_reset(self) -> None: await self.cycle(1) async def _run_read(self) -> None: + """Lifetime agent: serve DRP reads until cocotb ends the test.""" while True: await self._wait_while_reset() @@ -214,7 +216,8 @@ def __init__( dut.resetDone.setimmediatevalue(0) dut.resetErr.setimmediatevalue(0) - cocotb.start_soon(self._run()) + # Lifetime GT peer retained by its model owner. + self._model_task = cocotb.start_soon(self._run()) def arm_error(self, *, lead_cycles: int = 0) -> None: """Make the next alignment attempt report an error.""" @@ -251,6 +254,7 @@ def _reset_requested(self) -> bool: return False async def _run(self) -> None: + """Lifetime agent: model GT buffer bypass until cocotb ends the test.""" while True: # Track the checker's reset request. resetOut is registered in the # axilClk domain, so sample it from the RX clock like real hardware. @@ -303,7 +307,8 @@ def __init__(self, dut): self.pulses = 0 self.max_width = 0 self._width = 0 - cocotb.start_soon(self._run()) + # Lifetime observer retained by its monitor owner. + self._monitor_task = cocotb.start_soon(self._run()) def reset_counts(self) -> None: self.pulses = 0 @@ -311,6 +316,7 @@ def reset_counts(self) -> None: self._width = 0 async def _run(self) -> None: + """Lifetime agent: monitor reset pulses until cocotb ends the test.""" previous = 0 while True: await RisingEdge(self.dut.axilClk) From 93933ad26381fbd571020eb9ea2dbc1b027e2899 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 10:11:47 -0700 Subject: [PATCH 40/57] test: classify sampling and terminal behavior --- .../regression-test-compliance/README.md | 78 +++++++++++++++++-- tests/README.md | 16 ++++ tests/axi/axi4/test_AxiMemTester.py | 6 +- tests/axi/axi4/test_AxiMonAxiL.py | 6 +- tests/axi/axi4/test_AxiRam.py | 6 +- tests/axi/axi4/test_AxiRateGen.py | 19 ++--- tests/axi/axi4/test_AxiReadEmulate.py | 6 +- tests/axi/axi4/test_AxiReadPathFifo.py | 6 +- tests/axi/axi4/test_AxiReadPathMux.py | 21 ++--- tests/axi/axi4/test_AxiResize.py | 13 ++-- tests/axi/axi4/test_AxiRingBuffer.py | 7 +- tests/axi/axi4/test_AxiWriteEmulate.py | 6 +- tests/axi/axi4/test_AxiWritePathFifo.py | 6 +- tests/axi/axi4/test_AxiWritePathMux.py | 18 ++--- tests/axi/axi_lite/test_AxiLiteAsync.py | 5 +- tests/axi/axi_lite/test_AxiLiteFifoPop.py | 6 +- tests/axi/axi_lite/test_AxiLiteFifoPush.py | 6 +- tests/axi/axi_lite/test_AxiLiteFifoPushPop.py | 6 +- tests/axi/axi_lite/test_AxiLiteMaster.py | 9 +-- tests/axi/axi_lite/test_AxiLiteMasterProxy.py | 6 +- .../test_AxiLiteRamSyncStatusVector.py | 6 +- tests/axi/axi_lite/test_AxiLiteRegs.py | 6 +- tests/axi/axi_lite/test_AxiLiteRespTimer.py | 6 +- tests/axi/axi_lite/test_AxiLiteRingBuffer.py | 6 +- .../axi/axi_lite/test_AxiLiteSequencerRam.py | 6 +- tests/axi/axi_lite/test_AxiLiteSlave.py | 6 +- tests/axi/axi_lite/test_AxiLiteWriteFilter.py | 9 +-- tests/axi/axi_lite/test_AxiVersion.py | 6 +- .../axi_stream/test_AxiStreamBatchingFifo.py | 7 +- .../axi/axi_stream/test_AxiStreamCombiner.py | 6 +- tests/axi/axi_stream/test_AxiStreamCompact.py | 9 +-- tests/axi/axi_stream/test_AxiStreamConcat.py | 6 +- tests/axi/axi_stream/test_AxiStreamFlush.py | 6 +- .../axi_stream/test_AxiStreamFrameBuffer.py | 7 +- .../test_AxiStreamFrameRateLimiter.py | 6 +- .../axi_stream/test_AxiStreamGearboxPack.py | 7 +- .../axi_stream/test_AxiStreamGearboxUnpack.py | 7 +- tests/axi/axi_stream/test_AxiStreamMon.py | 6 +- tests/axi/axi_stream/test_AxiStreamMonAxiL.py | 6 +- .../axi_stream/test_AxiStreamPrbsFlowCtrl.py | 6 +- .../axi/axi_stream/test_AxiStreamRepeater.py | 6 +- tests/axi/axi_stream/test_AxiStreamResize.py | 19 +++-- .../axi_stream/test_AxiStreamRingBuffer.py | 7 +- .../axi_stream/test_AxiStreamScatterGather.py | 6 +- tests/axi/axi_stream/test_AxiStreamShift.py | 6 +- .../axi/axi_stream/test_AxiStreamSplitter.py | 6 +- tests/axi/axi_stream/test_AxiStreamTap.py | 6 +- tests/axi/axi_stream/test_AxiStreamTimer.py | 6 +- .../axi_stream/test_AxiStreamTrailerAppend.py | 6 +- .../axi_stream/test_AxiStreamTrailerRemove.py | 6 +- tests/axi/bridge/test_AxiLiteToIpBus.py | 9 +-- tests/axi/bridge/test_AxiToAxiLite.py | 6 +- tests/axi/bridge/test_IpBusToAxiLite.py | 9 +-- tests/axi/bridge/test_SlvArraytoAxiLite.py | 6 +- tests/axi/dma/test_AxiStreamDma.py | 6 +- tests/axi/dma/test_AxiStreamDmaFifo.py | 6 +- tests/axi/dma/test_AxiStreamDmaRead.py | 6 +- tests/axi/dma/test_AxiStreamDmaRingRead.py | 7 +- tests/axi/dma/test_AxiStreamDmaRingWrite.py | 7 +- tests/axi/dma/test_AxiStreamDmaV2.py | 6 +- tests/axi/dma/test_AxiStreamDmaV2Desc.py | 6 +- tests/axi/dma/test_AxiStreamDmaV2Fifo.py | 6 +- .../dma/test_AxiStreamDmaV2FifoLoopback.py | 6 +- tests/axi/dma/test_AxiStreamDmaV2Read.py | 6 +- tests/axi/dma/test_AxiStreamDmaV2Write.py | 12 ++- .../dma/test_AxiStreamDmaV2WriteContinue.py | 12 ++- tests/axi/dma/test_AxiStreamDmaV2WriteMux.py | 18 ++--- tests/axi/dma/test_AxiStreamDmaWrite.py | 6 +- tests/axi/utils.py | 5 +- tests/base/fifo/test_FifoAsync.py | 7 +- tests/base/fifo/test_FifoSync.py | 7 +- tests/base/sync/sync_test_utils.py | 5 +- tests/base/sync/test_SynchronizerFifo.py | 19 +++-- tests/common/README.md | 28 +++++-- tests/common/compliance_audit.py | 32 +++++++- tests/common/compliance_baseline.json | 9 +-- tests/common/regression_utils.py | 34 ++++++++ tests/common/test_compliance_audit.py | 47 +++++++++++ tests/dsp/generic/dsp_test_utils.py | 5 +- tests/dsp/generic/test_BoxcarFilter.py | 7 +- tests/dsp/generic/test_DspPreSubMult.py | 7 +- tests/dsp/generic/test_DspSquareDiffMult.py | 7 +- .../test_FirFilterSingleChannelTiming.py | 6 +- .../ethernet/EthMacCore/ethmac_test_utils.py | 13 ++-- .../EthMacCore/test_EthCrc32Parallel.py | 13 ++-- .../EthMacCore/test_EthMacFlowCtrl.py | 6 +- .../EthMacCore/test_EthMacRxImport.py | 2 + .../EthMacCore/test_EthMacTxExport.py | 2 + .../RawEthFramer/raw_eth_test_utils.py | 5 +- .../ethernet/RoCEv2/test_EthMacRxCheckICrc.py | 6 +- .../RoCEv2/test_RoCEv2AxiStreamRdmaCore.py | 23 ++++-- tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py | 24 ++++-- .../ethernet/RoCEv2/test_RoceConfigurator.py | 12 ++- .../ethernet/RoCEv2/test_RoceResizeAndSwap.py | 9 +-- tests/protocols/batcher/batcher_test_utils.py | 21 ++--- .../coaxpress/coaxpress_test_utils.py | 17 ++-- .../coaxpress/test_CoaXPressConfig.py | 7 +- .../protocols/coaxpress/test_CoaXPressCore.py | 9 +-- .../coaxpress/test_CoaXPressEventAckMsg.py | 15 ++-- .../test_CoaXPressOverFiberBridge.py | 21 ++--- .../test_CoaXPressOverFiberBridgeTx.py | 24 +++--- tests/protocols/coaxpress/test_CoaXPressRx.py | 6 +- .../coaxpress/test_CoaXPressRxHsFsm.py | 21 ++--- .../coaxpress/test_CoaXPressRxLaneMux.py | 6 +- .../coaxpress/test_CoaXPressRxWordPacker.py | 77 +++++++++--------- tests/protocols/coaxpress/test_CoaXPressTx.py | 11 ++- .../coaxpress/test_CoaXPressTxLsFsm.py | 15 ++-- .../protocols/jesd204b/jesd204b_test_utils.py | 23 ++---- tests/protocols/jesd204b/test_Jesd16bTo32b.py | 37 ++++----- .../jesd204b/test_Jesd204bLoopback.py | 10 +-- tests/protocols/jesd204b/test_Jesd32bTo16b.py | 28 +++---- .../jesd204b/test_JesdAlignFrRepCh.py | 27 +++---- tests/protocols/jesd204b/test_JesdIlasGen.py | 12 ++- tests/protocols/jesd204b/test_JesdLmfcGen.py | 50 +++++------- tests/protocols/jesd204b/test_JesdRxLane.py | 15 ++-- tests/protocols/jesd204b/test_JesdRxReg.py | 22 +++--- .../jesd204b/test_JesdScramblerWrapper.py | 11 +-- .../protocols/jesd204b/test_JesdSyncFsmRx.py | 6 +- .../protocols/jesd204b/test_JesdSyncFsmTx.py | 6 +- tests/protocols/jesd204b/test_JesdTxLane.py | 36 ++++----- tests/protocols/jesd204b/test_JesdTxReg.py | 13 ++-- .../line_codes/line_code_test_utils.py | 5 +- .../packetizer/packetizer_test_utils.py | 30 +++---- tests/protocols/pgp/axil_test_utils.py | 6 +- tests/protocols/pgp/pgp2_test_utils.py | 5 +- .../pgp/pgp2b/test_Pgp2bCoreWrappers.py | 9 +-- .../pgp/pgp2fc/test_Pgp2fcCoreWrappers.py | 9 +-- tests/protocols/pgp/pgp4/pgp4_test_utils.py | 6 +- tests/protocols/pgp/pgp4/test_Pgp4RxEb.py | 10 +-- .../protocols/rssi/test_RssiAxiLiteRegItf.py | 6 +- tests/protocols/rssi/test_RssiChksum.py | 6 +- tests/protocols/rssi/test_RssiHeaderReg.py | 5 +- tests/protocols/rssi/test_RssiMonitor.py | 5 +- tests/protocols/rssi/test_RssiTxFsm.py | 13 ++-- tests/protocols/srp/test_SrpV0Loopback.py | 6 +- tests/protocols/ssi/ssi_test_utils.py | 20 ++--- tests/protocols/ssi/test_SsiAxiLiteMaster.py | 4 +- tests/protocols/ssi/test_SsiFifo.py | 4 +- tests/protocols/ssi/test_SsiIncrementingTx.py | 5 +- tests/protocols/ssi/test_SsiPrbs.py | 10 +-- .../common/simlink_multi_instance_cocotb.py | 5 +- tests/simlink/ghdl/test_RogueTcpStreamWrap.py | 10 +-- .../simlink/rogue/test_RogueSideBandRogue.py | 10 +++ tests/simlink/rogue/test_RogueStreamRogue.py | 10 +++ .../general/gt_rx_align_check_test_utils.py | 10 +-- tests/xilinx/general/test_GtRxAlignCheck.py | 14 ++-- 146 files changed, 911 insertions(+), 794 deletions(-) diff --git a/docs/plans/regression-test-compliance/README.md b/docs/plans/regression-test-compliance/README.md index d0ca336fc1..54bab3cb32 100644 --- a/docs/plans/regression-test-compliance/README.md +++ b/docs/plans/regression-test-compliance/README.md @@ -198,6 +198,70 @@ their existing operation-level `with_timeout()` watchdogs and fixed-length CoaXPress capture loops remain directly cycle-bounded by their requested sample count. The RDMA-core pytest node passes serially in 35.55 seconds. +Phase 4 now has explicit common primitives for its two sampling categories. +`sample_after_delta_cycles()` enters `ReadOnly()` for delta-only observation; +`sample_after_tpd()` advances real simulated time for registered outputs driven +with `after TPD_G`. A reviewed propagation helper identifies itself with a +`Propagation sampling:` docstring, allowing the advisory audit to suppress the +classification at the helper boundary without path allowlists. AXI handshake, +base synchronizer, DSP, CoaXPress, packetizer, batcher, PGP2, and Ethernet +shared helpers now use the propagation primitive. The raw timing inventory fell +from 302 to 295 sites, and 21 representative consumer nodes pass under the +normal pytest configuration in 80.51 seconds. + +The final four legacy methodology exceptions are removed. The two RoCEv2 suites +now summarize their complete traffic/congestion sweeps, independent checks, +and watchdog ownership in the standard four-part block; the two real-Rogue +SimLink contracts document their bidirectional stimulus, JSON/DUT checks, +host/simulation bounds, and unconditional child cleanup. The blocking +`missing-methodology` baseline is now zero. + +The seven post-activity bare-return findings are also resolved. Five branches +now use explicit `if`/`else` or `for`/`else` structure. The two intentionally +disconnected XLGMII placeholders keep a terminal branch only after completing +their no-output and status assertions, with an immediate `Terminal scenario:` +explanation of that complete parameter contract. The audit recognizes only +that contiguous marker and now blocks every other post-activity bare return +against a zero baseline. All 16 affected AXI, synchronizer, DSP, CoaXPress, and +Ethernet nodes pass in 142.45 seconds; all six affected SimLink wrapper nodes +pass serially in 4.32 seconds with localhost socket access. + +The AXI timing migration classifies all 93 raw edge-plus-one-nanosecond sites +across AXI4, AXI-Lite, AXI Stream, bridge, and DMA benches as real propagation +sampling through `sample_after_tpd()`. This is an exact scheduling-preserving +replacement of the default SURF `TPD_G` wait, including handshake monitors and +protocol responders; no delay was converted to delta-only `ReadOnly()` +sampling. The AXI subtree now has zero advisory timing findings and is clean +under flake8. Its full 117-node suite passes under pytest-xdist in 54.19 seconds, +and a 17-node cross-subsystem serial matrix passes in 8.58 seconds. + +The DSP, Ethernet, Xilinx-general, and SimLink timing batch classifies the next +24 sites. Default registered-output and handshake samples use +`sample_after_tpd()`, including a deliberate two-nanosecond CRC margin. The GT +alignment reset test instead uses `wait_after_edge_offset()` to identify a real +mid-cycle asynchronous-stimulus offset without mislabeling it as `TPD_G` +propagation. Those subtrees now have zero timing findings and are clean under +flake8. The full affected helper-consumer run passes 81 nodes under pytest-xdist +in 341.53 seconds, a 38-node representative serial matrix passes in 107.72 +seconds, and the localhost SimLink multi-instance node passes serially in 4.03 +seconds. + +Phase 4 is complete. The final base FIFO sites use the explicit two-nanosecond +propagation contract, and all 25 base nodes pass in 133.43 seconds. The +remaining 174 protocol sites now use `sample_after_tpd()` or a reviewed +real-time timing contract: JESD204B alignment tests retain deliberate +time-measurement and midpoint samples, while CoaXPress and RSSI monitors retain +their explicit two-nanosecond propagation margins. The complete protocol suite +passes with 464 passed and 17 existing gated skips in 2612.94 seconds. A +before/after inventory of all 46 edited base/protocol Python files reports no +changes to cocotb entrypoints, pytest functions, parameter IDs, environment +controls, skips, or timeout decorators. + +The whole active test tree now reports zero compliance findings. The +`edge-then-timer` rule has therefore joined the checked-in zero-entry blocking +baseline: new raw edge-plus-delay sequences must use the appropriate sampling +helper or carry a narrowly scoped reviewed timing contract. + ## Non-Negotiable Preservation Rules - Do not remove an existing test, parameter case, opt-in gate, or behavioral @@ -458,10 +522,10 @@ criteria. ## Immediate Next Steps -1. Begin duplicate-source cleanup by subsystem, starting with the shared AXI - wrapper paths and a fresh ruckus import. -2. Migrate `test_AxiStreamDmaV2Read.py` to the shared runner and add duplicate - source diagnostics after caller cleanup establishes the legitimate - exception set. -3. Classify `force_compile=True` callers while touching their source lists, - retaining only cases with a documented cache or source-topology need. +1. Review protocol oracle independence and assertion diagnostics, adding only + focused known-answer anchors or context that closes a demonstrated gap. +2. Split the largest suites only where a coherent ownership boundary improves + maintainability without changing public pytest nodes, selectors, or gates. +3. Correct stale commands in `scripts/setup_regression_env.sh`, ensure the + blocking compliance check runs in CI, and perform the final repository-wide + validation and preservation audit. diff --git a/tests/README.md b/tests/README.md index 786879abb3..8b0160def7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -315,6 +315,18 @@ a real nonzero `after TPD_G`, wait for that configured propagation delay and then sample the stable value. Keep this distinction visible in a shared helper; do not add an unexplained fixed delay merely to make a race disappear. +The common helpers make that choice explicit. Use +`sample_after_delta_cycles(clock)` only when the next action is a read-only +observation of logic that settles without simulated time advancing; the +coroutine returns in cocotb's read-only phase, so do not drive signals from that +phase. Use `sample_after_tpd(clock, propagation_time=..., unit=...)` when the +RTL has a real `after TPD_G` assignment. Its default is the common SURF +one-nanosecond delay, but pass the elaborated value when a test changes `TPD_G`. +For a deliberate nonzero stimulus phase offset, such as asserting an +asynchronous input between clock edges, use +`wait_after_edge_offset(clock, offset_time=..., unit=...)`. This advances real +simulated time but does not claim that the delay models `TPD_G` propagation. + Keep skip reasons and opt-in coverage explicit, and distinguish why a case is not in the default run: @@ -408,6 +420,10 @@ Before considering a new regression ready: limitation and defect-catching assertion are documented. - Inapplicable scenarios are selected out or reported as skipped; no entrypoint silently returns before exercising its named behavior. +- Parameter-specific terminal branches use ordinary structured control flow + when practical. A necessary early terminal branch has completed assertions + and an immediate `# Terminal scenario:` comment explaining why those checks + are its complete contract. - Every wait is bounded directly or by a helper with a cycle/time limit. - Reset, backpressure, sidebands, and error/boundary cases relevant to the DUT are covered or explicitly documented as out of scope. diff --git a/tests/axi/axi4/test_AxiMemTester.py b/tests/axi/axi4/test_AxiMemTester.py index 7f31a5dfad..9fe8efb6ec 100644 --- a/tests/axi/axi4/test_AxiMemTester.py +++ b/tests/axi/axi4/test_AxiMemTester.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiLiteBus, AxiLiteMaster, AxiRam, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -42,8 +43,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Reset both the control plane and the AXI memory side before starting diff --git a/tests/axi/axi4/test_AxiMonAxiL.py b/tests/axi/axi4/test_AxiMonAxiL.py index f6af0758aa..00b600744e 100644 --- a/tests/axi/axi4/test_AxiMonAxiL.py +++ b/tests/axi/axi4/test_AxiMonAxiL.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32 @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/axi4/test_AxiRam.py b/tests/axi/axi4/test_AxiRam.py index f946d7c3fa..c689be369a 100644 --- a/tests/axi/axi4/test_AxiRam.py +++ b/tests/axi/axi4/test_AxiRam.py @@ -20,7 +20,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -36,8 +37,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) diff --git a/tests/axi/axi4/test_AxiRateGen.py b/tests/axi/axi4/test_AxiRateGen.py index 8da972cc79..786945bbb7 100644 --- a/tests/axi/axi4/test_AxiRateGen.py +++ b/tests/axi/axi4/test_AxiRateGen.py @@ -25,7 +25,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiLiteBus, AxiLiteMaster, AxiRam from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -72,8 +74,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold both reset domains active together because this regression keeps @@ -107,8 +108,7 @@ async def _track_cycles(self): async def _monitor_aw(self): """Lifetime agent: record accepted write addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_handshakes.append( ( @@ -123,8 +123,7 @@ async def _monitor_aw(self): async def _monitor_w(self): """Lifetime agent: record accepted write data for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_WVALID.value) and logic_int(self.dut.M_AXI_WREADY.value): self.w_handshakes.append( ( @@ -137,8 +136,7 @@ async def _monitor_w(self): async def _monitor_ar(self): """Lifetime agent: record accepted read addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_ARVALID.value) and logic_int(self.dut.M_AXI_ARREADY.value): self.ar_handshakes.append( ( @@ -153,8 +151,7 @@ async def _monitor_ar(self): async def _monitor_r(self): """Lifetime agent: record accepted read data for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_RVALID.value) and logic_int(self.dut.M_AXI_RREADY.value): self.r_handshakes.append( ( diff --git a/tests/axi/axi4/test_AxiReadEmulate.py b/tests/axi/axi4/test_AxiReadEmulate.py index 49e4cc50c6..4298007dae 100644 --- a/tests/axi/axi4/test_AxiReadEmulate.py +++ b/tests/axi/axi4/test_AxiReadEmulate.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test @@ -37,8 +38,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/axi4/test_AxiReadPathFifo.py b/tests/axi/axi4/test_AxiReadPathFifo.py index 76a3da4cf9..9339a7610b 100644 --- a/tests/axi/axi4/test_AxiReadPathFifo.py +++ b/tests/axi/axi4/test_AxiReadPathFifo.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -55,8 +56,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.sAxiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.sAxiClk) async def wait_for(self, predicate, *, cycles: int, message: str): for _ in range(cycles): diff --git a/tests/axi/axi4/test_AxiReadPathMux.py b/tests/axi/axi4/test_AxiReadPathMux.py index 0fc08acf26..83201c6788 100644 --- a/tests/axi/axi4/test_AxiReadPathMux.py +++ b/tests/axi/axi4/test_AxiReadPathMux.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus from tests.common.regression_utils import run_surf_vhdl_test @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold reset long enough for the shim layers and the DUT state machine @@ -63,8 +63,7 @@ def start_agents(self): async def _monitor_ar(self): """Lifetime agent: record accepted read addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if int(self.dut.M_AXI_ARVALID.value) and int(self.dut.M_AXI_ARREADY.value): self.ar_handshakes.append( ( @@ -99,24 +98,20 @@ async def issue_read(self, address: int) -> bytes: getattr(self.dut, f"{self.prefix}_ARVALID").value = 1 while not int(getattr(self.dut, f"{self.prefix}_ARREADY").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_ARVALID").value = 0 getattr(self.dut, f"{self.prefix}_RREADY").value = 1 while not logic_int(getattr(self.dut, f"{self.prefix}_RVALID").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) data = int(getattr(self.dut, f"{self.prefix}_RDATA").value).to_bytes(4, "little") assert int(getattr(self.dut, f"{self.prefix}_RRESP").value) == 0 assert int(getattr(self.dut, f"{self.prefix}_RLAST").value) == 1 - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_RREADY").value = 0 return data diff --git a/tests/axi/axi4/test_AxiResize.py b/tests/axi/axi4/test_AxiResize.py index 5609e94718..125bf5037d 100644 --- a/tests/axi/axi4/test_AxiResize.py +++ b/tests/axi/axi4/test_AxiResize.py @@ -26,7 +26,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiMaster, AxiRam from tests.common.regression_utils import parameter_case, run_surf_vhdl_test @@ -61,8 +63,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset the wrapper shims and the resize state so each parameter case @@ -81,8 +82,7 @@ def start_agents(self): async def _monitor_aw(self): """Lifetime agent: record resized write metadata for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_meta.append( ( @@ -95,8 +95,7 @@ async def _monitor_aw(self): async def _monitor_ar(self): """Lifetime agent: record resized read metadata for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_ARVALID.value) and logic_int(self.dut.M_AXI_ARREADY.value): self.ar_meta.append( ( diff --git a/tests/axi/axi4/test_AxiRingBuffer.py b/tests/axi/axi4/test_AxiRingBuffer.py index 5f16b7668d..fc50f83f9c 100644 --- a/tests/axi/axi4/test_AxiRingBuffer.py +++ b/tests/axi/axi4/test_AxiRingBuffer.py @@ -21,7 +21,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiRam, AxiStreamBus, AxiStreamSink from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -45,8 +47,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.dataRst.value = 1 diff --git a/tests/axi/axi4/test_AxiWriteEmulate.py b/tests/axi/axi4/test_AxiWriteEmulate.py index e5029db29d..c3347da143 100644 --- a/tests/axi/axi4/test_AxiWriteEmulate.py +++ b/tests/axi/axi4/test_AxiWriteEmulate.py @@ -20,7 +20,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test @@ -35,8 +36,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/axi4/test_AxiWritePathFifo.py b/tests/axi/axi4/test_AxiWritePathFifo.py index 4f21c97991..d8a79e7f4b 100644 --- a/tests/axi/axi4/test_AxiWritePathFifo.py +++ b/tests/axi/axi4/test_AxiWritePathFifo.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiResp, AxiWriteBus from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -58,8 +59,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.sAxiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.sAxiClk) async def wait_for(self, predicate, *, cycles: int, message: str): for _ in range(cycles): diff --git a/tests/axi/axi4/test_AxiWritePathMux.py b/tests/axi/axi4/test_AxiWritePathMux.py index edcc0de1aa..bbb57725d4 100644 --- a/tests/axi/axi4/test_AxiWritePathMux.py +++ b/tests/axi/axi4/test_AxiWritePathMux.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiResp, AxiWriteBus from tests.common.regression_utils import run_surf_vhdl_test @@ -52,8 +53,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset both the wrapper shims and the mux state machine before any @@ -70,8 +70,7 @@ def start_agents(self): async def _monitor_aw(self): """Lifetime agent: record accepted write addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_handshakes.append( ( @@ -119,8 +118,7 @@ async def issue_write(self, address: int, payload: bytes): aw_done = False w_done = False while not (aw_done and w_done): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) aw_done = aw_done or ( int(getattr(self.dut, f"{self.prefix}_AWVALID").value) and int(getattr(self.dut, f"{self.prefix}_AWREADY").value) @@ -136,12 +134,10 @@ async def issue_write(self, address: int, payload: bytes): getattr(self.dut, f"{self.prefix}_BREADY").value = 1 while not logic_int(getattr(self.dut, f"{self.prefix}_BVALID").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) resp = int(getattr(self.dut, f"{self.prefix}_BRESP").value) - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_BREADY").value = 0 return resp diff --git a/tests/axi/axi_lite/test_AxiLiteAsync.py b/tests/axi/axi_lite/test_AxiLiteAsync.py index b9f734b971..9fbc39c835 100644 --- a/tests/axi/axi_lite/test_AxiLiteAsync.py +++ b/tests/axi/axi_lite/test_AxiLiteAsync.py @@ -28,6 +28,8 @@ import pytest from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import ( @@ -132,8 +134,7 @@ def in_reset(self) -> bool: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.mAxiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.mAxiClk) async def _wait_while_reset(self): while self.in_reset(): diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPop.py b/tests/axi/axi_lite/test_AxiLiteFifoPop.py index 96d9c8c39c..65bd70b7ff 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPop.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPop.py @@ -21,7 +21,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -43,8 +44,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset the AXI-Lite side and the exposed FIFO interface together so diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPush.py b/tests/axi/axi_lite/test_AxiLiteFifoPush.py index e54864f999..767e6eed04 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPush.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPush.py @@ -21,7 +21,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -42,8 +43,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold both the AXI-Lite and push FIFO sides in reset long enough for diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py b/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py index dc3e81c897..b1abfe8a0b 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py @@ -21,7 +21,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset all visible domains together before any descriptor-style FIFO diff --git a/tests/axi/axi_lite/test_AxiLiteMaster.py b/tests/axi/axi_lite/test_AxiLiteMaster.py index 157ba0de3c..920bc017f2 100644 --- a/tests/axi/axi_lite/test_AxiLiteMaster.py +++ b/tests/axi/axi_lite/test_AxiLiteMaster.py @@ -26,7 +26,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -65,8 +66,7 @@ def in_reset(self) -> bool: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def _wait_while_reset(self): while self.in_reset(): @@ -180,8 +180,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Drive reset and hold the request inputs idle so the state machine diff --git a/tests/axi/axi_lite/test_AxiLiteMasterProxy.py b/tests/axi/axi_lite/test_AxiLiteMasterProxy.py index 69745dff67..38630f72ff 100644 --- a/tests/axi/axi_lite/test_AxiLiteMasterProxy.py +++ b/tests/axi/axi_lite/test_AxiLiteMasterProxy.py @@ -23,7 +23,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiLiteRam, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -40,8 +41,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) diff --git a/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py b/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py index 7350d3d597..c2636d0c28 100644 --- a/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py +++ b/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32 @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.wrRst.value = 1 diff --git a/tests/axi/axi_lite/test_AxiLiteRegs.py b/tests/axi/axi_lite/test_AxiLiteRegs.py index 336ed1c219..7a48e98735 100644 --- a/tests/axi/axi_lite/test_AxiLiteRegs.py +++ b/tests/axi/axi_lite/test_AxiLiteRegs.py @@ -28,7 +28,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_flag, env_sl, parameter_case, run_surf_vhdl_test @@ -62,8 +63,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Hold the flattened register inputs stable while reset is asserted so diff --git a/tests/axi/axi_lite/test_AxiLiteRespTimer.py b/tests/axi/axi_lite/test_AxiLiteRespTimer.py index bdb625ef69..569f4b43b9 100644 --- a/tests/axi/axi_lite/test_AxiLiteRespTimer.py +++ b/tests/axi/axi_lite/test_AxiLiteRespTimer.py @@ -25,7 +25,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -55,8 +56,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(self.reset_active_value()) diff --git a/tests/axi/axi_lite/test_AxiLiteRingBuffer.py b/tests/axi/axi_lite/test_AxiLiteRingBuffer.py index e567fc4f69..4a9ad5c2a5 100644 --- a/tests/axi/axi_lite/test_AxiLiteRingBuffer.py +++ b/tests/axi/axi_lite/test_AxiLiteRingBuffer.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -47,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Return both domains and the external control pins to a known idle diff --git a/tests/axi/axi_lite/test_AxiLiteSequencerRam.py b/tests/axi/axi_lite/test_AxiLiteSequencerRam.py index d1097b2b08..679cc4c52b 100644 --- a/tests/axi/axi_lite/test_AxiLiteSequencerRam.py +++ b/tests/axi/axi_lite/test_AxiLiteSequencerRam.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiLiteRam from tests.common.regression_utils import run_surf_vhdl_test @@ -43,8 +44,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(1) diff --git a/tests/axi/axi_lite/test_AxiLiteSlave.py b/tests/axi/axi_lite/test_AxiLiteSlave.py index a49ecaea5d..6956b999ad 100644 --- a/tests/axi/axi_lite/test_AxiLiteSlave.py +++ b/tests/axi/axi_lite/test_AxiLiteSlave.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -57,8 +58,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(self.reset_active_value()) diff --git a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py index 11326b7ebb..1ad641aad9 100644 --- a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py +++ b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py @@ -26,7 +26,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -56,8 +57,7 @@ def in_reset(self) -> bool: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def _wait_while_reset(self): while self.in_reset(): @@ -132,8 +132,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(self.reset_active_value()) diff --git a/tests/axi/axi_lite/test_AxiVersion.py b/tests/axi/axi_lite/test_AxiVersion.py index 7f1a418cdd..2f3511f137 100644 --- a/tests/axi/axi_lite/test_AxiVersion.py +++ b/tests/axi/axi_lite/test_AxiVersion.py @@ -25,7 +25,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -51,8 +52,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def reset(self): self.dut.S_AXI_ARESETN.setimmediatevalue(0) diff --git a/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py b/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py index 6e648d7ef1..2e61f98dcd 100644 --- a/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py +++ b/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py @@ -21,7 +21,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -40,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/axi_stream/test_AxiStreamCombiner.py b/tests/axi/axi_stream/test_AxiStreamCombiner.py index 414fcd5340..d499ea84ce 100644 --- a/tests/axi/axi_stream/test_AxiStreamCombiner.py +++ b/tests/axi/axi_stream/test_AxiStreamCombiner.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -44,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamCompact.py b/tests/axi/axi_stream/test_AxiStreamCompact.py index 6870c4c0bd..28c58f1164 100644 --- a/tests/axi/axi_stream/test_AxiStreamCompact.py +++ b/tests/axi/axi_stream/test_AxiStreamCompact.py @@ -25,7 +25,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test @@ -54,8 +55,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -66,8 +66,7 @@ async def reset(self): async def _monitor(self): """Lifetime agent: collect compacted output beats until the test ends.""" while True: - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) if int(self.dut.M_AXIS_TVALID.value) and int(self.dut.M_AXIS_TREADY.value): self.rx_beats.append( ( diff --git a/tests/axi/axi_stream/test_AxiStreamConcat.py b/tests/axi/axi_stream/test_AxiStreamConcat.py index d4539edbcd..9fa0390d23 100644 --- a/tests/axi/axi_stream/test_AxiStreamConcat.py +++ b/tests/axi/axi_stream/test_AxiStreamConcat.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -42,8 +43,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamFlush.py b/tests/axi/axi_stream/test_AxiStreamFlush.py index 4a9cb012c3..743425062d 100644 --- a/tests/axi/axi_stream/test_AxiStreamFlush.py +++ b/tests/axi/axi_stream/test_AxiStreamFlush.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py b/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py index 57f82f3220..abd1d55e62 100644 --- a/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py +++ b/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py @@ -31,7 +31,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp, AxiStreamBus, AxiStreamSink from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks, parameter_case @@ -99,8 +101,7 @@ def from_generics(cls, dut): async def cycle(self, clk, count=1): for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) # Wait for one cycle of the slowest clock async def cycleSlowest(self, count=1): diff --git a/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py b/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py index 95facc67e9..7e6f8faf0c 100644 --- a/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py +++ b/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamGearboxPack.py b/tests/axi/axi_stream/test_AxiStreamGearboxPack.py index 554b538e6a..8cbec10ecf 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearboxPack.py +++ b/tests/axi/axi_stream/test_AxiStreamGearboxPack.py @@ -23,7 +23,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.axi.axi_stream.gearbox_reference import pack_words, words_to_bytes @@ -43,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py b/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py index 96439d0c9c..646764c2f2 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py +++ b/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py @@ -24,7 +24,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.axi.axi_stream.gearbox_reference import unpack_words, words_to_bytes @@ -44,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamMon.py b/tests/axi/axi_stream/test_AxiStreamMon.py index 0c5405f426..ab18e882d3 100644 --- a/tests/axi/axi_stream/test_AxiStreamMon.py +++ b/tests/axi/axi_stream/test_AxiStreamMon.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/axi_stream/test_AxiStreamMonAxiL.py b/tests/axi/axi_stream/test_AxiStreamMonAxiL.py index f9ca24e262..758c54cefe 100644 --- a/tests/axi/axi_stream/test_AxiStreamMonAxiL.py +++ b/tests/axi/axi_stream/test_AxiStreamMonAxiL.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiStreamBus, AxiStreamFrame, AxiStreamSource from tests.axi.utils import axil_read_u32 @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axisRst.value = 1 diff --git a/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py b/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py index 71a7c4b9c9..18da81add0 100644 --- a/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py +++ b/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamRepeater.py b/tests/axi/axi_stream/test_AxiStreamRepeater.py index 68f6b701b2..f14e82bec1 100644 --- a/tests/axi/axi_stream/test_AxiStreamRepeater.py +++ b/tests/axi/axi_stream/test_AxiStreamRepeater.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -37,8 +38,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamResize.py b/tests/axi/axi_stream/test_AxiStreamResize.py index 719f865bcb..475923249c 100644 --- a/tests/axi/axi_stream/test_AxiStreamResize.py +++ b/tests/axi/axi_stream/test_AxiStreamResize.py @@ -188,17 +188,16 @@ async def backpressure_and_reset_test(dut): tb.dut.M_AXIS_TREADY.value = 1 await send_task await tb.wait_for_output_clear(timeout_cycles=4) - return - - await send_task - tb.dut.axisRst.value = tb.reset_active_value() - await tb.wait_for_output_clear(timeout_cycles=tb.pipe_stages + 8) - assert int(tb.dut.M_AXIS_TVALID.value) == 0 - assert int(tb.dut.M_SIDE_BAND.value) == 0 + else: + await send_task + tb.dut.axisRst.value = tb.reset_active_value() + await tb.wait_for_output_clear(timeout_cycles=tb.pipe_stages + 8) + assert int(tb.dut.M_AXIS_TVALID.value) == 0 + assert int(tb.dut.M_SIDE_BAND.value) == 0 - tb.dut.axisRst.value = tb.reset_inactive_value() - tb.dut.M_AXIS_TREADY.value = 1 - await tb.cycle(2) + tb.dut.axisRst.value = tb.reset_inactive_value() + tb.dut.M_AXIS_TREADY.value = 1 + await tb.cycle(2) PARAMETER_SWEEP = [ diff --git a/tests/axi/axi_stream/test_AxiStreamRingBuffer.py b/tests/axi/axi_stream/test_AxiStreamRingBuffer.py index 2d1c96aa81..52aecbd354 100644 --- a/tests/axi/axi_stream/test_AxiStreamRingBuffer.py +++ b/tests/axi/axi_stream/test_AxiStreamRingBuffer.py @@ -22,7 +22,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp, AxiStreamBus, AxiStreamSink from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -46,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.dataRst.value = 1 diff --git a/tests/axi/axi_stream/test_AxiStreamScatterGather.py b/tests/axi/axi_stream/test_AxiStreamScatterGather.py index 606e1dca11..d20fb78f11 100644 --- a/tests/axi/axi_stream/test_AxiStreamScatterGather.py +++ b/tests/axi/axi_stream/test_AxiStreamScatterGather.py @@ -23,7 +23,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -47,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset the stream and AXI-Lite paths together so the first sequence of diff --git a/tests/axi/axi_stream/test_AxiStreamShift.py b/tests/axi/axi_stream/test_AxiStreamShift.py index 3ce2f27214..f6e04424f2 100644 --- a/tests/axi/axi_stream/test_AxiStreamShift.py +++ b/tests/axi/axi_stream/test_AxiStreamShift.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamSplitter.py b/tests/axi/axi_stream/test_AxiStreamSplitter.py index d1634f0a29..a376789eec 100644 --- a/tests/axi/axi_stream/test_AxiStreamSplitter.py +++ b/tests/axi/axi_stream/test_AxiStreamSplitter.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -44,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamTap.py b/tests/axi/axi_stream/test_AxiStreamTap.py index 41cd87142a..19cca10937 100644 --- a/tests/axi/axi_stream/test_AxiStreamTap.py +++ b/tests/axi/axi_stream/test_AxiStreamTap.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -40,8 +41,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamTimer.py b/tests/axi/axi_stream/test_AxiStreamTimer.py index 96837148b0..3ecffdb972 100644 --- a/tests/axi/axi_stream/test_AxiStreamTimer.py +++ b/tests/axi/axi_stream/test_AxiStreamTimer.py @@ -19,7 +19,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -46,8 +47,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py b/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py index 1e239455fa..3414916087 100644 --- a/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py +++ b/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -37,8 +38,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py b/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py index 98ba294264..f8df9c5086 100644 --- a/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py +++ b/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -36,8 +37,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) diff --git a/tests/axi/bridge/test_AxiLiteToIpBus.py b/tests/axi/bridge/test_AxiLiteToIpBus.py index 81f050c548..b120ac5ac7 100644 --- a/tests/axi/bridge/test_AxiLiteToIpBus.py +++ b/tests/axi/bridge/test_AxiLiteToIpBus.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -42,8 +43,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -60,8 +60,7 @@ async def _ipb_model(self): pending = None delay = 0 while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) self.dut.ipbAck.value = 0 if pending is None and int(self.dut.ipbStrobe.value): pending = { diff --git a/tests/axi/bridge/test_AxiToAxiLite.py b/tests/axi/bridge/test_AxiToAxiLite.py index 5714b1e5ca..6d3ff8f04a 100644 --- a/tests/axi/bridge/test_AxiToAxiLite.py +++ b/tests/axi/bridge/test_AxiToAxiLite.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiLiteBus, AxiLiteRam, AxiMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -40,8 +41,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold reset across the bridge and both shim layers before issuing the diff --git a/tests/axi/bridge/test_IpBusToAxiLite.py b/tests/axi/bridge/test_IpBusToAxiLite.py index ae27cbd65c..846ba93d17 100644 --- a/tests/axi/bridge/test_IpBusToAxiLite.py +++ b/tests/axi/bridge/test_IpBusToAxiLite.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteRam from tests.common.regression_utils import run_surf_vhdl_test @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -60,8 +60,7 @@ async def ipb_request(self, *, address: int, write: bool, data: int = 0): self.dut.ipbWrite.value = int(write) self.dut.ipbStrobe.value = 1 for _ in range(1024): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if int(self.dut.ipbAck.value): break else: diff --git a/tests/axi/bridge/test_SlvArraytoAxiLite.py b/tests/axi/bridge/test_SlvArraytoAxiLite.py index 5b00ce66cf..79907176cc 100644 --- a/tests/axi/bridge/test_SlvArraytoAxiLite.py +++ b/tests/axi/bridge/test_SlvArraytoAxiLite.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteRam from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Hold the producer side and AXI-Lite side in reset together so the diff --git a/tests/axi/dma/test_AxiStreamDma.py b/tests/axi/dma/test_AxiStreamDma.py index ea51d9c727..7e57be84b5 100644 --- a/tests/axi/dma/test_AxiStreamDma.py +++ b/tests/axi/dma/test_AxiStreamDma.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaFifo.py b/tests/axi/dma/test_AxiStreamDmaFifo.py index 2d682ea706..c791a5e953 100644 --- a/tests/axi/dma/test_AxiStreamDmaFifo.py +++ b/tests/axi/dma/test_AxiStreamDmaFifo.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -38,8 +39,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaRead.py b/tests/axi/dma/test_AxiStreamDmaRead.py index 05a3da4703..8c5c91674b 100644 --- a/tests/axi/dma/test_AxiStreamDmaRead.py +++ b/tests/axi/dma/test_AxiStreamDmaRead.py @@ -20,7 +20,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus from tests.common.regression_utils import run_surf_vhdl_test @@ -47,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) diff --git a/tests/axi/dma/test_AxiStreamDmaRingRead.py b/tests/axi/dma/test_AxiStreamDmaRingRead.py index 3bea0551e3..d960bc8077 100644 --- a/tests/axi/dma/test_AxiStreamDmaRingRead.py +++ b/tests/axi/dma/test_AxiStreamDmaRingRead.py @@ -20,7 +20,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import ( AxiLiteBus, AxiLiteRam, @@ -51,8 +53,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axilRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaRingWrite.py b/tests/axi/dma/test_AxiStreamDmaRingWrite.py index 0f5e83f340..2398a6e8fc 100644 --- a/tests/axi/dma/test_AxiStreamDmaRingWrite.py +++ b/tests/axi/dma/test_AxiStreamDmaRingWrite.py @@ -21,7 +21,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import ( AxiLiteBus, AxiLiteMaster, @@ -55,8 +57,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axilRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaV2.py b/tests/axi/dma/test_AxiStreamDmaV2.py index db96860f3d..9de6336258 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2.py +++ b/tests/axi/dma/test_AxiStreamDmaV2.py @@ -23,7 +23,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaV2Desc.py b/tests/axi/dma/test_AxiStreamDmaV2Desc.py index 48fc0d47bb..1febb38cbe 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Desc.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Desc.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -53,8 +54,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaV2Fifo.py b/tests/axi/dma/test_AxiStreamDmaV2Fifo.py index 77cf57578f..d2b9e757bc 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Fifo.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Fifo.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py b/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py index 4211a31acb..ee8d5a411c 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py +++ b/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py @@ -26,7 +26,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import ( AxiBus, AxiRam, @@ -58,8 +59,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/dma/test_AxiStreamDmaV2Read.py b/tests/axi/dma/test_AxiStreamDmaV2Read.py index 14cd29bb5b..158943b0b3 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Read.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Read.py @@ -23,7 +23,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus, AxiStreamBus, AxiStreamSink from tests.common.regression_utils import run_surf_vhdl_test @@ -44,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) diff --git a/tests/axi/dma/test_AxiStreamDmaV2Write.py b/tests/axi/dma/test_AxiStreamDmaV2Write.py index 86b7edc038..0748652c2e 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Write.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Write.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiWriteBus from tests.common.regression_utils import hdl_parameters_from, run_surf_vhdl_test @@ -58,8 +59,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -78,8 +78,7 @@ async def _descriptor_responder(self): max_size = int(os.environ.get("DESC_MAX_SIZE", "32"), 0) timeout = int(os.environ.get("DESC_TIMEOUT", "32"), 0) while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) self.dut.dmaWrDescAckValid.value = 0 if int(self.dut.dmaWrDescReqValid.value): self.dut.dmaWrDescAckAddress.value = int(os.environ.get("WRITE_ADDR", "0x40"), 0) @@ -95,8 +94,7 @@ async def _descriptor_responder(self): async def _monitor_aw(self): """Lifetime agent: record DMA write addresses until the test ends.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_log.append( ( diff --git a/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py b/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py index 662814b3e6..1c9c6b37d0 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py +++ b/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py @@ -28,7 +28,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiWriteBus from tests.common.regression_utils import hdl_parameters_from, run_surf_vhdl_test @@ -74,8 +75,7 @@ def buf_addr(self, i): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -93,8 +93,7 @@ async def _descriptor_responder(self): """Lifetime agent: acknowledge DMA descriptors until the test ends.""" acked = False while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) self.dut.dmaWrDescAckValid.value = 0 req = int(self.dut.dmaWrDescReqValid.value) if not req: @@ -116,8 +115,7 @@ async def _descriptor_responder(self): async def _monitor_aw(self): """Lifetime agent: record DMA write addresses until the test ends.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_log.append((int(self.dut.M_AXI_AWADDR.value), int(self.dut.M_AXI_AWLEN.value))) diff --git a/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py b/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py index 5d77dcbbd3..87c364693b 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py +++ b/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiResp, AxiWriteBus from tests.common.regression_utils import run_surf_vhdl_test @@ -75,8 +76,7 @@ async def issue_write(self, address: int, payload: bytes): aw_done = False w_done = False while not (aw_done and w_done): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) aw_done = aw_done or ( logic_int(getattr(self.dut, f"{self.prefix}_AWVALID").value) and logic_int(getattr(self.dut, f"{self.prefix}_AWREADY").value) @@ -92,11 +92,9 @@ async def issue_write(self, address: int, payload: bytes): getattr(self.dut, f"{self.prefix}_BREADY").value = 1 while not logic_int(getattr(self.dut, f"{self.prefix}_BVALID").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) resp = int(getattr(self.dut, f"{self.prefix}_BRESP").value) - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_BREADY").value = 0 return resp @@ -118,8 +116,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -139,8 +136,7 @@ def start_agents(self): async def _monitor_aw(self): """Lifetime agent: record muxed write addresses until the test ends.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_order.append(int(self.dut.M_AXI_AWADDR.value)) diff --git a/tests/axi/dma/test_AxiStreamDmaWrite.py b/tests/axi/dma/test_AxiStreamDmaWrite.py index ad23610a43..8f24f22f24 100644 --- a/tests/axi/dma/test_AxiStreamDmaWrite.py +++ b/tests/axi/dma/test_AxiStreamDmaWrite.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiWriteBus, AxiStreamBus, AxiStreamFrame, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/utils.py b/tests/axi/utils.py index 0c17184993..102eb47478 100644 --- a/tests/axi/utils.py +++ b/tests/axi/utils.py @@ -8,7 +8,7 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd def ring_buffer_axil_addr(bus_index: int, buf: int = 0, high: int = 0) -> int: @@ -46,8 +46,7 @@ async def wait_sampled_ready( # source must hold its current beat stable until a clock edge confirms that # the sink presented `TREADY`. for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(settle_time_ns, unit="ns") + await sample_after_tpd(clk, propagation_time=settle_time_ns) if int(ready_signal.value) == 1: return diff --git a/tests/base/fifo/test_FifoAsync.py b/tests/base/fifo/test_FifoAsync.py index 374089e237..33591fd770 100644 --- a/tests/base/fifo/test_FifoAsync.py +++ b/tests/base/fifo/test_FifoAsync.py @@ -38,6 +38,7 @@ hdl_parameters_from, parameter_case, run_surf_vhdl_test, + sample_after_tpd, ) @@ -87,14 +88,12 @@ async def write_word(self, value: int) -> None: self.dut.wr_en.value = 1 await RisingEdge(self.dut.wr_clk) self.dut.wr_en.value = 0 - await RisingEdge(self.dut.wr_clk) # Let FWFT outputs settle before the next operation samples status. - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.wr_clk, propagation_time=2) async def cycle_rd(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.rd_clk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.rd_clk, propagation_time=2) async def read_word(self) -> int: if self.fwft_enabled: diff --git a/tests/base/fifo/test_FifoSync.py b/tests/base/fifo/test_FifoSync.py index 4bcf9408d9..83903600e5 100644 --- a/tests/base/fifo/test_FifoSync.py +++ b/tests/base/fifo/test_FifoSync.py @@ -34,6 +34,7 @@ hdl_parameters_from, parameter_case, run_surf_vhdl_test, + sample_after_tpd, ) @@ -79,8 +80,7 @@ async def write_word(self, value: int) -> None: self.dut.wr_en.value = 1 await RisingEdge(self.dut.clk) self.dut.wr_en.value = 0 - await RisingEdge(self.dut.clk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk, propagation_time=2) async def read_word(self) -> int: if self.fwft_enabled: @@ -142,8 +142,7 @@ async def simultaneous_cycle(self, write_value: int) -> int: await RisingEdge(self.dut.clk) self.dut.wr_en.value = 0 self.dut.rd_en.value = 0 - await RisingEdge(self.dut.clk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk, propagation_time=2) return read_value diff --git a/tests/base/sync/sync_test_utils.py b/tests/base/sync/sync_test_utils.py index e35d2c7c65..3f5f56b437 100644 --- a/tests/base/sync/sync_test_utils.py +++ b/tests/base/sync/sync_test_utils.py @@ -14,7 +14,7 @@ from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer -from tests.common.regression_utils import env_flag, env_sl +from tests.common.regression_utils import env_flag, env_sl, sample_after_tpd class SynchronizerLikeTB: @@ -67,8 +67,7 @@ async def cycle(self, count: int = 1) -> None: # signal has its next rising transition". This is the core way cocotb # synchronizes Python code to simulated hardware time. for _ in range(count): - await RisingEdge(self.dut.clk) - await self.settle() + await sample_after_tpd(self.dut.clk, propagation_time=2) async def reset(self) -> None: # Drive reset active first so the DUT starts from its known reset state. diff --git a/tests/base/sync/test_SynchronizerFifo.py b/tests/base/sync/test_SynchronizerFifo.py index 20f1596cac..9f8fec95c1 100644 --- a/tests/base/sync/test_SynchronizerFifo.py +++ b/tests/base/sync/test_SynchronizerFifo.py @@ -168,18 +168,17 @@ async def data_order_test(dut): # pass-through, so ordering reduces to immediate combinational delivery. for value in values: await tb.expect_common_clock_passthrough(value) - return - - # In dual-clock mode the DUT behaves like a tiny asynchronous FIFO. Write a - # known sequence, then verify reads emerge in the same order. - for value in values: - await tb.write(value) + else: + # In dual-clock mode the DUT behaves like a tiny asynchronous FIFO. + # Write a known sequence, then verify reads emerge in the same order. + for value in values: + await tb.write(value) - observed = [] - for _ in values: - observed.append(await tb.read()) + observed = [] + for _ in values: + observed.append(await tb.read()) - assert observed == values + assert observed == values @cocotb.test(skip=not env_flag("COMMON_CLK_G", default=False)) diff --git a/tests/common/README.md b/tests/common/README.md index ea0d052552..cec6793eb8 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -136,6 +136,22 @@ Give an intentional open-ended agent a function docstring containing audit recognizes that explicit classification; do not use the marker on a finite handshake, receive loop, or transaction that needs a cycle/time limit. +Prefer ordinary `if`/`else` structure when a parameter selects different test +behavior. If a fully checked parameter-specific branch must terminate early, +put a `# Terminal scenario:` comment immediately above its bare return and state +why the assertions above are that parameter's complete contract. The blocking +audit rejects every other post-activity bare return as ambiguous. + +Use `sample_after_delta_cycles()` and `sample_after_tpd()` from +`regression_utils.py` to state why a test samples after a clock edge. The former +enters cocotb's read-only phase for delta-settled observation; the latter waits +real simulated time for a VHDL `after TPD_G` update. A reusable propagation +helper carries a `Propagation sampling:` docstring so the audit can distinguish +that reviewed timing contract from an unexplained edge-plus-timer sequence. +Use `wait_after_edge_offset()` instead when real simulated time intentionally +places stimulus between edges; its `Real-time timing:` contract is likewise +recognized without pretending that the offset is output propagation. + ## Compliance Audit And Preservation Reports `compliance_audit.py` provides a read-only structural audit and a reproducible @@ -173,12 +189,12 @@ enough to enforce structurally: methodology presence, ordinary direct-runner exceptions, literal VHDL sources duplicated from the ruckus import, and a bare entrypoint return reached before any awaited simulator activity or assertion. It also rejects an unretained non-clock `cocotb.start_soon()` call and an -unclassified `while True` loop. Intentional lifetime loops must carry the -`Lifetime agent:` docstring contract described above; finite operations must -use a direct cycle/time bound instead. -Returns after test activity remain report-only because they can be intentional -successful terminal paths and require semantic review. Run the check after -importing the HDL tree: +unclassified `while True` loop, an unexplained post-edge real-time delay, and +an ambiguous post-activity bare return. Intentional lifetime loops must carry +the `Lifetime agent:` docstring contract described above; finite operations +must use a direct cycle/time bound instead. Reviewed propagation sampling and +real-time offsets must use the named helpers or documentation contracts above. +Run the check after importing the HDL tree: ```bash make MODULES="$PWD" import diff --git a/tests/common/compliance_audit.py b/tests/common/compliance_audit.py index a05b0b56ee..e99bac66ca 100644 --- a/tests/common/compliance_audit.py +++ b/tests/common/compliance_audit.py @@ -26,8 +26,10 @@ DEFAULT_BASELINE = Path(__file__).with_name("compliance_baseline.json") ENFORCED_RULES = { + "bare-return", "direct-runner", "duplicate-imported-source", + "edge-then-timer", "early-bare-return", "missing-methodology", "open-ended-loop", @@ -42,6 +44,9 @@ METHODOLOGY_MARKER = "Test methodology:" LIFETIME_AGENT_MARKER = "Lifetime agent:" +PROPAGATION_SAMPLING_MARKER = "Propagation sampling:" +REAL_TIME_TIMING_MARKER = "Real-time timing:" +TERMINAL_SCENARIO_MARKER = "# Terminal scenario:" ENVIRONMENT_CONTROL_RE = re.compile( r"^(?:RUN_[A-Z0-9_]*_TESTS|COCOTB_TEST_FILTER|COCOTB_TESTCASE|[A-Z0-9_]*TESTCASE)$" ) @@ -124,6 +129,16 @@ def _nested_tuples_to_lists( } +def _has_terminal_scenario_marker(source_lines: list[str], return_line: int) -> bool: + for line in reversed(source_lines[max(0, return_line - 4) : return_line - 1]): + stripped = line.strip() + if not stripped.startswith("#"): + break + if stripped.startswith(TERMINAL_SCENARIO_MARKER): + return True + return False + + def _qualified_name(node: ast.AST) -> str: if isinstance(node, ast.Name): return node.id @@ -408,6 +423,7 @@ def audit_source( imported_sources: frozenset[Path] | None = None, ) -> tuple[Finding, ...]: tree = ast.parse(source, filename=path) + source_lines = source.splitlines() findings = [] functions = _top_level_functions(tree) cocotb_functions = tuple(function for function in functions if _is_cocotb_test(function)) @@ -436,6 +452,11 @@ def audit_source( for prior in function_nodes ) rule = "bare-return" if has_prior_test_activity else "early-bare-return" + if ( + rule == "bare-return" + and _has_terminal_scenario_marker(source_lines, node.lineno) + ): + continue findings.append( Finding( rule, @@ -449,6 +470,11 @@ def audit_source( for function in _all_functions(tree): parents = _parent_map(function) lifetime_agent = _is_lifetime_agent(function) + timing_docstring = ast.get_docstring(function) or "" + classified_real_time = any( + marker in timing_docstring + for marker in (PROPAGATION_SAMPLING_MARKER, REAL_TIME_TIMING_MARKER) + ) for node in _walk_function(function): if isinstance(node, ast.While) and isinstance(node.test, ast.Constant): if node.test.value is True and not lifetime_agent: @@ -479,7 +505,11 @@ def audit_source( for statements in _statement_lists(function): for first, second in zip(statements, statements[1:]): - if _await_call_name(first).endswith("RisingEdge") and _await_call_name(second).endswith("Timer"): + if ( + _await_call_name(first).endswith("RisingEdge") + and _await_call_name(second).endswith("Timer") + and not classified_real_time + ): findings.append( Finding( "edge-then-timer", diff --git a/tests/common/compliance_baseline.json b/tests/common/compliance_baseline.json index 9121980a13..694daa3f3b 100644 --- a/tests/common/compliance_baseline.json +++ b/tests/common/compliance_baseline.json @@ -1,14 +1,11 @@ { "rules": { + "bare-return": {}, "direct-runner": {}, "duplicate-imported-source": {}, + "edge-then-timer": {}, "early-bare-return": {}, - "missing-methodology": { - "tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py": 1, - "tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py": 1, - "tests/simlink/rogue/test_RogueSideBandRogue.py": 1, - "tests/simlink/rogue/test_RogueStreamRogue.py": 1 - }, + "missing-methodology": {}, "open-ended-loop": {}, "unretained-start-soon": {} }, diff --git a/tests/common/regression_utils.py b/tests/common/regression_utils.py index 47999f3f19..41b7bb1ad4 100644 --- a/tests/common/regression_utils.py +++ b/tests/common/regression_utils.py @@ -79,6 +79,40 @@ def _optional_ghdl_warning_flags() -> list[str]: ] +async def sample_after_delta_cycles(clock) -> None: + """Wait for a rising edge and sample after combinational delta settling.""" + from cocotb.triggers import ReadOnly, RisingEdge + + await RisingEdge(clock) + await ReadOnly() + + +async def sample_after_tpd( + clock, + *, + propagation_time: float = 1.0, + unit: str = "ns", +) -> None: + """Propagation sampling: wait past a real VHDL ``after TPD_G`` delay.""" + from cocotb.triggers import RisingEdge, Timer + + await RisingEdge(clock) + await Timer(propagation_time, unit=unit) + + +async def wait_after_edge_offset( + clock, + *, + offset_time: float, + unit: str = "ns", +) -> None: + """Real-time timing: move stimulus by a deliberate offset after an edge.""" + from cocotb.triggers import RisingEdge, Timer + + await RisingEdge(clock) + await Timer(offset_time, unit=unit) + + def start_lockstep_clocks(*signals, period_ns: float): import cocotb from cocotb.triggers import Timer diff --git a/tests/common/test_compliance_audit.py b/tests/common/test_compliance_audit.py index cb377192c5..7e404898b3 100644 --- a/tests/common/test_compliance_audit.py +++ b/tests/common/test_compliance_audit.py @@ -136,6 +136,23 @@ async def successful_terminal_path(dut): } +def test_audit_accepts_an_explicit_post_check_terminal_scenario(): + source = LICENSE_AND_METHODOLOGY + ''' +import cocotb + +@cocotb.test() +async def parameter_terminal_path(dut): + await exercise_named_behavior(dut) + assert dut.done.value + # Terminal scenario: these checks are the complete contract for this parameter. + return +''' + + findings = compliance_audit.audit_source(source) + + assert "bare-return" not in _rules(findings) + + def test_audit_ignores_retained_tasks_and_clock_tasks(): source = LICENSE_AND_METHODOLOGY + """ import cocotb @@ -175,6 +192,33 @@ async def receive_transaction(): ] +def test_audit_distinguishes_classified_propagation_sampling_from_ambiguous_delay(): + source = ''' +from cocotb.triggers import RisingEdge, Timer + +async def sample_after_tpd(clk): + """Propagation sampling: wait past the configured RTL output delay.""" + await RisingEdge(clk) + await Timer(1, unit="ns") + +async def ambiguous_sample(clk): + await RisingEdge(clk) + await Timer(1, unit="ns") + +async def wait_after_edge_offset(clk): + """Real-time timing: place asynchronous stimulus between clock edges.""" + await RisingEdge(clk) + await Timer(2, unit="ns") +''' + + findings = compliance_audit.audit_source(source) + delays = [finding for finding in findings if finding.rule == "edge-then-timer"] + + assert [(finding.symbol, finding.line) for finding in delays] == [ + ("ambiguous_sample", 11), + ] + + def test_audit_reports_direct_runner_except_documented_simlink_helper(): source = """ from cocotb_test.simulator import run @@ -271,6 +315,7 @@ def test_compliance_baseline_allows_cleanup_but_rejects_new_findings(): baseline = { "schema_version": compliance_audit.SCHEMA_VERSION, "rules": { + "bare-return": {}, "direct-runner": {"tests/old.py": 1}, "duplicate-imported-source": {"tests/sources.py": 2}, "early-bare-return": {"tests/ambiguous.py": 1}, @@ -343,8 +388,10 @@ def test_compliance_baseline_counts_findings_by_rule_and_path(): assert baseline == { "schema_version": compliance_audit.SCHEMA_VERSION, "rules": { + "bare-return": {}, "direct-runner": {}, "duplicate-imported-source": {"tests/source.py": 2}, + "edge-then-timer": {"tests/timing.py": 1}, "early-bare-return": {}, "missing-methodology": {}, "open-ended-loop": {}, diff --git a/tests/dsp/generic/dsp_test_utils.py b/tests/dsp/generic/dsp_test_utils.py index 869f9a9f1d..364c5456c9 100644 --- a/tests/dsp/generic/dsp_test_utils.py +++ b/tests/dsp/generic/dsp_test_utils.py @@ -12,15 +12,14 @@ from collections import deque -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd SIM_SETTLE_NS = 2 async def tick(clk, *, count: int = 1, settle_ns: int = SIM_SETTLE_NS) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(settle_ns, unit="ns") + await sample_after_tpd(clk, propagation_time=settle_ns) def signed_samples(width: int) -> list[int]: diff --git a/tests/dsp/generic/test_BoxcarFilter.py b/tests/dsp/generic/test_BoxcarFilter.py index 8179fa9537..181664d0f8 100644 --- a/tests/dsp/generic/test_BoxcarFilter.py +++ b/tests/dsp/generic/test_BoxcarFilter.py @@ -24,7 +24,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, hdl_parameters_from, parameter_case, run_surf_vhdl_test from tests.dsp.generic.dsp_test_utils import boxcar_filter_reference, to_unsigned, truncate_signed @@ -45,8 +47,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = 1 diff --git a/tests/dsp/generic/test_DspPreSubMult.py b/tests/dsp/generic/test_DspPreSubMult.py index b7af821764..b74b26e804 100644 --- a/tests/dsp/generic/test_DspPreSubMult.py +++ b/tests/dsp/generic/test_DspPreSubMult.py @@ -26,7 +26,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, env_sl, hdl_parameters_from, parameter_case, run_surf_vhdl_test from tests.dsp.generic.dsp_test_utils import signed_samples, to_unsigned, truncate_signed @@ -58,8 +60,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = self.reset_active_value() diff --git a/tests/dsp/generic/test_DspSquareDiffMult.py b/tests/dsp/generic/test_DspSquareDiffMult.py index 45995d85e4..97f8eb53e6 100644 --- a/tests/dsp/generic/test_DspSquareDiffMult.py +++ b/tests/dsp/generic/test_DspSquareDiffMult.py @@ -25,7 +25,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, env_sl, hdl_parameters_from, parameter_case, run_surf_vhdl_test from tests.dsp.generic.dsp_test_utils import signed_samples, to_unsigned, truncate_signed @@ -54,8 +56,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = self.reset_active_value() diff --git a/tests/dsp/generic/test_FirFilterSingleChannelTiming.py b/tests/dsp/generic/test_FirFilterSingleChannelTiming.py index 479cfbdd6a..c6d84c6c91 100644 --- a/tests/dsp/generic/test_FirFilterSingleChannelTiming.py +++ b/tests/dsp/generic/test_FirFilterSingleChannelTiming.py @@ -208,9 +208,9 @@ async def output_hold_test(dut): int(dut.dout.value), int(dut.sbOut.value), ) - return - - raise AssertionError("Never reached a non-zero visible FIR output to hold") + break + else: + raise AssertionError("Never reached a non-zero visible FIR output to hold") PARAMETER_SWEEP = [ diff --git a/tests/ethernet/EthMacCore/ethmac_test_utils.py b/tests/ethernet/EthMacCore/ethmac_test_utils.py index d556721070..8c4420e996 100644 --- a/tests/ethernet/EthMacCore/ethmac_test_utils.py +++ b/tests/ethernet/EthMacCore/ethmac_test_utils.py @@ -19,6 +19,7 @@ from cocotb.triggers import RisingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd # Shared EMAC helpers centralize the flattened lane ordering and the common @@ -161,8 +162,7 @@ async def recv(self, *, clk, ready_signal=None, keep_ready: bool = False) -> Ema if ready_signal is not None: ready_signal.value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if ready_signal is not None and not keep_ready: ready_signal.value = 0 return beat @@ -384,8 +384,7 @@ def start_clock(signal, *, period_ns: float = 5.0) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_dut(dut, *, clk_name: str = "ethClk", rst_name: str = "ethRst") -> None: @@ -452,8 +451,7 @@ async def send_frame_burst( await send_contiguous_frame(endpoint, frame, clk=clk) if index != len(frames) - 1: for _ in range(inter_frame_gap_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def recv_frame(endpoint: FlatEmacEndpoint, *, clk, ready_signal=None, timeout_cycles: int = 64) -> list[EmacBeat]: @@ -463,8 +461,7 @@ async def recv_frame(endpoint: FlatEmacEndpoint, *, clk, ready_signal=None, time if ready_signal is not None: ready_signal.value = 1 for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(endpoint._sig("TValid").value) == 1: beat = endpoint.snapshot() beats.append(beat) diff --git a/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py b/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py index f2b29e017c..3156c2ffb7 100644 --- a/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py +++ b/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py @@ -31,16 +31,20 @@ from cocotb.triggers import RisingEdge, Timer from tests.base.crc.crc_test_utils import crc_out_from_remainder, crc_update, pack_active_bytes -from tests.common.regression_utils import hdl_parameters_from, parameter_case, run_surf_vhdl_test +from tests.common.regression_utils import ( + hdl_parameters_from, + parameter_case, + run_surf_vhdl_test, + sample_after_tpd, +) from tests.ethernet.EthMacCore.ethmac_test_utils import ETHMAC_RTL_SOURCES async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) # The CRC block registers state with `after TPD_G`, so leave a small # margin beyond that delay before sampling outputs in Python. - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) async def apply_word(dut, *, clk, byte_width: int, payload: list[int]) -> int: @@ -54,8 +58,7 @@ async def apply_word(dut, *, clk, byte_width: int, payload: list[int]) -> int: await Timer(2, unit="ns") # The resulting CRC is available on the following edge. - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) return int(dut.crcOut.value) diff --git a/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py b/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py index fc493935e2..a3741f3d10 100644 --- a/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py +++ b/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py @@ -25,7 +25,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, parameter_case, run_surf_vhdl_test from tests.ethernet.EthMacCore.ethmac_test_utils import ETHMAC_RTL_SOURCES @@ -36,8 +37,7 @@ async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) @cocotb.test() diff --git a/tests/ethernet/EthMacCore/test_EthMacRxImport.py b/tests/ethernet/EthMacCore/test_EthMacRxImport.py index f10941372d..6aec82169a 100644 --- a/tests/ethernet/EthMacCore/test_EthMacRxImport.py +++ b/tests/ethernet/EthMacCore/test_EthMacRxImport.py @@ -79,6 +79,8 @@ async def eth_mac_rx_import_test(dut): await expect_no_output(sink, clk=bench.clk, cycles=24) assert int(dut.rxCountEn.value) == 0 assert int(dut.rxCrcError.value) == 0 + # Terminal scenario: these no-output/status checks are the complete + # contract for the intentionally disconnected XLGMII import placeholder. return min_frame = build_ethernet_frame( diff --git a/tests/ethernet/EthMacCore/test_EthMacTxExport.py b/tests/ethernet/EthMacCore/test_EthMacTxExport.py index e3f73132e5..3431b621ff 100644 --- a/tests/ethernet/EthMacCore/test_EthMacTxExport.py +++ b/tests/ethernet/EthMacCore/test_EthMacTxExport.py @@ -82,6 +82,8 @@ async def eth_mac_tx_export_test(dut): assert int(dut.txCountEn.value) == 0 assert int(dut.txUnderRun.value) == 0 assert int(dut.txLinkNotReady.value) == 0 + # Terminal scenario: these no-output/status checks are the complete + # contract for the intentionally disconnected XLGMII export placeholder. return min_frame = build_ethernet_frame( diff --git a/tests/ethernet/RawEthFramer/raw_eth_test_utils.py b/tests/ethernet/RawEthFramer/raw_eth_test_utils.py index 344b675bcb..f62d046537 100644 --- a/tests/ethernet/RawEthFramer/raw_eth_test_utils.py +++ b/tests/ethernet/RawEthFramer/raw_eth_test_utils.py @@ -16,6 +16,8 @@ from cocotbext.axi import AxiLiteBus, AxiLiteMaster from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.axi.utils import axil_read_u32, axil_write_u32, wait_sampled_ready from tests.ethernet.EthMacCore.ethmac_test_utils import ( FlatEmacEndpoint, @@ -359,6 +361,5 @@ async def wait_lookup_request( async def pulse_signal(signal, *, clk, cycles: int = 1) -> None: signal.value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) signal.value = 0 diff --git a/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py b/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py index 993b8d4bf8..3d3965c894 100644 --- a/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py +++ b/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py @@ -29,7 +29,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test @@ -59,8 +60,7 @@ async def capture_crc_errors(dut, *, clk, timeout_cycles: int = 64) -> list[int] errors = [] for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(dut.mAxisTValid.value) == 1 and int(dut.mAxisTReady.value) == 1: errors.append(int(dut.mAxisCrcError.value)) if int(dut.mAxisTLast.value) == 1: diff --git a/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py b/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py index 7c9faa4e2e..c742a0fb6e 100644 --- a/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py +++ b/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py @@ -8,8 +8,20 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## -# Test methodology -# ---------------- +# Test methodology: +# - Sweep: Cover single and high-occupancy traffic, response/work-request +# backpressure, engine stall/restart, partial and oversized frames, dynamic +# lengths, partial-byte enables, retry replay, ring wrap, and counter reset. +# - Stimulus: Push deterministic 32-byte AXI Stream beats while a configurable +# in-order engine peer accepts work requests, issues DMA reads, drains their +# responses, and returns work completions. +# - Checks: Scoreboard every replayed byte, first/last marker, byte enable, +# opcode/immediate field, error indication, work-request length, and relevant +# AXI-Lite success/error/oversize/frame counters. +# - Timing: Engine latency and backpressure build FIFO occupancy deliberately; +# transaction progress has cycle limits and each liveness scenario has a +# simulated-time watchdog so a datapath wedge fails diagnostically. +# # RoCEv2AxiStreamRdma buffers an inbound AXI-Stream payload in a store-and-forward # repack FIFO, issues one RDMA-SEND-with-immediate work request per complete packet, # serves the engine's DMA read by draining that packet into the 290-bit dmaReadResp, counts @@ -30,7 +42,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -134,8 +148,7 @@ def __init__(self, dut): dut.S_WORKCOMP_ID.value = 0 async def _edge(self): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def _wait_asserted(self, signal, name: str) -> None: for _ in range(PROGRESS_TIMEOUT_CYCLES): diff --git a/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py b/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py index 27cd1288a0..743bcbfcea 100644 --- a/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py +++ b/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py @@ -8,8 +8,18 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## -# Test methodology -# ---------------- +# Test methodology: +# - Sweep: Compare the no-congestion baseline with one CNP event while holding +# rate recovery outside the observation window. +# - Stimulus: Drive full-rate one-beat frames, compress the DCQCN update +# intervals through AXI-Lite, and inject one synchronized CNP pulse. +# - Checks: Use both the AXI-Lite Rc/cnpCnt state and accepted M_AXIS beat rate; +# require the baseline token rate and the expected approximately 50-percent +# reduction after one CNP. +# - Timing: Count accepted beats over fixed 4000-clock windows. Each scenario is +# enclosed by a simulated-time watchdog and cancels its lifetime source on +# both success and failure. +# # DCQCN CNP rate-control bench for RoCEv2Dcqcn (via RoCEv2DcqcnWrapper). The bench # substitutes the RoCEv2Engine.cnp_received source with a TB-driven flat `cnp` port # and proves the congestion-control behavior with a DUAL PREDICATE: @@ -38,7 +48,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -111,8 +123,7 @@ def __init__(self, dut): dut.M_AXIS_TREADY.value = 1 async def _edge(self): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = 1 @@ -150,8 +161,7 @@ async def count_beats(self, window_clk): dut = self.dut n = 0 for _ in range(window_clk): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if int(dut.M_AXIS_TVALID.value) and int(dut.M_AXIS_TREADY.value): n += 1 return n diff --git a/tests/ethernet/RoCEv2/test_RoceConfigurator.py b/tests/ethernet/RoCEv2/test_RoceConfigurator.py index 1de72ce663..223fe268f0 100644 --- a/tests/ethernet/RoCEv2/test_RoceConfigurator.py +++ b/tests/ethernet/RoCEv2/test_RoceConfigurator.py @@ -27,7 +27,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32, wait_sampled_ready @@ -53,8 +54,7 @@ def __init__(self, dut): async def cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = 1 @@ -69,12 +69,10 @@ def start_agents(self): async def wait_for_metadata_request(self, *, timeout_cycles: int = 64) -> int: self.dut.M_META_REQ_TREADY.value = 1 for _ in range(timeout_cycles): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) if int(self.dut.M_META_REQ_TVALID.value) == 1: value = int(self.dut.M_META_REQ_TDATA.value) - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) self.dut.M_META_REQ_TREADY.value = 0 return value self.dut.M_META_REQ_TREADY.value = 0 diff --git a/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py b/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py index 03ad8cc58f..3c21dabf25 100644 --- a/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py +++ b/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py @@ -30,7 +30,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import env_flag, parameter_case, run_surf_vhdl_test @@ -70,14 +71,12 @@ def __init__(self, dut): async def cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def _monitor_sideband(self): """Lifetime agent: collect RoCE sidebands until the test ends.""" while True: - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) if int(self.dut.M_AXIS_TVALID.value) == 1 and int(self.dut.M_AXIS_TREADY.value) == 1: self.rx_sidebands.append(int(self.dut.M_SIDE_BAND.value)) diff --git a/tests/protocols/batcher/batcher_test_utils.py b/tests/protocols/batcher/batcher_test_utils.py index 38b5d544eb..84bc1c5f83 100644 --- a/tests/protocols/batcher/batcher_test_utils.py +++ b/tests/protocols/batcher/batcher_test_utils.py @@ -14,9 +14,10 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd @dataclass @@ -83,8 +84,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 256) -> AxisBeat: await Timer(1, unit="ns") if int(self._sig("TVALID").value) == 1: return self.snapshot() - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(self._sig("TVALID").value) == 1: return self.snapshot() raise AssertionError(f"Timed out waiting for {self.prefix} valid") @@ -92,8 +92,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 256) -> AxisBeat: async def recv(self, *, clk, keep_ready: bool = False) -> AxisBeat: self._sig("TREADY").value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if not keep_ready: self._sig("TREADY").value = 0 return beat @@ -105,8 +104,7 @@ def start_batcher_clock(dut, *, period_ns: float = 5.0) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_batcher_dut(dut, *, cycles: int = 4) -> None: @@ -224,8 +222,7 @@ async def recv_beats(endpoint: FlatAxisEndpoint, *, clk, count: int) -> list[Axi async def expect_no_valid(endpoint: FlatAxisEndpoint, *, clk, cycles: int) -> None: endpoint._sig("TREADY").value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert int(endpoint._sig("TVALID").value) == 0 endpoint._sig("TREADY").value = 0 @@ -242,12 +239,10 @@ async def recv_until_last_with_backpressure( for _ in range(max_beats): beat = await endpoint.wait_valid(clk=clk) for _ in range(hold_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert endpoint.snapshot() == beat endpoint._sig("TREADY").value = 1 - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) endpoint._sig("TREADY").value = 0 beats.append(beat) if beat.last: diff --git a/tests/protocols/coaxpress/coaxpress_test_utils.py b/tests/protocols/coaxpress/coaxpress_test_utils.py index fc0d6ce969..e700efeef2 100644 --- a/tests/protocols/coaxpress/coaxpress_test_utils.py +++ b/tests/protocols/coaxpress/coaxpress_test_utils.py @@ -16,9 +16,9 @@ import cocotb from cocotb.clock import Clock from cocotb.handle import Immediate -from cocotb.triggers import RisingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd CXP_IDLE = 0xB53C3CBC @@ -158,8 +158,7 @@ def set_initial_values(dut, values: dict[str, int]) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_signals(dut, *, clk, reset_names: tuple[str, ...], assert_cycles: int = 4, release_cycles: int = 2) -> None: @@ -208,8 +207,7 @@ async def send_rx_word( dut.rxLinkUp.value = link_up dut.rxData.value = data dut.rxDataK.value = data_k - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if capture is not None and valid_name is not None: snapshot = pulse_snapshot(dut, valid_name=valid_name, field_names=field_names) if snapshot is not None: @@ -255,8 +253,7 @@ async def collect_stream_bytes( if ready_name is not None: getattr(dut, ready_name).value = 1 for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(getattr(dut, valid_name).value) == 1: payload.append(int(getattr(dut, data_name).value)) if len(payload) >= count: @@ -293,8 +290,7 @@ async def send_axis_beats_no_ready( getattr(dut, f"{prefix}TData").value = beat.data getattr(dut, f"{prefix}TKeep").value = beat.keep getattr(dut, f"{prefix}TLast").value = beat.last - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if capture is not None and valid_name is not None: snapshot = pulse_snapshot(dut, valid_name=valid_name, field_names=field_names) if snapshot is not None: @@ -315,8 +311,7 @@ async def collect_pulses( ) -> list[dict[str, int]]: observed: list[dict[str, int]] = [] for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) snapshot = pulse_snapshot(dut, valid_name=valid_name, field_names=field_names) if snapshot is not None: observed.append(snapshot) diff --git a/tests/protocols/coaxpress/test_CoaXPressConfig.py b/tests/protocols/coaxpress/test_CoaXPressConfig.py index 9e5d598bf1..6cdbcebdb5 100644 --- a/tests/protocols/coaxpress/test_CoaXPressConfig.py +++ b/tests/protocols/coaxpress/test_CoaXPressConfig.py @@ -23,7 +23,9 @@ # rather than assuming an ideal one-cycle transfer through the assembly. import cocotb -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -52,8 +54,7 @@ async def _drive_cfg_rx_completion(dut, value: int, *, hold_cycles: int = 8) -> dut.cfgRxTData.value = value dut.cfgRxTValid.value = 1 for _ in range(hold_cycles): - await RisingEdge(dut.cfgClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.cfgClk) dut.cfgRxTValid.value = 0 dut.cfgRxTData.value = 0 diff --git a/tests/protocols/coaxpress/test_CoaXPressCore.py b/tests/protocols/coaxpress/test_CoaXPressCore.py index 66fd3833c9..8357a17db9 100644 --- a/tests/protocols/coaxpress/test_CoaXPressCore.py +++ b/tests/protocols/coaxpress/test_CoaXPressCore.py @@ -29,11 +29,12 @@ import os import cocotb -from cocotb.triggers import Event, RisingEdge, Timer, with_timeout +from cocotb.triggers import Event, with_timeout from cocotb.utils import get_sim_time from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.common.regression_utils import env_flag, env_int, run_surf_vhdl_test, start_lockstep_clocks +from tests.common.regression_utils import sample_after_tpd from tests.protocols.coaxpress.coaxpress_test_utils import ( CXP_IDLE, CXP_IDLE_K, @@ -197,8 +198,7 @@ async def _send_image_frame( async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict[str, int], key: str) -> None: """Lifetime agent: count asserted cycles until the owner sets stop_event.""" while True: - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if stop_event.is_set(): return counts[key] += int(signal.value) @@ -207,8 +207,7 @@ async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict async def _trace_first_signal_high(signal, clk, stop_event: Event, trace: dict[str, object], capture) -> None: """Lifetime agent: observe a signal until the owner sets stop_event.""" while True: - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if stop_event.is_set(): return if trace["seen"] or int(signal.value) == 0: diff --git a/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py b/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py index e37fa725a9..4826573ecc 100644 --- a/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py +++ b/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py @@ -22,7 +22,8 @@ # accepted handshakes once `TREADY` is asserted. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -54,16 +55,14 @@ def _expected_event_ack_bytes(tag: int) -> list[tuple[int, int, int]]: async def _pulse_event_ack(dut, tag: int) -> None: dut.eventTag.value = tag dut.eventAck.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.eventAck.value = 0 async def _collect_handshakes(dut, *, count: int, timeout_cycles: int) -> list[tuple[int, int, int]]: observed: list[tuple[int, int, int]] = [] for _ in range(timeout_cycles): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if int(dut.eventAckTValid.value) == 1 and int(dut.eventAckTReady.value) == 1: observed.append( ( @@ -93,8 +92,7 @@ async def coaxpress_event_ack_msg_serialize_and_backpressure_test(dut): stalled_byte = None for _ in range(8): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if int(dut.eventAckTValid.value) == 1: sample = ( int(dut.eventAckTData.value), @@ -109,8 +107,7 @@ async def coaxpress_event_ack_msg_serialize_and_backpressure_test(dut): assert stalled_byte == (word_to_bytes(CXP_SOP)[0], 1, 0) for _ in range(2): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) assert ( int(dut.eventAckTData.value), int(dut.eventAckTK.value), diff --git a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py index 75e655f89b..e9abb61a35 100644 --- a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py +++ b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py @@ -25,7 +25,8 @@ # checks robust to gearbox latency while still validating real output order. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -98,15 +99,13 @@ async def _setup_bridge(dut) -> None: async def _drive_rx64(dut, rxd: int, rxc: int) -> None: dut.xgmiiRxd.value = rxd dut.xgmiiRxc.value = rxc - await RisingEdge(dut.rxClk156) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk156) async def _capture_rx_words(dut, *, cycles: int) -> list[tuple[int, int]]: observed: list[tuple[int, int]] = [] for _ in range(cycles): - await RisingEdge(dut.rxClk312) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk312) sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): observed.append(sample) @@ -125,14 +124,12 @@ async def coaxpress_over_fiber_bridge_top_level_integration_test(dut): async def capture_tx_words(cycles: int) -> None: for _ in range(cycles): - await RisingEdge(dut.txClk156) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk156) tx_observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) async def capture_rx_words(cycles: int) -> None: for _ in range(cycles): - await RisingEdge(dut.rxClk312) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk312) sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): rx_observed.append(sample) @@ -143,10 +140,8 @@ async def capture_rx_words(cycles: int) -> None: dut.txLsData.value = 0xA5 dut.txLsDataK.value = 0 dut.txLsValid.value = 1 - await RisingEdge(dut.txClk312) - await Timer(1, unit="ns") - await RisingEdge(dut.txClk312) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk312) + await sample_after_tpd(dut.txClk312) dut.txLsValid.value = 0 await cycle(dut.rxClk156, 3) diff --git a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py index 3ae2b30e85..94f55efc6a 100644 --- a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py +++ b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py @@ -21,7 +21,8 @@ # actual start, payload, terminate, and return-to-idle ordering. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -77,8 +78,7 @@ async def coaxpress_over_fiber_bridge_tx_packet_format_test(dut): async def capture_words(count: int) -> None: while len(observed) < count: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) capture = cocotb.start_soon(capture_words(20)) @@ -86,8 +86,7 @@ async def capture_words(count: int) -> None: dut.txLsData.value = 0xA5 dut.txLsDataK.value = 0 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await cycle(dut.clk, 6) @@ -96,8 +95,7 @@ async def capture_words(count: int) -> None: dut.txLsData.value = 0x5C dut.txLsDataK.value = 1 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await capture @@ -147,8 +145,7 @@ async def coaxpress_over_fiber_bridge_tx_partial_lane_enable_test(dut): async def capture_words(count: int) -> None: while len(observed) < count: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) capture = cocotb.start_soon(capture_words(8)) @@ -156,8 +153,7 @@ async def capture_words(count: int) -> None: dut.txLsData.value = CXP_K28_1 dut.txLsDataK.value = 1 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await capture @@ -195,8 +191,7 @@ async def coaxpress_over_fiber_bridge_tx_lane_enable_idle_rotation_test(dut): async def capture_words(count: int) -> None: while len(observed) < count: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) async def send_byte(byte: int, lane_enable: int) -> None: @@ -204,8 +199,7 @@ async def send_byte(byte: int, lane_enable: int) -> None: dut.txLsData.value = byte dut.txLsDataK.value = 0 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await cycle(dut.clk, 4) diff --git a/tests/protocols/coaxpress/test_CoaXPressRx.py b/tests/protocols/coaxpress/test_CoaXPressRx.py index 0ae16869e5..08f688ac22 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRx.py +++ b/tests/protocols/coaxpress/test_CoaXPressRx.py @@ -26,10 +26,11 @@ import os import cocotb -from cocotb.triggers import Event, RisingEdge, Timer +from cocotb.triggers import Event import pytest from tests.common.regression_utils import env_flag, env_int, parameter_case, run_surf_vhdl_test, start_lockstep_clocks +from tests.common.regression_utils import sample_after_tpd from tests.protocols.coaxpress.coaxpress_test_utils import ( CXP_EOP, CXP_IDLE, @@ -380,8 +381,7 @@ async def _send_one_lane_frame( async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict[str, int], key: str) -> None: """Lifetime agent: count asserted cycles until the owner sets stop_event.""" while True: - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if stop_event.is_set(): return counts[key] += int(signal.value) diff --git a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py index 9f0a14ce73..3df1d76014 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py @@ -25,7 +25,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import env_int, parameter_case, run_surf_vhdl_test @@ -247,8 +248,7 @@ async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(6): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [{"hdrTData": _expected_header_data(), "hdrTLast": 1, "hdrTSof": 1}], ( @@ -316,8 +316,7 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(6): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [{"hdrTData": _expected_header_data(), "hdrTLast": 1, "hdrTSof": 1}], ( @@ -393,8 +392,7 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(6): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [{"hdrTData": _expected_header_data(), "hdrTLast": 1, "hdrTSof": 1}], ( @@ -481,8 +479,7 @@ async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(8): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [ @@ -612,8 +609,7 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): dut.sAxisTLast.value = 0 shared_beat_cycles = 0 for _ in range(1024): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) shared_beat_cycles += 1 error_seen |= int(dut.rxFsmError.value) == 1 trace.append( @@ -641,8 +637,7 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(8): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) error_seen |= int(dut.rxFsmError.value) == 1 trace.append( f"idle ready={int(dut.sAxisTReady.value)} err={int(dut.rxFsmError.value)} " diff --git a/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py b/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py index bba263f324..114f59d8b4 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py @@ -23,7 +23,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_int, parameter_case, run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import pack_words, reset_dut, start_clock @@ -80,8 +81,7 @@ async def coaxpress_rx_lane_mux_round_robin_test(dut): _set_lane_inputs(dut, current, num_lanes=num_lanes) dut.mAxisTReady.value = 0 if cycle_index < 2 else 1 - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) ready_bits = int(dut.sAxisTReady.value) for lane, queue in enumerate(lane_queues): diff --git a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py index de253cc33e..a41adbd105 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py @@ -76,51 +76,50 @@ async def coaxpress_rx_word_packer_repack_test(dut): {"mAxisTData": 0x11223344, "mAxisTKeep": 0xF, "mAxisTLast": 0}, {"mAxisTData": 0x55667788, "mAxisTKeep": 0xF, "mAxisTLast": 1}, ] - return - - # The wider case intentionally starts on lane 1, fills one output beat, - # then spills into a short final beat on the next cycle. - await send_axis_beats_no_ready( - dut, - beats=[ - AxisBeat( - data=pack_words([0x0, 0xAAA00001, 0xBBB00002, 0xCCC00003]), - keep=0xFFF0, - last=0, - ), - AxisBeat( - data=pack_words([0xDDD00004, 0xEEE00005, 0xFFF00006]), - keep=0x0FFF, - last=1, - ), - ], - clk=dut.rxClk, - capture=observed, - valid_name="mAxisTValid", - field_names=("mAxisTData", "mAxisTKeep", "mAxisTLast"), - ) - observed.extend( - await collect_pulses( + else: + # The wider case intentionally starts on lane 1, fills one output beat, + # then spills into a short final beat on the next cycle. + await send_axis_beats_no_ready( dut, + beats=[ + AxisBeat( + data=pack_words([0x0, 0xAAA00001, 0xBBB00002, 0xCCC00003]), + keep=0xFFF0, + last=0, + ), + AxisBeat( + data=pack_words([0xDDD00004, 0xEEE00005, 0xFFF00006]), + keep=0x0FFF, + last=1, + ), + ], clk=dut.rxClk, - cycles=6, + capture=observed, valid_name="mAxisTValid", field_names=("mAxisTData", "mAxisTKeep", "mAxisTLast"), ) - ) + observed.extend( + await collect_pulses( + dut, + clk=dut.rxClk, + cycles=6, + valid_name="mAxisTValid", + field_names=("mAxisTData", "mAxisTKeep", "mAxisTLast"), + ) + ) - assert observed == [ - { - "mAxisTData": pack_words([0xAAA00001, 0xBBB00002, 0xCCC00003, 0xDDD00004]), - "mAxisTKeep": keep_for_words(4), - "mAxisTLast": 0, - }, - { - "mAxisTData": pack_words([0xEEE00005, 0xFFF00006]), - "mAxisTKeep": keep_for_words(2), - "mAxisTLast": 1, - }, - ] + assert observed == [ + { + "mAxisTData": pack_words([0xAAA00001, 0xBBB00002, 0xCCC00003, 0xDDD00004]), + "mAxisTKeep": keep_for_words(4), + "mAxisTLast": 0, + }, + { + "mAxisTData": pack_words([0xEEE00005, 0xFFF00006]), + "mAxisTKeep": keep_for_words(2), + "mAxisTLast": 1, + }, + ] @cocotb.test(skip=env_int("NUM_LANES_G", default=1) == 1) diff --git a/tests/protocols/coaxpress/test_CoaXPressTx.py b/tests/protocols/coaxpress/test_CoaXPressTx.py index 8bf428bd1d..51b9a69260 100644 --- a/tests/protocols/coaxpress/test_CoaXPressTx.py +++ b/tests/protocols/coaxpress/test_CoaXPressTx.py @@ -26,6 +26,8 @@ import cocotb from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -83,15 +85,13 @@ async def _drive_cfg_packet(dut, beats: list[tuple[int, int]]) -> None: async def _pulse_event_ack(dut, tag: int) -> None: dut.eventTag.value = tag dut.eventAck.value = 1 - await RisingEdge(dut.cfgClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.cfgClk) dut.eventAck.value = 0 async def _pulse_sw_trigger(dut) -> None: dut.swTrig.value = 1 - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) dut.swTrig.value = 0 @@ -99,8 +99,7 @@ async def _collect_tx_bytes(dut, *, count: int, timeout_cycles: int) -> tuple[li observed: list[tuple[int, int, int]] = [] tx_trig_drop_seen = False for cycle_index in range(timeout_cycles): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txTrigDrop.value) == 1: tx_trig_drop_seen = True if int(dut.txLsValid.value) == 1: diff --git a/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py b/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py index dd04174ceb..9e6ca5e747 100644 --- a/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py @@ -23,7 +23,8 @@ # so cadence and serialized ordering are checked on the real byte timeline. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test @@ -64,8 +65,7 @@ async def _drive_cfg_bytes(dut, beats: list[tuple[int, int]]) -> None: async def _collect_strobes(dut, *, count: int, timeout_cycles: int) -> list[tuple[int, int, int]]: observed: list[tuple[int, int, int]] = [] for cycle_index in range(timeout_cycles): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txStrobe.value) == 1: observed.append((cycle_index, int(dut.txData.value), int(dut.txDataK.value))) if len(observed) == count: @@ -75,8 +75,7 @@ async def _collect_strobes(dut, *, count: int, timeout_cycles: int) -> list[tupl async def _pulse_trigger(dut) -> None: dut.txTrig.value = 1 - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) dut.txTrig.value = 0 @@ -143,8 +142,7 @@ async def pulse_again_mid_message() -> None: strobes: list[tuple[int, int, int]] = [] tx_trig_drop_seen = False for cycle_index in range(1400): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txTrigDrop.value) == 1: tx_trig_drop_seen = True if int(dut.txStrobe.value) == 1: @@ -226,8 +224,7 @@ async def coaxpress_tx_ls_fsm_pulse_width_update_terminates_active_trigger_test( tx_trig_drop_seen = False for cycle_index in range(1800): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txTrigDrop.value) == 1: tx_trig_drop_seen = True if int(dut.txStrobe.value) == 1: diff --git a/tests/protocols/jesd204b/jesd204b_test_utils.py b/tests/protocols/jesd204b/jesd204b_test_utils.py index 9a36afe539..aa7f8a9805 100644 --- a/tests/protocols/jesd204b/jesd204b_test_utils.py +++ b/tests/protocols/jesd204b/jesd204b_test_utils.py @@ -40,7 +40,7 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd from collections import deque from tests.common.regression_utils import run_surf_vhdl_test # noqa: F401 – re-exported for bench files @@ -702,8 +702,7 @@ async def drive_gt_lane_from_timeline( for data_32b, datak_4b in timeline[segment][start_idx:]: data_port.value = data_32b datak_port.value = datak_4b - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def wait_nSync(dut, *, value: int, clk, timeout_cycles: int = 128) -> None: @@ -723,8 +722,7 @@ async def wait_nSync(dut, *, value: int, clk, timeout_cycles: int = 128) -> None timeout_cycles: Maximum rising edges to wait. """ for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(dut.nSync_o.value) == value: return raise AssertionError( @@ -751,8 +749,7 @@ async def wait_data_valid_all( timeout_cycles: Maximum rising edges to wait. """ for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if all( int(getattr(dut, f"dataValid_{i}_o").value) == 1 for i in range(l_g) @@ -814,8 +811,7 @@ async def forward_gt_loopback( ] cycle = 0 while stop_event is None or not stop_event.is_set(): - await RisingEdge(clk) - await Timer(1, unit="ns") # TPD_G=1 ns registered-output settle + await sample_after_tpd(clk) # TPD_G=1 ns registered-output settle # nSync forwarding: RX nSync_o (sl) -> TX nSync_TX_i (slv L_G-1:0). # Replicate single-bit nSync_RX_o to all l_g TX lanes so both lanes advance # through SYNC_S->ILAS when nSync_o asserts. Writing plain int(nSync_RX_o) @@ -922,8 +918,7 @@ async def measure_lmfc_period(dut, *, clk, timeout_cycles: int = 512) -> int: """ # Wait for first rising edge of lmfc_o for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if dut.lmfc_o.value == 1: break else: @@ -934,8 +929,7 @@ async def measure_lmfc_period(dut, *, clk, timeout_cycles: int = 512) -> int: # Count cycles to second rising edge count = 0 for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) count += 1 if dut.lmfc_o.value == 1: return count @@ -969,8 +963,7 @@ def __init__(self, dut, *, clock_period_ns: float = CLOCK_PERIOD_NS) -> None: async def cycle(self, count: int = 1) -> None: """Advance count clock cycles, settling 1 ns after each rising edge.""" for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self, cycles: int = 4) -> None: """Assert rst for `cycles` clock cycles, then deassert.""" diff --git a/tests/protocols/jesd204b/test_Jesd16bTo32b.py b/tests/protocols/jesd204b/test_Jesd16bTo32b.py index 82cc945c53..b6f4540ee1 100644 --- a/tests/protocols/jesd204b/test_Jesd16bTo32b.py +++ b/tests/protocols/jesd204b/test_Jesd16bTo32b.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( hdl_parameters_from, @@ -83,26 +85,22 @@ async def reset(self) -> None: self.dut.rdRst.value = 1 # Hold reset across several wrClk cycles for _ in range(6): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) self.dut.wrRst.value = 0 # Hold rdRst for a couple more rdClk cycles to be safe for _ in range(4): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) self.dut.rdRst.value = 0 # Quiet settling time after reset deassertion for _ in range(4): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) async def write_word(self, data: int, trig: int) -> None: """Drive one 16-bit word on the write side on the next wrClk edge.""" self.dut.validIn.value = 1 self.dut.dataIn.value = data self.dut.trigIn.value = trig - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) async def write_pair(self, first: int, second: int, trig_first: int = 1, trig_second: int = 0) -> None: @@ -119,8 +117,7 @@ async def write_pair(self, first: int, second: int, async def wait_valid_out(self) -> None: """Block until validOut asserts on the rdClk domain (bounded).""" for _ in range(_VALID_TIMEOUT_CYCLES): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) if int(self.dut.validOut.value) == 1: return raise AssertionError( @@ -163,8 +160,7 @@ async def word_order_and_trig_test(dut): # Wait for validOut to deassert before next pair for _ in range(8): - await RisingEdge(dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rdClk) if int(dut.validOut.value) == 0: break @@ -219,19 +215,16 @@ async def overflow_underflow_quiet_test(dut): dut.validIn.value = 1 dut.dataIn.value = 0xA000 + pair_idx dut.trigIn.value = 1 - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) dut.dataIn.value = 0xB000 + pair_idx dut.trigIn.value = 0 - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) # Gap — deassert validIn for 2 wrClk cycles (accumulator resets) dut.validIn.value = 0 for _ in range(2): - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) overflow_seen |= int(dut.overflow.value) # Read side sampling @@ -241,8 +234,7 @@ async def overflow_underflow_quiet_test(dut): # Wait for validOut to deassert for _ in range(8): - await RisingEdge(dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rdClk) if int(dut.validOut.value) == 0: break @@ -259,8 +251,7 @@ async def overflow_underflow_quiet_test(dut): overflow_seen |= int(dut.overflow.value) # Wait for validOut to deassert before next pair for _ in range(8): - await RisingEdge(dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rdClk) if int(dut.validOut.value) == 0: break diff --git a/tests/protocols/jesd204b/test_Jesd204bLoopback.py b/tests/protocols/jesd204b/test_Jesd204bLoopback.py index f4eda70be7..b438e287e4 100644 --- a/tests/protocols/jesd204b/test_Jesd204bLoopback.py +++ b/tests/protocols/jesd204b/test_Jesd204bLoopback.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import Event, RisingEdge, Timer +from cocotb.triggers import Event, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster @@ -142,14 +144,12 @@ def __init__(self, dut, l_g: int) -> None: async def axi_cycle(self, n: int = 1) -> None: """Wait n AXI clock cycles with TPD settle.""" for _ in range(n): - await RisingEdge(self.dut.S_AXI_TX_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_TX_ACLK) async def dev_cycle(self, n: int = 1) -> None: """Wait n devClk cycles with TPD settle.""" for _ in range(n): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, axi_cycles: int = 8, dev_cycles: int = 8) -> None: """Assert both AXI and dev resets, hold, then deassert.""" diff --git a/tests/protocols/jesd204b/test_Jesd32bTo16b.py b/tests/protocols/jesd204b/test_Jesd32bTo16b.py index 6139818d95..2f034e6d03 100644 --- a/tests/protocols/jesd204b/test_Jesd32bTo16b.py +++ b/tests/protocols/jesd204b/test_Jesd32bTo16b.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( hdl_parameters_from, @@ -79,25 +81,21 @@ async def reset(self) -> None: self.dut.wrRst.value = 1 self.dut.rdRst.value = 1 for _ in range(6): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) self.dut.wrRst.value = 0 for _ in range(4): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) self.dut.rdRst.value = 0 # Quiet settling time after reset deassertion for _ in range(4): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) async def write_word(self, data: int, trig: int) -> None: """Drive one 32-bit word with the given trig[1:0] value on the write side.""" self.dut.validIn.value = 1 self.dut.dataIn.value = data self.dut.trigIn.value = trig - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) self.dut.validIn.value = 0 await Timer(1, unit="ns") @@ -108,8 +106,7 @@ async def wait_valid_out(self) -> None: valid signal — so we poll rdClk rising edges after Timer(1, "ns"). """ for _ in range(_VALID_TIMEOUT_CYCLES): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) if int(self.dut.validOut.value) == 1: return raise AssertionError( @@ -119,8 +116,7 @@ async def wait_valid_out(self) -> None: async def wait_valid_deassert(self, max_cycles: int = 16) -> None: """Wait until validOut deasserts (bounded).""" for _ in range(max_cycles): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) if int(self.dut.validOut.value) == 0: return @@ -228,13 +224,11 @@ async def overflow_underflow_quiet_test(dut): dut.validIn.value = 1 dut.dataIn.value = 0xA000_0000 + word_idx dut.trigIn.value = 0b01 - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) # Gap: deassert validIn for 2 cycles dut.validIn.value = 0 for _ in range(2): - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) overflow_seen |= int(dut.overflow.value) # Collect the two output halves diff --git a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py index f0ff3de808..6d8cdb9844 100644 --- a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py +++ b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py @@ -35,7 +35,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -128,8 +129,7 @@ async def test_char03_restore_nonscrambled(dut): for cycle_i in range(total_cycles): dut.dataRx_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if cycle_i >= _LATENCY: got_raw.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) @@ -203,8 +203,7 @@ async def test_char03_restore_scrambled(dut): for cycle_i in range(total_cycles): dut.dataRx_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if cycle_i >= _LATENCY: got_raw.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) @@ -253,8 +252,7 @@ async def test_char03_align_error(dut): for _ in range(4): dut.dataRx_i.value = 0x11111111 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Inject a K-char (K28.5 = 0xBC) at byte 0 — K28.5 is not /F/ or /A/, # so it will NOT be consumed by char restoration → residual charisk → alignErr. @@ -263,8 +261,7 @@ async def test_char03_align_error(dut): for _ in range(4): dut.dataRx_i.value = k_word dut.chariskRx_i.value = 0x1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # alignErr_o is combinatorial — sample it immediately after settle assert int(dut.alignErr_o.value) == 1, ( @@ -300,8 +297,7 @@ async def test_char03_position_error(dut): for _ in range(4): dut.dataRx_i.value = 0x55555555 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Drive alignFrame_i=1 with K_CHAR at byte 3 only (data[31:24] = K_CHAR) # and chariskRx_i=0x8 (bit 3 set for byte 3). @@ -312,16 +308,14 @@ async def test_char03_position_error(dut): dut.alignFrame_i.value = 1 dut.dataRx_i.value = illegal_word dut.chariskRx_i.value = 0x8 # only byte 3 flagged - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.alignFrame_i.value = 0 # positionErr_o is combinatorial from r.position. # After one more clock, r.position = 0xF → positionErr_o = 1. dut.dataRx_i.value = 0x55555555 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) assert int(dut.positionErr_o.value) == 1, ( "positionErr: expected positionErr_o=1 after alignFrame with " @@ -366,8 +360,7 @@ async def test_char03_random_soak(dut): for cycle_i in range(total_cycles): dut.dataRx_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if cycle_i >= _LATENCY: got_raw.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) diff --git a/tests/protocols/jesd204b/test_JesdIlasGen.py b/tests/protocols/jesd204b/test_JesdIlasGen.py index 49cc99353a..b27699e19e 100644 --- a/tests/protocols/jesd204b/test_JesdIlasGen.py +++ b/tests/protocols/jesd204b/test_JesdIlasGen.py @@ -36,7 +36,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -102,8 +103,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles=2048): """Bounded poll for signal == value; raise AssertionError on timeout.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -133,8 +133,7 @@ async def capture_ilas_words(dut, tb, *, k, f, num_mf=4): # together with the first pulse is an alignment the real FSM never # produces (post-merge reconciliation of plans 03-03/03-04). dut.lmfc_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.ilas_i.value = 1 dut.lmfc_i.value = 0 @@ -151,8 +150,7 @@ async def capture_ilas_words(dut, tb, *, k, f, num_mf=4): else: dut.lmfc_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) data = int(dut.ilasData_o.value) datak = int(dut.ilasK_o.value) diff --git a/tests/protocols/jesd204b/test_JesdLmfcGen.py b/tests/protocols/jesd204b/test_JesdLmfcGen.py index 1a8a469ce8..b5741dc179 100644 --- a/tests/protocols/jesd204b/test_JesdLmfcGen.py +++ b/tests/protocols/jesd204b/test_JesdLmfcGen.py @@ -27,6 +27,8 @@ import pytest from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.common.regression_utils import ( env_int, parameter_case, @@ -79,11 +81,9 @@ async def test_period(dut): # Align: assert SYSREF rising edge with nSync_i='0' dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 # rising edge - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # deassert after one cycle # measure_lmfc_period waits for the first lmfc_o pulse then counts to the @@ -103,9 +103,9 @@ async def test_period(dut): @cocotb.test() async def test_sysref_realign_clause_a(dut): - """SYSREF-gating clause (a): SYSREF edge while nSync_i='0' realigns the counter. + """Real-time timing: measure SYSREF realignment at explicit sim-time offsets. - Verifies: + SYSREF-gating clause (a) verifies: - sysrefRe_o='1' exactly 1 cc after the SYSREF rising edge - lmfc_o='0' at that same cycle (not yet) - lmfc_o='1' exactly 2 cc after the SYSREF rising edge (off-by-one guard) @@ -191,16 +191,13 @@ async def test_sysref_gate_clause_b(dut): # --- Align with nSync_i='0' --- dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # Wait for the alignment lmfc pulse (2 cc after edge) - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Now lmfc_o should be high (cycle N+2) # Let the counter free-run; measure period to confirm alignment @@ -216,8 +213,7 @@ async def test_sysref_gate_clause_b(dut): await tb.cycle(half) # Inject SYSREF rising edge (nSync_i='1' → counter should NOT reset) dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # Measure the period after the gated edge — should be unchanged @@ -248,16 +244,13 @@ async def test_sysref_phase_neutral_clause_c(dut): # --- Initial alignment --- dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # Wait for the alignment lmfc pulse - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Let it free-run for exactly one full period so we know the phase measured_baseline = await measure_lmfc_period(dut, clk=dut.clk) @@ -270,13 +263,11 @@ async def test_sysref_phase_neutral_clause_c(dut): # cycles away. Inject SYSREF at that same moment (nSync_i='0'). # Wait for the next lmfc_o pulse while simultaneously injecting SYSREF. for _ in range(512): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if dut.lmfc_o.value == 1: # We are at a period boundary — inject SYSREF now dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 break else: @@ -292,9 +283,10 @@ async def test_sysref_phase_neutral_clause_c(dut): @cocotb.test() async def test_sysref_re_pulse_clause_d(dut): - """SYSREF-gating clause (d): sysrefRe_o is a single-cycle pulse on every SYSREF rising edge. + """Real-time timing: sample the SYSREF pulse away from TPD boundaries. - Verifies the single-cycle pulse in both nSync states (gated and active). + SYSREF-gating clause (d) verifies the single-cycle pulse in both nSync + states (gated and active). """ dut.nSync_i.value = 1 dut.sysref_i.value = 0 @@ -307,8 +299,7 @@ async def test_sysref_re_pulse_clause_d(dut): # Sample sysrefRe_o mid-cycle (Timer(6ns) past the rising edge) to avoid the # TPD=1ns boundary race at RisingEdge+1ns when r.sysrefRe transitions. dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 # rising edge (cycle N) # Cycle N+1: sample 6ns into the clock cycle — solidly in the sysrefRe='1' window await RisingEdge(dut.clk) # latch at edge N+1 (r.sysrefRe = 1 registered) @@ -329,8 +320,7 @@ async def test_sysref_re_pulse_clause_d(dut): # --- Test with nSync_i='1' (gated — sysrefRe_o still pulses) --- dut.nSync_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 # rising edge (cycle N) # Cycle N+1: sample mid-cycle — sysrefRe_o should be high (pulse regardless of nSync) await RisingEdge(dut.clk) diff --git a/tests/protocols/jesd204b/test_JesdRxLane.py b/tests/protocols/jesd204b/test_JesdRxLane.py index 41a0511ec8..d244dd1f49 100644 --- a/tests/protocols/jesd204b/test_JesdRxLane.py +++ b/tests/protocols/jesd204b/test_JesdRxLane.py @@ -52,7 +52,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -186,8 +187,7 @@ def __init__(self, dut) -> None: async def cycle(self, count: int = 1) -> None: """Advance count clock cycles, settling 1 ns after each rising edge.""" for _ in range(count): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, cycles: int = 4) -> None: """Assert devRst_i for cycles clock cycles, then deassert.""" @@ -205,8 +205,7 @@ async def reset(self, cycles: int = 4) -> None: async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 128): """Wait up to timeout_cycles for signal to equal value (1 ns settle).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -217,8 +216,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 128): async def wait_for_bit(status_signal, *, bit_mask: int, clk, timeout_cycles: int = 128): """Wait until (status_signal & bit_mask) != 0.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if (int(status_signal.value) & bit_mask) != 0: return raise AssertionError( @@ -401,8 +399,7 @@ async def drive_timeline( dut.gtRxData_i.value = 0 dut.gtRxDataK_i.value = 0 - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) if cycle_i >= _LATENCY: got_samples.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) diff --git a/tests/protocols/jesd204b/test_JesdRxReg.py b/tests/protocols/jesd204b/test_JesdRxReg.py index 4a2a2ff897..bef0ed6ed4 100644 --- a/tests/protocols/jesd204b/test_JesdRxReg.py +++ b/tests/protocols/jesd204b/test_JesdRxReg.py @@ -37,7 +37,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp @@ -127,13 +129,11 @@ def __init__(self, dut, l_g: int) -> None: async def axi_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def dev_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, axi_cycles: int = 8, dev_cycles: int = 8) -> None: self.dut.S_AXI_ARESETN.value = 0 @@ -183,8 +183,7 @@ async def assert_decerr(axil_master: AxiLiteMaster, address: int) -> None: async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 128): """Wait up to timeout_cycles for signal to equal value.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -271,8 +270,7 @@ async def drive_rx_link_up( for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = data_32b getattr(dut, f"gtRxDataK_{lane}_i").value = datak_4b - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Enable scrEnable if scrambled link-up: write commonCtrl bit5=1 if scr: @@ -287,8 +285,7 @@ async def drive_rx_link_up( for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = data_32b getattr(dut, f"gtRxDataK_{lane}_i").value = datak_4b - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) data_cycle_count += 1 # Keep driving last data word while waiting for dataValid to assert @@ -514,8 +511,7 @@ async def measure_sysref_offset(delay_val: int) -> int: offset = 0 max_cycles = 600 for _ in range(max_cycles): - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) offset += 1 if int(dut.sysRefDbg_o.value) == 1: break diff --git a/tests/protocols/jesd204b/test_JesdScramblerWrapper.py b/tests/protocols/jesd204b/test_JesdScramblerWrapper.py index a0e1c2e76c..e3f57fe1c3 100644 --- a/tests/protocols/jesd204b/test_JesdScramblerWrapper.py +++ b/tests/protocols/jesd204b/test_JesdScramblerWrapper.py @@ -28,7 +28,7 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import parameter_case, run_surf_vhdl_test from tests.protocols.jesd204b.jesd204b_test_utils import ( @@ -133,8 +133,7 @@ async def run_scrambler_pattern( total_cycles = n + _DRAIN for i in range(total_cycles): dut.sampleData_i.value = input_words[i] if i < n else 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) raw_tx.append(int(dut.txData_o.value)) if int(dut.rxValid_o.value) == 1: raw_rx.append(int(dut.rxData_o.value)) @@ -212,8 +211,7 @@ async def _run_pattern( # Assertion 3: Error outputs deasserted during steady-state data # ----------------------------------------------------------------------- # Sample once after settling - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) assert int(dut.rxAlignErr_o.value) == 0, ( f"[{case_name}] rxAlignErr_o unexpectedly asserted in steady state" ) @@ -256,8 +254,7 @@ async def scrambler_known_answer_vectors(dut): """Known-answer assertion: ties the bench to hand-computed LFSR anchors.""" dut.rst.value = 1 cocotb.start_soon(Clock(dut.clk, 10.0, unit="ns").start()) - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.rst.value = 0 for input_word, lfsr_init, expected_scrambled in KNOWN_ANSWER_VECTORS: diff --git a/tests/protocols/jesd204b/test_JesdSyncFsmRx.py b/tests/protocols/jesd204b/test_JesdSyncFsmRx.py index 4ff7b1eb3e..5332975e5b 100644 --- a/tests/protocols/jesd204b/test_JesdSyncFsmRx.py +++ b/tests/protocols/jesd204b/test_JesdSyncFsmRx.py @@ -28,7 +28,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -66,8 +67,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles=32): """Wait up to timeout_cycles for signal to reach value (1ns settle after each edge).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if signal.value == value: return raise AssertionError( diff --git a/tests/protocols/jesd204b/test_JesdSyncFsmTx.py b/tests/protocols/jesd204b/test_JesdSyncFsmTx.py index 64114eed9a..d5110151bc 100644 --- a/tests/protocols/jesd204b/test_JesdSyncFsmTx.py +++ b/tests/protocols/jesd204b/test_JesdSyncFsmTx.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -52,8 +53,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles=32): """Wait up to timeout_cycles for signal to reach value (1ns settle after each edge).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if signal.value == value: return raise AssertionError( diff --git a/tests/protocols/jesd204b/test_JesdTxLane.py b/tests/protocols/jesd204b/test_JesdTxLane.py index eed54a4590..f26b68889b 100644 --- a/tests/protocols/jesd204b/test_JesdTxLane.py +++ b/tests/protocols/jesd204b/test_JesdTxLane.py @@ -36,7 +36,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -122,8 +123,7 @@ def __init__(self, dut) -> None: async def cycle(self, count: int = 1) -> None: """Advance count clock cycles, settling 1 ns after each rising edge.""" for _ in range(count): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, cycles: int = 4) -> None: """Assert devRst_i for `cycles` clock cycles, then deassert.""" @@ -141,8 +141,7 @@ async def reset(self, cycles: int = 4) -> None: async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 64): """Wait up to timeout_cycles for signal to equal value (1 ns settle).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -153,8 +152,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 64): async def wait_for_bit(status_signal, *, bit_mask: int, clk, timeout_cycles: int = 64): """Wait until (status_signal & bit_mask) != 0.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if (int(status_signal.value) & bit_mask) != 0: return raise AssertionError( @@ -273,21 +271,18 @@ async def _capture_ilas_stream( for _mf in range(_NUM_MF_CAPTURE): # Fire LMFC (1-cycle pulse): advances to T_lmfc dut.lmfc_i.value = 1 - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) dut.lmfc_i.value = 0 # Advance 1 extra clock to T+1 (lmfcD1 cycle), matching standalone bench. - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Collect one multiframe (k*f/4 GT words) starting from T+1. for _ in range(k * f // 4): data = int(dut.gtTxData_o.value) & _GT_WORD_MASK datak = int(dut.gtTxDataK_o.value) & _K4_MASK raw_words.append((data, datak)) - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Fire the 4th LMFC to complete the ILA sequence (FSM exits ILA_S after this). # Collect 2 extra words AFTER firing LMFC4 so that the lmfcD1 cycle of LMFC4 @@ -296,21 +291,18 @@ async def _capture_ilas_stream( # After LMFC4, the FSM transitions to DATA_S; the 2 extra words use the DATA # path (not ILAS), but they are outside the aligned 3k-word window. dut.lmfc_i.value = 1 - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) dut.lmfc_i.value = 0 # Advance 1 extra clock (lmfcD1 cycle of LMFC4 = /A/ of MF2) - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Collect 2 extra words (includes lmfcD1 = /A/, and lmfcD2 = /R/ before DATA mux) for _ in range(2): data = int(dut.gtTxData_o.value) & _GT_WORD_MASK datak = int(dut.gtTxDataK_o.value) & _K4_MASK raw_words.append((data, datak)) - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Find the first /R/ to align with the golden model start_offset = None @@ -656,8 +648,7 @@ async def test_char01_nonscrambled(dut): for cycle_i in range(total_cycles): dut.lmfc_i.value = 0 dut.sampleData_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 - await RisingEdge(tb.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(tb.dut.devClk_i) if cycle_i >= _LATENCY: got_data = int(dut.gtTxData_o.value) & _GT_WORD_MASK @@ -754,8 +745,7 @@ async def test_char02_scrambled(dut): for cycle_i in range(total_cycles): dut.lmfc_i.value = 0 dut.sampleData_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 - await RisingEdge(tb.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(tb.dut.devClk_i) if cycle_i >= _LATENCY: got_data = int(dut.gtTxData_o.value) & _GT_WORD_MASK diff --git a/tests/protocols/jesd204b/test_JesdTxReg.py b/tests/protocols/jesd204b/test_JesdTxReg.py index 3b50dae44f..655f4b2fe9 100644 --- a/tests/protocols/jesd204b/test_JesdTxReg.py +++ b/tests/protocols/jesd204b/test_JesdTxReg.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -113,13 +115,11 @@ def __init__(self, dut) -> None: async def axi_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def dev_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, axi_cycles: int = 8, dev_cycles: int = 8) -> None: self.dut.S_AXI_ARESETN.value = 0 @@ -156,8 +156,7 @@ async def assert_decerr(axil_master, address: int) -> None: async def wait_for_bit(status_signal, *, bit_mask: int, clk, timeout_cycles: int = 256): """Wait until (status_signal & bit_mask) != 0.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if (int(status_signal.value) & bit_mask) != 0: return raise AssertionError( diff --git a/tests/protocols/line_codes/line_code_test_utils.py b/tests/protocols/line_codes/line_code_test_utils.py index 05dc46d055..80443bc952 100644 --- a/tests/protocols/line_codes/line_code_test_utils.py +++ b/tests/protocols/line_codes/line_code_test_utils.py @@ -16,6 +16,8 @@ from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.common.regression_utils import ( env_flag, env_sl, @@ -192,8 +194,7 @@ async def drive_integration_symbol(dut, *, data_in: int, data_k_in: int) -> None for _ in range(INTEGRATION_VALID_OUT_TIMEOUT_CYCLES): if int(dut.validOut.value) == 1: return - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) raise AssertionError( f"Timed out waiting for validOut after {INTEGRATION_VALID_OUT_TIMEOUT_CYCLES} cycles" diff --git a/tests/protocols/packetizer/packetizer_test_utils.py b/tests/protocols/packetizer/packetizer_test_utils.py index 53c824125b..811a4709a5 100644 --- a/tests/protocols/packetizer/packetizer_test_utils.py +++ b/tests/protocols/packetizer/packetizer_test_utils.py @@ -15,9 +15,10 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd PACKETIZER2_VERSION = 0x2 PACKETIZER2_CRC_NONE = 0x0 @@ -99,8 +100,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 128) -> AxisBeat: await Timer(1, unit="ns") if int(self._sig("TVALID").value) == 1: return self.snapshot() - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(self._sig("TVALID").value) == 1: return self.snapshot() raise AssertionError(f"Timed out waiting for {self.prefix} valid") @@ -108,8 +108,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 128) -> AxisBeat: async def recv(self, *, clk, keep_ready: bool = False) -> AxisBeat: self._sig("TREADY").value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if not keep_ready: self._sig("TREADY").value = 0 return beat @@ -121,8 +120,7 @@ def start_packetizer_clock(dut, *, period_ns: float = 5.0) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_packetizer_dut(dut, *, cycles: int = 4) -> None: @@ -136,8 +134,7 @@ async def wait_debug_init_done(dut, *, timeout_cycles: int = 64) -> None: for _ in range(timeout_cycles): if int(dut.debugOut.value) & (1 << DEBUG_INIT_DONE): return - await RisingEdge(dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.axisClk) raise AssertionError("Timed out waiting for depacketizer initDone") @@ -356,8 +353,7 @@ async def assert_no_output( if drive_ready: endpoint._sig("TREADY").value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert int(endpoint._sig("TVALID").value) == 0 if drive_ready: endpoint._sig("TREADY").value = 0 @@ -404,16 +400,14 @@ async def send_unpaced_beats(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], # cadence. for beat in beats: endpoint.drive(beat) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) endpoint.set_idle() async def recv_valid_pulses(endpoint: FlatAxisEndpoint, count: int, *, clk) -> list[AxisBeat]: beats = [] while len(beats) < count: - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(endpoint._sig("TVALID").value): beats.append(endpoint.snapshot()) return beats @@ -458,12 +452,10 @@ async def recv_beats_with_backpressure( for _ in range(count): beat = await endpoint.wait_valid(clk=clk) for _ in range(hold_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert endpoint.snapshot() == beat endpoint._sig("TREADY").value = 1 - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) endpoint._sig("TREADY").value = 0 beats.append(beat) return beats diff --git a/tests/protocols/pgp/axil_test_utils.py b/tests/protocols/pgp/axil_test_utils.py index 47bb344060..d31b284932 100644 --- a/tests/protocols/pgp/axil_test_utils.py +++ b/tests/protocols/pgp/axil_test_utils.py @@ -12,10 +12,9 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer from cocotbext.axi import AxiLiteBus, AxiLiteMaster -from tests.common.regression_utils import start_lockstep_clocks +from tests.common.regression_utils import sample_after_tpd, start_lockstep_clocks class PgpAxiLiteTb: @@ -76,10 +75,9 @@ async def cycle(self, count: int = 1): """Advance the bench by a whole number of visible wrapper clock edges.""" for _ in range(count): - await RisingEdge(self.cycle_clk) # Most SURF RTL uses the default `TPD_G => 1 ns`, so the tests wait # a small amount after every edge before sampling outputs. - await Timer(1, unit="ns") + await sample_after_tpd(self.cycle_clk) async def reset(self, *, hold_cycles: int = 4, settle_cycles: int = 8): """Drive every declared reset signal through its active and idle state.""" diff --git a/tests/protocols/pgp/pgp2_test_utils.py b/tests/protocols/pgp/pgp2_test_utils.py index 3bed6a5a0e..ecb8fe2340 100644 --- a/tests/protocols/pgp/pgp2_test_utils.py +++ b/tests/protocols/pgp/pgp2_test_utils.py @@ -12,7 +12,7 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd from tests.base.crc.crc_test_utils import crc_out_from_remainder, crc_update @@ -51,8 +51,7 @@ async def cycle(self, count: int = 1): # Sample one nanosecond after each edge so registered outputs have # time to reflect the DUT's default `TPD_G`. for _ in range(count): - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) async def reset(self, *, hold_cycles: int = 4, settle_cycles: int = 4): # Using an explicit reset coroutine keeps every test's startup sequence diff --git a/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py b/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py index e3b6033737..8b1dc9aadd 100644 --- a/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py +++ b/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py @@ -12,7 +12,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.protocols.pgp.pgp_test_utils import pgp_family_sources, run_pgp_wrapper_test @@ -23,14 +24,12 @@ async def pgp2b_core_wrapper_elab_test(dut): dut.rst.setimmediatevalue(1) for _ in range(4): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.rst.value = 0 for _ in range(16): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) PARAMETER_SWEEP = [ diff --git a/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py b/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py index 0820ebd70d..5088fcd07d 100644 --- a/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py +++ b/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py @@ -12,7 +12,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.protocols.pgp.pgp_test_utils import pgp_family_sources, run_pgp_wrapper_test @@ -23,14 +24,12 @@ async def pgp2fc_core_wrapper_elab_test(dut): dut.rst.setimmediatevalue(1) for _ in range(4): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.rst.value = 0 for _ in range(16): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) PARAMETER_SWEEP = [ diff --git a/tests/protocols/pgp/pgp4/pgp4_test_utils.py b/tests/protocols/pgp/pgp4/pgp4_test_utils.py index 3b4c78c630..d067d00fb1 100644 --- a/tests/protocols/pgp/pgp4/pgp4_test_utils.py +++ b/tests/protocols/pgp/pgp4/pgp4_test_utils.py @@ -12,7 +12,8 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready @@ -47,8 +48,7 @@ def __init__(self, dut, *, clk_name: str = "clk", rst_name: str = "rst"): async def cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) async def reset(self, *, hold_cycles: int = 4, settle_cycles: int = 4): self.rst.setimmediatevalue(1) diff --git a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py index 9e14119f55..f7811a2e47 100644 --- a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py +++ b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py @@ -27,7 +27,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_flag, @@ -70,13 +72,11 @@ def __init__(self, dut): async def cycle_phy(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.phyClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.phyClk) async def cycle_pgp(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.pgpClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.pgpClk) async def sample_pgp_cycle(self): await FallingEdge(self.dut.pgpClk) diff --git a/tests/protocols/rssi/test_RssiAxiLiteRegItf.py b/tests/protocols/rssi/test_RssiAxiLiteRegItf.py index 5d70a7589b..b1f95a82d7 100644 --- a/tests/protocols/rssi/test_RssiAxiLiteRegItf.py +++ b/tests/protocols/rssi/test_RssiAxiLiteRegItf.py @@ -37,7 +37,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -80,8 +81,7 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) def _set_defaults(self) -> None: self.dut.negParamVersion_i.value = 1 diff --git a/tests/protocols/rssi/test_RssiChksum.py b/tests/protocols/rssi/test_RssiChksum.py index f6dfe7b107..0a86e438e2 100644 --- a/tests/protocols/rssi/test_RssiChksum.py +++ b/tests/protocols/rssi/test_RssiChksum.py @@ -34,9 +34,10 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd from tests.protocols.rssi.rssi_test_utils import ( RssiParams, build_ack_header, @@ -58,10 +59,9 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.clk_i) # Most RSSI RTL uses `after TPD_G`; wait past the default 1 ns # transport delay before sampling outputs. - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk_i, propagation_time=2) async def reset(self) -> None: # Hold all data/control inputs in a benign state during reset so the diff --git a/tests/protocols/rssi/test_RssiHeaderReg.py b/tests/protocols/rssi/test_RssiHeaderReg.py index b6a588c44a..816e707f0f 100644 --- a/tests/protocols/rssi/test_RssiHeaderReg.py +++ b/tests/protocols/rssi/test_RssiHeaderReg.py @@ -33,9 +33,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd from tests.protocols.rssi.rssi_test_utils import ( RssiParams, build_ack_header, @@ -58,10 +58,9 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.clk_i) # Wait past the default `TPD_G` so registered outputs are settled # before Python reads them. - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk_i, propagation_time=2) async def reset(self) -> None: # Reset all header request strobes and data fields before deasserting diff --git a/tests/protocols/rssi/test_RssiMonitor.py b/tests/protocols/rssi/test_RssiMonitor.py index e2e78631a7..663cab970b 100644 --- a/tests/protocols/rssi/test_RssiMonitor.py +++ b/tests/protocols/rssi/test_RssiMonitor.py @@ -32,9 +32,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd class TB: @@ -44,8 +44,7 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.axisClk, propagation_time=2) def _set_flag_defaults(self) -> None: self.dut.rxFlagsSyn_i.value = 0 diff --git a/tests/protocols/rssi/test_RssiTxFsm.py b/tests/protocols/rssi/test_RssiTxFsm.py index 21aef9f41d..673ea98fb3 100644 --- a/tests/protocols/rssi/test_RssiTxFsm.py +++ b/tests/protocols/rssi/test_RssiTxFsm.py @@ -37,7 +37,9 @@ import cocotb import pytest -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import ( @@ -81,8 +83,7 @@ async def _send_contiguous_frame_after_tpd(endpoint, beats: list[SsiBeat], *, cl for beat in beats: endpoint.drive(beat) for _ in range(1024): - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if int(endpoint._sig("TReady").value) == 1: break else: @@ -174,13 +175,11 @@ async def recv_frame_selected_fields( for field in fields: beat[field] = int(getattr(self.dut, field).value) beats.append(beat) - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) if beat.get("mAxisTLast", 0) == 1: return beats else: - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) finally: self.dut.mAxisTReady.value = 0 raise AssertionError("Timed out waiting for selected mAxis frame fields") diff --git a/tests/protocols/srp/test_SrpV0Loopback.py b/tests/protocols/srp/test_SrpV0Loopback.py index 93a2592ed5..fd25896b69 100644 --- a/tests/protocols/srp/test_SrpV0Loopback.py +++ b/tests/protocols/srp/test_SrpV0Loopback.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -38,8 +39,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def reset(self): # The wrapper uses the standard active-low AXI-Lite reset exposed by diff --git a/tests/protocols/ssi/ssi_test_utils.py b/tests/protocols/ssi/ssi_test_utils.py index a848b11c31..19c13f7539 100644 --- a/tests/protocols/ssi/ssi_test_utils.py +++ b/tests/protocols/ssi/ssi_test_utils.py @@ -17,6 +17,8 @@ from cocotb.clock import Clock from cocotb.triggers import FallingEdge, RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.axi.utils import wait_sampled_ready @@ -119,8 +121,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 64) -> SsiBeat: await Timer(1, unit="ns") if int(self._sig("TValid").value) == 1: return self.snapshot() - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(self._sig("TValid").value) == 1: return self.snapshot() raise AssertionError(f"Timed out waiting for {self.prefix} valid") @@ -131,8 +132,7 @@ async def recv(self, *, clk, ready_signal=None, keep_ready: bool = False) -> Ssi if ready_signal is not None: ready_signal.value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if ready_signal is not None and not keep_ready: ready_signal.value = 0 return beat @@ -169,8 +169,7 @@ async def cycle(clk, count: int = 1) -> None: # Most SSI benches sample a little after each edge so registered outputs # have time to settle before Python reads them. for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_dut(dut, *, clk_name: str = "axisClk", rst_name: str = "axisRst") -> None: @@ -264,8 +263,7 @@ async def wait_output_clear( ) -> None: ready_signal.value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(endpoint._sig("TValid").value) == 0: ready_signal.value = 0 return @@ -356,8 +354,7 @@ async def recv_visible_beat( ready_signal.value = 0 beat = await endpoint.wait_valid(clk=clk, timeout_cycles=timeout_cycles) ready_signal.value = 1 - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) ready_signal.value = 0 return beat @@ -499,8 +496,7 @@ async def wait_signal_level(signal, *, clk, expected: int, cycles: int = 32) -> for _ in range(cycles): if int(signal.value) == expected: return - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) raise AssertionError(f"Timed out waiting for {signal}={expected}") diff --git a/tests/protocols/ssi/test_SsiAxiLiteMaster.py b/tests/protocols/ssi/test_SsiAxiLiteMaster.py index eb8ad6dcf3..185dd37df7 100644 --- a/tests/protocols/ssi/test_SsiAxiLiteMaster.py +++ b/tests/protocols/ssi/test_SsiAxiLiteMaster.py @@ -28,6 +28,7 @@ from cocotbext.axi import AxiResp from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd from tests.protocols.ssi.ssi_test_utils import ( FlatSsiEndpoint, recv_frame_and_check, @@ -75,8 +76,7 @@ def in_reset(self) -> bool: async def cycle(self, count: int = 1): for _ in range(count): - await cocotb.triggers.RisingEdge(self.dut.axisClk) - await cocotb.triggers.Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def _wait_while_reset(self): # While reset is active, keep every ready/valid output deasserted. diff --git a/tests/protocols/ssi/test_SsiFifo.py b/tests/protocols/ssi/test_SsiFifo.py index 14de5b2164..2e68576a6a 100644 --- a/tests/protocols/ssi/test_SsiFifo.py +++ b/tests/protocols/ssi/test_SsiFifo.py @@ -32,6 +32,7 @@ import pytest from tests.common.regression_utils import env_flag, parameter_case, run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd from tests.protocols.ssi.ssi_test_utils import ( assert_beat_list, assert_beat_views, @@ -59,8 +60,7 @@ async def drive_ready_pattern(ready_signal, *, clk, pattern: list[int], cycles: # coroutine keeps watching what traffic was actually accepted. for index in range(cycles): ready_signal.value = pattern[index % len(pattern)] - await cocotb.triggers.RisingEdge(clk) - await cocotb.triggers.Timer(1, unit="ns") + await sample_after_tpd(clk) ready_signal.value = 0 diff --git a/tests/protocols/ssi/test_SsiIncrementingTx.py b/tests/protocols/ssi/test_SsiIncrementingTx.py index c90fdf0623..b44c5bdb06 100644 --- a/tests/protocols/ssi/test_SsiIncrementingTx.py +++ b/tests/protocols/ssi/test_SsiIncrementingTx.py @@ -23,7 +23,7 @@ import cocotb import pytest -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import run_surf_vhdl_test, sample_after_tpd from tests.protocols.ssi.ssi_test_utils import ( capture_accepted_beats, expect_no_output, @@ -39,8 +39,7 @@ async def pulse_trigger(dut): # Pulse the packet trigger for one cycle, matching how software would kick # the generator in hardware. dut.trig.value = 1 - await cocotb.triggers.RisingEdge(dut.axisClk) - await cocotb.triggers.Timer(1, unit="ns") + await sample_after_tpd(dut.axisClk) dut.trig.value = 0 diff --git a/tests/protocols/ssi/test_SsiPrbs.py b/tests/protocols/ssi/test_SsiPrbs.py index bd6d49eb99..da4ddd11f1 100644 --- a/tests/protocols/ssi/test_SsiPrbs.py +++ b/tests/protocols/ssi/test_SsiPrbs.py @@ -26,7 +26,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test @@ -41,13 +43,11 @@ def __init__(self, dut): async def fast_cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.fastClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.fastClk) async def slow_cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.slowClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.slowClk) async def reset(self): # Hold both domains in reset long enough for the internal loopback path diff --git a/tests/simlink/common/simlink_multi_instance_cocotb.py b/tests/simlink/common/simlink_multi_instance_cocotb.py index ef5b5f301d..2d9eb8248c 100644 --- a/tests/simlink/common/simlink_multi_instance_cocotb.py +++ b/tests/simlink/common/simlink_multi_instance_cocotb.py @@ -30,6 +30,8 @@ from cocotb.clock import Clock from cocotb.triggers import ReadOnly, RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.simlink.common.peer_orchestration import ( spawn_peer_group, terminate_peers, @@ -225,8 +227,7 @@ async def rogue_simlink_multi_instance_traffic_test(dut): dut.sideBandTxData.value = _pack((vectors[2] for vectors in sideband_vectors), 8) dut.sideBandTxEn.value = 0x3 - await RisingEdge(dut.clock) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clock) dut.streamIbValid.value = 0 dut.streamIbLast.value = 0 dut.sideBandTxEn.value = 0 diff --git a/tests/simlink/ghdl/test_RogueTcpStreamWrap.py b/tests/simlink/ghdl/test_RogueTcpStreamWrap.py index e64e878220..f36476549a 100644 --- a/tests/simlink/ghdl/test_RogueTcpStreamWrap.py +++ b/tests/simlink/ghdl/test_RogueTcpStreamWrap.py @@ -256,11 +256,11 @@ async def stream_wrapper_test(dut): tb = TB(dut) if CASE.get("chan_count", 1) != 1: await _run_elaboration_only(tb) - return - await _run_round_trip(tb) - await _run_sparse_tkeep(tb) - if CASE["paced"]: - await _run_pacing(tb) + else: + await _run_round_trip(tb) + await _run_sparse_tkeep(tb) + if CASE["paced"]: + await _run_pacing(tb) @pytest.mark.parametrize("case_name", CASES) diff --git a/tests/simlink/rogue/test_RogueSideBandRogue.py b/tests/simlink/rogue/test_RogueSideBandRogue.py index b78ab4ed9e..25fc578efd 100644 --- a/tests/simlink/rogue/test_RogueSideBandRogue.py +++ b/tests/simlink/rogue/test_RogueSideBandRogue.py @@ -8,6 +8,16 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## +# Test methodology: +# - Sweep: Exchange one opcode and one remData value in each direction across a +# real PyRogue SideBandSim process and the GHDL SimLink model. +# - Stimulus: Start the external client first, then drive the HDL-to-client +# opcode before remData so the transport is warm before the reply. +# - Checks: Require the client's JSON result and the DUT's received opcode and +# remData to match the independent constants in both directions. +# - Timing: Bound interpreter discovery, client readiness, result creation, and +# reply observation; always terminate the child process in cleanup. +# # Real-Rogue SideBand contract: a production pyrogue.interfaces.simulation.SideBandSim # (separate process) exchanges one opcode and one remData each direction with # RogueSideBandFlatHarness under GHDL. cocotb is the firmware-side sideband diff --git a/tests/simlink/rogue/test_RogueStreamRogue.py b/tests/simlink/rogue/test_RogueStreamRogue.py index 9396240ebf..aa75cb8277 100644 --- a/tests/simlink/rogue/test_RogueStreamRogue.py +++ b/tests/simlink/rogue/test_RogueStreamRogue.py @@ -8,6 +8,16 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## +# Test methodology: +# - Sweep: Exchange one frame in each direction across a real Rogue TcpClient +# process and the GHDL RogueTcpStream wrapper. +# - Stimulus: Drive the HDL-to-client frame first to establish the ZeroMQ path, +# then receive the independently generated client-to-HDL frame. +# - Checks: Compare both payloads byte-for-byte and require the client JSON +# result to report successful receipt of the HDL frame. +# - Timing: Bound interpreter discovery, client readiness, result creation, and +# the finite cocotb receive task; always terminate the child in cleanup. +# # Real-Rogue Stream contract: a production rogue.interfaces.stream.TcpClient # (separate process) exchanges one frame each direction with RogueTcpStreamWrap # under GHDL. cocotb is the firmware-side AXI-Stream endpoint and drives the diff --git a/tests/xilinx/general/gt_rx_align_check_test_utils.py b/tests/xilinx/general/gt_rx_align_check_test_utils.py index 49229803de..60102e6819 100644 --- a/tests/xilinx/general/gt_rx_align_check_test_utils.py +++ b/tests/xilinx/general/gt_rx_align_check_test_utils.py @@ -23,7 +23,9 @@ from __future__ import annotations import cocotb -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import RisingEdge + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiResp @@ -130,8 +132,7 @@ def in_reset(self) -> bool: async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def _wait_while_reset(self) -> None: while self.in_reset(): @@ -319,8 +320,7 @@ async def _run(self) -> None: """Lifetime agent: monitor reset pulses until cocotb ends the test.""" previous = 0 while True: - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) try: current = int(self.dut.resetOut.value) except ValueError: diff --git a/tests/xilinx/general/test_GtRxAlignCheck.py b/tests/xilinx/general/test_GtRxAlignCheck.py index 7f20010b8c..efa49e8dab 100644 --- a/tests/xilinx/general/test_GtRxAlignCheck.py +++ b/tests/xilinx/general/test_GtRxAlignCheck.py @@ -45,7 +45,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -54,6 +56,7 @@ env_int, parameter_case, run_surf_vhdl_test, + wait_after_edge_offset, ) from tests.xilinx.general.gt_rx_align_check_test_utils import ( CONFIG_ADDR, @@ -128,8 +131,7 @@ def __init__(self, dut, *, phases=(MATCHING_PHASE,)): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self) -> None: # Hold axilRst so the checker restarts from REG_INIT_C, then let the GT @@ -198,8 +200,10 @@ async def write_config(self, *, target: int, mask: int, rst_len: int) -> None: async def pulse_reset_in(self, *, hold_cycles: int = 4) -> None: # resetIn is documented ASYNC to axilClk, so move it mid-period rather # than on an edge to exercise the synchronizer instead of a clean setup. - await RisingEdge(self.dut.axilClk) - await Timer(RESET_IN_SKEW_NS, unit="ns") + await wait_after_edge_offset( + self.dut.axilClk, + offset_time=RESET_IN_SKEW_NS, + ) self.dut.resetIn.value = 1 await self.cycle(hold_cycles) await Timer(RESET_IN_SKEW_NS, unit="ns") From 1f573945031702e76a1b47fdd2184b0eb34b9d0e Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 10:11:47 -0700 Subject: [PATCH 41/57] test: complete regression compliance rollout --- .github/workflows/surf_ci.yml | 8 +++- .../regression-test-compliance/README.md | 46 +++++++++++++++---- scripts/setup_regression_env.sh | 8 ++-- .../jesd204b/test_JesdAlignFrRepCh.py | 4 +- tests/protocols/rssi/test_RssiChksum.py | 37 +++++++++++---- .../rssi/test_RssiCoreWrapperMultiStream.py | 28 ++++++++--- 6 files changed, 101 insertions(+), 30 deletions(-) diff --git a/.github/workflows/surf_ci.yml b/.github/workflows/surf_ci.yml index 82218e613e..5d55e71d3e 100644 --- a/.github/workflows/surf_ci.yml +++ b/.github/workflows/surf_ci.yml @@ -165,10 +165,15 @@ jobs: --ignore=tests/simlink ) + # Import once before either run mode. The compliance check consumes + # the same ruckus source inventory as the cocotb runner and fails + # fast on new structural violations before expensive simulations. + make MODULES=$PWD import + python -m tests.common.compliance_audit check tests + # Full integration/release runs build and simulate on apt mcode GHDL # and collect the coverage consumed by Codecov. if [[ "${{ steps.mode.outputs.value }}" == "full" ]]; then - make MODULES=$PWD import python -m pytest --cov -v -n auto --dist=worksteal "${full_ignores[@]}" "${full_targets[@]}" exit 0 fi @@ -192,7 +197,6 @@ jobs: fi echo "${selector_output}" - make MODULES=$PWD import if [[ "${selector_rc}" -ne 0 ]] || grep -qx "FORCE_FULL" <<< "${selector_output}"; then echo "Directory selector forced a full run (rc=${selector_rc})" diff --git a/docs/plans/regression-test-compliance/README.md b/docs/plans/regression-test-compliance/README.md index 54bab3cb32..5627149304 100644 --- a/docs/plans/regression-test-compliance/README.md +++ b/docs/plans/regression-test-compliance/README.md @@ -6,7 +6,7 @@ Bring the existing cocotb regression code into closer alignment with the authoritative [test guidance](../../../tests/README.md) without deleting coverage, hiding scenarios, or attempting a disruptive whole-tree rewrite. -Status: implementation is in progress on `verification-2-test-compliance`. +Status: implementation is complete on `verification-2-test-compliance`. Phase 0 delivered the structural audit, preservation-report comparison, and checked-in baseline ratchet under `tests/common/`. This branch was created from `verification-2` so the implementation can use the new guidance while it is @@ -262,6 +262,37 @@ The whole active test tree now reports zero compliance findings. The baseline: new raw edge-plus-delay sequences must use the appropriate sampling helper or carry a narrowly scoped reviewed timing contract. +The focused Phase 5 review adds an independent RSSI checksum anchor using the +literal header word `0x4008_1234_0000_0000` and its hand-worked one's-complement +result `0xADC3`; both the Python oracle and RTL must match it. RSSI multi-stream +frame comparisons now identify the failing beat and payload or sideband, and a +JESD204B soak uses a local seeded generator rather than mutating process-global +random state. The default affected selection passes with 7 passed and 3 +existing gated skips, and the checksum/JESD nodes pass all 7 cases serially. + +The five large-suite candidates were reviewed for coherent boundaries. Each +currently owns one expensive DUT topology and already separates behavior with +named cocotb entrypoints and local helpers; splitting by line count would +duplicate pytest wrappers and simulation launches without improving ownership. +No file split is planned until a behavior can move with a stable node mapping +and a genuinely independent simulation boundary. + +Phase 6 now exposes the structural check directly in CI after the ruckus import +and before any expensive simulator run. `scripts/setup_regression_env.sh` no +longer suggests the superseded flat `tests/test_*.py` layout; it points users to +the blocking check, a focused subsystem command, and `tests/README.md`. + +Final validation is complete. A fresh ruckus import succeeds, full Python +compile and flake8 checks are clean, and the blocking audit reports no baseline +differences. The CI-equivalent non-SimLink run passes with 946 passed and 17 +existing gated skips in 1344.94 seconds; the dedicated bounded-worker SimLink +run passes with 94 passed and 13 existing gated skips in 21.58 seconds. The +preservation comparison from branch base `4005e7ffe3c6d21953e53441fe8d9efae2b43309` +to the completed branch covers 430 versus 433 tracked Python test files and +reports zero removed pytest functions, cocotb entrypoints, parameter IDs, +environment controls, skips, or timeout decorators. The additions are the +compliance tool's 26 unit tests and 13 explicit selector/control identifiers. + ## Non-Negotiable Preservation Rules - Do not remove an existing test, parameter case, opt-in gate, or behavioral @@ -522,10 +553,9 @@ criteria. ## Immediate Next Steps -1. Review protocol oracle independence and assertion diagnostics, adding only - focused known-answer anchors or context that closes a demonstrated gap. -2. Split the largest suites only where a coherent ownership boundary improves - maintainability without changing public pytest nodes, selectors, or gates. -3. Correct stale commands in `scripts/setup_regression_env.sh`, ensure the - blocking compliance check runs in CI, and perform the final repository-wide - validation and preservation audit. +1. Push `verification-2-test-compliance` and open its implementation PR against + `pre-release` when the documentation branch is available there. +2. Include the branch-base preservation result and final validation totals in + the PR description. +3. Do not merge these implementation commits back into the documentation-only + `verification-2` branch. diff --git a/scripts/setup_regression_env.sh b/scripts/setup_regression_env.sh index 0bb38a2caa..d2cb1767ac 100755 --- a/scripts/setup_regression_env.sh +++ b/scripts/setup_regression_env.sh @@ -80,7 +80,9 @@ Activate the environment with: Then prepare HDL sources with: make MODULES="${ROOT_DIR}" import -Then run regressions with: - python -m pytest -v -n auto --dist=worksteal tests/axi tests/base tests/dsp - python -m pytest -v tests/test_*.py +Then check the regression structure and run the narrowest relevant suite: + python -m tests.common.compliance_audit check tests + python -m pytest -n 0 -q tests//test_.py + +See tests/README.md for the full methodology and parallel/full-suite commands. EOF diff --git a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py index 6d8cdb9844..d866ae7c72 100644 --- a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py +++ b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py @@ -346,9 +346,9 @@ async def test_char03_random_soak(dut): await tb.reset() # Fixed seed for reproducibility - random.seed(0xC0C0_BABE) + rng = random.Random(0xC0C0_BABE) n_words = 64 - stimulus = [random.randint(0, 0xFFFFFFFF) for _ in range(n_words)] + stimulus = [rng.randint(0, 0xFFFFFFFF) for _ in range(n_words)] golden = predict_char_restoration(stimulus, f=f, scr=bool(scr_enable), lfsr_init=0) diff --git a/tests/protocols/rssi/test_RssiChksum.py b/tests/protocols/rssi/test_RssiChksum.py index 0a86e438e2..fc1d9073ba 100644 --- a/tests/protocols/rssi/test_RssiChksum.py +++ b/tests/protocols/rssi/test_RssiChksum.py @@ -20,7 +20,9 @@ # - Stimulus: Feed ACK, DATA, and multi-word SYN headers with the checksum field # cleared for generation mode, then feed complete headers with the checksum # included for validation mode. Additional cases cover enable gaps and reset -# interruptions so the accumulator restart behavior is explicit. +# interruptions so the accumulator restart behavior is explicit. A raw, +# hand-worked ACK header anchors both the Python model and DUT independently +# of the header-builder path. # - Checks: Generated checksums must match the Python one's-complement oracle. # Complete valid headers must assert `check_o`; headers with one altered byte # must leave `check_o` deasserted. Dropping enable or asserting reset must @@ -109,6 +111,20 @@ async def known_header_vectors_test(dut): tb = TB(dut) await tb.reset() + # 0x4008 + 0x1234 + 0x0000 = 0x523c; one's complement is 0xadc3. + # Keep the input bytes and result literal so a shared defect in the header + # builders and checksum helper cannot make the model and DUT agree. + raw_ack_without_checksum = bytes.fromhex("4008123400000000") + hand_worked_checksum = 0xADC3 + assert ones_complement_checksum(raw_ack_without_checksum) == hand_worked_checksum, ( + "RSSI checksum oracle disagrees with the hand-worked ACK vector" + ) + observed, check_ok = await tb.run_words([0x4008_1234_0000_0000]) + assert observed == hand_worked_checksum, ( + f"raw ACK checksum: expected {hand_worked_checksum:#06x}, got {observed:#06x}" + ) + assert check_ok == 0, f"generation mode unexpectedly asserted check_o={check_ok}" + vectors = [ build_ack_header(sequence=0x12, acknowledge=0x34), build_data_header(sequence=0x56, acknowledge=0x78, ack=True, busy=True), @@ -133,13 +149,18 @@ async def known_header_vectors_test(dut): ), ] - for header in vectors: + for vector_index, header in enumerate(vectors): # Generation mode feeds the header with the checksum bytes cleared and # expects `chksum_o` to produce the value that belongs in those bytes. expected = ones_complement_checksum(header_without_checksum(header)) observed, check_ok = await tb.run_words(header_words(header_without_checksum(header))) - assert observed == expected - assert check_ok == 0 + assert observed == expected, ( + f"header vector {vector_index}: expected checksum {expected:#06x}, " + f"got {observed:#06x}" + ) + assert check_ok == 0, ( + f"header vector {vector_index}: generation mode asserted check_o={check_ok}" + ) @cocotb.test() @@ -153,16 +174,16 @@ async def validation_mode_accepts_good_and_rejects_bad_headers_test(dut): # Validation mode feeds the complete header, including checksum. A correct # header should reduce to zero after the RTL's final one's complement. observed, check_ok = await tb.run_words(header_words(good_header)) - assert observed == 0 - assert check_ok == 1 + assert observed == 0, f"valid header reduced to nonzero checksum {observed:#06x}" + assert check_ok == 1, f"valid header produced check_o={check_ok}" bad_header = bytearray(good_header) # Flip a negotiated field without updating the checksum; the same length and # structure should now fail only the checksum validation. bad_header[5] ^= 0x20 observed, check_ok = await tb.run_words(header_words(bytes(bad_header))) - assert observed != 0 - assert check_ok == 0 + assert observed != 0, "corrupted header unexpectedly reduced to a zero checksum" + assert check_ok == 0, f"corrupted header produced check_o={check_ok}" @cocotb.test() diff --git a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py index c979755045..cee37cbb02 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py +++ b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py @@ -84,14 +84,28 @@ def _explicit_pytest_selection(request, test_name: str) -> bool: def assert_frame_preserves_valid_bytes(actual: list[SsiBeat], expected: list[SsiBeat]) -> None: - assert len(actual) == len(expected) - for actual_beat, expected_beat in zip(actual, expected): + assert len(actual) == len(expected), ( + f"frame beat count: expected {len(expected)}, got {len(actual)}" + ) + for beat_index, (actual_beat, expected_beat) in enumerate(zip(actual, expected)): mask = data_mask_from_keep(expected_beat.keep) - assert actual_beat.data & mask == expected_beat.data & mask - assert actual_beat.keep == expected_beat.keep - assert actual_beat.last == expected_beat.last - assert actual_beat.sof == expected_beat.sof - assert actual_beat.eofe == expected_beat.eofe + assert actual_beat.data & mask == expected_beat.data & mask, ( + f"beat {beat_index} payload: expected " + f"{expected_beat.data & mask:#x}, got {actual_beat.data & mask:#x}" + ) + assert actual_beat.keep == expected_beat.keep, ( + f"beat {beat_index} TKEEP: expected {expected_beat.keep:#x}, " + f"got {actual_beat.keep:#x}" + ) + assert actual_beat.last == expected_beat.last, ( + f"beat {beat_index} TLAST: expected {expected_beat.last}, got {actual_beat.last}" + ) + assert actual_beat.sof == expected_beat.sof, ( + f"beat {beat_index} SOF: expected {expected_beat.sof}, got {actual_beat.sof}" + ) + assert actual_beat.eofe == expected_beat.eofe, ( + f"beat {beat_index} EOFE: expected {expected_beat.eofe}, got {actual_beat.eofe}" + ) class TB: From d745c304e0c35c3e5b26727197ef0981d6dce5c3 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 10:30:37 -0700 Subject: [PATCH 42/57] docs: remove completed compliance plan --- docs/plans/README.md | 5 + .../regression-test-compliance/README.md | 561 ------------------ 2 files changed, 5 insertions(+), 561 deletions(-) delete mode 100644 docs/plans/regression-test-compliance/README.md diff --git a/docs/plans/README.md b/docs/plans/README.md index 1b88473851..856af91ede 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -20,3 +20,8 @@ Each task directory should capture: - Open risks, blockers, and next steps. Keep notes concise and factual. Do not store large simulator logs, generated outputs, build products, or waveform files here; summarize them and link to durable locations when needed. + +Before merging completed work, move enduring usage, methodology, and +maintenance guidance into the nearest permanent README. Remove the completed +task directory unless it still serves as an intentional design record or an +active follow-up handoff. diff --git a/docs/plans/regression-test-compliance/README.md b/docs/plans/regression-test-compliance/README.md deleted file mode 100644 index 5627149304..0000000000 --- a/docs/plans/regression-test-compliance/README.md +++ /dev/null @@ -1,561 +0,0 @@ -# Regression Test Compliance - -## Goal And Status - -Bring the existing cocotb regression code into closer alignment with the -authoritative [test guidance](../../../tests/README.md) without deleting -coverage, hiding scenarios, or attempting a disruptive whole-tree rewrite. - -Status: implementation is complete on `verification-2-test-compliance`. -Phase 0 delivered the structural audit, preservation-report comparison, and -checked-in baseline ratchet under `tests/common/`. This branch was created from -`verification-2` so the implementation can use the new guidance while it is -reviewed. Test-code commits from this effort must not be merged back into -`verification-2`. Once the documentation branch lands, rebase or retarget the -implementation onto `pre-release`. - -This effort improves the structure and reliability of existing tests. It does -not reinstate the historical module-coverage queue or require new functional -tests for unrelated RTL. - -Phase 1 has completed its batcher pilot. The event-builder transition-frame -scenario is now excluded explicitly from the INDEXED configuration with -`COCOTB_TEST_FILTER` instead of returning a no-op pass. Preservation comparison -kept all 13 cocotb names, the pytest wrapper, and all three parameter IDs; the -INDEXED result reports one explicit skip while both ROUTED configurations run -all 13 scenarios. - -The RSSI pilot removed 19 additional no-op entrypoint returns across the -connection FSM, core, wrapper, multi-stream wrapper, and receive FSM suites. -Pytest now selects client/server role groups, excludes focused checksum/BUSY/ -loss/AXI-Lite/sequence cases from unrelated simulations, and retains the -existing known-issue and extended gates. The preservation comparison reports no -removed identifiers and four newly explicit `COCOTB_TEST_FILTER` controls. -Client/server FSM simulation passes in serial and xdist modes. Enabling the -known-issue RxFSM nodes still exposes their pre-existing behavioral failures; -their XUnit results confirm that the normal node skips only the disabled- -checksum case and the focused node skips the other 13 scenarios. - -The base-library migration converted 45 configuration and opt-in guards across -18 delay, FIFO, arbiter, RAM, and synchronizer modules into decorator-level -cocotb skips. The only remaining base early return is the intentional -common-clock terminal path in `SynchronizerFifo`: it first exercises and -asserts every passthrough value, then avoids the inapplicable dual-clock FIFO -path. All 171 base pytest nodes and all cocotb entrypoint names are preserved. -A representative 33-node serial run and the complete 94-node edited subset -under pytest-xdist both pass; the generated cocotb XUnit files report -configuration-inapplicable entrypoints as skips rather than passes. - -The AXI migration removed the remaining seven early no-op returns from -`AxiDualPortRam`, `AxiStreamPipeline`, and the AXI Stream FIFO integration -matrix. System-write, registered-pipeline reset, metadata truncation, -frame-ready, threshold, burst, and dynamic-pause scenarios now declare their -applicability in the cocotb decorator. The later return in -`AxiStreamResize.backpressure_and_reset_test` is retained because it follows a -completed equal-width handshake/clear check and intentionally terminates before -the inapplicable stateful-reset path. Preservation comparison reports no -removed nodes or entrypoints; all 17 edited parameter cases pass both serially -and under pytest-xdist. - -Phase 1 is complete. The protocol pass converted 36 remaining lane-, mode-, -known-issue-, overflow-, address-width-, subclass-, and scrambling-specific -no-op branches in CoaXPress, PGP4, SRPv0, and JESD204B into explicit cocotb -applicability skips. The audit now defines an early return by semantics—a bare -return before any awaited simulator activity or assertion—rather than by an -arbitrary line-distance threshold, and that rule is blocking with a zero-entry -baseline. Seven post-activity returns remain report-only and were reviewed as -intentional successful terminal paths. Protocol preservation comparison found -no removed pytest nodes or cocotb entrypoints. The 14 CoaXPress/PGP4/SRP -parameter nodes pass both serially and under pytest-xdist; the JESD-specific -24-node matrix also passes in both modes. - -Phase 2 has completed its AXI source-list pass. A fresh ruckus import confirmed -76 literal AXI paths were exact duplicates of design units already supplied by -`build/SRC_VHDL`; those entries were removed from 53 tests while the AXI4 and -DMA wrappers absent from the import were retained in their mixed source lists. -The AXI preservation report found no removed test identifiers. After deleting -only the ignored AXI simulator cache, the complete 117-node AXI suite passed a -fresh xdist build in 227.12 seconds and then passed again from cache in 17.74 -seconds. The checked-in duplicate-source baseline now contains no AXI entries. - -The remaining source-list pass removed 43 literal duplicate entries from DSP, -batcher, and RSSI tests, plus the dynamically expanded RSSI core source bundles -that the literal audit did not count. Test-only DSP and batcher wrappers absent -from ruckus remain explicit; every RSSI integration wrapper is already imported -and now uses that authoritative copy. The repository duplicate-source count and -blocking baseline are both zero, with no preservation-report changes. From -clean simulator caches, the combined suites pass with 37 passed and 17 gated -skips in 53.42 seconds; the cached repeat reports the same result in 15.51 -seconds. - -`test_AxiStreamDmaV2Read.py` now uses `run_surf_vhdl_test()` and keeps only its -two AXI4/DMA wrappers that are absent from the ruckus import; the production -read engine is supplied by ruckus. Both public parameter IDs are preserved. A -fresh serial build passes both cases in 16.95 seconds and the cached xdist -repeat passes in 1.10 seconds. The ordinary direct-runner finding and its -blocking baseline entry are now both zero. - -The shared source merge now rejects both an extra path resolving to an already -imported file and a different extra file redeclaring an entity, package, or -configuration in the same library. A dynamic-list sweep exercised 153 source -expressions without finding an undisclosed collision, then identified four -helper-mediated duplicates outside the literal audit: three line-code -integration testbenches and `Pgp4TxLiteWrapper`. Those tests now use the ruckus -copies. Their five focused cases pass from clean caches in serial mode and on a -cached parallel repeat; a 16-case Ethernet suite with a legitimate dynamic -source bundle also passes with the runtime guard enabled. - -Phase 2 is complete. The 17 RSSI `force_compile=True` arguments were blanket -settings rather than documented exceptions; the shared GHDL runner already -invalidates an elaborated image when any imported source is newer. After -removing them, the full default RSSI suite passed from a deleted simulator -cache with 6 passed and 17 gated skips in 33.68 seconds, then repeated from the -cache under pytest-xdist with the same results in 1.75 seconds. The remaining -VCS SimLink use is retained because that helper deliberately pre-analyzes its -mixed-language topology before asking cocotb-test to elaborate without source -arguments. - -Phase 3 has completed its first ownership batch. All 25 unretained non-clock -task findings in `tests/axi/` were lifetime monitors or protocol responders; -the owning benches now retain and name them, while the existing finite AXI -transaction tasks remain awaited. `start_lockstep_clocks()` now returns its -lifetime task so benches can retain that ownership as well. The common helper -suite passes all 84 tests, a representative 14-node serial AXI run passes, and -the full 117-node AXI suite passes under pytest-xdist. No pytest node, cocotb -entrypoint, parameter ID, selector, or gate changed. - -The next Phase 3 batch removed 12 more unretained-task findings across the base -FIFO, SSI, Xilinx GT alignment, CoaXPress low-speed TX, and PGP4 elastic-buffer -suites. The two CoaXPress stimulus coroutines are finite and are now awaited; -the remaining responders, source models, monitors, and collectors are named -lifetime agents retained by their owning model. All 14 affected pytest nodes -pass serially in 95.17 seconds and repeat under pytest-xdist in 6.22 seconds. - -The RoCEv2 lifecycle batch removed another 16 findings. Finite RDMA packet -producers are awaited, long-running engine/source peers are retained and -cancelled in `finally`, and the resize sideband monitor is bench-owned. This is -especially important on RDMA watchdog failures, which now clean up both the -engine and any incomplete producer before propagating the timeout. All five -affected pytest nodes pass serially in 149.54 seconds and under pytest-xdist in -96.92 seconds. - -The JESD204B loopback cleanup removed the final seven unretained-task findings. -GT-forwarding agents now travel with both their stop event and task handle; -every link restart signals and joins the old agent before a replacement starts, -while preserving the original two-cycle handoff margin. All nine loopback -pytest nodes pass serially in 156.58 seconds and under pytest-xdist in 21.40 -seconds. The repository unretained-task screening count is now zero. Phase 3 -continues with bounded-wait and external-resource review before that structural -result is promoted to a blocking ratchet. - -The open-loop audit now recognizes only functions whose docstring explicitly -contains `Lifetime agent:`. Forty-four monitor, responder, clock, source, -transport, and protocol-peer loops have been reviewed and annotated with that -ownership contract. This reduced the ambiguous open-loop report from 66 to 22 -finite operations, without allowlisting paths or function names. The remaining -22 are being converted to direct cycle/time bounds; the marker is not permitted -as a substitute for bounding a transaction. - -Eight AXI finite loops now use `wait_sampled_ready()` with its 1024-cycle limit -or an equivalent explicit IPbus acknowledgement limit. This removes ad hoc -ready/valid polling from the compact and FIFO integration benches while keeping -the accepting edge semantics unchanged. All 15 affected nodes pass both -serially and under pytest-xdist; 14 finite open-loop findings remain. - -Six more RoCEv2, CoaXPress, and PGP waits are bounded. DMA drains now fail with -the number of beats received after a conservative 65,536-cycle deadline; the -stream-facing cases use the shared 1024-cycle accepted-handshake primitive or -an equivalent diagnostic limit. All eight affected nodes pass serially in -94.57 seconds and under pytest-xdist in 37.05 seconds. Eight finite open-loop -findings remain, confined to SRP, SSI, and SimLink. - -The SRP and SSI batch removes six more finite open-loop findings. SRP response -collection remains protected by its existing 20-us `with_timeout()` and now -uses condition-driven completion internally; its direct AXI-Lite responder -waits also have 1024-cycle diagnostic limits. The SSI resize waiter retains its -existing 1-ms operation timeout while making terminal-beat completion explicit. -All 175 affected parameter cases pass serially in 1837.27 seconds and from the -cache under pytest-xdist in 76.68 seconds. - -The final two finite `while True` findings were SimLink traffic operations. -Multi-instance traffic now carries its existing wall-clock deadline directly -in the loop, and the native receive probe has its own four-second diagnostic -deadline in addition to the parent-process watchdog. Both retain unconditional -task/process/native-context cleanup. The focused native and GHDL SimLink suite -passes all nine nodes in 4.60 seconds with localhost socket access. - -The repository reports zero unretained non-clock `start_soon()` calls and zero -unclassified `while True` loops. Both rules now participate in the checked-in -zero-entry compliance baseline, so new implicit task ownership or open-ended -transaction loops fail the blocking repository check. Conditional polling -loops remain a distinct bounded-wait review and are not represented by this -structural zero. - -The first conditional-wait batch covers the RoCEv2 RDMA core's ten direct -ready/valid polls. They now use one bench-local 65,536-cycle helper that names -the stalled interface in its failure, while scoreboard/liveness waits retain -their existing operation-level `with_timeout()` watchdogs and fixed-length -CoaXPress capture loops remain directly cycle-bounded by their requested -sample count. The RDMA-core pytest node passes serially in 35.55 seconds. - -Phase 4 now has explicit common primitives for its two sampling categories. -`sample_after_delta_cycles()` enters `ReadOnly()` for delta-only observation; -`sample_after_tpd()` advances real simulated time for registered outputs driven -with `after TPD_G`. A reviewed propagation helper identifies itself with a -`Propagation sampling:` docstring, allowing the advisory audit to suppress the -classification at the helper boundary without path allowlists. AXI handshake, -base synchronizer, DSP, CoaXPress, packetizer, batcher, PGP2, and Ethernet -shared helpers now use the propagation primitive. The raw timing inventory fell -from 302 to 295 sites, and 21 representative consumer nodes pass under the -normal pytest configuration in 80.51 seconds. - -The final four legacy methodology exceptions are removed. The two RoCEv2 suites -now summarize their complete traffic/congestion sweeps, independent checks, -and watchdog ownership in the standard four-part block; the two real-Rogue -SimLink contracts document their bidirectional stimulus, JSON/DUT checks, -host/simulation bounds, and unconditional child cleanup. The blocking -`missing-methodology` baseline is now zero. - -The seven post-activity bare-return findings are also resolved. Five branches -now use explicit `if`/`else` or `for`/`else` structure. The two intentionally -disconnected XLGMII placeholders keep a terminal branch only after completing -their no-output and status assertions, with an immediate `Terminal scenario:` -explanation of that complete parameter contract. The audit recognizes only -that contiguous marker and now blocks every other post-activity bare return -against a zero baseline. All 16 affected AXI, synchronizer, DSP, CoaXPress, and -Ethernet nodes pass in 142.45 seconds; all six affected SimLink wrapper nodes -pass serially in 4.32 seconds with localhost socket access. - -The AXI timing migration classifies all 93 raw edge-plus-one-nanosecond sites -across AXI4, AXI-Lite, AXI Stream, bridge, and DMA benches as real propagation -sampling through `sample_after_tpd()`. This is an exact scheduling-preserving -replacement of the default SURF `TPD_G` wait, including handshake monitors and -protocol responders; no delay was converted to delta-only `ReadOnly()` -sampling. The AXI subtree now has zero advisory timing findings and is clean -under flake8. Its full 117-node suite passes under pytest-xdist in 54.19 seconds, -and a 17-node cross-subsystem serial matrix passes in 8.58 seconds. - -The DSP, Ethernet, Xilinx-general, and SimLink timing batch classifies the next -24 sites. Default registered-output and handshake samples use -`sample_after_tpd()`, including a deliberate two-nanosecond CRC margin. The GT -alignment reset test instead uses `wait_after_edge_offset()` to identify a real -mid-cycle asynchronous-stimulus offset without mislabeling it as `TPD_G` -propagation. Those subtrees now have zero timing findings and are clean under -flake8. The full affected helper-consumer run passes 81 nodes under pytest-xdist -in 341.53 seconds, a 38-node representative serial matrix passes in 107.72 -seconds, and the localhost SimLink multi-instance node passes serially in 4.03 -seconds. - -Phase 4 is complete. The final base FIFO sites use the explicit two-nanosecond -propagation contract, and all 25 base nodes pass in 133.43 seconds. The -remaining 174 protocol sites now use `sample_after_tpd()` or a reviewed -real-time timing contract: JESD204B alignment tests retain deliberate -time-measurement and midpoint samples, while CoaXPress and RSSI monitors retain -their explicit two-nanosecond propagation margins. The complete protocol suite -passes with 464 passed and 17 existing gated skips in 2612.94 seconds. A -before/after inventory of all 46 edited base/protocol Python files reports no -changes to cocotb entrypoints, pytest functions, parameter IDs, environment -controls, skips, or timeout decorators. - -The whole active test tree now reports zero compliance findings. The -`edge-then-timer` rule has therefore joined the checked-in zero-entry blocking -baseline: new raw edge-plus-delay sequences must use the appropriate sampling -helper or carry a narrowly scoped reviewed timing contract. - -The focused Phase 5 review adds an independent RSSI checksum anchor using the -literal header word `0x4008_1234_0000_0000` and its hand-worked one's-complement -result `0xADC3`; both the Python oracle and RTL must match it. RSSI multi-stream -frame comparisons now identify the failing beat and payload or sideband, and a -JESD204B soak uses a local seeded generator rather than mutating process-global -random state. The default affected selection passes with 7 passed and 3 -existing gated skips, and the checksum/JESD nodes pass all 7 cases serially. - -The five large-suite candidates were reviewed for coherent boundaries. Each -currently owns one expensive DUT topology and already separates behavior with -named cocotb entrypoints and local helpers; splitting by line count would -duplicate pytest wrappers and simulation launches without improving ownership. -No file split is planned until a behavior can move with a stable node mapping -and a genuinely independent simulation boundary. - -Phase 6 now exposes the structural check directly in CI after the ruckus import -and before any expensive simulator run. `scripts/setup_regression_env.sh` no -longer suggests the superseded flat `tests/test_*.py` layout; it points users to -the blocking check, a focused subsystem command, and `tests/README.md`. - -Final validation is complete. A fresh ruckus import succeeds, full Python -compile and flake8 checks are clean, and the blocking audit reports no baseline -differences. The CI-equivalent non-SimLink run passes with 946 passed and 17 -existing gated skips in 1344.94 seconds; the dedicated bounded-worker SimLink -run passes with 94 passed and 13 existing gated skips in 21.58 seconds. The -preservation comparison from branch base `4005e7ffe3c6d21953e53441fe8d9efae2b43309` -to the completed branch covers 430 versus 433 tracked Python test files and -reports zero removed pytest functions, cocotb entrypoints, parameter IDs, -environment controls, skips, or timeout decorators. The additions are the -compliance tool's 26 unit tests and 13 explicit selector/control identifiers. - -## Non-Negotiable Preservation Rules - -- Do not remove an existing test, parameter case, opt-in gate, or behavioral - assertion merely to make the suite conform to the style guide. -- Before changing a subsystem, record its pytest nodes, cocotb entrypoints, - parameter IDs, skips, known-issue gates, and extended-test gates. Afterward, - account explicitly for every removed, renamed, split, or consolidated case. -- A moved or split test must retain equivalent DUT stimulus and assertions. - Preserve public pytest node names and gate semantics when practical; otherwise - provide a before/after mapping in the commit or pull-request description. -- Treat a reduced test count, an unexpected skip, or a formerly exercised - cocotb entrypoint becoming unreachable as a regression until explained. -- Keep compliance cleanup separate from unrelated RTL behavior changes. A - discovered RTL defect should receive a focused reproducer/fix change rather - than being masked by relaxing an assertion. -- Make changes one subsystem or one shared mechanism at a time, and validate - both serial execution and the parallel mode used by CI. - -## Audit Baseline - -The initial source scan produced the following triage baseline. These are -screening signals, not confirmed violations; every site needs semantic review. - -- 726 cocotb entrypoints were found across 320 Python files. -- The AST audit reports 99 entrypoints in 33 files with a bare return within 12 - lines of the function declaration, plus 14 later bare returns in nine files. - Early returns are the highest priority because an inapplicable scenario can - be recorded as a pass. -- At least 119 literal paths passed through `extra_vhdl_sources`, across 70 - files, resolve to design units already present in the current ruckus import. - Variable-generated lists mean the true count may be higher. -- 447 non-clock `cocotb.start_soon()` calls were found. Of those, 61 calls in 23 - test modules and two helpers do not retain the returned task. Many may be - legitimate lifetime agents, but their ownership is currently implicit. -- The AST audit reports 304 directly adjacent edge-then-delay pairs across 129 - files. The initial broader text scan found 574 candidates. Some correctly - model a real nonzero `TPD_G`; others may only be compensating for delta-cycle - scheduling. -- 66 open-ended `while True` loop sites occur across 38 files, while 42 files - use `with_timeout()`. The counts do not establish whether an enclosing wait - is bounded. -- Two cocotb entrypoints use decorator-level timeouts. This is not a target - count: bounded operation waits are preferable, with decorator watchdogs - reserved for complex concurrent tests. -- One ordinary GHDL regression, `test_AxiStreamDmaV2Read.py`, bypasses the - shared runner without an obvious simulator-specific requirement. -- Four cocotb files lack the labeled methodology marker used by the structural - ratchet; they remain in the initial legacy baseline pending focused review. - -The largest reviewed test modules are useful structural-cleanup candidates: - -| Test module | Approximate lines | Cocotb tests | -| --- | ---: | ---: | -| `test_RssiCore.py` | 1,453 | 26 | -| `test_Jesd204bLoopback.py` | 1,254 | 4 | -| `test_CoaXPressRx.py` | 1,174 | 8 | -| `test_RoCEv2AxiStreamRdmaCore.py` | 848 | 16 | -| `test_AxiStreamFifoV2IpIntegrator.py` | 843 | 6 | - -Re-run the audit after material tree changes and before declaring the effort -complete. Record both the raw signal count and the number confirmed to require -change so the metrics are not mistaken for defect counts. - -## Implementation Plan - -### Phase 0: Capture And Ratchet The Baseline - -Create a lightweight, read-only audit under `tests/common/` that can report the -screening categories above. Where a static rule is reliable, add a checked-in -legacy allowlist so new or substantially edited tests cannot introduce another -violation while existing cases are migrated gradually. - -The first enforcement candidates are methodology-header presence, selected -tests that silently return before stimulus, ordinary direct-runner exceptions, -and literal duplicate imported sources. Coroutine and timing scans should begin -as reports because static syntax cannot determine intent reliably. - -Add a repeatable preservation report for each migrated subsystem. At minimum it -should capture pytest collection and the discovered cocotb test names. For gated -suites, capture the default, known-issue, and extended configurations -separately. Do not encode one expected repository-wide test count that changes -whenever legitimate coverage is added. - -### Phase 1: Eliminate No-Op Passes - -Review the 97 early-return candidates first, one subsystem at a time. - -- Move configuration applicability into the pytest parameter or selector - matrix when the scenario should not launch. -- Use an explicit skip with a configuration-specific reason when collection is - valuable but execution is unavailable. -- Keep an early return only when it follows meaningful assertions and represents - an intentional successful terminal path; add a short comment where that is - not self-evident. -- Start with batcher and RSSI because their routed/unrouted and gated scenarios - now have explicit local documentation. Continue with configuration-heavy AXI - and protocol suites. - -For each migrated subsystem, compare the preservation report before and after, -then run the focused suite serially and with pytest-xdist. - -### Phase 2: Normalize Runner And Source Handling - -Remove `extra_vhdl_sources` entries that duplicate units supplied by the ruckus -import. Audit exceptions rather than deleting every matching path mechanically; -a test may intentionally compile a different implementation or library, which -must be documented and isolated explicitly. - -After cleaning the callers, enhance `run_surf_vhdl_test()` or its source-merge -path to diagnose duplicate resolved files/design units before compilation. Use -an explicit, reviewed escape hatch only if a legitimate override exists. - -Migrate `test_AxiStreamDmaV2Read.py` to the shared runner if the focused audit -confirms that it needs no special lifecycle or simulator capability. Review -`force_compile=True` callers at the same time, but remove that setting only -after clean and cached builds prove reuse is safe. - -Validate source changes with a fresh `make MODULES="$PWD" import`, a clean -simulation build, a repeated cached run, and an xdist run. Source cleanup must -not change the elaborated DUT boundary. - -### Phase 3: Make Coroutine And Resource Ownership Explicit - -Classify each unretained non-clock task as finite work, a lifetime agent, or an -actual leak. - -- Await finite producers, consumers, transactions, and checks before the test - completes. -- Store monitors and protocol peers on the bench with names that communicate - their purpose. -- Add a small shared task registry/cleanup helper only if repeated lifecycle - mechanics justify it; avoid forcing simple leaf tests into a large framework. -- Put process, socket, port, temporary-file, and native-library cleanup in a - `finally` block or fixture teardown that also runs after assertion failure. -- Add bounded operation waits first. Add a decorator-level deadlock watchdog to - complex integration entrypoints only after their normal progress waits are - bounded. - -Prioritize RSSI core/wrappers, CoaXPress, RoCEv2, JESD204B, SimLink, and other -tests with several concurrent agents or external resources. - -### Phase 4: Clarify Sampling And Propagation Timing - -Classify edge-plus-delay sites into two semantic categories: - -1. delta-cycle settling after a clock edge, which should use `ReadOnly()`; and -2. real modeled propagation delay from a nonzero VHDL `after TPD_G`, which - should wait for the configured delay and then sample a stable value. - -Introduce narrowly named shared helpers for these two cases if they remove -repeated mechanics without hiding intent. Migrate high-reuse helper modules -before leaf tests. Do not perform a global textual replacement: changing a real -propagation wait to `ReadOnly()` can create a race, while retaining an arbitrary -fixed timer can conceal one. - -Timing cleanup must retain accepted-handshake behavior and should be validated -under both serial and parallel execution. For parameterized `TPD_G`, include at -least the default and a representative nonzero value when practical. - -### Phase 5: Improve Oracles, Diagnostics, And Structure - -Anchor protocol reference models with published or hand-worked known-answer -vectors independent of both the DUT and the main encoder path. Add these while -touching a protocol suite rather than launching a broad rewrite. - -Improve assertion context where pytest's rewritten expression is insufficient, -especially for parameter cases, frames, beats, sequence numbers, expected versus -observed sidebands, and randomized seeds. Do not mechanically add messages to -simple assertions that are already diagnostic. - -Split oversized modules by coherent behavior, not a line-count threshold. The -first candidates and suggested boundaries are: - -- RSSI core: negotiation/close, data/retransmission, flow control/keepalive, - connection lifecycle, and AXI-Lite control. -- JESD204B loopback: configuration/bring-up, data-path checking, error/recovery, - and long-running integration behavior. -- CoaXPress RX: framing/decoding, trigger/control behavior, error/recovery, and - integration scenarios. -- RoCEv2 RDMA: packet/oracle mechanics, operation classes, errors, and - multi-transaction integration. -- AXI Stream FIFO integration: configuration families, traffic/backpressure, - sideband behavior, and reset/status behavior. - -Keep shared mechanics in `*_test_utils.py`; keep the policy assertions and a -specific methodology block in the test module that owns each behavior. Preserve -case names and gates through every split. - -### Phase 6: Make The Ratchet Blocking - -Once each audit rule has low false-positive risk, make it blocking for new and -modified tests. Retain reviewed legacy exceptions and remove them as migration -PRs land. The final policy should reject: - -- a newly introduced no-op passing scenario; -- an unexplained duplicate ruckus/extra-source design unit; -- a new ordinary direct runner without a documented need; -- an unowned external resource or finite background transaction; and -- a methodology or local invocation that contradicts `tests/README.md`. - -Update `scripts/setup_regression_env.sh` separately so its suggested pytest -commands match the current directory layout and authoritative README. Keep this -small operational cleanup out of behavior-heavy migration commits. - -## Pull Request Slicing - -Prefer a sequence of independently reviewable pull requests rather than one -repository-wide compliance change: - -1. Audit/report infrastructure with nonblocking legacy baselines. -2. No-op-pass cleanup, one subsystem per PR. -3. Duplicate-source cleanup and runner enforcement, grouped by related source - manifests. -4. Coroutine/resource ownership, grouped by integration family. -5. Timing-helper introduction followed by subsystem migrations. -6. Large-suite splits, one suite per PR. -7. CI ratchet promotion as each rule becomes trustworthy. - -Every implementation PR should ultimately target `pre-release`, identify the -baseline commit used for comparison, include the before/after preservation -report, list focused and parallel validation, and state any remaining legacy -exceptions. Do not merge implementation commits into `verification-2`. - -## Validation Expectations - -Apply validation in proportion to each change, with the following minimums: - -- `git diff --check` and focused Python lint for edited test code. -- `make MODULES="$PWD" import` for source-list or runner-source changes. -- A serial focused pytest run with `-n 0` for readable simulator evidence. -- The nearest practical subsystem suite under pytest-xdist. -- Default and explicitly gated configurations when selection/gating changes. -- Fresh and cached simulation builds when build identity or source merging - changes. -- Before/after pytest/cocotb case accounting for every move, split, skip, or - selector change. -- VSG lint for any edited wrapper VHDL. - -No phase is complete solely because its screening count reaches zero. The -behavioral preservation report and focused regressions are the acceptance -criteria. - -## Risks And Mitigations - -- **Coverage loss during cleanup:** require before/after case accounting and - treat unexplained count reductions as failures. -- **False positives from static scans:** begin ambiguous rules as reports, use - reviewed allowlists, and enforce only semantically reliable checks. -- **Simulator cache changes hiding source mistakes:** validate both clean and - cached builds and reject duplicate design units before compilation. -- **Timing cleanup introducing races:** distinguish delta settling from real - `TPD_G` waits and migrate shared helpers incrementally. -- **Large diffs becoming unreviewable:** keep subsystem and mechanism changes in - separate PRs and avoid unrelated RTL/style cleanup. -- **Branch topology violating the documentation-only contract:** never merge - this implementation branch into `verification-2`; rebase or retarget onto - `pre-release` once the documentation is available there. - -## Immediate Next Steps - -1. Push `verification-2-test-compliance` and open its implementation PR against - `pre-release` when the documentation branch is available there. -2. Include the branch-base preservation result and final validation totals in - the PR description. -3. Do not merge these implementation commits back into the documentation-only - `verification-2` branch. From cda1d6bcd75178e8c4f079fdfcbc07fc880021ff Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Wed, 26 Aug 2026 11:05:20 -0700 Subject: [PATCH 43/57] code clean up --- protocols/rssi/v1/rtl/RssiCore.vhd | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/protocols/rssi/v1/rtl/RssiCore.vhd b/protocols/rssi/v1/rtl/RssiCore.vhd index a06c6bdfba..bfb17eff14 100644 --- a/protocols/rssi/v1/rtl/RssiCore.vhd +++ b/protocols/rssi/v1/rtl/RssiCore.vhd @@ -88,8 +88,7 @@ entity RssiCore is -- Counters MAX_RETRANS_CNT_G : positive := 2; - MAX_CUM_ACK_CNT_G : positive := 3 - ); + MAX_CUM_ACK_CNT_G : positive := 3); port ( clk_i : in sl; rst_i : in sl; @@ -699,8 +698,7 @@ begin generic map ( TPD_G => TPD_G, DATA_WIDTH_G => 64, - CSUM_WIDTH_G => 16 - ) + CSUM_WIDTH_G => 16) port map ( clk_i => clk_i, rst_i => rst_i, @@ -787,8 +785,7 @@ begin generic map ( TPD_G => TPD_G, DATA_WIDTH_G => 64, - CSUM_WIDTH_G => 16 - ) + CSUM_WIDTH_G => 16) port map ( clk_i => clk_i, rst_i => rst_i, From 588c5dfe4e5e5fe497d18aa4c899f99cfc6180ba Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 12:50:47 -0700 Subject: [PATCH 44/57] test: implement close methods and cancel tasks in AXI Lite tests --- tests/axi/axi_lite/test_AxiLiteAsync.py | 101 +++++++++++-------- tests/axi/axi_lite/test_AxiLiteMaster.py | 84 ++++++++------- tests/common/README.md | 3 + tests/common/regression_utils.py | 21 ++++ tests/common/test_regression_utils.py | 51 ++++++++++ tests/protocols/ssi/test_SsiAxiLiteMaster.py | 26 ++++- 6 files changed, 203 insertions(+), 83 deletions(-) diff --git a/tests/axi/axi_lite/test_AxiLiteAsync.py b/tests/axi/axi_lite/test_AxiLiteAsync.py index 9fbc39c835..6708c05bf6 100644 --- a/tests/axi/axi_lite/test_AxiLiteAsync.py +++ b/tests/axi/axi_lite/test_AxiLiteAsync.py @@ -33,6 +33,7 @@ from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import ( + cancel_and_join_tasks, env_flag, env_sl, parameter_case, @@ -65,6 +66,9 @@ def __init__(self, dut): ) self.slave = SimpleAxiLiteSlave(dut, self.reset_active) + async def close(self) -> None: + await self.slave.close() + def reset_active_value(self) -> int: return self.reset_active @@ -126,6 +130,9 @@ def __init__(self, dut, reset_active): cocotb.start_soon(self._run_read()), ) + async def close(self) -> None: + await cancel_and_join_tasks(self._responder_tasks) + def in_reset(self) -> bool: try: return int(self.dut.mAxiClkRst.value) == self.reset_active @@ -217,56 +224,62 @@ async def _run_read(self): @cocotb.test() async def bridge_round_trip_test(dut): tb = TB(dut) - await tb.reset() - - transactions = [ - (0x000, b"\x11\x22\x33\x44"), - (0x008, b"\xAA\xBB"), - (0x010, b"\x10\x20\x30\x40"), - ] - - # Sweep a few aligned accesses so the test proves the slave-side bus can - # drive data through the bridge into the master-side backing RAM. - for addr, payload in transactions: - wr_txn = await tb.axil.write(addr, payload) - assert wr_txn.resp == AxiResp.OKAY - assert tb.slave.mem[addr].to_bytes(4, "little")[: len(payload)] == payload + try: + await tb.reset() - rd_txn = await tb.axil.read(addr, len(payload)) - assert rd_txn.resp == AxiResp.OKAY - assert rd_txn.data == payload + transactions = [ + (0x000, b"\x11\x22\x33\x44"), + (0x008, b"\xAA\xBB"), + (0x010, b"\x10\x20\x30\x40"), + ] + + # Sweep a few aligned accesses so the test proves the slave-side bus + # can drive data through the bridge into the master-side backing RAM. + for addr, payload in transactions: + wr_txn = await tb.axil.write(addr, payload) + assert wr_txn.resp == AxiResp.OKAY + assert tb.slave.mem[addr].to_bytes(4, "little")[: len(payload)] == payload + + rd_txn = await tb.axil.read(addr, len(payload)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == payload + finally: + await tb.close() @cocotb.test() async def reset_behavior_test(dut): tb = TB(dut) - await tb.reset() - - baseline = b"\x5A\xA5\xC3\x3C" - wr_txn = await tb.axil.write(0x020, baseline) - assert wr_txn.resp == AxiResp.OKAY - rd_txn = await tb.axil.read(0x020, len(baseline)) - assert rd_txn.resp == AxiResp.OKAY - assert rd_txn.data == baseline - - # In common-clock mode the DUT reduces to direct pass-through, so the - # reset coverage is restart-and-recover rather than remote-domain error - # shaping. - self_reset = tb.reset_active_value() - self_release = tb.reset_inactive_value() - tb.dut.sAxiClkRst.value = self_reset - tb.dut.mAxiClkRst.value = self_reset - await tb.s_cycle(3) - tb.dut.sAxiClkRst.value = self_release - tb.dut.mAxiClkRst.value = self_release - await tb.s_cycle(3) - - recovery = b"\x89\x67\x45\x23" - wr_txn = await tb.axil.write(0x024, recovery) - assert wr_txn.resp == AxiResp.OKAY - rd_txn = await tb.axil.read(0x024, len(recovery)) - assert rd_txn.resp == AxiResp.OKAY - assert rd_txn.data == recovery + try: + await tb.reset() + + baseline = b"\x5A\xA5\xC3\x3C" + wr_txn = await tb.axil.write(0x020, baseline) + assert wr_txn.resp == AxiResp.OKAY + rd_txn = await tb.axil.read(0x020, len(baseline)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == baseline + + # In common-clock mode the DUT reduces to direct pass-through, so the + # reset coverage is restart-and-recover rather than remote-domain + # error shaping. + self_reset = tb.reset_active_value() + self_release = tb.reset_inactive_value() + tb.dut.sAxiClkRst.value = self_reset + tb.dut.mAxiClkRst.value = self_reset + await tb.s_cycle(3) + tb.dut.sAxiClkRst.value = self_release + tb.dut.mAxiClkRst.value = self_release + await tb.s_cycle(3) + + recovery = b"\x89\x67\x45\x23" + wr_txn = await tb.axil.write(0x024, recovery) + assert wr_txn.resp == AxiResp.OKAY + rd_txn = await tb.axil.read(0x024, len(recovery)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == recovery + finally: + await tb.close() PARAMETER_SWEEP = [ diff --git a/tests/axi/axi_lite/test_AxiLiteMaster.py b/tests/axi/axi_lite/test_AxiLiteMaster.py index 920bc017f2..146a8ae651 100644 --- a/tests/axi/axi_lite/test_AxiLiteMaster.py +++ b/tests/axi/axi_lite/test_AxiLiteMaster.py @@ -27,7 +27,7 @@ import pytest from cocotb.clock import Clock -from tests.common.regression_utils import sample_after_tpd +from tests.common.regression_utils import cancel_and_join_tasks, sample_after_tpd from cocotbext.axi import AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -58,6 +58,9 @@ def __init__(self, dut, reset_active): cocotb.start_soon(self._run_read()), ) + async def close(self) -> None: + await cancel_and_join_tasks(self._responder_tasks) + def in_reset(self) -> bool: try: return int(self.dut.axilRst.value) == self.reset_active @@ -172,6 +175,9 @@ def __init__(self, dut): self.slave = SimpleAxiLiteSlave(dut, self.reset_active) + async def close(self) -> None: + await self.slave.close() + def reset_active_value(self) -> int: return self.reset_active @@ -218,46 +224,56 @@ async def issue_request(self, *, rnw: bool, address: int, wr_data: int = 0): @cocotb.test() async def write_read_round_trip_test(dut): tb = TB(dut) - await tb.reset() + try: + await tb.reset() - ack_resp, _ = await tb.issue_request(rnw=False, address=0x24, wr_data=0x11223344) - assert ack_resp == int(AxiResp.OKAY) - assert tb.slave.mem[0x24] == 0x11223344 - assert tb.slave.last_write == (0x24, 0x11223344, 0xF, 0) + ack_resp, _ = await tb.issue_request(rnw=False, address=0x24, wr_data=0x11223344) + assert ack_resp == int(AxiResp.OKAY) + assert tb.slave.mem[0x24] == 0x11223344 + assert tb.slave.last_write == (0x24, 0x11223344, 0xF, 0) - ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x24) - assert ack_resp == int(AxiResp.OKAY) - assert ack_data == 0x11223344 - assert tb.slave.last_read == (0x24, 0) + ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x24) + assert ack_resp == int(AxiResp.OKAY) + assert ack_data == 0x11223344 + assert tb.slave.last_read == (0x24, 0) + finally: + await tb.close() @cocotb.test() async def error_and_idle_reset_test(dut): tb = TB(dut) - await tb.reset() - - tb.slave.mem[0x40] = 0xCAFEBABE - tb.slave.write_resp = AxiResp.SLVERR - tb.slave.read_resp = AxiResp.SLVERR - - ack_resp, _ = await tb.issue_request(rnw=False, address=0x40, wr_data=0xDEADBEEF) - assert ack_resp == int(AxiResp.SLVERR) - assert tb.slave.mem[0x40] == 0xCAFEBABE - - ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x40) - assert ack_resp == int(AxiResp.SLVERR) - assert ack_data == 0xCAFEBABE - - # Reassert reset after the error path so the test proves the DUT returns - # its request and ack outputs to the idle state cleanly. - tb.dut.axilRst.value = tb.reset_active_value() - await tb.cycle(2) - assert int(tb.dut.ackDone.value) == 0 - assert int(tb.dut.M_AXI_AWVALID.value) == 0 - assert int(tb.dut.M_AXI_WVALID.value) == 0 - assert int(tb.dut.M_AXI_ARVALID.value) == 0 - assert int(tb.dut.M_AXI_BREADY.value) == 1 - assert int(tb.dut.M_AXI_RREADY.value) == 1 + try: + await tb.reset() + + tb.slave.mem[0x40] = 0xCAFEBABE + tb.slave.write_resp = AxiResp.SLVERR + tb.slave.read_resp = AxiResp.SLVERR + + ack_resp, _ = await tb.issue_request( + rnw=False, + address=0x40, + wr_data=0xDEADBEEF, + ) + assert ack_resp == int(AxiResp.SLVERR) + assert tb.slave.mem[0x40] == 0xCAFEBABE + + ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x40) + assert ack_resp == int(AxiResp.SLVERR) + assert ack_data == 0xCAFEBABE + + # Reassert reset after the error path so the test proves the DUT + # returns its request and ack outputs to the idle state cleanly. + tb.dut.axilRst.value = tb.reset_active_value() + await tb.cycle(2) + assert int(tb.dut.ackDone.value) == 0 + assert int(tb.dut.M_AXI_AWVALID.value) == 0 + assert int(tb.dut.M_AXI_WVALID.value) == 0 + assert int(tb.dut.M_AXI_ARVALID.value) == 0 + assert int(tb.dut.M_AXI_BREADY.value) == 1 + assert int(tb.dut.M_AXI_RREADY.value) == 1 + finally: + await tb.close() PARAMETER_SWEEP = [ diff --git a/tests/common/README.md b/tests/common/README.md index cec6793eb8..d5c2d66ff2 100644 --- a/tests/common/README.md +++ b/tests/common/README.md @@ -81,6 +81,9 @@ and build-isolation conventions where they apply. - `start_lockstep_clocks()` drives multiple logically common clocks from one coroutine so their edges cannot drift. It returns the lifetime task; retain that task on the bench that owns the clock domains. +- `cancel_and_join_tasks()` cancels a bench's owned lifetime tasks, awaits all + of their termination paths, suppresses expected cancellation, and propagates + an unexpected task failure after every task has been joined. - `build_vhdl_sources()` and `merge_vhdl_sources()` are runner plumbing; tests should normally reach them only through `run_surf_vhdl_test()`. diff --git a/tests/common/regression_utils.py b/tests/common/regression_utils.py index 41b7bb1ad4..3368ce3340 100644 --- a/tests/common/regression_utils.py +++ b/tests/common/regression_utils.py @@ -10,6 +10,7 @@ from __future__ import annotations +from asyncio import CancelledError from functools import lru_cache import hashlib import os @@ -137,6 +138,26 @@ async def drive() -> None: return cocotb.start_soon(drive()) +async def cancel_and_join_tasks(tasks) -> None: + """Cancel owned lifetime tasks, await termination, and surface failures.""" + tasks = tuple(tasks) + for task in tasks: + task.cancel() + + first_error = None + for task in tasks: + try: + await task + except CancelledError: + pass + except BaseException as error: + if first_error is None: + first_error = error + + if first_error is not None: + raise first_error + + def env_flag(name: str, *, default: bool) -> bool: raw = os.environ.get(name) if raw is None: diff --git a/tests/common/test_regression_utils.py b/tests/common/test_regression_utils.py index b0decf2908..9217edba94 100644 --- a/tests/common/test_regression_utils.py +++ b/tests/common/test_regression_utils.py @@ -8,11 +8,13 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## +import asyncio import re import pytest from tests.common.regression_utils import ( + cancel_and_join_tasks, cocotb_filtered_env, cocotb_test_filter, cocotb_test_filter_excluding, @@ -20,6 +22,55 @@ ) +class FakeTask: + def __init__(self, name, events, *, error=None): + self.name = name + self.events = events + self.error = error + self.cancelled = False + + def cancel(self): + self.events.append(("cancel", self.name)) + self.cancelled = True + + def __await__(self): + async def join(): + self.events.append(("join", self.name)) + if self.error is not None: + raise self.error + if self.cancelled: + raise asyncio.CancelledError + + return join().__await__() + + +def test_cancel_and_join_tasks_cancels_all_before_joining(): + events = [] + tasks = (FakeTask("write", events), FakeTask("read", events)) + + asyncio.run(cancel_and_join_tasks(tasks)) + + assert events == [ + ("cancel", "write"), + ("cancel", "read"), + ("join", "write"), + ("join", "read"), + ] + + +def test_cancel_and_join_tasks_joins_all_before_raising_failure(): + events = [] + tasks = ( + FakeTask("write", events, error=RuntimeError("write responder failed")), + FakeTask("read", events), + ) + + with pytest.raises(RuntimeError, match="write responder failed"): + asyncio.run(cancel_and_join_tasks(tasks)) + + assert ("join", "read") in events + + def test_cocotb_test_filter_matches_only_named_entrypoints(): pattern = cocotb_test_filter("server_opens_test", "server_closes_test") diff --git a/tests/protocols/ssi/test_SsiAxiLiteMaster.py b/tests/protocols/ssi/test_SsiAxiLiteMaster.py index 185dd37df7..6e2a4d98b0 100644 --- a/tests/protocols/ssi/test_SsiAxiLiteMaster.py +++ b/tests/protocols/ssi/test_SsiAxiLiteMaster.py @@ -27,8 +27,11 @@ import pytest from cocotbext.axi import AxiResp -from tests.common.regression_utils import run_surf_vhdl_test -from tests.common.regression_utils import sample_after_tpd +from tests.common.regression_utils import ( + cancel_and_join_tasks, + run_surf_vhdl_test, + sample_after_tpd, +) from tests.protocols.ssi.ssi_test_utils import ( FlatSsiEndpoint, recv_frame_and_check, @@ -68,6 +71,9 @@ def __init__(self, dut): cocotb.start_soon(self._run_read()), ) + async def close(self) -> None: + await cancel_and_join_tasks(self._responder_tasks) + def in_reset(self) -> bool: try: return int(self.dut.axisRst.value) == 1 @@ -192,6 +198,9 @@ def __init__(self, dut): self.source.set_idle() dut.mAxisTReady.setimmediatevalue(1) + async def close(self) -> None: + await self.axil.close() + async def reset(self): await reset_dut(self.dut) @@ -240,9 +249,7 @@ async def send_read_request(tb: TB, *, echo: int, address: int, count: int): ) -@cocotb.test() -async def ssi_axi_lite_master_test(dut): - tb = TB(dut) +async def _exercise_ssi_axi_lite_master(tb: TB) -> None: await tb.reset() # First prove a single-word write round-trip, including the echoed request @@ -366,6 +373,15 @@ async def ssi_axi_lite_master_test(dut): await recv_task +@cocotb.test() +async def ssi_axi_lite_master_test(dut): + tb = TB(dut) + try: + await _exercise_ssi_axi_lite_master(tb) + finally: + await tb.close() + + @pytest.mark.parametrize("parameters", [pytest.param({}, id="same_clk_error_and_multiword")]) def test_SsiAxiLiteMaster(parameters): run_surf_vhdl_test( From 9ad515b76e5a9a6f0d8092028ecbb4c657d55c09 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 13:17:56 -0700 Subject: [PATCH 45/57] docs(axi-lite): update README with detailed AxiLiteAsync contract and reset handling --- axi/README.md | 2 +- axi/axi-lite/README.md | 46 +++++ .../axilite-async-remote-reset/README.md | 165 ------------------ tests/axi/axi_lite/README.md | 33 ++++ 4 files changed, 80 insertions(+), 166 deletions(-) create mode 100644 axi/axi-lite/README.md delete mode 100644 docs/plans/axilite-async-remote-reset/README.md create mode 100644 tests/axi/axi_lite/README.md diff --git a/axi/README.md b/axi/README.md index d5c2c23d23..3629633638 100644 --- a/axi/README.md +++ b/axi/README.md @@ -4,7 +4,7 @@ This tree contains reusable AXI-family RTL and wrappers. Top-level `axi/ruckus.t ## Layout -- `axi-lite/`: AXI-Lite records, crossbars, endpoints, masters, slaves, monitors, and IP-integrator adapters. +- [`axi-lite/`](axi-lite/README.md): AXI-Lite records, crossbars, endpoints, masters, slaves, monitors, and IP-integrator adapters. - `axi-stream/`: AXI Stream records, FIFOs, muxes, monitors, protocol adapters, and stream wrappers. - `axi4/`: full AXI4 support blocks and adapters. - `bridge/`: bridges between AXI-family buses and SURF protocol records. diff --git a/axi/axi-lite/README.md b/axi/axi-lite/README.md new file mode 100644 index 0000000000..a9e4f3ff07 --- /dev/null +++ b/axi/axi-lite/README.md @@ -0,0 +1,46 @@ +# AXI-Lite + +This directory contains reusable AXI-Lite records, interconnects, endpoints, +clock-domain bridges, and IP-integrator adapters. + +## Layout + +- `rtl/`: AXI-Lite packages and synthesizable cores. +- `ip_integrator/`: flattened wrappers for block-design and simulator-facing + integration. +- `tb/`: legacy VHDL testbenches. + +Executable cocotb regressions live under +[`tests/axi/axi_lite/`](../../tests/axi/axi_lite/README.md). + +## `AxiLiteAsync` Contract + +`AxiLiteAsync` directly connects the slave and master interfaces when +`COMMON_CLK_G = true`. Otherwise, five asynchronous FIFOs carry the read +request, read response, write address, write data, and write response channels. + +The asynchronous bridge permits one read and one write in flight. The write +address and data channels remain independent, so either may arrive first, but +each channel accepts only one pending beat until the write response completes. +The bridge enforces this limit with its READY outputs. External masters that +pipeline requests are therefore backpressured rather than buffered to the FIFO +depth. + +All five FIFOs share a registered reset request and are flushed when either AXI +domain resets. Reset handling follows these rules: + +- A slave/source-domain reset abandons outstanding source transactions; no + response is owed after that reset. +- A master/destination-domain reset while the slave domain remains active + completes each accepted read locally with `AXI_ERROR_RESP_G`. +- A locally completed write returns `AXI_ERROR_RESP_G` only after both its AW + and W beats have been accepted. +- Transactions discarded by reset are not replayed when the master domain + recovers, and stale responses do not survive a slave-domain reset. + +If a clock is unavailable, its corresponding reset must remain asserted. The +clock must be stable before reset is released, and traffic must remain inactive +until synchronized reset release completes. + +`AxiLiteAsyncIpIntegrator.vhd` exposes the same clocks, resets, and response +codes through flattened AXI-Lite ports. diff --git a/docs/plans/axilite-async-remote-reset/README.md b/docs/plans/axilite-async-remote-reset/README.md deleted file mode 100644 index 62cbad11b8..0000000000 --- a/docs/plans/axilite-async-remote-reset/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# AxiLiteAsync Remote Reset Recovery - -## Goal And Scope - -Stop `AxiLiteAsync` replaying transactions it already rejected. With -`COMMON_CLK_G = false`, an access issued while the master domain was reset was -answered with `AXI_ERROR_RESP_G` on the slave side but queued at the same time, -so it executed once `mAxiClk` and `mAxiClkRst` recovered. - -Limited to `axi/axi-lite/rtl/AxiLiteAsync.vhd` and its cocotb regression. No port -or generic changes. GitHub issue #1467. Requested constraints: minimal added -logic, no impact on timing closure. - -## Current Status - -Complete. Every defect below has a test that fails when only its own fix is -reverted, with one documented exception. - -## Defects - -All in the `GEN_ASYNC` branch. - -1. FIFO write enables were not gated by the remote reset, so a rejected request - was queued anyway. -2. Request FIFOs were reset only by `s2mRst`, which carries `sAxiClkRst`, so a - request queued before `mAxiClkRst` also survived. Mirror image: response FIFOs - were not flushed by a slave-domain reset. -3. `AW` and `W` cross in separate FIFOs, so a write straddling the reset left an - orphaned address that misdirected the next write's data. -4. `RVALID` and `BVALID` were forced to `'1'`, so a response could appear with no - matching request. -5. Reset comparisons were written against `'0'`, inverting fail-fast when - `RST_POLARITY_G = '0'`. - -## Design Decisions - -**One shared, registered, active-high `fifoRst` for all five FIFOs**, generated -in the `sAxiClk` control domain by a one-bit `RegisterVector`. `sAxiClkRst` -first passes through a local `RstSync`, then asynchronously asserts the -reset-request register; `mAxiClkRst` is synchronized into `sAxiClk` before -entering its synchronous data input. Release is synchronous and held until the -local error responder drains the abandoned transaction. `FifoAsync` then -resynchronizes the single registered request into both FIFO domains. The FIFO -instances use active-high reset internally regardless of the bridge's external -reset polarity, so the register directly drives every reset synchronizer -without an intervening polarity-select LUT. This avoids both combinational -logic and multi-clock fan-in ahead of the synchronizers. - -Clock/reset contract: if either AXI clock is unavailable, its corresponding -reset must remain asserted. The clock must be stable before reset release, and -traffic must remain inactive until synchronized release completes. A reset of -the slave/source domain abandons its outstanding transactions, so they require -no response. - -**One transaction in flight per channel, matching `AxiLiteCrossbar`** (maintainer -decision). The bridge does not need the bound: measured on unmodified RTL it -accepts 4 outstanding reads and returns all 4 in order. But `AxiLiteCrossbar` -does not release a slave slot until `rvalid and rready`, and `AxiLiteMaster` runs -one at a time, so no SURF topology exceeds one. - -Tracking must exist regardless, because `fifoRst` discards in-flight requests -that still owe a response. Matching the crossbar makes that one flag per channel -instead of a counter, which is the whole cost of the fix. The bound is therefore -enforced by the ready outputs and by making every FIFO write conditional on its -source-side handshake, in normal operation as well as during reset: -measured, a flag responder without the bound answers 1 of 4 abandoned reads and -hangs the master. Consequence, a master that pipelines is throttled to one -transaction, which matters because `AxiLiteAsyncIpIntegrator` exposes the slave -port to non-SURF masters. - -**Rejected: size tracking to FIFO capacity.** Three 7-bit counters, correct for -any AXI4-Lite master and preserving throughput, but +45 LUTs and +22 flip-flops -on a primitive used in more than 40 modules. - -Reset polarity is normalized once into active-high terms. - -## Resource And Timing - -Vivado 2025.2, out-of-context synthesis and implementation, -`xcku040-ffva1156-2-e`, `sAxiClk` at 2.5 ns and `mAxiClk` at 2.0 ns declared -asynchronous. - -| | Baseline | Counters (rejected) | Bounded (shipped) | -| --- | --- | --- | --- | -| CLB LUTs | 312 | 357 | 309 | -| CLB registers | 473 | 495 | 477 | -| WNS `sAxiClk` | +0.485 ns | +0.397 ns | +0.527 ns | -| WNS `mAxiClk` | +0.293 ns | +0.215 ns | +0.184 ns | - -Shipped version is 3 LUTs smaller than the unfixed baseline for 4 flip-flops. All -variants meet timing and no handshake output gained a logic level. The `mAxiClk` -worst path reports 0 logic levels, so its roughly 100 ps movement is routing -noise at this scale. - -## Files And Tests - -- `axi/axi-lite/rtl/AxiLiteAsync.vhd` -- `tests/axi/axi_lite/test_AxiLiteAsync.py` - -`AxiLiteAsyncIpIntegrator.vhd` needed no change; it already exposes both clocks -and resets and passes real response codes through. - -Sweep: 5 cases (common-clock, plus asynchronous active-high, active-low, -`RST_ASYNC_G = true`, `PIPE_STAGES_G = 2`), 9 cocotb tests each. - -| Defect or behaviour | Test | -| --- | --- | -| 2 | `remote_reset_inflight_flush_test` | -| 3 | `remote_reset_orphan_pairing_test` | -| 4 | `reset_behavior_test`, `remote_reset_write_order_test` | -| 5 | `async_active_low` case | -| Rejected request replayed | `remote_reset_ghost_test` | -| Mirror image of 2 | `source_reset_stale_response_test` | -| Reset of the new registered state | `source_reset_clears_outstanding_test` | -| One-transaction bound | `single_outstanding_bound_test` | - -`source_reset_clears_outstanding_test` is what gives the reset cases teeth. -Adding a `RST_ASYNC_G = true` case alone compiled the asynchronous reset branch -without checking it, and disabling that branch still passed. With this test, -breaking only the sequential asynchronous reset fails just that case and breaking -only the combinational synchronous reset fails just the others. - -**Exception:** defect 1 is not independently covered. Removing the write-enable -gating passes the whole regression, because `fifoRst` holds the FIFOs in reset -for the entire local-answer window so the write pointer cannot advance. Kept -anyway: one LUT input, it states the invariant explicitly instead of depending on -`FifoAsync` ignoring `wr_en` while reset, and it stays correct if `fifoRst` is -ever narrowed. - -`single_outstanding_bound_test` also holds a second `AR`, `AW`, and `W` while -the corresponding ready output is low and verifies that none of those -unaccepted beats crosses to the downstream slave. - -## Validation Run - -- Reproduced first: unmodified RTL replayed the rejected write and read - downstream after recovery. -- `pytest tests/axi/axi_lite/test_AxiLiteAsync.py`, 5 cases. -- `pytest -n auto tests/`, 1017 passed and 29 skipped. Run in full because - `AxiLiteAsync` is instantiated in `axi`, `dsp` and every GigE and 10GigE core. -- `make MODULES="$PWD" analysis` clean library wide. -- `vsg -c vsg-linter.yml` clean; `flake8 --count python/ scripts/ tests/` 0. -- Vivado numbers above: all SURF sources into library `surf` and ruckus into - `ruckus`, testbenches excluded, `synth_design -mode out_of_context` then - `opt_design`, `place_design`, `phys_opt_design`, `route_design`. - -Do not run `pytest -n auto` while Vivado implementation is running. The -`tests/simlink` tests are timing sensitive and fail from CPU starvation. - -## Open Risks And Next Steps - -- The bound is now enforced, so a previously pipelining external master is - throttled. Correctness unaffected, throughput not. -- Synthesis numbers are out-of-context on one part; absolute slack will differ - inside a real design. -- The resource and timing table predates the registered reset-request review - revision and must be remeasured before using the exact numbers in release - notes. -- Rerun Vivado `report_cdc -details -all_checks_per_endpoint` on the revised - reset topology and confirm that the former FIFO-reset CDC-10/CDC-12 paths are - gone. Vivado is not available in the current validation environment. -- `NUM_ADDR_BITS_G` exercised only at 12 in simulation and 32 in synthesis. -- With one transaction in flight the 16-deep FIFOs are oversized and their 96 - distributed-RAM LUTs now dominate the module. Shrinking `FIFO_ADDR_WIDTH_C` is - the obvious next saving, left out as an optimisation rather than part of the fix. diff --git a/tests/axi/axi_lite/README.md b/tests/axi/axi_lite/README.md new file mode 100644 index 0000000000..4412ce4eeb --- /dev/null +++ b/tests/axi/axi_lite/README.md @@ -0,0 +1,33 @@ +# AXI-Lite Regressions + +This directory contains cocotb regressions for the reusable AXI-Lite cores in +[`axi/axi-lite/`](../../../axi/axi-lite/README.md). Tests use thin +IP-integrator wrappers where a flattened simulator interface is needed. + +Run the directory with: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/axi/axi_lite +``` + +Use `-n 0` with a single test file when serial simulator logs are useful. + +## `AxiLiteAsync` + +`test_AxiLiteAsync.py` covers common-clock pass-through and four asynchronous +configurations: active-high reset, active-low reset, asynchronous reset, and +pipelined FIFO outputs. Its scenarios verify: + +- ordinary read/write round trips and recovery after reset; +- local error responses while the master domain is reset; +- no replay of rejected or already queued requests after recovery; +- no stale response after a slave-domain reset; +- correct AW/W ordering when a reset splits a write transaction; and +- the one-pending-beat limit on AR, AW, and W, including VALID held while READY + is low. + +The asynchronous reset tests use a gateable master clock so the slave domain +can remain live while the remote domain is unavailable. Accepted handshakes are +monitored on both sides; final memory contents alone are not used to infer that +a rejected transaction stayed out of the downstream interface. From 2ecce9a02a55ae79ea871a4c915a26e4d53dbd65 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 14:34:58 -0700 Subject: [PATCH 46/57] Auto use pattern check. --- devices/AnalogDevices/adcDdr/README.md | 6 +- .../serialized-ddr-adc-cleanup/README.md | 8 ++- python/surf/devices/analog_devices/_Ad9249.py | 10 +++- python/surf/devices/analog_devices/_Ad9252.py | 4 ++ python/surf/devices/analog_devices/_Ad9681.py | 4 ++ python/surf/devices/analog_devices/_AdcDdr.py | 12 +++- .../analog_devices/_AdcDdrCalibration.py | 9 +-- .../analog_devices/test_AdcDdrCalibration.py | 9 +-- .../analog_devices/test_AdcDdrModel.py | 55 ++++++++++++++++++- 9 files changed, 95 insertions(+), 22 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index f79a6a7be2..045b58e9b1 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -144,7 +144,9 @@ requiring both checkerboard values to appear. The final assembled snapshot and deep hardware window then require one shared phase across all channels. `UsePatternTester` adds deep full-channel measurements after the centered -snapshots pass; it does not replace the tap-scan backend. The bounded checker +snapshots pass; it does not replace the tap-scan backend. It defaults to the +readout model's `patternCheck` construction parameter, which must match the RTL +`PATTERN_CHECK_G` generic, and remains user-writable. The bounded checker in `AdcDdrPatternTester` evaluates `PatternTesterSamples` consecutive valid samples without transferring them to software. Alternating mode uses one reference channel for the complete channel group. PN23 mode checks the @@ -173,7 +175,7 @@ verification operation ends. | `GuardBand` | `2` taps | Required passing margin on each side of the selected center. | | `CircularDelays` | `False` | Whether the first and last passing scan runs are one wrapped eye. | | `SampleCount` | `2` | Number of four-sample measurement groups checked at each data tap. | -| `UsePatternTester` | `False` | Add deep hardware checkerboard and, when enabled, PN23 qualification after centering. | +| `UsePatternTester` | `patternCheck` | Add deep hardware checkerboard and, when enabled, PN23 qualification after centering. | | `PatternTesterSamples` | `4096` | Valid samples checked by each deep hardware window. | | `VerifyPn23` | Device dependent | Enable PN23 coherence and recurrence qualification when the adapter provides a PN-long reset control. | | `SettleTime` | `1 ms` | Wall-clock wait after delay, relock, or ADC test-mode changes. | diff --git a/docs/plans/serialized-ddr-adc-cleanup/README.md b/docs/plans/serialized-ddr-adc-cleanup/README.md index a44e41dd6e..1b5271b178 100644 --- a/docs/plans/serialized-ddr-adc-cleanup/README.md +++ b/docs/plans/serialized-ddr-adc-cleanup/README.md @@ -30,14 +30,16 @@ the alignment/calibration path deterministic, fast, and diagnostically useful. completed copy at process termination. Failed and stopped operations still publish their evidence when debug is disabled. - Calibration exposes an explicit terminal `Outcome` (`PASSED`, `FAILED`, or - `STOPPED`) and replaces PyRogue's generic successful `Message=Done` with - `Message=PASSED`. + `STOPPED`). `Message` retains PyRogue's normal process status instead of + duplicating that outcome. ## Deep Qualification Contract - `SampleCount` controls the shallow four-sample snapshot depth at each tap. - `PatternTesterSamples` controls the final hardware checkerboard depth and defaults to 4096 valid samples per deep window. +- `UsePatternTester` defaults to the readout model's `patternCheck` construction + parameter and remains user-writable. - The deep result retains per-channel word-error counts and accumulated bit-error masks plus per-FCO error counts. - A deep-check failure participates in the existing alternate-FCO-eye retry. @@ -53,7 +55,7 @@ deep-check success and failure, detailed error reporting, capability rejection, alternate FCO-eye retries, shallow and deep PN23 qualification, and state restoration. -- `test_AdcDdrCalibration.py` and `test_AdcDdrModel.py`: 71 passed. +- `test_AdcDdrCalibration.py` and `test_AdcDdrModel.py`: 72 passed. - `test_AdcDdrPatternTester.py`: 1 passed with GHDL/cocotb. - VSG: no violations in `AdcDdrPkg.vhd` or `AdcDdrPatternTester.vhd`. diff --git a/python/surf/devices/analog_devices/_Ad9249.py b/python/surf/devices/analog_devices/_Ad9249.py index 7cf136173b..749eef2fa7 100644 --- a/python/surf/devices/analog_devices/_Ad9249.py +++ b/python/surf/devices/analog_devices/_Ad9249.py @@ -420,6 +420,8 @@ class Ad9249ReadoutBank(analog_devices.AdcDdr): ---------- deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. **kwargs : Any Additional arguments forwarded to ``AdcDdr``. """ @@ -429,6 +431,7 @@ def __init__( *, deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', numChannels: int = 8, + patternCheck: bool = True, **kwargs: Any) -> None: """Create one normalized AD9249 bank readout. @@ -446,6 +449,7 @@ def __init__( sampleBits = 14, serializationFactor = 14, delayBits = delayBits, + patternCheck = patternCheck, **kwargs) @@ -489,6 +493,8 @@ class Ad9249Readout(pr.Device): ---------- deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. **kwargs : Any Additional arguments forwarded to ``pyrogue.Device``. """ @@ -497,6 +503,7 @@ def __init__( self, *, deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + patternCheck: bool = True, **kwargs: Any) -> None: """Create the complete normalized AD9249 readout.""" @@ -508,7 +515,8 @@ def __init__( self.add(Ad9249ReadoutBank( name = f'Bank[{i}]', offset = 0x1000*i, - deviceFamily = deviceFamily)) + deviceFamily = deviceFamily, + patternCheck = patternCheck)) class Ad9249ReadoutCalibration(pr.Device): diff --git a/python/surf/devices/analog_devices/_Ad9252.py b/python/surf/devices/analog_devices/_Ad9252.py index cb2a878198..4b5a036992 100644 --- a/python/surf/devices/analog_devices/_Ad9252.py +++ b/python/surf/devices/analog_devices/_Ad9252.py @@ -333,6 +333,8 @@ class Ad9252Readout(analog_devices.AdcDdr): Number of ADC channels exposed by the firmware. deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. **kwargs : Any Additional arguments forwarded to ``AdcDdr``. """ @@ -342,6 +344,7 @@ def __init__( *, channels: int = 8, deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + patternCheck: bool = True, **kwargs: Any) -> None: """Create the normalized AD9252 readout.""" @@ -356,6 +359,7 @@ def __init__( sampleBits = 14, serializationFactor = 14, delayBits = delayBits, + patternCheck = patternCheck, **kwargs) diff --git a/python/surf/devices/analog_devices/_Ad9681.py b/python/surf/devices/analog_devices/_Ad9681.py index f6090125d6..7b9a41a4a8 100644 --- a/python/surf/devices/analog_devices/_Ad9681.py +++ b/python/surf/devices/analog_devices/_Ad9681.py @@ -449,6 +449,8 @@ class Ad9681Readout(analog_devices.AdcDdr): ---------- deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. **kwargs : Any Additional arguments forwarded to ``AdcDdr``. """ @@ -457,6 +459,7 @@ def __init__( self, *, deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + patternCheck: bool = True, **kwargs: Any) -> None: """Create the normalized AD9681 readout.""" @@ -469,6 +472,7 @@ def __init__( sampleBits = 14, serializationFactor = 8, delayBits = delayBits, + patternCheck = patternCheck, **kwargs) diff --git a/python/surf/devices/analog_devices/_AdcDdr.py b/python/surf/devices/analog_devices/_AdcDdr.py index 2b69529022..c716166672 100644 --- a/python/surf/devices/analog_devices/_AdcDdr.py +++ b/python/surf/devices/analog_devices/_AdcDdr.py @@ -100,6 +100,8 @@ class AdcDdr(pr.Device): Number of serialized bits captured per data lane. delayBits : int, optional Width of each programmable input-delay value. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. **kwargs : Any Additional arguments forwarded to ``pyrogue.Device``. """ @@ -119,7 +121,7 @@ def checkGeometry(self) -> None: Raises ------ RuntimeError - If any advertised geometry field disagrees with this model. + If any advertised capability disagrees with this model. """ expected = { @@ -129,6 +131,7 @@ def checkGeometry(self) -> None: 'SampleBits': self._sampleBits, 'SerializationFactor': self._serializationFactor, 'DelayBits': self._delayBits, + 'PatternCheck': self._patternCheck, } mismatches = [] for name, modelValue in expected.items(): @@ -138,8 +141,9 @@ def checkGeometry(self) -> None: if mismatches: raise RuntimeError( f'{self.path}: AdcDdr register model does not match the RTL ' - 'capability registers; check the deviceFamily and other ' - 'construction generics against the firmware. Mismatches -- ' + 'capability registers; check the deviceFamily, patternCheck, ' + 'and other construction generics against the firmware. ' + 'Mismatches -- ' + '; '.join(mismatches)) def _getDataDelays(self, read: bool) -> list[int]: @@ -182,6 +186,7 @@ def __init__(self, *, sampleBits: int = 14, serializationFactor: int = 14, delayBits: int = 5, + patternCheck: bool = True, **kwargs: Any) -> None: """Create the normalized ADC readout model.""" @@ -204,6 +209,7 @@ def __init__(self, *, self._sampleBits = sampleBits self._serializationFactor = serializationFactor self._delayBits = delayBits + self._patternCheck = patternCheck self.add(pr.RemoteVariable( name = 'Version', diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index 24834f17c1..d44050aab0 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -465,7 +465,7 @@ def __init__( description = ( 'Run deep hardware checkerboard and optional PN23 qualification ' 'after centering'), - value = False, + value = readout._patternCheck, mode = 'RW')) self.add(pr.LocalVariable( @@ -576,12 +576,6 @@ def _process(self) -> None: # in its finally block, so retain the partial progress without # reporting the stop as an execution error. self.Message.set('Calibration stopped') - else: - # PyRogue's generic Process implementation sets Message to "Done" - # after the callback returns. Replace that ambiguous terminal text - # with the explicit successful outcome used by this process. - if self.Outcome.value() == self.OUTCOME_PASSED_C: - self.Message.set('PASSED') def _checkRun(self) -> None: """Raise the private stop exception at cooperative cancellation points.""" @@ -1295,7 +1289,6 @@ def _runCalibration(self, *, dev: Any) -> dict[str, Any]: raise else: self.Outcome.set(self.OUTCOME_PASSED_C) - self.Message.set('PASSED') return results finally: runTime = time.monotonic()-runStart diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 7a5dc4af7a..0bf3543ba9 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -208,6 +208,7 @@ def __init__(self): self._channels = 2 self._sampleBits = 14 self._delayBits = 3 + self._patternCheck = True self.FcoDelay = {0: FakeVariable(3)} self.FcoWord = {0: FakeVariable(0x3F00)} self.DataDelay = {0: FakeVariable(2), 1: FakeVariable(4)} @@ -543,7 +544,7 @@ def test_rejects_invalid_scans(passing, kwargs, exception, message): findAdcDdrEye(passing, **kwargs) -def make_calibration(): +def make_calibration(*, usePatternTester=False): config = FakeConfig() readout = FakeReadout() calibration = AdcDdrCalibration( @@ -554,6 +555,7 @@ def make_calibration(): root = pr.Root(name='Root', pollEn=False) root.add(calibration) root.start() + calibration.UsePatternTester.set(usePatternTester) calibration._testRoot = root calibration.DelayStop.set(7) calibration.MinimumEyeWidth.set(3) @@ -616,7 +618,6 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert readout.snapshotDelays == [(tap, tap) for tap in range(8)] + [(2, 5)] assert calibration.RunTime.value() > 0.0 assert calibration.Outcome.value() == calibration.OUTCOME_PASSED_C - assert calibration.Message.value() == 'PASSED' readout.FcoDelay[0].set(0) readout.DataDelay[0].set(0) @@ -647,13 +648,13 @@ def record(value, *args, **kwargs): assert publications[1]['Final']['passed'] -def test_process_gui_message_reports_passed(calibration_fixture): +def test_process_gui_message_keeps_framework_status(calibration_fixture): calibration, _, _ = calibration_fixture calibration._process() assert calibration.Outcome.value() == calibration.OUTCOME_PASSED_C - assert calibration.Message.value() == 'PASSED' + assert calibration.Message.value() == 'Done' def test_full_calibration_can_add_deep_pattern_qualification(calibration_fixture): diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py index 40018cd718..fab4117a35 100644 --- a/tests/devices/analog_devices/test_AdcDdrModel.py +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -199,6 +199,36 @@ def test_normalized_map_geometry_and_memory_readback(memory_backed_readout): readout.DataDelay[0].set(512) +def test_capability_check_includes_pattern_tester(): + memory = CountingMemory() + geometry = 3 | (2 << 8) | (2 << 16) | (14 << 24) + capabilities = 9 | (8 << 8) | (1 << 16) + memory._memory[0x004:0x008] = geometry.to_bytes(4, 'little') + memory._memory[0x008:0x00C] = capabilities.to_bytes(4, 'little') + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name = 'Readout', + memBase = memory, + dataLanes = 3, + fcoLanes = 2, + channels = 2, + sampleBits = 14, + serializationFactor = 8, + delayBits = 9, + patternCheck = True) + root.add(readout) + root.start() + try: + readout.checkGeometry() + readout._patternCheck = False + with pytest.raises( + RuntimeError, + match='PatternCheck: model=False, hardware=1'): + readout.checkGeometry() + finally: + root.stop() + + def test_debug_sample_array_uses_one_bulk_transaction(): memory = CountingMemory() expected = [0x100+index for index in range(32)] @@ -361,7 +391,7 @@ def test_device_specific_calibration_adapters(): assert ad9681.DelayStop.units == 'tap' assert ad9681.MinimumEyeWidth.units == 'tap' assert ad9681.GuardBand.units == 'tap' - assert ad9681.UsePatternTester.value() is False + assert ad9681.UsePatternTester.value() is True assert ad9681.UsePatternTester.mode == 'RW' assert ad9681.PatternTesterSamples.value() == 4096 assert ad9681.PatternTesterSamples.mode == 'RW' @@ -378,6 +408,14 @@ def test_device_specific_calibration_adapters(): assert ad9681.RunTime.mode == 'RO' assert ad9681.RunTime.units == 's' + ad9681WithoutPatternTester = Ad9681ReadoutCalibration( + name='Ad9681CalibrationWithoutPatternTester', + config=Ad9681Config(name='Ad9681ConfigWithoutPatternTester'), + readout=Ad9681Readout( + name='Ad9681ReadoutWithoutPatternTester', + patternCheck=False)) + assert ad9681WithoutPatternTester.UsePatternTester.value() is False + def test_ad9252_config_register_fields(): config = Ad9252Config(name='Config') @@ -501,6 +539,21 @@ def test_device_specific_readout_geometry(): ad9681._serializationFactor, ad9681._delayBits, ) == (16, 2, 8, 14, 8, 9) + assert ad9681._patternCheck is True + + assert Ad9252Readout( + name='Ad9252WithoutPatternTester', + patternCheck=False)._patternCheck is False + assert Ad9681Readout( + name='Ad9681WithoutPatternTester', + patternCheck=False)._patternCheck is False + + ad9249WithoutPatternTester = Ad9249Readout( + name='Ad9249WithoutPatternTester', + patternCheck=False) + assert all( + readout._patternCheck is False + for readout in ad9249WithoutPatternTester.Bank.values()) @pytest.mark.parametrize( From 81bca9708b6d20cbe57b9e54e546e54256d2c865 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 15:24:15 -0700 Subject: [PATCH 47/57] Refactor ADC calibration to standardize reset commands and enhance documentation --- devices/AnalogDevices/adcDdr/README.md | 18 ++-- .../serialized-ddr-adc-cleanup/README.md | 8 ++ python/surf/devices/analog_devices/_Ad9249.py | 7 +- python/surf/devices/analog_devices/_Ad9252.py | 9 +- python/surf/devices/analog_devices/_Ad9681.py | 7 +- .../analog_devices/_AdcDdrCalibration.py | 88 ++++++++++++++----- .../analog_devices/test_AdcDdrCalibration.py | 53 ++++++++--- .../analog_devices/test_AdcDdrModel.py | 6 ++ 8 files changed, 151 insertions(+), 45 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index 045b58e9b1..63ea178440 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -24,7 +24,9 @@ The PyRogue register model is device-neutral software calibration process is [`_AdcDdrCalibration.py`](../../../python/surf/devices/analog_devices/_AdcDdrCalibration.py). Device adapters in `_Ad9249.py`, `_Ad9252.py`, and `_Ad9681.py` supply the -physical-lane mapping and ADC configuration behavior. +physical-lane mapping and ADC configuration behavior. Their configuration +devices expose the same `DigitalReset()` and `ResetPNLong()` commands for +direct use from PyRogue and for the common calibration sequence. ## Delay Controller Readiness @@ -75,9 +77,12 @@ The `Full calibration` operation performs these phases: enabled, passing runs at the two ends of the scan are merged. All qualifying FCO eyes are retained, ordered by widest eye and then lowest center tap; the first eye is the initial choice. -4. **Enable the checkerboard pattern.** The ADC is placed in its device-specific - test mode after the initial FCO delays have been selected and frame lock has - been reacquired. +4. **Enable and synchronize the checkerboard pattern.** The ADC is placed in + its device-specific test mode after the initial FCO delays have been selected + and frame lock has been reacquired. Calibration then calls the configuration + device's normalized `DigitalReset()` command, waits for the converter output + to settle, and relocks the FPGA receiver. This establishes one deterministic + sample epoch before any data eye is measured. 5. **Build safe data-lane sweep groups.** Lanes on different logical channels can move together. Lanes on the same channel can also move together when their meaningful-bit masks do not overlap. Lanes with overlapping masks are @@ -215,9 +220,8 @@ debug setting so failure evidence is retained. `Outcome` reports `IDLE`, `RUNNING`, `PASSED`, `FAILED`, or `STOPPED` independently of PyRogue's generic process state. Successful GUI-driven runs -also finish with `Message` set to `PASSED` rather than the ambiguous generic -`Done`. `RunTime` and process progress provide the remaining operational -monitoring. +finish with PyRogue's normal process message. `RunTime` and process progress +provide the remaining operational monitoring. ## Device-Specific Behavior diff --git a/docs/plans/serialized-ddr-adc-cleanup/README.md b/docs/plans/serialized-ddr-adc-cleanup/README.md index 1b5271b178..5dac42a669 100644 --- a/docs/plans/serialized-ddr-adc-cleanup/README.md +++ b/docs/plans/serialized-ddr-adc-cleanup/README.md @@ -20,6 +20,12 @@ the alignment/calibration path deterministic, fast, and diagnostically useful. debug diagnostics enabled, but was quick with debug disabled. Calibration therefore retains the deterministic exhaustive scan and avoids repeatedly publishing the growing diagnostics tree. +- AD9249 hardware showed channel-dependent checkerboard epochs even though a + PN23 snapshot was coherent across every channel. Pulsing the ADC bank's + digital reset while checkerboard mode was selected restored coherence. + Each ADC configuration model now exposes a normalized `DigitalReset()` + command. Common calibration calls that command before data-eye measurement + and restarts the FPGA receiver afterward. ## Calibration UI Contract @@ -67,5 +73,7 @@ restoration. the target board. - Confirm the 4096-sample PN23 window acquires phase and reports all-zero reference recurrence and cross-channel coherence errors. +- Confirm repeated AD9249 calibrations retain shared checkerboard phase after + the automatic digital-datapath reset, including alternating between banks. - Increase `PatternTesterSamples` if a longer bounded checkerboard stress test is useful; this remains qualification, not a claimed BER measurement. diff --git a/python/surf/devices/analog_devices/_Ad9249.py b/python/surf/devices/analog_devices/_Ad9249.py index 749eef2fa7..d6fbe2058e 100644 --- a/python/surf/devices/analog_devices/_Ad9249.py +++ b/python/surf/devices/analog_devices/_Ad9249.py @@ -208,13 +208,14 @@ def __init__( )) self.add(pr.RemoteVariable( - name = 'ResetPNLong', + name = 'ResetPNLongReg', description = 'Reset the PN23 test-pattern generator', offset = (0x0D*4), bitSize = 1, bitOffset = 5, base = pr.Bool, mode = 'RW', + hidden = True, )) self.add(pr.RemoteVariable( @@ -330,6 +331,9 @@ def __init__( }, )) + analog_devices.addAdcDdrResetCommands( + self, self.InternalPdwnMode, self.ResetPNLongReg) + def writeBlocks(self, **kwargs: Any) -> None: """Write pending blocks and transfer them into the ADC.""" super().writeBlocks(**kwargs) @@ -482,7 +486,6 @@ def __init__( config = config, readout = readout, dataLaneToChannel = tuple(range(readout._dataLanes)), - pn23Reset = config.ResetPNLong, **kwargs) diff --git a/python/surf/devices/analog_devices/_Ad9252.py b/python/surf/devices/analog_devices/_Ad9252.py index 4b5a036992..394b23982c 100644 --- a/python/surf/devices/analog_devices/_Ad9252.py +++ b/python/surf/devices/analog_devices/_Ad9252.py @@ -132,12 +132,13 @@ def __init__(self, **kwargs: Any) -> None: base=pr.Bool)) self.add(pr.RemoteVariable( - name='ResetPNLong', + name='ResetPNLongReg', description='Reset PN long gen test mode', offset=0x34, bitSize=1, bitOffset=5, - base=pr.Bool)) + base=pr.Bool, + hidden=True)) self.add(pr.RemoteVariable( @@ -318,6 +319,9 @@ def __init__(self, **kwargs: Any) -> None: function=pr.BaseCommand.touchOne, )) + analog_devices.addAdcDdrResetCommands( + self, self.PowerDownMode, self.ResetPNLongReg, self.DeviceUpdate) + def writeBlocks(self, **kwargs: Any) -> None: """Write pending blocks and transfer them into the ADC.""" super().writeBlocks(**kwargs) @@ -393,5 +397,4 @@ def __init__( readout = readout, dataLaneToChannel = tuple(range(readout._channels)), configUpdate = config.DeviceUpdate, - pn23Reset = config.ResetPNLong, **kwargs) diff --git a/python/surf/devices/analog_devices/_Ad9681.py b/python/surf/devices/analog_devices/_Ad9681.py index 7b9a41a4a8..ec74a0fdf6 100644 --- a/python/surf/devices/analog_devices/_Ad9681.py +++ b/python/surf/devices/analog_devices/_Ad9681.py @@ -198,13 +198,14 @@ def __init__( )) self.add(pr.RemoteVariable( - name = 'ResetPNLong', + name = 'ResetPNLongReg', description = 'Reset the PN23 test-pattern generator', offset = (0x0D*4), bitSize = 1, bitOffset = 5, base = pr.Bool, mode = 'RW', + hidden = True, )) self.add(pr.RemoteVariable( @@ -436,6 +437,9 @@ def __init__( function=pr.BaseCommand.touchOne, )) + analog_devices.addAdcDdrResetCommands( + self, self.InternalPdwnMode, self.ResetPNLongReg) + def writeBlocks(self, **kwargs: Any) -> None: """Write pending blocks and transfer them into the ADC.""" super().writeBlocks(**kwargs) @@ -506,5 +510,4 @@ def __init__( readout = readout, dataLaneToChannel = tuple(range(8))+tuple(range(8)), dataLaneMasks = (0x003F,)*8+(0x3FC0,)*8, - pn23Reset = config.ResetPNLong, **kwargs) diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index d44050aab0..cd6f931808 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -29,6 +29,47 @@ class _AdcDdrCalibrationStopped(Exception): """Internal control flow for a user-requested process stop.""" +def addAdcDdrResetCommands( + device: Any, + powerMode: Any, + pn23Reset: Any, + configUpdate: Callable[[], None] | None = None) -> None: + """Add normalized digital and PN23 reset commands to an ADC config device.""" + + def digitalReset() -> None: + powerMode.set(3, write=True) + try: + if configUpdate is not None: + configUpdate() + # Retain a small explicit hold in addition to the blocking SPI + # transactions that assert and release the reset state. + time.sleep(0.001) + finally: + powerMode.set(0, write=True) + if configUpdate is not None: + configUpdate() + + def resetPnLong() -> None: + pn23Reset.set(True, write=True) + try: + if configUpdate is not None: + configUpdate() + finally: + pn23Reset.set(False, write=True) + if configUpdate is not None: + configUpdate() + + device.add(pr.LocalCommand( + name = 'DigitalReset', + description = 'Pulse the ADC digital datapath reset and return to normal operation', + function = digitalReset)) + + device.add(pr.LocalCommand( + name = 'ResetPNLong', + description = 'Synchronously restart the ADC PN23 test-pattern generator', + function = resetPnLong)) + + @dataclass(frozen=True) class AdcDdrEye: """One contiguous passing delay window and its chosen sampling tap. @@ -327,8 +368,6 @@ class AdcDdrCalibration(pr.Process): Callback that transfers staged ADC configuration writes. pn23Mode : int, optional ADC PN23 test-mode value. - pn23Reset : Any, optional - PyRogue variable or command used to restart the PN23 generator. **kwargs : Any Additional arguments forwarded to ``pyrogue.Process``. """ @@ -355,7 +394,6 @@ def __init__( expectedPatterns: Iterable[int] | None = None, configUpdate: Callable[[], None] | None = None, pn23Mode: int = 5, - pn23Reset: Any | None = None, **kwargs: Any) -> None: """Construct a calibration process for one normalized ADC readout.""" @@ -419,7 +457,6 @@ def __init__( self._testMode = testMode self._configUpdate = configUpdate self._pn23Mode = pn23Mode - self._pn23Reset = pn23Reset self._runDiagnostics = {} self._lastCaptureDiagnostics = {} self._deepPatternTesterActive = False @@ -488,7 +525,7 @@ def __init__( description = ( 'After checkerboard alignment, compare one four-sample PN23 ' 'snapshot across channels and verify its recurrence'), - value = pn23Reset is not None, + value = callable(getattr(config, 'ResetPNLong', None)), mode = 'RW')) self.add(pr.LocalVariable( @@ -875,19 +912,12 @@ def _captureDeepPn23Passes(self, xorMask: int) -> dict[str, Any]: def _setPn23Mode(self) -> None: """Select PN23 and synchronously restart every selected generator.""" - if self._pn23Reset is None: - raise RuntimeError('PN23 verification requires a PN-long reset control') + resetPnLong = getattr(self._config, 'ResetPNLong', None) + if not callable(resetPnLong): + raise RuntimeError('PN23 verification requires config.ResetPNLong()') - # Publish mode and asserted reset together, then explicitly clear reset. - # This avoids depending on whether a particular ADC implements the reset - # field as a pulse, a level, or a self-clearing staged register bit. - self._config.OutputTestMode.set(self._pn23Mode, write=True) - self._pn23Reset.set(True, write=True) - if self._configUpdate is not None: - self._configUpdate() - self._pn23Reset.set(False, write=True) - if self._configUpdate is not None: - self._configUpdate() + self._setTestMode(self._pn23Mode) + resetPnLong() def _capturePn23Passes(self) -> dict[str, Any]: """Check channel coherence and PN23 recurrence in one atomic snapshot.""" @@ -931,6 +961,24 @@ def _setTestMode(self, value: int) -> None: # command is issued. Device adapters provide that command here. self._configUpdate() + def _enterAlignmentPattern(self, settle: float) -> None: + """Select checkerboard and establish a deterministic ADC sample epoch.""" + + self._setTestMode(self._testMode) + time.sleep(settle) + digitalReset = getattr(self._config, 'DigitalReset', None) + if not callable(digitalReset): + raise RuntimeError('ADC calibration requires config.DigitalReset()') + # Alternating patterns can originate in independent channel-local + # digital pipelines. Reset those pipelines only after the pattern is + # selected, then restart FPGA word alignment against the newly + # synchronized ADC output. + self.Message.set('Resetting ADC digital datapath for pattern alignment') + digitalReset() + time.sleep(settle) + self._readout.Relock() + time.sleep(settle) + def _scanFco( self, lane: int, @@ -1045,8 +1093,7 @@ def _fullCalibration( # Put the ADC into a known alternating pattern only after FCO lock is # established. Each overlap-safe lane group is swept once and centered # before the next group is measured. - self._setTestMode(self._testMode) - time.sleep(settle) + self._enterAlignmentPattern(settle) for lanes in self._dataLaneGroups: self.Message.set(f'Scanning data lanes {list(lanes)}') groupPassing = self._scanDataGroup(lanes, taps, settle) @@ -1207,8 +1254,7 @@ def _verifyCurrent( self.setTotalSteps(candidatesPerLane*(len(originalFco)+len(originalData))) self.setStep(0) - self._setTestMode(self._testMode) - time.sleep(settle) + self._enterAlignmentPattern(settle) for kind, currentValues in ( ('Fco', originalFco), ('Data', originalData)): diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 0bf3543ba9..77e18e878e 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -58,12 +58,20 @@ def value(self): class FakeConfig: def __init__(self): self.OutputTestMode = FakeVariable(0) - self.ResetPNLong = FakeVariable(False) + self.ResetPNLongReg = FakeVariable(False) + self.pn23ResetValues = [] + self.digitalResetValues = [] self.updateCount = 0 def update(self): self.updateCount += 1 + def DigitalReset(self): + self.digitalResetValues.extend((3, 0)) + + def ResetPNLong(self): + self.pn23ResetValues.extend((True, False)) + class FakePatternTester: def __init__(self, readout): @@ -628,6 +636,35 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert readout.DataDelay[1].value() == 5 +def test_pattern_alignment_reset_runs_after_checkerboard_selection(): + events = [] + calibration, config, readout, root = make_calibration() + originalReset = config.DigitalReset + + def record(): + events.append({ + 'mode': config.OutputTestMode.value(), + 'snapshots': len(readout.snapshotDelays), + 'relocks': readout.relockCount, + }) + return originalReset() + + config.DigitalReset = record + try: + calibration._runCalibration(dev=calibration) + + assert events == [ + {'mode': 4, 'snapshots': 0, 'relocks': 9}, + ] + assert config.digitalResetValues == [3, 0] + assert config.updateCount == 2 + # The receiver restarts after the ADC digital reset has been released. + assert readout.relockCount >= events[0]['relocks']+1 + finally: + calibration._runEn = False + root.stop() + + def test_diagnostics_publish_only_at_process_boundaries( calibration_fixture, monkeypatch): calibration, _, _ = calibration_fixture @@ -744,8 +781,7 @@ def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): name='Calibration', config=config, readout=readout, - configUpdate=config.update, - pn23Reset=config.ResetPNLong) + configUpdate=config.update) root = pr.Root(name='Root', pollEn=False) root.add(calibration) root.start() @@ -769,7 +805,7 @@ def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): assert results['Final']['pn23']['passed'] assert results['Final']['passed'] assert config.OutputTestMode.value() == 0 - assert not config.ResetPNLong.value() + assert config.pn23ResetValues == [True, False] # PN23 always starts with one atomic snapshot. Deep qualification adds # one checkerboard and one PN23 tester window after centering. assert readout.snapshotDelays[-1] == (2, 5) @@ -792,8 +828,7 @@ def test_deep_pn23_error_beyond_snapshot_is_reported(): name='Calibration', config=config, readout=readout, - configUpdate=config.update, - pn23Reset=config.ResetPNLong) + configUpdate=config.update) root = pr.Root(name='Root', pollEn=False) root.add(calibration) root.start() @@ -837,8 +872,7 @@ def test_pn23_qualification_rejects_relative_and_common_errors( name='Calibration', config=config, readout=readout, - configUpdate=config.update, - pn23Reset=config.ResetPNLong) + configUpdate=config.update) root = pr.Root(name='Root', pollEn=False) root.add(calibration) root.start() @@ -1132,8 +1166,7 @@ def test_pn23_failure_retries_alternate_fco_eye_combination_in_checkerboard_mode readout=readout, dataLaneToChannel=(0, 0), dataLaneMasks=(0x003F, 0x3FC0), - configUpdate=config.update, - pn23Reset=config.ResetPNLong) + configUpdate=config.update) root = pr.Root(name='Root', pollEn=False) root.add(calibration) root.start() diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py index fab4117a35..f5cf79eced 100644 --- a/tests/devices/analog_devices/test_AdcDdrModel.py +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -361,6 +361,8 @@ def test_device_specific_calibration_adapters(): readout=ad9249Readout) assert ad9249._dataLaneToChannel == tuple(range(8)) assert ad9249._configUpdate is None + assert ad9249Config.DigitalReset.name == 'DigitalReset' + assert ad9249Config.ResetPNLong.name == 'ResetPNLong' ad9249Full = Ad9249ReadoutCalibration( name='Ad9249FullCalibration', @@ -378,6 +380,8 @@ def test_device_specific_calibration_adapters(): readout=ad9252Readout) assert ad9252._dataLaneToChannel == tuple(range(8)) assert ad9252._configUpdate == ad9252Config.DeviceUpdate + assert ad9252Config.DigitalReset.name == 'DigitalReset' + assert ad9252Config.ResetPNLong.name == 'ResetPNLong' ad9681Config = Ad9681Config(name='Ad9681Config') ad9681Readout = Ad9681Readout(name='Ad9681Readout') @@ -387,6 +391,8 @@ def test_device_specific_calibration_adapters(): readout=ad9681Readout) assert ad9681._dataLaneToChannel == tuple(range(8))+tuple(range(8)) assert ad9681._configUpdate is None + assert ad9681Config.DigitalReset.name == 'DigitalReset' + assert ad9681Config.ResetPNLong.name == 'ResetPNLong' assert ad9681.DelayStart.units == 'tap' assert ad9681.DelayStop.units == 'tap' assert ad9681.MinimumEyeWidth.units == 'tap' From 28be0791eb1458fc3fa059a3de7f9dada2104c23 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 15:32:38 -0700 Subject: [PATCH 48/57] Update FakeConfig to support pn23 parameter and adjust related tests --- .../devices/analog_devices/test_AdcDdrCalibration.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 77e18e878e..b729d5b445 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -56,12 +56,14 @@ def value(self): class FakeConfig: - def __init__(self): + def __init__(self, pn23=False): self.OutputTestMode = FakeVariable(0) self.ResetPNLongReg = FakeVariable(False) self.pn23ResetValues = [] self.digitalResetValues = [] self.updateCount = 0 + if not pn23: + self.ResetPNLong = None def update(self): self.updateCount += 1 @@ -775,7 +777,7 @@ def test_deep_pattern_error_is_reported_and_fails_final_qualification(): @pytest.mark.parametrize('usePatternTester', [False, True]) def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): - config = FakeConfig() + config = FakeConfig(pn23=True) readout = Pn23FakeReadout(config) calibration = AdcDdrCalibration( name='Calibration', @@ -822,7 +824,7 @@ def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): def test_deep_pn23_error_beyond_snapshot_is_reported(): - config = FakeConfig() + config = FakeConfig(pn23=True) readout = Pn23FakeReadout(config, fault='deepCommonCorruption') calibration = AdcDdrCalibration( name='Calibration', @@ -866,7 +868,7 @@ def test_deep_pn23_error_beyond_snapshot_is_reported(): ]) def test_pn23_qualification_rejects_relative_and_common_errors( fault, coherencePassed, recurrencePassed): - config = FakeConfig() + config = FakeConfig(pn23=True) readout = Pn23FakeReadout(config, fault=fault) calibration = AdcDdrCalibration( name='Calibration', @@ -1158,7 +1160,7 @@ def test_final_qualification_retries_alternate_fco_eye_combinations(usePatternTe def test_pn23_failure_retries_alternate_fco_eye_combination_in_checkerboard_mode(): - config = FakeConfig() + config = FakeConfig(pn23=True) readout = Pn23MultiWindowFcoFakeReadout(config) calibration = AdcDdrCalibration( name='Calibration', From e65c6e0c56f8eb0d16330144e4bde172a97ba17f Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 15:46:19 -0700 Subject: [PATCH 49/57] Enhance ADC calibration process by refining checkerboard qualification steps and updating related tests --- devices/AnalogDevices/adcDdr/README.md | 14 +++++++++----- docs/plans/serialized-ddr-adc-cleanup/README.md | 9 +++++++-- .../analog_devices/_AdcDdrCalibration.py | 17 +++++++++-------- .../analog_devices/test_AdcDdrCalibration.py | 14 +++++++++----- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index 63ea178440..7c7c66067a 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -97,11 +97,15 @@ The `Full calibration` operation performs these phases: partner half does not contaminate it. Each lane gets its own passing map and centered eye even though compatible lanes are swept together. -7. **Qualify the assembled checkerboard samples.** With every data lane centered, software - checks the complete sample width. Channel zero establishes the checkerboard - A/B phase, and every logical channel must match the same ordered sequence. - This catches half-word or sample-epoch errors that individual masked lane - tests and a repetitive FCO word cannot detect. +7. **Qualify the assembled checkerboard samples.** With every data lane + centered, software reselects checkerboard, issues another `DigitalReset()`, + waits for the ADC output to settle, and relocks the FPGA receiver immediately + before checking the complete sample width. This sequence is repeated for + every alternate FCO-eye attempt, closing the interval in which a parallel + bank calibration could disturb shared ADC digital state. Channel zero + establishes the checkerboard A/B phase, and every logical channel must match + the same ordered sequence. This catches half-word or sample-epoch errors that + individual masked lane tests and a repetitive FCO word cannot detect. 8. **Run the optional deep checkerboard window.** When `UsePatternTester` is enabled, the hardware pattern tester checks `PatternTesterSamples` complete samples at line rate. Every channel must maintain one shared alternating diff --git a/docs/plans/serialized-ddr-adc-cleanup/README.md b/docs/plans/serialized-ddr-adc-cleanup/README.md index 5dac42a669..7a3e5f0bc1 100644 --- a/docs/plans/serialized-ddr-adc-cleanup/README.md +++ b/docs/plans/serialized-ddr-adc-cleanup/README.md @@ -25,7 +25,11 @@ the alignment/calibration path deterministic, fast, and diagnostically useful. digital reset while checkerboard mode was selected restored coherence. Each ADC configuration model now exposes a normalized `DigitalReset()` command. Common calibration calls that command before data-eye measurement - and restarts the FPGA receiver afterward. + and restarts the FPGA receiver afterward. Because the failure was observed + primarily while eight banks calibrated in parallel, calibration now repeats + that checkerboard/reset/relock sequence immediately before every final + qualification attempt as well. This removes the long data-scan interval from + the ADC pattern-epoch assumption. ## Calibration UI Contract @@ -74,6 +78,7 @@ restoration. - Confirm the 4096-sample PN23 window acquires phase and reports all-zero reference recurrence and cross-channel coherence errors. - Confirm repeated AD9249 calibrations retain shared checkerboard phase after - the automatic digital-datapath reset, including alternating between banks. + the automatic digital-datapath reset, including all eight banks running in + parallel and alternating between banks. - Increase `PatternTesterSamples` if a longer bounded checkerboard stress test is useful; this remains qualification, not a claimed BER measurement. diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index cd6f931808..f87d8015a4 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -1139,7 +1139,8 @@ def _fullCalibration( if index != 0: # Data-eye locations are independent of which equivalent FCO # window establishes frame phase, so retries only move FCO taps - # and relock; the expensive data sweeps do not need repeating. + # before repeating final qualification; the expensive data + # sweeps do not need repeating. selected = { lane: eye.selected for lane, eye in enumerate(combination) @@ -1149,13 +1150,13 @@ def _fullCalibration( f'{[selected[lane] for lane in range(self._fcoLanes)]}') for lane, tap in selected.items(): self._readout.FcoDelay[lane].set(tap, write=True) - self._readout.Relock() - time.sleep(settle) - # A previous combination may have reached PN23 qualification. - # Restore checkerboard before judging this combination so every - # FCO candidate is subject to the same two-stage test. - self._setTestMode(self._testMode) - time.sleep(settle) + + # Re-establish the ADC pattern epoch immediately before every final + # attempt. This closes the long window between the reset preceding + # data-eye scans and final qualification, during which another bank + # calibrating in parallel may disturb shared ADC digital state. It + # also restores checkerboard after a prior attempt reached PN23. + self._enterAlignmentPattern(settle) checkerboard = self._captureFinalPasses() candidate = copy.deepcopy(checkerboard) diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index b729d5b445..5d117b947e 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -615,7 +615,7 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert results['Data'][1]['eye']['selected'] == 5 assert results['Final']['passed'] assert config.OutputTestMode.value() == 0 - assert config.updateCount == 2 + assert config.updateCount == 3 assert readout.FcoDelay[0].value() == 3 assert readout.DataDelay[0].value() == 2 assert readout.DataDelay[1].value() == 5 @@ -657,11 +657,12 @@ def record(): assert events == [ {'mode': 4, 'snapshots': 0, 'relocks': 9}, + {'mode': 4, 'snapshots': 8, 'relocks': 10}, ] - assert config.digitalResetValues == [3, 0] - assert config.updateCount == 2 - # The receiver restarts after the ADC digital reset has been released. - assert readout.relockCount >= events[0]['relocks']+1 + assert config.digitalResetValues == [3, 0, 3, 0] + assert config.updateCount == 3 + # The receiver restarts after each ADC digital reset has been released. + assert readout.relockCount >= events[-1]['relocks']+1 finally: calibration._runEn = False root.stop() @@ -1154,6 +1155,9 @@ def test_final_qualification_retries_alternate_fco_eye_combinations(usePatternTe assert readout.FcoDelay[1].value() == 1 assert readout.DataDelay[0].value() == 2 assert readout.DataDelay[1].value() == 5 + # One reset precedes the data-eye scan and one immediately precedes + # each of the two final FCO-combination attempts. + assert config.digitalResetValues == [3, 0, 3, 0, 3, 0] finally: calibration._runEn = False root.stop() From ea18bb7501624c66d650efa135f569c70bb7f99d Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 15:48:02 -0700 Subject: [PATCH 50/57] Refactor ADC reset commands to use descriptive power mode names and add tests for digital reset functionality --- .../analog_devices/_AdcDdrCalibration.py | 4 +-- .../analog_devices/test_AdcDdrModel.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index f87d8015a4..e93fbaa0f1 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -37,7 +37,7 @@ def addAdcDdrResetCommands( """Add normalized digital and PN23 reset commands to an ADC config device.""" def digitalReset() -> None: - powerMode.set(3, write=True) + powerMode.setDisp('Digital Reset', write=True) try: if configUpdate is not None: configUpdate() @@ -45,7 +45,7 @@ def digitalReset() -> None: # transactions that assert and release the reset state. time.sleep(0.001) finally: - powerMode.set(0, write=True) + powerMode.setDisp('Chip Run', write=True) if configUpdate is not None: configUpdate() diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py index f5cf79eced..c764aa9c2f 100644 --- a/tests/devices/analog_devices/test_AdcDdrModel.py +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -512,6 +512,32 @@ def test_device_update_writes_transfer_strobe(configType): root.stop() +@pytest.mark.parametrize( + ('configType', 'powerModeName'), + ( + (Ad9249ConfigGroup, 'InternalPdwnMode'), + (Ad9252Config, 'PowerDownMode'), + (Ad9681Config, 'InternalPdwnMode'), + )) +def test_digital_reset_uses_common_power_mode_enums(configType, powerModeName): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + config = configType(name='Config', memBase=memory) + root.add(config) + root.start() + try: + powerMode = getattr(config, powerModeName) + + assert powerMode.enum[3] == 'Digital Reset' + assert powerMode.enum[0] == 'Chip Run' + + config.DigitalReset() + + assert powerMode.getDisp(read=False) == 'Chip Run' + finally: + root.stop() + + def test_device_specific_readout_geometry(): ad9249 = Ad9249Readout(name='Ad9249') assert len(ad9249.Bank) == 2 From 166cbedce06ebe71ec32b137de1d0ef816efa211 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 16:00:06 -0700 Subject: [PATCH 51/57] Reduce combinational paths. --- .../adcDdr/rtl/AdcDdrPatternTester.vhd | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd index bb97c38805..b64baef3d5 100644 --- a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd @@ -252,6 +252,7 @@ begin if (r.wordErrorCount(i) /= x"FFFFFFFF") then v.wordErrorCount(i) := std_logic_vector(unsigned(r.wordErrorCount(i)) + 1); end if; + v.channelPassed(i) := '0'; v.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) := r.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) or v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0); @@ -263,15 +264,12 @@ begin if (unsigned(r.checkedSamples) + 1 >= unsigned(r.requestedSamples)) then v.busy := '0'; v.done := '1'; + -- channelPassed is maintained per-sample above, so completion is + -- just an AND-reduce of the clean flags over the masked channels. v.allChannelsPass := '1'; for i in CHANNELS_G-1 downto 0 loop - if (r.channelMask(i) = '1') then - if (v.wordErrorCount(i) = x"00000000") then - v.channelPassed(i) := '1'; - else - v.channelPassed(i) := '0'; - v.allChannelsPass := '0'; - end if; + if (r.channelMask(i) = '1' and v.channelPassed(i) = '0') then + v.allChannelsPass := '0'; end if; end loop; v.allFcoPass := '1'; @@ -325,7 +323,10 @@ begin v.pnHistory := (others => '0'); v.pnHistoryCount := 0; v.checkedSamples := (others => '0'); - v.channelPassed := (others => '0'); + -- channelPassed is a running "clean" flag: masked-in channels start + -- passing and clear on the first word error, so the completion decision + -- is a cheap reduce instead of a wide compare of the 32-bit counters. + v.channelPassed := r.cfgChannelMask; v.fcoPassed := (others => '0'); v.fcoSeen := (others => '0'); v.wordErrorCount := (others => (others => '0')); From 981e96c3481d22497eaae00d6e4efa7335d725f3 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 19:52:41 -0700 Subject: [PATCH 52/57] docs(gtx7): document fixed-latency RX alignment --- xilinx/7Series/gtx7/README.md | 46 +++++++++++++++++++++++++++++++++++ xilinx/README.md | 1 + 2 files changed, 47 insertions(+) create mode 100644 xilinx/7Series/gtx7/README.md diff --git a/xilinx/7Series/gtx7/README.md b/xilinx/7Series/gtx7/README.md new file mode 100644 index 0000000000..16216b1639 --- /dev/null +++ b/xilinx/7Series/gtx7/README.md @@ -0,0 +1,46 @@ +# GTX7 Support + +This directory contains the SURF wrapper, reset state machines, clock monitoring, and phase-alignment +helpers for the AMD/Xilinx 7 Series `GTXE2_CHANNEL` primitive. `Gtx7Core` is the main integration +point, and the directory-level `ruckus.tcl` loads the sources in `rtl/`. + +## Fixed-latency RX alignment + +`Gtx7RxFixedLatPhaseAligner` aligns a comma in raw parallel RX data while the RX elastic buffer is +bypassed. This configuration requires: + +- `RX_ALIGN_MODE_G = "FIXED_LAT"` +- `RX_BUF_EN_G = false` +- `RXSLIDE_MODE_G = "PMA"` + +In PMA slide mode, `RXSLIDE` moves the parallel data by one bit per pulse, but the recovered output +clock changes phase only on every other pulse. An even number of slides therefore preserves the +relationship between the aligned comma and `RXOUTCLK`. An odd comma landing needs one of two policies, +selected by `RX_ODD_ALIGN_MODE_G`: + +| Mode | Odd landing behavior | Latency and phase contract | +| --- | --- | --- | +| `"RESET"` | Request another RX initialization and accept only a landing requiring an even number of slides. | Intended for applications requiring the recovered-clock phase to match the aligned serial UI. Bring-up can retry without bound. | +| `"BITSLIP"` | Use only an even number of PMA slides, leave a one-bit residue, and repair the word boundary in fabric. | Adds exactly one `rxUsrClk` stage for every landing. Parallel-word latency is deterministic, but the odd and even landing classes may differ in recovered-clock phase by as much as one serial UI. | + +`"BITSLIP"` does not request an RX reset after an odd landing. Its caller must drive `rxDataValidIn` +from a decoder so `Gtx7RxRst` can restart alignment if the link later loses validity. Leaving +`rxDataValidIn` at its default of `'1'` disables that recovery path. + +## Shared CPLL reset ownership + +When TX and RX both select the channel CPLL, `Gtx7Core` gives the TX reset state machine sole ownership +of `CPLLRESET`. This prevents an RX-only retry from resetting the PLL underneath an active TX without +also resetting the TX datapath. The RX reset state machine still asserts `GTRXRESET`, which reinitializes +the RX datapath and CDR, but its separate PLL-reset request is not selected onto the shared CPLL reset. + +Consequently, `RX_ODD_ALIGN_MODE_G = "RESET"` retries do not reinitialize the shared CPLL. If odd/even +landing parity is correlated with CPLL or TX state, an RX-only retry can repeatedly return to the same +odd class. Do not fix that by ORing the RX PLL-reset request directly onto `CPLLRESET`: TX would lose +its clock while its reset state machine continued to report stale state. A system requiring both strict +serial-UI phase and shared-CPLL recovery must coordinate both reset state machines and let the TX reset +state machine remain the sole CPLL-reset owner. + +A coordinated CPLL restart changes more shared state than `GTRXRESET`, but the GTX documentation does +not guarantee that it changes odd/even comma-landing parity. Any such recovery should therefore remain +bounded and expose a failure condition rather than repeatedly disrupting TX without limit. diff --git a/xilinx/README.md b/xilinx/README.md index 14824b96e2..9914b89c60 100644 --- a/xilinx/README.md +++ b/xilinx/README.md @@ -5,6 +5,7 @@ This tree contains Xilinx-specific RTL wrappers, primitive integrations, and hel ## Layout - Family folders such as `7Series/`, `Virtex5/`, `UltraScale/`, `UltraScale+/`, and `Versal/` hold family-specific wrappers and primitive integrations. +- `7Series/gtx7/` contains the GTXE2 channel wrapper and documents its [fixed-latency RX alignment and shared-CPLL reset behavior](7Series/gtx7/README.md). - `general/` contains Xilinx helpers that are not tied to a single family directory. - `xvc-udp/` contains Xilinx Virtual Cable over UDP support and has its own [README.md](xvc-udp/README.md). - `dummy/` contains placeholder or compatibility support used by build flows. From 75585b4771edf0fb984e4249aa7b0284325c60b8 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 27 Aug 2026 13:56:49 -0700 Subject: [PATCH 53/57] docs(axi-lite): enhance README with additional details on reset handling for AxiLiteAsync --- axi/axi-lite/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/axi/axi-lite/README.md b/axi/axi-lite/README.md index a9e4f3ff07..670a8386d0 100644 --- a/axi/axi-lite/README.md +++ b/axi/axi-lite/README.md @@ -38,6 +38,12 @@ domain resets. Reset handling follows these rules: - Transactions discarded by reset are not replayed when the master domain recovers, and stale responses do not survive a slave-domain reset. +A slave/source-domain reset can flush only transactions still held by the +bridge. It cannot retract a write-address or write-data beat that the +downstream slave has already accepted. If a system must prevent a partially +accepted downstream write from surviving reset, it must quiesce AXI-Lite +traffic before resetting or coordinate reset of the destination domain. + If a clock is unavailable, its corresponding reset must remain asserted. The clock must be stable before reset is released, and traffic must remain inactive until synchronized reset release completes. From c546285045fa87ec3da522f14a5f9c9428f3ac0d Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 27 Aug 2026 14:14:37 -0700 Subject: [PATCH 54/57] The ADC cocotb tests now: - Explicitly classify and clean up lifetime clock tasks. - Bound FCO edge waits with timeouts. - Document intentional post-edge propagation delays. - Mark the pattern-disabled path as a complete terminal scenario. --- tests/devices/analog_devices/test_Ad9249Sim.py | 13 ++++++++----- tests/devices/analog_devices/test_Ad9249SimCore.py | 2 ++ .../devices/analog_devices/test_Ad9249SimTiming.py | 6 ++++-- tests/devices/analog_devices/test_Ad9252Sim.py | 14 +++++++++----- tests/devices/analog_devices/test_Ad9252SimCore.py | 2 ++ .../devices/analog_devices/test_Ad9252SimTiming.py | 6 ++++-- tests/devices/analog_devices/test_Ad9681Sim.py | 14 +++++++++----- tests/devices/analog_devices/test_Ad9681SimCore.py | 2 ++ .../devices/analog_devices/test_Ad9681SimTiming.py | 6 ++++-- tests/devices/analog_devices/test_AdcDdrCore.py | 3 +++ .../analog_devices/test_AdcDdrPatternTester.py | 1 + 11 files changed, 48 insertions(+), 21 deletions(-) diff --git a/tests/devices/analog_devices/test_Ad9249Sim.py b/tests/devices/analog_devices/test_Ad9249Sim.py index 228cc43df3..24da6dc93c 100644 --- a/tests/devices/analog_devices/test_Ad9249Sim.py +++ b/tests/devices/analog_devices/test_Ad9249Sim.py @@ -20,12 +20,13 @@ # both banks otherwise use ideal centered DCO/FCO timing in this test. import cocotb -from cocotb.triggers import Edge, Timer +from cocotb.triggers import Edge, Timer, with_timeout -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" half = period_ns / 2 while True: dut.clkP.value = 0 @@ -62,12 +63,13 @@ async def spi_write(dut, bank, address, value): async def wait_fco_rise(dut, bank): previous = (int(dut.fcoP.value) >> bank) & 1 - while True: - await Edge(dut.fcoP) + for _ in range(3): + await with_timeout(Edge(dut.fcoP), 100, "ns") current = (int(dut.fcoP.value) >> bank) & 1 if previous == 0 and current == 1: return previous = current + assert False, f"FCO bank {bank} did not produce a rising edge" async def capture_bank(dut, bank): @@ -91,7 +93,7 @@ async def ad9249_pin_level_device_sim_test(dut): dut.sdioDrive.value = 0 dut.sdioEnable.value = 0 dut.csb.value = 0b11 - cocotb.start_soon(differential_clock(dut)) + clock_task = cocotb.start_soon(differential_clock(dut)) # Allow the model's time-zero output initialization to settle before any # std_logic_vector is converted to an integer by the frame collector. await Timer(1, unit="ns") @@ -133,6 +135,7 @@ async def ad9249_pin_level_device_sim_test(dut): assert checkerboard[0] == checkerboard[2] assert checkerboard[1] == checkerboard[3] assert checkerboard[0] != checkerboard[1] + await cancel_and_join_tasks((clock_task,)) def test_Ad9249Sim(): diff --git a/tests/devices/analog_devices/test_Ad9249SimCore.py b/tests/devices/analog_devices/test_Ad9249SimCore.py index 1bc0a46743..d341d0e604 100644 --- a/tests/devices/analog_devices/test_Ad9249SimCore.py +++ b/tests/devices/analog_devices/test_Ad9249SimCore.py @@ -28,6 +28,7 @@ async def write(dut, addr, data): + """Propagation sampling: deassert writes after registered TPD updates.""" await FallingEdge(dut.sampleClk) dut.cfgAddr.value = addr dut.cfgWrData.value = data @@ -44,6 +45,7 @@ async def read(dut, addr): async def sample(dut): + """Propagation sampling: read each registered sample after its TPD update.""" await FallingEdge(dut.sampleClk) dut.sampleEnable.value = 1 await RisingEdge(dut.sampleClk) diff --git a/tests/devices/analog_devices/test_Ad9249SimTiming.py b/tests/devices/analog_devices/test_Ad9249SimTiming.py index ae3e15e7de..0d62f6792f 100644 --- a/tests/devices/analog_devices/test_Ad9249SimTiming.py +++ b/tests/devices/analog_devices/test_Ad9249SimTiming.py @@ -21,10 +21,11 @@ from cocotb.triggers import Edge, Timer from cocotb.utils import get_sim_time -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" half = period_ns / 2 while True: dut.clkP.value = 0 @@ -66,7 +67,7 @@ async def ad9249_binary_skew_and_jitter_test(dut): dut.sdioDrive.value = 0 dut.sdioEnable.value = 0 dut.csb.value = 0b11 - cocotb.start_soon(differential_clock(dut)) + clock_task = cocotb.start_soon(differential_clock(dut)) await Timer(480, unit="ns") @@ -105,6 +106,7 @@ async def ad9249_binary_skew_and_jitter_test(dut): assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + await cancel_and_join_tasks((clock_task,)) def test_Ad9249SimTiming(): diff --git a/tests/devices/analog_devices/test_Ad9252Sim.py b/tests/devices/analog_devices/test_Ad9252Sim.py index 4bc1bb3113..d143ec8088 100644 --- a/tests/devices/analog_devices/test_Ad9252Sim.py +++ b/tests/devices/analog_devices/test_Ad9252Sim.py @@ -19,12 +19,13 @@ # data and FCO otherwise use ideal centered timing in this test. import cocotb -from cocotb.triggers import Edge, Timer +from cocotb.triggers import Edge, Timer, with_timeout -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" half = period_ns / 2 while True: dut.clkP.value = 0 @@ -61,12 +62,14 @@ async def spi_write(dut, address, value): async def capture_frame(dut): previous = int(dut.fcoP.value) - while True: - await Edge(dut.fcoP) + for _ in range(3): + await with_timeout(Edge(dut.fcoP), 100, "ns") current = int(dut.fcoP.value) if previous == 0 and current == 1: break previous = current + else: + assert False, "FCO did not produce a rising edge" words = [0] * 8 frame = 0 for _ in range(14): @@ -86,7 +89,7 @@ async def ad9252_pin_level_device_sim_test(dut): dut.sdioDrive.value = 0 dut.sdioEnable.value = 0 dut.csb.value = 1 - cocotb.start_soon(differential_clock(dut)) + clock_task = cocotb.start_soon(differential_clock(dut)) # Allow the model's time-zero output initialization to settle before any # std_logic value is converted to an integer by the frame collector. await Timer(1, unit="ns") @@ -125,6 +128,7 @@ async def ad9252_pin_level_device_sim_test(dut): assert checkerboard[0] == checkerboard[2] assert checkerboard[1] == checkerboard[3] assert checkerboard[0] != checkerboard[1] + await cancel_and_join_tasks((clock_task,)) def test_Ad9252Sim(): diff --git a/tests/devices/analog_devices/test_Ad9252SimCore.py b/tests/devices/analog_devices/test_Ad9252SimCore.py index d92b21eb45..a4fb53cb78 100644 --- a/tests/devices/analog_devices/test_Ad9252SimCore.py +++ b/tests/devices/analog_devices/test_Ad9252SimCore.py @@ -26,6 +26,7 @@ async def write(dut, addr, data): + """Propagation sampling: deassert writes after registered TPD updates.""" await FallingEdge(dut.sampleClk) dut.cfgAddr.value = addr dut.cfgWrData.value = data @@ -42,6 +43,7 @@ async def read(dut, addr): async def sample(dut): + """Propagation sampling: read each registered sample after its TPD update.""" await FallingEdge(dut.sampleClk) dut.sampleEnable.value = 1 await RisingEdge(dut.sampleClk) diff --git a/tests/devices/analog_devices/test_Ad9252SimTiming.py b/tests/devices/analog_devices/test_Ad9252SimTiming.py index e8d7b3388b..3ca4eef611 100644 --- a/tests/devices/analog_devices/test_Ad9252SimTiming.py +++ b/tests/devices/analog_devices/test_Ad9252SimTiming.py @@ -21,10 +21,11 @@ from cocotb.triggers import Edge, Timer from cocotb.utils import get_sim_time -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" half = period_ns / 2 while True: dut.clkP.value = 0 @@ -66,7 +67,7 @@ async def ad9252_binary_skew_and_jitter_test(dut): dut.sdioDrive.value = 0 dut.sdioEnable.value = 0 dut.csb.value = 1 - cocotb.start_soon(differential_clock(dut)) + clock_task = cocotb.start_soon(differential_clock(dut)) await Timer(288, unit="ns") @@ -105,6 +106,7 @@ async def ad9252_binary_skew_and_jitter_test(dut): assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFF) assert int(dut.dcoN.value) == (not int(dut.dcoP.value)) assert int(dut.fcoN.value) == (not int(dut.fcoP.value)) + await cancel_and_join_tasks((clock_task,)) def test_Ad9252SimTiming(): diff --git a/tests/devices/analog_devices/test_Ad9681Sim.py b/tests/devices/analog_devices/test_Ad9681Sim.py index 6b110475b2..5424870a09 100644 --- a/tests/devices/analog_devices/test_Ad9681Sim.py +++ b/tests/devices/analog_devices/test_Ad9681Sim.py @@ -20,12 +20,13 @@ # both byte groups share an ideal centered DCO/FCO waveform. import cocotb -from cocotb.triggers import Edge, Timer +from cocotb.triggers import Edge, Timer, with_timeout -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test async def differential_clock(dut, period_ns=8): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" half = period_ns / 2 while True: dut.clkP.value = 0 @@ -62,12 +63,14 @@ async def spi_write(dut, address, value): async def capture_frame(dut): previous = int(dut.fcoP.value) & 1 - while True: - await Edge(dut.fcoP) + for _ in range(3): + await with_timeout(Edge(dut.fcoP), 100, "ns") current = int(dut.fcoP.value) & 1 if previous == 0 and current == 1: break previous = current + else: + assert False, "FCO did not produce a rising edge" low = [0] * 8 high = [0] * 8 frame = 0 @@ -89,7 +92,7 @@ async def ad9681_pin_level_device_sim_test(dut): dut.sdioDrive.value = 0 dut.sdioEnable.value = 0 dut.csb.value = 1 - cocotb.start_soon(differential_clock(dut)) + clock_task = cocotb.start_soon(differential_clock(dut)) await Timer(1, unit="ns") # A normal conversion captured with the first frame must remain absent for @@ -131,6 +134,7 @@ async def ad9681_pin_level_device_sim_test(dut): assert checkerboard[0] == checkerboard[2] assert checkerboard[1] == checkerboard[3] assert checkerboard[0] != checkerboard[1] + await cancel_and_join_tasks((clock_task,)) def test_Ad9681Sim(): diff --git a/tests/devices/analog_devices/test_Ad9681SimCore.py b/tests/devices/analog_devices/test_Ad9681SimCore.py index bbf3c4d8d8..7c03bb6a3c 100644 --- a/tests/devices/analog_devices/test_Ad9681SimCore.py +++ b/tests/devices/analog_devices/test_Ad9681SimCore.py @@ -29,6 +29,7 @@ async def write(dut, addr, data): + """Propagation sampling: deassert writes after registered TPD updates.""" await FallingEdge(dut.sampleClk) dut.cfgAddr.value = addr dut.cfgWrData.value = data @@ -45,6 +46,7 @@ async def read(dut, addr): async def sample(dut): + """Propagation sampling: read each registered sample after its TPD update.""" await FallingEdge(dut.sampleClk) dut.sampleEnable.value = 1 await RisingEdge(dut.sampleClk) diff --git a/tests/devices/analog_devices/test_Ad9681SimTiming.py b/tests/devices/analog_devices/test_Ad9681SimTiming.py index dc17bd3724..cb7768a37d 100644 --- a/tests/devices/analog_devices/test_Ad9681SimTiming.py +++ b/tests/devices/analog_devices/test_Ad9681SimTiming.py @@ -24,10 +24,11 @@ from cocotb.triggers import Edge, Timer from cocotb.utils import get_sim_time -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test async def differential_clock(dut, period_ns=8): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" half = period_ns / 2 while True: dut.clkP.value = 0 @@ -71,7 +72,7 @@ async def ad9681_binary_skew_and_jitter_test(dut): dut.sdioDrive.value = 0 dut.sdioEnable.value = 0 dut.csb.value = 1 - cocotb.start_soon(differential_clock(dut)) + clock_task = cocotb.start_soon(differential_clock(dut)) # Allow the modeled 16-sample ADC conversion pipeline to fill before # collecting data edges and their corresponding DCO sampling edges. @@ -109,6 +110,7 @@ async def ad9681_binary_skew_and_jitter_test(dut): assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + await cancel_and_join_tasks((clock_task,)) def test_Ad9681SimTiming(): diff --git a/tests/devices/analog_devices/test_AdcDdrCore.py b/tests/devices/analog_devices/test_AdcDdrCore.py index 02aaa30bdb..0d2cf3b80e 100644 --- a/tests/devices/analog_devices/test_AdcDdrCore.py +++ b/tests/devices/analog_devices/test_AdcDdrCore.py @@ -62,6 +62,7 @@ async def axil_poll(axil, address, predicate, limit=128): async def wait_load(clock, signal, mask, limit=80): + """Propagation sampling: observe registered delay-load pulses after TPD.""" for _ in range(limit): await RisingEdge(clock) await Timer(2, unit="ns") @@ -72,6 +73,7 @@ async def wait_load(clock, signal, mask, limit=80): @cocotb.test() async def core_integration_test(dut): + """Propagation sampling: check registered DUT outputs after their TPD updates.""" dut.axilRst.value = 1 dut.captureRst.value = 1 dut.streamRst.value = 1 @@ -319,6 +321,7 @@ async def core_integration_test(dut): if not pattern_check: response = await axil.read(AXIL_BASE_ADDR + 0x800, 4) assert response.resp == AxiResp.DECERR + # Terminal scenario: absent pattern hardware and DECERR complete this case. return # Run one shared-phase alternating-pattern window through the integrated diff --git a/tests/devices/analog_devices/test_AdcDdrPatternTester.py b/tests/devices/analog_devices/test_AdcDdrPatternTester.py index 6d78b4c986..406d759a2e 100644 --- a/tests/devices/analog_devices/test_AdcDdrPatternTester.py +++ b/tests/devices/analog_devices/test_AdcDdrPatternTester.py @@ -62,6 +62,7 @@ async def axil_poll(axil, address, predicate, limit=128): async def send_sample(dut, channel0, channel1): + """Propagation sampling: hold each sample through the registered TPD update.""" await FallingEdge(dut.clk) dut.sampleIn.value = (channel1 << 16) | channel0 dut.sampleValid.value = 1 From cebf2269e6883cc86f7cd2778d71a7fe59330e00 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Thu, 27 Aug 2026 14:30:33 -0700 Subject: [PATCH 55/57] test(gtx7): satisfy the regression compliance gate The mode gate at the top of each cocotb entrypoint returned early in the other mode's elaboration, so each test reported a vacuous pass in half of the sweep. Select the build's own entrypoint from the pytest wrapper with cocotb_filtered_env() instead, matching the RSSI suites, so every case runs its named checks. Harness.step() waited a bare 1 ns after the rising edge. That delay is the aligner's "after TPD_G" output propagation, which the elaboration leaves at its 1 ns default, so use the shared sample_after_tpd() helper that states that contract. --- .../gtx7/test_Gtx7RxFixedLatPhaseAligner.py | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py index fb73db1b45..d3345361c5 100644 --- a/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py +++ b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py @@ -11,6 +11,8 @@ # Test methodology: # - Sweep: Two elaborations, one per RX_ODD_ALIGN_MODE_G value. The generic # gates a constant and a generate, so it cannot be varied inside one build. +# The pytest wrapper selects each build's own entrypoint by name, so neither +# mode's checks can report a vacuous pass inside the other mode's build. # - Stimulus: A bit-accurate serial stream of tagged 20-bit frames feeds a GT # model that presents word k as stream[20k + b - d] for a landing offset d and # decrements d on every rxSlide pulse. Every one of the 20 possible comma @@ -43,12 +45,15 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import RisingEdge from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter, env_int, parameter_case, run_surf_vhdl_test, + sample_after_tpd, ) WORD_SIZE = 20 @@ -148,10 +153,13 @@ async def release_reset(self): self.dut.rxRunPhAlignment.value = 1 async def step(self): - """Advance one cycle, presenting the GT word and consuming rxSlide.""" + """Advance one cycle, presenting the GT word and consuming rxSlide. + + Sampling waits past the aligner's ``after TPD_G`` output delay, which + the elaboration leaves at its 1 ns default. + """ self.dut.rxData.value = gt_word(self.cycle, self.offset) - await RisingEdge(self.dut.rxUsrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rxUsrClk) self.cycle += 1 if self.dut.rxReset.value == 1: self.saw_reset = True @@ -185,9 +193,6 @@ async def run_landing(dut, landing): @cocotb.test() async def bitslip_landing_is_latency_invariant(dut): """Every landing must align with an even slide count and no RX reset.""" - if ODD_ALIGN_MODE != "BITSLIP": - return - tb = await run_landing(dut, LANDING) assert tb.aligned(), f"landing {LANDING}: never reached alignment" @@ -238,9 +243,6 @@ async def bitslip_landing_is_latency_invariant(dut): @cocotb.test() async def reset_mode_rejects_odd_landings(dut): """RESET mode's legacy contract, pinned against regression.""" - if ODD_ALIGN_MODE != "RESET": - return - tb = await run_landing(dut, LANDING) if LANDING % 2 == 1: @@ -279,6 +281,13 @@ async def reset_mode_rejects_odd_landings(dut): ) +# Each elaboration only carries one mode's contract, so only that mode's +# entrypoint is allowed to run in it. +MODE_ENTRYPOINT = { + "BITSLIP": "bitslip_landing_is_latency_invariant", + "RESET": "reset_mode_rejects_odd_landings", +} + PARAMETER_SWEEP = [ parameter_case(f"{mode.lower()}_landing{landing:02d}", RX_ODD_ALIGN_MODE_G=mode, @@ -290,9 +299,13 @@ async def reset_mode_rejects_odd_landings(dut): @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_Gtx7RxFixedLatPhaseAligner(parameters): + mode = parameters["RX_ODD_ALIGN_MODE_G"] run_surf_vhdl_test( test_file=__file__, toplevel="surf.gtx7rxfixedlatphasealigner", - parameters={"RX_ODD_ALIGN_MODE_G": parameters["RX_ODD_ALIGN_MODE_G"]}, - extra_env=parameters, + parameters={"RX_ODD_ALIGN_MODE_G": mode}, + extra_env=cocotb_filtered_env( + parameters, + cocotb_test_filter(MODE_ENTRYPOINT[mode]), + ), ) From 43b34d68b1eed247e523e8e4d91b92c41850a9d4 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 27 Aug 2026 14:35:10 -0700 Subject: [PATCH 56/57] Set Debug variable to False in calibration and update related tests --- devices/AnalogDevices/adcDdr/README.md | 2 +- python/surf/devices/analog_devices/_AdcDdrCalibration.py | 2 +- tests/devices/analog_devices/test_AdcDdrCalibration.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index 7c7c66067a..20a889fe7e 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -188,7 +188,7 @@ verification operation ends. | `PatternTesterSamples` | `4096` | Valid samples checked by each deep hardware window. | | `VerifyPn23` | Device dependent | Enable PN23 coherence and recurrence qualification when the adapter provides a PN-long reset control. | | `SettleTime` | `1 ms` | Wall-clock wait after delay, relock, or ADC test-mode changes. | -| `Debug` | `True` | Retain detailed per-tap diagnostics and publish one completed tree when the operation ends. | +| `Debug` | `False` | Retain detailed per-tap diagnostics and publish one completed tree when the operation ends. | The full delay range is the safest initial scan. Narrow `DelayStart` and `DelayStop` only when the board has a characterized region and the entire diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index e93fbaa0f1..1e0dea41d8 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -582,7 +582,7 @@ def __init__( self.add(pr.LocalVariable( name = 'Debug', description = 'Retain and publish detailed diagnostics when the operation ends', - value = True, + value = False, mode = 'RW')) self.add(pr.LocalVariable( diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 5d117b947e..2ada218ca1 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -605,7 +605,7 @@ def calibration_fixture(): def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): calibration, config, readout = calibration_fixture - assert calibration.Debug.value() is True + assert calibration.Debug.value() is False assert calibration.UsePatternTester.value() is False assert calibration.Outcome.value() == calibration.OUTCOME_IDLE_C results = calibration._runCalibration(dev=calibration) @@ -671,6 +671,7 @@ def record(): def test_diagnostics_publish_only_at_process_boundaries( calibration_fixture, monkeypatch): calibration, _, _ = calibration_fixture + calibration.Debug.set(True) publications = [] originalSet = calibration.Diagnostics.set @@ -699,6 +700,7 @@ def test_process_gui_message_keeps_framework_status(calibration_fixture): def test_full_calibration_can_add_deep_pattern_qualification(calibration_fixture): calibration, _, readout = calibration_fixture + calibration.Debug.set(True) calibration.UsePatternTester.set(True) calibration.PatternTesterSamples.set(32) From 3a41a2f451478ba7271fde512e1618f4f8f34b1b Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Thu, 27 Aug 2026 14:57:18 -0700 Subject: [PATCH 57/57] Add Margin feature to ADC calibration with reporting functionality --- devices/AnalogDevices/adcDdr/README.md | 7 ++ .../analog_devices/_AdcDdrCalibration.py | 91 +++++++++++++++++++ .../analog_devices/test_AdcDdrCalibration.py | 24 +++++ 3 files changed, 122 insertions(+) diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md index 20a889fe7e..04961c27b2 100644 --- a/devices/AnalogDevices/adcDdr/README.md +++ b/devices/AnalogDevices/adcDdr/README.md @@ -189,6 +189,7 @@ verification operation ends. | `VerifyPn23` | Device dependent | Enable PN23 coherence and recurrence qualification when the adapter provides a PN-long reset control. | | `SettleTime` | `1 ms` | Wall-clock wait after delay, relock, or ADC test-mode changes. | | `Debug` | `False` | Retain detailed per-tap diagnostics and publish one completed tree when the operation ends. | +| `Margin` | `Unavailable` | Single-line worst selected-eye margin from the most recent successful full calibration. | The full delay range is the safest initial scan. Narrow `DelayStart` and `DelayStop` only when the board has a characterized region and the entire @@ -215,6 +216,12 @@ transformation tried, the selected valid transformation, and the nested deep pattern-tester result. A failed scan may publish partial results for the lane that could not produce a qualifying eye. +After a successful full calibration, `Margin` reports the smallest left or +right margin across every selected FCO and data eye. `MarginReport()` prints a +per-lane table containing the eye range, selected tap, left and right margins, +worst-side margin, and bounded, scan-limit, or wrapped status. Both use native +`tap` units. + `Diagnostics` contains the measurement backend, expected patterns, raw and masked data captures, FCO words and lock masks, and the currently active scan point. The private working tree is not repeatedly copied into the public diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py index 1e0dea41d8..ea4661e5b0 100644 --- a/python/surf/devices/analog_devices/_AdcDdrCalibration.py +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -460,6 +460,7 @@ def __init__( self._runDiagnostics = {} self._lastCaptureDiagnostics = {} self._deepPatternTesterActive = False + self._marginResults = {} kwargs.setdefault( 'description', @@ -597,6 +598,17 @@ def __init__( value = {}, mode = 'RO')) + self.add(pr.LocalVariable( + name = 'Margin', + description = 'Worst margin from the most recent successful full calibration', + value = 'Unavailable', + mode = 'RO')) + + self.add(pr.LocalCommand( + name = 'MarginReport', + description = 'Print selected FCO and data-eye margins from the last full calibration', + function = self.marginReport)) + self.add(pr.LocalCommand( name = 'ApplyResults', description = 'Reapply selected taps from the last full calibration', @@ -1422,6 +1434,9 @@ def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: raise ValueError(f'Unsupported calibration operation {operation}') self.Results.set(results) retainResults = operation == self.FULL_C + if retainResults: + self._marginResults = copy.deepcopy(results) + self.Margin.set(self._marginHeadline(results)) return results finally: # Test mode is always temporary. Delay settings remain installed @@ -1434,6 +1449,82 @@ def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: self._readout._setDataDelays(dict(enumerate(originalData))) self._readout.Relock() + @staticmethod + def _marginRows(results: Mapping[str, Any]) -> list[dict[str, Any]]: + """Extract selected-eye margins from one complete calibration result.""" + + if not results or not results.get('Final', {}).get('passed', False): + raise RuntimeError('No successful full-calibration margin result is available') + + rows = [] + for kind, label in (('Fco', 'FCO'), ('Data', 'DATA')): + for lane, result in sorted( + results.get(kind, {}).items(), key=lambda item: int(item[0])): + eye = result.get('eye') + if eye is None: + raise RuntimeError( + 'No successful full-calibration margin result is available') + rows.append({ + 'type': label, + 'lane': int(lane), + 'start': int(eye['start']), + 'end': int(eye['end']), + 'selected': int(eye['selected']), + 'leftMargin': int(eye['leftMargin']), + 'rightMargin': int(eye['rightMargin']), + 'wraps': bool(eye['wraps']), + 'leftBounded': bool(eye['leftBounded']), + 'rightBounded': bool(eye['rightBounded']), + }) + if not rows: + raise RuntimeError('No successful full-calibration margin result is available') + return rows + + @classmethod + def _marginHeadline(cls, results: Mapping[str, Any]) -> str: + """Return one compact minimum-margin summary in native tap units.""" + + rows = cls._marginRows(results) + margin = min( + min(row['leftMargin'], row['rightMargin']) + for row in rows) + qualifiers = [] + if any(not row['leftBounded'] or not row['rightBounded'] for row in rows): + qualifiers.append('scan-limited eye(s)') + if any(row['wraps'] for row in rows): + qualifiers.append('wrapped eye(s)') + suffix = f"; {', '.join(qualifiers)}" if qualifiers else '' + unit = 'tap' if margin == 1 else 'taps' + return f'{margin} {unit} minimum{suffix}' + + def marginReport(self) -> None: + """Print a compact selected-eye margin table in native tap units.""" + + rows = self._marginRows(self._marginResults) + lines = [ + f'{self.path} ADC alignment margins (native tap units)', + 'Type Lane Eye Selected Left Right Worst Bounds', + ] + for row in rows: + bounds = [] + if row['wraps']: + bounds.append('wrapped') + if not row['leftBounded']: + bounds.append('left scan limit') + if not row['rightBounded']: + bounds.append('right scan limit') + if not bounds: + bounds.append('bounded') + eye = f"{row['start']}..{row['end']}" + lines.append( + f"{row['type']:<5} {row['lane']:>4} {eye:<10} " + f"{row['selected']:>8} {row['leftMargin']:>4} " + f"{row['rightMargin']:>5} " + f"{min(row['leftMargin'], row['rightMargin']):>5} " + f"{', '.join(bounds)}") + lines.append(f'Headline: {self._marginHeadline(self._marginResults)}') + print('\n'.join(lines)) + def applyResults(self) -> None: """Reinstall the selected taps from the last complete calibration.""" diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py index 2ada218ca1..b690b15392 100644 --- a/tests/devices/analog_devices/test_AdcDdrCalibration.py +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -606,6 +606,7 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): calibration, config, readout = calibration_fixture assert calibration.Debug.value() is False + assert calibration.Margin.value() == 'Unavailable' assert calibration.UsePatternTester.value() is False assert calibration.Outcome.value() == calibration.OUTCOME_IDLE_C results = calibration._runCalibration(dev=calibration) @@ -628,6 +629,7 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert readout.snapshotDelays == [(tap, tap) for tap in range(8)] + [(2, 5)] assert calibration.RunTime.value() > 0.0 assert calibration.Outcome.value() == calibration.OUTCOME_PASSED_C + assert calibration.Margin.value() == '1 tap minimum; scan-limited eye(s)' readout.FcoDelay[0].set(0) readout.DataDelay[0].set(0) @@ -638,6 +640,28 @@ def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): assert readout.DataDelay[1].value() == 5 +def test_margin_report_is_compact_and_interpretable(calibration_fixture, capsys): + calibration, _, _ = calibration_fixture + calibration._runCalibration(dev=calibration) + + calibration.MarginReport() + + report = capsys.readouterr().out + assert 'ADC alignment margins (native tap units)' in report + assert 'Type Lane Eye Selected Left Right Worst Bounds' in report + assert 'FCO 0 2..5 3 1 2 1 bounded' in report + assert 'DATA 0 1..4 2 1 2 1 bounded' in report + assert 'DATA 1 3..7 5 2 2 2 right scan limit' in report + assert 'Headline: 1 tap minimum; scan-limited eye(s)' in report + + +def test_margin_report_requires_successful_full_calibration(calibration_fixture): + calibration, _, _ = calibration_fixture + + with pytest.raises(RuntimeError, match='No successful full-calibration margin result'): + calibration.MarginReport() + + def test_pattern_alignment_reset_runs_after_checkerboard_selection(): events = [] calibration, config, readout, root = make_calibration()