Projetos no Arduino com FreeRTOS – semáforo 2

Nos artigos Projetos no Arduino com FreeRTOS – conceitos, tasks e consumo de memória por task e Projetos no Arduino com FreeRTOS – filas (queues), vimos os principais conceitos do FreeRTOS, como medir consumo de memória por uma task, como utilizar o FreeRTOS no Arduino e uso das filas (queues). Agora, vamos prosseguir com o assunto, mostrando um outro recurso fundamental: a utilização de um semáforo no FreeRTOS.

Material necessário

A lista de material aqui é idêntica ao do artigo Projetos no Arduino com FreeRTOS – filas (queues):

Semáforos – conceito principal

Um semáforo é um recurso disponibilizado pelo FreeRTOS para permitir que recursos únicos (uma porta serial, um GPIO, etc.) tenham um controle de acesso, de modo a poderem ser compartilhados entre várias tarefas distintas. Ou seja, os semáforos servem para controlar o acesso a recursos quando pode haver a chance, remota ou não, de um recurso (como uma interface de comunicação ou um GPIO, por exemplo) ser usado por duas ou mais tarefas simultaneamente.
As consequências de não se usar um semáforo podem ser desastrosas. No caso de uma porta serial, pode-se ter um buffer de envio ou recepção corrompido (duas ou mais tarefas alterando tal buffer), algo que pode até não ser tão prejudicial. Mas já imaginou se o recurso concorrido é um GPIO, onde esse GPIO controla algo forte e grande que, se manipulado incorretamente, pode causar danos (ou até mesmo morte) às pessoas? O resultado pode ser trágico.

O semáforo não tem esse nome a toa. Ele se assemelha realmente ao semáforo de trânsito da vida real. No caso, as ruas / vias são recursos compartilhados por carros de um cruzamento, e se todos tentarem acessar a via ao mesmo tempo, acidentes acontecerão com certeza. Logo, o semáforo libera o tráfego em uma via de cada vez, de modo a evitar acidentes. Nessa analogia, as ruas são os recursos visados pelas tarefas, e as tarefas são os carros.

Figura 1 - semáforos no FreeRTOS funcionam como na vida real: organizam o compartilhamento de recursos para não haver acidentes / desastres.
Figura 1 – semáforos no FreeRTOS funcionam como na vida real: organizam o compartilhamento de recursos para não haver acidentes / desastres.

Existem outros tipos de semáforo no FreeRTOS, mas o que iremos falar aqui é o semáforo do tipo MUTEX. O termo MUTEX significa Mutual Exclusion (exclusão mútua), e tem como objetivo restringir o acesso a um recurso (uma interface de comunicação serial, por exemplo) a uma única tarefa, enquanto esta não “liberar” o semáforo. Logo, o ciclo de uso de semáforos MUTEX é o seguinte:

  1. Uma tarefa tenta obter o semáforo;
  2. Se conseguir, ou seja, se nenhuma outra tarefa estiver utilizando o semáforo / recurso compartilhado no momento, a tarefa segue a execução normal de suas ações dependentes deste semáforo (manipulação de recursos compartilhados, como uma interface serial, por exemplo);
  3. Finalizado o uso dos recursos compartilhados, o semáforo é liberado. Dessa forma, o recurso protegido pelo semáforo pode ser utilizado por outra tarefa.

Em suma, um semáforo é uma ferramenta provida pelo FreeRTOS que sinaliza se determinado recurso está sendo utilizado naquele momento por alguma tarefa ou não. Dessa forma, a verificação do semáforo antes de utilizar um recurso compartilhado é suficientemente segura para evitar problemas ao se compartilhar recursos entre tarefas distintas. O fato de o semáforo ser um recurso provido pelo sistema operacional dá ainda mais segurança, uma vez que a chance de algo estar errado na implementação do semáforo é muito menor do que se fosse implementada do zero novamente pelo projetista.

Declaração e uso um semáforo no FreeRTOS

Quando estiver usando o FreeRTOS, um semáforo do tipo MUTEX deve ser declarado da seguinte maneira:

SemaphoreHandle_t xSemaforo_teste;

Após a declaração, é preciso criar o semáforo do tipo desejado, o que neste caso é o MUTEX. Sua criação deve ser feita da seguinte maneira:

xSemaforo_teste = xSemaphoreCreateMutex();

Após criado, o semáforo pode ser finalmente utilizado. O uso do semáforo é feito conforme a seguir:

Primeiramente, para obter o controle do semáforo (para proteger um recurso compartilhado entre outras tarefas), utilize o código abaixo. O “obter controle de um semáforo” é referido como take no FreeRTOS.

if( xSemaphoreTake( xSemaforo_teste, ( TickType_t ) NUMERO_TICKS_ESPERA ) == pdTRUE )
{
    /*  fazer aqui as rotinas que usam o recurso protegido pelo semáforo */
}

Onde NUMERO_TICKS_ESPERA significa o número máximo de ticks do processador que deve ser esperado para obter o controle do semáforo. Porém, há disponível também uma opção para este parâmetro que torna o tempo de espera “infinito” (indeterminado): portMAX_DELAY. Utilizando o portMAX_DELAY em NUMERO_TICKS_ESPERA, a tarefa é pausada no xSemaphoreTake até que o semáforo em questão esteja liberado para uso.

Quando não precisar mais proteger o recurso compartilhado, faça a liberação do semáforo, de forma que outras tarefas possam utilizar tal recurso. Para isso, utilize a linha abaixo. O “liberar o semáforo” é referido como give no FreeRTOS.

xSemaphoreGive( xSemaforo_teste );

Formas de tomar o controle (take) do semáforo no FreeRTOS

O FreeRTOS disponibiliza várias formas diferentes de tomar o controle de um semáforo. A razão disso é que, durante o desenvolvimento, há várias situações possíveis nas quais podemos ter que fazer isso (como, por exemplo, dentro de tratamentos de interrupções, dentro de tarefas e por aí vai). Logo, para o correto funcionamento do seu projeto, é essencial saber utilizar a forma adequada para a situação em que a task se encontra.

Segue abaixo as principais formas de tomar o controle de um semáforo no FreeRTOS:

  • xSemaphoreTake: como já visto neste post, toma o controle do semáforo considerando um tempo máximo de espera (ou permanece esperando até conseguir). Esta forma de obter o controle não deve ser usada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xSemaphoreTakeFromISR:  toma o controle do semáforo em um tratamento de interrupção. Aqui, é importante ressaltar que não há parâmetro de espera para tomar o controle do semáforo. A razão disso é que toda interrupção (seja ela qual for) deve sempre ser tratada o mais rápido possível. Logo não faz sentido tem um tempo de espera neste contexto.
    Esta forma de obter o controle deve ser usada exclusivamente dentro do tratamento de uma interrupção (ou dentro de callbacks).

Para saber mais sobre, acesse esse link da documentação oficial do FreeRTOS: https://www.freertos.org/a00113.html

Formas de liberar (give) o semáforo no FreeRTOS

Assim como ocorre no caso de tomar o controle de um semáforo, também há formas diferentes de liberar um semáforo no FreeRTOS. As principais estão listadas abaixo:

  • xSemaphoreGive: como já visto neste post, libera o semáforo. Esta forma de liberar o semáforo não deve ser usada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xSemaphoreGiveFromISR:  libera o semáforo em um tratamento de interrupção. Esta forma liberar o semáforo deve ser usada exclusivamente dentro do tratamento de uma interrupção (ou dentro de callbacks).

Para saber mais sobre, acesse esse link da documentação oficial do FreeRTOS: https://www.freertos.org/a00113.html

Boas práticas de uso de um semáforo

Assim como pra tudo na vida, há boas práticas para o uso de um semáforo. Estas podem evitar grandes dores de cabeça quando você precisar fazer debug de algo com FreeRTOS.

Figura 2 - seguir boas práticas é fundamental para não dar um salto no escuro
Figura 2 – seguir boas práticas é fundamental para não dar um salto no escuro

Tais boas práticas estão a seguir:

  1. Atenção às prioridades: conforme dito no artigo Projetos no Arduino com FreeRTOS – conceitos, tasks e consumo de memória por task, no FreeRTOS as tarefas possuem prioridades. Isso significa que se duas tarefas “brigarem” pelo controle de um semáforo, a que tiver a maior prioridade vai ganhar a briga. Portanto, pense bem nas prioridades das tarefas que irão fazer uso de um semáforo, de forma a não inibir nenhuma delas do controle do mesmo.
  2. Não use mais de um semáforo por recurso compartilhado: siga a regra básica de “um semáforo por recurso compartilhado”. Usar mais de um semáforo em um só recurso gera confusão, para entender e debugar o código-fonte, além de ter grandes chances de mal funcionamento.
  3. Use com sabedoria a macro portMAX_DELAY: em algumas situações é necessário que o controle do semáforo seja tomado a todo custo, não importa o tempo de espera. Porém, na grande maioria das situações, isso não é verdade. Portanto, só utilize a macro portMAX_DELAY em caso de necessidade.

Exemplo prático de uso de semáforo

É chegada a hora de botar a mão na massa! Vamos aprimorar o exemplo que vimos antes (no artigo Projetos no Arduino com FreeRTOS – filas (queues)), utilizando um semáforo para controlar o acesso à porta serial, de modo que somente uma tarefa possa utilizá-la num determinado instante de tempo. Todas as tarefas do exemplo agora irão escrever na serial seu valor de High Water Mark.

Para este projeto, monte o seguinte circuito esquemático:

Figura 3 - circuito esquemático do projeto
Figura 3 – circuito esquemático do projeto

O código-fonte deste projeto está disponível abaixo.
IMPORTANTE: leia com atenção os comentários do código-fonte para total entendimento do mesmo.

#include <Arduino_FreeRTOS.h>
#include <queue.h>
#include <task.h>
#include <semphr.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

/* defines - LCD */
#define LCD_16X2_CLEAN_LINE     "                "
#define LCD_16X2_I2C_ADDRESS    0x27
#define LCD_16X2_COLS           16
#define LCD_16X2_ROWS           2


/* defines - LED */
#define LED_PIN 12

/* defines - ADC */
#define ADC_MAX 1023.0
#define MAX_VOLTAGE_ADC 5.0

/* tasks */
void task_breathing_light( void *pvParameters );
void task_serial( void *pvParameters );
void task_lcd( void *pvParameters );
void task_sensor( void *pvParameters );
void task_led( void *pvParameters );

/* Variaveis relacionadas ao LCD */
LiquidCrystal_I2C lcd(LCD_16X2_I2C_ADDRESS, LCD_16X2_COLS, LCD_16X2_ROWS);

/* filas (queues) */
QueueHandle_t xQueue_LCD;

/* semaforos utilizados */
SemaphoreHandle_t xSerial_semaphore;


void setup() 
{
    /* Inicializa serial (baudrate 9600) */
    Serial.begin(9600);
   
    /* Inicializa o LCD, liga o backlight e limpa o LCD */
    lcd.init();
    lcd.backlight();
    lcd.clear();

    /* Inicializa e configura GPIO do LED */
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);
    
    while (!Serial) {
    ; /* Somente vai em frente quando a serial estiver pronta para funcionar */
    }

    /* Criação da fila (queue) */
    xQueue_LCD = xQueueCreate( 1, sizeof( float ) );

    /* Criação dos semaforos */
    xSerial_semaphore = xSemaphoreCreateMutex();

    if (xSerial_semaphore == NULL)
    {
        Serial.println("Erro: nao e possivel criar o semaforo");
        while(1); /* Sem semaforo o funcionamento esta comprometido. Nada mais deve ser feito. */
    }

    /* Criação das tarefas */
    xTaskCreate( task_sensor /* Funcao a qual esta implementado o que a tarefa deve fazer */
               , (const portCHAR *)"sensor" /* Nome (para fins de debug, se necessário) */
               , 128 /* Tamanho da stack (em words) reservada para essa tarefa */
               , NULL /* Parametros passados (nesse caso, não há) */
               , 3 /* Prioridade */
               , NULL ); /* Handle da tarefa, opcional (nesse caso, não há) */

    xTaskCreate(task_lcd
               , (const portCHAR *) "LCD"
               , 156
               , NULL
               , 2
               , NULL );

    xTaskCreate(task_led
               , (const portCHAR *)"LED"
               , 128
               , NULL
               , 1
               , NULL );

    /* A partir deste momento, o scheduler de tarefas entra em ação e as tarefas executam */
}

void loop()
{
    /* Tudo é executado nas tarefas. Há nada a ser feito aqui. */
}

/* --------------------------------------------------*/
/* ---------------------- Tarefas -------------------*/
/* --------------------------------------------------*/

void task_sensor( void *pvParameters )
{
    (void) pvParameters;
    int adc_read=0;
    UBaseType_t uxHighWaterMark;
    float voltage = 0.0;

    while(1)
    {
          adc_read = analogRead(0);
          voltage = ((float)adc_read/ADC_MAX)*MAX_VOLTAGE_ADC;
    
          /* Envia tensão lida em A0 para as tarefas a partir de filas */
          xQueueOverwrite(xQueue_LCD, (void *)&voltage);
          xQueueOverwrite(xQueue_LED, (void *)&voltage);

         /* Espera um segundo */
         vTaskDelay( 1000 / portTICK_PERIOD_MS );

        /* Para fins de teste de ocupação de stack, printa na serial o high water mark */
        xSemaphoreTake(xSerial_semaphore, portMAX_DELAY );
        uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
        Serial.print("task_sensor high water mark (words): ");
        Serial.println(uxHighWaterMark);
        Serial.println("---");
        xSemaphoreGive(xSerial_semaphore);
   }
}


void task_lcd( void *pvParameters )
{
    (void) pvParameters;
    float voltage_rcv = 0.0;
    UBaseType_t uxHighWaterMark;
 
    while(1)
    {
        /* Espera até algo ser recebido na queue */
        xQueueReceive(xQueue_LCD, (void *)&voltage_rcv, portMAX_DELAY);

        /* Uma vez recebida a informação na queue, a escreve no display LCD */
        lcd.setCursor(0,0);
        lcd.print("Tensao ADC:");
        lcd.setCursor(0,1);
        lcd.print(LCD_16X2_CLEAN_LINE);
        lcd.setCursor(0,1);
        lcd.print(voltage_rcv);
        lcd.setCursor(15,1);
        lcd.print("V");

        /* Para fins de teste de ocupação de stack, printa na serial o high water mark */
        xSemaphoreTake(xSerial_semaphore, portMAX_DELAY );
        uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
        Serial.print("task_lcd high water mark (words): ");
        Serial.println(uxHighWaterMark);
        Serial.println("---");
        xSemaphoreGive(xSerial_semaphore);
    }
}

void task_led( void *pvParameters )
{
    (void) pvParameters;
    float voltage_rcv = 0.0;
    UBaseType_t uxHighWaterMark;

    while(1)
    {
        /* ATENÇÃO: NUNCA USE A FUNÇÃO delay() QUANDO ESTIVER USANDO FREERTOS!
           em seu lugar, use a função vTaskDelay( tempo / portTICK_PERIOD_MS );, substituindo "tempo" pelo tempo de delay 
          (em ms) desejado.
        */
        digitalWrite(LED_PIN, HIGH);
        vTaskDelay( 250 / portTICK_PERIOD_MS );
        digitalWrite(LED_PIN, LOW);
        vTaskDelay( 250 / portTICK_PERIOD_MS );

        /* Para fins de teste de ocupação de stack, printa na serial o high water mark */
        xSemaphoreTake(xSerial_semaphore, portMAX_DELAY );
        uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
        Serial.print("task_led high water mark (words): ");
        Serial.println(uxHighWaterMark);
        Serial.println("---");
        xSemaphoreGive(xSerial_semaphore);
    }
}

Recapitulando: aqui, foi utilizado um semáforo para garantir que a porta serial (recurso compartilhado com todas as tarefas) seja usado em somente uma tarefa por vez. Cada tarefa irá escrever na serial seu High Water Mark, sem chance de “interferência” das outras tarefas que irão também utilizar a porta serial.

Gostou deste post sobre semáforos no FreeRTOS? Deixe seu comentário logo abaixo. Em caso de dúvidas, caso queira trocar uma ideia, ou até mesmo dividir seu projeto, acesse nosso Fórum!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

2 Comentários

  1. Olá! Assisti no Youtube um vídeo onde usam fitas de Led endereçáveis controladas pelo arduino nano que tem efeito de chama. Tentei copiar o código do link no simulador do tinkercad mas não funcionou, como posso resolver isso?

  2. Muito bom !