В этой статье я расскажу о энкодерах и попытаюсь объяснить, как их правильно подключить и обработать с помощью микроконтроллера AVR (в примерах я использую ATmega8A-PU, но он должен работать и на любом другом микроконтроллере, например, ATmega32 или Arduino Compatible ATmega168/328).
Contents
Немного теории
Инкрементальные энкодеры имеют два выхода, назовем их А и В. Когда мы поворачиваем ручку, мы получаем фазовый сдвиг прямоугольной формы на выходах А и В. Этот сигнал представляет собой не что иное, как двухбитный код Грея. На изображении ниже я нарисовал его в более читаемом виде.
Как мы видим на изображении, если энкодер повернуть по часовой стрелке, код Грея на выходах будет: 2->3->1->0->2 и так далее. Если мы начнем вращаться против часовой стрелки, то на выходе получим следующую последовательность кода Грея: 3->2->0->1. Зная эту последовательность, можно определить направление вращения ручки. Это один из двух способов считывания направления энкодера.
Второй способ основан на обнаружении фронта спада сигнала на одном из выходов и проверке текущего состояния на другом выходе.

У этого метода есть очень важная проблема. Мы теряем половину точности поворотного энкодера, потому что обнаруживаем только один фронт сигнала из двух. Это можно решить, подключив второй выход энкодера к другому прерыванию в процессоре и обнаружив противоположный фронт сигнала. Таким образом, если мы обнаруживаем задний фронт на выходе A, мы должны обнаружить нарастающий фронт на выходе B.

Сделав это, мы восстановили нашу полную точность, но потеряли два прерывания в нашем микроконтроллере. Обычно нам не нужна такая точность, и достаточно первого решения с использованием одного прерывания на микроконтроллере.
Подключение к микроконтроллеру
Если вы используете дорогой оптический энкодер, этот элемент не очень важен, потому что этот тип энкодера подает на выходы очень чистый сигнал, и нет необходимости его фильтровать или отражать. К сожалению, когда мы используем дешевые механические энкодеры, ситуация совсем другая. Внутренние контакты этих небольших устройств генерируют много помех и шумов, и когда мы превышаем их максимальные параметры, что может быть очень плохо (например, максимальную скорость вращения), мы начинаем иметь на выходах случайные сигналы.
Когда я подключаю один из этих энкодеров к микроконтроллеру, я всегда использую аппаратное устранение дребезга, потому что программное устранение дребезга может быть очень сложным и зависит от некоторых параметров, таких как текущая скорость вала и т. д. Мой debounce представляет собой простой низкочастотный 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 будет первым MSB, а значение с PD3 будет MSB. 0 .
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 в значении переменной Это значение может быть 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;
}
Полный код (включая make-файл, исходный код и заголовочные файлы) вместе с uart.h и соответствующим uart.c можно найти в репозитории GitHub здесь: https://github.com/leuconoeSH/avr-examples/tree/master/rotary-encoder/normal.
Заключение
Как видите, обработка поворотного инкрементного энкодера на самом деле очень проста, и большая проблема заключается в качестве самого энкодера и его дребезге. Решение состоит в использовании оптического энкодера, но это очень дорогое решение. Например, оптический энкодер может стоить около 100 евро (~ 140 долларов), а дешевый механический энкодер стоит всего несколько центов. Так что остается только пожелать вам, мои дорогие читатели, удачных опытов и проектов с энкодерами!