아두이노로 LED matrix 제어하기 #9 : Dot control 1

2018. 2. 20. 17:58

Arduino/Display

Arduino LedControl Library : 도트 단위로 LED 제어하기 #1

이전 글까지 시프트 효과를 통해서 간단한 움직임을 표현하는 프로그램을 구현하였습니다. 이번 글부터는 도트 단위의 좀 더 부드럽고 다양한 움직임 효과를 만들어 보겠습니다.

비트 단위의 연산자들을 이용한 개별 LED 제어와 간단한 가로, 세로 선 그리기 함수 작성이 주된 내용입니다.

도트(dot) 단위로 LED 모듈 제어하기

이제까지의 시프트 효과는 모듈 단위의 움직임입니다.

모듈에서 모듈로 이동하기 때문에 움직임이 부드럽지 못하고, 패턴 단위로 출력하기 때문에 다양한 표현에도 어려움이 있습니다. 그래서, 좀 더 나은 효과를 위해 위 그림처럼 도트(개별 LED) 단위로 움직임을 제어해 보겠습니다.

우선 아래의 예제를 보겠습니다.

#include "LedControl.h"
//
LedControl lc = LedControl(12,11,10,3);
//
void setup() {
  lc.shutdown(0, false);
  lc.shutdown(1, false);
  lc.shutdown(2, false);
  //
  lc.clearDisplay(0);
  lc.clearDisplay(1);
  lc.clearDisplay(2);
}
void loop() {
}

도트 단위로 제어하는 방법을 알아보기위해 위와 같은 간단한 예제로 시작하겠습니다. SPI 핀 번호를 제공하여 LedControl 인스턴스를 lc라는 이름으로 생성하고, setup() 함수내에서 적당히 초기화하였습니다. 이제 아래쪽 비어있는 loop() 함수를 작성해 가겠습니다.

void loop() {
  lc.setLed(0, 0, 0, true);
  lc.setLed(0, 0, 1, true);
  lc.setLed(0, 0, 2, true);
}

연재 초반에 소개했던 setLed() 함수를 이용하여 LED 세 개를 켜는 코드입니다. 첫 번째 인수는 모듈 번호이고 "0"번이므로 첫 번째 모듈을 가리킵니다. 두 번째 인수는 행(row) 번호입니다. "0 ~ 7"번까지 총 8개의 행을 가리키고, 예제는 "0"번이므로 첫 번째 행을 의미하겠죠! 세 번째 인수는 해당 행의 각각의 LED를 가리킵니다. 역시 "0 ~ 7"번까지이고 왼쪽부터 0번입니다. 마지막 인수인 "true"는 LED의 "on/off"를 지정합니다. 모두 "true"이므로 첫 번째 모듈, 첫 번째 행의 왼쪽부터 3개까지의 LED가 불이 들어오는 코드입니다.

결과 화면입니다. 켜기만 하고 따로 끄는 코드는 없기 때문에 차례대로 "on" 하여 상태를 유지하고 있습니다.

void loop() {
  lc.setLed(0, 0, 0, true);
  lc.setLed(0, 0, 1, true);
  lc.setLed(0, 0, 2, true);
  lc.setLed(0, 0, 3, true);
  lc.setLed(0, 0, 4, true);
  lc.setLed(0, 0, 5, true);
  lc.setLed(0, 0, 6, true);
  lc.setLed(0, 0, 7, true);
}

확인하지 않아도, 위 코드는 첫 번째 행이 모두 "on" 상태가 될 것입니다.

setRow() 함수를 이용한 bit 단위 제어

void loop() {
  lc.setRow(0, 0, B11111111);
}

연재 중에, setLed() 함수 다음에 배운 게 setRow() 함수입니다. 8 번의 setLed() 함수를 이용한 코드를 바로 위처럼 한 줄의 코드로 간단히 만들 수 있습니다.

LED를 "on/off"할 때 setLed() 함수는 마지막 인수의 "true/false"값으로 제어합니다. 이는 또 "1"과 "0"으로 대치할 수 있고, "1"과 "0"은 1 bit로 표현할 수 있는 데이터이며, 따라서 bit 하나가 하나의 LED를 제어할 수 있습니다. LED matrix의 한 행(row)은 8개의 LED로 구성되므로, 총 8비트 즉 1 byte의 데이터가 필요합니다.

