Manual

do

Maker

.

com

Duas seriais no ESP32, ESP32 com Nextion e motor de passo

Duas seriais no ESP32, ESP32 com Nextion e motor de passo

Usei tanta coisa nesse teste que até o título para esse artigo ficou difícil de escolher.

Resolvi fazer um teste do ESP32 com Nextion e como eu ainda estou utilizando o ESP32 do artigo anterior para controlar motor de passo, resolvi adicionar apenas o display. Com isso, nesse artigo você verá o controle do motor de passo com comandos enviados ao ESP32 a partir do display Nextion, a configuração de uma segunda serial para uso do display, a primeira serial (para subir o programa, debug e interação), controle do EasyDriver com deslocamento de bits usando bitwise em um PCF8574 e o pinout do ESP32 Wemos, do nosso parceiro CurtoCircuito. É um artigo apenas para fins didáticos, mas bem elaborado, hum?

Display Nextion

Já escrevi 6 artigos anteriores sobre o display Nextion, mas caso não tenha lido nenhum deles, recomendo que dê uma olhada nos artigos anteriores. Trata-se de um display HMI (Human Machine Interface), que é controlado por um processador ARM e possui uma IDE de desenvolvimento que, dependendo do que for utilizar, não precisará de 1 linha de código sequer para fazer uma interface bonita.

Esse display você encontra no nosso parceiro MASUGUX nas versões de 3.2" (relativamente barato, considerando um display, processador integrado e IDE de altíssimo nível). A versão de 4.3" não é tão em conta, mas essa polegada a mais pode ser o que você precisa em seu projeto.

Configurar duas seriais no ESP32

O código pode ser um pouco impactante para puristas, uma vez que estou fazendo uso de recursos da API do Arduino e do ESP32. A primeira serial está conecta à USB e se for utilizada para o display, não haverá como interagir também pelo computador para fins de debug, por exemplo. Outro ponto importante é que, antes de subir o firmware, o fio RX do display deve ser desconectado, caso contrário o upload falhará.

Pinout do ESP32 Wemos

Para fazer o wiring, é necessário saber o pinout da board, claro. Eu fiz esse desenho no próprio código para servir de referência:

esp32_wemos-pinout_to_serial.webp

Nesse desenho nota-se, além dos pinos, o wiring para I2C, alimentação do PCF8574, aterramento do EasyDriver e conexão da UART com o display. O que não aparece aí é o GND comum do display com o ESP32. O PCF8574 permite interconectar mais módulos para que seja possível expandir os dispositivos conectados via I2C, daí utilizei a saída GND do PCF8574 (que já está com aterramento comum ao ESP32).

O display não pode ser alimentado diretamente da MCU e nem do EasyDriver porque a saída 5V do EasyDriver só fornece 50mA e o display precisa de 85mA. Daí utilizei uma porta USB do notebook para alimentar o display, mas em um projeto seria necessário uma fonte step down ou um regulador de tensão para pegar diretamente da fonte, que no meu caso é 12V, para fazer a alimentação do motor de passo  Nema17. Alimentar o display com menos corrente o queimará facilmente, conforme orientações do fabricante.

Codificando

O código precisará de um pouco de explicação porque tem alguns conceitos que precisam de exclarecimento. Primeiramente, o formato da mensagem.

Para ver como a mensagem chegaria no ESP32, fiz um loop imprimindo byte a byte da mensagem, só para ver se haveria alguma anomalia, mas o formato haveria de ser o mesmo da urna eletrônica:

#Formato da mensagem:
#101 - msg
#0   -
#13  - ID do componente (Confirma)
#0   -
#255 - Termino
#255 - Termino
#255 - Termino
#17  - valor do candidato
#0   -

Depois, comecei a implementar código sobre o sketch que utilizei para a urna eletrônica, vou discorrendo conforme a implementação.

Adicionando as seriais

Utilizei a serial padrão colocando em setup() a inicialização dela, como fazemos em qualquer Arduino.

Serial.begin(115200);

Para a segunda serial, utilizei a biblioteca HardwareSerial, instanciei um objeto e inicializei também em setup.

Include da biblioteca:

#include <HardwareSerial.h>

Instancia do objeto antes do setup e fora de qualquer função:

HardwareSerial Serial1(2);

E em setup, logo abaixo da inicialização da serial padrão, fiz a inicialização da segunda serial, passando baud rate, pino RX e pino TX a utilizar:

Serial1.begin(9600,SERIAL_8N1,S1RX,S1TX);

Os pinos estão nomeados porque criei dois define próximo aos includes.

Criando uma thread

Se você não está familiarizado com os recursos do FreeRTOS, principalmente o do ESP32, recomendo que clique ali em cima no menu ESP32 e dê uma lida nos artigos relacionados, o ESP32 é poderoso demais para ser tratado como um Arduino.

No RTOS, uma task funciona de forma muito similar a uma thread. Com a task, podemos criar tarefas assíncronas. Como quero poder utilizar tanto a serial padrão como a do display, com a task garantimos a disponibilidade de ambas. No caso, fiz o monitoramento da segunda serial criando uma task no núcleo 0 do ESP32, uma vez que setup() e loop() rodam no núcleo 1.

Não vou repetir aqui todos os conceitos relacionados à criação de uma task, por isso recomendo mais uma vez que leia os artigos relacionados ao ESP32. Depois de expor o código da função da task, discorro a respeito.

/* Quando chegar dados na S1, deverá alimentar uma variável e levantar uma flag para ser lida pelo core principal. Essa task é levantada no setup()*/
void toS1(void *pvParameters){
    while (true){
        vTaskDelay(pdMS_TO_TICKS(500));
        if (Serial1.available()){
            Serial.println("dados chegando do display");
            Serial1.readBytesUntil('\n',displaySaids,9);
            thereIsMsgFromDisplay = true;
        }
    }
}

O que fiz aqui foi declarar uma função toS1 cujo parâmetro de função é void (porque sempre tem que ser na função da task). Deixei a leitura em loop utilizando um delay de 500ms. No ESP32 esse é o jeito correto de se fazer, mas a função delay do Arduino está modificada para utilizar a função vTaskDelay.

Declarada a função, no setup devemos iniciar a task, atrelada ao núcleo 0:

xTaskCreatePinnedToCore(toS1,"toS1",10000,NULL,0,NULL,0);

Enumerador

Podemos criar defines para cada pino do EasyDriver ou então, querendo um código mais organizado, podemos fazer um enumerador. Um enumerador tem uma estrutura semelhante a uma struct, mas podemos (ou não) definir valores sem definir o tipo numérico. Se não atribuirmos valores ao enumerador, ele se dará de forma automática e ordenada. No exemplo abaixo, motor_step é o bit 0 do PCF8574, enquanto motor_ms2 é bit 5. Depois mostro como manipular os bits mais adiante, mas tenho um artigo dedicado ao controle do PCF8574 utilizando bitwise, se interessar, sugiro que leia.

enum easyDriverPinMap{
  motor_step,
  motor_direction,
  motor_sleep,
  motor_ms1,
  motor_enable,
  motor_ms2
};

Na hora de utilizar, é só referenciar o nome do componente dessa estrutura. A função a seguir mostra seu uso. Essa função configura os bits relacionados à velocidade do motor, colocando-os em HIGH.

void motorSpeed(){
  //full step
    stat = stat|1<<motor_ms1;
    stat = stat|1<<motor_ms2;
}

Se não tem experiência com o EasyDriver, sugiro que leia esse artigo.

A função do_sleep() desativa a trava do motor. O Nema17 pode chegar a 70 graus quando em operação. Se não estiver em uso, colocá-lo em sleep fará com que ele não aqueça. Na impressora matricial que fiz (cuja biblioteca está no repositório oficial do Arduino) usei um truque para que o motor não aquecesse mesmo em operação, ou que o aquecimento seja mínimo.

void do_sleep(){
    //when sleeping, the motor doesn't gets hot
    stat = stat&~(1<<motor_sleep);
    Wire.beginTransmission(0x24);
    Wire.write(stat);
    Wire.endTransmission();
}

Outra função importante é a motorSetup(), que configura o estado inicial do EasyDriver, incluindo a direção que o motor deverá rodar. Veja o deslocamento de bits utilizando os componentes do enumerador:

void motorSetup(){
  motorSpeed();
  stat = stat&~1<<motor_direction; //add a bit on motor direction position
  stat = stat&~(1<<motor_sleep); //sleep 0
  stat = stat&~(1<<motor_ms1);
  stat = stat&~(motor_ms2);
  stat = stat|(1<<motor_enable); // enable 1
  Wire.beginTransmission(0x24);
  Wire.write(stat);
  Wire.endTransmission();
}

Finalmente, a função que move o motor, recebendo como parâmetro o número de passos, que será executado em um loop.

void moveMotor(int times){
  stat = stat|(1<<motor_sleep); //sleep 0
  stat = stat&~(1<<motor_enable); // enable 1
  Wire.beginTransmission(0x24);
  Wire.write(stat);
  Wire.endTransmission();


  for (int count = 0; count <times; count++){

      stat = stat|(1<<motor_step);

      Wire.beginTransmission(0x24);
      Wire.write(stat);
      Wire.endTransmission();

      delay(20);

      stat = stat&~(1<<motor_step);
      Wire.beginTransmission(0x24);
      Wire.write(stat);
      Wire.endTransmission();
      delay(20);
  }

    stat = stat&~(1<<motor_sleep); //sleep 0
    stat = stat|(1<<motor_enable); // enable 1
    Wire.beginTransmission(0x24);
    Wire.write(stat);
    Wire.endTransmission();
}

O setup inicia as duas portas serial, a task, o barramento I2C, faz o setup do motor, faz uma chamada preparatória do movimento do motor e limpa a variável number.

void setup() {
  Serial.begin(115200);
  Serial1.begin(9600,SERIAL_8N1,S1RX,S1TX);
  xTaskCreatePinnedToCore(toS1,"toS1",10000,NULL,0,NULL,0);
  Wire.begin(0,4);
  motorSetup();
  moveMotor(0);
  memset(number,0,sizeof(number));
}

A partir desse momento, a task já estará em execução, aguardando dados na segunda porta serial. No loop (que roda no núcleo 1), uma rotina lê dados da porta serial padrão, além de verificar uma flag para saber se tem dados a ler, então faz a leitura, muda a flag e limpa a variável.

Se chegar dados enquanto a variável estiver sendo manipulada, vai dar problema. Imagine uma variável sendo alterada enquanto sendo lida? Para evitar esse tipo de problema, utilizamos semáforos, mutex ou simplesmente uma flag, quando possível. Não vou implementar aqui porque não há propósito, só deixo claro para que tenha essa atenção no caso de querer implementar esse exemplo em algum projeto mais elaborado.

void loop() {
  if (Serial.available()){
   Serial.readBytesUntil('\n',number,5);
  }

  if (number[0] != 48 && number[0] != 0){
    Serial.print("ok: ");
    Serial.println(atoi(number));
    moveMotor(atoi(number));
    memset(number,0,sizeof(number));
  }

  if (thereIsMsgFromDisplay){
      Serial.println("Msg from display:");
      Serial.println(displaySaids[7]);

      thereIsMsgFromDisplay = false;
      memset(displaySaids,0,sizeof(displaySaids));
  }

}

Mover o motor de passo pelo display Nextion

Para mover o motor de passo, fiz uma tela simples com um slider, um botão para mudar a direção e um botão para enviar o valor do slider para movimentar o motor. Daí criei duas funções importantes. Vou mostrar e abaixo de cada uma explico sua função.

void changeDirection(){
    //XOR - toggle a bit
    stat = stat^(1<<motor_direction);
    Wire.beginTransmission(0x24);
    Wire.write(stat);
    Wire.endTransmission();
}

Essa função é chamada quando o botão Change Direction é clicado no display. O que ela faz é um toggle, simplesmente invertendo o estado do pino, independente de qual seja. Para isso, utilizei um XOR sobre a máscara de bits da variável stat. A constante motor_direction está dentro do enumerador EasyDriverPinMap.

void actionToComponent(){
//se o componente for de direção, muda a direção, senão, move o motor conforme o valor
//TODO: qual o valor maximo que pode ser enviado? qual o formato da mensagem? qual o id do componente que envia a direção?
    if (displaySaids[2] == 4){
        int val = displaySaids[8]*255 + displaySaids[7];
        Serial.print("pos ");
        Serial.println(displaySaids[2]);
        Serial.print("val: ");
        Serial.println(val);
        Serial.println(displaySaids[7]);
        Serial.println(displaySaids[8]);
        moveMotor(val);
    }
    else if (displaySaids[2] == 2){
    Serial.println("Mudando de direcao");
        changeDirection();
    }
}

Essa função analisa qual o ID do botão. Se for o botão com ID 4, movimenta o motor. Se for ID 2, muda a direção do motor.

