[自作キーボード]QMKファームウェアでカラーLCDに対応する~キーボードとの連携~

目次

Abstract

  • キーボードの動作に合わせてLCDの表示を変更するサンプルをいくつか作成

Introduction

前回までに画像、動画、フォントの表示が出来るようになったので、キーボードの動作に合わせて表示を変えてみます。

Environment

前回までに使っていた基盤に押しボタンつけただけのキーボードだと何のキーが割当たっているかわかりにくいので、 SU120を使って、Cherry MXスイッチがつけられるものを作成。 ついでにLCDも斜めにおいて見やすいように。

test keyboard

Method

キーボードの動作に応じてLCDの描画処理を切り替えるので、まずはキーイベントをの取得方法について見てみる


キーイベントを取得出来る関数

上記のリンクを読むとQMKでキーイベントを取得可能でユーザ側で使用出来るものは以下がある。
使用用途に応じて使い分ける

bool process_record_user(uint16_t keycode, keyrecord_t *record)

  • キーが押下されて、QMK側でキーイベントが処理される直前に呼ばれる関数

void post_process_record_user(uint16_t keycode, keyrecord_t *record)

  • QMK側でキーイベントが処理された後に呼ばれる関数

引数について

上記の両方の関数とも引数は共通しておりキーコードとキーイベントが取得出来る

引数keycode

参考: Keycodes Overview
上記のQMKのドキュメントに記載のあるKeyがそのまま渡されてくる形。

引数record

keyrecord_t型の構造体でデータで、以下のようなデータが取得出来る

  • 押されたキーマトリクスの行、列
  • キーが押された時間
  • キーイベントの種類
    • キー、エンコーダーとか
  • キーが押されたか、離されたかの情報

qmk_firmware/quantum/action.h

/* Key event container for recording */
typedef struct {
    keyevent_t event;
#ifndef NO_ACTION_TAPPING
    tap_t tap;
#endif
#if defined(COMBO_ENABLE) || defined(REPEAT_KEY_ENABLE)
    uint16_t keycode;
#endif
} keyrecord_t;

qmk_firmware/quantum/keyboard.h

/* key event */
typedef struct {
    keypos_t        key;        // キーマトリクスの位置(行、列)
    uint16_t        time;       // キーが押下された時間
    keyevent_type_t type;       // イベントの種類
    bool            pressed;    // キーが押されたか (true: 押された, false: 離した)
} keyevent_t;

typedef enum keyevent_type_t {
    TICK_EVENT = 0,
    KEY_EVENT = 1,
    ENCODER_CW_EVENT = 2,
    ENCODER_CCW_EVENT = 3,
    COMBO_EVENT = 4
} keyevent_type_t;

/* key matrix position */
typedef struct {
    uint8_t col;
    uint8_t row;
} keypos_t;

使用例

  • 下記のような処理の中にLCDの描画処理を記載していけば、キー押下に応じて描画を変更出来る

↓keymap.c

bool process_record_user(uint16_t keycode, keyrecord_t *record)
{
    bool lRet = true;   // 戻り値をfalseにすると、QMK側でキーコードが処理されなくなる

    // キーコード毎に応じた処理
    switch(keycode)
    {
        case KC_A:
            // Aキーのイベントが発生
            if(record->event.pressed)
            {
                // キーが押されたとき
            }
            else
            {
                // キーが離されたとき
            }
            break;
        case KC_B:
            // Bキーのイベントが発生
            break;
        default:
            break;
    }

    return lRet;
}

【その他】タイマー処理

アニメーション制御やキー押下時間などを取得したいときなどに時間情報を使いたいのでタイマー処理について調べる

qmkでは以下の関数がある

  • 現在の時間を読み取る
    • uint16_t timer_read(void)
    • uint32_t timer_read32(void)
  • lastからの経過時間を取得する
    • uint16_t timer_elapsed(uint16_t last)
    • uint32_t timer_elapsed(uint32_t last)
  • 単位はどちらもms。
  • 無印の16bit版と32bit版があるので用途に応じて。

タイマー自体はキーボードの起動時にkyeboard_init()とかからtimer_init()が呼ばれて勝手にスタートしているよう。
基本的にそのタイマーを止めず、uint16_t start = timer_read()で起動してからの時間を読み取り、timer_elapsed(start)でtimer_read()で取得した時間からの相対経過時刻を読み取るといった使い方みたい。

タイマーにタイムアウト時刻を設定して、タイムアウト時に何かを動作させるみたいな使い方はできないようなので、指定時間経過後に何かをしたいという場合は、qmkのmainループの最後で毎回呼ばれるhousekeeping_task_user()やマトリクススキャン毎に呼ばれるmatrix_scan_user()で、指定時間が経過したかを チェックするという使うとかの工夫が必要そう。


Sample

キーイベントの取り方がわかったので、それらの情報をもとにいくつかサンプルを作ってみる

キーを押下した回数をLCDに表示する

process_record_user()内でstaticのcount変数を作って押された回数をカウント、 その数値をフォントで描画してみる

サンプルコード

#include QMK_KEYBOARD_H
#include <stdio.h>
#include <qp.h>
#include "qff/JetBrainsMono-Regular.qff.h"

typedef struct {
    uint8_t hue;
    uint8_t sat;
    uint8_t val;
} HSL;
static const HSL black = {0,   0,   0};
static const HSL white = {0,   0,   255};
static const HSL red   = {0,   255, 127};
static const HSL blue  = {170, 255, 127};

typedef struct {
    uint16_t x;
    uint16_t y;
} Position;
static const Position pos_origin = {0,0};

static painter_device_t lcd;
static painter_font_handle_t font;

#define STEP_COUNT_DIGITS_MAX 5 + 1

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT_ortho_2x3(
        KC_NO,   KC_NO,   KC_NO, KC_NO, KC_NO,
        KC_Q,    KC_W,    KC_E,  KC_R,  KC_BACKSPACE,
        KC_A,    KC_S,    KC_D,  KC_F,  KC_ENTER
    )
};

void keyboard_post_init_user(void)
{
    // キーボード初期化
    setPinOutput(LCD_BACKLIGHT_PIN);
    writePinHigh(LCD_BACKLIGHT_PIN);

    lcd = qp_st7789_make_spi_device(LCD_HEIGHT, LCD_WIDTH, SPI_CS_PIN, SPI_MISO_PIN, LCD_RESET_PIN, SPI_DIVISOR, SPI_MODE);
    qp_init(lcd, QP_ROTATION_0);
    qp_power(lcd, true);
    qp_clear(lcd);

    // 背景描画
    qp_rect(lcd, pos_origin.x, pos_origin.y, LCD_WIDTH, LCD_HEIGHT, white.hue, white.sat, white.val, true);

    // フォント読み込み、起動時の描画
    font = qp_load_font_mem(font_JetBrainsMono_Regular);
    if(font != NULL)
    {
        Position startPos = {5, 0};
        qp_drawtext_recolor(lcd, startPos.x, startPos.y, font, "count", black.hue, black.sat, black.val, white.hue, white.sat, white.val);
        startPos.y = font->line_height; // 2行目に表示
        qp_drawtext_recolor(lcd, startPos.x, startPos.y, font, "00000", black.hue, black.sat, black.val, white.hue, white.sat, white.val);
    }
    qp_flush(lcd);
}

bool process_record_user(uint16_t keycode, keyrecord_t *record)
{
    bool ret = true;
    static uint16_t count = 0;  // キー押下回数
    static char step_count[STEP_COUNT_DIGITS_MAX];

    // キーが押されたとき
    if(record->event.pressed)
    {
        // 押された回数を文字列に変換、カウンタ文字列を更新
        snprintf(step_count, STEP_COUNT_DIGITS_MAX, "%05d", count);
        qp_drawtext_recolor(lcd, 5, font->line_height, font, step_count, black.hue, black.sat, black.val, white.hue, white.sat, white.val);
        count++;
    }

    qp_flush(lcd);
    return ret;
}


↓実行結果


押下したキーの文字を表示する(履歴機能付き)

  • process_record_user()の引数のkeycodeを、ASCIIコードに変換して、打った文字を表示する
  • 画面上側には打った文字、下側にはこれまで打った文字の履歴を表示するようにする
サンプルコード

#include QMK_KEYBOARD_H
#include <stdio.h>
#include <qp.h>
#include "qff/JetBrainsMono-Regular_72px.qff.h"
#include "qff/JetBrainsMono-Regular_32px.qff.h"

typedef struct
{
    uint8_t hue;
    uint8_t sat;
    uint8_t val;
} HSL;
const HSL black = {0,   0,   0};
const HSL white = {0,   0,   255};
const HSL red   = {0,   255, 127};
const HSL blue  = {170, 255, 127};

typedef struct {
    uint16_t x;
    uint16_t y;
} Position;
static const Position pos_origin = {0,0};
static Position pressPos = {0, 0};
static Position historyPos = {0, 0};

static painter_device_t lcd;
static painter_font_handle_t font;
static painter_font_handle_t font_32px;
static uint16_t font_width;

#define HISTORY_SIZE 12
static char history[HISTORY_SIZE + 1] = "            ";
static uint16_t history_width;

char toAscii(uint16_t);

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT_ortho_2x3(
        KC_NO,   KC_NO,   KC_NO, KC_NO, KC_NO,
        KC_Q,    KC_W,    KC_E,  KC_R,  KC_BACKSPACE,
        KC_A,    KC_S,    KC_D,  KC_F,  KC_SPACE
    )
};

void keyboard_post_init_user(void)
{
    setPinOutput(LCD_BACKLIGHT_PIN);
    writePinHigh(LCD_BACKLIGHT_PIN);

    lcd = qp_st7789_make_spi_device(LCD_HEIGHT, LCD_WIDTH, SPI_CS_PIN, SPI_MISO_PIN, LCD_RESET_PIN, SPI_DIVISOR, SPI_MODE);
    qp_init(lcd, QP_ROTATION_0);
    qp_power(lcd, true);
    qp_clear(lcd);

    qp_rect(lcd, pos_origin.x, pos_origin.y, LCD_WIDTH, LCD_HEIGHT, white.hue, white.sat, white.val, true);

    font = qp_load_font_mem(font_JetBrainsMono_Regular_72px);
    font_32px = qp_load_font_mem(font_JetBrainsMono_Regular_32px);
    if(font != NULL)
    {
        // 押したキーの表示箇所を描画
        font_width = qp_textwidth(font, " ");
        pressPos.x = (LCD_WIDTH / 2) - (font_width / 2 );    // 水平中央揃え
        pressPos.y = (LCD_HEIGHT / 3) - (font->line_height / 2);
        qp_drawtext_recolor(lcd, pressPos.x, pressPos.y, font, " ", white.hue, white.sat, white.val, black.hue, black.sat, black.val);

        // 履歴の表示箇所を描画
        history_width = qp_textwidth(font_32px, history);
        historyPos.x = (LCD_WIDTH / 2) - (history_width / 2);
        historyPos.y = (LCD_HEIGHT * 2 / 3);
        qp_drawtext_recolor(lcd, historyPos.x, historyPos.y, font_32px, history, black.hue, black.sat, black.val, white.hue, white.sat, white.val);

    }
    qp_flush(lcd);
}

bool process_record_user(uint16_t keycode, keyrecord_t *record)
{
    bool lRet = true;
    static uint16_t count = 0;
    static char pressKey[] = " ";

    if(record->event.pressed)
    {
        // QMKのキーコードをASCIIコードに変換
        pressKey[0] = toAscii(keycode);

        // 押したキーを描画
        qp_drawtext_recolor(lcd, pressPos.x, pressPos.y, font, pressKey, white.hue, white.sat, white.val, black.hue, black.sat, black.val);

        // 押されたキーの履歴を描画
        qp_drawtext_recolor(lcd, historyPos.x, historyPos.y, font_32px, history, black.hue, black.sat, black.val, white.hue, white.sat, white.val);

        // 履歴の更新
        for(int i = 0; i < HISTORY_SIZE-1; i++)
        {
            history[i] = history[i+1];
        }
        history[HISTORY_SIZE-1] = pressKey[0];

        count++;
    }
    qp_flush(lcd);
    return lRet;
}

#define ASCII_ALPHA_OFFSET 'a' - KC_A;
#define ASCII_NUM_OFFSET '0' - KC_1;
char toAscii(uint16_t keycode)
{
    char ascii = ' ';

    if( (KC_A <= keycode) && (keycode <= KC_Z)  )
    {
        ascii = keycode + ASCII_ALPHA_OFFSET;
    }
    if( (KC_1 <= keycode) && (keycode <= KC_0) )
    {
        ascii = keycode + ASCII_NUM_OFFSET
    }

    return ascii;
}


↓実行結果


キー押下でLCDに表示する画像を切り替える

無操作時にはgif動画を再生、キー押下時に複数枚の画像を順番に変更して表示するサンプルです。

サンプルコード

#include QMK_KEYBOARD_H
#include <stdio.h>
#include <qp.h>
#include "qgf/sample.qgf.h"
#include "qgf/1.qgf.h"
#include "qgf/2.qgf.h"
#include "qgf/3.qgf.h"
#include "qgf/4.qgf.h"
#include "qgf/5.qgf.h"
#include "qgf/6.qgf.h"
#include "qgf/7.qgf.h"
#include "qgf/8.qgf.h"


typedef struct {
    uint8_t hue;
    uint8_t sat;
    uint8_t val;
} HSL;
static const HSL black = {0,   0,   0};
static const HSL white = {0,   0,   255};
static const HSL red   = {0,   255, 127};
static const HSL blue  = {170, 255, 127};

typedef struct {
    uint16_t x;
    uint16_t y;
} Position;
static Position lcdOrigin = {0,0};

#define WALK_IMAGE_NUM 8
painter_image_handle_t  img_walk[WALK_IMAGE_NUM];

typedef struct {
    painter_image_handle_t  handle;
    deferred_token          token;
    bool                    isActive;
    bool                    isLoad;
} AnimeController;
static AnimeController waitingMotion;

static painter_device_t lcd;

static uint32_t lastPressTime_ms = 0;   // 最後にキー押下した時間
#define KEY_OPERATION_TIMEOUT 3000  // [ms]

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT_ortho_2x3(
        KC_NO,   KC_NO,   KC_NO, KC_NO, KC_NO,
        KC_Q,    KC_W,    KC_E,  KC_R,  KC_BACKSPACE,
        KC_A,    KC_S,    KC_D,  KC_F,  KC_SPACE
    )
};

void keyboard_post_init_user(void)
{
    // LCDの表示準備
    setPinOutput(LCD_BACKLIGHT_PIN);
    writePinHigh(LCD_BACKLIGHT_PIN);
    lcd = qp_st7789_make_spi_device(LCD_HEIGHT, LCD_WIDTH, SPI_CS_PIN, SPI_MISO_PIN, LCD_RESET_PIN, SPI_DIVISOR, SPI_MODE);
    qp_init(lcd, QP_ROTATION_0);
    qp_power(lcd, true);
    qp_clear(lcd);

    // 背景描画
    qp_rect(lcd, lcdOrigin.x, lcdOrigin.y, LCD_WIDTH, LCD_HEIGHT, white.hue, white.sat, white.val, true);

    // 画像と動画の読みおk味
    img_walk[0] = qp_load_image_mem(gfx_1);
    img_walk[1] = qp_load_image_mem(gfx_2);
    img_walk[2] = qp_load_image_mem(gfx_3);
    img_walk[3] = qp_load_image_mem(gfx_4);
    img_walk[4] = qp_load_image_mem(gfx_5);
    img_walk[5] = qp_load_image_mem(gfx_6);
    img_walk[6] = qp_load_image_mem(gfx_7);
    img_walk[7] = qp_load_image_mem(gfx_8);
    waitingMotion.handle = qp_load_image_mem(gfx_sample);

    // アニメーション再生
    waitingMotion.token = qp_animate(lcd, 100, 0, waitingMotion.handle);
    waitingMotion.isActive = true;
    qp_flush(lcd);
}

bool process_record_user(uint16_t keycode, keyrecord_t *record)
{
    bool lRet = true;
    static uint16_t pressCount = 0;

    // 何らかのキー押下
    if(record->event.pressed)
    {
        // アニメーション停止
        qp_stop_animation(waitingMotion.token);
        waitingMotion.isActive = false;

        // 画像を表示(キー押下毎に画像を切り替える)
        qp_drawimage(lcd, 100, 0, img_walk[pressCount % WALK_IMAGE_NUM]);

        // 最後にキーを押した時間、キー押下回数をカウント
        lastPressTime_ms = timer_read32();
        pressCount++;
    }
    return lRet;
}

void housekeeping_task_user(void)
{
    // アニメーション停止中のとき
    if (!waitingMotion.isActive)
    {
        // 無操作時間が一定時間続いたら
        if ( KEY_OPERATION_TIMEOUT < timer_elapsed32(lastPressTime_ms))
        {
            // アニメーションを再生
            waitingMotion.token = qp_animate(lcd, 100, 0, waitingMotion.handle);
            waitingMotion.isActive = true;
        }
    }

}


↓実行結果


Conclusion

キーボード連携でLCDの描画を変更するサンプルを作成してみました。色々な表示が出来るようになって楽しい。。
特に最後のサンプルを作って見て思いましたが、色々作り込めばキーボードのLCDにデスクトップマスコット(伺かみたいなやつ)的なのを表示させるのもできそうですね。

次回は、もう少しLCDで遊ぶか別項目をやる予定。