First, the schematic:
As I've already written about, the transistors I've been using are IRLML2502 which are rated at several amps each (assuming you don't let me get too hot) which is more than sufficient for each channel.
I've included a passably-accurate DS1307 RTC to allow me to define the starting and stopping time for the lights each day. It does drift, somewhere on the order of seconds a day and there is a fancy version of the driver for this IC that would allow me to custom-calibrate the device. For this purpose, not worth the effort.
Lastly, as you can see, I'm using a Mega and the reason is that, with a little bit of extra configuration code, you can enable several of the timers in 16-bit mode. This provides a greater number of steps when controlling the dimming of the lights with PWM.
And now for the code....
/**********************************************
* Written by Trevor Hardy 2016
*********************************************/
#include <PWM.h>
#include <Wire.h>
#include <Time.h>
#include <DS1307RTC.h>
/*******************************************************************
For LEDs, linear changes in brightness are not linear in value.
These constants below provide a linear increase in LED brightness
The brightness array below is zero-indexed. For example:
LED_brightness[0] = 0
LED_brightness[5] = 6
LED_brightness[33] = 61000
LED_brightness[34] = invalid
*******************************************************************/
const uint16_t linear16[33] = {0, 1, 2, 3, 4, 6, 8, 11, 16, 23, 32, 45, 64, 91, 128, 181, 256,\
362, 512, 724, 1024, 1448, 2048, 2896, 4096, 5792, 8192, 11585,\
16384, 23170, 32768, 46340, 61000};
//Pinouts of the three LED strips
const int s1Red = 44; //Timer 3 channel A
const int s1Green = 46; //Timer 3 channel C
const int s1Blue = 45; //Timer 3 channel B
const int s2Red = 8; //Timer 4 channel C
const int s2Green = 7; //Timer 4 channel B
const int s2Blue = 6; //Timer 4 channel A
const int s3Red = 5; //Timer 5 channel A
const int s3Green = 3; //Timer 5 channel B
const int s3Blue = 2; //Timer 5 channel C
//Delays
const int8_t step_delay = 75; //Time at each PWM step, ms
const unsigned int color_hold_time = 1000*3; //Time between fades, hold time at a given color.
const int32_t PWM_frequency = 131; //Frequency in Hz.
//Adjustment values used to mke the blue a little warmer
int blue_idx = 0; //Redefined inside each fade loop
const int blue_shift = 4; //Number of PWM steps down from the true max. blue_shift = 6 -> max PWM step = 8192
//Start and stop time
const int start_hour = 16;
const int start_minute = 00;
const int stop_hour = 7;
const int stop_minute = 30;
int run_lights = 0;
//Time-keeping variable
tmElements_t tm;
void setup() {
//initialize all timers except for 0, to save time keeping functions. Allow for 16-bit PWM natively
InitTimersSafe();
/*
Initializing freqency for all timers. Each timer affects all three channels (colors)
so only need to initilize one channel per group.
*/
//Blinking to indicate start of initialization
pinMode(13, OUTPUT);
digitalWrite(13, LOW);
delay(1000);
for (int idx = 0; idx < 2; idx++){
digitalWrite(13, HIGH);
delay(400);
digitalWrite(13, LOW);
delay(400);
}
delay(1000);
set_frequency(s1Red, PWM_frequency);
set_frequency(s2Red, PWM_frequency);
set_frequency(s3Red, PWM_frequency);
Serial.begin(9600);
pwmWriteHR(s1Red, 0);
pwmWriteHR(s1Green, 0);
pwmWriteHR(s1Blue, 0);
pwmWriteHR(s2Red, 0);
pwmWriteHR(s2Green, 0);
pwmWriteHR(s2Blue, 0);
pwmWriteHR(s3Red, 0);
pwmWriteHR(s3Green, 0);
pwmWriteHR(s3Blue, 0);
}
void loop() {
if (RTC.read(tm)) {
Serial.print("Ok, Time = ");
print2digits(tm.Hour);
Serial.write(':');
print2digits(tm.Minute);
Serial.write(':');
print2digits(tm.Second);
Serial.print(", Date (D/M/Y) = ");
Serial.print(tm.Day);
Serial.write('/');
Serial.print(tm.Month);
Serial.write('/');
Serial.print(tmYearToCalendar(tm.Year));
Serial.println();
} else {
if (RTC.chipPresent()) {
Serial.println("The DS1307 is stopped. Please run the SetTime");
Serial.println("example to initialize the time and begin running.");
Serial.println();
} else {
Serial.println("DS1307 read error! Please check the circuitry.");
Serial.println();
}
delay(9000);
}
delay(1000);
if ((tm.Hour == start_hour && tm.Minute >= start_minute) || (tm.Hour > start_hour) ||( tm.Hour < stop_hour) || (tm.Hour == stop_hour && tm.Minute < stop_minute)){
run_lights = 1;
}else if ((tm.Hour == stop_hour && tm.Minute >= stop_minute) || (tm.Hour > stop_hour) || (tm.Hour < start_hour) || (tm.Hour == start_hour && tm.Minute < start_minute)){
run_lights = 0;
}
Serial.print("run_lights: \t");
Serial.println(run_lights);
if (run_lights == 1){
//Christmas ramps
for(int idx=0; idx<=32; idx++){
blue_idx = max(0,idx-blue_shift);
//String 1 - Red to green
pwmWriteHR(s1Red, linear16[32-idx]);
pwmWriteHR(s1Green, linear16[idx]);
//String 2 - Green to white
pwmWriteHR(s2Red, linear16[idx]);
pwmWriteHR(s2Green, linear16[max(32-idx, idx)]);
pwmWriteHR(s2Blue, linear16[blue_idx]);
//String 3 - White to red
pwmWriteHR(s3Red, linear16[max(32-idx,idx)]);
pwmWriteHR(s3Green, linear16[32-idx]);
pwmWriteHR(s3Blue, linear16[32-blue_shift-blue_idx]);
delay(step_delay);
}
delay(color_hold_time);
for(int idx=0; idx<=32; idx++){
blue_idx = max(0,idx-blue_shift);
//String 1 - Green to white
pwmWriteHR(s1Red, linear16[idx]);
pwmWriteHR(s1Green, linear16[max(32-idx, idx)]);
pwmWriteHR(s1Blue, linear16[blue_idx]);
//String 2 - White to red
pwmWriteHR(s2Red, linear16[max(32-idx,idx)]);
pwmWriteHR(s2Green, linear16[32-idx]);
pwmWriteHR(s2Blue, linear16[32-blue_shift-blue_idx]);
//String 3 - Red to green
pwmWriteHR(s3Red, linear16[32-idx]);
pwmWriteHR(s3Green, linear16[idx]);
delay(step_delay);
}
delay(color_hold_time);
for(int idx=0; idx<=32; idx++){
blue_idx = max(0,idx-blue_shift);
//String 1 - White to red
pwmWriteHR(s1Red, linear16[max(32-idx,idx)]);
pwmWriteHR(s1Green, linear16[32-idx]);
pwmWriteHR(s1Blue, linear16[32-blue_shift-blue_idx]);
//String 2 - Red to green
pwmWriteHR(s2Red, linear16[32-idx]);
pwmWriteHR(s2Green, linear16[idx]);
//String 3 - Green to white
pwmWriteHR(s3Red, linear16[idx]);
pwmWriteHR(s3Green, linear16[max(32-idx, idx)]);
pwmWriteHR(s3Blue, linear16[blue_idx]);
delay(step_delay);
}
delay(color_hold_time);
}
else{
pwmWriteHR(s1Red, 0);
pwmWriteHR(s1Green, 0);
pwmWriteHR(s1Blue, 0);
pwmWriteHR(s2Red, 0);
pwmWriteHR(s2Green, 0);
pwmWriteHR(s2Blue, 0);
pwmWriteHR(s3Red, 0);
pwmWriteHR(s3Green, 0);
pwmWriteHR(s3Blue, 0);
}
}
//------------- Functions ----------------------
//Only used to initialize PWM timers
void set_frequency(int pin, int frequency){
bool success = SetPinFrequencySafe(pin, frequency);
//Use LED to indicate if successfully changed timer frequency
if(success) {
digitalWrite(13, HIGH);
delay(50);
digitalWrite(13, LOW);
delay(250);
}
}
void print2digits(int number) {
if (number >= 0 && number < 10) {
Serial.write('0');
}
Serial.print(number);
}
There is no glory in this code but it does work. As you can see the ramps are manually defined rather than through some cool function; maybe next year if we decide to do something more complicated. The visually linear stepping of the PWM follows is proportional to the root of two.
Though moving from 8 bit 16 bits does dramatically increase the number of steps, these extra bits only expand the low end of the dynamic range. My initial implementation was just using 8-bits and there was a clear need for dimmer values, thus the extra effort to turn on the 16-bit timers. The Uno has one or two 16-bit timers that can be enabled but I needed nine of them, thus the Mega (or the extra hassle of an external IC).
I still observe some steppy-ness at the brighter values and may end up adding in some extra steps along the curve for smoother transitions. This is just a matter of interpolating along the root-two curve and I've started very preliminary work on it already. At very low values interpolation is not possible as the required PWM values are already low-valued integers; the first five values are 0, 1, 2, 3, 4 which clearly do not follow a root-two progression. Thankfully, there's plenty of integers available at the bright end of the scale. Adding in extra values will introduce some non-linearity in the fade but I'm hoping it won't be too noticeable.
All three of the windows this year are being controlled by a single Mega. If I was to expand to other windows and wished to coordinate them, it would require coordination among the Mega's in some form. Getting a high-accuracy clock with very low drift might be enough (a cheap GPS clock would be sufficient) or using a simple radio-based protocol would do the trick as well. Either way, new hardware would be required; we'll see what my wife and I feel is appropriate.