WIFI로 제어하는 탁상시계 만들기 #7 Rotary Encoder로 LED 램프 제어

2019. 2. 15. 15:59

Project/Turtle Clock

Rotary Encoder로 LED Lamp 제어하기

Turtle Clock BigFont에 사용할 부품 중 마지막으로 로터리 엔코더에 대해 소개합니다. 네오픽셀(RGBW)의 백색(White) LED로 간이 램프를 만들고 on/off와 밝기 조절을 이 엔코더로 제어하겠습니다.

Rotary Encoder + Extras

이번에 사용할 엔코더입니다. Adafruit에서 다른 물품 주문할 때 같이 구입했는데 knob까지 포함된 구성입니다. 전에 연재했던 터틀 테이블에서 사용한 엔코더는 풀업(pull-up) 저항 및 입출력 포트를 적용한 브레이크아웃 보드 형태였지만, 이번 엔코더는 부품만 있기 때문에 사용하기는 좀 불편합니다.

엔코더의 입출력 포트는 위와 같습니다. 사진에서 위쪽 3개의 핀이 엔코더, 아래쪽 2개 핀은 푸시 스위치와 연결되어 있습니다. NodeMCU 보드와 Output A, Output B, Switch 이렇게 3개의 디지털 단자를 통해서 연결해야 하며, 노이즈 제거를 위해 별도의 풀업(pull-up) 또는 풀다운(pull-down) 레지스터가 필요합니다.

보드에 연결하기 위해 멀쩡한 점퍼케이블을 분리해 납땜하였습니다. 또한 되도록 쉬운 구성을 위해 NodeMCU 보드 내장 풀업 레지스터를 이용하고 별도의 회로는 구성하지 않았습니다.

보드와의 연결은 위와 같습니다. 디지털 5번에서 7번까지 차례로 연결하고 그라운드도 두 핀 연결하였습니다.

아두이노로 로터리 엔코더 제어하기
#define outA D5
#define outB D6
#define sw D7
//
void setup(){
  pinMode(outA, INPUT_PULLUP);
  pinMode(outB, INPUT_PULLUP);
  pinMode(sw, INPUT_PULLUP);
  //
}

엔코더를 위한 별도의 라이브러리는 사용하지 않겠습니다. 우선 엔코더가 연결된 D5, D6, D7 포트에 대해 내부 풀업 레지스터를 활성화하며 핀 모드를 설정합니다.

스위치 부분은 일반적인 스위치 제어와 동일하기 때문에 어렵지 않지만 엔코더 부분은 좀 까다롭습니다. 가장 좋은 방법은 엔코더와 관련된 두 핀(Output A, B)을 인터럽트 방식으로 연결하는 것인데, 저번 터틀 테이블과 마찬가지로 이번 프로젝트의 엔코더도 높은 정확도를 필요로 하는 것이 아니기 때문에 인터럽트 없이 연결하였습니다.

풀업 레지스터가 적용되었기 때문에 입력 값은 반전되어 들어옵니다. 입력이 없을 때는 "1", 입력이 발생하면 "0"이고, 그래서 평상시엔 두 핀의 값이 "1, 1"입니다. 시계 방향과 반시계 방향으로 한 번 딸깍 할 때, 입력 값은 위와 같습니다. 11 상태에서 01, 00, 10이 차례로 입력된 후 다시 11로 돌아가면 시계 방향으로 한 번의 입력이 발생한 것입니다.

#define outA D5
#define outB D6
#define sw D7
//
int previousA = 1;
int previousB = 1;
//
void setup() {
  Serial.begin(115200);
  //
  pinMode(outA, INPUT_PULLUP);
  pinMode(outB, INPUT_PULLUP);
  pinMode(sw, INPUT_PULLUP);
  //
}

NodeMCU는 매우 빠르기 때문에, 엔코더를 한 칸 돌리는 순간에 같은 값이 여러 번 입력되며 이를 무시하기 위해서 이전 값에서 변동이 있을 때만 처리 해야 합니다. 그래서 이전 값을 가지고 있을 두 개의 변수(previousA, previousB)를 선언했습니다.

void loop() {
  int currentA = digitalRead(outA);
  int currentB = digitalRead(outB);
  //
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      .
      .
      .
      previousA = currentA;
      previousB = currentB;
    }
  //
}

loop() 함수는 위와 같은 구조를 가집니다. 두 핀의 현재 값을 읽고 이전 값과 비교하여 둘 중 하나라도 변동이 발생하면 처리한 후, 현재 값은 다음 번 비교를 위해 이전 값으로 저장합니다. 스위치를 처리할 때도 동일한 구조로 코딩합니다.

.
.
.
int previousA = 1;
int previousB = 1;
unsigned long debounce = 0;
.
.
void loop() {
  int currentA = digitalRead(outA);
  int currentB = digitalRead(outB);
  //
  if ( ( millis() - debounce ) > 1 ) {
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      .
      .
      .
      previousA = currentA;
      previousB = currentB;
    }
    //
    debounce = millis();
  }
}

그 다음 중요한 부분이 디바운스(debounce) 처리입니다. 푸시 스위치 등을 처리할 때 꼭 염두에 둬야하는 내용으로, 이런 스위치 종류는 보통 내부에 스프링이 있습니다. 코일 모양일 수도 있고, 판 모양일 수도 있는데 돌리거나 누르는 동작 다음에 원래 자리로 복귀할 때 스프링의 탄성 때문에 불필요한 바운스가 일어납니다. 이 때문에 한 번의 동작 후에 여러 번의 동작이 또 입력된 것과 같은 현상이 발생하고 이를 해결하는 디바운스 처리가 필요합니다. 디바운스는 위 코드와 같이 다음 번 입력까지 약간의 시간 차이를 두어 순간적으로 발생하는 오동작을 건너뛰는 코드로 처리합니다. 위 코드는 1 밀리 초가 지나지 않으면 입력을 처리하지 않습니다.

void loop() {
  int currentA = digitalRead(outA);
  int currentB = digitalRead(outB);
  //
  if ( ( millis() - debounce ) > 1 ) {
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      if ( currentA == 0 ) { // A 입력이 "0"일때
        if ( currentB == 0 ) { // B 입력이 "0"일때
          // 입력이 0,0 일때의 처리
        } else { // B 입력이 "1"일때
          //  입력이 0,1 일때의 처리
        }
      } else { // A 입력이 "1"일때
        if ( currentB == 0 ) { // B 입력이 "0"일때
          // 입력이 1,0 일때의 처리
        } else { // B 입력이 "1"일때
          // 입력이 1,1 일때의 처리
        }
      }
      previousA = currentA;
      previousB = currentB;
    }
    //
    debounce = millis();
  }
}

엔코더의 출력 핀 A, B로부터 입력되는 값은 01, 00, 10, 11 이렇게 네 가지이며 중첩된 IF문을 통해 해당하는 처리를 합니다.  대기 상태 11에서 01, 00, 10, 그리고 다시 11이 나오면 시계 방향으로 한 칸 이동이고 10, 00, 01, 11 순서라면 반 시계 방향이며 각 네 단계를 모두 거쳐야 엔코더가 한 칸 이동한 것으로 처리합니다.

#define outA D5
#define outB D6
#define sw D7
//
int previousA = 1;
int previousB = 1;
int cwCount = 0;
int ccwCount = 0;
unsigned long debounce = 0;
//
void setup() {
  Serial.begin(115200);
  //
  pinMode(outA, INPUT_PULLUP);
  pinMode(outB, INPUT_PULLUP);
  //
}
void loop() {
  int currentA = digitalRead(outA);
  int currentB = digitalRead(outB);
  //
  if ( ( millis() - debounce ) > 1 ) {
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      if ( currentA == 0 ) {    // A 입력이 "0"일때
        if ( currentB == 0 ) {  // B 입력이 "0"일때
          // 입력이 0,0 일때의 처리
          if ( ( cwCount == 1 ) ) {
            cwCount = 2;
          } else if ( ( ccwCount == 1 ) ) {
            ccwCount = 2;
          }
        } else { // B 입력이 "1"일때
          //  입력이 0,1 일때의 처리
          if ( ( cwCount == 0 ) && ( ccwCount == 0 ) ) {
            cwCount = 1;
          } else if ( ( ccwCount == 2 ) ) {
            ccwCount = 3;
          }
        }
      } else { // A 입력이 "1"일때
        if ( currentB == 0 ) {  // B 입력이 "0"일때
          // 입력이 1,0 일때의 처리
          if ( ( cwCount == 2 ) ) {
            cwCount = 3;
          } else if ( ( cwCount == 0 ) && ( ccwCount == 0 ) ) {
            ccwCount = 1;
          }
        } else { // B 입력이 "1"일때
          // 입력이 1,1 일때의 처리
          if ( ( cwCount == 3 ) ) {
            Serial.println("ClockWise");
            cwCount = 0;
          } else if ( ( ccwCount == 3 ) ) {
            Serial.println("Counter ClockWise");
            ccwCount = 0;
          }
        }
      }
      previousA = currentA;
      previousB = currentB;
    }
    //
    debounce = millis();
  }
}

