IR + USB HID = очередной пульт для компа (часть 1)



Цель данной статьи — продолжение изучения семейства STM32. Поэтому все возражения на тему «а нафига такой мощный проц для такой задачи, я в лихих 90-х делал это на тиньке» или «а в Китае такие по пять рублей пучок» сразу же идут лесом. Тем более, что заглавная картинка взята с ebay, где такой комплект продается за 4-6 баксов.
Статья разделена на три части:
1. Захват и декодирование IR-протокола (таймеры)
2. Создание HID-устройства для выполнения действий на компе без драйверов (USB-HID библиотека)
3. Программа на компе для настройки параметров
4. (опционально, если руки дойдут) Реализация устройства в железе, разводка платы и пайка

Итак, часть первая…


Вводная


Контроллер я буду использовать из линейки STM32F10x, потому что у него есть аппаратный USB. Ну и потому что под рукой есть макетка с STM32F107V.

И да, я использую SPL. :)


По подключению IR-приемника в сети информации чуть больше, чем дофига. Да и сложно запутаться в трех ногах. Я использую TSOP4838 на несущую 38кГц. Напряжение питания у него 5В, входной пин у STM32 5 Volt Tolerant, так что включаю напрямую. Как видно из даташита, у него выход — открытый коллектор с внутренней подтяжкой, поэтому, когда есть несущая, на входе 0 и наоборот.


Выход TSOP подключаю к пину PC6 — это первый канал третьего таймера. Выбор пинов ограничен — нам надо попасть на первый или второй канал таймера, потому что я хочу использовать некоторые необычные режимы захвата.

Протокол IR remote

Все пульты, которые я нашел дома, работают по протоколу NEC, поэтому возьмем его за базовый. Бросим взгляд на этот протокол:
wiki.altium.com/display/ADOH/NEC+Infrared+Transmission+Protocol — первая же ссылка из гугл


Старт-бит: 9ms импульс, 4.5ms пауза
Логическая '0' – 562.5µs импульс, затем 562.5µs пауза, итого 1.125ms
Логическая '1' – 562.5µs импульс, затем 1.6875ms пауза, итого 2.25ms

Передаются 32 бита — 8 бит адрес, потом 8 бит того же адреса в инверсии, 8 бит команды и опять же, инверсия команды. Для помехоустойчивости, вестимо.

Существует еще так называемый расширенный протокол NEC. Поскольку адресов мало, а желающих много, то вместо инверсии адресного байта передается старший байт адреса. То есть всего адресов может быть 65536-256, потому что отличием расширенного протокола от стандартного как раз и является признак несовпадения инвертированного второго байта с первым. Байт команды по-прежнему передается в прямом и инвертированном виде.


Таймеры и захват


Чтобы нам все это захватить с минимальными телодвижениями, обратим внимание на таймеры STM32:

Тактирование счетчика идет по красной линии — от внутреннего генератора через предделитель. Все стандартно.
А вот сигнал от IR-приемника идет по зеленой линии — через фильтр и детектор фронтов сигнал запускает захват сразу двух каналов — первого и второго. Но только первый канал захватывается по заднему фронту (не забываем, что у нас сигнал инвертирован), а второй — по переднему. И одновременно с захватом первого канала (желтая линия) сбрасывается счетчик.
Что при этом произойдет (на примере стартового бита) — по первому фронту произойдет сброс, по истечению 9мс произойдет захват второго канала, после паузы произойдет захват первого канала и опять сброс счетчика. При этом в регистре захвата первого канала будет длина импульса и паузы, а во втором — только импульса.
Такой режим захвата в даташите называется PWM input mode, и действительно, для ШИМ-сигнала он позволяет определить частоту ШИМ (по длине импульса и паузы) и заполнение ШИМ (по отношению длины импульса к длине периода).
Третий канал таймера нужен для того, чтобы отследить таймаут, когда больше сигналов нет и можно декодировать команду
Инициализируем таймер:

