• SpinalWorkshop实验笔记(二)


    概述

    本文涉及Function、Apb3Decoder、Timer、BlackBoxAndClock四个实验。实验地址

    内容

    Function

    本实验的电路分两个阶段:

    • 识别字符串:用从Flow中获得的字符匹配参数字符串
    • 获得数据:匹配成功后,从字符串后面获得一定量的字节构成一个整数输出

    难点在于识别字符串。在前面的prime实验我们知道SpinalHDL的类型不能转换为scala基础类型,所以字符串索引无法使用。这个时候我们就要借鉴前面的思路:逐个比较,结果合并。对于一个SpinalHDL类型的索引,我们不能将其转换为scala基础类型,但我们可以和scala基础类型比较,比较结果就是SpinalHDL类型了。所以我们可以构造一个scala的int类型的表(整数区间)作为中介,判断是否存在表中有一个元素和当前索引相等,同时这个元素用来索引参数字符串得到的字符和当前Flow中得到的字符相等:

      def patternDetector(str : String) = new Area{
        val hit = False
        // TODO
        val cnt = Counter(str.length)
        when (io.cmd.valid) {
          when((0 until str.length).map(x => cnt === x && io.cmd.payload === str(x)).orR) {
            when (cnt.willOverflowIfInc) {
              hit := True
              cnt.clear
            } otherwise {
              cnt.increment
            }
          } otherwise {
            cnt.clear
          }
        }
      }
    

    注意这里不能写成循环形式:

      for (i <- 0 until str.length) {
    	  when (x => cnt === x && io.cmd.payload === str(x)) {
            when (cnt.willOverflowIfInc) {
              hit := True
              cnt.clear
            } otherwise {
              cnt.increment
            }
          } otherwise {
            cnt.clear
          }
      }
    

    虽然表面上是等价的,都是把循环/区间展开,但是在下面这种情况下cnt在循环中会改变,整个意思就变了。而电路中又没有break语句用,这个地方如果是在软件中的话应该是非常明显的bug,但硬件上我就花了很长时间才发现,说明还是经验太少了。

    获得数据阶段就不是很难了:

      def valueLoader(start : Bool,that : Data)= new Area{
        require(widthOf(that) % widthOf(io.cmd.payload) == 0) //You can make the assumption that the 'that' width is alwas an mulitple of 8
        // TODO
        val bytecnt = widthOf(that) / widthOf(io.cmd.payload)
        val hit = Reg(False) setWhen(start)
        val cnt = Counter(bytecnt)
        val data = Reg(Bits(widthOf(that) bits))
        when (hit && io.cmd.valid) {
          data.subdivideIn(bytecnt slices)(cnt) := io.cmd.payload
          hit.clearWhen(cnt.willOverflowIfInc)
          cnt.increment
        }
        that := data
      }
    

    注意data.subdivideIn(bytecnt slices)(cnt) := io.cmd.payload切片后索引的变量cnt必须符合切片的块数,比如切片块数是8,cnt就必须是3位二进制数。奇怪的是,当that的位数为8的时候,bytecnt等于1,声明出的Counter的范围只有一个数:0。相当于是0位二进制数,非常反直觉的定义,一开始我很纠结,后来发现并不影响结果,但还是觉得很别扭。

    Apb3Decoder

    这个实验评测需要用到python,注意用来评测的python库cocotb的版本必须是1.4.0及以下的,不然会有兼容性问题。

    这个实验主要是要明白这个译码器做什么的,其实很简单,就是在一组子设备中找到对应地址区间的子设备,这个子设备的片选信号等于输入设备(父设备)的片选信号,同时父设备的三个接收信号PRDATA、PREADY和PSLERROR需要接收子设备的相应信号;其他设备就直接连:

      //TODO fully asynchronous apb3 decoder
      io.input.PRDATA := io.outputs(0).PRDATA
      io.input.PREADY := io.outputs(0).PREADY
      if (apbConfig.useSlaveError) {
        io.input.PSLVERROR := io.outputs(0).PSLVERROR
      }
      for (i <- 0 until outputsMapping.length) {
        when (outputsMapping(i).hit(io.input.PADDR)) {
          io.outputs(i).PSEL := io.input.PSEL
          io.input.PRDATA := io.outputs(i).PRDATA
          io.input.PREADY := io.outputs(i).PREADY
          if (apbConfig.useSlaveError) {
            io.input.PSLVERROR := io.outputs(i).PSLVERROR
          }
        } otherwise {
          io.outputs(i).PSEL := 0
        }
        io.outputs(i).PENABLE := io.input.PENABLE
        io.outputs(i).PADDR := io.input.PADDR
        io.outputs(i).PWRITE := io.input.PWRITE
        io.outputs(i).PWDATA := io.input.PWDATA
      }
    

    这里需要注意io.input的三个接收信号在使用时不能空置,即使是在整个for循环中一个子设备都没对上,也要强行赋一个值,不然会报latch error。文档里latch error介绍的是组合逻辑回路错误,实际上出现这种错误更多的原因是线路空置。一个类似的错误是寄存器没有初始化,这种错误一般不会像上面那种可以在生成电路时检测出来,而是直接导致逻辑错误。在普通评测时只会输出一个错误的值,而用python评测时会输出高阻的xxx。可能和两者后端的模拟器有关,前者时verilator,后者是icarus verilog。显然是后者更容易检查出错误,不过我觉得SpinalHDL应该出一个寄存器没赋初值就报错的生成选项,从根本上杜绝这种错误。

    Timer

    这个实验有两个重点,一个是BusSlaveFactory对象的使用。我一开始一直不明白这个对象是干嘛用的,毕竟是fpga初学者。目前看来的作用应该是为地址映射提供便利,像前面pwm实验中就有根据地址读写电路内寄存器的需求,用了这个对象映射一个地址就是一句话的事;另一个是一连串信号源的处理,父模块可以将一连串信号都传入子模块由子模块来进行计算和连接:

       def driveFrom(busCtrl : BusSlaveFactory,baseAddress : BigInt)(ticks : Seq[Bool],clears : Seq[Bool]) = new Area {
        //TODO phase 2
        val clear = False
    
        val ticksEnable = busCtrl.createReadAndWrite(Bits(ticks.length bits), baseAddress + 0, 0) init(0)
        val clearsEnable = busCtrl.createReadAndWrite(Bits(clears.length bits), baseAddress + 0, 16) init(0)
        busCtrl.driveAndRead(io.limit, baseAddress + 4)
        clear.setWhen(busCtrl.isWriting(baseAddress + 4))
        busCtrl.read(io.value, baseAddress + 8)
        clear.setWhen(busCtrl.isWriting(baseAddress + 8))
    
        io.tick := (ticksEnable & ticks.asBits).orR
        io.clear := (clearsEnable & clears.asBits).orR | clear
      }
    

    createReadAndWrite创建一个可读可写的寄存器并映射;driveAndRead是映射一个已有的端口,并设置为可读可写;read也是映射一个已有的端口,但设置为只读。isWriting用于捕获对地址的写请求。

    计时器的电路代码非常简单:

      //TODO phase 1
      val v = Counter(width bits)
      when (io.clear) {
        v.clear
      } elsewhen (io.tick && v =/= io.limit) {
        v.increment
      }
      io.full := v === io.limit
      io.value := v
    

    BlackBoxAndClock

    本实验的重点是blackbox的使用,这部分SpinalHDL的文档写得非常详细,所以实际上不难:

      // TODO define Generics
      addGeneric("wordWidth", wordWidth)
      addGeneric("addressWidth", addressWidth)
    
      // TODO define IO
      val io = new Bundle {
        val wr = new Bundle {
          val clk = in Bool
          val en   = in Bool
          val addr = in UInt(addressWidth bit)
          val data = in Bits(wordWidth bit)
        }
        val rd = new Bundle {
          val clk = in Bool
          val en   = in Bool
          val addr = in UInt(addressWidth bit)
          val data = out Bits(wordWidth bit)
        }
      }
    
      // TODO define ClockDomains mappings
      mapClockDomain(writeClock, io.wr.clk)
      mapClockDomain(readClock, io.rd.clk)
    

    基本上就是分三步走:

    1. 定义参数,用addGeneric,指定verilog模块中的parameter
    2. 定义接口,这里的层次结构要和verilog模块里的端口名相符合,比如上面代码里的io.wr.clk对应的就是verilog里的io_wr_clk,如果要违反命名规范需要特殊设置,文档里也有写
    3. 映射时钟,将当前的时钟或者你定义的时钟映射到verilog的时钟端口上

    然后是电路定义,这个虽然不是重点,但是我却栽了很大的跟头,花了很长时间去研究这个时序。之前verilog课的考试也是栽在时序上。这里我根据波形总结了三条定律:

    1. 对于when (cond) {xxx}这样的语句,xxx的触发在cond变为高电平之后的下次时钟上升沿。举例来说,假设第一个上升沿cond变成了高电平,那么xxx的第一次触发在第二个上升沿

    2. 内存的读数据端口会在读地址变化的下次时钟上升沿才产生变化

    3. 在时钟上升沿时,首先会对内存读数据端口取值,接着读数据端口更新,最后寄存器产生变化

    这样我们就可以分析下面的代码了:

      val sumArea = new ClockingArea(sumClock){                                                                              // TODO define the memory read + summing logic
        val sum = Reg(io.sum.value) init(0)
        io.sum.value := sum
    
        val readAddr = Counter(widthOf(io.wr.addr) bits)
        var cntEnable = RegInit(False)
        val sumEnable = RegNext(cntEnable) init(False)
        ram.io.rd.en := cntEnable
        ram.io.rd.addr := readAddr
    
        when (io.sum.start) {
          cntEnable.set
          readAddr.clear
          sum.clearAll
        }
    
        when (cntEnable) {
          readAddr.increment
        }
        when (sumEnable) {
          sum := sum + ram.io.rd.data.asUInt
          cntEnable.clearWhen(readAddr.willOverflowIfInc)
        }
        io.sum.done.clear
        io.sum.done.setWhen(sumEnable.fall(False))
      }
    

    设io.sum.start被触发是第0时钟上升沿,根据定律1,cntEnable被置1和readAddr清零是第1上升沿。则在第2上升沿,readAddr变为1,且sumEnable紧随cntEnable被置1,又根据定律2,这时ram.io.rd.data才是地址为0的值。在第3上升沿,根据定律3,首先取ram.io.rd.data的值加到sum上,然后ram.io.rd.data更新为地址为1的值,最后readAddr变为2。在第4上升沿,同样地址为1的值被加到sum上,再更新内存读端口,再更新计数器,以此类推。

    可以看出,每次加在sum寄存器上的值是当前计数器减2作为地址得到的值,这也是为什么要两个enable的原因。对于结束时的信号,也得仔细分析,设readAddr被加到0xFF的那个上升沿为第0上升沿,这时sum加上了地址为0xFD的内存值,然后端口更新为地址为0xFE的内存值。根据定律1,在第1上升沿,cntEnable清零,同时sum加上地址为0xFE的内存值,端口更新为地址为0xFF的内存值,readAddr更新为0。这时io.sum.done不能置1,因为sum还没加完。在第2上升沿,sumEnable随之清零,同时sum加上地址为0xFF的内存值,端口更新,readAddr已经不会加了,io.sum.done在sumEnable清零的瞬间置1。这里只能用“清零的瞬间”这个条件,因为用高电平还是低电平判断怎么也不合适。

    然后有两个注意的地方:

    • 布尔型寄存器设置初始值要用RegInit或者Reg(Bool) init(),我以前以为Reg(False)就是赋初值为False了,结果并不是,只是说明这个寄存器和False是同类型而已

    • 关于xxx.fall函数,我看SpinalHDL源代码里有两个重载:

        /**
          * Falling edge detection of this with an initial value
          * @example{{{ val res = myBool.fall(False) }}}
          * @param initAt the initial value
          * @return a Bool
          */
        def fall(initAt: Bool): Bool = ! this && RegNext(this).init(initAt)
        /** Falling edge detection */
        def fall(): Bool = ! this && RegNext(this)
      

      实现方式是定义一个寄存器作为当前信号的后继,这样当前信号下降的时候后继寄存器还没下降。我一开始没仔细看,选了后面那个没参数的重载。结果那个后继寄存器没初始化!众所周知,没有初始化会导致一些匪夷所思的结果,我就遇到了诸如when (cond) {do A} otherwise {do B}和when (!cond) {do B} otherwise {do A}结果不一样和计数器只能用Reg不能用Counter之类的诡异问题。最后看了波形才明白,原因是后继寄存器一开始为1,所以只要当前信号是低电平都会返回1。我只想吐槽一句,写第二个重载的人是不是SpinalHDL项目组里的内鬼,这不是纯坑爹吗?事实上,我觉得初始值直接默认为假就可以了,根本就没有初始值为真能返回正确结果的情况。

  • 相关阅读:
    Flume案例
    推荐算法
    Hive安装
    打开相机
    打电话,发短信
    温度传感器,摇一摇
    经度,纬度,获取
    团队站立会议01
    团队项目计划会议
    "群英队"电梯演讲
  • 原文地址:https://www.cnblogs.com/YuanZiming/p/15703269.html
Copyright © 2020-2023  润新知