正月休みに onion 100円disk5 「機種によってオケヒ音が化ける」謎を HAL8999 さんと検証していました。 おおまかな流れは HAL8999さんの j80近況報告 2014.01.04 http://www.geocities.jp/upd780c1/pc-8001/note.html に上がっているのですが、ここでさらに細かい(自分には衝撃だった)事実について追記しておきます。 YM2608のエミュレータや、今更音源ドライバを作成してるような人にしか役に立たないレベルの話だと思います^^; 100円disk5では、START ADDRESS/STOP ADDRESS を指定後、FM音源ステータスポートのBRDYを読みながら、EOSが立つまで、ADPCMデータを転送しています。 BRDYは、1つデータが書き込み終了した時に1に、EOSは、転送終了した時に1になる(…と2608のマニュアルの記述からは読み取れる)フラグです。 x86的に書くと、 ; 初期化、START/STOP指定後 loop1: lodsb call pcmdata_out ;$08(ADPCM_DATA)にpcmdataを1バイト出力 loop2: in al,dx ;Get 2608 STATUS (dx=OPNA address 裏) test al,8 ;BRDY Check jz loop2 test al,4 ;EOS Check jz loop1 と、一見ごく普通に見えるコードだったわけですが、ここに罠がたくさん潜んでいました…。 ● BRDY は 勝手に0にはならない FM音源のステータスポートは、 BUSYフラグ=「音源にデータを出力すると音源LSIがデータを処理してる期間1になる」 なので、同じように BRDYフラグ=「PCMデータポート(レジスタ$08)にデータを出力したら、データを処理してる期間0になる」 だと自分もこの検証前まですっかり思い込んでいましたし、上記コードもそれが前提となっていますが、調査でなんと間違いだと解りました。 「IRQ RESET」もしくは「MASK BRDYで一回マスク&解除」という明示的にクリアすることを音源に対して指示しない限り、 1になったフラグが0になることはなかったのです。今考えればTimerA/Bと同列の扱いなので、そりゃそうかと思えなくもないんですが…^^; ですから、上のコードでのBRDY Checkは実は無意味で、jz loop2 でloop2にジャンプすることは一切ありません。 2608のマニュアル内の転送手順サンプルも見直すと実は、1バイト毎に明示的にクリアする形で書かれています。 じゃあなんでこれでも転送できてるの?というのは、調査の結果、次のデータの受け入れ準備が出来る(BRDYを0にしたとして1になる)のは、 $08にデータを出力してからBUSYが1→0になるタイミングとほぼ同じか一瞬速いくらいで、つまり相当に速いので大丈夫、なのでしょう。 100円disk5では、pcmdata_outサブルーチン内で最初にBUSY待ちをしているので、それがBRDY待ち相当になっていたとも思われます。 ● EOS は $08にデータ出力後、即1にはならない EOSも、BRDYとほぼ同じようなタイミングで0→1になります(少しだけ遅いことがあります※後述)。 しかし先のコードでは、BRDYはずっと1なのでin命令は一度しか実行されず、それもデータを出力した直後なので、まだBUSY期間であり、EOS=1が取得できません。 結果としてもう一周回って、1バイト余計にデータを出力してしまうことになります(オーバーラン)。 ● STOP ADDRESS を超えてデータを転送してしまった場合、書き込まれるアドレスは START ADDRESSに戻る オーバーランしてしまった場合、そのデータは捨てられるか、STOP ADDRESS を超えた領域に書き込まれるか、とまず想像するところで、 もしそうなら大きな問題にはならなかったのですが、なんと実際は書き込まれる場所が START ADDRESS に巻き戻ってしまうことが判明。これも驚きでした。 100円disk5の場合、一度の転送では88のメモリ容量的に足りないため、二度に分けて転送されていますが、 二度目の頭が、問題だったオケヒ途中のエリアになっていて、二度目の転送の最後にオーバーランし、そこに1バイト上書きしてしまっています。 ● 転送すべきデータサイズは、(STOP ADDRESS - START ADDRESS + 1) x4(またはx32) である +1というのが曲者で、例えば x1 bit Modeでは、$4000バイトのデータをADPCMメモリ頭へ転送しようとした場合、START=0000 STOP=1000 と設定を間違えがちです。 正しくは START=0000 STOP=0FFF でなければなりません。 100円disk5では、ここも意図的かミスかは解りませんが4バイト多かったため、オーバーランの1バイトと合わせ、実質5バイト余計に転送していました。 なので結果として $8004 という半端なアドレス、かつ機種依存のデータが、オケヒ途中のエリアに上書きされていたのでした。 ● 蛇足 BRDYは、1つデータが書き込み終了した時に1になる、というのは誤りで、$08が準備できている状態ならば1になり「続ける」が正解です。 IRQ RESETや、MASK BRDYでマスク&解除をして明示的に0クリアをしても、その時点で準備が出来ていたらすぐに1に変わります。 前述の通りBRDYは、$08に書き込み後、BUSYでなくなる瞬間かその直前くらいに1になりますが、EOSは、BUSYが0になるよりも一瞬遅れることがまれにあります。 想像ですが、音源がBUSYでなくなった瞬間にアドレスが STOP ADDRESSに達しているのか判定し、フラグに反映している、のではないかなと。 また、EOSの方は0→1になるのはその瞬間のみで、BRDYのように、クリアしても1になり続けることはありません。 なので2608マニュアルの転送サンプルは、MASKを使ってわざわざBRDY「のみ」クリアしているのだと思われます。 ● 最初のコードをお手軽に修正するとしたら… ; 初期化、START/STOP指定後 loop1: lodsb call pcmdata_out ;$08(ADPCM_DATA)にpcmdataを1バイト出力 loop2: in al,dx ;Get 2608 STATUS (dx=OPNA address 裏) test al,al ;BUSY Check (BRDYは無視) js loop2 in al,dx in al,dx ;二回くらい読み直す(EOS立ち遅れ対策) test al,4 ;EOS Check jz loop1 …本当はEOSもアテにせず、転送するサイズは自前でカウンタを持つのが正解かと。BRDY、EOSの存在価値っていったい…