吃透Chisel语言.37.Chisel实战之以FIFO为例(二)——基于FIFO的串口通信:串口发送“Hello World!”

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接口的约定是当readyvalid接口都被设置为有效的时候数据才会传输。

发送端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端接口有:

  1. txd端口,用串行的方式发送数据;
  2. 一个Channel类型的端口channel,发送端可以接受字符进行串行化然后发送;

为了生成正确的时序,我们通过计算一个串行位的时钟周期时间来计算常数BIT_CNT,过程的原理是这样的:

  1. 波特率baudRate就是串口每秒传输的二进制位数,单位为bps(bits per second),频率frequency就是时钟频率,那么frequency/baudRate就是每传输一个比特需要的时钟周期数;
  2. 但是frequency/baudRate可能是个小数,对于时钟周期数而言必须得是整数,而/运算符两边都是整数,结果就是个整数,而且是向下取整的整数,而我们希望结果是四舍五入得到的,因此需要先加上一个baudRate / 2再整除以baudRate
  3. 由于BIT_CNT是用于生成波特率的倒数计数器,因此应该从波特率-1开始,倒数到0

设计中用到了三个寄存器:

  1. shiftReg:用于移位数据的寄存器(即序列化);
  2. cntReg:用于生成正确波特率的倒数计数器;
  3. bitsReg:用于存放还需要被移出的比特位的数量;

没有其他的FSM状态需要额外编码,所有的状态都在这三个寄存器里面了。

计数器cntReg是在持续运行的,倒数到0.U然后到0.U时重置到初始值。所有的动作都是在cntReg值为0.U的时候进行的。因为我们要构建最小的发送端,因此我们就用个移位寄存器来存储数据。因此,channel仅在cntReg0.U且没有需要移出的比特时才设置ready

IO端口txd直接连接到移位寄存器的最低有效位上。

bitsReg =/= 0.U时,即还有未移出的比特,我们右移寄存器并在开头补一个1.U(发送器的空闲电平)。如果没有比特需要移出了,那就检查channel是否包含数据(由valid端口给出),如果有的话,那就构造移位寄存器的值,右边是个前导起始位(0.U),中间是数据,左边是两位停止位(3.U11),共11位,因此bitsReg初始值为11.U

这个很迷你的发送端实现没有额外的缓冲区,而且仅在移位寄存器为空且cntReg0.U的时候才能接受一个字符。仅在cntReg0.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包含一个小型的状态机来指示emptyfull这两种状态。Buffer驱动的握手信号in.readyout.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
}

这个BufferedTxTx前连接了一个单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”的实现

最后就是用BufferedTxRx实现串口通信了,具体实现如下:

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

这一部分没什么好解释的,就是把txtx连接起来,然后Comm生成数据给txtxrx之间进行串口通信。

完整的实现代码如下,注意,其中加入了用于调试和测试的时钟计数器和输出函数:

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开发。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值