1. 程式人生 > 其它 >SpinalWorkshop實驗筆記(一)

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關鍵字還是箭頭識別符號之類的都不用了,夠簡潔。