[SpinalHDL] 本家 UART コードを読み解く

(更新

Reading SpinalHDL original UART code to get better insight.

いままで、SpinalHDL で自前の UART を書いてきましたが、今日は SpinalHDL 本家の UART コードを読んでみました。どうも私は人様の書いたコードを読むのがあまり好きでないようで、ちょっとモチベーションが湧かなかったのですが、優秀な先達技術者の設計を読むのは大事ですし(「チャンスがあればいつでもコードを盗め」でしたっけ?)、いまならまだ、自分の経験も十分に記憶にあるので、自身にむち打って読んでみた次第です。

本家 UART コードに関する説明は、こちらの Docs » Examples » Intermediates ones » UART にあります。ただし、内容は少し古くなっているようで、最新版のコードは GitHub のこちらを参考にしたほうが良いようです。

設計の特徴

説明とコードをざっと読んで得た印象をまとめます。

  • データ構造、バス構造、コンフィグレーションをうまく階層的に抽象化しており、頭の中が整理された人が書いた設計を思わせること
  • UART 受信回路では、1ビットの信号を何度もサンプリングして、その多数決を取ることでノイズやジッタに強い設計となっていること
  • クロック分周器やカウンタを多く使い、また、tick と呼ぶ、一種のイネーブル論理(信号)を多く利用していること
  • new Area 構文を使って、論理(回路)をエリアに分割して設計していること
  • 各エリアでは論理を blocking 代入的に求めておき、エリアの外でレジスタに結びつけていること
  • SpinalHDL のドキュメントで(まだ)十分な解説がされていない機能(BufferCC, History, MajorityVote など)を多く使い、抽象度の高い設計となっていること
  • Stream, Flow インターフェイスをうまく利用していること
  • state machine(fsm)ライブラリを使っていないこと(おそらく、この UART を設計した当時には、fsm ライブラリがまだなかった)

いろいろと勉強になった一方で、state machine 内部状態中の各種論理は、十分に検討されたというより、試行錯誤的な結果という印象も感じられ、読んでいて少しほっとしました。

少し詳しく

SpinalHDL のサンプルコード解説を読んだだけでは分かりにくそうな部分を少しまとめておきます。

データ構造 UartCtrlGenerics

本家 UART サンプルコードでは、Scala の case class 構文を使ったデータ構造が多く見られますが、特徴的なものの一つが、この UartCtrlGenerics ではないでしょうか。後述する UartCtrl クラスの定義において、メタ情報的なデフォルト定数が、うまくこのデータ構造にまとめられています。一方で、これはデフォルト値に過ぎないものであって、UartCtrl クラスを利用する人が、クラスの実装コードに手を入れることなく、簡単に設計をチューニングできるようになっており、オブジェクト指向設計として秀逸な印象を受けました。

ただし、このデータ構造の内容は設計者が初めから整理して定義したというより、UartCtrl クラスの設計時に必要になった定数をくくり出していった結果のようにも思えます。(また、少しほっとしました。)

UART bus

コードを読んでいくと、Uart() という case class が出てきます。最初に読むとちょっと誤解しそうになるのですが、ここの Uart() とは、UART の機能を設計したものではなく、UART の信号線(バス)を定義するものです。

SpinalHDL では信号線を束ねるために Bundle という基本的なクラスがありますが、さらに IMasterSlave という trait(Java で言うところの interface)があり、Bundle 中の信号線に、マスタ/スレーブという概念を与えてくれます。

…なんのことやら分かりませんので例を挙げます。例えばいま、UART の外部インターフェイスを定義しようとして txd, rxd, rts, cts といった信号線を Uart() というインターフェイスにまとめたとします。それをある回路(Component)の io として利用する場合、ある回路はそのインターフェイスのマスタであるかも知れませんし、一方で別の回路はスレーブとして利用するかも知れません。そこで、次のように定義します。(SpinalHDL のコードから引用)

case class Uart() extends Bundle with IMasterSlave {
  val txd = Bool
  val rxd = Bool
  val cts = Bool
  val rts = Bool

  override def asMaster(): Unit = {
    out(txd)
    in(rxd)
    out(rts)
    in(cts)
  }
}

この Uart() を使う回路では、次のようにして利用します。

class UartCtrl() extends Component {
  val io = new Bundle {
    val uart   = master(Uart())
  }

  // 略

つまり、この UartCtrl クラス(回路)には uart という Uart クラスのインターフェイスがあるが、この回路から見て自分は uart のマスタですよ、つまり txd は出力信号であり、また cts は入力信号として扱いますよ、と明示できる訳ですね。

この IMasterSlave は、バスの定義でも利用されるようです。もう少し詳しい説明はこちらにあります。

BufferCC

この BufferCC も分かりづらいです。UartCtrlRx クラスのコードを読んでいるといきなり登場して面食らいますが、これはもともと、クロックドメインをまたがる信号のためのバッファのようです。説明はこちらにあります。UartCtrlRx では、外部からの入力信号 RxD が同期信号でないため、BufferCC() を介して読み込んでいるようですが、もう少し詳しい説明があると嬉しいところです。

RegNext

これは Reg クラス(レジスタ)のための略記法を提供するクラスのようで、たぶん、RegNext を使わなくても設計はできます。

val reg = Reg(UInt(8 bit))
reg := io.payload

と書く代わりに、

val reg = RegNext(io.payload)

と書けますよ、ということだと思います(すみません、試してません)。説明はこちらにあります。

History, MajorityVote

これらもいきなりコードに出てきて面食らいます。UartCtrlRx クラスにおいて、RxD 信号のサンプリング処理に使われています。つまり、あるタイミング間隔で信号の論理値を読み取りながら、過去の複数サンプル間で多数決処理を使うためのユーティリティクラスです。説明はこちらにあります。

Flow インターフェイスからレジスタに読み出す

サンプルコードの最後のほうに UartCtrlUsageExample というクラスの定義がありますが、中で、UartCtrl の io.read(Flow)からレジスタに値を読み出す、というコードがさらっと書かれています。これは確かに便利な書き方ですね。

  val io = new Bundle{
    val leds = out Bits(8 bits)
  }

  // 略

  //Assign io.led with a register loaded each time a byte is received
  io.leds := uartCtrl.io.read.toReg()

この構文(.toReg())を使うと、非明示的にレジスタが定義され、Flow インターフェイス uartCtrl.io.read の valid 信号に従って payload がレジスタにフェッチされ、そのレジスタの出力は io.leds にアサインされます。

Stream インターフェイスへの出力

これもさらっと書かれています。もう少し説明が欲しいですね。

  //Write the value of switch on the uart each 2000 cycles
  val write = Stream(Bits(8 bits))
  write.valid := CounterFreeRun(2000).willOverflow
  write.payload := io.switchs
  write >-> uartCtrl.io.write

ここでは write という Stream インターフェイスを定義し、その valid とpayload の出力論理を定義したのち、「>->」という奇妙な構文で uartCtrl.io.write(Stream)に繋いでいます。ここの「>->」は、実は先日説明した m2sPipe(レイテンシ 1 で 1段の FIFO )を使っています。つまり、Stream write 後段に m2sPipe() を繋ぎ、さらにその後段を uartCtrl.io.write に繋いでね、ということになります。

ここで FIFO を使う理由はよく分かりません。直接「>>」で繋いではダメなのでしょうか。機会があったら、実験してみたいところです。

最後にシミュレーションコード

この本家 UartCtrl をシミュレーションで動かしてみようと思い、簡単なコードを書いてみました。

import spinal.core._
import spinal.sim._
import spinal.core.sim._
import spinal.lib.master
import spinal.lib.com.uart._

package object mine {
  def my_assert(f: Boolean, msg: String): Unit = {
    assert(
      assertion = f,
      message = msg
    )
  }

  val PRD = BigDecimal(16e6 / 115200)
    .setScale(0, BigDecimal.RoundingMode.HALF_UP)
    .toInt
}

import mine._

class SpinalUart extends Component {
  val io = new Bundle {
    val uart = master(Uart())
    val valid = in Bool
    val payload = in Bits(8 bits)
    val ready = out Bool
  }

  val uartCtrl = new UartCtrl()
  uartCtrl.io.config.setClockDivider(115.2 kHz, 16 MHz)
  uartCtrl.io.config.frame.dataLength := 7 // 8 bits
  uartCtrl.io.config.frame.parity := UartParityType.NONE
  uartCtrl.io.config.frame.stop := UartStopType.ONE
  uartCtrl.io.uart <> io.uart
  uartCtrl.io.write.valid := io.valid
  uartCtrl.io.write.payload := io.payload
  io.ready := uartCtrl.io.write.ready
}

object SpinalUartSim {
  def main(args: Array[String]): Unit = {
    SimConfig.withWave.doSim(
      new SpinalUart(
      )
    ) { dut =>
      def wait(count: Int = 1) {
        dut.clockDomain.waitSampling(count)
      }

      def gen_uart_sig(data: Int, period: Int, assertion: Boolean): Unit = {
        // start-bit
        for (i <- 0 until period) {
          dut.io.uart.rxd #= false
          wait()
        }

        // character bits
        for (bit <- 0 to 7) {
          dut.io.uart.rxd #= (data & (1 << bit)) != 0
          for (i <- 0 until period) {
            wait()
          }
        }

        // stop-bit
        for (i <- 0 until period) {
          dut.io.uart.rxd #= true
          wait()
        }
      }

      dut.clockDomain.forkStimulus(period = 10)

      dut.io.valid #= false
      dut.io.payload #= 0x5a
      dut.io.uart.rxd #= true

      wait(5)
      dut.io.valid #= true

      while (dut.io.ready.toBoolean == false) {
        wait()
      }
      dut.io.valid #= false

      wait(600)

      gen_uart_sig(0x55, PRD, true)
      gen_uart_sig(0xaa, PRD, true)
      gen_uart_sig(0x5a, PRD, true)

      wait(10)
    }
  }
}

シミュレーションの結果を示します。波形のあちこちを詳細にチェックすると、いろいろ勉強になります。

今日はここまで。おつかれさまでした。