그래서, setRow() 함수의 마지막 인수는 1 Byte 데이터입니다. 영문자 "B"가 바이트 단위 상수임을 의미합니다.

void loop() {
  lc.setRow(0, 0, B00000001);
}

1 바이트의 각 비트 값이 "0"일 때 "off", "1"일 때 "on"을 의미하고 각 비트와 LED의 순서가 동일하므로, 위 코드는 첫 모듈, 첫 행의 첫 번째부터 일곱 번째 LED는 "off", 마지막 LED만 "on" 함을 의미합니다.

void loop() {
  lc.setRow(0, 0, B00000001);
  delay(500);
  lc.setRow(0, 0, B00000010);
  delay(500);
}

코드 3줄을 추가했습니다. 마지막 LED를 켠 후, 500밀리 초 딜레이 후에 일곱 번째 LED를 켭니다. 두 번째 setRow() 함수의 마지막 인수인 byte값의 마지막 숫자가 "0"이므로 첫 번째 setRow() 함수에서 켰던 마지막 LED는 "off"가 됩니다. 모든 코드를 실행한 후엔 다시 loop() 함수에 진입하면서 8번 7번 LED를 번갈아 켜고 끄게 됩니다.

void loop() {
  lc.setRow(0, 0, B00000001);
  delay(500);
  lc.setRow(0, 0, B00000010);
  delay(500);
  lc.setRow(0, 0, B00000100);
  delay(500);
  lc.setRow(0, 0, B00001000);
  delay(500);
  lc.setRow(0, 0, B00010000);
  delay(500);
  lc.setRow(0, 0, B00100000);
  delay(500);
  lc.setRow(0, 0, B01000000);
  delay(500);
  lc.setRow(0, 0, B10000000);
  delay(500);
}

모든 조건이 동일한 상태에서 비트값만 바뀌는 8개의 setRow() 함수입니다. 마치 숫자 "1"이 오른쪽에서 왼쪽으로 한 자리씩 이동하는 모양과 같고, 이에 따라 LED도 오른쪽에서 왼쪽으로 번갈아 켜집니다.

결과는 위 그림과 같습니다. 왼쪽 byte내의 bit값 "1"이 이동할 때마다 LED도 같은 방향으로 "on/off"되고 있습니다. 이와 같이 인수로 주어지는 바이트의 비트값을 이동(시프트) 하면 도트(LED) 단위로 제어할 수 있으며, 아두이노 프로그래밍 언어인 C(C++)언어에서 제공하는  비트 단위 연산자를 이용하여 이를 구현할 수 있습니다.

비트 단위 연산자를 이용한 LED matrix 제어

void loop() {
  byte data = B00000001;
  lc.setRow(0, 0, data);
}

우선, 연산의 편의를 위해서 data 라는 byte type 변수를 사용하였습니다.

void loop() {
  byte data = B00000001;
  lc.setRow(0, 0, data);
  delay(500);
  data = data << 1;
  lc.setRow(0, 0, data);
  delay(500);
}

5번 행에 사용된 "<<" 이 기호가 비트 연산자중 하나이며 왼쪽으로 시프트(shift)하라는 의미입니다. 비트 시프트는 말그대로 각 비트값이 왼쪽으로 한 자리씩 이동하고 마지막 오른쪽 끝자리는 "0"으로 채우는 동작입니다. 위 코드는 500밀리 초마다 7, 8번 LED가 번갈아 점등됩니다.

void loop() {
  byte data = B00000001;
  int i;
  for (i = 0; i < 8; i++) {
    lc.setRow(0, 0, data);
    data = data << 1;
    delay(500);
  }
}

8번부터 1번까지(오른쪽에서 왼쪽으로) LED가 순차적으로 점등되는 코드입니다.

void loop() {
  byte data = B10000000;
  int i;
  for (i = 0; i < 8; i++) {
    lc.setRow(0, 0, data);
    data = data >> 1;
    delay(500);
  }
}

