• 吃透Chisel语言.30.Chisel进阶之通信状态机(二)——FSMD:以Popcount为例


    Chisel进阶之通信状态机(二)——FSMD:以Popcount为例

    上一篇文章以闪光灯为例,介绍了通信状态机的写法,用于将大的复杂的状态机分解为小的多个相互通信的状态机来实现,可以保证使用资源更少,维护、修改也更容易。不过上一篇文章中的通信状态机之间的通信都是控制信号,还未涉及数据信号。这一篇文章就一起学习带数据通路的状态机,并以Popcount计数器为例进行介绍。

    带数据通路的状态机

    通信状态机的典型例子就是带数据通路的状态机,这种状态机有专门的名字,即FSMD(Finite-State Machine with Datapath,带数据通路的有限状态机)。其中状态机控制数据通路,数据通路执行计算。FSMD的输入有来自环境的输入和来自数据通路的输入,其中来自环境的输入会输入到数据通路,数据通路又生成数据。下图就是一个典型的FSMD:

    在这里插入图片描述

    Popcount的例子

    上图的例子其实就是个计算Popcount的FSMD,这个Popcount也叫Hamming Weight(汉明权重),指的是一个二进制串中1的数量。

    Popcount单元包含数据输入din和结果输出popCount,两个都连接到数据通路。对于输入输出,我们使用ready-valid握手协议。当发送端数据有效的时候,valid信号被设置,当接受端可以接受数据的时候,ready信号被设置。当两个信号都被设置的时候,数据传输就会发生。握手信号连接到FSM上,FSM连接到数据通路上,包括FSM到数据通路的控制信号和数据通路到FSM的状态信号。

    下一步我们就可以设计这个FSM了,首先从状态转换图开始,也就是上面给出的例子图。FSM从Idle状态开始,等待输入。当数据到达的时候,会给出一个valid信号,FSM会进入到Load状态来读取一个移位寄存器。然后FSM进入下一个状态Count,这里二进制串中的1会被顺序计数。这里我们使用一个移位寄存器,一个加法器,一个累加器寄存器,以及一个向下计数器来完成计数。当向下计数器到零的时候,计算就完成了,FSM进入下一个状态Done,此时带valid信号的FSM信号就给出了,FSM信号中包含了将要被使用的popCount值。当收到了接收端的ready信号后,FSM就转移到Idle状态,准备计算下一个popCount

    下面的代码是顶层模块的描述,会对FSM和数据部分进行初始化,并将他们连接起来:

    class PopCount extends Module {
        val io = IO(new Bundle {
            // 输入数据有效
            val dinValid = Input(Bool())
            // 可以接收数据
            val dinReady = Output(Bool())
            // 输入数据
            val din = Input(UInt(8.W))
            // 输出结果有效
            val popCountValid = Output(Bool())
            // 可以输出数据
            val popCountReady = Input(Bool())
            // 输出结果
            val popCount = Output(UInt(4.W))
        })
        
        // fsm部分
        val fsm = Module(new PopCountFSM)
        // 数据通路部分
        val data = Module(new PopCountDataPath)
        // fsm和顶层接口的连接
        fsm.io.dinValid := io.dinValid
        io.dinReady := fsm.io.dinReady
        io.popCountValid := fsm.io.popCountValid
        fsm.io.popCountReady := io.popCountReady
        // 数据通路和顶层接口的连接
        data.io.din := io.din
        io.popCount := data.io.popCount
        // 数据通路和fsm之间的连接
        data.io.load := fsm.io.load
        fsm.io.done := data.io.done
    }
    
    • 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
    • 29
    • 30
    • 31
    • 32

    注释简单地说明了顶层模块代码的含义,这里就不多说了。下面开始看数据通路的构造,下图是数据通路部分的示意图:

    在这里插入图片描述

    数据din首先输入到shf寄存器中。在加载数据的时候,cnt寄存器置零。为了统计1的数量,regData寄存会右移(图片中的shf),最低有效位在每个时钟周期都加到regPopCount上(图片中的cnt)。还有个寄存器图中没有画出来,它执行倒数计数,直到输入中所有的位都以最低有效位的形式移出,计数器为0的时候就表明popCount计算结束了。此时FSM会切换到Done状态,在popCountReady信号被设置时输出结果信号。当结果被读取时,通过设置popCountValid信号输出数据并让FSM切换回Idle状态。下面是数据通路部分的Chisel代码实现:

    class PopCountDataPath extends Module {
        val io = IO(new Bundle {
            val din = Input(UInt(8.W))
            val load = Input(Bool())
            val popCount = Output(UInt(4.W))
            val done = Output(Bool())
        })
        
        val dataReg = RegInit(0.U(8.W))
        val popCountReg = RegInit(0.U(4.W))
        val counterReg = RegInit(0.U(4.W))
        
        dataReg := 0.U ## dataReg(7, 1)
        popCountReg := popCountReg + dataReg(0)
        
        val done = counterReg === 0.U
        when (!done) {
            counterReg := counterReg - 1.U
        }
        
        when (io.load) {
            dataReg := io.din
            popCountReg := 0.U
            counterReg := 8.U
        }
        
        // 调试用
        printf("%b %d\t", dataReg, popCountReg)
        
        io.popCount := popCountReg
        io.done := done
    }
    
    • 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
    • 29
    • 30
    • 31
    • 32

    load信号有效时,regData寄存器会加载输入,regPopCount寄存器会复位到0,计数寄存器regCount会设置为需要被移位的位数。否则,regData寄存右移,被移出的最低有效位会加到regPopCount寄存器上,倒数计数器regCount自减一。当计数器为零时,regPopCount的值就是要计算的popCount

    PopCountFSM有三种状态,从idle开始。当输入数据有效信号dinValid被设置时,FSM会切换到count状态,并等待数据通路完成计算,当popCount有效时,FSM切换到done状态,直到接收到popCntReady信号并传送完数据,再切换为idle状态,等待下一轮计算。FSM部分的Chisel实现如下:

    class PopCountFSM extends Module {
        val io = IO(new Bundle {
            val dinValid = Input(Bool())
            val dinReady = Output(Bool())
            val popCountValid = Output(Bool())
            val popCountReady = Input(Bool())
            val load = Output(Bool())
            val done = Input(Bool())
        })
        
        val idle :: count :: done :: Nil = Enum(3)
        val stateReg = RegInit(idle)
        
        io.load := false.B
        
        io.dinReady := false.B
        io.popCountValid := false.B
        
        switch(stateReg) {
            is(idle) {
                io.dinReady := true.B
                when(io.dinValid) {
                    io.load := true.B
                    stateReg := count
                }
            }
            is(count) {
                when(io.done) {
                    stateReg := done
                }
            }
            is(done) {
                io.popCountValid := true.B
                when(io.popCountReady) {
                    stateReg := idle
                }
            }
        }
        // 调试用
        printf("state: %b\n", stateReg)
    }
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    这一部分的代码和之前状态机的代码类似,就不解释了。下面是测试代码:

    import chisel3._
    import chiseltest._
    import org.scalatest.flatspec.AnyFlatSpec
    
    
    class SimpleTestExpect extends AnyFlatSpec with ChiselScalatestTester {
        "DUT" should "pass" in {
            test(new PopCount) { dut =>
                dut.clock.step()
                dut.io.din.poke("b10010011".U)
                dut.io.dinValid.poke(true.B)
    
                for (a <- 0 until 12) {
                    dut.clock.step()
                }
                dut.io.popCountReady.poke(true.B)
                dut.clock.step()
                dut.clock.step()
                dut.clock.step()
                dut.clock.step()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    输出如下:

           0   0    state:  0
           0   0    state:  0
    10010011   0    state:  1
     1001001   1    state:  1
      100100   2    state:  1
       10010   2    state:  1
        1001   2    state:  1
         100   3    state:  1
          10   3    state:  1
           1   3    state:  1
           0   4    state:  1
           0   4    state: 10
           0   4    state: 10
           0   4    state: 10
           0   4    state:  0
    10010011   0    state:  1
     1001001   1    state:  1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    测试通过。

    结语

    这一篇文章以Popcount为例,介绍了带数据通路的有限状态机FSMD的写法与实现,对于后面写复杂的系统有很关键的指导意义。我们可以注意到,在FSMD的实现中,状态机之间的通信我们使用了Ready-Valid握手协议,这是一种常见的通信接口协议,但每次都这么写显然有点复杂。而Chisel中自带了Ready-Valid相关的函数DecoupledIO,用于对数据信号进行Ready-Valid协议的封装,下一篇文章我们就来学习这个重要又方便的函数。

  • 相关阅读:
    一种高精度紧耦合的双目VI-SLAM算法
    老卫带你学---leetcode刷题(72. 编辑距离)
    The First项目报告:Stargate Finance重塑跨链金融的未来
    【ES6知识】 Reflect 与 Proxy
    如何进行IP清洗
    Java容器详解(浅层)
    【MySQL Router】使用 systemd 管理 MySQL Router
    2、SySeVR环境配置(下)
    法制博览杂志法制博览杂志社法制博览编辑部2022年第24期目录
    LeetCode:两数之和
  • 原文地址:https://blog.csdn.net/weixin_43681766/article/details/126101133