picoZ80 デベロッパーズガイド

picoZ80 デベロッパーズガイド

このガイドは picoZ80 のファームウェア内部構造を理解して独自のペリフェラルドライバーを作成したいデベロッパー向けの包括的なリファレンスです。コア 1 のバスディスパッチループからドライバー登録、メモリフックのインストール、I/O 仮想化まで、ソフトウェアアーキテクチャ全体をカバーします。Sharp MZ-700 ドライバー(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 ハードウェア(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 です。
C コードはコンパイル時に各サイクルタイプの命令シーケンスをプリエンコードします。これらは 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);
ステートマシン間の連携:アドレス SM(z80_addr)とデータ SM(z80_data)はそれぞれ独自の IRQ フラグ(IRQ 0 と IRQ 1)を使用してプロデューサー/コンシューマーハンドシェイクを実装します:
  • SM が IRQ フラグをセットし wait 0 irq N でストール —「準備完了、データを送ってください」。
  • コア 1 がアドレスまたはデータを SM の TX FIFO にプッシュし、IRQ フラグをクリア。
  • SM がウェイクアップし、FIFO からデータを取得してピンを駆動。
このハンドシェイクにより、正しい値がロードされる前にバス信号が駆動されることがなく、また SM がまだ消費していない FIFO エントリーをコア 1 が上書きすることもありません。
データ SM(z80_data)のリードサイクルフロー:
  1. IRQ 1 をセットして待機 —「方向/データの準備完了」。
  2. コア 1 がピン方向(入力モード)とダミーデータバイトをプッシュした後、IRQ 1 をクリア。
  3. SM がピン方向を入力(トライステート)に設定し、ホストメモリが D0–D7 を駆動可能に。
  4. SM は wait 0 irq 0 で次のアドレス変更(サイクル終了を示す)まで待機。
  5. SM がピン方向を既知の状態に戻す。
ライトサイクルでは、コア 1 が出力ピン方向と実際のデータバイトをプッシュし、SM が D0–D7 をデータで駆動します。

主要な型とデータ構造

ドライバーフレームワークを見る前に、src/include/Z80CPU.h で定義されているコアデータ構造を理解することが不可欠です。これらの構造体はすべてのドライバー関数に渡され、ドライバーがメモリと I/O システムと対話する主要な手段です。

メモリブロックタイプ定数

Z80 の 64KB アドレス空間のすべての 512 バイトブロックは、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 アドレス  (バンク内のブロックのベースアドレス)
ブロックのタイプとバンクを設定するには、これら 3 つの値をビット OR で 1 つの 32 ビット整数にパックします:
// ブロック 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 サイクル同期を制御する 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;
waitStates:PSRAM バックメモリ領域が応答するためにより多くの時間が必要な場合(ハンドラー関数が追加作業を行うためなど)、ウェイトステートを追加します。各ウェイトステートはバスサイクルをホストクロックの 1 T サイクル延長します。
tCycSynctrue に設定すると、PIO の z80_sync ステートマシンが PSRAM アクセスを現在のバスサイクルの T1 立ち上がりエッジまで遅延させます。カセット I/O、シリアルビットバンギング、遅延ループなど精密なクロックサイクルタイミングに依存するホストソフトウェアのタイミングドリフトを防ぎます。

PSRAM 構造体(t_Z80PSRAM)

8MB の外部 PSRAM は単一の 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;
4 つのサブ配列には異なる役割があります:
  • 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 に書き戻されます。
重要:ハンドラー関数はコア 1 のホットループから直接呼び出されます。割り込みが無効の状態でコア 1 上で実行されます。短く、確定的でなければならず、決してブロック、スリープ、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;  // 非同期リセットフラグ
};

ドライバー設定構造体

ドライバーが初期化されると、JSON 設定パーサーによって設定された 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_ptrpoll_ptrtask_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 ディスパッチループ

コア 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() にコールバックします。

メモリ読み取りディスパッチ

Z80 エミュレーターがメモリ読み取りを実行すると、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;
}

ドライバーフレームワーク

ドライバーフレームワークは C ドライバーモジュールが発見され、JSON 設定からインスタンス化され、メモリと I/O システムに接続されるメカニズムです。2 つのレベルがあります:
  • トップレベルドライバーペルソナとも呼ばれる) — Z80CPU.cvirtualFuncMap[] に登録。各ペルソナはメモリレイアウト、バンキング、I/O ポート、オプションのサブインターフェース(インターフェースカード)セットを含むマシンペルソナ全体を設定します。
  • インターフェースドライバー — ペルソナ独自の interfaceFuncMap[] に登録。各インターフェースドライバーはペルソナに特定のペリフェラル(フロッピー、QuickDisk、RAM 拡張、ファイリングシステム)を追加します。
各ペルソナは独自の interfaceFuncMap[] を持ち、異なるサブセットのインターフェースを含みます。完全な互換性マトリックスについてはテクニカルガイドの「ペルソナ–インターフェース互換性」テーブルを、JSON 設定例についてはユーザーマニュアルを参照してください。

virtualFuncMap — トップレベルドライバー登録

src/Z80CPU.cvirtualFuncMap[] 配列は文字列名をドライバーの 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 をベースにする

OpenZ80 モデル(src/drivers/Other/Open.cbuild_tzpuPico.sh open でビルド)は、2 種類の実験者の出発点です。picoZ80 を自作ボードに搭載する人と、専用ペルソナがまだない Z80 マシンを立ち上げる人です。Open.cMZ700.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 ポートへ移動できます。
1 — 最も近いドライバーから始める
ターゲットに最も近いペルソナドライバーをコピーし、そのメモリマップ、I/O ハンドラー、ROM レイアウトを編集します。ペルソナドライバーは次のとおりです。
  • src/drivers/Other/Open.c — バニラペルソナ(まったく新しいボードに最適なベース)。
  • src/drivers/Sharp/MZ700.cMZ80A.cMZ80K.cMZ80B.cMZ800.cMZ1500.cMZ2000.cMZ2200.cMZ2500.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(...) でモデルターゲットを追加します。
2 — マシンの ROM を再利用またはパッチする
ドライバーが読み込むモニター、IPL、CP/M BIOS、フロッピーブート ROM は、関連する RFSTZFS プロジェクトの 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 をアセンブルし、生成されたバイナリを SD カードに配置して、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)
Z80 は I/O ポート 0x60 にモード値を書き込むことで tranZPUter メモリレイアウトを選択し、ドライバーはそれに応じてバンクポインターを再設定します。モードは TZMM_ORIGTZMM_BOOTTZMM_TZFSTZMM_TZFS2TZMM_TZFS3TZMM_TZFS4TZMM_CPMTZMM_CPM2TZMM_COMPAT です。CP/M の CPM2 レイアウトはバイト単位のブロック 0 ページングを使用します(0x00000x003F → ベクターブロック、0x00400x01FF → TPA の先頭)。
仮想 K64F サービスプロセッサー(OUT 0x68 → Core 0)
TZFS は、ファイリングシステム呼び出しを Z80 コアで処理する代わりに、仮想サービスプロセッサーを使用します。Z80 は 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 バイトセクターは ESP32ESP_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 から)。

初期化フロー

RP2350 が 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" 配列を適用(ドライバーのデフォルトを上書き)
順序に注意:ドライバーは最初に初期化され、その後 JSON の "memory""io" 配列が適用されます。つまり config.jsonmemory または io 配列の明示的なエントリはドライバーがそれらのアドレスに設定したものを上書きします。これにより、ドライバーのソースコードを変更せずにドライバーのデフォルトをユーザーが微調整できます。

ドライバーライフサイクルコールバック

ドライバーは init 中に 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 ドライバー

Sharp 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 ドライバー(デュアルモードペルソナ)

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 v1.8.3 以降、picoZ80 は MZ-80A / MZ-700 / MZ-800 のフロッピーディスク(MZ-800 CP/M を含む)を TZFS 内から直接ブートでき、フロッピーブートした OS の実行後にハードウェア RESET スイッチが押されると 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_A10ToggleFDC ハードウェアアクセラレーションのフェッチハンドラーが 0xF3FE / 0xF7FE で DRQ と A10 を OR することで、TZFS DSKREAD は 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 に復帰します。
関連する 3 つの低位 RAM / リセット修正が同じパスを支えています:ネイティブプログラムが低位 RAM にコードを LDIR でコピーした後にビデオモードを切り替えても、両方のパスで同じメモリを見るように、TZFS 下で 0x00000x0FFF をバンク 7 からマッピングすること(一般的な Flappy PSRAM バンク不一致の修正);/RESET が解除された時点で initVideoText を一度再実行し、GDG が再スキャンしてモニターの HBLK 同期が完了するようにすること(ハードウェアリセット GDG 回復);そして、オンボードの GDG/8255 がハードウェアリセットを完了してからファームウェアがビデオを再初期化するように、10ms の物理リセット安定化遅延を設けること。
MZ-700 は Z80A をベースとした 1982 年の Sharp 8 ビットコンピューターです。そのメモリマップには学習用の理想的な例となる特徴的な機能があります:
  • 下位 4KB(0x0000–0x0FFF)は電源投入時にはモニター ROM ですが、I/O ポートへの書き込みで RAM に入れ替えることができます — いわゆる「MZ-700 バンク切り替え」。
  • 上位領域(0xD000–0xFFFF)にはビデオ RAM、カラー VRAM、メモリマッピングハードウェアレジスタが含まれます。上位領域全体も I/O ポートで RAM に入れ替えることができます。
  • メモリバンキングは 0xE0–0xE6 の 6 つの I/O ポートを介して制御されます。

バンキングハンドラー — MZ700_IO_MemoryBankPorts()

これは MZ-700 バンキング制御ポートすべてを管理する I/O ハンドラーです。Z80 がポート 0xE0–0xE6 に書き込むたびに 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;
}
このハンドラーはドライバー作成において最も重要なパターンを示しています:I/O 書き込みへの応答でリアルタイムに _membankPtr[] を変更してメモリバンキングを実装すること。変更は即座に有効になります — Z80 からの次のメモリアクセスは新しいマッピングを使用します。
上位メモリ領域の保存/復元パターン(MZ700Ctrl.upmembankPtr[])は重要です:ハードウェアマッピング領域を RAM に入れ替えるとき、ソフトウェアが元に戻したときに復元できるように何があったかを記憶する必要があります。単純に PHYSICAL を再度割り当てると、サブドライバーまたは JSON 設定によって設定されたカスタムマッピングが失われます。

実例:仮想 CMT(カセット)ドライバー

仮想 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 ボーの変種あり)。
両ファミリーは 1 つのコーデックを共有します:1 バイトは長いスタートビットに続く MSB ファーストの 8 データビット;1 ブロックはリーダー + テープマーク + バイト列 + 16 ビットのチェックサム(1 ビットの個数);1 ファイルはヘッダー(インフォ)レコードとそれに続くデータレコードで、それぞれ 2 回書き込まれます。

ドライバーエンジンと登録

エンジンは 4 つの標準ライフサイクルコールバック — 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 カセットブロックの再タイプ化

Simple ファミリーでは、カセットはメモリマッピング 8255 領域の内部に存在します。ドライバーは 8255 ブロック — E000–E1FF の 512 バイト、ブロックインデックス 112 — をその物理タイプから 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 ホットパスはすべての非カセットアクセスに対してそのまま残されます — 再タイプ化は、必要な箇所にだけドライバーにフックをもたらします。
MZ-800 では、これと同等のフックはメモリブロックではなく I/O ポート D0–D3 に配置され、MZ-800 のポートマッピングカセットを反映します。

Controlled ファミリー — E0–E3 の ioPtr[] フック

Controlled ファミリー(MZ-80B / MZ-2000 / MZ-2200 / MZ-2500)はトランスポートを I/O ポート経由で駆動するため、CMT はポート E0–E3 に ioPtr[] ハンドラーをインストールします(I/O ポートフックを参照)。これらはトランスポートコマンド — PLAY/STOP/FF/REW/EJECT — と APSS プログラムサーチをデコードします。仮想テープはプログラムの順序付きキュー(後述)であるため、FF/REW はプログラム単位でスキップし、APSS は次/前のプログラム境界までシークしてそこで自動停止し、実機デッキの動作を再現します。

テープキュー(設定パラメータ)

仮想テープは、最大 16 個の MZF ファイルからなる GUI で設定される順序付きキューであり、インターフェースのパラメータ配列(t_ifParam、JSON の param[] リスト — ドライバー設定構造体を参照)としてドライバーに供給されます:各エントリは {enable, file} のペアであり、ファミリーはペルソナから推論されます。キューは新しい esp32/webserver/js/cmt.js パネルを介して Web GUI からライブで編集できます(追加 / 削除 / 並べ替え)。各プログラムの終わりでキューは自動的に前進します — 連続したテープの動作 — ため、次の LOAD はユーザーの介入なしに次のプログラムを見つけます。

コア間 SD I/O と録音

ハンドラーとポールコールバックはコア 1 で実行され、SD カードに直接触れてはなりません。すべての読み取り(ストリーミングする次の MZF の取得)と書き込み(録音の保存)は、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 ヘッダーの内部にそのまま保存されるため、プログラムは再読み込み時に元の名前を保持します。

実 ↔ 仮想の実行時トグル(リバースコマンドキュー)

Web GUI の Actions メニュー内の「CMT: Virtual / Real」項目(CMT インターフェースが有効な場合のみ表示)は、再起動なしでライブのドライバーフラグを切り替えます。GUI アクションは ESP32 → RP2350 のリバースコマンドキューCP_sendCmd)— コントロールプロセッサーリンクの ESP32 から RP2350 への方向 — を通り、ドライバーのモードフラグを設定します:
  • VIRTUAL は読み取り時に仮想カセットビットを重ね、物理的に接続されたデッキへのモーター/トランスポートの書き込みを抑制します。
  • REAL は純粋なパススルーです:CMT は読み取りデータラインを読み取り専用でスヌープし、モーターは実機のデッキを駆動します。
フラグはライブで切り替えられるため、仮想テープから読み込んでから物理デッキに SAVE したり、その逆を行ったりできます。完全なプロトコル、波形テーブル、コーデックの詳細は、picoZ80 リポジトリの docs/VIRTUAL_CMT_DESIGN.md に記載されています。

仮想ペリフェラルデバイス — 内部構造

仮想ペリフェラルデバイスとは、ペルソナにコンパイルして組み込むのではなく、設定からインスタンス化されるチップモデルです。フレームワークは src/devices.csrc/include/devices.h にあり、チップモデル自体は src/drivers/ 配下の通常のドライバーです。ユーザーは device 配列でインスタンスを宣言し、interlink 配列でそれらを配線します — どちらもテクニカルガイドに記載されています。
ここで作業する前に、3 層の区別を理解しておいてください。IC モデルZ80DMA.cZ80CTC.c)は純粋なチップエミュレーションであり、どのマシンに載っているかについては何の前提も持ちません。デバイスは、自分でシステムを構築する人のために、そうしたモデルを設定から宣言してインスタンス化したものです。インターフェースカードは既存の drivers[].if[] の仕組みで、ROM の切り出し、メディア処理、実 / 仮想の切り替えといったマシン固有のグルーコードを伴います。これらは互いに階層関係にあるのではなく、同じ IC モデルを共有する対等な存在です。IC モデルで行った精度向上の作業は、それを使うデバイスとカードの双方に恩恵をもたらします。

デバイスの登録

すべてのデバイスタイプは src/devices.cdeviceFuncMap[] に 1 行を持ちます。この行はチップ名を指定し、Web インターフェースが表示するタイトルと説明を与え、どのコアがサービスするかを宣言し、モデルのエントリーポイントとシグナル宣言テーブルを指し示します。デバイス名は大文字小文字を区別せずに照合されます。
各デバイスは自身のピンを t_devSignalDecl テーブルでも宣言します — ピン名、平易な言葉によるラベル、方向、負論理かどうか、アイドルレベル、オープンドレインかどうかです。このテーブルがピンに関する唯一の真実の情報源です。ファームウェアはこれを使ってシグナルテーブルを構築し、tools/gen_devicecat.py はビルド時にこれを読み取って、Web インターフェースが参照するカタログを生成します。

シグナルとネットリストの評価

ネットが運ぶのはピンの電気的レベルであり、抽象的なアサート状態ではありません。極性はデバイスに属します — DMA は自身のレディ極性の意味を決め、割り込み出力は自分が負論理であることを知っています。これによって、2 つのタイマー出力の NOR がネットリスト上でも回路図とまったく同じように振る舞います。
評価はプッシュ方式です。出力が変化すると、それを消費するノードが再評価され、その接続先が駆動されます。これをネットリストが収束するか反復回数の上限に達するまで繰り返します。上限があるのは、相互結合したゲートの対がラッチできるようにするためで、真の発振は一度だけ報告され、レベルはクランプされたまま残されます。ネットリストは設定の最後に一度収束させられるため、各入力は宣言されたアイドル値ではなく、その式が要求するレベルから開始します。
デバイスは Signal_drive() で出力を駆動し、双方向ピンについては Signal_setDir() で方向を設定します。入力側のレベル変化は、シグナルコールバックを通じてデバイスに届けられます。Z80 PIO は各ポートビットを独立して扱っており、新しい双方向デバイスを作るときに手本とすべき正しいモデルです。

コアの分担

フリーランで動くデバイスの処理はコア 0 で実行され、コア 1 のホットパスには一切負荷をかけません。コア 1 にはプロセッサーと同期していなければならないものだけ — レジスターの読み書きハンドラーと、バスを駆動して Z80 を保持する DMA 転送エンジン — を残します。カウンター / タイマーとインターバルタイマーはコア 0 のタイマーによって進められます。これがカタログでコア 0 デバイスと記されている理由です。
重要:コア 0 のティックは BaseZ80 モデルでのみ起動されます。Sharp、Amstrad、Tatung、OpenZ80 の各ビルドでは、カウンター系デバイスはアドレス指定できてもカウントは決して進みません。カウントを行うデバイスを追加する場合は、テスト対象のモデルでティックが動作していることを確認してください。さもないと、タイマーが常に同じ値を返すという症状になります。
カウンター / タイマーの時間基準は、エミュレートされた T ステート数ではなく、設定されたホストクロックでスケーリングした実時間です。これは意図的なもので、ハードウェアの挙動と一致します — 実際のカウンターのクロック入力は水晶発振子なので、DMA バースト中もウェイトステート中も HALT 中もカウントし続けます。

割り込みデイジーチェーン

優先度は設定値ではなく IEI / IEO の配線から決まるため、ネットリストがマシンの唯一の記述となります。各デバイスタイプは、自身のどのピンが IEIIEO であるかを deviceFuncMap[] の行で宣言し、あわせてチェーンが呼び出す 3 つのエントリーポイントも宣言します — デバイスが要求中かサービス中かを報告するもの、アクノリッジしてベクターを返すもの、そして RETI を処理するものです。
チェーンの順序はネットリスト自体から導出されます — あるデバイスの IEI を別のデバイスの IEO から駆動するノードがあれば、その別のデバイスが前段になります。IEO が High に駆動されるのは IEI が High で、かつデバイスが割り込み要求中でもサービス中でもない場合だけであり、これが優先度ルールのすべてです。あとはインターリンクがその抑止をチェーンの下流へ伝えます。
外部ハードウェアが先に解決されます。ホストバス上の実チップは、ファームウェアからは見えない IEI / IEO ピンを持っています — 割り当てられる予備のプロセッサーピンがないためです — したがって調停することは決してできません。そこでデバイスフレームワークは、既にインストールされている割り込みアクノリッジおよび RETI ハンドラーがあればそれにチェーンし、自分のデバイスを検討する前にアクノリッジをそちらへ提示します。これがなければ、独自のハンドラーをインストールする MZ-8BIO3 および MZ-1E24 カードは、デバイスを 1 つ追加しただけで黙って壊れていたことでしょう。
ホスト側の適合性テストは、このチェーンが依存する取り決めを網羅しています。アクノリッジがプログラムされたベクターを返し、ペンディングフラグをクリアしてサービス中を設定すること — これが下位優先度のデバイスを抑止します — そして RETI がサービス中を再びクリアすることです。

デバッグシェルのツール群

DBGSH ファームウェアバリアントは、デバイスを扱うための 5 つのコマンドを提供します。実装は 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 アドレスに割り当てるので、テストプログラムからネットリストに刺激を与え、その応答を読み戻すことができます。

新しいデバイスモデルの追加

手順は次のとおりです:
  1. チップモデルを src/drivers/ 配下の通常のドライバーとして、特定のマシンに依存しない形で記述します。レジスターは他のエミュレーターではなくメーカーのデータシートからモデル化してください — 既存のエミュレーターは、想像以上に高い頻度で公表された動作から乖離しています。
  2. ピンを t_devSignalDecl テーブルで宣言します。ラベルはそのチップを知らない読者に向けて書いてください。これらは Web インターフェースが表示する文言になります。
  3. deviceFuncMap[] に行を追加し、モデルの init、read、write、signal、reset の各エントリーポイントを指定して、サービスコアを選びます。
  4. パッケージングのデフォルト値 — アドレス空間、デコードサイズ、推奨ベースアドレス — を tools/gen_devicecat.pyDEFAULTS テーブルに追加します。これらは C のテーブルには存在しないためです。
  5. tools/gen_devicecat.py を実行して esp32/webserver/js/devicecat.js を再生成し、再生成したファイルをコミットします。このカタログは古くなり得るビルド成果物です — 再生成せずにデバイスやピンを追加しても、Web インターフェースには現れません。
  6. チップに自明でない動作がある場合は、test/ 配下にホスト側の適合性テストを追加します。DMA モデルには、gcc で偽のアドレス空間に対してドライバーをビルドし、データシート自身の表と突き合わせて検証するテストがあります。

現在の未完了事項

既知の未完了領域
  • デバイスはプロセッサーのリセットではリセットされません。リセット関数は存在しますが呼び出されることがなく、デバイスは設定が適用されるときに一度だけ初期化されます。
  • オープンドレイン配線は自動化されていません。openDrain フラグはピンごとに宣言されていますが評価時に参照されないため、ワイヤード AND の割り込みノードは明示的な AND 演算子で記述する必要があります。
  • 8255 の入力パスはビットを破壊します。入力ピンのレベルをポートに畳み込む際にまずポートの出力レジスターを読み出しますが、入力として設定されたポートではこれがゼロを返すため、入力ビットを 1 本駆動すると他のビットがクリアされてしまいます。修正する際は Z80 PIO のビット単位の入力パスが手本になります。
  • 再設定時にインスタンスカウンターが完全にはリセットされません。設定を再適用したときにクリアされるのは DMA プールだけなので、再設定を繰り返すとインスタンススロットがリークします。
  • mirrorrefreshtrace の各キーは解析されて保存されますが、決して使用されません。
picoZ80 リポジトリの設計文書 — docs/VIRTUAL_Z80DMA_DESIGN.mddocs/VIRTUAL_Z80CTC_Z80PIO_DESIGN.mddocs/VIRTUAL_DEVICE_GUI_DESIGN.md — には、完全な導出過程、データシートの参照先、各決定の根拠が記載されています。設計文書とコードが食い違う場合はコードが正となります。これらの文書のいくつかの箇所は、まだ実装されていない作業について記述しています。

新しいドライバーの作成 — ステップバイステップ

このセクションでは、ゼロから完全なドライバーを作成するために必要なすべてのステップを説明します。例では単純な RAM ディスク(Z80 が I/O マッピングメモリとしてアクセスする PSRAM の 64KB ブロック)を作成して、実際のハードウェアエミュレーションの複雑さなしにすべてのパターンを示します。

ステップ 1 — ソースファイルを作成する

2 つのファイルを作成します。ヘッダーファイルは他のモジュールが呼び出す関数を宣言し、C ファイルはそれらを実装します。
// ファイル: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.cvirtualFuncMap[] 配列を見つけてエントリを追加します。文字列 "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 にドライバーを追加する

SD カードの 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
ビルドシステムは 4 つのファームウェアバリアントを生成します:標準(デバッグシェルなし)と DBGSH(ICE デバッガー付き)× 2 パーティション。DBGSH バリアントは INCLUDE_DBGSH コンパイル定義で制御されます。ICE デバッグシェルを使用してドライバーをデバッグする場合は DBGSH バリアントをフラッシュしてください。
BaseZ80pZ80-BaseZ80)は全ドライバー(Sharp + Amstrad + Tatung)を含むユニバーサルバイナリで、INCLUDE_SHARP_DRIVERSINCLUDE_AMSTRAD_DRIVERSINCLUDE_TATUNG_DRIVERS を定義します。SharpZ80pZ80-SharpZ80)は Sharp MZ ドライバーのみ、AmstradZ80pZ80-AmstradZ80)は Amstrad PCW ドライバーのみ、TatungZ80pZ80-TatungZ80)は Tatung Einstein ドライバーのみを含み、それぞれ小さなファームウェアバイナリを生成します。OpenZ80pZ80-OpenZ80INCLUDE_OPEN_DRIVERS)はマシン非依存のインターフェースカードのみを公開する実験者向けペルソナで、build_tzpuPico.sh open でビルドします。ドライバー開発時は、開発中のドライバーのみを含むターゲット別ビルドを使用するとコンパイル時間を短縮でき、テストイテレーションが高速化されます。

ESP32 ファームウェア — ネットワークモード選択

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 サーバーを有効化。
これらは Kconfig(idf.py menuconfig)または上記のプリビルト sdkconfig ファイルで設定されます。

メモリフックパターンの詳細

このセクションでは完全な例を使用してすべてのフックパターンを詳しく説明します。これらはすべてのドライバーメモリ管理の構成要素です。

パターン 1 — 純粋な仮想デバイス(FUNC ブロック)

Z80 アドレス空間の領域を PSRAM バッキングなしでハンドラーが完全に制御したい場合に使用します。Z80 の読み書きは常に関数を呼び出します。PSRAM には何も保存されません。
// 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 として動作させたい(読み取りは PSRAM データを返し、書き込みは PSRAM を更新する)が、書き込みの通知も受けたい場合に使用します(例:ビデオ RAM の書き込みをシャドウバッファにミラーリングするため)。ブロックタイプを RAM のままにし、書き込みを後処理する memioPtr ハンドラーをインストールします。

パターン 3 — ROM 領域への書き込みをトラップする

一部のハードウェアは ROM マッピングアドレスへの書き込みをバンキングレジスタへの書き込みとして使用します(書き込みはハードウェアが「デコード」しますが ROM は変更されません)。ブロックを ROM のままにします;書き込みはハンドラーをトリガーしますが PSRAM は変更されません。

パターン 4 — スパースハンドラー(個別アドレス)

ブロック全体にハンドラーをインストールする必要はありません。RAM または ROM ブロック内の特定の 1 つのアドレスにハンドラーをインストールできます。ブロックタイプはそのブロック内の他のすべてのアドレスに何が起こるかを制御し、特定のアドレスハンドラーはその 1 つのアドレスのみを上書きします。

パターン 5 — I/O ポートハンドラー

I/O ハンドラーはシンプルです — I/O ポートには PSRAM バッキングがありません。ハンドラーはインストールされていれば呼び出されるか、または 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)

RS-232C シリアルカードは、上記のサブインターフェースパターンの完全な実例であり、再利用可能なデバイスエミュレーションモジュールを所有してそれを USB にブリッジするサブインターフェースの実例でもあります。2 つのペルソナサブインターフェースが提供されます — MZ-8BIO3src/drivers/Sharp/MZ8BIO3.c)と MZ-1E24src/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.cSIOCard_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_ResetSIOCard_PollCBSIOCard_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 を持たず("rom" エントリなし)、I/O ハンドラーとコールバックのみをインストールし、interfaceFuncMap[] にそれらをリストするペルソナによって選択されます。Web 設定側では configgui.jsdriverInterfaces マップと interfaceRomLimits テーブル。ここでは ROM 数がゼロとして扱われます)を通じて公開されます。

コア 0 / コア 1 の対話

ドライバーハンドラーはコア 1 のホットループ内で実行されます。数マイクロ秒以上かかる操作(ファイル I/O、UART コマンド、malloc)はコア間キューを使用してコア 0 にオフロードしなければなりません。

コア間キューの使用

パターンは以下の通りです:
  1. ハンドラー(コア 1 上)がファイル I/O または類似の操作が必要なことを検出します(例:Z80 がディスクコマンドレジスタにセクター番号を書き込んだ)。
  2. ハンドラーはステートフラグを設定し(例:diskState.pendingRead = true)、直ちに返します — I/O は実行しません
  3. ポールハンドラー(コア 1 上でも、約 2048 サイクルごとに呼び出される)がステートフラグを確認し、設定されている場合は cpu->requestQueue にリクエストメッセージをプッシュします。
  4. コア 0 がメッセージを受信し、ファイル I/O を実行し(例:SD カードからディスクセクターを読み取る)、結果を cpu->responseQueue にプッシュします。
  5. task_ptr が(コア 1 上で)タスク結果とともに呼び出されます。セクターデータを PSRAM にコピーしてペンディングフラグをクリアします。

よくある落とし穴

  • ハンドラー内でブロッキング。最も一般的な間違いです。ハンドラーまたはポールコールバック内からの debugfsleep_msfopen、または 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 に応答したり定期的なハウスキーピングを実行したりしません。

デバッグログ

picoZ80 ファームウェアのデバッグは困難です。これはマルチコア・マルチプロセッサシステムであり、RP2350 は異なるリアルタイムおよび非リアルタイムの責務を持つ 2 つの Cortex-M33 コアを実行し、ESP32 コプロセッサがすべてのネットワークとストレージ I/O を処理します。従来の printf スタイルのデバッグは簡単ではありません — 初期起動時に USB が利用できない場合があり、コア 1 のホットループはブロッキング呼び出しを許容できず、ウォッチドッグリセットは揮発性の状態を破壊します。ファームウェアはこれらの制約に対処するために 3 つの補完的なデバッグ出力メカニズムを提供しています。

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
典型的な使用パターン:起動中、各重要なマイルストーン(PSRAM 初期化、SPI ハンドシェイク、設定解析)で plogf() を呼び出します。ウォッチドッグが発火した場合、plog バッファはハングポイントまでのすべてのメッセージを保持します。次回の正常起動時に、dump_plog() がバッファをクリアする前にキャプチャされたメッセージを出力し、クラッシュ前に何が起こったかの明確なトレースを提供します。

適切なデバッグ出力の選択

マクロ 場所 Mutex WDT リセット後に保持 コア 1 から安全 用途
debugf() PSRAM(64KB バッファ) あり バッファ内容はあり。ポインタの再検証が必要 不可 — ブロックします 一般的なコア 0 デバッグ出力
plogf() PSRAM(末尾 4KB) なし あり 不可 — コア 0 専用 USB 前の起動パスログ
SWD + GDB ハードウェアプローブ N/A N/A 可(コアごとのポート) ライブデバッグ、ブレークポイント、検査

ウォッチドッグと起動進捗トラッキング

picoZ80 は厳しい環境で動作します:デュアルコア RP2350 が SPI 経由で ESP32 と通信し、PIO 経由でサイクル精度の Z80 バスインターフェースを処理しながら、8MB の外部 PSRAM を管理します。起動中の任意の時点でのハング — PSRAM 初期化、SPI ハンドシェイク、設定解析、コア 1 起動 — はボードを診断出力なしに無応答にします。ハードウェアウォッチドッグタイマーと起動進捗トラッキングシステムは、このような障害を回復可能かつ診断可能にするために設計されました。

ウォッチドッグタイマー

RP2350 ハードウェアウォッチドッグは 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();
}

ウォッチドッグスクラッチレジスタ

RP2350 はウォッチドッグハードウェアブロック内に 8 つの 32 ビットスクラッチレジスタを提供しており、ウォッチドッグリセットを越えて保持されますが、電源投入リセットではクリアされます。picoZ80 ファームウェアはこれらのうち 5 つを使用して完全な起動診断履歴を維持します:
// 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)
各ウォッチドッグリセット時に、現在のステージとリセット原因は上書きされる前に履歴 FIFO(scratch[0–3])にシフトされます。これにより最後の 4 回のリセット試行が得られ、1 回限りのグリッチと特定のステージでの繰り返し起動失敗を区別することが可能になります。

起動ステージリファレンス

コード 定数 説明
0x01BOOTP_STARTエントリポイント到達
0x02BOOTP_CLK_SETシステムクロック設定完了(CPU 周波数、PSRAM 周波数、電圧)
0x03BOOTP_PSRAM_INITPSRAM 初期化開始
0x04BOOTP_PSRAM_OKPSRAM 初期化およびテスト完了
0x05BOOTP_STDIO_INITUSB stdio 初期化完了
0x06BOOTP_PIO_INITPIO ステートマシンのロードおよび開始完了
0x07BOOTP_Z80_INITZ80 CPU コンテキスト作成完了
0x08BOOTP_USB_INITUSB ブリッジ初期化完了
0x0ABOOTP_ESP_HS_SYNCESP32 SPI ハンドシェイク同期
0x0BBOOTP_CORE1_LAUNCHmulticore_launch_core1() 経由でコア 1 起動
0x0DBOOTP_FSPI_INITFSPI バイナリ IPC 初期化完了(DMA チャネル確保)
0x0EBOOTP_ESP_INITESP32 通信レイヤー準備完了
0x10BOOTP_MAIN_LOOPメインループ開始 — 起動完了
0x11BOOTP_ML_POLL_USBメインループ:USB ポーリング
0x12BOOTP_ML_INTERCOREメインループ:コア間コマンド処理
0x20BOOTP_IC_DEQUEUEコア間:リクエストのデキュー
0x21BOOTP_IC_FD_LOADコア間:フロッピーディスクイメージロード
0x22BOOTP_IC_QD_LOADコア間:QuickDisk イメージロード
0x23BOOTP_IC_RF_LOADコア間:RAMFILE イメージロード
0x24–0x27BOOTP_IC_FILE_*コア間:ファイルロード/書き込み/応答/完了
ウォッチドッグリセットのデバッグ:SWD プローブを接続し、RP2350 を停止してスクラッチレジスタを読み取ります。scratch[5] == 0xB00710BE の場合、レジスタには有効な起動進捗データが含まれています。ステージコードは scratch[6] を読み取ります。例えば、scratch[6] == 0x0ABOOTP_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 診断

