diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac61ed8..2280b17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,6 +163,11 @@ jobs: - name: Extract VexRiscv Integration Tests run: | tar -x -f vexriscv-test-binaries.tar - - name: Run unittests + + - name: Run `clash-vexriscv` unittests + run: | + cabal run clash-vexriscv:unittests + + - name: Run `clash-vexriscv-sim` unittests run: | cabal run clash-vexriscv-sim:unittests diff --git a/clash-vexriscv/clash-vexriscv.cabal b/clash-vexriscv/clash-vexriscv.cabal index 42f547a..44d68ef 100644 --- a/clash-vexriscv/clash-vexriscv.cabal +++ b/clash-vexriscv/clash-vexriscv.cabal @@ -102,6 +102,7 @@ library default-language: Haskell2010 exposed-modules: VexRiscv + VexRiscv.ClockTicks VexRiscv.FFI VexRiscv.TH build-depends: @@ -118,3 +119,24 @@ library Glob, extra-libraries: VexRiscvFFI, stdc++ include-dirs: src/ + +test-suite unittests + import: common-options + hs-source-dirs: tests/unittests + type: exitcode-stdio-1.0 + main-is: main.hs + ghc-options: -Wall -Wcompat -threaded -rtsopts + other-modules: + Tests.Extra + Tests.VexRiscv.ClockTicks + build-depends: + HUnit, + base, + clash-vexriscv, + bytestring, + hedgehog >= 1.0 && < 1.1, + tasty >= 1.4 && < 1.5, + tasty-hedgehog >= 1.2 && < 1.3, + tasty-hunit, + tasty-th, + template-haskell, diff --git a/clash-vexriscv/src/VexRiscv/ClockTicks.hs b/clash-vexriscv/src/VexRiscv/ClockTicks.hs new file mode 100644 index 0000000..3751ac7 --- /dev/null +++ b/clash-vexriscv/src/VexRiscv/ClockTicks.hs @@ -0,0 +1,283 @@ +-- SPDX-FileCopyrightText: 2024 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +{-# LANGUAGE NamedFieldPuns #-} + +-- | Utilities dealing with clock ticks and edges in Clash. +-- +-- TODO: Figure out whether we want to upstream as is, or whether we want to +-- generalize to /N/ clocks first. +module VexRiscv.ClockTicks + ( ClockEdgeAB(..) + , clockTicksAbsolute + , clockTicksRelative + , clockEdgesAbsolute + , clockEdgesRelative + ) where + +import Prelude + +import Data.Coerce (coerce) +import Data.Int (Int64) +import Data.List (mapAccumL) +import Data.Ord () + +import Clash.Promoted.Nat (snatToNum) +import Clash.Signal + ( ActiveEdge(..), KnownDomain, Clock, SDomainConfiguration(..), knownDomain + , SActiveEdge(..), activeEdge + ) +import Clash.Signal.Internal (Signal((:-)), ClockAB(..), Femtoseconds(..), Clock(..)) + +-- | Given two clocks, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. In contrast to 'clockTicks', +-- this version also produces the absolute time at which the tick happened. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockB:ClockA:@. +-- +-- Returned time is in /femtoseconds/. +clockTicksAbsolute :: + (KnownDomain domA, KnownDomain domB) => + Clock domA -> + Clock domB -> + [(Int64, ClockAB)] +clockTicksAbsolute clkA clkB = + clockTicksEitherAbsolute (toEither clkA) (toEither clkB) + +-- | Given two clocks, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. In contrast to 'clockTicks', +-- this version also produces the time since the last tick. Note that the first +-- "time since last tick" is always zero. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockB:ClockA:@. +-- +-- Returned time is in /femtoseconds/. +clockTicksRelative :: + (KnownDomain domA, KnownDomain domB) => + Clock domA -> + Clock domB -> + [(Int64, ClockAB)] +clockTicksRelative clkA clkB = + clockTicksEitherRelative (toEither clkA) (toEither clkB) + +-- | Given two clocks, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockEdgeB edgeB : ClockEdgeA edgeA:@. +-- +-- Returned time is in /femtoseconds/. +clockEdgesAbsolute :: + forall domA domB . + (KnownDomain domA, KnownDomain domB) => + Clock domA -> + Clock domB -> + [(Int64, ClockEdgeAB)] +clockEdgesAbsolute clkA clkB = + clockEdgesEitherAbsolute + (toActiveEdge (activeEdge @domA)) (toActiveEdge (activeEdge @domB)) + (toEither clkA) (toEither clkB) + +-- | Given two clocks, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockEdgeB edgeB : ClockEdgeA edgeA:@. +-- +-- Returned time is in /femtoseconds/. +clockEdgesRelative :: + forall domA domB . + (KnownDomain domA, KnownDomain domB) => + Clock domA -> + Clock domB -> + [(Int64, ClockEdgeAB)] +clockEdgesRelative clkA clkB = + clockEdgesEitherRelative + (toActiveEdge (activeEdge @domA)) (toActiveEdge (activeEdge @domB)) + (toEither clkA) (toEither clkB) + +-- | GADT version of 'ActiveEdge' to 'ActiveEdge' conversion +toActiveEdge :: SActiveEdge edge -> ActiveEdge +toActiveEdge SRising = Rising +toActiveEdge SFalling = Falling + +toEither :: + forall dom. + KnownDomain dom => + Clock dom -> + Either Int64 (Signal dom Int64) +toEither (Clock _ maybePeriods) + | Just periods <- maybePeriods = + Right (unFemtosecondsSignal periods) + | SDomainConfiguration{sPeriod} <- knownDomain @dom = + -- Convert to femtoseconds - dynamic clocks use them + Left (1000 * snatToNum sPeriod) + where + -- Coerce whole signal instead of `fmap coerce` to prevent useless constructor + -- packing and unpacking. + unFemtosecondsSignal :: Signal dom Femtoseconds -> Signal dom Int64 + unFemtosecondsSignal = coerce + +-- | Given two clock periods, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. In contrast to 'clockTicksEither', +-- this version also produces the absolute time at which the event happened. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockB:ClockA:@. +clockTicksEitherAbsolute :: + Either Int64 (Signal domA Int64) -> + Either Int64 (Signal domB Int64) -> + [(Int64, ClockAB)] +clockTicksEitherAbsolute clkA clkB = + case (clkA, clkB) of + (Left tA, Left tB) | tA == tB -> zip (iterate (+tA) 0) (repeat ClockAB) + (Left tA, Left tB) -> goStatic 0 0 tA tB + (Right tA, Right tB) -> goDynamic 0 0 tA tB + (Left tA, Right tB) -> clockTicksEitherAbsolute (Right (pure tA)) (Right tB) + (Right tA, Left tB) -> clockTicksEitherAbsolute (Right tA) (Right (pure tB)) + where + goStatic :: Int64 -> Int64 -> Int64 -> Int64 -> [(Int64, ClockAB)] + goStatic absTimeA absTimeB tA tB = + case compare absTimeA absTimeB of + LT -> (absTimeA, ClockA) : goStatic (absTimeA + tA) absTimeB tA tB + EQ -> (absTimeA, ClockAB) : goStatic (absTimeA + tA) (absTimeB + tB) tA tB + GT -> (absTimeB, ClockB) : goStatic absTimeA (absTimeB + tB) tA tB + + goDynamic :: Int64 -> Int64 -> Signal domA Int64 -> Signal domB Int64 -> [(Int64, ClockAB)] + goDynamic absTimeA absTimeB tsA@(~(tA :- tsA0)) tsB@(~(tB :- tsB0)) = + -- Even though we lazily match on the signal's constructor, this shouldn't + -- build up a significant chain of chunks as 'absTimeX' gets evaluated + -- every iteration. + case compare absTimeA absTimeB of + LT -> (absTimeA, ClockA) : goDynamic (absTimeA + tA) absTimeB tsA0 tsB + EQ -> (absTimeA, ClockAB) : goDynamic (absTimeA + tA) (absTimeB + tB) tsA0 tsB0 + GT -> (absTimeB, ClockB) : goDynamic absTimeA (absTimeB + tB) tsA tsB0 + +-- | Given two clock periods, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. In contrast to 'clockTicksEither', +-- this version also produces the time since the last event. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockB:ClockA:@. +clockTicksEitherRelative :: + Either Int64 (Signal domA Int64) -> + Either Int64 (Signal domB Int64) -> + [(Int64, ClockAB)] +clockTicksEitherRelative clkA clkB = zip relativeTimestamps ticks + where + relativeTimestamps = 0 : zipWith (-) (tail timestamps) timestamps + (timestamps, ticks) = unzip (clockTicksEitherAbsolute clkA clkB) + +-- | Flip edge from rising to falling, and vice versa +oppositeEdge :: ActiveEdge -> ActiveEdge +oppositeEdge Rising = Falling +oppositeEdge Falling = Rising + +data ClockEdgeAB + = ClockEdgeA !ActiveEdge + | ClockEdgeB !ActiveEdge + | ClockEdgeAB !ActiveEdge !ActiveEdge + deriving (Show, Eq) + +-- | Given two clock periods, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. In contrast to 'clockTicksEither', +-- this version also produces the absolute time at which the event happened. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockEdgeB edgeB : ClockEdgeA edgeA:@. +clockEdgesEitherAbsolute :: + ActiveEdge -> + -- ^ First active edge for clock A + ActiveEdge -> + -- ^ First active edge for clock B + Either Int64 (Signal domA Int64) -> + -- ^ Clock periods for clock A + Either Int64 (Signal domB Int64) -> + -- ^ Clock periods for clock B + [(Int64, ClockEdgeAB)] +clockEdgesEitherAbsolute firstEdgeA firstEdgeB clkA clkB = + case (clkA, clkB) of + (Left tA, Left tB) | tA == tB -> goSame (halve tA) + (Left tA, Left tB) -> goStatic 0 0 firstEdgeA firstEdgeB (halve tA) (halve tB) + (Right tA, Right tB) -> goDynamic 0 0 firstEdgeA firstEdgeB (halves tA) (halves tB) + (Left tA, Right tB) -> + clockEdgesEitherAbsolute firstEdgeA firstEdgeB (Right (pure tA)) (Right tB) + (Right tA, Left tB) -> + clockEdgesEitherAbsolute firstEdgeA firstEdgeB (Right tA) (Right (pure tB)) + where + halves = go . fmap halve + where + go ((t0, t1) :- ts) = t0 :- t1 :- go ts + + halve t = + ( t `div` 2 + , t - (t `div` 2) + ) + + goSame :: (Int64, Int64) -> [(Int64, ClockEdgeAB)] + goSame (t0, t1) = + zip + (snd $ mapAccumL (\acc t -> (acc + t, acc)) 0 (cycle [t0, t1])) + (cycle [ ClockEdgeAB firstEdgeA firstEdgeB + , ClockEdgeAB (oppositeEdge firstEdgeA) (oppositeEdge firstEdgeB) + ]) + + goStatic :: + Int64 -> Int64 -> + ActiveEdge -> ActiveEdge -> + (Int64, Int64) -> (Int64, Int64) -> + [(Int64, ClockEdgeAB)] + goStatic absTimeA absTimeB !edgeA !edgeB (tA0, tA1) (tB0, tB1) = + case compare absTimeA absTimeB of + -- XXX: Sorry for breaking the 80/90 limit. I have no idea how to break this + -- over multiple lines without sacrificing readability. + LT -> (absTimeA, ClockEdgeA edgeA) : goStatic (absTimeA + tA0) absTimeB (oppositeEdge edgeA) edgeB (tA1, tA0) (tB0, tB1) + EQ -> (absTimeA, ClockEdgeAB edgeA edgeB) : goStatic (absTimeA + tA0) (absTimeB + tB0) (oppositeEdge edgeA) (oppositeEdge edgeB) (tA1, tA0) (tB1, tB0) + GT -> (absTimeB, ClockEdgeB edgeB) : goStatic absTimeA (absTimeB + tB0) edgeA (oppositeEdge edgeB) (tA0, tA1) (tB1, tB0) + + goDynamic :: + Int64 -> Int64 -> + ActiveEdge -> ActiveEdge -> + Signal domA Int64 -> Signal domB Int64 -> + [(Int64, ClockEdgeAB)] + goDynamic absTimeA absTimeB edgeA edgeB tsA@(~(tA :- tsA0)) tsB@(~(tB :- tsB0)) = + -- Even though we lazily match on the signal's constructor, this shouldn't + -- build up a significant chain of chunks as 'absTimeX' gets evaluated + -- every iteration. + case compare absTimeA absTimeB of + -- XXX: Sorry for breaking the 80/90 limit. I have no idea how to break this + -- over multiple lines without sacrificing readability. + LT -> (absTimeA, ClockEdgeA edgeA) : goDynamic (absTimeA + tA) absTimeB (oppositeEdge edgeA) edgeB tsA0 tsB + EQ -> (absTimeA, ClockEdgeAB edgeA edgeB) : goDynamic (absTimeA + tA) (absTimeB + tB) (oppositeEdge edgeA) (oppositeEdge edgeB) tsA0 tsB0 + GT -> (absTimeB, ClockEdgeB edgeB) : goDynamic absTimeA (absTimeB + tB) edgeA (oppositeEdge edgeB) tsA tsB0 + +-- | Given two clock periods, produce a list of clock ticks indicating which clock +-- (or both) ticked. Can be used in components handling multiple clocks, such +-- as @unsafeSynchronizer@ or dual clock FIFOs. In contrast to 'clockTicksEither', +-- this version also produces the time since the last event. For the first edge +-- the time since the last event is set to zero. +-- +-- If your primitive does not care about coincided clock edges, it should - by +-- convention - replace it by @ClockEdgeB edgeB : ClockEdgeA edgeA:@. +clockEdgesEitherRelative :: + ActiveEdge -> + -- ^ First active edge for clock A + ActiveEdge -> + -- ^ First active edge for clock B + Either Int64 (Signal domA Int64) -> + Either Int64 (Signal domB Int64) -> + [(Int64, ClockEdgeAB)] +clockEdgesEitherRelative firstEdgeA firstEdgeB clkA clkB = zip relativeTimestamps ticks + where + relativeTimestamps = 0 : zipWith (-) (tail timestamps) timestamps + (timestamps, ticks) = unzip (clockEdgesEitherAbsolute firstEdgeA firstEdgeB clkA clkB) diff --git a/clash-vexriscv/tests/unittests/Tests/Extra.hs b/clash-vexriscv/tests/unittests/Tests/Extra.hs new file mode 100644 index 0000000..3491650 --- /dev/null +++ b/clash-vexriscv/tests/unittests/Tests/Extra.hs @@ -0,0 +1,35 @@ +-- SPDX-FileCopyrightText: 2024 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +module Tests.Extra where + +import Prelude + +import Data.Functor ((<&>)) +import Language.Haskell.TH (mkName) +import Language.Haskell.TH.Lib + +-- | Generate a do-expression where each statement is a call to @test@ and the +-- arguments are determined by the carthesian product of given argument names. +-- +-- For example: +-- +-- > carthesianProductTests ["x", "y"] +-- > ======> +-- > do +-- > test x x +-- > test x y +-- > test y x +-- > test y y +-- +carthesianProductTests :: [String] -> ExpQ +carthesianProductTests names = doE $ + cartProd names <&> \(aName, bName) -> noBindS $ + let + aExp = varE (mkName aName) + bExp = varE (mkName bName) + in + [| test $aExp $bExp |] + where + cartProd xs = [(a, b) | a <- xs, b <- xs] diff --git a/clash-vexriscv/tests/unittests/Tests/VexRiscv/ClockTicks.hs b/clash-vexriscv/tests/unittests/Tests/VexRiscv/ClockTicks.hs new file mode 100644 index 0000000..fa6c90b --- /dev/null +++ b/clash-vexriscv/tests/unittests/Tests/VexRiscv/ClockTicks.hs @@ -0,0 +1,249 @@ +-- SPDX-FileCopyrightText: 2024 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +{-# LANGUAGE NoMonoLocalBinds #-} +{-# LANGUAGE TemplateHaskell #-} + +-- Suppress Clash domain warnings +{-# OPTIONS_GHC -Wno-orphans #-} + +-- Clock definitions aren't much more readable with top level signatures.. +{-# OPTIONS_GHC -Wno-missing-signatures #-} + +module Tests.VexRiscv.ClockTicks where + +import qualified Prelude as P +import Clash.Explicit.Prelude hiding (d122, d107, d61) + +import VexRiscv.ClockTicks + ( ClockEdgeAB(..), clockTicksAbsolute, clockTicksRelative + , clockEdgesAbsolute, clockEdgesRelative + ) +import Clash.Signal.Internal (ClockAB(..), Femtoseconds(..), clockTicks, dynamicClockGen) + +import qualified Data.List as L +import Data.Maybe (catMaybes) +import Data.Int (Int64) + +import Test.Tasty +import Test.Tasty.TH +import Test.Tasty.HUnit + +import Tests.Extra (carthesianProductTests) + +createDomain vSystem{vName="R61", vPeriod=61} +createDomain vSystem{vName="R107", vPeriod=107} +createDomain vSystem{vName="R122", vPeriod=122} + +createDomain vSystem{vName="F61", vPeriod=61, vActiveEdge=Falling} +createDomain vSystem{vName="F107", vPeriod=107, vActiveEdge=Falling} +createDomain vSystem{vName="F122", vPeriod=122, vActiveEdge=Falling} + +createDomain vSystem{vName="D61", vPeriod=61} +createDomain vSystem{vName="D107", vPeriod=107} +createDomain vSystem{vName="D122", vPeriod=122} + +r61 = clockGen @R61 +r107 = clockGen @R107 +r122 = clockGen @R122 + +f61 = clockGen @F61 +f107 = clockGen @F107 +f122 = clockGen @F122 + +-- | Clock whose clock period differs slightly from 61 ps every tick +d61 :: Clock D61 +d61 = dynamicClockGen (fromList periods) + where + -- Note that the random values are subtracted as femtoseconds. This makes sure + -- we end up with periods that are not divisable by 2, triggering an interesting + -- test case. + periods = P.cycle $ (\r -> Femtoseconds (1000*61 + r)) <$> rands + rands = [2, 3, 5, 8,-1, 7, -8, -2, -5, -7, -10, -9, 1, -3, 10, 0, 6,-6, 9, -4, 4] + +-- | Clock whose clock period differs slightly from 107 ps every tick +d107 :: Clock D107 +d107 = dynamicClockGen (fromList periods) + where + periods = P.cycle $ (\r -> Femtoseconds (1000*107 + r)) <$> rands + rands = [-1, -5, -3, 2, -8, -4, 8, -9, 9, 5, -6, 1, 6, 4, 0, 3, 7, -2, -7, 10, -10] + +-- | Clock whose clock period differs slightly from 122 ps every tick +d122 :: Clock D122 +d122 = dynamicClockGen (fromList periods) + where + periods = P.cycle $ (\r -> Femtoseconds (1000*122 + r)) <$> rands + rands = [0, 3, -8, -6, 10, -9, -4, -3, 5, 1, -10, 8, -1, 4, 6, -5, 2, -7, -2, 9, 7] + +-- | Compare to "infinite" lists, by comparing the first /N/ samples. See +-- implemenation for the value of /N/. +infEq :: (Eq a, Show a) => [a] -> [a] -> Assertion +infEq as bs = let n = 10000 in P.take n as @=? P.take n bs + +-- | Convert specific edges of 'ClockEdgeAB' to 'ClockAB'. +toClockAB :: ActiveEdge -> ActiveEdge -> ClockEdgeAB -> Maybe ClockAB +toClockAB filterA filterB = go + where + go (ClockEdgeA edge) | edge == filterA = Just ClockA + go (ClockEdgeB edge) | edge == filterB = Just ClockB + go (ClockEdgeAB edgeA edgeB) + | edgeA == filterA && edgeB == filterB = Just ClockAB + | edgeA == filterA = Just ClockA + | edgeB == filterB = Just ClockB + go _ = Nothing + +clockToActiveEdge :: forall dom. KnownDomain dom => Clock dom -> ActiveEdge +clockToActiveEdge _clk = case activeEdge @dom of + SRising -> Rising + SFalling -> Falling + +clockToPeriod :: forall dom a. (Integral a, KnownDomain dom) => Clock dom -> a +clockToPeriod _clk = snatToNum (clockPeriod @dom) + +-- | Convert specific edges of 'ClockEdgeAB' to 'ClockAB'. +toClockABs :: ActiveEdge -> ActiveEdge -> [ClockEdgeAB] -> [ClockAB] +toClockABs filterA filterB = catMaybes . P.map (toClockAB filterA filterB) + +-- | Convert a list of relative event timestamps to a list of absolute timestamps +relativeToAbsolute :: [Int64] -> [Int64] +relativeToAbsolute = snd . L.mapAccumL (\acc t -> let new = acc + t in (new, new)) 0 + +-- | Convert a list of absolute event timestamps to a list of relative timestamps +absoluteToRelative :: [Int64] -> [Int64] +absoluteToRelative absoluteTimestamps = + 0 : P.zipWith (-) (P.tail absoluteTimestamps) absoluteTimestamps + +unzipFirst :: ([a] -> [b]) -> [(a, c)] -> [(b, c)] +unzipFirst f (P.unzip -> (as, cs)) = P.zip (f as) cs + +unzipSecond :: ([a] -> [b]) -> [(c, a)] -> [(c, b)] +unzipSecond f (P.unzip -> (cs, as)) = P.zip cs (f as) + +-- | Check that 'clockTicksAbsolute' produces the same ratio of clock ticks as +-- @clash-prelude@'s 'clockTicks' +case_eqClockTicksAbsolute :: Assertion +case_eqClockTicksAbsolute = + $(carthesianProductTests ["r61", "r107", "r122", "d61", "d107", "d122", "f61", "f107", "f122"]) + where + test a b = fmap snd (clockTicksAbsolute a b) `infEq` clockTicks a b + +-- | Check that 'clockTicksRelative' produces the same ratio of clock ticks as +-- @clash-prelude@'s 'clockTicks' +case_eqClockTicksRelative :: Assertion +case_eqClockTicksRelative = + $(carthesianProductTests ["r61", "r107", "r122", "d61", "d107", "d122", "f61", "f107", "f122"]) + where + test a b = fmap snd (clockTicksRelative a b) `infEq` clockTicks a b + +-- | Check that 'clockEdgesAbsolute' produces the same ratio of clock ticks as +-- @clash-prelude@'s 'clockTicks' +case_eqClockEdgesAbsolute :: Assertion +case_eqClockEdgesAbsolute = + $(carthesianProductTests ["r61", "r107", "r122", "d61", "d107", "d122", "f61", "f107", "f122"]) + where + test a b = go a b (clockEdgesAbsolute a b) `infEq` clockTicks a b + go a b = toClockABs (clockToActiveEdge a) (clockToActiveEdge b) . fmap snd + +-- | Check that 'clockEdgesRelative' produces the same ratio of clock ticks as +-- @clash-prelude@'s 'clockTicks' +case_eqClockEdgesRelative :: Assertion +case_eqClockEdgesRelative = + $(carthesianProductTests ["r61", "r107", "r122", "d61", "d107", "d122", "f61", "f107", "f122"]) + where + test a b = go a b (clockEdgesRelative a b) `infEq` clockTicks a b + go a b = toClockABs (clockToActiveEdge a) (clockToActiveEdge b) . fmap snd + +-- | Check that 'clockEdgesAbsolute' produces the same ratio of clock ticks and +-- same timestamps as 'clockTicksAbsolute'. +case_eqClockEdgesTicksAbsolute :: Assertion +case_eqClockEdgesTicksAbsolute = + $(carthesianProductTests ["r61", "r107", "r122", "d61", "d107", "d122", "f61", "f107", "f122"]) + where + test a b = go a b (clockEdgesAbsolute a b) `infEq` clockTicksAbsolute a b + + go a b (P.unzip -> (times, edges)) = + catMaybes (P.zipWith (liftA2 (,)) maybeTimes maybeEdges) + where + maybeTimes = Just <$> times + maybeEdges = toClockAB (clockToActiveEdge a) (clockToActiveEdge b) <$> edges + +-- | Check that 'clockEdgesRelative' produces the same ratio of clock ticks and +-- same timestamps as 'clockTicksRelative'. +case_eqClockEdgesTicksRelative :: Assertion +case_eqClockEdgesTicksRelative = + $(carthesianProductTests ["r61", "r107", "r122", "d61", "d107", "d122", "f61", "f107", "f122"]) + where + test a b = go a b (clockEdgesRelative a b) `infEq` clockTicksRelative a b + + go a b (P.unzip -> (relativeTimes, edges)) = + unzipFirst + absoluteToRelative + (catMaybes (P.zipWith (liftA2 (,)) maybeAbsoluteTimes maybeEdges)) + where + maybeAbsoluteTimes :: [Maybe Int64] + maybeAbsoluteTimes = Just <$> absoluteTimes + + absoluteTimes :: [Int64] + absoluteTimes = relativeToAbsolute relativeTimes + + maybeEdges :: [Maybe ClockAB] + maybeEdges = toClockAB (clockToActiveEdge a) (clockToActiveEdge b) <$> edges + +-- | Check that `clockTicksRelative` has a sane time in between events when it +-- gets passed two of the same clocks. +case_sanityClockTicksRelativeSame :: Assertion +case_sanityClockTicksRelativeSame = do + test r61 + test r107 + test r122 + test f122 + test f107 + test f122 + where + test c = clockTicksRelative c c `infEq` expected c + expected c = P.zip (0 : P.repeat (1000 * clockToPeriod c)) (P.repeat ClockAB) + +-- | Check that `clockTicksRelative` has a sane time in between events when it +-- gets passed one fast clock and one slow clock, where the fast clock is exactly +-- twice as fast as the slow clock. +case_sanityClockTicksRelativeDouble :: Assertion +case_sanityClockTicksRelativeDouble = do + test r61 r122 + where + test c0 c1 = clockTicksRelative c0 c1 `infEq` expected c0 c1 + expected c0 _c1 = P.zip (0 : P.repeat (1000 * clockToPeriod c0)) (P.cycle [ClockAB, ClockA]) + +-- | Check that `clockTicksRelative` has a sane time in between events when it +-- gets passed two of the same clocks. +case_sanityClockEdgesRelativeSame :: Assertion +case_sanityClockEdgesRelativeSame = do + test r61 + test r107 + test r122 + test f122 + test f107 + test f122 + where + test c = clockTicksRelative c c `infEq` expected c + expected c = P.zip (0 : P.repeat (1000 * clockToPeriod c)) (P.repeat ClockAB) + +-- | Check that `clockTicksRelative` has a sane time in between events when it +-- gets passed one fast clock and one slow clock, where the fast clock is exactly +-- twice as fast as the slow clock. +case_sanityClockEdgesRelativeDouble :: Assertion +case_sanityClockEdgesRelativeDouble = do + test r61 r122 + where + test c0 c1 = clockEdgesRelative c0 c1 `infEq` expected c0 c1 + expected c0 _c1 = + P.zip + (0 : P.repeat ((1000 * clockToPeriod c0) `div` 2)) + (P.cycle [ ClockEdgeAB Rising Rising + , ClockEdgeA Falling + , ClockEdgeAB Rising Falling + , ClockEdgeA Falling + ]) + +tests :: TestTree +tests = $(testGroupGenerator) diff --git a/clash-vexriscv/tests/unittests/main.hs b/clash-vexriscv/tests/unittests/main.hs new file mode 100644 index 0000000..4793f75 --- /dev/null +++ b/clash-vexriscv/tests/unittests/main.hs @@ -0,0 +1,26 @@ +-- SPDX-FileCopyrightText: 2024 Google LLC +-- +-- SPDX-License-Identifier: Apache-2.0 + +module Main where + +import Prelude + +import Test.Tasty +import Test.Tasty.Hedgehog + +import qualified Tests.VexRiscv.ClockTicks + +tests :: TestTree +tests = testGroup "Tests" + [ Tests.VexRiscv.ClockTicks.tests + ] + +setDefaultHedgehogTestLimit :: HedgehogTestLimit -> HedgehogTestLimit +setDefaultHedgehogTestLimit (HedgehogTestLimit Nothing) = HedgehogTestLimit (Just 1000) +setDefaultHedgehogTestLimit opt = opt + +main :: IO () +main = defaultMain $ + adjustOption setDefaultHedgehogTestLimit + tests