ArduinoでUSB接続の分割キーボードを作った話 - ファームウェア編

f:id:hrhg:20170211154409j:plain

最初はErgoDox用にカスタマイズされたTMKを元に、IOのポートとIOエキスパンダあたりを改造するつもりでした。bootloader関係を調べているうちに、試しにArduinoでちょっと作り始めてみたら、すごく簡単にできそうだったので、Arduino入門もかねてArduino環境で作ってみました。 キーボードとして必要最低限の事しかしておらず、レイヤー機能とかはありません。

目次

プログラム本体

/**
  分割キーボード
  split_keyboard.ino

  Copyright (c) 2017 Hiroshi Higuchi

  Released under the MIT license
  http://opensource.org/licenses/mit-license.php

*/

#include <stdint.h>
#include  <MsTimer2.h>
#include  <Keyboard.h>
#include  <Adafruit_MCP23017.h>

#define     NUM_ROW     8
#define     NUM_COL     6
#define     TIMER_MS    1   // 1msec

uint8_t row_pins[] = { 1, 0, 4, 5, 6, 7, 8, 9};
uint8_t col_pins[] = {10, 16, 14, 15, 18, 19, 20, 21};  // output

#define   NUM_ROW_BASE   sizeof(row_pins)

#define   NUM_COL_BASE   sizeof(col_pins)
#define   NUM_COL_EXP     8
#define   NUM_COL         (NUM_COL_BASE + NUM_COL_EXP)

uint8_t row_tmp[NUM_COL];             // 行単位の仮状態
uint8_t row_st[NUM_COL];              // 行単位の確定状態
volatile uint8_t row_timer[NUM_COL];  // 行単位のタイマー
uint8_t col_index;

Adafruit_MCP23017 mcp;
uint8_t mcp_write_data;
uint8_t mcp_read_data;

#define     MCP_ADRS        1
#define     MCP_PORT_A      0
#define     MCP_PORT_B      1
#define     MCP_ALL_OUTPUT  0x00
#define     MCP_ALL_INPUT   0xff
#define     MCP_ALL_PULLUP  0xff

uint8_t key_table[] = {
  // right side
  K_,    K_,    K_,    K_,    K_,    K_,    K_,    K_,       // 0
  K_,    K_F7 , K_F8,  K_F9,  K_F10, K_F11, K_F12, K_DEL,    // 1
  K_,    K_EQL, K_6,   K_7,   K_8,   K_9,   K_0,   K_BS,     // 2
  K_,    K_CLS, K_Y,   K_U,   K_I,   K_O,   K_P,   K_,       // 3
  K_,    K_BSH, K_H,   K_J,   K_K,   K_L,   K_SCN, K_ENT,    // 4
  K_,    K_INT3,K_N,   K_M,   K_KNM, K_DOT, K_SLH, K_RSFT,   // 5
  K_RSFT,K_SP,  K_RCTL,K_RALT,K_LFT, K_DN,  K_UP,  K_RIT,    // 6
  K_,    K_,    K_,    K_,    K_,    K_,    K_,    K_,       // 7
  // left side
  K_,    K_,    K_,    K_,    K_,    K_,    K_,    K_,       // 0
  K_ESC, K_F1,  K_F2,  K_F3,  K_F4,  K_F5,  K_F6,  K_,       // 1
  K_BQT, K_1,   K_2,   K_3,   K_4,   K_5,   K_HFN, K_,       // 2
  K_TAB, K_Q,   K_W,   K_E,   K_R,   K_T,   K_OPN, K_,       // 3
  K_CPS, K_A,   K_S,   K_D,   K_F,   K_G,   K_SCT, K_,       // 4
  K_LSFT,K_Z,   K_X,   K_C,   K_V,   K_B,   K_INT1,K_,       // 5
  K_,    K_INT2,K_INT5,K_INT4,K_LALT,K_LCTL,K_BS  ,K_LSFT,   // 6
  K_,    K_,    K_,    K_,    K_,    K_,    K_,    K_        // 7
};

#define   INIT_ROW_TIMER  3

uint8_t   col_exp_data;

void timer_1msec() {
  for (uint8_t i = 0; i < NUM_COL; i++) {
    if (0 != row_timer[i]) {
      row_timer[i]--;
    }
  }
}

void setup() {
  // put your setup code here, to run once:

  // シリアルモニタ初期化
  Serial.begin(9600);

  // キーマトリクスの初期化
  col_index = NUM_COL;

  // バッファの初期化
  for (uint8_t i = 0; i < NUM_COL; i++) {
    row_tmp[i] = 0xff;
    row_st[i] = 0xff;
    row_timer[i] = 0;
  }

  // 出力ポート値の初期化
  for (uint8_t i = 0; i < NUM_COL_BASE; i++) {
    digitalWrite(col_pins[i], HIGH);
  }

  // ポート方向の初期化
  Serial.print("row_pins:");
  Serial.println(sizeof(row_pins));
  for (uint8_t i = 0; i < NUM_ROW_BASE; i++) {
    pinMode(row_pins[i], INPUT_PULLUP);
  }
  for (uint8_t i = 0; i < NUM_COL_BASE; i++) {
    pinMode(col_pins[i], OUTPUT);
  }

  // MCP23017の初期化
  mcp.begin(MCP_ADRS);
  col_exp_data = 0xff;
  mcp.writeGPIO(MCP_PORT_A, col_exp_data);
  mcp.pinModeByte(MCP_PORT_A, MCP_ALL_OUTPUT);
  mcp.pullUpByte(MCP_PORT_B, MCP_ALL_PULLUP);
  mcp.pinModeByte(MCP_PORT_B, MCP_ALL_INPUT);

  // HIDの初期化
  Keyboard.begin();

  // タイマーの初期化
  MsTimer2::set(TIMER_MS, timer_1msec);
  MsTimer2::start();
}

void loop() {
  // --- col ---
  if (col_index < (NUM_COL - 1)) {
    col_index++;
  } else {
    col_index = 0;
  }
  if (0) {
    Serial.print("col_index:");
    Serial.println(col_index);
  }
  if (col_index < NUM_COL_BASE) {
    for (uint8_t i = 0; i < NUM_COL_BASE; i++) {
      if (i == col_index) {
        digitalWrite(col_pins[i], LOW);
      } else {
        digitalWrite(col_pins[i], HIGH);
      }
    }
  } else {
    col_exp_data = ~(0x01 << (col_index - NUM_COL_BASE));
    mcp.writeGPIO(MCP_PORT_A, col_exp_data);
    if (0) {
      Serial.print("col_exp_data:");
      Serial.println(col_exp_data);
    }
  }
  // --- row ---
  uint8_t row_data = 0;

  if (col_index < NUM_COL_BASE) {
    for (int8_t i = 0; i < NUM_ROW_BASE; i++) {
      row_data = (row_data << 1) | digitalRead(row_pins[i]);
    }
  } else {
    row_data = mcp.readGPIO(MCP_PORT_B);
  }
  if (0) {
    Serial.print("row_data:");
    Serial.println(row_data);
  }

  if (row_data != row_tmp[col_index]) {
    // 変化があった場合
    row_tmp[col_index] = row_data;
    if (row_tmp[col_index] != row_st[col_index]) {
      // 変化開始の場合
      row_timer[col_index] = INIT_ROW_TIMER;
    } else {
      // 元に戻った場合
      // nop
    }
  } else {
    // 前回と変化がない場合
    if (row_tmp[col_index] != row_st[col_index]) {
      // 変化中(確定状態とは異なる場合)の場合
      if (0 == row_timer[col_index]) {
        // 確定した場合
        uint8_t row_xor = row_tmp[col_index] ^ row_st[col_index];
        // 全ビットの確認
        for (uint8_t i = 0; i < NUM_ROW_BASE; i++) {
          // ビット単位で変化があった場合
          if (0 != (row_xor & 0x01)) {
            uint8_t key_code = key_table[(col_index * NUM_ROW_BASE) + i];
            if (K_ != key_code) {
              if (0 != ((row_tmp[col_index] >> i) & 0x01)) {
                // off
                if (0) {
                  Serial.print("OFF:");
                  Serial.println((col_index * NUM_ROW_BASE) + i);
                }
                Keyboard.releaseEx(key_code);
              } else {
                // on
                if (0) {
                  Serial.print("ON:");
                  Serial.println((col_index * NUM_ROW_BASE) + i);
                }
                Keyboard.pressEx(key_code);
              }
            }
          }
          row_xor = row_xor >> 1;
        }
        row_st[col_index] = row_tmp[col_index];
      }
    } else {
      // 変化していない(確定状態と同じ)場合
      // nop
    }
  }
}

IOエキスパンダライブラリの拡張

I2Cの通信はそれほど早くないので、処理の効率化のためにIOへのアクセスを標準の1bit単位ではなく8bit単位で行えるように拡張しました。こちらの記事を参考に、Adafruit_MCP23017に対して

  • 8ビット単位でのモード設定
  • 8ビット単位でのプルアップ設定
  • 8ビット単位でのポート書込み

のメソッドを追加しました。

Arduino/libraries/Adafruit_MCP23017_Arduino_Library/Adafruit_MCP23017.hへのpublicメソッドの追加

  void pinModeByte(uint8_t p, uint8_t d);
  void pullUpByte(uint8_t p, uint8_t d);
  void writeGPIO(uint8_t p, uint8_t b);

Arduino/libraries/Adafruit_MCP23017_Arduino_Library/Adafruit_MCP23017.cppへのメソッドの追加

void Adafruit_MCP23017::pinModeByte(uint8_t p, uint8_t d){
    writeRegister(p, d);
}

void Adafruit_MCP23017::pullUpByte(uint8_t p, uint8_t d){
    if(p == 0){
        writeRegister(MCP23017_GPPUA, d);
    } else {
        writeRegister(MCP23017_GPPUB, d);
    }
}

void Adafruit_MCP23017::writeGPIO(uint8_t p, uint8_t a){
    Wire.beginTransmission(MCP23017_ADDRESS | i2caddr);
    if(p == 0){
        wiresend(MCP23017_GPIOA);
    } else {
        wiresend(MCP23017_GPIOB);
    }
    wiresend(a);
    Wire.endTransmission();
}

キーボードライブラリの拡張

標準のライブラリでは使えるキーコードの範囲が限定されているようでした。日本語配列特有のキー(変換キー等)に対応するために、標準ライブラリを以下のように拡張しました。

USBキーコードはこちらの資料の「10 Keyboard/Keypad Page (0x07)」を参照しました。

  • USB Usage のキーコードを定義
  • 最大値を変更
  • pressおよびreleaseの対応版メソッドの追加

Arduino\Libraries\Keyboard\src\Keyboard.hへの定数の追加 (macの場合 Arduino.app/Contents/Java/libraries/Keyboard/src/Keyboard.h)

/***************************************/
/* define USB HID Usage Table Keyboard */
/***************************************/
#define   K_        0 // Reserved (no event indicated)

