SharpKey マルチHIDインターフェース 開発者ガイド

概要

SharpKeyは、ESP32マイクロコントローラーをベースとしたマルチホストキーボードインターフェースです。最新のPS/2およびBluetoothキーボードをビンテージSharpおよびNECコンピュータのネイティブキーボードプロトコルに変換し、現代の入力デバイスをクラシックハードウェアで使用できるようにします。対応するホストマシンは以下の通りです:
  • Sharp MZ-2500 — SuperMZシリーズ
  • Sharp MZ-2800 — 16ビットSuperMZ
  • Sharp MZ-5500/5600/6500 — MZビジネスシリーズ
  • Sharp X1 — X1シリーズパーソナルコンピュータ
  • Sharp X68000 — 68000ベースパーソナルワークステーション
  • NEC PC-9801 — NEC 98アーキテクチャ
同じコードベースから、MZ-2500およびMZ-2800のみを対象としたシンプルなmz25keyバリアントもビルドでき、機能セットを縮小した小さなバイナリを生成します。
SharpKeyはデュアルコアのESP-32S AI Thinkerモジュールを中心に設計されています。Core 1はタイミングクリティカルなホストインターフェースに専用で割り当てられ、Core 0はFreeRTOSシステムタスク、WiFi、Bluetooth、およびHID入力処理を担当します。この分離により、ホスト側のナノ秒レベルのプロトコルタイミングがオペレーティングシステムのオーバーヘッドによって妨げられることはありません。
ファームウェアには、設定、キーマッピングのカスタマイズ、およびOTA(Over-The-Air)ファームウェアアップデート用の組み込みWiFi Webインターフェースが含まれています。Bluetooth HIDサポートにより、ワイヤレスキーボードやマウスをビンテージハードウェアで使用できます。
SharpKeyはGNU General Public License v3の下でライセンスされています。著者:Philip Smart。

前提条件

SharpKeyファームウェアのビルドおよび作業を開始する前に、以下のツールとコンポーネントが必要です:

ソフトウェア前提条件

  • ESP-IDF v4.4 — Espressif IoT Development Framework。ESP32ファミリーの公式開発フレームワークです。バージョン4.4が必要です。新しいバージョンではAPIに破壊的変更が導入される可能性があります。
  • Python 3.8+ — IDFツールチェーンのビルドスクリプト、menuconfig、およびeFuseテーブルジェネレーターに必要です。
  • Git — リポジトリのクローンおよびサブモジュール(arduino-esp32とesp_littlefs)の初期化に使用します。
  • Docker — オプション。Espressif IDF Dockerイメージ(espressif/idf:v4.4)は事前設定されたビルド環境を提供し、CI/CDパイプラインや再現可能なビルドに便利です。
  • LinuxまたはmacOS — 推奨ホストオペレーティングシステム。WindowsユーザーはIDFツールチェーンおよびシェルスクリプトとの完全な互換性のためにWSL2(Windows Subsystem for Linux 2)を使用してください。

ハードウェア前提条件

  • SharpKey PCB — ESP-32S AI Thinkerモジュールを搭載した、SharpKeyプロジェクト用に設計されたカスタムPCB。
  • PS/2キーボード — 有線キーボード入力テスト用。スキャンコードセット2を使用する標準的なPS/2キーボードに対応しています。
  • Bluetooth HIDキーボード — オプション。ワイヤレスキーボードサポートのテスト用。BT Classic HIDおよびBLE HIDデバイスの両方に対応しています。
  • USB-シリアルアダプター — ファームウェアの書き込みおよびシリアルモニターデバッグ用(Linuxでは通常/dev/ttyUSB0)。
  • 対象ビンテージマシン — エンドツーエンドテスト用の対応ホストマシンのいずれか。

リポジトリ構成

SharpKeyリポジトリは、Webインターフェースおよび外部コンポーネント用の追加ディレクトリを含む、標準的なESP-IDFプロジェクトレイアウトで構成されています。完全なディレクトリツリーは以下の通りです:
SharpKey/
├── main/
│   ├── SharpKey.cpp              — Entry point, initialisation, FreeRTOS task creation
│   ├── MZ2528.cpp                — MZ-2500/2800 host interface (Core 1, timing-critical)
│   ├── MZ5665.cpp                — MZ-5500/5600/6500 host interface
│   ├── X1.cpp                    — Sharp X1 host interface
│   ├── X68K.cpp                  — Sharp X68000 host interface
│   ├── PC9801.cpp                — NEC PC-9801 host interface
│   ├── HID.cpp                   — HID device manager (multiplexes PS/2 and BT)
│   ├── BTHID.cpp                 — Bluetooth HID keyboard/mouse support
│   ├── BT.cpp                    — Low-level Bluetooth stack management
│   ├── WiFi.cpp                  — WiFi AP/Client, web server, OTA updates, REST API
│   ├── PS2KeyAdvanced.cpp        — PS/2 keyboard protocol (Scan Code Set 2)
│   ├── PS2Mouse.cpp              — PS/2 mouse protocol handler
│   ├── Mouse.cpp                 — Mouse protocol abstraction
│   ├── KeyInterface.cpp          — Virtual base class for host interfaces
│   ├── LED.cpp                   — Status LED control
│   ├── NVS.cpp                   — Non-Volatile Storage (config persistence)
│   ├── SWITCH.cpp                — Configuration switch (WiFi/BT mode)
│   ├── esp_efuse_custom_table.c  — Custom eFuse field definitions (generated)
│   ├── esp_efuse_custom_table.csv — eFuse field CSV source
│   ├── Kconfig.projbuild         — Menuconfig options (~310 lines)
│   ├── CMakeLists.txt            — Component registration
│   └── include/
│       ├── KeyInterface.h        — Base class definition
│       ├── MZ2528.h              — MZ key matrix mapping (165+ entries)
│       ├── MZ5665.h              — MZ-5600 series mappings
│       ├── X1.h                  — X1 key mappings
│       ├── X68K.h                — X68000 key mappings
│       ├── PC9801.h              — PC-9801 key mappings
│       ├── HID.h                 — HID manager header
│       ├── BTHID.h               — Bluetooth HID header
│       ├── BT.h                  — Bluetooth stack header
│       ├── PS2KeyAdvanced.h      — PS/2 keyboard header
│       ├── PS2KeyCode.h          — PS/2 key code definitions
│       ├── PS2KeyTable.h         — PS/2 key lookup tables
│       ├── WiFi.h                — WiFi and web server header
│       ├── LED.h                 — LED control header
│       ├── NVS.h                 — NVS storage header
│       ├── SWITCH.h              — Mode switch header
│       ├── Mouse.h               — Mouse abstraction header
│       ├── PS2Mouse.h            — PS/2 mouse header
│       └── esp_efuse_custom_table.h  (generated from CSV)
├── webserver/
│   ├── index.html                — Main web UI dashboard
│   ├── keymap.html               — Key mapping editor
│   ├── mouse.html                — Mouse settings page
│   ├── ota.html                  — OTA firmware upload page
│   ├── wifimanager.html          — WiFi configuration page
│   ├── version.txt               — Firmware version string
│   ├── css/                      — Bootstrap and custom stylesheets
│   ├── js/                       — jQuery, Bootstrap, keymap and OTA scripts
│   └── font-awesome/             — Icon fonts for web UI
├── components/
│   ├── arduino-esp32/            — Arduino compatibility layer (submodule, v2.0.3)
│   └── esp_littlefs/             — LittleFS filesystem driver (submodule, v1.3.1)
├── build_webfs.sh                — Web filesystem build script
├── sharpkey_partition_table.csv  — Flash partition layout
├── sdkconfig                     — Current build configuration
├── version.txt                   — Release version
└── CMakeLists.txt                — Top-level project CMake configuration
main/ディレクトリにはすべてのファームウェアソースコードが含まれています。各ホストインターフェースは、キーマッピングテーブルを含む独自のヘッダーファイルを持つ個別のコンパイル単位として実装されています。webserver/ディレクトリには、LittleFSファイルシステムイメージにコンパイルされるWebインターフェースアセットが格納されています。components/ディレクトリには、Arduinoコンパティビリティとファイルシステムサポートを提供する2つのGitサブモジュールが含まれています。

ソフトウェアアーキテクチャ

SharpKeyファームウェアは、FreeRTOS上で動作するレイヤードアーキテクチャとして構成されており、ビンテージキーボードプロトコルが要求するタイミング精度を達成するためにESP32デュアルコアプロセッサの両方のコアを活用しています。

アーキテクチャ概要

アーキテクチャは3つの異なるレイヤーに分かれています:
  • 入力レイヤー — PS/2キーボードおよびマウスプロトコル(PS2KeyAdvanced、PS2Mouse)とBluetooth HIDデバイス(BTHID、BT)を処理します。これらのコンポーネントはFreeRTOSスケジューラーおよびWiFi/BTスタックとともにCore 0で動作します。
  • 処理レイヤー — HID抽象化レイヤー(HID.cpp)がPS/2およびBluetooth入力ソースを統合キーイベントストリームにマルチプレクスします。キーマッピングテーブルがHIDスキャンコードをホスト固有のマトリクスポジションに変換します。
  • 出力レイヤー — ホスト固有のインターフェース実装(MZ2528、MZ5665、X1、X68K、PC9801)がマッピングされたキーイベントを各ビンテージマシンが期待する電気信号に変換します。これらはリアルタイム優先度でCore 1上で動作します。
KeyInterface仮想基底クラスは、すべてのホストインターフェース実装が満たすべき契約を定義します。各ホストマシンにはKeyInterfaceを継承する具象実装があり、シングルトンとしてインスタンス化されます。ブート時にSharpKey.cppがGPIO設定ラインを読み取って接続されているホストを検出し、適切なインターフェースクラスをインスタンス化します。
┌─────────────────────────────────────────────────────────────────┐
│                        Input Layer (Core 0)                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐  │
│  │ PS2KeyAdvanced│  │  PS2Mouse    │  │  BTHID / BT          │  │
│  │ (Scan Code 2) │  │ (3-button)   │  │  (Classic + BLE)     │  │
│  └──────┬───────┘  └──────┬───────┘  └──────────┬───────────┘  │
│         │                 │                      │              │
│  ┌──────┴─────────────────┴──────────────────────┴───────────┐  │
│  │                    HID.cpp (Multiplexer)                   │  │
│  └────────────────────────────┬──────────────────────────────┘  │
│                               │                                 │
├───────────────────────────────┼─────────────────────────────────┤
│                     Processing Layer                            │
│  ┌────────────────────────────┴──────────────────────────────┐  │
│  │              Key Mapping Tables (t_keyMap[])               │  │
│  │         PS/2 scan code → host matrix row/column            │  │
│  └────────────────────────────┬──────────────────────────────┘  │
│                               │                                 │
├───────────────────────────────┼─────────────────────────────────┤
│                       Output Layer (Core 1)                     │
│  ┌─────────┐ ┌─────────┐ ┌────┐ ┌──────┐ ┌────────┐          │
│  │ MZ2528  │ │ MZ5665  │ │ X1 │ │ X68K │ │ PC9801 │          │
│  └─────────┘ └─────────┘ └────┘ └──────┘ └────────┘          │
│         GPIO register writes → Host machine connector          │
└─────────────────────────────────────────────────────────────────┘

FreeRTOSタスクモデル

SharpKeyは特定のコアに固定された専用FreeRTOSタスクを作成します。タスク割り当ては、タイミングクリティカルなホストインターフェースをCore 1に隔離し、WiFi、Bluetooth、またはFreeRTOSシステムタスクによる中断を受けないように設計されています。これらはすべてCore 0で動作します。
タスク コア 優先度 スタック(バイト) 用途
mz25Interface Core 1 25 4096 MZ-2500タイミングクリティカルGPIOループ(スピンロック保護)
mz28Interface Core 1 MAX-1 2048 MZ-2800タイミングクリティカルGPIOループ
hidInterface Core 0 0 4096 HID入力ポーリング(PS/2スキャン+Bluetoothイベント)
WiFi/BTタスク Core 0 各種 ESP-IDF管理システムタスク
Core 1はホストインターフェースタスクに排他的に割り当てられます。このタスクはホストからの行選択信号を読み取り、列データを返すタイトループで動作し、MZ-2800プロトコルでは650ナノ秒という厳しいタイミング要件があります。portMUX_TYPEスピンロックが、Core 0のHIDタスクによって書き込まれ、Core 1のホストインターフェースタスクによって読み取られる共有キーマトリクスデータ構造を保護します。
Core 0上のHIDインターフェースタスクは最低優先度で動作し、PS/2およびBluetooth入力をポーリングしてキーマトリクスを更新します。このタスクはWiFiおよびBluetoothスタックとCore 0を共有するため、協調スケジューリングを使用し、リアルタイム保証を必要としません — キーマトリクスは単に現在のキー状態のスナップショットであり、ホストがリクエストした際にCore 1のタスクが読み取ります。

KeyInterface基底クラス

KeyInterfaceクラスは、すべてのホストインターフェース実装の契約を定義する抽象基底クラスです。main/include/KeyInterface.hで定義され、main/KeyInterface.cppで実装されています。クラス階層は以下の通りです:
KeyInterface (abstract base)
├── MZ2528     — Sharp MZ-2500 and MZ-2800
├── MZ5665     — Sharp MZ-5500, MZ-5600, MZ-6500
├── X1         — Sharp X1 series
├── X68K       — Sharp X68000
└── PC9801     — NEC PC-9801
各ホストインターフェースが実装すべき主要な仮想メソッドは以下の通りです:
  • init() — GPIOピンの初期化、割り込みの設定、キーマトリクスのセットアップ、およびCore 1に固定されたFreeRTOSタスクの作成。ホスト検出後の起動時に一度だけ呼び出されます。
  • mapKey(uint16_t scanCode, uint8_t keyState) — 受信したPS/2スキャンコード(修飾キーフラグ付き)をホストキーマトリクスにマッピング。このメソッドはt_keyMap[]テーブルでスキャンコードを検索し、keyMatrix[]配列の適切なビットをセットまたはクリアします。
  • createKeyMapFile() — 再起動をまたいで永続化するため、現在のキーマッピングテーブルをNVSストレージにシリアライズ。Webインターフェースのキーマッピングエディターで使用されます。
  • getKeyMapData() — Webインターフェース用にJSON構造として現在のキーマッピングデータを返します。
  • suspend() — ホストインターフェースタスクを一時停止。WiFiとBluetoothモードの切り替え時やOTAアップデート中に使用されます。
  • resume() — 一時停止されたホストインターフェースタスクを再開します。
基底クラスは、タイミングクリティカルでないセクションで使用するためのvTaskDelay(1)をラップするyield()インラインメソッド、およびLED制御、NVSアクセス、WiFi状態管理用のユーティリティメソッドも提供します。各ホストインターフェースはシングルトンとしてインスタンス化されます — ブート時のホスト検出ロジックによって決定され、常に1つのインターフェースのみがアクティブです。

ホスト検出

起動時にSharpKey.cppは一連のGPIO設定ラインを読み取り、SharpKeyインターフェースに接続されているホストマシンを判別します。検出ロジックはホストコネクタ間で異なる特定のピンの電気的状態を検査します — 各ビンテージマシンはコネクタ配線とアクティブ信号の違いにより、これらのライン上に固有のシグネチャを示します。
ホストが特定されると、SharpKeyは適切なインターフェースクラス(例:MZ-2500の場合はMZ2528、X68000の場合はX68K)をインスタンス化し、そのinit()メソッドを呼び出します。menuconfigでビルドターゲットがMZ25KEY_MZ2500またはMZ25KEY_MZ2800に設定されている場合、検出ステップはスキップされ、対応するインターフェースが直接インスタンス化されます。これによりシングルターゲットビルドのコードサイズが削減されます。
機能セキュリティeFuseビットにより、実行時に利用可能なインターフェースをさらに制限できます。eFuseでENABLE_FEATURE_SECURITYが設定されている場合、GPIO検出がホストを特定しても、対応する有効化ビットがプログラムされているインターフェースのみがインスタンス化されます。詳細はeFuseカスタムフィールドセクションを参照してください。

キーマッピングシステム

キーマッピングシステムはSharpKeyの機能の中核です。最新のキーボードスキャンコードをビンテージホストマシンが期待するマトリクスポジションに変換し、修飾キー、マルチキーの組み合わせ、およびキーボードモデル固有のバリエーションを処理します。

スキャンコードパイプライン

キーイベントパイプラインはいくつかのステージを通じて入力を処理します:
PS/2 Keyboard
    │
    ▼
PS2KeyAdvanced (Scan Code Set 2 → ASCII + modifier flags)
    │
    ▼
HID.cpp (multiplex with Bluetooth input)
    │
    ▼
KeyInterface::mapKey() (lookup in t_keyMap[] table)
    │
    ▼
keyMatrix[16] (set/clear matrix bits)
    │
    ▼
keyMatrixAsGPIO[16] (pre-shifted for GPIO registers)
    │
    ▼
GPIO.out_w1ts / GPIO.out_w1tc (atomic register writes to host)
PS2KeyAdvancedライブラリは低レベルのPS/2プロトコルを処理し、スキャンコードセット2のシーケンス(E0プレフィックス付きの拡張キーやF0プレフィックス付きのブレイクコードを含む)をデコードして、下位バイトにASCIIキーコード、上位バイトに修飾キーフラグ(SHIFT、CTRL、ALT、GUI、CAPS、NUM、SCROLL)を含む16ビット値にします。
この16ビット値はアクティブなホストインターフェースのmapKey()メソッドに渡され、t_keyMap[]テーブルで一致するエントリを検索します。一致が見つかると、keyMatrix[]配列内の対応するマトリクス行および列ビットがセット(キー押下時)またはクリア(キーリリース時)されます。

マッピングテーブル構造

各ホストインターフェースのヘッダーファイル(例:MZ2528.h)には、キーマッピングエントリのt_keyMap[]配列が含まれています。テーブルの各エントリは以下のフィールドを持ちます:
フィールド 説明
ps2KeyCode uint8_t PS2KeyAdvancedからのASCIIキーコード(スキャン結果の下位バイト)
ps2Ctrl uint8_t 修飾キーフラグマスク(SHIFT、CTRL、ALT、CAPSなど)
keyboardModel uint8_t PS/2キーボードモデル識別子(0 = 全モデル)
machine uint8_t 対象マシン(MZ_ALL、MZ_80B、MZ_2000、MZ_2500、MZ_2800)
MK_ROW1 uint8_t Makeキー1:マトリクス行番号
MK_KEY1 uint8_t Makeキー1:列ビットポジション
MK_ROW2 uint8_t Makeキー2:マトリクス行番号(同時押しキー用)
MK_KEY2 uint8_t Makeキー2:列ビットポジション
MK_ROW3 uint8_t Makeキー3:マトリクス行番号(3キー同時押しコンボ用)
MK_KEY3 uint8_t Makeキー3:列ビットポジション
BRK_ROW1 uint8_t Breakキー1:リリースするマトリクス行
BRK_KEY1 uint8_t Breakキー1:リリースする列ビット
BRK_ROW2 uint8_t Breakキー2:リリースするマトリクス行
BRK_KEY2 uint8_t Breakキー2:リリースする列ビット
Make(MK)フィールドは最大3つの同時キー押下をサポートし、単一のPS/2キーでホスト上のマルチキーの組み合わせを生成できます。例えば、ホスト上でSHIFT+キーが必要な文字は、MK_ROW1/KEY1をSHIFTマトリクスポジションに、MK_ROW2/KEY2を文字ポジションに設定することで、シフトなしのPS/2キーから生成できます。Break(BRK)フィールドは最大2つの同時キーリリースをサポートします。
MZ-2500/2800は16行×8列のキーマトリクス(16行、行あたり8列ビット)を使用します。キーグループのマトリクスレイアウトは以下の通りです:
キーグループ
0 F1, F2, F3, F4, F5, F6, F7, F8
1 F9, F10, KP_*, KP_+, KP_=, HELP, COPY, ARGO
2 CLR, HOME, INST, DEL, BS, 前行, ESC, SCROLL
3 BREAK, RIGHT, LEFT, DOWN, UP, RETURN, SPACE, TAB
4-7 アルファベットA-Z(4行に分散)
8-9 数字0-9、マイナス、キャレット、円記号、アットマーク
10 括弧、セミコロン、コロン、カンマ、ピリオド、スラッシュ、アンダースコア
11 CTRL, KANA, SHIFT(左), LOCK, GRAPH, — , SHIFT(右), —
12-13 テンキー0-9、KP_period、KP_comma、KP_minus、KP_enter
14-15 拡張ファンクションキー、追加特殊キー

対応キーボードモデル

SharpKeyは複数のPS/2キーボードモデルをサポートしており、各モデルはマッピングテーブル内のモデル番号で識別されます。これにより、キーレイアウトやスキャンコード割り当てのバリエーションに対応するキーボードモデル固有のマッピングが可能です。keyboardModel値が0の場合、そのマッピングはすべてのモデルに適用されます。
モデルID キーボード 備考
0 全機種/汎用 任意のPS/2キーボード用デフォルトマッピング
1 Wyse KB3926 Wyseターミナルキーボード、コンパクトレイアウト
2 OADG109 日本語109キーレイアウト(JIS)
3 Sanwa SKBL1 サンワサプライバックライトキーボード
4 Periboard 810 Perixxワイヤレス(PS/2アダプター付き)
5 Omoton K8508 コンパクトBluetoothキーボード
6 Dell KB212-B 標準Dell USBキーボード(PS/2アダプター付き)
7 Logitech K120 一般的なLogitech有線キーボード
8 Cherry G80 Cherryメカニカルキーボードシリーズ
キーボードモデルは可能な場合自動検出されます(PS/2キーボードIDコマンドを使用)。または、Webインターフェースを通じて手動で設定できます。キーボードモデルが一致する場合、モデル固有のマッピングが汎用マッピングを上書きします。

ホストへのマトリクス出力

キーマトリクスは2つの並列配列として管理されています:
  • keyMatrix[16] — 各要素がマトリクスの1行を表すuint8_t配列。各ビットが列に対応し、ビットセット=キー押下、ビットクリア=キーリリースです。
  • keyMatrixAsGPIO[16] — 8列ビットがハードウェアが期待するGPIO出力ポジションにシフト済みの事前計算uint32_t配列。これにより、タイミングクリティカルな出力ループでのビットシフトが不要になります。
ホストがマトリクス行をスキャンする(行選択ラインKDB0-KDB3を駆動する)と、インターフェースタスクが選択された行番号を読み取り、keyMatrixAsGPIO[row]を参照し、アトミックなセット/クリア操作を使用してESP32のGPIO出力レジスタに値を直接書き込みます:
// Clear all column output bits
GPIO.out_w1tc = KDO_ALL_MASK;

// Set the bits for pressed keys in the selected row
GPIO.out_w1ts = keyMatrixAsGPIO[selectedRow];
out_w1ts(write-1-to-set)およびout_w1tc(write-1-to-clear)レジスタは、リード・モディファイ・ライトサイクルなしでのアトミックなビット操作を提供します。これはMZ-2500/2800プロトコルのサブマイクロ秒タイミング要件を満たすために不可欠です。

新しいホストインターフェースの追加

SharpKeyの重要な設計目標の一つは拡張性です。現在サポートされていないレトロコンピュータにSharpKeyを接続したい場合、 KeyInterfaceを継承するクラスのソースファイルペアを作成し、ホストのキーボードコネクタをSharpKey PCBに配線し、 検出ロジックに新しいホストを登録することで、新しいホストインターフェースを追加できます。このセクションでは、電気的要件とソフトウェア手順の両方を詳しく説明します。

電気的要件

SharpKey PCBはESP-32S AI Thinkerモジュールを使用しており、そのGPIOピンは74HCT257クワッド2-to-1マルチプレクサを介してホストコネクタに引き出されています。利用可能な信号とその電気的特性を以下にまとめます。新しいホストインターフェースはこれらの制約の範囲内で動作する必要があります。
利用可能なI/Oライン
ホストコネクタは以下のGPIOグループを公開しています。すべてのアクティブピンはESP32に直接接続されており、3.3Vロジックレベルで動作します。ターゲットホストが5V TTLロジックを使用する場合、既存の74HCT257レベルシフト回路が変換を処理します(HCT入力は3.3VをロジックHIGHとして受け入れ、出力は5Vトレラントです)。異なる電圧要件のホストの場合、ホストケーブルに外部レベルシフタを追加する必要があるかもしれません。
Group Signals Default GPIOs Direction Description
KDO[7:0] 8 data lines 14, 15, 16, 17, 18, 19, 21, 22 Output プライマリデータバス。既存のホストではESP32が駆動するキーマトリクスの列データを伝送します。アクティブローの規則:’1’ = キーリリース、’0’ = キー押下。サブマイクロ秒のアトミック書き込みのためにGPIO.out_w1ts / GPIO.out_w1tcに直接マッピングされます。
KDB[3:0] 4 address lines 23, 25, 26, 27 Input (default) / Output 既存ホストでの行選択入力(ホストが4ビット行番号を送信)。双方向バスが必要なホスト向けにreconfigADC2Ports()で出力として再構成可能です。X1インターフェースではループバック検出のために一時的に出力モードに設定されます。
RTSNI 1 strobe line 35 Input only ホストからのアクティブローストローブ。GPIO 35はESP32の入力専用バンク(GPIO_IN1_REG、ビット3)にあります。MZ-2500/2800では行転送ストローブとして使用されます。他のホストでは未使用の場合があります(ケーブル上でHIGHまたはLOWに直接接続)。
MPXI 1 multiplex line 12 Input ホストからのマルチプレクス/モード選択。MZマシンではMZ-2500とMZ-2800のタイミングを区別するために使用されます。新しいホストでは任意の目的に使用可能です。
KDI4 1 extra input 13 Input 補助データ入力。MZホストでは単一行モードとSTROBEALLモードの区別に使用されます。任意の目的に使用可能です。
PS/2 DATA 1 bidirectional 14 Bidirectional PS/2キーボードデータライン(mini-DINコネクタに直接接続)。
PS/2 CLK 1 interrupt-capable 13 Bidirectional PS/2キーボードクロックライン。Scan Code Set 2の受信のために割り込み対応が必要です。
PWRLED 1 output 25 Output 電源/ステータスLED。
WIFI_EN 1 input 34 Input only WiFi有効化スイッチ(アクティブロー、GPIO 34は入力専用)。
重要:一部のGPIOピンはグループ間で共有されていることに注意してください。GPIO 14はPS2_HW_DATAPINHOST_KDO0の両方のデフォルトです。GPIO 13はPS2_HW_CLKPINHOST_KDI4の間で共有されます。GPIO 25は HOST_KDB1PWRLEDの間で共有されます。実際にはPS/2とホストインターフェースは異なるタイミングで動作し (PS/2入力はCore 0で処理、ホスト出力はCore 1で処理)、KeyInterface::reconfigADC2Ports()メソッドが 必要に応じてGPIOの入出力方向の切り替えを処理します(特にADC2対応のGPIO 0, 2, 4, 12, 13, 14, 15, 25, 26, 27と競合するWiFiのため)。
電気信号バジェット
合計で、SharpKey PCBはホストインターフェース用に最大15本のGPIOラインを提供します(8 × KDO、4 × KDB、RTSNI、 MPXI、KDI4)。これは事実上すべてのビンテージキーボードマトリクスプロトコルに十分です:
  • マトリクススキャンキーボード(最も一般的)— ホストが行アドレス(通常4-6ビット)を送信し、列データ(通常8ビット)を読み返します。これはKDB[3:0]を行入力、KDO[7:0]を列出力として直接マッピングされます。MZ-2500/2800、X1、PC-9801はすべてこのパターンを使用します。
  • シリアルキーボード — 一部のホスト(例:特定のMSXマシン、初期のApple)はシリアルプロトコルを使用します。任意の単一GPIOをシリアルデータラインとして、別のGPIOをクロックとして使用できます。KDOまたはKDBラインは個別にアドレス指定できます。
  • アクティブ出力キーボード — X68000のようなホストは、キーボードがマトリクスを提示するのではなく、キーダウン/キーアップスキャンコードを能動的に送信することを期待します。SharpKeyはこのモデルをサポートしています — X68KインターフェースはKDOラインをシリアルトランスミッタとして使用し、KDB入力は完全に無視します。
  • 混合プロトコル — ホストがマトリクススキャンと追加の制御信号(例:CAPS LOCK LEDフィードバック、キーボードリセット)の両方を使用する場合、MPXI、KDI4、RTSNIラインを再利用できます。すべてのGPIO割り当てはKconfigで設定可能です。
ホストケーブルの設計
各ホストマシンには、SharpKeyホストコネクタのピンをターゲットコンピュータのキーボードコネクタにマッピングする専用ケーブルが必要です。新しいケーブルを設計する際の注意点:
  1. ホストのサービスマニュアルから、またはオリジナルキーボードのリバースエンジニアリングによって、キーボードコネクタのピン配列を入手します。
  2. どの信号が行選択(アドレス)で、どれが列データ(キー状態)で、どれが制御/ストローブ信号かを特定します。
  3. ホスト信号を最も適切なSharpKey GPIOグループにマッピングします(出力にはKDO、入力にはKDB、ストローブおよび制御信号にはRTSNI/MPXI/KDI4)。
  4. ホストがSharpKeyのアクティブハイと反対のアクティブロー信号を使用する場合(またはその逆)、ケーブルにインバータが必要な場合があります — ただしほとんどの場合、ソフトウェアで極性反転を処理できます。
  5. ホストが5V出力ドライブを必要とし(SharpKeyの74HCT257経由の3.3Vでは不十分な場合)、ケーブルPCBまたはインラインアダプタにバッファIC(例:74HCT245)を追加します。
  6. 電源とグランドが接続されていることを確認します。SharpKeyはキーボードコネクタで利用可能なホストの5V電源から給電するか、USBプログラミングポート経由で独立して給電できます。
既存のケーブル設計(MZ-2500、MZ-2800、X1、X68000、マウス)の詳細な例については、 テクニカルガイド — ホストインターフェースケーブルのセクションを参照してください。

ソフトウェア実装

ソフトウェア側では、KeyInterfaceを継承する新しいクラスの作成、キーマッピングテーブルの定義、ホスト側プロトコルの実装、 および検出・インスタンス化ロジックへの新しいホストの登録が必要です。以下の手順でプロセス全体を説明します。

ステップ1 — ヘッダーファイルの作成
main/include/NewHost.hを作成します。このファイルにはクラス定義、制御構造体、定数、およびPS/2からホストへのキーマッピングテーブルを定義します。テンプレートとして既存のヘッダー(例:MZ2528.hまたはX1.h)を参考にしてください。主な要素は以下の通りです:
  • クラス宣言KeyInterfaceを継承
  • マトリクスの次元 — ターゲットホストのキーボードマトリクスの行数と列数を定義
  • 制御構造体t_newHostControl)— キーマトリクス配列、GPIOビットマスク、モードフラグ、およびプロトコル固有の状態を保持
  • キーマッピングテーブルt_keyMap keyMap[])— 各PS/2キーコード+修飾キーの組み合わせを1つ以上のホストマトリクス行/列位置にマッピング。各エントリは最大3つの同時「make」キーと2つの「break」キーをエンコード可能
  • PS/2制御フラグ — 修飾キーマッチング用の標準PS2CTRL_*定数(SHIFT、CTRL、ALT、CAPSなど)を使用
  • マシンモデルタグ — ホストに異なるキーレイアウトのサブモデルがある場合、モデル定数を定義 (MZ2528.hのMZ_80BMZ_2000MZ_2500MZ_2800に類似)
// NewHost.h — Header for a hypothetical new host interface
#ifndef NEWHOST_H
#define NEWHOST_H

#include "KeyInterface.h"
#include "NVS.h"
#include "LED.h"
#include "HID.h"
#include <vector>
#include <map>

class NewHost : public KeyInterface {

    // Constants
    #define NEWHOST_VERSION             1.00
    #define NEWHOST_KEYMAP_FILE         "NewHost_KeyMap.BIN"
    #define NEWHOST_MATRIX_ROWS         12       // Number of rows in the host key matrix
    #define NEWHOST_MATRIX_COLS         8        // Number of columns (bits per row)

    // PS/2 → Host mapping table dimensions
    #define PS2TBL_NEWHOST_MAXROWS      128      // Maximum entries in the mapping table
    #define PS2TBL_NEWHOST_MAX_MKROW    3        // Max simultaneous make keys per entry
    #define PS2TBL_NEWHOST_MAX_BRKROW   2        // Max simultaneous break keys per entry

    // Machine model tags (if the host has sub-models)
    #define NEWHOST_ALL                 0xFF
    #define NEWHOST_MODEL_A             0x01
    #define NEWHOST_MODEL_B             0x02

    // The PS/2 to host mapping table type (same structure as other interfaces)
    typedef struct {
        uint16_t    ps2KeyCode;                                  // PS/2 key code from PS2KeyAdvanced
        uint8_t     ps2Ctrl;                                     // Modifier flags (PS2CTRL_SHIFT, etc.)
        uint8_t     keyboardModel;                               // 0 = all keyboards, or specific model ID
        uint8_t     machine;                                     // NEWHOST_ALL or specific model
        struct {
            uint8_t row;
            uint8_t col;                                         // Bitmask: bit position in the row
        }           mk[PS2TBL_NEWHOST_MAX_MKROW];               // Make (key press) targets
        struct {
            uint8_t row;
            uint8_t col;
        }           brk[PS2TBL_NEWHOST_MAX_BRKROW];             // Break (key release) targets
    } t_newHostKeyMap;

    // Control structure holding all runtime state
    typedef struct {
        uint8_t     keyMatrix[NEWHOST_MATRIX_ROWS];              // Virtual key matrix (1 = released, 0 = pressed)
        uint32_t    keyMatrixAsGPIO[NEWHOST_MATRIX_ROWS];        // Pre-shifted GPIO bitmasks for atomic writes
        uint8_t     strobeAll;                                   // AND of all rows (used by some protocols)
        uint32_t    strobeAllAsGPIO;
        bool        noKeyPressed;
    } t_newHostControl;

    public:
        // Constructors matching the KeyInterface pattern
                     NewHost(void) : KeyInterface() {};
                     NewHost(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char *fsPath);
                     NewHost(NVS *hdlNVS, HID *hdlHID);
                    ~NewHost();

        // Overridden virtual methods
        void         init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID);
        void         init(NVS *hdlNVS, HID *hdlHID);
        IRAM_ATTR uint32_t mapKey(uint16_t scanCode);

        // Key map file I/O (for web interface persistence)
        bool         createKeyMapFile(std::fstream &outFile);
        bool         getKeyMapData(std::vector<uint32_t>& dataArray, int *row, bool start);
        void         getKeyMapHeaders(std::vector<std::string>& headerList);
        std::string  getKeyMapFileName(void);

        // FreeRTOS task functions — static so they can be passed to xTaskCreatePinnedToCore
        static void IRAM_ATTR hostInterfaceTask(void *pvParameters);
        static void           hidInterface(void *pvParameters);

    private:
        t_newHostControl      hostCtrl;
        TaskHandle_t          TaskHostIF = NULL;
        TaskHandle_t          TaskHIDIF  = NULL;
        portMUX_TYPE          hostMux    = portMUX_INITIALIZER_UNLOCKED;
};

// ─── Key Mapping Table ──────────────────────────────────────────────────────
// Each row maps one PS/2 key (+ optional modifier) to host matrix position(s).
// Fields: ps2KeyCode, ps2Ctrl, kbModel, machine,
//         mk[0].row, mk[0].col, mk[1].row, mk[1].col, mk[2].row, mk[2].col,
//         brk[0].row, brk[0].col, brk[1].row, brk[1].col
//
// Example: map PS/2 'A' (0x1C) to host row 4, column bit 0
static const t_newHostKeyMap newHostKeyMap[] = {
    { PS2_KEY_A,     PS2CTRL_NONE, 0, NEWHOST_ALL, {{4,0x01},{0,0},{0,0}}, {{4,0x01},{0,0}} },
    { PS2_KEY_B,     PS2CTRL_NONE, 0, NEWHOST_ALL, {{4,0x02},{0,0},{0,0}}, {{4,0x02},{0,0}} },
    { PS2_KEY_ENTER, PS2CTRL_NONE, 0, NEWHOST_ALL, {{3,0x04},{0,0},{0,0}}, {{3,0x04},{0,0}} },
    // ... complete mapping for all host keys ...
};

#endif // NEWHOST_H

ステップ2 — ソースファイルの作成
main/NewHost.cppを作成します。このファイルは3つのコア機能を実装します:初期化(GPIOセットアップ+タスク作成)、キーマッピング(PS/2→ホストマトリクス変換)、およびホスト側インターフェースタスク(タイミングクリティカルなGPIOループ)。マトリクススキャンホストについてはMZ2528.cpp(1244行)、出力専用シリアルホストについてはX1.cpp(1124行)を参考にしてください。
// NewHost.cpp — Implementation of a new host interface
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "soc/gpio_struct.h"
#include "soc/gpio_reg.h"
#include "NewHost.h"

#define MAINTAG "NewHost"

// ─── Constructor (with hardware) ────────────────────────────────────────────
NewHost::NewHost(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID, const char *fsPath)
{
    init(ifMode, hdlNVS, hdlLED, hdlHID);
}

// ─── Constructor (without hardware, for WiFi probing) ───────────────────────
NewHost::NewHost(NVS *hdlNVS, HID *hdlHID)
{
    init(hdlNVS, hdlHID);
}

// ─── Destructor ─────────────────────────────────────────────────────────────
NewHost::~NewHost() { }

// ─── init() with hardware ───────────────────────────────────────────────────
// ホストが検出された際に呼び出されます。GPIOを設定しFreeRTOSタスクを起動します。
void NewHost::init(uint32_t ifMode, NVS *hdlNVS, LED *hdlLED, HID *hdlHID)
{
    // 制御変数を初期化します(キーマトリクスを全キーリリース状態に)。
    init(hdlNVS, hdlHID);

    // 基底クラスのinitを呼び出します(NVS、LED、HIDハンドルを格納し、LEDを点灯)。
    KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlLED, hdlHID, ifMode);

    // ── GPIO設定 ──────────────────────────────────────────────────────
    // 基底クラスKeyInterface::reconfigADC2Ports()はすでにKDB[3:0]を入力、
    // KDI4/MPXIを入力、RTSNIを入力として設定しています。
    // ここではホスト固有のGPIO要件を設定します:
    gpio_config_t io_conf = {};

    // KDO[7:0]を出力として設定(ESP32が直接駆動)
    io_conf.intr_type    = GPIO_INTR_DISABLE;
    io_conf.mode         = GPIO_MODE_OUTPUT;
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
    io_conf.pull_up_en   = GPIO_PULLUP_DISABLE;
    for(int pin : {CONFIG_HOST_KDO0, CONFIG_HOST_KDO1, CONFIG_HOST_KDO2, CONFIG_HOST_KDO3,
                   CONFIG_HOST_KDO4, CONFIG_HOST_KDO5, CONFIG_HOST_KDO6, CONFIG_HOST_KDO7})
    {
        io_conf.pin_bit_mask = (1ULL << pin);
        gpio_config(&io_conf);
    }

    // すべてのKDO出力をHIGHに設定(全キーリリース)
    GPIO.out_w1ts = (1 << CONFIG_HOST_KDO7) | (1 << CONFIG_HOST_KDO6) |
                    (1 << CONFIG_HOST_KDO5) | (1 << CONFIG_HOST_KDO4) |
                    (1 << CONFIG_HOST_KDO3) | (1 << CONFIG_HOST_KDO2) |
                    (1 << CONFIG_HOST_KDO1) | (1 << CONFIG_HOST_KDO0);

    // ── FreeRTOSタスクの作成 ──────────────────────────────────────────
    // ホストインターフェースタスクをCore 1で実行(タイミングクリティカル)
    ESP_LOGW(MAINTAG, "Starting NewHost interface thread on Core 1...");
    ::xTaskCreatePinnedToCore(&NewHost::hostInterfaceTask, "newHostIf",
                              4096, this, 25, &this->TaskHostIF, 1);
    vTaskDelay(500);

    // HID入力ポーリングタスクをCore 0で実行
    ESP_LOGW(MAINTAG, "Starting HID interface thread on Core 0...");
    ::xTaskCreatePinnedToCore(&NewHost::hidInterface, "hidIf",
                              4096, this, 0, &this->TaskHIDIF, 0);
}

// ─── init() without hardware (WiFiパラメータプローブ用) ──────────────────────
void NewHost::init(NVS *hdlNVS, HID *hdlHID)
{
    // キーマトリクスを全キーリリース状態に初期化
    hostCtrl.strobeAll       = 0xFF;
    hostCtrl.strobeAllAsGPIO = 0x00000000;
    hostCtrl.noKeyPressed    = true;
    for(int i = 0; i < NEWHOST_MATRIX_ROWS; i++)
    {
        hostCtrl.keyMatrix[i]       = 0xFF;         // 全キーリリース
        hostCtrl.keyMatrixAsGPIO[i] = 0x00000000;   // クリアするGPIOビットなし
    }

    // 基底クラスのinitを呼び出し(ハードウェアなし)
    KeyInterface::init(getClassName(__PRETTY_FUNCTION__), hdlNVS, hdlHID);
}

// ─── mapKey() — PS/2スキャンコードをホストマトリクスに変換 ───────────────────
// Core 0のHIDインターフェースタスクから呼び出されます。共有の
// keyMatrix/keyMatrixAsGPIO配列に書き込む際はスピンロックを使用する必要があります。
IRAM_ATTR uint32_t NewHost::mapKey(uint16_t scanCode)
{
    uint8_t  keyCode  = scanCode & 0xFF;
    uint8_t  ctrlKeys = (scanCode >> 8) & 0xFF;
    bool     isBreak  = (ctrlKeys & PS2_BREAK) ? true : false;

    // マッピングテーブルを検索
    for(int idx = 0; idx < sizeof(newHostKeyMap)/sizeof(newHostKeyMap[0]); idx++)
    {
        if(newHostKeyMap[idx].ps2KeyCode == keyCode)
        {
            // マトリクスにキーを適用
            portENTER_CRITICAL(&hostMux);
            for(int mk = 0; mk < PS2TBL_NEWHOST_MAX_MKROW; mk++)
            {
                uint8_t row = newHostKeyMap[idx].mk[mk].row;
                uint8_t col = newHostKeyMap[idx].mk[mk].col;
                if(col == 0) break;

                if(isBreak)
                    hostCtrl.keyMatrix[row] |= col;    // ビットセット = キーリリース
                else
                    hostCtrl.keyMatrix[row] &= ~col;   // ビットクリア = キー押下
            }
            // strobeAllを再計算(全行のAND)
            hostCtrl.strobeAll = 0xFF;
            for(int r = 0; r < NEWHOST_MATRIX_ROWS; r++)
                hostCtrl.strobeAll &= hostCtrl.keyMatrix[r];
            portEXIT_CRITICAL(&hostMux);
            break;
        }
    }
    return(0);
}

// ─── hostInterfaceTask() — Core 1タイミングクリティカルループ ─────────────────
// メインのホスト側プロトコルハンドラです。Core 1でタイトループとして実行され、
// ホストの行選択ストローブに列データで応答します。
// 確定的な実行タイミングのために関数を命令RAMに保持するためIRAM_ATTRでマークします。
void IRAM_ATTR NewHost::hostInterfaceTask(void *pvParameters)
{
    NewHost *pThis = (NewHost *)pvParameters;

    // KDO出力ライン用のGPIOビットマスクを事前計算
    const uint32_t KDO_ALL_MASK = (1 << CONFIG_HOST_KDO7) | (1 << CONFIG_HOST_KDO6) |
                                  (1 << CONFIG_HOST_KDO5) | (1 << CONFIG_HOST_KDO4) |
                                  (1 << CONFIG_HOST_KDO3) | (1 << CONFIG_HOST_KDO2) |
                                  (1 << CONFIG_HOST_KDO1) | (1 << CONFIG_HOST_KDO0);

    // 各列ビットのGPIOマスクをビットごとに事前計算
    const uint32_t colGPIO[8] = {
        (1 << CONFIG_HOST_KDO0), (1 << CONFIG_HOST_KDO1),
        (1 << CONFIG_HOST_KDO2), (1 << CONFIG_HOST_KDO3),
        (1 << CONFIG_HOST_KDO4), (1 << CONFIG_HOST_KDO5),
        (1 << CONFIG_HOST_KDO6), (1 << CONFIG_HOST_KDO7)
    };

    while(true)
    {
        // ── サスペンド要求の確認(WiFiモード切替に必要)─────────────────
        pThis->yield(0);

        // ── ホストプロトコルをここに実装 ──────────────────────────────
        // 例:マトリクススキャンプロトコル(MZ-2500と同様)
        //
        // 1. ホストからのストローブ信号を待つ
        //    while((REG_READ(GPIO_IN1_REG) & RTSNI_MASK) == 0) { }
        //
        // 2. KDB[3:0]から行番号を読み取る
        //    uint32_t gpioIn = REG_READ(GPIO_IN_REG);
        //    uint8_t row = ((gpioIn >> CONFIG_HOST_KDB3) & 1) << 3 |
        //                  ((gpioIn >> CONFIG_HOST_KDB2) & 1) << 2 |
        //                  ((gpioIn >> CONFIG_HOST_KDB1) & 1) << 1 |
        //                  ((gpioIn >> CONFIG_HOST_KDB0) & 1);
        //
        // 3. すべてのKDOをHIGH(非アクティブ)に設定し、押下キービットをクリア
        //    GPIO.out_w1ts = KDO_ALL_MASK;
        //    uint8_t rowData = pThis->hostCtrl.keyMatrix[row];
        //    uint32_t clearMask = 0;
        //    for(int b = 0; b < 8; b++)
        //        if(!(rowData & (1 << b)))  clearMask |= colGPIO[b];
        //    GPIO.out_w1tc = clearMask;
        //
        // 4. ストローブのデアサートを待つ(ホストがデータをラッチ)
        //    while((REG_READ(GPIO_IN1_REG) & RTSNI_MASK) != 0) { }
        //
        // シリアル出力ホスト(X68000のような)の場合、ループは代わりに
        // 保留中のキーイベントを確認し、KDOライン上でビットバンギングにより
        // スキャンコードを送信します。

        vTaskDelay(1);  // プレースホルダー — 実際のプロトコル実装時に削除
    }
}

