Manual
do
Maker
.
com
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.
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.
O primeiro passo é criar o projeto. Faça-o na IDE de sua preferência, então instale 2 bibliotecas fundamentais.
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.
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.
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) e (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.
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.
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.
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.
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.
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.
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).
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);
}
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*/
}
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, "Manual do Maker - LVGL");
lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0);
lv_ex_btn_1(); //inicializa os botões
}
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);
}
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
Inscreva-se no nosso canal Manual do Maker no YouTube.
Também estamos no Instagram.
Autor do blog "Do bit Ao Byte / Manual do Maker".
Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.