ChiselでBF処理系を作る(5)
プロセッサの実装と全体結線
前回はUARTの実装をした。本命のBF Processorを実装する。
graph LR
PC -- USB --- USB_Uart
USB_Uart --Tx--> UartTxRx
UartTxRx--Rx--> USB_Uart
UartTxRx -- program/stdin -->FIFO_din
FIFO_din -- program --> BF_Processor
FIFO_din -- stdin--> BF_Processor
BF_Processor -- stdout --> FIFO_dout
FIFO_dout -- stdout --> UartTxRx
External_SW --program/run -->UntiChatter
UntiChatter -->BF_Processor
style BF_Processor fill:#f66,stroke:#f33
BFの実装
ChiselでBF処理系を作る(1)で解説したが、しばらく前なので再掲する。
brainfxxk.cpp - Gist
今回は高速化等については触れず、C++で記述したこの処理をそのままハードウェアに落とし込む形で実装する。
scalaやchiselに関して目新しい要素はないので、それぞれのコードブロックごとに簡単に解説する。
高速化するのであれば、例えばbranch([
, ]
)のジャンプテーブルの事前作成や同一命令の一括実行、パイプライン化など様々な余地があると思う。プロセッサ設計にはあまり詳しくないのでなんとも言えないところではある。
ともあれ、今回完成したコードは以下の通り。
BrainfuckProcessor.scala - Github
制御フロー
個々の要素については順番に解説するが、全体として以下の状態遷移を行うように設計している。
graph TD
halted--"io.program == 1"-->program
program--"io.program==0"-->halted
halted--"posedge io.run"-->run
run--"inst == `\s`, `#`"-->halted
run--"inst == `>`, `<`, `+`, `-`, `,`, `.`]"-->run
run--"inst==`[` and !stackData"-->nest=1
nest=1-->branchJump
branchJump--"inst == `[`"-->nest+=1
nest+=1-->branchJump
branchJump--"inst == `]`"-->nest-=1
nest-=1--"nest > 0"-->branchJump
nest-=1--"nest == 0"-->run
branchJump--"other"-->branchJump
runモードに移行するためには、外部ポートのrunに立ち上がりエッジを入力する。><+-,.
の命令群に関しては1cycで実行できるようになっている。
少し特殊なのが[]
の命令でループを抜ける場合である。inst==[ and !stackData
と記載したが、ループを抜ける際は対応する]
にプログラムカウンタを進める必要があるため、branchJump
モードに移行する。
このモードでは順番に命令を舐めていき、対応するカッコが見つかった場合にrun
モードに制御を戻す設計とした。
ただし、カッコについてはネストする可能性があるのでnest
の変数を持って対応するカッコの判断を行う。
今回作成したBF Processorをまっさきに高速化するのであればここのジャンプテーブルを事前に作成しておくことが必要だと考えている。
各要素について以下に解説する。
プログラム書き込み
halted
の状態でio.program
がアサートされた場合にプログラム書き込みモードに移行する。
FIFOから受信したデータを順番にinstMem
に書き込んでいる。アドレスのオートインクリメントはprogramモードではない時点で初期化されるので、programスイッチを有効にした後にデータ転送を行えば必ず0番地から書かれるようになっている。
プログラム開始
run入力の立ち上がりエッジを検出している。検出した場合、halted
をfalseにセットして実行開始ステートに遷移している。
ポインタ操作
現在のアドレスを一つ進める(もしくは一つ戻す。その際に次のアドレスにあるデータをstackData
に予め読み込んでおく。
また、アドレスのオーバーフローとアンダーフロー時には動作を停止させるようなコードも追加。1cycで動作可能。
データ操作
現在のアドレスにあるデータに1を足した(もしくは1を引いた)値を現在のstackMem[stackPtr]
に書き込む。特に特別な操作はなく、1cycで動作可能。
データ出力
現在のstackData
の値を、出力FIFOに流しているだけである。命令実行の処理を止めないために、Ackの検知とValidの取り下げはプログラムをデコードしている回路ブロックとは別の場所に記述してある。
これも出力先のFIFOがReadyでありつづけるならば、1cycで処理できる。そうでない場合はReadyがアサートされるまで待機。
データ入力
FIFOから受信したデータを現在のstackMem[stackPtr]
に書き込む。すでにデータが来ているのであれば1cycで動作する。
データを受け取った場合、今回のオレオレFIFOインターフェースではAckを返すことになっていたのでstdinAck
も立てている。上げ続けているとFIFOのデータがどんどん流れていしまうので、次のサイクル時にAckを取り下げている。
ループ
まず、[
については、現在のstackData
が0かどうかで分岐する。
- 0だった場合→branchJumpモードに以降
- それ以外→現在のアドレスをメモして、プログラムカウンタを一つすすめる
]
は、先程メモしたアドレスにプログラムカウンタを戻すだけである。
注意すべき点としてループがネストする場合にアドレスのメモが上書きされてしまうため、アドレスのメモにはStack構造を用いる。
branchJumpモードの動作は以下の通りである。最初にお見せしたフローの通り、多重ループの検出のために[
, ]
の有無でbranchJumpNest
の値を増減させている。もし0になったらbranchJumpモードを抜け、]
のあとにある命令を実行する。
全体結線
ともあれこれで一通りのモジュールが完成したので、頭の図にあるようにすべてのモジュールを接続する。
DebugAccessPort.scala - Github
<>
演算子は双方の信号のバルク接続を示している。他のコードはそのまま見てもらえればわかるがModule(new MyModule())
でインスタンシェートしてそれぞれ結線している。
またstdinとprogramDataのFIFOはio.program
によってどちらの信号を使うか選択式にしている。
全体テスト
実は最初は全体を結線したテストを行っていなかった。ところが以下ツイートにあるようにprogramDataの転送が3重に発生するバグが有ったため、シミュレーションによるものなのか論理合成やその後の処理が原因なのか切り分けるために簡単な全体テストを記述し実行した。Hello world!を出すのにMacBook Airで15分ほど要した。
DebugAccessPortSpec.scala - Github
結果はバグが見つかり、修正後ちゃんと出力された。やはりテストは端折ってはいけない、だいたい端折ったところでバグが出る。
Verilogコード生成
最後に以下のコードでVerilog HDLファイルを生成した。
結果はDebugAccessPortに関係したModuleすべてのVerilogが連結された1ファイルとして出力されている。
DebugAccessPort.v - Github
Vivadoでの作業
Topモジュール
最後にArty A7ボードにインプリメントするためにVivado 2018.3を使って作業を行った。
Artyのボード上には100MHzのTCXOが実装されていたが、(周波数を変更する兼ね合いもあるので)一旦MMCMに入力して合成した周波数をシステムに供給することとした。
これは明らかにChiselでやるのは冗長(BlackBox機能を使えばできるが)なので、直接Verilogで以下のように記述した。
top.v - Github
制約
ピンアサインとクロック指定などはDigilent社が配布しているxdcファイルを、top.vに適用できる形で編集した。
top.xdc - Github
ILA
万が一動かなかったときのデバッグなどに、ロジアナが使えると便利である。
VivadoにはWebPackライセンスであってもILAという、FPGA内部にロジアナを合成して波形観測する機能が利用できる。
これを使ってデバッグする環境を整えてた(そしてバグの特定に大いに貢献した)。
Vivadoの使い方自体は詳解しないが、簡単に説明するとSynthesized Designを開いた状態でSetup Debugを押す→観測したい波形とキャプチャ長さを選択→xdcファイルを出力。といった手順だ。
参考までに最後動作したときのDebug向けに吐き出したxdcファイルを置いておく。
generated.xdc - Github
ピンクのマーカーが観測するための配線の両端である。右上に見えるのがILAである。
完成
これで合成したビットストリームをFPGAに書き込んで動作することを確認できた。前述したFIFOのバグはあったが、UartTxRxやBF Processorがscalaシミュレーションどおりに一発で動作したことは本当にすごいという感想だった。
設計の反省点
今回完成したBF Processorを見てみると、まだVerilog HDLをScalaで書き下したに過ぎない程度の物となっている。
以下のページに有るような、Arbiter, Counter, Decoupled, BitPatなどを見るともっと手続きをTraitやModuleクラスの継承、Bundleを活用して抽象化している。
また、モジュールの設定もConfigをうまく活用して切り替えることも可能であるようだった。
最初のVerilog HDLからの移行お題としては適切であったが、Chiselならではの記述についても理解を深めていく必要がある。
freechipsproject/chisel3 chisel3.util - Github
まとめ
これでchiselでBF処理系を設計する内容は終了した。反省点にもある箇所はあるが、ともあれchiselが全くわからない状態から多少は記述できるようになっただけでも大きな進歩だと思う。
scalaとhdlどちらに対してもある程度の理解が必要なことなど学習コストの高さが目立つが、かなり実用性のあるaltHDLとして機能している、と私は率直に感じている。
今後のFPGA案件に使いたいかと言われると、間違いなく使いたいと言うと思う。