// ─── hidInterface() — Core 0 HID入力ポーリング ──────────────────────────────
void NewHost::hidInterface(void *pvParameters)
{
    NewHost *pThis = (NewHost *)pvParameters;

    while(true)
    {
        // PS/2またはBluetoothキーイベントをポーリング
        uint16_t scanCode = pThis->hid->read();
        if(scanCode > 0)
        {
            pThis->mapKey(scanCode);
        }
        vTaskDelay(1);
    }
}

ステップ3 — ビルドシステムへの登録
新しいソースファイルをCMakeビルドに追加し、SharpKey.cppの検出ロジックを更新します:
a) CMakeLists.txtの更新main/CMakeLists.txtCOMPONENT_SRCSリストにNewHost.cppを追加します:
set(COMPONENT_SRCS SharpKey.cpp NVS.cpp LED.cpp SWITCH.cpp KeyInterface.cpp
    MZ2528.cpp X1.cpp X68K.cpp Mouse.cpp MZ5665.cpp PC9801.cpp
    HID.cpp WiFi.cpp PS2KeyAdvanced.cpp PS2Mouse.cpp BT.cpp BTHID.cpp
    esp_efuse_custom_table.c
    NewHost.cpp)
set(COMPONENT_ADD_INCLUDEDIRS "." "include")
register_component()
b) SharpKey.cppへのホスト検出の追加SharpKey.cppgetHostType()関数(505行目付近)は、起動時にGPIOラインをサンプリングして接続されているホストを判別します。各ホストはRTSNI、MPXI、KDB[3:0]、KDI4のアイドル状態と動作に基づく固有の電気的シグネチャを持っています。新しいホストの検出ロジックを追加します:
// In getHostType(), after existing detection cases:

// Check for NewHost — describe the electrical signature
// e.g., KDI4 = high, MPXI = high, RTSNI = low, KDB[3:0] = 1100
if(tKDI4 == 1 && tMPXI == 1 && tRTSNI == 0 &&
   tKDB3 == 1 && tKDB2 == 1 && tKDB1 == 0 && tKDB0 == 0 &&
   eFuseInvalid == false &&
   (sharpkeyEfuses.disableRestrictions == true || sharpkeyEfuses.enableNewHost == true))
{
    ifMode = 12345;  // 新しいホスト用のユニークなID番号を選択
}
c) setup()へのインスタンス化の追加setup()関数のswitch(ifMode)ブロック(975行目付近)に、新しいホストのケースを追加します:
#include "NewHost.h"    // SharpKey.cppの先頭に追加

// setup()内のswitch(ifMode)ブロック:
case 12345:
{
    ESP_LOGW(SETUPTAG, "Detected NewHost.");
    keyIf = new NewHost(ifMode, &nvs, led, hid, LITTLEFS_DEFAULT_PATH);
    break;
}

ステップ4 — Kconfigオプションの追加(任意)
新しいホストが設定可能なGPIOピンやビルド時の機能トグルを必要とする場合、main/Kconfig.projbuildにエントリを追加します。既存のエントリのパターンに従ってください。例えば、eFuse機能有効化ビットを追加するには:
// In Kconfig.projbuild, under the "Host Interface" menu:
config ENABLE_NEWHOST
    bool "Enable NewHost support"
    default true
    help
        Enable support for the NewHost retro computer interface.
機能セキュリティ(eFuseベースの機能ゲーティング)を使用する場合は、esp_efuse_custom_table.csvに対応するビットを追加し、SharpKey.cppsharpkeyEfuses構造体を更新します。

ステップ5 — ビルドとテスト
  1. idf.py menuconfigを実行し、新しいホストのKconfigオプションが表示され正しく設定されていることを確認します。
  2. ビルド:idf.py build
  3. ESP32にフラッシュ:idf.py -p /dev/ttyUSB0 flash
  4. カスタムケーブルを使用してSharpKeyを新しいホストに接続します。
  5. シリアル出力を監視:idf.py -p /dev/ttyUSB0 monitor — ホスト検出が正しいifMode値を表示することを確認します。
  6. menuconfigでCONFIG_DEBUG_SERIALを有効にして、詳細なキーイベントログを出力します。各PS/2スキャンコード、修飾キー状態、および結果のホストマトリクス行/列が表示されるはずです。
  7. CONFIG_DEBUG_DISABLE_KDOを使用して、検出とキーマッピングロジックを単独でテストしている間、ホストへの出力を無効にします。
  8. キーマッピングが確認できたら、出力を有効にしてホストコンピュータがキーストロークを正しく受け付けることを検証します。

作成/変更するファイルの要約
Action File What to do
作成 main/include/NewHost.h クラス宣言、キーマッピングテーブル、定数
作成 main/NewHost.cpp init()、mapKey()、hostInterfaceTask()、hidInterface()
変更 main/CMakeLists.txt NewHost.cppをCOMPONENT_SRCSに追加
変更 main/SharpKey.cpp #include "NewHost.h"の追加、getHostType()の検出ロジック、setup()のswitchブロックへのインスタンス化
変更 main/Kconfig.projbuild (任意)GPIOピンと機能有効化オプションの追加
変更 main/esp_efuse_custom_table.csv (任意)機能セキュリティeFuseビットの追加
作成 ホストケーブル SharpKeyコネクタからターゲットホストのキーボードコネクタへの物理配線

キーマップの更新

キーマッピングは、Webインターフェースを通じて実行時にカスタマイズするか、恒久的な変更のためにソースコードを修正することで変更できます。

Webインターフェース経由

キーマッピングエディターは、WiFiが有効な場合にhttp://<sharpkey-ip>/keymap.htmlでアクセスできます。エディターはPS/2ソースキーボードとターゲットホストキーボードマトリクスの両方の視覚的表現を提供します。ユーザーは以下の操作が可能です:
  • ソート可能なグリッドにすべてのエントリが表示された現在のキーマッピングテーブルの参照
  • PS/2キーを選択し、1つ以上のホストマトリクスポジションに割り当て
  • 修飾キー要件の設定(マッピングをアクティブにするためにSHIFT、CTRL、ALTを保持する必要あり)
  • キーボードモデル固有のオーバーライドの設定
  • NVSへの変更保存(再起動やファームウェア更新をまたいで保持)
  • NVSキーマッピングデータをクリアして工場出荷時のデフォルトにリセット
Webインターフェースを通じて行われた変更はESP32の不揮発性ストレージ(NVS)パーティションに保存され、再起動不要で即座に有効になります。NVSマッピングはソースコードからのコンパイル済みデフォルトを上書きします。

ソースコード経由

キーマッピングを恒久的に変更するには、適切なホストインターフェースヘッダーファイル内のt_keyMap[]配列を編集します。MZ-2500/2800の場合はmain/include/MZ2528.hです。
各エントリは以下のフォーマットに従います:
// Map PS/2 'A' key (ASCII 0x61) to MZ matrix row 4, column bit 0
// No modifiers required, all keyboard models, all MZ machines
{ 0x61, 0x00, 0, MZ_ALL,
  4, 0x01,    // Make key 1: row 4, bit 0
  0, 0x00,    // Make key 2: unused
  0, 0x00,    // Make key 3: unused
  4, 0x01,    // Break key 1: row 4, bit 0
  0, 0x00     // Break key 2: unused
},

// Map PS/2 SHIFT+'a' to MZ SHIFT + 'A' (two simultaneous make keys)
{ 0x61, PS2CTRL_SHIFT, 0, MZ_ALL,
  11, 0x04,   // Make key 1: row 11 (SHIFT), bit 2
  4,  0x01,   // Make key 2: row 4 ('A'), bit 0
  0,  0x00,   // Make key 3: unused
  11, 0x04,   // Break key 1: release SHIFT
  4,  0x01    // Break key 2: release 'A'
},
ps2Ctrl修飾キーフラグはビットマスク定数として定義されています:
フラグ 説明
PS2CTRL_SHIFT 0x01 Shiftキーを保持する必要あり
PS2CTRL_CTRL 0x02 Ctrlキーを保持する必要あり
PS2CTRL_CAPS 0x04 Caps Lockがアクティブである必要あり
PS2CTRL_ALT 0x08 Altキーを保持する必要あり
PS2CTRL_ALTGR 0x10 AltGr(右Alt)を保持する必要あり
PS2CTRL_GUI 0x20 Windows/Superキーを保持する必要あり
PS2CTRL_FUNC 0x40 Functionキーを保持する必要あり
PS2CTRL_EXACT 0x80 修飾キーの完全一致が必要(追加の修飾キー不可)

新しいキーボードモデルの追加

新しいPS/2キーボードモデルのサポートを追加するには:
  1. スキャンコードの特定 — 新しいキーボードを接続し、menuconfigでCONFIG_DEBUG_SERIALを有効にします。シリアルモニターが各キー押下の生スキャンコードデータを出力します。非標準のスキャンコードを生成するキーをメモしてください。
  2. モデルIDの割り当て — 次に利用可能なモデル番号を選択し、PS2KeyTable.hに定数として定義します。
  3. モデル固有エントリの追加 — ホストインターフェースヘッダー(例:MZ2528.h)に、汎用マッピングと異なるキーについて、新しいモデルIDをkeyboardModelフィールドに持つt_keyMap[]エントリを追加します。
  4. 徹底的なテスト — すべてのキーがホストマシンで正しい出力を生成することを確認します。修飾キー、特殊文字、およびキーボードレイアウト固有のキーに特に注意してください。
  5. Webインターフェースの更新 — ユーザーが手動で選択できるよう、webserver/keymap.htmlのキーボードモデルドロップダウンに新しいモデル名を追加します。

GPIOピン設定

ESP32のGPIO割り当てはmain/Kconfig.projbuildにデフォルトとして定義されており、idf.py menuconfigを通じて変更できます。以下の表にすべてのGPIO割り当てとデフォルト値を記載します。

PS/2インターフェース

信号 GPIO 方向 用途
PS2_DATA 14 双方向 PS/2キーボードデータライン(オープンコレクタ)
PS2_CLK 13 双方向 PS/2キーボードクロックライン(割り込み駆動)

ホストインターフェース — 行選択(ホストからの入力)

信号 GPIO 方向 用途
KDB0 23 入力 行選択ビット0(ホストスキャン中にアクティブ)
KDB1 25 入力 行選択ビット1
KDB2 26 入力 行選択ビット2
KDB3 27 入力 行選択ビット3
4つのKDBラインは、キーボードスキャン中にホストマシンが駆動する4ビット行アドレス(0-15)を形成します。ホストはすべての行を巡回し、選択された各行の列データを読み取ります。

ホストインターフェース — スキャンデータ(74HCT257経由でホストに出力)

信号 GPIO 方向 用途
KDO0 14 出力 マトリクス列データビット0
KDO1 15 出力 マトリクス列データビット1
KDO2 16 出力 マトリクス列データビット2
KDO3 17 出力 マトリクス列データビット3
KDO4 18 出力 マトリクス列データビット4
KDO5 19 出力 マトリクス列データビット5
KDO6 21 出力 マトリクス列データビット6
KDO7 22 出力 マトリクス列データビット7
8つのKDOラインは現在選択されている行の列データを伝送します。これらはSharpKey PCB上の74HCT257マルチプレクサを通過してからホストコネクタに到達します。各ビットは選択された行内の1つのキーを表し、ロジックHIGHはキーが押されていることを示します。

