Manual
do
Maker
.
com
Esse assunto é um dos que mais me agrada. Mas primeiro permita-me discorrer brevemente a respeito de tasks, antes que vejamos como utilizar mutex no FreeRTOS. Se não leu os artigos anteriores, recomendo:
Selecionar uma CPU para executar tasks no ESP32 Atribuir tarefa a um núcleo do ESP32
E não posso deixar de indicar a CurtoCircuito para essa compra, que é o novo nome de loja para nossos brinquedos.
O conceito de task é muito parecido com thread, mas eu acredito fortemente que em sistemas operacionais de tempo real, 'task' é uma definição melhor, porque dentro da filosofia das linguagens, uma thread tem características e conceitos muito específicos.
Um RTOS tem a habilidade de ser multitask, ou multitarefa. Criando tasks (como pode ser visto nos 2 artigos anteriores) escrevemos bem menos código e desperdiçamos menos processamento, uma vez que determinado código só será processado quando requisitado, invés de ter uma execução infinita em um loop. Mas quanto mais recursos, mais controle é necessário ter. Por exemplo, temos duas tarefas distintas que compartilham dados de uma mesma variável. Para que não haja colisão das tarefas precisamos, de alguma maneira, sinalizar que a variável está sendo manipulada, e para isso podemos utilizar semáforos, semáforos binários ou MUTEX. O MUTEX é bastante simples de usar e resolve a grande maioria dos problemas. Já fiz programas bem complexos com "muitas", mas "muitas" threads mesmo, e controlava variáveis compartilhadas através de MUTEX.
A palavra MUTEX significa MUTual EXclusion. Quando um recurso faz uso do mutex, deve-se ter garantido o acesso através de um block no mutex. Quando termina-se o uso desse recurso, devolve-se a permissão de acesso ao recurso, caso contrário, nunca mais ninguém faz o acesso a essa porção de código ou váriável. Vai ficar mais claro.
Uma boa analogia é a chave de uma porta. Suponhamos 2 pessoas morando em uma mesma casa, mas só possuem uma chave, que fica embaixo do capacho. Quem pegar a chave primeiro abrirá a porta, entrará e fechará. A próxima pessoa só conseguirá fazer o mesmo quando a chave for devolvida para debaixo do capacho. O tempo em que a pessoa ficou com a chave e o que ela fez (ou está fazendo) não pode ser interrompido. Somente poderão haver modificações quando o acesso for novamente liberado. Isso evita colisões e inconsistências de dados.
Sou fraco com analogias, mas suponhamos que Chaves tem 6 bolachas e dois amigos; Kiko afastado e Chiquinha perto do Chaves. Daí Chiquinha vai até Kiko perguntar se ele quer duas bolachas, porque o Chaves tem 6. Aí ele diz que sim, mas quando Chiquinha volta para pegar 1/3, Chaves já tinha comido 3. Daí ele volta pra perguntar pro Kiko se pode ser 1 só mesmo, porque o Chaves tem boca de sapo. Ele diz que sim (já não tão feliz). Daí Chiquinha vai pegar a bolacha e Chaves já tinha comido mais 2. Ou seja, não teve controle.
A mesma situação; Chaves está com 6 bolachas e vai dividir, se Chiquinha e Kiko quiserem. Kiko pega 4 bolachas da mão do Chaves e leva até a Chiquinha. Chiquinha só quis 1 bolacha, então Kiko volta ao Chaves e lhe devolve uma. Agora Chaves pode fazer o que quiser com aquela bolacha restante. Percebeu que houve um bloqueador para o faminto Chaves?
Última hipótese. Tem duas bolachas, uma pro Chaves e uma para o Kiko, em cima de um prato. Ambos pegam na mesma bolacha. Se Chaves tivesse ordenado uma fila para distribuir, essa "colisão" não ocorreria, ou seja, sempre devemos ter alguém controlando algo que pode ser manipulado por mais de 1 sujeito.
O mutex faz um lock em determinada variável, uma função manipula o valor e então libera para o próximo acesso (unlock), que poderá ser feita pela mesma função ou uma outra em um momento diferente.
No FreeRTOS temos uma regra extra; mutex não pode ser utilizado dentro de funções de interrupção.
Para criação e interação com o mutex, temos 3 funções:
Com a primeira, criamos um mutex. Com a segunda, obtemos o lock e com a terceira, liberamos o mutex.
Precisamos criar uma variável para o mutex. Essa variável é do tipo SemaphoreHandle_t. Um exemplo:
SemaphoreHandle_t myMutex;
void vATask(void * pvParameters){
xSemaphore = xSemaphoreCreateMutex();
if( xSemaphore != NULL ){
Serial.println("mutex pronto para uso");
return;
}
Serial.println("Nao pude criar o mutex");
}
Para usar o mutex, agora podemos fazer algo como:
void teste(char *str){
xSemaphoreTake(myMutex,portMAX_DELAY);
string_global = str;
serial.println(string_global);
xSemaphoreGive(myMutex);
}
Agora, acessando através de tasks:
String string_global = "";
void teste(char *str){
xSemaphoreTake(myMutex,portMAX_DELAY);
string_global = str;
serial.println(string_global);
xSemaphoreGive(myMutex);
}
void setup(){
xMutex = xSemaphoreCreateMutex();
if(xMutex != NULL){
xTaskCreate( teste, "Print1", 1000,"Task 1\r\n", 1, NULL);
xTaskCreate( teste, "Print2", 1000,"Task 2\r\n", 2, NULL);
vTaskStartScheduler();
}
}
void loop(){
}
Repare que as prioridades são diferentes e não estamos especificando em qual CPU devem ser executadas, mas o importante aqui é a prioridade. Com isso teremos um resultado sem conflitos.
Semáforos são um pouquinho mais complicados, mas escrevo a respeito em outro artigo.
Revisão: Ricardo Amaral de Andrade
Inscreva-se no nosso canal Manual do Maker no YouTube.
Também estamos no Instagram.
Autor do blog "Do bit Ao Byte / Manual do Maker".
Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.