ども。AUDIYです。(雑)
先日、FPGAでの信号処理実装で必要になったので積分器と微分器をVerilogでコーディングしたのですが、これが色々とハマって奥が深かったので共有しておこうと思います。
足し続ければイイんでしょ?引き続ければイイんでしょ?
本題に入る前に、まずはコンピュータを用いた積分・微分(数値積分、数値微分)と解析学における積分・微分の違いを明確にしておく必要があります。
基本的には関数のx軸を一定区間ごとに区切り、区間ごとに出した長方形や台形の面積を足し合わせることで実現しています。
コンピュータで面積を出す場合など精度が要求される局面ではこの区間を細かくしたりなどの様々な工夫を施していますが、今回はオーディオ信号の積分なのでそこまでは考えないことにします。
微分は積分の逆なので、一定区間ごとの関数の数値を引いていけば基本的には実現できます。
時間信号の積分器についてはこちらがわかりやすいです。
足し続けると、引き続けると・・・・
しかし、論理回路で上記記事の積分器をそのまま実現しようとすると問題が生じます。
C言語などのプログラミング言語で整数の足し算引き算を実装されたことのある方なら心当たりがあるかもしれませんが、「オーバーフロー」という現象が発生します。
たとえば、4ビットの整数に1ずつ足していく場合を考えます。
0, 1, 2, 3, 4, ..., 12, 13, 14, 15, 0, 1, .....
16は2進数で10000なので、下4ビットの値が繰り返されることで15の次は急激に0になってしまいます。これでは積分器の意味を成しません。
実際にVerilogで「足すだけ」の積分器を書いてみましょう。
module INTEGRATOR(
CLK_I,
DATA_I,
DATA_O
);
parameter DATA_Bit_Length = 4; // Bit Length
input wire CLK_I;
input wire unsigned [DATA_Bit_Length-1:0] DATA_I;
output reg unsigned [DATA_Bit_Length-1:0] DATA_O = {DATA_Bit_Length{1'b0}};
/* wire/reg */
wire unsigned [DATA_Bit_Length-1:0] DATA_SUM;
/* RTL */
assign DATA_SUM = DATA_I + DATA_O;
always @ (negedge CLK_I) begin
// Output the data with Negative Edge
DATA_O <= DATA_SUM;
end
endmodule
以下はテストベンチです。
`timescale 1 ns / 1 ps
module INTEGRATOR_tb ();
reg CLK_I = 1'b0;
reg unsigned [3:0] DATA_I = 4'h1;
wire unsigned [3:0] DATA_O;
INTEGRATOR u1(
.CLK_I(CLK_I),
.DATA_I(DATA_I),
.DATA_O(DATA_O)
);
always begin
#10 CLK_I <= ~CLK_I;
end
/* Enable this section when use Icarus Verilog (iVerilog) */
/*
initial begin
$dumpfile("INTEGRATOR.vcd");
$dumpvars(0, INTEGRATOR_tb);
#20000 $finish;
end
*/
endmodule
ModelSimでRTLシミュレーションを実行してみます。
11, 12, 13, 14, 15, 0, 1, 2, ....となっています。
見にくいので波形の表示方法を変えてみましょう。
これがオーバーフローです。
微分器でも同様の事象が起こりえます。HDLに落とし込む際はこれを防ぐような処理を追加しないといけません。
では、どうするか?
まず、オーバーフローが発生する状況を想像してみます。
例えば、符号なし4bit同士の計算だと、
4'b1111 + 4'b0001 = 5'b10000 (15 + 1 = 16)
で、下4桁がそのまま出力になるのでここでオーバーフローが発生します。
つまり、オーバーフローの発生条件は「bit幅で表現できる数値の範囲を計算結果が超過したとき」となります。
出力のビット幅を無限に確保できれば良いですがそういうわけにもいきません。
そこででてくる処理が「飽和処理」(サチュレーション、クリッピング)と呼ばれるものです。
飽和処理
飽和処理とは「数値が表現可能な最大値・最小値を超える場合は最大値、最小値を保持する」処理です。
同じく4ビット符号無しでの処理を考えます。
0, 1, 2, ....., 12, 13, 14, 15, 15, 15, 15, 15, ...........
と、積分結果が15を超える場合は15を維持します。
この「15を超える」というのが重要で、今回の積分器では結果が15を超えたことを検出せねばいけません。これは「一時的に計算結果を5ビットで保存する必要がある」ということです。
検出条件
5ビット幅の符号なし整数で「15を超えたことを検出する」ということは「数値が16以上である」ことを検出すればよいわけです。
5ビット符号なし整数で16は5'b10000となり、HDLでは「数値ビットの最上位が1」が検出条件になります。
実際に確認する
Verilogコードを以下のように書き直して再度シミュレーションします。
module INTEGRATOR(
CLK_I,
DATA_I,
DATA_O
);
parameter DATA_Bit_Length = 4; // Bit Length
input wire CLK_I;
input wire unsigned [DATA_Bit_Length-1:0] DATA_I;
output reg unsigned [DATA_Bit_Length-1:0] DATA_O = {DATA_Bit_Length{1'b0}};
/* wire/reg */
wire unsigned [DATA_Bit_Length:0] DATA_SUM;
/* RTL */
assign DATA_SUM = DATA_I + DATA_O;
always @ (negedge CLK_I) begin
if (DATA_SUM[DATA_Bit_Length] == 1'b1) begin
/* Saturation */
// Ex. 4'b1111 + 4'b0001 = 4'b1111 (5'b10000)
DATA_O <= {1'b1, {(DATA_Bit_Length-1){1'b1}}};
end else begin
/* Normal Operation */
DATA_O <= DATA_SUM[DATA_Bit_Length-1:0];
end
end
endmodule
always文の中にif文で飽和処理の判定を加えます。
テストベンチはそのままで再度ModelSimでシミュレーションします。
これにて飽和処理は追加できました。今回の例では符号なしですが、符号付きの場合は・・・・少々考えてみると面白いと思います。
あと、今回の例では最小値方向の飽和処理は記述していません。
せっかくなので・・・
積分からの出力を受け取るモジュールなどがこのオーバーフローに合わせて処理(リセットなど)できるように、積分器から飽和処理のフラグを出してあげたいところです。
そこで、OFDET_O(OverFlow DETection Output)という出力を追加し、飽和処理が実行されている間論理値1を出力します。
module INTEGRATOR(
CLK_I,
DATA_I,
DATA_O,
OFDET_O
);
parameter DATA_Bit_Length = 4; // Bit Length
input wire CLK_I;
input wire unsigned [DATA_Bit_Length-1:0] DATA_I;
output reg unsigned [DATA_Bit_Length-1:0] DATA_O = {DATA_Bit_Length{1'b0}};
output reg OFDET_O = 1'b0;
/* wire/reg */
wire unsigned [DATA_Bit_Length:0] DATA_SUM;
/* RTL */
assign DATA_SUM = DATA_I + DATA_O;
always @ (negedge CLK_I) begin
if (DATA_SUM[DATA_Bit_Length] == 1'b1) begin
/* Saturation */
// Ex. 4'b1111 + 4'b0001 = 4'b1111 (5'b10000)
DATA_O <= {1'b1, {(DATA_Bit_Length-1){1'b1}}};
OFDET_O <= 1'b1;
end else begin
/* Normal Operation */
DATA_O <= DATA_SUM[DATA_Bit_Length-1:0];
OFDET_O <= 1'b0;
end
end
endmodule
テストベンチにもOFDET_Oを追加し、確認します。
`timescale 1 ns / 1 ps
module INTEGRATOR_tb ();
reg CLK_I = 1'b0;
reg unsigned [3:0] DATA_I = 4'h1;
wire unsigned [3:0] DATA_O;
wire OFDET_O;
INTEGRATOR u1(
.CLK_I(CLK_I),
.DATA_I(DATA_I),
.DATA_O(DATA_O),
.OFDET_O(OFDET_O)
);
always begin
#10 CLK_I <= ~CLK_I;
end
/* Enable this section when use Icarus Verilog (iVerilog) */
/*
initial begin
$dumpfile("INTEGRATOR.vcd");
$dumpvars(0, INTEGRATOR_tb);
#20000 $finish;
end
*/
endmodule
最初の15(4'b1111)から次の15(4'b1111→飽和処理スタート)からOFDET_OがHになっています。
今回の積分器・微分器のVerilog記述を通じて上記のような考えることが多々あったので、今回ブログにまとめてみました。
実際に符号付きに拡張したものや微分器については下記GitHubリンク内にテストベンチとともに保存していますので、ぜひ確認していただければと思います。
また、最近は一度Verilogで記述したRTLをVHDLに書き直すことにハマっていまして、両方とも記述できる人間になりたいと思っています。
コードレビュー大歓迎です。
ではまた。