Verilogで固定小数点の設計

Verilogで固定小数点の設計

 こちらで固定小数点の解説をしたので、実際にVerilogで設計してみます。

 ソースコードは短いのですが、今回は検証の方が大きくなっています。

//  TITLE:					"multiQ14.v"
//  MODULE NAME:			
//  PROJECT CODE:			
//  AUTHOR:					 (****@nakaharagiken.com)
//  CREATION DATE:			2020.8.14
//  SOURCE:            		
//  LICENSE:           		Copyright (c) 2020 nakaharagiken All rights reserved. 
//  DESCRIPTION:            
//  NOTES TO USER:			Q14フォーマットどうしの乗算器
//  SOURCE CONTROL:			
//  REVISION HISTORY:  	    v0.1	2020.8.14	First edition
//
//							丸めは上位のブロックで行う事
//							Q14 = 2^14 * flortingdata
//
//              15,14,13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
//               |  |^<--------------小数点以下 -------------->
//               |  ||------ 小数点
//               |  |------- 整数
//               |---------- ± 1=負
//
//             (15)14*13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
//      ×)   (15)14*13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0(13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
//     ---------------------------------------------------------------------------------------------------------------
//        31,30,29,28*27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
//
//
`timescale 1ns/1ps


module multiQ14(
	indatA,		// (i) [15:0] Q14 format input
	indatB,		// (i) [15:0] Q14 format input
	outdat		// (o) [29:0] Q28 format output
);

input	[15:0]	indatA;				// 
input	[15:0]	indatB;				// 
output	[29:0]	outdat;				// 

wire	[31:0]  multi;

assign	multi = $signed(indatA) * $signed(indatB);
assign outdat =	multi[29:0];

endmodule

 乗算しているのは43行目だけです。符号付きを示す$signedを付けて乗算式を書いているだけなので、悩むことは無いでしょう。乗算の場合には桁数(bit数)が増えますので注意してください。丸めについては、テストベンチで解説します。

 テストベンチは少しアナログっぽい事をしたいので、Sin波形のROMを作って、その波形をアンプすることにします。倍率は0.1~1倍。それと反転もできるので、-0.1~-1倍。ここまで一気にシミュレートします。

 まずはSin波形のROMを作ります。

//  TITLE:					"SIN_ROM.v"
//  MODULE NAME:			
//  PROJECT CODE:			
//  AUTHOR:					 (****@nakaharagiken.com)
//  CREATION DATE:			2020.8.14
//  SOURCE:            		
//  LICENSE:           		Copyright (c) 2020 nakaharagiken All rights reserved. 
//  DESCRIPTION:            
//  NOTES TO USER:			Q14フォーマットSIN波形
//  SOURCE CONTROL:			
//  REVISION HISTORY:  	    v0.1	2020.8.14	First edition
//							
//							sin data 10Msps 500kHz
//

`timescale 1ns/1ps

module SIN_ROM(
	clock,					// (i)
	nreset,					// (i)							__reset___|~~normal~~~
	enable,					// (i)			イネーブル		_____|~|_________
	address,				// (i) [4:0]	アドレス
	Sindata					// (o) [15:0] 	Sin波形
);

input			clock;		// (i)
input			nreset;		// (i) 			リセット
input			enable;		// (i)			イネーブル
input	[4:0]	address;	// (i) [4:0]	アドレス
output	[15:0]	Sindata;	// (o) [15:0] 	Sin波形

reg		[15:0]	rSindata;

// sin data 10Msps 500kHz
parameter [15:0]	ROM0   = 16'h0000;
parameter [15:0]	ROM1   = 16'h0FD2;
parameter [15:0]	ROM2   = 16'h1E18;
parameter [15:0]	ROM3   = 16'h296B;
parameter [15:0]	ROM4   = 16'h30B1;
parameter [15:0]	ROM5   = 16'h3333;
//	parameter [15:0]	ROM6   = 16'h30B1;	ROM4と同じ
//	parameter [15:0]	ROM7   = 16'h296B;	ROM3と同じ
//	parameter [15:0]	ROM8   = 16'h1E18;	ROM2と同じ
//	parameter [15:0]	ROM9   = 16'h0FD2;	ROM1と同じ
//	parameter [15:0]	ROM10  = 16'h0000;	ROM0と同じ
parameter [15:0]	ROM11  = 16'hF02E;
parameter [15:0]	ROM12  = 16'hE1E8;
parameter [15:0]	ROM13  = 16'hD695;
parameter [15:0]	ROM14  = 16'hCF4F;
parameter [15:0]	ROM15  = 16'hCCCD;
//	parameter [15:0]	ROM16  = 16'hCF4F;	ROM14と同じ
//	parameter [15:0]	ROM17  = 16'hD695;	ROM13と同じ
//	parameter [15:0]	ROM18  = 16'hE1E8;	ROM12と同じ
//	parameter [15:0]	ROM19  = 16'hF02E;	ROM11と同じ


