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

投稿者: | 2020年2月9日

Implementing AMBA 3 APB (Advanced Peripheral Bus) for the designed UART transmitter.

ソフト屋のための SpinalHDL FPGA 設計入門「UART 設計編」の第3回目です。前回は、UART 送信器の HDL コードをクリーンアップすると同時に、文字列送出のためのロジックを設計しました。さらにレジスタの初期化も盛り込みました。今回は、設計した UART 送信器にバス回路を追加してみましょう。

前回までの流れ

  1. UART を作って FPGA で動かす(その1)
  2. UART を作って FPGA で動かす(その2)

皆様がマイコンの UART を使うときは、UART ペリフェラルのレジスタを操作して利用することと思います。レジスタには送信レジスタですとか、設定レジスタ、ステータスレジスタなどがあると思います。あれはどのように設計すれば良いのでしょうか。

SpinalHDL のドキュメント(この辺)を読んでいると、各種のバス設計を助けるライブラリが用意されていることが分かります。いろいろな種類のバスがあり、また、ソフト屋が普段見聞きしないような用語が並んでいるので少々面食らいます。しかし、こちらの Memory mapped UART という解説では APB3 というバスを使っているようですし、確かに UART はペリフェラルなので、AMBA 3 APB (Advanced Peripheral Bus)  というものを試してみましょう。

SpinalHDL には APB3 バスのライブラリがあるようですが、今回もまた勉強を兼ねて、自分で一から HDL コードを書いてみることにします。そうすることでバスアーキテクチャの理解が深まりますし、SpinalHDL のライブラリがなぜそのように設計されているのか、という洞察も得られるでしょう。

まずは Git repo

今回も最初に Git repo へのリンクを書いてしまいます。

修正版

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

AMBA 3 APB の仕様

AMBA 3 は、Wikipedia によると open standard ということになっていますが、仕様書を見るには ARM のウェブサイトにユーザー登録しないとダウンロードできないようです。ちょっと悔しい(?)ですが、ユーザー登録してダウンロードしてみましょう。Google などで「amba 3 apb」などと検索すると見つかるはずです。

仕様の説明については割愛します。上記仕様書は 34ページと短いものですし、実際には 5ページほど読めば、各信号の役割、レジスタのリード、ライトの仕方は理解できると思います。

早速、SpinalHDL で書いてみましょう。まずはクラス宣言の部分と、io Bundle から行きます。なお、クロック入力とリセット入力は SpinalHDL が自動的に繋いでくれるので省略しています。

import spinal.core._

