雑記帳

技術ネタとか

ファームビルド時の"Options to Xassembler do not match: -adhlns"というWarningについて

ビルド時にWarningが出るので一応調査。

QMKファームビルド時に、rules.mkにて-fltoオプションを付けると以下のようなWarningが出てくる。

 | lto-wrapper: warning: Options to Xassembler do not match: -adhlns=.build/obj_ble_micro_pro_my_crkbd/oled_bongo.lst, -adhlns=.build/obj_ble_micro_pro_my_crkbd/oled_luna.lst, dropping all -Xassembler
and -Wa options.
 | Memory region         Used Size  Region Size  %age Used
 |            FLASH:       92508 B       744 KB     12.14%
 |              RAM:       28572 B       160 KB     17.44%
 |

-Xassemblerというのはgccコンパイルオプションで、アセンブラに渡すパラメータを指定するもの。

$ man gcc
   Passing Options to the Assembler
       You can pass options to the assembler.

       -Wa,option
           Pass option as an option to the assembler.  If option contains commas, it is split into multiple options at the commas.

       -Xassembler option
           Pass option as an option to the assembler.  You can use this to supply system-specific assembler options that GCC does not recognize.

           If you want to pass an option that takes an argument, you must use -Xassembler twice, once for the option and once for the argument.

-adhlnsについてはアセンブラ(asコマンド)のオプションとして定義されている。

$ man as
       -a[cdghlmns]
           Turn on listings, in any of a variety of ways:

           -ac omit false conditionals

           -ad omit debugging directives

           -ag include general information, like as version and options passed

           -ah include high-level source

           -al include assembly

           -am include macro expansions

           -an omit forms processing

           -as include symbols

           =file
               set the name of the listing file

           You may combine these options; for example, use -aln for assembly listing without forms processing.  The =file option, if used, must be the last one.  By itself, -a defaults to -ahls.

このオプションを付けるとコンパイル時に以下のようなアセンブルリストファイルを作成できる。

$ gcc -c -Wa,-adhlns=a.lst a.c
$ head a.lst
   1                            .file   "a.c"
   2                            .text
   3                            .section        .rodata
   4                    .LC0:
   5 0000 613A2564              .string "a:%d\n"
   5      0A00
   6                            .text
   7                            .globl  main
   9                    main:
  10                    .LFB0:

このオプションだが、コンパイル時に-fltoオプションによりLTOを有効化するとオブジェクトファイルにも埋め込まれてしまう。 ちなみにLTOというのはリンク時最適化のことで、こちらの記事で解説されている。 LTOを有効化しなければ引数は埋め込まれない。

$ gcc -c -flto -Wall -Werror -Wa,-adhlns=a.lst a.c
$ strings a.o | grep adh
'-fno-openmp' '-fno-openacc' '-fPIC' ... (中略) ... '-Xassembler' '-adhlns=a.lst'  # 引数埋め込み
$ gcc -c -Wall -Werror -Wa,-adhlns=a.lst a.c
$ strings a.o | grep adh
$                         # ltoを指定しなければ引数は埋め込まれない

今回確認されたログメッセージは、-adhlnsが埋め込まれたオブジェクトファイル同士をリンクさせると出力される。 オブジェクトファイルごとに異なる-adhlnsオプションがついていて競合するのでマージ時には無視するという趣旨のメッセージと思われる。

$ gcc -c -flto -Wall -Werror -Wa,-adhlns=a.lst a.c
$ gcc -c -flto -Wall -Werror -Wa,-adhlns=b.lst b.c
$ gcc -c -flto -Wall -Werror -Wa,-adhlns=c.lst c.c
$ for i in a.o b.o c.o; do strings $i | grep "\(-Xassembler\|-Wa\)" | sed -e "s/.*\(-Xassembler\|-Wa\)/\1/g"; done
-Xassembler' '-adhlns=a.lst'
-Xassembler' '-adhlns=b.lst'
-Xassembler' '-adhlns=c.lst'
$ gcc -flto -Wa,-adhlns=x.lst a.o b.o c.o
lto-wrapper: warning: Options to ‘-Xassembler’ do not match: -adhlns=b.lst, -adhlns=c.lst, dropping all ‘-Xassembler’ and ‘-Wa’ options.

さて、QMKのファームウェアは、デフォルトだとmakeしたときにmakefileのこの辺りの定義により自動的に-adhlnsオプションが付与されるようになっている。これにより、オブジェクトファイルのほかにアセンブルリストファイルも出力される。

$ ls .build/obj_ble_micro_pro_my_crkbd/quantum/quantum.* | cat
.build/obj_ble_micro_pro_my_crkbd/quantum/quantum.d
.build/obj_ble_micro_pro_my_crkbd/quantum/quantum.lst  # 勝手に作成される
.build/obj_ble_micro_pro_my_crkbd/quantum/quantum.o

そのため、この状態でrules.mk等により-fltoオプションを追加すると、オブジェクトファイル同士をリンクする際に先述のWarningが出力されてしまう。 しかし、-adhlns自体はアセンブルリストファイルを出力するためのオプションであり、既にアセンブルリストファイルはオブジェクトファイルごとに出力されている。 また、それ以外の-Xassembler系のオプションはオブジェクトファイルに仕込まれていないため、このWarningは無視しても問題なさそう。

$ for i in $(find .build/obj_ble_micro_pro_my_crkbd/ | grep "\.o"); do strings $i | grep "\(-Xassembler\|-Wa\)" | sed -e "s/.*\(-Xassembler\|-Wa\)/\1/g"; done
-Xassembler' '-adhlns=.build/obj_ble_micro_pro_my_crkbd/common/host.lst'
-Xassembler' '-adhlns=.build/obj_ble_micro_pro_my_crkbd/common/nrf/bootloader.lst'
-Xassembler' '-adhlns=.build/obj_ble_micro_pro_my_crkbd/common/nrf/eeprom.lst'
-Xassembler' '-adhlns=.build/obj_ble_micro_pro_my_crkbd/common/nrf/platform.lst'
-Xassembler' '-adhlns=.build/obj_ble_micro_pro_my_crkbd/common/nrf/printf.lst'
-Xassembler' '-adhlns=.build/obj_ble_micro_pro_my_crkbd/common/nrf/suspend.lst'
-Xassembler' '-adhlns=.build/obj_ble_micro_pro_my_crkbd/common/nrf/timer.lst'
(以下略)

Corne cherry + BMPの左右接続の有線化

BMPを搭載したCorne cherryの左右のキーボードの通信を一部有線化した話

はじめに

Corne cherryにBMPを搭載してしばらく使ってみたが、左右のBLE接続が不安定になる時がたまにある。

キー入力の不安定さの要因としては左右のBMPの接続性の問題のほか、そもそもmasterとPCとの接続の安定性の問題の可能性もあるが、masterとPCの間をUSB接続にしても調子悪い時はあるし、右手側のキー入力だけ遅延して日本語入力がちぐはぐになったりすることもあった。 単にミスタイプしているだけという可能性もなくはないが、接続の調子が悪い時はゆっくり丁寧に入力してもうまく認識しないので、接続性に問題があるときがあるというのは事実と思う。

今回はCorne cherryの左右のキーボードの通信を有線化するという話になる。 それで接続不安定が少しでも解消すると嬉しい。

(余談)ログ解析

接続不安定というだけなので当然普段は正常に動いている。 ハードやソフトのバグという可能性もなくはないが、普通に考えて結局は室内環境が電波的にノイズが多くそもそも通信が失敗している可能性が最も高い。 しかし電波というのは目に見ないし耳にも聞こえないので、ノイズが多いといわれてもピンとこない。ログを探してみる。

左右のキーボードをそれぞれPCにUSB接続するとシリアル経由でログが出力されるが、それを見ると結構頻繁に再接続していることがわかる。以下のようなメッセージが確認できた。(一部改造によって出力している行もあるが)

master側

NUS disconnected ...
<info> app: Slave keyboard is disconnected
<info> app: Scanning slave keyboard...
<info> app: adv:Nordic_UART
<info> app: Connected to the slave keyboard
<info> app: Connected to device 0.
NUS disconnected ...
<info> app: Slave keyboard is disconnected
<info> app: Scanning slave keyboard...
<warning> ble_nus_c: Connection handle invalid.
<warning> ble_nus_c: Connection handle invalid.
<info> app: adv:Nordic_UART
<info> app: Connected to the slave keyboard
<info> app: Connected to device 0.

