diff --git a/bittide/bittide.cabal b/bittide/bittide.cabal index 403a7d510..467943e54 100644 --- a/bittide/bittide.cabal +++ b/bittide/bittide.cabal @@ -63,13 +63,14 @@ common common-options Cabal, array, -- clash-prelude will set suitable version bounds for the plugins + clash-lib >= 1.6.3 && < 1.7, clash-prelude >= 1.6.3 && < 1.7, - ghc-typelits-natnormalise, - ghc-typelits-extra, - ghc-typelits-knownnat, + constraints >= 0.13.3 && < 0.15, containers >= 0.4.0 && < 0.7, contranomy, - constraints >= 0.13.3 && < 0.15 + ghc-typelits-extra, + ghc-typelits-knownnat, + ghc-typelits-natnormalise, library import: common-options hs-source-dirs: src @@ -97,6 +98,7 @@ test-suite unittests Tests.DoubleBufferedRam Tests.ScatterGather Tests.Switch + Tests.Shared default-extensions: ImplicitPrelude build-depends: diff --git a/bittide/src/Bittide/ScatterGather.hs b/bittide/src/Bittide/ScatterGather.hs index 7b39db346..acb6324fe 100644 --- a/bittide/src/Bittide/ScatterGather.hs +++ b/bittide/src/Bittide/ScatterGather.hs @@ -2,25 +2,30 @@ -- -- SPDX-License-Identifier: Apache-2.0 -module Bittide.ScatterGather(scatterEngine, gatherEngine, scatterGatherEngine) where +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} + +module Bittide.ScatterGather + ( scatterEngine + , scatterUnitWb + , gatherUnitWb + ) where import Clash.Prelude +import Contranomy.Wishbone +import Data.Type.Equality ((:~:)(Refl)) + import Bittide.Calendar import Bittide.DoubleBufferedRam import Bittide.SharedTypes +import Data.Constraint.Nat.Extra - --- | Contains a calendar entry that can be used by a scatter or gather engine. +-- | Calendar entry that can be used by a scatter or gather engine. type CalendarEntry memDepth = Index memDepth --- | The Calendar for the scatter and gather engines contains entries of a single index --- to the engine. For more information regarding calendar datatypes see --- see NOTE [component calendar types] -type Calendar calDepth memDepth = Vec calDepth (CalendarEntry memDepth) - --- | Write operation for a scatter or gather calendar. -type ConfigurationPort calDepth memDepth = Maybe (Index calDepth, CalendarEntry memDepth) +-- TODO: scatterEngine should be removed after merging #71, which removes scatterEngine -- | scatterEngine is a memory bank that allows for random writes and sequentially -- reads data based on an internal counter that runs up to the maximum index, then wraps around. @@ -41,70 +46,174 @@ scatterEngine newMetaCycle frameIn writeAddr = readAddr = register 0 $ satSucc SatWrap <$> readAddr writeFrame = combineFrameWithAddr frameIn writeAddr --- | gatherEngine is a memory bank that allows for random reads and sequentially --- writes data based on an internal counter that runs up to the maximum index and wraps around. --- The initial contents are undefined. -gatherEngine :: - (HiddenClockResetEnable dom, KnownNat memDepth, 1 <= memDepth, NFDataX a) => - -- | Indicates when a new metacycle has started. - Signal dom Bool -> - -- | Incoming frame from link, if it contains Just a, a will be written to the memory. - Signal dom (Maybe a) -> - -- | Read address. - Signal dom (Index memDepth) -> - -- | Data at the read address, delayed by one clock cycle. - Signal dom a -gatherEngine newMetaCycle frameIn readAddr = - doubleBufferedRamU newMetaCycle readAddr writeFrame - where - writeAddr = register 0 $ satSucc SatWrap <$> writeAddr - writeFrame = combineFrameWithAddr frameIn writeAddr - -combineFrameWithAddr :: Signal dom (Maybe dat) -> Signal dom addr -> Signal dom (Maybe (addr,dat)) +combineFrameWithAddr :: + Signal dom (Maybe dat) -> + Signal dom addr -> + Signal dom (Maybe (addr,dat)) combineFrameWithAddr frameIn writeAddr = combine <$> frameIn <*> writeAddr where combine :: Maybe dat -> addr -> Maybe (addr,dat) combine frame addr = fmap (addr,) frame --- | scatterGatherEngine is a 4 port memory component that enables gathering and scattering for the processing element. --- Scattering and gathering data is done using two seperate memory banks with each their own calendar, --- the writeAddrSwitch dictate the read and write address on the switch side for the gather and scatter memory respectively. --- If the read address for the scatter engine is 0, a null frame (Nothing) will be sent to the switch. -scatterGatherEngine :: - forall dom calDepthG calDepthS memDepthG memDepthS frameWidth . - (HiddenClockResetEnable dom, KnownNat calDepthG, KnownNat calDepthS, KnownNat memDepthG, KnownNat memDepthS, KnownNat frameWidth, - 1 <= calDepthG , 1 <= calDepthS, 1 <= memDepthG, 1 <= memDepthS) => - -- | Bootstrap calendar gather memory. - Calendar calDepthS memDepthS -> - -- | Bootstrap calendar scatter memory. - Calendar calDepthG memDepthG -> - -- | Configuration port for the scatter engine. - Signal dom (ConfigurationPort calDepthS memDepthS) -> - -- | Configuration port for the gather engine. - Signal dom (ConfigurationPort calDepthG memDepthG) -> - -- | Incoming frame from the switch - Signal dom (DataLink frameWidth) -> - -- | Incoming data from the PE. +-- | Double buffered memory component that can be written to by a Bittide link. The write +-- address of the incoming frame is determined by the incorporated 'calendarWB'. The buffers +-- are swapped at the beginning of each metacycle. Reading the buffer is done by supplying +-- a read address. Furthermore this component offers ports to control the incorporated 'calendarWB'. +scatterUnit :: + ( HiddenClockResetEnable dom + , KnownNat memDepth, 1 <= memDepth + , KnownNat frameWidth) => + -- | Configuration for the 'calendarWB'. + CalendarConfig bytes addressWidth (CalendarEntry memDepth) -> + -- | Wishbone (master -> slave) port for the 'calendarWB'. + Signal dom (WishboneM2S bytes addressWidth) -> + -- | Swap active calendar and shadow calendar. + Signal dom Bool -> + -- | Incoming frame from Bittide link. Signal dom (DataLink frameWidth) -> - -- | Read address of the PE frame. - Signal dom (Index memDepthS) -> - -- | Write address for the PE link. - Signal dom (Index memDepthG) -> - -- | Frame read by the switch and the data read by the PE. - (Signal dom (BitVector frameWidth),Signal dom (DataLink frameWidth)) -scatterGatherEngine bootCalS bootCalG scatterConfig gatherConfig - frameInSwitch frameInPE readAddrPE writeAddrPE = (toPE, toSwitch) - where - -- Scatter engine - frameInSwitch' = mux ((==0) <$> writeAddrSwitch) (pure Nothing) frameInSwitch - (writeAddrSwitch, newMetaCycleS) = calendar bootCalS (pure False) scatterConfig - writeFrameSwitch = (\f a -> fmap (a,) f) <$> frameInSwitch' <*> writeAddrSwitch - scatterOut = doubleBufferedRamU newMetaCycleS readAddrPE writeFrameSwitch - toPE = scatterOut - - -- Gather engine - frameInPE' = mux ((==0) <$> writeAddrPE) (pure Nothing) frameInPE - (readAddrSwitch, newMetaCycleG) = calendar bootCalG (pure False) gatherConfig - writeFramePE = (\f a -> fmap (a,) f) <$> frameInPE' <*> writeAddrPE - gatherOut = doubleBufferedRamU newMetaCycleG readAddrSwitch writeFramePE - toSwitch = mux (register True $ (==0) <$> readAddrSwitch) (pure Nothing) $ Just <$> gatherOut + -- | Read address. + Signal dom (Index memDepth) -> + -- | (Data at read address delayed 1 cycle, Wishbone (slave -> master) from 'calendarWB') + (Signal dom (BitVector frameWidth), Signal dom (WishboneS2M bytes)) +scatterUnit calConfig wbIn calSwitch linkIn readAddr = (readOut, wbOut) + where + (writeAddr, metaCycle, wbOut) = mkCalendar calConfig calSwitch wbIn + writeOp = (\a b -> (a,) <$> b) <$> writeAddr <*> linkIn + readOut = doubleBufferedRamU metaCycle readAddr writeOp + +-- | Double buffered memory component that can be written to by a generic write operation. The +-- write address of the incoming frame is determined by the incorporated 'calendarWB'. The +-- buffers are swapped at the beginning of each metacycle. Reading the buffer is done by +-- supplying a read address. Furthermore this component offers ports to control the +-- incorporated 'calendarWB'. +gatherUnit :: + ( HiddenClockResetEnable dom + , KnownNat memDepth, 1 <= memDepth + , KnownNat frameWidth, 1 <= frameWidth + , KnownNat (DivRU frameWidth 8), 1 <= (DivRU frameWidth 8)) => + -- | Configuration for the 'calendarWB'. + CalendarConfig bytes addressWidth (CalendarEntry memDepth) -> + -- | Wishbone (master -> slave) port for the 'calendarWB'. + Signal dom (WishboneM2S bytes addressWidth) -> + -- | Swap active calendar and shadow calendar. + Signal dom Bool -> + -- | Write operation writing a frame. + Signal dom (Maybe (LocatedBits memDepth frameWidth)) -> + -- | Byte enable for write operation. + Signal dom (ByteEnable (DivRU frameWidth 8)) -> + -- | (Transmitted frame to Bittide Link, Wishbone (slave -> master) from 'calendarWB') + (Signal dom (DataLink frameWidth), Signal dom (WishboneS2M bytes)) +gatherUnit calConfig wbIn calSwitch writeOp byteEnables= (linkOut, wbOut) + where + (readAddr, metaCycle, wbOut) = mkCalendar calConfig calSwitch wbIn + linkOut = mux (register True ((==0) <$> readAddr)) (pure Nothing) (Just <$> bramOut) + bramOut = doubleBufferedRamByteAddressableU metaCycle readAddr writeOp byteEnables + +-- | Wishbone interface for the 'scatterUnit' and 'gatherUnit'. It makes the scatter and gather +-- unit, which operate on 64 bit frames, addressable via a 32 bit wishbone bus. +wbInterface :: + forall bytes addressWidth addresses . + ( KnownNat bytes + , KnownNat addresses, 1 <= addresses + , KnownNat addressWidth, 2 <= addressWidth) => + -- | Maximum address of the respective memory element as seen from the wishbone side. + Index addresses -> + -- | Wishbone (master -> slave) data. + WishboneM2S bytes addressWidth -> + -- | Read data to be send to over the (slave -> master) port. + Bytes bytes -> + -- | (slave - master data, read address memory element, write data memory element) + (WishboneS2M bytes, Index addresses, Maybe (Bytes bytes)) +wbInterface addressRange WishboneM2S{..} readData = + (WishboneS2M{readData, acknowledge, err}, memAddr, writeOp) + where + masterActive = strobe && busCycle + (alignedAddress, alignment) = split @_ @(addressWidth - 2) @2 addr + wordAligned = alignment == 0 + err = masterActive && ((alignedAddress > resize (pack addressRange)) || not wordAligned) + acknowledge = masterActive && not err + wbAddr = unpack . resize $ pack alignedAddress + memAddr = wbAddr + writeOp | strobe && writeEnable && not err = Just writeData + | otherwise = Nothing + +-- | Wishbone addressable 'scatterUnit', the wishbone port can read the data from this +-- memory element as if it has a 32 bit port by selecting the upper 32 or lower 32 bits +-- of the read data. +scatterUnitWb :: + forall dom memDepth awSU bsCal awCal . + ( HiddenClockResetEnable dom + , KnownNat memDepth, 1 <= memDepth + , KnownNat awSU, 2 <= awSU) => + -- | Configuration for the 'calendarWB'. + CalendarConfig bsCal awCal (CalendarEntry memDepth) -> + -- | Wishbone (master -> slave) port 'calendarWB'. + Signal dom (WishboneM2S bsCal awCal) -> + -- | Swap active calendar and shadow calendar. + Signal dom Bool -> + -- | Incoming frame from Bittide link. + Signal dom (DataLink 64) -> + -- | Wishbone (master -> slave) port scatter memory. + Signal dom (WishboneM2S 4 awSU) -> + -- | (Wishbone (slave -> master) port scatter memory, Wishbone (slave -> master) port 'calendarWB') + (Signal dom (WishboneS2M 4), Signal dom (WishboneS2M bsCal)) +scatterUnitWb calConfig wbInCal calSwitch linkIn wbInSU = + (delayControls wbOutSU, wbOutCal) + where + (wbOutSU, memAddr, _) = unbundle $ wbInterface maxBound <$> wbInSU <*> scatteredData + (readAddr, upperSelected) = unbundle $ coerceIndices <$> memAddr + (scatterUnitRead, wbOutCal) = scatterUnit calConfig wbInCal calSwitch linkIn readAddr + (upper, lower) = unbundle $ split <$> scatterUnitRead + selected = register (errorX "scatterUnitWb: Initial selection undefined") upperSelected + scatteredData = mux selected upper lower + +-- | Wishbone addressable 'gatherUnit', the wishbone port can write data to this +-- memory element as if it has a 32 bit port by controlling the byte enables of the +-- 'gatherUnit' based on the third bit. +gatherUnitWb :: + forall dom memDepth awSU bsCal awCal . + ( HiddenClockResetEnable dom + , KnownNat memDepth, 1 <= memDepth + , KnownNat awSU, 2 <= awSU) => + -- | Configuration for the 'calendarWB'. + CalendarConfig bsCal awCal (CalendarEntry memDepth) -> + -- | Wishbone (master -> slave) data 'calendarWB'. + Signal dom (WishboneM2S bsCal awCal) -> + -- | Swap active calendar and shadow calendar. + Signal dom Bool -> + -- | Wishbone (master -> slave) port gather memory. + Signal dom (WishboneM2S 4 awSU) -> + -- | (Wishbone (slave -> master) port gather memory, Wishbone (slave -> master) port 'calendarWB') + (Signal dom (DataLink 64), Signal dom (WishboneS2M 4), Signal dom (WishboneS2M bsCal)) +gatherUnitWb calConfig wbInCal calSwitch wbInSU = + (linkOut, delayControls wbOutSU, wbOutCal) + where + (wbOutSU, memAddr, writeOp) = unbundle $ wbInterface maxBound <$> wbInSU <*> pure 0b0 + (writeAddr, upperSelected) = unbundle $ coerceIndices <$> memAddr + (linkOut, wbOutCal) = + gatherUnit calConfig wbInCal calSwitch gatherWrite gatherByteEnables + gatherWrite = mkWrite <$> writeAddr <*> writeOp + gatherByteEnables = mkEnables <$> upperSelected <*> (busSelect <$> wbInSU) + + -- We update the 64 bit entry of the 'gatherUnit' in chunks of 32 bits. Thus we repeat + -- the writeData of the wishbone bus twice in the write operation to the 'gatherUnit' and + -- use the byte enables to either update the upper or lower + mkWrite address (Just write) = Just (address, write ++# write) + mkWrite _ _ = Nothing + mkEnables selected byteEnables + | selected = byteEnables ++# 0b0 + | otherwise = 0b0 ++# byteEnables + +-- | Coerces an index of size (n*2) to index n with the LSB as separate boolean. +coerceIndices :: forall n. (KnownNat n, 1 <= n) => Index (n*2) -> (Index n, Bool) +coerceIndices = case clog2axiom @n of Refl -> bitCoerce + +-- | Delays the output controls to align them with the actual read / write timing. +delayControls :: HiddenClockResetEnable dom => + Signal dom (WishboneS2M bytes) -> Signal dom (WishboneS2M bytes) +delayControls wbIn = wbOut + where + delayedAck = register False (acknowledge <$> wbIn) + delayedErr = register False (err <$> wbIn) + wbOut = (\wb newAck newErr-> wb{acknowledge = newAck, err = newErr}) + <$> wbIn <*> delayedAck <*> delayedErr diff --git a/bittide/src/Bittide/Switch.hs b/bittide/src/Bittide/Switch.hs index 64307cd2d..4463484a1 100644 --- a/bittide/src/Bittide/Switch.hs +++ b/bittide/src/Bittide/Switch.hs @@ -18,6 +18,8 @@ type CrossbarIndex links = Index (links+1) -- memory and a crossbar index to select the outgoing frame. type CalendarEntry links memDepth = Vec links (Index memDepth, CrossbarIndex links) +-- TODO: Remove Bittide.ScatterEngine and its tests before merging #71 + -- | The Bittide Switch routes data from incoming to outgoing links based on a calendar. -- The switch consists of a crossbar, a calendar and a scatter engine for all incoming links. -- The crossbar selects one of the scatter engine outputs for every outgoing link, index 0 diff --git a/bittide/src/Data/Constraint/Nat/Extra.hs b/bittide/src/Data/Constraint/Nat/Extra.hs index b095c9dc5..64661d4ef 100644 --- a/bittide/src/Data/Constraint/Nat/Extra.hs +++ b/bittide/src/Data/Constraint/Nat/Extra.hs @@ -16,9 +16,14 @@ solved by the constraint solver. module Data.Constraint.Nat.Extra where import Data.Constraint +import Data.Type.Equality +import GHC.TypeLits.Extra import GHC.TypeNats import Unsafe.Coerce -- | b <= ceiling(b/a)*a timesDivRU :: forall a b . Dict (b <= (Div (b + (a - 1)) a * a)) timesDivRU = unsafeCoerce (Dict :: Dict ()) + +clog2axiom :: CLog 2 (n * 2) :~: (CLog 2 n + 1) +clog2axiom = unsafeCoerce Refl diff --git a/bittide/tests/Tests/Calendar.hs b/bittide/tests/Tests/Calendar.hs index c20f93ac2..fdcba912b 100644 --- a/bittide/tests/Tests/Calendar.hs +++ b/bittide/tests/Tests/Calendar.hs @@ -20,6 +20,8 @@ import Clash.Hedgehog.Sized.Vector import Bittide.Calendar import Bittide.SharedTypes +import Tests.Shared + import Clash.Sized.Vector (unsafeFromList) import Contranomy.Wishbone import Data.Constraint @@ -71,18 +73,6 @@ instance Show (BVCalendar addressWidth) where -- TODO: Remove this show instance after issue (https://github.com/clash-lang/clash-compiler/issues/2190) has been fixed. deriving instance Show (SNatLE a b) -data IsInBounds a b c where - InBounds :: (a <= b, b <= c) => IsInBounds a b c - NotInBounds :: IsInBounds a b c - -deriving instance Show (IsInBounds a b c) - --- | Returns 'InBounds' if a <= b <= c, otherwise returns 'NotInBounds'. -isInBounds :: SNat a -> SNat b -> SNat c -> IsInBounds a b c -isInBounds a b c = case (compareSNat a b, compareSNat b c) of - (SNatLE, SNatLE) -> InBounds - _ -> NotInBounds - -- | Generates a configuration for 'Bittide.Calendar.calendarWb', with as first argument -- the maximum depth of the stored calendar and as second argument a generator for the -- calendar entries. diff --git a/bittide/tests/Tests/ScatterGather.hs b/bittide/tests/Tests/ScatterGather.hs index acd932d7f..1111fbe7d 100644 --- a/bittide/tests/Tests/ScatterGather.hs +++ b/bittide/tests/Tests/ScatterGather.hs @@ -2,39 +2,36 @@ -- -- SPDX-License-Identifier: Apache-2.0 +{-# OPTIONS_GHC -fconstraint-solver-iterations=10 #-} + {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE FlexibleContexts #-} module Tests.ScatterGather(sgGroup) where -import Clash.Prelude +import Clash.Prelude hiding (fromList) import qualified Prelude as P -import Clash.Sized.Internal.BitVector -import Clash.Sized.Vector (unsafeFromList) -import Data.Bifunctor +import Clash.Sized.Vector (fromList) +import Contranomy.Wishbone import Data.String import Hedgehog import Test.Tasty import Test.Tasty.Hedgehog -import qualified Data.List as L -import qualified Data.Set as Set +import qualified Clash.Util.Interpolate as I import qualified GHC.TypeNats as TN import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range import Bittide.ScatterGather - -isUndefined :: forall n . KnownNat n => BitVector n -> Bool -isUndefined (BV mask _) = mask == full - where full = 1 `shiftL` (natToNum @n @Int) - 1 - -maybeIsUndefined :: (KnownNat n, 1 <= n) => BitVector n -> Maybe (BitVector n) -maybeIsUndefined v | isUndefined v = Nothing - | otherwise = Just v +import Bittide.Calendar +import Data.Maybe +import Bittide.SharedTypes +import Tests.Shared -- | The extra in SomeCalendar extra defines the minimum amount of elements in the vector -- and the minimum addressable indexes in the vector elements. I.e, vectors of 0 elements @@ -45,115 +42,244 @@ data SomeCalendar extra where instance Show (SomeCalendar extra) where show (SomeCalendar SNat list) = show list --- | Returns a calendar for a engine with a memory depth equal to the calendar depth, --- the addresses in the calendar are all unique. -genSomeCalendar :: Gen (SomeCalendar 1) -genSomeCalendar = do - calendarSize <- Gen.enum 1 255 - case TN.someNatVal calendarSize of - (SomeNat size) -> do - cal <- Gen.shuffle [0.. fromIntegral calendarSize] - return (SomeCalendar (snatProxy size) $ unsafeFromList cal) - genData :: Gen (BitVector 64) genData = Gen.integral Range.linearBounded genFrame :: Gen (Maybe (BitVector 64)) genFrame = Gen.maybe genData -genFrameList :: Gen [Maybe (BitVector 64)] -genFrameList = Gen.list (Range.constant 1 100) genFrame +genFrameList :: Range Int -> Gen [Maybe (BitVector 64)] +genFrameList range = Gen.list range genFrame sgGroup :: TestTree sgGroup = testGroup "Scatter Gather group" - [ testPropertyNamed "GatherSequential - No overwriting implies no lost frames." "engineNoFrameLoss gatherEngine" (engineNoFrameLoss gatherEngine) - , testPropertyNamed "ScatterSequential - No overwriting implies no lost frames." "engineNoFrameLoss scatterEngine" (engineNoFrameLoss scatterEngine) - , testPropertyNamed "ScatterGather - No overwriting implies no lost frames." "scatterGatherNoFrameLoss" scatterGatherNoFrameLoss] - --- |The type of a sequential engine that is tested by engineNoFrameLoss. -type MemoryEngine = - forall dom memDepth a . - (NFDataX a, KnownNat memDepth, 1 <= memDepth, HiddenClockResetEnable dom) => - -- | Indicates when a new metacycle has started. - Signal dom Bool -> - -- | Incoming frame from link, if it contains Just a, a will be written to the memory. - Signal dom (Maybe a) -> - -- | Write address, when the incoming frame contains Just a, a will be written to this address. - Signal dom (Index memDepth) -> - -- | Outgoing data - Signal dom a - --- | Tests that for a calendar that contains only unique entries, --- all frames send to the gather engine will appear at its output. --- The amount of frames we test for is equal to twice the size of the buffer, since --- it is double buffered and +2 to account for reset cycle and the one cycle delay. -engineNoFrameLoss :: MemoryEngine -> Property -engineNoFrameLoss engine = property $ do - someCalendar <- forAll genSomeCalendar - case someCalendar of - SomeCalendar size@SNat (toList -> calendar) -> do - inputFrames <- forAll genFrameList - let - topEntity (unbundle -> (frameIn, calIn, newMeta)) = - maybeIsUndefined <$> withClockResetEnable clockGen resetGen enableGen - engine newMeta frameIn calIn - -- Simulate for at least twice the size of the doublebuffered memory + the - -- length of the inputdata + 2 to compensate for the reset cycle and delay cycle. - simLength = 2 * snatToNum size + P.length inputFrames + 2 - inputFrames' = P.take simLength $ inputFrames <> P.repeat Nothing - newMetaSignal = cycle $ (==0) <$> [0..P.length calendar - 1] - topEntityInput = P.zip3 inputFrames' (cycle calendar) newMetaSignal - simOut = simulateN @System simLength topEntity topEntityInput - footnote . fromString $ showX simOut - footnote . fromString $ showX topEntityInput - Set.fromList inputFrames' === Set.fromList simOut - -filterSgOut :: - (KnownDomain dom, KnownNat n, 1 <= n) => - (Signal dom (BitVector n), Signal dom (Maybe (BitVector n))) -> - (Signal dom (Maybe (BitVector n)), Signal dom (Maybe (BitVector n))) -filterSgOut (toP, toS) = (maybeIsUndefined <$> toP, (maybeIsUndefined =<<) <$> toS) - -filterZeroes :: (Num a, Eq a) => [Maybe f] -> [a] -> [Maybe f] -filterZeroes fs as = [ if a == 0 then Nothing else f | (f, a) <- P.zip fs as] - --- | Tests that for a calendar that contains only unique entries, --- all frames send from the PE side to a non zero address, appear at the PE side output. -scatterGatherNoFrameLoss :: Property -scatterGatherNoFrameLoss = property $ do - someCalendarS <- forAll genSomeCalendar - someCalendarG <- forAll genSomeCalendar - inputFramesSwitch <- forAll genFrameList - inputFramesPE <- forAll genFrameList - case (someCalendarS, someCalendarG) of - (SomeCalendar depthScat@SNat calScat, SomeCalendar depthGath@SNat calGath) -> do - let - addressesScat = cycle $ toList calScat - addressesGath = cycle $ toList calGath - inputFramesSwitch' = inputFramesSwitch <> P.repeat Nothing - inputFramesPE' = inputFramesPE <> P.repeat Nothing - - topEntity (unbundle -> (frameInS, frameInP, readAddrPE, writeAddrPE)) = bundle $ - filterSgOut @System (withClockResetEnable clockGen resetGen enableGen - scatterGatherEngine calScat calGath (pure Nothing) (pure Nothing) frameInS frameInP - readAddrPE writeAddrPE) - - maxCalDepth = max (snatToNum depthScat) (snatToNum depthGath) - maxInputLength = max (P.length inputFramesSwitch) (P.length inputFramesPE) - -- Simulate for at least the largest calendar + twice the length of the longest input. - -- This ensures all frames will appear at the output. - simLength = maxCalDepth + 2 * maxInputLength + 1 - topEntityInput = L.zip4 inputFramesSwitch' inputFramesPE' addressesScat addressesGath - simOut = simulateN simLength topEntity topEntityInput - - expectedScat = filterZeroes (P.take simLength inputFramesSwitch') addressesScat - expectedGath = filterZeroes (P.take simLength inputFramesPE') addressesGath - expected = bimap Set.fromList Set.fromList (expectedScat, expectedGath) - result = bimap Set.fromList Set.fromList $ P.unzip simOut - - footnote . fromString $ "Frames to PE: " <> showX (fmap fst simOut) - footnote . fromString $ "Frames to Switch: " <> showX (fmap snd simOut) - footnote . fromString $ "Frames from PE: " <> show (P.zip addressesGath $ P.take simLength inputFramesPE') - footnote . fromString $ "Frames from Switch: " <> show (P.zip addressesScat $ P.take simLength inputFramesSwitch') - - expected === result + [ testPropertyNamed "scatterUnitWb - No overwriting implies no lost frames." "scatterUnitNoFrameLoss" scatterUnitNoFrameLoss + , testPropertyNamed "gatherUnitWb - No overwriting implies no lost frames." "gatherUnitNoFrameLoss" gatherUnitNoFrameLoss + ] + +-- TODO: Remove instance once https://github.com/clash-lang/clash-compiler/pull/2197 is released +deriving instance Show (SNatLE a b) + +-- | Generates a 'CalendarConfig' for the 'gatherUnitWb' or 'scatterUnitWb' +genCalendarConfig :: + forall bytes addressWidth calEntry maxDepth . + ( KnownNat bytes + , 1 <= bytes + , KnownNat maxDepth + , 1 <= maxDepth + , calEntry ~ Index maxDepth + , KnownNat addressWidth) => + SNat maxDepth -> + Gen (CalendarConfig bytes addressWidth calEntry) +genCalendarConfig sizeNat@(snatToNum -> dMax) = do + dA <- Gen.enum 1 dMax + dB <- Gen.enum 1 dMax + case (TN.someNatVal dA, TN.someNatVal dB) of + ( SomeNat (snatProxy -> depthA) + ,SomeNat (snatProxy -> depthB)) -> do + let + regAddrBits = SNat @(NatRequiredBits (Regs calEntry (bytes * 8))) + bsCalEntry = SNat @(BitSize calEntry) + case + ( isInBounds d1 depthA sizeNat + , isInBounds d1 depthB sizeNat + , compareSNat regAddrBits (SNat @addressWidth) + , compareSNat d1 bsCalEntry) of + (InBounds, InBounds, SNatLE, SNatLE) -> go depthA depthB + (a, b, c, d) -> error [I.i| + genCalendarConfig: calEntry constraints not satisfied: + + a: #{a} + b: #{b} + b: #{c} + c: #{d} + + ... + |] + where + go :: forall depthA depthB . + ( LessThan depthA maxDepth + , LessThan depthB maxDepth + , NatFitsInBits (Regs calEntry (bytes * 8)) addressWidth) => + SNat depthA -> + SNat depthB -> + Gen (CalendarConfig bytes addressWidth (Index maxDepth)) + go SNat SNat = do + calActive <- fromMaybe errmsg . fromList @depthA . P.take (natToNum @depthA) + <$> Gen.shuffle @_ @(Index maxDepth) [0.. natToNum @(maxDepth-1)] + calShadow <- fromMaybe errmsg . fromList @depthB . P.take (natToNum @depthB) + <$> Gen.shuffle @_ @(Index maxDepth) [0.. natToNum @(maxDepth-1)] + return $ CalendarConfig sizeNat calActive calShadow + errmsg = errorX "genCalendarConfig: list to vector conversion failed" + +-- | Check if the scatter unit with wishbone interface loses no frames. +scatterUnitNoFrameLoss :: Property +scatterUnitNoFrameLoss = property $ do + maxCalSize <- forAll $ Gen.enum 2 32 + case TN.someNatVal (maxCalSize - 1) of + SomeNat (succSNat . snatProxy -> p) -> do + runTest =<< forAll (genCal p) + where + runTest :: + (KnownNat maxSize, 1 <= maxSize) => + CalendarConfig 4 32 (Index maxSize) -> + PropertyT IO () + runTest calConfig@(CalendarConfig _ calA@(length -> depth) _) = do + -- Number of metacycles of input to generate + metaCycles <- forAll $ Gen.enum 1 10 + let + -- reset cycle + cycle delay, last metacycle's writes can be read in (metacycles + 1) + simLength = 2 + (1+metaCycles) * depth + inputGen = Gen.list (Range.singleton metaCycles) + metaCycleNothing = P.replicate depth Nothing + -- Generate at most depth `div` 2 elements to be written each metacycle since + -- we need two cycles to read a written element. + metaCycleGen = genFrameList (Range.singleton $ depth `div` 2) + + inputFrames <- forAll $ padToLength (simLength `div` depth + 1) metaCycleNothing + <$> inputGen (padToLength depth Nothing <$> metaCycleGen) + let + topEntity (unbundle -> (wbIn, linkIn)) = fst $ + withClockResetEnable clockGen resetGen enableGen (scatterUnitWb @System @_ @32) + calConfig (pure $ wishboneM2S SNat SNat) (pure False) linkIn wbIn + + wbReadOps = P.take simLength $ P.replicate depth idleM2S P.++ P.concat + (padToLength depth idleM2S . P.concat . P.zipWith wbRead (toList calA) <$> inputFrames) + + topEntityInput = P.zip wbReadOps (P.concat inputFrames) + simOut = simulateN simLength topEntity topEntityInput + footnote . fromString $ "simOut: " <> showX simOut + footnote . fromString $ "simIn: " <> showX wbReadOps + footnote . fromString $ "cal: " <> showX calA + wbDecoding simOut === P.take simLength (catMaybes (P.concat inputFrames)) + + genCal :: forall maxSize . 1 <= maxSize => SNat maxSize -> Gen (CalendarConfig 4 32 (Index maxSize)) + genCal SNat = genCalendarConfig @4 @32 (SNat @maxSize) + padToLength l padElement g = P.take l (g P.++ P.repeat padElement) + +-- | Check if the gather unit with wishbone interface loses no frames. +gatherUnitNoFrameLoss :: Property +gatherUnitNoFrameLoss = property $ do + maxCalSize <- forAll $ Gen.enum 2 32 + case TN.someNatVal (maxCalSize - 1) of + SomeNat (succSNat . snatProxy -> p) -> do + runTest =<< forAll (genCal p) + where + runTest :: + (KnownNat maxSize, 1 <= maxSize) => + CalendarConfig 4 32 (Index maxSize) -> PropertyT IO () + runTest calConfig@(CalendarConfig _ calA@(length -> depth) _) = do + metaCycles <- forAll $ Gen.enum 1 10 + let + simLength = 2 + (1+metaCycles) * depth + inputGen = Gen.list (Range.singleton metaCycles) + metaCycleNothing = P.replicate depth Nothing + metaCycleGen = genFrameList (Range.singleton $ depth `div` 2) + inputFrames <- forAll $ padToLength (simLength `div` depth + 1) metaCycleNothing + <$> inputGen (padToLength depth Nothing <$> metaCycleGen) + let + topEntity wbIn = (\ (a, _ ,_) -> a) $ + withClockResetEnable clockGen resetGen enableGen (gatherUnitWb @System @_ @32) + calConfig (pure $ wishboneM2S SNat SNat) (pure False) wbIn + + wbWriteOps = P.take simLength . P.concat $ + padToLength depth idleM2S . P.concat . P.zipWith wbWrite (toList calA) <$> inputFrames + + simOut = simulateN simLength topEntity wbWriteOps + addressedFrames = P.zip (P.concat inputFrames) (cycle $ toList calA) + writtenFrames = [if snd e /= 0 then fst e else Nothing | e <- addressedFrames] + prePad items = P.replicate (1+depth) Nothing P.++ items + expectedOutput = P.take simLength (fromMaybe 1 <$> P.filter isJust writtenFrames) + + footnote . fromString $ "simOut: " <> showX simOut + footnote . fromString $ "simIn: " <> showX wbWriteOps + footnote . fromString $ "cal: " <> showX calA + footnote . fromString $ "writtenFrames: " <> showX writtenFrames + + directedDecode (prePad writtenFrames) simOut === expectedOutput + + genCal :: forall maxSize . + 1 <= maxSize => + SNat maxSize -> + Gen (CalendarConfig 4 32 (Index maxSize)) + genCal SNat = genCalendarConfig @4 @32 (SNat @maxSize) + padToLength l padElement g = P.take l (g P.++ P.repeat padElement) + +directedDecode :: [Maybe a] -> [Maybe b] -> [b] +directedDecode ((Just _) : as) ((Just b) : bs) = b : directedDecode as bs +directedDecode (Nothing : as) (_ : bs) = directedDecode as bs +directedDecode _ _ = [] + +-- | Decode an incoming slave bus by consuming two acknowledged signals and concatenating +-- their readData's. +wbDecoding :: + KnownNat bytes => + [WishboneS2M bytes] -> + [BitVector ((8 * bytes) + (8 * bytes))] +wbDecoding (s2m0 : s2m1 : s2ms) + | acknowledge s2m0 && acknowledge s2m1 = out : wbDecoding s2ms + | otherwise = wbDecoding (s2m1 : s2ms) + where + out = readData s2m1 ++# readData s2m0 +wbDecoding _ = [] + +-- | Tranform a read address with expected frame into a wishbone read operation for testing +-- the 'scatterUnitWb'. The second argument indicate wether or not a frame can be read from +-- that read address. The read operation reads data over 2 read cycles. +wbRead :: + forall bytes addressWidth maxIndex a . + ( KnownNat bytes + , KnownNat addressWidth + , KnownNat maxIndex + , 1 <= maxIndex) => + Index maxIndex -> + Maybe a -> + [WishboneM2S bytes addressWidth] +wbRead readAddr (Just _) = + [ (wishboneM2S SNat (SNat @addressWidth)) + { addr = (`shiftL` 3) . resize $ pack readAddr + , busCycle = True + , strobe = True } + + , (wishboneM2S SNat (SNat @addressWidth)) + { addr = 4 .|. ((`shiftL` 3) . resize $ pack readAddr) + , busCycle = True + , strobe = True } + ] +wbRead _ Nothing = [] + +-- | Transform a write address with frame to a wishbone write operation for testing the +-- 'gatherUnitWb'. The write operation writes the incoming bitvector over 2 write cycles. +wbWrite :: + forall bytes addressWidth maxIndex . + ( KnownNat bytes + , KnownNat addressWidth + , KnownNat maxIndex + , 1 <= maxIndex) => + Index maxIndex -> + Maybe (BitVector (bytes*2*8)) -> + [WishboneM2S bytes addressWidth] +wbWrite writeAddr (Just frame) = + [ (wishboneM2S @bytes @addressWidth SNat SNat) + { addr = (`shiftL` 3) . resize $ pack writeAddr + , busSelect = maxBound + , busCycle = True + , strobe = True + , writeEnable = True + , writeData = lower } + + , (wishboneM2S @bytes @addressWidth SNat SNat) + { addr = 4 .|. ((`shiftL` 3) . resize $ pack writeAddr) + , busSelect = maxBound + , busCycle = True + , strobe = True + , writeEnable = True + , writeData = upper } + ] + where + (upper, lower) = split frame +wbWrite _ Nothing = [] + +-- | Idle 'WishboneM2S' bus. +idleM2S :: forall bytes aw . (KnownNat bytes, KnownNat aw) => WishboneM2S bytes aw +idleM2S = wishboneM2S SNat (SNat @aw) diff --git a/bittide/tests/Tests/Shared.hs b/bittide/tests/Tests/Shared.hs new file mode 100644 index 000000000..86654f22c --- /dev/null +++ b/bittide/tests/Tests/Shared.hs @@ -0,0 +1,20 @@ +-- SPDX-FileCopyrightText: 2022 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +{-# LANGUAGE GADTs #-} +module Tests.Shared where + +import Clash.Prelude + +data IsInBounds a b c where + InBounds :: (a <= b, b <= c) => IsInBounds a b c + NotInBounds :: IsInBounds a b c + +deriving instance Show (IsInBounds a b c) + +-- | Returns 'InBounds' when a <= b <= c, otherwise returns 'NotInBounds'. +isInBounds :: SNat a -> SNat b -> SNat c -> IsInBounds a b c +isInBounds a b c = case (compareSNat a b, compareSNat b c) of + (SNatLE, SNatLE) -> InBounds + _ -> NotInBounds diff --git a/contranomy/src/Contranomy/Wishbone.hs b/contranomy/src/Contranomy/Wishbone.hs index 7a867d553..dc609eab1 100644 --- a/contranomy/src/Contranomy/Wishbone.hs +++ b/contranomy/src/Contranomy/Wishbone.hs @@ -66,9 +66,9 @@ data BurstTypeExtension wishboneM2S :: SNat bytes -> SNat addressWidth -> WishboneM2S bytes addressWidth wishboneM2S SNat SNat = WishboneM2S - { addr = undefined - , writeData = undefined - , busSelect = undefined + { addr = deepErrorX "wishboneM2S: addr undefined." + , writeData = deepErrorX "wishboneM2S: writeData undefined." + , busSelect = deepErrorX "wishboneM2S: busSelect undefined." , busCycle = False , strobe = False , writeEnable = False @@ -79,7 +79,7 @@ wishboneM2S SNat SNat wishboneS2M :: SNat bytes -> WishboneS2M bytes wishboneS2M SNat = WishboneS2M - { readData = 0 + { readData = deepErrorX "wishboneM2S: readData undefined." , acknowledge = False , err = False }