엔코더의 단계별 입력을 처리하도록 코딩하였습니다. 현재 단계를 카운팅하기 위해 변수 cwCount, ccwCount 두 개를 선언하였고, 순서대로 단계를 거칠 때에만 입력으로 처리합니다.

시리얼 모니터를 통해 결과를 확인하였습니다. 위쪽 화면은 결과가 잘 나왔지만, 아래쪽은 시계 방향(clockwise)으로만 돌린 경우인데도 가끔 반대 방향으로 인식을 하였습니다. 아주 빠른 속도로 이쪽저쪽 돌려보면  아래 결과처럼 나오는데, 일반적인 조작 속도 내에선 제대로 결과가 나왔습니다.

엔코더의 푸시 스위치로 입력 받기

엔코더의 푸시 스위치를 이용해 White 색상 LED를 온/오프하기 위해 우선 위에서 만든 소스에 스위치를 위한 코드를 추가하겠습니다.

#define outA D5
#define outB D6
#define sw D7
//
int previousA = 1;
int previousB = 1;
int previousPush = 1;
int cwCount = 0;
int ccwCount = 0;
unsigned long debounce = 0;
//
void setup() {
  Serial.begin(115200);
  //
  pinMode(outA, INPUT_PULLUP);
  pinMode(outB, INPUT_PULLUP);
  pinMode(sw, INPUT_PULLUP);
  //
}

푸시 스위치를 위한 코드도 엔코더 입력과 마찬가지 구조입니다. 내부 풀업 레지스터를 이용해 "입력모드"로 설정하고, 스위치로부터 입력이 발생했는지 체크하기 위해 이전 값을 저장하는 previousPush 변수를 선언하였습니다. 역시 풀업(pull-up) 하였기 때문에 입력이 없을 때는 "1"을 입력이 발생하면 "0"을 입력 받습니다.

void loop() {
  int currentA = digitalRead(outA);
  int currentB = digitalRead(outB);
  //
  if ( ( millis() - debounce ) > 1 ) {
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      ...
      previousA = currentA;
      previousB = currentB;
    }
    // 엔코더의 push 스위치 처리
    int currentPush = digitalRead(sw);
    //
    if ( currentPush != previousPush ) {
      if ( currentPush == 0 ) {
        Serial.println("Switch Pushed!");
      }
      previousPush = currentPush;
    }
    debounce = millis();
  }
}

loop() 함수에 입력 처리 루틴을 작성하였습니다. 푸시 스위치도 디바운스 처리를 해야 하므로 엔코더의 회전 처리 아래쪽에 위치시켰습니다.

결과 화면입니다. 스위치와 회전 입력이 제대로 처리되고 있습니다. 이 스위치를 이용해 온/오프 효과를 내기 위해서, 스위치를 누를 때마다 상태가 토글(toggle) 되도록 하겠습니다.

bool whiteOn = false;
unsigned long debounce = 0;
.
.
.
void loop() {
  .
  .
  .
  if ( ( millis() - debounce ) > 1 ) {
    // 엔코더의 push 스위치 처리
    int currentPush = digitalRead(sw);
    //
    if ( currentPush != previousPush ) {
      if ( currentPush == 0 ) {
        if ( whiteOn ) {
          whiteOn = false;
          Serial.println("Lamp OFF!");
        } else {
          whiteOn = true;
          Serial.println("White Lamp ON!");
        }
      }
      previousPush = currentPush;
    }
    debounce = millis();
  }
}

하나의 스위치로 두 가지 상태를 토글하기 위해서 현재 값을 저장할 변수 whiteOn을 선언하였습니다. 스위치가 눌렸을 때, whiteOn이 참이라면 현재 Lamp On 상태이므로 Off 상태로 토글하고, 아니라면 반대로 처리합니다.

시리얼 모니터로 결과를 확인하였습니다. 엔코더의 회전은 LED의 밝기를 제어합니다. 이 부분에 대한 처리는 아래와 같습니다.

int whiteBrightness = 125;
.
.
void loop() {
  .
  .
  if ( ( millis() - debounce ) > 1 ) {
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      if ( currentA == 0 ) {    // A 입력이 "0"일때
        if ( currentB == 0 ) {  // B 입력이 "0"일때
          // 입력이 0,0 일때의 처리
        } else { // B 입력이 "1"일때
          //  입력이 0,1 일때의 처리
        }
      } else { // A 입력이 "1"일때
        if ( currentB == 0 ) {  // B 입력이 "0"일때
          // 입력이 1,0 일때의 처리
        } else { // B 입력이 "1"일때
          // 입력이 1,1 일때의 처리
          if ( ( cwCount == 3 ) ) {
            if ( ++whiteBrightness > 255 ) whiteBrightness = 255;
            Serial.println(whiteBrightness);
            cwCount = 0;
          } else if ( ( ccwCount == 3 ) ) {
            if ( --whiteBrightness < 1 ) whiteBrightness = 1;
            Serial.println(whiteBrightness);
            ccwCount = 0;
          }
        }
      }
      previousA = currentA;
      previousB = currentB;
    }
    // 엔코더의 push 스위치 처리
    .
    .
    debounce = millis();
  }
}

밝기 값을 저장할 whiteBrightness 변수를 선언하고, 엔코더 노브를 돌릴 때마다 증가 또는 감소시키도록 코딩하였습니다. 최대 밝기는 255, 최소 밝기는 1로 하여 범위를 벗어나지 않도록 합니다.

네오픽셀 LED 모듈에 적용하기

이제, 작성한 코드를 이용해 실제로 LED를 제어하겠습니다. 우선 네오픽셀 LED를 위한 라이브러리와 오브젝트 생성 및 오브젝트 시작 등 관련 코드를 삽입합니다.

#include <Adafruit_NeoPixel.h>
.
.
.
Adafruit_NeoPixel ledBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
//
void setup() {
  .
  .
  ledBar.begin();
  for ( int i = 0; i < 8; i++ ) {
    ledBar.setPixelColor(i, 0, 0, 0, 0);
  }
  ledBar.show();
}

위와 같이 수정합니다. ledBar라는 이름으로 오브젝트를 생성하고, 모든 색상을 "0"값으로 채워 모든 LED를 Off시켰습니다. 다음으로 White LED를 제어하는 코드를 작성합니다.

void loop() {
  .
  .
  //
  if ( ( millis() - debounce ) > 1 ) {
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      if ( currentA == 0 ) {    // A 입력이 "0"일때
        .
        .
      } else { // A 입력이 "1"일때
        if ( currentB == 0 ) {  // B 입력이 "0"일때
          // 입력이 1,0 일때의 처리
        } else { // B 입력이 "1"일때
          // 입력이 1,1 일때의 처리
          if ( ( cwCount == 3 ) ) {
            whiteBrightness += 5; // 밝기 5씩 증가
            if ( whiteBrightness > 255 ) whiteBrightness = 255;
            if ( whiteOn ) {
              for ( int i = 0; i < 8; i++ ) {
                ledBar.setPixelColor(i, 0, 0, 0, whiteBrightness);
              }
              ledBar.show();
            }
            cwCount = 0;
          } else if ( ( ccwCount == 3 ) ) {
            whiteBrightness -= 5; // 밝기 5씩 감소
            if ( whiteBrightness < 1 ) whiteBrightness = 1;
            if ( whiteOn ) {
              for ( int i = 0; i < 8; i++ ) {
                ledBar.setPixelColor(i, 0, 0, 0, whiteBrightness);
              }
              ledBar.show();
            }
            ccwCount = 0;
          }
        }
      }
      previousA = currentA;
      previousB = currentB;
    }
    // 엔코더의 push 스위치 처리
    .
    .
    .
    debounce = millis();
  }
}

엔코더의 A, B 두 입력 값이 1, 1일 때의 처리를 위와 같이 수정합니다. 밝기 값은 5씩 증감하도록 수정했습니다. 1씩 변하는 건 별 차이도 없고 너무 많이 돌려야 하네요.

void loop() {
  .
  .
  //
  if ( ( millis() - debounce ) > 1 ) {
	.
    .
    // 엔코더의 push 스위치 처리
    int currentPush = digitalRead(sw);
    //
    if ( currentPush != previousPush ) {
      if ( currentPush == 0 ) {
        if ( whiteOn ) {
          whiteOn = false;
          for ( int i = 0; i < 8; i++ ) {
            ledBar.setPixelColor(i, 0, 0, 0, 0);
          }
          ledBar.show();
        } else {
          whiteOn = true;
          for ( int i = 0; i < 8; i++ ) {
            ledBar.setPixelColor(i, 0, 0, 0, whiteBrightness);
          }
          ledBar.show();
        }
      }
      previousPush = currentPush;
    }
    debounce = millis();
  }
}

푸시 스위치 부분도 수정하였습니다. 시리얼 모니터로 출력하는 부분만 고쳐주면 됩니다. 여기까지의 전체 소스는 아래 숨겨진 부분에 있고, 결과는 아래 동영상을 참고 하세요!

전체 소스
#include <Adafruit_NeoPixel.h>
//
#define outA D5
#define outB D6
#define sw D7
//
int previousA = 1;
int previousB = 1;
int previousPush = 1;
int cwCount = 0;
int ccwCount = 0;
int whiteBrightness = 125;
bool whiteOn = false;
unsigned long debounce = 0;
//
Adafruit_NeoPixel ledBar = Adafruit_NeoPixel(8, D3, NEO_GRBW + NEO_KHZ800);
//
void setup() {
  Serial.begin(115200);
  //
  pinMode(outA, INPUT_PULLUP);
  pinMode(outB, INPUT_PULLUP);
  pinMode(sw, INPUT_PULLUP);
  //
  ledBar.begin();
  ledBar.show();
  for ( int i = 0; i < 8; i++ ) {
    ledBar.setPixelColor(i, 0, 0, 0, 0);
  }
  ledBar.show();
}
//
void loop() {
  int currentA = digitalRead(outA);
  int currentB = digitalRead(outB);
  //
  if ( ( millis() - debounce ) > 1 ) {
    if ( ( currentA != previousA ) || ( currentB != previousB ) ) {
      if ( currentA == 0 ) {    // A 입력이 "0"일때
        if ( currentB == 0 ) {  // B 입력이 "0"일때
          // 입력이 0,0 일때의 처리
          if ( ( cwCount == 1 ) ) {
            cwCount = 2;
          } else if ( ( ccwCount == 1 ) ) {
            ccwCount = 2;
          }
        } else { // B 입력이 "1"일때
          //  입력이 0,1 일때의 처리
          if ( ( cwCount == 0 ) && ( ccwCount == 0 ) ) {
            cwCount = 1;
          } else if ( ( ccwCount == 2 ) ) {
            ccwCount = 3;
          }
        }
      } else { // A 입력이 "1"일때
        if ( currentB == 0 ) {  // B 입력이 "0"일때
          // 입력이 1,0 일때의 처리
          if ( ( cwCount == 2 ) ) {
            cwCount = 3;
          } else if ( ( cwCount == 0 ) && ( ccwCount == 0 ) ) {
            ccwCount = 1;
          }
        } else { // B 입력이 "1"일때
          // 입력이 1,1 일때의 처리
          if ( ( cwCount == 3 ) ) {
            whiteBrightness += 5;
            if ( whiteBrightness > 255 ) whiteBrightness = 255;
            if ( whiteOn ) {
              for ( int i = 0; i < 8; i++ ) {
                ledBar.setPixelColor(i, 0, 0, 0, whiteBrightness);
              }
              ledBar.show();
            }
            cwCount = 0;
          } else if ( ( ccwCount == 3 ) ) {
            whiteBrightness -= 5;
            if ( whiteBrightness < 1 ) whiteBrightness = 1;
            if ( whiteOn ) {
              for ( int i = 0; i < 8; i++ ) {
                ledBar.setPixelColor(i, 0, 0, 0, whiteBrightness);
              }
              ledBar.show();
            }
            ccwCount = 0;
          }
        }
      }
      previousA = currentA;
      previousB = currentB;
    }
    // 엔코더의 push 스위치 처리
    int currentPush = digitalRead(sw);
    //
    if ( currentPush != previousPush ) {
      if ( currentPush == 0 ) {
        if ( whiteOn ) {
          whiteOn = false;
          for ( int i = 0; i < 8; i++ ) {
            ledBar.setPixelColor(i, 0, 0, 0, 0);
          }
          ledBar.show();
        } else {
          whiteOn = true;
          for ( int i = 0; i < 8; i++ ) {
            ledBar.setPixelColor(i, 0, 0, 0, whiteBrightness);
          }
          ledBar.show();
        }
      }
      previousPush = currentPush;
    }
    debounce = millis();
  }
}

여기까지 벌써 일곱 개의 연재가 진행되었습니다. 사용할 부품은 모두 소개했고, 이제 3D 출력 및 조립, 그리고 무선(Wi-Fi) 제어 부분만 남았네요. 서둘러서 마무리하도록 하겠습니다. 이상입니다.

Comments