憂慮

長年 web 検索に google を使っていたがおせっかいな機能(映画のネタバレのような知らないふりをして楽しむことに事実を目立つ位置に堂々と書いてきた)とUIがおかしくなってきたことが原因で別の検索エンジンを検討している.

google が直接悪くはないが、プログラミングや数学について調べごとをすると、参考にできないような試行錯誤の報告がでてくるならまだいいものの、現役xxxが解説!!という google が間接的に悪くしているものがあまりにも多くなりすぎてる. 特定のドメインを消すプラグインがいまはたくさんあるのでいれてみたが、そういうツールはいつの間にか使えなくなることが何度もあったので気休めかもしれない.

プログラミングや数学については規格書原文、開発ツール提供団体、大学、試行錯誤した個人の報告順に優先順位をつける. 個人の報告は利用した手段を提供団体や規格書まで遡って信憑性や妥当性を確認する.

ruby ができて mruby にできないことは提供団体が提供する C レベルの実装に対する文書があまりにも少なく10年前の個人の報告をもとにヘッダファイルを読むことがある(例えば Array.size に相当する C レベルの呼び出し). 運が悪いとソースコードを読むことになり、本来の目的から離れていく.

その点では Microsoft ドメインが提供する WindowsAPI の解説は優秀である. 検索でもノイズをかき分けながら頭の方にでてきてくれる.

開発日記

DMA 転送途中への USB での送信

8月中に実装した機能で録音と disksystem は相手のデバイスを動かしながらデータを取得し、バッファが半分溜まったら USB で送信するというリングバッファ状態を実装していた. ここまで下地を作ってしまったので ROM dump でも実装をしてみた. 結果はシミュレータの実装にかなり手間取ったがちゃんと動いた.

転送速度

1 byte あたりの転送速度は下記となる.

  • voice: 48 kHz (1 sample 12 bit で、サンプル頻度 24 kHz)
  • disk: 12 kHz (かなりブレるので早めの参考値)
  • ROM: 747 kHz (これもブレるので早めの参考値, 740 kHz の場合もある)

voice は DMA を利用しているが, disk では evsys の制約から DMA を利用できないので (MCUの中の) CPU でバッファへ転送している. このレベルであれば ROM は相手が動くのを待つことを無視できるので MCU への実装次第でもっと早くなるが、安いこの MCU の機能を利用して安定させたらこの速度が限界と思われる.

ROM 向けの DMA の実装

DMA channel を 3 つ用意し AH (出力, A15:8), AL (出力, A7:0), DR (入力, D7:0) に割り振る. AL は 0x100 byte 周期で descriptor 1個無限ループ. ここまではいままでの実装.

DR を descriptor を 2つ用意し、buffer を半分に割り振り、descriptor 2つで無限ループ. descriptor 1 つが終わったときに evsys 経由で割り込みをかける. 録音で実装済みで使い回す.

AH は buffer は buffer size を 0x100 bytes 単位で割り振り, 最大 0x100 bytes を 1 descriptor とする. 例えば buffer が 0x550 bytes, 開始アドレスが 0x0080 なら下記となる.

 # data  count link
 0 0x00  0x80  1
 1 0x01  0x100 2
 2 0x02  0x100 3
 3 0x03  0x100 4
 4 0x04  0x100 5
 5 0x05  0x0d0 end

これがいままでの実装でこれを下記のように AH の DMA を #1 から #5 で無限ループにする.

 # data  count link
 0 0x00  0x80  1
 1 0x01  0x100 2
 2 0x02  0x100 3
 3 0x03  0x100 4
 4 0x04  0x100 5
 5 0x05  0x100 1

DR descriptor の1つが終わると 0x2a8 bytes 読んだので address 0x0080 + 0x2a8 = 0x0328 まで読み終わったので descriptor の参照先の data の 0, 1, 2 を更新する. (ただし data 0 は無限ループ外なので書き換えても意味がない)

 # data  count link
 0 0x00  0x80  1
 1 0x06  0x100 2
 2 0x07  0x100 3
 3 0x03  0x100 4
 4 0x04  0x100 5
 5 0x05  0x100 1

