Урок 55. Работа с инкрементальным энкодером в Ардуино. Библиотека Encod_er.h.

Encoder EC11

В уроке расскажу о принципе действия инкрементального энкодера, о подключении его к плате Ардуино. Покажу способ программной обработки сигналов энкодера и представлю библиотеку для работы с ним.

Предыдущий урок     Список уроков     Следующий урок

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

 

Совсем коротко, о чем идет речь, т.е. о классификации энкодеров.

Энкодеры это цифровые датчики угла поворота. Другими словами преобразователи угол-код.

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

Энкодеры бывают абсолютные и накапливающие (инкрементальные).

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

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

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

В подобных случаях инкрементальные энкодеры становятся идеальными устройствами управления, установки параметров, выбора меню. Они намного удобнее, чем кнопки ”+” и ”-”.

В этих случаях можно использовать самые простые механические инкрементальные энкодеры, которые отличаются низкой ценой.

По моей партнерской ссылке механический инкрементальный энкодер с кнопкой EC11 стоит всего 50 руб. При покупке 5 штук 45 руб., при 10 - 40 руб.

Encoder EC11

 

Принцип действия механического инкрементального энкодера.

Импульсы на выходе инкрементального энкодера должны сообщать не только о повороте вала, но и о направлении поворота. Поэтому необходимо использовать 2 сигнала, которые обычно обозначаются A и B. Эти сигналы подключены к механическим контактам энкодера, другие выводы которых соединены на выводе C.

Принцип действия энкодера

Если подключить к сигналам A и B подтягивающие резисторы, то диаграмма выходных сигналов при вращении энкодера будет выглядеть так.

Диаграмма работы инкрементального энкодера

Если вы покрутите вал энкодера, то заметите, что у него есть фиксированные положения. У моего энкодера 20 таких положений на оборот. Т.е. точность определения угла 360° / 20 = 18° или 20 импульсов на оборот.

Эти, механически зафиксированные, положения отмечены на диаграмме стрелочками снизу вверх. В этот момент оба контакта разомкнуты, сигналы A и B находятся в высоком уровне.

На первых 2 отрезках диаграммы энкодер повернули по часовой стрелке. Сначала в низкий уровень упал сигнал A, затем B. На 3 и 4 интервалах вал вращают против часовой стрелки. После фиксированного положения сначала становится равным нулю сигнал B. По последовательности изменения сигналов можно определить, в какую сторону повернули энкодер.

Формализованный алгоритм анализа сигналов энкодера выглядит так.

  • Если в момент перехода сигнала A в низкое состояние сигнал B находится в высоком уровне, то произошел поворот по часовой стрелке.
  • Если в момент перехода сигнала A в низкое состояние сигнал B находится в низком уровне, то был поворот против часовой стрелки.

Возможны другие кодировки положения энкодера. Все, что я пишу верно, относительно EC11.

 

Параметры инкрементальных энкодеров.

Подробно технические характеристики и параметры энкодера EC11 в формате PDF можно посмотреть по этой ссылке EC11.pdf. Я приведу и поясню основные.

Encoder EC11

  • Главный параметр – число импульсов на оборот. Определяет точность измерения угла. У моего датчика 20 импульсов на оборот, точность 18°.
  • Предельно допустимые электрические параметры определяют предельные значения тока и напряжения для контактов. В моем случае это 10 мА и 5 В. Эти параметры влияют на выбор подтягивающих резисторов. Сопротивление резисторов не должно быть ниже 500 Ом.
  • Прочность изоляции. Я бы не рискнул использовать подобный энкодер в цепях, гальванически связанных с высоким напряжением.
  • Износоустойчивость. Разработчики гарантируют от 15 000 до 1 000 000 циклов, в зависимости от модификации.
  • Рабочий диапазон температур -30 … +85 °C.
  • Механические параметры: габаритные и установочные размеры, моменты вращения и т.п.

 

Подключение энкодера к плате Ардуино.

