본문 바로가기
HW 프로그래밍/아두이노

스크랩[아두이노로 7 segment LED 제어하기 #3 TM1637 라이브러리 2]

by N2info 2019. 1. 5.

TM 1637에 대해 명쾌하게 정리한 사이트로, 참고를 위해 페이지를 복사함[원본]

아두이노로 7 segment LED 제어하기 #3 TM1637 라이브러리 2


Using 7 segments LED with Arduino : 라이브러리를 활용한 예제 2

계속해서, TM1637Display 라이브러리 함수들에 대한 설명을 이어 가겠습니다. 이전 글에서 소개한 showNumberDec 함수보다 좀 더 어려운 부분이 있어 설명이 길어질 듯 합니다.

도트까지 제어하는 showNumberDecEx 함수

숫자뿐만아니라 숫자 아래쪽 도트나 디스플레이 가운데의 콜론을 출력하기 위한 함수입니다. 숫자만 출력하는 showNumberDec() 함수도 결국 이 함수를 통해서 처리됩니다.

  • void showNumberDecEx(int num, uint8_t dots = 0, bool leading_zero = false, uint8_t length = 4, uint8_t pos = 0);
  • 두 번째 인수가 도트 및 콜론을 제어하는 값입니다. "0"값을 주면 off되고, 생략시 디폴트 값도 "0"입니다.
  • 나머지 인수들은 순서만 하나씩 뒤로 밀렸을 뿐, 내용은 showNumberDec() 함수와 동일합니다.
숫자 한 자리는 1 바이트 데이터로 표시합니다.

위 그림에서 첫 번째 자리에 표시된 영문자는 각 LED를 구분하기 위한 기호로, 7 세그먼트 LED의 데이터시트 등에서 설명을 위해 사용합니다. 각각의 LED를 세그먼트라 하고, 한 숫자를 위하여 7개의 세그먼트가 모여야 하기 때문에, 7 세그먼트라는 이름이 붙었습니다. 순서상으로, a 세그먼트가 먼저이고, g 세그먼트가 마지막입니다. 이를 번호로 붙여보면 두 번째 자리에서 보듯 숫자를 표현하기 위한 7개의 LED와 도트용 LED까지 합쳐 한 자리당 총 8개의 LED를 제어해야 합니다. LED의 on/off 두 가지 정보를 표현하기 위해 1비트(bit)가 필요하므로, 한 자리당 8비트 즉 1 바이트(byte)의 데이터가 필요합니다.

여기서 사용하는 모듈은 총 4자리이므로 4 바이트 데이터로 제어할 수 있는데, 문제는 시계 표현을 위한 콜론(":")입니다. 이 표시를 위한 여유 비트가 없어서, 이 모듈은 도트에 할당할 비트를 콜론에 주었고 따라서 도트는 사용할  수 없습니다. 제품을 구매할 때도 도트 버전과 콜론 버전을 구분하여 구매해야 합니다. 

콜론에 할당한 비트는 두 번째 자리의 도트에 해당하는 비트입니다. 두번째 도트를 켜는 방법이 바로 콜론을 제어하는 방법입니다.

showNumberDecEx() 함수의 첫 번째 인수는 생략할 수 없습니다!

showNumberDec() 함수와 마찬가지로 showNumberDecEx() 함수는 첫 번째 인수를 생략할 수 없습니다. 두 함수 모두 십진 숫자를 출력하기 위한 용도이기 때문에 출력할 숫자 데이터가 필수 인수입니다. showNumberDecEx() 함수는 여기에 추가로 출력하는 데이터의 각 자리별 도트 on/off 여부를 같이 제공합니다. 바꿔 말하면, 내가 on 시키려는 도트가 출력 범위안에 있어야 합니다. 범위 밖에 있는 도트는 제어할 수 없습니다. length, pos 인수를 이용해서 출력 범위를 지정한다면, 그에 맞게 도트 출력 여부도 맞춰주어야 합니다.

함수 호출시, length = 3, pos = 0 으로 인수를 준다면, 출력 범위는 위 그림의 첫 번째 붉은 사각형과 같고 이 경우 마지막 자리의 도트는 출력할 수 없습니다. 두 번째 사각형은 length = 2, pos = 1 로 주었을 때인데, 첫 번째와 마지막 도트는 출력할 수 없습니다.

만약, 콜론을 출력하고자 한다면, 두 번째 도트에 해당하므로, 위 그림의 마지막 사각형이 꼭 포함되도록 인수값을 제공해야 합니다.

두 번째 인수 dots에 들어갈 값 구하기

그럼 이제, 가운데 있는 콜론을 출력하기 위해서 어떤 값을 두 번째 인수로 제공해야 하는 지 알아보겠습니다. 도트는 지원하지 않아 결과를 확인할 수 없기 때문에 다루지 않겠습니다. 즉, 이제 부터의 설명은 두 번째 도트(콜론에 해당)를 어떻게 제어하는 지에 대한 것과 같습니다.

콜론을 찍기 위하여, 두 번째 인수는 "0x40, 0x20, 0x10" 3개의 값 중에 하나를 선택해서 제공합니다.

  • 0x80 = B10000000
  • 0x40 = B01000000
  • 0x20 = B00100000
  • 0x10 = B00010000

각각의 경우에 "1"의 값을 갖는 비트의 위치가 변하고 있음을 확인할 수 있습니다.

각 값에 있는 "0x" 기호는 이 숫자가 16진수임을 알려주는 접두사입니다. 16진수 한 자리는 4비트이므로, 위 값들은 8비트 즉 1바이트에 해당합니다. showNumberDec(), showNumberDecEx() 함수는 십진수를 출력하기 위한 함수이고, 십진수 한 자리(0 ~ 9)를 표현하기 위해 4비트만 있으면 충분하며, 여기에 도트를 위한 4비트를 추가하여 1바이트 데이터를 출력 데이터로 제공하는 것입니다. 비트OR 연산을 위해 하위 4비트를 "0"으로 채웠습니다. 도트 값을 비트OR 연산하는데 원래의 십진 값이 바뀌면 안되니까요!

상위 4비트가 도트에 해당하는 값인데, 비트값이 "1"이면 출력 범위의 해당 도트가 on되고, "0"이면 off됩니다. 총 비트가 4개이니까 모듈에 있는 4개의 도트를 제어할 수 있습니다. 그런데, 여기서 중요한 것은 출력 범위입니다.

만약, 4자리 모두 출력 범위가 된다면,

  • 상위 4비트가 "1000"이면 첫 번째 도트만 on되고 나머지는 off입니다.
  • 비트가 "1100" 값이면 첫째 둘째 도트가 on됩니다.
  • 비트가 "0001" 이라면, 마지막 도트만 on되고, "1111"이라면 모든 도트가 on됩니다.
  • 콜론을 켜고 싶다면, 두번째 도트가 on되도록 "0100"으로 해야 합니다.

이진수 "1000"은 16진수 "8"이므로, 네 자리 출력데이터의 첫 번째 도트만 켜고 싶다면 두 번째 인수는 0x80으로 제공해야 합니다. "1100"일 경우 "0xC0", "0001"은 "0x10", "1111"은 "0xF0", 마지막으로 "0100"은 "0x40"으로 제공합니다.

4자리 모두 출력할 때, 콜론에 해당하는 두 번째 도트를 켜고 싶다면, 왼쪽에서 두 번째 비트가 "1"이 되어야 합니다.

  • "0100", "0101", "0110", "0111", "1100", "1101", "1110", "1111" 이 경우가 모두 해당되고,
  • 0x40, 0x50, 0x60, 0x70, 0xC0, 0xD0, 0xE0, 0xF0 이 모두 인수로 가능합니다.

그러나, 여기서 사용하는 모듈은 콜론 외에는 사용이 불가능하므로, 복잡한 것 빼고 "0x40" 하나만 기억하면 될 듯합니다.

출력 범위가 4보다 작다면 출력될 위치(인수 pos)에 따라 달라집니다.

두 번째 인수 dots는 모듈내 도트의 물리적 위치가 아니라, 매 번 출력하는 데이터의 각 자리별 도트를 제어합니다. 네 자리 출력할 때는 상관없는데, 출력 범위가 4보다 작은 경우에는 달라집니다.

위 그림은 인수 length를 "3"으로 주고, 인수 pos를 위쪽은 "0", 아래쪽은 "1"로 주었을 때의 출력 범위입니다.

우선, 첫 번째의 경우, 출력 데이터 "5"는 모듈의 물리적 위치로는 오른쪽에서 두 번째이지만, 출력 범위에서는 오른쪽 끝자리입니다. 따라서, 이 자리의 도트를 켜려면, 동일하게 오른쪽 끝자리 비트만 "1"이 되는 "0001"이 비트값에 해당합니다. 마찬가지로, 출력 데이터 "4"는 "0010", "3"은 "0100"이고, "0111"은 세 자리 모두 도트가 켜지게 됩니다. 출력 범위의 오른쪽부터 비트를 매기면 되는데, 만약, 콜론을 켜고 싶다면 출력 범위 3자리중 두 번째 도트(출력 숫자 4)에 해당하므로, "0010" 즉, "0x20"이 인수값이 됩니다.

두 번째 예의 경우, 콜론은 출력할 데이터 "456"의 오른쪽에서 3번째에 해당합니다. 따라서 인수는 "0100", "0x40"입니다.

출력 범위가 두 자리일 경우입니다. 각각의 경우 아래와 같습니다.

  • 첫 번째 그림은 length = 2, pos = 0 이고, 콜론이 출력 범위의 오른쪽 끝(출력 숫자 4)이므로 "0001" 즉 "0x10"이 인수가 됩니다.
  • 두 번째 그림은, pos = 1 이고, 출력 범위의 오른쪽에서 두 번째가 콜론의 위치이므로 "0010" 즉 "0x20"이 인수값입니다.
  • 마지막 그림은 콜론이 출력 범위 밖이므로 제어할 수 없습니다.

출력 범위가 한 자리일 경우에는, length = 1, pos = 1, dots = 0x10 일때만 콜론이 출력되고, 콜론은 끄고 싶을 땐 "1"을 "0"으로 바꿔서 계산하면 되겠지요!

#include <TM1637Display.h>
// Module connection pins (Digital Pins)
#define CLK 2
#define DIO 3
// 오브젝트 생성하고 초기화
TM1637Display dsp(CLK, DIO);
//
unsigned long cTime;
unsigned long rTime = 0;
unsigned long bTime = 0;
int second = 0;
int minute = 0;
int timeData = 0;
bool blinkToggle = false;
//
void setup() {
  dsp.setBrightness(7);
}
//
void loop() {
  dsp.showNumberDecEx(1, 0);
  delay(1000);
  dsp.showNumberDecEx(1111, 0x40);
  delay(1000);
  dsp.showNumberDecEx(1111, 0);
  delay(1000);
  dsp.showNumberDecEx(222, 0x20, false, 3, 0);
  delay(1000);
  dsp.showNumberDecEx(222, 0, false, 3, 0);
  delay(1000);
  dsp.showNumberDecEx(333, 0x40, false, 3, 1);
  delay(1000);
  dsp.showNumberDecEx(333, 0, false, 3, 1);
  delay(1000);
  dsp.showNumberDecEx(44, 0x10, false, 2, 0);
  delay(1000);
  dsp.showNumberDecEx(44, 0, false, 2, 0);
  delay(1000);
  dsp.showNumberDecEx(55, 0x20, false, 2, 1);
  delay(1000);
  dsp.showNumberDecEx(55, 0, false, 2, 1);
  delay(1000);
  dsp.showNumberDecEx(6, 0x10, false, 1, 1);
  delay(1000);
  dsp.showNumberDecEx(6, 0, false, 1, 1);
  delay(1000);
}

위 예제는 인수를 여러 가지로 변경해보며 콜론을 on / off 하는 것입니다.

#include <TM1637Display.h>
// Module connection pins (Digital Pins)
#define CLK 2
#define DIO 3
// 오브젝트 생성하고 초기화
TM1637Display dsp(CLK, DIO);
//
unsigned long cTime;
unsigned long rTime = 0;
unsigned long bTime = 0;
int second = 0;
int minute = 0;
int timeData = 0;
bool blinkToggle = false;
//
void setup() {
  dsp.setBrightness(7);
  dsp.showNumberDecEx(0, 0x40, true);
}
//
void loop() {
  cTime = millis();
  //
  if (cTime - rTime > 999) {
    second++;
    //
    if (second > 59) {
      second = 0;
      minute++;
      if (minute > 59) {
        minute = 0;
      }
    }
    timeData = minute * 100 + second;
    rTime = cTime;
  }
  // 0.5초마다 콜론이 깜박이도록...
  if (cTime - bTime > 499) {
    if (blinkToggle) {
      dsp.showNumberDecEx(timeData, 0x40, true);
      blinkToggle = false;
    } else {
      dsp.showNumberDecEx(timeData, 0, true);
      blinkToggle = true;
    }
    bTime = cTime;
  }
}

위 예제는 분과 초를 표시하는 간단한 시계를 구현한 것으로, 1초마다 콜론이 깜박이도록 하였습니다. 아래 동영상을 통해 결과를 확인할 수 있습니다.

Raw 데이터로 제어하는 setSegments 함수

showNumberDec, showNumberDecEx 함수는 우리가 다루기 쉬운 십진수를 출력 데이터로 제공하여 출력합니다. 이와는 다르게, setSegments 함수는 숫자와 도트를 구성하는 각각의 LED를 직접 제어하여 원하는 모양을 만드는 함수이고, 이런 데이터를 RAW 데이터라고 합니다.

  • setSegments(const uint8_t segments[], uint8_t length, uint8_t pos)
  • 첫 번째 인수는 출력할 Raw 데이터이며, 출력할 글자수 만큼의 데이터를 배열로 제공합니다.
  • 나머지 인수는 이전의 함수와 동일합니다. 특히, length는 데이터 배열의 크기가 됩니다.
  • 도트(콜론)를 위한 인수는 필요 없습니다. 출력 데이터에 이미 들어있기 때문입니다.
  • leading-zero에 대한 인수도 없습니다. 자동으로 채우는 방식이 아니고, 모든 자리의 데이터를 모두 제공해야 합니다.
켜고 싶은 LED는 해당 비트를 "1"로 세팅합니다.

한 자리에 대한 출력 데이터는 숫자와 도트를 합쳐서 8개의 LED에 대한 8비트 즉, 1바이트 데이터로 구성됩니다. 이 1 바이트 데이터에 "1"이 있다면 해당 비트의 LED는 "on"이 되고, "0"이라면 반대가 되겠죠.

byte data[] = { 0, 0, 0, 0 };
byte data[] = { 0x00, 0x00, 0x00, 0x00 };
byte data[] = { B00000000, B00000000, B00000000, B00000000 };

출력 데이터를 위한 배열을 선언하고 위와 같이 초기화할 때, 위 코드는 모두 동일합니다. 결국 모든 비트가 "0"입니다.

void loop() {
  byte data[] = { B00000000, B00000000, B00000000, B00000000 };
  dsp.setSegments(data);
}

위 코드의 setSegments() 함수는 다른 인수가 모두 생략되었기 때문에, 디폴트값으로 4자리를 모두 출력합니다. 그리고, 출력 데이터의 모든 비트가 "0"이므로 모든 LED가 꺼지게 됩니다.

void loop() {
  byte data[] = { B00000001, B00000000, B00000000, B00000000 };
  dsp.setSegments(data);
}

위 코드는 첫 번째 데이터만 변경되었습니다. 마지막 비트, 오른쪽 첫 번째 비트만 "1"이므로 모든 LED는 꺼지고, 첫 번째 자리의 가장 위쪽 LED만 들어옵니다. 가장 오른쪽 비트가 바로 a 세그먼트에 해당함을 알수 있습니다.

void loop() {
  byte data[] = { B00000000, B00000001, B00000000, B00000000 };
  dsp.setSegments(data);
  delay(1000);
  data[1] = B00000010;
  dsp.setSegments(data);
  delay(1000);
  data[1] = B00000100;
  dsp.setSegments(data);
  delay(1000);
  data[1] = B00001000;
  dsp.setSegments(data);
  delay(1000);
  data[1] = B00010000;
  dsp.setSegments(data);
  delay(1000);
  data[1] = B00100000;
  dsp.setSegments(data);
  delay(1000);
  data[1] = B01000000;
  dsp.setSegments(data);
  delay(1000);
  data[1] = B10000000;
  dsp.setSegments(data);
  delay(1000);
}

도트는 가장 왼쪽 비트에 해당합니다. 이를 확인하기 위해서 콜론이 있는 두 번째 자리를 차례로 변경하며 출력하는 코드입니다. a 세그먼트부터 g 세그먼트까지 번갈아 켜고 마지막으로 콜론을 켠 후 다시 처음부터 반복합니다.

void loop() {
  byte data[] = { B00000000, B00000001, B00000000, B00000000 };
  for (int i = 0; i < 8; i++) {
    dsp.setSegments(data);
    data[1] = data[1] << 1;
    delay(1000);
  }
}

바이트형 데이터를 제공하기에 비트 연산을 통하여 위와 같이 코드를 간단하게 줄일수 있습니다.

void loop() {
  byte data[] = { B00000001, B00000001, B00000001, B00000001 };
  for (int i = 0; i < 6; i++) {
    dsp.setSegments(data);
    data[0] = data[0] << 1;
    data[1] = data[1] << 1;
    data[2] = data[2] << 1;
    data[3] = data[3] << 1;
    delay(10);
  }
}

각 자리마다 원을 그리며 빠르게 돌아가는 모양을 동일하게 출력합니다.

#include <TM1637Display.h>
// Module connection pins (Digital Pins)
#define CLK 2
#define DIO 3
// 오브젝트 생성하고 초기화
TM1637Display dsp(CLK, DIO);
//
byte data[][4] = {
    { 0x01, 0x00, 0x00, 0x08 },
    { 0x00, 0x01, 0x08, 0x00 },
    { 0x00, 0x08, 0x01, 0x00 },
    { 0x08, 0x00, 0x00, 0x01 },
    { 0x10, 0x00, 0x00, 0x02 },
    { 0x20, 0x00, 0x00, 0x04 }
};
//
void setup() {
  dsp.setBrightness(7);
}
//
void loop() {
  for (int i = 0; i < 6; i++) {
    dsp.setSegments(data[i]);
    delay(20);
  }
}

위 코드는 전체적으로 원을 그리며 돌아가는 표현입니다.

#define SEG_A   0b00000001
#define SEG_B   0b00000010
#define SEG_C   0b00000100
#define SEG_D   0b00001000
#define SEG_E   0b00010000
#define SEG_F   0b00100000
#define SEG_G   0b01000000

라이브러리 헤더 파일 tm1637Display.h 파일을 열어 보면 위와 같이 각각의 세그먼트에 대해 미리 키워드를 만들어 놓았습니다.

const uint8_t SEG_DONE[] = {
	SEG_B | SEG_C | SEG_D | SEG_E | SEG_G,           // d
	SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F,   // O
	SEG_C | SEG_E | SEG_G,                           // n
	SEG_A | SEG_D | SEG_E | SEG_F | SEG_G            // E
	};

위 코드는 라이브러리 기본 예제에 있는 것으로, 미리 정의된 키워드를 가지고 필요한 데이터를 미리 만들어 놓고 사용하는 예입니다.

숫자를 세그먼트 코드값으로 변환하는 encodeDigit 함수

showNumberDec(), showNumberDecEx() 함수는 출력할 숫자를 인수로 제공하지만, setSegments() 함수는 출력할 숫자의 코드값을 제공해야 합니다. "0 ~ 15"까지의 숫자를 인수로 주면 "0 ~ 9, 그리고 A, b, C, d, E, F 이렇게 16진수에 대한 코드값을 반환합니다.

void loop() {
  byte data[4] = { 0, 0, 0, 0 };
  for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 16; j++) {
      data[i] = dsp.encodeDigit(j);
      dsp.setSegments(data);
      delay(100);
    }
  }
}

왼쪽 첫 번째 자리부터 16진수를 하나씩 출력하는 코드입니다. 간단한 함수라 언급할 내용도 없네요!

encodeDigit() 함수와 setSegments() 함수를 이용한 시계(분, 초) 표현
#include <TM1637Display.h>
// Module connection pins (Digital Pins)
#define CLK 2
#define DIO 3
// 오브젝트 생성하고 초기화
TM1637Display dsp(CLK, DIO);
//
unsigned long cTime = 0; // 현재 시간 저장
unsigned long pTime = 0; // 1초마다 시간 증가
unsigned long bTime = 0; // 콜론의 blink 간격
bool bFlag = false; // 콜론의 blink 상태 체크
byte data[4] = { 0, 0, 0, 0 };
int second = 0;
int minute = 0;
//
void setup() {
  dsp.setBrightness(7);
}
//
void loop() {
  cTime = millis();
  if (cTime - pTime >= 1000) {
    second++;
    if (second == 60) {
      second = 0;
      minute++;
      if (minute == 60) minute = 0;
    }
    data[3] = dsp.encodeDigit(second - second / 10 * 10);
    data[2] = dsp.encodeDigit(second / 10);
    data[1] = dsp.encodeDigit(minute - minute / 10 * 10);
    data[0] = dsp.encodeDigit(minute / 10);
    pTime = cTime;
  }
  //
  if (cTime - bTime >= 500) {
    if (bFlag) {
      data[1] = data[1] | 0x80; // 콜론 비트 활성
      dsp.setSegments(data);
      bFlag = false;
    } else {
      data[1] = data[1] & 0x7f; // 콜론 비트 비활성
      dsp.setSegments(data);
      bFlag = true;
    }
    bTime = cTime;
  }
}

시간을 위한 정수 계산과 코드 변환, 또 출력할 코드 보관까지 해야 해서, showNumberDec() 함수보다 더 복잡해졌습니다. LED를 직접 제어해야 할 경우에는 setSegments() 함수가 좋겠지만, 시계 표현처럼 십진 계산과 더불어 출력해야 할 경우에는 showNumberDec() 함수가 훨씬 좋을 듯 합니다.

이상으로, TM1637 칩을 사용하는 7 세그먼트 LED 모듈에 대한 연재를 마치겠습니다.