ゆかりメモ

いろんなことのメモです。

オリジナルキーボードを作ってみる その9「シリアル通信の実装」

さて、前回で左右分離型でないキーボードのファームウェアは作れるようになったと思います。 今回はシリアル通信を実装して、左右分離型のキーボードを作れるようになりましょう。

前回の記事はこちら。

eucalyn.hatenadiary.jp

ではまずシリアル通信の規格を考えることにしましょう。

シリアル通信で送るべきデータを考える

初期の企画を見直して、最終形にちゃんと対応できる企画を考えます。

eucalyn.hatenadiary.jp

企画編で書いていたとおり、このキーボードの概要は以下のとおりです。

  1. 片手にそれぞれ23キーずつ配置された左右分離型キーボードである。
  2. ジョイスティック及びトラックボールといったポインティングデバイスを備える。
  3. レイヤー機能を実装する。

つまり今回のシリアル通信においては、最終的にマウスのデータとの共存を考える必要があり、更に片手23キー分を捌けないといけないですね。 且つレイヤー機能のことも視野に入れる必要があります。

Arduinoのシリアル通信の基本は1Byte(8bit)単位でのやりとりです。 Arduinoの場合、Serial.begin()のときの引数によってそのあたりをある程度柔軟に設定できるので、 最悪それに頼ることにしますが、とりあえず1Byteのやり取りで可能かどうかを考えてみましょう。

キーのデータのやり取りの場合、単純に考えると2通りのデータのやり取りの方法が考えられます。

一つはキーコードそのものを送る方法、もう一つは押されたキーの物理的な座標を送る方法。

前者はキーが押された際にキーマップと照らし合わせて、aは0x61といったようなキーコードを送る方式。 後者は、(0,1)といったキーの位置を送って、受け取った側でキーマップと照らし合わせる方法です。

ただし前者の場合そもそもキーコード自体が1Byte分のデータなので、それ以上何も送れなくなってしまうということと、 後々実装予定のレイヤー機能に対応が難しそうだと考えました。

後者の場合、今回は4x6の最大24キーを捌ければ十分なので、それに必要なbit数は5。 残り3bitも残るなので余裕がありそうです。

なので今回はキーの位置を送る方式にします。

ビット演算を使って、8bitの中にうまくいろんなデータを詰め込むことにします。

細かい紆余曲折は省きますが、僕の考えた通信の規格(キーボード分)はこんな感じになりました。

8桁目:キーボードorマウス(キーボードの場合は0)
7桁目:離したor押した(離した時0)
6桁目:左半分or右半分(左の場合0)
4-5桁目:行数(0から3)
1-3桁目:列数(0から5)

例えば以下の通りです。

0b00110010=右手分の(2,2)のキーを離した
0b01011101=左手分の(3,5)のキーを押した

実は6桁目はなくてもいいのですが(シリアルで送られてきている時点でマスターとは反対側であることが明確だから)、この先の処理の流れがわかりやすかったのでつけています。
このままでは数字行を含んだ5行キーには対応できないので、4-6桁目を行数に使用すれば 最大8x8キーに対応できるようになります。

キーの指定を楽にする

さて、ちょっとこのままではキーマップを書くのがあまりにも大変なので、他のキーボードでよく使われているQMK Firmwareライクにキーを指定できるようにしましょう。

もちろん最初の変数宣言の辺りで#define KC_A 0x61 みたいな感じでダラダラ書き連ねていってもいいのですが、それでは無駄に長くなりますので、ここでは別にヘッダファイルを用意することにします。

今のファームウェアと同じ階層にKeydefine.hという新規ファイルを作り、下記のコードをすべてコピペしてください。

