Предыстория
Мой частный дом отапливается при помощи электрических конвекторов. Всё в них хорошо: и лёгкость монтажа и автоматическое управление температурой и режим день/ночь и режим 50% мощности. Но есть и минус — датчик температуры воздуха закреплён прямо на корпусе конвектора, поэтому нагревается и остывает вместе с ним. Из-за этого конвектор включается/выключается гораздо чаще, чем хотелось бы, невозможно установить желаемую температуру воздуха в комнате, т.к. реальная будет ниже градусов на 5, да и на надёжности постоянные переключения реле сказываются негативно. Можно было, конечно, удлинить датчик температуры и отнести его подальше, но это не наш метод. Т.к. я давно занимаюсь беспроводными технологиями и есть наработки, то решил оснастить конвектор беспроводным датчиком температуры. Это позволит разместить его в любом месте комнаты, не тянуть провода, а если нужно, использовать не один, а несколько датчиков и рассчитывать среднюю по комнате температуру. (Под катом картинки)
Переделка
Как я уже сказал, я плотно занимаюсь беспроводкой и имею по этой теме наработки. Для мониторинга различных датчиков как нельзя лучше подходит ZigBee, а качестве ZigBee микроконтроллера я уже давно использую JN5148 фирмы Jennic (куплена NXP). Для быстрого изготовления макетов я сделал себе несколько модулей с этим микроконтроллером.

Схема модуля (кликабельна):

В схему модуля включен сам микроконтроллер, внешняя память программ для него (обязательный компонент для JN5148), кварц, конденсаторы по линиям питания, ВЧ часть с антенной. Для быстрого старта нужен только разъём программирования и питание 3.3 В. Платки заказывал в seeedstudio, дёшево и сердито. Для того чтобы быстро что-то сваять отлично подходят.
Датчик температуры тоже был сделан з��ранее и ждал своего часа.

Схема датчика (кликабельна):

В качестве измерителя температуры использована микросхема TMP102 фирмы Texas Instruments. Микросхема довольно недорогая, измеряет температуру с точностью 0.5 градуса в диапазоне -25..+85, имеет ток потребления 10 мкА в активном режиме и 1 мкА в спящем, очень компактная, а также работает в диапазоне напряжений питания от 1.4 до 3.6 В, что важно при питании от одной литиевой батарейки. В остальном схема датчика отличается от схемы модуля наличием батарейки, делителя для измерения её напряжения, включателя питания и разъёма для программирования.
Чтобы закончить с железом и перейти собственно к переделке, забегу вперёд и скажу, что сначала я хотел только измерять температуру, передавать его конвектору и каким то образом подсовывать её микроконтроллеру вместо его родного датчика. В последствии появилась идея устанавливать температурный порог так же удаленно, с ПК. Для этого я использовал USB свисток с тем же JN5148.

Схема (тоже кликабельна):

Схема свистка включает в себя схему модуля, рассмотренного выше и USB-UART конвертер на микросхеме FT232R, который одновременно является программатором для микроконтроллера.
Теперь перейдём к реверс-инжинирингу. В качестве подопытного использовался конвектор фирмы Ballu мощностью 1000 Вт с электронной системой управления. Разобрав конвектор, я обнаружил 2 платы: силовую и плату управления.
Силовая плата:

Плата управления:

На силовой плате расположен сетевой источник питания, стабилизатор напряжения +5В на L7805, 2 реле, которые включают либо нагрузку 500Вт (50%) либо 1000Вт (100%) и зуммер. Отдельно расположены термопредохранитель и ионизатор воздуха. На плате управления расположен микроконтроллер, кнопки, а также семисегментный индикатор температуры.
Осмотр показал, что для измерения температуры используется полупроводниковый диод, который, как известно, обладает довольно линейной зависимостью прямого падения напряжения от температуры. Диод включен в верхнее плечо делителя напряжения питания, а напряжение с делителя подаётся на вход АЦП микроконтроллера.

Исходя из этой схемы измерения, самый простой способ эмулировать датчик температуры — это подавать на АЦП конвектора напряжение с ЦАП, который имеется на борту JN5148. Т.к. напряжение питания (и одновременно опорное АЦП) контроллера в конвекторе составляет 5 В, а опорное у ЦАПа — 2.4, необходимо усилить напряжение с ЦАП при помощи операционника примерно в 2 раза. Исходя из этого рисуем схему эмулятора датчика температуры (кликабельна).

Дополнительно к модулю она включает в себя усилитель на операционнике, преобразователь 5 В — 3.3 В для питания JN5148 и разъём программирования. Дальше изготавливаем плату: утюжим, травим, сверлим, лудим, паяем.



Устанавливаем плату на место и начинаем кодить. Кстати, то что плата управления отключается от силовой платы оказалось очень удобно. На неё достаточно подать +5 В и она может работать полностью автономно, поэтому в конвектор я её устанавливал после полной отладки работы системы.
Программирование
Опытным путём я снял зависимость температуры, измеренной конвектором, от напряжения на входе АЦП.

Видно, что в середине диапазона разница между реальной характеристикой и идеальной составляет примерно 1 градус, поэтому я принял решение записать соответствующие коды ЦАП в массив и в зависимости от температуры брать нужный код из массива и отправлять ЦАПу.
В качестве основы для программирования я использовал шаблон от фирмы Jennic — JN-AN-1123-ZBPro-Application-Template, который можно скачать здесь. В нём реализован весь базовый функционал сети ZigBee, которая работает на основе операционной системы JenOS, собственной разработке фи��мы Jennic для её микроконтроллеров. Кому интересно, могут скачать шаблон и посмотреть, я же приведу здесь только самый важный код.
В данной системе представлены все типы устройств сети ZigBee: координатор (конвектор), маршрутизатор (USB свисток) и спящее оконечное устройство (датчик). Начнём с самого простого — с USB свистка. Он занимается тем, что сканит UART на предмет появления байта с компьютера и отправляет принятый байт координатору.
Функция сканирования представляет собой задачу операционной системы, которая запускается один раз в 50 мс. Она проверяет не пришла ли команда и выдаёт все пришедшие команды в очередь сообщений, которая обрабатывается основной задачей.
OS_TASK(APP_CommandScan) { APP_tsEvent sCommandEvent; int16 word; //Считываем слово из УАРТа word=i16Serial_RxChar(E_AHI_UART_0); //Если успешно if(word != -1) { //Отправляем системное сообщение с кодом команды sCommandEvent.eType = APP_E_EVENT_COMMAND; sCommandEvent.sCommand.u8Value = (uint8)word; OS_ePostMessage(APP_CommandEvent, &sCommandEvent); } //Перезапускаем таймер задачи OS_eContinueSWTimer(APP_tmrCommandScan, APP_TIME_MS(50), NULL); }
В основном цикле все пришедшие команды отправляются координатору.
//Пришло системное сообщение о принятии команды по УАРТу if (APP_E_EVENT_COMMAND == sAppEvent.eType) { //Создаём указатель APDU PRIVATE PDUM_thAPduInstance s_hAPduInst = PDUM_INVALID_HANDLE; //Форматируем APDU для передачи команды s_hAPduInst = PDUM_hAPduAllocateAPduInstance(apduCommand); //Записываем в APDU данные для передачи PDUM_u16APduInstanceWriteNBO(s_hAPduInst, 0, //Стартовая позиция для записи "b", //Строка форматирования, передаём один байт (b) sAppEvent.sCommand.u8Value); //Устанавливаем размер APDU равным 1 байту PDUM_eAPduInstanceSetPayloadSize(s_hAPduInst, 1); //Отправляем данные, с указанием кластера, конечных точек отправителя и приёмника u8Status = ZPS_eAplAfUnicastDataReq(s_hAPduInst, MYPROFILE_MYCLUSTER_CLUSTER_ID, USBSTICK_MYENDPOINT_ENDPOINT, THERMOSTAT_MYENDPOINT_ENDPOINT, 0x0000, //Адрес получателя (в данном случае координатора) ZPS_E_APL_AF_UNSECURE, 0, &u8SeqNum); }
Датчик температуры просыпается один раз в секунду (время, конечно, настраивается), измеряет температуру и напряжение батарейки, отправляет всё конвектору и снова засыпает.
PRIVATE void vSendSensorData() { uint8 u8Status; uint8 u8SeqNum; //Создаём структуру для измеренных данных SensorData NewSensorData; //Измеряем температуру и напряжение батарейки NewSensorData.TempValue = TempMeasurement(); NewSensorData.BattValue = BatVoltageMeasurment(); //МАС адрес считывается один раз при запуске //и отправляется для того, чтобы различать несколько датчиков температуры uint32 MacH, MacL; MacH = MacAddr.u32H; MacL = MacAddr.u32L; //Создаём указатель APDU PRIVATE PDUM_thAPduInstance s_hAPduInst = PDUM_INVALID_HANDLE; //Форматируем APDU для передачи температуры s_hAPduInst = PDUM_hAPduAllocateAPduInstance(apduTemperature); //Записываем в APDU данные для передачи PDUM_u16APduInstanceWriteNBO( s_hAPduInst, 0, //Начальная позиция записываемых данных "wwbh", //Строка форматирования (в данном случае мы //хотим записать две 32-битных переменных (ww) //одну 8-битную (b) и одну 16-битную (h) MacH, MacL, NewSensorData.TempValue, NewSensorData.BattValue); //Устанавливаем общий размер отправляемых данных равным 11 байт PDUM_eAPduInstanceSetPayloadSize(s_hAPduInst, 11); //Отправляем данные широковещательным пакетом с указанием кластера и конечной точки отправителя u8Status = ZPS_eAplAfBroadcastDataReq(s_hAPduInst, MYPROFILE_TEMPERATURE_CLUSTER_ID, TEMPSENSOR_TEMPSENSORENDPOINT_ENDPOINT, 0xFF, ZPS_E_BROADCAST_ZC_ZR, ZPS_E_APL_AF_UNSECURE, 0 , &u8SeqNum); }
Координатор в свою очередь определяет от кого пришли данные и если это температура, то устанавливает соответствующее напряжение на выходе ЦАПа, а если это команда с компьютера, то выдаёт импульсы на кнопки установки температуры (эмулирует нажатие).
Функция установки температуры:
void SetTemp(int8 temp) { //Проверяем, что температура не выходит за диапазон if(temp > 33) temp = 33; else if(temp < 0) temp = 0; //Устанавливаем напряжение ЦАП, соответствующее текущей температуре vAHI_DacOutput(E_AHI_AP_DAC_1, temp_levels[temp]); }
Приём данных:
//Если пришли данные с датчика температуры if(MYPROFILE_TEMPERATURE_CLUSTER_ID == sStackEvent.uEvent.sApsDataIndEvent.u16ClusterId) { //Считываем PDUM_u16APduInstanceReadNBO(sStackEvent.uEvent.sApsDataIndEvent.hAPduInst, 8, "bh", &ReceivedTempSensorData); //Устанавливаем температуру SetTemp(ReceivedTempSensorData.TempValue); } else //Пришла команда с USB свистка { //Считываем PDUM_u16APduInstanceReadNBO(sStackEvent.uEvent.sApsDataIndEvent.hAPduInst, 0, "b", &Command); //Выдаём импульс, эмулирующий нажатие на соответствующую кнопку конвектора if(Command== '+') { vAHI_DioSetOutput(0, (1 << PLUS)); vDelay(50); vAHI_DioSetOutput((1 << PLUS), 0); } if(Command== '-') { vAHI_DioSetOutput(0, (1 << MINUS)); vDelay(50); vAHI_DioSetOutput((1 << MINUS), 0); } }
И напоследок, видео работы системы:
Полезные ссылки:
Описание операционной системы JenOS
Описание стека ZigBee Pro
Описание функций для работы с периферией JN5148
Архив с проектом
Архив со схемами
И ещё, статья опубликована 7 мая, так что всех с днём Радио!