IoT

Introdução a ESP-IDF com VS Code – parte 2

Eletrogate 21 de novembro de 2024

Introdução

Nesse post vamos dar continuidade aos assuntos comentados no post anterior: “Introdução a ESP-IDF no VS Code”, abordando o gerenciamento de componentes, drivers de Timer e LCD e uma pequena introdução a LVGL.

Recapitulando

Antes de começar, vamos fazer um breve resumo dos tópicos abordados no post anterior:

  • Núcleo Real Time: por ser focado no uso profissional onde gerenciamento de tempo e recursos é extremamente importante, a ESP-IDF usa um RTOS chamado “FreeRTOS” e a maioria das suas APIs são “Thread Safe“;
  • Geração de logs via ESP_LOGx: logs são ferramentas muito úteis para debug do nosso projeto, e mais convenientes que o uso de “prints”, pois podem ser facilmente desabilitadas nas configurações do SDK;
  • Tratamento de erro: além da ESP-IDF prover um meio padronizado para lidar com erros via “esp_err_t” e ESP_ERROR_CHECK”, também tem suporte para tratamento de erros no estilo POSIX e Exceções do C++;
  • Drivers: drivers são uma forma padronizada da ESP-IDF para acessar os periféricos do hardware de forma simples.

Por fim, em nosso último projeto vimos como incluir bibliotecas de terceiros e fizemos uso de alguns drivers ainda não abordados. Nesse post não só vamos explicar o uso desses drivers, como ao decorrer do texto vamos falar de forma mais aprofundada sobre o que foi abordado anteriormente.

Outro ponto importante para citar é que essa sequência de artigos é baseada na versão 5.x da IDF, a maioria dos drives não teve grandes mudanças entre as versões passadas, mas a configuração do projeto pode ser incompatível, então sempre verifique a documentação oficial para ficar por dentro das novidades.


Materiais

Nesse post, vamos usar os seguintes materiais:


Instalação de componentes na ESP-IDF

No post passado nos vimos uma forma básica de incluir bibliotecas, onde manualmente baixamos e configuramos, apesar de simples, esse processo pode ser uma dor de cabeça em projetos maiores, imagine lidar com as versões de todas as bibliotecas instaladas onde a cada atualização você teria que manualmente ir alterando uma por uma…, por sorte a ESP-IDF tem um sistema de gerenciamento de dependências baseado em “componentes”, os componentes nada mais são de que uma forma padronizada  de criar bibliotecas para ESP-IDF.

Component Manager

O Component Manager é a fermenta da ESP-IDF que vai gerenciar os componentes, ele automaticamente vai baixar e incluir as dependências no nosso projeto, é bastante semelhante a “Library Manager” presente na Arduino IDE.

Instalando componentes pelo Visual Studio Code

com o nosso projeto aberto, vamos abrir o “Command palette” do vscode, isso pode ser feito com pressionando as teclas: Ctrl + Shift + P ou indo em View->Command palette, isso vai abrir a aba de comandos do editor, nessa aba basta digitar: “ESP-IDF: Show ESP Component Registry” e isso vai abrir a pagina de componentes da IDF:

Nessa pagina basta pesquisar o nome do componente que você deseja instalar e clicar no componente:

isso vai abrir a pagina do componente, onde normalmente vai ter todos os detalhes sobre ele, para instalar, selecione a versão desejada e clique em install, pronto!, componente instalado, simples assim!

Instalando componentes pelo terminal

caso não esteja usando o vscode, a instalação de um componente ainda é bem simples, primeiro nos temos que acessar a pagina “ESP Component Registry“, essa pagina é exatamente a mesma que a do Vscode, a única diferença aqui é que em vez de clicar instalar você vai rodar o comando especificado na linha “To add this component to your project, run:na pasta do projeto.

Nos vamos fazer uso de alguns componentes no projeto desse post, mas antes disso temos mais alguns tópicos para abordar.


Timer Drivers

Outra coisa que usamos no ultimo exemplo do post passado foi o uso de timers para gerar o delay para nosso LCD, no caso o Driver usado foi o: GPTimer (General Purpose Timer), nesse post também vamos fazer uso deles, mas antes precisamos entender como eles funcionam.

Timers na ESP-IDF

a ESP-IDF tem 3 tipos de timers diferentes, FreeRTOS timer, esp-timer e GPTimer timer, todos eles podem ser usados para controle de tempo e geração de eventos, mas cada um deles tem uma peculiaridade que o torna especial.

FreeRTOS timer:

Esse é um software timer provido pelo FreeRTOS, aqui no blog eletrogate temos um artigo inteiro sobre a configuração e uso desse timer: FreeRTOS – Temporizadores de Software – Blog Eletrogate.

esp-timer:

Assim como o FreeRTOS, esse também é um software timer, mas diferente do timer do FreeRTOS que usa ticks do RTOS, o esp-timer usa o clock interno do ESP32, então ele tem uma resolução muito maior e consequentemente uma precisão maior.

configuração:

A configuração do esp-timer é bem simples e requer apenas 1 função e uma struct de configuração, para configurar o esp-timer temos:

  1. incluir a biblioteca <esp_timer.h>
  2. declarar um handler de esp_timer: “esp_timer_handle_t“, esse é o handler onde as configurações do timer vão ser salvas, não é necessário pré-configurar.
  3. criar uma struct de configuração: “esp_timer_create_args_t“, essa é a struct onde vamos configurar o timer, ela contem os seguintes campos:
    • callback: a função que vai ser chamada quando ocorrer um evento de timer. (a assinatura dessa função deve ser (void)(void *args), uma função sem retorno que recebe um ponteiro de tipo indefinido)
    • argum ponteiro de tipo indefinido para a informação passada para a função anterior. (pode ser NULL)
    • dispatch_method: define se a função vai ser chamada como uma task ou como uma ISR, recebe um dos seguintes valores:
      • ESP_TIMER_TASK.
      • ESP_TIMER_ISR.  (é desabilitado por padrão, pode ser habilitado pelas configurações do projeto)
    • name: uma string com o nome do timer (útil para debug, não é obrigatório).
    • skip_unhandled_events: define se o timer deve pular eventos ainda não executados, recebe true ou false.
  4. efetivar as configurações com a função: esp_timer_create(esp_timer_create_args_t*, esp_timer_handle_t*) o primeiro argumento é um ponteiro para struct de configuração, o segundo é um pónterio para o handler do timer.

Uso:

O esp timer pode ser usado de duas formas:

  • para gerar eventos “one-shot” (eventos que só vão acontecer uma vez), usando a função: esp_timer_start_once(esp_timer_handle_t, uint64_t).
  • para gerar eventos periódicos (eventos que ocorrem varias vezes em determinada frequência) usando a função: esp_timer_start_periodic(esp_timer_handle_t, uint64_t).

Ambas as funções recebem como primeiro argumento o handler do timer e como segundo argumento o tempo em microssegundos para o evento ocorrer.

Considerações:

uma lista de coisas que você deve ficar atento ao usar esp-timer:

  • cada handler de timer só pode ser associado a um evento de cada vez, para interromper o evento atual de um handler, use a função: esp_timer_stop(esp_timer_handle_t) .
  • existe um limite de tempo mínimo para os eventos do timer: 20uS para “one-shot” e 50uS para periódicos.
  • configuração manual do “sleep-mode” pode interromper o funcionamento do timer.

