• SpinalWorkshop实验笔记(一)


    概述

    最近在学习SpinalHDL,在github上看到了SpinalHDL实验,于是试着做了做。虽然这些实验的答案在仓库里给出来了,但我是FPGA初学者,虽然会一点verilog却对各种总线一窍不通,也不了解scala,所以即使要理解这些实验也花费了一番功夫。在这里记录一下我做这些实验的感想。本文涉及Counter、PWM、UART、Prime四个实验。

    内容

    Counter

    Counter实验很简单,只要实现一个计数器,并且不要求时钟信号。所以直接按教程里的图片连线即可:

      val r = Reg(UInt(width bits)) init(0)
      r := io.clear ? U(0) | r + 1
      io.value := r
      io.full := r === (1 << width) - 1
    

    注意的点:

    • 比较SpinalHDL的类型需要使用三等于号
    • 寄存器声明的时候一定要初始化,不初始化测试会出错

    PWM

    难度陡然上升(当然可能是我太菜)。这个实验的关键是理解SpinalHDL的master/slave模型。我们可以把实现了IMasterSlave的端口集合理解为一个两种用途的端口集合。当它在模块里被声明成master时,集合里某些端口是输入端口,另一些是输出,而被声明成slave时,那些master时的输入端口此时变为输出端口,master的输出端口变为输入端口。也就是所有端口的输入输出方向发生了反转。

    由于教程的Component interfaces指定了模块的apb端口集合是slave类型的,所以我们需要APB这个端口集合中哪些端口在被声明成slave时是输入端口,哪些是输出端口。这回我们需要理解这个模块是干啥的,查看波形可以发现这个模块似乎是一个方波发生器,就是比较timer寄存器和dutycycle寄存器的值,前者小于后者输出高电平,否则输出低电平。同时有另外一个需求,就是外面的模块要能读写enable和dutycycle两个寄存器(其实我一开始没看出来,这教程写得不清不楚的)。很容易想出当APB作为slave时,是外界向本模块提供要写入的值,同时读出值。因此,写相关的端口当然是输入,读相关的端口当然是输出。加上一些控制信号PSEL、PENABLE和PADDR也明显是输入进这些模块的,因此就可以写出APB的声明:

    //APB interface definition
    case class Apb(config: ApbConfig) extends Bundle with IMasterSlave {
      //TODO define APB signals
      val PSEL = Bits(config.selWidth bits)
      val PENABLE = Bool()
      val PWRITE = Bool()
      val PADDR = UInt(config.addressWidth bits)
      val PWDATA = Bits(config.dataWidth bits)
      val PRDATA = Bits(config.dataWidth bits)
      val PREADY = Bool()
    
      override def asMaster(): Unit = {
        //TODO define direction of each signal in a master mode
        out(PSEL, PENABLE, PWRITE, PADDR, PWDATA)
        in(PRDATA, PREADY)
    
      }
    }
    

    声明里定义端口的输入和输出是通过重写asMaster方法,而且针对的是master声明的,因此需要将我们刚才的讨论倒过来,就是上面的代码。

    io端口的声明和logic块很简单,和Counter实验差不多:

      val io = new Bundle{
        val apb = slave(Apb(apbConfig)) //TODO
        val pwm = out Bool() //TODO
      }
    
      val logic = new Area {
        //TODO define the PWM logic
        val enable = Reg(False)
        val timer = Reg(UInt(timerWidth bits)) init(0)
        when (enable) {
          timer := timer + 1
        }
        val dutycycle = Reg(UInt(timerWidth bits)) init(0)
        val output = Reg(False)
        output := timer < dutycycle
        io.pwm := output
      }
    

    control块就比较复杂,其负责控制那两个寄存器的读写,所以首先需要根据地址来决定读写那个寄存器,同时写寄存器需要收到控制信号的制约,包括片选信号(PSEL)、使能信号(PENABLE)和写信号(PWRITE)。读寄存器直接读,写寄存器需要控制信号均为真才可以写:

      val control = new Area{
        //TODO define the APB slave logic that will make PWM's registers writable/readable
        val doWrite = io.apb.PSEL(0) && io.apb.PENABLE && io.apb.PWRITE
        io.apb.PRDATA := 0
        io.apb.PREADY := True
        switch(io.apb.PADDR){
          is(0){
            io.apb.PRDATA(0) := logic.enable
            when(doWrite){
              logic.enable := io.apb.PWDATA(0)
            }
          }
          is(4){
            io.apb.PRDATA := logic.dutycycle.asBits.resized
            when(doWrite){
              logic.dutycycle := io.apb.PWDATA.asUInt.resized
            }
          }
        }
      }
    

    UART

    这个实验只看文档也挺懵的,实际上它描述的采样状态机应该是这样:

    • IDLE:什么也不做,直到满足跳转条件

    • START:等待一定的时间,然后跳转

    • DATA:重复8次以下操作(用bitCounter计数):

      • 获取preSamplingSize + samplingSize + postSamplingSize个采样tick里rxd信号的值,取其中出现次数超过一半的电平作为本次采样的输出电平(因为samplingSize大于preSamplingSize + samplingSize + postSamplingSize的一半)
      • 假设当前是第i次操作,那么采样的输出电平放到输出寄存器的第i-1位

      这样重复结束后输出寄存器的值就是采样得到的8位整数

    • STOP:等待preSamplingSize + samplingSize + postSamplingSize个采样tick,回到IDLE状态

    DATA这一步还是比较清晰的,可能实际的电路没有那么稳定,所以才要通过采样一段时间然后用MajorityVote取出现次数超过一半的电平。但是START和STOP为什么要等待这个数量的采样tick我就不明白了,尤其是START的式子preSamplingSize + (samplingSize - 1) / 2 - 1更是奇怪,不知道怎么回事:

      // Statemachine that use all precedent area
      val stateMachine = new StateMachine {
        //TODO state machine
        val value = Reg(io.read.payload)
        io.read.valid := False
        always {
          io.read.payload := value
        }
        val IDLE: State = new State with EntryPoint {
          whenIsActive {
            when (sampler.tick && !sampler.value) {
              bitTimer.recenter := True
              goto(START)
            }
          }
        }
        val START = new State {
          whenIsActive {
            when (bitTimer.tick) {
              bitCounter.clear := True
              goto(DATA)
            }
          }
        }
        val DATA = new State {
          whenIsActive {
            when (bitTimer.tick) {
              value(bitCounter.value) := sampler.value
              when (bitCounter.value === 7) {
                goto(STOP)
              }
            }
          }
        }
        val STOP = new State {
          whenIsActive {
            when (bitTimer.tick) {
              io.read.valid := True
              goto(IDLE)
            }
          }
        }
      }
    

    “等待一定的时间”和“采样一定的时间”这些操作都由bitTimer计算,当recenter信号被触发时,等待的采样tick是preSamplingSize + (samplingSize - 1) / 2 - 1,在START结束后bitTimer里的计数器减到0自然溢出到(1 << width) - 1,由于程序前面规定了preSamplingSize + samplingSize + postSamplingSize必须是2的幂,所以两条式子相等,之后在recenter下一次被触发前计都是按那条式子计算等待时间和采样时间。

    另外是Flow的用法,当数据有效时把valid端口置为真,数据传到payload端口。注意寄存器传到payload端口这句必须放在状态机之外或者状态机内的always块里,否则会报“latch”错误。因为payload端口应该是短时间的信号而不是寄存器,所以不能在“某个状态”内赋值。同时,对于bitCounter和bitTimer内的信号进行的操作也是短时间的,在当前状态内是高电平,出了这个状态就又变回低电平了。

    最后注意状态机,我用的是文档里介绍的style A写法,这种写法需要注意如果是当前声明的状态被后面声明的状态使用(如当前声明的IDLE在后面声明STOP中用到了),那么当前声明的状态就不能用自动类型推断,不然会报“递归定义”的错误。如上面的IDLE就在声明里显式指出了其类型State。

    Prime

    代码很简单,但是新手很容易误解。比如我,一开始看见源程序里给出了基于scala基础类型判断质数的函数apply,就在想能不能将传给我的SpinalHDL类型的数转换成基础类型然后调用apply判断,但找不到转换函数。看了答案才意识到这样肯定是不行的,因为apply不能被编译成电路,怎么能用来判断呢。正确方法是利用apply函数构造一个质数表,然后将传给我的数依次和质数表里的数比较,如果其中一个为真就返回真。这种情况下质数表可以转换成一组常量信号,然后用比较器和参数进行比较最后用一个大或门就能计算结果,可以编译成电路的形式,实际上就类似于教程里给出的Verilog代码:

      def apply(n : UInt) : Bool = {
        //TODO
        return (0 until 1 << widthOf(n)).filter(apply(_)).map(n === _).orR
      }
    

    这里缩成一行,意思是构造整数区间[0, 1 << n的位宽)(注意是左闭右开区间),将整数区间里的所有数用apply检查一下(这里的apply是针对scala基础类型的),选出是质数的,这样就构造出了一个质数表。然后将质数表里的每个数和n比较一下,相等的为真,不等的为假,这样就构造出了一个布尔表,注意这个布尔,指的是SpinalHDL的Bool而不是scala的Boolean了,因为是UInt参与比较,用的也是三等于号。最后将布尔表或一下,如果表里有一个真值,即n和一个质数相等,则返回真。

    另外scala的匿名函数写起来真爽,连lambda关键字还是箭头标识符之类的都不用了,够简洁。

  • 相关阅读:
    点滴积累【JS】---JS小功能(JS实现侧悬浮浮动)
    点滴积累【JS】---JS小功能(JS实现隐藏显示侧边栏,也就是分享栏的隐藏显示)
    点滴积累【JS】---JS小功能(JS实现排序)
    php修改排序,上移下移
    PHP获取上周、本周、上月、本月、本季度、上季度时间方法大全
    php简陋版实现微信公众号主动推送消息
    JQuery处理json与ajax返回JSON实例
    页面滚动动态加载数据,页面下拉自动加载内容 jquery
    CGI/FASTCGI/ISAPI区别
    CodeIgniter类库之Benchmarking Class ,计算代码的执行时间
  • 原文地址:https://www.cnblogs.com/YuanZiming/p/15691951.html
Copyright © 2020-2023  润新知