picoZ80 デベロッパーズガイド
picoZ80 デベロッパーズガイド
MZ700.c)を具体的な実例として全体を通じて使用します。
picoZ80 コードベースの事前知識は想定していません。各概念は原則から説明され、実際のソースコードで示されます。このガイドを終える頃には、ゼロからドライバーを作成し、ビルドシステムに追加し、フレームワークに登録し、JSON で設定できるようになります。
ハードウェアアーキテクチャ、PIO バスインターフェースの詳細、JSON 設定リファレンスについてはpicoZ80 テクニカルガイドを参照してください。エンドユーザーのセットアップについてはpicoZ80 ユーザーマニュアルを参照してください。
ソースツリー
projects/tzpuPico/ に存在します(正確なチェックアウト場所はシステムによって異なります)。以下のレイアウトはドライバー開発に関連するファイルを示しています:
tzpuPico/ ├── CMakeLists.txt トップレベルのビルドファイル ├── src/ │ ├── CMakeLists.txt ソースレベルのビルドファイル — 新しいドライバーファイルをここに追加 │ ├── Z80CPU.c メイン Z80 エミュレーション、バスディスパッチ、ドライバーフレームワーク │ ├── Z80CPU.h (レガシー — include/ 経由でインクルード) │ ├── M6502CPU.c 6502 並行(同じアーキテクチャ) │ ├── FSPI.c / FSPI.h フラッシュ SPI インターフェース │ ├── ESP.c / ESP.h ESP32 通信レイヤー │ ├── psram.c / psram.h PSRAM の割り当てと管理 │ ├── cJSON.c / cJSON.h config.json 用 JSON パーサー │ ├── dbgsh.c デバッグシェル(ICE デバッガー)— USB CDC チャネル 1 │ ├── PIT8253.c Intel 8253 PIT エミュレーションモジュール │ ├── PPI8255.c Intel 8255 PPI エミュレーションモジュール │ ├── include/ │ │ ├── Z80CPU.h *** 重要なファイル:すべての型定義とマクロ *** │ │ ├── dbgsh.h デバッグシェル(ICE デバッガー)ヘッダー │ │ └── drivers/ │ │ ├── Z80SIO.h Zilog Z80 SIO/2 エミュレーションヘッダー(共有) │ │ ├── Sharp/ │ │ │ ├── MZ.h MZ シリーズ共通定数 │ │ │ ├── MZ700.h MZ-700 ドライバーヘッダー │ │ │ ├── MZ80A.h MZ-80A ドライバーヘッダー │ │ │ ├── MZ2000.h MZ-2000 ドライバーヘッダー │ │ │ ├── MZ2200.h MZ-2200 ドライバーヘッダー │ │ │ ├── MZ80B.h MZ-80B ドライバーヘッダー │ │ │ ├── MZ2500.h MZ-2500 ドライバーヘッダー │ │ │ ├── MZ800.h MZ-800 ドライバーヘッダー │ │ │ ├── MZ8BIO3.h MZ-8BIO3 RS-232C カード(Z80 SIO)ヘッダー │ │ │ ├── MZ1E24.h MZ-1E24 RS-232C カード(Z80 SIO)ヘッダー │ │ │ ├── RFS.h / TZFS.h ファイリングシステムヘッダー │ │ │ ├── WD1773.h フロッピーコントローラーヘッダー │ │ │ ├── MZ8BFI.h MZ-8BFI ドライバーヘッダー │ │ │ ├── MZ1E30.h MZ-1E30 SASI ドライバーヘッダー │ │ │ └── QDDrive.h QuickDisk ヘッダー │ │ ├── Amstrad/ │ │ │ └── PCW9512.h PCW-9512 ドライバーヘッダー │ │ └── Tatung/ │ │ ├── EinsteinTC01.h Tatung Einstein TC-01 ドライバーヘッダー │ │ ├── EinsteinFDC.h Einstein FDC サブインターフェースヘッダー │ │ └── WD1770.h WD1770 FDC ヘッダー │ ├── drivers/ │ │ ├── Z80SIO.c Zilog Z80 SIO/2 エミュレーション(RS-232C カードで共有) │ │ ├── Sharp/ │ │ │ ├── MZ700.c *** サンプルドライバー *** │ │ │ ├── MZ80A.c MZ-80A ペルソナドライバー │ │ │ ├── MZ2000.c MZ-2000 ペルソナドライバー │ │ │ ├── MZ2200.c MZ-2200 ペルソナドライバー │ │ │ ├── MZ80B.c MZ-80B ペルソナドライバー │ │ │ ├── MZ2500.c MZ-2500 ペルソナドライバー │ │ │ ├── MZ800.c MZ-800 ペルソナドライバー(デュアルモード MZ-700/MZ-800) │ │ │ ├── MZ8BIO3.c MZ-8BIO3 RS-232C カード(共有 SIOCard_* 実装) │ │ │ ├── MZ1E24.c MZ-1E24 RS-232C カード(SIOCard_* ラッパー) │ │ │ ├── RFS.c ROM ファイリングシステムドライバー │ │ │ ├── TZFS.c TranZPUter ファイリングシステムドライバー │ │ │ ├── WD1773.c WD1773 フロッピーコントローラードライバー │ │ │ ├── QDDrive.c QuickDisk ドライブドライバー │ │ │ ├── MZ1500.c MZ-1500 ペルソナドライバー │ │ │ ├── MZ-1E05.c / MZ-1E14.c / MZ-1E19.c / MZ-1E30.c ペリフェラルインターフェースカード │ │ │ ├── MZ8BFI.c MZ-8BFI フロッピーディスクインターフェース(MZ-2000) │ │ │ ├── MZ-1R12.c / MZ-1R18.c RAM 拡張カード │ │ │ ├── MZ-1R23.c 漢字 ROM / 辞書 ROM ボード │ │ │ ├── MZ-1R37.c 640KB 拡張メモリマネージャー │ │ │ ├── PIO-3034.c IO DATA 320KB EMM │ │ │ └── Celestite.c Celestite LAN/メモリ複合ボード │ │ ├── Amstrad/ │ │ │ └── PCW9512.c Amstrad PCW-9512 ペルソナドライバー │ │ └── Tatung/ │ │ ├── EinsteinTC01.c Tatung Einstein TC-01 ペルソナドライバー │ │ ├── EinsteinFDC.c Einstein FDC サブインターフェース(2 ドライブ、DSK/D88) │ │ └── WD1770.c WD1770 FDC エミュレーション(再利用可能モジュール、WD1773.c とは別) │ │ └── Other/ │ │ └── Open.c OpenZ80 バニラ / 実験者向けペルソナドライバー │ └── model/ │ ├── BaseZ80/ 全ドライバー(Sharp + Amstrad + Tatung) │ │ ├── CMakeLists.txt モデルごとのビルドターゲット │ │ ├── main.c エントリポイント(コア 0 + コア 1 起動) │ │ ├── main_memmap_partition_1.ld スロット 1 用リンカースクリプト │ │ └── main_memmap_partition_2.ld スロット 2 用リンカースクリプト │ ├── SharpZ80/ Sharp MZ ドライバーのみ(小さなバイナリ) │ │ ├── CMakeLists.txt │ │ ├── main.c │ │ ├── main_memmap_partition_1.ld │ │ └── main_memmap_partition_2.ld │ ├── AmstradZ80/ Amstrad PCW ドライバーのみ(小さなバイナリ) │ │ ├── CMakeLists.txt │ │ ├── main.c │ │ ├── main_memmap_partition_1.ld │ │ └── main_memmap_partition_2.ld │ ├── TatungZ80/ Tatung Einstein ドライバーのみ(小さなバイナリ) │ │ ├── CMakeLists.txt │ │ ├── main.c │ │ ├── main_memmap_partition_1.ld │ │ └── main_memmap_partition_2.ld │ ├── OpenZ80/ OpenZ80 実験者向けペルソナのみ(マシン非依存カード) │ │ ├── CMakeLists.txt │ │ ├── main.c │ │ ├── main_memmap_partition_1.ld │ │ └── main_memmap_partition_2.ld │ └── Bootloader/
PIO バスインターフェース — C コードによるステートマシンの駆動方法
z80.pio)で動作しますが、コア 1 の C コードがどのバスサイクルをいつ実行するかをオーケストレートします。この連携を理解することは、バスタイミングのデバッグや新しいサイクルタイプの追加に重要です。
中心的なメカニズムは out exec, 16 — TX FIFO から 16 ビット値を取得し PIO 命令として実行する PIO 命令です。z80_cycle ステートマシン(PIO 0 SM 2)はこれをタイトループで使用します:
// z80_cycle SM プログラム(PIO 0 SM 2): // // start_cycle: // wait 0 irq 6 ; BUSREQ アクティブならストール // irq set 0 ; 「準備完了」を通知 // wait 0 irq 0 ; C コードがアドレスをロードして IRQ 0 をクリアするまで待機 // wait 1 gpio CLK ; T1 立ち上がりエッジに同期 // cycle_exec: // out exec, 16 ; FIFO から命令を取得し実行 // jmp cycle_exec ; 繰り返し // // C コードはエンコード済み 16 ビット PIO 命令のシーケンスを // サイクル SM の TX FIFO にプッシュします。シーケンスはバスサイクルの // すべての側面を制御:どの制御信号をアサート/デアサートするか、 // いつクロックエッジを待つか、いつデータをサンプリングするか。 // すべてのシーケンスの最終命令は start_cycle への JMP です。
uint16_t 値の配列として格納されます。実行時に、コア 1 のホットループは現在のバストランザクションに基づいて適切なシーケンスを FIFO にプッシュします:
// メモリリードサイクルの簡略化されたコア 1 フロー: // 1. z80_cycle SM が IRQ 0 をセット — 新しいサイクルの準備完了。 // z80_addr SM が IRQ 0 をセット — アドレスの準備完了。 // 2. コア 1 がアドレスを解決しバストランザクションを準備: pio_sm_put(pio0, SM_ADDR, (pindirs_16 << 16) | address); // z80_addr にプッシュ pio_interrupt_clear(pio0, 0); // IRQ 0 クリア → addr SM 実行 // 3. コア 1 がサイクルタイプ命令シーケンスをプッシュ: pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_low); // wait 0 gpio CLK pio_sm_put(pio0, SM_CYCLE, encoded_set_mreq_rd); // set pins: /MREQ ロー、/RD ロー pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_high); // wait 1 gpio CLK (T2) pio_sm_put(pio0, SM_CYCLE, encoded_wait_clk_low); // wait 0 gpio CLK (T2) // ... ウェイトステート確認、T3、データサンプリング ... pio_sm_put(pio0, SM_CYCLE, encoded_jmp_start); // JMP start_cycle(終了) // 4. コア 1 が RX FIFO からデータバイトを読み取り: uint8_t data = pio_sm_get(pio0, SM_DATA);
z80_addr)とデータ SM(z80_data)はそれぞれ独自の IRQ フラグ(IRQ 0 と IRQ 1)を使用してプロデューサー/コンシューマーハンドシェイクを実装します:
- SM が IRQ フラグをセットし
wait 0 irq Nでストール —「準備完了、データを送ってください」。 - コア 1 がアドレスまたはデータを SM の TX FIFO にプッシュし、IRQ フラグをクリア。
- SM がウェイクアップし、FIFO からデータを取得してピンを駆動。
z80_data)のリードサイクルフロー:
- IRQ 1 をセットして待機 —「方向/データの準備完了」。
- コア 1 がピン方向(入力モード)とダミーデータバイトをプッシュした後、IRQ 1 をクリア。
- SM がピン方向を入力(トライステート)に設定し、ホストメモリが D0–D7 を駆動可能に。
- SM は
wait 0 irq 0で次のアドレス変更(サイクル終了を示す)まで待機。 - SM がピン方向を既知の状態に戻す。
主要な型とデータ構造
src/include/Z80CPU.h で定義されているコアデータ構造を理解することが不可欠です。これらの構造体はすべてのドライバー関数に渡され、ドライバーがメモリと I/O システムと対話する主要な手段です。
メモリブロックタイプ定数
membankPtr エントリの上位 8 ビットにエンコードされたタイプを持ちます。タイプはコア 1 のディスパッチループがそのブロック内に収まるバストランザクションをどのように処理するかを伝えます。
// src/include/Z80CPU.h #define MEMBANK_TYPE_UNKNOWN 0x00 // 未初期化 — 実行時に現れてはならない #define MEMBANK_TYPE_PHYSICAL 0x01 // パススルー:RP2350 がバスを解放し、ホストハードウェアが応答 #define MEMBANK_TYPE_PHYSICAL_VRAM 0x02 // ホストビデオ RAM 用ウェイトステート付きパススルー #define MEMBANK_TYPE_PHYSICAL_HW 0x04 // ホスト I/O マッピングハードウェアレジスタのパススルー #define MEMBANK_TYPE_RAM 0x08 // 読み書き — PSRAM バンクによってバック #define MEMBANK_TYPE_VRAM 0x10 // PSRAM ビデオ RAM — 書き込みは物理 VRAM にもミラーリング #define MEMBANK_TYPE_ROM 0x20 // 読み取り専用 — PSRAM バンクによってバック;書き込みはサイレントに無視 #define MEMBANK_TYPE_FUNC 0x40 // 仮想デバイス — すべてのアクセスが C 関数ハンドラーを呼び出す #define MEMBANK_TYPE_PTR 0x80 // 間接 — 各バイトが別のアドレスにリダイレクト
Z80CPU_readMem() と Z80CPU_writeMem() で何が起こるかを決定します:
- PHYSICAL / PHYSICAL_VRAM / PHYSICAL_HW — RP2350 がバストランザクションを横取りしません;ホストボードの実際のハードウェアが応答します。
- RAM — 読み書きは 8MB PSRAM の 64KB バンクに行きます。その特定のアドレスに
memioPtr関数がインストールされている場合、その関数が代わりに(FUNC の場合)または PSRAM アクセスと並行して(ハンドラーが横取りまたは後処理できる)呼び出されます。 - ROM — 読み取りは PSRAM から来ます(通常は起動時にイメージファイルから読み込まれます)。書き込みサイクルはインストールされた
memioPtrハンドラーに到達しますが PSRAM は変更されません。 - VRAM — PSRAM から読み取り;書き込みは PSRAM と物理ホスト VRAM の両方に並行して行きます。
- FUNC — PSRAM バッキングなし。すべての読み書きが
memioPtr[addr]にインストールされた関数を呼び出します。 - PTR — 512 バイトブロックのすべてのバイトが独立して異なる PSRAM の場所やメモリタイプを指すことができます。
membankPtr エンコーディング
_membankPtr[] 配列には 128 エントリがあります — Z80 の 64KB アドレス空間の 512 バイトブロックごとに 1 つ。各エントリは 3 つのフィールドをエンコードする 1 つの 32 ビット値です:
ビット 31..24 = メモリタイプ (MEMBANK_TYPE_xxx 定数) ビット 23..16 = PSRAM バンク (0-63、8MB PSRAM のどの 64KB バンクか) ビット 15..0 = Z80 アドレス (バンク内のブロックのベースアドレス)
// ブロック idx(各ブロック = 512 バイト)をバンク 0 の RAM タイプにマッピング:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24) // 上位バイトにタイプ
| (MZ700_MEMBANK_0 << 16) // バンク番号
| (idx * MEMORY_BLOCK_SIZE); // このブロックのベースアドレス
// 同じブロックをバンク 2 の ROM にマッピング:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24)
| (2 << 16)
| (idx * MEMORY_BLOCK_SIZE);
// 同じブロックを物理(パススルー)にマッピング:
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24)
| (0 << 16)
| (idx * MEMORY_BLOCK_SIZE);
MEMORY_BLOCK_SIZE は 512 バイト。0x0000–0xFFFF をカバーする 128 ブロックがあります。Z80 アドレスのブロックインデックスは:addr / MEMORY_BLOCK_SIZE = addr >> 9。
メモリ属性(t_memAttr)
t_memAttr エントリもあります。これらはバンクとブロックでインデックスされた 2D 配列に保存されています:
// src/include/Z80CPU.h
typedef struct {
uint8_t waitStates; // アクセス時に挿入する追加 T サイクルウェイトステート数
bool tCycSync; // true = 各バスサイクルの T1 立ち上がりエッジに PSRAM アクセスを同期
} t_memAttr;
// アクセスパターン:
cpu->_memAttr[bank][idx].waitStates = 1;
cpu->_memAttr[bank][idx].tCycSync = true;
true に設定すると、PIO の z80_sync ステートマシンが PSRAM アクセスを現在のバスサイクルの T1 立ち上がりエッジまで遅延させます。カセット I/O、シリアルビットバンギング、遅延ループなど精密なクロックサイクルタイミングに依存するホストソフトウェアのタイミングドリフトを防ぎます。
PSRAM 構造体(t_Z80PSRAM)
t_Z80PSRAM 構造体にマッピングされます。起動時に一度割り当てられ、cpu->_z80PSRAM が指します。これはシステム内の最大かつ最も重要なデータ構造です — Z80 がアクセスできるすべてのものがここにあります。
// src/include/Z80CPU.h
typedef struct {
// 4MB データ空間:64 バンク × 64KB RAM/ROM イメージストレージ
uint8_t RAM[MEMORY_PAGE_BANKS * MEMORY_PAGE_SIZE];
// 64KB バイト単位リダイレクトテーブル(MEMBANK_TYPE_PTR ブロックが使用)
uint32_t memPtr[MEMORY_PAGE_SIZE];
// 64KB メモリアドレス関数ポインターテーブル
// インデックス = Z80 アドレス(0x0000-0xFFFF)
// 値 = NULL(オーバーライドなし)または C ハンドラー関数へのポインター
MemoryFunc memioPtr[MEMORY_PAGE_SIZE];
// 64KB I/O ポート関数ポインターテーブル
// インデックス = Z80 I/O ポートアドレス(0x0000-0xFFFF;Z80 は実際のポートに下位 8 ビットを使用)
// 値 = NULL(物理 I/O にパス)または C ハンドラー関数へのポインター
MemoryFunc ioPtr[IO_PAGE_SIZE];
} t_Z80PSRAM;
- RAM[] — PSRAM バックメモリバンクすべての生バイトストレージ。合計サイズは 64 バンク × 64KB = 4MB。SD カードまたはフラッシュから読み込まれた ROM イメージが起動時にここに書き込まれます。
- memPtr[] — PTR タイプブロックのみで使用。各エントリは
cpu->_membankPtr[]と同じ方法でエンコードされた完全なmembankPtr値で、単一バイトのアクセスを完全に異なる場所にリダイレクトします。 - memioPtr[] — メモリ関数フックテーブル。Z80 アドレスごとに 1 つのスロット。スロットが非 NULL の場合、コア 1 はブロックタイプ(RAM、ROM、FUNC)に関係なくそのアドレスへのすべてのメモリアクセスでそのスロットの関数を呼び出します。
- ioPtr[] — I/O ポートフックテーブル。Z80 I/O アドレスごとに 1 つのスロット。非 NULL の場合、そのポートをターゲットとするすべての IN または OUT 命令で関数が呼び出されます。NULL の場合、I/O サイクルは物理ハードウェアに渡されます。
MemoryFunc ハンドラーシグネチャ
memioPtr[] と ioPtr[] の両スロットは同じ型の関数ポインター — MemoryFunc を保持します:
// src/include/Z80CPU.h typedef uint8_t (*MemoryFunc)(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
- cpu — Z80CPU コンテキストへのポインター。
_membankPtr[]、_z80PSRAM、その他すべてにアクセスできます。コア 1 のホットループから呼び出される可能性のある I/O ハンドラー内から_membankPtr[]を変更しないでください;そのような操作にはコア間キューを使用してください。 - read — これが読み取りサイクル(Z80 が読み取っている)の場合は
true;書き込みサイクル(Z80 が書き込んでいる)の場合はfalse。 - addr — 完全な Z80 アドレス(メモリは 0x0000–0xFFFF、I/O ポートは 0x0000–0xFFFF)。I/O では、Z80 は実際のポート番号に下位 8 ビット(A0–A7)のみを使用します;上位 8 ビット(A8–A15)は命令中の B レジスタの値です。
- data — 書き込みサイクルでは、Z80 が書き込んでいるバイト。RAM/ROM ブロックからの読み取りサイクルでは、その PSRAM の現在の値です(使用するか無視するか選べます)。
- 読み取り時:Z80 に返すバイト。Z80 がデータバスで見る値です。
- 書き込み時:I/O ハンドラーの場合は戻り値は一般的に未使用。RAM タイプブロックの
memioPtrハンドラーの場合、戻り値は元のデータの代わりに PSRAM に書き戻されます。
debugf の呼び出し、またはコア 1 をストールさせる可能性のある操作を実行してはいけません。ファイル I/O と UART 通信はコア間キューを介してコア 0 に送信しなければなりません。
Z80CPU コンテキスト構造体
Z80CPU *cpu ポインターを受け取ります。これはエミュレーション全体のマスターコンテキストです。ドライバー作成者に最も関連するフィールド:
// src/include/Z80CPU.h(簡略化)
struct Z80CPU {
Z80 _Z80; // Zeta Z80 エミュレーター状態(レジスタ、フラグ、PC など)
// 高速ディスパッチテーブル:128 エントリ、Z80 アドレス空間の 512 バイトブロックごとに 1 つ
uint32_t _membankPtr[MEMORY_PAGE_BLOCKS]; // MEMORY_PAGE_BLOCKS = 128
// ブロックごとのウェイトステートと同期属性、[バンク][ブロック] でインデックス
t_memAttr _memAttr[MEMORY_PAGE_BANKS][MEMORY_PAGE_BLOCKS];
// 8MB PSRAM 構造体へのポインター(RAM[]、memPtr[]、memioPtr[]、ioPtr[])
t_Z80PSRAM *_z80PSRAM;
// 読み込まれたドライバー設定(JSON 解析から)
t_drivers _drivers;
// コア間通信キュー(コア 1 → コア 0 およびコア 0 → コア 1)
queue_t requestQueue;
queue_t responseQueue;
bool halt; // Z80 が HALT 状態
bool hold; // コア 0 がコア 1 に一時停止を要求
bool holdAck; // コア 1 が保留リクエストを確認応答
bool forceReset; // 非同期リセットフラグ
};
ドライバー設定構造体
t_drvConfig 構造体へのポインターを受け取ります。これにより、与えられたインターフェース、読み込む ROM イメージ、適用するアドレスリマップ、ユーザーが設定したパラメータがドライバーに伝えられます。
// src/include/Z80CPU.h
// 単一パラメータのキーと値のペア(JSON "param" 配列から)
typedef struct {
const char *name; // パラメータ名文字列
const char *value; // パラメータ値文字列(常に文字列;必要に応じて解析する)
} t_ifParam;
// 単一 ROM イメージ割り当て(JSON "rom" 配列から)
typedef struct {
const char *file; // ROM ファイルへの SD カードパス
uint16_t addr; // この ROM の Z80 ターゲットアドレス
uint8_t bank; // 読み込み先の PSRAM バンク
uint16_t size; // 読み込むバイト数
uint32_t fileofs; // ROM ファイルのバイトオフセット
uint8_t waitStates; // このブロックのウェイトステート
bool tCycSync; // このブロックの T1 同期
} t_drvROMConfig;
// 単一アドレス空間リマップ(JSON "addrmap" 配列から)
typedef struct {
uint16_t srcaddr; // 元の Z80 アドレス
uint16_t dstaddr; // リダイレクト先アドレス
uint16_t size; // 範囲サイズ
uint8_t bank; // PSRAM バンク
uint8_t type; // MEMBANK_TYPE_xxx
} t_addrReMap;
// 単一 I/O リマップ(JSON "iomap" 配列から)
typedef struct {
uint16_t srcaddr; // I/O ポートアドレス
uint16_t size; // ポート範囲サイズ
const char *funcName; // ハンドラー関数の名前(メモリ関数マップで検索)
} t_ioReMap;
// 1 つのインターフェースブロック(JSON "if" 配列の 1 エントリ)
typedef struct t_drvIFConfig {
const char *name; // インターフェース名(例:"RFS"、"MZ-1E05")
bool isPhysical; // 仮想または物理インターフェース
int romCount; // ROM エントリ数
t_drvROMConfig *romConfig; // ROM 設定の配列
int addrMapCount; // アドレスリマップ数
t_addrReMap *addrMap; // アドレスリマップテーブル
int ioMapCount; // I/O リマップ数
t_ioReMap *ioMap; // I/O リマップテーブル
int ifParamCount; // パラメータ数
t_ifParam *ifParam; // パラメータ配列
} t_drvIFConfig;
// トップレベルドライバー設定(JSON "drivers" 配列の 1 エントリ)
typedef struct t_drvConfig {
const char *name; // ドライバー名 — virtualFuncMap エントリと一致する必要がある
bool isPhysical; // 仮想または物理ドライバー
int ifCount; // インターフェース数
t_drvIFConfig *ifConfig; // インターフェース配列
ResetFunc reset_ptr; // ドライバー init が設定するリセットハンドラー
PollFunc poll_ptr; // ドライバー init が設定するポールハンドラー
TaskFunc task_ptr; // ドライバー init が設定するタスクハンドラー
} t_drvConfig;
reset_ptr、poll_ptr、task_ptr フィールドは JSON から設定されるものではありません — コア 1 が適切なタイミングでドライバーのハウスキーピング関数を呼び出せるよう、ドライバーの init 関数によって設定されます。
再配置可能なベース I/O ポート。マシン非依存カードをカスタム / 実験者ボード上で使用できるように(OpenZ80 を参照)、いくつかのドライバーはベース I/O ポートをハードコードせず、インターフェースの iomap エントリから読み取ります。エントリの dstaddr がカードのベースになり(srcaddr はカード本来のポート)、各カードはヘッダーに *_DEFAULT_BASE 定数を持ち、iomap エントリが存在しない場合に使用されます。再配置可能なカードとデフォルトは、MZ-1R12(0xF8/3)、MZ-1R18(0xEA/2)、MZ-1R23(0xB8/2)、MZ-1R37(0xAC/2)、PIO-3034(0x00/4)、MZ-8BIO3 / MZ-1E24(0xB0/4)、MZ-1E05(0xD8/7)、Celestite(0x60/16)です。JSON の iomap/addrmap キーは小文字(srcaddr / dstaddr)で、数値として解析されます。GUI 設定ページの Base I/O Port フィールドがこれらを書き込みます。
コア 1 ディスパッチループ
Z80CPU_cpu() 内のタイトな無限ループを実行します。このループで何が起こるかを理解することが正しいドライバーを作成するための基本です。
メインループ構造
// src/Z80CPU.c — コア 1 エントリポイント
void __func_in_RAM(Z80CPU_cpu)(Z80CPU *cpu)
{
// コア 0 にコア 1 が動作中であることをシグナルする
multicore_fifo_push_blocking(1);
while(1)
{
// --- 保留チェック ---
// コア 0 はコア 1 を一時停止できる(例:安全な設定リロード用)
if(cpu->hold == true)
{
cpu->holdAck = true;
while(cpu->hold == true); // スピン待機
cpu->holdAck = false;
}
// --- ドライバーポール ---
// 短い定期的なハウスキーピングのために各ドライバーのポールハンドラーを呼び出す
for(int idx = 0; idx < cpu->_drivers.drvCount; idx++)
{
if(cpu->_drivers.driver[idx].poll_ptr != NULL)
cpu->_drivers.driver[idx].poll_ptr(cpu);
}
// --- Z80 実行 ---
// 2048 クロックサイクル Z80 エミュレーターを実行する
z80_run(&cpu->_Z80, 2048);
// --- リセットチェック ---
// PIO IRQ 3 = ホストバスでハードウェア RESET がアサートされた
if(cpu->forceReset || (pio_2->irq & (1u << 3)) != 0)
{
Z80CPU_reset(cpu);
CLEAR_IRQ(pio_2, 3);
}
}
}
- ループはイテレーションごとに 2048 サイクル
z80_run()を実行します。イテレーション間に、すべてのドライバーポールハンドラーが呼び出されます。つまりポールハンドラーはおよそ 2048 Z80 クロックサイクルごとに呼び出されます — 3.5MHz では約 585 マイクロ秒ごとです。 - ポールハンドラーは非常に短くなければなりません。エミュレーションの重要なパスにあります。遅いポールハンドラーは Z80 バスタイミングにジッターをもたらします。
z80_run()関数(Zeta Z80 ライブラリから)は Z80 命令を実行し、すべてのバストランザクションでZ80CPU_readMem()、Z80CPU_writeMem()、Z80CPU_readIO()、Z80CPU_writeIO()にコールバックします。
メモリ読み取りディスパッチ
Z80CPU_readMem() が呼び出されます。この関数はメモリシステムの中核です — それを理解することでハンドラーが何をする必要があるかが正確にわかります。
// src/Z80CPU.c(簡略化・注釈付き)
uint8_t __func_in_RAM(Z80CPU_readMem)(Z80CPU *cpu, uint16_t addr)
{
// ステップ 1:このアドレスを含む 512 バイトブロックを検索
uint8_t blockIdx = addr >> 9; // addr / 512
uint32_t membankptr = cpu->_membankPtr[blockIdx]; // 32 ビットエンコードエントリ
// ステップ 2:エンコードされたエントリから 3 つのフィールドを抽出
uint8_t memType = (membankptr >> 24) & 0xFF; // 上位バイト = タイプ
uint8_t bank = (membankptr >> 16) & 0xFF; // 中位バイト = バンク
uint16_t blockBase = membankptr & 0xFFFF; // 下位 16 ビット = ベースアドレス
// ステップ 3:このアドレスの PSRAM バンクへのオフセットを計算
uint32_t RAMaddr = (bank * MEMORY_PAGE_SIZE) + blockBase;
uint16_t blockOfs = addr & (MEMORY_BLOCK_SIZE - 1); // addr % 512 = ブロック内オフセット
uint8_t waitStates = cpu->_memAttr[bank][blockIdx].waitStates;
uint8_t data = 0x00;
switch(memType)
{
case MEMBANK_TYPE_PHYSICAL:
case MEMBANK_TYPE_PHYSICAL_VRAM:
case MEMBANK_TYPE_PHYSICAL_HW:
// パススルー:ホストハードウェアに応答させる
data = Z80CPU_readPhysicalMem(cpu, addr);
break;
case MEMBANK_TYPE_RAM:
case MEMBANK_TYPE_VRAM:
case MEMBANK_TYPE_ROM:
if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
{
// この特定のアドレスにハンドラーがインストールされている。
// ハンドラーが使用または変更できるように現在の PSRAM 値を 'data' として渡す。
data = cpu->_z80PSRAM->memioPtr[addr](
cpu, true, addr,
cpu->_z80PSRAM->RAM[RAMaddr + blockOfs]);
}
else
{
// ハンドラーなし — PSRAM から直接読み取る
data = cpu->_z80PSRAM->RAM[RAMaddr + blockOfs];
}
if(waitStates) Z80CPU_waitPhysicalStates(cpu, waitStates);
break;
case MEMBANK_TYPE_FUNC:
// 純粋な仮想デバイス — PSRAM バッキングなし
if(cpu->_z80PSRAM->memioPtr[addr] != NULL)
data = cpu->_z80PSRAM->memioPtr[addr](cpu, true, addr, 0);
break;
case MEMBANK_TYPE_PTR:
// 間接 — バイトごとのポインターに従って再帰
data = Z80CPU_readMem(cpu, addr, cpu->_z80PSRAM->memPtr[addr]);
break;
}
return data;
}
ドライバーフレームワーク
- トップレベルドライバー(ペルソナとも呼ばれる) —
Z80CPU.cのvirtualFuncMap[]に登録。各ペルソナはメモリレイアウト、バンキング、I/O ポート、オプションのサブインターフェース(インターフェースカード)セットを含むマシンペルソナ全体を設定します。 - インターフェースドライバー — ペルソナ独自の
interfaceFuncMap[]に登録。各インターフェースドライバーはペルソナに特定のペリフェラル(フロッピー、QuickDisk、RAM 拡張、ファイリングシステム)を追加します。
interfaceFuncMap[] を持ち、異なるサブセットのインターフェースを含みます。完全な互換性マトリックスについてはテクニカルガイドの「ペルソナ–インターフェース互換性」テーブルを、JSON 設定例についてはユーザーマニュアルを参照してください。
virtualFuncMap — トップレベルドライバー登録
src/Z80CPU.c の virtualFuncMap[] 配列は文字列名をドライバーの init 関数にマッピングします。すべてのトップレベルドライバー(ペルソナ)はここにエントリが必要です。文字列名は JSON の "drivers" 配列の "name" フィールドと正確に一致する必要があります(大文字小文字を区別しない)。
// src/Z80CPU.c
#ifdef INCLUDE_SHARP_DRIVERS
{"MZ700", MZ700_Init}, // Sharp MZ-700 ペルソナ
{"MZ80A", MZ80A_Init}, // Sharp MZ-80A ペルソナ
{"MZ2000", MZ2000_Init}, // Sharp MZ-2000 ペルソナ
{"MZ2200", MZ2200_Init}, // Sharp MZ-2200 ペルソナ
{"MZ80B", MZ80B_Init}, // Sharp MZ-80B ペルソナ
{"MZ2500", MZ2500_Init}, // Sharp MZ-2500 ペルソナ
{"MZ800", MZ800_Init}, // Sharp MZ-800 ペルソナ(デュアルモード MZ-700/MZ-800)
{"MZ1500", MZ1500_Init}, // Sharp MZ-1500 ペルソナ
{"MZ1R23", MZ1R23_Init}, // 漢字 ROM / 辞書 ROM ボード
{"MZ1R37", MZ1R37_Init}, // 640KB 拡張メモリマネージャー
{"PIO3034", PIO3034_Init}, // IO DATA 320KB EMM
{"Celestite", Celestite_Init}, // Celestite LAN/メモリ複合ボード
#endif
#ifdef INCLUDE_PCW_DRIVERS
{"PCW9512", PCW9512_Init}, // Amstrad PCW-9512 ペルソナ
#endif
#ifdef INCLUDE_TATUNG_DRIVERS
{"EinsteinTC01", EinsteinTC01_Init}, // Tatung Einstein TC-01 ペルソナ
#endif
#ifdef INCLUDE_OPEN_DRIVERS
{"Open", Open_Init}, // OpenZ80 バニラ / 実験者向けペルソナ
#endif
};
OpenZ80 — 既存のドライバーや BIOS をベースにする
src/drivers/Other/Open.c、build_tzpuPico.sh open でビルド)は、2 種類の実験者の出発点です。picoZ80 を自作ボードに搭載する人と、専用ペルソナがまだない Z80 マシンを立ち上げる人です。Open.c は MZ700.c をモデルにしていますが、マシンハードウェアをすべて取り除いています。PHYSICAL モードでは 64K のメモリと I/O 空間全体を実際のボードへ通過させ(インターフェースカードは固定ポートに重ねられます)、VIRTUAL モードではフラットな 64K RAM を提示し、そこへドライバーレベルの ROM が 0x0000 から順に読み込まれます。#ifdef INCLUDE_OPEN_DRIVERS のもとで {"Open", Open_Init} として登録され、マシン非依存のインターフェースカード(MZ-1R12/1R18/1R23/1R37、PIO-3034、MZ-8BIO3、MZ-1E24、MZ-1E05、Celestite)のみを提供します。そのいずれも任意のベース I/O ポートへ移動できます。
src/drivers/Other/Open.c— バニラペルソナ(まったく新しいボードに最適なベース)。src/drivers/Sharp/—MZ700.c、MZ80A.c、MZ80K.c、MZ80B.c、MZ800.c、MZ1500.c、MZ2000.c、MZ2200.c、MZ2500.c。src/drivers/Amstrad/PCW9512.c— 統合された uPD765 FDC を備えた自己完結型マシン。src/drivers/Tatung/EinsteinTC01.c— 統合された WD1770 FDC を備えた自己完結型マシン。
virtualFuncMap[] に {"MyMachine", MyMachine_Init} 行を追加し(適切な #ifdef のもとで)、そのソースを src/CMakeLists.txt に組み込みます(例: pZ80_drivers_open_src リストに追加)。さらに add_z80_model_targets(...) でモデルターゲットを追加します。
asm/ ディレクトリに、コメント付きの Z80 アセンブラーソースとして管理されています(GLASS Z80 アセンブラーでアセンブル)。これらをベースとして、自分のマシン向けに再ビルドやパッチができます。
| Purpose | Source files |
|---|---|
| Monitor ROMs | RFS/asm/sp1002.asm (MZ-80K SP-1002), RFS/asm/sa1510.asm (MZ-80A SA-1510), RFS/asm/1z-013a.asm & TZFS/asm/1z-013a.asm (MZ-700), RFS/asm/mz800_iocs.asm (MZ-800 IOCS) |
| Boot loaders (IPL) | TZFS/asm/mz2000_ipl.asm (MZ-2000), TZFS/asm/mz80b_ipl.asm (MZ-80B), RFS/asm/ipl.asm |
| CP/M BIOS | RFS/asm/cbios.asm, RFS/asm/cpm22-bios.asm, TZFS/asm/cbios.asm, TZFS/asm/cbiosII.asm |
| Floppy / QuickDisk boot ROMs | RFS/asm/mz80afi.asm (MZ-80A FDC), TZFS/asm/mz80kfdif.asm (MZ-80K FDIF), RFS/asm/mz-1e05.asm, RFS/asm/mz-1e14.asm (QD), RFS/asm/sfd700.asm |
| In-ROM filing systems | RFS/asm/rfs.asm (banked), TZFS/asm/tzfs.asm (banked) |
rom[].loadaddr エントリ(インターフェースカードの場合)から、または OpenZ80 の仮想モードでは 0x0000 に読み込まれるドライバーレベル ROM として参照します。RFS と TZFS はどちらもこれらの ROM イメージを生成する自己完結型のビルドスクリプトを同梱しています(各デベロッパーズガイドを参照)。
TZFS — メモリモードと仮想サービスプロセッサー
src/drivers/Sharp/TZFS.c は、バンク切り替え式のメモリモードエミュレーションとコア間サービスモデルを組み合わせたドライバーの良い実例です。CP/M を下層に持つマルチバンクの低レベルモニター兼ファイリングシステム(MONITOR 1Z-013A の拡張)を実装し、tranZPUter SW の TZFS とその K64F 仮想 I/O プロセッサーをモデルとしています。これはトップレベルのペルソナではなく、MZ-700 ペルソナ上の選択可能なインターフェースとして登録されます(MZ700.c 内で RFS と並んで — 実際には排他的に — 登録)。
0x60 にモード値を書き込むことで tranZPUter メモリレイアウトを選択し、ドライバーはそれに応じてバンクポインターを再設定します。モードは TZMM_ORIG、TZMM_BOOT、TZMM_TZFS、TZMM_TZFS2、TZMM_TZFS3、TZMM_TZFS4、TZMM_CPM、TZMM_CPM2、TZMM_COMPAT です。CP/M の CPM2 レイアウトはバイト単位のブロック 0 ページングを使用します(0x0000–0x003F → ベクターブロック、0x0040–0x01FF → TPA の先頭)。
OUT (0x68) を実行し、これが MSG_TZFS_SVCREQ メッセージを Core 0 にキューイングします。Core 0 はそれを TZFS_processServiceRequest を通じてディスパッチします。ファイリングシステムサービスは READDIR / NEXTDIR(キャッシュされた 16 エントリのディレクトリブロック)、READFILE / NEXTREADFILE、LOADFILE(MZF ヘッダーおよびバンクオブジェクト対応)、CHANGEDIR、CLOSE です。CP/M サービスは LOADBDOS(ウォームブート CCP + BDOS リロード)、ADDSDDRIVE、READSDDRIVE、WRITESDDRIVE、および CPU 周波数サービスです。picoZ80 には直接の SD アクセスがないため、CP/M の 512 バイトセクターは ESP32(ESP_readSector / ESP_writeSector)を介してイメージ全体のファイルに対してルーティングされます。ドライブごとのイメージパスはインターフェース JSON の param[].file エントリから取得され、フォールバックテンプレートは CPM/SDC16M/RAW/CPMDSK<nn>.RAW です。TZFS ROM(roms/tzfs.bin)は、付属プロジェクトの TZFS/asm/tzfs.asm のアセンブル出力です(その CP/M BIOS は TZFS/asm/cbios.asm / cpm22.asm から)。
初期化フロー
config.json を読み取り解析した後、ドライバーの初期化は以下の順序で発生します:
Z80CPU_configFromJSON()
├── Z80CPU_configDriversFromJSON() JSON から "drivers" 配列を解析
│ 各ドライバーエントリに対して:
│ ├── virtualFuncMap[] で名前を検索
│ ├── インターフェース設定を解析(ROM、addrmap、iomap、param)
│ └── VirtualFunc(cpu, appConfig, &drvConfig, NULL) を呼び出す
│ ↓
│ ドライバー init が設定する:
│ ├── _membankPtr[] エントリ(ブロックタイプとバンク)
│ ├── _memAttr[][] エントリ(ウェイトステート、同期)
│ ├── memioPtr[] ハンドラー(メモリアドレスフック)
│ ├── ioPtr[] ハンドラー(I/O ポートフック)
│ ├── config->reset_ptr = MyDriver_Reset
│ ├── config->poll_ptr = MyDriver_PollCB
│ └── config->task_ptr = MyDriver_TaskProcessor
│
├── Z80CPU_configMemoryFromJSON() "memory" 配列を適用(ドライバーのデフォルトを上書き)
└── Z80CPU_configIOFromJSON() "io" 配列を適用(ドライバーのデフォルトを上書き)
"memory" と "io" 配列が適用されます。つまり config.json の memory または io 配列の明示的なエントリはドライバーがそれらのアドレスに設定したものを上書きします。これにより、ドライバーのソースコードを変更せずにドライバーのデフォルトをユーザーが微調整できます。
ドライバーライフサイクルコールバック
t_drvConfig 構造体に関数ポインターを保存することで 3 つの継続的なコールバックを登録します。コア 1 は実行中の特定のポイントでこれらを呼び出します:
| コールバック | シグネチャ | 呼び出しタイミング | 典型的な用途 |
|---|---|---|---|
reset_ptr |
uint8_t f(Z80CPU *cpu) |
ホストの RESET ラインがアサートされた;Z80 PC = 0x0000 | デフォルトメモリマップを復元、バンク状態をクリア |
poll_ptr |
uint8_t f(Z80CPU *cpu) |
約 2048 Z80 サイクルごと(コア 1 上で) | ステータスフラグを確認、コア 0 にリクエストを送信 |
task_ptr |
uint8_t f(Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param) |
コア間タスクリクエストへの応答で | ファイル I/O の結果、ディスクセクターの配信を処理 |
poll_ptr はコア 1 から呼び出されるため高速でなければなりません。I/O を実行する必要がある場合(ディスクセクターの読み込み、UART コマンドの送信)は cpu->requestQueue を介してコア 0 にメッセージを送信して即座に返すべきです。コア 0 が I/O を実行し、次の機会に cpu->responseQueue を介して task_ptr をトリガーして結果を配信します。
実例:MZ-700 ドライバー
src/drivers/Sharp/MZ700.c)はコードベース内で最も完全なペルソナドライバーです。詳しく解説することで、独自のドライバーに必要なすべてのパターンを示します。MZ-80A ドライバー(src/drivers/Sharp/MZ80A.c)は、Intel 8253 PIT エミュレーションおよび MEMSW メモリスワップメカニズムを実演する、もう一つのリファレンス実装として利用できます。MZ-2000 ドライバー(MZ2000.c)は、物理モード(実 MZ-2000 ハードウェアでのドロップイン Z80 置換、ブート/通常モード自動検出)と仮想モード(PSRAM ベースの完全エミュレーション、IPL ROM ミラーリング)の両方をサポートし、BST/NST メモリモード切替と VRAM オーバーレイを実演するリファレンス実装です。MZ-2200 ドライバー(MZ2200.c)は MZ-2000 アーキテクチャをベースとし、カラー CRT 対応を追加しています。MZ-800 ドライバー(src/drivers/Sharp/MZ800.c)は、GDG ディスプレイモードレジスタを追跡してメモリマップのデコードをオンザフライで再構築するデュアルモードペルソナであり、仮想モードでの割り込みデイジーチェーンブリッジングを実演します(後述の MZ-800 ドライバー を参照)。
再利用可能なペリフェラルエミュレーションモジュールがいくつか利用可能です:PIT8253.c(Intel 8253 プログラマブルインターバルタイマー — 全 6 カウンターモード、BCD/バイナリカウント、カウンターラッチ、LSB/MSB 読み書きモード)、PPI8255.c(Intel 8255 プログラマブルペリフェラルインターフェース — モード 0 I/O、ポート C のビットセット/リセット、ポートごとの出力コールバックと入力インジェクション)、WD1773.c(WD1773 フロッピーディスクコントローラー — Sharp MZ ペルソナドライバーで使用)、WD1770.c(WD1770 フロッピーディスクコントローラー — Tatung Einstein ペルソナで使用、Extended CPC DSK、D88、標準 DSK フォーマット対応)、および uPD765.c(NEC uPD765 フロッピーディスクコントローラー — Amstrad PCW-9512 ペルソナで使用、CPC DSK フォーマットおよび物理ディスクイメージング対応)。これらのモジュールは任意のマシンペルソナドライバーからインスタンス化できるように設計されています。
MZ-800 ドライバー(デュアルモードペルソナ)
src/drivers/Sharp/MZ800.c)は、メモリマップが固定ではなくホストソフトウェアの制御下で実行時に変化するペルソナの好例です。MZ-800 は MZ-700 のスーパーセットであり、MZ-700 互換モードで起動し、異なるメモリレイアウトとグラフィック幅を持つネイティブ MZ-800 モードに切り替えることができます。ドライバーはマシンモードを追跡し、モードが変化するたびにブロックデコードテーブルを再構築します。
GDG ディスプレイモードレジスタ(ポート 0xCE)。ドライバーはポート 0xCE に ioPtr[] ハンドラーをインストールし、GDG ディスプレイモードレジスタ(DMD)への書き込みをスヌープします:
- ビット 3 はマシンモードを選択します — クリア = ネイティブ MZ-800、セット = MZ-700 互換。
- ビット 2 はグラフィック幅を選択します — 320 ピクセル対 640 ピクセル。
書き込みがいずれかのビットを変更すると、ドライバーは MZ800_applyMemoryMap() を呼び出します。これは 128 個の 512 バイトブロックを走査し、新しいレイアウトを反映するように _membankPtr[] エントリをオンザフライで書き換えます。これは membankPtr エンコーディング で説明したのと同じ高速ディスパッチメカニズムです — デコードテーブルのみが再構築され、基盤となる PSRAM は変更されないため、バスストールは不要です。
メモリマップ制御フラグ。現在のレイアウトは小さな状態構造体にフラグのビットマスクとして保持されます:ROM_0000(0x0000 に表示されるモニター ROM)、ROM_1000(0x1000 の CG-ROM ウィンドウ)、CGRAM_VRAM(0x1000 ウィンドウにマッピングされたキャラクタージェネレーター RAM/VRAM)、ROM_E000(0xE000 の IOCS ROM / ハードウェア領域)。MZ800_applyMemoryMap() は、これらのフラグと現在の MZ-700/MZ-800 および 320/640 モードビットからブロックタイプを導出します。電源投入/リセット時のマップはモニター ROM、CG-ROM、IOCS ROM を公開します。MZ-800 のメモリバンキングポート(0xE0–0xE6)はドライバーによって処理され、下流の実機が同期を保つように物理ハードウェアにミラーリングされます。
割り込みデイジーチェーンブリッジング(仮想モード)。これはドライバーの中で最も微妙な部分で、MZ-2500 ドライバーと同じパターンに従います。ネイティブ MZ-800 モードでは、周期割り込みは実 8253 によって生成され、割り込みデイジーチェーンに位置する物理 Z80-PIO を通じて供給されます。PIO のインサービスラッチは、実バス上で RETI オペコードフェッチ(ED 4D)を観測したときにのみクリアされます — しかし仮想モードでは割り込みサービスルーチンとその RETI は PSRAM から実行されるため、PIO はそれらを見ることがなくラッチされたままとなり、後続のすべての割り込みをブロックします。ドライバーは Zeta コアに 2 つのフックをインストールすることでこれをブリッジします:
MZ800_readIntAck()—cpu->_Z80.inta(割り込みアクノリッジコールバック)としてインストールされます。MZ800_retiHandler()—cpu->_Z80.reti(RETI コールバック)としてインストールされます。
どちらも mz800PhysicalReti() を呼び出し、実バス上で物理 RETI を再生します:実ホスト RAM の SP-2 に 2 バイト ED 4D を配置し、物理バス上で両バイトの M1 オペコードフェッチを実行し(PIO のデイジーチェーンロジックが RETI を観測してインサービスラッチをクリアするように)、その後、置き換えた元のバイトを復元します。これらのフックはインターフェースが仮想的に実行されている場合(!isPhysical)にのみインストールされます。物理モードでは実 PIO が実 RETI を直接見るため、ブリッジングは不要です。
リセット。MZ800_Reset() は電源投入時のメモリマップを復元し、ゲートアレイのリセットを模倣するために OUT 0xE4 を発行し、RFS/MZF 作業領域(0x1000–0x1168)をクリアします。また、8253 PIT にはリセットピンがないため、MZ-700 モードでは 8253 を再プログラムしてマスクし、リセット後に古いカウンターが誤った割り込みを発生させないようにします。
デベロッパーノート:TZFS からのフロッピーブートとリセット時の TZFS への復帰
- TZFS
GETBOOTDSKは、あらゆる Sharp マシン ID(01/02/03)とIPLPROシグネチャを受け付けるようになり、1000H 未満に読み込む OS(例:MZ-800 CP/M)のためにブロック 7 の DRAM を事前に 0000–0FFF にページングし、ネイティブの 9Z-504M IPL とまったく同じように読み込んだプログラムにBC=0200Hを渡すため、2 段目のローダーが独自のディレクトリ読み取りを駆動できます。 JP (HL)前の 8253 のなだめ。TZFS は読み込んだプログラムへジャンプする前に、カウンター 2 を約 1Hz に再プログラムし、8255 PC2 をマスクします(IPL の SORES を模倣)。これにより、フリーランニングのカウンターが Z80 を RST 38H 割り込みで溢れさせ、早期に割り込みを再有効化するローダーを中断させることを防ぎます。これが「P-CP/M80 → No system file」の失敗を修正します。- MZ-1E05
MZ1E05_IO_A10Toggle。FDC ハードウェアアクセラレーションのフェッチハンドラーが 0xF3FE / 0xF7FE で DRQ と A10 を OR することで、TZFSDSKREADは MZ-80A と同じように MZ-800 でもブートセクターをストリーミングします。 - TZFS
TZMM_DSKLOAD/TZMM_DSKRUNモードは、フロッピーブートした OS をマシン自身の IPL とまったく同じようにネイティブのブロック 0 DRAM で読み込んで実行します。 - MZ-800
cgWindow追跡。PCG フォントを読み込む MZ-800 モードの OS(IN 0xE0)は、TZFS 下で実 CG-ROM / CG-RAM に到達し(ディスク BASIC の文字化けを修正)、Flappy などのネイティブゲームは影響を受けません。 - リセット復元。リセット時、ドライバーはクリーンなブロック 0 のモニター + TZFS UROM を再読み込みし(
TZMM_DSKRUNで OS が上書きしたもの。ブロック 0 の ROM/RAM は同じ PSRAM にエイリアスされているため)、その後 TZFS をコールドブートします。これにより RESET スイッチは素の 1Z-013A モニターではなく TZFS に復帰します。
LDIR でコピーした後にビデオモードを切り替えても、両方のパスで同じメモリを見るように、TZFS 下で 0x0000–0x0FFF をバンク 7 からマッピングすること(一般的な Flappy PSRAM バンク不一致の修正);/RESET が解除された時点で initVideoText を一度再実行し、GDG が再スキャンしてモニターの HBLK 同期が完了するようにすること(ハードウェアリセット GDG 回復);そして、オンボードの GDG/8255 がハードウェアリセットを完了してからファームウェアがビデオを再初期化するように、10ms の物理リセット安定化遅延を設けること。
- 下位 4KB(0x0000–0x0FFF)は電源投入時にはモニター ROM ですが、I/O ポートへの書き込みで RAM に入れ替えることができます — いわゆる「MZ-700 バンク切り替え」。
- 上位領域(0xD000–0xFFFF)にはビデオ RAM、カラー VRAM、メモリマッピングハードウェアレジスタが含まれます。上位領域全体も I/O ポートで RAM に入れ替えることができます。
- メモリバンキングは 0xE0–0xE6 の 6 つの I/O ポートを介して制御されます。
バンキングハンドラー — MZ700_IO_MemoryBankPorts()
Z80CPU_writeIO() によって呼び出されます。これらのポートからの読み取りには副作用がありません(0xFF を返す)。書き込みは _membankPtr[] エントリをリアルタイムで変更することでメモリマップを変更します。
// src/drivers/Sharp/MZ700.c
uint8_t MZ700_IO_MemoryBankPorts(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
uint8_t port = (uint8_t)(addr & 0xFF); // Z80 アドレスからポート番号を抽出
// 読み取りには副作用なし
if(read) return 0xFF;
// --- ポート 0xE0:下位 4KB DRAM を有効化 ---
// モニター ROM(0x0000-0x0FFF)をバンク 1 の RAM に入れ替える
if(port == 0xE0 && !MZ700Ctrl.loDRAMen)
{
for(int idx = 0; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24)
| (MZ700_MEMBANK_1 << 16)
| (idx * MEMORY_BLOCK_SIZE);
}
MZ700Ctrl.loDRAMen = true;
}
// --- ポート 0xE1:上位 DRAM を有効化(0xD000-0xFFFF)---
// 現在の上位 membankPtr エントリを保存してバンク 1 の RAM に置き換える
if(port == 0xE1 && !MZ700Ctrl.inhibit && !MZ700Ctrl.hiDRAMen)
{
// ... バンクを入れ替えて保存する
MZ700Ctrl.hiDRAMen = true;
}
// --- ポート 0xE2:下位 ROM を復元 ---
if(port == 0xE2 && MZ700Ctrl.loDRAMen)
{
// ... バンク 0 の ROM に復元する
MZ700Ctrl.loDRAMen = false;
}
// --- ポート 0xE3:上位ハードウェアマッピングを復元 ---
if(port == 0xE3 && MZ700Ctrl.hiDRAMen)
{
// ... 保存されたマッピングを復元する
MZ700Ctrl.hiDRAMen = false;
}
// --- ポート 0xE4:上位 DRAM スワップを禁止 ---
if(port == 0xE4)
MZ700Ctrl.inhibit = true;
// --- ポート 0xE5:上位禁止を有効化(エイリアス)---
if(port == 0xE5)
MZ700Ctrl.inhibit = false;
return 0;
}
_membankPtr[] を変更してメモリバンキングを実装すること。変更は即座に有効になります — Z80 からの次のメモリアクセスは新しいマッピングを使用します。
上位メモリ領域の保存/復元パターン(MZ700Ctrl.upmembankPtr[])は重要です:ハードウェアマッピング領域を RAM に入れ替えるとき、ソフトウェアが元に戻したときに復元できるように何があったかを記憶する必要があります。単純に PHYSICAL を再度割り当てると、サブドライバーまたは JSON 設定によって設定されたカスタムマッピングが失われます。
実例:仮想 CMT(カセット)ドライバー
src/drivers/Sharp/CMT.c + src/include/drivers/Sharp/CMT.h)は、Sharp MZ シリーズ向けの波形レベルの仮想カセットデッキです。ローダーではなくカセットの波形をエミュレートするため、Z80 から見ると実機のデッキと見分けがつきません:モニターの LOAD、BASIC、カスタム/ターボローダー、そして SAVE はすべてそのまま動作します。SD カードからの読み込みは引き続き高速パスであり、CMT は本物のパスです — .mzf を取って「実カセットのように」読み込んだり、新しい .mzf に録音したりできます。これは、実時間に対してタイミングを正確に合わせる必要があり、チューニング済みの物理ホットパスを乱すことなく既存のハードウェアブロックを再タイプ化し、両コアと ESP32 へ戻るリバースコマンドキューにまたがるドライバーの好例です。
1 つのエンジンが、ペルソナから推論される 2 つのハードウェアファミリーを扱います:
- SIMPLE(MZ-80K / MZ-80A / MZ-700 / MZ-800)— 8255 のモーターオン/オフ、リニアテープ、1200 ボー(0 ~ 504µs、1 ~ 958µs)。MZ-800 は約 379/964µs に切り詰め、E000–E003 のメモリマッピング 8255 ではなく I/O ポート D0–D3 を通じてカセットを読み書きします。
- CONTROLLED(MZ-80B / MZ-2000 / MZ-2200 / MZ-2500)— コンピューター制御のトランスポート(PLAY/STOP/FF/REW/EJECT に加えて APSS プログラムサーチ)、2000 ボー(約 333/667µs、約 1800 ボーの変種あり)。
ドライバーエンジンと登録
CMT_Init / CMT_Reset / CMT_PollCB / CMT_TaskProcessor — を公開し、RFS や MZ-1E05 とまったく同じように、対応する各マシンの interfaceFuncMap[] にインターフェース(サブドライバー)として登録されます(インターフェース関数マップを参照):
// 各マシンの interfaceFuncMap[](例:src/drivers/Sharp/MZ700.c):
static t_InterfaceFuncMap interfaceFuncMap[] = {
{"RFS", false, RFS_Init, RFS_Reset, RFS_PollCB, RFS_TaskProcessor},
{"CMT", false, CMT_Init, CMT_Reset, CMT_PollCB, CMT_TaskProcessor},
// ... 他のインターフェース ...
};
time_us_64() に対して測定され — RFS CMT が使うのと同じワンショットタイマー方式 — エミュレートされた Z80 の T ステートに対しては決して測定されません。そのため、あるスライスの z80_run() が何サイクル実行したかに関わらず、エミュレートされる波形は実機のデッキに忠実なままです。ストリーミングエッジカーソルは償却 O(1) 時間で進み、巨大な事前計算済みエッジバッファーは存在しないため、プログラム全体が固定された小さなメモリフットプリントでストリーミングされます。CMT_Init は通常のデュアルパーパス契約(ifName != NULL のときは検証モード、config != NULL のときは設定モード)に従い、設定モードではペルソナからファミリーを推論して、以下で説明する適切なフックをインストールします。
Simple ファミリー — 8255 カセットブロックの再タイプ化
MEMBANK_TYPE_RAM に再タイプ化し、その上に単一のブロック全体の memioPtr ハンドラーをインストールします(メモリブロックタイプ定数と RAM インターセプトを参照)。ブロック全体のハンドラーの要点は、仮想化されるのは Port C のカセットビットのみであり、ブロック内の他のすべては実ハードウェアとまったく同じように振る舞わなければならない、ということです:
- 非カセットレジスタはそのまま通過します。キーボード(E000/E001)、8253(E004–E007)、ジョイスティック(E008)およびそれらのミラーは、
Z80CPU_readPhysicalMem()/Z80CPU_writePhysicalMem()を呼び出すことで処理されるため、実ホストチップは手を加えていないMEMBANK_TYPE_PHYSICAL_HWマッピングの下と同じように応答します。 - 重ね合わせ、あるいはスヌープされるのは Port C のカセットビットのみです。読み取り時には仮想カセットの現在の読み取りデータレベルが物理 Port C から返されるバイトに重ねられ、書き込み時にはモーター/録音ビットがスヌープされます。したがって、チューニング済みの
PHYSICAL_HWホットパスはすべての非カセットアクセスに対してそのまま残されます — 再タイプ化は、必要な箇所にだけドライバーにフックをもたらします。
Controlled ファミリー — E0–E3 の ioPtr[] フック
ioPtr[] ハンドラーをインストールします(I/O ポートフックを参照)。これらはトランスポートコマンド — PLAY/STOP/FF/REW/EJECT — と APSS プログラムサーチをデコードします。仮想テープはプログラムの順序付きキュー(後述)であるため、FF/REW はプログラム単位でスキップし、APSS は次/前のプログラム境界までシークしてそこで自動停止し、実機デッキの動作を再現します。
テープキュー(設定パラメータ)
t_ifParam、JSON の param[] リスト — ドライバー設定構造体を参照)としてドライバーに供給されます:各エントリは {enable, file} のペアであり、ファミリーはペルソナから推論されます。キューは新しい esp32/webserver/js/cmt.js パネルを介して Web GUI からライブで編集できます(追加 / 削除 / 並べ替え)。各プログラムの終わりでキューは自動的に前進します — 連続したテープの動作 — ため、次の LOAD はユーザーの介入なしに次のプログラムを見つけます。
コア間 SD I/O と録音
cpu->requestQueue / cpu->responseQueue のペアを通じてコア 0 に送られ、コア 0 / コア 1 の相互作用で説明したハンドラーでフラグ設定 → ポールでエンキュー → タスクで適用というパターンにまったく従います。
録音(保存)。モーターが動作し録音モードが選択されている状態で、書き込みデータラインがサンプリングされ、その場でデコードされます:各半波区間が短/長に分類され、短/長のランがビットになり、ビットがバイトに組み立てられ、ブロックチェックサムが検証されます。その後、ドライバーは MZF を合成し、それをコア 0 に渡して SD カードに以下の名前で保存します:
CMT/taperecord_<name>_<YYYYMMDD_HHMMSS>.mzf
<name> は Sharp のヘッダーファイル名で、Sharp のディスプレイコードから ASCII にトランスコードされ、その後 FAT 用にサニタイズされます(不正な文字 → _)。ESP32 の時計が未設定の場合、タイムスタンプはシーケンス番号(taperecord_<name>_0001.mzf)にフォールバックし、FAT-1980 の日付衝突を回避します。実際の Sharp 名は MZF ヘッダーの内部にそのまま保存されるため、プログラムは再読み込み時に元の名前を保持します。
実 ↔ 仮想の実行時トグル(リバースコマンドキュー)
CP_sendCmd)— コントロールプロセッサーリンクの ESP32 から RP2350 への方向 — を通り、ドライバーのモードフラグを設定します:
- VIRTUAL は読み取り時に仮想カセットビットを重ね、物理的に接続されたデッキへのモーター/トランスポートの書き込みを抑制します。
- REAL は純粋なパススルーです:CMT は読み取りデータラインを読み取り専用でスヌープし、モーターは実機のデッキを駆動します。
SAVE したり、その逆を行ったりできます。完全なプロトコル、波形テーブル、コーデックの詳細は、picoZ80 リポジトリの docs/VIRTUAL_CMT_DESIGN.md に記載されています。
仮想ペリフェラルデバイス — 内部構造
src/devices.c と src/include/devices.h にあり、チップモデル自体は src/drivers/ 配下の通常のドライバーです。ユーザーは device 配列でインスタンスを宣言し、interlink 配列でそれらを配線します — どちらもテクニカルガイドに記載されています。
Z80DMA.c、Z80CTC.c)は純粋なチップエミュレーションであり、どのマシンに載っているかについては何の前提も持ちません。デバイスは、自分でシステムを構築する人のために、そうしたモデルを設定から宣言してインスタンス化したものです。インターフェースカードは既存の drivers[].if[] の仕組みで、ROM の切り出し、メディア処理、実 / 仮想の切り替えといったマシン固有のグルーコードを伴います。これらは互いに階層関係にあるのではなく、同じ IC モデルを共有する対等な存在です。IC モデルで行った精度向上の作業は、それを使うデバイスとカードの双方に恩恵をもたらします。
デバイスの登録
src/devices.c の deviceFuncMap[] に 1 行を持ちます。この行はチップ名を指定し、Web インターフェースが表示するタイトルと説明を与え、どのコアがサービスするかを宣言し、モデルのエントリーポイントとシグナル宣言テーブルを指し示します。デバイス名は大文字小文字を区別せずに照合されます。
t_devSignalDecl テーブルでも宣言します — ピン名、平易な言葉によるラベル、方向、負論理かどうか、アイドルレベル、オープンドレインかどうかです。このテーブルがピンに関する唯一の真実の情報源です。ファームウェアはこれを使ってシグナルテーブルを構築し、tools/gen_devicecat.py はビルド時にこれを読み取って、Web インターフェースが参照するカタログを生成します。
シグナルとネットリストの評価
Signal_drive() で出力を駆動し、双方向ピンについては Signal_setDir() で方向を設定します。入力側のレベル変化は、シグナルコールバックを通じてデバイスに届けられます。Z80 PIO は各ポートビットを独立して扱っており、新しい双方向デバイスを作るときに手本とすべき正しいモデルです。
コアの分担
割り込みデイジーチェーン
IEI / IEO の配線から決まるため、ネットリストがマシンの唯一の記述となります。各デバイスタイプは、自身のどのピンが IEI と IEO であるかを deviceFuncMap[] の行で宣言し、あわせてチェーンが呼び出す 3 つのエントリーポイントも宣言します — デバイスが要求中かサービス中かを報告するもの、アクノリッジしてベクターを返すもの、そして RETI を処理するものです。
IEI を別のデバイスの IEO から駆動するノードがあれば、その別のデバイスが前段になります。IEO が High に駆動されるのは IEI が High で、かつデバイスが割り込み要求中でもサービス中でもない場合だけであり、これが優先度ルールのすべてです。あとはインターリンクがその抑止をチェーンの下流へ伝えます。
IEI / IEO ピンを持っています — 割り当てられる予備のプロセッサーピンがないためです — したがって調停することは決してできません。そこでデバイスフレームワークは、既にインストールされている割り込みアクノリッジおよび RETI ハンドラーがあればそれにチェーンし、自分のデバイスを検討する前にアクノリッジをそちらへ提示します。これがなければ、独自のハンドラーをインストールする MZ-8BIO3 および MZ-1E24 カードは、デバイスを 1 つ追加しただけで黙って壊れていたことでしょう。
RETI がサービス中を再びクリアすることです。
デバッグシェルのツール群
src/dbgsh_devices.inl にあります。それぞれインスタンス識別子を省略可能な引数として取り、省略した場合はそのタイプの最初のデバイスが使われます。
| コマンド | 目的 |
|---|---|
dev |
設定されたデバイスを、アドレス空間、アドレス、サイズ、DMA 設定とともに一覧表示します。 |
sig |
宣言されたすべてのピンを方向と現在のレベルとともに一覧表示します。sig set <dev.pin> <0\|1> はレベルを強制し、ネットリストに伝播させます。 |
dma [id] [force] |
DMA のライトレジスターをデコードして、作業アドレス、カウンター、ステータス、状態とともにダンプします。force は転送エンジンを直接実行します。 |
ctc [id] |
各チャンネルの制御バイト、プリスケーラー、時定数、ダウンカウンター、割り込み状態、および次のイベントが発生する時刻をダンプします。 |
pio [id] |
各ポートのモード、出力レジスター、入力レジスター、方向マスク、割り込み状態をダンプします。 |
sig set です — Z80 のコードを 1 行も書かずに、手動でピンをアサートしてネットリストの他の部分がどう反応するかを観察できます。Z80 側からテストする場合は Signal Port を宣言してください。8 本の配線を 1 つの I/O アドレスに割り当てるので、テストプログラムからネットリストに刺激を与え、その応答を読み戻すことができます。
新しいデバイスモデルの追加
- チップモデルを
src/drivers/配下の通常のドライバーとして、特定のマシンに依存しない形で記述します。レジスターは他のエミュレーターではなくメーカーのデータシートからモデル化してください — 既存のエミュレーターは、想像以上に高い頻度で公表された動作から乖離しています。 - ピンを
t_devSignalDeclテーブルで宣言します。ラベルはそのチップを知らない読者に向けて書いてください。これらは Web インターフェースが表示する文言になります。 deviceFuncMap[]に行を追加し、モデルの init、read、write、signal、reset の各エントリーポイントを指定して、サービスコアを選びます。- パッケージングのデフォルト値 — アドレス空間、デコードサイズ、推奨ベースアドレス — を
tools/gen_devicecat.pyのDEFAULTSテーブルに追加します。これらは C のテーブルには存在しないためです。 tools/gen_devicecat.pyを実行してesp32/webserver/js/devicecat.jsを再生成し、再生成したファイルをコミットします。このカタログは古くなり得るビルド成果物です — 再生成せずにデバイスやピンを追加しても、Web インターフェースには現れません。- チップに自明でない動作がある場合は、
test/配下にホスト側の適合性テストを追加します。DMA モデルには、gccで偽のアドレス空間に対してドライバーをビルドし、データシート自身の表と突き合わせて検証するテストがあります。
現在の未完了事項
- デバイスはプロセッサーのリセットではリセットされません。リセット関数は存在しますが呼び出されることがなく、デバイスは設定が適用されるときに一度だけ初期化されます。
- オープンドレイン配線は自動化されていません。
openDrainフラグはピンごとに宣言されていますが評価時に参照されないため、ワイヤード AND の割り込みノードは明示的なAND演算子で記述する必要があります。 - 8255 の入力パスはビットを破壊します。入力ピンのレベルをポートに畳み込む際にまずポートの出力レジスターを読み出しますが、入力として設定されたポートではこれがゼロを返すため、入力ビットを 1 本駆動すると他のビットがクリアされてしまいます。修正する際は Z80 PIO のビット単位の入力パスが手本になります。
- 再設定時にインスタンスカウンターが完全にはリセットされません。設定を再適用したときにクリアされるのは DMA プールだけなので、再設定を繰り返すとインスタンススロットがリークします。
mirror、refresh、traceの各キーは解析されて保存されますが、決して使用されません。
docs/VIRTUAL_Z80DMA_DESIGN.md、docs/VIRTUAL_Z80CTC_Z80PIO_DESIGN.md、docs/VIRTUAL_DEVICE_GUI_DESIGN.md — には、完全な導出過程、データシートの参照先、各決定の根拠が記載されています。設計文書とコードが食い違う場合はコードが正となります。これらの文書のいくつかの箇所は、まだ実装されていない作業について記述しています。
新しいドライバーの作成 — ステップバイステップ
ステップ 1 — ソースファイルを作成する
// ファイル:src/include/drivers/Sharp/MyDriver.h
#ifndef MYDRIVER_H
#define MYDRIVER_H
#include "Z80CPU.h"
#include "flash_ram.h" // t_FlashAppConfigHeader 用
// トップレベル init(virtualFuncMap に登録)
uint8_t MyDriver_Init(Z80CPU *cpu,
t_FlashAppConfigHeader *appConfig,
t_drvConfig *config,
const char *ifName);
// ライフサイクルコールバック(init で設定、コア 1 ループが呼び出す)
uint8_t MyDriver_Reset(Z80CPU *cpu);
uint8_t MyDriver_PollCB(Z80CPU *cpu);
uint8_t MyDriver_TaskProcessor(Z80CPU *cpu,
enum Z80CPU_TASK_NAME task,
char *param);
// メモリ / I/O ハンドラー関数
uint8_t MyDriver_MemHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MyDriver_IOHandler(Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
#endif // MYDRIVER_H
ステップ 2 — CMakeLists.txt に追加する
src/CMakeLists.txt を開いて新しいソースファイルを Sharp ドライバーリストに追加します:
# src/CMakeLists.txt — Sharp ドライバーリストにファイルを追加
set(pZ80_drivers_sharp_src
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ700.c
# ... 既存のドライバー ...
# ドライバーをここに追加:
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MyDriver.c
)
ステップ 3 — Z80CPU.c にヘッダーをインクルードする
src/Z80CPU.c を開いて既存の Sharp ドライバーインクルードと並べてドライバーヘッダーのインクルードを追加します:
// src/Z80CPU.c — 他のドライバーインクルードの近く #ifdef INCLUDE_SHARP_DRIVERS #include "drivers/Sharp/MZ700.h" // ... 既存のインクルード ... // インクルードを追加: #include "drivers/Sharp/MyDriver.h" #endif
ステップ 4 — virtualFuncMap に登録する
src/Z80CPU.c で virtualFuncMap[] 配列を見つけてエントリを追加します。文字列 "MyDriver" は JSON の "name" フィールドに含まれる必要があります:
// src/Z80CPU.c
static const t_VirtualFuncMap virtualFuncMap[] = {
#ifdef INCLUDE_SHARP_DRIVERS
{"MZ700", MZ700_Init},
{"MZ80A", MZ80A_Init},
{"MZ2000", MZ2000_Init},
{"MZ2200", MZ2200_Init},
{"MZ80B", MZ80B_Init},
{"MZ2500", MZ2500_Init},
{"MZ800", MZ800_Init},
#endif
#ifdef INCLUDE_AMSTRAD_DRIVERS
{"PCW9512", PCW9512_Init},
#endif
#ifdef INCLUDE_TATUNG_DRIVERS
{"EinsteinTC01", EinsteinTC01_Init},
#endif
// ドライバーを追加:
{"MyDriver", MyDriver_Init},
};
ステップ 5 — config.json にドライバーを追加する
config.json に "drivers" エントリを追加します。"name" フィールドは virtualFuncMap[] に登録した文字列と一致する必要があります:
"drivers": [
{
"enable": 1,
"name": "MZ700",
"type": "VIRTUAL",
"if": []
},
{
"enable": 1,
"name": "MyDriver",
"type": "VIRTUAL",
"if": []
}
]
ステップ 6 — ビルドとテスト
# プロジェクトルートから ./build_tzpuPico.sh DEBUG # ファームウェア出力: # build/bin/model/BaseZ80/BaseZ80_0x10020000.elf (デバッグ ELF — GDB で使用) # build/bin/model/BaseZ80/BaseZ80_0x10020000.bin (OTA バイナリ) # OTA ウェブページ経由でフラッシュするか、直接デバッグする: openocd -f interface/cmsis-dap.cfg -f target/rp2350_tzpu.cfg -c "adapter speed 5000" & cd build/bin/model/BaseZ80 gdb-multiarch BaseZ80_0x10020000.elf (gdb) break MyDriver_Init (gdb) continue
INCLUDE_DBGSH コンパイル定義で制御されます。ICE デバッグシェルを使用してドライバーをデバッグする場合は DBGSH バリアントをフラッシュしてください。
BaseZ80(pZ80-BaseZ80)は全ドライバー(Sharp + Amstrad + Tatung)を含むユニバーサルバイナリで、INCLUDE_SHARP_DRIVERS、INCLUDE_AMSTRAD_DRIVERS、INCLUDE_TATUNG_DRIVERS を定義します。SharpZ80(pZ80-SharpZ80)は Sharp MZ ドライバーのみ、AmstradZ80(pZ80-AmstradZ80)は Amstrad PCW ドライバーのみ、TatungZ80(pZ80-TatungZ80)は Tatung Einstein ドライバーのみを含み、それぞれ小さなファームウェアバイナリを生成します。OpenZ80(pZ80-OpenZ80、INCLUDE_OPEN_DRIVERS)はマシン非依存のインターフェースカードのみを公開する実験者向けペルソナで、build_tzpuPico.sh open でビルドします。ドライバー開発時は、開発中のドライバーのみを含むターゲット別ビルドを使用するとコンパイル時間を短縮でき、テストイテレーションが高速化されます。
ESP32 ファームウェア — ネットワークモード選択
sdkconfig をコピーして希望のネットワークモードを選択します:
# ネットワークモードの選択(ESP32 ファームウェアビルド前): cp sdkconfig.mode_ncm_only sdkconfig # NCM のみ(WiFi なし、FCC/RED 安全) # cp sdkconfig.mode_wifi_only sdkconfig # WiFi のみ # cp sdkconfig.mode_wifi_and_ncm sdkconfig # WiFi と NCM の両方 idf.py build
CONFIG_IF_WIFI_ENABLED— WiFi 無線と AP/Client コードを有効化。CONFIG_IF_USB_NCM_ENABLED— USB NCM ネットワークインターフェースと DHCP サーバーを有効化。
idf.py menuconfig)または上記のプリビルト sdkconfig ファイルで設定されます。
メモリフックパターンの詳細
パターン 1 — 純粋な仮想デバイス(FUNC ブロック)
// 0xC000-0xCFFF をバンク 4 の純粋な仮想デバイスとしてマッピング
int startBlock = 0xC000 / MEMORY_BLOCK_SIZE;
int endBlock = 0xD000 / MEMORY_BLOCK_SIZE;
for(int idx = startBlock; idx < endBlock; idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_FUNC << 24)
| (4 << 16)
| (idx * MEMORY_BLOCK_SIZE);
}
// 範囲内のすべてのアドレスにハンドラーをインストール
for(uint32_t addr = 0xC000; addr < 0xD000; addr++)
cpu->_z80PSRAM->memioPtr[addr] = (MemoryFunc)MyVirtualDevice_Handler;
パターン 2 — RAM 領域への書き込みを横取りする
RAM のままにし、書き込みを後処理する memioPtr ハンドラーをインストールします。
パターン 3 — ROM 領域への書き込みをトラップする
ROM のままにします;書き込みはハンドラーをトリガーしますが PSRAM は変更されません。
パターン 4 — スパースハンドラー(個別アドレス)
パターン 5 — I/O ポートハンドラー
// I/O ポート 0x80-0x8F(16 ポート)にハンドラーをインストール
for(int port = 0x80; port <= 0x8F; port++)
cpu->_z80PSRAM->ioPtr[port] = (MemoryFunc)MyIO_Handler;
サブインターフェースの実例:RS-232C シリアルカード(Z80 SIO)
src/drivers/Sharp/MZ8BIO3.c)と MZ-1E24(src/drivers/Sharp/MZ1E24.c) — どちらも共有の Zilog Z80 SIO/2 エミュレーション(src/drivers/Z80SIO.c / src/include/drivers/Z80SIO.h)上に構築されています。
Z80 SIO エミュレーション(Z80SIO.c)。これはスタンドアロンでレジスタ精度の Zilog Z80 SIO/2 モデルであり、USB と CPU エミュレーションの両方から完全に分離されているため、任意のカードで再利用できます。以下を実装します:
- 2 チャネル — チャネル A はオフセット 0/1(データ/コントロール)、チャネル B はオフセット 2/3。
- 完全なライトレジスタセット WR0–WR7 とリードレジスタセット RR0–RR2。WR0 の 2 バイトポインター/コマンドプロトコルを含みます(レジスタ選択/コマンドバイトを書き込み、続くデータバイトが選択されたレジスタを対象とする)。
- 4 レベルのインサービススタックと厳密なデイジーチェーン優先順位(チャネル A 特殊受信が最高、チャネル B 外部/ステータスが最低まで)を持つ Z80 モード 2 のベクター割り込み、およびステータスがベクターに影響するオプション。
- チャネルごとに 2 つのロックフリーなシングルプロデューサー/シングルコンシューマーリング(各 1 KB):Tx リング(コア 1 プロデューサー → コア 0 コンシューマー)と Rx リング(コア 0 プロデューサー → コア 1 コンシューマー)。リングのプッシュ/ポップヘルパーは USB ポンプ用に公開され、/INT 線用に
volatile bool*が公開されます。
(実シリアル回線ではなく)USB 経由で動作する場合、DCD と CTS はアサート状態に保たれるため、モデム制御ステータスをポーリングするホストファームウェアがキャリアを待ってブロックすることはありません。
共有カード実装と薄いラッパー。MZ8BIO3.c は共有の SIOCard_* 実装(init、reset、poll、task、USB ポンプ)を含みます。MZ1E24.c は SIOCard_Init() に渡されるコネクターモードのみが異なる薄いラッパーです — MZ-8BIO3 は SIOCARD_MODE_BI、MZ-1E24 は SIOCARD_MODE_ST。カードの init 関数は以下を行います:
"port"パラメーター(デフォルト 0xB0)を解析し、4 つの SIO レジスタがきれいなベースに配置されるように 4 ポート境界にマスクします。- Z80 SIO をインスタンス化し、その /INT 線を
cpu->swIntAssert(CPU エミュレーションのソフトウェア割り込みソースフック)に接続します。 - 4 つのポートそれぞれの 256 通りの上位バイトバリアントすべてに I/O ハンドラーをインストールします(Z80 は I/O 中に A8–A15 にレジスタ B を配置するため、すべての上位バイトバリアントが同じハンドラーに解決される必要があります)。
- ソフトウェア割り込みアクノリッジ/RETI フック(
swIntAckVector/swIntReti)を登録し、モード 2 のベクター配信とインサービスクリアが機能するようにします。 - USB ポンプ
SIOCard_usbPump()をグローバルのg_usbSerialPumpフックにインストールします。
物理モードではカードは何もせず 0 を返します — 実ハードウェアカードがホストバス上で応答します。
USB ブリッジ。チャネル A は USB CDC 2(VSER_CHANNEL_A)に、チャネル B は USB CDC 3(VSER_CHANNEL_B)にブリッジされます。これら 2 つの CDC ポートには物理 UART のバッキングがありません。代わりにコア 0 の pollUSBtoUART() が SIOCard_usbPump() を呼び出して、CDC FIFO と SIO チャネルリングの間でバイトをやり取りします(ホスト → Rx リング、Tx リング → ホスト)。カードは標準的な 4 つのサブインターフェースコールバック — SIOCard_Reset、SIOCard_PollCB、SIOCard_TaskProcessor — を公開し、SIO 対応の各ペルソナ(MZ-700、MZ-800、MZ-80B、MZ-1500)の interfaceFuncMap[] に、例えば次のように登録されます:
</div>
// 各 SIO 対応ペルソナの interfaceFuncMap[] 内:
{"MZ-8BIO3", false, MZ8BIO3_Init, SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
{"MZ-1E24", false, MZ1E24_Init, SIOCard_Reset, SIOCard_PollCB, SIOCard_TaskProcessor},
"rom" エントリなし)、I/O ハンドラーとコールバックのみをインストールし、interfaceFuncMap[] にそれらをリストするペルソナによって選択されます。Web 設定側では configgui.js(driverInterfaces マップと interfaceRomLimits テーブル。ここでは ROM 数がゼロとして扱われます)を通じて公開されます。
コア 0 / コア 1 の対話
コア間キューの使用
- ハンドラー(コア 1 上)がファイル I/O または類似の操作が必要なことを検出します(例:Z80 がディスクコマンドレジスタにセクター番号を書き込んだ)。
- ハンドラーはステートフラグを設定し(例:
diskState.pendingRead = true)、直ちに返します — I/O は実行しません。 - ポールハンドラー(コア 1 上でも、約 2048 サイクルごとに呼び出される)がステートフラグを確認し、設定されている場合は
cpu->requestQueueにリクエストメッセージをプッシュします。 - コア 0 がメッセージを受信し、ファイル I/O を実行し(例:SD カードからディスクセクターを読み取る)、結果を
cpu->responseQueueにプッシュします。 task_ptrが(コア 1 上で)タスク結果とともに呼び出されます。セクターデータを PSRAM にコピーしてペンディングフラグをクリアします。
よくある落とし穴
- ハンドラー内でブロッキング。最も一般的な間違いです。ハンドラーまたはポールコールバック内からの
debugf、sleep_ms、fopen、または UART 関数の呼び出しはコア 1 をストールさせ、ホスト Z80 が不正なバスタイミングを見る原因になります。すべてのブロッキング操作をリクエストキュー経由でコア 0 に移してください。USB が利用可能になる前の起動パスログにはdebugf()の代わりにplogf()を使用してください(デバッグログを参照)。 - ハンドラー登録でのループ境界の間違い。ループを使用して範囲にわたってハンドラーをインストールする場合、終了アドレスに
<=ではなく<を使用していることを確認してください — 1 つのずれのエラーは隣接するハンドラーを破壊する可能性があります。 - ドライバーシャットダウンまたはリセット時にハンドラーをクリアし忘れる。リセットハンドラーがドライバーがインストールした
memioPtr[]またはioPtr[]スロットをクリアしない場合、それらのハンドラーはリセット後も呼び出され続けます。 - バンク番号の衝突。各 PSRAM バンクは 64KB。JSON 設定はバンクをメモリ領域に割り当てます。2 つのドライバーが同じバンク番号を使用すると互いのデータを上書きします。各ドライバーに固有のバンク番号を使用してください。
- ハンドラーなしの MEMBANK_TYPE_FUNC。ブロックを FUNC タイプに設定したが
memioPtrハンドラーをインストールしない場合、読み取りは 0x00 を返し書き込みはサイレントにドロップされます。これは有効な動作ですが多くの場合バグです。 - virtualFuncMap 名と JSON 名の不一致。ルックアップは大文字小文字を区別しませんが文字列はそれ以外では完全に一致する必要があります。どちらかの場所のタイポはドライバーがエラーメッセージなしにサイレントにスキップされる原因になります。
- reset_ptr / poll_ptr / task_ptr の設定を忘れる。init 関数でこれらを割り当てない場合、コア 1 はリセット、ポール、またはタスク関数を呼び出しません。ドライバーは正しく初期化されますが RESET に応答したり定期的なハウスキーピングを実行したりしません。
デバッグログ
debugf() — バッファ付きデバッグ出力
debugf() はプライマリデバッグ出力マクロです。printf() のように動作しますが、シリアルポートに直接書き込むのではなく、PSRAM 内の 64KB バッファ(アドレス 0x117EF004)に書き込みます。バッファはメインループがアイドル時間を持つときに USB CDC にフラッシュされます。mutex(debugMutex)が両方のコアからの同時アクセスからバッファを保護します。
// src/include/debug.h
// Primary debug output — mutex-protected, buffered in PSRAM, flushed to USB
debugf("Boot stage %d reached, PSRAM size = %d bytes\n", stage, psramSize);
// Single character / string output (also mutex-protected)
debug_putchar('.');
debug_puts("Init complete\n");
- バッファサイズ:PSRAM 内 64KB(
MAX_DEBUG_BUFFER_SIZE = 65536)。 - スレッドセーフ:マルチコアアクセスのために mutex で保護。
- PSRAM 常駐:バッファはウォッチドッグリセットを越えて保持されます(PSRAM はデータを保持)が、リセット後に
0x117EF004のバッファポインタを再検証する必要があります。 - コア 1 のハンドラーやポールコールバックから呼び出さないでください。mutex 取得がブロックする可能性があり、バスタイミングジッターが発生します。起動パスメッセージには
plogf()を使用するか、コア間キュー経由でデバッグリクエストをコア 0 に送信してください。
plogf() — PSRAM 永続起動ログ
plogf() は 8MB PSRAM の最後の専用 4KB 領域(アドレス 0x117FF000)に書き込みます。debugf() とは異なり、mutex を使用せず、コア 0 の起動パスログ専用です — 通常のデバッグ出力に USB が利用可能になる前の重要な初期起動段階でメッセージをキャプチャします。PSRAM はウォッチドッグリセットを越えて内容を保持するため、これらのメッセージはクラッシュ後も残り、次回の正常起動時に確認できます。
// src/include/debug.h
// PSRAM persistent log structure (at 0x117FF000)
#define PLOG_ADDR 0x117FF000
#define PLOG_SIZE 3840 // 4KB minus 256 bytes reserved for fault diagnostics
#define PLOG_MAGIC 0x504C4F47 // "PLOG"
typedef struct {
uint32_t magic; // PLOG_MAGIC if buffer contains valid data
uint32_t len; // Current write position in buf[]
char buf[PLOG_SIZE - 8]; // Circular text buffer
} t_PsramLog;
// Write to persistent log (no mutex — Core 0 boot path only)
plogf("FSPI init: DMA TX=%d RX=%d\n", gDmaTx, gDmaRx);
// Dump captured log on next successful boot (called from main loop)
dump_plog(); // Outputs plog contents via debugf(), then clears the buffer
plogf() を呼び出します。ウォッチドッグが発火した場合、plog バッファはハングポイントまでのすべてのメッセージを保持します。次回の正常起動時に、dump_plog() がバッファをクリアする前にキャプチャされたメッセージを出力し、クラッシュ前に何が起こったかの明確なトレースを提供します。
適切なデバッグ出力の選択
| マクロ | 場所 | Mutex | WDT リセット後に保持 | コア 1 から安全 | 用途 |
|---|---|---|---|---|---|
debugf() |
PSRAM(64KB バッファ) | あり | バッファ内容はあり。ポインタの再検証が必要 | 不可 — ブロックします | 一般的なコア 0 デバッグ出力 |
plogf() |
PSRAM(末尾 4KB) | なし | あり | 不可 — コア 0 専用 | USB 前の起動パスログ |
| SWD + GDB | ハードウェアプローブ | N/A | N/A | 可(コアごとのポート) | ライブデバッグ、ブレークポイント、検査 |
ウォッチドッグと起動進捗トラッキング
ウォッチドッグタイマー
main() の早い段階で 30 秒のタイムアウトで有効化されます:
// src/model/BaseZ80/main.c watchdog_enable(30000, true); // 30s timeout, pause-on-debug enabled
watchdog_update() は各起動マイルストーンおよびメインループ全体で呼び出されます。クリティカルな長時間実行操作(リトライロジック付きフロッピーディスクイメージロード、DMA 転送、ESP32 SPI ハンドシェイク)には、正当に遅い操作中の誤ったリセットを防ぐための明示的なウォッチドッグキックが含まれています:
// Floppy disk load with retry logic — kick watchdog between attempts
for (int attempt = 0; attempt < 10; attempt++)
{
watchdog_update();
bytesXfer = ESP_readFloppyDiskFile(filename, ..., diskNo);
if (bytesXfer > 0) break;
watchdog_update();
sleep_ms(500);
}
// QD disk change — kick watchdog while waiting for Core 1 hold acknowledge
z80CPU->hold = true;
for (int w = 3000; !z80CPU->holdAck && w > 0; w--)
{
sleep_ms(1);
if ((w % 1000) == 0) watchdog_update();
}
ウォッチドッグスクラッチレジスタ
// src/model/BaseZ80/main.c
// Scratch register allocation
#define BOOTP_SCR_MAGIC 5 // Magic marker: 0xB00710BE
#define BOOTP_SCR_STAGE 6 // Current boot stage code
#define BOOTP_SCR_RSTCAUS 7 // Reset cause from hardware
// scratch[0-3] = boot attempt history (FIFO, most recent in [3])
// scratch[4] = SPI diagnostic counters (packed bitfield)
#define BOOTP_MAGIC 0xB00710BE // "BOOT-PROBE" marker
// Inline function to record current boot stage
static inline void bootStage(uint32_t stage)
{
watchdog_hw->scratch[BOOTP_SCR_STAGE] = stage;
}
// Usage throughout boot sequence:
bootStage(BOOTP_START); // 0x01 — entry point
// ... clock setup ...
bootStage(BOOTP_CLK_SET); // 0x02
// ... PSRAM init ...
bootStage(BOOTP_PSRAM_INIT); // 0x03
watchdog_update();
bootStage(BOOTP_PSRAM_OK); // 0x04
// ... and so on through BOOTP_MAIN_LOOP (0x10)
scratch[0–3])にシフトされます。これにより最後の 4 回のリセット試行が得られ、1 回限りのグリッチと特定のステージでの繰り返し起動失敗を区別することが可能になります。
起動ステージリファレンス
| コード | 定数 | 説明 |
|---|---|---|
0x01 | BOOTP_START | エントリポイント到達 |
0x02 | BOOTP_CLK_SET | システムクロック設定完了(CPU 周波数、PSRAM 周波数、電圧) |
0x03 | BOOTP_PSRAM_INIT | PSRAM 初期化開始 |
0x04 | BOOTP_PSRAM_OK | PSRAM 初期化およびテスト完了 |
0x05 | BOOTP_STDIO_INIT | USB stdio 初期化完了 |
0x06 | BOOTP_PIO_INIT | PIO ステートマシンのロードおよび開始完了 |
0x07 | BOOTP_Z80_INIT | Z80 CPU コンテキスト作成完了 |
0x08 | BOOTP_USB_INIT | USB ブリッジ初期化完了 |
0x0A | BOOTP_ESP_HS_SYNC | ESP32 SPI ハンドシェイク同期 |
0x0B | BOOTP_CORE1_LAUNCH | multicore_launch_core1() 経由でコア 1 起動 |
0x0D | BOOTP_FSPI_INIT | FSPI バイナリ IPC 初期化完了(DMA チャネル確保) |
0x0E | BOOTP_ESP_INIT | ESP32 通信レイヤー準備完了 |
0x10 | BOOTP_MAIN_LOOP | メインループ開始 — 起動完了 |
0x11 | BOOTP_ML_POLL_USB | メインループ:USB ポーリング |
0x12 | BOOTP_ML_INTERCORE | メインループ:コア間コマンド処理 |
0x20 | BOOTP_IC_DEQUEUE | コア間:リクエストのデキュー |
0x21 | BOOTP_IC_FD_LOAD | コア間:フロッピーディスクイメージロード |
0x22 | BOOTP_IC_QD_LOAD | コア間:QuickDisk イメージロード |
0x23 | BOOTP_IC_RF_LOAD | コア間:RAMFILE イメージロード |
0x24–0x27 | BOOTP_IC_FILE_* | コア間:ファイルロード/書き込み/応答/完了 |
scratch[5] == 0xB00710BE の場合、レジスタには有効な起動進捗データが含まれています。ステージコードは scratch[6] を読み取ります。例えば、scratch[6] == 0x0A(BOOTP_ESP_HS_SYNC)の場合、ファームウェアは ESP32 SPI ハンドシェイク中にハングしました — ESP32 がフラッシュされ実行中であることを確認し、SPI 配線を検証してください。
ICE デバッグシェル(dbgsh.c)
dbgsh.c / dbgsh.h)は USB CDC チャネル 1 で 49 コマンドの ICE デバッガーを実装しています。コア 0 で動作し、t_Z80CPU コンテキスト構造体の共有フラグを介してコア 1 と通信します:
cpu->hold/cpu->holdAck— コア 0 シェルとコア 1 エミュレーションループ間の一時停止/再開ハンドシェイク。cpu->dbgBpAddr[DBG_MAX_BP]— ブレークポイントアドレス配列(8 スロット、0xFFFF = 未使用)。コア 1 は各オペコードフェッチ前にチェック。cpu->dbgStepCount— シングルステップカウンター。コア 1 は各命令後にデクリメントし、ゼロで自動ホールド。cpu->dbgTrace[DBG_TRACE_SZ]— 512 エントリのリングバッファ、各実行命令の[31:16]=PC, [15:8]=opcode, [7:0]=Fを記録。
dbg_sprintf()(軽量 RAM 常駐 printf)を使用して、コア 1 が PSRAM をアクティブに使用している間のコア 0 からの出力時に XIP フラッシュストールを回避します。物理メモリおよび I/O アクセスは Z80CPU_readPhysicalMem() / Z80CPU_writePhysicalMem() / Z80CPU_readPhysicalIO() / Z80CPU_writePhysicalIO() を介して行われ、PIO ステートマシンを通じて実 Z80 バスサイクルを駆動します。
フォールトハンドラーと PSRAM 診断
PSRAM フォールト診断構造体
0x117FFF00)はフォールト診断用に予約されています。フォールトが発生すると、ハンドラーは完全なレジスタスナップショットを保存します:
// src/fault_handlers.c
#define PSRAM_DIAG_ADDR 0x117FFF00 // Last 256 bytes of 8MB PSRAM
#define PSRAM_DIAG_MAGIC 0xFA017000 // "FAULT" marker
typedef struct {
uint32_t magic; // PSRAM_DIAG_MAGIC if valid
uint32_t faultType; // 1=Hard, 2=MemManage, 3=BusFault, 4=UsageFault
uint32_t pc; // Program counter at fault
uint32_t lr; // Link register (return address)
uint32_t sp; // Stack pointer
uint32_t r0, r1, r2, r3, r12; // General-purpose registers
uint32_t psr; // Program Status Register
uint32_t cfsr; // Configurable Fault Status Register
uint32_t hfsr; // Hard Fault Status Register
uint32_t bfar; // Bus Fault Address Register
uint32_t mmfar; // Memory Management Fault Address Register
uint32_t coreId; // Which core faulted (0 or 1)
} t_PsramFaultDiag;
フォールトハンドラーの実装
- 適切なマジックマーカーとフォールトタイプを使用して、診断構造体を PSRAM の
0x117FFF00に書き込みます。 - USB が利用可能な場合、
debugf()経由でレジスタダンプとフォールト詳細を出力します。 - 無限ループ(
while(1))に入り、ウォッチドッグがリセットをトリガーできるようにします。
0x117FFF00 の PSRAM_DIAG_MAGIC マーカーをチェックします。存在する場合、保存されたフォールト情報を debugf() 経由でダンプしマーカーをクリアして、完全な事後トレースを提供します。
// Interpreting fault diagnostics (from GDB or from debugf output): // // faultType=1 (Hard Fault): // Check HFSR bit 30 (FORCED) — indicates escalated fault. // Check CFSR for the original fault type. // // faultType=3 (Bus Fault): // Check CFSR bits [15:8] for bus fault status. // If BFARVALID (bit 15), BFAR contains the faulting address. // Common cause: PSRAM SPI contention between Core 0 and Core 1. // // faultType=4 (Usage Fault): // Check CFSR bits [25:16] for usage fault status. // UNDEFINSTR = undefined instruction (corrupted code in Flash/PSRAM). // DIVBYZERO = division by zero (if enabled). // // PC value: the instruction that faulted. // LR value: the return address (caller of the faulting function).
PSRAM 診断メモリマップ
| アドレス範囲 | サイズ | 内容 |
|---|---|---|
0x117EF004 – 0x117FEFFF |
64KB | debugf() 出力バッファ(0x117EF004 の揮発性ポインタ) |
0x117FF000 – 0x117FFEFF |
~4KB | plogf() 永続起動ログ |
0x117FFF00 – 0x117FFFFF |
256B | フォールト診断スナップショット |
バイナリ IPC プロトコル (FSPI v1.1)
sdkconfig.mode_wifi_only、sdkconfig.mode_wifi_and_ncm、sdkconfig.mode_ncm_only)。NCM モードでは、ESP32 は内蔵 DHCP サーバー(デフォルト IP: 192.168.7.1)を備えた USB CDC-NCM Ethernet アダプターを提供し、WiFi ハードウェアなしで Web インターフェースにアクセスできます。NCM のみモードは、FCC/RED 認証なしで出荷されるボードに必須です。
フレーム構造
// src/include/ipc_protocol.h
typedef struct __attribute__((packed)) {
uint8_t frameType; // 0: IPCF_TYPE_COMMAND / RESPONSE / NOP
uint8_t command; // 1: IPCF_CMD_* opcode
uint8_t status; // 2: IPCF_STATUS_* (response only)
uint8_t seqNum; // 3: Sequence number (retry detection)
uint16_t payloadLen; // 4: Payload bytes (little-endian)
uint16_t sectorCount; // 6: Sectors in burst (1–16)
uint32_t fileOffset; // 8: Byte offset in file
uint8_t diskNo; // 12: Drive number
uint8_t flags; // 13: IPCF_FLAG_*
uint16_t reserved; // 14: Reserved
char filename[48]; // 16: Null-terminated path (48 bytes)
} t_IpcFrameHdr; // Total: 64 bytes
// Frame layout:
// [64-byte header][0–8192 bytes payload][4-byte CRC32]
#define IPCF_HEADER_SIZE 64
#define IPCF_MAX_SECTORS 16 // Max sectors per burst
#define IPCF_SECTOR_SIZE 512 // Bytes per sector
#define IPCF_CRC_SIZE 4 // CRC32 trailer
#define IPCF_MAX_PAYLOAD (IPCF_MAX_SECTORS * IPCF_SECTOR_SIZE) // 8192
#define IPCF_MAX_FRAME_SIZE (IPCF_HEADER_SIZE + IPCF_MAX_PAYLOAD + IPCF_CRC_SIZE) // 8260
コマンドオペコード
| オペコード | 名前 | 説明 |
|---|---|---|
0x00 |
IPCF_CMD_NOP |
No-operation(全二重読み取り中のダミー TX) |
0x01 |
IPCF_CMD_RDS |
単一 512 バイトセクタ読み取り |
0x02 |
IPCF_CMD_WRS |
単一 512 バイトセクタ書き込み |
0x03 |
IPCF_CMD_RBURST |
バースト読み取り:1 回の SPI トランザクションで 1–16 セクタ |
0x04 |
IPCF_CMD_WBURST |
バースト書き込み:1–16 セクタ |
0x05 |
IPCF_CMD_RFILE |
ファイル全体読み取り(最大ペイロード超過時はチャンク分割) |
0x06 |
IPCF_CMD_WFILE |
ファイル全体書き込み |
0x07 |
IPCF_CMD_INF |
RP2350 バージョン/パーティション情報を ESP32 に転送 |
0x08 |
IPCF_CMD_RFD |
フロッピーディスクイメージファイル読み取り |
0x09 |
IPCF_CMD_RQD |
QuickDisk イメージファイル読み取り |
0x0A |
IPCF_CMD_RRF |
RAMFILE バックアップイメージ読み取り |
DMA と整合性
- 事前確保された DMA チャネル:TX および RX DMA チャネル(
gDmaTx、gDmaRx)は FSPI 初期化時に一度確保され、解放されません。これにより転送ごとの確保/解放オーバーヘッドが排除され、コア 1 が QMI バスを争奪しているときに発生する DMA チャネル枯渇レースが防止されます。 - RX 優先度の引き上げ:RX DMA チャネルは SPI RX FIFO オーバーフローを防ぐために HIGH PRIORITY に設定されます。コア 1 の QMI バス経由の PSRAM アクセスが AHB バスファブリックをストールさせる可能性があり、RX DMA チャネルが通常の優先度の場合、転送が SPI FIFO がオーバーフローするほど長く遅延する可能性があります。
- CRC32 整合性:すべてのフレームは標準 IEEE 802.3 CRC32(多項式
0xEDB88320、リフレクテッド)で保護されます。ESP32 は同じ結果を生成するesp_rom_crc32_le()を使用します。CRC 不一致の場合、フレームはシーケンスカウンター(seqNum)を使用した重複検出でリトライされます。 - ウォッチドッグ統合:DMA 待機には毎秒ウォッチドッグキックを伴う 2 秒のタイムアウトが含まれます。DMA 転送がハングした場合(例:ESP32 リセットによる)、チャネルはアボートされ、CS は解放され、起動が続行されます。
ツール
tools/NetFileServer/netfs.py は、Windows または Linux PC 上で動作する Python ベースのネットワークファイルサーバーです。Sharp MZ BASIC の NETx: デバイスに MZF ファイルを提供します。Celestite ドライバーの Phase 2 ネットワーキング実装(ESP32 ブリッジ経由の実際の TCP/IP)と組み合わせて使用し、MZ 上の BASIC プログラムが PC のファイルシステムからファイルのロード、セーブ、一覧表示を行えます。デフォルトポート 6800、ユニット 1-7(NET1: から NET7:)対応、各ユニットを PC 上のディレクトリにマッピング。
参考サイト
| リソース | リンク |
|---|---|
| picoZ80 プロジェクトページ | /picoz80/ |
| picoZ80 ユーザーマニュアル | /picoz80-usermanual/ |
| picoZ80 テクニカルガイド | /picoz80-technicalguide/ |
| pico6502 プロジェクトページ | /pico6502/ |
| RP2350 データシート | datasheets.raspberrypi.com |
| Pico SDK マルチコア API | raspberrypi.github.io/pico-sdk-doxygen |
| Zeta Z80 ライブラリ | github.com/superzazu/z80 |
| Zilog Z80 CPU ユーザーマニュアル | zilog.com |
| cJSON ライブラリ | github.com/DaveGamble/cJSON |
無線規制に関する注意事項
- 組み立てられたデバイスは、完成品が独自にテストされ、該当する管轄区域で機器認可(例:FCC ID、認定機関による CE マーキング評価)を取得しない限り、第三者への販売、販売の申し出、贈与、またはその他の方法での配布を行ってはなりません。
- 個人使用のために少数を製作することは、趣味愛好家および実験使用の規定(例:FCC § 15.23)に基づき、デバイスが有害な干渉を引き起こさない限り、一般的に許可されています。
- 規制要件は国によって異なります。米国外の製作者は、適用される規則について自国の無線周波数当局に確認してください。
本設計に基づいて製作されたデバイスが、管轄区域内の適用されるすべての無線周波数規制に準拠することは、製作者の単独の責任です。著者は本設計を個人使用、教育、および趣味愛好家向けに提供しており、本設計から製作されたデバイスが商業的配布の規制要件を満たすことについて、いかなる表明も行いません。