Motor de passo com ESP32, EasyDriver e PCF8674

Atualmente tenho focado meus esforços em DIY porque já escrevi um bocado além de 500 artigos e acredito que tenha informação mais que o suficiente para você fazer praticamente qualquer controle que desejar. Mas, tem coisas que saem do padrão. É o caso desse projeto do cuco, que estou fazendo a programação inicial pra por o relógio pra funcionar. Vou explicando no decorrer do artigo.

Motor de passo com ESP32

Escolhi o ESP32 para o relógio Cuco porque ele deverá ser finalizado com as seguintes características:

  • Conectar à Internet para pegar horário e se ajustar
  • Fazer uma iluminação correspondente à variação da luz do dia
  • Tocar o som de cuco em horas inteiras
  • Mover o cuco para fora da casinha

Não vou entrar em todos os detalhes do Cuco, mas o projeto vai bem. Só que já estou fazendo um outro relógio cuja eletrônica será bem mais viável, inclusive para projetos escolares.

Bem, com esse tanto de recursos, a primeira coisa óbvia é que faltaria pinos se fosse fazer apenas com o ESP32. Uma segunda opção poderia ser um Arduino Mega e um ESP-01 para buscar a hora e entregar via serial, mas preferi fazer diretamente com o ESP32 porque é mais legal de programar.

Motor de passo Nema17 com EasyDriver

E eu gosto bastante do EasyDriver para controlar motores de passo. Precisaria ser um Nema 17? – Respondo que não, mas ele tem uma precisão de passo muito boa; 200 passos em full-step, podendo ir até 1/8 de passo com o EasyDriver. Isso ajuda bastante porque fica fácil ajustar a posição das engrenagens do relógio (o motor controlará apenas 1 das engrenagens, a do segundos). Mas ainda assim são necessários 6 pinos para controlar o driver. Considerando que preciso de pinos para fazer o PWM dos LEDs RGB e ainda controlar um DFPlayer para tocar o cuco, e ainda controlar um SG90 para movimentar o pássaro, não tive outra opção que não usar um expansor de IO. Não um shift register, mas um driver I2C, que é o PCF9574. Caso o que você esteja procurando não seja exatamente o material desse artigo, talvez lhe interesse um desses outros artigos:

Controlar motor de passo com EasyDriver

Controlar motor de passo com o driver A4988

Uma impressora matricial que fiz

CNC com driver de DVD

Buffer não inversor CD4050

Dominando PCF8574

Utilizando PCF8574 para expandir IO

E eu preferi utilizar o PCF8574 justamente por utilizar apenas 2 pinos, já que ele é I2C. O único inconveniente é que sem escrever um código para o controle, não fica muito intuitivo sua utilização com motor de passo. Daí, como estou fazendo essa parte justamente agora, resolvi já compartilhar essa informação nesse artigo porque quando eu publicar o do cuco, vou depender muito da sua paciência com leitura e se eu deixar tudo para discorrer no artigo do Cuco, a leitura ficará insuportável.

Nos links supracitados você encontra uma referência sobre o PCF8574, caso não saiba como utilizá-lo ainda e deseja dominar bitwise. Basicamente o que faço é aplicar uma máscara e ativar ou desativar um determinado bit, sem alterar os demais. O interessante (e novo) desse artigo que você está lendo agora é que estou utilizando-o com o ESP32 e para controlar um motor de passo com dicas importantes que citei em um desses artigos cujo links deixei mais acima; o mais importante deles é o controle ideal para que não haja aquecimento da controladora nem do motor.

Utilizando I2C com ESP32

Para utilizar o barramento I2C, utilizei os pinos GPIO 4 e 0, sendo o 0 SDA e 4 SCL. No módulo PCF8574 SDA vai conectado ao SDA e SCL conectado ao SCL, simples e direto. O módulo PCF8574 é endereçável, meu módulo está no endereço 0x24. Você poderá descobrir o endereço do seu (caso não saiba) utilizando um scanner de barramento, como o desse sketch:

//
//    FILE: MultiSpeedI2CScanner.ino
//  AUTHOR: Rob Tillaart
// VERSION: 0.1.7
// PURPOSE: I2C scanner at different speeds
//    DATE: 2013-11-05
//     URL: http://forum.arduino.cc/index.php?topic=197360
//
// Released to the public domain
//

#include <Wire.h>
#include <Arduino.h>

TwoWire *wi;

const char version[] = "0.1.7";


// INTERFACE COUNT (TESTED TEENSY 3.5 AND ARDUINO DUE ONLY)
int wirePortCount = 1;
int selectedWirePort = 0;


// scans devices from 50 to 800KHz I2C speeds.
// lower than 50 is not possible
// DS3231 RTC works on 800 KHz. TWBR = 2; (?)
const long allSpeed[] = {
  50, 100, 200, 300, 400, 500, 600, 700, 800
};
long speed[sizeof(allSpeed) / sizeof(allSpeed[0])];
int speeds;

int addressStart = 0;
int addressEnd = 127;


// DELAY BETWEEN TESTS
#define RESTORE_LATENCY  5    // for delay between tests of found devices.
bool delayFlag = false;


// MINIMIZE OUTPUT
bool printAll = true;
bool header = true;


// STATE MACHINE
enum states {
  STOP, ONCE, CONT, HELP
};
states state = STOP;


// TIMING
uint32_t startScan;
uint32_t stopScan;


void setup()
{
  Serial.begin(115200);
  delay(2000);
  Serial.println("Starting...");
  Wire.begin(0,4);

#if defined WIRE_IMPLEMENT_WIRE1 || WIRE_INTERFACES_COUNT > 1
  Wire1.begin();
  wirePortCount++;
#endif
#if defined WIRE_IMPLEMENT_WIRE2 || WIRE_INTERFACES_COUNT > 2
  Wire2.begin();
  wirePortCount++;
#endif
#if defined WIRE_IMPLEMENT_WIRE3 || WIRE_INTERFACES_COUNT > 3
  Wire3.begin();
  wirePortCount++;
#endif

  wi = &Wire;

  setSpeed('0');
  displayHelp();
}


void loop()
{
  char command = getCommand();
  switch (command)
  {
    case '@':
      selectedWirePort = (selectedWirePort + 1) % wirePortCount;
      Serial.print(F("I2C PORT=Wire"));
      Serial.println(selectedWirePort);
      switch (selectedWirePort)
      {
        case 0:
          wi = &Wire;
          break;
        case 1:
#if defined WIRE_IMPLEMENT_WIRE1 || WIRE_INTERFACES_COUNT > 1
          wi = &Wire1;
#endif
          break;
        case 2:
#if defined WIRE_IMPLEMENT_WIRE2 || WIRE_INTERFACES_COUNT > 2
          wi = &Wire2;
#endif
          break;
        case 3:
#if defined WIRE_IMPLEMENT_WIRE3 || WIRE_INTERFACES_COUNT > 3
          wi = &Wire3;
#endif
          break;
      }
      break;

    case 's':
      state = ONCE;
      break;
    case 'c':
      state = CONT;
      break;
    case 'd':
      delayFlag = !delayFlag;
      Serial.print(F("<delay="));
      Serial.println(delayFlag ? F("5>") : F("0>"));
      break;

    case 'e':
      // eeprom test TODO
      break;

    case 'h':
      header = !header;
      Serial.print(F("<header="));
      Serial.println(header ? F("yes>") : F("no>"));
      break;
    case 'p':
      printAll = !printAll;
      Serial.print(F("<print="));
      Serial.println(printAll ? F("all>") : F("found>"));
      break;

    case '0':
    case '1':
    case '2':
    case '4':
    case '8':
      setSpeed(command);
      break;

    case 'a':
      setAddress();
      break;

    case 'q':
    case '?':
      state = HELP;
      break;
    default:
      break;
  }

  switch (state)
  {
    case ONCE:
      I2Cscan();
      state = HELP;
      break;
    case CONT:
      I2Cscan();
      delay(1000);
      break;
    case HELP:
      displayHelp();
      state = STOP;
      break;
    case STOP:
      break;
    default: // ignore all non commands
      break;
  }
}


void setAddress()
{
  if (addressStart == 0)
  {
    addressStart = 8;
    addressEnd = 120;
  }
  else
  {
    addressStart = 0;
    addressEnd = 127;
  }
  Serial.print(F("<address Range = "));
  Serial.print(addressStart);
  Serial.print(F(".."));
  Serial.print(addressEnd);
  Serial.println(F(">"));

}

