https://kmong.com/gig/465716?selfMarketingCode=HskZcR53S1
관련글
잡동사니 세상 :: 엔코더 모터 제어 (0. 소개) (tistory.com)
엔코더 모터 제어 (2. 속도 계산) (tistory.com)
엔코더 모터 제어 (3. PID 제어로 속도 제어 [PID 함수들 만들기 Part 01]) (tistory.com)
서론
엔코더 모터를 사용하는 이유 중 하나는 위치를 알 수 있다는 점입니다. 몇 바퀴 회전했는지, 또는 어느정도 각도만큼 움직였는지를 쉽게 측정할 수 있는지 말입니다. 엔코더 모터의 위치는 일반적인 엔코더 사용법과 완전히 동일합니다.
avr 로터리 엔코더 사용하기
제가 사용할 모터에 달려 있는 엔코더는 자석 기반의 엔코더이며, 11 PPR의 해상도를 가지고 있습니다. 한 바퀴에 11개의 펄스가 한 채널에서 발생된다는 거죠. 총 2개의 채널이 있으니 엔코더는 한 바퀴당 최대 22개의 펄스를 발생시킬 수 있습니다. 여기서 주의할 점은 엔코더 기준이지 모터 기준은 아니라는 겁니다. 모터의 ratio가 330이니 모터가 한 바퀴 돌 때, 엔코더는 330바퀴를 회전합니다. 이 덕분에 모터 한 바퀴당 엔코더의 한 채널에서는 11*330 = 3,630개의 펄스가 발생한다는 것을 알 수 있습니다.
여기서 펄스는 다음과 같이 직사각형 모양을 의미합니다. (펄스는 High의 길이(시간)와 관계 없이 Low -> High -> Low로 변하는 과정을 의미합니다.)
관계를 표현하면 다음과 같습니다.
두 채널에 발생된 펄스 | 한 채널에 발생된 펄스 | 엔코더 회전수 | 모터 회전수 |
7,260 개 | 3,630 개 | 330 바퀴 | 1 바퀴 |
이번 시간에는 엔코더에서 펄스 값을 계산하고, 위의 관계식을 이용하여 몇 바퀴 회전했는지 계산해 보겠습니다.
엔코더 설명
체배
엔코더 값 읽기
외부인터럽트
1체배
2체배
4체배
엔코더 설명
엔코더에는 A, B라고 불리우는 채널이라는 개념이 있습니다. 그냥 엔코더의 신호가 나오는 선이라고 생각하시면 됩니다. 엔코더가 시계방향 (Clock Wise) 또는 반시계방향(Count Clock Wise)에 따라 A, B 채널에서 다음과 같은 신호가 발생됩니다.
시계 방향일 경우 A핀이 Low -> High로 변할 때(1번) B는 Low이고, B가 Low -> High일 때(2번) A가 High입니다. 마찬가지로 A가 High -> Low일 때(3번) B는 High이고, B가 High -> Low일 때(4번) A가 Low입니다. 표로 정이하면 다음과 같네요.
시계 방향으로 회전할 때 각 edge(rising or falling)에서의 핀들의 상태
채널 | A rising일 때 | A falling일 때 | B rising일 때 | B falling일 때 |
A핀 상태 | low -> high | high -> low | High | Low |
B핀 상태 | Low | High | low -> high | high -> low |
반시계 방향으로 회전할 때 각 edge(rising or falling)에서의 핀들의 상태
채널 | A rising일 때 | A falling일 때 | B rising일 때 | B falling일 때 |
A핀 상태 | low -> high | high -> low | Low | High |
B핀 상태 | High | Low | low -> high | high -> low |
위의 표를 보면 A핀이 low -> high 즉, rising인 상태일 때 B의 상태가 Low면 시계 방향(CW), High면 반시계 방향(CCW)라는 것을 알 수 있습니다.
우리는 이러한 edge의 개수를 기반으로 얼만큼 회전했는지를 알 수 있는 것입니다.
체배
체배라는 것은 엔코더의 한 개념으로, A rising, A falling, B rising, B falling 등의 edge를 몇개 사용하여 각도를 측정할 것인가에 대한 것입니다.
위에서 설명할 때 A rising, A falling, B rising, B falling 4개의 edge가 있다는 것을 알았고, 각각의 edge에서 다른 채널의 상태에 따라 CW인지, CCW인지를 알 수 있었습니다. 그러면 A rising일 때 B의 상태만 안다면 CW인지, CCW인지 알 수 있다는 의미입니다. 이렇듯 하나의 edge에서도 회전 방향을 알 수 있습니다. 4개 중 1개의 edge로 판단하든, 4개 중 4개 모두의 edge로 판단하든 회정 방향을 마찬가지라는 것이죠.
여기서 4개 중 1개의 edge를 사용하는 것이 1체배 방법, 2개를 사용하는 것이 2체배, 4개를 사용하면 4체배가 되는 거죠. 여기서 중요한 점은 체배가 높을 수록 보다 정확한 각도를 알 수 있다는 것입니다.
예를 들어 11 PPR 엔코더는 엔코더 한 바퀴당 한 채널에서 11개의 펄스가 나오고, 2개의 채널(A, B)이 있으니 총 22개의 펄스가 나옵니다. 한 펄스당 2개의 edge(rising, falling)가 있으니 총 44개의 edge가 발생합니다. 한 바퀴에 44개의 edge가 발생되니 360도 회전하는데 44개의 edge가 발생한다는 의미입니다.
역으로 생각한다면 하나의 edge가 감지 되면 대략 8.18도 회전했다는 것을 알 수 있죠. 근데 여기서 44개의 edge 말고 한 채널만의 edge만 고려한다면 22개의 edge가 발생하고, 그러면 1 edge당 16.36도 회전했다는 것을 알 수 있죠. 더 나아가 한 채널의 rising edge만 고려한다면 11개의 edge가 발생하고, 그러면 1 edge당 32.73도 회전했다는 것을 알 수 있죠.
이를 체배로 설명하면 다음과 같은 표를 생각할 수 있습니다.
한 펄스당 감지하는 edge의 개수 | 한 바퀴당 감지하는 edge의 개수 | 한 edge당 회전 각도 | |
1체배 | 1 | 11 | 32.73 (낮은 정확도) |
2체배 | 2 | 22 | 16.36 (중간 정확도) |
4체배 | 4 | 44 | 8.18 (높은 정확도) |
즉 1체배, 2체배, 4체배 기법 모두 회전 방향을 알 수 있지만, 정확도가 다르다는 것을 알 수 있습니다.
보통 2체배 방법을 이용하여 간단하게 처리하며, 높은 정확도를 원할 때는 4체배 그리고 1체배는 거의 안 쓰는 것 같습니다.
엔코더 값 읽기
제가 산 엔코더 모터의 색에 다른 의미는 다음과 같습니다.
색 | 의미 | 전압 범위 | stm32f103zet6 연결 |
빨강 | 모터 전원선 | 0v ~ 12v | - (펄스 값만 읽을 때는 연결 x) |
검정 | 엔코더 전원선 | 0v (GND) | GND |
노랑 | 엔코더 신호선 (펄스선 A) | 3.3v ~ 5v | PG0 (입맛에 따라 변경 가능) |
초록 | 엔코더 신호선 (펄스선 B) | 3.3v ~ 5v | PG1 (입맛에 따라 변경 가능) |
파랑 | 엔코더 전원선 | 3.3v ~ 5v | 3.3v |
하양 | 모터 전원선 | 0v ~ 12v | - (펄스 값만 읽을 때는 연결 x) |
선이 대칭으로 되어 있으니, 헷갈리지 않으시기를 바랍니다.
펄스선 A, B 이렇게 나눴지만, 이를 반대로 한다고 해도 방향만 바뀌는 것이기 때문에 사실상 어떻게 연결하든 상관이 없습니다.
외부 인터럽트
엔코더에서 값을 읽는 시점은 앞에서 말한 것처럼 rising, falling 등의 edge에서 읽어야 합니다. 그렇다면 우리는 rising 또는 falling에서 발생되는 외부 인터럽트 (또는 EXTI)를 이용하면 이를 쉽게 구현할 수 있을 것입니다.
인터럽트 & 외부 인터럽트
이번 포스트에서는 PG0, PG1을 사용할 것이기 때문에 다음과 같이 작성해 줍니다.
void EXTIInit(void);
int main(void) {
GPIOG -> CRL = (4 << MODE0) | (4 << MODE1);
// PG0, 1을 floating input으로 설정합니다.
EXTIInit();
}
void EXTIInit(void) {
/*
PG0, 1을 EXTI0, 1에 연결하고 NVIC에 연결하여 활성화시킵니다.
Rising, Falling edges에서 동작되도록 설정합니다.
*/
RCC -> APB2ENR |= (1 << IOPGEN) | (1 << AFIOEN);
// GPIOG에 클럭을 공급하고, Alternate Funcion을 사용하기 위해 AFIO에 클럭을 공급합니다.
RCC -> APB1ENR |= (1 << 0);
// TIM2에 클럭을 공급합니다. (36 MHz)
AFIO -> EXTICR1 |= (6 << 0) | (6 << 4);
// PG0와 PG1을 EXTI0와 EXTI1에 연결합니다.
EXTI -> RTSR |= (1 << 0) | (1 << 1);
EXTI -> FTSR |= (1 << 0) | (1 << 1);
// EXTI0, 1을 rising, falling edge에서 발생되도록 합니다.
EXTI -> IMR |= (1 << 0) | (1 << 1);
// EXTI0, 1을 활성화할 준비를 합니다.
NVIC_ISER0 |= (1 << 6) | (1 << 7);
// NVIC에 EXTI0, 1을 연결하여 최종 활성화합니다.
}
PG0은 EXTI0에 PG1은 EXTI1에 연결되어야하기 때문에 EXTIInit()을 이에 맞게 작성했습니다.
그 다음으로 Handler의 기본 구조를 만들고 pulse를 저장할 변수를 지정합니다. (void EXTIInit(void);와 int main(void) {사이에 작성하세요.)
volatile long encoderPulse = 0;
void EXTI0_IRQHandler(void) {
}
void EXTI1_IRQHandler(void) {
}
volatile은 encoderPulse라는 변수가 인터럽트 내부에서 사용될 것이기 때문에 사용하는 것이라고 생각하시면 됩니다.
long 자료형을 사용한 이유는 모터 한 바퀴당 적개는 330(1체배), 많을 때는 1,320의 펄스가 발생하기 때문이며, CW로 회전할 땐 +, CCW로 회전할 땐 -로 계산하기 위해 부호가 있는 자료형으로 설정했습니다.
1체배
한 채널만 사용하며 rising / falling edge 둘 중 하나에서만 pulse를 측정합니다.
여기서는 PG0핀만 interrupt를 받아지게할 예정이기 때문에 관련 코드를 지웁니다. (PG1을 안 사용하는 게 아니라 Interrupt로 사용 안 하겠다는 의미입니다.)
void EXTIInit(void);
int main(void) {
GPIOG -> CRL = (4 << MODE0) /*| (4 << MODE1)*/;
EXTIInit();
}
void EXTIInit(void) {
/*
PG0, 1을 EXTI0, 1에 연결하고 NVIC에 연결하여 활성화시킵니다.
Rising, Falling edges에서 동작되도록 설정합니다.
*/
RCC -> APB2ENR |= (1 << IOPGEN) | (1 << AFIOEN);
// GPIOG에 클럭을 공급하고, Alternate Funcion을 사용하기 위해 AFIO에 클럭을 공급합니다.
RCC -> APB1ENR |= (1 << 0);
// TIM2에 클럭을 공급합니다. (36 MHz)
AFIO -> EXTICR1 |= (6 << 0) /*| (6 << 4)*/;
// PG0와 PG1을 EXTI0와 EXTI1에 연결합니다.
// *PG1을 사용하지 않을 것이기 때문에 (6 << 4)를 지웁니다.
EXTI -> RTSR |= (1 << 0)/* | (1 << 1)*/;
/* EXTI -> FTSR |= (1 << 0) | (1 << 1);*/
// EXTI0, 1을 rising, falling edge에서 발생되도록 합니다.
// *PG1을 사용하지 않을 것이기 때문에 (1 << 1)을 지웁니다.
// Rising edge에서만 interrupt를 발생시키기 위해 EXTI -> FTSR을 지웁니다.
EXTI -> IMR |= (1 << 0)/* | (1 << 1)*/;
// EXTI0, 1을 활성화할 준비를 합니다.
// *EXTI0만 사용할 것이기 때문에 (1 << 1)을 지웁니다.
NVIC_ISER0 |= (1 << 6)/* | (1 << 7)*/;
// NVIC에 EXTI0, 1을 연결하여 최종 활성화합니다.
// EXTI0만 사용할 것이기 때문에 (1 << 7)을 제거합니다.
}
EXTI0만 사용할 것이기 때문에 Handler도 하나만 사용하며, 그 내부는 다음과 같이 수정합니다.
volatile long encoderPulse = 0;
void EXTI0_IRQHandler(void) {
if (EXTI -> PR & (1 << 0)) {
// EXTI0가 발생되었는지 확인합니다. //EXTI9_5 같은 인터럽트도 있기 때문에 존재
EXTI -> PR |= (1 << 0);
// 1을 write하면 0으로 되는 register이기 때문에 1을 write하여 인터럽트가 발생되었다는 것을 알립니다.
if(GPIOG -> IDR & (1 << 1)) {
// A채널이 Rising일 때 B채널이 High라면 -> CCW
encoderPulse--;
} else {
// A채널이 Rising일 때 B채널이 Low라면 -> CW
encoderPulse++;
}
}
}
/*
void EXTI1_IRQHandler(void) {
}*/
설명했던 것처럼 A가 rising일 때 B가 High이면 CCW, Low이면 CW입니다. (EXTI0_IRQHandler는 A가 rising일 때 발생된다는 것을 잊지 마세요.)
이렇게 보면 너무 간단하죠? 하지만 이게 정말 끝입니다.
다음 코드는 1체배 모드로 엔코더 모터에서 펄스를 읽는 코드입니다.
void EXTIInit(void);
volatile long encoderPulse = 0;
void EXTI0_IRQHandler(void) {
if (EXTI -> PR & (1 << 0)) {
// EXTI0가 발생되었는지 확인합니다. //EXTI9_5 같은 인터럽트도 있기 때문에 존재
EXTI -> PR |= (1 << 0);
// 1을 write하면 0으로 되는 register이기 때문에 1을 write하여 인터럽트가 발생되었다는 것을 알립니다.
if(GPIOG -> IDR & (1 << 1)) {
// A채널이 Rising일 때 B채널이 High라면 -> CCW
encoderPulse--;
} else {
// A채널이 Rising일 때 B채널이 Low라면 -> CW
encoderPulse++;
}
}
}
int main(void) {
GPIOG -> CRL = (4 << MODE0);
EXTIInit();
while(1) {
//encoderPulse를 확인할 수 있는 코드 삽입
}
}
void EXTIInit(void) {
/*
PG0을 EXTI0에 연결하고 NVIC에 연결하여 활성화시킵니다.
Rising edges에서 동작되도록 설정합니다.
*/
RCC -> APB2ENR |= (1 << IOPGEN) | (1 << AFIOEN);
RCC -> APB1ENR |= (1 << 0);
AFIO -> EXTICR1 |= (6 << 0);
EXTI -> RTSR |= (1 << 0);
EXTI -> IMR |= (1 << 0);
NVIC_ISER0 |= (1 << 6);
}
2체배
두 채널을 사용하여 구현하는 것으로, 1체배 코드를 EXTI1에도 응용해서 적용하면 됩니다.
EXTI0 handler코드는 동일하며, EXTIInit에서 PG1을 EXTI1에 등록하고 활성화하고, PG1을 floating input으로 설정했다는 점, EXTI1은 EXTI0 handler를 응용했다는 점이 1체배 방식과 다릅니다.
void EXTIInit(void);
volatile long encoderPulse = 0;
void EXTI0_IRQHandler(void) {
if (EXTI -> PR & (1 << 0)) {
// EXTI0가 발생되었는지 확인합니다. //EXTI9_5 같은 인터럽트도 있기 때문에 존재
EXTI -> PR |= (1 << 0);
// 1을 write하면 0으로 되는 register이기 때문에 1을 write하여 인터럽트가 발생되었다는 것을 알립니다.
if(GPIOG -> IDR & (1 << 1)) {
// A채널이 Rising일 때 B채널이 High라면 -> CCW
encoderPulse--;
} else {
// A채널이 Rising일 때 B채널이 Low라면 -> CW
encoderPulse++;
}
}
}
void EXTI1_IRQHandler(void) {
if (EXTI -> PR & (1 << 1)) {
// EXTI1가 발생되었는지 확인합니다. //EXTI9_5 같은 인터럽트도 있기 때문에 존재
EXTI -> PR |= (1 << 1);
// 1을 write하면 0으로 되는 register이기 때문에 1을 write하여 인터럽트가 발생되었다는 것을 알립니다.
if(GPIOG -> IDR & (1 << 0)) {
// B채널이 Rising일 때 A채널이 High라면 -> CCW
encoderPulse++;
} else {
// B채널이 Rising일 때 A채널이 Low라면 -> CW
encoderPulse--;
}
}
}
int main(void) {
GPIOG -> CRL = (4 << MODE0) | (4 << MODE1);
EXTIInit();
}
void EXTIInit(void) {
/*
PG0, 1을 EXTI0, 1에 연결하고 NVIC에 연결하여 활성화시킵니다.
Rising, Falling edges에서 동작되도록 설정합니다.
*/
RCC -> APB2ENR |= (1 << IOPGEN) | (1 << AFIOEN);
RCC -> APB1ENR |= (1 << 0);
AFIO -> EXTICR1 |= (6 << 0) | (6 << 4);
EXTI -> RTSR |= (1 << 0) | (1 << 1);
EXTI -> IMR |= (1 << 0) | (1 << 1);
NVIC_ISER0 |= (1 << 6) | (1 << 7);
}
주의해야할 점은 EXTI1에서는 encoderPulse의 가감하는 때가 다르다는 것입니다. (EXTI0코드와 비교해 보세요)
4체배
2체배 방식에서 falling edge까지 고려한 것으로, 정확도는 가장 높지만, 저성능 MCU에서는 동작하지 않을 수 있는 방식입니다.
void EXTIInit(void);
volatile long encoderPulse = 0;
void EXTI0_IRQHandler(void) {
if (EXTI -> PR & (1 << 0)) {
// EXTI0가 발생되었는지 확인합니다. //EXTI9_5 같은 인터럽트도 있기 때문에 존재
EXTI -> PR |= (1 << 0);
// 1을 write하면 0으로 되는 register이기 때문에 1을 write하여 인터럽트가 발생되었다는 것을 알립니다.
if(GPIOG -> IDR & (1 << 0)) { // A채널이 rising일 때 -> 기존 코드를 내부에 복붙
if(GPIOG -> IDR & (1 << 1)) {
// A채널이 Rising일 때 B채널이 High라면 -> CCW
encoderPulse--;
} else {
// A채널이 Rising일 때 B채널이 Low라면 -> CW
encoderPulse++;
}
} else { // A채널이 falling일 때
if(GPIOG -> IDR & (1 << 1)) {
// A채널이 Falling일 때 B채널이 High라면 -> CW
encoderPulse++;
} else {
// A채널이 Falling일 때 B채널이 Low라면 -> CCW
encoderPulse--;
}
}
}
}
void EXTI1_IRQHandler(void) {
if (EXTI -> PR & (1 << 1)) {
// EXTI1가 발생되었는지 확인합니다. //EXTI9_5 같은 인터럽트도 있기 때문에 존재
EXTI -> PR |= (1 << 1);
// 1을 write하면 0으로 되는 register이기 때문에 1을 write하여 인터럽트가 발생되었다는 것을 알립니다.
if(GPIOG -> IDR & (1 << 1) { // B채널이 rising일 때 -> 기존 코드를 내부에 복붙
if(GPIOG -> IDR & (1 << 0)) {
// B채널이 Rising일 때 A채널이 High라면 -> CCW
encoderPulse++;
} else {
// B채널이 Rising일 때 A채널이 Low라면 -> CW
encoderPulse--;
}
} else {
if(GPIOG -> IDR & (1 << 0)) {
// B채널이 Rising일 때 A채널이 High라면 -> CCW
encoderPulse--;
} else {
// B채널이 Rising일 때 A채널이 Low라면 -> CW
encoderPulse++;
}
}
}
}
int main(void) {
GPIOG -> CRL = (4 << MODE0) | (4 << MODE1);
EXTIInit();
}
void EXTIInit(void) {
/*
PG0, 1을 EXTI0, 1에 연결하고 NVIC에 연결하여 활성화시킵니다.
Rising, Falling edges에서 동작되도록 설정합니다.
*/
RCC -> APB2ENR |= (1 << IOPGEN) | (1 << AFIOEN);
RCC -> APB1ENR |= (1 << 0);
AFIO -> EXTICR1 |= (6 << 0) | (6 << 4);
EXTI -> RTSR |= (1 << 0) | (1 << 1);
EXTI -> FTSR |= (1 << 0) | (1 << 1);
EXTI -> IMR |= (1 << 0) | (1 << 1);
NVIC_ISER0 |= (1 << 6) | (1 << 7);
}
Continue
https://kmong.com/gig/465716?selfMarketingCode=HskZcR53S1
'stm32 > 실전' 카테고리의 다른 글
엔코더 모터 제어 (3. PID 제어로 속도 제어 [PID 함수들 만들기 Part 01]) (8) | 2021.04.01 |
---|---|
엔코더 모터 제어 (2. 속도 계산) (4) | 2021.03.16 |
엔코더 모터 제어 (0. 소개) (5) | 2021.02.28 |
02 쉬운 stm32 버튼 제어 (0) | 2018.11.20 |
01. 쉬운 stm32 GPIO, LED제어 (0) | 2018.11.19 |