При создании HTPC одним из вопросов является способ управления оболочкой. Думаю, не стоит рассказывать о том, что традиционные устройства ввода — клавиатура и мышь не подходят для данной задачи. Гораздо удобнее управлять HTPC так же как и другой бытовой электроникой — с помощью ПДУ. Чаще всего используются ПДУ от DVD-плееров и аналогичной техники совместно с LIRC / WinLIRC, или Windows MCE-совместимые пульты с USB-приемниками, коих полно в китайских интернет-магазинах. Такие ПДУ эмулируют usb-hid клавиатуру (и иногда мышь). У этих пультов есть существенный недостаток — если материнская плата и BIOS не поддерживают включение питания и пробуждение от usb-устройств, то с помощью такого пульта можно будет управлять устройством, выключать его, но включить не выйдет. С этим недостатком я и решил бороться.
Для своего HTPC я выбрал материнскую плату Intel D2700MUD. Как позже выяснилось — опрометчиво, поскольку встроенный видеоконтроллер GMA 3650, основанный на PowerVR, полностью поддерживается только в 32-битных Windows, а в Linux поддержка очень ограничена — не работает аппаратное декодирование видео. Но меня устраивает работа HTPC под управлением Windows 7 Home Basic. Также эта плата не умеет пробуждаться по сигналу от USB-клавиатуры.
Также у меня уже был пульт (Philips 2422 549 01930), который мне показался подходящим. Но WinLIRC с ним работал крайне нестабильно. Видимо, использовался какой-то необычный протокол.
Первая мысль была такой — подключить к Arduino IR-приемник, питание взять от шины 5VSB блока питания HTPC, а включение питания (и выключение) осуществлять с помощью имитации замыкания пинов на материнской плате, к которым подключается кнопка включения питания, а остальные команды передавать через RS-232. Но Arduino слишком дорога и занимает много места в и без того небольшом корпусе. Поэтому я решил обойтись дешёвым микроконтроллером ATMega8, в который можно зашить бутлоадер Arduino и программировать его как Arduino NG.
Разбор протокола
Для Arduino существует неплохая библиотека для работы с инфракрасными ПДУ — IRRemote, но она не работает с Arduino NG — не хватает памяти. К тому же, мне не удалось заставить эту библиотеку понимать мой пульт даже на Arduino UNO. Как я писал выше, LIRC (и WinLIRC) не очень хорошо работали с этим пультом — часто пропускали нажатия кнопок, иногда неверно определяли нажатую клавишу. Сам пульт работал исправно — с «родным» DVD-плейером проблем не возникало. Поиски информации о протоколе для этого пульта ничего не дали, так что я решил разобраться с ним самостоятельно.
Для этого мне понадобился осциллограф, но его под руками не оказалось, да и взять было негде. Но в случае с сигналами ПДУ вполне подходит линейный вход звуковой карты. К нему я подключил инфракрасный приёмник TSOP 31236.
Далее с помощью любого аудиоредактора можно записать входной сигнал и проанализировать:
Сначала идет длинная посылка, необходимая для установки уровня автоматической регулировки усиления в приёмнике. Затем следуют импульсы различной длины. Я написав промежуточный скетч для Arduino, который определял время между фронтами импульсов в микросекундах и выводил их в терминал через RS232. Полученные данные я загрузил в Excel:
Когда я только начал изучать протокол, моей главной ошибкой было то, что я считал длительности самих импульсов, но игнорировал длительность интервалов между ними. После того, как я начал считать интервалы между фронтами все встало на свои места. Как видно, большинство импульсов интервалов между фронтами имеют длительности в пределах 350-550 мкс и 700-1000 мкс. Значит так обозначаются значения передаваемых битов — «0» и «1». В протоколе ПДУ используется toggle-бит. Это означает что при нескольких последовательных нажатиях одной и той же клавиши на пульте, в коде будет меняться один бит (иногда несколько). В моем случае toggle-бит имеет необычную длительность — 1200-1400 мкс. К тому же число фронтов в «четных» и «нечетных» посылках отличается. Позже я пришёл к выводу, что интервал 1200-1400 мкс это сумма их двух битов с одинаковым уровнем, но различной длительностью (400+800). В коде я такую посылку обозначил как последовательность из двух бит — «01». Тогда количество бит на выходе стало постоянным.
Скетч для Arduino
Затем я написал скетч, который считал время между изменениями уровня на входе и выводил полученное число в uart. С помощью несложной отладки я добился стабильности определения кодов, добавил сложение с маской для игнорирования toggle-бита (у меня не было функций, в которых он пригодился бы). Потом я жестко закодил функцию, чтобы при получении кода кнопки «Power» микроконтроллер прижимал к земле один из «цифровых» пинов Arduino (в моем случае — 12).
// Заранее прошу прощения за индусский код. Я знаю что можно было сделать намного правильнее и лучше,
// но для меня было важным решить задачу, а на изящность решения я не претендовал.
byte a = 0; // Текущий уровень на входе
byte b = 0; // Сохраненный уровень на входе
byte bc = 0; // Счетчик битов
const boolean dbg=false; // Режим отладки =)
boolean start=false; // Запущен ли отсчет импульсов
unsigned int usecs = 0; // Таймер 1 - определяет длительность фронтов
unsigned int usecs2 = 0; // Таймер 2 - определяет паузы между посылками
unsigned int plen = 0; // Длительность интервала между фронтами
unsigned int plen2 = 0; // Длительность паузы между посылками
String bits; // Представление буфера в виде единиц и нулей, для отладки
unsigned int buffer; // Буфер. Собственно, сюда помещается код нажатой кнопки
void setup()
{
// Initialize the digital pin as an output.
// Pin 13 has an LED connected on most Arduino boards
pinMode(13, OUTPUT); // Ок, пусть светодиод там и будет.
pinMode(12, OUTPUT); // А пин 12 будет подключаться к разъему кнопки питания на мат.плате.
digitalWrite(13,HIGH); // Включаем светодиод.
digitalWrite(12,HIGH); // Настраиваем на выходе высокий уровень, чтобы плата считала что контакт разомкнут.
pinMode(7, INPUT); // Сюда подаем сигнал от ИК-приемника TSOP.
Serial.begin(115200);
Serial.println("READY");
usecs = micros(); // Инициализируем индусский таймер
usecs2 = micros();
}
void loop()
{
a = digitalRead(7); // Считываем значение уровня на входе
if(a != b) { // Уровень изменился! Фронт импульса.
start=true; // Отсчет импульсов считаем начатым.
b = a; // Сбрасываем детектор фронта
plen = micros() - usecs; // Замеряем прошедшее время между предыдущим и текущим фронтом
usecs = micros(); // Сбрасываем таймер
usecs2 = micros();
if (plen<2000) { // фильтруем стартовый импульс и длительность между импульсами
if (plen>200 && plen < 620) { // ноль
bits += "0";
buffer = buffer << 1;
buffer = buffer | 0;
bc++;
}
if (plen>620 && plen < 1150) { // единица
bits += "1";
buffer = buffer << 1;
buffer = buffer | 1;
bc++;
}
if (plen>1150 && plen < 1600) { // тоггл-бит
bits += "01";
buffer = buffer << 2; // Тут сдвигаем на два бита!
buffer = buffer | 1;
bc++;
bc++;
}
if(dbg==true){
Serial.print(plen); // если включена отладка - выводим длительности импульсов
Serial.print(";");
}
}
} else { // Если в этом цикле изменения уровня на входе не произошло
plen2 = micros() - usecs2; // Определяем длительность паузы
if(plen2 > 5000 ) { // ЕСЛИ ПАУЗА БОЛЬШЕ 5 МИЛЛИСЕКУНД, ЭТО ЗНАЧИТ ЧТО ПОСЫЛКА ЗАКОНЧИЛАСЬ ИЛИ ЕЩЕ НЕ НАЧИНАЛАСЬ
usecs2 = micros(); // сбрасываем Таймер 2
if(start==true){ // Если до этого был запущен отсчет импульсов, значит у нас в буфере должен был скопиться код клавиши.
usecs = micros(); // сбрасываем таймер 1
while(bc<40){ // выравниваем вывод. честно говоря, я уже не помню почему так сделал, но раз сделал - значит так надо.
bits += "0";
buffer = buffer << 1;
bc++;
}
buffer = buffer & 1048575; // Использование magic-numbers плохо влияет на карму. Это маска для toggle-бита.
Serial.println(buffer); // ВЫВОДИМ КОД!
if (dbg){ // если включена отладка
Serial.print("bits="); // выводим код в виде единиц и нулей
Serial.println(bc); // выводим число битов
}
digitalWrite(13,LOW); // Мигаем светодиодом в знак того,
delay(100); // что код от пульта был
digitalWrite(13,HIGH); // принят и распознан.
switch(buffer){ // Прочие действия при нажатии некоторых кнопок.
case 8448: // Кнопка Power на моём пульте.
Serial.println("POWER"); // Выводим сообщение POWER помимо кода кнопки. Удобно при разборе логов EventGhost.
digitalWrite(12,LOW); // Прижимаем к земле пин кнопки питания на мат.плате
delay(300); // На 300мс
digitalWrite(12,HIGH); // И отпускаем
default:
break;
}
buffer=0; // Обнуляем буффер
bits=""; // Обнуляем отладочную строку с битами
start=false; // Сбрасываем признак отсчета
bc=0; // Сбрасываем количество бит
}
}
}
}
А что дальше?
А дальше приложение EventGhost считывает коды из порта RS-232 и «дёргает за ниточки» всем известной оболочки XBMC. Настройка того и другого индивидуальна и в то же время проста, поэтому не заслуживает внимания.
Результат
Устройство работает уже несколько месяцев. Пользуется им в основном мой отец, далекий от техники, поэтому минимально необходимое количество кнопок на пульте является достаточно удобным обстоятельством. Питание включается и отключается, ошибочных срабатываний не бывает.
Автор: k0ldbl00d