こんにちは、パレイド技術部の夏目です。前回は「LLM に 6502 の機械語を書かせられるか」という問いを立てました。その後、実際にテトリスを機械語で動かすところまで進んだのですが、AI に頼むより前に、人間が覚えておくことが多すぎると分かりました。今回は、その前提を AI が自分で読める 1 本の資料にまとめます。
本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。
人間の頭にしかなかった前提
ファミリーベーシック V3 で機械語(CALL)を使うとき、BASIC の知識だけでは踏み抜く落とし穴があります。テトリスを作る過程で、実際に踏んだものを一覧にします。どれも「AI に最初から伝えておかないと、動かない」類のものです。
| 前提 | 内容 | 知らないと起きること |
|---|---|---|
| 置き場所 | 機械語を置ける RAM は $6000〜$6FFF の 4KB だけ。BASIC の本文も同じ領域を使う |
上書きして暴走する |
| NMI の飛び先 | NMI(画面を 1 枚描き終えるたび、CPU を呼ぶ割り込み)の飛び先が ROM でなく RAM の $00ED にある |
割り込みを自分で握れない |
| PPU の設定 | BASIC が $32・$33 に画面設定を持ち、毎フレーム書き戻している |
自前の割り込みで書き戻さないと画面が真っ黒 |
| 描画の時間 | 絵は垂直ブランク(画面を描いていない短い休み時間)の間だけ書ける | 画面が崩れる・ちらつく |
| 絵柄 | 背景のタイルは内蔵の文字・図形だけ。自作の絵柄は使えない | 存在しないタイルを指定してしまう |
| 音 | BASIC の PLAY は演奏が終わるまで次の行に進まない(同期)。自前の割り込みと両立しない |
ゲームが止まる・音が競合する |
| 6502 の癖 | 分岐命令(BPL など)の落とし穴 | ループが 1 回で抜ける |
これを毎回プロンプトに書くのは現実的ではありません。しかも、書いたところで AI が「なぜそうなるか」を理解して応用できるとは限りません。そこで、検証できた事実だけを 1 つの資料にして、AI に URL で読ませる方針に切り替えます。以下がその中身です。
リファレンス: ファミリーベーシック V3 の機械語
エミュレータ上の ファミリーベーシック V3 で動作を確かめた内容だけを書きます。実機では未確認です。
1. 置き方と呼び出し
機械語は 16 進の文字列を DATA に並べ、POKE で RAM に書き込みます。普通のサブルーチンなら CALL で起動しますが、次の節の NMI のように「割り込みから呼ばれる処理」は、起動せずに飛び先の番地だけを登録します。DATA の文字列は 1 行 30 文字(15 バイト)で動作を確認しました。60 文字にすると ?ST ERROR になります(実測)。
10 CLEAR &H6C00
20 A=&H6C00:FOR K=1 TO 3:READ S$:FOR J=1 TO LEN(S$) STEP 2
30 POKE A,VAL("&H"+MID$(S$,J,2)):A=A+1:NEXT:NEXT
50 POKE &HEE,0:POKE &HEF,&H6C
60 GOTO 60
100 DATA 48AD0220A9008D0320A9028D1440A5
110 DATA 3209808D0020A5338D0120ADF06F8D
120 DATA 0520A9008D0520EEF06F6840
10 行目は $6C00 より上を機械語用に空けます。20〜30 行目が 16 進文字列を 1 バイトずつ書き込む部分、50 行目が NMI の飛び先の登録です(次の節で説明します)。BASIC のプログラム本体は同じ 4KB を食うので、ローダは最小限にします。2,265 バイトのローダで約 15〜20 秒かかった実測があります。
2. NMI を自前のものに差し替える
BASIC は起動時に $00ED へ JMP $8971 を書いています。$00EE と $00EF に自前の処理の番地を書けば、NMI を奪えます。書き換えは NMI を無効にしたまま、初期化の最後に行います(途中で NMI が入ると飛び先が壊れるため)。
自前の NMI は、次の 2 つを必ず行います。
$2000と$2001に、BASIC が$32・$33に持つ値を書き戻す(これを忘れると真っ黒)- スクロールを使うなら
$2005に 2 回書く
次の 42 バイトは、画面を 1 フレームごとに 1 ドット横へ流す最小の例です。上のローダの DATA がそのままこの内容で、動作を確認済みです(画面が横に流れ、NMI の飛び先が $6C00 に書き換わっていることを確認)。
48 PHA ; レジスタを退避
AD 02 20 LDA $2002 ; スクロールの書き込み位置を初期化
A9 00 8D 03 20 LDA #0 : STA $2003
A9 02 8D 14 40 LDA #2 : STA $4014 ; スプライトの転送(BASIC の標準処理に合わせる)
A5 32 09 80 8D 00 20 LDA $32 : ORA #$80 : STA $2000 ; 画面設定の書き戻し + NMI 有効
A5 33 8D 01 20 LDA $33 : STA $2001 ; 表示設定の書き戻し
AD F0 6F 8D 05 20 LDA $6FF0 : STA $2005 ; 横スクロール量
A9 00 8D 05 20 LDA #0 : STA $2005 ; 縦スクロール量
EE F0 6F INC $6FF0 ; 毎フレーム 1 ずつ増やす
68 40 PLA : RTI ; 退避を戻して割り込みから復帰
3. 描画は「貯める」と「流す」を分ける
画面メモリ(VRAM)への書き込みは、$2006 に上位・下位の順にアドレスを 2 回書き、$2007 にデータを書きます。書くたびにアドレスは自動で進みます。書けるのは垂直ブランクの間だけで、その長さは約 2,273 CPU サイクル(計算値)しかありません。
そこで、ゲームの計算は NMI の外(メインループ)で行い、「書きたい場所と値」を RAM のキューに貯める構成にします。NMI の中は、キューを VRAM へ流すだけにします。
; キューの形: [上位アドレス, 下位アドレス, 個数, データ…] を並べる
flushq: LDX #0
fq1: CPX QN ; 貯めた量を読み切ったら終わり
BCS fqe
LDA QUE,X : STA $2006 ; アドレス上位
INX
LDA QUE,X : STA $2006 ; アドレス下位
INX
LDA QUE,X : TAY ; 個数
INX
fq2: LDA QUE,X : STA $2007 ; データを個数ぶん流す
INX : DEY : BNE fq2
JMP fq1
fqe: LDA #0 : STA QN
RTS
テトリスでは、ミノが 1 つ動くたびに書くのは「消す 4 マス + 描く 4 マス」だけです。BASIC 版は毎回 180 マスを書き直していました。縦の線は $2000 の bit2 を立てると、書くたびのアドレスの進みが 32 になり、一気に書けます。
4. 音
APU(音の担当)は $4000〜$4017 です。$4015 の下位 5 ビットで音源(矩形波 2・三角波・ノイズ・DMC)を有効にします。BASIC からでも次の 4 つの POKE で矩形波が鳴ることを確認しています。
POKE &H4015,1 : POKE &H4000,&HBF
POKE &H4002,&HFD : POKE &H4003,&H08
自前の NMI から毎フレームこれらを書けば、ゲームの計算と並行して音を進められます。BASIC の PLAY は ROM の中で $4000・$4015 などを直接書くため、自前の NMI と同時には使いません。
5. 実際に踏んだ罠
- BPL でループを組むと、カウンタが 128 以上のとき最初の 1 回で抜ける。0 まで数える形(
BNE)に直す - 分岐命令は ±127 バイトまで。超えたら
JMPで中継する LDAで入れた値を、使う前に別の値で上書きしていた(枠線の右端が「,」の字になった)- 計算を NMI の中に入れると垂直ブランクに収まらず、画面の上端が乱れる
- NMI を奪うと BASIC のキー入力処理も止まる可能性がある(調査時にリスクとして確認。このテトリスはパッドを自分で読んでいる)。終わるときはリセットする想定
ここまでの実測と次回
この資料の元になったテトリスは、機械語 2,029 バイトで動いています。同じ盤面・操作の BASIC 版と並べた実測では、入力を読む間隔が 71.5 フレームから 1 フレームへ、盤面の全面再描画が 559 フレームから 5 フレームへ縮みました(エミュレータ上の測定)。
資料はまだ「人間が書いた事実の集まり」です。AI がこれだけを読んで動くものを書けるかは、これから確かめます。次回は、この資料の URL だけを渡した 1 行のプロンプトで、前提を持たない AI にテトリスの土台を作らせます。動かなかった箇所は資料に戻って直し、その往復を記録します。