问答 店铺
热搜: ZYNQ FPGA discuz

QQ登录

只需一步,快速开始

微信登录

微信扫码,快速开始

微信扫一扫 分享朋友圈

已有 21 人浏览分享

开启左侧

第三章:四则运算的实现

[复制链接]
21 0
      数字运算的核心是加、减、乘、除四种基本操作。在FPGA中,所有运算均基于二进制进行,但其数学本质与运算规律与常规数字运算一致。只有深入掌握FPGA中符号数、小数表示及数据位扩展等关键设计方法,才能实现更复杂的数字信号处理算法。本章将具体介绍这些运算在FPGA中的实现方式。
1原理剖析
1.1 FPGA实现运算的硬件基础
      与CPU不同,FPGA不是执行一条条指令,而是将算法直接构建成硬件电路。用于数字运算的主要硬件资源有:
      查找表 (LUT - Look-Up Table): 这是FPGA最基本的逻辑单元。一个N输入的LUT可以实现任何N个输入的组合逻辑函数。对于小型运算(如4位加法器),可以直接用LUT来“查表”得出结果。
      进位链 (Carry Chain): 为了高效地实现加法,FPGA内部有专用的高速进位链,将LUT连接起来,快速传递进位信号。这使得构建宽位宽(如64位)的加法器速度远快于仅用普通逻辑。
      DSP Slice (或 DSP Block): 这是FPGA中的“大杀器”。现代FPGA内部集成了大量专用的数字信号处理(DSP)模块。一个典型的DSP Slice通常包含:
      一个硬件乘法器(例如 25x18位)。
      一个加法器/减法器/累加器。
      一些寄存器和控制逻辑。