DR descriptor が終わるたびに更新を続ける. address 0x1f7f まで読み込むとすると AH の参照先 data 0x1f で count と link を書き換えてしまう.

 # data  count link
 0 0x00  0x80  1
 1 0x1f  0x80  end
 2 0x1b  0x100 3
 3 0x1c  0x100 4
 4 0x1d  0x100 5
 5 0x1e  0x100 1

感想

参照先を書き換えるのは問題ないのは確認しているが descrptor を書き換えてしまうとちゃんと動くのか少し不安だったが動いてくれた.
今回の実装での手間はこの descriptor と参照先の書き換えをシミュレータで実現すること. C だけで作っていればポインタで更新されるが、 mruby の変数にしていたのでこれを更新する仕組みを作るのがかなりの手間.

一旦実装は出来たものの末尾の更新がうまくいかずにかなりの時間を要してしまった. 上記のリストであれば #0 から更新していたので #1 の link が end になっていると #2 以降が古いままになっている. これに気づくのに2日間、一番古いデータから(上記では#2 から)更新する仕組みを思いつくまで2日間かかってしまった.

今後は flash programming 以外の基本機能はようやくこれで実装したので後回しにしていたUI周りをちゃんとつくる.

開発日記

Disksystem の対応

この手の ROM dumper で diskimage を dump できるのかをようやく挑戦した. カードエッジの CPU(2A03) IRQ# を MCU の外部割り込み端子とをこの目的のために接続してありようやく動作確認した. 結果は数日で diskimage を dump できるまでになった. いまは解析資料が豊富にあるからとても助かる. nesdev wiki のひと、そこに貼ってあったリンクの方々、ありがとうございます.

GAP 区間と RAM アダプタ

RAM adapter からデータを取る場合に特有な問題は GAP 区間に read/write にするレジスタを有効化し、正味のデータの区間が終わった後はそのレジスタを無効化する必要がある. これをやらないと次の正味のデータ区間の中身がデタラメになってしまう. この制約をクリアするにはハードにより近い MCU で効率的に行う必要があり、PC からスクリプト経由で随時命令を送ることが困難である.

この制限のせいか、エミュレータFDSファイルは正味のデータのみで、正味のデータでも末尾のCRCは捨てている.

実装途中はこの仕様を理解していなかったのでデタラメにレジスタを有効にしていたもので、1度のシーケンシャルアクセスで正味のデータが1区間だけ見えるということになった. ただ GAP とか ブロック開始符号が見えたので、試せば正味以外のデータ区間も見ることができるのではないだろうか.

でもディスクドライブのコネクタに直接つなぐような機材は実在して安く手に入るし、そっちでは完璧なデータが効率よく見えるのはわかりきっている.

ディスク末尾のデータ

FDS ファイルは正味のデータだけを1面 65500 bytes と定義されているがファイル(ブロック)を増やせば GAP 区間は増えて正味のデータ区間は減る. この矛盾を理解できていないので、ディスク末尾はどこまで読めばいいのかよくわからない.

そのせいなのか、私の作った dumper では末尾に配置されるセーブデータが dump できていないとか起動しないというソフトが1つずつ見つかった. 逆に言うとそれ以外はほとんど問題がないらしい.

MCU の話

外部割り込み自体はあっさり認識したものの、 DMA の CTRLB と EVSYS のあたりがまたうまく行かずに RAM アダプタからの IRQ をトリガに動かすということに苦労した.

GAP 区間では待ち時間の対応を入れるためにタイマのソースコードを書き直した. DMAのつなぎ合わせで待ち時間 2 clock だけのタイマを大量に利用しているとかでタイマが足りなくなっているため、必要応じて切り替えを多用することにした. これは実はかなりキツイので実装の見直しを考えた方がいい.

例えばアクセスランプの点滅にタイマを確保していたのだが、これもやめたほうがいい. 10から60Hzの点滅にタイマを使うぐらいなら割り込みのたびにソフトでIOを ON.OFF したほうがいいと思う.

ソフトウェア面では先述のディスク制御特有の処理のために C で実装したので ROM の空きが 10% を切ってしまった. ほかは RAM 管理を静的に確保するのに限界をとても感じているので動的に確保するようにしたい. 今回のせいで C++ ではないのにヘッダファイルが200行にもなってしまった巨大な struct を分離する必要が出てきた.

開発日記

複数の似た処理の統合

かなり手間取ったが統合できた. 入力命令の解釈が不適切で DMA 2ch で動かせる内容を DMA 4ch で動いていて結果としてはあっているという問題が直せた.
data を increment-pattern でいれると MCU が止まってしまうバグがあったが、統合時に increment-pattern と無限ループに対する定義が仕様書になかったので明示して、それに準拠した.

DMAC が RAM に結果を書くタイプの設定不具合

ソースがきれいになったのでいままでやれなかった電源投入後のDMA開始の調整と DMA descriptor と EVSYS を再設定無しに再実行を実装.

ADC が動かない. これまで3度の改訂で処理を少しずつ変えているのだが毎回理由不明で苦しめられる. 今回に関しては再実行で read した data (DR) の先頭にゴミデータが入るという不具合も発生.

今回の傾向から判断するに...

  • DR と ADC は EVSYS 経由ではなく CHCTRLB.TRIGSRC から取って来るものである.
  • CHCTRLA.ENABLE = 0, CHCTRL.SWRST = 1 にしても改善しない.
  • DMA 完了割り込みのときのみに CHCTRL.SWRST = 1 するとなぜか改善する.

以上から CHCTRLA.ENABLE = 0 であっても TRIGSRC を書き換えていないと、DMA ch は TRIGGER を認識しているが ENABLE = 0 なので pending になってしまって DMA がつまるのだろうという仮説ができた. 今回の実装では DMA 完了割り込みのあとに SPI を動かしたりすること、 TIMER が止まってないと ADC が再度動いてしまうことが予想できたからだ.

ということで CHCTRLB = 0 (全ビット)としたら改善した. DR と ADC の channel の TRIGSRC のみ = 0 としたがこれは改善せず、全ての channel の CHCTRLB = 0 としたら期待通りになった. 他の要因は EVSYS につなぐことなんだろうけどここらへんは直接レジスタを見づらく対処に困る.

TRIG がつまってしまった場合にそれを明確に止める方法がないのが対策に時間がかかった.

開発日記

前振り(ながい)

昨年8月まで設計は 74595 を 3 つにして、 CPU PHI2, CPU ROMSEL といった制御線は GPIO で制御していたが、 flash programming の性能がよくない(具体的にはソースが複雑になりすぎる上に遅い)ので制御線も 74595 を 1つ追加して DMA で動かすことにした.

ROM dump では address が increment, data 出力は不要. この場合は動かす address 16 bit で 74595 は 2 個ですむため追加した制御線の DMA を利用するかは条件を設定して切り替えるようにした. 一方 flash programming は制御線を DMA で激しく替える. ROM dump で制御線もいれて動かすこともできるが処理速度は半分になる.

複数の似た処理の統合

このため制御線の DMA を利用するか否かはソースでは処理の途中で新旧の実装でかなり分岐していて、最後にハードウェアを動かすところで1つになる. 途中の分岐は無駄が多いのでできるだけ減らすのが今回の目的.

ハードウェアに近いレイヤの統合は予想外に早く終わったのでその上位のレイヤの統合をし始めた. こちらは難易度が高く、統合ができていない. 統合の処理は使い回す関数をコピーして flash programming のコードは一旦削除して、簡単に構造に戻している.

転送の限界調査

ハードウェアに近いレイヤの統合の時点で ADC の Sampling Freq を可変値にすることができた. 音質は重要ではなく sampling freq を上げていって、どの速さまで限界を知りたい. できれば ROM dump の DMA が動いているときに USB の転送も ROM dump より早く動いてくれているとわかればもっと早くできるし、DMA を頻繁に止める処理もいらなくなる.

sampling freq は前回は定数 16 kHz だったので、20 kHz, 30 kHz と上げていったが 21 kHz で止まった. 一番長い音声が約3秒で、21 kHz の時点でデータ量が約 65,000 bytes になっていた. 止まった理由は一度に転送できるデータを 16 bit に指定していてそれを超える量を想定した設計になってない. つまりプロトコルの再設計が必要.