TIM_TimeBaseInitTypeDef timer_base;
	  TIM_TimeBaseStructInit(&timer_base);
	  timer_base.TIM_Prescaler = 72 - 1; // частота 72МГц, поэтому один тик таймера - 1мкс
	  TIM_TimeBaseInit(TIM3, &timer_base);

	  TIM_ICInitTypeDef TIM_ICStructure;
	  TIM_ICStructure.TIM_Channel = TIM_Channel_1; // первый канал
	  TIM_ICStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; // по заднему фронту
	  TIM_ICStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // прямо с ножки
	  TIM_ICStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // без делителя
	  TIM_ICStructure.TIM_ICFilter = 0; // без фильтра

	  // эта функция и включает режим PWM input - автоматически настраивает комплементарный канал
	  // правда в стандартной библиотеке работает на 1 и 2 канале, на 3 и 4 - не умеет
	  TIM_PWMIConfig(TIM3, &TIM_ICStructure);

	  /* Выбираем источник для триггера: вход 1 */
	  TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
	  /* По событию от триггера счётчик будет сбрасываться. */
	  TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
	  /* Включаем события от триггера */
	  TIM_SelectMasterSlaveMode(TIM3, TIM_MasterSlaveMode_Enable);

   	  // это третий канал, для таймаута. Таймаут 15мс, поскольку максимальный бит (старт) 13.5мс
	  TIM_OCInitTypeDef TIM_OCStructure;
	  TIM_OCStructure.TIM_OCMode = TIM_OCMode_Timing;
	  TIM_OCStructure.TIM_OutputState = TIM_OutputState_Disable;
	  TIM_OCStructure.TIM_OutputNState = TIM_OutputNState_Disable;
	  TIM_OCStructure.TIM_Pulse = 15000;
	  TIM_OC3Init(TIM3, &TIM_OCStructure);

	  /* Разрешаем таймеру генерировать прерывание по захвату */
	  TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
	  // и по таймауту третьего канала
	  TIM_ITConfig(TIM3, TIM_IT_CC3, ENABLE);

	  TIM_ClearFlag(TIM3, TIM_FLAG_CC1);
	  TIM_ClearFlag(TIM3, TIM_FLAG_CC3);

	  /* Включаем таймер */
	  TIM_Cmd(TIM3, ENABLE);
	  // разрешаем прерывания
	  NVIC_EnableIRQ(TIM3_IRQn);


Настроили таймер, настроили ногу PC6 на вход, не забыли тактирование:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
	  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	  // чтобы подключиться к ноге PC6 надо сделать ремап
	  GPIO_PinRemapConfig(GPIO_FullRemap_TIM3, ENABLE);

	  GPIO_InitTypeDef GPIO_InitStructure;

	  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	  GPIO_Init(GPIOC, &GPIO_InitStructure);


У меня на макетке было проще всего подключиться к ноге PC6, поэтому надо сделать ремап для таймера 3.
По поводу ремапа читаем раздел 8.3 Reference manual'a


По поводу, какой режим устанавливать для ножки, обращаемся в раздел 8.1.11


Теперь, когда у нас происходит захват первого канала, вызывается прерывание. Обработаем его:

void TIM3_IRQHandler() {
	uint16_t cnt1, cnt2;
	if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET) {
		TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update);

		cnt1 = TIM_GetCapture1(TIM3);
		cnt2 = TIM_GetCapture2(TIM3);

		if (recState == REC_Recording) {
			if (captCount < MAX_CAPT_COUNT) {
				captArray[captCount*2] = cnt1; //width
				captArray[captCount*2+1] = cnt2; //pulse
				captCount++;
			}
		}

		if (recState == REC_Idle) {
			//Пришел первый фронт, начинаем запись
			recState = REC_Recording;
			captCount = 0;

		}

	}

	if (TIM_GetITStatus(TIM3, TIM_IT_CC3) != RESET) {
		TIM_ClearITPendingBit(TIM3, TIM_IT_CC3);
		if (recState == REC_Recording) {
			recState = REC_Captured;
		}
	}

}


recState — переменная, которая определяет, не пора ли декодировать захваченную информацию.
Общий смысл — все захваченное складываем в массив captArray, попарно значение периода и ширины импульса. А вот когда все закончилось (произошел таймаут по каналу 3) производим декодирование

Декодирование

Поскольку декодирование отделено от захвата, то в дальнейшем будет легко добавить поддержку других протоколов. Ну а пока код:

/*          ___________       _______
 *         |           |     |
 *         |           |     |
 *  _______|<--Pulse-->|_____|
 *
 *         |<-----Width----->|
 */

#define IR_NO_COMMAND	0

#define IR_NEC_START_PULSE	9000
#define IR_NEC_START_WIDTH	13500

#define IR_NEC_0_PULSE		562
#define IR_NEC_0_WIDTH		1125

#define IR_NEC_1_PULSE		562
#define IR_NEC_1_WIDTH		2250

#define IR_NEC_TOL			20

// этот дефайн просто сравнивает значения с заданной точностью в процентах
#define checkVal(var,val,tol) (var>(val*(100-tol)/100) && var<(val*(100+tol)/100))

uint16_t IR_decodeNEC() {
	uint32_t frame=0;
	uint8_t i;
	uint16_t decoded;

	if (captCount<33) {
		return IR_NO_COMMAND;
	}

	//проверяем на правильный стартовый импульс
	if (!checkVal(captArray[0], IR_NEC_START_WIDTH, IR_NEC_TOL)) {
		return IR_NO_COMMAND;
	}

	if (!checkVal(captArray[1], IR_NEC_START_PULSE, IR_NEC_TOL)) {
		return IR_NO_COMMAND;
	}

	for(i=0; i<32; i++) {
		if (checkVal(captArray[(i+1)*2], IR_NEC_0_WIDTH, IR_NEC_TOL)
				&& checkVal(captArray[(i+1)*2+1], IR_NEC_0_PULSE, IR_NEC_TOL)) {
			frame = frame >> 1;
			continue;
		}

		if (checkVal(captArray[(i+1)*2], IR_NEC_1_WIDTH, IR_NEC_TOL)
				&& checkVal(captArray[(i+1)*2+1], IR_NEC_1_PULSE, IR_NEC_TOL)) {
			frame = (frame >> 1) | 0x80000000;
			continue;
		}

		return IR_NO_COMMAND;
	}

	if ( (frame & 0xff000000) != ( (~(frame) & 0x00ff0000) <<8 )  ) {
		return IR_NO_COMMAND;
	}

	if ( (frame & 0xff00) != ( ((~frame) & 0x00ff) <<8 )  ) {
		return IR_NO_COMMAND;
	}

	decoded = ((frame & 0xff000000) >> 16) | ((frame & 0xff00)>>8);

	return decoded;
}


Как мне кажется — все прозрачно. Проверили, наш старт или нет. Потом пробежались по битам собирая переменную frame. Потом уже внутри нее проверили, совпадает ли команда с ее инверсным представлением или нет, и вывалили наружу декодированное значение. Ну и для поддержки расширенного протокола надо закомментировать сами-знаете-что :) и переходить на 32-битные коды.
Просто? Просто :)

Дальше будет возня с HID-устройством и библиотекой USB

  • +14
  • 24 ноября 2012, 00:53
  • steel_ne

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

RSS свернуть / развернуть
Очень грамотно все описано
-1
if ( (frame & 0xff000000) != ( (~(frame) & 0x00ff0000) <<8 )  ) {
            return IR_NO_COMMAND;
}
с некоторыми пультами это не будет работать, у них Custom code 16-битный, а не 8-битный и его инверсия
0
Да и вычислительно проще сравнить с 16-битным кодом, а эффект будет тот же — отметание битых посылок.
0
STM32 — 32-х битный. Поэтому ему практически пофигу, 16 или 32 бита.
0
Я не про битность, а про то, что там будет просто сравнение двух чисел, без сдвигов и прочих логических операций. Точнее, эту проверку можно будет вообще выкинуть — она автоматически выполнится, если сравнивать поле custom code с кодами кнопок как 16-битное, а не 8-битное.
0
проще сравнивать сразу все 32 бита
0
Ничего не мешает добавить поддержку других пультов, хоть RC5 )
Из тех, что есть у меня — все отработались нормально. Ясное дело, что существуют какие-то vendor specific отклонения. Но это уже в рамках домашнего задания.
0
16 бит адрес это не vendor specific, а официальный расширенный nec протокол
0
Да, действительно. Сейчас добавлю в статью.
0
Отличная статья, все поонятно расписано. Если такая же будет про USB — будет невероятно круто
0
Код повтора NEC не обрабатывается — это очень нехорошо.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.