로터리 엔코더로 세그먼트 LED 제어하기

2019. 3. 20. 23:00

Arduino/Sensor



로터리 엔코더를 이용하여 시간 설정하는 인터페이스 만들기

만약 아두이노로 시계를 만든다면, 현재 시간이나 알람 등을 위해 시간 값을 조절하는 인터페이스를 구현해야 합니다. 현재 제 블로그에서 연재중인 Turtle Clock BigFont의 경우 NTP 프로토콜을 이용하여 자동으로 현재 시간을 설정하고 또 알람 기능도 없어서 구현하지 않았지만, 대부분의 시계들은 하나 이상의 버튼 등을 이용하여 이런 기능을 제공하고 있습니다. 이번 연재에서는 로터리 엔코더 하나를 이용하여 비슷한 기능을 만들어 보겠습니다.

코멘트로 요청하신 분이 있어 작성했는데, 다른 일들로 포스팅이 많이 늦었네요!



아두이노에 LED 세그먼트 연결하기





여기서 사용할 보드는 아두이노 Uno이고, 시간 표시를 위한 디스플레이 장치로 LED 세그먼트를 사용하겠습니다. 이 LED 세그먼트는 다른 포스트에서 몇 번 사용했던 모듈로 TM1637 드라이버를 이용하는 제품입니다. LED 모듈 및 라이브러리에 대한 정보는 아래 관련 글들을 참고시면 됩니다. 여기서는 해당 라이브러리가 이미 설치된 환경에서 진행하겠습니다.









위와 같이 LED 모듈의 CLK, DIO 핀은 차례로 아두이노의 D2, D3 핀에 연결하고 5V, Gnd 핀도 아두이노 전원부에 맞게 연결합니다.





7 세그먼트 LED에 시간 출력하기


아두이노로 시간을 계산하고 관리하기 위해서는 별도의 RTC(Real Time Clcok) 모듈을 이용하는 것이 일반적이지만, 여기서는 간단한 테스트가 목적이므로 아두이노 보드의 내장 clock을 이용하겠습니다. 우선 예제에 사용할 시간 데이터를 구성하겠습니다.


int second = 0;
int minute = 30;
int hour = 5;
unsigned long prevTime = 0;


먼저, 시, 분, 초 시간 데이터를 저장할 변수 second, minute, hour를 선언하고 테스트를 위해 5시 30분으로 초기화했습니다. 또 loop() 함수 내에서 1초마다 체크하기 위한 변수 prevTime도 필요합니다.


void setup() {
  prevTime = millis();
}


loop() 함수가 시작되면 바로 시간 비교를 해야하기 때문에 setup() 함수 내에서 millis() 함수를 이용하여 현재 아두이노 클럭 시간을 저장합니다.


void loop() {
  if ( (millis() - prevTime) > 999 ) {
    second++; // 1초 증가
    //
    if ( second > 59 ) {
      second = 0;
      minute++;   // 1분 증가
      //
      if ( minute > 59 ) {
        minute = 0;
        hour++;  // 1시간 증가
        //
        if ( hour > 23 ) {
          hour = 0;
        }
      }
    }
    prevTime = millis();
  }
}


위와 같이 간단한 시간 계산 및 값 저장 루틴을 작성하였습니다. 이제 이렇게 저장된 시와 분 값을 1초마다 LED에 출력하겠습니다.


#include <TM1637Display.h>
//
// Module connection pins (Digital Pins)
#define CLK 2
#define DIO 3
//
TM1637Display display(CLK, DIO);


세그먼트 LED를 사용하기 위해 우선 해당 라이브러리를 추가하고 아두이노와 연결된대로 핀 설정을 한 후, display 라는 이름으로 인스턴스를 하나 생성하였습니다.


void setup() {
  prevTime = millis();
  //
  display.setBrightness(15);
}


세그먼트 LED에 데이터를 출력하기 전에 미리 밝기를 설정하지 않으면 화면에 아무것도 표시되지 않습니다. setup() 함수 내에서 위와 같이 밝기를 설정해 줍니다.


void loop() {
  if ( (millis() - prevTime) > 999 ) {
    .
    .
    prevTime = millis();
    //
    dspDigit();
  }
}
//
void dspDigit() {
  display.showNumberDecEx(hour * 100 + minute, 0x40, true);
}


마지막으로 loop() 함수에 위와 같이 몇 가지 코드를 추가하면 됩니다. 데이터를 LED에 출력하는 dspDigit() 함수를 만들고, showNumberDecEx() 함수를 이용하여 출력합니다. 첫 번째 인수로 시와 분 값을 10진수 네 자리로 만들어서 전달하고, 두 번째 인수 0x40은 가운데 콜론(colon)을 표시하기 위함이며, 마지막 인수 true는 선행 제로 즉 빈자리도 숫자 "0"으로 채운다는 의미입니다. 자세한 함수 사용법은 관련 글을 참고하세요.





결과 화면입니다. 1분 동안 기다린 후 제대로 카운팅 되는 것을 확인하였습니다. 여기에 좀더 시계 답게 보이기 위해 가운데 콜론이 깜박이도록 코드를 추가하겠습니다.


int second = 0;
int minute = 30;
int hour = 5;
unsigned long prevTime = 0;
bool colon; // colon blinking check


void loop() {
  if ( (millis() - prevTime) > 999 ) {
    .
    .
    .
    dspDigit();
    colon = true;
  }
  // colon blink
  if ( millis() - prevTime > 499 ) {
    if ( colon ) {
      display.showNumberDecEx(hour * 100 + minute, 0, true);
      colon = false;
    }
  }
}


가운데 콜론이 켜져있는지 여부를 체크하기 위한 변수 colon을 추가하고 1초마다 콜론을 켜고 1/2초마다 끄는 코드입니다. 출력 함수의 두번째 인수가 "0"이면 콜론을 출력하지 않습니다. 결과적으로 1/2초마다 깜박입니다.






결과 화면입니다. 실행시킨 후 2시간여 흐른 뒤의 모습입니다.





로터리 엔코더로 시간 조정하기





이제 이 시계의 시간 설정을 위한 사용자 인터페이스를 만들어보겠습니다. 로터리 엔코더의 회전 입력과 푸시 스위치를 이용하여 구현합니다.





이 글에서 사용할 로터리 엔코더 모듈입니다. 터틀 테이블 프로젝트에서 사용한 제품으로 풀업(pull-up) 레지스터와 입출력 포트를 추가하여 아두이노에 붙여 사용하기에 좀더 쉽게 만들어져 있습니다. 회전 데이터를 출력하는 CLK, DT 포트와 가운데 노브(knob)로 작동하는 푸시 스위치 데이터를 출력하는 SW 포트가 있으며 차례로 아두이노의 D4, D5, D6 포트와 연결하였습니다. 나머지 두 핀은 전원부로 역시 아두이노 전원 핀(Vin, Gnd)에 맞게 연결합니다.



// Module connection pins (Digital Pins)
#define CLK 2
#define DIO 3
//
// Encoder connection pins (Digital Pins)
#define outA 4 // CLK
#define outB 5   // DT
#define sw 6   // SWITCH


// rotary encoder
unsigned long debounce = 0; // 디바운싱
int prevA;                  // CLK의 이전 입력 값, 변동 체크를 위해...
int prevB;                  // DT의 이전 입력 값, 변동 체크를 위해...
int prevSw;                 // SW의 이전 입력 값......
int cwCount = 0;            // 시계 방향 단계 체크
int ccwCount = 0;           // 반시계 방향 단계 체크


void setup() {
  pinMode(sw, INPUT_PULLUP);
  prevA = digitalRead(outA);
  prevB = digitalRead(outB);
  prevTime = millis();
  //
  display.setBrightness(15);
}





세그먼트 LED와 마찬가지로 우선 핀 설정 먼저 하고, 필요한 변수들을 선언하였으며 setup() 함수 내에서 필요한 초기화를 진행하였습니다. 위 사진을 보시면 기판 아래쪽에 레지스터가 2개 연결되어 있습니다. 엔코더를 풀업하기 위한 것이고, 스위치를 위한 레지스터가 없기 때문에 별도로 내부 풀업 레지스터를 이용하였습니다.


void loop() {
  if ( millis() - debounce > 1 ) {
    int currentA = digitalRead(outA);
    int currentB = digitalRead(outB);
    //
    if ( (currentA != prevA) || (currentB != prevB) ) {
      .
      .
      .
      prevA = currentA;
      prevB = currentB;
    }
    //
    debounce = millis();
  }
  //
  if ( (millis() - prevTime) > 999 ) {
    .
    .
    prevTime = millis();
    //
    dspDigit();
    colon = true;
  }
  // colon blink
  .
  .
}


엔코더 입력 처리는 위와 같이 loop() 함수의 시간 데이터 처리 부분 위쪽에 작성하였습니다. 엔코더 손잡이를 돌리는 동안 발생하는 잘못된 입력을 방지하기 위해 디바운싱이 필요하며 엔코더 관련 코드를 전체적으로 둘러싸고 있습니다. 루프를 도는 동안 CLK, DT의 값에 변동이 있다면 IF문 안쪽에서 처리해 줍니다.





CLK DT cwCount
1 1 0
1 0 1
0 0 2
0 1 3
1 1 0
Clock Wise



CLK DT ccwCount
1 1 0
0 1 1
0 0 2
1 0 3
1 1 0
Counter Clock Wise



엔코더의 입력 신호는 위와 같습니다. 시계 방향이든 반대 방향이든 "1, 1"로 시작해서 다시 "1,1"이 입력되는 동안 위 표에 있는 순서대로 신호가 들어와야 한 번의 딸깍하는 움직임이 완성됩니다. 이 순서를 체크하기 위해 cwCount, ccwCount 변수를 사용하며 위 입력 값은 사용하는 제품에따라 달라질 수 있습니다.


    if ( (currentA != prevA) || (currentB != prevB) ) {
      if ( currentA == 0 ) {
        if ( currentB == 0 ) {
          // 입력이 0, 0 일때의 처리
          if ( (ccwCount == 1) ) {
            ccwCount = 2;
          } else if ( cwCount == 1 ) {
            cwCount = 2;
          }
        } else {
          // 입력이 0, 1 일때의 처리
          if ( (ccwCount == 0) && (cwCount == 0) ) {
            ccwCount = 1;
          } else if ( cwCount == 2 ) {
            cwCount = 3;
          }
        }
      } else {
        if ( currentB == 0 ) {
          // 입력이 1, 0 일때의 처리
          if ( ccwCount == 2 ) {
            ccwCount = 3;
          } else if ( (ccwCount == 0) && (cwCount == 0) ) {
            cwCount = 1;
          }
        } else {
          // 입력이 1, 1 일때의 처리
          if ( cwCount == 3 ) {
            minute++;
            if ( minute > 59 ) {
              minute = 0;
            }
            cwCount = 0;
          } else if ( ccwCount == 3 ) {
            minute--;
            if ( minute < 0 ) {
              minute = 59;
            }
            ccwCount = 0;
          }
          //
          dspDigit();
        }
      }


IF문 안쪽을 위와 같이 완성하였습니다. 01, 00, 10, 11 각각의 경우에 시계 방향일때, 반시계 방향일때를 모두 조건문으로 처리하였습니다. 우선 테스트를 위해 분(minute) 값만 변경시키는 코드이며 마지막 부분의 입력이 모두 1,1 일때 cwCount, ccwCount 변수가 3이라면 각 방향으로 한 번의 딸깍임이 발생한 것이므로 1분 증가 또는 1분 감소로 처리합니다. 결과 화면은 아래 이미지를 참고하세요!







로터리 엔코더의 스위치 이용하기


바로 위 소스는 테스트를 위해 분 단위 값만 변경시켰고, 이제 로터리 엔코더의 푸시 스위치를 이용하여 시와 분을 모두 수정할 수 있도록 코딩하겠습니다. 버튼을 누를 때마다 시간 표시, 시(hour) 값 변경, 분(minute) 값 변경 상태가 로테이션 됩니다.


// rotary encoder
unsigned long debounce = 0; // 디바운싱
int prevA;                  // CLK의 이전 입력 값, 변동 체크를 위해...
int prevB;                  // DT의 이전 입력 값, 변동 체크를 위해...
int prevSw;                 // SW의 이전 입력 값......
int cwCount = 0;            // 시계 방향 단계 체크
int ccwCount = 0;           // 반시계 방향 단계 체크
int mode = 0;               // 일반, 시 변경, 분 변경 상태 체크


엔코더 관련해서 mode라는 변수를 하나 선언하였습니다. 이 변수의 값이 0 이면 그냥 시간만 표시하고 엔코더의 회전에 반응하지 않습니다. 값이 1 이면 엔코더를 돌렸을 때 시(hour) 값이 변하고, 변수 값이 2 이면 분(minute) 값이 변합니다. 그리고 엔코더의 푸시 스위치를 누를 때마다 이 변수의 값이 0, 1, 2 순서대로 변경되도록 코딩합니다.


void setup() {
  pinMode(sw, INPUT_PULLUP);
  prevA = digitalRead(outA);
  prevB = digitalRead(outB);
  prevSw = digitalRead(sw);
  prevTime = millis();
  .
  .
}


setup() 함수 내에 비교를 위해 prevSw 변수를 현재 값으로 초기화합니다. prevA, prevB 변수와 마찬가지입니다.



    if ( (currentA != prevA) || (currentB != prevB) ) {
      if ( currentA == 0 ) {
        if ( currentB == 0 ) {
          // 입력이 0, 0 일때의 처리
          .
        } else {
          // 입력이 0, 1 일때의 처리
          .
        }
      } else {
        if ( currentB == 0 ) {
          // 입력이 1, 0 일때의 처리
          .
        } else {
          // 입력이 1, 1 일때의 처리
          if ( cwCount == 3 ) {
            if ( mode == 1 ) {	// hour increase
              hour++;
              if ( hour > 23 ) {
                hour = 0;
              }
            } else if ( mode == 2 ) {	// minute inc
              minute++;
              if ( minute > 59 ) {
                minute = 0;
              }
            }
            cwCount = 0;
          } else if ( ccwCount == 3 ) {
            if ( mode == 1 ) {	// houre dec
              hour--;
              if ( hour < 0 ) {
                hour = 23;
              }
            } else if ( mode == 2 ) {	// minute dec
              minute--;
              if ( minute < 0 ) {
                minute = 59;
              }
            }
            ccwCount = 0;
          }
          //
          dspDigit();
        }
      }
      prevA = currentA;
      prevB = currentB;
    }


엔코더의 입력을 처리하는 부분은 위와 같이 수정하였습니다. 변수 mode의 값에 따라 시 또는 분을 변경합니다.



  if ( millis() - debounce > 1 ) {
    int currentA = digitalRead(outA);
    int currentB = digitalRead(outB);
    //
    if ( (currentA != prevA) || (currentB != prevB) ) {
      .
      .
      prevA = currentA;
      prevB = currentB;
    }
    //
    int currentSw = digitalRead(sw);
    if ( currentSw != prevSw ) {
      if ( currentSw == 0 ) {
        mode++;
        if ( mode > 2 ) {
          mode = 0;
        }
      }
      prevSw = currentSw;
    }
    //
    debounce = millis();
  }


마지막으로, 엔코더의 회전 입력 처리 아래쪽에 스위치 관련 코드를 추가하였습니다. 버튼을 누를 때마다 변수 mode 의 값을 변경하는 역할만 합니다. 여기 까지 수정한 후 실행하면 의도했던 대로 실행됨을 확인할 수 있습니다. 처음 실행되고 푸시 버튼을 누르지 않으면 엔코더의 회전 입력을 처리하지 않은 채 시간만 표시하고, 버튼을 한 번 누르면 시(hour) 값 변경, 다시 또 누르면 분(minute) 값이 변경되며, 여기서 한 번 더 누르면 원래대로 돌아와 시간만 표시합니다.


그런데 문제가 하나 있습니다. 시와 분 어느 쪽이 수정 가능 상태인지 돌려보기 전까진 알수 없다는 것인데, 이를 해결하기 위해 수정하지 않는 쪽은 세그먼트 LED에 표시되지 않도록 처리하겠습니다.


//
byte data[] = { 0, 0, 0, 0};
//


시(hour) 변경 모드에선 분(minute) 값이 보이지 않고, 분 변경 모드에선 시 값이 보이지 않도록 하기 위해서 화면을 모두 지워주는 기능이 필요합니다. 이를 처리하기 위해 setSegments() 함수를 이용하며 위 data 변수를 인수로 제공하면 각 자리의 모든 LED 세그먼트들을 off 시킵니다.



int currentSw = digitalRead(sw);
    if ( currentSw != prevSw ) {
      if ( currentSw == 0 ) {
        mode++;
        if ( mode > 2 ) {
          mode = 0;
        }
        display.setSegments(data);
      }
      prevSw = currentSw;
    }


위와 같이 스위치에 의해 모드가 변경될 때마다 화면을 한 번씩 지워줍니다. 결과 동영상을 보면 어떤 의미인지 확인할 수 있습니다.



  // colon blink
  if ( (millis() - prevTime > 499) && (mode == 0) ) {
    if ( colon ) {
      display.showNumberDecEx(hour * 100 + minute, 0, true);
      colon = false;
    }
  }


가운데 콜론을 깜박이도록 하는 부분도 약간의 수정이 필요합니다. 콜론의 깜박임은 수정 모드(hour, minute)에서는 처리하지 않도록 조건문을 변경하였습니다. 



void dspDigit() {
  if ( mode == 0 ) {
    display.showNumberDecEx(hour * 100 + minute, 0x40, true);
  } else if ( mode == 1 ) {
    display.showNumberDecEx(hour, 0x10, true, 2, 0);
  } else {
    display.showNumberDecEx(minute, 0x40, false, 3, 1);
  }
}


각 모드에 맞게 출력하도록 dspDigit() 함수의 내용도 변경하였습니다. 여기까지 하면 모든 코드가 완성됩니다. 전체 소스와 작동 영상은 아래를 참고하세요. 이번 글은 여기서 마치겠습니다.



#include <TM1637Display.h>
//
// Module connection pins (Digital Pins)
#define CLK 2
#define DIO 3
//
// Encoder connection pins (Digital Pins)
#define outA 4 // CLK
#define outB 5   // DT
#define sw 6   // SWITCH
//
TM1637Display display(CLK, DIO);
//
int second = 0;
int minute = 30;
int hour = 5;
unsigned long prevTime = 0;
bool colon; // colon blinking check
//
// rotary encoder
unsigned long debounce = 0; // 디바운싱
int prevA;                  // CLK의 이전 입력 값, 변동 체크를 위해...
int prevB;                  // DT의 이전 입력 값, 변동 체크를 위해...
int prevSw;                 // SW의 이전 입력 값......
int cwCount = 0;            // 시계 방향 단계 체크
int ccwCount = 0;           // 반시계 방향 단계 체크
int mode = 0;               // 일반, 시 변경, 분 변경 상태 체크
//
byte data[] = { 0, 0, 0, 0};
//
void setup() {
  pinMode(sw, INPUT_PULLUP);
  prevA = digitalRead(outA);
  prevB = digitalRead(outB);
  prevSw = digitalRead(sw);
  prevTime = millis();
  //
  display.setBrightness(15);
}
//
void loop() {
  if ( millis() - debounce > 1 ) {
    int currentA = digitalRead(outA);
    int currentB = digitalRead(outB);
    //
    if ( (currentA != prevA) || (currentB != prevB) ) {
      if ( currentA == 0 ) {
        if ( currentB == 0 ) {
          // 입력이 0, 0 일때의 처리
          if ( (ccwCount == 1) ) {
            ccwCount = 2;
          } else if ( cwCount == 1 ) {
            cwCount = 2;
          }
        } else {
          // 입력이 0, 1 일때의 처리
          if ( (ccwCount == 0) && (cwCount == 0) ) {
            ccwCount = 1;
          } else if ( cwCount == 2 ) {
            cwCount = 3;
          }
        }
      } else {
        if ( currentB == 0 ) {
          // 입력이 1, 0 일때의 처리
          if ( ccwCount == 2 ) {
            ccwCount = 3;
          } else if ( (ccwCount == 0) && (cwCount == 0) ) {
            cwCount = 1;
          }
        } else {
          // 입력이 1, 1 일때의 처리
          if ( cwCount == 3 ) {
            if ( mode == 1 ) {
              hour++;
              if ( hour > 23 ) {
                hour = 0;
              }
            } else if ( mode == 2 ) {
              minute++;
              if ( minute > 59 ) {
                minute = 0;
              }
            }
            cwCount = 0;
          } else if ( ccwCount == 3 ) {
            if ( mode == 1 ) {
              hour--;
              if ( hour < 0 ) {
                hour = 23;
              }
            } else if ( mode == 2 ) {
              minute--;
              if ( minute < 0 ) {
                minute = 59;
              }
            }
            ccwCount = 0;
          }
          //
          dspDigit();
        }
      }
      prevA = currentA;
      prevB = currentB;
    }
    //
    int currentSw = digitalRead(sw);
    if ( currentSw != prevSw ) {
      if ( currentSw == 0 ) {
        mode++;
        if ( mode > 2 ) {
          mode = 0;
        }
        display.setSegments(data);
      }
      prevSw = currentSw;
    }
    //
    debounce = millis();
  }
  //
  if ( (millis() - prevTime) > 999 ) {
    second++; // 1초 증가
    //
    if ( second > 59 ) {
      second = 0;
      minute++;   // 1분 증가
      //
      if ( minute > 59 ) {
        minute = 0;
        hour++;  // 1시간 증가
        //
        if ( hour > 23 ) {
          hour = 0; 
        }
      }
    }
    prevTime = millis();
    //
    dspDigit();
    colon = true;
  }
  // colon blink
  if ( (millis() - prevTime > 499) && (mode == 0) ) {
    if ( colon ) {
      display.showNumberDecEx(hour * 100 + minute, 0, true);
      colon = false;
    }
  }
}
void dspDigit() {
  if ( mode == 0 ) {
    display.showNumberDecEx(hour * 100 + minute, 0x40, true);
  } else if ( mode == 1 ) {
    display.showNumberDecEx(hour, 0x10, true, 2, 0);
  } else {
    display.showNumberDecEx(minute, 0x40, false, 3, 1);
  }
}




Comments