使用DSP Slice进行乘法和乘加运算,速度极快,且不消耗宝贵的LUT资源。
1.2 数字的表示方法
      在FPGA中,如何表示数字直接影响了电路的复杂度和性能。
      无符号数 (Unsigned): 表示非负整数,所有位都用于表示数值。
      有符号数 (Signed): 通常使用二进制补码 (Two's Complement) 表示。这是硬件中最自然的表示方法,因为加法和减法可以用同一套电路实现(A - B 等价于 A + (-B))。
      定点数 (Fixed-Point): 这是在FPGA中表示小数最常用的方法。它本质上是一个整数,但设计者在头脑中“约定”了一个小数点的位置。例如,一个16位的数,可以约定高8位为整数部分,低8位为小数部分。
      优点: 运算电路和整数运算完全一样(加法、乘法),资源开销小,速度快。
      缺点: 表示的范围和精度是固定的,需要手动处理位宽和溢出问题。
      浮点数 (Floating-Point): 类似于软件中的 float 和 double,由符号位、阶码和尾数组成。
      优点: 表示范围广,精度高,使用灵活。
      缺点: 实现起来非常复杂,占用大量逻辑资源(LUTs和寄存器),除非FPGA有专用的硬核浮点单元,否则性能通常不如定点数。通常通过IP Core来实现。
      在FPGA设计中,除非必要,否则应优先考虑使用定点数。

2 FPGA实现:加法和减法
      实现: 这是最基础的运算。通常由FPGA的LUT和高速进位链构成。
      HDL代码: 在Verilog中,直接使用 + 和 - 运算符即可。综合工具会自动将其映射到最优的硬件资源上。
      关键点: 注意结果的位宽。两个N位数相加,结果最大可能是N+1位。必须为结果分配足够的位宽,否则会发生溢出 (Overflow)。另外,当有符号数与无符号数相加或相减时,它们不能直接运算,直接运算会产生错误的结果,对于这种情况,要统一操作数的数据类型,才能得出正确的结果。
// 假设a, b是8位有符号数
reg signed [7:0] a, b;
reg signed [8:0] sum; // 结果位宽要增加1位以防止溢出

always @(posedge clk) begin
    sum <= a + b;
end

3 FPGA实现:乘法
      实现:使用DSP Slice: 这是最高效、最推荐的方式。只要乘数的位宽符合DSP Slice的要求(例如,Xilinx 7系列是25x18),综合工具会自动推断并使用DSP Slice。
      使用LUT: 如果乘数位宽很小,或者设计中DSP资源已用尽,综合工具会使用LUT来构建乘法器。这种方式资源消耗大,且随着位宽增加,性能急剧下降。
      关键点: 乘法结果的位宽是两个乘数位宽之和(N*M 结果是 N+M位)。同样要统一操作数的数据类型。
3.1 直接运算
      对于FPGA的乘法,可以直接用乘法符号(*)来计算,需要注意的是进行直接运算时需要将操作数统一类型,否则会出错,操作数直接影响结果的数据的类型,而跟结果的数据类型的定义无关。
/*****************************************************************************            有符号数和无符号数相乘错误做法
******************************************************************************/
    // wire            [2:0]  a = 3'b011;//3
    // wire   signed     [2:0]  b = 3'b111;//-1

    // wire       [6:0] sum_1;   //21
    // wire signed  [6:0] sum_2;   //21

    // assign sum_1 = a * b;
    // assign sum_2 = a * b;

/******************************************************************************
            有符号数和无符号数相乘正确做法
******************************************************************************/
    wire   signed     [2:0]  a = 3'b011;//3
    wire   signed     [2:0]  b = 3'b111;//-1

    wire       [6:0] sum_1; //-3
    wire signed  [6:0] sum_2; //-3

    //统一操作数类型,统一为有符号数
    assign sum_1 = a * b;
    assign sum_2 = a * b;
/*********************************************/
    // wire         [2:0]  a = 3'b011;//3
    // wire         [2:0]  b = 3'b111;//7

    // wire       [6:0] sum_1; //21
    // wire signed  [6:0] sum_2; //21

    // //统一操作数类型,统一为无符号数
    // assign sum_1 = a * b;
    // assign sum_2 = a * b;

3.2 乘法器IP
1.乘法器IP介绍
选择Multiplier IP
image.jpg

2. Basic
image.jpg
image.jpg
Multiplier Type:乘法器的类型
  • Parallel Multiplier : 并行乘法器。输入两个并行变量相乘。
  • Constant Coefficient Multiplier:恒定系数乘法器。输入一个变量和设定的常数相乘。
Input Options:并行乘法器模式下
  • Data Type:Signed 二进制补码有符号数或 Unsigned二进制无符号数。
  • Width:操作数位宽。
  • Multiplier Construction: 选择该核的实现方式(LUT/专用乘数原语)。
  • Optimization Options:优化方式,Area Optimized资源优先或Speed Optimized速度优先。
Coefficient:恒定系数乘法器模式下
  • Constant Value (Integer):恒定系数的整数值,可正可负。
  • Memory Type:选择乘法器的内存类型(分布式RAM/块RAM/DSP片)

3 .Output and Control
image.jpg
Output Product Range:
  • Use Custom Output Width:自定义输出宽度。如果只需要取输出的某些位,可以通过MSB和LSB进行设置。
Pipelining and Control Signals:
  • Pipeline Stages: 选择流水线级数,可以看出该IP核是通过流水线实现乘法的,
Pipeline Stages = 0:表示核是组合的;
Pipeline Stages = 1:表示只寄存核输出;
Pipeline Stages > 1 :表示寄存器插入到输入和输出之间,直到最佳流水线级值。
  • Clock Enable:时钟使能,加一个时钟使能控制端CE。
  • Synchronous Clear:同步清除,加一个同步复位控制端SCLR。
  • SCLR/CE Priority:  CE和SCLR同时存在时,可以设置其优先级。
一般默认即可

4.演示demo

有符号数乘有符号数
Parallel Multiplier模式下,IP配置成有符号数乘有符号数,结果为位宽为48比特的有符号数-3。
/******************************************************************************
                  乘法IP:Multiplier:Parallel Multiplier模式
******************************************************************************/
//有符号数乘有符号数
  wire signed [23:0] A = 24'h000001; //1
  wire signed [23:0] B = 24'hFFFFFD; //-3
  wire signed [47:0] P;

  mult_gen_0 signed_signed (
    .CLK(I_sys_clk),  // input wire CLK
    .A(A),      // input wire [23 : 0] A
    .B(B),      // input wire [23 : 0] B
    .P(P)      // output wire [47 : 0] P
  );

Constant Coefficient Multiplier模式,IP配置成无符号数乘常数129(常数为有符号数),结果都为有符号数。
/******************************************************************************
           乘法IP:Multiplier:Constant Coefficient Multiplier模式
******************************************************************************/
  //有符号数乘常数129
  wire signed [23:0] A = 24'hFFFFFF; //-1
  wire signed [23:0] B = 24'h000001; //1
  wire        [31:0] P1;
  wire        [31:0] P2;

  mult_gen_0 signed_fushu (
    .CLK(I_sys_clk),  // input wire CLK
    .A(A),      // input wire [23 : 0] A
    .P(P1)      // output wire [31 : 0] P
  );

  mult_gen_0 signed_zhengshu (
    .CLK(I_sys_clk),  // input wire CLK
    .A(B),      // input wire [23 : 0] A
    .P(P2)      // output wire [31 : 0] P
  );

无符号数乘无符号数
Parallel Multiplier模式下,IP配置成无符号数乘无符号数,结果为位宽为48比特的无符号数3。
/******************************************************************************
                  乘法IP:Multiplier:Parallel Multiplier模式
******************************************************************************/
  //无符号数乘无符号数
  wire  [23:0] A = 24'h000001; //1
  wire  [23:0] B = 24'h000011; //3
  wire  [47:0] P;

  mult_gen_0 unsigned_unsigned (
    .CLK(I_sys_clk),  // input wire CLK
    .A(A),      // input wire [23 : 0] A
    .B(B),      // input wire [23 : 0] B
    .P(P)      // output wire [47 : 0] P
  );

无符号数乘有符号数
Parallel Multiplier模式下,IP配置成无符号数乘有符号数,结果为位宽为48比特的有符号数-3。
/******************************************************************************
              乘法IP:Multiplier:Parallel Multiplier模式
******************************************************************************/
  //无符号数乘有符号数
  wire       [23:0] A = 24'h000001; //1
  wire  signed [23:0] B = 24'hFFFFFD; //-3
  wire  [47:0] P;

  mult_gen_0 signed_unsigned (
    .CLK(I_sys_clk),  // input wire CLK
    .A(A),      // input wire [23 : 0] A
    .B(B),      // input wire [23 : 0] B
    .P(P)      // output wire [47 : 0] P
  )

Constant Coefficient Multiplier模式,IP配置成无符号数乘常数-3(常数为有符号数),结果为位宽为27比特的有符号数-3。
/******************************************************************************
           乘法IP:Multiplier:Constant Coefficient Multiplier模式
******************************************************************************/
  //无符号数乘常数-3
  wire        [23:0] A = 24'h000001; //1
  wire        [26:0] P;

  mult_gen_0 unsigned_constant (
    .CLK(I_sys_clk),  // input wire CLK
    .A(A),      // input wire [23 : 0] A
    .P(P)      // output wire [26 : 0] P
  );


3.3 移位实现
1.移位乘法实现(乘以2的幂次方)
有符号数的乘法可以使用算数左移(<<<)和逻辑左移(<<)实现,同样无符号数的乘法也可以使用算数左移和逻辑左移实现,以下代码仅适用于乘数为2n的情况。
/****************************************************************************\
                          移位乘法实现(乘以2的幂次方)
\****************************************************************************/
// 当乘数是2的n次方时,直接左移n位即可实现乘法。
// 结果位宽 = 输入位宽 + 移位位数(防止溢出)。
// 仅适用于乘数为2^n的情况。

/*********************************************/
//无符号数乘法:乘以8(即2^3)
wire  [7:0]  a = 8'b00000001 ;//1
wire  [10:0] result_1;   //10'b00000001000 (8)
wire  [10:0] result_2;   //10'b00000001000 (8)

assign result_1 = a <<<  3;// 算数左移
assign result_2 = a <<   3;// 逻辑左移

/*********************************************/
// //有符号数乘法:乘以8(即2^3)
//  wire  signed [7:0]  a = 8'b11111000 ;//-8
//  wire  signed [10:0] result_1;    //10'b11111000000 (-64)
//  wire  signed [10:0] result_2;    //10'b11111000000 (-64)

//  assign result_1 = a <<<  3;// 算数左移
//  assign result_2 = a <<   3;// 逻辑左移

2.通用移位乘法实现(任意常数)
对于乘数不是2的幂次方,可分解为多个2的幂次方之和,通过移位和加法实现,以下为参考代码。

/***********************************************************************\
            通用移位乘法实现(任意常数)
\***********************************************************************/
//若乘数不是2的幂次方,可分解为多个2的幂次方之和,通过移位和加法实现
//乘法结果位宽 = 输入位宽 + 移位位数(防止溢出)。
//适用于任意常数。
/*********************************************/
//无符号数乘法:乘以13(13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0),2^3 < 13 < 2^4,取四位
//位宽:8+4=12位(最大255*13=3315)
wire  [7:0]  a = 8'b00000001 ;//1
wire  [11:0] result_1;   //12'b000000001101 (13)
wire  [11:0] result_2;   //12'b000000001101 (13)

//算数左移
wire [10:0] shift3_1 =  a <<< 3;
wire [9:0]  shift2_1 =  a <<< 2;
assign result_1 = shift3_1 + shift2_1 + a;

//逻辑左移
wire [10:0] shift3_2 =  a << 3;
wire [9:0]  shift2_2 =  a << 2;
assign result_2 = shift3_2 + shift2_2 + a;
/*********************************************/
// //有符号数乘法:乘以13(13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0),2^3 < 13 < 2^4,取四位
// //位宽:8+4=12位(最大255*13=3315)
// wire  signed [7:0]  a = 8'b11111000 ;//-8
// wire  signed [11:0] result_1;   //12'hF98 (-104)
// wire  signed [11:0] result_2;   //12'hF98 (-104)

// //算数左移
// wire signed [10:0] shift3_1 =  a <<< 3;
// wire signed [9:0]  shift2_1 =  a <<< 2;
// assign result_1 = shift3_1 + shift2_1 + a;
// //逻辑左移
// wire signed [10:0] shift3_2 =  a << 3;
// wire signed [9:0]  shift2_2 =  a << 2;
// assign result_2 = shift3_2 + shift2_2 + a;


4 FPGA实现:除法
实现: FPGA中没有像乘法器那样的专用硬件除法器。除法是一个复杂且耗时的操作。
常见算法:
      迭代算法 (Shift-and-Subtract): 类似于手算除法,每个时钟周期计算一位结果。速度慢,但资源消耗少。
      查找表法 (LUT-based): 对于除数是常数或范围很小的情况,可以预计算结果存入ROM中。
      乘法器法 (Newton-Raphson): 通过迭代乘以一个因子来逼近倒数,再用乘法完成除法。速度快,但资源消耗大。
最佳实践:
      使用厂商提供的IP Core: Xilinx和Intel都提供高度优化的除法器IP核。你可以配置算法(资源优先或速度优先)、位宽和延迟。这是最推荐的方法。
      避免除法: 在算法层面,尽量将除法转换为乘法。例如,x / C (C是常数) 可以转换为 x * (1/C)。(1/C)可以预先计算好,作为一个定点数乘子。
      除以2的幂: 这可以简单地通过右移操作 >> 实现,几乎不消耗任何资。

除法IP
1.除法IP介绍
在vivado工程界面左栏点击 IP Catalog 搜索 Divider Generator,点击进入IP配置。
image.jpg
第一个配置界面为Channel Settings,如下图所示:
image.jpg
Common Options:
Algorithm Type:这个选项,一共有三种类型,分别是High Radix、LutMult、Radix2。用户可根据除数和被除数数位宽大小和延迟需求选择不同算法类型,具体介绍如下:
High Radix:带预缩放的高位基数除法。适用于操作数宽度超过约 16 位的情况。这种类型采用DSP块和Block RAMs来实现。
LutMult:适用于位宽小于或等于 12 位的情况。它的实现采用了 DSP 块、块式随机存取存储器以及少量的 FPGA 逻辑元件(寄存器和查找表)。
Radix2:适用于操作数宽度小于约 16 位的情况,或者适用于需要高吞吐量的应用程序。这种方式的实现不使用 DSP 或块 RAM 基本单元,因此当这些基本单元在其他地方有需求时,Radix2是推荐的选择。
Divivend Channel
Dividend Width: 设置被除数位宽,在不同的Algorithm Type下,支持的最大位宽不同。在High Radix类型和Radix2类型下,最大支持64位宽;在LutMult类型下,最大支持17位宽。
TLAST和TUSER端口,IP核不使用此端口信息,但会以与数据路径相同的延迟传输到输出通道,用户勾选后,可以设置TUSER端口宽度,一般忽略即可。
Divisor Channel
Divisor Width: 设置除数位宽,在不同的Algorithm Type下,支持的最大位宽不同。在High Radix类型和Radix2类型下,最大支持64位宽;在LutMult类型下,最大支持11位宽。
TLAST和TUSER端口,IP核不使用此端口信息,但会以与数据路径相同的延迟传输到输出通道,用户勾选后,可以设置TUSER端口宽度,一般忽略即可。
Output Channel
Remainder Type:设置余数类型。
设置为Remainder时,余数模式位数固定,由系统根据除数自动设定。输出格式为商+余数。
设置为Fractional时,可以在Fractional Width那一栏输入自己想要的余数位数。输出格式为商+小数。
如果勾选Detect Divide_By_Zero,则会多出一个m_axis_dout_tuser端口,以便在执行除0操作时发出信号。
第二个配置界面为Options,如下图所示:
image.jpg
Clocks Per Division: Radix-2模式下独有,用于确定吞吐量(输入或输出之间的时钟间隔)。此参数的值越低,吞吐量越高,但资源使用量也越大。
AXI4-Stream Options
Flow Control: Blocking or NonBlocking。
Blocking阻塞模式与NonBlocking非阻塞模式
Latency Configuration:
Latency Configuration: 支持手动或自动配置延迟。
Latency: 手动指定从输入到输出的延迟。

2.实验demo
参考demo,配置为Radix2模式,对比两种不同Remainder Type时,对输出数据的影响。代码顶层如下:
`timescale 1ns / 1ps

module divider(
    input                             aclk                    ,
    //Remainder
    input                             s_axis_divisor_tvalid_0    ,
    input                [  15: 0]      s_axis_divisor_tdata_0    ,
    input                             s_axis_dividend_tvalid_0  ,
    input                [  15: 0]      s_axis_dividend_tdata_0  ,
    output                            m_axis_dout_tvalid_0    ,
    output               [  31: 0]      m_axis_dout_tdata_0    ,
    //Fractional
    input                             s_axis_divisor_tvalid_1    ,
    input                [  15: 0]      s_axis_divisor_tdata_1    ,
    input                             s_axis_dividend_tvalid_1  ,
    input                [  15: 0]      s_axis_dividend_tdata_1  ,
    output                            m_axis_dout_tvalid_1    ,
    output               [  31: 0]      m_axis_dout_tdata_1         
  );


//配置IP输出模式为Remainder ,输出的形式为:余数、商
  div_gen_0 Remainder_16_16 (
    .aclk                           (aclk                   ),
    .s_axis_divisor_tvalid              (s_axis_divisor_tvalid_0   ),
    .s_axis_divisor_tdata              (s_axis_divisor_tdata_0    ),
    .s_axis_dividend_tvalid             (s_axis_dividend_tvalid_0 ),
    .s_axis_dividend_tdata             (s_axis_dividend_tdata_0  ),
    .m_axis_dout_tvalid               (m_axis_dout_tvalid_0    ),
    .m_axis_dout_tdata               (m_axis_dout_tdata_0     )
);

//配置IP输出模式为Fractional ,输出的输出的形式为:小数、商
  div_gen_1 Fractional_16_16_12 (
    .aclk                            (aclk                  ),
    .s_axis_divisor_tvalid              (s_axis_divisor_tvalid_1   ),
    .s_axis_divisor_tdata              (s_axis_divisor_tdata_1   ),
    .s_axis_dividend_tvalid            (s_axis_dividend_tvalid_1  ),
    .s_axis_dividend_tdata              (s_axis_dividend_tdata_1   ),
    .m_axis_dout_tvalid                (m_axis_dout_tvalid_1     ),
    .m_axis_dout_tdata                (m_axis_dout_tdata_1      )
);

endmodule

仿真代码如下:

`timescale 1ns / 1ps

module tb_top();

reg aclk;

reg s_axis_divisor_tvalid_0;
reg s_axis_divisor_tvalid_1;

reg s_axis_dividend_tvalid_0;
reg s_axis_dividend_tvalid_1;

reg [  15: 0] s_axis_divisor_tdata_0;
reg [  15: 0] s_axis_divisor_tdata_1;

  
reg [  15: 0] s_axis_dividend_tdata_0;
reg [  15: 0] s_axis_dividend_tdata_1;
  
wire m_axis_dout_tvalid_0;
wire m_axis_dout_tvalid_1;

wire [  31: 0] m_axis_dout_tdata_0;
wire [  31: 0] m_axis_dout_tdata_1;

wire [15:0] quotient_0;
wire [15:0] remainder;

wire [15:0] quotient_1;
wire [11:0] fractional;

//Remainder_16_16
assign quotient_0 =  m_axis_dout_tdata_0[31:16];
assign remainder  =  m_axis_dout_tdata_0[15:0];

//Fractional_16_16_12
//具体位宽看IP核的配置(Implementation Details)
assign quotient_1   = m_axis_dout_tdata_1[27:12];
assign fractional   = m_axis_dout_tdata_1[11:0];

initial begin
    aclk = 1'b1;
    forever #10 aclk = ~aclk;
end

initial begin
    s_axis_dividend_tdata_0 = 16'd0;
    s_axis_dividend_tvalid_0 = 1'b0;
    s_axis_divisor_tvalid_0 = 16'd0;
    s_axis_divisor_tdata_0 = 1'b0;

    s_axis_dividend_tdata_1 = 16'd0;
    s_axis_dividend_tvalid_1 = 1'b0;
    s_axis_divisor_tdata_1 = 16'd0;
    s_axis_divisor_tvalid_1 = 1'b0;

    # 40;
    s_axis_dividend_tdata_0 = 16'd12345;
    s_axis_dividend_tvalid_0 = 1'b1;
    s_axis_divisor_tdata_0 = 16'd100;
    s_axis_divisor_tvalid_0 = 1'b1;

    s_axis_dividend_tdata_1 = 16'd12345;
    s_axis_dividend_tvalid_1 = 1'b1;
    s_axis_divisor_tdata_1 = 16'd100;
    s_axis_divisor_tvalid_1 = 1'b1;
end

  divider  inst0 (
    .aclk(aclk),
    .s_axis_divisor_tvalid_0(s_axis_divisor_tvalid_0),
    .s_axis_divisor_tdata_0(s_axis_divisor_tdata_0),
    .s_axis_dividend_tvalid_0(s_axis_dividend_tvalid_0),
    .s_axis_dividend_tdata_0(s_axis_dividend_tdata_0),
    .m_axis_dout_tvalid_0(m_axis_dout_tvalid_0),
    .m_axis_dout_tdata_0(m_axis_dout_tdata_0),
    .s_axis_divisor_tvalid_1(s_axis_divisor_tvalid_1),
    .s_axis_divisor_tdata_1(s_axis_divisor_tdata_1),
    .s_axis_dividend_tvalid_1(s_axis_dividend_tvalid_1),
    .s_axis_dividend_tdata_1(s_axis_dividend_tdata_1),
    .m_axis_dout_tvalid_1(m_axis_dout_tvalid_1),
    .m_axis_dout_tdata_1(m_axis_dout_tdata_1)
  );

endmodule

仿真结果如下:
对于fractional信号,要用实数显示,右击信号选择Radix,点击Real settings,选择Fixed point,然后点击Singned,设置合适的Binary point,本项目这里我们设置11,Apply即可。
image.jpg
可以看到,对于输出模式为Remainder,数据输出的形式为整数商和余数;而Fractional ,输出的形式为整数商和小数。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

0

关注

10

粉丝

150

主题
精彩推荐
热门资讯
    网友晒图
      图文推荐
        
        • 微信公众平台

        • 扫描访问手机版