Пришла зима, короткий день, домашней пальме мало света. Нужно организовать подсветку. Готовую лампу покупать как-то неловко, да и надо ж чем-то заняться долгими зимними вечерами. Поехали ;)

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 будет:

16000000/256=62500 Гц

Длительность самого короткого импульса:

1/16МГц=62 нС

Думаю, это приемлемо. В даташите на ключевой транзистор есть 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 мС, этот счетчик переполнится очень не скоро.

16000000/256/6250 = 10 Гц

Чтобы получить такие временные интервалы с кварцем 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++;
}

Переполнится счетчик через

2^{32}/3600/24/365/10 = 13 лет

Больше чем достаточно ;)

Измерение частоты вращения кулера

В кулере есть встроенный датчик, который замыкается на землю 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

Прототип паялся на макетной плате.