Running code on SPI ROM of TinyFPGA BX with VexRiscv (small) Briey SoC, or XiP (execute-in-place) in short.
前回は、VexRiscv 上の RISC-V SoC に AXI4 のバスマスタを追加してみましたが、今回はその逆に、AXI4 のバススレーブを追加してみました。それだけでは面白くないので、TinyFPGA BX 上の SPI ROM にメモリマップでアクセスできるようにして、SPI ROM 上のバイナリを直接 RISC-V から実行できるようにしました。
つまり、例えば 0x8000_0000 番地を RISC-V CPU コアからリードアクセスすると、SPI ROM にアドレスを送信してデータを読み出すようにする訳です。TinyFPGA BX に載っている iCE40-LP8K 上のブロック RAM は 128K ビット(16K バイト)しかないので、SPI ROM 上にバイナリを載せることができれば、ずっと大きなプログラムを実行できるようになります。
なお、今回は SPI ROM のアクセスを通常のシリアルアクセスで実装しました。これをさらに、Quad-I/O Read ですとか DDR(double data rate)アクセスできるようにすると、もっと高速にできますが、次回以降への宿題にしたいと思います。
GitHub レポジトリ
コードは、こちらに置きました。
ビルド方法の説明はこちらです。
コードの説明
SpiMasterCtrl クラス
class SpiMasterCtrl というのが、SPI ROM にアクセスする部分です。
case class SpiMasterCtrlCmdSet(
g: At25sf081Generics = At25sf081Generics()
) extends Bundle {
val spiCmd = Bits(g.widthCmd bits)
val addr = UInt(g.widthAddr bits)
val withAddr = Bool
val withRead = Bool
val lenRead = UInt(log2Up(256 * 4) bits)
}
class SpiMasterCtrl(
g: At25sf081Generics = At25sf081Generics()
) extends Component {
val io = new Bundle {
val cmd = slave(Stream(SpiMasterCtrlCmdSet()))
val data = master(Flow(Fragment(Bits(g.widthData bits))))
val spi = master(SpiMaster(useSclk = true))
}
cmd は、外部からコマンドを受け付ける Stream インターフェイスで、data は、読み出したデータを返す Flow インターフェイスです。spi は、SPI インターフェイスへの接続部分ですね。
cmd ストリームのペイロードには、以下が含まれます。
- spiCmd: SPI メモリへのコマンド(8ビット)
- addr: アドレスが必要な場合は、アドレス(24ビット)
- withAddr: アドレスが必要か否か(Bool)
- withRead: データを読み出すか否か(Bool)
- lenRead: データを読み出す場合は、そのバイト数マイナス1(10ビット)
SpiMasterCtrl のシミュレーション
シミュレーション用のオブジェクト SpiMasterSim を作ったので、実際に動かしてみましょう。
まずは、コマンドだけを指定する場合です。(クリックすると拡大できます)
ここではコマンド 0xab を送っています。良さそうですね。ちなみに、コマンド送信が終わると io.data.valid 信号がアサートされます。
次に、コマンドとアドレスを指定する場合です。これは実際には使わないと思いますが、作ってしまったのでテストします。
これも大丈夫そうです。
最後に、データリード(4バイト)を伴うテストです。
これも OK ですね。なお、1バイト読み出すたびに io.data.valid がアサートされ、io.data.payload.fragment でデータが返ってきます。最後は、io.data.payload.last ビットもアサートされます。
AXI4 スレーブインターフェイスの実装
続いて、VexRiscv SoC の AXI4 クロスバに接続するために AXI4 スレーブインターフェイスを設計します。最初、スレーブのほうがマスタより簡単だろうと思ったのですが、実はとんでもなく、スレーブのほうが大変でした。なぜかというと、スレーブはマスタからの指示内容を限定できないので、ワードサイズやバーストアクセス長を正しく解釈して実装しないといけないからです。
ただし、完全に厳密に全ての AXI4 仕様を満たすのは無理なので、以下のような制約を設けました。
- ARID(リードアクセスID): これは正しく処理します。
- ARLEN(バーストアクセス長): これは 0〜255まで正しく処理します。
- ARSIZE(バーストサイズ): これは常に 0b010(4バイト転送)と仮定します。実際には VexRiscv はバイトアクセスやハーフワードアクセスもしてきますが、RDATA(32ビット幅)の適切な場所にデータが入っていれば問題なく動作するようなので、そのようにしました。
- ARBURST(バーストタイプ): 常に INCR と仮定します。これで大丈夫そうです。
- AR系の、それ以外の信号ラインは使いません。
- RID, RDATA, RLAST, RVALID は正しく処理します。RREADY は無視します。
AXI4 スレーブインターフェイスは、Axi4RomAt25sf081 クラス で実装しています。
アドレスマップ
さて、この AXI4 スレーブの ROM をどのようにメモリマップするか、です。TinyFPGA BX では、ROM(1Mバイト)の 0x2_8000 からビットストリームが入り、0x5_0000 からの 704Kバイトはユーザー用に開放されています。そこで、ROM のユーザーエリアを CPU から見て 0x8000_0000 にマップし、同時に、ROM 全体のメモリ空間を 0x8010_0000 からマップすることにしました。
AXI4 スレーブインターフェイスのテスト
上記を組み込んだ(ミニ)Briey を Verilator シミュレーションでテストしてみました。まずは、I キャッシュを使わない場合です。(クリックすると拡大できます)
バスからのアドレス 0x0(バス幅は 21ビット)のときに 0x5_0000 に 4バイトのリードが発生し、AXI4 の R インターフェイスで 32ビットのデータが一つ返されていることが分かります。(SPI ROM のシミュレーションをしていないので、返り値 RDATA は 0x0 になっています。)
続いて、I キャッシュをオンで試してみましょう。(見づらくてスミマセン。クリックして拡大するか、御自身でシミュレーションしてみてください。sbt “runMain flogics.vexriscv.tinyfpga.Briey” でビルドしてから、ディレクトリ VexRiscv/src/test/cpp/flogics/briey で make clean run TRACE=yes すると波形データ Briey.vcd を得られます。ただし、BOOT という文字が出力されたら、すぐに Ctrl-C で停めたほうが良いです。波形データが巨大になります。)
ちゃんと RVALID が 8回アサートされ、最後に RLAST が立っているのが分かります。
あ、I キャッシュを有効にするパッチを示しておきます。
diff --git a/src/main/scala/vexriscv/flogics/Briey.scala b/src/main/scala/vexriscv/flogics/Briey.scala
index dba5301..a394bf1 100644
--- a/src/main/scala/vexriscv/flogics/Briey.scala
+++ b/src/main/scala/vexriscv/flogics/Briey.scala
@@ -189,6 +189,24 @@ object BrieyConfig{
rxFifoDepth = 16
),
cpuPlugins = ArrayBuffer(
+ new IBusCachedPlugin(
+ resetVector = 0x80000000l,
+ prediction = STATIC,
+ config = InstructionCacheConfig(
+ cacheSize = 512,
+ bytePerLine =32,
+ wayCount = 1,
+ addressWidth = 32,
+ cpuDataWidth = 32,
+ memDataWidth = 32,
+ catchIllegalAccess = true,
+ catchAccessFault = true,
+ asyncTagMemory = false,
+ twoCycleRam = true,
+ twoCycleCache = true
+ )
+ ),
+/*
new IBusSimplePlugin(
resetVector = 0x80000000l,
cmdForkOnSecondStage = true,
@@ -198,6 +216,7 @@ object BrieyConfig{
catchAccessFault = false,
compressedGen = false
),
+*/
new DBusSimplePlugin(
catchAddressMisaligned = false,
catchAccessFault = false,
SPI ROM を Resume コマンドで起こす
一つ大事なことを忘れてました。SpiMasterCtrl を設計したとき、どうしても SPI ROM の内容が読めず、オシロで覗いても分からず、えらく苦労したのでした。実は、どうも TinyFPGA のブートローダーで SPI ROM からビットストリームを読み出して起動した後、SPI ROM が Deep Power-Down モードになっているようなのです。確かにそのほうが消費電流が小さくなるのですが…。
ようやくそのことに気づいて、SPI ROM に Resume from Deep Power-Down コマンドを送るようにしたら、正しく動作するようになりました。そのロジックは、前述の AXI4 スレーブインターフェイスの Axi4RomAt25sf081 クラスにシーケンサとして組み込んであります(fsmResume 参照)。
その動作が、以下の波形です。
なお、Deep Power-Down からの resume や、その後のデータアクセスには最低 5マイクロ秒のウェイトが必要らしいので(データシート参照)、そのウェイトおよびリセット論理を設計してあります。class MiniBriey の、以下の部分です。
val resetCtrl = new ClockingArea(resetCtrlClockDomain) {
val timeWait = 10 us
val marginBy = 2
val cycleWait1 = (timeWait * axiClkFreq).toBigInt
val cycleWait2 =
(timeWait * 2 * axiClkFreq + (2 * 8 + 2) * marginBy).toBigInt
SpinalInfo("timeWait = " + timeWait.toString)
SpinalInfo("cycleWait1 = " + cycleWait1.toString)
SpinalInfo("cycleWait2 = " + cycleWait2.toString)
val reset1 = Reg(Bool) init (True)
val reset2a = Reg(Bool) init (True)
val reset2b = Reg(Bool) init (True)
val systemResetCounter = Counter(cycleWait2 + 1)
when(!systemResetCounter.willOverflowIfInc) {
systemResetCounter.increment()
}
when(systemResetCounter.value === cycleWait1) {
reset1 := False
}
when(systemResetCounter.value === cycleWait2) {
reset2a := False
reset2b := False
}
when(BufferCC(io.asyncReset)) {
systemResetCounter.clear()
}
val spiRomReset = reset1
val systemReset = reset2a
val axiReset = reset2b
}
val spiRomClockDomain = ClockDomain(
clock = io.axiClk,
reset = resetCtrl.spiRomReset,
frequency = FixedFrequency(axiClkFreq)
)
val spiRomArea = new ClockingArea(spiRomClockDomain) {
val spiRom = new Axi4RomAt25sf081(byteCount = 2 MB)
}
なお余談ですが、なぜ reset2a と reset2b が分かれているかというと、これを一緒にまとめると、FPGA 上で誤動作するのです。(具体的には、JTAG インターフェイスがおかしくなり、GDB の monitor reset halt コマンドが効かなくなる。) 理由は不明ですが、リセット信号の fan-out が大きくなりすぎることと関連があるのかも知れません。
RISC-V CPU プログラムのロードと実行
上記のような修正により、RISC-V CPU のプログラムは、もはや Verilog コードで実装してビットストリームとして書き込む必要がなくなりました。ビルドしたプログラムを objcopy コマンドの -O binary オプションで .bin ファイルに変換し、tinyprog の -u オプションで指定して SPI ROM のユーザーエリアに書き込めば、それで OK です。
なお、リンカスクリプトの修正が必要な点に注意してください。初期化する部分は SPI ROM 上に配置して良いですが、NOLOAD の部分(スタックやヒープ、.bss や .noinit など)は RAM 上に配置する必要があります。
今日はここまでとします。