キーボードの自作からJIS, ANSI両用のファームウェアを作成するまで

# 2021/09/23追記
こちらの記事で紹介されているやり方より洗練された方法があります。
こちらのREADMEをご覧ください。

* [https://github.com/m47ch4n/qmk-translate-ansi-to-jis](https://github.com/m47ch4n/qmk-translate-ansi-to-jis)

# はじめにキーボードを作ろう！
　この記事を読んでいる方は大体自作キーボードに興味があると思います。私は分割キーボードに憧れていたのと、先輩から勧められたのもあり、先日[遊舎工房](https://yushakobo.jp/)様で[ErgoDash](https://yushakobo.jp/shop/ergodash)というキーボードを購入しました。

　遊舎工房様が扱うキーボードはほとんどが自作用であり、ハンダ付けなどの作業が必要です。自分のハンダごてを使うか、秋葉原の実店舗にある作業スペースを利用しましょう。私としては、実店舗に行くことをおすすめします。なぜなら作業スペースには、ハンダごてはもちろんアクリルのフィルムを簡単に剥がすことができる便利な工具があったり、スタッフさんがトラブルシュートしてくれたりと、至り尽くせリだからです。協力していただいたスタッフさんには本当に感謝します。ありがとうございました！:raised_hands:

　実際に出来上がったものはこちら。ホームポジションが極めてわかりにくい。後で質感の違うキーキャップに替えてみます。

![IMG_2105.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132844/f3de8c9d-de20-5e5f-c3fa-541ad2b4893a.jpeg)

# ファームウェアの作成
　自作キーボードの作成はまだ中間地点です。これからこのキーボードにファームウェア`qmk_firmware`を書き込みます。ファームウェアの作成には以下のサイトを参考にします。

* [QMK Firmware - An open source firmware for AVR and ARM based keyboards](https://qmk.fm/)
* [qmk/qmk_firmware: Open-source keyboard firmware for Atmel AVR and Arm USB families](https://github.com/qmk/qmk_firmware)

## 開発環境の構築
[Install Build Tools - QMK Firmware](https://docs.qmk.fm/#/getting_started_build_tools)に各OSの構築手順が紹介されています。私はmacOSを使っているので今回はmacOSでの手順を紹介します。

 macOSでは以下のコマンドを実行することで環境を構築できます。事前にhomebrewのインストールが必要です。

```bash
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`をクローンしたら、テストとして自分のキーボードのデフォルトファームウェアを書き込んでみましょう。ビルドと転送は以下のコマンドで行えます。

```bash
make <keyboard>:<keymap>:<target>
```

例として、ErgoDash(rev1)のdefaultキーマップを書き込むコマンドを以下に示します。ErgoDashのマイコンはpro microで、この場合のtargetは`avrdude`となります。

```bash
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に接続する時、キーボード設定アシスタントが起動します。このアシスタントの最後でキーボードのレイアウトを選択することができます。
<img width="728" alt="スクリーンショット 2019-08-31 12.26.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132844/039cfd07-ffa0-49a8-ebb6-9ab2cd3b5839.png">
　例えば自作したキーボードは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レイアウトの感覚）で打てるようにします。

* [qmk_firmware/keyboards/ergodash/rev1/keymaps/m47ch4n at add-m47ch4ns-keymap · m47ch4n/qmk_firmware](https://github.com/m47ch4n/qmk_firmware/tree/add-m47ch4ns-keymap/keyboards/ergodash/rev1/keymaps/m47ch4n)

```c

#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`をご覧ください。今回はマクロの解説をします。

## マクロ
```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`変数の値を見て元のシフトの状態に戻すコードを転送します。

```c
#define KEY(CODE) \
  (record->event.pressed ? SEND_STRING_RESTORE(SS_DOWN(X_ ## CODE)) : SEND_STRING_RESTORE(SS_UP(X_ ## CODE)))
```
　純粋にキーコード`CODE`を転送するものです。シフトの状態を変えないので、シフトを押した状態では押している状態のキーコード、シフトを押していない状態では押していない状態のキーコードが転送されます。

```c
#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`をシフトが押された状態で転送します。

```c
#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`をシフトが押されていない状態で転送します。

```c
#define ShiftUnshift(CODE_DS, CODE) (SHIFT_PRESSED ? CODE_DS : CODE)
```
　シフトが押されていたら1つ目のコードを、シフトが押されていなかったら2つ目のコードを出力します。

```c
#define caseUS(CODE, US, JP) case CODE: (LAYOUT_STATUS == JP_LAYOUT ? JP : US); return false;
```
　第1引数の`CODE`が押された時、USモードなら第2引数のコード、JPモードなら第3引数ののコードを出力します。

　以上のマクロを使って、マッピングしていきます。かなりの力技ですね...

## 結果
![ezgif.com-video-to-gif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132844/ccc15f70-51a2-f4fb-add8-1095e0ae57b8.gif)

　ANSIレイアウトで作ったキーボードを**あえてJIS環境で**使っています。テストとしてLキー右側の2つのキーを試します。1行目から順に`;`,`:`,`'`,`"`を打ってみます。
USモードでは動画のようにJISレイアウトのキーが出力されてしまいますが、JPモードだと、ANSIレイアウトのキーが出力できています。このことからJIS環境においてANSIレイアウトを再現できるモード`JPモード`が正常に動作していることが確認できました。また、ANSI環境では`USモード`で入力することで本来通り入力できることも確認しました。

# まとめ
　今回、JIS環境でもANSI環境でもANSIレイアウトの感覚で入力できるファームウェアを作成しました。上記の通りかなり力技なので、もっとスマートな方法をご存じの方は、コメントしていただけると幸いです。
　キーボード自作をしてみた感想ですが、ハンダ付けなど苦手な作業があって大変でしたが、ファームウェアを自分で書くことができる自由を手に入れたのはとても大きな喜びです。今この記事も自作したキーボードで書いていますが、キー配置に未だ満足できず無限に悩んでいる最中です。
　初めてのQiitaへの投稿でめちゃくちゃな文章だったと思いますが、誰かの手助けになれば嬉しいです。ご清覧ありがとうございました。

# 参考
* [ErgoDash/ergodash_jp.md at master · omkbd/ErgoDash](https://github.com/omkbd/ErgoDash/blob/master/Doc/ergodash_jp.md)
* [QMK Firmware - An open source firmware for AVR and ARM based keyboards](https://qmk.fm/)
* [qmk/qmk_firmware: Open-source keyboard firmware for Atmel AVR and Arm USB families](https://github.com/qmk/qmk_firmware)
* [Ryo33 / helix ryo33 · GitLab](https://gitlab.com/ryo33/helix-ryo33)