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です。振幅が変化しているのが読み取れると思います。
-
前の記事
デジタル信号処理の固定小数点設計方法 2020.08.13
-
次の記事
VHDLで固定小数点の設計 2020.08.17

コメントを書く