#ifndef INCLUDED_Keydefine_h_

  #define INCLUDED_Keydefine_h_

  #define NONE    0x00
  #define _______ 0x00
        
  #define KC_ENT  0xB0
  #define KC_ESC  0xB1
  #define KC_BSPC 0xB2
  #define KC_DEL  0xD4
  #define KC_TAB  0xB3
  #define KC_CAPS 0xC1
  #define KC_LCTL 0x80
  #define KC_LSFT 0x81
  #define KC_LALT 0x82
  #define KC_LGUI 0x83
  #define KC_RCTL 0x84
  #define KC_RSFT 0x85
  #define KC_RALT 0x86
  #define KC_RGUI 0x87

  #define KC_PGUP 0xD3
  #define KC_PGDN 0xD6
  #define KC_HOME 0xD2
  #define KC_END  0xD5
  #define KC_LEFT 0xD8
  #define KC_RGHT 0xD7
  #define KC_UP   0xDA
  #define KC_DOWN 0xD9

  #define KC_0 0x30
  #define KC_1 0x31
  #define KC_2 0x32
  #define KC_3 0x33
  #define KC_4 0x34
  #define KC_5 0x35
  #define KC_6 0x36
  #define KC_7 0x37
  #define KC_8 0x38
  #define KC_9 0x39

  #define KC_EXLM 0x21
  #define KC_AT   0x40
  #define KC_HASH 0x23
  #define KC_DLR  0x24
  #define KC_PERC 0x25
  #define KC_CIRC 0x5E
  #define KC_AMPR 0x26
  #define KC_ASTR 0x2A
  #define KC_LPRN 0x28
  #define KC_RPRN 0x29

  #define KC_MINS 0x2D
  #define KC_EQL  0x3D
  #define KC_SPC  0x20
  #define KC_LBRC 0x5B
  #define KC_RBRC 0x5D
  #define KC_LCBR 0x7B
  #define KC_RCBR 0x7D
  #define KC_BSLS 0x5C
  #define KC_SCLN 0x3B
  #define KC_QUOT 0x27
  #define KC_GRV  0x60
  #define KC_COMM 0x2C
  #define KC_DOT  0x2E
  #define KC_SLSH 0x2F

  #define KC_A 0x61
  #define KC_B 0x62
  #define KC_C 0x63
  #define KC_D 0x64
  #define KC_E 0x65
  #define KC_F 0x66
  #define KC_G 0x67
  #define KC_H 0x68
  #define KC_I 0x69
  #define KC_J 0x6A
  #define KC_K 0x6B
  #define KC_L 0x6C
  #define KC_M 0x6D
  #define KC_N 0x6E
  #define KC_O 0x6F
  #define KC_P 0x70
  #define KC_Q 0x71
  #define KC_R 0x72
  #define KC_S 0x73
  #define KC_T 0x74
  #define KC_U 0x75
  #define KC_V 0x76
  #define KC_W 0x77
  #define KC_X 0x78
  #define KC_Y 0x79
  #define KC_Z 0x7A

#endif

僕が自分が使いたいキーを雑にまとめて書いただけなので、多々足りないキーがあると思いますがご了承ください。

さらにファームの一番上でincludeをしてる辺りに#include "Keydefine.h"を書き足します。

そうするとこれからはKeydefine.hで指定されているものに関してはその名前で呼び出すことができます。

ゆーかりキーボードにおけるキー座標の処理について

シリアル通信を実装する前に、ゆーかりキーボードの処理について説明します。 ここからはどんどんゆーかりキーボードの話になってくるので、もしオリジナルで作られている方は適宜読み替えていってください。

キー座標の処理について、例えばLet's SplitキーボードはPlanckの分離のような形で、左右が列で接続されています。 左は0-5列、右は6-11列という感じです。

ゆーかりキーボードの場合、左右は行で繋いでいます。 左は0-3行目、右は4-7行目です。

これは他への汎用性はないのですが、こうすると処理がとても単純になったためです。

先程シリアル通信の規格を考える際に

6桁目:左半分or右半分(左の場合0)
4-5桁目:行数(0から3)
1-3桁目:列数(0から5)

と言っていましたが、これを4-6桁目を行数と読み解くと、ちょうど左右合わせて0-7行を取り出すことができます。

具体的には以下のようになります。

右の2行目 = 0b110 = 6
左の3行目 = 0b011 = 3
右の0行目 = 0b100 = 4

つまり処理は5桁で行い、マッピングを調べる時に6桁目に左右の判定を足すだけでちょうどうまく処理することができます。

このあたりは汎用性がないので、もし他の形のキーボードを考える際には考え直す必要があると思います。

そのため、この方法をうまく活かすには片手4行がちょうどよいわけです。

ProMicroのシリアル通信の注意点

Serial.write();で送っても届きません。

ProMicroのTx,RxピンはSerial1という名前で呼び出す必要があります。

SerialはPCとの通信専用です。

ファームウェアの書き換え

そんなわけでいきなりですがもう4x6キーボードを2台という体のファームウェアを書くことになります。

2台のArduinoが必要ですが、ここからはProMicroを使用していると仮定しています。

#include "Keyboard.h"

// Left : 0  Right : 1
const byte thisSide = 0;

const int rowNum = 4;
const int colNum = 6;

const int rowPin[rowNum] = { 3, 4, 11, 12 };
const int colPin[colNum] = { 5, 6, 7, 8, 9, 10 };
const byte keyMap[rowNum*2][colNum]  = {
  // LeftSide
  {KC_TAB,  KC_Q,    KC_W,    KC_E,   KC_R,    KC_T},
  {KC_CAPS, KC_A,    KC_S,    KC_D,   KC_F,    KC_G},
  {KC_LSFT, KC_Z,    KC_X,    KC_C,   KC_V,    KC_B},
  {NONE,    KC_LALT, KC_LGUI, KC_LSFT, KC_SPC, NONE},
  // RightSide
  {KC_Y, KC_U,    KC_I,    KC_O,    KC_P,    KC_EQL},
  {KC_H, KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT},
  {KC_N, KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_MINS},
  {NONE, KC_BSPC, KC_ENT,  KC_RGUI, KC_BSLS, NONE}
};

bool currentState[rowNum][colNum];
bool beforeState[rowNum][colNum];

int i,j,row,isPress;

byte sendData,readData;

void setup() {
  
  for( i = 0; i < rowNum; i++){
    pinMode(rowPin[i],OUTPUT);
    digitalWrite(rowPin[i],HIGH);
  }
  
  for( i = 0; i < colNum; i++){
    pinMode(colPin[i],INPUT_PULLUP);
  }
  
  for( i = 0; i < rowNum; i++){
    for( j = 0; j < colNum; j++){
      currentState[i][j] = HIGH;
      beforeState[i][j] = HIGH;
    }
  }
  
  Serial.begin(9600);
  Serial1.begin(9600);
  Keyboard.begin();
}

void loop() {
  for( i = 0; i < rowNum; i++){
    digitalWrite( rowPin[i], LOW );
    
    row = i + 4 * thisSide;
    
    for( j = 0; j < colNum; j++){
      currentState[i][j] = digitalRead(colPin[j]);
      
      if ( currentState[i][j] != beforeState[i][j] ){
        
        Serial.print("key(");
        Serial.print(i);
        Serial.print(",");
        Serial.print(j);
        Serial.print(")");
        
        if ( currentState[i][j] == LOW){
          isPress = 1;
          Serial.println(" Push!");
          Keyboard.press( keyMap[row][j] );
        } else {
          isPress = 0;
          Serial.println(" Release!");
          Keyboard.release( keyMap[row][j] );
        }
        beforeState[i][j] = currentState[i][j];
        sendData = isPress << 6 | row << 3 | j;
        Serial1.write(sendData);
      }
    }
    digitalWrite( rowPin[i], HIGH );
  }
  if(Serial1.available()){
    readSerial();
  }
}

void readSerial(){
  int keyRow, keyCol;
  
  readData = Serial1.read();
  Serial.println(readData);
  
  if ( readData & 0b10000000 ){
  } else {
    isPress = readData >> 6;
    keyRow = readData & 0b00111000 >> 3;
    keyCol = readData & 0b00000111;
    
    if( isPress ){
      Keyboard.press( keyMap[keyRow][keyCol] );
    } else {
      Keyboard.release( keyMap[keyRow][keyCol] );
    }
  }
  
}

ちょっと試せてないのですが、こんな感じだと思います。 (動かなかったらTwitterで教えてください…。)

もっと機能ごとに関数を分けて、適宜それを呼び出した方がキレイですね。

これを2台のProMicroに書き込みます。
(書き込む方に応じて、thisSideの部分を書き換えます。)

L側のProMicroをPCに繋ぐとすると、RのTXをLのRXへ、あとは5Vを5V、GNDをGNDに繋ぐとR側の電源が入り、シリアル通信が始まるはずです。

LのTXをRのRXへも繋げば、両手分どちらをPCに繋いでも使えるキーボードになります。


さて、これでとりあえずファームウェア編は一段落かと思います。

ここまでの記事を読んで頂いた方は、キーボードの配線をして、ファームウェアを書くところまでできるようになったでしょう。

更に適度に応用すればレイヤー機能まで実装できると思います。

(ゆーかりキーボードの場合、実際は最終的に2次元配列処理自体を辞めたのでまたかなり違う感じになっています。 ゆーかりキーボードの最終的なファームウェアの説明ももしかしたらいつかするかもしれませんが、今のところ完成していないので未定です。)

でもいくらファームウェアがかけてもそれだけじゃ困りますね。そう筐体がないからです。

次回からは3Dプリンタで立体的な筐体を作ることを目指します。

なお、本来はFusion360といったCADソフトを使った方がいいのかもしれませんが、 僕自身それらに慣れていないということと、ゆーかりキーボードの筐体はBlenderで作ったので、 Blenderを使ってモデリング→DMM3Dプリントに発注するという流れを説明したいと思います。

ツールによって得意な面と不得意な面があると思うので、参考にしてみてください。


まとめ記事はこちら。

eucalyn.hatenadiary.jp