串口
规范
本UART控制器教程基于 此文件 实现。
该实现的特征有:
ClockDivider/Parity/StopBit/DataLength的配置由组件输入设置。
RXD输入通过使用N个样本的采样窗口和多数投票法进行滤波。
该UartCtrl控制端口为:
名称 |
类型 |
描述 |
---|---|---|
config |
UartCtrlConfig |
将所有配置发给控制器 |
write |
Stream[Bits] |
系统向控制器发送传输顺序所用的端口 |
read |
Flow[Bits] |
控制器发送已成功接收帧给系统所用的端口 |
uart |
Uart |
带rxd/txd的Uart接口 |
数据结构
在实现控制器本体之前,我们需要定义一些数据结构。
控制器构造参数
名称 |
类型 |
描述 |
---|---|---|
dataWidthMax |
Int |
使用单个UART帧可以发送的最大数据位数 |
clockDividerWidth |
Int |
时钟分频器的位数 |
preSamplingSize |
Int |
采样窗口开始时要丢弃的样本数 |
samplingSize |
Int |
使用多个窗口中部的样本来获得过滤后的 RXD 值 |
postSamplingSize |
Int |
采样窗口结束时丢弃的样本数 |
为了使实现更容易,我们假设 preSamplingSize + samplingSize + postSamplingSize
始终是 2 的幂次方。这样做可以在一些地方跳过计数器清零操作。
同时,不需要将每个构造参数(泛型)一一添加到 UartCtrl
中,我们可以将它们分组到一个类中,该类将用作 UartCtrl
的单个参数。
case class UartCtrlGenerics(dataWidthMax: Int = 8,
clockDividerWidth: Int = 20, // baudrate = Fclk / rxSamplePerBit / clockDividerWidth
preSamplingSize: Int = 1,
samplingSize: Int = 5,
postSamplingSize: Int = 2) {
val rxSamplePerBit = preSamplingSize + samplingSize + postSamplingSize
assert(isPow2(rxSamplePerBit))
if((samplingSize % 2) == 0)
SpinalWarning(s"It's not nice to have a odd samplingSize value (because of the majority vote)")
}
UART接口
让我们定义一个没有流量控制的UART接口线束。
case class Uart() extends Bundle with IMasterSlave {
val txd = Bool()
val rxd = Bool()
override def asMaster(): Unit = {
out(txd)
in(rxd)
}
}
UART配置枚举
让我们定义奇偶校验和停止位枚举。
object UartParityType extends SpinalEnum(binarySequential) {
val NONE, EVEN, ODD = newElement()
}
object UartStopType extends SpinalEnum(binarySequential) {
val ONE, TWO = newElement()
def toBitCount(that: C): UInt = (that === ONE) ? U"0" | U"1"
}
UartCtrl配置线束
让我们定义一些线束,它们将被用于设置 UartCtrl
的IO单元。
case class UartCtrlFrameConfig(g: UartCtrlGenerics) extends Bundle {
val dataLength = UInt(log2Up(g.dataWidthMax) bits) // Bit count = dataLength + 1
val stop = UartStopType()
val parity = UartParityType()
}
case class UartCtrlConfig(g: UartCtrlGenerics) extends Bundle {
val frame = UartCtrlFrameConfig(g)
val clockDivider = UInt(g.clockDividerWidth bits) // see UartCtrlGenerics.clockDividerWidth for calculation
def setClockDivider(baudrate: Double, clkFrequency: HertzNumber = ClockDomain.current.frequency.getValue): Unit = {
clockDivider := (clkFrequency.toDouble / baudrate / g.rxSamplePerBit).toInt
}
}
实现
在 UartCtrl
中会实例化3个东西 :
一个时钟分频器,以UART RX采样率产生采样脉冲。
一个
UartCtrlTx
组件一个
UartCtrlRx
组件
UARTCtrlTx
该 Component
的接口如下:
名称 |
类型 |
描述 |
---|---|---|
configFrame |
UartCtrlFrameConfig |
包含数据位宽计数和奇偶校验位/停止位配置 |
samplingTick |
Bool |
以每UART波特 |
write |
Stream[Bits] |
系统向控制器发出传输命令的端口 |
txd |
Bool |
UART txd引脚 |
让我们定义用于存储 UartCtrlTx
状态的枚举:
object UartCtrlTxState extends SpinalEnum {
val IDLE, START, DATA, PARITY, STOP = newElement()
}
让我们定义 UartCtrlTx
的框架:
class UartCtrlTx(g : UartCtrlGenerics) extends Component {
import g._
val io = new Bundle {
val configFrame = in(UartCtrlFrameConfig(g))
val samplingTick = in Bool()
val write = slave Stream (Bits(dataWidthMax bits))
val txd = out Bool()
}
// Provide one clockDivider.tick each rxSamplePerBit pulses of io.samplingTick
// Used by the stateMachine as a baud rate time reference
val clockDivider = new Area {
val counter = Reg(UInt(log2Up(rxSamplePerBit) bits)) init(0)
val tick = False
..
}
// Count up each clockDivider.tick, used by the state machine to count up data bits and stop bits
val tickCounter = new Area {
val value = Reg(UInt(Math.max(dataWidthMax, 2) bits))
def reset() = value := 0
..
}
val stateMachine = new Area {
import UartCtrlTxState._
val state = RegInit(IDLE)
val parity = Reg(Bool())
val txd = True
..
switch(state) {
..
}
}
io.txd := RegNext(stateMachine.txd) init(True)
}
完整实现如下:
class UartCtrlTx(g : UartCtrlGenerics) extends Component {
import g._
val io = new Bundle {
val configFrame = in(UartCtrlFrameConfig(g))
val samplingTick = in Bool()
val write = slave Stream Bits(dataWidthMax bits)
val txd = out Bool()
}
// Provide one clockDivider.tick each rxSamplePerBit pulse of io.samplingTick
// Used by the stateMachine as a baudrate time reference
val clockDivider = new Area {
val counter = Reg(UInt(log2Up(rxSamplePerBit) bits)) init 0
val tick = False
when(io.samplingTick) {
counter := counter - 1
tick := counter === 0
}
}
// Count up each clockDivider.tick, used by the state machine to count up data bits and stop bits
val tickCounter = new Area {
val value = Reg(UInt(log2Up(Math.max(dataWidthMax, 2)) bits))
def reset(): Unit = value := 0
when(clockDivider.tick) {
value := value + 1
}
}
val stateMachine = new Area {
import UartCtrlTxState._
val state = RegInit(IDLE)
val parity = Reg(Bool())
val txd = True
when(clockDivider.tick) {
parity := parity ^ txd
}
io.write.ready := False
switch(state) {
is(IDLE) {
when(io.write.valid && clockDivider.tick) {
state := START
}
}
is(START) {
txd := False
when(clockDivider.tick) {
state := DATA
parity := io.configFrame.parity === UartParityType.ODD
tickCounter.reset()
}
}
is(DATA) {
txd := io.write.payload(tickCounter.value)
when(clockDivider.tick) {
when(tickCounter.value === io.configFrame.dataLength) {
io.write.ready := True
tickCounter.reset()
when(io.configFrame.parity === UartParityType.NONE) {
state := STOP
} otherwise {
state := PARITY
}
}
}
}
is(PARITY) {
txd := parity
when(clockDivider.tick) {
state := STOP
tickCounter.reset()
}
}
is(STOP) {
when(clockDivider.tick) {
when(tickCounter.value === UartStopType.toBitCount(io.configFrame.stop)) {
state := io.write.valid ? START | IDLE
}
}
}
}
}
io.txd := RegNext(stateMachine.txd, True)
}
UartCtrlRx
该 Component
的接口如下:
名称 |
类型 |
描述 |
---|---|---|
configFrame |
UartCtrlFrameConfig |
包含数据位宽和奇偶校验/停止位配置 |
samplingTick |
Bool |
以每UART波特 |
read |
Flow[Bits] |
控制器发送已成功接收帧给系统所用的端口 |
rxd |
Bool |
UART rxd 引脚,与当前时钟域不同步 |
让我们定义用于存储 UartCtrlTx
状态的枚举:
object UartCtrlRxState extends SpinalEnum {
val IDLE, START, DATA, PARITY, STOP = newElement()
}
让我们定义 UartCtrlRx 的框架:
class UartCtrlRx(g : UartCtrlGenerics) extends Component {
import g._
val io = new Bundle {
val configFrame = in(UartCtrlFrameConfig(g))
val samplingTick = in Bool()
val read = master Flow (Bits(dataWidthMax bits))
val rxd = in Bool()
}
// Implement the rxd sampling with a majority vote over samplingSize bits
// Provide a new sampler.value each time sampler.tick is high
val sampler = new Area {
val syncroniser = BufferCC(io.rxd)
val samples = History(that=syncroniser,when=io.samplingTick,length=samplingSize)
val value = RegNext(MajorityVote(samples))
val tick = RegNext(io.samplingTick)
}
// Provide a bitTimer.tick each rxSamplePerBit
// reset() can be called to recenter the counter over a start bit.
val bitTimer = new Area {
val counter = Reg(UInt(log2Up(rxSamplePerBit) bits))
def reset() = counter := preSamplingSize + (samplingSize - 1) / 2 - 1)
val tick = False
...
}
// Provide bitCounter.value that count up each bitTimer.tick, Used by the state machine to count data bits and stop bits
// reset() can be called to reset it to zero
val bitCounter = new Area {
val value = Reg(UInt(Math.max(dataWidthMax, 2) bits))
def reset() = value := 0
...
}
val stateMachine = new Area {
import UartCtrlRxState._
val state = RegInit(IDLE)
val parity = Reg(Bool())
val shifter = Reg(io.read.payload)
...
switch(state) {
...
}
}
}
完整实现如下:
class UartCtrlRx(g : UartCtrlGenerics) extends Component {
import g._
val io = new Bundle {
val configFrame = in(UartCtrlFrameConfig(g))
val samplingTick = in Bool()
val read = master Flow Bits(dataWidthMax bits)
val rxd = in Bool()
}
// Implement the rxd sampling with a majority vote over samplingSize bits
// Provide a new sampler.value each time sampler.tick is high
val sampler = new Area {
val synchronizer = BufferCC(io.rxd)
val samples = spinal.lib.History(that=synchronizer, when=io.samplingTick, length=samplingSize)
val value = RegNext(MajorityVote(samples))
val tick = RegNext(io.samplingTick)
}
// Provide a bitTimer.tick each rxSamplePerBit
// reset() can be called to recenter the counter over a start bit.
val bitTimer = new Area {
val counter = Reg(UInt(log2Up(rxSamplePerBit) bits))
def reset(): Unit = counter := preSamplingSize + (samplingSize - 1) / 2 - 1
val tick = False
when(sampler.tick) {
counter := counter - 1
tick := counter === 0
}
}
// Provide bitCounter.value that count up each bitTimer.tick, Used by the state machine to count data bits and stop bits
// reset() can be called to reset it to zero
val bitCounter = new Area {
val value = Reg(UInt(log2Up(Math.max(dataWidthMax, 2)) bits))
def reset(): Unit = value := 0
when(bitTimer.tick) {
value := value + 1
}
}
val stateMachine = new Area {
import UartCtrlRxState._
val state = RegInit(IDLE)
val parity = Reg(Bool())
val shifter = Reg(io.read.payload)
// Parity calculation
when(bitTimer.tick) {
parity := parity ^ sampler.value
}
io.read.valid := False
switch(state) {
is(IDLE) {
when(!sampler.value) {
state := START
bitTimer.reset()
bitCounter.reset()
}
}
is(START) {
when(bitTimer.tick) {
state := DATA
bitCounter.reset()
parity := io.configFrame.parity === UartParityType.ODD
when(sampler.value) {
state := IDLE
}
}
}
is(DATA) {
when(bitTimer.tick) {
shifter(bitCounter.value) := sampler.value
when(bitCounter.value === io.configFrame.dataLength) {
bitCounter.reset()
when(io.configFrame.parity === UartParityType.NONE) {
state := STOP
} otherwise {
state := PARITY
}
}
}
}
is(PARITY) {
when(bitTimer.tick) {
state := STOP
bitCounter.reset()
when(parity =/= sampler.value) {
state := IDLE
}
}
}
is(STOP) {
when(bitTimer.tick) {
when(!sampler.value) {
state := IDLE
}.elsewhen(bitCounter.value === UartStopType.toBitCount(io.configFrame.stop)) {
state := IDLE
io.read.valid := True
}
}
}
}
}
io.read.payload := stateMachine.shifter
}
UartCtrl
让我们编写 UartCtrl
来实例化 UartCtrlRx
和 UartCtrlTx
部分,生成时钟分频器逻辑,并将它们相互连接。
class UartCtrl(g: UartCtrlGenerics=UartCtrlGenerics()) extends Component {
val io = new Bundle {
val config = in(UartCtrlConfig(g))
val write = slave(Stream(Bits(g.dataWidthMax bits)))
val read = master(Flow(Bits(g.dataWidthMax bits)))
val uart = master(Uart())
}
val tx = new UartCtrlTx(g)
val rx = new UartCtrlRx(g)
// Clock divider used by RX and TX
val clockDivider = new Area {
val counter = Reg(UInt(g.clockDividerWidth bits)) init 0
val tick = counter === 0
counter := counter - 1
when(tick) {
counter := io.config.clockDivider
}
}
tx.io.samplingTick := clockDivider.tick
rx.io.samplingTick := clockDivider.tick
tx.io.configFrame := io.config.frame
rx.io.configFrame := io.config.frame
tx.io.write << io.write
rx.io.read >> io.read
io.uart.txd <> tx.io.txd
io.uart.rxd <> rx.io.rxd
}
为了更简单地使用具有固定设置的 UART,我们引入了 UartCtrl
的伴生对象。这使我们能够以不同的参数集实例化UartCtrl组件,提供了额外的实例化方式。这里我们定义了 UartCtrlInitConfig
,用它保存那些无法在运行时配置的组件的设置。请注意,如果需要一个能在运行时进行配置的UART,您仍然可以像所有其他组件一样(通过 val uart = new UartCtrl()
)手动实例化UartCtrl。
case class UartCtrlInitConfig(baudrate: Int = 0,
dataLength: Int = 1,
parity: UartParityType.E = null,
stop: UartStopType.E = null
) {
require(dataLength >= 1)
def initReg(reg : UartCtrlConfig): Unit = {
require(reg.isReg)
if(baudrate != 0) reg.clockDivider init((ClockDomain.current.frequency.getValue / baudrate / reg.g.rxSamplePerBit).toInt-1)
if(dataLength != 1) reg.frame.dataLength init (dataLength - 1)
if(parity != null) reg.frame.parity init parity
if(stop != null) reg.frame.stop init stop
}
}
object UartCtrl {
def apply(config: UartCtrlInitConfig, readonly: Boolean = false): UartCtrl = {
val uartCtrl = new UartCtrl()
uartCtrl.io.config.setClockDivider(config.baudrate)
uartCtrl.io.config.frame.dataLength := config.dataLength - 1
uartCtrl.io.config.frame.parity := config.parity
uartCtrl.io.config.frame.stop := config.stop
if (readonly) {
uartCtrl.io.write.valid := False
uartCtrl.io.write.payload := B(0)
}
uartCtrl
}
}
简单应用
用 115200-N-8-1
的参数综合 UartCtrl
:
val uartCtrl = UartCtrl(
config=UartCtrlInitConfig(
baudrate = 115200,
dataLength = 7,
parity = UartParityType.NONE,
stop = UartStopType.ONE
)
)
如果您仅使用 txd
引脚,请添加:
uartCtrl.io.uart.rxd := True
io.tx := uartCtrl.io.uart.txd
相反,如果您仅使用 rxd
引脚:
val uartCtrl = UartCtrl(
config = UartCtrlInitConfig(
baudrate = 115200,
dataLength = 7,
parity = UartParityType.NONE,
stop = UartStopType.ONE
),
readonly = true
)
带TestBench的例子
下面是一个顶层的示例,它执行以下操作:
实例化
UartCtrl
并将其配置设置为 921600 baud/s,无奇偶校验,1 个停止位。每次从 UART 接收到一个字节时,它都会将其写到 LED 输出上。
把switches输入值以每2000个周期发送到 UART。
case class UartCtrlUsageExample() extends Component {
val io = new Bundle {
val uart = master(Uart())
val switches = in Bits(8 bits)
val leds = out Bits(8 bits)
}
val uartCtrl = new UartCtrl()
// set config manually to show that this is still OK
uartCtrl.io.config.setClockDivider(921600)
uartCtrl.io.config.frame.dataLength := 7 // 8 bits
uartCtrl.io.config.frame.parity := UartParityType.NONE
uartCtrl.io.config.frame.stop := UartStopType.ONE
uartCtrl.io.uart <> io.uart
// Assign io.led with a register loaded each time a byte is received
io.leds := uartCtrl.io.read.toReg()
// Write the value of switch on the uart each 2000 cycles
val write = Stream(Bits(8 bits))
write.valid := CounterFreeRun(2000).willOverflow
write.payload := io.switches
write >-> uartCtrl.io.write
}
object UartCtrlUsageExample extends App {
SpinalConfig(
defaultClockDomainFrequency = FixedFrequency(100 MHz)
).generateVhdl(UartCtrlUsageExample())
}
您可以在 这里 为这个小 UartCtrlUsageExample
获取一个简单的 VHDL 测试文件。
额外奖励:享受 Stream 带来的乐趣
如果您想将从 UART 接收到的数据入队:
val queuedReads = uartCtrl.io.read.toStream.queue(16)
如果要在写接口上添加一个队列并做一些流控制:
val writeCmd = Stream(Bits(8 bits))
val stopIt = Bool()
writeCmd.queue(16).haltWhen(stopIt) >> uartCtrl.io.write
如果您想在发送switches值之前发送 0x55 标头,可以将上例中的写生成器替换为:
val write = Stream(Fragment(Bits(8 bits)))
write.valid := CounterFreeRun(4000).willOverflow
write.fragment := io.switches
write.last := True
write.stage().insertHeader(0x55).toStreamOfFragment >> uartCtrl.io.write