ファームウェアは Cortex-M33 フォールトハンドラーをインストールし、ウォッチドッグがシステムをリセットする前に完全な診断スナップショットを PSRAM にキャプチャします。これにより、ライブデバッガセッションを必要とせずに事後分析機能を提供します — マルチコアリアルタイムシステムでの間欠的な障害を診断するために不可欠です。

PSRAM フォールト診断構造体

8MB PSRAM の最後の 256 バイト(アドレス 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;

フォールトハンドラーの実装

各フォールトタイプ(ハードフォールト、メモリ管理フォールト、バスフォールト、使用フォールト)には、フォールト時にどちらがアクティブだったかに応じてメインスタックポインタ(MSP)またはプロセススタックポインタ(PSP)を抽出するアセンブリラッパーがあり、共通の C ハンドラーに渡します。C ハンドラーは:
  1. 適切なマジックマーカーとフォールトタイプを使用して、診断構造体を PSRAM の 0x117FFF00 に書き込みます。
  2. USB が利用可能な場合、debugf() 経由でレジスタダンプとフォールト詳細を出力します。
  3. 無限ループ(while(1))に入り、ウォッチドッグがリセットをトリガーできるようにします。
次回の正常起動時に、ファームウェアは 0x117FFF00PSRAM_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 診断メモリマップ

8MB PSRAM の上位は 3 つの診断領域に分割されています:
アドレス範囲 サイズ 内容
0x117EF004 – 0x117FEFFF 64KB debugf() 出力バッファ(0x117EF004 の揮発性ポインタ)
0x117FF000 – 0x117FFEFF ~4KB plogf() 永続起動ログ
0x117FFF00 – 0x117FFFFF 256B フォールト診断スナップショット
3 つの領域すべてがウォッチドッグリセットを越えて保持されます。PSRAM は電源が維持されている限り内容を保持するためです。電源投入リセット時には内容は未定義であり、ファームウェアはマジックマーカーをチェックして再初期化します。

バイナリ IPC プロトコル (FSPI v1.1)

RP2350 は 50MHz 4 線式 SPI リンクを介してバイナリ IPC プロトコル(バージョン 1.1)で ESP32 と通信します。これは以前のテキストベースプロトコルを、CRC32 整合性チェック、バーストセクタ転送、およびレイテンシ削減と信頼性向上のための事前確保された DMA チャネルをサポートする構造化バイナリフレームフォーマットに置き換えるものです。
ESP32 ファームウェアは、ビルド時に選択可能な 3 つのネットワークモードをサポートします:WiFi のみ、WiFi+NCM(両方同時)、NCM のみ。各モード用のプリビルト sdkconfig ファイルが提供されています(sdkconfig.mode_wifi_onlysdkconfig.mode_wifi_and_ncmsdkconfig.mode_ncm_only)。NCM モードでは、ESP32 は内蔵 DHCP サーバー(デフォルト IP: 192.168.7.1)を備えた USB CDC-NCM Ethernet アダプターを提供し、WiFi ハードウェアなしで Web インターフェースにアクセスできます。NCM のみモードは、FCC/RED 認証なしで出荷されるボードに必須です。

フレーム構造

すべての IPC トランザクションは、固定 64 バイトヘッダーとオプションのペイロードおよび 4 バイト CRC32 トレーラーで構成されます:
// 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 チャネル(gDmaTxgDmaRx)は 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

無線規制に関する注意事項

本デバイスは 2.4 GHz ISM バンドで送信する ESP32-S3-PICO-1 無線モジュールを搭載しており、世界各国の無線周波数規制(米国の FCC Part 15 Subpart C、欧州連合の無線機器指令 2014/53/EU を含む)において意図的放射器に該当します。
ESP32-S3-PICO-1 モジュール自体は既存の規制認証(FCC、CE など)を取得していますが、そのモジュールレベルの認証は、モジュールを組み込んだ完成品に自動的に適用されるものではありません。事前認証モジュールの免除規定は、個人の趣味愛好家個人使用、実験、または教育目的で少数のデバイスを製作する場合に、個別の機器認可を取得せずに行うことを許可するものです。
重要な制限事項
  • 組み立てられたデバイスは、完成品が独自にテストされ、該当する管轄区域で機器認可(例:FCC ID、認定機関による CE マーキング評価)を取得しない限り、第三者への販売、販売の申し出、贈与、またはその他の方法での配布を行ってはなりません
  • 個人使用のために少数を製作することは、趣味愛好家および実験使用の規定(例:FCC § 15.23)に基づき、デバイスが有害な干渉を引き起こさない限り、一般的に許可されています。
  • 規制要件は国によって異なります。米国外の製作者は、適用される規則について自国の無線周波数当局に確認してください。
製作者の責任
本設計に基づいて製作されたデバイスが、管轄区域内の適用されるすべての無線周波数規制に準拠することは、製作者の単独の責任です。著者は本設計を個人使用、教育、および趣味愛好家向けに提供しており、本設計から製作されたデバイスが商業的配布の規制要件を満たすことについて、いかなる表明も行いません。