Энкодер - это устройство преобразования механического перемещения или угловых изменений положения в цифровой сигнал. В статье рассматривается самый популярный в DIY сообществе инкрементальный энкодер EC11 с кнопкой. При его вращении на выходах A и B формируются TTL сигналы в виде импульсов сдвинутые между собой по фазе на 90 градусов. Таким образом с его помощью, можно определить направление и скорость вращения, а так же рассчитать угол поворота. В отличие от потенциометров, энкодер KY-040 гораздо надежней и долговечный.

Немного подробностей

Собирая один из проектов с использованием encoder. Я не смог найти код для Ардуино выполняющий все мои условия. Так как для проекта нужно обрабатывать следующие команды: "Вращение без нажатия", "Вращение с нажатием", "Нажатие" и "Длинное нажатие", а так же требуется стабильная работа энкодера. Скетчи использующие один пин с прерыванием INT0 или INT1, работают отвратительно и при вращении вала энкодера вылетает очень много ошибок. Код без использования прерываний работает стабильно, но он не работает в фоновом режиме, его нужно встраивать в тело основной программы, что в свою очередь приводит к не своевременному срабатыванию обработчика и пропускам при вращении энкодера. Еще хуже обстоят дела с обработкой нажатия с вращением вала энкодера и обычным с нажатием. Пришлось написать свой код обработки, который исключает описанные выше проблемы. С дребезгом контактов я не стал бороться программно, так как это приводит к задержкам обработки. Проще и надежней использовать керамические конденсаторы.

Схема подключения энкодера к Ардуино

Для считывания сигналов с выходов EC-11, нужно использовать три цифровых входа Arduino. В схеме подключения я использовал редко используемые мной в своих проектах выводы Arduino(A1, A2 и A3). Внешние подтягивающие резисторы отсутствуют, так как я использовал внутреннюю подтяжку микроконтроллера. Конденсаторы нужны для гашения импульсов дребезга контактов. Если у вас новый и хороший энкодер, то можно обойтись и без них. Но на кнопку в любом случае потребуется конденсатор, так как ее дребезг неизбежен.

Используемые в схеме компоненты:

Arduino nano - 1 шт.
Энкодер EC11 -1 шт.
Соединительные повода - 4 шт.
Керамические конденсаторы 0,1 мкФ - 3 шт.

Скетч для Ардуино

Для того что бы отслеживать изменение положения энкодера в фоновом режиме, я использую прерывание PCINT1. Обработка всех функций происходит в прерывании, обработчик в зависимости от произошедшего действия изменяет переменную enc_state. Если значение переменной enc_state=0 - ничего не произошло, enc_state=1 - экодер вращался без нажатия, enc_state=2 - экодер вращался с нажатием, enc_state=3 - было нажатие на кнопку, enc_state=4 - было длинное нажатие на кнопку, Прерывание будет срабатывать каждый раз по изменению состояния входов, как с высокого уровня на низкий, так и наоборот. То есть при одном щелчке энкодера прерывание сработает 4 раза. Или по 2 раза для каждого из входов. Но обработчик выдаст сигнал поворота только 1 раз на все 4 прерывания.
Код обработчика при каждом срабатывании записывает в переменную lastcomb состояние входов, к которым подключен энкодер. И ждет состояние когда выходы A и B будут замкнуты на GND, это гарантированный сигнал того, что энкодер вращается. После того как этот сигнал получен, обработчик проверяет в какую сторону было вращение. Для этого он сравнивает его предыдущее значение из переменной lastcomb и в зависимости от фазы сдвига определит в какую сторону был поворот ротора. Как я писал ранее, сложнее всего отслеживать нажатие кнопки.
Так как использовать определенные тайминги я не планировал, потому, что они неизбежно приводят длительным задержкам работы обработчика и основной программы, или требуют использование таймера, которых в микроконтроллере всего 3 шт. их, как правило никогда не хватает. Собственно проблема состояла в том, чтобы разделить "нажатие с последующим вращением" от простого нажатия. В итоге как вы уже можете убедиться, я решил эту задачу. Оптимизацией кода я не стал заниматься, потому как все работает и меня все устраивает. Для наглядности в коде все действия с энкодером, отображаются в Serial мониторе программы Adruino IDE.

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

#define btn_long_push 1000   // Длительность долинного нажатия кнопки
volatile uint8_t lastcomb=7, enc_state, btn_push=0;
volatile int enc_rotation=0, btn_enc_rotate=0;
volatile boolean btn_press=0;
volatile uint32_t timer;

//********************************
void setup() 
{
  pinMode(A1,INPUT_PULLUP); // ENC-A
  pinMode(A2,INPUT_PULLUP); // ENC-B
  pinMode(A3,INPUT_PULLUP); // BUTTON

  PCICR =  0b00000010; // PCICR |= (1<<PCIE1); Включить прерывание PCINT1
  PCMSK1 = 0b00001110; // Разрешить прерывание для  A1, A2, A3
  
  Serial.begin(115200);
}

//****************************************
void loop() 
{
  switch (enc_state)
    {
  case 1:   {
            Serial.print("Вращение без нажатия ");
            Serial.println(enc_rotation);
            }
   break;
   
  case 2:  {
            Serial.print("Вращение с нажатием ");
            Serial.println(btn_enc_rotate);  
           }
    break;
    
  case 3: Serial.println("Нажатие кнопки ");
  break;

  case 4: Serial.println("Длинное нажатие кнопки ");
  break;
    }
  enc_state=0; //обнуляем статус энкодера
}

//****************************************
ISR (PCINT1_vect) //Обработчик прерывания от пинов A1, A2, A3
{
  uint8_t comb = bitRead(PINC, 3) << 2 | bitRead( PINC, 2)<<1 | bitRead(PINC, 1); //считываем состояние пинов энкодера и кнопки

 if (comb == 3 && lastcomb == 7) btn_press=1; //Если было нажатие кнопки, то меняем статус
 
 if (comb == 4)                         //Если было промежуточное положение энкодера, то проверяем его предыдущее состояние 
 {
    if (lastcomb == 5) --enc_rotation; //вращение по часовой стрелке
    if (lastcomb == 6) ++enc_rotation; //вращение против часовой
    enc_state=1;                       // был поворот энкодера    
    btn_enc_rotate=0;                  //обнулить показания вращения с нажатием
  }
  
   if (comb == 0)                      //Если было промежуточное положение энкодера и нажатие, то проверяем его предыдущее состояние 
   {
    if (lastcomb == 1) --btn_enc_rotate; //вращение по часовой стрелке
    if (lastcomb == 2) ++btn_enc_rotate; //вращение против частовой
    enc_state=2;                        // был поворот энкодера с нажатием  
    enc_rotation=0;                     //обнулить показания вращения без нажатия
    btn_press=0;                         //обнулить показания кнопки
   }

   if (comb == 7 && lastcomb == 3 && btn_press) //Если было отпускание кнопки, то проверяем ее предыдущее состояние 
   {
    if (millis() - timer > btn_long_push)         // проверяем сколько прошло миллисекунд
    {
      enc_state=4;                              // было длинное нажатие 
    } else {
             enc_state=3;                    // было нажатие 
            }
      btn_press=0;                           //обнулить статус кнопки
    }
   
  timer = millis();                       //сброс таймера
  lastcomb = comb;                        //сохраняем текущее состояние энкодера
}

Заключение

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

Если у Вас остались вопросы и замечания, пишите их в комментариях. Я с удовольствием на них отвечу.

Комментарии (101)


  1. Maxon-top
    31.10.2021 18:57
    +1

    А где Вы такую лампу купили?


    1. CyberBot Автор
      31.10.2021 18:59
      +5

      Сам сделал. Кольцо ws2812 на 45 светодиодов


  1. anonymous
    00.00.0000 00:00


    1. CyberBot Автор
      31.10.2021 19:31

      Если в мышь установить потенциометр вместо энкодера, то он умрет через 1 неделю интенсивного использования.
      Для исключения дребезга контактов программно нужна задержка и немного дополнительного кода. И это все равно не избавит от ошибок.
      Слишком Вы упрощаете. Если посмотреть на библиотеку analogRead повнимательней, да еще добавить обработку всех считанных показаний потенциометра, то быстрее не получится. Тем более при каждом чтении на АЦП программно выдерживается пауза, для того что бы входной конденсатор разрядился


      1. Serge78rus
        31.10.2021 20:07

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


  1. anonymous
    00.00.0000 00:00


  1. CyberBot Автор
    31.10.2021 19:56

    На видео настольная лампа электронщика с энкодером. В течении 1 недели смонтирую видео про него и выложу


  1. anonymous
    00.00.0000 00:00


  1. CyberBot Автор
    31.10.2021 20:33

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


    1. sterr
      01.11.2021 19:33
      +1

      Вы не сделали еще одну важную деталь - изменение скорости прокрутки. Допустим вам надо регулировать от 0 до 100. И каждый щелчок увеличивает переменную на 1. И если крутить просто по программе то надо сделать 100 щелчков. А в нормальной аппаратуре количество прибавления зависит от скорости вращения энкодера. Медленно вращаем - приращивается на 1, быстрее - на 2, еще быстрее на 3 и т.д.


  1. anonymous
    00.00.0000 00:00


    1. Mike-M
      01.11.2021 22:11

      Вроде бы и крутишь, но часть щелчков фейковые.
      Это известная проблема. Из описания Transistor Tester: "The figure 2.8 shows a rotary pulse encoder, which has not only bouncing contacts, but also has a unstable state of one of the switches at the indexed position (detent)".


  1. serafims
    31.10.2021 23:21
    +1

    Допустим, вам надо регулировать условную яркость из двух мест одновременно.

    Конечно, есть моторизованные потенциометры, но программно синхронизировать уставку параметра, пришедшую "извне" со с переменной, привязанной к энкодеру - проще.

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


  1. Tomasina
    01.11.2021 00:35

    analogWrite(9, readEncoder() );

    Вот и весь код.


  1. igarkam
    31.10.2021 20:36

    У Алекса Гайвера есть неплохая библиотека собственной разработки для работы с энкодером. https://alexgyver.ru/encoder/


    1. CyberBot Автор
      31.10.2021 20:42

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


      1. AlexGyver
        02.11.2021 18:27
        -1

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


        1. CyberBot Автор
          06.11.2021 16:04
          +1

          Начнем с твоего плагиаторства, ты украл мой проект (2011г.) и выложил в своем видео блоге(2017г.) от своего имени и еще добавил, что ты сам разработал ))))). Хотя даже код не изменил для неузнаваемости. Ровно как и большинство твоих видео, это проекты других людей, выложенные тобой как от супер-гениального разработчика.
          По поводу твоей библиотеки, я считаю ниже своего достоинства использовать в своих проектах, твои г..но библиотеки и все остальное, что как то связано с твоим ником или именем.


          1. AlexGyver
            06.11.2021 16:20
            +1

            ахахах, это тебя пришлось забанить в группе за идиотию и клевету? Отвечу здесь ещё раз, мне не сложно:

            • Я ничего никогда ни у кого не крал. Код был взят с форума arduino.ru, он был там в свободном доступе. И я не выдавал его за свой.

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

            • А вот плату разработал я, на чём сделан акцент в видео, схема там - классическая для симистора и детектора нуля, находится в гугле по запросу arduino dimmer.

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


            1. CyberBot Автор
              06.11.2021 17:24

              Не крал говоришь? Смотрим здесь https://youtu.be/jPbptVGZisc?t=438

              Твоя фраза в видео - "Мой алгоритм" )))

              Если ты взял в интернете, то при публичном использовании ты обязан сослаться на первоисточник. Но ты вместо этого говоришь "Мой алгоритм" при этом показываешь мой код.
              Схема нарисована мной в нулевых годах, я реализовал ее тогда на pic контроллере и выложил на форум Микрочипа. В те времена ардуино еще не родился. Потом в 2010г. реализовал на ардуино. Так, что схема тоже впервые нарисована мной.
              Если ты взял проект в интернете, то в этом нет ничего плохого, но вот если ты используешь его в публичном доступе и говоришь, что это твое, то ты "Плагиатор".

              Про достоинство: Как вообще у тебя язык повернулся говорить про достоинство в твоей ситуации? Если оно у тебя еще осталось, то удали видео и пожмем руки.


              1. AlexGyver
                06.11.2021 18:00

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

                Давай так - извиняешься за фонтан говна и ничем не обоснованной клеветы в мою сторону в отношении остальных проектов (мы это обсуждали года два назад, я подробно расписывал какие проекты были лично мои, а какие - ДОРАБОТАНЫ и об этом по-русски сказано в видео, а ты ни ничего не смог на это ответить) - и я прямо сейчас убираю видео про диммер, вообще без проблем, и также приношу свои извинения.


                1. CyberBot Автор
                  06.11.2021 18:20

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

                  Я не помню такого, если предлагал то извини за невнимательность, это решает наш вопрос и ссылки будет достаточно. Хотя форум уже полумертвый, но тем не менее. Если разместишь ссылку на оригинал http://cyber-place.ru/showthread.php?t=525 , то все претензии снимаются. После чего дашь ссылки на то, что нужно мне удалить я удалю без проблем и извинюсь


                  1. AlexGyver
                    06.11.2021 18:28
                    +1

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


                    1. CyberBot Автор
                      06.11.2021 18:47

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


                      1. uburame
                        06.12.2021 21:37

                        Это было эпично! Но всё же, чей код брать? :)


  1. xshura
    31.10.2021 20:57

    Вдруг кому пригодится информация (кто не в "теме").

    Есть еще абсолютные энкодеры, которые "запоминают" свое положение.

    Например: EAW0J-B24-AE0128L

    Они дороже конечно, но не намного. Энкодер выше, стоит в чип-дип 1240р.


    1. gorbln
      31.10.2021 21:06
      +7

      1240 это примерно в 10 раз дороже инкрементальника из статьи. Очень намного.

      А ещё есть микросхема AS5600, которая представляет собой абсолютный энкодер на 4096 отсчётов. И вот она стоит 200 рублей. Радиально намагниченный магнит к ней - ещё 68 рублей. Работает изумительно. На температуру почти не реагирует, +- 2 отсчёта на 60 градусов.


      1. xshura
        31.10.2021 21:26

        Спасибо за инфу, буду иметь ввиду.

        Так то мне нужен энкодер с разрешением 1градус (поворотное устройство для антенны придумываю). Вот на всякий и прикупил такой для "разминки".


        1. gorbln
          31.10.2021 21:37
          +2

          Нудык 12 бит - это разрешение меньше 0,1 градуса, и ошибка будет не более 0,3. Такие энкодеры продаются на алиэкспрессе и применяются в ЧПУ станочках для контроля шагового двигателя - что он шаги не пропускает. Стоят ещё дешевле, чем я написал - первый же линк из поиска али - 130 рублей.


          1. xshura
            31.10.2021 21:45

            1. gorbln
              31.10.2021 21:49

              Такая штука имеет смысл, когда обороты под 20000 и есть специальная микросхема-обработчик энкодера, ну либо магнитные помехи просто ОМГ. Во всех остальных случаях сейчас нет большого резона применять оптический энкодер.


              1. Gromushka
                31.10.2021 22:37

                Максимальная Механическая скорость: 1000 об/мин

                Вы по ссылке ходили?


            1. gorbln
              31.10.2021 21:55

              Кстати говоря, для больших оборотов и минимальных задержек есть синуснокосинусные преобразователи. Это такой сенсор, внутри которого два магниторезистивных моста, причём они сдвинуты по фазе на 90°. Один выдаёт синус угла поворота, а другой - косинус. Таким образом, обрабатывая сигнал с обоих мостов, мы можем рассчитать точное значение угла поворота. Такие штуки бывают как со встроенным обработчиком и цифровым интерфейсом, так и чисто мосты "без никто". Например, TLE5501


      1. kot_martovskiy
        01.11.2021 02:21

        А этой AS5600 надолго хватит, если я ее следить за направлением ветра в метеостанцию поставлю? Сейчас стоит последовательность из 16 герконов и резисторов разных номиналов.


        1. gorbln
          01.11.2021 13:23

          Она бесконтактная. Будет работать, пока питание подано. Лет на 10 точно хватит, а может и больше. Магниту тоже ничего от времени не будет, плюс у AS-ки довольно большой запас по напряжённости магнитного поля и АРУ.

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


      1. nochkin
        01.11.2021 17:18

        AS5600 на I2C и нельзя менять адрес. То есть, если надо парочку, то это не очень удобно.

        Есть вариант AS5048 на SPI, но они уже намного дороже. Есть ещё недорогие варианты типа такого, но что бы можно было без проблем больше одного подключить?


        1. gorbln
          01.11.2021 20:44

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

          Варианты, конечно, есть - поставить мультиплексор порта, например, но это какие-то адовые костыли, которых бы не было, если бы, например, у микрухи была нога "ENABLE".


          1. nochkin
            01.11.2021 21:21

            Ага, именно так. Видимо, расчёт на то, что обычно нужна одна такая.

            С мультиплексором, конечно, перебор. Проще поставить AS5048, это явно проще и дешевле будет на фоне продукта.

            А мне как раз надо на оба колеса для робота такие поставить.


            1. gorbln
              01.11.2021 21:27

              Если на оба колеса - то, как по мне, проще найти камень с двумя аппаратными I2C или сделать софтовый. Ну или, например, выводить ШИМ и его измерять - тупо, но работает. А ШИМ AS5600 делать точно умеет.


              1. nochkin
                02.11.2021 02:37
                +1

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

                С ШИМом вроде говорят, что не так надёжно получается на этих датчиках. К сожалению, не помню подробностей -- может помехоустойчивость не очень или ещё что.

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

                Правда, проводов будет чуть больше. Но этот вариант уже есть смысл рассмотреть.


            1. Gengenid
              02.11.2021 09:08

              Не, мультиплексор i2c дешевле будет. Типа TCA9548A


              1. nochkin
                03.11.2021 04:26

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


  1. reticular
    31.10.2021 21:00

    очень приятно пользоваться устройствами с отзывчивым управлением
    но энкодеры, подвержены износу, и даже конденсаторы не спасают :(
    пользуюсь решением от DI HALT опрос по таймеру 1-5 мс
    если же необходимо по прерыванию: великолепное железное решение — MC14490 -аппаратный подавитель дребезга


    1. CyberBot Автор
      31.10.2021 21:11

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


      1. serafims
        31.10.2021 23:24
        +4

        Часто в мышках энкодер делают на основе оптопары.


        1. CyberBot Автор
          01.11.2021 00:00

          Это с шариками мышки так делали, а колесо на оптических мышках обычно на инкрементном энкодере


          1. aamonster
            01.11.2021 01:40

            Так на оптопарах он тоже инкрементный... Но я не помню в мышках чаще оптический или механический на колесе (вот x/y на мышках с шариком точно оптические были).


            1. Gengenid
              01.11.2021 09:34
              +1

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


              1. CyberBot Автор
                01.11.2021 10:03

                Ну да, я забыл ты же писатель, а не читатель. Специально для тебя https://yandex.ru/images/search?from=tabbar&text=%D1%80%D0%B0%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%BD%D0%BD%D0%B0%D1%8F%20%D0%BC%D1%8B%D1%88%D1%8C

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


                1. BigBeaver
                  01.11.2021 13:09

                  Давно не разбирал мышей, но

                  потрудитесь обьяснить
                  image


                  1. CyberBot Автор
                    01.11.2021 13:46

                    Фото всей платы можно?


                    1. BigBeaver
                      01.11.2021 14:01

                      У меня нет.


                    1. aitras
                      01.11.2021 19:52

                      У моей мышки Logitech




                      1. LAutour
                        02.11.2021 12:20

                        Ну это Logitech, а остальные предпочитают экономить.


        1. LAutour
          01.11.2021 09:48
          +4

          Часто в мышках энкодер делают на основе оптопары.

          Уже давно — редко.


          1. Gengenid
            01.11.2021 12:53

            Действительно, посмотрел, сейчас две из трех мыши дома с механикой.


      1. borisxm
        01.11.2021 11:46

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


    1. Mike-M
      01.11.2021 22:27

      энкодеры, подвержены износу
      От конкретного энкодера зависит. Не знаю, какой энкодер установлен в MP3-ресивере Pioneer, но работает он без единого нарекания вот уже 18-й год. И это с учетом колебаний температуры и влажности в салоне автомобиля!


  1. smart_pic
    31.10.2021 21:12

    Конденсаторы можно уменьшить до 0.01мкФ. Номинал подтягивающих резисторов к +питания?

    Непонятно зачем такой сложный код обработки энкодера? Достаточно одного прерывания по спаду (или фронту) сигнала на выходе А энкодера для того чтобы определять направление и вести подсчет импульсов.

    Определение нажатия на кнопку энкодера лучше сделать в void loop()


  1. gorbln
    31.10.2021 21:16
    +1

    Много раз порывался поставить энкодер в свои поделия, и каждый раз приходил к тому, что он прекрасно заменяется тремя кнопками: ▲,▼ и ●. Количество пинов - то же, количество кода - в 100 раз меньше, кнопка подтверждения - есть. Единственное отличие - требуется три дырки, и нет красивой и приятной тактильно крутилки. Тут да, крутилка - это другое ощущение.


    1. Tomasina
      01.11.2021 12:26
      +1

      Крутилка гораздо удобнее, особенно когда надо вводить большие интервалы.


  1. IceStalker
    31.10.2021 21:53
    +2

    Спасибо за инфу.

    Видео длиннее и не надо: коротко и по делу


  1. smart_pic
    31.10.2021 22:13
    -2

    Код обработки длинного и короткого нажатия кнопки без библиотек на чистом С

    //****************************************************************************
    void ScanKeyboardTast(void)
    {
    static DWORD Tdl_key=0; 								// штамп времени
    static BOOL Key_Up;
    static BOOL Key_Down;
    static BOOL LongPres=0;
    static BOOL Key_Sost=1;
    static BOOL Key_OldSost=1;
    static BYTE Status_Input; 								// Состояние входа
    
    		Status_Input=Status_Input<<1;
    		if (BXOD1 == 1)	Status_Input=Status_Input | 0x01; else Status_Input=Status_Input & 0xFE;
    		if 		(Status_Input== 0xFF) Key_Sost=0; 
    		else if (Status_Input== 0x00) Key_Sost=1;
    
    //	Если не нужна защита от дребезга то нужна всего одна строка
    		Key_Sost=BXOD1;									// Кнопка замыкает подтянутый к +питания вход на землю
    
    		if	((Key_Sost ==0) && (Key_OldSost ==1)) 		// Кнопка нажата ?
    			{
    			Key_Down=1;									// Установили флаг нажатия
    			Tdl_key = TickGet();						// сделали штамп времени
    			}
    
    		if	((Key_Sost ==1) && (Key_OldSost ==0)) 		// Если кнопка отжата ?	
    			{
    			if	(LongPres==1) 							// Проверяем было ли длинное нажатие
    				{
    				LongPres=0;
    				// {.....}								// Действие по длинному нажатию
    				}
    			Key_Down=0;
    			// {.....}									// Действие по короткому нажатию нажатию
    			}
    	
    		if (Key_Down==1)								// если было нажатие
    			{
    			if (TickGet() - Tdl_key > Time_long_press)	// проверяем вдруг будет длинное нажатие
    				{
    				LongPres=1;								// зафиксировали длинное нажатие
    				}
    			}
    		Key_OldSost=Key_Sost;							// запомнили предыдущее состояние кнопки
    }
    //****************************************************************************
    // Примечание функция TickGet() очень похожа на функцию millis() в Ардуино
    // BXOD1  - вход подключения кнопки
    


    1. CyberBot Автор
      31.10.2021 22:17
      -3

      У меня в прерывании обрабатывается вот так:

         if (comb == 7 && lastcomb == 3 && btn_press) //Если было отпускание кнопки, то проверяем ее предыдущее состояние 
         {
          if (millis() - timer > btn_long_push)         // проверяем сколько прошло миллисекунд
          {
            enc_state=4;                              // было длинное нажатие 
          } else {
                   enc_state=3;                    // было нажатие 
                  }
            btn_press=0;                           //обнулить статус кнопки
          }

      Можно так же добавить обработку очень длинных нажатий


  1. Gromushka
    31.10.2021 22:26
    +4

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

    Поэтому в нормальной программе если используется глобальная переменная как enc_state и в обработчике, и в основном теле программы, то код, который изменяет значение переменной в основной части лупа (обнуляет) должен быть закрыт DISABLE_INT() / ENABLE_INT() в противном случае даже в такой простой программе, все может легко сбиваться. То, что работает - подозреваю дело времени. Хотя, возможно запрет на прерывания в процессе основного цикла в ардуино может быть автоматом.

    По поводу 10 строк обнуления и кучи if () вместо обычного switch case прикрыт скромной фразой про отсутствие оптимизации, только оптимизация тут не причем.

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

    Я уже молчу про то, что если сработало первое условие смысла продолжать код нет, и нет проверки на то, есть ли что-то в enc_state отличное от нуля.

    Ну в общем - не код это как по мне.

    ПыСы: добавлю, что в нашем случае когда состояний всего 4 основной цикл должен выглядеть так:

    #define MAX_STATE 4
    void * pFunc(void)[MAX_STATE] = {
      func1(),
      func2(),
      func3(),
      func4()
    };
    
    void loop()
    {
      if ((enc_state > 0) && (enc_state < MAX_STATE))
      {
        pFunc[enc_state]();
        enc_state=0;
      }
    }
    


    1. CyberBot Автор
      31.10.2021 22:34
      -5

      Поэтому в нормальной программе если используется глобальная переменная как enc_state и в обработчике, и в основном теле программы, то код, который изменяет значение переменной в основной части лупа (обнуляет) должен быть закрыт DISABLE_INT() / ENABLE_INT() в противном случае даже в такой простой программе, все может легко сбиваться. То, что работает - подозреваю дело времени.

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


      1. Gromushka
        31.10.2021 22:47
        +7

        Причем тут volatile?

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

        Про switch я промолчу, но основы программирования - это то, на чем вы остановились. Не буду вам мешать - наслаждайтесь.


      1. Serge78rus
        31.10.2021 22:58
        +4

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


        1. Gromushka
          31.10.2021 23:05
          +4

          @Serge78rus

          Проблема глубже и дело даже не в атомарности операции обнуления / присвоения.

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

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

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


          1. Serge78rus
            31.10.2021 23:27
            +2

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


          1. CyberBot Автор
            31.10.2021 23:29
            -3

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

            ISR (PCINT1_vect) //Обработчик прерывания от пинов A1, A2, A3

            {

            uint8_t comb = bitRead(PINC, 3) << 2 | bitRead( PINC, 2)<<1 | bitRead(PINC, 1);

            PCICR = 0b00000000; // PCICR |= (1<<PCIE1); Выключить прерывание PCINT1

            INTstate=1;

            }


            1. Gromushka
              01.11.2021 08:24

              Отключать прерывания в обработчике прерываний? Это просто какой-то позор.


        1. RTFM13
          02.11.2021 02:23

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

          а enc_rotation не байтовая.

          по этому если мы начинаем читать на 8-битном контроллере 0x01FF считали старший байт, в середине присвоения вызвалось прерывание которое инкрементировало значение до 0x0200, затем мы считали младший байт и получили на выходе 0x100.

          По этому или запрет прерываний на время присваивания или двойная буферизация в прерывании и флаг запрета обновления в прерывании. Или любой другой механизм (который как правило сводится к этим двум).


          1. Serge78rus
            02.11.2021 11:58

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


      1. Exclipt
        31.10.2021 23:08
        +2

        А расскажите, чем вас не устроил коммент про swith? А то я вот смотрю на 4 одинаковых куска вида:

        if(enc_state==1) // Если энкодер вращался без нажатия

        {

        Serial.print("Вращение без нажатия ");

        Serial.println(enc_rotation);

        enc_state=0; //обнуляем статус энкодера

        }

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


        1. CyberBot Автор
          31.10.2021 23:21
          -1

          Нужно же было писать в какой функции Вы хотели вставить switch. Я то подумал, что Вы предлагаете в прерывании его вставить. В главном цикле loop этот кусок кода всего лишь демонстрация, он потом выбрасывается от туда


          1. Exclipt
            31.10.2021 23:27
            +3

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


            1. CyberBot Автор
              31.10.2021 23:40
              -2

              Во первых это многословность и громоздкость самой конструкции, только взгляните — switch, case, break и default

              Вы бы написали свой пример обработки на switch, потом мы бы порассуждали, что лучше и компактнее


              1. Exclipt
                01.11.2021 00:12
                +2

                Я писал про структурированность кода, а не про компактность, странно, что это надо объяснять. Компактно вы можете хоть "a = 3 - a;" написать по мотивам легенд из прошлого века "как заменить 1 на 2 и наоборот". Но даже если это не вспоминать, у нас еще в 98-м на лабах универа снижали оценки за лишние бессмысленные if, жрущие производительность.

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


                1. CyberBot Автор
                  01.11.2021 00:34

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


                  1. Gromushka
                    01.11.2021 08:34

                    В компиляторе С switch на 4 состояния и if () 4 раза обычно компилируется одинаково

                    switch (enc_status)
                    {
                    case 1: 
                       func(1);
                       break;
                      case 2:
                        func(2);
                        break;
                      case 3: 
                        func(3)
                        break;
                    }
                    enc_state = 0;
                    

                    Сравните с вашей

                    if (enc_state == 1)
                    { 
                      func(1);
                      enc_state = 0;
                    }
                    
                    if (enc_state == 2) 
                    {    
                      func(2);  
                      enc_state = 0; 
                    }
                    
                    if (enc_state == 3) 
                    {    
                      func(3);
                      enc_state = 0; 
                    }  

                    Даже в этом случае ваш код на три строчки длиннее, проходит все if () не зависимо обработалось или нет значение enc_state и во всех случаях содержит одну и ту же строку enc_state = 0

                    И вы еще заявляете о производительности? Учитывая volatile на все типы данных - у вас нет оптимизации и break в switch конструкции нужен для выхода из перебора значений - чего у вас вообще нет.

                    К сожалению - вы просто не умеете писать программы.


                    1. unsignedchar
                      01.11.2021 08:49
                      +1

                      if (enc_state !=0)
                      {
                      func(enc_state);
                      enc_state=0;
                      }

                      Когда вижу простыню if-ов, рука сама тянется к пистолету ;)


                      1. unsignedchar
                        01.11.2021 09:59
                        +1

                        Опсь, сорян, с этого же и началось ;)


                    1. CyberBot Автор
                      01.11.2021 09:34
                      -1

                      Чукча не читатель, чукча писатель! Я же писал выше, что этот кусок кода не обязательный, вставлен только для демонстрации работы и да я согласился, что его лучше поменять. Речь же шла о замене if() в прерывании


                      1. Gromushka
                        01.11.2021 10:14
                        -1

                        Читать не умеешь? Хотя вопрос риторический.


                    1. CyberBot Автор
                      01.11.2021 10:39

                      if (enc_state == 1) func(1);
                      if (enc_state == 2) func(2);  
                      if (enc_state == 3) func(3);
                       enc_state = 0; 

                      сравнивайте

                      switch (enc_status)
                      {
                      case 1: 
                         func(1);
                      break;
                      case 2:
                          func(2);
                       break;
                       case 3: 
                          func(3)
                        break;
                      }
                      enc_state = 0;


                      1. unsignedchar
                        01.11.2021 11:03

                        Дело вкуса. Если в вариант с if-ами добавить else (чтобы не гонять лишние проверки) и скобки (если вдруг вздумается делать что-то еще кроме func()) — никакой разницы.
                        ЗЫ: волшебные константы нехорошо в любом случае.


                      1. Serge78rus
                        01.11.2021 12:16
                        +4

                        К сожалению, привычка всегда окружать блок кода внутри if фигурными скобками, даже если это одиночное выражение, приходит только после набития некоторого числа шишек. А до этого превалируют рассуждения о краткости, лаконичности и т.д.


                      1. BigBeaver
                        01.11.2021 13:20

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


                      1. unsignedchar
                        01.11.2021 13:41

                        Можно, конечно и в одну строчку (чтобы строки экономить), и все переменные длиной в 1 букву (чтобы символы экономить) ;)


                      1. Serge78rus
                        01.11.2021 13:48

                        Поправляю: с директивами препроцессора будут проблемы — со всякими #define и т.д.


                      1. Gromushka
                        01.11.2021 14:45
                        -1

                        Сравнивайте, enc_state == 1 - сколько в вашем варианте лишних операций if ()?


                      1. Exclipt
                        01.11.2021 21:52

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


            1. BigBeaver
              01.11.2021 15:24
              +1

              но и в прерывании правильное структурирование кода тоже никак бы не помешало
              Если говорить о серьезном коде, то надо в принципе переделать архитектуру программы. Обработчик прерывания должен быть предельно коротким — он должен просто преобразовывать его в некоторое событие (переменная comb), которое пишется в fifo очередь событий и уже обрабатывается в основном цикле или где-то еще. В общем, железные прерывания и их интерпретация должны быть в идеале разделены.

              Нужно это чтобы прерывания были настолько атомарными, насколько возможно (если так можно сказать). А это нужно чтобы максимально исключить возможность прихода других прерываний во время обработки текущего. В крутилке для лампы это, конечно, не важно, но в сложных задачах с кучей переферии может быть критичным. Также это может быть важным в системах требовательных к таймингам разного рода (например, если есть софтварный bitbang генератор [например, для той же ws2812 ленты] и тд) — очевидно, что длинные обработчики прерываний будут сбивать тайминги, а очередь событий позволит нивелировать эффект, обрабатывая логику «между кадрами».


    1. aamonster
      01.11.2021 01:53

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

      Ну и по целой функции на состояние – жирно, достаточно значения +1, 0 или -1. Табличка на 16 signed char. Можно, конечно, поиграться с логическими операциями и избавиться от неё (кто тут умеет в карты Карно?), но смысла нет.

      Да, и при такой обработке почти пофиг, работать по прерыванию с пина или по таймеру (с пина чуть хуже, есть мизерный шанс на дребезге "отстать" на 1 отсчёт энкодера... плюс таймер "съест" дребезг)


  1. RTFM13
    01.11.2021 00:28
    +4

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

    RC фитр и вход в режиме триггера Шмитта, тогда уж.

    И совсем не стоит коротить энкодером конденсаторы достаточно приличной емкости.


  1. NanoVHF
    01.11.2021 09:25

    Алекс(ей/андр?), вас не затруднит на ваш скетч нарисовать подробный алгоритм? Это позволит начинающим портировать код на другие языки. Если вы планируете несколько статей, то приведение в начале рисунка алгоритма было бы здорово.


  1. srg27y
    01.11.2021 10:18
    +2

    я кроме конденсаторов еще и 100Ом по общему проводу поставил

    идея с вращением при нажатии понравилась, дажэ сходу понял где мне это надо.


    1. uburame
      06.12.2021 21:43

      В настольной лампе от Xiaomi такое используется: просто поворот - изменение яркости, поворот при нажатии - изменение цветовой температуры, нажатие - вкл/выкл.


  1. dvserg
    01.11.2021 10:29
    +1

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


  1. FGV
    01.11.2021 21:24
    +1

    Хм. Давно на авр не кодил, но в свое время программно ловил фронт одного канала по таймеру (период то ли 1мс то ли 100мкс) и в зависимости от состояния соседнего канала инкрементировал/декрементировал положение вала (так называемая одинарная точность, есть еще двойная - ловится фронт/спад одного канала и четверная - фронт/спад обоих каналов).

    На современных мк (вроде стм32) квадратурный энкодер встроен в переферию и таких заморочек не нужно.