Печать

Инфракрасное дистанционное управление - Процедура обработки прерывания

Опубликовано . Опубликовано в Справочник

Рейтинг:   / 8
ПлохоОтлично 

Процедура обработки прерывания выглядит следующим образом:

void interrupt_ext(void)
{
Dir = Int_pin; // читаем состояние входа Dir=1 после переднего, Dir=0 после заднего фронта
PTime = Timer; // читаем содержимое таймера, таймер считает от 0 вверх
ReloadTimer(); // перезапускаем таймер
               // здесь выполняем остальные действия
}

В других - прерывание вызывается только по одному фронту, полярность которого определяется специальным битом внутри MCU. В этом случае - мы точно знаем по какому фронту пришло прерывание, но должны проинвертировать этот бит для приема следующего фронта другого направления:

void interrupt_ext(void)
{
Dir = Int_Direction; // читаем состояние бита направления фронта
Int_DirectionA=1;    // следующее прерывание будет по противоположному фронту
PTime = Timer;       // читаем содержимое таймера, таймер считает от 0 вверх
ReloadTimer();       // перезапускаем таймер
                     // здесь выполняем остальные действия
}

Далее я буду ссылаться именно на второй способ.

Если микроконтроллеру больше делать нечего, кроме как принимать и декодировать код - можно конечно и не использовать ни прерывание, ни таймер, если микроконтроллер будет достаточно быстрым, чтобы время выполнения следующего цикла не приводило к существенным погрешностям:

Pin_was = RC_PIN; // запоминаем текущее состояние фотоприемника

While(Pin_was == RC_PIN)
{                 // ожидаем изменения состоянии фотоприемника
Timer++;          // при этом увеличиваем программный таймер (счетчик) на единицу
}

PTime = Timer;    // после изменения состояния - запоминаем счетчик

Однако, если прием и декодирование ИКДУ не является единственной задачей MCU - лучше все же использовать прерывание и аппаратный таймер. Как правило, при переполнении таймера может быть также сгенерировано прерывание - его удобно использовать для выхода из всевозможных ошибок. Например посередине посылки что-то случилось в пультом, или кто-то заслонил собою его луч. MCU может долго ждать следующего импульса (причем нужной полярности!), прежде чем он зафиксирует ошибку. Если же выбрать период таймера (т.е. время, за которое он считает от нуля до своего максимума) чуть больше максимально возможного периода импульсов, то при нормальной работе прерывания от таймера возникать не должно. Если же оно возникло - значит был сбой, и надо переинициализировать всю систему и ждать следующей посылки.

void interrupt_timer(void)
{
Start_IRC();
}

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

enum stIR {stIRCWait, stIRCSyncStart, stIRCSyncEnd, stIRCDatal, stIRCDataO, stIRCDataEnd};

Первое состояние stIRCWait соответствует ожиданию, точнее концу ожидания - приход первого фронта. Начало преамбулы. При этом нам еще не надо запоминать никаких таймеров, наоборот -нам нужно его запустить, и сказать программе что следующее состояние stIRCSyncStart будет концом первого импульса.

При возникновении прерывания в этом состоянии, нужно уже запомнить таймер - это будет длительность начала импульса преамбулы (S1), перезапустить таймер, и перейти в состояние stIRCSyncStart. С приходом сигнала прерывания в этом состоянии, значение таймера будет отражать длительность паузы преамбулы (S0). Нужно снова перезапустить таймер и перейти к состоянию определения информационных бит. В том случае, если преамбула правильная. Здесь может быть несколько вариантов - или мы сравниваем длительности S1 и S0 с заранее
известными константами, зависящими от того, по какому протоколу мы работаем, или же мы последовательно сравниваем со всеми вариантами, тем самым определяя сам протокол. Реально это выглядит так:

// процедура инициализации приема ИКДУ

void Start_IRC(void)
{
Int_Direction = 0; // ожидаем перехода из 1 в 0
Disable_Timer(); // запрещаем работу таймера
IR_State = stIRCWait; // устанавливаем начальное состояние стейт-машины
Enable_Ext_Interrupt(); // разрешаем прерывание от фотоприемника
}

// процедура обработки таймерного прерывания

void interrupt_timer(void)
{                      // если произойдет прерывание от таймера -
Start_IRC();           // то начинаем все с начала
}

// процедура обработки внешнего прерывания

void interrupt_ext(void)
{
Dir = Int_Direction; // читаем состояние бита направления фронта
Int_DirectionA = 1;    // следующее прерывание будет по противоположному фронту

PTime = Timer; // читаем содержимое таймера, таймер считает от 0 вверх
ReloadTimer(); // перезапускаем и стартуем таймер

Switch (IR_State)
{
case stIRCWait:
IR_State = stIRCSyncStart;
Break;

case, stIRCSyncStart:
S1 = PTime;                // длительность импульса преамбулы
IR_State = stIRCSyncEnd;
Break;

case stIRCSyncEnd:
S0 = PTime;                // длительность пазы преамбулы

If ((S1 > S1 min) && (S1 < S1max) && (s0 > S0min) && (s0 < S0max)) // проверяем что это "наша" преамбула
IR_State = stIRCData1;
Else;                   // иначе - начинаем все с начала
Start_IRC();
Break;

case stIRCData1: // здесь делаем прием информационных бит согласно
case stIRCData0: // выбранному протоколу
Break;

case stIRCDataEnd:
IRC_Ready = 1; //
Start_IRC();
Break;
}
}

В примере выше производится сравнение преамбулы с заранее известной преамбулой выбранного протокола. Можно построить "лестницу" проверок, и выделить чему соответствует принятый первый импульс посылки:

Prot = prNone;

if((S1 >= ST1_RC5_MIN)&&(S1 <= ST1_RC5_MAX)&&(S0 >= ST0_RC5_MIN)&&(S0 <= ST0_RC5_MAX))

Prot = prRC5;

else if((S1 >= ST1_SIR_MIN)&&(S1 <= ST1_SIR_MAX)&&(S0 >= ST0_SIR_MIN)&&(S0 <= ST0_SIR_MAX))

Prot = prSIR;

else if((S1 >= ST1_JAP_MIN)&&(S1 <= ST1_JAP_MAX)&&(S0 >= ST0_JAP_MIN)&&(S0 <= ST0_JAP_MAX))

Prot = prJAP;

else if((S1> = ST1_SAM_MIN)&&(S1 <= ST1_SAM_MAX)&&(S0 >= ST0_SAM_MIN)&&(S0 <= ST0_SAM_MAX))

Prot = prSAM;

else if((S1 >= ST1_NEC_MIN)&&(S1 <= ST1_NEC_MAX)&&(S0 >= ST0_NEC_MIN)&&(S0 <= ST0_NEC_MAX))

Prot = prNEC;

Общий алгоритм работы аппаратуры, настроенной на прием сигналов определенного пульта ИКДУ таков:
1. Ожидание начала посылки
2. Прием преамбулы (измерение длительности первого импульса и паузы за ним)
3. Определение "наша" преамбула или чужая. Если чужая - переход к началу.
4. Прием информационных бит согласно "нашему" протоколу.
5. Валидация принятых бит и преобразование принятых бит в более короткую форму, выделение адреса/команды, служебных бит (типа "toggle" для RC5), получение окончательного кода посылки.
6. Сравнение окончательного кода посылки с кодами команд, при соответствии - установи кода команды.

Рассмотрим п.5 - преобразование в более короткую форму. Как описано выше - разные протоколы имеют разное количество информационных бит. Одни используют 12, другие 32, а Панасоник вообще 48. Некоторая информация передается дважды. Но нам интересно знать только адрес и команду. Если устройство рассчитано на работу с одним конкретным протоколо то длину окончательного кода можно выбрать совпадающим с длиной адреса и команды. Если ж протокол заранее неизвестен - нужно выбирать максимально возможную. Некоторые протокол имеют длину не более чем word (два байта), другие укладываются в long (два word или четыре байта), а Panasonic (JAP) передает аж три слова (шесть байтов). Но многобитные протоколы как правило, содержат избыточную информацию, которую можно использовать для валидации. Т.е. проверки правильности приема посылки. Например в протоколе NEC:

Как мы видим, адрес и команды передаются дважды, в прямом и инверсном виде. Значит проверив прямое и инверсное значение адреса и аналогично команды, можно судить о правильности приема. Но для дальнейшей обработки нам не нужны все 4 байта - достаточно двух.
Предположим, мы имеем две переменные - iRI и iResult^ первую записывается "сырой код", т.е принятые биты, во вторую помещается результат

#define unsigned int word
#define unsigned char byte

union
{
unsigned long Input;
word In[2];
byte a[4];
}

iRI;
union
{
word Output;
byte a[2];
}
iResult;

Подобная запись означает, что переменная iRI имеет длину long, и к ней можно обращаться как iRI.Input, как к 4-х байтовой переменной. Вместе с тем, к ней же можно обращаться по ее составляющим - как по словам, соответственно iRI.In[1] для старшего и iRI.In[0] для младшего. Или по отдельным байтам - от iRI.a[3] для старшего байта, до iRI.a[0] для младшего. Это позволяет оперировать как с переменной как одно целое так и по отдельности с ее частями. Если в переменной iRI находится принятая информация по протоколу NEC,to ее валидацию можно произвести следующим образом:

if (( iRI.a[3] == ~iRI.a[2]) && (iRI.a[1 ] == ~iRI.a[0])) valid = 1;
else valid = 0;

// После чего можно записать результат в другую переменную:

if (valid)
{
iResult.a[1] = iRI.a[3]; // адрес
iResult.a[0] = iRI.a[1]; // команда
}
else
iResult.Output = 0;

Надобность в подобной валидации и преобразовании, зависит также от того- в каком виде Вы запоминаете коды команд (с которыми потом сравниваете - см. п. 6). Т.е. сколько байт на каждую команды Вы выделяете. Компромиссным выбором, который пока меня не подводил, является выбор 4-х байт на каждую команду. При этом у 48-битового протокола жертвуем старшим (из 4-х) байтом адреса и можем не делать валидацию при приеме 4-х байтовых посылок, в которых половина или четверть (как у Samsung) являются инверсными - валидация автоматически произойдет при сравнении кодов. С одной стороны это несколько избыточно и расточительно (с т.з. расходования ЕЕПРОМ, в котором обычно хранятся коды команд и времени на сравнение), с другой стороны - наиболее универсально. Сравнение же кодов делается циклическим перебором:

for (i = 0, j = eeCmdAddr, valid = 0; i < MaxCmd; j+ = 4)
if ( (iResult.a[3] == EE_Read(j ))&&(iResult.a[2] == EE_Read(j+1))&&(iResult.a[1] == EE_Read(j+2))&&(iResult.a[0] == EE_Read(j+3)) )
{
valid = 1;
break;
}

Здесь eeCmdAddr - начало области памяти (ЕЕПРОМ), где хранятся коды команд, MaxCmd -число команд. При завершении цикла, переменная valid равна 1, а переменная i - номер команды.

Некоторые протоколы (RC5) имеют отличное средство "toggle" для определения нажатия новой кнопки на пульте ИКДУ (тут "новой" считается не новая вообще, а любая, нажатая после отпускания предыдущей. Это может быть и та же самая кнопка), или специальный признак повтора. Для всех же остальных - это придется делать самим, программно. Запоминая код предыдущей команды и используя программный таймер автоповтора.


Для универсального (т. е. работающего по любому протоколу), это лучше делать одинаково для всех протоколов. Программная реализация автоповтора позволяет задавать различные периоды для разных кнопок, или отменять автоповтор вообще. Например для кнопок увеличения и уменьшения громкости, скорость автоповтора стоит сделать больше, чем для кнопок перебора каналов, для кнопки приглушения громкости (mute) - скорость лучше понизить или выключить вообще автоповтор. Для кнопки включения (Power, Standby, etc.) также лучше выключить автоповтор совсем или сделать его период побольше.

Сводная таблица длительностей преамбул и битов: