I2S→左詰め16bit(BCK 32fs)への変換をFPGAでやる


きっかけ

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

やってみた

PCM-501ESで実験したときの様子を示します。

急ぎでつくったので配線は超適当ですが、このヒョロヒョロのGND(黒線)でもそこそこまともな音が出ます。
ポテンシャルを感じます!

おわりに

・USB-DDC(こういうの
・ちっちゃいFPGAボード(こういうの
さえあれば簡単にできるので、興味があるけど手が出せてない人のお助けになると幸いです。
あとVerilogのコーディングに関しては限りなく素人なので詳しいかた突っ込んでいただけると助かります…。

参考文献

PCM-501ESの驚愕音質とSPDIF化改造


  1. CM6631のように、1fsでBCK=128fsになる特殊なケースを除きます。またサンプリング周波数の遷移などイレギュラーな動作については確認していませんので、ハードウェアや耳の破損には責任もてませんのでご了承ください。