![cover](https://img-blog.csdnimg.cn/img_convert/2f34ac565b0a4984a7686ac5c281d2d8.png)
基于FPGA的数字信号处理(17)--定点运算的实现实例(饱和Saturate与四舍五入Round)
基于FPGA的数字信号处理(17)--定点运算的实现实例(饱和Saturate与四舍五入Round)
目录
文章总目录点这里:《基于FPGA的数字信号处理》专栏的导航与说明
1、前言
在前面文章中,我们谈到了如何对一个定点数的整数和小数分布进行截位处理,对小数截位相当于四舍五入(或floor、ceil等其他方式),而对整数截位则相当于如何处理溢出,常见的办法有两种:Wrap和Saturate。Wrap相当于不对溢出进行处理,任何其从高位绕回到低位。Saturate则是进行饱和处理,将所有超出范围的数值都用最大值/最小值来表示。
今天我们来通过几个例子,看看在运算中具体要如果处理这些位扩展和截位的问题。
首先对定点数的格式做如下约定:
-
Qm.n 表示整体长度为m,小数部分长度为n的有符号定点数。例如,Q8.5表示 1bits符号位 + 2bits整数 + 5bits小数的有符号定点数
-
UQm.n表示整体长度为m,小数部分长度为n的无符号定点数。例如,UQ8.5表示 3bits整数 + 5bits小数的无符号定点数
2、例1:无符号定点数的加法
考虑两个无符号定点数的加法:c = a + b,它们的数据格式如下:
输入a为3位整数,4位小数,无符号,一共是7位
输入b为2位整数,3位小数,无符号,一共是5位
输出c为3为整数,1位小数,无符号,一共是4位,小数截位要求四舍五入
运算过程可以分为步骤:
a的格式是UQ7.4,b的格式是UQ5.3,二者的小数点没有对齐,所以无法相加,需要把b的格式变为UQ6.4(就是左移1位),才能将二者的小数点对齐,然后相加。整数部分其实也可以对齐(因为高位会自动补0,所以对齐不对齐都差不多,不过还是统一下吧),所以有
assign b_U7Q4 = {1'b0,b_U5Q3,1'b0};
然后做 a+b 的加法,但是为了防止和溢出,所以要把结果在它们的位宽扩展1位,即
assign c_U8Q4 = a_U7Q4 + b_U7Q4;
接着四舍五入(方法就不讲了,看前面的文章),同样的,为了仿真溢出,也把结果扩展1位,即
assign c_U6Q1 = c_U8Q4[7:3] + c_U8Q4[2];
然后对整数部分进行饱和处理,结果要求保留3位整数,而c_U6Q1共有5位整数,所以只要判断高两位有没有1存在就知道数据是否溢出了,如果溢出了,就锁定在4bits数的最大值即4'hf,如果没有溢出,就不动它,所以有:
assign c_U4Q1 = ( |c_U6Q1[5:4] ) ? 4'hf : c_U6Q1[3:0];
综上,RTL部分的代码如下:
module test(
input [6:0] a_U7Q4, //无符号数,整数3位,小数4位
input [4:0] b_U5Q3, //无符号数,整数2位,小数3位
output [3:0] c_U4Q1 //无符号数,整数3位,小数1位
);
wire [6:0] b_U7Q4;
wire [7:0] c_U8Q4;
wire [5:0] c_U6Q1;
assign b_U7Q4 = {1'b0,b_U5Q3,1'b0};
assign c_U8Q4 = a_U7Q4 + b_U7Q4;
//四舍五入
assign c_U6Q1 = c_U8Q4[7:3] + c_U8Q4[2]; //防止加1产生溢出,扩展一位
//饱和处理
assign c_U4Q1 = ( |c_U6Q1[5:4] ) ? 4'hf : c_U6Q1[3:0];
endmodule
验证的话,因为数据量也不大(4096个),可以把输入和输出都用matlab来生成,因为matlab有能直接处理定点数的函数 fi ,所以作为验证手段还是非常方便的。
matlab部分的代码:
%--------------------------------------------------
% 关闭无关内容
clear;
close all;
clc;
%--------------------------------------------------
% 确定a的数据范围和精度
a_U7Q4_min = 0;
a_U7Q4_max = 2^3-1/2^4;
a_U7Q4_step = 1/2^4;
% 确定b的数据范围和精度
b_U5Q3_min = 0;
b_U5Q3_max = 2^2-1/2^3;
b_U5Q3_step = 1/2^3;
%--------------------------------------------------
% 将a、b转换为对应格式的定点数
F = fimath('RoundingMethod','round'); % 确定舍入方式为四舍五入
a_U7Q4 = fi(a_U7Q4_min:a_U7Q4_step:a_U7Q4_max,0,7,4); % fi格式:fi(数据,符号,字长,小数长度)
b_U5Q3 = fi(b_U5Q3_min:b_U5Q3_step:b_U5Q3_max,0,5,3);
% 生成定点数c_U8Q4
for i = 1:length(a_U7Q4)
for k = 1:length(b_U5Q3)
c_U8Q4(k+(i-1)*length(b_U5Q3)) = a_U7Q4(i) + b_U5Q3(k);
end
end
%--------------------------------------------------
% 把c从U4Q4格式转成U4Q1格式,四舍五入和饱和截位都在这一步
c_U4Q1 = fi(c_U8Q4,0,4,1,F);
% 把a写入TXT文件中
fid_a = fopen('a_U7Q4_ref.txt','w');
for k = 1:length(a_U7Q4)
fprintf(fid_a, '%s\n', hex( a_U7Q4(k) ) );
end
fclose(fid_a);
%--------------------------------------------------
% 把b写入TXT文件中
fid_b = fopen('b_U5Q3_ref.txt','w');
for k = 1:length(b_U5Q3)
fprintf(fid_b, '%s\n', hex( b_U5Q3(k) ) );
end
fclose(fid_b);
% 把c写入TXT文件中
fid_c = fopen('c_U4Q1_ref.txt','w');
for k = 1:length(c_U4Q1)
fprintf(fid_c, '%s\n', hex( c_U4Q1(k) ) );
end
fclose(fid_c);
现在我们把所有正确的输入和输出分别存在三个文件里:<a_U7Q4_ref.txt>,<b_U5Q3_ref.txt>,<c_U4Q1_ref.txt>。然后写个TB文件,把这些输入和输出读取进去。把这些输入送到被测电路,然后将被测电路的输出和matlab的输出做比较就可以判断被测电路功能是否正确了。TB如下:
`timescale 1ns/1ns
module test_tb();
reg [6:0] a_U7Q4; //无符号数,整数3位,小数4位
reg [4:0] b_U5Q3; //无符号数,整数2位,小数3位
wire [3:0] c_U4Q1; //无符号数,整数3位,小数1位
integer i,j,cnt; //循环变量
reg [12:0] err; //错误统计计数器
reg [6:0] a_U7Q4_ref [0:127]; //将matlab生成的a_U7Q4数据保存到该memory中,做为标准输入
reg [4:0] b_U5Q3_ref [0:31]; //将matlab生成的b_U5Q3数据保存到该memory中,做为标准输入
reg [3:0] c_U4Q1_ref [0:4095]; //将matlab生成的c_U4Q1数据保存到该memory中,做为标准输出,与RTL输出进行比较
//从文件中读取数据写入到对应的MEM中
initial begin
$readmemh("G:/matlab_test/a_U7Q4_ref.txt",a_U7Q4_ref);
$readmemh("G:/matlab_test/b_U5Q3_ref.txt",b_U5Q3_ref);
$readmemh("G:/matlab_test/c_U4Q1_ref.txt",c_U4Q1_ref);
end
initial begin
a_U7Q4 = 0; //输入赋初值
b_U5Q3 = 0;
cnt = 0;
err = 0;
//遍历所有的输入,共128*32=4096个
for(i=0;i<128;i=i+1)begin
a_U7Q4 = a_U7Q4_ref[i]; //从matlab生成的文件里载入输入
for(j=0;j<32;j=j+1)begin
b_U5Q3 = b_U5Q3_ref[j]; //从matlab生成的文件里载入输入
#5;
if(c_U4Q1 != c_U4Q1_ref[cnt])begin //如果matlab的输出和RTL的输出不同
err = err + 1; //错误个数加1
$display("a_U7Q4:%b b_U5Q3:%b matlab output:%b RTL output:%b",a_U7Q4_ref[i],b_U5Q3_ref[j],c_U4Q1_ref[cnt],c_U4Q1);
end
cnt = cnt + 1;
end
end
#20
if(err == 0)
$display("pass");
else
$display("fail,there is %d errors",err);
$stop(); //结束仿真
end
//例化被测试模块
test test_inst(
.a_U7Q4 (a_U7Q4),
.b_U5Q3 (b_U5Q3),
.c_U4Q1 (c_U4Q1)
);
endmodule
仿真结束后,在窗口打印了仿真成功的信息:
同时观察波形,也会发现错误统计的个数为0:
3、例2:有符号定点数的加法
在例1的基础上,把输入和输出都改成有符号的定点数。两个有符号定点数的加法:c = a + b,它们的数据格式如下:
输入a为3位整数,4位小数,有符号,一共8位
输入b为2位整数,3位小数,有符号,一共6位
输出c为3为整数,1位小数,有符号,一共5位,小数截位要求四舍五入
运算过程可以其实和例1是一样的,只是由于负数的存在,所以要多考虑一些边界情况,步骤如下:
a的格式是Q8.4,b的格式是Q6.3,二者的小数点没有对齐和整数都没有对齐,所以无法相加,需要把b的格式变为Q8.4,然后相加。需要注意的是两个符号数数的位宽不一致只要把和的位宽扩展一位结果就不会有问题。但是两个有符号数的加法,只扩展结果的位宽是有可能出问题的,比如:
-1-4= -5,即 1111 + 100 = 10011,不扩位,那结果就是0011即3是错的,扩位后结果是10011即-5,是正确的。
-1+2=1,即 1111 + 010 = 10001,不扩位,那结果就是0001即1是对的,扩位后结果是10001即-15,却是错的。
为了防止上面这种情况,可以采用的办法是:把和的位宽扩展1位,同时也把两个加数的位宽扩展到跟和的位宽一致(注意高位补符号位)。比如:
-1-4= -5,即 1_1111 + 11_100 = 1_11011,结果只有5位:11011,即-5,正确。
-1+2=1,即 1_1111 + 00_010 = 1_00001,结果只有5位:00001,即1,正确。
把b的格式转换为Q8.4,有
assign b_8Q4 = {b_6Q3[5],b_6Q3,1'b0};
然后做 a+b 的加法,但是为了防止和溢出,所以要把结果在它们的位宽扩展1位,同时两个加数也要扩展1位符号位,即
assign c_9Q4 = {a_8Q4[7],a_8Q4} + {b_8Q4[7],b_8Q4};
接着四舍五入(方法就不讲了,看前面的文章),同样的,为了仿真溢出,也把结果扩展1位,即
assign carry = c_9Q4[8] ? (c_9Q4[2] && (|c_9Q4[1:0]) ) : c_9Q4[2]; assign c_7Q1 = {c_9Q4[8],c_9Q4[8:3]} + carry;
然后对整数部分进行饱和处理。c_7Q1比c_5Q1多了两位,如果数据没有溢出,那这两位肯定就是符号位,再加上c_5Q1的最高位,3个数一起构成了符号位,也就是只要判断c_7Q1的高三位是否为全1或全0即可。之所以可以这么推断是因为一个有符号数往高位扩展符号位,它的数值是不会改变的。比如:
-2的2bits补码是 10,3bits补码是 110 ,4bits补码是 1110······
1的2bits补码是 01,3bits补码是 001,4bits补码是 0001······
如果不为全0或全1则溢出了,需要将它饱和到最大值/最小值:
正向饱和到最大值,不同位宽的最大值分别是 01/011/0111/01111 等,即最高位是符号位0,其他位是全1
负向饱和到最大值,不同位宽的最大值分别是 10/100/1000/10000 等,即最高位是符号位1,其他位是全0
因为符号位就是c_7Q1的最高位,所以有:
assign c_5Q1 = ( c_7Q1[6:4] == 3'b000 || c_7Q1[6:4] == 3'b111) ? c_7Q1[4:0] : {c_7Q1[6],{4{~c_7Q1[6]}}};
综上,RTL部分的代码如下:
module test(
input [7:0] a_8Q4, //有符号数,符号1位,整数3位,小数4位
input [5:0] b_6Q3, //有符号数,符号1位,整数2位,小数3位
output [4:0] c_5Q1 //有符号数,符号1位,整数3位,小数1位
);
wire carry;
wire [7:0] b_8Q4;
wire [8:0] c_9Q4;
wire [6:0] c_7Q1;
assign b_8Q4 = {b_6Q3[5],b_6Q3,1'b0};
assign c_9Q4 = {a_8Q4[7],a_8Q4} + {b_8Q4[7],b_8Q4};
//四舍五入 .x xxx
assign carry = c_9Q4[8] ? (c_9Q4[2] && (|c_9Q4[1:0]) ) : c_9Q4[2];
assign c_7Q1 = {c_9Q4[8],c_9Q4[8:3]} + carry;
//饱和处理
assign c_5Q1 = ( c_7Q1[6:4] == 3'b000 || c_7Q1[6:4] == 3'b111) ? c_7Q1[4:0] : {c_7Q1[6],{4{~c_7Q1[6]}}};
endmodule
测试的话,依然用matlab来生成输入和输出,代码和上面差不多,只有部分修改,如下:
%--------------------------------------------------
% 关闭无关内容
clear;
close all;
clc;
%--------------------------------------------------
% 确定a的数据范围和精度
a_8Q4_min = -2^3;
a_8Q4_max = 2^3-1/2^4;
a_8Q4_step = 1/2^4;
% 确定b的数据范围和精度
b_6Q3_min = -2^2;
b_6Q3_max = 2^2-1/2^3;
b_6Q3_step = 1/2^3;
%--------------------------------------------------
% 将a、b转换为对应格式的定点数
F = fimath('RoundingMethod','round'); % 确定舍入方式为四舍五入
a_8Q4 = fi(a_8Q4_min:a_8Q4_step:a_8Q4_max,1,8,4); % fi格式:fi(数据,符号,字长,小数长度)
b_6Q3 = fi(b_6Q3_min:b_6Q3_step:b_6Q3_max,1,6,3);
%c_9Q4 = fi(zeros(1,256*64),1,9,4);
% 生成定点数c_9Q4
for i = 1:length(a_8Q4)
for k = 1:length(b_6Q3)
c_9Q4(k+(i-1)*length(b_6Q3)) = a_8Q4(i) + b_6Q3(k);
end
end
%--------------------------------------------------
% 把c从9Q4格式转成5Q1格式,四舍五入和饱和截位都在这一步
c_5Q1 = fi(c_9Q4,1,5,1,F);
% 把a写入TXT文件中
fid_a = fopen('a_8Q4_ref.txt','w');
for k = 1:length(a_8Q4)
fprintf(fid_a, '%s\n', hex( a_8Q4(k) ) );
end
fclose(fid_a);
%--------------------------------------------------
% 把b写入TXT文件中
fid_b = fopen('b_6Q3_ref.txt','w');
for k = 1:length(b_6Q3)
fprintf(fid_b, '%s\n', hex( b_6Q3(k) ) );
end
fclose(fid_b);
% 把c写入TXT文件中
fid_c = fopen('c_5Q1_ref.txt','w');
for k = 1:length(c_5Q1)
fprintf(fid_c, '%s\n', hex( c_5Q1(k) ) );
end
fclose(fid_c);
TB部分也差不多,稍微改了一些数值和变量名,如下:
`timescale 1ns/1ns
module test_tb();
reg [7:0] a_8Q4; //有符号数,符号1位,整数3位,小数4位
reg [5:0] b_6Q3; //有符号数,符号1位,整数2位,小数3位
wire [4:0] c_5Q1; //有符号数,符号1位,整数3位,小数1位
integer i,j,cnt; //循环变量
reg [12:0] err; //错误统计计数器
reg [7:0] a_8Q4_ref [0:255]; //将matlab生成的a_8Q4数据保存到该memory中,做为标准输入
reg [5:0] b_6Q3_ref [0:63]; //将matlab生成的b_6Q3数据保存到该memory中,做为标准输入
reg [4:0] c_5Q1_ref [0:16383]; //将matlab生成的c_5Q1数据保存到该memory中,做为标准输出,与RTL输出进行比较
//从文件中读取数据写入到对应的MEM中
initial begin
$readmemh("G:/matlab_test/a_8Q4_ref.txt",a_8Q4_ref);
$readmemh("G:/matlab_test/b_6Q3_ref.txt",b_6Q3_ref);
$readmemh("G:/matlab_test/c_5Q1_ref.txt",c_5Q1_ref);
end
initial begin
a_8Q4 = 0; //输入赋初值
b_6Q3 = 0;
cnt = 0;
err = 0;
//遍历所有的输入,共256*64=16384个
for(i=0;i<256;i=i+1)begin
a_8Q4 = a_8Q4_ref[i]; //从matlab生成的文件里载入输入
for(j=0;j<64;j=j+1)begin
b_6Q3 = b_6Q3_ref[j]; //从matlab生成的文件里载入输入
#5;
if(c_5Q1 != c_5Q1_ref[cnt])begin //如果matlab的输出和RTL的输出不同
err = err + 1; //错误个数加1
$display("a_8Q4:%b b_6Q3:%b matlab output:%b RTL output:%b",a_8Q4_ref[i],b_6Q3_ref[j],c_5Q1_ref[cnt],c_5Q1);
end
cnt = cnt + 1;
end
end
#20
if(err == 0)
$display("pass");
else
$display("fail,there is %d errors",err);
$stop(); //结束仿真
end
//例化被测试模块
test test_inst(
.a_8Q4 (a_8Q4),
.b_6Q3 (b_6Q3),
.c_5Q1 (c_5Q1)
);
endmodule
仿真结束后,在窗口打印了仿真成功的信息:
同时观察波形,也会发现错误统计的个数为0:
更多推荐
所有评论(0)