この記事は、インテル® FPGA Advent Calendar 2020 13日目の記事です。
TL;DR
FPGAでLチカするための専用言語と処理系を作ってみました。
前口上
ソフトウェアプログラミングって開発に使える言語とか便利なフレームワークとかがたくさんありますね。中にはジョークみたいな言語やツールもあったりして、なんだか楽しいです。というわけで、FPGA向けにも気軽にいろいろ処理系をつくって楽しんでみるというのはいかがでしょうか。
今回作る処理系は
冒頭で書いた通り、今回はFPGAでLチカするための専用言語L
とその処理系を作ってみます。おそらくLチカはFPGAにもっともよく実装されているアプリケーションではないでしょうか?簡単に実装できれば、FPGAにかかわる人の開発効率の大幅な向上に寄与できるハズです。
早速使ってみよう
L
のコードを書いてLチカするまでの一通りの手順をみてみましょう。
Lコードを書く
デフォルト環境でLチカしたい場合のL
のコードは、
L
です。hello.l
という名前でファイルに保存します。
コンパイル(?)
処理系は https://github.com/miyo/blink にあります。gitとpipenvが使える環境であれば
$ git clone https://github.com/miyo/blink.git
$ cd blink
$ pipenv install
$ pipenv shell
$ python -m blink hello.l
でL言語から、Lチカのための5つのファイルが生成されます。
- hello.v - LチカするVerilogモジュールの本体
- hello.qpf - Quartusのプロジェクトファイル
- hello.qsf - ピン配置など定義ファイル
- hello.sdc - タイミング制約を設定するファイル
- tb_hello.v - テストベンチ
実機での動作確認
生成されたhello.qpfをQuartusで開きましょう。
こんな感じで設定済のQuartusが起動します。デフォルトでは、Cyclone V (5CEBA4F23C7) を搭載したDE0-CVボード向けに関連ファイルが生成されています。
あとは、いつものようにツールバーの三角形のボタンStart Compilation
でsofファイル生成します。生成されたsofファイルをProgrammerで書き込むとDE0-CVの左端のLEDが点滅します。
シミュレーションで動作確認してみる
一緒に生成されたテストベンチを使ってシミュレータで動作を確認することもできます。iverilog
をインストールしていれば、
$ iverilog hello.v tb_hello.v
$ ./a.out
とするとシミュレーション用の実行バイナリを生成と実行ができます(結構な時間と500MB程度のダンプファイルが生成されます)。ダンプファイルをGtkWaveで開くと、次のように出力Q
が反転する様子を確認できます。
拡張しよう
Lチカするときは、与えられたボードの供給クロックに応じて点滅間隔を変えたいですよね。また、別のLEDやFPGA向けに利用したくなることもあるでしょう。というわけでL
の機能拡張をしていきましょう。
処理系の概要
L
と書いてあるテキストが与えられたら想定するLチカコードを生成するだけであれば、出力したい文字列をコード中に定義しておいて、それをprint文で出力するだけでおしまいです。また単にLチカの周期を指定した間隔にしたいという程度であれば、そこだけ置換するだけで十分です。
ちなみに、そのような実装が https://github.com/miyo/blink/tree/b11ced868e5191f0487d4672ea6cce005dfd243a です。
しかし、後述するように少しずつ複雑な構造を扱うことを考えると、コード生成処理の対象として内部データ構造を考え、データ構造のパラメタを設定するための文法とコード生成処理を用意するというように分離するほうが実装が楽になります。
内部データ構造
実際のFPGA上で動作させるLチカの表現として、生成されるVerilogモジュールが合成される際に必要な合成ツールの名前やピン配置を属性として持つModule
というクラスと、Lチカのために内部に作るカウンタに相当するCounter
を考えることにします。Counter
はModule
のサブクスラスとして定義することにします。次のような感じですね。
入力言語と出力スタイル
おしゃれな文法を考えることを放棄してデータ構造を素直に表現するようなS式を入力にとることにします。具体例はあとで紹介します。
出力はLチカを実現できるようなVerilogモジュールです。これは自分が手で書く時のものをベースにします。
処理系の構造
データ構造と入出力形式が決まれば、処理系はパーサとジェネレータを実装して作ることができます。もし将来的に最適化を考えることがあれば、データ構造を変更することで実現することもできますね。
新しい機能を追加したければ、(1)想定する構造を考えて、(2)対応するデータ構造を考え、(3)そのデータ構造にデータを設定できるような入力を考えることになります。また、もっと便利に記述したいというような場合には、希望する記述を内部データ構造にマッピングできるようなルールを考えればよいでしょう。
拡張したLを使ってみよう
点滅サイクルを指定する
L
の引数に点滅サイクル数を指定できるようにしてみました。次のように記述します。
(L 8)
これで8回に1回、出力が反転する回路が生成されます。シミュレータで動作を確認してみた様子が次の通りです。
ピン配置やデバイスを自分で指定したい
Module
に付与した変数のパラメタを設定します。次のように記述できるように考えてみました。
(L 25000000
:synth-tool "QUARTUS"
:device "5CEBA4F23C7"
:iomap '(CLOCK (M9 "3.3-V LVTTL") RESET (U13 "3.3-V LVTTL") Q (AA2 "3.3-V LVTTL"))
:period '(CLOCK 20.0)
)
これは引数で指定した25000000
(=25M)の周期、すなわち動作クロックが50MHzなら1秒で点滅を繰り返すLチカを生成し、クロック、リセット、出力信号をそれぞれ指定したピンにマッピングするように指示しています。
処理系でVerilogコードやQuartusプロジェクトファイルを生成して合成、実機で実行した様子が次の写真です。AA2に接続された並んでいるLEDの一番右側が点滅します。
複数ビット出力したい
Lチカには複数ビットにまとめて出力するという流派(?)もありますね。出力ポートに名前とビット幅を指定できるようにして次のように書けるようにしてみましょう。
(L 1024 :out '(Q 7 3))
シミュレータで動作を確認するとカウンタの下位3bitを捨ててその上の5bitがQに出力されていることがわかります。
ピン配置を指定して実機で動作確認できるようにしてみましょう。ピン配置にリストを指定できるようにして次のように記述できるように拡張してみました。
(L 4294967296 :out '(Q 29 20)
:synth-tool "QUARTUS"
:device "5CEBA4F23C7"
:iomap '(CLOCK (M9 "3.3-V LVTTL") RESET (U13 "3.3-V LVTTL")
Q ((AA2 "3.3-V LVTTL")
(AA1 "3.3-V LVTTL")
(W2 "3.3-V LVTTL")
(Y3 "3.3-V LVTTL")
(N2 "3.3-V LVTTL")
(N1 "3.3-V LVTTL")
(U2 "3.3-V LVTTL")
(U1 "3.3-V LVTTL")
(L2 "3.3-V LVTTL")
(L1 "3.3-V LVTTL")))
:period '(CLOCK 20.0)
)
これは、4294967296
つまり32bitのカウンタをつくっておいて、そのうちの29:20
を出力、それぞれを指定したピンに接続することを意図しています。
実機で動作させてみると、次のように10bitのカウンタを使ったLチカができました。
ネストしたい
時計風の出力がしたいような場合には指定したカウント数ごとの繰り上がりが必要になります。というわけで、:at
というキーワードを導入して、フラグが立った時だけカウントアップするように拡張します。
(L 1 :at (L 2 :at (L 4 :at (L 4))))
:at
を使ったこんなコードを書いて、つぎのように動作する入れ子のカウンタを作れるようになりました。
それぞれに名前がついていれば外側のモジュールまで引き出せるようにしましょう。ネストしたモジュールのクロックの立ち上がりエッジを揃えるには、モジュールをまたぐたびにフリップフロップに接続することにすればよいでしょう。
(L 1 :at (L 2 :at (L 4 :at (L 4))))
こんなコードが次のように動作するロジックに変換されます。
もし時計をつくりたければ、以下のようにクロック周波数を考慮して秒や分を作り、実際のLEDに出力できるようにピン配置を設定して実現できます。
(L 30 :at (L 5 :at (L 5 :at (L 250000 :out Q0) :out Q1) :out Q2) :out Q3
:synth-tool "QUARTUS"
:device "5CEBA4F23C7"
:iomap '(CLOCK M9 RESET U13 Q3 Y3 Q2 W2 Q1 AA1 Q0 AA2)
:period '(CLOCK 20.0)
)
もっと拡張する
とりあえずLチカができるようになったので、Lチカ言語としてはこのあたりでいいかもしれません。さらに拡張するとすると次のようなことが考えられます。
パターンも出力したい
複数のLEDにカウンタをそのまま出力するのではなくて、ナイトライダー風に複数のLEDの一つを順々に点滅させたいという流派(?)もあります。出力にパタンを設定できるとよさそうです。
たとえば、
(M (L 1024 :out '(Q 7 5)) :table '((0 1) (1 2) (2 4) (3 8) (4 8) (5 4) (6 2) (7 1)) )
などとして、デコーダが書けるようになると用が足りるかもしれません。M
をトップモジュールにして合成することも当然かんがえられますので、M
を表現するクラスもCounter
同様にModule
を継承してつくるとよさそうです。
出力だけじゃなくて入力もしたい
出力ができれば入力もしたくなりますね。出力(LED)との対で考えると入力はスイッチでしょうか。
(L 1 :at (S))
などとしてスイッチが入力を受け付けたタイミングでカウンタをインクリメントしてLチカするようにすると面白いかもしれません。
おわりに
FPGAに触れる人が少なくとも一度は実装するであろうLチカを効率よく実装するため、という言い訳のもとでLチカ専用言語L
とその処理系を考えてみました。この言語では
L
と記述したファイルを用意すればデフォルトのFPGAボード上でLチカできます。また、少し凝ったLチカの実装ができるような仕組みが用意されています。
さて、ここで考えてみたLチカ言語はジョーク言語にすぎませんが、パターン出力や複数カウンタのマージや分割ができれば実用的な設計言語にならないとも限りません。
というのは半分冗談ですが、実装したいターゲットを想定してモデル化、書きやすい入力形式を用意するというのはFPGA開発を手軽にするための第一歩と言えます。たとえば、ディープニューラルネットワーク的な何かをFPGAに実装するという場合に、べたにHDLで実装するのではなく、いったんジェネレータを挟むことで開発効率が向上するかもしれません。
また、入力言語や、あるいは最適化に凝ってみるというのも処理系を作って楽しむ醍醐味です。たとえば、好きなアプリケーションのDSLからFPGA上のロジックを実装してみるとか、高位合成処理系の中身に手を入れてみるといったことが考えられます。
というわけで、みなさんもFPGA開発をする上でも気軽に処理系を作って開発を楽しんでみるのはいかがでしょうか?(ちなみに、やりすぎると処理系と生成したものを両方メンテナンスし続ける必要がありますので粗造乱造は大変危険です。)