Arduino Uno R3 で作るESC専用USBキーボード

[mixiグループアドベントカレンダー](http://qiita.com/advent-calendar/2016/mixi) 9日目を担当させていただきます。 
普段はSNSミクシィのAndroidアプリを開発しています。Android全く触れない内容ですが、どうぞよろしくお願いします。

# 概要

新しいMacBookProが発表され、ファンクションキーの代わりにTouchBarになりました。
Vimを使っている私としてはESCは残してほしかったので完全に＼(^o^)／状態でした。
多分、設定でESCは残せるだろうし、`Ctrl-[` を使えばいいのですが、ESCをまず数回叩かないと気持ち悪いレベルになっている自分としては非常に寂しい。
とにかくESCの打鍵感が欲しいので、ESC専用キーボードは売っているのだろうかと軽くググったらなかった。なら作ってみようという話です。

正直なところ、ATmega32u4チップが搭載されたArduino LeonardoかArudiono Microを使うとUSBデバイスとして認識してくれるので、[ライブラリ](https://www.arduino.cc/en/Reference/MouseKeyboard)を使えば簡単にできると思います。
しかし、ここでは多くの人がもっているであろうであるArudino Uno R3(以下R3) をターゲットにして、DFUによるもう一つのチップを書き込むなどしてR3をキーボードデバイスにします。

## R3のチップ構成について

R3はざっくり2つのAtmelチップがついています。

|チップ|概要|仕様|
|---|---|---|
|`ATmega 16u2` | usb serialとして使われ328pに書き込むために使われ、USBポート付近にあるやつ。R2からAtmelチップになったのでDFUができるようになった。ちなみにR2は `ATmega 8u2` | [link](http://www.atmel.com/images/doc7799.pdf) |
|`ATmeaga 328p` | sketchなどで書き込まれるMCU。一般的にはこちらがメイン | [link](http://www.atmel.com/ja/jp/devices/ATMEGA328P.aspx) |

Arudinoを開発する場合、作成したSketchを[公式IDE](https://www.arduino.cc/en/Main/Software)などからR3に書き込みエルチカなどします。IDEを使って書き込んでいるチップは `ATmeaga 328p` です。今回はR3をUSBキーボードデバイスとして認識させるために `ATmega 16u2` も書き換えます。

## やること

- R3をUSBキーボードデバイスとして認識させてみる (DFUで `ATmega 16u2` に書き込み)
- R3をスケッチから書き込みできるようにUSBシリアルに戻す (DFUで `ATmega 16u2` に書き込み)
- ESCキーコードを送るプログラム作成 (sketch)
- sketchを `ATmeaga 328p` に書き込み
- 再度R3をUSBキーボードデバイスとして認識させる (DFUで `ATmega 16u2` に書き込み)


# やってみる

## R3をUSBキーボードとして認識させてみる

R3をUSBキーボードとして認識させてみます。
そのためには、DFUで `ATmega 16u2` に対してキーボードHexファイルを書き込みます。
[DFU(Device Firmware Update)](https://www.arduino.cc/en/Hacking/DFUProgramming8U2)とは、ざっくりとUSBプロトコルを使用してプログラム(hexファイル)をチップに書き込むことです。

### hexファイルの取得

Hexファイルは、Atmelチップ開発環境を構築すればビルドして作成できます。macなら[CrossPack](https://www.obdev.at/products/crosspack/index.html)、linxuなら[toolchain](http://www.atmel.com/tools/atmelavrtoolchainforlinux.aspx)でクロスコンパイルするための `avr-gcc` を導入する必要があります。
[LUFA](http://www.fourwalledcubicle.com/LUFA.php)ライブラリを使うと様々なUSBデバイスになるHexファイルを作成することができます。2つのHexファイルもLUFAライブラリを使っています。

今回必要なHexファイルは2つです。

#### `UNO-dfu_and_usbserial_combined.hex` 

最初から書き込まれているデフォルトのHexファイル。`ATmeaga 328p` にsketchプログラムを書き込めたりします。[GitHub](https://github.com/arduino/Arduino/tree/master/hardware/arduino/avr/firmwares/atmegaxxu2)から取得できます。[公式IDE](https://www.arduino.cc/en/Main/Software)をインストール済みなら `/Applications/Arduino.app/Contents/Java/hardware/arduino/avr/firmwares/atmegaxxu2/` にあります。

#### `Arduino-keyboard-0.3.hex`

USBkeyboardデバイスとして認識するためのHexファイル。[ここ](http://hunt.net.nz/users/darran/weblog/b3029/Arduino_UNO_Keyboard_HID_version_03.html)から入手できますが、現在リンク切れなので[今回のプロジェクトリポジトリ](https://github.com/ssatoru0420/esc_keyboard/tree/develop/hex)に追加ておきました。

### dfu-programmerのインストール

[`dfu-programmer`](https://dfu-programmer.github.io/) という便利なコマンドラインプログラマがあります。
これはISP対応のUSBブートローダを備えたAtmelチップに対してコマンドラインで書き込むことができます。
`brew` を使ってインストールします。(UbuntuとかのDebian系なら `apt` でインストール)

```bash
$ brew update
$ brew install dfu-programmer

# versionを確認してみる
$ dfu-programmer --version
    dfu-programmer 0.7.2
```

### DFUプログラムをする

dfuプログラムは以下の手順で行います。

- dfuモードにする
- `dfu-programmer` による書き込み(削除・書き込み・リセット)


#### dfuモードにする

以下のようなアナログ操作です。

- USBから抜く
- リセットスイッチの前のピン2つをジャンパーピンで接続
- USB接続する
- ジャンパーピンを抜く

dfuモードになったかどうかはシステムレポートのUSBの状態を見て判断できます。

- 左上のアップルマーク -> このMacについて -> システムレポート -> USB で `複合装置` になっていればOKです。

|normal|dfu mode|
|---|---|
|![dfu_off.png](https://qiita-image-store.s3.amazonaws.com/0/99856/d618922c-d691-bb2f-9c28-31fe665899d6.png)|![dfu_on.png](https://qiita-image-store.s3.amazonaws.com/0/99856/e458b3e4-8c43-f369-358a-41967c4e00e6.png)|


#### `dfu-progmrammer` による書き込み

削除・書き込み・リセットを一連の手順とします。

```bash
# 削除する
$ dfu-programmer atmega16u2 erase
    Checking memory from 0x0 to 0x2FFF...  Not blank at 0x1.
    Erasing flash...  Success

# 書き込む (ここではUSBKeyboard Hexファイル)
$ dfu-programmer atmega16u2 flash ./hex/Arduino-keyboard-0.3.hex
    Checking memory from 0x0 to 0xFFF...  Empty.
    0%                            100%  Programming 0x1000 bytes...
    [>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>]  Success
    0%                            100%  Reading 0x3000 bytes...
    [>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>]  Success
    Validating...  Success
    0x1000 bytes written into 0x3000 bytes memory (33.33%).

# reset
$ dfu-programmer atmega16u2 reset
```

USBを抜き差しするとキーボードとして認識されるはずです。システムレポートで確認してみます。

![スクリーンショット 2016-12-06 23.52.24.png](https://qiita-image-store.s3.amazonaws.com/0/99856/693a3154-26ea-0ec8-78b1-effad95c9d6c.png)

## R3をUSBシリアルとして認識させてみる (元に戻す)

USBキーボードとして認識できましたが、 GPIOからのインプットをESCキーとして送信するためのプログラムは本丸である `ATmeaga 328p` に書き込む必要があります。
このままでは通常のSketchを書き込むことができないので、先程の手順でHexファイルを書き込み、もとに戻してみます。

- [前述](#dfu%e3%83%a2%e3%83%bc%e3%83%89%e3%81%ab%e3%81%99%e3%82%8b)の方法で、DFUモードにしてください
- `dfu-programmer` でUSBシリアルHexファイルを書き込みます

```bash
# 削除
$ dfu-programmer atmega16u2 erase
    :
# 書き込み
$ dfu-programmer atmega16u2 flash /Applications/Arduino.app/Contents/Java/hardware/arduino/avr/firmwares/atmegaxxu2/UNO-dfu_and_usbserial_combined.hex
    :
# リセット
$ dfu-programmer atmega16u2 reset
    :
```

次はESCキーコードを送るSketchプログラムを作成します。

## ESCキーコードを送るプログラム作成 (sketch)

ESCキーコードを送るプログラムを作成します。このプログラムの書き込み先は `ATmeaga 328p` なのでArduinoの一般的な開発と同じです。

### USBプロトコルと送信するキーコードについて

- uno R3 はKeyboardライブラリが使えないのでSerialで書き込みます。
- Serialで送信するのは基本的には8バイトのデータで、プロトコルは以下の通りとなります。
- [USB仕様HID編](http://www.usb.org/developers/hidpage/HID1_11.pdf)の `8.3 Report Format for Array Items` に詳細が記載されています。

| Byte|Contents|
|---|---|
| 0 : Bit0 | Left CTRL    |
| 0 : Bit1 | Left SHIFT   |
| 0 : Bit2 | Left ALT     |
| 0 : Bit3 | Left GUI     |
| 0 : Bit4 | Right CTRL   |
| 0 : Bit5 | Right SHIFT  |
| 0 : Bit6 | Right ALT    |
| 0 : Bit7 | Right GUI    |
| 1        | 今回使わない |
| 2~7      | HID active key codes |

キーコードについては [仕様](http://www.usb.org/developers/hidpage/Hut1_12v2.pdf)の `10 Keyboard/Keypad Page (0x07)` にあるリストから探してください。
送信したいのは `ESC` キーでなのでリストから `0x29` となります。

### 回路作成

誤操作を防ぐためにプルダウン回路にします。

![switch.png](https://qiita-image-store.s3.amazonaws.com/0/99856/edd69bc6-625f-00a8-0f8c-a4f1b5c028de.png)

### ビルド環境作成

コマンドラインで開発したいので、[platformIO](http://platformio.org/)を使います。IDEでSketchをプログラムする場合は飛ばしてください。

- virtualenv上で、platformIOをインストールします

```bash
# workon virtualenv
$ mkvirtualenv --no-site-package arduino

# install platformio
$ pip install platformio
$ pip install --egg scons
```

- プロジェクト作成

```bash
$ mkdir esc_keyboard; cd ./esc_keyboard;
$ platformio init --board=uno
$ echo ".pioenvs" > .gitignore
$ git init; git add -A; git commit -a -m "init";
```

- R3のデバイスを調べてコンフィグに設定します (下のスクリプトだと `peco` が必要)

```
$ uno=`platformio serialports list |grep usb | peco`; echo "upload_port="$uno >> platformio.ini
$ git commit -a -m "add upload port"
```

- makefileも作成

```makefile:Makefile
all: build install monitor

build:
	platformio run

# ターゲットボードにアップロードします
install:
	platformio run --target=upload

# シリアルをモニタします
# デバイス指定したい場合はオプションを追加してください (-p /dev/xxx)
# モニタを止めたい場合 ctrl+[
monitor:
	platformio serialports monitor

clean:
	platformio run --target=clean

.PHONY: all build install clean monitor
```

### プログラム作成して書き込む

- プログラムは愚直なコードですが以下のとおりです。

```c:./src/main.ino
#define LED_PIN 13

#define PIN_ESC  2

#define KEY_RESET       0x00
#define KEY_ESC         0x29

uint8_t keyData[8] = { 0, 0, 0, 0, 0, 0, 0 };

void handler(int pin, uint8_t keycode) {
    if (digitalRead(pin) == 1) {
        digitalWrite(LED_PIN, HIGH);

        keyData[2] = keycode;
        Serial.write(keyData, 8);   // send keypress
        keyData[2] = KEY_RESET;
        Serial.write(keyData, 8);   // release key

        while( digitalRead(pin) == 1) {
            delay(100);
        }
        digitalWrite(LED_PIN, LOW);
    }
}

void setup() {
    pinMode(LED_PIN, OUTPUT);
    pinMode(PIN_ESC,  INPUT);
    Serial.begin(9600);
    delay(2000);
}

void loop() {
    handler(PIN_ESC,  KEY_ESC);
}

```

- buildして書き込む

```bash
# build (platformio run と同じ)
$ make build

# 書き込み (platformio run --target=upload と同じ)
$ make install

# シリアル出力を確認 (platformio serialports monitor と同じ)
$ make monitor
```

スイッチ押下でR3に組み込まれているLEDが点灯し、よくわからない文字列がシリアル出力されていればプログラムはOKです。
USBプロトコルに従ったシリアルデータを送るプログラムの書き込みができました。最後に再度USBキーボードとして認識させるために `ATmega 16u2` にHexファイルを書き込みます。

## R3をUSBキーボードとして認識させる

USBキーボードとして認識させるために、再度DFUモードにしてHexファイルを書き換えます。

- [前述](#dfu%e3%83%a2%e3%83%bc%e3%83%89%e3%81%ab%e3%81%99%e3%82%8b)の方法で、DFUモードにしてください
- `dfu-programmer` でUSBシリアルHexファイルを書き込みます

```bash
# 削除
$ dfu-programmer atmega16u2 erase
    :
# 書き込み
$ dfu-programmer atmega16u2 flash ./hex/Arduino-keyboard-0.3.hex
    :
# リセット
$ dfu-programmer atmega16u2 reset
    :
```

どうでしょうか、R3がキーボードとして認識して、かつスイッチ押下すると、、、ESCキー押したことになっていると思います。ESCキーボードの完成！無理やりDashButtonの箱にR3入れてみました。

![スクリーンショット 2016-12-07 1.00.25.png](https://qiita-image-store.s3.amazonaws.com/0/99856/59ca19b1-5048-50ee-7b22-8379d761f7f4.png)

# まとめ

DFUも含めた開発の流れとしては面倒ですが以下のような感じです。

0. DFUモードでusbシリアルHexファイルを `ATmega 16u2` に書き込む
1. sketchプログラム作成
2. IDEなどで `ATmeaga 328p` に書き込む
3. DFUモードでKeyboardデバイスHexファイルを `ATmega 16u2` に書き込む
4. 確認してsketchプログラムを修正したいのならば 最初へ戻る

今回は単純な構造なのでシンプルなプログラムで実装しましたが、フルスペックキーボードの場合、もっと効率的なプログラムが必要かと思います。
正直 `Arudiono Micro` の方が小さくて簡単にキーボード認識させることができるので実用的だと思いますが、DFUというもう少し踏み込んだ開発は楽しいですね。
USBキーボードだけでなく、Hexファイルを変えればマウスにも、MIDI([Moco For LUFA](http://www.dm9records.com/index.php/tips/arduino/moco/))にもなります。
USB仕様を把握してLUFAライブラリを使って自分で好きなデバイスにもできると思います。サンプルも充実しているので是非チェックしてみてください。

今回のソースは[Github](https://github.com/ssatoru0420/esc_keyboard)にあります。