ディスプレイにテキスト表示
深い理由はないが、RTOSとしてのZephyr確認する前の準備として、何かしらのテキストをディスプレイに表示したくなった。本稿はZephyrでディスプレイにテキスト表示する時のメモである。ZephyrでのBuild環境については、その1参照。
テキスト表示環境
まずは、所持するディスプレイ付きマイコンボードおよびサンプルコードをトライ&エラー。結果としては、下記を利用。
- ボード:Wio Terminal
- サンプルコード:samples/subsys/display/lvgl
サンプルコード実行
上記サンプルコードを実行すると、
となり、文字が裏返しとなってしまった。これについて、調べる(AIさんに問い合わせ)と、用いているディスプレイドライバili9341を修正する必要があるとのこと。
<略>
} else if (config->quirks->cmd_set == CMD_SET_2) {
if (orientation == DISPLAY_ORIENTATION_NORMAL) {
/* Do nothing */
} else if (orientation == DISPLAY_ORIENTATION_ROTATED_90) {
//この行がオリジナル data->madctl |= ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY;
data->madctl |= ILI9XXX_MADCTL_MV;
} else if (orientation == DISPLAY_ORIENTATION_ROTATED_180) {
data->madctl |= ILI9XXX_MADCTL_MY | ILI9XXX_MADCTL_MX |
ILI9XXX_MADCTL_ML;
} else if (orientation == DISPLAY_ORIENTATION_ROTATED_270) {
data->madctl |= ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MX;
}
}
<略>
上記のように、ローテーション90度のところを修正。後述するDevice Treeファイルの記述内容とセットで、正しく表示されるようだ。
本題(サンプルプロジェクトの修正)
Zephyrプロジェクト構成
これについては、下記サイトの説明がわかりやすい。
で、今回のプロジェクト構成は下記となっている。
% tree
.
├── CMakeLists.txt
├── prj.conf
├── src
│ └── main.c
└── wio_terminal.overlay
Zephyrの最小構成としては、CMakeLists.txt、src/main.cとなる。ここでは、ディスプレイにテキストを表示するため、prj.confおよびwio_terminal.overlayが存在している。
ファイル
CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(DisplayText)
FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})
ここはお決まりの記法のようだ。先述したサイトの説明にお任せ。
prj.conf
CONFIG_LV_Z_MEM_POOL_SIZE=16384
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_DISPLAY=y
CONFIG_DISPLAY_LOG_LEVEL_ERR=y
CONFIG_LVGL=y
CONFIG_LV_FONT_MONTSERRAT_20=y
サンプルコードにあったprj.confを、トライ&エラーにより最小化したものである。なお、表示するフォントサイズを大きくするため、「CONFIG_LV_FONT_MONTSERRAT_20=y」を定義している。
wio_terminal.overlay
Device Treeファイル。
&ili9341 {
status = "okay";
rotation = <90>;
width = <320>;
height = <320>;
};
ここはAIさんとともに、トライ&エラーした内容。先ほどのdisplay_ili9xxx.cとともに、rotationに90度を、widthおよびheightに適宜値を指定。なお、ファイル名は、westコマンドのオプション「-b」で指定するボード名(ここでは「wio_terminal」)に拡張子「.overlay」をつけたものと一致させる。
src/main.c
サンプルコードをもとに、AIさんに頼りながら作成。動作内容は下記。
- 文字列”Bonjour”が表示
- 1秒ごとにインクリメントするカウンタが表示
- 左ボタン押下でカウンタリセット
- 中央ボタン押下でカウンタが100プラス
- 右ボタン押下でカウンタが1000プラス
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/gpio.h>
#include <lvgl.h>
#include <stdio.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <lvgl_input_device.h>
#define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(app);
static uint32_t count;
static void call_arg_unused(const struct device *port,
struct gpio_callback *cb,
uint32_t pins)
{
ARG_UNUSED(port);
ARG_UNUSED(cb);
ARG_UNUSED(pins);
}
static void button0_isr_callback(const struct device *port,
struct gpio_callback *cb,
uint32_t pins)
{
call_arg_unused(port, cb, pins);
count = 0;
printk("Button0 Pressed\n");
}
static void button1_isr_callback(const struct device *port,
struct gpio_callback *cb,
uint32_t pins)
{
call_arg_unused(port, cb, pins);
count += 100*100;
printk("Button1 Pressed\n");
}
static void button2_isr_callback(const struct device *port,
struct gpio_callback *cb,
uint32_t pins)
{
call_arg_unused(port, cb, pins);
count += 1000*100;
printk("Button2 Pressed\n");
}
enum btn {
BTN0,
BTN1,
BTN2,
BTN_MAX,
};
static struct gpio_callback button0_callback;
static struct gpio_callback button1_callback;
static struct gpio_callback button2_callback;
typedef void (*button_isr_t)(const struct device *port, struct gpio_callback *cb, uint32_t pins);
struct btn_gpio {
struct gpio_dt_spec dts;
struct gpio_callback *cb;
button_isr_t isr_handler;
};
static struct btn_gpio buttons[BTN_MAX] = {
[BTN0] = {
.dts = GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw0), gpios, {0}),
.cb = &button0_callback,
.isr_handler = button0_isr_callback
},
[BTN1] = {
.dts = GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw1), gpios, {0}),
.cb = &button1_callback,
.isr_handler = button1_isr_callback
},
[BTN2] = {
.dts = GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw2), gpios, {0}),
.cb = &button2_callback,
.isr_handler = button2_isr_callback
},
};
static void setup_buttons(void)
{
int err;
for (int i = 0; i < BTN_MAX; i++) {
if (!gpio_is_ready_dt(&buttons[i].dts)) {
LOG_ERR("Button %d is not ready", i);
continue;
}
err = gpio_pin_configure_dt(&buttons[i].dts, GPIO_INPUT);
if (err) {
LOG_ERR("Failed to configure Button %d: %d", i, err);
continue;
}
gpio_init_callback(buttons[i].cb, buttons[i].isr_handler, BIT(buttons[i].dts.pin));
err = gpio_add_callback(buttons[i].dts.port, buttons[i].cb);
if (err) {
LOG_ERR("Failed to add callback for Button %d: %d", i, err);
continue;
}
err = gpio_pin_interrupt_configure_dt(&buttons[i].dts, GPIO_INT_EDGE_TO_ACTIVE);
if (err) {
LOG_ERR("Failed to enable interrupt for Button %d: %d", i, err);
continue;
}
}
}
int main(void)
{
char count_str[11] = {0};
const struct device *display_dev;
lv_obj_t *hello_world_label;
lv_obj_t *count_label;
int ret;
display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
LOG_ERR("Device not ready, aborting test");
return 0;
}
setup_buttons();
LV_FONT_DECLARE(lv_font_montserrat_20);
hello_world_label = lv_label_create(lv_screen_active());
lv_label_set_text(hello_world_label, "Bonjour");
lv_obj_set_style_text_font(hello_world_label, &lv_font_montserrat_20, 0);
lv_obj_align(hello_world_label, LV_ALIGN_CENTER, 0, -30);
count_label = lv_label_create(lv_screen_active());
lv_obj_set_style_text_font(count_label, &lv_font_montserrat_20, 0);
lv_obj_align(count_label, LV_ALIGN_CENTER, 0, 30);
lv_timer_handler();
ret = display_blanking_off(display_dev); // Must
if (ret < 0 && ret != -ENOSYS) {
LOG_ERR("Failed to turn blanking off (error %d)", ret);
return 0;
}
while (1) {
if ((count % 100) == 0U) {
sprintf(count_str, "%d", count/100U);
lv_label_set_text(count_label, count_str);
//printk("Uptime: %u seconds\n", count / 100);
}
lv_timer_handler();
++count;
k_sleep(K_MSEC(10));
}
}
内容的にはサンプルコードとほぼ同じ。補足説明。
- 左ボタン
- GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw0)
- button0_isr_callback():押下時の割り込みルーチン
- 中央ボタン
- GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw1)
- button1_isr_callback():押下時の割り込みルーチン
- 右ボタン
- GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw2)
- button2_isr_callback():押下時の割り込みルーチン
- setup_buttons():ボタン処理初期化
- LV_FONT_DECLARE(lv_font_montserrat_20):20ポイントフォントの適用(prj.conf参照)
- テキスト表示
- lv_label_create():テキストラベル作成
- lv_label_set_text():テキストセット
- lv_obj_set_style_text_font():フォント指定
- lv_obj_align():配置指定
なお、ボタンチャタリングは考慮していない。
Build、書き込み、実行
まずはBuild。
(.venv) name@Ahost display % west build -t menuconfig -b wio_terminal
-- west build: generating a build system
Loading Zephyr default modules (Zephyr base).
<略>
Converted to uf2, output size: 566784, start address: 0x4000
Wrote 566784 bytes to zephyr.uf2
書き込み時には、Wio Terminalの横のスイッチを2回すばやく動かす必要がある。その後、下記コマンドを実行。
(.venv) name@Ahost display % west flash
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner bossac
<略>
Done in 7.058 seconds
Set boot flash true
(.venv) name@Ahost display %
書き込み終了後の実行時の様子。
文字が見える(裏返しではなく)。
シリアルターミナル
ボタンを押した時のログ。
$ screen /dev/tty.usbmodem1101 115200
*** Booting Zephyr OS build v4.4.0-1647-g29d97bdaf61b ***
Button2 Pressed
Button0 Pressed
Button1 Pressed
おまけ
ボタンの位置変更
Device Treeファイルの定義でボタン操作を変更できる。例えば、
/ {
aliases {
sw0 = &user_button_1;
sw1 = &user_button_0;
};
};
&ili9341 {
status = "okay";
rotation = <90>;
width = <320>;
height = <320>;
};
とすると、左ボタン(user_button_0)と中央ボタン(user_button_1)との操作を入れ替えることができる。
Kconfig
% west build -t menuconfig -b wio_terminal
上記コマンドにより、Build時のオプションを選択できる。
今回は未使用であったが、Kconfigに下記内容を記述したとする。
source "Kconfig.zephyr"
mainmenu "Text Sample"
config RESET_COUNTER_SW0
bool "Reset counter when sw0 is pressed"
depends on $(dt_alias_enabled,sw0)
select GPIO
default y
すると、先ほどのコマンドでは、下記が表示される。
Buildオプションとして、上記内容が表示される。また、選択されている場合、ソースコード上で、下記が内容が有効化される(含まれる)。
#ifdef CONFIG_RESET_COUNTER_SW0
終わりに
次は、RTOSとしての内容をトライ。



