TinyGo で自作キーボードを作ってみた

これは [モダン言語によるベアメタル組込み開発 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/baremetal_embedded_modern_lang) 14日目の記事です。

この記事では、 TinyGo で作った自作キーボードを紹介していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63649/7cad64c5-c0cd-874d-b8f3-751426b85831.png)


## TinyGo とは

マイコンや WASM などの Small Places 向けの Go Compiler です。ここでは Go の文法でマイコン開発ができるもの、という程度の認識でよいです。

https://tinygo.org/

TinyGo + Wio Terminal という題材で TinyGo の使い方を解説した書籍「基礎から学ぶ TinyGoの組込み開発」も好評発売中です。
書籍の中でも、ごくごく簡単ではありますが HID キーボードのサンプルを紹介しています。

https://book.mynavi.jp/manatee/c-r/books/detail/id=134168

https://www.amazon.co.jp/exec/obidos/ASIN/4863544006/honmori-22


## TinyGo で USB HID Keyboard を作る

基本的には以下のようなコードを実装して、パソコンにつなぐことで HID Keyboard として認識します。とても簡単です。

```go
// https://github.com/tinygo-org/tinygo/blob/v0.26.0/src/examples/hid-keyboard/main.go

package main

import (
	"machine"
	"machine/usb/hid/keyboard"
	"time"
)

func main() {
	button := machine.BUTTON
	button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})

	kb := keyboard.New()

	for {
		if !button.Get() {
			kb.Write([]byte("tinygo"))
			time.Sleep(200 * time.Millisecond)
		}
	}
}
```

ただし、上記は button を押したら `tinygo` というキー列を送るようになっているのでキーボードには適していません。キーボードを作るためには以下の `Down()` と `Up()` を使います。`Down()` はキー押下イベントを、 `Up()` はキーリリースイベントを発生させます。


```go
// https://github.com/tinygo-org/tinygo/blob/v0.26.0/src/machine/usb/hid/keyboard/keyboard.go

// Down transmits a key-down event for the given Keycode.
//
// The host will interpret the key as being held down continuously until a
// corresponding key-up event is transmitted, e.g., via method Up().
//
// See godoc comment on method Press() for details on what input is accepted and
// how it is interpreted.
func (kb *keyboard) Down(c Keycode) error {
    // 省略
}

// Up transmits a key-up event for the given Keycode.
//
// See godoc comment on method Press() for details on what input is accepted and
// how it is interpreted.
func (kb *keyboard) Up(c Keycode) error {
    // 省略
}
```

あとはチャタリング除去を入れたらいったんは出来上がり。最初に作ったコードは以下のようなものでした。一定周期ループを回してキー状態を監視、キーが押されたり離したりしたら USB HID でイベントを送信します。なお、以下のコードは押しっぱなしの時は何度も `kb.Down()` をコールしてしまいますが、 machine/usb/hid/keyboard 側でいい感じに扱ってくれるので以下のコードで問題ありません。

```go
// https://gist.github.com/sago35/32a19a15d3554d34d25be3b836fbc2f4

func run() error {
	wait := 1 * time.Millisecond
	d := New([]machine.Pin{c1}, []machine.Pin{r1, r2})
	kb := keyboard.Port()

	for {
		// キーの状態を取得
		d.Get()
		a1, a2, a3, a4 := d.Current[0][0], d.Current[0][1], d.Current[1][0], d.Current[1][1]

		// キーの状態に対応したキーを送る
		if a1 {
			kb.Down(keyboard.KeyA)
		} else {
			kb.Up(keyboard.KeyA)
		}
		// (省略)

		fmt.Printf("%5t %5t %5t %5t\n", a4, a3, a2, a1)

		// 簡易的なチャタリング防止として約 32ms 待つ
		time.Sleep(32*time.Millisecond - wait*3)
	}

	return nil
}
```

使用した回路は以下のような [Duplex-Matrix](https://ratelog.net/intro-trialboard/) というもの。動作原理などは、先ほどのリンクの「duplexmatrixの回路設計、検証用ソフト」の部分を読んでいただくと良いです。以下の場合は 3 pin で合計 4 つのスイッチを受け取っています。Duplex-Matrix を使うと例えば 6 ピンで 3 x 3 x 2 = 18 ピンまで処理することができます。すごい。

https://twitter.com/sago35tk/status/1592146671068393474?s=20&t=e0OL3koRMDGjxHf2yzqljA

Duplex-Matrix の処理コードは以下の通り。特に難しいことはないですが、 PinConfig を使って Output と (Input ではなく) InputPulldown を切り替えているのがポイント。

```go
// https://gist.github.com/sago35/32a19a15d3554d34d25be3b836fbc2f4

func (d *Device) Get() [][]bool {
	wait := 1 * time.Millisecond

	for c := range d.Col {
		d.Col[c].Configure(machine.PinConfig{Mode: machine.PinOutput})
		d.Col[c].High()
		time.Sleep(wait)
		for r := range d.Row {
			d.Current[r][c] = d.Row[r].Get()
		}
		d.Col[c].Low()
		d.Col[c].Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
	}

	for r := range d.Row {
		d.Row[r].Configure(machine.PinConfig{Mode: machine.PinOutput})
		d.Row[r].High()
		time.Sleep(wait)
		for c := range d.Col {
			d.Current[r][c+len(d.Col)] = d.Col[c].Get()
		}
		d.Row[r].Low()
		d.Row[r].Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
	}

	return d.Current
}
```

## 出来上がったもの

上記プロトタイプのコードをベースに、色々追加して以下のリポジトリを作成／公開しました。

https://github.com/sago35/tinygo-keyboard

左右独立させているので 12 キーまで同時押しできるようになりました。反応も上々だと思います。

https://twitter.com/sago35tk/status/1597561737955667968

最終的には、以下に対応しました。

* matrix scan / duplex-matrix scan
* Mod キーによるキーマップのレイヤー切り替え
* マウスクリックやマウスホイール
* 左右キーボード間の Mod キー状態を BLE で同期

## 失敗したと感じている部分

自作キーボード界隈の分割キーボードは左右のキーボード間を TRRS ケーブルなどで接続しているのですが、今回はそれを省略 (※1) しています。省略した結果として、 mod キー (レイヤー切り替えのためのキー) を押した時に、左右キーボードで同期がとれません。今は、同期をとるために BLE で通信していますが、やはり少し遅延が気になります。遠くない未来に分割ではないキーボードか、もしくは TRRS ケーブルなどを使った形でハードウェアを作り直す予定 (※2) です。

※1 TRRS ケーブルの存在理由に気づいてなかったので省略しただけです・・・
※2 ソフトウェアは最低限出来ている気持ち


## まとめ

TinyGo はいいぞ。自作キーボードもいいぞ。

自作の左右分割キーボードのソースコードは以下にあります。

https://github.com/sago35/tinygo-keyboard


## 追記

TinyGo の使い方などは、以下の記事を参考にしてください。今回のキーボードは xiao-ble で作成しているので、 `tinygo flash --target xiao-ble` で書き込むことができます。

https://qiita.com/sago35/items/92b22e8cbbf99d0cd3ef