如何在 FPGA 中做数学运算

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

FPGA 非常适合进行数学运算但是需要一点技巧所以我们今天就看看如何在 FPGA 中进行简单和复杂的数学运算。

7917196fc7422763e3bb6281471b6d36.jpeg

介绍

由于FPGA可以对算法进行并行化所以FPGA 非常适合在可编程逻辑中实现数学运算。我们可以在 FPGA 中使用数学来实现信号处理、仪器仪表、图像处理和控制算法等一系列应用。这意味着 FPGA 可用于从自动驾驶汽车图像处理到雷达和飞机飞行控制系统的一系列应用。

因为 FPGA 寄存器丰富并且包含专用乘法器累加器 (DSP48) 等功能所以在 FPGA 中实现数学运算需要一些技巧。

3e0c96d36465c2e23eb37620d4002d2a.png c8809efac12c97f7e34a5931b9bf9e71.png

这使它们成为实现定点数学运算的理想选择但是这与我们倾向于使用的浮点运算不同因此在进行浮点运算时候我们需要一点技巧。

定点数学运算

定点数的小数点位于向量中的固定位置。小数点左边是整数元素小数点右边是小数元素。这意味着我们可能需要使用多个寄存器来准确量化数字。幸运的是 FPGA 中的寄存器通常很多。

e6a63bc16ebaee7ae9cd6fd5b6ad59e6.png

相比之下浮点数可以存储比固定寄存器宽度例如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 个小数位结果就是 390061.4530986319x10^-4 x 2^28 的值存储在小数寄存器中。这给出了更准确的量化结果。

了解了量化的基础知识后下一步就是了解有关数学运算小数点对齐的规则。如果我们执行运算操作但是小数点没有对齐我们就不会得到正确的结果。

  • 加法——小数点必须对齐

  • 减法——小数点必须对齐

  • 除法——小数点必须对齐

  • 乘法——小数点不需要对齐

我们还需要考虑操作对结果向量的影响。不考虑结果的大小可能会导致溢出。下表显示了结果大小调整的规则。

3013b833cb96ca1b0968c3fd9cc88e79.png

假设我们有两个向量一个 16 位另一个 8 位在进行运算的时候将出现以下情况

  • C16  downto  0= A15  downto  0+ B7  downto  0

  • C16  downto  0= A15  downto  0- B7  downto  0

  • C22  downto  0= A15  downto  0* B7  downto  0

  • C8  downto -1= A15  downto 0/ B7  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));

仿真结果如下

87c20bebf716354a1d3c522fa8f58bd9.png

上面就简单介绍了一个简单的运算系统该系统使用加法、减法和乘法代替除法。

然而我们可能会面临需要在 FPGA 中实现的更复杂的数学运算。

复杂算法

更复杂的算法可能具有挑战性例如用于将 PRT 电阻转换为温度的 Callendar-Van Dusen 方程

9d507f79e05fe85055e3f1c0aba265bd.png

上面的公式简单的运算我们知道怎么实现但是平方根是一个新的运算我们可以使用 CORDIC 算法实现平方根。

上面说的很简单但是实施起来却很复杂并且验证过程中要捕获所有极端情况会很耗时。

如果我们在感兴趣的温度范围内绘制方程我们可以采用更好的方法-使用多项式近似方式。

e438ba9cec6275674b22b64977ba303b.png

我们可以从此图中提取趋势线

556bd160a13449787fabe255e9cff76c.png

在 FPGA 中实现这个多项式方程比实现上面所示的复杂方程要容易得多。

我们需要做的第一件事是量化参数为此我们需要使用 Excel (MatLab也行)进行相关的数据处理。

详细的表格

https://github.com/suisuisi/FPGATechnologyGroup/blob/main/complex_example.xlsx

通过上面的表格计算出量化参数接下来就可以使用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;

仿真结果如下

3bb00489e1014a4a080d9cce1443d6a7.png

总结

在这个项目中从简单算法到复杂算法我们逐步了解了如何使用 HDL 在 FPGA 上进行数学运算。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6