slave側

State changed event: BLE_CONNECTED, connection status: 0x0000
<info> app: Connected to master keyboard:0
State changed event: BLE_DISCONNECTED, connection status: 0x0000
<info> app: Disconnected from master keyboard
State changed event: BLE_ADVERTISING_START, connection status: 0x0000
<info> app: BLE_ADV_EVT_FAST_WHITELIST

これらのメッセージは割と定常的に出力されているが、調子が悪い時は再接続が遅かったり、接続断の頻度が高いように思う。試しにログのタイムスタンプをベースにエラー発生頻度をグラフにしてみた。

以下はMaster側で採取されたログで、各時刻(分単位)において、1分間に何回程度接続が切れたかを示す。なお、そのままの値だとばらつきが多くグラフが見づらかったため、前後10分間で平均した値を利用している。

こちらは同Slave側。

以下はMaster側で採取されたログで、各時刻(分単位)において、前後10分間で再接続に要した時間の平均値を示す。

こちらは同Slave側

ここから読み取れるのは

  • 20時ごろから翌2時頃まで、再接続の回数が1分間で2回程度発生するようになっている。深夜3時~深夜4時の間は1分間で4回以上接続が切れている。4時以降も定常的に1分間に1回程度接続が切れている模様。
  • 接続断の回数についてはSlaveはMasterとほぼ同じ傾向を示すが、深夜3時~4時の間の接続試行回数はMasterほど多くない。
  • 平均接続時間に関しては、MasterもSlaveも割と安定している。MasterからSlaveに接続する際は約2.5秒、SlaveからMasterに接続する際は4秒弱かかっている。
    • MasterとSlaveによって接続にかかる時間が異なるのは不明だが、BLE接続処理において接続完了と判断するタイミングがそもそも異なっている可能性がある。
  • Slave側の平均接続時間については、深夜3時~4時近辺で大幅に伸びている。
    • Master側の接続再試行回数がSlave側の接続再試行回数に比べて多いこととあわせて考えると、Masterは一瞬だけ接続が確立したと誤認識したものの、Slave側では接続が確立したと認識しておらず、通信に矛盾が生じてすぐにMaster側で再接続処理が走った可能性がある。

ログ採取は一回しかやっていないので、なぜ通信が不安定になったのかについては不明だが、少なくとも使用感としては 夜8時以降はキーボード入力にストレスを感じるようになったことから、1分間に2回、4秒程度の接続断が発生すると厳しい。

修正方針

今回Corneで有線接続を実現するにあたって重視するポイントは以下。

  • 左右のキーボードそれぞれでOLEDによる情報表示を行っているため、I2C通信は左右通信で使わない
  • TRRSケーブルのジャックがCorneにあるので、左右の通信はTRRSケーブルを利用する
    • TRRSケーブルなので4極。VDDとGNDで2つ端子を使うので、通信に使える端子は2つ

これに対しnRF52840のマニュアルにはいくつか通信方法が記載されているが、SPIは必要なピン数が多いしI2Cは使えないので実質UART一択になる。 GPIOでピンを認識してアプリ側でbitbangによる通信を行うこともできなくはないが、BLEやUSBでPCと通信したりOLED描画をしたりキーボードとしての本業の処理もある中、bitbangによる処理までさせて大丈夫か不安。 UARTであればペリフェラルなので、性能影響は低いはず。DMAの機能もあるみたいだし。

物理的な線の増設

Corne cherryキーボードにおいては、左右のキーボードはTRRSケーブルによって接続される。 しかし、TRRSケーブル自体は4極でCorneのキーボードに取り付ける際も4箇所はんだ付けするにもかかわらず、実は基盤回路上はそのうち3本しか配線されていない。 1つはVDD、1つはGND、1つはD2ピンで、もう一つは接続なし。

UART通信では送信用と受信用で2つ配線が必要なので、BMPがUART通信をするためにはD2ピンのほかにもう一つピンを決めてTRRSジャックのピンに接続しなくてはならない。

実際にピンの配線を見てみると、Corneの基盤ではB2/B4/B5/B6のあたりがどこにも接続されておらず、未使用になっている。 ざっとソースコードやドキュメントを確認し、B6ピンあたりはCorne cherry+BMPにおいては使ってなさそうと判断。 UARTの送受信はD2とB6ピンを用いることとし、TRRSケーブルの余った端子にはD6ピンを接続する。

実際にはんだ付けをしたのが以下。新しい配線によりコンスルーがささらなくなっては意味がないので、D6ピン側は基盤の端子の周辺の金属部分にぎりぎり乗せるだけとし、コンスルーの抜き差しに影響がないようにした。

右手側

左手側

なお、UART通信においては送信ポートと受信ポートを互いに配線する必要があり、いわゆるクロスの配線となるのだが、Corneの基盤上D2は既に配線がありこれを後付けで変更することはできないため、slave側のキーボードは内部でソフトウェア的に送受信のピンを入れ替える方針としている。そのため両方同じ用にケーブルを追加した。

ソフトウェア処理の改造

物理的な配線が終わったら今度はソフトウェア処理。大きく分けて以下三つの対応が必要

  • 左右のキーボードでD2/B6ピンによる安定したUART通信が行えるようにすること
  • キーボードの入力処理において、現在BLE経由で左右のキーボード間で情報共有を行っている個所を特定し、方式をUART通信に差し替え
  • (オプション)キーボードの入力処理以外の周辺処理についてもBLE通信処理に依存している部分を探しUARTに差し替え

とりあえず上二つの実現を目指す。

BMPにおけるUARTのAPI

好都合なことに、BMPにおいてはUART利用のためのAPIが既に用意されている。

https://github.com/sekigon-gonnoc/qmk_firmware/blob/dev/ble_micro_pro/tmk_core/protocol/nrf/sdk15/apidef.h

typedef struct {
    uint8_t  tx_pin;
    uint8_t  rx_pin;
    uint32_t baudrate;
    void (*rx_callback)(uint8_t recv);
    uint8_t  rx_protocol;
} bmp_uart_config_t;
...
typedef struct {
    void (*init)(bmp_uart_config_t const* const config);
    uint32_t (*send)(uint8_t const* dat, uint32_t len);
} bmp_api_uart_t;

init処理でrxとtxのピンが指定できるので指定をする。 ボーレートは通信速度に関連するパラメータっぽい。nordicのドキュメント上、安定した通信には外部クロックが必要と書いてあるので、まずは低いボーレートで試して支障がありそうなら調整する方針とする。 体感で遅延がなければいい程度の性能要求なので低くても問題ないと思う。 rx_protocolパラメータは謎。もとのnordicの機能にそんな機能はないが、とりあえず1にして動いている。keyboard_quantizerに実装が既にあり、initで1にしているのでこれに倣えば問題ないと思う。

    bmp_uart_config_t uconf = {
        .tx_pin      = (is_keyboard_master()) ? D2 : B6,
        .rx_pin      = (is_keyboard_master()) ? B6 : D2,
        .baudrate    = Baud1200,
        .rx_callback = data_recv_cb,
        .rx_protocol = 1
    };

callback関数に関しては、NUSのやり方と関数の引数が違うため、注意が必要。呼び出し側はデータへのポインタとデータ長を渡すが、受け取り側は1byteずつの受け取りとなるので、バッファを自分で用意する必要がある。

128byte制約

実際にAPIを使って通信をしてみたが、最初うまく通信できなかった。 D2/B6ピンをUARTではなくGPIOで初期化して通信してみると、信号自体は通っている模様。 いろいろ試したところ、データは1byteずつ送信されるのではなく、128バイト単位で送信されているようだった。 送信側で128byte以上データを書き込むことで、受信側で128回受信関数が呼び出される。

送信側か受信側か、どちらかでAPI内部でバッファリングしているものと思われる。 アプリ側のデータ通信の即応性を上げるためには、データ本体を送った後にダミーデータを128byte送信するなどして、データをバッファから押し流す必要がある。

また、送信データと受信データの位置や認識タイミングがずれると認識しなくなる恐れがあり、 また上位関数に受信データを渡す際にはデータのバッファリングとデータ長の管理をする必要もあるので、 送受信するデータに対して、どこからデータが始まりどれだけの長さのデータなのかが識別できるように、以下のようにフレームを作成してUART通信するような実装を追加した。

マトリックススキャンの処理

QMKにおけるキー入力の認識は、マトリックスのスキャンという仕組みによって実現される。

(解説記事) https://docs.qmk.fm/#/ja/how_a_matrix_works

キーボードの基盤においてはマイコンの各ピンから格子状に配線が伸びており、その交点にキースイッチが配置されている。 マイコンは各ピンに電圧をかけて、どのピンが反応したかをスキャンすることでどのキースイッチが押されているか判断する。

例えば以下の画像で示すスイッチの場合、B1/D7のピンの間で電流が通るかどうかを確認することで入力状態を確認できる。

どのピンをどの行に割り当てるか、電流が流れる方向はどちらか等の細かい違いはキーボードによってあるが、基本的にマトリックススキャンをやっているはず。

BMPファームウェアにおいてマトリックススキャンを実施しているのはmatrix_scan_implで、ここでweak付きで定義されている。 これを自分のキーボードのディレクトリでオーバーライド定義してやれば処理をカスタマイズできる。

この関数の処理の概観は以下

__attribute__((weak)) uint8_t matrix_scan_impl(matrix_row_t *_matrix) {
    const bmp_api_config_t *config     = BMPAPI->app.get_config();
    const uint8_t           device_row = matrix_func->get_device_row();
    const uint8_t           device_col = matrix_func->get_device_col();
    uint8_t                 matrix_offset =
        config->matrix.is_left_hand ? 0 : config->matrix.rows - device_row;
    int matrix_changed = 0;

    uint32_t raw_changed = matrix_func->scan(matrix_debouncing);           //このへんで片手分のキー状態をスキャン
    bmp_api_key_event_t key_state[16];
    matrix_changed = bmp_debounce(
        matrix_debouncing + matrix_offset, matrix_dummy + matrix_offset,
        device_row, device_col,
        config->matrix.debounce * MAINTASK_INTERVAL, raw_changed, key_state);

    for (int i = 0; i < matrix_changed; i++) {
        BMPAPI->app.push_keystate_change(&key_state[i]);                   //このへんでslaveからmasterにデータ送信
    }

    if (debug_config.keyboard && matrix_changed > 0) {                     //片手分デバッグログ
        dprintf("device rows:\n");
        for (uint8_t idx = 0; idx < device_row; idx++) {
            if (config->matrix.cols <= 8) {
                dprintf("\tdr%02d:0x%02x\n", idx,
                        matrix_debouncing[idx + matrix_offset]);
            } else if (config->matrix.cols <= 16) {
                dprintf("\tdr%02d:0x%04x\n", idx,
                        matrix_debouncing[idx + matrix_offset]);
            } else {
                dprintf("\tdr%02d:0x%08x\n", idx,
                        matrix_debouncing[idx + matrix_offset]);
            }
        }
        dprintf("\n");
    }

    uint32_t pop_cnt = BMPAPI->app.pop_keystate_change(                    //このへんでmasterで情報取得
        key_state, sizeof(key_state) / sizeof(key_state[0]),
        config->param_central.max_interval / MAINTASK_INTERVAL + 3);

    for (uint32_t i = 0; i < pop_cnt; i++) {                               //masterとslave合わせてキー状態を更新
        if (key_state[i].state == 0) {
            _matrix[key_state[i].row] &= ~(1 << key_state[i].col);
        } else {
            _matrix[key_state[i].row] |= (1 << key_state[i].col);
        }
    }

    if (debug_config.keyboard && pop_cnt > 0) {                           //最終的なキー状態のデバッグログ
        dprintf("matrix rows:\n");
        for (uint8_t idx = 0; idx < device_row; idx++) {
            if (device_col <= 8) {
                dprintf("\tr%02d:0x%02x\n", idx, _matrix[idx]);
            } else if (device_col <= 16) {
                dprintf("\tr%02d:0x%04x\n", idx, _matrix[idx]);
            } else {
                dprintf("\tr%02d:0x%08x\n", idx, _matrix[idx]);
            }
        }
        dprintf("\n");
    }

    return pop_cnt;
}

その中でもslave側からキーを送っているのがBMPAPI->app.push_keystate_changeの部分で、master側でキーを受け取っているのがBMPAPI->app.pop_keystate_changeの部分。

この挙動については、debug keyboardを有効化しておくことでデバッグログを有効化でき、右手側と左手側で出力されるログが異なることを確認できる。 分割型キーボードのキースキャン結果は、まず左右それぞれのキーボードで別々に行われ、そのあとそれらの結果がマージされる。

左右個別のスキャン結果は以下のようにdevice rowsとして出力され、これはmaster/slaveそれぞれで確認ができる。以下の例だとdr03:0x20となっており、3行目の6列目のスイッチが押されたことを意味する。

device rows:
        dr00:0x00
        dr01:0x00
        dr02:0x00
        dr03:0x20

master側では本体で検出したキー状態とslave側から送られてきたキー状態を組み合わせて、マトリックスとして両手合わせたスキャン結果を認識する。 両側のキーボードの情報を組み合わせるにあたって、情報が混ざるのを防ぐためにslave側のキー入力情報はdr0, dr1, dr2, dr3がそれぞれr4, r5, r6, r7に自動的に再割り当てられる。

このスキャン結果はmasterのデバイスにおいて、上記のdevice rowsの出力とは別に、matrix rowsとして表示される。(と思ったがログ表示のためにはこの修正が必要だった。)

matrix rows:
        r00:0x00
        r01:0x00
        r02:0x00
        r03:0x00
        r04:0x00
        r05:0x10
        r06:0x00
        r07:0x00

このBMPAPI->app.push_keystate_changeBMPAPI->app.pop_keystate_changeによるキー入力状態の送受信およびマージ処理をUARTの通信を使った通信に置き換える。

こういう構造体を作っておいて、

typedef struct {
    uint8_t count;
    bmp_api_key_event_t key_state[16];
} bmp_uart_keystate_t;

static bmp_uart_keystate_t uart_buf;

Slave側の処理として、key_stateのデータとmatrix_changedの値をUARTのバッファに詰め込んで送信する。

Slave側のdr0, dr1, dr2, dr3をそれぞれr4, r5, r6, r7に再割り当てする処理はBMPAPI->app.push_keystate_changeの内部で実施しているようなので、 UARTの通信に置き換える場合はこの部分の計算はアプリ側で書き加える必要がある。

    if (is_uart_established()) {
        for (int i = 0; i < matrix_changed; i++) {
            key_state[i].row += matrix_offset;
        }

        if (!is_keyboard_master() && matrix_changed > 0) { //SLAVE
            uart_buf.count = matrix_changed;
            memcpy(uart_buf.key_state, key_state,
                    sizeof(bmp_api_key_event_t) * matrix_changed);
            uart_send(UART_TYPE_KC_SYNC, (uint8_t *)&uart_buf,
                    sizeof(bmp_api_key_event_t) * matrix_changed + 1);
        }
    } else {
        for (int i = 0; i < matrix_changed; i++) {
            BMPAPI->app.push_keystate_change(&key_state[i]);
        }
    }

また、master側ではそれを受信してkey_stateに格納しなおす処理を追加する。 試した感じ、pop_keystate_changeは内部でスタックを持っているようで、masterとslaveでpushされたデータをマージした結果をmaster側で取り出せるようになっていた。 シンプルにUARTでデータを受信する場合、そこにmaster側のデータは入っていないので、誤ってmaster側が自身で持っているkey_state変数を上書きしないように注意する。

    uint32_t pop_cnt = 0;
    if (is_uart_established()) {
        if (is_keyboard_master()) { //MASTER
            pop_cnt = matrix_changed + uart_buf.count;
            memcpy(&key_state[matrix_changed], uart_buf.key_state,
                   sizeof(bmp_api_key_event_t) * uart_buf.count);
            uart_buf.count = 0;
        }
    } else {
        pop_cnt = BMPAPI->app.pop_keystate_change(
            key_state, sizeof(key_state) / sizeof(key_state[0]),
            config->param_central.max_interval / MAINTASK_INTERVAL + 3);
    }

余談だがkey_stateのサイズが16なので、両手で16以上のキーを同時押しするとバッファオーバーフローで問題動作を起こす可能性がありそう。 両手の指は10本しかないので16キー同時押し問題は考慮する必要ないが。

UART/BLEの動的選択

ここまででひとまずUART通信でキーボード入力を認識させることはできるようになったが、逆にケーブルがない時に左右のキーボードで通信できないのは困る。 キーボード初期化処理の一環としてUARTによる通信を試み、そこで通信が確認できるまではBLEによる通信経路を利用するようにする。具体的にはis_uart_established()がFalseを返すようにする。

以下のような感じで、相互に簡単なメッセージを送って、それぞれ受信が確認出来たら接続OKとする。

static void uart_start_connection_cb(uint8_t* data, uint8_t len) {
    if (strcmp("SYN", (char*)data) == 0) {
        uint8_t msg[] = "ACK";
        uart_send(UART_TYPE_ESTABLISH, msg, sizeof(msg));
        is_connection_established = true;
        log_info("UART: syn received, starting uart communication.\n");
    } else if (strcmp("ACK", (char*)data) == 0) {
        is_connection_established = true;
        log_info("UART: ack received, starting uart communication.\n");
    }
}

void uart_start_connection(void) {
    uint8_t msg[] = "SYN";
    uart_send(UART_TYPE_ESTABLISH, msg, sizeof(msg));
}

bool is_uart_established(void) {
    return is_connection_established;
}

おわりに

これでCorneの左右のキーボード入力に関する通信を有線で行うようにすることができた。 安定性は格段に上がったように思う。

でもBMPとしては有線接続をする場合はLPME-IOをI2C接続することが本来想定された使い方なので、 OLEDとかにこだわりがなければそっちの方がずっときれいな実装になるはず。

Cherry corneのテンティング対応など

Corne cherryのテンティング対応と、そのほか細かいアップデート

テンティング

テンティングについてはこちらの方の記事がわかりやすい。 キーボードを入力しやすいように傾きを付けることで、分割キーボードにおいてはこれがあるとさらに入力が楽になる。

Corneにおいては公式にテンティングプレートの設計図と組み立て手順書が提供されているため、 これに準拠した部品さえ確保できれば自分で組み立てることが可能。

テンティングプレート選定

準備が難しいのがテンティングプレート。 試しにwebでアクリルプレートの切り出し費用を見積もってみたが、それなりにいい値段するというのが印象だった。 アクリルプレート加工の知識がないので期待通りの寸法のものが出てくるかわからないし、選択肢としては微妙。

先駆者の方のブログにあるように出来合いのプレートを探すのがよさそう。検索したら海外だが以下二つが見つかった。

アクリルプレートだったら自分でプレート加工を発注するよりはキットを頼んでしまった方が安くて楽。

バックライト等のLEDを光らせるのであればテンティングプレートの材質はアクリルの方がきれいに光ると思われるが、 なんかアクリルの方の商品写真、あまりきれいに撮れていなくてあまり印象が良くないし、 そもそもそんなに光らせることを重視していないので、カーボンの方を選択した。

今確認したら上だけカーボンで下はアクリルという商品もあった。こっちでもよかったかも。

組み立て

届いたものがこれ

これをガイドに従って組み立てて完成。特に難しい部分はなかった。せっかくなのでブログ用にもっと写真撮っておけばよかった。

TRRSケーブルの付け替え

手元にあるTRRSケーブルは直線タイプでCorneに接続すると机の上で場所をとってしまったので、TRRSジャックを90度曲げる器具を購入。

Corneに装着。

これで配線もすっきり。

マグネット吸着USB端子の導入

USBを普通に抜き差しするのではなく、マグネット吸着式で毎回細かい抜き差しが不要になるものを導入。

www.amazon.co.jp/dp/B0C48TZZ84

届いたものがこんな感じ。

ケーブルに触れると頻繁にOSからの認識がなくなる程度には接触が悪く、必要以上にケーブルに触らないように少し気を使う。 しかし使い勝手は悪くなく、ファーム書き換えのためにRESETボタンを押しながらケーブルを抜き差しするのが非常に楽になったので、 それなりに満足はしている。

RGBライトの点灯確認

一部LEDが点灯しなくなっていたので調査。

通常のProMicro利用時はLEDは正しく点灯していたはずだが、BMP換装後、両手とも同じあたり(左手側はFとVの間、右手側はJとMの間)でLEDが点灯しなくなっていた。

Corne cherryで利用しているLEDは、WS2812BおよびSK6812MINI-E。

これらのLEDは発光の色合いや輝度等を信号によって制御できるようになっており、QMKだとRGBLightという機能で管理されている。 マイコンのDINピンから出力される信号により制御されており、そのプロトコルは細かいパラメータが多少異なるものの大体同じ。 LED同士を数珠つなぎすることを念頭に設計されているようで、一つのLEDはDINピンからn個のLED向けの信号を受け取り、最初の一つだけ自身で解釈したのち、残りの(n-1)個分の信号をDOUTからそのまま次のLEDに向けて出力するようになっている。 Corneの基盤上でも、LEDのDIN/DOUTを数珠つなぎにする形で配線されている。

CorneにおいてはWS2812BおよびSK6812MINI-E向けの信号はD3/TX0のピンから出て、まずWS2812Bを一通り数珠繋ぎにし

最後のWS2812BのDOUTがそのまま親指キーのSK6812MINI-EのDINに接続され、以後SK6812MINI-Eが数珠つなぎされている。

今回の問題はFとVの間でLEDが点灯しなくなっているので、この二つの間のDIN/DOUTの配線が被疑箇所。

はんだ付けがBMP換装の際にもげたのではないかと疑って、はんだごてではんだを少しこねてみたが変わらず。ついでにVDDとGNDのはんだも修正してみたが駄目だった。 テスターで導通確認したらOKになっていることは確認できたが、実際にLEDを点灯して確認すると事象は改善していなかった。

正しく導通しているのにLEDが点灯しないとなるとLED本体の故障の可能性があり、その場合LEDの交換が必要にある。 LEDの予備はあるので交換は可能だが、四か所はんだ付けで固定しているパーツからはんだを除去して取り外すのはなかなか大変そう。 ハード交換する前に、ソフトウェア側で何か不備がないか確認してみることに。

その結果、ws2812に関連するドライバにおいてLED数を引数としてとっていることを確認。

https://github.com/sekigon-gonnoc/qmk_firmware/blob/dev/ble_micro_pro/drivers/nrf52/ws2812.c#L20C50-L20C64

ここで適切な値を渡しているかprint文を仕込んで確認したところ、12という値が渡されていた。

12というと、ちょうど今点灯しているLEDの数もちょうど12であるので、これが原因の可能性が非常に高い

この12という数がどこから渡されているのかを調査。この関数の呼び出し元を調べると、rgblight.cから呼び出していて、さらにソースコードをたどって調べるとrgblight_ranges.clipping_num_ledsという変数の値らしい。この変数はデフォルト値がRGBLED_NUMであり、デフォルトだとこれは127にセットされているはずだが…。

と思ったところで、BMPにおいてはconfigファイルで動的にキーマップ等の変数を設定できることを思い出し、ドキュメントを確認。 以下のページにled->numとしてLEDの数を設定する記載があり、実際にconfig.jsonを確認したら12という値が書かれていた。

https://sekigon-gonnoc.github.io/BLE-Micro-Pro/#/edit_config_file

というわけで、結論としてはハードの故障でも何でもなく、ドキュメントにきちんと記載されているパラメータの設定ミスという話だった。 この値の設定を直したところ、LEDはおおむね正しく点灯するようになった。

Twinkleモードへの対応

RGBライトの点灯ついでに一通りモードを確認していたところ、Twinkleモードで全く点灯しないことに気が付いた。

調べてみたところ、どうやら全体的なロジックに問題はないもののLEDを確率で点灯させる計算に誤りがあり、極低確率でしかLEDが点灯しなくなっている模様。

各キーのLEDを点灯させるかどうか判定する処理の部分の処理において、

//void rgblight_effect_twinkle(animation_status_t *anim) {

        } else if (rand() < scale((uint16_t)RAND_MAX * RGBLIGHT_EFFECT_TWINKLE_PROBABILITY, 127 + rgblight_config.val / 2)) {

scale関数で計算される閾値rand()関数で生成した乱数を比較し、値が閾値以下なら点灯させるという動作になっていたが、 nrf52840においてはrand()関数はint型の値を返し、その最大値はRAND_MAX=2147483647(0x7fffffff)だが、その一方でscale関数の返り値はuint8_t型なので最大でも取れる値は255。 そのためLEDが点灯する確率は高くても255 / 2147483647で、だいたい0.0000119%。これではほぼ点灯しない。

この件の問題はrand()の返り値と閾値の型の不一致であり、オリジナルのQMKの方の実装を見たら既に修正済みだった。 その修正をBMP側にそのまま持ってきて適用したら、Twinkleモードが正常に動くことが確認できた。

ついでにプルリクエストを出しておいた。

まとめ

今回、CorneのテンティングおよびTRRS/USBケーブルコネクタ周りの改善、RGBライトへの対応ができた。

RGBライトはTwinkleが明るすぎずなかなかいい感じで気に入っている。

Cherry corne BMP対応

Corne cherryをBMP対応したのでメモ。

BMP対応

BMP(BLE Micro Pro)とは、Pro Microと入れ替えて利用可能なマイコンのこと。 これを利用するとBluetooth Low EnergyによるPCとの接続が可能になる。

https://github.com/sekigon-gonnoc/BLE-Micro-Pro https://sekigon-gonnoc.github.io/BLE-Micro-Pro/#/

Pro Microの置き換え用マイコンだが、通常のQMKファームウェアではなく、作者のせきごん氏が改造を施した独自のQMKファームウェアを利用する必要がある。

https://github.com/sekigon-gonnoc/qmk_firmware

Pro Microはこちらの記事に記載があるように、ATmega32U4というマイコンをベースに作られており、AVRという種類のRISCプロセッサを搭載している(ATmega32U4データシート)。一方、BMPnrf52840をベースに作られていてこちらはARMベースとなる。

QMKの公式ドキュメントではnrf52840はサポートされておらずBluetoothとしてもAVRベースの実装しかサポートされていないようなので、このあたりがせきごん氏のリポジトリの独自実装になっている模様。

BMP導入の今回の目的としては以下。

  • Corneを複数のPC間で接続を切り替えて利用可能にすること。
    • ケーブルを物理的に差し替えるのではなく、BLE(/USB)の切り替えにより接続先を有線接続の変更なしに切り替えたい。
    • Corneは家で使うので電源も含めた完全無線化の必要性はない。給電はUSBでも可。

ついでに、CorneにはOLEDがあるのでそこでBluetoothの接続情報を表示するようにする。

BMPにおける分割型キーボードの左右の接続について

左右の手のデータ通信は、標準構成だとBluetooth通信により行われる。当然左右のキーボードにBMPを搭載する必要がある。

ソースコードを見た感じ、キーボードのマトリックススキャン時にmaster/slaveそれぞれここでデータをプッシュしたのち、master側でここでまとめてデータをポップしている。このpush/popの間にBLEで情報転送をしていると思われる。

有線接続する場合はLPME-IOを使うよう公式ドキュメントで案内されている。

LPME-IOについて

有線接続をしたい場合は、片手側にBMP、もう片方にLPME-IOを使うことで、左右の手の間の通信をBT通信ではなくTRRSケーブル経由のI2C通信で行うことができる。

ただし、Corne cherryのキーボードにおいては左右のキーボードの通信はD2ピンにおける単線の通信しか想定されておらず、しかもせっかくTRRSなのに一つ端子を使っていない。 また、I2C通信はOLEDのピンに接続されている。

そのため、左右のキーボードでI2C通信を行うためには、こちらの方のブログで説明があるように、 I2C通信で使うピンをTRRSジャックに別途接続する必要がある。また、I2C通信をLPME-IOのために使ってしまうので、OLEDは利用不可。

今回キーボードの情報表示にOLEDを使いたいので、残念だがLPME-IOは使わない予定。2つほど買っちゃったけど…。

有線通信について

BMP同士で左右の通信を行うのであれば、マトリックススキャン等キーボード側の対応のほかに、そもそも左右で有線通信する手段が必要になる。

左右のキーボードの有線通信としてはマイコン側で以下のあたりをサポートしている。 この辺りの機能にはDMA機能が付いているので、MPU側でややこしいメモリ転送処理やbit bangingをする必要もない。

  • SPI
  • TWI(I2C)
  • UART

これらの機能については、tmk_core/protocol/nrf/sdk15/apidef.hにおいてBMPAPIとして実装が既に提供されている。

このうち2本の線で通信できそうなのはTWIとUART。TWIはOLED通信で使うので、UARTは利用できそう。 もちろんTRRSケーブルで通信するにはCorne上で追加の配線が必要になるが。

ケーブルの追加配線をして、アプリ側でUARTによる左右の必要な通信を実装すれば、BMPにおいて左右の有線通信ができそうな気がする。 でも現状そこまで左右のBLE通信で不自由していないので、基板上に追加配線をしてまでやるかというと微妙。

電源供給について

回路を見ると、左右のTRRSジャックの間でVCCとGNDが共有されている。 左右をTRRSケーブルで接続し左側だけ電源USBに接続することで、左右両方のキーボードに給電し正常に動作させることができた。

不自由はないのでこれで運用することになりそう。

完全にケーブルなしで電池運用する場合、電池による電源供給が必要になる。電池基盤は遊舎工房で販売されている。

https://shop.yushakobo.jp/products/ble-micro-pro-battery-board

こちらの方のブログだとCorne cherryに BMPと電池基盤を重ねて配置している。 これ以上ないほどにスマート実装案だと思ったが、実際に試してみるとOLED設置スペースと干渉し、電池基盤を挟むとOLEDが基盤に刺さらなくなってしまった。

OLEDのピンソケットをもうひとつ噛ませれば回避できそうだが、どのピンソケットを購入したらいいのかわからないので、しばらくは電池基盤なし運用とする。 基本家で使うので完全無線化するよりはOLEDでキーボードの接続先等の内部情報を確認できた方が良い。

アプリケーションの改造

BMPにおいてはブートローダとアプリケーションの二つのソフトウェアが存在する。

BLE Micro Pro Web ConfiguratorからBMPのセットアップを実施すると、ビルド済みのブートローダとアプリケーションをBMPにインストールすることができる。 ブートローダについてはソースコードは公開されていないため改造できないが、アプリケーションについてはqmk_firmwareをベースに作成されており、 先述のせきごん氏のgithubにて公開されているため、改造が可能。

キーマップ変更をはじめ、ある程度の設定は設定ファイルから書き換えられるので通常はアプリケーションの改造は不要だが、 今回はOLEDにて情報表示ができるようにしたいのでアプリケーションの改造を行う。

アプリケーションの書き込みについて

試しにアプリケーションをビルドし、出来上がったuf2ファイルをBMPに書き込んでみたところ、さっそく動かない問題に直面した。

BMPの動作としては以下2種類があるが(参考: https://bigotor.com/attack25-ble/ )、自分でビルドしたuf2ファイルを書き込む場合は1で起動する。

  1. リセットスイッチを押しながらUSBケーブルを接続する場合
    BMPブートローダが起動し、uf2ファイルを書き込むモードとなる。uf2ファイルを書き込み可能。
  2. 普通にBMPをUSBケーブルで接続した場合
    アプリが起動し、キーマップ等が書き込める状態になる。 PCとの間のシリアルコンソールも有効化され、CLIデバッグ表示を確認できる。

WSL環境でuf2ファイルをビルドし、出来上がったuf2ファイルを1のフォルダにそのままコピーしたところ、1の接続が終了した後そのまま応答しなくなった。 本来であればuf2ファイルが書き込まれたのちファームウェアが再起動し、2のモードに入ることが期待値。

再度リセットスイッチを押しながらUSB接続することで1のモードには入れるので、 ファームとしては問題ないがアプリに何か問題があると思われる。

いろいろ試したところ、WSL環境でビルドしたuf2ファイルを1のフォルダにコピーするところに一つ問題があるようだった。 トラブルが発生するときはWSLのディレクトリ(\\wsl.localhost\配下)から直接1のフォルダ(H:\配下)にコピーしていたが、 そうではなく、一度Windowsの標準ディレクトリ(C:\Users\<user>\Documents\firmware)にuf2ファイルをコピーしてから1のフォルダに格納するようにしたら動いた。

なお、この問題を回避しても、まだある程度の確率(体感3割くらい?)でアプリの書き込みに失敗することがあるが、 リトライすれば書き込み可能なのでその先はあまり調べていない。

アプリケーション実装時のTIPS

以下、アプリケーションを改造しているときに気づいたことをメモ。

APIについて

アプリではキーボードとしての動作をいろいろ定義することができ、OLEDへの情報表示等の処理もここで定義可能。 一方、アドバタイズしたり左右のキーボードで通信したりといったBluetooth関連の低レイヤの機能はブートローダ側で実装されている模様。 ソースコード中でたびたび出てくるBMPAPIについては、以下のようにポインタで定義され、ブートローダの方のアドレスを指していると思われる。 qmk_firmwareソースコード中には存在しない。

// tmk_core/protocol/nrf/sdk15/apidef.h

typedef struct {
    //////DO NOT CHANGE///////
    uint32_t api_version;
    void (*bootloader_jump)(void);
    /////////////////////////
    const char* (*get_bootloader_info)(void);
    bmp_api_app_t        app;
    bmp_api_usb_t        usb;
    bmp_api_ble_t        ble;
    bmp_api_gpio_t       gpio;
    bmp_api_i2c_master_t i2cm;
    bmp_api_i2c_slave_t  i2cs;
    bmp_api_spi_master_t spim;
    bmp_api_ws2812_t     ws2812;
    bmp_api_logger_t     logger;
    bmp_api_web_config_t web_config;
    bmp_api_encoder_t    encoder;
    bmp_api_adc_t        adc;
    bmp_api_uart_t       uart;
    bmp_api_ecs_t        ecs;
    bmp_api_cpu_temp_t   temp;
    bmp_api_spi_slave_t  spis;
} bmp_api_t;

#define BMPAPI ((bmp_api_t*)0xFDE00)

左右の手の判別について

左右の手の接続は電源投入時に自動で行われる。マスターとスレーブの役割については、 BMPの最初のセットアップ時の情報で判定される模様。 BMPをPCに通常接続したときに確認できるCONFIG.JSNファイルの.config.modeにてsplit側なのかmaster側なのかが書いてある。 なお、この値はアプリの書き換えでもリセットされないので、恒久的に持っているものと思われる。

アプリ内で判定したい場合は、configからmodeを確認可能。tmk_core/protocol/nrf/main_master.cでもそのように実装されている。 qmk側のソースコードis_keyboard_master関数がオーバーライド可能な形で実装されているので、keymap.cの中で以下のように実装するのがよさそう。

// keymap.cで追加定義
bool is_keyboard_master(void) {
    return BMPAPI->app.get_config()->mode == SPLIT_MASTER;
}

左右の手でのデータ同期

今回、OLED表示機能を実装するにあたって、Master側の情報をSlave側に転送する必要が出てきた。 具体的には、キー入力があった際にディスプレイの電源をONにするための通知を行ったり、WPM(word per minutes)の情報を転送したり。

BMPbluetooth関連の実装だと、以下の関数が提供されている。

    bmp_error_t (*nus_send_bytes)(uint8_t* buf, uint16_t len);
    bmp_error_t (*set_nus_rcv_cb)(bmp_error_t (*callback)(const uint8_t* dat, uint32_t len));
    bmp_error_t (*set_nus_disconnect_cb)(bmp_error_t (*callback)(void));

nus_send_bytesの関数でSlave側からMaster側にデータ通信ができる。 受信側のset_nus_rcv_cbはすでに使用されていたので、ここに受信側のフックを仕込み、 フックの方をkeymap.cにて実装する形でデータ通信を実現した。

+ __attribute__((weak)) void ble_slave_user_data_rcv_1byte_cb(bmp_user_data_1byte* dat) {}
+
 bmp_error_t nus_rcv_callback(const uint8_t* dat, uint32_t len)
 {
     if (len == sizeof(rgblight_syncinfo_t))
     {
         rgblight_update_sync((rgblight_syncinfo_t*)dat, false);
+    } else if (len == sizeof(bmp_user_data_1byte)){
+        ble_slave_user_data_rcv_1byte_cb((bmp_user_data_1byte*) dat);
     }
     return BMP_OK;
 }

bmp_user_data_1byteはtypeとdataフィールドを持つ全2バイトのデータとした。受信側でtypeにより処理を切り替える。

typedef struct {
    uint8_t type;
    uint8_t data;
} bmp_user_data_1byte;

BLE_CONNECTEDイベント発生時の挙動

ID指定なしでアドバタイズを実施した時に、BLE_CONNECTEDイベント発生時点では実際の接続先の情報がconnection statusとして設定されないという事象を確認した。

bmp@Corne>stat
Host connection:1, target id:3
bmp@Corne>adv 1
<info> app: slaveIdx:0
bmp@Corne>State changed event: BLE_ADVERTISING_START, connection status: 0x0101
State changed event: BLE_DISCONNECTED, connection status: 0x0001
<info> app: Fast advertising with whitelist.
<info> app: Disconnected
State changed event: BLE_CONNECTED, connection status: 0x0101
<info> app: Connected to device 1.

bmp@Corne>adv
bmp@Corne>State changed event: BLE_ADVERTISING_START, connection status: 0x0101
State changed event: BLE_DISCONNECTED, connection status: 0x0001
<info> app: Fast advertising.
<info> app: Disconnected
State changed event: BLE_CONNECTED, connection status: 0x0101        # BLE_CONNECTEDイベントのフックで取得したconnection statusによると接続先は1
<info> app: Connected to device 3.                                   # 実際の接続先は3

connection statusを取得する処理を実装する場合、イベント発生後に少しスリープを入れる必要がある。 実測した感じ1.5秒ほどスリープを噛ませたら更新後のconnection statusが取得できるようになった。

描画性能

画面いっぱいに描画を更新するアニメーションを動かしてみたが、どうやら右下のあたりが完全に描画されていない模様。 試しに画面全体を白黒と明滅させるアニメーションを試してみたところ、左下のあたりが応答していないことが確認できた。

Pro Microでアニメーション描画領域をきちんと試していないのであまりはっきりしたことは言えないが、 少なくともPro Microで同種のアニメーションを実装した時は描画が途中で途切れるということには気づかなかった。 この動画を見る限り、全部で4段ある表示領域のうち、4段目は全く更新されておらず、 3段目も場合によっては最後まで描画されていない。 Pro Microの時に問題に気付かなかったということは、Pro Microは少なくとも4段目の真ん中あたりまでは 安定して描画していたはず。

BMPチップセットが変わったことによる性能変化なのか、 BMP固有の処理が増えたことにより性能に影響が出たのか、 はたまたその他の要因があるのか詳細は不明。

実装としてはアニメーションを全領域再描画するのではなく、動く部分だけに限定して画面を更新するような処理に切り替えることで、正しく最後までアニメーションを表示することができた。

追加調査結果

ソースコードを見ていたら、画面の描写領域が一度にすべてOLEDに転送されていないことに気が付いた。

OLEDで表示する情報はアプリ側とOLED側それぞれのメモリ領域で保持され、アプリ側のメモリ領域に対する更新が順次OLED側のメモリに転送されることで実現される。 アプリ側では画面の領域全体を16(OLED_BLOCK_COUNT)の領域に分割して管理しており、 画面の内容に変更があった場合は、該当領域についてdirtyフラグを立てる。 それにより、OLEDの画面描写において、毎回アプリ領域のすべてのメモリ領域をOLEDに転送するのではなく、dirtyフラグが付いている メモリ領域だけ転送すればよいようにし、処理の効率化を実現している。

しかし、現在の実装だと、1回のoled_render処理に対しdirtyフラグが付いた最初の領域しかOLED側に転送されない。

そのため、すべての領域にdirtyフラグが付き画面全体を再描画するためには、oled_renderが16回呼ばれる必要がある。

しかし、その一方で現在のアニメーションは200msごとに1フレーム描写するようになっており、かつ、BMP側の実装としてMAINTASK_INTERVALは17msに設定されている。この設定だと、1つのフレームを描写している期間においてoled_renderは12回程度しか呼ばれず、常に13-16番目の領域は更新されなくなってしまう。

この事象を改善するためには、先述のようにそもそもdirtyな領域を減らすか、OLED_BLOCK_COUNTを減らしてメモリ領域の更新に必要なoled_renderの回数を減らす必要がある。

diff --git a/drivers/oled/oled_driver.h b/drivers/oled/oled_driver.h
index 13b73ede9d..828a02ca0f 100644
--- a/drivers/oled/oled_driver.h
+++ b/drivers/oled/oled_driver.h
@@ -83,7 +83,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #        define OLED_BLOCK_TYPE uint16_t  // Type to use for segmenting the oled display for smart rendering, use unsigned types only
 #    endif
 #    ifndef OLED_BLOCK_COUNT
-#        define OLED_BLOCK_COUNT (sizeof(OLED_BLOCK_TYPE) * 8)  // 16 (compile time mathed)
+#        define OLED_BLOCK_COUNT (sizeof(OLED_BLOCK_TYPE) * 4)  // 16 (compile time mathed)
 #    endif
 #    ifndef OLED_BLOCK_SIZE
 #        define OLED_BLOCK_SIZE (OLED_MATRIX_SIZE / OLED_BLOCK_COUNT)  // 32 (compile time mathed)

出来上がったもの

QMKにOLEDでアニメーションする例を実装していた方が二人ほどいたので、それらを借りてきて両手側それぞれに実装。 左右でBT関連の情報表示ができるようになった。

https://github.com/nwii/oledbongocat/blob/main/README.md https://github.com/HellSingCoder/qmk_firmware/tree/master/keyboards/sofle/keymaps/helltm

画面は以下のような感じ。動くものができたので満足。

左手側

右手側

新しいデバイスとのペアリング

BLE_CONNECTEDイベント発生時の挙動のところでも書いたが、ID指定なしでアドバタイズしているのに、他のデバイスが勝手に応答することがある。

また、ID指定であってもペアリング未実施の番号の場合、ペアリング済みの別デバイスが応答することがある。 なんならペアリング済みのIDであっても対象のデバイスが応答しないと別デバイスと接続される。とにかく何かと接続する仕様になっている模様。

新規デバイスをペアリングする際は他のデバイスが応答しないように、既存デバイスBluetooth機能は一時的にOFFにしておくのが良い。 ペアリング済みデバイスに接続する場合は、今のところ対象デバイスが真っ先に応答しているのでさしあたって特に不自由は感じていない。

キーマップについて

余談だが、今まで特に何も考えずにLOWER, RAISE, ADJUSTをLayer 1, 2, 3としてキーマップし、LOWERとRAISEレイヤにLayer 3に遷移させるキーコードを配置して使っていたが、以下の記事で少し違うということを知った。 これまでのやり方だと、RAISE->ADJUST->LOWERというレイヤ遷移が正しく認識しない。

https://kbigwheel.hateblo.jp/entry/update-tri-layer

ADJUSTへの遷移条件がLOWERとRAISEのキーを両方押した場合であるならば、キーマップとしてはLayer 1と2のキーのみを定義し、 ADJUSTレイヤへの遷移判定はupdate_tri_layer_stateで計算させるのが正しそう。 以下のようにlayer_state_set_user関数をkeymap.cでオーバーライドしておくと、レイヤ遷移時に自動的にupdate_tri_layer_state関数を呼び出してくれる。

layer_state_t layer_state_set_user(layer_state_t state) {
    layer_state_t new;
    new = update_tri_layer_state(state, _RAISE, _LOWER, _ADJUST);
    dprintf("state: %u, new: %u\n", state, new);
    return new;
}

まとめ

Corne cherryのBMP対応と左右の手でのOLEDでの情報表示ができた。

電池基盤についても優先度が高くないので今回は見送ったが、将来的にはOLEDの下に仕込みたい。何とかならないものか・・・。

Corne cherry v3ビルドログ

はじめに

以前Ergodash miniを作りしばらく使っていたが、もう一台作りたくなってきた。 yokada996.hatenablog.com

キー数はそこまでいらないことに気が付いたので、次は42キーのCorne cherryに挑戦。

購入物品

  • Corne cherry v3
    • 遊舎工房にて販売されているキットを利用。必要なものが全部入りなのでとても楽。
    • ダイオードやPCBソケットなど、細かい品類は予備用なのか必要数よりいくつか多めに入っていた。
    • Corneとしてはcherry(通常版)、chocolate(キースイッチが薄くスリムなもの)、chrry light(cherryからPCBソケット対応やLED対応を外し、構築難易度を下げたもの)の三つがあるらしいが、今回は二回目の作成なので通常版のcherryを選択。
  • キースイッチDurock Black Lotus Linear 2023 Lubed Version
    • Linearで静かな打鍵音だったので購入。
  • キーキャップ
    • 前組んだやつが明るめの色合いだったので今回は暗めを選択。

工具類

今回特に追加で必要な工具類はなかった。 ただし、はんだ吸い取り器が詰まっていたので手入れが必要だった。 きちんとした器具だとそれなりの手順があるらしいが、セット購入した安物なのでピンセットや細いドライバーを口の方から差し込んで詰まっているはんだを取り出した。

ビルド作業

公式ドキュメントこちらの方のビルドログを参考に作業。

パーツ確認

基盤本体

アクリルプレート(ボトムプレート及びOLEDプレート)

ProMicro

ねじ、スペーサー、タクトスイッチ、TRRSジャック、ゴム足

PCBソケットとダイオード

キースイッチ

OLEDモジュール

そのほかTRRSケーブルは手持ちのものを利用(写真なし)

ダイオードのはんだ付け

作業環境

はんだから少し煙が出るため、作業は窓際のスペースを確保。作業用ファンで煙は窓の外に排出する。ちょっと暗いけど何とか作業はできた。

ダイオード

米粒大のダイオードをはんだ付けする。光にあてるとなんとかダイオード上の線が見えるので、それと基盤上の印刷を頼りに向きを確認する。

表面実装については、予備はんだに対してダイオードを横から差し込む方法がやりにくかったので、予備はんだの上からダイオードを軽く押し付けつつはんだを溶かすやり方にした。落ち着いて位置調整ができて、はんだごてを操作するときははんだを溶かすことだけに集中できるからやりやすい。逆作用ピンセットを用いると左手でダイオードを保持しておく力もいらないのでさらに便利。

右手側裏面のダイオードはんだ付け完了。Corne cherry v3においては表面実装部品は基本的に裏面に取り付けるため、左右が一見逆になるので注意。

同様に左手側

Undergrow LEDの取り付け

マスキングテープで仮止め

WS2812B という部品をはんだ付けする。Ergodash miniの時にも利用したのだが、これだけはんだ付け難易度が高い気がする。LED自体の過熱を警戒して十分な予熱できないのと、端子の方も小さいうえ側面のみなので、はんだを載せるのに苦労した。一見うまく乗ったとしても、実際に導通してみると途中で配線が途切れており、何度かやり直した。 写真ではマスキングテープで仮止めしているが、マスキングテープだとしっかりした固定が難しく、そのままはんだ付けしようとするとちょっとずれたりするので、ダイオードの時と同様に予備はんだを盛ってからパーツを取り付けた。

両手ともに完了。

バックライトの取り付け

こちらはUndergrow LEDと違って、はんだ付けするための端子が伸びているパーツなのではんだ付けの難易度は高くない。 はんだを盛りすぎると隣接する端子間ではんだ付けがつながってしまい回路が短絡するので注意。

取付完了。

表側から見ると窓からLEDが顔を出す形になる。

モゲ対策

ProMicroはある程度長く使っていると端子部がモゲることがあるらしいので、そういった悲劇を回避するために事前にエポキシ系接着剤で強化。

接着剤の塗布

端子の穴に接着剤が流れ込まないようにマスキングテープで保護しておいて、接着剤の塗布が終わったら固まる前に剥がす。

その他スルーホール実装パーツの実装

OLED用ピンソケット、TRRSジャックおよびタクトスイッチの取り付け。

PCBの裏表に注意が必要だが、スルーホール実装なので普通に取り付けてはんだ付けするだけ。

ProMicro取り付け

コンスルーの向きに注意しつつ取り付け。ProMicroの場合はProMicro側ははんだ付けが必要。

OLED取り付け。

接続確認

ProMicroにファームを書き込む。ビルドガイドの手順に従い、両手にファームウェアを書き込み、viaでキーレイアウトを書き込む。

キーレイアウトはこんな感じ。

マウスの右クリックをキーに割り当てようとしたが、デフォルトのファームだとマウスキーが有効化されていない模様。 有効化のためにはあとでファームのリビルドが必要になりそう。

ProMicroを基盤に取り付けてPCに接続すると、はんだ付けが不十分だったのでLEDが全部点灯しなかった。

はんだ付けを見直して導通確認ヨシ!

PCBソケット取り付け

PCBソケットを取り付ける。大きな部品なのでこれもあまり難しいことはなかった。

組み立て

各種アクリルプレートの取り付け

トッププレート及びキースイッチの取り付け

キーキャップの取り付け

ひとまずここまでで完成。

おわりに

ここまででCorneのビルド作業は完了。 前回のErgodash miniから、打鍵感に結構な違いが出ている。キースイッチがクリッキーだったものからリニアになったせいか、少しキーが重く感じる。キースイッチのホットスワップが可能になっているので、次はタクタイル系のスイッチを試してみたいところ。また、打鍵感の違いとして、Ergodash miniの方はキーボード自体が柔らかくCorneのほうが硬い印象があり、(Ergodash mimiの方はトッププレートがアクリルだったから?)こちらの方が好み。

追加作業:テンティング

Corneのテンティングについてはこちらの方のブログにて説明がある。 どうやら公式リポジトリにある設計図通りにアクリルプレートを切り出して組み立てればいい模様。

調べてみるとCorneキーボードは海外でも人気なのか、既製品のテンティングプレートが何種類か販売されていた。アクリルカットサービスを自分で注文するより出来合いの品を頼んだ方が安いように見える。

正式なテンティングキットもあるが、今回はすぐにキーボードを使い始めたいので、取り急ぎこちらの方が動画で解説されているミニ三脚によるテンティングを実施。

https://www.youtube.com/watch?v=QC791UY_M6Y&t=418s

マグネット吸着用鉄板をamazonで購入して、

Corne cherryの背面に貼り付け。

Ergodash Miniから三脚を取り外して、

Corneに装着。

机の上に配置してこんな感じ。イスの高さに合わせて角度を調整して完成。

この方式、簡易的なものだからか三脚の安定感がいまいちなので、机の上に配置してから安定してキー入力できる角度を模索する必要がある。 よく使う親指の一番外側、小指側一番下、小指側一番上でキー入力してガタつかないようにするために、三脚の2本の足が手前側にくるような角度にするのが良いと思う。

ねじのゆるみ対策

Corne cherryを机の上に設置して使っていたら、気が付いたらねじが緩んで外れていた。

ねじの締め付け強化のために、amazonでM2のねじとワッシャー、スプリングワッシャーを購入して、

キーボードの裏面のアクリルプレートを取り付けるねじとして利用。

これでねじが緩むことはなくなった。

ErgoDash miniのVIA対応

はじめに

先日作成したErgoDash miniのファームウェアを書き換える話。 yokada996.hatenablog.com

これまでのキーマップ変更にはファームのビルドを伴っていたが、めんどいのでweb経由でできるようにしたい。

(ファームのリビルドを含む旧手順)

  1. QMK Configuratorでキーマップを検討・作成
  2. jsonデータをダウンロード
  3. QMKビルド環境(Ubuntu)にコピー
  4. qmk json2cコマンドでQMK configuratorのjsonファイルからc言語用のキーマップ定義を生成
  5. 4で出力した文字列をkeymap.cにコピペ
  6. ファームウェアをビルド
  7. ファームウェアをビルド環境からWindows環境にコピー
  8. QMK toolkitでファームウェアをキーボードにインストール

VIAというツールがある。

https://docs.keeb.io/via

VIA公式では現時点ではErgoDash miniはサポートしていないらしいが、 一応(miniではない)ErgoDash本体であればErgoDash作者様のほうでVIA向けファームウェアをビルドしてgit上で公開してくださっているし、 サリチル酸氏のブログでもそんなに難しくないと書かれてあるのできっと何とかなる。

ファームウェア作成

公式ドキュメントやブログ記事、gitに格納されている他のキーボードの定義ファイルを参考にファームウェアを作る。作成に関する補足事項を以下に記す。

  • keymap.c
    現状4レイヤで使っていて増やす予定はないので、defaultのキーマップディレクトリからそのままコピペ。
  • rules.mk
    backlightとunderglowを点灯する場合、VIA_ENABLE以外にもそれぞれの有効化設定を入れておく必要あり。入れておかないとRGB_TOGキーやBL_TOGGキーを押しても正しく反応しない。
VIA_ENABLE = yes
BACKLIGHT_ENABLE = yes
RGBLIGHT_ENABLE = yes
  • config.h
    必要な定義は以下二つだけだった。それ以外はビルド時に自動生成される模様。むしろ書いておくとビルド時に多重定義でエラーする。VENDOR_IDは作者様のErgoDash用のjsonファイルから引用。DESCRIPTIONは適当に埋めた。
#define VENDOR_ID      0xFEED
#define DESCRIPTION    ErgoDash mini

設定を用意したらviaキーマップを指定してファームウェアをビルドしておく。

KLEでのキーレイアウト作成

ErgoDash向けのキーレイアウトをもとに改造。実質段数を一つ減らすだけ。お手本があるとやっぱり楽。

QMK configuratorからのレイアウトファイルの移行

QMKのキーレイアウトとKLEで定義したキーレイアウトが異なるため、QMK Configuratorで作成したキーレイアウトを変換する。 以下のようなスクリプトを組んでキー変換をかけてみたが、日本語系キーマップとBL_UPBL_DOWNがうまく変換されなかったので、そのあたりのキーは追加で手動設定した。

import json

qmk_json_file = 'win_home/Downloads/ergodash_mini_tmp.json'
via_json_file = 'win_home/Downloads/ergodash_mini_via.json'

with open(qmk_json_file) as f:
  qmk_data = json.load(f)

via_data = {
  "name": "ErgoDash mini",
  "vendorProductId": 4276969568, # hex: 0xfeed6060
  "macros": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
  "layers": [
  ],
  "encoders": []
}

for i in qmk_data['layers']:
  L0 = i[0:7]
  R0 = i[7:14][::-1]
  L1 = i[14:21]
  R1 = i[21:28][::-1]
  L2 = i[28:35]
  R2 = i[35:42][::-1]
  L3 = i[42:49]
  R3 = i[49:56][::-1]

  new_layout = L0 + L1 + L2 + L3 + R0 + R1 + R2 + R3
  via_data["layers"].append(new_layout)

with  open(via_json_file, "w") as f:
  json.dump(via_data, f)

デフォルトのキーマップとして最初からkeymap.cに書いておく方がよいかもしれない。

VIAでのキーマップ変更

VIAのWebアプリがあるのでこれを利用するのが楽。最近のwebアプリはローカルデバイスにまでアクセスできるので驚いた。

https://usevia.app/

通常のファームウェアインストール手順でキーボードにVIA用ファームウェアをインストールし、ブラウザからAuthorizeを実施。 KLEのキーマップを読み込ませるとErgoDash miniとしてキーボードの形とキー配列を認識するようになる。 認識したらあとはキーマップをマウスで置き換えるなり、jsonファイルで定義をインポートするだけ。

おわりに

これでErogDash miniでもVIAを用いてファームウェアの書き換えなしにキーマップを変更できるようになった。 ブラウザ上でキーマップを変更した瞬間に実際のキーボード上に反映される。「書き込み」とか「Flash」とかそういうボタンはないので使いやすい。

日本語HHKBのキーマップカスタマイズ

はじめに

ここ半年くらいHHKB(日本語配列)のキーマップを試行錯誤したのでその結果の紹介。 Fnキーの場所と、その押下時のキーレイアウトをカスタマイズするとかなり使いやすくなると思う。

通常状態

Fnキーを押下しない状態でのキーマップは以下。

ポイント

  • Fnキーを利用しやすい親指近辺に移動。日本語入力のON/OFFはIME側の設定で変換/無変換キーに割り当ててあるので、変換キーと入れ替え。
  • 半角全角キーは使わないのでEscキーで置換。結局Escキーは左上にあるのであまり使ってない

Fn押下時

Fnキーを押した際のキーマップは以下。

2023/8/6更新版

ポイント

  • HJKLに矢印キーを配置。(viキーバインドと同じ)
  • ホームポジションから遠いいくつかのキーをホームポジション近辺に移動
    • EnterキーとBackspaceキーを親指人差し指で入力できるようにFnキー近辺に配置
    • 右上の\|キーはバックスラッシュつながりで\_のキーの場所に配置
  • HomeとEndもホームポジション上で利用できるように、また覚えやすいように左右横並びになるように配置
    • Print Screenキーはもしかしたら使うことがあるかもしれないのでデフォルトで残しておく
  • 左手側は右手をマウスに乗せた状態で細かいカーソル操作ができるようにキーを配置
    • WASDに矢印キーを配置
    • PgUp/PgDownをWASDの右側に配置
    • Backspace/Enterをコピー&ペーストと同じようなキー配置で利用できるように設置
    • (2023/8/6)他のキーボードとキーマップを合わせて微修正

おわりに

細かいカーソル移動が非常に楽になった。また、BSキーやエンターキー等の配置が遠いキーの扱いも楽になる。 が、物理的にちょっと手を伸ばせばオリジナルのキーがあるので、このあたりは意識して使わないと慣れなそう。