米国、台湾、日本の銀行手続き

Union Bank → US Bank (米国)

UFJ 銀行の口座を持っているとなぜか米国の Union Bank の口座が作れるというやつで 2018年 ごろに作りました. 滞在時以外は休眠口座となっていたのですが、昨年と今年は日本円の価値が少なすぎて米ドルで決済するようにしてます.

2023年6月ごろに Union Bank は買収された US Bank に口座番号作り直しシステム移行ってことで、去年の今頃はエラーばかりで地獄でした. 去年は米国内の送金で Zelle ってのを使ってみようとしたのですが、携帯電話番号を持っていないのでエラー. ほかの手段で試そうとしてもエラー... で銀行に電話したら携帯電話番号を持っていないとなにも使えないという残念な回答でした.

2024年2月ごろに再度試してみるとシステムの統合をかなり直したのか、Debit Card で ATM でおろせたり、オンライン(おそらく米国内)決済はできるようになりました. エラーで携帯電話番号を登録してないから使えないというメッセージもでるようになりました.

自分の online banking ではなぜか住所が米国滞在時のままで、Debit Card 認証ではそこの Zip Code をいれると通ります. 納税の書類は日本の現住所に届きます. ... 謎です.

US Bank の Debit Card を日本のATMで使う

  • 出金金額は1万円単位で最高金額は5万円から10万円のようです.
  • セブン銀行で試したところ10万円までです.
  • セブン銀行ATMでは引き出し額を右側「米ドルに換算した値を米ドルで相手の銀行に請求」か左側「日本円を相手の銀行に請求」の2択がでますが、右側は絶対に不利ですので選ばないでください. ギリギリ合法の詐欺です.
  • ちゃんと読んでも理解ないとよく考えず右利きは損をする右側のボタンを押してしまいます. 具体的には:
    • 右側: 公定レートから3.5%を手数料としてセブン銀行が取ります. さらにその決済額から手数料として 3% を手数料として US Bank が取ります.
    • 左側: 決済額を US Bank が公定レートで米ドルにした上で手数料として 3% を手数料として US Bank が取ります.
  • どのケースでもこれに $2.5 の手数料がかかるはずですがなぜかそれは返してくれました.

US Bank の Debit Card を台湾のATMで使う

  • 出金金額は 2 万台湾元が限度のようです.
  • 台湾銀行ATMの場合、通貨の質問はなく台湾元を US Bank に請求し、US Bank は規定の 3% + $2.5 手数料を取りました.
  • 國泰世華銀行ATMの場合、通貨の質問はあるので台湾元を US Bank に請求する選択肢を選びます. この場合2つの選択肢は上下にあり、詐欺っぽい文面でもありませんでした. 手数料は規定通りでした.
    • 決済後にカードを60秒以内に取れとカウントダウンが始まります. 私はここで手続きに60秒かかりますと勝手に解釈して待機してしまいました. ATMにカードを飲まれ翌日現金輸送車にきてもらい回収してもらいました. (大事になってしまった)
    • エラーになった決済は online banking には載ってましたが後日全額戻っていました.
  • セブン銀行ATMでは返してくれた $2.5 は返してくれませんでした.

US Bank → Wise → 日本の銀行

Wise 関連は提灯記事がWeb検索結果に大量にでてとにかく Wise の Debit Card を作れ!! とでて欲しい情報がまったくでてきません. この Debit Card は作らなくていいです.

米国内送金はできないようですが、これは例外的に. Wise と接続して外国の(JPY)通貨に両替して Wise の(JPY)残高にすることができるようです. まだ送金してませんが、この記事(2023年7月)では自動でできなかったと書いてある PLAID の接続はできました. わたしはこの経路ではまだ送金はしていません.
https://dattesar.com/usbank-to-wise/

この記事は知らずに3000円を預けて米国の口座を作り、そこに送金してもらったのですが送金相手はオンラインでできずに手数料を$25もとられたと言われてしまいました.

新光銀行 (台湾)

口座を作った 2017年当時、 Online Banking は IC カードリーダを自分のPCにつなぐ Online ATM がありました. これは Active X を多用しているとかで Internet Explorer のサポート終了で2019年ごろなくなりました.

それと一緒に新光銀行独自の online banking と携帯電話アプリ(Mobile Gardian)の2段階方式があります. 携帯電話アプリは携帯電話を新しくすると窓口で手続きする必要があり、2度ほどかえてもらっていました.

