Skip to content

Instantly share code, notes, and snippets.

@GOROman
Created May 8, 2025 01:03
Show Gist options
  • Save GOROman/1db0e02ac3eb0dfe98c7247c82f1e86b to your computer and use it in GitHub Desktop.
Save GOROman/1db0e02ac3eb0dfe98c7247c82f1e86b to your computer and use it in GitHub Desktop.
秋月電子で280円(Z80円)で売ってた保守用スーパーAKI-80の足りない部品(ROM、リセットIC、クロック)をESP32でどうにかする

注意点:

  • このプロジェクトは、CPUのバサイクルタイミングに非常に敏感であり、高度な知識とデバッグ能力を要します。ESP32の処理速度が追いつかない場合、eZ80のクロック周波数を非常に低く設定する必要があります。
  • eZ80の詳細なバスタイミングについては、Zilog社のeZ80F0808MODデータシートおよびeZ80 CPUユーザーマニュアルをご参照ください。
  • 配線が非常に多くなるため、慎重に行ってください。

1. 必要なもの

  • ESP32開発ボード (ESP-WROOM-32搭載のものなど)
  • Z80マイコンCPUカード スーパーAKI-80
  • ブレッドボード
  • ジャンパーワイヤー
  • LED x1
  • 抵抗 x1 (LED用、220Ω~1kΩ程度)

2. 回路設計と結線

2.1. eZ80基板のブートモード設定

eZ80を外部メモリからブートするように設定します。基板上のジャンパ JP1 と JP2 を以下のように設定してください。

  • JP1 (BM0): 1-2番ピンをショート (BM0 = H)
  • JP2 (BM1): 2-3番ピンをショート (BM1 = L)
    • これにより、ブートモードは「External Memory Boot, nCS0 active for addresses 000000h - 00FFFFh (64KB)」となります。今回は256バイトのROMをエミュレートしますが、この設定で問題ありません。

2.2. ESP32とeZ80の接続

ESP32のGPIOとeZ80基板のコネクタ(CN1, CN2, CN3)のピンを接続します。ここではROMサイズを256バイトと仮定し、アドレスバスはA0-A7のみ使用します。

ESP32 GPIO (例) eZ80ピン (コネクタ-ピン番号) 信号名 役割
GPIO12 CN1-34 EXTAL eZ80クロック入力 (ESP32から供給)
GPIO13 CN1-36 nRST eZ80リセット制御 (ESP32から制御)
GPIO14 CN2-12 nWAIT eZ80ウェイト制御 (ESP32が出力)
アドレスバス
GPIO27 CN2-32 A0 アドレスバス bit 0
GPIO26 CN2-31 A1 アドレスバス bit 1
GPIO25 CN2-30 A2 アドレスバス bit 2
GPIO33 CN2-29 A3 アドレスバス bit 3
GPIO32 CN2-28 A4 アドレスバス bit 4
GPIO15 CN2-27 A5 アドレスバス bit 5
GPIO2 CN2-26 A6 アドレスバス bit 6
GPIO4 CN2-25 A7 アドレスバス bit 7
データバス
GPIO16 CN3-22 D0 データバス bit 0 (双方向)
GPIO17 CN3-21 D1 データバス bit 1 (双方向)
GPIO5 CN3-20 D2 データバス bit 2 (双方向)
GPIO18 CN3-19 D3 データバス bit 3 (双方向)
GPIO19 CN3-18 D4 データバス bit 4 (双方向)
GPIO21 CN3-17 D5 データバス bit 5 (双方向)
GPIO22 CN3-16 D6 データバス bit 6 (双方向)
GPIO23 CN3-15 D7 データバス bit 7 (双方向)
制御信号
GPIO34 (入力専用) CN3-12 nMREQ メモリリクエスト (eZ80から入力)
GPIO35 (入力専用) CN3-13 nRD リード信号 (eZ80から入力)
GPIO39 (入力専用) CN3-11 nIORQ I/Oリクエスト (eZ80から入力)
GPIO36 (入力専用) CN3-14 nWR ライト信号 (eZ80から入力)
GPIO0 CN3-10 nCS0 チップセレクト0 (eZ80から入力)
その他
ESP32 GND eZ80基板 GND (例: CN2-1) GND グランド共通化
ESP32 3V3 eZ80基板 VCC3 (例: CN2-2) VCC 3.3V電源 (共通供給も可、要確認)

注意:

  • ESP32のGPIOピン番号は一般的な開発ボードを想定しています。お使いのボードに合わせて変更してください。
  • GPIO0 はESP32のブートモードにも関わるため、接続には注意が必要です。プルアップ/プルダウン抵抗の状態によっては起動に影響する場合があります。
  • eZ80基板への電源供給は、ESP32から行うか、別途安定した3.3V電源を用意してください。ESP32から供給する場合は、ESP32の3.3V出力の電流容量に注意してください。

2.3. LEDの接続

LEDはESP32のGPIOに接続し、ESP32がeZ80のI/O出力をエミュレートして制御します。

  • ESP32 GPIO20 (例) --- 抵抗 (220Ω) --- LEDアノード --- LEDカソード --- ESP32 GND

2.4. 結線図(概念図)

Fritzingのようなツールでの完全な図示は複雑になるため、主要な接続を示す概念図をイメージしてください。 すべてのGNDは一点で接続します。

+---------+                               +-----------------+
| ESP32   |                               | eZ80 CPU Card   |
|         |                               | (111324)        |
| GPIO12  | -- CLK (EXTAL) ------------- | CN1-34          |
| GPIO13  | -- nRST -------------------- | CN1-36          |
| GPIO14  | -- nWAIT ------------------- | CN2-12          |
|         |                               |                 |
| GPIO27  | -- A0 ---------------------- | CN2-32          |
|  ...    |    ... (A1-A7)               | ...             |
| GPIO4   | -- A7 ---------------------- | CN2-25          |
|         |                               |                 |
| GPIO16  | -- D0 <--------------------> | CN3-22          |
|  ...    |    ... (D1-D7)               | ...             |
| GPIO23  | -- D7 <--------------------> | CN3-15          |
|         |                               |                 |
| GPIO34  | -- nMREQ <------------------- | CN3-12          |
| GPIO35  | -- nRD <--------------------- | CN3-13          |
| GPIO39  | -- nIORQ <------------------- | CN3-11          |
| GPIO36  | -- nWR <--------------------- | CN3-14          |
| GPIO0   | -- nCS0 <-------------------- | CN3-10          |
|         |                               |                 |
| GND     | -- GND ---------------------- | CN2-1 (GND)     |
| 3V3     | -- VCC ---------------------- | CN2-2 (VCC3)    |
+---------+                               +-----------------+

ESP32 GPIO20 --- R --- LED --- GND (ESP32)

3. Z80 (eZ80) プログラム

I/Oポート0番に 0xFF0x00 を交互に出力し、LEDを点滅させるプログラムです。

アセンブリコード (z80_led_blink.asm):

ORG 0000H

START:
  LD A, 0FFH     ; LED ON (0xFF を出力)
  OUT (00H), A   ; ポート0に出力
  CALL DELAY
  LD A, 00H      ; LED OFF (0x00 を出力)
  OUT (00H), A   ; ポート0に出力
  CALL DELAY
  JP START

DELAY:           ; 簡単なウェイトループ
  LD BC, 01000H  ; ディレイカウント (調整してください)
DELAY_LOOP:
  DEC BC
  LD A, B
  OR C
  JP NZ, DELAY_LOOP
  RET

END START

マシンコード (C言語配列形式): 上記アセンブリコードをアセンブル(例: pasmo z80_led_blink.asm z80_led_blink.bin)し、バイナリを16進数配列に変換します。 例 (アドレスは相対):

//マシンコードの例 (実際のアセンブル結果を使用してください)
// LD A, 0FFh  -> 3E FF
// OUT (00h),A -> D3 00
// CALL DELAY  -> CD xx yy (DELAYのアドレス)
// LD A, 00h   -> 3E 00
// OUT (00h),A -> D3 00
// CALL DELAY  -> CD xx yy
// JP START    -> C3 00 00 (STARTのアドレス = 0000h)
// DELAY:
// LD BC, 01000h -> 01 00 10 (BCに1000h)
// DEC BC        -> 0B
// LD A, B       -> 78
// OR C          -> B1
// JP NZ, DELAY_LOOP -> C2 yy xx (DELAY_LOOPのアドレス)
// RET           -> C9

// 256バイトのROMイメージ (先頭部分のみ)
const uint8_t rom_image[256] = {
    0x3E, 0xFF, // LD A, 0FFH
    0xD3, 0x00, // OUT (00H), A
    0xCD, 0x0A, 0x00, // CALL 000AH (DELAY)
    0x3E, 0x00, // LD A, 00H
    0xD3, 0x00, // OUT (00H), A
    0xCD, 0x0A, 0x00, // CALL 000AH (DELAY)
    0xC3, 0x00, 0x00, // JP 0000H (START)
    // DELAY routine at 000AH
    0x01, 0x00, 0x10, // LD BC, 1000H (0x01, LSB, MSB for Z80)
    0x0B,             // DEC BC
    0x78,             // LD A, B
    0xB1,             // OR C
    0xC2, 0x0D, 0x00, // JP NZ, 000DH (DELAY_LOOP)
    0xC9,             // RET
    // 残りはNOP (0x00) などで埋める
    0x00, 0x00, /* ... up to 256 bytes ... */
};

重要: 上記のマシンコードは手動で一部作成したものです。正確なアセンブラで生成し、アドレスを正しく解決したバイナリを使用してください。CALLJP のアドレスは、プログラムの配置によって変わります。

4. ESP32ファームウェア (PlatformIO - Arduino Framework)

4.1. platformio.ini

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =

4.2. src/main.cpp

#include <Arduino.h>

// eZ80 Pin definitions (ESP32 GPIO numbers)
// クロック・リセット・ウェイト
const int EZ80_CLK_PIN = 12;
const int EZ80_NRST_PIN = 13;
const int EZ80_NWAIT_PIN = 14;

// アドレスバス (A0-A7)
const int EZ80_A_PINS[] = {27, 26, 25, 33, 32, 15, 2, 4}; // A0, A1, ..., A7

// データバス (D0-D7)
const int EZ80_D_PINS[] = {16, 17, 5, 18, 19, 21, 22, 23}; // D0, D1, ..., D7

// 制御信号
const int EZ80_NMREQ_PIN = 34; // Input only
const int EZ80_NRD_PIN = 35;   // Input only
const int EZ80_NIORQ_PIN = 39; // Input only
const int EZ80_NWR_PIN = 36;   // Input only
const int EZ80_NCS0_PIN = 0;   // External interrupt capable

// LED (ESP32側)
const int LED_PIN = 20; // Example GPIO for LED

// eZ80 クロック設定
const int CLK_PWM_CHANNEL = 0;
const double EZ80_CLK_FREQ = 100000; // 100kHz (最初は低速でテスト)

// Z80プログラム (マシンコード)
// 上記で作成したマシンコード配列をここに記述
const uint8_t rom_image[256] = {
    0x3E, 0xFF,       // LD A, 0FFH
    0xD3, 0x00,       // OUT (00H), A
    0xCD, 0x0A, 0x00, // CALL 000AH (DELAY)
    0x3E, 0x00,       // LD A, 00H
    0xD3, 0x00,       // OUT (00H), A
    0xCD, 0x0A, 0x00, // CALL 000AH (DELAY)
    0xC3, 0x00, 0x00, // JP 0000H (START)
    // DELAY routine at 000AH
    0x01, 0x00, 0x10, // LD BC, 1000H (BC = 0x1000)
    0x0B,             // DEC BC
    0x78,             // LD A, B
    0xB1,             // OR C
    0xC2, 0x0D, 0x00, // JP NZ, 000DH (DELAY_LOOP)
    0xC9,             // RET
    // Fill rest with NOPs (0x00) or halt (0x76) if needed
    // For this example, the rest can be 0x00.
    // Make sure the array has 256 elements.
    // This example is short, pad with 0x00 until 256 bytes.
    // Example padding:
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 20 bytes
    // ... continue padding until rom_image[255]
};


// --- Helper Functions ---
void setup_pins_input(const int pins[], int count) {
    for (int i = 0; i < count; ++i) {
        pinMode(pins[i], INPUT);
    }
}

void setup_pins_output(const int pins[], int count) {
    for (int i = 0; i < count; ++i) {
        pinMode(pins[i], OUTPUT);
    }
}

uint8_t read_address_bus() {
    uint8_t address = 0;
    for (int i = 0; i < 8; ++i) { // A0-A7
        if (digitalRead(EZ80_A_PINS[i])) {
            address |= (1 << i);
        }
    }
    return address;
}

uint8_t read_data_bus() {
    setup_pins_input(EZ80_D_PINS, 8);
    uint8_t data = 0;
    for (int i = 0; i < 8; ++i) {
        if (digitalRead(EZ80_D_PINS[i])) {
            data |= (1 << i);
        }
    }
    return data;
}

void write_data_bus(uint8_t data) {
    setup_pins_output(EZ80_D_PINS, 8);
    for (int i = 0; i < 8; ++i) {
        digitalWrite(EZ80_D_PINS[i], (data >> i) & 0x01);
    }
}

void ez80_reset() {
    digitalWrite(EZ80_NRST_PIN, LOW);
    delayMicroseconds(100); // リセットパルス幅
    digitalWrite(EZ80_NRST_PIN, HIGH);
    delay(10); // リセット後の安定待ち
}

// --- Interrupt Service Routines (or polling functions) ---
// For simplicity, this example will use polling in the main loop.
// Real-time applications would benefit from interrupts,
// but careful synchronization with nWAIT is key.

void setup() {
    Serial.begin(115200);
    Serial.println("eZ80 Emulator (ROM/IO/CLK/RST) starting...");

    // LED pin
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);

    // eZ80 Control Pins
    pinMode(EZ80_NRST_PIN, OUTPUT);
    digitalWrite(EZ80_NRST_PIN, HIGH); // Start with reset de-asserted
    pinMode(EZ80_NWAIT_PIN, OUTPUT);
    digitalWrite(EZ80_NWAIT_PIN, HIGH); // nWAIT is active LOW

    // eZ80 Bus Pins
    setup_pins_input(EZ80_A_PINS, 8);  // Address bus is input to ESP32
    setup_pins_input(EZ80_D_PINS, 8);  // Data bus initially input

    // eZ80 Control Signal Inputs
    pinMode(EZ80_NMREQ_PIN, INPUT);
    pinMode(EZ80_NRD_PIN, INPUT);
    pinMode(EZ80_NIORQ_PIN, INPUT);
    pinMode(EZ80_NWR_PIN, INPUT);
    pinMode(EZ80_NCS0_PIN, INPUT);

    // eZ80 Clock setup
    ledcSetup(CLK_PWM_CHANNEL, EZ80_CLK_FREQ, 8); // Channel, Freq, Resolution (8-bit for 50% duty)
    ledcAttachPin(EZ80_CLK_PIN, CLK_PWM_CHANNEL);
    ledcWrite(CLK_PWM_CHANNEL, 128); // 50% duty cycle (128/255 for 8-bit resolution)

    Serial.println("Performing eZ80 reset...");
    ez80_reset();
    Serial.println("eZ80 reset complete. Starting bus monitoring.");
}

void loop() {
    // Monitor eZ80 bus signals (polling method)
    // This needs to be VERY fast or eZ80 clock must be VERY slow.
    // nWAIT is crucial.

    bool ncs0_active = (digitalRead(EZ80_NCS0_PIN) == LOW);
    bool nmreq_active = (digitalRead(EZ80_NMREQ_PIN) == LOW);
    bool nioreq_active = (digitalRead(EZ80_NIORQ_PIN) == LOW);

    if (ncs0_active && nmreq_active) { // Memory cycle on CS0 range
        digitalWrite(EZ80_NWAIT_PIN, LOW); // Assert nWAIT to make eZ80 wait

        if (digitalRead(EZ80_NRD_PIN) == LOW) { // Memory Read cycle
            uint8_t address = read_address_bus();
            if (address < sizeof(rom_image)) { // Check if address is within our emulated ROM
                uint8_t data_to_send = rom_image[address];
                write_data_bus(data_to_send);
                //Serial.printf("MEM RD: Addr=0x%02X, Data=0x%02X\n", address, data_to_send);
            } else {
                // Address out of ROM range, send NOP or 0xFF
                write_data_bus(0x00); // Or 0xFF
                //Serial.printf("MEM RD OOR: Addr=0x%02X\n", address);
            }
        }
        // Note: Memory Write (nWR LOW) to ROM area is usually ignored or could be logged.

        // De-assert nWAIT after data is ready (or after a fixed short delay)
        // eZ80 needs some time to read the data after nWAIT goes high.
        // The exact timing depends on the eZ80 clock and bus specs.
        // A small delay might be needed here before de-asserting nWAIT,
        // or ensure data bus is stable long enough.
        digitalWrite(EZ80_NWAIT_PIN, HIGH);
        // After nWAIT goes high, eZ80 completes the cycle.
        // Set data bus back to input to avoid bus contention.
        setup_pins_input(EZ80_D_PINS, 8);


    } else if (nioreq_active) { // I/O cycle
        digitalWrite(EZ80_NWAIT_PIN, LOW); // Assert nWAIT

        uint8_t port_address = read_address_bus(); // Lower 8 bits for I/O port

        if (digitalRead(EZ80_NWR_PIN) == LOW) { // I/O Write cycle (OUT instruction)
            if (port_address == 0x00) {
                uint8_t data_received = read_data_bus();
                Serial.printf("IO WR: Port=0x%02X, Data=0x%02X\n", port_address, data_received);
                if (data_received == 0xFF) {
                    digitalWrite(LED_PIN, HIGH);
                } else if (data_received == 0x00) {
                    digitalWrite(LED_PIN, LOW);
                }
            }
        } else if (digitalRead(EZ80_NRD_PIN) == LOW) { // I/O Read cycle (IN instruction)
            // For this example, we don't expect IN from port 0.
            // If needed, provide data here.
            // write_data_bus(0x00); // Example: send 0x00 for any IN
            Serial.printf("IO RD: Port=0x%02X\n", port_address);
        }

        digitalWrite(EZ80_NWAIT_PIN, HIGH);
        setup_pins_input(EZ80_D_PINS, 8);
    }

    // A small delay might be needed in the loop if polling too fast,
    // or if CPU usage is too high. However, this loop needs to be
    // as fast as possible to catch bus signals.
    // delayMicroseconds(1); // Experiment with this
}

