AXI4 バスマスタを Briey SoC に追加してみた

(更新

Added an AXI4 bus master (DMA) to Briey, RISC-V SoC written by SpinalHDL.

久しぶりの投稿です。先日、TinyFPGA BX で、AXI4 クロスバ付きの VexRiscv SoC を動かしてみました。今回は重い腰を上げて、この AXI4 クロスバに新しいバスマスタ(つまり DMA)を設計して繋いでみましょう。

今回はデータの内容は特に問わないのですが、前回、外部クロック同期で SPI データを受信できるようになったので、まずは書き込み専用の DMA を設計してみました。具体的には、あるアドレス範囲に決まったデータを繰り返し書き込むようなものです。擬似的な C 言語で書くと、こんなイメージです。

int ct = 0;
volatile unsigned int * pt = (volatile unsigned int*) 0x80000f00;

// ...

    for (;;) {
        for (int idx = 0; idx < 0x100; idx ++, ct ++) {
            *(pt + idx) = ct;
        }
        wait_a_moment();
    }

これを、SpinalHDL が用意している Axi4CrossbarFactory を使って設計してみましょう。

コードをでっち上げる

SpinalHDL のドキュメントで、AXI4 に関する記述があるのは、以下の 2項です。

はっきり言ってこれだけ読んで理解するのは難しいのですが、SpinalHDL のソースコードを読んだりしながら、コードをでっち上げました。じゃん。

それでは、コードを順を追って説明して参りましょう。

クラスの宣言など

package flogics.lib.axi4

import spinal.core._
import spinal.lib._
import spinal.lib.fsm._
import spinal.lib.bus.amba3.apb.{Apb3, Apb3SlaveFactory}
import spinal.lib.bus.amba4.axi.{Axi4Config, Axi4WriteOnly}

これは、package(名前空間)の宣言と、使うライブラリの指定ですね。次にクラスの宣言部分です。

class SimpleAxi4Master(
    addr_begin: Long = 0x80000f00L,
    bits_range: Int = 8
) extends Component {

次が、io の定義です。Axi4WriteOnly という case class に、AXI4 のバスコンフィグレーションを渡して定義します。

  val io = new Bundle {
    val axi = master(
      Axi4WriteOnly(
        Axi4Config(
          addressWidth = 32,
          dataWidth = 32,
          useId = false,
          useRegion = false,
          useBurst = false,
          useLock = false,
          useCache = false,
          useSize = false,
          useQos = false,
          useLen = false,
          // 'false' useLast causes an assertion error in Axi4WriteOnlyDecoder
          useLast = true,
          useResp = false,
          useProt = false,
          useStrb = false
        )
      )
    )
  }

とりあえずシンプルな構成にしたかったので、ほとんどを false で定義していますが、useLast だけは、これを true にしないと Axi4WriteOnlyDecoder というクラスがエラーを出してきたので、true にしました。

Axi4WriteOnly

Axi4WriteOnly では、次の 3つの SpinalHDL Stream(つまり valid-ready ハンドシェイク)を使います。

  • AW: writeCmd(あるいは aw)
  • W: writeData(あるいは w)
  • B: writeRsp(あるいは b)

データの書き込みでは、writeCmd でアドレスを指定し、writeData でデータを指定し、writeRsp で書き込み完了の通知を受け取ります。つまり、バスマスタから見ると、AW と W の Stream が書き込み方向で、B が読み込み方向です。(もちろん、ready 信号は逆向きです。)

2つの状態マシン

上述のように、3つの Stream を別々にハンドシェイクしなくてはいけないので、個別に状態マシンを用意しました。AMBA のドキュメントにあるように、このハンドシェイクを適切にやらないとデッドロックの可能性があるようです。なお、今回は BREADY(writeRsp.ready)は監視しないので、状態マシンは 2つとしました。

状態マシンは、fsmAddr と fsmData です。以下にコードを示します。

  val fsmAddr = new StateMachine {
    val idle = new State with EntryPoint
    val writing = new State

    idle
      .whenIsActive {
        when(ct.willOverflow) {
          ct.clear()
          goto(writing)
        }
      }

    writing
      .whenIsActive {
        io.axi.writeCmd.valid := True
        io.axi.writeCmd.payload.addr := addr_begin | addr.resized
        when(io.axi.writeCmd.ready) {
          when(addr === (1 << bits_range) - 4) {
            addr := 0
          } otherwise {
            addr := addr + 4
          }
          goto(idle)
        }
      }
  }

  val fsmData = new StateMachine {
    val idle = new State with EntryPoint
    val writing = new State

    idle
      .whenIsActive {
        when(ct.willOverflow) {
          goto(writing)
        }
      }

    writing
      .whenIsActive {
        io.axi.writeData.valid := True
        io.axi.writeData.payload.data := B(data.value)
        when(io.axi.writeData.ready) {
          data.increment()
          goto(idle)
        }
      }
  }

2つの状態マシンで、valid と payload を assert し、ready が来たら valid を de-assert するという、簡単な仕組みです。

なお、デフォルトの信号出力は次のようになっています。

  io.axi.writeCmd.valid := False
  io.axi.writeCmd.payload.addr := U(0, 32 bits)
  io.axi.writeData.valid := False
  io.axi.writeData.last := True
  io.axi.writeData.payload.data := B(0, 32 bits)
  io.axi.writeRsp.ready := True

Briey クラスの修正

続いて、Soc Briey のコードを修正します。diff 形式で示します。

diff --git a/src/main/scala/vexriscv/yokoyama/Briey.scala b/src/main/scala/vexriscv/yokoyama/Briey.scala
index 1176069..caab5a5 100644
--- a/src/main/scala/vexriscv/yokoyama/Briey.scala
+++ b/src/main/scala/vexriscv/yokoyama/Briey.scala
@@ -22,6 +22,7 @@ import spinal.lib.system.debugger.{JtagAxi4SharedDebugger, JtagBridge, SystemDeb
 import spinal.lib.blackbox.lattice.ice40.SB_IO
 import flogics.lib.pwm._
 import flogics.lib.spi._
+import flogics.lib.axi4._
 
 import scala.collection.mutable.ArrayBuffer
 
@@ -373,6 +374,8 @@ class MiniBriey(
       }
     }
 
+    val simpleAxi4Master = new SimpleAxi4Master()
+
 
     val axiCrossbar = Axi4CrossbarFactory()
 
@@ -383,7 +386,8 @@ class MiniBriey(
 
     axiCrossbar.addConnections(
       core.iBus       -> List(ram.io.axi),
-      core.dBus       -> List(ram.io.axi, apbBridge.io.axi)
+      core.dBus       -> List(ram.io.axi, apbBridge.io.axi),
+      simpleAxi4Master.io.axi -> List(ram.io.axi)
     )
 
     axiCrossbar.addPipelining(apbBridge.io.axi)((crossbar,bridge) => {

これまた簡単ですね。new SimpleAxi4Master() で SimpleAxi4Master をインスタンス化し、axiCrossbar.addConnections でクロスバの接続を指定してあげるだけです。詳しくは、こちらを参考にしてください。

なお、そこの説明にある addPipelining() というのが非常に気になったのですが、これはオプション扱いのようで、記述しなくても動作するようです。addPipelining() というのは、既に説明した 3つの Stream にパイプラインを挿入することで、1クロックで通過する論理ゲートの数を減らし、動作周波数 f_max を上げるためのもののようです。これはちょっと難しいので、また機会があったら勉強してみたいと思います。

シミュレーションしてみる

それでは早速、動作確認してみましょう。オリジナルの Briey には Verilator と C++ で書かれたシミュレーション系があるので、それを使います。つまり、上記修正をオリジナルの Briey に適用する訳です。

まずは CPU にプログラムを流さず、TRACE=yes で動かしてみましょう。GTKWave の波形を示します。(クリックすると拡大できます)

いい感じですね。

なお、画面は引用しませんが、この状態(TRACE=no のほうが現実的)で OpenOCD + gdb で接続してみたところ、ちゃんと 0x8000_0f00 からのメモリにデータが書き込まれていることを確認できました。

TinyFPGA BX で動かしてみる

それでは、先ほどの修正を私の「Briey ミニ版」に適用し、TinyFPGA BX で動かしてみましょう。RISC-V コア上で動かすプログラムも修正し、以下のようにしてみました。

つまり、main() 関数中でのループで、0x8000_0f00 の値を読み出して表示している訳です。

動作中の画面を示します。

無事に動いているようですね。

SpinalHDL の Axi4CrossbarFactory を使うと、AXI4 のバスマスタも比較的簡単に設計できることが分かりました。

今日はここまで。

お問い合わせはお気軽に!

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。必ず返信致しますが、時々アドレス誤りと思われる返信エラーがございます。返答が届かない場合、大変お手数ではございますが別のメールアドレスで督促頂けますと幸いです。