ハードウェア開発における関数型言語

関数型言語は、コード生成に使用されますが、原則として、低レベルのプログラミングにはあまり適していません。



プロジェクト例
安全なCコードの生成(Kaspersky Labで使用) アイボリーArduinoでのリアクティブプログラミングのサポートなど、 AtomIon


しかし、ハードウェアレベルにさらに下がった場合、突然FPは非常に便利です。 結局、組み合わせロジックのブロックは、着信信号の値と発信信号の値の関数に過ぎず、シーケンシャルロジックの場合は、パラメータと結果に古い状態と新しい状態を追加するだけで十分です。



私が最初にHaskellを研究したとき、私は「RSトリガーをシミュレートするのが何が良いのか」という熱い議論に加わりました。 勉強したばかりの言語が、この議論で浮かんだ問題をすべて解決していることにすぐに気付きました。



モデリングでは、モデルの状態の経時的な変化を観察する必要がありますが、Haskellにはそのような可変状態はありません。 そのために、「水平時間」に変わる遅延リストがあります。



今何時?
このばかげた言葉は参加者の1人によって作られました。1つの信号の進化全体を行に入れ、次の進化が他の信号の進化全体を行う方が便利であることに非常に驚いていました通常の言語と同様に、Haskellではデータ出力が最も困難です)。 また、クロックサイクルごとに1行ですべての信号の値を出力するのに比べて、オシロスコープの信号に似ているため、この形式が気に入っています。


信号をモデル化する簡単な方法は、いつでも値のリストを表示することです。 1つの信号が時間的に1つのクォンタムのオフセットを持つ別の信号と等しい場合、リストの先頭に0を追加します。



delay s = 0:s
      
      





またはそう
 delay = 0:
      
      







信号用に独自のタイプを作成できます-これはより効率的で、より安全で、より正確ですが、簡単にするために、ここでは単純なリストの使用に制限します。



 data Signal v = S v (Signal v) delay vs = S vs
      
      





動作時間の正確なモデリングが必要な場合、信号はペアのリスト(時間間隔、信号値)で表すことができ、過渡値の表現を提供できます。









RSトリガーは、相互に再帰的に接続された2つのNORノードです。 このシステムには、1つのNORの出力が1つで、もう1つのNORがゼロである2つの安定した状態があります。 NORノードの1つの第2入力にユニットを供給することにより、状態を切り替えることができます。



一般的に、RSトリガーは非同期回路です。 ただし、例を簡単にするために、同期としてシミュレートしますが、これは完全に正しいわけではありません(短い「メジャー」サイズを選択してもトランジェントをシミュレートするのは困難です。信号の異なる表現を使用することをお勧めします)。



 nor '_' '_' = '~' nor _ _ = '_' rs rs = (q, nq) where q = '_' : zipWith nor r nq nq = '_' : zipWith nor qs main = let r = "~_______" s = "___~~___" (q,nq) = rs rs in do print r print s print q print nq
      
      





Haskellクイックリファレンス
変数の名前(より正確には定数、スコープの制限内で変更できないため)および関数は、小文字で始まり、英数字または特殊文字で構成され、「:」で始まらない。



大文字(または「:」で始まる)は、型の名前、コンストラクター(列挙内の定数の名前であると想定できます)、およびモジュールの名前で始まります。



(:)-リストコンストラクター。 新しいリストを作成し、最初に古い要素に1つの要素を追加します。

 0 : [1,2,3,4,5]
      
      



[0,1,2,3,4,5]と同等

Haskellの文字列は、文字のリストとして表されます。 「1234」は['1'、 '2'、 '3'、 '4']と同じ意味です



zip-2つのリストをペアのリストに変換します。



 zip [1,2,3,4] "1234"
      
      



ブデンイコール
 [(1,'1'),(2,'2'),(3,'3'),(4,'4')]
      
      







zipWithは、2つのリストのアイテムに関数を適用します



 zipWith (+) [1,2,3,4] [1,3,5,7]
      
      



リストの要素ごとの合計を計算します[2,5,8,11]

zipはzipWithで表されます

 zip = zipWith (,)
      
      







zip3とzipWith3は同様に機能しますが、3つのリストが対象です。



scanlは、累積リストの各要素に関数を適用します。 そのタイプ(署名)は次のように説明されます。
 scanl :: (b -> a -> b) -> b -> [a] -> [b]
      
      





scanlの最初の引数は2つの引数の関数、2番目はアキュムレータの初期値、3番目は入力リストです。

 scanl (+) 0 [1,2,3,4]
      
      



部分量のリストを計算します:[0,1,3,6,10]



($)関数を引数に適用するための接尾辞エントリです。

 f $ x = fx
      
      





多くの場合、より少ない括弧を書くために使用されます。

fx $ gyはfx(gy)と同等です



\ xy-> fyxという表記は、パラメーターxおよびyを持つ匿名関数(クロージャーとも呼ばれます)を意味します。



