コンテンツにスキップ
【自作キーボード】自作のConta基板で遊んでみよう!

【自作キーボード】自作のConta基板で遊んでみよう!

こんにちは。アルバイトの田中です。

2ヶ月ほど前の記事にて、「Conta規格基板の自作をしてみたでお会いしましょう」と締めて、早速第二弾の記事です!

今回はConta規格のサイズに目を付けて、機能を維持して取り外し可能なキーボードモジュールを製作してみました。最終的に、Picossciをキーボード化してみようと思います!

誰でも使えるツールで簡単に設計して発注できるので、ぜひ真似してみてください。

設計について

設計したのはキーボードのキースイッチとEEPROMが入っているだけのモジュールですが、電子工作の初心者でConta規格の自作erが現れることを期待して、設計の手順を簡単に書いていこうと思います。

まず、簡単に規格の寸法とピン配列のルールを紹介します。

基板のサイズは以下の画像の通りです。 基板サイズは788 milの正方形で、取り付け穴としてM2サイズの下穴が二つ開いています。 基板設計の際に基板カット用の外形寸法が必要となりますので、こちらからConta規格の外形図(dxf拡張子)をダウンロードしてご利用ください。

ピン配置は以下の通りです。I2CとSPI用ピンと、機能の指定がないIOピンが四つあります。今回は使いませんが、5Vの電源を使うことも可能です。

その他規格の詳細はこちらをご参照ください。

使用部品

部品は以上のものを使いました。Picossciへ搭載するので、同じものを4個製作します。大体一つ当たりの原価は300円くらいでしょうか。贅沢にEEPROMを搭載しているので少々コストがかかりますね。

使用ツール

  • KiCad 8.0(2025年3月現在で最新の9.0でも問題なし)
  • FreeCAD 1.0(最近大型アップデートが入り、使いやすくなりました。安定版の1.0になるまで何年かかったのでしょうか...)

ツールの詳しい使い方は、先人の知恵がインターネット上に掲載されているので、そちらをご参照ください。ここでは簡単に設計内容を説明します。KiCadでは以下のような回路設計しました。3 x ピンヘッダ1x6を規格通りに配線します。RP2350の内部プルアップ、プルダウン機能はラッチされるバグがあるそうなので、しっかりと回路でプルアップしてあげます。

それでは、実際の基板配線をしていきます。先ほど掲載したconta規格準拠のdxfファイルを読み込んで、基板外形のデータを読み込みます。

PCBのデザインは以下のようにしました。非公式の基板なのでConta 【Unofficial】KeyBoard Moduleと書いてみました。配線をしたあとは、安定動作のためにベタGNDも忘れないようにしましょう。

25AA010AT-I/OTはSPI通信で読み書きする不揮発メモリです。 今回のコンセプトとして、モジュールを差し込んで使える(機能が維持される)ことを考えていたため、押した際のキーの内容をメモリに保存していきます。

25AA010AT-I/OTのデータシートはこちら

設計が終わったら、KiCadでガーバーデータ(発注用のデータ形式)を生成して、基板発注先へデータをアップロードしてください。発注先は海外(特に中国)がおすすめです。国内業者はとても高品質な基板を製造してくれる一方で、趣味の工作用途ではコストが高いためおすすめしません。

ソフトについて

ソフトは自作キーボードの定番ファームウエアを改造しても良いですが、今回は簡単に開発することが目標なのでArduinoで開発していきます。

使用ツール

  • Arduino IDE 2.3.4
  • Keyboardライブラリ

Arduinoの「Keyboard」ライブラリを使った開発では、USB HID機能に対応していないため、PCからキーボードとして認識されず、オートリピート機能なども利用できません。そこで、Picossciに書き込むソフトウェア内で、これらの機能を補う仕組みを実装しています。

基板を発注してから気づいたのですが、PicossciのM1とM3、M2とM4はSPI通信用のCSピンが共通でした。そのため少々面倒ではありますが、書き込み時、読み込み時ともに抜き差ししてキーの機能を保存、出力する仕組みとなっています。(規格違反気味ながら、共通配線でないIO1またはIO4をEEPROMのCSピンにつないでおけばよかったと思いました...。)