Cにおけるラベル直後の変数宣言とswitch

void hoge0(int v)
{
	//{} がない switch, 分岐が1つだけで使い物にならない
	switch(v)
	label: break;
	
	//{} がないので label は有効
	goto label;
}

void hoge1(int v)
{
	//{} がある switch 
	switch(v){
	case 1: break;
	label: break;
	}
	
	//label: は switch{} のそとでも使える (なんで?)
	goto label;
}

int hoge2(int v)
{
	switch(v){
	case 1: //error, case x:, label: の直後に変数宣言はできない
		int i = 0;
		break;
	label: break;
	}
	return i;
}

int hoge3(int v)
{
	switch(v){
	case 1: ; //ダミーの ; をいれれば変数宣言は通る
		int i = 0;
		break;
	label: 
		i = 2; //i は有効
		break;
	}
	//error, i の有効範囲は switch の {} に制限されている
	return i;
}

int hoge4(int v)
{
	switch(v){
	case 1: { //ダミーの {} をいれれば変数宣言は通るが
		int i = 0;
		}break;
	label: 
		i = 2; //error, 上の {} の外では i は 無効
		break;
	}
	//error, この i も無効
	return i;
}
int hoge5(int v)
{
	switch(v){
	int i = 0; //ラベルをいれなかったらこの変数宣言は有効
	case 1:
		i = 2;
		break;
	label:
		i += 3; //この場合, i の宣言だけが有効で代入(i=0)は無効
		//case の前か後かは関係ないようだ
		//コンパイラが未初期化の値を利用していると警告した
		break;
	}
	if(i < 4){ //error, i の有効期間は {} の中まで
		goto label;
	}
	return i;
}

開発日記

カードエッジの Sound Out 端子から MCU に配線し ADC を使う. これによって燃えプロ系のボイスをアナログ経路で録音することに成功. *1

録音手法

  • TCx->EVSYS->ADC, ADC->DMA->EVSYS をつなぐ.
  • TC から ADC はタイマの OVERFLOW イベントを ADC につなぐ.
  • ADC から DMA は EVSYS でもいけると思うが、TRIGSRC からでもいける.
  • DMA は block 単位転送完了を EVSYS でつなぎ、割り込みをおこす.

この実装に関して EVSYS の仕様書を見たらバスのつなぎ方に非同期、同期、再同期がありデバイスが求めるバスを適切に接続する必要がある. それに気づいていなかったので全部非同期にしていた. それを直すのに手間取った. 直す過程でバスサイクルが少し悪くなったので後日要調整.

音質に関して

人の声レベルなので元々の音がサンプリングレートが低くく、 MCU の汎用の ADC で取るには結構マシな音質であった. Youtube でのプレイ動画の音質と比べても素人の私には大体同じに聞こえた.

DMA に関して

block 単位の転送完了の割り込みは DMA 単体からは suspend 動作がついてしまうが、 EVSYS 経由で割り込みをおこすと suspend がつかない.

voice の場合は音声再生時間が長くなると MCU の RAM の容量を超えてしまうので、録音途中にPCへ随時送り続けるようにしたい. バッファRAMを半分で2つに分ける. 1つの block を DMA の書き込み、もう1つの block を読み込み(PC へ送信)にする.

この場合、DMA descriptor の使用数を転送サイズ/バッファサイズ/2 にすることも可能だが、 2つの descriptor で無限ループを作ることも可能. 実は以前から address の increment pattern の bit7:0 は 1 つの descriptor で無限ループにしている. address 出力は MCU からは write (送信) なので割り込みはいらない.
この転送途中に割り込みをかけて随時送る方式は今後他の目的でも使いたいので ADC をネタにちゃんと実装したいので実証実験も兼ねている. 実のところ MCU 用のソースコードは早い段階でできているのだが、シミュレータでは無限ループ型の DMA は先述の1つのパターンのみ対応でそれの対応に手間取っている. だから動作確認がとれてないので MCU で動かすのは意味がない.

