本文转载自: OpenFPGA微信公众号
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 位),在进行运算的时候将出现以下情况:
做除法时的 -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 包文件。
让我们一步一步地看一下这些文件以及它们在做什么:
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 -数组的下界
信号累加器 - 存储我们的值
signal accumulator : ufixed(11 downto 0) := (others => '0');
信号平均值-输出值
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,进行以下操作:
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也行)进行相关的数据处理。
详细的表格:
https://github.com/suisuisi/FPGATechnologyGroup/blob/main/complex_exampl...
通过上面的表格计算出量化参数,接下来就可以使用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;
加下来我们简单介绍下代码:
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 个状态:
type fsm is (idle, powers, sum, result);
在类型声明之后是我们在设计中使用的信号
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 - 转换为有符号数时我们存储输入的转换
signal calc : sfixed(49 downto -32) :=(others=>'0');
signal store : sfixed(8 downto 0) := (others =>'0');
常数 ae - 先前在 excel 中使用 Callendar-Van Dusen 方程计算的系数
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,那么
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 上进行数学运算。