3.4.Encoder를 이용하여 모터의 위치 계산하기
in Mechatronics on Classnote
주의사항
[고장주의] 12V 전원을 잘못 연결하면 라즈베리파이가 망가진다. 회로를 구성한 후에 확인을 반드시 하기 바라며 전원 공급을 가장 마지막에 하도록 한다.
모터 인코더 이용하기
모터 인코더란
앞서서 모터를 작동시키긴 했는데, 이 모터가 얼마나 돌아갔는지를 어떻게 알 수 있을 것인가?
-모터의 인코더
지급된 키트의 모터의 뒷면을 보면 작은 자석 원판과 주변에 있는 검은색 소자를 두 개 볼 수 있다. 여기서 검은색 소자는 홀 센서라고 하며, 이는 자기장의 변화에 반응하여 전기 신호를 내놓는 소자이다. 자석 원판은 모터와 연결되어 있다. 따라서 모터가 돌면 자석 원판이 돌아갈 것이고, 자석 원판이 돌면 자기장이 변할 것이고, 자기장의 변화를 홀 센서에서 감지할 것이므로, 홀 센서에서 나오는 신호를 해석하면 모터의 회전에 대한 정보를 알 수 있다. 이러한 과정을 통해 모터의 위치를 파악할 수 있는 장치를 인코더라고 한다.
인코더의 신호 - Interrupt
-모터의 핀별 기능과 회로 구성
-홀 센서의 출력 파형
제조사에서 제공하는 모터 스펙 시트를 확인하면 회로를 어떻게 구성해야 할지 파악할 수 있다. 위의 사진에서 갈색 3번 핀에 라즈베리파이에서 나오는 3.3V를, 초록색 4번 핀에 라즈베리파이의 임의의 GND에 연결하고, 청색 5번핀과 보라색 6번 핀을 각각 라즈베리파이의 임의의 GPIO에 연결한다. 여기선 GPIO17, GPIO27을 이용한다.
인코더에서 나오는 신호를 누락없이 정확하게 받아야 모터의 현재의 위치를 정확하게 알 수 있다. 그래서 인코더에서 나오는 신호를 모두 인식하기 위해서 라즈베리파이에서 인코더 신호를 받는 GPIO에 특별한 기능을 추가한다. 이러한 기능을 interrupt라고 한다. 인터럽트는 일종의 사건의 발생, 변화라고 생각하면 쉬운데, 여기에서는 핀의 신호 변화에 의해 interrupt된다고 해서 pin interrupt라고 부른다. 우리는 인코더 신호를 받는 GPIO 핀에 신호의 변화가 감지되면 곧바로 위치 변화를 계산할 수 있는 함수를 호출하도록 프로그램을 제작할 것이다.
제공된 인코더의 스펙 시트를 따르면 30도 회전할 때 1번의 엣지가 발생하므로, 이론상 1바퀴 돌면 12번의 엣지가 발생한다. 그리고 또한 모터에는 1:18의 감속비를 갖는 감속기가 달려있어서, 이론상 모터가 18바퀴 돌아야 감속기는 1바퀴가 돌아간다. 따라서 인코더에서 이론상 216번의 엣지가 발생하면 감속기가 1번 회전하게 된다. 우리가 관심 있는 것은 감속기의 위치이므로, 감속기의 위치를 개수로 표현할 수 있을 것이다.
순차적으로 배운 내용들을 활용하기 위해 우선 아래의 회로를 구성하고 코드를 작성하여, pin interrupt가 잘 작동하는지 확인해본다.
엔코더 회로 구성 및 예제
-엔코더 구동을 위한 회로의 구성
#include <stdio.h>
#include <wiringPi.h>
#include <softPwm.h>
#define ENCODERA 17
#define ENCODERB 27
int encA;
int encB;
void funcEncoderA()
{
printf("funcEncoderA() ");
encA = digitalRead(ENCODERA);
encB = digitalRead(ENCODERB);
printf("%d %d\n", encA, encB);
}
void funcEncoderB()
{
printf("funcEncoderB() ");
encA = digitalRead(ENCODERA);
encB = digitalRead(ENCODERB);
printf("%d %d\n", encA, encB);
}
int main(void)
{
wiringPiSetupGpio();
pinMode(ENCODERA, INPUT);
pinMode(ENCODERB, INPUT);
// wiringPiISR: Pin interrupt setting
wiringPiISR(ENCODERA, INT_EDGE_BOTH, funcEncoderA);
wiringPiISR(ENCODERB, INT_EDGE_BOTH, funcEncoderB);
while (1)
{
// LOOP FOREVER
}
return 0;
}
상기 예제를 실행하고 인코더를 돌리면 pin interrupt에 의해서 funcEncoderA()
, funcEncoderB()
가 반복적으로 실행된다. 그리고 encA
와 encB
에 각각 17번, 27번 핀에서 읽은 HIGH 또는 LOW 중 하나의 상태를 저장하고 출력하는 것을 확인할 수 있다.
인코더의 신호 - 펄스
인코더에서 내보내는 HIGH와 LOW는 과연 어떠한 의미를 가지고 있는가? 지급된 모터에 사용되는 인코더는 quadrature encoder라고 부르는데, 아래의 그림처럼 90도만큼 위상이 차이가 나는 사각파가 발생한다. 따라서 펄스가 발생하는 규칙을 알아내면 상대 위치와 이동 방향을 알아낼 수 있다.
-Quadrature encoder에서의 펄스 발생
HIGH에서 LOW로 떨어지는 때를 Falling edge, LOW에서 HIGH로 올라가는 때를 Rising edge라고 부른다. 그림의 위 부분에서처럼, A가 Rising일 때 B가 LOW인 경우, B가 Rising일 때 A가 HIGH인 경우, A가 Falling일 때 B가 HIGH인 경우, B가 Falling일 때 A가 LOW인 경우 오른쪽을 향하는 방향으로 움직이고 있는 것이다. 또한 확인해보면 아래 부분에서는 A가 Rising일 때 B가 LOW인 경우가 절대로 없고, 나머지도 마찬가지이다. 그림의 하단부에서는, B가 Rising일 때 A가 LOW, A가 Rising일 때 B가 HIGH, B가 Falling일 때 A가 HIGH, A가 Falling일 때 B가 LOW이면 왼쪽 화살표 방향으로 움직이고 있는 것이다. 따라서 모터의 회전 방향을 파악할 수 있으며 이를 아래의 리스트로 정리하면 아래와 같다.
- A의 신호가 바뀐 경우
- A의 신호변화 (Rising: LOW -> HIGH)
- B의 신호 = LOW
- 오른쪽 (+)
- B의 신호 = HIGH
- 왼쪽 (-)
- B의 신호 = LOW
- A의 신호변화(Falling: HIGH - >LOW)
- B의 신호 = LOW
- 왼쪽 (-)
- B의 신호 = HIGH
- 오른쪽 (+)
- B의 신호 = LOW
- A의 신호변화 (Rising: LOW -> HIGH)
- B의 신호가 바뀐 경우
- B의 신호변화(Rising: LOW -> HIGH)
- A의 신호 = LOW
- 왼쪽 (-)
- A의 신호 = HIGH
- 오른쪽 (+)
- A의 신호 = LOW
- B의 신호변화(Falling: HIGH -> LOW)
- A의 신호 = LOW
- 왼쪽 (+)
- A의 신호 = HIGH
- 오른쪽 (-)
- A의 신호 = LOW
- B의 신호변화(Rising: LOW -> HIGH)
인코더로 모터 위치 구하기
이제 신호에서 펄스가 어떻게 작동하는지를 배웠기에, 앞서 배운 pin interrupt와 같이 이용하면 모터의 위치를 기록할 수 있다. 다음과 같은 예제를 실행해보자.
#include <stdio.h>
#include <wiringPi.h>
#include <softPwm.h>
#define ENCODERA 17
#define ENCODERB 27
#define ENC2REDGEAR 216 // 12 Edge x 18:1 Gear ratio
int encA;
int encB;
int encoderPosition = 0; // Position of Encoder
float redGearPosition = 0; // Position of Motor
void funcEncoderA()
{
encA = digitalRead(ENCODERA);
encB = digitalRead(ENCODERB);
if (encA == HIGH) // Rising at A
{
if (encB == LOW) encoderPosition++;
else encoderPosition--;
}
else // Falling at A
{
if (encB == LOW) encoderPosition--;
else encoderPosition++;
}
redGearPosition = (float)encoderPosition / ENC2REDGEAR;
printf("funcEncoderA() A: %d B: %d encPos: %d gearPos: %f\n",
encA, encB, encoderPosition, redGearPosition);
}
void funcEncoderB()
{
encA = digitalRead(ENCODERA);
encB = digitalRead(ENCODERB);
if (encB == HIGH) // Rising at B
{
if (encA == LOW) encoderPosition--;
else encoderPosition++;
}
else // Falling at B
{
if (encA == LOW) encoderPosition++;
else encoderPosition--;
}
redGearPosition = (float)encoderPosition / ENC2REDGEAR;
printf("funcEncoderB() A: %d B: %d encPos: %d gearPos: %f\n",
encA, encB, encoderPosition, redGearPosition);
}
int main(void)
{
wiringPiSetupGpio();
pinMode(ENCODERA, INPUT);
pinMode(ENCODERB, INPUT);
wiringPiISR(ENCODERA, INT_EDGE_BOTH, funcEncoderA);
wiringPiISR(ENCODERB, INT_EDGE_BOTH, funcEncoderB);
while(1)
{
// Loop Forever
}
return 0;
}
해당 예제를 실행하고 모터 뒤에 장착된 인코더를 회전시켜 보면, 다음 그림과 같이 인코더의 위치와 감속기의 위치가 터미널에 나타나는 것을 확인할 수 있다.
-Pin interrupt를 통한 모터 위치 계산 예시
타이머 인터럽트 모사하기
모터의 정밀 제어를 위해 적절한 제어 주기 확보가 필요하며 라즈베리파이에서 제어 주기를 구현할 수 있는 두 가지 방법에 대해 소개한다. 첫 번째는 리눅스 커널에 접근하여 라즈베리파이의 timer를 사용하는 방법이며 두 번째는 코드 단에서 시간을 측정하는 함수를 사용해 Time Interrupt를 모사하는 것이다. 본 실습에서는 두 번째 방법인 시간 측정 함수를 이용하여 제어 주기를 구현해볼 예정이며 아래는 일정한 제어 주기를 갖는 예시 코드이다.
#include <stdio.h>
#include <wiringPi.h>
#include <softPwm.h>
int main(void)
{
wiringPiSetupGpio();
unsigned int startTime = millis();
unsigned int checkTimeBefore = millis();
unsigned int checkTime;
while(1)
{
checkTime = millis();
if (checkTime - checkTimeBefore >=5) // 5 msec
{
printf("loop time: %d msec, After init: %d msec\n",
checkTime - checkTimeBefore,
checkTime - startTime);
checkTimeBefore = checkTime;
}
}
return 0;
}
모터의 P 제어 예제
비례 제어는 목표 값과 시스템의 현재 출력 값 사이의 오차에 비례 상수를 곱하여 계산된 출력을 사용하여 제어하는 방식이다. 모터의 회전 수 기반 비례 제어 예시 코드이다.
#include <stdio.h>
#include <wiringPi.h>
#include <softPwm.h>
#define LOOPTIME 5 // Sampling Time
#define ENCODERA 17 // Hall Sensor A
#define ENCODERB 27 // Hall Sensor B
#define ENC2REDGEAR 216
#define MOTOR1 19
#define MOTOR2 26
#define PGAIN 10
int encA;
int encB;
int encoderPosition = 0;
float redGearPosition = 0;
float referencePosition = 10;
float errorPosition = 0;
unsigned int checkTime;
unsigned int checkTimeBefore;
void funcEncoderA()
{
encA = digitalRead(ENCODERA);
encB = digitalRead(ENCODERB);
if (encA == HIGH)
{
if (encB == LOW) encoderPosition++;
else encoderPosition--;
}
else
{
if (encB == LOW) encoderPosition--;
else encoderPosition++;
}
redGearPosition = (float)encoderPosition / ENC2REDGEAR;
errorPosition = referencePosition - redGearPosition;
}
void funcEncoderB()
{
encA = digitalRead(ENCODERA);
encB = digitalRead(ENCODERB);
if (encB == HIGH)
{
if (encA == LOW) encoderPosition--;
else encoderPosition++;
}
else
{
if (encA == LOW) encoderPosition++;
else encoderPosition--;
}
redGearPosition = (float)encoderPosition / ENC2REDGEAR;
errorPosition = referencePosition - redGearPosition;
}
int main(void)
{
wiringPiSetupGpio();
pinMode(ENCODERA, INPUT); // Set ENCODERA as input
pinMode(ENCODERB, INPUT); // Set ENCODERB as input
softPwmCreate(MOTOR1, 0, 100); // Create soft Pwm
softPwmCreate(MOTOR2, 0, 100); // Create soft Pwm
wiringPiISR(ENCODERA, INT_EDGE_BOTH, funcEncoderA);
wiringPiISR(ENCODERB, INT_EDGE_BOTH, funcEncoderB);
errorPosition = referencePosition - redGearPosition;
checkTimeBefore = millis();
while(1)
{
checkTime = millis();
if (checkTime - checkTimeBefore > LOOPTIME)
{
if (errorPosition > 0)
{
softPwmWrite(MOTOR1, errorPosition*PGAIN);
softPwmWrite(MOTOR2, 0);
}
else
{
softPwmWrite(MOTOR2, -errorPosition*PGAIN);
softPwmWrite(MOTOR1, 0);
}
checkTimeBefore = checkTime;
}
}
return 0;
}