12. Clash, последовательные схемы

В предыдущий раз мы познакомились с тем, как писать комбинационные схемы. Этого, конечно, недостаточно, по тем же самым причинам, которые мы озвучивали ранее. И в Clash есть механизмы, упрощающие нам составление подобных схем — высокоуровневые обёртки типа автоматов Мили и Мура и сигналов. Всё вышеперечисленное (и ещё много-много всего) позволяет воспринимать последовательную логику как необходимую обвязку над более простыми функциями и типами, что очень естественно для парадигмы функционального программирования.

Свёртка с суммой

Давайте рассмотрим на примере свёртки массива чисел через сумму.

src/11-clash/clash-examples/src/SumReduce.hs
module SumReduce where

import Clash.Prelude

sumReduceAcc (1)
  :: Int
  -> Int
  -> (Int, Int)
sumReduceAcc acc num = (mySum, mySum)
  where
    mySum = acc + num

mealySumReduce
  :: (KnownDomain dom, HiddenClockResetEnable dom) (2)
  => Signal dom Int (2)
  -> Signal dom Int
mealySumReduce = mealy sumReduceAcc 0

{-# ANN
  topEntity (3)
  ( Synthesize
      { t_name = "sumReduce"
      , t_inputs = [PortName "CLK", PortName "RST", PortName "ENBL", PortName "NUM"]
      , t_output = PortName "SUM"
      }
  )
  #-}
topEntity
  :: Clock System (4)
  -> Reset System
  -> Enable System
  -> Signal System Int (5)
  -> Signal System Int
topEntity = exposeClockResetEnable mealySumReduce (6)
1 У нас всё последовательное, то есть мы где-то хотим аккумулировать сумму и принимать следующее число. Довольно естественно в таком случае воспользоваться автоматом Мили[1]. Это как раз функция переходов .
2 Самое интересное. Signal Domain[2] — это обёртка, которая берёт на себя все заботы об управляющих сигналах. Домены могут быть разными, можно даже делать свои[3]. На них можно накладывать ограничения, что мы и сделали[4].
3 Альтернативой аннотациями при помощи TemplateHaskell, которые мы делали в прошлый раз, является аннотации верхнего модуля[5].
4 В автомате у нас спрятаны управляющие сигналы, однако в синтезируемой функции они должны торчать явно.
5 System — стандартный домен[6]. Тут нужно заметить, что мы обязаны указать какой-то конкретный, это требование Clash-а на верхнеуровневую функцию.
6 Как и говорилось в 4 пункте, в автомате управляющие сигналы спрятаны в ограничение типа, потому что на том уровне абстракции про них не надо думать. Здесь же нам нужно их вытащить, это делается в том числе так[7].

Появилось много нового. Это очень понятно: много забот с нас снимают Clash-овские абстракции. Заметим, что довольно много содержательного написано как раз на уровне типов, это особенность Clash.

Давайте же рассмотрим, как тестировать такие функции.

src/11-clash/clash-examples/tests/Tests/SumReduce.hs
{-# OPTIONS_GHC -Wno-type-defaults #-}

module Tests.SumReduce where

import qualified Clash.Prelude as C
import qualified Hedgehog as H
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import SumReduce (topEntity)
import Test.Tasty
import Test.Tasty.Hedgehog
import Test.Tasty.TH
import Prelude

prop_sumReduceDefault :: H.Property
prop_sumReduceDefault = H.property $ do
  ls <- H.forAll $ Gen.list (Range.constant 0 10) $ Gen.int $ Range.constant 0 255 (1)
  let expected = scanl1 (+) ls (2)
      actual = C.simulateN (length ls) (topEntity C.clockGen C.resetGen C.enableGen) ls (3)
  expected H.=== actual

accumTests :: TestTree
accumTests = $(testGroupGenerator)

main :: IO ()
main = defaultMain accumTests
1 Длинная строчка, которая на самом деле очень простая. Просто генерируем списки ограниченной длинны из случайных int-ов ограниченного размера[8].
2 В отличии от обычной свёртки, у нас тут ещё есть все промежуточные значения. Для такого есть стандартная функция[9] (напомню, что здесь есть и стандартная Prelude, и Clash-овая, когда используем последнюю, мы это явно указываем через префикс C.).
3 Симулируем работу нашего модуля[10]. Генерируем определённое количество выходов при фиксированных входах, все их записываем в список.

Видно, что тестировать очень приятно, за это благодарим Haskell с его развитыми для этого инструментами.

Разбиение на пары

Опять-таки пример, который нам знаком.

src/11-clash/clash-examples/src/Pairwise.hs
module Pairwise where

import Clash.Prelude

pairwiseAcc
  :: Maybe Int
  -> Int
  -> (Maybe Int, Maybe (Int, Int))
pairwiseAcc state inputInt = case state of
  Just s -> (Just inputInt, Just (s, inputInt))
  Nothing -> (Just inputInt, Nothing)

mealyPairwise
  :: (KnownDomain dom, HiddenClockResetEnable dom)
  => Signal dom Int
  -> Signal dom (Maybe (Int, Int))
mealyPairwise = mealy pairwiseAcc Nothing

topEntity (1)
  :: Clock System
  -> Reset System
  -> Enable System
  -> Signal System Int
  -> Signal System (Maybe (Int, Int))
topEntity = exposeClockResetEnable mealyPairwise
1 До этого мы каким-либо образом делали аннотации, можно совсем без этого.
Попробуйте синтезировать этот модуль в SystemVerilog и понять, почему лучше всё-таки писать аннотации.

Тут ничего принципиально нового нет, опять автомат Мили. Однако тут мы используем совсем-совсем функциональную вещь — монаду Maybe. И оказывается, что оно синтезируется без лишних танцев с бубнами.

Обязательно порешайте упражнения.