問題:
隨著NI的FPGA產品的廣泛使用,很多同事和客戶都碰到了一些FPGA編程時遇到的問題。由于FPGA不能實時調試,每次修改一點代碼之后都要編譯很長時間之后才能看到修改的效果,所以,我們希望盡量在FPGA編寫代碼時就將更多的問題考慮到位。本文針對項目過程中碰到的一些實際問題進行闡述,希望可以為大家在FPGA編程過程中提供一些幫助。
項目描述:該項目是一個實時頻譜監測、流盤以及跳頻信號檢測的需求。具體參數是IQ速率為100MHz,流盤5分鐘(要做類似Reference Trigger的效果,即按下按鈕之前的一分鐘和按下按鈕之后的四分鐘信號一起流盤),檢測跳頻信號的時間點和相應的頻點,其中跳頻信號的參數是:突發性,每次持續1ms~20ms,跳頻信號在每個頻點上的持續時間是1us~20us,跳頻頻率70000/s,每個頻點上的信號帶寬是5MHz;使用的硬件是5792+7966.其中與FPGA相關的部分就是數據采集和跳頻信號的檢測。對于數據采集部分,5792有專門的采集范例可以供大家參考,而跳頻信號的檢測算法是將數據每隔128個點做一次FFT(100M的采樣率,對于1us的跳頻信號持續時間,對應為100個時域采樣點)。做出的FFT結果如果超過閾值,則將FFT結果的序號回傳給上位機進行保存。
解答:
一、DMA傳輸的速率
對于PXIe-7966R,官網上標定的DMA的傳輸速率為800MB/s,理論上可以完全滿足項目中的400MB/s的傳輸速率要求。但實際測試過程中,傳輸的速率接近但總是達不到400MB,這直接影響了信號的實時采集和流盤。經了解,FPGA的DMA BenchMark與FIFO的數據位寬、總線帶寬以及DMA控制器的速率都有一定關系。見下圖:
注:上圖中PXIe系統下,FPGA的時鐘使用的是200MHz的時鐘。而PXI系統下,FPGA的時鐘使用的是160M (U8 和U16),133M(U32和U64)。
注:上圖中使用的機箱是PXIe-1075,使用的控制器是PXIe-8130.
通過觀察上述PXIe和PXI板卡的速率統計,可以看到:
對于PXI板卡,在 U8場景下,FPGA每秒鐘產生160MB的數據,U16場景下FPGA每秒產生320MB的數據。眾所周知,PCI總線的傳輸帶寬為133MB/s,那為什么上圖中U8情況下的速率才33MB/s,U16情況下的速率才66MB/s呢?這是因為PCI總線的傳輸模式是并行傳輸,總線位寬和時鐘頻譜分別是32位和33M(一般的desktop都是這樣的配置),也就是說每個時鐘周期傳輸32個比特的數據。因此,如果傳輸的是U8或者U16的數據類型,那么相當于每個傳輸周期浪費了3/4或者1/2的位寬。因此,U8情況下每次只能傳輸一個字節,結合33MHz的時鐘頻率,使得DMA 的Benchmark只有33MB/s。U16的情況下的DMA速率為66M也是可以理解的。而U32以及U64的情況下,只能達到133M的極限速率。
對于PXIe板卡,FPGA讀寫 FIFO的時鐘固定為200MHz,因此,如果FIFO的數據類型是U8,那么每秒鐘FPGA端就產生200MB的數據;如果FIFO的數據類型是U16,那么每秒鐘FPGA端就產生400MB的數據量。這些數據量遠小于1075機箱的單槽帶寬(PCIe Gen 1 ×4,1GB/s),因此,總的DMA Benchmark 就等于FPGA端產生數據的速率。如果FIFO的數據類型為U32,那么FPGA產生數據的速率就達到了800MB,這幾乎1075機箱的單槽傳輸極限,因此U32場景下DMA的BenchMark接近800MB/s。繼續增加FPGA端FIFO的位寬,在200MHz的時鐘頻率下FPGA每秒鐘產生1.6GB的數據,但這時PCIe總線的傳輸速率不可能再大幅提升,因此,U64情況下DMA的Benchmark 還是在800MB左右,所以我們很容易得出DMA的傳輸速率限制為總線的單槽傳輸帶寬。
實測1085+8135環境下的DMA速率
為了驗證此種情況(U64,200M時鐘)下DMA的傳輸速率限制確實為總線的單槽傳輸帶寬,我們有理由假設,如果使用1085機箱(單槽傳輸速率為4GB/s),DMA的傳輸速率應該可以達到1.6GB/s。因此,我又搭建了相關的系統對1085機箱下的7966 DMA速率進行測試:
硬件環境:PXIe-8135+PXIe-1085+PXIe-7966
測試方法:在7966中以200M的時鐘不斷將U64數據寫入到FIFO中,FIFO大小2048;上位機中對FIFO進行全速讀取(緩沖區中有多少點讀多少點),上位機緩沖區設置為200M;
測試結果:DMA速率900MB/s左右.修改上位機中FIFO的緩沖區大小,DMA速率沒有出現大的波動,依然在900MB/s。觀察CPU占用率在18~20%,每次循環查看緩沖區中的點數10000點不到,說明上位機的讀取速率完全不是瓶頸。
分析結果可以發現:此時系統總線的單槽帶寬已經不是限制因素,上位機的讀取速率也完全可以跟上。DMA傳輸鏈路上的瓶頸,除了FPGA產生數據的速率、總線傳輸帶寬、上位機的讀取速率之外,還有一點是我們之前沒有考慮到的——就是DMA控制器的速率。在我們實際測試的這種場景下,DMA控制器的速率成了DMA鏈路傳輸速率受限的主要原因。這個結果與官網上說7966的DMA傳輸速率為800MB/s以上是相互吻合的。
總結:
i) PXI總線的帶寬比較低,目前支持PXI總線的FlexRIO只有較老的795x系列,這種情況下限制DMA傳輸速率的主要是總線帶寬;
ii) PXIe 總線的帶寬比較高,不同機箱的單槽傳輸帶寬各有不同,因此,對DMA傳輸速率上限有一定的影響;
iii) FPGA端在一定的時鐘頻率下,如果希望提升數據率,可以使用更寬的數據位寬;
iv) DMA控制器的速率是板卡本身硬件上對DMA速率的限制——7966的DMA速率為800M以上,7975的DMA速率可以達到1.6GB/s;
當然, DMA的傳輸性能的影響因素,比如上下行的影響。上圖中給出的數據中,對于7965的上下行DMA速率相差不大,但對于7954的上下行DMA速率相差很大。對于這一點,還需要更加深入地研究。本文不再深入討論。
二、 FPGA與上位機的握手
FPGA中FIFO的使用使得FPGA端和上位機端在很多時候都可以異步操作。上位機中沒有有效的數據傳遞到FPGA端,FPGA端就不會進行有效的計算并傳輸無效的數據。但在有些情況下,FPGA端與上位機端需要進行握手的操作。比如:一個項目需要通過Adapter端口進行數據采集,然后將數據傳回到上位機。這種情況下,如果FPGA先于上位機開始執行,那么FPGA采集到的端口數據會迅速地填滿FIFO(此時上位機還沒有準備好去讀取FIFO的數據)導致后續的數據無法保存到FIFO中(寫入超時)。而當上位機開始讀取FIFO時,會發現FIFO的前一段數據(數據的長度與FPGA端配置的FIFO大小有關)與后續的數據是非連續的。為了避免類似的問題,需要在FPGA與上位機之間握手。
通常用于FPGA程序與Host程序同步的方法是使用一個握手控件。在FPGA的主程序之前,使用一個while循環,條件接線端鏈接著布爾控件。在上位機中,當一切準備就緒時(對FPGA的初始化以及其他初始化工作),可以為FPGA的Start控件賦上真值,這樣,FPGA與Host端的數據傳輸就同步了,這是非常容易實現的方法。代碼如下:
另一種方法是使用FPGA中提供的中斷。在FPGA端需要等待上位機的數據或者等待上位機做好接收數據的準備時,產生一個中斷。該中斷會阻塞程序直到該中斷被確認。當上位機中準備好向FPGA傳輸數據或者接收來自FPGA的數據時,上位機可以對中斷進行確認。代碼如下:
三、FIFO的使用
1) 使用場景:
該項目需要在回放時對信號進行精細的分析。尤其是有一個跳頻信號,跳頻的頻帶寬度為70MHz,跳頻信號本身的帶寬為5MHz,跳頻信號的持續時間為1us~20us,跳頻信號以突發的模式出現。需求是以較快的速率分析出信號文件中所有的頻率點以及各個跳頻信號所處的時間點。其實分析的原理不難,只需要對信號以足夠精細的時間分辨率(1us)做FFT,判斷該段信號是否有功率大于閾值的點就可以了。
在項目初期,曾經嘗試過直接在上位機中做FFT,判斷閾值,找到頻點以及對應的時間點。但經過測試,上位機端進行數據處理的瓶頸在于大的數據塊的分割。本程序中的數據源是存在磁盤陣列中的數據文件,從磁盤陣列中讀取文件本身的速率可以達到非常高,500MB以上,但達到此速率的前提就是每次讀取一個大的數據塊(幾兆到幾十兆字節),否則,讀取速率無法達到理想值;但這種情況下,就需要在程序中對大塊的數據進行分割,然后進行FFT。雖然測試時已經開通了多個流水線進行數據分割,但速率依然達不到要求。整個系統的最高處理速率只有40~50MB/s,這對于120G(流盤5分鐘)的跳頻信號分析顯然是不能滿足要求的(分析5分鐘的數據需要花將近1個小時的時間)。
基于這個原因,本程序將FFT的操作放在FPGA中完成。上位機中將大塊的數據傳輸到FPGA中,FPGA天然的單點操作特性使得不需要進行大數據分割就可以完成小點數的FFT操作。由于N點時域信號做完FFT之后數據量沒有減小,不能將FFT結果直接傳回到上位機中進行閾值檢測,所以FFT結果的閾值比較也在FPGA端進行單點比較,并將超過閾值的頻點的索引值傳回到上位機。
最終,FPGA端以400MB/s的速率對信號進行處理。
2) 初始化和配置:
每次在程序重新運行時,系統會對所有FIFO進行初始化,清除原有的數據。在上位機中對FIFO進行讀取之前,要首先對FIFO進行開始操作,否則,雖然最終能讀出數據,但前期會出現數據的丟失。當然,也可以通過編程的方式對FIFO進行初始化:使用FIFO的調用節點,先開始FIFO,再停止FIFO,再開始FIFO,就能完成FIFO的初始化并使FIFO處于就緒的狀態。不過這種編程方法來初始化FIFO的操作不常用。
FPGA與Host端通常采用DMA的方式進行數據傳輸,數據在DMA控制器的作用下從FPGA傳輸到上位機的緩沖區中,LabVIEW從緩沖區讀取到應用程序內部;在從上位機往下位機傳輸數據時,LabVIEW將數據從應用程序內部寫入到緩沖區中,然后DMA控制器通過總線從緩沖區獲取數據。在配置時,一方面需要配FPGA端的FIFO大小,這是通過窗口配置的方式來實現的。FIFO本身的大小在一定程度上可以減低數據覆蓋的危險,但FIFO設置的過大,會占用太多FPGA的資源,且是沒有必要的;另一方面需要配置上位機端用于DMA FIFO的緩沖區大小。一般,緩沖區的大小為每次讀取的數據塊大小的5~10倍左右。
3) FIFO讀取和寫入超時的利用:
參考FFT Co-processor范例,當FIFO中沒有有效數據時,讀取操作會發生超時,這時,不將無效數據寫入到下一級的FIFO;在下一次定時循環時繼續讀取;當寫入FIFO時發生超時,說明被寫入的FIFO中已沒有足夠的空間存入新的數據,這時,下一次定時循環時,依然向FIFO中寫入上一次循環的數據。當然,使用這種方式來進行數據傳輸時,FIFO讀取和寫入的數據都不會發生異常,但如果該流程中包含Host與FPGA之間的數據傳輸,則會影響FPGA執行的速率:I)Host端發送數據不夠快,則FPGA中總是無法得到有效數據,導致FPGA處理流程受限于數據源;II)Host端接收數據不夠快,則FPGA無法將FPGA-to-Host FIFO中的數據快速清空,進而影響FPGA從Host端取數據的速率。因此,在使用FPGA處理并向上位機傳輸數據的時候,上位機要以FPGA處理速率相當的速率或者以盡可能快的速率讀取數據。推薦的方式如下:
很多時候,從FPGA中讀取出來的數據需要直接流盤到磁盤陣列中。在高速流盤時對每次寫入文件的數據塊大小有要求——必須是扇區大小的整數倍,這種情況下,怎么進行讀取呢?
當然,很多函數本身就自帶輸入就緒、輸入有效和輸出就緒、輸出有效這些端口,可以將這些端口與FIFO讀取和寫入超時端口結合起來,保證讀取或者寫入數據的有效性。如下:
四線制的握手交互:上圖中展示的已經是一種四線制的握手方式,完成了三個模塊之間的交互。我們可以把這種交互分成左右兩部分來查看,每一部分分別都是兩線制的控制回環。首先來看左側部分:讀取FIFO IN時,如果發生超時,那么它輸出的數據就是無效的,可以將這個端口與下一個模塊的“輸入有效”端口相連,來告知下一個模塊這個必要的信息,對于無效數據,該模塊應該丟棄;而如果第二個模塊準備好接收下一個數據之后,第二個模塊也將該信號反饋給第一個模塊,告知第一個模塊可以輸出新的數據,否則,第一個模塊不應該輸出新的數據;這就構成了一個控制回環。再來看右側部分:第三個模塊如果準備好了接收新的數據,需要將這個信息告知第二個模塊(連接到第二個模塊的“輸出就緒”端口),這時第二個模塊就可以輸出新的數據了,否則第二個模塊不應該輸出有效數據;而當第二個模塊輸出了有效的數據,那么第三個模塊就應該接收數據,否則忽略。這又構成了一個控制回環。按照這樣的思路,可以進行多級級聯,完成多線制的握手。
四、FPGA中的數據類型轉換
FPGA中的很多運算都用整型或者定點數完成,這其中會經常碰到數據轉換的問題。比如,前端Adaptor采集到的信號的位寬是14,如果在FPGA中對數據進行FFT運算,就需要將整型數據轉換成定點數;很多時候,在將整型數轉換成定點數時,有客戶會問:我的整數位數應該設置為多少呢?其實在數據轉換時,首先要搞清楚整條鏈路上數據轉換的步驟。前端的Adaptor在進行AD采樣時,將數據轉換從DBL的模擬量量化成N位的整數。在量化的過程中,有一個量程的概念。比如量程為±M,那么轉換成將以±M作為歸一化的參考值。假設實際的模擬值為A,則A采樣值量化的方法如下:
量化值=A/M×2^((N-1))
之所以乘以2^((N-1)),是將最高位作為符號位進行處理。進一步,在FPGA端,將I16的數據轉換成定點數,道理是一樣的。在轉換成定點數時需要設置整數位數,這個整數位數就是在FPGA中進行定點轉換時的量程。當然,這里的量程表示的范圍與AD量化時的量程有可能是不匹配的,導致轉換之后的定點數與采樣之前的信號實際值是不一致的,但是沒有關系,只要牢記轉換過程中的相對關系,可以基于相對的轉換值進行計算。在將數據傳遞到上位機之后,如果要獲取絕對的數值,則可以按照轉換過程中的相對關系進行還原,即進行增益補償。
另一種可能出現的轉換就是:如果DMA接收到的數據是64位,其中包含兩個樣值,而后續處理是以32位的Sample為單位的,這就需要將64位FIFO中取出的數據轉換成32位的數據,然后寫入后續的FIFO中。前文有提到過,DMA的最高傳輸速率是基于64位數據位寬的,所以,很多時候,為了追求Host與FPGA之間的高速數據傳輸,FPGA從Host端接收的是64位數據。本程序就涉及到將64位數據寫入32位的FIFO。具體實現的方法是取出一個64位數據之后,拆分成高位和低位兩部分。定時循環每隔兩個循環讀取一個數據,但每次循環都要向32位FIFO中寫入數據。有人可能認為,這是很簡單的事兒啊:只要用一個奇偶判斷,奇數循環時寫入高位,偶數循環時寫入低位不就行了?但測試發現,在一個循環內部,不能出現多個FIFO寫入,否則,編譯器會提示無法決定數據源。另外,這種方法本身也是不可靠的,如果奇數循環在寫入高位時出現了超時,那么在下一個循環(即偶數循環),應該重新寫入高位數據。同理,如果偶數循環寫入時發生了超時,那么在下一個循環(即奇數循環)應該重新寫入低位數據。最終,如下方法是經過測試的既有可靠性而在編程上又非常簡便的方法:
五、FPGA編程時的普通循環和單周期定時循環
在說循環之前,我們首先來看一下FPGA在對代碼編譯到硬件上時會做什么樣的操作。默認情況下,LabVIEW需要確保程序在頂層時鐘下能編譯成功。在編程時,我們常常對數據進行多種聯結在一起的運算操作,如下所示。這些聯結在一起的運算使得聯合路徑較長并導致總的時鐘速率降低。基于這樣的原因,LabVIEW會在如下代碼的各運算函數之間增加寄存器,來拆分關鍵路徑的長度,確保程序可以在頂層時鐘下執行。每個寄存器的執行都需要一個tick,那么以下這段代碼的執行之間就是3個ticks。
While循環和For循環都可以用來執行重復的計算操作。這一點,與上位機中的功能是完全一致的。通常,我們會將一些操作放在循環中進行持續的處理。那么上述代碼在循環中的執行時序如下:
循環中每一個函數都在預留的硬件資源中等待上一個函數給出有效的輸出。因此,每次循環的執行都需要三個時鐘周期。上圖中顯示的還是一些簡單的運算,例如乘法、加法、邏輯比較等。如果程序中用到了一些多周期的數值運算函數,比如商與余數、倒數、平方根、除法等,那么一個循環執行的時間將會更長。總結來說,一個普通的while或者for循環執行的速率依賴于頂層時鐘以及一行代碼最多有多少個運算。
基于上述的原理,很多時候,普通的while循環或者for循環并不能滿足實際的需求。即使將頂層時鐘提升,也是有上限的。這種情況下,可以使用單周期定時循環(SCTL)來實現功能。在單周期定時循環中,程序員可以控制聯合路徑的長度,因為程序不再自動在函數之間增加寄存器,而使用到的反饋節點以及移位寄存器在硬件上都使用觸發器來實現。另外,單周期定時循環可以使用的時鐘頻率更加豐富。基于這些原因,單周期定時循環的效率接近HDL語言。同樣將之前所述的代碼放入到單周期定時循環中。
我們來看一下這段代碼在普通的循環中和在單周期定時循環中的執行效率。
可以看見,普通的循環由于 有系統自動添加的寄存器的效果,每個時鐘周期執行一個運算函數,而單周期定時循環可以在一個時鐘周期內執行循環內的所有代碼。當然,如果循環內的代碼無法在一個時鐘周期內完成,那么,程序在編譯時就會報出定時錯誤。這種情況下,需要修改代碼,減少關鍵路徑所占用的時間。
六、FPGA編程時的流水線使用
如上所述,如果單周期定時循環內部代碼執行所需的時間超出了定時時鐘限定的范圍,比如100MHz的時鐘限定了單周期定時循環每次執行的時間為10ns,那么程序在編譯過程中就會報出定時錯誤。消除這種錯誤的方法就是減少關鍵路徑所占用的時間(關鍵路徑是代碼中需要時間最長的一條代碼路徑)。那如何消除?使用流水線。
流水線的編程方法就是人為地在關鍵路徑(最長執行時延的路徑)中插入一些寄存器。這些寄存器可以將聯合的路徑拆分開,這一點與普通循環中系統自動添加的寄存器的作用是一致的。但在普通循環中,每個時鐘周期只執行一個函數,后續的函數雖然占用著硬件的資源,但由于沒有接收到前端的有效數據所以不會開始執行。因此,整個循環的代碼的執行時間是幾乎函數個數的總和。但是,既然各個函數都有各自預留的硬件資源,那為什么不能讓所有的函數在同一時間并行執行呢?這就是流水線的含義。而在單周期定時循環內部,在每個時鐘周期內,每一個流水線節點都從各自的起始點開始執行。
上圖分別展示了普通的循環、普通的單周期定時循環以及具有流水線的單周期定時循環對于同一個程序的執行流程。對于流水線的單周期定時循環,前兩個周期的輸出數據是無效的,從第三個周期開始輸出了第一個有效的數據,此后每個周期都會輸出一個有效的數據。所以,帶流水線的單周期定時循環的執行效果只是會引入一定的輸出數據時延,其他方面沒有任何不同。而帶流水線的單周期定時循環,由于使用了人為加入的反饋節點或者移位寄存器,將原來的關鍵路徑拆分成了好幾個部分,因此,一方面不容易在編譯時出現定時錯誤,另一方面由于縮短了關鍵路徑使得可以使用更高的時鐘頻率。具體編程時,請在時延和最高可達的時鐘頻率之間做一個權衡。
一些簡單的流水線編程技例子如下:
評論