1. 引言
在利用Verilog寫數字信號處理相關算法的過程中往往需要對數據進行量化以及截位。而在實際項目中,一種比較精確的處理方式就是先對截位后的數據進行四舍五入(round),如果在四舍五入的過程中由於進位導致數據溢出,那么我們一般會對信號做飽和(saturation)處理。所謂飽和處理就是如果計算結果超出了要求的數據格式能存儲的數據的最大值,那么就用最大值去表示這個數據,如果超出了要求的數據格式能存儲的數據的最小值,那么就用最小值去表示這個數據。
為了敘述方便,做出如下規定:如果一個有符號數的總位寬為32位(其中最高位為符號位),小數位寬為16位,那么這個有符號數的數據格式記為32Q16。依此類推,10Q8表示這個數是一個有符號數(最高位為符號位),且總位寬為10位,小數位寬為8位。16Q13表示這個數是一個有符號數(最高位為符號位),且總位寬為16位,小數位寬為13位。總而言之,下文如果定義一個數據為mQn(m和n均為正數且m>n)格式,那么我們可以得到三個重要信息:
- mQn是一個有符號數,最高位為符號位;
- mQn數據的總位寬為m;
- mQn數據的小數位寬為n。
2. Verilog中有符號數據的補位與截位
2.1 有符號數與無符號數
有符號數指的就是帶有符號位的數據,其中最高位就是符號位(如果高位為0,那么表示是正數,如果最高位為1,那么表示是負數);無符號數就是不帶有符號位的數據。
考慮一個4位的整數"4'b1011",如果它是一個無符號數,那么它表示的值為:\(1\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0} = 11\)。如果它是一個有符號數,那么它表示的值為:\((-1)^{1}\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0} = -5\),所以相同的二進制數把它定義為有符號數和無符號數表示的數值大小有可能是不同的。同時,這里也告訴大家,有符號數和無符號數轉化為10進制表示的時候唯一區別就是最高位的權重不同,拿上例來說,無符號數最高位的權重是\(2^{3}\)而有符號數最高位的權重是\((-1)^{1}\times 2^{3}\)。
正因為有符號數和無符號數最高位的權重不同,所以它們所表示的數據范圍也是不同的。比如,一個4位無符號整數的數據范圍為0$\sim\(15,分別對應二進制4'b0000\)\sim\(4'b1111,而一個4位的有符號整數的數據范圍為-8\)\sim\(7,分別對應二進制4'b1000\)\sim$4'b0111。
擴展到一般情況,一個位寬為m的無符號整數的數據范圍為\([0, 2^{m}-1]\),而一個位寬為\(m\)的有符號整數的數據范圍為:\([-2^{m-1},2^{m-1}-1]\)。
2.2 有符號整數的符號位擴展
一個4位的有符號整數4'b0101,顯然由於最高位為0,它是一個正數,如果要把它擴展成6為,那么只需要在最前加2個0即可,擴展之后的結果為:6'b000101。
一個4位有符號整數為4'b1011,顯然由於最高位為1,它是一個負數,如果要把它擴展成6位,此時最前面添加的是兩個1,而非2個0,擴展之后的結果為:6'b11101。為了確保擴數據擴位以后沒有發生錯誤,做一個簡單驗證:
$ 4'b1011 = (-1)^{1}\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0}=-8+0+2+1=-5$
$ 6'b111011 = (-1)^{1}\times 2^{5}+1\times 2^{4} + 1\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0}=-32+16+8+2+1=-5$
顯然,擴位以后數據大小並未發生改變。
對一個有符號數進行擴位的時候為了保證數據大小不發生改變,擴位的時候應該添加的是符號位
2.3 有符號小數
有了前面兩小節的基礎,接下來研究一下有符號小數。前面已經規定了有符號小數的記法。
假設一個有符號小數為4'b1011,它的數據格式為4Q2,也就是說它的小數位為2位。那么看看這個數表示的十進制數是多少
顯然,小數的計算方法實際上和整數的計算方法是一樣的,只不過我們要根據小數點的位置確定對應的權重。
接下來看看有符號小數的數據范圍。就拿4Q2格式的數據來說,它的數據范圍為\([-2,2-\frac{1}{2^{2}}]\),分別對應二進制[4'b1000,4'b0111]。擴展到一般情況,mQn格式數據的范圍為\([-2^{m-n-1},2^{m-n}-\frac{1}{2^{n}}]\)。
最后再來看看有符號小數的數據擴展。假設一個有符號小數為4'b1011,它的數據格式為4Q2,現在要把這個數據用6Q3格式的數據存儲。顯然需要把整數部分和小數部分分別擴展一位。整數部分采用上一節提到的符號位擴展,小數部分則在最后添加一個0,擴展以后的結果為6'b110110,接下來仍然做一個驗證。
\(4'b10.11=1\times (-2^{1})+0\times 2^{0}+1\times 2^{-1}+1\times 2^{-2}=-2+0+0.5+0.25=-1.25\)
\(6'b110.110=1\times (-2^{2})+1\times 2^{1}+0\times 2^{0}+1\times 2^{-1}+1\times 2^{-2}+0\times 2^{-3}=-4+2+0+0.5+0.25+0=-1.25\)
顯然,擴位以后數據大小並未發生變化。
有符號小數進行擴位時整數部分進行符號位擴展,小數部分在末尾添0。
2.4 兩個有符號數的和
兩個有符號數相加,為了保證和不溢出,首先應該把兩個數據進行擴展使小數點對齊,然后把擴展后的數據繼續進行一位的符號位擴展,這樣相加的結果才能保證不溢出。
例子:現在要把5Q2的數據5'b100.01和4Q3的數據4'b1.011相加。
step1: 由於5Q2的數據小數位只有2位,而4Q3的數據小數點有3位,所以先把5Q2的數據5'b100.01擴展為6Q3的數據6'b100.010,使它和4Q3數據的小數點對齊;
step2: 小數點對齊以后,然后把4Q3的數據4'b1.011進行符號位擴展成6Q3的數據6'b111.011;
step3: 兩個6Q3數據相加,為了保證和不溢出,和應該用7Q3的數據來存儲。所以需要先把兩個6Q3的數據進行符號位擴展成7Q3的數據,然后相加,這樣才能保證計算結果是完全正確的。
以上就是兩個有符號數據相加需要做的一系列轉化。回過頭來思考為什么兩個6Q3的數據相加必須用7Q3的數據才能准確地存儲它們的和。因為6Q3格式數據的數據范圍為\([-4,4-1/2^{3}]\);那么兩個6Q3格式的數據相加和的范圍為\([-8,8-1/2^{2}]\);顯然如果和仍然用6Q3來存儲一定會溢出,而7Q3格式數據的數據范圍為[-8,8-1/2^{3}],因此用7Q3格式的數據來存兩個6Q3格式數據的和一定不會溢出。
在用Verilog做加法運算時,兩個加數一定要對齊小數點並做符號位擴展以后相加,和才能保證不溢出。
2.5 兩個有符號數的積
兩個有符號數相乘,為了保證積不溢出,積的總數居位寬為兩個有符號數的位寬之和,積的小數數據位寬為兩個有符號數的小數位寬之和。簡單來說,兩個4Q2數據相乘,要想保證積不溢出,積應該用8Q4格式來存儲。這是因為4Q2格式數據的范圍為:\([-2,2-1/2^{2}]\),那么兩個4Q2數據乘積的范圍為:\([-4+1/2,4]\),而8Q4格式的數據范圍為:\([-8,8-1/2^{4}]\),一定能夠准確地存放兩個4Q2格式數據的積。
mQn和aQb數據相乘,積應該用(m+a)Q(n+b)格式的數據進行存儲。
2.6 四舍五入(round)
前面講的都是對數據進行擴位,這一節說的是對數據截位時如何進行四舍五入以提高截位后數據的精度。
假設一個9Q6個數的數據為:9'b011.101101,現在只想保留3位小數位,顯然必須把最后三位小數位截掉,但是不能直接把數據截成6'b011.101,這樣是不精確的,工程上一般也不允許這么做,正確的做法是先看這個數據是正數還是負數,因為9'b011.101101的最高位為0,所以它是一個正數,然后再看截掉部分(此例中截掉部分是最末尾的101)的最高位是0還是1,在數據是正數的情況下,如果截掉部分的最高位為1,那么需要產生進位的,所以最終9'b011.101101應該被截成6'b011.110。
如果是負數則正好相反。假設一個9Q6格式數據為:9'b100.101101,由於最高位是1,所以這個數是一個負數,然后再看截斷部分的最高位以及除最高位的其他位是否有1,此例中截斷部分(截斷部分為末尾的101)的最高位為1,而除最高位以外的其他位也有為1的情況,由於負數最高位的權重為(\(-2^{2}\)),所以對於這種情況是不需要進位的,與正數不同的是,負數不進位是需要加1的。因此最終9'b100.101101應該被截成6'b100.110。
假設a是一個9Q6格式的數據,要求把小數點截成3位。下面是Verilog代碼:
assign carry_bit = a[8] ? (a[2]&(|a[1:0])):a[2];
assign a_round = {a[8],a[8:3]} + carry_bit;
上面代碼的第一行是通過判斷符號位a[8]和截斷部分數據特征來確定是否需要進位,如果a[8]是0,計算得到的carry_bit為1,則表示a是正數,且截斷是需要進位的;如果a[8]是1,計算得到的carry_bit為1,則表示a是負數,且截斷是不需要進位的,負數不進位需要加1。代碼第二行是為了保證進位后數據不溢出,所以擴展了符號位。
2.7 飽和(saturation)截位
所謂飽和處理就是如果計算結果超出了要求的數據格式能夠存儲的最大值,那么就用最大值去表示這個數據,如果結算結果超出了數據格式能夠存儲的數據的最小值,那么就用最小值去表示這個數據。
例1:有一個6Q3的數據為6'b011.111,現在要求用4Q2格式的數據去存儲它,顯然6'b011.111轉化為10進制如下:
\(6'b011.111 = 1*2^{1}+1*2^{0}+1*2^{-1}+1*2^{-2}+1*2^{-3}=3.875\)
而4Q2格式的數據能夠表示的數據的最大值為4'b01.11,轉化為10進制為1.75,因此4Q2格式的數據根本無法准確地存放3.875這個數據,這樣就是所謂的飽和情況。在這種情況下,飽和處理就是把超過了1.75的所有數據全部用1.75來表示,也就是說,6Q3的數據6'b011.111如果非要用4Q2格式的數據來存儲的話,在進行飽和處理的情況下最終的存儲結果為:4'b01.11。
例2:有一個6Q3的數據為6'b100.111,現在要求用4Q2格式的數據去存儲它,顯然6'b100.111轉化為10進制如下:
\(6'b100.111=1*(-2)^2+1*2^{-1}+1*2^{-2}+1*2^{-3}=-4+0.5+0.25+0.125=-3.125\)
而4Q2格式的數據能表示的數據的最小值為4'b10.00,轉化為10進制為-2,因此4Q2格式的數據根本無法准確的存放-3.125這個數據,這是另一中飽和情況。在這種飽和情況下,飽和處理就是小於-2的所有數據全部用-2來表示,也就是說,6Q3的數據6'b100.111,如果非要用4Q2格式的數據來存儲的話,在進行飽和處理的情況下最終的存儲結果為:4'b10.00。
3. 實例(a+b*c)
3.1 要求
假設a,b的數據格式均為16Q14,c的數據格式為16Q15,計算出s=a+b*c的值,其中s的數據格式為16Q14。在截位的過程中利用四舍五入(round)的方式保證數據精度,如果有溢出的情況,用飽和截位的方式進行處理。編寫完Verilog代碼以后利用Matlab產生a,b,c的數據對Verilog代碼進行仿真,並保證Matlab運算得到的數據和Verilog運算得到的數據全部相同。最后,有條件的利用VCS統計代碼覆蓋率(Code Coverage),確保條件覆蓋率(Condition Coverage)達到100%。
3.2 要求分析
- 先分析b*c。由於b的數據格式為16Q14,c的數據格式為16Q15,所以為了保證b*c的乘積不溢出,那么b*c的乘積的數據格式為(16+16)Q(14+15),即32Q29;
- 再分析加法。由於a的數據格式為16Q14,而b*c的積的數據格式為32Q29,所以相加之前要先把a擴展成32Q29格式的數據,又為了保證相加的結果不溢出,相加之前還要把兩個32Q29格式的數據進行1位符號位擴展成33Q29格式的數據以后再相加,相加得到的和的數據格式為33Q29。
- 最后,由於要求最終的結果為16Q14,所以需要把33Q29的數據截位為16Q14,如果出現數據溢出的情況,需要用飽和截位的方式進行處理。
3.3 代碼
module dsp
(
input wire clk ,
input wire rst_n ,
input wire signed [15:0] a , //16Q14
input wire signed [15:0] b , //16Q14
input wire signed [15:0] c , //16Q14
output wire signed [15:0] s //16Q14
);
/****************************************************
* regs to register the input
*****************************************************/
reg signed [15:0] ra_16q14;
reg signed [15:0] rb_16q14;
reg signed [15:0] rc_16q14;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
ra_16q14 <= 16'd0;
rb_16q14 <= 16'd0;
rc_16q14 <= 16'd0;
end
else
begin
ra_16q14 <= a;
rb_16q14 <= b;
rc_16q14 <= c;
end
end
/*******************************************************
* operation results
********************************************************/
wire signed [31:0] b_mul_c_32q29;
wire signed [32:0] s_33q29;
wire signed [31:0] a_32q29;
assign b_mul_c_32q29 = b * c;
assign a_32q29 = {a[15],a,{15{1'b0}}};
assign s_33q29 = {a[31],a_32q29} + {b_mul_c_32q29[31], b_mul_c_32q29};
/******************************************************
* round
*******************************************************/
wire carry_bit;
wire [18:0] s_19q14_round;
assign carry_bit = s_33q29[32] ? (s_33q29[14] & (|s_33q29[13:0])) : s_33q29[14];
assign s_19q14_round = {s_33q29[32],s_33q29[32:15]} + carry_bit;
assign s = (s_19q14_round[18:15]==4'b0000 || s_19q14_round[18:15]==4'b1111) ?
s_19q14_round[15:0] :
{s_19q14_round[18],{15{!s_19q14_round[18]}}};
endmodule
3.4 測試文件
`timescale 1ns / 1ps
module tb_dsp;
reg I_clk ;
reg I_rst_n ;
reg [15:0] I_a ;
reg [15:0] I_b ;
reg [15:0] I_c ;
wire [15:0] O_s ;
parameter C_DATA_LENGTH = 4096 ;
reg [15:0] M_mem_a[0:C_DATA_LENGTH - 1] ;
reg [15:0] M_mem_b[0:C_DATA_LENGTH - 1] ;
reg [15:0] M_mem_c[0:C_DATA_LENGTH - 1] ;
reg [13:0] R_mem_addr ;
reg R_data_vaild ;
reg R_data_vaild_t ;
dsp u_dsp
(
.I_clk,
.I_rst_n(I_rst_n),
.I_a(I_a), // 16Q14
.I_b(I_b), // 16Q14
.I_c(I_c), // 16Q15
.O_s(O_s)
);
initial begin
I_clk = 0 ;
I_rst_n = 0 ;
#67 I_rst_n = 1 ;
end
initial begin
$readmemh("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/a_16Q14.txt",M_mem_a);
$readmemh("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/b_16Q14.txt",M_mem_b);
$readmemh("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/c_16Q15.txt",M_mem_c);
end
always #5 I_clk = ~I_clk ;
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
I_a <= 16'd0 ;
I_b <= 16'd0 ;
I_c <= 16'd0 ;
R_mem_addr <= 14'd0 ;
R_data_vaild <= 1'b0 ;
end
else if(R_mem_addr == C_DATA_LENGTH )
begin
R_mem_addr <= C_DATA_LENGTH ;
R_data_vaild <= 1'b0 ;
end
else
begin
I_a <= M_mem_a[R_mem_addr] ;
I_b <= M_mem_b[R_mem_addr] ;
I_c <= M_mem_c[R_mem_addr] ;
R_mem_addr <= R_mem_addr + 1'b1 ;
R_data_vaild <= 1'b1 ;
end
end
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_data_vaild_t <= 1'b0 ;
else
R_data_vaild_t <= R_data_vaild ;
end
integer fid ;
initial begin
fid = $fopen("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/s_vivado.txt" , "w");
if(!fid)
begin
$display("**********************Can Not Openle*************************************");
$finish;
end
else
begin
$display("**********************Open Fileccess*************************************");
end
end
always @(posedge I_clk )
begin
if(R_data_vaild_t)
$fdisplay(fid,"%d",$signed(O_s));
else if(R_mem_addr == C_DATA_LENGTH)
begin
$fclose(fid) ;
$finish ;
end
end
endmodule
3.5 利用Matlab產生測試數據
Matlab里面又現成的量化數據的函數,它們分別是quantizer函數和quantize函數。其中quantizer函數用來產生量化格式,quantize函數用來調用quantizer函數的結果讀數據進行量化,具體用法,可以查看相關文檔。
產生a,b,c三個數據的Matlab代碼如下:
clear
clc
data_length = 4096 - 8 ; % 定義數據長度,其中排除8種邊界條件
a_min = -2 ; %a的數據格式為16Q14,所以它的最小值為-2
a_max = 2 - 1/(2^14) ; %a的數據格式為16Q14,所以它的最大值為2 - 1/(2^14)
b_min = -2 ; %b的數據格式為16Q14,所以它的最小值為-2
b_max = 2 - 1/(2^14) ; %b的數據格式為16Q14,所以它的最大值為2 - 1/(2^14)
c_min = -1 ; %c的數據格式為16Q15,所以它的最小值為-1
c_max = 1 - 1/(2^15) ; %c的數據格式為16Q15,所以它的最大值為1 - 1/(2^15)
% 產生4088個均勻分布在a、b、c最大值與最小值之間的隨機數
a_rand = a_min + (a_max - a_min)*rand(1,data_length) ;
b_rand = b_min + (b_max - b_min)*rand(1,data_length) ;
c_rand = c_min + (c_max - c_min)*rand(1,data_length) ;
% 產生8種邊界條件
a_boundary = [a_min a_min a_min a_min a_max a_max a_max a_max] ;
b_boundary = [b_min b_min b_max b_max b_min b_min b_max b_max] ;
c_boundary = [c_min c_max c_min c_max c_min c_max c_min c_max] ;
% 隨機數與邊界值組合成為待量化的數據
a = [a_boundary a_rand];
b = [b_boundary b_rand];
c = [c_boundary c_rand];
% 定義量化規則,根據題目要求量化需采用四舍五入與飽和截位的方式
quan_16Q14_pattern = quantizer('fixed','round','saturate',[16,14]);
quan_16Q15_pattern = quantizer('fixed','round','saturate',[16,15]);
quan_33Q29_pattern = quantizer('fixed','round','saturate',[33,29]);
% 把a、b、c三個數據按照要求進行量化
a_16Q14 = quantize(quan_16Q14_pattern,a);
b_16Q14 = quantize(quan_16Q14_pattern,b);
c_16Q15 = quantize(quan_16Q15_pattern,c);
% 計算a + b * c的值
s = a_16Q14 + b_16Q14 .* c_16Q15 ;
% 根據題目要求,s的數據格式為16Q14,所以這里把s量化為16Q14格式的數據
s_16Q14 = quantize(quan_16Q14_pattern,s);
% 把量化后的a、b、c變成整數方便寫入.txt文件中
a_integer = a_16Q14 * 2^14 ;
b_integer = b_16Q14 * 2^14 ;
c_integer = c_16Q15 * 2^15 ;
s_integer = s_16Q14 * 2^14 ;
% 由於在Verilog中測試激勵文件的系統調用$readmemh讀入的數據格式為16進制,所以
% 把數據寫入.txt文件中之前需要把數據轉化為補碼的格式,這樣負數才不會寫錯
a_complement = zeros(1,length(a_integer));
b_complement = zeros(1,length(b_integer));
c_complement = zeros(1,length(c_integer));
% 把量化后的a轉化為補碼
for i = 1:length(a_complement)
if(a_integer(i) < 0)
a_complement(i) = 2^16 + a_integer(i) ;
else
a_complement(i) = a_integer(i) ;
end
end
% 把量化后的b轉化為補碼
for i = 1:length(b_complement)
if(b_integer(i) < 0)
b_complement(i) = 2^16 + b_integer(i) ;
else
b_complement(i) = b_integer(i) ;
end
end
% 把量化后的c轉化為補碼
for i = 1:length(c_complement)
if(c_integer(i) < 0)
c_complement(i) = 2^16 + c_integer(i) ;
else
c_complement(i) = c_integer(i) ;
end
end
% 把量化后的a的補碼寫入txt文件
fid_a = fopen('a_16Q14.txt','w');
fprintf(fid_a, '%x\n', a_complement);
fclose(fid_a);
% 把量化后的b的補碼寫入txt文件
fid_b = fopen('b_16Q14.txt','w');
fprintf(fid_b, '%x\n', b_complement);
fclose(fid_b);
% 把量化后的c的補碼寫入txt文件
fid_c = fopen('c_16Q15.txt','w');
fprintf(fid_c, '%x\n', c_complement);
fclose(fid_c);
% 把量化后的s以整數形式寫入txt文件,方便和vivado計算的結果進行對比
fid_s = fopen('s_matlab.txt','w');
fprintf(fid_s, '%d\n', s_integer);
fclose(fid_s);
3.6 驗證代碼的正確性
clear
clc
filename1 = 's_matlab.txt' ;
filename2 = 's_vivado.txt' ;
% s_matlab = textread(filename1, '%d') ;
% s_vivado = textread(filename2, '%d') ;
% 把txt文件中的數據讀入並轉化為一維數組,上面2行注釋的代碼和下面6行代碼的作用是完全一樣的
% 由於Matlab目前的版本不推薦使用textread,所以我使用了textscan函數進行處理
fid1 = fopen(filename1, 'r');
fid2 = fopen(filename2, 'r');
s_matlab = textscan(fid1, '%d') ;
s_vivado = textscan(fid2, '%d') ;
s_matlab = cell2mat(s_matlab) ;
s_vivado = cell2mat(s_vivado) ;
count = 0 ;
% 對s_vivado.txt的數據與s_matlab.txt的數據進行對比
for i = 1:length(s_vivado)
if(s_vivado(i) == s_matlab(i))
msg = sprintf('s_vivado(%d) is equal s_matlab(%d), Verification Pass', i , i) ;
disp(msg) ;
count = count + 1 ;
else
msg = sprintf('s_vivado(%d) is not equal s_matlab(%d), Verification Fail', i , i) ;
disp(msg) ;
end
end
msg = sprintf('Total Pass Number is %d', count) ;
disp(msg) ;
