today   2019-03-30

access_time  12 mins

ChiselでBF処理系を作る(4)

UART送受信モジュールの実装

前回はFIFOの実装をした。次はPCとの通信の要であるUart送受信の実装を行う。

graph LR PC -- USB --- USB_Uart USB_Uart --Tx--> UartTxRx UartTxRx--Rx--> USB_Uart UartTxRx -- program/stdin -->FIFO_din FIFO_din -- program --> BF_Processor FIFO_din -- stdin--> BF_Processor BF_Processor -- stdout --> FIFO_dout FIFO_dout -- stdout --> UartTxRx External_SW --program/run -->UntiChatter UntiChatter -->BF_Processor style UartTxRx fill:#f66,stroke:#f33

UART信号

まずは信号についておさらいする。

データ線は普段はHigh固定で、最初のLowをスタートビットと言う通信開始のシグナルとして使う。 その後D0~D7の順でデータを送信し、その後ストップビットと呼ばれるHigh期間を(一般的には)1つ設ける。

設定によってはStopbitが2bitあったり、パリティビットを挿入することもできるが今回は不要なので省略する。

通信周波数としてはbaudrateとして決まっており、一般的には9600~115200あたりがよく使われている。お気づきかもしれないが非常に低速である。

実装

以下のようなコードで実現した。Chiselとしての新規要素はほぼないため、設計方針と合わせて送受について解説する。

まず全体の戦略としてbaudrateがシステム周波数に対して非常に遅いことを利用し、システム周波数を使った回路で送受を行う。 (baudrateに合わせた周波数をPLLなどで合成してもよいが、システムクロックとの間に非同期パスができるためである)

具体的にはチャタリング除去回路のときと同様にカウンタを用意してデータを読み書きするタイミングを制御する。

FPGA内部とのI/Fは、前回作成したFIFOに接続できるように準拠する。

UartTxRx.scala - Github

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
  }
}

送信(Tx)

こちらはかなり単純で、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
}

受信(Rx)

基本的には送信と同じだが、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と全体結線について解説する。

次回 - ChiselでBF処理系を作る(5)