(Referencia: ESP Timer (High Resolution Timer) – ESP32 – — ESP-IDF Programming Guide latest documentation (espressif.com))

GPTimer (General Purpose Timer):

Diferente dos timers anteriores, esse é uma representação dos hardware timers do ESP32, com eles nos temos acesso a funcionalidades mais complexas, com uma resolução muito maior do que a dos timers anteriores, o que tornar os GPTimer muito importante em aplicações em que o controle de tempo é extremamente importante, o uso dos GPTimers é mais difícil do que os software timers, garantir seu funcionamento requer conhecimento mais avançado sobre o hardware do ESP32.

Configuração:

Para configurar o GPTimer é necessário:

  • incluir a biblioteca: <driver/gptimer.h>
  • declarar um handler: gptimer_handle_t, esse é o handler onde as configurações serão salvas, não é necessário pre-configurar.
  • criar uma struct de configuração: gptimer_config_t, essa struct contem os seguintes campos:
    • clk_src: a fonte de clock do timer, no ESP32 pode ser dos seguintes valores:
      • GPTIMER_CLK_SRC_APB ou GPTIMER_CLK_SRC_DEFAULT (no esp32 os dois são iguais)
    • direction: diz se a contagem do timer vai incrementar ou decrementar, recebe um dos seguintes valores:
      • GPTIMER_COUNT_DOWN
      • GPTIMER_COUNT_UP
    • resolution_hz: resolução em Hz do timer
    • intr_priority: prioridade do interrupção do timer
    • flags.intr_shared: se o número da interrupção pode ser compartilhado com outros periféricos
  • criar um novo timer com a função: gptimer_new_timer(const gptimer_config_t*, gptimer_handle_t*), o primeiro argumento é um ponteiro para nossa struct de configuração o segundo é um ponteiro para o handler do GPTimer

Pronto GPTimer configurado!

Uso:

O GPTimer pode ser usado para gerar eventos “one-shot” e periódicos ou como um “Wall Clock” (mecanismo para contagem de tempo, sem eventos), para gerar eventos devemos seguir os passos:

  1. criar uma struct de configuração de eventos: gptimer_alarm_config_t, essa struct contem os seguintes campos:
    • alarm_count: o tempo em ticks* em que o evento vai acorrer
    • reload_count: o  valor em ticks* que o timer vai carregar após o reset
      (tikcs*: atenção! o tempo de cada tick depende da resolução do timer, Ex: se um timer tem uma resolução de 1Mhz, cada tick vale 1uS)
    • flags.auto_reload_on_alarm: se o timer deve resetar após o evento (1=true/0=false)
  2. criar uma struct do callback do evento: gptimer_event_callbacks_t, essa struct só tem um campo: .on_alarm, que recebe um ponteiro de função com a assinatura: bool()(gptimer_handle_t, const gptimer_alarm_event_data_t*, void*)
  3. efetivar as configurações do evento com a função: gptimer_set_alarm_action(gptimer_handle_t, const gptimer_alarm_config_t*), o primeiro argumento é o nosso handler o segundo é um ponteiro para as configurações  do evento.
  4. registrar o evento com a função: gptimer_register_event_callbacks(gptimer_handle_t, const gptimer_event_callbacks_t *, void*), o primeiro argumento é o nosso handler, o segundo é um ponterio para struct de callback o ultimo é um ponteiro com argumentos para nosso callback (pode ser NULL caso nosso evento não receba argumentos).

para iniciar o timer basta chamar a função: gptimer_enable(gptimer_handle_t) seguida da função: gptimer_start(gptimer_handler_t),isso vai inciar a contagem do nosso timer, e iniciar novo evento como periódico, para tornar “one-shot” temos que manualmente chamar gptimer_stop (gptimer_handler_t) após o evento; Para configurar como “Wall clock” basta pular a etapa de configuração e iniciar o timer.

a leitura e escrita do valor da contagem do timer pode ser feito pelas seguintes funções: gptimer_get_raw_count(gptimer_handle_t, uint64_t*) e gptimer_set_raw_count(gptimer_handle_t, uint64_t)

Considerações:

uma lista de coisas que você deve ficar atento ao usar GPTimer:

  • configurações devem ser feitas com o timer desabilitado.
  • eventos do GPTimer são interrupções, você deve usar as APIs do FreeRTOS com o prefixo: FromISR.
  • configurações que afetem a fonte de clock do timer impactam diretamente no seu funcionamento.
devido a sua complexidade, GPTimer só deve ser usado em tarefas onde realmente é necessário, e se deve ter muito cuidado com a interação dele e os outras partes do sistema.

Exemplo: Led blink com timers

Nesse primeiro Exemplo vamos ver na pratica o funcionamento do timers e ver uma característica  interessante sobre os GPIOs do ESP32.

Esquema:

Código:

/*
===================================
ESP timers exemplo (Introdução ESP-IDF pt2 - Eletrogate blog)
blink basico com GPTimer e ESPTimer
Author: Guilherme SilVA Schultz (RecursiveError)
data: 2024-08-09
===================================
*/

//C includes
#include <stdio.h>

//FreeRTOS includes
#include <freertos/FreeRTOS.h>

//esp-idf includes
#include <driver/gptimer.h>
#include <driver/gpio.h>
#include <esp_log.h>
#include <esp_err.h>
#include <esp_timer.h>


//Led difines
#define GPTIMER_LED GPIO_NUM_23
#define ESP_TIMER_LED GPIO_NUM_22

//timer defines
#define ESP_TIMER_DELAY_US 500*1000 //~0.5s
#define GPTIMER_DELAY_US   800*1000 //~0.8s

//init tasks
void GPIO_init();
void GPTimer_init();
void ESPTimer_init();

//timer callbacks
void esptimer_callback(void *args);
bool gptimer_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx);

//timers handlers
esp_timer_handle_t esp_timer_h;
gptimer_handle_t gptimer_h;

//timer consts
const char esp_timer_name[] = "esp_timer_example";

void app_main(void){
    const char TAG[] = "main";
    ESP_LOGI(TAG, "start GPIO config");
    GPIO_init();
    ESP_LOGI(TAG, "start ESPTimer config");
    ESPTimer_init();
    ESP_LOGI(TAG, "start GPtimer config");
    GPTimer_init();
    ESP_LOGI(TAG, "main loop start");
    while(1){
        vTaskDelay(portMAX_DELAY);
    }
}


void GPIO_init(){
    const char TAG[] = "GPIO_init";
    const gpio_config_t conf = {
        .pin_bit_mask = (1<<GPTIMER_LED) | (1<<ESP_TIMER_LED),
        .mode = GPIO_MODE_INPUT_OUTPUT, //esse modo nos permite usar as funções de output e input nos mesmo pino!
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    ESP_ERROR_CHECK(gpio_config(&conf));
    ESP_LOGI(TAG, "GPIO config OK");
}

void ESPTimer_init(){
    const char TAG[] = "ESPTimer_init";
    const esp_timer_create_args_t timer_conf = {
        .callback = esptimer_callback,
        .arg = NULL, //nosso evento não revebe argumentos
        .dispatch_method = ESP_TIMER_TASK, //i evento vai ser executado como task do FreeRTOS
        .name = esp_timer_name,
        .skip_unhandled_events = true,
    };
    ESP_ERROR_CHECK(esp_timer_create(&timer_conf, &esp_timer_h));
    ESP_ERROR_CHECK(esp_timer_start_periodic(esp_timer_h, ESP_TIMER_DELAY_US)); // inivia o evento como periodico
    ESP_LOGI(TAG, "ESPtimer config OK");
}

void GPTimer_init(){
    const char TAG[] = "GPTimer_init";
    const gptimer_config_t gpt_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, //fonte de clock padrão
        .direction = GPTIMER_COUNT_UP,
        .resolution_hz = 1000*1000, //clock = 1Mhz, cada tick = 1uS
        .intr_priority = 0, //prioridade automatica
        .flags.intr_shared = 0,
    };

    const gptimer_alarm_config_t gpt_event_cb_conf = {
        .alarm_count = GPTIMER_DELAY_US,
        .reload_count = 0,
        .flags.auto_reload_on_alarm = 1,
    };

    const gptimer_event_callbacks_t gpt_event_cb = {
        .on_alarm = gptimer_callback,
    };

    //inicia o timer e o evento periodico
    ESP_ERROR_CHECK(gptimer_new_timer(&gpt_config, &gptimer_h));
    ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer_h, &gpt_event_cb_conf));
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer_h, &gpt_event_cb, NULL));
    ESP_ERROR_CHECK(gptimer_enable(gptimer_h));
    ESP_ERROR_CHECK(gptimer_start(gptimer_h));

    ESP_LOGI(TAG, "GPtimer config OK");
}


void esptimer_callback(void *args){
    int gpio_level =  1 & ~(gpio_get_level(ESP_TIMER_LED)); //le o valor no pino de saida e inverte o sinal
    gpio_set_level(ESP_TIMER_LED, (uint32_t) gpio_level);
}

bool gptimer_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx){
    int gpio_level =  1 & ~(gpio_get_level(GPTIMER_LED)); //le o valor no pino de saida e inverte o sinal
    gpio_set_level(GPTIMER_LED, (uint32_t) gpio_level);
    return true;
}

 

Resultado:

Veja que além do timers nos fizemos uso das GPIOs no modo “INPUT_OUTPUT” que permitiu a leitura e escrita da porta sem a necessidade de guardar o estado atual em uma variável.


Driver: LEDC

Para finalizar o tema dos timers temos que falar de um dos usos mais comuns de timers: PWM; A esp-idf provem uma API para PWM por meio do driver: LedC (Led Contol), inicialmente esse driver era apenas para controle de brilho de leds, por isso o nome, mas atualmente pode ser usada sem problemas como saída PWM comum.

O esp32 tem dois grupos de PWM, um de alta velocidade e outro de baixa velocidade, com oito canais cada, ou seja no máximo você pode ter 16 saídas PWM.

A primeira parte para configuração do PWM é a configuração do timer que será usado como fonte do sinal PWM, e isso pode ser feito da seguinte forma:

  • inclua a biblioteca <driver/ledc.h>
  • crie uma struct de configuração de timer: ledc_timer_config_t, essa struct contem os seguintes campos:
    • speed_mode: o grupo PWM, pode receber um dos seguintes valores:
      • LEDC_LOW_SPEED_MODE.
      • LEDC_HIGH_SPEED_MODE. (nem todos tem grupos de PWM de alta velocidade)
    • duty_resolution: tamanho de bits de resolução, Max 20Bits.
    • timer_num: número do timer que estamos configurando.
    • freq_hz: frequência do timer.
    • clk_cfg: fonte de clock do timer.
    • deconfigure: false para configurar o timer, true para desconfigurar o timer.

Após a configuração, basta chamar a função:  ledc_timer_config(const ledc_timer_config_t*)passando um ponteiro para nossa struct de configuração.

O esp32 permite a escolha do tamanho da resolução entre: 1 e 20 bits, esse valor depende da frequência do timer, quando maior a frequência do timer, menor é o limite máximo de resolução, Exemplo: um timer de 5Khz é capaz de obter uma resolução máxima de 13bits.

O valor máximo de resolução de um timer pode ser obtido por meio de um calculo envolvendo a fonte de clock e a frequência do timer, por sorte a ESP-IDF fornece meios para realizar esse calculo de forma automática:

  • inclua as bibliotecas: <esp_clk_tree.h> e <soc/clk_tree_defs.h>
  • chame a função: esp_clk_tree_src_get_freq_hz(soc_module_clk_t, esp_clk_tree_src_freq_precision_t, uint32_t*),para obter a velocidade atual de uma fonte de clock, os argumentos dessa função são respectivamente:
    • fonte de clock a ser lida
    • precisão da leitura
    • ponteiro para uma variável que vai receber o valor do clock
  • chame a função: ledc_find_suitable_duty_resolution(uint32_t, uint32_t) passado o a velocidade da fonte de clock (valor obtido na função anterior), e a frequência desejada do timer em Hz, essa função vai retornar o número de bits máximos que podem ser usados na resolução.

Exemplo: obtendo valor máximo de resolução:

uint32_t apb_clock_speed = 0;
ESP_ERROR_CHECK(esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_APPROX, &apb_clock_speed)); //obtem a velocidade aproximada da fonte de clock APB
ESP_LOGI(TAG, "APB CLOCK SPEED: %lu", apb_clock_speed);
uint32_t duty = ledc_find_suitable_duty_resolution(apb_clock_speed, 10*1000); //APB CLOCK 80Mhz|timer freq 10Khz = max 12bits
ESP_LOGI(TAG,"MAX DUTY RESOLUTION: %lu", duty)

Nesse post não vamos entrar em detalhes sobre as fontes de clock do ESP32, vamos fazer uso apenas do APB.

Pronto, timer configurado, agora vamos configurar a saída PWM, para isso temos que seguir esses passos:

  • crie uma struct de configuração PWM: ledc_channel_config_t, essa struct contem os seguintes campos:
    • gpio_num: número da GPIO que vamos ter a saida PWM
    • speed_mode: o grupo PWM (deve ser a mesma do timer)
    • channel: canal PWM que estamos configurando (0-7, 8 canais totais)
    • intr_type: tipo de interrupção do PWM (nesse post vamos manter em: LEDC_INTR_DISABLE)
    • timer_sel: timer usado para gerar o sinal PWM (deve ser o mesmo timer configurado na struct anterior)
    • duty: resolução do sinal PWM (pode ir de 0 até 2**resolução do timer)
    • hpoint: valor do Hpoint (hpoint um o valor de comparação do timer no ESP32, quando o valor do contador chegar no valor hpoint o sinal vai para HIGH), pode ir de 0 até (2**resolução do timer)-1
    • flags.output_invert: se o sinal deve ser invertido, 1-sim, 0-não

Com a struct configurada, basta chamar a função: “ledc_channel_config(const ledc_channel_config_t*)” passando um ponteiro para nossa struct de configuração.

Uso:

Após a configuração, o sinal PWM vai imediatamente iniciar, podemos alterar o Duty-cycle usando as fuinções:

