WIFI로 제어하는 탁상시계 만들기 #6 NEOPIXEL RGB AND White LED

2019. 1. 30. 14:11

Project/Turtle Clock

NEOPIXEL RGBW LED Bar 연결하기

Turtle Clock BigFont에 대한 여섯 번째 연재입니다. 이번 글에서는 아직 설명하지 않은 두 개의 부품중 하나인 네오픽셀 RGBW 색상 LED에 대해 소개합니다. 원래 필요한 LED 색상은 백색(White)입니다. 밤중에, 아이가 자고 있는 침실에 들어갈 때나 아픈 아이 챙기거나 할 때, 주로 스마트폰의 손전등 기능을 사용합니다. 스탠드가 있긴 한데 넘어질 위험에 치워버린 상태입니다. 그래서 생각한 방법이, 이번에 만들 시계에 LED를 넣어 간이 스탠드로 사용하는 것입니다.

8 Bit SK6812 5050 Stick RGBW LED Natural 4500K with Integrated Drivers RGBW White

이번 시계에 사용할 LED 모듈 스틱입니다. 필요한 것은 간이 스탠드 기능이기 때문에 몇 개의 White Color LED만 있으면 되지만, 더불어 시계에 장식 효과도 더할 겸 이 제품을 골랐습니다. 게다가, Neopixel LED는 하나의 제어선으로 모두 제어가 가능하다는 장점까지 있습니다. 가격도 저렴한데, 아마도 Adafruit에서 만드는 Neopixel 제품을 카피한 듯합니다. 총 8개의 LED 모듈이 하나의 스틱에 연결되어 있고, 각각의 모듈이 RGB 색상과 White 색상을 동시에 발광합니다.

이 제품은 RED, GREEN, BLUE, WHITE 네 가지 색상의 LED와 이를 제어할 드라이버가 하나의 LED칩 안에 통합되어 있습니다. 사용한 드라이버는 SK6812이고 LED칩 내부에 내장되어 있으며 8비트(bit) PWM 제어를 통해 한 채널(색상)당 0에서 255까지 256 단계로 밝기를 조절할 수 있습니다. 네 가지 색상이므로 총 32bit의 제어 신호가 필요합니다.

5050은 LED칩 하나의 크기를 말하는데 가로, 세로 5mm를 의미하고, RGB 세 가지 색상을 가장 밝게 하면 40mA의 전류를 소모한다고 합니다. White 색상의 색온도는 4500K라서 취침 스탠드 용도로 적당한 듯합니다.

연결 포트는 뒷면에 있습니다. 전원은 5V, 제어 신호는 DIN 포트를 이용하고 DOUT 포트를 통해 제어 데이터를 다시 출력하기 때문에 여러 개의 모듈을 직렬로 연결할 수 있습니다.

LED 칩의 노란색 부분이 백색 LED이고 반대쪽이 RGB LED입니다.

연결을 위해 케이블을 연결하였습니다. 전원 및 제어 선을 합쳐 3개만 연결하면 되고, 모듈은 하나만 사용하기 때문에 출력 쪽은 연결하지 않았습니다.

라이브러리 설치 및 기본 예제 실행

우선 전원은 배터리 쉴드에, 제어 선은 NodeMCU 보드의 D3핀에 연결하였고 라이브러리는 Adafruit에서 제공하는 Neopixel용을 사용합니다.

Library Manager에서 neopixel로 검색하여 위와 같이 “Adafruit NeoPixel” 라이브러리를 찾아 설치합니다.

테스트를 위해 라이브러리 제공 예제 중에 “RGBWstrandtest”를 불러와서 실행하였습니다.

#define PIN D3
//
#define NUM_LEDS 8

불러온 예제에서 위와 같이 Pin 넘버와 LED 모듈의 개수만 수정하고 실행하면 결과를 확인할 수 있습니다.

간단한 예제를 통한 Neopixel LED 사용법 소개

간단한 몇 가지 예제를 통해 기본적인 사용법을 알아보겠습니다. 우선 첫 번째 LED 모듈만 켜는 예제입니다.

#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel rgbwBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);

가장 먼저 Adafruit_NeoPixel.h 헤더 파일을 포함시키고 적당한 이름으로 객체를 생성합니다. 객체 생성시의 첫 번째 인수 “8”은 연결된 LED 모듈의 개수이고, 두 번째 인수 D3는 연결된 NodeMCU 포트이며 세 번째는 기본 예제와 동일하게 하였습니다.

void setup() {
  rgbwBar.begin();
  rgbwBar.show();
  //
}

이번 예제는 setup() 함수 내에만 코딩합니다. 생성된 객체 이름으로 객체를 시작하고, 바로 show() 함수를 실행합니다. show() 함수는 버퍼에 있는 내용을 실제 Neopixel LED 모듈에 출력하는 함수이지만, begin() 함수에 붙여서 사용하면 모든 픽셀을 “Off”하는 초기화와 같은 기능을 합니다.

void setup() {
  .
  .
  //
  rgbwBar.setPixelColor(0, 255, 0, 0);
  rgbwBar.show();
}

마지막으로 위 두 줄만 삽입하면 예제가 완성됩니다. setPixelColor() 함수의 첫 번째 인수는 제어할 LED 모듈을 지정합니다. 숫자 “0”부터 차례대로 접근하므로 첫 번째 인수 “0”은 연결된 첫 번째 LED(DIN 포트 쪽...) 모듈을 가리킵니다. 나머지 세 개의 인수는 차례대로 RED, GREEN, BLUE 색상에 대한 밝기 값입니다. 0부터 255까지의 숫자로 조절하며, 위 예제는 첫 번째 LED 모듈의 RED LED를 가장 밝게 켜는 코드입니다. setPixelColor() 함수를 이용해 출력할 데이터를 버퍼에 저장했다면, 꼭 show() 함수를 호출해야 실제로 출력 됩니다.

#include <Adafruit_NeoPixel.h>
//
Adafruit_NeoPixel rgbwBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
//
void setup() {
  rgbwBar.begin();
  rgbwBar.show();
  //
  rgbwBar.setPixelColor(0, 255, 0, 0);
  rgbwBar.show();
}
//
void loop() {
}

전체 소스와 결과 화면입니다. 빨간색이 켜진 LED 모듈이 첫 번째 모듈입니다. 라이브러리에서 제공하는 기능은 많지만, 이 프로젝트에선 기본적인 몇 가지만 사용하겠습니다.

  rgbwBar.setPixelColor(0, 0, 255, 0);
  rgbwBar.setPixelColor(1, 0, 255, 0);
  rgbwBar.setPixelColor(2, 0, 255, 0);
  rgbwBar.setPixelColor(3, 0, 255, 0);
  rgbwBar.setPixelColor(4, 0, 255, 0);
  rgbwBar.setPixelColor(5, 0, 255, 0);
  rgbwBar.setPixelColor(6, 0, 255, 0);
  rgbwBar.setPixelColor(7, 0, 255, 0);
  rgbwBar.show();

8개의 모듈을 모두 제어하려면 위와 같이 첫 번째 인수를 달리하여 모든 LED 모듈에 데이터를 제공하는 명령을 줘야 합니다. setPixelColor() 함수를 이용하여 픽셀 데이터를 버퍼에 저장한 후 show() 함수로 LED에 출력합니다.

for ( int i = 0; i < 8; i++ ) {
  rgbwBar.setPixelColor(i, 0, 255, 0);
}
rgbwBar.show();

같은 의미의 구문입니다. 이렇게 주로 FOR문을 이용하여 8개의 모듈에 대한 출력 데이터를 제어 합니다.

  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 0, 0, 30);
  }
  rgbwBar.show();

모듈에 통합된 백색 LED를 제어하려면 setPixelColor() 함수에 다섯 번째 인수를 제공하면 됩니다. 위 코드는 다른 색은 모두 “Off”하고 백색만 “On”하고 있습니다.

  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 0, 30 * i + 10);
    rgbwBar.show();
    delay(300);
  }

각 색상에 해당하는 인수 값으로 밝기를 조절하는 예제입니다.

#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel rgbwBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
void setup() {
  rgbwBar.begin();
  rgbwBar.show();
}
void loop() {
  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 35 * i + 10, 0, 0);
    rgbwBar.show();
    delay(200);
  }
  rgbwBar.clear();
  rgbwBar.show();
  delay(300);
  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 35 * i + 10, 0);
    rgbwBar.show();
    delay(200);
  }
  rgbwBar.clear();
  rgbwBar.show();
  delay(300);
  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 0, 35 * i + 10);
    rgbwBar.show();
    delay(200);
  }
  rgbwBar.clear();
  rgbwBar.show();
  delay(300);
}

라이브러리 함수를 하나 더 소개합니다. clear() 함수는 버퍼를 모두 지워주는 함수이며 역시 show() 함수와 병행하여 사용합니다. 위 예제는 이 함수를 이용하여 다음 색상을 출력하기 전에 LED를 모두 OFF하는 동작을 수행합니다.

#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel rgbwBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
int red = 0;
int green = 40;
int blue = 80;
int ledCount = 0;
void setup() {
  rgbwBar.begin();
  rgbwBar.show();
}
void loop() {
  red += 10;
  green += 10;
  blue += 10;
  //
  rgbwBar.setPixelColor(ledCount++, red, green, blue);
  rgbwBar.show();
  //
  if ( red > 90 ) red = 0;
  if ( green > 90 ) green = 0;
  if ( blue > 90 ) blue = 0;
  if ( ledCount > 7 ) ledCount = 0;
  delay(10);
}

R, G, B 색상을 조합하여 몇 가지 색상을 만드는 예제입니다.

#include <Adafruit_NeoPixel.h>
//
Adafruit_NeoPixel rgbwBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
//
void setup() {
  rgbwBar.begin();
  rgbwBar.show();
}
//
void loop() {
  rgbwBar.setBrightness( 0 );
  //
  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 0, 255);
  }
  rgbwBar.show();
  delay(500);
  //
  rgbwBar.setBrightness( 80 );
  //
  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 0, 255);
  }
  rgbwBar.show();
  delay(500);
  //
  rgbwBar.setBrightness( 160 );
  //
  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 0, 255);
  }
  rgbwBar.show();
  delay(500);
  //
  rgbwBar.setBrightness( 240 );
  //
  for ( int i = 0; i < 8; i++ ) {
    rgbwBar.setPixelColor(i, 0, 0, 255);
  }
  rgbwBar.show();
  delay(500);
}

마지막으로 소개할 함수는 setBrightness()입니다. 인수는 0에서 255사이의 숫자이고 이름 그대로 출력 밝기를 조절하지만 혼자서는 아무런 기능을 하지 못합니다. setPixelColor() 함수로 출력할 색상 값을 버퍼에 저장할 때, 이 setBrightness() 함수로 설정한 밝기 값을 비율 계산하여 적용하는 방식입니다. 원하는 색상을 만들 때, 만들어진 색상의 최종 밝기까지 생각하여 계산하는 것은 좀 복잡할 수 있지만, 이 함수를 사용하면 최종 출력 밝기를 색상 값에 상관없이 조절할 수 있습니다. 예를 들어, R, G, B 값을 모두 255로 설정하면 LED는 가장 밝은 빛을 내게 되는데, setBrightness() 함수로 “0”을 설정하면 LED는 OFF, “255”는 색상 값 그대로, “127”이면 절반의 밝기로 조절합니다. 따라서 setBrightness() 함수는 더 밝게는 못하고, 비율대로 어둡게 하는 기능입니다.

Neopixel을 이용하여 초침 효과 만들기

이제, 프로젝트에 이 부품을 적용하겠습니다. 256단계의 LED들을 조합하면 정말 멋진 비주얼 효과를 낼 수 있겠지만, 저는 그냥 1초마다 한 칸씩 이동하는 간단한 초침 효과만 적용하겠습니다.

수정된 이전 소스
#include <NTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_LEDBackpack.h>
//
const char *ssid     = "xxxxxxxx";
const char *password = "xxxxxxxx";
//
int previousMin = 0;
int previousSec = 0;
bool colonBlink = true;
unsigned long colonOnTime;
bool hours12 = true;
//
WiFiUDP udp;
NTPClient timeClient(udp, "kr.pool.ntp.org", 32400, 3600000);
Adafruit_7segment led = Adafruit_7segment();
//
void setup(){
  Wire.begin(D1, D2);
  led.begin(0x70);
  //
  wifiConnect();
  //
  timeClient.begin();
}
//
void loop() {
  ntpUpdate();
  //
  effectSec();
}
//
void wifiConnect() {
  WiFi.begin(ssid, password);
  int count = 0;
  while ( WiFi.status() != WL_CONNECTED ) {
    led.displaybuffer[0] = 0x01 << count;
    led.displaybuffer[1] = 0x01 << count;
    led.displaybuffer[3] = 0x01 << count;
    led.displaybuffer[4] = 0x01 << count;
    led.writeDisplay();
    //
    count = count + 1;
    if ( count > 5 ) count = 0;
    delay ( 200 );
  }
}
//
void ntpUpdate() {
  timeClient.update();
  int currentMin = timeClient.getMinutes();
  if (previousMin != currentMin) {
    previousMin = currentMin;
    int currentTime = timeClient.getHours() * 100 + currentMin;
    //
    if (hours12) {
      if (currentTime > 1259) {
        led.print(currentTime - 1200, DEC);
      } else {
        led.print(currentTime, DEC);
      }
      //
      if (currentTime > 1159) {
        led.displaybuffer[2] = 8; // PM
      } else {
        led.displaybuffer[2] = 4; // AM
      }
    } else {
      led.print(currentTime, DEC);
    }
    led.writeDisplay();
  }
}
//
void effectSec() {
  int currentSec = timeClient.getSeconds();
  if (previousSec != currentSec) {
    previousSec = currentSec;
    led.displaybuffer[2] = led.displaybuffer[2] | 0x02;
    led.writeDisplay();
    colonOnTime = millis();
  }
  //
  if (colonBlink) {
    if (millis() - colonOnTime > 500) {
      led.displaybuffer[2] = led.displaybuffer[2] & 0xFD;
      colonOnTime = millis();
    }
    led.writeDisplay();
  }
}