#define   K_A       4
#define   K_B       5
#define   K_C       6
#define   K_D       7
#define   K_E       8
#define   K_F       9
#define   K_G       10
#define   K_H       11
#define   K_I       12
#define   K_J       13
#define   K_K       14
#define   K_L       15
#define   K_M       16
#define   K_N       17
#define   K_O       18
#define   K_P       19
#define   K_Q       20
#define   K_R       21
#define   K_S       22
#define   K_T       23
#define   K_U       24
#define   K_V       25
#define   K_W       26
#define   K_X       27
#define   K_Y       28
#define   K_Z       29
#define   K_1       30  // 1 and !
#define   K_2       31  // 2 and @
#define   K_3       32  // 3 and #
#define   K_4       33  // 4 and $
#define   K_5       34  // 5 and %
#define   K_6       35  // 6 and ^
#define   K_7       36  // 7 and &
#define   K_8       37  // 8 and *
#define   K_9       38  // 9 and (
#define   K_0       39  // 0 and )
#define   K_ENT     40  // Return (ENTER)
#define   K_ESC     41  // ESCAPE
#define   K_BS      42  // DELETE (Backspace)
#define   K_TAB     43  // Tab
#define   K_SP      44  // Spacebar
#define   K_HFN     45  // - and _
#define   K_EQL     46  // = and +
#define   K_OPN     47  // [ and {
#define   K_CLS     48  // ] and }
#define   K_BSH     49  // \ and |
#define   K_NUS     50  // Non-US # and ~ (?)
#define   K_SCN     51  // ; and :
#define   K_SCT     52  // ' and "
#define   K_BQT     53  // Grave Accent and Tilder
#define   K_KNM     54  // , and <
#define   K_DOT     55  // . and >
#define   K_SLH     56  // / and ?
#define   K_CPS     57  // Caps Lock
#define   K_F1      58
#define   K_F2      59
#define   K_F3      60
#define   K_F4      61
#define   K_F5      62
#define   K_F6      63
#define   K_F7      64
#define   K_F8      65
#define   K_F9      66
#define   K_F10     67
#define   K_F11     68
#define   K_F12     69
#define   K_PSC     70  // PrintScreen
#define   K_SLK     71  // Scroll Lock
#define   K_PAS     72  // Pauses
#define   K_INS     73  // Insert
#define   K_HOM     74  // Home
#define   K_PUP     75  // PageUp
#define   K_DEL     76  // Delete Forward
#define   K_END     77  // END
#define   K_PDWN    78  // PageDown
#define   K_RIT     79  // RightArrow
#define   K_LFT     80  // LeftArrow
#define   K_DN      81  // DownArrow
#define   K_UP      82  // UpArrow
#define   K_NLK     83  // Num Lock and Clear

#define   K_INT1    135 // *15,28 \ and _
#define   K_INT2    136 // *16 hiragana
#define   K_INT3    137 // *17  \ and |
#define   K_INT4    138 // *18 henkan
#define   K_INT5    139 // *19 muhenkan
#define   K_INT6    140 // *20
#define   K_INT7    141 // *21
#define   K_INT8    142 // *22
#define   K_INT9    143 // *22
#define   K_LNG1    144
#define   K_LNG2    145
#define   K_LNG3    146
#define   K_LNG4    147
#define   K_LNG5    148
#define   K_LNG6    149
#define   K_LNG7    150
#define   K_LNG8    151
#define   K_LNG9    152

#define   K_LCTL    224
#define   K_LSFT    225
#define   K_LALT    226
#define   K_LGUI    227
#define   K_RCTL    228
#define   K_RSFT    229
#define   K_RALT    230
#define   K_RGUI    231

Arduino\Libraries\Keyboard\src\Keyboard.hへのpublicメソッドの追加 (macの場合 Arduino.app/Contents/Java/libraries/Keyboard/src/Keyboard.h)

  size_t pressEx(uint8_t k);
  size_t releaseEx(uint8_t k);

Arduino\Libraries\Keyboard\src\Keyboard.cppへの書換え

//    0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
    0x29, (K_LCTL - 1),            //   USAGE_MAXIMUM (Keyboard Application)

Arduino\Libraries\Keyboard\src\Keyboard.cppへのメソッドの追加 (macの場合 Arduino.app/Contents/Java/libraries/Keyboard/src/Keyboard.cpp)

size_t Keyboard_::pressEx(uint8_t k) 
{
  uint8_t i;
  if (k >= K_LCTL){  // it's a modifier key
    _keyReport.modifiers |= (1<<(k-K_LCTL));
    k = 0;
  } else {
    if (!k) {
      setWriteError();
      return 0;
    }
  }
    
  // Add k to the key report only if it's not already present
  // and if there is an empty slot.
  if (_keyReport.keys[0] != k && _keyReport.keys[1] != k && 
    _keyReport.keys[2] != k && _keyReport.keys[3] != k &&
    _keyReport.keys[4] != k && _keyReport.keys[5] != k) {
        
    for (i=0; i<6; i++) {
      if (_keyReport.keys[i] == 0x00) {
        _keyReport.keys[i] = k;
        break;
      }
    }
    if (i == 6) {
      setWriteError();
      return 0;
    }   
  }
  sendReport(&_keyReport);
  return 1;
}

