В данной статье расскажу об энкодерах и попытаюсь объяснить, как их подключать и обрабатывать правильным способом с помощью микроконтроллера AVR (в примерах я использую ATmega8A-PU, но это должно работать на любом другом микроконтроллере, например, на ATmega32 или совместимом с Arduino ATmega168/328).
Немного теории
Инкрементальные энкодеры имеют два выхода, назовем их A и B. Когда мы вращаем ручку, на выходах A и B мы получаем фазовый сдвиг квадратичного сигнала. Этот сигнал представляет собой ничто иное, как двухбитный код Грея. На изображении ниже я нарисовал это в более читабельном виде.
![](https://habrastorage.org/getpro/habr/upload_files/5c7/2bb/e95/5c72bbe9529919d8434b8219e4d1bee9.png)
Как мы видим на изображении, если энкодер поворачивается по часовой стрелке, то код Грея на выходах будет следующим: 2->3->1->0->2 и так далее. Если мы начнем поворачивать против часовой стрелки, мы получим следующую последовательность кода Грея на выходе: 3->2->0->1. Зная эту последовательность, мы можем определить направление вращения ручки. Это один из двух способов чтения направления энкодера.
Второй метод основан на обнаружении фронта спадающего сигнала на одном из выходов и проверки текущего состояния на другом выходе.
![](https://habrastorage.org/getpro/habr/upload_files/22b/cc8/d2e/22bcc8d2e103599040c003cc908da1b9.png)
У этого метода есть одна очень важная проблема. Мы теряем половину точности вращательного энкодера, потому что мы обнаруживаем только каждый второй фронт сигнала. Это можно исправить, подключив второй выход энкодера к другому прерыванию в процессоре и обнаруживая противоположный фронт сигнала. Таким образом, если мы обнаруживаем спадающий фронт на выходе A, нам нужно обнаружить нарастающий фронт на выходе B.
![](https://habrastorage.org/getpro/habr/upload_files/fed/a81/438/feda81438938ecdc8e7de8bcccc8bc28.png)
Сделав это, мы восстановили нашу полную точность, но потеряли два прерывания в нашем микроконтроллере. Обычно нам не нужна такая высокая точность, и первое решение с использованием только одного прерывания на микроконтроллере достаточно хорошо.
Подключение к микроконтроллеру
Если вы используете дорогой оптический энкодер, то этот пункт не очень важен, потому что этот тип энкодеров обеспечивает очень чистый сигнал на выходах, и нет необходимости его фильтровать или подавлять дребезг. К сожалению, когда мы используем дешевые механические энкодеры, ситуация сильно отличается. Внутренние контакты этих маленьких устройств генерируют много помех и шумов, и когда мы превышаем их максимальные параметры, что может быть очень плохо (например, максимальная скорость вращения), мы начинаем получать случайные сигналы на выходах.
Когда я подключаю один из этих энкодеров к микроконтроллеру, я всегда использую аппаратное подавление дребезга, потому что программное подавление дребезга может быть очень сложным и зависит от некоторых параметров, таких как текущая скорость вращения вала и т.д. Мое подавление дребезга - это простой RC-фильтр низкой частоты, как показано на рисунке ниже.
![](https://habrastorage.org/getpro/habr/upload_files/a0c/7e4/ce2/a0c7e4ce2789dd24bf17b8264c5bdeab.png)
На схеме выше резистор и конденсатор, подключенные параллельно, создают фильтр низких частот с частотой среза, рассчитанной по следующей формуле: fg = 1 / 2*pi*R*C. Таким образом, если мы используем значения из схемы, мы получим:
fg = 1 / 2*3.1415*10000*0.0000001 ~= 159.2Hz
Решение на основе прерываний (пример на основе ATmega8)
Первый пример - это решение на основе прерываний, в котором мы обнаруживаем падающий и/или нарастающий фронт, и в зависимости от текущего состояния на втором пине мы определяем направление вращения вала.
Если мы подключим выходы A и B энкодера к пинам PD2 и PD3 микроконтроллера, нам нужно установить PD2 и PD3 как входы:
/* set PD2 and PD3 as input */
DDRD &=~ (1 << PD2); /* PD2 and PD3 as input */
DDRD &=~ (1 << PD3);
PORTD |= (1 << PD3)|(1 << PD2); /* PD2 and PD3 pull-up enabled */
Затем мы включаем обработчик прерываний. Для этого нам нужно записать следующие биты в регистры GICR и MCUCR:
GICR |= (1<<INT0)|(1<<INT1); // enable INT0 and INT1
MCUCR |= (1<<ISC01)|(1<<ISC11)|(1<<ISC10); // INT0 - falling edge, INT1 - raising edge
И мы также должны включить прерывания:
/* enable interrupts */
sei();
Теперь мы должны написать некоторый код в наших обработчиках прерываний:
//INT0 interrupt
ISR(INT0_vect )
{
if(!bit_is_clear(PIND, PD3))
{
UART_putchar(*PSTR("+"));
}
else
{
UART_putchar(*PSTR("-"));
}
}
//INT1 interrupt
ISR(INT1_vect )
{
if(!bit_is_clear(PIND, PD2))
{
UART_putchar(*PSTR("+"));
}
else
{
UART_putchar(*PSTR("-"));
}
}
Код для обработчика INT0 и INT1 очень похож. После возникновения прерывания, мы проверяем состояние второго входа, и это определяет текущее направление вала. В приведенном выше примере, если вал энкодера был повернут в правую сторону, отправляется символ "+", а если нет, то символ "-". Если мы удалим одно прерывание (INT0 или INT1 - это не имеет значения), следующий код все еще будет функциональным, но мы потеряем половину точности энкодера. Весь код выглядит следующим образом:
#define F_CPU 8000000
#define UART_BAUD 9600 /* serial transmission speed */
#define UART_CONST F_CPU/16/UART_BAUD-1
#include <stdio.h>
#include <avr/io.h>
#include <util/delay.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include "uart.h"
//INT0 interrupt
ISR(INT0_vect )
{
if(!bit_is_clear(PIND, PD3))
{
UART_putchar(*PSTR("+"));
}
else
{
UART_putchar(*PSTR("-"));
}
}
//INT1 interrupt
ISR(INT1_vect )
{
if(!bit_is_clear(PIND, PD2))
{
UART_putchar(*PSTR("+"));
}
else
{
UART_putchar(*PSTR("-"));
}
}
int main(void)
{
/* init uart */
UART_init(UART_CONST);
/* set PD2 and PD3 as input */
DDRD &=~ (1 << PD2); /* PD2 and PD3 as input */
DDRD &=~ (1 << PD3);
PORTD |= (1 << PD3)|(1 << PD2); /* PD2 and PD3 pull-up enabled */
GICR |= (1<<INT0)|(1<<INT1); /* enable INT0 and INT1 */
MCUCR |= (1<<ISC01)|(1<<ISC11)|(1<<ISC10); /* INT0 - falling edge, INT1 - reising edge */
/* enable interrupts */
sei();
while(1)
{
//do nothing ;)
_delay_ms(1);
}
return 0;
}
Этот пример и все соответствующие файлы (makefile, исходный код и заголовочные файлы) свободно доступны в репозитории на GitHub здесь: https://github.com/leuconoeSH/avr-examples/tree/master/rotary-encoder/interrupt. В репозитории также содержатся процедуры для обработки передачи UART.
Метод кода Грея
Второй метод использует код Грея. В этом случае нам не нужны никакие прерывания вообще.
Давайте начнем с процедуры, которая преобразует два состояния на входах A и B (пин PD2 и пин PD3) в двоичное значение из 2 битов, где значение с PD2 будет первым старшим битом, а значение с PD3 будет нулевым старшим битом.
uint8_t read_gray_code_from_encoder(void )
{
uint8_t val=0;
if(!bit_is_clear(PIND, PD2))
val |= (1<<1);
if(!bit_is_clear(PIND, PD3))
val |= (1<<0);
return val;
}
В коде выше мы объявляем 8-битную беззнаковую переменную с начальным значением 0 (00000000b), затем проверяем, есть ли высокий логический уровень на пине PD2, и если есть, то ставим двоичную 1 на бит, который первый справа. Таким образом, мы получаем это значение: 00000010b. Затем мы делаем то же самое со вторым входным пином PD3, единственное отличие состоит в том, что теперь мы устанавливаем значение на позиции 0. Это дает нам 2-битовый код Грея, соответствующий состояниям на входах PD2 и PD3, в переменной val. Это значение может быть равно 0 (00b), 1 (01b), 2(10b) или 3 (11b).
Затем нам просто нужно записать это значение как начальное, и проверить, соответствует ли новое значение последовательности 2->3->1->0 или последовательности 3->2->0->1, после чего мы узнаем, в каком направлении повернулся вал нашего энкодера.
/* ready start value */
val = read_gray_code_from_encoder();
while(1)
{
val_tmp = read_gray_code_from_encoder();
if(val != val_tmp)
{
if( /*(val==2 && val_tmp==3) ||*/
(val==3 && val_tmp==1) ||
/*(val==1 && val_tmp==0) ||*/
(val==0 && val_tmp==2)
)
{
UART_putchar(*PSTR("+"));
}
else if( /*(val==3 && val_tmp==2) ||*/
(val==2 && val_tmp==0) ||
/*(val==0 && val_tmp==1) ||*/
(val==1 && val_tmp==3)
)
{
UART_putchar(*PSTR("-"));
}
val = val_tmp;
}
_delay_ms(1);
}
Используя код выше, последовательности 2->3, 1->0, 3->2 и 0->1 были закомментированы, потому что они соответствуют переходному состоянию энкодера, и если бы мы оставили их раскомментированными, то каждый отдельный "клик" энкодера генерировал бы два импульса.
Если мы не хотим включать процедуры энкодера в основной цикл нашей программы, мы можем использовать внутренний таймер/счётчик, чтобы запускать эту процедуру в прерывании каждый раз, когда счётчик переполняется. Весь код будет выглядеть так:
#define F_CPU 8000000 /* crystal f */
#define UART_BAUD 9600 /* serial transmission speed */
#define UART_CONST F_CPU/16/UART_BAUD-1
#include <stdio.h>
#include <avr/io.h>
#include <util/delay.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include "uart.h"
uint8_t read_gray_code_from_encoder(void )
{
uint8_t val=0;
if(!bit_is_clear(PIND, PD2))
val |= (1<<1);
if(!bit_is_clear(PIND, PD3))
val |= (1<<0);
return val;
}
int main(void)
{
uint8_t val=0, val_tmp =0;
/* init UART */
UART_init(UART_CONST);
/* set PD2 and PD3 as input */
DDRD &=~ (1 << PD2); /* PD2 and PD3 as input */
DDRD &=~ (1 << PD3);
PORTD |= (1 << PD3)|(1 << PD2); /* PD2 and PD3 pull-up enabled */
/* ready start value */
val = read_gray_code_from_encoder();
while(1)
{
val_tmp = read_gray_code_from_encoder();
if(val != val_tmp)
{
if( /*(val==2 && val_tmp==3) ||*/
(val==3 && val_tmp==1) ||
/*(val==1 && val_tmp==0) ||*/
(val==0 && val_tmp==2)
)
{
UART_putchar(*PSTR("+"));
}
else if( /*(val==3 && val_tmp==2) ||*/
(val==2 && val_tmp==0) ||
/*(val==0 && val_tmp==1) ||*/
(val==1 && val_tmp==3)
)
{
UART_putchar(*PSTR("-"));
}
val = val_tmp;
}
_delay_ms(1);
}
return 0;
}
Весь код (включая makefile, исходный код и заголовочные файлы), а также uart.h и соответствующий uart.c, можно найти в репозитории GitHub здесь: https://github.com/leuconoeSH/avr-examples/tree/master/rotary-encoder/normal.
Заключение
Как вы можете видеть, обработка вращающегося инкрементального энкодера на самом деле очень проста, и большой проблемой является качество самого энкодера и его дребезг. Решением является использование оптического энкодера, но это очень дорогое решение. Например, оптический энкодер может стоить около 100 евро (~140 долларов США), в то время как дешевый механический стоит всего несколько центов. Так что не остается ничего другого, кроме как пожелать вам, мои дорогие читатели, множество успешных экспериментов и проектов с энкодерами!