2021/09/23追記
こちらの記事で紹介されているやり方より洗練された方法があります。
こちらのREADMEをご覧ください。
はじめにキーボードを作ろう!
この記事を読んでいる方は大体自作キーボードに興味があると思います。私は分割キーボードに憧れていたのと、先輩から勧められたのもあり、先日遊舎工房様でErgoDashというキーボードを購入しました。
遊舎工房様が扱うキーボードはほとんどが自作用であり、ハンダ付けなどの作業が必要です。自分のハンダごてを使うか、秋葉原の実店舗にある作業スペースを利用しましょう。私としては、実店舗に行くことをおすすめします。なぜなら作業スペースには、ハンダごてはもちろんアクリルのフィルムを簡単に剥がすことができる便利な工具があったり、スタッフさんがトラブルシュートしてくれたりと、至り尽くせリだからです。協力していただいたスタッフさんには本当に感謝します。ありがとうございました!
実際に出来上がったものはこちら。ホームポジションが極めてわかりにくい。後で質感の違うキーキャップに替えてみます。
ファームウェアの作成
自作キーボードの作成はまだ中間地点です。これからこのキーボードにファームウェアqmk_firmware
を書き込みます。ファームウェアの作成には以下のサイトを参考にします。
- QMK Firmware - An open source firmware for AVR and ARM based keyboards
- qmk/qmk_firmware: Open-source keyboard firmware for Atmel AVR and Arm USB families
開発環境の構築
Install Build Tools - QMK Firmwareに各OSの構築手順が紹介されています。私はmacOSを使っているので今回はmacOSでの手順を紹介します。
macOSでは以下のコマンドを実行することで環境を構築できます。事前にhomebrewのインストールが必要です。
brew tap osx-cross/avr
brew tap PX4/homebrew-px4
brew update
brew install avr-gcc@8
brew link --force avr-gcc@8
brew install dfu-programmer
brew install dfu-util
brew install gcc-arm-none-eabi
brew install avrdude
Git
qmk_firmware
はpull request歓迎らしいので、forkするのが良いと思います。
forkしてからローカルにクローンしましょう。--recurse-submodules
オプションをつけて、サブモジュールもクローンすると良いです。
git clone --recurse-submodules https://github.com/<your name>/qmk_firmware.git
cd qmk_firmware
ビルド
qmk_firmware
をクローンしたら、テストとして自分のキーボードのデフォルトファームウェアを書き込んでみましょう。ビルドと転送は以下のコマンドで行えます。
make <keyboard>:<keymap>:<target>
例として、ErgoDash(rev1)のdefaultキーマップを書き込むコマンドを以下に示します。ErgoDashのマイコンはpro microで、この場合のtargetはavrdude
となります。
make ergodash/rev1:default:avrdude
出力は以下のとおりです。Connecting to programmer: ...
と表示されたときに、ErgoDashのリセットスイッチを押すと書き込みが始まります。
QMK Firmware 0.6.457
Making ergodash/rev1 with keymap default and target avrdude
avr-gcc (GCC) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Size before:
text data bss dec hex filename
0 18134 0 18134 46d6 .build/ergodash_rev1_default.hex
Copying ergodash_rev1_default.hex to qmk_firmware folder [OK]
Checking file size of ergodash_rev1_default.hex [OK]
* The firmware size is fine - 18134/28672 (63%, 10538 bytes free)
Detecting USB port, reset your controller now........
Device /dev/tty.usbmodem14201 has appeared; assuming it is the controller.
Waiting for /dev/tty.usbmodem14201 to become writable.
Connecting to programmer: .
Found programmer: Id = "CATERIN"; type = S
Software Version = 1.0; No Hardware Version given.
Programmer supports auto addr increment.
Programmer supports buffered memory access with buffersize=128 bytes.
Programmer supports the following devices:
Device code: 0x44
avrdude: AVR device initialized and ready to accept instructions
Reading | ################################################## | 100% 0.00s
avrdude: Device signature = 0x1e9587 (probably m32u4)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file ".build/ergodash_rev1_default.hex"
avrdude: input file .build/ergodash_rev1_default.hex auto detected as Intel Hex
avrdude: writing flash (18134 bytes):<img width="728" alt="スクリーンショット 2019-08-31 12.26.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132844/c0748903-d84a-4f1f-ccbe-71bcb5dafc8f.png">
Writing | ################################################## | 100% 1.40s
avrdude: 18134 bytes of flash written
avrdude: verifying flash memory against .build/ergodash_rev1_default.hex:
avrdude: load data flash data from input file .build/ergodash_rev1_default.hex:
avrdude: input file .build/ergodash_rev1_default.hex auto detected as Intel Hex
avrdude: input file .build/ergodash_rev1_default.hex contains 18134 bytes
avrdude: reading on-chip flash data:
Reading | ################################################## | 100% 0.16s
avrdude: verifying ...
avrdude: 18134 bytes of flash verified
avrdude: safemode: Fuses OK (E:FB, H:D8, L:FF)
avrdude done. Thank you.
キーマップの作成
デフォルトのファームウェアの書き込みができたことを確認したら、マイキーマップ作成に取り掛かりましょう。
keyboards/<keyboard>/keymaps
にdefault
というフォルダがあると思うので、それをコピーしましょう。コピーのフォルダ名は、キーマップ名なので任意ですが、後でpull requestするのを見越して、GitHubのアカウント名と同じにすることが推奨されています。
JIS環境とANSI環境で両用できるファームウェアの作成
以上の手順で通常のファームウェア作成手順は終わりですが、今回はさらに、便利なファームウェアを作成してみましょう。
キーボードをmacOSに接続する時、キーボード設定アシスタントが起動します。このアシスタントの最後でキーボードのレイアウトを選択することができます。
例えば自作したキーボードはJISレイアウト用にファームウェアを書いてあるとして、設定をJISにしようとした時、この設定はPCに接続されているすべてのキーボードに適用されてしまいます。したがって、PCに接続されている他のキーボードがANSIレイアウトの場合、そのキーボードではANSIレイアウト通りのキーを打つことができません。
- 上記のような理由から、レイアウト設定を変更することは非常に面倒で、基本的に行われないと仮定します。この記事ではJISレイアウトで設定されたPCを
JIS環境
、ANSIレイアウトで設定されたPCをANSI環境
と呼ぶことにします。
1つのPCしか使わない場合はそこまで問題ではないですが、チーム開発などで複数のPCを共用していて、JIS環境とANSI環境が混在している場合、普通のキーボードは両方で使うことができません。
そこで私はファームウェアにJPモード
とUSモード
を追加することで両環境ともANSIレイアウトの感覚で使えるファームウェアを作成しました。JIS環境のPCではJPモード
、ANSI環境ではUSモード
に切り替えることにより、どちらの環境でも同じ感覚(私の場合はANSIレイアウトの感覚)で打てるようにします。
#define JP_LAYOUT true
#define US_LAYOUT false
bool LAYOUT_STATUS = US_LAYOUT;
bool SHIFT_PRESSED = false;
#define SEND_STRING_RESTORE(STR) \
(SHIFT_PRESSED ? SEND_STRING(STR SS_DOWN(X_LSHIFT)) : SEND_STRING(STR SS_UP(X_LSHIFT)))
#define KEY(CODE) \
(record->event.pressed ? SEND_STRING_RESTORE(SS_DOWN(X_ ## CODE)) : SEND_STRING_RESTORE(SS_UP(X_ ## CODE)))
#define KEY_SHIFT(CODE) \
(record->event.pressed ? SEND_STRING_RESTORE(SS_DOWN(X_LSHIFT) SS_DOWN(X_ ## CODE)) : SEND_STRING_RESTORE(SS_UP(X_ ## CODE)))
#define KEY_UPSHIFT(CODE) \
(record->event.pressed ? SEND_STRING_RESTORE(SS_UP(X_LSHIFT) SS_DOWN(X_ ## CODE)) : SEND_STRING_RESTORE(SS_UP(X_ ## CODE)))
#define ShiftUnshift(CODE_DS, CODE) (SHIFT_PRESSED ? CODE_DS : CODE)
#define caseUS(CODE, US, JP) case CODE: (LAYOUT_STATUS == JP_LAYOUT ? JP : US); return false;
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
caseUS(Z_0, KEY(0), ShiftUnshift(KEY_SHIFT(9), KEY(0)));
caseUS(Z_1, KEY(1), KEY(1));
caseUS(Z_2, KEY(2), ShiftUnshift(KEY_UPSHIFT(LBRACKET), KEY(2)));
caseUS(Z_3, KEY(3), KEY(3));
caseUS(Z_4, KEY(4), KEY(4));
caseUS(Z_5, KEY(5), KEY(5));
caseUS(Z_6, KEY(6), ShiftUnshift(KEY_UPSHIFT(EQUAL), KEY(6)));
caseUS(Z_7, KEY(7), ShiftUnshift(KEY_SHIFT(6), KEY(7)));
caseUS(Z_8, KEY(8), ShiftUnshift(KEY_SHIFT(QUOTE), KEY(8)));
caseUS(Z_9, KEY(9), ShiftUnshift(KEY_SHIFT(8), KEY(9)));
caseUS(DEL, KEY(DELETE), KEY_UPSHIFT(BSPACE));
caseUS(TILD, KEY_SHIFT(GRAVE), KEY_SHIFT(EQUAL));
caseUS(EXLM, KEY_SHIFT(1), KEY_SHIFT(1));
caseUS(AT, KEY_SHIFT(2), KEY(LBRACKET));
caseUS(HASH, KEY_SHIFT(3), KEY_SHIFT(3));
caseUS(DLR, KEY_SHIFT(4), KEY_SHIFT(4));
caseUS(PERC, KEY_SHIFT(5), KEY_SHIFT(5));
caseUS(CIRC, KEY_SHIFT(6), KEY(EQUAL));
caseUS(AMPR, KEY_SHIFT(7), KEY_SHIFT(6));
caseUS(ASTR, KEY_SHIFT(8), KEY_SHIFT(QUOTE));
caseUS(LPRN, KEY_SHIFT(9), KEY_SHIFT(8));
caseUS(RPRN, KEY_SHIFT(0), KEY_SHIFT(9));
caseUS(LBRC, KEY(LBRACKET), ShiftUnshift(KEY_SHIFT(RBRACKET), KEY(RBRACKET)));
caseUS(RBRC, KEY(RBRACKET), ShiftUnshift(KEY_SHIFT(NONUS_HASH), KEY(NONUS_HASH)));
caseUS(LCBR, KEY_SHIFT(LBRACKET), KEY_SHIFT(RBRACKET));
caseUS(RCBR, KEY_SHIFT(RBRACKET), KEY_SHIFT(NONUS_HASH));
caseUS(GRV, KEY(GRAVE), ShiftUnshift(KEY_SHIFT(EQUAL), KEY_SHIFT(LBRACKET)));
caseUS(BSLS, KEY(BSLASH), ShiftUnshift(KEY_SHIFT(INT3), KEY(INT3)));
caseUS(PIPE, KEY_SHIFT(BSLASH), KEY_SHIFT(INT3));
caseUS(MINS, KEY(MINUS), ShiftUnshift(KEY_SHIFT(INT1), KEY(MINUS)));
caseUS(UNDS, KEY_SHIFT(MINUS), KEY_SHIFT(INT1));
caseUS(EQL, KEY(EQUAL), ShiftUnshift(KEY_SHIFT(SCOLON), KEY_SHIFT(MINUS)));
caseUS(PLUS, KEY_SHIFT(EQUAL), KEY_SHIFT(SCOLON));
caseUS(SCLN, KEY(SCOLON), ShiftUnshift(KEY_UPSHIFT(QUOTE), KEY(SCOLON)));
caseUS(QUOT, KEY(QUOTE), ShiftUnshift(KEY_SHIFT(2), KEY_SHIFT(7)));
case JP:
if (record->event.pressed)
LAYOUT_STATUS = JP_LAYOUT;
return false;
break;
case US:
if (record->event.pressed)
LAYOUT_STATUS = US_LAYOUT;
return false;
break;
case SHIFT:
if (record->event.pressed) {
SEND_STRING(SS_DOWN(X_LSHIFT));
SHIFT_PRESSED = true;
} else {
SEND_STRING(SS_UP(X_LSHIFT));
SHIFT_PRESSED = false;
}
return false;
break;
...
}
return true;
}
やっていることは単純で、SEND_STRING
で転送するコードを、モードやシフトの状態から変化させているだけです。詳しくはkeymap.c
をご覧ください。今回はマクロの解説をします。
マクロ
#define SEND_STRING_RESTORE(STR) \
(SHIFT_PRESSED ? SEND_STRING(STR SS_DOWN(X_LSHIFT)) : SEND_STRING(STR SS_UP(X_LSHIFT)))
KEY_SHIFT
やKEY_UNSHIFT
では、シフトの状態を変えるコードを転送しています。これは転送後に元に戻すべきなのでSEND_STRING_RESTORE
ではSHIFT_PRESSED
変数の値を見て元のシフトの状態に戻すコードを転送します。
#define KEY(CODE) \
(record->event.pressed ? SEND_STRING_RESTORE(SS_DOWN(X_ ## CODE)) : SEND_STRING_RESTORE(SS_UP(X_ ## CODE)))
純粋にキーコードCODE
を転送するものです。シフトの状態を変えないので、シフトを押した状態では押している状態のキーコード、シフトを押していない状態では押していない状態のキーコードが転送されます。
#define KEY_SHIFT(CODE) \
(record->event.pressed ? SEND_STRING_RESTORE(SS_DOWN(X_LSHIFT) SS_DOWN(X_ ## CODE)) : SEND_STRING_RESTORE(SS_UP(X_ ## CODE)))
CODE
をシフトが押された状態で転送します。
#define KEY_UPSHIFT(CODE) \
(record->event.pressed ? SEND_STRING_RESTORE(SS_UP(X_LSHIFT) SS_DOWN(X_ ## CODE)) : SEND_STRING_RESTORE(SS_UP(X_ ## CODE)))
CODE
をシフトが押されていない状態で転送します。
#define ShiftUnshift(CODE_DS, CODE) (SHIFT_PRESSED ? CODE_DS : CODE)
シフトが押されていたら1つ目のコードを、シフトが押されていなかったら2つ目のコードを出力します。
#define caseUS(CODE, US, JP) case CODE: (LAYOUT_STATUS == JP_LAYOUT ? JP : US); return false;
第1引数のCODE
が押された時、USモードなら第2引数のコード、JPモードなら第3引数ののコードを出力します。
以上のマクロを使って、マッピングしていきます。かなりの力技ですね...
結果
ANSIレイアウトで作ったキーボードをあえてJIS環境で使っています。テストとしてLキー右側の2つのキーを試します。1行目から順に;
,:
,'
,"
を打ってみます。
USモードでは動画のようにJISレイアウトのキーが出力されてしまいますが、JPモードだと、ANSIレイアウトのキーが出力できています。このことからJIS環境においてANSIレイアウトを再現できるモードJPモード
が正常に動作していることが確認できました。また、ANSI環境ではUSモード
で入力することで本来通り入力できることも確認しました。
まとめ
今回、JIS環境でもANSI環境でもANSIレイアウトの感覚で入力できるファームウェアを作成しました。上記の通りかなり力技なので、もっとスマートな方法をご存じの方は、コメントしていただけると幸いです。
キーボード自作をしてみた感想ですが、ハンダ付けなど苦手な作業があって大変でしたが、ファームウェアを自分で書くことができる自由を手に入れたのはとても大きな喜びです。今この記事も自作したキーボードで書いていますが、キー配置に未だ満足できず無限に悩んでいる最中です。
初めてのQiitaへの投稿でめちゃくちゃな文章だったと思いますが、誰かの手助けになれば嬉しいです。ご清覧ありがとうございました。