次に、いくつかのあいまいな用語があります。 彼らが読者を怖がらないように願っています。 この説明が複雑すぎる場合でも、例を使用してこれらの関数を使用する方法を理解するのは簡単です。



fmap-関数を単一の値からコンテナ全体の上の関数に「レイズ」します。 コンテナはファンクタでなければなりませんが、ほとんどすべてがファンクタです。 特に、このようなコンテナは、各瞬間の値を保存する信号です。 リストもそのようなコンテナですが、歴史的な理由から、同じ機能を持つ特別なマップ機能があります。

liftAはfmapと同じですが、適用可能なファンクター用です(名前の文字「A」で証明されています)。 シグナルも適用可能なファンクターであり、リストはより複雑です。 正式には、リストも適用可能なファンクターであり、liftAはそれらと期待どおりに機能します。 ただし、liftA2とliftA3は予期しない動作をしますが、これは別の記事のトピックです。



liftA2およびliftA3は、コンテナからの関数への2つおよび3つの引数から関数を「レイズ」します。 それらはシグナルで動作し、リストの場合はzipWithおよびzipWith3を使用する方が適切です。



このアプローチにより、 RTLレベルで非常に複雑なスキームを比較的簡単にシミュレートできます。 クロック信号は明らかに存在しませんが、必要な場所はどこでも暗示されています。 レジスターは、遅延を使用して、またはパラメーターの状態とノードコードの戻り値を明示的に指定することでモデル化できます。



 macD rxy = acc where prods = zipWith (*) xy sums = zipWith (+) acc prods acc = 0 : zipWith (\rv -> if r == 1 then 0 else v) r sums macS rxy = scanl macA 0 $ zip3 rxy where macA acc (r,x,y) = if r == 1 then 0 else acc+x*y
      
      





ここでは、バッテリーを使用した2つの同等のMAC(乗算と加算)操作について説明します。 macD-再帰的な遅延信号を使用、macS-明示的に記述された状態を使用。



Haskellサブセットが同期ハードウェアを非常によくシミュレートする場合、それからHDLを合成してみませんか? これを可能にするいくつかのコンパイラ拡張プロジェクトがあります:商用のBluespec 、無料のLava、およびCλaSHです。



衝突



例として、VHDL、SystemVerilog、および古き良きVerilogの両方でコンパイルできるため、Clashを検討します( マイクロエレクトロニクスだけでなく使用されるため、私は魅力的です:)



インストールプロセスの詳細は、サイトで説明されています。 慎重に検討する必要があります-まず、ghc-7.xとの互換性が宣言されています(つまり、8.xでは動作しない可能性があります)。次に、「cabal install clash」を実行しないでください。これは古いパッケージです。clash-ghcをインストールする必要があります( 「Cabal install clash-ghc --enable-documentation」)。



クラッシュ実行可能ファイル(またはOSによってはclash.exe)は「〜/ .cabal / bin」ディレクトリにインストールされます。$ PATHに追加することをお勧めします。



クラッシュがコンパイルを開始するメインノードはtopEntityと呼ばれます。これは、着信信号から発信信号への関数です(当然、信号は合成できます)。



たとえば、シングルビット加算器を考えてみましょう。



 topEntity :: Signal (Bool, Bool) -> Signal (Bool, Bool) topEntity s = fmap (\(s1,s2) -> (s1 .&. s2, s1 `xor` s2)) s
      
      





ファイル全体
 module ADD1 where import CLaSH.Prelude topEntity :: Signal (Bool, Bool) -> Signal (Bool, Bool) topEntity = fmap (\(s1,s2) -> (s1 .&. s2, s1 `xor` s2))
      
      





fmapは、関数を論理量のペアから信号の関数に変換します。 コマンド「clash --verilog ADD1.hs」を使用して、Verilogでファイルをコンパイルできます。



結果
 // Automatically generated Verilog-2001 module ADD1_topEntity_0(a1 ,result); input [1:0] a1; output [1:0] result; wire [0:0] app_arg; wire [0:0] case_alt; wire [0:0] app_arg_0; wire [1:0] case_alt_0; wire [0:0] s1; wire [0:0] s2; assign app_arg = s1 & s2; reg [0:0] case_alt_reg; always @(*) begin if(s2) case_alt_reg = 1'b0; else case_alt_reg = 1'b1; end assign case_alt = case_alt_reg; reg [0:0] app_arg_0_reg; always @(*) begin if(s1) app_arg_0_reg = case_alt; else app_arg_0_reg = s2; end assign app_arg_0 = app_arg_0_reg; assign case_alt_0 = {app_arg ,app_arg_0}; assign s1 = a1[1:1]; assign s2 = a1[0:0]; assign result = case_alt_0; endmodule
      
      





状態を操作するには、ムーアとマイルのマシンを使用できます。 まず、ムーアオートマトンを使用した分周器について考えます。



 data DIV3S = S0 | S1 | S2 div3st S0 _ = S1 div3st S1 _ = S2 div3st S2 _ = S0 div3out S2 = True div3out _ = False topEntity :: Signal Bool -> Signal Bool topEntity = moore div3st div3out S0
      
      





dataは、データ型を記述するHaskellコンストラクトです。 このプログラムでは、マシンの状態を表すDIV3Sのタイプを説明します。 このタイプの可能な値は、「|」でリストされます -S0、S1、およびS3。

div3st-状態関数(記号「_」は通常、未使用パラメーター、この場合は入力信号の値と呼ばれます)。

div3out-状態から出力信号の値までの関数。



ムーアライブラリ関数は、これら2つの関数と初期状態に基づいてノードを作成します。



出力システム
 // Automatically generated SystemVerilog-2005 module DIV3Moore_moore(w3 ,// clock system1000 ,// asynchronous reset: active low system1000_rstn ,result); input logic [0:0] w3; input logic system1000; input logic system1000_rstn; output logic [0:0] result; logic [1:0] s1_app_arg; logic [1:0] s1; always_comb begin case(s1) 2'b00 : s1_app_arg = 2'd1; 2'b01 : s1_app_arg = 2'd2; default : s1_app_arg = 2'd0; endcase end // register begin logic [1:0] dout; always_ff @(posedge system1000 or negedge system1000_rstn) begin : DIV3Moore_moore_register if (~ system1000_rstn) begin dout <= 2'd0; end else begin dout <= s1_app_arg; end end assign s1 = dout; // register end always_comb begin case(s1) 2'b10 : result = 1'b1; default : result = 1'b0; endcase end endmodule
      
      





マイルアサルトライフルでも同じことが言えます。



 data DIV3S = S0 | S1 | S2 div3 S0 _ = (S1, False) div3 S1 _ = (S2, False) div3 S2 _ = (S0, True) topEntity :: Signal Bool -> Signal Bool topEntity = mealy div3 S0
      
      





VHDL出力
 -- Automatically generated VHDL-93 library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.NUMERIC_STD.ALL; use IEEE.MATH_REAL.ALL; use std.textio.all; use work.all; use work.div3mealy_types.all; entity div3mealy_mealy is port(w2 : in boolean; -- clock system1000 : in std_logic; -- asynchronous reset: active low system1000_rstn : in std_logic; result : out boolean); end; architecture structural of div3mealy_mealy is signal y : boolean; signal result_0 : div3mealy_types.tup2; signal x : unsigned(1 downto 0); signal x_app_arg : unsigned(1 downto 0); signal x_0 : unsigned(1 downto 0); begin result <= y; y <= result_0.tup2_sel1; with (x) select result_0 <= (tup2_sel0 => to_unsigned(1 ,2) ,tup2_sel1 => false) when "00", (tup2_sel0 => to_unsigned(2,2) ,tup2_sel1 => false) when "01", (tup2_sel0 => to_unsigned(0,2) ,tup2_sel1 => true) when others; -- register begin div3mealy_mealy_register : process(system1000,system1000_rstn) begin if system1000_rstn = '0' then x <= to_unsigned(0,2); elsif rising_edge(system1000) then x <= x_app_arg; end if; end process; -- register end x_app_arg <= x_0; x_0 <= result_0.tup2_sel0; end;
      
      





Clashはリストの代わりに固定サイズのベクトルを使用し、ほとんどのライブラリ関数はそれらと連携するように再定義されています。 標準のリスト関数にimport qualified Data.List as L



するには、ラインにimport qualified Data.List as L



ファイルに追加します(またはREPLで実行します)。 その後、接頭辞「L」を明示的に指定することにより、関数を使用できます。 例えば



 *DIV3Mealy L> L.scanl (+) 0 [1,2,3,4] [0,1,3,6,10]
      
      





最もよく知られているリスト関数はベクターで機能します。



 *DIV3Mealy L> scanl (+) 0 (1 :> 2 :> 3 :> 4 :> Nil) <0,1,3,6,10> *DIV3Mealy L> scanl (+) 0 $(v [1,2,3,4]) <0,1,3,6,10>
      
      





しかし、多くの微妙な点があります 。詳細については、 ドキュメントを参照する価値があります



例付きのガイドはこちらにあります



このサイトには、クラッシュに関するプロジェクトの例、特に6502プロセッサの実装があります



見込み



Haskellは非常に強力な言語であり、DSLの開発、たとえばデバイスソフトウェアインターフェイスの開発(HDLに加えて、仮想化システムのアイボリードライバーとエミュレーターを使用した生成)、またはアーキテクチャとマイクロアーキテクチャの説明(最適化するLLVMバックエンドの生成)に使用できますこのマイクロアーキテクチャ用)。



私はこの機会に、現在読んでいるこの本を書くことを奨励した教科書「デジタル回路とコンピュータアーキテクチャ」の出版を組織してくれたユリパンチョルに感謝の意を表します。



All Articles