ソフトはページ最下部に掲載しています。

完成

完成したキーボードモジュールは以下の通りです。

使い方

  1. キーモジュールに機能を書き込む必要があるので、書き込み用ソフトをPicossciに入れます。書き込みたいキーの機能はコードの以下の部分に入力します。対応する修飾キーはkeyboardライブラリの説明を参照ください。

    eepromWriteTwoBytes(SPI◯_SS, SPI◯_SCK, SPI◯_MOSI, SPI◯_MISO, 修飾キー(ctrlなど), 通常キー(英数字));
  2. Picossciからのシリアルデータに従ってキーを差し込み、データを書き込みます。

    以下の写真はM1、M2に差し込んだ状態。

    以下の写真はM3、M4に差し込んだ状態。


  3. キーへのデータ書き込みが終わったら、読み込みとキーボード機能用のソフトをPicossciに入れます。
  4. Picossciからのシリアルデータに従ってキーを差し込み、データをPicossciに読み込ませます。

  5. キーを押すとキーに書き込んだ機能が使えます。

    今回はM1にctrl+c、M2にctrl+v、M3にctrl、M4にDキーを書き込んだキーを差して読み込ませました。

大変面倒なことにCSピンが共通なため、キーの位置を入れ替える際はPicossciをリセットしてキーモジュールをすべて取り外し、順番に差し込む必要があります...。みなさまが設計する際は、共通配線でないIO1またはIO4をEEPROMのCSピンにしましょう。

この機能の利点は、ショートカットキーを割り当てて物理的に好きな場所にキーを割り当てられる点です。同じソフトを書いた別のボードでも、キーの使い回しができる点もメリットです。たとえば、キーに割り当てた機能に沿ったデコレーションをしていて、そのキーを差し替えるだけで任意の場所に機能が割り当てられたりします。

さいごに

最後まで読んでいただきありがとうございます。CSピンの仕様に気づかず、キーボードをリセットしてからキーの抜き差しをしなけらばならない点が非常に心残りです。次に改善するなら、EEPROMのCSピンをConta規格のIOピンに割り当てておきたいですね。

また、写真右下のようなベースシールドを用いて、さらにキーを拡張していきたいですね。


※価格は2025年3月時点のものです。

個人で簡単に基板を発注できると工作の幅が一気に広がります!!
この記事を読んで、Conta規格準拠ボードのためにConta規格基板の自作をしてみよう!と思った方がいらっしゃったら嬉しいです。

では、Conta規格記事の第3弾があればそちらでお会いしましょう。さようなら~。

 

今回使用したボード→Picossci 2 Conta™ Base(ESP-WROOM-02/RP2350A搭載)

Contaシリーズ特集ページはこちら

 

サンプルコード

書き込み用コード


#include <Keyboard.h>
#include <Wire.h>

#define SPI1_SS   13
#define SPI1_SCK  10
#define SPI1_MOSI 11
#define SPI1_MISO 12

#define SPI0_SS   5
#define SPI0_SCK  2
#define SPI0_MOSI 3
#define SPI0_MISO 4

#define EEPROM_OPCODE_READ   0x03 // 読み出し
#define EEPROM_OPCODE_WRITE  0x02 // 書き込み
#define EEPROM_OPCODE_WREN   0x06 // 書き込みイネーブル
#define EEPROM_OPCODE_RDSR   0x05 // ステータスレジスタ読み出し
#define SR_WIP_BIT           0x01 // Write In Progress フラグ

void softSPI_shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val) {
  for (int i = 0; i < 8; i++) {
    if (bitOrder == MSBFIRST) {
      digitalWrite(dataPin, (val & 0x80) ? HIGH : LOW);
      val <<= 1;
    } else {
      digitalWrite(dataPin, (val & 0x01) ? HIGH : LOW);
      val >>= 1;
    }
    digitalWrite(clockPin, HIGH);
    digitalWrite(clockPin, LOW);
  }
}

uint8_t softSPI_shiftIn(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder) {
  uint8_t value = 0;
  for (int i = 0; i < 8; i++) {
    digitalWrite(clockPin, HIGH);
    if (bitOrder == MSBFIRST) {
      value <<= 1;
      if (digitalRead(dataPin)) value |= 0x01;
    } else {
      value >>= 1;
      if (digitalRead(dataPin)) value |= 0x80;
    }
    digitalWrite(clockPin, LOW);
  }
  return value;
}

void eepromSendWREN(uint8_t csPin, uint8_t sckPin, uint8_t mosiPin) {
  // CS Low → オペコード送信 → CS High
  digitalWrite(csPin, LOW);
  softSPI_shiftOut(mosiPin, sckPin, MSBFIRST, EEPROM_OPCODE_WREN);
  digitalWrite(csPin, HIGH);
}

uint8_t eepromReadStatus(uint8_t csPin, uint8_t sckPin, uint8_t mosiPin, uint8_t misoPin) {
  uint8_t status;
  digitalWrite(csPin, LOW);
  softSPI_shiftOut(mosiPin, sckPin, MSBFIRST, EEPROM_OPCODE_RDSR);
  status = softSPI_shiftIn(misoPin, sckPin, MSBFIRST);
  digitalWrite(csPin, HIGH);
  return status;
}

void eepromWriteByte(uint8_t csPin, uint8_t sckPin, uint8_t mosiPin, uint8_t misoPin,
                     uint8_t address, uint8_t dataByte)
{
  // 1) WREN
  eepromSendWREN(csPin, sckPin, mosiPin);

  // 2) WRITE コマンド
  digitalWrite(csPin, LOW);
  softSPI_shiftOut(mosiPin, sckPin, MSBFIRST, EEPROM_OPCODE_WRITE);
  softSPI_shiftOut(mosiPin, sckPin, MSBFIRST, address);
  softSPI_shiftOut(mosiPin, sckPin, MSBFIRST, dataByte);
  digitalWrite(csPin, HIGH);

  // 3) 書き込み完了待ち (WIP=0になるまで)
  while (true) {
    uint8_t sr = eepromReadStatus(csPin, sckPin, mosiPin, misoPin);
    if ((sr & SR_WIP_BIT) == 0) {
      break;
    }
    delay(1);
  }
}

uint8_t eepromReadByte(uint8_t csPin, uint8_t sckPin, uint8_t mosiPin, uint8_t misoPin,
                       uint8_t address)
{
  uint8_t data;
  digitalWrite(csPin, LOW);
  softSPI_shiftOut(mosiPin, sckPin, MSBFIRST, EEPROM_OPCODE_READ);
  softSPI_shiftOut(mosiPin, sckPin, MSBFIRST, address);
  data = softSPI_shiftIn(misoPin, sckPin, MSBFIRST);
  digitalWrite(csPin, HIGH);
  return data;
}

void eepromWriteTwoBytes(uint8_t csPin, uint8_t sckPin, uint8_t mosiPin, uint8_t misoPin,
                         uint8_t modKey, uint8_t normalKey)
{
  eepromWriteByte(csPin, sckPin, mosiPin, misoPin, 0x00, modKey);
  eepromWriteByte(csPin, sckPin, mosiPin, misoPin, 0x01, normalKey);
}

