Primeiros passos com LVGL no ESP32

LVGL no ESP32

Após alguns dias de tremendo esforço, enfim coloquei a biblioteca LVGL no ESP32. Sério, não foi fácil, mas não por ser complexa demais. O problema é que não estava claro o que deveria ser feito para o pontapé inicial.

Entre os problemas, o PlatformIO no VSCode pegou estranhamente uma versão antiga como sendo a mais recente do LVGL. Depois de me debater muito com erros de compilação, vi que a versão instalada do LVGL não era compatível com o repositório de exemplos, que era mais atual. Daí fiz o download das bibliotecas na IDE do Arduino e copiei para o projeto no VSCode, mas de forma alguma conseguia fazer funcionar – até perceber que a versão que deveria ser instalada era outra. Por isso, esse artigo não é o “tutorial definitivo de LVGL”, mas é o primeiro passo para os próximos artigos relacionados.

O que é a LVGL?

LVGL é o acrônimo de Light and Versatile Graphics Library, uma biblioteca inacreditável criada para dispositivos embarcados, desde os mais simples aos mais poderosos, como Raspberry. Seu código é otimizado para total compatibilidade com C++, podendo-se dizer que ela é o supra-sumo (dane-se a nova ortografia) das bibliotecas gráficas para embarcados. Com mais de 30 widgets, diversos temas e muita beleza, essa biblioteca tende a ser o novo padrão para projetos que prezam pelo visual tanto quanto pelo backend.

Como usar a biblioteca LVGL no ESP32

O primeiro passo é criar o projeto. Faça-o na IDE de sua preferência, então instale 2 bibliotecas fundamentais.

lv_arduino

Essa é a biblioteca LVGL para o framework Arduino. Instale-a, não importa se vai programar um ESP32 ou um Arduino. Ela está disponível pelo gerenciador de bibliotecas, seja no PlatformIO ou na IDE do Arduino.

TFT_eSPI

Já escrevi 3 importantes artigos relacionados a essa biblioteca. Um deles, dedicado ao display ILI9341 touch. Na vez posterior foi quando criei o misturador de cores CMYK. No artigo anterior a esse escrevi uma interface bonitinha usando a TFT_eSPI na AFSmartControl. Essa biblioteca é incrível, só não é tão bonita quanto a LVGL, e deve ter uns 3% de sua capacidade, como veremos em artigos posteriores.

Mesmo usando a LVGL, os eventos do touch serão pegos com a TFT_eSPI, ou seja, a LVGL terá a função exclusiva de tratar da interface.

Configurar a biblioteca lv_arduino

Se estiver utilizando a LVGL em outras plataformas, não é a lv_arduino que você deverá instalar. Nesse caso, use diretamente a lvgl. Se utilizar a lvgl diretamente, copie para fora do diretório lvgl o arquivo lv_conf_templ.h com o nome lv_conf.h. Esse arquivo deve ficar no mesmo nível de diretório da biblioteca lvgl.

Feita a cópia, edite o arquivo e troque if 0 por if 1 e a resolução para (320)(240).

Só pra salientar: eu citei o display, mas enfatizo que o artigo se refere à placa AFSmartControl com o display ILI9341, ambos vendidos pela AFEletronica.

Configurar a biblioteca TFT_eSPI

Esse procedimento já foi citado nos outros artigos, mas não custa repetir.

Edite o arquivo User_Setup.h e comente as linhas relacionadas a GPIO (são apenas 3), então adicione essas linhas:

#define TOUCH_CS 33 
#define TFT_MISO 19 
#define TFT_MOSI 23 
#define TFT_SCLK 18
#define TFT_CS   12       
#define TFT_DC    2           
#define TFT_RST   4   
#define TFT_RST  -1

Essa é a configuração do display na AFSmartControl. Se for usar outra placa, defina os pinos conforme seu wiring.

Criar botões com LVGL – primeiro sketch

Feitas as alterações anteriores, podemos criar nosso primeiro programa, mas terei que comentar os blocos de código para que fique claro o que cada parte faz. E apesar de ser uma sequência de linhas específicas das bibliotecas, sabendo o que são elas fica bem fácil entender o todo.

Includes

Temos que incluir 3 bibliotecas:

#include <lvgl.h>
#include <TFT_eSPI.h>
#include <stdio.h>

Então definir algumas coisas antes de declarar funções:

TFT_eSPI tft = TFT_eSPI(); //Instância do controlador do touch (fará apenas isso)
static lv_disp_buf_t disp_buf; //criar um buffer para o dispositivo (explico adiante)
static lv_color_t buf[LV_HOR_RES_MAX * 10]; //reservar um tamanho de buffer para o dispositivo (explico também)

uint16_t t_x = 0, t_y = 0; //variáveis para as coordenadas x,y

Uma maneira de controlar o display de forma fluida é criar um buffer para a imagem que será renderizada. Pela documentação, é recomendável e suficiente 10% do total do frame. São essas as linhas 2 e 3 acima.

Funções precedentes ao setup() e loop()

Se estiver utilizando VScode, tenha em mente essa dica: crie protótipos das funções antes de declará-las, ou então preceda as funções setup() e loop() com as demais. De qualquer modo, se chamar uma função que tenha sido declarada após a chamada, haverá um erro na compilação, por isso o ideal é criar protótipos primeiro.

A primeira função declarada é a event_handler. Não criei os protótipos como sempre faço, mas isso porque estava há 3 dias me debatendo com a biblioteca, então gerei montes de códigos diferentes até chegar em um funcional. O protótipo seria:

static void event_handler(lv_obj_t * obj, lv_event_t event);

Todas as funções podem ser prototipadas antes de serem declaradas, depois começa-se suas declarações.

Manipulador de eventos

Um manipulador de eventos é uma função que trata os eventos gerados pelos widgets. Um evento pode ser um click, um toque, um dado chegando na serial etc.

static void event_handler(lv_obj_t * obj, lv_event_t event)
{
    if(event == LV_INDEV_STATE_PR) {
        printf("Clicked\n");
    }
}

Essa função é do manipulador de eventos. O evento é emitido pelo callback do widget, como veremos em detalhes.

Widget button – um exemplo prático do LVGL no ESP32

Esse é um dos códigos de exemplo da documentação, no caso, para o botão.

Um widget faz parte de uma janela; o título sobre o botão faz parte do botão. Então, temos um tipo de container dentro de outro container, sendo esse o nível máximo. Como uma série de linhas são necessárias para configurar adequadamente um widget, normalmente cria-se uma função que comporta todas essas linhas. Depois, basta chamar a função em setup().

void lv_ex_btn_1(void)
{
    lv_obj_t * label; //cria um objeto label

    lv_obj_t * btn1 = lv_btn_create(lv_scr_act(), NULL); //cria um objeto button

    lv_obj_set_event_cb(btn1, event_handler); //cria o callback para o botão
    lv_obj_align(btn1, NULL, LV_ALIGN_CENTER, 0, -40); //posiciona o botão, usando como artifício a macro de alinhamento central x,y

    label = lv_label_create(btn1, NULL); //atribui o label ao botão
    lv_label_set_text(label, "Button"); //configura o titulo do botão

    //cria o segundo botão, da mesma forma
    lv_obj_t * btn2 = lv_btn_create(lv_scr_act(), NULL);
    lv_obj_set_event_cb(btn2, event_handler);
    lv_obj_align(btn2, NULL, LV_ALIGN_CENTER, 0, 40);
    lv_btn_set_checkable(btn2, true);
    lv_btn_toggle(btn2);
    lv_btn_set_fit2(btn2, LV_FIT_NONE, LV_FIT_TIGHT);

    label = lv_label_create(btn2, NULL);
    lv_label_set_text(label, "Toggled");

}

