Меня попросили объяснить, как работать с энкодером. Считаю, что эта информация может быть полезна начинающим программистам. Поэтому публикую здесь
Я рассмотрю принципы работы с наиболее распространенными механическими энкодерами, имеющими фиксированные положения вала ("щелчки") - 1 или 2 на импульс. В таком энкодере имеются 2 механических контакта. При равномерном вращении вала, ток через 2 контакта будет иметь вид прямоугольных импульсов, сдвинутых относительно друг друга приблизительно на 90 градусов, отсюда и название для таких энкодеров - квадратурные:
(Извините, но у Вас нет доступа в Галерею)
Можно заметить также, что такой энкодер выдает двоичный код Грея, то есть каждое последующее состояние отличается от предыдущего только одним битом.
Наиболее распространены механические энкодеры с 1 или 2 "щелчками" - устойчивыми положениями вала - на импульс. У первых, вал находится в устойчивом состоянии, когда оба контакта разомкнуты, у вторых - еще и когда оба контакта замкнуты. На рисунке устойчивые положения отмечены красными линиями.
Самый простой способ опроса энкодера: в прерывании по фронту (или спаду) импульса от одного из контактов, опрашивается состояние второго контакта, если он замкнут - то вращение в одну сторону, если разомкнут - в другую. Если внимательно посмотреть на рисунок, станет ясно, что такой способ будет работать с энкодером с 1 щелчком на импульс. С 2 щелчками будет распознаваться только каждый второй щелчок. Чтобы это исправить, с такими энкодерами надо использовать прерывание, срабатывающее и по фронту, и по спаду, и в прерывании считывать состояния обоих контактов. Если они одинаковые - вращение в одну сторону, если разные - в другую.
Вроде бы все просто, но есть одно НО - механические контакты имеют дребезг, поэтому будем иметь множество ложных срабатываний. Мой опыт показывает что контакты энкодеров стабилизируются в течение 2-5 мс. Способ борьбы с этим явлением общеизвестен - если с момента предыдущего прерывания прошло меньше определенного времени, то считаем, что имеет место дребезг, и ничего не делаем. Этот способ требует одновременно использования таймера и внешних прерываний. Многовато для такой простой задачи.
Я использую другой способ борьбы с дребезгом контактов энкодера, позволяющий во многих случаях обойтись вообще без прерываний. Основан он на том, что состояние контактов меняется как минимум дважды между щелчками. Представим каждое состояние контактов энкодера в виде числа. У нас 2 контакта, значит всего может быть 4 состояния, от 0 до 3.
Напишем процедуру, которая будет вызваться периодически, и проверять, изменилось ли состояние энкодера с момента предыдущего вызова. Если изменилось - то проверяем, из какого состояние перешли в текущее. Для энкодера с 1 щелчком на импульс, изменение состояния нужно фиксировать только если изменилось состояние одного из контактов. Для энкодера с 2 щелчками на импульс - при изменении состояния любого из контактов.
Посмотрим на рисунок. Для энкодера с 1 щелчком на импульс, вращению по часовой стрелке соответствует переход из состояния 0 в состояние 3, а против часовой - из 2 в 1. Может возникнуть вопрос - откуда взялся переход из 3 в 0, если по рисунку идет сначала переход из 3 в 1, и потом из 1 в 0? Дело в том, что переход из 3 в 1 мы "не увидим", так так фиксируем смену состояния при переключении только одного из контактов. Как все это помогает бороться с дребезгом? Представим, что энкодер находится в устойчивом состоянии (3). Начинаем вращать вал по часовой стрелке, на 1 щелчок. Будут зафиксированы переходы 3-0-1-0-1-0-1-0-3-2-3-2-3-2-3. Переход из 0 в 3 мы увидели только один, то есть дребезг подавлен. Аналогично и при вращении в обратную сторону.
Для энкодера с 2 щелчками на импульс, процедура немного сложнее. Состояние энкодера надо фиксировать при переключении любого из контактов, и запоминать не одно, а 2 предыдущих состояния. Вращению в одну сторону соответствуют переходы 3-1-0 и 0-2-3, в другую - 0-1-3 и 3-2-0.
Но как сказано, "Talk is cheap. Show me the code"
Приведу пример программы работы с энкодером.
Первое. Контроллер ATMega32, энкодер с 1 щелчком на импульс, подключен к контактам PORTD2 и PORTD3. Контроллер этот выбран потому, что у меня есть с ним удобная макетка, на которой проверялся код. Перенести на другой контроллер думаю не составит большого труда.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>
#include <util/delay.h>
// предыдущие состояния энкодера
// в битах 0,1 - текущее состояние
// в битах 2,3 - состояние перед текущим изменением
static uint8_t encoder_state;
// количество подсчитанных щелчков энкодера.
// при вращении в одну сторону увеличивается, в другую - уменьшается.
// Переменная объявлена как volatile, потому что ее состояние изменяется
// в прерывании.
static volatile int encoder_pos;
// макрос для удобства. Распихивает 2 числа, соответствующих
// текущему, и предыдущему состояниям,
// по нужным битам
#define TR(x, y) (((x) << 2) | (y) )
// процедура сканирования энкодера
static void scan_encoder(void)
{
// считываем состояние энкодер
uint8_t pins = (PIND & 0x0C) >> 2;
// если состояние 0-го бита не изменилось с предыдущего вызова, ничего не делаем
if ((encoder_state & 1) == (pins & 1))
return;
// задвигаем новое состояние в переменную, забывая самое старое из
// 2 сохраненных состояний
encoder_state = ((encoder_state << 2) | pins) & 0x0F;
// проверяем условия, и изменяем количество щелчков, если нужно
if (encoder_state == TR(0,3)) {
encoder_pos++;
}
if (encoder_state == TR(2, 1)) {
encoder_pos--;
}
}
// прерывание от таймера, раз приблизительно в 2 мс
ISR(SIG_OVERFLOW0)
{
// вызываем опрос энкодера
scan_encoder();
}
// это основная программа
int main(void)
{
// на ногах PORTC5 и PORTB3 у меня светодиоды, настраиваю эти ноги как выходы
DDRC |= (1 << 5);
DDRB |= (1 << 3);
PORTD |= 0x0C; // подтяжка на входах, к которым подключен энкодер - но лучше использовать внешнюю подтяжку
// прескалер таймера0 = 64, при тактовой частоте 8 МГц,
// переполнение будет приблизительно раз в 2 мс
TCCR0 = 3 << CS00;
// разрешаю прерывание по переполнению таймера.
TIMSK |= 1 << TOIE0;
// глобальное разрешение прерываний
sei();
for (;;) {
// в бесконечном цикле, берем число, которое насчитала процедура
// обработки энкодера, и обнуляем. Делать это надо при запрещенных
// прерываниях, чтобы не сбился счет.
cli();
int n = encoder_pos;
encoder_pos = 0;
sei();
// и мигаем светодиодом столько раз, сколько щелчков насчитано
// Таймер продолжает тикать, энкодер опрошивается. Новое количество
// щелчков мы подхватим на следующем проходе цикла
while (n) {
if (n < 0) { // если против часовой - мигаем одним светодиодом
PORTC |= (1 << 5);
n++;
} else { // по часовой - другим
PORTB |= (1 << 3);
n--;
}
_delay_ms(30);
PORTC &= ~(1 << 5);
PORTB &= ~(1 << 3);
_delay_ms(30);
}
}
}
Второе. Контроллер ATMega32, энкодер с 2 щелчками на импульс, подключен к контактам PORTD2 и PORTD3.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>
#include <util/delay.h>
// предыдущие состояния энкодера
// в битах 0,1 - текущее состояние
// в битах 2,3 - состояние перед текущим изменением
// в битах 4,5 - состояние перед предыдущим изменением
static uint8_t encoder_state;
// количество подсчитанных щелчков энкодера.
// при вращении в одну сторону увеличивается, в другую - уменьшается.
// Переменная объявлена как volatile, потому что ее состояние изменяется
// в прерывании.
static volatile int encoder_pos;
// макрос для удобства. Распихивает 3 числа, соответствующих
// текущему, предыдущему и пред-предыдущему состояниям,
// по нужным битам
#define TR(x, y, z) (((x) << 4) | ((y) << 2) | (z) )
// процедура сканирования энкодера
static void scan_encoder(void)
{
// считываем состояние энкодер
uint8_t pins = (PIND & 0x0C) >> 2;
// если не изменилось с предыдущего вызова, ничего не делаем
if ((encoder_state & 3) == pins )
return;
// задвигаем новое состояние в переменную, забывая самое старое из
// 3 сохраненных состояний
encoder_state = ((encoder_state << 2) | pins) & 0x3F;
// проверяем условия, и изменяем количество щелчков, если нужно
if (encoder_state == TR(3, 1, 0) || encoder_state == TR(0,2,3)) {
encoder_pos++;
}
if (encoder_state == TR(0, 1, 3) || encoder_state == TR(3,2,0)) {
encoder_pos--;
}
}
// прерывание от таймера, раз приблизительно в 2 мс
ISR(SIG_OVERFLOW0)
{
// вызываем опрос энкодера
scan_encoder();
}
// это основная программа
int main(void)
{
// на ногах PORTC5 и PORTB3 у меня светодиоды, настраиваю эти ноги как выходы
DDRC |= (1 << 5);
DDRB |= (1 << 3);
PORTD |= 0x0C; // подтяжка на входах, к которым подключен энкодер - но лучше использовать внешнюю подтяжку
// прескалер таймера0 = 64, при тактовой частоте 8 МГц,
// переполнение будет приблизительно раз в 2 мс
TCCR0 = 3 << CS00;
// разрешаю прерывание по переполнению таймера.
TIMSK |= 1 << TOIE0;
// глобальное разрешение прерываний
sei();
for (;;) {
// в бесконечном цикле, берем число, которое насчитала процедура
// обработки энкодера, и обнуляем. Делать это надо при запрещенных
// прерываниях, чтобы не сбился счет.
cli();
int n = encoder_pos;
encoder_pos = 0;
sei();
// и мигаем светодиодом столько раз, сколько щелчков насчитано
// Таймер продолжает тикать, энкодер опрошивается. Новое количество
// щелчков мы подхватим на следующем проходе цикла
while (n) {
if (n < 0) { // если против часовой - мигаем одним светодиодом
PORTC |= (1 << 5);
n++;
} else { // по часовой - другим
PORTB |= (1 << 3);
n--;
}
_delay_ms(30);
PORTC &= ~(1 << 5);
PORTB &= ~(1 << 3);
_delay_ms(30);
}
}
}
В этих примерах для периодического вызова процедуры обработки энкодера, используется прерывание от таймера. Но можно обойтись и без него, если вызывать процедуру прямо в главном цикле программы - но для этого он должен быть достаточно шустрым. Между вызовами процедуры обработки энкодера должно быть не более 5 мс, иначе будут пропуски щелчков, и другие неприятные вещи