NES on FPGA 16:37 2012/08/18

dev APU

 ファミコンのサウンドユニットを実装することで、 なつかしのサウンドを再現することができました。 モジュールごとに分割すれば見通しよく実装できます。

_
▼ クロック

 CPUのクロックはとにかくフレームレートの60Hzに合わせればよかったため、 クロック数が重要であり、周波数については正確ではありません。 APUはサウンドジェネレータであるため、ベースクロックが均一でないと、 サウンドも不安定になります。 そこでAPUへはできるだけ実機の約1.789MHzに近くて均一な周期を入力する必要があります。

 CQ出版評価ボードの33MHzを18で分割すると1.833MHz、 19で分割すると1.737MHzとなるので、 18分割を何回、19分割を何回と繰り返すことで1.789MHzに近い周波数が得られるようです。 その比率は18分割を56.2%、19分割を43.8%となり、18分割の回数が多くなります。 18分割と19分割を交互に行いながら、 12.4%分の18分割をできるだけ短い範囲にちりばめることで、実機に近い周波数を生成できます。

 分数-N合成よりもよりばらつきを抑え、単純な回路で構成可能なのがDDS。 ポイントは適切なビット幅と加算値を使用すること。

! 今回使用した周波数合成は分数-N合成と呼ばれるらしい。

他にもダイレクトディジタル周波数合成(DDS)があるらしい。

_
▼ 1ビットDAC

 (DMCは置いといて)各チャンネルの出力は4bitなので、 これを加算すると6bitになります。 出力ピンを減らすために、1ビットDACを使用します。 ベースクロックとして評価ボードの33MHzを使用しました。

 とりあえずPWMを実装してみたところ、サウンドは聞こえますが、やはりバリが強く感じられます。 これはPWMを使用すると、量子化誤差によるノイズ成分が各周波数帯域に均等にのってしまうためです。 また可聴領域外の波形を拾ってしまい、サウンドの再現性は低くなります。

 このためデルタシグマ型DACを実装しました。 これはシフトレジスタと加算器のみで構成できるため実装も容易です。 全体の量子化誤差量は変わりませんが、 ノイズ成分を人間の可聴領域よりも高い周波数帯へ押しやってしまうため、 PWMより音のバリは低減されます。

! ハナッから実装してればいいものを…

_
▼ NSF演奏機能

 NESサウンド用ファイルフォーマットNSFの演奏が可能です。 マッパー0のシーケンシャル32Kバイトまで。

 バージョンアップ! 1MBまでのNSFファイルに対応。 複数曲対応。SDカードからの読み込み。 各種拡張音源実装(N106、MMC5、FDS、VRC6、SN5B、VRC7)。

 → NSF Player on FPGA !

! 仕事は単純なんだけど、思いのほか感動。

_
▼ ステレオサウンド

 ディレイユニットにより、音源の定位をずらしてステレオにしました。 正面を0度として、+60度、+30度、-30度、-60度の位置にそれぞれ 矩形波1、三角波、ノイズ、矩形波2というように配置。

 定位をずらすには、ずらしたい方向のサウンドを遅らせればいいので、 256サンプル遅れで60度、128サンプル遅れで30度と、だいたいですが。 サンプリングはAPU内の1.79MHzで行っています。

! ほら、GBってステレオっしょ。 ステレオ感ではGBの勝ち。

_
▼ 高音域になるほど音痴になる?

 これはNESに限ったことではないですが、 出力周波数の低域ではそうでなくとも、 高域になるにつれてなんだか音痴になるそうです。
サウンドの出力周波数を決めているタイマは、 約1.79MHzを分周して設定された周期のクロックを生成します。 このため440Hzや880Hzといった標準的な音階ちょうどの周波数を生成することはできず、 いくらかの誤差が含まれます。三角波の場合、

所望(Hz) 理想設定 実際設定 出力(Hz) 誤差(Hz)
   55     1015.9    1016      55.00   0.00
  110      507.4     507     110.10   0.10  *
  220      253.2     253     220.20   0.20  *
  440      126.1     126     440.40   0.40  *
  880       62.5      63     873.91   6.09  *
 1760       30.7      31    1747.82  12.18  *
 3520       14.8      15    3495.65  24.35
 7040        6.9       7    6991.30  48.70
14080        2.9       3   13982.60  97.40
                              * よく使用される

 欲しい出力周波数に対する理想設定を四捨五入し、 実際にタイマへ設定する値を算出します。 希望した出力周波数とその設定値のときの実際の出力周波数の誤差をとると、 高音域になるほど誤差が多くなっています。 これは高域になるにつれて理想設定と実際の設定値の差の割合が大きくなるためです。

 三角波のテストをエミュ形式(.nes)にしておいときます。 Aボタンを押す度に周波数が2バイ2バイ。 [apu_tri.nes]

! 聞く所によると、「チューニングの許容範囲は0.5Hz以内」(ジャズ研部長談)らしいので、 数値だけを見れば聴く人が聴けば違和感を感じそうです。

しかしNESの中で完結していれば(他の楽器と聴き比べなければ)、 音階は相対的なものなのでそう気になることはないと思います。

「これがNESサウンドなんだよ」と言ってしまえばそれまで。

_
▼ 三角波のリニアカウンタを使う 2005/1/19

 三角波のコントロールレジスタ$4008のビット7については、 1を書き込んだ場合、長さカウンタが停止されず、 またラインカウンタの値も反映されないため音が出続けてしまう。 (W:Write)

 W $4015 $04 三角波チャンネル有効
 W $4008 $F8 リニアカウンタOFF
 W $400A $D5 周波数下位$D5
 W $400B $08 周波数上位$0、長さカウンタ $01(2.1秒)
  ♪ドーーー…と鳴り続ける。

 そこで、ラインカウンタを有効にするため、$4008のビット7に0をセットし、 ラインカウンタ値を有効にする。 これで長さカウンタが動作している間にリニアカウンタも動作し、 0.5秒間発音する。

 W $4015 $04 三角波チャンネル有効
 W $4008 $78 リニアカウンタON、リニアカウンタ120(0.5秒),
 W $400A $D5 周波数下位$D5
 W $400B $08 周波数上位$0、長さカウンタ $01(2.1秒)
  ♪ド、と0.5秒間鳴る。

 これはマリオのコードを参照したものだが、 音長を長さカウンタでなくリニアカウンタでコントロールしているからと思われる。 リニアカウンタは240Hzで動作するため、 フレームタイミングと合わせやすいからこのような方法が使われるのでしょうか。

_
▼ フレームIRQとドラクエ 2009/11/23

 ドラゴンクエストでサウンドが鳴らない不具合が発覚。 ログを調査したところどうやらフレームIRQを使用してサウンドを鳴らしている模様。

 フレームIRQとは、$4017へ上位2ビット0の値をwriteするとフレームシーケンサモード0:240Hz、 IRQ有効となり、60HzでIRQがCPUに対して生成される。 フレームIRQはカートリッジからのIRQ同様に、 イネーブルされるとCPUへIRQ割り込みが入るが、 NMI割り込みと違ってCPUのIフラグがセットされているとすぐに割り込み処理は実行されない。

 ここでポイントなのは、IRQはイネーブルホールドであり、 ワンクロックのトリガではないということ。 CLI命令やRTI命令実行後にIフラグがクリアされ、 そのときまでIRQがイネーブルであればIRQ割り込みが処理される。

 はじめ、IRQ信号をワンクロックのトリガで実装していたところ、 Iフラグがセットされていた場合にIRQが無視され(当然だが)てしまい、 ドラクエのサウンドが不安定になっていた。 IRQをイネーブルホールドすることでCLI命令の後にIRQ割り込みが実行され、 正常にサウンドが鳴った。

 ちなみにこのイネーブルホールドされるフレームIRQは、 $4015へのreadによってディセーブルされる。

