Przejdź do treści

Lopaka Basics 08: Interfejs dotykowy z LVGL ​

Ekran dotykowy przypominający telefon pokazujący sterowanie światłem Living room z przełącznikiem zasilania, pokrętłem na 70% Brightness, suwakiem Warm do Cool oraz przyciskami Relax i Focus

Wygląda to jak aplikacja na telefon, ale działa na mikrokontrolerze z małym ekranem dotykowym. Obracasz pokrętło palcem, przesuwasz od ciepłego do zimnego, dotykasz przycisku, a pokrętło przesuwa się do nowej wartości.

W tym odcinku zobaczysz, jak framework UI zwany LVGL zamienia mały ekran w interfejs dotykowy i co odróżnia go od bibliotek graficznych z wcześniejszych odcinków. Następnie ułożysz ekran w Lopaka, dodasz kilka linii własnego kodu dla dotyków i uruchomisz go na symulowanym ESP32 z prawdziwym panelem dotykowym.

Co zbudujesz: ekran dotykowy 240×320 sterujący światłem w salonie: pokrętło jasności z dużą liczbą, suwak Warm ↔ Cool, przełącznik zasilania i dwa przyciski scen, Relax i Focus. Czego potrzebujesz: przeglądarki i symulatora online z odcinka 1. Prawdziwa płytka jest opcjonalna: ESP32 i wyświetlacz ILI9341 2,8 cala z pojemnościowym panelem dotykowym FT6206.

Długopis albo zestaw części ​

Biblioteka graficzna rysuje obrazek przycisku ​

Dotąd rysowaliśmy każdy ekran biblioteką graficzną, taką jak U8g2 czy Adafruit GFX. Biblioteka graficzna to długopis: prostokąt tutaj, tekst tam. Przycisk narysowany w ten sposób jest tylko obrazkiem przycisku.

Dotyk też jest twoim problemem. Gdy palec ląduje na ekranie, twój kod musi odczytać współrzędne, sprawdzić, czy mieszczą się wewnątrz prostokąta (x > 124 && x < 224 && y > 274 …), narysować przycisk w wciśniętym wyglądzie i przerysować ekran, wszystko ręcznie.

Framework UI daje ci prawdziwe obiekty ​

LVGL to framework UI. Nie rysujesz przycisku, tworzysz go: lv_button_create(). Od tego momentu przycisk jest obiektem, który zna swój rozmiar, kolory i stan.

LVGL odczytuje dotyk, znajduje obiekt pod palcem, pokazuje go jako wciśnięty i mówi twojemu kodowi, co się stało: „Focus został kliknięty”. Przerysowuje też tylko tę część ekranu, która się zmieniła, a nie cały obraz.

Po lewej biblioteka graficzna: przycisk jest tylko obrazkiem, a kod sprawdza współrzędne i przerysowuje wszystko. Po prawej LVGL: lv_button_create tworzy prawdziwy obiekt, który znajduje się pod palcem, pokazuje się jako wciśnięty, mówi twojemu kodowi CLICKED → on_focus() i przerysowuje tylko siebie

Krok 1: nowy projekt LVGL ​

Kliknij + Nowy projekt i skonfiguruj okno:

  • Platforma: LVGL (v9).
  • Tytuł: „Living Room”.
  • Rozmiar ekranu: włącz Niestandardowy i wpisz W 240, H 320. Ekran stoi pionowo, jak telefon.
  • Kolor tła: #0E1116, ciemny niebieskoczarny.

Kliknij Zapisz. Kolor tła to pierwsza linia wygenerowanego kodu:

cpp
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x0E1116), LV_PART_MAIN);

lv_screen_active() to sam obiekt ekranu, a każdy dodany widżet będzie na nim żył.

Krok 2: dodaj widżety ​

W projekcie LVGL pasek narzędzi zawiera prawdziwe widżety: obok narzędzia zaznaczania i malowania są tekst (Aa), Obszar tekstowy, Panel, Przycisk, Przełącznik, Suwak, Pole wyboru i Łuk. Kliknij narzędzie, a widżet pojawi się na ekranie. Następnie ustaw jego pozycję, rozmiar i kolory w inspektorze po prawej.

Pokrętło ​

Wybierz narzędzie Łuk. Ustaw X 30, Y 52, W 180 i H 180, a następnie Wartość 70. Ustaw Kolor wypełnienia na #2196F3, a Tło na #2A3039.

