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/ │ │ └── Sharp/ │ │ ├── MZ.h MZ シリーズ共通定数 │ │ ├── MZ700.h MZ-700 ドライバーヘッダー │ │ ├── MZ80A.h MZ-80A ドライバーヘッダー │ │ ├── MZ2000.h MZ-2000 ドライバーヘッダー │ │ ├── RFS.h / TZFS.h ファイリングシステムヘッダー │ │ ├── WD1773.h フロッピーコントローラーヘッダー │ │ ├── MZ8BFI.h MZ-8BFI ドライバーヘッダー │ │ └── QDDrive.h QuickDisk ヘッダー │ ├── drivers/ │ │ └── Sharp/ │ │ ├── MZ700.c *** サンプルドライバー *** │ │ ├── MZ80A.c MZ-80A ペルソナドライバー │ │ ├── MZ2000.c MZ-2000 ペルソナドライバー │ │ ├── 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 ペリフェラルインターフェースカード │ │ ├── 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/メモリ複合ボード │ └── model/ │ ├── BaseZ80/ │ │ ├── CMakeLists.txt モデルごとのビルドターゲット │ │ ├── main.c エントリポイント(コア 0 + コア 1 起動) │ │ ├── main_memmap_partition_1.ld スロット 1 用リンカースクリプト │ │ └── main_memmap_partition_2.ld スロット 2 用リンカースクリプト │ └── 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 からデータを取得してピンを駆動。
z80_data)のリードサイクルフロー:
- IRQ 1 をセットして待機 —「方向/データの準備完了」。
- コア 1 がピン方向(入力モード)とダミーデータバイトをプッシュした後、IRQ 1 をクリア。
- SM がピン方向を入力(トライステート)に設定し、ホストメモリが D0–D7 を駆動可能に。
- SM は
wait 0 irq 0で次のアドレス変更(サイクル終了を示す)まで待機。 - SM がピン方向を既知の状態に戻す。
主要な型とデータ構造
ドライバーフレームワークを見る前に、
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 サイクル延長します。
tCycSync:
true に設定すると、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 に書き戻されます。
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_ptr、poll_ptr、task_ptr フィールドは JSON から設定されるものではありません — コア 1 が適切なタイミングでドライバーのハウスキーピング関数を呼び出せるよう、ドライバーの init 関数によって設定されます。
コア 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.cのvirtualFuncMap[]に登録。各ペルソナはメモリレイアウト、バンキング、I/O ポート、オプションのサブインターフェース(インターフェースカード)セットを含むマシンペルソナ全体を設定します。 - インターフェースドライバー — ペルソナ独自の
interfaceFuncMap[]に登録。各インターフェースドライバーはペルソナに特定のペリフェラル(フロッピー、QuickDisk、RAM 拡張、ファイリングシステム)を追加します。
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 ペルソナ
{"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
};
初期化フロー
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.json の memory または 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 オーバーレイを実演するリファレンス実装です。
再利用可能なペリフェラルエミュレーションモジュールが 2 つ追加されています:PIT8253.c(Intel 8253 プログラマブルインターバルタイマー — 全 6 カウンターモード、BCD/バイナリカウント、カウンターラッチ、LSB/MSB 読み書きモード)および PPI8255.c(Intel 8255 プログラマブルペリフェラルインターフェース — モード 0 I/O、ポート C のビットセット/リセット、ポートごとの出力コールバックと入力インジェクション)。これらのモジュールは任意のマシンペルソナドライバーからインスタンス化できるように設計されています。
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 設定によって設定されたカスタムマッピングが失われます。
新しいドライバーの作成 — ステップバイステップ
このセクションでは、ゼロから完全なドライバーを作成するために必要なすべてのステップを説明します。例では単純な 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.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},
// ドライバーを追加:
{"MyDriver", MyDriver_Init},
#endif
};
ステップ 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 バリアントをフラッシュしてください。
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 サーバーを有効化。
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;
コア 0 / コア 1 の対話
ドライバーハンドラーはコア 1 のホットループ内で実行されます。数マイクロ秒以上かかる操作(ファイル I/O、UART コマンド、malloc)はコア間キューを使用してコア 0 にオフロードしなければなりません。
コア間キューの使用
パターンは以下の通りです:
- ハンドラー(コア 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 に応答したり定期的なハウスキーピングを実行したりしません。
デバッグログ
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 回限りのグリッチと特定のステージでの繰り返し起動失敗を区別することが可能になります。
起動ステージリファレンス
| コード | 定数 | 説明 |
|---|---|---|
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_* | コア間:ファイルロード/書き込み/応答/完了 |
ウォッチドッグリセットのデバッグ:SWD プローブを接続し、RP2350 を停止してスクラッチレジスタを読み取ります。
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 で 42 コマンドの 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 ハンドラーは:
- 適切なマジックマーカーとフォールトタイプを使用して、診断構造体を 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 診断メモリマップ
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_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 認証なしで出荷されるボードに必須です。
フレーム構造
すべての 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 チャネル(
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 は解放され、起動が続行されます。
参考サイト
| リソース | リンク |
|---|---|
| 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)に基づき、デバイスが有害な干渉を引き起こさない限り、一般的に許可されています。
- 規制要件は国によって異なります。米国外の製作者は、適用される規則について自国の無線周波数当局に確認してください。
本設計に基づいて製作されたデバイスが、管轄区域内の適用されるすべての無線周波数規制に準拠することは、製作者の単独の責任です。著者は本設計を個人使用、教育、および趣味愛好家向けに提供しており、本設計から製作されたデバイスが商業的配布の規制要件を満たすことについて、いかなる表明も行いません。