🕊️

KMKfw(KMK Firmware)の書き方の解説をするよ!

2024/09/22に公開

KMKファームウェアの書き方を解説します

実際に私が自作キーボードを作った中で使用した範疇、理解の範疇で解説を行います。
流れとしては記事の内容を上からなぞると動くファームウェアができていることを目指します。

気持ち

ファームウェアを書くときの公式と公式のgithubに登録されているのを参考にしたのだけど、いまいち使われていない機能やアプデで増えた機能などはドキュメントしか例なくて使いこなすのがちょっと大変だった。なので動くサンプルを提供したくて書いてる。
ある程度ソースを斜め読みして仕様を把握したが、理解よりも動くことを優先しているのでそこらへんは勘弁。

環境構築

KMKの公式
circuit pythonの公式
日本人のブログ
どれでもいいです。バージョンが違っても手順は変わっていないです。
Thonnyはデバッグの際に便利なので入れています。

基礎のキーボード

最小限の構成では次のようになります。
このソースコードにいろいろ足していくことになります。
JisakuKBについては好きな名前できます。keyboardとしていることが多いですが、キーボードの名前にすることもあり、他のボードのソースを参考にする場合は適宜読み替えることになります。
マトリクスやキーマップもここで定義しています。qmkのkeymap.cみたいな内容を書きます。
使用したキーコードについてはキーコードのページにあります。

code.py
#自作44キー - Waveshare RP2040-Tiny
import board
from kmk.kmk_keyboard import KMKKeyboard
from kmk.keys import KC
from kmk.scanners import DiodeOrientation
JisakuKB = KMKKeyboard()

# matrix
JisakuKB.col_pins = (board.GP8, board.GP7, board.GP6, board.GP5, board.GP4, board.GP3, board.GP29, board.GP28, board.GP27, board.GP26, board.GP15, board.GP14)
JisakuKB.row_pins = (board.GP10, board.GP11, board.GP12,board.GP13)
JisakuKB.diode_orientation = DiodeOrientation.COL2ROW

# keymap
JisakuKB.keymap = [
   [ # layer 0 アルファベットのレイヤー
     #  GP8,  GP7,     GP6,     GP5,     GP4,     GP3,           GP29,    GP28,    GP27,    GP26,    GP15,     GP14)
     KC.TAB,  KC.Q,    KC.W,    KC.E,    KC.R,    KC.T,          KC.Y,    KC.U,    KC.I,    KC.O,    KC.P,     KC.BSPC, # GP10
     KC.LCTL, KC.A,    KC.S,    KC.D,    KC.F,    KC.G,          KC.H,    KC.J,    KC.K,    KC.L,    KC.MINS,  KC.QUOT, # GP11
     KC.LSFT, KC.Z,    KC.X,    KC.C,    KC.V,    KC.B,          KC.N,    KC.M,    KC.COMM, KC.DOT,  KC.JYEN,  KC.SLSH, # GP12
     KC.NO,   KC.NO,   KC.LGUI, KC.LALT, KC.SPC,  KC.SPC,        KC.ENT,  KC.ENT,  KC.NO,   KC.NO,   KC.NO,    KC.NO,   # GP13
   ]
]

if __name__ == '__main__':
   JisakuKB.go()

デバッグ

デバックログをオンにする項目

JisakuKB.debug_enabled = True

これをTrueにするとキー押下時のイベントのログが出るので非常に便利です。めちゃめちゃログが出ます。

from kmk.utils import Debug
debug = Debug(__name__)
debug('任意のログメッセージ')

こちらは任意のログを流したいときに使います。python標準のprint文でも問題ありませんが、実行時の時間やどのモジュールかなどの情報が多いので役に立つこともあるやもしれません。わからん。

新しいキーの作成

任意のキーに任意の名前をつけて使用できます。

T = KC.TAB

としておくと、キーマップには単に Tと記載します。
シフト押しながらA などのような同時押しキーを作成するには

BigA = KC.LSFT(KC.A)

としてキーマップにBigAを記載します。
さらに複雑なキーの同時押し、コンボ、HoldTapなどを行う場合は後述のモジュールを使用します。
その際にも同様に任意の名前をつけて、その名前をキーマップに記載することになります。

エクステンションとモジュール

これ以上の機能はエクステンションあるいはモジュールとして実装されています。
このふたつの違いは説明しにくいので各ページを見比べてほしいです。
実用上、ソースがあるフォルダやimport時に微妙に違いが出る程度なので自分の中の認識があいまいです。

extension

国際キー

メディアキーに載っているキーが使いたい場合はこれを導入する。JIS配列っぽいキーボードの場合は必要になります。
手元の環境だと、importするとエラーになるが、最新版だと治っているはず
よくわからないけど動くからヨシ!

memo

メインのプログラムのココに実装されているので、extensionではなくなったのかもしれない。これが追加されたのはこのcommit
で、この部分9月のコミットで修正が入ってなおったっぽい

メディアキー

メディアキーに載っているキーが使いたい場合はこれを導入する。

from kmk.extensions.media_keys import MediaKeys
JisakuKB.extensions.append(MediaKeys())

RGB LED

公式ページ
単色LEDではなく NeoPixel のRGBLEDの制御 単色は別項目
今回はrp2040tinyに乗ってるRGBLEDを制御します
まずneopixelのライブラリをしかるべき場所においてね

from kmk.extensions.peg_rgb_matrix import Rgb_matrix,Rgb_matrix_data,Color
from kmk.extensions.RGB import RGB, AnimationModes
pixel_pin = board.GP16
num_pixels = 1
rgb = RGB(pixel_pin=pixel_pin,
          num_pixels=num_pixels,
          hue_default = 0,
          sat_default = 0,
          val_default = 100,
          )
JisakuKB.extensions.append(rgb)

ここでのデフォルトHSV値でキーボードのboot時のLEDが設定できるはずだが、なんか思ってたように変わらないので謎
キー入力に合わせてLEDの光を変更する例としてレイヤーインジゲーターを実装する例は後述

モジュール

公式ページ

レイヤー機能

公式ページ
例として数字キーレイヤーとFnキーレイヤーを追加します。
まずモジュールを読み込み、追加します。

# Layers support
from kmk.modules.layers import Layers
JisakuKB.modules.append(Layers())

次にレイヤー移動のためのキーを設定します。
数字レイヤーはレイヤーキーを押している間のみ有効
Fnレイヤーはレイヤーキー押下でアクティブにし、他のすべてのレイヤーを非アクティブとします。
使っているレイヤーの番号は後述のキーマップの配列の番号に対応しています。

# 数字レイヤー
Num = KC.MO(1)
# Fnレイヤー
Fn = KC.TO(2)
# デフォルトレイヤー
Def = KC.TO(0)

最後にキーマップを追加します。

JisakuKB.keymap = [
   [ # layer 0 アルファベットレイヤー
     KC.TAB,  KC.Q,    KC.W,    KC.E,    KC.R,    KC.T,          KC.Y,    KC.U,    KC.I,    KC.O,    KC.P,     KC.BSPC,
     KC.LCTL, KC.A,    KC.S,    KC.D,    KC.F,    KC.G,          KC.H,    KC.J,    KC.K,    KC.L,    KC.MINS,  KC.ENT,
     KC.LSFT, KC.Z,    KC.X,    KC.C,    KC.V,    KC.B,          KC.N,    KC.M,    KC.COMM, KC.DOT,  KC.JYEN,  KC.SLSH,
     KC.NO,   KC.NO,   KC.LGUI, KC.LALT, KC.MHEN, KC.SPC,        KC.ENT,  KC.HENK, KC.NO,   Num,     Fn,       KC.NO,    
   ],
   [ # layer 1 数字レイヤー      
     KC.ESC,  KC.N1,   KC.N2,   KC.N3,   KC.N4,   KC.N5,         KC.N6,   KC.N7,   KC.N8,   KC.N9,   KC.N0,    KC.BSPC,
     KC.LCTL, KC.N6,   KC.N7,   KC.N8,   KC.N9,   KC.N0,         KC.LEFT, KC.DOWN, KC.UP,   KC.RGHT, KC.MINS,  KC.ENT,
     KC.LSFT, KC.NO,   KC.NO,   KC.NO,   KC.NO,   KC.NO,         KC.NO,   KC.NO,   KC.NO,   KC.NO,   KC.NO,    KC.NO,
     KC.NO,   KC.NO,   KC.LGUI, KC.LALT, KC.MHEN, KC.SPC,        KC.ENT,  KC.HENK, KC.NO,   KC.NO,   KC.NO,    KC.NO,    
   ],
   [ # layer 2 Fnレイヤー      
     KC.ESC,  KC.F1,   KC.F2,   KC.F3,   KC.F4,   KC.F5,         KC.F6,   KC.F7,   KC.F8,   KC.F9,   KC.F10,   KC.NO,
     KC.LCTL, KC.F11,  KC.F12,  KC.F13,  KC.F14,  KC.F15,        KC.F16,  KC.F17,  KC.F18,  KC.F19,  KC.F20,   KC.NO,
     KC.NO,   KC.NO,   KC.NO,   KC.NO,   KC.NO,   KC.NO,         KC.NO,   KC.NO,   KC.NO,   KC.NO,   KC.NO,    KC.NO,
     KC.NO,   KC.NO,   KC.LGUI, KC.LALT, KC.MHEN, KC.SPC,        KC.ENT,  KC.HENK, KC.NO,   KC.NO,   Def,      KC.NO,    
   ],
]

レイヤーLEDインジゲーター機能

この機能にはレイヤー、RGBLED、マクロ機能の3種類を用いています。
なんでこれでうまく動くのかわかっていませんが、レイヤーに応じてLEDの色が変わるので満足しています。

# レイヤー
from kmk.modules.layers import Layers
JisakuKB.modules.append(Layers())
# LED
from kmk.extensions.peg_rgb_matrix import Rgb_matrix,Rgb_matrix_data,Color
from kmk.extensions.RGB import RGB, AnimationModes
pixel_pin = board.GP16
num_pixels = 1
rgb = RGB(pixel_pin=pixel_pin,
          num_pixels=num_pixels,
          hue_default = 0,
          sat_default = 0,
          val_default = 100,
          )
JisakuKB.extensions.append(rgb)
# マクロ
from kmk.modules.macros import Macros, Press, Release, Tap
macros = Macros()
JisakuKB.modules.append(macros)

def LED_SET0(keyboard):
    rgb.set_rgb_fill((255,255,255))
    rgb.show()
def LED_SET1(keyboard):
    rgb.set_rgb_fill((0,0,255))
    rgb.show()
def LED_SET2(keyboard):
    rgb.set_rgb_fill((0,255,0))
    rgb.show()
Def = KC.MACRO(
    Tap(KC.TO(0)),
    LED_SET0,
)
Num = KC.MACRO(
    Tap(KC.MO(1)),
    LED_SET1,
)
Fn  = KC.MACRO(
    Tap(KC.TO(2)),
    LED_SET2,
)

ホールドタップ

公式ページ
ホールドタップという機能自体が複雑でつかってない、教えてほしい。

コンボ機能

公式ページ

マクロ機能

公式ページ
かなり有用かつ、複雑で理解が及んでいません。
レイヤーLEDインジゲーター機能や応用編で使ってはいますが、解説は何も書けない。
今度ソースコード読むから...

応用編

REALFORCEのKill Switch、あるいは格闘ゲームのレバーレスコンのSOCDクリーナーのような機能を実装することができます。最近だとwootingも有名ですね。
動いたものを乗っけておきます。

# kill switch macro
pressed_keys = []
def RePress(key):
    def generator(keyboard):
        if (key =="W" and "W" in pressed_keys):
            KC.W.on_press(keyboard)
            yield
        if (key =="A" and "A" in pressed_keys):
            KC.A.on_press(keyboard)
            yield
        if (key =="S" and "S" in pressed_keys):
            KC.S.on_press(keyboard)
            yield
        if (key =="D" and "D" in pressed_keys):
            KC.D.on_press(keyboard)
            yield
    return generator

Wkl =  KC.MACRO(
    on_press=(
        Release(KC.S),
        lambda _: pressed_keys.append("W"),
        Press(KC.W),
    ),
    on_release=(
        lambda _: pressed_keys.remove("W"),
        Release(KC.W),        
        RePress("S")
    ),
    blocking=False
)

Akl =  KC.MACRO(
    on_press=(
        Release(KC.D),
        lambda _: pressed_keys.append("A"),
        Press(KC.A),
    ),
    on_release=(
        lambda _: pressed_keys.remove("A"),
        Release(KC.A),        
        RePress("D")
    ),
    blocking=False
)

Skl =  KC.MACRO(
    on_press=(
        Release(KC.W),
        lambda _: pressed_keys.append("S"),
        Press(KC.S),
    ),
    on_release=(
        lambda _: pressed_keys.remove("S"),
        Release(KC.S),        
        RePress("W")
    ),
    blocking=False
)

Dkl =  KC.MACRO(
    on_press=(
        Release(KC.A),
        lambda _: pressed_keys.append("D"),
        Press(KC.D),
    ),
    on_release=(
        lambda _: pressed_keys.remove("D"),
        Release(KC.D),        
        RePress("A")
    ),
    blocking=False
)

この記事は自作のusui44で書きました。

Discussion