diff --git a/.github/synthesis/all.json b/.github/synthesis/all.json index 21c58fd432..761d1040c4 100644 --- a/.github/synthesis/all.json +++ b/.github/synthesis/all.json @@ -28,6 +28,7 @@ {"top": "vexRiscvTcpTest", "stage": "bitstream", "cc_report": false}, + {"top": "asyncCommsDemoTest", "stage": "test", "cc_report": true}, {"top": "boardTestExtended", "stage": "test", "cc_report": false}, {"top": "boardTestSimple", "stage": "test", "cc_report": false}, {"top": "ddr4Test", "stage": "test", "cc_report": false}, diff --git a/.github/synthesis/main.json b/.github/synthesis/main.json index 3d8b314de2..1d8206c987 100644 --- a/.github/synthesis/main.json +++ b/.github/synthesis/main.json @@ -1,6 +1,7 @@ [ {"top": "receiveRingBufferPnr", "stage": "pnr", "cc_report": false}, {"top": "safeDffSynchronizer", "stage": "hdl" , "cc_report": false}, + {"top": "asyncCommsDemoTest", "stage": "test", "cc_report": true}, {"top": "transmitRingBufferPnr", "stage": "pnr", "cc_report": false}, {"top": "vexRiscvTest", "stage": "test", "cc_report": false}, {"top": "wireDemoTest", "stage": "test", "cc_report": true} diff --git a/bittide-instances/bittide-instances.cabal b/bittide-instances/bittide-instances.cabal index 715e69ff8b..9afc87a6bf 100644 --- a/bittide-instances/bittide-instances.cabal +++ b/bittide-instances/bittide-instances.cabal @@ -164,6 +164,10 @@ library Bittide.Instances.Common Bittide.Instances.Domains Bittide.Instances.Hacks + Bittide.Instances.Hitl.AsyncCommsDemo.Driver + Bittide.Instances.Hitl.AsyncCommsDemo.MemoryMaps + Bittide.Instances.Hitl.AsyncCommsDemo.TopEntity + Bittide.Instances.Hitl.AsyncCommsDemo.UserCore Bittide.Instances.Hitl.BoardTest Bittide.Instances.Hitl.Ddr4 Bittide.Instances.Hitl.DnaOverSerial diff --git a/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/Driver.hs new file mode 100644 index 0000000000..c7625b1faa --- /dev/null +++ b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/Driver.hs @@ -0,0 +1,126 @@ +-- SPDX-FileCopyrightText: 2026 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 +{-# LANGUAGE PackageImports #-} + +module Bittide.Instances.Hitl.AsyncCommsDemo.Driver where + +import Clash.Prelude + +import Bittide.ClockControl.Config (defCcConf) +import Bittide.Hitl +import Bittide.Instances.Hitl.Setup (FpgaCount) +import Bittide.Instances.Hitl.Utils.Driver +import Bittide.Instances.Hitl.Utils.Gdb (initGdb) +import Bittide.Instances.Hitl.Utils.OpenOcd (parseBootTapInfo, parseTapInfo) +import Bittide.Instances.Hitl.Utils.Serial (initSerial) +import Bittide.Instances.Hitl.Utils.Usb (resetUsbDeviceByLocation) +import Bittide.Instances.Hitl.Utils.Utils (dumpCcSamples) +import Control.Concurrent.Async (forConcurrently_, mapConcurrently_) +import Control.Concurrent.Async.Extra (zipWithConcurrently3_) +import Control.Monad (forM_) +import Control.Monad.IO.Class +import Data.Vector.Internal.Check (HasCallStack) +import Project.Chan +import Project.FilePath +import Project.Handle (assertEither) +import System.Exit +import System.FilePath +import Vivado.Tcl (HwTarget) +import Vivado.VivadoM (VivadoM) +import "bittide-extra" Control.Exception.Extra (brackets) + +import qualified Bittide.Instances.Hitl.AsyncCommsDemo.MemoryMaps as MemoryMaps +import qualified Bittide.Instances.Hitl.Utils.OpenOcd as Ocd +import qualified Data.List as L +import qualified Gdb +import qualified System.Timeout.Extra as T + +driver :: + (HasCallStack) => + String -> + [(HwTarget, DeviceInfo)] -> + VivadoM ExitCode +driver testName targets = do + let (_hwTargets, deviceInfos) = L.unzip targets + liftIO + . putStrLn + $ "Running driver function for targets " + <> show ((.deviceId) <$> deviceInfos) + + projectDir <- liftIO $ findParentContaining "cabal.project" + let hitlDir = projectDir "_build/hitl" testName + + forM_ targets (assertProbe "probe_test_start") + + -- Reset USB adapter, see documentation of "Bittide.Instances.Hitl.Utils.Usb" + liftIO $ forM_ deviceInfos $ \d -> resetUsbDeviceByLocation d.usbAdapterLocation + + let + expectedJtagIds = [0x0514C001, 0x1514C001, 0x2514C001] + toInitArgs deviceInfo targetIndex = + Ocd.InitOpenOcdArgs{deviceInfo, expectedJtagIds, hitlDir, targetIndex} + initArgs = L.zipWith toInitArgs deviceInfos [0 ..] + optionalBootInitArgs = L.repeat def{Ocd.logPrefix = "boot-", Ocd.initTcl = "vexriscv_boot_init.tcl"} + openOcdBootStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalBootInitArgs + + let serialStarts = liftIO <$> L.zipWith (initSerial hitlDir) targets [0 ..] + brackets serialStarts (liftIO . snd) $ \(L.map fst -> serials) -> do + brackets openOcdBootStarts (liftIO . (.cleanup)) $ \initOcdsData -> do + let bootTapInfos = parseBootTapInfo <$> initOcdsData + + Gdb.withGdbs (L.length targets) $ \bootGdbs -> do + liftIO + $ zipWithConcurrently3_ (initGdb hitlDir "async-comms-demo-boot") bootGdbs bootTapInfos targets + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs + liftIO $ mapConcurrently_ Gdb.continue bootGdbs + liftIO + $ T.tryWithTimeout T.PrintActionTime "Waiting for done" 60_000_000 + $ forConcurrently_ serials + $ \pico -> + waitForLine pico "[BT] Going into infinite loop.." + + let + optionalInitArgs = L.repeat def + openOcdStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalInitArgs + + brackets openOcdStarts (liftIO . (.cleanup)) $ \initOcdsData -> do + let + allTapInfos = parseTapInfo expectedJtagIds <$> initOcdsData + + _bootTapInfos, muTapInfos, ccTapInfos :: [Ocd.TapInfo] + (_bootTapInfos, muTapInfos, ccTapInfos) + | all (== L.length expectedJtagIds) (L.length <$> allTapInfos) + , [boots, mus, ccs] <- L.transpose allTapInfos = + (boots, mus, ccs) + | otherwise = + error + $ "Unexpected number of OpenOCD taps initialized. Expected: " + <> show (L.length expectedJtagIds) + <> ", but got: " + <> show (L.length <$> allTapInfos) + + Gdb.withGdbs (L.length targets) $ \ccGdbs -> do + liftIO + $ zipWithConcurrently3_ (initGdb hitlDir "async-comms-demo-clock-control") ccGdbs ccTapInfos targets + Gdb.withGdbs (L.length targets) $ \muGdbs -> do + liftIO + $ zipWithConcurrently3_ (initGdb hitlDir "async-comms-demo-management-unit") muGdbs muTapInfos targets + brackets serialStarts (liftIO . snd) $ \(L.map fst -> serials) -> do + let goDumpCcSamples = dumpCcSamples MemoryMaps.clockControl hitlDir (defCcConf (natToNum @FpgaCount)) ccGdbs + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs + liftIO $ mapConcurrently_ Gdb.continue (ccGdbs <> muGdbs) + liftIO + $ T.tryWithTimeoutOn + T.PrintActionTime + "Waiting for CPU test status" + (60_000_000) + goDumpCcSamples + $ forConcurrently_ serials + $ \pico -> + waitForLine pico "[MU] Demo complete." + + liftIO goDumpCcSamples + + pure ExitSuccess diff --git a/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/MemoryMaps.hs b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/MemoryMaps.hs new file mode 100644 index 0000000000..de50b055d6 --- /dev/null +++ b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/MemoryMaps.hs @@ -0,0 +1,12 @@ +-- SPDX-FileCopyrightText: 2026 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +module Bittide.Instances.Hitl.AsyncCommsDemo.MemoryMaps where + +import Bittide.Instances.Hitl.AsyncCommsDemo.UserCore (mkUserCore, muConfig, ringBufferDepth) +import Bittide.Instances.Hitl.GenericDemo.MemoryMaps (extractMemoryMaps) +import Protocols.MemoryMap (MemoryMap) + +boot, managementUnit, clockControl :: MemoryMap +(boot, managementUnit, clockControl) = extractMemoryMaps ringBufferDepth muConfig mkUserCore diff --git a/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/TopEntity.hs b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/TopEntity.hs new file mode 100644 index 0000000000..0a5ba75867 --- /dev/null +++ b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/TopEntity.hs @@ -0,0 +1,21 @@ +-- SPDX-FileCopyrightText: 2026 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 +-- Defined in GenericDemo +{-# OPTIONS_GHC -Wno-missing-signatures #-} + +module Bittide.Instances.Hitl.AsyncCommsDemo.TopEntity where + +import Bittide.Hitl (HitlTestGroup) +import Bittide.Instances.Hitl.AsyncCommsDemo.UserCore (mkUserCore, muConfig, ringBufferDepth) +import Bittide.Instances.Hitl.GenericDemo.TopEntity (demoTest, mkTests) +import Clash.Annotations.TH (makeTopEntity) + +import qualified Bittide.Instances.Hitl.AsyncCommsDemo.Driver as Driver + +asyncCommsDemoTest = demoTest ringBufferDepth muConfig mkUserCore +{-# OPAQUE asyncCommsDemoTest #-} +makeTopEntity 'asyncCommsDemoTest + +tests :: HitlTestGroup +tests = mkTests 'asyncCommsDemoTest Driver.driver diff --git a/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/UserCore.hs b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/UserCore.hs new file mode 100644 index 0000000000..8b258687f8 --- /dev/null +++ b/bittide-instances/src/Bittide/Instances/Hitl/AsyncCommsDemo/UserCore.hs @@ -0,0 +1,56 @@ +-- SPDX-FileCopyrightText: 2026 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- | User core for the async-comms demo: essentially an empty circuit. +module Bittide.Instances.Hitl.AsyncCommsDemo.UserCore ( + UserCoreBusses, + RingBufferDepth, + ringBufferDepth, + mkUserCore, + muConfig, +) where + +import Clash.Explicit.Prelude +import Protocols + +import Bittide.Instances.Hitl.GenericDemo.BringUp (NmuRemBusWidth, UserCoreCircuit) +import Bittide.Instances.Hitl.GenericDemo.Core (NmuInternalBusses) +import Bittide.ProcessingElement (PeConfig (..), PrefixWidth) + +import qualified Bittide.Cpus.Riscv32imc as Riscv32imc + +type UserCoreBusses = 0 + +type RingBufferDepth = 128 + +ringBufferDepth :: SNat RingBufferDepth +ringBufferDepth = SNat + +{- | Management unit configuration for the async comms demo. Its firmware is +much larger than that of the other demos (it links in smoltcp), so it needs +more memory than the default 'Bittide.Instances.Hitl.GenericDemo.Core.muConfig' +provides. +-} +muConfig :: + ( KnownNat n + , PrefixWidth (n + NmuInternalBusses) <= 30 + ) => + PeConfig (n + NmuInternalBusses) +muConfig = + PeConfig + { cpu = Riscv32imc.vexRiscv1 + , depthI = SNat @(Div (10 * 16 * 1024) 4) -- One RAMB18E2 is 16KB, this uses 10 of them. + , depthD = SNat @(Div (10 * 16 * 1024) 4) -- One RAMB18E2 is 16KB, this uses 10 of them. + , initI = Nothing + , initD = Nothing + , iBusTimeout = d0 + , dBusTimeout = d0 + , includeIlaWb = False + } + +mkUserCore :: UserCoreCircuit UserCoreBusses (NmuRemBusWidth UserCoreBusses) +mkUserCore _bitClk _bitRst _bitEna _localCounter _maybeDna = + circuit $ \(muBusses, _rxs2Raw, rxLinks) -> do + [] <- idC -< muBusses + idC -< rxLinks diff --git a/bittide-instances/src/Bittide/Instances/Hitl/Driver/DnaOverSerial.hs b/bittide-instances/src/Bittide/Instances/Hitl/Driver/DnaOverSerial.hs index 5d8beb7cee..7ca2bb464b 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/Driver/DnaOverSerial.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/Driver/DnaOverSerial.hs @@ -36,22 +36,22 @@ dnaOverSerialDriver :: VivadoM ExitCode dnaOverSerialDriver _name targets = do -- Reset USB adapter, see documentation of "Bittide.Instances.Hitl.Utils.Usb" - liftIO $ forM_ targets $ \(_, d) -> resetUsbDeviceByLocation d.usbAdapterLocation + liftIO $ forM_ deviceInfos $ \d -> resetUsbDeviceByLocation d.usbAdapterLocation results <- brackets (liftIO <$> initSerials) (liftIO . snd) $ \initSerialsData -> do let targetSerials = fst <$> initSerialsData liftIO $ putStrLn "Starting all targets to read DNA values" -- start all targets - forM_ targets $ \(hwT, _) -> do + forM_ hwTargets $ \hwT -> do openHardwareTarget hwT updateVio "vioHitlt" [("probe_test_start", "1")] liftIO $ putStrLn "Expecting specific DNAs for all serial ports" liftIO $ putStrLn "Serial ports:" - mapM_ (liftIO . putStrLn) [d.serial | (_, d) <- targets] + mapM_ (liftIO . putStrLn) [d.serial | d <- deviceInfos] - forM (L.zip targets targetSerials) $ \((_, d), serialHandle) -> do + forM (L.zip deviceInfos targetSerials) $ \(d, serialHandle) -> do liftIO $ putStrLn $ "Waiting for output on port: " <> d.serial res <- liftIO $ checkDna d serialHandle pure res @@ -65,9 +65,9 @@ dnaOverSerialDriver _name targets = do where -- Must match the gateware's UART baud rate (`dnaOverSerial` uses @SNat \@9600@). baud = 9600 - + (hwTargets, deviceInfos) = L.unzip targets initSerials :: [IO (Serial.SerialHandle, IO ())] - initSerials = flip L.map targets $ \(_hwT, dI) -> do + initSerials = flip L.map deviceInfos $ \dI -> do (serialHandle, serialClean) <- Serial.start dI.serial baud hSetBuffering serialHandle.handle LineBuffering diff --git a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/BringUp.hs b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/BringUp.hs index a01d091e32..7090d8ecb6 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/BringUp.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/BringUp.hs @@ -100,6 +100,8 @@ bringUp :: , NmuRemBusWidth userCoreBusses <= 27 ) => SNat ringBufferDepth -> + -- | Management unit processing element configuration + PeConfig (NmuExternalBusses userCoreBusses + NmuInternalBusses) -> UserCoreCircuit userCoreBusses (NmuRemBusWidth userCoreBusses) -> "REFCLK" ::: Clock Basic125 -> "TEST_RST" ::: Reset Basic125 -> @@ -113,7 +115,7 @@ bringUp :: , "UART_TX" ::: CSignal Basic125 Bit , "FINC_FDEC" ::: CSignal Bittide (FINC, FDEC) ) -bringUp bufferDepth mkUserCore refClk refRst = +bringUp bufferDepth muConfig mkUserCore refClk refRst = withLittleEndian $ circuit $ \(memoryMaps, jtag, gths) -> do ([bootMm], coreMemoryMaps) <- Vec.split -< memoryMaps @@ -158,6 +160,7 @@ bringUp bufferDepth mkUserCore refClk refRst = ) <- core bufferDepth + muConfig mkUserCore (refClk, refRst) (bittideClk, bittideRst, enableGen) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/Core.hs b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/Core.hs index d1378e3fda..701626c993 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/Core.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/Core.hs @@ -43,6 +43,7 @@ module Bittide.Instances.Hitl.GenericDemo.Core ( NmuRemBusWidth, UserCoreCircuit, core, + muConfig, ) where import Clash.Explicit.Prelude @@ -194,6 +195,8 @@ managementUnit :: , KnownNat userCoreBusses , PrefixWidth (NmuExternalBusses userCoreBusses + NmuInternalBusses) <= 30 ) => + -- | Processing element configuration + PeConfig (NmuExternalBusses userCoreBusses + NmuInternalBusses) -> -- | External counter Signal dom (Unsigned 64) -> -- | DNA value @@ -207,10 +210,10 @@ managementUnit :: , Bitbone dom (NmuRemBusWidth userCoreBusses) ) ) -managementUnit externalCounter maybeDna = +managementUnit peConfig externalCounter maybeDna = circuit $ \(mm, jtag) -> do -- Core and interconnect - allBusses <- processingElement NoDumpVcd muConfig -< (mm, jtag) + allBusses <- processingElement NoDumpVcd peConfig -< (mm, jtag) ([timeBus, uartBus, dnaBus], restBusses) <- Vec.split -< allBusses -- Peripherals @@ -232,6 +235,8 @@ core :: , 1 <= NmuRemBusWidth userCoreBusses ) => SNat ringBufferDepth -> + -- | Management unit processing element configuration + PeConfig (NmuExternalBusses userCoreBusses + NmuInternalBusses) -> UserCoreCircuit userCoreBusses (NmuRemBusWidth userCoreBusses) -> (Clock Basic125, Reset Basic125) -> (Clock Bittide, Reset Bittide, Enable Bittide) -> @@ -250,7 +255,7 @@ core :: , "UARTS" ::: Vec InternalCpuCount (Df Bittide (BitVector 8)) , "MU_TRANSCEIVER" ::: BitboneMm Bittide (NmuRemBusWidth userCoreBusses) ) -core bufferDepth mkUserCore (refClk, refRst) (bitClk, bitRst, bitEna) rxClocks rxResets = withXilinx +core bufferDepth peConfig mkUserCore (refClk, refRst) (bitClk, bitRst, bitEna) rxClocks rxResets = withXilinx $ circuit $ \(memoryMaps, jtag, mask, linksSuitableForCc, Fwd rxs0) -> do [muMm, ccMm] <- idC -< memoryMaps @@ -263,7 +268,7 @@ core bufferDepth mkUserCore (refClk, refRst) (bitClk, bitRst, bitEna) rxClocks r -- Start management unit (muUartBytesBittide, muWbAll) <- - withBittideClockResetEnable managementUnit localCounter maybeDna -< (muMm, muJtag) + withBittideClockResetEnable managementUnit peConfig localCounter maybeDna -< (muMm, muJtag) (ebWbs, muWbs1) <- Vec.split -< muWbAll (rxBufferBusses, muWbs2) <- Vec.split -< muWbs1 (txBufferBusses, muWbs3) <- Vec.split -< muWbs2 diff --git a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/MemoryMaps.hs b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/MemoryMaps.hs index cfe16be091..1be2745bb6 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/MemoryMaps.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/MemoryMaps.hs @@ -18,7 +18,7 @@ import Bittide.Instances.Hitl.GenericDemo.BringUp ( bringUp, ) import Bittide.Instances.Hitl.GenericDemo.Core (NmuExternalBusses, NmuInternalBusses) -import Bittide.ProcessingElement (PrefixWidth) +import Bittide.ProcessingElement (PeConfig, PrefixWidth) import Clash.Sized.Vector.ToTuple (vecToTuple) import Protocols.MemoryMap (MemoryMap) import VexRiscv (JtagIn (..)) @@ -35,12 +35,14 @@ extractMemoryMaps :: , NmuRemBusWidth userCoreBusses <= 27 ) => SNat ringBufferDepth -> + -- | Processing element configuration + PeConfig (NmuExternalBusses userCoreBusses + NmuInternalBusses) -> UserCoreCircuit userCoreBusses (NmuRemBusWidth userCoreBusses) -> -- | (boot, management unit, clock control) (MemoryMap, MemoryMap, MemoryMap) -extractMemoryMaps bufferDepth mkUserCore = (bootMm, managementUnitMm, clockControlMm) +extractMemoryMaps bufferDepth muConfig mkUserCore = (bootMm, managementUnitMm, clockControlMm) where - Circuit circuitFn = bringUp bufferDepth mkUserCore clockGen noReset + Circuit circuitFn = bringUp bufferDepth muConfig mkUserCore clockGen noReset (SimOnly bootMm, SimOnly managementUnitMm, SimOnly clockControlMm) = vecToTuple memoryMaps diff --git a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/TopEntity.hs b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/TopEntity.hs index ecc3903076..50e35d04bd 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/TopEntity.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/TopEntity.hs @@ -34,7 +34,7 @@ import Bittide.Instances.Hitl.GenericDemo.BringUp ( ) import Bittide.Instances.Hitl.GenericDemo.Core (NmuExternalBusses, NmuInternalBusses) import Bittide.Instances.Hitl.Setup (LinkCount, allHwTargets, channelNames, clockPaths) -import Bittide.ProcessingElement (PrefixWidth) +import Bittide.ProcessingElement (PeConfig, PrefixWidth) import Clash.Xilinx.ClockGen (clockWizardDifferential) import System.Exit (ExitCode) import System.FilePath (()) @@ -47,7 +47,7 @@ import qualified Protocols.Spi as Spi {- | Shared top-entity body for the wire and soft-UGN demos. Wired up by each demo's thin @TopEntity.hs@ wrapper, which supplies the demo-specific -@ringBufferDepth@ and @mkUserCore@. +@ringBufferDepth@, management unit configuration, and @mkUserCore@. -} demoTest :: forall userCoreBusses ringBufferDepth. @@ -59,6 +59,8 @@ demoTest :: , NmuRemBusWidth userCoreBusses <= 27 ) => SNat ringBufferDepth -> + -- | Management unit processing element configuration + PeConfig (NmuExternalBusses userCoreBusses + NmuInternalBusses) -> UserCoreCircuit userCoreBusses (NmuRemBusWidth userCoreBusses) -> "SMA_MGT_REFCLK_C" ::: DiffClock Ext200 -> "SYSCLK_125" ::: DiffClock Ext125 -> @@ -81,7 +83,7 @@ demoTest :: , "USB_UART_RXD" ::: Signal Basic125 Bit , "SYNC_OUT" ::: Signal Basic125 Bit ) -demoTest ringBufferDepth mkUserCore boardClkDiff refClkDiff rxs rxns rxps spiS2M jtagIn _uartRx syncIn = +demoTest ringBufferDepth muConfig mkUserCore boardClkDiff refClkDiff rxs rxns rxps spiS2M jtagIn _uartRx syncIn = ( txs , txns , txps @@ -113,7 +115,7 @@ demoTest ringBufferDepth mkUserCore boardClkDiff refClkDiff rxs rxns rxps spiS2M ) ) = toSignals - (bringUp ringBufferDepth mkUserCore refClk testReset) + (bringUp ringBufferDepth muConfig mkUserCore refClk testReset) ( (repeat (), jtagIn, (boardClk, rxs, rxns, rxps, channelNames, clockPaths)) , (spiS2M, syncIn, (), ()) ) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index 0b3d5430a5..d001ee5806 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -43,10 +43,11 @@ driver :: [(HwTarget, DeviceInfo)] -> VivadoM ExitCode driver testName targets = do + let (_hwTargets, deviceInfos) = L.unzip targets liftIO . putStrLn $ "Running driver function for targets " - <> show ((\(_, info) -> info.deviceId) <$> targets) + <> show ((.deviceId) <$> deviceInfos) projectDir <- liftIO $ findParentContaining "cabal.project" let hitlDir = projectDir "_build/hitl" testName @@ -54,14 +55,14 @@ driver testName targets = do forM_ targets (assertProbe "probe_test_start") -- Reset USB adapter, see documentation of "Bittide.Instances.Hitl.Utils.Usb" - liftIO $ forM_ targets $ \(_, d) -> resetUsbDeviceByLocation d.usbAdapterLocation + liftIO $ forM_ deviceInfos $ \d -> resetUsbDeviceByLocation d.usbAdapterLocation let -- BOOT / MU / CC IDs expectedJtagIds = [0x0514C001, 0x1514C001, 0x2514C001] - toInitArgs (_, deviceInfo) targetIndex = + toInitArgs deviceInfo targetIndex = Ocd.InitOpenOcdArgs{deviceInfo, expectedJtagIds, hitlDir, targetIndex} - initArgs = L.zipWith toInitArgs targets [0 ..] + initArgs = L.zipWith toInitArgs deviceInfos [0 ..] optionalBootInitArgs = L.repeat def{Ocd.logPrefix = "boot-", Ocd.initTcl = "vexriscv_boot_init.tcl"} openOcdBootStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalBootInitArgs diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/MemoryMaps.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/MemoryMaps.hs index b9deec070b..baae97f022 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/MemoryMaps.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/MemoryMaps.hs @@ -4,9 +4,10 @@ module Bittide.Instances.Hitl.SoftUgnDemo.MemoryMaps where +import Bittide.Instances.Hitl.GenericDemo.Core (muConfig) import Bittide.Instances.Hitl.GenericDemo.MemoryMaps (extractMemoryMaps) import Bittide.Instances.Hitl.SoftUgnDemo.UserCore (mkUserCore, ringBufferDepth) import Protocols.MemoryMap (MemoryMap) boot, managementUnit, clockControl :: MemoryMap -(boot, managementUnit, clockControl) = extractMemoryMaps ringBufferDepth mkUserCore +(boot, managementUnit, clockControl) = extractMemoryMaps ringBufferDepth muConfig mkUserCore diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs index 7ca37a92fa..792015a624 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs @@ -7,13 +7,14 @@ module Bittide.Instances.Hitl.SoftUgnDemo.TopEntity where import Bittide.Hitl (HitlTestGroup) +import Bittide.Instances.Hitl.GenericDemo.Core (muConfig) import Bittide.Instances.Hitl.GenericDemo.TopEntity (demoTest, mkTests) import Bittide.Instances.Hitl.SoftUgnDemo.UserCore (mkUserCore, ringBufferDepth) import Clash.Annotations.TH (makeTopEntity) import qualified Bittide.Instances.Hitl.SoftUgnDemo.Driver as Driver -softUgnDemoTest = demoTest ringBufferDepth mkUserCore +softUgnDemoTest = demoTest ringBufferDepth muConfig mkUserCore {-# OPAQUE softUgnDemoTest #-} makeTopEntity 'softUgnDemoTest diff --git a/bittide-instances/src/Bittide/Instances/Hitl/Tests.hs b/bittide-instances/src/Bittide/Instances/Hitl/Tests.hs index 1d376fe982..1f9df3ef65 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/Tests.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/Tests.hs @@ -20,6 +20,7 @@ module Bittide.Instances.Hitl.Tests ( import Bittide.Hitl (ClashTargetName, HitlTestCase (..), HitlTestGroup (..)) import Prelude +import qualified Bittide.Instances.Hitl.AsyncCommsDemo.TopEntity as AsyncCommsDemo import qualified Bittide.Instances.Hitl.BoardTest as BoardTest import qualified Bittide.Instances.Hitl.Ddr4 as Ddr4 import qualified Bittide.Instances.Hitl.DnaOverSerial as DnaOverSerial @@ -44,6 +45,7 @@ hitlTests = <> [Ethernet.tests] <> [FincFdec.tests] <> [LinkConfiguration.tests] + <> [AsyncCommsDemo.tests] <> [Si539xConfiguration.tests] <> [SoftUgnDemo.tests] <> [SyncInSyncOut.tests] diff --git a/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/Driver.hs index f27046522e..7680fc39fe 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/Driver.hs @@ -240,10 +240,11 @@ driver :: [(HwTarget, DeviceInfo)] -> VivadoM ExitCode driver testName targets = do + let (_hwTargets, deviceInfos) = L.unzip targets liftIO . putStrLn $ "Running driver function for targets " - <> show ((\(_, info) -> info.deviceId) <$> targets) + <> show ((.deviceId) <$> deviceInfos) projectDir <- liftIO $ findParentContaining "cabal.project" let hitlDir = projectDir "_build/hitl" testName @@ -251,14 +252,14 @@ driver testName targets = do forM_ targets (assertProbe "probe_test_start") -- Reset USB adapter, see documentation of "Bittide.Instances.Hitl.Utils.Usb" - liftIO $ forM_ targets $ \(_, d) -> resetUsbDeviceByLocation d.usbAdapterLocation + liftIO $ forM_ deviceInfos $ \d -> resetUsbDeviceByLocation d.usbAdapterLocation let -- BOOT / MU / CC IDs expectedJtagIds = [0x0514C001, 0x1514C001, 0x2514C001] - toInitArgs (_, deviceInfo) targetIndex = + toInitArgs deviceInfo targetIndex = Ocd.InitOpenOcdArgs{deviceInfo, expectedJtagIds, hitlDir, targetIndex} - initArgs = L.zipWith toInitArgs targets [0 ..] + initArgs = L.zipWith toInitArgs deviceInfos [0 ..] optionalBootInitArgs = L.repeat def{Ocd.logPrefix = "boot-", Ocd.initTcl = "vexriscv_boot_init.tcl"} openOcdBootStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalBootInitArgs diff --git a/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/MemoryMaps.hs b/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/MemoryMaps.hs index ef0891c290..d701e6b26a 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/MemoryMaps.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/MemoryMaps.hs @@ -4,9 +4,10 @@ module Bittide.Instances.Hitl.WireDemo.MemoryMaps where +import Bittide.Instances.Hitl.GenericDemo.Core (muConfig) import Bittide.Instances.Hitl.GenericDemo.MemoryMaps (extractMemoryMaps) import Bittide.Instances.Hitl.WireDemo.UserCore (mkUserCore, ringBufferDepth) import Protocols.MemoryMap (MemoryMap) boot, managementUnit, clockControl :: MemoryMap -(boot, managementUnit, clockControl) = extractMemoryMaps ringBufferDepth mkUserCore +(boot, managementUnit, clockControl) = extractMemoryMaps ringBufferDepth muConfig mkUserCore diff --git a/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/TopEntity.hs b/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/TopEntity.hs index 2327d59ac2..c576605023 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/TopEntity.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/WireDemo/TopEntity.hs @@ -7,13 +7,14 @@ module Bittide.Instances.Hitl.WireDemo.TopEntity where import Bittide.Hitl (HitlTestGroup) +import Bittide.Instances.Hitl.GenericDemo.Core (muConfig) import Bittide.Instances.Hitl.GenericDemo.TopEntity (demoTest, mkTests) import Bittide.Instances.Hitl.WireDemo.UserCore (mkUserCore, ringBufferDepth) import Clash.Annotations.TH (makeTopEntity) import qualified Bittide.Instances.Hitl.WireDemo.Driver as Driver -wireDemoTest = demoTest ringBufferDepth mkUserCore +wireDemoTest = demoTest ringBufferDepth muConfig mkUserCore {-# OPAQUE wireDemoTest #-} makeTopEntity 'wireDemoTest diff --git a/bittide-instances/src/Bittide/Instances/MemoryMaps.hs b/bittide-instances/src/Bittide/Instances/MemoryMaps.hs index 712222e9de..267236fec6 100644 --- a/bittide-instances/src/Bittide/Instances/MemoryMaps.hs +++ b/bittide-instances/src/Bittide/Instances/MemoryMaps.hs @@ -21,6 +21,7 @@ import Language.Haskell.TH (reportError, runIO) import System.Directory (createDirectoryIfMissing, removePathForcibly) import System.FilePath +import qualified Bittide.Instances.Hitl.AsyncCommsDemo.MemoryMaps as AsyncCommsDemo import qualified Bittide.Instances.Hitl.Si539xConfiguration as Si539xConfiguration import qualified Bittide.Instances.Hitl.SoftUgnDemo.MemoryMaps as SoftUgnDemo import qualified Bittide.Instances.Hitl.WireDemo.MemoryMaps as WireDemo @@ -46,6 +47,9 @@ $( do ------------------------------- let memoryMaps = [ ("AddressableBytesWb", AddressableBytesWb.memoryMap) + , ("AsyncCommsDemoBoot", AsyncCommsDemo.boot) + , ("AsyncCommsDemoManagementUnit", AsyncCommsDemo.managementUnit) + , ("AsyncCommsDemoClockControl", AsyncCommsDemo.clockControl) , ("ClockControlWb", ClockControlWb.dutMm) , ("CaptureUgnTest", CaptureUgn.memoryMap) , ("DnaPortE2Test", DnaPortE2.dutMm) diff --git a/bittide-instances/src/Bittide/Instances/Tests/RingBuffer.hs b/bittide-instances/src/Bittide/Instances/Tests/RingBuffer.hs index adf89f9949..46a391b79b 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/RingBuffer.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/RingBuffer.hs @@ -21,6 +21,7 @@ import Project.FilePath import Protocols import Protocols.Df.Extra (tdpbramRamOp) import Protocols.Experimental.Simulate (SimulationConfig (..), sampleC) +import Protocols.Extra (fmapC) import Protocols.Idle import Protocols.MemoryMap import VexRiscv (DumpVcd (NoDumpVcd)) @@ -33,6 +34,7 @@ import Bittide.SharedTypes (withLittleEndian) import Bittide.Wishbone import qualified Data.List as L +import qualified Protocols.Vec as Vec createDomain vSystem{vName = "Slow", vPeriod = hzToPeriod 1_000_000} @@ -40,6 +42,12 @@ createDomain vSystem{vName = "Slow", vPeriod = hzToPeriod 1_000_000} memDepth :: SNat 16 memDepth = SNat +{- | Apply the same circuit to every element of a vector of protocols. This is +just 'fmapC' with an explicit 'SNat' argument to guide type inference. +-} +replicateC :: forall n a b. SNat n -> Circuit a b -> Circuit (Vec n a) (Vec n b) +replicateC SNat = fmapC + dutMM :: (HasCallStack) => Protocols.MemoryMap.MemoryMap dutMM = (\(SimOnly mm, _) -> mm) @@ -52,25 +60,34 @@ dutMM = dutWithPeConfig :: (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom, KnownNat latency) => SNat latency -> - PeConfig 6 -> + PeConfig 8 -> Circuit (ToConstBwd Mm) (Df dom (BitVector 8)) dutWithPeConfig latency peConfig = withLittleEndian $ circuit $ \mm -> do (uartRx, jtagIdle) <- idleSource - [uartBus, wbTx, wbRx, timeBus] <- - processingElement NoDumpVcd peConfig -< (mm, jtagIdle) + ([uartBus, timeBus], wbTxs0, wbRxs0) <- + Vec.split3 + <| processingElement NoDumpVcd peConfig + -< (mm, jtagIdle) (uartTx, _uartStatus) <- uartInterfaceWb d16 d2 uartBytes -< (uartBus, uartRx) - txOut <- transmitRingBuffer (tdpbramRamOp tdpbram hasClock hasClock) memDepth -< wbTx - txOutDelayed <- applyC (toSignal . delayN latency 0 . fromSignal) id -< txOut - receiveRingBuffer (\ena -> blockRam hasClock ena (replicate memDepth 0)) memDepth - -< (wbRx, txOutDelayed) + txOuts <- + replicateC + d2 + (transmitRingBuffer (tdpbramRamOp tdpbram hasClock hasClock) memDepth) + -< wbTxs0 + -- Add configurable latency between TX and RX ringbuffers + txOutDelayeds <- fmapC (applyC (toSignal . delayN latency 0 . fromSignal) id) -< txOuts + idleSink + <| fmapC (receiveRingBuffer (\ena -> blockRam hasClock ena (replicate memDepth 0)) memDepth) + <| Vec.zip + -< (wbRxs0, txOutDelayeds) _cnt <- timeWb Nothing -< timeBus idC -< uartTx {-# OPAQUE dutWithPeConfig #-} -type IMemWords = DivRU (64 * 1024) 4 +type IMemWords = DivRU (80 * 1024) 4 type DMemWords = DivRU (64 * 1024) 4 -peConfigFromBinaryName :: String -> IO (PeConfig 6) +peConfigFromBinaryName :: String -> IO (PeConfig 8) peConfigFromBinaryName binaryName = do peConfigFromElf (SNat @IMemWords) @@ -88,13 +105,18 @@ takeUntilList prefix xs@(y : ys) | prefix `L.isPrefixOf` xs = [] | otherwise = y : takeUntilList prefix ys --- RingBuffer test simulation -simRingBuffer :: IO () -simRingBuffer = putStr =<< simResultRingBuffer d4 - -simResultRingBuffer :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> IO String -simResultRingBuffer latency = do - peConfig <- peConfigFromBinaryName "ring_buffer_test" +{- | Simulate the ring buffer DUT loaded with the given firmware binary, +returning the UART output up to the test-complete marker. +-} +simResultForBinary :: + forall latency. + (HasCallStack, KnownNat latency) => + Int -> + String -> + SNat latency -> + IO String +simResultForBinary timeoutAfter binaryName latency = do + peConfig <- peConfigFromBinaryName binaryName let dutNoMm = circuit $ do mm <- ignoreMM @@ -103,6 +125,20 @@ simResultRingBuffer latency = do $ (dutWithPeConfig @System latency peConfig) -< mm idC -< uartTx - uartStream = sampleC def{timeoutAfter = 1_000_000} dutNoMm + uartStream = sampleC def{timeoutAfter} dutNoMm result = takeUntilList "=== Test Complete ===" $ chr . fromIntegral <$> catMaybes uartStream pure result + +-- RingBuffer test simulation +simRingBuffer :: IO () +simRingBuffer = putStr =<< simResultRingBuffer d4 + +simResultRingBuffer :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> IO String +simResultRingBuffer = simResultForBinary 1_500_000 "ring_buffer_test" + +-- TCP simultaneous open test simulation +simTcpOpen :: IO () +simTcpOpen = putStr =<< simResultTcpOpen d4 + +simResultTcpOpen :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> IO String +simResultTcpOpen = simResultForBinary 2_000_000 "tcp_simultaneous_open_test" diff --git a/bittide-instances/tests/Wishbone/RingBuffer.hs b/bittide-instances/tests/Wishbone/RingBuffer.hs index acf2ebed95..16bb4c3ab9 100644 --- a/bittide-instances/tests/Wishbone/RingBuffer.hs +++ b/bittide-instances/tests/Wishbone/RingBuffer.hs @@ -20,6 +20,7 @@ import Test.Tasty.TH import Bittide.Instances.Tests.RingBuffer ( simResultRingBuffer, + simResultTcpOpen, ) import qualified Hedgehog as H @@ -39,5 +40,22 @@ prop_ring_buffer_test = H.annotate [i|Result of ring_buffer_test with latency #{latency} cycles: \n#{result}|] H.assert ("TEST PASSED" `isInfixOf` result) +prop_tcp_open_test :: H.Property +prop_tcp_open_test = + -- This test is _very_ slow, so we only run it once at size 100. + H.withTests 1 $ H.property $ do + latency <- H.forAll $ Gen.integral (Range.constant 0 100) + liftIO + $ putStrLn + $ "Testing tcp_simultaneous_open_test with latency " + <> show latency + <> " cycles" + result <- liftIO + $ case someNatVal (fromInteger latency) of + Just (SomeNat (_ :: Proxy n)) -> simResultTcpOpen (SNat @n) + Nothing -> error [i|Invalid latency value: #{latency}|] + H.annotate [i|Result of tcp_simultaneous_open_test with latency #{latency} cycles: \n#{result}|] + H.assert ("=== Simultaneous Open Success ===" `isInfixOf` result) + tests :: TestTree tests = $(testGroupGenerator) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index e79a965e47..fadf290ee1 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -16,5 +16,5 @@ SPDX-License-Identifier: Apache-2.0 - [Demos]() - [Soft UGN Demo](sections/demos/soft-ugn-demo.md) - [Wire Demo](sections/demos/wire-demo.md) -- [Ring Buffer alignment](sections/ringbuffer-alignment.md) +- [Ring Buffer alignment](sections/ring-buffer-alignment.md) - [Asynchronous communication](sections/asynchronous-communication.md) diff --git a/docs/sections/architecture.md b/docs/sections/architecture.md index 2bba10b000..b0fb5b7d3c 100644 --- a/docs/sections/architecture.md +++ b/docs/sections/architecture.md @@ -107,7 +107,7 @@ However, the CPU approach comes with a major limitation - unlike the UGN Capture [^1]: This difference occurs because the Ring Buffer (RB) only supports accessing one address per cycle. The UGN Capture sits before the Ring Buffer (RB) in the data pipeline, while the MU CPU sits behind the RB. So UGN capture can inspect every new word, while the MU needs to know which RB element to inspect. If the MU were to scan the entire RB, it would find the right element, but it would then not know on which clock cycle the element was put into the RB. -This way, the MU does not need to inspect every element in the RB for the clock cycle, it just needs to inspect the one entry it knows the clock cycle will eventually be in. For more detail, see the [Ring Buffer alignment](ringbuffer-alignment.html) section. +This way, the MU does not need to inspect every element in the RB for the clock cycle, it just needs to inspect the one entry it knows the clock cycle will eventually be in. For more detail, see the [Ring Buffer alignment](ring-buffer-alignment.html) section. Once the relationship has been mapped, the sending node can send a "UGN event" (5 bittide words), which will be read by the receiving MU. diff --git a/docs/sections/asynchronous-communication.md b/docs/sections/asynchronous-communication.md index d6be86ea7f..1ef4d806c0 100644 --- a/docs/sections/asynchronous-communication.md +++ b/docs/sections/asynchronous-communication.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0 # Asynchronous Communication Protocol ## Context -In a bittide system, we need asynchronous communication between nodes, particularly during the boot phase. The [Ring Buffer Alignment Protocol](ringbuffer-alignment.md) provides an `AlignedRing Buffer` abstraction that allows for packet exchange. +In a bittide system, we need asynchronous communication between nodes, particularly during the boot phase. The [Ring Buffer Alignment Protocol](ring-buffer-alignment.md) provides an `AlignedRing Buffer` abstraction that allows for packet exchange. However, as described in that protocol's documentation, the raw `AlignedRing Buffer` link is unreliable, subject to packet corruption and loss due to hardware/software speed mismatches. @@ -41,4 +41,4 @@ Develop a proof-of-concept application that: ## Assumptions * An **Aligned Ring Buffer** abstraction exists that provides a read/write interface for single aligned packets. -* The ringbuffer size is sufficient to hold at least one MTU-sized packet plus overhead. +* The ring buffer size is sufficient to hold at least one MTU-sized packet plus overhead. diff --git a/docs/sections/components/receive-ring-buffer.md b/docs/sections/components/receive-ring-buffer.md index 77a70748f2..ac9810509b 100644 --- a/docs/sections/components/receive-ring-buffer.md +++ b/docs/sections/components/receive-ring-buffer.md @@ -16,7 +16,7 @@ The Receive Ring Buffer consists of: - **Memory Buffer**: A single memory buffer written by hardware and read by the CPU. - **Free-Running Write Counter**: A hardware counter that increments each cycle, wrapping around the buffer. When the ring buffer is enabled, incoming frames are written at the address indicated by this counter. - **Enable Register**: When enabled, incoming frames from the network are written to the buffer. When disabled, incoming frames are ignored, but the counter continues to increment to maintain alignment. -- **Clear-at-Count Register**: Resets the write counter to zero when it reaches the configured value. This is used during the [Ring Buffer Alignment](../ringbuffer-alignment.md) procedure to synchronize the TX and RX counters. +- **Clear-at-Count Register**: Resets the write counter to zero when it reaches the configured value. This is used during the [Ring Buffer Alignment](../ring-buffer-alignment.md) procedure to synchronize the TX and RX counters. - **Wishbone Interface**: Allows the CPU to read received data. ### Operation @@ -27,4 +27,4 @@ The Receive Ring Buffer consists of: 2. **CPU Access**: - The CPU reads data from the buffer using the Wishbone interface. - - Because the hardware continuously overwrites the buffer, the CPU must read data before it is overwritten. See [Ring Buffer Alignment](../ringbuffer-alignment.md) and [Asynchronous Communication](../asynchronous-communication.md) for protocols that handle this. + - Because the hardware continuously overwrites the buffer, the CPU must read data before it is overwritten. See [Ring Buffer Alignment](../ring-buffer-alignment.md) and [Asynchronous Communication](../asynchronous-communication.md) for protocols that handle this. diff --git a/docs/sections/components/transmit-ring-buffer.md b/docs/sections/components/transmit-ring-buffer.md index e260add184..100f0ba734 100644 --- a/docs/sections/components/transmit-ring-buffer.md +++ b/docs/sections/components/transmit-ring-buffer.md @@ -26,4 +26,4 @@ The Transmit Ring Buffer consists of: 2. **CPU Access**: - The CPU writes data to the buffer using the Wishbone interface. - - Because the hardware continuously reads from the buffer, the CPU must ensure it writes data before the hardware's read counter reaches that address. See [Ring Buffer Alignment](../ringbuffer-alignment.md) and [Asynchronous Communication](../asynchronous-communication.md) for protocols that handle this. + - Because the hardware continuously reads from the buffer, the CPU must ensure it writes data before the hardware's read counter reaches that address. See [Ring Buffer Alignment](../ring-buffer-alignment.md) and [Asynchronous Communication](../asynchronous-communication.md) for protocols that handle this. diff --git a/docs/sections/demos/soft-ugn-procedure.md b/docs/sections/demos/soft-ugn-procedure.md index b4cb37f651..25a5841f3f 100644 --- a/docs/sections/demos/soft-ugn-procedure.md +++ b/docs/sections/demos/soft-ugn-procedure.md @@ -14,7 +14,7 @@ The procedure operates by exchanging timestamped messages between neighbors. By The procedure relies on the underlying ring buffers. Specifically: * The transmit and receive ring buffers are of equal size and operate as ring buffers with free-running hardware counters. -* The alignment of the ring buffers (where data starting at address `0` in a transmit ring buffer arrives at address `n` in a receive ring buffer) is unknown at the start and is measured by the [Ring Buffer Alignment Procedure](../ringbuffer-alignment.md). +* The alignment of the ring buffers (where data starting at address `0` in a transmit ring buffer arrives at address `n` in a receive ring buffer) is unknown at the start and is measured by the [Ring Buffer Alignment Procedure](../ring-buffer-alignment.md). ## Message Types @@ -49,7 +49,7 @@ Events are scheduled according to the send and receive periods: ## Procedure Operation 1. **Initialization**: - * The MU aligns the ring buffers leveraging the [Ring Buffer Alignment Procedure](../ringbuffer-alignment.md). + * The MU aligns the ring buffers leveraging the [Ring Buffer Alignment Procedure](../ring-buffer-alignment.md). * Initial `SEND` and `RECEIVE` events are scheduled for all ports. 2. **Discovery Loop**: diff --git a/docs/sections/ringbuffer-alignment.md b/docs/sections/ring-buffer-alignment.md similarity index 100% rename from docs/sections/ringbuffer-alignment.md rename to docs/sections/ring-buffer-alignment.md diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index 96905b10dc..7786b74a5a 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -27,6 +27,43 @@ dependencies = [ "memchr", ] +[[package]] +name = "async-comms-demo-boot" +version = "0.1.0" +dependencies = [ + "bittide-build-utils", + "bittide-cpus", + "bittide-hal", + "bittide-macros", + "riscv-rt 0.11.0", +] + +[[package]] +name = "async-comms-demo-clock-control" +version = "0.1.0" +dependencies = [ + "bittide-build-utils", + "bittide-cpus", + "bittide-hal", + "riscv-rt 0.11.0", +] + +[[package]] +name = "async-comms-demo-management-unit" +version = "0.1.0" +dependencies = [ + "bittide-build-utils", + "bittide-hal", + "bittide-sys", + "heapless", + "log", + "riscv 0.10.1", + "riscv-rt 0.11.0", + "smoltcp", + "ufmt", + "zerocopy", +] + [[package]] name = "axi_stream_self_test" version = "0.1.0" @@ -83,7 +120,7 @@ dependencies = [ "memorymap-compiler-rust", "proc-macro2", "quote", - "smoltcp 0.11.0", + "smoltcp", "subst_macros", "ufmt", ] @@ -115,13 +152,15 @@ dependencies = [ "bittide-macros", "clash-bindings", "clash-macros", + "crc", "fdt", "heapless", "itertools", "log", "rand", - "smoltcp 0.12.0", + "smoltcp", "ufmt", + "zerocopy", ] [[package]] @@ -246,6 +285,21 @@ dependencies = [ "typewit", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "critical-section" version = "1.2.0" @@ -745,19 +799,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smoltcp" -version = "0.11.0" -source = "git+https://github.com/smoltcp-rs/smoltcp.git?rev=dc08e0b42e668c331bb2b6f8d80016301d0efe03#dc08e0b42e668c331bb2b6f8d80016301d0efe03" -dependencies = [ - "bitflags", - "byteorder", - "cfg-if", - "heapless", - "log", - "managed", -] - [[package]] name = "smoltcp" version = "0.12.0" @@ -784,7 +825,7 @@ dependencies = [ "log", "riscv 0.10.1", "riscv-rt 0.11.0", - "smoltcp 0.12.0", + "smoltcp", "ufmt", ] @@ -856,6 +897,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tcp_simultaneous_open_test" +version = "0.1.0" +dependencies = [ + "bittide-build-utils", + "bittide-hal", + "bittide-sys", + "log", + "riscv 0.10.1", + "riscv-rt 0.11.0", + "smoltcp", + "ufmt", + "zerocopy", +] + [[package]] name = "time_self_test" version = "0.1.0" @@ -1015,3 +1071,24 @@ dependencies = [ "riscv-rt 0.11.0", "ufmt", ] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] diff --git a/firmware-binaries/Cargo.toml b/firmware-binaries/Cargo.toml index 463f9d04ac..da8759dd3f 100644 --- a/firmware-binaries/Cargo.toml +++ b/firmware-binaries/Cargo.toml @@ -27,6 +27,7 @@ members = [ "sim-tests/axi_stream_self_test", "sim-tests/registerwb_test", "sim-tests/ring_buffer_test", + "sim-tests/tcp_simultaneous_open_test", "sim-tests/capture_ugn_test", "sim-tests/clock-control-wb", "sim-tests/dna_port_e2_test", @@ -41,11 +42,14 @@ members = [ "hitl-tests/vexriscv-hello", "hitl-tests/clock-board", - "demos/soft-ugn-demo-clock-control", + "demos/async-comms-demo-boot", + "demos/async-comms-demo-clock-control", + "demos/async-comms-demo-management-unit", "demos/soft-ugn-demo-boot", + "demos/soft-ugn-demo-clock-control", "demos/soft-ugn-demo-management-unit", - "demos/wire-demo-clock-control", "demos/wire-demo-boot", + "demos/wire-demo-clock-control", "demos/wire-demo-management-unit", ] resolver = "2" diff --git a/firmware-binaries/demos/async-comms-demo-boot/Cargo.lock.license b/firmware-binaries/demos/async-comms-demo-boot/Cargo.lock.license new file mode 100644 index 0000000000..a6421285b9 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-boot/Cargo.lock.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 Google LLC + +SPDX-License-Identifier: CC0-1.0 diff --git a/firmware-binaries/demos/async-comms-demo-boot/Cargo.toml b/firmware-binaries/demos/async-comms-demo-boot/Cargo.toml new file mode 100644 index 0000000000..ebda0522cd --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-boot/Cargo.toml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2026 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "async-comms-demo-boot" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +[lints] +workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +riscv-rt = "0.11.0" +bittide-cpus = { path = "../../../firmware-support/bittide-cpus" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +bittide-macros = { path = "../../../firmware-support/bittide-macros" } + +[build-dependencies] +bittide-build-utils = { path = "../../../firmware-support/bittide-build-utils" } diff --git a/firmware-binaries/demos/async-comms-demo-boot/build.rs b/firmware-binaries/demos/async-comms-demo-boot/build.rs new file mode 100644 index 0000000000..1dae9d6ef1 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-boot/build.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use bittide_build_utils::standard_memmap_build; + +fn main() { + standard_memmap_build("AsyncCommsDemoBoot.json", "DataMemory", "InstructionMemory"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/firmware-binaries/demos/async-comms-demo-boot/src/main.rs b/firmware-binaries/demos/async-comms-demo-boot/src/main.rs new file mode 100644 index 0000000000..77de7abbd3 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-boot/src/main.rs @@ -0,0 +1,39 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] + +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use core::panic::PanicInfo; + +use bittide_hal::async_comms_demo_boot::DeviceInstances; +use bittide_hal::manual_additions::si539x_spi::Config; +use bittide_macros::load_clock_config_csv; + +#[cfg(not(test))] +use riscv_rt::entry; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; +const CONFIG_200: Config<3, 590, 5> = load_clock_config_csv!( + "../../../bittide/data/clock_configs/Si5395J-200MHz-10ppb-Registers.csv" +); + +#[cfg_attr(not(test), entry)] +fn main() -> ! { + let mut uart = INSTANCES.uart; + bittide_cpus::boot::run( + INSTANCES.si539x_spi, + INSTANCES.timer, + INSTANCES.transceivers, + &mut uart, + &CONFIG_200, + ) +} + +#[panic_handler] +fn panic_handler(_info: &PanicInfo) -> ! { + loop { + continue; + } +} diff --git a/firmware-binaries/demos/async-comms-demo-clock-control/Cargo.lock.license b/firmware-binaries/demos/async-comms-demo-clock-control/Cargo.lock.license new file mode 100644 index 0000000000..a6421285b9 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-clock-control/Cargo.lock.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 Google LLC + +SPDX-License-Identifier: CC0-1.0 diff --git a/firmware-binaries/demos/async-comms-demo-clock-control/Cargo.toml b/firmware-binaries/demos/async-comms-demo-clock-control/Cargo.toml new file mode 100644 index 0000000000..167fe20a57 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-clock-control/Cargo.toml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "async-comms-demo-clock-control" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +[lints] +workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bittide-cpus = { path = "../../../firmware-support/bittide-cpus" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +riscv-rt = "0.11.0" + +[build-dependencies] +bittide-build-utils = { path = "../../../firmware-support/bittide-build-utils" } diff --git a/firmware-binaries/demos/async-comms-demo-clock-control/build.rs b/firmware-binaries/demos/async-comms-demo-clock-control/build.rs new file mode 100644 index 0000000000..63fbca5ff5 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-clock-control/build.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use bittide_build_utils::standard_memmap_build; + +fn main() { + standard_memmap_build( + "AsyncCommsDemoClockControl.json", + "DataMemory", + "InstructionMemory", + ); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/firmware-binaries/demos/async-comms-demo-clock-control/src/main.rs b/firmware-binaries/demos/async-comms-demo-clock-control/src/main.rs new file mode 100644 index 0000000000..e655e003bf --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-clock-control/src/main.rs @@ -0,0 +1,36 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] + +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use core::panic::PanicInfo; + +use bittide_hal::async_comms_demo_clock_control::DeviceInstances; + +#[cfg(not(test))] +use riscv_rt::entry; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; + +#[cfg_attr(not(test), entry)] +fn main() -> ! { + let mut uart = INSTANCES.uart; + bittide_cpus::clock_control::run( + INSTANCES.clock_control, + INSTANCES.timer, + &mut uart, + INSTANCES.freeze, + INSTANCES.sample_memory, + INSTANCES.sync_out_generator, + INSTANCES.domain_diff_counters, + ) +} + +#[panic_handler] +fn panic_handler(_info: &PanicInfo) -> ! { + loop { + continue; + } +} diff --git a/firmware-binaries/demos/async-comms-demo-management-unit/Cargo.toml b/firmware-binaries/demos/async-comms-demo-management-unit/Cargo.toml new file mode 100644 index 0000000000..51d9f9e847 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-management-unit/Cargo.toml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2026 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "async-comms-demo-management-unit" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +[dependencies] +riscv = "^0.10" +riscv-rt = "0.11.0" +bittide-sys = { path = "../../../firmware-support/bittide-sys" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +ufmt = "0.2.0" +heapless = "0.8.0" +zerocopy = { version = "0.7", default-features = false } + +[dependencies.smoltcp] +version = "0.12.0" +default-features = false +features = ["log", "medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] + +[dependencies.log] +version = "0.4.21" +features = ["max_level_trace", "release_max_level_info"] + +[build-dependencies] +bittide-build-utils = { path = "../../../firmware-support/bittide-build-utils" } diff --git a/firmware-binaries/demos/async-comms-demo-management-unit/build.rs b/firmware-binaries/demos/async-comms-demo-management-unit/build.rs new file mode 100644 index 0000000000..320bf6a2ba --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-management-unit/build.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use bittide_build_utils::standard_memmap_build; + +fn main() { + standard_memmap_build( + "AsyncCommsDemoManagementUnit.json", + "DataMemory", + "InstructionMemory", + ); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/firmware-binaries/demos/async-comms-demo-management-unit/src/main.rs b/firmware-binaries/demos/async-comms-demo-management-unit/src/main.rs new file mode 100644 index 0000000000..d54b766660 --- /dev/null +++ b/firmware-binaries/demos/async-comms-demo-management-unit/src/main.rs @@ -0,0 +1,479 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] +#![feature(sync_unsafe_cell)] + +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use bittide_hal::hals::async_comms_demo_management_unit::devices::CaptureUgns; +use bittide_hal::hals::async_comms_demo_management_unit::DeviceInstances; +use bittide_hal::manual_additions::ring_buffer::AlignedReceiveBuffer; +use bittide_hal::manual_additions::timer::Duration; +use bittide_sys::hitl::{FPGA_DNAS, LINK_COUNT, LINK_NEIGHBORS}; +use bittide_sys::link_startup::LinkStartup; +use bittide_sys::net_state::{UgnEdge, UgnReport}; +use bittide_sys::smoltcp::link_interface::LinkInterface; +use bittide_sys::smoltcp::link_protocol::{Command, CommandWire, UgnEdgeWire}; +use core::fmt::Write; +use log::{debug, error, info, warn, LevelFilter}; +use riscv::register::{mcause, mepc, mtval}; +use smoltcp::iface::SocketStorage; +use ufmt::uwriteln; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; + +/// Size of the smoltcp TCP socket buffers (in bytes). Must be large enough to +/// hold the largest message exchanged in this demo (a `UgnEdgeWire` is 40 bytes). +const TCP_BUFFER_SIZE: usize = 256; + +/// The FPGA designated as manager: rig position 6 (FPGA id 210308B3A22D). +const MANAGER_DNA: [u8; 12] = FPGA_DNAS[6]; + +#[cfg(not(test))] +use riscv_rt::entry; + +#[cfg_attr(not(test), entry)] +fn main() -> ! { + let mut uart = INSTANCES.uart; + unsafe { + use bittide_sys::uart::log::LOGGER; + let logger = &mut (*LOGGER.get()); + logger.set_logger(uart.clone()); + logger.set_timer(INSTANCES.timer); + logger.display_source = LevelFilter::Warn; + log::set_logger_racy(logger).ok(); + log::set_max_level_racy(LevelFilter::Debug); + } + + info!("=== Async Comms Demo MU ==="); + let transceivers = &INSTANCES.transceivers; + let timer = INSTANCES.timer; + let handshakes = &INSTANCES.handshakes; + let cc = INSTANCES.clock_control; + let elastic_buffers = [ + &INSTANCES.elastic_buffer_0, + &INSTANCES.elastic_buffer_1, + &INSTANCES.elastic_buffer_2, + &INSTANCES.elastic_buffer_3, + &INSTANCES.elastic_buffer_4, + &INSTANCES.elastic_buffer_5, + &INSTANCES.elastic_buffer_6, + ]; + let capture_ugns = &INSTANCES.capture_ugns; + // Pseudocode setup: + // 1) Initialize MU peripherals and scatter/gather calendars for ring_buffers. + // 2) Run LinkStartup per port to bring up physical links and capture UGNs. + // 3) Wait for clock stability; stop auto-centering and record EB deltas. + // 4) Align ring_buffers on all ports (two-phase protocol). + // 5) For each port, create RingBufferDevice + smoltcp Interface with static IP. + // 6) Exchange DNAs and ports with all neighbors over TCP. + // 7) Collect UGN edges over TCP and aggregate at the manager. + + info!("Bringing up links..."); + let mut link_startups = [LinkStartup::new(); LINK_COUNT]; + let timeout = timer.now() + Duration::from_secs(10); + while timer.now() <= timeout && !link_startups.iter().all(|ls| ls.is_done()) { + for (i, link_startup) in link_startups.iter_mut().enumerate() { + link_startup.next(transceivers, handshakes, i, elastic_buffers[i]); + } + } + + let mut unstarted_links = 0; + for (i, ls) in link_startups.iter().enumerate() { + if !ls.is_done() { + error!("Link {} did not start", i); + unstarted_links += 1; + } + } + if unstarted_links > 0 { + panic!("Some links did not start") + } + info!("Waiting for stability..."); + let all_links_stable: u8 = (1 << LINK_COUNT) - 1; + let timeout = timer.now() + Duration::from_secs(10); + loop { + let stable = cc.links_stable()[0]; + if stable == all_links_stable { + break; + } + if timer.now() > timeout { + panic!("Links did not become stable in time.") + } + } + + // Auto-centering is disabled from here on and never re-enabled, making this demo a + // "happy path": it only works while the clock frequencies stay close together. In a + // real system we want to do async comms while the system is still synchronizing, + // which requires either hardware accelerated alignment or continuously polling and + // accounting for auto-center activity. That is planned for a follow-up, see + // https://github.com/bittide/bittide-hardware/issues/1173. + info!("Stopping auto-centering..."); + elastic_buffers + .iter() + .for_each(|eb| eb.set_auto_center_enable(false)); + elastic_buffers + .iter() + .for_each(|eb| eb.wait_auto_center_idle()); + let eb_deltas = elastic_buffers + .iter() + .map(|eb| eb.auto_center_total_adjustments()); + + for (i, eb_delta) in eb_deltas.enumerate() { + capture_ugns.set_elastic_buffer_delta(i, eb_delta).unwrap(); + } + + info!("Captured hardware UGNs"); + for i in 0..LINK_COUNT { + debug!( + " UGN {}: local={}, remote={}", + i, + capture_ugns.local_counter(i).unwrap(), + capture_ugns.remote_counter(i).unwrap() + ); + } + + // Create rx/tx buffer arrays + let rx_buffers = [ + INSTANCES.receive_ring_buffer_0, + INSTANCES.receive_ring_buffer_1, + INSTANCES.receive_ring_buffer_2, + INSTANCES.receive_ring_buffer_3, + INSTANCES.receive_ring_buffer_4, + INSTANCES.receive_ring_buffer_5, + INSTANCES.receive_ring_buffer_6, + ]; + let tx_buffers = [ + INSTANCES.transmit_ring_buffer_0, + INSTANCES.transmit_ring_buffer_1, + INSTANCES.transmit_ring_buffer_2, + INSTANCES.transmit_ring_buffer_3, + INSTANCES.transmit_ring_buffer_4, + INSTANCES.transmit_ring_buffer_5, + INSTANCES.transmit_ring_buffer_6, + ]; + + let dna = INSTANCES.dna.dna(); + let is_manager = dna == MANAGER_DNA; + writeln!( + uart, + "DNA: {dna:?}, Role: {}", + if is_manager { "manager" } else { "subordinate" } + ) + .unwrap(); + + // Create aligned receive buffers for all links and align them + let start_time = INSTANCES.timer.now(); + let mut rx_aligned = rx_buffers.map(AlignedReceiveBuffer::new); + while !rx_aligned + .iter_mut() + .zip(tx_buffers.iter()) + .all(|(rx, tx)| rx.align_step(tx)) + {} + let stop_time = INSTANCES.timer.now(); + uwriteln!( + uart, + "Aligning all buffers took {}.", + (stop_time - start_time) + ) + .unwrap(); + + // Create all LinkInterfaces + info!("Creating LinkInterfaces"); + let make_timer = || unsafe { bittide_hal::shared_devices::Timer::new(INSTANCES.timer.0) }; + + // TCP buffers, borrowed by the LinkInterfaces for their whole lifetime + let mut tcp_rx_buffers = [[0u8; TCP_BUFFER_SIZE]; LINK_COUNT]; + let mut tcp_tx_buffers = [[0u8; TCP_BUFFER_SIZE]; LINK_COUNT]; + let mut socket_storage = [const { [SocketStorage::EMPTY; 1] }; LINK_COUNT]; + let mut links: heapless::Vec, { LINK_COUNT }> = rx_aligned + .into_iter() + .zip(tx_buffers) + .zip(tcp_rx_buffers.iter_mut()) + .zip(tcp_tx_buffers.iter_mut()) + .zip(socket_storage.iter_mut()) + .map(|((((rx, tx), rx_buf), tx_buf), storage)| { + LinkInterface::new(rx, tx, make_timer(), rx_buf, tx_buf, storage) + }) + .collect(); + + // Step 1: Wait for all connections to establish + uwriteln!(uart, "Step 1: Waiting for all connections to establish...").unwrap(); + loop { + links.iter_mut().for_each(|l| l.poll()); + if links.iter().all(|l| l.is_established()) { + break; + } + } + uwriteln!(uart, " All {} links established", LINK_COUNT).unwrap(); + + // Step 2: Exchange DNA with all neighbors + info!("Step 2: Exchanging DNA and ports with all neighbors..."); + debug!(" Our DNA: {:02X?}", dna); + + // Prepare DNA and port data to send + let port_bytes: [[u8; 4]; LINK_COUNT] = core::array::from_fn(|i| (i as u32).to_le_bytes()); + + // Track what we've sent and received + let mut dna_sent = [false; LINK_COUNT]; + let mut port_sent = [false; LINK_COUNT]; + let mut partner_dnas: [Option<[u8; 12]>; LINK_COUNT] = [None; LINK_COUNT]; + let mut partner_ports = [0u32; LINK_COUNT]; + let mut port_received = [false; LINK_COUNT]; + + uwriteln!( + uart, + " Waiting to receive DNA and ports from all neighbors..." + ) + .unwrap(); + + let deadline = timer.now() + Duration::from_secs(1); + while timer.now() < deadline { + for (i, link) in links.iter_mut().enumerate() { + link.poll(); + + // Try receiving partner DNA if we haven't already + if partner_dnas[i].is_none() { + let mut dna_buf = [0u8; 12]; + match link.try_recv_bytes(&mut dna_buf) { + Ok(12) => { + partner_dnas[i] = Some(dna_buf); + debug!(" Link {}: received partner DNA: {:02X?}", i, dna_buf); + } + Ok(n) if n > 0 => { + error!(" Link {}: partial DNA recv: {} bytes", i, n); + } + _ => {} + } + } + + // Try receiving partner port if we received their DNA and haven't received their port yet + if partner_dnas[i].is_some() && !port_received[i] { + if let Ok(port) = link.try_recv::() { + partner_ports[i] = port; + port_received[i] = true; + debug!(" Link {}: received partner port: {}", i, partner_ports[i]); + } + } + + // Try sending our DNA if we haven't already + if !dna_sent[i] && link.try_send_bytes(&dna).is_ok() { + debug!(" Link {}: sent our DNA", i); + dna_sent[i] = true; + } + + // Try sending our port if we sent our DNA and haven't sent our port yet. + if dna_sent[i] && !port_sent[i] && link.try_send_bytes(&port_bytes[i]).is_ok() { + debug!(" Link {}: sent our port {}", i, i); + port_sent[i] = true; + } + } + if partner_dnas.iter().all(|dna| dna.is_some()) && port_received.iter().all(|&r| r) { + info!(" DNA and port exchange complete for all links!"); + break; + } + } + + let dna_success_count = partner_dnas.iter().filter(|dna| dna.is_some()).count(); + if dna_success_count == LINK_COUNT { + info!( + " DNA and port exchange complete for all {} links", + LINK_COUNT + ); + } else { + error!( + " Only received DNA from {} of {} links", + dna_success_count, LINK_COUNT + ); + for (i, dna) in partner_dnas.iter().enumerate() { + if dna.is_none() { + error!(" Link {}: no DNA received", i); + } + } + } + + // Step 2b: Verify the exchanged DNAs and ports against the known demo rig topology + info!("Step 2b: Verifying partner DNAs against demo rig topology..."); + verify_partner_dnas(&dna, &partner_dnas, &partner_ports); + + // Step 3: Build complete UGN report from all neighbor information + info!("Step 3: Building UGN report..."); + + let partner_info: [Option<(&[u8; 12], u32)>; LINK_COUNT] = + core::array::from_fn(|i| partner_dnas[i].as_ref().map(|dna| (dna, partner_ports[i]))); + + let mut report: UgnReport = build_complete_report(&dna, &partner_info, capture_ugns); + info!(" Built report with {} edges", report.count); + + // Step 4: Execute role-specific protocol + if is_manager { + info!("Step 4: Manager collecting reports from subordinates..."); + + for (i, link) in links.iter_mut().enumerate() { + let wire_command: CommandWire = Command::RequestUgnReport.into(); + if let Err(e) = link.send_blocking(&wire_command, Duration::from_secs(1)) { + error!(" Link {}: failed to send command: {:?}", i, e); + continue; + } + let count: u32 = match link.recv_blocking(Duration::from_secs(1)) { + Ok(c) => c, + Err(e) => { + error!(" Link {}: failed to receive count: {:?}", i, e); + continue; + } + }; + debug!(" Link {}: expecting {} edges", i, count); + for j in 0..count { + let edge: UgnEdgeWire = match link.recv_blocking(Duration::from_secs(1)) { + Ok(e) => e, + Err(e) => { + error!(" Link {}: failed to receive edge {}: {:?}", i, j, e); + break; + } + }; + report.insert_edge(edge.into()); + } + } + info!(" Complete UGN graph: {} edges", report.count); + writeln!(uart, " Final UGN Report: {:?}", report).unwrap(); + } else { + // Subordinate role: wait for manager command, then send report + info!("Step 4: Subordinate waiting for manager command..."); + let mut manager_idx = None; + + while manager_idx.is_none() { + for (i, link) in links.iter_mut().enumerate() { + link.poll(); + if let Ok(wire) = link.try_recv::() { + if let Ok(Command::RequestUgnReport) = Command::try_from(wire) { + info!(" Received RequestUgnReport on link {}", i); + manager_idx = Some(i); + break; + } else { + warn!(" Link {}: received unknown command: {:?}", i, wire); + } + } + } + } + + let manager_link = &mut links[manager_idx.unwrap()]; + manager_link + .send_blocking(&report.count, Duration::from_secs(1)) + .expect("Failed to send count"); + + let mut edges_sent = 0; + let mut edge_idx = 0; + while edges_sent < report.count as usize && edge_idx < report.edges.len() { + manager_link.poll(); + if let Some(edge) = report.edges[edge_idx] { + let wire: UgnEdgeWire = edge.into(); + if manager_link.try_send(&wire).is_ok() { + edges_sent += 1; + edge_idx += 1; + } + } else { + edge_idx += 1; + } + } + info!(" Sent {} edges to manager", edges_sent); + } + + uwriteln!(uart, "Demo complete.").unwrap(); + // Keep polling links to ensure all transmissions complete + loop { + links.iter_mut().for_each(|l| l.poll()); + } +} + +#[panic_handler] +fn panic_handler(info: &core::panic::PanicInfo) -> ! { + let mut uart = INSTANCES.uart; + writeln!(uart, "Panicked! #{info}").unwrap(); + loop { + continue; + } +} + +#[export_name = "ExceptionHandler"] +fn exception_handler(_trap_frame: &riscv_rt::TrapFrame) -> ! { + let mut uart = INSTANCES.uart; + riscv::interrupt::free(|| { + uwriteln!(uart, "... caught an exception. Looping forever now.\n").unwrap(); + writeln!(uart, "mcause: {:?}\n", mcause::read()).unwrap(); + writeln!(uart, "mepc: {:?}\n", mepc::read()).unwrap(); + writeln!(uart, "mtval: {:?}\n", mtval::read()).unwrap(); + }); + loop { + continue; + } +} + +/// Check the DNAs and ports received from our neighbors against the known demo +/// rig topology (`FPGA_DNAS` / `LINK_NEIGHBORS`), so wiring or alignment problems +/// show up immediately instead of producing a silently wrong UGN graph. +fn verify_partner_dnas( + own_dna: &[u8; 12], + partner_dnas: &[Option<[u8; 12]>; LINK_COUNT], + partner_ports: &[u32; LINK_COUNT], +) { + let Some(own_idx) = FPGA_DNAS.iter().position(|d| d == own_dna) else { + error!(" Own DNA {:02X?} not found in demo rig table", own_dna); + return; + }; + let mut ok = true; + for (link, partner_dna) in partner_dnas.iter().enumerate() { + let Some(partner_dna) = partner_dna else { + ok = false; + continue; + }; + let neighbor = LINK_NEIGHBORS[own_idx][link]; + if *partner_dna != FPGA_DNAS[neighbor] { + error!( + " Link {}: expected DNA of rig FPGA {} ({:02X?}), got {:02X?}", + link, neighbor, FPGA_DNAS[neighbor], partner_dna + ); + ok = false; + } + // The partner should have reported the port that connects back to us. + let expected_port = LINK_NEIGHBORS[neighbor] + .iter() + .position(|&n| n == own_idx) + .unwrap() as u32; + if partner_ports[link] != expected_port { + error!( + " Link {}: expected partner port {}, got {}", + link, expected_port, partner_ports[link] + ); + ok = false; + } + } + if ok { + info!(" All partner DNAs and ports match the demo rig topology"); + } else { + error!(" Partner DNA/port verification FAILED"); + } +} + +fn build_complete_report( + dna: &[u8; 12], + partner_info: &[Option<(&[u8; 12], u32)>; LINK_COUNT], + capture_ugns: &CaptureUgns, +) -> UgnReport { + let mut report = UgnReport::new(); + for (link_idx, partner) in partner_info.iter().enumerate() { + if let Some((partner_dna, partner_port)) = *partner { + report.count += 1; + let ugn = capture_ugns.local_counter(link_idx).unwrap().into_inner() as i64 + - capture_ugns.remote_counter(link_idx).unwrap().into_inner() as i64; + report.edges[link_idx] = Some(UgnEdge { + src_node: *dna, + src_port: link_idx as u32, + dst_node: *partner_dna, + dst_port: partner_port, + ugn, + }); + } + } + + report +} diff --git a/firmware-binaries/examples/smoltcp_client/Cargo.toml b/firmware-binaries/examples/smoltcp_client/Cargo.toml index 48b8a04653..ddceecbc9d 100644 --- a/firmware-binaries/examples/smoltcp_client/Cargo.toml +++ b/firmware-binaries/examples/smoltcp_client/Cargo.toml @@ -39,7 +39,7 @@ features = ["max_level_trace", "release_max_level_info"] [dependencies.smoltcp] version = "0.12.0" default-features = false -features = ["medium-ethernet", "proto-ipv4", "socket-tcp", "socket-dhcpv4"] +features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp", "socket-dhcpv4"] [build-dependencies] bittide-build-utils = { path = "../../../firmware-support/bittide-build-utils" } diff --git a/firmware-binaries/examples/smoltcp_client/src/main.rs b/firmware-binaries/examples/smoltcp_client/src/main.rs index 6faa503f83..69cb9885bf 100644 --- a/firmware-binaries/examples/smoltcp_client/src/main.rs +++ b/firmware-binaries/examples/smoltcp_client/src/main.rs @@ -11,7 +11,7 @@ use bittide_hal::manual_additions::timer::{Duration, Instant}; use bittide_sys::axi::{AxiRx, AxiTx}; use bittide_sys::mac::MacStatus; use bittide_sys::smoltcp::axi::AxiEthernet; -use bittide_sys::smoltcp::{set_local, set_unicast}; +use bittide_sys::smoltcp::mac::{set_local, set_unicast}; use bittide_sys::uart::log::LOGGER; use log::{debug, info, LevelFilter}; @@ -71,7 +71,7 @@ fn main() -> ! { // Initialize peripherals let timer = INSTANCES.timer; // TODO: Use EthMacStatus instead of MacStatus - let _mac = INSTANCES.mac_status; + let mut _mac = INSTANCES.mac_status; let axi_tx = unsafe { AxiTx::new(TX_AXI_ADDR) }; let axi_rx: AxiRx = unsafe { AxiRx::new(RX_AXI_ADDR) }; @@ -152,7 +152,7 @@ fn main() -> ! { mac_status = unsafe { MAC_ADDR.read_volatile() }; debug!( "Connecting from {:?}:1234 to {SERVER_IP}:{SERVER_PORT}", - my_ip.unwrap().octets(), + my_ip.unwrap().octets() ); match socket.connect(cx, (SERVER_IP, SERVER_PORT), 1234) { Ok(_) => debug!("Connected to {SERVER_IP}:{SERVER_PORT}"), diff --git a/firmware-binaries/sim-tests/ring_buffer_test/src/main.rs b/firmware-binaries/sim-tests/ring_buffer_test/src/main.rs index a0b974b87f..33165d45e0 100644 --- a/firmware-binaries/sim-tests/ring_buffer_test/src/main.rs +++ b/firmware-binaries/sim-tests/ring_buffer_test/src/main.rs @@ -45,8 +45,8 @@ fn verify_loopback(tx: &[[u8; 8]; LEN], rx: &[[u8; 8]; LEN]) -> bool { fn main() -> ! { let mut uart = INSTANCES.uart; let timer = INSTANCES.timer; - let tx = INSTANCES.transmit_ring_buffer; - let rx = INSTANCES.receive_ring_buffer; + let tx = INSTANCES.transmit_ring_buffer_0; + let rx = INSTANCES.receive_ring_buffer_0; let mut all_passed = true; diff --git a/firmware-binaries/sim-tests/tcp_simultaneous_open_test/Cargo.toml b/firmware-binaries/sim-tests/tcp_simultaneous_open_test/Cargo.toml new file mode 100644 index 0000000000..d8b3c8c04c --- /dev/null +++ b/firmware-binaries/sim-tests/tcp_simultaneous_open_test/Cargo.toml @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2026 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "tcp_simultaneous_open_test" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +[dependencies] +riscv-rt = "0.11.0" +riscv = "0.10.1" +bittide-sys = { path = "../../../firmware-support/bittide-sys" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +ufmt = "0.2.0" + +[dependencies.smoltcp] +version = "0.12.0" +default-features = false +features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] + +[dependencies.log] +version = "0.4.21" +features = ["max_level_trace", "release_max_level_info"] + +[dependencies.zerocopy] +version = "0.7" +default-features = false +features = ["byteorder", "derive"] + +[build-dependencies] +bittide-build-utils = { path = "../../../firmware-support/bittide-build-utils" } diff --git a/firmware-binaries/sim-tests/tcp_simultaneous_open_test/build.rs b/firmware-binaries/sim-tests/tcp_simultaneous_open_test/build.rs new file mode 100644 index 0000000000..eee252b733 --- /dev/null +++ b/firmware-binaries/sim-tests/tcp_simultaneous_open_test/build.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use bittide_build_utils::standard_memmap_build; + +fn main() { + standard_memmap_build("RingBufferTest.json", "DataMemory", "InstructionMemory"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/firmware-binaries/sim-tests/tcp_simultaneous_open_test/src/main.rs b/firmware-binaries/sim-tests/tcp_simultaneous_open_test/src/main.rs new file mode 100644 index 0000000000..9422db77a3 --- /dev/null +++ b/firmware-binaries/sim-tests/tcp_simultaneous_open_test/src/main.rs @@ -0,0 +1,344 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +//! Simulation test for TCP simultaneous open over ring_buffer links. +//! +//! A single CPU drives both endpoints of a cross-wired pair of ring_buffers +//! (link0 transmits on the buffer link1 receives from, and vice versa). Both +//! endpoints actively connect to each other at the same time, exercising the +//! TCP simultaneous open path of [`LinkInterface`]. After the connection is +//! established, the test exchanges raw bytes (`try_recv_bytes`) and typed +//! messages (`try_recv`) in both directions, then closes both links +//! gracefully. +//! +//! # Usage +//! +//! This binary is loaded by `simResultTcpOpen` in +//! `Bittide.Instances.Tests.RingBuffer` (`bittide-instances:unittests`), which +//! simulates it and inspects the UART output. On success the test prints +//! `=== Simultaneous Open Success ===`; the simulation reads until +//! `=== Test Complete ===`. +#![no_std] +#![cfg_attr(not(test), no_main)] +#![feature(sync_unsafe_cell)] + +use bittide_hal::manual_additions::ring_buffer::AlignedReceiveBuffer; +use bittide_hal::ring_buffer_test::DeviceInstances; +use bittide_sys::smoltcp::link_interface::{LinkInterface, RecvError}; +use core::fmt::Write; +use log::{error, info, LevelFilter}; +use riscv::register::{mcause, mepc, mtval}; +#[cfg(not(test))] +use riscv_rt::entry; +use smoltcp::iface::SocketStorage; +use ufmt::uwriteln; +use zerocopy::byteorder::{I64, LE, U16, U32}; +use zerocopy::{AsBytes, FromBytes, FromZeroes, Unaligned}; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; + +/// Size of the smoltcp TCP socket buffers (in bytes). Must be large enough to +/// hold the largest message exchanged in this test. +const TCP_BUFFER_SIZE: usize = 512; + +// Test data structures for structured data exchange +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct TestMessage { + pub id: u32, + pub value: i64, + pub flags: u16, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] +struct TestMessageWire { + id: U32, + value: I64, + flags: U16, +} + +impl From for TestMessageWire { + fn from(msg: TestMessage) -> Self { + Self { + id: U32::new(msg.id), + value: I64::new(msg.value), + flags: U16::new(msg.flags), + } + } +} + +impl From for TestMessage { + fn from(msg: TestMessageWire) -> Self { + Self { + id: msg.id.get(), + value: msg.value.get(), + flags: msg.flags.get(), + } + } +} + +#[cfg_attr(not(test), entry)] +fn main() -> ! { + let mut uart = INSTANCES.uart; + let timer = INSTANCES.timer; + + // Set up logging + unsafe { + use bittide_sys::uart::log::LOGGER; + let logger = &mut (*LOGGER.get()); + logger.set_logger(uart.clone()); + logger.set_timer(INSTANCES.timer); + logger.display_source = LevelFilter::Warn; + log::set_logger_racy(logger).ok(); + log::set_max_level_racy(LevelFilter::Trace); + } + + info!("=== LinkInterface Example ==="); + + // Align ring_buffers + let tx_buffer0 = INSTANCES.transmit_ring_buffer_0; + let rx_buffer0 = INSTANCES.receive_ring_buffer_0; + let tx_buffer1 = INSTANCES.transmit_ring_buffer_1; + let rx_buffer1 = INSTANCES.receive_ring_buffer_1; + + let mut rx_aligned0 = AlignedReceiveBuffer::new(rx_buffer0); + let mut rx_aligned1 = AlignedReceiveBuffer::new(rx_buffer1); + while !(rx_aligned0.is_aligned() & rx_aligned1.is_aligned()) { + rx_aligned0.align_step(&tx_buffer0); + rx_aligned1.align_step(&tx_buffer1); + } + + // Create two links with TCP simultaneous open + let mut tcp_rx_buffer0 = [0u8; TCP_BUFFER_SIZE]; + let mut tcp_tx_buffer0 = [0u8; TCP_BUFFER_SIZE]; + let mut socket_storage0 = [SocketStorage::EMPTY; 1]; + let mut link0 = LinkInterface::new( + rx_aligned0, + tx_buffer1, + timer, + &mut tcp_rx_buffer0, + &mut tcp_tx_buffer0, + &mut socket_storage0, + ); + + let mut tcp_rx_buffer1 = [0u8; TCP_BUFFER_SIZE]; + let mut tcp_tx_buffer1 = [0u8; TCP_BUFFER_SIZE]; + let mut socket_storage1 = [SocketStorage::EMPTY; 1]; + let mut link1 = LinkInterface::new( + rx_aligned1, + tx_buffer0, + unsafe { bittide_hal::shared_devices::Timer::new(INSTANCES.timer.0) }, + &mut tcp_rx_buffer1, + &mut tcp_tx_buffer1, + &mut socket_storage1, + ); + + info!("Links created, waiting for connection..."); + info!("Link0 initial state: {:?}", link0.state()); + info!("Link1 initial state: {:?}", link1.state()); + + // Poll until both links are established. All this manual polling (here and + // below) highlights the need to switch to `async` (e.g. `embassy`), see + // https://github.com/bittide/bittide-hardware/issues/1116. + for _ in 0..10000 { + link0.poll(); + link1.poll(); + + if link0.is_established() && link1.is_established() { + info!("Both links established!"); + info!("Link0 state: {:?}", link0.state()); + info!("Link1 state: {:?}", link1.state()); + break; + } + } + + if !link0.is_established() || !link1.is_established() { + error!("Connection timeout!"); + uwriteln!(uart, "[ERROR] === Test Failed ===").unwrap(); + uwriteln!(uart, "=== Test Complete ===").unwrap(); + loop { + continue; + } + } + + // Exchange data - demonstrating the new try_recv_bytes pattern + info!("Exchanging data with try_recv_bytes pattern..."); + + let msg_0_to_1 = b"Hello from Node 0"; + let msg_1_to_0 = b"Hello from Node 1"; + + link0.send(msg_0_to_1); + link1.send(msg_1_to_0); + + // Pattern 1: Using try_recv_bytes with manual polling + let mut node0_received = false; + let mut node1_received = false; + + for _ in 0..5000 { + link0.poll(); + link1.poll(); + + if !node0_received { + let mut buffer = [0u8; 64]; + match link0.try_recv_bytes(&mut buffer) { + Ok(n) if &buffer[..n] == msg_1_to_0 => { + info!("Node 0 received correct data (try_recv_bytes)"); + node0_received = true; + } + Ok(n) => { + info!("Node 0 received {} bytes but data mismatch", n); + } + Err(RecvError::WouldBlock) => { /* No data yet, continue polling */ } + Err(e) => { + info!("Node 0 recv error: {:?}", e); + break; + } + } + } + + if !node1_received { + let mut buffer = [0u8; 64]; + match link1.try_recv_bytes(&mut buffer) { + Ok(n) if &buffer[..n] == msg_0_to_1 => { + info!("Node 1 received correct data (try_recv_bytes)"); + node1_received = true; + } + Ok(n) => { + info!("Node 1 received {} bytes but data mismatch", n); + } + Err(RecvError::WouldBlock) => { /* No data yet, continue polling */ } + Err(e) => { + info!("Node 1 recv error: {:?}", e); + break; + } + } + } + + if node0_received && node1_received { + break; + } + } + + // Exchange structured data using the new typed API + info!("Exchanging structured data with typed try_recv..."); + + let struct_0_to_1 = TestMessage { + id: 42, + value: -123456789, + flags: 0xABCD, + }; + let struct_1_to_0 = TestMessage { + id: 99, + value: 987654321, + flags: 0x1234, + }; + + let wire_0: TestMessageWire = struct_0_to_1.into(); + let wire_1: TestMessageWire = struct_1_to_0.into(); + + link0.send(wire_0.as_bytes()); + link1.send(wire_1.as_bytes()); + + let mut struct0_received = false; + let mut struct1_received = false; + + for _ in 0..5000 { + link0.poll(); + link1.poll(); + + if !struct0_received { + if let Ok(wire_msg) = link0.try_recv::() { + let received: TestMessage = wire_msg.into(); + if received == struct_1_to_0 { + info!( + "Node 0 received correct struct (typed): id={}, value={}, flags={:#x}", + received.id, received.value, received.flags + ); + struct0_received = true; + } else { + info!("Node 0 received struct but data mismatch"); + } + } + } + + if !struct1_received { + if let Ok(wire_msg) = link1.try_recv::() { + let received: TestMessage = wire_msg.into(); + if received == struct_0_to_1 { + info!( + "Node 1 received correct struct (typed): id={}, value={}, flags={:#x}", + received.id, received.value, received.flags + ); + struct1_received = true; + } else { + info!("Node 1 received struct but data mismatch"); + } + } + } + + if struct0_received && struct1_received { + break; + } + } + + // Report results + if node0_received && node1_received && struct0_received && struct1_received { + info!("=== All tests passed! ==="); + uwriteln!(uart, "=== Simultaneous Open Success ===").unwrap(); + } else { + info!("=== Test failed ==="); + uwriteln!(uart, "[ERROR] === Test Failed ===").unwrap(); + } + + // Demonstrate graceful shutdown + info!("Closing connections..."); + link0.close(); + link1.close(); + + for _ in 0..5000 { + link0.poll(); + link1.poll(); + + if link0.is_closing_or_closed() && link1.is_closing_or_closed() { + info!("Both links initiated close handshake"); + info!("Link0 final state: {:?}", link0.state()); + info!("Link1 final state: {:?}", link1.state()); + + if link0.is_closed() && link1.is_closed() { + info!("Both links reached Closed state"); + } + break; + } + } + + if !link0.is_closing_or_closed() || !link1.is_closing_or_closed() { + info!("Warning: Links did not initiate close"); + } + + uwriteln!(uart, "=== Test Complete ===").unwrap(); + loop { + continue; + } +} + +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + let mut uart = INSTANCES.uart; + writeln!(uart, "PANIC: {}", info).ok(); + loop {} +} + +#[export_name = "ExceptionHandler"] +fn exception_handler(_trap_frame: &riscv_rt::TrapFrame) -> ! { + let mut uart = INSTANCES.uart; + riscv::interrupt::free(|| { + uwriteln!(uart, "... caught an exception. Looping forever now.\n").unwrap(); + writeln!(uart, "mcause: {:?}\n", mcause::read()).unwrap(); + writeln!(uart, "mepc: {:?}\n", mepc::read()).unwrap(); + writeln!(uart, "mtval: {:?}\n", mtval::read()).unwrap(); + }); + loop { + continue; + } +} diff --git a/firmware-support/Cargo.lock b/firmware-support/Cargo.lock index 67788cb014..334b517f13 100644 --- a/firmware-support/Cargo.lock +++ b/firmware-support/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ "memorymap-compiler-rust", "proc-macro2", "quote", - "smoltcp 0.11.0", + "smoltcp", "subst_macros", "ufmt", ] @@ -117,6 +117,7 @@ dependencies = [ "bittide-macros", "clash-bindings", "clash-macros", + "crc", "fdt", "heapless", "itertools", @@ -126,10 +127,11 @@ dependencies = [ "object", "proptest", "rand 0.8.5", - "smoltcp 0.12.0", + "smoltcp", "tempfile", "test-strategy", "ufmt", + "zerocopy 0.7.35", ] [[package]] @@ -185,6 +187,21 @@ dependencies = [ "typewit", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -507,7 +524,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.30", ] [[package]] @@ -764,19 +781,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smoltcp" -version = "0.11.0" -source = "git+https://github.com/smoltcp-rs/smoltcp.git?rev=dc08e0b42e668c331bb2b6f8d80016301d0efe03#dc08e0b42e668c331bb2b6f8d80016301d0efe03" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "cfg-if", - "heapless", - "log", - "managed", -] - [[package]] name = "smoltcp" version = "0.12.0" @@ -1007,13 +1011,34 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + [[package]] name = "zerocopy" version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.30", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] diff --git a/firmware-support/bittide-cpus/src/clock_control.rs b/firmware-support/bittide-cpus/src/clock_control.rs index e641632f02..362ad7b049 100644 --- a/firmware-support/bittide-cpus/src/clock_control.rs +++ b/firmware-support/bittide-cpus/src/clock_control.rs @@ -49,6 +49,18 @@ impl DomainDiffCountersInterface } } +impl DomainDiffCountersInterface + for bittide_hal::hals::async_comms_demo_clock_control::devices::DomainDiffCounters +{ + const ENABLE_LEN: usize = Self::ENABLE_LEN; + fn enable(&self, idx: usize) -> Option { + Self::enable(self, idx) + } + fn set_enable(&self, idx: usize, val: bool) -> Option<()> { + Self::set_enable(self, idx, val) + } +} + pub fn run( cc: ClockControl, timer: Timer, diff --git a/firmware-support/bittide-hal/Cargo.toml b/firmware-support/bittide-hal/Cargo.toml index 562a39c44f..64c4321274 100644 --- a/firmware-support/bittide-hal/Cargo.toml +++ b/firmware-support/bittide-hal/Cargo.toml @@ -22,10 +22,9 @@ clash-bindings = { git = "https://github.com/QBayLogic/clash-protocols-memmap", clash-macros = { git = "https://github.com/QBayLogic/clash-protocols-memmap", rev = "745be2eff82fd8d712b0e50d825837d2f39fb920" } [dependencies.smoltcp] -git = "https://github.com/smoltcp-rs/smoltcp.git" -rev = "dc08e0b42e668c331bb2b6f8d80016301d0efe03" +version = "0.12.0" default-features = false -features = ["log", "medium-ethernet", "proto-ipv4", "socket-tcp"] +features = ["log", "medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dependencies.subst_macros] git = "https://github.com/QBayLogic/subst_macros.git" diff --git a/firmware-support/bittide-hal/src/manual_additions/ring_buffer.rs b/firmware-support/bittide-hal/src/manual_additions/ring_buffer.rs index 1515ec76ff..b46d8c09c4 100644 --- a/firmware-support/bittide-hal/src/manual_additions/ring_buffer.rs +++ b/firmware-support/bittide-hal/src/manual_additions/ring_buffer.rs @@ -209,6 +209,12 @@ impl_ring_buffer_interfaces! { cidx: Index<4000, u16>, } +impl_ring_buffer_interfaces! { + rx: crate::hals::async_comms_demo_management_unit::devices::ReceiveRingBuffer, + tx: crate::hals::async_comms_demo_management_unit::devices::TransmitRingBuffer, + cidx: Index<128, u8>, +} + #[derive(Debug, PartialEq, Eq)] enum AlignPhase { Unaligned, diff --git a/firmware-support/bittide-sys/Cargo.toml b/firmware-support/bittide-sys/Cargo.toml index 7e51444eb0..2826d0aac1 100644 --- a/firmware-support/bittide-sys/Cargo.toml +++ b/firmware-support/bittide-sys/Cargo.toml @@ -23,10 +23,12 @@ clash-bindings = { git = "https://github.com/QBayLogic/clash-protocols-memmap", clash-macros = { git = "https://github.com/QBayLogic/clash-protocols-memmap", rev = "745be2eff82fd8d712b0e50d825837d2f39fb920" } bittide-hal = { path = "../bittide-hal" } bittide-macros = { path = "../bittide-macros" } +crc = { version = "3.0", default-features = false } fdt = "0.1.0" itertools = { version = "0.14.0", default-features = false } log = "0.4.21" ufmt = "0.2.0" +zerocopy = { version = "0.7", default-features = false, features = ["byteorder", "derive"] } [dependencies.heapless] version = "0.8" @@ -40,7 +42,7 @@ default-features = false [dependencies.smoltcp] version = "0.12.0" default-features = false -features = ["log", "medium-ethernet", "proto-ipv4", "socket-tcp"] +features = ["log", "medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dev-dependencies] lazy_static = "1.0" diff --git a/firmware-support/bittide-sys/src/hitl.rs b/firmware-support/bittide-sys/src/hitl.rs new file mode 100644 index 0000000000..1a9f6c4069 --- /dev/null +++ b/firmware-support/bittide-sys/src/hitl.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +pub const FPGA_COUNT: usize = 8; +pub const LINK_COUNT: usize = FPGA_COUNT - 1; + +/// DNA values of the FPGAs in the demo rig, in order, as read from the DNA +/// port (i.e. the little-endian byte representation of the `dna` fields of +/// `demoRigInfo` in `Bittide.Instances.Hitl.Setup`). +pub const FPGA_DNAS: [[u8; 12]; 8] = [ + [ + 0xc5, 0x64, 0x41, 0x04, 0x40, 0xc0, 0x69, 0x01, 0x01, 0x00, 0x02, 0x40, + ], // 210308B3B272 + [ + 0x85, 0x82, 0x10, 0x05, 0xe8, 0x60, 0x51, 0x81, 0x01, 0x00, 0x02, 0x40, + ], // 210308B0992E + [ + 0x45, 0x84, 0x80, 0x2c, 0xe7, 0x5c, 0x69, 0x01, 0x01, 0x00, 0x02, 0x40, + ], // 210308B0AE73 + [ + 0x05, 0x23, 0x70, 0x2c, 0xe7, 0x5c, 0x69, 0x01, 0x01, 0x00, 0x02, 0x40, + ], // 210308B0AE6D + [ + 0x85, 0xa2, 0x81, 0x25, 0xe5, 0xa8, 0x6b, 0x01, 0x01, 0x00, 0x02, 0x40, + ], // 210308B0AFD4 + [ + 0x45, 0xc3, 0x01, 0x2d, 0x86, 0xf4, 0x57, 0x01, 0x01, 0x00, 0x02, 0x40, + ], // 210308B0AE65 + [ + 0x85, 0x81, 0x30, 0x04, 0x40, 0xc0, 0x69, 0x01, 0x01, 0x00, 0x02, 0x40, + ], // 210308B3A22D + [ + 0x05, 0xe4, 0x20, 0x2d, 0x86, 0x64, 0x56, 0x01, 0x01, 0x00, 0x02, 0x40, + ], // 210308B0B0C2 +]; + +/// For each FPGA in the demo rig (rig order), the rig index of the FPGA on the +/// other end of each of its links. Mirrors `fpgaSetup` in +/// `Bittide.Instances.Hitl.Setup`. +pub const LINK_NEIGHBORS: [[usize; LINK_COUNT]; 8] = [ + [3, 2, 4, 5, 6, 7, 1], + [2, 3, 5, 6, 7, 4, 0], + [1, 0, 6, 7, 4, 5, 3], + [0, 1, 7, 4, 5, 6, 2], + [7, 6, 0, 3, 2, 1, 5], + [6, 7, 1, 0, 3, 2, 4], + [5, 4, 2, 1, 0, 3, 7], + [4, 5, 3, 2, 1, 0, 6], +]; diff --git a/firmware-support/bittide-sys/src/lib.rs b/firmware-support/bittide-sys/src/lib.rs index 4f79a736fc..04fdadd692 100644 --- a/firmware-support/bittide-sys/src/lib.rs +++ b/firmware-support/bittide-sys/src/lib.rs @@ -7,8 +7,10 @@ pub mod axi; pub mod callisto; +pub mod hitl; pub mod link_startup; pub mod mac; +pub mod net_state; pub mod sample_store; pub mod smoltcp; pub mod stability_detector; diff --git a/firmware-support/bittide-sys/src/link_startup.rs b/firmware-support/bittide-sys/src/link_startup.rs index 0f0a8424dd..bddf5cef3b 100644 --- a/firmware-support/bittide-sys/src/link_startup.rs +++ b/firmware-support/bittide-sys/src/link_startup.rs @@ -4,6 +4,7 @@ use bittide_hal::shared_devices::{ElasticBuffer, Handshakes, Transceivers}; use bittide_hal::types::mode::Mode; +use log::debug; #[derive(Debug, Copy, Clone, PartialEq)] pub enum LinkStartupState { @@ -39,6 +40,7 @@ impl LinkStartup { channel: usize, elastic_buffer: &ElasticBuffer, ) { + let old_state = self.state; self.state = match self.state { LinkStartupState::WaitForRxInitDone => { if transceivers @@ -122,6 +124,12 @@ impl LinkStartup { } LinkStartupState::Done => self.state, }; + if self.state != old_state { + debug!( + "link_startup[{}]: {:?} -> {:?}", + channel, old_state, self.state + ); + } } pub fn is_done(&self) -> bool { diff --git a/firmware-support/bittide-sys/src/net_state.rs b/firmware-support/bittide-sys/src/net_state.rs new file mode 100644 index 0000000000..a029b1141c --- /dev/null +++ b/firmware-support/bittide-sys/src/net_state.rs @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use log::warn; +use zerocopy::byteorder::{I64, LE, U32}; +use zerocopy::{AsBytes, FromBytes, FromZeroes, Unaligned}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NodeRole { + Manager, + Subordinate, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ManagerState { + WaitForSession, + Identifying, + ReceivingUgns, + Done, + Failed, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SubordinateState { + WaitForSession, + Identifying, + SendingUgns, + Done, + Failed, +} + +pub const MAX_UGN_EDGES: usize = 64; +pub const UGN_EDGE_BYTES: usize = core::mem::size_of::(); + +/// Number of bytes in an FPGA DNA identifier. +pub const N_DNA_BYTES: usize = 12; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct UgnEdge { + pub src_node: [u8; N_DNA_BYTES], + pub src_port: u32, + pub dst_node: [u8; N_DNA_BYTES], + pub dst_port: u32, + pub ugn: i64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UgnReport { + pub count: u32, + pub edges: [Option; MAX_UGN_EDGES], +} +impl Default for UgnReport { + fn default() -> Self { + Self::new() + } +} +impl UgnReport { + pub fn new() -> Self { + Self { + count: 0, + edges: [None; MAX_UGN_EDGES], + } + } + pub fn insert_edge(&mut self, edge: UgnEdge) -> bool { + if self.count >= MAX_UGN_EDGES as u32 { + warn!("report is full, cannot insert edge"); + return false; + }; + self.edges[self.count as usize] = Some(edge); + self.count = self.count.saturating_add(1); + true + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] +struct UgnEdgeWire { + src_node: [u8; N_DNA_BYTES], + dst_node: [u8; N_DNA_BYTES], + src_port: U32, + dst_port: U32, + ugn: I64, +} + +impl From for UgnEdgeWire { + fn from(edge: UgnEdge) -> Self { + Self { + src_node: edge.src_node, + dst_node: edge.dst_node, + src_port: U32::new(edge.src_port), + dst_port: U32::new(edge.dst_port), + ugn: I64::new(edge.ugn), + } + } +} + +impl From for UgnEdge { + fn from(edge: UgnEdgeWire) -> Self { + Self { + src_node: edge.src_node, + dst_node: edge.dst_node, + src_port: edge.src_port.get(), + dst_port: edge.dst_port.get(), + ugn: edge.ugn.get(), + } + } +} diff --git a/firmware-support/bittide-sys/src/smoltcp/link_interface.rs b/firmware-support/bittide-sys/src/smoltcp/link_interface.rs new file mode 100644 index 0000000000..3f905ec452 --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/link_interface.rs @@ -0,0 +1,329 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use bittide_hal::manual_additions::ring_buffer::{ + AlignedReceiveBuffer, ReceiveRingBufferInterface, TransmitRingBufferInterface, +}; +use bittide_hal::manual_additions::timer::{Duration, Instant}; +use smoltcp::iface::{Config, Interface, SocketSet, SocketStorage}; +use smoltcp::socket::tcp; +use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr, IpEndpoint}; +use zerocopy::{AsBytes, FromBytes, FromZeroes}; + +use log::debug; + +use crate::smoltcp::ring_buffer::RingBufferDevice; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RecvError { + WouldBlock, + NotEstablished, + Closed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SendError { + WouldBlock, + NotEstablished, + Closed, +} + +// Each link is a point-to-point connection with its own interface and socket, so no +// routing takes place and the addresses don't matter: we only use smoltcp's TCP state +// machine. Both endpoints of every link therefore use this same IP and port, which +// also makes the connection a TCP simultaneous open. +const LINK_IP: IpAddress = IpAddress::v4(100, 100, 100, 100); +const LINK_PORT: u16 = 8080; + +/// TCP communication over a pair of ring_buffers. +/// +/// The TCP buffers are borrowed from the caller for `'a`, so the struct +/// contains no self-references and can move freely. Constructing it +/// initializes smoltcp and starts TCP simultaneous open. +pub struct LinkInterface<'a, RxRb, TxRb> { + device: RingBufferDevice, + timer: bittide_hal::shared_devices::Timer, + iface: Interface, + sockets: SocketSet<'a>, + socket_handle: smoltcp::iface::SocketHandle, + last_state: tcp::State, +} + +impl<'a, RxRb, TxRb> LinkInterface<'a, RxRb, TxRb> +where + RxRb: ReceiveRingBufferInterface + 'static, + TxRb: TransmitRingBufferInterface + 'static, +{ + /// Initialize smoltcp and start TCP simultaneous open. + pub fn new( + rx_aligned: AlignedReceiveBuffer, + tx_buffer: TxRb, + timer: bittide_hal::shared_devices::Timer, + tcp_rx_buffer: &'a mut [u8], + tcp_tx_buffer: &'a mut [u8], + socket_storage: &'a mut [SocketStorage<'a>], + ) -> Self { + let mut device = RingBufferDevice::new(rx_aligned, tx_buffer); + + let config = Config::new(HardwareAddress::Ip); + let now = to_smoltcp_instant(timer.now()); + let mut iface = Interface::new(config, &mut device, now); + + iface.update_ip_addrs(|addrs| { + addrs.push(IpCidr::new(LINK_IP, 24)).unwrap(); + }); + + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(tcp_rx_buffer), + tcp::SocketBuffer::new(tcp_tx_buffer), + ); + + let mut sockets = SocketSet::new(socket_storage); + let handle = sockets.add(socket); + + let remote = IpEndpoint::new(LINK_IP, LINK_PORT); + sockets + .get_mut::(handle) + .connect(iface.context(), remote, LINK_PORT) + .unwrap(); + + let last_state = sockets.get::(handle).state(); + debug!("LinkInterface connected, initial state: {last_state:?}"); + Self { + device, + timer, + iface, + sockets, + socket_handle: handle, + last_state, + } + } + + fn socket(&self) -> &tcp::Socket<'a> { + self.sockets.get::(self.socket_handle) + } + + fn socket_mut(&mut self) -> &mut tcp::Socket<'a> { + self.sockets.get_mut::(self.socket_handle) + } + + pub fn poll(&mut self) { + let timestamp = to_smoltcp_instant(self.timer.now()); + self.iface + .poll(timestamp, &mut self.device, &mut self.sockets); + + let new_state = self.socket().state(); + if new_state != self.last_state { + debug!("TCP state: {:?} -> {:?}", self.last_state, new_state); + self.last_state = new_state; + } + } + + pub fn is_established(&self) -> bool { + self.socket().state() == tcp::State::Established + } + + pub fn is_open(&self) -> bool { + self.socket().is_open() + } + + pub fn is_active(&self) -> bool { + self.socket().is_active() + } + + pub fn is_closed(&self) -> bool { + self.socket().state() == tcp::State::Closed + } + + pub fn is_closing_or_closed(&self) -> bool { + !matches!( + self.socket().state(), + tcp::State::Established | tcp::State::SynSent | tcp::State::SynReceived + ) + } + + pub fn state(&self) -> tcp::State { + self.socket().state() + } + + pub fn send(&mut self, data: &[u8]) -> usize { + let socket = self.socket_mut(); + if socket.can_send() { + socket.send_slice(data).unwrap_or(0) + } else { + 0 + } + } + + pub fn try_send_bytes(&mut self, data: &[u8]) -> Result { + let socket = self.socket_mut(); + if socket.state() == tcp::State::Closed { + return Err(SendError::Closed); + } + if !socket.is_active() { + return Err(SendError::NotEstablished); + } + if socket.can_send() { + let n = socket.send_slice(data).unwrap_or(0); + if n > 0 { + Ok(n) + } else { + Err(SendError::WouldBlock) + } + } else { + Err(SendError::WouldBlock) + } + } + + pub fn send_blocking_bytes( + &mut self, + data: &[u8], + timeout: Duration, + ) -> Result { + let deadline = self.timer.now() + timeout; + loop { + self.poll(); + match self.try_send_bytes(data) { + Ok(n) => return Ok(n), + Err(SendError::WouldBlock) if self.timer.now() < deadline => continue, + Err(SendError::WouldBlock) => return Err(SendError::WouldBlock), + Err(e) => return Err(e), + } + } + } + + pub fn try_send(&mut self, value: &T) -> Result<(), SendError> { + let bytes = value.as_bytes(); + { + let socket = self.socket(); + if socket.state() == tcp::State::Closed { + return Err(SendError::Closed); + } + if !socket.is_active() { + return Err(SendError::NotEstablished); + } + // Only enqueue if the whole value fits; a partial send followed by + // a retry would duplicate the already-sent prefix in the TCP stream. + if socket.send_capacity() < bytes.len() { + return Err(SendError::WouldBlock); + } + } + self.socket_mut().send_slice(bytes).unwrap_or(0); + Ok(()) + } + + pub fn send_blocking( + &mut self, + value: &T, + timeout: Duration, + ) -> Result<(), SendError> { + let deadline = self.timer.now() + timeout; + loop { + self.poll(); + match self.try_send(value) { + Ok(()) => return Ok(()), + Err(SendError::WouldBlock) if self.timer.now() < deadline => continue, + Err(SendError::WouldBlock) => return Err(SendError::WouldBlock), + Err(e) => return Err(e), + } + } + } + + pub fn recv(&mut self, buffer: &mut [u8]) -> usize { + let socket = self.socket_mut(); + if socket.can_recv() { + socket.recv_slice(buffer).unwrap_or(0) + } else { + 0 + } + } + + pub fn try_recv_bytes(&mut self, buffer: &mut [u8]) -> Result { + let socket = self.socket_mut(); + if socket.state() == tcp::State::Closed { + return Err(RecvError::Closed); + } + if !socket.is_active() { + return Err(RecvError::NotEstablished); + } + if socket.can_recv() { + let n = socket.recv_slice(buffer).unwrap_or(0); + if n > 0 { + Ok(n) + } else { + Err(RecvError::WouldBlock) + } + } else { + Err(RecvError::WouldBlock) + } + } + + pub fn recv_blocking_bytes( + &mut self, + buffer: &mut [u8], + timeout: Duration, + ) -> Result { + let deadline = self.timer.now() + timeout; + loop { + self.poll(); + match self.try_recv_bytes(buffer) { + Ok(n) => return Ok(n), + Err(RecvError::WouldBlock) if self.timer.now() < deadline => continue, + Err(RecvError::WouldBlock) => return Err(RecvError::WouldBlock), + Err(e) => return Err(e), + } + } + } + + pub fn try_recv(&mut self) -> Result + where + T: FromBytes + FromZeroes + AsBytes, + { + let size = core::mem::size_of::(); + { + let socket = self.socket(); + if socket.state() == tcp::State::Closed { + return Err(RecvError::Closed); + } + if !socket.is_active() { + return Err(RecvError::NotEstablished); + } + // Only consume bytes once a complete T has arrived; a partial read + // would permanently corrupt the TCP stream on retry. + if socket.recv_queue() < size { + return Err(RecvError::WouldBlock); + } + } + let mut buffer = T::new_zeroed(); + self.socket_mut() + .recv_slice(buffer.as_bytes_mut()) + .unwrap_or(0); + Ok(buffer) + } + + pub fn recv_blocking(&mut self, timeout: Duration) -> Result + where + T: FromBytes + FromZeroes + AsBytes, + { + let deadline = self.timer.now() + timeout; + loop { + self.poll(); + match self.try_recv::() { + Ok(value) => return Ok(value), + Err(RecvError::WouldBlock) if self.timer.now() < deadline => continue, + Err(RecvError::WouldBlock) => return Err(RecvError::WouldBlock), + Err(e) => return Err(e), + } + } + } + + pub fn close(&mut self) { + debug!("closing TCP connection (state: {:?})", self.state()); + self.socket_mut().close(); + } +} + +pub fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { + smoltcp::time::Instant::from_micros(instant.micros() as i64) +} diff --git a/firmware-support/bittide-sys/src/smoltcp/link_protocol.rs b/firmware-support/bittide-sys/src/smoltcp/link_protocol.rs new file mode 100644 index 0000000000..e63ac3f75a --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/link_protocol.rs @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +//! Wire Format Types for Link Protocol +//! +//! This module provides wire format types for communication between nodes. +//! Use these with LinkInterface's zerocopy send/recv methods. + +use crate::net_state::UgnEdge; +use zerocopy::byteorder::{I64, LE, U32}; +use zerocopy::{AsBytes, FromBytes, FromZeroes, Unaligned}; + +pub use crate::net_state::N_DNA_BYTES; + +/// Command types that can be sent between nodes +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Command { + RequestUgnReport, + // Future commands can be added here +} + +/// Wire format for commands +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned, PartialEq, Eq)] +pub struct CommandWire { + cmd_type: u8, + _padding: [u8; 3], +} + +impl From for CommandWire { + fn from(cmd: Command) -> Self { + let cmd_type = match cmd { + Command::RequestUgnReport => 1, + }; + Self { + cmd_type, + _padding: [0; 3], + } + } +} + +impl TryFrom for Command { + type Error = &'static str; + + fn try_from(wire: CommandWire) -> Result { + match wire.cmd_type { + 1 => Ok(Command::RequestUgnReport), + _ => Err("Unknown command type"), + } + } +} + +/// Wire format for UGN edges +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] +pub struct UgnEdgeWire { + pub src_node: [u8; N_DNA_BYTES], + pub dst_node: [u8; N_DNA_BYTES], + pub src_port: U32, + pub dst_port: U32, + pub ugn: I64, +} + +impl From for UgnEdgeWire { + fn from(edge: UgnEdge) -> Self { + Self { + src_node: edge.src_node, + dst_node: edge.dst_node, + src_port: U32::new(edge.src_port), + dst_port: U32::new(edge.dst_port), + ugn: I64::new(edge.ugn), + } + } +} + +impl From for UgnEdge { + fn from(edge: UgnEdgeWire) -> Self { + Self { + src_node: edge.src_node, + dst_node: edge.dst_node, + src_port: edge.src_port.get(), + dst_port: edge.dst_port.get(), + ugn: edge.ugn.get(), + } + } +} diff --git a/firmware-support/bittide-sys/src/smoltcp.rs b/firmware-support/bittide-sys/src/smoltcp/mac.rs similarity index 90% rename from firmware-support/bittide-sys/src/smoltcp.rs rename to firmware-support/bittide-sys/src/smoltcp/mac.rs index 6eaa828e62..76fcf7e535 100644 --- a/firmware-support/bittide-sys/src/smoltcp.rs +++ b/firmware-support/bittide-sys/src/smoltcp/mac.rs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2024 Google LLC // // SPDX-License-Identifier: Apache-2.0 -pub mod axi; use smoltcp::wire::EthernetAddress; @@ -9,7 +8,7 @@ use smoltcp::wire::EthernetAddress; /// This is the most significant bit of the first byte. /// ``` /// use smoltcp::wire::EthernetAddress; -/// use bittide_sys::smoltcp::set_unicast; +/// use bittide_sys::smoltcp::mac::set_unicast; /// let mut mac = EthernetAddress::from_bytes(&[0xFF; 6]); /// assert!(!mac.is_unicast()); /// set_unicast(&mut mac); @@ -23,7 +22,7 @@ pub fn set_unicast(addr: &mut EthernetAddress) { /// This is the second least significant bit of the first byte. /// ``` /// use smoltcp::wire::EthernetAddress; -/// use bittide_sys::smoltcp::set_local; +/// use bittide_sys::smoltcp::mac::set_local; /// let mut mac = EthernetAddress::from_bytes(&[0; 6]); /// assert!(!mac.is_local()); /// set_local(&mut mac); diff --git a/firmware-support/bittide-sys/src/smoltcp/mod.rs b/firmware-support/bittide-sys/src/smoltcp/mod.rs new file mode 100644 index 0000000000..3b34bcf519 --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/mod.rs @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +pub mod axi; +pub mod link_interface; +pub mod link_protocol; +pub mod mac; +pub mod ring_buffer; diff --git a/firmware-support/bittide-sys/src/smoltcp/ring_buffer.rs b/firmware-support/bittide-sys/src/smoltcp/ring_buffer.rs new file mode 100644 index 0000000000..477e565223 --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/ring_buffer.rs @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +//! smoltcp Device implementation for aligned ring_buffers. +//! +//! This module provides a `Device` implementation that uses scatter/gather units +//! as ring_buffers for point-to-point communication. The ring_buffers must +//! be aligned using the alignment protocol before use. +//! +//! The device uses IP medium for point-to-point links. +//! +//! # Packet Format +//! +//! Each packet consists of: +//! - Header (8 bytes): CRC32 (4 bytes LE) + sequence number (2 bytes LE) + length (2 bytes LE) +//! - Payload (variable, up to MTU) +//! +//! The CRC32 is calculated over sequence + length + payload (i.e., everything except the CRC itself). +//! +//! # Volatile Buffer Handling +//! +//! The receive ring_buffer is volatile - its contents can change at any time due to +//! incoming data. To safely handle this, we: +//! 1. Disable writes to the buffer so incoming frames can't change its contents while +//! we process them. The ring buffer itself keeps spinning (its internal counter keeps +//! incrementing to maintain alignment); only writes to the buffer memory are suppressed. +//! 2. Read packet header directly from hardware buffer to get CRC, sequence number, and length +//! 3. Create a slice pointing directly to the hardware buffer +//! 4. Verify the CRC32 over sequence + length + payload +//! 5. If CRC validates, extract payload and consume packet +//! 6. Track sequence numbers to detect repeated packets +use bittide_hal::manual_additions::ring_buffer::{ + AlignedReceiveBuffer, ReceiveRingBufferInterface, TransmitRingBufferInterface, +}; + +use crc::{Crc, CRC_32_ISCSI}; +use log::{debug, trace, warn}; +use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; +use smoltcp::time::Instant; + +/// Size of packet header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) +const PACKET_HEADER_SIZE: usize = 8; + +/// Minimum IP packet size (20 byte IPv4 header minimum) +const MIN_IP_PACKET_SIZE: usize = 20; + +/// Upper bound on the MTU, matching the standard Ethernet MTU. Keeps packets a +/// sane size even when the ring_buffers are large. +const MAX_MTU: usize = 1500; + +/// CRC-32 (Castagnoli) instance for packet integrity checking. +/// Initialized at compile-time for zero-cost runtime usage. +const CRC: Crc = Crc::::new(&CRC_32_ISCSI); + +/// Device implementation for ring_buffer communication. +/// +/// Provides a simple interface for point-to-point IP communication using +/// scatter/gather units. The ring_buffers handle all internal state management. +/// +/// The MTU is automatically calculated from the minimum of the scatter and gather +/// buffer sizes (in bytes), minus space for packet header (which includes CRC32). +pub struct RingBufferDevice { + rx_buffer: AlignedReceiveBuffer, + tx_buffer: Tx, + mtu: usize, + /// Last received CRC we saw (to detect repeated packets) + last_crc: u32, + /// Transmit sequence number (incremented for each packet sent) + tx_seq_num: u16, +} + +impl RingBufferDevice +where + Rx: ReceiveRingBufferInterface + 'static, + Tx: TransmitRingBufferInterface + 'static, +{ + /// Create a new ring_buffer device. + /// + /// The ring_buffers must already be aligned using the alignment protocol. + /// The MTU is calculated as the minimum of the RX and TX buffer sizes in bytes. + pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: Tx) -> Self { + // Calculate MTU from buffer sizes (each word is 8 bytes) + // Reserve space for packet header (CRC is part of header) + let mtu = (Rx::DATA_LEN * 8).min(MAX_MTU).min(Tx::DATA_LEN * 8) - PACKET_HEADER_SIZE; + assert!(rx_buffer.is_aligned(), "RX buffer is not aligned "); + // The alignment procedure leaves the RX buffer disabled; enable it here. + // From this point on the device owns the enable bit: `receive` expects the + // buffer to be enabled between calls and panics otherwise. + rx_buffer.buffer.set_enable(true); + debug!( + "ring_buffer device init rx_words {} tx_words {} mtu {}", + Rx::DATA_LEN, + Tx::DATA_LEN, + mtu + ); + + Self { + rx_buffer, + tx_buffer, + mtu, + last_crc: 0, + tx_seq_num: 0, + } + } + + /// Get the maximum transmission unit (in bytes) for this device. + pub fn mtu(&self) -> usize { + self.mtu + } +} + +impl Device for RingBufferDevice +where + Rx: ReceiveRingBufferInterface + 'static, + Tx: TransmitRingBufferInterface + 'static, +{ + type RxToken<'a> + = RxToken<'a, Rx> + where + Self: 'a; + type TxToken<'a> = TxToken<'a, Tx>; + + fn capabilities(&self) -> DeviceCapabilities { + let mut cap = DeviceCapabilities::default(); + cap.max_transmission_unit = self.mtu; + cap.medium = Medium::Ip; + cap + } + + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + // The buffer should always be enabled between calls to receive; finding it disabled + // means a previous call failed to restore it, which would silently drop all traffic. + if !(self.rx_buffer.buffer.get_enable()) { + panic!("RX buffer is disabled at start of receive - this should not happen."); + } + // Disable writes to the RX buffer so incoming frames can't change its contents while + // we process them. The ring buffer itself keeps spinning to maintain alignment. + self.rx_buffer.buffer.set_enable(false); + + // Make sure loads and stores do not cross this boundary to ensure correct order of + // hardware accesses. + core::sync::atomic::fence(core::sync::atomic::Ordering::AcqRel); + + // Get direct pointer to hardware buffer (base pointer points to array of [u8; 8]) + let hw_buffer_ptr = self.rx_buffer.buffer.base_ptr() as *const u8; + + unsafe { + // Read header fields directly from hardware buffer + // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + let crc = u32::from_le((hw_buffer_ptr as *const u32).read_volatile()); + let seq_num = u16::from_le((hw_buffer_ptr.add(4) as *const u16).read_volatile()); + let packet_len = + u16::from_le((hw_buffer_ptr.add(6) as *const u16).read_volatile()) as usize; + + // Make sure loads and stores do not cross this boundary to ensure correct order of + // hardware accesses before returning. + core::sync::atomic::fence(core::sync::atomic::Ordering::AcqRel); + // Check if this is the same packet we saw before (based on CRC) + if crc == self.last_crc { + debug!("Detected repeated packet with CRC {crc}"); + self.rx_buffer.buffer.set_enable(true); + return None; + } + + // Validate packet length + if packet_len < MIN_IP_PACKET_SIZE || packet_len > self.mtu { + warn!( + "Invalid packet length: {packet_len} (must be {MIN_IP_PACKET_SIZE}-{})", + self.mtu + ); + self.rx_buffer.buffer.set_enable(true); + return None; + } + + // Calculate total packet size: header + payload + let total_len = PACKET_HEADER_SIZE + packet_len; + debug!("rx packet seq {seq_num} len {packet_len} total {total_len}"); + + // Create slice directly from hardware buffer for CRC validation. Writes to the + // buffer are disabled at this point, so its contents are stable and plain + // (non-volatile) reads are fine. + let packet_bytes = core::slice::from_raw_parts(hw_buffer_ptr, total_len); + + // Validate CRC + let calculated_crc = CRC.checksum(&packet_bytes[4..]); + + // Make sure loads and stores do not cross this boundary to ensure correct order of + // hardware accesses before returning. + core::sync::atomic::fence(core::sync::atomic::Ordering::AcqRel); + if calculated_crc != crc { + debug!("CRC validation failed for packet seq {seq_num}"); + self.rx_buffer.buffer.set_enable(true); + return None; + } + + trace!("Valid packet: seq {seq_num}, payload {packet_len} bytes"); + self.last_crc = crc; + + // Create token that will process directly from hardware buffer + let rx = RxToken { + rx_buffer: &self.rx_buffer.buffer, + packet_len, + }; + trace!("rx token ready len {packet_len}"); + let tx = TxToken { + tx_buffer: &mut self.tx_buffer, + mtu: self.mtu, + seq_num: &mut self.tx_seq_num, + }; + Some((rx, tx)) + } + } + + fn transmit(&mut self, _timestamp: Instant) -> Option> { + self.tx_buffer.set_enable(true); + Some(TxToken { + tx_buffer: &mut self.tx_buffer, + mtu: self.mtu, + seq_num: &mut self.tx_seq_num, + }) + } +} + +/// Receive token for ring_buffer device. +/// +/// Holds reference to RX buffer to read packet directly from hardware. +/// Re-enables RX buffer after consumption. +pub struct RxToken<'a, Rx: ReceiveRingBufferInterface> { + rx_buffer: &'a Rx, + packet_len: usize, +} + +impl phy::RxToken for RxToken<'_, Rx> { + fn consume(self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + trace!( + "Consuming packet directly from hardware buffer ({} bytes)", + self.packet_len + ); + + let result = unsafe { + // Create slice pointing to payload in hardware buffer (skip header) + let payload_ptr = (self.rx_buffer.base_ptr() as *const u8).add(PACKET_HEADER_SIZE); + let payload_slice = core::slice::from_raw_parts(payload_ptr, self.packet_len); + + // Let smoltcp process the packet + let result = f(payload_slice); + + // Memory fence: Ensure all reads from the buffer complete before re-enabling + core::sync::atomic::fence(core::sync::atomic::Ordering::AcqRel); + result + }; + + // Re-enable the RX buffer so it can advance to the next packet + self.rx_buffer.set_enable(true); + trace!("RX buffer re-enabled after packet consumption"); + result + } +} + +/// Transmit token for ring_buffer device +pub struct TxToken<'a, Tx> +where + Tx: TransmitRingBufferInterface, +{ + tx_buffer: &'a mut Tx, + mtu: usize, + seq_num: &'a mut u16, +} + +impl phy::TxToken for TxToken<'_, Tx> +where + Tx: TransmitRingBufferInterface, +{ + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + assert!( + len <= self.mtu, + "Packet length {} exceeds MTU {}", + len, + self.mtu + ); + trace!("tx consume len {} mtu {}", len, self.mtu); + + // Calculate total packet length + let total_len = PACKET_HEADER_SIZE + len; + + // Get direct pointer to hardware buffer (zero-copy approach) + // This points directly to the memory-mapped TX ring_buffer + let hw_buffer_ptr = self.tx_buffer.base_ptr() as *mut u8; + + unsafe { + // Write header fields directly to hardware buffer + // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + trace!("Writing header fields to hardware buffer"); + + // Write sequence and length (CRC written later after payload) + (hw_buffer_ptr.add(4) as *mut u16).write_unaligned(self.seq_num.to_le()); + (hw_buffer_ptr.add(6) as *mut u16).write_unaligned((len as u16).to_le()); + + // Create payload slice directly in hardware buffer for smoltcp to write to + trace!("Creating payload slice in hardware buffer for smoltcp"); + let payload_slice = + core::slice::from_raw_parts_mut(hw_buffer_ptr.add(PACKET_HEADER_SIZE), len); + + // Let smoltcp fill the packet data directly into hardware buffer + trace!("Filling payload using provided closure"); + let result = f(payload_slice); + + // Calculate CRC over sequence + length + payload + // Write CRC to header + + trace!("Calculating CRC for packet"); + let crc_data = core::slice::from_raw_parts(hw_buffer_ptr.add(4), total_len - 4); + let crc: u32 = CRC.checksum(crc_data); + + trace!("Writing CRC to hardware buffer header"); + (hw_buffer_ptr as *mut u32).write_unaligned(crc.to_le()); + + trace!("tx packet len {len} total {total_len}"); + + trace!( + "Transmitted packet: checksum {crc}, seq {}, total length {total_len}", + self.seq_num + ); + *self.seq_num = self.seq_num.wrapping_add(1); + + result + } + } +}