どもです。
8月の夏休みから4ヶ月ほどかけてやっと動作確認までできたので公開します。
サンプリング周波数選択もできたぁぁぁぁぁぁぁぁぁ
— AUDIY (@AUDIY14) November 29, 2023
データも問題無し! pic.twitter.com/Gek3xQQBYP
PCMデータを2倍オーバーサンプリングするFIRデジタルフィルタIPです。
きっかけ
AUDIYがFPGAに興味を持ったのは以下のビデオがきっかけです。
CHORDというオーディオメーカーがFPGAとフリップフロップを使用してDAコンバータを実現している、ということを知り、自分でもDAコンバータの構造を知りたく、FPGAを使用したオーディオ信号処理に興味が湧いたのでした。
大学・大学院では信号処理について勉強こそすれどその実装はソフトウェアばかりで、興味こそ持ちつつもなかなか手が出せなかったのですが偶然にも就職してハードウェア設計を行うことになりました。
「・・・コレは良い機会だ!」
ということで、オーディオ信号処理で多用されることの多いオーバーサンプリングFIRフィルタの実装に着手したのでした。
参考資料
実際にオーバーサンプリングデジタルフィルタを実装するにあたり、参考とした資料はリンク先のPDFです。
https://shop.cqpub.co.jp/hanbai/books/I/I000139/I000139kou.pdf
トランジスタ技術2014年12月号~2015年1月号にかけて特集された記事の一部のようです。
PDF上の図では「RAM、ROM、乗算器、加算器1つずつでできる」とありますが、実際にそれっぽく動くようになるまでには複数のハードルがありましたので、Verilogコードのリンク先とともに共有させていただきます。
ポリフェーズ・フィルタ
乗算器、加算器を1チャンネルあたり1つしか使わないということであれば、自ずとポリフェーズ・フィルタの実装になります。
今回はオーディオ用のマスタークロック(多くの場合で22.5792MHzとか24.576MHzの整数倍だと思います。)を使用して、サンプリング周波数の整数倍だけFIRフィルタのタップ数を格納する方式にしました。
RAMのアドレス制御
オーバーサンプリングデジタルフィルタに使用するRAMですが、
- 入力はサンプリング周波数のレートでデータを保存していく
- 出力はマスタークロックに合わせ保存したデータを呼び出していく
- 最も古いデータが保存されていた場所に最新のデータを保存していく
- 入力するアドレスと出力するアドレスが重複するのは禁止
必要があります。
つまり、入力はFIFO(シフトレジスタ)のように扱いつつも出力は任意のデータを呼び出せるようにしなければなりません。
本IPではRAMがリングバッファとなるようにRAMコントローラIPを設計し、上記仕様を実現しました。
今回リングバッファを実現する方法ですが、
- 最も古いデータのアドレスを記憶しておく
- データ出力の先頭アドレスを「最も古いデータのアドレス」から一つ進め、アドレスをインクリメントさせる
- データ書き込み先のアドレスに「最も古いデータのアドレス」を指定する。
- 「最も古いデータのアドレス」を一つインクリメントさせる
- 1に戻る
を繰り返すことで実現します。
ROMアドレスの指定方法
オーバーサンプリングの説明ではよく「データの間に0を挿入してFIRフィルタを畳み込む」と説明がなされるかと思います(上記PDFにもあります)。
ただ、コレを律儀にFPGAに実装すると以下のように無駄が生じてしまいます。
- ゼロデータとFIRフィルタ係数の乗算が発生する。
- マスタークロック周波数が一定なので、1により加算できる有効データ数が減少する
ということで、今回のオーバーサンプリングデジタルフィルタではフィルタ係数を出力するためのアドレスを「奇数で一巡→偶数で一巡」と繰り返す仕様にすることで0データとの乗算が行われたことと等価の処理を実現しています。(個人的にここが最大のキモだと思います。)
ROMに格納するデータを並び替えればこんなことせずにアドレスを1ずつ増やすこともできますが、データの準備が面倒ですしね・・・・
新たなビットクロックとワードクロック
FPGAで処理する以上は最終的に外部のDAコンバータICにデータを送り出すためのビットクロックとワードクロック(サンプリング周波数のクロック)が必要です。
本IPではROMアドレスの任意ビットをそのまま出力することでコレを実現しています。
(マスタークロックの分周は危険なのでやめました)
ひょっとしたらRAMのアドレスでも同じことを実現できるかもしれませんが、ROMのほうが計算の始点・終点がハッキリしているのでこのようにしました。
積分器のリセット
デジタルフィルタでは畳み込みの実現のために積分器を使用しますが、入力データを積分し続ける一般的な積分器とは少し挙動が異なります。
FIRフィルタとデータの乗算が完了したらデータが更新されるため、FIRフィルタの係数を全て使用したら積分データをリセットする必要があります。
本IPでは先述の「新たなワードクロック」の立ち上がりエッジを検出することで「全てのFIRフィルタの係数が入力データと乗算された」として積分データをリセットすることで実現しています。
できる限りのクロックドメインクロッシングの排除
本IPは内部の処理が全てマスタークロックの立ち上がりエッジまたはたち下がりエッジに同期して動作します。
逆位相でフリップフロップを駆動する箇所があるため非同期設計が無いわけではありませんが、ビットクロックやワードクロックを一切使用していないだけでもだいぶトラブルは起こりにくい仕様になっていると思います。
I2Sで出す場合はビットクロックのたち下がりエッジに同期してデータが出力されるのが標準仕様となっています。
https://www.nxp.com/docs/en/user-manual/UM11732.pdf
かといって完全同期のためにODDRとか使い出すとベンダー依存が強くなるので考えどころではあります・・・・
ビットクロック:マスタークロックの比を1:2以上にすればODDRも必要にならないのでそれが最適解ですかね・・・
以上、実装上の工夫を今回は共有しました。
次回はRTLシミュレーションと実際にFPGAで動かす例を記したいと思います。