Friday, December 23, 2016

Christmas Lights - Final Technical Details

Since I've already posted here a short article about the completion of this project weeks ago, I figured it was probably time to provide all the details.

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.

No comments:

Post a Comment