今年も同様の手続きにいったところこの Mobile Gardian は銀行員が知らず、銀行員が本部に問い合わせたところ、この方式は古いからSMS認証に替えると台湾の携帯電話番号を教えろといわれ、半ば強制的に方式を替えられてしまいました. その携帯電話番号は短期滞在専用のものでもう使えません...

今回の台湾旅行でホテルの予約で国内送金を多用できたのに残酷です.

みずほ銀行 (日本)

みずほ銀行では携帯電話アプリかトークンでの2段階認証に替えるとメールがきました. トークンを希望する場合は電話をしろとあるので電話して、住所と名前と誕生日と口座番号を伝えて書類の郵送を頼みました. とどいた書類は住所と名前と口座番号を手書きして返信用封筒にいれて、後日郵便ポストにいれることになりました.

UFJ銀行では10年前に同様の認証に替えるとメールがきて、オンラインで手続きしたら、トークンが来ました. (先月そのトークンの電池が切れてオンラインでt更新手続きをしました)

みずほ銀行の認証方式は10年間遅れています. 手続きはすべてアナログで個人情報を口頭で言って復唱させて、さらに送られてきた紙に書いて、郵送する. みずほ銀行は0120番号を設営し、電話番を雇い、往復の郵便代を払っています.

みずほ銀行を解約したくなりました.

開発日記

Disk System の data の続き. head が最外周に移動した直後に読み込みを開始したところ、予想通り data の byte の並びに対して数 bit ズレたデータを取得できた. 別のデータは data の初期値が 1 になることも確認した. 予想外なのはこの2つ以外のパラメータがまだあるということ.

この状態のデータは bit 7 か bit 0 が正常なデータと一致しないので、別の初期化が足りておらず、この状態から復元することは難しいと判断した. CRC のフラグが該当 bit の 出力に影響しているような予想も立ててみたが、関連性が不明. 具体的にはファイル開始からCRCのまでの長さは可変であるために、ハード実装としてCRCチェックを埋め込むには複雑すぎて挙動の予想がつかない.

C での文字列領域の確保

前置き(長い)

現在開発中の機材の不具合を分析していたところ MCU ではなく PC 側のソフトがよくないと判断をした. 問題の機能を修正したところ、プログラム終了時に segmentation error が出るようになった. その理由は memory leak であり、malloc はしたのに free を忘れているとか、free を多重にやってしまうことだ. つまり潜在的なバグが表に出てきてしまった.

文字列領域の確保と可変長配列 (C99 VLA)

自分のソースを分析したところ malloc で多いものは下記だった.

  • 可変長となる文字列領域の確保
  • クラスもどきの初期化と終了

文字列に関しては可変長であることから、1つの関数内で malloc-free が完結し有効期間が短い. クラスもどきは初期化と終了で行うもので有効期間が長い.

void func(int a, int b, int c)
{
	static const char format[] = "%d...%d %d\n";
	int n = snprintf(NULL, 0, format, a, b, c) + 1;
	char *buf = malloc(n);
	snprintf(buf, n, format, a, b, c);
	hoge(buf);
	free(buf);
}

これを可変長配列に変える.

void func(int a, int b, int c)
{
	static const char format[] = "%d...%d %d\n";
	int n = snprintf(NULL, 0, format, a, b, c) + 1;
	char buf[n];
	snprintf(buf, n, format, a, b, c);
	hoge(buf);
}

可変長配列について、実際にアセンブラレベルでどういう処理をしているのかは知らないので個人的には MCU 用に使う気はないのだが、性能が高い PC であれば気にはならない(と思い込んでいる). それよりも free を書かなくていいことは気分が楽になる.

上の例では malloc-free が1つの関数内であるので忘れにくいが、実際には文字列生成関数の中で malloc をして生成済みデータのポインタを返す例がかなりあって、その処理では free を忘れていた. 作った直後は忘れることはないが、後日その関数を使い回すと忘れてしまう.

この例を自分が書いたソースで多数発見した. 仕方ないので文字列生成関数は長さを得るだけと、確保済みの領域に書き込む処理の2つに分けて可変長配列を使う前提に変えた.

関数 main() の引数 argv を UTF-8 にする

Windows の main() の argv の文字コードは default code page となっているので、制限や問題が多く一般配布するようなソフトでは使えない. よって UTF-8 を使うことに仕様を決めた.

#if defined(_WIN32_) || defined(_WIN64_) 
static int utf8_main(int argc, char **argv)
#else
int main(int argc, char **argv)
#endif
{
...
}

#if defined(_WIN32_) || defined(_WIN64_) 
static void argv_wchar_to_utf8(int c, wchar_t **wv, char **v, char *dest)
{
	for(int i = 0; i < c; i++){
		int l = wchar_to_utf8_length_get(wv[i]);
		wchar_to_utf8_set(wv[i], dest, l);
		v[i] = dest;
		dest += l;
	}
}
#include <windows.h>
int main(void)
{
	int c;
	wchar_t **wv = CommandLineToArgvW(GetCommandLineW(), &c);
	int length = 0;
	for(int i = 0; i < c; i++){
		length += wchar_to_utf8_length_get(wv[i]);
	}
	char *v[c], utf8_buffer[length]; //C99 VLA
	argv_wchar_to_utf8(c, wv, v, utf8_buffer);
	const int r = utf8_main(c, v);
	LocalFree(wv);
	return r;
}
#endif
  • GetCommandLineW() について, 関数 main の argv を default code page → wchar_t → UTF-8 にすることも可能だが、無駄なので wchar_t を取得する.
  • CommandLineToArgvW() については結局 LocalFree というものが必要で本題と矛盾している.
  • この2つの関数は wmain() を利用することで不要となるはずだが、 msys2 ではうまく使えなかった.
  • 変数 length, utf8_buffer[] については長さを取得した都度変数宣言をすると scope の都合でうまく実装できないので for ループを2つにわける必要が出る.
  • main() での argv に渡される引数は元々は wchar_t で、utf8_main() の中で内容がファイル名である場合はファイル名を渡す APIUTF-8 を wchar_t にまた戻す、その過程でその逆の変換が多重に発生している. この無駄は今のところみなかったことにしている.

結局、矛盾と無駄は残りつつその中ではマシな実装ということになってしまった.

ファミコンのスクロールテクニックを確認した

前置き(長い)

最近息抜きに Castlevania 3 をやっている. そこで気づいたのは縦スクロールのための空白(空黒?)領域が広いということ.

この領域をなくすか考えてある程度プログラムを作った. この方針でも解決できるとは思うがプログラムが複雑になりすぎた*1こと、不具合の原因調査の過程で nesdev wiki に Split X/Y scroll というテクニックが載っていたので、これのほうが簡単に実装できると判断したので、思いついたアイディアはボツにした.

画面描画中に scroll register を更新する場合、特に Y 座標は CPU address $2006 経由で更新すると 8 pixel 単位の制限がつく. なのでスコアのようなスクロールをしない部分があるゲームで縦スクロールするゲームではスコアは下にあることが大半. スコアが上で縦スクロールするゲームで違和感なく実装できた市販品は悪魔城伝説忍者龍剣伝3の2本だけだと思う. だめな例はデビルマンの洞窟のシーンをみるとわかる. ゼルダの伝説に似たレイアウトは 8 pixel 単位で動いていることが大半.

Split X/Y scroll の自分の解釈

https://www.nesdev.org/wiki/PPU_scrolling
記事のほうが正確なので、自分の解釈はおかしいと思ったら元の記事を確認すること.

レジスタを4度書くだけでそれらの制限から開放される. 各種レジスタ t,v,x,y の動作原理は後述する. 用意することは下記.

  • x,y で 9 bit のスクロール値を作っておく.
    • x は通常の 9 bit の値
    • y は bit 7:0 は 0 から 239 の値を取り 240 から 255 の区間は計算して carray を bit8 に設ける.
  • 下記のプログラムで 4 byte の値を作っておき、スクロールを切り替えるタイミングでレジスタへ書き込む.

自分が作った算出プログラムは下記.

;注意: y bit7:0 の範囲は 0 から 239 (0x00 から 0xef) とする
scroll_bit_concat:
;3 = y8
;2 = x8
	lda	scroll_x_val+1
	lsr	a
	lda	scroll_y_val+1
 rept 3
	rol	a
 endm
	sta	w_2006_0
;7:6 = y7:6
;2:0 = y2:0
	lda	scroll_y_val
	sta	w_2005_1
;2:0 = x2:0
	ldy	scroll_x_val
	sty	w_2005_0
;7:5 = y5:3
;4:0 = x7:3
;y >>= 3
;3.times{
;  y >>= 1
;  x = (x >> 1) | (c << 7)
;}
;	ldy	scroll_x_val
	sty	w_2006_1
;	lda	scroll_y_val
 rept 3
	lsr	a
 endm
 rept 3
	lsr	a
	ror	w_2006_1
 endm
	rts

nametable やパレットはデバッガで出したデータを自分のプログラムに組み込んだ結果は下記のようになる.


サンプルプログラム制作所感

  • スクロールレジスタを書き換える部分はタイミングがかなり重要でできれば HBlank である cycle 256 から 320 の間に4度目のレジスタを書くとちらつかない. mesen ではこれを簡単に確認できるが、それ以前はかなり大変な確認である.
  • タイミング調整のためには IRQ のハンドラ内部では予め計算した結果をレジスタに書き換えるだけにしてループを含む分岐を一切なくす. jsr-rts もできればやらずインライン展開すべきである.
  • 制作したプログラムはスクロールレジスタの更新の後バンクレジスタを書き換える処理をいれたのでちらつきが発生している. ちらつきを避けるには割り込み期間は 2 line の時間を確保しておき、スクロールレジスタを書き換えた後に市販品のような空白領域を設ける.
  • 空白領域でバンクレジスタを書き換えた後、HBlank で空白領域を解除する.

市販品の解析

mesen の event viewer でみた所感.

  • 悪魔城伝説: 先頭の端数分で何も見えないバンクを用意して違和感がない. かなり試行錯誤をしていて無駄な処理が多いように見える.
  • 忍者龍剣伝3: 先頭の端数分で横scrollをすることで空白のnametableを割り当てて端数部を処理している. 処理は簡潔でタイミング調整がきれい.
  • デビルマン: 基本的なプログラムの質が低い. なぜスコア部を下に配置しなかったのか謎.

スクロールレジスタの解説

描画途中に CPU address $2006 を書くときに data で nametable address (例:$20a0)を書くと次の描画で 2 line ずれる. この port を書く用途は CPU address $2007 を介して、CPU から PPU 領域への読み書きに使うのは正しいが、スクロールを更新する場合は name table 参照元として $2xxx を書いてしまいがちで市販品もそれがかなり多いし自分も勘違いした.

CPU address $2006 の1度目の書き込みの data bit 5:4 へ書くと内部で保持されるレジスタは nesdev wiki では t の fine Y scroll と呼ばれるもの. t の fine Y scroll の bit 2 は 0 が代入されて、同 bit1:0 は CPU data bit 5:4 が代入される.

  • CPU address $2007 を介す場合は v の fine Y scroll の bit 1:0 は PPU A13:12 の出力として利用する.
  • PPU が描画として name table を参照する場合は v の fine Y scroll の bit 2:0 は PPU A2:0 の出力として利用し、 PPU A13:12 は 2'b10 が出力される.

この2つは理解しづらく、描画で 2 line ずれる原因となる. CPU address $2006 の1度目の書き込みで fine Y scroll の bit2 は 0 が代入されるのが制限の原因となる. これを回避できるのが Split X/Y scroll の手法である.

*1:8pixelで縦に調整した置いた絵を用意して IRQ を2回かけて空白をなくす

開発日記

database を改める

ROM の hash や構成は MAME の hash/nes.xml を正規化して SQLite にいれて、再集計している. ただ nes.xml の中身は信憑性が低いものが散見されるのでそれの集計や傾向を追っていると時間を浪費してしまう.

信憑性の低さを上げるには図書館や博物館の記録としての知識がいると思う. 集計だけしていても楽しくない.

そんなことを気にしながら本来の目的である、nametable control register がないソフトに対してどのドライバを使うのかという検索のためのデータを出力したり API を作った. 中断期間をいれて1週間かかった.

API を改める

それ以外にも新しい GUI に対しての API の更新が必要でそれの修正. 7月に設定ファイルの管理はGUI レイヤで GUI 担当者に作ってもらったらうまくいなかった. これは私の設計が悪いので、 GUI/CUI 共通部分は mruby レイヤでやれるように9月上旬に修正した. 修正したものの GUI の更新を放置してしまっており CUI だけで使えるようになっていたので、今回ちゃんと統合した.

この処理の都合でファイルの管理がかなり必要になった. 具体的には一時的なファイルの削除と作成、ディレクトリの作成がほしい. cruby ではここらへんは File とか Dir のメソッドが豊富なのに対して mruby はかなり貧弱になっている.

そこで mruby から C レイヤで msys がサポートしている mkdir() とか unlink() をその場しのぎにいれていたが、GUI の統合のために直した. 20年以上前から変わっていない(変えられない)話で Windows のファイル名は非 ASCII であると面倒である. 先述の mkdir() や unlink() のファイル名は最終的には Windows API にリンクするはずだが(未確認)、それらの関数が期待するファイル名の文字コードは default code page というやつで、日本語設定になっている Windows では Windows-31J という文字コードをいれればちゃんと動くが、他の言語を使っている場合は deafault code page は latin とか big-5 なのでだめになる.

これの対処は char * の Windows-31J なり UTF-8 を wchar_t * の Unicode に変換してから Windows API の名前の末尾が W の関数を使うのが適切である(たぶん).

この対応も中断期間をいれて1週間かかってしまった.

ファームウェアの修正にようやく入れる.

完成が見えない. つらい.

68000 アセンブラテクニックその2

move.[bw] #0,dn

clr.[bw] dn

clr.l では moveq に置換するの対して move.[bw] では clr.[bw] を使う. moveq に変えてしまうと使用容量と実行速度が同じ上に bit 31:16 が 0 になってしまうのでよいことはない.

話がそれるが clr 命令はレジスタのみに使う. clr 命令をメモリに使うのはよくない. マイクロコードの都合なのか不要な memory read が発生して遅い. 68010 以降はこの問題がない. bclr 命令はメモリを読んでフラグを更新してからメモリに書くので memory read の必要性はあるんだが、clr 命令はそれがない.

lea (an,dn.[wl]),an

src と dest の an が同じ場合に限る.

adda.[wl] dn,an

data pointer table 共通事項

jump table 共通事項の jmp/jsr がない. 基本方針は jump table と同じで dc.l を dc.w か dc.b の配列に変える.

このため index が pointer の数を超えている動作の場合は同等にならない. jump table では高確率でおかしくなるので元々のプログラム開発時に気づいて直していると思うが, data table は気づかずに動いてしまっていることが十分に考えられる.

pointer の中身が1種類

jump table の場合
	jmp pointer
data table の場合
	lea poiter,an

意外なことにこれが結構あった.

pointer が等間隔

pointer0
	dc.w	0,1,2,3,4,5
pointer1:
	dc.w	6,7,8,9,10,11

基本は data pointer table だけに適用する. jump table の場合は定数設定だけで分岐している場合だったので適切なコードを作った方がいい.

;間隔12の場合は index * 4 + index * 8 -> (index << 2) + (index << 3)
;として乗算をシフトと加算に変える. 
	lsl.w	#2,dn
 if near
	lea	(pc,pointer0,dn.w),an
 else
	lea	pointer0,an
	adda.w	dn,an
 endif
	lsl.w	#1,dn
	adda.w	dn,an

2の累乗ではない値の乗算回避のテーブルと思いきや間隔が 2 や 8 もあった.
加算回数が多すぎる場合は自動生成をやめて元通りテーブルを使うのも悪くはない. その場合は dc.l (→ lea; movea.l) から dc.w (→ lea; adda.w) に変えるほうが容量を削減できる.

pointer の中身が -0x80 から 0x7f で収まる場合

ほかの条件は jump table とやることは大体同じなので省略.

 if near
	move.b	(pc,table,dn.w),dn
	ext.w	dn
	lea	(pc,table,dn.w),an
 else
	lea	table,an
	move.b	(an,dn.w),dn
	ext.w	dn
	adda.w	dn,an
 endif
	...
	...
table:
	dc.b	pointer0 - table
	dc.b	pointer1 - table
	align	2

その他

pointer table の index を 8 bit でうち bit7 を別の意味を持たせている場合.

元のコード
	lea	table,an
	tst.w	dn
	bmi	xxx
	andi.w	#$007f,d7
	lsl.w	#2,d7
	movea.l	(an,d7.w),an
	...
	...
	...
xxx:
	andi.w	#$007f,d7
	lsl.w	#2,d7
	movea.l	(an,d7.w),an
	...
	...
	...
修正後
	lea	table,an
	andi.w	#$00ff,d7 ;bit15:8 を 0 にする
	lsl.b	#1,d7 ;.w から .b に変えて bit7 の有無をフラグにする
	movea.w	(an,d7.w),an ;movea はフラグが変わらない
	bcs	xxx
	...
	...
	...
xxx:
	...
	...
	...

src の bit0 を dest の bit7 へ代入

static uint8_t hoge(uint8_t src, uint8_t dest)
{
	dest &= 0x7f;
	if(src & 1){
		dest |= 0x80;
	}
	return dest;
}
;d0 = src, d1 = dest
	lsl.b	#1,d1 ;d1 <<= 1
	lsr.b	#1,d0 ;x=d0[0]
	roxr.b	#1,d1 ;d1[7:0]= {x,d1[7:1]}

68000 アセンブラテクニックその1

データ分離して逆アセンブルした状態で、プログラムから置換をするという試行. プログラム全体の流れをみることはないので正常動作としては100%同じ動作になることを優先.

clr.l dn

moveq #0,dn

move.l #imm,dn

imm は -0x80 から 0x7f に限る.

moveq #imm,dn

(add|sub)[ai]?.[bwl] #imm,ea

imm は 1 から 8 に限る. moveq と違って ea の制限が少ない.

(add|sub)q.[bwl] #imm,ea

(jsr|bsr) ea; rts

削除する rts にラベルが登録してない場合に限る.

(jmp|bra) ea

(jsr|jmp|lea) abs(,an)?

jsr,jmp,lea で例が多いので ea 次第では他の命令で利用可能.

ea が -0x8000から0x7fffの場合
(jsr|jmp|lea) abs.w(,an)?
ea が pc+2 との距離が -0x8000から0x7fffの場合
(jsr|jmp|lea) (pc,ea),an

bCC ea

bsr ではなく, ea と pc+2 の距離が 0 の場合
(削除)
ea と pc+2 の距離が -0x80 から 0x7e の場合
bCC.s ea
  • 元のプログラム作成側では省略すると .w になっていたような挙動なので削ることが結構できる.
  • 私が利用しているアセンブラの機能として .s か .w を書かないと最短にしてくれるがアセンブルに時間が異様にかかる.

jump table 共通次項

下記の命令を想定.

	lea	table.l,an
	move.w	ram,dn
	lsl.w	#2,dn
	movea.l	(an,dn),an
	jmp	(an)
table:
	dc.l	pointer0,pointer1,...

jump table が近い場合

  • jmp/jsr の pc+2 との距離が table が -0x80 から 0x7e の場合. jsr は近くにないことがある.
  • addressing (offset,pc,ix) を使うため offset が狭い.
pointer と bra.s の PC + 2 との距離が -0x80 から 0x7e で収まる場合
	move.w	ram,dn
	lsl.w	#1,dn
	jmp	(table,pc,dn.w)
table:
	bra.s	pointer0
	bra.s	pointer1

命令数は減るが実行数は変わらないと思う.

pointer の値が -0x8000 から 0x7ffe で収まる場合
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(table,pc,dn.w),an
	jmp	(an)
table:
	dc.w	pointer0
	dc.w	pointer1
pointer と table の距離が -0x8000 から 0x7ffe で収まる場合
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(table,pc,dn.w),an
	jmp	(table,pc,an.l)
table:
	dc.w	pointer0 - table
	dc.w	pointer1 - table

jump table が遠い場合

pointer と bra.s の PC + 2 との距離が -0x80 から 0x7e で収まる場合
	lea	table,an ;先述の table.w, pc(table) も併用する
	move.w	ram,dn
	lsl.w	#1,dn
	jmp	(an,dn.w)
	...
	...
	...
table:
	bra.s	pointer0
	bra.s	pointer1
pointer の値が -0x8000 から 0x7ffe で収まる場合
	lea	table,an ;同上
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(an,dn.w),an
	jmp	(an)
	...
	...
	...
table:
	dc.w	pointer0
	dc.w	pointer1

pc に関わらず利用できる.

pointer と table の距離が -0x8000 から 0x7ffe で収まる場合
	lea	table,an ;同上
	move.w	ram,dn
	lsl.w	#1,dn
	adda.w	(an,dn.w),an
	jmp	(an)
	...
	...
	...
table:
	dc.w	pointer0 - table
	dc.w	pointer1 - table

これはあまりいいものが思いつかなかった.

pointer が上記の方法で16bit以下に収まらない場合

pointer の最大値と最小値の差が 0x10000 未満の場合
center	set	pointer.min + (pointer.max - pointer.min) / 2
	lea	table,an ;同上
	move.w	ram,dn
	lsl.w	#1,dn
	movea.w	(an,dn.w),an
 if center < 0x8000
	jmp	(an,center)
 else
	adda.l	#center,an
	jmp	(an)
 endif
table:
	dc.w	pointer0 - center
	dc.w	pointer1 - center

一応動作確認したが pointer の最大値と最小値を算出するのはビルドの過程で効率が悪すぎる. table の中身が奇数になることもありちょっと変. また adda.l が増えるので遅いので実用に向いていない.

憂慮

長年 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 を分離する必要が出てきた.