
Для освещения аквариума, Kalle Hyvönen использовал четыре мощных белых светодиода (закреплённых на паре радиаторов), а для более интеллектуального освещения, он подключил их к контроллеру Arduino, который управляет импульсным источником питания светодиодов, с помощью PWM. 
Также, автор реализовал возможность автоматического переключения света и возможность изменения графика освещения при помощи команд через последовательный порт (USB).
Скетч:
#include <SPI.h>
#include <EEPROM.h>
#include "pins_arduino.h"
// RTC registry addresses
#define HOURS_WRITE 0x82
#define HOURS 0x02
#define MINUTES_WRITE 0x81
#define MINUTES 0x01
#define SECONDS_WRITE 0x80
#define SECONDS 0x00
#define ALARM2_HOURS_WRITE 0x8C
#define ALARM2_HOURS 0x0C
#define ALARM2_MINUTES_WRITE 0x8B
#define ALARM2_MINUTES 0x0B
#define ALARM1_HOURS_WRITE 0x89
#define ALARM1_HOURS 0x09
#define ALARM1_MINUTES_WRITE 0x88
#define ALARM1_MINUTES 0x08
#define ALARM1_SECONDS_WRITE 0x87
#define ALARM1_SECONDS 0x07
#define ALARM1_DATE_WRITE 0x8A
#define ALARM1_DATE 0x0A
#define CONTROL_WRITE 0x8E
#define CONTROL 0x0E
#define STATUS_WRITE 0x8F
#define STATUS 0x0F
// define pin connected to the RTC alarm (!INT) pin
#define ALARM 2
// define the pin which is connected to the LED driver
#define LED 3
// this sets the maximum PWM level for brightness
int max_brightness = 255;
// this sets the timing period, ie. for how long the lights are on (in hours)
char timing_period = 11;
// variable so we know if we are going to increase or decrease the PWM value
bool direction_up = true;
// this variable sets how many interrupts (they come in once a second) it takes to change the PWM value
int pwm_speed = 3;
// these variables not for the user to modify
int brightness = 0;
char incomingByte = 0x00;
int interrupt_cycle = 0;
bool enabled = true;
void setup() {
	noInterrupts();
// define IO
	pinMode(ALARM, INPUT);
	pinMode(LED, OUTPUT);
	digitalWrite(ALARM, HIGH);
	digitalWrite(LED, LOW);
	pwm_speed = EEPROM.read(0x01);
	timing_period = EEPROM.read(0x02);
// check if the eeprom was empty or not
	if(pwm_speed == 0xff) { pwm_speed = 0; }
	if(timing_period == 0xff) { timing_period = 11; }
// start UART
	Serial.begin(9600);
// set SPI mode 3
	SPI.setDataMode(SPI_MODE3);
// set SPI clock to system clock / 16 ie. 1MHz
	SPI.setClockDivider(SPI_CLOCK_DIV16);
// enable SPI
	SPI.begin();
	if(EEPROM.read(0x00) == 0) {
		enabled = false;
		enableAlarm1(false);
		enableAlarm2(false);
		brightness = 0;
		invAnalogWrite(LED, brightness);
	}
	else {
		enabled = true;
		enableAlarm1(true);
		enableAlarm2(true);
// set A2M4 bit (needed for alarm)
		writeRTC(0x8D, 0x80);
// disable both alarm flags, leave other bits untouched
		writeRTC(STATUS_WRITE, (readRTC(STATUS)&0xFC));
// check the time and see if we should put the lights on or off
		checkTime();
	}
// attach RTC alarm to interrupt on pin 2
	attachInterrupt(0, changePWM, FALLING);
	interrupts();
}
void loop() {
	if (Serial.available() > 0) {
// read the incoming byte:
		incomingByte = Serial.read();
		switch(incomingByte) {
// if we get the symbol "t", we want to read the temperature from the RTC
			case 0x74:
				Serial.flush();
				initTempConv();
// wait for the conversion to finish
				delay(250);
				Serial.print("Temperature: ");
				Serial.print(readRTC(0x11), DEC);
				Serial.print(".");
// figure out the decimals, does not work, needs fixing...
				switch (readRTC(0x12)) {
					case 0x00:
						Serial.print("0");
						break;
					case 0x40:
						Serial.print("25");
						break;
					case 0x80:
						Serial.print("50");
						break;
					case 0xC0:
						Serial.print("75");
						break;
				}
				Serial.println("C");
				break;
// if we receive symbol "h" which has to be followed by an ascii number value for the hours (eg. 01 or 13)
			case 0x68:
				incomingByte = hoursToBCD(readAsciiHours());
				checkAndWrite(HOURS_WRITE, incomingByte);
				printTime();
				checkTime();
				break;
// if we receive symbol "m" which has to be followed by a value for the minutes register (the amount of minutes straight in hex, like 0x40 is 40 minutes etc.)
			case 0x6D:
				incomingByte = minutesToBCD(readAsciiMinutes());
				checkAndWrite(MINUTES_WRITE, incomingByte);
				printTime();
				checkTime();
				break;
// if we receive symbol "s" which has to be followed by a value for the seconds register (the amount of seconds straight in hex)
			case 0x73:
				incomingByte = minutesToBCD(readAsciiMinutes());
				checkAndWrite(SECONDS_WRITE, incomingByte);
				printTime();
				break;
// if we receive symbol "a" which has to be followed by an int value for the alarm2 hours (ie. convert decimal to hex)
			case 0x61:
				incomingByte = hoursToBCD(readAsciiHours());
				checkAndWrite(ALARM2_HOURS_WRITE, incomingByte);
				printAlarm2();
				checkTime();
				break;
// if we receive symbol "b" which has to be followed by a value for the alarm2 minutes register (the amount of minutes straight in hex)
			case 0x62:
				incomingByte = minutesToBCD(readAsciiMinutes());
				checkAndWrite(ALARM2_MINUTES_WRITE, incomingByte);
				printAlarm2();
				checkTime();
				break;
// if we receive symbol "c" which has to be followed by the timing_period value as ascii
			case 0x63:
				incomingByte = readAsciiHours();
				if(incomingByte > 0) {
					timing_period = incomingByte;
					EEPROM.write(0x02, timing_period);
					checkTime();
				}
				printTp();
				break;
// if we receive symbol "d" which has to be followed by the pwm_speed value as two-number ascii (eg "01")
			case 0x64:
				incomingByte = readAsciiMinutes();
				pwm_speed = incomingByte;
				EEPROM.write(0x01, pwm_speed);
				printPwmSpeed();
				break;
// if we get symbol "e", disable or enable the alarms
			case 0x65:
				if(enabled == true) {
					enableAlarm1(false);
					enableAlarm2(false);
					Serial.println("Disabled");
					enabled = false;
					EEPROM.write(0x00, 0);
					brightness = 0;
					invAnalogWrite(LED, brightness);
				}
				else {
					enableAlarm1(true);
					enableAlarm2(true);
					Serial.println("Enabled");
					enabled = true;
					EEPROM.write(0x00, 1);
					checkTime();
				}
				break;
// if we receive symbol "p" which prints the current time and current alarm2 values to serial port
			case 0x70:
				printTime();
				printAlarm2();
				printAlarm1();
				printTp();
				printPwmSpeed();
				if(enabled = true) {
					Serial.println("Enabled: yes");
				}
				else {
					Serial.println("Enabled: no");
				}
				break;
// if we get LF ('\n') just do nothing and ignore it
			case 0x0A:
				break;
			default:
				Serial.flush();
// return input if it makes no sense and print some help
				Serial.println("Incorrect input");
				Serial.println("Correct input  (replace 00 with value):");
				Serial.println("h00, m00, s00 to change the time");
				Serial.println("a00, b00 to change the time of alarm");
				Serial.println("c00 to change the timing period");
				Serial.println("d00 to change the dimming speed");
				Serial.println("p to print current values");
				Serial.println("t to print temperature");
				Serial.println("e to enable or disable");
		}
	}
}
void changePWM() {
// disable both alarm flags, leave other bits untouched
	writeRTC(STATUS_WRITE, (readRTC(STATUS)&0xFC));
	switch (direction_up) {
		case true:
			if(brightness == 0) {
// pwm_speed variable does not affect the first cycle
				setAlarm1_every_second(true);
				enableAlarm1(true);
				brightness++;
				invAnalogWrite(LED, brightness);
			}
			else {
// if not enough interrupts have not occurred after last pwm change, just increase the interrupt_cycle timer
				if(interrupt_cycle == pwm_speed) {
					brightness++;
					interrupt_cycle = 0;
					if(brightness == max_brightness) {
						invAnalogWrite(LED, brightness);
						direction_up = false;
						setAlarm1_every_second(false);
// we are at full brightness, set alarm1 to timing_period ahead of alarm2
						setAlarm1();
					}
					else { invAnalogWrite(LED, brightness); }
				}
				else { interrupt_cycle++; }
			}
			break;
		case false:
			if(brightness == max_brightness) {
				setAlarm1_every_second(true);
				enableAlarm1(true);
				brightness--;
				invAnalogWrite(LED, brightness);
			}
			else {
				if(interrupt_cycle == pwm_speed) {
					brightness--;
					interrupt_cycle = 0;
					if(brightness == 0) {
						direction_up = true;
						enableAlarm1(false);
						invAnalogWrite(LED, brightness);
					}
					else { invAnalogWrite(LED, brightness); }
				}
				else { interrupt_cycle++; }
			}
			break;
	}
}
void checkAndWrite(char reg, char val) {
	if(val >= 0x00) {
		writeRTC(reg, val);
	}
	else {
		Serial.println("Incorrect input");
	}
	Serial.flush();
}
// check the time and if we should put the lights on or off
void checkTime() {
	int lighting_time = 0;
	int time = 0;
	lighting_time = convertTime(readRTC(ALARM2_HOURS), readRTC(ALARM2_MINUTES)) + (timing_period * 60);
	time = convertTime(readRTC(HOURS), readRTC(MINUTES));
	if(lighting_time >= 1440) {
  		lighting_time -= 1440;
	}
// if the time is inside the lighting period, fire up the lights
	if(time < lighting_time && time > convertTime(readRTC(ALARM2_HOURS), readRTC(ALARM2_MINUTES))) {
// make sure we are going to dim the lights next
		direction_up = false;
		brightness = max_brightness;
		invAnalogWrite(LED, brightness);
// set control bits, enable both of the alarms
		writeRTC(CONTROL_WRITE, 0x07);
// set alarm1 to timing_period ahead of alarm2
		setAlarm1();
// set A1M4 bit to enable hours, minutes, seconds match for alarm1
		setAlarm1_every_second(false);
	}
	else {
		direction_up = true;
		brightness = 0;
		invAnalogWrite(LED, brightness);
// disable alarm1
		enableAlarm1(false);
	}
}
char convertAsciiNumber(char character) {
// deduct 0x30 from the ascii character to get a number
	return (character - 0x30);
}
// converts hours from BCD to int
char convertHours(char hours) {
	char temp = 0x00;
// check tens of hours
	if(((hours >> 4)&1) == 1) {
		temp += 10;
	}
// check 20h
	if(((hours >> 5)&1) == 1) {
		temp += 20;
	}
// add all the remaining hours
	temp += hours&0x0F;
	return temp;
}
// converts time from register value to an int which contains amount of minutes passed
int convertTime(char hours, char minutes) {
	int time = 0;
// check tens of hours
	if(((hours >> 4)&1) == 1) {
		time += 600;
	}
// check 20h
	if(((hours >> 5)&1) == 1) {
		time += 1200;
	}
// add all the remaining hours
	time += (hours&0x0F)*60;
	time += (minutes >> 4)*10;
	time += minutes&0x0F;
	return time;
}
void enableAlarm1(bool val) {
	switch (val) {
		case true:
			writeRTC(CONTROL_WRITE, (readRTC(CONTROL)|0x01));
			break;
		case false:
// disable alarm1
			writeRTC(CONTROL_WRITE, (readRTC(CONTROL)&0xFE));
			break;
	}
}
void enableAlarm2(bool val) {
	switch (val) {
		case true:
			writeRTC(CONTROL_WRITE, (readRTC(CONTROL)|0x02));
			break;
		case false:
// disable alarm2
			writeRTC(CONTROL_WRITE, (readRTC(CONTROL)&0xFD));
			break;
	}
}
// converts hours as int to hours as BCD
char hoursToBCD(char hours) {
	char temp = 0x00;
	if(hours >= 20) {
  // switch 20h bit
		temp = temp|0x20;
		hours -= 20;
	}
	else if(hours >= 10) {
		temp = temp|0x10;
		hours -= 10;
	}
// add rest of the hours
	temp += hours;
	return temp;
}
void initTempConv() {
	writeRTC(CONTROL_WRITE, (readRTC(CONTROL)|0x20));
}
void invAnalogWrite(int pin, int value) {
	analogWrite(pin, ~value);
}
char minutesToBCD(char minutes) {
	char temp = 0x00;
// take off tens of minutes and add to temp until we have 10min or less
	while(minutes >= 10) {
		minutes -= 10;
		temp++;
	}
// shift to make them tens of minutes in bcd
	temp = temp << 4;
// add rest of the minutes
	if(minutes > 0) {
		temp += minutes;
	}
	return temp;
}
void printAlarm1() {
	Serial.print("Current alarm1: ");
	Serial.print(convertHours(readRTC(ALARM1_HOURS)), DEC);
	Serial.print(":");
// while printing the hours, ignore the config bit
	Serial.print((readRTC(ALARM1_MINUTES)&0x7F), HEX);
	Serial.print("\n");
}
void printAlarm2() {
	Serial.print("Current alarm: ");
	Serial.print(convertHours(readRTC(ALARM2_HOURS)), DEC);
	Serial.print(":");
	Serial.print(readRTC(ALARM2_MINUTES), HEX);
	Serial.print("\n");
}
void printTime() {
	Serial.print("Current time: ");
	Serial.print(convertHours(readRTC(HOURS)), DEC);
	Serial.print(":");
	Serial.print(readRTC(MINUTES), HEX);
	Serial.print(":");
	Serial.print(readRTC(SECONDS), HEX);
	Serial.print("\n");
}
void printPwmSpeed() {
	Serial.print("Current PWM speed: ");
	Serial.print(pwm_speed, DEC);
}
void printTp() {
	Serial.print("Current timing period: ");
	Serial.print(timing_period, DEC);
}
char readAsciiHours() {
	char number1 = 0x00;
	char number2 = 0x00;
	char number = 0x00;
// convert user input to an int value
	number1 = convertAsciiNumber(waitForByte());
	number2 = convertAsciiNumber(waitForByte());
	number = 10*number1 + number2;
// check if the input makes any sense
	if(number >= 24 || number < 0) {
		Serial.println("Incorrect input");
		number = 0x00;
	}
	return number;
}
// this works for seconds as well
char readAsciiMinutes() {
	char number1 = 0x00;
	char number2 = 0x00;
	char number = 0x00;
// convert user input to an int value
	number1 = convertAsciiNumber(waitForByte());
	number2 = convertAsciiNumber(waitForByte());
	number = 10*number1 + number2;
// check if the input makes any sense
	if(number > 60 || number < 0) {
		Serial.println("Incorrect input");
		number = 0x00;
	}
	return number;
}
char readRTC(char reg) {
	noInterrupts();
	char temp = 0x00;
	digitalWrite(SS, LOW);
	SPI.transfer(reg);
// dont write anything, just put out clock signal and read
	temp = SPI.transfer(0x00);
	digitalWrite(10, HIGH);
	interrupts();
	return temp;
}
// set alarm1 to timing_period ahead of alarm2
void setAlarm1() {
	char hours = 0x00;
// calculate hours for timer1 hours
	hours = (convertHours(readRTC(ALARM2_HOURS)) + timing_period);
// if hours more than 24, deduct 24 for correct time
	if(hours >= 24) {
		hours -= 24;
	}
// make sure we dont touch the A1M4 bit
	writeRTC(ALARM1_HOURS_WRITE, ((readRTC(ALARM1_HOURS)&0x80)|hoursToBCD(hours)));
// set alarm1 minutes to same as alarm2, just the hours are alarm2_hours + timing period
	writeRTC(ALARM1_MINUTES_WRITE, ((readRTC(ALARM1_MINUTES)&0x80)|readRTC(ALARM2_MINUTES)));
// set alarm1 seconds to 0
	writeRTC(ALARM1_SECONDS_WRITE, ((readRTC(ALARM1_SECONDS)&0x80)));
	hours = 0x00;
}
void setAlarm1_every_second(bool val) {
	switch (val) {
		case true:
// change A1M1 bit via bitmask to make alarm1 give alarms every second
			writeRTC(ALARM1_SECONDS_WRITE, (readRTC(ALARM1_SECONDS)|0x80));
// change A1M2 bit via bitmask
			writeRTC(ALARM1_MINUTES_WRITE, (readRTC(ALARM1_MINUTES)|0x80));
// change A1M3 bit via bitmask
			writeRTC(ALARM1_HOURS_WRITE, (readRTC(ALARM1_HOURS)|0x80));
// change A1M4 bit via bitmask
			writeRTC(ALARM1_DATE_WRITE, (readRTC(ALARM1_DATE)|0x80));
			break;
		case false:
// change A1M1 bit via bitmask to make alarm1 give alarms when hours, seconds, and minutes match
			writeRTC(ALARM1_SECONDS_WRITE, (readRTC(ALARM1_SECONDS)&0x7F));
// change A1M2 bit via bitmask
			writeRTC(ALARM1_MINUTES_WRITE, (readRTC(ALARM1_MINUTES)&0x7F));
// change A1M3 bit via bitmask
			writeRTC(ALARM1_HOURS_WRITE, (readRTC(ALARM1_HOURS)&0x7F));
			break;
	}
}
char waitForByte() {
	char temp = 0x00;
// wait for the UART to fill up
	while(Serial.available() == 0) { }
	temp = Serial.read();
	return temp;
}
void writeRTC(char reg, char val) {
	noInterrupts();
	digitalWrite(SS, LOW);
// select register
	SPI.transfer(reg);
// print value to register
	SPI.transfer(val);
	digitalWrite(SS, HIGH);
	interrupts();
}
Ссылки
LED aquarium lighting with an Arduino based PWM timer
