FPGA 非常適合進行數學運算,但是需要一點技巧,所以我們今天就看看如何在 FPGA 中進行簡單和復雜的數學運算。
介紹
由于FPGA可以對算法進行并行化,所以FPGA 非常適合在可編程邏輯中實現數學運算。我們可以在 FPGA 中使用數學來實現信號處理、儀器儀表、圖像處理和控制算法等一系列應用。這意味著 FPGA 可用于從自動駕駛汽車圖像處理到雷達和飛機飛行控制系統的一系列應用。
因為 FPGA 寄存器豐富并且包含專用乘法器累加器 (DSP48) 等功能,所以在 FPGA 中實現數學運算需要一些技巧。
這使它們成為實現定點數學運算的理想選擇,但是這與我們傾向于使用的浮點運算不同,因此在進行浮點運算時候我們需要一點技巧。
定點數學運算
定點數的小數點位于向量中的固定位置。小數點左邊是整數元素,小數點右邊是小數元素。這意味著我們可能需要使用多個寄存器來準確量化數字。幸運的是 FPGA 中的寄存器通常很多。
相比之下,浮點數可以存儲比固定寄存器寬度(例如,32 位)寬得多的范圍。寄存器的格式分為符號、指數和尾數,小數點可以浮動,因此直接使用 32 位寄存器時,其能表達的值遠遠超過 2^32-1。
然而,在可編程邏輯中實現定點數學運算有幾個優點,而且實現起來要簡單得多。由于定點解決方案使用的資源顯著減少,因此可以更輕松地進行布線,從而提高性能,因此在邏輯中進行定點數學運算時可以實現更快的解決方案。
需要注意的一點是,在處理定點數學運算時會使用一些規則和術語。第一個也是最重要的問題之一是工程師如何描述向量中小數點的位置。最常用的格式之一是 Q 格式(長格式的量化格式)。Q 格式表示為 Qx,其中 x 是數字中小數位數。例如,Q8 表示小數點位于第 8 和第 9 個寄存器之間。
我們如何確定必要的小數元素的數量取決于所需的精度。例如,如果我們想使用 Q 格式存儲數字 1.4530986319x10^-4,我們需要確定所需的小數位數。
如果我們想使用 16 個小數位,我們將數字乘以 63356 (2^16),結果將是 9.523023。這個值可以很容易地存儲在寄存器中;然而,它的精度不是很好,因為我們不能存儲小數元素,所以因為9.523023≈9, 9/65536 = 1.37329101563x10^-4。這會導致準確性下降,從而可能影響最終結果。
我們可以使用 28 個小數位,而不是使用 16 個小數位,結果就是 39006(1.4530986319x10^-4 x 2^28) 的值存儲在小數寄存器中。這給出了更準確的量化結果。
了解了量化的基礎知識后,下一步就是了解有關數學運算小數點對齊的規則。如果我們執行運算操作但是小數點沒有對齊,我們就不會得到正確的結果。
加法——小數點必須對齊
減法——小數點必須對齊
除法——小數點必須對齊
乘法——小數點不需要對齊
我們還需要考慮操作對結果向量的影響。不考慮結果的大小可能會導致溢出。下表顯示了結果大小調整的規則。
假設我們有兩個向量(一個 16 位,另一個 8 位),在進行運算的時候將出現以下情況:
C(16 ?downto ?0)= A(15 ?downto ?0)+ B(7 ?downto ?0)
C(16 ?downto ?0)= A(15 ?downto ?0)- B(7 ?downto ?0)
C(22 ?downto ?0)= A(15 ?downto ?0)* B(7 ?downto ?0)
C(8 ?downto -1)= A(15 ?downto 0)/ B(7 ?downto 0)
做除法時的 -1 ,反映了小數元素的寄存器大小的增加。根據所使用的類型,如果使用 VHDL 定點包,這可能是 8 到 -1,如果使用 Q1 時可能是 9 到 0。
關于除法的最后一點說明它可能會占用大量資源,因此通常最好盡可能使用移位實現除法運算。
簡單算法
現在我們了解了定點數學的規則,讓我們來看看我們如何能夠實現一個簡單的算法。在這種情況下,我們將實現一個簡單的滑動平均(exponential moving average)函數。
要開始使用此應用程序,我們需要先打開一個新的 Vivado 項目并添加兩個文件。第一個文件是源文件,第二個文件是測試文件。
?
library?IEEE; use?IEEE.STD_LOGIC_1164.ALL; --library?ieee_proposed; use?ieee.fixed_pkg.all; entity?math_example?is?port( ?clk?:?in?std_logic; ?rst?:?in?std_logic; ? ?ip_val?:?in?std_logic; ?ip?:?in?std_logic_vector(7?downto?0); ? ?op_val?:?out?std_logic; ?op?:?out?std_logic_vector(7?downto?0)); end?math_example; architecture?Behavioral?of?math_example?is constant?divider?:?ufixed(-1?downto?-16):=?to_ufixed(?0.1,?-1,-16?)?;? signal?accumulator?:?ufixed(11?downto?0)?:=?(others?=>?'0'); signal?average?:?ufixed(11?downto?-16?)?:=?(others?=>?'0'); begin acumm?:?process(rst,clk) begin ? ?if?rising_edge(clk)?then? ????if?rst?=?'1'?then? ????????accumulator?<=?(others?=>?'0'); ????????average?<=?(others?=>?'0'); ????elsif?ip_val?=?'1'?then? ????????accumulator?<=?resize?(arg?=>?(accumulator?+?to_ufixed(ip,7,0)-average(11?downto?0)),?size_res?=>?accumulator); ????????average?<=?accumulator?*?divider; ????end?if; ?end?if; end?process; op?<=?to_slv(average(7?downto?0)); end?Behavioral;
?
測試文件如下所示:
?
library?IEEE; use?IEEE.STD_LOGIC_1164.ALL; use?ieee.numeric_std.all; entity?tb_math?is --??Port?(?); end?tb_math; architecture?Behavioral?of?tb_math?is component?math_example?is?port( ?clk?:?in?std_logic; ?rst?:?in?std_logic; ? ?ip_val?:?in?std_logic; ?ip?:?in?std_logic_vector(7?downto?0); ? ?op_val?:?out?std_logic; ?op?:?out?std_logic_vector(7?downto?0)); end?component?math_example; type?input?is?array(0?to?59)?of?integer?range?0?to?255; signal?stim_array?:?input?:=?(70,71,69,67,65,68,69,66,65,72,70,69,67,65,70,64,69,65,68,64,69,70,72,68,65,72,69,67,67,68,70,71,69,67,65,68,69,66,65,72,70,69,67,65,70,64,69,65,68,64,69,70,72,68,65,72,69,67,67,68); constant?clk_period?:?time?:=?100?ns; signal?clk?:?std_logic?:=?'0'; signal?rst?:??std_logic:='0'; signal?ip_val?:??std_logic?:='0'; signal?ip?:??std_logic_vector(7?downto?0):=(others=>'0'); signal?op_val?:??std_logic?:='0'; signal?op?:?std_logic_vector(7?downto?0):=(others=>'0'); begin clk_gen?:?clk?<=?not?clk?after?(clk_period/2); uut:?math_example?port?map(clk,?rst,?ip_val,?ip,?op_val,?op); stim?:?process begin? ?rst?<=?'1'?; ?wait?for?1?us; ?rst?<=?'0'; ?wait?for?clk_period; ?wait?until?rising_edge(clk); ?for?i?in?0?to?59?loop ??wait?until?rising_edge(clk); ??ip_val?<=?'1'; ??ip?<=?std_logic_vector(to_unsigned(stim_array(i),8)); ??wait?until?rising_edge(clk); ??ip_val?<=?'0'; ?end?loop; ?wait?for?1?us; ?report?"simulation?complete"?severity?failure; end?process; ? end?Behavioral;
?
這些文件利用了 VHDL 2008 包文件。
讓我們一步一步地看一下這些文件以及它們在做什么:
Clock - 模塊的同步時鐘
Reset - 將模塊復位為已知狀態
Input Valid (op_val) - 這表示新的輸入值可用于計算
ip = 8 bit (7 downto 0) - 輸入平均值
Output Valid (in_val) - 這表示計算后的值可用
op = 8 bit (7 downto 0) - 輸出平均值
?
entity?math_example?is?port( ?clk?:?in?std_logic; ?rst?:?in?std_logic; ? ?ip_val?:?in?std_logic; ?ip?:?in?std_logic_vector(7?downto?0); ? ?op_val?:?out?std_logic; ?op?:?out?std_logic_vector(7?downto?0)); end?math_example;
?
我們架構的第一部分是除法器,它是一個常數。它被定義為無符號定點類型 (ufixed),因為我們使用的是無符號數。這是完全小數,沒有整數位,所以我們將它定義為從 -1 到 -16。
?
constant?divider?:?ufixed(-1?downto?-16):=?to_ufixed(?0.1,?-1,-16?)?;?
?
精度可以通過以下方式確定:
0.1 x 2^16 = 6553.6
6553/2^16 = 0.999 = 99.9% 準確度
To_ufixed (0.1, -1, -16)
0.1 -我們要轉換的值
-1 -數組的上限
-16 -數組的下界
信號累加器 - 存儲我們的值
初始化為0進行仿真
最多可以累加 10 個 8 位值。
如果所有 10 個 8 位數字都達到其最大計數 (max = 255) 并將它們加在一起,我們將需要一個 12 位數字,因此我們將 ufixed 定義為 (11 downto 0)。
我們只存儲整數位,沒有小數位。
?
signal?accumulator?:?ufixed(11?downto?0)?:=?(others?=>?'0');
?
信號平均值-輸出值
初始化為0進行仿真
12 位數字(整數)乘以 0.1(我們的分頻器數字),結果為 -16。這給了我們 11 downto -16 的范圍。
?
signal?average?:?ufixed(11?downto?-16?)?:=?(others?=>?'0');
?
讓我們繼續看代碼——有一個同步復位的進程。在使用 AMD FPGA時,我們應該使用同步復位。
如果對模塊進行復位,那么將會對所有內容設置為零。
?
if?rising_edge(clk)?then? ????if?rst?=?'1'?then? ????????accumulator?<=?(others?=>?'0'); ????????average?<=?(others?=>?'0');
?
如果復位信號為“0”,則在時鐘的上升沿并且輸入有效 (ip_val) = 1,進行以下操作:
將獲取輸入值(作為標準邏輯向量出現),將其添加到當前累加器值
To_ufixed (ip, 7, 0) - VHDL 會將值從標準邏輯向量轉換為無符號定點
然后,因為它是滑動平均值,將從累加器中減去之前的平均值
?
elsif?ip_val?=?'1'?then? ????????accumulator?<=?resize?(arg?=>?(accumulator?+?to_ufixed(ip,7,0)-average(11?downto?0)),?size_res?=>?accumulator);
?
完成累加后,我們將累加器的值乘以"除法器"(乘法器實現的除法器)
?
average?<=?accumulator?*?divider;
?
最后我們輸出結果縮放平均值
?
op?<=?to_slv(average(7?downto?0));
?
仿真結果如下:
上面就簡單介紹了一個簡單的運算系統,該系統使用加法、減法和乘法代替除法。
然而,我們可能會面臨需要在 FPGA 中實現的更復雜的數學運算。
復雜算法
更復雜的算法可能具有挑戰性,例如用于將 PRT 電阻轉換為溫度的 Callendar-Van Dusen 方程:
上面的公式,簡單的運算我們知道怎么實現,但是平方根是一個新的運算,我們可以使用 CORDIC 算法實現平方根。
上面說的很簡單,但是實施起來卻很復雜,并且驗證過程中要捕獲所有極端情況會很耗時。
如果我們在感興趣的溫度范圍內繪制方程,我們可以采用更好的方法-使用多項式近似方式。
我們可以從此圖中提取趨勢線:
在 FPGA 中實現這個多項式方程比實現上面所示的復雜方程要容易得多。
我們需要做的第一件事是量化參數,為此,我們需要使用 Excel (MatLab也行)進行相關的數據處理。
通過上面的表格計算出量化參數,接下來就可以使用HDL實現算法。這次我們將使用二進制補碼的符號數字進行運算。
完整的實現如下所示
?
library?IEEE; use?IEEE.STD_LOGIC_1164.ALL; use?ieee.fixed_pkg.all; entity?complex_example?is?port( clk?:?in?std_logic;? ip_val?:?in?std_logic; ip?????:?in?std_logic_vector(7?downto?0); op_val?:?out?std_logic; op?????:?out?std_logic_vector(8?downto?0)); end?complex_example; architecture?Behavioral?of?complex_example?is type?fsm?is?(idle,?powers,?sum,?result); signal?current_state?:?fsm?:=?idle; signal?power_a?:?sfixed(35?downto?0):=(others=>'0'); signal?power_b?:?sfixed(26?downto?0):=(others=>'0'); signal?power_c?:?sfixed(17?downto?0):=(others=>'0'); signal?calc??:?sfixed(49?downto?-32)?:=(others=>'0'); signal?store?:?sfixed(8?downto?0)?:=?(others?=>'0'); constant?a?:?sfixed(9?downto?-32):=?to_sfixed(?2.00E-09,?9,-32?);? constant?b?:?sfixed(9?downto?-32):=?to_sfixed(?4.00E-07,?9,-32?); constant?c?:?sfixed(9?downto?-32):=?to_sfixed(?0.0011,?9,-32?);? constant?d?:?sfixed(9?downto?-32):=?to_sfixed(?2.403,?9,-32?);? constant?e?:?sfixed(9?downto?-32):=?to_sfixed(?251.26,?9,-32?);? begin cvd?:?process(clk) begin? ?if?rising_edge(clk)?then? ??op_val?<='0'; ??case?current_state?is? ???when?idle?=>? ????if?ip_val?=?'1'?then? ?????store?<=?to_sfixed('0'&ip,store); ?????current_state?<=?powers; ????end?if; ???when?powers?=> ????power_a?<=?store?*?store?*?store?*?store; ????power_b?<=?store?*?store?*?store; ????power_c?<=?store?*?store; ????current_state?<=?sum; ???when?sum?=> ????calc?<=?(power_a*a)?-?(power_b?*?b)?+?(power_c?*?c)?+?(store?*?d)?-?e; ????current_state?<=?result; ???when?result?=> ????current_state?<=?idle; ????op?<=?to_slv(calc(8?downto?0)); ????op_val?<='1'; ???end?case; ??end?if; end?process; end?Behavioral;
?
加下來我們簡單介紹下代碼:
時鐘 (clk) - 驅動模塊的主時鐘
Input Valid (ip_val) 和 Input (ip) - 表示我們在 8 位輸入上有有效數據(7 downto 0)
Output Valid (op_val) 和 Output (op) ?- 一旦我們運行了我們的算法,我們輸出一個 9 位數(8 downto 0)
?
entity?complex_example?is?port( clk?:?in?std_logic;? ip_val?:?in?std_logic; ip?????:?in?std_logic_vector(7?downto?0); op_val?:?out?std_logic; op?????:?out?std_logic_vector(8?downto?0)); end?complex_example;
?
接下來我們做的是為狀態機定義狀態,狀態機中有 4 個狀態:
Idle- 從第一個狀態開始等待輸入有效
Powers- 計算
Sum - 將所有的算子相加
Result- 將結果傳遞給下游模塊
?
type?fsm?is?(idle,?powers,?sum,?result);
?
在類型聲明之后是我們在設計中使用的信號
計算出的信號 a-c 當它們在算法中被提升到各自的冪時。
用于存儲每個結果的位數取決于輸入的大小和它們的冪次。首先要做的是將 8 位無符號數轉換為 9 位有符號數。然后對于 power_a,生成的向量大小是四次九位向量乘法,這意味著一個 36 位向量。對于 power_b,這是三個九位向量乘法等等。
?
signal?power_a?:?sfixed(35?downto?0):=(others=>'0'); signal?power_b?:?sfixed(26?downto?0):=(others=>'0'); signal?power_c?:?sfixed(17?downto?0):=(others=>'0');
?
計算結果-輸出值的計算結果位寬 -49 downto -32
Signal Store - 轉換為有符號數時我們存儲輸入的轉換
我們的輸入被轉換成什么。
這是一個 9 位存儲,代表 8 位加上一個符號位來組成 9 位
實際輸入永遠不會是負數,因為輸入代表一個大約在 70 歐姆到幾百歐姆之間的電阻值
?
signal?calc??:?sfixed(49?downto?-32)?:=(others=>'0'); signal?store?:?sfixed(8?downto?0)?:=?(others?=>'0');
?
常數 ae - 先前在 excel 中使用 Callendar-Van Dusen 方程計算的系數
to_sfixed 中的第一個值是電子表格中的值
第二個值是我們存儲值的整數位數。在本例中它是 9 位,因為常量需要達到 251.26 的值
第三個值是我們將使用的小數位數,它是 -32,由 2.00E-09 的最低常量值決定。定義小數位數是我們有效計算量化的地方
?
constant?a?:?sfixed(9?downto?-32):=?to_sfixed(?2.00E-09,?9,-32?);? constant?b?:?sfixed(9?downto?-32):=?to_sfixed(?4.00E-07,?9,-32?); constant?c?:?sfixed(9?downto?-32):=?to_sfixed(?0.0011,?9,-32?);? constant?d?:?sfixed(9?downto?-32):=?to_sfixed(?2.403,?9,-32?);? constant?e?:?sfixed(9?downto?-32):=?to_sfixed(?251.26,?9,-32?);?
?
在時鐘的上升沿,該過程將被執行。op_val 是默認賦值,除非在流程的其他地方將其設置為“1”,否則它將始終為“0”輸出。在進程中,信號被分配最后一個分配給它們的值,默認分配使代碼更具可讀性。
Idle狀態 -如果一個值進來(ip_val) = 1,那么
我們將值加載到存儲寄存器并將符號位設置為 0 (指示正數)
然后狀態機進入Power狀態。
?
if?ip_val?=?'1'?then? ?????store?<=?to_sfixed('0'&ip,store); ?????current_state?<=?powers; end?if;
?
Power 狀態- 在Power狀態下,我們計算三個冪運算,然后進入求和狀態。
Power a - 將存儲的值乘以自身 4 次。Power b - 將存儲的值乘以自身 3 倍Power c - 將存儲的值乘以自身 2 倍
?
when?powers?=> ????power_a?<=?store?*?store?*?store?*?store; ????power_b?<=?store?*?store?*?store; ????power_c?<=?store?*?store; ????current_state?<=?sum;
?
Sum狀態——這是我們將所有值輸入 excel 圖表的多項式近似值并進行加法和減法以獲得最終輸出值的地方。
請注意,無論我們的數字有多大,上面定義的常量都將小數點對齊到 -32 位,這對于計算總和很重要。
?
?calc?<=?(power_a*a)?-?(power_b?*?b)?+?(power_c?*?c)?+?(store?*?d)?-?e; ?current_state?<=?result;
?
Results狀態- 此處我們采用最終值(并將其作為標準邏輯向量放入輸出中。斷言 Op_val 表示存在新的輸出值)。
下面的仿真文件用來仿真這次的算法核心。
?
library?IEEE; use?IEEE.STD_LOGIC_1164.ALL; use?IEEE.NUMERIC_STD.ALL; entity?tb_complex?is --??Port?(?); end?tb_complex; architecture?Behavioral?of?tb_complex?is component?complex_example?is?port( clk????:?in?std_logic;? ip_val?:?in?std_logic; ip?????:?in?std_logic_vector(7?downto?0); op_val?:?out?std_logic; op?????:?out?std_logic_vector(8?downto?0)); end?component?complex_example; signal?clk????:??std_logic:='0';? signal?ip_val?:??std_logic:='0'; signal?ip?????:??std_logic_vector(7?downto?0):=(others=>'0'); signal?op?????:??std_logic_vector(8?downto?0):=(others=>'0'); signal?op_val?:??std_logic:='0'; constant?clk_period?:?time?:=?100?ns; begin clk?<=?not?clk?after?(clk_period/2); uut:?complex_example?port?map(clk,ip_val,ip,op_val,op); stim?:?process begin? ?wait?for?1?us; ?wait?until?rising_edge(clk); ?ip_val?<=?'1'; ?ip?<=?std_logic_vector(to_unsigned(61,8)); ?wait?until?rising_edge(clk); ?ip_val?<=?'0'; ?wait?for?1?us; ?report?"?simulation?complete"?severity?failure; end?process; end?Behavioral;
?
仿真結果如下:
總結
在這個項目中,從簡單算法到復雜算法,我們逐步了解了如何使用 HDL 在 FPGA 上進行數學運算。
審核編輯:劉清
評論
查看更多