today 2019-03-30
access_time 12 mins
UART送受信モジュールの実装
前回はFIFOの実装をした。次はPCとの通信の要であるUart送受信の実装を行う。
まずは信号についておさらいする。
データ線は普段はHigh固定で、最初のLowをスタートビットと言う通信開始のシグナルとして使う。 その後D0~D7の順でデータを送信し、その後ストップビットと呼ばれるHigh期間を(一般的には)1つ設ける。
設定によってはStopbitが2bitあったり、パリティビットを挿入することもできるが今回は不要なので省略する。
通信周波数としてはbaudrateとして決まっており、一般的には9600~115200あたりがよく使われている。お気づきかもしれないが非常に低速である。
以下のようなコードで実現した。Chiselとしての新規要素はほぼないため、設計方針と合わせて送受について解説する。
まず全体の戦略としてbaudrateがシステム周波数に対して非常に遅いことを利用し、システム周波数を使った回路で送受を行う。 (baudrateに合わせた周波数をPLLなどで合成してもよいが、システムクロックとの間に非同期パスができるためである)
具体的にはチャタリング除去回路のときと同様にカウンタを用意してデータを読み書きするタイミングを制御する。
FPGA内部とのI/Fは、前回作成したFIFOに接続できるように準拠する。
package bf
import chisel3._
import chisel3.util._
import scala.math.{ log10, ceil }
class UartTxRx(freq: Double, baud: Double) extends Module {
val io = IO(new Bundle{
val rx = Input(Bool())
val tx = Output(Bool())
val rxData = Output(UInt(8.W))
val rxReady = Input(Bool())
val rxValid = Output(Bool())
val rxAck = Input(Bool())
val txData = Input(UInt(8.W))
val txReady = Output(Bool())
val txValid = Input(Bool())
val txAck = Output(Bool())
val rxActive = Output(Bool())
val txActive = Output(Bool())
val rxDebugBuf = Output(UInt(8.W))
val txDebugBuf = Output(UInt(8.W))
})
val tx = RegInit(Bool(), true.B)
val rxData = RegInit(UInt(8.W), 0.U)
val rxValid = RegInit(Bool(), false.B)
val txReady = RegInit(Bool(), true.B)
val txAck = RegInit(Bool(), false.B)
io.tx := tx
io.rxData := rxData
io.rxValid := rxValid
io.txReady := txReady
io.txAck := txAck
val log2 = (x: Double) => log10(x)/log10(2.0)
val duration = (freq / baud).toInt
val halfDuration = duration / 2
val durationCounterWidth = ceil(log2(duration)).toInt + 1
val rxActive = RegInit(Bool(), false.B)
io.rxActive := rxActive
val rxTrigger = RegInit(Bool(), false.B)
val rxDurationCounter = RegInit(SInt(durationCounterWidth.W), 0.S)
val rx1 = RegInit(Bool(), true.B)
val rx2 = RegInit(Bool(), true.B)
rx1 := io.rx
rx2 := rx1
when(!rxActive) {
when(rx2 && !rx1) {
rxActive := true.B
rxTrigger := false.B
rxDurationCounter := -halfDuration.S
} .otherwise {
rxActive := false.B
rxTrigger := false.B
}
} .otherwise {
when(rxDurationCounter < duration.S) {
rxDurationCounter := rxDurationCounter + 1.S
rxTrigger := false.B
} .otherwise {
rxDurationCounter := 0.S
rxTrigger := true.B
}
}
val rxBuf = RegInit(UInt(8.W), 0.U)
io.rxDebugBuf := rxBuf
val rxCounter = RegInit(UInt(4.W), 0.U)
when(rxActive) {
when(rxTrigger) {
rxCounter := rxCounter + 1.U
rxBuf := (rxBuf >> 1).asUInt + Mux(io.rx, 0x80.U, 0x0.U)
when(rxCounter > 7.U) {
rxActive := false.B
rxData := rxBuf
rxValid := true.B
}
}
} .otherwise {
rxBuf := 0.U
rxCounter := 0.U
}
when(rxValid & io.rxAck) {
rxData := 0.U
rxValid := false.B
}
val txActive = RegInit(Bool(), false.B)
io.txActive := txActive
val txTrigger = RegInit(Bool(), false.B)
val txDurationCounter = RegInit(SInt(durationCounterWidth.W), 0.S)
val txBuf = RegInit(UInt(8.W), 0.U)
io.txDebugBuf := txBuf
val txCounter = RegInit(UInt(4.W), 0.U)
when(!txActive) {
when (io.txValid) {
txActive := true.B
txBuf := io.txData
txAck := true.B
txDurationCounter := 0.S
} .otherwise {
txAck := false.B
}
} .otherwise {
txAck := false.B
when (txDurationCounter < duration.S) {
txDurationCounter := txDurationCounter + 1.S
txTrigger := false.B
} .otherwise {
txDurationCounter := 0.S
txTrigger := true.B
}
}
when(txActive) {
when(txTrigger) {
when(txCounter === 0.U) {
tx := false.B
txCounter := txCounter + 1.U
} .elsewhen(txCounter < 9.U) {
tx := txBuf(txCounter - 1.U)
txCounter := txCounter + 1.U
} .elsewhen(txCounter < 10.U) {
tx:= true.B
txCounter := txCounter + 1.U
} .otherwise {
txActive := false.B
tx := true.B
txCounter := 0.U
}
}
} .otherwise {
tx := true.B
txCounter := 0.U
}
}
こちらはかなり単純で、FIFOからデータを受信したらカウンタを使った単純なステートマシンを用いてstartbit, d[0] ~ d[8], stopbitを順繰り送信する。
以下はFIFOからのデータの受信を待機し(txActive
がフラグ)、baudrateに一致するようなタイミングでtxTrigger
をアサートする回路である。このbaudrateの周期を作るために幅の大きいtxDurationCounter
を使っている。
val txActive = RegInit(Bool(), false.B)
io.txActive := txActive
val txTrigger = RegInit(Bool(), false.B)
val txDurationCounter = RegInit(SInt(durationCounterWidth.W), 0.S)
val txBuf = RegInit(UInt(8.W), 0.U)
io.txDebugBuf := txBuf
val txCounter = RegInit(UInt(4.W), 0.U)
when(!txActive) {
when (io.txValid) {
txActive := true.B
txBuf := io.txData
txAck := true.B
txDurationCounter := 0.S
} .otherwise {
txAck := false.B
}
} .otherwise {
txAck := false.B
when (txDurationCounter < duration.S) {
txDurationCounter := txDurationCounter + 1.S
txTrigger := false.B
} .otherwise {
txDurationCounter := 0.S
txTrigger := true.B
}
}
こちらはtxActive
かつtxTrigger
のときに送信するデータを制御している。
txCounter
を使って送信データを切り替えているだけのかなりシンプルな回路である。
when(txActive) {
when(txTrigger) {
when(txCounter === 0.U) {
tx := false.B
txCounter := txCounter + 1.U
} .elsewhen(txCounter < 9.U) {
tx := txBuf(txCounter - 1.U)
txCounter := txCounter + 1.U
} .elsewhen(txCounter < 10.U) {
tx:= true.B
txCounter := txCounter + 1.U
} .otherwise {
txActive := false.B
tx := true.B
txCounter := 0.U
}
}
} .otherwise {
tx := true.B
txCounter := 0.U
}
基本的には送信と同じだが、baudrateのTrigger生成のみ差分がある。UARTにはクロックがないため、周波数を一致させるだけだと遷移中に値をキャプチャしてしまう可能性がある。
上図のbad rxTriggerの用にスタートビットを検出した瞬間からbaudrateに応じた周期を生成すると遷移した瞬間をキャプチャしてしまう。 そこでカウンタの値を符号付きにし、初回だけ半周期分引いた値を設定することで位相を遅らせ、データの中央でキャプチャすることができる。
以下のコードはほとんどがTxと同様だが、rxDurationCounter := -halfDuration.S
されているところが特徴
val rxActive = RegInit(Bool(), false.B)
io.rxActive := rxActive
val rxTrigger = RegInit(Bool(), false.B)
val rxDurationCounter = RegInit(SInt(durationCounterWidth.W), 0.S)
val rx1 = RegInit(Bool(), true.B)
val rx2 = RegInit(Bool(), true.B)
rx1 := io.rx
rx2 := rx1
when(!rxActive) {
when(rx2 && !rx1) {
rxActive := true.B
rxTrigger := false.B
rxDurationCounter := -halfDuration.S
} .otherwise {
rxActive := false.B
rxTrigger := false.B
}
} .otherwise {
when(rxDurationCounter < duration.S) {
rxDurationCounter := rxDurationCounter + 1.S
rxTrigger := false.B
} .otherwise {
rxDurationCounter := 0.S
rxTrigger := true.B
}
}
あとは生成されたTriggerでデータをキャプチャし、完成されたデータをFIFOに送信している。
val rxBuf = RegInit(UInt(8.W), 0.U)
io.rxDebugBuf := rxBuf
val rxCounter = RegInit(UInt(4.W), 0.U)
when(rxActive) {
when(rxTrigger) {
rxCounter := rxCounter + 1.U
rxBuf := (rxBuf >> 1).asUInt + Mux(io.rx, 0x80.U, 0x0.U)
when(rxCounter > 7.U) {
rxActive := false.B
rxData := rxBuf
rxValid := true.B
}
}
} .otherwise {
rxBuf := 0.U
rxCounter := 0.U
}
when(rxValid & io.rxAck) {
rxData := 0.U
rxValid := false.B
}
今回はUART送受信について解説した。HDLの種類はともあれ、どのような方式で実装するかイメージできるかが重要な気がする。自分ももっと高度な回路を設計する能力を身につける必要がある。
次回はBF Processorと全体結線について解説する。