数字运算的核心是加、减、乘、除四种基本操作。在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
2. Basic
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
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配置。
第一个配置界面为Channel Settings,如下图所示:
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,如下图所示:
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即可。
可以看到,对于输出模式为Remainder,数据输出的形式为整数商和余数;而Fractional ,输出的形式为整数商和小数。
|