always @(posedge  clock, negedge nreset) begin
	if (!nreset) begin
		rSindata <= ROM0;
	end else begin
		if (enable == 1'b1) begin
			case (address)
				5'h00	:	begin
								rSindata <= ROM0;
							end
				5'h01	:	begin
								rSindata <= ROM1;
							end
				5'h02	:	begin
								rSindata <= ROM2;
							end
				5'h03	:	begin
								rSindata <= ROM3;
							end
				5'h04	:	begin
								rSindata <= ROM4;
							end
				5'h05	:	begin
								rSindata <= ROM5;
							end
				5'h06	:	begin
								rSindata <= ROM4;
							end
				5'h07	:	begin
								rSindata <= ROM3;
							end
				5'h08	:	begin
								rSindata <= ROM2;
							end
				5'h09	:	begin
								rSindata <= ROM1;
							end
				5'h0a	:	begin
								rSindata <= ROM0;
							end
				5'h0b	:	begin
								rSindata <= ROM11;
							end
				5'h0c	:	begin
								rSindata <= ROM12;
							end
				5'h0d	:	begin
								rSindata <= ROM13;
							end
				5'h0e	:	begin
								rSindata <= ROM14;
							end
				5'h0f	:	begin
								rSindata <= ROM15;
							end
				5'h10	:	begin
								rSindata <= ROM14;
							end
				5'h11	:	begin
								rSindata <= ROM13;
							end
				5'h12	:	begin
								rSindata <= ROM12;
							end
				5'h13	:	begin
								rSindata <= ROM11;
							end
				default:begin
								rSindata <= ROM0;
						end
			endcase
		end else begin
			rSindata <= ROM0;
		end
	end
end

assign	Sindata = rSindata;

endmodule

 このROMはSin波形を20個サンプリングしている状態です。ご存じのようにSin波形は繰り返し波形なので、同じ値が半分ありますので、そこは省略してリソースを節約しました。41~45行目、51~54行目のコメントを読んでください。

 このようなROM波形を作る時、表計算ソフトで計算するとQフォーマットまで作れますので便利です。

 次にテストベンチです。

//  TITLE:					"TB_multiQ14.v"
//  MODULE NAME:			TB_multiQ14
//  PROJECT CODE:			
//  AUTHOR:					 (****@nakaharagiken.com)
//  CREATION DATE:			2020.8.14
//  SOURCE:            		
//  LICENSE:           		Copyright (c) 2020 nakaharagiken All rights reserved. 
//  DESCRIPTION:            multiQ14.vのテストベンチ
//  NOTES TO USER:			
//  SOURCE CONTROL:			
//  REVISION HISTORY:  	    v0.1	2020.8.14	First edition
//
//
`timescale 1ns/1ps

module TB_shift;

parameter SYSCLK_PERIOD = 100;		// 10MHz


reg				i_clock;					// system clock
reg				i_nreset;					// asynchronous reset 				____reset___|~~~normal~~~
reg				i_enable;					// イネーブル
reg		[4:0]	raddress;					// アドレス
reg		[15:0]	i_coeff;					// 掛け算
wire	[15:0]	w_sindata;					// Sin波形
wire	[29:0]	o_Multiout;					// 出力信号
wire	[15:0]	w_Round;					// 丸め出力

//////////////////////////////////////////////////////////////////////
// Clock Driver
//////////////////////////////////////////////////////////////////////
always @(i_clock)
    #(SYSCLK_PERIOD / 2.0) i_clock <= !i_clock;


//////////////////////////////////////////////////////////////////////
// make address
//////////////////////////////////////////////////////////////////////
always @(posedge  i_clock) begin
	if (raddress < 5'h13) begin
		raddress <= raddress + 1;
	end else begin
		raddress <= 0;
	end
end
//////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////
initial
begin

	i_clock <= 0;
	i_nreset <= 0;									//	初期値
	i_enable <= 0;
	i_coeff <= 0;
	raddress <= 0;

	repeat(50) @(posedge i_clock);
	i_nreset <= 1;
	repeat(100) @(posedge i_clock);

	i_enable <= 1'b1;
	i_coeff <= 16'h0666;				// x0.1
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h0666;				// x0.1
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h0CCC;				// x0.2
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h1333;				// x0.3
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h1999;				// x0.4
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h2000;				// x0.5
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h2666;				// x0.6
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h2CCC;				// x0.7
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h3333;				// x0.8
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h3999;				// x0.9
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'h4000;				// x1.0
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hF99A;				// x-0.1
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hF334;				// x-0.2
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hECCD;				// x-0.3
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hE667;				// x-0.4
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hE000;				// x-0.5
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hD99A;				// x-0.6
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hD334;				// x-0.7
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hCCCD;				// x-0.8
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hC667;				// x-0.9
	repeat(100) @(posedge i_clock);
	i_coeff <= 16'hC000;				// x-1.0

end


multiQ14 inst_multiQ14(
	.indatA		(w_sindata),		// (i) [15:0] Q14 format input
	.indatB		(i_coeff),			// (i) [15:0] Q14 format input
	.outdat		(o_Multiout)		// (o) [29:0] Q28 format output
);

SIN_ROM inst_SIN_ROM(
	.clock		(i_clock),					// (i)
	.nreset		(i_nreset),					// (i)							__reset___|~~normal~~~
	.enable		(i_enable),					// (i)			イネーブル		_____|~|_________
	.address	(raddress),					// (i) [4:0]	アドレス
	.Sindata	(w_sindata)					// (o) [15:0] 	Sin波形
);

//        29,28*27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
//        15,14*13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
assign	w_Round = o_Multiout[29:14] + {15'b000000000000000,o_Multiout[13]};		// 実際に使う時にはラッチすること

endmodule

 テストベンチも難しい事はしていません。40行目でアドレスカウンタをまわして、ROMのデータを延々に読んでいます。ROMのデータはw_sindataに入りますので、i_coeffと乗算を行います。i_coeffは0.1~1および-0.1~-1まで変化させています。

 丸めについてはテストベンチの125行目で行っています。仮に丸めをおこなうとすればこうします。という事になります。今回は乗算の入力が16ビットだったので、出力も合わせて16ビットにします。123行目のコメントが分かりやすいと思います。乗算で得られた30bitのうち、MSBから16bitを使います。この場合、o_Multiout[13]のビットが丸め対象になりますので、このビットをo_Multiout[14]の桁に加算しています。これで四捨五入(零捨一入)になります。四捨五入の定理に従って、条件分けしなくても2進数なのでこれでOKです。

 FPGAで乗算というと、最初はどうすれば良いのかわからない。という人が多いのですが、このようにやり方を見てしまえば、難しくありません。

 乗算は桁数が増える。という事は、ロジック的に非常に複雑な回路になります。HDLで単純に乗算記号の1行で書いてしまいましたが、回路規模は大きく速度も遅くなります。したがって、各社DSPをFPGAに搭載して乗算・加算の専用ハードウェアで速度を上げるという事をしています。各社のコンパイラでは、この乗算のHDL1行を専用DSPに割り当ててくれますので、DSPの仕様書をよく読んで、DSPのビット数と乗算の回路規模を想像しながら設計するようにします。

 まずは、このような回路を組んでコンパイルしたなら、コンパイルレポートを読んでみると良いでしょう。いくつDSPが割り当てられたのかをレポートしているはずです。

 最後に、本ブログページの冒頭のアイキャッチ画像はModelSimでシミュレートした出力波形です。下2つのアナログ波形が乗算後の波形o_Multioutとw_Roundです。振幅が変化しているのが読み取れると思います。