見出し画像

自作ショートカット用キーボードを作ろう(回路図・コード付き)

とりあえず作ったもの

見にくいけど中身

こんな感じで自作ショートカットキーボードを作ったので、これの作り方の説明をしていこうと思います。
いつも通り配線図→コード説明の流れで書いていこうと思います。
ただケースとかは3Dプリンターでそれ用に作ったものなので、ガワに関しては省きます。

使ったもの

・ACEIRMC Pro Micro(Arduino Arduino Leonardo互換マイクロコントローラ)
・PCB SU120 自作キーボード基盤 TALP KEYBOARD

・1KΩ 抵抗器 *2
・ダイオード 1N5819 *4

前提

回路は4*2のキーマトリックス回路で作成しました。
こちらの記事を参考にしました。

4*4のキーマトリックス回路

上記の回路図を4*2にして作成しています。

キーマトリックス回路とはなんぞやという方もいるかと思います。
自分も最初はよく分かってなかったですが、説明としては以下となります。
..

うまく説明できないのでChatGPTに説明してもらいます。

キーマトリックス回路(Key matrix circuit)は、主にキーボードやボタン入力などの多数の入力を処理するために使用される回路です。この回路は、キーマトリックスと呼ばれる行列状の配線構造を使用して、複数のキーやボタンを制御します。

キーマトリックス回路では、行と列の組み合わせで各キーやボタンを識別します。一般的なキーボードの場合、キーマトリックスは8x8や10x10の行列として構成されていますが、大きさや形状は特定のアプリケーションやデバイスによって異なる場合もあります。

回路の基本的な動作は、各キーが特定の行と列に接続されたスイッチで構成されているということです。行と列の間には、キースキャンコントローラやマイコン(マイクロコントローラ)といった制御デバイスが接続されています。制御デバイスは、キーマトリックスの各行と列を順番にスキャンし、押されたキーの位置を特定します。

キーマトリックス回路の動作は、マトリックス内の特定の行をアクティブにすることで始まります。その後、制御デバイスは各列を順番にチェックし、アクティブな行と接続されたキーが押されているかどうかを検出します。キーが押されていれば、制御デバイスはその位置を特定し、適切なアクションを実行します(たとえば、キーの値を読み取る、特定の処理をトリガするなど)。

この方法により、限られた数のピンで多数のキーを制御できるため、キーマトリックス回路は非常に効率的な方法として広く使用されています。ただし、同時に複数のキーを押す(キーチャタリングとして知られる問題)場合や、特定のキーが同時に押されることがある場合には、回路のデバウンシングやゴースト入力の問題が発生することがあります。これらの問題を解決するために、さまざまなテクニックやアルゴリズムが使用されます。
以上が、キーマトリックス回路の基本的な説明です。

ChatGPT

まぁ行と列で制御ができるので、16個のボタンを接続しようとするとpinも16個必要になってしまいますが、4*4のキーマトリックス回路で作ることでpinの数を8個に抑えられるという感じですね。
まぁ今回のとかは8個そのまま使ってもpin足りてるので、やる必要はなかったんですが、勉強のためですね。

説明している記事や、なんか他の回路図のものもあったり、情報は多々あるので、興味あればそちらで。。

回路図

実際に自分がArduinoで組んだ配線はこちらになります。

見にくいですが、D2-D5のpinからダイオード(逆流防止のため)を挟んで、各々のボタンにつなげています。
列が4個、行が2個なので一つのpinにボタンが2個接続されています。

抵抗はプルダウン抵抗のためですね。


コード

全体

 #include  "Keyboard.h"

const int KEYIN[] = { 8, 9 };
const int KEYOUT[] = { 2, 3, 4, 5 };

unsigned long previousMillis = 0;
const long interval = 30;
int columnNum = 0;
int sw[4][2] = { 0 };
char keyMap[4][2] = {
  { 'A', 'B' },
  { 'C', 'D' },
  { 'E', 'F' },
  { 'G', 'H' }
};

void setup() {
  for (int i = 0; i < 4; i++) {
    pinMode(KEYOUT[i], OUTPUT);
    digitalWrite(KEYOUT[i], LOW);
  }
  for (int i = 0; i < 2; i++) {
    pinMode(KEYIN[i], INPUT);
  }
  Keyboard.begin();
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    digitalWrite(KEYOUT[columnNum], HIGH);

    for (int rowNum = 0; rowNum < 2; rowNum++) {
      int readValue = digitalRead(KEYIN[rowNum]);
      if (readValue != sw[columnNum][rowNum] && readValue == HIGH) {
        keyPress(keyMap[columnNum][rowNum]);
      }

      sw[columnNum][rowNum] = readValue;
      delay(10);
      Keyboard.releaseAll();
    }

    digitalWrite(KEYOUT[columnNum], LOW);
    columnNum++;
    columnNum %= 4;
  }
}

