Sistemas Operacionais

FreeRTOS – Compartilhamento com Semáforo Binário

Eletrogate 10 de março de 2022

Introdução

Olá, caro leitor, tudo bem? Continuemos com a série de artigos abordando o uso do FreeRTOS. No último post, tratamos sobre a fila (em inglês, queue), demos uma breve explicação de seu funcionamento e apresentamos as funções mais comuns para manipular queue’s disponíveis no FreeRTOS. Vimos que o FreeRTOS permite que o recurso de fila seja utilizado como mecanismo de sincronismo entre tarefas. Neste artigo, será apresentado mais um recurso dedicado a sincronizar tarefas e gerenciar recursos compartilhados.


Semáforo

Semáforo é o mecanismos de controle de acesso a recursos compartilhados entre processos. Esses recursos podem ser um conjunto de dados, periféricos, dispositivos e qualquer outro item ao qual se deseja controlar o acesso. No FreeRTOS, os semáforos são utilizados para fins de sincronização e exclusão mútua. Essa sincronização pode ser realizada entre tarefas ou entre tarefas e interrupções. O kernel do FreeRTOS oferece dois tipos de semáforos; do tipo binários e de contagem. Neste artigo, abordaremos apenas o semáforo binário. Nos próximos artigos da série, teremos um post dedicado a explicar o semáforo de contagem.


Semáforo Binário

Como o próprio nome sugere, esse semáforo possui apenas dois valores (disponível ou indisponível). Para o melhor entendimento, podemos pensar o semáforo binário como uma fila que possui apenas um único item. Portanto, a fila só pode estar vazia ou cheia. As tarefas ou funções de interrupções que estão utilizando essa fila não querem saber do conteúdo da fila, estão interessadas apenas em saber se a fila está vazia ou cheia. Esse recurso pode ser executado, por exemplo, para sincronizar a execução de uma tarefa com uma função responsável por tratar uma interrupção. 


Criando um semáforo binário

Conforme citado nos artigos anteriores, o FreeRTOS contém duas abordagem para manipular a memória RAM. A primeira utiliza o método de alocação da memória RAM dinâmica, a partir do Heap. O segundo modo é por alocação de memória RAM estática em tempo de compilação. Para entender melhor a diferença entre alocação de memória estática e dinâmica, consulte o link. Para esse e os demais artigos, vamos explicar as funções do FreeRTOS que trabalham com alocação de memória RAM dinâmica. 

Para criar uma semáforo é necessário declarar uma variável do tipo “SemaphoreHandle_t”. Essa variável será utilizada como o identificador do semáforo.

Protótipo da função:

SemaphoreHandle_t xSemaphoreCreateBinary(void);

Retorno:

  • Quando o semáforo binário é criado com sucesso, a função retorna o identificador. Na ocorrência de falha na criação, o valor retornado é NULL. 

Obtendo o Semáforo

Conforme visto no artigo anterior, no qual falamos a respeito de fila (queue), existem duas tratativas distintas na utilização das API’s do FreeRTOS: funções especializadas e funções genéricas, semáforos binários obedecem à mesma regra. Quando a obtenção do semáforo é feita a partir de uma tarefa, deve-se usar a função genérica. Se for obter o semáforo a partir de interrupção ou em uma função de callback, é recomendado utilizar API especializada.

xSemaphoreTake é a função genérica para obtenção do semáforo.

Protótipo da função:

xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

Parâmetros 

  • xSemaphore: O identificador do semáforo a ser obtido.
  • xTicksToWait: O tempo em Ticks para aguardar o semáforo ficar disponível. A macro portTICK_PERIOD_MS pode ser utilizada para converter o valor em milissegundos. Também é possível bloquear a tarefa até que o semáforo esteja livre utilizando a macro portMAX_DELAY, sem limite de tempo. Para que esse recurso esteja disponível, é necessário habilitar o recurso INCLUDE_vTaskSuspend; Para mais informações consulte o link.

Retorno 

  • A função retornará o valor pdTRUE quando o Semáforo for obtido com sucesso. Caso a função retorne o valor pdFALSE, significa que o tempo para aguardar a disponibilidade do semáforo, expirou.

xSemaphoreTakeFromISR é a função especializada recomendada para interrupções e funções de callback. 

Protótipo da função

xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken );

Parâmetro

  • xSemaphore: O identificador do semáforo a ser obtido. 
  • pxHigherPriorityTaskWoken: Se esse parâmetro for definido como pdTRUE, uma troca de contexto deve ser solicitada ao kernel antes que a interrupção seja encerrada. 

Retorno

  • A função retorna o valor pdTRUE se o semáforo foi obtido com sucesso e pdFALSE se o semáforo não foi obtido com sucesso porque não estava disponível.

Liberando o Semáforo

Uma vez obtido um determinado semáforo, executado o algoritmo, é necessário liberar o mesmo para que outras funções da aplicação possam utilizar. Como visto na obtenção do semáforo, o FreeRTOS possui duas abordagens, oferecendo funções genéricas e especializadas. Para liberar o semáforo, a tratativa é a mesa, função genérica para ser utilizada a partir de uma tarefa e API especializada para uso em interrupções e funções de callback.

xSemaphoreGive é a função genérica para liberar o semáforo.  

Protótipo da função 

xSemaphoreGive( SemaphoreHandle_t xSemaphore );

Parâmetro:

  • xSemaphore: O identificador do semáforo a ser liberado. 

Retorno

  • A função retorna o valor pdTRUE quando o semáforo é liberado com sucesso. Quando o valor retornado é igual a pdFALSE, significa que ocorreu alguma falha.

xSemaphoreGiveFromISR é a função especializada recomendada para interrupções e funções de callback.

Protótipo da função:

xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken);

Parâmetro:

  • xSemaphore: O identificador do semáforo a ser liberado.
  • pxHigherPriorityTaskWoken: Se esse parâmetro for definido como pdTRUE, uma troca de contexto deve ser solicitada  ao kernel antes que a interrupção seja encerrada. 

Retorno 

  • A função retorna o valor pdTRUE quando o semáforo é liberado com sucesso. Caso o contrário, o valor retornado será o errQUEUE_FULL.

Deletando um Semáforo

Como qualquer objeto no FreeRTOS, o semáforo binário pode ser deletado. Quando é excluído um semáforo, é liberada toda a memória RAM alocada. 

Nota: É recomendado não excluir um semáforo que tenha tarefas bloqueadas nele (tarefas que estão no estado bloqueado aguardando a disponibilidade do semáforo).

Protótipo da função 

void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

Parâmetro: 

xSemaphore: O identificador do semáforo que está sendo deletado.


Projeto de Demonstração

A aplicação desenvolvida para o projeto de demonstração conta com circuito integrado (CI) PCF8574. Esse CI é um expansor de I/O, que se comunica com o módulo ESP32 por meio do barramento I2C. Além dos sinais do I2C, necessário para comunicação com o CI, o pino INT está conectado ao ESP32. Esse é o pino de interrupção do CI PCF8574, utilizado para notificar quando há mudança de valor em suas portas. O projeto desenvolvido utiliza o expansor de I/O PCF8574 com cada porta conectada a um push-button. Então, a cada vez que algum botão é pressionado, o valor do pino INT é alterado. Este, por sua vez, no algoritmo da aplicação, provoca a execução da função de interrupção. Para gerar o sincronismo entre a função que trata interrupção e tarefa responsável por interagir com CI, foi criado um semáforo.  

Foram criadas três tarefas e dois semáforos binários, um dos semáforos é utilizado como mecanismo de sincronização entre função que trata a interrupção e a tarefa responsável por comunicar com o CI PCF8574. O segundo semáforo é encarregado da gestão na escrita no barramento serial, já que esse recurso é compartilhado com todas as tarefas da aplicação. As demais tarefas criadas ficam periodicamente escrevendo no barramento serial. A seguir, temos a figura com o circuito elétrico do projeto de demonstração.

Circuito elétrico

Circuito elétrico

Materiais necessários para o projeto Compartilhamento com Semáforo Binário

  1. Módulo WiFi ESP32s Bluetooth 38 pinos
  2. 2 x Resistor 2K2
  3. 8 x Resistor 10K
  4. 8 x Push Button (Chave Táctil)
  5. Expansor de I/O PCF8574
  6. Protoboard
  7. Jumpers
/**
 * @file main.cpp
 * @author Evandro Teixeira
 * @brief 
 * @version 0.1
 * @date 21-01-2022
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <Arduino.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
/* https://github.com/RobTillaart/PCF8574 */
#include "PCF8574.h"  

#define COLOR_BLACK       "\e[0;30m"
#define COLOR_RED         "\e[0;31m"
#define COLOR_GREEN       "\e[0;32m"
#define COLOR_YELLOW      "\e[0;33m"
#define COLOR_BLUE        "\e[0;34m"
#define COLOR_PURPLE      "\e[0;35m"
#define COLOR_CYAN        "\e[0;36m"
#define COLOR_WRITE       "\e[0;37m"
#define COLOR_RESET       "\e[0m"
#define TASK_DELAY                1000
#define PRIMARY_CORE              PRO_CPU_NUM
#define SECONDARY_CORE            APP_CPU_NUM
#define ENABLE                    1
#define DISABLE                   0
#define ENABLE_SEMAPHORE_SERIAL   DISABLE 
#define PIN_IRQ_EXP_IO            15 
#define ADDRESS_I2C_EXPIO         0x20
#define DEBONCE_IRQ               500

/**
 * @brief 
 */
void Tarefa_A(void *parameters);
void Tarefa_B(void *parameters);
void Tarefa_ExpIO(void *parameters);

/**
 * @brief 
 */
const char txt[] = {"Contribua com a comunidade Maker"};
const char txt1[] = {"Torne-se um Eletrogater Expert"};

/**
 * @brief 
 */
SemaphoreHandle_t xSemaphore_Serial = NULL;
SemaphoreHandle_t xSemaphore_ExpIO = NULL;

/**
 * @brief Cria objeto PCF8574
 */
PCF8574 ExpIO(ADDRESS_I2C_EXPIO);

/**
 * @brief 
 */
void IRAM_ATTR ExpIO_ISR()
{
  static uint32_t last_time = 0;
  if((millis() - last_time) >= DEBONCE_IRQ)
  {
#if ENABLE_SEMAPHORE_SERIAL
    xSemaphoreTakeFromISR(xSemaphore_Serial,pdFALSE);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

    Serial.printf("\n\rIRQ Libera o Semaforo Binario xSemaphore_ExpIO");

#if ENABLE_SEMAPHORE_SERIAL
    xSemaphoreGiveFromISR(xSemaphore_Serial,pdFALSE);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

    last_time = millis();
    xSemaphoreGiveFromISR(xSemaphore_ExpIO, (BaseType_t)(pdFALSE));
  }
}

/**
 * @brief 
 * 
 */
void setup() 
{
  // Inicializa a Serial 
  Serial.begin(115200);
  Serial.printf("\n\rFreeRTOS - Semafaro Binario \n\r");

  // Inicializa pino 15 como entra e inicializa interrupção 
  pinMode(PIN_IRQ_EXP_IO, INPUT_PULLUP);
  attachInterrupt(PIN_IRQ_EXP_IO, ExpIO_ISR, FALLING);

  // Inicializar PCF8574
  ExpIO.begin();

#if ENABLE_SEMAPHORE_SERIAL
  // Cria semafaro binario xSemaphore_Serial  
  vSemaphoreCreateBinary( xSemaphore_Serial );
  if(xSemaphore_Serial == NULL)
  {
    Serial.printf("\n\rFalha em criar o semafaro para Serial");
  }
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  // Cria semafaro binario xSemaphore_ExpIO
  vSemaphoreCreateBinary( xSemaphore_ExpIO );
  if(xSemaphore_ExpIO == NULL)
  {
    Serial.printf("\n\rFalha em criar o semafaro para Expansor I/O");
  }
  // Obtem o semafaro xSemaphore_ExpIO
  xSemaphoreTake(xSemaphore_ExpIO,(TickType_t)100);

  // Cria tarefas da aplicação
  xTaskCreatePinnedToCore(Tarefa_A, "Tarefa_A",  configMINIMAL_STACK_SIZE * 3, NULL, tskIDLE_PRIORITY + 1, NULL, SECONDARY_CORE);
  xTaskCreatePinnedToCore(Tarefa_B, "Tarefa_B",  configMINIMAL_STACK_SIZE * 3, NULL, tskIDLE_PRIORITY + 1, NULL, PRIMARY_CORE);
  xTaskCreatePinnedToCore(Tarefa_ExpIO, "Tarefa_ExpIO",  configMINIMAL_STACK_SIZE * 3, NULL, tskIDLE_PRIORITY + 2, NULL, tskNO_AFFINITY);

}
/**
 * @brief 
 * 
 */
void loop() 
{

#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreTake(xSemaphore_Serial,portMAX_DELAY);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  Serial.printf("\n\rTarefa Loop");
  vTaskDelay((TASK_DELAY/5)/portTICK_PERIOD_MS);

#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreGive( xSemaphore_Serial );
#endif /* ENABLE_SEMAPHORE_SERIAL  */
}

/**
 * @brief 
 * 
 * @param parameters 
 */
void Tarefa_A(void *parameters)
{

#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreTake(xSemaphore_Serial,portMAX_DELAY);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  Serial.print(COLOR_RED);                      // altera para vermelho impressão da mensagem na serial
  Serial.printf("\n\r%s",pcTaskGetTaskName(NULL));
  Serial.printf("\n\r%s",txt);
  Serial.printf("\n\r%s",txt1);
  Serial.print(COLOR_RESET);                    // reset a cor da impressão da mensagem na serial 

#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreGive( xSemaphore_Serial );
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  while (1)
  {

#if ENABLE_SEMAPHORE_SERIAL 
    xSemaphoreTake(xSemaphore_Serial,portMAX_DELAY);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

    Serial.print(COLOR_RED);                      // altera para vermelho impressão da mensagem na serial
    Serial.printf("\n\r%s",pcTaskGetTaskName(NULL));
    Serial.print(COLOR_RESET);                    // reset a cor da impressão da mensagem na serial 

#if ENABLE_SEMAPHORE_SERIAL 
    xSemaphoreGive( xSemaphore_Serial );
#endif /* ENABLE_SEMAPHORE_SERIAL  */

    vTaskDelay((TASK_DELAY)/portTICK_PERIOD_MS);    // Pausa a execução da tarefa por 2000 milessegundos 
  }
}

/**
 * @brief 
 * 
 * @param parameters 
 */
void Tarefa_B(void *parameters)
{
  
#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreTake(xSemaphore_Serial,portMAX_DELAY);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  Serial.print(COLOR_YELLOW);                   // altera para vermelho impressão da mensagem na serial
  Serial.printf("\n\r%s",pcTaskGetTaskName(NULL));
  Serial.printf("\n\r%s",txt);
  Serial.printf("\n\r%s",txt1);
  Serial.print(COLOR_RESET);                    // reset a cor da impressão da mensagem na serial 

#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreGive( xSemaphore_Serial );
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  while (1)
  {

#if ENABLE_SEMAPHORE_SERIAL 
    xSemaphoreTake(xSemaphore_Serial,portMAX_DELAY);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

    Serial.print(COLOR_YELLOW);                   // altera para vermelho impressão da mensagem na serial
    Serial.printf("\n\r%s",pcTaskGetTaskName(NULL));
    Serial.print(COLOR_RESET);                    // reset a cor da impressão da mensagem na serial 

#if ENABLE_SEMAPHORE_SERIAL 
    xSemaphoreGive( xSemaphore_Serial );
#endif /* ENABLE_SEMAPHORE_SERIAL  */

    vTaskDelay(TASK_DELAY/portTICK_PERIOD_MS);    // Pausa a execução da tarefa por 2000 milessegundos 
  }
}

/**
 * @brief 
 * 
 * @param parameters 
 */
void Tarefa_ExpIO(void *parameters)
{
  uint8_t value = 0;

#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreTake(xSemaphore_Serial,portMAX_DELAY);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  Serial.print(COLOR_PURPLE);                   // altera para roxo impressão da mensagem na serial
  Serial.printf("\n\r%s",pcTaskGetTaskName(NULL));
  Serial.printf("\n\r%s",txt);
  Serial.printf("\n\r%s",txt1);
  Serial.print(COLOR_RESET);                    // reset a cor da impressão da mensagem na serial 

#if ENABLE_SEMAPHORE_SERIAL 
  xSemaphoreGive( xSemaphore_Serial );
#endif /* ENABLE_SEMAPHORE_SERIAL  */

  while (1)
  {
    if(xSemaphoreTake(xSemaphore_ExpIO,portMAX_DELAY) == pdTRUE)
    {
      value = ExpIO.read8();
      
#if ENABLE_SEMAPHORE_SERIAL 
      xSemaphoreTake(xSemaphore_Serial,portMAX_DELAY);
#endif /* ENABLE_SEMAPHORE_SERIAL  */

      Serial.print(COLOR_PURPLE);                // altera para roxo impressão da mensagem na serial
      Serial.printf("\n\rPCF8574 Value: %d",value);
      Serial.print(COLOR_RESET);                 // reset a cor da impressão da mensagem na serial 

#if ENABLE_SEMAPHORE_SERIAL 
      xSemaphoreGive( xSemaphore_Serial );
#endif /* ENABLE_SEMAPHORE_SERIAL  */

    }
  }
}

Resultado da Aplicação

Para melhor exemplificar a utilização de semáforo binário como mecanismo de controle de acesso de recurso compartilhado, optei em executar a aplicação com e sem o semáforo habilitado. A imagem a seguir exibe o LOG de mensagem no barramento serial da execução proposta.

 

As mensagens na figura foram enumeradas para detalhar o comportamento do algoritmo implementado no projeto de demonstração:

  1. A tarefa A imprime as suas mensagens iniciais, observe que a mensagem foi impressa com a cor amarela. Analisando o código fonte, é a tarefa B que escreve as suas mensagens com a cor amarela. Isso indica que a tarefa B começou o processo de impressão de suas mensagens, porém a tarefa A tomou para si o uso do barramento serial. 
  2. Tarefa A imprime a sua mensagem presente no loop infinito. 
  3. Tarefa B exibe a primeira linha da mensagem inicial. 
  4. A função loop imprime a sua mensagem, as mensagens das tarefas B e ExpIO são impressas com a mesma cor. A tarefa ExpIO, quando chega em seu loop infinito é bloqueada, aguardando o semáforo de sincronismo com a interrupção.  
  5. Essa mensagem é difícil de identificar de qual tarefa responsável pela impressão. 
  6. Tarefa B imprime a sua mensagem presente no loop infinito.  
  7. A função loop imprime a sua mensagem novamente.
  8. A função de interrupção é executada e libera o semáforo. 
  9. Tarefa ExpIO é executada, lê os dados de CI PCF8574, imprime os dados no barramento serial e obtém o semáforo. O semáforo é alterado para o estado ocupado e a tarefa ExpIO passa novamente para estado bloqueado esperando que um novo evento na interrupção ocorra e libere o semáforo.
  10. Esse item faz parte da execução com o semáforo dedicado para gerenciar o acesso ao barramento serial. A tarefa B é a primeira a ser executada, que imprime toda a sua mensagem inicial.
  11. A tarefa ExpIO é a segunda tarefa a ser executada, também imprime a sua mensagem inicial completa.
  12. Tarefa A é executada, imprime sua mensagem inicial. Neste ponto, já foi possível observar a atuação do semáforo ao gerenciar o uso do barramento serial, cada tarefa imprimiu toda a sua mensagem sem que nenhuma tarefa tomasse para si o uso do barramento. 
  13. A função é executada várias vezes, pois possui uma periodicidade maior  que as demais tarefas.
  14. A função de interrupção é executada e libera o semáforo de sincronismo com a tarefa ExpIO.
  15. Tarefa ExpIO é executada, lê os dados de CI PCF8574 e imprime os dados no barramento serial e obtém o semáforo. Com o semáforo ocupado, a tarefa passa novamente para estado bloqueado esperando um novo evento na interrupção.

Conclusão

Neste artigo da série sobre o FreeRTOS, foi apresentado o recurso de semáforo binário. Esse é um dos tipos de semáforo oferecidos pelo sistema operacional. Semáforo é um dos principais mecanismos para sincronização de tarefas, também podendo ser empregado para controle de acesso a recursos compartilhados entre processos. Detalhamos as principais API’s para o manuseio de semáforo disponível no FreeRTOS e foi apresentado uma aplicação de demonstração. O projeto desenvolvido traz dois modos de utilização do semáforo binário, a primeira como mecanismo de sincronização entre uma função que trata interrupção com uma tarefa, e o segundo para controle de acesso de recurso compartilhado entre processos. Ao final, analisamos o LOG de mensagem impressa no barramento serial para o melhor entendimento do algoritmo. Para os próximos artigos serão apresentados mais recursos do FreeRTOS. O que você achou? Você já utiliza o FreeRTOS em seus projetos? Deixe o seu comentário abaixo.

 


Sobre o autor


Evandro Teixeira

Desenvolvedor de sistemas embarcados com mais de 10 anos de experiência, com atuação em diferentes segmentos de mercado tais como eletromédicos, automação industrial, ITS (Sistemas Inteligentes de Transporte) e automotivo.


Eletrogate

10 de março de 2022

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!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!