Manual
do
Maker
.
com
Tenho o orgulho de ser o primeiro a apresentar o CLP i4.0 da VDC, um CLP com ESP32 e um monte de recursos que descreverei no decorrer do artigo. Prepare seu coração!
Vou começar pelas especificações, depois entro em detalhes técnicos e "filosóficos".
Invés das entradas analógicas, a VDC desenvolveu módulos LoRa com entrada analógica para conectar ao sensor, de modo a eliminar cabeamento para leituras dos sensores e eliminando os limites impostos por portas analógicas, seja por custo de hardware ou limitação física. Outra vantagem é a possibilidade de atualização de software por OTA. Um espetáculo!
Já no caso do WiFi, certamente a melhor opção é desabilitá-lo, assim como o bluetooth, por questões de segurança. O importante de ter o ESP32 nessa aplicação é o RTOS, dois núcleos de 240MHz e um monte de memória disponível para uma aplicação mais elaborada.
Como não programo em ladder, não posso indicar o caminho, mas o OpenPLC (veja aqui) provavelmente adicionará suporte ao ESP32 em algum momento, vale a pena acompanhar.
Uma segunda forma de programá-lo é através de blocos, utilizando o KBIDE. Por fim, podemos programá-lo livremente em nossas tradicionais IDEs.
As entradas e saída são gerenciadas via I2C, através do PCF8574. A melhor maneira de fazê-lo é utilizando bitwise, bem exemplificado nesse artigo. Como exemplo, estou adicionando um controle de vácuo que precisa executar uma rotina de intervalos (cujos intervalos definirei empiricamente essa semana diretamente no cliente). Trata-se de um processo de remoção de bolhas de um líquido, com sequência intervalar progressiva, para que a espuma que se forma no início da execução não adentre o tubo de vácuo. Para o programa de conceito, utilizei 5 intervalos proporcionais, apenas para testar a execução assíncrona dos relés, que são controlados por I2C.
Como já exemplificado em diversos artigos, utilizamos tasks para fazer execução assíncrona no ESP32, já que ele roda um sistema operacional de tempo real. Já escrevi sobre o controle de tasks nesse outro artigo, agora é um caso que se faz fundamental seu uso.
Como o desejado é controle assíncrono dos 6 relés, criei 6 manipuladores para gerenciar as tasks:
TaskHandle_t task_zero = NULL;
TaskHandle_t task_one = NULL;
TaskHandle_t task_two = NULL;
TaskHandle_t task_three = NULL;
TaskHandle_t task_four = NULL;
TaskHandle_t task_five = NULL;
As tasks são iniciadas em setup(), mas não devem permanecer em execução sem necessidade. Por essa razão adicionei um array para memória de estados; se o programa acabou de subir, as tarefas perceberão isso e se suspenderão. Quando chamadas novamente a partir da função loop(), executarão toda a sequência da função e se suspenderão novamente. O array de estado se chama starting no meu código, contendo 6 posições de unsigned char, apenas para guardar 0 ou 1. Em setup() inicializo esse array com valores 1.
memset(starting,1,sizeof(starting));
No código estão dispostas 6 tasks, lendo e escrevendo para um único PCF8574. Antes de escrever o novo estado do relé X, é necessário saber o estado dos demais relés para fazer a máscara, pois se quero ligar o relé 0 não posso simplesmente escrever 1 no bit 0 do PCF8574; se houver outro relé ligado, ele será desligado desse modo. Por essa razão, devemos usar máscara com a ajuda de bitwise. Agora imagine a seguinte situação:
Para evitar esse tipo de problema, algumas técnicas são aplicáveis. Pensei de imediato em utilizar mutex, que é um mecanismo de acesso exclusivo. Com o mutex, um recurso é travado, manipulado e então deve ser liberado por quem o travou. Se outros processos tentarem fazer a trava exclusiva, não conseguirão até que o recurso tenha sido liberado, mas os demais processos deverão aguardar pela liberação para que haja coerência nos temporizadores da função. Para tal, devemos criar o dispositivo de trava:
SemaphoreHandle_t myMutex;
E dentro da função devemos utilizar xSemaphoreTake e xSemaphoreGive.
uint8_t fromPCF(uint8_t addr){
xSemaphoreTake(myMutex,portMAX_DELAY);
Wire.requestFrom(addr,1);
uint8_t data_local = 0;
data_local = Wire.read();
xSemaphoreGive(myMutex);
return data_local;
}
Os parâmetros de xSemaphoreTake são a mutex criada e portMAX_DELAY, para aguardar indefinidamente pela liberação do recurso, até que se possa fazer a trava. No xSemaphoreGive passamos apenas a mutex que criamos. Escrevi um artigo exclusivo sobre mutex nesse link.
A leitura é feita com a função anterior, então aplica-se a máscara e escreve-se para o PCF8574. Para trocar o estado do pino utilizando bitwise, a maneira que achei mais adequada foi criar uma função que força o estado do pino; uma função para baixar, uma para levantar.
void pinLow(uint8_t addr, uint8_t position){
data = fromPCF(0x27);
data = data&~(1<<position);
Wire.beginTransmission(addr);
Wire.write(data);
Wire.endTransmission();
vTaskDelay(pdMS_TO_TICKS(50));
}
void pinHigh(uint8_t addr, uint8_t position){
data = fromPCF(0x27);
data = data|(1<<position);
Wire.beginTransmission(addr);
Wire.write(data);
Wire.endTransmission();
vTaskDelay(pdMS_TO_TICKS(50));
}
Essa é uma das vantagens de utilizar task. Além de fazermos execução assíncrona, com apenas uma função controlamos diferentes recursos. Para isso, precisamos definir os identificadores, os manipuladores e precisamos fazer a identificação do que deve ser feito de alguma forma. Me pareceu mais correto passar o relé a ser manipulado através de parâmetro da task. Repare que o parâmetro da task é do tipo void *, que precisará passar por um casting para que seu tipo seja redefinido. Já escrevi em detalhes sobre parâmetros de função para task nesse outro artigo,
Essa é uma função simples. Se fosse necessário maior complexidade, mais identificadores e outras coisas, poderíamos controlar de diversas maneiras; queues, structs etc. Por ser de baixa complexidade, ficou fácil torná-la genérica:
void taskRelays(void *pvParameters){
uint8_t &position = *(uint8_t *) pvParameters;
while (true){
uint8_t relay = position-48;
Serial.print("relay: ");
Serial.println(relay);
Serial.print("value: ");
Serial.println(starting[relay]);
if (starting[relay] == 1){
starting[relay] = 0;
vTaskSuspend(NULL);
}
for (int i=1000;i<=5000;i+=1000){
pinLow(0x27,relay);
vTaskDelay(pdMS_TO_TICKS(i));
pinHigh(0x27,relay);
vTaskDelay(pdMS_TO_TICKS(1000));
}
Serial.println("desligando");
pinHigh(0x27,relay);
vTaskSuspend(NULL);
}
}
Podemos utilizar o gerenciador do próprio sistema para iniciar as tasks e o sistema automaticamente define em qual processador executar e quando executar as tarefas. Também podemos iniciar uma a uma, sem especificar um núcleo. Eu prefiro controlar manualmente e definir o núcleo.
A criação das tasks ficou dessa maneira:
xTaskCreatePinnedToCore(taskRelays,"relay0",10000,(void*) "0",0,&task_zero,0);
xTaskCreatePinnedToCore(taskRelays,"relay1",10000,(void*) "1",0,&task_one,0);
xTaskCreatePinnedToCore(taskRelays,"relay2",10000,(void*) "2",0,&task_two,0);
xTaskCreatePinnedToCore(taskRelays,"relay3",10000,(void*) "3",0,&task_three,0);
xTaskCreatePinnedToCore(taskRelays,"relay4",10000,(void*) "4",0,&task_four,0);
xTaskCreatePinnedToCore(taskRelays,"relay5",10000,(void*) "5",0,&task_five,0);
O primeiro parâmetro é a função. Usamos a mesma função para todas as tarefas, mudando o identificador, o parâmetro e o manipulador. O nome que acredito ser mais adequado para o manipulador seria "relay_zero", invés de "task_zero", mas de qualquer modo, a identificação por número é para saber a qual bit pertence.
Para testar, utilizei a serial do CLP com ESP32 como interface com o programa nesse primeiro momento. Basta digitar o número de 0 a 5 para executar a respectiva task. Isso pode ser feito a qualquer momento e todos os relés poderão ser executados, cada qual será acionado em seu tempo, sem interferir nos demais.
O código completo ficou assim:
#include <Arduino.h>
#include <initializer_list>
#include <Wire.h>
/*
Sem usar o include abaixo, dá pra controlar desse jeito:
while( xSemaphoreTake( xSemaphore, portMAX_DELAY ) != pdPASS );
*/
#define INCLUDE_vTaskSuspend 1
uint8_t starting[6];
TaskHandle_t task_zero = NULL;
TaskHandle_t task_one = NULL;
TaskHandle_t task_two = NULL;
TaskHandle_t task_three = NULL;
TaskHandle_t task_four = NULL;
TaskHandle_t task_five = NULL;
SemaphoreHandle_t myMutex;
String vol = "-1";
uint8_t data = -1;
uint8_t fromPCF(uint8_t addr){
xSemaphoreTake(myMutex,portMAX_DELAY);
Wire.requestFrom(addr,1);
uint8_t data_local = 0;
data_local = Wire.read();
xSemaphoreGive(myMutex);
return data_local;
}
void pinLow(uint8_t addr, uint8_t position){
data = fromPCF(0x27);
data = data&~(1<<position);
Wire.beginTransmission(addr);
Wire.write(data);
Wire.endTransmission();
vTaskDelay(pdMS_TO_TICKS(50));
}
void pinHigh(uint8_t addr, uint8_t position){
data = fromPCF(0x27);
data = data|(1<<position);
Wire.beginTransmission(addr);
Wire.write(data);
Wire.endTransmission();
vTaskDelay(pdMS_TO_TICKS(50));
}
void taskRelays(void *pvParameters){
uint8_t &position = *(uint8_t *) pvParameters;
while (true){
uint8_t relay = position-48;
if (starting[relay] == 1){
starting[relay] = 0;
vTaskSuspend(NULL);
}
for (int i=1000;i<=5000;i+=1000){
pinLow(0x27,relay);
vTaskDelay(pdMS_TO_TICKS(i));
pinHigh(0x27,relay);
vTaskDelay(pdMS_TO_TICKS(1000));
}
Serial.println("desligando");
pinHigh(0x27,relay);
vTaskSuspend(NULL);
}
}
void setup() {
memset(starting,1,sizeof(starting));
myMutex = xSemaphoreCreateMutex();
Wire.begin(21,22);
vTaskDelay(pdMS_TO_TICKS(100));
Wire.beginTransmission(0x27);
Wire.write(255);
Wire.endTransmission();
Serial.begin(9600);
delay(2000);
Serial.println("Started");
xTaskCreatePinnedToCore(taskRelays,"relay0",10000,(void*) "0",0,&task_zero,0);
xTaskCreatePinnedToCore(taskRelays,"relay1",10000,(void*) "1",0,&task_one,0);
xTaskCreatePinnedToCore(taskRelays,"relay2",10000,(void*) "2",0,&task_two,0);
xTaskCreatePinnedToCore(taskRelays,"relay3",10000,(void*) "3",0,&task_three,0);
xTaskCreatePinnedToCore(taskRelays,"relay4",10000,(void*) "4",0,&task_four,0);
xTaskCreatePinnedToCore(taskRelays,"relay5",10000,(void*) "5",0,&task_five,0);
}
void loop() {
while (Serial.available()){
vol = Serial.readString();
Serial.println(vol.toInt());
}
switch (vol.toInt()){
case 0:
vTaskResume(task_zero);
break;
case 1:
vTaskResume(task_one);
break;
case 2:
vTaskResume(task_two);
break;
case 3:
vTaskResume(task_three);
break;
case 4:
vTaskResume(task_four);
break;
case 5:
vTaskResume(task_five);
break;
}
vol = "-1";
}
Se estivéssemos utilizando os GPIO (um para cada relé) a execução seria estupidamente óbvia. Fazer a execução assíncrona dos bits em um único controlador é uma tarefa mais elaborada, por isso que apesar de visualmente ser comum, a beleza se esconde nos bastidores do código. O resultado desse código no CLP com ESP32:
Essa placa da VDC (indústria brasileira) está disponível na AFEletronica. Visite o link para conferir o CLP com ESP32. Aproveite a oportunidade de pegar esse mega lançamento!
A placa de transmissão LoRa para esse CLP também está disponível e já foi artigo, confira aqui.
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.