A mensagem do valor que vai atrelada ao botão tem 8 bytes, sendo 00 00 00 00. Quando o incremento dos primeiros 2 bytes ultrapassar 255, começa o incremento da próxima posição. Isso significa que se escolher o valor 255 a resposta será FF 00 00 00. Se o valor for 256, a resposta será 01 01 00 00. Quando o segundo grupo for preenchido, inicia o terceiro. Como o máximo que programei para esse projeto utiliza apenas os primeiros 4 bytes, fiz uma conta simples. Mas primeiro vamos relembrar as posições das mensagens:

#Formato da mensagem:
#65 - botão
#0   -
#4  - ID do componente
#0   -
#255 - Termino
#255 - Termino
#255 - Termino
#10  - valor do slider (ate 255)
#0   - valor do slider  (255*n)

#conta do valor: 255*n + x
val = 255 * displaySaids[8] + displaySaids[7]
#Ou:
val = displaySaids[8] << 8|displaysSaids[7]<<0

Assim, se o segundo grupo for 0, ao multiplicar continuará sendo 0, caso contrário, será somado ao valor da posição 7.

O código completo ficou assim:

#include <HardwareSerial.h>
#include <Wire.h>
#define CLOCKWISE 1
#define REVERSE   0
#define fullTurn 800
#define quarterTurn 200
#define midTurn 400
#define svteen 600
#define S1RX 14
#define S1TX 12

/*
ESP32 Wemos
______________
|o          o|
|        svn |
|SVP     6   |
|28      5   |
|26      4   |<--- I2C PCF8574
|S1      0   |<--- I2C PCF8574
|CM      2   |
|SO      14  |<--- Serial 1 RX ---> TX Display
|CL      12  |<--- Serial 1 TX ---> RX Display
|3V      13  |
|SND     15  |
|5V      RX  |
|        TX  |
|        3v3 |
|        GND |<--- EasyDriver
|        GND |<--- PCF8574
|        5v  |<--- PCF8574
|            |
|            |
|            |
|o___|   |__o|
     |___|

#Formato da mensagem:
#65 - botão
#0   -
#4  - ID do componente
#0   -
#255 - Termino
#255 - Termino
#255 - Termino
#10  - valor do slider (ate 255)
#0   - valor do slider  (255*n)

conta do valor: 255*n + x
val = 255 * displaySaids[8] + displaySaids[7]
---------------
A mensagem vindo do número (em hexa) vem como 00 00 00 00. Os números inferiores a 256 ficam na primeira posição (até 255 ou, FF) e a quantidade até 255 x 255 fica na segunda e assim por diante. Ex para 513:
01 02 00 00

*/

HardwareSerial Serial1(2);

unsigned char stat             = 0;
unsigned char displaySaids[20] = {0};

bool thereIsMsgFromDisplay     = false;

char number[6]                 = {0};


enum easyDriverPinMap{
  motor_step,
  motor_direction,
  motor_sleep,
  motor_ms1,
  motor_enable,
  motor_ms2
};

enum buttons{
  button_direction = 2,
  button_send      = 4
};

/* Quando chegar dados na S1, deverá alimentar uma variável e levantar uma flag para ser lida pelo core principal. Essa task é levantada no setup()*/
void toS1(void *pvParameters){
    while (true){
        vTaskDelay(pdMS_TO_TICKS(500));
        if (Serial1.available()){
            Serial.println("dados chegando do display");
            Serial1.readBytesUntil('\n',displaySaids,9);
            //Serial.println(displaySaids[2]);
            thereIsMsgFromDisplay = true;
        }
    }
}

void changeDirection(){
    //XOR - toggle a bit
    stat = stat^(1<<motor_direction);
}

void motorSpeed(){
  //full step - OR - set bit
    stat = stat|1<<motor_ms1;
    stat = stat|1<<motor_ms2;
}

void do_sleep(){
    //when sleeping, the motor doesn't gets hot - AND NOT - clear a bit
    stat = stat&~(1<<motor_sleep);
    Wire.beginTransmission(0x24);
    Wire.write(stat);
    Wire.endTransmission();
}

void turnOff(){
    stat = 0;
    motorSpeed();
    Wire.beginTransmission(0x24);
    Wire.write(stat);
    Wire.endTransmission();
}

