В данной статье расскажу об энкодерах и попытаюсь объяснить, как их подключать и обрабатывать правильным способом с помощью микроконтроллера AVR (в примерах я использую ATmega8A-PU, но это должно работать на любом другом микроконтроллере, например, на ATmega32 или совместимом с Arduino ATmega168/328).
Немного теории
Инкрементальные энкодеры имеют два выхода, назовем их A и B. Когда мы вращаем ручку, на выходах A и B мы получаем фазовый сдвиг квадратичного сигнала. Этот сигнал представляет собой ничто иное, как двухбитный код Грея. На изображении ниже я нарисовал это в более читабельном виде.
Как мы видим на изображении, если энкодер поворачивается по часовой стрелке, то код Грея на выходах будет следующим: 2->3->1->0->2 и так далее. Если мы начнем поворачивать против часовой стрелки, мы получим следующую последовательность кода Грея на выходе: 3->2->0->1. Зная эту последовательность, мы можем определить направление вращения ручки. Это один из двух способов чтения направления энкодера.
Второй метод основан на обнаружении фронта спадающего сигнала на одном из выходов и проверки текущего состояния на другом выходе.
У этого метода есть одна очень важная проблема. Мы теряем половину точности вращательного энкодера, потому что мы обнаруживаем только каждый второй фронт сигнала. Это можно исправить, подключив второй выход энкодера к другому прерыванию в процессоре и обнаруживая противоположный фронт сигнала. Таким образом, если мы обнаруживаем спадающий фронт на выходе A, нам нужно обнаружить нарастающий фронт на выходе B.
Сделав это, мы восстановили нашу полную точность, но потеряли два прерывания в нашем микроконтроллере. Обычно нам не нужна такая высокая точность, и первое решение с использованием только одного прерывания на микроконтроллере достаточно хорошо.
Подключение к микроконтроллеру
Если вы используете дорогой оптический энкодер, то этот пункт не очень важен, потому что этот тип энкодеров обеспечивает очень чистый сигнал на выходах, и нет необходимости его фильтровать или подавлять дребезг. К сожалению, когда мы используем дешевые механические энкодеры, ситуация сильно отличается. Внутренние контакты этих маленьких устройств генерируют много помех и шумов, и когда мы превышаем их максимальные параметры, что может быть очень плохо (например, максимальная скорость вращения), мы начинаем получать случайные сигналы на выходах.
Когда я подключаю один из этих энкодеров к микроконтроллеру, я всегда использую аппаратное подавление дребезга, потому что программное подавление дребезга может быть очень сложным и зависит от некоторых параметров, таких как текущая скорость вращения вала и т.д. Мое подавление дребезга - это простой RC-фильтр низкой частоты, как показано на рисунке ниже.
На схеме выше резистор и конденсатор, подключенные параллельно, создают фильтр низких частот с частотой среза, рассчитанной по следующей формуле: 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 долларов США), в то время как дешевый механический стоит всего несколько центов. Так что не остается ничего другого, кроме как пожелать вам, мои дорогие читатели, множество успешных экспериментов и проектов с энкодерами!
Комментарии (9)
RigidStyle
16.04.2023 15:04Есть же магнитные еще энкодеры. Линейные стоят тоже несколько долларов. С кольцевыми сложнее. Но кольцевые оптические можно найти надорогие, или просто сделать из пары датчиков концевых и кружка с дырками, что стоит меньше доллара в сумме.
zatim
16.04.2023 15:04+1Кстати, есть аппаратные декодеры, например LS7366R и аналоги из того же семейства. Обрабатывать программно можно только, наверное, ручной энкодер, который используется пользователем для ввода информации с какого-нибудь пульта и который вращается относительно медленно.
Если необходимо обрабатывать квадратурные сигналы с какого-нибудь датчика вращения или станочного энкодера с разрешением 2500 имп/оборот, скорости микроконтроллера может и не хватить и он начнет пропускать импульсы или же будет заниматься только этим.
Аппаратный декодер можно собрать также на нескольких микросхемах ттлш логики.
nixtonixto
16.04.2023 15:04+2Аппаратный декодер есть во всех микроконтроллерах типа STM32, скорости его таймера хватит для любого механического энкодера.
gleb_l
16.04.2023 15:04+2Сделать диодное ИЛИ, и пустить выход на прерывание - будет любой фронт. На обычный входной порт пустить выход любого из каналов до диода. Итого - пришло прерывание - значит обнаружен любой фронт. Смотрим потенциал - если там 1, значит фронт был с CW (или CCW - в зависимости от того, как подключили) вывода. Этой информации достаточно, чтобы раскодировать направление вращения.
Kudesnick33
16.04.2023 15:04+1Не получится, переходы с 10 или 01 на 11 не будут детектироваться.
gleb_l
16.04.2023 15:04Да-да, я внимательно посмотрел и понял, что если вход прерывания только один - то нужны и диоды, и потенциальный сигнал с обеих выходов. Теперь сижу думаю, нельзя ли соорудить какую-нибудь диодно-конденсаторную обвязку, чтобы по полярности импульса сразу судить о направлении вращения..
PS - если бы мог - сам бы себе поставил за предыдущий коммент минус
devprodest
16.04.2023 15:04Второй метод использует код Грея. В этом случае нам не нужны никакие прерывания вообще.
чтобы запускать эту процедуру в прерывании каждый раз, когда счётчик переполняется
Метод без прерываний требует прерываний ????
То есть либо тратим такты на бесполезную обработку в основном цикле, либо такты в бесполезной обработке таймера, даже если энкодер не двигается.
Уж лучше в прерывании от ноги все это делать
cujos
перевод статьи как закрутить лампочку... с помощью домкрата...
вместо кучи условий можно сделать таблицу размером 16 и состояниями +1, -1, 0 и fault - на вход подаются 4 бита: предыдущее состояние и новое, ну и логичнее считать переменную внутри, а не слать в уарт
fault тоже желательно обрабатывать, потому как ошибка счетчика - выдача неверного положения с кучей проблем типа поломка контуров foc, ПИД регуляторов и исполнительных устройств в принципе