Turtle Table for rotation photo #3

2018. 12. 31. 16:18

Project/Turtle Table

제품 촬영을 위한 Turtle Table 3/3

터틀 테이블과 관련된 세 번째 연재입니다. 이 글에서 프로그램 코딩을 마무리하고 부품을 조립하여 완성까지 하겠습니다. 우선, 이전 글에 이어서 로터리 엔코더에 대한 프로그램 소스(source)를 작성하겠습니다.

로터리 엔코더의 CLK 입력 처리

이제까지의 소스(이전 포스트 참고)는 입력이 있든 없든 모든 값을 처리합니다. 이제 입력이 있을 때만 반응하도록 수정하겠습니다.

void loop() {
  int clkValue = digitalRead(clk);
  if (clkValue == LOW) {
    Serial.println(clkValue);
  }
}

위 코드는 엔코더의 CLK을 읽고(clkValue 변수에 저장) 그 값이 “LOW”일 때만 시리얼 모니터에 출력합니다. 이전 소스의 loop() 함수를 위와 같이 수정한다면 입력 값이 “LOW”일 때 즉, 엔코더를 돌렸을 때만 시리얼 모니터에 출력하여 “0”값으로만 채워질 것입니다.

이 정도만으로도 원하는 결과를 얻을 수 있지만, loop() 함수가 워낙 빨리 반복하기 때문에 입력은 한 번인데 처리는 여러 번 하는 경우도 발생할 수 있습니다. 엔코더가 한 번 딸깍 하는 사이에도 여러 번 읽기 때문인데, 약간의 딜레이를 주어서 이를 해결할 수도 있고, 지금 읽은 값이 이전에 읽은 값과 다를 때만 처리하도록 할 수도 있습니다. 즉, 엔코더가 딸~깍 하는 동안은 "LOW"값만 변화 없이 여러 번 입력되므로 이런 때는 무시하고 값의 변화가 생겼을 때만 처리하는 것인데 저는 이 방법을 사용하겠습니다.

int oldClk = HIGH;
int currentClk;

우선 위와 같이 두 개의 변수를 선언하고 이전에 사용한 clkValue 변수는 삭제합니다. oldClk 변수는 이전에 입력 받았던 값을 기억하고, currentClk 변수는 현재 읽어 들인 입력 값입니다. 프로그램이 처음 실행되면 아직 이전 값이 존재하지 않기 때문에, 입력이 없을 때의 값으로 oldClk 변수를 초기화하였습니다.

void loop() {
  currentClk = digitalRead(clk);
  if (oldClk != currentClk) {
    // 처리
    oldClk = currentClk;
  }
}

loop() 함수도 변경합니다. CLK를 읽어서 currentClk에 저장한 후에 이전 값과 “다른 지”를 검사합니다. 다르다면 엔코더로부터 입력이 들어왔다는 의미이므로 해당하는 처리를 해준 후, 현재 값은 다음 번 비교를 위해 oldClk에 저장합니다. 이제 이 소스는 “0”에서 “1”로 또는 “1”에서 “0”으로 입력 값이 변경될 때만 처리하며 동일한 값이 유지될 때는 반응하지 않습니다. 따라서 한 번 딸깍하는 동안 비정상적으로 여러 번 처리되는 경우는 해결됩니다.

void loop() {
  currentClk = digitalRead(clk);
  if (oldClk != currentClk) {
    if (currentClk == LOW) {
      Serial.println(currentClk);
    }
    oldClk = currentClk;
  }
}

입력 값이 변동되었다고 해도 “HIGH”에서 “LOW”로 바뀔 때만 처리하므로 안쪽에 IF문을 두어 currentClk 변수 값이 “LOW”인지 체크하고 해당하는 명령을 수행합니다. currentClk 변수가 “HIGH”라면 “LOW”에서 “HIGH”로 변한 상황이므로 엔코더를 돌리고 난 후에 해당합니다. 처리할 필요 없겠죠.

#define sw 6
#define dt 7
#define clk 8
int oldClk = HIGH;
int currentClk;
void setup() {
  Serial.begin(9600);
  pinMode(clk, INPUT);
}
void loop() {
  currentClk = digitalRead(clk);
  if (oldClk != currentClk) {
    if (currentClk == LOW) {
      Serial.println(currentClk);
    }
    oldClk = currentClk;
  }
}

여기까지의 소스와 결과 화면입니다. 입력이 있을 때만 처리하고 있고, 한 번 입력에 여러 번 처리되는 경우도 예방하였습니다. 결과는 "LOW"에 해당하는 숫자 "0"만 출력됩니다.

로터리 엔코더의 DT 입력 처리
#define sw 6
#define dt 7
#define clk 8
int oldClk = HIGH;
int currentClk;
int dtValue;
void setup() {
  Serial.begin(9600);
  pinMode(clk, INPUT);
  pinMode(dt, INPUT);
}
void loop() {
  currentClk = digitalRead(clk);
  dtValue = digitalRead(dt); 
  if (oldClk != currentClk) {
    if (currentClk == LOW) {
      Serial.print(currentClk);
      Serial.print("  ");
      Serial.println(dtValue);
    }
    oldClk = currentClk;
    delay(50);
  }
}

이 예제는 엔코더의 DT 입력 값을 처리하도록 추가하였습니다. DT 값을 읽어 저장하기 위한 dtValue 변수를 선언하고 관련된 코드들을 더했는데, CLK와 동시에 발생하는 입력이기 때문에 별도의 입력 값 체크는 생략하였습니다.

결과를 보면, 시계 방향으로 돌릴 때 DT는 숫자 “0(LOW)”을, 반시계 방향일 땐 숫자 “1(HIGH)”을 출력합니다. 이를 체크하면 어느 쪽으로 돌렸는지를 알 수 있습니다.

int speedValue = 0;

이 프로젝트에서 엔코더를 이용해 속도 조절을 할 예정입니다. 엔코더의 회전 방향에 따라 속도를 높이거나 낮추도록 할 건데, 이를 위해서 DT값의 체크가 필요합니다. 우선 속도값을 저장하기 위한 speedValue 라는 변수를 선언하고 기본 값으로 “0”을 주었습니다.

if (dtValue == LOW) {
  speedValue += 1;
} else {
  speedValue -= 1;
}

DT 값을 체크하는 코드입니다. 제가 가진 엔코더의 경우, 시계 방향으로 돌릴 때 DT 값이 "LOW"이며, 반 시계 방향은 "HIGH"입니다. 위 코드는 시계 방향일 때 속도를 증가시킵니다.

if (speedValue < 0) {
  speedValue = 0;
} else if (speedValue > 90) {
  speedValue = 90;
}

속도는 “0”에서 “90”사이의 범위를 갖습니다. 이 범위가 넘어가지 않도록 처리하는 코드도 추가하였습니다.

#define sw 6
#define dt 7
#define clk 8
//
int oldClk = HIGH;
int currentClk;
int dtValue;
int speedValue = 0;
//
void setup() {
  Serial.begin(9600);
  pinMode(clk, INPUT);
  pinMode(dt, INPUT);
}
//
void loop() {
  currentClk = digitalRead(clk);
  dtValue = digitalRead(dt); 
  if (oldClk != currentClk) {
    if (currentClk == LOW) {
      if (dtValue == LOW) {
        speedValue += 1;
      } else {
        speedValue -= 1;
      }
      //
      if (speedValue < 0) {
        speedValue = 0;
      } else if (speedValue > 90) {
        speedValue = 90;
      }
      Serial.println(speedValue);
    }
    oldClk = currentClk;
    delay(50);
  }
}

전체 소스와 결과 화면입니다. 마지막 줄에 너무 빨리 돌려서 생길 오류를 방지하기 위해 약간의 딜레이를 주었습니다. 결과 화면을 통해 스피드 값의 변화를 확인할 수 있습니다.

예제가 진행되는 동안 결과 확인이 쉽도록 임시로 세그먼트 LED 모듈을 추가하였습니다. 자세한 사용법은 제 블로그의 다른 포스트를 참고하세요!

#include <TM1637Display.h>
//
#define led_clk 2 // LED segments CLK
#define led_dio 3 // LED segments DIO
#define sw 6
#define dt 7
#define clk 8
//
int oldClk = HIGH;
int currentClk;
int dtValue;
int speedValue = 0;
//
TM1637Display dsp(led_clk, led_dio);
//
void setup() {
  pinMode(clk, INPUT);
  pinMode(dt, INPUT);
  dsp.setBrightness(7);
}
//
void loop() {
  currentClk = digitalRead(clk);
  dtValue = digitalRead(dt);
  //
  if (oldClk != currentClk) {
    if (currentClk == LOW) {
      if (dtValue == LOW) {
        speedValue += 1;
      } else {
        speedValue -= 1;
      }
      if (speedValue < 0) {
        speedValue = 0;
      } else if (speedValue > 90) {
        speedValue = 90;
      }
      dsp.showNumberDec(speedValue);
    }
    oldClk = currentClk;
    delay(50);
  }
}

전체 소스 및 결과 영상입니다. 이전 소스에 LED를 위한 코드를 추가하고 출력 부분만 수정하였습니다. 엔코더를 돌려서 속도 값(speedValue)을 변화시키고 이를 세그먼트 LED를 통해 출력합니다.

서보 모터 관련 코드 추가하기
#include <Servo.h>
#include <TM1637Display.h>
//
#define led_clk 2 // LED segments CLK
#define led_dio 3 // LED segments DIO
#define sw 6
#define dt 7
#define clk 8
//
int oldClk = HIGH;
int currentClk;
int dtValue;
int speedValue = 0;
//
Servo myservo;  // create servo object to control a servo
TM1637Display dsp(led_clk, led_dio);
//
void setup() {
  pinMode(clk, INPUT);
  pinMode(dt, INPUT);
  oldClk = digitalRead(clk);
  dsp.setBrightness(7);
  myservo.attach(9);  // attaches the servo on pin 9 to the servo object
}
//
void loop() {
  currentClk = digitalRead(clk);
  dtValue = digitalRead(dt);
  //
  if (oldClk != currentClk) {
    if (currentClk == LOW) {
      if (dtValue == LOW) {
        speedValue += 1;
      } else {
        speedValue -= 1;
      }
      if (speedValue < 0) {
        speedValue = 0;
      } else if (speedValue > 90) {
        speedValue = 90;
      }
      dsp.showNumberDec(speedValue);
      myservo.write(90 - speedValue);
    }
    oldClk = currentClk;
    delay(50);
  }
}

이전 예제에 서보 모터를 위한 코드를 추가한 후, 엔코더로 조절한 속도 값으로 모터를 구동하는 소스입니다. 아직 시계 방향으로만 회전합니다. 속도가 “0”값일 때 가장 빠르므로 이에 대한 변환이 필요합니다.

엔코더의 푸시 버튼을 이용한 회전 방향 전환

이제 소스 코드의 마지막 단계로 엔코더의 푸시 버튼 스위치를 이용해 회전 방향을 제어하는 부분을 작성하겠습니다.

int rotation = HIGH;

회전 방향을 기억할 변수입니다. "HIGH"일 때 시계 방향이 되도록 했습니다.

int oldsw = HIGH;
int currentsw;

푸시 버튼의 입력을 처리할 변수 두 개를 선언합니다. 이 부분은 엔코더의 CLK 입력 처리와 동일하므로 설명하지 않겠습니다.

  if (oldsw != currentsw) {
    if (currentsw == LOW) {
      rotation = 1 - rotation;
    }
    oldsw = currentsw;
    delay(50);
  }

버튼 입력을 처리합니다. 역시 CLK와 동일합니다. 버튼이 눌려질 때마다 rotation 변수의 값이 "0"에서 "1" 또는 "1"에서 "0"으로 반대가 됩니다.

  if (rotation == HIGH) {
    myservo.write(90 - speedValue);
  } else {
    myservo.write(90 + speedValue);
  }

loop() 함수의 마지막 부분에 위 코드를 추가하였습니다. loop를 한 번 돌 때마다 수행되며 방향 전환이나 속도 조절된 부분이 이 때 적용됩니다. 

#include <Servo.h>
#include <TM1637Display.h>
//
#define led_clk 2 // LED segments CLK
#define led_dio 3 // LED segments DIO
#define sw 6
#define dt 7
#define clk 8
//
int oldClk = HIGH;
int currentClk;
int dtValue;
int speedValue = 0;
int rotation = HIGH;
int oldsw = HIGH;
int currentsw;
//
Servo myservo;  // create servo object to control a servo
TM1637Display dsp(led_clk, led_dio);
//
void setup() {
  pinMode(sw, INPUT_PULLUP);
  pinMode(clk, INPUT);
  pinMode(dt, INPUT);
  dsp.setBrightness(7);
  myservo.attach(9);  // attaches the servo on pin 9 to the servo object
}
//
void loop() {
  currentClk = digitalRead(clk);
  dtValue = digitalRead(dt);
  //
  if (oldClk != currentClk) {
    if (currentClk == LOW) {
      if (dtValue == LOW) {
        speedValue += 1;
      } else {
        speedValue -= 1;
      }
      if (speedValue < 0) {
        speedValue = 0;
      } else if (speedValue > 90) {
        speedValue = 90;
      }
      dsp.showNumberDec(speedValue);
    }
    oldClk = currentClk;
    delay(50);
  }
//
  currentsw = digitalRead(sw);
//
  if (oldsw != currentsw) {
    if (currentsw == LOW) {
      rotation = 1 - rotation;
    }
    oldsw = currentsw;
    delay(50);
  }
  //
  if (rotation == HIGH) {
    myservo.write(90 - speedValue);
  } else {
    myservo.write(90 + speedValue);
  }
  delay(50);
}

여기까지의 전체 소스와 결과 영상입니다.

부품 조립하여 완성하기

인서트 너트라는 부품입니다. 3D 프린팅 출력물을 볼트로 조립할 때 유용한 방법으로, 전기인두 등을 이용해 미리 만든 구멍에 삽입하고 안쪽의 나사산을 이용하여 볼트로 고정합니다. 인서트 너트를 이용할 경우 볼트를 풀고 조여도 나사산이 마모되지 않기 때문에 탈부착이 자주 발생하는 곳에 사용하면 좋습니다.

이번 프로젝트에선 테이블과 케이스 상판 고정하는 부분만 사용했고 다른 부분은 한 번 고정하면 다시 분리할 일이 없을 듯해서 좁은 구경으로 출력한 후 볼트를 힘으로 돌려 끼웠습니다.

프로젝트에 사용할 엔코더의 한쪽이 약간 들떠있어서 바로 잡아 주었습니다.

우선 내부 부품들을 장착했습니다. M2, M3 볼트를 사용하였고, 탈부착할 일이 없기 때문에 좁은 구경으로 출력한 후 인서트 너트 없이 볼트를 강제로 돌려서 고정했습니다.

상판에 서보 모터를 고정하였습니다. M3 볼트와 너트를 이용했고, 이 때문에 상판을 조금 두껍게 디자인했습니다.

케이블 연결을 완료한 상태입니다. 모터를 제외하고 적당한 길이로 다시 만들어 주었습니다.

케이스에 상판을 결합하였습니다. 케이스에 사용한 인서트 너트가 M3 규격이라서 같은 규격의 육각 렌치 볼트를 사용하였습니다. 인서트 너트를 사용했기 때문에 부품 테스트 등을 위해 자주 열고 닫아도 마모될 일은 없습니다.

로터리 엔코더용 노브(knob)를 삽입할 차례입니다. 사진에서 보면 구멍이 한쪽으로 약간 쏠려 있습니다. 설계 및 출력 후에 엔코더의 수평을 맞추었더니 중심이 약간 틀어졌네요.

출력한 노브(knob)입니다. 엔코더의 축(shaft)과 같이 반달 모양으로 디자인했습니다.

끝까지 삽입하여 위와 같이 딱 맞도록 설계하였습니다.

장착한 모습입니다. 큼지막해서 돌려주기 좋습니다.

이제 테이블을 위한 지지대를 모터에 장착하겠습니다. 모터 혼(horn)을 미리 설계된 홈에 끼우고 볼트로 조여 주었습니다. 혼 자체에 M3 규격의 나사산이 있기 때문에 별도의 너트 등이 필요 없습니다. 사진 오른쪽을 보면 볼트 머리 네 개가 지지대 위로 솟았는데, 이를 위해 테이블 아래쪽에 같은 모양으로 구멍을 설계하였습니다.

모터에 혼을 올리고 가운데 구멍을 통해서 볼트로 결합하였습니다. M3 규격의 볼트를 사용하면 되는데, 문제가 하나 있네요. 제가 사용한 혼이 약간 좁아서 모터의 기어에 확실히 장착 되지가 않습니다. 억지로 끼우고 볼트로 조였더니 결국 테이블의 수평이 약간 맞지 않습니다. 완성한 후에 찍은 영상을 보니 수평 조절을 위해 다시 조립해야 하겠습니다.

테이블은 위와 같이 지지대에 볼트로 하나씩 조입니다. 케이스에 가려서 다른 곳은 잘 보이지 않습니다. 테이블 위쪽에 볼트 구멍을 내지 않기 위해서 지지대를 사용하는 방법을 썼습니다.

테이블까지 장착하여 완성한 모습입니다.

사진과 같이 USB케이블을 이용해 전원을 공급합니다. 전원 스위치는 따로 없지만 기본 속도가 “0”이기 때문에 아직 회전하진 않습니다.

테스트를 위해 아무거나 하나 올렸습니다. 이 테이블을 위한 화면은 아래 영상을 참고 하세요. 이상으로 터틀 테이블에 대한 제작기를 마치겠습니다. 다음 버전에서는 보다 폭 넓은 속도 조절과 원하는 각도로 회전하는 기능을 추가하겠습니다.

Comments