|
1 | 1 | -- SPDX-FileCopyrightText: 2026 Google LLC |
2 | 2 | -- |
3 | 3 | -- SPDX-License-Identifier: Apache-2.0 |
| 4 | +{-# LANGUAGE OverloadedStrings #-} |
| 5 | + |
4 | 6 | module Tests.Protocols.Df.Extra where |
5 | 7 |
|
6 | 8 | import Clash.Prelude |
7 | 9 |
|
8 | | -import Hedgehog (Gen, Property, Range) |
| 10 | +import Clash.Hedgehog.Sized.BitVector |
| 11 | +import Clash.Hedgehog.Sized.Unsigned |
| 12 | +import Clash.Hedgehog.Sized.Vector |
| 13 | +import Data.Maybe |
| 14 | +import Data.String.Interpolate (i) |
| 15 | +import Hedgehog (Gen, Property, Range, assert, cover, footnote, forAll, (===)) |
9 | 16 | import Protocols |
10 | | -import Protocols.Hedgehog (defExpectOptions, idWithModelSingleDomain) |
| 17 | +import Protocols.Df.Extra (skid) |
| 18 | +import Protocols.Hedgehog ( |
| 19 | + ExpectOptions (..), |
| 20 | + defExpectOptions, |
| 21 | + idWithModelSingleDomain, |
| 22 | + idWithModelSingleDomainT, |
| 23 | + propWithModelSingleDomain, |
| 24 | + ) |
| 25 | +import Protocols.Internal (circuitMonitor) |
11 | 26 | import Test.Tasty (TestTree) |
12 | 27 | import Test.Tasty.Hedgehog (testProperty) |
13 | 28 | import Test.Tasty.TH (testGroupGenerator) |
14 | 29 |
|
15 | | -import Protocols.Df.Extra (skid) |
16 | | - |
17 | 30 | import qualified Clash.Prelude as C |
| 31 | +import qualified Data.List as L |
| 32 | +import qualified Hedgehog as H |
18 | 33 | import qualified Hedgehog.Gen as Gen |
19 | 34 | import qualified Hedgehog.Range as Range |
| 35 | +import qualified Protocols.Df as Df |
| 36 | +import qualified Protocols.Df.Extra as Df |
| 37 | +import qualified Prelude as P |
20 | 38 |
|
21 | 39 | smallInt :: Range Int |
22 | 40 | smallInt = Range.linear 0 10 |
@@ -51,5 +69,225 @@ prop_skid = |
51 | 69 | (C.exposeClockResetEnable id) |
52 | 70 | (C.exposeClockResetEnable skidDropReady) |
53 | 71 |
|
| 72 | +-- | Merges two BitVectors according to a mask. |
| 73 | +mergeWithMask :: |
| 74 | + forall bv m. |
| 75 | + (KnownNat m, KnownNat bv) => |
| 76 | + BitVector (bv * m) -> |
| 77 | + BitVector (bv * m) -> |
| 78 | + BitVector m -> |
| 79 | + BitVector (bv * m) |
| 80 | +mergeWithMask (unpack -> old) (unpack -> new) (unpack -> mask) = |
| 81 | + pack (mux @(Vec m) @(BitVector bv) mask new old) |
| 82 | + |
| 83 | +-- | Simply try reading the initial contents of a blockram |
| 84 | +prop_fromBlockram :: Property |
| 85 | +prop_fromBlockram = |
| 86 | + idWithModelSingleDomain @System |
| 87 | + defExpectOptions |
| 88 | + (genData (genUnsigned Range.linearBounded)) |
| 89 | + (\_ _ _ -> model) |
| 90 | + top |
| 91 | + where |
| 92 | + mem = iterate d16 succ 0 :: Vec 16 Int |
| 93 | + |
| 94 | + dut :: forall dom. (HiddenClockResetEnable dom) => Circuit (Df dom (Unsigned 4)) (Df dom Int) |
| 95 | + dut = circuit $ \rd -> do |
| 96 | + wr <- Df.empty |
| 97 | + Df.fromBlockram (\ena -> withEnable ena (blockRam mem)) -< (rd, wr) |
| 98 | + |
| 99 | + top clk rst ena0 = withClockResetEnable @System clk rst ena0 dut |
| 100 | + model = fmap (mem !!) |
| 101 | + |
| 102 | +-- | First write a new configuration to the blockram, then read it back |
| 103 | +prop_fromBlockramWrites :: Property |
| 104 | +prop_fromBlockramWrites = H.property $ do |
| 105 | + oldMem <- forAll $ genVec @16 $ Gen.integral Range.linearBounded |
| 106 | + newMem <- forAll $ genVec @16 $ Gen.integral Range.linearBounded |
| 107 | + let |
| 108 | + writes = L.zip [0 ..] (toList newMem) |
| 109 | + model = fmap (newMem !!) |
| 110 | + |
| 111 | + dut :: forall dom. (HiddenClockResetEnable dom) => Circuit (Df dom (Unsigned 4)) (Df dom Int) |
| 112 | + dut = circuit $ \rd0 -> do |
| 113 | + wr <- Df.drive def (fmap Just writes) |
| 114 | + rd1 <- Df.stall def{resetCycles = 0} StallWithNack [100] -< rd0 |
| 115 | + Df.fromBlockram (\ena -> withEnable ena (blockRam oldMem)) -< (rd1, wr) |
| 116 | + |
| 117 | + top clk rst ena0 = withClockResetEnable @System clk rst ena0 dut |
| 118 | + |
| 119 | + idWithModelSingleDomainT @System |
| 120 | + defExpectOptions |
| 121 | + (genData (genUnsigned Range.linearBounded)) |
| 122 | + (\_ _ _ -> model) |
| 123 | + top |
| 124 | + |
| 125 | +-- | Write a configuration to the blockram with byte enables, then read it back |
| 126 | +prop_fromBlockramWithMaskWrites :: Property |
| 127 | +prop_fromBlockramWithMaskWrites = H.property $ do |
| 128 | + oldMem <- forAll $ genVec @8 genDefinedBitVector |
| 129 | + newValues <- forAll $ genVec genDefinedBitVector |
| 130 | + masks <- forAll $ genVec genDefinedBitVector |
| 131 | + |
| 132 | + let |
| 133 | + newMem = zipWith3 mergeWithMask oldMem newValues masks |
| 134 | + |
| 135 | + -- First write old memory with full masks, then write new values with given masks |
| 136 | + writes = |
| 137 | + L.zip3 [0 ..] (L.repeat maxBound) (toList oldMem) |
| 138 | + <> L.zip3 [0 ..] (toList masks) (toList newValues) |
| 139 | + model = fmap (newMem !!) |
| 140 | + |
| 141 | + dut :: |
| 142 | + forall dom. (HiddenClockResetEnable dom) => Circuit (Df dom (Unsigned 3)) (Df dom (BitVector 32)) |
| 143 | + dut = circuit $ \rd0 -> do |
| 144 | + wr <- Df.drive def{resetCycles = 0} (fmap Just writes) |
| 145 | + rd1 <- Df.stall def{resetCycles = 0} StallWithNack [50] -< rd0 |
| 146 | + Df.fromBlockramWithMask (exposeEnable $ blockRamByteAddressableU d8) -< (rd1, wr) |
| 147 | + |
| 148 | + top clk rst ena0 = withClockResetEnable @System clk rst ena0 dut |
| 149 | + |
| 150 | + idWithModelSingleDomainT @System |
| 151 | + defExpectOptions{eoStopAfterEmpty = Just 100} |
| 152 | + (genData (genUnsigned Range.linearBounded)) |
| 153 | + (\_ _ _ -> model) |
| 154 | + top |
| 155 | + |
| 156 | +prop_fromDSignal :: Property |
| 157 | +prop_fromDSignal = |
| 158 | + idWithModelSingleDomain @System |
| 159 | + defExpectOptions |
| 160 | + (genData genSmallInt) |
| 161 | + (\_ _ _ -> id) |
| 162 | + dut |
| 163 | + where |
| 164 | + reference clk ena = withClock clk $ withEnable ena $ delayN d10 (0 :: Int) |
| 165 | + dut clk rst ena = Df.fromDSignal clk rst ena (reference clk) |
| 166 | + |
| 167 | +{- | Verify that the circuit always produces less backpressure than it receives |
| 168 | +This should check that the circuit can run at without more stalls than strictly necessary |
| 169 | +-} |
| 170 | +prop_fromDSignalBackpressure :: Property |
| 171 | +prop_fromDSignalBackpressure = H.property $ do |
| 172 | + inputData <- forAll $ Gen.list (Range.linear 0 20) $ Gen.maybe $ pure () |
| 173 | + stalls <- forAll (Gen.list (Range.linear 0 10) (Gen.integral (Range.linear 0 10))) |
| 174 | + let |
| 175 | + reference clk ena = withClock @System clk $ withEnable ena $ delayN d5 () |
| 176 | + dut clk rst ena = Df.fromDSignal clk rst ena (reference clk) |
| 177 | + top clk rst ena = circuit $ do |
| 178 | + (drive1, driveMonitor) <- circuitMonitor <| driveC def inputData |
| 179 | + (sample1, sampleMonitor) <- circuitMonitor <| dut clk rst ena -< drive1 |
| 180 | + withReset rst Df.consume <| Df.stall def{resetCycles = 0} StallCycle stalls -< sample1 |
| 181 | + idC -< (driveMonitor, sampleMonitor) |
| 182 | + |
| 183 | + isStalled (fwd, (Ack bwd)) = isJust fwd && not bwd |
| 184 | + isTransfer (fwd, (Ack bwd)) = isJust fwd && bwd |
| 185 | + isIdle (fwd, _) = isNothing fwd |
| 186 | + |
| 187 | + getStalls = L.scanl (\acc inps -> if isStalled inps then succ acc else acc) (0 :: Int) |
| 188 | + getTransfers = L.foldl (\acc inps -> if isTransfer inps then succ acc else acc) (0 :: Int) |
| 189 | + getIdles = L.foldl (\acc inps -> if isIdle inps then succ acc else acc) (0 :: Int) |
| 190 | + (driveSignals, sampleSignals) = sampleC def{timeoutAfter = 200} (top clockGen resetGen enableGen) |
| 191 | + driveStalls = getStalls driveSignals |
| 192 | + sampleStalls = getStalls sampleSignals |
| 193 | + |
| 194 | + assert (getTransfers driveSignals == L.length (catMaybes inputData)) |
| 195 | + assert (getTransfers sampleSignals == L.length (catMaybes inputData)) |
| 196 | + cover 1 "Idle cycles in driver" (getIdles driveSignals > 0) |
| 197 | + cover 1 "Idle cycles in sampler" (getIdles sampleSignals > 0) |
| 198 | + |
| 199 | + footnote |
| 200 | + $ [i|Drive stalls: #{show (runLengthEncode driveStalls)} \nSample stalls: #{show (runLengthEncode sampleStalls)}|] |
| 201 | + assert $ and $ L.zipWith (<=) driveStalls sampleStalls |
| 202 | + |
| 203 | +-- | Utility function to run-length encode a list |
| 204 | +runLengthEncode :: (Eq a) => [a] -> [(a, Int)] |
| 205 | +runLengthEncode = go Nothing |
| 206 | + where |
| 207 | + go (Just (a, n)) (x : xs) |
| 208 | + | a == x = go (Just (a, n + 1)) xs |
| 209 | + | otherwise = (a, n) : go (Just (x, 1)) xs |
| 210 | + go Nothing (x : xs) = go (Just (x, 1)) xs |
| 211 | + go (Just s) [] = [s] |
| 212 | + go Nothing [] = [] |
| 213 | + |
| 214 | +prop_iterate :: Property |
| 215 | +prop_iterate = |
| 216 | + propWithModelSingleDomain |
| 217 | + defExpectOptions{eoResetCycles = 10} |
| 218 | + gen |
| 219 | + (\_ _ _ -> model) |
| 220 | + dut |
| 221 | + prop |
| 222 | + where |
| 223 | + f = (+ 1) :: Int -> Int |
| 224 | + model = const $ L.take 100 (P.iterate f 0) |
| 225 | + |
| 226 | + -- After 100 cycles stall comes out of reset and stalls communication to |
| 227 | + -- terminate the simulation. |
| 228 | + dut = |
| 229 | + exposeClockResetEnable |
| 230 | + (Df.stall def{resetCycles = 100} StallCycle [1000] <| Df.iterate f 0 :: Circuit () (Df System Int)) |
| 231 | + gen = pure () |
| 232 | + prop expected actual = do |
| 233 | + let len = L.length actual |
| 234 | + footnote [i|Expected length: Actual length: #{show len}|] |
| 235 | + assert (len > 10) |
| 236 | + L.take len expected === actual |
| 237 | + |
| 238 | +-- Start of shamelessly copied code from bittide |
| 239 | + |
| 240 | +{- | Version of 'blockRamByteAddressable' with undefined initial contents. It is similar |
| 241 | +to 'blockRam' with the addition that it takes a byte select signal that controls |
| 242 | +which nBytes at the write address are updated. |
| 243 | +-} |
| 244 | +blockRamByteAddressableU :: |
| 245 | + forall dom memDepth n m addr. |
| 246 | + ( HiddenClockResetEnable dom |
| 247 | + , Enum addr |
| 248 | + , NFDataX addr |
| 249 | + , KnownNat memDepth |
| 250 | + , 1 <= memDepth |
| 251 | + , KnownNat n |
| 252 | + , KnownNat m |
| 253 | + ) => |
| 254 | + -- | Memory depth |
| 255 | + SNat memDepth -> |
| 256 | + -- | Read address. |
| 257 | + Signal dom addr -> |
| 258 | + -- | Write operation. |
| 259 | + Signal dom (Maybe (addr, BitVector (n * m))) -> |
| 260 | + -- | Byte enables that determine which nBytes get replaced. |
| 261 | + Signal dom (BitVector n) -> |
| 262 | + -- | Data at read address (1 cycle delay). |
| 263 | + Signal dom (BitVector (n * m)) |
| 264 | +blockRamByteAddressableU SNat readAddr newEntry byteSelect = |
| 265 | + pack <$> readBytes |
| 266 | + where |
| 267 | + writeBytes = unbundle $ splitWriteInBytes <$> newEntry <*> byteSelect |
| 268 | + readBytes = bundle $ ram readAddr <$> writeBytes |
| 269 | + ram = blockRamU NoClearOnReset (SNat @memDepth) |
| 270 | + |
| 271 | +{- | Takes singular write operation (Maybe (Index maxIndex, writeData)) and splits it up |
| 272 | +according to a supplied byteselect bitvector into a vector of byte sized write operations |
| 273 | +(Maybe (Index maxIndex, Byte)). |
| 274 | +-} |
| 275 | +splitWriteInBytes :: |
| 276 | + forall addr m n. |
| 277 | + (KnownNat n, KnownNat m) => |
| 278 | + -- | Incoming write operation. |
| 279 | + Maybe (addr, BitVector (n * m)) -> |
| 280 | + -- | Incoming byte enables. |
| 281 | + BitVector m -> |
| 282 | + -- | Per byte write operation. |
| 283 | + Vec m (Maybe (addr, BitVector n)) |
| 284 | +splitWriteInBytes (Just (addr, writeData)) byteSelect = |
| 285 | + (\m d -> if m then Just d else Nothing) |
| 286 | + <$> unpack byteSelect |
| 287 | + <*> fmap (addr,) (unpack writeData) |
| 288 | +splitWriteInBytes Nothing _ = repeat Nothing |
| 289 | + |
| 290 | +-- End of shamelessly copied code from bittide |
| 291 | + |
54 | 292 | tests :: TestTree |
55 | 293 | tests = $(testGroupGenerator) |
0 commit comments