Tratar interrupções na UART com ESP32

interrupções na uart

Anteriormente, escrevi um artigo mostrando como utilizar a UART do ESP32 para não utilizar mais o recurso Serial do Arduino. Esse artigo é um complemento, para mostrar o tratamento de interrupções na UART com ESP32 invés de ficar fazendo polling em um loop.

Recursos: Queue

No menu ESP32 você vai encontrar detalhes da utilização de diversos recursos, como semáforo e mutex (que também é um tipo de semáforo). Já discorri sobre filas também e para tratar as interrupções da UART, a utilização de queue é o recurso ideal, porque não queremos perder nada do que ocorrer e eventos podem ocorrer em sequência. Colocando-os em uma fila, trataremos todos na ordem que chegarem.

Interrupções na uart – UART a utilizar



Como explicado no artigo mencionado no primeiro parágrafo, temos diversas UARTs no ESP32 e vamos utilizar a UART0, que é a equivalente à Serial padrão utilizada na IDE do Arduino. No código você encontrará um define indicando-a:

#define EX_UART_NUM UART_NUM_0

Também precisamos definir o tamanho do buffer. Normalmente, 1024 Bytes, mas vamos fazer um buffer minúsculo aqui:

#define BUF_SIZE (4)

Precisaremos criar uma queue para enfileirar os evetos da UART:

static QueueHandle_t uart0_queue;

Task

Escrevi diversos artigos relacionados à task no ESP32. A task é o equivalente a uma thread, que roda de forma assíncrona. Além disso, o ESP32 nos dá o poder de selecionar o núcleo que essa task deve rodar, podendo ser o núcleo 0 (que está sempre livre) ou o núcleo 1 (que é o núcleo utilzado pela IDE do Arduino para rodar a função loop() setup() ). Além disso, podemos definir a prioridade das tarefas; uma prioridade inferior só permitirá a execução da task quando o núcleo em que ela será executada esteja livre para sua execução. Uma prioridade superior prioriza sua execução sobre qualquer outra tarefa. Uma prioridade idêntica fará execução paralela das tarefas em round-robin.

Sugiro que leia os artigos relacionados às tasks para ver os detalhes. Para a execução da tarefa de gerenciamento da UART, definimos previamente uma função. Essa função tratará os tipos de eventos que ocorrerem na UART. No código abaixo deixei comentado o máximo que pude para tentar tornas mais claras as atuações.

static void uart_event_task(void *pvParameters){
    //cria um manipulador de eventos
    uart_event_t event;

    size_t buffered_size;

    //aloca o buffer na memória, do tamanho especificado em BUF_SIZE
    uint8_t *dtmp = (uint8_t *) malloc(BUF_SIZE);

    while (1){
        /* Agora aguardamos pela ocorrência de um evento, depois analisamos
        seu tipo.*/
        if (xQueueReceive(uart0_queue, (void * )&event, (portTickType)portMAX_DELAY)){
            //FAZ ALGUMA COISA
            //Ocorreu um evento, então verificamos seu tipo e em seguida encerramos o loop, chamando break.
            switch (event.type){
            case UART_DATA:
                /* Esse é o evento mais comum esperado; os dados de uma comunicação. Devemos tomar alguns
                cuidados nesse tipo de evento. Por exemplo, não fazer seu tratamento dentro da interrupção,
                porque se tomarmos muito tempo na ISR, o buffer pode se esgotar. Aqui podemos repassar os dados
                para uma outra fila ou para outro buffer e então sinalizar através de uma flag. Desse modo,
                o programa principal ou uma outra task fará o gerenciamento desses dados.
                No momento, apenas exibiremos o que chegou na UART.
                */
                uart_get_buffered_data_len(EX_UART_NUM, &buffered_size);
                ESP_LOGI(TAG, "data, len: %d; buffered len: %d", event.size, buffered_size);
                break;
            case UART_FIFO_OVF:
                //FAZ ALGUMA COISA
                /*Aqui já se trata de um problema.Se esse tipo de evento ocorrer, devemos adicionar um
                controle de fluxo para nossa aplicação. Duas decisões podem ser tomadas aqui. Ler o buffer
                para algum lugar ou, já sabendo de que se trata, simplesmente limpamos o buffer RX e tratamos
                o erro. Como aqui é só um exemplo da interação com os eventos de interrupção, apenas limpamos 
                o buffer.*/
                uart_flush(EX_UART_NUM);
                break;
            case UART_BUFFER_FULL:
                //FAZ ALGUMA COISA 
                Sempre que ocorrer um evento desse tipo, já tenha em mente que os dados não estarão completos.
                Você pode ler a parcial ou simplesmente descartar o buffer e gerar um alarme, um log etc.
                Em outro artigo mostrei como criar arquivos no sistema de arquivos do ESP32, vale a pena dar
                uma conferida.*/
                uart_flush(EX_UART_NUM);
                break;
            case UART_BREAK:
                /*Não me ocorre a razão desse tipo de evento, mas ele existe. No caso de sua ocorrência, tendo
                um sistema de logs facilitará sua compreensão e tratamento.*/ 
                //FAZ ALGUMA COISA
                break;
            case UART_PARITY_ERR:
                //auto-explicativo
                //FAZ ALGUMA COISA
                break;
            case UART_FRAME_ERR:
                //FAZ ALGUMA COISA
                break;
            case UART_PATTERN_DET:
                //FAZ ALGUMA COISA
                break;
            default:
                //Um evento desconhecido
                //FAZ ALGUMA COISA
                break;
            }
        }
    }
    /*devemo sempre desalocar uma área de memória alocada previamente com malloc(), senão teremos leaks
    de memória, até seu esgotamento.*/
    free(dtmp);
    dtmp = NULL;
    //Tarefa concluída, também devemos excluí-la
    vTaskDelete(NULL);
}

setup()

Não era o pretendido, mas ainda estou escrevendo artigos utilizando a IDE do Arduino. Desse modo, ainda precisamos respeitar uma determinada ordem no código. Primeiramente, a configuração da UART (explicada nesse artigo):

uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};

Não se incomode com essa estrutura. Como ela será um padrão, você pode simplesmente guardá-la em algum sketch para quando precisar configurar a UART do ESP32.

O segundo passo é executar a configuração da UART:

uart_param_config(EX_UART_NUM, &uart_config);

Daí definimos os pinos da UART e instalamos o driver. As UARTs já tem uma configuração inicial de seus pinos padrão, de modo que não precisamos fazer nada além de passar como parâmetros os valores UART_PIN_NO_CHANGE.

uart_set_pin(EX_UART_NUM, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(EX_UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 10, &uart0_queue, 0);

Aqui já é diferente da configuração básica da UART, pois está relacionado à ISR:

uart_enable_pattern_det_intr(EX_UART_NUM, '+', 3, 10000, 10, 10);

Agora criamos uma tarefa para manipular evento da UART via ISR. Podemos fazer isso de diversos modos, escolhendo o núcleo a executar, por exemplo. Se chamarmos a função como abaixo, ela será executada no núcleo 1, onde estará a função loop() da IDE do Arduino. A função loop() é ininterrupta, mas a prioridade dessa task é maior. Desse modo, sempre que ocorrer um evento na UART, seu tratamento será priorizado:

xTaskCreate(uart_event_task, "uart_event_task", 2048, NULL, 12, NULL);

Globalmente (lá no header do arquivo, fora do setup) reservamos um buffer para tratamento dos dados entrantes:

uint8_t *data = (uint8_t *) malloc(BUF_SIZE);

loop()

Na função loop() fazemos a leitura e simplesmente imprimimos o dado de volta, quando houver:

int len = uart_read_bytes(EX_UART_NUM, data, BUF_SIZE, 100 / portTICK_RATE_MS);
if (len > 0){
    ESP_LOGI(TAG, "uart read : %d", len);
    uart_write_bytes(EX_UART_NUM, (const char *)data, len);
}

Wemos ESP32 com OLED onboard

Fica mais legal jogar alguma informação em um display do que simplesmente na UART, hum? Para isso, vamos utilizar o ESP32 com OLED onboard que você encontra na CurtoCircuito.

Já mostrei no artigo anterior a esse como utilizá-lo, agora daremos a ele essa divertida aplicação. Invés de detalhar por partes, vou incluir abaixo o código completo, basta fazer a modificação da biblioteca do display como mostrado no artigo supracitado e subir esse código pela IDE do Arduino:

#include "driver/uart.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define EX_UART_NUM UART_NUM_0
#define BUF_SIZE (70)

#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

static QueueHandle_t uart0_queue;

uint8_t *data = (uint8_t *) malloc(BUF_SIZE);

char *str_out = (char *)malloc(sizeof(char) * 20); //nao usado agora, vou deixar pra depois

uart_config_t uart_config = {
    .baud_rate = 115200,
    .data_bits = UART_DATA_8_BITS,
    .parity = UART_PARITY_DISABLE,
    .stop_bits = UART_STOP_BITS_1,
    .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};

void drawText(const void *text,byte posX, byte posY){
    char *text2display = (char*) text;
    //display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(posX,posY);
    display.println((char*)text);
    //display.setCursor(0,10);
    display.display();
}

static void uart_event_task(void *pvParameters){
    //cria um manipulador de eventos
    uart_event_t event;

    size_t buffered_size;

    //aloca o buffer na memória, do tamanho especificado em BUF_SIZE
    uint8_t *dtmp = (uint8_t *) malloc(BUF_SIZE);

    while (1){
        /* Agora aguardamos pela ocorrência de um evento, depois analisamos
        seu tipo.*/
        if (xQueueReceive(uart0_queue, (void * )&event, (portTickType)portMAX_DELAY)){
            uart_write_bytes(UART_NUM_0, (const char *) "Evento: ", 8);
            //Ocorreu um evento, então verificamos seu tipo e em seguida encerramos o loop, chamando break.
            switch (event.type){
            case UART_DATA:
                /* Esse é o evento mais comum esperado; os dados de uma comunicação. Devemos tomar alguns
                cuidados nesse tipo de evento. Por exemplo, não fazer seu tratamento dentro da interrupção,
                porque se tomarmos muito tempo na ISR, o buffer pode se esgotar. Aqui podemos repassar os dados
                para uma outra fila ou para outro buffer e então sinalizar através de uma flag. Desse modo,
                o programa principal ou uma outra task fará o gerenciamento desses dados.
                No momento, apenas exibiremos o que chegou na UART.
                */              
                drawText("Incoming data",0,0);
                break;
            case UART_FIFO_OVF:
                drawText("hw overflow",0,0);
                /*Aqui já se trata de um problema.Se esse tipo de evento ocorrer, devemos adicionar um
                controle de fluxo para nossa aplicação. Duas decisões podem ser tomadas aqui. Ler o buffer
                para algum lugar ou, já sabendo de que se trata, simplesmente limpamos o buffer RX e tratamos
                o erro. Como aqui é só um exemplo da interação com os eventos de interrupção, apenas limpamos 
                o buffer.*/
                uart_flush(EX_UART_NUM);
                break;
            case UART_BUFFER_FULL:
                drawText("data > buffer",0,0);
                /*
                Sempre que ocorrer um evento desse tipo, já tenha em mente que os dados não estarão completos.
                Você pode ler a parcial ou simplesmente descartar o buffer e gerar um alarme, um log etc.
                Em outro artigo mostrei como criar arquivos no sistema de arquivos do ESP32, vale a pena dar
                uma conferida.*/
                uart_flush(EX_UART_NUM);
                break;
            case UART_BREAK:
                /*Não me ocorre a razão desse tipo de evento, mas ele existe. No caso de sua ocorrência, tendo
                um sistema de logs facilitará sua compreensão e tratamento.*/ 
                drawText("UART_BREAK",0,0);
                break;
            case UART_PARITY_ERR:
                //auto-explicativo
                drawText("UART_PARITY_ERR",0,0);
                break;
            case UART_FRAME_ERR:
                drawText("UART_FRAME_ERR",0,0);
                break;
            case UART_PATTERN_DET:
                drawText("UART_PATTERN_DET",0,0);
                break;
            default:
                //Um evento desconhecido
                drawText("unknown error",0,0);
                break;
            }
        }
    }
    /*devemo sempre desalocar uma área de memória alocada previamente com malloc(), senão teremos leaks
    de memória, até seu esgotamento.*/
    free(dtmp);
    dtmp = NULL;
    //Tarefa concluída, também devemos excluí-la
    vTaskDelete(NULL);
}

void setup() {
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  uart_param_config(EX_UART_NUM, &uart_config);
  uart_set_pin(EX_UART_NUM, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
  uart_driver_install(EX_UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 10, &uart0_queue, 0);
  uart_enable_pattern_det_intr(EX_UART_NUM, '+', 3, 10000, 10, 10);

  //xTaskCreate(uart_event_task, "uart_event_task", 2048, NULL, 12, NULL);
  xTaskCreatePinnedToCore(uart_event_task,"uart_event_task",10000,NULL,1,NULL,0);

}

void loop() {
  int len = uart_read_bytes(EX_UART_NUM, data, BUF_SIZE, 100 / portTICK_RATE_MS);
  if (len > 0){
    uart_write_bytes(UART_NUM_0, (const char *) data, len);
    //strcpy(str_out,(char *)data);
    drawText((char *)data,0,10);
    delay(1000);
    display.clearDisplay();
    display.display();
  }
}

Repare que nesse código deixei uma variável para utilizar com strcpy, para imprimir todo o texto diretamente no momento do evento, mas não quis implementar agora porque é necessário saber se a variável foi alimentada (o que só ocorre depois do evento) e precisa colocar um mutex pra controlar o acesso à variável, daí eu iria deixar o código maior. Invés disso, preferi algo ineficiente, que é a utilização de delay no loop, mas serve muito bem para exemplificar o tipo de evento e os dados de entrada. E já que utilizei delay, deixei a função que limpa o display também no loop. Não é nada bonito esparramar um controle específico, mas outra vez, é apenas para exemplificar.

Para testar outro tipo de evento que não seja UART_DATA, troque case UART_DATA por 12345. Como esse valor não casa com nenhuma macro, esse switch irá cair diretamente no default, que é “erro desconhecido” e isso mudará a mensagem do evento no display. Já na linha de baixo, como não tem erro e o loop analisa por dado entrante na UART, o que for digitado na serial sempre será exibido.

Siga-nos no Do bit Ao Byte no Facebook.

Prefere twitter? @DobitAoByte.

Inscreva-se no nosso canal Do bit Ao Byte Brasil no YouTube.

Nossos grupos:

Arduino BR – https://www.facebook.com/groups/microcontroladorarduinobr/
Raspberry Pi BR – https://www.facebook.com/groups/raspberrybr/
Orange Pi BR – https://www.facebook.com/groups/OrangePiBR/
Odroid BR – https://www.facebook.com/groups/odroidBR/
Sistemas Embarcados BR – https://www.facebook.com/groups/SistemasEmbarcadosBR/
MIPS BR – https://www.facebook.com/groups/MIPSBR/
Do Bit ao Byte – https://www.facebook.com/groups/dobitaobyte/

Próximo post a caminho!