четверг, 12 сентября 2013 г.

Выключатель света для ленивых

Как то раз, философствуя на тему "лень - двигатель прогресса" мой друг заметил, что с дивана он может управлять любой бытовой техникой стоящей в комнате, кроме лампочки на потолке. А очень хотелось бы, потому что, засыпая под фильм, так лень вставать и идти к выключателю. Я пообещал исправить ситуацию, и вот что получилось:

Аппаратная часть.

Требования к самой железке достаточно простые: минимальные габариты, чтоб можно было встроить в штатное посадочное место для выключателя или в люстру; минимальное энергопотребление; простота конструкции - чем проще, тем надежнее. Схемотехника тоже в стиле минимализма.
Следить за наличием инфракрасных лучей у нас будет фотоприемник серии TSOP348xx - малогабаритный, потребление чуть больше 1 мА. Декодировать посылки назначим МК Tiny13, хотя даже его много, учитывая число доступных лап и количество памяти. Исполнительный узел представлен связкой оптосимистора MOC3063 и симистора BT139, в принципе классическое решение. Питание в данном случае целесообразно сделать по схеме с гасящим конденсатором, руками лазить в устройство не требуется, потому и гальваноразвязка от сети нам не нужна, кроме того, это самый компактный и дешевый вариант. Надо отметить, что при всех очевидных достоинствах такого источника питания, у него есть и серьезный минус - сильно нелинейная зависимость выделяемого тепла от тока нагрузки, посему вопрос энергопотребления устройства ставится особо остро. Оценим потребление основных компонентов: микроконтроллер и фотоприемник требуют суммарно около 5,5 мА, что не так много, а оптосимистор товарищ прожорливый. Из всей  серии MOC306x, 63-й самый экономичный, минимальный ток светодиода для отпирания прибора составляет 5 мА. Не будем жадничать, на всякий случай дадим ему с запасом - 7,5 мА. Итого, около 13 мА. Многовато, но не смертельно, тем более в режиме ожидания оптосимистор ничего не потребляет. Конструктивно все удалось вписать в плату размером 50 * 33 мм, высота по самому высокому компоненту - 30 мм.


Включаем, проверяем. Ничего сильно не греется, не взрывается, не горит :) Напряжение питания низковольтных элементов в норме. Пора переходить к программной части.

Программная часть.

Полдня изучения протоколов передачи по инфракрасному каналу и анализ пакетов, полученых со всех пультов что были дома, привели к выводу: ничего сложного в разборе посылок нет, поэтому есть повод размяться старым добрым языком ассемблера. Также выяснилось, что все доступные мне на тот момент пульты работают по протоколу разработаному фирмой NEC. На всякий случай спросил у Гугла относительно наиболее популярных бытовых протоколов, он подтвердил мои наблюдения - большинство техники использует этот стандарт передачи команд. Значит на него и будем ориентироваться. Несущая частота 38 кГц, отлично, как раз пара TSOP34838 лежало дома без дела. Формат посылки следующий: преамбула из импульса длиной 9 мс и паузы в 4,5 мс, далее 4 байта информации и стоп-бит. Фактически байт информации всего два - адрес устройства и код команды, просто передаются они в прямом и инверсном виде. Каждый передаваемый бит начинается с импульса длиной 0,56 мс, после чего следует пауза, определяющая состояние бита, 1,6 мс - логическая единица, 0,56 мс - логический ноль. Декодировать, разумеется, будем весь пакет с выделением из него адреса и команды. Адрес нас не интересует, а команды как раз нужны. Логика программы будет примерно следующая: при изменении состояния входа МК к которому подключен фотоприемник начинаем квантовать временнЫе интервалы с шагом 0,1 мс, то есть проверяем состояние порта, ждем 0,1 мс, опять проверяем состояние порта, и так до тех пор, пока не изменится его состояние. Количество вызовов подпрограммы задержки и даст нам длину импульса или паузы. Не исключена ситуация возникновения помехи в оптическом канале и, как следствие, сбоя передачи пакета, либо попадется пульт с нестандартным протоколом в котором количество информационных бит будет меньше ожидаемого. Оба варианта приведут к зависанию программы в режиме ожидания смены состояния пина, чего хотелось бы избежать. Делать проверки на недопустимые длины интервалов получится громоздко, да и не нужно, проще натравить сторожевую собаку. Для обеспечения хотя бы минимальной простоты и гибкости использования устройства рядовым пользователем, было бы неплохо сделать приемник самообучаемым. Хозяин пульта понятия не имеет какой код команды передается при нажатии на ту или иную кнопку и, тем более, не сможет этот код занести в память МК, но зато точно знает какая кнопка на пульте не используется и может быть выделена под управление светом. Для этого был введен режим программирования, в котором перемычкой закорачиваются 3 и 4 ноги МК, подается питание, нажимается нужная кнопка на пульте. Все, приемник принял пакет, декодировал и положил в EEPROM. Снимаем питание, убираем перемычку, включаем, радуемся жизни. Перепрограммировать, если верить документации на Tiny13, можно будет 100 000 раз, думаю хватит. :)
Итак, собственно код:
Определяем константы.

//NEC protocol constants
.equ PREAMBULA = 90 // (* 0.1 ms)
.equ PREAMB_PAUSE = 45 // (* 0.1 ms)
.equ LOG_1 = 16 // (* 0.1 ms)
.equ LOG_0 = 6 // (* 0.1 ms)
.equ DETECT_TIMEOUT = 20 // (* 50ms)  запрет повторного приема пакетов

Константа DETECT_TIMEOUT, вероятно, требует пояснения.
В числе прочих попался мне пульт который работает не совсем по стандарту. Вместо передачи одного пакета команды, а затем, если кнопка зажата, символа повтора каждые 110 мс, он передавал пакет дважды и потом молчал, независимо от длительности зажатия кнопки. Учитывая, что устройство работает по принципу триггера: первое нажатие кнопки - свет включен, второе - выключен, то такая сдвоеная посылка нам ломает весь сарай. Пришлось ввести костыль в виде таймаута запрета приема команд.
Определяем рабочие регистры.

.def command = r9  //тут храним принятую команду
.def address = r10   //тут храним принятый адрес
.def byte_counter = r14   //счетчик байт в посылке
.def bit_counter = r15      //счетчик бит в посылке
.def byte = r16                //временное хранилище принятого целого байта
.def tmp1 = r19               //временные регистры 1 и 2
.def tmp2 = r20
.def count = r25             //счетчик в подпрограмме задержки 


Инициализация программы
;PortB(in/out)
.equ led = PORTB2
.equ tsop = PINB3
.equ jmpr = PINB4
.cseg
.org $000
rjmp RESET
.org $010
RESET:
;Init prog
ldi tmp1,low(RamEnd)
out SPL,tmp1
ldi tmp1,0b00000100
out DDRB,tmp1
ldi tmp1,0x14
out PORTB,tmp1
rcall delay_10ms
rcall delay_10ms
rcall delay_10ms
rcall delay_10ms
rcall delay_10ms
clr byte
clr bit_counter
clr byte_counter


Тут проверка состояния перемычки для входа в режим программирования кнопки

sbis PINB,jmpr
rcall programm_button
clt
clr tmp1                     //настраиваем сторожевой таймер
ldi tmp1,0x1E
out WDTCR,tmp1
ldi tmp1,0x0E
out WDTCR,tmp1

Основной цикл программы

main:
sbis PINB,tsop           //проверяем наличие сигнала с фотоприемника
rcall recieve_com        //переходим в подпрограмму приема и декодирования пакета
wdr                             //гладим собаку
brtc main
clt
 rcall eeprom_read       //берем из EEPROM  код команды назначеный на выключатель света
cp command,tmp1       //сравниваем его с принятым от пульта кодом команды
brne m1
sbis PINB,2                 //тут изменяем состояние лампочки на противоположное
rjmp m0
cbi PORTB,led
rjmp m1
m0:
sbi PORTB,led
m1:
ldi tmp2,DETECT_TIMEOUT         //ничего не делаем на время запрета приема команд
m2:
rcall delay_10ms
rcall delay_10ms
rcall delay_10ms
rcall delay_10ms
rcall delay_10ms
dec tmp2
brne m2
rjmp main


Далее, используемые подпрограммы:

Прием команды
//*******************************
recieve_com: //function return 1 in T flag if success
rcall preamb_detect
brts rc_com_0
ret
rc_com_0:
clt
rcall get_bit
inc bit_counter
lsr byte
bld byte,7
mov tmp1,bit_counter
cpi tmp1,8
brne rc_com_0
inc byte_counter
clr bit_counter
mov tmp1,byte_counter
cpi tmp1,1
brne rc_com_2
mov address,byte
rjmp rc_com_3
rc_com_2:
cpi tmp1,3
brne rc_com_3
mov command,byte
rc_com_3:
clr byte
cpi tmp1,4
brne rc_com_0
rcall get_stop_bit
clr byte_counter
set
ret


Задержка 0,1 мс
//*******************************
;-------------------------
;rcall+ret+cbi+inc=3+4+2+1=10 once used
;inc+cpi+brne+nop=1+1+2+1=5 in cycle
;(5*190)+10=960
delay_100us: //100us = 960 cycles
clr count
d100:
nop
inc count
cpi count,190
brne d100
ret


Задержка 10 мс
//******************************
delay_10ms:
clr tmp1
d_10:
rcall delay_100us
inc tmp1
cpi tmp1,100
brne d_10
ret


Определение преамбулы пакета
//******************************
preamb_detect:
clr tmp1
pr_d_0: //wait until signal is low
rcall delay_100us
inc tmp1
sbis PINB,tsop
rjmp pr_d_0
cpi tmp1,PREAMBULA-2 //check preambula pulse lenght
brlo pr_d_ex //less than preambula length. Exit
cpi tmp1,PREAMBULA+2
brge pr_d_ex //more than preambula length. Exit
clr tmp1
pr_d_3: //wait until preambula pause
rcall delay_100us
inc tmp1
sbic PINB,tsop
rjmp pr_d_3
cpi tmp1,PREAMB_PAUSE-1
brge pr_d_5
rjmp pr_d_ex
pr_d_5:
set //flag T is set if preambula and pause is valid
ret
pr_d_ex:
nop
sbis PINB,tsop //wait until signal low then exit with error
rjmp pr_d_ex
ret


Прием одного информационного бита
//*****************************
get_bit:
nop
sbis PINB,tsop
rjmp get_bit
clr tmp1
g_b_0:
rcall delay_100us
inc tmp1
sbic PINB,tsop
rjmp g_b_0
cpi tmp1,LOG_0+2
brge g_b_1
ret
g_b_1:
set
ret


Ожидание стоп-бита
//*****************************
get_stop_bit:
nop
sbis PINB,tsop
rjmp get_stop_bit
ret


Программирование кнопки пульта
//*****************************
programm_button:
sbis PINB,tsop
rcall recieve_com
nop
brtc programm_button
ldi tmp1,(0< out EECR,tmp1
ldi tmp1,EEPROM_ADR
out EEARL,tmp1
out EEDR,command
sbi EECR,EEMPE
sbi EECR,EEPE
ret


Чтение из EEPROM записаной команды
//*******************************
eeprom_read:
sbic EECR,EEPE
rjmp eeprom_read
ldi tmp1,EEPROM_ADR
out EEARL,tmp1
sbi EECR,EERE
in tmp1,EEDR
ret


Отладочный стенд :)

Программа получилась размером в 145 слов, или 290 байт, то есть еще осталось куча свободного места в памяти.
Для полного счастья может быть добавлю поддержку менее распространенного протокола RC5/RC6, чтобы можно было работать с еще большим количеством пультов. 
Залил прошивку, включил, заработало. Команды принимает уверенно, ложных срабатываний не возникало. При длительно включеной 100-ваттной лампочке симистор ощутимо нагрелся, что при такой нагрузке еще не критично, но для более мощных потребителей нужно будет прикручивать радиатор.

Комментариев нет: