Chisel实战之以FIFO为例(二)——基于FIFO的串口通信:串口发送“Hello World!”
上一篇文章介绍了FIFO Buffer的概念,然后用Chisel实现了单Buffer的FIFO,接着又用单Buffer实现了完整的FIFO Buffer,即气泡Buffer。这种方法很简单,在数据率低于时钟频率的时候很好用,比如在作为串口的解耦合缓冲区的时候,这一篇文章就会介绍如何基于FIFO实现串口并实现串口发送“Hello World!”。
串口的概念
串行接口即串口(也叫UART,常用的是RS-232协议),是在个人笔记本电脑和FPGA板卡之间通信的最简单的方法。既然是串行接口,数据当然是串行传输的。比如对于一个8位的数据,从最低有效位bit(0)
开始,然后每次传输一个或两个比特。如果没有数据要传输,那么输出就是1。下图展示了用串口传输一个字节数据的时序:
现在我们用模块化的设计我们的UART,每个模块只具备最小的功能。下面分别对发送端(TX),接收端(RX)和缓冲区进行设计,然后展示这些基本组件的用法。
端口设计
首先,我们需要一个接口,即端口的定义。对于UART设计,一般都会使用ready-valid
握手接口,方向是从串口的发送端侧的角度来看的:
class Channel extends Bundle {
val data = Input(Bits(8.W))
val ready = Output(Bool())
val valid = Input(Bool())
}
使用ready-valid
接口的约定是当ready
和valid
接口都被设置为有效的时候数据才会传输。
发送端Tx
设计
下面是基本的发送端的实现。
class Tx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val txd = Output(Bits(1.W))
val channel = new Channel()
})
val BIT_CNT = ((frequency + baudRate / 2) / baudRate - 1).asUInt
val shiftReg = RegInit(0x7ff.U) // 共11位,一个开始位,两个结束位
val cntReg = RegInit(0.U(20.W))
val bitsReg = RegInit(0.U(4.W))
io.channel.ready := (cntReg === 0.U) && (bitsReg === 0.U)
io.txd := shiftReg(0)
when(cntReg === 0.U) {
cntReg := BIT_CNT
when(bitsReg =/= 0.U) {
val shift = shiftReg >> 1
shiftReg := Cat(1.U, shift(9, 0))
bitsReg := bitsReg - 1.U
}.otherwise {
when(io.channel.valid) {
// 一个起始位0和两个结束位11
shiftReg := Cat(Cat(3.U, io.channel.data), 0.U)
bitsReg := 11.U
}.otherwise {
shiftReg := 0x7ff.U
}
}
}.otherwise {
cntReg := cntReg - 1.U
}
}
它的IO端接口有:
txd
端口,用串行的方式发送数据;- 一个
Channel
类型的端口channel
,发送端可以接受字符进行串行化然后发送;
为了生成正确的时序,我们通过计算一个串行位的时钟周期时间来计算常数BIT_CNT
,过程的原理是这样的:
- 波特率
baudRate
就是串口每秒传输的二进制位数,单位为bps(bits per second),频率frequency
就是时钟频率,那么frequency/baudRate
就是每传输一个比特需要的时钟周期数; - 但是
frequency/baudRate
可能是个小数,对于时钟周期数而言必须得是整数,而/
运算符两边都是整数,结果就是个整数,而且是向下取整的整数,而我们希望结果是四舍五入得到的,因此需要先加上一个baudRate / 2
再整除以baudRate
; - 由于
BIT_CNT
是用于生成波特率的倒数计数器,因此应该从波特率-1
开始,倒数到0
;
设计中用到了三个寄存器:
shiftReg
:用于移位数据的寄存器(即序列化);cntReg
:用于生成正确波特率的倒数计数器;bitsReg
:用于存放还需要被移出的比特位的数量;
没有其他的FSM状态需要额外编码,所有的状态都在这三个寄存器里面了。
计数器cntReg
是在持续运行的,倒数到0.U
然后到0.U
时重置到初始值。所有的动作都是在cntReg
值为0.U
的时候进行的。因为我们要构建最小的发送端,因此我们就用个移位寄存器来存储数据。因此,channel
仅在cntReg
为0.U
且没有需要移出的比特时才设置ready
。
IO端口txd
直接连接到移位寄存器的最低有效位上。
当bitsReg =/= 0.U
时,即还有未移出的比特,我们右移寄存器并在开头补一个1.U
(发送器的空闲电平)。如果没有比特需要移出了,那就检查channel
是否包含数据(由valid
端口给出),如果有的话,那就构造移位寄存器的值,右边是个前导起始位(0.U
),中间是数据,左边是两位停止位(3.U
即11
),共11位,因此bitsReg
初始值为11.U
。
这个很迷你的发送端实现没有额外的缓冲区,而且仅在移位寄存器为空且cntReg
为0.U
的时候才能接受一个字符。仅在cntReg
为0.U
时接受新数据意味着,ready
标志也会在移位寄存器中有空间的时候被设置为无效。不过我们不希望把这种复杂性引入发送端,而是想把这件事委托给缓冲区来干。
单字节缓冲区的实现
首先我们需要实现一个单字节缓冲区,这个和Bubble FIFO中的FIFO寄存器是类似的。具体实现如下:
class Buffer extends Module {
val io = IO(new Bundle {
val in = new Channel()
val out = Flipped(new Channel())
})
val empty :: full :: Nil = Enum(2)
val stateReg = RegInit(empty)
val dataReg = RegInit(0.U(8.W))
io.in.ready := stateReg === empty
io.out.valid := stateReg === full
when(stateReg === empty) {
when(io.in.valid) {
dataReg := io.in.data
stateReg := full
}
}.otherwise {
when(io.out.ready) {
stateReg := empty
}
}
io.out.data := dataReg
}
它的输入接口还是Channel
类型的,输出是Channel
接口的翻转。Buffer
包含一个小型的状态机来指示empty
或full
这两种状态。Buffer
驱动的握手信号in.ready
和out.valid
取决于状态寄存的值。
当状态为empty
时,且输入端的数据是valid
的,我们可以寄存数据并将状态切换为full
;如果状态为full
,且下游的接收器是ready
的,那么向下游的数据传输就会发生,状态又会切换回empty
。
基于单字节缓冲区的发送端实现
现在我们就可以基于上面的Buffer
来实现带缓冲区的发送端了,BufferedTx
的实现如下:
class BufferedTx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val txd = Output(Bits(1.W))
val channel = new Channel()
})
val tx = Module(new Tx(frequency, baudRate))
val buf = Module(new Buffer())
buf.io.in <> io.channel
tx.io.channel <> buf.io.out
io.txd <> tx.io.txd
}
这个BufferedTx
是Tx
前连接了一个单Buffer的组合。现在有了缓冲区之后,就解决了之前Tx
只能ready
一个周期的问题了。单字缓冲区到真实FIFO的拓展也可以在不修改发送端或单字节缓冲区的情况下轻松实现。
接收端实现
接下来是接收端(Rx
)的实现,接收端的实现更有技巧性,因为它需要重新构建串行数据的时序。Rx
的Chisel实现如下:
class Rx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val rxd = Input(Bits(1.W))
val channel = Flipped(new Channel())
})
val BIT_CNT = ((frequency + baudRate / 2) / baudRate -1).U
// 接收器在起始位下降沿的1.5个比特时间后开始接收数据
val START_CNT = ((3 * frequency / 2 + baudRate / 2) / baudRate -1).U
// 同步异步的RX数据
// 复位时会复位为1以停止读数据
val rxReg = RegNext(RegNext(io.rxd, 1.U), 1.U)
val shiftReg = RegInit('A'.U(8.W))
val cntReg = RegInit(0.U(20.W))
val bitsReg = RegInit(0.U(4.W))
val valReg = RegInit(false.B)
when(cntReg =/= 0.U) {
cntReg := cntReg - 1.U
}.elsewhen(bitsReg =/= 0.U) {
cntReg := BIT_CNT
shiftReg := Cat(rxReg, shiftReg >> 1)
bitsReg := bitsReg - 1.U
// 移入最后一位时
when(bitsReg === 1.U) {
valReg := true.B
}
}.elsewhen(rxReg === 0.U) {
cntReg := START_CNT
bitsReg := 8.U
}
when(valReg && io.channel.ready) {
valReg := false.B
}
io.channel.data := shiftReg
io.channel.valid := valReg
}
代码中的START_CNT
的意义是,接收器等待起始位的下降沿,而接收器在起始位下降沿的1.5个比特时间后开始接收数据。之后的每个比特时间内,都会将接收到的位移入寄存器。因此代码中有两个等待时间,一个是START_CNT
,一个是BIT_CNT
,不过两个时间用的都是同一个计数器cntReg
。移入全部8个比特之后,valReg
会给出一个有效的字节。
串口通信“Hello World”的实现
最后就是用BufferedTx
和Rx
实现串口通信了,具体实现如下:
class Comm(frequency: Int, baudRate: Int) extends Module {
val tx = Module(new BufferedTx(frequency, baudRate))
val rx = Module(new Rx(frequency, baudRate))
val msg = "Hello World!"
val text = VecInit(msg.map(_.U))
val len = msg.length().U
val cntReg = RegInit(0.U(8.W))
tx.io.channel.data := text(cntReg)
tx.io.channel.valid := cntReg =/= len
rx.io.channel.ready := tx.io.channel.ready
rx.io.rxd := tx.io.txd
when(tx.io.channel.ready && cntReg =/= len) {
cntReg := cntReg + 1.U
}
}
这一部分没什么好解释的,就是把tx
和tx
连接起来,然后Comm
生成数据给tx
,tx
和rx
之间进行串口通信。
完整的实现代码如下,注意,其中加入了用于调试和测试的时钟计数器和输出函数:
import chisel3._
import chisel3.util._
class Channel extends Bundle {
val data = Input(Bits(8.W))
val ready = Output(Bool())
val valid = Input(Bool())
}
class Tx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val txd = Output(Bits(1.W))
val channel = new Channel()
})
val clkReg = RegInit(0.U(16.W))
clkReg := clkReg + 1.U
val BIT_CNT = ((frequency + baudRate / 2) / baudRate - 1).asUInt
val shiftReg = RegInit(0x7ff.U) // 共11位,一个开始位,两个结束位
val cntReg = RegInit(0.U(20.W))
val bitsReg = RegInit(0.U(4.W))
io.channel.ready := (cntReg === 0.U) && (bitsReg === 0.U)
io.txd := shiftReg(0)
when(cntReg === 0.U) {
cntReg := BIT_CNT
when(bitsReg =/= 0.U) {
val shift = shiftReg >> 1
shiftReg := Cat(1.U, shift(9, 0))
bitsReg := bitsReg - 1.U
printf("%d: sending ...%b\n", clkReg, shiftReg(1))
}.otherwise {
when(io.channel.valid) {
// 一个起始位0和两个结束位11
shiftReg := Cat(Cat(3.U, io.channel.data), 0.U)
printf("%d: sending start 0 ...%b\n", clkReg, 0.U)
bitsReg := 11.U
}.otherwise {
shiftReg := 0x7ff.U
}
}
}.otherwise {
cntReg := cntReg - 1.U
}
}
class Buffer extends Module {
val io = IO(new Bundle {
val in = new Channel()
val out = Flipped(new Channel())
})
val empty :: full :: Nil = Enum(2)
val stateReg = RegInit(empty)
val dataReg = RegInit(0.U(8.W))
io.in.ready := stateReg === empty
io.out.valid := stateReg === full
when(stateReg === empty) {
when(io.in.valid) {
dataReg := io.in.data
stateReg := full
}
}.otherwise {
when(io.out.ready) {
stateReg := empty
}
}
io.out.data := dataReg
}
class BufferedTx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val txd = Output(Bits(1.W))
val channel = new Channel()
})
val tx = Module(new Tx(frequency, baudRate))
val buf = Module(new Buffer())
buf.io.in <> io.channel
tx.io.channel <> buf.io.out
io.txd <> tx.io.txd
}
class Rx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val rxd = Input(Bits(1.W))
val channel = Flipped(new Channel())
})
val clkReg = RegInit(0.U(16.W))
clkReg := clkReg + 1.U
val BIT_CNT = ((frequency + baudRate / 2) / baudRate - 1).U
// 接收器在起始位下降沿的1.5个比特时间后开始接收数据
val START_CNT = ((3 * frequency / 2 + baudRate / 2) / baudRate - 1).U
// 同步异步的RX数据
// 复位时会复位为1以停止读数据
val rxReg = RegNext(RegNext(io.rxd, 1.U), 1.U)
val shiftReg = RegInit('A'.U(8.W))
val cntReg = RegInit(0.U(20.W))
val bitsReg = RegInit(0.U(4.W))
val valReg = RegInit(false.B)
when(cntReg =/= 0.U) {
cntReg := cntReg - 1.U
}.elsewhen(bitsReg =/= 0.U) {
cntReg := BIT_CNT
shiftReg := Cat(rxReg, shiftReg >> 1)
bitsReg := bitsReg - 1.U
printf("%d: reading ...%b\n", clkReg, rxReg)
// 移入最后一位时
when(bitsReg === 1.U) {
valReg := true.B
printf("Done reading ...%c\n", Cat(rxReg, shiftReg >> 1))
}
}.elsewhen(rxReg === 0.U) {
cntReg := START_CNT
bitsReg := 8.U
printf("%d: reading start 0 ...%b\n", clkReg, rxReg)
}
when(valReg && io.channel.ready) {
valReg := false.B
}
io.channel.data := shiftReg
io.channel.valid := valReg
}
class Comm(frequency: Int, baudRate: Int) extends Module {
val tx = Module(new BufferedTx(frequency, baudRate))
val rx = Module(new Rx(frequency, baudRate))
val msg = "Hello World!"
val text = VecInit(msg.map(_.U))
val len = msg.length().U
val cntReg = RegInit(0.U(8.W))
tx.io.channel.data := text(cntReg)
tx.io.channel.valid := cntReg =/= len
rx.io.channel.ready := tx.io.channel.ready
rx.io.rxd := tx.io.txd
when(tx.io.channel.ready && cntReg =/= len) {
cntReg := cntReg + 1.U
}
}
测试代码如下:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class CommTest extends AnyFlatSpec with ChiselScalatestTester {
"Comm" should "pass" in {
test(new Comm(2000, 80)) { dut =>
dut.clock.setTimeout(0)
for (i <- 0 until 4000) {
dut.clock.step(1)
}
}
}
}
输出如下:
25: sending start 0 ...0
28: reading start 0 ...0
50: sending ...0
66: reading ...0
75: sending ...0
91: reading ...0
100: sending ...0
116: reading ...0
125: sending ...1
141: reading ...1
150: sending ...0
166: reading ...0
175: sending ...0
191: reading ...0
200: sending ...1
216: reading ...1
225: sending ...0
241: reading ...0
Done reading ...H
250: sending ...1
275: sending ...1
300: sending ...1
325: sending start 0 ...0
328: reading start 0 ...0
350: sending ...1
366: reading ...1
375: sending ...0
391: reading ...0
400: sending ...1
416: reading ...1
425: sending ...0
441: reading ...0
450: sending ...0
466: reading ...0
475: sending ...1
491: reading ...1
500: sending ...1
516: reading ...1
525: sending ...0
541: reading ...0
Done reading ...e
550: sending ...1
575: sending ...1
600: sending ...1
625: sending start 0 ...0
628: reading start 0 ...0
650: sending ...0
666: reading ...0
675: sending ...0
691: reading ...0
700: sending ...1
716: reading ...1
725: sending ...1
741: reading ...1
750: sending ...0
766: reading ...0
775: sending ...1
791: reading ...1
800: sending ...1
816: reading ...1
825: sending ...0
841: reading ...0
Done reading ...l
850: sending ...1
875: sending ...1
900: sending ...1
925: sending start 0 ...0
928: reading start 0 ...0
950: sending ...0
966: reading ...0
975: sending ...0
991: reading ...0
1000: sending ...1
1016: reading ...1
1025: sending ...1
1041: reading ...1
1050: sending ...0
1066: reading ...0
1075: sending ...1
1091: reading ...1
1100: sending ...1
1116: reading ...1
1125: sending ...0
1141: reading ...0
Done reading ...l
1150: sending ...1
1175: sending ...1
1200: sending ...1
1225: sending start 0 ...0
1228: reading start 0 ...0
1250: sending ...1
1266: reading ...1
1275: sending ...1
1291: reading ...1
1300: sending ...1
1316: reading ...1
1325: sending ...1
1341: reading ...1
1350: sending ...0
1366: reading ...0
1375: sending ...1
1391: reading ...1
1400: sending ...1
1416: reading ...1
1425: sending ...0
1441: reading ...0
Done reading ...o
1450: sending ...1
1475: sending ...1
1500: sending ...1
1525: sending start 0 ...0
1528: reading start 0 ...0
1550: sending ...0
1566: reading ...0
1575: sending ...0
1591: reading ...0
1600: sending ...0
1616: reading ...0
1625: sending ...0
1641: reading ...0
1650: sending ...0
1666: reading ...0
1675: sending ...1
1691: reading ...1
1700: sending ...0
1716: reading ...0
1725: sending ...0
1741: reading ...0
Done reading ...
1750: sending ...1
1775: sending ...1
1800: sending ...1
1825: sending start 0 ...0
1828: reading start 0 ...0
1850: sending ...1
1866: reading ...1
1875: sending ...1
1891: reading ...1
1900: sending ...1
1916: reading ...1
1925: sending ...0
1941: reading ...0
1950: sending ...1
1966: reading ...1
1975: sending ...0
1991: reading ...0
2000: sending ...1
2016: reading ...1
2025: sending ...0
2041: reading ...0
Done reading ...W
2050: sending ...1
2075: sending ...1
2100: sending ...1
2125: sending start 0 ...0
2128: reading start 0 ...0
2150: sending ...1
2166: reading ...1
2175: sending ...1
2191: reading ...1
2200: sending ...1
2216: reading ...1
2225: sending ...1
2241: reading ...1
2250: sending ...0
2266: reading ...0
2275: sending ...1
2291: reading ...1
2300: sending ...1
2316: reading ...1
2325: sending ...0
2341: reading ...0
Done reading ...o
2350: sending ...1
2375: sending ...1
2400: sending ...1
2425: sending start 0 ...0
2428: reading start 0 ...0
2450: sending ...0
2466: reading ...0
2475: sending ...1
2491: reading ...1
2500: sending ...0
2516: reading ...0
2525: sending ...0
2541: reading ...0
2550: sending ...1
2566: reading ...1
2575: sending ...1
2591: reading ...1
2600: sending ...1
2616: reading ...1
2625: sending ...0
2641: reading ...0
Done reading ...r
2650: sending ...1
2675: sending ...1
2700: sending ...1
2725: sending start 0 ...0
2728: reading start 0 ...0
2750: sending ...0
2766: reading ...0
2775: sending ...0
2791: reading ...0
2800: sending ...1
2816: reading ...1
2825: sending ...1
2841: reading ...1
2850: sending ...0
2866: reading ...0
2875: sending ...1
2891: reading ...1
2900: sending ...1
2916: reading ...1
2925: sending ...0
2941: reading ...0
Done reading ...l
2950: sending ...1
2975: sending ...1
3000: sending ...1
3025: sending start 0 ...0
3028: reading start 0 ...0
3050: sending ...0
3066: reading ...0
3075: sending ...0
3091: reading ...0
3100: sending ...1
3116: reading ...1
3125: sending ...0
3141: reading ...0
3150: sending ...0
3166: reading ...0
3175: sending ...1
3191: reading ...1
3200: sending ...1
3216: reading ...1
3225: sending ...0
3241: reading ...0
Done reading ...d
3250: sending ...1
3275: sending ...1
3300: sending ...1
3325: sending start 0 ...0
3328: reading start 0 ...0
3350: sending ...1
3366: reading ...1
3375: sending ...0
3391: reading ...0
3400: sending ...0
3416: reading ...0
3425: sending ...0
3441: reading ...0
3450: sending ...0
3466: reading ...0
3475: sending ...1
3491: reading ...1
3500: sending ...0
3516: reading ...0
3525: sending ...0
3541: reading ...0
Done reading ...!
3550: sending ...1
3575: sending ...1
3600: sending ...1
[info] CommTest:
[info] Comm
[info] - should pass
[info] Run completed in 3 seconds, 342 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
测试通过,这里的时序是值得研究一下的,这里就不展开讲了。
结语
这一篇文章首先讲述了串口的概念,对实现串口通信的最小系统做了简单阐述,然后从发送端开始,实现了基于单字节FIFO缓冲区的发送端,接着类似地实现了接收端,最后将二者综合在一起,实现了发送“Hello World!”信息的串口通信demo。可以看到,到这里代码量相对之前来说多了很多,但是因为模块化设计且利用了Chisel的特性,因此条理也很清晰。下一篇文章,我们将继续实现FIFO的几种变体,进一步实践Chisel开发。