위와 같이 data의 초기값을 "B10000000"로 주고 6번 행처럼 오른쪽 비트 시프트 연산자를 사용하면 위쪽 코드와는 반대로 왼쪽 LED부터 오른쪽으로 차례로 점등됩니다. data 변수의 bit "1"값이 왼쪽에서 오른쪽으로 한 자리씩 이동하기 때문입니다.

void loop() {
  byte data = B11111111;
  int i;
  for (i = 0; i < 8; i++) {
    lc.setRow(0, 0, data);
    data = data >> 1;
    delay(500);
  }
}

비트 시프트 연산은 새로 추가되는 값은 "0"으로 세팅합니다. 위 2번행과 같이 비트값을 모두 "1"로 준 후, 시프트하면 점점 "0"으로 채워지고 따라서 결과는 처음 8개의 LED가 모두 "on"됐다가 왼쪽부터 하나씩 "off"되는 모양입니다.

비트 OR 연산자 : |(pipe, vertical bar)

void loop() {
  byte data1 = B00000001;
  byte data2 = B00000010;
  lc.setRow(0, 0, data1 | data2);
}

비트 연사자 "|"는 OR연산을 합니다. 즉, 두 비트중 하나만 "1"이어도 결과는 "1"이며 둘 다 "0"일 때만 결과가 "0"입니다. 따라서, 위 코드의 결과는 "B00000011"이고 오른쪽 마지막 두 LED가 점등됩니다.

void loop() {
  byte data = B00000001;
  byte temp;
  int i;
  for (i = 0; i < 8; i++) {
    lc.setRow(0, 0, data);
    temp = data << 1;
    data = data | temp;
    delay(500);
  }
}
void loop() {
  byte data = B00000001;
  int i;
  for (i = 0; i < 8; i++) {
    lc.setRow(0, 0, data);
    data = data | (data << 1);
    delay(500);
  }
}

위 두 코드는 결과가 동일합니다. 임시 변수를 사용하는 방법과 사용하지 않고 간단히 표현한 방법 두 가지입니다. 왼쪽 비트 시프트 연산을 하지만 기존의 값과 OR 연산을 하기 때문에 "1"값이 점점 쌓여 갑니다. 따라서, 오른쪽 끝 LED부터 "off"되는 것 없이 차례로 켜집니다. 

비트 연산을 이용하여 선 그리기

LED 매트릭스에 가로 선을 그리는 함수를 작성해 보겠습니다. 이전 연재에서 사용한 방법을 이용하려면 선 길이마다 그리기 위해 필요한 여러가지 패턴을 가지고 있어야 하지만, 비트 연산을 이용하면 계산에 의해 좀 더 간단하게 구현할 수 있을 것입니다.

선 그리기 함수 만들기

우선, 선 그리기 함수를 만들어 보겠습니다. 한 모듈안에서 시작 점과 끝 점을 인수로 받아 LED를 "on"하여 선 모양을 만들겠습니다.

void loop() {
  lineRow(0, 1, 8);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  lc.setRow(0, row, data);
}

행 방향으로 선을 그리는 lineRow() 함수를 먼저 작성하겠습니다. 첫 번째 인수(row)는 출력할 행을 지정하고, 두 번째와 세 번째는 시작 점과 끝 점을 인수로 받습니다. 위 코드의 data 변수는 모든 비트가 "1"로 채워져 있기 때문에, 위 코드를 실행하면 첫 번째 행의 모든 LED가 점등됩니다.

setRow() 함수로 출력하기 전, data 변수에 적당한 수정을 준다면 원하는 대로 출력할 수 있을 듯 합니다.

void loop() {
  lineRow(0, 1, 7);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  data = data << 1;
  lc.setRow(0, row, data);
}

함수 호출시의 인수값과 상관없이, 1번 LED부터 7번 LED까지 출력하려면 data변수를 왼쪽으로 1번  비트 시프트하면 됩니다. 시프트하면 마지막에 추가되는 비트값은 "0"이기 때문에 data변수는 "B11111110" 값을 갖게 되며 이 값을 이용해 출력하면 마지막 LED만 "off"됩니다.

void loop() {
  lineRow(0, 1, 6);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  data = data << 2;
  lc.setRow(0, row, data);
}

위와 마찬가지 형태이고, 이번엔 두 번 시프트하여 1번부터 6번 LED만 점등해야 합니다. 8개의 LED중 6개만 "on" 하려면 8에서 6을 뺀 2만큼 "off"하면 되고, 위 코드처럼 왼쪽 비트시프트 연산을 2자리 해주면 됩니다. 앞의 말을 그대로 식으로 만들면 함수 인수를 이용해서 처리할 수 있습니다.

// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  data = data << (8 - endLed);
  lc.setRow(0, row, data);
}

위 4번 행처럼 전체 LED(8개)에서 출력할 LED(endLed)만큼 빼면 "off"시켜야할 LED의 개수이고 이 만큼 왼쪽 비트시프트하면 원하는 결과를 얻을 수 있습니다.

void loop() {
  lineRow(0, 3, 8);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  data = data >> (startLed - 1);
  lc.setRow(0, row, data);
}

위쪽 코드와는 반대로 앞 쪽 LED를 "off"시키는 코드입니다. startLed 만큼 오른쪽 시프트를 실행하면 원하는 결과를 얻을 수 있는데, 7번 행을 보면 "startLed - 1"로 계산하고 있습니다. 이는 1번부터 8번 LED를 내부적으로는 0번부터 7번으로 참조하기 때문이며, 함수 호출시부터 시스템과 같은 방식으로 지정하면 아래 코드처럼 "1"을 빼줄 필요가 없습니다.

void loop() {
  lineRow(0, 2, 7);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  data = data >> startLed;
  lc.setRow(0, row, data);
}

위 코드는 "1"을 빼주지 않아도 됩니다. 그리고, endLed를 처리할 때 "8"이 아니라 "7"에서 빼도록 수정해 주는 것도 필요합니다.

이제, 어느 한 쪽 끝까지 가는 선이 아닌 중간에서 중간까지 출력하는 선을 그리기 위한 방법이 필요합니다. 위 두 가지 방법이 동시에 진행이 되어야 하는데, 비트 AND 연산자 "&"를 이용하여 처리할 수 있습니다.

void loop() {
  lineRow(0, 3, 5);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  byte tempA = data >> startLed;
  byte tempB = data << (7 - endLed);
  data = tempA & tempB;
  lc.setRow(0, row, data);
}

임시 변수 두 개(tempA, tempB)를 사용하여 원하는 결과를 얻었습니다. 각각 시작하는 LED와 끝나는 LED를 계산해서 저장한 후, 두 변수를 비트 AND 연산을 합니다. 양 쪽 모두 "1"일 때만 결과도 "1"이기 때문에 의도한 부분만 "1"로 세팅할 수 있습니다.

#include "LedControl.h"
//
LedControl lc = LedControl(12,11,10,3);
//
void setup() {
  lc.shutdown(0, false);
  lc.shutdown(1, false);
  lc.shutdown(2, false);
  //
  lc.clearDisplay(0);
  lc.clearDisplay(1);
  lc.clearDisplay(2);
}
//
void loop() {
  lineRow(0, 3, 5);
  lineRow(2, 0, 7);
  lineRow(3, 5, 7);
  lineRow(5, 0, 4);
  lineRow(7, 2, 3);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  byte tempA = data >> startLed;
  byte tempB = data << (7 - endLed);
  data = tempA & tempB;
  lc.setRow(0, row, data);
}

다섯 개의 가로 선을 그리는 소스와 결과 화면입니다. 시간 딜레이를 주지 않았기 때문에 동시에 그려지는 듯 보일 겁니다.

세로 방향 선 그리기 함수 만들기

우선, setRow() 함수를 이용하여 세로 방향으로 LED를 하나씩 점등하는 코드를 작성해 보겠습니다.

void loop() {
  byte data = B00000001;
  int i;
  for (i = 0; i < 8; i++) {
    lc.setRow(0, i, data);
    delay(500);
  }
}

for문을 이용해서 첫 행부터 마지막 행까지 같은 값을 출력합니다. data변수 값이 마지막만 "1"이므로 가장 오른쪽 LED만 차례로 점등됩니다. 켜진 LED를 끄는 코드는 없기 때문에 결과적으로 세로로 마지막 줄 모든 LED가 "on"되어 있게 됩니다.

void loop() {
  byte data = B00000001;
  int i;
  for (i = 0; i < 8; i++) {
    lc.setRow(0, i, data);
    delay(500);
    lc.setRow(0, i, B00000000);
  }
}

7번 행에의해 500밀리초마다 출력된 LED를 다시 "off"합니다. LED가 위에서 아래쪽으로 한 칸씩 시프트하는 모양을 출력합니다.

가로에 이어 세로 방향으로 선 그리기를 수행하는 함수를 만들기 위해 위 코드와 같이 setRow() 함수를 이용할 수 있지만, LedControl 라이브러리에서 제공하는 setColumn() 함수를 이용하면 아주 쉽게 구현할 수 있습니다. 가로 선그리기 함수를 약간만 수정하여 사용할 수 있기 때문입니다.

// 선 그리기 - 열 방향
void lineCol(int col, int startLed, int endLed) {
  byte data = B11111111;
  byte tempA = data >> startLed;
  byte tempB = data << (7 - endLed);
  data = tempA & tempB;
  lc.setColumn(0, col, data);
}

기존 코드에서 2, 7번 행만 수정하면 됩니다. 2번 행은 함수 이름과 첫 번째 인수명을 row에서 col로 수정했고, 7번 행에서 출력 함수를 setColum() 함수로 변경하였습니다. 함수를 호출하는 부분과 결과는 아래와 같습니다.

void loop() {
  lineCol(0, 3, 5);
  lineCol(2, 0, 7);
  lineCol(3, 5, 7);
  lineCol(5, 0, 4);
  lineCol(7, 2, 3);
}

// 선 그리기 - 열 방향
void lineCol(int col, int startLed, int endLed) {
  byte data = B11111111;
  data = (data >> startLed) & (data << (7 - endLed));
  lc.setColumn(0, col, data);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  data = (data >> startLed) & (data << (7 - endLed));
  lc.setRow(0, row, data);
}

위 코드는 시작 점과 끝 점을 계산할 때 임시 변수(tempA, tempB)를 사용하지 않고 간단히 줄인 코드입니다. 두 코드도 거의 동일하기 때문에 하나로 합칠  수 있지만, 아직 하나의 모듈에 대한 부분만 구현돼 있기 때문에 그냥 두도록 하겠습니다.

#include "LedControl.h"
// 세로 선 그리기
LedControl lc = LedControl(12,11,10,3);
//
void setup() {
  lc.shutdown(0, false);
  lc.shutdown(1, false);
  lc.shutdown(2, false);
  //
  lc.clearDisplay(0);
  lc.clearDisplay(1);
  lc.clearDisplay(2);
}
//
void loop() {
  lineRow(0, 0, 7);
  lineRow(2, 2, 6);
  lineRow(4, 4, 5);
  lineRow(7, 5, 7);
  lineCol(0, 0, 7);
  lineCol(2, 2, 6);
  lineCol(4, 4, 5);
  lineCol(7, 5, 7);
}
// 선 그리기 - 열 방향
void lineCol(int col, int startLed, int endLed) {
  byte data = B11111111;
  data = (data >> startLed) & (data << (7 - endLed));
  lc.setColumn(0, col, data);
}
// 선 그리기 - 행 방향
void lineRow(int row, int startLed, int endLed) {
  byte data = B11111111;
  data = (data >> startLed) & (data << (7 - endLed));
  lc.setRow(0, row, data);
}

가로, 세로 모두 출력하는 예제의 전체 소스와 결과 화면입니다.

이번 연재에서는 비트 연산자를 이용하여 개별 LED단위로 출력을 제어하는 방법을 알아보았고 간단한 가로, 세로 선 그리기 함수도 작성했습니다. 다음 글에선 이 함수들을 좀 더 확장해 보거나 움직임을 주는 함수들을 구현하겠습니다. 이상입니다.

Comments