ENG | Zephyr RTOS: Displays
Playing with displays in Zephyr RTOS
So long, dark late autumn evenings are here and with them comes boredom. Thus I returned to experiments with Zephyr RTOS. I forgot how frustrating it can be to write device tree and cryptic book-long compiler error outputs, which can be caused by a single typo or copy-paste error.
Nonetheless, I set a goal to make Sintronix ST7567, Ilitek ILI9341 running to see how much device tree differ. I also want to explore using direct framebuffer writes, which I used with MicroPython to render font I want, then I want to experiment with character frame buffer and LVGL.
This examples required some exploration of zephyr/ dts/bindings/displays directory and `zephyr/drivers
Recap
First recap of useful commands on Linux
1
2
3
4
5
6
7
8
9
10
# Activate virtual environment for west command
source ~/zephyrproject/.venv/bin/activate
# Build project in the current directory (`-p always` for clean rebuild)
west build -p auto -b xiao_rp2040
# Mount virtual usb drive while rp2040 is in boot mode and flash firmware
# To enter boot mode press RESET while holding BOOT button on XIAO RP2040
# or plug in USB cable while holding BOOTSEL on Raspberry Pi Pico
# To list available block device, use `lsblk -o NAME,SIZE,MODEL,LABEL,TRAN`
# which list device name, size, disk model, partition label and type (usb, nvme)
udisksctl mount -b /dev/sdb1 && west flash -r uf2
TODO: RPI pinout
Code examples for Sintronix ST7567 display with XIAO RP2040 board
Overlay file which defines wiring and is HOPEFULLY common for all examples. Typically takes hours to write correctly. Note that integer values are written as <42> and bool values are just defined when true.
Example using raw framebuffer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// https://github.com/zephyrproject-rtos/zephyr/issues/86929
// https://www.seeedstudio.com/XIAO-RP2040-v1-0-p-5026.html
// https://docs.zephyrproject.org/latest/boards/seeed/xiao_rp2040/doc/index.html
// https://github.com/zephyrproject-rtos/zephyr/blob/main/boards/seeed/xiao_rp2040/xiao_rp2040.dts
// zephyr/dts/bindings/display/sitronix,st7565-common.yaml
/ {
chosen {
zephyr,display = &st7567;
zephyr,console = &usb_cdc;
};
mipi_dbi {
compatible = "zephyr,mipi-dbi-spi";
spi-dev = <&xiao_spi>;
dc-gpios = <&gpio0 27 GPIO_ACTIVE_HIGH>; // pin P27/D1
reset-gpios = <&gpio0 28 GPIO_ACTIVE_LOW>; // pin P28/D2
write-only;
#address-cells = <1>;
#size-cells = <0>;
st7567: st7567@0 {
compatible = "sitronix,st7567";
mipi-max-frequency = <20000000>; // 20MHz
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
reg = <0>; // Uses cs-gpios[0]
width = <128>;
height = <64>;
status = "okay";
column-offset = <0>;
line-offset = <0>;
regulation-ratio = <0x3>; // 3bits
//segment-invdir; // flipX=True
com-invdir;
bias;
};
};
};
// XIAO RP2040 SPI_TX P3, SPI_SCK P2
&xiao_spi {
status = "okay";
clock-frequency = <8000000>; // be safe
cs-gpios = <&gpio0 26 GPIO_ACTIVE_LOW>; // PIN D0/P26
};
&zephyr_udc0 {
usb_cdc: usb_cdc_0 {
compatible = "zephyr,cdc-acm-uart";
};
};
CMake file is basically the same for every project, just project name can differ.
1
2
3
4
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(st7567)
target_sources(app PRIVATE src/main.c)
Project file, which defines driver for display, GPIO ports and serial console over USB. Note that contrast parameter seems two weeks old.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CONFIG_GPIO=y
CONFIG_SPI=y
CONFIG_HEAP_MEM_POOL_SIZE=32678
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_LOG=y
CONFIG_DISPLAY=y
CONFIG_ST7567=y
CONFIG_ST7567_DEFAULT_CONTRAST=0
CONFIG_MIPI_DBI=y
CONFIG_MIPI_DBI_SPI=y
CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_DEVICE_PRODUCT="Zephyr Weather Station"
CONFIG_USB_DEVICE_PID=0x0004
CONFIG_USB_CDC_ACM=y
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=y
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
Actual source code, which draws space invader.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
int main()
{
printk("** Starting ST7567 test **\n");
const struct device *display_dev;
display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
printk("Display device not ready!\n");
return -1;
}
// Check display capabilities
struct display_capabilities caps;
display_get_capabilities(display_dev, &caps);
printk("Display size: %dx%d\n", caps.x_resolution, caps.y_resolution);
printk("Current pixel format: %d\n", caps.current_pixel_format);
printk("Supported formats: 0x%x\n", caps.supported_pixel_formats);
printk("Screen info: 0x%x\n", caps.screen_info);
// Note: height must be divisible by 8, pitch=width
struct display_buffer_descriptor buf_desc = {
.buf_size = 11,
.width = 11,
.height = 8,
.pitch = 11
};
static uint8_t buf[11] = "\x70\x18\x7d\xb6\xbc\x3c\xbc\xb6\x7d\x18\x70";
display_write(display_dev, 0, 0, &buf_desc, buf);
display_blanking_off(display_dev);
printk("Done.\n");
return 0;
}
TODO: image and console output
Example using character framebuffer
prj.conf file needs one extra line: CONFIG_CHARACTER_FRAMEBUFFER=y CMakeLists.txt is unchanged.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <zephyr/display/cfb.h>
int main()
{
printk("** Starting ST7567 CFB test **\n");
const struct device *display_dev;
display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
printk("Display device not ready!\n");
return -1;
}
// Initialize CFB
if (display_set_pixel_format(display_dev, PIXEL_FORMAT_MONO10) != 0) {
printk("Failed to set pixel format\n");
}
if (display_set_contrast(display_dev, 0)) {
printk("Failed to set contrast\n");
}
if (cfb_framebuffer_init(display_dev)) {
printk("CFB init failed!\n");
return -1;
}
cfb_framebuffer_clear(display_dev, true);
display_blanking_off(display_dev);
// Print available fonts
uint8_t font_count = cfb_get_numof_fonts(display_dev);
printk("Available fonts: %d\n", font_count);
// Set font (try font 0)
cfb_framebuffer_set_font(display_dev, 0);
// Print hello world
cfb_print(display_dev, "Hello World!", 0, 0);
cfb_framebuffer_finalize(display_dev);
printk("Done.\n");
return 0;
}
Example using LVGL (Lightweight graphic library)
This was not fun at all. I struggled with crashes of very simple example code. Even printk just after main() did not work, but when I placed return 0; before main loop it worked. LLM proposed some code for display updates, but then I noticed that when I looked into display driver source code, it assumed that framebuffer updates allows only full memory pages so y and width must by multiplies of 8. First, I wanted to implement callback, then I found out looking at west -t build menuconfig that there are magic parameters to allow only full display updates and also I can set LVGL to draw into 100% of display, rather than update it in parts, which fit into memory. So I added two lines into prj.conf and it worked:
1
2
CONFIG_LV_Z_VDB_SIZE=100
CONFIG_LV_Z_FULL_REFRESH=y
Now simple C code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <lvgl.h>
int main(void)
{
const struct device *display_dev;
display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
if (!device_is_ready(display_dev)) {
printk("Display not ready!\n");
return -1;
}
// Create UI
lv_obj_t *scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_black(), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_t *label = lv_label_create(scr);
lv_label_set_text(label, "Hello");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_color(label, lv_color_white(), 0);
display_blanking_off(display_dev);
// Main loop
while (1) {
lv_timer_handler();
k_msleep(50);
}
return 0;
}
/* vim: set expandtab shiftwidth=4 softtabstop=4 tabstop=4 : */
Full prj.conf:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
CONFIG_GPIO=y
CONFIG_SPI=y
CONFIG_HEAP_MEM_POOL_SIZE=32678
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_DISPLAY=y
CONFIG_ST7567=y
CONFIG_ST7567_DEFAULT_CONTRAST=0
CONFIG_MIPI_DBI=y
CONFIG_MIPI_DBI_SPI=y
CONFIG_LVGL=y
CONFIG_LV_Z_MEM_POOL_SIZE=16384
CONFIG_LV_COLOR_DEPTH_1=y
CONFIG_LV_Z_VDB_SIZE=100
CONFIG_LV_Z_FULL_REFRESH=y
CONFIG_DISPLAY_LOG_LEVEL_ERR=y
CONFIG_LV_USE_LABEL=y
#CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_DEFAULT_UNSCII_16=y # 16 or 8 monospace
CONFIG_LV_FONT_UNSCII_16=y
CONFIG_LV_USE_LOG=y
CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_DEVICE_PRODUCT="Zephyr Weather Station"
CONFIG_USB_DEVICE_PID=0x0004
CONFIG_USB_CDC_ACM=y
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=y
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_LV_USE_GENERIC_MIPI=y
CMakeLists.txt and xiao_rp2040.overlay files are unchanged.
ILI9341
TO BE DONE