概述
最近在學習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關鍵字還是箭頭標識符之類的都不用了,夠簡潔。