これは モダン言語によるベアメタル組込み開発 Advent Calendar 2022 14日目の記事です。
この記事では、 TinyGo で作った自作キーボードを紹介していきます。
TinyGo とは
マイコンや WASM などの Small Places 向けの Go Compiler です。ここでは Go の文法でマイコン開発ができるもの、という程度の認識でよいです。
TinyGo + Wio Terminal という題材で TinyGo の使い方を解説した書籍「基礎から学ぶ TinyGoの組込み開発」も好評発売中です。
書籍の中でも、ごくごく簡単ではありますが HID キーボードのサンプルを紹介しています。
TinyGo で USB HID Keyboard を作る
基本的には以下のようなコードを実装して、パソコンにつなぐことで HID Keyboard として認識します。とても簡単です。
// 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()
はキーリリースイベントを発生させます。
// 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 側でいい感じに扱ってくれるので以下のコードで問題ありません。
// 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 というもの。動作原理などは、先ほどのリンクの「duplexmatrixの回路設計、検証用ソフト」の部分を読んでいただくと良いです。以下の場合は 3 pin で合計 4 つのスイッチを受け取っています。Duplex-Matrix を使うと例えば 6 ピンで 3 x 3 x 2 = 18 ピンまで処理することができます。すごい。
Duplex-Matrix の処理コードは以下の通り。特に難しいことはないですが、 PinConfig を使って Output と (Input ではなく) InputPulldown を切り替えているのがポイント。
// 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
}
出来上がったもの
上記プロトタイプのコードをベースに、色々追加して以下のリポジトリを作成/公開しました。
左右独立させているので 12 キーまで同時押しできるようになりました。反応も上々だと思います。
最終的には、以下に対応しました。
- matrix scan / duplex-matrix scan
- Mod キーによるキーマップのレイヤー切り替え
- マウスクリックやマウスホイール
- 左右キーボード間の Mod キー状態を BLE で同期
失敗したと感じている部分
自作キーボード界隈の分割キーボードは左右のキーボード間を TRRS ケーブルなどで接続しているのですが、今回はそれを省略 (※1) しています。省略した結果として、 mod キー (レイヤー切り替えのためのキー) を押した時に、左右キーボードで同期がとれません。今は、同期をとるために BLE で通信していますが、やはり少し遅延が気になります。遠くない未来に分割ではないキーボードか、もしくは TRRS ケーブルなどを使った形でハードウェアを作り直す予定 (※2) です。
※1 TRRS ケーブルの存在理由に気づいてなかったので省略しただけです・・・
※2 ソフトウェアは最低限出来ている気持ち
まとめ
TinyGo はいいぞ。自作キーボードもいいぞ。
自作の左右分割キーボードのソースコードは以下にあります。
追記
TinyGo の使い方などは、以下の記事を参考にしてください。今回のキーボードは xiao-ble で作成しているので、 tinygo flash --target xiao-ble
で書き込むことができます。