2018年03月25日

ΣΔDAC (2) オーバーサンプリング

ΣΔDAC (1) の続き

ΣΔDAC はできたとして、普通のサウンドDAC として使うには、オーバーサンプリングが必須である。それをするためには、デジタルフィルタ(LPF) が必要だそうだ。もとのサンプリングデータに 0 のデータを挿入したものに LPF をかけると、綺麗に補間されたデータが出来るとのこと。デジタルフィルタには FIR フィルタというものを使う。この FIR フィルタを作ってみよう。

おさらい

    14.314 MHz から作った 45.8 MHz を 16 x 65 で分周して 44.056 kHz を作る。オーバーサンプリングは 65 で 16 までのタップ数の FIR フィルタを使う。

    もし NTSC 画面表示と同時に使うのであれば、44.056 kHZ の 15/16 にサンプリング周波数を下げる。
    (外部 PLL が使えれば、44.056 kHZ も可能になる。クロックが変わるだけで、コードは変えない。 )

    NTSC 画面表示は、14.314 MHz の 3倍クロックで検討する。
FIR フィルタ

FIR フィルタ自体は、N-1 個前までのデータについて積和演算するもの。N をタップ数という。係数の選択によって特性がきまる。

 ・FIR フィルタ 設計(窓関数法)

積和の係数が全然分からないのだが、計算してくれるサイトがある。それにしてもパラメータ自体が分からないのだが、先に行く。

さて、「オーバーサンプリングは 65 で 16 までのタップ数の FIR フィルタを使う」と書いたが間違い。
16 までの積和計算である。0 を挿入したデータでは、65 * 16 = 1040 までのタップ数の計算が可能である。0 のデータに何をかけても 0 であるから、1/65 の演算ですむのである。

しかし、上の計算サイトは、199 まで。どうするのが良いのだろう? 13 倍にしたものを 入力としてさらに 5 倍にする?
まず 13 倍。計算可能なのは、16 * 5 に変わる。タップ数としては 13 * 16 * 5 = 1040 まで。 要するに計算リソースはほとんど使わない。次の 5 倍は、 5 * 16 = 80 までのタップ数という計算。

やっぱり分からないのであるが、とりあえず 最初の 13 倍は、143 タップ。これでも 実質 11 個の実データを見てるだけで 実質 11 タップ。同じように 次の 5倍は 55 タップ 。
計算リソースは、全部で 1040 回だが、13 倍のところでは、13 * 11 = 143 回分。次の 5 倍では、13 * 5 * 11 = 715 回。ー めでたく 1040 クロックで実現できる。

これが決まれば、回路は設計できる。ROM に入れる係数が分からないだけである。

どう作る? プログラムであれば、最初に 13 倍のデータを1つ生成し、5倍のデータを 5個生成することを 13回繰り返す。これをバッファに詰め込んでいくというコードにする。
13 x (1 + 5) のデータ生成で 11 回の積和演算をするのであるから、858 クロック。これを 16 クロック毎に 65 回 = 1040 クロックかけて引き出して、ΣΔ DAC にかける。バッファあふれで演算の方を止めれば良いから、バッファは2段あれば良い。16 クロック毎に引き出すが、生成で 22 クロックかかる場合がある。−これだけを吸収できれば良いのである。

オーバーサンプリングのデータさえ出来れば、あとは DAC に流すだけである。1bit であろうと 3 bit であろうと好きにすれば良い。

さて、143 タップ や 55 タップを 11 タップと書いたが、どういうことか?
例えば 13 倍では、確かに 11 個の積和で良いのであるが、クロックが進むにつれ 係数は切り替わっていく。係数は 143 必要になるのだ。また、13倍も5倍も 11クロックである。乗算器を中心にして考えれば、データと係数が切り替わるだけである。
係数は ROM に入れる。11 個の係数のセットが、13 個と 5 個である。4 bit + 5bit だから 512 ワードの ROM があれば良い。ー EBR 2 個分である。 (詰め込めば 256 ワードに収まるので、簡単だったらトライするかも。)

    これで行こうと思うが、係数があれば いきなり 11 x 65 タップでも良いのである。係数が 11 x 65 = 715 必要なだけで、むしろ回路は簡単。タップ数も 15 x 65 までいける。

    ところで、何故 FIR フィルタなのか? 分かっているわけではないが、ハードウェアで組む場合 乗算器をぶん回せるのが良いのではないか? 演算数を減らしても 乗算器は遊ぶだけである。また、係数をテーブルに置ける。ハード量は多いが、構造はシンプルというのが良いのではないか? 逆に CPU にやらせるなら、計算量は少ない方が良い。IIR フィルタを使うとか 別の方式を取りたくなる。あと画像処理で良く使われるスプライン関数やベジェ曲線で補間すれば良いのでは?とも思うが、特性は悪いそうだ。

    それはともかく、FIR フィルタを採用するのは、実現可能だからでもある。作りこめるのだから、やってみたいという所がある。 乗算器をぶん回せるのは気分が良い。