制御信号

信号 GPIO 方向 用途
RTSNI 35 入力 ホストからの行ストローブ(アクティブロー、GPIO_IN1_REG経由で読み取り)
MPXI 12 入力 マルチプレクス選択(ホスト読み取りサイクル中にアクティブ)
KDI4 13 入力 追加キーボードデータ入力ライン
PWRLED 25 出力 電源/ステータスLEDインジケーター
WIFI_EN 34 入力 WiFi有効化スイッチ(アクティブロー、入力専用GPIO)
RTSNI(Row Strobe Not Active)はクリティカルなタイミング信号です。MZ-2500では、ホストがスキャン中に行あたり約1.2マイクロ秒RTSNIをLOWにパルスします。SharpKeyインターフェースはこのウィンドウ内で行アドレスを読み取り、有効な列データを提示する必要があります。GPIO 35はGPIO_IN1_REG(GPIO 32-39用)に接続された入力専用ピンであるため使用されます。単一のレジスタアクセスで読み取ることができます。

ADC2/WiFi GPIOの競合

ESP32にはハードウェアの制限があり、ADC2ペリフェラルに割り当てられたGPIOは、WiFi無線がアクティブな間はデジタルI/Oに使用できません。影響を受けるピンは:
ADC2 GPIOs: 0, 2, 4, 12, 13, 14, 15, 25, 26, 27
これらのピンのいくつかはSharpKeyでPS/2インターフェース(GPIO 13、14)とホスト行選択(GPIO 25、26、27)の両方に使用されています。ホストインターフェースクラスのreconfigADC2Ports()メソッドは、WiFi初期化後に影響を受けるGPIOを再設定することでこの競合を処理します。WiFiがアクティブな場合、ADC2ペリフェラルは無効化され、ピンはデジタルI/O機能に強制的に戻されます。これはinit()中およびWiFi状態変更後に呼び出されます。

MZ-2500/2800プロトコル

MZ-2500とMZ-2800は、ホストが行選択ラインを駆動し列データを読み返すスキャンキーボードプロトコルを使用します。タイミング要件は極めて厳しく、直接GPIOレジスタアクセスとCore 1の専用化が必要です。

MZ-2500タイミング

MZ-2500は14のキーボードマトリクス行を、RTSNストローブあたり約1.2マイクロ秒のサイクルタイムでスキャンします。完全なスキャンシーケンスは以下の通りです:
  1. ホストがKDB0-KDB3に行アドレス(0-13)を駆動
  2. ホストがRTSNをLOWにアサート(約600ns)
  3. SharpKeyがKDB0-KDB3から行アドレスを読み取り
  4. SharpKeyが対応する列データをKDO0-KDO7に提示
  5. ホストが列データを読み取り
  6. ホストがRTSNをHIGHにデアサート
  7. SharpKeyが列データ出力をクリア

MZ-2800タイミング

MZ-2800はさらに厳しいタイミングで、1.78マイクロ秒のRTSN周期と、行アドレスの読み取りおよび有効な列データの提示に利用可能なのはわずか650ナノ秒です。このため、インターフェースタスクは以下を満たす必要があります:
  • 最大優先度でCore 1上で動作する
  • IRAM_ATTRを使用してすべてのコードを命令RAMに保持する(フラッシュキャッシュミスなし)
  • 共有データアクセスにportMUX_TYPEスピンロックを使用する(mutexではなく)
  • GPIO.out_w1tsGPIO.out_w1tc、およびREG_READ(GPIO_IN1_REG)を介してGPIOレジスタに直接アクセスする
  • ホットループ内でFreeRTOS API呼び出しを行わない(vTaskDelayなし、xSemaphoreTakeなし)
  • 事前計算されたkeyMatrixAsGPIO[]を使用してストローブウィンドウ中のビットシフトを排除する
// Simplified MZ-2800 hot loop (IRAM_ATTR, Core 1)
void IRAM_ATTR MZ2528::mz28InterfaceTask(void *pvParameters) {
    while (1) {
        // Wait for RTSN strobe (active low)
        while (REG_READ(GPIO_IN1_REG) & RTSNI_MASK) { }

        // Read row address from KDB0-KDB3
        uint32_t gpioIn = REG_READ(GPIO_IN_REG);
        uint8_t row = (gpioIn >> KDB_SHIFT) & 0x0F;

        // Present column data
        GPIO.out_w1tc = KDO_ALL_MASK;          // Clear all columns
        GPIO.out_w1ts = keyMatrixAsGPIO[row];   // Set pressed keys

        // Wait for RTSN deassert
        while (!(REG_READ(GPIO_IN1_REG) & RTSNI_MASK)) { }

        // Clear outputs
        GPIO.out_w1tc = KDO_ALL_MASK;
    }
}
オシロスコープのトレースや詳細なタイミング分析を含む完全なプロトコルドキュメントについては、SharpKeyテクニカルガイドを参照してください。

Bluetooth統合

SharpKeyはBluetooth Classic HIDおよびBluetooth Low Energy(BLE)HIDキーボードおよびマウスをサポートしており、ワイヤレス入力デバイスをビンテージマシンで使用できます。

アーキテクチャ

Bluetoothサブシステムは2つのクラスに分かれています:
  • BT.cpp — GAP(Generic Access Profile)イベント処理、SDP(Service Discovery Protocol)登録、およびHIDデバイスコールバックを含む低レベルBluetoothスタックを管理します。ESP-IDF BluetoothコントローラーおよびBluedroidホストスタックの初期化を処理します。
  • BTHID.cpp — 高レベルのBluetooth HIDインターフェースを提供します。BluetoothキーボードおよびマウスからのHIDレポートをPS/2パスで使用されるのと同じスキャンコードフォーマットに変換し、パイプラインの残りの部分(HID.cpp、mapKey)が同一に処理できるようにします。
Bluetoothスタックは、NVSに保存された最大5台のボンディングデバイスをサポートします。ブート時に、SharpKeyは以前にペアリングされたデバイスとの再接続のために自動スキャンを開始します。ボンディングされたデバイスが見つからない場合、60秒間のペアリングウィンドウが自動的に開きます。

ペアリングプロセス

Bluetoothペアリングプロセスは以下のように動作します:
  1. ボンディングされたデバイスが接続されていない場合、SharpKeyが検出可能モードに入ります
  2. ステータスLEDが8Hzで点滅し、ペアリングモードがアクティブであることを示します
  3. キーボードがHIDホストをスキャンしてペアリングを開始します
  4. PINが要求された場合、デフォルトPINは"1234"です
  5. ペアリングが成功すると、デバイスボンドがNVSに保存されます
  6. LEDが常時点灯に切り替わり、デバイスが接続されたことを示します
  7. デバイスが接続されない場合、60秒後にペアリングウィンドウが閉じます
再ペアリングを強制するには(例:キーボードを変更する場合)、Webインターフェースを通じてNVSボンドデータをクリアするか、idf.py erase-flashでNVSパーティションを消去します。

HIDマルチプレクシング

HID.cppはPS/2とBluetooth入力ソース間のマルチプレクサとして機能します。マルチプレクシングロジックは以下のルールに従います:
  • PS/2が優先 — データ/クロックラインでPS/2キーボードが検出された場合、Bluetoothスキャンは無効化され、すべての入力はPS/2ソースから取得されます。
  • Bluetoothフォールバック — ブート時にPS/2キーボードが検出されない場合、Bluetoothが初期化され、Bluetooth HIDソースから入力が取得されます。
  • ホットプラグ検出 — Bluetoothがアクティブな状態でブート後にPS/2キーボードが接続された場合、システムはPS/2入力に切り替わります。ただし、ESP-IDFスタックの制限により、Bluetoothへの切り替えには再起動が必要です。
  • マウス入力 — PS/2およびBluetoothマウスはキーボード入力とは独立してマルチプレクスされます。両方を同時にアクティブにできます。
WiFiとBluetoothは同時にアクティブにできないことに注意してください。両方ともESP32の共有無線アンテナを使用し、IDFスタック要件が競合します。WiFiとBluetoothモードの切り替えには完全な再起動が必要です。モード選択はWIFI_ENスイッチ(GPIO 34)またはNVS設定を通じて制御されます。

WiFiとWebインターフェース

SharpKeyには、設定、キーマッピングのカスタマイズ、およびファームウェアアップデート用の組み込みWiFiアクセスポイントとWebサーバーが含まれています。WiFiサブシステムはmain/WiFi.cppに実装されています。

WiFiモード

SharpKeyは2つのWiFi動作モードをサポートしています:
  • アクセスポイントモード(デフォルト) — SharpKeyが独自のWiFiネットワークを作成します。デフォルトSSID:sharpkey、デフォルトパスワード:sharpkey。ラップトップまたはスマートフォンからこのネットワークに接続し、http://192.168.4.1にアクセスしてWebインターフェースを使用します。このモードは既存のネットワークインフラストラクチャなしで動作します。
  • ステーションクライアントモード — SharpKeyがクライアントとして既存のWiFiネットワークに接続します。SSIDとパスワードはwifimanager.htmlページまたはmenuconfigを通じて設定されます。SharpKeyはDHCPを通じてIPアドレスを取得します。このモードでは同じネットワーク上の任意のデバイスからアクセスできます。
アクティブなWiFiモードはNVSに保存され、再起動をまたいで保持されます。ESP-IDFのWiFiとBluetoothスタックは無線ハードウェアを共有し共存できないため、WiFiとBluetoothの切り替えには完全な再起動が必要です。

Webサーバーエンドポイント

組み込みWebサーバーは以下のエンドポイントを提供します:
エンドポイント メソッド 用途
/index.html GET メインダッシュボード — ファームウェアバージョン、ホストタイプ、WiFiステータス、接続デバイスを表示
/keymap.html GET キーマッピングエディター — PS/2からホストキーへの変換のビジュアルエディター
/mouse.html GET マウス設定 — 感度、加速度、ボタンマッピング
/ota.html GET OTAファームウェアアップロードページ — ドラッグアンドドロップでのファームウェアバイナリアップロード
/wifimanager.html GET WiFi設定 — APとクライアントモードの切り替え、SSID/パスワードの設定
/version.txt GET ファームウェアバージョン文字列(プレーンテキスト)
/api/update POST OTAファームウェアアップロードエンドポイント — バイナリファームウェアデータを受信
すべてのページはレスポンシブデザインにBootstrapを使用しており、デスクトップとモバイルの両方のブラウザで動作します。静的アセット(CSS、JavaScript、フォント)は帯域幅効率のためにgzip Content-Encodingで配信されます。

Webファイルシステム

WebインターフェースファイルはESP32のフラッシュメモリ上のLittleFSファイルシステムパーティションに保存されています。Webファイルシステムのビルドプロセスは以下の通りです:
  1. build_webfs.shスクリプトがwebserver/からHTMLファイルをステージングディレクトリwebfs/にコピーします。
  2. CSS、JavaScript、およびフォントファイルはフラッシュ使用量の削減と圧縮配信のために、コピー時にgzip圧縮されます。
  3. CMakeビルドシステムがesp_littlefsコンポーネントを使用してwebfs/ディレクトリからLittleFSパーティションイメージを作成します。
  4. パーティションイメージは書き込み時にfilesysパーティション(640KB、タイプspiffs)に書き込まれます。
  5. 実行時にESP32がLittleFSパーティションをマウントし、ファイルを直接配信します。圧縮ファイルはContent-Encoding: gzipヘッダー付きで配信されるため、ブラウザが透過的に解凍します。
Webファイルシステムは、LittleFSパーティションデータのみを含むFilePack OTAイメージにより、ファームウェアとは独立して更新できます。

OTAファームウェアアップデート

SharpKeyはWebインターフェースを通じたOTA(Over-The-Air)ファームウェアアップデートをサポートしており、シリアルポートへの物理アクセスなしにファームウェアを更新できます。

パーティション構成

ESP32フラッシュは、自動ロールバック機能を備えた信頼性の高い更新のためにデュアルOTAパーティションに分割されています。完全なパーティションテーブルはsharpkey_partition_table.csvで定義されています:
# Name,     Type,  SubType,  Offset,   Size
otadata,    data,  ota,      0x9000,   0x2000
nvs,        data,  nvs,      ,         0x4000
phy_init,   data,  phy,      ,         0x1000
ota_0,      app,   ota_0,    ,         0x1A0000
ota_1,      app,   ota_1,    ,         0x1A0000
filesys,    data,  spiffs,   ,         0xA0000
パーティション サイズ 用途
otadata 8 KB OTA選択データ(どのパーティションからブートするか)
nvs 16 KB 不揮発性ストレージ(キーマッピング、WiFi設定、BTボンド)
phy_init 4 KB PHYキャリブレーションデータ(WiFi/BT無線)
ota_0 1.625 MB アプリケーションパーティションA
ota_1 1.625 MB アプリケーションパーティションB
filesys 640 KB LittleFS Webファイルシステム

アップデートプロセス

OTAアップデートプロセスは、デュアルパーティションのラウンドロビン方式でESP-IDF OTA APIを使用します:
  1. アップロード — ファームウェアバイナリは/ota.htmlWebページ(ドラッグアンドドロップ)を通じて、または/api/updateエンドポイントにバイナリをPOSTしてアップロードされます。
  2. ヘッダー検証 — アップロードされたバイナリの最初のバイトがESP_APP_DESC_MAGIC_WORDヘッダーでチェックされ、有効なESP32アプリケーションイメージであることを確認します。
  3. バージョンチェック — アプリケーションヘッダーのファームウェアバージョン文字列が実行中のバージョンと比較されます。eFuse設定で明示的にオーバーライドされない限り、ダウングレードは防止されます。
  4. フラッシュへのストリーミング — バイナリデータは現在非アクティブなOTAパーティションにストリーミングされます(ota_0が動作中の場合、データはota_1に書き込まれ、その逆も同様)。これにより、更新が完了するまで実行中のファームウェアが保持されます。
  5. アクティブパーティションの切り替え — 書き込みと検証が成功した後、次回の再起動時に新しく書き込まれたパーティションからブートするようOTAデータパーティションが更新されます。
  6. 自動再起動 — ESP32が自動的に新しいファームウェアで再起動します。新しいファームウェアが起動に失敗した場合(例:初期init中にクラッシュ)、ブートローダーは設定可能なタイムアウト後に前のパーティションにロールバックします。

eFuseカスタムフィールド

SharpKeyはEFUSE_BLK3のカスタムeFuseフィールドを使用して、ハードウェア識別および機能セキュリティデータを保存します。これらのフィールドはワンタイムプログラマブル(OTP)です — 一度書き込むと変更できません。
カスタムeFuseフィールドはmain/esp_efuse_custom_table.csvで定義され、ビルド中にESP-IDFのefuse_table_gen.pyツールによってCソースおよびヘッダーファイルにコンパイルされます。

識別フィールド

フィールド ビット範囲 サイズ(ビット) 用途
HARDWARE_REVISION 56-71 16 SharpKey PCBのハードウェアリビジョン番号
SERIAL_NO 72-87 16 このユニットの一意なシリアル番号
DISABLE_RESTRICTIONS 88 1 セットすると、すべての機能制限を無効化
RESERVED1 89-95 7 将来のフラグビット用に予約
BUILD_DATE 160-183 24 24ビット値としてエンコードされたビルド日

機能セキュリティビット

menuconfigでENABLE_FEATURE_SECURITYが設定されている場合、ファームウェアはeFuseビットをチェックして、どのホストインターフェースと機能が有効かを判断します。これにより、ハードウェアバリアントや顧客設定に基づいて特定の機能セットを持つ製品ユニットを出荷できます。
ビット フィールド名 用途
8 ENABLE_BT Bluetoothキーボード/マウスサポートの有効化
9 ENABLE_MZ5665 MZ-5500/5600/6500ホストインターフェースの有効化
10 ENABLE_PC9801 NEC PC-9801ホストインターフェースの有効化
11 ENABLE_MOUSE マウスサポート(PS/2およびBluetooth)の有効化
12 ENABLE_X68000 Sharp X68000ホストインターフェースの有効化
13 ENABLE_X1 Sharp X1ホストインターフェースの有効化
14 ENABLE_MZ2800 Sharp MZ-2800ホストインターフェースの有効化
15 ENABLE_MZ2500 Sharp MZ-2500ホストインターフェースの有効化
DISABLE_RESTRICTIONSビット(ビット88)を設定すると、すべての機能セキュリティビットが上書きされ、個別の有効化ビットに関係なくすべての機能が有効になります。これは開発およびデバッグ目的で使用されます。
CSVソースファイルが権威ある定義です。生成されたCファイル(esp_efuse_custom_table.cおよびesp_efuse_custom_table.h)は.gitignore*efuse*にマッチ)によりバージョン管理から除外されますが、CSVソースは明示的に含まれています(!*efuse*.csv)。

ビルド環境

ネイティブビルドセットアップ

LinuxまたはmacOSでネイティブESP-IDF v4.4ビルド環境をセットアップするには:
# Install prerequisites (Ubuntu/Debian)
sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv \
    cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0

# Clone ESP-IDF v4.4
mkdir -p ~/esp
cd ~/esp
git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf

# Install toolchain and tools
./install.sh

# Set up environment variables (run in each new terminal session)
. ./export.sh
インストール後、idf.pyコマンドがPATHで利用可能になります。自動セットアップのためにシェルプロファイルに. ~/esp/esp-idf/export.shを追加できます。

クローンとビルド

SharpKeyリポジトリをクローンしてファームウェアをビルドするには:
# Clone the repository
git clone https://git.eaw.app/eaw/SharpKey.git
cd SharpKey

# Initialise submodules (arduino-esp32 and esp_littlefs)
git submodule update --init --recursive

# Configure build options (select target, GPIO pins, features)
idf.py menuconfig

# Build the web filesystem (compress CSS/JS/fonts, create LittleFS image)
./build_webfs.sh

# Compile the firmware
idf.py build

# Flash to ESP32 (connect via USB-to-serial adapter)
idf.py -p /dev/ttyUSB0 flash

# Open serial monitor for debug output
idf.py -p /dev/ttyUSB0 monitor
build_webfs.shスクリプトは最初のビルド前およびWebインターフェースファイルが変更されるたびに実行する必要があります。idf.py buildコマンドはeFuseテーブルジェネレーターの実行とすべてのソースファイルのコンパイルを自動的に行います。ビルドの問題が発生した場合はidf.py fullcleanを使用してすべてのビルド成果物を削除し、最初からやり直してください。

idf.py menuconfigコマンドでテキストベースの設定インターフェースが開きます。SharpKeyのオプションはmain/Kconfig.projbuild(約310行)で定義されています。主な設定カテゴリは:
  • Build Target — ファームウェアバリアントの選択:
    • SHARPKEY — 完全なマルチホストビルド(MZ-2500、MZ-2800、MZ-5600、X1、X68000、PC-9801)
    • MZ25KEY_MZ2500 — MZ-2500専用のシングルターゲットビルド
    • MZ25KEY_MZ2800 — MZ-2800専用のシングルターゲットビルド
  • Feature Security — eFuseベースの機能ロックの有効化(プログラムされたeFuseビットに基づいて利用可能なホストインターフェースと機能を制限)。
  • PS2 Keyboard — PS/2のDATAおよびCLKライン用GPIOピン割り当て。
  • Host Interface — 以下のGPIOピン割り当て:
    • KDB0-KDB3(ホストからの行選択入力)
    • KDO0-KDO7(ホストへの列データ出力)
    • RTSNI(行ストローブ入力)
    • MPXI(マルチプレクス選択入力)
    • KDI4(追加データ入力)
  • Mouse UART — ホストへのマウスシリアル出力用にソフトウェアビットバンギングとハードウェアUARTの選択。
  • WiFi — WiFiの有効/無効、デフォルトSSIDとパスワード、チャンネル番号、最大同時接続数の設定。
  • Debug Options — シリアルデバッグ出力の有効化、およびハードウェア隔離テスト用の個別GPIOグループの無効化(KDB、KDO、RTSNI、MPXI、KDIの無効化)。
  • Power LED — ステータスLED用GPIOピン割り当て。

Dockerビルド

IDFツールチェーンをローカルにインストールせずにコンテナ化されたビルドを行うには、Espressif公式Dockerイメージを使用します:
# Simple Docker build
docker run --rm -v $(pwd):/project -w /project espressif/idf:v4.4 idf.py build
ビルドコンテナがシブリングコンテナ(ネストされたDockerではない)であるJenkins CIパイプラインでは、ワークスペースパスをJenkinsコンテナパスからDockerホストパスにマッピングする必要があります:
# Jenkins sibling container build (host path mapping)
docker run --rm \
    -v "${hostWorkspace}:${workspace}" \
    -w "${workspace}" \
    espressif/idf:v4.4 \
    idf.py build
DockerイメージにはすべてのIDFツール、ツールチェーン、およびPython依存関係が含まれています。追加のセットアップは不要です。menuconfigはターミナルが必要なため、Dockerで非対話的に実行することはできません — 代わりに事前設定されたsdkconfigファイルを使用してください。

継続的インテグレーション

SharpKeyは自動ビルド、テスト、およびリリース管理のためにJenkins CI/CDパイプラインを使用しています。パイプラインはGitea webhookによってトリガーされ、配布用のバージョン付きファームウェアパッケージを生成します。

Jenkinsパイプライン概要

SharpKey CI/CDパイプラインはEaW VPS上でホストされているJenkinsインスタンスで実行されます。パイプラインはmainまたはmasterブランチにコミットがプッシュされた際にGitea webhookによって自動的にトリガーされます。パイプラインのステージは以下の通りです:
  1. Checkout — ワークスペースのクリーン、GiteaからのSharpKeyリポジトリのクローン、およびすべてのGitサブモジュール(arduino-esp32とesp_littlefs)の初期化。
  2. Determine Version — リポジトリからversion.txtを読み取り。前回のコミットからバージョンが変更されていない場合、最新のGiteaリリースタグからバージョン番号を自動インクリメント。
  3. Build Web Filesystembuild_webfs.shを実行してWebインターフェースアセットをwebfs/ステージングディレクトリにコピーおよび圧縮。
  4. Generate eFuse Table — IDF Dockerコンテナ内でefuse_table_gen.pyを実行し、CSVソースからesp_efuse_custom_table.cおよび.hを生成。
  5. Build Firmware — IDF Dockerコンテナ内でidf.py buildを使用してファームウェアをコンパイル。
  6. Package Release — 3つのリリースアーカイブを作成:FW(ファームウェアバイナリ)、FilePack(Webファイルシステムイメージ)、FlashPack(完全なフラッシュイメージ)。
  7. Create Gitea Release — バージョン番号でタグ付けした新しいGiteaリリースにリリース成果物をアップロード。

Webhook設定

