AVR Урок 55. Инкрементальный энкодер

 

 

 

В данном уроке мы поговорим о таком устройстве, как инкрементальный энкодер.

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

Энкодер – это цифровой датчик, который определяет угол поворота.

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

Абсолютный энкодер – это энкодер, на выходе которого мы сразу же получаем двоичный код угла поворота.

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

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

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

Мы будем работать с самым дешёвым и доступным контактным инкрементальным энкодером, правда поставляемым в виде модуля KY-040, который также является очень недорогим и удобным, так как там есть уже подтягивание контактов резисторами к шине питания, а также на выходе мы имеем удобные контакты для подключения к нашим отладочным платам

 

 

 

А вот его схема

 

 

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

Теперь думаю самый интересный вопрос. Почему именно AVR? А потому, что в STM есть аппаратная реализация работы с энкодерами и там всё легко и мы не поймём механизм работы с ним, а в AVR отслеживать изменение состояний контактов мы будем программно.

В качестве отладочной платы мы возьмём Arduino NANO. Это удобная плата, в ней есть встроенный переходник USB-TTL, упрощающий работу с шиной UART при соединении платы с ПК. Только не забывайте о том, что если мы работаем с данной платой не в среде Arduino IDE и не через UART, а через интерфейс ISP, то загрузчик мы затираем и впоследствии уже не сможем пользоваться данной платой как платой Arduino и её не увидит среда Arduino IDE, вернее увидит, но только после определённых мероприятий по восстановлению загрузчика. Это не так уж и сложно, информация об этом есть, причём мне, как ни странно, загрузчик у платы NANO удавалось восстановить только на неоригинальной плате, на оригинальной не удалось, видимо там какой-то другой загрузчик, но так как таких плат у меня много, я заморачиваться особо и не стал и пользуюсь данной платой как обычной отладочной платой AVR.

К нашей плате мы подключим символьный дисплей разрешением 20×4, подключенный по шине I2C посредством переходника на соответствующие ножки 27 и 28 нашей платы (на плате они обозначены как A4 и A5, а также подведём прямо с платы к данному дисплею питание +5 вольт и общий провод. Также мы подключим модуль энкодера соответственно к следующим ножкам

 

VCC – +5V

GND – GND

SW – PD2 (D2)

DT – PD3 (D3)

CLK – PD4 (D4)

 

Также подключим программатор через переходник 10 к 6 и в результате у нас получится вот такая схема

 

 

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

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

 

 

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

 

 

За основу мы возьмём картинку с добавленными линиями, и в случае вращения вперёд мы будем отслеживать переход из состояния по пунктирной линии в состояние по сплошной. То же самое мы будем делать в случае вращения назад, только переход у нас будет справа налево, и состояние смотреть будем уже от сплошной линии к пунктирной. В первом случае мы значение будем инкрементировать, а во втором – декрементировать.

Создадим новый проект в MPLAB X, назвав его Encoder01 и выбрав контроллер Atmega328P, добавим для начала файлы main.c и main.h следующего содержания

 

 

 

Так как мы подключаем символьный дисплей по шине I2C (у Atmel она называется TWI), нам нужна библиотека по работе с данной шиной. С ней мы работали и раньше, поэтому файлы twi.h и twi.c, а заодно и файлы lcdtwi.h и lcdtwi.c для работы непосредственно с контроллером дисплея мы возьмём из проекта I2CLCD80 урока 16 части 7 и скопируем в каталог с нашим новым проектом, подключив их затем к дереву проекта. Подключим затем данные библиотеки в файле main.h

 

 

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

В файле twi.c в функции I2C_Init изменим значение регистра частоты шины, так как у нас кварцевый резонатор теперь не 8 МГц, а 16

 

TWBR=0x48;//скорость передачи (при 16 мгц получается 100 кгц, что и необходимо для общения с ds1307)

 

В данном файле мы больше ничего не трогаем.

В файле twi.h удалим подключение файла main.h, зачем нам эти перекрёстные подключения

 

#include "main.h"

 

Вместо этого для работы с шиной нам достаточно подключить вот этот файл из стандартной библиотеки

 

 

В файле lcdtwi.c для начала изменим имена функций на более привычные нам.

Старое имя – новое имя:

clearlcdLCD_Clear,

setposLCD_SetPos,

str_lcdLCD_String,

Не забываем также переименовать функции и в прототипах.

Также в функции инициализации дисплея изменим задержки на более устоявшиеся с годами. После этого она примет вот такой вид

 

 

Также немного перестроим функцию вывода строки на дисплей. Сначала изменим во входном параметре массив неизвестного размера на указатель

void LCD_String (char *str1)

Внутри также изменим код, так как вместо цикла for здесь уместнее использовать while

 

 

Не забываем также исправить параметр функции и в прототипе.

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

 

 

Соберём код, прошьём контроллер и посмотрим результат

 

 

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

Удалим наш тестовый вывод строки

 

LCD_SetPos(0,0);

sprintf(str1, "String1");

LCD_String(str1);

 

 

Настроим для начала ножки порта. Резистор, как мы помним, мы подтягиваем только к ножке SW (PD2)

 

 

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

Добавим функцию инициализации нашего таймера

 

 

Вызовем данную функцию в main(), запретив перед вызовом глобальные прерывания, а после вызова разрешив их

 

 

Объявим глобальную переменную для счётчика

 

 

Также ниже функции инициализации таймера timer_ini добавим функцию обработки прерываний данного таймера по событию совпадения

 

 

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

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

 

 

Возьмём состояние ножек из порта D, сдвинем его на 3 позиции для того, чтобы биты ножек стали младшими, и присвоим всё это переменной

 

 

Если вдруг обнаружим низкое состояние ножки кнопки (кнопка нажата), то обнулим счётчик

 

 

А дальше мы применим оператор вариантов, в котором будем одновременно исследовать и старое и новое состояние. А чтобы не плодить кучу веток с условиями, мы старое состояние будем хранить не в младших битах переменной, а в битах 2 и 3. Поэтому в проверяемом выражении мы старое значение способом логического сложения соединим с новым. Таким образом, в нашем выражении старое состояние окажется в битах 3 и 2, а новое – в битах 1 и 0

 

 

Вспомним наш рисунок состояний со сплошными и пунктирными полосочками. Следуя ему, мы получим следующие изменения.

 

Вращение вала вперёд:

00 -> 01 (0x01)

11 -> 10 (0x0e)

 

Вращение вала назад:

01 -> 00 (0x04)

10 -> 11 (0x0b)

 

Вот на такие значения мы и будем проверять наше выражение. И, если значение совпадёт с одним из верхних вариантов – то увеличим значение счётчика, а если с одним из нижних – уменьшим

 

 

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

 

 

Вот, в принципе, и вся логика.

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

Поэтому в функции main() сначала объявим ещё одну локальную переменную для счётчика

 

 

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

 

 

В бесконечном цикле заберём значение из глобальной переменной в локальное, причём перед данной операцией мы глобальные прерывания запретим, а после – разрешим

 

 

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

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

 

 

Выйдя из тела условия, применим задержку на 10 милисекунд, чтобы не так часто выводить на дисплей значения и не отвлекать таймер запретом прерываний

 

 

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

Соберём код, прошьём контроллер, и покрутив в разные стороны ручку нашего энкодера, посмотрим, как работает наша программа

 

 

 

 

 

Нажмём на ручку энкодера

 

 

Показания обнулились.

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

Все спасибо за внимание!

 

Предыдущий урок Программирование МК AVR Следующий урок

 

Исходный код

 

 

Приобрести плату Arduino Nano V3.0 оригинальный FT232RL можно здесь.

Программатор (продавец надёжный) USBASP USBISP 2.0

Приобрести плату энкодера можно здесь KY-040.

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

AVR Инкрементальный энкодер

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

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

*