Manual

do

Maker

.

com

Controle de LED RGB com PCA9685 e ESP32

Controle de LED RGB com PCA9685 e ESP32

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.

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.

Utilizando Atom com Platformio

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 PlatformIOClang. 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).

Código completo

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.

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.