QMK Firmware でカラー LCD に画像を表示する方法

前置き

構想中の左手用キーボードでカラー LCD を使うためのテストとして Raspberry pi pico でカラー LCD を使ってみたのですが、使うためには公式リファレンスの Quantum Painter に加えて SPI Master Driver も確認する必要があり、色々苦労しましたので、使い方を忘れないうちに備忘録としてまとめます。

環境

QMK Firmware

1❯ qmk --version
21.1.5

マイコン

Raspberry Pi Pico を使います。

カラーLCD

Tztft-arduino用LCDディスプレイモジュール,ラウンド,rgb,240x240,gc9a01ドライバー,4ワイヤー,spiインターフェイス,240x240, PCB, 1.28インチ - AliExpress 502 を使っています。LCDドライバーが GC9A01 であれば同じ方法で使えると思います。

なお、現在の QMK Firmware が対応している LCD ドライバは、公式リファレンスの “Supported devices” で確認できます。私は丸型のディスプレイを使ってみたかったので、上記の商品を選択しました。

事前準備

ここから LCD に画像を表示するための事前準備について説明します。なお、本記事の執筆時点ではカラー LCD の設定は (keyboard_name).json に書けないため、config.hrules.mk に設定します。

また、カラー LCD にはテキストや図形も描画できますが、私が描画したいのは画像なので、画像を描画するための設定を説明します。

rules.mk

rules.mk にカラー LCD のドライバに合わせた設定を書きます。今回使うドライバは GC9A01 なので、公式リファレンスに従って以下の設定を追加します。

1# rules.mk
2QUANTUM_PAINTER_ENABLE = yes
3QUANTUM_PAINTER_DRIVERS += gc9a01_spi

rules.mk には LCD に表示する画像ファイルの設定も書きますが、その点は後ほど説明します。

config.h

config.h に公式リファレンスの Quantum PainterSPI Master Driver に従って LCD と SPI の設定を追加します。

LCD の設定

LCD の設定は以下のとおりです。大まかな説明はコメントに書いていますので、細かい説明はコードの後に書きます。

 1/* config.h */
 2
 3// マイコンと LCD の通信に使うピンを定義
 4// Raspberry pi pico の SPI0 のピンの近くのピンを適当に選択
 5#define LCD_RST_PIN GP4
 6#define LCD_DC_PIN GP6
 7#define LCD_CS_PIN GP5
 8#define LCD_BLK_PIN GP10
 9
10// マイコンの動作クロックを割り算するための値
11#define LCD_SPI_DIVISOR 2
12
13// 画像を回転させないので回転角度を 0°に設定
14#define LCD_ROTATION QP_ROTATION_0
15
16// 今回の LCD の解像度(240x240)を設定
17#define LCD_HEIGHT 240
18#define LCD_WIDTH 240
19
20// ドライバとディスプレイで原点にズレがないためオフセットは0に設定
21#define LCD_OFFSET_X 0
22#define LCD_OFFSET_Y 0
23
24// 今回の LCD では色を反転させる必要がないのでコメントアウト
25//#define LCD_INVERT_COLOR
26
27// 256色の画像を扱うための設定
28#define QUANTUM_PAINTER_SUPPORTS_256_PALETTE TRUE
29
30// LCD に表示させる画像の数を設定
31#define QUANTUM_PAINTER_NUM_IMAGES 9
32
33// 画面を常時点灯にするためタイムアウト時間を 0 に設定
34#define QUANTUM_PAINTER_DISPLAY_TIMEOUT 0
#define LCD_SPI_DIVISOR 4

マイコンの動作クロックを割り算して SPI デバイスの動作クロックに合わせるための設定のようです。Raspberry pi pico の RP2040 の動作クロックが 125MHz というのは分かるのですが、GC9A01 の動作クロックが分からなかったので、とりあえずデフォルト値の 2 にしています。

#define LCD_ROTATION

画像を回転させる角度を指定します。回転は90°単位で、QP_ROTATION_0QP_ROTATION_90QP_ROTATION_180QP_ROTATION_270 の中から選択します。

#define QUANTUM_PAINTER_SUPPORTS_256_PALETTE

256色のカラー画像またはモノクロ画像を扱う時に必要な設定です。もし、65536色の画像を扱う時は QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORSTRUE に、16777216色の画像を扱う時は QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORSTRUE に設定します。

#define QUANTUM_PAINTER_NUM_IMAGES

LCD に表示させる画像の数を設定するものですが、デフォルト値が 8 なので大抵の場合は設定不要ではないかと思います。今回はテストなのでデフォルト値を越えた数の画像を扱う設定にしています。

#define QUANTUM_PAINTER_DISPLAY_TIMEOUT

デフォルト値の 30000 だと画面が30秒で暗転するので、0 を設定して常時点灯にしています。

SPI の設定

SPI は以下のとおり設定しています。大まかな説明はコメントに書いていますので、細かい説明はコードの後に書きます。

 1/* config.h */
 2
 3// マイコンと LCD の通信に使うピンを定義
 4// Raspberry pi pico の SPI0 のピンから適当に選択
 5#define SPI_SCK_PIN GP2
 6#define SPI_MOSI_PIN GP3
 7
 8// LCD にMISO ピンに当たる端子が無いので `NO_PIN` を設定
 9#define SPI_MISO_PIN NO_PIN
10
11#define SPI_DRIVER SPID0
12#define SPI_MODE 0
#define SPI_MOSI_PIN

この設定には苦労しました。というのも、商品ページでは SPI 通信対応と書いてあるのに、実際の商品に書かれているピンの名称は I2C 通信で使う “SDA” や “SCL” なので、どう対応するかが分かりませんでした。参考にしたサイトで「SCL ⇔ SCK」、「SDA ⇔ TX ⇔ MOSI」という対応が書かれているのを見つけられたので何とか設定できました。

#define SPI_DRIVER

SPI_DRIVER はデフォルトで “SPI2” を使う設定になっていますが、Raspberry pi pico に “SPI2” はありませんので、“SPI0” を使う設定にしています。

#define SPI_MODE

SPI_MODE には 0 - 3 の値が設定可能で、手元の LCD が動いたのは 0, 1, 3 なので、参考にしたサイトと同じ 0 を選択しています。ドライバによっては 3 でないと動かないものもあるみたいです。

ここまでの設定のうち、Raspberry pi pico と LCD の通信にかかるピンの設定を抜粋すると次表のとおりとなります。Raspberry pi pico と LCD を結線するときの参考になります。

config.hRaspberry pi pico GPIO PinLCD Pin
LCD_RST_PINGP4RES
LCD_DC_PINGP6DC
LCD_CS_PINGP5CS
LCD_BLK_PINGP10BLK
SPI_SCK_PINGP2SCL
SPI_MOSI_PINGP3SDA

mcuconf.h

参考にしたサイトでは mcuconf.hconfig.h と同じ場所に作成して以下の設定を書くよう示されていますが、私が試した限りでは mcuconf.h が無くても大丈夫みたいです。なお、以下の設定は参考にしたサイトのコードをそのままコピペしていますので、上の設定を踏まえるなら “SPI1” は “SPI0” になります。

 1/* mcuconf.h */
 2
 3#pragma once
 4
 5#include_next <mcuconf.h>
 6
 7#undef RP_SPI_USE_SPI1
 8#define RP_SPI_USE_SPI1 TRUE
 9
10#undef RP_PWM_USE_PWM3
11#define RP_PWM_USE_PWM3 TRUE

halconf.h

SPI 通信を使うため、halconf.hconfig.h と同じ場所に作成して以下の設定を追加します(公式リファレンスの ChibiOS/ARM Configuration 参照)。

1/* halconf.h */
2
3#define HAL_USE_SPI TRUE
4#define SPI_USE_WAIT TRUE
5#define SPI_SELECT_MODE SPI_SELECT_MODE_PAD

これはマイコンが ARM の場合の設定ですので、マイコンが AVR の場合の設定は公式リファレンスの AVR Configuration で確認してください。

LCD に表示する画像の作成

次は LCD に表示する画像を作成します。大まかな手順は次のとおりです。

  • 画像を保存するディレクトリを作成します。
  • 表示したい画像をディレクトリに保存します
  • 画像のサイズを LCD の解像度に合わせて 240x240 にします
  • qmk painter-convert-graphics コマンドで画像を QMK 対応形式(.qgf)に変換します。
  • rules.mk に変換した画像を使うための設定を追加します。

keyboards/test/images/icon.png を16色の .qgf ファイルに変換して同じディレクトリに保存するコマンドは次のとおりです。なお、256色とか65536色の画像にも変換できますが、試した限りでは、16色で必要充分かなと思います。高画質にするほどファームウェアの容量も大きくなりますので、適宜調整してください。

1qmk painter-convert-graphics -f pal16 -v -i keyboards/test/images/icon.png

このコマンドを実行すると images/ ディレクトリに icon.qgf.hicon.qgf.c が生成されます。画像を変換したら、変換した .qgf ファイルを QMK で使うため rules.mk に設定します。

1# rules.mk
2
3SRC += images/icon.qgf.c

使いたい画像が複数ある場合、上記の手順を必要なだけ繰り返します。

なお、painter-convert-graphics コマンドのオプションは qmk painter-convert-graphics --help で確認できます。

LCD に画像を表示するための設定

これでようやく事前準備ができましたので、実際に LCD に画像を表示する処理を書いていきます。

LCD に画像を表示する処理は keymap.c に書けますが、keymap.c には本来のキーマップの処理だけを書きたいので、test_color_lcd.c ファイルを作成してそこに書くことにしました。なお、test_color_lcd.c というファイル名は、このカラー LCD のテストをするためのキーボード名を便宜的に “test_color_lcd” にしているためです。

画像を表示するコードは以下のとおりです。簡単な説明はコメントに書いていますが、この後で細かい説明を追記します。なお、このコードは、9つの画像をキー操作に応じて切り替えるという動作を実現するためのものです。

 1/* test_color_lcd.c */
 2
 3// LCD に表示するための API を使うためのインクルード
 4#include <qp.h>
 5
 6// LCD に表示する画像のヘッダファイルをインクルードします
 7#include "images/blender.qgf.h"
 8#include "images/clipstudio.qgf.h"
 9#include "images/excel.qgf.h"
10#include "images/fusion360.qgf.h"
11#include "images/illustrator.qgf.h"
12#include "images/kicad.qgf.h"
13#include "images/photoshop.qgf.h"
14#include "images/qmk.qgf.h"
15#include "images/steam.qgf.h"
16
17// コードから LCD を扱うための変数を用意します
18// この後の dip_switch_update_mask_kb() でも使うのでグローバル変数にしています
19painter_device_t lcd;
20
21// コードから LCD に表示する画像を扱うための変数を用意します
22// この後の dip_switch_update_mask_kb() でも使うのでグローバル変数にしています
23painter_image_handle_t blender_logo;
24painter_image_handle_t clipstudio_logo;
25painter_image_handle_t excel_logo;
26painter_image_handle_t fusion360_logo;
27painter_image_handle_t illustrator_logo;
28painter_image_handle_t kicad_logo;
29painter_image_handle_t photoshop_logo;
30painter_image_handle_t qmk_logo;
31painter_image_handle_t steam_logo;
32
33// キーボード初期化の最終処理の段階で LCD を描画する
34void keyboard_post_init_kb(void) {
35  // LCD を定義します
36  lcd = qp_gc9a01_make_spi_device(
37      LCD_HEIGHT,
38      LCD_WIDTH,
39      LCD_CS_PIN,
40      LCD_DC_PIN,
41      LCD_RST_PIN,
42      LCD_SPI_DIVISOR,
43      SPI_MODE
44  );
45
46  // LCD を初期化します
47  qp_init(lcd, LCD_ROTATION);
48
49  // LCD のオフセット位置を指定します
50  qp_set_viewport_offsets(lcd, LCD_OFFSET_X, LCD_OFFSET_Y);
51
52  // LCD をオンにします
53  qp_power(lcd, true);
54
55  // .qgfファイルの画像を変数に格納します
56  blender_logo = qp_load_image_mem(gfx_blender);
57  clipstudio_logo = qp_load_image_mem(gfx_clipstudio);
58  excel_logo = qp_load_image_mem(gfx_excel);
59  fusion360_logo = qp_load_image_mem(gfx_fusion360);
60  illustrator_logo = qp_load_image_mem(gfx_illustrator);
61  kicad_logo = qp_load_image_mem(gfx_kicad);
62  photoshop_logo = qp_load_image_mem(gfx_photoshop);
63  qmk_logo = qp_load_image_mem(gfx_qmk);
64  steam_logo = qp_load_image_mem(gfx_steam);
65
66  // blender_logo を X=0 Y=0 の位置に描画します
67  qp_drawimage(lcd, 0, 0, blender_logo);
68
69  // LCD の表示を更新します
70  qp_flush(lcd);
71}

以下ではもう少し細かい説明を書いていきます。

#include “images/blender.qgf.h”

キーボードが起動した時点では表示しない画像までインクルードしているのは、キー操作に応じて画像を切り替える処理の中で画像を動的に読み込んだら LCD がフリーズしたためです。おそらく、メモリ容量の限界にぶつかったのではないかと思います。QMK の qp_close_image() 関数で読み込んだ画像を解放しながら動的に画像を読み込めばメモリの問題は回避できると思いますが、その処理を書くのが面倒だったので、最初の段階で全ての画像を読み込むようにしました。

keyboard_post_init_kb()

キーボード初期化の最後の段階で LCD の設定をしていますが、初期化の最後の段階で何か処理するのであれば keyboard_post_init_user() に書いてもOKです。私は公式リファレンスが keyboard_post_init_kb に処理を書いていたので、同様にしています。

qp_gc9a01_make_spi_device

LCD を定義する関数で、この関数は GC9A01 用です。公式リファレンスに LCD のドライバ毎にどの関数を使うかが説明されていますので、自分が使うドライバに合った関数を使います。

qp_load_image_mem()

LCD に表示する画像を変数に格納する関数です。引数は、.qgf.c で定義されている画像データの定数です。

qp_flush(lcd)

qp_drawimage(lcd, 0, 0, blender_logo) で画像を描画しても、qp_flush() 関数を実行しないと LCD の表示が更新されません。そのため、qp_drawimage()qp_flush() はセットで使う必要があります。

キー操作に応じて画像を切り替える

上記の設定でキーボード起動後に LCD に画像が表示されるようになりますが、画像1枚を表示するだけでは面白くないので、キーを押す度に画像が切り替わる設定を keymap.c に追加します。上で説明した画像表示の応用なので、処理の説明はコメントを参照してください。

実際の動作は次の動画のとおりです。

  1/* keymap.c */
  2
  3#include QMK_KEYBOARD_H
  4
  5// test_color_lcd.c で定義したグローバル変数を利用します
  6extern painter_device_t lcd;
  7extern painter_image_handle_t blender_logo;
  8extern painter_image_handle_t clipstudio_logo;
  9extern painter_image_handle_t excel_logo;
 10extern painter_image_handle_t fusion360_logo;
 11extern painter_image_handle_t illustrator_logo;
 12extern painter_image_handle_t kicad_logo;
 13extern painter_image_handle_t photoshop_logo;
 14extern painter_image_handle_t qmk_logo;
 15extern painter_image_handle_t steam_logo;
 16
 17// 画像切り替え用のキーコードを定義します
 18enum custom_keycodes {
 19  QWERTY = SAFE_RANGE,
 20  forward,
 21  back
 22};
 23
 24const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
 25  [0] = LAYOUT( forward, back )
 26};
 27
 28// キータイプ毎に実行される process_record_user() に画像切り替え処理を書きます
 29bool process_record_user(uint16_t keycode, keyrecord_t *record) {
 30  // キーを押した回数を保存する変数を静的変数として宣言
 31  static uint16_t counter = 0;
 32  switch (keycode) {
 33    case forward:
 34      // 画像切り替えをループさせるための条件分岐
 35      if (record->event.pressed) {
 36        if (counter < 8) {
 37          counter++;
 38        }
 39        else {
 40          counter = 0;
 41        }
 42        switch (counter) {
 43          case 0:
 44            qp_drawimage(lcd, 0, 0, blender_logo);
 45            qp_flush(lcd);
 46            break;
 47          case 1:
 48            qp_drawimage(lcd, 0, 0, clipstudio_logo);
 49            qp_flush(lcd);
 50            break;
 51          case 2:
 52            qp_drawimage(lcd, 0, 0, excel_logo);
 53            qp_flush(lcd);
 54            break;
 55          case 3:
 56            qp_drawimage(lcd, 0, 0, fusion360_logo);
 57            qp_flush(lcd);
 58            break;
 59          case 4:
 60            qp_drawimage(lcd, 0, 0, illustrator_logo);
 61            qp_flush(lcd);
 62            break;
 63          case 5:
 64            qp_drawimage(lcd, 0, 0, kicad_logo);
 65            qp_flush(lcd);
 66            break;
 67          case 6:
 68            qp_drawimage(lcd, 0, 0, photoshop_logo);
 69            qp_flush(lcd);
 70            break;
 71          case 7:
 72            qp_drawimage(lcd, 0, 0, qmk_logo);
 73            qp_flush(lcd);
 74            break;
 75          case 8:
 76            qp_drawimage(lcd, 0, 0, steam_logo);
 77            qp_flush(lcd);
 78            break;
 79        }
 80      }
 81      break;
 82  case back:
 83      if (record->event.pressed) {
 84        if (counter > 0) {
 85          counter--;
 86        }
 87        else {
 88          counter = 8;
 89        }
 90        switch (counter) {
 91          case 0:
 92            qp_drawimage(lcd, 0, 0, blender_logo);
 93            qp_flush(lcd);
 94            break;
 95          case 1:
 96            qp_drawimage(lcd, 0, 0, clipstudio_logo);
 97            qp_flush(lcd);
 98            break;
 99          case 2:
100            qp_drawimage(lcd, 0, 0, excel_logo);
101            qp_flush(lcd);
102            break;
103          case 3:
104            qp_drawimage(lcd, 0, 0, fusion360_logo);
105            qp_flush(lcd);
106            break;
107          case 4:
108            qp_drawimage(lcd, 0, 0, illustrator_logo);
109            qp_flush(lcd);
110            break;
111          case 5:
112            qp_drawimage(lcd, 0, 0, kicad_logo);
113            qp_flush(lcd);
114            break;
115          case 6:
116            qp_drawimage(lcd, 0, 0, photoshop_logo);
117            qp_flush(lcd);
118            break;
119          case 7:
120            qp_drawimage(lcd, 0, 0, qmk_logo);
121            qp_flush(lcd);
122            break;
123          case 8:
124            qp_drawimage(lcd, 0, 0, steam_logo);
125            qp_flush(lcd);
126            break;
127        }
128      }
129      break;
130  return true;
131}

参考にしたサイト