はじめに
QMKとだいぶ勝手が違って困った。環境が整いすぎてて困ったのでここにまとめていく。
また、オリジナルキーボードのファームウェアの作り方がいまいちまとまってなかったので、自分用にまとめる。
今回はthumbRotary というキーボードを作ったので、これが出てくるところは随時、ご自身のキーボード名に置き換えてください。
thumbRotaryについて
XIAO nRF52840を使った分割無線機ボードです。
Keyballライクな外径にし、左右対象のキー配置で、親指でロータリエンコーダを操作できるものとなっています。
一体型とは異なる部分があるので、一体型を作る場合は適宜公式ドキュメントをご参照ください。
環境構築
1. Githubアカウントを用意する
CICDが自動で回るようになっているので、Githubアカウントが必要です。また、Web上でキーマップを編集する作業をするためにリポジトリを接続する必要があるのもGithubが必要な理由です。
2. テンプレートリポジトリをローカルにクローン
以下のコマンドから、zmkファームウェアのテンプレートをローカルにクローンしてください。
後ほど、Github Actionsを回すためにリモートに紐づけます。
git clone https://github.com/zmkfirmware/unified-zmk-config-template.git zmk-config-thumbRotary
ファームウェア作成
1. 必要ファイルを作成
先ほどクローンしたzmk-config-thumbRotaryディレクトリ内が左図です。
手順は主に以下です。
- boards/shieldsにキーボード名のドキュメントを追加。
- 追加したディレクトリ内に、左右共通のdefconfig, shield, dtsiファイルを追加
- 左右それぞれの設定ファイルである、conf, overlayファイルを追加
- configディレクトリ内に、info.json, conf, keymapファイルを追加
ちゃんと理解しているわけではないので、詳細な説明は省きます。
2. キーレイアウトを用意
Keyboard Layout Editorを使用してレイアウトを作成します。
Col,Rowでラベルを入れることを忘れないようにする。
基盤に実装した形(上図)でまずはレイアウトを作成したが、ファームウェアを作成するにあたって、オーソリニア配列で作成すると扱いやすかったので、以下に変更。
Row Dataは以下となった。
["0,0","0,1","0,2","0,3","0,4","0,5",{x:7},"0,6","0,7","0,8","0,9","0,10","0,11"],
["1,0","1,1","1,2","1,3","1,4","1,5",{x:7},"1,6","1,7","1,8","1,9","1,10","1,11"],
["2,0","2,1","2,2","2,3","2,4","2,5",{x:7},"2,6","2,7","2,8","2,9","2,10","2,11"],
["3,0","3,1","3,2","3,3","3,4","3,5","3,6",{x:5},"3,7","3,8","3,9","3,10","3,11","3,12","3,13"]
3. info.jsonを生成
QMK FirmwareのKLE to QMKを利用して、作成したRow Dataをinfo.jsonの形式に変換する。
出力は以下となった。
{
"keyboard_name": "",
"url": "",
"maintainer": "qmk",
"layouts": {
"LAYOUT": {
"layout": [{"label":"0,0", "x":0, "y":0}, {"label":"0,1", "x":1, "y":0}, {"label":"0,2", "x":2, "y":0}, {"label":"0,3", "x":3, "y":0}, {"label":"0,4", "x":4, "y":0}, {"label":"0,5", "x":5, "y":0}, {"label":"0,6", "x":13, "y":0}, {"label":"0,7", "x":14, "y":0}, {"label":"0,8", "x":15, "y":0}, {"label":"0,9", "x":16, "y":0}, {"label":"0,10", "x":17, "y":0}, {"label":"0,11", "x":18, "y":0}, {"label":"1,0", "x":0, "y":1}, {"label":"1,1", "x":1, "y":1}, {"label":"1,2", "x":2, "y":1}, {"label":"1,3", "x":3, "y":1}, {"label":"1,4", "x":4, "y":1}, {"label":"1,5", "x":5, "y":1}, {"label":"1,6", "x":13, "y":1}, {"label":"1,7", "x":14, "y":1}, {"label":"1,8", "x":15, "y":1}, {"label":"1,9", "x":16, "y":1}, {"label":"1,10", "x":17, "y":1}, {"label":"1,11", "x":18, "y":1}, {"label":"2,0", "x":0, "y":2}, {"label":"2,1", "x":1, "y":2}, {"label":"2,2", "x":2, "y":2}, {"label":"2,3", "x":3, "y":2}, {"label":"2,4", "x":4, "y":2}, {"label":"2,5", "x":5, "y":2}, {"label":"2,6", "x":13, "y":2}, {"label":"2,7", "x":14, "y":2}, {"label":"2,8", "x":15, "y":2}, {"label":"2,9", "x":16, "y":2}, {"label":"2,10", "x":17, "y":2}, {"label":"2,11", "x":18, "y":2}, {"label":"3,0", "x":0, "y":3}, {"label":"3,1", "x":1, "y":3}, {"label":"3,2", "x":2, "y":3}, {"label":"3,3", "x":3, "y":3}, {"label":"3,4", "x":4, "y":3}, {"label":"3,5", "x":5, "y":3}, {"label":"3,6", "x":6, "y":3}, {"label":"3,7", "x":12, "y":3}, {"label":"3,8", "x":13, "y":3}, {"label":"3,9", "x":14, "y":3}, {"label":"3,10", "x":15, "y":3}, {"label":"3,11", "x":16, "y":3}, {"label":"3,12", "x":17, "y":3}, {"label":"3,13", "x":18, "y":3}]
}
}
}
zmkで使用できるように "label":"0,0" を”row”:0, ”col”:0 に変換し、諸々情報を書き加えて以下となった。
{
"id": "thumbRotary",
"name": "thumbRotary",
"keyboard_name": "",
"url": "",
"maintainer": "qmk",
"layouts": {
"LAYOUT": {
"layout": [{"row":0, "col":0, "x":0, "y":0}, {"row":0, "col":1, "x":1, "y":0}, {"row":0, "col":2, "x":2, "y":0}, {"row":0, "col":3, "x":3, "y":0}, {"row":0, "col":4, "x":4, "y":0}, {"row":0, "col":5, "x":5, "y":0}, {"row":0, "col":6, "x":13, "y":0}, {"row":0, "col":7, "x":14, "y":0}, {"row":0, "col":8, "x":15, "y":0}, {"row":0, "col":9, "x":16, "y":0}, {"row":0, "col":10, "x":17, "y":0}, {"row":0, "col":11, "x":18, "y":0}, {"row":1, "col":0, "x":0, "y":1}, {"row":1, "col":1, "x":1, "y":1}, {"row":1, "col":2, "x":2, "y":1}, {"row":1, "col":3, "x":3, "y":1}, {"row":1, "col":4, "x":4, "y":1}, {"row":1, "col":5, "x":5, "y":1}, {"row":1, "col":6, "x":13, "y":1}, {"row":1, "col":7, "x":14, "y":1}, {"row":1, "col":8, "x":15, "y":1}, {"row":1, "col":9, "x":16, "y":1}, {"row":1, "col":10, "x":17, "y":1}, {"row":1, "col":11, "x":18, "y":1}, {"row":2, "col":0, "x":0, "y":2}, {"row":2, "col":1, "x":1, "y":2}, {"row":2, "col":2, "x":2, "y":2}, {"row":2, "col":3, "x":3, "y":2}, {"row":2, "col":4, "x":4, "y":2}, {"row":2, "col":5, "x":5, "y":2}, {"row":2, "col":6, "x":13, "y":2}, {"row":2, "col":7, "x":14, "y":2}, {"row":2, "col":8, "x":15, "y":2}, {"row":2, "col":9, "x":16, "y":2}, {"row":2, "col":10, "x":17, "y":2}, {"row":2, "col":11, "x":18, "y":2}, {"row":3, "col":0, "x":0, "y":3}, {"row":3, "col":1, "x":1, "y":3}, {"row":3, "col":2, "x":2, "y":3}, {"row":3, "col":3, "x":3, "y":3}, {"row":3, "col":4, "x":4, "y":3}, {"row":3, "col":5, "x":5, "y":3}, {"row":3, "col":6, "x":6, "y":3}, {"row":3, "col":7, "x":12, "y":3}, {"row":3, "col":8, "x":13, "y":3}, {"row":3, "col":9, "x":14, "y":3}, {"row":3, "col":10, "x":15, "y":3}, {"row":3, "col":11, "x":16, "y":3}, {"row":3, "col":12, "x":17, "y":3}, {"row":3, "col":13, "x":18, "y":3}]
}
},
"sensors": [
{
"ref": "left_encoder",
"name": "encoder_left",
"identifier": "encoder_left",
"compatible": "alps,ec11",
"enabled": false
},
{
"ref": "right_encoder",
"name": "encoder_right",
"identifier": "encoder_right",
"compatible": "alps,ec11",
"enabled": false
}
]
}
4. configディレクトリのconfとkeymapを生成
以下を参考に合わせて書いてみてください
CONFIG_ZMK_BLE=y
CONFIG_ZMK_USB=y
// 背面のNFCパッドをGPIOとして利用する
CONFIG_NFCT_PINS_AS_GPIOS=y
// ロータリエンコーダを利用する
CONFIG_INPUT=y
CONFIG_EC11=y
CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y
// マウス操作を使用したいので
CONFIG_ZMK_POINTING=y
CONFIG_ZMK_BATTERY_REPORTING=y
// 以下バッテリーや接続、レイヤーの状態でXIAO BLEのLEDを変化させるため
CONFIG_RGBLED_WIDGET=y
CONFIG_RGBLED_WIDGET_INTERVAL_MS=250
CONFIG_RGBLED_WIDGET_BATTERY_LEVEL_HIGH=50
CONFIG_RGBLED_WIDGET_BATTERY_LEVEL_CRITICAL=10
CONFIG_RGBLED_WIDGET_SHOW_LAYER_CHANGE=n
CONFIG_RGBLED_WIDGET_SHOW_LAYER_COLORS=y
#num_cyan
CONFIG_RGBLED_WIDGET_LAYER_1_COLOR=6
#sym_yellow
CONFIG_RGBLED_WIDGET_LAYER_2_COLOR=3
#jis_white
CONFIG_RGBLED_WIDGET_LAYER_3_COLOR=7
#jisnum_cyan
CONFIG_RGBLED_WIDGET_LAYER_4_COLOR=6
#jissys_yellow
CONFIG_RGBLED_WIDGET_LAYER_5_COLOR=3
#macarrow_magenta
CONFIG_RGBLED_WIDGET_LAYER_6_COLOR=5
#winarrow_magenta
CONFIG_RGBLED_WIDGET_LAYER_7_COLOR=5
#BT_blue
CONFIG_RGBLED_WIDGET_LAYER_8_COLOR=4
#include <behaviors.dtsi>
#include <dt-bindings/zmk/bt.h>
#include <dt-bindings/zmk/keys.h>
#include <dt-bindings/zmk/pointing.h>
#define ZMK_POINTING_DEFAULT_MOVE_VAL 1500
#define ZMK_POINTING_DEFAULT_SCRL_VAL 80
/ {
behaviors {
mouse_scroll_x: mouse_scroll_x {
compatible = "zmk,behavior-sensor-rotate";
#sensor-binding-cells = <0>;
bindings = <&msc SCRL_LEFT>, <&msc SCRL_RIGHT>;
tap-ms = <5>;
};
mouse_scroll_y: mouse_scroll_y {
compatible = "zmk,behavior-sensor-rotate";
#sensor-binding-cells = <0>;
bindings = <&msc SCRL_UP>, <&msc SCRL_DOWN>;
tap-ms = <5>;
};
mouse_move_y: mouse_move_y {
compatible = "zmk,behavior-sensor-rotate";
label = "MOUSE_MOVE_Y";
#sensor-binding-cells = <0>;
bindings = <&mmv MOVE_UP>, <&mmv MOVE_DOWN>;
tap-ms = <5>;
};
mouse_move_x: mouse_move_x {
compatible = "zmk,behavior-sensor-rotate";
label = "MOUSE_MOVE_X";
#sensor-binding-cells = <0>;
bindings = <&mmv MOVE_RIGHT>, <&mmv MOVE_LEFT>;
tap-ms = <5>;
};
};
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <
&kp ESC &kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P &kp BACKSPACE
&kp TAB &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMICOLON &kp ENTER
&kp LEFT_SHIFT &kp Z &kp X &kp C &kp V &kp B &kp N &kp M &kp COMMA &kp DOT &kp SLASH &kp RIGHT_SHIFT
&kp LANG2 &kp LEFT_CONTROL < 2 LEFT_COMMAND &mkp MCLK &kp SPACE &mkp LCLK < 1 LEFT_COMMAND < 1 RIGHT_COMMAND &mkp RCLK &kp SPACE &mkp MCLK < 2 RIGHT_COMMAND &kp RIGHT_ALT &kp LANG1
>;
sensor-bindings = <&mouse_move_x>, <&mouse_move_y>;
};
layer_1 {
bindings = <
&kp GRAVE &kp NUMBER_1 &kp NUMBER_2 &kp NUMBER_3 &kp NUMBER_4 &kp N5 &kp NUMBER_6 &kp N7 &kp N8 &kp N9 &kp N0 &kp BACKSPACE
&trans &trans &trans &kp UP_ARROW &trans &trans &trans &kp MINUS &kp EQUAL &kp LEFT_BRACKET &kp RIGHT_BRACKET &kp BACKSLASH
&kp LEFT_SHIFT &trans &kp LEFT_ARROW &kp DOWN_ARROW &kp RIGHT_ARROW &trans &trans &trans &trans &trans &kp SQT &kp RIGHT_SHIFT
&trans &trans &trans &trans &kp SPACE &trans &trans &trans &trans &kp SPACE &trans &trans &trans &trans
>;
sensor-bindings = <&mouse_scroll_x>, <&mouse_scroll_y>;
};
layer_2 {
bindings = <
&kp F1 &kp F2 &kp F3 &kp F4 &kp F5 &kp F6 &kp F7 &kp F8 &kp F9 &kp F10 &kp F11 &kp F12
&trans &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
&trans &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
&trans &trans &trans &trans &trans &sys_reset &bt BT_CLR_ALL &bt BT_CLR_ALL &sys_reset &trans &trans &trans &trans &trans
>;
};
};
};
5. boards/shields/thumbRotaryディレクトリのKconfigファイルを生成
4までで上流のファームウェアが書けたはずなので、ここから分割のそれぞれの設定や、マイコンとの接続部分の設定を書いていきます。
まずは左右を定義するために、shieldに定義を書きます。
# Copyright (c) 2020 Pete Johanson
# SPDX-License-Identifier: MIT
config SHIELD_THUMBROTARY_LEFT
def_bool $(shields_list_contains,thumbRotary_left)
config SHIELD_THUMBROTARY_RIGHT
def_bool $(shields_list_contains,thumbRotary_right)
defconfigには先ほど定義した左右のシールドに対する設定を書きます。
ここでは、左手のデバイスをPCと接続するBLEデバイスとしています。
if SHIELD_THUMBROTARY_LEFT
config ZMK_KEYBOARD_NAME
default "tr_l"
config ZMK_SPLIT_BLE_ROLE_CENTRAL
default y
endif
if SHIELD_THUMBROTARY_RIGHT
config ZMK_KEYBOARD_NAME
default "tr_r"
endif
if SHIELD_THUMBROTARY_LEFT || SHIELD_THUMBROTARY_RIGHT
config ZMK_SPLIT
default y
endif
6. dtsiファイルを生成
このファイルが左右の共通の設定ファイルとなります。
左右で共通する項目はここで定義し、左右で独立で設定するものはoverlayファイルに書きます。
曖昧ですが、physical-layoutでレイアウトを定義した方がいいらしいのでこれで定義しています。
今回はi2cを使用しないので、disabledしています。また、エンコーダはここでは定義してdisabledにしておく。後で左右のoverlayでそれぞれ有効化する。
matrix_transformでRCのcolを逆順にしているのは、KiCadの設計でcolをミスっていたためです。
/*
* Copyright (c) 2020 Pete Johanson
*
* SPDX-License-Identifier: MIT
*/
#include <dt-bindings/zmk/matrix_transform.h>
/ {
chosen {
zmk,kscan = &kscan0;
zmk,physical-layout = &physical_layout0;
};
default_transform: matrix_transform {
compatible = "zmk,matrix-transform";
columns = <14>;
rows = <4>;
map = <
RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,5) RC(0,12) RC(0,11) RC(0,10) RC(0,9) RC(0,8) RC(0,7)
RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,5) RC(1,12) RC(1,11) RC(1,10) RC(1,9) RC(1,8) RC(1,7)
RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,12) RC(2,11) RC(2,10) RC(2,9) RC(2,8) RC(2,7)
RC(3,0) RC(3,1) RC(3,2) RC(3,3) RC(3,4) RC(3,5) RC(3,6) RC(3,13) RC(3,12) RC(3,11) RC(3,10) RC(3,9) RC(3,8) RC(3,7)
>;
};
physical_layout0: physical_layout_0 { // First physical layout, use different naming for other layouts
compatible = "zmk,physical-layout";
display-name = "Default Layout";
kscan = <&kscan0>; // Label of the kscan node, optional if all layouts use the same
transform = <&default_transform>; // Label of the matrix transform for this layout
};
kscan0: kscan {
compatible = "zmk,kscan-gpio-matrix";
label = "KSCAN";
diode-direction = "col2row";
row-gpios
= <&xiao_d 0 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> //Row0
, <&xiao_d 1 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> //Row1
, <&xiao_d 2 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> //Row2
, <&xiao_d 3 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> //Row3
;
};
left_encoder: encoder_left {
compatible = "alps,ec11";
a-gpios = <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
b-gpios = <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
steps = <80>;
status = "disabled";
};
right_encoder: encoder_right {
compatible = "alps,ec11";
a-gpios = <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
b-gpios = <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
steps = <80>;
status = "disabled";
};
sensors: sensors {
compatible = "zmk,keymap-sensors";
sensors = <&left_encoder &right_encoder>;
triggers-per-rotation = <20>;
};
};
&xiao_i2c { status = "disabled"; };
7. 左右の固有ファイルを生成
今回のキーボードは左右で完全に対称となるので、confファイルに定義するのは左右の名前だけです。
ここで命名を極端に短くしている理由ですが、初めthumbRotary_LEFT などにしていたときに、名前が長いというように理由でビルドエラーが発生したからです。
CONFIG_ZMK_KEYBOARD_NAME="tr_l"
CONFIG_ZMK_KEYBOARD_NAME="tr_r"
overlayファイルに、col-gpiosをそれぞれ定義していますが、ここは同じなのでdtsiに書いても良かったなと思っています。個人的な理解では多分dtsiに書いても動きます。
また、それぞれdtsiでdisabledしていたロータリエンコーダの使用する方をokayとして使用できるように設定しています。
rightのoverlayではdefault_transformでオフセットを設定しています。これを設定しないとキーマップがうまくバインドされないので、左右分割の場合はオフセットを設定するようにしましょう。
#include "thumbRotary.dtsi"
&kscan0 {
col-gpios
= <&xiao_d 4 GPIO_ACTIVE_HIGH> //Col0
, <&xiao_d 5 GPIO_ACTIVE_HIGH> //Col1
, <&xiao_d 6 GPIO_ACTIVE_HIGH> //Col2
, <&xiao_d 7 GPIO_ACTIVE_HIGH> //Col3
, <&xiao_d 8 GPIO_ACTIVE_HIGH> //Col4
, <&xiao_d 9 GPIO_ACTIVE_HIGH> //Col5
, <&xiao_d 10 GPIO_ACTIVE_HIGH> //Col6
;
};
&left_encoder {
status = "okay";
};
#include "thumbRotary.dtsi"
&kscan0 {
col-gpios
= <&xiao_d 4 GPIO_ACTIVE_HIGH> //Col0
, <&xiao_d 5 GPIO_ACTIVE_HIGH> //Col1
, <&xiao_d 6 GPIO_ACTIVE_HIGH> //Col2
, <&xiao_d 7 GPIO_ACTIVE_HIGH> //Col3
, <&xiao_d 8 GPIO_ACTIVE_HIGH> //Col4
, <&xiao_d 9 GPIO_ACTIVE_HIGH> //Col5
, <&xiao_d 10 GPIO_ACTIVE_HIGH> //Col6
;
};
&default_transform { // Offset of 3 because the left side has 3 columns
col-offset = <7>;
};
&right_encoder {
status = "okay";
};
8. build.yamlを編集
これが最後です。
左右分割基盤なので、シールドを2つ追加します。
加えて、rgbled_adapter を追加しているのは、confで書いたXIAOのLEDを動作させるためです。
---
include:
- board: seeeduino_xiao_ble
shield: thumbRotary_left rgbled_adapter
- board: seeeduino_xiao_ble
shield: thumbRotary_right rgbled_adapter
9. git pushしてActionsのビルドを確認
ここまできたらプッシュしてGithub Actionsでビルドが通る確認しましょう。
特に抜け漏れがなければ通るかと思います。
この記事はファームウェアの作成の備忘録となるため、書き込みについては別の方の記事を参考にしてください。
参考文献
XIAO nRF52840 と ZMK で1キーだけのキーボードを作る
小市民、キーボードを設計する。 ② 〜ZMK Firmware構築〜|mame
Zonkeyキーボード購入者のための「はじめてのZMKキーマップ編集」入門記事|Zonkey製作者(Goya)


