Данный протокол уже много где описан. Я хочу показать и подробно описать свою реализацию на конкретном микроконтроллере. Мне было необходимо принимать сигнал с пульта RGB — такого, как на картинке. Его система команд приведена внизу статьи.

Краткий экскурс


Каждый пакет протокола NEC состоит из стартовой последовательности – импульса длиной 9 мс и паузы длиной 4,5 мс. Дабы не грузить вас теоретическими рисунками, покажу реальные скриншоты с логического анализатора.



Сам протокол основан для кодировании нулей и единиц длиной паузы. Начало каждого бита определяется импульсом длиной 560 мкс (одновременно этот импульс сигнализирует о конце предыдущего бита). Длина следующей за импульсом паузы определяет логическое значение бита. Так, суммарная длина логического нуля получается 1.12мс, а логической единицы 2.25мс. Соответственно в реальной ситуации значения немного разнятся.




Пакет состоит из стартовой последовательности, адреса и команды. В стандартной версии протокола пакет всегда имеет фиксированную длительность, так как адрес и команда передаются как в прямом, так и в инверсном виде.



При удержании кнопки посылка повторно не передаётся. Вместо этого каждые 110 мс передаётся специальный код повтора длительностью 11,25 мс.



Программа


Я не принимал адрес, так как он мне не нужен. В случае необходимости, вы легко сможете доработать программу. В качестве микроконтроллера была выбрана ATmega32 с 16МГц кварцем. Следовательно, все временные интервалы рассчитаны для 16Мгц. Для реализации протокола нам понадобится таймер для отсчета времени и внешнее прерывание по ниспадающему фронту. Таймер настроен с делителем 1024, один такт 1024/16МГц = 64мкс, прерывание по переполнению 64мкс * 256 = 16мс (что заведомо больше любого из битов в пакете, это нам пригодится).
Начальная инициализация и макросы старт/стоп таймера выглядят так:

#define StopT0	TIMSK &= ~(1 << TOIE0); //макрос для старта таймера
#define StartT0	TIMSK |= (1 << TOIE0);	//макрос стоп таймера
  
SREG|= (1<<7); //Global Interrupt Enable
GICR|= (1<<INT0); //Разрешаем прерывание по INT0   
MCUCR|=(1<<ISC01)|(0<<ISC00); //по ниспадающему фронту
  
TCCR0|=(1<<CS02)|(1<<CS00); //Делитель 1024, один такт 64мкс, переполнение 16,38 мс
asm("sei");  //Разрешаем все прерывания


Код написан в среде IAR, но легко переносится в другую среду, путем замены заголовков прерываний.
Прерывание по переполнению таймера нужно только для “завершения” приема. Переполнение в 16мс больше любой составляющей пакета, будь то преамбула или бит, так что такое прерывание можно считать окончанием приема пакета и подготовиться к приему следующего.

Здесь у меня возник один нюанс, объяснение которого я не знаю. При пуске таймера сразу (само собой после обработки прерывания INT0) срабатывало прерывание по переполнению таймера. Как? Зачем? Возможно, это был какой-то индивидуальный косяк, ибо общая программа на тот момент уже была не маленькая, но я решил первое прерывание не обрабатывать, а обрабатывать второе, т.е уже после 32мс.

#pragma vector=TIMER0_OVF_vect
__interrupt void TIMER0_interrupt (void) //переполнение Т0 16мс, что больше любого из кусков пакета
{
if (firstT ==0) { firstT = 1; }  
/* Первое прерывание возникает при его запуске, оно нам не надо. 
Даже если не возникает, временной интервал просто увеличится до 32мс. */
else { 
	startC = 0; //ждем новой команды
	firstT = 0; //для следующего "первого" запуска
	StopT0; //стоп таймер
}
}

Прерывание по ниспадающему фронту INT0. Тут считаем биты и анализируем время, пройденное с последнего прерывания. По величине таймера TCNT0 легко понять, какой это был бит. В конце обработчика таймер обнуляем, чтобы начать отсчет сначала.

#pragma vector=INT0_vect
__interrupt void INT0_interrupt (void)
{
if (startC == 0) { //первое прерывание
newC = 0; //флаг новой команды
startC = 1; //начали принимать
StartT0; //старт Т0
} else {  		//один отсчет таймера равен 64мкс, все значения проверяеются на неком временном диапазоне

			if(TCNT0>0xD2 & TCNT0<0xFF){ //13,5 мс СТАРТ бит (диапазон 13,4 ... 16,3 мс)
				i = 32; //количество бит ожидаемой посылки
				}

			if(TCNT0>0x07 & TCNT0<0x16){ //1,12мс НОЛЬ (диапазон 0,45 ... 1,41 мс)
				if ((i>0) & (i<9)) Command1 &= ~(1<<(i-1)); //запись бита в прямую 
				if ((i>8) & (i<17)) Command |= (1<<(i-9)); //и инверсную команды
				i--; 
				}
				
			if(TCNT0>0x19 & TCNT0<0x28){ //2,25мс ЕДИНИЦА (диапазон 1,60 ... 2,56 мс)
				if ((i>0) & (i<9)) Command1 |= (1<<(i-1)); //запись бита в прямую
				if ((i>8) & (i<17)) Command &= ~(1<<(i-9)); //и инверсную команды
				i--;
				}

			if(TCNT0>0xA9 & TCNT0<0xB8){ //11,25мс повтор команды (диапазон 10,8 ... 11,8 мс)
				newC = 1; //повтор команды
				}
				
			if (i==0) { //все биты приняты
				StopT0; //стоп таймер
				newC = 1; //это новая команда
				startC = 0; //ждем новой команды
				firstT = 0;	//для следующего "первого" запуска таймера			
            }
}
TCNT0 = 0; //обнуляем счетчик
}

