[아두이노] WS2812 color LED 사용하기
칼라 LED는 LED 내부에 빛의 3원색에 해당하는 Red, Green, Blue LED가 들어있어 각각의 밝기를 조절하면 원하는 색을 만들어 낼 수 있다. 일반적인 형태의 칼라 LED는 아래 사진과 같은 형태로 4개의 다리가 나와 있다. Common Anode 타입의 LED인 경우 가장 긴 다리가 common anode로 이 다리는 +에 연결되어야 하고 나머지 3개의 다리는 프로세서의 I/O에 연결해 LED를 제어한다. 각 IO핀이 LOW면 해당 LED가 켜지고, HIGH면 LED가 꺼지게 된다. (PWM의 경우 duty가 0%면 가장 밝게 켜지고 100%면 꺼짐)
반대로 common cathode 타입의 LED인 경우, common cathode는 GND에 연결하고 나머지 3개 다리는 프로세서의 I/O에 연결한다. 이 경우 common anode와 반대로 IO핀이 HIGH이면 해당 LED가 켜지고, LOW면 LED가 꺼지게 된다. (PWM의 경우 duty가 100%면 가장 밝게 켜지고 0%면 꺼짐)
아래 그림은 common cathode타입의 컬러 LED를 사용할 때의 연결이다.
보통 위와 같이 전류제한 용으로 저항이 필요하다.
이제 LED가 연결되면 컬러 LED를 제어하는 2가지 방법이 있다. 첫번째는 GPIO를 이용하는 것으로, 이 경우 각각 IO핀은 2개의 상태, LOW(0V)/HIGH(Vcc),를 가질 수 있으므로 총 8개의 다른 색(검은색 포함해서)을 낼 수 있다.
다른 방법으로는 각 색깔별 LED의 밝기를 조절하는 것이다. PWM을 사용해 각각의 밝기를 조절하면 매우 다양한 색을 만들어 낼 수가 있게 된다.
아두이노의 경우 6개의 8-bit PWM 채널을 가지고 있기 때문에, 각 PWM은 0~255 사이의 값을 가질 수 있다. 그러므로 칼라 LED에 3개의 PWM 채널이 연결되기 때문에 총 256*256*256=16,777,216가지의 조합이 만들어 질 수 있다.
이 방법은 원하는 색을 만들어 낼 수 있지만, 단점은 칼라 LED 하나당 3개씩의 PWM 채널이 필요하다는 것이다. 아두이노 우노의 경우 6개, 메가의 경우 14개의 PWM을 가지고 있기 때문에 각각 2개, 4개의 칼라 LED밖에 연결할 수 없다. 물론 쉬프트 레지스터나 멀티플렉서등을 사용해서 더 많은 칼라 LED를 연결하는 방법이 있긴 해도 금새 매우 복잡해진다.
WS2812-based LED
Worldsemi라는 회사에서 이런 문제점을 완전히 해결해주는 새로운 칩을 만들었다. 처음에 만든것은 WS2811이라는 SMD IC로 내부에 시리얼 통신, 3개의 8-bit PWM 채널, 전류제한회로등을 가지고 있다.
다음으로 만든것은 WS2812로 5mm*5mm 정사각형 패키지 안에 WS2811에 추가로 고휘도 RGB LED를 다 집어 넣어 버렸다.
이 칩의 가장 좋은 점은 단지 4개의 핀(GND, Power(5V), Data In, Data Out)만 있으면 된다는 것이다. 즉 이 칩은 서로 daisy-chain으로 여러개를 연결해 줄 수 있다. 프로세서가 체인의 첫번째 칩의 Data-In을 구동하고, 첫번째 칩의 Data-Out이 두번째 칩의 Data-In을 다시 구동하는 식이다. 다음 그림을 보면 좀 더 이해하기 쉬울 것이다.
이런식으로 체인으로 연결하는데 특별히 칩 갯수 제한은 없다.
프로세서는 24-bit 값을 연속으로 보내게 되는데, 각 24-bit 값은 3개의 8-bit RGB 값을 나타낸다. 프로세서가 매번 24-bit 값을 보낼때마다 이 값은 체인의 첫번째 LED에 로드된다. 동시에 첫번째 LED는 자신이 가지고 있던 24-bit값을 두번째 LED로 전달한다. 두번째 LED는 세번째 LED로, 세번째 LED는 4번째 LED로 전달해 결국 값은 체인의 맨 마지막까지 전달되게 된다. 이 모든 작업이 매우 빠르게 진행되기 때문에 사람 눈에는 순식간에 일어난 것으로 보이게 된다.
결과적으로 프로세서의 IO 핀 1개(PWM이 아닌 일반 GPIO핀)만으로 수백개의 칼라 LED를 제어할 수 있게 된다.
여러 회사들이 이 Worldsemi의 WS2812를 사용해 제품을 만들어 판매하고 있다. Adafruit의 경우 NeoPixels라는 이름의 제품군을 만들었다. 이 NeoPixels에는 다양한 형태의 패키지가 있다. 아래는 NeoPixels Ring이다.
위의 NeoPixel Ring은 16개의 WS2812를 가지고 있지만 12, 24, 60개짜리도 판매하고 있다.
또한 aliexpress에 보면 아래와 같이 띠 형태의 WS2812 LED도 판매하고 있다. 아래 사진은 1m당 30개씩의 WS2812가 붙어있는 제품인데, 1m당 60개 또는 1m당 144개의 WS2812가 붙어있는 제품들도 구할 수 있다.
Feel the power!
보통 LED는 상대적으로 적은 전류를 소모한다고 생각하기 쉽지만, 여러개의 LED를 구동하려면 얼마나 많은 전류가 필요한지 알게되면 놀랄것이다. 각 NeoPixel은 최대 60mA(3개의 LED가 최대밝기일때, 즉 밝은 흰색인 경우)를 소모한다. 즉 프로세서의 전원을 컴퓨터의 USB포트에서 뽑아오거나 작은 아답터를 사용하는 경우 몇개의 WS2812를 구동할 수 있는가가 제한되게 된다. 만일 256개의 WS2812를 사용한다면 최대 15A를 전류를 사용할 수 있기 때문에 전원도 그에 맞게 준비해 줘야만 한다.
여기서는 두가지 매우 중요한 포인트를 이야기하겠다. 첫번째는 NeoPixel에 전원을 공급하는 파워서플라이의 +와 GND 단자 사이에 1000uF의 전해콘덴서를 연결해 줘야 한다는 것이다. 두번째는 프로세서와 NeoPixel의 첫번째 data in 사이에 300~500오옴 저항을 직렬로 연결해 줘야 한다. (보통 390오옴을 사용)
스트립에 WS2812가 몇개 안되는 경우는 다음과 같이 USB에서 전원을 공급받아도 충분하다.
하지만 WS2812의 갯수가 많아지거나 (5~6개 이상), 아두이노 미니 3.3V등을 사용하는 경우는 아래와 같이 별도의 5V 아답터를 사용해 줘야 한다. 아답터의 용량은 충분한 전류를 흘려줄 수 있는 것을 사용해야 한다. (LED 개당 최대 60mA를 사용하므로 WS2812 갯수 * 0.06A 보다 조금 더 큰 용량을 사용한다. 즉 WS2812가 16개 붙어있다고 하면 16*0.06A = 0.96A 가 되므로 최소한 1A 이상의 아답터를 사용하는것이 좋다.)
Timing is everything
WS2812 기반의 LED를 사용하려면 가장 쉬운 방법은 믿을만한 라이브러리를 사용하고 그 중 다른 사람에 의해 테스트 된 함수를 사용하는 것이다. Adafruit NeoPixel 라이브러리를 권장한다.
중요한점은 이 라이브러리는 아두이노 우노와 메가에서 사용될 수 있도록 하드코딩 되었다는 것이다. 여기서 ‘하드코딩’의 믜미는 이 라이브러리 함수는 타이밍을 정확하게 맞추기 위해 어셈블리 코드를 사용하고 있다는 것이다. 그 결과 매우 사용하기 쉬운 라이브러리가 만들어졌지만, 아두이노 패밀리의 다른 보드에 바로 사용할 수 없을수도 있다. 또한 이 라이브러리 함수는 한가지 목적을 가지고 만들어졌기 때문에 NeoPixel 스트링에 새 값을 보내기 위한 함수를 호출하면 가장 먼저 모든 인터럽트를 비활성화 시킨다. 인터럽트를 사용하지 않는 경우는 문제가 없지만, 코드에서 인터럽트를 많이 활용한다면 큰 문제가 될 수도 있다.
Example programs using the Adafruit Library
Ex1) Lighting the pixels one after the other
#include <Adafruit_NeoPixel.h>
#define pinPix 12 // WS2812에 연결하는데 사용하는 pin 번호
#define numPix 16 // 링에 연결되어 있는 WS2812 LED 갯수
// Parameter 1 = 링에 연결되어 있는 WS2812 LED 갯수
// Parameter 2 = WS2812에 연결하는데 사용하는 pin 번호
// Parameter 3 = pixel type flags, add together as needed:
// NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
// NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
// NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products)
// NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
Adafruit_NeoPixel myLeds = Adafruit_NeoPixel(numPix, pinPix, NEO_GRB + NEO_KHZ800);
void setup() {
myLeds.begin(); // Initialize the NeoPixel array in the Arduino's memory,
myLeds.show(); // turn all pixels off, and upload to ring or string
}
void loop() {
int pause = 100;
for (int i=0; i<numPix; i++) {
myLeds.setPixelColor(i,255,255,255);
myLeds.show();
delay(pause);
}
for (int i=0; i<numPix; i++) {
myLeds.setPixelColor(i,0,0,0);
myLeds.show();
delay(pause);
}
}
코드의 맨 처음은 Adafruit NeoPixel 라이브러리를 include 하는걸로 시작한다.
#include <Adafruit_NeoPixel.h>
다음은 WS2812에 데이터를 보내기 위해 사용하는 핀 번호와, 링이나 스트립에 몇개의 WS2812 LED가 붙어있는가를 정의한다. 여기서는 각각 12번 핀과 16개의 LED를 사용한다.
#define pinPix 12
#define numPix 16
정의가 끝나면 WS2812 LED 오브젝트를 인스턴스화 해 줘야 한다. 여기서는 인스턴스의 이름을 myLeds로 한다.
Adafruit_NeoPixel myLeds = Adafruit_NeoPixel(numPix, pinPix, NEO_GRB + NEO_KHZ800);
첫번째 파라미터(numPix)는 링/스트립에 붙어있는 WS2812 LED 갯수이다. 두번째 파라미터(pinPix)는 WS2812의 Data In에 연결된 핀 번호이다. 일단 여기서 세번째 파라미터는 신경쓰지 말고 넘어간다.
setup() 함수에서 가장 먼저 begin() 함수를 호출해 아두이노에 스트링을 위한 메모리를 초기화 한다. 그리고 show()를 호출해 스트링을 초기화 해 준다.
void setup() {
myLeds.begin();
myLeds.show();
}
이제 잠시 쉬면서 지금까지의 내용을 정리해 보자. 인스턴스화와 초기화 단계의 일부분으로 Adafruit 라이브러리는 아두이노 메모리에 배열을 만든다. 이 배열은 링/스트링의 WS2812 LED 갯수와 같은 크기가 된다. 아래 그림은 위의 예제에서 16개를 사용한 경우의 배열을 도식화 한 것이다.
각 24-bit 항목은 3개의 8-bit 서브필드로 구성되어 있고, 각 서브필드가 항목의 R, G, B 값을 나타낸다. Adafruit 라이브러리는 4개의 파라미터를 받아들이는 setPixelColor()라는 함수를 가지고 있다. 첫번째 파라미터는 값을 변경하길 원하는 WS2812 LED의 인덱스(여기서는 0~15 사이의 값)이고 나머지 3개의 파라미터는 원하는 R,G,B 값이다. 또한 중요한 것은 setPixelColor() 함수는 단지 아두이노 메모리에 있는 배열의 값만을 바꿀 뿐이라는 것이다. 값을 변경한 후 show() 함수를 호출해 배열에 들어있는 값들을 실제 WS2812 링/스트립에 전달해 주지 않으면 LED의 색은 바뀌지 않는다.
void loop() {
int pause = 100;
for (int i=0;i<numPix;i++) {
myLeds.setPixelColor(i, 255,255,255);
myLeds.show();
delay(pause);
}
for (int i=0;i<numPix;i++) {
myLeds.setPixelColor(i, 0,0,0);
myLeds.show();
delay(pause);
}
}
첫번째 for 루프에서는 각 LED를 100ms 간격으로 하나씩 흰색(255,255,255)으로 켜 준다. setPixelColor로 색을 변경한 후에 show()를 호출하는걸 잊으면 안된다.
두번째 for 루프에서는 각 LED를 100ms 간격으로 하나씩 검은색(0,0,0)으로 바꿔준다.
Ex2) Lighting all the pixels simultaneosly
#include <Adafruit_NeoPixel.h> // Library for NeoPixels
#define pinPix 12 // Pin driving NeoPixel Ring or String
#define numPix 16 // Number of NeoPixels in the Ring or Strip
Adafruit_NeoPixel myLeds = Adafruit_NeoPixel(numPix, pinPix, NEO_GRB + NEO_KHZ800);
void setup() {
myLeds.begin(); // Initialize the NeoPixel array in the Arduino's memory,
myLeds.show(); // turn all pixels off, and upload to ring or string
}
void loop() {
for (int i=0; i<numPix; i++) {
myLeds.setPixelColor(i,255,255,255);
}
myLeds.show();
delay(pause);
for (int i=0; i<numPix; i++) {
myLeds.setPixelColor(i,0,0,0);
}
myLeds.show();
delay(pause);
}
1번 예제와 거의 유사하지만 이번에는 show()와 delay() 함수를 for 루프 바깥으로 빼 냈다. 즉 for 루프에서 setPixelColor() 함수로 아두이노 메모리에 있는 모든 배열의 값을 변경한 다음에 show()를 호출해 변경된 값을 한꺼번에 LED에 반영시키는 것이다.