- Цена: $1.25
 
На разработку этой поделки меня подвигла статья USB регулятор громкости для ПК и не только.
Но просто «volume + mute» меня не устраивала, хотелось бы иметь функциональные кнопки для управления плеером. Так как ранее с AVR дела не имел от слова совсем — решил заказать одну штучку Digispark85 (с microUSB портом). Пока ехала посылка — курил мануалы, рылся в уголке некроманта и прикидывал дизайн корпуса.
В обзоре не будет распаковки, ТТХ, описания Attiny85 — сразу к делу.
Вводная часть
Контроллер использует постоянное соединение с хост устройством по USB, задействуя два порта (P3/ADC3, Р4/ADC2). Еще два порта задействуются для энкодера (о них ниже), один используется при (пере-)программировании контроллера (Р5/ADC0/Reset). Таким образом, для блока функциональных кнопок остается один порт. Проще всего его подключить по классической схеме с резистивным делителем напряжения, а для распознавания нажатий использовать встроенный в контроллер АЦП. Исходя из ограничений (см выше) остается единственный вариант такого подключения – в порт Р2/ADC1.
Энкодер будем подключать на порты Р0 и Р1 (вместо Р0 и Р2 как в базовом варианте), а встроенную в энкодер кнопку «mute» вынесем на блок кнопок (сорри за тавтологию).
Принципиальная схема контроллера
Обратите внимание: из схемы исключен светодиод подключенный к порту PB1.

Немного математики
Количество дополнительных кнопок зависит от вашей фантазии, но ограничено объемом программного кода, точностью АЦП и выбранной разрядностью. Для моих нужд достаточно 5 кнопок и 8-битного режима АЦП. В качестве опорного напряжения лучше всего использовать Vcc (при питании от USB это примерно +5в +-допуски), при этом результаты отсчетов АЦП не будут зависеть от колебаний этого напряжения. Для четкого распознавания нажатий я подобрал номиналы резисторов таким образом, чтобы разница между отсчетами АЦП для соседних кнопок была не менее 40.
Для упрощения расчетов сделал таблицу (прилагаю) по которой можно подобрать оптимальные параметры делителя и рассчитать пороги для определения нажатой кнопки. Три правые колонки с серым заголовком содержат реальные измерения и расчеты для АЦП после сборки блока, в программном коде используются именно они.

Скорость преобразования АЦП зависит текущей тактовой частоты микроконтроллера и выбранного делителя, т.н. prescaler’а. Устройство работает на 16.5МГц, поэтому для функционирования АЦП необходимо будет установить максимальное значение prescaler =128. Это обеспечит тактирование с частотой примерно 128КГц, время на одно преобразование составит 1/10000 сек.
Программная часть
Представляет модифицированный код из TrinketVolumeKnobPlus, из которого убрано лишнее и добавлено нужное.
При нажатии и удержании любой кнопки подается только одна команда (блокировка от keystroke repeat), но при желании можно модифицировать код таким образом, чтобы при нажатии << / >> контроллер повторял соответствующие команды.
После компиляции прошивка занимает примерно 60% от отведенных 8КБ flash памяти (часть занимает специальный загрузчик).
Листинг под спойлером
#include "TrinketHidCombo.h"
#define PIN_ENCODER_A 0
#define PIN_ENCODER_B 1 //changeв from P2 port
#define TRINKET_PINx PINB
static uint8_t enc_prev_pos = 0;
static uint8_t enc_flags = 0;
static char sw_was_pressed = 0; //keystroke flag
static uint8_t adc_val = 255; //ADC value
void setup()
{
  pinMode(PIN_ENCODER_A, INPUT);
  pinMode(PIN_ENCODER_B, INPUT);
  digitalWrite(PIN_ENCODER_A, HIGH);
  digitalWrite(PIN_ENCODER_B, HIGH);
  // init ADC1 at 8bit 16MHz 128 prescaler
  ADMUX =
      (1 << ADLAR) |     // left shift result
      (0 << REFS1) |     // Sets ref. voltage to VCC, bit 1
      (0 << REFS0) |     // Sets ref. voltage to VCC, bit 0
      (0 << MUX3)  |     // use ADC1 for input (PB2), MUX bit 3
      (0 << MUX2)  |     // use ADC1 for input (PB2), MUX bit 2
      (0 << MUX1)  |     // use ADC1 for input (PB2), MUX bit 1
      (1 << MUX0);       // use ADC1 for input (PB2), MUX bit 0
  ADCSRA = 
      (1 << ADEN)  |     // Enable ADC 
      (1 << ADPS2) |     // set prescaler to 128, bit 2
      (1 << ADPS1) |     // set prescaler to 128, bit 1
      (1 << ADPS0);     // set prescaler to 128, bit 0
      
  TrinketHidCombo.begin();
  if (digitalRead(PIN_ENCODER_A) == LOW) {
    enc_prev_pos |= (1 << 0);
  }
  if (digitalRead(PIN_ENCODER_B) == LOW) {
    enc_prev_pos |= (1 << 1);
  }
}
void loop()
{
  int8_t enc_action = 0;
  uint8_t enc_cur_pos = 0;
  if (bit_is_clear(TRINKET_PINx, PIN_ENCODER_A)) {
    enc_cur_pos |= (1 << 0);
  }
  if (bit_is_clear(TRINKET_PINx, PIN_ENCODER_B)) {
    enc_cur_pos |= (1 << 1);
  }
  if (enc_cur_pos != enc_prev_pos) {
    if (enc_prev_pos == 0x00) {
      if (enc_cur_pos == 0x01) {
        enc_flags |= (1 << 0);
      }
      else if (enc_cur_pos == 0x02) {
        enc_flags |= (1 << 1);
      }
    }
    if (enc_cur_pos == 0x03) {
      enc_flags |= (1 << 4);
    }
    else if (enc_cur_pos == 0x00) {
      if (enc_prev_pos == 0x02) {
        enc_flags |= (1 << 2);
      }
      else if (enc_prev_pos == 0x01) {
        enc_flags |= (1 << 3);
      }
      if (bit_is_set(enc_flags, 0) && (bit_is_set(enc_flags, 2) || bit_is_set(enc_flags, 4))) {
        enc_action = 1;
      }
      else if (bit_is_set(enc_flags, 2) && (bit_is_set(enc_flags, 0) || bit_is_set(enc_flags, 4))) {
        enc_action = 1;
      }
      else if (bit_is_set(enc_flags, 1) && (bit_is_set(enc_flags, 3) || bit_is_set(enc_flags, 4))) {
        enc_action = -1;
      }
      else if (bit_is_set(enc_flags, 3) && (bit_is_set(enc_flags, 1) || bit_is_set(enc_flags, 4))) {
        enc_action = -1;
      }
      enc_flags = 0; // reset for next time
    }
  }
  enc_prev_pos = enc_cur_pos;
  if (enc_action > 0) {
    TrinketHidCombo.pressMultimediaKey(MMKEY_VOL_UP);
  }
  else if (enc_action < 0) {
    TrinketHidCombo.pressMultimediaKey(MMKEY_VOL_DOWN);
  }
// Buttons decoder block
ADCSRA |= (1 << ADSC);         // start ADC measurement
while (ADCSRA & (1 << ADSC)); // waiting for completion
adc_val = ADCH; // read ADC register
if (adc_val < 210) { //some button pressed
  if (sw_was_pressed == 0) { // check for hold down button
    if (adc_val < 150) {
      if (adc_val < 95) {
        if (adc_val < 55) {
          if (adc_val < 35) { //sw1 is pressed
            TrinketHidCombo.pressMultimediaKey(MMKEY_MUTE);
            }
          else
          { //sw2 is pressed
          TrinketHidCombo.pressMultimediaKey(MMKEY_SCAN_PREV_TRACK);
          }
        }
        else
        { //sw3 is pressed
        TrinketHidCombo.pressMultimediaKey(MMKEY_STOP);
        }
      }
      else
      { //sw4 is pressed
      TrinketHidCombo.pressMultimediaKey(MMKEY_PLAYPAUSE);
      }
    }
    else
    { //sw5 is pressed
    TrinketHidCombo.pressMultimediaKey(MMKEY_SCAN_NEXT_TRACK);
    }
  delay(5);
  sw_was_pressed = 1; // set keystroke flag
  }
 }
 else
 {
 sw_was_pressed = 0; // clear keystroke flag
 }
TrinketHidCombo.poll();
}
Печатная плата и корпус
Печатная плата — это сам Digispark. Подключение к внешним устройствам — через штатный microUSB порт.
Для блока кнопок я использовал платку управления от старого сгоревшего монитора LG, поменяв в соответствии с таблицей SMD резисторы. Саму платку и поддерживающую рамку с толкателями немного укоротил, подклеил для жесткости бортики убрал гнездо для шлейфа. Соединение с Digispark85 — пайкой.

А вот корпус пока существует в предварительном чертеже и дубовом полене 🙂