우선 이전 글에서 완성한 소스를 가져와 약간 수정하였습니다. NTP 서버에 접속하여 시간 데이터를 가져오고, 7 세그먼트 LED 장치에 현재 시간을 출력하며, 12/24시간제 및 가운데 콜론의 깜박임까지 적용된 소스인데, loop() 함수가 너무 복잡해지지 않도록 몇 개의 함수를 만들어 정리하였습니다.

#include <NTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_LEDBackpack.h>
#include <Adafruit_NeoPixel.h>
WiFiUDP udp;
NTPClient timeClient(udp, "kr.pool.ntp.org", 32400, 3600000);
Adafruit_7segment led = Adafruit_7segment();
Adafruit_NeoPixel ledBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
void setup(){
  Wire.begin(D1, D2);
  led.begin(0x70);
  //
  ledBar.begin();
  ledBar.show();
  //
  wifiConnect();
  //
  timeClient.begin();
}

이전 소스에 Neopixel LED 장치를 사용할 준비를 합니다.

적용하려는 효과는 위와 같습니다. 1초마다 한 LED씩 슬라이드 합니다.

bool ledBlink = true;
  if ( ledBlink ) {
    .
    .
  }

Neopixel LED를 이용한 초침 효과는 출력 여부를 사용자가 선택할 수 있습니다. 이 때 사용하는 변수를 ledBlink라는 이름으로 선언하였습니다.

bool ledBlink = true;
byte ledCount = 0;
  if ( ledBlink ) {
    ledBar.setPixelColor(ledCount, , , );
    if ( ++ledCount > 7 ) ledCount = 0;
    ledBar.show();
  }

loop() 함수가 무수히 회전하는 동안, 매 1초마다 다음 LED 모듈에 출력하도록 체크할 변수가 필요합니다. ledCount 변수가 이 역할을 합니다.

bool ledBlink = true;
byte ledCount = 7;
int bgRed = 100;
int bgGreen = 30;
int bgBlue = 30;
int fgRed = 0;
int fgGreen = 0;
int fgBlue = 255;
  if ( ledBlink ) {
    ledBar.setPixelColor(ledCount, bgRed, bgGreen, bgBlue);
    if ( ++ledCount > 7 ) ledCount = 0;
    ledBar.setPixelColor(ledCount, fgRed, fgGreen, fgBlue);
    ledBar.show();
  }

초침 역할을 할 LED 모듈의 색상과 나머지 배경색에 대한 변수를 선언하고 값을 미리 초기화 하였습니다. 초침이 다음 LED로 이동한 후, 이전 LED는 다시 배경색으로 채워야 하는데, 이를 위해 위와 같이 코드를 작성하였습니다. ledCount 변수의 초기 값도 이 기능을 위해 수정하였습니다. 첫 번째(0번) LED가 초침이라면 이전 위치인 마지막(7번) LED를 배경색으로 변경해야하기 때문입니다.

bool ledBlink = true;
bool ledBlinkFirst = true;
byte ledCount = 7;
int bgRed = 100;
int bgGreen = 30;
int bgBlue = 30;
int fgRed = 0;
int fgGreen = 0;
int fgBlue = 255;
  if ( ledBlink ) {
    if ( ledBlinkFirst ) {
      for ( int i = 0; i < 8; i++ ) {
        ledBar.setPixelColor(i, bgRed, bgGreen, bgBlue);
      }
      ledBlinkFirst = false;
    }
    ledBar.setPixelColor(ledCount, bgRed, bgGreen, bgBlue);
    if ( ++ledCount > 7 ) ledCount = 0;
    ledBar.setPixelColor(ledCount, fgRed, fgGreen, fgBlue);
    ledBar.show();
  }