void setSpeed(char sp)
{
  switch (sp)
  {
    case '1':
      speed[0] = 100;
      speeds = 1;
      break;
    case '2':
      speed[0] = 200;
      speeds = 1;
      break;
    case '4':
      speed[0] = 400;
      speeds = 1;
      break;
    case '8':
      speed[0] = 800;
      speeds = 1;
      break;
    case '0':  // reset
      speeds = sizeof(allSpeed) / sizeof(allSpeed[0]);
      for (int i = 0; i < speeds; i++)
      {
        speed[i] = allSpeed[i];
      }
      break;
  }
}

char getCommand()
{
  char c = '\0';
  if (Serial.available())
  {
    c = Serial.read();
  }
  return c;
}

void displayHelp()
{
  Serial.print(F("\nArduino MultiSpeed I2C Scanner - "));
  Serial.println(version);
  Serial.println();
  Serial.print(F("I2C ports: "));
  Serial.println(wirePortCount);
  Serial.println(F("\t@ = toggle Wire - Wire1 - Wire2 [TEENSY 3.5 or Arduino Due]"));
  Serial.println(F("Scanmode:"));
  Serial.println(F("\ts = single scan"));
  Serial.println(F("\tc = continuous scan - 1 second delay"));
  Serial.println(F("\tq = quit continuous scan"));
  Serial.println(F("\td = toggle latency delay between successful tests. 0 - 5 ms"));
  Serial.println(F("Output:"));
  Serial.println(F("\tp = toggle printAll - printFound."));
  Serial.println(F("\th = toggle header - noHeader."));
  Serial.println(F("\ta = toggle address range, 0..127 - 8..120"));
  Serial.println(F("Speeds:"));
  Serial.println(F("\t0 = 50 - 800 Khz"));
  Serial.println(F("\t1 = 100 KHz only"));
  Serial.println(F("\t2 = 200 KHz only"));
  Serial.println(F("\t4 = 400 KHz only"));
  Serial.println(F("\t8 = 800 KHz only"));
  Serial.println(F("\n\t? = help - this page"));
  Serial.println();
}


void I2Cscan()
{
  startScan = millis();
  uint8_t count = 0;

  if (header)
  {
    Serial.print(F("TIME\tDEC\tHEX\t"));
    for (uint8_t s = 0; s < speeds; s++)
    {
      Serial.print(F("\t"));
      Serial.print(speed[s]);
    }
    Serial.println(F("\t[KHz]"));
    for (uint8_t s = 0; s < speeds + 5; s++)
    {
      Serial.print(F("--------"));
    }
    Serial.println();
  }

  // TEST
  // 0.1.04: tests only address range 8..120
  // --------------------------------------------
  // Address  R/W Bit Description
  // 0000 000   0 General call address
  // 0000 000   1 START byte
  // 0000 001   X CBUS address
  // 0000 010   X reserved - different bus format
  // 0000 011   X reserved - future purposes
  // 0000 1XX   X High Speed master code
  // 1111 1XX   X reserved - future purposes
  // 1111 0XX   X 10-bit slave addressing
  for (uint8_t address = addressStart; address <= addressEnd; address++)
  {
    bool printLine = printAll;
    bool found[speeds];
    bool fnd = false;

    for (uint8_t s = 0; s < speeds ; s++)
    {
#if ARDUINO >= 158
      wi->setClock(speed[s] * 1000);
#else
      TWBR = (F_CPU / (speed[s] * 1000) - 16) / 2;
#endif
      wi->beginTransmission (address);
      found[s] = (wi->endTransmission () == 0);
      fnd |= found[s];
      // give device 5 millis
      if (fnd && delayFlag) delay(RESTORE_LATENCY);
    }

    if (fnd) count++;
    printLine |= fnd;

    if (printLine)
    {
      Serial.print(millis());
      Serial.print(F("\t"));
      Serial.print(address, DEC);
      Serial.print(F("\t0x"));
      if (address < 0x10) Serial.print(0, HEX);
      Serial.print(address, HEX);
      Serial.print(F("\t"));

      for (uint8_t s = 0; s < speeds ; s++)
      {
        Serial.print(F("\t"));
        Serial.print(found[s] ? F("V") : F("."));
      }
      Serial.println();
    }
  }

  stopScan = millis();
  if (header)
  {
    Serial.println();
    Serial.print(count);
    Serial.print(F(" devices found in "));
    Serial.print(stopScan - startScan);
    Serial.println(F(" milliseconds."));
  }
}

Basta subir o sketch abrir o terminal serial (na velocidade escolhida) e utilizar o comando s para fazer um single scan. Depois, basta ver o endereço na relação que aparecerá.

Conectar o PCF8574 ao EasyDriver

O wiring do EasyDriver está detalhado nesse artigo. Nele, descrevo detalhes importantes da utilização do EasyDriver. Um dos cuidados mais importantes é a forma de utilizá-lo economizando energia e reduzindo em até 100% o aquecimento. No caso do relógio, o motor nunca aquecerá.
Para conectá-lo, fiz o seguinte enumerador no código:

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

Os pinos do módulo PCF8574 vão de 0 à 7, sendo 8 pinos de IO e um de interrupção (que não será utilizado nesse projeto). Para organizá-los no código, criei esse enumerador. Um enumerador é semelhante a uma struct. Uma das diferenças é, como você pode notar, a ausência da declaração de tipos e valores. Montando-o dessa maneira, seus valores são atribuidos automaticamente a partir de 0. Logo, no pino 5 do PCF8574 já sei que tenho o controle do micro-step da segunda bobina do motor, apenas olhando para o código. Para utilizá-lo, apenas referencie seu nome, como se fosse um define.

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

Nessa pequena porção de código estou ajustando a velocidade do passo para full-step. Criei o buffer stat do tipo unsigned char para guardar os 8 bits. A operação que está utilizando pipe se chama bitwise e você vê em detalhes como manipular bits usando máscara no artigo Dominando PCF8574

Por enquanto estou em pleno desenvolvimento do código, mas já dá pra provar o conceito com esse código, assim você já pode tirar sua dúvida ou matar a curiosidade sobre o funcionamento do motor de passo por I2C:

#include <Wire.h>
#define CLOCKWISE 1
#define REVERSE   0

unsigned char stat = 0;

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

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

void do_sleep(){
    //when sleeping, the motor doesn't gets hot 
    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_enable); // enable 1
  Wire.beginTransmission(0x24);
  Wire.write(stat);
  Wire.endTransmission();
}

void moveMotor(){
  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 <600; count++){
      
      stat = stat|(1<<motor_step);
      
      Wire.beginTransmission(0x24);
      Wire.write(stat);
      Wire.endTransmission();
      
      delay(5);
      
      stat = stat&~(1<<motor_step);
      Wire.beginTransmission(0x24);
      Wire.write(stat);
      Wire.endTransmission();
      delay(5);
  }

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

void setup() {
  Serial.begin(115200);
  Wire.begin(0,4);
  motorSetup();
  moveMotor();

}

void loop() {
  // put your main code here, to run repeatedly:

}

Onde comprar

Vou recomendar mais parceiros. Nesse artigo, recomendo a MASUGUX para a compra do ESP32, o módulo DFPlayer que será utilizado para reprodução de som no Cuco, O servo motor SG90 para o acionamento do passarinho, jumpers para fazer o wiring e mais uma lista enorme de componentes para projetos de eletrônica digital, recomendo!

Bem, por enquanto é isso, em breve publico o projeto do Cuco funcionando!

Siga-nos no Do bit Ao Byte no Facebook.

Prefere twitter? @DobitAoByte.

Inscreva-se no nosso canal Do bit Ao Byte Brasil no YouTube.

Nossos grupos:

Arduino BR

https://www.facebook.com/groups/microcontroladorarduinobr/
Raspberry Pi BR

https://www.facebook.com/groups/raspberrybr/
Orange Pi BR

https://www.facebook.com/groups/OrangePiBR/
Odroid BR

https://www.facebook.com/groups/odroidBR/
Sistemas Embarcados BR

https://www.facebook.com/groups/SistemasEmbarcadosBR/

Sistemas Eletrônicos

https://www.facebook.com/groups/544679592355388/

Projetos Arduino | Eletrônica

https://www.facebook.com/groups/projetosarduinoeletronica/

ESP8266 e ESP32

https://www.facebook.com/groups/ESP8266BR/

ARM Brasil

https://www.facebook.com/groups/508153815944410/

MIPS BR

https://www.facebook.com/groups/MIPSBR/
Do Bit ao Byte

https://www.facebook.com/groups/dobitaobyte/

Próximo post a caminho!