Простой универсальный декодер ИК ДУ


Использование ИК ДУ пультов от бытовой техники достаточно популярно для управления различными самодельными устройствами от различных умных выключателей света, систем умного дома и до игрушек и роботов. Существует большое разнообразие протоколов ИК ДУ пультов и способов кодирования сигнала. Предлагаю один очень простой способ обработки ИК сигналов, работающий с большинством распространённых протоколов.

В сети опубликовано большое количество различных программных декодеров для приема команд с пультов ДУ, большинство заточены под какой-то определенный протокол, например, RC-5, RC-6, Sony, Nec и т.д., но есть и универсальные. Со специализированными декодерами всё понятно, они хорошо работают (только) со своими пультами, но по этой-же причине их применение ограниченно. Универсальные алгоритмы, как правило, либо сравнивают длительности импульсов и пауз между ними по таблице, либо производят выборку по таблице контрольных точек. Это требует относительно большого объёма памяти для хранения кодов кнопок.
А нужно-ли для уверенного детектирования определенной команды ее в точности разбирать по определенному протоколу, или точно сопоставлять длительности всех импульсов/пауз? В общем-то не нужно. Минимальная длительность импульсов ограниченна используемой несущей частотой. Например, для ИК приёмников серий TSOP17xx (где xx — шесущая частота КГц) минимально детектируемый импульс — 15 периодов тактовой частоты ~0.42 мс. Минимальная длительность импульсов в посылках пультов примерно вдвое больше и составляет около 0.85 мс. Увеличивать длительность импульсов больше этой величины не имеет смысла, так как на отправку команды будет уходить слишком много времени. Тоже самое касается пауз между импульсами. На практике, разница между длинами импульсов разных пультов ДУ меньше чем в в два раза, это позволяет отказаться от точного измерения длительности импульсов и пауз и характеризовать их только как «короткие» и «длинные» (стартовые импульсы нас не интересуют). То есть нам нужно запоминать один бит на фронт и спад.

Демонстрационное устройство

Схема демонстрационного устройства очень проста — контроллер AtTiny26 с минимальной обвязкой, приёмник TSOP1736 и знакосинтезирующий дисплей. Оно принимает от пулта и отображает полученный код на ЖК дисплее. На диодах vd1, vd2 и конденсаторах С1, С2 собрана схема накачки заряда (charge pump) для получения отрицательного напряжения контраста ЖК дисплея — мне попался экземпляр для работы при низких температурах, и ему нужно где-то минус 1.5 вольта на выводе CONTR относительно земли.

Для замера длительностей импульсов используется таймер и внешнее прерывание, но стем-же успехом можно использовать таймер с модулем захвата.


#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>
#include "lcd.h"

// пороговое значение для сравнения длинн импульсов и пауз
static const IrPulseThershold = 9;// 1024/8000 * 9 = 1.152 msec
// определяет таймаут приема посылки 
// и ограничивает максимальную длину импульса и паузы
static const uint8_t TimerReloadValue = 150;
static const uint8_t TimerClock = (1 << CS02) | (1 << CS00);// 8 MHz / 1024

volatile struct ir_t
{
	// флаг начала приема полылки
	uint8_t rx_started;
	// принятый код
	uint32_t code, 
	// буфер приёма
rx_buffer;
} ir;

static void ir_start_timer()
{
	TIMSK = (1 << TOIE0);
	TCNT0 = 0;
	TCCR0 = TimerClock;
}

// когда таймер переполнится, считаем, что посылка принята
// копируем принятый код из буфера
// сбрасываем флаги и останавливаем таймер
ISR(TIMER0_OVF0_vect)
{
	ir.code = ir.rx_buffer;
	ir.rx_buffer = 0;
	ir.rx_started = 0;
	if(ir.code == 0)
		TCCR0 = 0;
	TCNT0 = TimerReloadValue;
}

// внешнее прерывание по фронту и спаду 
ISR(INT0_vect)
{
	uint8_t delta;
	if(ir.rx_started)
	{
		// если длительность импульса/паузы больше пороговой 
		// сдвигаем в буфер единицу иначе ноль.
		delta = TCNT0 - TimerReloadValue;
		ir.rx_buffer <<= 1;
		if(delta > IrPulseThershold) ir.rx_buffer |= 1;
	}
	else{
		ir.rx_started = 1;
	ir_start_timer();
	}
	TCNT0 = TimerReloadValue;
}

static inline void ir_init()
{
	GIMSK |= _BV(INT0);
	MCUCR |= (1 << ISC00) | (0 <<ISC01);
	ir_start_timer();
}

char buf[10];

int main()
{
	PORTA|=_BV(0) | _BV(1);
	DDRA=0; 
	DDRB|=_BV(PB3) | _BV(PB5);
	PORTB|=_BV(PB5);

	ir_init();
	lcd_init();
	sei();
	lcd_puts("IR Lcd");

	for(;;)
	{
	// если ir.code не ноль, значит мы приняли новую комманду
	// ir.code будет сохранять свое значение до следующего переполнения таймера
		if(ir.code)
		{
			// конвертируем код в строку и выводим на дисплей
			ultoa(ir.code, buf, 16);
			lcd_clear();
			lcd_puts((buf));
		}
		// дёргаем ногу, на которой у нас зарядовая подкачка для контраста дисплея
		PORTB^=_BV(PB3);
	}
}


Декодер предельно прост, компактен и эффективен, но тем не менее работает с большинством протоколов ИК пультов.

Исходник к статье

  • +7
  • 17 марта 2011, 19:43
  • neiver

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

RSS свернуть / развернуть
Интересно.
Моя система с контрольными точками действительно проигрывает этому алгоритму :)
0
Любопытно. А не в курсе, какой код использует фирма Philips?
0
  • avatar
  • Vga
  • 17 марта 2011, 20:32
RC5 вроде-бы… (манчестерский код)
0
О и Neiver влез в лодку. Атлична атлична =)
0
О! полезный девайс. Тока «Исходник к статье» поправь, файл не качается.
0
  • avatar
  • BenG
  • 17 марта 2011, 22:08
У меня всё работает.
0
классно. а я запаривался
0
А разве не вывод R/W дисплея должен идти на землю, вместо RS?
0
  • avatar
  • qic
  • 18 марта 2011, 12:16
Конечно. По запарке не туда прилипил. Спасибо.
0
Хм… решил попробовать этот способ… и чёт не так работает как надо… взял пульт с RC-5 и потыкал кнопки 1-9, и в коде сделал маску ir.code &= 0x3F; чтобы только код кнопки получить в итоге какие то непонятные цифры лезут…
0
И у меня кстати тоже. Правда, я перенес код под атмегу в пинборде. Для этого GIMSK на GIFR поменял. И приемник переставил на PD2/INT0.
Оно еще и с дисплеем криво работает кстати. После lcd_clear дисплей начинает глючить, вроде из-за отсутствия увеличенной задержки после команды 0х01.
0
что за непонятные цифры? Они уникальны для каждой нажатой кнопки? Этот алгоритм не декодирует какой-то протокол непосредственно, он вычисляет некую свертку(хеш) от ИК посылки, который однозначно иднтифицирует нажатую кнопку.
Чтобы определить нажата-ли нужная кнопка, нужно запомнить какое значение ir.code ей соответствует и потом с ним сравнивать.
ir.code &= 0x3F — так желать не нужно, это сокращает объем хеша до 64 значений, разным кнопкам даже на одном пульте при таком раскладе могут(будут) соответствовать один код. Нужно сравнивать с полным кодом.
Пример, управление простым роботом:
switch(ir.code)
{
	case 0: 	stop();	
		break;
	case 0x06f9: fwd();		
		break; // wfd
	case 0xa659: left(); 
		break; //left
	case 0x46b9: right();
		break; //right
	case 0x8679: back();
		break; // back
	case 0x16e9: left();
		break; //vol+
	case 0xb44b: right();
		break; //vol-
}
0
В том то и проблема, что код нестабилен и прыгает от 24 до 8808a808 (не совсем так, но типа того). Пульт — универсальный филипс, посылка (первая, по крайней мере) — 14-битовый манчестер. Вторую не анализировал, 4 Кт памяти DSO201 хватило на захват только начала второй посылки, а ригол было неудобно тащить с собой в сортир).
Параметры: несущая манчестера 595 Гц, период посылок 107 мс, несущая пульта 38 кГц. Правда, вместо TSOP'а (который я невовремя сжег) я юзал шарповский приемник-микросборку из старого ТВ, но по докам он такой же.
0
значит нужно IrPulseThershold подкорректировать. Увеличить где-нибудь до 10-12 для 8 МГц. Ну и вообще поэкспериментировать с этим значением.
Хотя у меня пультом от филипса с такими-же параметрами эта штука работала нормально.
Кстати, на какой частоте МК работает?
0
Int RC 8 MHz. Стандартно для пинборда. МК ATmega16A
0
Вы бы не могли помочь реализовать захват длительного нажатия в коде, если например код кнопки 0x8aaa2?
Застрял, не знаю как дальше улавливать код повтора

switch(ir.code)
{
   case 0x8aaa2:
  
   break;
}
0
Переделал под Input Capture. Работает неплохо, но надо допиливать для специфичных случаев. Для NEC, например, код повтора отдельный, для RC5 инверсия бита и т.д. Но как база хорошее решение. Спасибо.
0
Из мыслей по теме можно, наверное, 32 бита сверткой до 8 сократить, например CRC8. Буду проверять.
0
До восми — ничего хорошего не получится — коллизий много будет. До 16 можно попробывать, надо только подходящую хеш функцию подобрать.
0
день убил а значения всегда разные на 4х разных пультах проверял, минимум 2 протокола
с временем всё перепробоал
использовал cpp захват
0
Скорее всего это всё-таки что-то с таймингами. С какой частотой таймер тикает, какое значение IrPulseThershold?
У меня только с пультами от Philips некоторые затыки были — у них период бит почти в два раза больше остальных пультов, которые мне попадались. Вобщем, нужно посчитать, а лучше измерить частоту таймера, измерить длительности самого короткого и самого длинного импусльсов в посылках пультов и высчитать значение IrPulseThershold, чтоб этот период был примерно посередине между самым коротким и самым длинным импусльсами посылки.
0
1 тик 16битного таймера 0,5мкс (это максимум, проц на 64мгц крутится), камень pic18f25k20

подставлял разные значения от 200, 500, 1000, 2000 ну и до 40000, работает примерно одинакого, при каждом нажатии кнопки разный код

фотоприёмник tsop34836

завтра осликом посмотрю что с приёмника идёт(
0
пульт с RC5, и пульт SONY от магнитолы тестировал
0
а можно github.com/KonstantinChizhov/AvrProjects/raw/master/samples/ir_lcd.zip ещё куданибуль залить
0
  • avatar
  • sIP
  • 23 января 2012, 09:58
Можно. А куда?
0
скачивается без проблем от другого провайдера :)
0
я совсем недавно разбираюсь с AVR, до си ещё не добрался
мне нужно на ATTiny2313A-PU принимать сигналы от IR (PD0/RTX) и по окончании в регистрах или в стеке записать первых четыре байта
помогите плиз
0
  • avatar
  • sIP
  • 17 февраля 2012, 15:27
А как бы код переделать под кварц в 16МГц
0
Нужно пересчитать IrPulseThershold — это пороговый период в тиках таймера, должен быть чуть побольше миллисекунды. Для 16 МГц будет примерно вдвое больше, можно попробовать значения 16 — 20. И TimerReloadValue нужо увеличить, скажем, до 250.
0
А зачем в программе происходит конфигурирование не используемых по схеме пинов ???
Вот
PORTA|=_BV(0) | _BV(1); // PA0, PA1 подтянули к VCC, хотя по схеме не используются

DDRB|=_BV(PB3) | _BV(PB5); // PB5 сказали что направление OUT, хотя по схеме не используется
PORTB|=_BV(PB5); // PB5 выставил логическую 1 на порте
0
Видимо для отладки использовались, забыл удалить.
0
Вот моя либа: codetidy.com/3996/, codetidy.com/3997/
Работает методом программного опроса, без использования захвата таймером. Пока реализованы протоколы Sony/SIRC, NEC/Samsung. Настройка приема — заданием модели пульта (REMOTE_MODEL) или протокола REMOTE_PROTOCOL, битов в посылке REMOTE_BITS, и длительностей отдельных импульсов.

Для настройки нужно определить несколько констант, и функцию/макрос чтения пина ИК приемника:
#define REMOTE_F             20000 //частота, Гц
#define REMOTE_MODEL         REMOTE_MODEL_KWORLD_TV878RF //модель пульта
#define REMOTE_CNT_MAX       65535 //макс.значение счетчика состояния
#define REMOTE_CNT_TOLERANCE 30 //допуск длительности импульса, %
#define REMOTE_RELEASE_DELAY 200 //задержка отпускания кнопки, мс

//чтение выхода приемника
U8 remote_scan(void) {
	return *BITP(&GPIOA->IDR, 14);
}

Для работы нужно вызывать функцию remote_update() с заданной частотой, и проверять состояние флага remote.ready. Если 1 — код находится в приемном буфере. Чтение — ф-я remote_code() (попутно очищает флаг готовности и обнуляет приемный буфер).
0
Помогите, пожалуйста, до конца не понял алгоритм. Т.е. не понял временные составляющие.
1.152 мс — это критическая пауза между изменениями уровня на приемнике. Если время между изменениями меньше этого порога, то добавляем в код 1, если больше — то 0.
А какой все же лимит (в мс), обозначающий конец пакета? 19.2 мс?
0
  • avatar
  • Urvin
  • 28 февраля 2014, 12:40
Именно так. В протоколах некоторых пультов есть в начале пакета длинные импульсы/паузы, типа таких. Чтоб их не принять за конец пакета длительность таймаута длжна быть примерно в 2 раба больше.
0
Спасибо! Буду стараться воплощать.
0
В первую очередь огромное спасибо за статью.
Извините за возможно глупый вопрос, просто я новичек, мне необходимо чтобы при длительном нажатии производилось какое то действие(как например при регулировании громкости), а не получается…
0
Тут есть ньюансы. Одни пульты при длительном нажатии посылают повторно код кнопки, а другие специальную короткую посылку повтора. Это надо учесть при обработки повторов.
А так ничего сложного: по таймеру (можно програмному) периодически опрашиваем принятый код, если он такой-же как при прошлом опросе, значит повтор.
0
а эта короткая посылка повтора индивидуальна для каждой кнопки или общая для пульта? К чему я задаю этот вопрос, просто при длительном нажатии на какую либо кнопку — на LCD сперва мелькнет код кнопки, а потом стабильно мерцает цифра «6» и такая цифра появляется при длительном нажатии любой из кнопок, может это и есть посылка повтора?
0
Общая для пульта, там порой буквально пара бит передается.
0
Универсальный пульт для stm8s, может кому пригодится

//структура
// пороговое значение для сравнения длинн импульсов и пауз
static const u8 Ir_Thershold = 11;// = 1.1 msec
// определяет таймаут приема посылки 
// и ограничивает максимальную длину импульса и паузы
static const u8 IR_Timer_Stop = 40; // 4ms

volatile struct ir_t {
        bool rx_started;
        u8 pulse_width;
        uint32_t code, rx_buffer;
} ir;


//прерывание от таймера 0.1ms
INTERRUPT_HANDLER(TIM4_OVF_IRQHandler, 23) //0,1ms
{
  if (++ir.pulse_width>IR_Timer_Stop) {
    ir.code        = ir.rx_buffer;
    ir.rx_buffer   = 0;
    ir.rx_started  = 0;
    ir.pulse_width = 0;
    TIM4->CR1 &= (uint8_t)(~TIM4_CR1_CEN);
  }
    /* Clear the IT pending Bit */
    TIM4->SR1 = (uint8_t)(~TIM4_IT_UPDATE);
}

//от ноги приемника
 INTERRUPT_HANDLER(ITC_IRQ_PORTAHandler, 3) //PA1 Interrupt
{
  //uint8_t delta;
  GPIO_WriteReverse(GPIOC,GPIO_PIN_3);
  if(ir.rx_started)
  {
          // если длительность импульса/паузы больше пороговой 
          // сдвигаем в буфер единицу иначе ноль.
          ir.rx_buffer <<= 1;
          if(ir.pulse_width > Ir_Thershold) ir.rx_buffer |= 1;
          ir.pulse_width = 0;
  }
  else{
          ir.rx_started = 1;
          ir.pulse_width = 0;
          TIM4->CR1 |= TIM4_CR1_CEN;
  }
}

//в main обработка принятой команды
while (1) {
  if   (ir.code) {
            // конвертируем код в строку и выводим на дисплей
            usend((uint8_t)(ir.code>>24)); 
            usend((uint8_t)(ir.code>>16));
            usend((uint8_t)(ir.code>>8));
            usend((uint8_t)(ir.code));
            usend((uint8_t)(0x0D)); //CR
            usend((uint8_t)(0x0A)); //LF
    ir.code=0x00;
  } //if ir.code
       
  }//while (1)
}//main

void usend(u8 msg) {
  while(!(UART1->SR & UART1_FLAG_TC));
  UART1_SendData8(msg);
}


исходник тут github.com/isenser/ir_decoder
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.