class UartApb3(
    len_data: Int,
    clock_rate: HertzNumber,
    bit_rate: HertzNumber
) extends Component {
  val io = new Bundle {
    val PADDR = in UInt (32 bits)
    val PSEL = in Bool
    val PENABLE = in Bool
    val PREADY = out Bool
    val PWRITE = in Bool
    val PWDATA = in Bits (32 bits)
    val PRDATA = out Bits (32 bits)
    val PSLVERROR = out Bool
    val txd = out Bool
  }

P で始まる信号名が APB3 のものです。UartApb3 は UartCore のラッパーとして設計するので、UART 出力の txd をインターフェイスに含めています。

次に、バスで UartApb3 にアクセスするレジスタのアドレスを決めましょう。レジスタはとりあえず 2つだけです。(Keep it Simple!)

  • TXD レジスタ: アドレス 0x2000_0000
  • STATUS レジスタ: アドレス 0x2000_0004

データバスは 32ビット用意していますが、UART のキャラクタ長は 8ビットですので、TXD は下位 8ビットだけ使います。STATUS レジスタは、最下位ビット(LSB)だけを用い、これを TX_READY ビットと決めましょう。つまり、UartCore が送信中の間は TX_READY が 0(low)となり、アイドル状態では 1(high)となる訳です。メーカー製マイコンの UART にも、似たようなビットがあるかと思います。

これをコードに書いてみましょう。ここではレジスタのアドレスを定義するだけです。動作については後で記述します。

  /*
   * Register Address Definitions
   */
  val addr_base = 0x20000000
  val addr_txd = addr_base + 0
  val addr_status = addr_base + 4 // bit 0: tx_ready

続いて、UartCore をインスタンス化します。また、UartCore の txd 信号を io.txd に繋いでおきます。

  /*
   * Instantiation of a UART Core
   */
  val uart = new UartCore(
    len_data = 8,
    clock_rate = clock_rate,
    bit_rate = bit_rate
  )
  uart.io.txd <> io.txd

ここからが論理設計です。最初に、UartApb3 がレジスタ書き込みサイクルなのか読み出しサイクルなのかを決めるための論理式(条件式)をまとめておきましょう。今回は本体がシンプルですのであまり恩恵はありませんが、後々、本体の論理回路を記述しやすくなります。

APB3 の仕様書を見ると、レジスタへの書き込みは PSEL & PENABLE & PWRITE のとき(もちろん、クロックに同期します)であり、レジスタからの読み出しは PSEL & PENABLE & (not PWRITE) だということが分かりますので、次のように書いておきます。

  /*
   * Write and Read Conditions
   */
  val write: Bool = io.PSEL && io.PENABLE && io.PWRITE
  val read: Bool = io.PSEL && io.PENABLE && !io.PWRITE

続いて、UartApb3 の出力信号や UartCore への出力信号のデフォルトを記述します。今回のロジックにエラー処理はないので、PSLVERROR は常に False です。

  io.PREADY := False
  io.PRDATA := 0
  io.PSLVERROR := False

  uart.io.valid := False
  uart.io.payload := 0

最後が本体です。一つは、アドレス信号が TXD レジスタを指していて write 条件のときの出力で、もう一つは、アドレス信号が STATUS レジスタを指していて read 条件のときの出力です。

  when(io.PADDR === addr_txd && write) {
    io.PREADY := True
    uart.io.valid := True
    uart.io.payload := io.PWDATA.resized
  }.elsewhen(io.PADDR === addr_status && read) {
    io.PREADY := True
    io.PRDATA := uart.io.ready.asBits(1 bits).resized
  }

.resized というのは、32ビット幅の io.PWDATA の下位 8ビットを取り出して uart.io.payload に繋ぎために記述しています。

さて。こんな簡単な記述でいいのかなあ、と適当に書いてしまいましたが、果たして思った通りの動作になるでしょうか。最終的には、FPGA 上にマイコンを実装して繋いでみないと分からないのですが、とりあえず俺々(オレオレ)テストベンチを書いて波形を確認することにしましょう。

テストベンチを書く

シミュレーション用テストベンチのコードは、こちらの UartApb3Sim を見て頂くことにして、内容を簡単に説明しますと、

  • 最初の 10クロックサイクル目(idx == 10)から、1クロック毎に
    1. PADDR, PSEL, PWRITE, PWDATA を設定する
    2. PENABLE を True にする
    3. PSEL と PENABLE を False にする
  • 同 idx == 20 から、1クロック毎に
    1. PADDR, PSEL, PWRITE を設定する
    2. PENABLE を True にする
    3. PSEL と PENABLE を False にする
  • 同 idx == 1620 から、1クロック毎に
    1. PADDR, PSEL, PWRITE を設定する
    2. PENABLE を True にする
    3. PSEL と PENABLE を False にする

となっています。それぞれ何をしているかというと、

  • 10サイクル目からは、UartApb3 の TXD レジスタに 0x5a(UART 送信回路の送出キャラクタ)を書き込む。
  • 20サイクル目からは、UartApb3 の STATUS レジスタを読み出す。ここでは UartCore はまだ送信中なので、TX_READY は False のはず。
  • 1620サイクル目からは、再び UartApb3 の STATUS レジスタを読み出す。ここでは UartCore は送信完了しているので、TX_READY は True のはず。

ということになります。早速シミュレーションを動かして、GTKwave で波形を覗いてみましょう。sbt で、runMain UartApb3Sim してみましょう。

GTKwave で波形を観察する

まずは、idx == 10 辺りを見てみます。(クリックすると拡大できます)

私は同期回路の信号を読むのがどうも苦手なのですが、SpinalHDL のコードから考えるに、PENABLE が True の状態での rising edge で PREADY が True になるはずですから、これで良いのではないかと思います。(自信なし)

次に、idx == 20 辺りを見てみます。

PREADY が True のところで PRDATA が 0 になってますので、TX_READY = False ということになり、UartCore はまだキャラクタを送信中だということになります。良い感じですね。

次に、キャラクタ送信中の全体を眺めてみます。

ちゃんと、0x5a(LSB ファースト)でキャラクタが送出されていることが分かります。

最後に、idx == 1620 辺りを見てみましょう。

PREADY が True のところで PRDATA が 1 となっており、TX_READY = True ということになります。UartCore はキャラクタを送信済みでアイドル状態だということです。これも良さそうです。

…という感じで見て参りましたが、あくまでも俺々テストベンチですので、最終的には、他の方が設計したマイコンと繋いで評価してみたいと思います。

余談:「レジスタを見ればペリフェラルが完全に分かる」は本当か?(tl; dr)

メーカー製のマイコンでは、ハードウェアマニュアルでペリフェラル(UART など)のレジスタや使い方を説明するだけではなく、多くの場合、それらペリフェラルのドライバソフトウェア(ライブラリ)を提供していると思います。

最近の流れの一つとして、マイコンのペリフェラル(UART など)の動作定義、あるいはユーザーインターフェイスを、ハードウェアマニュアルの記述ではなく、「ドライバソフトウェアの API をもって責任分界点とする」メーカーが増えていることが挙げられます。このため、ハードウェアマニュアルにおいてレジスタや使い方の説明を省略し、ドライバソフトウェアの使用例を示しておしまいにしてしまうメーカーも現れています。

このような潮流に対して、「メーカーがペリフェラルの使い方を説明する際に、レジスタ内容を説明するのは当然である。なぜなら、レジスタを操作すればペリフェラルを完全に制御でき、また、レジスタを見ればペリフェラルの内部状態が完全に分かるのだから」と言う人があります。

私としては、心情的にはハードウェアの詳細をできるだけ示してくれるほうが安心ですし、そのようなメーカーのほうに好感を持てるのは確かですが、「レジスタを見ればペリフェラルの内部状態が完全に分かる」というのは本当なのでしょうか。

今回 UartApb3 の設計で示したように、レジスタというのはあくまでも設計のインターフェイス、つまり、設計者と利用者の間の責任分界点の一つに過ぎない、ということが分かります。レジスタというのはペリフェラルのあらゆる信号を操作するためのものではなく、またペリフェラルの内部状態をくまなく提示しているものではない、ということです。(制御理論では「可制御」「可観測」という言葉が使われますが、その意味からいうと、いくらレジスタアクセスが提供されているからといって、ペリフェラルの内部全てが必ずしも可制御、可観測であるわけではない、ということが言えます。)

ちなみに、マイコンのペリフェラルレジスタで「書けるのに読めない」、「同じアドレスに書くのと読むのでは、働きが全く異なる」ものがあります。私も以前は釈然としなかったものですが、自分で UartApb3 を設計してみて納得しました。レジスタはメモリではないのです。レジスタがどのように振る舞うかは、ペリフェラルの設計次第です。

あらゆる「部品」には設計者と利用者の間のインターフェイスが存在しますが、それがハードウェアインターフェイス(レジスタインターフェイス)であるか、ソフトウェアインターフェイス(API)であるかにおいて、本質的な違いはない、というのが私の考え方です。部品設計が利用者の視点に立っていて、

  • 利用者に必要な制御や観測手段が提供されており、
  • それらが仕様書通りに正しく動作する

のであれば、責任分界点がハードウェアインターフェイスであろうが、ソフトウェアインターフェイスであろうが、どちらも良いのではないか、と思います。ソフトウェアインターフェイスを責任分界点とするメリットは、仮にハードウェアの設計に誤りがあったり、ハードウェアの改版によってハードウェア仕様が変更になったとしても、ドライバソフトウェアを修正すれば済む、ということが挙げられます。一般には後者(ソフトウェアインターフェイスを分界点にする)が、利用者に対する改版のインパクトは少ないものです。

ただし、現在のところ多くのソフトウェアインターフェイス(API)は、利用者の視点に立っていないものであったり、仕様書通りに正しく動作しないものだったりすることが問題かと思います。マイコン等のペリフェラル設計を見ていると、ハードウェア設計者とドライバ設計者が十分にコミュニケーションできていないものが多いと感じます。設計者同士が十分に議論し、利用者の視点に立ったペリフェラル設計が増えることを期待したいところです。

今回、私が FPGA を勉強しながらブログにまとめている背景には、実はそのような期待があるからと言っても過言ではありません。ソフトウェア設計者がペリフェラルハードウェアの動作を理解できるということは、ハードウェア設計とソフトウェア設計の間のギャップを埋め、結果として、利用者の視点に立ったペリフェラル設計に繋がるのではないか、と考えています。

今後の宿題

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

おつかれさまでした。