packages feed

clash-vexriscv-0.1: tests/unittests/Tests/VexRiscv/ClockTicks.hs

-- SPDX-FileCopyrightText: 2024 Google LLC
--
-- SPDX-License-Identifier: Apache-2.0
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE TemplateHaskell #-}
-- Clock definitions aren't much more readable with top level signatures..
{-# OPTIONS_GHC -Wno-missing-signatures #-}
-- Suppress Clash domain warnings
{-# OPTIONS_GHC -Wno-orphans #-}

module Tests.VexRiscv.ClockTicks where

import Clash.Explicit.Prelude hiding (d107, d122, d61)
import Clash.Signal.Internal (ClockAB (..), Femtoseconds (..), clockTicks, dynamicClockGen)
import Data.Bifunctor (second)
import Data.Int (Int64)
import Data.Maybe (catMaybes)
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.TH

import qualified Data.List as L
import qualified Prelude as P

import Tests.Extra (carthesianProductTests)
import VexRiscv.ClockTicks (
  ClockEdgeAB (..),
  clockEdgesAbsolute,
  clockEdgesRelative,
  clockTicksAbsolute,
  clockTicksRelative,
 )

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

{- | Used in production code. We're seeing strange things there, so we add some
tests here making sure that it's not this module messing up.
-}
createDomain vXilinxSystem{vName = "CPU"}
createDomain vXilinxSystem{vName = "JTAG", vPeriod = hzToPeriod 50_000}

-- | 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.drop 1 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
          ]
      )

-- | Make sure that swapping the arguments makes no difference for timing calculations
case_flipped :: Assertion
case_flipped =
  $(carthesianProductTests ["r61", "r107", "r122", "d61", "d107", "d122", "f61", "f107", "f122"])
 where
  test a b = do
    clockEdgesAbsolute a b `infEq` P.map (second flipClockEdge) (clockEdgesAbsolute b a)
    clockEdgesRelative a b `infEq` P.map (second flipClockEdge) (clockEdgesRelative b a)
    clockTicksAbsolute a b `infEq` P.map (second flipClock) (clockTicksAbsolute b a)
    clockTicksRelative a b `infEq` P.map (second flipClock) (clockTicksRelative b a)

  flipClockEdge :: ClockEdgeAB -> ClockEdgeAB
  flipClockEdge (ClockEdgeA edge) = ClockEdgeB edge
  flipClockEdge (ClockEdgeB edge) = ClockEdgeA edge
  flipClockEdge (ClockEdgeAB edgeA edgeB) = ClockEdgeAB edgeB edgeA

  flipClock :: ClockAB -> ClockAB
  flipClock ClockA = ClockB
  flipClock ClockB = ClockA
  flipClock ClockAB = ClockAB

{- | Check results produced by 'clockTicksAbsolute' and 'clockEdgesAbsolute' manually
to rule out functions in "ClockTicks" causing the strange behavior we're seeing
in production.
-}
case_sanityJtagCpu :: Assertion
case_sanityJtagCpu = do
  expectedAbsJtagEdges `infEq` P.map fst absJtagEdges
  expectedAbsJtagTicks `infEq` P.map fst absJtagTicks
  expectedAbsCpuEdges `infEq` P.map fst absCpuEdges
  expectedAbsCpuTicks `infEq` P.map fst absCpuTicks
 where
  -- JTAG
  expectedAbsJtagEdges = [0, halfJtagPeriodFs ..]
  expectedAbsJtagTicks = [0, jtagPeriodFs ..]

  halfJtagPeriodFs = jtagPeriodFs `div` 2
  jtagPeriodFs = 1000 * clockToPeriod jtagClk :: Int64

  isJtagEdge (ClockEdgeA _) = False
  isJtagEdge _ = True

  isJtagTick ClockA = False
  isJtagTick _ = True

  absJtagEdges = filter (isJtagEdge . snd) absEdges
  absJtagTicks = filter (isJtagTick . snd) absTicks

  -- CPU
  expectedAbsCpuEdges = [0, halfCpuPeriodFs ..]
  expectedAbsCpuTicks = [0, cpuPeriodFs ..]

  halfCpuPeriodFs = cpuPeriodFs `div` 2
  cpuPeriodFs = 1000 * clockToPeriod cpuClk :: Int64

  isCpuEdge (ClockEdgeB _) = False
  isCpuEdge _ = True

  isCpuTick ClockB = False
  isCpuTick _ = True

  absCpuEdges = filter (isCpuEdge . snd) absEdges
  absCpuTicks = filter (isCpuTick . snd) absTicks

  -- BOTH
  absTicks = clockTicksAbsolute cpuClk jtagClk
  absEdges = clockEdgesAbsolute cpuClk jtagClk

  cpuClk = clockGen @CPU
  jtagClk = clockGen @JTAG

tests :: TestTree
tests = $(testGroupGenerator)