1)摘自【正點原子】領航者ZYNQ之HLS 開發指南
2)實驗平臺:正點原子領航者ZYNQ開發板
3)平臺購買地址:https://item.taobao.com/item.htm?&id=606160108761
4)全套實驗源碼+手冊+視頻下載:http://www.openedv.com/docs/boards/fpga/zdyz_linhanz.html
5)對正點原子FPGA感興趣的同學可以加群討論:876744900
6)正點原子資料更新和新品發布,請加正點原子公眾號:正點原子
關注方法:微信→添加好友→公眾號→輸入:正點原子
第一章HLS簡介
為了儘快把新產品推向市場,數字系統的設計者需要考慮如何加速設計開發的周期。設計加速主要可以從「設計的重用」和「抽象層級的提升」這兩個方面來考慮。Xilinx推出的Vivado HLS工具可以直接使用C、C++或System C來對Xilinx系列的FPGA進行編程,從而提高抽象的層級,大大減少了使用傳統RTL描述進行FPGA開發所需的時間。
本章包括以下幾個部分:
1.1高層綜合簡介
1.2HLS設計流程
1.3接口綜合
1.4算法綜合
1.5HLS庫
1.1高層綜合簡介
在介紹HLS之前,我們先來了解一下FPGA設計過程中的不同抽象層級,如下圖所示:
圖 1.1.1 FPGA設計中的抽象層級
如圖 1.1.1所示,FPGA設計中從底層向上一共存在著四種抽象層級,依次為:結構性的、RTL、行為性的和高層。其中最底層的抽象(結構性的)涉及到對底層硬體單元直接的例化,比如邏輯門,甚至是更底層的LUT或者觸發器。設計者更常用的是在「寄存器傳輸級(Register Transfer Level,RTL)」 進行設計,這個層級的抽象隱藏了底層的細節,是在描述寄存器和寄存器之間可執行的操作。更上層的「行為性的」描述是對電路的算法描述,也就是描述電路表現出什麼樣的功能(行為),而不是描述每個寄存器該如何進行操作。
前面介紹的幾種抽象層級都是在使用硬體描述語言HDL進行設計,可以看出,隨著抽象層級的提升,設計最終在硬體上實現的細節逐漸被弱化。而本章重點介紹的「高層」設計方法則直接使用高級語言,如C/C++進行設計,然後由Vivado HLS編譯器將C代碼綜合成HDL描述,最後再進行邏輯綜合得到網表,這個網表最終會被映射到具體的FPGA器件上。
就像C語言或者其他高級語言針對不同的處理器架構有著不同的編譯器,Xilinx Vivado High-Level Synthesis(高層綜合,HLS)工具同樣是一種編譯器,只不過它是用來將C或者C++程序部署到FPGA上,而不是部署到傳統的處理器上。
在Vivado HLS中可以使用三種語言進行設計開發,分別是 C、C++ 和 SystemC。其中C語言是一種非常通用的面向過程的程式語言,我們在《正點原子ZYNQ嵌入式開發指南》中均是使用C語言進行嵌入式設計。
C++是一個基於C的面向對象的語言,它在C的基礎上擴展了類、模板、多態和虛函數的概念,還有一些其他的特性。C++的抽象層次總的來說比C要高,能做更精密、靈活的代碼開發。另一方面來說,C的語言特性和編程風格和C++是兼容的,因此C++可以認為是C的擴展集。總的來說,C++是比C更高級的語言,但是仍保留對低層C程序的支持。在《正點原子ZYNQ HLS開發指南》中,C和C++這兩種語言均有涉及。
在這裡我們把 SystemC 也當作一種獨立的語言,但是嚴格來說它是 C++ 的一種擴展。SystemC 能以 C++ 風格的代碼來實現 HDL 的以硬體為中心的概念,比如層次結構、並行和周期精確,這些都無法以標準 C++ 的形式來表達。因為在本教程中不涉及使用SystemC進行設計開發,在此我們不多作介紹。
1.2HLS設計流程
Vivado HLS 的功能簡單地來說就是把 C、C++ 或 SystemC 的設計轉換成 RTL 實現,然後就可以在 Xilinx FPGA 或 Zynq 晶片的可編程邏輯中綜合併實現了。需要注意的是,這裡我們說的使用C/C++完成的設計與運行在處理器(ZYNQ中的ARM處理器或MicroBlaze軟核處理器)中的軟體代碼是截然不同的。在HLS中,所有的C設計都是要在可編程邏輯中實現的,也就是說,我們仍然是在進行硬體設計,只不過使用的不再是硬體描述語言。
使用Vivado HLS進行設計的流程如下圖所示:
圖 1.2.1 HLS設計流程
HLS設計的主要輸入是一個 C/C++/SystemC 設計,以及一個基於 C 的測試集(TestBench)。我們首先要知道C語言的本質就是函數,那麼這個測試集就是用於驗證C設計中的函數,驗證過程需要一個「黃金參考」。這個「黃金參考」類似於一個標準答案,用來和C設計中函數所產生的輸出做比對。
在對HLS設計進行綜合之前,我們要先對其進行「功能性驗證」,也就是C仿真,其目的是驗證HLS 輸入的C代碼的功能是否正確。驗證的方式就是在TestBench中調用C設計的函數,然後將其輸出與「黃金參考」進行比對,如果與黃金參考有差異就需要先對C設計進行修改調試。
接下來就是對設計進行高層綜合,即HLS過程本身。該過程涉及到分析和處理基於 C 的代碼,加上用戶所給出的指令和約束,來創建RTL描述。高層綜合結束後會產生一組輸出文件,包括以Veilog或者VHDL語言編寫的RTL設計文件。
綜合過程結束後得到的RTL模型,可以在 Vivado HLS 中進行 C/RTL 協同仿真,來進一步驗證綜合得到的RTL設計的正確性。在這個過程中Vivao HLS會自動產生一個測試集為RTL設計提供輸入,然後拿它的輸出與預期的值做比對。C功能性驗證和C/RTL協同仿真的區別如下圖所示:
圖 1.2.2 C 功能性驗證和 C/RTL 協同仿真
在圖 1.2.2左側的功能性驗證(C仿真)中,原始測試集是用戶輸入的測試文件TestBench。而右側的C/RTL協同仿真所需的RTL測試集是由 Vivado HLS 自動產生的,這樣就不再需要人工創建了,所產生的測試集包括了原始測試集和被測RTL模塊之間的數據傳遞。
除了對功能進行驗證,我們還要評估 RTL設計的實現和性能。比如,在FPGA中所需的資源的數量,設計的延遲、所支持的最高時鐘頻率等是否滿足要求。如果不滿足要求,那麼就需要設計者通過修改指令和約束,然後再次進行高層綜合,如圖 1.2.1中右側的迴路所示。一個設計可能要做多次HLS設計迭代,來找到「最佳 」的解決方案。如果有必要,設計者也可以返回修改C設計代碼,然後從頭開始重新對設計進行驗證。
在設計被驗證了之後,而且實現也滿足了期望的設計目標,那麼就可以集成進更大的系統裡了。我們可以直接使用 HLS 過程所產生的RTL文件(即VHDL 或 Verilog 代碼),更方便的做法是使用 Vivado HLS 的 IP 打包功能。對Vivado HLS 所產生的輸出打包意味著 HLS 設計能夠以IP核的形式引入其他 Xilinx 工具中,比如Vivado中的IP 集成器。這兩種類型的輸出如下圖所示:
圖 1.2.3 HLS綜合的輸出
1.3接口綜合
在做 HLS 的時候,設計者需要分析設計的兩個主要方面:
• 設計的接口,也就是它的頂層連接;
• 設計的功能,也就是它所實現的算法;
我們給出一個HLS設計中接口和功能的概念圖,如圖 1.3.1所示。
圖 1.3.1 接口和功能的說明
在上圖中,兩端的綠色區域表示設計的輸入和輸出接口,其中展示了部分接口類型,如RAM接口、FIFO接口,以及總線類型的接口等。這些接口可以是工具從代碼中通過接口綜合(Interface Synthesis)得到的,也可以由設計者手動指定具體的接口類型。
圖中間黃色的區域表示HLS設計具體能夠實現的功能,對於不同的應用,其功能也各不相同。在 Vivado HLS 設計中,功能是從輸入的代碼中,經過算法綜合(Algorithm Synthesis)的過程得到的。
在這裡我們先簡單介紹一下接口綜合。顧名思義,Interface Synthesis指的是 HLS 設計中對接口的綜合,綜合出來的接口能夠與系統中的其他模塊通信,還有可能需要與系統中的處理器進行通信。
這裡接口的概念既包括埠(port),也包含所使用的協議。所有埠的細節(如類型、位寬和方向)是從 C/C++ 文件中頂層函數的參數和返回值裡推斷出來的;而協議是從埠的表現(行為)推斷出來的。比如,最簡單的接口可以是一條 1 比特的線(wire),而更複雜的接口,可能要用總線或 RAM 接口。接口綜合能夠推斷出來的接口類型包括:線、寄存器、單向和雙向握手信號、FIFO、存儲器和總線等。
下面我們給出一個簡單的C設計的頂層函數,函數名為find_average_of_best_X(),其參數如下圖所示:
圖 1.3.2 C設計的頂層函數
圖 1.3.2中函數內部工作的詳細情況無關緊要,不過每個參數的讀/寫操作將決定綜合出來的埠的方向。這個函數定義包含三個參數,數組「sample」和整數「X」是函數的輸入,而average作為函數的輸出。因此,簡單來說,這三個函數參數要被 HLS 轉換成兩個輸入接口和一個輸出接口,如下圖所示:
圖 1.3.3 函數find_average_of_best_X()綜合後的簡化接口
需要注意的是,圖 1.3.3隻是一個簡化了的接口示意圖。根據所用的協議,這些接口可能包括數據埠自身以外的控制輸入或輸出,如下圖所示:
圖 1.3.4 函數find_average_of_best_X()的RTL接口圖
圖 1.3.4是函數find_average_of_best_X()經HLS綜合出來的完整的RTL模塊的接口圖。從圖中可以看到由函數的三個參數所綜合出來的接口分別擁有了各自的協議,如ap_memory協議、ap_none協議和ap_vld協議。同時模塊還多出來了一些埠,如ap_clk和ap_rst等,它們使用的是ap_ctrl_hs協議。這些協議決定了相應的接口是如何與系統中其他模塊進行交互的,至於各協議具體的含義以及如何為接口選擇其協議,我們將在後續的章節中介紹。
1.4算法綜合
算法綜合關注的是設計的功能,即設計所期望的行為,它是由輸入的 C設計所描述的。算法綜合從代碼中推出各種運算操作,然後轉換成一組 RTL 語句。
算法綜合包括三個主要階段,依次是:
1. 解析出數據通路和控制電路;
2. 調度和綁定;
3. 優化;
解析出數據通路和控制電路
HLS 的第一個階段是分析 C/C++/SystemC 代碼,並且解釋所需的功能。Vivado HL從以下幾個方面分析程序:邏輯和算法的運算、條件語句和分支、數組運算和循環等。
所產生的實現會具有一個數據通路元件,一般還會有一個控制元件。需要澄清的是,這裡的「數據通路」處理指的是在數據樣本上作的運算,而「控制」是需要協同數據流處理所需的電路。算法的本質定義出數據通路和控制元件,設計者可以在 HLS 中採取專門的步驟來最小化控制元件的複雜度。
調度和綁定
HLS 是由兩個主要過程組成的:調度(Scheduling)和綁定(Binding)。它們是交替進行的,彼此互相影響,如下圖所示:
圖 1.4.1 HLS的調度和綁定過程
• 調度是把由 C 代碼解釋得到的 RTL 語句翻譯成一組運算,每個運算都關聯著一定的執行時間,以時鐘周期為單位。這個階段所作的決策,受時鐘頻率和不確定度、目標晶片的技術和用戶所施加的指令所影響。
• 綁定是調度好了的運算和目標晶片上的實際資源聯繫起來的過程。這些資源的功能和時序特徵可能會影響調度,因此綁定信息會反饋給調度過程。比如使用DSP48x 資源就表明關鍵路徑比採用邏輯資源的方案要短。
比如,如果綜合出來的算法需要做一組算術運算,HLS 過程就必須根據目標的時鐘頻率和不確定度來決定如何調度這些運算(要分配多少個時鐘周期來完成),以及如何綁定這些運算(也就是如何把運算映射到 PL 上的可計算資源裡)。C源碼並不能表達或指定硬體架構,但是通過施加指令,源碼確實可以產生不同的架構。
優化
有兩種方法可以用來調整 HLS 過程的行為,讓高層綜合朝著設計者的實現目標而努力,從而影響結果:
• 約束 — 設計者可以對設計的某些指標加以限制。比如,可以指定最低的時鐘周期。這樣就能確保實現結果能夠滿足要集成進去的系統的要求。類似的,設計者可以選擇約束資源的利用情況或其他的指標,從而優化應用的設計。
• 指令 — 設計者可以通過指令對 RTL 的實現參數施加更具體的影響。有各種類型的指令,分別映射在代碼的某些特徵上,比如讓設計者可以指定 HLS 引擎如何處理 C 代碼中識別出來的循環或數組,或是某個特定運算的延遲。這能導致RTL 輸出的巨大改變。因此,具有了指令的知識,設計者就可以根據應用的需求來做優化了。
1.5HLS庫
Vivado HLS中包含了一系列的C庫(包括C和C++),方便對一些常用的硬體結構或功能使用C/C++進行建模,並且能夠綜合成RTL。在Vivado HLS中提供的C庫有下面幾種類型:
1、任意精度數據類型庫
2、HLS Stream庫
3、HLS數學庫
4、HLS視頻庫
5、HLS IP庫
6、HLS線性代數庫
在HLS設計中調用庫中的函數可以大大提高開發效率,比如在本教程中我們用到了大量的「HLS視頻庫」中的函數,來進行基於HLS的視頻圖像處理。對於上面列出的各個庫,我們同樣會在後續章節中用到時候再進行介紹。