Manual
do
Maker
.
com
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.
Escolhi o ESP32 para o relógio Cuco porque ele deverá ser finalizado com as seguintes características:
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.
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
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.
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á.
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:
}
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!
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.