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

Работа микроконтроллера с энкодером

Энкодер - штука, внешне похожая на переменный резистор, но, в отличие от последнего, не имеет ограничителей и может вращаться в любую сторону бесконечно. С помощью энкодера очень удобно организовывать всякие экранные меню, вообще, один “нажимабельный” энкодер (т.е., если он умеет работать ещё и как кнопка) идеально подходит для для организации одномерных циклических меню.

Энкодеры бывают двух типов: абсолютные - сразу выдающие код угла поворота и инкрементальные - выдающие импульсы при вращении. Для последних подсчётом импульсов и их преобразованием их в угол поворота должен заниматься микроконтроллер.

С точки зрения конструкции энкодеры бывают механические и оптические, в первых импульсы при вращении генерируются на паре контактов при их замыкании скользящим контактом вала, во вторых же роль контактов выполняют фотодиоды, а роль замыкателя - светодиод, светящий через диск с прорезями (привет шариковой мышке).

Хотя информации по программированию энкодеров в сети навалом, как и готовых библиотек для этого, но все они какие-то излишне громозкие (имхо) - опрос состояния, как правило, реализуется в виде конечного автомата в виде блока switch с вложенными if-ами, что выглядит несколько сложно (особенно, будучи написанным на ассмеблере). Хотя, реализация может быть проще.

Наибольшей популярностью в народном хозяйстве пользуются дешёвые механические инкрементальные энкодеры, их и рассмотрим. Процесс вращения вала энкодера схематично показан на рисунке (сверху - вращение по часовой стрелке, снизу - против):

Диаграммы вращения энкодера

Тут А и В - это те самые контакты, уровни на которых предстоит обрабатывать микроконтроллеру. Подвижный контакт замыкает их на “землю”, если они не попадают в его отверстия. Тут заметим, что на рисунке показаны только четыре отверстия для упрощения. На самом деле этих отверстий намного больше (опять вспоминаем шариковыю мышь, и как выглядит её колёсико оптического прерывателя). Выводы А и В подтягиваются резисторами к напряжению питания. В результате, при вращении получаются диаграммы, показанные на рисунке выше.

Пусть изначально оба контакта попадают в отверстие, тогда на них будет высокий уровень напряжения (они же подтянуты к питанию). Далее, при повороте по часовой стрелке, контакт А первым будет замкнут на землю, затем, к нему присоединится и контакт В. Далее, добравшись до следующего отверстия в диске, контакт А разомкнётся и получит высокий уровень, после чего его догонит контакт В. После этих перемещений контакты вернуться в исходное состояние, и при дальнейшем вращении эта диаграмма будет циклически повторяться.

Таким образом, получается, что текущее состояние энкодера описывается двух-битовым значением. Но само по себе текущее состояние несёт мало полезной информации, и для анализа вращения его надо рассматривать в связке со значением предыдущего состояния. И вот эта пара уже однозначно определяет направление вращения ручки. Для удобства возьмём четырёх-битовое число, два старших байта которого содержат предыдущие состояния контактов А и В, а два младших - текущие.

3 2 1 0
A[i-1] B[i-1] A[i] B[i]

По этому значению мы всегда можем однозначно определить направление вращения энкодера. Так, при вращении по часовой стрелке, получается следующий ряд значений:

Двоичное Десятичное
1101 13
0100 4
0010 2
1011 11

А при вращении против часовой стрелке

Двоичное Десятичное
1110 14
0001 1
0010 2
0111 7

Теперь алгоритм определения направления вращения энкодера выглядит очень просто: получаем значение и сравниваем, попадает ли оно в одно из множеств (2, 4, 11, 13) и (1, 7, 8, 14). Если да, то имеем поворот в соответствующем направлении. В противном случае, вал либо не вращался совсем, либо вращался так быстро, что проскочил несколько состояний (если такое часто случается, то стоит задуматься о повышении частоты опроса состояния), либо имел место "дребезг" контактов. Не вникая в причину, все прочие значения можно смело игнорировать.

В качестве примера рассмотрим работу энкодера в связке с микроконтроллером AVR:

Пример подключения энкодера к микроконтроллеру AVR

Тут для подключения использованы два младших вывода порта PB микроконтроллера ATMega8. Пара резисторов подтягивают эти линии к напряжению питания (т.к. внутренних резисторов атмеги тут может оказаться недостаточно для устойчивой работы), пара конденсаторов установлены для подавления импульсных помех.

Для такой схемы подключения можно набросать следующую реализацию на языке Си:


static uint8_t encoderGetVal() {
  return PINB & 3;
}

static uint8_t encoderGetCode() {
  static uint8_t prev;
  uint8_t val = encoderGetVal();
  uint8_t code = (prev << 2) | val;
  prev = val;
  return code;
}

static void encoderInit() {
  DDRB &= ~0b11;
  PORTB |= 0b11;
  encoderGetCode();
}
  
void onEncoderEvent(bool direction);
  
void encoderCheck() {
  uint8_t code = encoderGetCode();
  if (code == 1 || code == 7 || code == 8 || code == 14) {
    onEncoderEvent(true);
  } else if (code == 2 || code == 4 || code == 11 || code == 13) {
    onEncoderEvent(false);
  }
}

Код прост до безобразия - пара if-ов и никаких конечных автоматов. Функция encoderInit() вызывается в начале для инициализации порта и запоминания стартового значения. Функция encoderCheck() вызывается в цикле обработки событий (внутри main() или по таймеру). Обработчик onEncoderEvent(bool) будет вызываться всякий раз, когда произойдёт вращение экнодера и получать флаг направления вращения.

Но тут есть один важный момент: энкодер - штука чувствительная, и если пытаться обрабатывать таким образом, например, события навигации по меню, то даже небольшой поворот ручки энкодера будет многократно вызывать обработчик onEncoderEvent(), в результате чего, курсор меню вместо перемещения на следующий/предыдущий элемент, будет улетать сразу в конец/начало списка. Регулировать чувствительность энкодера можно изменением частоты вызова encoderCheck() (обычно оптимальная частота ~ 10 Гц). При этом метод encoderGetCode() следует вызывать как можно чаще, чтобы всегда иметь актуальное значение последнего состояния контактов (с частотой где-то ~ 100 Гц).

На ассемблере этот код мог бы выглядеть следующим образом:


.EQU encoder_port  PORTB
.EQU encoder_pin   PINB
.EQU encoder_ddr   DDRB

.DSEG
.ORG SRAM_START

sEncoderPrev:
.BYTE 1

...

.CSEG
.ORG $0000

...

Encoder_init:
	cbi encoder_ddr, 0
	cbi encoder_ddr, 1
	sbi encoder_port, 0
	sbi encoder_port, 1

	in	r0, encoder_pin
	andi	r0, 3
	sts  sEncoderPrev, r0
	
	
...


Encoder_check
	lds  ZL, sEncoderPrev
	lsl  ZL
	lsl  ZL
	in	r0, encoder_pin
	andi	r0, 3
	sts  sEncoderPrev, r0
	or  ZL, r0

	; 1 7 8 14 -> по часовой стрелке
	cpi  ZL, 1
	breq Encoder_clockwise
	cpi  ZL, 7
	breq Encoder_clockwise
	cpi  ZL, 8
	breq Encoder_clockwise
	cpi  ZL, 14
	breq Encoder_clockwise
	; 2 4 11 13 -> против часовой стрелки
	cpi  ZL, 2
	breq Encoder_counterclockwise
	cpi  ZL, 4
	breq Encoder_counterclockwise
	cpi  ZL, 11
	breq Encoder_counterclockwise	
	cpi  ZL, 13
	breq Encoder_counterclockwise	
	rjmp Encoder_done
Encoder_clockwise:
;
; тут код обработчика вращения по часовой стрелке
;
Encoder_counterclockwise:
;
; тут код обработчика вращения против часовой стрелки
;
Interval_enc_done:
Rating: 
0
No votes yet