SpinalHDL上板過程記錄

YuanZiming發表於2024-06-30

背景

最近幫老師做一個硬體專案,使用SpinalHDL實現。實際用起來還是覺得這玩意不錯,它能夠抽象到“生成Verilog程式碼”這一層面,透過程式簡化生成的邏輯,可以減少很多直接用Verilog需要編寫的重複程式碼。同時它宣告的埠名稱和硬體邏輯是能夠直接對應到Verilog程式碼的,所以檢視波形除錯也比較方便,這就比Chisel乃至HLS有優勢了。不過Scala我用起來還是不太習慣,主要是它同時包含了函式式和麵向物件的大量特性,很多概念我也不是很熟悉,只能勉強著用。

當然,SpinalHDL有個缺點,那就是資料和教程太少,中文和英文的都一樣,所以剛開始做專案的時候老師也在擔心SpinalHDL開發會不會遇到什麼問題。好在最後在FPGA開發板上順利跑通了。硬體程式碼本身的開發沒什麼好說的,主要是模擬跑通後到在真正硬體上跑通頗費了一番周折。所以這裡記錄一下SpinalHDL上板過程遇到的幾個問題,也算是拋磚引玉,希望大佬們補充一下SpinalHDL資料和教程在這方面的空白吧。

片上儲存

先說一下SpinalHDL的片上儲存模組,也就是最基本的Mem。假設我需要一個寬度為64bits,深度為65536的片上儲存,SpinalHDL裡是這樣寫的:

val ram = Mem(Bits(64 bits), 1 << 16)

在Verilog裡,它會生成一個暫存器陣列:

reg [63:0] ram[0:65535];

正常情況下,Vivado能夠自動將這個暫存器陣列例化為BRAM,當然,這是有前提的,就是這個暫存器陣列的讀寫邏輯符合BRAM的埠配置,即最多為兩個讀寫埠,否則綜合的時候就會報錯。那麼SpinalHDL裡如何指定埠數目呢?其實就是看程式裡執行了多少條writereadAsyncreadSyncreadWriteSync方法,一條對應一個埠。前面三個方法對應的是隻讀/只寫埠,最後一個對應讀寫埠。

注意只讀和只寫邏輯不會自動合併為讀寫埠,假設一個模組呼叫了writereadSyncreadWriteSync三個方法,那麼將會生成一個只寫埠,一個只讀埠和一個讀寫埠,將無法透過Vivado的綜合流程。因此,如果模組中同時包含和讀資料相關的訊號和寫資料相關的訊號,又希望這兩條訊號共用一個讀寫埠,則只能用readWriteSync方法實現,然後透過多路選擇器和讀、寫使能訊號控制讀和寫的選擇。

AxiLite

我們做的專案用的是Zynq系列的開發板。前面的文章也說過,這種開發板上執行的硬體程式主要透過AxiLite匯流排接收控制資訊。AxiLite在SpinalHDL裡有支援:

val io = new Bundle {
  val axilite = slave(AxiLite4(addressWidth=6, dataWidth=32))
}
val registers = new AxiLite4SlaveFactory(io.instruction)

即可宣告一個控制最多64個32位暫存器的AxiLite匯流排,之後就可以透過BasSlaveFactory提供的相關方法繫結和控制這些暫存器了,似乎也可以透過RegInterface實現類似的工作,不過我沒有嘗試。

模擬的話就使用AxiLite4Driver即可:

val axilite = AxiLite4Driver(dut.io.axilite, dut.clockDomain)
axilite.write(0x4, BigInt("2333", 16))
println(axilite.read(0x4).toInt)

AxiLite4Driver文件裡好像沒有介紹用法,因此需要自己查閱原始碼

直接把SpinalHDL生成的Verilog放進Vivado裡,生成的IP核AxiLite相關的埠都是分散的,在Block Design的時候不會被正確連線,因此還需要使用Vivado的打包功能Create and Package New IP,然後在Ports and Interfaces裡建立Axi對映,將Verilog裡Axilite對應埠和Vivado裡的Axilite訊號對映起來,之後在Addressing and Memory裡檢視剛對映的埠地址有沒有被自動配置上,注意自動配置經常會沒有更新,這時得多重啟幾次專案,確保自動配置完成才行。打包完的IP核就可以在Block Design裡順利以Axilite的方式連線了。

另一個需要密切注意的地方就是SpinalHDL的清零埠是高電平清零(reset),這個和Vivado裡其他IP核常見的低電平清零(resetn)不一樣。因此在使用Block Design的清零訊號源的時候,注意是將名字帶有reset的訊號連到我們IP核的清零端,而不是名字帶有resetn的。這一點坑了我非常久,因為清零訊號始終為高電平導致IP一直清零,無法正常工作,也不能接收任何外界訊號,讓我非常的迷惑。

Axi

大量資料的傳輸,也就是和DRAM的互動需要使用Axi匯流排。SpinalHDL透過Axi4介面類支援Axi協議,其中包含Axi4arAxi4rAxi4w等多個通道,分別對應Axi的讀地址傳輸、讀資料傳輸、寫資料傳輸等。由於Axi的Burst傳輸邏輯比較複雜,所以可以用一些別人寫好的類,如DmaUnit等(其實我覺得SpinalHDL官方應該提供一些開箱即用的模組,像這個DmaUnit裡也存在一些邏輯上的問題,用的時候總覺得膽戰心驚的)。另外,注意Axi埠是master,和Axilite埠是slave不一樣。

模擬則有官方模組,即AxiMemorySim

val dramSim = AxiMemorySim(dut.io.axi, dut.clockDomain, new AxiMemorySimConfig)
dramSim.memory.loadBinary(0x0, "dram32.bin")
dramSim.start()
// ...
dramSim.memory.saveBinary(0x0, 4096, "output.bin")

Axilite4Driver一樣,沒有官方文件,需要查閱原始碼才知道用法。

上板的時候和Axilite一樣,需要在Vivado裡打包IP功能裡建立新的Axi對映,對映對應埠和訊號,才能在Block Design裡順利連線。

關於Axi,需要注意的一點是SpinalHDL預設的是Axi4協議,而部分FPGA的PS核只支援最高Axi3協議,這個可以雙擊PS核檢視。這兩個協議差別不是很大,只有兩點比較重要:一個是兩者支援的最高Burst Size不一樣,Axi3的比較小,最高為16,所以如果要在這樣的FPGA上執行需要將硬體模組的Burst Size也設定成16;另一個是兩者的Burst Cache含義不同,因此如果你不是非常熟悉Burst Cache的話,保險起見,直接將Axi讀地址和寫地址通道的cache埠置0或者直接在Axi的構造引數配置類Axi4Config裡將useCache設定為0。我就被後者坑了很久,cache埠設定錯了,結果讀資料和寫資料返回的resp全部為3(0為正常),我看了網上的資料還以為是no slave,查了半天連線和地址對映,一直沒想到是快取的問題。硬體博大精深,像我這種小白出了錯只能盲試然後把希望交給運氣😥,所以才希望將經驗留給後來者,貫徹人人為我為我人人的雷鋒精神😎。

總結

這次感覺SpinalHDL確實是蠻好用的,而且也證明了使用SpinalHDL設計硬體原型並在FPGA上跑通的可行性,至少在體系結構的科研方面確實是一個好用的工具。希望我的這篇文章能幫大家解決一些類似的問題,大家也來使用SpinalHDL吧!

相關文章