Как вы могли заметить, я давно работаю с процессором STM32 ARM при помощи Mbed. Были времена, когда Mbed был весьма прост, но многое изменилось с тех пор, как он превратился в Mbed OS. К сожалению, это означает, что многие примеры и библиотеки, которые вы могли бы найти, с относительно новой системой работать не будут.

Мне нужен был поворотный энкодер — и я вытянул дешевый экземпляр из одного набора «49 плат для Arduino», какие продаются повсюду. Уверен, это не самый филигранный поворотный энкодер из имеющихся в природе, но для поставленной задачи его должно было хватить. К сожалению, в Mbed OS нет драйвера для такого датчика, а первые несколько сторонних библиотек, которые я нашел, либо работали по принципу опроса, либо не компилировались под последнюю версию Mbed. Разумеется, для чтения поворотного энкодера никакой магии не требуется. Но насколько сложно самостоятельно написать для него код? В самом деле, довольно сложно. Подумал, поделюсь моим кодом и расскажу, как к этому коду пришел.

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

Так что цель моя была проста: хотел найти что-нибудь, управляемое прерываниями. Найденные мной образцы по большей части требовали периодически вызывать некоторую функцию или устанавливать прерывание таймера. Затем соорудили машину состояний, чтобы отслеживать состояние энкодера. Это хорошо, но такая конструкция будет отжирать много процессорного времени только лишь для проверки энкодера, даже если он не движется. Процессор STM32 может с легкостью выполнять прерывания при изменении пина, и именно этого я хотел.

Загвоздка

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

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

Теория

Теоретически, прочитать энкодер – проще простого. У него два выхода, назовем их A и B. Перещелкиваешь рычажок – и эти выходы испускают импульсы. Механическая компоновка внутри такова, что, когда рычажок поворачивается в одном направлении, импульсы от A на 90 градусов опережают импульсы от B. Если перещелкнуть рычажок в другую сторону, фаза будет обратной.

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

В левой части схемы отметим, что сигнал B всякий раз падает перед сигналом A. Если замерить B на нисходящем фронте A, то в таком случае у вас всегда получится 0. Ширина импульсов, конечно же, зависит от скорости перещелкивания. Перещелкивая рычажок в другую сторону, оказываемся на правой стороне схемы. Здесь сигнал A сначала идет на низкий уровень. Если замерить B в той же точке, что и в предыдущем примере, то теперь он будет равен 1.

Обратите внимание: нет никакого волшебства ни с A, ни с B, ни с метками, указывающими движение по часовой стрелке или против часовой стрелки. Все это, в сущности, означает «туда» или «сюда». Если вам не нравится, как движется энкодер, то просто можете поменять местами A и B или сделать это на уровне программы. Я выбрал эти направления произвольно. Как правило, считается, что канал A «ведет» по часовой стрелке, но это зависит и от того, какой фронт сигнала вы измеряете и как все подключили. На программном уровне обычно добавляем единицу к счетчику в одном направлении и вычитаем единицу из счетчика в другом – чтобы представлять, где вы окажетесь со временем.

Есть много способов читать подобный ввод. Если вы замеряете его, то весьма просто собрать из двух битов машину состояний – и таким образом обрабатывать ввод. Вывод образует код Грея, что позволяет вам отбросить плохие состояния и плохие переходы между состояниями. Однако, если вы уверены в вашем входном сигнале, то все может быть гораздо проще. Просто читаем B на фронте A (или наоборот). Можно проверить второй фронт, если хочется добиться немного большей надежности.

Практика

В реальности, к сожалению, механические энкодеры выглядят не так, как на вышеприведенной схеме. Скорее, они выглядят вот так:

Здесь возникает проблема. Если сделать прерывание по обоим фронтам входа A (верхняя линия в области видимости), то получим серию импульсов по обоим фронтам. Обратите внимание: состояния B отличаются на каждом фронте A, поэтому, если у вас в сумме получится четное количество импульсов, то общий счет будет равен нулю. Если вам повезет, то вы можете получить нечетное число в правильном направлении. Либо в неправильном. Что за каша.

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

План

Наш план таков: заметить, когда A переходит с верхнего уровня на нижний, и именно тогда прочитать B. Далее игнорировать A, пока B не изменится. Конечно же, если вы хотите отслеживать B, то возникнет такая же проблема, поэтому его нужно замкнуть на значение A, которое в момент изменения будет стабильным.  Я в данном случае не хочу использовать еще два прерывания, поэтому стану следовать такой логике:

  1. Когда A падает, записать состояние B и обновить счетчик. Затем установить флаг блокировки.

  2. Если A снова падает, то: если флаг блокировки установлен или B не изменилось – ничего не делать .

  3. Когда A поднимается, то: если B изменилось, записать состояние B и снять флаг блокировки.

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

Проблема

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

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

Я воспользовался фичей Kernel::Clock::now из Mbed. Непонятно, требуется ли от вас вызывать ее из обработчика прерывания (ISR), но я так и делаю и, как кажется, тут все работает без проблем.

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

Код

Весь код выложен на GitHub. Если вы продрались через мои объяснения, то вам не составит труда его прочитать.

void Encoder::isrRisingA()
{
   int b=BPin; // прочитать B
   if (lock && lastB==b) return; // не время для блокировки 
// если lock=0 и _lastB==b, то две эти строки ничего не делают
// но, если lock = 1 и/или _lastB!=b, то в одной из них что-то делается
   lock=0;
   lastB=b;
   locktime=Kernel::Clock::now()+locktime0; // даже если не заблокировано,
 // выдержать задержку для lastB
}
 
// Падающий фронт – там, где выполняется счет
// Обратите внимание: если выдержать паузу в бит, то блокировка истечет,
// так как в противном случае
// нам также придется отслеживать B, чтобы знать, 
// состоялось ли изменение направления 
// Здесь очень хочется попытаться взаимно заблокировать/разблокировать ISR,
// но в реальной практике
// за фронтами следует ряд дребезжащих фронтов, пока B стабильно
// B изменится, пока A стабильно
// Поэтому, если вы не хотите также наблюдать B в сравнении с A,
// то придется пойти на какой-то компромисс,
// и на практике это работает достаточно хорошо
void Encoder::isrFallingA()
{
   int b;
   // снять блокировку в случае timedout, и в любом случае забыть lastB,
// если мы достаточно давно не видели фронт
   if (locktime<Kernel::Clock::now())
     {
     lock=0;
     lastB=2; // такого значения быть не может, поэтому данное   
// событие необходимо прочитать.
     }
   if (lock) return; // блокировка состоялась, так что все готово
   b=BPin; // прочитать B
   if (b==lastB) return; // без изменений в B
   lock=1; // не читать последующего дребезга
   locktime=Kernel::Clock::now()+locktime0; // установить задержку для блокировки
   lastB=b; // запомнить, где сейчас B 
   accum+=(b?-1:1); // наконец, посчитать!
}

Установить прерывание просто, поскольку есть класс InterruptIn. Он подобен объекту DigitalIn, но предусматривает способ прикрепления функции к восходящему или нисходящему фронту. В данном случае используем обе.

Задержка

Я заинтересовался, сколько времени требуется на обработку прерывания в такой конфигурации, так что этот код будет доступен, если установить #define TEST_LATENCY 1. Также можете посмотреть видео о том, что у меня получилось, но, если вкратце: чтобы получить прерывание, уходило не более 10 микросекунд, часто даже около пяти.

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

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

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


  1. atd
    26.04.2022 16:29
    +13

    Забыли тег "машинный перевод"

    (в данном случае encoder внезапно переводится как энкодер, «шифратором» данное устройство в литературе на русском языке обычно не называется)

    added:

    Перещелкиваешь рычажок – и эти выходы испускают импульсы.

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


    1. atd
      26.04.2022 16:38
      +3

      P.P.S.: оригинальная статья тоже не образец для подражания. В реальных девайсах за такое надо руки отрывать, конечно же.

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


    1. HardWrMan
      26.04.2022 17:18
      +1

      А мне вот интересно, почему "encoder" машина переводит как "шифратор", если это "кодировщик"? А у "шифратор" есть своё слово "encryptor". Непонятно.


      1. Moskus
        27.04.2022 08:19

        Вообще, Гугл переводит encoder как "кодер". А Яндекс - как "кодировщик". Так что возможно, что это автор сам постарался.


        1. HardWrMan
          27.04.2022 09:12

          Ну если строго, то "кодер" тоже правильный перевод. Ну и по аналогии вместо "шифратор" может быть "шифровальщик". Всё дело в контексте.


    1. nochkin
      26.04.2022 18:33
      +2

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


    1. Gryphon88
      27.04.2022 14:11

      Насколько я помню, по-старорежимному encoder - "преобразователь угол-код". Ну и статья конечно, и изложение, и алгоритм... А ведь кто-то это скопипастит с гитхаба.


  1. tandzan
    26.04.2022 16:29
    +5

    ОМГ, разве кто-то называет энкодер шифратором?


  1. REPISOT
    26.04.2022 16:44
    +3

    Я уж думал, тут устройство Энигмы будет описано…


  1. PKav
    26.04.2022 17:23
    +11

    В статье речь идёт об STM32, а даже у STM32F1 таймер TIM1 (Advanced Control который) умеет работать в режиме считывания энкодера и имеет свой настраиваемый фильтр дребезга. Просто подключаешь энкодер к выходам TI1 и TI2, настраиваешь таймер, и в его регистре CNT будет всегда актуальное количество щелчков энкодера.


    1. jar_ohty
      26.04.2022 21:19

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


      1. PKav
        26.04.2022 21:21

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


        1. jar_ohty
          27.04.2022 03:49

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


      1. Tiriet
        27.04.2022 06:50
        +1

        У меня тоже такой был как-то раз. А потом я понял, что у него, условно, три ноги- A,B, Gnd, и если подключить А-А, B-Gnd, Gnd-B, то будет наблюдаться именно описанное Вами поведение. Подключил я свой "неправильный" энкодер как надо, и он, внезапно, оказался нормальным.


        1. jar_ohty
          28.04.2022 00:56

          Ах вон оно что! А я-то думал, это китайцы так намудрили. При том, что энкодер очень качественный, как показала практика: прибор с ним съездил уже в три рейса, эксплуатировался на палубе и энкодер как работал четко, так и работает.


  1. lab412
    26.04.2022 20:59
    +4

    Столько лестных отзывов в коментах, плюс бессмысленная работа учитывая что STM32F1 умеет из коробки читать энкодеры. Но вопрос в другом - кто за это плюсы то понаставил? все лишь ругаются, но по факту у стати +1. видимо такие же переводилы поддерживают друг друга ставя плюсы... уровень контента хабра падает... стремительно движется вниз...


    1. Radish
      26.04.2022 21:34

      нынче - не то что давеча


    1. Moskus
      27.04.2022 06:37

      Я не ясновидящий, но склонен подозревать в положительном голосовании не солидарность говнопереводчиков, а всех этих последователей идеи, что "приз" полагается каждому просто за участие, а критика - это плохо, потому что "демотивирует авторов".


      1. HardWrMan
        27.04.2022 06:55

        Думаете, что Хабр заболел повесточкой? Этот полный набор %SUBJ_NAME% позитив.


        1. Moskus
          27.04.2022 08:14

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


          1. HardWrMan
            27.04.2022 09:14

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


            1. Moskus
              27.04.2022 09:26

              Не уверен, что это началось именно так как вы представляете, и это "движение" только во вторую очередь, в первую - это состояние психики.


      1. atd
        27.04.2022 13:03

        Подозреваю скорее именно говнопереводчиков. Вот для сравнения такая-же бесполезная, но хотя-бы не вредная и не криво-переведённая статья: https://habr.com/ru/company/first/blog/661511/
        набрала гораздо меньше плюсов (пришлось плюсануть для уравновешивания, из этих двух она явно лучше)


    1. Polaris99
      27.04.2022 13:58

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


  1. Tiriet
    27.04.2022 07:05
    +2

    чего не придумаешь, лишь бы не ставить фильтрующую цепочку...


    1. nixtonixto
      27.04.2022 16:37

      Фильтры стоят денег и занимают место на плате. Поэтому в микроконтроллерной технике так принято: если проблему можно решить программно — её в первую очередь решают программно. Это на жёсткой логике дешевле поставить конденсатор, чем добавлять пару корпусов RS-триггеров, а в микроконтроллере лишние 100 байт кода — бесплатны…


      1. Tiriet
        27.04.2022 18:57
        +1

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


  1. Borjomy
    27.04.2022 23:30

    Empty