[自作キーボード]QMKファームウェアでカラーLCDに対応する~基本編~

目次

Abstract

  • QMKファームウェアを使ってST7789のカラーLCDに図形を描画してみた
  • 画像やアニメーション, フォント表示などは次回を予定

Introduction

自作キーボード関連でもあまりカラーLCD対応したキーボード見かけないので試してみる。

Method

QMKではQuantum Painterという機能を使うことで、カラーLCDに絵や動画、文字を表示させることが可能とのこと。

QMK Quantum Painterのページに詳細が有るので見ながら実装していく。

0. 使用するディスプレイの選定

現時点でQMK対応しているディスプレイドライバICは全部で7種類あるのでそれぞれ見ていく

  • GC9A01
    • GALAXYCORE社の240x240の円形ディスプレイドライバ
    • 円形ディスプレイはこれしか対応してないみたいなので円形にしたいならこれ一択
    • Aliexpressで400~500円くらい
  • ILI9163/ILI9341/ILI9488
    • ILITEK社のLCDドライバICで解像度別に3種類対応
      • ILI9488はQMKで対応しているLCDの中で最大の320x480の解像度
    • ILI9488がAliexpressで1000円くらい。その他2つはそれより少し安め。
  • SSD1351
    • SOLOMON SYSTECH社のRGB OLEDドライバICで解像度は128x128の正方
    • 唯一のカラーOLEDで、バックライトが不要になるとかコントラスト高いとかありそうだけどお値段高めで解像度低い。。。
    • Aliexpressで2000円弱程度
  • ST7735/ST7789

基本どのLCD/OLEDも国内で入手可能なので、入手性的なものはどれも同じっぽい。円形ディスプレイは今のところ使う予定無いし、OLEDである必要も特に無いので、ILIかSTのどちらかを購入しようと思ったが、昔に買ったWaveshareのST7789のLCDが引き出しに眠っていたのでそれを使う。

1. ファームウェアにQuantum Painterの機能を組み込む

quantum painterのドキュメントに以下の記述があるので、言われた通りに2文をrule.mkに追加して、コンパイルしてみる。

To enable overall Quantum Painter to be built into your firmware, add the following to rules.mk:

今回はST7789を使うの以下を追記

rule.mk

QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += st7789_spi

コンパイル結果

(venv) user@user:~/qmk/qmk_firmware$ qmk compile -kb cepst/rp2040test -km default
Ψ Compiling keymap with gmake --jobs=1 cepst/rp2040test:default

~省略~

Compiling: platforms/chibios/bootloaders/rp2040.c                                                   [OK]
Compiling: platforms/chibios/drivers/spi_master.c                                                  platforms/chibios/drivers/spi_master.c:24:8: error: unknown type name 'SPIConfig'
   24 | static SPIConfig spiConfig = {NULL, 0, 0, 0};
      |        ^~~~~~~~~
platforms/chibios/drivers/spi_master.c:24:31: error: initialization of 'int' from 'void *' makes integer from pointer without a cast [-Werror=int-conversion]
   24 | static SPIConfig spiConfig = {NULL, 0, 0, 0};

~省略~

spi_master.cでSPIConfigという型が未定義というコンパイルエラーが発生した。

2. ファームウェアでSPI機能を有効化する

QMKでSPIの機能が有効になっていない(SPI用のソースが取り込まれてない)ことが問題と思われるので、ドキュメントでSPIを有効にする方法を探してみると以下のページがあった。

SPI Master Driver

“AVR Configuration"と"ChibiOS/ARM Configuration"というセクションが有るが、RP2040はARMなのでARMの方のセクションを読んでみると、ChibiOS/ARM構成でSPIを有効にするためには、halconf.hというファイルに以下のコードを追加する必要があるとのこと。

#define HAL_USE_SPI TRUE
#define SPI_USE_WAIT TRUE
#define SPI_SELECT_MODE SPI_SELECT_MODE_PAD

ただ、qmkのドキュメントを探したが、halconf.hの編集方法やhalconf.hとは何ぞという記載が見つからない。。

ビルドディレクトリ(qmk_firmware\.build\obj_cepst_rp2040test_default)で今回コンパイルしたときのmakeのデバッグ情報ファイルを探すと、spi_master.dで以下のように2種類のhalconf.hが読み込まれるようになっていた。

spi_master.d

// ~省略~
 keyboards/cepst/rp2040test/halconf.h \
 platforms/chibios/boards/common/configs/halconf.h \
// ~省略~

platforms/下の方はデフォルト設定っぽいので、キーボード固有で設定を変えたい場合はkeyboards/下の自分のキーボードフォルダのところにhalconf.hを置けば良いっぽい。info.jsonとかのテンプレートファイルが置かれている場所辺りを探すとhalconf.hのテンプレートファイルを見つけたので※1、それをコピーして自分のキーボードディレクトリにコピーして↑のdefineを追加して、コンパイルしてみる。

※1 qmk_firmware\data\templates\config-overrides\chibios\halconf.h
※2 halconf.h内の#pragma once#include_next <halconf.h>は削除してはいけないので注意。詳細はAppendixへ

追加したhalconf.h (keyboards/cepst/rp2040test/halconf.h)

#pragma once

// #define HAL_USE_DAC TRUE
#define HAL_USE_SPI TRUE                        // <-追記箇所
#define SPI_USE_WAIT TRUE                       // <-追記箇所
#define SPI_SELECT_MODE SPI_SELECT_MODE_PAD     // <-追記箇所

#include_next <halconf.h>

コンパイル結果

~省略~

Compiling: platforms/chibios/bootloaders/rp2040.c                                                   [OK]
Compiling: platforms/chibios/drivers/spi_master.c                                                   [OK]
Archiving: .build/obj_cepst_rp2040test_default/spi_master.o                                         [OK]
Compiling: tmk_core/protocol/chibios/usb_main.c                                                     [OK]

~省略~

Compiling: platforms/chibios/wait.c                                                                 [OK]
Linking: .build/cepst_rp2040test_default.elf                                                        [OK]
Creating UF2 file for deployment: .build/cepst_rp2040test_default.uf2                               [OK]
Copying cepst_rp2040test_default.uf2 to qmk_firmware folder                                         [OK]
(Firmware size check does not yet support RP2040; skipping)

前回コンパイルエラーとなっていたspi_master.cのコンパイルに成功して、ファームウェアファイルの生成にも成功した。

3. 使用LCDについて確認

描画処理書く前に使用するLCDのピンアサインとRaspberry Pi Pico側のPINの対応付けが必要なので確認しておく.今回使うWaveShare 15867は基盤にピンアサイン書いているので楽ちん。Picoのピンとは以下のように割り当てることにする.

参考: Raspberry Pi Picoのピンアサイン

Table.1 WaveShare 15867とRaspberry Pi Picoのピンの対応付け

LCD PIN Name Description Pico PIN
VCC Power (3.3V/5V input) 3V3
GND Ground GND
DIN SPI data input GP7(SPI0 TX)
CLK SPI clock input GP6(SPI0 CLK)
CS Chip selection, low active GP5(SPI0 CSn)
DC Data/Command selection (high for data, low for command) GP8(SPI0 RX)
RST Reset, low active GP9
BL Backlight GP10

LCDの動作確認

とりあえずQMKのファーム使わずにPicoの3V3にVCC/BL、GNDにGNDを接続してLCDのバックライトが付くことを確認。

BL(BackLight)ピンについて

BackLightは3.3Vを供給すればONになるので、↑のようにVCCと一緒にPicoの3V3を供給してあげても良いですが、USB抜かないとOFFできなくなっちゃうのでGPIOで制御するようにしてキーボード側等からON/OFF制御できるようにしたいと思います。

4. LCDへの描画処理を作成する

参考: Quantum Painter Drawing API

↑のページに描画APIの各種説明が有るので、参考にしながらコードを作成してみる。上記ページにはどのファイルに処理書くみたいな記載はなかったが、とりあえずkeymap.cに書いていけば良いっぽい。

4.1 描画処理を記載すべき場所(関数)を探す

Quantum Painterのドキュメント読んでもどこに描画処理を書いていけば良いのか不明だったので、キーボード以外のハードウェア(LED)とかってどこで制御してるかみたいなのを探してみると以下の辺りに記載があった。

Keyboard Initialization Code

For most people, the keyboard_post_init_user function is what you want to call. For instance, this is where you want to set up things for RGB Underglow.

keyboard_post_init_user()は、キーボードの初期化処理の最後に呼ばれる処理みたいで、LEDとかの制御処理もここに書いているようなのでLCDもここで問題なさそう。キー押下で描画変えるとかのときはまた別な処理で書く必要があるとは思うが、初期化処理や画像変化させないのであればここで描画処理しても問題なさそう。

4.2 描画処理を書く

ドキュメントを読むとやらないと行けないのは以下の6つの処理

  • BACKLIGHTピンの制御
  • ① qp.hのインクルード
  • ② デバイスの初期化
  • ③ ディスプレイパネルのON処理
  • ④ ディスプレイのクリア
  • ⑤ 描画関数の呼び出し(線とか四角形とか円とかを作成する関数)
  • ⑥ ディスプレイへの書き込み処理

上記を全部記載したのが以下のコード

config.h

#pragma once

/* SPI Setting */
#define SPI_DRIVER      SPID0
#define SPI_CS_PIN      GP5     // Chip Select
#define SPI_SCK_PIN     GP6     // Clock
#define SPI_MOSI_PIN    GP7     // Master -> Slave
#define SPI_MISO_PIN    GP8     // Master <- Slave
#define SPI_DIVISOR     1
#define SPI_MODE        3

/* LCD setting */
#define LCD_RESET_PIN       GP9
#define LCD_BACKLIGHT_PIN   GP10
#define LCD_HEIGHT          240
#define LCD_WIDTH           240
  • SPI_DRIVER, SPI_CS_PIN, SPI_SCK_PIN, SPI_MOSI_PIN, SPI_MISO_PIN
    • QMKでSPIを使用するときに必要なピン設定。定数名は決まっている※のでピン番号だけ変更する(SPI Master Driver - ChibiOS/ARM Configuration
      • ※SPI_DRIVER, SPI_CS_PIN等がplatform側のソースコードで使用されているため
    • RP2040用に使う場合はGP+整数で指定
  • SPI_DIVISOR, SPI_MODE, LCD_RESET_PIN
    • 詳細は後述
    • ディスプレイデバイスの作成関数で引数に指定する定数
  • LCD_BACKLIGHT_PIN
    • LCDのバックライトピンの定義
  • LCD_HEIGHT, LCD_WIDTH
    • LCDの解像度
    • ディスプレイデバイスの作成関数で引数に指定する定数

keymap.c

#include QMK_KEYBOARD_H
#include <qp.h> // 1. Quantum Painter機能ヘッダの追加

// ディスプレイデバイスの宣言
static painter_device_t lcd;

// HSV形式での色定義
enum HSV
{
    HUE,
    SAT,    // Saturation
    VAL,    // Value
};
const uint8_t black[] = {0,   0,   0};
const uint8_t white[] = {0,   0,   255};
const uint8_t red[]   = {0,   255, 127};
const uint8_t blue[]  = {170, 255, 127};

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT_ortho_2x3(
        KC_A,    KC_B,    KC_C,
        KC_D,    KC_E,    KC_F
    )
};

void keyboard_post_init_user(void) {

    // LCDのBACKLIGHT PINをHIGHにする
    setPinOutput(LCD_BACKLIGHT_PIN);
    writePinHigh(LCD_BACKLIGHT_PIN);

    // 2. LCDデバイスの初期化処理
    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);

    // 3. ディスプレイパネルのON処理
    qp_power(lcd, true);

    // 4. ディスプレイのクリア
    qp_clear(lcd);
    
    // 5. 描画
    qp_rect(lcd, 0, 0, 240, 240, white[HUE], white[SAT], white[VAL], true);
    qp_circle(lcd, 120, 120, 120, black[HUE], black[SAT], black[VAL], true);
    qp_line(lcd, 120, 0, 120, 240, red[HUE], red[SAT], red[VAL]);
    qp_line(lcd, 0, 120, 240, 120, blue[HUE], blue[SAT], blue[VAL]);

    // 6. ディスプレイへの書き込み処理 
    qp_flush(lcd);
}

以下、keymap.cのコードの説明

バックライトピンの制御

  • 参考: GPIO Control
  • Quantum PainterのAPIにバックライトピンを専用制御するようなAPIは無さそうなので、このピンの制御は通常のGPIOの制御処理を使う
  • setPinOutput()で出力用のピンに設定、WritePinHighでHレベルに変更

#include <qp.h>

  • インクルードしてるだけ

painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode)

  • 参考: qp_st7789.h

  • ST7789デバイスの作成処理

  • 引数

    • panel_width, panel_height
      • LCDのサイズを指定する
    • chip_select_pin, dc_pin
      • SPI通信で使うCS(Chip Select), DC(MISO, TX)ピンを指定
    • reset_pin
      • LCDのリセットピンを指定
    • spi_divisor
      • SPIの除数。。と言われても少しピンと来ないがqmkのspiのところに説明があった
        • SPI Master Driver
          • The SPI clock divisor, will be rounded up to the nearest power of two. This number can be calculated by dividing the MCU’s clock speed by the desired SPI clock speed. For example, an MCU running at 8 MHz wanting to talk to an SPI device at 4 MHz would set the divisor to 2.
          • とのことで、SPIのクロックを設定するための除数らしい。
        • RP2040のクロックが125MHzだから, 1:125MHz, 2:62.5MHz, 4:31.25MHz, …
        • LCD表示できないときは調整したりが必要かも。とりあえず1にしておく。
    • spi_mode
      • SPIのモードを指定
        • ST7789について調べるとモード3しか表示できないっぽい?
        • とりあえず3で表示できたので3を設定
  • その他SPI通信に必要なSCK(Clock), MOSI(TX)はこの関数で指定していないが、そちらはconfig.hで設定することになっている

bool qp_init(painter_device_t device, painter_rotation_t rotation)

  • 参考: qp.h
  • ディスプレイデバイスの初期化処理
  • 第2引数のrotationは回転角度を指定

bool qp_power(painter_device_t device, bool power_on)

  • 参考: qp.h
  • ディスプレイをON/OFFするための関数, trueでON, falseでOFF
  • LCDのBACKLIGTHピンのHIGH/LOWとの違いは以下。消費電力が変わると思うので用途に応じて使い分ける感じかな。。
    • qp_power
      • OFFにするとLCDの黒画になるが、バックライトは消えない
    • BACKLIGHTピン
      • LOWにすると黒画になって、バックライトも消える

qp_clear(lcd)

  • 参考: qp.h
  • ディスプレイ画面をクリアする処理
  • 自分が使った感じだとすでに何かが表示されている状態でqp_clear()を使うと、一瞬だけ黒画になる感じになって、すぐに前に表示されていた画面に戻る動作
    • 画面のリフレッシュ的な扱い?自分の環境だけかどうか不明
  • 今回のコードでは無くても良さそうだったが、一応何か表示する前にクリアしておこうという意味で書いておいた

⑤ 描画関数の呼び出し

  • 参考: qp.h
  • 今回使ったのは線、四角形、円の3種類を描く関数を使用
    • ↑のkeymap.cでは正常に描画できたかを確かめるために以下のような描画をしている
      • 白い四角形を240x240でfill, 黒い半径120の円, 青と赤の縦横の中心線
  • 引数の意味はqp.hを参照
    • 色指定はHSV系を指定
      • Hueは0~360, Sat/Valは100%表記のものを255段階に落とし込んだ値を使う(Satが50%なら127みたいな感じ )
    • 座標指定については、LCDドライバスペックの解像度と実際のLCD解像度が不一致でオフセットが必要なケースも有るので注意
      • 今回のケースではLCDドライバ(ST7789)が240x320, LCDの解像度自体は240x240で不一致だが、原点座標は一緒なので問題になっていない
      • 一方、下図のように原点座標がずれているLCDも見たことが有るので、その場合はオフセットが必要になる


Fig.1 オフセットが不要なケース(今回使ったWaveshareのLCD)


Fig.2 オフセットが必要なケース(この場合は、原点座標を(40,52)と考えて描画する必要がある)

bool qp_flush(painter_device_t device)

  • 参考: qp.h
  • 描画API(qp_line,etc)を使っただけではLCDに表示されない
  • 最後にこのflushの関数を叩くことで描画したものがLCDに表示される

コンパイル

上記描画処理を記載した状態でコンパイルを実施。
特にコンパイルエラーは発生しないので、そのまま検証に

Result

Raspberry Pi Picoにコンパイルして生成したファームウェアを書き込んで動作確認を実施。 問題なくLCDに描画された。

Fig.3 検証動画

Conclusion

長々と書きましたが、LCDに簡単な描画をするところまでしか書けなかった。。。次回はフォントの描画を演る予定。。



Appendix

LCDの描画が30秒程度で消える

  • デフォルトの設定だとキー入力が無い状態が30秒続くと黒画になる仕様です
  • タイムアウト時間を変更したい場合は、config.hにQUANTUM_PAINTER_DISPLAY_TIMEOUTの追加が必要です
  • 詳細はQuantum Painter Configurationを参考

↓追加した例(0にするとタイムアウトしなくなります)

// ~省略~
/* Quantum Painter setting */
#define QUANTUM_PAINTER_DISPLAY_TIMEOUT 0

#pragma onceと#include_next

あまり見たことが無い記述だったのでメモ。

  • #pragma onece
  • #include_next
    • ヘッダーファイルをオーバーライドしたいときに使うもので、GNU拡張らしい
    • 参考: https://gcc.gnu.org/onlinedocs/gcc-9.3.0/cpp/Wrapper-Headers.html#index-_0023include_005fnext
    • ↑を見たなんとなくの解釈
      • 同名のヘッダファイルが存在すると、検索で先に見つかるヘッダファイルが優先されて、後に見つかったヘッダファイルの内容は取り込まれない
      • 先に見つかったヘッダファイルから後に見つかったヘッダファイルの内容を取り込みたい場合の方法として、#include_nextがある

halconf.hに#include_nextを使う意味について

今回の例でいうとkeyboards\下とplatforms\下の両方にhalconf.hがある場合、spi_master.dの依存関係を読むにkeyboards\下の方だけ読み込まれて、platforms\下の方のhalconf.hが読み込まれなくなってしまう。

platforms\下のhalconf.hの方の記述を見ると以下のように、ユーザ側のhalconf.hでの定義状況によってデフォルトの定義の値を使うかを決めており、#include_nextによってユーザ側(keyboards\)からデフォルト側platforms\を取り込む前提の記述になっている。なので、ユーザ側でhalconf.hを使う(配置する)ときは必ず#include_next <halconf.h>の記述が必要みたい。

qmk_firmware\platforms\chibios\boards\common\configs\halconf.h

/**
 * @brief   Enables the SPI subsystem.
 */
#if !defined(HAL_USE_SPI) || defined(__DOXYGEN__)
#define HAL_USE_SPI                         FALSE
#endif

試しにkeyboards\側のhalconf.hから#include_next <halconf.h>をコメントアウトしてビルドすると、platforms\側のhalconf.hでincludeされているmcuconf.hが見つからない、halconf.hで定義されている定数情報が無いといったエラーが出てしまう。

なので、ユーザ側でhalconf.hを定義するときは必ず#include_next <halconf.h>をつける必要があるっぽい

keyboard_post_init_user ()が呼ばれるタイミング

Understanding QMK’s Codeとソースコードを読むと以下の感じで、諸々の初期化処理が終わってメインループ処理が呼ばれる直前で呼ばれる関数のようです。

  • qmk_firmware/quantum/main.c
    • main()
      • ここがqmkファームウェアのエントリポイント
      • 以下の順で関数を読んでいる
        • platform_setup()
        • protocol_setup()
        • keyboard_setup()
        • protocol_init()
          • protocol_pre_init()
          • keyboard_init()
            • keyboard_post_init_kb()
              • keyboard_post_init_user()
        • protocol_post_init();
        • while(true) {protocol_task(); housekeeping_task();}
          • メインループ処理

実装例を見るとkeyboard_post_init_kb(), keyboard_post_init_kb()のどちらにも描画処理を書いている例があるので、どちらのほうが良いのだろうと思ってコード見てみましたが、どちらもデフォルトだと何も処理が書かれておらず、ただkeyboard_post_init_kbからkeyboard_post_init_userを呼び出す処理になっているようです。__attribute__((weak))付いてるのでどちらもユーザ側でオーバーライドできるようにもなっているし、どちらに処理書いても問題なさそうですが、ドキュメントの方で通常はkeyboard_post_init_user使うとありますし、今回はkeyboard_post_init_userの方に処理を書いていきました。

qmk_firmware/quantum/keyboard.c

__attribute__((weak)) void keyboard_post_init_user(void) {}

__attribute__((weak)) void keyboard_post_init_kb(void) {
    keyboard_post_init_user();
}