void keyPress(char key) {
  switch (key) {
    case 'A':
      Keyboard.write('p');
      Keyboard.write('r');
      Keyboard.write('i');
      Keyboard.write('n');
      Keyboard.write('t');
      Keyboard.write('(');
      Keyboard.write(')');
      break;

    case 'B':
      Keyboard.press(KEY_LEFT_ALT);
      Keyboard.press(KEY_RIGHT_ARROW);
      break;

    case 'C':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press('/');
      break;

    case 'D':
      Keyboard.press(KEY_LEFT_ALT);
      Keyboard.press(KEY_LEFT_ARROW);
      break;

    case 'E':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('j');
      break;

    case 'F':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press(KEY_RIGHT_ARROW);
      break;

    case 'G':
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('i');
      break;

    case 'H':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press(KEY_LEFT_ARROW);
      break;

    default:
      break;
  }
}


そしたら部分的に説明していこう思います。
まぁ面倒なのでChatGPTにも説明してもらおうと思います。。

プロパティや変数、定数を定義

 #include  "Keyboard.h"

const int KEYIN[] = { 8, 9 };
const int KEYOUT[] = { 2, 3, 4, 5 };

unsigned long previousMillis = 0;
const long interval = 30;
int columnNum = 0;
int sw[4][2] = { 0 };
char keyMap[4][2] = {
  { 'A', 'B' },
  { 'C', 'D' },
  { 'E', 'F' },
  { 'G', 'H' }
};

#include "Keyboard.h": これはArduinoのキーボードライブラリをインクルードするためのディレクティブです。これにより、キーボードからの入力を読み取るための関数や定数にアクセスすることができます。

  • const int KEYIN[] = { 8, 9 };: これはキーボードの入力行をArduinoのピンにマッピングする定数配列を定義します。値 89 は、それぞれArduinoのピン番号8と9を表します。

  • const int KEYOUT[] = { 2, 3, 4, 5 };: これはキーボードの出力列をArduinoのピンにマッピングする定数配列を定義します。値 2, 3, 4, 5 は、それぞれArduinoのピン番号2、3、4、5を表します。

  • unsigned long previousMillis = 0;: これは前回のスキャンからの経過時間を追跡するための変数を定義します。

  • const long interval = 30;: これはキーボードのスキャンレートを制御する定数を定義します。ここでは30ミリ秒ごとにキーボードがスキャンされます。

  • int columnNum = 0;: これは現在スキャン中の列のインデックスを追跡する変数を定義します。

  • int sw[4][2] = { 0 };: これはキーボードの各キーの現在の状態(押されているか放されているか)を追跡するための2次元配列を定義します。

  • char keyMap[4][2] = { { 'A', 'B' }, { 'C', 'D' }, { 'E', 'F' }, { 'G', 'H' } };: これは物理キーを特定の文字にマッピングする2次元配列を定義します。ここでは、AからHまでの8つのキーが、キーボードの各行と列にマッピングされています。

void setup()

void setup() {
  for (int i = 0; i < 4; i++) {
    pinMode(KEYOUT[i], OUTPUT);
    digitalWrite(KEYOUT[i], LOW);
  }
  for (int i = 0; i < 2; i++) {
    pinMode(KEYIN[i], INPUT);
  }
  Keyboard.begin();
}

こちらは初期化設定ですね。
各pinのINPUT,OUTPUT設定とkeyboard.begin()を書くことでキーボードライブラリの機能を使えるようにします。

void keyPress()

void keyPress(char key) {
  switch (key) {
    case 'A':
      Keyboard.write('p');
      Keyboard.write('r');
      Keyboard.write('i');
      Keyboard.write('n');
      Keyboard.write('t');
      Keyboard.write('(');
      Keyboard.write(')');
      break;

    case 'B':
      Keyboard.press(KEY_LEFT_ALT);
      Keyboard.press(KEY_RIGHT_ARROW);
      break;

    case 'C':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press('/');
      break;

    case 'D':
      Keyboard.press(KEY_LEFT_ALT);
      Keyboard.press(KEY_LEFT_ARROW);
      break;

    case 'E':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('j');
      break;

    case 'F':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press(KEY_RIGHT_ARROW);
      break;

    case 'G':
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('i');
      break;

    case 'H':
      Keyboard.press(KEY_LEFT_GUI);
      Keyboard.press(KEY_LEFT_ARROW);
      break;

    default:
      break;
  }
}

