Энкодер магнус 21 схема подключения. Подключение энкодера к микроконтроллеру PIC. По типу выдаваемых данных энкодеры делятся на две большие группы

Для реализации демонстрационного проекта нам понадобятся:

  • 24-позиционный энкодер;
  • 16 светодиодов (3 мм);
  • драйвер светодиодов A6276 ;
  • микроконтроллер PIC18F2550 .

Энкодер - современный и оригинальный элемент управления цифровыми устройствами, и по внешнему виду похож на переменный резистор (см. рисунок ниже). Другое название этого элемента управления - датчик угла, датчик поворота. Вращение вала сопровождается щелчками, например 24 щелчка на один оборот. Энкодер имеет 3 вывода - A, B, C и применяется для быстрого ввода данных в цифровые устройства. Некоторые модели имеют встроенную кнопку, которая срабатывает по нажатию на вал энкодера (добавляется еще один вывод).

Принцип работы энкодера

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

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

Принципиальная схема

Выводы энкодера A и B подключаются к портам микроконтроллера RB4 и RB5, вывод С энкодера подключается к «земле». Стоит заметить, что на сигнальные линии выводов A и B должны быть подключены подтягивающие резисторы. Энкодер не случайно подключен к указанным линиям ввода/вывода микроконтроллера: во-первых, порт B имеет встроенные подтягивающие резисторы и нам не придется подключать внешние, во-вторых, порт B микроконтроллера имеет очень полезную функцию - «interrupt-on-change» - прерывание по изменению уровня, что позволит нам отслеживать состояние энкодера.

16 обычных 3 мм светодиодов используются для визуализации вводимых данных и расположены они будут на печатной плате вокруг установленного энкодера. Светодиоды подключены к микросхеме A6276.

Микросхема A6276 представляет собой драйвер светодиодов с 16-битным последовательным вводом информации. Драйвер содержит 16-битный КМОП сдвиговый регистр, соответствующие защелки и драйверы для управления светодиодами и может управлять большим количеством светодиодов, чем это позволяет микроконтроллер. Кроме того, драйвером можно управлять по интерфейсу SPI, что дополнительно сокращает количество используемых линий ввода/вывода и делает проект масштабируемым.

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

  • Режим позиционирования на 360° - в этом режиме светодиоды указывают текущую «позицию» энкодера, пользователь может поворачивать вал энкодера влево и вправо на любой угол;
  • Режим «Громкость/Уровень» - в этом режиме светодиоды указывают текущее значение между минимальным и максимальным уровнями диапазона ввода (как уровень громкости в аудиоустройствах);
  • Режим 3-позиционного ротационного тумблера - в этом режиме имеется только три выбираемых позиции, которые пользователь выбирает, поворачивая вал энкодера влево/вправо.

Демонстрация работы проекта

Загрузки

ZIP-архив с проектом в среде MPLAB и исходным кодом на Hitech C, а также, принципиальная схема и топология печатной платы находятся .

Энкодер это всего лишь цифровой датчик угла поворота, не более того.

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

Если с абсолютным энкодером все просто, то с инкрементальным бывают сложности. Как его обрабатывать?

С Энкодера выходят два сигнала А и В, сдвинутых на 90 градусов по фазе, выглядит это так:

В оптическом же может быть два фонаря и два фотодиода, святящие через диск с прорезями (шариковая мышка, ага. Оно самое).

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


Оптический подключается в зависимости от типа оптодатчика, обычно там стоит два с общим анодом.

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

Метод прост:
Подставим нули и единички, в соответствии с уровнем сигнала и запишем последовательность кода:


A:0 0 1 1 0 0 1 1 0 0 1 1 0
B:1 0 0 1 1 0 0 1 1 0 0 1 1

Если A и B идут на одни порт контроллера (например на A=PB0 B=PB1), то при вращении энкодера у нас возникает меняющийся код:

11 = 3
10 = 2
00 = 0
01 = 1
11 = 3

Теперь остается только циклически опрашивать наш энкодер сравнивая текущее состояние с новым и на основании этого делающего выводы о вращении. Причем частота опроса должна быть такой, чтобы не пропустить ни одного импульса. Например, мой EC12 имеет 24 импульса на оборот. Вращать его предпологается вручную и я вряд ли смогу вращать его с космической скоростью, но решил все же замерить. Подключился к осциллографу, крутнул ручку что есть мочи:

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // Эту задачу надо запускать каждую миллисекунду. // EncState глобальная переменная u08 -- предыдущее состояние энкодера // EncData глобальная переменная u16 -- счетный регистр энкодера void EncoderScan(void ) { u08 New; New = PINB & 0x03 ; // Берем текущее значение // И сравниваем со старым // Смотря в какую сторону оно поменялось -- увеличиваем // Или уменьшаем счетный регистр switch (EncState) { case 2 : { if (New == 3 ) EncData++; if (New == 0 ) EncData--; break ; } case 0 : { if (New == 2 ) EncData++; if (New == 1 ) EncData--; break ; } case 1 : { if (New == 0 ) EncData++; if (New == 3 ) EncData--; break ; } case 3 : { if (New == 1 ) EncData++; if (New == 2 ) EncData--; break ; } } EncState = New; // Записываем новое значение // Предыдущего состояния SetTimerTask(EncoderScan, 1 ) ; // Перезапускаем задачу через таймер диспетчера }

// Эту задачу надо запускать каждую миллисекунду. // EncState глобальная переменная u08 -- предыдущее состояние энкодера // EncData глобальная переменная u16 -- счетный регистр энкодера void EncoderScan(void) { u08 New; New = PINB & 0x03; // Берем текущее значение // И сравниваем со старым // Смотря в какую сторону оно поменялось -- увеличиваем // Или уменьшаем счетный регистр switch(EncState) { case 2: { if(New == 3) EncData++; if(New == 0) EncData--; break; } case 0: { if(New == 2) EncData++; if(New == 1) EncData--; break; } case 1: { if(New == 0) EncData++; if(New == 3) EncData--; break; } case 3: { if(New == 1) EncData++; if(New == 2) EncData--; break; } } EncState = New; // Записываем новое значение // Предыдущего состояния SetTimerTask(EncoderScan,1); // Перезапускаем задачу через таймер диспетчера }

Почему я под счетчик завел такую большую переменную? Целых два байта? Да все дело в том, что у моего энкодера, кроме импульсов есть еще тактильные щелчки. 24 импульса и 24 щелчка на оборот. А по моей логике, на один импульс приходится четыре смены состояния, т.е. полный период 3201_3201_3201 и один щелчок дает 4ре деления, что некрасиво. Поэтому я считаю до 1024, а потом делю сдвигом на четыре. Получаем на выходе один щелочок — один тик.

Скоростной опрос на прерываниях
Но это механические, с ними можно простым опросом обойтись — частота импульсов позволяет. А бывают еще и высокоскоростные энкодеры. Дающие несколько тысяч импульсов на оборот, либо работающие на приводах и вращающиеся очень быстро. Что с ними делать?

Ускорять опрос занятие тупиковое. Но нас спасает то, что у таких энкодеров, как правило, есть уже свои схемы подавления дребезгов и неопределенностей, так что на выходе у них четкий прямоугольный сигнал (правда и стоят они совершенно негуманно. От 5000р и до нескольких сотен тысяч. А что ты хотел — промышленное оборудование дешевым не бывает).

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


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

А в обработчике прерывания INT0 щупаем вторым выводом канал В. И дальше все элементарно!

Если там высокий уровень — делаем +1, если низкий -1 нашему счетному регистру. Кода на три строчки, мне даже писать его лень.

Конечно, можно такой метод прикрутить и на механический энкодер. Но тут надо будет заблокировать прерывания INT0 на несколько миллисекунд. И НИ В КОЕМ СЛУЧАЕ нельзя делать это в обработчике.

Алгоритм прерывания с антидребезгом будет выглядеть так:

  • Зашли в обработчик INT0
  • Пощупали второй канал
  • +1 или -1
  • Запретили локально INT0
  • Поставили на таймер событие разрешающее INT0 через несколько миллисекунд
  • Вышли из обработчика

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

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

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

Прежде всего энкодеры бывают нескольких типов, рассматриваемый в данной статье — механический инкрементальный. В качестве испытуемого, был использован pec12-4220f-s0024. Внешне он похож на переменный резистор, но, в отличие от резистора, он не имеет ограничителей, т.е. может крутиться бесконечно в любую сторону.

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

Теперь рассмотрим все более детально. Электрически он представляет собой 2 кнопки без фиксации, когда мы начинаем крутить они по очереди срабатывают — сначала одна, затем вторая. В зависимости от того, в какую сторону мы вращаем, одна из кнопок срабатывает раньше или позднее. Для того чтобы узнать, в каком состоянии находятся эти кнопки, ножки порта (к которому подсоединен энкодер) должны быть подтянуты к «+» питания.

На разобранном энкодере 1/3 площадки относится к 1 контакту, 1/3 к 2 контакту, сплошной участок — общий. Когда скользящие контакты попадают на изолированные участки (черные), слышны щелчки. В этот момент энкодер, находится в устойчивом состоянии, когда обе кнопки разомкнуты. На ножках порта будут лог единицы(состояние 11).

Как только мы начинаем вращать в какую либо сторону, один из контактов замыкается на землю. На этой ножеке появится лог 0, на второй ножке по прежнему будет лог1 (состояние 01). Если мы продолжаем вращать, на второй ножке появится лог0(состояние 00). Далее, на первой ножке пропадает контакт (состояние 10), в конце концов энкодер возвращается в устойчивое состояние (11). Т.е. на один щелчок приходится 4 изменения состояния. Временная диаграмма выглядит так:

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

Если выписать эти состояния в двоичной системе и перевести их в десятичную, то получится следующий порядок(для вращения в одну сторону):
11=3
01=1
00=0
10=2

При вращении в противоположную сторону:
11=3
10=2
00=0
01=1

Теперь осталось понять, как эти значение обрабатывать. Допустим, энкодер подключен к ножкам порта В0 и В1. Нам нужно прочитать эти ножки. Есть довольно хитрый способ, но для начала нам нужно понять операцию «логического и» (&).

Результат будет равен единице, только если оба числа равны 1, т.е. результат операции 1&1, будет равен 1. Следовательно 1&0=0, 0&0=0, 0&1=0.

Логическое & поможет нам вычленить из целого порта, только интересующие нас ножки. Т.е. операция x=0b00000011 & PINB; позволит нам прочитать в переменную «х» состояние первых двух ножек, независимо от того, что находится на остальных ножках. Число 0b00000011 можно перевести в шестнадцатеричную систему 0х3.

Теперь все необходимые знания для написания прошивки у нас есть. Задача: увеличивать/уменьшать переменную Vol, при помощи энкодера, результат вывести на lcd дисплей.

#include int NewState, OldState, Vol, upState, downState; #asm .equ __lcd_port= 0x12 ; PORTD #endasm #include #include interrupt [ TIM1_COMPA] void timer1_compa_isr(void ) { NewState= PINB & 0b00000011 ; if (NewState!= OldState) { switch (OldState) { case 2 : { if (NewState == 3 ) upState++; if (NewState == 0 ) downState++; break ; } case 0 : { if (NewState == 2 ) upState++; if (NewState == 1 ) downState++; break ; } case 1 : { if (NewState == 0 ) upState++; if (NewState == 3 ) downState++; break ; } case 3 : { if (NewState == 1 ) upState++; if (NewState == 2 ) downState++; break ; } } OldState= NewState; } TCNT1H= 0x00 ; TCNT1L= 0x00 ; } void main(void ) { char lcd_buf[ 17 ] ; // Input/Output Ports initialization // Port B initialization // Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In // State7=T State6=T State5=T State4=T State3=T State2=T State1=P State0=P PORTB= 0x03 ; DDRB= 0x00 ; // Timer/Counter 1 initialization // Clock source: System Clock // Clock value: 1000,000 kHz // Mode: CTC top=OCR1A // OC1A output: Discon. // OC1B output: Discon. // Noise Canceler: Off // Input Capture on Falling Edge // Timer 1 Overflow Interrupt: Off // Input Capture Interrupt: Off // Compare A Match Interrupt: On // Compare B Match Interrupt: Off TCCR1A= 0x00 ; TCCR1B= 0x0A ; TCNT1H= 0x00 ; TCNT1L= 0x00 ; ICR1H= 0x00 ; ICR1L= 0x00 ; OCR1AH= 0x03 ; OCR1AL= 0xE8 ; OCR1BH= 0x00 ; OCR1BL= 0x00 ; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK= 0x10 ; // Global enable interrupts #asm("sei") lcd_init(8 ) ; while (1 ) { if (upState >= 4 ) { Vol++; upState = 0 ; } if (downState >= 4 ) { Vol--; downState = 0 ; } sprintf (lcd_buf, "vol=%d" , Vol) ; lcd_gotoxy(0 , 0 ) ; lcd_clear() ; lcd_puts(lcd_buf) ; } ; }

#include int NewState,OldState,Vol,upState,downState; #asm .equ __lcd_port=0x12 ;PORTD #endasm #include #include interrupt void timer1_compa_isr(void) { NewState=PINB & 0b00000011; if(NewState!=OldState) { switch(OldState) { case 2: { if(NewState == 3) upState++; if(NewState == 0) downState++; break; } case 0: { if(NewState == 2) upState++; if(NewState == 1) downState++; break; } case 1: { if(NewState == 0) upState++; if(NewState == 3) downState++; break; } case 3: { if(NewState == 1) upState++; if(NewState == 2) downState++; break; } } OldState=NewState; } TCNT1H=0x00; TCNT1L=0x00; } void main(void) { char lcd_buf; // Input/Output Ports initialization // Port B initialization // Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In // State7=T State6=T State5=T State4=T State3=T State2=T State1=P State0=P PORTB=0x03; DDRB=0x00; // Timer/Counter 1 initialization // Clock source: System Clock // Clock value: 1000,000 kHz // Mode: CTC top=OCR1A // OC1A output: Discon. // OC1B output: Discon. // Noise Canceler: Off // Input Capture on Falling Edge // Timer 1 Overflow Interrupt: Off // Input Capture Interrupt: Off // Compare A Match Interrupt: On // Compare B Match Interrupt: Off TCCR1A=0x00; TCCR1B=0x0A; TCNT1H=0x00; TCNT1L=0x00; ICR1H=0x00; ICR1L=0x00; OCR1AH=0x03; OCR1AL=0xE8; OCR1BH=0x00; OCR1BL=0x00; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK=0x10; // Global enable interrupts #asm("sei") lcd_init(8); while (1) { if (upState >= 4) { Vol++; upState = 0; } if (downState >= 4) { Vol--; downState = 0; } sprintf(lcd_buf,"vol=%d",Vol); lcd_gotoxy(0,0); lcd_clear(); lcd_puts(lcd_buf); }; }

В качестве пояснений: таймер 1 настроен на срабатывание 1000 раз в секунду, строкой NewState=PINB & 0b00000011; считываем состояние ножек 0 и 1 портаВ. if(NewState!=OldState) если состояние не изменилось, значит вращения нет.
Если состояние изменилось конструкция switch определяет в какую сторону было произведено вращение, в зависимости от этого увеличивается значение переменной downState(влево) или upState(вправо).

От щелчка до следующего щелчка 4 изменения состояния, поэтому 1 раз за 4 импульса изменяем переменную Vol. Ее же и выводим на дисплей. Прошивка доступна

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

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

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

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

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


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

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

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

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

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

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

В качестве примера рассмотрим работу энкодера в связке с микроконтроллером 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.

Принцип действия, схема включения и исходник библиотеки для работы с инкрементным энкодером уже рассматривался мной в одной из статей. Сегодня мы поговорим о практическом применении энкодера. В качестве примера я выбрал программу генератора прямоугольного сигнала с диапазоном рабочих частот 1 – 100 Гц. Первоначальный замысел предполагал диапазон 1 - 1000 Гц, но на практике выяснилось, что перебирать тысячу значений утомительно даже с энкодером.

Подготовка

Создаем в пустом workspace`е новый проект

Project > Create New Project…

Тип шаблона C > main

Копируем в папку проекта файлы исходника библиотеки для работы с энкодером
encoder.h и encoder.c

Подключаем к нашему проекту файл encoder.c
Правая кнопка мышки в окне workspace и в открывшемся меню Add > Add Files…

Копируем файл bits_macros.h в папку проекта.


Подключаем заголовочные файлы

В начале файла main.c забиваем следующие строки
#include
#include
#include "encoder.h"
#include "bits_macros.h"

Задаем настройки проекта

Project > Options

Тип микроконтроллера
General Options > Target > Processor Configuration > ATMega8535

Разрешение использования имен битов определенных в заголовочных файлах
General Options > System > Enable bit defenitions...

Оптимизация кода по размеру
C/C++ Compiler > Optimisations >Size High

Тип выходного файла
Linker > Output File галочка Override default и поменять расширение на hex
Linker > Format > Other выбрать Intel Standart

Жмем Ок. Сохраняем проект и workspace.
Теперь у нас есть пустой проект с подключенной либой и заданными настройками.

Задача

Заставить микроконтроллер генерировать меандр с частотой от 1 до 100 Гц. Значение частоты должно задаваться с помощью энкодера. Поворот энкодера на одну позицию должен соответствовать изменению частоты генератора на 1 Гц.

Схема для нашего примера

К выводу, на котором будет генерироваться меандр, подключен светодиод, чтобы хоть как-то видеть результат работы программы. Вряд ли у многих под рукой осциллограф.

Алгоритм программы

Прямоугольный сигнал генерируется с помощью 16 разрядного таймера Т1, который работает в режиме СТС – сброс при совпадении. Во флэш памяти микроконтроллера храниться массив, содержащий для каждого значения требуемой частоты константу. Для доступа к элементам массива используется переменная pTimerValue. В прерываниях таймера Т1 значение константы считывается и записывается в регистр сравнения.

Для генерации сигнала используется вывод PD5 (OC1A). У него есть альтернативные функции – он может менять свое состояние на противоположное при равенстве счетного регистра и регистра сравнения.

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

В самом начале main`а располагается код инициализации периферии и необходимых переменных.

Структура программы

Для наглядности я изобразил структуру программы в виде диаграммы.

Это типовая структура построения простых программ. Прерывания естественно происходят в произвольном месте цикла.

  • Инициализация.
  • Бесконечный цикл (так называемый superloop), в котором происходит ожидание события, обычно в виде опроса флагов или какого-нибудь буфера.
  • Параллельная работа периферийных устройств, вызывающих прерывания. В них выполняется какой-то код (желательно короткий) и выставляются флаги.

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

Расчет констант для таймера Т1

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

Тактовая частота микроконтроллера 16 МГц (смотрите схему). Коэффициент предделителя таймера - 256. Он позволяет получить прерывания с любой частотой из нашего диапазона.

Период одного тика таймера будет равен 1/(16 МГц/ 256) = 16 мкс

На выводе PD5 нам нужно получить сигнал частотой 1 Гц. Вывод меняет свое состояние на каждое прерывание таймера и значит, частота прерываний должна быть в 2 раза больше. Для нашего случая - 2 Гц.

Сколько тиков таймера уложится в 2 Герца? (1/2 Гц)/16 мкс = 31250
Это и есть искомая константа.

Остальные значения рассчитываются аналогично. Я для этого обычно использую Exel.


Полученные значения мы помещаем в массив

__flash unsigned int timerValue =
{

сохраняем его в отдельном файле – timer_value.h и подлючаем его к файлу main.c

#include "timer_value.h"

Да, еще нужно добавить парочку констант в этот файл

#define MAX_TIM_VALUE 99
#define MIN_TIM_VALUE 0

Убедимся, что правильно рассчитали константы для таймера. Запустим его. Код программы будет такой.

//программирование AVR на Си

//сайт 17.10.09
#include
#include
#include "encoder.h"
#include "bits_macros.h"
#include "timer_value.h"

//индекс для доступа к элементам массива
volatile unsigned char pTimerValue = 0;

int main(void )
{
//инициализация таймера Т1
TCNT1 = 0;
TCCR1A = (0<TCCR1B = (0<

//настройка вывода PD5 на выход
SetBit(PORTD, PD5);
SetBit(DDRD, PD5);

//ничего не делаем в бесконечном цикле
while (1);
return 0;
}

Думаю, пояснения требует только кусок инициализации таймера.

Обнуление счетного регистра
TCNT1 = 0;

Инициализация конфигурационных регистров таймера Т1.
TCCR1A = (0<TCCR1B = (0<

Где биты WGM13, WGM12, WGM11, WGM10 задают режим работы таймера – СТС,
CS12, CS11, CS10 – определяют коэффициент предделителя таймера –256,

COM1A1, COM1A0 – определяют поведение вывода PD5(OC1F) – в данном случае по сигналу таймера он будет менять свое состояние на противоположное


Инициализация регистра совпадения начальным значением.
OCR1A = timerValue;

Компилируем программу и грузим в микроконтроллер. Светодиод должен моргать с частотой 1 Гц.
В программе нет никаких прерываний. Нет никаких манипуляций с выводом PD5. Однако светодиод моргает!

Программа

Теперь нужно “прикрутить” к этой программе энкодер. Зададим настройки в хедер файле encoder.h – порт и выводы, к которым подключен энкодер, значения констант.


#define PORT_Enc PORTA
#define PIN_Enc PINA
#define DDR_Enc DDRA
#define Pin1_Enc 2
#define Pin2_Enc 1

#define RIGHT_SPIN 0x01
#define LEFT_SPIN 0xff

Хедер содержит прототипы трех функций. Вспомним их назначение.

void ENC_InitEncoder(void) настраивает выводы микроконтроллера, к которым подключен энкодер на вход. Эту функцию нужно вызвать в начале main`а.


void ENC_PollEncoder(void) – однократно опрашивает энкодер, анализирует текущее и предыдущее состояния и записывает в буфер соответствующие константы (RIGHT_SPIN и LEFT_SPIN). Эта функция будет сидеть в прерывании таймера Т0.


unsigned char ENC_GetStateEncoder(void) – возвращает содержимое буфера энкодера. Если поворот на одну позицию не был зафиксирован – функция вернет 0, если поворот был зафиксирован функция вернет значение соответствующей константы. При этом значение буфера очистится. Эта функция будет вызываться в основном программе – в цикле while.


Дополняем нашу программу. Можете попробовать сделать это самостоятельно.

//программирование AVR на Си
//пример использования энкодера
//сайт 17.10.09

#include
#include
#include "encoder.h"
#include "bits_macros.h"
#include "timer_value.h"

#define TCNT0_const 253
#define TCCR0_const 5

volatile unsigned char pTimerValue = 0;

int main(void )
{
ENC_InitEncoder();

//инициализация таймера т0
TCNT0 = TCNT0_const;
TCCR0 = TCCR0_const;

//инициализация таймера т1
TCNT1 = 0;
TCCR1A = (0<TCCR1B = (0<OCR1A = timerValue;

//разрешение прерываний от таймеров
//т0 - по переполнению, т1 - по совпадению

TIMSK = (1<

//настраиваем PD5 на выход
SetBit(PORTD, PD5);
SetBit(DDRD, PD5);

__enable_interrupt ();
while (1){
//считываем содержимое буфера энкодера
//после считывания он очищается

unsigned char stateEnc = ENC_GetStateEncoder();

//если не пустой
if (stateEnc != 0){
//определяем направление вращения и изменяем переменную timerValue
if (stateEnc == RIGHT_SPIN){
if (pTimerValue == MAX_TIM_VALUE) pTimerValue = MIN_TIM_VALUE;
else pTimerValue++;
}
if (stateEnc == LEFT_SPIN) {
if (pTimerValue == MIN_TIM_VALUE) pTimerValue = MAX_TIM_VALUE;
else pTimerValue--;
}
}
}
return 0;
}

//опрос энкодера
#pragma vector=TIMER0_OVF_vect
__interrupt void timer0_ovf_my(void )
{
TCNT0 = TCNT0_const;
ENC_PollEncoder();
}

#pragma vector=TIMER1_COMPA_vect
__interrupt void timer1_compa_my(void )
{
//обновляем значение регистра стравнения
OCR1A = timerValue;
}

Вроде все должно быть понятно.
Кусок кода, в котором изменяется значение pTimerValue, можно было бы написать еще так:

if (stateEnc != 0) {
pTimerValue = pTimerValue + stateEnc;
if (pTimerValue == (MAX_TIM_VALUE + 1)) pTimerValue = MIN_TIM_VALUE;
else if (pTimerValue == (MIN_TIM_VALUE - 1)) pTimerValue = MAX_TIM_VALUE;
}

При вращении энкодера вправо pTimerValue складывается с 1, то есть инкрементируется.

При вращении энкодера влево pTimerValue складывается с 0хff, что равносильно вычитанию 1. Одна и та же операция, а результат прямо противоположный.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: