《FPGA並行編程》讀書筆記(第一期)02_Fir濾波器


說在前面的話:
我最初的規劃是一個星期更新一篇文章,但根據過去一天的統計,在沒有刻意宣傳的情況下,公衆號粉絲從0到170人,單篇文章閱讀量達600人,這結果對小編來說是個非常好的開始,因此決定熬夜寫稿加更一期文章,來回饋粉絲的熱情!同時非常歡迎關注公衆號拍的小夥伴們推薦給身邊有需要的童靴,讓更多的人只要有個這個樣的公衆號分享經驗。

1. 緒論

大家上個讀書筆記的內容都掌握了嗎?個人感覺至少需要6個小時纔可以對HLS有個概念性的認識,要真正熟練掌握HLS還得靠接下來10個章節循序漸進的歷練。爲了使大家對上節內容的理解更加深刻,我聯繫了Xilinx SAE的軍哥,轉載他的《跟Xilinx SAE學HLS》系列視頻教程,今天轉載他的4個與上節內容相關的視頻。
Vivado HLS基本流程

作者高亞軍,FPGA技術愛好者、分享者,出版圖書《基於FPGA的數字信號處理》、《Vivado從此開始》,發佈視頻“Vivado入門與提高”、“跟Xilinx SAE學HLS”。公衆號來源及ID(Lauren的FPGA,Lauren_FPGA)
上述視頻是權威資源,Xilinx官方錄製的視頻。

2. 讀書筆記源碼說明

該書的配套源碼下載,見我的Github:PP4FPGAS_Study_Notes_S1C02_HLS_FirFilter
該代碼源自原版書籍的源碼的重新組織,爲的是方便小夥伴們進行學習。

文件組織說明:
github截圖


本章有6個用戶文件,按照第1章給大家的HLS入門資料新建工程。新建好的工程目錄截圖如下:
工程目錄截圖
只要取消對應Solution的註釋,並雙擊對應的Solution進行激活,就可以進行接下來的工作了。

3. 9個Solution來學習HLS

3.1 S1_Baseline

大家閱讀完《FPGA並行編程》第二章FIR濾波器的概述、背景以及結構基礎就可以進行接下來奇妙的實驗之旅了。在此鄭重說明,大家一定要對FIR有個比較清晰的理解,否則進行很難進行代碼重構以及一些HLS的Directive優化。
首先要進行的當然是C Simulation,以此驗證C的邏輯正確性。

下面是進行C Simulation容易出現的問題,一定注意把這兩個文件加入Test Bench。

在這裏插入圖片描述
否則的話運行C-Simulation運行會出現這個結果
錯誤仿真結果
如果自己嘗試把英文原版書籍的源碼導入工程,就會出現上述莫名其妙的錯誤。大家可能會好奇,官方給的源碼爲啥會有問題呢???這裏的官方源碼當然沒有問題,有問題的是自己的操作,沒有把out.gold.dat與input.dat加入到Test Bench當中導致出現這個錯誤。其實官方給了script.tcl這個文件,估計很多小夥伴沒有注意。tcl命令說明需要加入這兩個文件,這樣就可以正常進行仿真出結果了。這個命令行的操作對很多小夥伴不友好,我就不在此介紹了。
tcl命令截圖

正確仿真結果爲
正確仿真結果截圖
從上圖可以看出,仿真正確執行了,這個濾波器輸出的結果與文件中已經算好的結果一致。
完成了仿真還不算完,我帶大家說明下爲啥不添加文件就不可以正確仿真呢。小編當初學習《FPGA並行編程》這本書的時候就沒有各位小夥伴當初那麼幸運了,想着用官方的源碼怎麼不可以正確地仿真呢,於是一步一步利用Debug找到了罪惡的源頭,發現這些數據文件根本沒有被正確的讀取…。

錯誤源頭
這裏面採用的相對路徑,相對路徑和絕對路徑我就不在這裏科普了,大家可以移步至Google找下。我們可以通過觀察Console來看出一點端倪來。Console觀察分析錯誤來源
通過這個Console輸出的消息我們可以找到對應的C simulation指令所生成的exe文件。仿真exe文件目錄
對相對路徑和絕對路徑熟悉的小夥伴現在應該知道問題出在哪了!也有的小夥伴想,乾脆直接使用絕對路徑,這也是個辦法,只不過代碼的可移植性立馬降下來了。
然後大家可以直接進行C Synthesis了,綜合的結果如下圖所示。這個是沒有加入優化的原始代碼,所以效率比較低。可以簡單分析這個fir濾波器的C代碼實現,發現它並沒有並沒有充分利用並行性,後面會通過加入HLS特有的Directive以及代碼重構來提升算法的並行性。S1資源利用S1Directive

3.2 S2_Remove_if

首先進行一次簡單的代碼重構,即刪除for循環中的條件語句,來實現一個更加有效的硬件結構。

#ifdef S2_Remove_if
//*******************S2_Remove_if
#include "fir.h"

void fir (data_t *y,data_t x)
{
	coef_t c[N] = {53, 0, -91, 0, 313, 500, 313, 0, -91, 0,53};
	// Write your code here
	static data_t shift_reg[N];
	acc_t acc;
	int i;

	acc = 0;

	Shift_Accum_Loop:

	for (i = N - 1; i > 0; i--) {
		shift_reg[i] = shift_reg[i - 1];
		acc += shift_reg[i] * c[i];
	}

	acc += x * c[0];
	shift_reg[0] = x;
	*y = acc;
}


#endif

對比S1與S2的時間以及資源情況,發現並沒有產生太大變化。而且fir濾波器的任務延遲好像還“增大了”。小夥伴們先不要急我們的優化纔剛開始,慢慢來,最後你會發現HLS的神奇之處。
S2資源

3.3 S3_Cycle_Partition

現在我們開始進行循環拆分優化,優化後的代碼爲

#ifdef S3_Cycle_Partition
//*******************S3_Cycle_Partition
#include "fir.h"

void fir (data_t *y,data_t x)
{
	coef_t c[N] = {53, 0, -91, 0, 313, 500, 313, 0, -91, 0,53};
	// Write your code here
	static data_t shift_reg[N];
	acc_t acc;
	int i;

	acc = 0;

	TDL:
	for (i = N - 1; i > 0; i--) {
		shift_reg[i] = shift_reg[i - 1];
	}
	shift_reg[0] = x;

	acc = 0;
	MAC:
	for (i = N - 1; i >= 0; i--) {
		acc += shift_reg[i] * c[i];
	}
	*y = acc;
}


#endif

循環拆分雖然並不可以提高硬件實現的效率,這從下面的Solution綜合報告可以看出,但可以允許我們在每個循環上進行不同程度的優化。S3資源

3.4 S4_Manual_Unroll_TDL

首先我們對TDL進行手動循環展開,代碼如下

#ifdef S4_Manual_Unroll_TDL
//*******************S4_Manual_Unroll_TDL
#include "fir.h"

void fir (data_t *y,data_t x)
{
	coef_t c[N] = {53, 0, -91, 0, 313, 500, 313, 0, -91, 0,53};
	// Write your code here
	static data_t shift_reg[N];
	acc_t acc;
	int i;

	acc = 0;

	TDL:
	for (i = N - 1; i > 1; i = i - 2) {
		shift_reg[i] = shift_reg[i - 1];
		shift_reg[i - 1] = shift_reg[i - 2];
	}
	if (i == 1) {
		shift_reg[1] = shift_reg[0];
	}
	shift_reg[0] = x;

	acc = 0;
	MAC:
	for (i = N - 1; i >= 0; i--) {
		acc += shift_reg[i] * c[i];
	}
	*y = acc;
}


#endif

通過在Analysis界面對比S3、S4可以發現TDL實現的時間縮短了一半
S4S3Directive1
S4S3Directive2
S4S4Directive1
S4S4Directive2

3.5 S5_Unroll_TDL

除了手動進行展開外,可以利用Directive進行指令展開,展開factor=2,這裏就不進行代碼貼圖了
S5Directive
實現效果與S4_Manual_Unroll_TDL一致。

3.6 S6_Unroll_MAC

對MAC也進行展開,展開factor=4 S6Compare
進行綜合結果對比發現對MAC進行展開時,並沒有像對TDL展開那樣,有非常好的性能提升。在Analysis界面也可以明顯看到,MAC展開好像並沒有效果呀!不像TDL那麼直觀明瞭,需要的時間直接減少了一半。這裏讀者可能就要懷疑了,是不是HLS有缺陷,導致了這個錯誤,如果你這樣想的話,那就太幼稚了,接下來引出一個非常重要的知識點ARRAY_PARTITION,官方講解可以去pragma HLS array_partition瀏覽。
我這裏就按照我的理解,結合UG902來給大家講解吧!
下面是直接Google翻譯的上述網站的內容。
ARRAY_PARTITION功能描述:將數組分爲較小的數組或單個元素。

具有四個關鍵參數

  • variable=(name):必需參數,指定要分區的數組變量。
  • (type):可選擇指定分區類型。默認類型是complete。
  • factor=(int):指定要創建的較小數組的數量。切記對於完整類型分區,不需要指定因子。但對於塊和循環分區,這factor= 是必需的。
  • dim=(int):指定要分區的多維數組的維度。
    • 如果使用值0,則使用指定的類型和因子選項對多維數組的所有維度進行分區。
    • 任何非零值僅分區指定的維度。例如,如果使用值1,則僅對第一個維度進行分區。

具有三種分區類型

  • cyclic:循環分區通過交錯原始數組中的元素來創建較小的數組。通過在返回第一個數組之前將一個元素放入每個新數組中來循環分區數組,以重複循環直到數組完全分區。例如,如果factor=3使用:
    • 元素0被分配給第一個新數組。
    • 元素1被分配給第二個新陣列。
    • 元素2被分配給第三個新陣列。
    • 元素3再次分配給第一個新陣列。
  • block:塊分區從原始數組的連續塊創建較小的數組。這有效地將數組拆分爲N個相等的塊,其中N是由factor=參數定義的整數。
  • complete:完全分區將陣列分解爲單個元素。對於一維數組,這對應於將存儲器解析爲單獨的寄存器。這是默認的 (type)。

下面截取UG902來加深大家對ARRAY_PARTITION的理解。
一維ARRAY_PARTITION
一維數組,factor=2。大家結合上面的介紹來充分理解這張圖,對ARRAY_PARTITION的深刻理解對以後的各種優化都非常重要哦!數組ARRAY_PARTITION
上面是一個三維數組進行ARRAY_PARTITION,要理解好各個參數的對應關係,我在這裏就不多費口舌了。
自動ARRAY_PARTITION
最後一個我這裏就不多說了,有能力者可以先學下。
大家如果讀懂上述的ARRAY_PARTITION如何使用之後,接下來我們就繼續進行優化了。

3.7 S7_ARRAY_PARTITION

通過上節對ARRAY_PARTITION進行講解之後,大家應該都明白ARRAY_PARTITION如何使用了吧。對循環展開涉及到的c 、shift_reg兩個數組的讀取進行如下優化。
S7Directive
通過下圖發現這個優化策略仍然沒有實質性的性能提升。S7Compare
分析Analysis界面的結果也可以看出,並沒有並行執行呀!這是因爲循環之間有依賴關係,這個問題的解決需要在後面章節見分曉!現在我們來執行個手動展開來優化,使之可以綜合出並行效果的硬件。S7analysis

3.8 S8_Manual_Unroll_MAC

手動展開MAC代碼見下圖

#ifdef S8_Manual_Unroll_MAC
//*******************S8_Manual_Unroll_MAC
#include "fir.h"

void fir (data_t *y,data_t x)
{
	coef_t c[N] = {53, 0, -91, 0, 313, 500, 313, 0, -91, 0,53};
#pragma HLS ARRAY_PARTITION variable=c complete dim=1
	// Write your code here
	static data_t shift_reg[N];
#pragma HLS ARRAY_PARTITION variable=shift_reg complete dim=1
	acc_t acc;
	int i;

	acc = 0;

	TDL:
	for (i = N - 1; i > 0; i--) {
#pragma HLS UNROLL skip_exit_check factor=11
		shift_reg[i] = shift_reg[i - 1];
	}
	shift_reg[0] = x;


	MAC:
	for (i = N - 1; i >= 2; i -= 3) {
		acc += 	shift_reg[i] * c[i] + 
				shift_reg[i - 1] * c[i - 1] +
				shift_reg[i - 2] * c[i - 2] +
				shift_reg[i - 3] * c[i - 3];
	}

	for (; i >= 0; i--) {
		acc += shift_reg[i] * c[i];
	}
	*y = acc;
}


#endif

對比綜合結果,是不是很神奇,現在僅用0.17us就可以實現fir濾波了。S8Compare
資源佔用也僅僅是提高一點點,真是皆大歡喜。通過觀察Analysis界面的結果,可以發現算法按照預計的並行執行了。S8analysis
爲了進一步提高效率,有的小夥伴想不如完全手動展開,這樣算法的執行效率不就更高了嗎?答案當然是否定的,雖然執行效率更高了,但是佔用了很多DSP資源,得不償失。後面會利用更有效的方法來進一步的減少任務延遲,優化邏輯資源的利用率,讓這些資源沒有片刻休息時間。另外我在此章節刻意隱藏一個bug,這個bug我還沒找到解決方案,因此就暫且擱置,我會在本讀書筆記的後續版本進行更新,歡迎大家進行原文閱讀導向CSDN,裏面有我文章的最新版本。

3.9 S9_Pipeline

pipeline本章節就不做過多介紹了,會在DFT章節進行詳細講解,代碼截圖爲

#ifdef S9_Pipeline
//*******************S9_Pipeline
#include "fir.h"

void fir (data_t *y,data_t x)
{
	coef_t c[N] = {53, 0, -91, 0, 313, 500, 313, 0, -91, 0,53};
#pragma HLS ARRAY_PARTITION variable=c complete dim=1
	// Write your code here
	static data_t shift_reg[N];
#pragma HLS ARRAY_PARTITION variable=shift_reg complete dim=1
	acc_t acc;
	int i;

	acc = 0;

	TDL:
	for (i = N - 1; i > 0; i--) {
#pragma HLS UNROLL skip_exit_check factor=11
		shift_reg[i] = shift_reg[i - 1];
	}
	shift_reg[0] = x;


	MAC:
	for (i = N - 1; i >= 2; i -= 3) {
#pragma HLS PIPELINE
		acc += 	shift_reg[i] * c[i] +
				shift_reg[i - 1] * c[i - 1] +
				shift_reg[i - 2] * c[i - 2] +
				shift_reg[i - 3] * c[i - 3];
	}

	for (; i >= 0; i--) {
#pragma HLS PIPELINE
		acc += shift_reg[i] * c[i];
	}
	*y = acc;
}


#endif

綜合結果對比
S9Compare
任務延遲繼續減小至0.11us,邏輯資源基本保持不變,算法的並行效果更好。

4. 總結

本章重點介紹了ARRAY_PARTITION、UNROLL以及進行簡單代碼重構的UNROLL,提及了PIPELINE但沒有進行重點講解,後續會在DFT章節進行詳細介紹,還提及了進行C Simulation因爲路徑問題而要特別注意的地方。位寬優化與複數FIR濾波器我這裏就先不進行詳細說明了,大家有精力可以自己研究,不是很複雜。最後要說的是,大家最好多看幾遍本章的內容,後續內容的複雜度是循序漸進的,不慢慢來的話會面會吃不消的。

原創不易,切勿剽竊!

公衆號二維碼

歡迎大家關注我剛創建的微信公衆號——小白倉庫
原創經驗資料分享:包含但不僅限於FPGA、ARM、Linux、LabVIEW等軟硬件開發。目的是建立一個平臺記錄學習過的知識,並分享出來自認爲有用的與感興趣的道友相互交流進步。


XIlinx學術合作
最後要提的是,本文很多資料都是Xilinx大學計劃提供,該公衆號提供很多的權威信息、開源項目、開發板租借,強烈推薦對FPGA感興趣的道友關注——XIlinx學術合作。


注:個人精力能力有限,歡迎批評指正!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章