Edytor Lopaka z paskiem narzędzi widżetów LVGL; warstwa łuku dial jest zaznaczona, a inspektor pokazuje X 30, Y 52, W 180, H 180, Rotate 135, Start 0, End 270, Value 70, Fill color #2196F3 i Back #2A3039

Pokrętło o średnicy 180 pikseli na ekranie o szerokości 240 pikseli pozostawia 30 pikseli po każdej stronie, więc X 30 je wyśrodkowuje. Pozostałe trzy liczby towarzyszą łukowi:

  • Start 0 i End 270 sprawiają, że ścieżka to trzy czwarte koła.
  • Rotate 135 obraca ją tak, aby przerwa była na dole. LVGL liczy kąty zgodnie z ruchem wskazówek zegara od prawej, więc łuk zaczyna się na dole po lewej i kończy na dole po prawej.
  • Wartość 70 to stopień wypełnienia ścieżki, w skali od 0 do 100.

Kolor wypełnienia maluje wypełnioną część, a Tło resztę ścieżki.

Etykiety, przełącznik, suwak i dwa przyciski ​

Dodaj resztę ekranu. Nadaj każdemu widżetowi kolory w inspektorze na bieżąco:

  • Liczba: tekst 70% w kolorze białym (#FFFFFF) wewnątrz pokrętła, na X 104, Y 150.
  • Tytuł: tekst Living room w kolorze białym w lewym górnym rogu.
  • Przełącznik w prawym górnym rogu, 52×28 pikseli, z Kolorem wypełnienia #2196F3 i Tłem #3A404A. Zaznaczony jest włączony: światło zaczyna się jako włączone.
  • Suwak pod pokrętłem, szeroki na 208 pikseli i wysoki na 8, z Kolorem wypełnienia #2196F3.
  • Dwa przyciski wzdłuż dołu, Relax i Focus, każdy 100×36 pikseli. Ustaw R na 17, kolor tekstu na #FFFFFF, a Tło na #2A3039. Promień prawie połowy wysokości zamienia każdy przycisk w pastylkę.
  • Trzy małe etykiety w kolorze szarym (#8A93A0): Brightness pod liczbą, Warm i Cool nad dwoma końcami suwaka.

Każdy widżet pojawia się pośrodku ekranu, gdy go dodasz, a pola X i Y przenoszą go na miejsce.

Krok 3: nazwij warstwy ​

Każdy widżet jest warstwą w panelu Warstwy, a nazwa warstwy staje się nazwą zmiennej w kodzie. Kliknij dwukrotnie warstwę i nadaj sześciu, których będziesz potrzebować w kodzie, krótkie nazwy:

WarstwaNazwaW kodzie
łukdialarc_dial
tekst 70%levellabel_level
przełącznikpowerswitch_power
suwakwarmthslider_warmth
przycisk Relaxrelaxbutton_relax
przycisk Focusfocusbutton_focus

Lopaka umieszcza typ widżetu przed nazwą. Tytuł i szare etykiety zachowują domyślne nazwy; nic się do nich nie odwołuje.

Wszystko jest obiektem ​

LVGL widzi zbudowany ekran jako zestaw obiektów. Wszystko na ekranie jest obiektem, a obiekty żyją w drzewie. Ekran przechowuje pokrętło, liczbę, suwak, przełącznik i przyciski. Każdy przycisk przechowuje własną etykietę, więc oba poruszają się razem.

Ekran Living room obok jego drzewa obiektów: ekran przechowuje arc_dial, label_level, slider_warmth, switch_power, button_relax i button_focus, każdy przycisk przechowuje etykietę, plus cztery kolejne etykiety

Style i stany ​

Każdy obiekt ma style: kolor, promień, czcionkę. Style mogą zależeć od stanu obiektu. Ten sam przycisk Focus wygląda inaczej domyślnie, gdy jest wciśnięty i gdy jest wyłączony, a przełącznik zmienia się, gdy jest zaznaczony. LVGL przełącza się między tymi wyglądami samodzielnie.

Style przycisku Focus: kolor, promień i czcionka oraz jego trzy stany domyślny, wciśnięty i wyłączony; poniżej przełącznik wyłączony i zaznaczony

Zdarzenia ​

Na koniec zdarzenia. Gdy suwak się porusza, wysyła zdarzenie VALUE_CHANGED. Gdy przycisk zostanie dotknięty, wysyła CLICKED. Twoja funkcja działa i decyduje, co się stanie dalej: on_warmth() przyciemnia pokrętło, on_focus() wysyła je na 100%.

Lopaka pisze obiekty i ich style. Zdarzenia są twoje.

Zdarzenia: suwak wysyła VALUE_CHANGED do on_warmth(), które przyciemnia pokrętło; przycisk Focus wysyła CLICKED do on_focus(), które ustawia pokrętło na 100%. Lopaka pisze obiekty i style, ty piszesz zdarzenia

Krok 4: twój kod ​

Co generuje Lopaka ​

Otwórz panel kodu. Każdy widżet to jedno wywołanie tworzenia, po którym następują jego pozycja, rozmiar i kolory:

cpp
// [BEGIN lopaka generated]

lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x0E1116), LV_PART_MAIN);

