From e7c9b2a332c6666ad96d0cfcae05786d8255e873 Mon Sep 17 00:00:00 2001 From: Hidde Moll Date: Thu, 5 Feb 2026 15:48:35 +0100 Subject: [PATCH 1/4] Add programmable mux --- bittide/bittide.cabal | 2 + bittide/src/Bittide/ProgrammableMux.hs | 85 +++++++++++++++++++++ bittide/tests/Tests/ProgrammableMux.hs | 100 +++++++++++++++++++++++++ bittide/tests/UnitTests.hs | 2 + 4 files changed, 189 insertions(+) create mode 100644 bittide/src/Bittide/ProgrammableMux.hs create mode 100644 bittide/tests/Tests/ProgrammableMux.hs diff --git a/bittide/bittide.cabal b/bittide/bittide.cabal index c43a74f47..0848ee4ed 100644 --- a/bittide/bittide.cabal +++ b/bittide/bittide.cabal @@ -223,6 +223,7 @@ library Bittide.ProcessingElement.ProgramStream Bittide.ProcessingElement.ReadElf Bittide.ProcessingElement.Util + Bittide.ProgrammableMux Bittide.ScatterGather Bittide.SharedTypes Bittide.Shutter @@ -305,6 +306,7 @@ test-suite unittests Tests.GeneralPurposeProcessingElement.Calculator Tests.Handshake Tests.ProcessingElement.ReadElf + Tests.ProgrammableMux Tests.ScatterGather Tests.Shared Tests.Switch diff --git a/bittide/src/Bittide/ProgrammableMux.hs b/bittide/src/Bittide/ProgrammableMux.hs new file mode 100644 index 000000000..9a1c7a56c --- /dev/null +++ b/bittide/src/Bittide/ProgrammableMux.hs @@ -0,0 +1,85 @@ +-- SPDX-FileCopyrightText: 2024 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +module Bittide.ProgrammableMux (programmableMux) where + +import Clash.Prelude +import Protocols + +import Bittide.ElasticBuffer (stickyE) +import Clash.Class.BitPackC (ByteOrder) +import GHC.Stack (HasCallStack) +import Protocols.MemoryMap (Access (ReadWrite, WriteOnly), Mm) +import Protocols.MemoryMap.Registers.WishboneStandard ( + RegisterConfig (access, description), + deviceWb, + registerConfig, + registerWbI, + ) +import Protocols.Wishbone (Wishbone, WishboneMode (Standard)) + +{- | A mux which switches its output from input 'A' to input 'B' when the local counter is +'cycle_to_switch' and the device is armed. Once switched the mux cannot switched back +using the registers, but requires a reset. + +An intended use for this component is in the 'WireDemo'. In this demo, the management unit +(input A) starts with control over the link, while the processing element (input B) is +held in reset. When the management unit has finished its setup, it sets the +'cycle_to_switch' and arms the mux. At the specified cycle, the mux switches to the +processing element's links and deasserts its reset, allowing it to start writing to the +link. +-} +programmableMux :: + forall dom addrW nBytes a. + ( HasCallStack + , HiddenClockResetEnable dom + , KnownNat addrW + , KnownNat nBytes + , 1 <= nBytes + , ?busByteOrder :: ByteOrder + , ?regByteOrder :: ByteOrder + ) => + -- | Local counter + Signal dom (Unsigned 64) -> + Circuit + ( (ToConstBwd Mm, Wishbone dom 'Standard addrW nBytes) + , "A" ::: CSignal dom a + , "B" ::: CSignal dom a + ) + ( "B_RESET" ::: CSignal dom Bool + , "OUT" ::: CSignal dom a + ) +programmableMux localCounter = circuit $ \(bus, a, b) -> do + [wbCycleToSwitch, wbArm] <- deviceWb "ProgrammableMux" -< bus + + let + -- Trigger one cycle earlier to account for the delay from 'sticky' + trigger = stickyE hasClock hasReset $ arm .&&. (localCounter .==. (cycleToSwitch - 1)) + bReset = not <$> trigger + Fwd linkOut <- muxC trigger -< (b, a) + + (Fwd cycleToSwitch, _cycleToSwitchActivity) <- + registerWbI + (registerConfig "cycle_to_switch") + { access = ReadWrite + , description = "Clock cycle to switch from input 'A' to 'B'." + } + maxBound + -< (wbCycleToSwitch, Fwd (pure Nothing)) + + (Fwd arm, _armActivity) <- + registerWbI + (registerConfig "arm") + { access = WriteOnly + , description = "Arm the mux to switch on cycle_to_switch. Prevents atomicity issues." + } + False + -< (wbArm, Fwd (pure Nothing)) + + idC -< Fwd (bReset, linkOut) + where + muxC :: Signal dom Bool -> Circuit (CSignal dom a, CSignal dom a) (CSignal dom a) + muxC bool = Circuit go + where + go ((t, f), _) = (units, mux bool t f) diff --git a/bittide/tests/Tests/ProgrammableMux.hs b/bittide/tests/Tests/ProgrammableMux.hs new file mode 100644 index 000000000..36e47058e --- /dev/null +++ b/bittide/tests/Tests/ProgrammableMux.hs @@ -0,0 +1,100 @@ +-- SPDX-FileCopyrightText: 2026 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 +{-# LANGUAGE OverloadedStrings #-} + +module Tests.ProgrammableMux where + +import Clash.Prelude +import Protocols + +import Bittide.ProgrammableMux (programmableMux) +import Bittide.SharedTypes (withBittideByteOrder) +import Data.Maybe (fromJust) +import Protocols.Hedgehog (defExpectOptions) +import Protocols.MemoryMap +import Protocols.Wishbone.Standard.Hedgehog (WishboneMasterRequest (..)) + +import Clash.Hedgehog.Sized.Unsigned (genUnsigned) +import Hedgehog (Property) +import Test.Tasty +import Test.Tasty.Hedgehog (testProperty) +import Test.Tasty.TH (testGroupGenerator) + +import qualified Data.List as L +import qualified Data.Map as Map +import qualified Data.String.Interpolate as Str +import qualified Hedgehog as H +import qualified Hedgehog.Range as Range +import qualified Protocols.Wishbone.Standard.Hedgehog as Wb + +{- | Differentiate between the data from the management unit and processing element in the +constructor. The Unsigned 64 is the cycle number at which the data is generated, so we can +verify the exact cycle the programmable mux switches. +-} +data LinkData = Mu (Unsigned 64) | Pe (Unsigned 64) + deriving (Show, ShowX, Eq, Generic, NFDataX, BitPack) + +isMu :: LinkData -> Bool +isMu (Mu _) = True +isMu _ = False + +isPe :: LinkData -> Bool +isPe (Pe _) = True +isPe _ = False + +prop_ProgrammableMux :: Property +prop_ProgrammableMux = H.property $ do + switchCycle <- H.forAll $ genUnsigned @64 (Range.linear 100 200) + let + dut = + withBittideByteOrder + $ withClockResetEnable @System clockGen resetGen enableGen + $ circuit + $ \wb -> do + let + counter = register 0 (counter + 1) + muLinks = fmap Mu counter + peLinks = fmap Pe counter + programmableMux @System @32 @8 counter -< (wb, Fwd muLinks, Fwd peLinks) + + deviceName = "ProgrammableMux" + defs = (((getMMAny dut).deviceDefs) Map.! deviceName) + + counterLoc = L.find (\loc -> loc.name.name == "cycle_to_switch") defs.registers + armLoc = L.find (\loc -> loc.name.name == "arm") defs.registers + + counterAddr = fromIntegral (fromJust counterLoc).value.address `div` 8 + armAddr = fromIntegral (fromJust armLoc).value.address `div` 8 + + switchCycleBv = resize $ pack switchCycle + requests = fmap (,0) [Write counterAddr maxBound switchCycleBv, Write armAddr maxBound 1] + + (resets, outLink) = + sampleC def{timeoutAfter = simLength} (unMemmap dut <| Wb.driveStandard defExpectOptions requests) + resetsBeforeSwitch = L.length $ L.takeWhile id resets + resetsAfterSwitch = L.length (L.takeWhile (== False) (L.dropWhile (== True) resets)) + + outLinkBeforeSwitch = L.length $ L.takeWhile isMu outLink + outLinkAfterSwitch = L.length $ L.takeWhile isPe (L.dropWhile isMu outLink) + + H.footnote [Str.i|Asserted Cycles: #{resetsBeforeSwitch}|] + H.footnote [Str.i|Deasserted Cycles: #{resetsAfterSwitch}|] + let interestingResetCycles = L.take 10 (L.drop (fromIntegral switchCycle - 5) resets) + let interestingLinkCycles = L.take 10 (L.drop (fromIntegral switchCycle - 5) outLink) + H.footnote [Str.i|Interesting reset cycles (around the switch point): \n#{interestingResetCycles}|] + H.footnote [Str.i|Interesting link cycles (around the switch point): \n#{interestingLinkCycles}|] + + -- Check that the reset is asserted before the switch cycle, and deasserted after. The + -- +-1 is to account for the single cycle the dut is in reset because of 'resetGen'. + resetsBeforeSwitch H.=== fromIntegral switchCycle + 1 + resetsAfterSwitch H.=== simLength - fromIntegral switchCycle - 1 + + -- Check that the out link is from the MU before the switch cycle, and from the PE after. + outLinkBeforeSwitch H.=== fromIntegral switchCycle + 1 + outLinkAfterSwitch H.=== simLength - fromIntegral switchCycle - 1 + where + simLength = 300 + +tests :: TestTree +tests = $(testGroupGenerator) diff --git a/bittide/tests/UnitTests.hs b/bittide/tests/UnitTests.hs index 606ab1825..40c944c6f 100644 --- a/bittide/tests/UnitTests.hs +++ b/bittide/tests/UnitTests.hs @@ -24,6 +24,7 @@ import qualified Tests.ElasticBuffer import qualified Tests.GeneralPurposeProcessingElement.Calculator import qualified Tests.Handshake import qualified Tests.ProcessingElement.ReadElf +import qualified Tests.ProgrammableMux import qualified Tests.ScatterGather import qualified Tests.Switch import qualified Tests.SwitchDemoProcessingElement @@ -53,6 +54,7 @@ tests = , Tests.GeneralPurposeProcessingElement.Calculator.tests , Tests.Handshake.tests , Tests.ProcessingElement.ReadElf.tests + , Tests.ProgrammableMux.tests , Tests.ScatterGather.tests , Tests.Switch.tests , Tests.SwitchDemoProcessingElement.Calculator.tests From 808235667deef1a6ff7878aec68153ab5ca81747 Mon Sep 17 00:00:00 2001 From: Hidde Moll Date: Tue, 17 Mar 2026 10:50:54 +0100 Subject: [PATCH 2/4] Play around with endianness in unittest --- bittide/tests/Tests/ProgrammableMux.hs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/bittide/tests/Tests/ProgrammableMux.hs b/bittide/tests/Tests/ProgrammableMux.hs index 36e47058e..bee44d984 100644 --- a/bittide/tests/Tests/ProgrammableMux.hs +++ b/bittide/tests/Tests/ProgrammableMux.hs @@ -9,7 +9,8 @@ import Clash.Prelude import Protocols import Bittide.ProgrammableMux (programmableMux) -import Bittide.SharedTypes (withBittideByteOrder) +import Bittide.SharedTypes (withBigEndian) +import Clash.Class.BitPackC (ByteOrder (BigEndian), packC) import Data.Maybe (fromJust) import Protocols.Hedgehog (defExpectOptions) import Protocols.MemoryMap @@ -48,7 +49,7 @@ prop_ProgrammableMux = H.property $ do switchCycle <- H.forAll $ genUnsigned @64 (Range.linear 100 200) let dut = - withBittideByteOrder + withBigEndian $ withClockResetEnable @System clockGen resetGen enableGen $ circuit $ \wb -> do @@ -67,8 +68,9 @@ prop_ProgrammableMux = H.property $ do counterAddr = fromIntegral (fromJust counterLoc).value.address `div` 8 armAddr = fromIntegral (fromJust armLoc).value.address `div` 8 - switchCycleBv = resize $ pack switchCycle - requests = fmap (,0) [Write counterAddr maxBound switchCycleBv, Write armAddr maxBound 1] + switchCycleBv = resize $ pack $ packC BigEndian switchCycle + armBv = resize $ pack $ packC BigEndian True + requests = fmap (,0) [Write counterAddr maxBound switchCycleBv, Write armAddr maxBound armBv] (resets, outLink) = sampleC def{timeoutAfter = simLength} (unMemmap dut <| Wb.driveStandard defExpectOptions requests) @@ -96,5 +98,20 @@ prop_ProgrammableMux = H.property $ do where simLength = 300 +-- <| trace "ProgrammableMux" +-- trace :: +-- (KnownDomain dom, KnownNat addrW, KnownNat nBytes) => +-- String -> +-- Circuit (Wishbone dom mode addrW nBytes) (Wishbone dom mode addrW nBytes) +-- trace msg = +-- Circuit +-- (unbundle . withClockResetEnable clockGen resetGen enableGen mealy go (0 :: Int) . bundle) +-- where +-- go cnt ~(m2s, s2m) +-- | m2s.busCycle = (cnt + 1, (s2m', m2s)) +-- | otherwise = (cnt + 1, (s2m, m2s)) +-- where +-- s2m' = Debug.trace [Str.i| Df.Trace #{msg} | #{cnt}: #{showX m2s}, #{showX s2m}|] s2m + tests :: TestTree tests = $(testGroupGenerator) From 77ff4a9c910b91d504a43f573aa895a0dbfd19b7 Mon Sep 17 00:00:00 2001 From: Hidde Moll Date: Wed, 18 Mar 2026 14:43:41 +0100 Subject: [PATCH 3/4] More experimentation with endianness --- bittide/tests/Tests/ProgrammableMux.hs | 51 +++++++++++++++----------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/bittide/tests/Tests/ProgrammableMux.hs b/bittide/tests/Tests/ProgrammableMux.hs index bee44d984..db92553f5 100644 --- a/bittide/tests/Tests/ProgrammableMux.hs +++ b/bittide/tests/Tests/ProgrammableMux.hs @@ -9,15 +9,16 @@ import Clash.Prelude import Protocols import Bittide.ProgrammableMux (programmableMux) -import Bittide.SharedTypes (withBigEndian) -import Clash.Class.BitPackC (ByteOrder (BigEndian), packC) +import Bittide.SharedTypes (withByteOrderings) +import Clash.Class.BitPackC (ByteOrder (..)) import Data.Maybe (fromJust) import Protocols.Hedgehog (defExpectOptions) import Protocols.MemoryMap -import Protocols.Wishbone.Standard.Hedgehog (WishboneMasterRequest (..)) +import Protocols.Wishbone import Clash.Hedgehog.Sized.Unsigned (genUnsigned) import Hedgehog (Property) +import Protocols.Wishbone.Standard.Hedgehog (WishboneMasterRequest (..)) import Test.Tasty import Test.Tasty.Hedgehog (testProperty) import Test.Tasty.TH (testGroupGenerator) @@ -25,7 +26,9 @@ import Test.Tasty.TH (testGroupGenerator) import qualified Data.List as L import qualified Data.Map as Map import qualified Data.String.Interpolate as Str +import qualified Debug.Trace as Debug import qualified Hedgehog as H +import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range import qualified Protocols.Wishbone.Standard.Hedgehog as Wb @@ -44,12 +47,17 @@ isPe :: LinkData -> Bool isPe (Pe _) = True isPe _ = False +{- Lucas suspects a bug with the 'offset' in `maskWriteData', which now only triggers +because we use a bus datawidth of 64 bits instead of the usual 32 bits. +-} + prop_ProgrammableMux :: Property prop_ProgrammableMux = H.property $ do switchCycle <- H.forAll $ genUnsigned @64 (Range.linear 100 200) + regByteOrder <- H.forAll $ Gen.element [BigEndian, LittleEndian] let dut = - withBigEndian + withByteOrderings BigEndian regByteOrder $ withClockResetEnable @System clockGen resetGen enableGen $ circuit $ \wb -> do @@ -68,12 +76,14 @@ prop_ProgrammableMux = H.property $ do counterAddr = fromIntegral (fromJust counterLoc).value.address `div` 8 armAddr = fromIntegral (fromJust armLoc).value.address `div` 8 - switchCycleBv = resize $ pack $ packC BigEndian switchCycle - armBv = resize $ pack $ packC BigEndian True + switchCycleBv = pack switchCycle + armBv = resize $ pack True requests = fmap (,0) [Write counterAddr maxBound switchCycleBv, Write armAddr maxBound armBv] (resets, outLink) = - sampleC def{timeoutAfter = simLength} (unMemmap dut <| Wb.driveStandard defExpectOptions requests) + sampleC + def{timeoutAfter = simLength} + (unMemmap dut <| trace "ProgrammableMux" <| Wb.driveStandard defExpectOptions requests) resetsBeforeSwitch = L.length $ L.takeWhile id resets resetsAfterSwitch = L.length (L.takeWhile (== False) (L.dropWhile (== True) resets)) @@ -98,20 +108,19 @@ prop_ProgrammableMux = H.property $ do where simLength = 300 --- <| trace "ProgrammableMux" --- trace :: --- (KnownDomain dom, KnownNat addrW, KnownNat nBytes) => --- String -> --- Circuit (Wishbone dom mode addrW nBytes) (Wishbone dom mode addrW nBytes) --- trace msg = --- Circuit --- (unbundle . withClockResetEnable clockGen resetGen enableGen mealy go (0 :: Int) . bundle) --- where --- go cnt ~(m2s, s2m) --- | m2s.busCycle = (cnt + 1, (s2m', m2s)) --- | otherwise = (cnt + 1, (s2m, m2s)) --- where --- s2m' = Debug.trace [Str.i| Df.Trace #{msg} | #{cnt}: #{showX m2s}, #{showX s2m}|] s2m +trace :: + (KnownDomain dom, KnownNat addrW, KnownNat nBytes) => + String -> + Circuit (Wishbone dom mode addrW nBytes) (Wishbone dom mode addrW nBytes) +trace msg = + Circuit + (unbundle . withClockResetEnable clockGen resetGen enableGen mealy go (0 :: Int) . bundle) + where + go cnt ~(m2s, s2m) + | m2s.busCycle = (cnt + 1, (s2m', m2s)) + | otherwise = (cnt + 1, (s2m, m2s)) + where + s2m' = Debug.trace [Str.i| Df.Trace #{msg} | #{cnt}: #{showX m2s}, #{showX s2m}|] s2m tests :: TestTree tests = $(testGroupGenerator) From 0aa7c9629535f516d66f03bf05bfd29a4e902f4c Mon Sep 17 00:00:00 2001 From: Hidde Moll Date: Thu, 19 Mar 2026 11:34:30 +0100 Subject: [PATCH 4/4] WIP --- bittide/tests/Tests/ProgrammableMux.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bittide/tests/Tests/ProgrammableMux.hs b/bittide/tests/Tests/ProgrammableMux.hs index db92553f5..98553a80d 100644 --- a/bittide/tests/Tests/ProgrammableMux.hs +++ b/bittide/tests/Tests/ProgrammableMux.hs @@ -11,6 +11,7 @@ import Protocols import Bittide.ProgrammableMux (programmableMux) import Bittide.SharedTypes (withByteOrderings) import Clash.Class.BitPackC (ByteOrder (..)) +import Clash.Class.BitPackC.Padding (packWordC) import Data.Maybe (fromJust) import Protocols.Hedgehog (defExpectOptions) import Protocols.MemoryMap @@ -54,10 +55,11 @@ because we use a bus datawidth of 64 bits instead of the usual 32 bits. prop_ProgrammableMux :: Property prop_ProgrammableMux = H.property $ do switchCycle <- H.forAll $ genUnsigned @64 (Range.linear 100 200) + busByteOrder <- H.forAll $ Gen.element [BigEndian, LittleEndian] regByteOrder <- H.forAll $ Gen.element [BigEndian, LittleEndian] let dut = - withByteOrderings BigEndian regByteOrder + withByteOrderings busByteOrder regByteOrder $ withClockResetEnable @System clockGen resetGen enableGen $ circuit $ \wb -> do @@ -76,8 +78,8 @@ prop_ProgrammableMux = H.property $ do counterAddr = fromIntegral (fromJust counterLoc).value.address `div` 8 armAddr = fromIntegral (fromJust armLoc).value.address `div` 8 - switchCycleBv = pack switchCycle - armBv = resize $ pack True + switchCycleBv = pack $ packWordC @8 busByteOrder switchCycle + armBv = pack $ packWordC @8 busByteOrder True requests = fmap (,0) [Write counterAddr maxBound switchCycleBv, Write armAddr maxBound armBv] (resets, outLink) =