このドキュメントは、本テンプレートの本体プログラムを CMT (カセットテープ) からロードできる形 へ変換する仕組みを解説します。出力した T77 (テープイメージ) / WAV (FSK 音声) は、実機のカセット入力/対応エミュレータの CMT 入力にそのまま使えます。
ディスク (D77/HFE) ベースの起動については DETAIL.md を参照してください。ここでは「ディスクを持たない経路」=テープロードに特化します。
ℹ️ テープロードは機種・入力経路に依存する部分があるため、実機/対応エミュレータでの検証が前提です。波形パラメータやブロック形式は下記の通り固定値で生成しますが、最終的な可否は再生環境(実機のカセット入力/対応エミュレータの CMT 入力)にも依存します。
通常の make は D77 (+ HFE + bootrom) のみを生成します。テープ用の成果物は別ターゲットで作ります。
make t77make t77 は、make と同じ D77 イメージを入力として、以下の 3 つを同時に生成します。
| 生成物 | 内容 |
|---|---|
build/<name>.t77 |
テープイメージ (FM-7 カセットテープイメージ形式)。対応エミュレータの CMT 入力に使う |
build/<name>.wav |
FSK 変調済みの音声 (44.1kHz / 16bit / モノラル)。実機のカセット入力やライン入力に使う |
build/<name>.cmt.txt |
F-BASIC 上での操作手順テキスト (CLEAR / LOADM / EXEC の入力順) |
<name> は config.mk の NAME、本体のロード先/エントリは同じく ORG (既定 $0400) です。
変換の実体は scripts/d77_to_t77_chunks.py (MIT, D77TOT77WAV 由来) と、5 種類の小さなトランポリン .bin (中継コード) です。
ℹ️ 変換ツールは既定で D77 から取り出したデータの先頭 1 セクタ (256 バイト) をスキップします (= IPL ブートセクタ相当を読み飛ばし、本体だけをテープに載せる)。
make t77はこの既定のまま呼び出します。
CMT からの機械語ロードは F-BASIC の LOADM で行いますが、本テンプレートを素直にテープへ載せるだけでは動きません。理由は 2 つあります。
F-BASIC の LOADM は、おおむね $0800 未満へは直接ロードできません。本テンプレートの本体は ORG=$0400、つまりこの直接ロード不可域に置かれます。
そこで、いったん安全な作業域へロードしてから、小さな中継コード (トランポリン) で最終アドレス $0400 へコピーして実行します。テープに載るのは「本体チャンク + それを最終位置へ運ぶトランポリン」のセットです。
LOADM の作業域 (ロードバッファ) は 16KiB です。本体がこれを超える場合、1 回の LOADM では配置しきれません。そこで本体を 16KiB チャンクに分割し、各チャンクをトランポリン付きで順に流し込みます。各チャンクはいったんバッファに入り、トランポリンが裏 RAM (URA RAM) 経由で最終位置へ運びます。
すべてのパス (= 1 回の LOADM) は、同じメモリ配置を前提にします。
まず F-BASIC 側で MEMSIZ を下げて作業域を空けます。
CLEAR ,&H13FFこれで MEMSIZ が $13FF に下がり、$1400-$7FFF がユーザ作業域として確保されます。各 LOADM ブロックはこの中の $1400-$5FFF に単一連続で展開されます。
| 範囲 | 用途 |
|---|---|
$1400-$1419 |
Stage1 (26 バイト、固定) |
$141A-$143x |
Stage2 ソース (22 / 23 / 39 バイトのいずれか。実行前に $D000 裏 RAM へコピーされる) |
$143x-$1FFF |
ゼロパディング |
$2000-$5FFF |
16KiB ロードバッファ (チャンク本体が着地する) |
- Stage1 (
$1400から実行): IRQ をマスクし、ROM オーバレイを OFF にして$8000-$FBFFを裏 RAM にし、続く Stage2 を$D000(裏 RAM) へコピーしてJMP $D000します。$D000は裏 RAM なので、後続のチャンク上書きでも壊れません。 - Stage2 (
$D000で実行): バッファ$2000-$5FFFのチャンクを最終位置へコピー (または relocate) します。末尾の処理がパス種別で変わります。- 中間パス: ROM オーバレイを ON に戻して
RTS(= F-BASIC へ復帰)。 - 最終パス:
LDS #$FBFFでスタックを張り直してからJMP <エントリ>(= 本体実行)。
- 中間パス: ROM オーバレイを ON に戻して
トランポリン本体は scripts/trampoline.asm (6809 アセンブラ) にあり、変換ツールがプレースホルダを各パスのアドレスでパッチして 5 種の .bin を埋め込みます。
エントリアドレス (= ORG) によって、チャンクの運び方が 2 パターンに分かれます。
最終配置がすべてバッファ ($2000-$5FFF) より上になるパターンです。高位メモリのチャンクから順に最終位置へ staging します。後続パスの LOADM ブロック ($1400-$5FFF) が、すでに置いたチャンク ($6000 以上) を壊さないため、チャンク数の制限は実質ありません。
- 最初のパス: 最上位チャンクをロード → トランポリンで最終位置へ staging (中間パス)。
- 中間パスを繰り返し、最後にチャンク 0 を
LOADM "CAS:",,Rでロード → 最終位置へ staging してJMPでエントリへ。
最終配置がバッファ域とぶつかるパターンです。チャンク 0 をいったん裏 RAM の退避枠へ退避し、最終パスで 2 手のコピー (逆方向コピー+退避分の順方向コピー) を行ってからエントリへ JMP します (relocate2 トランポリン)。
裏 RAM の退避枠は 16KiB の 1 枠しか取れないため、このパターンは 最大 2 チャンク = 32KiB までです。
- パス 1: チャンク 0 をロード → 裏 RAM の退避枠へコピー (退避、中間パス)。
- 最終パス: チャンク 1 を
LOADM "CAS:",,Rでロード → relocate2 が「バッファ→チャンク1最終位置 (逆方向)」「退避枠→チャンク0最終位置 (順方向)」の 2 手を実行し、LDS #$FBFF/JMP <エントリ>。
本テンプレートの既定 ORG=$0400 は $2000 未満なので、ARTICLE パターン = 上限 32KiB になります。
これを超えるサイズの本体に対して make t77 を実行すると、「CMT ロード不可」エラーで停止します。
⚠️ D77 と CMT の非対称性: D77 はディスクなので、32KiB を超える本体でもイメージを作れます (makeは通る)。しかし CMT には収まりません (make t77が止まる)。「ディスクは作れるがテープは作れない」という非対称がある点に注意してください。本体を 32KiB 以下に収めるか、ORGを$2000以上に移す (= SIMPLE パターン化、チャンク数無制限) と回避できます。
テープ波形は FSK (周波数偏移変調) で、1 (mark) と 0 (space) を異なる周波数で表します。T77 の半サイクル長と WAV の波形は同じパラメータから作られます。
時間の基準単位は tick です。
- 1 tick = 16 CPU サイクル = 約 8.92 マイクロ秒 (CPU クロック 1.794MHz)。
各シンボルの半サイクル長:
| シンボル | 半サイクル長 | 全サイクル | おおよその周波数 |
|---|---|---|---|
mark (1) |
50 tick | 約 893 マイクロ秒 | 約 1120Hz |
space (0) |
26 tick | 約 465 マイクロ秒 | 約 2150Hz |
各バイトは UART 風のフレーミングで並べます。
スタートビット(0) ×1 + データ 8 ビット(LSB ファースト) + ストップビット(1) ×2
各ビットを、上記の半サイクル長 2 つ (正極性 / 負極性) に展開して波形にします。1 ビット = 半サイクル 2 つです。
1 つの LOADM ファイルは、リーダ (無音/トーンの前置き。先頭は長め、各ブロック間は短め) に続けて、複数のブロックを並べた構成です。各ブロックは同期バイト 01 3C で始まります。
01 3C 同期
00 14 ヘッダ識別
<ファイル名 8 文字> 空白詰め (ASCII 大文字)
02 属性 (機械語)
<予備 11 バイト>
<チェックサム> ここまでの内容バイトの総和 & 0xFF
01 3C 同期
01 FF データ識別
<255 バイト本体> 短ければゼロ詰め
<チェックサム> 内容バイトの総和 & 0xFF
01 3C 同期
FF 終端識別
<チェックサム>
データブロックに載るペイロード (= ロードされる本体) は、LOADM が解釈する次の構造で包まれます。アドレス/長さはすべてビッグエンディアン (BE) です。
00 ブロック開始マーカ
<長さ 2 バイト BE>
<ロードアドレス 2 バイト BE>
<データ>
FF ブロック終端マーカ
<実行アドレス 2 バイト BE>
各パスのロードアドレスは $1400 (= Stage1 の先頭)、実行アドレスも $1400 です (auto-exec / EXEC でトランポリンが起動します)。
T77 は半サイクル長の列をそのまま並べたテープイメージです。
- 先頭 18 バイト: 固定のマジックヘッダ (FM-7 カセットテープイメージ形式の識別子)。バイト列としての固定値で、生成ツール側で 16 進の固定値を書き込みます。
- 以降: 16 ビット ビッグエンディアンの半サイクル長 (tick) の連続。
- bit15 が極性フラグ (正極性 / 負極性)。
- 値
$0000はファイル間の無音キュー (= 複数LOADMファイルの区切り)。
実際の半サイクル長は §5 のパラメータ (mark=50 tick / space=26 tick) に bit15 の極性を OR して書き込みます。
WAV は T77 と同じ半サイクル列から PCM 波形を合成します。
- フォーマット: 44.1kHz / 16bit 符号付き / モノラル PCM。
- 振幅: ±24000 (フルスケールの約 73%。ヘッドルームを残す)。
- ソフトエッジ: 極性反転のたびに 3 サンプルのコサイン状ランプを入れ、完全な矩形ではなくアナログテープ風になまらせます (実機テープの立ち上がりロールオフの近似)。
- 無音挿入: 頭・各
LOADMファイル間・末尾に DC センター (振幅 0) の無音を挿入します (既定 5 秒)。
無音長は変換ツールの --silence で変更できます (既定 5.0 秒)。
make t77 を実行すると、build/<name>.cmt.txt に F-BASIC 上での具体的な操作手順が出力されます。以下は典型的な流れです。
本体が 16KiB 以下なら 1 パスで済みます。
CLEAR ,&H13FFWAV / T77 を CMT 入力につないでから、
LOADM "CAS:",,Rを実行します。,,R の auto-exec でトランポリンが起動し、本体を $0400 へ展開して実行します。
中間パスは LOADM してから EXEC でトランポリンを起動し、これをチャンク数ぶん繰り返します。最終パスのみ LOADM "CAS:",,R です。
CLEAR ,&H13FF
LOADM "CAS:"
EXEC &H1400
LOADM "CAS:"
EXEC &H1400
... (中間パスを繰り返す) ...
LOADM "CAS:",,R
⚠️ EXECは必ずアドレスを明示してください (EXEC &H1400)。引数なしのEXECは実機で挙動が不安定です。
正確なパス数・各パスのコピー方向・最終配置は、生成された build/<name>.cmt.txt に出力されるので、そちらを正本としてください。
このテンプレートは描画をサブ CPU に任せる構成です (詳細は SUBPROGRAM.md)。ここでは「テープから起動すると、ディスク起動と起動条件が違うために描画が出ない」という問題と、その対策 (= なぜ本体側にこの処理を入れているか) を説明します。
| 起動経路 | 本体突入時のマシン状態 | サブ CPU の状態 |
|---|---|---|
| ディスク起動 (cold start) | ブート ROM → IPL → 本体。リセット直後に近い | サブシステム ROM のコマンド待ちループに居る (クリーン) |
| テープ起動 (warm start) | OK プロンプト (= BASIC 稼働中) で LOADM ",,R" |
BASIC のサブシステム処理の途中に居る (非クリーン) |
本体は起動時にサブ CPU を「HALT させる → 共有 RAM にコマンドを書く → RELEASE してサブシステム ROM に処理させる」手順で乗っ取り、描画コード (subprog) を転送します。この手順は サブがコマンド待ちループに居る ことを前提にしています。
ディスク起動ではサブはリセット直後でコマンド待ちに居るので問題ありません。ところがテープ起動では、LOADM を打つ時点で BASIC が動いており、サブ CPU は BASIC のサブシステム処理の最中です。この状態のまま乗っ取り手順を流しても 同期せず、subprog の転送が成立せず、画面が出ません (= 本体は動いているのに背景もキャラも描かれない)。
FM-7 のサブシステムには、サブ CPU の実行中コマンドを 中断 させる CANCEL の仕組みがあります (主 CPU が $FD05 の該当ビットを立てるとサブへ割り込みが入り、サブシステム ROM の割り込みハンドラが現コマンドを打ち切ってコマンド待ちループへ戻ります)。
そこで本体は起動シーケンスの最初 (カーソル OFF より前) で一度 sub_cancel() を呼び、サブを既知のコマンド待ち状態へ揃えてから乗っ取りを始めます。これでテープ起動 (warm start) でもディスク起動と同じクリーンな状態から転送でき、描画が正常に出ます。ディスク起動 (cold start) では既にコマンド待ちなので 無害 です。
実装: src/asm_subsys.s の sub_cancel、呼び出しは src/c_main.c の起動シーケンス (0)。
サブの状態 ($FD05 のステータス読み) は「HALT 受理中」でも「BUSY 中」でも同じビットが 1 になります。さらに HALT 要求は即時ではなく、サブ CPU が 次の命令境界に達した時に受理 されます。
このため、サブが BUSY の時に HALT を要求すると、まだ HALT していないのに BUSY 由来の 1 を「HALT 受理」と取り違える恐れがあります。共有 RAM は サブが HALT している間しか主 CPU から書けない ので、HALT 未完了のまま書くと write が捨てられ、転送が壊れます。
そこで subsys_halt は HALT 要求後に短い settle (空転) を入れ、「次の命令境界での HALT 受理」を確実に跨いでから戻ります。これでサブが idle でも BUSY でも、戻った時点で必ず HALT 済みになります。実装は src/asm_subsys.s の subsys_halt。
ℹ️ idle (BUSY=0) になるまで待ってから HALT する方法もありますが、warm start では BASIC のサブシステムが回り続けて idle になかなか戻らず停止し得るため、本テンプレートでは「CANCEL でコマンド待ちへ戻す + settle 付き HALT」を採っています。
ディスク起動・テープ起動の両方で、subprog がサブ $C300 へ正しく転送され、背景タイルとスプライトが同一に描画されることを確認しています (どちらも同じ本体イメージ)。
LOADM/CLEAR/EXEC/CAS:の挙動は、F-BASIC の文法に基づきます。出典は公開資料 (F-BASIC 文法書/ユーザーズマニュアル) です。- サブ CPU の HALT / CANCEL / 共有 RAM (
$FC80-$FCFF) /$FD05ステータスの挙動は、FM-7 のサブシステム (= デュアル CPU) のハードウェア仕様に基づきます。 - 変換ツール (scripts/d77_to_t77_chunks.py およびトランポリン群) は MIT ライセンス (D77TOT77WAV 由来)。ライセンス全文は scripts/D77TOT77WAV.LICENSE.txt を参照してください。
- 動作確認は対応エミュレータの CMT 入力/実機のカセット入力で行ってください。波形・ブロック形式は本書の固定値で生成しますが、最終的な可否は再生環境にも依存します。