RomeoGolf

Пт 01 Июнь 2018

USB-polygon-20: Мышиный энкодер

Исправление ошибки

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

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

В общем, нашел ошибку в коде.

В третьем опусе цикла я уже описывал, что сталкивался с подобной проблемой в самом начале работы над проектом, и виноват был сторожевой таймер — «собака». Однако в коде с использованием библиотеки LUFA это вроде бы предусмотрено. В функции SetupHardware() в самом начале есть такой код, отключающий «собаку»:

#if (ARCH == ARCH_AVR8)
    /* Disable watchdog if enabled by bootloader/fuses */
    MCUSR &= ~(1 << WDRF);
    wdt_disable();

Сперва предположил, что этот участок не выполняется, потому что макроопределение ARCH не установлено.

Кстати сказать, в makefile переменная ARCH = AVR8 — это совсем другое, она действует только в makefile, и к коду программы отношения не имеет, хотя должна соответствовать одноименному макроопределению в коде, этого требует документация библиотеки LUFA. И в принципе можно бы определить макрос при помощи переменной CC_FLAGS в makefile, прибавив к ней -DARCH=0 (ARCH_AVR8 определено как 0), но это не обязательно. MassStorage.c содержит строчку #include "MassStorage.h", в данном файле есть #include <LUFA/Drivers/USB/USB.h>, который в свою очередь содержит #include "../../Common/Common.h", где записано #include "Architectures.h", а там, наконец, есть такой код:

            /** Selects the Atmel 8-bit AVR (AT90USB* and ATMEGA*U* chips) architecture. */
            #define ARCH_AVR8           0

            /** Selects the Atmel 32-bit UC3 AVR (AT32UC3* chips) architecture. */
            #define ARCH_UC3            1

            /** Selects the Atmel XMEGA AVR (ATXMEGA* chips) architecture. */
            #define ARCH_XMEGA          2

            #if !defined(__DOXYGEN__)
                #define ARCH_           ARCH_AVR8

                #if !defined(ARCH)
                    #define ARCH        ARCH_AVR8
                #endif
            #endif

А так как __DOXYGEN__ у меня нигде не определен, ARCH по умолчанию будет определен как ARCH_AVR8.

Кстати, анализ файла MassStorage.lss показывает, что таки да, код после условия #if (ARCH == ARCH_AVR8) компилируется. Тогда в чем дело?

А дело банально в том, что перед вызовом SetupHardware() я навставлял кучу кода, связанного с инициализацией экрана от мобильника. И к тому времени, как доходит дело до остановки «собаки», она уже успевает «гавкнуть». И надо всего-навсего поднять вызов SetupHardware() на самый верх функции main().

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

Энкодер мыши

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

mouse

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

Энкодер

Ролик (или колесико) мыши — съемный узел. В его состав входят:

  • «Качелька», которая служит для крепления собственно энкодера, а также давит на кнопки. В моем случае таких кнопок три, так как на ролик можно не только нажимать, но и качать вправо – влево;
  • Энкодер с шестигранным отверстием для оси ролика и тремя проводами, которые через разъем подключаются к плате мыши;
  • Пластиковый ролик на оси. Так сказать, колесный диск. Ось с краев круглого сечения для нормального вращения в точках крепления «качельки», а по центру шестигранная для зацепа в энкодере;
  • Резиновая шина (ну, или покрышка, как хотите).

Из мыши этот узел извлекается необычайно просто: отключается от платы разъем и выщелкивается из единственной точки крепления «качелька». Далее узел также легко разбирается на составные детали.

Энкодер разобранный

Энкодер сам по себе (без «качельки» и ролика) использовать неудобно. Поэтому буду применять его в сборе, только резинку сниму, ибо проскальзывает. За «качельку» удобно держать, а за ролик удобно крутить.

Итак, у энкодера имеются три провода: белый, желтый и черный. Можно не сомневаться, что белый и желтый — это фазы A и B (не обязательно именно в этом соответствии), а замыкаются они при вращении на черный. Тестер в режиме омметра подтверждает это предположение. Заодно определяем, что при вращении энкодера тактильно ощущаются 18 щелчков, причем, на каждый щелчок приходится две смены общего состояния выводов: по одному перепаду на каждом выводе.

Хотелось применить энкодер вместо кнопок для изменения картинки на подключенном экране мобильника, как в опусе 18. Там было подготовлено 20 кадров в расчете на энкодер KY-040, который предполагалось заказать на Aliexpress. Но побаловаться с запчастями от полудохлой мыши интереснее, особенно в рамках нищебродского проекта с минимальным бюджетом.

Значит, надо переписать файл для SD-карты на 18 кадров, изменив «стрелочки» на каждом кадре для равномерности. Кроме того, в коде программы заменить cnt20 на cnt18, cnt20old на cnt18old, и проверку переполнения счетчика делать сравнением не с 19, а с 17.

Подключение энкодера

Энкодер следует подготовить к подключению. Порты контроллера ожидают логические уровни, а энкодер пока что может только замыкать два провода на третий. Стало быть, подключим черный провод к «корпусу», а белые через подтягивающие резисторы — к +3,3 В. Таким образом, разомкнутый белый провод, подключенный к выводу порта, будет показывать логическую единицу, а замкнутый — логический ноль. При вращении энкодера состояние выводов будет последовательно изменяться.

Теперь о том, куда подключать. На порту B остался всего один свободный вывод, а для энкодера нужно два. Можно задействовать порт C, на котором «сидят» кнопки. Нижние две из них используются для переключения «спецрежима», а точнее, флаговой переменной canDo.

Отныне буду для этой цели использовать одну среднюю кнопку. В блоке, отвечающем за опрос кнопок я заменяю код, который четвертой кнопкой включал canDo, а пятой — выключал:

                               if ((bt_now & BT_4) == 0) {
                                       canDo = 1;
                                       PORTB |= (1 << 6);
                               }
                               if ((bt_now & BT_5) == 0) {
                                       canDo = 0;
                                       PORTB &= ~(1 << 6);

на код, который третьей кнопкой переключает этот флаг:

                               if ((bt_now & BT_3) == 0) {
                                       canDo = canDo ^ 1;
                                       PORTB |= (1 << 6);
                               }

А вот измененный фрагмент схемы:

Схема включения

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

Перемычки JP3 и JP5, соответствующие портам 6 и 7, снимаю, таким образом, порты нижних кнопок освободились, и к их контактным площадкам можно подпаять выводы энкодера. Но не просто так, а с джамперными пинами, которые воткну в разъемчик энкодера. Добавлю кошмара к устройству. Сплошной навесной монтаж, куча проводов. «Пудель», одним словом. Но это ненадолго, скоро все разберу, а для отладочной времянки допустимо. На фото виднеются емкости, о которых расскажу несколько ниже.

Аверс Реверс

Обработка событий энкодера

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

Допустим, изначально на обоих проводах «0». При вращении сначала изменится состояние одного провода, будет «1» и «0». Потом изменится состояние второго провода, получим «1» и «1». Затем снова будет изменение на первом: «0» и «1». И, наконец, все вернется в исходное: два нолика.

Если рассматривать два провода, как двухразрядное число, получается такое изменение: 0 -> 2 -> 3 -> 1 -> 0 -> … при вращении в одну сторону, или 0 -> 1 -> 3 -> 2 -> 0 -> … в другую.

При обсуждении этого дела на форумах в этом месте обязательно кто-нибудь начинает кричать: «Это же код Грея!» Ну да, код Грея. По определению и исходя из принципа действия энкодера. Это код, в котором последовательно расположенные числа отличаются друг от друга на один разряд. Ну и что? Да, код Грея легко привести к последовательности натуральных чисел. Буквально в пару операций. Но зачем? Совершенно спокойно можно его использовать в прямом виде.

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

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

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

  \ 0 2 3 1
0 0 + x -
2 - 0 + x
3 x - 0 +
1 + x - 0

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

«0» означает, что вращения не было. Оно и понятно: если текущее состояние равно предыдущему… «+» означает вращение в прямом направлении, «-» — в обратном. Понятия «прямой» и «обратный» довольно-таки относительны и еще зависят от того, какой провод мы выберем фазой А, а какой — Б. «x» соответствует ошибке: такой переход в нормальной ситуации возникнуть не может, значит, ошибка вызвана либо дребезгом контактов, либо слишком быстрым поворотом вала энкодера, либо сбоем программы (например,вызвавшим аномальную задержку между последовательными опросами), либо метафизическими причинами.

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

старое
состояние
новое
состояние
код
(hex)
действие
0 0 0 0
0 2 2 +
0 3 3 x
0 1 1 -
2 0 8 -
2 2 A 0
2 3 B +
2 1 9 x
3 0 C x
3 2 E -
3 3 F 0
3 1 D +
1 0 4 +
1 2 6 x
1 3 7 -
1 1 5 0

Добавляю в код обработку энкодера. Сперва перед основным циклом программы добавляю пару переменных:

       uint8_t encoderState = 0;
       int8_t encoderCounter = 0;

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

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

                               /* for encoder */
                               uint8_t nowEnc = (bt_now >> 6) & 0x03;  /* выделяю энкодерные биты порта С со сдвигом в начало */
                               encoderState <<= 2;                  /* бывшее текущее состояние делаю предыдущим */
                               encoderState |= nowEnc;              /* добавляю текущее состояние */
                               encoderState &= 0x0F;                /* отрезаю лишнее */

                               /* действие по состоянию: */
                               switch (encoderState) {
                                       case 0x00: /* 0 */
                                               break;
                                       case 0x02:
                                               encoderCounter++;
                                               break;
                                       case 0x03: /* err */
                                               break;
                                       case 0x01:
                                               encoderCounter--;
                                               break;
                                       case 0x08:
                                               encoderCounter--;
                                               break;
                                       case 0x0a: /* 0 */
                                               break;
                                       case 0x0b:
                                               encoderCounter++;
                                               break;
                                       case 0x09: /* err */
                                               break;
                                       case 0x0c: /* err */
                                               break;
                                       case 0x0e:
                                               encoderCounter--;
                                               break;
                                       case 0x0f: /* 0 */
                                               break;
                                       case 0x0d:
                                               encoderCounter++;
                                               break;
                                       case 0x04:
                                               encoderCounter++;
                                               break;
                                       case 0x06: /* err */
                                               break;
                                       case 0x07:
                                               encoderCounter--;
                                               break;
                                       case 0x05: /* 0 */
                                               break;
                               }
                               /* обработка переполнения счетчика */
                               if (encoderCounter >= (18 * 2)) {encoderCounter = 0;}
                               if (encoderCounter < 0) {encoderCounter = (18 * 2 - 1);}
                               cnt18 = encoderCounter >> 1; /* с учетом двух изменений на одмн щелчок */

Два замечания помимо комментариев в коде: во-первых, код избыточен, достаточно восьми case (четыре плюса и четыре минуса), но так нагляднее; во-вторых, счетчик энкодера переписывается в счетчик cnt18, вывод которого на экран мобильника в виде «циферблата» уже реализован.

Компилирую, прошиваю, запускаю. На экране мобильника появляется логотип, на светодиодах — счетчик. Нажимаю третью кнопку. Счетчик на светодиодах пропадает, на экране появляется «циферблат» с нулями и «стрелкой» вверху.

Медленно проворачиваю ролик энкодера — «циферблат» изменяется, циферки увеличиваются или уменьшаются, «стрелочка» поворачивается в соответствии с направлением, как в опусе 18. Но есть небольшие сбои: часть щелчков энкодера пропускается, а иногда «стрелочка» дергается назад. На быстрое вращение реакция вообще странная: то «циферблат» не изменяется, то сразу прыгает на несколько значений в произвольном направлении.

Некоторое время думал над кодом. Попробовал увеличить время опроса, перепрограммировав таймер, сначала в 256 раз, потом еще в 8 — не помогло. Попробовал сделать проверку состояния по прерываниям, несколько изменив логику защиты от дребезга — тоже нисколько не улучшилось. Я догадывался, что дело в дребезге, но осциллографа дома нету, а до ближайшего прибора тащить далеко и неудобно. Но все-таки добрался я до средств измерения и малость ужаснулся.

При медленном повороте порой все почти в порядке:

Осциллограмма

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

Осциллограмма

А вот при быстрой прокрутке вообще кошмар:

Осциллограмма

Короткие импульсы сплошняком заполнены дребезгом! Это невозможно обработать программно никаким автоматом. И наверхосытку такая одиночная прокрутка:

Осциллограмма

И укрупненно отдельный срез:

Осциллограмма

В общем, только программными методами это победить нереально. Надо ставить фильтр. Резистор стоит и так — подтягивающий, на 12 килоом, — добавлю емкость, и получится RC-цепочка, «сливающая» короткие импульсы на корпус. Вот измененный фрагмент схемы с добавкой конденсаторов:

Схема включения

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

Но суровая реальность ограничивает мой выбор пары конденсаторов тремя вариантами (если отбросить то, что меньше единиц нанофарад и больше десятков микрофарад, которые не подойдут даже по беглой прикидке). Есть 2,2 мкФ, 10 нФ и 100 нФ. Как настоящий радиолюбитель, буду сначала паять, потом считать.

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

Ну, теперь можно и посчитать. 12 kilo * 2.2 micro = 0.0264 За это время (в секундах) фронт импульса достигнет 1 - 1/e от единицы, это около 63%. А в три раза позже, через 0,0792 с, фронт достигнет 95%. То есть, импульсы длительностью 80 мс до напряжения питания дойдут, хоть и некрасивые, а то, что покороче — под вопросом. А при быстрой прокрутке импульсы имеют длительность около 10 мс.

Возьмем самый маленький номинал из трех. 12 kilo * 10 nano * 3 = 0.00036. Импульсы длительностью 360 мкс будут пропускаться. Да и 100 мкс могут быть замечены выше уровня логической единицы. Практика показала, что дребезг почти весь остается.

Ставлю 100 нФ, постоянная времени 1,2 мс. Картинка на медленном вращении такая:

Осциллограмма

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

Осциллограмма

Картина примерно такая же, дребезг в рамках допустимого, работе не мешает.

Эпилог

Подключить энкодер получилось более-менее успешно. Ни одно животное при этом не пострадало: мышь была восстановлена. Энкодер вернул на место, предварительно вымыв колесный диск и шину с мылом и капнув немного суперклея. Теперь ролик не проскальзывает. И заменил дохлую кнопку на исправную, снятую с каких-то мышиных обломков. В качестве бонуса оказалось, что «новая» кнопка еще и тихая. Короче, мышь снова в строю. И при проверке оказалось, что ее ролик в составе мыши работает даже хуже, чем в составе моего устройства: медленная прокуртка длинных страниц иногда отпрыгивает на шаг назад, а быстрая не работает. Так что не вполне качественная работа энкодера — это не кривой код и не кривые руки. Иногда так бывает — и вправду деталь виновата.


Теги: