mz25key Interface Developer's Guide
mz25key Developer's Guide
The mz25key is a dedicated keyboard interface for the Sharp MZ-2500 and MZ-2800 computers, built around the Espressif ESP32 dual-core microcontroller. It translates modern PS/2 and Bluetooth HID keyboards into the native MZ-2500/MZ-2800 keyboard matrix protocol, allowing any standard USB or wireless keyboard to be used with these vintage Sharp machines. The mz25key shares its entire codebase with the SharpKey multi-host interface project but is built with a simplified configuration targeting only the MZ-2500 or MZ-2800. The firmware is licensed under GPL v3. Author: Philip Smart.
This guide is the primary developer reference for the mz25key firmware. It covers the complete software architecture, every source module, the MZ-2500/MZ-2800 keyboard protocol in depth, the key mapping system, web interface, Bluetooth integration, build environment, CI/CD pipeline, and debugging techniques. For hardware design, schematics, and end-user operation see the mz25key project page. For the full multi-host SharpKey developer's guide see the SharpKey Developer's Guide.
Key difference from SharpKey: The mz25key is the same source code as SharpKey but built with the Kconfig target
MZ25KEY_MZ2500 or MZ25KEY_MZ2800 instead of SHARPKEY. The critical difference is that the mz25key git repository does not use git submodules — the required component libraries (arduino-esp32 and esp_littlefs) must be cloned manually into the components/ directory at specific versions. The CI/CD pipeline handles this automatically in a dedicated "Fetch Components" stage.
Prerequisites
Before you can build and flash the mz25key firmware you need a working development environment. There are two approaches: a native ESP-IDF installation, or Docker (covered in the Docker Build section below).
- ESP-IDF v4.4: The Espressif IoT Development Framework, version 4.4 specifically. Later versions (v5.x) are not compatible due to Arduino component API changes. Install natively or use the official Docker image
espressif/idf:v4.4. - Python 3.8+: Required by the ESP-IDF build system scripts and menuconfig. Use
update-alternativeson Linux to ensurepython3points to a recent version. - Git: For cloning the repository and the two required component libraries. Note: unlike SharpKey, the mz25key repository does not use git submodules.
- USB-to-TTL UART adapter: For flashing firmware and serial debug output. Must support 3.3V logic levels with RTS and DTR signals for automated bootstrap/programming mode entry.
- Docker (optional): As an alternative to installing ESP-IDF natively. The
espressif/idf:v4.4Docker image contains the complete toolchain. - Component versions:
arduino-esp32at tag2.0.3andesp_littlefsat tagv1.3.1(with submodules initialised).
Repository Structure
The mz25key repository is hosted at https://git.eaw.app/eaw/mz25key. It shares the same source files as the SharpKey project but lives in a separate repository with one critical structural difference: there is no
.gitmodules file. The components/ directory is empty after a fresh clone and must be populated manually by cloning the two required libraries at their specific versions.
mz25key/ ├── main/ │ ├── SharpKey.cpp — Entry point (same source as SharpKey) │ ├── MZ2528.cpp — MZ-2500/2800 host interface (Core 1) │ ├── MZ5665.cpp — MZ-5500/5600/6500 (compiled but unused in mz25key build) │ ├── X1.cpp — Sharp X1 interface (compiled but unused) │ ├── X68K.cpp — Sharp X68000 interface (compiled but unused) │ ├── PC9801.cpp — NEC PC-9801 interface (compiled but unused) │ ├── Mouse.cpp — Mouse support │ ├── HID.cpp — HID device manager / input aggregator │ ├── BTHID.cpp — Bluetooth HID report translation │ ├── BT.cpp — Bluetooth GAP/GATT discovery and pairing │ ├── WiFi.cpp — WiFi AP/client, HTTP server, OTA, LittleFS │ ├── PS2KeyAdvanced.cpp — PS/2 keyboard protocol (interrupt-driven) │ ├── PS2Mouse.cpp — PS/2 mouse protocol │ ├── KeyInterface.cpp — Virtual base class for host interfaces │ ├── LED.cpp — Status LED control │ ├── NVS.cpp — Non-volatile storage wrapper │ ├── SWITCH.cpp — Timed button handler │ ├── esp_efuse_custom_table.c — eFuse field accessors (generated) │ ├── esp_efuse_custom_table.csv — eFuse field definitions (source) │ ├── Kconfig.projbuild — Build configuration menu │ └── include/ — Headers with key mapping tables │ ├── MZ2528.h — MZ-2500/2800 mapping table (165 entries) │ ├── X1.h, X68K.h, ... — Other host mapping tables │ └── ... ├── webserver/ — HTML/CSS/JS web interface │ ├── version.txt — Release version (NOTE: not in project root) │ └── html/ — Web page source files ├── components/ — EMPTY after clone! Must populate manually │ ├── arduino-esp32/ — Clone from GitHub at tag 2.0.3 │ └── esp_littlefs/ — Clone from GitHub at tag v1.3.1 + submodules ├── build_webfs.sh — Web filesystem builder script ├── sharpkey_partition_table.csv — Flash partition layout (same as SharpKey) ├── sdkconfig — Default build configuration (MZ25KEY target) ├── CMakeLists.txt — CMake project file └── version.txt — (May exist — see webserver/version.txt for CI)
Important: After cloning the repository, the
components/ directory will be empty. You must manually clone the two component libraries before building:
# Clone the mz25key repository git clone https://git.eaw.app/eaw/mz25key.git cd mz25key # Clone required components manually (NOT submodules!) mkdir -p components # Arduino-ESP32 compatibility layer — must be v2.0.3 git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32 cd components/arduino-esp32 && git checkout 2.0.3 && cd ../.. # LittleFS filesystem support — must be v1.3.1 WITH submodules git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs cd components/esp_littlefs && git checkout v1.3.1 && git submodule update --init --recursive && cd ../..
The
esp_littlefs component requires its own submodules to be initialised — specifically the mklittlefs tool. Without running git submodule update --init --recursive inside components/esp_littlefs/, the build will fail with a "No rule to make target 'dist'" error during the filesystem image generation step.
Differences from SharpKey
The mz25key and SharpKey share the same source code, but there are several important differences in how the project is structured, built, and deployed:
- Build target: mz25key uses
CONFIG_MZ25KEY_MZ2500orCONFIG_MZ25KEY_MZ2800in Kconfig, notCONFIG_SHARPKEY. This selects only the MZ-2500 or MZ-2800 host interface path and disables the runtime GPIO-based host auto-detection used by SharpKey. - No git submodules: The mz25key repository has no
.gitmodulesfile. Thecomponents/directory is empty after a fresh clone. You must manually clonearduino-esp32(v2.0.3) andesp_littlefs(v1.3.1 with submodules) intocomponents/. SharpKey usesgit clone --recursiveto fetch these automatically. - Single host interface: Only the MZ-2500 or MZ-2800 interface path is active at runtime. SharpKey detects the connected host via GPIO pin patterns and selects the appropriate interface dynamically.
- Simplified configuration: Fewer Kconfig options are exposed. The multi-host GPIO detection pins, X1/X68K/PC9801/MZ5665 specific settings, and the universal build target are not relevant.
- Separate repository: https://git.eaw.app/eaw/mz25key (SharpKey is at https://git.eaw.app/eaw/SharpKey).
- CI/CD pipeline: Has an additional "Fetch Components" stage that clones the two component libraries. SharpKey's pipeline uses
git submodule update --init --recursiveinstead. - Version file location: The CI pipeline reads the version from
webserver/version.txt, notversion.txtin the project root as SharpKey does. - Release artifact naming: Uses the
mz25key-prefix (e.g.mz25key-FW-v1.00.bin.gz) instead ofSharpKey-.
Software Architecture
The mz25key firmware shares the same layered software architecture as SharpKey. It is a dual-core ESP32 application built on FreeRTOS, with the two CPU cores serving distinct roles. The input layer accepts key events from PS/2 keyboards and Bluetooth HID devices. The key mapping layer translates these events into MZ-2500/MZ-2800 scan matrix positions via
MZ2528::mapKey(). The output layer drives the timing-critical GPIO signals on Core 1 to emulate the native keyboard.
Architecture Overview
┌───────────────────────────────────────────────────────────────────────────┐
│ ESP-32S (Dual Core 240MHz) │
│ │
│ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ │
│ │ CORE 0 (PRO) │ │ CORE 1 (APP) │ │
│ │ FreeRTOS Scheduled │ │ Spinlocked (no FreeRTOS) │ │
│ │ │ │ │ │
│ │ ┌──────────────────────┐ │ │ ┌────────────────────────────┐ │ │
│ │ │ PS/2 Keyboard Input │ │ │ │ MZ-2500/2800 Interface │ │ │
│ │ │ (PS2KeyAdvanced) │ │ │ │ (MZ2528 class) │ │ │
│ │ └─────────┬────────────┘ │ │ │ │ │ │
│ │ │ │ │ │ - Monitor RTSN strobe │ │ │
│ │ ┌─────────▼────────────┐ │ │ │ - Read row from KDB[3:0] │ │ │
│ │ │ Bluetooth Keyboard │ │ │ │ - Output column data │ │ │
│ │ │ (BTHID class) │ │ │ │ via KDO[7:0] │ │ │
│ │ └─────────┬────────────┘ │ │ │ - Handle KD4/MPX timing │ │ │
│ │ │ │ │ └────────────┬───────────────┘ │ │
│ │ ┌─────────▼────────────┐ │ │ │ │ │
│ │ │ HID Processing │ │ │ │ │ │
│ │ │ PS/2 → internal │ │ │ ┌────────────▼───────────────┐ │ │
│ │ │ scan code │ │ │ │ Virtual Scan Matrix │ │ │
│ │ └─────────┬────────────┘ │ │ │ 14 rows × 8 columns │ │ │
│ │ │ │ │ │ (15 rows for MZ-2800) │ │ │
│ │ ┌─────────▼────────────┐ │ │ └────────────────────────────┘ │ │
│ │ │ MZ2528::mapKey() │──┼──┤ │ │
│ │ │ internal → matrix │ │ │ │ │
│ │ └──────────────────────┘ │ │ │ │
│ │ │ │ │ │
│ │ ┌──────────────────────┐ │ │ │ │
│ │ │ WiFi / Web Server │ │ │ │ │
│ │ │ OTA / NVS / LED │ │ │ │ │
│ │ └──────────────────────┘ │ │ │ │
│ └─────────────────────────────┘ └──────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
PS/2 Keyboard MZ-2500 / MZ-2800
┌─────────┐ ┌───────────────┐
│ CLK/DAT │◄────── GPIO ──────────►│ RTSN/KDB/KDO │
└─────────┘ │ KD4/MPX │
└───────────────┘
FreeRTOS Task Model
The ESP32's dual cores are assigned distinct roles to meet the sub-microsecond timing requirements of the MZ-2500/MZ-2800 keyboard protocol:
| Task | Core | Priority | Stack | Purpose |
|---|---|---|---|---|
| mz25Interface | Core 1 | 25 | 4096 | MZ-2500 timing-critical GPIO strobe response |
| mz28Interface | Core 1 | MAX-1 | 2048 | MZ-2800 timing-critical GPIO strobe response |
| hidInterface | Core 0 | 0 | 4096 | PS/2 and Bluetooth input polling and key mapping |
Only one of
mz25Interface or mz28Interface runs at any time, determined by the Kconfig build target (MZ25KEY_MZ2500 or MZ25KEY_MZ2800).
- Core 0 (PRO_CPU): Runs the standard FreeRTOS scheduler with WiFi/BT stack tasks, the PS/2 keyboard interrupt handler, the HID polling task, LED blinking, switch monitoring, and the WiFi HTTP server. All non-timing-critical work happens here.
- Core 1 (APP_CPU): Detached from FreeRTOS using a spinlock. It runs a bare-metal tight loop that monitors the host machine's RTSN strobe signal and responds within the required timing window (~600ns for MZ-2500, ~1.78us for MZ-2800). This core does not participate in FreeRTOS scheduling while the host interface is active.
The data flow for a keystroke follows this path:
- Physical key press: The PS/2 keyboard generates a scan code (set 2) and clocks it out over the PS/2 DATA and CLK lines, or a Bluetooth HID device sends an HID report.
- Input driver:
PS2KeyAdvancedcaptures the scan code via GPIO interrupt and assembles it into a 16-bit internal code (upper byte = modifier flags, lower byte = keycode). For Bluetooth,BTHIDtranslates the HID report into the same 16-bit format using a 179-entry lookup table. - HID aggregator: The
HIDclass polls both input sources and presents a unified stream of key events. - Key mapping:
MZ2528::mapKey()receives the 16-bit key code and looks it up in the 165-entry mapping table. The table maps PS/2 key codes to host-specific row/column pairs in the virtual key matrix. - Virtual scan matrix: A 14-element (or 15-element for MZ-2800) array in memory is updated — the relevant bit in the row byte is set (key pressed) or cleared (key released).
- GPIO output: Core 1 continuously monitors RTSN. When the host requests a row, Core 1 reads the row index from KDB[3:0] and outputs the corresponding byte via KDO[7:0] through the 74HCT257 multiplexer. All done with
IRAM_ATTRfunctions, spinlock, and direct register access.
MZ-2500 Keyboard Protocol
The MZ-2500 keyboard interface uses a strobe-based parallel protocol. The main unit's gate array continuously scans the 14-row by 8-column keyboard matrix by sending row addresses and reading back column data. The protocol sequence for a single row scan:
- RTSN rising edge: The host signals it is sending a row address. The ESP32 begins monitoring.
- Read KDB[3:0]: After ~160ns, the 4-bit row number (0-13) is valid on GPIO_IN_REG. The ESP32 reads it via direct register access.
- Set KDO[7:0] HIGH: All 8 output bits are initially set to 0xFF (inactive / no keys pressed).
- Check KDI4 (KD4): If KD4 is HIGH, the host wants the column data for the specific requested row. If KD4 is LOW, the host wants the STROBEALL value — the logical AND of all rows (used for idle key detection).
- Clear active key bits: For the requested row, the active key bits are cleared via
GPIO.out_w1tc(atomic clear operation). - RTSN falling edge: The host latches the data from the 74HCT257 multiplexer. The ESP32 waits for this edge before processing the next row.
Timing: approximately 1.2us per RTSN cycle. A full matrix scan of 14 rows takes approximately 16.8us. All hot-path code uses
IRAM_ATTR to ensure it resides in instruction RAM (not flash cache), and direct GPIO register access (REG_READ/REG_WRITE) bypasses the ESP-IDF HAL for single-cycle (~4ns at 240MHz) performance.
MZ-2500 Key Data Retrieval Protocol (one row scan cycle):
┌────────── 660ns ──────────┐┌────────── 680ns ──────────┐
│ ││ │
┌────────┘ └┘ └────────┐
RTSN │ HIGH (row address phase) │ LOW (data return phase) │
│ │ │
│ ┌─ 160ns ─┐ │ │
│ │ │ │ │
KDB │ │ ROW VALID │ ┌── nibble hi ──┐┌── nibble lo ──┐ │
[3:0] │ │ (row 0-13) │ │ KDO[7:4] ││ KDO[3:0] │ │
│ │ │ │ ││ │ │
│ │ │
KD4 ──── HIGH (row data mode) ──────────── HIGH ────────────────────────────────
│ │ │
MPX ──────────────────────────────────────┘ ┌── 320ns ──┐┌── 320ns ──┐ │
│ │ HIGH ││ LOW │ │
│ │ upper nib ││ lower nib │ │
└───────────────────────────────────────────────────────────────────────────┘
STROBEALL Mode: When no key is pressed, KD4 is held LOW by the host. The main unit sends a fixed row number continuously (row 4 on MZ-2500, row 0 on MZ-2800 — the value is ignored). In this mode, the keyboard returns the logical AND of all rows for each column — if any key in a column is pressed, the corresponding AND bit goes LOW, triggering the host to switch to key data retrieval mode where it scans each row individually.
Key Data Retrieval: When a key press is detected via STROBEALL, the host switches KD4 HIGH and begins scanning specific rows. It first probes row 11 (modifier keys: CTRL, SHIFT, LOCK, KANA, GRAPH) for approximately 30us to allow for debounce, then row 12 (Japanese transform keys) for approximately 50us, followed by a sequential scan from row 0 through row 13. When the row containing the pressed key is found, that row is scanned for over 600us for debounce verification before the sequential scan continues.
Performance note: Direct register access (
REG_READ/REG_WRITE) is used instead of the Arduino digitalRead()/digitalWrite() functions. The Arduino GPIO functions add significant overhead (function call, parameter validation, pin mapping lookup) which would exceed the ~300ns timing window. Direct register access completes in a single clock cycle at 240MHz (~4ns).
The Core 1 spinlock is critical. It is acquired in
app_main() before calling the host interface loop and is never released. This prevents FreeRTOS from scheduling any tasks on Core 1 or servicing interrupts, ensuring the RTSN monitoring loop runs with zero jitter:
// In app_main(), after all Core 0 tasks are created: portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; portENTER_CRITICAL(&mux); // Disables interrupts, prevents scheduling hostInterface->run(); // Never returns — tight RTSN polling loop portEXIT_CRITICAL(&mux); // Never reached
MZ-2800 Keyboard Protocol
The MZ-2800 uses the same physical signals as the MZ-2500 but with different timing and an additional row:
- Longer setup times: KDB[3:0] row data trails RTSN by at least 650ns (compared to 160ns on the MZ-2500). The ESP32 must insert a longer delay before reading the row number.
- Higher RTSN frequency: The RTSN period is approximately 1.78us.
- Higher task priority: The MZ-2800 interface task runs at
MAX_PRIORITIES-1instead of priority 25. - Row 14: In native 80286 mode, the MZ-2800 scans a 15th row (row 14) containing keys specific to the 80286 operating system. In MZ-2500 compatibility mode, only rows 0-13 are scanned.
- Shorter debounce: ~100us initial, ~300us repeat (compared to ~600us on the MZ-2500).
- Different connector: The MZ-2800 uses an AMP 9-pin D-Sub connector (3M 10120-6000L/10220-5212PL compatible replacement). The MZ-2500 uses an 8-pin mini-DIN.
- STROBEALL row: The MZ-2800 sends row 0 during STROBEALL mode; the MZ-2500 sends row 4. In both cases the row value is ignored.
These differences are handled automatically by the Kconfig build target selection (
MZ25KEY_MZ2500 vs MZ25KEY_MZ2800), which adjusts timing constants and enables the 15th row.
Key Matrix Structure
The virtual scan matrix is 14 rows by 8 columns for the MZ-2500 (15 rows for MZ-2800). Each row is stored as a single byte with active-LOW logic — 0xFF means no keys pressed in that row:
Row 0: F1, F2, F3, F4, F5, NUM, GRAPH, KANA Row 1: F6, F7, F8, F9, F10, HOME/CLR, semicolons, colon Row 2: 1, 2, 3, 4, 5, 6, 7, 8 Row 3: 9, 0, -, ^, YEN, TAB, Q, W Row 4: E, R, T, Y, U, I, O, P Row 5: @, [, RETURN, A, S, D, F, G Row 6: H, J, K, L, ;, :, ], SPACE Row 7: Z, X, C, V, B, N, M, , Row 8: ., /, _, cursor down, cursor left, cursor right, cursor up, DEL/BS Row 9: ROLLUP, ROLLDOWN, INS, CLR, cursor keys (alternate) Row 10: Tenkey 0, 1, 2, 3, 4, 5, 6, 7 Row 11: Tenkey 8, 9, *, +, -, /, ., = Row 12: CTRL, SHIFT, LOCK, KANA_LOCK, GRAPH_LOCK Row 13: Japanese transform keys (HENKAN, MUHENKAN, etc.) Row 14: MZ-2800 specific keys (80286 mode only — not scanned on MZ-2500)
Source Modules
The firmware comprises 17 source files organised into four groups. All source resides in the
main/ directory. For mz25key builds, only the MZ2528 host interface is active — the other host interface modules (X1, X68K, PC9801, MZ5665) are compiled but their code paths are never entered.
| File | Class | Purpose |
|---|---|---|
| SharpKey.cpp | — | Application entry point, GPIO host detection, FreeRTOS task creation |
| MZ2528.h / MZ2528.cpp | MZ2528 | MZ-2500/MZ-2800 host keyboard interface (timing-critical, Core 1) |
| X1.h / X1.cpp | X1 | Sharp X1 series host interface (compiled but unused) |
| X68K.h / X68K.cpp | X68K | Sharp X68000 host interface (compiled but unused) |
| PC9801.h / PC9801.cpp | PC9801 | NEC PC-9801 host interface (compiled but unused) |
| MZ5665.h / MZ5665.cpp | MZ5665 | Sharp MZ-5600/6500 host interface (compiled but unused) |
| Mouse.h / Mouse.cpp | Mouse | PS/2 mouse pass-through |
| KeyInterface.h / KeyInterface.cpp | KeyInterface | Virtual base class for all host interfaces |
| HID.h / HID.cpp | HID | Input device aggregator (PS/2 + Bluetooth) |
| PS2KeyAdvanced.h / PS2KeyAdvanced.cpp | PS2KeyAdvanced | PS/2 keyboard driver (interrupt-driven, scan code set 2) |
| PS2Mouse.h / PS2Mouse.cpp | PS2Mouse | PS/2 mouse driver |
| BT.h / BT.cpp | BT | Bluetooth GAP/GATT device discovery and pairing |
| BTHID.h / BTHID.cpp | BTHID | Bluetooth HID report translation (179-entry lookup table) |
| NVS.h / NVS.cpp | NVS | Thread-safe non-volatile storage (mutex-protected key-value) |
| LED.h / LED.cpp | LED | Status LED (OFF/ON/BLINK/BLINK_ONESHOT with duty cycle) |
| SWITCH.h / SWITCH.cpp | SWITCH | Timed button handler (1s=cancel, 2-4s=WiFi, 5-9s=AP, 10-14s=BT, 15-19s=reset) |
| WiFi.h / WiFi.cpp | WiFi | WiFi AP/client, HTTP server, REST API, OTA, LittleFS |
All host interfaces inherit from
KeyInterface, which defines the common API: virtual void hostInterface() (the main loop driving GPIO signals, marked IRAM_ATTR for timing-critical interfaces), virtual void mapKey(uint16_t keyCode) (translates 16-bit key codes into matrix positions), virtual void persistConfig() (saves active keymap to NVS), and virtual uint8_t* getKeyMapData(size_t *len) (returns binary keymap data for the web interface).
Key Mapping System
The key mapping system is the core of the mz25key's functionality. It translates PS/2 scan codes into the specific row/column activations expected by the MZ-2500/MZ-2800 keyboard controller. The system supports multiple mapping layers and runtime switching between keyboard layouts.
Scan Code Pipeline
Each keystroke passes through three translation stages before reaching the host machine:
PS/2 Scan Code Set 2 Internal 16-bit Code MZ Matrix Position ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Make: 0x1C │ │ Upper: modifiers │ │ Row: 5 │ │ Break: 0xF0 0x1C │──►│ Lower: 0x41 (A) │──► │ Col bit: 0x08 │ │ (PS/2 protocol) │ │ (PS2KeyAdvanced) │ │ (MZ2528::mapKey) │ └──────────────────┘ └──────────────────┘ └──────────────────┘ Stage 1: PS2KeyAdvanced Stage 2: HID class Stage 3: MZ2528::mapKey() (interrupt-driven, (normalisation, (165-entry table lookup, scan code set 2) modifier tracking) row/column output)
The 16-bit internal key code format uses the upper byte for modifier flags and the lower byte for the key identifier:
Bit 15 Bit 14 Bit 13 Bit 12 Bit 11 Bit 10 Bit 9 Bit 8 ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ BREAK │ SHIFT │ CTRL │ CAPS │ ALT │ ALT_GR │ GUI │FUNCTION │ └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ Bits 7-0: Key code (unique identifier for each physical key)
Mapping Table
The MZ2528 mapping table (
PS2TBL[] in MZ2528.h) contains 165+ entries. Each entry maps a PS/2 key code to one or two positions in the virtual key matrix. The table fields are:
Column Type Description ────────────── ──────── ────────────────────────────────────────────────── PS2 Keycode uint8_t Lower byte of the 16-bit internal key code Ctrl Key uint8_t Modifier key requirements (PS2CTRL_NONE, SHIFT, etc.) Keyboard Model uint8_t PS/2 keyboard model filter (0 = all models) Host Model uint8_t Target machine (MZ_ALL, MZ_80B, MZ_2000, MZ_2500, MZ_2800) Make Row1 uint8_t Row in virtual matrix to activate on key press Make Key1 uint8_t Column bit mask to set in that row Make Row2 uint8_t Second row (for SHIFT+key combos; 0xFF = unused) Make Key2 uint8_t Second column bit mask Break Row1 uint8_t Row to deactivate on key release Break Key1 uint8_t Column to clear on release Break Row2 uint8_t Second row to deactivate on release Break Key2 uint8_t Second column bit mask on release
A single PS/2 key press can activate up to two positions in the virtual matrix simultaneously. This is essential for keys that need to appear as a modifier+key combination on the host — for example, mapping PS/2
@ to the MZ-2500 SHIFT+2 position. Machine-specific variants (MZ_ALL, MZ_80B, MZ_2000, MZ_2500, MZ_2800) allow the same table to serve different keyboard layouts.
Updating Key Mappings
Key mappings can be modified in two ways:
Method 1: Web Interface (recommended) — When WiFi is enabled, navigate to the keymap editor page (
keymap.html). The editor displays the current mapping table and allows editing, saving, exporting, and importing keymaps. Changes are saved as binary keymap files on the LittleFS partition and take effect immediately.
Method 2: Source Code — Edit the
PS2TBL[] array in MZ2528.h. Each row follows the format described above. After editing, rebuild and reflash the firmware.
Example of adding a key mapping entry:
// Map PS/2 F11 to MZ row 3, bit 7 (HELP key)
// { PS2Code, CtrlKey, KbdModel, HostModel, MkRow1, MkKey1, MkRow2, MkKey2,
// BkRow1, BkKey1, BkRow2, BkKey2 }
{PS2_KEY_F11, PS2CTRL_NONE, 0, MZ_ALL, {3, 0x80, 0, 0, 0, 0}, {3, 0x80, 0, 0}},
// Map PS/2 Backslash to MZ row 6, bit 2 (single-position mapping)
// 0xFF in Row2 means no second matrix position needed
{0x5D, 0x00, 0x00, 0x00, 6, 0x04, 0xFF, 0x00, 6, 0x04, 0xFF, 0x00},
The ALT+F1/F2/F3 hotkeys switch between pre-loaded keymap files at runtime:
- ALT+F1: MZ-2500 layout (default) — the standard 14-row matrix.
- ALT+F2: MZ-2000 layout — adjusts the matrix to match MZ-2000 key positions.
- ALT+F3: MZ-80B layout — reconfigures for the MZ-80B keyboard matrix arrangement.
The active layout is saved to NVS and persists across power cycles. Each layout uses a different subset of rows in the mapping table, filtered by the Host Model column.
Supported Keyboards
The following PS/2 and Bluetooth keyboards have been tested with the mz25key interface:
- Wyse KB-3926 (PS/2, compact layout)
- OADG 109 Japanese keyboard (PS/2)
- Sanwa SKBL1 (PS/2, backlit)
- Periboard 810 (Bluetooth, compact)
- Omoton K8508 (Bluetooth, slim)
- Standard IBM Model M (PS/2, via DIN-to-PS/2 adapter)
- Cherry G80 series (PS/2)
- Logitech K380 (Bluetooth multi-device)
Any PS/2 keyboard that sends scan code set 2 (the default for AT/PS/2 keyboards) should work. Bluetooth keyboards must support the HID profile (UUID 0x1124). The
Keyboard Model field in the mapping table allows model-specific overrides for keyboards with non-standard scan code behaviour.
Binary Keymap Files
In addition to the compiled-in static mapping table, the mz25key supports loading custom keymaps from binary files stored on the LittleFS filesystem partition. Binary keymap files follow the naming convention:
MZ2528_KeyMap.BIN — custom keymap for MZ-2500/MZ-2800
The binary file contains the same data as the static array but in a packed binary format. When a binary keymap file exists on the filesystem, it takes precedence over the compiled-in table. This allows end users to customise key mappings through the web interface without recompiling the firmware. The web UI keymap editor downloads the current keymap via the REST API, displays it in an editable HTML table, validates changes, and uploads the modified keymap back to the device as a
MZ2528_KeyMap.BIN file on the LittleFS partition.
GPIO Configuration
The ESP32 GPIO pin assignments are configured via Kconfig and match the SharpKey PCB layout. The key pins are:
| Signal | GPIO | Direction | Notes |
|---|---|---|---|
| RTSNI (Row Strobe) | 35 | Input | Input-only pin, read via GPIO_IN1_REG bank |
| KDB0 | 36 | Input | Row select bit 0 (input-only) |
| KDB1 | 37 | Input | Row select bit 1 (input-only) |
| KDB2 | 38 | Input | Row select bit 2 (input-only) |
| KDB3 | 39 | Input | Row select bit 3 (input-only) |
| KDO0 | 12 | Output | Column data bit 0 to 74HCT257 |
| KDO1 | 13 | Output | Column data bit 1 |
| KDO2 | 14 | Output | Column data bit 2 |
| KDO3 | 15 | Output | Column data bit 3 |
| KDO4 | 16 | Output | Column data bit 4 |
| KDO5 | 17 | Output | Column data bit 5 |
| KDO6 | 18 | Output | Column data bit 6 |
| KDO7 | 19 | Output | Column data bit 7 |
| KDI4 | 34 | Input | KD4 type strobe (input-only) |
| MPXI | 33 | Input | MPX nibble select |
| PS/2 CLK | 26 | Input | PS/2 keyboard clock (interrupt) |
| PS/2 DATA | 27 | Input/Output | PS/2 keyboard data (bidirectional) |
| LED | 2 | Output | Status LED |
| SWITCH | 0 | Input | WiFi/mode button |
Important notes:
- RTSNI on GPIO 35: This is an input-only pin on the ESP32. It is read via the
GPIO_IN1_REGbank (GPIOs 32-39), notGPIO_IN_REG(GPIOs 0-31). The firmware uses a pre-computed bit mask for direct register access. - KDO0-7 output: These 8 pins drive the 74HCT257 quad 2:1 multiplexer. The MPX signal from the host directly controls the multiplexer to select the upper or lower nibble, eliminating the need for the ESP32 to respond to MPX timing.
- ADC2/WiFi GPIO conflict: GPIOs 0, 2, 4, 12-15, 25-27 are shared with ADC2, which cannot be used while WiFi is active. The PCB design uses these pins for digital GPIO only, avoiding the conflict.
Bluetooth Integration
The mz25key firmware includes Bluetooth Classic HID support, allowing wireless keyboards to be used as an input source instead of or in addition to the PS/2 connector. The Bluetooth stack is split across two classes:
BT handles GAP/GATT device discovery and connection management; BTHID handles HID report parsing and translation to the internal key code format.
- Protocols: Bluetooth Classic + BLE HID support.
- Bonded devices: Up to 5 devices can be stored in NVS.
- Pairing window: 60 seconds after initiating scan via the button (hold 10-14 seconds).
- PIN: "1234" for devices that require legacy pairing. Most modern devices use SSP (no PIN).
- Priority: PS/2 keyboard input has priority over Bluetooth when both are connected. Events from both sources are merged by the HID aggregator.
Pairing process:
- Press and hold the mz25key button for 10-14 seconds. The LED enters fast-blink mode.
- Put the Bluetooth keyboard into pairing mode (usually by holding its pairing button).
- The mz25key scans for BT HID devices for approximately 60 seconds.
- When a device is found, pairing initiates automatically (SSP, no PIN required for most devices).
- On success, the LED returns to normal blink. The device address is saved to NVS for automatic reconnection on subsequent power-ups.
Limitations: WiFi and Bluetooth are mutually exclusive in the current firmware to avoid latency spikes on the keyboard interface. Enabling WiFi disables Bluetooth and vice versa. The ESP32 uses a single antenna shared between both radios.
WiFi and Web Interface
The mz25key firmware includes an embedded web server for browser-based configuration, key mapping, and firmware management. The web interface is identical to SharpKey's and runs on Core 0 as a FreeRTOS task.
- AP mode: The mz25key creates its own WiFi network (default SSID:
sharpkey). Connect a device to this network and access the web interface athttp://192.168.4.1. - Client mode: The mz25key connects to an existing WiFi network using credentials stored in NVS. The web interface is accessible at the DHCP-assigned IP address.
- Mode switching: Hold the button 2-4 seconds for client mode, 5-9 seconds for AP mode. A reboot is required to switch between WiFi and Bluetooth.
The web interface serves files from a LittleFS filesystem partition (640 KB, named
filesys). Five HTML pages are available:
| Page | URL | Purpose |
|---|---|---|
| index.html | / | Dashboard — firmware version, host type, WiFi/BT status |
| keymap.html | /keymap.html | Visual key mapping editor |
| mouse.html | /mouse.html | Mouse configuration (if applicable) |
| ota.html | /ota.html | Firmware and filesystem OTA update |
| wifimanager.html | /wifimanager.html | WiFi network scan, credentials, mode switching |
REST API endpoints for programmatic access:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/status | Device status (version, host type, uptime) |
| GET | /api/keymap | Download current keymap as binary |
| POST | /api/keymap | Upload new keymap |
| GET | /api/wifi/scan | Trigger WiFi scan and return results |
| POST | /api/wifi/connect | Connect to a WiFi network |
| POST | /api/ota/firmware | Upload firmware update |
| POST | /api/ota/filesystem | Upload filesystem update |
| POST | /api/reboot | Trigger device reboot |
Building the Web Filesystem (build_webfs.sh)
The
build_webfs.sh script prepares the web files for inclusion in the firmware image:
- Copies all HTML files from
webserver/html/into thewebfs/staging directory. - Compresses CSS and JavaScript files with gzip (the HTTP server serves them with
Content-Encoding: gzipto save flash space). - Copies any binary assets (default keymap files, favicon) into
webfs/. - The ESP-IDF build system then packages the
webfs/directory into a LittleFS filesystem image (build/filesys.bin) that is flashed to thefilesyspartition.
# Build the web filesystem before building firmware chmod +x build_webfs.sh ./build_webfs.sh # The output is used automatically by the ESP-IDF build
If you modify any web page, JavaScript, or CSS file, re-run
build_webfs.sh before rebuilding the firmware. The LittleFS partition is 640 KB — this limits the total size of all web assets including compressed JavaScript and CSS.
Adding new web pages: Create the HTML file in
webserver/html/. If the page needs new REST API endpoints, add the handler registrations in WiFi.cpp in the startWebServer() function. Add a navigation link in index.html. Run build_webfs.sh to package the new files. Rebuild and reflash both firmware and filesystem.
OTA Updates
The mz25key supports Over-The-Air firmware and filesystem updates via the web interface. This allows updating the device without a physical UART connection — only WiFi access is needed.
Partition Scheme
The ESP32 flash is divided into partitions defined in
sharpkey_partition_table.csv:
Name Type SubType Offset Size Description ────────────── ────── ──────── ───────── ───────── ──────────────────────────── nvs data nvs 0x9000 0x5000 Non-volatile storage otadata data ota 0xE000 0x2000 OTA boot selection data ota_0 app ota_0 0x10000 0x1A0000 Firmware bank 0 (1664 KB) ota_1 app ota_1 0x1B0000 0x1A0000 Firmware bank 1 (1664 KB) filesys data 0x80 0x350000 0xA0000 LittleFS web filesystem (640 KB) coredump data coredump 0x3F0000 0x10000 Core dump storage (64 KB)
The dual-bank OTA scheme (
ota_0 and ota_1) means the device always has a known-good firmware image. New firmware is written to the inactive bank while the active bank continues running. After a successful write and SHA-256 verification, the otadata partition is updated to boot from the new bank on the next reset. If the new firmware fails to boot, the boot loader automatically rolls back to the previous bank.
Update process:
- Connect to the mz25key web interface (AP or client mode).
- Navigate to
/ota.html. - Select "Firmware Update" (uploads
main.bin) or "Filesystem Update" (uploadsfilesys.bin). - Choose the binary file and click Upload. A progress bar shows transfer status.
- After upload, the device verifies image integrity (SHA-256 hash check).
- If verification passes, the device reboots into the new firmware. Version validation prevents downgrade.
eFuse Custom Fields
The ESP32 contains one-time-programmable eFuse bits that can be used to store permanent device information. The mz25key firmware defines custom eFuse fields in
EFUSE_BLK3, generated by the ESP-IDF efuse_table_gen.py tool from the CSV definition file (main/esp_efuse_custom_table.csv):
| Field | Block | Bits | Description |
|---|---|---|---|
| HARDWARE_REVISION | BLOCK3 | 0-7 | PCB hardware revision number (e.g. 0x12 = v1.2) |
| SERIAL_NO | BLOCK3 | 8-23 | Unique serial number for the device (16-bit) |
| DISABLE_RESTRICTIONS | BLOCK3 | 24-31 | Feature gate flags — each bit enables/disables a restricted feature |
| RESERVED1 | BLOCK3 | 32-47 | Reserved for future use |
| BUILD_DATE | BLOCK3 | 48-63 | Build date encoded as days since 2020-01-01 |
# Read all eFuse values espefuse.py --port /dev/ttyUSB0 summary # Write the hardware revision (WARNING: one-time-programmable, cannot be undone!) espefuse.py --port /dev/ttyUSB0 burn_efuse BLOCK3 0x12 --offset 0 --size 8 # Write the serial number espefuse.py --port /dev/ttyUSB0 burn_efuse BLOCK3 0x0042 --offset 8 --size 16
WARNING: eFuse bits are one-time-programmable. Once a bit is set to 1, it cannot be reverted to 0. Double-check all values before writing. The
DISABLE_RESTRICTIONS field allows different product SKUs to be built from the same firmware image by programming different eFuse patterns during manufacturing.
The eFuse table is generated during the CI/CD build. The
.gitignore excludes the generated C source file but includes the CSV definition, ensuring the field definitions are version-controlled while the generated code is always rebuilt from the CSV.
Feature gating via DISABLE_RESTRICTIONS: The
DISABLE_RESTRICTIONS field contains 8 bits, each controlling access to a specific feature. When a bit is 0 (default, unprogrammed), the feature is restricted; when set to 1, the feature is enabled. The firmware reads this field at boot and configures feature availability accordingly. This mechanism allows different product SKUs to be built from the same firmware image by programming different eFuse patterns during manufacturing.
Build Environment
Native Build Setup
To build the mz25key firmware natively, install ESP-IDF v4.4 following the Espressif Getting Started Guide:
# Install ESP-IDF v4.4 mkdir -p ~/esp cd ~/esp git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git esp-idf-v4.4 cd esp-idf-v4.4 ./install.sh # Activate the environment (needed in every new terminal session) . ~/esp/esp-idf-v4.4/export.sh
Clone and Build
CRITICAL: Unlike SharpKey, the mz25key repository does not use git submodules. You must manually clone the two component libraries at the correct versions before building. This is the most common source of build failures.
# Clone the mz25key repository (no --recursive needed) git clone https://git.eaw.app/eaw/mz25key.git cd mz25key # Clone required components manually (NOT submodules!) mkdir -p components # Arduino-ESP32 compatibility layer — must be exactly v2.0.3 git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32 cd components/arduino-esp32 && git checkout 2.0.3 && cd ../.. # LittleFS filesystem support — must be v1.3.1 WITH submodules initialised git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs cd components/esp_littlefs && git checkout v1.3.1 && git submodule update --init --recursive && cd ../.. # Configure the build target (select MZ25KEY_MZ2500 or MZ25KEY_MZ2800) idf.py menuconfig # Build the web filesystem (required before first firmware build) chmod +x build_webfs.sh && ./build_webfs.sh # Build the firmware idf.py build # Flash to the device idf.py -p /dev/ttyUSB0 flash # Flash and monitor in one command idf.py -p /dev/ttyUSB0 flash monitor
Build output files:
build/main.bin — Firmware binary (for OTA update) build/filesys.bin — Web filesystem image (for OTA update) build/bootloader/bootloader.bin — Bootloader build/partition_table/partition-table.bin — Partition table build/ota_data_initial.bin — OTA data partition
Menuconfig Options
Run
idf.py menuconfig to open the configuration editor. The key mz25key-specific options are under the SharpKey Configuration menu:
- Build Target: Select
mz25key - MZ2500ormz25key - MZ2800. This sets the Kconfig symbolMZ25KEY_MZ2500orMZ25KEY_MZ2800and determines the protocol timing, row count, and task priority. - PS/2 Keyboard GPIO: Configure the DATA and CLK pins for the PS/2 keyboard interface.
- Host Interface GPIO: Pin assignments for RTSN, KDB[3:0], KDO[7:0], KD4, MPX.
- WiFi Settings: Default SSID, password, AP mode channel.
- Bluetooth: Enable/disable Bluetooth HID stack.
- Debug Options: Enable serial debug output (
CONFIG_DEBUG_SERIAL), disable individual GPIO groups for testing.
A pre-configured
sdkconfig file is included in the repository with sensible defaults for the MZ-2500 target. You only need to run menuconfig if you want to change the target machine, pin assignments, or enable debug features.
Docker Build
An alternative to installing ESP-IDF natively is to use Espressif's official Docker image. This requires only Docker — the entire toolchain runs inside the container:
# Clone and prepare (same manual component cloning required)
git clone https://git.eaw.app/eaw/mz25key.git
cd mz25key
mkdir -p components
git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32
cd components/arduino-esp32 && git checkout 2.0.3 && cd ../..
git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs
cd components/esp_littlefs && git checkout v1.3.1 && git submodule update --init --recursive && cd ../..
# Build web filesystem
chmod +x build_webfs.sh && ./build_webfs.sh
# Build firmware using Docker
docker run --rm -v $(pwd):/project -w /project espressif/idf:v4.4 idf.py build
# Flash via Docker (requires USB device access)
docker run --rm --privileged \
--volume /dev:/dev --volume /sys:/sys:ro --volume /dev/bus/usb:/dev/bus/usb \
-v $(pwd):/project -w /project \
espressif/idf:v4.4 \
idf.py -p /dev/ttyUSB0 build flash monitor
For convenience, create a shell alias:
# Add to ~/.bashrc or ~/.zshrc
alias idf44='docker run --rm --privileged --volume /dev:/dev --volume /sys:/sys:ro \
--volume /dev/bus/usb:/dev/bus/usb -v $PWD:/project -w /project -it \
espressif/idf:v4.4 idf.py "$@"'
# Then use:
idf44 build
idf44 menuconfig
idf44 -p /dev/ttyUSB0 flash monitor
Continuous Integration (Jenkins)
What is CI/CD? Continuous Integration (CI) is a practice where a dedicated server automatically builds your project every time you push code changes. Instead of manually compiling the firmware and uploading release files by hand, a CI server clones the repository, builds the firmware, packages the release artifacts, and publishes them to a download page — all automatically. If the build breaks, you receive an email notification immediately.
The mz25key project uses Jenkins running on a VPS to automatically build the ESP32 firmware on every push to the
master branch. The build uses the Espressif IDF v4.4 Docker image, so no native ESP-IDF installation is needed on the server.
Pipeline Overview
The mz25key Jenkins pipeline is similar to SharpKey's but includes an additional "Fetch Components" stage. The pipeline stages are:
- Checkout: Clean workspace, clone repository from Gitea. Note: NO submodule init is performed (there are no submodules).
- Fetch Components: Clone
arduino-esp32at tag2.0.3andesp_littlefsat tagv1.3.1(with submodules) intocomponents/. This stage is unique to mz25key — SharpKey usesgit submodule update --init --recursiveinstead. - Determine Version: Auto-increment from the latest Gitea release tag, or read from
webserver/version.txt. - Build Web Filesystem: Run
build_webfs.shto package HTML, CSS, and JavaScript into the LittleFS image. - Generate eFuse Table: Run
efuse_table_gen.pyinside the IDF Docker container to generate the eFuse C source from the CSV definition. - Build Firmware: Launch the
espressif/idf:v4.4Docker container and runidf.py build. - Package Release: Create versioned FW, FilePack, and FlashPack archives.
- Create Gitea Release: Upload artifacts to Gitea as a tagged release.
Fetch Components Stage
This is the key CI/CD difference from SharpKey. Because the mz25key repository has no
.gitmodules, the pipeline must explicitly clone the component libraries:
stage('Fetch Components') {
steps {
sh """
mkdir -p components
git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32
cd components/arduino-esp32 && git checkout 2.0.3 && cd ../..
git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs
cd components/esp_littlefs && git checkout v1.3.1 \
&& git submodule update --init --recursive && cd ../..
"""
}
}
Critical note: The
esp_littlefs component requires its submodules to be initialised. Without running git submodule update --init --recursive inside components/esp_littlefs/, the mklittlefs tool will not be available and the build will fail with a "No rule to make target 'dist'" error during the web filesystem generation step.
Webhook Configuration
In the Gitea repository, go to Settings -> Webhooks -> Add Webhook -> Gitea and set:
- Target URL:
http://your-server:8080/generic-webhook-trigger/invoke?token=mz25key-build-trigger - Content Type:
application/json - Trigger On: Push Events
- Branch Filter:
refs/heads/(main|master)
After saving, push a commit to
master and check Jenkins — a new build should appear automatically.
Version Management
The mz25key uses the same version scheme as SharpKey but reads the version from a different location. The CI pipeline reads
webserver/version.txt (not version.txt in the project root as SharpKey does). Version auto-incrementing works by querying the latest Gitea release tag and bumping the minor version.
Release Artifacts
Three release artifacts are produced by each successful build:
mz25key-FW-vX.XX.bin.gz— Gzip-compressed firmware binary for OTA update via the web interface. Upload to the device through/ota.htmlas a "Firmware" update.mz25key-FilePack-vX.XX.bin.gz— Gzip-compressed LittleFS filesystem image for OTA update. Upload as a "Filesystem" update to update web pages, keymaps, and other web-served content.mz25key-FlashPack-vX.XX.tar.gz— Complete flash image archive containing bootloader, partition table, firmware, filesystem, and OTA data. Used for initial programming viaesptool.pyor for factory recovery.
Pipeline Configuration
The Jenkins server setup is shared with the FusionX and SharpKey projects. See the FusionX Developer's Guide — Continuous Integration section for the full Jenkins installation walkthrough including Docker, docker-compose.yml, initial configuration, and plugin installation.
The Jenkins
docker-compose.yml must mount the Docker socket so the pipeline can launch the ESP-IDF container as a sibling:
volumes: - /srv/jenkins/data:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock # Required for ESP-IDF sibling container
The pipeline uses Docker sibling containers (not Docker-in-Docker), so workspace paths must be mapped correctly. The
WORKSPACE_HOST_PATH environment variable provides the host-side path to the Jenkins workspace, which is passed to the ESP-IDF Docker container via -v. Post-build notifications are sent via email on both success and failure.
eFuse Table Generation
The "Generate eFuse Table" pipeline stage runs
efuse_table_gen.py inside the IDF Docker container to convert the CSV eFuse field definitions (main/esp_efuse_custom_table.csv) into a C source file (main/esp_efuse_custom_table.c). The .gitignore excludes the generated .c file but includes the .csv source, ensuring the definitions are version-controlled while the generated code is always rebuilt from the authoritative CSV during CI.
Differences from SharpKey CI Pipeline
The mz25key CI pipeline differs from SharpKey's in the following specific ways:
- Fetch Components stage: mz25key explicitly clones
arduino-esp32andesp_littlefsintocomponents/. SharpKey usesgit submodule update --init --recursiveduring the Checkout stage instead. - Generate eFuse Table stage: mz25key includes an additional pipeline stage that generates the eFuse configuration table from the CSV definition. The SharpKey pipeline handles this within the Build stage.
- Version file location: mz25key reads its version from
webserver/version.txt, whereas SharpKey reads fromversion.txtin the project root. - Artifact naming: Release artifacts use the
mz25key-prefix (e.g.mz25key-FW-v1.00.bin.gz) instead ofSharpKey-. - Webhook token: Uses
mz25key-build-trigger(notsharpkey-build-trigger). - No submodule init: The Checkout stage does not run any submodule commands because there are no submodules. SharpKey's Checkout stage runs
git submodule update --init --recursive.
Debugging
Debugging an embedded firmware that interfaces with vintage hardware at sub-microsecond timing can be challenging. The following techniques and tools help diagnose common issues.
Serial Monitor
Enable
CONFIG_DEBUG_SERIAL in menuconfig to activate verbose debug output over the UART. Connect a USB-to-TTL adapter to the programming header and run:
idf.py -p /dev/ttyUSB0 monitor
# Or with Docker:
docker run --rm --privileged --volume /dev:/dev --volume /sys:/sys:ro \
-v $PWD:/project -w /project -it espressif/idf:v4.4 \
idf.py -p /dev/ttyUSB0 monitor
The firmware uses the ESP-IDF logging framework. Key log tags include:
Tag Source Description ────────── ────────────────── ─────────────────────────────────────── sharpkey SharpKey.cpp Application-level events (init, host detection) MZ2528 MZ2528.cpp MZ-2500/MZ-2800 interface events HID HID.cpp Key event processing PS2 PS2KeyAdvanced.cpp PS/2 scan code reception BT BT.cpp Bluetooth discovery and pairing BTHID BTHID.cpp Bluetooth HID report translation WiFi WiFi.cpp WiFi and HTTP server events NVS NVS.cpp Configuration storage LED LED.cpp LED state changes SWITCH SWITCH.cpp Button press events
Set the log level per-tag to focus on specific subsystems:
esp_log_level_set("*", ESP_LOG_WARN); // Default: warnings and errors only
esp_log_level_set("MZ2528", ESP_LOG_DEBUG); // Verbose MZ2528 output
GPIO Debug Flags
The menuconfig debug menu provides options to selectively disable individual GPIO groups. This is invaluable for isolating problems through progressive testing — start with all groups disabled and enable them one at a time:
- CONFIG_DEBUG_DISABLE_KDB: Disables the keyboard bidirectional data bus (KD[3:0]). Use when testing host strobe detection without driving data back.
- CONFIG_DEBUG_DISABLE_KDO: Disables the keyboard data output (scan data to multiplexer). Use when testing input-side logic only.
- CONFIG_DEBUG_DISABLE_RTSNI: Disables the RTSN input monitoring. Use to test passive matrix updates without host polling.
- CONFIG_DEBUG_DISABLE_MPXI: Disables the MPX input monitoring. Use when debugging row strobe detection independently of nibble multiplexing.
- CONFIG_DEBUG_DISABLE_KDI: Disables the KD4 input. Use when testing without the type strobe signal.
Common Issues
- Components not cloned — "component not found" errors:
- The most common build failure. After cloning the mz25key repository, the
components/directory is empty. You must manually clonearduino-esp32(v2.0.3) andesp_littlefs(v1.3.1) as described in the Clone and Build section.
- The most common build failure. After cloning the mz25key repository, the
- esp_littlefs submodules missing — mklittlefs build fails:
- The
esp_littlefscomponent has its own git submodules (including themklittlefstool). If you forget to rungit submodule update --init --recursiveinsidecomponents/esp_littlefs/, the web filesystem build will fail with "No rule to make target 'dist'".
- The
- Timing issues — host not detecting keys:
- Ensure the MZ2528 interface is pinned to Core 1 with the spinlock. Any FreeRTOS scheduling on Core 1 will cause missed RTSN strobes.
- Verify all hot-path functions have
IRAM_ATTR— flash cache misses add hundreds of nanoseconds of latency. - Use a logic analyser to verify the RTSN response time is within the required window.
- ADC2/WiFi GPIO conflict:
- GPIOs 0, 2, 4, 12-15, 25-27 are shared with ADC2. Analog reads on these pins return garbage when WiFi is active. The mz25key PCB uses these pins for digital GPIO only.
- eFuse CSV not committed:
- The
.gitignoremay exclude*.cfiles in the eFuse directory. Ensure the CSV definition file (esp_efuse_custom_table.csv) is committed. The generated C file is rebuilt during CI.
- The
- MZ-2800 mode keys not working:
- Ensure the build target is
MZ25KEY_MZ2800(notMZ25KEY_MZ2500). The MZ-2800 has longer setup times (650ns for KDB vs 160ns) and an additional 15th row.
- Ensure the build target is
- PS/2 keyboard not detected:
- Check PS/2 DATA and CLK wiring. PS/2 uses open-collector signalling; 4.7K pull-up resistors to 5V are required.
- Some keyboards require a power cycle after ESP32 reset.
- WiFi will not connect:
- The ESP32 supports 2.4GHz only — it cannot connect to 5GHz networks.
- WiFi and Bluetooth are mutually exclusive. Disable BT before enabling WiFi.
- Bluetooth keyboard not pairing:
- Ensure WiFi is disabled (mutual exclusion).
- Put the keyboard into pairing mode before initiating the scan from the mz25key.
- Some keyboards require removing the existing pairing first.
- OTA update fails:
- Ensure the correct file type —
main.binfor firmware,filesys.binfor filesystem. - Version validation prevents downgrade — upload a newer version.
- Ensure the correct file type —
- ESP32 core panic / reboot loop:
- Usually a stack overflow or null pointer. The UART monitor shows the panic register dump. Increase stack sizes in menuconfig if needed.
Reference Sites
The following resources are useful for mz25key development:
| Resource | URL |
|---|---|
| mz25key Project Page | https://eaw.app/mz25key/ |
| mz25key Gitea Repository | https://git.eaw.app/eaw/mz25key |
| SharpKey Developer’s Guide | https://eaw.app/sharpkey-developersguide/ |
| SharpKey Gitea Repository | https://git.eaw.app/eaw/SharpKey |
| ESP-IDF v4.4 Documentation | https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/ |
| ESP-IDF Arduino Component | https://docs.espressif.com/projects/arduino-esp32/en/latest/esp-idf_component.html |
| esp_littlefs Component | https://github.com/joltwallet/esp_littlefs |
| PS/2 Scan Code Set 2 | OSDev Wiki — PS/2 Scan Code Set 2 |
| USB HID Usage Tables | USB.org — HID Usage Tables 1.4 |
| ESP32 Technical Reference | ESP32 Technical Reference Manual |
| FreeRTOS Documentation | FreeRTOS Reference Manual |
| FusionX CI/CD Setup Guide | Jenkins CI/CD Setup |
| Sharp MZ-2500 Technical Manual | Available from original.sharpmz.org |
| GPL v3 License | https://www.gnu.org/licenses/gpl-3.0.en.html |
Wireless Regulatory Notice
This device incorporates a pre-certified ESP32-S wireless module (AI Thinker) that transmits in the 2.4 GHz ISM band, making it an intentional radiator under radio-frequency regulations worldwide (including FCC Part 15 Subpart C in the United States, and the Radio Equipment Directive 2014/53/EU in the European Union).
The ESP32-S module is used unmodified and carries its own regulatory certifications (FCC, CE, and others). However, those module-level certifications do not automatically extend to a finished product that incorporates the module. The pre-certified module exemption permits individual hobbyists to build a limited number of devices for personal, experimental, or educational use without obtaining separate equipment authorisation.
Important Limitations
It is the builder’s sole responsibility to ensure that any device constructed from these designs complies with all applicable radio-frequency regulations in their jurisdiction. The author provides these designs for personal, educational, and hobbyist use and makes no representation that a device built from them satisfies the regulatory requirements for commercial distribution.
- Assembled devices must not be sold, offered for sale, gifted, or otherwise distributed to third parties unless the finished product has been independently tested and granted its own equipment authorisation (e.g. FCC ID, CE marking with a Notified Body assessment) in the relevant jurisdiction.
- Building this project for personal use in limited quantities is generally permitted under hobbyist and experimental-use provisions (e.g. FCC § 15.23), provided the device does not cause harmful interference.
- Regulatory requirements vary by country. Builders outside the United States should consult their national radio-frequency authority for applicable rules.
It is the builder’s sole responsibility to ensure that any device constructed from these designs complies with all applicable radio-frequency regulations in their jurisdiction. The author provides these designs for personal, educational, and hobbyist use and makes no representation that a device built from them satisfies the regulatory requirements for commercial distribution.