- Цена: $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 — пайкой.
А вот корпус пока существует в предварительном чертеже и дубовом полене 🙂