Как-то мне позвонили из Ростелекома и предложили подключить IP TV. Ну что же, решил я, пусть жена с сыном смотрят в спальне мультики и согласился. И вот принесли мне заветную коробочку. Т.к. отдельного телевизора для неё у меня нет, то решил я значит подключить её к старому монитору, через переходник HDMI-VGA. Для звука у меня были старые компьютерные колонки. Решено — сделано. Всё прекрасно завелось с одним но: с пульта, который шёл в комплекте с приставкой, невозможно регулировать громкость звука. Как так то? Честно сказать никогда с таким не сталкивался. Особо я в причинах не разбирался, но вроде как пульт от Ростелекома прописывается в телевизоре, так что с пульта меняется громкость на самом телевизоре, а не на выходе из приставки. Удобно? Конечно, если подключить приставку к современному телевизору. А вот вставать с кровати и крутить крутилку на колонках каждый раз, когда нужно поменять громкость — неудобно. Решением этого вопроса и займёмся. Соберём отдельное устройство, которое будет регулировать громкость на наших колонках по сигналу с пульта.
Для начала давайте посмотрим, что за сигналы у нас генерирует пульт при нажатии кнопок "громкость вверх", "громкость вниз" и "mute". В качестве приёмника сигналов с пульта я использовал VS1838B.
Это удобный приёмник, т.к. он уже демодулирует 38кГц инфракрасный сигнал от пульта.
Оказалось, что указанные выше кнопки, генерируют два вида сигнала попеременно. Сначала один вариант, при следующем нажатии другой вариант. На рисунке показан один из вариантов сигнала при нажатии кнопки "mute". Сигналы считывал при помощи логического анализатора.
Время между пакетами импульсов около 100 мс. Каждый пакет состоит из 24-х изменений уровня сигнала. Длительность короткого импульса (низкого и верхнего уровней) около 900 мкс, а длительность длинного импульса (также как низкого, так и верхнего уровней) около 1800 мкс. Обозначим короткий импульс нулём, а длинный единицей, тогда полученные наблюдения можно свести в таблицу:
Таблица 1. Сигналы нажатий кнопок от пульта ТВ приставки.
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Громкость вверх 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
Громкость вверх 2 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
Громкость вниз 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
Громкость вниз 2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
Mute 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 |
Mute 2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 |
Теперь нужно выбрать техническую реализацию изменения громкости. Физически громкость в моих колонках меняется при помощи крутилки на корпусе, а крутилка вращает сдвоенный потенциометер на 50К. Т.е. вращением ручки громкости мы как бы одновременно вращаем два переменных резистора, где один отвечает за громкость левого канала, а другой — за громкость правого. Для удалённого регулирования громкости нам потребуется "ручной" сдвоенный потенциометр заменить на два электронных. У меня завалялась пара X9C103P. Это электронный потенциометр на 10 К. Я глянул в даташит микросхемы, на которой построен усилитель моих колонок, и оказалось, что 10К для потенциометра для неё тоже нормально. Поэтому будем использовать эти микросхемы.
Принимать сигналы с пульта будет вышеупомянутый ИК приёмник VS1838B. Осталось разобраться с управляющей электроникой. Я быстро набросал скетч на ардуино, который детектировал сигналы с пульта, но потом подумал, что это будет не так интересно. Во первых, готовое устройство получится громадным, а во вторых, захотелось попробовать более интересное программирование. И я выбрал Attiny13A. Преимущества: размер микроконтроллера с четверть ногтя, очень дешёвый. Недостатки: всего 1кб флэш памяти и 64 байта SRAM. Для сравнения в ардуине (Atmega 328) 32кБ флэш и 2кБ SRAM.
Чтобы уместить в очень ограниченную память паттерны, обозначенные в таблице выше, надо эти данные как-то сокращать. Если в прототипе на ардуино я прямо создавал массивы с временами длительности импульса, например:
const unsigned long UP1_DATA[] = {860, 900, 1750, 900, 860, 900, 860, 900, 860, 900, 860, 900, 860, 900, 860, 1750, 1750, 900, 860, 900, 860, 900, 860};
То, в attiny такой фокус не пройдёт. В идеале нужно уместить паттерн для одного варианта сигнала в один байт. И как видно из таблицы — это не такая сложная задача, т.к. в ней в основном нули. А именно: первые два столбца, столбцы с 6-го по 15-ый, 19-ый и 20-ый и последний столбец — нули. Не будем их записывать в память микроконтроллера. Ещё нулевой столбец — четвёртый. Оставим его. В итоге получим ровно 8 бит на один сигнал (таблица 2).
Таблица 2 — сокращённый вариант таблицы 1
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|
Громкость вверх 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
Громкость вверх 2 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 |
Громкость вниз 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 |
Громкость вниз 2 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
Mute 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 |
Mute 2 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
В коде это выглядит так:
#define UP1_DATA 0b00011001
#define UP2_DATA 0b00011100
#define DOWN1_DATA 0b10011100
#define DOWN2_DATA 0b10011001
#define MUTE_ON_DATA 0b11100100
#define MUTE_OFF_DATA 0b11100001
Но теперь нужно восстановить полный набор данных. Напишем функцию, которая будет возвращать ожидаемую длительность импульса (низкого или верхнего уровней) в зависимости от его номера. Полученный результат будем сравнивать с фактической длительностью полученного импульса от приёмника ИК сигналов. Таким образом, сможем определить какая кнопка была нажата.
Время на нашем микроконтроллере будем измерять по прерыванию переполнения единственного таймера. В нём будем инкрементировать счётчик _timer:
volatile unsigned long _timer = 0;
ISR(TIM0_OVF_vect)
{
_timer++;
}
Максимальная частота Attiny13A 9,6 МГц. Прерывание случается каждые 256 тактов, значит за одну секунду происходит 37500 прерываний по переполнению таймера. Чтобы отмерить 900 мкс нам нужно отсчитать примерно 33 прерывания, а чтобы отмерить 1800 мкс нужно примерно 67 прерываний. Как видно, частоты 9,6 МГц с запасом хватает, чтобы детектировать сигнал с пульта от ТВ приставки.
#define SHORT_TIME 33UL
#define LONG_TIME 67UL
uint8_t _counter = 0;
unsigned long getExpectedTime(uint8_t data)
{
uint8_t index;
if (_counter >= 2 && _counter <= 4)
{
index = _counter - 2;
}
else if (_counter >= 15 && _counter <= 17)
{
index = _counter - 12;
}
else if (_counter >= 20 && _counter <= 21)
{
index = _counter - 14;
}
else
{
return SHORT_TIME;
}
if (data & (1 << index)) return LONG_TIME;
return SHORT_TIME;
}
Переменная _counter глобальная — инкрементируется после изменения уровня сигнала на ИК приёмнике. Параметр data — это один из 6 паттернов сигналов: UP1_DATA, UP2_DATA, DOWN1_DATA, DOWN2_DATA, MUTE_1_DATA, MUTE_2_DATA.
Т.е. задача сводится к тому, чтобы микроконтроллер реагировал на изменение уровня сигнала с ножки ИК приёмника, вычислял длительность сигнала (низкого или высокого уровня) и при помощи функции getExpectedTime определял совпадает ли реальная длительность с ожидаемой. Если набирается один из паттернов кнопочных сигналов — значит соответствующая кнопка и была нажата.
Длительность импульсов будем вычислять в прерывании
volatile bool _hasPulse = false;
volatile unsigned long _RXPreviousTime = 0;
volatile unsigned long _pulseDuration = 0;
ISR(INT0_vect)
{
_pulseDuration = _timer - _RXPreviousTime;
_RXPreviousTime = _timer;
_hasPulse = true;
_rxPinStatus = !!(PINB & (1 << RX_PIN)); //Аналог digitalRead на Ардуино.
}
Итак,
#define SIZE_OF_PATTERNS 6
#define PAUSE_TIME 375UL //10000 мкс
#define HAS_PATTERN_START 0b00111111
#define ERROR_VALUE 19UL
#define SIZE_OF_DATA 23
#define UP1_BT 0
#define UP2_BT 1
#define DOWN1_BT 2
#define DOWN2_BT 3
#define MUTE_ON_BT 4
#define MUTE_OFF_BT 5
//Индекс паттернов в массиве должен быть в соответствии со значениями: UP1_BT, UP2_BT, DOWN1_DATA, DOWN2_DATA, MUTE_ON_BT, MUTE_OFF_BT
const uint8_t PATTERNS[] = {UP1_DATA, UP2_DATA, DOWN1_DATA, DOWN2_DATA, MUTE_ON_DATA, MUTE_OFF_DATA};
uint8_t _hasPattern = HAS_PATTERN_START;
//Если паттерн получен полностью, то возвращаем номер кнопки в массиве PATTERNS.
uint8_t incrementCounter() //Если паттерн получен полностью, то возвращаем номер кнопки в массиве PATTERNS.
{
if (_pulseDuration > PAUSE_TIME)
{
_counter = 0;
_hasPattern = HAS_PATTERN_START;
return 255;
}
if (_hasPattern)
{
unsigned long eTime;
for (uint8_t i = 0; i < SIZE_OF_PATTERNS; i++)
{
if (_hasPattern & (1 << i)) //Если раньше шаблон совпадал.
{
eTime = getExpectedTime(PATTERNS[i]);
if (!((_rxPinStatus ^ !!(_counter % 2)) && _pulseDuration >= eTime - ERROR_VALUE && _pulseDuration <= eTime + ERROR_VALUE)) //Шаблон не совпадает.
{
_hasPattern &= ~(1 << i);
}
}
}
_counter++;
if (_counter == SIZE_OF_DATA)
{
if (_hasPattern) //Какая-то кнопка совпала
{
switch (_hasPattern)
{
case 1: return UP1_BT;
case 2: return UP2_BT;
case 4: return DOWN1_BT;
case 8: return DOWN2_BT;
case 16: return MUTE_ON_BT;
case 32: return MUTE_OFF_BT;
default: return 255;
}
}
else
{
return 255;
}
}
else
{
return 255; //Пока никакая кнопка не совпала
}
}
else
{
return 255; //Никакая кнопка не совпала
}
}
В этой функции переменная _hasPattern — это байт, первые шесть бит которого соответствует одному из шести вариантов кнопок. Изначально биты для всех кнопок равны 1. По мере получения сигналов с ИК-приёмника, если паттерн для заданной кнопки нарушается, то соответствующий бит сбрасывается в 0. В конце должен остаться только один ненулевой бит, если совпал какой-либо паттерн. По положению этого бита определяем какая кнопка была нажата и возвращаем её номер в массиве PATTERNS.
Теперь остаётся только сделать мигание светодиода при получении сигналов с пульта, и управление микросхемой X9C103P.
Размер итоговой прошивки получился 1020 байт из 1024 доступных, прямо тютелька в тютельку, с учётом включённой оптимизацией по размеру (-Os). Полный код доступен в репозитории на GitHub. При разработке использовалась плата ардуино UNO, т.к. отлаживать в ней код проще, чем в Attiny13A. Также на другой плате ардуино UNO был собран эмулятор сигналов пульта, опять-таки для отладки. Все скетчи также есть в репозитории.
Физическая реализация устройства была выполнена на печатной плате (см рисунок), изготовленной при помощи ЛУТ на однослойном текстолите.
Внутри колонок производится питание 9 вольт, что много для нашего микроконтроллера, поэтому снизим напряжение при помощи линейного преобразователя 78L05.
Всего к устройству подводится от колонок 8 проводов: 3 на один потенциометр, 3 на другой и два для питания. Очень удобно для этих целей использовать сетевой UTP кабель, в котором как раз 8 проводников. Теперь, вместо ручки громкости из колонок будет выходить сетевой кабель с нашим устройством на конце.
Фото готового устройства:
Опыта разводки и изготовления печатных плат у меня очень мало, поэтому, конечно, я накосячил и перепутал контакты ИК-приёмника. Хорошо, что GND посередине, получилось просто развернуть сам приёмник. Исходник уже поправил. И да, smd резистора на 100 Ом я у себя не нашёл, решил временно поставить обычный.
От изготовления корпуса для устройства пока отказался, просто покрыл плату акриловым лаком.
Ну и видео работы: