Сохраняя энергию с помощью .NET Micro Framework
Наш дом оснащен относительно старым газовым водонагревателем, выпущенным в 1996 году. Он до сих пор работает прекрасно, регулярно проходит обслуживание с момента установки, и поэтому, пока нет причин для его замены. Однако он не является настолько же энергосберегающим, как большинство современных моделей.
Особенностью этого газового водонагревателя является то, что он держит воду горячей 24 часа 7 дней в неделю, 365 дней в году, неважно, нужна она нам или нет. В моем доме обычно горячая вода требуется утром между 7 и 9 часами и вечером от 17 до 21 часа. По выходным наше расписание слегка меняется и нам требуется горячая вода с 8 до 21 часа.
Итак, 30 часов в рабочие дни, плюс 26 часов в выходные, эти 56 часов в неделю нам действительно нужна горячая вода, против 168 часов в неделю, когда нагреватель остается в покое. Иными словами, в нашем доме требуется лишь треть энергии водонагревателя, которую мы нормально потребляем.
Более того, водонагреватель установлен строителями в гараже и в зимнее время он довольно холодный.
Предполагая, что около 25% счетов за тепло приходится на нагрев воды, я почувствовал необходимость в прекращении этих бессмысленных затрат.
Идея, которой я придерживался, состояла в том, чтобы сконструировать диспетчер, который бы следовал бы графику нашего недельного потребления горячей воды и уменьшал температуру воды до минимума в оставшиеся часы.
Мне бы хотелось, чтобы он был дешев и состоял из деталей, которые легко найти, и при этом был бы очень надежен, так как моя жена не будет признательна за холодный душ. Я решил использовать микроконтроллер Netduino-мини, часы реального времени AdaFruit DS1307 и сервомотор для регулировки температуры водонагревателя.
Здесь показан результат в действии:
Начальная последовательность и переход на ручное управление
Низкая скорость сервомотора задана умышленно для того, чтобы уменьшить изнашиваемость сервомеханизма и полной сборки.
Обзор решения
- Каждые 60 секунд приложение проверяет текущую дату и время и сравнивает с недельным расписанием, отслеживая ежедневные временные рамки, в которые газовый водонагреватель должен быть включен и когда он должен быть выключен.
- Когда наступает время включения нагревателя, программа подает питание на сервомотор, приводящий в действие диск термостата, и затем дает команду сервомотору повернуть диск до положения «Максимальный нагрев». Затем питание с сервомотора снимается.
- Наоборот, когда наступает время выключить нагреватель, выполняются те же шаги, но на этот раз диск поворачивается в обратном направлении.
- Настройка расписания и часов осуществляется через последовательный интерфейс.
- Пользователь может изменить расписание, нажав на кнопку, которая немедленно переводит водонагреватель в положение «включено».
Архитектура
Приложение построено на библиотеке «netduino.helpers» с открытым исходным кодом, которая обеспечивает набор аппаратных драйверов, написанных на C# и предназначенных для микроконтроллеров с .Net Micro Framework семейства netduino.
Приложением используются следующие драйверы:
- DS1307. cs: Этот класс реализует полный драйвер для часов реального времени Dallas Semiconductors / Maxim DS1307 I2C. Часы имеют 56 байт памяти с питанием от батареи, которые использует приложение для сериализации и десериализации данных еженедельного расписания.
- HS6635HBServo . cs: Этот класс реализует драйвер для сервомотора HiTech HS-6635HB. Хотя класс был протестирован только с этим сервомотором, он достаточно общий и может использоваться со многими другими марками сервомоторов.
- SerialUserInterface . cs: Этот класс обеспечивает строительные блоки, необходимые для получения и отображения данных через последовательный порт. Ввод пользователя обрабатывается с помощью прерываний и приводит к обратному вызову при нажатии клавиши «Ввод». Также он обеспечивает запоминание введенной величины, а первичная проверка данных удобна для проверки вводимых последовательностей при выборе пунктов меню и управлении состояниями.
- PushButton . cs: Этот класс представляет собой оболочку вокруг класса InterruptPort .Net Micro Framework и упрощает моментальное переключение приложения без опроса портов ввода.
Детали приложения
Просматривая приложение, необходимо отметить несколько деталей:
Приложение начинается с определения условной компиляции задающего используемое аппаратное обеспечение. Поскольку платы netduino имеют различные форм-факторы и наборы устройств на борту, разводка контактов будет меняться.
По умолчанию приложение настроено на netduino-mini:
#define NETDUINO_MINI
Все остальные определения относятся в обычному Netduino. Мне не удалось проверить их с Netduino Plus, так как в коде пока не было обеспечения для него.
#define NETDUINO_MINI
using System;
using System.Threading;
using System.Collections;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using netduino.helpers.Hardware;
using netduino.helpers.SerialUI;
using netduino.helpers.Servo;
using System.IO.Ports;
Вы также должны не забыть включить правильную ссылку на сборку Secret Labs для платформы. Если не сделать этого, то код может работать, но поведение GPIO будет фантастическим .
#if NETDUINO_MINI
// Вы должны быть уверены, что в проекте имеется ссылка на SecretLabs.NETMF.Hardware.NetduinoMini
// Вы также должны удалить SecretLabs.NETMF.Hardware.Netduino, если она там имеется.
using SecretLabs.NETMF.Hardware.NetduinoMini;
#else
// Вы должны быть уверены, что в проекте имеется ссылка на SecretLabs.NETMF.Hardware.Netduino
// Вы также должны удалить SecretLabs.NETMF.Hardware.NetduinoMini, если она там имеется.
using SecretLabs.NETMF.Hardware.Netduino;
#endif
Большинство основных объектов в приложении сохраняются на протяжении жизни приложения, за исключением объекта SerialUserInterface, который может быть использован повторно при ошибке связи.
namespace WaterHeaterController {
public class Program {
#if NETDUINO_MINI
private static readonly OutputPort _servoPowerEnable = new OutputPort(Pins.GPIO_PIN_16, false);
private static readonly PushButton _pushButton = new PushButton(Pin: Pins.GPIO_PIN_17, Target: PushButtonHandler);
private static readonly OutputPort _ledOverride = new OutputPort(Pins.GPIO_PIN_13, false);
private static readonly OutputPort _ledServoPowerEnable = new OutputPort(Pins.GPIO_PIN_15, false);
private static readonly PWM _ledLowHeat = new PWM(Pins.GPIO_PIN_18);
private static readonly PWM _ledHighHeat = new PWM(Pins.GPIO_PIN_19);
private static readonly HS6635HBServo _servo = new HS6635HBServo(Pins.GPIO_PIN_20,minPulse: 700, centerPulse: 1600);
private static SerialUserInterface _serialUI = new SerialUserInterface(Serial.COM2);
#else
private static readonly OutputPort _servoPowerEnable = new OutputPort(Pins.GPIO_PIN_D3, false);
private static readonly PushButton _pushButton = new PushButton(Pin: Pins.ONBOARD_SW1, Target: PushButtonHandler);
private static readonly OutputPort _ledOverride = new OutputPort(Pins.GPIO_PIN_D2, false);
private static readonly OutputPort _ledServoPowerEnable = new OutputPort(Pins.GPIO_PIN_D4, false);
private static readonly PWM _ledLowHeat = new PWM(Pins.GPIO_PIN_D5);
private static readonly PWM _ledHighHeat = new PWM(Pins.GPIO_PIN_D6);
private static readonly HS6635HBServo _servo = new HS6635HBServo(Pins.GPIO_PIN_D9,minPulse: 700, centerPulse: 1600);
private static SerialUserInterface _serialUI = new SerialUserInterface();
#endif
private static readonly Schedule _schedule = new Schedule();
private static readonly Status _status = new Status();
private static readonly DS1307 _clock = new DS1307();
Следующие константы задают абсолютное положение сервопривода, выраженное в градусах. В зависимости от физической конфигурации сервомотора (вертикальное положение, горизонтальное, слева или справа от термостата), возможно, вам понадобится изменить эти параметры так, чтобы сервомотор двигался в нужном направлении.
Значения ниже соответствуют «перевернутому» сервомеханизму (плечо направлено вниз), помещенному слева от газового затвора.
private const uint LowHeat = 180;
private const uint Center = 100;
private const uint HighHeat = 0;
Когда программа запускается, она проверяет, правильно ли настроены часы реального времени.
Если методом _clock.Get() генерируется исключение, вызванное сбоем инициализации объекта DateTime, то часам присваивается значение по умолчанию, расписание, хранящееся в резервируемой памяти часов, заполняется нулями, и запускается внутренний осциллятор часов.
private static void InitializeClock() {
try {
_clock.Get();
} catch (Exception e) {
Debug.Print("Initializating the clock with default values due to: " + e);
byte[] ram = new byte[DS1307.DS1307_RAM_SIZE];
_clock.Set(new DateTime(2011, 1, 1, 12, 0, 0));
_clock.Halt(false);
_clock.SetRAM(ram);
}
}
На следующем шаге происходит позиционирование сервомеханизма: во избежание случайного или жесткого движения диска термостата, приложение ожидает, что пользователь вручную повернул рычаг сервомехнизма на угол 90º до подачи электроэнергии на контроллер: когда рычаг сервомеханизма выставлен на центр, вызов _servo.Center() вряд ли приведет к какому-то движению.
InitializeClock();
Log("\r\nWater Heater Controller v1.0\r\n");
Log("Initializing...");
LoadSchedule();
PowerServo(true);
Log("Centering servo");
_servo.Center();
Log("Setting heater on high heat by default");
_servo.Move(Center, currentHeat);
Log("Running...");
PowerServo(false);
Важно отметить, что всем вызовам движений сервомеханизма предшествует вызов PowerServo(): плата котроллера сконструирована так, что подача напряжения питания 5 В на сервомотор выполняется через транзистор, активируемый и деактивируемый этим вызовом. В то же время, светодиод включается и выключается, показывая, когда сервомеханизм выполняет движение.
Чтобы быть уверенным, что движение сервомотора всегда правильное и медленное, насколько возможно предотвращающее износ газовой заслонки, метод _servo.Move() задает движение сервомотора с шагом в один градус и с короткой паузой между шагами:
// Медленное движение сервомотора от одного положения к другому
public void Move(uint startDegree, uint endDegree, int delay = 80) {
if (delay <= 1) {
delay = 10;
}
if (startDegree < endDegree) {
for (var degree = startDegree; degree <= endDegree; degree++) {
Degree = degree;
Thread.Sleep(delay);
}
} else {
for (var degree = startDegree; degree > endDegree; degree--) {
Degree = degree;
Thread.Sleep(delay);
}
}
Release();
}
Свойство Degree преобразует значение угла от 0 до 180 в «сервоимпульсы» в диапазоне от 900 до 2100, основываясь на спецификации HS-6635HB и используя функцию преобразования, описанную здесь: https://rosettacode.org/wiki/Map_range#C.
Заметим, что спецификация сервомеханизма не всегда соответствует действительным возможностям механизма и может понадобиться подогнать минимальное и максимальное значения импульсов после тестирования конкретного сервомотора:
public static void Main() {
using (var servo = new HS6635HBServo(Pins.GPIO_PIN_D9))
{
servo.Center();
servo.Move(90, 0, 25);
while (true)
{
servo.Move(0, 180, 25);
servo.Move(180, 0, 25);
}
}
}
В моем случае я обнаружил, что угол 0 соответствует значению 700, а не 900, как утверждается в спецификации.
После завершения инициализации выполняется главный цикл приложения до тех пор, пока не поступит команда выключения. Основная работа цикла состоит в проверке времени и его сравнении с расписанием работы водонагревателя и реагирование на события, вызываемые пользователем.
Взаимодействие с пользователем происходит через последовательный порт netduino, основную работу с которым выполняет класс SerialInterface.cs: он облегчает создание простых меню и получает ввод пользователя с помощью управления событиями, чтобы избежать постоянного опроса последовательного порта.
Когда пункт меню выбран или в поле формы нажата клавиша CR/LF, происходит обратный вызов метода с контекстом, необходимым для обработки пользовательского ввода.
Работа с главным меню приложения показана ниже:
public static void MainMenu(SerialInputItem item) {
var SystemStatus = new ArrayList();
_status.Display(SystemStatus, _clock, _schedule);
_serialUI.Stop();
_serialUI.AddDisplayItem(Divider);
_serialUI.AddDisplayItem(SystemStatus);
_serialUI.AddDisplayItem("\r\n");
_serialUI.AddDisplayItem("Main Menu:\r\n");
_serialUI.AddInputItem(new SerialInputItem { Option = "1", Label = ": Show Schedule", Callback = ShowSchedule });
_serialUI.AddInputItem(new SerialInputItem { Option = "2", Label = ": Set Schedule", Callback = SetSchedule });
_serialUI.AddInputItem(new SerialInputItem { Option = "3", Label = ": Set Clock", Callback = SetClock, Context = 0 });
_serialUI.AddInputItem(new SerialInputItem { Option = "4", Label = ": Swith Heater ON / Resume Schedule", Callback = SwitchHeaterOn });
_serialUI.AddInputItem(new SerialInputItem { Option = "X", Label = ": Shutdown", Callback = Shutdown });
_serialUI.AddInputItem(new SerialInputItem { Callback = RefreshMainMenu });
_serialUI.Go();
}
Управление интерфейсом пользователя происходит между ограничивающими вызовами _serialUI.Stop(), который запрещает прерывания от последовательного порта и очищает любые ранее определенные интерфейсы и _serialUI.Go(), который снова разрешает прерывания последовательного порта и восстанавливает состояние входного буфера.
Вывод текста для пользователя осуществляется с помощью последовательных вызовов метода_serialUI.AddDisplayItem(), который отображает текст в порядке поступления.
Определение пользовательского ввода сделано тем же способом, с помощью последовательных вызовов _serialUI.AddInputItem(), принимающего следующие параметры:
Option (вариант): определяет элемент в списке из одного или нескольких значений. Это не обязательный параметр. Когда он опущен, ввод будет осуществляться в свободной форме. В этом случае должен быть только один вводимый элемент, предоставленный пользователю в единицу времени.
Label (метка): текстовая метка показана следом за полем Option. Это необязательный параметр. Если поле Option опущено, то поле Label должно присутствовать, позволяя определить, какой формат свободного пользовательского ввода ожидается. Например:
_serialUI.AddInputItem(new SerialInputItem { Label = "End Hour (00-23)?", Callback = SetSchedule, Context = 4, StoreKey = "sched.endHour" });
Callback (обратный вызов): обязательный делегат метода, вызываемый когда пользователь нажимает клавишу «Enter». Если заданы один или несколько параметров Option, пользователь увидит сообщение об ошибке, если не обеспечит допустимое значение при вводе вместо вызова делегата метода. Задание единственного параметра Callback позволяет определить обработчик, «захватывающий всё».
Context (контекст): необязательное целое значение используемое для управления переходами между состояниями, когда для заполнения формы требуются множественные последовательные операции ввода. Например:
switch (item.Context) {
case 0:
_serialUI.Store.Clear();
_serialUI.AddDisplayItem(Divider);
_serialUI.AddDisplayItem("Set Clock:\r\n");
_serialUI.AddInputItem(new SerialInputItem { Label = "Year (YYYY)?", Callback = SetClock, Context = 1, StoreKey = "clock.YYYY" });
break;
case 1:
_serialUI.AddInputItem(new SerialInputItem { Label = "Month (01-12)?", Callback = SetClock, Context = 2, StoreKey = "clock.MM" });
break;
case 2:
_serialUI.AddInputItem(new SerialInputItem { Label = "Day (01-31)?", Callback = SetClock, Context = 3, StoreKey = "clock.DD" });
break;
case 3:...
StoreKey (ключ хранилища): необязательный параметр используется для сохранения вводимой пользователем информации в словаре, где StoreKey – имя значения, введенного пользователем. В примере выше вызов _serialUI.Store.Clear(); позволяет убедиться, что словарь пуст до того как пользователь введет что-либо.
Другие моменты, о которых стоит сказать: .NET Micro Framework на Netduino имеет спартанский дизайн, так что эта среда прекрасно впишется в ограничения чипа Atmel ARM 7. В результате возможности, которые многие программисты считают само собой разумеющимися, не всегда присутствуют или же могут поддерживаться лишь частично: обобщения, отражение, сериализация, создание динамичных доменов приложений и т. д. Поэтому следует ожидать, что придется идти на компромисс, и принимать, что код не всегда будет таким «чистым», как он мог бы быть в другом случае.
Это одна из причин создания библиотеки netduino.helpers, предназначенной для предоставления повторно используемых строительных блоков, относящихся к платформе Netduino, и пытающейся снизить барьер для начала разработки встраиваемого аппаратного обеспечения для народа, пришедшего с чисто программным опытом разработки.
Построение платы контроллера газового водонагревателя
Электронные компоненты
Плата контроллера газового нагревателя воды построена с использованием:
· Одних часов реального времени DS1307
· Одного сервомеханизма HiTech HS-6635HB
А также следующих компонентов:
· 1 выпрямительный диод 1N4001, используемый на выводе земли источника питания 9 B
· 1 транзистор 2N2222, переключающий питание на сервомеханизме
· 1 выпрямительный диод 1N4001, подсоединенный к базе транзистора
· 1 резистор сопротивлением 300 Ом, соединенный последовательно с базой транзистора
· 4 светодиода различных цветов
· 4 резистора сопротивлением по ~220 Ом для ограничения тока через светодиоды
· 1 быстродействующий переключатель
· 1 резистор сопротивлением 100 Ом (используется с переключателем)
· 1 резистор сопротивлением 10К (используется с переключателем)
· 1 источник питания на 9 В и 1 А, работающий от настенной розетки
· 1 плата для макетирования общего типа
· Прямые и изогнутые контактные гнезда
· 1 соединитель для источника питания
Простая электрическая схема
Чтобы увеличить время жизни сервомеханизма, я решил управлять подачей энергии на него с помощью транзистора 2N2222A. Это прекрасно сработало, так как сервомеханизму не нужно было удерживать ручку водонагревателя в течение всего времени: однажды повернутая, ручка оставалась в новом положении и сервомеханизму не требовалась энергия, чтобы удерживать ее в этом положении.
База транзистора подсоединялась к выводу «Servo Power Enable» (вывод 16 на Netduino mini) через резистор 300 Ом, соединенный последовательно с выпрямительным диодом 1N4001. Диод убирал 0.7 В, присутствующих на выводе даже в случае, когда на выводе не было сигнала.
Здесь ветка дискуссии на форуме по поводу проблемы с присутствием 0,7 В на выводе сервомеханизма, которая, кажется, является характерной для Netduino mini.
Будучи поборником безопасности, я также добавил выпрямительный диод на вывод 23 (земля питания) Netduino mini для предотвращения любого возможного повреждения микроконтроллера при неверном подключении питания. Я не нашел такой защиты в схемах mini.
Сборка платы:
Окончательный вариант:
Механические части
- 2 пластиковых палочки от мороженого (popsicle sticks)
- Моток тонкой металлической проволоки
- Тонкий, но прочный латунный стержень
- Резиновая прокладка, достаточная для того, чтобы обернуть ручку водонагревателя по окружности. Убедитесь, что ширина прокладки не превышает ширины рукоятки.
- Регулируемый металлический хомут
- Самодельный кронштейн для длинной палочки от мороженого из тонкой полоски латуни
- 10 пластиковых стяжек
- 1 небольшая деревянная доска для монтажа сервомеханизма
Монтаж сервомотора на водонагревателе
Я решил закрепить сервомотор на газовой трубе водонагревателя, используя секцию трубы слева от газового клапана. Чтобы сделать это, я вырезал кусок из остатков покрытия пола, сделанного из твердого дерева, так чтобы подогнать его к нижней левой части трубы. Я просверлил отверстия в верхней и левой стороне доски так, чтобы закрепить ее на газовой трубе с помощью стяжек. Также я просверлил четыре отверстия для крепления на доске сервомотора с помощью винтов и стопорных гаек.
Соединение сервомотора с управляющей рукояткой
- Просверлить два отверстия вблизи концов палочек от мороженого.
- Убедиться, что тонкий латунный стержень легко проходит через эти отверстия, но не имеет зазоров.
- Закрепить латунный кронштейн на одной из палочек с помощью липкой ленты, а затем плотно обмотать его металлической проволокой
- Ввести латунный кронштейн между резиновой прокладкой и металлическим хомутом
- Натянуть металлический хомут вокруг ручки водонагревателя
Окончательная сборка должна сидеть туго и прочно при вращении рукоятки.
Чтобы закрепить другую палочку на плече сервомотора просверлите в ней несколько отверстий, соответствующих отверстиям плеча сервомотора. Затем пропустите тонкую металлическую проволоку через отверстия и плотно примотайте ею палочку к плечу сервомотора.
Соедините палочки вместе с помощью латунного стержня и закрепите его, аккуратно сгибая стержень вокруг концов палочек. Это легко сделать, пока плечо сервомотора не прикреплено к сервомотору.
Наконец, подсоедините плечо к сервомотору и проверьте сборку, медленно его вращая.
Рукоятка должна легко и синхронно поворачиваться вместе с сервомотором:
Приведение в действие
Подсоедините простой терминал к COM2 на Netduino mini (или к COM1 обычного Netduino).
Использование последовательного интерфейса:
- Установите дату и время на часах и определите, когда нагреватель должен включаться
- Расписание отслеживает 7 дней с четырьмя временными интервалами в каждом дне. Каждый временной интервал имеет время начала, определяющее, когда нагреватель должен включиться и время конца, когда он должен выключиться.
- Установка временного интервала в 0 сбрасывает его, и нагреватель остается выключенным.
- Имейте в виду, что временные интервалы нагревателя и часов задаются по 24-часовому расписанию.
Простой вывод:
[02/19/2011 19:29:40]
Water Heater Controller v1.0
[02/19/2011 19:29:40] Initializing...
[02/19/2011 19:29:40] Loading schedule
[02/19/2011 19:29:40] Centering servo
[02/19/2011 19:29:41] Setting heater on high heat by default
[02/19/2011 19:29:51] Running...
-------------------------------------------------------------
Time: Saturday, 19 February 2011 19:29:51
Heater Schedule Today: Sat [8-21] [0-0] [0-0] [0-0]
Heater Status: ON [scheduled]
Main Menu:
1 : Show Schedule
2 : Set Schedule
3 : Set Clock
4 : Swith Heater ON / Resume Schedule
X : Shutdown
[02/19/2011 19:29:52] Heater state change
[02/19/2011 19:29:52] Setting heater on high
1
-------------------------------------------------------------
Heater Weekly Schedule:
Sun [8-21] [0-0] [0-0] [0-0]
Mon [6-9] [17-21] [0-0] [0-0]
Tue [6-9] [17-21] [0-0] [0-0]
Wed [6-9] [17-21] [0-0] [0-0]
Thu [6-9] [17-21] [0-0] [0-0]
Fri [6-9] [17-21] [0-0] [0-0]
Sat [8-21] [0-0] [0-0] [0-0]
Значения светодиодов
- Светодиод сильного нагрева: состояние «включено» означает, что нагреватель сильно греет воду
- Светодиод слабого нагрева: состояние «включено» означает, что нагреватель слабо греет воду
- Светодиод активности сервомотора: состояние «включено» означает, что сервомеханизм меняет состояние
- Светодиод пересмотра сильного нагрева: состояние «включено» означает, что нажатие на кнопку отменило действие расписания
Первый запуск контроллера водонагревателя
Прежде чем впервые подать питание на схему, убедитесь, что плечо сервомотора отцентрировано (в вертикальном направлении). Это поможет удостовериться, что начальная последовательность пройдет гладко. С этого момента контроллер будет медленно устанавливать рукоятку водонагревателя на сильный нагрев, прежде чем начнет отслеживать расписание.
Пока,
-Фабье.