AVR. Учебный Курс. Инкрементальный энкодер.

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

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

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

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

А дальше пляшем от типа энкодера. А они бывают разные.

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

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

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

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

Обычно, все пытаются работать с ними через прерывания 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);	// Перезапускаем задачу через таймер диспетчера
}

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

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

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

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

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

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

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

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

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

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

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

116 thoughts on “AVR. Учебный Курс. Инкрементальный энкодер.”

  1. Если делать все совсем правильно то у нашего автомата 16 состояний:

    Enc_state.0 = Phase_a
    Enc_state.1 = Phase_b
    Enc_state.2 = Phase_a_l ‘прошлое состояние
    Enc_state.3 = Phase_b_l ‘прошлое состояние

    Select Case Enc_state
    Case &B00000000 : ‘не изменилось
    Case &B00000001 : Count = Count — 1 ‘вниз
    Case &B00000011 : Err_c = Err_c + 1 ‘запрещенное состояние
    Case &B00000010 : Count = Count + 1 ‘вверх
    Case &B00000110 : Err_c = Err_c + 1 ‘запрешенное состояние
    Case &B00000111 : Count = Count — 1 ‘вниз
    Case &B00000101 : ‘не изменилось
    Case &B00000100 : Count = Count + 1 ‘вверх
    Case &B00001100 : Err_c = Err_c + 1 ‘запрешенное состояние
    Case &B00001101 : Count = Count + 1 ‘вверх
    Case &B00001111 : ‘не изменилось
    Case &B00001110 : Count = Count — 1 ‘вниз
    Case &B00001010 : ‘не изменилось
    Case &B00001011 : Count = Count + 1 ‘вверх
    Case &B00001001 : Err_c = Err_c + 1 ‘запрешенное состояние
    Case &B00001000 : Count = Count — 1 ‘вниз
    Case Else Return
    End Select

    Phase_a_l = Phase_a ‘перекладываем новое в старое
    Phase_b_l = Phase_b ‘перекладываем новое в старое

    Код написан на баскоме.

    Опрос скоростных энкодеров надо делать на специализированых микрушках, со встроеным декодером, оно сразу отвечает на сколько повернулось и все.
    Кстати на 2313 методом тупого опроса ножек 500об/мин при 2000 импульсов на оборот ловилось без проблем, быстрее тупо не крутилось.

        1. А по мне гораздо лаконичнее чем мешанина из паскаля. Но это уже холивар, который не входит в цели этого сайта :) Всёравно мне ничего доказывать не хочется — нравится паскаль, пишите на паскале. Другим читать правда вырвиглазно.

        1. Насчет самого короткого эт ты зря.
          Выборка из памяти по смещению — минимум три команды. Инициализация массива — еще дочерта команд (минимум16) + держать в ОЗУ эту херь постоянно (а ОЗУ вообще может и не быть!). Проверка условия, и наконец, инкремент. Получается весьма жирно.

          1. Ну так надо массив в флеше располагать есесно. Тогда останется загрузить начало массива в X, добавить смещение, считайть байт из флеша.
            Если не считать количество ошибок (а оно вообще надо?), останется сложить два регистра.
            Если делать case или вычислять адрес перехода, вряд ли будет быстрее. И главное — с вычислением адреса перехода уже не получается универсально с точки зрения различных языков програмирования.

            Кстати, не знаешь как сделать ijump на сях?

            1. а из флеша его долго доставать, впрочем можно и так.

              А что void указатели уже отменили? Делаешь указатель на функцию и все. Впрочем да, придется ваять ряд меток аки для GOTO ЕМНИП си их поддерживает, но редко кто их пользует. Вот можно таблицу сварганопупить. Хотя по идее компилятор это должен сделать сам, когда встречает case сплошняком. Щас проверю…

              1. Если ты хочешь сделать ijmp, то реально быстро будет, если ты возьмешь аргумент, умножишь его на константу и сделаешь ijmp, либо icall.

                Сделать это реально имхо только на ассемблере, ну да фиг с ним.
                Получим 16 блоков одинаковой длины (минимум — два слова — inc Rxx и jmp в конец либо inc Rxx и ret). Это будет быстро выполняться, но будет занимать 16*2 слова работы +подготовка аргумента регистра Z (установка первоначального значения, добавления умноженного аргумента)+ijmp (2 два такта)+inc Rxx+ rjmp в конец(еще два такта).

                Если берем массив states из флеша, то: устанавливаем X (Y,Z), увеличиваем его на аргумент (не умноженный, поэтому на один такт быстрее, сдвигать не надо), читаем значение (да, три такта), добавляем к текущему положению. Если надо обработать ошибку — обрабатываем. Занимать в флеше будет 16 байт константы+ код подготовки данных.

                Итого — имхо если не надо обрабатывать ошибки, по скорости примерно одинаково, по объему флеша — вариант с массивом в флеше раза в два меньше.

            2. Чето не получилось у меня сварганить ijmp — не хотят адреса меток загоняться в массив, хоть ты тресни. Только icall — функции нормально адрес берут.

              А на функциях, табличный кейс выглядит так:

              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
              
              // Варианты действий
              void M1(void)
              {
              UDR = 1;
              }
               
              void M2(void)
              {
              UDR = 2;
              }
               
              void M3(void)
              {
              UDR = 3;
              }
               
              void M4(void)
              {
              UDR = 4;
              }
               
              //Таблица переходов
              static TPTR CaseSW[] PROGMEM =
              	{
              	&M1,
              	&M2,
              	&M3,
              	};
               
              //Собственно, процесс выбора:
              f=(void*)pgm_read_word_near(CaseSW+EncState);
              f();

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

              1. Пасиб, буду знать как icall делать!

                прикол в том, что при переходе по таблице придется точно также считать из флеша, но не байт, а целое слово (адрес перехода) :-)

      1. кстати, прикольный косяк!

        вот тут :
        u08 states[] = new u08[]{0, -1, 0xFF, 1, 0xFF, -1, 0, 1, 0xFF, 1, 0, -1, 0, 1, 0xFF, -1};

        косяк заключается в том, что 0xFF и -1 — Одно и то же :-))). Надо другую константу выбрать.

    1. Я тут мучаю РВ2 и решил почитать про энкодер. У VEC7OR довольно интересный подход и я решил его перевести в асм. Но сначала несколько выводов 1: если коды старый и новый код одинаковые то мы не крутим. 2: если коды являются инверсиями друг друга то это ошибка. 3: если посмотреть на коды в слитом состоянии как у VEC7OR то напрашивается вывод что 1,7,8,14(в десятичном виде) это коды на действие убавить 4: а 2,4,11,13 это коды на действие прибавить. Теперь если это расписать то у меня получилось вот так. Симулятор показывает что все верно.
      http://easyelectronics.ru/repository.php?act=view&id=96

  2. Делал на Альтере счетчик для подобного енкодера… Только там скорости на много порядков больше были (360 тысяч периодов на оборот при сотнях оборотов в секунду). Если надо кому — обращайтесь.

  3. Можно уменьшить число проверок (и время выполнения), проверяя не оба направления изменения, а только одно, приняв другое (более вероятное) по умолчанию.
    Также предварительно проверять, было ли перемещение. Вот одометр моего робота на 2 колеса. Оптопары на портах B4,B5 и B6,B7:

    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
    
    // Одометр!
        D_kol := PORTB and $F0;
        if D_kol  <> D_kol_old then // Если сдвинулись!
          begin
            D_kol_x     := D_kol      and $C0;
            D_kol_x_old := D_kol_old  and $C0;
            if D_kol_x  <> D_kol_x_old then // Если левое колесо повернулось
              begin
                Fl_napr_L := 0; // Вперед!
                Case D_kol_x of
                  $00 : if D_kol_x_old = $80 then Fl_napr_L := 1; // Назад!
                  $40 : if D_kol_x_old = $00 then Fl_napr_L := 1; // Назад!
                  $80 : if D_kol_x_old = $C0 then Fl_napr_L := 1; // Назад!
                  $C0 : if D_kol_x_old = $40 then Fl_napr_L := 1; // Назад!
                 end;
          // Считаем путь!
                if Fl_napr_L = 0 then DWL_6 := DWL_6 + 1 else DWL_6 := DWL_6 - 1;
              end;
            D_kol_x     := D_kol      and $30;
            D_kol_x_old := D_kol_old  and $30;
            if D_kol_x  <> D_kol_x_old then // Если правое колесо повернулось
              begin
                Fl_napr_P := 0; // Вперед!
                Case D_kol_x of
                  $00 : if D_kol_x_old = $20 then Fl_napr_P := 1; // Назад!
                  $10 : if D_kol_x_old = $00 then Fl_napr_P := 1; // Назад!
                  $20 : if D_kol_x_old = $30 then Fl_napr_P := 1; // Назад!
                  $30 : if D_kol_x_old = $10 then Fl_napr_P := 1; // Назад!
                 end;
          // Считаем путь!
                if Fl_napr_P = 0 then DWP_6 := DWP_6 + 1 else DWP_6 := DWP_6 - 1;
              end;
          end;
        D_kol_old := D_kol;
    // Конец  Одометр!
    Вертится в главном цикле. (время прогона цикла около 250 мкс).
        1. Вот в этих строках:
          if D_kol «не равно» D_kol_old then // Если сдвинулись!
          if D_kol_x «не равно» D_kol_x_old then // Если левое колесо повернулось
          if D_kol_x «не равно» D_kol_x_old then // Если правое колесо повернулось
          В Паскале «не равно» пишут в виде пары «меньше» «больше».
          Без отступов тоже наглядность ухудшается…

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

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

        1. Не будет там дополнительных веток. Суди сам — вначале вычисляем полученный код два бита прошлого значения+два бита текущего. Потом, этот код юзаем как ключ для вычисления адреса по переходу сразу в нужное действие. ВСЕ! Больше проверок нет. Сразу же действие. Причем если во всех вариантах у нас икремент/декремент (не важно счетчика данных или счетчика ошибок) то можно сделать так, что проход по всем вариантом будет одинаковой длительности, такт в такт. Разумеется писать на ассемблере. ИМХО в таком случае четкое время выполнения всех ветвей это большой плюс.

      1. Несмотря на отличное знание английского языка и возможно (не видал, не могу судить), лучший получаемый код, что-то в интернете не так уж много попадалось чего-то толкового(скажем так, чтобы не обижать), написанного «любителями перла»… Или они упорно скрываются, чтобы не сглазили…

          1. «2) За такое: if D_kol_x D_kol_x_old then меня сразу с работы уволят, если я такое выдам. ;)»
            Так я уже выше указал, что при сохранении знаки “больше” — “меньше” теряются. Вот когда DI HALT восстановил начальный вид, стало вполне нормально.
            «1) Ваш код рид онли» Ошибаетесь. Он уже второй год прекрасно работает в реальном бегающем роботе. Чего и вам желаю. И многие мои коды работали уже четверть века назад, и не только дома.

  4. Эмм. Чего то вы, ребята, намутили тут сложного!
    Берем два пина — один — прерывание по по смене сигнала с 0 в 1, второй считываем по прерыванию первого, если считали 1, то повернули в одну сторону, если считали 0, то повернули в другую сторону. Количество прерываний — количество щелчков.

    Все прерывание выглядит так:
    if (bit_is_clear(BUTTONS_PIN, BUTTONA_PIN))
    {
    if (bit_is_set(BUTTONS_PIN, BUTTONB_PIN))
    Button.Dir—;
    else
    Button.Dir++;
    }

    1. А. Малость ошибся — то не само прерывание а функция, которая у меня запускается по таймеру после прерывания (чтобы отфильтровать дребезг, как раз таки), но смысл, все равно, остается тот же — все просто!

      Ну и прерывание я там указал с 0 в 1, а надо наоборот с 1 в 0. Ну это кто как подключит этот энкодер.
      Я с таким энкодером уже кучу проектов понаделал, один из них успешно продается на просторах немеции ;-) Ссылок могу накидать, если что.

      1. У меня там есть и на прерываниях (последний абзац, без примера кода) и на опросах. Прерывание это ценный ресурс. Его можно пустить и на более интересные вещи.

    2. У меня внешнее прерывание для другого уже занято. И мне удобнее делать опрос в главном цикле — проще и никаких проблем. Быстродействия большого тоже не надо — колесо делает 1, максимум 2 оборота в сек, а с датчиками на отражение много штрихов не сделаешь (~20-40 на оборот, в зависимости от диаметра окружности). Сейчас думаю, стоит ли ставить оптопары на просвет, но и в таком случае на 1 смену состояния будет несколько опросов. Кроме того, в вашем варианте нет защиты от ситуации, когда при перегрузке колесо не вращается, а дергается или вибрирует (довольно обычное явление в маломощных устройствах), при этом у вас возможен быстрый счет в одну сторону, накручивающий ложный путь, у меня же будет поочередно идти +1 -1, хоть часами дрожи. Я уже анализировал подобную ситуацию. Да и у меня тоже по 2 пина на колесо, как и у вас. Только вам еще нужно и 2 прерывания. Как не крути, явное усложнение при отсутствии каких-либо преимуществ. Даже по коду выигрыша не будет, а при прерываниях еще и контекст надо сохранять. Я ведь не первое пришедшее на ум использую, а анализирую кучу вариантов, выбирая из них более оптимальный по нескольким критериям.

      1. >Только вам еще нужно и 2 прерывания.
        Я там вроде как про одно прерывание писал ;-) Один пин на прерывание, и в ЭТОМ прерывании считываем второй пин.
        Про дрожание — да, если энкодер используется не такой, как у автора — в качестве пользовательского интерфейса, так как именно тут особой точности не надо. В вашем же случае, как я понимаю, вы используете энкодер в своем роботе для отслеживания пройденного пути, тут и подход совершенно другой, так как требования другие.

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

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

  5. Что-то мутно как-то. Давно же уже на электрониксе обсосали тему. Два сигнала энкодера — это собственно два младших бита положения в коде Грея. Дальше надо только перенос проверить.

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

    Все это приводит к 30 что-ли тактам на обработку.

    1. Да хоть горшком назови. Что изменится от того, что состояния 2х бит попытаетесь привязать к коду Грея? Их что, обрабатывать не надо будет? Да и еще учитывать несовпадение с Греем при одном из переходов, когда изменятся оба бита сразу. Что это даст в реальности, кроме еще одного навешенного ярлыка? И что в этом лучше того, что уже было изложено выше?

  6. Уважаемый DI HALT! Очень нравится как Вы преподносите материал, эта статья не исключение. Вы сейчас заново переделываете весь учебный курс, хотелось бы чтобы в курсе был урок о том, как использовать код на других контроллерах данного семейства, какие проблемы при этом могут возникнуть, как лучше использовать уже отлаженный на одном контроллере код на аналогичном. Недавно встретил в сети готовые библиотеки на ассемблере от Pa5ha и ARV на протокол 1-Ware и LCD-дисплей, с подробным описанием и рекомендациями. Очень хотелось бы увидеть на Вашем сайте сборник библиотек. Энкодер — устройство которое можно применять не только в роботах, мне недавно очень понравилась схема кухонного таймера, где в из органов управления только кнопка и колесик от мыши.

  7. Хорошая статейка. Особенно переделанный вариант :)
    А вообще, давно уже думал над тем, чтобы использовать оптодатчики из старых механических мышей. Думал, что там обычные фотодиоды стоят, и не понимал, как 1 диод может передавать направление. А щас доперло, что там как раз энкодеры стоят :) Только оптические :) Теперь знаю, что с ними надо будет делать, чтобы определить направление…

  8. делюсь своим обработчиком.

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

    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
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    
    #define EncA    4
    #define EncB    5
    #define EncSw   6
    #define EncIN   PINB
    #define EncPort PORTB
    #define EncMask ((1<<EncB)|(1<<EncA)|(1<<EncSw))
     
    unsigned char EncN = 0, EncOld[4] = {0, 0, 1, 0};
     
    void Encoder (unsigned char *EncData,
                  unsigned char EncStep,
                  unsigned char EncDataLow,
                  unsigned char EncDataHigh,
                  bool EncRotate)
    {
      unsigned char EncTemp;
      EncTemp = EncIN;
      __delay_cycles(700_uS); // антидребезговая задержка
      EncTemp |= EncIN;
    #if(EncA < EncB) //поиск младшего бита
      EncTemp >>= EncA;
    #else
      EncTemp >>= EncB;
    #endif
      EncTemp &= 0x03; 
      if (EncN < 4)
      {
        if (((EncOld[0] == 0x03)&&(EncTemp != EncOld[EncN - 1]))||
            ((EncOld[0] == 0x00)&&(EncTemp == 0x03))) // проверка последовательности комманд от энкодера
        {
          EncOld[EncN] = EncTemp;
          EncN ++;
        }
      }
      else
      {
          if ((EncOld[1] == 0x01)&&
              (EncOld[2] == 0x00)&&
              (EncOld[3] == 0x02)) // проверка совпадения данных принятых с энкодера с прямой последовательностью
          {
            if (*EncData < EncDataHigh)
              *EncData += EncStep;
            else if (EncRotate == 1)
              *EncData = EncDataLow;
          }
          else
            if ((EncOld[1] == 0x02)&&
                (EncOld[2] == 0x00)&&
                (EncOld[3] == 0x01)) // проверка совпадения данных принятых с энкодера с обратной последовательностью
            {
              if (*EncData > EncDataLow)
                *EncData -= EncStep;
              else if (EncRotate == 1)
                *EncData = EncDataHigh;
            }
        EncN = 0; // обнуление данных
        EncOld[0] = 0x00;
        EncOld[1] = 0x00;
        EncOld[2] = 0x01;
        EncOld[3] = 0x00;
      }
    }
    1. Добавлю.

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

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

      Вот тут есть модель меню с использованием оного девайса (видео мое).
      http://www.youtube.com/watch?v=ksDp4tffhjg

      Кстати, очень актуальна тема с меню. можно бы пост создать и обменяться опытом.

  9. Привет DIHALT, вопрос есть. Есть энкодер на 24 импульса оборот. При вращении вправо на LCD дисплей будет выводится число от 0 до 25 с шагом 0.1. При вращении влево наоборот, от 25 до 0. Но! Если начать вращать быстрее шаг должен стать 1. Это реально так сделать? Если да, то как? Может у тебя есть какие мысли на это счет?

    1. Засекать время задержки между импульсами. Если оно велико тогда 0.1, если мало (быстро крутят) — тогда 1. Еще вариант — считать количество импульсов за единицу времени (по принципу частотомера). Если частота выше пороговой — тогда крутят быстро ;)

  10. Что-то я не понимаю: что значат щелчки и что такое импульсы?
    «кроме импульсов есть еще тактильные щелчки» — вот эта фраза меня сбивает с понимания: там же всего три вывода, откуда щелчки взялись?)

    И в инете про энкодеры как-то не густо…

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

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

  12. Хм… понравился вариант с массивчиком. Вот вариант поближе к боевому. Всяко работает в железе.

    [code]
    #include «EncPoll.h»

    const int8_t EncState[] PROGMEM =
    {
    0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0
    };

    uint8_t EncPoll( void )
    {
    static uint8_t EncVal=0;
    static uint8_t Enc=0;

    Enc += PIN(ENCPOLL_PORT) & ((1<<ENCPOLL_A_PIN)|(1<<ENCPOLL_B_PIN));
    EncVal += pgm_read_byte(&(EncState[Enc]));
    Enc < 0xfe) EncVal = 0;
    if (EncVal > 200) EncVal = 200;

    return EncVal;
    }

    int8_t EncPollDelta( void )
    {
    static uint8_t Enc=0;

    Enc <<= 2;
    Enc &= 0b00001111;
    Enc += PIN(ENCPOLL_PORT) & ((1<<ENCPOLL_A_PIN)|(1<<ENCPOLL_B_PIN));

    return pgm_read_byte(&(EncState[Enc]));
    }
    [/code]

  13. Друзья, а есть ли где-нибудь на просторах интернета модели энкодера для протеуса? Чего-то я весь гугль перерыл, нашел только какую-то старую модель от 2006 года, которая, сцуко, даже прилепленная средней ногой к земле, не может даже пересилить внутреннюю подтяжку меги. Не говоря уж о том, что сигнал она выдает совсем не как в даташитах. Импульс на ноге A в два раза длинне, чем на ноге B получается. Я уж думаю, может подцепить к меге еще и тиньку 13 с двумя кнопками, которя будет сигнал энкодера эмулировать?! :)))

      1. Похоже, ты не дочитал мой комментарий до конца, но за оперативность комментирования спасибо. И мыслим мы точно в одинаковом направлении, что не может не радовать ;)

  14. не пойму, на кой вручную битовый поток от энкодеров молотить ..

    в таймерах того же STM32 есть аппаратные режимы, которые и положение и в связке с другим таймером — скорость посчитают. AVR любительский чип, если использовать энкодеры с нормальным разрешением в 4,000-10,000 ppc, то ваш алгоритм — коту под хвост.

    Но если хочется занять % — то можно и поизголяться.

      1. Вместо того чтобы «поржать» — обратитесь к документации SMT32 и его таймерам, поток от энкодера обрабатывается — аппаратно. События, флаги, етс — выставляются соответственно.

        Насчет «всосет» и ПЛИС — не лучшие аргументы.

        1. Да я знаю, только частоты у порта до 50мгц. С 200мгц на которых может работать средняя плиска даже рядом не стояла.

          Меня позабавила характеристика «Любительский» то, что контроллер семейства которому уже лет 10 как уступает новому stm32 это понятно. Однако это не делает AVR контроллером для любителей.

  15. Всегда смешно читать суждения человека, ни черта непонимающего в том о чем пишет , какие бы ни были его достижения в чем то другом .
    Это ламерство уважаемый .
    У С дурацкий синтакс … ну ну …
    Вот только почему то всегда такие «перлы» выдают люди знающие только какой то один камень …

  16. Есть простой алгоритм обхода дребезга — регистрировать не только подъёмы, но и спады.
    Соответственно, если подъём На канале A соответствует +1, то спад при том же уровне на канале B соответствует как -1.

    Тогда шумы дребезга в процессе смены уровня тупо +-1 сделают несколько раз, но в результате окажется всё-равно +1 (или -1).

    Главное предусмотреть ситуации если прерывание захлебнётся (когда МК ещё не доотработал прерывание, а канал A успех поменяться ещё несколько раз своё состояние… Такой вот злой шум :)… )

    Сорри, если кого дублирую…

  17. DI HALT, Объясните «для дурачков» плиз, у меня энкодер 30 щелчков, 15 импульсов, так указано в ДШ.
    Что значит 15 импульсов, это когда делаешь полный оборот на каждой ноге будет 15 импульсов (с учетом подтяжки)?
    Я опрашиваю энкодер с частотой 625 Гц, все хорошо работает, дребезга нет, но со щелчками не совпадает. Можно пример на пальцах, что надо делать, умножать или делить, чтобы попадать в щелчки?

    1. Да, 15 периодов импульса на оборот. Что сильно не совпадает?

      Умножать и делить не надо ничего. Главное четко отслеживать каждый щелчок. Можно даже опросами не заниматься, а сделать на прерываниях с подавлением дребезга.

      1. Да вообще как-то все отдельно. Когда крутишь быстро — щелчки эти не парят, а когда медленно (по щелчку) как раз где-то на 2 щелчка одно срабатывание. У меня не на прерываниях, у меня просто обработчик энкодера в прерывании таймера по сравнению.

  18. Такая проблема. Надо подключить инкрементальный энкодер 5 В с тремя фазами к контроллеру, который питает 24 В входные элементы. Входной ток у контроллера 5 мА.
    Подскажите пожалуйста.

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

  20. Отлично! Именно то, что искал!
    Хочу собрать usb hid девайс для регулировки громкости.

    Одна только сложность: от чего бы отпилить рукоятку? Хорошо, если с куском корпуса.
    Где вообще можно найти крупную рукоять (от 5см в диаметре), в идеале из алюминия?

  21. Ребята, подскажите начинающему.
    В коде для инкрементальных написано: New = PINB & 0x03 . То есть PINB сравнивается с числом 11. А если мне нужно подключить в PB1 и PB2, с каким числом сравнивать чтобы сохранить код в таком виде, с 110?

      1. Тогда получается нужно наложить побитовую маску 0х06. И сделать так:
        New = PINB & 0x06;
        New = New >> 1
        А дальше как в оригинале статьи! И переменная EncData будет счётчиком.
        Ещё маленький вопросик: строка SetTimerTask(EncoderScan,1) — я так понимаю перезапускает всю эту функцию и в какой-то библиотеке описана?
        Спрашиваю, потому что мопед что-то заводится не хочет, блин.. =(

        1. SetTimerTask(EncoderScan,1) это перезапуск сей бодяги через 1мс посредством диспетчера задач и службы таймеров. Подробней в цикле статей про RTOS и конвеерный диспетчер.

          1. Я правильно понимаю принцип работы Инкрементального: крутим ручку, при этом два щелчка вывод замыкается с землёй, два щелчка — размыкается (и соответственно, при наличии подтягивающего резистора там +5В). На втором выводе всё то же самое, только сдвинуто на 1 щелчок?
            Просто сколько ни кручу — на выводах постоянно напряжение питания (все контакты прозвонил), в выключенном состоянии со средней ногой не замыкается… неисправен девайс или я безнадёжен?
            Прости за глупые вопросы, очень хочется разобраться.
            p.s. как бы предыдущий пост удалить?

  22. Два слова по поводу контроля по прерываниям (вариант с опросом, честно сказать, не очень нравится, во-первых, нужна большая частота опроса при работе с оптическим энкодером с приличным количеством импульсов на оборот, во-вторых — анализ всех состояний).
    1. При работе оптических энкодеров на промышленном оборудовании есть очень неприятный момент, когда энкодер стоит на месте и из-за вибрации оборудования слегка «болтается». Легко видеть, что при «болтании» в районе одного из фронтов и прерывании только по положительному (или только по отрицательному) фронту программа будет вовсю считать в ту или другую сторону. При стоящем оборудовании! То есть нужно работать по прерываниям по каждому фронту и анализировать предыдущее состояние. Так я, собственно, и делал, но тут пришла такая мысль.
    2. Если (при работе по прерываниям по обоим фронтам) внимательно посмотреть, то видно, что после прерывания по фронту (любому), при движении вправо возможны только варианты сигналов 01 и 10, а при движении влево — только 11 и 00. Т.е. состояние сигналов однозначно определяет направление. Причем, чтобы не считать двойное количество импульсов, достаточно взять по одной цифре для каждого направления, а остальные игнорировать. Но есть один ньюанс.
    3. Чтобы уйти от ошибок «болтанки», нужно брать цифры соответствующие одному фронту в двух направлениях, тогда при болтанке возле него будет компенсация +1/-1, а на другом фронте мы игнорируем, 0/0. Т.е., в сухом остатке: делаем прерывание по любому изменению сигнала на входе A, в обработчике проверяем сочетание сигналов — 10 — вправо, 00 — влево. (или 01 и 11 соответственно, без разницы), остальные игнорим. Элементарнейшие два сравнения, нет запоминания предыдущего состояния, нет таблиц, не говоря о полном анализе конечных автоматов. :)
    4. А теперь скажите, пожалуйста — я где-то дико туплю ( засыпаю и мой ДР только что закончился) или таки все гениальное просто? ;)

  23. а есть у кого пример на асме для атмеги например? я конечно и сам напишу, но вдруг…

    p.s. кстати, di halt, при авторизации по openid возникает обратный линк с неправильным путем (лишнее слово вордпресс в начале), если убрать то всё ок.

  24. Присоединяюсь к словам Tima. Тоже хотелось бы увидеть код на асме. В язык си пока что еще не пытался вникнуть. Я начинающий — пишу на асме, буду рад, если кто-то выложит пример кода для обработки энкодера на этом языке. Желательно с комментариями, раскрывающими суть того или иного действия. Цель — на один щелчок одна смена состояний некоторой переменной

      1. Уважаемый DI HALT, не могли бы Вы посмотреть на придуманный мной обработчик энкодера. Если Вам не сложно, прокомментируйте ошибки либо недочеты в данном алгоритме.
        Комментарии к блок схеме:
        • Счетчик ожиданий нужен для того, что бы программа не зациклилась и не зависла в случае, если вдруг энкодер остановиться не на щелчке (не на состоянии 00). Он считает циклы ненулей, а потом просто выходит из обработчика энкодера.
        • Проверка нулей и — так называемый доворот энкодера так же защищает от ложных циклически-повторяющихся срабатываний.
        • Инкремент и Декремент взяты условно и представляют собой какую либо реакцию на поворот энкодера.

    1. Мне кажется, на асме это должно выглядеть примерно так:

      Enc:
      in temp,PinB ;считываем состояние выводов
      cp temp1,temp ;если не поменялось, то выходим
      breq NoAction

      cpi temp,0x00 ;если поменялось, выбираем подходящий вариант
      breq case0

      cpi temp,0x01
      breq case1

      cpi temp,0x02
      breq case2

      cpi temp,0x03
      breq case3

      ;в темп1 хранится предыдущее состояние

      case0:
      cpi temp1,0x01 ;выбираем, куда идти дальше
      breq Action1

      cpi temp1,0x02
      breq Action2

      case1:
      cpi temp1,0x03
      breq Action1

      cpi temp1,0x00
      breq Action2

      case2:
      cpi temp1,0x00
      breq Action1

      cpi temp1,0x03
      breq Action2

      case3:
      cpi temp1,0x02
      breq Action1

      cpi temp1,0x01
      breq Action2

      Action1:
      mov temp1, temp ;копируем в темп1 новое состояние выводов
      nop

      Action2:
      mov temp1, temp ;копируем в темп1 новое состояние выводов
      nop

  25. А может проще сделать так:
    Входа: Sin, Cos, Zero (без прерываний) и третий с прерыванием (экономия интовых пинов, для тинек, например)
    На вспомогательном входе с прерывание сигнал формируется схемой логического ИЛИ трех сигналов Sin, Cos, Zero (тупо 3 диода и резюк).
    Возникло прерывание:
    Sin=1 -> Inc
    Cos=1 -> Dec
    Zero=1->Reset counter
    (Sin=1 & Cos=1) Or (Sin=1 & Zero=1) Or (Cos=1 & Zero=1) ->Error
    Так, по моему мнению, и алгоритм проще, и задержки (дребезг) рулить проще.
    «Разделяй и властвуй…» :-)

  26. Уважаемый, DI HALT, не могли бы вы на поподробнее разъяснить
    обоснование выбора 2-хбайтной переменной под счетчик. Вы пишете:

    «…т.е. полный период 3201_3201_3201 и один щелчок дает 4ре деления,
    что некрасиво. Поэтому я считаю до 1024, а потом делю сдвигом на четыре…»

    Вопросы:
    -почему полный период — это три(а не 24) последовательности 3201?
    -почему полный период и щелчок дает деления(а не сумму состояний)?
    -каким образом происходит счет до 1024 и что это дает в итоге?

    Извиняюсь за глупые вопросы, просто очень хочется понять.

  27. Конечно же прошло уже много лет, но:
    00 = 0
    01 = 1
    11 = 3
    10 = 2

    это код Грея, используя x ^ (x >> 1) для преобразования двухбитного кода Грея получаем на выходе 0, 1, 2, 3, соответственно вообще отпадает необходимость в switch

  28. Пример реализации алгоритма в статье по опросу энкодера, к сожалению алгоритмически не оптимален и технически не точен.

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

    Автор, хоть и молодец, что рассказал другим как работает энкодер, сам то так и не разобрался до конца.

    Инкремент/декремент должен происходить 1 раз за щелчек, а не суммировать все 4 переходных состояния 01 11 10 00.

    Причем во время дребезга, будет постоянная последовательность декрементов с инкрементом, которые в результате будут давать некоторую но не 100% фильтрацию.

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

    Вообще за старание 5, за практическую применимость 3.

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

  29. а у меня вот такая обработка получилась, имхо простенько и работает как часы:

    ISR (ENCODER_VECTOR){
    if (!ENC_GetState()) enc_rotation_flag = true;
    if (enc_rotation_flag) {
    if (ENC_GetState() == 1){
    if (enc_position) enc_position—;
    enc_rotation_flag = false;
    }
    if (ENC_GetState() == 2){
    if (enc_position != 255) enc_position++;
    enc_rotation_flag = false;
    }
    }
    }

    ENC_GetState возвращает 2-битное число с произвольных пинов произвольных портов

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