В основной программе анализируем флаг newC, не забыв обнулить его. Ну и дальнейшая обработка команды.

while(1)
    {
	if (newC) { //есть команда?
			newC = 0;	//теперь нет
            if (Command == Command1) { //равны ли прямая и инверсная?
			switch (Command) { //Обработка команд
                case 0x5F: { } //Up
                case 0xDF: { } //Down
				//...........
				}
			}
			}
	}

Система команд


А вот и сама система команд для пульта ED618 (покупаю их на dx.com):



Для других таких же RGB пультов система команд может быть другая. У меня есть точно такой же пульт, достался мне от какого-то покупного контроллера RGB, так там система команд весьма отличается. Считывайте и смотрите сами. Я, например, принимал команду и скидывал ее по UART'у на комп.

Алгоритм сам по себе не сложный, надеюсь кому-то пригодится. У меня уже сделано не одно устройство на основе пульта с таким протоколом, все работает хорошо. В следующей статье расскажу про реализацию протокола светодиодов ws2812b.

Комментарии (18)


  1. switlle
    30.04.2015 12:30

    С ИК все довольно просто, а вот как быть с точно таким же RF пультом — пока окончательного решения не нашел.


    1. popsodav
      30.04.2015 16:03

      У меня с таким RF-пультом прекрасно справляется библиотечка RCSwitch.


      1. switlle
        30.04.2015 16:17

        А с помощью чего принимаете в железе?


        1. popsodav
          30.04.2015 17:11

          с помощью самых дешёвых китайских модулей на 433 МГц:


  1. AngelOfSnow
    30.04.2015 13:06

    Здесь у меня возник один нюанс, объяснение которого я не знаю. При пуске таймера сразу (само собой после обработки прерывания INT0) срабатывало прерывание по переполнению таймера.


    а вы не забыли перед стартом таймера счетчик его обунлить


    1. oWart Автор
      30.04.2015 13:09

      Не забыл, пробовал обнулять


  1. g1t5
    30.04.2015 15:47

    А чем вас IRremote не устроил? Себе недавно на нём «пульт» для SOLO 7c восстановил.
    Хотя да у вас не arduino.


    1. oWart Автор
      30.04.2015 15:54

      Далеко не arduino))


      1. g1t5
        30.04.2015 16:32

        Ну не так уж и далеко, всё таже ATmega. А почему тогда не ШИМ как в том же IRremote?


        1. oWart Автор
          30.04.2015 16:38

          Не представляю, что за IRremote и про какой ШИМ речь



  1. Gordon01
    30.04.2015 18:18

    > Здесь у меня возник один нюанс, объяснение которого я не знаю. При пуске таймера сразу (само собой после обработки прерывания INT0) срабатывало прерывание по переполнению таймера. Как? Зачем?

    У меня было точно так же, только на 16 атмеге. Счетчик точно обнуляется, но с первым же импульсом приходит и прерывание. В эмуляторе так же. Что за фигня?


    1. oWart Автор
      30.04.2015 22:04

      Ответа пока не нашел.


      1. arkamax
        30.04.2015 22:17
        +1

        Не пробовали смотреть значение счетчика сразу после первого прерывания по переполнению? Atmel Studio показывает все регистры контроллера, включая счетные. Если значение 0, то вот выдержка из даташита:

        «In normal operation the Timer/Counter Overflow Flag (TOV0) will be set in the same timer clock cycle as the TCNT0 becomes zero.»

        Если этому верить, то именно TCNT0 == 0 приводит к возникновению прерывания переполнения, а не переход TOP -> 0. Я на такое не натыкался, т.к. обычно использую COMP_interrupt (сравнение). Если хотите работать с переполнением, попробуйте поставить TCNT0 в 1 и посмотреть, сохранится ли это «лишнее» прерывание.


        1. oWart Автор
          01.05.2015 09:47

          Я думал об этом и, кажется, пробовал задавать начальное значение, уже не помню. Попробую еще раз проверить.


  1. icoz
    01.05.2015 12:18

    А код где-нибудь на гитхабе выложен?


    1. oWart Автор
      01.05.2015 22:51

      к сожалению нет


  1. Phizio
    19.05.2015 11:10

    Хорошая статья) как-то адаптировал промышленные диодные часы под такие пульты, правда не RF а ИК-шные.
    У всех подобных пультов один большой плюс: легко самоклейку наклеить со своим рисунком кнопок ;)
    Но был один нюанс — от партии к партии первая буква HEX-кода команд «плавала»… Может мой поставщик производителей разных возил, не знаю. Я если честно не заморачивался — брал просто RGB-контроллеры, использовал только пульт (китайцы давали почти одинаковые цены на контроллер в сборе и пульт отдельно, видимо не понимали логики брать только пульт и считали такой заказ на тот момент эксклюзивным, не знаю...).
    Кстати, может кому нужны «тушки» контроллеров — штук 20 лежит, за шоколадку могу выслать. Выкидывать жалко. Там мозг и полевики как-никак ))))