パイプラインはJenkins Generic Webhook Triggerプラグインに対してPOSTリクエストを送信するGitea webhookによってトリガーされます。設定は以下の通りです:
  • Webhook URLhttps://<jenkins-host>/generic-webhook-trigger/invoke?token=sharpkey-build-trigger
  • Tokensharpkey-build-trigger
  • Content Typeapplication/json
  • Branch Filterrefs/heads/mainまたはrefs/heads/masterへのプッシュでのみトリガー。フィーチャーブランチ、タグ、およびその他のrefへのプッシュは無視されます。

バージョン管理

ファームウェアバージョンはリポジトリルートのversion.txtMAJOR.MINORフォーマット(例:"1.05")で保存されています。バージョン管理は以下のルールに従います:
  • トリガーコミットでversion.txtが変更されている場合、ファイルに指定されたバージョンがそのままリリースに使用されます。
  • version.txtが変更されていない場合、パイプラインはGitea APIに最新のリリースタグを照会し、マイナーバージョン番号を自動インクリメントします。
  • マイナーバージョンは100でロールオーバーします:バージョン1.99は2.00にインクリメントされます(メジャーバージョンが1増加、マイナーが00にリセット)。
  • バージョン文字列はファームウェアバイナリヘッダーに埋め込まれ、Webインターフェースで表示するためにwebserver/version.txtにも書き込まれます。

リリース成果物

各パイプライン実行で3つのリリース成果物が生成されます(すべてgzip圧縮):
成果物 内容 使用ケース
SharpKey-FW-vX.XX.bin.gz ファームウェアアプリケーションバイナリ Webインターフェース経由のOTAアップデート(ota_0/ota_1に書き込み)
SharpKey-FilePack-vX.XX.bin.gz LittleFS Webファイルシステムイメージ Webインターフェースのみの OTAアップデート(filesysに書き込み)
SharpKey-FlashPack-vX.XX.tar.gz 完全なフラッシュイメージ:ブートローダー、パーティションテーブル、OTAデータ、ファームウェア、ファイルシステム esptool(シリアル)による初期書き込み
FWおよびFilePack成果物はワイヤレスアップデート用に/ota.htmlWebページからアップロードできます。FlashPackは新しい基板の初期プログラミングまたは文鎮状態からの復旧に使用され、シリアル接続が必要です:
# Extract and flash the FlashPack
tar xzf SharpKey-FlashPack-v1.05.tar.gz
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 \
    write_flash --flash_mode dio --flash_size 4MB \
    0x1000 bootloader.bin \
    0x8000 partition-table.bin \
    0x9000 ota_data_initial.bin \
    0x10000 sharpkey.bin \
    0x3A0000 filesys.bin

パイプライン設定

Jenkinsパイプラインはgroovyスクリプト(Jenkinsfileまたはインラインパイプライン定義)として定義されています。パイプライン全体で使用される主要な環境変数は以下の通りです:
変数 用途
GITEA_URL Giteaインスタンスのベース URL(例:https://git.eaw.app)
REPO_URL SharpKeyリポジトリの完全なクローンURL
GITEA_TOKEN リリースの作成とアセットのアップロード用APIトークン
GITEA_OWNER リポジトリオーナー(例:”eaw”)
GITEA_REPO リポジトリ名(例:”SharpKey”)
IDF_DOCKER ESP-IDF Dockerイメージタグ(例:”espressif/idf:v4.4”)
Docker-in-Dockerビルドの重要な詳細は、ホストパスマッピングです。Jenkins自体がDockerコンテナで実行され、IDFビルドがシブリングコンテナ(ネストされたコンテナではない)で実行されるため、ワークスペースパスをJenkinsコンテナパスからDockerホストパスに変換する必要があります:
// Groovy path mapping
def hostWorkspace = workspace.replace('/var/jenkins_home', '/srv/jenkins/data')

// The IDF container mounts the host path, not the Jenkins container path
docker run --rm \
    -v "${hostWorkspace}:${workspace}" \
    -w "${workspace}" \
    espressif/idf:v4.4 \
    idf.py build
このマッピングがない場合、IDFコンテナはJenkinsコンテナ内にのみ存在するパスをマウントしようとし、ワークスペースが空になりビルドが失敗します。

eFuseテーブル生成

パイプラインはメインビルドの前にCSV定義からeFuse Cソースおよびヘッダーファイルを生成する個別のステージとして実行します。これは、生成されたファイルが.gitignore*efuse*にマッチ)によりバージョン管理から除外されるのに対し、CSVソースは明示的に含まれている(!*efuse*.csv)ために必要です。
# Run inside the IDF Docker container
python /opt/esp/idf/components/efuse/efuse_table_gen.py \
    --idf_target esp32 \
    /opt/esp/idf/components/efuse/esp32/esp_efuse_table.csv \
    main/esp_efuse_custom_table.csv
このコマンドは標準ESP32 eFuseテーブルをベースとして、CSVからSharpKeyのカスタムフィールド定義をマージします。出力ファイル(esp_efuse_custom_table.cおよびesp_efuse_custom_table.h)はCMakeが期待するmain/ディレクトリに配置されます。

デバッグ

シリアルモニター

主要なデバッグツールはESP-IDFシリアルモニターで、USB-シリアル接続を通じて115200ボーでリアルタイムログ出力を提供します:
idf.py -p /dev/ttyUSB0 monitor
詳細出力のためにmenuconfigでCONFIG_DEBUG_SERIALを有効にします。有効にすると、ファームウェアは以下をログに記録します:
  • 受信したPS/2スキャンコード(生データおよびデコード済み)
  • キーマッピング検索(どのテーブルエントリが一致したか)
  • マトリクス状態変更(各キー押下/リリースの行と列)
  • Bluetoothペアリングイベントおよび HIDレポート
  • WiFi接続状態の変化
  • ブート時のホスト検出結果
  • NVS読み書き操作
シリアルモニターを終了するにはCtrl+]を使用します。モニターは書き込みと組み合わせて単一のコマンドで実行できます:idf.py -p /dev/ttyUSB0 flash monitor

GPIOデバッグフラグ

開発およびテスト中のハードウェアの問題を切り分けるため、menuconfigを通じて個別のGPIOグループを無効化できます。これは、ホストマシンを接続せずに単体PCBでデバッグする場合や、信号整合性の問題をトラブルシューティングする場合に特に便利です。
Kconfigオプション 効果
DEBUG_DISABLE_KDB ホスト行選択入力(KDB0-KDB3)の無効化。インターフェースタスクは行アドレスを読み取りません。
DEBUG_DISABLE_KDO ホストデータ出力(KDO0-KDO7)の無効化。ホストに列データが駆動されません。
DEBUG_DISABLE_RTSNI 行ストローブ入力(RTSNI)の無効化。インターフェースタスクはストローブ信号を待ちません。
DEBUG_DISABLE_MPXI マルチプレクス選択入力(MPXI)の無効化。
DEBUG_DISABLE_KDI 追加データ入力(KDI4)の無効化。
これらのフラグはコンパイル時オプションです。グループが無効化されると、対応するGPIOピンは初期化されず、インターフェースタスクは関連する読み書き操作をスキップします。これにより、ホストマシンを接続せずにファームウェアを実行してHID入力を処理できます。

よくある問題

開発中に遭遇する一般的な問題とその解決策は以下の通りです:
  • ADC2/WiFi GPIOの競合 — GPIO 0、2、4、12、13、14、15、25、26、27はADC2ペリフェラルと共有されており、WiFiがアクティブな間はデジタルI/Oに使用できません。reconfigADC2Ports()メソッドがこれを自動的に処理しますが、これらのピンに新しいGPIO割り当てを追加する場合は、WiFi初期化後に再設定されることを確認してください。
  • eFuseファイルが見つからないesp_efuse_custom_table.hが見つからずビルドが失敗する場合、CSVソースファイル(main/esp_efuse_custom_table.csv)がコミットされていることを確認し、メインビルドの前にefuse_table_gen.pyが実行されることを確認してください。CIではこれは個別のパイプラインステージです。ローカルビルドではidf.py buildがこれを自動的に処理するはずですが、初回ビルドでは手動実行が必要な場合があります。
  • サブモジュールエラーarduino-esp32またはesp_littlefsコンポーネントが見つからないことに関連するビルドエラーは、Gitサブモジュールが初期化されていないことを示しています。git submodule update --init --recursiveを実行して解決してください。
  • MZ-2500/2800でのタイミング問題 — ホストマシンがキー押下を認識しない、またはランダムな文字が表示される場合、以下を確認してください:
    • インターフェースタスクがCore 1に固定されていること(xTaskCreatePinnedToCore(..., 1)
    • タスク関数がIRAM_ATTRでマークされていること
    • portMUX_TYPEスピンロックが使用されていること(FreeRTOS mutexではなく)
    • ホットループ内でFreeRTOS API呼び出しが行われていないこと
    • GPIOレジスタアクセスが使用されていること(オーバーヘッドが追加されるgpio_set_level()ではなく)
  • WiFi/Bluetoothモード切り替え — ESP-IDFのWiFiとBluetoothスタックは共存できません。モード切り替えには完全な再起動が必要です。切り替え後にファームウェアがハングする場合、esp_restart()を呼び出す前にモード選択がNVSに保存されていることを確認してください。
  • Webファイルシステムが見つからない — Webインターフェースが404エラーを返す場合、ビルド前にbuild_webfs.shが実行されていること、およびfilesysパーティションが書き込まれていることを確認してください。FilePack OTAイメージを個別にアップロードして、Webファイルシステムのみを更新できます。
  • PS/2キーボードが検出されない — DATA(GPIO 14)とCLK(GPIO 13)ラインが正しく接続されていることを確認してください。PS/2クロックラインは割り込み駆動です — 他のペリフェラルによって割り込みがマスクされていないか確認してください。スキャンコードが受信されているかどうかを確認するためにシリアルデバッグを有効にしてください。

参考サイト

SharpKeyファームウェアを扱う際に役立つ外部リソースは以下の通りです:
  • ESP-IDFプログラミングガイド(v4.4)https://docs.espressif.com/projects/esp-idf/en/v4.4/
    FreeRTOS、GPIO、WiFi、Bluetooth、NVS、OTA、およびパーティションテーブルのドキュメントを含む、ESP-IDFフレームワークの完全なAPIリファレンスおよびプログラミングガイド。
  • SharpKeyリポジトリhttps://git.eaw.app/eaw/SharpKey
    SharpKeyプロジェクトのソースコード、イシュー、リリース、およびCI/CDパイプライン。
  • PS/2プロトコルリファレンス — PS/2キーボードプロトコルは、11ビットシリアルフレーム(1スタートビット、8データビット、1パリティビット、1ストップビット)を使用するスキャンコードセット2を使用します。クロックは10-16.7kHzでキーボードによって駆動されます。PS2KeyAdvancedライブラリは拡張キー(E0プレフィックス)、ブレイクコード(F0プレフィックス)、およびポーズ/ブレイクシーケンスを含むすべてのプロトコル詳細を処理します。
  • ESP32テクニカルリファレンスマニュアル — GPIOレジスタ(GPIO_OUT_REG、GPIO_OUT_W1TS_REG、GPIO_OUT_W1TC_REG、GPIO_IN_REG、GPIO_IN1_REG)、割り込みマトリクス、およびWiFiとのADC2競合を含むペリフェラル割り当てをカバーするEspressifのハードウェアリファレンス。
  • FreeRTOSリファレンス — ESP32デュアルコアアプリケーション向けのタスク作成、スピンロック(portMUX_TYPE)、コアピニング(xTaskCreatePinnedToCore)、およびIRAM_ATTR使用法。
  • GNU General Public License v3 — SharpKeyはGPL v3の下でリリースされています。すべての派生著作物は同じライセンス条件を維持する必要があります。


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

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