
【自作キーボード】自作のConta基板で遊んでみよう!
こんにちは。アルバイトの田中です。
2ヶ月ほど前の記事にて、「Conta規格基板の自作をしてみたでお会いしましょう」と締めて、早速第二弾の記事です!
今回はConta規格のサイズに目を付けて、機能を維持して取り外し可能なキーボードモジュールを製作してみました。最終的に、Picossciをキーボード化してみようと思います!
誰でも使えるツールで簡単に設計して発注できるので、ぜひ真似してみてください。
設計について
設計したのはキーボードのキースイッチとEEPROMが入っているだけのモジュールですが、電子工作の初心者でConta規格の自作erが現れることを期待して、設計の手順を簡単に書いていこうと思います。
まず、簡単に規格の寸法とピン配列のルールを紹介します。
基板のサイズは以下の画像の通りです。 基板サイズは788 milの正方形で、取り付け穴としてM2サイズの下穴が二つ開いています。 基板設計の際に基板カット用の外形寸法が必要となりますので、こちらからConta規格の外形図(dxf拡張子)をダウンロードしてご利用ください。
ピン配置は以下の通りです。I2CとSPI用ピンと、機能の指定がないIOピンが四つあります。今回は使いませんが、5Vの電源を使うことも可能です。
その他規格の詳細はこちらをご参照ください。
使用部品
- ロープロファイル MX互換キースイッチ(12個パック)
- 自作基板
- 普通のピンヘッダ10本セット
- 25AA010AT-I/OT
- 10kΩ抵抗(1608 Metric)
部品は以上のものを使いました。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ピンにつないでおけばよかったと思いました...。)
ソフトはページ最下部に掲載しています。
完成
完成したキーボードモジュールは以下の通りです。
使い方
-
キーモジュールに機能を書き込む必要があるので、書き込み用ソフトをPicossciに入れます。書き込みたいキーの機能はコードの以下の部分に入力します。対応する修飾キーはkeyboardライブラリの説明を参照ください。
eepromWriteTwoBytes(SPI◯_SS, SPI◯_SCK, SPI◯_MOSI, SPI◯_MISO, 修飾キー(ctrlなど), 通常キー(英数字));
-
Picossciからのシリアルデータに従ってキーを差し込み、データを書き込みます。
以下の写真はM1、M2に差し込んだ状態。
以下の写真はM3、M4に差し込んだ状態。
- キーへのデータ書き込みが終わったら、読み込みとキーボード機能用のソフトをPicossciに入れます。
- Picossciからのシリアルデータに従ってキーを差し込み、データをPicossciに読み込ませます。
- キーを押すとキーに書き込んだ機能が使えます。
今回は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);
}