size_t Keyboard_::releaseEx(uint8_t k) 
{
  uint8_t i;
  if (k >= K_LCTL){  // it's a modifier key
    _keyReport.modifiers &= ~(1<<(k-K_LCTL));
    k = 0;
  } else {
    if (!k) {
      setWriteError();
      return 0;
    }
  }
  
  // Test the key report to see if k is present.  Clear it if it exists.
  // Check all positions in case the key is present more than once (which it shouldn't be)
  for (i=0; i<6; i++) {
    if (0 != k && _keyReport.keys[i] == k) {
      _keyReport.keys[i] = 0x00;
    }
  }

  sendReport(&_keyReport);
  return 1;
}

2017/05/01 mac用のパスを追記

ArduinoでUSB接続の分割キーボードを作ってみた話 - ハードウェア編

コントローラ部とキーマトリクス部をコネクタで分離し、後々別なインターフェースや別な物理キー配置のものに交換できるようにしてあります。

目次

コントローラ

PIC版として物理的なキー配列部とコントロール部を分離してそれぞれ汎用的に使えるように作成していたので、PIC版のコントロール部をArduinoを使ったものと差替えました。Arduino版ではProMicroクローンでPIC版で使ったXXXXに比べてポートが多く、8x8マトリクスとI2Cのポートがちょうどまかなえたので、IOエキスパンダは使わなくてすみました。あとPIC版と比べて、I2Cのためのプルアップ抵抗を使わなくて済むようでした。
f:id:hrhg:20170201055436p:plain f:id:hrhg:20170211154007j:plain

IOエキスパンダ

PIC版そのままですが、ErgoDox使用しているMCP23017はあまり国内では見当たらず、同じようなI2Cで16bitI/Oが拡張できるMCP23018を使いました。 f:id:hrhg:20170201055723p:plain f:id:hrhg:20170211154019j:plain

キーマトリクス

これもPIC版そのままですが、8x8のキーマトリクスを前提として、8x6と8x7を作りました。キースイッチは指に負担がかからない軽い感触のものが好みなので、1台目はCherry MX 赤、2台目はGateron 白を使いました。手はんだですが、コツがつかめればそれほど大変ではないと思います。 f:id:hrhg:20170201055728p:plain f:id:hrhg:20170211154233j:plain

シンプルに各部品をつないだだけでできました。

ArduinoでUSB接続の分割キーボードを作ってみた話

f:id:hrhg:20170211153801j:plain 先に分割型キーボードをPICで作っていましたが、

等の理由でつくってみました。ファームウェアのtmk_keyboardはまだ試してなく、Arduino入門を兼ねて自前で作ってみました。記事が長くなりそうなので

くらいに分けて書く予定です。うまくまとめきれていませんが、順次公開してみます。

自作キーボードの話

PIC版とArduino版のキーボードを作った話、そのうちもうちょっと詳細に書きたいと考えてます。

エルゴノミクスっぽいキーボードを試作してみました(概要)

ErgoDoxだとファンクションキーが無かったりして満足できそうもないので、USB接続のものを試作してみました。

7列x6行と8列x6行の2種類作りました。 f:id:hrhg:20170211155337j:plain

基板無しの手配線で、キーマトリクス部と制御部を分離しました。 f:id:hrhg:20170211155349j:plain

回路は秋月のPIC18F14K50を使用したボードとMCP23017を使いました。 f:id:hrhg:20170211155355j:plain f:id:hrhg:20170211155414j:plain

ソフトウェアはMicrochipのライブラリの中のHIDサンプルをベースにしました。