基本MCU用のソースだけ書いても1発でちゃんと動くわけがないし、デバッグ情報がとれないので MCU 用のソースは依存部分をできるだけ排除して PC でもほぼ同じコードを動かしてデバッグ情報を得ることで直したほうが早い...早いはウソでこれがないと直せない.

*1:注意: 音源ICへカートリッジ内部からクロックを供給しているから録音ができる. 燃えプロ系以外は内部でクロックを供給していないことが多い.

開発日記

休止期間も含めて3か月もまともに動作しなかった LZ93D50 とその周辺の対応の大半がようやく終わった. variant が2つあってそれはまだ対応してない.

問題その1/未完: write cycle で MCU が死ぬ

CPU_RW の配線が不適切だったので OR gate をいれて修正したことで発生頻度がとても下がったが、まれに起きるので完全に直ってないと思う.

問題その2/完了: register への write cycle を認識しない

register への write cycle の前には必ず read cycle がいる.

問題その3/完了: read cycle と write cycle を1度に含めると通信がおかしくなる

長らく動作不安定の原因として悩まされたもの. 原因究明が問題その1と問題その3の切り分けに相当時間がかかり、カートリッジに関係なく MCU 単体で問題が発生する.

MCU のソフトの実装が悪いまではわかったものの、明確におかしい場所の特定ができなかった. おかしい場所は3つの送信処理を3つにわけていたのでうち2つを1つにまとめて送ることで直りそうな気がした. その後も原因特定ができなかったので直る保証がないまま実装したところ問題その3は起きなくなった.

問題その4/完了: 静的なバッファ管理の限界

問題その2とその3の対策として複合的な問題. MCU へ送る命令は複数記載できるものの容量は 0xc0 bytes, ROM dump での容量は 0x400 bytes と固定になっていた. これで問題その2の dummy read を実装した上で EEPROM を効率的に制御しようものなら 0xc0 bytes は少なすぎる. そして ROM dump の buffer はこのときはそんなにいらない.

また backup RAM (パラレルのSRAM) write でも同じ問題があり、その場しのぎに無茶castしてたが限界を感じたので動的な確保をすることにした. これに関して MCU 内部に各種 alignment 制限があるので動的管理領域を100%使い切れることはないが、柔軟性が増えた.

問題その5/新規: MCU の ROM 容量の限界

firmware を修正したり効率的な機能を実装していったら残り 0x1000 bytes を切ってしまった. RAM では場合によって使わない部分は動的管理すれば資源はわりと使い回せるが、 ROM ではそういうことができない.

もうちょっと機能を追加したいのでそれができた場合に使用率95%では将来性が全くない...

問題その6/新規: flash programming の再実装

問題その4やその他の過程で flash programming に関する RAM 管理や今見ると不必要に複雑で何をやっているかわからないコードをいったん捨てて実装したのでちゃんと動くように戻す必要がある.

古いPCゲームソフトとMIDI ファイルの読み込み

ソフト開発としてつまっていることはないがスランプになってしまってコードが見たくなり別件が入ったことで停滞中... 作業量が少ない対応として Windows 98 時代のゲームソフトの修正をしてみた.

BGM として MIDI (.mid) ファイルを利用するのだが、ファイル再生前に5秒間ぐらいソフトが止まる. 原因を探った. (Windows ソフトの hack はほとんど経験無し)

Win32 API として当時の midi の再生手段は mci を利用して mciSendCommand() か mciSendString() を利用するらしい. 該当のゲームソフトを逆アセンブルしてみた、 UsaMimi32.exe から実行中のプログラムを逆アセンブルしたり、ブレークをかけることができる. (すごい)

解析手順

  • モジュールを逆アセンブル → 逆アセンブル実行 → 検索:APIコールで mci を入れる
  • 検索結果で mciSendCommand() を利用した数か所でてくる
  • mciSendCommand() の実装前に引数の確認. ABI はちゃんとよんでないが引数を逆から push するらしい.
    • 第2引数の uMSG は immediate を push していたのでその値の意味を調べる. mingw では /mingw32/include/mmsystem.h に定義があった.
    • mciSendCommand() を呼び出してる箇所で引数が MCI_OPEN の場所は2個に絞り込めた
  • ブレークポイントの挿入/削除でコード実行ブレークポイント(INT3) で該当の2つの PC (=EIP) をいれる (ここで書き写しが発生するのがちょっと...)
    • ゲーム側で音を鳴らしてみて2つの PC は midi と wave で別れていることが判明した.

API をみる

midi を利用するための関数の逆アセンブルされた命令*1を眺めてみる.
http://www13.plala.or.jp/kymats/study/MULTIMEDIA/mciCommand_play.html このサイトの説明通りの手順らしい. そこで第3引数の lpOpen のもととなる変数をみたところ, ここの説明通りで下記のようだった.

static MCI_OPEN_PARMS mop;
mop.lpstrDeviceType= "sequencer"; //.exe ファイルの(たぶん) rodata section にある
mop.lpstrElementName= xx; //ini ファイルから読んだ文字列をわたしていたので動的確保された領域のようだ

これの原因は上記で言う変数 lpstrDeviceType が "sequencer" の場合は Windows NT 系(2000,xp,それ以降) だと遅く "MPEGVideo" だと早いらしい. ただしなぜそうなるかはちゃんとした文書が見当たらなかった. そしてこの議論は2005年にされているがおそらく 2000 年の時点で明らかになり20年間以上放置されているらしい.
https://www.activebasic.com/forum/viewtopic.php?t=99

以上より .exe ファイルのなかをバイナリエディタで探り "sequencer" を "MPEGVideo" に書き換えることで問題は解消された*2. あとは ElementName の参照先が記載された ini ファイルのファイル名を書き換えると MPEGVideo で開けるファイルの種類がたくさんあるので mp3 も使える.

レジストリでは aiff も "MPEGVideo" で使えるらしいのでやってみたい. 噂で aiff はループ再生ができると聞いており、仕様書では loop の記述を確認したが、編集ソフトでそれに対応しているものは今はないだろうし、 mci でそれが動くかはわからない.

他のソフト

同様の現象があったがこのソフトでは関数は mciWndCreate() で動いているようでそこには DeviceType を明示する方法が分からなかった.

*1:私は 80386 はほとんど知識なしなので addressing mode がよくわからない

*2:注: "" で囲んでいるのは \0 終端文字列である

開発日記

現在は協力者にファミコンソフトを大量に借りて、 dump 機能の動作確認をしている. 意外とやることがおおく flash programming の機能修正ができていない.

設計初期から構想があった nametable control register によるカートリッジハードの検出を実装した. 個人的には優秀な機能だと思っているが欠点もいくつかある.

利点:

  • わりと早い.
  • nametable control で PPU A10,A11 の MUX を介さない TKSROM のような CHR ROM Address MSB を VRAM A10 に配線する variant の検出の信頼度がとても高い.
  • ROM data を参照しないのでバージョン違いを誤認しない.

欠点:

  • nametable control register がない CNROM, UNROM その他単純なハードの検出ができない.
  • MMC1A を利用したハードでは backup RAM に write protect register がついていないので、この領域に nametable control register があるハード(Taito-Xxx ほか数種類)の検出でバックアップデータを消してしまう.
  • VRC1-VRC2, X1-005-X1-017 のようにレジスタ仕様が全く同じだと別の異なる仕様を確認するがそれは ROM bank の違いぐらいなので、ROM data の内容によっては誤認する.

variant hardware の発生原因は大まかに2種類あり上記の nametable 制御と work RAM の容量の違いである. nametable 制御は先述のようにほぼ完璧に検出できる反面、 work RAM の容量とそれに派生する電池の有無の検出は実装しづらい. その理由は30年以上経過しているであろう電池が死んでいると検出を誤認する点である.

今回 +5V の電源スイッチは MCU の GPIO から操作できるので、一旦電源を止めてしばらく待って再度電源をいれてデータが残っていれば電池があると判別の実装は可能. また電池が生きていたとしてもセーブデータを誤って消すリスクがあるのでそれは問題となる.

以上の点からハードウェアの検出は ROM hash (dump 前例)を起因とするデータベースなんかつかわないと豪語しておきながら、電池の有無はデータベースを利用する実装にしてしまった.