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

в 11:06, , рубрики: arduino, ATmega8, Eventghost, HTPC, lirc, XBMC, Программинг микроконтроллеров, метки: , , , , ,

При создании 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

Результат

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

Автор: k0ldbl00d

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js