ファームウェアに関する注記:

  • タイミング: 上記のloop()関数内のポーリングは、eZ80のクロックが非常に遅い(例: 数十kHz~100kHz)場合にのみ機能する可能性があります。digitalRead/Writeは比較的遅いため、より高速なクロックに対応するには、レジスタ直接操作やESP-IDFのGPIO関数、割り込み駆動型アプローチが必要です。
  • nWAIT: nWAIT信号の制御は非常に重要です。ESP32がアドレスを読み取り、データを準備/読み取りする間、nWAITをLOWにしてeZ80を待たせる必要があります。データ準備完了後、nWAITをHIGHに戻します。
  • データバスの方向: データバスは双方向です。ROM読み出し時はESP32が出力、I/O書き込み時はESP32が入力(eZ80からのデータを読む)になります。使用後は必ず入力モードに戻し、バスの衝突を避けてください。
  • デバッグ: Serial.print文はデバッグに役立ちますが、処理を遅くするため、タイミングがクリティカルな場合はコメントアウトしてください。
  • 割り込み駆動: より堅牢な実装のためには、nCS0nIORQの立ち下がりエッジで割り込みを発生させ、ISR内でバス処理を行うことを検討してください。ISR内ではdelay()は使えず、処理は非常に短くする必要があります。

5. 動作の仕組みと注意点

  1. クロック供給: ESP32がPWM機能を使ってeZ80のEXTALピンにクロック信号を供給します。
  2. リセット: ESP32がeZ80のnRSTピンを制御し、起動時にリセットをかけます。
  3. ブート: eZ80はブートモード設定に従い、アドレス0x0000からプログラムのフェッチを開始します。nCS0がアクティブになります。
  4. ROMエミュレーション:
    • ESP32はnCS0 (またはnMREQ) と nRD がアクティブになるのを監視します。
    • これらがアクティブになると、ESP32はnWAITをアサート(LOWに)します。
    • eZ80のアドレスバスからアドレスを読み取ります。
    • ESP32内のrom_image配列から対応するデータを取得します。
    • 取得したデータをeZ80のデータバスに出力します。
    • nWAITをデアサート(HIGHに)し、eZ80がデータを読み取って処理を続行できるようにします。
  5. I/Oエミュレーション (LED点滅):
    • eZ80が OUT (00H), A 命令を実行すると、nIORQnWRがアクティブになり、アドレスバスにポート番号 00H が出力されます。
    • ESP32はこれらの信号を検出し、nWAITをアサートします。
    • データバスからeZ80が出力したデータ(0xFFまたは0x00)を読み取ります。
    • 読み取ったデータに応じて、ESP32に接続されたLEDを点灯/消灯します。
    • nWAITをデアサートします。

重要な注意点:

  • クロック周波数: 最初は非常に低い周波数(例: 10kHz~100kHz)から始めてください。ESP32の処理が追いつかないと、eZ80は正しく動作しません。オシロスコープやロジックアナライザがあると、デバッグに非常に役立ちます。
  • GPIOの速度: ArduinoのdigitalRead/Writeは比較的低速です。より高速な動作を目指す場合は、ESP32のレジスタを直接操作するなどの最適化が必要です。
  • 安定性: 配線が多いシステムはノイズの影響を受けやすいです。適切なGND配線や、必要に応じてパスコンを追加することを検討してください。

6. 今後のステップ

  1. 配線: 慎重にESP32とeZ80基板を配線します。
  2. 最小限のテスト: まずはクロック供給とリセット制御が機能することを確認します。ESP32からクロックが出ているか、リセット信号が正しく制御できているかをオシロスコープで確認します。
  3. ROMエミュレーションテスト: eZ80が最初の命令(例: NOP)をフェッチしようとするか、アドレスバスや制御信号をロジックアナライザで監視します。
  4. I/Oエミュレーションテスト: 簡単なOUT命令を実行させ、ESP32がデータを受信できるか確認します。
  5. 完全なプログラム実行: LED点滅プログラム全体を実行します。

このプロジェクトは挑戦的ですが、成功すればマイコンの基本的な動作原理について深い理解が得られるでしょう。頑張ってください!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment