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

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

在这里插入图片描述

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

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

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

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

输出如下:

       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

测试通过。

结语

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

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值