Разное и полезное > Микроконтроллеры и их программирование

Как работать с энкодером

(1/7) > >>

blindman:
Меня попросили объяснить, как работать с энкодером. Считаю, что эта информация может быть полезна начинающим программистам. Поэтому публикую здесь

Я рассмотрю принципы работы с наиболее распространенными механическими энкодерами, имеющими фиксированные положения вала ("щелчки") - 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 мс, иначе будут пропуски щелчков, и другие неприятные вещи

WolfTheGrey:
Что такое один щелчек на импульс и 2 щелчка на импульс? Что такое импульс?
И у тебя схема не правильная, когда крутиш в одну сторону, то сперва замыкается на землю In0, потом In1, потом оба сбрасываются.
Когда крутишь в обратную сторону, то In0 и In1 сразу замыкаются на землю, потом размыкается In1 , а после него размыкается In0.

Основываясь на это, вывод In0 прицепляют к ТС, и заводят событие по прерыванию. Где смотрят, если In1 замкнут, то уменьшают, если нет, то увеличивают.

blindman:
WolfTheGrey, к сожалению не знаю имени. Я с энкодерами не одну конструкцию сделал, и с механическими, и с оптическими. и с теми что рукой крутят, и на валах двигателей с большой частотой вращения. Схема правильная. Если бы энкодеры работали так, как ты описываешь, то подавить дребезг контактов было бы невозможно в принципе.

Импульс - это и есть импульс. Применительно к энкодеру -  замкнутое состояние контакта.  А щелчок,или клик - это устойчивое состояние вала, шаг. Можешь почувствовать эти клики, покрутив колёсико мышки.
1 клик на импульс - это значит что чтобы контакт сначала разомкнулся, а потом замкнулся, нужно повернуть вал на 1 шаг
2 клика на импульс - это значит что чтобы контакт сначала разомкнулся, а потом замкнулся, нужно повернуть вал на 2 шага

lgedmitry:
Таки разобрался. Хоть и долго провозился, ибо Андрей не по-нашему программирует ??? :d_know: ??? :d_know:
Ну не учили нас таким языкам. Вот Турбопаскаль 5.0 - учили, но очень давно. CoDeSyS учили совсем недавно, но очень сжато. Ещё был Бейсик для ROBOTRON-1715, но ещё давнее, чем турбопаскаль.
Вот что вышло в итоге:
Tim0_isr:                                                   'частотомер для измерения скорости сэмплирования по счётчику 1, подключенному к выходу lrclk dir9001
If Pinb.6 = 0 Then If Timer1 > 102 Then Let Freq = 1 Else Let Freq = 0
Let Timer1 = 0
                                                            'энкодер по-хабаровски
If Enco <> Pind.6 Then
Let Enc1 = Enc
Let Enc = Pind
Let Enc = Enc And &B11000000

   If Enc = &HC0 And Enc1 = 0 Then
      If Volprint1 > 0 Then Decr Volprint1                                            'энкодер висит на PIND.6 PIND.7, общим контактом на корпус
      End If
   If Enc = &H40 And Enc1 = &H80 Then
      If Volprint1 < 80 Then Incr Volprint1
      End If
   End If
   Let Enco = Pind.6
Return

Работает на уррра!
А встроенная в BASCOM энкодеропроцедура почему-то работает через  _!_

Андрей, спасибище! :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v: :v:

blindman:

--- Цитата: lgedmitry от 27 Февраля 2013, 11:57:08 ---Андрей не по-нашему программирует ??? :d_know: ??? :d_know:
Ну не учили нас таким языкам
--- Конец цитаты ---
Меня тоже не учили, вот так и разбирался, глядя на чужой код. Еще в школе когда учился, была у меня книжка про контроллеры 8051. Но ни самих контроллеров, ни тем более компьютера было не купить. Так в одной тетрадке рисовалась схема, а другая использовалась в качестве отладчика программ. Слева код на ассемблере, справа состояние переменных на каждом шаге. Сколько я таких тетрадок исписал - не сосчитать  :%):

Сергей, я рад, что все получилось! А DIR9001 всегда на выход импульсы выдает, даже если входного сигнала нет?

Навигация

[0] Главная страница сообщений

[#] Следующая страница

Перейти к полной версии