12. Clash, последовательные схемы
В предыдущий раз мы познакомились с тем, как писать комбинационные схемы. Этого, конечно, недостаточно, по тем же самым причинам, которые мы озвучивали ранее. И в Clash есть механизмы, упрощающие нам составление подобных схем — высокоуровневые обёртки типа автоматов Мили и Мура и сигналов. Всё вышеперечисленное (и ещё много-много всего) позволяет воспринимать последовательную логику как необходимую обвязку над более простыми функциями и типами, что очень естественно для парадигмы функционального программирования.
Свёртка с суммой
Давайте рассмотрим на примере свёртки массива чисел через сумму.
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.
Давайте же рассмотрим, как тестировать такие функции.
{-# 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 с его развитыми для этого инструментами.
Разбиение на пары
Опять-таки пример, который нам знаком.
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
.
И оказывается, что оно синтезируется без лишних танцев с бубнами.
Обязательно порешайте упражнения. |