void motorSetup(){
  motorSpeed();
  stat = stat&~1<<motor_direction; //add a bit on motor direction position
  stat = stat&~(1<<motor_sleep); //sleep 0
  stat = stat&~(1<<motor_ms1);
  stat = stat&~(motor_ms2);
  stat = stat|(1<<motor_enable); // enable 1
  Wire.beginTransmission(0x24);
  Wire.write(stat);
  Wire.endTransmission();
}

void moveMotor(int times){
  stat = stat|(1<<motor_sleep); //sleep 0
  stat = stat&~(1<<motor_enable); // enable 1
  Wire.beginTransmission(0x24);
  Wire.write(stat);
  Wire.endTransmission();


  for (int count = 0; count <times; count++){

      stat = stat|(1<<motor_step);

      Wire.beginTransmission(0x24);
      Wire.write(stat);
      Wire.endTransmission();

      delay(20);

      stat = stat&~(1<<motor_step);
      Wire.beginTransmission(0x24);
      Wire.write(stat);
      Wire.endTransmission();
      delay(20);
  }

    stat = stat&~(1<<motor_sleep); //sleep 0
    stat = stat|(1<<motor_enable); // enable 1
    Wire.beginTransmission(0x24);
    Wire.write(stat);
    Wire.endTransmission();
}

void actionToComponent(){
//se o componente for de direção, muda a direção, senão, move o motor conforme o valor
//TODO: qual o valor maximo que pode ser enviado? qual o formato da mensagem? qual o id do componente que envia a direção?
    if (displaySaids[2] == 4){
        int val = displaySaids[8]*255 + displaySaids[7];
        Serial.print("pos ");
        Serial.println(displaySaids[2]);
        Serial.print("val: ");
        Serial.println(val);
        Serial.println(displaySaids[7]);
        Serial.println(displaySaids[8]);
        moveMotor(val);
    }
    else if (displaySaids[2] == 2){
    Serial.println("Mudando de direcao");
        changeDirection();
    }
}

void setup() {
  Serial.begin(115200);
  Serial1.begin(9600,SERIAL_8N1,S1RX,S1TX);
  xTaskCreatePinnedToCore(toS1,"toS1",10000,NULL,0,NULL,0);
  Wire.begin(0,4);
  motorSetup();
  moveMotor(0);
  memset(number,0,sizeof(number));
}

void loop() {
  if (Serial.available()){
   Serial.readBytesUntil('\n',number,5);
  }

  if (number[0] != 48 && number[0] != 0){
    Serial.print("ok: ");
    Serial.println(atoi(number));
    moveMotor(atoi(number));
    memset(number,0,sizeof(number));
  }

  if (thereIsMsgFromDisplay){
      Serial.println("Msg from display:");
      Serial.println(displaySaids[7]);
      actionToComponent();

      thereIsMsgFromDisplay = false;
      memset(displaySaids,0,sizeof(displaySaids));
  }

}

Código do display

No palco do display utilizei os widgets Variable, Number, Text, Slider e 2 Button. O texto é para exibir "Left" e "Right" para quando selecionada a direção do motor. O slider limitei a 800 com valor padrão 10.  O botão de mudar direção tem o ID 2, enquanto o botão de enviar o valor do slider tem o ID 4. O widget Number é utiliizado para exibir o valor que está no slider.

Código do botão Send

É o mais simplório possível, com apenas 1 linha:

prints h0.val,0

Também marquei para enviar o ID do componente, por isso no código do ESP32 tem duas condicionais para tratar o evento conforme o widget que o enviou.

Código do botão Change Direction

Esse é um pouco mais elaborado. Ele muda o label da direção e o valor a enviar pela serial:

if(va0.val==0)
{
  va0.val=1
  t0.txt="Left"
}else
{
  va0.val=0
  t0.txt="Right"
}
prints va0.val,0

O código do display se resume a isso, mas tenha atenção ao modo que a variável deve ser utilizada; tem que pular a linha para as chaves, não tem ponto-e-vírgula no final da linha, não pode ter espaço do if nem dos parênteses. Qualquer coisa diferente disso dará erro na compilação.

Utilizar a chamada prints com o valor a enviar e ",0" aceitará variável de qualquer tamanho. Qualquer coisa diferente de 0 limitará o comprimento da variável ao número de bytes selecionado.

Video

Fiz só um demonstrativo, do funcionamento para você acreditar e, com uma notícia triste. Esse display é bastante sensível à estática e eu já danifiquei o meu, como você pode notar à esquerda dele.

https://www.youtube.com/watch?v=v8xR8Ly2esE

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.