これは後で説明するvoid loop()内で発火させている関数になります。
どのボタンが押されたかをA-Hのchar(文字)で判断し、それに伴った処理を行います。
ちなみにこれらはショートカットキー用として作成してあるので例えばCの場合だと Keyboard.press(KEY_LEFT_GUI);  Keyboard.press('/'); となっているのでこれが発火されるとcommand + / のショートカットが発火されることになります。
press();を組み合わせることでキーの同時押しをさせていると言うことですね。
ちなみに通常の文字を打つ際にはwrite();を使用すればOKです。
なのでAの場合だと"print()"と言う文字列がそのまま打ち込まれるようになっています。


void loop()

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    digitalWrite(KEYOUT[columnNum], HIGH);

    for (int rowNum = 0; rowNum < 2; rowNum++) {
      int readValue = digitalRead(KEYIN[rowNum]);
      if (readValue != sw[columnNum][rowNum] && readValue == HIGH) {
        keyPress(keyMap[columnNum][rowNum]);
      }

      sw[columnNum][rowNum] = readValue;
      delay(10);
      Keyboard.releaseAll();
    }

    digitalWrite(KEYOUT[columnNum], LOW);
    columnNum++;
    columnNum %= 4;
  }
}

このloop関数はArduinoプログラムの主要な部分で、一度開始されると無限ループします。この関数はキーボードをスキャンして特定のキーが押されたかどうかを確認し、押されていた場合には対応するコマンドを送信します。

以下はこの関数の各行の説明です:

  • unsigned long currentMillis = millis();: 現在の時間をミリ秒単位で取得し、currentMillis変数に保存します。

  • if (currentMillis - previousMillis >= interval) {: 前回のキーボードスキャンから設定した間隔(interval)以上時間が経過していた場合に以下のコードブロックを実行します。

  • previousMillis = currentMillis;: 現在の時間を前回のスキャン時間として保存します。

  • digitalWrite(KEYOUT[columnNum], HIGH);: 現在スキャン中の列をアクティブ(HIGH)にします。

  • for (int rowNum = 0; rowNum < 2; rowNum++) {: それぞれの行について以下の処理を行うループを開始します。

  • int readValue = digitalRead(KEYIN[rowNum]);: 現在アクティブな列と指定された行が交差するキーの状態を読み取ります。

  • if (readValue != sw[columnNum][rowNum] && readValue == HIGH) {: キーが前回のスキャンから状態が変わり、かつ現在押されている(HIGH)場合、以下のコードブロックを実行します。

  • keyPress(keyMap[columnNum][rowNum]);: 対応するキーが押された場合の処理を呼び出します。押されたキーはkeyMapから取得されます。

  • sw[columnNum][rowNum] = readValue;: 現在のキーの状態を保存します。

  • delay(10);: 小さな遅延を挿入します。これは、一部のキーボードハードウェアがすべてのキーストロークを正確に登録するのに必要な時間を確保するためです。

  • Keyboard.releaseAll();: すべてのキーをリリースします。これは、次のスキャンが始まる前に任意のキーストロークが残っていないことを確認します。

  • digitalWrite(KEYOUT[columnNum], LOW);: 現在アクティブだった列を非アクティブ(LOW)に戻します。

  • columnNum++;: 次の列に移動します。

  • columnNum %= 4;: 列数が4を超えた場合、0にリセットします。これにより、スキャンがキーボードの最初の列から再び始まります。

簡単に流れをChatGPTにまとめてもらうと下記の感じです。

現在の時間をチェックし、前回のチェックから一定時間(30ミリ秒)が経過していたら処理を行います。

  1. 次に、各列を順に選択(アクティブ化)し、その列の全てのキー(行)を順に読み取ります。

  2. もし、特定のキーが押されている状態(HIGH)で、かつそのキーの状態が前回と異なる場合、そのキーに対応する動作を行います(keyPress関数が呼び出されます)。

  3. その後、そのキーの状態を保存し、一定時間待った後(10ミリ秒)、全てのキーの押下状態をリセット(releaseAll)します。

  4. 最後に、選択していた列を非アクティブ化し、次の列へ移動します。全ての列をスキャンしたら、最初の列に戻ります。

このように、この関数は定期的にキーボードの各キーをスキャンし、特定のキーが押されたときの動作を制御します。


終わりに

正直コード自体も結構ChatGPTのおかげでなんとか完成できたって感じですね。
このnoteも最初自分の言葉で書いてはいたんですが。。
めんどくさくなったのと、書いたとしても絶対GPTに書いてもらったやつの方が分かりやすいという結論になりましたね。

キーマトリックス回路の理解ができ、少しは成長できたと感じましたが、それよりChatGPTの凄さが肌身に感じましたね。

まぁこの記事が何かの参考になれば幸いです。


参考資料


この記事が気に入ったらサポートをしてみませんか?