jef-sure/dgx-lvgl

0.0.1

Latest
uploaded 1 day ago
LVGL on the displays of DGX: SPI, QSPI, parallel and MIPI-DSI panels as LVGL displays, and the touch panels of DGX-DI as LVGL input devices, with no task and no lock of its own

Readme

# DGX-LVGL

[LVGL](https://lvgl.io) on the displays of [DGX](https://github.com/jef-sure/esp32-component-dgx):
LVGL draws the widgets, DGX puts them on the panel, and
[DGX-DI](https://github.com/jef-sure/esp32-component-dgx-di) tells LVGL where the finger is.

Any 16-bit screen of DGX becomes an LVGL display, on SPI, QSPI, the 8-bit parallel bus or MIPI-DSI. That
includes panels ESP-IDF has no driver for, such as the NV3041A. The component starts no task and takes no
lock: the task that owns the display calls `lv_timer_handler()` in its own loop.

How it works and why it is made this way: [docs/how-it-works-en.md](docs/how-it-works-en.md)
([Russian](docs/how-it-works-ru.md)).

## Contents

- [Installation](#installation)
- [The smallest program](#the-smallest-program)
- Recipes
  - [A touch panel](#a-touch-panel)
  - [Touches that land in the wrong place](#touches-that-land-in-the-wrong-place)
  - [An on-screen keyboard](#an-on-screen-keyboard)
  - [Other tasks and LVGL](#other-tasks-and-lvgl)
  - [DGX drawing inside a widget](#dgx-drawing-inside-a-widget)
  - [A note over the widgets](#a-note-over-the-widgets)
  - [A game pad over the widgets](#a-game-pad-over-the-widgets)
  - [Gestures beside LVGL](#gestures-beside-lvgl)
  - [Turning the screen](#turning-the-screen)
  - [LVGL smaller than the display](#lvgl-smaller-than-the-display)
- [Memory](#memory)
- [When something goes wrong](#when-something-goes-wrong)
- [What it does not do](#what-it-does-not-do)
- [Examples](#examples)
- [Reference](#reference)
- [Tests](#tests)

## Installation

```sh
idf.py add-dependency "jef-sure/dgx-lvgl^0.0.1"
```

This brings DGX, DGX-DI and LVGL 9.3 or later. Turn on the bus and the driver of your panel in menuconfig,
as for any program on DGX; the component turns on `CONFIG_DGX_ENABLE_VSCREEN` itself.

LVGL renders on the stack of the task that calls `lv_timer_handler()`, so give that task room. When it is
the main task:

```
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
```

## The smallest program

```c
#include "bus/dgx_spi_esp32.h"
#include "dgx_lvgl.h"
#include "drivers/ili9341.h"
#include "esp_heap_caps.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

/* LVGL counts its time by this */
static uint32_t ms_now(void)
{
    return (uint32_t)(esp_timer_get_time() / 1000);
}

void app_main(void)
{
    dgx_bus_protocols_t *bus    = dgx_spi_init(SPI2_HOST, SPI_DMA_CH_AUTO, GPIO_NUM_13, GPIO_NUM_12, GPIO_NUM_14,
                                               GPIO_NUM_15, GPIO_NUM_2, 40 * 1000 * 1000, 0);
    dgx_screen_t        *screen = dgx_ili9341_init(bus, GPIO_NUM_NC, GPIO_NUM_21, 16, DgxScreenRGB);

    lv_init();
    lv_tick_set_cb(ms_now);

    /* LVGL renders a band of the screen at a time into this */
    const uint32_t size   = screen->width * screen->height * 2 / 10;
    void          *buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, MALLOC_CAP_DMA);
    dgx_lvgl_display_create(screen, buffer, size);

    lv_label_set_text(lv_label_create(lv_screen_active()), "Hello");

    for (;;) {
        const uint32_t wait = lv_timer_handler();
        vTaskDelay(pdMS_TO_TICKS(wait < 1 ? 1 : wait > 30 ? 30 : wait));
    }
}
```

The task of `app_main` owns the display: it alone calls LVGL or draws on this screen.

The whole program with a touch panel: [examples/hello](examples/hello).

## A touch panel

Make a pointer of DGX-DI on the same screen, hand it to LVGL, and read it in the loop:

```c
static dgx_di_pointer_t pointer;

dgx_di_pointer_init(&pointer, device, flags, screen);
dgx_di_pointer_calibrate(&pointer, &board_calibration); /* when the board has one */
lv_display_t *display = dgx_lvgl_display_create(screen, buffer, size);
dgx_lvgl_pointer_create(display, &pointer);

for (;;) {
    dgx_di_pointer_read(&pointer, lv_tick_get());
    const uint32_t wait = lv_timer_handler();
    vTaskDelay(pdMS_TO_TICKS(wait < 1 ? 1 : wait > 30 ? 30 : wait));
}
```

`device` is a touch panel as DGX-DI sees it, `flags` tell how the panel lies under the display: see the
[README of DGX-DI](https://github.com/jef-sure/esp32-component-dgx-di/blob/main/README.md).

LVGL gets the first finger; its widgets take one. The read stays in the loop, as in any program on DGX-DI,
so its gestures, rectangles and pad work beside LVGL on the same read.

The calibration is the pointer's, so LVGL gets the corrected place, in pixels of the screen as it stands, and
has no calibration of its own to set. It is kept, not copied: it has to outlive the pointer.

To measure one on the board and keep it in NVS: [examples/calibrate](examples/calibrate).
`dgx_di_calibrate_measure()` draws its targets straight on the screen and blocks, so it runs before LVGL
starts.

## Touches that land in the wrong place

A swipe that goes the other way along an axis means the flags are wrong for that axis: `DGX_DI_MIRROR_X`,
`DGX_DI_MIRROR_Y`, `DGX_DI_SWAP_XY`. A calibration carries its own flags and replaces those of
`dgx_di_pointer_init()`, so with a calibration, measure again rather than change the flags.

A GT911 keeps its axes in a configuration of its own, which may change: a calibration that was right may
mirror the touches later. Measure again: [examples/calibrate](examples/calibrate).

## An on-screen keyboard

Use the keyboard of LVGL, with a text area:

```c
lv_obj_t *text     = lv_textarea_create(lv_screen_active());
lv_obj_t *keyboard = lv_keyboard_create(lv_screen_active());
lv_keyboard_set_textarea(keyboard, text);
```

It is a widget like any other, so it needs a touch panel and nothing else. Its OK key sends
`LV_EVENT_READY`. Its letters are in a font of LVGL, and the Montserrat fonts LVGL comes with are Latin:
other alphabets need a font of your own and `lv_keyboard_set_map()`.

The keyboard of DGX-DI, `dgx_di_keyboard.h`, does not go over LVGL: it draws opaque keys straight on the
display and takes a band of it for itself, and LVGL knows nothing of that band and draws over it.

Example: [examples/keyboard](examples/keyboard).

## Other tasks and LVGL

LVGL is not safe for tasks. Other tasks do not call it: they send the owner of the display a message, and
the owner shows it between its calls of LVGL.

```c
typedef struct { int value; } message_t;
static QueueHandle_t messages;

/* in any task */
const message_t message = {.value = measured};
xQueueSend(messages, &message, 0);

/* in the owner's loop */
message_t message;
while (xQueueReceive(messages, &message, 0)) lv_bar_set_value(bar, message.value, LV_ANIM_ON);
const uint32_t wait = lv_timer_handler();
```

Example: [examples/messages](examples/messages).

## DGX drawing inside a widget

A virtual screen of 16 bits keeps its pixels the way LVGL reads `LV_COLOR_FORMAT_RGB565_SWAPPED`, so its
memory is a picture LVGL can show as it lies, with nothing copied. Handwritten text, a morph, a gauge of DGX
go in among the widgets:

```c
dgx_screen_t  *canvas  = dgx_vscreen_init(width, height, 16, DgxScreenRGB);
lv_image_dsc_t picture = {
    .header    = {.magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_RGB565_SWAPPED, .w = width, .h = height, .stride = width * 2},
    .data_size = width * height * 2,
    .data      = ((dgx_vscreen_t *)canvas)->v_array,
};
lv_obj_t *image = lv_image_create(lv_screen_active());
lv_image_set_src(image, &picture);

/* DGX draws, then LVGL is told */
dgx_draw_circle(canvas, 40, 40, 20, color);
lv_obj_invalidate(image);
```

To have LVGL render only what has changed, take the changed part from DGX:

```c
int left, top, w, h;
dgx_screen_progress_up(canvas);
/* draw */
if (dgx_screen_take_dirty(canvas, &left, &top, &w, &h)) {
    lv_area_t area;
    lv_obj_get_coords(image, &area);
    area.x1 += left, area.y1 += top, area.x2 = area.x1 + w - 1, area.y2 = area.y1 + h - 1;
    lv_obj_invalidate_area(image, &area);
}
dgx_screen_progress_down(canvas);
```

Example: [examples/dgx_in_lvgl](examples/dgx_in_lvgl).

## A note over the widgets

The overlay layer of a screen, `dgx_overlay.h`, is mixed into what LVGL sends. When the layer changes, LVGL
renders that part again by itself, so a program only draws into the layer. A small layer costs little
memory:

```c
dgx_screen_t *note = dgx_vscreen_init(150, 30, 8, DgxScreenRGB);
dgx_fill_rectangle(note, 0, 0, 150, 30, 1);
dgx_font_string_utf8_screen(note, 10, 20, "A note", 2, DgxOutputNormal, 1, font, NULL, NULL);
dgx_overlay_set(screen, note, x, y, colors, 192); /* the widgets show through at 1/4 */
/* later */
dgx_overlay_clear(screen);
```

Example: [examples/overlay](examples/overlay).

## A game pad over the widgets

Take the pad between the read and LVGL: the finger it claims does not reach the widgets under it, and a
finger anywhere else is LVGL's.

```c
dgx_di_pad_init(&pad, screen, mm_w, mm_h, dgx_di_pad_classic, DGX_DI_PAD_CLASSIC);
dgx_di_pad_show(&pad, screen, 160);

for (;;) {
    dgx_di_pointer_read(&pointer, lv_tick_get());
    dgx_di_pad_take(&pad, &pointer);
    if (pad.buttons & DGX_DI_PAD_LEFT) lv_obj_set_x(ball, lv_obj_get_x(ball) - 3);
    const uint32_t wait = lv_timer_handler();
    vTaskDelay(pdMS_TO_TICKS(wait < 1 ? 1 : wait > 10 ? 10 : wait));
}
```

A pad takes a byte a pixel of the display twice, for its layer and its mask, so it is for a board with PSRAM.

Example: [examples/pad](examples/pad).

## Gestures beside LVGL

A touch of DGX-DI takes the same first finger LVGL gets:

```c
static dgx_di_touch_t touch;
dgx_di_touch_display(&touch, screen, mm_w, mm_h);

/* in the loop */
int x, y;
dgx_di_pointer_read(&pointer, lv_tick_get());
const bool down = dgx_di_pointer_first(&pointer, &x, &y);
dgx_di_touch_take(&touch, x, y, down, lv_tick_get());
if (touch.edge & DGX_DI_EDGE_TOP) lv_obj_set_y(panel, 0);
if (touch.swipe & DGX_DI_SWIPE_LEFT) lv_screen_load_anim(next, LV_SCREEN_LOAD_ANIM_MOVE_LEFT, 200, 0, false);
touch.edge = touch.swipe = 0;
```

Gestures are measured in millimetres, so they feel the same on any display.

Example: [examples/swipe](examples/swipe).

## Turning the screen

Before `dgx_lvgl_display_create()`, turn the screen of DGX with the orientation call of its driver: the
display takes the size of the screen as it stands. A pointer turns with the screen by itself.

```c
dgx_ili9341_orientation(screen, DgxScreenRightLeft, DgxScreenTopBottom, true); /* 320x240 */
```

To turn it while the program runs, turn the screen and tell LVGL the new size:

```c
dgx_ili9341_orientation(screen, dir_x, dir_y, swap_xy);
lv_display_set_resolution(display, screen->width, screen->height);
```

`lv_display_set_rotation()` is not for this display: LVGL leaves turning the pixels to the driver.

Example: [examples/turn](examples/turn).

## LVGL smaller than the display

Give LVGL a virtual screen, and send what it changes to the display through any transfer of DGX: made
larger, smoothed, turned, framed.

```c
dgx_screen_t *small = dgx_vscreen_init(display->width / 2, display->height / 2, 16, DgxScreenRGB);
dgx_lvgl_display_create(small, buffer, size);

for (;;) {
    int x, y, w, h;
    dgx_screen_progress_up(small);
    const uint32_t wait = lv_timer_handler();
    if (dgx_screen_take_dirty(small, &x, &y, &w, &h)) {
        dgx_vscreen_region_to_screen(display, x * 2, y * 2, small, x, y, w, h, DGX_VSCREEN_SCALE(2));
    }
    dgx_screen_progress_down(small);
    vTaskDelay(pdMS_TO_TICKS(wait < 1 ? 1 : wait > 30 ? 30 : wait));
}
```

Example: [examples/scaled](examples/scaled).

## Memory

- **The buffer** is the only picture memory the component asks for: no virtual screen the size of the display
  is made, so it fits a chip without PSRAM. LVGL renders a band of the screen at a time into it. A tenth of
  the screen is the usual size; a smaller one means more bands, and a row of the screen is the least.
- **Where it lies.** With `MALLOC_CAP_DMA` the pixels are sent where they lie. A buffer anywhere else works
  too: the pixels then go through the buffer of the bus.
- **The pool of LVGL** keeps its widgets: `CONFIG_LV_MEM_SIZE_KILOBYTES`, 64 KB unless you lower it. It is
  static memory, so it is taken whether the program needs it or not.
- **The stack** of the task that renders: see [Installation](#installation).

## When something goes wrong

| What you see | Why, and what to do |
|---|---|
| Colors are wrong in a regular way | The panel's RGB order: `DgxScreenBGR` in the driver's init call. |
| The picture is drawn once and never changes | `lv_tick_set_cb()` was not called, or the loop does not call `lv_timer_handler()`. |
| A reset in `lv_timer_handler()`, stack overflow | The stack of the task: see [Installation](#installation). |
| `dgx_lvgl_display_create()` returns NULL | The screen is not of 16 bits, the buffer does not hold a row, or memory has run out. The log says which. |
| A program hangs in LVGL with a second task running | Only the owner of the display calls LVGL: see [Other tasks and LVGL](#other-tasks-and-lvgl). |
| Swipes go the wrong way | See [Touches that land in the wrong place](#touches-that-land-in-the-wrong-place). |
| A finger is not seen at all | The loop has no `dgx_di_pointer_read()`. |
| A pad presses the widgets under it | `dgx_di_pad_take()` has to come between the read and `lv_timer_handler()`. |
| What DGX drew in an image does not appear | LVGL was not told: `lv_obj_invalidate()`. |
| A picture on the display is half LVGL's, half old | A program drew straight on the screen under LVGL: `lv_obj_invalidate(lv_screen_active())` redraws it. |

## What it does not do

- Only 16 bits a pixel. LVGL renders in the byte order of the wire, which is what a virtual screen of DGX
  keeps.
- LVGL waits for every band to reach the display, so a second buffer would bring nothing and is not taken.
- On MIPI-DSI the bands are copied into the frame buffer. A refresh of LVGL is one batch of DGX, so with
  `DgxDsiTwoFrames` the display shows it whole.
- The keyboard of DGX-DI does not go over LVGL: see [An on-screen keyboard](#an-on-screen-keyboard).

## Examples

All of them build for a CYD, a Guition JC4827W543, a Guition JC1060P470 and a round ST77916 module on an
ESP32-S3: see [examples/README.md](examples/README.md).

| Example | What it shows |
|---|---|
| [hello](examples/hello) | the whole of a program, every step in sight |
| [widgets](examples/widgets) | LVGL's own show of its widgets |
| [benchmark](examples/benchmark) | LVGL's benchmark, the time to render and to send a frame in the log |
| [keyboard](examples/keyboard) | a text area and LVGL's keyboard |
| [messages](examples/messages) | tasks that send the owner of the display what to show |
| [dgx_in_lvgl](examples/dgx_in_lvgl) | handwriting of DGX inside an LVGL widget |
| [overlay](examples/overlay) | a note of DGX over the widgets |
| [pad](examples/pad) | a game pad of DGX-DI over LVGL |
| [swipe](examples/swipe) | gestures of DGX-DI beside LVGL |
| [turn](examples/turn) | the display turned while the program runs |
| [scaled](examples/scaled) | LVGL on a virtual screen, made larger by DGX |
| [calibrate](examples/calibrate) | a calibration measured on the board and kept in NVS |

## Reference

| Call | What it does |
|---|---|
| `lv_display_t *dgx_lvgl_display_create(dgx_screen_t *screen, void *buffer, uint32_t size)` | An LVGL display on a 16-bit screen of DGX. `lv_display_delete()` frees what it took. |
| `lv_indev_t *dgx_lvgl_pointer_create(lv_display_t *display, const dgx_di_pointer_t *pointer)` | An LVGL input device on the first finger of a pointer. `lv_indev_delete()` frees it. |

## Tests

`make` in [test/host](test/host) runs LVGL and DGX on a PC with a recording in place of the display, and
`make all-versions` does it for each LVGL it was checked with. It needs checkouts of DGX and DGX-DI next to
this repository, and takes LVGL from the ESP Component Registry.

## License

MIT, see [LICENSE](LICENSE).

Links

Supports all targets

Maintainer

  • Anton Petrusevich <anton.petrusevich.mobile@gmail.com>
To add this component to your project, run:

idf.py add-dependency "jef-sure/dgx-lvgl^0.0.1"

download archive

Stats

  • Archive size
    Archive size ~ 23.74 KB
  • Downloaded in total
    Downloaded in total 0 times
  • Downloaded this version
    This version: 0 times

Badge

jef-sure/dgx-lvgl version: 0.0.1
|