[SpinalHDL] UART を作って FPGA で動かす(その1)

(更新

How software programmers design a UART peripheral on FPGA.

ソフト屋のための SpinalHDL FPGA 設計入門の続編です。前回は SpinalHDL で PWM 回路を設計し、FPGA(TinyFPGA BX)で動かしてみました。今回はもう少し実用性と今後の発展を考えて、UART 送信回路を設計してみることにします。

ちなみに、SpinalHDL にはちゃんとした UART の設計例が載っているのですが、初めから見てしまうと勉強にならないので、ガマンします。

繰り返しになりますが、私の本業は組込ソフトウェアの設計です。拙い設計を示していくことになりますが、「ソフトウェア技術者だって簡単な FPGA 設計くらいできたら嬉しいよね」程度の心づもりで御参考いただければ幸いです。(いつか時間があったら本職のロジック設計屋さんに見て貰い、いろいろアドバイスを頂こうと思っています。)

余談ですが、マイコン上のペリフェラルの内部動作を理解できると、ソフトウェア設計でもいろいろと参考になるのではないか、と思っています。もちろん、いずれ RISC-V の SoC を FPGA で自由に動かせるようになるのが長期的な目標です。

設計する UART の仕様

各社のマイコン製品の UART ペリフェラルを見てみると、次のような、ほとんど使わないような機能がたくさん網羅されています。

  • データビット長: 5〜8ビット(8ビットだけで十分じゃね?)
  • 奇数/偶数パリティ(いまどきパリティなんて要らなくね?)
  • ストップビット長: 1, 1.5, 2ビット(略)

どうせ HDL で設計するのであれば、必要に応じてデータビット長 16ビットだろうがパリティだろうが、いくらでも後から追加できます。汎用 UART を IC にマスクする訳ではないので、「いま必要なものだけ」を設計に盛り込もうと思います。とりあえず、以下のように決めます。

  • データビット長: 8ビット
  • パリティ: なし
  • ストップビット長: 1ビット
  • ビットレート: 115.2kbps

ビットレートくらいは動的に変えられたほうが良さそうですが、まずは 115.2kbps 固定で作ってみます。

まずは Git repo

今回はいきなり Git repo へのリンクを書いてしまいます。SpinalHDL の基本的な使い方(私の悪戦苦闘)はこちらにありますので、御参考ください。

なお、今後少しずつ改良を重ねていきたいと思いますので、コミットのタグに御注意ください。

修正版

blocking assignment と non-blocking assignment あたりの理解を整理したので、修正版を上げておきます。なお、その辺の経緯は 3月10日の投稿に詳しく書いてあります。

初心者が悩んだポイント

前回のホワホワ PWM LED でもいろいろ悩みました、まだまだ SpinalHDL の理解が不十分なため、いろいろ悩みます。

回路の入出力インターフェイス

マイコンなどの UART では、送信したいキャラクタを何かレジスタ(*uart_tx_reg など)に書き込むと、キャラクタがシリアル形式に変換されて出力されます。また、キャラクタの送信中にはビジービット(uart_status->tx_busy など)が立って、プログラムで検出できます。

いきなりこのような UART を設計するのは難しいですし、またテストベンチを書くのも容易でないので、今回は SpinalHDL で説明されている「Valid Ready Payload バス」という簡単なインターフェイスを使うことにします。イメージ的には、こちらの Steam というのが参考になります。

結果として、今回の UartCore の io 定義は次のようになります。

class UartCore(len_data: Int, verbose_delay: Boolean) extends Component {
  val io = new Bundle {
    val valid = in Bool
    val ready = out Bool
    val payload = in Bits (len_data bits)
    val txd = out Bool
  }

必要なレジスタ長を決めるにはどうするの?

UART の送信では、ビットレート生成のためのクロックジェネレータが必要になります。今回で言うと、115.2kbps のクロック回路ですね。このためには、原クロックを分周して得る訳ですが、SpinalHDL(つまり Scala)の構文で、カウンタレジスタに必要なビット幅を計算する方法を見つけるのに苦労しました。

結論としては、次のようなコードでレジスタを定義できます。

  val period_timer = 16 * 1000 * 1000 / 115200
  val ct_timer = Reg(UInt(log2Up(tmp_period) bits))

period_timer の値は 138.88… となり、これは 0〜255 の間に納まるので、結果として、8ビットのカウンタ ct_timer が定義されます。16MHz というのは TinyFPGA BX のクロック入力です。なお、period_timer という定数を定義しないで一行で書くこともできます。ポイントは、log2Up() という関数ですね。これは、SpinalHDL core の Misc.scala で定義されています。

実は最初、こんなふうに書いてました。これがダメな理由は、皆さんでお考え、あるいはお試しください。

val ct_timer = Reg(UInt((16 * 1000 * 1000 / 115200) bits))

状態遷移内で、レジスタ無しで出力ピンに代入する

これは意味が分かりづらいですね。UART 送信回路クラス UartCore には io.txd という出力ピンを設けました。つまり、UART の TxD ですね。UartCore 内部の状態遷移の各状態(出力される Verilog HDL では case 文で実装されます)で io.txd の出力値を決めたいのですが、各状態の中だけで(例えば)io.txd := True のように書くと、SpinalHDL コードのコンパイル時に次のようなエラーが出ます。

[error] LATCH DETECTED from the combinatorial signal (toplevel/io_txd : out Bool),
    defined at

ソフト屋的には、C 言語で言うところの switch (…) default: …文みたいのがあれば良いようになあ、と思ったのですが、ここを読む限り、FSM(あるいは when オブジェクト)に入る前にデフォルト値を代入しておくしかないようです。つまりこんな感じにします。

  io.txd := True // これが必要

  val fsm = new StateMachine {
    val idle = new State with EntryPoint
    val startbit = new State
    val sending = new State
    val stopbit = new State
    (略)

もしかすると、io.txd のための専用レジスタ(内部状態)を 1ビット用意すればもっと簡単に書けるのかも知れませんが、なんだ無駄のような気もします。今後、ロジック設計屋さんに聞いてみたいと思います。

たくさんの冗長なカウンタ定義

今回のコードを見ると、

        ct_timer := ct_timer + 1

        // ...

        when(ct_timer === period_timer - 1) {
          ct_timer := 0
          goto(どこそこ)
        }

みたいな記述がたくさん出てきます。ソフト屋的考えだと、この辺はコンパイラが良きに計らって最適化してくれそうな気がしますが、SpinalHDL が出力する Verilog コードを見ると、それぞれ個別にカウンタの回路が出力されているように見えます。なんだかとてももったいなく見えるのですが、どこか(論理合成時とか)で最適化されたりするのでしょうか。

今度、これを綺麗に書き直して、生成されるロジックセルの数に違いが出てくるか見てみたいと思います。ちなみに、前回の PWM では、このような余計なカウンタ処理を書かないように注意したんですが、今回はわざと冗長に書いてみた次第です。

ディレイを入れてみた

この UartCore を後で FPGA 上で動かすとき、一点だけ問題があります。それは、私の書いたトップレベルの設計がダサいので、UART がキャラクタをフルスピードで送信してしまうのです。ま、それはそれで良いのですが、オシロや相手方の UART 受信回路に入力すると、ストップビットとスタートビットの間の切れ目を認識できなくなってしまうので、間に余計なディレイ(ストップビットの延長)を入れてみることにしました。

あまり綺麗でない quick hack ですが、御容赦ください。(UartCore クラスの verbose_delay: Boolean というのがそれです。)

動かしてみた

まず最初に、UartCoreSim クラスを動かして、Verilator で動作確認してみました。

大丈夫そうだったので、実際に TinyFPGA BX で動かしてみます。

まずはオシロで覗いてみます。大丈夫そうですね。

実際に、FTDI の UART-USB ブリッジを介してターミナルソフトで覗いてみると、こんな感じになります。

ま、今日のところは上出来としておきましょう。 🙂

今後の宿題

今後は、以下のような感じで作業を進めていこうと思っています。