CLP i4.0 da VDC – CLP com ESP32

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!

CLP i4.0 da VDC – especificações

Vou começar pelas especificações, depois entro em detalhes técnicos e “filosóficos”.

  • Tensão de operação: 12V a 28V com 2A
  • 6 mini relés com NA, CO, NF para até 250VAC@5A ou 30VDC@5A
  • Dimensões: 105,86mm x 90,18mm x 67,17mm
  • ESP32 como MCU
  • Porta USB para comunicação
  • Slot para display touch 2.4″
  • Slot para display OLED 0,96″
  • RS485 para utilização RAW, serial ou MODBUS
  • 8 entradas digitais
  • 6 saídas (mini relés)
  • 1 saída transistor open collector
  • 3 botões de funções (programação, reset, uso geral)
  • Slot para LoRa1276
  • RTC
  • Slot para módulo Ethernet

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.

Programação

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 fazer execução assíncrona no CLP com ESP32?

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));

Como evitar conflito no acesso ao PCF8574?

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:

  • Relé 0 precisa ser ligado. Valor atual é lido (0b00000000).
  • Relé 1 precisa ser ligado. Valor atual é lido (0b00000000).
  • Relé 0 foi ligado (0b00000001).
  • Relé 1 foi ligado, mas como o valor lido não contemplava o novo estado do relé 0, ele foi desligado (0b00000010).

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 xSemaphoreTakexSemaphoreGive.

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.

Funções para levantar bit específico do PCF8574

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));
}

Escrever uma task para múltiplas execuções no CLP com ESP32

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);
    }
}

Inicializar tasks

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.

Executar tasks pela serial

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";
    
}

Vídeo

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:

Onde comprar o PLC 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!

Transmissores

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

Djames Suhanko

Sobre o autor: Djames Suhanko é Perito Forense Digital. Já atuou com deployer em sistemas de missão critica em diversos países pelo mundão. Programador Shell, Python, C, C++ e Qt, tendo contato com embarcados ( ora profissionalmente, ora por lazer ) desde 2009.