• 吃透Chisel语言.36.Chisel实战之以FIFO为例(一)——FIFO Buffer和Bubble FIFO的Chisel实现


    Chisel实战之以FIFO为例(一)——FIFO Buffer

    这一部分,我们将以FIFO以及FIFO的各种变体为例,进行Chisel数字设计的实战。这一部分的实战会以小规模的数字设计为例,比如一个FIFO Buffer,它是大规模数字设计中的常用构件块。再比如串行接口(串口),它本身也可以用到FIFO Buffer。这一篇文章就从最基本的FIFO Buffer开始。

    FIFO Buffer概念

    写端(发送端)和读端(接收端)之间可以通过在它们之间构建个缓冲区(Buffer)来解耦合。最常见的一种Buffer就是先进先出缓冲区(First-In First-Out Buffer,FIFO Buffer)。下图就展示了发送端(Writer)、FIFO Buffer和接收端(Reader)之间的信号连接:

    在这里插入图片描述

    数据由发送端在write有效时通过din放到FIFO里面,而接收端在read有效时通过dout来从FIFO中读取数据。

    FIFO刚开始是空的,由empty信号给出空状态。从空的FIFO中读取数据是未定义的行为。当数据一直在往FIFO里面写但是不被读的话,FIFO就会满,由full信号给出满状态。向一个满的FIFO中写数据通常会被忽略,数据也会被丢弃。换句话来说,empty信号和full信号充当了握手信号的角色。

    可能有这么两种FIFO的实现:

    1. 用片上内存和读写指针实现;
    2. 用寄存器链和小型状态机实现;

    对于小的缓冲区(最多10个元素)而言,由独立的寄存器链接到一起形成的缓冲区的链条组织成的FIFO实现起来更简单,需要的资源也很少。

    单FIFO Buffer的Chisel实现

    我们首先从定义FIFO分别在发送端和接收端侧的IO接口开始,假定数据的尺寸是通过size可配置的。

    首先是发送端侧的接口,规定写数据为din并由write信号使能,信号full指示FIFO的状态,执行发送端的流控制。发送端侧接口代码如下:

    class WriterIO(size: Int) extends Bundle {
        val write = Input(Bool())
        val full = Output(Bool())
        val din = Input(UInt(size.W))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后是接收端侧的接口,规定数据从dout输入并由read信号使能,信号empty指示FIFO的状态,并执行接收端的控制流。接收端侧接口代码如下:

    class ReaderIO(size: Int) extends Bundle {
        val read = Input(Bool())
        val empty = Output(Bool())
        val dout = Output(UInt(size.W))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接下来就可以实现一个简单的单FIFO Buffer了,也就是只能存放一个数据的FIFO缓冲区。这个Buffer有一个WriterIO类型的入队端口enq和一个ReaderIO类型的出队端口deq。Buffer的状态机是一个保存数据的寄存器dataReg和一个简单的FSM状态寄存器stateReg。这个FSM只有两种状态,要么empty要么full。如果Buffer是empty的,给定write信号会向寄存器中写入输入数据并将状态切换为full,如果Buffer是full的,给定read信号会读出寄存器中的数据并将状态切换为empty。实现如下:

    class FIFORegister(size: Int) extends Module {
        val io = IO(new Bundle {
            val enq = new WriterIO(size)
            val deq = new ReaderIO(size)
        })
        
        val empty :: full :: Nil = Enum(2)
        val stateReg = RegInit(empty)
        val dataReg = RegInit(0.U(size.W))
    
        when(stateReg === empty) {
            when(io.enq.write) {
                stateReg := full
                dataReg := io.enq.din
            }
        } .elsewhen(stateReg === full) {
            when(io.deq.read) {
                stateReg := empty
                dataReg := 0.U      // 单纯方便在波形图中看是不是空了
            }
        } .otherwise {
            // 也应该没有“否则”的状态了
        }
    
        io.enq.full := stateReg === full
        io.deq.empty := stateReg === empty
        io.deq.dout := dataReg
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    用单FIFO构建完整的FIFO(Bubble FIFO)

    上面的FIFORegister的缓冲区只能存放一个数据,这一小节我们实现可定义深度的FIFO,即完整的FIFO,将会实现为BubbleFifo类,它的接口和FIFORegister的接口是一样的,但是它有两个参数,一个是用于给定数据宽度的size,另一个是用于给定Buffer深度的depth

    我们可以用depthFIFORegister来构建深度为depth的气泡FIFO Buffer。我们将这些单个的Buffer放到一个Scala的Array里面来创建多Buffer。不过Scala的数组没有硬件意义,只是提供了一个用来引用被创建的Buffer的容器。然后我们可以用一个for循环将这些Buffer链接起来。第一个Buffer的入队端链接到完整的FIFO的入队IO上,同样,最后一个Buffer的出队端也链接到完整的FIFO的出队IO上。实现代码如下:

    class BubbleFifo(size: Int, depth: Int) extends Module {
        val io = IO(new Bundle {
            val enq = new WriterIO(size)
            val deq = new ReaderIO(size)
        })
    
        val buffers = Array.fill(depth) {
            Module(new FIFORegister(size))
        }
    
        for (i <- 0 until depth - 1) {
            buffers(i + 1).io.enq.din := buffers(i).io.deq.dout
            buffers(i + 1).io.enq.write := buffers(i).io.deq.read
            buffers(i).io.deq.read := ~buffers(i + 1).io.enq.full
        }
    
        io.enq <> buffers(0).io.enq
        io.deq <> buffers(depth - 1).io.deq
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这个通过把多个单Buffer串联起来实现FIFO的方法叫做气泡FIFO,即Bubble FIFO,因为数据跟气泡一样穿过队列。

    结语

    这一篇文章介绍了FIFO Buffer的概念,然后用Chisel实现了单Buffer的FIFO,接着又用单Buffer实现了完整的FIFO Buffer,即气泡Buffer。这种方法很简单,在数据率低于时钟频率的时候很好用,比如在作为串口的解耦合缓冲区的时候,下一篇文章就会介绍在串口中使用Bubble FIFO。然而在数据率达到时钟频率的时候,Bubble FIFO就存在两个限制了:一是因为每个Buffer的状态都在emptyfull之间来回切换,这就意味着FIFO存在每个字两时钟周期的吞吐上限;二是数据需要像气泡一样穿过完整的FIFO,从因此从输如到输出的时延至少为Buffer的深度。对于这两个问题,在下下篇文章中也会得到解决。

  • 相关阅读:
    搜维尔科技:Varjo-最自然和最直观的互动
    094:vue+openlayers根据zoom的不同,显示不同的地图
    实现一个自定义的vue脚手架
    Apache Paimon Flink引擎解析
    C语言典范编程
    UE 调整材质UV贴图长宽比例
    会声会影2022智能、快速、简单的视频剪辑软件
    C语言基础 - 文件编程 —— 概述篇
    Godot信号教程(使用C#语言)| 创建自定义信号 | 发出自定义信号 | 使用代码监听信号
    通过SIMULINK实现飞轮储能系统,对风力发电场的功率波动进行补偿,改善故障时的电压跌落并可以抑制母线电流谐波
  • 原文地址:https://blog.csdn.net/weixin_43681766/article/details/126238459