はじめに
まさかこんな形でQiitaの初投稿をするとは思いもよらず
しかも今までソフト側かじっていたのにまさかのHDL投稿となることに
自分では驚きつつ、がんばって書いてみようと思います。
経緯
高専時代に半導体のことが一度いやになり、数年たった今、もう一度学び直すかと思い始めたコンピュータアーキテクチャの分野で、完全に1からトランジスタ〜プロセッサにかけて学び直していたところ、「思ったよりも面白いぞ?」ということがわかり気づいたらOSAであるRISC-Vの勉強を始めていました。
なかなか独学の初学者にとって学びやすいジャンルではなかったのですが、
こういう書籍からまずは実行していって、Privilege(特権)の仕様書を読んだりして
理論と実装を行ったり来たりしていました。
この本はVerilog-HDLの言語を使ってパイプラインの流れをざっと理解するのには良い本でした。
ただ、これマスターしたら RISC-V扱えるの?とか、特権レベルの扱いとかどうするの?拡張は?など実用的に使うためにはどうしたらいいのか。という疑問が残るばかりでした。
そんな中、とあるサイトでVerylを使って実用的なRISC-Vを実装するためのチュートリアルを示しているサイトを見つけました。
なるほどこれは面白いと。
RV32Iから64Iと、ビット幅の違いについて書きつつ、
MAC拡張まで網羅されている。おまけに特権階級についても記述されている。
これを書けるようになったらだいぶ成長できるのでは?
と思ったわけです。
しかもかなりのボリュームで順序立っているし
diffが記載されていてめちゃめちゃわかりやすいです。
本当にありがとうございました。
一つ枷を作りました
というわけでまずはトレースする形でも実行されることを確認したかったのですが
私はこの段階でまだSystemVerilogもまだ扱ったことがなかったので
「VerylをSystemVerylogに逆変換したらどっちの勉強にもなるのではないか」
という思考になりました。
というわけで、Verylで書かれているこの教材を、
SystemVerilogで書き換えながら学習を進めることにしました。
実際に実装してみた
ついに実装が始まったのですが、初めは思ったより手こずることはなく
VerylとSystemVeryligの対応を見ながらなんとか進めることができました。
これはちゃんと中に入れたHello,World! がRISC-Vで
アウトプットできるのかをチェックするためのコードです
~/risc-v-cpu ❯❯❯ DBG_ADDR=0x40000000 ./obj_dir/sim ~/risc-v-cpu/core/test/bootrom.hex ~/risc-v-cpu/core/test/test.bin.hex 0 ✘ 1 main
memory: initializing from /Users/shiraitouma/risc-v-cpu/core/test/test.bin.hex
memory: initializing from /Users/shiraitouma/risc-v-cpu/core/test/bootrom.hex
[DEBUG-IO] input = 'H' (0x48)
H[DEBUG-IO] input = 'e' (0x65)
e[DEBUG-IO] input = 'l' (0x6c)
l[DEBUG-IO] input = 'l' (0x6c)
l[DEBUG-IO] input = 'o' (0x6f)
o[DEBUG-IO] input = ',' (0x2c)
,[DEBUG-IO] input = 'w' (0x77)
w[DEBUG-IO] input = 'o' (0x6f)
o[DEBUG-IO] input = 'r' (0x72)
r[DEBUG-IO] input = 'l' (0x6c)
l[DEBUG-IO] input = 'd' (0x64)
d[DEBUG-IO] input = '!' (0x21)
![DEBUG-IO] input = '
' (0x0a)
test success!
- src/top.sv:111: Verilog $finish
無事テストがうまくいっている事がわかります。
しかし、ここに来るまでにSystemVerilogで詰まったところ(Verilatorで弾かれたところ)
がいくつかあったので、記録として残しておくことにします。
githubに一応アップしているのでもし興味があればご覧ください。
Memory Mapped I/O コントローラー
プロセッサを扱う場合、Boot用(初期化など)のROMを介した後に、RAM(アプリ)へジャンプする必要があります。つまり、アドレスに対して、アクセスするデバイスを切り替えを行うために、それぞれのメモリバスを管理したり、独立させる必要があります。
そのためには、まずはメモリのインターフェースを定義して、
そのI/Oをそれぞれの処理で記述する必要があります。
Verylではメモリのインターフェースを以下のように定義することができます。
interface membus_if::<DATA_WIDTH: u32, ADDR_WIDTH: u32> {
var valid : logic ;
var ready : logic ;
var addr : logic<ADDR_WIDTH> ;
var wen : logic ;
var wdata : logic<DATA_WIDTH> ;
var wmask : logic<DATA_WIDTH / 8>;
var rvalid: logic ;
var rdata : logic<DATA_WIDTH> ;
modport master {
valid : output,
ready : input ,
addr : output,
wen : output,
wdata : output,
wmask : output,
rvalid : input ,
rdata : input ,
wmask_expand: import,
}
modport slave {
wmask_expand: import,
..converse(master)
}
modport all_input {
..input
}
modport response {
rvalid: output,
rdata : output,
}
modport slave_output {
ready: output,
..same(response)
}
modport master_output {
valid: output,
addr : output,
wen : output,
wdata: output,
wmask: output,
}
// get DATA_WIDTH-bit expanded wmask
function wmask_expand () -> logic<DATA_WIDTH> {
var result: logic<DATA_WIDTH>;
for i: u32 in 0..DATA_WIDTH {
result[i] = wmask[i / 8];
}
return result;
}
}
alias interface Membus = membus_if::<eei::MEMBUS_DATA_WIDTH, eei::XLEN>;
メモリをコントロールするときは、CPU側(Master)と、メモリ側(slave)を意識する必要があるので、modport を利用して、inputとoutputを逆にする必要があります。
verilでは
..converse(master)とするだけで、masterの真逆だということを宣言する事ができます。
modport slave {
wmask_expand: import,
..converse(master)
}
SystemVerilogではそのまま記載することになります。
modport master (
import wmask_expand,
import reset_master,
output valid,
input ready,
output addr,
output wen,
output wdata,
output wmask,
input rvalid,
input rdata
);
modport slave(
import wmask_expand,
input valid,
output ready,
input addr,
input wen,
input wdata,
input wmask,
output rvalid,
output rdata
);
SystemVerilogではそういった宣言は存在しないので、
全て書き直す必要があります。(コピペしてinputとoutputを入れ替えるだけなのですが、Verylの方がミスは発生しにくいです。)
こいったところはまだいいのですが、かなり手こずったのが
alias interface Membus = membus_if::<eei::MEMBUS_DATA_WIDTH, eei::XLEN>;
これです。これは既存のインターフェース内のパラメータだけを変更して、そっくりそのまま利用できるVerylの便利な機能なのですが、これはSystemVerilogで代替法が見つからなかったので、
頑張って丸ごとコピペして、パラメータの部分だけ入れ替える方法を取りました。
interface Membus #(
parameter int DATA_WIDTH = MEMBUS_DATA_WIDTH,
parameter int ADDR_WIDTH = XLEN
);
logic valid;
logic ready;
logic [ADDR_WIDTH-1:0] addr;
//以下省略
いちばんの罠 function
メモリインターフェースを定義し終わって、MMIOコントローラの実装を開始することができました。
基本的には、Verylの構文をそのままSystemVerilogの形にやりなおすような方式で行っていたので、書くこと自体にそこまで時間はかかりませんでした。
// masterを0でリセットする
function reset_membus_master (
master: modport Membus::master_output,
) {
master.valid = 0;
master.addr = 0;
master.wen = 0;
master.wdata = 0;
master.wmask = 0;
}
function void reset_membus_master (
Membus.master_output master
);
master.valid = 1'b0;
master.addr = '0;
master.wen = 1'b0;
master.wdata = '0;
master.wmask = '0;
endfunction
しかしこれで実際にverilatorで make build しようとすると
エラーが起きてしまいます。
どうやら、SystemVerilogではfunctionの()の中に「.」などを入れたらうまくいかなくなってしまうようです。
つまり、頑張って作ったmodportなどは、functionでは動作しないということになります。
これはとても悲しかったですね。
色々工夫してみたのですがうまくいかず、結局最終手段を取ることにしました。
function void reset_membus_master (
output logic valid,
output logic [XLEN-1:0] addr,
output logic wen,
output logic [MEMBUS_DATA_WIDTH-1:0] wdata,
output logic [(MEMBUS_DATA_WIDTH/8)-1:0] wmask
);
valid = 1'b0;
addr = '0;
wen = 1'b0;
wdata = '0;
wmask = '0;
endfunction
元々インターフェースとしてうまく表現できていたものを、直接書く方法で切り抜けました。
実際入るデータに変わりはないので問題はないのですが、
こう考えるとVerylはスマートに書けているように考えられているな。と感じました。
こうなると実際にこのfunctionを実行する際にも
// すべてのデバイスのmasterをリセットする
function reset_all_device_masters () {
reset_membus_master(ram_membus);
reset_membus_master(rom_membus);
reset_membus_master(dbg_membus);
}
これでよかったものが
function void reset_all_device_masters ();
reset_membus_master(ram_membus.valid, ram_membus.addr, ram_membus.wen, ram_membus.wdata, ram_membus.wmask);
reset_membus_master(rom_membus.valid, rom_membus.addr, rom_membus.wen, rom_membus.wdata, rom_membus.wmask);
reset_membus_master(dbg_membus.valid, dbg_membus.addr, dbg_membus.wen, dbg_membus.wdata, dbg_membus.wmask);
endfunction
このようにわざわざ全て分割して書かなければいけなくなりました。
記載する要素が多くなればなるほど、新しいmembusが増えた時に
修正する要素が多くなり、結果エラーの数が大きくなるため
実際に実装する時にはかなりのタイムロス+リスクが大きくなることがわかりました。
終わりに
まだまだVerylとSystemVerilogの違いはありますが、
やはりVerylはSystemVerilogのフラストレーションだった部分をどう簡略化して
効率よく書けるようにするかを、かなり考えられている言語だと感じました。
ただそうはいっても、実際にこの言語が使われて仕事になっている場所の母数なども考慮するとSystemVerilogをもう少し自由に扱えるようになってから、
Verylも触っていくのが個人的にはいいと思いました。(Chiselとかも日本だと本が多そうだし、RISCV協会も推してそうだし、勉強するかな・・・)