Энкодер

Время рассматривать всякие кнопочки-датчики)
Начнём с простейшего – с механического накапливающего энкодера.
Энкодер, он же датчик угла поворота – устройство, "предназначенное для преобразования угла поворота вращающегося объекта (вала) в электрические сигналы, позволяющие определить угол его поворота". Иными словами, крутим крутилку, а на выход нам даётся информация о том, насколько мы повернули ручку.
По способу выдачи информации энкодеры бывают абсолютные и накапливающие (инкрементные). Абсолютные показывают непосредственно сам угол поворота, накапливающие формируют импульсы «Был поворот вправо» или «Был поворот влево».
Мы будем рассматривать накапливающие энкодеры. Вот принцип их работы:


Рисунок 1. Принцип работы механического накапливающего энкодера

Таким образом, если датчик оказывается в пропуске пластины, то с него есть сигнал.
По принципу действия энкодеры бывают оптическими, резистивными, магнитными, индуктивными и механическими.Наиболее простые – механические накапливающие. Их можно рассматривать как просто две кнопки. Если мы поворачиваем крутилку вправо, то сначала включается кнопка b, если влево – то кнопка a. Подробнее на рисунке:


Рисунок 2. Состояние "кнопок" внутри энкодера при вращении

Таким образом, можно просто регулярно проверять две ножки микросхемы, и мы будем знать, куда крутится энкодер.
Попробуем реализовать простенькую программку: пусть у нас будет ATtiny2313, энкодер и парочка светодиодов. Крутим энкодер в одну сторону – горит один светодиод, крутим в другую – другой. Заморачиваться с погашением светодиодов не будем. Сейчас главное – понять, как работает энкодер. "Пощупать", так сказать!
Итак, алгоритм программы:

  1. Проверяем состояние энкодера. В "спокойном состоянии" на ножках микросхемы "единичка" - ножки, слушающие энкодер, установлены на вход с подтяжкой. Как только на этих ножках оказывается не "11", а "01" или "10" - энкодер начал крутиться. Запоминаем, что нажалось первое, и переходим на шаг 2.
  2. Ждём следующего состояния энкодера – нажатия двух внутренних кнопок – "00". Как только получили его, считаем, что энкодер уже точно щелкнул в ту или иную сторону, поэтому зажигаем нужный светодиод и гасим ненужный. Переходим на шаг 3.
  3. Просто дожидаемся окончания поворота энкодера – то есть, прихода "11". Как только получили, переходим снова на шаг 1.

Тут программа на Си.

typedef unsigned char byte;
typedef unsigned int word;
//глобальные переменные и константы, определяющие распиновку
//переменные для энкодера
byte EncoderDDR at DDRB;
byte EncoderPORT at PORTB;
byte EncoderPIN at PINB;
const byte ENCODER_OUT_A_PIN = 0;
const byte ENCODER_OUT_B_PIN = 1;
//переменные для светодиодов
byte LED_DDR at DDRD;
byte LED_PORT at PORTD;
const byte LED_A_PIN = 0;
const byte LED_B_PIN = 1;

enum TTurnSide {tsLeft, tsRight};                                               //перечисление сторон поворота энкодера
enum TEncoderState {esWaitChanges,                                              //перечисление состояний энкодера: ожидание изменений ("01 или 10");
                    esWaitValidation,                                           //ожидание подтверждения поворота ("00");
                    esWaitEnding};                                              //ожидание окончания поворота ("11");

void main()
{
  //объявляем переменные
  enum TTurnSide TurnSide;                                                      //переменная, определяющая сторону поворота энкодера.
  enum TEncoderState EncoderState = esWaitChanges;                              //переменная, определяющая текущее состояние энкодера; изначально - "ждём изменений"
  //начальная инициализация - все ножки в Z-состояние
  PORTA = 0;
  DDRA = 0;
  PORTB = 0;
  DDRB = 0;
  PORTD = 0;
  DDRD = 0;
  //определяем конкретные биты:
  EncoderDDR &= ˜((1 << ENCODER_OUT_A_PIN) | (1 << ENCODER_OUT_B_PIN));
  EncoderPORT |= (1 << ENCODER_OUT_A_PIN) | (1 << ENCODER_OUT_B_PIN);
  LED_DDR |= (1 << LED_A_PIN) | (1 << LED_B_PIN);
  LED_PORT |= (1 << LED_A_PIN) | (1 << LED_B_PIN);
  while(1)
  {
    switch (EncoderState)                                                       //в зависимости от состояния энкодера
    {
    case esWaitChanges:                                                         //состояние "Ждём начала поворота"
      //если есть изменения ("01" или "10")
      if ((EncoderPIN.ENCODER_OUT_A_PIN == 0) ˆ (EncoderPIN.ENCODER_OUT_B_PIN == 0))
      {
        //то запоминаем, в какую сторону поворот
        if (EncoderPIN.ENCODER_OUT_A_PIN == 0)
          TurnSide = tsLeft;
        else
          TurnSide = tsRight;
        //переходим на следующий шаг - ждём подтверждения поворота
        EncoderState = esWaitValidation;
      }
      break;
    case esWaitValidation:                                                      //состояние "Ждём подтверждения поворота"
      //если пришло подтверждение поворота ("00"):
      if ((EncoderPIN.ENCODER_OUT_A_PIN == 0) && (EncoderPIN.ENCODER_OUT_B_PIN == 0))
      {
        //зажигаем нужный светодиод и гасим ненужный
        if (TurnSide == tsLeft)
        {
          LED_PORT.LED_A_PIN = 0;
          LED_PORT.LED_B_PIN = 1;
        }
        else
        {
          LED_PORT.LED_B_PIN = 0;
          LED_PORT.LED_A_PIN = 1;
        }
        //переходим на следующий шаг - ждём окончания поворота
        EncoderState = esWaitEnding;
      }
      break;
    case esWaitEnding:                                                          //состояние "Ждём окончания поворота"
      //если перешли в "спокойное" состояние ("11")
      if ((EncoderPIN.ENCODER_OUT_A_PIN == 1) && (EncoderPIN.ENCODER_OUT_B_PIN == 1))
        EncoderState = esWaitChanges;
      break;
    }
  }
}

А теперь – невероятно, но факт! На mikroPascal не были реализованы ни перечисления, ни наборы, ни объединения.
Жизнь – боль. Удар ниже пояса.
Соответственно, придётся обходить перечисления просто числами – но тогда код становится более запутанным.
Вот программа на Паскале.

program Encoder;
const
  ENCODER_OUT_A_PIN = 0;
  ENCODER_OUT_B_PIN = 1;
  LED_A_PIN = 0;
  LED_B_PIN = 1;

var
  EncoderDDR: byte at DDRB;
  EncoderPORT: byte at PORTB;
  EncoderPIN: byte at PINB;
  LED_DDR: byte at DDRD;
  LED_PORT: byte at PORTD;
  TurnSide: byte;
  EncoderState: byte;

begin
  EncoderState := 0;
  //начальная инициализация - все ножки в Z-состояние
  PORTA := 0;
  DDRA := 0;
  PORTB := 0;
  DDRB := 0;
  PORTD := 0;
  DDRD := 0;
  //определяем конкретные биты:
  EncoderDDR := EncoderDDR and (not((1 shl ENCODER_OUT_A_PIN) or (1 shl ENCODER_OUT_B_PIN)));
  EncoderPORT := EncoderPORT or (1 shl ENCODER_OUT_A_PIN) or (1 shl ENCODER_OUT_B_PIN);
  LED_DDR := LED_DDR or (1 shl LED_A_PIN) or (1 shl LED_B_PIN);
  LED_PORT := LED_PORT or (1 shl LED_A_PIN) or (1 shl LED_B_PIN);
  while(1) do
  begin
    case (EncoderState) of                                                       //в зависимости от состояния энкодера
     0:                                                             //состояние "Ждём начала поворота"
      //если есть изменения ("01" или "10")
      if ((EncoderPIN.ENCODER_OUT_A_PIN = 0) xor (EncoderPIN.ENCODER_OUT_B_PIN = 0)) then
      begin
        //то запоминаем, в какую сторону поворот
        if (EncoderPIN.ENCODER_OUT_A_PIN = 0) then
          TurnSide := 0
        else
          TurnSide := 1;
        //переходим на следующий шаг - ждём подтверждения поворота
        EncoderState := 1;
      end;
    1:                                                           //состояние "Ждём подтверждения поворота"
      //если пришло подтверждение поворота ("00"):
      if ((EncoderPIN.ENCODER_OUT_A_PIN = 0) and (EncoderPIN.ENCODER_OUT_B_PIN = 0)) then
      begin
        //зажигаем нужный светодиод и гасим ненужный
        if (TurnSide = 0) then
        begin
          LED_PORT.LED_A_PIN := 0;
          LED_PORT.LED_B_PIN := 1;
        end
        else
        begin
          LED_PORT.LED_B_PIN := 0;
          LED_PORT.LED_A_PIN := 1;
        end;
        //переходим на следующий шаг - ждём окончания поворота
        EncoderState := 2;
      end;
    2:                                                               //состояние "Ждём окончания поворота"
      //если перешли в "спокойное" состояние ("11")
      if ((EncoderPIN.ENCODER_OUT_A_PIN = 1) and (EncoderPIN.ENCODER_OUT_B_PIN = 1)) then
        EncoderState := 0;
      end;
  end;
end.

Итак – оно работает. Но если долго пробовать крутить энкодер в разные стороны, можно заметить, что иногда может, например, включиться не тот светодиодик, а изредка микроконтроллер вообще пропускает обороты.

Почему все работает не идеально? Не будем забывать, что раз у нас энкодер – это просто две кнопки, то, значит, может возникнуть дребезг контактов - это раз. А во-вторых, может так случиться, что начало нашего поворота - "01" или "10" - придется не на момент проверки именно этих состояний.