Comunicação entre tarefas no FreeRTOS: Filas

IAR com o FreeRTOS Comunicação entre tarefas alocação dinâmica de memória
Este post faz parte da série Projeto com FreeRTOS. Leia também os outros posts da série:

Em um sistema multitarefa como o FreeRTOS, frequentemente é necessária alguma forma de comunicação que permita a troca de dados entre as tarefas do sistema.

 

Uma forma bastante simples de implementar esta comunicação tarefas é através de uma variável global que será escrita/lida pelas tarefas envolvidas conforme a Figura 1. Neste exemplo, a tarefa 1 deseja enviar um bloco de dados (bytes) para a tarefa 2. Para realizar isto, a tarefa 1 escreve os dados em uma variável global que é acessível às duas tarefas. Posteriormente, a tarefa 2 faz a leitura dos dados armazenados na variável global.

 

Comunicação entre tarefas através de variável global
Figura 1: Comunicação entre tarefas através de variável global

 

A solução acima, apesar de ser bastante simples, não é viável pois apresenta um grande problema. Em um sistema multitarefa preemptivo o sistema operacional irá interromper a execução de uma tarefa para disponibilizar tempo de processamento para a tarefa seguinte. É impossível prever o instante em que esta interrupção ocorrerá. A escrita ou leitura de uma variável global é quase sempre uma operação composta de várias instruções assembly, portanto tarefa poderá ser interrompida durante a escrita nas variáveis globais. Se isto ocorrer o conteúdo da variável global estará inconsistente ou incompleto. Se nesta situação, o controle for entregue a uma tarefa que lê os dados da variável global, o resultado da leitura será imprevisível, provavelmente incorreto/incompleto.

 

A princípio, pode parecer que uma situação tão particular como esta raramente irá ocorrer, mas como o sistema operacional realiza o chaveamento entre as diversas tarefas centenas de vezes por segundo, a probabilidade deste tipo de situação ocorrer deixa de ser desprezível. E basta que este tipo de problema ocorra apenas uma vez para comprometer o bom funcionamento do sistema.

 

Mecanismos de comunicação entre tarefas que não são suscetíveis a este tipo de problema são denominados de thread safe. As tarefas envolvidas em comunicação thread safe poderão ser interrompidas ou executadas a qualquer momento sem que isso gere um estado inconsistente nos dados que são transferidos entre estas tarefas.

 

Normalmente os sistemas operacionais, como o FreeRTOS, oferecem alguns mecanismos de comunicação entre tarefas. Como tanto o controle dos mecanismos de comunicação quanto o chaveamento entre as tarefas são controladas pelo sistema operacional, este consegue garantir que nenhuma tarefa seja interrompida de forma a deixar dados inconsistentes.

 

O FreeRTOS oferece alguns mecanismos que permitem realizar comunicação entre tarefas de forma thread safe tais como:

  • Notificações
  • Filas
  • Semáforos
  • Mutexes

 

A escolha de um dos tipos de comunicação acima depende da aplicação. Neste artigo vamos apresentar a comunicação através de filas. Este tipo de comunicação é útil quando se deseja transferir bytes entre tarefas. As filas do FreeRTOS permitem transferir desde um único byte até grandes blocos de bytes entre as tarefas.

 

 

Filas

 

Uma fila é uma estrutura em que os dados são organizados de forma sequencial. Uma fila tem um comportamento First-In-First-Out (FIFO) ou seja, o primeiro byte a ser inserido na fila será o primeiro a ser removido. Este comportamento é semelhante a uma fila de banco em que o primeiro cliente a entrar na fila será o primeiro a ser atendido.

 

Dois ponteiros são usados para indexar os elementos da fila:

  • Início (Head) – aponta para a posição da fila que contém o elemento a ser removido da fila.
  • Final (Tail) – aponta para a posição da fila em que novos elementos devem ser inseridos.

 

Uma fila, normalmente, é implementada como um vetor localizado na memória RAM do microcontrolador. Os ponteiros funcionam como índices deste vetor. Observe que a medida que elementos vão sendo inseridos na fila, o ponteiro do início vai se aproximando do final do vetor. O mesmo acontece com o ponteiro do final da fila a medida que elementos vao sendo retirados da fila. A Figura 2 mostra um exemplo de fila com cinco posições. Neste exemplo um novo elemento será inserido na posição 3 pois esta é a primeira posição vaga e por isto é apontada pelo ponteiro de fim da fila. Um elemento poderá ser retirado da posição 1 que é apontada pelo ponteiro de início. As posições 4 e 5 estão vazias neste momento.

 

Figura 2: Exemplo de fila

 

A fila é denominada de fila circular se ao chegar ao final do vetor os ponteiros são reposicionados para o início do vetor. Desta forma a fila comporta-se como pedaço de memória RAM em forma de círculo em que elementos são inseridos ou removidos nas posições indexadas pelos ponteiros.

 

A Figura 3 mostra um exemplo de fila circular. Neste exemplo as posições 1,2,3 e 4 contém elementos que foram enfileirados e as demais posições da fila estão livres. O ponteiro Inicio indica o próximo elemento a ser removido da fila. O ponteiro Fim aponta a próxima posição livre da fila.

 

Figura 3: Exemplo de fila circular

 

 

Filas no FreeRTOS

 

Embora implementar uma fila circular não seja uma tarefa complexa, o leitor não precisa ter esta preocupação pois o FreeRTOS oferece uma implementação de filas circulares.

 

As filas circulares são implementadas no arquivo queue.c do código fonte do FreeRTOS (para entender a organização do código fonte do FreeRTOS, recomendo este artigo).

 

A tabela abaixo mostra as principais funções do FreeRTOS relacionados a manipulação de filas.

 

 

Nome da função

Finalidade

Observação

xQueueCreate

Cria uma fila nova

Antes de realizar qualquer operação com filas, esta função deve ser executada para criar a fila.

vQueueDelete

Apaga uma fila e libera toda a memória alocada para a fila

Em sistemas com pouca memória sempre é importante liberar memória não utilizada

xQueueSend

Enfileira um elemento no final da fila.

Esta função NÃO deve ser usada dentro de uma rotina de tratamento de interrupção.

xQueueSendFromISR

Enfileira um elemento no final da fila.

Esta função pode ser usada dentro de uma rotina de tratamento de interrupção.

xQueueReceive

Remove um elemento do início da uma fila.

Esta função NÃO deve ser usada dentro de uma rotina de tratamento de interrupção.

xQueueReceiveFromISR

Remove um elemento do início de uma fila.

Esta função pode ser usada dentro de uma rotina de tratamento de interrupção.

 

Nas próximas subseções, cada uma das funções acima será descrita em detalhes.  

 

xQueueCreate

 

Cria uma nova fila e aloca a memória necessária. Esta função retorna um identificador único (handle) da fila recém criada.

 

Parâmetros:

uxQueueLength – Número máximo de elementos contidos na fila.

uxItemSize – O tamanho, em bytes, de cada elemento da fila.

 

Retorna:

Se a fila for criada, a função retornará o identificador da fila. Se a fila não for criada, retornará 0.

 

vQueueDelete

 

Apaga uma fila existente. Toda a memória associada a fila será liberada.

 

Parâmetros:

xQueue  - Identificador da fila que será apagada.

 

xQueueSend

 

Esta função envia um elemento para o final da fila. É importante observar que esta função não deve ser executada de dentro de uma rotina de tratamento de interrupção.

 

Parâmetros:

xQueue  - Identificador da fila em que o elemento será inserido.

pvItemToQueue – Ponteiro para o elemento a ser inserido na fila. O tamanho do elemento deve ser igual ao tamanho informando quando a fila foi criada.

xTicksToWait – Caso a fila esteja cheia, este parâmetro informa o tempo máximo que a tarefa deverá aguardar. Se espaço for disponibilizado na fila durante este intervalo, o elemento será inserido na fila.

 

Retorna:

A constante pdTRUE se o elemento foi inserido na fila corretamente. Caso não seja possível inserir na fila, a função retornará a constante errQUEUE_FULL.

 

xQueueSendFromISR

 

 

Esta função envia um elemento para o final da fila. Esta função pode ser usada a partir de uma rotina de tratamento de interrupção.

 

Parâmetros:

xQueue  - Identificador da fila em que o elemento será inserido.

pvItemToQueue – Ponteiro para o elemento a ser inserido na fila. O tamanho do elemento deve ser igual ao tamanho informando quando a fila foi criada.

xTaskPreviouslyWoken – Este parâmetro informa ao sistema operacional se uma tarefa que realiza leituras da fila em que os elementos estão sendo inseridos, precisa ser executada para processar os novos elementos da fila.

 

Retorna:

pdTRUE se uma tarefa será executada devido a inserção na fila. Isto informa a rotina de tratamento de interrupção se é necessário realizar uma troca de contexto.

 

xQueueReceive

 

 

Remove um elemento do início de uma fila. É importante observar que esta função não deve ser executada de dentro de uma rotina de tratamento de interrupção.

 

Parâmetros:

xQueue  - Identificador da fila do qual o elemento será removido.

pvBuffer – ponteiro para um buffer que receberá o elemento a ser removido da fila.

xTicksToWait – caso a fila esteja vazia, este parâmetro informa o tempo máximo que a tarefa deverá aguardar. Se durante este intervalo um elemento for inserido na fila por outra tarefa, este elemento será removido.

 

Retorna:

A constante pdTRUE se um elemento foi removido da fila corretamente. Caso nenhum elemento seja removido da fila, a função retornará a constante pdFALSE.

 

xQueueReceiveFromISR

 

 

Remove um elemento do início de uma fila. Esta função pode ser usada a partir de uma rotina de tratamento de interrupção.

 

Parametros:

xQueue  - Identificador da fila do qual ó elemento será removido.

pvBuffer – ponteiro para um buffer que receberá o elemento a ser removido da fila.

xTaskPreviouslyWoken – Este parâmetro informa ao sistema operacional se uma tarefa que está aguardando por espaço na fila, precisa ser executada para inserir novos elementos na fila.

 

Retorna:

pdTRUE se um elemento foi removido da fila corretamente.

 

 

Exemplo prático

 

O exemplo a seguir mostra como usar filas para envio de dados entre duas tarefas. Inicialmente, antes mesmo da criação das tarefas, uma fila é criada usando a função xQueueCreate. Observe que a fila possui 5 posições (conforme definido pela constante QUEUE_LENGTH). A ideia deste exemplo é enviar um byte da tarefa de cada vez por isso o tamanho do elemento da fila foi definido como sendo um byte (conforme constante QUEUE_ITEM_SIZE). Observe que não é necessário se preocupar com a alocação de memória para a fila pois o xQueueCreate faz isso de forma transparente.

 

Logo após a criação da fila, são criadas duas tarefas usando a função xTaskCreate. As tarefas são chamadas de Producer e Consumer.

 

Segue abaixo um trecho de código ilustrando estas etapas:

 

 

Segue abaixo o código da tarefa Producer. Esta tarefa insere um byte na fila a cada 20ms. O delay de 20ms é garantido pela função vTaskDelay.

 

 

Este exemplo foi implementado em uma placa MCB2300 (http://www.keil.com/mcb2300/) equipado com um processador LPC2368, um ARM7 da NXP. Nesta placa os 8 primeiros pinos da porta 2 são conectados a LEDs. Desta forma, no código abaixo, a tarefa Consumer retira um byte da fila e escreve este valor nos LEDS da placa.

 

 

Neste ponto vale um esclarecimento quanto ao funcionamento dos pinos de IO deste microcontrolador. O controlador de GPIO trabalha com dois registradores FIOxSET e FIOxCLR onde o x deve ser substituído pelo número da porta em que se deseja escrever. Ao contrário do que pode parecer, o valor escrito no registrador FIOxSET não é o valor que irá aparecer nos pinos do microcontrolador. Os bits 1 escritos neste registrador irão setar os pinos correspondentes, mas um bit 0 NÃO irá zerar o pino correspondente. O registrador FIOxSET deve ser visualizado como um registrador que permite setar pinos do microcontrolador, mas não permite zerar estes pinos. Já o registrador FIOxCLR permite zerar os pinos do microcontrolador, mas NÃO permite setar estes pinos. Portanto se você deseja setar um determinado pino do microcontrolador, deverá escrever 1 no bit correspondente do registrador FIOxSET. Se deseja zerar este mesmo pino deverá escrever 1 (isso mesmo, você deve escrever 1) no bit correspondente ao pino no registrador FIOxCLR.

 

O exemplo abaixo ajuda a esclarecer este ponto. Neste exemplo, por simplicidade, estamos assumindo que aporta possui 8 bits (na verdade as portas do LPC2368 possuem 32 bits).

 

Imagine que o estado dos pinos seja 00110011. Se o valor escrito no registrador FIOxSET for 00001111, o estado dos pinos será alterado para 00111111. Ou seja, foi realizada uma operação OR entre o conteúdo anterior dos pinos e o valor escrito no registrador FIOxSET conforme mostra a Figura 4.

 

FreeRTOS-IAR-tarefas-or-04

Figura 4: Exemplo de escrita no registrador FIOxSET

 

Imagine agora que o estado dos pinos seja 00111111 e que o valor 00110000 seja escrito no registrador FIOxCLR. Todos os bits escritos no FIOxCLR serão invertidos (operação lógica NOT) e depois será feita uma operação AND com o conteúdo anterior dos pinos. O resultado será que os bits 1 escritos no registrador FIOxCLR serão zerados. O resultado do nosso exemplo será: 00001111 conforme a Figura 5.

 FreeRTOS-IAR-tarefas-and-05

Figura 5: Exemplo de escrita no registrador FIOxCLR

 

Para mais detalhes sobre a operações lógicas bit a bit, recomendo esse artigo: Bits em Linguagem C - Conceito e Aplicação.

 

Com este exemplo chegamos ao fim deste artigo sobre comunicação entre tarefas. Espero ter ajudado a entender melhor esta importante funcionalidade do FreeRTOS.

Outros artigos da série

<< Criando um projeto no IAR com o FreeRTOSEntendendo a alocação dinâmica de memória no FreeRTOS >>
Este post faz da série Projeto com FreeRTOS. Leia também os outros posts da série:
NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Gabor Sipkoi
Engenheiro eletrônico (UFPE) com mestrado em engenharia de sistemas (UPE) com 18 anos de experiencia com desenvolvimento de hardware e software para sistemas embarcados de tempo real. Tem grande experiencia com desenvolvimento de software para as famílias de microcontroladores ARM, PIC, Freescale e 8051. Atualmente é engenheiro consultor no CESAR - Centro de Estudos e Sistemas Avançados do Recife.

3
Deixe um comentário

avatar
 
2 Comment threads
1 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
Pedro BertoletiGabor SipkoiOtavio Augusto Gomes Recent comment authors
  Notificações  
recentes antigos mais votados
Notificar
Pedro Bertoleti
Membro

Artigo muito bom!

Otavio Augusto Gomes
Visitante
Otavio Augusto Gomes

Olá Gabor. Obrigado pelo artigo!!

Apenas gostaria de verificar essa essa descrição está correta:

"Início (Head) – aponta para a posição da fila que contém o elemento a ser removido da fila."
"Final (Tail) – aponta para a posição da fila em que novos elementos devem ser inseridos."

Não estaria invertido? Parece que não condiz com a explicação da Figura 2.

Abraço

Gabor Sipkoi
Visitante
Gabor Sipkoi

Otavio,
Espero que estes artigos estejam sendo uteis e ajudando a conhecer um pouco mais sobre o FreeRTOS. Você tem razão quanto as figuras: os ponteiros estão invertidos. Agradeço a observação e já estou providenciando a correção.

Abraço.