Анализ протоколов работы пультов ДУ

от автора

При создании 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 с ним работал крайне нестабильно. Видимо, использовался какой-то необычный протокол.

image

Первая мысль была такой — подключить к Arduino IR-приемник, питание взять от шины 5VSB блока питания HTPC, а включение питания (и выключение) осуществлять с помощью имитации замыкания пинов на материнской плате, к которым подключается кнопка включения питания, а остальные команды передавать через RS-232. Но Arduino слишком дорога и занимает много места в и без того небольшом корпусе. Поэтому я решил обойтись дешёвым микроконтроллером ATMega8, в который можно зашить бутлоадер Arduino и программировать его как Arduino NG.

Разбор протокола

Для Arduino существует неплохая библиотека для работы с инфракрасными ПДУ — IRRemote, но она не работает с Arduino NG — не хватает памяти. К тому же, мне не удалось заставить эту библиотеку понимать мой пульт даже на Arduino UNO. Как я писал выше, LIRC (и WinLIRC) не очень хорошо работали с этим пультом — часто пропускали нажатия кнопок, иногда неверно определяли нажатую клавишу. Сам пульт работал исправно — с «родным» DVD-плейером проблем не возникало. Поиски информации о протоколе для этого пульта ничего не дали, так что я решил разобраться с ним самостоятельно.

Для этого мне понадобился осциллограф, но его под руками не оказалось, да и взять было негде. Но в случае с сигналами ПДУ вполне подходит линейный вход звуковой карты. К нему я подключил инфракрасный приёмник TSOP 31236.

image

Далее с помощью любого аудиоредактора можно записать входной сигнал и проанализировать:

image

Сначала идет длинная посылка, необходимая для установки уровня автоматической регулировки усиления в приёмнике. Затем следуют импульсы различной длины. Я написав промежуточный скетч для Arduino, который определял время между фронтами импульсов в микросекундах и выводил их в терминал через RS232. Полученные данные я загрузил в Excel:

image

Когда я только начал изучать протокол, моей главной ошибкой было то, что я считал длительности самих импульсов, но игнорировал длительность интервалов между ними. После того, как я начал считать интервалы между фронтами все встало на свои места. Как видно, большинство импульсов интервалов между фронтами имеют длительности в пределах 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. Настройка того и другого индивидуальна и в то же время проста, поэтому не заслуживает внимания.

image

Результат

Устройство работает уже несколько месяцев. Пользуется им в основном мой отец, далекий от техники, поэтому минимально необходимое количество кнопок на пульте является достаточно удобным обстоятельством. Питание включается и отключается, ошибочных срабатываний не бывает.

ссылка на оригинал статьи http://habrahabr.ru/post/204906/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *