Manual
do
Maker
.
com
Estou mais ou menos na metade do projeto do relógio cuco, já fiz o controle da parte mecânica, conforme os recursos utilizados nesse outro artigo, também já fiz o teste inicial de pegar hora da internet usando NTP como descrito nesse artigo. Agora é hora de controlar os LEDs RGB que farão a simulação da iluminação do dia, desde a alvorada até o crepúsculo, utilizando PCA9685 e ESP32.
A biblioteca é a mesma utilizada no Arduino (existem diversas, vou mostrar a que escolhi). Esse módulo serve tanto para fazer PWM para LEDs (podendo ter até 5 LEDs RGB ou 16 comuns) como para controlar até 16 servos motor. Provavelmente é a melhor opção para o controle de LEDs RGB tradicionais, mas uma opção que poderia ser melhor que essa seria o uso de LEDs endereçáveis como o WS2812, que é bem prático e fácil de controlar. Não lembro se já escrevi sobre ele, mas farei um artigo tão logo seja possível.
Antes que eu me esqueça, o LED RGB deve ser anodo comum. Se você utilizar catodo comum, os LEDs não acenderão porque os pinos do PCA9685 são apenas sinal.
O PCA9685 é um módulo i2c com 12 bits de resolução, expansível até 4 módulos. Não sei se todos vem com o mesmo endereço, mas como eu não sabia qual era o endereço padrão, utilizei um scanner i2c para detectar. O código do scanner é esse:
//
// 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(18,19);
#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 esse sketch e abrir o terminal, então quando aparecer o menu você pode optar por um single scan (s) ou continuous (c). Deve aparecer uma linha completa com V maiúsculo no endereço encontrado. Caso não apareça, procure acertar o SDA e SCL, que é a causa mais comum para um eventual erro.
No meu caso, o scanner mostrou o dispositivo no endereço 0x70, então reatribuí o valor da macro que definia o endereço do dispositivo na biblioteca. Estou utilizando o ESP32 Wemos da CurtoCircuito e selecionei os pinos 18 para SDA e 19 para SCL. Vou fragmentar o código que escrevi para explicá-lo, mas se você não gosta de absorver conceitos e quer apenas o código para funcionar, é só ir à seção Codigo Completo.
Apesar da recomendação do Atom em 2018, atualmente é mais propício utilizar VS Code com PlatformIO. Instale o PlatformIO no VS Code através do menu de plugins e tudo estará pronto para uso em um clique.
Dessa vez resolvi usar o Atom com alguns plugins para ficar bem legal. Baixe o Atom no site oficial ou use o repositório de sua distribuição Linux, se for seu sistema:
sudo su
apt-get update
apt-get install atom
Após a instalação do Atom, você poderá instalar recursos guiado pela própria IDE. Procure por PlatformIO e Clang. Mais uma vez, se estiver no Linux, basta utilizar o mesmo esquema acima:
sudo su
apt-get update
apt-get install clang
exit
O comando exit é para sair do usuário root. Depois de instalados e reiniciado o Atom, abrir-se-á uma aba intitulada PlatformIO Home.
A partir dela você escolhe a plataforma que deseja programar e as dependências serão baixadas automaticamente. Nesse mesmo menu tem uma seção Libraries, onde você precisará digitar PCA9685.
É essa primeira a que instalei. Depois é só criar um projeto novo, escolher a placa e a plataforma de programação (ESP-IDF, Arduino etc).
Os includes são o Wire, a PCA9685 e Arduino, para importar os recursos da API do Arduino. Fiz os defines para o I2C e para o endereço do módulo e o resto explico no próprio código.
#include <Arduino.h>
#include <PCA9685.h>
#include <Wire.h>
//pinos escolhidos para o i2c
#define SDA 18
#define SCL 19
//endereço da placa
#define PCA9685_BASEADR 0x70
//instância da PCA9685. Os parâmetros são endereço, tipo de controle e frequência
PCA9685 driver = PCA9685(PCA9685_BASEADR, PCA9685_MODE_LED_DIRECT, 800.0);
//cockcrow canto do galo
// função para controlar os LEDs RGB
void ledControl(byte led_number);
//variável alocada para copia do array correspondente à iluminação, conforme a hora do dia
int *weather = (int*) malloc(sizeof(int)*3);
byte myTime = 20; //TODO: criar um timer para acumular horas
//estrutura para armazenar as cores que correspondam à iluminação conforme a hora do dia
struct schemes{
int dawn[3] = {600,600,600};
int morning[3] = {1200,1200,1200};
int midday[3] = {4095,4095,4095};
int afternoon[3] = {1500,1500,1500};
int almostNight[3] = {400,400,400};
int twilight[3] = {100,100,100};
};
struct schemes schemesValues;
void setup() {
// Serial para debug
Serial.begin(115200);
// inicializa o i2c
Wire.begin(SDA,SCL);
// inicializa a PCA9685
driver.setup();
//desliga todos os pinos da PCA9685
for (byte i=0;i<16;i++){
driver.getPin(i).setValueAndWrite(0);
}
//delay para dar tempo de abrir a serial e assistir o debug
delay(2000);
ledControl(2);
}
/* Como estou fazendo o teste apenas no primeiro LED, a função se limita a
um array de 3 bits, para R, G e B. Depois os valores serão simplesmente
estendidos aos demais pinos, mas eu queria testar o controle primeiro.
*/
void ledControl(byte led_number){
//como free() está com bug, não posso criar a cada chamada, então
//deixo global e só reatribuo o valor
//int *weather = (int*) malloc(sizeof(int)*3);
byte max = led_number*3; // desse modo configuro o array nos pinos corretos
/*as condicionais abaixo podem ser modificadas como quiser. Eu selecionei
aproximadamente os horários em que a luz pode variar mais. A variavel
myTime guardará as horas por intermédio de uma task (que será criada) a
posteriori. Conforme o horário, os valores virão com as cores (ainda a ajustar)
que representem aproximadamente a luz do respectivo horário.*/
if (myTime > 0 && myTime < 6){
weather = schemesValues.dawn;
}
else if (myTime >5 && myTime < 11){
weather = schemesValues.morning;
}
else if (myTime >10 && myTime < 16){
weather = schemesValues.midday;
}
else if (myTime >15 && myTime < 17){
weather = schemesValues.afternoon;
}
else if (myTime >16 && myTime < 19){
weather = schemesValues.almostNight;
}
else{
weather = schemesValues.twilight;
}
//Tendo determinado o array de cores a utilizar, agora atribui-se aos pinos.
for (byte i=0;i<3;i++){
driver.getPin(max).setValueAndWrite(weather[i]);
Serial.println(weather[i]);
max++;
}
/* Como free() deu um bug, tive que criar a variável weather global e apenas
zerar os valores após o uso.*/
memset(weather, 0, 3);
//free está dando erro no esp32
//free(weather);
}
void loop() {
/* Essa rotina faz um fade no LED, foi usada só para teste no pino 0.
Serial.println("loop ok");
for (int i = 0; i < PCA9685_MAX_VALUE; i = i + 8){
// set the pwm value of the first led
driver.getPin(PCA9685_LED0).setValueAndWrite(weather[i]);
delay(5);
}
*/
}
É só fazer o wiring, upload do programa e usar. Farei um video em algumas horas fazendo um tour pela IDE e mostrando a placa e o funcionamento, não deixe de se inscrever em nosso canal DobitAoByteBrasil no Youtube!
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.