FM復調のFPGA実装(1)

前回までに、ADCでキャプチャした生データをパソコン上で処理することで、 FMラジオを視聴できることが確認できました。

PC上のソフトウェアでFM復調を行っている部分を、徐々にFPGA化していこうとしています。

いきなり全体をRTLで実装しても、動作しなかった場合のデバッグが大変です。 それで、ADCに近い側からFPGAの回路に落とし込みます。今回は複素ミキサ(ADC入力にチューニング周波数のsin, cosを掛け算することで、I, Q信号を得る)と、 CICフィルタを実装しました。

2の補数表現とビットシフトを注意深く行えばそれほど難しくないはずの回路でしたが、わりと手こずりました。 複素ミキサはFPGAのBRAMにてテーブルルックアップを行ってsin, cosの値を得るようにしています。

具体的には、32bitの値を持つ400エントリを使用して、各エントリに対応するsin, cosの値を符号付きで13bitずつ入れています。 なぜ400エントリかというと、40MHzのサンプリングレートで例えば84.7MHzを受信しようとする場合、 これはナイキスト周波数以上なので実際には4.7MHzとしてADCから見えます。 (4.710^6) / (4010^6) = 47 / 400ですから、ADCの400サンプルに対して、4.7MHzは47周期現れます。 この400回分について、sin(2 * π * i * (47/400)) (i=0…399)をテーブルとして準備しておきます。 テーブルの中身自体は、Zynqで動作するLinuxアプリケーションにて初期化しています。 BRAMはZynqのアドレス空間と、RTLの両方からアクセスできるようにTrue Dual Portで動作させています。

ADCの各クロックごとにアドレスをインクリメントすることで、対応する周波数のsin, cosの値を得ることができます。 それをADCから出力されている符号付き12bitと掛け算し、下位ビットは切り捨ててCICフィルタに入力します。

CICフィルタは3ステージの1/32間引き(1.25MHz出力)を行っています。出力は29bitとしています。

これで毎秒のデータ量は1.25MHz*8bytes=10MBytesに減少しましたので、メモリ上に取り込むことのできる時間が一気に長くなりました。 PC側でサンプリングレートの調整を実装すれば(基板に実装されている発信器の周波数とPC上の周波数は異なるため、 48kHz出力を行うにしても、クロックレートの微調整のためにサンプルレート変換が必要)、 リアルタイム転送とFM変調処理が行えるのですが、そちらはまだ今後の課題ということで。

結局最後まで手こずった原因は、BRAMのアドレスの下位2bitは無視されていることに気づかず、 0-399までのカウンタをそのままアドレスに入力していたことでした。 0-399のカウンタに下位2bit"00"を付加してBRAMの32bitアドレスとすることで、 想定通りの正弦波が得られたようです。

それでも、PC上で処理した場合より明らかに音質は悪いです。もう少しミキサのビット数を増やしたりしてみようかと思います。

2017/4/20追記: ミキサの出力を17bitにして、CICを32bitまで拡張しました。 ミキサ出力の部分も実際のデータを観測して、オーバーフローしない範囲でできるだけ下位ビットを取るようにして、 可能な限り精度を落とさないようにしました。CICの結果から残りをパソコンのソフトで処理してみたら、 かなり音質は改善しました。これならひとまず良さそうです。 残るFIRとCORDICによるarctanの実装を粛々と行っていけばFM復調のFPGA化が完成する予定です。

また、これまでソフトウェアではarctanを求める前に複素数の割り算をすることで、偏角の差分を一発で求めていました。 でも、FPGA化する場合、これだと乗算と加算が入るので、CICのデータが32bitあると64bitほどになってしまいます。 それで、まずarctanを各データに対して行って、その結果の偏角を引き算する、という計算順序に変更しました。 arctanが返す角度の範囲が-πからπまでなので、単純に引き算するだけだと、この範囲を超えてしまうことがあります。 そのため、下記のように必要な場合2πずらすようにしました。

    private double[] fm_demodulate_sub(double[] i, double[] q)
    {
        double[] result = new double[i.Length];
        double prev = 0.0, tmp;

        for (int x = 0; x < i.Length; x++)
        {
            tmp = Math.Atan2(i[x], q[x]);
            if (tmp - prev > Math.PI)
            {
                result[x] = tmp - prev - Math.PI * 2.0;
            }
            else if (tmp - prev < -Math.PI)
            {
                result[x] = tmp - prev + Math.PI * 2.0;
            }
            else
            {
                result[x] = tmp - prev;
            }
            prev = tmp;
        }
        return result;
    }

これならばarctanの計算に必要な精度はI, Qそれぞれ32bitで良いことになります。

comments powered by Disqus