Ещё одна фитолампа на Arduino
Пришла зима, короткий день, домашней пальме мало света. Нужно организовать подсветку. Готовую лампу покупать как-то неловко, да и надо ж чем-то заняться долгими зимними вечерами. Поехали ;)
1. Купил на Али мощные светодиоды с питанием в 220В. Вот такие:
Отзывы оказались верными: греются, и очень сильно. Нужно огранизовать охлаждение. Пассивного (прикрутил к дюралевому профилю) недостаточно, значит, нужно придумать что-то активное. Например, радиатор с кулером от старого процессора - это недорого, и их как грязи. Правда, где кулер - там питание 12В. Ок, это тоже несложно. Но при питании от 12 В кулер (особенно недорогой бу) будет достаточно громко шуметь.
2. Регулятор оборотов кулера
Пусть это будет step-down конвертор. Хороший КПД, меньше помех в цепях питания.
Ключевой транзистор и диод выпаял из покойной материнской платы, дроссель оттуда же. Оригинальную обмотку из толстого провода срезал, намотал свою проводом 0.3мм - сколько поместилось. Транзистор Q1 - какой-то маломощный npn из закромов. Подаем на вход импульсы разной скважности - на выходе получаем напряжение от 0 до 12В. Удобно, но нужен генератор импульсов с достаточно высокой частотой (десятки кГц и выше).
3. Как сделать генератор импульсов со скважностью от 0 до 100%?
Причем с возможностью автоматической регулировки? Проще всего взять микроконтроллер, который умеет в PWM. Я склоняюсь к AtMega8 - простой, отлично документированный и дешевый. Но использовать микроконтроллер только для вращения кулера - это из пушки по воробьям. Можно на этом же железе решить и дополнительные задачи.
Подсветку неплохо автоматически включать и выключать. Напрашивается функция часов/будильника, но делать часы (и усложнять простое устройство) не хочется. В часах нужен индикатор (или веб-интерфейс), нужен способ синхронизации (ручной или через интернет), нужен резервный источник питания. Но в лампе всего этого не нужно. Поэтому алгоритм будет такой:
подсветка работает 8 часов и отключается по таймеру или кнопкой;
включается подсветка по кнопке или с наступлением рассвета;
рассвет детектируется светодиодом. Для этого функционала достаточно фоторезистора и АЦП.
Кулер желательно крутить помедленнее, но не допуская ни полной остановки, ни перегрева светодиода. Для детектирования скорости вращения у кулера есть встроенный датчик, для измерения температуры возьмем терморезистор. Мне попался NTC MF52A1, с сопротивлением при 25С 10кОм. В даташите есть таблица с зависимостью сопротивления от температуры; так что можно сделать простой и очень дешевый измеритель температуры с приемлемой точностью. Итого, от контроллера нужно:
АЦП - 2 входа
PWM - 1 выход
прерывание датчика вращения кулера - 1 вход
кнопка вкл/выкл - 1 вход
управление подсветкой - 1 выход
UART - удобно для диагностики в железе
4. Конфигурирование микроконтроллера
Fuse bits.
Чтобы не получить окирпиченный МК, проще всего воспользоваться готовым калькулятором fuse битов. Например, таким. Главное, что нужно - выбрать синхронизацию от внешнего кварца. Остальные биты можно не трогать ;) У меня получилось
lfuse | 0xFF |
hfuse | 0xD9 |
Таймеры
Во первых, PWM. Для этого пригодны 2 таймера: 16-битный Timer1 и 8-битный Timer2. Timer1 будет считать системное время (об этом позже), Timer2 (с 256 уровнями PWM) - отлично подходит для регулятора напряжения. При тактовой частоте 16 МГц максимальная частота PWM будет:
Длительность самого короткого импульса:
Думаю, это приемлемо. В даташите на ключевой транзистор есть Rise Time 33 nS. В окончательном варианте устройства можно использовать встроенный генератор на 8МГц - это тоже сработает.
Настройка Timer2
См https://sites.google.com/site/qeewiki/books/avr-guide/pwm-atmega8
Нам нужны следующие функции:
No Prescaling - чтобы получить частоту повыше
Fast PWM
Inverted mode (LOW at bottom, HIGH on Match) - преобразователь напряжения с инвертором; когда напряжение на выходе прямо зависит от значения счетчика - это удобно.
// PWM
// Выход жестко приколочен к пину PB3
#define PWM_FAN_PIN _BV(PB3)
DDRB |= PWM_FAN_PIN;
OCR2 = 255;
// set PWM for 100% duty cycle
TCCR2 |= (1 << COM21);
TCCR2 |= (1 << COM20);
// set inverting mode
TCCR2 |= (1 << WGM21) | (1 << WGM20);
// set fast PWM Mode
TCCR2 |= (1 << CS20);
// set prescaler to 0 and starts PWM
Измерение времени
В Arduino принято измерять время в миллисекундах - с такой частотой тикает таймер. Такая точность для наших целей не нужна, да и милисекундный счетчик переполняется через 49 суток. Мы будем считать интервалы по 100 мС, этот счетчик переполнится очень не скоро.
Чтобы получить такие временные интервалы с кварцем 16 МГц, нужен предделитель 256 и счетчик до 6250. Прекрасно.
Настройка Timer1
См https://sites.google.com/site/qeewiki/books/avr-guide/timer-on-the-atmega8
// 16000000/256/6250 = 10 hz
// timer 1, CTC, 256 prescaler
TCCR1B |= (1 << CS12) | (1 << WGM12);
TIMSK |= (1 << OCF1A); // enable interrupt
OCR1A = 6250;
Код счетчика времени очень простой:
uint32_t uptime_x_0_1s = 0;
ISR (TIMER1_COMPA_vect)
{
// action to be done every 0.1 sec
uptime_x_0_1s++;
}
Переполнится счетчик через
Больше чем достаточно ;)
Измерение частоты вращения кулера
В кулере есть встроенный датчик, который замыкается на землю 1 раз (или 2 раза - разные источники пишут разное) за оборот. Для измерения частоты вращения подключим датчик к пину, умеющему генерировать прерывания, сконфигурируем его как вход с внутренним pull-up резистором.
Прерывания от кулера
См http://www.atmega8.ru/wiki/view/doc.9.html
// FAN interrupt
#define TAHO_FAN_PIN _BV(PD3)
SREG |= (1<<7);
PORTD |= TAHO_FAN_PIN; // pull-up
GIMSK |= _BV(INT1);
MCUCR |= _BV(ISC11); // Прерывание вызывается по возрастающему фронту сигнала на входе INT
Счетчик прерываний совсем простой:
uint16_t ts_fan_interrupt;
ISR (INT1_vect)
{
/* interrupt code here */
// убираем дребезг - на совсем малых оборотах
// он почему-то наблюдался
if (TCNT1 - ts_fan_interrupt <10) return;
ts_fan_interrupt = TCNT1;
fan_cnt ++;
}
Функция do_fan_rps срабатывает не чаще чем 1 раз в секунду и подсчитывает число прерываний за 1 секунду.
unsigned int rps = 0;
void do_fan_rps()
{
static uint32_t rps_ts;
unsigned int fan_cnt_;
int dt = uptime_x_0_1s - rps_ts;
if ( dt < 10) return;
cli();
fan_cnt_ = fan_cnt;
fan_cnt = 0;
sei();
rps = fan_cnt_ * 10 /dt ;
rps_ts = uptime_x_0_1s;
}
АЦП будем использовать для измерения температуры и освещенности. АЦП в AtMega 10-разрядный, и он может измерять 1024 уровня от 0 до опорного напряжения Vref. 2 младших разряда можно отбросить, и работать с результатом в uint8_t - это экономит немного памяти. Имеется также встроенный источник опорного напряжения 2.56 В.
Обработка АЦП
См https://narodstream.ru/avr-urok-22-izuchaem-acp-chast-1/
ADCSRA |= (1<<ADEN); // Разрешение использования АЦП
ADCSRA |=(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);//Делитель 128 = 125 кГц
ADMUX |= (1<<REFS1)|(1<<REFS0)|(1<<ADLAR); //Внутренний Источник ОН 2,56в, adc0, use high 8b (ADCH)
Чтение из АЦП процесс медленный, лучше им не злоупотреблять
uint8_t ADC_read(uint8_t channel)
{
ADMUX &= ~7;
ADMUX |= (channel & 7);
ADCSRA |= (1<<ADSC); //Начинаем преобразование
while((ADCSRA & (1<<ADSC))); //проверим закончилось ли аналого-цифровое преобразование
return ADCH;
}
Измеритель температуры представляет собой резисторный делитель напряжения. Зависимость напряжения от температуры, скорее всего, можно представить в виде формулы. Но проще составить таблицу соответствия температуры и напряжения с точностью 1 С - размер ее не будет слишком велик. Я взял datasheet на терморезистор, librecalc, закон Ома и получил что-то такое:
uint8_t adc_measures_base24[] = { 255, 250, 245, 239, 234, 228, 223, 218, 213, 207, 202, 197, 192, 188, 183, 178, 173, 169, 164, 160, 156, 151, 147, 143, 139, 135, 132, 128, 124, 121, 117, 114, 111, 108, 105, 102, 99, 96, 93, 90, 88, 85, 83, 80, 78, 76, 73, 71, 69, 67, 65, 63, 61, 60, 58, 56, 55, 53, 52, 50, 49, 47, 46, 45, 43, 42, 41, 40, 39, 38, 36, 35, 34, 33, 33, 32, 31, 30, 29, 28, 28, 27, 26, 25, 25, 24, 23, 23, 22, 22, 21, 20, 20, 19, 19, 18, 18, 18, 17, 17, 16, 16};
Температуру ниже 24С и выше 120С измерять не имеет смысла.
UART. Как оказалось, здесь тоже имеются свои тонкости. Передача данных через UART требует высокой точности временных интервалов; поэтому использовать UART без внешнего кварца не стоит. Также, не любую частоту передачи можно получить с помощью встроенных делителей частоты. Очень удобно, что есть готовая таблица, из которой можно найти скорость с минимальной погрешностью. У меня уверенно заработала передача на 38400bps.
Настройка UART
#define BAUD 38400
#define MY_UBRR (F_CPU/16/BAUD - 1)
// UART
UBRRH = (unsigned char) ((MY_UBRR) >> 8);
UBRRL = (unsigned char) (MY_UBRR);
UCSRB = (1<<TXEN);
UCSRC = (1<<URSEL)|(1<<UCSZ0)|(1<<UCSZ1);
PID-регулятор
Что ж за самоделка без PID-регулятора? ;) Здесь из 2. Нужно поддерживать температуру около 40С, и обороты около 30 rps. 2 датчика (цифровой и аналоговый), и одно управляющее воздействие - скважность PWM.
Код регулятора
#define GOAL_T 40
#define GOAL_RPS 30
// K_T = 1/2
#define K_T_A 1
#define K_T_B 2
// K_RPS = 1/5
#define K_RPS_A 1
#define K_RPS_B 5
void do_pid()
{
static uint32_t pid_ts;
if (uptime_x_0_1s-pid_ts < 10) return; // не чаще 1 раз/сек
pid_ts = uptime_x_0_1s;
int pwm_val = OCR2;
int dt = t1 - GOAL_T;
int d_rps = GOAL_RPS - rps;
int d_pwm_t = (dt * K_T_A ) / K_T_B;
if (d_pwm_t < -5) {
d_pwm_t = -5;
};
int d_pwm_rps = (d_rps * K_RPS_A) / K_RPS_B;
int pwm_val_t = pwm_val + d_pwm_t;
int pwm_val_rps = pwm_val + d_pwm_rps;
int d_pwm;
if (pwm_val_t > pwm_val_rps)
{
d_pwm = d_pwm_t;
} else {
d_pwm = d_pwm_rps;
};
pwm_val += d_pwm;
if (pwm_val > 255) pwm_val = 255;
if (pwm_val <0 ) pwm_val = 0;
OCR2 = pwm_val;
}
Коэффициенты для обоих регуляторов заданы в виде натуральной дроби, чтобы не использовать float вычисления.
Про обработку кнопки с подавлением дребезга и прочее мигание светодиодом, пожалуй, писать не нужно ;)
В результате получилась вот такая конструкция на макетной плате.
Шакальное фото
Выводы
Поставленная задача решается: светодиод светится, кулер крутится, температура 40С держится с достаточно низкими оборотами кулера; можно брать и более мощные светодиоды.
Код прошивки и схема устройства выложены на github.
TODO:
Научиться разводить платы в KiCad и сделать более изящную конструкцию.
Избавиться от кварца; точности встроенного генератора должно хватить для измерения интервала 8ч.
Вместо step-down конвертора использовать источник питания 5В и step-up конвертор.
Сделать то же самое, но на AtTiny - ресурсов должно хватить.
ЗЫ: Пожалуй, нужно упомянуть о использованном софте. При работе над устройством ни один правообладатель не пострадал.
ОС - Ubuntu 18
Компилятор - avr-gcc
IDE - geany
Программатор - Arduino Uno с прошивкой ArduinoISP
Схема электрическая принципиальная рисовалась в KiCad
Прототип паялся на макетной плате.