Manual

do

Maker

.

com

FreeRTOS com ESP32 - Filas e dicas para evitar problemas

FreeRTOS com ESP32 - Filas e dicas para evitar problemas

Estou gradativamente escrevendo sobre a utilização de recursos do FreeRTOS com ESP32 e antes de começar a fazer programas elaborados, é bom ter em mente algumas regras.

Você precisa de um ESP32?

"Sim" é a resposta. Todo mundo precisa de um ESP32, senão a alegria de um maker não é plena. Cada dia é uma novidade, tem um mundo ainda pra desbravar com o ESP32, por isso recomendo muito a aquisição. Pegue o seu na CurtoCircuito e acompanhe os tutoriais aqui!

Sobre o FreeRTOS

Ele é um sistema operacional de tempo real portável e de código aberto; é preemptivo (isto é, ele pode interromper uma tarefa e depois retomá-la). Tarefas que tenham a mesma prioridade são executadas em round-robin ("um pra mim, um pra você", continuamente).

A CPU pode ser retomada através da primitiva taskYELD() (veremos em algum artigo).

IPC - Inter Proccess Communication

Os processos podem se relacionar através de filas ou de semáforos. Já escrevi um artigo dando como exemplo a utilização de um mutex, que é um tipo de semáforo também.

Kernel

O kernel é minimalista e o código é simples o suficiente para ser portado. Ainda, ele é um código compreendido por diversas arquiteturas de processadores. Executa em MCUs e CPUs de 8, 16 e 32 bits, ainda que tenham recursos limitados, bastando que a flash tenha no mínimo 32K e no mínimo 16KB de RAM.

As regras de programação

Normalmente as variáveis são precedidas pelo seu tipo:

cchar
sshort
llong
vvoid
ppointer
xportBASE_TYPE

Os nomes das funções são precedidas pelo seu tipo, seguido pelo arquivo de sua definição. Por isso para criar uma task temos a xTaskCreate() - um tipo BASE_TYPE(dependente da arquitetura) no arquivo task.c. Significa que a função não tem retorno.

As macros são precedidas pelo nome do arquivo em que estão definidas. A portMAX_DELAY será encontrada como MAX_DELAY no arquivo port.h.

Algumas macros já utilizei em outros artigos, como a pdTRUE, pdFALSE, pdPASS e pdFAIL, que retornam 1, 0, 1, 0, respectivamente.

Em algum momento escreverei um artigo sobre como programar para o ESP32 sem utilizar a IDE do Arduino e aí a estrutura do programa muda. Um código mínimo seria algo como:

short main(void){
    //inicialização do hardware
    prvSetupHardware();
    //criação de tasks
    xTaskCreate(...);
    //inicializa o scheduler
    vTaskStartScheduler();
    return 0;
}

Tasks

Já mostrei as tasks em artigos anteriores. Aqui só tenho a dizer que as funções das tasks sempre devem retornar void, assim como receber o parâmetro void *, cujo conteúdo precisará que seja feito um casting para o tipo. Eu exemplifiquei isso nesse artigo.

Normalmente as tarefas tem um ciclo infinito e cada execução de uma tarefa é chamada de job ou instância.

As tarefas nunca, nunca, nunca devem sair da função associada à sua implementação. Não use return dentro das tasks.

O encerramento de uma tarefa deve ser feita explicitamente. Isso significa que quando você quer sair de um loop, deve utilizar o break e então chamar a vTaskDelete(NULL). Isso eu exemplifiquei nesse outro artigo.

Quando uma tarefa é excluída, o stack e o TCB são liberados, mas sua reutilização dependerá do gerenciamento de memória dinâmica e podem não estar livres imediatamente.

Uma task pode estar em um dos quatro possíveis estados:

  • Running  - quando em execução
  • Ready      - Pronta para ser executada
  • Suspend - Execução suspensa
  • Blocked  - Aguardando um recurso para se executar

A função **vTaskDelay(ticks)**coloca a tarefa em modo de suspensão, portanto ela deixa de consumir ciclos e recursos de CPU. Expliquei em detalhes esse controle neste artigo.

Controle de prioridade da tarefa

Já mostrei como definir a prioridade de tarefas e como selecionar uma CPU do ESP32 para uma determinada task, mas a prioridade pode ser mudada posteriormente, se for necessário. Para isso, utilizamos a função vTaskPrioritySet, que recebe os parâmetros pxTaskUxNewPriority. Isto é, você deve indicar a task e a nova prioridade dela.

Para obter a prioridade de uma tarefa, utilize a função vTaskPriorityGet().

Suspensão de tarefas

Em algum momento você poderá precisar que uma tarefa "durma", por exemplo, por falta de dados a serem processados. Nesse caso, você pode chamar a função vTaskSuspend, que recebe como parâmetro a tarefa em questão. Ainda, se você cria uma tarefa para uma execução simples, depois pode precisar dela em algum outro momento, invés de fazer o delete da tarefa, você pode incluir essa chamada do vTaskSuspend passando NULL como parâmetro, dentro da própria tarefa. Assim ela se executa e se suspende:

void vMyTask(...){
    //faz alguma coisa
    ...
    //se suspende
    vTaskSuspend(NULL);
}

Quando desejar que a task seja retomada, agora a partir de outro ponto qualquer de seu código (e que esteja em execução) chame a vTaskResume passando o manipulador da tarefa (o "nome" passado via parâmetro ao criá-la) para a função. Suponhamos que você tenha criado uma tarefa assim:

xTaskCreatePinnedToCore(sirene,"sirene", 10000, NULL, 1, &dobitaobyte,0);

Previamente você deve ter criado o manipulador dobitaobyte:

TaskHandle_t dobitaobyte;

Então, quando quiser que a tarefa volte a ser executada, você faz a seguinte chamada:

vTaskResume(dobitaobyte);

Também é possível retomar a tarefa a partir de uma ISR, mas ainda preciso escrever sobre as interrupções para ficar claro.

Filas

Além da utilização dos semáforos para acessar dados em uma variável, existe outro meio de relacionamento entre as tarefas. Esse caso é mais específico para uma tarefa de entrada e uma tarefa de saída, utilizando o chamado FIFO (First In, First Out - ou, o primeiro que entra é o primeiro que sai). Para quem é do mundo Linux, já deve ter criado um pipe pelo próprio shell, através do mkfifo. Nesse caso, é mais que compreendido seu funcionamento. Mas se você não entendeu o conceito, explico. Uma fila é exatamente como uma fila de pessoas. Por exemplo, para comprar ingresso no cinema. Conforme a ordem de chegada é a ordem de saída da fila. Sempre saindo pelo caixa. No sistema é a mesma coisa; o primeiro dado que entrar na fila será o primeiro a sair, portanto não é necessário que entrada e saída sejam consumidas sincronamente pelas tarefas, pois a hora que um valor for retirado da fila, o seguinte passa a ser o primeiro. Porém existe limite para isso. Um exemplo básico seria um tamanho fixo definido.

Agora um ponto importante; a fila pode ser consumida por várias tarefas, assim como alimentada por várias tarefas, mas não ao mesmo tempo.

As tarefas que vão ler dessa fila podem ter um timeout, assim, se não conseguirem ler no tempo especificado, elas voltam para o estado Ready e podem ser chamadas novamente.

Uma fila vazia ou uma fila cheia passa seu estado para blocked para leitura ou escrita, respectivamente. Isso significa que se não houver dados, não poderá ser lida e, se estiver cheia, não poderá ser escrita até que seja consumido ao menos 1 valor dessa fila e então ela voltará ao estado permissivo. Para a escrita também é possível especificar um timeout.

Criação de filas

Se desejar utilizar uma fila, primeiramente você deverá criá-la. Para tal, utlizamos a função xQueueHanfle xQueueCreate(unsigned portBASE_TYPE uxQueuelength, unsigned portBASE_TYPE uxItemSize). O primeiro parâmetro é o número máximo de ítens comportados pela fila. O segundo parâmetro é o tamanho de cada ítem, em Bytes. Se o retorno for NULL, a fila não foi criada:

struct myStruct{
  char ucMessageID;
  char ucData[20];
};

void vMyTask(void *pvParameters){
    QueueHandle_t xQueue1, xQueue2;
    //Cria uma fila para 10 uint32
    xQueue1 = xQueueCreate(10,sizeof(uint32_t));
  
    //se a fila for 0 (ou NULL)...
    if(xQueue1 == 0 ){
        Serial.println("A fila nao foi criada :(");
    }
    //Uma fila apontando para a struct criada previamente
    xQueue2 = xQueueCreate(10,sizeof(struct myStruct * ));
    if( xQueue2 == 0 ){
       Serial.println("A fila nao foi criada :(");
    }
    ...
}

Escrevendo no PIPE

Tendo as filas sido criadas previamente, já podemos iniciar as tasks que farão uso dela. Essas tasks podem ser iniciadas, por exemplo, dentro da condicional que verifica se a fila foi criada. Nesse caso, garantimos que a task só rodará se a fila foi criada. Já na implementação da task, podemos iniciar a escrita. No ESP32 temos as funções xQueueSendToFront (escreve no início da fila) e xQueueSendToBack (equivalente a xQueueSend). Acho que não precisa explicar, hum? Todas elas pegam os mesmos parâmetros:

BaseType_t xQueueSend(
                    QueueHandle_t xQueue,
                    const void * pvItemToQueue,
                    TickType_t xTicksToWait
                );

Repare que no ESP-IDF portBASE_TYPE é chamado BaseType_t. É, algumas mexidas a Espressif deu no FreeRTOS. Um exemplo de escrita:

void myTask(void *pvParameters){
    struct myStruct *pxMessage;
    //se a fila foi criada..
    if(xQueue1 != 0){
        // Envia um uint32_t e aguarda 100ms pra repetir, se necessário
        if(xQueueSend(xQueue1, (void *) &ulVar, (TickType_t) pdMS_TO_TICKS(100)) != pdPASS){
            // Falhou no envio, então espera os 100ms (tempo altissimo, apenas exemplo)
        }
    }  
    //se a fila foi criada...
    if(xQueue2 != 0){
        // envia um ponteiro para a struct myStruct. Não bloqueia se a fila já estiver cheia
        pxMessage = & xMessage;
        xQueueSend(xQueue2, (void *) &pxMessage, (TickType_t) 0 );
    }
}

O retorno da escrita é pdPASS.

Ler do PIPE

A leitura é feita com xQueueReceive.

struct AMessage{
  char ucMessageID;
  char ucData[20];
} xMessage;

QueueHandle_t xQueue;
// Cria uma fila e envia o valor
void vATask(void *pvParameters){
    struct AMessage *pxMessage;
    /*Cria uma file que pode conter 10 ponteiros para AMessage*/
    xQueue = xQueueCreate(10, sizeof(struct AMessage *));
    if( xQueue == 0 ){
       // Falha ao criar a fila
    }
    ...
    // Envia um ponteiro para uma struct AMessage. Não bloqueia se a fila esstiver cheia.
    pxMessage = & xMessage;
    xQueueSend( xQueue, (void *) &pxMessage, (TickType_t) 0 );
    ...
}

// Uma tarefa para receber da fila
void vADifferentTask(void *pvParameters){
    struct AMessage *pxRxedMessage;
    if( xQueue != 0 ){
        // Recebe uma msg na fila criada. Bloqueia por 100ms se a fila não se tornar disponível na hora
        if(xQueueReceive( xQueue, &(pxRxedMessage), (TickType_t) pdMS_TO_TICKS(100))){
            // pcRxedMessage aponta para a struct AMessage da vATask.
        }
    }
    ...
}

Estado da fila

Também é possível saber o estado da fila utilizando:

UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);

Essa função retorna o número de ítens contidos em uma fila, permitindo saber se a fila está cheia ou não.

Se for possível para o próximo artigo, pretendo discorrer sobre interrupções. Espero que esse artigo tenha lhe agradado.

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.