Projekt: Sterownik pasków RGBW cz. 1

Hej! W dzisiejszym artykule zaczniemy realizację kolejnego projektu od schematu ideowego aż do finalnego produktu. Tym razem zajmiemy się wykonaniem sterownika do pasków ledowych (np. do zamontowania w roli dekoracji w jakimś pomieszczeniu). W pierwszej części zaprojektujemy płytkę drukowaną oraz napiszemy kod na Arduino, dzięki któremu będziemy mogli regulować jasność, natężenie oraz barwę światła. W drugiej części do sterownika dorobimy aplikację na system Android, za pomocą której będziemy mogli ustawić dowolny kolor.

Schemat i PCB

Pierwszą czynnością jaką musimy zrobić jest określenie swoich założeń oraz potrzebnych materiałów. Wiemy, że LEDowy pasek może zużywać trochę mocy, dlatego też powinniśmy użyć tranzystorów MOSFET, którymi wysterujemy sobie każdy z kolorów. Nie chcemy także, aby w przypadku uszkodzenia któegoś z tranzystorów upalić też pin procesora, dlatego postanowiłem odseparować te dwa elementy od siebie za pomocą optotranzystorów PC817. Jeżeli chodzi o MOSFETa to dobrym wyborem będzie BUZ11 (lub mocniejszy jeżeli wiemy, że będziemy sterowali naprawdę dużą mocą). Sterowanie LEDami będzie odbywało się za pomocą jednego przycisku (krótkei wciśnięcie: wł. / wył., długie przytrzymanie: zmiana koloru) oraz portu szeregowego (np. modułu bluetooth). Poza tym na płytce przewidziałem też złącze na odbiornik podczerwieni, termometr i zestaw par pinów cyfrowych i masy (mogą posłużyć np. jako piny konfigurowane zworkami) – tak na przyszłość :)

Rzućmy okiem na schemat:

Poza tym co już wypisałem mamy tutaj też zworkę do wyboru źródła zasilania LEDów. W przypadku pasków zasilanych z 24V nie moglibyśmy tym samym zasilaczem dostarczyć prądu dla naszej płytki, gdyż 5V stabilizator LM7805 nie dałby sobie rady z tak dużym obniżeniem napięcia.

Po rozmieszczeniu wszyskich elementów oraz poprowadzeniu ścieżek layout płytki drukowanej wygląda następująco:

A tutaj pliki do programu EAGLE.

Jak widać wykonanie tej płytki nie będzie nas kosztowało wiele części. Potrzebujemy stabilizatora LM7805, czterech tranzystorów BUZ11 lub kompatybilnych, czterech optotranzystorów PC817, kwarc 16 MHz, trochę drobnicy takiej jak rezystory, kondensatory, dławik, goldpiny i złącza ARK. I to tyle :) Możemy teraz przystąpić do wykonania płytki oraz polutowania elementów.

Kiedy płytka jest już gotowa możemy wgrać sobie bootloader do czytej Atmegi 328, lub kupić już taką z wgranym bootloaderem. Jeżeli mamy już dobrze przygotwany mikrokontroler mozemy włożyć go w gniazdo DIP28 w naszej płytce i zacząć programować.

Program

Cały program można oczywiście pobraż w formie zip’a tutaj.

W projekcie skorzystamy z biblioteki, którą kiedyś napisaliśmy w jednym z artykułów. Zmodyfikujemy ją jednak odrobinkę, aby przekształcała paletę HSV na RGBW, a nie tak jak oryginalnie RGB.

Dodatkowo projekt w Arduino rozbijemy na dwa pliki, aby łatwiej było nawigować po kodzie.

Zacznijmy od poprawek w bibliotece od kolorów. Zmianie uległ konstruktor (dodana została nowa składowa) oraz funkcja setHSV. Poszperałem trochę na stackoverflow jak przełożyć kolor RGB na RGBW i znalazłem to. Przekształciłem kod z C# na C++ i dopisałem do metody setHSV. Pojawiła się także nowa metoda o nazwie isTurnedOn, która zwraca fałsz, jeżeli pasek jest całkowicie wyłączony.

Przejdźmy teraz do pliku zawierającego funkcję setup oraz główną pętlę naszego programu i sprawdźmy, co się dzieje podczas wstępnej konfiguracji:

void setup() {
  Serial.begin(9600);
  led.setTransitionTime(1000);
  
  led.setInitHue(0).setHue(0);
  led.setInitSaturation(0).setSaturation(0);
  led.setInitValue(0).setValue(1);
 
  led.setOnFinishListener(transitionFinished);

  pinMode(PIN_BTN, INPUT_PULLUP);
  lastBtnState = digitalRead(PIN_BTN);
}

Włączamy tutaj port szeregowy, ustawiamy czas przejścia na sekundę, jakieś początkwoe wartości (takie, aby po rozpoczęciu pierwszej animacji kolor ustawił się na biały) i ustawiamy przycisk jako INPUT_PULLUP.

Po wykonaniu funkcji setup() następuje przejście do loop():

void loop() {
  readSerial();

  currentBtnState = digitalRead(PIN_BTN);
  
  if(lastBtnState && !currentBtnState) {
    lastBtnRead = millis();
  }

  if(!lastBtnState && currentBtnState && !led.isInProgress()) {
    unsigned long btnPressTime = millis() - lastBtnRead;
    if(btnPressTime <= 10) return;
    if(btnPressTime <= 300) {
      if(led.isTurnedOn()) {
        led.setValue(0);
        led.setOnFinishListener(turnedOff);
      } else {
        led.setValue(1);
      }
      led.doTransition(true);
    }
  }

  if(!lastBtnState && !currentBtnState && led.isTurnedOn()) {
    unsigned long btnPressTime = millis() - lastBtnRead;
    if(btnPressTime > 300 && !(btnPressTime % 30)) {
      led.setInitSaturation(0.4).setSaturation(0.4);
      led.setInitValue(1).setValue(1);
      led.setOnFinishListener(transitionFinished);

      if((currentHue+1) % 60 == 0) {
        hueStoppedTime = millis();
        currentHue++;
      }

      if(millis() - hueStoppedTime >= 1000) currentHue++;
      
      led.setInitHue(currentHue).setHue(currentHue);
      if(currentHue > 359) currentHue = 0;
      led.doTransition(true);
    }
  }
  
  led.doTransition();
  lastBtnState = currentBtnState;
}

Jak widać za każdym wykonaniem pętli wywołujemy funkcję readSerial(). Ją omówię na końcu :)

Poza odczytywaniem portu szeregowego w loopie mamy tylko odczyt pstryczka do włączania / wyłączania oraz ustawiania koloru. Jeżeli poprzedni stan przycisku mówił, że przycisk nie jest wciśnięty, a obecny informuje, że jest wciśnięty to zapisujemy sobie do zmiennej lastBtnRead czas rozpoczęcia wciskania. Teraz możemy wyodrębnić 2 większe bloki.

Przycisk został puszczony: sprawdzamy, czy nastąpiło to między 10 a 300 ms od jego wciśnięcia. Jeżeli tak to oznacza to krótkie wciśnięcie, czyli włączenie / wyłączenie paska.

W następnym bloku sprawdzamy, czy przycisk jest dalej wciśnięty, i czy jest wciśnięty dłużej niż 300 ms. Jeżeli tak, to w 30 milisekundowych odstępach będziemy zwiększali zmienną currentHue o 1.

Początkowo bardzo trudno było ustawić kolor idealnie w pozycjach takich jak całkowita czerwień lub całkowity niebieski. Aby to osiągnąć dodałem kolejnego ifa, któy sprawdza, czy następny kolor jest podzielny przez 60. Jeżeli tak to zatrzymuje zwiększanie currentHue na sekundę. W sam raz, żeby w tym momencie puścić przycisk :)

Zobaczmy teraz funkcję readSerial:

void readSerial() {
  static String data = "";
  static unsigned long lastRead = millis();
  char temp;
  
  while(Serial.available()) {
    temp = Serial.read();
    lastRead = millis();
    if(temp == '\r' || temp == '\n') continue;
    if(temp == ';') {
      lastRead = 0;
      break;
    }
    data += temp;
  }

  if(data.length() > 0 && lastRead + 100 < millis()) {
    if(data.equals("GO")) {
      led.doTransition(true);
      Serial.println("Transition started");
    }
    
    else if(data.startsWith("H=")) {
      data = data.substring(2);
      data.trim();
      int value = data.toInt();
      led.setHue(value);
      Serial.print("Hue set to ");
      Serial.println(value);
    }

    else if(data.startsWith("V=")) {
      data = data.substring(2);
      data.trim();
      int value = data.toInt();
      led.setValue(value/100.0);
      Serial.print("Value set to ");
      Serial.println(value);
    }

    else if(data.startsWith("S=")) {
      data = data.substring(2);
      data.trim();
      int value = data.toInt();
      led.setSaturation(value/100.0);
      Serial.print("Saturation set to ");
      Serial.println(value);
    }

    else if(data.startsWith("T=")) {
      data = data.substring(2);
      data.trim();
      int value = data.toInt();
      led.setTransitionTime(value);
      Serial.print("Time set to ");
      Serial.println(value);
    }

    data = "";
  }
}

Podobny mechanizm omawiałem w tym artykule, dlatego skupię się tu na jednej „nowości”, a mianowicie mozliwości obsługi kilku komend za jednym zamachem (oddzielając je znakiem średnika). Cała magia twki w pętli while na początku i jednym dodatkowym ifie :)

while(Serial.available()) {
    temp = Serial.read();
    lastRead = millis();
    if(temp == '\r' || temp == '\n') continue;
    if(temp == ';') {
      lastRead = 0;
      break;
    }
    data += temp;
  }

Jeżeli temp będzie średnikiem, to ustawiamy lastRead na 0 i wychodizmy z pętli. Dlaczego na 0?

Spójrzmy na warunek wykonania komendy:

if(data.length() > 0 && lastRead + 100 < millis()) {

Jeżeli od 100 ms nie będzie żadnych nowych danych to program spróbuje przetworzyć komendę. Wiemy, że te dane mogą się pojawić, dlatego też ustawiamy lastRead na 0, wtedy na pewno ten warunek będzie prawdziwy i przetworzy nam to, co odczytało do momentu wstawienia średnika.

To tyle na dziś. W kolejnej części dowiemy się jak napisać aplikację na system Android, za pomocą której łatwo wysterujemy sobie nasze LEDy ;)