// arcLvgl: dial
lv_obj_t * arc_dial = lv_arc_create(lv_screen_active());
lv_obj_set_pos(arc_dial, 30, 52);
lv_obj_set_size(arc_dial, 180, 180);
lv_arc_set_rotation(arc_dial, 135);
lv_arc_set_bg_angles(arc_dial, 0, 270);
lv_arc_set_value(arc_dial, 70);
lv_obj_set_style_arc_color(arc_dial, lv_color_hex(0x2196F3), LV_PART_INDICATOR);
lv_obj_set_style_arc_color(arc_dial, lv_color_hex(0x2A3039), LV_PART_MAIN);

// string: level
lv_obj_t * label_level = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_level, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_pos(label_level, 104, 132);
lv_label_set_text(label_level, "70%");

// string: string 3
lv_obj_t * label_string_3 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_3, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_pos(label_string_3, 16, 14);
lv_label_set_text(label_string_3, "Living room");

// switch: power
lv_obj_t * switch_power = lv_switch_create(lv_screen_active());
lv_obj_set_pos(switch_power, 172, 12);
lv_obj_set_size(switch_power, 52, 28);
lv_obj_add_state(switch_power, LV_STATE_CHECKED);
lv_obj_set_style_bg_color(switch_power, lv_color_hex(0x2196F3), LV_PART_INDICATOR | LV_STATE_CHECKED);
lv_obj_set_style_bg_color(switch_power, lv_color_hex(0x3A404A), LV_PART_MAIN);

// slider: warmth
lv_obj_t * slider_warmth = lv_slider_create(lv_screen_active());
lv_slider_set_value(slider_warmth, 50, LV_ANIM_OFF);
lv_obj_set_pos(slider_warmth, 16, 252);
lv_obj_set_size(slider_warmth, 208, 8);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_KNOB);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_MAIN);

// button: relax
lv_obj_t * button_relax = lv_button_create(lv_screen_active());
lv_obj_set_pos(button_relax, 16, 274);
lv_obj_set_size(button_relax, 100, 36);
lv_obj_set_style_radius(button_relax, 17, LV_PART_MAIN);
lv_obj_set_style_bg_color(button_relax, lv_color_hex(0x2A3039), LV_PART_MAIN);
lv_obj_t * button_relax_label = lv_label_create(button_relax);
lv_label_set_text(button_relax_label, "Relax");
lv_obj_center(button_relax_label);

// button: focus
lv_obj_t * button_focus = lv_button_create(lv_screen_active());
lv_obj_set_pos(button_focus, 124, 274);
lv_obj_set_size(button_focus, 100, 36);
lv_obj_set_style_radius(button_focus, 17, LV_PART_MAIN);
lv_obj_set_style_bg_color(button_focus, lv_color_hex(0x2A3039), LV_PART_MAIN);
lv_obj_t * button_focus_label = lv_label_create(button_focus);
lv_label_set_text(button_focus_label, "Focus");
lv_obj_center(button_focus_label);

// string: string 8
lv_obj_t * label_string_8 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_8, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_8, 84, 188);
lv_label_set_text(label_string_8, "Brightness");

// string: string 9
lv_obj_t * label_string_9 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_9, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_9, 16, 226);
lv_label_set_text(label_string_9, "Warm");

// string: string 10
lv_obj_t * label_string_10 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_10, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_10, 194, 226);
lv_label_set_text(label_string_10, "Cool");
// [END lopaka generated]

Jest to kod przy wyłączonej opcji Funkcja opakowująca, jak będzie potrzebne za chwilę. Kilka szczegółów w nim:

  • Części. Łuk ma ścieżkę (LV_PART_MAIN) i część wypełnioną (LV_PART_INDICATOR); suwak i przełącznik mają też gałkę (LV_PART_KNOB). Każde wywołanie stylu nazywa część, którą maluje. Kolor przełącznika dotyczy LV_PART_INDICATOR | LV_STATE_CHECKED, części wypełnionej, gdy przełącznik jest włączony: styl dla stanu.
  • Przycisk i jego etykieta. lv_label_create(button_relax) tworzy etykietę wewnątrz przycisku, a nie na ekranie, a lv_obj_center() utrzymuje ją pośrodku. To drzewo obiektów z wyjaśnienia, w kodzie.
  • Dlaczego liczba mówi 132. lv_obj_set_pos() umieszcza obiekt według jego lewego górnego rogu. W inspektorze Lopaka Y tekstu to dół jego linii, jak na każdej platformie. Linia tekstu ma 18 pikseli wysokości, więc Y 150 w inspektorze staje się 132 w kodzie.
  • Domyślne pozostają pominięte. Lopaka pomija style pasujące do domyślnych LVGL. Dlatego przyciski nie mają linii dla swojego białego tekstu: biały jest już domyślnym kolorem tekstu przycisku.

Wyłącz funkcję opakowującą ​

Przy włączonej opcji Funkcja opakowująca Lopaka opakowuje kod w void drawScreen_1(void) { … }. Wyłącz ją: wtedy wygenerowane linie znajdują się wewnątrz funkcji, którą sam piszesz, z twoim kodem powyżej i poniżej nich. Lopaka przechowuje to, co wpiszesz poza znacznikami BEGIN i END, i aktualizuje tylko część między nimi, gdy zmienisz projekt.

Twój kod nad znacznikami ​

Nad // [BEGIN lopaka generated] wpisz konfigurację wyświetlacza i kilka małych funkcji:

cpp
#include "touch_display.h"

lv_obj_t *dial, *level, *warmth;

void show_level(void *obj, int32_t v) {
  lv_arc_set_value(dial, v);
  lv_label_set_text_fmt(level, "%d%%", v);
}

void tint(int w) {
  lv_color_t warm = lv_color_hex(0xFF8A3D), white = lv_color_hex(0xFFF1DC), cool = lv_color_hex(0x4FC3F7);
  lv_color_t c = w < 50 ? lv_color_mix(white, warm, w * 255 / 50)
                        : lv_color_mix(cool, white, (w - 50) * 255 / 50);
  lv_obj_set_style_arc_color(dial, c, LV_PART_INDICATOR);
  lv_obj_set_style_bg_color(dial, c, LV_PART_KNOB);
}

void set_warmth(void *obj, int32_t w) {
  lv_slider_set_value(warmth, w, LV_ANIM_OFF);
  tint(w);
}

void scene(int to_level, int to_warmth) {
  lv_anim_t a;
  lv_anim_init(&a);
  lv_anim_set_duration(&a, 700);
  lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
  lv_anim_set_exec_cb(&a, show_level);
  lv_anim_set_values(&a, lv_arc_get_value(dial), to_level);
  lv_anim_start(&a);
  lv_anim_set_exec_cb(&a, set_warmth);
  lv_anim_set_values(&a, lv_slider_get_value(warmth), to_warmth);
  lv_anim_start(&a);
}

void on_dial(lv_event_t *e) { show_level(NULL, lv_arc_get_value(dial)); }
void on_warmth(lv_event_t *e) { tint(lv_slider_get_value(warmth)); }
void on_relax(lv_event_t *e) { scene(30, 10); }
void on_focus(lv_event_t *e) { scene(100, 90); }

void on_power(lv_event_t *e) {
  lv_obj_t *sw = lv_event_get_target_obj(e);
  bool on = lv_obj_has_state(sw, LV_STATE_CHECKED);
  lv_obj_t *scr = lv_screen_active();
  for (uint32_t i = 0; i < lv_obj_get_child_count(scr); i++) {
    lv_obj_t *o = lv_obj_get_child(scr, i);
    if (o == sw) continue;
    lv_obj_set_state(o, LV_STATE_DISABLED, !on);
    lv_obj_set_style_opa(o, on ? LV_OPA_COVER : LV_OPA_30, 0);
  }
}

void build_ui() {
  • show_level() przesuwa pokrętło i zapisuje liczbę: %d%% wypisuje wartość, po której następuje znak procenta.
  • tint() zamienia pozycję suwaka, od 0 do 100, w kolor: ciepły pomarańczowy na 0, ciepła biel na 50, chłodny niebieski na 100. lv_color_mix() miesza dwa kolory; jego ostatni argument, od 0 do 255, mówi, ile pierwszego koloru wziąć.
  • scene() animuje pokrętło i suwak do nowych wartości w 700 milisekund, zwalniając na końcu. Relax przechodzi na 30% i ciepło, Focus na 100% i zimno.
  • on_power() działa, gdy przełącznik się zmieni. Przy wyłączonym świetle wyłącza każdy inny obiekt na ekranie i przygasza go do 30%.

Ostatnia linia otwiera build_ui(), funkcję, w której teraz znajduje się wygenerowany kod.

Twój kod pod znacznikami ​

Pod // [END lopaka generated] zamknij funkcję i dodaj style, zdarzenia oraz zwykłe setup() i loop():

cpp
  dial = arc_dial;
  level = label_level;
  warmth = slider_warmth;

  // styles
  lv_obj_set_style_text_font(level, &lv_font_montserrat_48, 0);
  lv_obj_align(level, LV_ALIGN_TOP_MID, 0, 116);
  lv_obj_set_style_bg_color(warmth, lv_color_hex(0xFF8A3D), LV_PART_MAIN);
  lv_obj_set_style_bg_opa(warmth, LV_OPA_COVER, LV_PART_MAIN);
  lv_obj_set_style_bg_grad_color(warmth, lv_color_hex(0x4FC3F7), LV_PART_MAIN);
  lv_obj_set_style_bg_grad_dir(warmth, LV_GRAD_DIR_HOR, LV_PART_MAIN);
  lv_obj_set_style_bg_opa(warmth, LV_OPA_TRANSP, LV_PART_INDICATOR);
  lv_obj_set_style_bg_color(warmth, lv_color_white(), LV_PART_KNOB);
  tint(lv_slider_get_value(warmth));

  // events
  lv_obj_add_event_cb(arc_dial, on_dial, LV_EVENT_VALUE_CHANGED, NULL);
  lv_obj_add_event_cb(slider_warmth, on_warmth, LV_EVENT_VALUE_CHANGED, NULL);
  lv_obj_add_event_cb(switch_power, on_power, LV_EVENT_VALUE_CHANGED, NULL);
  lv_obj_add_event_cb(button_relax, on_relax, LV_EVENT_CLICKED, NULL);
  lv_obj_add_event_cb(button_focus, on_focus, LV_EVENT_CLICKED, NULL);
}

void setup() {
  display_begin();
  build_ui();
}

void loop() {
  lv_timer_handler();
  delay(5);
}

Panel kodu w Lopaka przy wyłączonej funkcji opakowującej: pod znacznikiem END blok stylów z czcionką Montserrat 48 i gradientem suwaka, a następnie pięć linii lv_obj_add_event_cb, setup i loop

Pierwsze trzy linie kopiują wygenerowane obiekty do zmiennych globalnych, aby funkcje powyżej mogły do nich dotrzeć. Wygenerowane zmienne istnieją tylko wewnątrz build_ui().

Style. Jedna linia stylu sprawia, że liczba jest duża: lv_font_montserrat_48 to czcionka Montserrat LVGL o rozmiarze 48 pikseli. Większa liczba jest też szersza, więc lv_obj_align() wyśrodkowuje ją na górze pokrętła zamiast pozostawiać ją w starym lewym górnym rogu. Następne linie malują ścieżkę suwaka jako gradient od ciepłego do zimnego, ukrywają jego część wypełnioną i sprawiają, że gałka jest biała.

Zdarzenia. Jedna linia na widżet łączy go z funkcją: lv_obj_add_event_cb(object, function, event, NULL). Pokrętło aktualizuje liczbę, suwak przyciemnia pokrętło, przyciski przełączają sceny, a przełącznik przyciemnia ekran. Pokrętło, suwak i przełącznik wysyłają LV_EVENT_VALUE_CHANGED; przyciski wysyłają LV_EVENT_CLICKED.