ledc_set_duty(ledc_mode_t, ledc_channel_t, uint32_t),para carregar o valor para o PWM, recebe: o grupo PWM, canal PWM (os mesmos definidos na configuração) e valor do duty,

ledc_update_duty(ledc_mode_t, ledc_channel_t), para efetivar o PWM selecionado na função anterior, recebe: o grupo PWM, canal PWM.

Para pausar e resumir o sinal PWM usamos as funções:

ledc_timer_pause(ledc_mode_t, ledc_timer_t) e ledc_timer_resume(ledc_mode_t, ledc_timer_t) ambas recebem o grupo PWM e o canal PWM.

Exemplo: saída PWM

Nesse exemplo vamos mostrar o funcionamento básico da saida PWM, controlando o brilho de um LED.

Esquema:

Código:

/*
===================================
ESP PWM exemplo (Introdução ESP-IDF pt2 - Eletrogate blog)
uso basico do PWM
Author: Guilherme Silva Schultz (RecursiveError)
data: 2024-08-13
===================================
*/


// C includes
#include <stdio.h>
#include <math.h>

//ESP-IDF includes
#include <driver/ledc.h>
#include <driver/gpio.h>
#include <esp_err.h>
#include <esp_log.h>
#include <esp_clk_tree.h>
#include <soc/clk_tree_defs.h>

//FreeRTOS includes
#include <freertos/FreeRTOS.h>

#define PWM_CLOCK_SPEED 10*1000 //10Khz
#define PWM_OUT_GPIO GPIO_NUM_2 //saida do PWM
#define PWM_CH LEDC_CHANNEL_0 //canal PWM
#define PWM_TIMER LEDC_TIMER_0 //timer do PWM

uint32_t PWM_init();

void app_main(void){
    const char TAG[] = "main";
    ESP_LOGI(TAG, "INIT PWM");
    uint32_t duty = PWM_init();
    uint32_t max_duty_pwm = pow(2,duty);//obtem o valor maximo do duty-cycle
    ESP_LOGI(TAG, "MAX duty: %lu",max_duty_pwm);
    while(1){
        //aumento em 50 o valor do duty a cada 100ms
        for(uint32_t duty_ac = 0; duty_ac<max_duty_pwm; duty_ac = duty_ac + 50){
            ledc_set_duty(LEDC_LOW_SPEED_MODE, PWM_CH, duty_ac);
            ledc_update_duty(LEDC_LOW_SPEED_MODE, PWM_CH);
            vTaskDelay(pdMS_TO_TICKS(100));
        }
    }

}

uint32_t PWM_init(){
    const char TAG[] = "PWM CONFIG";
    //obtem o a velocidade do clock APB
    uint32_t apb_clock_speed = 0;
    ESP_ERROR_CHECK(esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_APPROX, &apb_clock_speed));
    ESP_LOGI(TAG, "APB CLOCK SPEED: %lu", apb_clock_speed);

    //obtem o maximo de bits da resolução
    uint32_t duty = ledc_find_suitable_duty_resolution(apb_clock_speed, PWM_CLOCK_SPEED); //APB CLOCK 80Mhz/ timer freq 10Khz
    ESP_LOGI(TAG,"MAX DUTY RESOLUTION: %lu", duty);
    

    ledc_timer_config_t pwm_timer_conf = {
        .speed_mode = LEDC_LOW_SPEED_MODE, //pwm de baixa velocidade
        .duty_resolution = duty, //qtd de bits da resolução
        .timer_num = PWM_TIMER, //timer usado
        .freq_hz = PWM_CLOCK_SPEED, //frequencia de 10Khz
        .clk_cfg = LEDC_USE_APB_CLK,//fonte de clock APB
        .deconfigure = false,
    };
    ledc_channel_config_t pwm_ch = {
        .gpio_num = PWM_OUT_GPIO, //pino de saida do sinal PWM
        .speed_mode = LEDC_LOW_SPEED_MODE,//PWM de baixa velocidade
        .channel = PWM_CH,
        .intr_type = LEDC_INTR_DISABLE,//interrupçoes desabilitadas
        .timer_sel = PWM_TIMER,
        .duty = pow(2, duty), //valor maximo possivel de duty com a resolução de bits do timer
        .hpoint = 0,
        .flags.output_invert = 0//outout normal
    };
    //inicia o sinal PWM
    ESP_ERROR_CHECK(ledc_timer_config(&pwm_timer_conf));
    ESP_ERROR_CHECK(ledc_channel_config(&pwm_ch));
    return duty;
}

 

Resultado:

Com esse assunto finalizado podemos ir a o foco do projeto desse Post, o uso de displays LCD gráficos usando LVGL.


Driver: LCD

A esp-idf provem uma interface genérica para trabalhar com displays gráficos, essa interface nos permite usar vários tipos de LCD  gráficos de forma fácil, seja ele: SPI, I2C, PARALELO/ com ou sem touch, e muito mais, nesse post vamos abordar apenas a configuração do display TFT 1.44″, que possui interface SPI, Essa configuração é dividida em 3 etapas: configuração do I/O, configuração da interface do display e configuração do bus de comunicação.

 

Configurando a I/O:

Essa configuração vai dizer para nossa placa como ela deve se comunicar com o display, para isso temos que executar os seguintes passos:

  • Inclua a biblioteca: <esp_lcd_panel_io.h>
  • crie um handler de IO do display: esp_lcd_panel_io_handle_t
  • crie uma struct de configuração: esp_lcd_panel_io_spi_config_t, essa é a struct que vai guardar as informações básicas do nosso display, nessa struct só é necessário configurar os seguintes itens:
    • cs_gpio_num: GPIO do pino CS do display
    • dc_gpio_num: GPIO do pino DC do display (esse pino também pode ser chamado de A0)
    • spi_mode: modo de SPI do display (no nosso caso é modo 0)
    • pclk_hz: velocidade em Hz, do clock de pixel do display (o recomendado para 1.44″ é 15 Mhz)
    • lcd_cmd_bits: quantidade de Bits em comandos do display (no nosso caso é 8)
    • lcd_param_bits: quantidade de Bits em parâmetros do display (no nosso caso é 8)
    • trans_queue_depth: tamanho da fila de transferência (não é uma configuração do display, isso é apenas o tamanho do buffer onde a placa pode salvar as transações)

isso é tudo que temos que fazer por agora, a próxima etapa é configurar a interface do LCD.

Configurando a interface:

Essa é a configuração que vai dizer para nossa placa o formato da informação que ela deve enviar para o display, para isso temos que executar os seguintes passos:

  • Inclua a biblioteca: <esp_lcd_panel_io.h>
  • crie um handler de LCD:  esp_lcd_panel_handle_t
  • crie uma struct de configuração de IO: esp_lcd_panel_dev_config_t, essa struct contem os seguintes campos:
    • reset_gpio_num: GPIO do pino de reset do display
    • .rgb_ele_order: padão de cores do display, pode ser LCD_RGB_ELEMENT_ORDER_RGB  ou LCD_RGB_ELEMENT_ORDER_BGR
    • .bits_per_pixel: quantidade de Bits de cores (nosso display possui 16Bits de cores)

Com o I/O configurado nosso próximo passo é iniciar o bus SPI.

Configurando Bus SPI:

