diff --git a/.github/synthesis/debug.json b/.github/synthesis/debug.json new file mode 100644 index 000000000..63b2d418e --- /dev/null +++ b/.github/synthesis/debug.json @@ -0,0 +1,3 @@ +[ + {"top": "softUgnDemoTest", "stage": "test", "cc_report": true} +] diff --git a/bittide-cpus/data/Riscv32imc0.scala b/bittide-cpus/data/Riscv32imc0.scala index 846ed48b4..87337c303 100644 --- a/bittide-cpus/data/Riscv32imc0.scala +++ b/bittide-cpus/data/Riscv32imc0.scala @@ -44,7 +44,7 @@ object Riscv32imc0 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true diff --git a/bittide-cpus/data/Riscv32imc1.scala b/bittide-cpus/data/Riscv32imc1.scala index 20548229e..bd949067e 100644 --- a/bittide-cpus/data/Riscv32imc1.scala +++ b/bittide-cpus/data/Riscv32imc1.scala @@ -44,7 +44,7 @@ object Riscv32imc1 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true diff --git a/bittide-cpus/data/Riscv32imc2.scala b/bittide-cpus/data/Riscv32imc2.scala index 3e57f1238..38f1d8597 100644 --- a/bittide-cpus/data/Riscv32imc2.scala +++ b/bittide-cpus/data/Riscv32imc2.scala @@ -44,7 +44,7 @@ object Riscv32imc2 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true diff --git a/bittide-cpus/data/Riscv32imc3.scala b/bittide-cpus/data/Riscv32imc3.scala index 90628fdce..420d626f5 100644 --- a/bittide-cpus/data/Riscv32imc3.scala +++ b/bittide-cpus/data/Riscv32imc3.scala @@ -44,7 +44,7 @@ object Riscv32imc3 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true diff --git a/bittide-extra/src/Project/Chan.hs b/bittide-extra/src/Project/Chan.hs index 42a74b8b5..5f9b377ac 100644 --- a/bittide-extra/src/Project/Chan.hs +++ b/bittide-extra/src/Project/Chan.hs @@ -5,6 +5,7 @@ module Project.Chan where import Prelude hiding (filter) +import Control.Concurrent.Async (async, waitAnyCancel) import Control.Concurrent.Chan import Data.ByteString (ByteString) import Debug.Trace @@ -46,3 +47,15 @@ Do not use on Handles that might return non-ASCII characters. -} readUntilLine :: Chan ByteString -> String -> IO [String] readUntilLine h = readUntilLineWith h readChan + +{- | Wait for a line to appear on any channel and return its index. +Only use in combination with sensible timeouts. +-} +waitForLineAny :: (HasCallStack) => [Chan ByteString] -> String -> IO Int +waitForLineAny chans expected = do + asyncs <- + mapM + (\(idx, chan) -> async $ waitForLine chan expected >> pure idx) + (zip [0 :: Int ..] chans) + (_, idx) <- waitAnyCancel asyncs + pure idx diff --git a/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs b/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs index f30bfddfe..9f9ba2b57 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs @@ -68,7 +68,7 @@ driverFunc _name targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \gdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-board") gdbs peTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-board" Release) gdbs peTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) gdbs let picocomStarts = liftIO <$> L.zipWith (initPicocom hitlDir) targets [0 ..] diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs index 18ef9f888..c4b4d3144 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs @@ -75,8 +75,8 @@ muConfig :: muConfig = PeConfig { cpu = Riscv32imc.vexRiscv1 - , depthI = SNat @(Div (128 * 1024) 4) - , depthD = SNat @(Div (64 * 1024) 4) + , depthI = SNat @(Div (320 * 1024) 4) + , depthD = SNat @(Div (80 * 1024) 4) , initI = Nothing , initD = Nothing , iBusTimeout = d0 diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index d3668fb8f..3ff6b02a8 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -71,7 +71,7 @@ driver testName targets = do Gdb.withGdbs (L.length targets) $ \bootGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) bootGdbs bootTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs liftIO $ mapConcurrently_ Gdb.continue bootGdbs liftIO @@ -102,11 +102,11 @@ driver testName targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \ccGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs Gdb.withGdbs (L.length targets) $ \muGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-mu") muGdbs muTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-mu" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do @@ -131,17 +131,6 @@ driver testName targets = do hardwareRoundtrips = calculateRoundtripLatencies $ L.concat hardwareUgns _ <- liftIO $ do putStrLn "\n=== Hardware UGN Roundtrip Latencies ===" - mapM print hardwareRoundtrips - liftIO - $ T.tryWithTimeoutOn - T.PrintActionTime - "Waiting for calendar initialization" - (30_000_000) - goDumpCcSamples - $ forConcurrently_ picocoms - $ \pico -> - waitForLine pico "[MU] All calendars initialized" - softwareUgnsPerNode <- liftIO $ T.tryWithTimeoutOn @@ -222,3 +211,117 @@ driver testName targets = do liftIO goDumpCcSamples pure ExitSuccess + +driver2 :: + (HasCallStack) => + String -> + [(HwTarget, DeviceInfo)] -> + VivadoM ExitCode +driver2 testName targets = do + liftIO + . putStrLn + $ "Running driver function for targets " + <> show ((\(_, info) -> info.deviceId) <$> targets) + + projectDir <- liftIO $ findParentContaining "cabal.project" + let hitlDir = projectDir "_build/hitl" testName + + forM_ targets (assertProbe "probe_test_start") + + let + -- BOOT / MU / CC IDs + expectedJtagIds = [0x0514C001, 0x1514C001, 0x2514C001] + toInitArgs (_, deviceInfo) targetIndex = + Ocd.InitOpenOcdArgs{deviceInfo, expectedJtagIds, hitlDir, targetIndex} + initArgs = L.zipWith toInitArgs targets [0 ..] + optionalBootInitArgs = L.repeat def{Ocd.logPrefix = "boot-", Ocd.initTcl = "vexriscv_boot_init.tcl"} + openOcdBootStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalBootInitArgs + + let picocomStarts = liftIO <$> L.zipWith (initPicocom hitlDir) targets [0 ..] + brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do + -- Start OpenOCD that will program the boot CPU + brackets openOcdBootStarts (liftIO . (.cleanup)) $ \initOcdsData -> do + let bootTapInfos = parseBootTapInfo <$> initOcdsData + + Gdb.withGdbs (L.length targets) $ \bootGdbs -> do + liftIO + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) 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_ picocoms + $ \pico -> + waitForLine pico "[BT] Going into infinite loop.." + + let + optionalInitArgs = L.repeat def + openOcdStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalInitArgs + + -- Start OpenOCD instances for all CPUs + 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) + + gdbStarts = liftIO <$> fmap (const Gdb.start) targets + gdbCleanupAction gdb = do + putStrLn "Retrieving final CPU state" + Gdb.interruptCommand gdb + Gdb.runCommand gdb + . unlines + $ [ "printf \"Final CPU state\\n\"" + , "i r" + , "bt" + ] + Gdb.stop gdb + + brackets gdbStarts (liftIO . gdbCleanupAction) $ \ccGdbs -> do + -- Gdb.withGdbs (L.length targets) $ \ccGdbs -> do + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs + + brackets gdbStarts (liftIO . gdbCleanupAction) $ \muGdbs -> do + -- Gdb.withGdbs (L.length targets) $ \muGdbs -> do + liftIO + $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-demo-mu-2" Release) muGdbs muTapInfos targets + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs + + brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do + let goDumpCcSamples = dumpCcSamples hitlDir (defCcConf (natToNum @FpgaCount)) ccGdbs + + _ <- liftIO $ do + mapConcurrently + ( \gdb -> do + Gdb.setBreakpoints gdb ["_start_trap_rust"] + Gdb.setBreakpointHook gdb + ) + muGdbs + + liftIO $ mapConcurrently_ Gdb.continue ccGdbs + liftIO $ mapConcurrently_ Gdb.continue muGdbs + + liftIO + $ T.tryWithTimeoutOn + T.PrintActionTime + "Waiting for CPU test status" + (360_000_000) + goDumpCcSamples + $ forConcurrently_ picocoms + $ \pico -> + waitForLine pico "[MU] Demo complete." + + liftIO goDumpCcSamples + + pure ExitSuccess diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs index 264ac9b30..bf5a4a806 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs @@ -109,11 +109,11 @@ tests = , externalHdl = [] , testCases = [ HitlTestCase - { name = "Bittide_Demo_DUT" + { name = "Bittide_Demo_DUT_mu2" , parameters = paramForHwTargets allHwTargets () , postProcData = () } ] - , mDriverProc = Just Driver.driver + , mDriverProc = Just Driver.driver2 , mPostProc = Nothing } diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs index 3f4c19e22..3b522d6c5 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs @@ -115,15 +115,16 @@ dumpCcSamples hitlDir ccConf ccGdbs = do initGdb :: FilePath -> String -> + CargoBuildType -> Gdb -> Ocd.TapInfo -> (HwTarget, DeviceInfo) -> IO () -initGdb hitlDir binName gdb tapInfo (hwT, _d) = do +initGdb hitlDir binName buildType gdb tapInfo (hwT, _d) = do Gdb.setLogging gdb $ hitlDir "gdb-" <> binName <> "-" <> show (getTargetIndex hwT) <> ".log" - Gdb.setFile gdb $ firmwareBinariesDir "riscv32imc" Release binName + Gdb.setFile gdb $ firmwareBinariesDir "riscv32imc" buildType binName Gdb.setTarget gdb tapInfo.gdbPort Gdb.setTimeout gdb Nothing Gdb.runCommand gdb "echo connected to target device" @@ -376,7 +377,7 @@ driver testName targets = do Gdb.withGdbs (L.length targets) $ \bootGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) bootGdbs bootTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs liftIO $ mapConcurrently_ Gdb.continue bootGdbs liftIO @@ -407,12 +408,12 @@ driver testName targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \ccGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs Gdb.withGdbs (L.length targets) $ \muGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-mu") muGdbs muTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-mu" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs let goDumpCcSamples = dumpCcSamples hitlDir (defCcConf (natToNum @FpgaCount)) ccGdbs diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs index 360f069b0..595eda52e 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs @@ -70,7 +70,7 @@ driver testName targets = do Gdb.withGdbs (L.length targets) $ \bootGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) bootGdbs bootTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs liftIO $ mapConcurrently_ Gdb.continue bootGdbs liftIO @@ -101,17 +101,17 @@ driver testName targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \ccGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs Gdb.withGdbs (L.length targets) $ \muGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-mu") muGdbs muTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-mu" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs Gdb.withGdbs (L.length targets) $ \gppeGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-gppe") gppeGdbs gppeTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-gppe" Release) gppeGdbs gppeTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) gppeGdbs brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do diff --git a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs index 83cb1d6c6..369c72245 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs @@ -5,9 +5,10 @@ module Bittide.Instances.Tests.Ringbuffer where -import Clash.Explicit.Prelude +import Clash.Explicit.Prelude hiding (delayN) import Clash.Prelude ( HiddenClockResetEnable, + delayN, exposeEnable, hasClock, withClockResetEnable, @@ -19,6 +20,7 @@ import Data.Maybe (catMaybes) import GHC.Stack (HasCallStack) import Project.FilePath import Protocols +import Protocols.Extra (fmapC) import Protocols.Idle import Protocols.MemoryMap import System.FilePath (()) @@ -33,6 +35,9 @@ import Bittide.Ringbuffer import Bittide.SharedTypes (withBittideByteOrder) import Bittide.Wishbone +import qualified Data.List as L +import qualified Protocols.Vec as Vec + createDomain vSystem{vName = "Slow", vPeriod = hzToPeriod 1000000} -- | Memory depth for the ringbuffers (16 entries of 8 bytes each) @@ -42,24 +47,37 @@ memDepth = SNat dutMM :: (HasCallStack) => Protocols.MemoryMap.MemoryMap dutMM = (\(SimOnly mm, _) -> mm) - $ withClockResetEnable @System clockGen (resetGenN d2) enableGen - $ toSignals (dutWithBinary "") ((), pure $ deepErrorX "memoryMap") + $ withClockResetEnable @Slow clockGen (resetGenN d2) enableGen + $ toSignals (dutWithBinary d0 "") ((), pure $ deepErrorX "memoryMap") + +replicateC :: forall n a b. SNat n -> Circuit a b -> Circuit (Vec n a) (Vec n b) +replicateC SNat c = repeatC c --- | Parameterized DUT that loads a specific firmware binary. +-- | Parameterized DUT that loads a specific firmware binary with configurable latency. dutWithBinary :: - (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom) => + (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom, KnownNat latency) => + SNat latency -> String -> Circuit (ToConstBwd Mm) (Df dom (BitVector 8)) -dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do +dutWithBinary latency binaryName = withBittideByteOrder $ circuit $ \mm -> do (uartRx, jtagIdle) <- idleSource - [uartBus, wbTx, wbRx, timeBus] <- - processingElement NoDumpVcd (peConfig binaryName) -< (mm, jtagIdle) + ([uartBus, timeBus], wbTxs, wbRxs) <- + Vec.split3 + <| processingElement NoDumpVcd (peConfig binaryName) + -< (mm, jtagIdle) (uartTx, _uartStatus) <- uartInterfaceWb d16 d2 uartBytes -< (uartBus, uartRx) - txOut <- - transmitRingbufferWb (exposeEnable $ blockRamByteAddressable (Vec (repeat 0))) memDepth - -< wbTx - receiveRingbufferWb (\ena -> blockRam hasClock ena (replicate memDepth 0)) memDepth - -< (wbRx, txOut) + txOuts <- + replicateC + d2 + ( transmitRingbufferWb (exposeEnable $ blockRamByteAddressable (Vec (repeat 0))) memDepth + ) + -< wbTxs + -- Add configurable latency between TX and RX ringbuffers + txOutDelayeds <- fmapC (applyC (toSignal . delayN latency 0 . fromSignal) id) -< txOuts + idleSink + <| fmapC (receiveRingbufferWb (\ena -> blockRam hasClock ena (replicate memDepth 0)) memDepth) + <| Vec.zip + -< (wbRxs, txOutDelayeds) _cnt <- timeWb Nothing -< timeBus idC -< uartTx where @@ -92,20 +110,61 @@ dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do type IMemWords = DivRU (300 * 1024) 4 type DMemWords = DivRU (256 * 1024) 4 +takeUntilList :: (Eq a) => [a] -> [a] -> [a] +takeUntilList _ [] = [] +takeUntilList prefix xs@(y : ys) + | prefix `L.isPrefixOf` xs = [] + | otherwise = y : takeUntilList prefix ys + -- Ringbuffer test simulation simRingbuffer :: IO () -simRingbuffer = putStr simResultRingbuffer +simRingbuffer = putStr $ simResultRingbuffer d0 + +simResultRingbuffer :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String +simResultRingbuffer lat = takeUntilList "=== Test Complete ===" $ chr . fromIntegral <$> catMaybes uartStream + where + uartStream = sampleC def{timeoutAfter = 1_000_000} (dutNoMM lat) + + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df Slow (BitVector 8)) + dutNoMM latency = circuit $ do + mm <- ignoreMM + uartTx <- + withClockResetEnable clockGen (resetGenN d2) enableGen + $ (dutWithBinary latency "ringbuffer_test") + -< mm + idC -< uartTx + +simSmolTcp :: IO () +simSmolTcp = putStr $ simResultSmolTcp d0 + +simResultSmolTcp :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String +simResultSmolTcp lat = takeUntilList "=== Test Complete ===" $ chr . fromIntegral <$> catMaybes uartStream + where + uartStream = sampleC def{timeoutAfter = 10_000_000} (dutNoMM lat) + + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df Slow (BitVector 8)) + dutNoMM latency = circuit $ do + mm <- ignoreMM + uartTx <- + withClockResetEnable clockGen (resetGenN d2) enableGen + $ (dutWithBinary latency "ringbuffer_smoltcp_test") + -< mm + idC -< uartTx + +simAlignedRingbuffer :: IO () +simAlignedRingbuffer = putStr $ simResultAlignedRingbuffer d0 -simResultRingbuffer :: (HasCallStack) => String -simResultRingbuffer = chr . fromIntegral <$> catMaybes uartStream +simResultAlignedRingbuffer :: + forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String +simResultAlignedRingbuffer lat = takeUntilList "=== Test Complete ===" $ chr . fromIntegral <$> catMaybes uartStream where - uartStream = sampleC def{timeoutAfter = 250_000} dutNoMM + uartStream = sampleC def{timeoutAfter = 250_000} (dutNoMM lat) - dutNoMM :: (HasCallStack) => Circuit () (Df System (BitVector 8)) - dutNoMM = circuit $ do + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df Slow (BitVector 8)) + dutNoMM latency = circuit $ do mm <- ignoreMM uartTx <- withClockResetEnable clockGen (resetGenN d2) enableGen - $ (dutWithBinary "ringbuffer_test") + $ (dutWithBinary latency "aligned_ringbuffer_test") -< mm idC -< uartTx diff --git a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs index bf30bceb6..4444d17da 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs @@ -58,7 +58,7 @@ dutMM = -- | Parameterized DUT that loads a specific firmware binary. dutWithBinary :: - (HasCallStack, HiddenClockResetEnable dom) => + (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom) => String -> Circuit (ToConstBwd Mm) (Df dom (BitVector 8)) dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do @@ -68,11 +68,13 @@ dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do , wbGu , wbSuCal , wbGuCal + , timeBus ] <- processingElement NoDumpVcd (peConfig binaryName) -< (mm, jtagIdle) (uartTx, _uartStatus) <- uartInterfaceWb d16 d2 uartBytes -< (uartBus, uartRx) Fwd link <- gatherUnitWbC gatherConfig -< (wbGu, wbGuCal) scatterUnitWbC scatterConfig link -< (wbSu, wbSuCal) + _cnt <- timeWb Nothing -< timeBus idC -< uartTx where peConfig binary = unsafePerformIO $ do @@ -101,5 +103,5 @@ dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do } {-# OPAQUE dutWithBinary #-} -type IMemWords = DivRU (64 * 1024) 4 -type DMemWords = DivRU (32 * 1024) 4 +type IMemWords = DivRU (300 * 1024) 4 +type DMemWords = DivRU (256 * 1024) 4 diff --git a/bittide-instances/tests/Wishbone/Ringbuffer.hs b/bittide-instances/tests/Wishbone/Ringbuffer.hs index 796553fb1..599c887fd 100644 --- a/bittide-instances/tests/Wishbone/Ringbuffer.hs +++ b/bittide-instances/tests/Wishbone/Ringbuffer.hs @@ -10,20 +10,45 @@ module Wishbone.Ringbuffer where import Clash.Explicit.Prelude +import Control.Monad.IO.Class (liftIO) import Data.List (isInfixOf) +import Data.Proxy (Proxy (..)) +import qualified Hedgehog as H +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range import Test.Tasty -import Test.Tasty.HUnit +import Test.Tasty.Hedgehog (testProperty) import Test.Tasty.TH -import Bittide.Instances.Tests.Ringbuffer (simResultRingbuffer) +import Bittide.Instances.Tests.Ringbuffer (simResultAlignedRingbuffer, simResultRingbuffer) -case_ringbuffer_test :: Assertion -case_ringbuffer_test = do - assertBool - msg - ("TEST PASSED" `isInfixOf` simResultRingbuffer) - where - msg = "Received the following from the CPU over UART:\n" <> simResultRingbuffer +prop_ringbuffer_test :: H.Property +prop_ringbuffer_test = H.withTests 1 $ H.property $ do + latency <- H.forAll $ Gen.integral (Range.constant 0 100) + liftIO $ putStrLn $ "Testing ringbuffer_test with latency " <> show latency <> " cycles" + let result = case someNatVal (fromInteger latency) of + Just (SomeNat (_ :: Proxy n)) -> simResultRingbuffer (SNat @n) + Nothing -> error $ "Invalid latency value: " <> show latency + H.annotate + $ "Running ringbuffer_test with latency " + <> show latency + <> " cycles\nReceived the following from the CPU over UART:\n" + <> result + H.assert ("TEST PASSED" `isInfixOf` result) + +prop_aligned_ringbuffer_test :: H.Property +prop_aligned_ringbuffer_test = H.withTests 1 $ H.property $ do + latency <- H.forAll $ Gen.integral (Range.constant 0 100) + liftIO $ putStrLn $ "Testing ringbuffer_test with latency " <> show latency <> " cycles" + let result = case someNatVal (fromInteger latency) of + Just (SomeNat (_ :: Proxy n)) -> simResultAlignedRingbuffer (SNat @n) + Nothing -> error $ "Invalid latency value: " <> show latency + H.annotate + $ "Running aligned_ringbuffer_test with latency " + <> show latency + <> " cycles\nReceived the following from the CPU over UART:\n" + <> result + H.assert ("ALL TESTS PASSED" `isInfixOf` result) tests :: TestTree tests = $(testGroupGenerator) diff --git a/bittide-instances/tests/Wishbone/ScatterGather.hs b/bittide-instances/tests/Wishbone/ScatterGather.hs index a92ae4c42..2ed15c771 100644 --- a/bittide-instances/tests/Wishbone/ScatterGather.hs +++ b/bittide-instances/tests/Wishbone/ScatterGather.hs @@ -1,6 +1,8 @@ -- SPDX-FileCopyrightText: 2024 Google LLC -- -- SPDX-License-Identifier: Apache-2.0 +-- Don't warn about orphan instances, caused by `createDomain`. +{-# OPTIONS_GHC -Wno-orphans #-} -- Don't warn about partial functions: this is a test, so we'll see it fail. {-# OPTIONS_GHC -Wno-x-partial #-} @@ -22,6 +24,9 @@ import Bittide.Instances.Tests.ScatterGather (dutWithBinary) import qualified Prelude as P +createDomain vSystem{vName = "Slow", vPeriod = hzToPeriod 1000000} + +-- Simple sim :: IO () sim = putStr simResult diff --git a/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs b/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs index 84db21f95..b9d7e536c 100644 --- a/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs +++ b/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs @@ -166,7 +166,7 @@ testIncreaseBuswidth power = property $ do -- Depth is half the number of addresses to also tests error on out-of-range accesses. depth = SNat @(2 ^ (AddressWidth - 2)) - lastAddress = snatToNum $ predSNat depth + lastAddress = snatToNum $ mulSNat d2 depth lastAddressSmall = lastAddress * (natToNum @(2 ^ power)) genAddr = Gen.integral (Range.linear 0 lastAddressSmall) genInputs = Gen.list (Range.linear 1 10) (genWishboneTransfer genAddr) diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index eee8720c2..645978f1d 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -24,6 +24,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned_ringbuffer_test" +version = "0.1.0" +dependencies = [ + "bittide-hal", + "bittide-sys", + "memmap-generate", + "riscv-rt 0.11.0", + "ufmt", +] + [[package]] name = "axi_stream_self_test" version = "0.1.0" @@ -84,6 +95,7 @@ name = "bittide-sys" version = "0.1.0" dependencies = [ "bittide-hal", + "crc", "fdt", "heapless", "itertools", @@ -91,6 +103,7 @@ dependencies = [ "rand", "smoltcp 0.12.0", "ufmt", + "zerocopy", ] [[package]] @@ -217,6 +230,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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "critical-section" version = "1.2.0" @@ -488,6 +516,20 @@ dependencies = [ "ufmt", ] +[[package]] +name = "ringbuffer_smoltcp_test" +version = "0.1.0" +dependencies = [ + "bittide-hal", + "bittide-sys", + "log", + "memmap-generate", + "riscv 0.10.1", + "riscv-rt 0.11.0", + "smoltcp 0.12.0", + "ufmt", +] + [[package]] name = "ringbuffer_test" version = "0.1.0" @@ -708,6 +750,20 @@ dependencies = [ "ufmt", ] +[[package]] +name = "soft-ugn-demo-mu-2" +version = "0.1.0" +dependencies = [ + "bittide-hal", + "bittide-sys", + "log", + "memmap-generate", + "riscv 0.10.1", + "riscv-rt 0.11.0", + "smoltcp 0.12.0", + "ufmt", +] + [[package]] name = "soft-ugn-mu" version = "0.1.0" @@ -911,3 +967,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 de9b1ef8f..45a69b90a 100644 --- a/firmware-binaries/Cargo.toml +++ b/firmware-binaries/Cargo.toml @@ -17,9 +17,11 @@ members = [ "examples/smoltcp_client", "sim-tests/addressable_bytes_wb_test", + "sim-tests/aligned_ringbuffer_test", "sim-tests/axi_stream_self_test", "sim-tests/registerwb_test", "sim-tests/ringbuffer_test", + "sim-tests/ringbuffer_smoltcp_test", "sim-tests/capture_ugn_test", "sim-tests/clock-control-wb", "sim-tests/dna_port_e2_test", @@ -40,6 +42,7 @@ members = [ "demos/clock-control", "demos/soft-ugn-mu", + "demos/soft-ugn-demo-mu-2", "demos/switch-demo1-boot", "demos/switch-demo1-mu", "demos/switch-demo2-mu", diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml new file mode 100644 index 000000000..d77352a34 --- /dev/null +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2026 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "soft-ugn-demo-mu-2" +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" + +[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_trace"] + +[build-dependencies] +memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/build.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/build.rs new file mode 100644 index 000000000..568ae2efb --- /dev/null +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/build.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use memmap_generate::build_utils::standard_memmap_build; + +fn main() { + standard_memmap_build("SoftUgnDemoMu.json", "DataMemory", "InstructionMemory"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs new file mode 100644 index 000000000..0a9234cce --- /dev/null +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -0,0 +1,379 @@ +#![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::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; +use bittide_hal::hals::soft_ugn_demo_mu::DeviceInstances; +use bittide_hal::manual_additions::ringbuffer::{ + AlignedReceiveBuffer, TransmitRingbufferInterface, +}; +use bittide_hal::manual_additions::timer::Instant; +use bittide_sys::link_startup::LinkStartup; +use bittide_sys::net_state::{Manager, Subordinate, UgnEdge, UgnReport}; +use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; +use bittide_sys::stability_detector::Stability; +use core::fmt::Write; +use log::{info, trace, warn, LevelFilter}; +use riscv::register::{mcause, mepc, mtval}; +use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; +use smoltcp::socket::tcp; +use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr}; +use ufmt::uwriteln; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; +const LINK_COUNT: usize = 7; +const TCP_BUF_SIZE: usize = 256; +const MANAGER_DNA: [u8; 12] = [133, 129, 48, 4, 64, 192, 105, 1, 1, 0, 2, 64]; +const LOG_TICK_EVERY: u32 = 500; +const CLIENT_IP: [u8; 4] = [100, 100, 100, 100]; +const SERVER_IP: [u8; 4] = [100, 100, 100, 101]; + +static mut TCP_RX_BUFS: [u8; TCP_BUF_SIZE] = [0; TCP_BUF_SIZE]; +static mut TCP_TX_BUFS: [u8; TCP_BUF_SIZE] = [0; TCP_BUF_SIZE]; + +#[cfg(not(test))] +use riscv_rt::entry; + +fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { + smoltcp::time::Instant::from_micros(instant.micros() as i64) +} + +fn set_iface_ip(iface: &mut Interface, ip: [u8; 4]) { + iface.update_ip_addrs(|addrs| { + addrs.clear(); + addrs + .push(IpCidr::new(IpAddress::v4(ip[0], ip[1], ip[2], ip[3]), 24)) + .unwrap(); + }); +} + +fn socket_set<'a>(storage: &'a mut [SocketStorage<'static>]) -> SocketSet<'a> { + // SAFETY: Socket buffers are backed by static memory, and SocketSet does not + // outlive the borrow of storage in this scope. + let storage: &'a mut [SocketStorage<'a>] = unsafe { core::mem::transmute(storage) }; + SocketSet::new(storage) +} + +fn make_device( + rx: ReceiveRingbuffer, + tx: TransmitRingbuffer, +) -> RingbufferDevice { + let mut rx_aligned = AlignedReceiveBuffer::new(rx); + rx_aligned.align(&tx); + info!( + "Aligned RX buffer with offset {}", + rx_aligned.get_alignment_offset().unwrap() + ); + RingbufferDevice::new(rx_aligned, tx) +} + +#[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::Info); + } + + info!("=== Soft UGN Demo MU2 ==="); + let transceivers = &INSTANCES.transceivers; + 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_ugn_0, + INSTANCES.capture_ugn_1, + INSTANCES.capture_ugn_2, + INSTANCES.capture_ugn_3, + INSTANCES.capture_ugn_4, + INSTANCES.capture_ugn_5, + INSTANCES.capture_ugn_6, + ]; + // Pseudocode setup: + // 1) Initialize MU peripherals and scatter/gather calendars for ringbuffers. + // 2) Align ringbuffers on all ports (two-phase protocol). + // 3) Run LinkStartup per port to bring up physical links and capture UGNs. + // 4) Wait for clock stability; stop auto-centering and record EB deltas. + // 5) For each port, create RingbufferDevice + smoltcp Interface with static IP. + // 6) Run manager state machines to connect to neighbors and request UGNs. + // 7) Collect UGN edges over TCP and aggregate locally. + + info!("Bringing up links..."); + let mut link_startups = [LinkStartup::new(); LINK_COUNT]; + while !link_startups.iter().all(|ls| ls.is_done()) { + for (i, link_startup) in link_startups.iter_mut().enumerate() { + link_startup.next( + transceivers, + i, + elastic_buffers[i], + capture_ugns[i].has_captured(), + ); + } + } + + info!("Waiting for stability..."); + loop { + let stability = Stability { + stable: cc.links_stable()[0], + settled: 0, + }; + if stability.all_stable() { + break; + } + } + + 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 (capture_ugn, eb_delta) in capture_ugns.iter().zip(eb_deltas) { + capture_ugn.set_elastic_buffer_delta(eb_delta); + } + + info!("Captured hardware UGNs"); + for (i, capture_ugn) in capture_ugns.iter().enumerate() { + info!( + "Capture UGN {}: local = {}, remote = {}", + i, + capture_ugn.local_counter(), + capture_ugn.remote_counter() + ); + } + + let receive_ringbuffers = [ + INSTANCES.receive_ringbuffer_0, + INSTANCES.receive_ringbuffer_1, + INSTANCES.receive_ringbuffer_2, + INSTANCES.receive_ringbuffer_3, + INSTANCES.receive_ringbuffer_4, + INSTANCES.receive_ringbuffer_5, + INSTANCES.receive_ringbuffer_6, + ]; + let transmit_ringbuffers = [ + INSTANCES.transmit_ringbuffer_0, + INSTANCES.transmit_ringbuffer_1, + INSTANCES.transmit_ringbuffer_2, + INSTANCES.transmit_ringbuffer_3, + INSTANCES.transmit_ringbuffer_4, + INSTANCES.transmit_ringbuffer_5, + INSTANCES.transmit_ringbuffer_6, + ]; + for tx in transmit_ringbuffers.iter() { + tx.clear(); + } + let mut receive_iter = receive_ringbuffers.into_iter(); + let mut transmit_iter = transmit_ringbuffers.into_iter(); + let mut devices: [RingbufferDevice; LINK_COUNT] = + core::array::from_fn(|i| { + let rx = receive_iter.next().expect("missing receive ringbuffer"); + let tx = transmit_iter.next().expect("missing transmit ringbuffer"); + let device = make_device(rx, tx); + trace!("Made device for link {}, with MTU {}", i, device.mtu()); + device + }); + + let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[..] }; + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(rx_buf), + tcp::SocketBuffer::new(tx_buf), + ); + let mut sockets_storage: [SocketStorage<'static>; 1] = Default::default(); + let socket_handle = { + let mut sockets = socket_set(&mut sockets_storage[..]); + sockets.add(socket) + }; + let dna = INSTANCES.dna.dna(); + info!("My dna: {:?}", dna); + let is_manager = dna == MANAGER_DNA; + info!( + "Role: {}", + if is_manager { "manager" } else { "subordinate" } + ); + unsafe { + log::set_max_level_racy(LevelFilter::Trace); + } + + if is_manager { + info!("Starting manager state machines..."); + let mut reports: [Option; LINK_COUNT] = [None; LINK_COUNT]; + for link in 0..LINK_COUNT { + info!("Starting manager for link {}", link); + let now = to_smoltcp_instant(INSTANCES.timer.now()); + let mut iface = + Interface::new(Config::new(HardwareAddress::Ip), &mut devices[link], now); + set_iface_ip(&mut iface, CLIENT_IP); + let mut manager = Manager::new(iface, socket_handle, link, SERVER_IP); + + trace!("Starting manager loop for link {}", link); + loop { + let now = to_smoltcp_instant(INSTANCES.timer.now()); + let mut sockets = socket_set(&mut sockets_storage[..]); + trace!("Polling manager link {}", link); + manager.poll(now, &mut devices[link], &mut sockets); + trace!("manager link {} state {:?}", link, manager.state()); + if manager.is_done() { + trace!("manager link {} is done", link); + break; + } + } + reports[link] = Some(manager.report()); + } + + info!("UGN reports from subordinates:"); + for (idx, report) in reports.iter().enumerate() { + if let Some(report) = report { + info!("Link {}: {} edges", idx, report.count); + for (edge_idx, edge) in report.edges.iter().enumerate() { + if edge_idx >= report.count as usize { + break; + } + if let Some(edge) = edge { + info!( + " Edge {}: {}:{} -> {}:{}, ugn={}", + edge_idx, + edge.src_node, + edge.src_port, + edge.dst_node, + edge.dst_port, + edge.ugn + ); + } else { + warn!(" Edge {}: missing", edge_idx); + } + } + } else { + warn!("Link {}: no report", idx); + } + } + } else { + info!("Starting subordinate state machines..."); + let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = + core::array::from_fn(|_| Default::default()); + let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx| { + let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[..] }; + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(rx_buf), + tcp::SocketBuffer::new(tx_buf), + ); + let mut sockets = socket_set(&mut sockets_storage[idx][..]); + sockets.add(socket) + }); + let mut subordinates: [Subordinate; LINK_COUNT] = core::array::from_fn(|idx| { + let now = to_smoltcp_instant(INSTANCES.timer.now()); + let mut iface = + Interface::new(Config::new(HardwareAddress::Ip), &mut devices[idx], now); + set_iface_ip(&mut iface, SERVER_IP); + Subordinate::new(iface, socket_handles[idx], idx, dna) + }); + let mut tick: u32 = 0; + for link in 0..LINK_COUNT { + subordinates[link].set_report(build_report_for_link(link, &capture_ugns[link], &dna)); + } + + loop { + tick = tick.wrapping_add(1); + if tick % LOG_TICK_EVERY == 0 { + info!("subordinate loop tick {}", tick); + } + let now = to_smoltcp_instant(INSTANCES.timer.now()); + for link in 0..LINK_COUNT { + let mut sockets = socket_set(&mut sockets_storage[link][..]); + trace!("Polling subordinate link {}", link); + subordinates[link].poll(now, &mut devices[link], &mut sockets); + { + let socket = sockets.get::(socket_handles[link]); + trace!( + "subordinate link {} socket open {} active {} can_send {} can_recv {} state {:?}", + link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.state() + ); + } + trace!( + "subordinate link {} ip addrs {:?}", + link, + subordinates[link].iface().ip_addrs() + ); + trace!( + "subordinate link {} state {:?}", + link, + subordinates[link].state() + ); + } + } + } + + uwriteln!(uart, "Demo complete.").unwrap(); + loop { + continue; + } +} + +#[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(); + info!("mcause: {:?}\n", mcause::read()); + info!("mepc: {:?}\n", mepc::read()); + info!("mtval: {:?}\n", mtval::read()); + }); + loop { + continue; + } +} + +fn build_report_for_link( + link: usize, + capture_ugn: &bittide_hal::shared_devices::CaptureUgn, + dna: &[u8; 12], +) -> UgnReport { + let mut report = UgnReport::new(); + report.count = 1; + report.edges[0] = Some(UgnEdge { + src_node: dna[0] as u32, + src_port: link as u32, + dst_node: 0, + dst_port: link as u32, + ugn: capture_ugn.local_counter() as i64, + }); + trace!("Prepared report for link {}", link); + report +} diff --git a/firmware-binaries/examples/smoltcp_client/Cargo.toml b/firmware-binaries/examples/smoltcp_client/Cargo.toml index af8ecfd04..89165d899 100644 --- a/firmware-binaries/examples/smoltcp_client/Cargo.toml +++ b/firmware-binaries/examples/smoltcp_client/Cargo.toml @@ -31,12 +31,12 @@ default-features = false [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_info"] +features = ["max_level_trace", "release_max_level_trace"] [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] memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/examples/smoltcp_client/src/main.rs b/firmware-binaries/examples/smoltcp_client/src/main.rs index e7eaabcf9..539c6df69 100644 --- a/firmware-binaries/examples/smoltcp_client/src/main.rs +++ b/firmware-binaries/examples/smoltcp_client/src/main.rs @@ -13,7 +13,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}; diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/Cargo.toml b/firmware-binaries/sim-tests/aligned_ringbuffer_test/Cargo.toml new file mode 100644 index 000000000..1cabc1300 --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/Cargo.toml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "aligned_ringbuffer_test" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +riscv-rt = "0.11.0" +bittide-sys = { path = "../../../firmware-support/bittide-sys" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +ufmt = "0.2.0" + +[build-dependencies] +memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs new file mode 100644 index 000000000..e72dc82e9 --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use memmap_generate::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/aligned_ringbuffer_test/memory.x b/firmware-binaries/sim-tests/aligned_ringbuffer_test/memory.x new file mode 100644 index 000000000..751b2cfd3 --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/memory.x @@ -0,0 +1,18 @@ +/* +SPDX-FileCopyrightText: 2025 Google LLC + +SPDX-License-Identifier: CC0-1.0 +*/ + +MEMORY +{ + IMEM : ORIGIN = 0x80000000, LENGTH = 64K + DMEM : ORIGIN = 0x20000000, LENGTH = 32K +} + +REGION_ALIAS("REGION_TEXT", IMEM); +REGION_ALIAS("REGION_RODATA", DMEM); +REGION_ALIAS("REGION_DATA", DMEM); +REGION_ALIAS("REGION_BSS", DMEM); +REGION_ALIAS("REGION_HEAP", DMEM); +REGION_ALIAS("REGION_STACK", DMEM); diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs new file mode 100644 index 000000000..a59ed1756 --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +#![no_std] +#![cfg_attr(not(test), no_main)] + +use bittide_hal::{ + manual_additions::{ + ringbuffer::{AlignedReceiveBuffer, TransmitRingbufferInterface}, + timer::Duration, + }, + ringbuffer_test::{devices::TransmitRingbuffer, DeviceInstances}, +}; +use core::fmt::Write; +#[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; + let timer = INSTANCES.timer; + + writeln!(uart, "=== Aligned Ringbuffer Test ===").unwrap(); + + let tx_ringbuffer = INSTANCES.transmit_ringbuffer_0; + let rx_ringbuffer = INSTANCES.receive_ringbuffer_0; + + // Step 1: Perform alignment procedure + writeln!(uart, "\n--- Step 1: Alignment Discovery ---").unwrap(); + writeln!(uart, "Running alignment procedure...").unwrap(); + + // Create a copy of rx_ringbuffer for alignment, then get back the aligned buffer + let rx_copy = + unsafe { bittide_hal::ringbuffer_test::devices::ReceiveRingbuffer::new(rx_ringbuffer.0) }; + let mut rx_aligned = AlignedReceiveBuffer::new(rx_copy); + rx_aligned.align(&tx_ringbuffer); + let alignment_offset = rx_aligned + .get_alignment_offset() + .expect("Failed to discover alignment offset"); + + writeln!( + uart, + "SUCCESS: Discovered alignment offset = {} words ({} bytes)", + alignment_offset, + alignment_offset * 8 + ) + .unwrap(); + + // Step 2: Test aligned transmission + writeln!(uart, "\n--- Step 2: Aligned Transmission Test ---").unwrap(); + writeln!(uart, "Writing pattern and verifying with alignment").unwrap(); + + // Clear TX buffer + tx_ringbuffer.clear(); + + // Create pattern: each word = frame number + let tx_pattern: [[u8; 8]; TransmitRingbuffer::DATA_LEN] = + core::array::from_fn(|i| (0x1000 + i as u64).to_le_bytes()); + + writeln!( + uart, + "Writing {} words to TX at offset 0", + TransmitRingbuffer::DATA_LEN + ) + .unwrap(); + tx_ringbuffer.write_slice(&tx_pattern, 0); + + // Wait for data to propagate + timer.wait(Duration::from_cycles( + TransmitRingbuffer::DATA_LEN as u32, + timer.frequency(), + )); + + // Read using aligned buffer + let mut rx_data: [[u8; 8]; TransmitRingbuffer::DATA_LEN] = + [[0u8; 8]; TransmitRingbuffer::DATA_LEN]; + writeln!( + uart, + "Reading {} words from RX at offset 0 (with alignment)", + TransmitRingbuffer::DATA_LEN + ) + .unwrap(); + rx_aligned.read_slice(&mut rx_data, 0); + + // Verify all words match + let mut all_match = true; + let mut first_mismatch = None; + + for (i, (expected, actual)) in tx_pattern.iter().zip(rx_data.iter()).enumerate() { + if expected != actual && first_mismatch.is_none() { + first_mismatch = Some((i, expected, actual)); + all_match = false; + } + } + + if all_match { + writeln!( + uart, + "*** TEST PASSED: All {} words matched with alignment! ***", + TransmitRingbuffer::DATA_LEN + ) + .unwrap(); + } else { + writeln!(uart, "\n*** TEST FAILED: Data corruption detected ***").unwrap(); + + if let Some((idx, expected, actual)) = first_mismatch { + writeln!( + uart, + "First mismatch at word {}: expected {:02x?}, got {:02x?}", + idx, expected, actual + ) + .unwrap(); + } + + writeln!(uart, "\nTX pattern written:").unwrap(); + for (i, word) in tx_pattern.iter().enumerate() { + write!(uart, " TX[{:2}]: ", i).unwrap(); + for byte in word { + write!(uart, "{:02x} ", byte).unwrap(); + } + writeln!(uart).unwrap(); + } + + writeln!( + uart, + "\nRX pattern received (with alignment offset {}):", + alignment_offset + ) + .unwrap(); + for (i, (expected, actual)) in tx_pattern.iter().zip(rx_data.iter()).enumerate() { + write!(uart, " RX[{:2}]: ", i).unwrap(); + for byte in actual { + write!(uart, "{:02x} ", byte).unwrap(); + } + writeln!(uart, "{}", if expected == actual { "✓" } else { "✗" }).unwrap(); + } + } + + writeln!(uart, "\n=== Test Summary ===").unwrap(); + writeln!(uart, "Alignment discovery: PASS").unwrap(); + writeln!( + uart, + "Aligned transmission: {}", + if all_match { "PASS" } else { "FAIL" } + ) + .unwrap(); + + if all_match { + writeln!(uart, "\n*** ALL TESTS PASSED ***").unwrap(); + } else { + writeln!(uart, "\n*** SOME TESTS FAILED ***").unwrap(); + } + writeln!(uart, "Test done").unwrap(); + + loop { + continue; + } +} + +#[panic_handler] +fn panic_handler(info: &core::panic::PanicInfo) -> ! { + let mut uart = INSTANCES.uart; + writeln!(uart, "Panicked! #{info}").unwrap(); + loop { + continue; + } +} diff --git a/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c b/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c index 28a7e8087..bcc1ab8f5 100644 --- a/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c +++ b/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c @@ -152,6 +152,7 @@ void c_main(void) { } else { uart_puts(uart, "Scatter/Gather HAL tests FAILED\n"); } + uart_puts(uart, "Test done\n"); while (1) { } diff --git a/firmware-binaries/sim-tests/c_timer_wb/src/main.c b/firmware-binaries/sim-tests/c_timer_wb/src/main.c index 5c66b9d44..480482de1 100644 --- a/firmware-binaries/sim-tests/c_timer_wb/src/main.c +++ b/firmware-binaries/sim-tests/c_timer_wb/src/main.c @@ -174,6 +174,7 @@ int c_main(void) { uart_puts(uart, "\r\n=== All tests PASSED! ===\r\n\r\n"); uart_puts(uart, "C Timer HAL test completed successfully!\r\n"); + uart_puts(uart, "Test done\r\n"); // Infinite loop to keep program running while (1) { diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml new file mode 100644 index 000000000..6daf1838c --- /dev/null +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2025 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "ringbuffer_smoltcp_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_trace"] + +[build-dependencies] +memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs new file mode 100644 index 000000000..46314f9a2 --- /dev/null +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use memmap_generate::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/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs new file mode 100644 index 000000000..0a9997a04 --- /dev/null +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +#![no_std] +#![cfg_attr(not(test), no_main)] +#![feature(sync_unsafe_cell)] + +use bittide_hal::manual_additions::ringbuffer::AlignedReceiveBuffer; +use bittide_hal::manual_additions::timer::Instant; +use bittide_hal::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; +use bittide_hal::ringbuffer_test::DeviceInstances; +use bittide_sys::net_state::{Manager, Subordinate, UgnEdge, UgnReport}; +use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; +use core::fmt::Write; +use log::{info, trace, LevelFilter}; +use smoltcp::iface::{Config, Interface, SocketSet, SocketStorage}; +use smoltcp::socket::tcp; +use smoltcp::wire::HardwareAddress; +use ufmt::uwriteln; + +#[cfg(not(test))] +use riscv_rt::entry; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; + +fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { + smoltcp::time::Instant::from_micros(instant.micros() as i64) +} + +#[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::Info); + } + + info!("=== Ringbuffer smoltcp Loopback Test ==="); + + // Set up ringbuffers + info!("Step 1: Finding ringbuffer alignment..."); + let tx_buffer0 = INSTANCES.transmit_ringbuffer_0; + let rx_buffer0 = INSTANCES.receive_ringbuffer_0; + + let tx_buffer1 = INSTANCES.transmit_ringbuffer_1; + let rx_buffer1 = INSTANCES.receive_ringbuffer_1; + let mut rx_aligned0 = AlignedReceiveBuffer::new(rx_buffer0); + let mut rx_aligned1 = AlignedReceiveBuffer::new(rx_buffer1); + + rx_aligned0.align(&tx_buffer0); + rx_aligned1.align(&tx_buffer1); + + let rx_offset0 = rx_aligned0 + .get_alignment_offset() + .expect("Failed to find RX buffer alignment"); + let rx_offset1 = rx_aligned1 + .get_alignment_offset() + .expect("Failed to find RX buffer alignment"); + trace!(" Alignment offset 0: {}", rx_offset0); + trace!(" Alignment offset 1: {}", rx_offset1); + + // Step 2: Create smoltcp device + info!("Step 2: Creating RingbufferDevice..."); + let mut device0: RingbufferDevice = + RingbufferDevice::new(rx_aligned0, tx_buffer1); + let mut device1: RingbufferDevice = + RingbufferDevice::new(rx_aligned1, tx_buffer0); + let mtu = device0.mtu(); + trace!(" MTU: {} bytes", mtu); + + // Step 3: Configure network interface + info!("Step 3: Configuring network interfaces..."); + let hw_addr = HardwareAddress::Ip; + let config0 = Config::new(hw_addr); + let config1 = Config::new(hw_addr); + let now = to_smoltcp_instant(timer.now()); + let mut iface0 = Interface::new(config0, &mut device0, now); + let mut iface1 = Interface::new(config1, &mut device1, now); + let server_ip = [100, 100, 100, 100]; + let client_ip = [100, 100, 100, 101]; + iface0.update_ip_addrs(|addrs| { + addrs + .push(smoltcp::wire::IpCidr::new( + smoltcp::wire::IpAddress::v4( + client_ip[0], + client_ip[1], + client_ip[2], + client_ip[3], + ), + 24, + )) + .unwrap(); + }); + iface1.update_ip_addrs(|addrs| { + addrs + .push(smoltcp::wire::IpCidr::new( + smoltcp::wire::IpAddress::v4( + server_ip[0], + server_ip[1], + server_ip[2], + server_ip[3], + ), + 24, + )) + .unwrap(); + }); + + // Step 4: Create TCP sockets + info!("Step 4: Creating TCP sockets..."); + + // Server socket - reduced buffer sizes to fit in memory + static mut SERVER_RX_BUF0: [u8; 256] = [0; 256]; + static mut SERVER_TX_BUF0: [u8; 256] = [0; 256]; + + let server_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_RX_BUF0[..] }); + let server_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_TX_BUF0[..] }); + let server_socket = tcp::Socket::new(server_rx_buffer, server_tx_buffer); + + // Client socket - reduced buffer sizes to fit in memory + static mut CLIENT_RX_BUF: [u8; 256] = [0; 256]; + static mut CLIENT_TX_BUF: [u8; 256] = [0; 256]; + let client_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut CLIENT_RX_BUF[..] }); + let client_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut CLIENT_TX_BUF[..] }); + let client_socket = tcp::Socket::new(client_rx_buffer, client_tx_buffer); + + let mut server_sockets_storage: [SocketStorage; 1] = Default::default(); + let mut client_sockets_storage: [SocketStorage; 1] = Default::default(); + let mut server_sockets = SocketSet::new(&mut server_sockets_storage[..]); + let mut client_sockets = SocketSet::new(&mut client_sockets_storage[..]); + + let server_handle = server_sockets.add(server_socket); + let client_handle = client_sockets.add(client_socket); + + // Step 5: Initialize link state machines + info!("Step 5: Initializing link state machines..."); + + // Main event loop + info!("Step 7: Running main event loop..."); + let mut done_logged = false; + + let mut manager = Manager::new(iface0, client_handle, 0, server_ip); + let dna: [u8; 12] = core::array::from_fn(|i| i as u8); + let mut subordinate = Subordinate::new(iface1, server_handle, 0, dna); + subordinate.set_report(build_placeholder_report()); + + for _ in 0..1000 { + let timestamp = to_smoltcp_instant(timer.now()); + manager.poll(timestamp, &mut device0, &mut client_sockets); + subordinate.poll(timestamp, &mut device1, &mut server_sockets); + + if manager.is_done() && subordinate.is_done() && !done_logged { + info!(" Manager collected UGN report"); + done_logged = true; + break; + } + } + + // Verify results + info!("Step 8: Verifying results..."); + + if !done_logged { + info!(" FAILURE: UGN report timeout!"); + } else { + let report = manager.report(); + info!(" SUCCESS: Manager received {} UGN edges", report.count); + for (idx, edge) in report.edges.iter().enumerate() { + if idx >= report.count as usize { + break; + } + if let Some(edge) = edge { + info!( + " Edge {}: {}:{} -> {}:{}, ugn={}", + idx, edge.src_node, edge.src_port, edge.dst_node, edge.dst_port, edge.ugn + ); + } else { + info!(" Edge {}: missing", idx); + } + } + } + + uwriteln!(uart, "=== Test Complete ===").unwrap(); + + loop { + continue; + } +} + +fn build_placeholder_report() -> UgnReport { + let mut report = UgnReport::new(); + report.count = 8; + for idx in 0..8 { + report.edges[idx] = Some(UgnEdge { + src_node: idx as u32, + src_port: idx as u32, + dst_node: (idx as u32).saturating_add(1), + dst_port: idx as u32, + ugn: 100 + idx as i64, + }); + } + report +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + let mut uart = INSTANCES.uart; + writeln!(uart, "PANIC: {}", info).ok(); + loop {} +} diff --git a/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs index d69ade07e..43316708c 100644 --- a/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs @@ -4,13 +4,11 @@ #![no_std] #![cfg_attr(not(test), no_main)] -use bittide_hal::{ - manual_additions::timer::Duration, - ringbuffer_test::{devices::TransmitRingbuffer, DeviceInstances}, -}; +use bittide_hal::{manual_additions::timer::Duration, ringbuffer_test::DeviceInstances}; use core::fmt::Write; #[cfg(not(test))] use riscv_rt::entry; +use ufmt::{uwrite, uwriteln}; const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; @@ -24,10 +22,10 @@ fn main() -> ! { let mut uart = INSTANCES.uart; let timer = INSTANCES.timer; - writeln!(uart, "=== Ringbuffer Loopback Test (Byte-Level) ===").unwrap(); + uwriteln!(uart, "=== Ringbuffer Loopback Test (Byte-Level) ===").unwrap(); - let tx_ringbuffer = INSTANCES.transmit_ringbuffer; - let rx_ringbuffer = INSTANCES.receive_ringbuffer; + let tx_ringbuffer = INSTANCES.transmit_ringbuffer_0; + let rx_ringbuffer = INSTANCES.receive_ringbuffer_0; // Create pattern: 4 MSBs = frame number, 4 LSBs = byte index in frame // Frame 0: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07] @@ -47,10 +45,13 @@ fn main() -> ! { } // Wait for data to propagate through the loopback - timer.wait(Duration::from_cycles( - TransmitRingbuffer::DATA_LEN as u32, - timer.frequency(), - )); + // Need to wait long enough for: + // - TX to cycle through entire buffer (DATA_LEN cycles) + // - Propagation delay through configurable latency (0-100 cycles) + // - Pipeline delays in TX/RX logic + // - RX to write all frames + // Wait for 2000 cycles to definitively rule out timing issues + timer.wait(Duration::from_cycles(2000, timer.frequency())); // Read RX buffer byte by byte let mut rx_bytes = [0u8; TOTAL_BYTES]; @@ -94,30 +95,33 @@ fn main() -> ! { } if all_match { - writeln!(uart, "*** TEST PASSED: All data matches! ***").unwrap(); + uwriteln!(uart, "*** TEST PASSED: All data matches! ***").unwrap(); } else { - writeln!(uart, "\n*** TEST FAILED: Data corruption detected ***").unwrap(); + uwriteln!(uart, "\n*** TEST FAILED: Data corruption detected ***").unwrap(); if let Some((frame, byte_idx, expected, actual)) = first_mismatch { - writeln!( + uwriteln!( uart, "First mismatch at RX frame {}, byte {}: expected 0x{:02x}, got 0x{:02x}", - frame, byte_idx, expected, actual + frame, + byte_idx, + expected, + actual ) .unwrap(); } - writeln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); + uwriteln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); for frame in 0..RINGBUFFER_SIZE { let start = frame * 8; - write!(uart, " TX[{:2}]: ", frame).unwrap(); + uwrite!(uart, " TX[{}]: ", frame).unwrap(); for byte_idx in 0..8 { - write!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); + uwrite!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); } - writeln!(uart).unwrap(); + uwriteln!(uart, "").unwrap(); } - writeln!( + uwriteln!( uart, "\nRX pattern received (starting at frame {}):", offset @@ -128,45 +132,47 @@ fn main() -> ! { let tx_start = frame * 8; let rx_start = rx_frame * 8; - write!(uart, " RX[{:2}]: ", rx_frame).unwrap(); + uwrite!(uart, " RX[{:02x}]: ", rx_frame).unwrap(); let mut frame_matches = true; for byte_idx in 0..8 { let expected = tx_pattern[tx_start + byte_idx]; let actual = rx_bytes[rx_start + byte_idx]; - write!(uart, "{:02x} ", actual).unwrap(); + uwrite!(uart, "{:02x} ", actual).unwrap(); if actual != expected { frame_matches = false; } } - writeln!(uart, "{}", if frame_matches { "✓" } else { "✗" }).unwrap(); + uwriteln!(uart, "{}", if frame_matches { "✓" } else { "✗" }).unwrap(); } + uwriteln!(uart, "Test done").unwrap(); } } else { - writeln!( + uwriteln!( uart, "\n*** TEST FAILED: First frame not found in RX buffer ***" ) .unwrap(); - writeln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); + uwriteln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); for frame in 0..RINGBUFFER_SIZE { let start = frame * 8; - write!(uart, " TX[{:2}]: ", frame).unwrap(); + uwrite!(uart, " TX[{}]: ", frame).unwrap(); for byte_idx in 0..8 { - write!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); + uwrite!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); } - writeln!(uart).unwrap(); + uwriteln!(uart, "").unwrap(); } - writeln!(uart, "\nRX buffer contents (byte-by-byte):").unwrap(); + uwriteln!(uart, "\nRX buffer contents (byte-by-byte):").unwrap(); for frame in 0..RINGBUFFER_SIZE { let start = frame * 8; - write!(uart, " RX[{:2}]: ", frame).unwrap(); + uwrite!(uart, " RX[{:02x}]: ", frame).unwrap(); for byte_idx in 0..8 { - write!(uart, "{:02x} ", rx_bytes[start + byte_idx]).unwrap(); + uwrite!(uart, "{:02x} ", rx_bytes[start + byte_idx]).unwrap(); } - writeln!(uart).unwrap(); + uwriteln!(uart, "").unwrap(); } + uwriteln!(uart, "Test done").unwrap(); } loop { diff --git a/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs b/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs index badf44426..3f455b0b6 100644 --- a/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs +++ b/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs @@ -63,6 +63,7 @@ fn main() -> ! { writeln!(uart, "Read from scatter memory:").unwrap(); writeln!(uart, "{:?}", destination).unwrap(); } + writeln!(uart, "Test done").unwrap(); loop { continue; } diff --git a/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml b/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml index a2dae01d9..8b8e8a4d9 100644 --- a/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml +++ b/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml @@ -19,7 +19,7 @@ ufmt = "0.2.0" [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_info"] +features = ["max_level_trace", "release_max_level_trace"] [build-dependencies] memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-support/Cargo.lock b/firmware-support/Cargo.lock index 24deb6ec4..4c9ad547a 100644 --- a/firmware-support/Cargo.lock +++ b/firmware-support/Cargo.lock @@ -89,6 +89,7 @@ name = "bittide-sys" version = "0.1.0" dependencies = [ "bittide-hal", + "crc", "fdt", "heapless", "itertools", @@ -102,6 +103,7 @@ dependencies = [ "tempfile", "test-strategy", "ufmt", + "zerocopy 0.7.35", ] [[package]] @@ -135,6 +137,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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -412,7 +429,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.30", ] [[package]] @@ -864,13 +881,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-hal-c/include/bittide_ring_transmit.h b/firmware-support/bittide-hal-c/include/bittide_ring_transmit.h index 359015a52..84d27f77e 100644 --- a/firmware-support/bittide-hal-c/include/bittide_ring_transmit.h +++ b/firmware-support/bittide-hal-c/include/bittide_ring_transmit.h @@ -41,7 +41,7 @@ */ static inline void transmit_ringbuffer_write_slice_unchecked( TransmitRingbuffer unit, uint64_t *src, uint32_t offset, uint32_t len) { - for (uint32_t i = 0; i < len; i++) { + for (uint32_t i = 0; i < len; i++) { transmit_ringbuffer_set_data_unchecked(unit, offset + i, (uint8_t const *)&src[i]); } diff --git a/firmware-support/bittide-hal/Cargo.toml b/firmware-support/bittide-hal/Cargo.toml index 3381e81c5..3e93274ad 100644 --- a/firmware-support/bittide-hal/Cargo.toml +++ b/firmware-support/bittide-hal/Cargo.toml @@ -19,7 +19,7 @@ ufmt = "0.2.0" git = "https://github.com/smoltcp-rs/smoltcp.git" rev = "dc08e0b42e668c331bb2b6f8d80016301d0efe03" 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/mod.rs b/firmware-support/bittide-hal/src/manual_additions/mod.rs index 5da1ace56..7e999178a 100644 --- a/firmware-support/bittide-hal/src/manual_additions/mod.rs +++ b/firmware-support/bittide-hal/src/manual_additions/mod.rs @@ -8,9 +8,9 @@ pub mod calendar; pub mod capture_ugn; pub mod dna; pub mod elastic_buffer; +pub mod ringbuffer; pub mod scatter_gather_pe; pub mod si539x_spi; -pub mod soft_ugn_demo_mu; pub mod timer; pub mod uart; diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs new file mode 100644 index 000000000..d0ef7f238 --- /dev/null +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs @@ -0,0 +1,310 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use log::{debug, trace, warn}; + +const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; +const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; + +pub trait TransmitRingbufferInterface { + const DATA_LEN: usize; + + /// Get a pointer to the base address of the TransmitRingbuffer + fn base_ptr(&self) -> *mut [u8; 8]; + + /// Write a slice to the transmit buffer at the given offset. The slice must not exceed the buffer length when combined with the offset. + fn write_slice(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() + offset <= Self::DATA_LEN); + trace!( + "ringbuffer tx write_slice len {} offset {}", + src.len(), + offset + ); + unsafe { + self.write_slice_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer at the given offset without checking bounds. The caller must ensure that `src.len() + offset` does not exceed the buffer length. + /// + /// # Safety + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. + unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { + let dst_ptr = self.base_ptr().add(offset); + let src_ptr = src.as_ptr(); + if (src_ptr as usize) % 4 != 0 || (dst_ptr as usize) % 4 != 0 { + warn!( + "ringbuffer tx write_slice_unchecked unaligned: src {:p} dst {:p}", + src_ptr, dst_ptr + ); + } + + core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, src.len()); + } + + /// Write a slice to the transmit buffer wrapping if `src.len() + offset`exceeds the length + /// of the buffer. + /// + /// # Panic + /// Panics if the length of the source exceeds the length of the buffer. + fn write_slice_with_wrap(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() <= Self::DATA_LEN); + unsafe { + self.write_slice_with_wrap_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer without bounds checking. It wraps if `src.len() + offset`exceeds the length + /// of the buffer. + /// + /// # Safety + /// Caller must ensure that the size of the source does not exceed the buffer length. + unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { + if src.len() + offset <= Self::DATA_LEN { + self.write_slice(src, offset); + } else { + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = src.split_at(first_part_len); + debug!( + "ringbuffer tx write_slice_with_wrap offset {} first {} second {}", + offset, + first.len(), + second.len() + ); + self.write_slice(first, offset); + self.write_slice(second, 0); + } + } + + fn clear(&self) { + debug!("ringbuffer tx clear len {}", Self::DATA_LEN); + let zero = [[0u8; 8]; 1]; + for i in 0..Self::DATA_LEN { + self.write_slice(&zero, i); + } + } +} + +pub trait ReceiveRingbufferInterface { + const DATA_LEN: usize; + + fn base_ptr(&self) -> *const [u8; 8]; + + fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= Self::DATA_LEN); + trace!( + "ringbuffer rx read_slice len {} offset {}", + dst.len(), + offset + ); + unsafe { + self.read_slice_unchecked(dst, offset); + } + } + + /// Read a slice from the buffer into the destination without bounds checking. + /// + /// # Safety + /// Will fail if the requested size exceeds the size of the buffer. + unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + let dst_ptr = dst.as_mut_ptr(); + let src_ptr = self.base_ptr().add(offset); + if (src_ptr as usize) % 4 != 0 || (dst_ptr as usize) % 4 != 0 { + warn!( + "ringbuffer rx read_slice_unchecked unaligned: src {:p} dst {:p}", + src_ptr, dst_ptr + ); + } + core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, dst.len()); + } + + /// Read a slice from the buffer, wrapping around if the `dst.len() + offset` exceeds the + /// length of the buffer. + /// + /// # Panics + /// Will panic if the destination requests more bytes than the size of the buffer. + fn read_slice_with_wrap(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() <= Self::DATA_LEN); + unsafe { + self.read_slice_with_wrap_unchecked(dst, offset); + } + } + + /// Read a slice from the buffer with wrapping, but no bounds checking + /// + /// # Safety + /// Will fail if the destination requests more bytes than the size of the buffer. + unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + if dst.len() + offset <= Self::DATA_LEN { + self.read_slice(dst, offset); + } else { + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = dst.split_at_mut(first_part_len); + debug!( + "ringbuffer rx read_slice_with_wrap offset {} first {} second {}", + offset, + first.len(), + second.len() + ); + self.read_slice(first, offset); + self.read_slice(second, 0); + } + } +} + +macro_rules! impl_ringbuffer_interfaces { + (rx: $rx:ty, tx: $tx:ty) => { + const _: () = { + if <$rx>::DATA_LEN != <$tx>::DATA_LEN { + const_panic::concat_panic!( + "Ringbuffer sizes do not match for ", + stringify!($rx), + " and ", + stringify!($tx) + ); + } + }; + + impl ReceiveRingbufferInterface for $rx { + const DATA_LEN: usize = <$rx>::DATA_LEN; + + fn base_ptr(&self) -> *const [u8; 8] { + self.0.cast::<[u8; 8]>() + } + } + + impl TransmitRingbufferInterface for $tx { + const DATA_LEN: usize = <$tx>::DATA_LEN; + + fn base_ptr(&self) -> *mut [u8; 8] { + self.0.cast::<[u8; 8]>() + } + } + }; +} + +impl_ringbuffer_interfaces! { + rx: crate::hals::ringbuffer_test::devices::ReceiveRingbuffer, + tx: crate::hals::ringbuffer_test::devices::TransmitRingbuffer +} + +impl_ringbuffer_interfaces! { + rx: crate::hals::soft_ugn_demo_mu::devices::ReceiveRingbuffer, + tx: crate::hals::soft_ugn_demo_mu::devices::TransmitRingbuffer +} + +pub struct AlignedReceiveBuffer +where + Rx: ReceiveRingbufferInterface, + Tx: TransmitRingbufferInterface, +{ + pub rx: Rx, + rx_alignment_offset: Option, + tx_reference: usize, + _tx: core::marker::PhantomData, +} + +impl AlignedReceiveBuffer +where + Rx: ReceiveRingbufferInterface, + Tx: TransmitRingbufferInterface, +{ + pub fn new(rx: Rx) -> Self { + debug!("ringbuffer aligned receive buffer new"); + Self { + rx, + rx_alignment_offset: None, + tx_reference: 0, + _tx: core::marker::PhantomData, + } + } + + pub fn is_aligned(&self) -> bool { + self.rx_alignment_offset.is_some() + } + + pub fn get_alignment_offset(&self) -> Option { + self.rx_alignment_offset + } + + pub fn align(&mut self, tx: &Tx) { + debug!("ringbuffer align start"); + tx.clear(); + let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; + tx.write_slice(&announce_pattern, 0); + + let rx_offset = 'outer: loop { + for rx_idx in 0..Rx::DATA_LEN { + let mut data_buf = [[0u8; 8]; 1]; + self.rx.read_slice(&mut data_buf, rx_idx); + let value = u64::from_le_bytes(data_buf[0]); + + if value == ALIGNMENT_ANNOUNCE || value == ALIGNMENT_ACKNOWLEDGE { + debug!("ringbuffer align marker at rx_idx {}", rx_idx); + break 'outer rx_idx; + } + } + }; + + let ack_pattern = [ALIGNMENT_ACKNOWLEDGE.to_le_bytes()]; + tx.write_slice(&ack_pattern, 0); + + loop { + let mut data_buf = [[0u8; 8]; 1]; + self.rx.read_slice(&mut data_buf, rx_offset); + let value = u64::from_le_bytes(data_buf[0]); + + if value == ALIGNMENT_ACKNOWLEDGE { + debug!("ringbuffer align ack at rx_idx {}", rx_offset); + break; + } + } + self.rx_alignment_offset = Some(rx_offset); + self.tx_reference = tx.base_ptr() as *const _ as usize; + debug!("ringbuffer align complete offset {}", rx_offset); + } + + pub fn clear_alignment(&mut self) { + debug!("ringbuffer clear alignment"); + self.rx_alignment_offset = None; + self.tx_reference = 0; + } + + pub fn verify_aligned_to(&self, tx: &Tx) -> bool { + let tx_addr = tx.base_ptr() as *const _ as usize; + let aligned = self.is_aligned() && self.tx_reference == tx_addr; + if !aligned { + warn!( + "ringbuffer verify aligned failed: aligned {} tx_ref 0x{:x} tx_addr 0x{:x}", + self.is_aligned(), + self.tx_reference, + tx_addr + ); + } + aligned + } + + pub fn get_alignment_reference(&self) -> usize { + self.tx_reference + } + + pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= Rx::DATA_LEN); + let rx_offset = self + .rx_alignment_offset + .expect("Alignment offset not discovered yet. Call align() first."); + let mut aligned_offset = offset + rx_offset; + if aligned_offset >= Rx::DATA_LEN { + aligned_offset -= Rx::DATA_LEN; + } + trace!( + "ringbuffer aligned read len {} offset {} aligned_offset {}", + dst.len(), + offset, + aligned_offset + ); + self.rx.read_slice_with_wrap(dst, aligned_offset) + } +} diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs deleted file mode 100644 index a09b14ea3..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 -pub mod scatter_gather; diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/scatter_gather.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/scatter_gather.rs deleted file mode 100644 index 46b60d165..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/scatter_gather.rs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 -use crate::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; - -impl TransmitRingbuffer { - /// Write a slice to the transmit ringbuffer memory. - /// - /// # Panics - /// - /// The source memory size must be smaller or equal to the memory size of - /// the `TransmitRingbuffer` memory. - pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { - assert!(src.len() + offset <= Self::DATA_LEN); - let mut off = offset; - for &val in src { - unsafe { - self.set_data_unchecked(off, val); - } - off += 1; - } - } -} - -impl ReceiveRingbuffer { - /// Read a slice from the receive ringbuffer memory. - /// - /// # Panics - /// - /// The destination memory size must be smaller or equal to the memory size - /// of the `ReceiveRingbuffer`. - pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() + offset <= Self::DATA_LEN); - let mut off = offset; - for val in dst { - unsafe { - *val = self.data_unchecked(off); - } - off += 1; - } - } -} diff --git a/firmware-support/bittide-sys/Cargo.toml b/firmware-support/bittide-sys/Cargo.toml index e58d240d0..6870636b0 100644 --- a/firmware-support/bittide-sys/Cargo.toml +++ b/firmware-support/bittide-sys/Cargo.toml @@ -21,6 +21,8 @@ log = "0.4.21" ufmt = "0.2.0" bittide-hal = { path = "../bittide-hal" } itertools = { version = "0.14.0", default-features = false } +crc = { version = "3.0", default-features = false } +zerocopy = { version = "0.7", default-features = false, features = ["byteorder", "derive"] } [dependencies.heapless] version = "0.8" @@ -34,7 +36,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] proptest = "1.0" diff --git a/firmware-support/bittide-sys/src/lib.rs b/firmware-support/bittide-sys/src/lib.rs index b6084b77d..ac267ca77 100644 --- a/firmware-support/bittide-sys/src/lib.rs +++ b/firmware-support/bittide-sys/src/lib.rs @@ -4,11 +4,14 @@ #![no_std] #![feature(sync_unsafe_cell)] +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] pub mod axi; pub mod callisto; pub mod link_startup; pub mod mac; +pub mod net_state; pub mod program_stream; pub mod sample_store; pub mod smoltcp; 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 000000000..83c8ca0ce --- /dev/null +++ b/firmware-support/bittide-sys/src/net_state.rs @@ -0,0 +1,584 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use log::{debug, info, trace, warn}; +use smoltcp::iface::{Interface, SocketHandle, SocketSet}; +use smoltcp::socket::tcp; +use smoltcp::wire::IpAddress; +use zerocopy::byteorder::{I64, LE, U32}; +use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref, 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 = 8; +pub const UGN_EDGE_BYTES: usize = core::mem::size_of::(); + +const DNA_BYTES: usize = 12; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct UgnEdge { + pub src_node: u32, + pub src_port: u32, + pub dst_node: u32, + pub dst_port: u32, + pub ugn: i64, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct UgnReport { + pub count: u8, + pub edges: [Option; MAX_UGN_EDGES], +} + +impl UgnReport { + pub fn new() -> Self { + Self { + count: 0, + edges: [None; MAX_UGN_EDGES], + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] +struct UgnEdgeWire { + src_node: U32, + dst_node: U32, + src_port: U32, + dst_port: U32, + ugn: I64, +} + +impl From for UgnEdgeWire { + fn from(edge: UgnEdge) -> Self { + Self { + src_node: U32::new(edge.src_node), + dst_node: U32::new(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.get(), + dst_node: edge.dst_node.get(), + src_port: edge.src_port.get(), + dst_port: edge.dst_port.get(), + ugn: edge.ugn.get(), + } + } +} + +// const _: [(); UGN_EDGE_BYTES] = [(); core::mem::size_of::()]; + +const TCP_SERVER_PORT: u16 = 8080; +const TCP_CLIENT_PORT: u16 = 49152; + +pub struct Manager { + iface: Interface, + socket_handle: SocketHandle, + link: usize, + partner_ip: [u8; 4], + state: ManagerState, + retries: u8, + report: UgnReport, + partner_dna: Option<[u8; DNA_BYTES]>, +} + +impl Manager { + pub fn new( + iface: Interface, + socket_handle: SocketHandle, + link: usize, + partner_ip: [u8; 4], + ) -> Self { + Self { + iface, + socket_handle, + link, + partner_ip, + state: ManagerState::WaitForSession, + retries: 0, + report: UgnReport::new(), + partner_dna: None, + } + } + + pub fn iface(&self) -> &Interface { + &self.iface + } + + pub fn iface_mut(&mut self) -> &mut Interface { + &mut self.iface + } + + pub fn step(&mut self, sockets: &mut SocketSet) { + let prev_state = self.state; + let next_state = match self.state { + ManagerState::WaitForSession => { + self.connect(sockets); + if self.can_receive(sockets) { + debug!( + "manager connected to peer {:?} on link {}", + self.partner_ip, self.link + ); + ManagerState::Identifying + } else { + self.state + } + } + ManagerState::Identifying => { + // Expect the subordinate to identify itself + + let mut buf = [0u8; DNA_BYTES]; + match self.recv(sockets, &mut buf) { + Some(len) if len == DNA_BYTES => { + self.partner_dna = Some(buf); + info!( + "manager received partner DNA {:02X?} on link {}", + self.partner_dna, self.link + ); + ManagerState::ReceivingUgns + } + Some(len) => { + warn!( + "manager received invalid identify len {} on link {}", + len, self.link + ); + self.reset(sockets); + self.bump_retry_or_fail(ManagerState::Identifying) + } + None => self.state, + } + } + ManagerState::ReceivingUgns => { + trace!("manager receiving ugns on link {}", self.link); + if self.can_receive(sockets) { + let mut buf = [0u8; UGN_EDGE_BYTES]; + match self.recv(sockets, &mut buf) { + Some(len) if len == UGN_EDGE_BYTES => match parse_ugn_edge(&buf[..len]) { + Some(edge) => { + info!( + "manager received edge src {} dst {} ugn {} on link {}", + edge.src_node, edge.dst_node, edge.ugn, self.link + ); + self.insert_edge(edge); + self.state + } + None => { + warn!("manager received invalid ugn edge on link {}", self.link); + self.reset(sockets); + self.bump_retry_or_fail(ManagerState::ReceivingUgns) + } + }, + Some(len) => { + warn!( + "manager received invalid ugn edge len {} on link {}", + len, self.link + ); + self.reset(sockets); + self.bump_retry_or_fail(ManagerState::ReceivingUgns) + } + None => self.state, + } + } else { + ManagerState::Done + } + } + ManagerState::Done => self.state, + ManagerState::Failed => self.state, + }; + if next_state != prev_state { + info!("manager state {:?} -> {:?}", prev_state, next_state); + } + self.state = next_state; + } + + pub fn poll( + &mut self, + timestamp: smoltcp::time::Instant, + device: &mut impl smoltcp::phy::Device, + sockets: &mut SocketSet, + ) { + self.step(sockets); + self.iface.poll(timestamp, device, sockets); + } + + pub fn is_done(&self) -> bool { + self.state == ManagerState::Done + } + + pub fn is_connected(&self) -> bool { + self.is_done() + } + + pub fn state(&self) -> ManagerState { + self.state + } + + pub fn report(&self) -> UgnReport { + self.report + } + + fn reset(&mut self, sockets: &mut SocketSet) { + let socket = sockets.get_mut::(self.socket_handle); + debug!("link {} resetting interface", self.link); + socket.close(); + } + + fn connect(&mut self, sockets: &mut SocketSet) -> bool { + let socket = sockets.get_mut::(self.socket_handle); + if !socket.is_open() && !socket.is_active() { + let cx = self.iface.context(); + match socket.connect( + cx, + ( + IpAddress::v4( + self.partner_ip[0], + self.partner_ip[1], + self.partner_ip[2], + self.partner_ip[3], + ), + TCP_SERVER_PORT, + ), + TCP_CLIENT_PORT, + ) { + Ok(()) => { + debug!( + "link {} connect requested to {:?}:{} from {}", + self.link, self.partner_ip, TCP_SERVER_PORT, TCP_CLIENT_PORT + ); + } + Err(err) => { + debug!("link {} connect error: {:?}", self.link, err); + } + } + } + trace!( + "link {} connect state open {} active {} can_send {} can_recv {} may_recv {}", + self.link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.may_recv() + ); + socket.is_active() + } + + fn recv(&mut self, sockets: &mut SocketSet, buf: &mut [u8]) -> Option { + let socket = sockets.get_mut::(self.socket_handle); + if socket.can_recv() { + match socket.recv_slice(buf) { + Ok(len) => { + trace!("link {} recv {} bytes", self.link, len); + return Some(len); + } + Err(err) => { + debug!("link {} recv error: {:?}", self.link, err); + } + } + } else { + trace!( + "link {} recv blocked open {} active {}", + self.link, + socket.is_open(), + socket.is_active() + ); + } + None + } + + fn can_receive(&self, sockets: &SocketSet) -> bool { + let socket = sockets.get::(self.socket_handle); + socket.may_recv() + } + + fn bump_retry_or_fail(&mut self, retry_state: ManagerState) -> ManagerState { + self.retries = self.retries.saturating_add(1); + if self.retries >= 3 { + warn!("manager retry limit hit, entering Failed"); + ManagerState::Failed + } else { + retry_state + } + } + + fn insert_edge(&mut self, edge: UgnEdge) -> bool { + if self.report.count >= MAX_UGN_EDGES as u8 { + warn!("manager received edge but report is full, ignoring"); + return false; + } + self.report.edges[self.report.count as usize] = Some(edge); + self.report.count = self.report.count.saturating_add(1); + true + } +} + +pub struct Subordinate { + iface: Interface, + socket_handle: SocketHandle, + link: usize, + state: SubordinateState, + report: UgnReport, + sent_edges: [bool; MAX_UGN_EDGES], + sent_count: u8, + dna: [u8; DNA_BYTES], +} + +impl Subordinate { + pub fn new( + iface: Interface, + socket_handle: SocketHandle, + link: usize, + dna: [u8; DNA_BYTES], + ) -> Self { + Self { + iface, + socket_handle, + link, + state: SubordinateState::WaitForSession, + report: UgnReport::new(), + sent_edges: [false; MAX_UGN_EDGES], + sent_count: 0, + dna, + } + } + + pub fn iface(&self) -> &Interface { + &self.iface + } + + pub fn iface_mut(&mut self) -> &mut Interface { + &mut self.iface + } + + pub fn set_report(&mut self, report: UgnReport) { + self.report = report; + self.sent_edges = [false; MAX_UGN_EDGES]; + self.sent_count = 0; + } + + pub fn insert_edge(&mut self, edge: UgnEdge) -> bool { + if self.report.count >= MAX_UGN_EDGES as u8 { + warn!("subordinate received edge but report is full, ignoring"); + return false; + } + self.report.edges[self.report.count as usize] = Some(edge); + self.report.count = self.report.count.saturating_add(1); + true + } + + pub fn step(&mut self, sockets: &mut SocketSet) { + let prev_state = self.state; + let next_state = match self.state { + SubordinateState::WaitForSession => { + trace!( + "subordinate state {:?} attempting listen on link {}", + self.state, + self.link + ); + if self.listen(sockets) { + debug!("subordinate listening on link {}", self.link); + + SubordinateState::Identifying + } else { + self.state + } + } + SubordinateState::Identifying => { + if self.can_send(sockets) { + let dna = self.dna; + if self.send(sockets, &dna) { + debug!( + "subordinate sent dna {:02X?} on link {}", + self.dna, self.link + ); + SubordinateState::SendingUgns + } else { + self.state + } + } else { + self.state + } + } + SubordinateState::SendingUgns => { + trace!( + "subordinate state {:?} checking for manager connection on link {}", + self.state, + self.link + ); + if self.can_send(sockets) { + if let Some((idx, edge)) = self.next_unsent_edge() { + let edge_bytes = encode_ugn_edge(edge); + if self.send(sockets, &edge_bytes) { + debug!( + "subordinate sent edge idx {} src {} dst {} ugn {} on link {}", + idx, edge.src_node, edge.dst_node, edge.ugn, self.link + ); + self.sent_edges[idx] = true; + self.sent_count = self.sent_count.saturating_add(1); + } + } + if self.sending_done() { + info!("Closing link {}", self.link); + self.close(sockets); + SubordinateState::Done + } else { + self.state + } + } else { + self.state + } + } + SubordinateState::Done => self.state, + SubordinateState::Failed => self.state, + }; + if next_state != prev_state { + info!("subordinate state {:?} -> {:?}", prev_state, next_state); + } + self.state = next_state; + } + + pub fn poll( + &mut self, + timestamp: smoltcp::time::Instant, + device: &mut impl smoltcp::phy::Device, + sockets: &mut SocketSet, + ) { + self.iface.poll(timestamp, device, sockets); + self.step(sockets); + } + + pub fn is_done(&self) -> bool { + self.state == SubordinateState::Done + } + + pub fn is_connected(&self) -> bool { + self.is_done() + } + + pub fn state(&self) -> SubordinateState { + self.state + } + + fn listen(&mut self, sockets: &mut SocketSet) -> bool { + let socket = sockets.get_mut::(self.socket_handle); + if !socket.is_open() { + match socket.listen(TCP_SERVER_PORT) { + Ok(()) => { + debug!("link {} listen on port {}", self.link, TCP_SERVER_PORT); + } + Err(err) => { + debug!("link {} listen error: {:?}", self.link, err); + } + } + } + trace!( + "link {} listen state open {} active {} can_send {} can_recv {} may_recv {}", + self.link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.may_recv() + ); + socket.is_open() + } + + fn send(&mut self, sockets: &mut SocketSet, data: &[u8]) -> bool { + let socket = sockets.get_mut::(self.socket_handle); + if socket.can_send() { + match socket.send_slice(data) { + Ok(len) => { + trace!("link {} sent {} bytes", self.link, len); + return true; + } + Err(err) => { + debug!("link {} send error: {:?}", self.link, err); + } + } + } else { + trace!( + "link {} send blocked open {} active {}", + self.link, + socket.is_open(), + socket.is_active() + ); + } + false + } + + fn close(&mut self, sockets: &mut SocketSet) { + let socket = sockets.get_mut::(self.socket_handle); + socket.close(); + } + + fn next_unsent_edge(&self) -> Option<(usize, UgnEdge)> { + let limit = core::cmp::min(self.report.count as usize, self.report.edges.len()); + for idx in 0..limit { + if !self.sent_edges[idx] { + if let Some(edge) = self.report.edges[idx] { + return Some((idx, edge)); + } + } + } + None + } + + fn sending_done(&self) -> bool { + self.sent_count as usize >= self.report.count as usize + } + + fn can_send(&self, sockets: &SocketSet) -> bool { + let socket = sockets.get::(self.socket_handle); + socket.can_send() + } +} + +fn encode_ugn_edge(edge: UgnEdge) -> [u8; UGN_EDGE_BYTES] { + let wire: UgnEdgeWire = edge.into(); + let mut buf = [0u8; UGN_EDGE_BYTES]; + buf.copy_from_slice(wire.as_bytes()); + buf +} + +fn parse_ugn_edge(msg: &[u8]) -> Option { + let (wire, _) = Ref::<_, UgnEdgeWire>::new_from_prefix(msg)?; + Some(UgnEdge::from(*wire)) +} + +// pub fn ip_for_link(role: NodeRole, port: usize) -> [u8; 4] { +// let host = match role { +// NodeRole::Manager => 1, +// NodeRole::Subordinate => 2, +// }; +// [10, 0, port as u8, host] +// } diff --git a/firmware-support/bittide-sys/src/smoltcp.rs b/firmware-support/bittide-sys/src/smoltcp/mac.rs similarity index 98% rename from firmware-support/bittide-sys/src/smoltcp.rs rename to firmware-support/bittide-sys/src/smoltcp/mac.rs index 6eaa828e6..082cf5d23 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; 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 000000000..29b1f0fbe --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/mod.rs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +pub mod axi; +pub mod mac; +pub mod ringbuffer; diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs new file mode 100644 index 000000000..44d19d07e --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -0,0 +1,347 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +//! smoltcp Device implementation for aligned ringbuffers. +//! +//! This module provides a `Device` implementation that uses scatter/gather units +//! as ringbuffers for point-to-point communication. The ringbuffers 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 ringbuffer is volatile - its contents can change at any time due to +//! incoming data. To safely handle this, we: +//! 1. Read the packet header to get CRC, sequence number, and length +//! 2. Copy the entire packet (header + payload) to a local buffer +//! 3. Verify the CRC32 over sequence + length + payload +//! 4. If CRC validates, extract payload and consume packet +//! 5. Track sequence numbers to detect repeated packets +use bittide_hal::manual_additions::aligned::Aligned4; +use bittide_hal::manual_additions::ringbuffer::{ + 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; + +/// CRC-32 (Castagnoli) instance for packet integrity checking.lgienalized at compile-time for zero-cost runtime usage. +const CRC: Crc = Crc::::new(&CRC_32_ISCSI); + +/// Verify packet integrity by checking the CRC32 in the header. +/// +/// Returns true if the CRC stored in the first 4 bytes matches the calculated +/// CRC over bytes[4..] (sequence + length + payload). Uses unaligned-safe reads. +/// +/// # Arguments +/// * `buffer` - Complete packet buffer including header and payload +fn is_valid(buffer: &[u8]) -> bool { + if buffer.len() < PACKET_HEADER_SIZE { + return false; + } + let stored_crc = u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]); + let calculated_crc = CRC.checksum(&buffer[4..]); + stored_crc == calculated_crc +} + +/// Device implementation for ringbuffer communication. +/// +/// Provides a simple interface for point-to-point IP communication using +/// scatter/gather units. The ringbuffers 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 +where + Rx: ReceiveRingbufferInterface, + Tx: TransmitRingbufferInterface, +{ + rx_buffer: AlignedReceiveBuffer, + tx_buffer: Tx, + mtu: usize, + /// Last valid sequence number we saw (to detect repeated packets) + last_rx_seq: u16, + /// Transmit sequence number (incremented for each packet sent) + tx_seq_num: u16, +} + +impl RingbufferDevice +where + Rx: ReceiveRingbufferInterface + 'static, + Tx: TransmitRingbufferInterface + 'static, +{ + /// Create a new ringbuffer device. + /// + /// The ringbuffers 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(1500).min(Tx::DATA_LEN * 8) - PACKET_HEADER_SIZE; + assert!(rx_buffer.is_aligned(), "RX buffer is not aligned "); + debug!( + "ringbuffer device init rx_words {} tx_words {} mtu {}", + Rx::DATA_LEN, + Tx::DATA_LEN, + mtu + ); + + Self { + rx_buffer, + tx_buffer, + mtu, + last_rx_seq: u16::MAX, + 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, + [(); Rx::DATA_LEN * 8]:, + [(); Tx::DATA_LEN * 8]:, +{ + type RxToken<'a> + = RxToken<{ Rx::DATA_LEN * 8 }> + where + [(); Rx::DATA_LEN * 8]:; + type TxToken<'a> + = TxToken<'a, Tx, { Tx::DATA_LEN * 8 }> + where + [(); Tx::DATA_LEN * 8]:; + + 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<'_>)> { + // Allocate aligned buffer for reading from ringbuffer + trace!("Creating packet_buffer"); + let mut packet_buffer = Aligned4::new([[0u8; 8]; Rx::DATA_LEN]); + + trace!("Reading packet header from RX buffer"); + // Read first word containing the header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + self.rx_buffer + .read_slice(&mut packet_buffer.get_mut()[0..1], 0); + + // Extract CRC, sequence number, and length from header using direct pointer reads + trace!("Extracting header fields"); + let header_ptr = packet_buffer.get()[0].as_ptr(); + let _stored_crc = unsafe { (header_ptr as *const u32).read_unaligned() }; + let seq_num = unsafe { (header_ptr.add(4) as *const u16).read_unaligned() }; + let packet_len = unsafe { (header_ptr.add(6) as *const u16).read_unaligned() } as usize; + + // Check if this is the same packet we saw before (based on sequence number) + if seq_num == self.last_rx_seq { + trace!("Detected repeated packet with seq {}", seq_num); + return None; + } + + // Validate packet length + if packet_len < MIN_IP_PACKET_SIZE || packet_len > self.mtu { + warn!( + "Invalid packet length: {} (must be {}-{})", + packet_len, MIN_IP_PACKET_SIZE, self.mtu + ); + return None; + } + + // Calculate total packet size: header + payload + let total_len = PACKET_HEADER_SIZE + packet_len; + let num_words = total_len.div_ceil(8); + trace!( + "rx packet seq {} len {} total {} words {}", + seq_num, + packet_len, + total_len, + num_words + ); + + // Read remaining words if any (we already read the first word) + if num_words > 1 { + trace!("Reading packet payload from RX buffer"); + self.rx_buffer + .read_slice(&mut packet_buffer.get_mut()[1..num_words], 1); + } + + // Flatten to bytes for CRC validation + trace!("Flattening packet buffer to byte slice for CRC validation"); + let packet_bytes = unsafe { + core::slice::from_raw_parts(packet_buffer.get().as_ptr() as *const u8, total_len) + }; + + // Validate CRC + if !is_valid(packet_bytes) { + trace!("CRC validation failed for packet seq {}", seq_num); + return None; + } + + trace!( + "Valid packet: seq {}, payload {} bytes", + seq_num, + packet_len + ); + self.last_rx_seq = seq_num; + + // Extract payload (skip header) + trace!("Creating slice of payload bytes"); + let mut payload = Aligned4::new([0u8; Rx::DATA_LEN * 8]); + let payload_bytes = unsafe { + core::slice::from_raw_parts( + (packet_buffer.get().as_ptr() as *const u8).add(PACKET_HEADER_SIZE), + packet_len, + ) + }; + + trace!("Copying payload to local buffer"); + payload.get_mut()[..packet_len].copy_from_slice(payload_bytes); + + let rx = RxToken { + buffer: payload, + length: 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> { + Some(TxToken { + tx_buffer: &mut self.tx_buffer, + mtu: self.mtu, + seq_num: &mut self.tx_seq_num, + }) + } +} + +/// Receive token for ringbuffer device. +/// +/// Contains a local copy of the packet payload that has been validated +/// against CRC32 corruption. +pub struct RxToken { + buffer: Aligned4<[u8; RX_BYTES]>, + length: usize, +} + +impl phy::RxToken for RxToken { + fn consume(self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + trace!("Consuming validated packet ({} bytes)", self.length); + f(&self.buffer.get()[..self.length]) + } +} + +/// Transmit token for ringbuffer device +pub struct TxToken<'a, Tx, const TX_WORDS: usize> +where + Tx: TransmitRingbufferInterface, +{ + tx_buffer: &'a mut Tx, + mtu: usize, + seq_num: &'a mut u16, +} + +impl phy::TxToken for TxToken<'_, Tx, TX_WORDS> +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); + + // Prepare aligned buffer: header + payload + let mut buffer = Aligned4::new([[0u8; 8]; TX_WORDS]); + + // Write header fields using direct pointer writes + // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + trace!("Writing header fields to buffer"); + let header_ptr = buffer.get_mut().as_mut_ptr() as *mut u8; + unsafe { + // Write sequence and length (CRC written later after payload) + (header_ptr.add(4) as *mut u16).write_unaligned(self.seq_num.to_le()); + (header_ptr.add(6) as *mut u16).write_unaligned((len as u16).to_le()); + } + + // Let smoltcp fill the packet data + trace!("Creating payload slice for smoltcp"); + let payload_slice = + unsafe { core::slice::from_raw_parts_mut(header_ptr.add(PACKET_HEADER_SIZE), len) }; + trace!("Filling payload using provided closure"); + let result = f(payload_slice); + + // Calculate total length and seal packet with CRC in header + trace!("Calculating CRC for packet"); + let total_len = PACKET_HEADER_SIZE + len; + let crc_data = unsafe { core::slice::from_raw_parts(header_ptr.add(4), total_len - 4) }; + let crc = CRC.checksum(crc_data); + + trace!("Writing CRC to header"); + unsafe { + (header_ptr as *mut u32).write_unaligned(crc.to_le()); + } + + // Calculate number of words needed + let num_words = total_len.div_ceil(8); + trace!( + "tx packet len {} total {} words {}", + len, + total_len, + num_words + ); + + // Write to ringbuffer starting at offset 0 + self.tx_buffer.write_slice(&buffer.get()[..num_words], 0); + + trace!( + "Transmitted packet: checksum {}, seq {}, total length {}", + crc, + self.seq_num, + total_len, + ); + *self.seq_num = self.seq_num.wrapping_add(1); + + result + } +} diff --git a/gdb-hs/src/Gdb.hs b/gdb-hs/src/Gdb.hs index ad886759d..3002cda6a 100644 --- a/gdb-hs/src/Gdb.hs +++ b/gdb-hs/src/Gdb.hs @@ -4,6 +4,8 @@ module Gdb ( -- * GDB process management Gdb, + start, + stop, withGdb, withGdbs,