By adding APB3 and a state machine to my UART, convert character code on TinyFPGA BX.
前回まで、しばらく自作の UART 回路を改良(?)してきましたが、新学期も近いことですし(?)今回は以下を完成させ、自作 UART にいちおうケリをつけようと思います。
- UART 受信回路にも APB3(AMBA 3 APB)を設ける
- シミュレーションコードの掃除
- マイコンを使わずに文字コード変換(小文字 → 大文字)
Git repo
最初に Git repo へのリンクを書いてしまいます。
送信/受信回路のハンドシェイク修正
今回、UART と APB3 バス周りのハンドシェイクを全体的に見直すことにしました。
まず最初に送信側ですが、従来、UART 送信回路(UartCore クラス、あるいは名前変更後の UartTxCore クラス)の valid-ready の ready 信号を持って tx_ready としていたものを、送信回路の内部状態を使う方式に変更しました。その理由ですが、前回に書いたように「valid-ready バスの ready とは、シンク側から、ソース(送信側)が次のデータを送って良い『状態』を伝えているのではない」ということです。変更後は、APB3 バスは UART 送信回路に文字を送り出した段階でバスアクセスのサイクルを完全に終了し、次の文字を送れるかどうかの判断は、送信回路が idle 状態になっているかどうか、で判断するようにします。
コードで言いますと、まずは UartTxCore クラスにおいて
diff --git a/src/main/scala/Uart.scala b/src/main/scala/Uart.scala
index 8f5cb24..479ed1a 100644
--- a/src/main/scala/Uart.scala
+++ b/src/main/scala/Uart.scala
@@ -10,6 +10,7 @@ class UartTxCore(
val valid = in Bool
val ready = out Bool
val payload = in Bits (len_data bits)
+ val tx_ready = out Bool
val txd = out Bool
}
@@ -36,6 +37,7 @@ class UartTxCore(
}
io.ready := False
+ io.tx_ready := False
io.txd := True
val fsm = new StateMachine {
@@ -47,6 +49,7 @@ class UartTxCore(
idle
.whenIsActive {
io.ready := False
+ io.tx_ready := True
io.txd := True
when(io.valid) {
n_bits_sent := 0
のように修正し、さらに UartApb3 クラスでは
}.elsewhen(io.PADDR === addr_status && read) {
io.PREADY := True
- io.PRDATA := uart.io.ready.asBits(1 bits).resized
+ io.PRDATA := (
+ 1 -> rx_ready,
+ 0 -> uart_tx.io.tx_ready,
+ default -> False
+ )
}
}
のように修正しました。(あ、rx_ready については後述します。)
続いて受信側ですが、valid-ready バスにおいて ready 信号を使うことを止めました。つまり、ソース側(UartRxCore)はシンク側(UartApb3)の状態に関わらず、いつでもデータを送信できるようになります。SpinalHDL の用語で言うと、従来 Stream インターフェイスを使っていたものを、Flow インターフェイスに変更した、ということになります。
変更した理由ですが、簡単に言うと「UART では(RTS-CTS を使わない限り)もともと(送信側を待たせるための)ハンドシェイクという概念がないので、シンク側の ready 信号に対処することが現実的に不可能」ということがあります。最初、シンク側から ready 信号が返るまで状態回路で「ready 待ち」を設ける実験もしてみたのですが、結局のところ、ready 待ち状態で新しいデータ(文字)が RxD から届いてしまう(スタートビットが始まってしまう)と、UartRxCore 回路では、それに対する手立てがないのです。そのため、いさぎよく Flow インターフェイスに変更しました。コードで言いますと、UartRxCore の状態回路(FSM)に新しい状態 rx_valid(名前が良くありませんが)を追加し、その状態の 1サイクルでのみ valid をアサートし、次のサイクルですぐに de-assert するようにしました。(つまり、シンク側はいつでもデータを読み取れるものと想定している。)
@@ -156,24 +160,28 @@ class UartRxCore(
data := (data |>> 1) | (io.rxd.asBits << (len_data - 1))
n_bits_received := n_bits_received + 1
when(n_bits_received === len_data - 1) {
- goto(stopbit)
+ goto(rx_valid)
}
}
}
- stopbit
+ rx_valid
.whenIsActive {
io.valid := True
io.payload := data
- when(io.ready) {
+ goto(stopbit)
+ }
+
+ stopbit
+ .whenIsActive {
+ when(ct_full) {
goto(idle)
}
}
ちなみに SpinalHDL の UART サンプルコードをカンニングしてみると、やはりそちらでも Flow インターフェイスを採用していることが分かります。つまり、これは現実的な選択なのだろうと思います。
なおこれに加えて、UartApb3 クラスの status レジスタ(?)には rx_ready という状態ビットを設けることにしました。
+ when(uart_rx.io.valid) {
+ rx_data := uart_rx.io.payload
+ rx_ready := True
+ }
(略)
}.elsewhen(io.PADDR === addr_status && read) {
io.PREADY := True
- io.PRDATA := uart.io.ready.asBits(1 bits).resized
+ io.PRDATA := (
+ 1 -> rx_ready,
+ 0 -> uart_tx.io.tx_ready,
+ default -> False
+ )
}
UART 受信回路の APB3 バス
上記までで、既に UART 受信回路の APB3 バスに関するコードを示してしまった形になりますが、改めて整理しますと、まずは 2つのレジスタ(D-FF セット)を追加します。
+ val rx_data = Reg(Bits(len_data bits)) init (0)
+ val rx_ready = Reg(Bool) init (False)
前者は、UartRxCore からの payload をラッチするレジスタです。後者は、そのレジスタが有効なデータ(文字)を保持していることを示すレジスタです。前述のコードのように、uart_rx.io.valid において rx_data に payload をラッチし、rx_ready を True にしています。APB3 バスに rx_data の読み出し要求が来た場合には、
}.elsewhen(io.PADDR === addr_rxd && read) {
io.PREADY := True
io.PRDATA := rx_data.resized
rx_ready := False
のように、rx_data を渡すと同時に rx_ready を False に戻しています。
余談ですが、新しいデータが UartRxCore から届いた同じサイクルで addr_rxd への読み出しが発生するとレーシングが発生しますが、その時は addr_rxd からは古いデータが読み出され、rx_ready は False となり、1キャラクタの取りこぼしが発生します。今回は FIFO を設けていないので、これで良しとします。FIFO を使う際には、FIFO 中のカウンタ等を使って rx_ready を定義し、取りこぼしが無いようにする必要があるかと思います。
シミュレーションコードの掃除
今まで、ファイル UartSim.scala の中にシミュレーション用コードを書いてましたが、全体を for ループで囲んだ汚らしいものだったので、書き直すことにしました。主眼は 3つあり、
- wait() という関数を定義し、clock 立ち上がりまでの待ち合わせをすることにした。もはや、for ループのカウンタ変数は参照しない。
- できるだけ assert() を使用し、シミュレーション結果の自動的なテストをできるようにする。
- UART 信号の送信や受信を(できるだけ)関数にまとめる。
というものです。変更後のコードは、GitHub のレポジトリで御確認ください。(Scala に詳しければ、もう少し綺麗にできそうですが。)
マイコンを使わずに文字コード変換
ここまで、UART の受信回路や APB3 バスを設計してきましたが、動作確認はあくまでもシミュレーションであり、実際に FPGA で動かしてきませんでした。その理由(言い訳)は、
- FPGA で UART から受信をしても、結果を目視チェックする手段がない(あるいは、LED にモールス信号で出力する!?)
- APB3 バスを FPGA 外部に出しても、テストするのが面倒
だったのですが、うまい方法を見つけました。それは一種のループバックで、外部から UART RxD ピンで受信した文字が ASCII のアルファベット小文字であれば、大文字に変換して TxD ピンに出力する、というものです(小文字でない場合はそのまま出力)。それも、valid-ready バスで直結するのではなく、APB3 バス経由で繋げば、APB3 回路の動作確認もできて、一石二鳥と思いました。
しかし、FPGA 内部にソフトコアプロセッサでもあれば簡単ですが、まだそこまでの技量はありません。また、今回のテーマである「先達さまの既存の設計をできるだけ見ないで、自分で解決する」に反します。そこで、上記の「文字コード変換」回路を状態回路(FSM)として設計することにしました。
各状態ですが、
- WaitingRxReady: rx_ready が True になるまで、APB3 への REG_STATUS 読み出しアクセスを繰り返す(ポーリング)
- ReadingRxData: APB3 から REG_READ でキャラクタを読み出す
- WritingTxData: 小文字大文字変換した結果のキャラクタを、APB3 の REG_WRITE に書き出す
- WaitingTxReady: tx_ready が True に なるまで、APB3 への REG_STATUS 読み出しアクセスを繰り返す(ポーリング)
実際の状態回路は、GitHub 上のコード UartToUpper.scala を御参考ください。
次に、文字コード(ASCII)を変換するための SpinalHDL コードを示します。最初はいろいろとエラーを出しましたが、変数の型に注意をすれば、それほど難しくないようです。
var v: UInt = uart.io.PRDATA.resize(chr_size).asUInt
when(v >= 0x61 && v <= 0x7a) {
data := (v - 0x20).asBits
} otherwise {
data := v.asBits
}
これだけで、比較器(コンパレータ)と演算器(二の補数の加算器かな?)ができてしまうのですから、驚きです。(この辺は、SpinalHDL でなくて Verilog HDL などでも同等だと思います。)
最後に、実際に Yosys で合成し、nextpnr で TinyFPGA BX に書き込んでみました。結果を示すのは難しいのですが、正しく動作していることを確認しました。文字を連続送信で RxD に与えても大丈夫でした。
最後に全体のブロック図をあげ、これで一応完成といたします。
長い間お付き合い頂き、ありがとうございました。
いままでの投稿まとめ
SpinalHDL UART 編の全ての記事は、以下から御覧頂けます。