(No último post nos vimos apenas os protocolos UART e I2C, mas não o SPI, isso porque a configuração e uso do SPI é mais complexa que a dos outros protocolos e inclui tópicos ainda não abordados como interrupções e DMA, nesse post vamos fazer apenas a inicialização do Bus e deixar SPI para um próximo post onde falaremos de forma mais aprofundadas das features do ESP32.)

Com o display já configurado o ultimo passo é configurar o periférico usado na comunicação, no nosso caso temos que configurar o SPI, para isso basta seguir os passos:

  • inclua a biblioteca: <driver/spi_master.h>
  •  crie uma struct de configuração SPI: spi_bus_config_t, nessa struct temos que configurar os seguintes campos:
    • sclk_io_num: GPIO do pino SCK do display.
    • mosi_io_num: GPIO do pino SDI do display (esse pino também pode estar marcado como “SDA”).
    • miso_io_num: GPIO do pino SDO do display (nosso display não envia informações então esse pino é marcado como -1)
    • max_transfer_sz: tamanho máximo de transferência, use zero para valor padrão de 4096.

Com todas as structs configuradas podemos iniciar o driver LCD!

Iniciando o driver:

Para iniciar o driver temos que chamar as funções de configuração na seguinte ordem:

  • inicializa o bus SPI:spi_bus_initialize(spi_host_device_t, const spi_bus_config_t*, spi_dma_chan_t), cada argumento dessa função significa:
    1. o HOST SPI para ser inicializado (o ESP32 tem 3 HOST SPI chamados SPIx_HOST, onde X é um valor de 1 a 3)
    2. um ponteiro para struct de configuração bus SPI
    3. canal DMA para o periférico ( use: SPI_DMA_CH_AUTO para configuração automática)
  • salvas as configurações de I/O esp_lcd_new_panel_io_spi(esp_lcd_spi_bus_handle_t, const esp_lcd_panel_io_spi_config_t*, esp_lcd_panel_io_handle_t*) cada argumento dessa função significa:
    1. HOST SPI usado (mesmo configurado na função anterior)
    2. ponteiro para struct de configuração de IO
    3. ponteiro para handler de IO

 

  • inclua a biblioteca do display utilizado e Crie um novo dispositivo LCD: esp_lcd_new_panel_x(const esp_lcd_panel_io_handle_t, const esp_lcd_panel_dev_config_t*, esp_lcd_panel_handle_t*) onde X é o nome do controlador do nosso display, cada argumento dessa função significa:
    1. nosso handler de IO
    2. ponteiro para struct de configuração da interface
    3. ponteiro para nosso handler de LCD

Infelizmente esp-idf não tem uma implementação para o controlador do nosso display, mas podemos instalar um compatível pelo gerenciador de componentes!

Seguindo os passos do primeiro tópico desse post:

  1. abra o gerenciador de componentes
  2. pesquise por: esp_lcd_ili9341  (esse controlador é quase igual a o do nosso display, a única diferença é o suporte a touch-screen, mesmo que nosso display não tenha essa funcionalidade, ainda podemos usar a parte de comunicação)
  3. instale a biblioteca: esp_lcd_ili9341

com a biblioteca instalada:

  1. inclua a biblioteca: <esp_lcd_ili9341.h>
  2. chame a função: esp_lcd_new_panel_ili9341, seguindo o ultimo passo da inicialização do driver.

Inicializando o display:

Com o display configurado, a ultima etapa é inicializar, para isso: chame as funções:  esp_lcd_panel_reset (para resetar as variáveis internas e garantir uma inicialização limpa) e  esp_lcd_panel_init (para inicializar os display), ambas recebem apenas o handler do LCD como argumento, feito isso basta ligar o output do display com a função: esp_lcd_panel_disp_on_off(esp_lcd_panel_handle_t, bool) passando o handler do nosso LCD como primeiro argumento e true com segundo.

Display inicializado com sucesso, agora vamos ver o seu uso com um exemplo pratico!


Exemplo: Cores do display LCD

Nesse Exemplo vamos configurar e usar um display LCD SPI para exibir cores de 16Bits.

Esquema:

(a imagem usa um display 160×128, mas o funcionamento é igual para o display 128×128)

Código:

/*
ESP LCD exemplo (Introdução ESP-IDF pt2 - Eletrogate blog)
uso basico do Driver LCD
Author: Guilherme Silva Schultz (RecursiveError)
data: 2024-08-18
*/


//C includes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//FreeRTOS includes
#include <freertos/FreeRTOS.h>

//ESP-IDF includes
#include <esp_log.h>
#include <esp_err.h>
#include <driver/gpio.h>
#include <driver/spi_master.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_lcd_panel_ops.h>

//extra includes
#include <esp_lcd_ili9341.h>


//LCD IO DEFINES
#define LCD_LED_PIN   GPIO_NUM_23
#define LCD_SCK_PIN   GPIO_NUM_22 
#define LCD_SDA_PIN   GPIO_NUM_21
#define LCD_A0_PIN    GPIO_NUM_19
#define LCD_RESET_PIN GPIO_NUM_18
#define LCD_CS_PIN    GPIO_NUM_5
#define LCD_SPI_BUS   SPI2_HOST
#define LCD_H_RES     128 //resolução horizontal do display
#define LCD_V_RES     128 //resolução vertical do display

//lcd global vars
esp_lcd_panel_handle_t lcd_handler;
esp_lcd_panel_io_handle_t lcd_io_handler;

uint16_t color_buf[LCD_H_RES][LCD_V_RES]; //buffer de pixels

void GPIO_init();
void LCD_init();

void app_main(void){
    ESP_LOGI("main", "init GPIOs start");
    GPIO_init();
    ESP_LOGI("main", "init GPIOs done, init LCD");
    LCD_init();
    ESP_LOGI("main", "init LCD done");
    ESP_LOGI("main", "START MAIN LOOP");
    uint16_t color = 0;
    while(1){
        //passa por todas as cores de 16Bits
        for(uint16_t red = 0; red < 31; red++){
            for(uint16_t green = 0; green < 63; green++){
                for(uint16_t blue = 0; blue < 31; blue++){
                    color = (red<<11) | (green<<5) | blue;
                    memset(color_buf, color, sizeof(color_buf)); //salva a cor no buffer
                    esp_lcd_panel_draw_bitmap(lcd_handler, 0, 0, LCD_H_RES, LCD_V_RES, color_buf); //envia um buffer de pixels para o display
                    vTaskDelay(pdMS_TO_TICKS(155));
                }
            }
        }
    }

}

void GPIO_init(){
    gpio_config_t bk_conf = {
        .pin_bit_mask = 1<<LCD_LED_PIN,
        .mode = GPIO_MODE_OUTPUT,
        .intr_type = GPIO_INTR_DISABLE,
        .pull_down_en = 0,
        .pull_up_en = 0,
    };
    ESP_ERROR_CHECK(gpio_config(&bk_conf));
}

void LCD_init() {
    spi_bus_config_t spi_bus_conf ={
        .sclk_io_num = LCD_SCK_PIN,
        .mosi_io_num = LCD_SDA_PIN,
        .miso_io_num = -1,
        .max_transfer_sz = 0,
    };
    esp_lcd_panel_io_spi_config_t lcd_io_spi_conf = {
        .cs_gpio_num = LCD_CS_PIN,
        .dc_gpio_num = LCD_A0_PIN,
        .spi_mode = 0,
        .pclk_hz = 15*1000*1000, //15Mhz
        .lcd_cmd_bits = 8,
        .lcd_param_bits = 8,
        .trans_queue_depth =  10
    };

    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = LCD_RESET_PIN,
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
        .bits_per_pixel = 16
    };
    ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_BUS, &spi_bus_conf, SPI_DMA_CH_AUTO));
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi( (esp_lcd_spi_bus_handle_t) LCD_SPI_BUS,&lcd_io_spi_conf, &lcd_io_handler));
    ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(lcd_io_handler, &panel_config, &lcd_handler));

    //reseta as variáveis internas do display para uma inicialização limpa
    ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_handler));
    //inicia o display
    ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_handler));
    //liga o LED do display
    ESP_ERROR_CHECK(gpio_set_level(LCD_LED_PIN, 1));
    //liga o output do display
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(lcd_handler, true));
}

alguns tópicos importantes desse código:

  • o pino LED do display é só uma GPIO, onde podemos usar HIGH(1) para ligar a luz de fundo e LOW(0) para desligar.
  • todas as funções para uso do display estão no biblioteca: <esp_lcd_panel_ops.h>
  • todas as funções para uso do display recebem um handler do LCD.
  • a função esp_lcd_panel_draw_bitmap responsável pelo envio de imagens para o display, pode enviar vários pixels de uma só vez.

Resultado:

Nos conseguimos enviar dados para o display, mas como monta uma interface a partir disso? manipular diretamente um array de cores para exibir informações é uma tarefa muito complexa para o dia a dia,  por sorte, existem soluções prontas para isso, se você Arduino a algum tempo, deve conhecer coisas como Adafruit GFX, uma biblioteca gráfica muito popular na comunidade maker, mas apesar de sua facilidade de uso, a Adafruit GFX não tem tantas funcionalidades quando se fala da criação de interfaces gráficas profissionais, para esses casos existe a:


Light and Versatile Graphics Library

Light and Versatile Graphics Library ou LVGL é uma das bibliotecas gráficas mais famosas entre profissionais de sistemas embarcados, ela permite a criação de UIs leves, altamente personalizáveis, interativas e portáveis em dispositivos com recursos limitados, é usado por Grandes empresas de tecnologia como exemplo: ST, NXP, Xiaomi, Samsung até a Espressif fabricante dos ESP32 recomenda o uso dessa biblioteca,  e a cereja do bolo: é totalmente grátis e open-source!

Instalando LVGL:

Podemos instalar a LVGL direto pelo gerenciador de componentes:

  • abra o gerenciador de componentes.
  • pesquise por LVGL
  • instale a versão 9.1.0

Setup basico:

A LVGL já possui APIs prontas para seu uso com ESP-IDF e FreeRTOS, mas nesse post vamos ver como criar um porte do zero para usar LVGL.

(não se preocupe em entender tudo agora, assim como no post passado, vamos fazer um breve introdução e posteriormente abordar com mais detalhes)

para iniciar o uso da LVGL temos que chamar a função: lv_init() e implementar 4 funções básicas:

  • uma função de contagem de tempo que chame: lv_tick_inc(x) (onde x é o tempo um Ms decorrido), nos já vimos sobre como timers podem gerar eventos periódicos, podemos usar esp_timer para gerar um evento que chame: lv_tick_inc
  • uma função que chame periodicamente: lv_timer_handler, essa função retorna o tempo em Ms que voce precisa esperar para chamar lv_timer_handler novamente, podemos usar uma task para isso chamando a função: lv_timer_handler em um loop com  vTaskDelay(pdMS_TO_TICKS(x)) (onde X é o valor retornado por lv_timer_handler)
  • uma função que notifique que o display está pronto para receber dados com: lv_display_flush_ready(display_lvgl), podemos usar os campos: on_color_trans_done e user_ctx da struct de IO do lcd:
     esp_lcd_panel_io_spi_config_t para chamar o evento de notificação do LVGL
  • uma função que receba os dados do LVGL e  envie para o display, podemos usar a função: esp_lcd_panel_draw_bitmap vista no exemplo anterior para isso

com essas 4 funções implementadas já podemos fazer usa da LGVL, vamos ver um exemplo dessa implementação da LVGL e fazer um Hello world.

Exemplo implementação e Hello world:

Seguindo o mesmo esquema usado no exemplo de LCD:

vamos escrever o seguinte código:

/* LVGL hello world exemplo (Introdução ESP-IDF pt2 - Eletrogate blog) 
setup basico da LVGL + Hello world
Author: Guilherme Silva Schultz (RecursiveError) 
data: 2024-08-21 
*/

//C includes
#include <stdio.h>
#include <stdlib.h>

//FreeRTOS includes
#include <freertos/FreeRTOS.h>


//ESP_IDF includes
#include "esp_timer.h"
#include <driver/spi_master.h>
#include <driver/gpio.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_err.h>
#include <esp_log.h>


//extra includes
#include <lvgl.h>
#include <esp_lcd_ili9341.h>


//SPI CONFIG
#define LCD_LED_PIN   GPIO_NUM_23
#define LCD_SCK_PIN   GPIO_NUM_22 
#define LCD_SDA_PIN   GPIO_NUM_21
#define LCD_A0_PIN    GPIO_NUM_19
#define LCD_RESET_PIN GPIO_NUM_18
#define LCD_CS_PIN    GPIO_NUM_5
#define LCD_SPI_BUS   SPI2_HOST
#define LCD_H_RES     128 //resolução horizontal do display
#define LCD_V_RES     128 //resolução vertical do display



//=============================== init functions ===============================

void GPIO_init();
void LCD_init(spi_host_device_t host, esp_lcd_panel_io_handle_t *io_handler, esp_lcd_panel_handle_t *p_handler, lv_display_t *disp);
void timer_init();
void LVGL_init(lv_display_t **display, lv_draw_buf_t *display_bufs[2], esp_lcd_panel_handle_t *lcd_handler);


//=============================== LVGL functions ===============================
bool lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx); //avisa o LVGL que o display está pronto para receber novos dados
void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map); //envia os novos dados para o display
void lvgl_tick_inc(void *arg); //incrementa o timer interno do LVGL
void lv_example_get_started_1();

void app_main(void){
    //LCD handlers
    esp_lcd_panel_io_handle_t io_handler = NULL;
    esp_lcd_panel_handle_t lcd_handler = NULL;

    //LVGL handlers
    lv_display_t *display = NULL;
    lv_draw_buf_t *bufs[2] = {NULL, NULL};
    
    GPIO_init();
    LVGL_init(&display, bufs, &lcd_handler);
    LCD_init(LCD_SPI_BUS,&io_handler,&lcd_handler, display);
    timer_init();
    lv_example_get_started_1();
    uint32_t task_delay_ms = 0;
    while (1) { 
        //espera o tempo indicado por lv_timer_handler
        task_delay_ms = lv_timer_handler(); 
        vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
    }

}
//=============================== init functions ===============================

void GPIO_init(){
    //configura o led do display
    gpio_config_t conf_output = {
        .pin_bit_mask = 1 << LCD_LED_PIN,
        .mode = GPIO_MODE_OUTPUT
    };
    gpio_config(&conf_output);
}

void LCD_init(spi_host_device_t host, esp_lcd_panel_io_handle_t *io_handler, esp_lcd_panel_handle_t *p_handler, lv_display_t *disp){
    spi_bus_config_t spi_bus_conf ={
        .sclk_io_num = LCD_SCK_PIN,
        .mosi_io_num = LCD_SDA_PIN,
        .miso_io_num = -1,
        .max_transfer_sz = 0,
    };
    esp_lcd_panel_io_spi_config_t lcd_io_spi_conf = {
        .cs_gpio_num = LCD_CS_PIN,
        .dc_gpio_num = LCD_A0_PIN,
        .spi_mode = 0,
        .pclk_hz = 15*1000*1000, //15Mhz
        .lcd_cmd_bits = 8,
        .lcd_param_bits = 8,
        .trans_queue_depth =  10,
        .on_color_trans_done = lvgl_flush_ready, //chama o evento que indica o estado do display para a LVGL
        .user_ctx = disp
    };

    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = LCD_RESET_PIN,
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR,
        .bits_per_pixel = 16
    };

    ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_BUS, &spi_bus_conf, SPI_DMA_CH_AUTO));
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi( (esp_lcd_spi_bus_handle_t) LCD_SPI_BUS, &lcd_io_spi_conf, io_handler));
    ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(*io_handler, &panel_config, p_handler));

    //reseta as variaveis internas do display para uma inicialização limpa
    ESP_ERROR_CHECK(esp_lcd_panel_reset(*p_handler));
    //inicia o display
    ESP_ERROR_CHECK(esp_lcd_panel_init(*p_handler));
    //liga o LED do display
    ESP_ERROR_CHECK(gpio_set_level(LCD_LED_PIN, 1));
    //liga o output do display
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(*p_handler, true));
    ESP_ERROR_CHECK(esp_lcd_panel_invert_color(*p_handler, false));
}

void LVGL_init(lv_display_t **display, lv_draw_buf_t *display_bufs[2], esp_lcd_panel_handle_t *lcd_handler){
    lv_init();
    //cria um display LVGL
    *display = lv_display_create(LCD_H_RES, LCD_V_RES);

    //cria buffers de escrita do display LVGL
    display_bufs[0] = lv_draw_buf_create(LCD_H_RES,LCD_V_RES, LV_COLOR_FORMAT_RGB565, 0);
    display_bufs[1] = lv_draw_buf_create(LCD_H_RES,LCD_V_RES, LV_COLOR_FORMAT_RGB565, 0);

    //envia nosso LCD handler como argumento para os eventos do LVGL
    lv_display_set_user_data(*display, lcd_handler);
    lv_display_set_flush_cb(*display, lvgl_flush_cb);
    lv_display_set_draw_buffers(*display, display_bufs[0], display_bufs[1]);
    lv_display_set_color_format(*display, LV_COLOR_FORMAT_RGB565);
}

void timer_init(){
        const esp_timer_create_args_t lvgl_tick_timer_args = {
        .callback = &lvgl_tick_inc, //evento que vai indicar a passagem de tempo para a LVGL
        .name = "lvgl_tick"
    };
    esp_timer_handle_t lvgl_tick_timer = NULL;
    ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, 1 * 1000)); //chama o evento a cada 1Ms
}

//=============================== LVGL functions ===============================

bool lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx){
    lv_display_t *disp_driver = (lv_display_t *)user_ctx;
    lv_display_flush_ready(disp_driver); //avisa a LVGL que o display está pronto para receber dados
    return false;
}

void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map){
    esp_lcd_panel_handle_t *lcd_handler = (esp_lcd_panel_handle_t *)lv_display_get_user_data(disp);
    if(lcd_handler != NULL){
        esp_lcd_panel_draw_bitmap(*lcd_handler,area->x1, area->y1, area->x2+1, area->y2+1, px_map); //envia os dados da LVGL para o display
    }
}


void lvgl_tick_inc(void *arg){
    lv_tick_inc(1); //indica para LVGL que passou 1Ms
}


//Esse exemplo foi tirado da documentação do LVGL:
//links: https://github.com/lvgl/lvgl/blob/b78a4de8984e7e9b76ec4fc0e437fc952435f433/examples/get_started/lv_example_get_started_1.c
void lv_example_get_started_1(){

        /*Change the active screen's background color*/
    lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x003a57), LV_PART_MAIN);

    /*Create a white label, set its text and align it to the center*/
    lv_obj_t * label = lv_label_create(lv_screen_active());
    lv_label_set_text(label, "Hello world");
    lv_obj_set_style_text_color(lv_screen_active(), lv_color_hex(0xffffff), LV_PART_MAIN);
    lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}

 

Resultado:

considerações:

  • LVGL não é thread-safe, evite uso de variáveis globais e sempre use mutex antes de manipular o display LVGL.
  • buffers criados com: lv_draw_buf_create devem ter no mínimo 1/10 da resolução da placa, use 0 para configuração automática.
  • somente um buffer é necessário para a configuração: lv_display_set_draw_buffers, o segundo é opcional, podendo ser NULL caso não utilizado.

Projeto: LVGL click counter

Vamos juntar tudo que nos vimos até agora e criar um projeto que contem a quantidade de vezes que um botão foi pressionado usando LVGL

Esquema:

Código:

//C includes
#include <stdio.h>
#include <stdlib.h>

//FreeRTOS includes
#include <freertos/FreeRTOS.h>

//ESP_IDF includes
#include "esp_timer.h"
#include <driver/spi_master.h>
#include <driver/gpio.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_err.h>
#include <esp_log.h>


//extra includes
#include <lvgl.h>
#include <esp_lcd_ili9341.h>


//SPI CONFIG
#define LCD_LED_PIN   GPIO_NUM_23
#define LCD_SCK_PIN   GPIO_NUM_22 
#define LCD_SDA_PIN   GPIO_NUM_21
#define LCD_A0_PIN    GPIO_NUM_19
#define LCD_RESET_PIN GPIO_NUM_18
#define LCD_CS_PIN    GPIO_NUM_5
#define LCD_SPI_BUS   SPI2_HOST
#define LCD_H_RES     128 //resolução horizontal do display
#define LCD_V_RES     128 //resolução vertical do display

//LVGL inputs
#define LVGL_INC_BTN GPIO_NUM_17


//=============================== init functions ===============================

void GPIO_init();
void LCD_init(spi_host_device_t host, esp_lcd_panel_io_handle_t *io_handler, esp_lcd_panel_handle_t *p_handler, lv_display_t *disp);
void timer_init();
void LVGL_init(lv_display_t **display, lv_draw_buf_t *display_bufs[2], esp_lcd_panel_handle_t *lcd_handler, lv_indev_t **input_device);


//=============================== LVGL functions ===============================
bool lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx); //avisa o LVGL que o display está pronto para receber novos dados
void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map); //envia os novos dados para o display
void lvgl_tick_inc(void *arg); //incrementa o timer interno do LVGL
void lvgl_example();
void lvgl_btn_read(lv_indev_t * indev, lv_indev_data_t*data);

//LVGL global vars
const lv_point_t btn_coords[] = {{10,10}, {1,1}};

void app_main(void){
    //LCD handlers
    esp_lcd_panel_io_handle_t io_handler = NULL;
    esp_lcd_panel_handle_t lcd_handler = NULL;

    //LVGL handlers
    lv_display_t *display = NULL;
    lv_draw_buf_t *bufs[2] = {NULL, NULL};
    lv_indev_t *input_device = NULL;
    
    GPIO_init();
    LVGL_init(&display, bufs, &lcd_handler, &input_device);
    LCD_init(LCD_SPI_BUS,&io_handler,&lcd_handler, display);
    timer_init();
    lvgl_example();
    uint32_t task_delay_ms = 0;
    while (1) { 
        task_delay_ms = lv_timer_handler();
        vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
    }

}
//=============================== init functions ===============================

void GPIO_init(){

    gpio_config_t conf_output = {
        .pin_bit_mask = 1 << LCD_LED_PIN,
        .mode = GPIO_MODE_OUTPUT
    };
    gpio_config(&conf_output);

    gpio_config_t conf_input = {
        .pin_bit_mask = (1 << LVGL_INC_BTN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE
    };
    gpio_config(&conf_input);
}

void LCD_init(spi_host_device_t host, esp_lcd_panel_io_handle_t *io_handler, esp_lcd_panel_handle_t *p_handler, lv_display_t *disp){
    spi_bus_config_t spi_bus_conf ={
        .sclk_io_num = LCD_SCK_PIN,
        .mosi_io_num = LCD_SDA_PIN,
        .miso_io_num = -1,
        .max_transfer_sz = 0,
    };
    esp_lcd_panel_io_spi_config_t lcd_io_spi_conf = {
        .cs_gpio_num = LCD_CS_PIN,
        .dc_gpio_num = LCD_A0_PIN,
        .spi_mode = 0,
        .pclk_hz = 15*1000*1000, //15Mhz
        .lcd_cmd_bits = 8,
        .lcd_param_bits = 8,
        .trans_queue_depth =  10,
        .on_color_trans_done = lvgl_flush_ready,
        .user_ctx = disp
    };

    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = LCD_RESET_PIN,
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR,
        .bits_per_pixel = 16
    };

    ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_BUS, &spi_bus_conf, SPI_DMA_CH_AUTO));
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi( (esp_lcd_spi_bus_handle_t) LCD_SPI_BUS, &lcd_io_spi_conf, io_handler));
    ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(*io_handler, &panel_config, p_handler));

    //reseta as variaveis internas do display para uma inicialização limpa
    ESP_ERROR_CHECK(esp_lcd_panel_reset(*p_handler));
    //inicia o display
    ESP_ERROR_CHECK(esp_lcd_panel_init(*p_handler));
    //liga o LED do display
    ESP_ERROR_CHECK(gpio_set_level(LCD_LED_PIN, 1));
    //liga o output do display
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(*p_handler, true));
    ESP_ERROR_CHECK(esp_lcd_panel_invert_color(*p_handler, false));
}

void LVGL_init(lv_display_t **display, lv_draw_buf_t *display_bufs[2], esp_lcd_panel_handle_t *lcd_handler, lv_indev_t **input_device){
    lv_init();
    //cria um display LVGL
    *display = lv_display_create(LCD_H_RES, LCD_V_RES);

    //cria buffers de escrita do display LVGL
    display_bufs[0] = lv_draw_buf_create(LCD_H_RES,LCD_V_RES, LV_COLOR_FORMAT_RGB565, 0);
    display_bufs[1] = lv_draw_buf_create(LCD_H_RES,LCD_V_RES, LV_COLOR_FORMAT_RGB565, 0);

    //envia nosso LCD handler como argumento para os eventos do LVGL
    lv_display_set_user_data(*display, lcd_handler);
    lv_display_set_flush_cb(*display, lvgl_flush_cb);
    lv_display_set_draw_buffers(*display, display_bufs[0], display_bufs[1]);
    lv_display_set_color_format(*display, LV_COLOR_FORMAT_RGB565);

    *input_device = lv_indev_create();
    lv_indev_set_type(*input_device, LV_INDEV_TYPE_BUTTON);
    lv_indev_set_read_cb(*input_device, lvgl_btn_read);
    lv_indev_set_button_points(*input_device, btn_coords);
}

void timer_init(){
        const esp_timer_create_args_t lvgl_tick_timer_args = {
        .callback = &lvgl_tick_inc,
        .name = "lvgl_tick"
    };
    esp_timer_handle_t lvgl_tick_timer = NULL;
    ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, 2 * 1000));
}

//=============================== LVGL functions ===============================

bool lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx){
    lv_display_t *disp_driver = (lv_display_t *)user_ctx;
    lv_display_flush_ready(disp_driver);
    return false;
}

void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map){
    esp_lcd_panel_handle_t *lcd_handler = (esp_lcd_panel_handle_t *)lv_display_get_user_data(disp);
    if(lcd_handler != NULL){
        esp_lcd_panel_draw_bitmap(*lcd_handler,area->x1, area->y1, area->x2+1, area->y2+1, px_map);
    }
}


void lvgl_tick_inc(void *arg){
    lv_tick_inc(2);
}


//Esse exemplo foi tirado da documentação do LVGL:
//links:
//btn counter: https://github.com/lvgl/lvgl/blob/b78a4de8984e7e9b76ec4fc0e437fc952435f433/examples/get_started/lv_example_get_started_2.c 
//btn indev: https://docs.lvgl.io/master/porting/indev.html#button
static void btn_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * btn = lv_event_get_target(e);
    if(code == LV_EVENT_PRESSED) {
        static uint8_t cnt = 0;
        cnt++;

        /*Get the first child of the button which is the label and change its text*/
        lv_obj_t * label = lv_obj_get_child(btn, 0);
        lv_label_set_text_fmt(label, "Button: %d", cnt);
    }
}

void lvgl_example(){

    lv_obj_t * btn = lv_button_create(lv_screen_active());     /*Add a button the current screen*/
    lv_obj_set_pos(btn, 5, 10);                            /*Set its position*/
    lv_obj_set_size(btn, 120, 50);                          /*Set its size*/
    lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_ALL, NULL);           /*Assign a callback to the button*/

    lv_obj_t * label = lv_label_create(btn);          /*Add a label to the button*/
    lv_label_set_text(label, "Button: 0");                     /*Set the labels text*/
    lv_obj_center(label);
}

void lvgl_btn_read(lv_indev_t * indev, lv_indev_data_t*data){
    int btn_state = gpio_get_level(LVGL_INC_BTN);
    data->state = LV_INDEV_STATE_RELEASED;
    if(btn_state == 0){
        data->state = LV_INDEV_STATE_PRESSED;
    }
    data->btn_id = 0;
}

Resultado:

Com isso finalizamos a segunda parte da ESP-IDF, dúvidas, dicas e sugestões são sempre bem vindas nos comentários, obrigado por ler até aqui e até a próxima!


Sobre o Autor


Guilherme Schultz
LinkedIn

Apaixonado por tecnologia, autodidata em eletrônica e desenvolvimento embarcado.


Eletrogate

21 de novembro de 2024

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!