注意点:
- このプロジェクトは、CPUのバサイクルタイミングに非常に敏感であり、高度な知識とデバッグ能力を要します。ESP32の処理速度が追いつかない場合、eZ80のクロック周波数を非常に低く設定する必要があります。
- eZ80の詳細なバスタイミングについては、Zilog社のeZ80F0808MODデータシートおよびeZ80 CPUユーザーマニュアルをご参照ください。
- 配線が非常に多くなるため、慎重に行ってください。
- ESP32開発ボード (ESP-WROOM-32搭載のものなど)
- Z80マイコンCPUカード スーパーAKI-80
- ブレッドボード
- ジャンパーワイヤー
- LED x1
- 抵抗 x1 (LED用、220Ω~1kΩ程度)
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をエミュレートしますが、この設定で問題ありません。
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出力の電流容量に注意してください。
LEDはESP32のGPIOに接続し、ESP32がeZ80のI/O出力をエミュレートして制御します。
ESP32 GPIO20
(例) --- 抵抗 (220Ω) --- LEDアノード --- LEDカソード ---ESP32 GND
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)
I/Oポート0番に 0xFF
と 0x00
を交互に出力し、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 ... */
};
重要: 上記のマシンコードは手動で一部作成したものです。正確なアセンブラで生成し、アドレスを正しく解決したバイナリを使用してください。CALL
や JP
のアドレスは、プログラムの配置によって変わります。
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
#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
文はデバッグに役立ちますが、処理を遅くするため、タイミングがクリティカルな場合はコメントアウトしてください。 - 割り込み駆動: より堅牢な実装のためには、
nCS0
やnIORQ
の立ち下がりエッジで割り込みを発生させ、ISR内でバス処理を行うことを検討してください。ISR内ではdelay()
は使えず、処理は非常に短くする必要があります。
- クロック供給: ESP32がPWM機能を使ってeZ80の
EXTAL
ピンにクロック信号を供給します。 - リセット: ESP32がeZ80の
nRST
ピンを制御し、起動時にリセットをかけます。 - ブート: eZ80はブートモード設定に従い、アドレス0x0000からプログラムのフェッチを開始します。
nCS0
がアクティブになります。 - ROMエミュレーション:
- ESP32は
nCS0
(またはnMREQ
) とnRD
がアクティブになるのを監視します。 - これらがアクティブになると、ESP32は
nWAIT
をアサート(LOWに)します。 - eZ80のアドレスバスからアドレスを読み取ります。
- ESP32内の
rom_image
配列から対応するデータを取得します。 - 取得したデータをeZ80のデータバスに出力します。
nWAIT
をデアサート(HIGHに)し、eZ80がデータを読み取って処理を続行できるようにします。
- ESP32は
- I/Oエミュレーション (LED点滅):
- eZ80が
OUT (00H), A
命令を実行すると、nIORQ
とnWR
がアクティブになり、アドレスバスにポート番号00H
が出力されます。 - ESP32はこれらの信号を検出し、
nWAIT
をアサートします。 - データバスからeZ80が出力したデータ(
0xFF
または0x00
)を読み取ります。 - 読み取ったデータに応じて、ESP32に接続されたLEDを点灯/消灯します。
nWAIT
をデアサートします。
- eZ80が
重要な注意点:
- クロック周波数: 最初は非常に低い周波数(例: 10kHz~100kHz)から始めてください。ESP32の処理が追いつかないと、eZ80は正しく動作しません。オシロスコープやロジックアナライザがあると、デバッグに非常に役立ちます。
- GPIOの速度: Arduinoの
digitalRead/Write
は比較的低速です。より高速な動作を目指す場合は、ESP32のレジスタを直接操作するなどの最適化が必要です。 - 安定性: 配線が多いシステムはノイズの影響を受けやすいです。適切なGND配線や、必要に応じてパスコンを追加することを検討してください。
- 配線: 慎重にESP32とeZ80基板を配線します。
- 最小限のテスト: まずはクロック供給とリセット制御が機能することを確認します。ESP32からクロックが出ているか、リセット信号が正しく制御できているかをオシロスコープで確認します。
- ROMエミュレーションテスト: eZ80が最初の命令(例:
NOP
)をフェッチしようとするか、アドレスバスや制御信号をロジックアナライザで監視します。 - I/Oエミュレーションテスト: 簡単な
OUT
命令を実行させ、ESP32がデータを受信できるか確認します。 - 完全なプログラム実行: LED点滅プログラム全体を実行します。
このプロジェクトは挑戦的ですが、成功すればマイコンの基本的な動作原理について深い理解が得られるでしょう。頑張ってください!