Pętla. lv_timer_handler() to miejsce, w którym LVGL wykonuje swoją pracę: odczytuje dotyk, wysyła zdarzenia, uruchamia animacje i przerysowuje to, co się zmieniło. Wywołuj ją co kilka milisekund i utrzymuj loop() wolne od długich opóźnień, w przeciwnym razie ekran przestanie reagować.

Krok 5: skopiuj kod ​

Kliknij Kopiuj nad kodem. Otrzymasz cały szkic: twój kod, wygenerowaną część między znacznikami oraz setup() i loop().

Krok 6: uruchom to ​

Plik wyświetlacza ​

Szkic zaczyna się od #include "touch_display.h". Ten plik łączy LVGL z tym wyświetlaczem i jego chipem dotykowym. Jest taki sam dla każdego projektu z tym wyświetlaczem, więc piszesz go raz:

cpp
// Display + touch glue for LVGL: ILI9341 (SPI) + FT6206 (I2C), 240x320 portrait.
#include <lvgl.h>
#include <Adafruit_ILI9341.h>
#include <Adafruit_FT6206.h>

Adafruit_ILI9341 tft(23, 21, 22);  // CS, DC, RST
Adafruit_FT6206 touch;
#define DRAW_BUF_SIZE (240 * 160 * 2)   // half the screen
static uint8_t *draw_buf;

static void flush(lv_display_t *d, const lv_area_t *a, uint8_t *px) {
  uint32_t w = a->x2 - a->x1 + 1, h = a->y2 - a->y1 + 1;
  tft.startWrite();
  tft.setAddrWindow(a->x1, a->y1, w, h);
  tft.writePixels((uint16_t *)px, w * h);
  tft.endWrite();
  lv_display_flush_ready(d);
}

static void read_touch(lv_indev_t *i, lv_indev_data_t *data) {
  if (touch.touched()) {
    TS_Point p = touch.getPoint();
    data->point.x = 240 - p.x;   // the touch panel is mirrored
    data->point.y = 320 - p.y;
    data->state = LV_INDEV_STATE_PRESSED;
  } else {
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

void display_begin() {
  pinMode(5, OUTPUT);
  digitalWrite(5, HIGH);  // backlight
  SPI.begin(18, 17, 19);  // SCK, MISO, MOSI
  Wire.begin(4, 16);      // SDA, SCL
  tft.begin();
  tft.setRotation(0);
  touch.begin(40);
  lv_init();
  lv_tick_set_cb(millis);
  lv_display_t *disp = lv_display_create(240, 320);
  lv_display_set_flush_cb(disp, flush);
  draw_buf = (uint8_t *)malloc(DRAW_BUF_SIZE);
  lv_display_set_buffers(disp, draw_buf, NULL, DRAW_BUF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
  lv_indev_t *indev = lv_indev_create();
  lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
  lv_indev_set_read_cb(indev, read_touch);
}

Ma dwa zadania:

  • flush() wysyła piksele na wyświetlacz. LVGL nie przechowuje całego ekranu w pamięci: pełny obraz 240×320 z dwoma bajtami na piksel zająłby 153 600 bajtów. Rysuje do bufora o połowie ekranu, 240 × 160 × 2 = 76 800 bajtów, i przekazuje jeden obszar naraz. Gdy zmienia się tylko liczba, tylko ten obszar jest rysowany i wysyłany.
  • read_touch() mówi LVGL, gdzie jest palec. Chip FT6206 zgłasza punkt dotyku. Na tym panelu jego współrzędne biegną odwrotnie do współrzędnych wyświetlacza, więc 240 - p.x i 320 - p.y je odwracają.

lv_tick_set_cb(millis) daje LVGL zegar dla jego animacji, a touch.begin(40) ustawia, jak mocny musi być dotyk, aby się liczył.

Plik ustawień ​

LVGL odczytuje swoje ustawienia z pliku o nazwie lv_conf.h. Weź lv_conf_template.h z biblioteki LVGL, zapisz go jako lv_conf.h i zmień dwie linie:

cpp
#if 1 /* Set this to "1" to enable content */
cpp
#define LV_FONT_MONTSERRAT_48 1

Pierwsza linia włącza plik. Druga dodaje czcionkę 48-pikselową dla dużej liczby: LVGL kompiluje tylko czcionki, które włączysz, a szablon włącza tylko Montserrat 14, domyślną czcionkę każdej etykiety.

Uruchom to w symulatorze ​

W symulatorze online z odcinka 1 utwórz nowy projekt ESP32 i dodaj wyświetlacz ILI9341 Cap Touch: ekran ILI9341 z panelem dotykowym FT6206. Podłącz go do ESP32:

  • VCC do 3V3, GND do GND, LED (podświetlenie) do 5,
  • CS do 23, D/C do 21, RST do 22,
  • SCK do 18, MOSI do 19, MISO do 17,
  • SDA do 4 i SCL do 16 dla panelu dotykowego.

Dodaj biblioteki lvgl, Adafruit ILI9341 i Adafruit FT6206 Library. Utwórz dwa nowe pliki, touch_display.h i lv_conf.h, o powyższej zawartości. Wklej kod z Lopaka do sketch.ino i naciśnij Play.

Po zbudowaniu mysz staje się palcem. Obróć pokrętło: liczba podąża za nim. Przesuń od ciepłego do zimnego: pokrętło zmienia kolor. Dotknij Focus: pokrętło przesuwa się na 100%. Następnie wyłącz światło, a reszta ekranu przygasa.

Symulowane ESP32 podłączone do wyświetlacza ILI9341 Cap Touch pokazującego ekran Living room z pokrętłem na 43% i kursorem na gałce pokrętła

Na prawdziwej płytce ​

Ten sam szkic działa na prawdziwym ESP32 z modułem ILI9341 2,8 cala. W Arduino IDE zainstaluj te same trzy biblioteki i wybierz płytkę ESP32 Dev Module. Szkic zajmuje około połowy flash i 27% RAM; 64 KB tego RAM to własna pamięć LVGL na obiekty i style. Trzy rzeczy do sprawdzenia:

  • Gdzie trafia lv_conf.h. W Arduino IDE nie trafia obok szkicu. Umieść go w folderze libraries, obok folderu lvgl, a nie wewnątrz niego. Jeśli LVGL go nie znajdzie, brakuje czcionki 48-pikselowej i szkic się nie kompiluje.
  • Chip dotykowy. Wiele niedrogich modułów ILI9341 2,8 cala ma rezystancyjny panel dotykowy z chipem XPT2046 zamiast pojemnościowego FT6206. W ich przypadku read_touch() potrzebuje biblioteki dla XPT2046; reszta szkicu pozostaje taka sama.
  • Kierunek dotyku. Jeśli dotknięcie ląduje po złej stronie ekranu, twój panel nie jest odbity jak ten. Użyj p.x i p.y bezpośrednio w read_touch() albo odbij tylko jedną z nich.

Kiedy LVGL jest wart swojej ceny ​

LVGL potrzebuje pamięci. Jego własna dokumentacja wymaga co najmniej 32 KB RAM. Arduino UNO ma 2 KB, więc samo LVGL potrzebuje 16 razy więcej niż całe UNO.

Czy LVGL zawsze jest wart swojej ceny: pomarańczowy pasek LVGL potrzebuje co najmniej 32 KB jest 16 razy dłuższy niż 2 KB RAM Arduino UNO

ESP32 daje twojemu szkicowi 320 KB RAM, a ten interfejs używa 27% z tego. Wybierz narzędzie według zadania:

  • Kilka liczb na OLED? Biblioteka graficzna wystarczy.
  • Dotyk, widżety i animacja? To LVGL.

Na ESP32 z 320 KB dla twojego szkicu LVGL pozostawia miejsce. Kilka liczb na OLED: biblioteka graficzna. Dotyk, widżety, animacja: LVGL

Podsumowanie ​

  1. Obiekty, nie piksele: LVGL buduje ekran z obiektów takich jak łuki, suwaki i przyciski. Tworzysz przycisk zamiast go rysować.
  2. Style dla każdego stanu: kolory, promień i czcionki decydują, jak obiekt wygląda domyślnie, gdy jest wciśnięty, zaznaczony lub wyłączony.
  3. Zdarzenia łączą dotknięcie z twoim kodem: jedna linia lv_obj_add_event_cb na widżet. Lopaka rozmieszcza widżety, a ty piszesz zdarzenia.

Następny odcinek: Poza Arduino. Jeden projekt, wiele platform, od ESPHome po MicroPython i Flipper Zero. Przeczytaj odcinek 9 lub obejrzyj go na YouTube.

Gotowy, aby spróbować? Otwórz Lopaka i ułóż własny ekran dotykowy.