이제까지의 소스는 1초마다 LED 하나는 초침 색, 이전 것은 배경색 이렇게 두 개의 LED만 변경합니다. 그래서 프로그램이 처음 실행될 때는 아직 나머지 6개의 LED는 색상이 채워지지 않은 상태입니다. 이를 체크하기 위해 ledBlinkFirst라는 변수를 선언하고 배경색 초기화 기능을 삽입하였습니다.

완성된 코드는 세그먼트 LED의 가운데 콜론을 출력하는 함수에 삽입하였습니다. 실행 결과와 전체 소스는 아래와 같습니다.

전체 소스
#include <NTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_LEDBackpack.h>
#include <Adafruit_NeoPixel.h>
//
const char *ssid     = "coffee";
const char *password = "coffee11";
//
int previousMin = 0;
int previousSec = 0;
bool colonBlink = true;
unsigned long colonOnTime;
bool hours12 = true;
//
bool ledBlink = true;
bool ledBlinkFirst = true;
byte ledCount = 7;
int bgRed = 100;
int bgGreen = 30;
int bgBlue = 30;
int fgRed = 0;
int fgGreen = 0;
int fgBlue = 255;
//
WiFiUDP udp;
NTPClient timeClient(udp, "kr.pool.ntp.org", 32400, 3600000);
Adafruit_7segment led = Adafruit_7segment();
Adafruit_NeoPixel ledBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
//
void setup(){
  Wire.begin(D1, D2);
  led.begin(0x70);
  //
  ledBar.begin();
  ledBar.show();
  //
  wifiConnect();
  //
  timeClient.begin();
}
//
void loop() {
  ntpUpdate();
  //
  effectSec();
}
//
void wifiConnect() {
  WiFi.begin(ssid, password);
  int count = 0;
  while ( WiFi.status() != WL_CONNECTED ) {
    led.displaybuffer[0] = 0x01 << count;
    led.displaybuffer[1] = 0x01 << count;
    led.displaybuffer[3] = 0x01 << count;
    led.displaybuffer[4] = 0x01 << count;
    led.writeDisplay();
    //
    count = count + 1;
    if ( count > 5 ) count = 0;
    delay ( 200 );
  }
}
//
void ntpUpdate() {
  timeClient.update();
  int currentMin = timeClient.getMinutes();
  if (previousMin != currentMin) {
    previousMin = currentMin;
    int currentTime = timeClient.getHours() * 100 + currentMin;
    //
    if (hours12) {
      if (currentTime > 1259) {
        led.print(currentTime - 1200, DEC);
      } else {
        led.print(currentTime, DEC);
      }
      //
      if (currentTime > 1159) {
        led.displaybuffer[2] = 8; // PM
      } else {
        led.displaybuffer[2] = 4; // AM
      }
    } else {
      led.print(currentTime, DEC);
    }
    led.writeDisplay();
  }
}
//
void effectSec() {
  int currentSec = timeClient.getSeconds();
  if (previousSec != currentSec) {
    previousSec = currentSec;
    led.displaybuffer[2] = led.displaybuffer[2] | 0x02;
    led.writeDisplay();
    colonOnTime = millis();
    //
    if ( ledBlink ) {
      if ( ledBlinkFirst ) {
        for ( int i = 0; i < 8; i++ ) {
          ledBar.setPixelColor(i, bgRed, bgGreen, bgBlue);
        }
        ledBlinkFirst = false;
      }
      ledBar.setPixelColor(ledCount, bgRed, bgGreen, bgBlue);
      if ( ++ledCount > 7 ) ledCount = 0;
      ledBar.setPixelColor(ledCount, fgRed, fgGreen, fgBlue);
      ledBar.show();
    }
  }
  //
  if (colonBlink) {
    if (millis() - colonOnTime > 500) {
      led.displaybuffer[2] = led.displaybuffer[2] & 0xFD;
      colonOnTime = millis();
    }
    led.writeDisplay();
  }
}

이번 글은 여기서 마무리합니다. White 색상을 이용한 간이 스탠드 기능은 새로운 부품이 필요하기 때문에 다음 글로 넘기겠습니다. 이상입니다.

Comments