Распиновка энкодера с кнопкой. Подключение поворотного энкодера к компьютеру через USB

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

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

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

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

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


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

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

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

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

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

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

В качестве примера рассмотрим работу энкодера в связке с микроконтроллером AVR:


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

Для такой схемы подключения можно набросать следующую реализацию на языке Си:

Static uint8_t encoderGetVal() { return PINB & 3; } static uint8_t encoderGetCode() { static uint8_t prev; uint8_t val = encoderGetVal(); uint8_t code = (prev << 2) | val; prev = val; return code; } static void encoderInit() { DDRB &= ~0b11; PORTB |= 0b11; encoderGetCode(); } void onEncoderEvent(bool direction); void encoderCheck() { uint8_t code = encoderGetCode(); if (code == 1 || code == 7 || code == 8 || code == 14) { onEncoderEvent(true); } else if (code == 2 || code == 4 || code == 11 || code == 13) { onEncoderEvent(false); } }

Код прост до безобразия - пара if-ов и никаких конечных автоматов. Функция encoderInit() вызывается в начале для инициализации порта и запоминания стартового значения. Функция encoderCheck() вызывается в цикле обработки событий (внутри main() или по таймеру). Обработчик onEncoderEvent(bool) будет вызываться всякий раз, когда произойдёт вращение экнодера и получать флаг направления вращения.

Но тут есть один важный момент: энкодер - штука чувствительная, и если пытаться обрабатывать таким образом, например, события навигации по меню, то даже небольшой поворот ручки энкодера будет многократно вызывать обработчик onEncoderEvent() , в результате чего, курсор меню вместо перемещения на следующий/предыдущий элемент, будет улетать сразу в конец/начало списка. Регулировать чувствительность энкодера можно изменением частоты вызова encoderCheck() (обычно оптимальная частота ~ 10 Гц). При этом метод encoderGetCode() следует вызывать как можно чаще, чтобы всегда иметь актуальное значение последнего состояния контактов (с частотой где-то ~ 100 Гц).

На ассемблере этот код мог бы выглядеть следующим образом:

EQU encoder_port PORTB .EQU encoder_pin PINB .EQU encoder_ddr DDRB .DSEG .ORG SRAM_START sEncoderPrev: .BYTE 1 ... .CSEG .ORG $0000 ... Encoder_init: cbi encoder_ddr, 0 cbi encoder_ddr, 1 sbi encoder_port, 0 sbi encoder_port, 1 in r0, encoder_pin andi r0, 3 sts sEncoderPrev, r0 ... Encoder_check lds ZL, sEncoderPrev lsl ZL lsl ZL in r0, encoder_pin andi r0, 3 sts sEncoderPrev, r0 or ZL, r0 ; 1 7 8 14 -> по часовой стрелке cpi ZL, 1 breq Encoder_clockwise cpi ZL, 7 breq Encoder_clockwise cpi ZL, 8 breq Encoder_clockwise cpi ZL, 14 breq Encoder_clockwise ; 2 4 11 13 -> против часовой стрелки cpi ZL, 2 breq Encoder_counterclockwise cpi ZL, 4 breq Encoder_counterclockwise cpi ZL, 11 breq Encoder_counterclockwise cpi ZL, 13 breq Encoder_counterclockwise rjmp Encoder_done Encoder_clockwise: ; ; тут код обработчика вращения по часовой стрелке; Encoder_counterclockwise: ; ; тут код обработчика вращения против часовой стрелки; Interval_enc_done.

Узнайте, как использовать инкрементальный поворотный энкодер в проекте на Arduino.

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

В данной статье мы покажем вам, как использовать инкрементальный поворотный энкодер в проекте на Arduino. Мы объясним, как бороться с дребезгом контактов и интерпретировать сигналы энкодера в программе микроконтроллера, используя прерывания.

Сигнал квадратурного выхода инкрементального энкодера

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

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

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

Механические энкодеры имеют встроенные переключатели, которые формируют сигнал на квадратурном выходе во время вращения.

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

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

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

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

Простое приложение

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

Схема построена на базе платы Arduino Uno. Для графического интерфейса используется LCD дисплей Nokia 5110. В качестве средств управления добален механический поворотный энкодер с кнопкой и RC-фильтрами.

Мы разработаем простое программное меню, в котором и продемонстрируем работу поворотного энкодера.

Обработка сигналов энкодера с помощью прерываний

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

В Atmega328 есть два типа прерываний, которые можно использовать для этих целей; внешнее прерывание и прерывание по изменению состояния вывода. Выводы INT0 и INT1 назначены на внешнее прерывание, а PCINT0 - PCIN15 назначены на прерывание по изменению состояния вывода. Внешнее прерывание может определить, произошел ли спад или нарастание входного сигнала, и может быть запущено при одном из следующих состояний: нарастание, спад или переключение. Для прерывания по изменению состояния выводов существует гораздо больше аппаратных ресурсов, но оно не может обнаруживать нарастающий и спадающий фронты, и оно вызывается, когда происходит любое изменение логического состояния (переключение) на выводе.

Чтобы использовать прерывание по изменению состояния выводов, подключите выходы поворота энкодера A и B к выводам A1 и A2 , а выход кнопки - к выводу A0 платы Arduino, как показано на принципиальной схеме. Установите выводы A0 , A1 и A2 в режим входа и включите их внутренние подтягивающие резисторы. Включите прерывание по изменению состояния выводов в регистре PCICR и включите прерывания для выводов A0 , A1 и A2 в регистре PCMS1 . При обнаружении любого изменения логического состояния на одном из этих входов будет вызовано ISR(PCINT1_vect) (прерывание по изменению состояния выводов).

Поскольку прерывание по изменению состояния выводов вызывается для любого логического изменения, нам необходимо отслеживать оба сигнала (и A, и B) и обнаруживать вращение при получение ожидаемой последовательности. Как видно из диаграммы сигналов, движение по часовой стрелке генерирует A = …0011… и B = …1001… . Когда мы записываем оба сигналы в байты seqA и seqB , сдвигая последнее чтение вправо, мы можем сравнить эти значения и определить новый шаг вращения.

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