Repare que tanto o label quanto o botão são objetos do mesmo tipo (lv_obj_t). O que diferenciará sua função é justamente os valores que vêm depois do sinal de igualdade (lv_btn_create ou lv_label_create, para o exemplo do widget button).

Controlador do display

A função de descarga do display é responsável por limpar a tela. Não precisamos de muitos detalhes aqui, apenas precisamos lembrar de declarar essa função:

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
    uint32_t w = (area->x2 - area->x1 + 1);
    uint32_t h = (area->y2 - area->y1 + 1);

    tft.startWrite();
    tft.setAddrWindow(area->x1, area->y1, w, h);
    tft.pushColors(&color_p->full, w * h, true);
    tft.endWrite();

    lv_disp_flush_ready(disp);
}

Função de callback do touchscreen

Os eventos gerados no display precisam ser gerenciados pelo driver (visto mais adiante). A função de controle de toque é feito na função de exemplo my_input_read. Aqui que brilha o nosso famigerado TFT_eSPI, que gerencia o touchscreen e repassa o evento.

bool my_input_read(lv_indev_drv_t * drv, lv_indev_data_t*data)
{
    
    if (tft.getTouch(&t_x, &t_y)){ //se houver evento de toque, a função retorna true e armazena a posição em t_x e t_y
        data->state = LV_INDEV_STATE_PR ; //como houve evento, mudamos state para o tipo do evento. No caso do touchscreen, LV_INDEV_STATE_PR
        data->point.x = t_x; //atribuimos à struct a posição em que o evento foi gerado
        data->point.y = t_y; //o mesmo para y
    }
    else{
        data->state = LV_INDEV_STATE_REL; //caso contrário, zera tudo.
        data->point.x = 0;
        data->point.y = 0;
    }
    return false; /*No buffering now so no more data read*/
}

Quase finalizado: setup()

Concluímos as funções periféricas, agora em setup devemos inicializar tudo o que será usado na execução do programa: serial, tft, posição do display etc.

void setup(){
    Serial.begin(9600); 
    lv_init(); //inicializa o lvgl
    
    uint16_t calData[5] = {331, 3490, 384, 3477, 6}; //calibração feita para orientação do display, explicado nos artigos anteriores

    tft.begin(); //inicializa a TFT_eSPI
    tft.setRotation(0); //rotaciona para a mesma posição utilizada no colorMixer

    tft.setTouch(calData); //executa a calibração do touchscreen

    lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10); //inicializa o buffer com 10% do total do frame

    /*Inicialização do display (padrão)*/
    lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.hor_res = 240;
    disp_drv.ver_res = 320;
    disp_drv.flush_cb = my_disp_flush;
    disp_drv.buffer = &disp_buf;
    lv_disp_drv_register(&disp_drv);

    /*Inicialização do driver do dispositivo de entrada (usando o touchscreen aqui)*/
    lv_indev_drv_t indev_drv;
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;
    indev_drv.read_cb = my_input_read; //callback de leitura
    lv_indev_drv_register(&indev_drv);

    /* Criando um label no centro da tela. Nada importante ou funcional, só um label */
    lv_obj_t *label = lv_label_create(lv_scr_act(), NULL);
    lv_label_set_text(label, "Do bit Ao Byte - LVGL");
    lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0);

    lv_ex_btn_1(); //inicializa os botões
}

loop()

Enfim, terminamos o programa do LVGL no ESP32. Aqui devemos apenas criar um delay entre 1 e 10ms, chamando a função lv_task_handler. Depois, é só desfrutar do resultado!

void loop(){
    lv_task_handler(); /* let the GUI do its work */
    delay(5);
}

Vídeo

Não fiz vídeo para esse exemplo básico de LVGL no ESP32, mas agora vou construir uma interface elaborada e, a partir de então teremos vídeos com mais explicações e demonstrações interessantes, por isso, se não é inscrito, inscreva-se em nosso canal DobitaobyteBrasil no Youtube e clique no sininho para receber notificações. Até o próximo artigo!

 

Revisão: Ricardo Amaral de Andrade