[36호]카멜레온 DIY LED 이야기 2
카멜레온 DIY LED 이야기 2
WS2812B의 동작원리와 12색상환 만들기
글 | 신상석 ssshin@jcnet.co.kr
디바이스마트 매거진 독자 여러분, 안녕하세요. 앞으로 5회에 걸쳐 [카멜레온 DIY LED 이야기]를 진행할 신상석입니다. 이 이야기는 WS2812B라는 컬러 LED를 기반으로 제작된 [카멜레온 DIY LED] 시리즈를 이용하여 생활에 필요한 다양한 형상을 꾸며보고 이것을 다양한 컬러로 디스플레이 해보는 내용입니다. 앞으로 진행할 내용에 대하여 간단히 알아보면 다음과 같습니다. (약간 변경될 수도 있습니다.) 앞으로 즐겁고 유익한 강의가 될 수 있도록 많은 격려와 성원 부탁드립니다. |
디바이스마트 매거진 독자 여러분, 안녕하세요.
[카멜레온 DIY LED 이야기] 두번째 시간이 돌아왔습니다.뜸하게 만나므로 지난 시간에 어디까지 이야기했는지 기억이 잘 나지 않으실 것 같은데, 지난 시간에는 카멜레온 DIY LED에 대한 정의, 형태, 종류, 타 모듈과의 비교, 응용 예 등과 같이 전반적인 소개가 이루어졌습니다. 이번 호부터는 좀 더 세부적으로 카멜레온 DIY LED에 대하여 알아보는 시간을 갖도록 하겠습니다. 일단은, 카멜레온 DIY LED의 기본 LED인 WS2812B의 동작 원리에 대한 이야기로 시작해 보겠습니다. 어떤 분야든 개념과 원리가 중요하니까요.
WS2812B의 동작 원리
WS2812B의 특징은 간단하게 말해서, “내가 원하는 개수만큼 연속적으로 간단하게 연결하면서, 내가 원하는 시간에 내가 원하는 컬러(껐다 켰다 포함)로 자유롭게 표현할 수 있는 LED”라고 이야기한 바 있습니다.
WS2812B의 외관 모습은 지난번에 보았으므로 오늘은 신호선과 핀 배열을 살펴보겠습니다. 아래와 같이 생겼습니다.
4핀 중, VCC(+)와 GND(-)는 전원이고, DIN과 DOUT은 각각 데이터입력, 데이터출력 신호입니다. VCC와 GND는 3.5 V ~ 5.3 V 사이의 전원으로 여러 개의 WS2812B가 연결되는 경우에도 모두에게 공통으로 연결되어져야만 합니다. DIN은 WS2812B로 전달되는 데이터의 입력 신호선이고, DOUT은 DIN으로 들어온 데이터 중 자신이 사용하지 않는 데이터를 통과(bypass)시키는 출력 신호선인데, 여러 개의 WS2812B를 연결하는 경우에는 앞쪽 WS2812B의 DOUT이 뒤쪽 WS2812B의 DIN에 연결되는 형태로 직렬(시리얼) 연결합니다. 데이터를 공급해주는 제어기를 포함한 연결 관계를 그림으로 나타내 보면 아래와 같이 되겠습니다. 연결 방식은 상당히 간결한 편이지요.
데이터를 공급해주는 제어기를 포함한 연결 관계 |
이렇게 연결하고도 LED마다 따로따로 여러가지 색채와 동작을 표현할 수 있다는 것이 신기할 따름인데… 과연 어떻게 동작하는 것일까요? 살펴 보겠습니다.
세부적인 내부 회로도가 공개되어 있지 않아 정확하게 알 수는 없지만, 데이터시트에 나온 내용을 기준으로 WS2812B의 동작 방법을 설명하면 다음과 같습니다.
· 내부에 R(Red), G(Green), B(Blue)에 해당하는 컬러 LED와 이를 제어하는 로직이 함께 통합되어 있음
· DIN으로 입력된 데이터는 내부의 데이터 래치에 의하여 저장되고 신호증폭 로직을 거쳐 DOUT으로 출력됨
· 전원이 켜지거나, 데이터리셋이 되면, 내부 로직은 DIN 포트로부터 데이터를 받아들이는데, 처음 24비트는 자신의 내부 데이터 래치로 보내고 이후의 데이터는 DOUT 포트로 bypass하여 내보냄. 한편, LOW 전압이 50uS 이상 지속되면 데이터 리셋으로 간주하고 다시 처음부터 시작함
· 내부에 전송된 3바이트(24비트) 데이터는 1바이트(8비트)씩 녹색, 빨강, 파랑의 컬러 LED 값을 나타내며, 전송 순서도 녹빨파(GRB, Green, Red, Blue) 순임
· 전원은 3.5 V ~ 5.3 V까지 사용이 가능하며, 입력 데이터는 0.7 x VCC 이상의 전압이면 HIGH, 0.3 x VCC 이하의 전압이면 LOW로 처리됨 (5 V 전원의 경우 3.5 V 이상이면 HIGH, 1.5 V 이하이면 LOW)
· 데이터 전송 프로토콜은 펄스폭의 길이로 데이터값을 산정하는 방식을 사용하며, 아래와 같은 규격으로 판정함
즉, DIN 라인의 레벨이 400ns LOW + 850ns HIGH 를 유지하면 1비트의 데이터 HIGH로 인식하고, 800ns HIGH + 450ns LOW 를 유지하면 1비트의 LOW로 인식하는 방식
이와 같은 규격을 기준으로 처음 그림과 같은 연결에서 데이터가 전송되는 방법을 나타내면 아래 그림과 같습니다.
예를 들어 만약 첫번째 LED에는 빨강, 두번째 LED에는 파랑, 세번째 LED에는 녹색으로 LED를 1초 동안 켜고 싶다면, 제어기에서는 (0×00 ▶ 0xFF ▶ 0×00) ▶ (0×00 ▶ 0×00 ▶ 0xFF) ▶ (0xFF ▶ 0×00 ▶ 0×00) 을 보내면 LED가 원하는 색으로 켜지고 1초 후에 다시 (0×00 ▶ 0×00 ▶ 0×00) ▶ (0×00 ▶ 0×00 ▶ 0×00) ▶ (0×00 ▶ 0×00 ▶ 0×00) 을 보내면 LED가 모두 꺼지게 됩니다.
처음에 연속해서 9바이트의 데이터를 보내면, 첫번째 LED에서 3바이트를 사용하고, 이후 전달된 6바이트는 2번째 LED로 전달되며, 마찬가지로 두번째 LED는 전달된 6바이트 중 처음 3바이트를 사용하고 마지막 3바이트는 세번째 LED로 보내게 되는 방식을 사용하는 것입니다. 1초가 지난 후에 다시 9바이트의 데이터를 보내면 이것은 50us 보다 긴 시간 이후에 들어온 데이터이므로 이전 데이터의 연속으로 보지 않고, 새로운 데이터가 들어온 것으로 간주하여 다시 첫번째 LED에서 3바이트를 사용하는 방식으로 진행되겠습니다.
이런 원리를 잘만 이용하면, WS2812B LED를 내가 원하는 개수만큼 원하는 형태로 연결한 후, 이들의 색상이 시간에 따라 변화하도록 프로그램하는 것이 가능할 것 같네요. 오, 뭔가 있을 법한 느낌!!!
LED(JLED-BAR-1) 1개 불 켜기
그럼, 이제 문제는 0, 1 데이터, 나아가서 GRB 데이터, 더 나아가서 여러 개의 WS2812B 연결시의 데이터를 어떻게 프로그램으로 생성할 수 있는가 하는 것인데…
“천리길도 한 걸음부터”라고 했으니 일단 WS2812B 1개로 구성된 카멜레온 DIY LED인 JLED-BAR-1에 파랑색 불을 켜는 것으로부터 시작해 보겠습니다. 이 후 이것을 응용해서 1600만가지 이상의 컬러를 골라서 디스플레이 해보는 데까지 가보시지요. (와우!!!)
일단 JLED-BAR-1의 모습을 보실까요. 짜잔~~~!
앞면에는 WS2812B가 1개 배치되어 있고(컨덴서 1개 포함), 뒷면에는 2.54mm 1X3 핀헤더가 양쪽으로 배치되어 있습니다. 뒷면의 실크 표시는, V는 VCC(+5V), G는 GND, I는 DIN(데이터입력), O는 DOUT(데이터 출력)입니다.
JLED-BAR-1은 WS2812B를 기반으로 하여 이미 출시되어 있는 다른 상품과 확연히 차이가 나는 것이 3가지 있습니다.
첫째, 1X3 입력핀과 1X3 출력핀의 형태가 DIP 타입(2.54mm 핀헤더 타입)입니다. 입출력 신호가 SMD로 되어 있는 것은 납땜을 해야하는 번거로움이 있지만 이렇게 DIP 타입을 사용하면 케이블이나 점퍼 등을 사용하여 연결을 매우 쉽게 할 수 있는 장점이 있습니다.
둘째, 가로와 세로의 길이가 각각 1cm입니다. BAR 시리즈의 경우는 LED가 증가할 때마다 1cm씩 가로 길이가 증가하도록 설계되어 있습니다. 이렇게 하면 용도에 따라 매우 편리하게 적용이 가능합니다. 예를 들어 30cm X 20 cm 되는 액자의 테두리를 BAR 시리즈로 만든다면 가로는 JLED-BAR-10을 3개 연속 연결하면 딱 알맞게 적용이 되는 것이지요. 급할 때는 자 대용으로도 사용이 가능합니다. 그러면 아래 JLED-BAR-10 의 길이는 얼마일까요? (답이 없어도 알겠죠?)
셋째, 그림으로는 식별이 잘 안되지만 1X3 입출력 핀헤더는 가장자리에서 안쪽으로 1.27mm 거리에 위치합니다. 이렇게 되면 카멜레온 DIY LED 모듈 여러 개를 연속적으로 연결시 2X3 쇼트핀(JLED-CON-0, 2핀간의 거리 2.54mm)으로 바로 연결이 가능하므로 레고처럼 붙였다 떼었다 하는 동작이 매우 용이합니다. JLED-BAR-1과 JLED-TRI-3을 쇼트핀 3개로 결합한 형태를 보시지요.
쇼트핀 3개가 하나로 결합되어 있는 JLED-CON-0를 연결하면 아래와 같이 좀 더 깔끔하게 연결할 수 있습니다.
앞면도 보여드리지요.
자, 그럼 이제 JLED-BAR-1을 아두이노에 연결하여 파랑색 불을 켜는 것을 예로 들어 본격적으로 프로그램을 시작해 보겠습니다.
일단, 제어기 연결은 해놓고 시작합니다. 아두이노 UNO를 제어기로 사용하는 경우 아래와 같이 연결이 되겠습니다. 아두이노와 JLED-BAR-1의 신호쌍으로 +5V-V(VCC)(빨강선), GND-G(GND)(검정선), D2-I(DIN)(녹색선)을 각각 연결합니다.
파랑색 불이 들어오게 하려면 제어기(아두이노)에서 (0×00, 0×00, 0xFF)의 24비트 데이터를 JLED-BAR-1으로 보내주어야 합니다. 첫번째 데이터인 0×00을 보내려면 ‘0’ 을 나타내는 비트 신호를 8번 연속해서 보내주어야 하는데, 규격에 보면 ‘0’ 신호는 실제 WS2812B의 DIN 라인의 레벨을 ‘800ns HIGH + 450ns LOW’로 유지하여야 합니다. 우리는 제어기(아두이노)의 데이터 포트인 D2를 연결하였으므로 이 핀을 ‘800ns HIGH + 450ns LOW’로 8번 만들면 되겠네요.
엥? 그런데 어떻게 만들죠?
digitalWrite(D2) = HIGH;
delay_ns(400); // nano sec 딜레이함수
digitalWrite(D2) = LOW;
delay_ns(850); // nano sec 딜레이함수
이렇게 하면 될까요?
바로 문제에 봉착하게 됩니다. 첫번째 문제는 delay_ns() 라는 ns 단위 딜레이 함수가 아두이노에 존재하지 않는다는 것이고,(ms, us 딜레이 함수는 있는데…) 두번째 문제는 혹시 함수를 새로 만든다고 가정해도, ns 와 같이 아주 짧은 시간 동안의 딜레이를 적용할 수 있는 정확한 함수를 프로그램 하는 것이 가능하지 않을 것으로 판단된다는 것이고, 세번째 문제는 혹 이런 함수를 비슷하게 작성한다 하더라도 실제 이 함수를 실행하는데 소요되는 시간을 매우 정교하게 제어하는 것이 어렵다는 사실입니다.
그렇다면 … 어떻게 해야 하나요?
예. 정답! 이것을 해결하려면, 고급 언어인 C 언어 레벨에서의 프로그램을 포기하고 어셈블리(assembly) 언어를 이용하여 프로그램을 작성하여야 합니다.
어셈블리 언어는 기계어이므로, 실행 속도가 정해져 있습니다. 예를 들어 ‘nop’ 이라는 어셈블리 명령어는 거의 모든 프로세서에서 1 클록의 시간만을 사용합니다. 제어기(아두이노)가 만약 16Mhz 클록으로 동작하는 제어기(아두이노)라고 가정하면 ‘nop’을 실행하는데 걸리는 시간은 1/16000000 초 = 62.5ns가 됩니다. 이것을 잘 이용한다면 우리가 필요로 하는 형태의 데이터를 만들 수도 있을 것 같네요. 한 번 같이 해보실까요?
아두이노 UNO(16Mhz 클록)을 기준으로 ‘0’ 데이터를 한 번 만들어 보겠습니다.
‘0’ 데이터는 400ns HIGH + 850ns LOW 의 쌍으로 이루어집니다.
400ns의 HIGH를 유지하려면, 400/62.5 = 6.4 = 약 6 클록이 필요하고, 850ns의 LOW를 유지하려면, 850/62.5 = 13.6 = 약 14 클록이 필요하므로, 아래와 같이 프로그램하면 될 것 같습니다. (규격에 보면 +/- 2 클록 정도의 오차는 허용이 되므로 정수배를 취하여도 무방함)
// WS2812B ‘0’ 데이터 만들기
PORTD |= 0b00000100; // D2 디지털핀을 1로 만듬, 1-2 클록 소모(?)
asm volatile(“nop”); // 첫번째 nop, 어셈블리 명령어로 실행, 컴파일러 최적화 금지!
asm volatile(“nop”); // 두번째 nop, 어셈블리 명령어로 실행, 컴파일러 최적화 금지!
…
…
asm volatile(“nop”); // 다섯번째 “nop”, 여기까지 6-7 클록 소모(?)
PORTD |= 0b00000000; // D2 디지털핀을 0으로 만듬, 1-2 클록 소모(?)
asm volatile(“nop”); // 첫번째 nop, 어셈블리 명령어로 실행, 컴파일러 최적화 금지!
asm volatile(“nop”); // 두번째 nop, 어셈블리 명령어로 실행, 컴파일러 최적화 금지!
…
…
asm volatile(“nop”); // 열세번째 “nop” , 여기까지 14-15 클록 소모(?)
D2 핀의 상태를 바꾸려는 명령어는 최소한 1-2 클록은 소모될 것이라고 가정하고 ‘nop’ 명령어를 필요한 개수보다 1개 적은 개수만큼만 시행하면 가장 근접한 신호 파형을 만들 수 있을 것입니다. 물론, 좀 더 정확하게 측정하려면 어셈블리어로 컴파일된 .S 파일을 분석하던지 실제 이 부분의 실행 파형을 오실로스코프로 확실하게 확인하는 것이 좋겠습니다. 우리는 일단 그냥 사용해 보고 실제 프로그램을 실행해서 정말 되는지를 확인해 보는 것으로 하겠습니다. 그리고, 1비트 데이터를 보낼 때마다 위와 같은 프로그램을 직접 써 넣어 프로그램할 수는 없으니까, 이것을 마크로(MACRO)를 이용하여 선언을 해 놓은 것이 좋을 듯 합니다.
아래와 같이 마크로를 정의해 놓으면 조금 편리하게 사용할 수 있겠네요.
#define NOP asm volatile(“nop”)
#define NOP2 NOP; NOP
#define NOP3 NOP2; NOP
#define NOP5 NOP3; NOP2
#define NOP6 NOP3; NOP3
#define NOP12 NOP6; NOP6
#define NOP13 NOP6; NOP6; NOP
// WS2812B ‘0’ 데이터 만들기
PORTD |= 0b00000100; // D2 디지털핀을 1로 만듬
NOP5; // 5번의 “nop”
PORTD &= 0b11111011; // D2 디지털핀을 0으로 만듬
NOP13; // 13번의 “nop”
같은 방법으로 ‘1’ 데이터를 만들어 보겠습니다.
400ns의 HIGH를 유지하려면, 800/62.5 = 12.8 = 약 13클록이 필요하고, 450ns의 LOW를 유지하려면, 450/62.5 = 7.2 = 약 7클록이 필요하므로, 아래와 같이 프로그램하면 될 것 같습니다. (규격에 보면 150ns(+/- 2 클록 이상)의 오차가 허용이 되므로 정수배를 취하여도 무방합니다.)
// WS2812B ‘1’ 데이터 만들기
PORTD |= 0b00000100; // D2 디지털핀을 1로 만듬
NOP12; // 12번의 “nop”
PORTD |= 0b00000000; // D2 디지털핀을 0으로 만듬
NOP6; // 6번의 “nop”
0과 1을 만드는 것도 계속 사용할 것이니, 아예 이것도 ‘CODE0’, ‘CODE1’이라는 마크로로 선언해 놓는 것이 좋겠습니다. 덤으로 데이터리셋은 LOW 상태로 50us를 유지하는 것이므로 이것까지 ‘RES’라는 마크로로 함께 선언해 놓겠습니다.
#define CODE0 PORTD |= 0b00000100; NOP12; PORTD &= 0b11111011; NOP6
#define CODE1 PORTD |= 0b00000100; NOP5; PORTD &= 0b11111011; NOP13
#define RES PORTD &= 0b00000000; delay Microseconds(50)
자, 이제 기본 준비가 되었으니, 파랑색을 나타내는 (G, R, B) 값 (0×00, 0×00, 0xFF) 총 24비트의 값을 연속적으로 보내는 프로그램을 작성해 봅시다. 1비트 값을 보내는 코딩을 확장하여 순서대로 8비트씩 3번, 총 24비트를 전송하면 되겠습니다. G->R->B 순으로 보내야 하고, 바이트 내에서는 HIGH 비트 순으로 먼저 보내는 것이 규격이므로, 아래와 같이 되겠네요.
#define NOP asm volatile(“nop”)
#define NOP2 NOP; NOP
#define NOP3 NOP2; NOP
#define NOP5 NOP3; NOP2
#define NOP6 NOP3; NOP3
#define NOP12 NOP6; NOP6
#define NOP13 NOP6; NOP6; NOP
#define CODE0 PORTD |= 0b00000100; NOP5; PORTD &= 0b11111011; NOP13
#define CODE1 PORTD |= 0b00000100; NOP12; PORTD &= 0b11111011; NOP6
#define RES PORTD &= 0b00000000; delayMicroseconds(50)
void setup()
{
pinMode(2, OUTPUT);
}
void loop()
{
RES; // WS2812B 리셋, 0 상태로 50us delay
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // G(Green)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // R(Red)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // B(Blue)
}
잘 되려나요? 한 번 해보겠습니다.
아두이노 스케치 돌리고, 컴파일 & 다운로드 얍!
파랑색 불이 잘 켜졌네요. 잘 된 것이겠죠?
그럼, 한 숨 돌리고, 잠시 쉬었다가, 조금 더 전진해 보도록 하겠습니다.
10분간 휴식~!
JLED-BAR-1으로 무지개색 불 켜기
파랑색 불 켜는 것에 성공하였으니, 빨강색과 녹색 불 켜는 것은 쉽게 할 수 있겠지요?. 빨강색은 (G, R, B) 값을 (0×00, 0xFF, 0×00)로, 녹색은 (G, R, B) 값을 (0xFF, 0×00, 0×00)로 보내면 되니까 바로 되겠습니다. 그런데, 노랑색을 디스플레이 하려면, 어떻게 해야 할까요?
그림에서와 같이 빛의 3원색은 빨강, 녹색, 파랑이고, 노랑색은 빨강과 녹색을 동일 비율로 합하면 될 것 같으므로, 노랑색은 (G, R, B) 값인 (0xFF, 0xFF, 0×00) = (0b111111111111111100000000)을 생성하여 보내면 될 것 같습니다.
void loop()
{
RES; // WS2812B 리셋, 0 상태로 50us delay
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // G(Green)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // R(Red)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // B(Blue)
}
조심하여야 할 것은 일반적인 색상표는 코드를 (R, G, B) 순으로 표시하므로 이것을 WS2812B로 전송할 때는 이 순서를 그대로 사용하면 안되고 (G, R, B) 순으로 순서를 바꾸어 전송하여야 한다는 점입니다. 잊으시면 안됩니다!
예를 들어 주황색을 디스플레이하고 싶다면, 주황색의 (R, G, B) 값은 (FF, 66, 00)이므로 (G, R, B) 코드값은 (66, FF, 00)=0b011001101111111100000000 이 되므로 프로그램은 아래와 같이 됩니다.
void loop()
{
RES; // WS2812B 리셋, 0 상태로 50us delay
CODE0; CODE1; CODE1; CODE0; CODE0; CODE1; CODE1; CODE0; // G(Green)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // R(Red)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // B(Blue)
}
자, 그러면 이제 무지개색 표현에 도전해 보도록 합시다.
한가지 컬러가 디스플레이 되는 시간을 1초 정도로 하면 보기가 좋을 것 같습니다.
빨강색을 디스플레이하고 1초 기다렸다가, 다음에는 주황색을 디스플레이하고… 이렇게 7가지 컬러를 디스플레이하면 되겠네요. 1초 딜레이는 아두이노 라이브러리 delay()을 이용하면 쉽게 되겠지요? 모두 한 번 해 보겠습니다. 5분 기다려 봅니다.
(5, 4, 3, 2, 1, 0)
같이 해보시지요.
일단, 빨주노초파남보에 해당되는 (G, R, B) 코드를 위 색상표에서 눈대중으로 찾으면,
빨강 = (00, FF, 00), 주황 = (66, FF, 00), 노랑 = (FF, FF, 00), 초록 = (FF, 00, 00), 파랑 = (00, 00, FF),남색 = (00, 33, 99), 보라 = (00, 66, FF) 정도로 표시될 것 같습니다.
무식한 방법이긴 하지만 이 코드를 이용하여 위에서 사용했던 방법을 반복적으로 7번 사용하면서 디스플레이될 컬러만 순서대로 바꾸어주면 됩니다. 뭐 저라고 뾰족한 수는 없습니다.
#define NOP asm volatile(“nop”)
#define NOP2 NOP; NOP
#define NOP3 NOP2; NOP
#define NOP5 NOP3; NOP2
#define NOP6 NOP3; NOP3
#define NOP12 NOP6; NOP6
#define NOP13 NOP6; NOP6; NOP
#define CODE0 PORTD |= 0b00000100; NOP5; PORTD &= 0b11111011; NOP13
#define CODE1 PORTD |= 0b00000100; NOP12; PORTD &= 0b11111011; NOP6
#define RES PORTD &= 0b00000000; delayMicroseconds(50)
void setup()
{
pinMode(2, OUTPUT); // D2를 데이터비트로 사용
}
void loop()
{
RES; // 빨강, WS2812B 리셋, 0 상태로 50us delay
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // G(Green)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // R(Red)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // B(Blue)
delay (1000); // 1초 디스플레이
RES; // 주황, WS2812B 리셋, 0 상태로 50us delay
CODE0; CODE1; CODE1; CODE0; CODE0; CODE1; CODE1; CODE0; // G(Green)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // R(Red)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // B(Blue)
delay (1000); // 1초 디스플레이
RES; // 노랑, WS2812B 리셋, 0 상태로 50us delay
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // G(Green)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // R(Red)
CODE0; CODE0
; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // B(Blue)
delay (1000); // 1초 디스플레이
RES; // 초록, WS2812B 리셋, 0 상태로 50us delay
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // G(Green)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // R(Red)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // B(Blue)
delay (1000); // 1초 디스플레이
RES; // 파랑, WS2812B 리셋, 0 상태로 50us delay
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // G(Green)
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // R(Red)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // B(Blue)
delay (1000);// 1초 디스플레이
RES; // 남색, WS2812B 리셋, 0 상태로 50us delay
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // G(Green)
CODE0; CODE0; CODE1; CODE1; CODE0; CODE0; CODE1; CODE1; // R(Red)
CODE1; CODE1; CODE1; CODE0; CODE1; CODE1; CODE1; CODE0; // B(Blue)
delay (1000); // 1초 디스플레이
RES; // 보라, WS2812B 리셋, 0 상태로 50us delay
CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; CODE0; // G(Green)
CODE0; CODE1; CODE1; CODE0; CODE0; CODE1; CODE1; CODE0; // R(Red)
CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; CODE1; // B(Blue)
delay (1000); // 1초 디스플레이
}
잘 되셨다면 자축의 물개박수~~~
내가 원하는 7가지 무지개 색깔을 디스플레이 하는데 성공하였다는 이야기는 R, G, B로 나타낼 수 있는 모든 경우의 컬러를 디스플레이할 수 있다는 이야기가 됩니다. 총 몇가지나 될까요? R, G, B 강도를 나타내는 비트가 각각 8비트씩이므로 만들어질 수 있는 총 가지수는 256X256X256 = 16,777,226(1600만 이상)이 됩니다.
와~ 내가 JLED-BAR-1 1개를 가지고 표현할 수 있는 컬러가 1600만가지라니… 뭔가 좀 뿌듯한 감정이 올라오는 것도 같죠?
JLED-RING-12 로 12색상환 만들기
이제 1개의 WS2812B LED의 색상은 자유롭게 표현 가능하므로 이번에는 여러 개의 WS2812B LED가 연속적으로 연결된 형태에서 다양한 색상을 만들어 보는 연습을 해보겠습니다. 목표는 WS2812B 12개를 연속적으로 연결하여 둥근 링 형태를 구성하고 여기에 12가지 컬러를 넣어 아래와 같은 12 색상환을 만들어보는 것입니다.
12색상환은 초등학교 미술시간에 한번쯤은 색칠해 보았을 것으로 생각되는데요. 마침 카멜레온 DIY LED에 이것과 비슷하게 생긴 JLED-RING-12 라는 제품이 있으니 여기에 색상을 입히면 안성맞춤이 될 것 같네요.
JLED-RING-12는 앞에서 WS2812B 동작 원리를 설명하면서 사용하였던 연결관계 그림과 동일한 방법으로 WS2812B 12개를 직렬(시리얼)로 연결한 둥근 반지(RING) 형태를 띄고 있습니다.
프로그램 방법은 JLED-BAR-1에서 사용한 것과 거의 동일합니다. 조금 다른 것은 JLED-BAR-1의 컬러를 준비할 때는 1개의 RGB 쌍을 준비하였다면, 이번에는 12개의 RGB 쌍을 준비하여야 한다는 것과, 이 데이터를 순서대로 딜레이없이(중요!, 딜레이가 50us를 넘으면 12개 LED가 켜지는 것이 아니고 1개 LED만 컬러가 12번 바뀌면서 켜짐) 연속해서 내보내야 한다는 것입니다.
그럼 준비를 해볼까요?
인터넷에서 12색상환에 사용될 12가지 색상에 대한 코드를 찾아보니 아래와 같이 나옵니다. (RGB 순)
조금 번거롭긴 하지만 아직 라이브러리를 사용하는 방법을 배우지 않았으니, 오늘은 그냥 머슴처럼 꾹 참고 한 개씩 순서대로 내보내는 코딩으로 계속 가겠습니다. (좀 더 깔끔하고 간단한 방법은 다음 시간에…)
숫자가 많이 나오므로 마크로 정의를 음성기호(phonetic alphabet)로 더 만들어 사용하면 편할 것 같으므로 아래와 같이 형태로 마크로를 추가합니다.
#define ONE CODE0; CODE0; CODE0; CODE1 // 1 = 0×1 = 0b0001
#define TWO CODE0; CODE0; CODE1; CODE0 // 2 = 0×2 = 0b0010
…
#define ALPHA CODE1; CODE0; CODE1; CODE0; // 10 = 0xa = 0b1010
…
#define FOXTROT CODE1; CODE1; CODE1; CODE1; // 15 = 0xf = 0b1111
#define NOP asm volatile(“nop”)
#define NOP2 NOP; NOP
#define NOP3 NOP2; NOP
#define NOP5 NOP3; NOP2
#define NOP6 NOP3; NOP3
#define NOP12 NOP6; NOP6
#define NOP13 NOP6; NOP6; NOP
#define CODE0 PORTD |= 0b00000100; NOP5; PORTD &= 0b11111011; NOP13
#define CODE1 PORTD |= 0b00000100; NOP12; PORTD &= 0b11111011; NOP6
#define RES PORTD &= 0b00000000; delayMicroseconds(50)
#define ZERO CODE0; CODE0; CODE0; CODE0 // 0 = 0×00 = 0b0000
#define ONE CODE0; CODE0; CODE0; CODE1 // 1 = 0×01 = 0b0001
#define TWO CODE0; CODE0; CODE1; CODE0 // 2 = 0×02 = 0b0010
#define THREE CODE0; CODE0; CODE1; CODE1 // 3 = 0×03 = 0b0011
#define FOUR CODE0; CODE0; CODE1; CODE1 // 4 = 0×04 = 0b0100
#define FIVE CODE0; CODE0; CODE1; CODE1 // 5 = 0×05 = 0b0101
#define SIX CODE0; CODE0; CODE1; CODE1 // 6 = 0×06 = 0b0110
#define SEVEN CODE0; CODE0; CODE1; CODE1 // 7 = 0×07 = 0b0111
#define EIGHT CODE0; CODE0; CODE1; CODE1 // 8 = 0×08 = 0b1000
#define NINE CODE0; CODE0; CODE1; CODE1 // 9 = 0×09 = 0b1001
#define ALPHA CODE0; CODE0; CODE1; CODE1 // 10 = 0x0a = 0b1010
#define BRAVO CODE0; CODE0; CODE1; CODE1 // 11 = 0x0b = 0b1011
#define CHARLIE CODE0; CODE0; CODE1; CODE1 // 12 = 0x0c = 0b1100
#define DELTA CODE0; CODE0; CODE1; CODE1 // 13 = 0x0d = 0b1101
#define ECHO CODE0; CODE0; CODE1; CODE1 // 14 = 0x0e = 0b1110
#define FOXTROT CODE1; CODE1; CODE1; CODE1; // 15 = 0x0f = 0b1111
void setup()
{
pinMode(2, OUTPUT); // D2를 데이터비트로 사용
}
void loop()
{
// WS2812B 리셋, 0 상태로 50us delay
RES;
// 빨강 : 0xff0000 ▶ 0x00ff00
ZERO; ZERO; FOXTROT; FOXTROT; ZERO; ZERO;
// 다홍 : 0xdc143c ▶ 0x14dc3c
ONE; FOUR; DELTA; CHARLIE; THREE; CHARLIE;
// 주황 : 0xff7f00 ▶ 0x7fff00
SEVEN; FOXTROT; FOXTROT; FOXTROT; ZERO; ZERO;
// 귤색 : 0xf89b00 ▶ 0x9bf800
NINE; BRAVO; FOXTROT; EIGHT; ZERO; ZERO;
// 노랑 : 0xffd400 ▶ 0xd4ff00
DELTA; FOUR; FOXTROT; FOXTROT; ZERO; ZERO;
// 연두 : 0x66cc00 ▶ 0xcc6600
CHARLIE; CHARLIE; SIX; SIX; ZERO; ZERO;
// 녹색 : 0x00ff00 ▶ 0xff0000
FOXTROT; FOXTROT; ZERO; ZERO; ZERO; ZERO;
// 청록 : 0×005666 ▶ 0×560066
FIVE; SIX; ZERO; ZERO; SIX; SIX;
// 파랑 : 0x0000ff ▶ 0000ff
ZERO; ZERO; ZERO; ZERO; FOXTROT; FOXTROT;
// 남색 : 0x080b54 ▶ 0x0b0854
ZERO; BRAVO; ZERO; EIGHT; FIVE; FOUR;
// 보라 : 0xee82ee ▶ 0x82eeee
EIGHT; TWO; ECHO; ECHO; ECHO; ECHO;
// 자주 : 0×800080 ▶ 0×008080
ZERO; ZERO; EIGHT; ZERO; EIGHT; ZERO;
RES;
}
컴파일 ▶ 업로드 하면, … 와우~~~ 멋진 색상환이 나왔습니다.
환상적이네요.
너무 밝아서 가까이에 가서 다시 크게 찍어 본 사진을 올려 봅니다. 또렷ㅍ하지는 않네요.
실제 색상은 훨씬 더 화려하고 또렷합니다.
여러분도 모두 성공하셨죠? 또 축하의 박수!
그리고, 또 하나의 중요한 사실은 이제 컬러 세팅하는 방법과 여러 개의 WS2812B를 제어하는 방법을 습득했으므로, 이제부터는 어떤 모양, 어떤 컬러, 어떤 동작도 조금씩만 응용하면 모두 다 만들 수 있는 능력을 갖추었다는 사실입니다. 엄청난 힘을 갖게 된 것이지요. 어벤저스(?) 같은 힘!
물론, 조금 찜찜하게 남아 있는 부분도 있습니다.
CODE0, CODE1을 생성하는 처리 방법이 확실하지 않다는 점과, 코드 작성이 MACRO로 하나씩 작성하도록 되어 있어 지루하다는 점입니다. 이것 때문일까요? JLED-RING-12의 불빛이 아주 살짝 흔들리는 것 같기도 하구요. 어쨌든, 여기에 대한 해결책은 다음 번 이야기 “카멜레온 DIY LED를 위한 아두이노 라이브러리”에서 깔끔하게 해결하는 것으로 하겠습니다.
즐거운 시간 되셨나요? 오늘밤에는 꿈 속에서 알록달록한 12가지 아름다운 꿈이 펼쳐지도록 여러분의 머리맡에 12색상환 프로그램을 수행하는 JLED-RING-12를 켜놓고 주무시는 경험을 해보는 것도 좋겠습니다. 그럼, 이번 호의 이야기는 여기서 마치고 다음 호에서는 좀 더 재미있는 이야기를 가지고 다시 만나겠습니다. 안녕히 계십시오.