С электрической точки зрения энкодер это 3 кнопки: сигналы A, B и кнопка. Я использовал внутренние подтягивающие резисторы, но в рабочих схемах лучше добавить внешние резисторы сопротивлением 2 – 10 кОм.

Подключение Encoder EC11 к Ардуино

Программная обработка сигналов энкодера.

Сначала приведу простую программу, позволяющую определить кодировку энкодера.

void setup() {
  Serial.begin(9600); // инициализируем порт, скорость 9600
  pinMode(2, INPUT_PULLUP); // определяем вывод как вход
  pinMode(8, INPUT_PULLUP); // определяем вывод как вход
}
void loop() {
  if( digitalRead(2) == HIGH ) Serial.print("H ");
  else Serial.print("L ");
  if( digitalRead(8) == HIGH ) Serial.println("H");
  else Serial.println("L");
  delay(200);
}

У меня энкодер подключен к выводам 2 и 8. Программа в цикле выводит через последовательный порт состояния сигналов A и B.

Если я плавно вращаю вал по часовой стрелке у меня монитор последовательного порта выводит:
HH -> LH -> LL -> HL -> HH

При вращении  против часовой стрелки:
HH -> HL -> LL -> LH -> HH

Все как на диаграмме выше.

Алгоритм обработки сигналов энкодера простой:

  • выделяем отрицательный фронт сигнала A;
  • считываем состояние сигнала B;
  • если у сигнала B:
    • высокий уровень – был поворот по часовой стрелке;
    • низкий  уровень – был поворот против часовой стрелки.

Но у механического энкодера механические контакты. Им свойственно явление дребезга. К тому же энкодеры часто устанавливаю на передней панели устройства, и подключают к контроллеру достаточно длинными проводами. Т.е. нужна цифровая фильтрация сигналов и устранение дребезга. Все это реализовано в моей библиотеке Button.h. Ее вполне можно использовать для работы с энкодером.

Ну и самая главная мысль в каждом моем уроке. Давайте сделаем обработку энкодера параллельным процессом. Программа в основном цикле занимается своими задачами, а параллельный процесс обрабатывает сигналы энкодера в прерывании по таймеру.

#include <TimerOne.h>
#include <Button.h>

Button encoderA (2, 4); // сигнал A
Button encoderB (8, 4); //  сигнал B
Button encoderButton(10, 40); // кнопка

long pos=0; // пооложение энкодера

void setup() {
  Serial.begin(9600); // инициализируем порт, скорость 9600
  Timer1.initialize(250); // инициализация таймера 1, период 250 мкс
  Timer1.attachInterrupt(timerInterrupt, 250); // задаем обработчик прерываний
}

void loop() {
// сброс положения 
  if( encoderButton.flagClick == true ) {
    encoderButton.flagClick= false;
    pos= 0;
  }
  Serial.println(pos); // вывод положения
}

// обработчик прерывания 250 мкс
void timerInterrupt() {
  encoderA.filterAvarage(); // вызов метода фильтрации
  encoderB.filterAvarage(); // вызов метода фильтрации 
  encoderButton.filterAvarage(); // вызов метода фильтрации

// обработка сигналов энкодера
  if( encoderA.flagClick == true ) {
    encoderA.flagClick= false;
      if( encoderB.flagPress == true) {
        // против часовой стрелки
        pos--;
      }
      else {
        // по часовой стрелке
        pos++;
      }
  }
}

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

Вопрос, с какой частотой необходимо опрашивать энкодер. У меня в программе цикл прерывания 250 мкс и 4 выборки при фильтрации. В итоге период реакции на изменения состояния сигналов 1 мс.

Все зависит от того с какой частотой сигналы энкодера могут изменять свое состояние. А это определяют максимальная скорость вращения энкодера и параметр - число импульсов на оборот.

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

  • Запомнить механическое положение вала.
  • Сбросить вычисленное положение кнопкой (привязать к механическому положению).
  • Теперь сколько бы вы ни вращали вал, при попадании в механическое положение, к которому он был привязан, на экране должно быть значение кратное числу импульсов на оборот. В моем случае это 0,20, 40…

 

Библиотека Encod_er.h.

Представленный выше способ обработки энкодера вполне имеет право на жизнь. Но в моей задаче надо было определять, с какой скоростью вращают энкодер.

Энкодером перемещался шпиндель сверлильного станка. С одной стороны нужна была высокая точность позиционирования.  С другой – приходилось долго крутить ручку, чтобы переместить шпиндель в нижнее положение.

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

Для реализации этого алгоритма необходимо вычислять скорость вращения энкодера. Я разработал библиотеку обработки энкодера с этой функцией.

Загрузить библиотеку Encod_er.h можно по этой ссылке.

 

Описание класса Encod_er.

Я привел только public свойства и методы.

class Encod_er {
  public:
    Encod_er(byte pinA, byte pinB, byte timeFilter);
    byte timeRight; // время/признак вращения вправо (* 8 периодов)
    byte timeLeft; // время/признак вращения влево (* 8 периодов)
    long position; // текущее положение 
    void scanState(); // метод проверки состояния
    long read(); // метод чтения положения 
};

Encod_er(byte pinA, byte pinB, byte timeFilter)  - конструктор.

  • pinA – вывод сигнала A;
  • pinB – вывод сигнала B;
  • timeFilter – число выборок фильтрации сигналов.

Encod_er myEncoder(2, 3, 5);   // энкодер к выводам 2 и 3, 5 выборок фильтрации

void  scanState() – метод сканирования состояния энкодера. Должен вызываться регулярно в параллельном процессе.

// обработчик прерывания 250 мкс
void timerInterrupt() {
  myEncoder.scanState();
}

byte timeRight и timeLeft – признаки поворота энкодера соответственно вправо и влево. Если равны 0, то поворота не было. Если не равны 0, то содержат время поворота на один шаг. Время вычисляется, как

Tповорота = Tвызова scanState()  * 8 * timeRight.

Т.е. единица значения timeRight или timeLeft это период вызова scanState(),  умноженный на 8. В моей программе это 250 мкс * 8 = 2 мс. На 8 я умножил, чтобы использовать один байт. Значение переменных  timeRight или timeLeft ограничиваются на уровне 255.

Если признаки используются, то должны сбрасываться  в основном цикле.

if(encoder.timeRight != 0) {
  encoder.timeRight= 0;
...........
}

long  position – текущее положение энкодера. Может быть прочитано или записано.

long  read() – метод чтения положения энкодера. Возвращает переменную position.

Все просто. Предыдущую программу можно переписать с использованием библиотеки Encod_er.h.

#include <TimerOne.h>
#include <Encod_er.h>

Encod_er encoder( 2, 8, 4);

void setup() {
  Serial.begin(9600); // инициализируем порт, скорость 9600
  Timer1.initialize(250); // инициализация таймера 1, период 250 мкс
  Timer1.attachInterrupt(timerInterrupt, 250); // задаем обработчик прерываний
}

void loop() {
  if(encoder.timeRight != 0) {
    Serial.print("R=");
    Serial.print(encoder.timeRight);
    Serial.print(" Pos=");
    Serial.println(encoder.read()); // вывод текущего положения 
    encoder.timeRight= 0;
  }
  if(encoder.timeLeft != 0) {
    Serial.print("L=");
    Serial.print(encoder.timeLeft);
    Serial.print(" Pos=");
    Serial.println(encoder.read()); // вывод текущего положения
    encoder.timeLeft= 0;
  }
}

// обработчик прерывания 250 мкс
void timerInterrupt() {
  encoder.scanState();
}

На каждый поворот вала в монитор последовательного порта выводится направление вращение и значение timeRight или timeLeft.

Окно монитора порта

Обработку кнопки энкодера я в библиотеку не включил. Кнопку лучше обрабатывать библиотекой Button.h.

С помощью этой библиотеки я реализовал управление перемещением шпинделя сверлильного станка по описанному выше алгоритму.

 

Анализировал признаки timeRight и timeLeft. При ненулевом значении признаков шаговый двигатель перемещения шпинделя делал количество шагов в зависимости от значения активного признака. Зависимость задал в массиве.

Предыдущий урок     Список уроков     Следующий урок

9 комментариев на «Урок 55. Работа с инкрементальным энкодером в Ардуино. Библиотека Encod_er.h.»

  1. Здравствуйте, мне нужно добавить в условие,
    if(encoder.read() = 4095)
    {??? = 4095;
    }
    как правильно записать в энкодер, что если меньше нуля то всегда равен нулю и если больше 4095, то равен 4095?
    Подскажите пожалуйста?

    • Здравствуйте!

      noInterrupts();
      if(encoder.position < 0) encoder.position=0; if(encoder.position > 4095) encoder.position=4095;
      interrupts();

      У переменной position права public.
      x=encoder.read(); и x=encoder.position; одно и то же. Только прерывание надо запрещать, чтобы не считывать частично модифицированную переменную.

  2. Здравствуйте!
    Очень интересные уроки. Все замечательно работает.
    Есть такой вопрос:
    Возможна ли одновременная корректная работа библиотек , из этого урока и высокочастотной ШИМ из Урока 37 (Широтно-импульсная модуляция в Ардуино)? Как лучше реализовать управление заполнением высокочастотной ШИМ с помощью енкодера и учетом скорости вращения?

    • Конечно. ШИМ реализуется аппаратным способом. Программа не тратит на него время.
      Для управления двигателем надо делать регулятор. Измерять скорость вращения. Возможно вам не подойдет эта библиотека. При высоких скоростях вращения не хватит времени на обработку 2 сигналов энкодера. Вам достаточно обрабатывать один сигнал. Вы же знаете в какую сторону крутите двигатель. И энкодер должен быть другой, не механический, с большим ресурсом.

  3. Добрый день. Нужно чтобы нажали кнопку, реле 1 замкнулось, как только будет n оборотов энкодера, реле выключилось. Как это можно реализовать?

  4. Здравствуйте!
    У Вас очень хорошие уроки, спасибо Вам за труд!
    Подскажите пожалуйста, почему Вы решили делать прерывания по таймеру? Почему просто не привязать аппаратное прерывание к А с параметром FALLING?

    • Здравствуйте!
      Спасибо за отзыв по сайту.
      Я стараюсь использовать внешние прерывания только в крайнем случае. Они обладают низкой помехозащищенностью, срабатывают на дребезг контактов, система становится асинхронной. К тому же я предполагаю, что программа выполняет параллельные задачи, которые требуют прерывания по таймеру.

  5. Добрый день, пожалуйста приведите пример добавления произвольного таймера, в код. Пробую по этому примеру: http://arduino.ru/Reference/Millis
    В итоге компилятор ошибки выдаёт. Полагаю TimerOne не позволяет использовать эти средства. Как с его помощью реализовать тоже самое, что и в примере на ардуино.ру ?

    • Здравствуйте!
      millis() возвращает время (в мс), с момента запуска контроллера. Для отработки временного интервала необходимо в начале отсчета считать функцией millis() текущее время. Затем периодически считывать время этой функцией, проверяя, не достигло ли оно требуемого значения (значение считанного в начале отсчета + требуемое время интервала). В промежутках между проверками текущего времени программа может заниматься другими задачами.
      Т.е. при использовании millis() вы должны все время проверять значение времени и реализовывать переход при выполнении условия. На проверку тратится определенное время. Кроме того точность отработки времени таймера будет зависеть и от частоты вызова функции millis().
      При использовании TimerOne программа перейдет на обработку прерывания аппаратными средствами без затрат программного времени.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *