flash への不揮発データ書き込みの高速化検討

W29C040 への書き込みは 0x100 bytes 単位であり、この転送がとても早いことにいまさら気づいた. 1 byte 単位への書き込みでも polling なしに 0x100 bytes 単位で書いたほうが早いのではないかといまさら気づいた. byte programming data の command 発行から不揮発データ書き込み完了は typ(ical) 7 us から 9 us, SST39SF040 は typ の記載がなく max 20 us となる.

一方 MCU から SPI 経由では 1 memory cycle が 4 SPI cycle となり、これが実測 2.0 us 程度で、コマンド発行に 4 memory cycle がいるので 8.0 us となる. よって片方の flash で待ってる間にもう片方へコマンドをいれると書き込みの待ち時間は減らせる.

ここで2通りの転送方法を検討する.

従来の 1 DMA cycle を拡張

従来の 1 DMA cycle は CPU memory へ command 発行 → PPU memory へ command 発行 → CPU memory へ polling → PPU memory へ polling としているから、(CPU memory へ command 発行 → PPU memory へ command 発行) x n → DMA 終了として polling を省略してしまう. n は 4 から 8 程度をいまのところ想定している. これで単純計算で n 倍早くなる.

利点はファームウェアの変更が少ないことと、memory で 0xff で埋まってる場所を飛ばすとか小さすぎる sector erase の対応(未実装)などの細かい処理を入れやすい.

欠点は次の方法より遅いことと descriptor を多用するので MCU の RAM を消費しがちである. 現状としては RAM (.data) の容量不足は感じておらず、 flash (.text と .rodata) の容量不足は確定的で、flash の容量が多いモデルにすることが決まっており必然的に RAM の容量が増えるから大したことではない.

1 DMA cycle で動的に拡張

1 DMA cycle は (CPU memory へ command 発行 → PPU memory へ command) x n として DMA 動作中に別の DMA をかけて SPI 出力前にデータを更新するなり、n 回ループを達成できたら DMA を止めてしまうなどを MCU のデバイスだけで動かして、 MCU の中の CPU は関知しないという方法. n は 0x100 単位を想定している.

この方法を検討したところ、address の部分は TC をカウンタとして利用すること address と data の更新用の DMA を設けるなどすれば動かせることを確認. ただし、 TC は 2 つ, TC または TCC は 1つ、 DMA ch は 4 つ必要となる. address の increment counter は TCC でもできるのだが、 TCC はカウンタ読み出しコマンドの発行が必要でこれが手間である(TC はいらない). ただ、TC は 16 bit で比較することに制限があるのでこれだけのための DMA を止める条件に TCC のカウンタが必要となる.

この動的更新の確認はできており、そのついでに EVSYS の組み方を修正して、タイマで 1 memory cycle にわざと待ちをいれることも可能にした. typ 8 us では動かないこともあるとか、 typ 25 us のデバイスもあるので polling をしない代わりに周期時間の調整は重要となる.

この方式は data 0xff を飛ばすとか、sector size を考慮するとか細かい部分で並列的に動かすというのは実質不可能なので、chip erase や erase に対する polling は同時、バンク切り替えも同時、PCからのデータ更新も同時として1まとまりに動作させることが必要になると予想される.

erase の polling

これは必要であるが、現状の方式では programming cycle のあとに必ず polling をいれていたので、erase と programming の polling は兼用となっていた. 片方がなくなるので polling の組み直しが必要となってしまった.

米国と台湾の銀行の話

US Bank → Wise → 日本の銀行

PLAID の接続後、 自分の US Bank 口座 → 自分の Wise USD 残高へ送金できました. 手数料は 0.13 % (0.0013 倍) でした. この後 Wise で日本円に両替して(手数料$2ぐらい)、自分の日本の銀行口座へ日本円で送金(手数料200円)できました. 自分の操作はすべてPCからオンラインでかなり早くできました.

これは立て替えの精算が目的です. 外国人(私)が Zelle を使えないなど、事情を話して送金してもらっています. 相手はわざわざ US bank の店舗まで行って送金してくださっているようです. だから、商売として同じことをしてほしいというと渋い顔をされそうです.

新光銀行 (台湾)

新光銀行の Online ATM (網路ATM *1 ) が廃止されたのは事実なのですが、台湾の別の銀行は IE + Active X なしでドライバを提供することでサポートを続けているところがすこしあります.

これを利用してキャッシュカードをICカートリーダにつなぐことで他行扱いで残高を見ることはできました. いまのところ必要がないので試してませんが少し手数料は増えますが送金もできそうです.

今のところ大手でもサポートする銀行(台湾銀行,台北富邦銀行)と廃止するところ(郵局)がわかれているので網路ATMはそのうち全部なくなるかもしれません.

*1:網路銀行と別,それは日本のオンラインバンキングと同じ

Flash Cartridge Troubleshooting: Challenges and Solutions

タイトルはAIが決めた. 内容は開発日誌でローカルのテキストファイルからの抜粋.

04-30
基板 TxROM と Memory AM29F040B の組み合わせは終わったので、以前の別の flash cartridge を出して動作確認開始.
基板 VRC6B と Memory W49F002 と W29C040 をみて W49F002 の使用確認とドライバの実装. ただしこれは変則的な設定がいるので一旦保留.
基板 UOROM と Memory MX29F040 を先に試す.

05-01
基板 UOROM と Memory MX29F040 は簡単にいける予想は外れて ID を取るまでにチェックしていない部分が大量にあり実装に手間取った. UOROM の場合は単純な配線では command address A14 の都合で program できない.
device で command address A14 がある場合に途中でエラーを出して停める必要があるが今のところ簡単にできないため、 VRC6B の組み合わせの対応をみてから実装したほうがいい.
基板 VRC6B はシミュレータを dump ができるまで実装. CPU address $8000-$bfff と同 $c000-$dfff の可変バンクを2つとも有効にすると、バンクサイズがあわないのでうまくいかないことを再確認.
programming に関しては flash address A14 を計算して, 15'h2aaa と 15'h5555 を割り振るアドレスを交換する必要がある. (どちらのバンクでも可能) ただしスクリプトの実装でこれを交換するようにできていないのでこれらのパラメータをクラス化したうえで VRC6B だけの例外実装を組めるようにする必要がある.
明日あさってで実装がうまくいかない場合は当分の間保留にする.

別途 UOROM の配置について flash A14 = CPU A14 とする方法もあるが, 現状のシステムで sector erase までちゃんと動かすにはかなり難しそうである. 管理可能容量が 0x44000 bytes (17 pages) になるので nesfile format としても変になる.

05-02
基板 VRC6 の flash ID 取得について例外的なクラスを実装.

05-03
W29C040 のコードを通すが assert で止まった.

05-05
assert で止まる理由を探したところ、 1 回左シフトにすべきところを 10 回左シフトになっていて、mruby の配列から uint8_t *の変数にできないというエラーだった.
その過程で mruby, C ともに W29C040 のコードを通っていない部分が多々あり修正した. とりあえずシミュレーションは通ったが、 fifo の buffer size は 0x100 の倍数である必要があるはずでバッファサイズの確保のコードを確認する必要がある.

05-06, 05-07
バッファサイズのエラーチェックを追加.
基板 VRC6 への command address A14:0 を確保する例外的なバンクアドレスの確保について実装をするため、 class ProgrammerBase にあった変数を class CommandAddress, class CommandDestination に分離. また class DriverFlash にもメソッドを分離. 中途半端な変数を除去し、 VRC6 への command address の例外的実装を継承したクラスとして実装、動作確認.
この動作自体は問題なかったが別途問題が発生.

  • VRC6 への programming は 0x4000 bytes の bank でやっているが最小 bank 単位は 0x2000 bytes で ROM image が 0x40000 bytes 未満で layout=top だと矛盾が発生してちゃんとかけない.
  • W29C040 と W49F002U の並列 programming はできるが、 W29C040 が圧倒的に早い. W29C040 のコードが残っている関係で W49F002U の転送速度が異様に遅い.
  • programming bank size より小さい sector を持つ場合の sector erase の対応を考えていない. SST39SFxxx と W49F002U が該当する. 39SF のほうは均等な sector size に対して、 49F の不均一なそれの対応は面倒そうである. 不均一 sector size は古めの flash に多く PC の bios で使われていたのだろうか.
  • 転送 bank の末尾 data が 0xff だと MCU が deadlock する. なんでいままででなかったのか? すぐに直した. ファームウェアの実装はかなり出来上がっていると思う.

基本的な programming 機能は実装したので他の機能の実装または、ハードの実装に切り替える.

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

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 が増えるので遅いので実用に向いていない.