きっかけ
80年代の積分型の(NOS:ノンオーバーサンプリング)DACには、現代のDACと比較しても音質的な優位性があるらしく、
機会があったらこの時代のCDプレイヤーを入手して、DACに改造しようかな~と思っていました。
そんなタイミングで、
PCM-501ESの驚愕音質とSPDIF化改造
のような記事を見つけて、どんな音か聞いてみたい気持ちが高まりました。
調べてみるとこの時代のDACに入力するデジタルオーディオフォーマットは、
16bit 左詰めフォーマット(BCK = 32fs)
つまり昔の16bit DAC 専用のI2S入力(に準じたもの)である必要があるらしく、
各種ブログの作例としては、
・16bit 左詰めフォーマット で出すDAIを使用する
ことが紹介されていることが多いのですが、このような出力を持つDAIは古いものに限られています。
ということで悩んでいたのですが、
「これFPGAでI2Sを直接I/F変換すればよいのでは…?」
と思ったので、久しぶりにFPGAを触るリハビリもかねてやってみることにしました。
I2S(Inter IC Sound)について(作成中)
このタイトルでピンと来た方にわざわざ説明する必要はないと思いますが、
I2Sは以下の3つの信号線からなります(場合によりMCKが必要)。
- BCK
- LRCK
- DATA
目標の出力
上段の3つのCLK(I2S)
↓
下段の3つのCLK(16LJ~)
のように変換されていればよいです。
- LRCKの周波数は変わらず、L、RのDataがLRCKと同じタイミングで出る
- BCKの周波数を半分にする
- 上記に伴いLRCK1周期で元データの先頭16bit分のDATAを出力
Source Code(参考)
Verilogソースコードを示します。
Fs = 44.1/48kHzで USBDDCからI2S出力を行い、
これをCX890 / CX20017(積分型DAC)に入力させ、
それなりに安定動作1しました。
//I2Sを16LJに変換
module I2S_to_16LJ(bck, data, lrck, bck_out, data_out, lrck_out);
input bck;
input data;
input lrck;
output bck_out;
output data_out;
output lrck_out;
reg data_out = 0;
reg[4:0] data_counter = 0 ; //元のI2Sにおける何bitめのDATAか
reg[31:0] data_fifo = 0; //元のI2S DATA (32bit) を 1sample分格納する
//LRCK を 元々のBCK2周期分遅らせる
wire lrck_32LJ;
delay_1BCK I2S_to_32LJ (lrck, bck, lrck_32LJ);
delay_1BCK delay (lrck_32LJ, bck, lrck_out);
//BCKを2分周
//元のBCKに同期して、lrck_changedでリセットをかける
reg lrck_changed;
half_freq half_freq_ins (bck, lrck_changed, bck_out);
//分周したBCKに同期させてDATA出力をする
reg lrck_before = 0;
always @ (posedge bck)
begin
//lrckの切り替わりを検出、読んでるbitのインデックスを0にする
if(lrck_before != lrck_32LJ)
begin
data_counter <= 0;
data_fifo[0] <= data;
lrck_changed <= 1;
end
else
begin
data_counter <= data_counter + 1;
data_fifo[data_counter + 1] <= data;
lrck_changed <= 0;
end
lrck_before <= lrck_32LJ;
end
always @ (negedge bck_out)
begin
data_out <= data_fifo [data_counter / 2];
end
endmodule
解説と、各モジュールの詳細説明
I2S → 32bit左詰め に変換
これは簡単です。I2S信号はDataがLRCKに対して1BCK分ずれていますので
この分LRCK側をずらしてあげればよいです。
module delay_1BCK (lrck, bck, lrck_out);
input lrck;
input bck;
output lrck_out;
reg lrck_out;
reg lrck_before;
always @ (posedge bck)
begin
lrck_before <= lrck;
end
always @ (negedge bck)
begin
lrck_out <= lrck_before;
end
endmodule
BCKの2分周
単なる2分周回路だと、パワーOnタイミングによってはBCKの正負が反転する可能性があります。
(なので本当はパワーオンリセットが必要)
これを防ぐため、LRCKの立ち上がり/下がり(lrck_changed)に同期してリセットがかかるようにしています。
module half_freq(bck, lrck_changed, bck_out);
input bck;
input lrck_changed;
output bck_out;
reg bck_out;
always @ (negedge bck)
begin
if(lrck_changed == 1)
begin
bck_out <= 0;
end
else
begin
bck_out <= ~bck_out;
end
end
endmodule
Dataの乗せ換え
BCKの立ち上がりで元々のI2S信号をラッチし、レジスタdata_fifoに保持しておきます。
分周したBCKの立下りタイミングに同期して、data_fifo内のデータを1bitずつ出力します。
これはちょうど data_counter / 2 ビット目 のデータになります。
これディスクリートのロジックでやろうと思うとちょっと大変そうですね。
2*LRCKが必要な場合
PCM-501ES のように LRCKに同期し倍の周波数を持つ信号 = 2*LRCK が必要な場合には、
LRCKをBCK8個分遅らせた信号を作成し、これを元のLRCKとNXOR すればよいです。
module make_WCK(bck, lrck, WCK);
input bck;
input lrck;
output WCK;
reg delayed_lrck = 0;
reg[7:0] lrck_fifo;
assign WCK = lrck ~^ delayed_lrck;
always @ (posedge bck)
begin
lrck_fifo <= {lrck_fifo[6:0],lrck};
end
always @ (negedge bck)
begin
delayed_lrck <= lrck_fifo[7];
end
endmodule
やってみた
急ぎでつくったので配線は超適当ですが、このヒョロヒョロのGND(黒線)でもそこそこまともな音が出ます。
ポテンシャルを感じます!
おわりに
・USB-DDC(こういうの)
・ちっちゃいFPGAボード(こういうの)
さえあれば簡単にできるので、興味があるけど手が出せてない人のお助けになると幸いです。
あとVerilogのコーディングに関しては限りなく素人なので詳しいかた突っ込んでいただけると助かります…。
参考文献
-
CM6631のように、1fsでBCK=128fsになる特殊なケースを除きます。またサンプリング周波数の遷移などイレギュラーな動作については確認していませんので、ハードウェアや耳の破損には責任もてませんのでご了承ください。 ↩