From 5841623dd39323bcc3e049ca48910b9bcdca5e34 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Sun, 14 Jun 2026 15:47:36 -0700 Subject: [PATCH 1/2] Add cocotb-facing VHDL wrappers for AXI Stream packetizer family Thin flattened-port wrappers under protocols/packetizer/wrappers/ so cocotb can drive and observe the existing packetizer RTL through standard AXI Stream ports without embedding stimulus in VHDL: - AxiStreamBytePackerWrapper - AxiStreamPacketizerWrapper / AxiStreamDepacketizerWrapper - AxiStreamPacketizer2Wrapper / AxiStreamDepacketizer2Wrapper - AxiStreamPacketizer2LoopbackWrapper (end-to-end packetize/depacketize topology) --- .../wrappers/AxiStreamBytePackerWrapper.vhd | 126 ++++++++++++++ .../AxiStreamDepacketizer2Wrapper.vhd | 138 +++++++++++++++ .../wrappers/AxiStreamDepacketizerWrapper.vhd | 115 ++++++++++++ .../AxiStreamPacketizer2LoopbackWrapper.vhd | 163 ++++++++++++++++++ .../wrappers/AxiStreamPacketizer2Wrapper.vhd | 135 +++++++++++++++ .../wrappers/AxiStreamPacketizerWrapper.vhd | 121 +++++++++++++ 6 files changed, 798 insertions(+) create mode 100644 protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd create mode 100644 protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd create mode 100644 protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd create mode 100644 protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd create mode 100644 protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd create mode 100644 protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd diff --git a/protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd new file mode 100644 index 0000000000..c1c724122d --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd @@ -0,0 +1,126 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamBytePacker +------------------------------------------------------------------------------- +-- 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.AxiStreamPkg.all; + +entity AxiStreamBytePackerWrapper is + generic ( + TPD_G : time := 1 ns; + RST_ASYNC_G : boolean := false; + SLAVE_BYTES_G : positive range 1 to 8 := 4; + MASTER_BYTES_G : positive range 1 to 8 := 8); + port ( + axisClk : in sl; + axisRst : in sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(8*SLAVE_BYTES_G-1 downto 0); + S_AXIS_TKEEP : in slv(SLAVE_BYTES_G-1 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(8*SLAVE_BYTES_G-1 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(8*MASTER_BYTES_G-1 downto 0); + M_AXIS_TKEEP : out slv(MASTER_BYTES_G-1 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(8*MASTER_BYTES_G-1 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamBytePackerWrapper; + +architecture rtl of AxiStreamBytePackerWrapper is + + constant SLAVE_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => SLAVE_BYTES_G, + TDEST_BITS_C => 0, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_COMP_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_FIRST_LAST_C); + + constant MASTER_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => MASTER_BYTES_G, + TDEST_BITS_C => 0, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_COMP_C, + TUSER_BITS_C => 8, + TUSER_MODE_C => TUSER_FIRST_LAST_C); + + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + +begin + + assert (MASTER_BYTES_G >= SLAVE_BYTES_G) + report "AxiStreamBytePackerWrapper does not support downsizing" severity failure; + + --------------- + -- Bus shims -- + --------------- + comb : process (S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, S_AXIS_TKEEP, + S_AXIS_TLAST, S_AXIS_TUSER, + S_AXIS_TVALID, mAxisMaster) is + variable vS : AxiStreamMasterType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData := (others => '0'); + vS.tData(8*SLAVE_BYTES_G-1 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(SLAVE_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(SLAVE_BYTES_G-1 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(8*SLAVE_BYTES_G-1 downto 0) := S_AXIS_TUSER; + + sAxisMaster <= vS; + + S_AXIS_TREADY <= '1'; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(8*MASTER_BYTES_G-1 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(MASTER_BYTES_G-1 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(8*MASTER_BYTES_G-1 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamBytePacker + generic map ( + TPD_G => TPD_G, + RST_ASYNC_G => RST_ASYNC_G, + SLAVE_CONFIG_G => SLAVE_CONFIG_C, + MASTER_CONFIG_G => MASTER_CONFIG_C) + port map ( + axiClk => axisClk, + axiRst => axisRst, + sAxisMaster => sAxisMaster, + mAxisMaster => mAxisMaster); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd b/protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd new file mode 100644 index 0000000000..29c80b1668 --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd @@ -0,0 +1,138 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamDepacketizer2 +------------------------------------------------------------------------------- +-- 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.AxiStreamPkg.all; +use surf.AxiStreamPacketizer2Pkg.all; + +entity AxiStreamDepacketizer2Wrapper is + generic ( + TPD_G : time := 1 ns; + RST_POLARITY_G : sl := '1'; + RST_ASYNC_G : boolean := false; + MEMORY_TYPE_G : string := "distributed"; + REG_EN_G : boolean := false; + CRC_PIPELINE_G : natural range 0 to 1 := 0; + CRC_MODE_G : string := "NONE"; + SEQ_CNT_SIZE_G : natural range 0 to 16 := 16; + TDEST_BITS_G : natural := 8; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1); + port ( + axisClk : in sl; + axisRst : in sl; + linkGood : in sl; + debugOut : out slv(12 downto 0); + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(15 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(63 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamDepacketizer2Wrapper; + +architecture rtl of AxiStreamDepacketizer2Wrapper is + + signal debug : Packetizer2DebugType := PACKETIZER2_DEBUG_INIT_C; + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + debugOut <= debug.initDone & debug.sof & debug.eof & debug.eofe & debug.sop & + debug.eop & debug.packetError & debug.sofError & debug.seqError & + debug.versionError & debug.crcModeError & debug.eofeError & + debug.crcError; + + --------------- + -- Bus shims -- + --------------- + comb : process (M_AXIS_TREADY, S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, + S_AXIS_TKEEP, S_AXIS_TLAST, S_AXIS_TUSER, S_AXIS_TVALID, + mAxisMaster, sAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(15 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAxisMaster <= vS; + mAxisSlave <= vM; + + S_AXIS_TREADY <= sAxisSlave.tReady; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(63 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamDepacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_PIPELINE_G => CRC_PIPELINE_G, + CRC_MODE_G => CRC_MODE_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + linkGood => linkGood, + debug => debug, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd new file mode 100644 index 0000000000..87b5adbf4f --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd @@ -0,0 +1,115 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamDepacketizer +------------------------------------------------------------------------------- +-- 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.AxiStreamPkg.all; + +entity AxiStreamDepacketizerWrapper is + generic ( + TPD_G : time := 1 ns; + RST_ASYNC_G : boolean := false; + INPUT_PIPE_STAGES_G : integer := 0; + OUTPUT_PIPE_STAGES_G : integer := 0); + port ( + axisClk : in sl; + axisRst : in sl; + restart : in sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(15 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(63 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamDepacketizerWrapper; + +architecture rtl of AxiStreamDepacketizerWrapper is + + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + --------------- + -- Bus shims -- + --------------- + comb : process (M_AXIS_TREADY, S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, + S_AXIS_TKEEP, S_AXIS_TLAST, S_AXIS_TUSER, S_AXIS_TVALID, + mAxisMaster, sAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(15 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAxisMaster <= vS; + mAxisSlave <= vM; + + S_AXIS_TREADY <= sAxisSlave.tReady; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(63 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamDepacketizer + generic map ( + TPD_G => TPD_G, + RST_ASYNC_G => RST_ASYNC_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + restart => restart, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd new file mode 100644 index 0000000000..5b30b3389c --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd @@ -0,0 +1,163 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing AxiStreamPacketizer2/AxiStreamDepacketizer2 loopback +------------------------------------------------------------------------------- +-- 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.AxiStreamPkg.all; +use surf.AxiStreamPacketizer2Pkg.all; + +entity AxiStreamPacketizer2LoopbackWrapper is + generic ( + TPD_G : time := 1 ns; + RST_POLARITY_G : sl := '1'; + RST_ASYNC_G : boolean := false; + MEMORY_TYPE_G : string := "distributed"; + REG_EN_G : boolean := false; + CRC_PIPELINE_G : natural range 0 to 1 := 0; + CRC_MODE_G : string := "NONE"; + MAX_PACKET_BYTES_G : positive := 64; + SEQ_CNT_SIZE_G : positive range 4 to 16 := 16; + TDEST_BITS_G : natural := 8; + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 1); + port ( + axisClk : in sl; + axisRst : in sl; + linkGood : in sl; + maxPktBytes : in slv(bitSize(MAX_PACKET_BYTES_G)-1 downto 0) := toSlv(MAX_PACKET_BYTES_G, bitSize(MAX_PACKET_BYTES_G)); + rearbitrate : out sl; + debugOut : out slv(12 downto 0); + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(63 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(63 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamPacketizer2LoopbackWrapper; + +architecture rtl of AxiStreamPacketizer2LoopbackWrapper is + + signal debug : Packetizer2DebugType := PACKETIZER2_DEBUG_INIT_C; + signal sAppAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAppAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal pktAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal pktAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAppAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAppAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + debugOut <= debug.initDone & debug.sof & debug.eof & debug.eofe & debug.sop & + debug.eop & debug.packetError & debug.sofError & debug.seqError & + debug.versionError & debug.crcModeError & debug.eofeError & + debug.crcError; + + --------------- + -- Bus shims -- + --------------- + comb : process (M_AXIS_TREADY, S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, + S_AXIS_TKEEP, S_AXIS_TLAST, S_AXIS_TUSER, S_AXIS_TVALID, + mAppAxisMaster, sAppAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(63 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAppAxisMaster <= vS; + mAppAxisSlave <= vM; + + S_AXIS_TREADY <= sAppAxisSlave.tReady; + M_AXIS_TVALID <= mAppAxisMaster.tValid; + M_AXIS_TDATA <= mAppAxisMaster.tData(63 downto 0); + M_AXIS_TKEEP <= mAppAxisMaster.tKeep(7 downto 0); + M_AXIS_TLAST <= mAppAxisMaster.tLast; + M_AXIS_TDEST <= mAppAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAppAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAppAxisMaster.tUser(63 downto 0); + end process comb; + + U_Packetizer : entity surf.AxiStreamPacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_MODE_G => CRC_MODE_G, + MAX_PACKET_BYTES_G => MAX_PACKET_BYTES_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => 0) + port map ( + axisClk => axisClk, + axisRst => axisRst, + rearbitrate => rearbitrate, + maxPktBytes => maxPktBytes, + sAxisMaster => sAppAxisMaster, + sAxisSlave => sAppAxisSlave, + mAxisMaster => pktAxisMaster, + mAxisSlave => pktAxisSlave); + + U_Depacketizer : entity surf.AxiStreamDepacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_PIPELINE_G => CRC_PIPELINE_G, + CRC_MODE_G => CRC_MODE_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + INPUT_PIPE_STAGES_G => 0, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + linkGood => linkGood, + debug => debug, + sAxisMaster => pktAxisMaster, + sAxisSlave => pktAxisSlave, + mAxisMaster => mAppAxisMaster, + mAxisSlave => mAppAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd b/protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd new file mode 100644 index 0000000000..411219ece3 --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd @@ -0,0 +1,135 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamPacketizer2 +------------------------------------------------------------------------------- +-- 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.AxiStreamPkg.all; + +entity AxiStreamPacketizer2Wrapper is + generic ( + TPD_G : time := 1 ns; + RST_POLARITY_G : sl := '1'; + RST_ASYNC_G : boolean := false; + MEMORY_TYPE_G : string := "distributed"; + REG_EN_G : boolean := false; + CRC_MODE_G : string := "NONE"; + MAX_PACKET_BYTES_G : positive := 64; + SEQ_CNT_SIZE_G : positive range 4 to 16 := 16; + TDEST_BITS_G : natural := 8; + OUTPUT_TDEST_G : slv(7 downto 0) := (others => '0'); + OUTPUT_TID_G : slv(7 downto 0) := (others => '0'); + INPUT_PIPE_STAGES_G : natural := 0; + OUTPUT_PIPE_STAGES_G : natural := 0); + port ( + axisClk : in sl; + axisRst : in sl; + maxPktBytes : in slv(bitSize(MAX_PACKET_BYTES_G)-1 downto 0) := toSlv(MAX_PACKET_BYTES_G, bitSize(MAX_PACKET_BYTES_G)); + rearbitrate : out sl; + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(63 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(15 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamPacketizer2Wrapper; + +architecture rtl of AxiStreamPacketizer2Wrapper is + + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + --------------- + -- Bus shims -- + --------------- + comb : process (M_AXIS_TREADY, S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, + S_AXIS_TKEEP, S_AXIS_TLAST, S_AXIS_TUSER, S_AXIS_TVALID, + mAxisMaster, sAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(63 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAxisMaster <= vS; + mAxisSlave <= vM; + + S_AXIS_TREADY <= sAxisSlave.tReady; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(15 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamPacketizer2 + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => RST_ASYNC_G, + MEMORY_TYPE_G => MEMORY_TYPE_G, + REG_EN_G => REG_EN_G, + CRC_MODE_G => CRC_MODE_G, + MAX_PACKET_BYTES_G => MAX_PACKET_BYTES_G, + SEQ_CNT_SIZE_G => SEQ_CNT_SIZE_G, + TDEST_BITS_G => TDEST_BITS_G, + OUTPUT_TDEST_G => OUTPUT_TDEST_G, + OUTPUT_TID_G => OUTPUT_TID_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + rearbitrate => rearbitrate, + maxPktBytes => maxPktBytes, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; diff --git a/protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd b/protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd new file mode 100644 index 0000000000..4a75737e42 --- /dev/null +++ b/protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd @@ -0,0 +1,121 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Cocotb-facing wrapper for surf.AxiStreamPacketizer +------------------------------------------------------------------------------- +-- 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.AxiStreamPkg.all; + +entity AxiStreamPacketizerWrapper is + generic ( + TPD_G : time := 1 ns; + RST_ASYNC_G : boolean := false; + MAX_PACKET_BYTES_G : integer := 64; + MIN_TKEEP_G : slv(7 downto 0) := x"01"; + OUTPUT_SSI_G : boolean := true; + INPUT_PIPE_STAGES_G : integer := 0; + OUTPUT_PIPE_STAGES_G : integer := 0); + port ( + axisClk : in sl; + axisRst : in sl; + maxPktBytes : in slv(bitSize(MAX_PACKET_BYTES_G)-1 downto 0) := toSlv(MAX_PACKET_BYTES_G, bitSize(MAX_PACKET_BYTES_G)); + S_AXIS_TVALID : in sl; + S_AXIS_TDATA : in slv(63 downto 0); + S_AXIS_TKEEP : in slv(7 downto 0); + S_AXIS_TLAST : in sl; + S_AXIS_TDEST : in slv(7 downto 0); + S_AXIS_TID : in slv(7 downto 0); + S_AXIS_TUSER : in slv(63 downto 0); + S_AXIS_TREADY : out sl; + M_AXIS_TVALID : out sl; + M_AXIS_TDATA : out slv(63 downto 0); + M_AXIS_TKEEP : out slv(7 downto 0); + M_AXIS_TLAST : out sl; + M_AXIS_TDEST : out slv(7 downto 0); + M_AXIS_TID : out slv(7 downto 0); + M_AXIS_TUSER : out slv(15 downto 0); + M_AXIS_TREADY : in sl); +end entity AxiStreamPacketizerWrapper; + +architecture rtl of AxiStreamPacketizerWrapper is + + signal sAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal sAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + signal mAxisMaster : AxiStreamMasterType := AXI_STREAM_MASTER_INIT_C; + signal mAxisSlave : AxiStreamSlaveType := AXI_STREAM_SLAVE_INIT_C; + +begin + + --------------- + -- Bus shims -- + --------------- + comb : process (M_AXIS_TREADY, S_AXIS_TDATA, S_AXIS_TDEST, S_AXIS_TID, + S_AXIS_TKEEP, S_AXIS_TLAST, S_AXIS_TUSER, S_AXIS_TVALID, + mAxisMaster, sAxisSlave) is + variable vS : AxiStreamMasterType; + variable vM : AxiStreamSlaveType; + begin + vS := AXI_STREAM_MASTER_INIT_C; + vS.tValid := S_AXIS_TVALID; + vS.tData(63 downto 0) := S_AXIS_TDATA; + vS.tStrb := (others => '0'); + vS.tStrb(7 downto 0) := S_AXIS_TKEEP; + vS.tKeep := (others => '0'); + vS.tKeep(7 downto 0) := S_AXIS_TKEEP; + vS.tLast := S_AXIS_TLAST; + vS.tDest(7 downto 0) := S_AXIS_TDEST; + vS.tId(7 downto 0) := S_AXIS_TID; + vS.tUser := (others => '0'); + vS.tUser(63 downto 0) := S_AXIS_TUSER; + + vM := AXI_STREAM_SLAVE_INIT_C; + vM.tReady := M_AXIS_TREADY; + + sAxisMaster <= vS; + mAxisSlave <= vM; + + S_AXIS_TREADY <= sAxisSlave.tReady; + M_AXIS_TVALID <= mAxisMaster.tValid; + M_AXIS_TDATA <= mAxisMaster.tData(63 downto 0); + M_AXIS_TKEEP <= mAxisMaster.tKeep(7 downto 0); + M_AXIS_TLAST <= mAxisMaster.tLast; + M_AXIS_TDEST <= mAxisMaster.tDest(7 downto 0); + M_AXIS_TID <= mAxisMaster.tId(7 downto 0); + M_AXIS_TUSER <= mAxisMaster.tUser(15 downto 0); + end process comb; + + --------------------- + -- DUT instancing -- + --------------------- + U_DUT : entity surf.AxiStreamPacketizer + generic map ( + TPD_G => TPD_G, + RST_ASYNC_G => RST_ASYNC_G, + MAX_PACKET_BYTES_G => MAX_PACKET_BYTES_G, + MIN_TKEEP_G => MIN_TKEEP_G, + OUTPUT_SSI_G => OUTPUT_SSI_G, + INPUT_PIPE_STAGES_G => INPUT_PIPE_STAGES_G, + OUTPUT_PIPE_STAGES_G => OUTPUT_PIPE_STAGES_G) + port map ( + axisClk => axisClk, + axisRst => axisRst, + maxPktBytes => maxPktBytes, + sAxisMaster => sAxisMaster, + sAxisSlave => sAxisSlave, + mAxisMaster => mAxisMaster, + mAxisSlave => mAxisSlave); + +end architecture rtl; From 965fc6747fad1050a71e09aa4fe78d8d63c23d01 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Sun, 14 Jun 2026 15:47:37 -0700 Subject: [PATCH 2/2] Add packetizer cocotb regression coverage Python cocotb tests and shared helpers under tests/protocols/packetizer/ covering byte-width packing, packetizer/depacketizer payload round trips, packetizer2/depacketizer2 framing, CRC and EOFE/error handling, link-drop behavior, sequence-number wrap, and packetizer2 loopback recovery. --- tests/protocols/packetizer/__init__.py | 9 + .../packetizer/packetizer_test_utils.py | 469 ++++++++++++++++++ .../packetizer/test_AxiStreamBytePacker.py | 277 +++++++++++ .../packetizer/test_AxiStreamDepacketizer.py | 233 +++++++++ .../packetizer/test_AxiStreamDepacketizer2.py | 285 +++++++++++ .../test_AxiStreamDepacketizer2Crc.py | 112 +++++ .../test_AxiStreamDepacketizer2LinkDrop.py | 125 +++++ .../packetizer/test_AxiStreamPacketizer.py | 266 ++++++++++ .../packetizer/test_AxiStreamPacketizer2.py | 404 +++++++++++++++ .../test_AxiStreamPacketizer2Loopback.py | 124 +++++ .../test_AxiStreamPacketizer2SeqWrap.py | 121 +++++ 11 files changed, 2425 insertions(+) create mode 100644 tests/protocols/packetizer/__init__.py create mode 100644 tests/protocols/packetizer/packetizer_test_utils.py create mode 100644 tests/protocols/packetizer/test_AxiStreamBytePacker.py create mode 100644 tests/protocols/packetizer/test_AxiStreamDepacketizer.py create mode 100644 tests/protocols/packetizer/test_AxiStreamDepacketizer2.py create mode 100644 tests/protocols/packetizer/test_AxiStreamDepacketizer2Crc.py create mode 100644 tests/protocols/packetizer/test_AxiStreamDepacketizer2LinkDrop.py create mode 100644 tests/protocols/packetizer/test_AxiStreamPacketizer.py create mode 100644 tests/protocols/packetizer/test_AxiStreamPacketizer2.py create mode 100644 tests/protocols/packetizer/test_AxiStreamPacketizer2Loopback.py create mode 100644 tests/protocols/packetizer/test_AxiStreamPacketizer2SeqWrap.py diff --git a/tests/protocols/packetizer/__init__.py b/tests/protocols/packetizer/__init__.py new file mode 100644 index 0000000000..b0085f1a17 --- /dev/null +++ b/tests/protocols/packetizer/__init__.py @@ -0,0 +1,9 @@ +############################################################################## +## 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. +############################################################################## diff --git a/tests/protocols/packetizer/packetizer_test_utils.py b/tests/protocols/packetizer/packetizer_test_utils.py new file mode 100644 index 0000000000..53c824125b --- /dev/null +++ b/tests/protocols/packetizer/packetizer_test_utils.py @@ -0,0 +1,469 @@ +############################################################################## +## 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 os +from dataclasses import dataclass + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.axi.utils import wait_sampled_ready + +PACKETIZER2_VERSION = 0x2 +PACKETIZER2_CRC_NONE = 0x0 +PACKETIZER2_CRC_DATA = 0x1 +PACKETIZER2_CRC_FULL = 0x2 +PACKETIZER0_VERSION = 0x0 +SSI_EOFE = 0 +SSI_SOF = 1 +DEBUG_INIT_DONE = 12 + +CRC_MODE_VALUES = { + "NONE": PACKETIZER2_CRC_NONE, + "DATA": PACKETIZER2_CRC_DATA, + "FULL": PACKETIZER2_CRC_FULL, +} + + +@dataclass +class AxisBeat: + data: int + keep: int = 0xFF + last: int = 0 + dest: int = 0 + tid: int = 0 + user: int = 0 + + +class FlatAxisEndpoint: + def __init__(self, dut, *, prefix: str): + self.dut = dut + self.prefix = prefix + + def _sig(self, suffix: str): + return getattr(self.dut, f"{self.prefix}_{suffix}") + + def set_idle(self) -> None: + for suffix, value in ( + ("TVALID", 0), + ("TDATA", 0), + ("TKEEP", 0), + ("TLAST", 0), + ("TDEST", 0), + ("TID", 0), + ("TUSER", 0), + ): + if hasattr(self.dut, f"{self.prefix}_{suffix}"): + self._sig(suffix).value = value + + def drive(self, beat: AxisBeat) -> None: + self._sig("TVALID").value = 1 + self._sig("TDATA").value = beat.data + self._sig("TKEEP").value = beat.keep + self._sig("TLAST").value = beat.last + self._sig("TDEST").value = beat.dest + self._sig("TID").value = beat.tid + self._sig("TUSER").value = beat.user + + def snapshot(self) -> AxisBeat: + return AxisBeat( + data=int(self._sig("TDATA").value), + keep=int(self._sig("TKEEP").value), + last=int(self._sig("TLAST").value), + dest=int(self._sig("TDEST").value), + tid=int(self._sig("TID").value), + user=int(self._sig("TUSER").value), + ) + + async def send(self, beat: AxisBeat, *, clk) -> None: + self.drive(beat) + await wait_sampled_ready(self._sig("TREADY"), clk=clk) + self.set_idle() + + 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() + for _ in range(timeout_cycles): + await FallingEdge(clk) + await Timer(1, unit="ns") + if int(self._sig("TVALID").value) == 1: + return self.snapshot() + await RisingEdge(clk) + await Timer(1, unit="ns") + if int(self._sig("TVALID").value) == 1: + return self.snapshot() + raise AssertionError(f"Timed out waiting for {self.prefix} valid") + + 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") + if not keep_ready: + self._sig("TREADY").value = 0 + return beat + + +def start_packetizer_clock(dut, *, period_ns: float = 5.0) -> None: + cocotb.start_soon(Clock(dut.axisClk, period_ns, unit="ns").start()) + + +async def cycle(clk, count: int = 1) -> None: + for _ in range(count): + await RisingEdge(clk) + await Timer(1, unit="ns") + + +async def reset_packetizer_dut(dut, *, cycles: int = 4) -> None: + dut.axisRst.setimmediatevalue(1) + await cycle(dut.axisClk, cycles) + dut.axisRst.value = 0 + await cycle(dut.axisClk, 2) + + +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") + raise AssertionError("Timed out waiting for depacketizer initDone") + + +def crc_mode_from_env(default: str = "NONE") -> int: + return CRC_MODE_VALUES[os.getenv("CRC_MODE_G", default)] + + +def word_from_bytes(data: bytes) -> int: + return int.from_bytes(data.ljust(8, b"\x00"), "little") + + +def bytes_from_word(word: int, *, keep: int = 0xFF) -> bytes: + raw = word.to_bytes(8, "little") + return bytes(raw[index] for index in range(8) if keep & (1 << index)) + + +def payload_to_beats( + payload: bytes, + *, + dest: int, + tid: int, + first_user: int, + last_user: int, +) -> list[AxisBeat]: + beats = [] + for offset in range(0, len(payload), 8): + chunk = payload[offset : offset + 8] + is_first = offset == 0 + is_last = offset + 8 >= len(payload) + keep = (1 << len(chunk)) - 1 + user = 0 + if is_first: + user |= first_user + if is_last: + user |= last_user << (8 * (len(chunk) - 1)) + beats.append( + AxisBeat( + data=word_from_bytes(chunk), + keep=keep, + last=int(is_last), + dest=dest, + tid=tid, + user=user, + ) + ) + return beats + + +def user_from_bytes(values: list[int]) -> int: + user = 0 + for lane, value in enumerate(values): + user |= (value & 0xFF) << (8 * lane) + return user + + +def tuser_for_lane(lane: int, value: int) -> int: + return (value & 0xFF) << (8 * lane) + + +def packetizer2_header_word(*, crc_mode: int, sof: int, tuser: int, tdest: int, tid: int, seq: int) -> int: + return ( + (PACKETIZER2_VERSION & 0xF) + | ((crc_mode & 0xF) << 4) + | ((tuser & 0xFF) << 8) + | ((tdest & 0xFF) << 16) + | ((tid & 0xFF) << 24) + | ((seq & 0xFFFF) << 32) + | ((sof & 0x1) << 63) + ) + + +def packetizer2_tail_word(*, eof: int, tuser: int, byte_count: int, crc: int = 0) -> int: + return ( + (tuser & 0xFF) + | ((eof & 0x1) << 8) + | ((byte_count & 0xF) << 16) + | ((crc & 0xFFFFFFFF) << 32) + ) + + +def packetizer0_header_word(*, frame: int, packet: int, tdest: int, tid: int, tuser: int) -> int: + return ( + (PACKETIZER0_VERSION & 0xF) + | ((frame & 0xFFF) << 4) + | ((packet & 0xFFFFFF) << 16) + | ((tdest & 0xFF) << 40) + | ((tid & 0xFF) << 48) + | ((tuser & 0xFF) << 56) + ) + + +def packetizer0_tail_byte(*, eof: int, tuser: int) -> int: + return ((eof & 0x1) << 7) | (tuser & 0x7F) + + +def packetizer2_header_beat( + *, + sof: int, + tuser: int, + dest: int, + tid: int, + seq: int, + crc_mode: int = PACKETIZER2_CRC_NONE, +) -> AxisBeat: + return AxisBeat( + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=sof, + tuser=tuser, + tdest=dest, + tid=tid, + seq=seq, + ), + keep=0xFF, + last=0, + user=0x2, + ) + + +def packetizer2_data_beat(payload: bytes) -> AxisBeat: + return AxisBeat(data=word_from_bytes(payload), keep=0xFF, last=0, user=0) + + +def packetizer2_tail_beat(*, eof: int, tuser: int, byte_count: int, crc: int = 0) -> AxisBeat: + return AxisBeat( + data=packetizer2_tail_word(eof=eof, tuser=tuser, byte_count=byte_count, crc=crc), + keep=0xFF, + last=1, + user=0, + ) + + +def packetizer0_header_beat(*, frame: int, packet: int, tuser: int, dest: int, tid: int) -> AxisBeat: + return AxisBeat( + data=packetizer0_header_word( + frame=frame, + packet=packet, + tdest=dest, + tid=tid, + tuser=tuser, + ), + keep=0xFF, + last=0, + user=0x2, + ) + + +def packetizer_data_beat(payload: bytes, *, keep: int = 0xFF, last: int = 0) -> AxisBeat: + return AxisBeat(data=word_from_bytes(payload), keep=keep, last=last, user=0) + + +def assert_packetized_beat( + beat: AxisBeat, + *, + data: int, + keep: int = 0xFF, + last: int = 0, + user: int = 0, + dest: int = 0, + tid: int = 0, +) -> None: + assert beat.data == data + assert beat.keep == keep + assert beat.last == last + assert beat.dest == dest + assert beat.tid == tid + assert beat.user == user + + +def assert_packetizer2_tail_beat( + beat: AxisBeat, + *, + eof: int, + tuser: int, + byte_count: int, + crc_mode: int = PACKETIZER2_CRC_NONE, +) -> None: + expected = packetizer2_tail_word(eof=eof, tuser=tuser, byte_count=byte_count) + assert (beat.data & 0xFFFFFFFF) == (expected & 0xFFFFFFFF) + if crc_mode == PACKETIZER2_CRC_NONE: + assert (beat.data >> 32) == 0 + else: + assert (beat.data >> 32) != 0 + assert beat.keep == 0xFF + assert beat.last == 1 + assert beat.dest == 0 + assert beat.tid == 0 + assert beat.user == 0 + + +def assert_app_beat( + beat: AxisBeat, + *, + payload: bytes, + keep: int = 0xFF, + last: int = 0, + dest: int, + tid: int, + user: int = 0, +) -> None: + assert bytes_from_word(beat.data, keep=keep) == payload + assert beat.keep == keep + assert beat.last == last + assert beat.dest == dest + assert beat.tid == tid + assert beat.user == user + + +async def assert_no_output( + endpoint: FlatAxisEndpoint, + *, + clk, + cycles: int, + drive_ready: bool = False, +) -> None: + if drive_ready: + endpoint._sig("TREADY").value = 1 + for _ in range(cycles): + await RisingEdge(clk) + await Timer(1, unit="ns") + assert int(endpoint._sig("TVALID").value) == 0 + if drive_ready: + endpoint._sig("TREADY").value = 0 + + +def byte_packer_source_beat(payload: bytes, *, last: int = 0, user_values: list[int]) -> AxisBeat: + return AxisBeat( + data=word_from_bytes(payload), + keep=(1 << len(payload)) - 1, + last=last, + user=user_from_bytes(user_values), + ) + + +def byte_packer_source_beats_from_payload( + payload: bytes, + *, + max_beat_bytes: int, + user_base: int, + first_size: int | None = None, +) -> list[AxisBeat]: + beats = [] + offset = 0 + while offset < len(payload): + if offset == 0 and first_size is not None: + size = min(first_size, max_beat_bytes, len(payload)) + else: + size = min(max_beat_bytes, len(payload) - offset) + chunk = payload[offset : offset + size] + beats.append( + byte_packer_source_beat( + chunk, + last=int(offset + size == len(payload)), + user_values=list(range(user_base + offset, user_base + offset + len(chunk))), + ) + ) + offset += size + return beats + + +async def send_unpaced_beats(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], *, clk) -> None: + # `AxiStreamBytePacker` has no slave-ready output. Each driven beat is held + # for exactly one rising edge, which is the module's intended acceptance + # cadence. + for beat in beats: + endpoint.drive(beat) + await RisingEdge(clk) + await Timer(1, unit="ns") + 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") + if int(endpoint._sig("TVALID").value): + beats.append(endpoint.snapshot()) + return beats + + +def assert_packed_beat( + beat: AxisBeat, + *, + payload: bytes, + user_values: list[int], + last: int = 0, +) -> None: + keep = (1 << len(payload)) - 1 + assert bytes_from_word(beat.data, keep=keep) == payload + assert beat.keep == keep + assert beat.last == last + assert beat.user == user_from_bytes(user_values) + + +async def send_beats(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], *, clk) -> None: + for beat in beats: + await endpoint.send(beat, clk=clk) + + +async def recv_beats(endpoint: FlatAxisEndpoint, count: int, *, clk) -> list[AxisBeat]: + beats = [] + for _ in range(count): + beats.append(await endpoint.recv(clk=clk, keep_ready=True)) + endpoint._sig("TREADY").value = 0 + return beats + + +async def recv_beats_with_backpressure( + endpoint: FlatAxisEndpoint, + count: int, + *, + clk, + hold_cycles: int = 2, +) -> list[AxisBeat]: + beats = [] + endpoint._sig("TREADY").value = 0 + for _ in range(count): + beat = await endpoint.wait_valid(clk=clk) + for _ in range(hold_cycles): + await RisingEdge(clk) + await Timer(1, unit="ns") + assert endpoint.snapshot() == beat + endpoint._sig("TREADY").value = 1 + await RisingEdge(clk) + await Timer(1, unit="ns") + endpoint._sig("TREADY").value = 0 + beats.append(beat) + return beats diff --git a/tests/protocols/packetizer/test_AxiStreamBytePacker.py b/tests/protocols/packetizer/test_AxiStreamBytePacker.py new file mode 100644 index 0000000000..b06465041a --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamBytePacker.py @@ -0,0 +1,277 @@ +############################################################################## +## 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: Use a standalone `AxiStreamBytePacker` wrapper with several +# compressed-keep input/output byte-width pairs. +# - Stimulus: Drive one input beat per clock because this RTL intentionally has +# no ready handshake, using partial keeps, exact-width terminal beats, and +# reset while a partial packed word is buffered. +# - Checks: The output valid pulses must contain compacted payload bytes in +# arrival order, matching per-byte `TUSER`, compressed `TKEEP`, and `TLAST` +# only on frame-terminating words. +# - Timing: Output is sampled on clocked valid pulses rather than through a +# sink-ready handshake, matching the module's no-backpressure contract. + +import os + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_no_output, + assert_packed_beat, + byte_packer_source_beat, + byte_packer_source_beats_from_payload, + recv_valid_pulses, + reset_packetizer_dut, + send_unpaced_beats, + start_packetizer_clock, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + self.slave_bytes = int(os.getenv("SLAVE_BYTES_G", "4")) + self.master_bytes = int(os.getenv("MASTER_BYTES_G", "8")) + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(1) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def pack_partial_beats_test(dut): + tb = TB(dut) + await tb.reset() + + # The final input beat crosses an output-word boundary: one output word + # becomes full and the remaining bytes start the next terminal output word. + payload = bytes(range(0x10, 0x10 + tb.master_bytes + 3)) + input_beats = byte_packer_source_beats_from_payload( + payload, + max_beat_bytes=tb.slave_bytes, + user_base=0xA0, + first_size=max(1, min(tb.slave_bytes - 1, 3)), + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 2, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload[: tb.master_bytes], + user_values=list(range(0xA0, 0xA0 + tb.master_bytes)), + ) + assert_packed_beat( + rx_beats[1], + payload=payload[tb.master_bytes :], + user_values=list(range(0xA0 + tb.master_bytes, 0xA0 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def pack_exact_width_last_test(dut): + tb = TB(dut) + await tb.reset() + + # A frame exactly as wide as the output should produce one full terminal + # word regardless of how many narrower input beats it takes to fill it. + payload = bytes(range(0x30, 0x30 + tb.master_bytes)) + input_beats = byte_packer_source_beats_from_payload( + payload, + max_beat_bytes=tb.slave_bytes, + user_base=0x10, + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x10, 0x10 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def reset_flushes_partial_word_test(dut): + tb = TB(dut) + await tb.reset() + + # Start a partial packed word and prove it does not leak out before reset. + partial = bytes(range(0x40, 0x40 + min(3, tb.slave_bytes, tb.master_bytes - 1))) + await send_unpaced_beats( + tb.source, + [byte_packer_source_beat(partial, user_values=list(range(0x20, 0x20 + len(partial))))], + clk=dut.axisClk, + ) + await assert_no_output(tb.sink, clk=dut.axisClk, cycles=3) + + # Reset should discard the buffered partial word. The only subsequent output + # should be the new short frame driven after reset releases. + await tb.reset() + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + new_frame = bytes(range(0x50, 0x50 + min(2, tb.slave_bytes))) + await send_unpaced_beats( + tb.source, + [ + byte_packer_source_beat( + new_frame, + last=1, + user_values=list(range(0x30, 0x30 + len(new_frame))), + ) + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=new_frame, + user_values=list(range(0x30, 0x30 + len(new_frame))), + last=1, + ) + + +@cocotb.test() +async def idle_gap_preserves_partial_word_test(dut): + tb = TB(dut) + await tb.reset() + + # The packer should keep a partial word across idle cycles and continue + # filling it when traffic resumes. + payload = bytes(range(0x60, 0x60 + tb.master_bytes)) + first_len = max(1, min(3, tb.slave_bytes, tb.master_bytes - 1)) + first = byte_packer_source_beat( + payload[:first_len], + user_values=list(range(0x40, 0x40 + first_len)), + ) + remaining = byte_packer_source_beats_from_payload( + payload[first_len:], + max_beat_bytes=tb.slave_bytes, + user_base=0x40 + first_len, + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, [first], clk=dut.axisClk) + await assert_no_output(tb.sink, clk=dut.axisClk, cycles=4) + await send_unpaced_beats(tb.source, remaining, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x40, 0x40 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def zero_keep_beat_is_ignored_test(dut): + tb = TB(dut) + await tb.reset() + + # A valid beat with no asserted keep bits should not contribute data or + # sideband bytes to the packed output word. The following short terminal + # frame should emerge exactly as if the zero-keep beat had been idle. + payload = bytes(range(0x70, 0x70 + min(3, tb.slave_bytes))) + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats( + tb.source, + [ + AxisBeat( + data=(1 << (8 * tb.slave_bytes)) - 1, + keep=0x00, + last=0, + user=(1 << (8 * tb.slave_bytes)) - 1, + ), + byte_packer_source_beat( + payload, + last=1, + user_values=list(range(0x60, 0x60 + len(payload))), + ), + ], + clk=dut.axisClk, + ) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x60, 0x60 + len(payload))), + last=1, + ) + + +@cocotb.test() +async def output_ready_is_ignored_test(dut): + tb = TB(dut) + await tb.reset() + + # The RTL explicitly has no ready handshaking. Holding output ready low must + # not suppress the valid pulse for a complete packed output word. + dut.M_AXIS_TREADY.value = 0 + payload = bytes(range(0x80, 0x80 + tb.master_bytes)) + input_beats = byte_packer_source_beats_from_payload( + payload, + max_beat_bytes=tb.slave_bytes, + user_base=0x50, + ) + + rx_task = cocotb.start_soon(recv_valid_pulses(tb.sink, 1, clk=dut.axisClk)) + await send_unpaced_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + assert_packed_beat( + rx_beats[0], + payload=payload, + user_values=list(range(0x50, 0x50 + len(payload))), + last=1, + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"SLAVE_BYTES_G": 1, "MASTER_BYTES_G": 8}, id="comp_keep_1_to_8"), + pytest.param({"SLAVE_BYTES_G": 2, "MASTER_BYTES_G": 5}, id="comp_keep_2_to_5"), + pytest.param({"SLAVE_BYTES_G": 3, "MASTER_BYTES_G": 6}, id="comp_keep_3_to_6"), + pytest.param({"SLAVE_BYTES_G": 3, "MASTER_BYTES_G": 7}, id="comp_keep_3_to_7"), + pytest.param({"SLAVE_BYTES_G": 4, "MASTER_BYTES_G": 8}, id="comp_keep_4_to_8"), + pytest.param({"SLAVE_BYTES_G": 5, "MASTER_BYTES_G": 7}, id="comp_keep_5_to_7"), + pytest.param({"SLAVE_BYTES_G": 7, "MASTER_BYTES_G": 8}, id="comp_keep_7_to_8"), + ], +) +def test_AxiStreamBytePacker(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreambytepackerwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamBytePackerWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer.py new file mode 100644 index 0000000000..88b75e82fa --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer.py @@ -0,0 +1,233 @@ +############################################################################## +## 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: Use the standalone legacy `AxiStreamDepacketizer` wrapper with an +# 8-byte packetized input stream. +# - Stimulus: Present hand-built V0 header and payload beats directly, +# including both tail encodings produced by the legacy packetizer. +# - Checks: The depacketized application stream must restore payload bytes, +# `TDEST`, `TID`, SOF on first-beat `TUSER`, final-byte `TUSER`, `TKEEP`, +# and `TLAST` for appended-tail and separate-tail packets. +# - Timing: The application sink is kept ready while source and sink tasks run +# concurrently, with no packetizer loopback used as the stimulus generator. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_app_beat, + assert_no_output, + packetizer0_header_beat, + packetizer0_tail_byte, + packetizer_data_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.restart.setimmediatevalue(0) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def depacketize_appended_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # Build the packetizer-V0 stream directly: header, one full payload beat, + # and a final beat whose last byte is the EOF/user tail marker. + first = bytes(range(0x10, 0x18)) + last = bytes(range(0x18, 0x1F)) + tail = packetizer0_tail_byte(eof=1, tuser=0x41) + packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x20, dest=0x3, tid=0xA5), + packetizer_data_beat(first), + packetizer_data_beat(last + bytes([tail]), last=1), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + # The depacketizer restores header sideband onto the first application beat + # and sets SSI SOF in the first byte's `TUSER`. + assert_app_beat(rx_beats[0], payload=first, dest=0x3, tid=0xA5, user=0x22) + # For an appended tail, the final byte lane is stripped via `TKEEP`, and the + # tail user bits move onto the last real payload byte. + assert_app_beat( + rx_beats[1], + payload=last, + keep=0x7F, + last=1, + dest=0x3, + tid=0xA5, + user=0x41 << 48, + ) + + +@cocotb.test() +async def depacketize_separate_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # This packet uses the other legal tail placement: a full final payload word + # followed by a separate one-byte EOF/user marker. + first = bytes(range(0x30, 0x38)) + last = bytes(range(0x38, 0x40)) + tail = packetizer0_tail_byte(eof=1, tuser=0x42) + packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x10, dest=0x2, tid=0x5A), + packetizer_data_beat(first), + packetizer_data_beat(last), + AxisBeat(data=tail, keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + # The first output beat proves sideband restoration, while the second proves + # that the one-byte marker was consumed without becoming payload data. + assert_app_beat(rx_beats[0], payload=first, dest=0x2, tid=0x5A, user=0x12) + assert_app_beat( + rx_beats[1], + payload=last, + last=1, + dest=0x2, + tid=0x5A, + user=0x42 << 56, + ) + + +@cocotb.test() +async def depacketize_split_sequence_test(dut): + tb = TB(dut) + await tb.reset() + + # Hand-build a valid two-packet V0 frame. The first packet terminates with + # EOF=0, so the depacketizer must retain frame state and accept packet 1 as + # a continuation without adding another SOF. + chunks = [ + bytes(range(0x60, 0x68)), + bytes(range(0x68, 0x70)), + bytes(range(0x70, 0x78)), + ] + packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x30, dest=0x4, tid=0x22), + packetizer_data_beat(chunks[0]), + packetizer_data_beat(chunks[1]), + AxisBeat(data=packetizer0_tail_byte(eof=0, tuser=0), keep=0x01, last=1, user=0), + packetizer0_header_beat(frame=0, packet=1, tuser=0x00, dest=0x4, tid=0x22), + packetizer_data_beat(chunks[2]), + AxisBeat(data=packetizer0_tail_byte(eof=1, tuser=0x43), keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat(rx_beats[0], payload=chunks[0], dest=0x4, tid=0x22, user=0x32) + assert_app_beat(rx_beats[1], payload=chunks[1], dest=0x4, tid=0x22) + assert_app_beat( + rx_beats[2], + payload=chunks[2], + last=1, + dest=0x4, + tid=0x22, + user=0x43 << 56, + ) + + +@cocotb.test() +async def depacketize_bad_continuation_bleeds_and_recovers_test(dut): + tb = TB(dut) + await tb.reset() + + # Start a multi-packet frame, then skip packet number 1. The depacketizer + # should bleed the malformed packet instead of merging out-of-order payload + # into the frame, then return to HEADER state for the next valid frame. + chunks = [ + bytes(range(0x90, 0x98)), + bytes(range(0x98, 0xA0)), + bytes(range(0xA0, 0xA8)), + ] + first_packet = [ + packetizer0_header_beat(frame=0, packet=0, tuser=0x34, dest=0x6, tid=0x44), + packetizer_data_beat(chunks[0]), + packetizer_data_beat(chunks[1]), + AxisBeat(data=packetizer0_tail_byte(eof=0, tuser=0), keep=0x01, last=1, user=0), + ] + bad_packet = [ + packetizer0_header_beat(frame=0, packet=2, tuser=0x00, dest=0x6, tid=0x44), + packetizer_data_beat(chunks[2]), + AxisBeat(data=packetizer0_tail_byte(eof=1, tuser=0x47), keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, first_packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat(rx_beats[0], payload=chunks[0], dest=0x6, tid=0x44, user=0x36) + assert_app_beat(rx_beats[1], payload=chunks[1], dest=0x6, tid=0x44) + + await send_beats(tb.source, bad_packet, clk=dut.axisClk) + await assert_no_output(tb.sink, clk=dut.axisClk, cycles=8, drive_ready=True) + + recovery = bytes(range(0xB0, 0xB8)) + recovery_packet = [ + packetizer0_header_beat(frame=1, packet=0, tuser=0x35, dest=0x7, tid=0x45), + packetizer_data_beat(recovery), + AxisBeat(data=packetizer0_tail_byte(eof=1, tuser=0x48), keep=0x01, last=1, user=0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, recovery_packet, clk=dut.axisClk) + recovery_beat = (await with_timeout(rx_task, 4, "us"))[0] + + assert_app_beat( + recovery_beat, + payload=recovery, + last=1, + dest=0x7, + tid=0x45, + user=0x37 | (0x48 << 56), + ) + + +@pytest.mark.parametrize("parameters", [pytest.param({}, id="legacy_v0")]) +def test_AxiStreamDepacketizer(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizerwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizerWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer2.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer2.py new file mode 100644 index 0000000000..f8d8ab5ccb --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer2.py @@ -0,0 +1,285 @@ +############################################################################## +## 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: Use the standalone `AxiStreamDepacketizer2` wrapper with CRC +# disabled and a small `TDEST_BITS_G=2` address space so startup RAM +# initialization stays short under GHDL. +# - Stimulus: Present packetizer-V2 header, payload, and tail beats directly, +# including a two-packet continuation sequence with incrementing packet +# sequence numbers. +# - Checks: The depacketized application stream must restore payload bytes, +# `TDEST`, `TID`, first-beat SOF, final-beat `TUSER`, `TKEEP`, and `TLAST` +# without relying on `AxiStreamPacketizer2` as the stimulus generator. +# - Timing: The test waits for the depacketizer `initDone` debug bit before +# traffic, then keeps the application sink ready while source and sink tasks +# run concurrently. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + PACKETIZER2_CRC_DATA, + PACKETIZER2_CRC_NONE, + DEBUG_INIT_DONE, + assert_app_beat, + cycle, + packetizer2_data_beat, + packetizer2_header_beat, + packetizer2_header_word, + packetizer2_tail_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +def assert_error_beat(beat: AxisBeat, *, dest: int, tid: int, header_user: int) -> None: + assert beat.last == 1 + assert beat.dest == dest + assert beat.tid == tid + assert (beat.user & 0xFF) == (header_user | 0x3) + assert (beat.user >> 56) & 0x1 == 0x1 + + +@cocotb.test() +async def depacketize_single_packet_test(dut): + tb = TB(dut) + await tb.reset() + + first = bytes(range(0x10, 0x18)) + last = bytes(range(0x18, 0x20)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x20, dest=0x3, tid=0xA5, seq=0), + packetizer2_data_beat(first), + packetizer2_data_beat(last), + packetizer2_tail_beat(eof=1, tuser=0x41, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat(rx_beats[0], payload=first, dest=0x3, tid=0xA5, user=0x22) + assert_app_beat(rx_beats[1], payload=last, last=1, dest=0x3, tid=0xA5, user=0x41 << 56) + + +@cocotb.test() +async def depacketize_split_sequence_test(dut): + tb = TB(dut) + await tb.reset() + + chunks = [ + bytes(range(0x30, 0x38)), + bytes(range(0x38, 0x40)), + bytes(range(0x40, 0x48)), + ] + packets = [ + packetizer2_header_beat(sof=1, tuser=0x10, dest=0x2, tid=0x5A, seq=0), + packetizer2_data_beat(chunks[0]), + packetizer2_data_beat(chunks[1]), + packetizer2_tail_beat(eof=0, tuser=0, byte_count=8), + packetizer2_header_beat(sof=0, tuser=0x00, dest=0x2, tid=0x5A, seq=1), + packetizer2_data_beat(chunks[2]), + packetizer2_tail_beat(eof=1, tuser=0x43, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, packets, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat(rx_beats[0], payload=chunks[0], dest=0x2, tid=0x5A, user=0x12) + assert_app_beat(rx_beats[1], payload=chunks[1], dest=0x2, tid=0x5A) + assert_app_beat(rx_beats[2], payload=chunks[2], last=1, dest=0x2, tid=0x5A, user=0x43 << 56) + + +@cocotb.test() +async def depacketize_partial_last_tkeep_test(dut): + tb = TB(dut) + await tb.reset() + + first = bytes(range(0x50, 0x58)) + last = bytes(range(0x58, 0x5B)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x24, dest=0x1, tid=0xC3, seq=0), + packetizer2_data_beat(first), + packetizer2_data_beat(last), + packetizer2_tail_beat(eof=1, tuser=0x47, byte_count=3), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat(rx_beats[0], payload=first, dest=0x1, tid=0xC3, user=0x26) + assert_app_beat( + rx_beats[1], + payload=last, + keep=0x07, + last=1, + dest=0x1, + tid=0xC3, + user=0x47 << 16, + ) + + +@cocotb.test() +async def depacketize_crc_none_nonzero_crc_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0x70, 0x78)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x30, dest=0x2, tid=0x55, seq=0), + packetizer2_data_beat(payload), + packetizer2_tail_beat(eof=1, tuser=0x40, byte_count=8, crc=0x1), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + # CRC mode NONE requires the CRC field to be zero. A nonzero field marks + # the frame as terminal-error while still forwarding the held payload beat. + assert_app_beat( + rx_beats[0], + payload=payload, + last=1, + dest=0x2, + tid=0x55, + user=0x32 | (0x41 << 56), + ) + + +@cocotb.test() +async def depacketize_bad_version_header_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + bad_header = packetizer2_header_word( + crc_mode=PACKETIZER2_CRC_NONE, + sof=1, + tuser=0x29, + tdest=0x1, + tid=0x66, + seq=0, + ) + packet = [AxisBeat(data=(bad_header & ~0xF) | 0x7, keep=0xFF, last=0, user=0x2)] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_error_beat(rx_beats[0], dest=0x1, tid=0x66, header_user=0x29) + + +@cocotb.test() +async def depacketize_bad_crc_mode_header_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + packet = [ + packetizer2_header_beat( + sof=1, + tuser=0x2B, + dest=0x2, + tid=0x77, + seq=0, + crc_mode=PACKETIZER2_CRC_DATA, + ) + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_error_beat(rx_beats[0], dest=0x2, tid=0x77, header_user=0x2B) + + +@cocotb.test() +async def depacketize_link_drop_recovers_test(dut): + tb = TB(dut) + await tb.reset() + + dut.linkGood.value = 0 + await cycle(dut.axisClk, 4) + assert (int(dut.debugOut.value) & (1 << DEBUG_INIT_DONE)) == 0 + + dut.linkGood.value = 1 + await tb.wait_init_done() + + payload = bytes(range(0x90, 0x98)) + packet = [ + packetizer2_header_beat(sof=1, tuser=0x34, dest=0x3, tid=0x88, seq=0), + packetizer2_data_beat(payload), + packetizer2_tail_beat(eof=1, tuser=0x48, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat( + rx_beats[0], + payload=payload, + last=1, + dest=0x3, + tid=0x88, + user=0x36 | (0x48 << 56), + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param( + { + "TDEST_BITS_G": 2, + }, + id="crc_none_tdest2", + ) + ], +) +def test_AxiStreamDepacketizer2(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer2Crc.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer2Crc.py new file mode 100644 index 0000000000..493fc9a028 --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer2Crc.py @@ -0,0 +1,112 @@ +############################################################################## +## 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: Use the standalone `AxiStreamDepacketizer2` wrapper with CRC +# checking enabled in DATA and FULL modes. +# - Stimulus: Drive one hand-built packet whose header advertises the active +# CRC mode but whose tail CRC field is deliberately wrong. +# - Checks: The depacketizer must forward the held payload beat, terminate the +# application frame, and mark the last byte-lane `TUSER` with SSI `EOFE`. +# - Timing: The test waits for depacketizer initialization, then uses ordinary +# AXI Stream source and sink handshakes around a short packet. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + FlatAxisEndpoint, + assert_app_beat, + crc_mode_from_env, + packetizer2_data_beat, + packetizer2_header_beat, + packetizer2_tail_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +@cocotb.test() +async def depacketize_bad_crc_marks_eofe_test(dut): + tb = TB(dut) + await tb.reset() + + # The CRC field is intentionally not a computed CRC. In DATA and FULL modes + # that must be treated as a terminal frame error on the held payload word. + payload = bytes(range(0x20, 0x28)) + packet = [ + packetizer2_header_beat( + crc_mode=crc_mode_from_env("DATA"), + sof=1, + tuser=0x30, + dest=0x2, + tid=0x5C, + seq=0, + ), + packetizer2_data_beat(payload), + packetizer2_tail_beat(eof=1, tuser=0x42, byte_count=8, crc=0x0), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, packet, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_app_beat( + rx_beats[0], + payload=payload, + last=1, + dest=0x2, + tid=0x5C, + user=0x32 | (0x43 << 56), + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "DATA"}, id="crc_data_bad_crc"), + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "FULL"}, id="crc_full_bad_crc"), + ], +) +def test_AxiStreamDepacketizer2Crc(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamDepacketizer2LinkDrop.py b/tests/protocols/packetizer/test_AxiStreamDepacketizer2LinkDrop.py new file mode 100644 index 0000000000..b656f79eb5 --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamDepacketizer2LinkDrop.py @@ -0,0 +1,125 @@ +############################################################################## +## 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: Use the standalone `AxiStreamDepacketizer2` wrapper in CRC NONE +# mode with a small two-bit destination state table. +# - Stimulus: Start a packet without sending its tail, then drop `linkGood` +# while the depacketizer has active frame state. +# - Checks: The depacketizer must terminate the incomplete frame with SSI +# `EOFE` during link cleanup and accept a fresh frame after link recovery. +# - Timing: The test runs alone in this file so the link-cleanup sweep observes +# only the intentionally opened destination state. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + FlatAxisEndpoint, + assert_app_beat, + bytes_from_word, + packetizer2_data_beat, + packetizer2_header_beat, + packetizer2_tail_beat, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +@cocotb.test() +async def depacketize_mid_frame_link_drop_terminates_and_recovers_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0xA0, 0xA8)) + open_packet = [ + packetizer2_header_beat(sof=1, tuser=0x36, dest=0x1, tid=0x91, seq=0), + packetizer2_data_beat(payload), + ] + + await send_beats(tb.source, open_packet, clk=dut.axisClk) + rx_task = cocotb.start_soon(recv_beats(tb.sink, 2, clk=dut.axisClk)) + dut.linkGood.value = 0 + rx_beats = await with_timeout(rx_task, 4, "us") + + assert bytes_from_word(rx_beats[0].data) == payload + assert rx_beats[0].last == 0 + assert rx_beats[0].dest == 0x1 + assert rx_beats[0].tid == 0x91 + assert rx_beats[0].user == 0x36 + assert rx_beats[1].last == 1 + assert bytes_from_word(rx_beats[1].data) == payload + assert ((rx_beats[1].user >> 56) & 0x1) == 0x1 + + dut.linkGood.value = 1 + await tb.wait_init_done() + + recovery = bytes(range(0xB0, 0xB8)) + recovery_packet = [ + packetizer2_header_beat(sof=1, tuser=0x38, dest=0x1, tid=0x92, seq=0), + packetizer2_data_beat(recovery), + packetizer2_tail_beat(eof=1, tuser=0x4A, byte_count=8), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 1, clk=dut.axisClk)) + await send_beats(tb.source, recovery_packet, clk=dut.axisClk) + recovery_beats = await with_timeout(rx_task, 3, "us") + + assert_app_beat( + recovery_beats[0], + payload=recovery, + last=1, + dest=0x1, + tid=0x92, + user=0x3A | (0x4A << 56), + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"TDEST_BITS_G": 2}, id="crc_none_tdest2"), + ], +) +def test_AxiStreamDepacketizer2LinkDrop(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreamdepacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamDepacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer.py b/tests/protocols/packetizer/test_AxiStreamPacketizer.py new file mode 100644 index 0000000000..0c2d05ab19 --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer.py @@ -0,0 +1,266 @@ +############################################################################## +## 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: Use the standalone legacy `AxiStreamPacketizer` wrapper with an +# 8-byte stream width and the default SSI packetized output mode. +# - Stimulus: Drive application AXI Stream beats directly into the packetizer, +# including `TDEST`, `TID`, first-beat `TUSER`, and final-byte `TUSER`. +# - Checks: The packetized output must contain the expected V0 header, payload, +# SSI SOF bit, and both supported EOF tail placements: appended into a +# partially-filled final word and emitted as a separate one-byte final word. +# - Timing: The sink is kept ready while a concurrent source task sends input +# beats, so the bench observes every accepted packetized beat without using +# a depacketizer loopback as an oracle. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_packetized_beat, + packetizer0_header_word, + packetizer0_tail_byte, + recv_beats, + recv_beats_with_backpressure, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + tuser_for_lane, + word_from_bytes, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(64) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + +@cocotb.test() +async def packetize_appended_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # A 15-byte frame leaves one spare byte in the final packet word, so the + # legacy packetizer should append its EOF/user tail marker into that word. + payload = bytes(range(0x10, 0x1F)) + tail = packetizer0_tail_byte(eof=1, tuser=0x41) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x3, + tid=0xA5, + user=0x20, + ), + AxisBeat( + data=word_from_bytes(payload[8:15]), + keep=0x7F, + last=1, + dest=0x3, + tid=0xA5, + user=tuser_for_lane(7, 0x41), + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + # The first packet word is protocol overhead: version/frame/packet plus the + # application sideband fields copied out of the first input beat. + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x3, tid=0xA5, tuser=0x20), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + # The final output beat carries seven payload bytes plus the tail marker in + # byte lane 7; no depacketizer is involved in forming this expectation. + assert_packetized_beat( + rx_beats[2], + data=word_from_bytes(payload[8:15] + bytes([tail])), + last=1, + ) + + +@cocotb.test() +async def packetize_separate_tail_test(dut): + tb = TB(dut) + await tb.reset() + + # A 16-byte frame fills the last payload word completely, so the legacy + # packetizer must emit the EOF/user tail marker as its own one-byte word. + payload = bytes(range(0x30, 0x40)) + tail = packetizer0_tail_byte(eof=1, tuser=0x42) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x2, + tid=0x5A, + user=0x10, + ), + AxisBeat( + data=word_from_bytes(payload[8:16]), + keep=0xFF, + last=1, + dest=0x2, + tid=0x5A, + user=0x42, + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + # The packetized sideband is only present in the header; following payload + # beats should have neutralized `TDEST`, `TID`, and `TUSER`. + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x2, tid=0x5A, tuser=0x10), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + # The separate tail word uses only byte lane 0 and terminates the packet. + assert_packetized_beat(rx_beats[3], data=tail, keep=0x01, last=1) + + +@cocotb.test() +async def packetize_split_frame_on_max_size_test(dut): + tb = TB(dut) + dut.maxPktBytes.value = 32 + await tb.reset() + + # With a 32-byte packet limit, the legacy packetizer can carry two 8-byte + # payload words plus header/tail overhead. A three-word frame must therefore + # become a non-EOF packet followed by an EOF continuation packet. + payload = bytes(range(0x60, 0x78)) + first_tail = packetizer0_tail_byte(eof=0, tuser=0x00) + final_tail = packetizer0_tail_byte(eof=1, tuser=0x43) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x4, + tid=0x22, + user=0x31, + ), + AxisBeat( + data=word_from_bytes(payload[8:16]), + keep=0xFF, + last=0, + dest=0x4, + tid=0x22, + user=0, + ), + AxisBeat( + data=word_from_bytes(payload[16:24]), + keep=0xFF, + last=1, + dest=0x4, + tid=0x22, + user=0x43, + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x4, tid=0x22, tuser=0x31), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetized_beat(rx_beats[3], data=first_tail, keep=0x01, last=1) + assert_packetized_beat( + rx_beats[4], + data=packetizer0_header_word(frame=0, packet=1, tdest=0x4, tid=0x22, tuser=0x43), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:24])) + assert_packetized_beat(rx_beats[6], data=final_tail, keep=0x01, last=1) + + +@cocotb.test() +async def packetize_output_backpressure_test(dut): + tb = TB(dut) + await tb.reset() + + # Stall every packetized output beat before accepting it. This stresses the + # legacy packetizer's output hold behavior across header, payload, and the + # separate one-byte tail marker. + payload = bytes(range(0x80, 0x90)) + tail = packetizer0_tail_byte(eof=1, tuser=0x45) + input_beats = [ + AxisBeat( + data=word_from_bytes(payload[0:8]), + keep=0xFF, + last=0, + dest=0x5, + tid=0x35, + user=0x25, + ), + AxisBeat( + data=word_from_bytes(payload[8:16]), + keep=0xFF, + last=1, + dest=0x5, + tid=0x35, + user=0x45, + ), + ] + + rx_task = cocotb.start_soon(recv_beats_with_backpressure(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + assert_packetized_beat( + rx_beats[0], + data=packetizer0_header_word(frame=0, packet=0, tdest=0x5, tid=0x35, tuser=0x25), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetized_beat(rx_beats[3], data=tail, keep=0x01, last=1) + + +@pytest.mark.parametrize("parameters", [pytest.param({}, id="legacy_v0")]) +def test_AxiStreamPacketizer(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizerwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizerWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer2.py b/tests/protocols/packetizer/test_AxiStreamPacketizer2.py new file mode 100644 index 0000000000..c3ed8da69e --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer2.py @@ -0,0 +1,404 @@ +############################################################################## +## 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: Use the standalone `AxiStreamPacketizer2` wrapper across CRC NONE, +# DATA, and FULL modes with an 8-byte stream width and a reduced packet-size +# limit for the split-frame case. +# - Stimulus: Drive application AXI Stream beats directly into the packetizer, +# including first/last `TUSER`, `TDEST`, `TID`, and a three-beat frame that +# must be divided into two packetizer packets. +# - Checks: The packetized output must contain the expected V2 header words, +# payload words, tail words, sequence numbers, SOF/EOF flags, and output +# sideband remapping. +# - Timing: The sink is kept ready while a concurrent source task sends input +# beats, so the bench observes every accepted packetized beat without using +# a depacketizer loopback as an oracle. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + assert_packetized_beat, + assert_packetizer2_tail_beat, + crc_mode_from_env, + packetizer2_header_word, + payload_to_beats, + recv_beats, + recv_beats_with_backpressure, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + word_from_bytes, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(32) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def packetize_single_frame_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0x10, 0x20)) + input_beats = payload_to_beats( + payload, + dest=0x3, + tid=0xA5, + first_user=0x22, + last_user=0x41, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x22, + tdest=0x3, + tid=0xA5, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=1, tuser=0x41, byte_count=8, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_split_frame_test(dut): + tb = TB(dut) + await tb.reset() + + payload = bytes(range(0x30, 0x48)) + input_beats = payload_to_beats( + payload, + dest=0x2, + tid=0x5A, + first_user=0x12, + last_user=0x43, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x12, + tdest=0x2, + tid=0x5A, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[4], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0, + tdest=0x2, + tid=0x5A, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:24])) + assert_packetizer2_tail_beat(rx_beats[6], eof=1, tuser=0x43, byte_count=8, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_partial_last_tkeep_test(dut): + tb = TB(dut) + await tb.reset() + + # A partial final input beat should still produce a full-width packetized + # tail word whose byte-count field tells the depacketizer how much of the + # previous payload word is real frame data. + payload = bytes(range(0x50, 0x5B)) + input_beats = payload_to_beats( + payload, + dest=0x1, + tid=0xC3, + first_user=0x24, + last_user=0x47, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 4, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 2, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x24, + tdest=0x1, + tid=0xC3, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:11])) + assert_packetizer2_tail_beat(rx_beats[3], eof=1, tuser=0x47, byte_count=3, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_one_byte_over_max_packet_boundary_test(dut): + tb = TB(dut) + await tb.reset() + + # With `maxPktBytes=32`, two 8-byte payload words plus header/tail exactly + # fill one packet. A 17-byte frame must therefore split after the first 16 + # payload bytes and carry the final byte in a continuation packet. + payload = bytes(range(0x58, 0x69)) + input_beats = payload_to_beats( + payload, + dest=0x2, + tid=0xB4, + first_user=0x26, + last_user=0x49, + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 3, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x26, + tdest=0x2, + tid=0xB4, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[4], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0x49, + tdest=0x2, + tid=0xB4, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:17])) + assert_packetizer2_tail_beat(rx_beats[6], eof=1, tuser=0x49, byte_count=1, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_interleaved_tdest_state_test(dut): + tb = TB(dut) + await tb.reset() + + # Changing TDEST mid-frame forces a non-EOF tail for the active destination + # without accepting the new beat. The source helper holds the new beat until + # the packetizer rearbitrates and returns ready. + dest_a_first = bytes(range(0x70, 0x78)) + dest_b_frame = bytes(range(0x90, 0x98)) + dest_a_last = bytes(range(0x78, 0x80)) + input_beats = [ + AxisBeat( + data=word_from_bytes(dest_a_first), + keep=0xFF, + last=0, + dest=0x1, + tid=0x11, + user=0x21, + ), + AxisBeat( + data=word_from_bytes(dest_b_frame), + keep=0xFF, + last=1, + dest=0x2, + tid=0x22, + user=0x31 | (0x44 << 56), + ), + AxisBeat( + data=word_from_bytes(dest_a_last), + keep=0xFF, + last=1, + dest=0x1, + tid=0x11, + user=0x45 << 56, + ), + ] + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 9, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 4, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x21, + tdest=0x1, + tid=0x11, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(dest_a_first)) + assert_packetizer2_tail_beat(rx_beats[2], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[3], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x31, + tdest=0x2, + tid=0x22, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[4], data=word_from_bytes(dest_b_frame)) + assert_packetizer2_tail_beat(rx_beats[5], eof=1, tuser=0x44, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[6], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0x00, + tdest=0x1, + tid=0x11, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[7], data=word_from_bytes(dest_a_last)) + assert_packetizer2_tail_beat(rx_beats[8], eof=1, tuser=0x45, byte_count=8, crc_mode=crc_mode) + + +@cocotb.test() +async def packetize_output_backpressure_test(dut): + tb = TB(dut) + await tb.reset() + + # Hold each packetized beat at the sink for multiple clocks before + # accepting it. The helper checks that VALID-side data, keep, last, and + # sideband remain stable while TREADY is low. + payload = bytes(range(0xA0, 0xB8)) + input_beats = payload_to_beats( + payload, + dest=0x3, + tid=0x33, + first_user=0x2A, + last_user=0x4A, + ) + + rx_task = cocotb.start_soon(recv_beats_with_backpressure(tb.sink, 7, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 5, "us") + + crc_mode = crc_mode_from_env() + + assert_packetized_beat( + rx_beats[0], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=1, + tuser=0x2A, + tdest=0x3, + tid=0x33, + seq=0, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[1], data=word_from_bytes(payload[0:8])) + assert_packetized_beat(rx_beats[2], data=word_from_bytes(payload[8:16])) + assert_packetizer2_tail_beat(rx_beats[3], eof=0, tuser=0, byte_count=8, crc_mode=crc_mode) + assert_packetized_beat( + rx_beats[4], + data=packetizer2_header_word( + crc_mode=crc_mode, + sof=0, + tuser=0, + tdest=0x3, + tid=0x33, + seq=1, + ), + user=0x2, + ) + assert_packetized_beat(rx_beats[5], data=word_from_bytes(payload[16:24])) + assert_packetizer2_tail_beat(rx_beats[6], eof=1, tuser=0x4A, byte_count=8, crc_mode=crc_mode) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({}, id="crc_none"), + pytest.param({"CRC_MODE_G": "DATA"}, id="crc_data"), + pytest.param({"CRC_MODE_G": "FULL"}, id="crc_full"), + ], +) +def test_AxiStreamPacketizer2(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer2Loopback.py b/tests/protocols/packetizer/test_AxiStreamPacketizer2Loopback.py new file mode 100644 index 0000000000..72e4f70d5c --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer2Loopback.py @@ -0,0 +1,124 @@ +############################################################################## +## 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: Use a cocotb-facing loopback wrapper that connects +# `AxiStreamPacketizer2` directly to `AxiStreamDepacketizer2`, sweeping the +# NONE, DATA, and FULL CRC modes. +# - Stimulus: Drive application AXI Stream frames with partial final `TKEEP`, +# nonzero first/last `TUSER`, `TDEST`, and `TID` through a small packet-size +# limit that forces the packetizer to emit continuation packets internally. +# - Checks: The loopback output must restore payload bytes, `TKEEP`, `TLAST`, +# `TDEST`, `TID`, and first/last `TUSER`, while the sink applies backpressure. +# - Timing: The test waits for depacketizer RAM initialization before traffic, +# then uses ordinary AXI Stream ready/valid handshakes on the application +# input and output only. + +import os + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + FlatAxisEndpoint, + assert_app_beat, + payload_to_beats, + recv_beats_with_backpressure, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + wait_debug_init_done, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.linkGood.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(32) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + await self.wait_init_done() + + async def wait_init_done(self, timeout_cycles: int = 64): + await wait_debug_init_done(self.dut, timeout_cycles=timeout_cycles) + + +@cocotb.test() +async def loopback_split_partial_frame_with_backpressure_test(dut): + tb = TB(dut) + await tb.reset() + + # This 19-byte frame forces an internal V2 packet split at the 32-byte + # packetized limit, then ends with a three-byte final output word. + payload = bytes(range(0x10, 0x23)) + tdest_bits = int(os.getenv("TDEST_BITS_G", "2")) + dest = 0x2 + if tdest_bits == 0: + expected_dest = 0 + dest = 0 + else: + expected_dest = (1 << min(tdest_bits, 2)) - 1 + dest = expected_dest + input_beats = payload_to_beats( + payload, + dest=dest, + tid=0x39, + first_user=0x2C, + last_user=0x4C, + ) + + rx_task = cocotb.start_soon(recv_beats_with_backpressure(tb.sink, 3, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 6, "us") + + assert_app_beat(rx_beats[0], payload=payload[0:8], dest=expected_dest, tid=0x39, user=0x2E) + assert_app_beat(rx_beats[1], payload=payload[8:16], dest=expected_dest, tid=0x39) + assert_app_beat( + rx_beats[2], + payload=payload[16:19], + keep=0x07, + last=1, + dest=expected_dest, + tid=0x39, + user=0x4C << 16, + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"TDEST_BITS_G": 2}, id="crc_none"), + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "DATA"}, id="crc_data"), + pytest.param({"TDEST_BITS_G": 2, "CRC_MODE_G": "FULL"}, id="crc_full"), + pytest.param({"TDEST_BITS_G": 0}, id="tdest0"), + pytest.param({"TDEST_BITS_G": 1}, id="tdest1"), + ], +) +def test_AxiStreamPacketizer2Loopback(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizer2loopbackwrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizer2LoopbackWrapper.vhd"], + }, + ) diff --git a/tests/protocols/packetizer/test_AxiStreamPacketizer2SeqWrap.py b/tests/protocols/packetizer/test_AxiStreamPacketizer2SeqWrap.py new file mode 100644 index 0000000000..b326377fcf --- /dev/null +++ b/tests/protocols/packetizer/test_AxiStreamPacketizer2SeqWrap.py @@ -0,0 +1,121 @@ +############################################################################## +## 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: Use the standalone `AxiStreamPacketizer2` wrapper with +# `SEQ_CNT_SIZE_G=4`, the smallest supported sequence counter width. +# - Stimulus: Drive one long frame that requires 17 internal packetizer +# packets, forcing the packet sequence field to wrap from 15 back to 0. +# - Checks: Every packet header must carry the expected wrapped sequence value, +# SOF must appear only on the first packet, and EOF must appear only on the +# final packet tail. +# - Timing: The sink is kept ready while the source frame is sent through the +# normal ready/valid handshake. + +import cocotb +import pytest +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import run_surf_vhdl_test +from tests.protocols.packetizer.packetizer_test_utils import ( + AxisBeat, + FlatAxisEndpoint, + PACKETIZER2_CRC_NONE, + assert_packetized_beat, + assert_packetizer2_tail_beat, + packetizer2_header_word, + recv_beats, + reset_packetizer_dut, + send_beats, + start_packetizer_clock, + word_from_bytes, +) + + +class TB: + def __init__(self, dut): + self.dut = dut + self.source = FlatAxisEndpoint(dut, prefix="S_AXIS") + self.sink = FlatAxisEndpoint(dut, prefix="M_AXIS") + + start_packetizer_clock(dut) + dut.axisRst.setimmediatevalue(1) + dut.maxPktBytes.setimmediatevalue(32) + dut.M_AXIS_TREADY.setimmediatevalue(0) + self.source.set_idle() + + async def reset(self): + await reset_packetizer_dut(self.dut) + + +@cocotb.test() +async def packetize_sequence_counter_wrap_test(dut): + tb = TB(dut) + await tb.reset() + + # Each packet carries two full payload words at this 32-byte packet limit. + # Seventeen packets therefore require thirty-four 8-byte input beats. + input_beats = [] + for index in range(34): + payload = bytes((0x20 + index + lane) & 0xFF for lane in range(8)) + input_beats.append( + AxisBeat( + data=word_from_bytes(payload), + keep=0xFF, + last=int(index == 33), + dest=0x1, + tid=0x55, + user=(0x24 if index == 0 else 0) | ((0x46 << 56) if index == 33 else 0), + ) + ) + + rx_task = cocotb.start_soon(recv_beats(tb.sink, 68, clk=dut.axisClk)) + await send_beats(tb.source, input_beats, clk=dut.axisClk) + rx_beats = await with_timeout(rx_task, 20, "us") + + for packet_index in range(17): + base = packet_index * 4 + seq = packet_index & 0xF + assert_packetized_beat( + rx_beats[base], + data=packetizer2_header_word( + crc_mode=PACKETIZER2_CRC_NONE, + sof=int(packet_index == 0), + tuser=0x24 if packet_index == 0 else 0x00, + tdest=0x1, + tid=0x55, + seq=seq, + ), + user=0x2, + ) + assert_packetizer2_tail_beat( + rx_beats[base + 3], + eof=int(packet_index == 16), + tuser=0x46 if packet_index == 16 else 0x00, + byte_count=8, + ) + + +@pytest.mark.parametrize( + "parameters", + [ + pytest.param({"SEQ_CNT_SIZE_G": 4}, id="seq4_wrap"), + ], +) +def test_AxiStreamPacketizer2SeqWrap(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.axistreampacketizer2wrapper", + parameters=parameters, + extra_env=parameters, + extra_vhdl_sources={ + "surf": ["protocols/packetizer/wrappers/AxiStreamPacketizer2Wrapper.vhd"], + }, + )