_
▼ Ver.1 仰せのままに

 資料を参考にして5日ほどで矩形波×2、三角波、ノイズを実装。 一番楽なのは三角波でした。

 各チャンネルで共通に使用されるタイマユニット、 エンベロープジェネレータ、長さカウンタはインスタンスとして利用できるようにまとめました。 長さカウンタテーブルは、各チャンネルの周波数レジスタ2への書き込み時にのみ使用されるため、 一つだけ実装して使いまわしです。
論理合成結果

Total equivalent gate count for design:  8,037
あんまり意味があるデータとは思えない。

 各チャンネルの出力は、実機では非線形の加算回路で加算されて出力されるようです。 FPGA内部でテーブル用意して真似できそうですが、とりあえず単なる加算して、PWMで出力。 出力には簡単なローパスフィルタをかましてヘッドフォンジャックへ。

 この時点では、DMCのサンプルが手に入ってないためまだ実装していません。

_
▼ Ver.2 辛酸愚弄

 フレームシーケンサの実装でマリオのサウンドがより生々しくなりました。 エンベロープジェネレータをそのまま240Hzでたたいてしまうと、 マリオのジャンプ時にピコノイズが入ってしまうようです。 実質192Hzでたたいていることになりますが、これでいいんだろうか?

 沙羅曼蛇はパワーオンリセットを使ってるらしく、 ボードの電源ONからFPGAへの書き込みで音が鳴った。 別に書き込みタイミングが違うとかそういう問題じゃないんですね。

 アルマジロでは三角波の無音化のために周期1の波形を指定しているようです。 これは56KHzで出力されるため人間の耳には聞こえないはずが、「キーン」という音が…。 いや俺の耳が良いわけじゃなくて。
どうやらPWMでサンプリングしてたため、可聴領域外の周波数を拾っていたらしい。 デルタシグマ型のDACを実装して無事カットできました。

 スパルタンXの「アター」がDMCを使っているようなので、 実装しました。これに伴いAPUからのメモリアクセスが必要になります。

[ APUからのメモリアクセス > DMA > CPUからのメモリアクセス ]

という優先順位にして、自分より優先度が高いアクセスが発生している間は スリープしておくという状況。

合成結果

Total equivalent gate count for design:  10,495
 サウンドチャンネルを全て実装し、自前の演算器を使用してこのゲート数。 多いんだか少ないんだか。

 サウンドをmidiで出力するっての一度やってみたかったんだが、 NESサウンドを愛してる人たちから怒られそうだ。

 それよりNSFファイル再生機能がほしいなあ、ということでVer.3で追加。

_
▼ Ver.3 たまにはっていつも

 ただ音を鳴らしてるだけじゃ芸がないので、ステレオにしてみました。 パッドの振動に加えて、より臨場感あふれるマリオがお楽しみいただけます。

 ついでに、ある意味これがAPUの耐久テストとなるNSFサウンドの再生をやってみた。 サンプルとして、恐れ多くも例のコンペで優勝された方のオリジナル曲を使用させていただきました。 NES実機でのサウンド比較でも使用されています。聴いたらスゲッス。

 結果はオリジナルに遠く及ばず…実機が100としたら60くらいか。 なんだかいろんなところがまだまだだということがよーっく分かった。 でもよく見るとNMIから入る演奏位置からの復帰はRTSなんだよな。 だから変なアドレスに飛んでる。もうひとつ自前でRTIを挟むようにすれば…。 ということは単にイニシャライズルーチンの間違いか。 これで鳴りそうだ。

 ということで付加ルーチンを修正して聴いてみたところ、 高い再現性で演奏することができました。これはイイ!スゲエ! mp3にレコしてアップしたいっす。他のnsfも概ね良好で、 こうなったらN106とかほしいなあ。

! その気でいれば。

*耐久テスト まるでAPUが火を噴きそうなnsfだ!

エッ? NESでビブラートって可能なん?

この音圧はなんなんだ…。

Copyright(C) pgate1 All Rights Reserved.