void setup() {
  Serial.begin(115200);
  while (!Serial) ;
  Keyboard.begin();

  pinMode(SPI1_SS, OUTPUT); digitalWrite(SPI1_SS, HIGH);
  pinMode(SPI1_SCK, OUTPUT); digitalWrite(SPI1_SCK, LOW);
  pinMode(SPI1_MOSI, OUTPUT); digitalWrite(SPI1_MOSI, LOW);
  pinMode(SPI1_MISO, INPUT);

  pinMode(SPI0_SS, OUTPUT); digitalWrite(SPI0_SS, HIGH);
  pinMode(SPI0_SCK, OUTPUT); digitalWrite(SPI0_SCK, LOW);
  pinMode(SPI0_MOSI, OUTPUT); digitalWrite(SPI0_MOSI, LOW);
  pinMode(SPI0_MISO, INPUT);

  Serial.println("[STEP1] M1, M2 を挿した状態で電源ON");
  Serial.println("    これから M1, M2 に 2バイト(修飾キー + 通常キー)を書き込みます。");
  Serial.println("    Enter を押してください。");
}
void loop() {
  static int step = 0;
  if (Serial.available() > 0) {
    // ユーザーが何かキーを押したら次のステップへ
    while (Serial.available()) {
      Serial.read(); // バッファを消費
    }
    step++;
    switch (step) {
      case 1:
        // M1
        eepromWriteTwoBytes(SPI1_SS, SPI1_SCK, SPI1_MOSI, SPI1_MISO,
                            KEY_RIGHT_CTRL, 'c');

        // M2
        eepromWriteTwoBytes(SPI0_SS, SPI0_SCK, SPI0_MOSI, SPI0_MISO,
                            KEY_RIGHT_CTRL, 'v');

        Serial.println();
        Serial.println("[STEP2] 書き込み完了。M1, M2 を取り外して M3, M4 を挿してください。");
        Serial.println("    Enter で次へ。");
        break;

      case 2:
        // M3
        eepromWriteTwoBytes(SPI1_SS, SPI1_SCK, SPI1_MOSI, SPI1_MISO,
                            KEY_LEFT_CTRL, 0x00);

        // M4
        eepromWriteTwoBytes(SPI0_SS, SPI0_SCK, SPI0_MOSI, SPI0_MISO,
                            0x00, 'D');

        Serial.println();
        Serial.println("すべての書き込みが完了しました。");
        break;

      default:
        Serial.println("これ以上のステップはありません。");
        break;
    }
  }
}

読み込みとキーボード機能のコード


#include <Keyboard.h> #include <SPI.h> #define M1_IO4 17 #define M2_IO4 19 #define M3_IO4 21 #define M4_IO4 23 // SPI0 #define SPI0_SS 5 #define SPI0_SCK 2 #define SPI0_MOSI 3 #define SPI0_MISO 4 // SPI1 #define SPI1_SS 13 #define SPI1_SCK 10 #define SPI1_MOSI 11 #define SPI1_MISO 12 #define EEPROM_READ_OPCODE 0x03 const unsigned long INITIAL_REPEAT_DELAY = 500; // ms const unsigned long REPEAT_INTERVAL = 50; // ms const unsigned long DEBOUNCE_DELAY = 20; struct KeyDefinition { uint8_t modKey; uint8_t normalKey; }; // 4つのモジュールに対応するキー定義 KeyDefinition keyM1; KeyDefinition keyM2; KeyDefinition keyM3; KeyDefinition keyM4; struct KeyState { bool wasPressed; unsigned long pressStart; unsigned long lastRepeat; bool holdActive; }; // 4つのモジュール分の状態 KeyState ksM1 = {false, 0, 0, false}; KeyState ksM2 = {false, 0, 0, false}; KeyState ksM3 = {false, 0, 0, false}; KeyState ksM4 = {false, 0, 0, false}; struct DebounceState { bool stableState; // 現在確定している論理状態(LOW=true=押下, HIGH=false=未押下)かどうか bool lastRawReading; // 直近の生の入力読み取り値 unsigned long lastChangeTime; // 状態が変わったときの時刻 }; // 4つのモジュール分のデバウンス状態 DebounceState dbM1 = {false, false, 0}; DebounceState dbM2 = {false, false, 0}; DebounceState dbM3 = {false, false, 0}; DebounceState dbM4 = {false, false, 0}; uint8_t readEEPROM_25AA010A(uint8_t spiNumber, uint8_t csPin, uint8_t address) { digitalWrite(csPin, LOW); if (spiNumber == 0) { shiftOut(SPI0_MOSI, SPI0_SCK, MSBFIRST, EEPROM_READ_OPCODE); shiftOut(SPI0_MOSI, SPI0_SCK, MSBFIRST, address); pinMode(SPI0_MISO, INPUT); uint8_t dataIn = shiftIn(SPI0_MISO, SPI0_SCK, MSBFIRST); digitalWrite(csPin, HIGH); return dataIn; } else { shiftOut(SPI1_MOSI, SPI1_SCK, MSBFIRST, EEPROM_READ_OPCODE); shiftOut(SPI1_MOSI, SPI1_SCK, MSBFIRST, address); pinMode(SPI1_MISO, INPUT); uint8_t dataIn = shiftIn(SPI1_MISO, SPI1_SCK, MSBFIRST); digitalWrite(csPin, HIGH); return dataIn; } } bool isHoldKey(uint8_t keyCode) { switch (keyCode) { case KEY_LEFT_CTRL: case KEY_LEFT_SHIFT: case KEY_LEFT_ALT: case KEY_LEFT_GUI: case KEY_RIGHT_CTRL: case KEY_RIGHT_SHIFT: case KEY_RIGHT_ALT: case KEY_RIGHT_GUI: return true; } return false; } void beginHoldKey(uint8_t keyCode, KeyState &ks) { Keyboard.press(keyCode); ks.holdActive = true; } void endHoldKey(uint8_t keyCode, KeyState &ks) { if (ks.holdActive) { Keyboard.release(keyCode); ks.holdActive = false; } } void sendNormalKey(uint8_t keyCode) { Keyboard.press(keyCode); delay(10); Keyboard.release(keyCode); } void sendCombinedKey(uint8_t modKey, uint8_t normalKey) { Keyboard.press(modKey); Keyboard.press(normalKey); delay(10); Keyboard.releaseAll(); } void handleKeyPressed(const KeyDefinition &kd, KeyState &ks) { if (kd.modKey != 0) { if (isHoldKey(kd.modKey)) { if (kd.normalKey == 0) { // 修飾キーのみ → hold beginHoldKey(kd.modKey, ks); } else { // 修飾キー + 通常キー → ワンショット同時押し sendCombinedKey(kd.modKey, kd.normalKey); } } else { // modKey が holdKey じゃない(例: CapsLockなど) if (kd.normalKey == 0) { sendNormalKey(kd.modKey); } else { sendCombinedKey(kd.modKey, kd.normalKey); } } } else { // 修飾キーなし → 通常キーのみ if (kd.normalKey != 0) { sendNormalKey(kd.normalKey); } } } void handleKeyPress(bool nowPressed, const KeyDefinition &kd, KeyState &ks) { unsigned long now = millis(); // 押され始めた if (nowPressed && !ks.wasPressed) { handleKeyPressed(kd, ks); ks.pressStart = now; ks.lastRepeat = 0; } // 継続押下 else if (nowPressed && ks.wasPressed) { bool hasHoldKey = (kd.modKey != 0 && isHoldKey(kd.modKey)); if (!hasHoldKey && kd.normalKey != 0) { // オートリピート対象 unsigned long heldTime = now - ks.pressStart; if (heldTime > INITIAL_REPEAT_DELAY) { if ((now - ks.lastRepeat) > REPEAT_INTERVAL) { handleKeyPressed(kd, ks); ks.lastRepeat = now; } } } // holdKeyあり → ずっと押しっぱなし維持 } // 離された else if (!nowPressed && ks.wasPressed) { // holdKey中なら release if (kd.modKey != 0 && isHoldKey(kd.modKey)) { endHoldKey(kd.modKey, ks); } } ks.wasPressed = nowPressed; } bool updateDebounce(DebounceState &db, bool rawReading) { // 今回のrawが前回と異なる? if (rawReading != db.lastRawReading) { // 状態が変わった → 最終変更時刻を更新 db.lastChangeTime = millis(); db.lastRawReading = rawReading; } else { // 変わっていない → 一定時間経過したら stableState を更新 if ((millis() - db.lastChangeTime) > DEBOUNCE_DELAY) { // stable状態をrawに合わせる db.stableState = db.lastRawReading; } } return db.stableState; } void setup() { Serial.begin(115200); while (!Serial) {} Keyboard.begin(); // CSピン設定 pinMode(SPI0_SS, OUTPUT); digitalWrite(SPI0_SS, HIGH); pinMode(SPI1_SS, OUTPUT); digitalWrite(SPI1_SS, HIGH); // スイッチ入力ピン pinMode(M1_IO4, INPUT_PULLUP); pinMode(M2_IO4, INPUT_PULLUP); pinMode(M3_IO4, INPUT_PULLUP); pinMode(M4_IO4, INPUT_PULLUP); // ソフトSPI用ピン pinMode(SPI0_SCK, OUTPUT); pinMode(SPI0_MOSI, OUTPUT); pinMode(SPI0_MISO, INPUT); pinMode(SPI1_SCK, OUTPUT); pinMode(SPI1_MOSI, OUTPUT); pinMode(SPI1_MISO, INPUT); Serial.println("[STEP1] M1, M2 を挿してEnterを押してください。"); while (!Serial.available()) {} while (Serial.available()) { Serial.read(); } keyM1.modKey = readEEPROM_25AA010A(1, SPI1_SS, 0x00); keyM1.normalKey = readEEPROM_25AA010A(1, SPI1_SS, 0x01); keyM2.modKey = readEEPROM_25AA010A(0, SPI0_SS, 0x00); keyM2.normalKey = readEEPROM_25AA010A(0, SPI0_SS, 0x01); Serial.print("M1 => modKey=0x"); Serial.print(keyM1.modKey, HEX); Serial.print(", normalKey=0x"); Serial.println(keyM1.normalKey, HEX); Serial.print("M2 => modKey=0x"); Serial.print(keyM2.modKey, HEX); Serial.print(", normalKey=0x"); Serial.println(keyM2.normalKey, HEX); Serial.println(); Serial.println("[STEP2] M1, M2 を外し、M3, M4 を挿してEnterを押してください。"); while (!Serial.available()) {} while (Serial.available()) { Serial.read(); } keyM3.modKey = readEEPROM_25AA010A(1, SPI1_SS, 0x00); keyM3.normalKey = readEEPROM_25AA010A(1, SPI1_SS, 0x01); keyM4.modKey = readEEPROM_25AA010A(0, SPI0_SS, 0x00); keyM4.normalKey = readEEPROM_25AA010A(0, SPI0_SS, 0x01); Serial.print("M3 => modKey=0x"); Serial.print(keyM3.modKey, HEX); Serial.print(", normalKey=0x"); Serial.println(keyM3.normalKey, HEX); Serial.print("M4 => modKey=0x"); Serial.print(keyM4.modKey, HEX); Serial.print(", normalKey=0x"); Serial.println(keyM4.normalKey, HEX); Serial.println(); Serial.println("読み込み完了"); } void loop() { // 1) 各入力ピンの生の状態(LOW=押下, HIGH=未押下)を読む bool rawM1 = (digitalRead(M1_IO4) == LOW); bool rawM2 = (digitalRead(M2_IO4) == LOW); bool rawM3 = (digitalRead(M3_IO4) == LOW); bool rawM4 = (digitalRead(M4_IO4) == LOW); // 2) デバウンス更新 → 確定した押下状態 bool pressedM1 = updateDebounce(dbM1, rawM1); bool pressedM2 = updateDebounce(dbM2, rawM2); bool pressedM3 = updateDebounce(dbM3, rawM3); bool pressedM4 = updateDebounce(dbM4, rawM4); // 3) handleKeyPress() で処理 handleKeyPress(pressedM1, keyM1, ksM1); handleKeyPress(pressedM2, keyM2, ksM2); handleKeyPress(pressedM3, keyM3, ksM3); handleKeyPress(pressedM4, keyM4, ksM4); // 適当にループを少し待つ(1〜5ms程度) delay(5); }

 

 

前の記事 Fill In the Gaps With These New Products
次の記事 【MyActuator】RMD-X4を動かしてみる