MC6800 ROMモニタ自作 その25 ~電大版Tiny BASICをCOMにするまで、意外と長かった話~

前回はMC6800 SD/RTC/USB KEY-B基板が届いて、SDFS/68とキーボードが1枚基板でつながる話でした。ハード側が落ち着いたので、今回はソフト側の話です。SDFS/68の.COMローダは前々からできていたのですが、実は「本当のプログラム」を.COMで動かせたことがなくてですね。ずっとHELLOやARGSのようなサンプルばかり回していました。
今回のゴールは、電大版Tiny BASICを.COMにして、DBS.COMとプロンプトで叩けばREADYが出てくる、というやつです。仕組み的にはすぐ終わるはずでした。実際には全然終わらなくて、途中でstage1のFATバグを踏んで、refactorまでやって、ようやくたどり着いたのでちょっと整理しておきます。
まずは素直に p2bin してみる
電大版BASICは、SBC6800用にD-BASIC-SBC6800.ASMという版が用意されています。これはSTACKとMEMTSTを先に潰してくれていて、SDFS/68の低RAMとも当たらない形になっていました。ラッキーであります。
やることは単純で、asl の出力.pファイルをp2binに通せば、ORG $100起点のraw binaryが出てきます。
`cd sample
asl -q -L -cpu 6800 D-BASIC-SDFS.ASM
p2bin D-BASIC-SDFS.p DBS.COM -q`
これで2132バイトのDBS.COMができます。中身はBD 01 B3 7E 01 CB ... で始まっていて、JSR START(=$01B3) → JMP CONTRL の順で走る作りです。SDFS/68の.COMABIは「$0100固定ロード → JSR $0100」なので、これはこれで正しいはず、と思って書き込みました。
いきなり無反応
SDにDBS.COMをcpで放り込んで、SDFS> DBS.COMと叩いてみたら、しばらく画面が固まって、そのまま何も出ずに戻ってこない、という状態になりました。「バナーぐらい出るはずなのに、なんで?」というやつです。
原因はすぐには分からず、いくつか思い当たる節を潰していきました。
誤仮説その1: POLCAAのACIA1直読み
電大版BASICのソースを追うと、OUTルーチンが文字を1個出すたびに JSR BREAK を挟んでいます。BREAKは POLCAA を呼び、POLCAAは ACIACS=$8018(=ACIA1ステータス)を直接読んで、bit0が立っていたらBSR INで1文字読みに行く、という作りでした。
うちのK6802-VDGスタンドアロン運用では、ACIA1は基板についていません。つまり$8018はフローティングで、読むたびに違うゴミが返ってきます。運悪くbit0が立っていると、そこで永久に入力待ちに入ってしまう、という筋書きです。
`OUT INC CHCNT
JSR BREAK ← 出力するたびここへ
JMP OUTEEE
BREAK PSHA
JSR POLCAA
BCC BR5
BSR IN ← フローティングACIA1でここに刺さる
BR5 PULA
BR6 RTS`
まずここを、POLCAAをCLC; RTSにする形でnop化しました。BREAK検出は諦めることにして、sample/D-BASIC-SDFS.ASMという名前で置いてあります。5バイトだけの差分です。
誤仮説その2: これで解決かと思ったら、実は違った
DBS.COMを作り直してSDに書き戻して、boot して、DBS.COMと叩きました。しばらく読みに行っている雰囲気があって、そのあと ? だけ表示してプロンプトへ帰ってきます。「え、.COM扱いにすらならなかったの?」というやつです。
.COMのload側の何かで失敗しているのは分かるのですが、SDFS/68側は失敗理由を?しか出しません。診断出力を追加して、SDFS_CMD_COMのどのstageで死んだか、FAT_READ_PTRがどこまで進んだか、を見えるようにしました。

実機で走らせるとboot自体が暴走
診断入りのSDFS.BINをSDに書いて、電源を入れると、SDFS/68のバナーすら出ずにROMモニタのBRKトラップに落ちるようになりました。以前まで動いていたSDFS.BINを書き戻すと直る、というのを何度か繰り返して、これは私のパッチのせいだ、と思い込みました。
結論を先に書いてしまうと、これも実は誤仮説で、私のパッチとは無関係のstage1側の設計問題でした。ただ、犯人にたどり着くまでのレジスタの読み方が個人的に勉強になったので、その順で書いておきます。
BRK時のレジスタダンプを見ると、こんな感じでした。
`BRK 4D4F A=06 B=7F X=A420 SP=C001 CC=C0`
SP=$C001が気になります。K6802-VDG構成ではワークRAMが$A000-$BFFF、VRAMが$C000-$DFFFです。stackは$BFFFから下向きに伸ばして使う設計なので、$C001はRAMの上をはみ出しています。ということは、RTSの右辺が壊れて、SPが上に飛ばされている、というやつです。
PC=$4D4FもランダムなユーザーRAM番地で、ここに$3F(=SWI)が偶然あって、そこで捕まった格好でした。
stage1のS1_BOOT_DONEでこけていた
追いかけていくと、実は私のパッチとは無関係で、stage1側の設計問題でした。
CMD_BOOTはROMからjmp CMD_BOOTで来ていて、S1_BOOT_SDFSにもjmpで入ります。push はどこにもされていません。ここでSDFS.BINのload / validate が失敗すると、bcs S1_BOOT_DONEからrtsが走って、上の図の下半分の状態になります。
つまり、SDFS.BINのloadが「失敗」しているのはこの経路の入口の話であって、私が入れた診断コードは何も動いていなかったのです。ここは別issueとして#244に切り出しました。
本命の犯人: FAT32_NEXT_CLUSTER のcluster ≥ 64 バグ
「なぜloadが失敗するのか」の方が本題です。
fat32.asmのFAT32_NEXT_CLUSTERを、cluster番号を変えながら電卓叩いて手で追いかけました。すると、FATエントリの位置計算がざっくり壊れているのを見つけました。
`FAT_NEXT_OFFSET_PREP:
ldaa FAT_CUR_CLUS3 ; cluster低バイトだけ使う
asla ; ×2 (bit8 は捨てられる)
asla ; ×4 (bit8 は捨てられる)
tab`
cluster N のFATエントリは、FAT先頭からのbyte offsetが N×4 です。FAT32の1sector=512byteに128エントリ入ります。つまりcluster 100 のFATエントリは、byte offset 400、sector内の位置400です。
上のコードは cluster 低バイトだけを×4するので、cluster 100 なら (100 * 4) mod 256 = 144 にしか届きません。sector 内の別のcluster(=cluster 36)のFATエントリを読んでしまう、というやつです。
さらに、cluster ≥ 128 の場合はFAT sector番号自体が0じゃなくなります。cluster 128 なら byte offset が 128×4=512 なので、次の FAT sector の offset 0 を読む必要があります。ところが旧コードはFAT_FAT_LBA固定で FAT sector 0 を読み続けていて、そこも対応されていません。
cluster 100 の entry は本当は offset 400 にあるのに、旧コードは 144 を読みに行く
これが分かった瞬間に、いろんな「なぜ?」がつながりました。
- fresh SDだとboot する → SDFS.BINが必ずcluster 3 に置かれるので低cluster側で正常
- 何度も
cpで上書きしていくと、macOS のFAT drvが別のclusterに再配置する → 高cluster に飛んで壊れる - 「元のSDFS.BINに戻したら動く/動かない」も、コンテンツじゃなくクラスタ配置の運
fresh SDにdd したら全部解決
mk_sdfs_image でstage1、SDFS.BIN、DBS.COMを含むSDイメージを作って、そのままdd で書きました。
`python3 tools/mk_sdfs_image.py \
--stage1 build/stage1-k6802-vdg.bin \
--sdfs build/SDFS-k6802-vdg.BIN \
--output /tmp/sd_fresh.img \
DBS.COM
diskutil list # /dev/diskN 特定
diskutil unmountDisk /dev/diskN
sudo dd if=/tmp/sd_fresh.img of=/dev/rdiskN bs=1m
diskutil eject /dev/diskN`
fresh imageなら、SDFS.BINもDBS.COMもcluster 3, 4 のような低cluster に置かれるので、FAT32_NEXT_CLUSTERのバグを踏みません。boot したらSDFS/68 V1.3 #157のバナーが出て、DBS.COMとプロンプトへ打ったらREADYが返ってきました。
とりあえずBASICが動くところにゴールインしたので、そのままの勢いで根っこの方も直しました。
ついでにstage1 APIをdedup した
上のバグはstage1側とSDFS/68側の両方に同じコードで居ました。SDFS/68は自分でFATのchainを追う都合上、SDFS_STREAM_OPEN、SDFS_STREAM_GETC、SDFS_NEXT_CLUSTER、SDFS_CLUSTER_TO_SD_LBAなど、stage1と同じ関数を1セット持っていた形です。
このままだと同じバグを2箇所に持っていて、直すのも2箇所です。ちょうどSDFS/68の余りが100バイト強しかなくて、これから機能を足したい方向とも噛み合わないので、いっそAPIを増やしてSDFS/68側は自分では持たない形にしました。
末尾に slot を足すだけの非破壊拡張なので VERSION は 1 のまま
VERSIONは 1 据置(既存slot の意味は変えない=非破壊拡張)、COUNTだけ6→12に上げました。実際には Step 2 で streaming 3 slot を足して 6→9、Step 3 で cluster プリミティブ 3 slot を足して 9→12、と2段でbumpしています。SDFS/68側はSDFS_S1_COUNTを9→12に上げるだけで、あとは自前のFAT関数(12個)を消してSDFS_API_STREAM_GETCのようなwrapperに置き換えです。
その結果、こんな感じで縮みました。
` before after diff
stage1 2514 B 2618 B +104 B (API +6 slot と cluster fix)
SDFS.BIN (k6802) 3614 B 3144 B -470 B (FAT関数 12個 dedup)`
SDFS/68の余りが 226 B → 696 B に増えたので、次に何か機能を足す余地ができました。診断出力の再導入も、たぶんこの余りに収まると思います。
副次効果として、FAT32_NEXT_CLUSTERのcluster ≥ 64 fixがSDFS/68 の.COMロード / DIR / FIND の全経路に自動で効くようになりました。これはうれしい形の副次効果でありました。
リグレッションテストを1本置いた
fixが効いていること自体は追いかけたのですが、後々また踏む未来もあるので、tests/sd_fixtures.pyに cluster 100 の chain を持つ SD fixture を足して、tests/test_stage1_build.pyからFAT_CUR_CLUS=100をセット→S1_NEXT_CLUSTER→101が返ってくることをアサートするテストを1本足しました。
fixture + ハーネス + assert の3ブロック。旧バグに戻したら下段で fail する
旧バグコードだと、offset 144 の位置に何もないのでゼロが返ってきて、テストが落ちます。「次に誰かが同じところをうっかり戻したら、ここで気づける」というやつです。
実機で動いた
fresh SDにdd した状態で、K6802-VDG+MC6800 SD/RTC/USB KEY-Bで電源を入れました。
`] BOOT
SDFS/68 V1.3 #157
SDFS> DIR
SDFS.BIN A 00000C48
DBS.COM A 00000854
SDFS> DBS.COM
READY
#`
出ました。一発です。10 PRINT "HELLO"、RUN、LIST あたりが素直に通ります。BASICでちょっと遊べる状態です。
やってみると、cluster配置の話は基板とも配線とも無関係のところで刺さっていて、あれはあれで面白かったのですが、しばらくBRK 4D4Fに殴られていたのは正直しんどかったですw
次のはなし
BRK 4D4Fの原因になったS1_BOOT_DONEのsilent RTS暴走(=issue #244)は残しています。これは「SDFS.BINのloadが何かで失敗したときに、エラーメッセージを出してROMモニタへ戻す」だけの話なので、そんなに大きくないと思います。debugしやすさが大きく変わるので、そのうち直しておきたいところであります。
診断出力?C のSDFS/68への再導入は、余った 696 B のうちほんの一部で済むはずなので、これも別issueで戻せそうです。
あとは、電大版BASIC のLIST出力をSD側へ流し込む/流し出す方向で、.COMとは別ラインでの機能追加を考えています。これはSDFS/68のFAT writeが必要なので、別の大きい話です。今回はここまでとします。
まとめ
- 電大版BASICを
.COMにするのは、p2bin一発では終わらなかった。ACIA1直読みでのフローティング拾いを回避する必要があった。 - 実機で動かないときの犯人が、書き換えたパッチではなく、SDのcluster配置だった、という展開が面白かった(しんどかったとも言う)。
- 根本の
FAT32_NEXT_CLUSTERはstage1とSDFS/68の両方に居たので、まとめてstage1 API v2 化して1箇所に集約した。 - SDFS.BINが -470 B、余りが 696 B に増えたので、次の機能追加がしやすくなった。
- fresh SD に dd で書き戻す運用が、しばらく標準になります。