Nesse post vamos aprender sobre a ESP-IDF, a framework nativa para desenvolvimento no ESP32, explicando seu uso, as diferenças entre a IDF e a Arduino, instalação no Visual Studio Code e a criação de projetos, antes de iniciar a leitura é importante ressaltar que a IDF é uma framework pensada no uso profissional, sendo recomendado ter conhecimento nos seguintes tópicos:
Nesse post vamos usar a placa DOIT ESP32 DevKit V1 e componentes discretos como LEDs e resistores, além de um display I2C. Vale lembrar que é possível usar qualquer placa baseada nos SoCs da família ESP32, ou seja, as linhas ESP32, ESP32-S, ESP32-C, ESP32-H e ESP32-P.
Apenas por curiosidade, as placas baseadas no ESP8266, como o ESP-01 e ESP-12 (NodeMCU) usam uma SDK diferente da ESP-IDF, a ESP8266 Non-OS SDK
Abaixo, a lista completa de materiais utilizados:
A Espressif IoT Development Framework ou simplesmente ESP-IDF, é a SDK nativa para a família ESP32, criada pela Espressif. Ela é um conjunto completo de ferramentas para todo o ciclo de desenvolvimento de uma Aplicação IoT, desde o protótipo até o produto final, contendo:
Além disso, a ESP-IDF também dá suporte total a linguagem C e sua biblioteca padrão, desde <stdio.h> até POSIX Sockets, então se você tem experiência com desenvolvimento C em plataformas POSIX (Linux, Mac, FreeBSD, etc.) vai poder aproveitar todo seu conhecimento!
A ESP-IDF também possui suporte total a C++20 (com algumas pequenas limitações) e a STL, isso mesmo que você leu, suporte total, RTTI, Exception Handling até mesmo Multithreading tudo disponível para você usar, diferente da Arduino que apenas exige um suporte parcial a C++.
Falando em Arduino…
Assim como a Arduino IDE, a ESP-IDF tem ferramentas para facilitar o desenvolvimento, então quando usar um ou outro?
A Arduino IDE tem suporte a mais arquiteturas e placas, diferente da IDF que só suporta as família ESP32, mas isso vem com um preço: para conseguir suportar tantas placas, a Arduino criou uma padronização das funcionalidades comuns de um microcontrolador (I/O básico e alguns protocolos de comunicação). Por conta dessa padronização, hardwares com funcionalidades mais avançadas como o próprio ESP32 acabam ficando bastante limitados. No caso da IDF, como apenas precisa suportar um número pequeno de placas, é mais fácil criar APIs para extrair 100% do hardware!
Então se você vai criar um código que precisa rodar em várias placas diferentes, o Arduino é a melhor escolha, mas se seu objetivo é criar um produto com ESP32, a ESP-IDF é o melhor caminho.
Agora que aprendemos o que é a ESP-IDF, vamos para a instalação.
Para instalar o VS Code, basta ir até o site oficial: Visual Studio Code – Code Editing. Redefined., clicar no botão azul para iniciar o download ou na setinha, para escolher o sistema. Após o download, apenas execute o instalador e siga as instruções, não se preocupe, pois a instalação é bem simples.
Com o VS Code instalado, temos que instalar as seguintes extensões:
Para instalar uma extensão no VS Code, clique no menu de extensões, vá até a barra de pesquisa, digite o nome da extensão, selecione a extensão e clique em “Install”:
Feito isso, feche o programa e abra novamente.
Antes de instalar a IDF é necessário a instalação de alguns programas dependendo do seu sistema operacional:
Windows: talvez seja necessário instalar C++ Build Tools (dependendo da sua versão do Windows isso já vem instalado).
Se você utiliza Linux ou MacOS, confira os pré-requisitos acessando os links a seguir:
Linux: Pré-requisitos
MacOS: Pré-requisitos
Feito isso, vamos instalar a ESP-IDF. Clique no ícone da Espressif e aguarde um momento. Caso o menu de Setup não abra, clique em “Configure ESP-IDF” e, em sequida, selecione a opção “EXPRESS”.
Seguindo, vamos ser direcionados a outro menu, aonde vamos apenas clicar em “Find ESP-IDF in your system” e selecionar a última versão de release – caso exista uma nova versão no momento em que você lê este post, pode instalar sem problemas – clicando em “install“, na sequência.
Agora é só aguarda a instalação, isso pode demorar dependendo da sua velocidade de internet.
Com a IDF já instalada podemos ir para a próxima etapa: a criação de um projeto!
Para criar um novo projeto, clique no ícone da Espressif. Isso vai abrir o menu inicial, que nos retornará algumas opções:
Configure Extension: reparar problemas de instalação e alterar o comportamento da extensão.
New Project: Menu para a criação de novos projetos.
Import Project: Abrir um projeto já existente.
Show Examples: Criar um projeto usando um exemplo.
Components Manager: Baixar e gerenciar componentes externos.
Nesse tutorial, vamos focar apenas na criação de projetos. Clicando em “New Project” vamos ser redirecionados para outro menu:
Esse outro menu é bem intuitivo, não tem segredo. A primeira opção é o nome do projeto (vamos colocar “primeiro-projeto”), a segunda é a pasta em que vai ficar esse projeto, que vamos deixar na opção padrão mesmo. Na opção “Choose ESP-IDF Board“, vamos selecionar nossa placa, a usada nesse projeto é a primeira opção, ESP-WROVER-KT 3.3V. Logo abaixo selecionamos a porta que nossa placa está, você pode selecionar a porta agora ou configurar após criar o projeto.
Feito isso, vamos clicar na opção “Choose Template” para abrir o menu de Templates. A primeira caixa nos permite alterar a Opção “Extension” para “ESP-IDF“, para selecionar a lista de templates, “Extension” para templates-base e “ESP-IDF” para criar o projeto com um exemplo. Para TODOS os projetos demonstrados neste post vamos deixar selecionada a opção padrão e usar o template “template-app“. Agora basta clicar em “Create Project” e clicar em “sim”/”yes” na caixinha de diálogo seguinte, que pergunta se queremos abrir o projeto, e pronto! Já podemos dar início aos primeiros passos com a IDF!
Com o projeto criado, vamos nos deparar com vários arquivos, no momento vamos alterar apenas o arquivo principal, localizado no diretório main/main.c.
Antes de escrevemos nosso primeiro código, vamos fazer uma pequena lista dos comandos básicos:
Para testar se esta tudo ok, a primeira coisa que devemos fazer quando criar um projeto é fazer a build dele, ou seja, a compilação do código. Na primeira vez que um projeto é compilado, a ESP-IDF vai criar os arquivos de configuração e pré-compilar as bibliotecas da IDF. É um processo demorado, mas não se preocupe, esse processo só precisa ser feito uma vez a cada mudança no arquivo “sdkconfig”.
Para iniciar, clique no comando de build (comando n.6) e aguarde. Após compilado, uma caixa de texto será exibida perguntando como queremos fazer o upload para placa. Em nosso caso, vamos selecionar “UART”.
Macete: caso o projeto compile sem problemas, mas o VS Code acusar erro de #includes, apenas reinicie o programa. Esse problema pode acontecer a qualquer momento.
O resultado desse processo é muito interessante: primeiro podemos ver que foi criado um arquivo chamado “sdkconfig“, esse arquivo guarda todas as configurações do nosso projeto, desde features do hardware até as configurações do FreeRTOS (configurações do FreeRTOSConfig.h), além disso, podemos ver que mesmo com nenhum código escrito no arquivo main.c, temos algumas informações sendo escritas no terminal:
Essas informações são chamadas de Logs, e é sobre isso que vamos falar agora!
Como já comentamos, a ESP-IDF tem como foco o uso profissional, e por isso uma das características mais importantes dela é o seu sistema de geração de logs e tratamento de erros, uma característica crucial na etapa de prototipação. Não precisamos dominar esse assunto por agora, mas é bom entender a base desse conceito, pois toda IDF faz uso desse sistema.
Logs são registros dos eventos do sistema, como informações do hardware, avisos ou até erros. Logs são essenciais para o monitoramento de detecção de problemas, se você tem experiência com Arduino já deve ter feito algo parecido com isso: é muito comum usar “Serial.print()” para obter informações da execução do nosso código, e pensando nisso a ESP-IDF configura por padrão a UART0 para este fim (pode ser alterado nas configurações da SDK), ou seja, sem carregar nenhum código conseguimos escrever informações na tela.
Vamos escrever um clássico “hello world” para mostrar essa característica:
#include <stdio.h> void app_main(void) { printf("hello world\n\r"); }
Para gerar logs na ESP-IDF, devemos incluir a biblioteca <esp_log.h>.
essa biblioteca vai disponibilizar 5 macros, cada uma com nível de verbosidade:
ESP_LOGE
para erros.ESP_LOGW
para avisos.ESP_LOGI
para informações gerais.ESP_LOGD
para informações de Debug.ESP_LOGV
para detalhes adicionais.Todas essas macros possuem as mesmas assinaturas: (TAG, string, args) onde TAG é o nome do evento que gerou o Log. String e args são o mesmo que no printf, desse jeito:
ESP_LOGx("Taks1", "texto: %d %d", var1, var2.....);
, onde “x” é o nível de verbosidade (E/W/I/D/V).
A essa altura você já aprendeu a como gerar logs, mas quais as vantagens de se usar logs ao invés de um simples print? Logs são uma forma mais sofisticada de prints de debug, além de ter os mesmos mecanismos de formatação do printf. Eles mostram o nome do evento e o momento que o log foi gerado (muito útil quando lidamos com um sistema multitarefa, como a ESP-IDF), e tem a maior vantagem de todas: podem ser desativadas no arquivo de configuração. Quando estamos na etapa de prototipação, queremos receber o máximo de informação possíveis sobre o funcionamento, mas quando o produto está finalizado, não queremos que o usuário veja essas informações. Se optarmos por usar prints no processo de prototipação, teremos que, manualmente, removê-los antes de lançar o produto, mas se estivermos usando Logs, podemos deixá-los no código e controlar seu funcionamento pelo menuconfig, da seguinte forma:
Esse tópico é bem extenso, como a ESP-IDF oferece suporte completo tanto a C quanto a C++, erros do padrão POSIX (errno) e Exceptions do C++ são permitidos, nesse post faremos apenas uma introdução ao assunto, então vamos ficar apenas na biblioteca <esp_err.h>
.
Assim como no FreeRTOS temos funções que retornam: pdPASS
e pdFAIL.
Na ESP-IDF temos ESP_OK, ESP_FAIL e ESP_ERR_x (onde X é o nome do erro). A grande maioria das funções da IDF retornam o tipo esp_err_t
, que representa um dos valores citados acima, além disso, também temos algumas macros para nos ajudar a lidar com erros:
ESP_ERROR_CHECK(x);
caso X seja um erro, a execução do código é interrompida.ESP_ERROR_CHECK_WITHOUT_ABORT(x);
caso X seja um erro, gera um log de erro, mas continua a execução.ESP_RETURN_ON_ERROR(x, TAG, "fail reason");
caso X seja um erro, exibe uma mensagem personalizada e chama: return x;
.ESP_GOTO_ON_ERROR(x, label, TAG, "fail reason");
caso X seja um erro, exibe uma mensagem personalizada e chama: goto label;
.Vamos ver essas macros em vários pontos no decorrer desse post. Terminadas as introduções, vamos desenvolver nosso primeiro projeto, para fixar o que foi aprendido até aqui!
Esse projeto é apenas um exercício de fixação dos tópicos recém-abordados. Nesse código vamos criar duas task que exibem mensagens de logs a cada 5 segundos e configurar a exibição deles.
//libC includes #include <stdio.h> //FreeRTOS includes #include <freertos/FreeRTOS.h> #include <freertos/task.h> //ESP-IDF includes #include <esp_log.h> //task para teste void task1(void*); void task2(void*); void app_main(void){ static const char *TAG = "app_main"; //tag da main if(xTaskCreatePinnedToCore(task1,"task1",2048,NULL, 1,NULL, 0) != pdPASS){ ESP_LOGE(TAG, "ERRO A iniciar a task1"); }else{ ESP_LOGI(TAG, "task1 criada com sucesso"); } if(xTaskCreatePinnedToCore(task2,"task2",2048,NULL, 1,NULL, 0) != pdPASS){ ESP_LOGE(TAG, "ERRO A iniciar a task2"); }else{ ESP_LOGI(TAG, "task2 criada com sucesso"); } while(true){ vTaskDelay(pdMS_TO_TICKS(10000)); //10Seg de delay printf("hello world\n\r"); } } void task1(void *args){ static const char *TAG = "Task1"; //tag da task1 while(true){ vTaskDelay(pdMS_TO_TICKS(5000)); //5Seg de delay ESP_LOGE(TAG,"Logs de ERRO tem a cor vermelha!"); ESP_LOGW(TAG,"Logs de AVISO tem a cor amarela!"); ESP_LOGI(TAG,"Logs de INFO tem a cor verde!"); } } void task2(void *args){ static const char *TAG = "Task2";//tag da task2 while(true){ vTaskDelay(pdMS_TO_TICKS(5000)); //5Seg de delay ESP_LOGD(TAG,"Logs de DEBUG tem a cor Branca e alta verbosidade"); ESP_LOGV(TAG,"Logs VERBOSOS tem a cor Branca e a maior verbosidade, são usadas para mensagens grandes e detalhadas sobre o funcionamento do aplicação \ assim como essa mensagem, por padrão Logs verbosos e debug estão desabilitados"); } }
Veja no vídeo abaixo o funcionamento:
Agora que já temos uma noção básica de como a ESP-IDF funciona, vamos para próxima etapa: controlar os periféricos da placa!
a ESP-IDF padroniza as APIs de acesso aos periféricos por meio de drivers. Esses drivers servem para facilitar o uso dos recursos de hardware e a portabilidade entre SoCs da família ESP32.
Começando pelo tipo de periférico mais básico, as GPIOs, mesmo que sejam algo básico, não significa que sejam simples. O ESP32 tem uma grande variedade de recursos e o driver das GPIOs conta com mais de 30 funções!
Faremos apenas uma demonstração das funcionalidades básicas. Para ter acesso ao driver de GPIO basta incluir o header: <driver/gpio.h>
Na GPIO Driver há duas maneiras de configurar as GPIOS: individualmente por meio de funções livre ou múltiplos GPIOs por meio da “gpio_config_t” struct (GPIO PAD). Vamos mostrar, na sequência, como utilizar as duas formas:
Assim como na Arduino temos pinMode() para configurar a direção de uma GPIO, na ESP-IDF temos “gpio_set_direction(gpio_num_t, gpio_mode_t)
“. Essa função recebe 2 parâmetros:
gpio_num_t
é o número da GPIO que queremos configurar. Recebe um valor no formato GPIO_NUM_x, onde “x” é o número da GPIO (Ex: num. GPIO10 = GPIO_NUM_10).gpio_mode_t
é a direção que queremos configurar a GPIO. Esta pode receber 6 valores:
Para configurar os resistores internos de pull-up e pull-down, usamos a função “gpio_set_pull_mode(gpio_num_t, gpio_pull_mode_t)
“, onde o primeiro argumento é o número da GPIO e o segundo é o modo de PULL, que na ESP-IDF pode receber 4 valores:
(também é possível usar funções especificas para cada tipo de pull, Ex: gpio_pullup_en(gpio_num_t)
e gpio_pulldown_en(gpio_num_t)
)
Como já foi comentado, podemos configurar múltiplos GPIOs de uma só vez. Para isso vamos usar a struct gpio_config_t
, essas struct possui os seguintes campos:
uint64_t pin_bit_mask
: A posição de cada bit desse valor representa uma GPIO, ou seja 0b…001 = GPIO1, 0b…010 = GPIO2, 0b….111 = GPIO1, GPIO2 e GPIO3. Todas as GPIOS selecionadas nessa bitmask vão ser configurados da mesma forma.gpio_mode_t mode
Seleciona o modo, igual já comentado na função anterior.gpio_pullup_t pull_up_en
: Configura o pull-up, recebendo:
gpio_pulldown_t pull_down_en
: Configura o pull-down, recebendo:
gpio_int_type_t intr_type
Configura modo de interrupção (vamos manter em GPIO_INTR_DISABLE por enquanto)Após configurar os valores na struct, basta passar por referência para a função: gpio_config(gpio_config_t*)
. Isso vai configurar o hardware conforme as informações na struct.
Agora que sabemos como configurar as portas, para utilizá-las, independente da forma de configuração, usamos as funções:
gpio_set_level(gpio_num_t gpio_num, uint32_t level)
, que é semelhante ao “digitalWrite()
” e recebe o número da GPIO juntamente com o nível lógico da porta (1 = HIGH e 0 = LOW).int gpio_get_level(gpio_num_t gpio_num)
, semelhante ao “digitalRead()
” e recebe o número da GPIO, retornando o nível lógico da porta (1 = HIGH e 0 = LOW).
Nesse projeto, vamos configurar as GPIOs das duas formas que aprendemos para implementar um botão que seleciona qual LED irá piscar.
// C includes #include <stdio.h> //FreeRTOS includes #include <freertos/FreeRTOS.h> //esp-idf includes #include <esp_log.h> #include <esp_err.h> #include <driver/gpio.h> //defines #define BTN GPIO_NUM_21 #define LED_V GPIO_NUM_22 #define LED_A GPIO_NUM_23 //função que configura os GPIOs void GPIO_init(); void app_main(void){ const static char TAG[] = "main task"; //tag main GPIO_init(); //inicia as GPIOS gpio_num_t leds[] = {LED_A, LED_V}; int led_atual = 0; int input = 0; int input_anterior = 1; while(true){ input = gpio_get_level(BTN); if(input == 0 && input != input_anterior){ led_atual = (led_atual + 1)%2;// muda o led atual ESP_LOGI(TAG, "botão pressionado!"); } input_anterior = input; //pisca led atual gpio_set_level(leds[led_atual], 1); vTaskDelay(pdMS_TO_TICKS(100)); gpio_set_level(leds[led_atual], 0); vTaskDelay(pdMS_TO_TICKS(100)); } } void GPIO_init(){ const static char TAG[] = "GPIO_init"; //configura GPIOS com a struct gpio_config gpio_config_t leds = { .pin_bit_mask = (1<<LED_A) | (1<<LED_V), //BITMASK das portas 23 e 22 .mode = GPIO_MODE_DEF_OUTPUT, //modo output only .pull_up_en = GPIO_PULLUP_DISABLE, //sem pullup .pull_down_en = GPIO_PULLDOWN_DISABLE,// sem pulldown .intr_type = GPIO_INTR_DISABLE, //interrupções desligadas }; ESP_ERROR_CHECK(gpio_config(&leds)); //inicia a configuração e verifica erros //configura GPIO com funções gpio_set_direction(BTN, GPIO_MODE_DEF_INPUT); // configura a porta 21 como input gpio_set_pull_mode(BTN, GPIO_PULLUP_ONLY); //configura pull_up na porta 21 ESP_LOGI(TAG, "GPIOs configuradas!!!"); }
Interessado em melhorar suas habilidades com a ESP-IDF? Aqui vai um desafio: sabendo que a ESP-IDF tem base no FreeRTOS, modifique o código anterior para torná-lo multi-task. Crie uma task que pisque o led atual e outra task que leia o botão, de forma que o delay do blink não atrapalhe a leitura do botão como ocorre no código atual.
Agora vamos aprender a configurar a UART na ESP-IDF. Assim como as GPIOS, a UART também tem seu driver, sendo incluída pelo header: <driver/uart.h>
.
O processo para usar a UART na ESP-IDF é dividido em 3 etapas:
Vamos ver cada um deles parte por parte. Caso você não tenha muita experiência com o protocolo UART e não entenda o que cada configuração signifique, não se preocupe, no projeto desse tópico vamos mostrar como configurar e usar.
Os parâmetros de configuração ditam como a comunicação deve acontecer, algo como: baud rate, quantidade de Bits, quantidade de stop bits, paridade, etc. Isso é feito usando a struct: uart_config_t
, essa struct tem os seguintes campos:
int baud_rate
: Velocidade da comunicação uart_word_length_t data_bits
: Tamanho dos pacotes, que recebe um dos seguintes valores:UART_DATA_5_BITS
UART_DATA_6_BITS
UART_DATA_7_BITS
UART_DATA_8_BITS
UART_DATA_BITS_MAX
uart_parity_t parity
: Habilita e configura a quantidade de Bits de paridade. Recebe um dos seguintes valores:UART_PARITY_DISABLE
UART_PARITY_EVEN
UART_PARITY_ODD
uart_stop_bits_t stop_bits
: Habilita e configura a quantidade de Stop Bits. Recebe um dos seguintes valores: UART_STOP_BITS_
UART_STOP_BITS_1_5
UART_STOP_BITS_2
UART_STOP_BITS_MAX
uart_hw_flowcontrol_t flow_ctrl
: Configura o controle de fluxo (nesse post, esse campo ficará desabilitado); uint8_t rx_flow_ctrl_thresh
: Configura o fluxo do pino RTS (nesse post, esse campo ficará desabilitado);uart_sclk_t source_clk
: Seleciona a fonte de clock (nesse post, esse campo ficará no valor padrão);(esses parâmetros também podem ser definidos e modificados a qualquer momento por funções livres de cada item citado na struct).
Após a definição dos parâmetros, chamamos a função uart_param_config(uart_port_t, const uart_config_t*)
, onde o primeiro parâmetro é o barramento UART que vamos usar. O ESP32 conta com 3 barramentos UART:
UART_NUM_0
UART_NUM_1
UART_NUM_2
O segundo parâmetro é um ponteiro para a struct de anterior, essa função vai carregar as configurações para o barramento UART escolhido.
Você provavelmente já deve ter visto o pinout do ESP32. Cada pino tem uma função própria, então, por que temos que configurar os pinos? Isso acontece, pois as placas da família ESP32 tem a capacidade de usar qualquer pino para praticamente qualquer função desde que você siga algumas condições especiais, tais como: velocidade de comunicação, fonte de clock, etc. Nesse post não vamos fazer uso dessa peculiaridade, vamos trabalhar apenas com os pinos padrão.
Para configurar os pinos é bem simples, basta chamar a função: uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num)
. O primeiro parâmetro é o barramento UART que vamos configurar, e os 4 últimos são o número de cada pino que vamos usar, nessa ordem: TX, RX, RTS e CTS, caso você não queira utilizar algum pino, como nesse caso os pinos RTS e CTS, basta utilizar o valor: UART_PIN_NO_CHANGE
em vez de usar o número da porta.
Com todas as configurações já feitas esta na hora de iniciar o nosso driver UART. Para isso, apenas chame a função: uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int event_queue_size, QueueHandle_t *uart_queue, int intr_alloc_flags)
, sendo seus parâmetros:
Para enviar informações o processo é bem simples, basta chamar a função: uart_write_bytes(uart_port_t uart_num, const void *src, size_t size)
, selecionando a UART que queremos enviar a mensagem, um ponteiro para a informação que queremos enviar, e o tamanho em bytes da informação.
Receber informações também é relativamente fácil, primeiro vamos verificar se há informações disponíveis para leitura com a função: uart_get_buffered_data_len(uart_port_t uart_num, size_t *size)
. Nesse ponto já sabemos o que é o primeiro parâmetro, o segundo parâmetro é um ponteiro para a variável que queremos salvar a informação (o retorno por referência nessa função é usado, pois ela retorna esp_err_t para tratamento de erros).
Caso haja informações a serem lidas, usamos a função: uart_read_bytes(uart_port_t uart_num, void *buf, uint32_t length, TickType_t ticks_to_wait)
, que recebe os parâmetros:
Com isso em mente, já sabemos o necessário para criar nosso próximo projeto!
Nesse projeto vamos fazer um simples ECHO via UART (echo significa que vamos apenas repetir a informação recebida), passando por todas as etapas de configuração da UART, leitura e escrita de informação.
Nesse código vamos usar a UART0, então é recomendado desligar todos os Logs, como já vimos início desse post.
//C includes #include <stdio.h> //freeRTOS includes #include <freertos/FreeRTOS.h> #include <freertos/queue.h> //esp-idf includes #include <esp_log.h> #include <esp_err.h> #include <driver/uart.h> //UART defines #define UART UART_NUM_0 #define BAUDRATE 115200 #define UART_BUF_LEN 2048 #define UART_EVENT_BUF_LEN 10 //UART tipes QueueHandle_t uart_event_queue; //função que inicia o driver de UART void UART_init(); void app_main(void){ size_t RX_len = 0; //contador de Bytes no RX buffer char Recive_buf[UART_BUF_LEN] = {0};//inicializa o array com 0 //inicializa a UART UART_init(); while(true){ //verifica se há informações para ler uart_get_buffered_data_len(UART,&RX_len); if(RX_len != 0){ uart_read_bytes(UART,Recive_buf,RX_len, pdMS_TO_TICKS(1000)); //le as informações do RX uart_write_bytes(UART, Recive_buf, RX_len); // envia as informações para TX RX_len = 0; // reseta o contadotor de Bytes } vTaskDelay(pdMS_TO_TICKS(100)); //espera 100MS a cada leitura } } void UART_init() { //configuração basica de UART uart_config_t uart_params = { .baud_rate = BAUDRATE, .data_bits = UART_DATA_8_BITS, //pacotes de 8bits .parity = UART_PARITY_DISABLE, //sem paridade .stop_bits = UART_STOP_BITS_1, //apenas 1Bit de stop .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, //sem pino de CTS, função desabilitada .rx_flow_ctrl_thresh = 0,// sem pino RTS, função desabilitada .source_clk = UART_SCLK_DEFAULT, //clock padrão da UART }; //aplicando as configuraçõe ESP_ERROR_CHECK(uart_param_config(UART, &uart_params)); //configuração dos pinos ESP_ERROR_CHECK(uart_set_pin(UART, 1, 3, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); //----------------TXD, RXD, CTS(sem pino),RTS(sem pino) //inicilizando Driver de UART ESP_ERROR_CHECK(uart_driver_install(UART, UART_BUF_LEN, UART_BUF_LEN, UART_EVENT_BUF_LEN, &uart_event_queue, 0)); }
Para o último tópico desse post vamos falar como configurar e usar o driver I₂C. Vamos começar incluindo o header <driver/i2c.h>
, que tem as etapas bem parecidas com a configuração do driver da UART (até um pouco mais fácil, eu diria). Assim como na UART, esse processo segue algumas etapas, nesse caso são apenas duas, sendo a primeira a configuração dos parâmetros, tanto I₂C master quanto slave são configurados pela struct: i2c_config_t
. Essa struct tem os seguintes campos:
i2c_mode_t mode
: É aqui que vamos definir se nosso I₂C vai estar no modo master ou slave, podendo receber os seguintes valores:
int sda_io_num
: Número da GPIO que será usada para SDAint scl_io_num
: Número da GPIO vamos usar para SCLbool sda_pullup_en
: Habilita o pull-up para o pino SDA (true = habilitado, false = desabilitado)bool scl_pullup_en
: Habilita o pull-up para o pino SCL (true = habilitado, false = desabilitado)clk_flags
: Seleciona a fonte de clock do I₂C (nesse post vamos manter na opção padrão)O último item dessa struct é uma union anonima, onde você vai escolher entre as configurações de master e slave. Caso escolha master, a configuração termina com os seguintes itens:
master.clk_speed
clock da comunicação i2c, não pode ser maior que 1Mhz.Caso escolha slave, a configuração termina com os seguintes itens:
slave.addr_10bit_en
: Habilita endereços de 10Bits para i2c (0 = desabilitado, 1 habilitado)slave.uint16_t slave_addr
: Endereço desse dispositivo no barramento I₂Cslave.uint32_t maximum_speed
: Clock máximo que esse dispositivo pode trabalhar (não pode ser maior que 1Mhz)com a configuração feita, usamos a função: i2c_param_config(i2c_port_t, const i2c_config_t*)
. Os argumentos dessa função significam:
Barramento configurado, agora basta inicializar o Driver, fazemos isso com a função: i2c_driver_install(i2c_port_t, i2c_mode_t, size_t, size_t, int)
, onde cada parâmetro dessa função significa:
Assim como os Drivers anteriores, dezenas de funções podem ser utilizadas, mas vamos abordar somente as mais básicas no momento.
Para escrever uma informação como master usamos a função i2c_master_write_to_device(i2c_port_t, uint8_t*, const uint8_t, size_t TickType_t)
, onde cada argumento significa:
Para ler informações como master usamos a função: i2c_master_read_from_device(i2c_port_t, uint8_t, uint8_t*, size_t TickType_t)
, onde cada argumento significa:
Para ler informações no modo slave usamos a função i2c_slave_read_buffer(i2c_port_t, uint8_t*, size_t, TickType_t)
, onde cada argumento significam:
Para escrever informações como slave usamos a função i2c_slave_write_buffer(i2c_port_t, const uint8_t*, int, TickType_t)
, onde cada argumento significa:
Para finalizar, vamos ao nosso último projeto, logo abaixo:
Nesse projeto vamos fazer algo um pouco diferente. Como comentamos no início do artigo, a ESP-IDF também suporta o uso da linguagem C++, então vamos fazer uso disso: nesse projeto vamos aprender como instalar bibliotecas de terceiros (fora da lista de componentes).
Nesse código vamos usar a biblioteca: RecursiveError/UniversalLCD. Para baixar, acesse o link e vá em Code -> Local -> Download ZIP:
Feito isso, vamos extrair o arquivo na pasta “main/” do nosso projeto:
Após, vamos renomear o arquivo main.c para main.cpp e adicionar o seguinte código no arquivo CMakeLists.txt:
idf_component_register(SRCS "main.cpp" "./UniversalLCD-main/universallcd.cpp" INCLUDE_DIRS "." "UniversalLCD-main")
Após isso, tudo está pronto para começar o projeto!
Nota: código abaixo contempla também as funções do display, mas não se preocupe com elas no momento, vamos abordá-las em posts futuros.
Repare também que adicionamos o comando extern “C” antes de void app_main. Todo código feito em C++ na ESP-IDF deve conter essa expressão!
//Lib C includes #include <stdio.h> //FreeRTOS includes #include <freertos/FreeRTOS.h> //ESP-IDF includes #include <driver/i2c.h> #include <driver/gptimer.h> #include <esp_err.h> #include <esp_log.h> //extra includes #include <UniversalLCD-main/universallcd.hpp> //DEFINES #define LCD_ADDR 0x27 #define I2C_MASTER I2C_NUM_0 #define TIMER_HZ (1000*1000) //lcd types ------------------------------------------------------------------------------------------------- struct LcdDrive : public universalLCD::BusInterface{ void send(uint8_t config, uint8_t data){ uint8_t package = (config & 0b00000111) | (data & 0xF0) | 0x08; i2c_master_write_to_device(I2C_MASTER, LCD_ADDR,&package,1,pdMS_TO_TICKS(100)); } }; //lcd driver delay (This code runs at a frequency of 1Khz, so it is not necessary to block the task for a microsecond delay) static bool timer_delay_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx); void delay(uint32_t us); void lcd_timer_init(void); //lcd drive handler gptimer_handle_t lcd_driver_delay; gptimer_config_t timer_config; gptimer_alarm_config_t delay_config; gptimer_event_callbacks_t delay_callback; volatile bool timer_flag; //-------------------------------------------------------------------------------------------------------------------------------------- void I2C_init(); extern "C" void app_main(){ I2C_init(); lcd_timer_init(); LcdDrive drive; universalLCD::UniversalLCD lcd(drive,universalLCD::Bus4Bits,delay); lcd.begin() .write("Hello world") .cursorOn() .cursorBlinkOn(); while(true) vTaskDelay(portMAX_DELAY); } void I2C_init(){ //configura I2C i2c_config_t conf = { .mode = I2C_MODE_MASTER, //modo master .sda_io_num = GPIO_NUM_21, .scl_io_num = GPIO_NUM_22, .sda_pullup_en = GPIO_PULLUP_ENABLE, //pull-ups habilitados .scl_pullup_en = GPIO_PULLUP_ENABLE, .master = 100000, //100Khz de clock .clk_flags = I2C_SCLK_SRC_FLAG_FOR_NOMAL, //clock padrão }; //aplica as configuraçoes ESP_ERROR_CHECK(i2c_param_config(I2C_MASTER, &conf)); //inicia o driver I2C ESP_ERROR_CHECK(i2c_driver_install(I2C_MASTER, conf.mode, 0, 0, 0)); } //LCD Functions void lcd_timer_init(void){ timer_config = { .clk_src = GPTIMER_CLK_SRC_DEFAULT, .direction = GPTIMER_COUNT_UP, .resolution_hz = TIMER_HZ, .intr_priority = 0, .flags = 0, }; ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &lcd_driver_delay)); delay_callback = { .on_alarm = timer_delay_callback, }; delay_config = { .alarm_count = 0, .reload_count = 0, .flags = 0, }; ESP_ERROR_CHECK(gptimer_register_event_callbacks(lcd_driver_delay,&delay_callback,NULL)); ESP_ERROR_CHECK(gptimer_enable(lcd_driver_delay)); ESP_ERROR_CHECK(gptimer_start(lcd_driver_delay)); } static bool timer_delay_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx){ timer_flag = true; return true; } void delay(uint32_t us){ uint64_t count; gptimer_stop(lcd_driver_delay); gptimer_get_raw_count(lcd_driver_delay, &count); delay_config = { .alarm_count = (uint64_t)(us+count), .reload_count = 0, .flags = 0, }; timer_flag = false; ESP_ERROR_CHECK(gptimer_set_alarm_action(lcd_driver_delay, &delay_config)); gptimer_start(lcd_driver_delay); while(!timer_flag); }
Finalizamos aqui essa introdução. Esperamos que esse post tenha contribuído para seu aprendizado e desenvolvimento de novas habilidades, é sempre bom aprender novas ferramentas e funcionalidades para implementar em nossos projetos.
Sinta-se a vontade para nos enviar dúvidas e/ou sugestões nos comentários e fique atento às novidades, traremos outros artigos sobre a ESP-IDF no futuro.
Obrigado por ler até aqui, e até mais!
|
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.
Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!