Давно читаю рубрику DIY и руки чесались что-нибудь сделать на микроконтроллере. И наудачу прочитал о MSP430 Launch Pad от Texas Instruments по цене $4.30. Идеальный набор для старта.
В качестве объекта для экспериментов был выбран автоматический освежитель воздуха одной известной марки.
Задача
Оригинальный освежитель имеет функцию распыления через заданные промежутки времени, что не всегда бывает удобно. Так как днем дома никого нет, и баллон расходуется впустую. К тому же разработчики встроили «защиту» от использования баллонов других производителей, которая хоть и обходится достаточно просто, но без нее жить становится проще. К тому же, для «ручного» распыления приходилось доставать баллон и пользоваться им как обычным освежителем.
Что же хотелось получить
Во-первых, сделать алгоритм работы более интеллектуальным. А именно сделать так, чтобы освежитель срабатывал после посещения туалета. А во-вторых, добавить функцию «ручного» срабатывания без открывания корпуса.
Решение
Алгоритм
Автоматизацию срабатывания распылителя я решил сделать на основе датчика освещенности. То есть при выключении света через 5 секунд должен сработать распылитель. После этого не реагировать на включение-выключение света в течение 15 минут. Для того чтобы не «переосвежить» помещение так что дышать будет не возможно. Так же добавить кнопку для «ручного» распыления не зависимо от режима работы.
Вскрытие «пациента»
Прежде чем добавлять что-то новое, решил посмотреть, что же внутри у этого «чудо-агрегата».
И первое на что наткнулся это шурупы с нестандартной «треугольной» головкой:
Благо, в наборе микро отверток нашлась подходящая и процесс на этом не остановился. Внутри же все оказалось очень просто. Механизм спуска представляет из себя обычный электромотор через редуктор из пары шестеренок соедененный с язычком, который и давит на распылитель баллона. Плата управления интереса не представляет, только в качестве шаблона для изготовления своей, да как источник разъемов для подключения питания и мотора спуска.
Электронная часть
Когда стало ясно, что из себя представляет это устройство можно приступать к проектированию собственного модуля управления.
Схема модуля управления. Хочу сразу заметить, это мое первое готовое устройство, поэтому и схема и плата далеки от стандартов качества:
Схема составлялась исходя из рекомендаций Datasheet на контроллер и статей из интернета. Отдельное спасибо DIHALT его учебник по электронике для начинающих стал для меня отправной точкой.
По компонентам все очень просто. Фильтр на конденсаторах С1 и С2 согласно рекомендациям из Datasheet увеличивает точность работы АЦП. Фоторезистор LDR1 является датчиком освещенности и подключен через делитель напряжения на резисторе R3. Мотор спуска управляется через транзистор BC337. Так же на схеме добавлены кнопка ручного спуска S1 и светодиоды двух цветов. Я использовал двухцветный светодиод. Он мигает зеленым раз в секунду как индикатор нормального режима работы, при разряде батарей ниже 2.5В начинает мигать красным. Источник сигнала часов реального времени — кварц Z1 на 32768 Гц который идет в комплекте c платой и контроллерами.
Программная часть
Программу для контроллера я писал в IAR Embedded Workbench на С++. Привожу здесь ее код с комментариями.
Заголовочный файл spray.h
#ifndef __SPRAY_H_
#define __SPRAY_H_
#include <sstream>
// Константы
#define BUTTON BIT4; // Изменить в релизе
#define SPRAY_PORT BIT6;// P1.6
#define LED_GREEN BIT1; // P2.1
#define LED_RED BIT2; // P2.2
#define LIGHT_SENSOR INCH_5; // A5
#define DARK_VALUE 650; // Значения АЦП соответствющее "Свет выкл."
#define LOWBATT_VALUE 520; // Значение АЦП соответствующее разряженной батарее.
// Переменные константы
unsigned int SleepPeriod = 900; // Период спящего режима после срабатывания. 15 минут. (900)
unsigned int SpayDelay = 5; // Задержка перед срабатыванием 5с.
unsigned int DarkValue = 650; // Значения АЦП соответствющее "Свет выкл."
unsigned int LowBattValue = 520; // Значение АЦП соответствующее разряженной батарее.
unsigned int BattInterval = 5; // Измерять напряжение каждые 5сек.
// Переменные
unsigned int ADCValue; // Значение АЦП
unsigned int SprayCount; // Счетчик числа срабатываний
unsigned int TimerCount = 0; // Счетчик временных интервалов
unsigned int BattTimer = 0; // Счетчик интервалов замера напряжения
unsigned int RXByte; // Переменная для хранения принятого байта по UART
// Флаги
bool ADCDone = false; // Флаг завершения преобразования
bool LightOn = false; // Флаг включения света (ожидание выключения)
bool SleepMode = false; // Флаг спящего режима (после срабатывания 15 минут)
bool IsCounting = false; // Флаг поднимаемый после выключения света, индикация
// режима ожидания перед срабатыванием распылителя
bool LowBatt = false; // Флаг низкого напряжения батареи
bool BlinkOn = false; // Флаг включения мигающего светодиода
bool Tick = false; // Флаг прерывания по таймеру реального времени
bool IsADCOn = false; // Флаг показывающий работу АЦП
bool IsADCLight = true; // Флаг режима работы АЦП (true - измерение уровня
// освещенности, false - напряжения)
bool ForceSpay = false; // Принудительное распыление по нажатию на кнопку
bool UARTReceived = false; // Флаг получения команды по UART
// Функции
void UARTSend(string str); // Отправка строки UART
void LightSensorOn(); // Включение АЦП и измерение уровня освещенности
void VCCSensorOn(); // Включение АЦП и измерение напряжения питания
unsigned int ReadCountFromFlash(); // Считать значение счетчика срабатываний
void WriteCountToFlash(unsigned int); // Запись значения счетчика срабатываний
void Spray(); // Включение распылителя
#endif
Текст программы. main.cpp:
#include <msp430g2553.h>
#include "spray.h"
int main( void )
{
// Остановка WDT нам не потребуется
WDTCTL = WDTPW + WDTHOLD;
// Блок инициализаци переферии
//-------------------------
// Порты ввода-вывода
P1DIR &= ~BUTTON; // Порт кнопки на вход
P1REN |= BUTTON; // Включение внутреннего подтягивающего резистора
P1OUT &= ~BUTTON; // Направление подтяжки - GND
P1IES |= BUTTON; // Прерывание по фронту сигнала
P1IFG &= ~BUTTON; // Сброс флага прерывания
P1IE |= BUTTON; // Разрешение прерывания по кнопке
P1DIR |= SPRAY_PORT; // Выход на управление распылителем
P1OUT &= ~SPRAY_PORT; // низкий уровень на порту
// Настройка выводов на светодиоды
P2DIR |= LED_GREEN
P2DIR |= LED_RED;
// Включаем зеленый светодиод (Признак загрузки)
P2OUT |= LED_GREEN;
P2OUT &= ~LED_RED; // Тушим красный.
//-------------------------
// Системные таймеры
DCOCTL = CALDCO_1MHZ; // Главный и вспомогательные таймеры ~ 1МГц
BCSCTL1 = CALBC1_1MHZ;
//-------------------------
// Таймер реального времени
TACTL = TASSEL_1 + MC_1; // Источник тактового сигнала ACLK (кварц),
// прерывание по достижению значения CCR0
CCR0 = 0x8000; // Т.к. частота кварца 32768Гц, то и считаем до этого значения
// в результате получаем прерывание раз в секунду
CCTL0 = CCIS0 + OUTMOD0 + CCIE; // Настройки таймера, простой счет и вызов
// прерывания по достижению значения
//-------------------------
// UART. Нужен для считывания значения из FLASH
P1SEL = BIT1 + BIT2; // Выбор функции портов
P1SEL2 = BIT1 + BIT2; // UART
UCA0CTL1 |= UCSSEL_2; // Тактовый сигнал - вспомогательный таймер SMCLK ~ 1MHz
UCA0BR0 = 0x68; // Скорость передачи 9600
UCA0BR1 = 0; // 1000000Гц / 9600 = 104 = 0x68
UCA0MCTL = UCBRS0;
UCA0CTL1 &= ~UCSWRST; // Выбор модуляции и сброс флага блокировки
IE2 = UCA0RXIE; // Разрешаем прерывания по приему UART
//-------------------------
// Flash
FCTL2 = FWKEY + FSSEL1 + FN1; // Выбор источника тактового сигнала и делителя
// Проверка значения счетчика срабатываний. Если значение 65535 - считчик
// не инициализирован
SprayCount = ReadCountFromFlash();
if(SprayCount == 65535)
{
SprayCount = 0; // Обнуляем счетчик
WriteCountToFlash(SprayCount); // Записываем 0 во Flash
}
//-------------------------
__delay_cycles(2000); // Ожидание инициализации переферии
//-------------------------
__bis_SR_register(GIE); // Глобальное разрешение прерываний
P2OUT &= ~LED_GREEN; // Выключаем зеленый светодиод. Загрузка окончена.
UARTSend("Readyrn");
// Главный цикл
while(true)
{
if(Tick) // Раз в секунду.
{
Tick = false; // Опускаем флаг
BattTimer++;
if(BattTimer >= BattInterval) // Если прошло время между замерами напряжения
{
BattTimer = 0; // Сброс таймера
VCCSensorOn(); // Измерение напряжения батареи
}
if(!SleepMode) // Если не в спящем режиме
{
LightSensorOn(); // Измерить уровень освещенности
if(IsCounting) // Если в режиме задержки
{
if(TimerCount >= SpayDelay) // Время задержки истекло
{
Spray(); // Включение распылителя
}
}
}
else
{
//Проверка таймера в спящем режиме
if(TimerCount >= SleepPeriod)
{
TimerCount = 0; // Сброс таймера
SleepMode = false; // Выход из спящего режима
}
}
}
if(ADCDone) // При завершении преобразования
{
ADCDone = false; // Опускаем флаг преобразования
if(IsADCLight) // Если проводился замер уровня освещения
{
if(ADCValue < DarkValue) // Горит свет.
{
LightOn = true; // Поднимаем флаг "Горит свет"
IsCounting = false; // Останавливаем отсчет (был или не было, не важно)
TimerCount = 0; // Сбрасываем таймер
}
else // Свет не горит
{
if(LightOn) // Если горел свет
{
LightOn = false; // Опускаем флаг
IsCounting = true; // Запускаем таймер задержки перед срабатыванием
TimerCount = 0;
}
}
}
else // Если замерялось напряжение
{
if(ADCValue < LowBattValue)
{
LowBatt = true; // Поднимаем флаг низкого напряжения
}
else
{
LowBatt = false; // Иначе - сброс флага
}
}
}
if(ForceSpay) // Нажата кнопка. Принудительное распыление.
{
ForceSpay = false;
Spray();
}
if(UARTReceived) // Получена команда UART. Отправить значение счетчика.
{
UARTReceived = false;
ostringstream s;
s << SprayCount << "rn";
UARTSend(s.str());
}
}
}
// Отправка строки UART
void UARTSend(string str)
{
int len = str.length();
for(int i=0;i<len;i++)
{
while(!(IFG2&UCA0TXIFG));
UCA0TXBUF = str[i];
}
}
// Включение АЦП и измерение уровня освещенности
void LightSensorOn()
{
if(!IsADCOn)
{
IsADCOn = true;
IsADCLight = true;
ADC10CTL0 &= ~ENC;
ADC10CTL0 = ADC10SHT_3 + ADC10ON + ADC10IE;
ADC10CTL1 = ADC10SSEL_2 + LIGHT_SENSOR;
ADC10CTL0 |= ENC + ADC10SC;
}
}
// Включение АЦП и измерение напряжения питания
void VCCSensorOn()
{
if(!IsADCOn)
{
IsADCOn = true;
IsADCLight = false;
ADC10CTL0 &= ~ENC;
ADC10CTL0 = SREF_1 + ADC10SHT_3 + REFON + ADC10ON + REF2_5V + ADC10IE;
ADC10CTL1 = ADC10SSEL_2 + INCH_11;
__delay_cycles(128);
ADC10CTL0 |= ENC + ADC10SC;
}
}
// Считать значение счетчика срабатываний
unsigned int ReadCountFromFlash()
{
__bic_SR_register(GIE); // Отключение всех прерываний
int *Flash_ptr;
Flash_ptr = (int *) 0x1040; // Сегмент С
*Flash_ptr = 0; // Инициализация указателя
unsigned int value = *Flash_ptr; // Считывание значения
__bis_SR_register(GIE); // Включение прерываний
return value; // Возврат значения
}
// Запись значения счетчика срабатываний
void WriteCountToFlash(unsigned int value)
{
__bic_SR_register(GIE); // Отключение прерываний
int *Flash_ptr;
Flash_ptr = (int *) 0x1040; // Сегмент С
FCTL1 = FWKEY + ERASE; // Установка бита стирания сегмента
FCTL3 = FWKEY;
*Flash_ptr = 0; // Инициализация указателя
FCTL1 = FWKEY + WRT;
*Flash_ptr++ = value; // Запись значения во Flash
FCTL1 = FWKEY;
FCTL3 = FWKEY + LOCK; // Установка бита запрещения записи
__bis_SR_register(GIE); // Разрешение прерываний
}
// Включение распылителя
void Spray()
{
__bic_SR_register(GIE); // Отключаем прерывания
P1OUT |= SPRAY_PORT; // Включаем распылитель
__delay_cycles(500000); // Ждем ~ 0.5 сек (при частоте 1МГц)
P1OUT &= ~SPRAY_PORT; // Выключаем распылитель
// Сброс таймера ожидания и переход в спящий режим
LightOn=false;
SleepMode=true;
IsCounting=false;
TimerCount=0;
SprayCount++;
WriteCountToFlash(SprayCount);
__bis_SR_register(GIE); // Включаем прерывания обратно
}
//---------------------------
// Обработчики прерываний
// Обработка АЦП
#pragma vector=ADC10_VECTOR
__interrupt void ADC_Handler()
{
ADCValue = ADC10MEM; // Считываем значение из регистра АЦП
ADCDone = true; // Поднимаем флаг завершения преобразования
IsADCOn = false;
}
// Обработка нажатия на кнопку
#pragma vector=PORT1_VECTOR
__interrupt void Button_Handler()
{
P1IE &= ~BUTTON; // Временное отключение прерываний с кнопки
P1IFG &= ~BUTTON; // Сброс прерывания
ForceSpay = true; // Поднятие флага принудительного распыления
// Борьба с дребезгом контактов
__delay_cycles(300000);
P1IE |= BUTTON; // Разрешение прерываний
}
// Обработка прерывания по таймеру 1раз в сек.
#pragma vector=TIMER0_A0_VECTOR
__interrupt void Timer_Handler()
{
// Мигание раз в секунду
if(!BlinkOn) // Если светодиод погашен
{
// Включить в зависимости от состояния батареи соответствующий светодиод
// При флаге низкого напряжения батареи - красный, иначе зеленый
if(LowBatt)
{
P2OUT |= LED_RED;
}
else
{
P2OUT |= LED_GREEN;
}
BlinkOn = true;
}
else // Потушить оба светодиода
{
P2OUT &= ~LED_RED;
P2OUT &= ~LED_GREEN;
BlinkOn = false;
}
TimerCount++; // Прибавить значение счетчика секунд
Tick=true; // Поднять флаг
}
// Обработка получения данных по UART
#pragma vector=USCIAB0RX_VECTOR
__interrupt void UART_Handler()
{
RXByte = UCA0RXBUF;
UARTReceived = true;
}
Программа в данном виде не содержит блока для индикации окончания баллона, так как на момент написания не известно на сколько распылений его хватит.
Сборка
После компиляции и отладки программы на плате, модуль был собран для тестов на макетной плате:
Отладочная плата LaunchPad тут является просто держателем для кварца, так как он настолько мал, что поместить его на макете просто невозможно.
Когда я убедился что все работает без сбоев и согласно алгоритма, занялся изготовлением платы для модуля управления, которую можно поместить в корпус на место родной.
Разводка платы для модуля управления (сделано в Sprint-Layout):
Затем была изготовлена плата. Рисунок нанесен методом ЛУТ (еще раз спасибо DIHALT), затем плата протратравлена в хлорном железе и залужена.
Далее, напаяны компоненты. Микроконтроллер помещен в панельку, чтобы было легко его снять и считать значение счетчика, а так же поменять на другой при окончательной доработке программы.
И затем после тестирования устройство собрано уже с новым модулем управления.
Заключение
В результате работы над этим устройством, которая длилась около недели вечерами, я убедился что разработка устройств на современных микроконтроллерах не представляет трудностей даже для начинающих постигать электронику. То, что все заработало, как планировалось, еще больше распалило энтузиазм. И удовольствие от чего-то сделанного своими руками от и до было получено немалое.
И еще раз подчеркну, что до этого ни опыта программирования микроконтроллеров, ни опыта работы с электроникой у меня почти не было. Поэтому, скорее всего я наделал ошибок, как в программной, так и в аппаратной части, за что прошу не судить строго.
Спасибо за внимание.
Автор: exp131