Void setup() { pinMode(A0, INPUT); pinMode(A1, INPUT); pinMode(A2, INPUT); // Включить внутренние подтягивающие резисторы digitalWrite(A0, HIGH); digitalWrite(A1, HIGH); digitalWrite(A2, HIGH); PCICR = 0b00000010; // 1. PCIE1: Включить прерывание 1 по изменению состояния PCMSK1 = 0b00000111; // Включить прерывание по изменению состояния для A0, A1, A2 } void loop() { // Основной цикл } ISR (PCINT1_vect) { // Если прерывание вызвано кнопкой if (!digitalRead(A0)) { button = true; } // Если прерывание вызвано сигналами энкодера else { // Прочитать сигналы A и B boolean A_val = digitalRead(A1); boolean B_val = digitalRead(A2); // Записать сигналы A и B в отдельные последовательности seqA <<= 1; seqA |= A_val; seqB <<= 1; seqB |= B_val; // Маскировать четыре старших бита seqA &= 0b00001111; seqB &= 0b00001111; // Сравнить запсанную последовательность с ожидаемой последовательностью if (seqA == 0b00001001 && seqB == 0b00000011) { cnt1++; left = true; } if (seqA == 0b00000011 && seqB == 0b00001001) { cnt2++; right = true; } } }

Использование внешнего прерывания делает процесс более простым, но поскольку для этого прерывания назначено только два вывода, то вы не сможете использовать его для других целей, если займете его энкодером. Чтобы использовать внешнее прерывание, вы должны установить выводы 2 (INT0) и 3 (INT1) в режим входа и включить их внутренние подтягивающие резисторы. Затем выберите вариант спадающего фронта для вызова обоих прерываний в регистре EICRA . Включите внешние прерывания в регистре EIMSK . Когда начнется вращение вала энкодера, сначала ведущий сигнал падает до логического нуля, а второй сигнал некоторое время остается на уровне логической единицы. Поэтому нам нужно определить, какой из сигналов во время прерывания находится в состоянии логической единицы. После того, как ведущий сигнал упал до логического нуля, через некоторое время второй сигнал также упадет до логического нуля, что вызовет другое прерывание. Но этот раз и другой (ведущий) сигнал будет на низком логическом уровне, что означает, что это не начало вращения, поэтому мы игнорируем его.

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

Void setup() { pinMode(2, INPUT); pinMode(3, INPUT); // Включить внутренние подтягивающие резисторы digitalWrite(2, HIGH); digitalWrite(3, HIGH); EICRA = 0b00001010; // Выбрать вызов по спадающему фронту EIMSK = 0b00000011; // Включить внешнее прерывание } void loop() { // Основной цикл } ISR (INT0_vect) { // Если второй сигнал находится в состоянии логической единицы, то это новое вращение if (digitalRead(3) == HIGH) { left = true; } } ISR (INT1_vect) { // Если второй сигнал находится в состоянии логической единицы, то это новое вращение if (digitalRead(2) == HIGH) { right = true; } }

Полный код скетча Arduino, включающий основной цикл приведен ниже:

#include #include #include volatile byte seqA = 0; volatile byte seqB = 0; volatile byte cnt1 = 0; volatile byte cnt2 = 0; volatile boolean right = false; volatile boolean left = false; volatile boolean button = false; boolean backlight = true; byte menuitem = 1; byte page = 1; Adafruit_PCD8544 display = Adafruit_PCD8544(13, 12,11, 8, 10); void setup() { pinMode(A0, INPUT); pinMode(A1, INPUT); pinMode(A2, INPUT); // Включить внутренние подтягивающие резисторы digitalWrite(A0, HIGH); digitalWrite(A1, HIGH); digitalWrite(A2, HIGH); // Включить подсветку LCD pinMode(9, OUTPUT); digitalWrite(9, HIGH); PCICR = 0b00000010; // 1. PCIE1: Включить прерывание 1 по изменению состояния PCMSK1 = 0b00000111; // Включить прерывание по изменению состояния для A0, A1, A2 // Initialize LCD display.setRotation(2); // Установить ориентацию LDC display.begin(60); // Установить контрастность LCD display.clearDisplay(); // Очистить дисплей display.display(); // Применить изменения sei(); } void loop() { // Создать страницы меню if (page==1) { display.setTextSize(1); display.clearDisplay(); display.setTextColor(BLACK, WHITE); display.setCursor(15, 0); display.print("MAIN MENU"); display.drawFastHLine(0,10,83,BLACK); display.setCursor(0, 15); if (menuitem==1) { display.setTextColor(WHITE, BLACK); } else { display.setTextColor(BLACK, WHITE); } display.print(">Contrast: 99%"); display.setCursor(0, 25); if (menuitem==2) { display.setTextColor(WHITE, BLACK); } else { display.setTextColor(BLACK, WHITE); } display.print(">Test Encoder"); if (menuitem==3) { display.setTextColor(WHITE, BLACK); } else { display.setTextColor(BLACK, WHITE); } display.setCursor(0, 35); display.print(">Backlight:"); if (backlight) { display.print("ON"); } else { display.print("OFF"); } display.display(); } else if (page==2) { display.setTextSize(1); display.clearDisplay(); display.setTextColor(BLACK, WHITE); display.setCursor(15, 0); display.print("ENC. TEST"); display.drawFastHLine(0,10,83,BLACK); display.setCursor(5, 15); display.print("LEFT RIGHT"); display.setTextSize(2); display.setCursor(5, 25); display.print(cnt1); display.setCursor(55, 25); display.print(cnt2); display.setTextSize(2); display.display(); } // Выполнить действие, если от энкодера принята новая команда if (left) { left = false; menuitem--; if (menuitem==0) { menuitem=3; } } if (right) { right = false; menuitem++; if (menuitem==4) { menuitem=1; } } if (button) { button = false; if (page == 1 && menuitem==3) { digitalWrite(9, LOW); if (backlight) { backlight = false; digitalWrite(9, LOW); } else { backlight = true; digitalWrite(9, HIGH); } } else if (page == 1 && menuitem==2) { page=2; cnt1=0; cnt2=0; } else if (page == 2) { page=1; } } } ISR (PCINT1_vect) { // Если прерывание вызвано кнопкой if (!digitalRead(A0)) { button = true; } // Или если прерывание вызвано сигналами энкодера else { // Прочитать сигналы A и B boolean A_val = digitalRead(A1); boolean B_val = digitalRead(A2); // Записать сигналы A и B в отдельные последовательности seqA <<= 1; seqA |= A_val; seqB <<= 1; seqB |= B_val; // Маскировать четыре старших бита seqA &= 0b00001111; seqB &= 0b00001111; // Сравнить запсанную последовательность с ожидаемой последовательностью if (seqA == 0b00001001 && seqB == 0b00000011) { cnt1++; left = true; } if (seqA == 0b00000011 && seqB == 0b00001001) { cnt2++; right = true; } } }

Энкодер в действии вы можете увидеть на видео, приведенном ниже.

Давно хотел приспособить к ноуту регулятор громкости, сделанный из энкодера . Подключать этот регулятор нужно будет к USB, чтобы все было «по-взрослому» (да и по-другому никак внешнее устройство к ноуту не подключишь). Крутим энкодер влево - громкость должна уменьшаться, вправо - должна увеличиваться. Жмем вниз ручку энкодера - запускаем какую-нибудь полезную программу, или переключаемся на регулирование тембра.

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

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

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

Чтобы заработал регулятор громкости, нужно решить, как минимум, три инженерные задачи:

Шаг 1 . Создание низкоскоростного USB-устройства на макетке.
Шаг 2 . Подключить к этому USB-устройству энкодер, добиться, чтобы микроконтроллер его отрабатывал, и передавал в компьютер информацию о вращении энкодера.
Шаг 3 . Разобраться, как можно программно управлять регулятором громкости. Наверняка есть какое-нибудь мультимедиа-API, которое позволяет это делать. Программа минимум - нужно написать программку, которая будет принимать сигналы от USB-устройства и управлять громкостью. Неплохо бы, конечно, написать драйвер, но за это браться страшновато. Лучше оставим на потом.

Итак, опишу процесс создания регулятора по шагам. Подробности опускаю, иначе будет слишком скучно. Кому интересно, см. исходники и документацию по ссылкам.

[Шаг 1. Создание низкоскоростного USB-устройства на макетке ]

Этот шаг прошел, даже не начавшись - как-то слишком просто и банально. Тупо скачал пример проекта по ссылке . Поправил файлик usbconfig.h - для понтов назвал мое устройство ENCODER DEMO , на большее фантазии не хватило. Проверил в Makefile тип проца (ATmega16), частоту кварца (16 МГц) - чтобы соответствовало моей макетке AVR-USB-MEGA16. Скомпилил проект в AVRStudio, прошил макетку, подключил к компьютеру - все завелось с полоборота, мое USB-устройство исправно заработало как виртуальный COM-порт - все в точности так, как написано в статье .

[Шаг 2. Подключить к USB-устройству энкодер ]

Этот шаг у меня вызывал самые большие опасения, что все заработает как надо. Что энкодер подключу и смогу его читать - в этом я не сомневался. Были сомнения, что смогу его считывать качественно, когда в фоне работает ещё и обработка протокола USB - все-таки это задача для микроконтроллера не из легких (как впоследствии оказалось - волновался я совершенно напрасно).

Как обычно, начал рыться в Интернете в поисках готовых подпрограмм для чтения энкодера. Нашел очень быстро то, что нужно - именно для AVR, очень простой код на C , файлики encoder.c и encoder.h. Что ни говори, а open source крутая штука.

Приделал два индикационных светодиода - ЗЕЛЕНЫЙ и ЖЕЛТЫЙ - для обозначения направления вращения энкодера. Подключил энкодер для удобства прямо к разъему ISP, воспользовавшись тем, что сигналы MOSI, MISO и SCK - это всего лишь ножки PB5, PB6 и PB7 микроконтроллера ATmega16 (подключил туда фазы A и B, а также кнопку энкодера).

Поправил определения ножек, добавил код инициализации. Присоединил к проекту модуль encoder.c. Добавил в главный цикл main управление зеленым и желтым светодиодами, когда приходит инфа с энкодера. КРАСНЫЙ светодиод привязал к кнопке энкодера - когда её нажимаем, красный светодиод зажигается, отпускаем - гаснет. Скомпилировал, прошил - работает. Кручу ручку влево, и в такт щелчкам энкодера вспыхивает зеленый светодиод. Кручу ручку вправо - вспыхивает желтый светодиод. Несмотря на то, что чтение энкодера происходит методом поллинга, благодаря эффективному коду к чтению энкодеру НИКАКИХ нареканий даже при одновременной работе с библиотекой V-USB (респект, Pashgan!). Добавил вывод информации от энкодера в виртуальный COM-порт (крутим энкодер влево вывожу в консоль минусики "-", крутим вправо вывожу в консоль плюсики "+"). По таймеру каждые 10 мс вывожу состояние кнопки энкодера и индицирую её красным светодиодом (кнопка нажата - передаю символ "1", отпущена - "0"). Все работает. Скукотища.

В заключение выкинул модули cmd.c, crc16.c, eepromutil.c, strval.c. Объем кода упал до 3 килобайт - отлично, теперь поместится и в память ATtiny45 (можно в будущем задействовать макетку AVR-USB-TINY45, она меньше по размерам и дешевле).

[Шаг 3. Разобраться, как можно программно управлять регулятором громкости ]

Как обычно, прогуглил вопрос. Отсеял кучу мусора, и наконец выгреб жемчужину - . Дальше дело техники. Достаю любимый детский конструктор - Visual Studio. Ни о чем не думая, визардом генерю dialog-based приложение. Бросаю на панель движок регулятора громкости, привязываю к нему переменную, добавляю обработчик положения движка. При старте приложения настраиваю движок на минимум 0 и максимум 65535 (чтобы соответствовало границам значения громкости, которым манипулируют библиотеки управления микшером). Считываю функцией mixerGetControlDetails текущее значение громкости, и ставлю движок регулятора в соответствующее положение. В обработчике положения движка все наоборот - читаю положение движка и функцией mixerSetControlDetails устанавливаю нужную громкость. Управление громкостью делаю в точности так, как написано в статье . Проверил - работает.

Теперь осталось дело за малым - читать, что приходит с виртуального COM-порта (на нём у нас висит свежеиспеченное USB-устройство с энкодером). Если пришел минусик (-) то двигаем движок влево (уменьшаем громкость), плюсик (+), то двигаем движок вправо (увеличиваем громкость). Если приходят символы 0 и 1, то соответственно управляем состоянием чекбокса (просто для индикации - нажата кнопка энкодера, или нет). С COM-портом можно работать, как с обычным файлом (см. ). Инициализируем подключение к COM-порту как открытие файла (вызовом ::CreateFile ) в блокирующем режиме. Запускаем отдельный поток, туда в бесконечный цикл добавляем чтение файла (блокирующим вызовом ::ReadFile ) по одному символу, и этот символ анализируем. По тому, какой символ пришел, крутим движок слайдера в нужную сторону (громкость будет регулировать обработчик слайдера) или обновляем состояние чекбокса. Проверил - работает.

Вот и все, собственно. Дальше можно заниматься бесконечным (и, наверное, бесполезным) улучшательством. Сделать автоматический поиск нужного виртуального COM-порта (сейчас для упрощения имя COM-порта передается через командную строку). Переделать USB-устройство с CDC -класса на HID - это может упростить код USB-устройства, а также упростить программный поиск и открытие устройства на компьютере по VID и HID. Или написать вместо программы сервис (чтобы не надо было запускать отдельную программу). Или даже драйвер. Это очень интересно, но не умею (может, кто из хабравчан научит уму-разуму?..). Прикрутить к кнопке энкодера какое-нибудь действие. Ну и так далее до бесконечности.

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

[UPD120803 ]

Один грамотный человек собрал на микроконтроллере AVR