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.h
や rules.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 Painter と SPI 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_0
、QP_ROTATION_90
、QP_ROTATION_180
、QP_ROTATION_270
の中から選択します。
#define QUANTUM_PAINTER_SUPPORTS_256_PALETTE
256色のカラー画像またはモノクロ画像を扱う時に必要な設定です。もし、65536色の画像を扱う時は QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS
を TRUE
に、16777216色の画像を扱う時は QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS
を TRUE
に設定します。
#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.h | Raspberry pi pico GPIO Pin | LCD Pin |
---|---|---|
LCD_RST_PIN | GP4 | RES |
LCD_DC_PIN | GP6 | DC |
LCD_CS_PIN | GP5 | CS |
LCD_BLK_PIN | GP10 | BLK |
SPI_SCK_PIN | GP2 | SCL |
SPI_MOSI_PIN | GP3 | SDA |
mcuconf.h
参考にしたサイトでは mcuconf.h
を config.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.h
を config.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.h
と icon.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}