Chiselを使ったRISC-Vの勉強(14.Intel HEXの生成)
Intel Hexファイルの生成
Intel FPGAで初期値をRAMに与えたい場合、インテルHEXかMIFファイルにしておく必要があります。
https://www.intel.co.jp/content/dam/altera-www/global/ja_JP/pdfs/literature/ug/ug_ram_rom_j.pdf
今後、OpenOCDでデバッグできるIFを実装する予定もありMIFではなくインテルHEXを選択しました。
lynxeyed.hatenablog.com
今までChiselプロジェクトには独自形式のHEXファイルを使用してきました。チェックサムもアドレスもない、0x0000番地から始まることかつ32bit長が前提のものです。文字列処理が煩雑にならないので便利でしたが、ここでインテル形式も生成できるようにしました。
インテルHex形式の規格
Wikipediaにあります。これ以上分かりやすい解説がなかったので。
en.wikipedia.org
今まで生成していたHexファイルにスタートバイト、バイトカウント、アドレス、レコードタイプ、チェックサムを追加するのが楽そうです。
ここはshとかMakefileで書ければ良かったのですが、ちょっとめんどくさかったので、pythonで書きました。10~20分もあればできてしまうような内容です。
僕は半日かかりました。天才なので。
コードの主要な部分です。
アドレスをインクリメントして、16進数で表記、チェックサムを間違えなければ難しいところはありません。
ex_start_code = ':' hex_byte_count = '04' hex_start_addr = 0x0000 hex_record_type = '00' hex_end_of_file = ':00000001FF' line = fr.readline() while line: hexdata = line.strip() li_hexdata = conv_hexlist(split_n(hexdata,2)) # read hexdata -> list li_hexaddr = conv_hexlist(split_n(format(hex_start_addr,'04x'),2)) checksum = (0 - (int(hex_byte_count) + sum(li_hexaddr) + int(hex_record_type) + sum(li_hexdata))) & 0xff wr_hex = hex_start_code+hex_byte_count+format(hex_start_addr,'04x')+hex_record_type+hexdata+format(checksum,'02x')+'\n' fw.write(wr_hex) hex_start_addr = hex_start_addr + 1 #increment address line = fr.readline() fw.write(hex_end_of_file+'\n') fw.close fr.close
適当。
KyogenRVのプロジェクトトップで下記を実行します。
make test #ここで今まで通りのHEXファイル生成 ./mk_intel_hex.py #上記HEXファイルを読み込み、インテルHEXファイルを生成
test_intel.hex
というインテルHEX形式ファイルが、fpga/chisel_generated/
に生成されます。
コード全文
高度運転支援向け単眼カメラの実装(1.MATLABでパーティクルフィルタを実装し評価してみる)
パーティクルフィルタの詳しい内容は扱いません。OpenCVのチュートリアルなどに詳しく扱われています。
例題として、特定の色のカラーボールの追跡はよく扱われます。
以下の手順をとることが多いでしょう。
- 入力画像をHSV変換→ 一定の輝度を持っているピクセルから色相を取り出し → 特徴点
- パーティクルフィルタをばら撒く
- 各パーティクル上、または一定の距離以内存在する特徴点の数*1 → 尤度の決定
- 尤度の高い座標にカラーボールが存在すると判定
- 4.で得られた座標を考慮しつつ、カラーボールが次の状態でどこに向かうか推定する(リサンプリング)
- 3.へ
今回やってみた内容もそれほど変わりませんが、少し工夫をしています。
特徴点の定義
グリーン背景などで、カラーボールを動かす場合は一定の輝度のときの色相を取り出して判定し、特徴点とすることが多いようです。この場合はこの方法はとても有用です(室内の光が少々変わっても追いかけられる)
しかし、車の場合は様々な色をしており、とりまく環境光は絶えず変化しています。加えて昼夜の違いによる大きな輝度変化などがあります。
MATLABでさまざまな画像を入力して、何をパラメータとするとよいか調べたところ、
輝度勾配と色相勾配の内積
が今のところ有用ではないかとみています。
具体的には画像から5x5ピクセル画像を走査し
それぞれの内積を考慮すると道路と車の区別がつきやすそう、という判断をしました。
これについてはもう少し調査が必要です。現在試験運用中ですが、かなり課題が見えてきています。追加のパラメータが必要だと思います。
パーティクルフィルタの実装
演算量が少ないのは良い事です。
車のドラレコ映像を例にあげます。
衝突回避の目的であれば、この場合必要なデータは
このように考えれば、入力映像のほぼ上半分は演算から除外することができます。
入力される映像は1000x1000ピクセルを想定しています。
特徴点を5x5で演算するので、パーティクルフィルタの演算に必要な領域は1000/5 x 100/5 = 200 x 200ピクセルです。
パーティクルフィルタの行列を以下のように定めます。
PM(x座標, y座標, 重み)
これが、パーティクル数Pだけ増えます。今回のアプローチではPは200~500くらいが良いかと思われます。あまり多いと演算に時間がかかりすぎます。少なすぎると追跡ができません。
PM(x, y, w, P)
ここではいきなりMATLABコードを出します。
別言語で実装を考えている方にもある程度判断がつくようにコメントしてます。私も最終的にSystemVerilog + C/C++での実装が必要なので。
パーティクルの初期化
% パーテクルフィルタ初期化 % xは1~(200-20)までのレンジ、yは画像下半分(100 ~ 200-20)までのレンジ % 中央の周辺にまずパーティクルを集めてばら撒く function PM = pm_init(PM,P) for m=1:1:P PM(1,1,m) = 100 + randi([-70 70]); PM(1,2,m) = 100 + randi([0 80]); end PM(1,3,P) = 1; end
先ほど述べたように、
パーティクルフィルタの動き回れる領域は200x200です。y座標だけはパーティクル フィルタの動き回れる領域を下半分にしています。(なので実質200x100)
尤度の計算
ランダムにおかれたパーティクルを左上端として、20x20ピクセルを走査し、合計値を格納します。行列Xaは特徴点が格納された200x200の行列です。
% 尤度計算 function PM = likelihood(PM,P,Xa) for m = 1:1:P x = PM(1,1,m); y = PM(1,2,m); A = sum(Xa(y:y+19, x:x+19),'all'); PM(1,3,m) = A; end end
リサンプリング
今回は単純ランダムサンプリングという手法を用います。パーティクルフィルタの重みの累積和を求めて格納し、重みに応じて乱数を生成し、次のサンプリング座標を決定します。重みが小さい=成績の悪いフィルタは、良いものに置き換えられます。
% リサンプリング function [last_w, new_PM] = resample(PM,P) new_PM = zeros(1,4,P); %新しい配列の作成 weights = cumsum(PM(1,3,:)); %重みの累積和 last_w = weights(length(weights)); if last_w == 0 last_w = 1; end for n = 1:1:P w = randi(last_w); for lp = 1:1:length(weights) if w < weights(lp) break; end end %disp(lp); % lp => index new_PM(1,3,n) = PM(1,3,lp); % ウェイトをコピー new_PM(1,1,n) = PM(1,1,lp); % xをコピー new_PM(1,2,n) = PM(1,2,lp); % yをコピー end end
期待値の計算
期待値の座標から推定して黄色い四角を描きます。
元の入力画像に書き戻す関係で、x,y比率を5倍にしています。
function picRGB = pm_draw(PM, P, picRGB) weight = sum(PM(1,3,:)); x = 0; y = 0; for m=1:1:P x = x + (PM(1,1,m) * PM(1,3,m)); y = y + (PM(1,2,m) * PM(1,3,m)); end cX = (x * 5 / weight); cY = (y * 5 / weight); picRGB = insertShape(picRGB,'Rectangle',[cX cY 100 100],'LineWidth',5); %黄色の四角を描画 end
次の状態の予測
トラッキングしたパーティクルフィルタが次にどこへ移動するかを予測します。ここでパーティクルが動ける領域を可変させると、より良い追跡が可能かもしれません。
function PM = pm_predict(PM,P) for m=1:1:P x = PM(1,1,m) + randi([-10 10]); if x < 1 % x座標の稼働領域を指定 x = 1; elseif x > (200-20) x = 200 - 20; end y = PM(1,2,m) + randi([-10 10]); if y < 100 % y座標の稼働領域を指定 y = 100; elseif y > (200-20) y = 200 - 20; end PM(1,1,m) = x; PM(1,2,m) = y; end end
作ってみた
重みの合計値などから、特徴点が異様に少ない時を判別できると思います。この時は全体的に暗めな場合が多いです。
もちろん特徴点が本当にない時と区別をつける必要があります。この辺りは工夫して実装すると面白いと思います。
youtu.be
参考
*1:あくまで例です。なにをもって尤度とするかは実装に依ります
Chiselを使ったRISC-Vの勉強(13.FPGAへの実装1)
メモリを同期読み込み書き込みにする
製作してきたChiselプロジェクトをFPGAに実装してみました。
Chiselを使ったRISC-Vの勉強(1)から目標にしていたようにAvalon-MM Masterとして動作させようとすると、
- 命令フェッチ時にデータ取得が1クロック遅れる
- ロード動作時に1クロック遅れる
(= どちらもメモリ読み込み動作時にのみ1クロック遅れる)
という現象が生じていました。これはChiselで命令メモリ/データメモリをMem(同期書き込み・非同期読み込みメモリ)として実装していたからです。
命令メモリ、データメモリ共にSyncReadMemに置き換えました。
github.com
git clone http://github.com/panda5mt/KyogenRV -b 0.1.1 --depth 1
超ミラクルスーパーウルトラ大規模改修工事の恐れがありましたが、パイプライン動作そのものを大きく変更するような変更がなかったため比較的小規模な改修ですみました。riscv-testsも変わらずパスしています。
この修正に伴い、追加で修正した分
- メモリR/W動作をハンドシェイクからレイテンシ動作に変更
Avalon-MMでは指定されたレイテンシでメモリリードライトを行います。もしスレーブが期待されたレイテンシで応答できない場合、スレーブ側はホストへwaitrequest信号を発行します。
ですので、Chisel側も準じた実装に変更しました。レイテンシに対応したステージでR/W動作を行い、waitrequest信号がきたらストールするだけです。(かえってハンドシェイク方式より単純になりました)
FPGAへの実装テスト
ここでQuartus Primeの使い方を紹介するつもりはないので少し駆け足で説明します。
ハーバードアーキテクチャCPUソフトコアをAvalon-MM Masterとする場合、命令メモリとデータメモリの2つのMasterを持つと思います。
この二つは独立ではなく、Back Pressureに支配されて連携動作をします。(今回はキャッシュ非搭載のため、命令/データは都度フェッチします)
- ChiselプロジェクトからVerilogコードを生成させる。
- PlatformDesignerに読み込ませるためのトッププロジェクトを作り(今回はSystemVerilog)、上記コードをインスタンスする。
- 新規プロジェクトを作り、PDで上記CPU + On-chip Memory + IOを使ったプロジェクトを作る
- hexエディタで命令を書く
- SigmalTapで実行中のCPUの動作を確認
項目2.の信号はavalon-MM Master信号と下記のように対応させました。
- clock
- reset
- byteenable
- waitrequest
- address
- read
- readdata
- write
- writedata
PlatformDesignerに登録が終わったら、項目3に進んでいきます。
それぞれの接続は以下のようになりました。onchip_memoryは0x0000番地から、I/Oはデータメモリ開始番地0x8000に配置します。
項目4.
以下は疑似コードです。これをアセンブラで記述します。今回書くコードの目的は命令/データメモリが正しく動作しているかの確認です。
uint32_t data; *(*PIO_BASE_ADDRESS) = 0xAA; // PIO = 0xAA data = *(*PIO_BASE_ADDRESS); data = data + 1; *(*PIO_BASE_ADDRESS) = data; // PIO = 0xAB
上記をアセンブラで記述します。
lui x1, 0x08 # x1=0x8000 li x2, 0xAA # x2 = 0xAA sw x2, 0(x1) # dmem[0x8000] = 0xAA lw x3, 0(x1) # x3 = dmem[0x8000] addi x3, x3, 1 # x3 = x3 + 1 sw x3, 0(x1) # dmem[0x8000] = x3 _loop: jal x0, _loop # loop
と書いてもいいのですが、これだとデータメモリに格納される前にデータフォワーディングでRWされるデータがよしなに取り扱われてしまうので、適宜パイプライン段数以上のnopを入れてやります。そうすることで本当にデータメモリの読み書きが機能しているかを確認することができます。
lui x1, 0x08 # x1=0x8000 li x2, 0xAA # x2 = 0xAA sw x2, 0(x1) # dmem[0x8000] = 0xAA nop nop nop nop nop lw x3, 0(x1) # x3 = dmem[0x8000] nop nop nop nop nop addi x3, x3, 1 # x3 = x3 + 1 nop nop nop nop nop sw x3, 0(x1) # dmem[0x8000] = x3 _loop: jal x0, _loop # loop
みたいな感じです。アセンブルして、得られた機械語をQuartusのHexエディタで記録保存、Onchip Memoryの初期値データとします。
SignalTapで動作状況を見てみました。
ロード命令も動作しています。waitrequestにより1クロック待たされていますが、動作しています。
Chiselを使ったRISC-Vの勉強(12. riscv-testsの全項目クリア)
riscv-testsクリア
riscv-testsのリグレッションテストを全てクリアしました。(fence.i命令は除外しました。RISC-Vの基本Iアーキテクチャでは必須ではなくなったからです。)
テストがfailしたもののほとんどはCSRに起因し、CSRレジスタのフォワーディングの実装の間違いに起因するものでした。
これはトレース結果を見ればすぐにわかるので修正→全テストのやり直しを繰り返せばクリアできるものが仕上がっていきます。
ここでは覚書としてクリアするのに1日以上かかった項目を挙げていきます。
CSRの実装 ≒ 例外の実装
intel FPGAに実装する際、カスタム命令を実行するコプロセッサを接続する予定なのでCSRを結局外してしまうのですが、ISAに定められている例外を正しく実装する事はコプロセッサを作る際の参考にもなるため、CSRをできる限り正しく実装することには意味があると言えます。
RISC-Vはフラグを持っていないCPUなので例外に頼る必要もあると思われます。
躓いていたテストは以下の実装でした。
- rv32mi-p-illegal
- rv32mi-p-ma_addr
- rv32mi-p-ma_fetch
- rv32mi-p-shamt
- rv32mi-p-illegal
illegalに関しては今回はRV32Iのみのアーキテクチャであり、RVC(圧縮命令)の実装をしていないので、16bit命令が来た際に正しく不正命令例外を発生させればよくクリアできました。
- rv32mi-p-ma_addr
load/storeのミスアラインアドレスのテストです。
ここで例外が発生したときにmepc(発生時のpc)に加えて、mtval(発生する要因となった命令そのもの)を格納する実装をしました。
- rv32mi-p-ma_fetch
CSRモジュールはEXステージの時のPC,ALUへの入力値(rs1,rs2,immなど)を与える実装にしています。ミスアラインが発生するのは分岐命令時です。分岐先に指定されているアドレスが不正なために例外が発生します。
- jal, jalrは条件なく分岐
- branch命令は条件成立で分岐
これが少し厄介な状況を生み出します。jal/jalr命令は分岐先を調べて即例外を発行できるのに対し、branchは少なくともmemステージ(=ALUから比較結果の回答待ち)まで待たないといけません。ですので以下のような対応にしました。
- jal, jalrは不正なアドレスを確認したら即例外発行
- branch命令はpcと命令を一旦仮に格納(mepc,mtval)→分岐後不正なpcであった場合仮格納していたpcと命令を復元し例外発生
としました。ここで、「jal/jalrもいっしょの挙動にすりゃいいじゃん」または「CSR動作中は本体CPUストールすれば?」となります。
後者は検討中です。CSR自体もパイプライン動作をしているので「CSRモジュールが動作中か否か」という条件を洗い出す作業とALUをCSRに引き渡すロジックの追加が必要になります。
前者は容易に思いつきますができません。理由は以下の通りです。
jal rd, <jump_address>
とした場合、rdにはこの命令の次のpcが格納されます。不正なアドレスにジャンプした後に例外判定をすると、rdが更新された後になってしまうため、rdの値をこの命令実行前の状態に復元する必要があります。
できなくはないけどする必要は全くないですし、いらない32bitのレジスタと配線を増やしてFPGAの動作を低下させる必要はないですね。
この命令もそうですが、パイプラインのなるべく早い段階で例外を発見し処理すると不要なパイプラインレジスタを実装せずにすみ、論理回路消費を抑え、高速化に寄与します。
- rv32mi-p-shamt
シフト命令で(※純粋なシフト命令のみのテストではない)テストパスできないなんてあるのか楽勝じゃん?とおもったら最後までPassできずに泣いていました。
躓いていたのはこの命令。
slli a0,a0,0x20 // a0 = a0 << 32
32bitシフトしたらゼロにすりゃいいのでは?と思っていたら、そういう理由ではありませんでした。
RV32Iでこの命令の即値は5bitです。つまり、0x20という表現はできません。不正命令として例外吐く動作が正しい挙動でした。基本的すぎるミスでウケる。
と、こんな感じでようやくクリアできました。
ここまで到達するのに3ヶ月。長かったですね。
今後
ランダムテストを通しながら以下の独立した試行をやっていきます。
コード
ここまでのコード全文は以下で。
github.com
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.1.0 --depth 1
Chiselを使ったRISC-Vの勉強(11. riscv-testsの自動化)
riscv-testsのリグレッションテストコードを自動生成
あまり記事にするほど内容は厚くないのですが備忘録として。
一つのテストコードをpassできても、今までpassできていたものがfailしてしまうロジックを埋め込んでしまう可能性は否めません。
全てのリグレッションテストを通しでpassさせる必要があります。今回の構成の場合、マシンモード/メモリウォーキング非搭載/32bit整数のみ/圧縮命令なしなのでrv32ui-p-/rv32mi-p-に対応すれば良く、テストコードは少なめの48個です。
まぁ比較的少ないのですが、手動で書いているとしんどいのです。
"rv32ui-p-add.hex test using Driver.execute" should "be pass test." in { iotesters.Driver.execute(Array(), () => new CpuBus())(testerGen = c => { CpuBusTester(c, "src/sw/rv32ui-p-add.hex") }) should be (true) }
こんな感じのコードをリグレッションテスト数だけ作り、今後64bit拡張、Sv39搭載なども考えているのでテストは増大することになります。今のうちにpythonで自動化することにしました。
参考にさせていただいたのはid:msyksphinz氏のブログ。
msyksphinz.hatenablog.com
方針を決めていきます。
- 手作業で頑張る部分
- riscv-testsをgit cloneし、kyogenrvのプロジェクト内にmakeするところまでは手動(これは1回実行すればいいはずなので)
- 自動化した部分
- フォルダ内のrv32ui-p-* rv32mi-p-*で始まるelfファイルを走査し、hexファイルを作る
- 作成したhexファイルのScalaTestコードを自動生成
- テストコード実行
使い方
readmeには記載したのですが、改めて。
- kyogenrvを導入
git clone http://github.com/panda5mt/KyogenRV
- riscv-testsを導入します
git clone https://github.com/riscv/riscv-tests cd riscv-tests git submodule update --init --recursive
- リンカスクリプトの修正
nano env/p/link.ld
プログラム (.text)セクションが0x00000000
から開始されるように修正
SECTIONS { . = 0x00000000; # -> ここを修正 .text.init : { *(.text.init) } . = ALIGN(0x1000); .tohost : { *(.tohost) } . = ALIGN(0x1000); .text : { *(.text) } . = ALIGN(0x1000); .data : { *(.data) } .bss : { *(.bss) } _end = .; }
- link.ldを上書きしriscv-testsをビルド
autoconf ./configure --prefix=<KyogenRVsプロジェクトのルートディレクトリのフルパス>/tests/ make make install cd ../
- KyogenRVのプロジェクトディレクトリに戻ります
cd KyogenRV/ make clean make riscv-tests
これでテストコードのhex生成からリグレッションテストまで一貫して行います。終了するまで3分ほどかかりました。長い。
ちょっと工夫しないとなぁ。
IntelliJのGUIで実行すると便利
IntelliJ IDEAユーザの方はmake riscv-tests
するのではなくterminalで
make tester-gen
すると、テストコード生成までで終えてくれます。あとはGUIで行う方がpass/failが分かりやすいと思います。
/src/test/scala/TestCoreAll.scala
が自動生成されたコードです。
下図のようにクリックし、[Run TestCoreAll]を選択します。
実行中。プログレスバーも出て進行度がわかります。
リグレッションテストがどこでfailしたかも一目瞭然。(下図の場合はfence.i命令テストでエラー)
Chiselを使ったRISC-Vの勉強(10. Load/Store全命令の実装)
実装中のRV32I RISC-V CPUですが、5段パイプラインのまま継続するか、6段パイプラインに増やすか見積もるため、Load/Storeを真面目に実装していませんでした。(今まで対応していたのは32bit長のlw/swのみ)
結論から申しますと、5段パイプラインのまま継続することにしました。詳細は後述する「パイプライン数の見積もり」をご覧ください。また、RV32Iとして一定の完成度になりつつあると思えてきたので、riscv-testsによるリグレッションテストも実施しました。
.dataセクションにも書き込めるようにする
Load/Store命令の実装がISA仕様を満たしているかをtestするにはriscv-testsのうち以下のものをクリアしなければいけません。
- rv32ui-p-sb
- rv32ui-p-sh
- rv32ui-p-sw
- rv32ui-p-lb
- rv32ui-p-lbu
- rv32ui-p-lh
- rv32ui-p-lhu
- rv32ui-p-lw
Dumpファイルを見れば分かりますが、上記のコードでは.dataセクションに初期値を置くため、データメモリが実装されていないとテストにパスしません。
今回製作したchiselプロジェクトではchisel-iotestersでコードを命令バス経由でメモリに書き込みます。
データメモリもこの命令バスからでもアクセスできるよう改造しました。
ですので、アンチグリッドロジックの実装は完全にAvalon-MM任せにできるので行いません。intel FPGA便利。
byte/halfword/wordの読み書きに対応する
データメモリ、命令メモリとも32bitアドレス/32bit幅のみのアクセスしかできていませんでしたが、RV32Iの場合load/store命令はbyte(8bit)/halfword(16bit)/word(32bit)のアクセスができないといけません。
これもAvalon-MMバス上ではbyteenable信号によりアラインメントの制約があるものの、8/16/32bitアクセスが可能です。
例えば、下位8bitのみ書き込みがしたい場合、
- writedata = (32bitデータ)
- byteenable = 0b’0001
とすれば32bitのうち、下位8bitのみ書き込まれます。
- writedata = (32bitデータ)
- byteenable = 0b’1100
とすれば32bitのうち、上位16bitのみ書き込まれます。
参考:
https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/manual/mnl_avalon_spec.pdf
Chisel上でのメモリ側の実装は8bit x 4レーンに分け、byteenable信号によって書き込むレーンを選択するロジックを追加しAvalon-MM接続時と同等の環境を用意しました。
CPU側から、データメモリへの書き込みロジック(Store命令)は以下の通りです。
// send bus write size io.w_dmem_dat.byteenable := DontCare // mem_rs(1) when(mem_ctrl.mem_wr === M_XWR) { // Store命令か? when(mem_ctrl.mask_type === MT_B) { // byteライト switch(mem_alu_out(1, 0)){ // アドレスの下位2bitでアラインを判断 is("b00".U){ io.w_dmem_dat.byteenable := "b0001".U io.w_dmem_dat.data := mem_rs(1) } is("b01".U){ io.w_dmem_dat.byteenable := "b0010".U io.w_dmem_dat.data := mem_rs(1) << 8.U } is("b10".U){ io.w_dmem_dat.byteenable := "b0100".U io.w_dmem_dat.data := mem_rs(1) << 16.U } is("b11".U){ io.w_dmem_dat.byteenable := "b1000".U io.w_dmem_dat.data := mem_rs(1) << 24.U } } }.elsewhen(mem_ctrl.mask_type === MT_H) { // halfwordライト switch(mem_alu_out(1, 0)){ is("b00".U){ io.w_dmem_dat.byteenable := "b0011".U io.w_dmem_dat.data := mem_rs(1) } is("b10".U){ io.w_dmem_dat.byteenable := "b1100".U io.w_dmem_dat.data := (mem_rs(1) << 16.U) } } }.otherwise { // wordライト io.w_dmem_dat.byteenable := "b1111".U io.w_dmem_dat.data := mem_rs(1) } }
CPU側から、データメモリへの読み込みロジック(Load命令)は以下の通りです。
when(mem_ctrl.mem_wr === M_XRD) { // Load命令か? when(mem_ctrl.mask_type === MT_B) { // byteリード switch(mem_alu_out(1, 0)){ is("b00".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(7)), io.r_dmem_dat.data( 7, 0)) } is("b01".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(15)),io.r_dmem_dat.data(15, 8)) } is("b10".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(23)),io.r_dmem_dat.data(23,16)) } is("b11".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(31)),io.r_dmem_dat.data(31,24)) } } }.elsewhen(mem_ctrl.mask_type === MT_BU) { // byteリード符号なし switch(mem_alu_out(1, 0)){ is("b00".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data( 7, 0)) } is("b01".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(15, 8)) } is("b10".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(23,16)) } is("b11".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(31,24)) } } }.elsewhen(mem_ctrl.mask_type === MT_H) { // halfwordリード switch(mem_alu_out(1, 0)){ is("b00".U){ wb_dmem_read_data := Cat(Fill(16, io.r_dmem_dat.data(15)), io.r_dmem_dat.data( 15, 0)) } is("b10".U){ wb_dmem_read_data := Cat(Fill(16, io.r_dmem_dat.data(31)), io.r_dmem_dat.data( 31, 16)) } } }.elsewhen(mem_ctrl.mask_type === MT_HU) { // halfwordリード符号なし switch(mem_alu_out(1, 0)){ is("b00".U){ wb_dmem_read_data := Cat(0.U(16.W), io.r_dmem_dat.data( 15, 0 )) } is("b10".U){ wb_dmem_read_data := Cat(0.U(16.W), io.r_dmem_dat.data( 31, 16)) } // others is("b01".U){ wb_dmem_read_data := 0.U } is("b11".U){ wb_dmem_read_data := 0.U } } }.otherwise { wb_dmem_read_data := io.r_dmem_dat.data } }.otherwise { wb_dmem_read_data := io.r_dmem_dat.data }
パイプライン数の見積もり
上記実装の試行錯誤をしながらパイプライン数の見積もりをしていました。
結論として、
- Load命令をStore命令にくらべて極端に多用する場合は6段パイプライン(たとえばGPIOの入力を絶えずポーリングするなど)
- それ以外の場合は5段パイプラインで十分そう
Load命令が連続すると、ストール時間が増えます。加えて
(中略) Load命令 分岐命令
という順序の場合、Load命令が分岐命令によってフラッシュされないようストール時間がさらに1クロック分増えます。この処理を多用するポーリング動作の場合は6段に拡張した方が処理速度は上がります。
が、低速な8bitマイコンではないのでこういう処理ばかりに専念させないと思われます。割り込み使うし。
加えていうのであれば、そこまでレイテンシ気にする場合はパイプラインマシンにとってコストが高い分岐命令を使うな、というところにまで発展するので。。。
こう言った結論から、5段パイプラインのままにしました。
riscv-testsを通してみた
RV32Iに要求されているテストのうち、以下がパスできたものです。
- rv32ui-p-add
- rv32ui-p-addi
- rv32ui-p-and
- rv32ui-p-andi
- rv32ui-p-auipc
- rv32ui-p-beq
- rv32ui-p-bge
- rv32ui-p-bgeu
- rv32ui-p-blt
- rv32ui-p-bltu
- rv32ui-p-bne
- rv32ui-p-jal
- rv32ui-p-jalr
- rv32ui-p-lb
- rv32ui-p-lbu
- rv32ui-p-lh
- rv32ui-p-lhu
- rv32ui-p-lui
- rv32ui-p-lw
- rv32ui-p-or
- rv32ui-p-ori
- rv32ui-p-sb
- rv32ui-p-sh
- rv32ui-p-simple
- rv32ui-p-sll
- rv32ui-p-slli
- rv32ui-p-slt
- rv32ui-p-slti
- rv32ui-p-sltiu
- rv32ui-p-sltu
- rv32ui-p-sra
- rv32ui-p-srai
- rv32ui-p-srl
- rv32ui-p-srli
- rv32ui-p-sub
- rv32ui-p-sw
- rv32ui-p-xor
- rv32ui-p-xori
- rv32mi-p-breakpoint
- rv32mi-p-csr
- rv32mi-p-mcsr
- rv32mi-p-sbreak
- rv32mi-p-scall
パスできなかったもの
- rv32ui-p-fence_i
- rv32mi-p-ma_addr
- rv32mi-p-ma_fetch
- rv32mi-p-illegal
- rv32mi-p-shamt
なおfence.i
命令はこの記事執筆時点(ISA v.2.1)ではRV32/64/128の基本I命令からは外されており、"Zifencei"extensionという拡張扱いになっています。
実装は必須ではありません。命令キャッシュとデータキャッシュのコヒーレント性を保つ唯一の命令ですが、実装コストの大きさ、およびunix系OSで命令フェッチした際のコヒーレンス維持命令としては実用に耐えないと見たようです。
詳しくは以下の、p31,32を参照。
https://github.com/riscv/riscv-isa-manual/releases/download/draft-20200611-d08e29e/riscv-spec.pdf
あと、rv32mi-p-ma_*のテストコード、dumpファイルを見ると16bit命令が埋め込まれているので圧縮命令なのかな?(ma=misalignedってこと?)今回Cには対応しないのでこれはパスしなくていいかなという気持ち。あるいは例外吐く処理を入れるべきかな。。調査中
ミスアライン例外を起こすのが正解でした。
対応します。
その他のものはCSRの実装が現状で不完全なため動作が確認できていませんが、OpenOCDなどデバッガを接続した際に必須となるものばかりなので、実装していきます。
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.25 --depth 1 cd KyogenRV/
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。
brew install coreutils
makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。
nano src/sw/test.s
Chiselを使ったRISC-Vの勉強(9.CSR:外部割り込みの実装)
外部割り込みを実装しました。
RISC-Vに実装する割り込みはCPU外部からの割り込みだけ(あるいは各割り込み入力をマルチプレクスしてここへ入力、または割り込み信号の判別だけできるようにして入力を増やしていく感じで)でいいかな、と思ってきた次第です。
ターゲットFPGAはintel P.S.GのCyclone10LP/GX/MAX10シリーズを考えているのでペリフェラルの実装はPlatform Designerで行います。
各モジュールはCPUの外部かつAvalon-MM上に存在することになります。割り込みはこの外部からの信号のみ受け付けるようにします。
事の発端
タイマー割り込みの調査をしていました。
- mtime : 時間計測用。一定のクロックでカウントアップ。RV32/RV64でともに64bit長
- mtimecmp:時間比較用レジスタ。mtime >= mtimecmpでタイマー割り込み発動。RV32/RV64でともに64bit長
となっており、タイマー割り込み発火のためには以下のようにプログラムを記述します。
- mtvecに割り込みトラップ先のアドレスを格納
- mtimecmpに割り込み発動の時間を格納
- mie.mtie=1にしてタイマー割り込みを許可
なるほどねーと
アセンブラを書いてみました。
addi x1, x0, 0x3FF csrrw x0, mtimecmp, x1
コンパイルすると
Error: unknown CSR `mtimecmp'
???
Machine time register (memory-mapped control register). Machine time compare register (memory-mapped control register).
メモリーマップドなのかーい!
Privileged Architecture Version 1.7を見るとまだmtime
,mtimecmp
は32bit長でCSRレジスタになっており、v.1.9あたりからメモリマップドに変わっています。特にhart>=2であれば(いわゆるマルチコアであれば)割り込み関連のSFRはメモリマップドにする方が全てのhartのSFRに素早くアクセスができ、得策といえます。一見、当たり前のことのなのですが、ちょっとびっくり。
ここで方針を変更します。
CSRからメモリマップドIOにダイレクトにアクセスするのは得策とは言えません。
CPUとリソースの取り合いになるからです。割り込み発動も遅くなります。それにこんな事で調停回路つくるの超めんどくさい。
せっかくメモリーマップドなので、メモリーマップドなタイマーモジュールを作ります。
このモジュールにアクセスするのにCPUがmtime
,mtimecmp
またはそれ相当のSFRをRWするというわけです。
そしてこのモジュールがmtime >= mtimecmp
となったときにCSRレジスタMIP.MTIP=1となるようにします。
割り込み回路実装の検討
ここで外部割り込みでもタイマー割り込みでも仕組みが同じになってしまいました。
違いは割り込み要因が外部割り込みMIP.MEIP=1
かタイマー割り込みMIP.MTIP=1
かの違いです。
タイマー割り込み回路を作る前の素振りとして、外部割り込み回路を作ります。
外部割り込み回路実装
CPUへの実装をしていきます。
- 割り込み入力用の1ビット信号を用意
- 任意のタイミングで割り込みビット=1
- 割り込み発生
- mepcへ発生時のアドレスを格納
- mtvecのアドレスに分岐
簡単です。
このとき、割り込み発生時に分岐命令処理中ですと割り込み信号による例外が握り潰されてしまうことがあるので、pc更新の論理回路が例外発生時の処理を最優先にするよう修正しています。
mepcがゼロになるんだが
mepcは前回のブログでも取り上げた通り例外発生時にいたpcを格納するCSRレジスタです。
実はむずかしいのはmepcの実装でした。理由は以下の通りです。
マルチパイプラインCPUなので、分岐中は実際にデコードや演算処理しないけど便宜上フェッチする(あとでnopにするかフラッシュする)命令があったり、ストール中は一旦pcをゼロにして、分岐完了後に正しいカウンターをフェッチして再カウントしたりします。
という訳で、いつ発動するかわからない割り込みは、場合によっては有り得ないpcを戻り値としてmepcへ格納してしまうことになりかねません。
対策としては、パイプラインが処理中の内容を監視し、ストール中か分岐中かを判別する。もしどちらかの状態であれば、その時点で有効だった最後のpcの値を戻り値としてmepcに格納します。
判別用にpc_invalidという信号を用意しました。
// program counter check val pc_invalid: Bool = inst_kill_branch || (ex_pc === pc_ini)
割り込みが発動した場合かつ分岐中、ストール中(pc=0)はmepcにpcを格納しないようにしました。
この場合は最後に有効だったpcをmepcへ格納します。
それ以外の例外発生時、例えばecall命令などではどうなるかというと、自身が原因でストールが発生、次フェーズで例外要因の分岐が発動しているはずなので上記の処理したpcを戻り値として格納してはいけません。
例外発動するまえのpcの値が格納されるので正確ではなくなる。そのまま現在のpcをmepcへ格納するのが正しいといえます。
とまぁ、src/main/scala/core/csr.scalaを見てもらえればこの辺りは分かっていただけるかと思います。
実行結果
アセンブラを記述します。<0xXX>と書いてあるのはその命令のアドレスです。割り込みが発生すると_label_expc(=0x18番地に飛びます)
_label0: addi x1, x0, 0x18 # <0x00> csrrw x0, mtvec, x1 # <0x04> : mtvec = <0x18> lui x1, 0x01 srli x1, x1, 1 csrrw x0, mie, x1 # enable external interrupt(mie.mtie = 1) jal x0, _label1 # jump _label1 _label_expc: addi x2, x0, 0xAA # <0x18> csrr x3, mepc # csrr x4, mcause # jal x0, _label_expc # <0x24> : loop _label1: addi x5, x0, 0xBB # <0x28> _label2: addi x6, x0, 0xCC # <0x2C> jal x0, _label2 # <0x30>
さて今回は、アセンブラを書いただけだと割り込みが発生したかどうかわかりません。
chisel-iotestersでCPUに擬似的に任意の時間に割り込み信号を注入します。(今回はlp==96としました。割り込み許可フラグが1になった後であればどのタイミングでもOK)
// src/main/scala/core.scalaの578行目付近 (中略) if(lp == 96){ poke(signal = c.io.sw.w_interrupt_sig, value = true.B) } else{ poke(signal = c.io.sw.w_interrupt_sig, value = false.B) }
実行後。
x2 = 0xAA
となっているので無事割り込みトラッピングに成功しています。また
x3 = mepc = 0x2c
となっているので、割り込み発動時にaddi x6, x0, 0xCC
を実行中だったことがわかります。割り込み発動のタイミングによっては
x3 = mepc = 0x30
となる場合もあると思います。コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.21 --depth 1 cd KyogenRV/
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。
brew install coreutils
makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。
nano src/sw/test.s