ごたくはこれぐらいにして、FIR フィルタを使ったオーバーサンプリングを記述してみよう。


module over_sampling_65 (
input CLK // sampling x 65 x 16
,input [15:0] I_DATA
,input I_VALID
,output [15:0] O_DATA
,output O_VALID;
);

こんなインターフェイスにしようかと。入力データが有効であるとき I_VALID = 1; 逆に出力データが有効であるとき O_VALID = 1 。 I_VALID = 1 になるタイミングは正確でなければならないが、そうでない場合でもおかしなことにはならないようにする。リセットは、初期データを 11 回以上入力させる。(FIR フィルタだから タップ数以上前のデータは覚えていない)。出力はされてしまうので問題だが、別途考える。

I_VALID = 1 になるタイミングが遅いとき、65 個のデータが出力されたあと、アイドルになる。I_VALID = 1 になるタイミングが早いとき、次のデータが来た時点で打ち切る。タイミングが正確なとき、最後のデータが出力中の状態であるはずだが、O_VALID = 1になって 次の段に受け取られているはずで、打ち切っても問題は起きない。

イメージとしては、データが来た時点で処理スタート。65 個目のデータ出力で処理終了。11 個前までのデータを覚えているが、それ以外はデータが来た時に初期化される。

という設計で次に状態を考える。中身は後。

reg [6:0] x13_cnt; // 0....13
reg [3:0] fir_cnt; // (x13)5 (x5)0...4
reg [3:0] tap_cnt; // tap counter 0..10
reg [3:0] out_cnt; // for output timming


 ・calc_cnt :前段の x13 でいくつデータを計算したかで、13 個計算したら終わり。x13係数テーブルのインデックスとしても使う。
 ・fir_cnt : 何を計算しているかの状態。まず x13 をひとつ計算するが 値は 5 。次に x5 を 5 回 0...4 。これも x5 係数テーブルのインデックスとしても使う。
 ・tap_cnt : 1つのデータの FIR の計算で何番目をやっているかのカウンタ 0...11
 ・out_cnt : 出力タイミングのためのカウンタ。16 クロック毎に出力。

重要なのは、これだけではないだろうか?

always @(posedge CLK)
begin
iv_prev <= ~I_VALID;
if (~iv_prev && I_VALID) begin
x13_cnt <= 0;
fir_cnt <= 5;
tap_cnt <= 0;
out_cnt <= 0;
end else begin
out_cnt <= out_cnt + 1;
if (tap_cnt == 10) begin
tap_cnt <= 0;
if (fir_cnt == 4) begin
fir_cnt <= 5;
x13_cnt <= x13_cnt +1;
end else if (fir_cnt == 5)
fir_cnt <= 0;
else
fir_cnt <= fir_cnt + 1;
end else
tap_cnt <= tap_cnt + 1;
end
end

ステート制御は、だいたいこんな感じで。

乗算器

module muladd (input clk,
input init,
input sel,
input [15:0] a,
input [15:0] b,
output [17:0] c1,
output [17:0] c2
);
reg [33:0] prod1, prod2;
assign c1 = prod1[33:16];
assign c2 = prod2[33:16];
reg [15:0] a_reg, b_reg;
wire [33:0] mult_out;

assign mult_out = $signed(a_reg) * $signed(b_reg);

always@(posedge clk)
begin
a_reg <= a;
b_reg <= b;
if (sel)
prod1 <= mult_out + (init?0:prod1);
else
prod2 <= mult_out + (init?0:prod2);
end
endmodule

乗算器はこんな風にしようかと。c += a x b みたいなのを作りたいのだが、c が x13 用と x5 用に2つ必要。また、桁あふれが起きるとまずいので、c は 32 +2bit にしたい。最終的には クランプ処理をして 16bit を出力する。

この乗算器は、未完成で、ブン回るのみ。動作する条件を別途入れないといけない。
これをどう使うかというと、

wire [3:0] k_addrh = (fir_cnt == 5)? x13_cnt : {1'b0 ,fir_cnt};
wire [3:0] k_addrl = tap_cnt;
wire [8:0] rom_addr = { (fir_cnt == 5), k_addrh, k_addrl};
wire [15:0] rom_data;

rom16 krom_inpl (.CLK(CLK), .ADDR(rom_addr), .DO(rom_data));

まず係数。ROM で生成。

reg [15:0] i_datas [0:15];
reg [3:0] i_ptr;
wire [3:0] i_ptr_n = i_ptr - 1;

reg [15:0] x13_datas [0:15];
reg [3:0] x13_ptr;
wire [3:0] x13_ptr_n = x13_ptr - 1;

次にタップのデータ。配列に逆方向に入れていく。いわゆるリングバッファ。EBR に割り当てることを考えるとこのやり方しかないのだが、LUT で実装するならシフトする方が良いかも知れない。

これらを入力として乗算器にかける。

wire sel = (fir_cnt == 5);
wire [16:0] muladd_a = sel?i_datas[(i_ptr + tap_cnt) & 4'b1111]
:x13_datas[(x13_ptr + tap_cnt) & 4'b1111];

muladd muladd_inpl (.clk(CLK) , .sel(sel), .c1(r13), .c2(r5),
.init(tap_cnt == 0), .a(muladd_a), .b(rom_data)
);

・・・これで良いのかというとダメなのである。ROM を使う以上入力は、1クロック遅れる。乗算器も1クロックかかる。・・・ということは2段のパイプラインにしないといけない。i_datas, x13_datas についても、書き込みと読み出しタイミングを詰めないといけない。まぁでもだいたいこんな感じでいけるだろう。

タップレジスタ

module tap_reg (
input CLK,
input PUSH,
input SEL,
input [3:0] ADDR,
input [15:0] DI,
output [15:0] DO
);
reg [15:0] mem [0:31];

reg [3:0] i_ptr1;
reg [3:0] i_ptr2;
wire [3:0] i_ptr1_n = (i_ptr1 -1) & 4'b1111;
wire [3:0] i_ptr2_n = (i_ptr2 -1) & 4'b1111;
wire [4:0] i_addr = {SEL, SEL? i_ptr1_n : i_ptr2_n};
wire [3:0] o_ptr1 = (i_ptr1 + ADDR) & 4'b1111;
wire [3:0] o_ptr2 = (i_ptr2 + ADDR) & 4'b1111;
reg [4:0] r_addr;
always @(posedge CLK)
begin
if (PUSH) begin
mem[i_addr] <= DI;
if (SEL) i_ptr1 <= i_ptr1_n;
else i_ptr2 <= i_ptr2_n;
end
r_addr <= {SEL, SEL? o_ptr1: o_ptr2};
end
assign DO = mem[r_addr];
endmodule

i_datas[],x32_datas[] の部分をモジュールにして、ブロック RAM を1つ使うようにしたもの。どうせ パラレルには使わないし、データ の更新も読み出しとは同じタイミングでは行わない。モジュールにすることで、どのようなタイミングで使うか、きっちり決めていく。

SEL は、2セットのうちどちらを使うか選ぶ。データ更新は PUSH である。読み出しは新しいデータから 0, 1, .. 10 のアドレスで読み出す。




タイミング設計(詳細)

今までは、イメージをコードにしてみただけのおおざっぱなもの。ここからタイミングを詰めていく。
まずは、I_VALID が 0 → 1 になるのを検出した時点が起点。ここで既に 1 クロックずれているかも知れないのだが、次も同じタイミングになるので気にしない。

ここを 0T としよう、いきなり 計算は始められない。0T は、入力データのセットアップ。x13 の最初のデータが得られるのは次からであるが、乗算のデータを得るのにさらに 1T 使う。2 〜 12T で x13 の FIR の積和をする。結果が出るのは、13T のとき。14T で x5 のためにセットアップしてやる。15T からは x5 の積和を 11T x 5 連続で行う。70T で x5 の最後のデーターが出力される。ここまでで、5個のデータが生成されている。あと 12 回やるのであるが、0T の処理は必要ない。1T 〜 70T までをループさせる。このようにして、911 クロック使って 65 個のデータを生成する。一方 実際の出力は、65 x 16T = 1040 クロックの期間である。FIFO の段数を増やせばよいのだが、仮に2 段と決めたので x5 のデータ出力の際にウェイトを挿入できるようにしないといけない。
常にウェイトを入れるようにして、ウエイトを伸ばせるようにするのが、コード的に楽なのではないか? そうなると 全体が 65T 延びて 976T で完了ということに。

さて、実際の出力はどうなるかというと、15T + 11T = 26T に最初のデーターが出力される。16T おきに出力されるのであるから、最後の出力は、1040 +26T と決まっている。1040T には次のデータが来るのであるから、最低でも 3 段の FIFO がないと、演算が終わっていないことになる。

さて、出力段。タイミングとしては、26T で最初の出力。次からは 10 に設定して、 +16T 毎に出力。 5 bit カウンタで制御。このカウンタは、I_VALID で初期化できない。フリーランニングにして、FIFO をもう1段増やす。計4段。

module out_fifo (
input CLK,
input PUSH,
output BUSY,
output O_VALID,
input [15:0] DI,
output [15:0] DO
);
reg [15:0] out0;
reg [15:0] out1;
reg [15:0] out2;
reg [15:0] out3;
reg [3:0] cnt;
reg [2:0] n;
reg r_busy;
reg r_valid;

assign O_VALID = r_valid;
assign BUSY = r_busy;
assign DO = out0;

always @(posedge CLK)
begin
cnt <= cnt+1;
if (PUSH && (cnt == 8)) begin
out3 <= DI;
out2 <= (n == 2)? DI :out3;
out1 <= (n == 1)? DI :out2;
out0 <= (n == 0)? DI: out1;
if (n != 0) r_valid <= 1;
if (n == 0) n <= 1;
end else if (PUSH) begin
if (n == 0) out0 <= DI;
else if (n == 1) out1 <= DI;
else if (n == 2) out2 <= DI;
else if (n == 3) out3 <= DI;
else r_busy <= 1;
if (n <= 3) n <= n +1;
end else if (cnt == 8) begin
out2 <= out3;
out1 <= out2;
out0 <= out1;
if (n != 0) n <= n -1;
if (n != 0) r_valid <= 1;
r_busy <= 0;
end else if (cnt == 0) begin
r_valid <= 0;
end;
end
endmodule

強引に作ってみる。D-FF が FIFO に 64 個必要で、その他制御に 9 個必要。iCE40 では LUT を浪費するが、仕方がない。と言って 4 個のデータのために ブロック RAM を使うのもやりたくない。出力と入力が同時のときがめんどくさい。



係数について
デジタルフィルタ研究室」を発見。
DSPLinks というソフトを使ってのチュートリアルがある。

これも係数生成ができ、シミュレーションもしてくれる。また、入力データがどう出力されるかや FFT 解析までやってくれる。
 (12) オーバサンプリングの基礎 - osf1.DE2
 (19) オーバサンプリングにおける量子化 - osf1.DE2
 (21) dsPIC用係数の生成と実装(FIR編)
このあたり。

とにかく、係数を生成し、ファイルに保存できることは分かった。ただし、400 タップまでという制限があり、2段フィルタにするしかないのは変わらない。係数は 16進数での作成が可能。ただし 8桁なので 簡単な編集が必要。さらに並べ替えが必要。こうなるとスクリプトを書かないと。

RMZLPF が良く使われると書いてあった。が、パラメータが適切でないとダメらしい。他には WINLPF がある。x13 の方は RMZLPF で、なんとか出来たのだが、x5 でうまくいかなかったので WINLPF にしてみた。

係数は 16bit にするとやたら小さい値になる。乗算でオーバフローしない程度に bit 数を増やすとか 積和結果がオーバーフローしないようにするとか、検討すべきことがあるようだ。あと、ゲインがどうなるか? 0 を沢山挿入しているから、小さくなってしまうのではないか? それも検証しておかないといけない。



コードはおやすみと書いたが、ここまでは先に作ったのであった。ΣΔDAC (1) の部分よりも怪しいレベルではあるが、コードも ひとまず FIX。
posted by すz at 16:55| Comment(0) | TrackBack(0) | MachXO2
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/182763244
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック