11. Введение в Clash

Мотивация

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

Начало работы

В прошлый раз мы уже установили Haskell. Так что поставить Clash не составит труда (выбираем опцию B). Там будет базовый пример, который мы не будем удалять. Проверить, что всё работает можно, например, запустив тесты.

$ stack test
Хоть Clash и является просто ещё одним пакетом, он перекрывает стандартную Prelude своей, надо иметь это в виду.

Опять сумматор

Знакомство с SystemVerilog мы начинали с реализации полного сумматора, здесь поступим так же. С точки зрения логики тут нет ничего незнакомого, но дьявол, как водится, кроется в деталях.

src/11-clash/clash-examples/src/FullAdder.hs
module FullAdder (fullAdder) where

import Clash.Annotations.TH (1)
import Clash.Prelude (2)

fullAdder :: Bit -> Bit -> Bit -> (Bit, Bit) (3)
fullAdder a b c_in = (res_sum, c_out)
  where
    res_sum = a `xor` b `xor` c_in
    c_out = (a .&. b) .|. (c_in .&. (a `xor` b))

topEntity (4)
  :: "a" ::: Bit (5)
  -> "b" ::: Bit
  -> "c_in" ::: Bit
  -> ("sum" ::: Bit, "c_out" ::: Bit)
topEntity = fullAdder

makeTopEntity 'topEntity (6)
1 Модуль для создания аннотаций верхнеуровевой функции.
2 Как и было сказано, Prelude переписывается самим Clash[1], а стандартная по умолчанию не импортирована.
3 Сигнатура нашего сумматора. Bit именно Clash-вский тип, можно было бы сделать и на Bool-ах.
4 Верхнеуровневая функция, замена main. Именно то, что написано здесь, будет в итоге синтезироваться в SystemVerilog.
5 Можно задать имена аргументам в сигнатуре функции, это сделано, чтобы сгенерированный .sv файл было легче читать[2].
6 Заклинание, чтобы проделанное в 5 работало[3].

Попробуйте сгенерировать .sv файлик. Это можно сделать так (из директории src/11-clash/clash-examples).

$ stack run clash -- FullAdder --systemverilog

Убедитесь, что он похож на то, что писали мы.

Самое приятное здесь то, что мы отделили функциональную часть от той, которая по итогу пойдёт на синтез. Так что можно написать тесты к функции fullAdder, и если они проходятся, то с большой вероятностью сгенерированный модуль будет работать также исправно.

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

module Tests.FullAdder where

import qualified Clash.Prelude as C
import FullAdder (fullAdder)
import qualified Hedgehog as H
import qualified Hedgehog.Gen as Gen
import Test.Tasty
import Test.Tasty.Hedgehog
import Test.Tasty.TH
import Prelude

prop_sumBit :: H.Property
prop_sumBit = H.property $ do
  a <- H.forAll Gen.bool (1)
  b <- H.forAll Gen.bool
  cIn <- H.forAll Gen.bool
  let aBit = C.boolToBit a (2)
      bBit = C.boolToBit b
      cInBit = C.boolToBit cIn
      actual = fullAdder aBit bBit cInBit
      sumBV = (C.boolToBV a + C.boolToBV b + C.boolToBV cIn) :: C.BitVector 2 (3)
      expected = (sumBV C.! 0, sumBV C.! 1)
  actual H.=== expected

accumTests :: TestTree
accumTests = $(testGroupGenerator) (4)

main :: IO ()
main = defaultMain accumTests
1 К сожалению, Clash-овые типы не могут генерироваться Hedgehog.Gen, поэтому приходится брать Bool.
2 На наше счастье, Bool легко переделать в Bit[4].
3 Мы можем привычно складывать битвектора, но функция boolToBV требует, чтобы мы уточнили итоговый тип (напомню, что в Clash в типе BitVector зашита его длинна). Здесь можно было бы уточнить тип сразу после применения функции boolToBV (это выглядело бы так C.boolToBV a :: BitVector 2), дальше haskell сам бы догадался, какой тип у остальных применений, потому что сложение не меняет его. Это распространённая ситуация: иногда, если у вас что-то не работает, хотя по логике должно, то стоит подумать о том, чтобы явно указать тип (это можно, как видно, делать в любом месте, хоть после каждого применения функции).
4 Тут и ниже — стандартная магия, чтобы всё заработало.

Тут несколько иначе настроена инфраструктура для тестов, так что важно.

  1. Добавить модуль в src/11-clash/clash-examples/clash-examples.cabal в library и test-suite.

  2. Добавить нужную функцию в src/11-clash/clash-examples/tests/unittests.hs.

Многобитный сумматор

Не отходя далеко, давайте напишем многобитный сумматор через реализованную функцию.

{-# LANGUAGE PartialTypeSignatures #-}
{-# OPTIONS_GHC -Wno-partial-type-signatures #-}

module FullAdderMultibits where

import Clash.Annotations.TH
import Clash.Prelude
import FullAdder (fullAdder)

fullAdderMultiBits
  :: (KnownNat n) (1)
  => Vec n Bit (2)
  -> Vec n Bit
  -> Bit
  -> (Vec n Bit, Bit)
fullAdderMultiBits a b c_in = res
  where
    zero = repeat 0 :: Vec _ Bit (3)
    res = foldr func (zero, c_in) (zip a b) (4)
    func (fstBit, sndBit) (ansVec, prevCarry) = (resBit +>> ansVec, nextCarry) (5)
      where
        (resBit, nextCarry) = fullAdder fstBit sndBit prevCarry

topEntity
  :: "a" ::: Vec 8 Bit
  -> "b" ::: Vec 8 Bit
  -> "c_in" ::: Bit
  -> ( "sum" ::: Vec 8 Bit
     , "c_out" ::: Bit
     )
topEntity = fullAdderMultiBits

makeTopEntity 'topEntity
1 Задаём ограничение типа через класс[5]. Очень-очень часто встречается, оно позволяет нам в некотором смысле параметризовать функции, как мы делали это в SystemVerilog. Однако этот механизм несколько более хитрый и мощный.
2 Берём не BitVector, а просто Vec потому что с ним можно сделать сильно больше вещей, из которых нам нужна будет свёртка (foldr).
3 Делаем вектор из одних нулей. Здесь Haskell сам догадается какой длинны он должен быть, именно для этого написаны 2 первые строчки подключения расширений[6]. Здесь пример вырожденный, но такие вещи достаточно распространены и в более сложных случаях.
4 Свёртка, но не простая, а как раз на векторах[7], Ещё одно проявление спрятанной стандартной Prelude. Работает аналогично.
5 Вектор фиксированной длинны, так что оператор вставки в начало "вытесняет" последний бит[8].
Обязательно порешайте упражнения.