/////////////////////////////////////////////////////////////////////////// // litecontrol.c // // Copyright (C) 2002, Dale Botkin, all rights reserved. This code may // // not be reproduced commercially without prior written consent. // /////////////////////////////////////////////////////////////////////////// // Program to control low-voltage AC landscaping lights. The goal is to // // reproduce the functionality of an existing light controller by adding // // new abilities to a simple transformer power supply. Specifically: // // * Photocell detection of sunset // // * Automatic ON at sunset // // * Delay for user-defined number of hours before shutting off // // * Retain settings after power loss // // * Soft-start of lights to extend bulb life // // Power is drawn from the transformer, and MOSFETS are used to control // // power to the lights. A simple photocell is used to detect ambient // // light. // /////////////////////////////////////////////////////////////////////////// #include "litecontrol.h" struct time { char hours; char minutes; char seconds; } elapsed, set; int32 clock; short digit, daylight, now_on = FALSE; short was_light = TRUE; char dtimer = 30, tripdelay = 0; #bit now_light = 0x1f.7 // These are the patterns used for the 7-segment displays. const char sseg[11] = { 0b01101111, //0 0b00000110, //1 0b10101011, //2 0b10001111, //3 0b11000110, //4 0b11001101, //5 0b11101100, //6 0b00000111, //7 0b11101111, //8 0b11000111, //9 0b10101110 //d }; // Defualt setting is for lights on 6 hours after sunset #define DEF_ON_HOURS 6 #define EE_ON_HOURS 0 // EEPROM Locations #define EE_SAVED 1 // TIMER0 interrupt service routine. First we update the elapsed // time counter, then take care of the LED displays. #int_rtcc void tmr0_int() { char digit1, digit2; // This is part of the "zero-error" clock method proposed by // Roman Black on the PICList. We get a TIMER0 interrupt every // 8192 microseconds - 4MHz clock/4 = 1MHz T0 clock, with a /32 // prescaler, divided by 256 since we get an interrupt when T0 // overflows. Did you follow that? OK. So we load the clock // value with 1000 (cycles per second), then on each interrupt // subtract 8192 (cycles per interrupt). If there is less than // 8192 left, we add one second and add 10000 to the clock value. // That way any error in one second is compensated for in the // next - we can be off by 8191 uSec per second, but the // cumulative error over time should be zero, plus any inherent // clock frequency error (like 50ppm for a crystal, etc). We're // using the internal RC oscillator for this project which will // not be as accurate, but we're not terribly picky. clock -= 8192; restart_wdt(); if(clock < 8192) { elapsed.seconds++; clock += 1000000; if(dtimer) dtimer--; // The dtimer variable is the // number of seconds to keep the // LED display turned on. } if(elapsed.seconds == 60) { elapsed.minutes++; elapsed.seconds = 0; if(tripdelay) tripdelay--; // tripdelay is used to keep the // lights from turning off or on // due to short-lived shadows or // bright lights. } if(elapsed.minutes == 60) { elapsed.hours++; elapsed.minutes = 0; } if(set.hours == 25) { // set.hours=25 is a special condition digit1 = sseg[10]; // used to indicate we are in dusk to digit2 = sseg[10]; // dawn mode - so indicate "dd" on LEDs } else { digit1 = sseg[set.hours % 10]; // Otherwise indicate hours on LEDs. digit2 = sseg[set.hours/10]; // This splits the hours into two digits. } if(digit == TRUE) { // digit is used to select the first (tens) // or second (ones) digit. if(dtimer) // dtimer determines if the digits are output_b(digit1); // displayed or not. else output_b(0); // Blank if dtimer == 0 output_low(ones); output_high(tens); digit = FALSE; // Flip so we display the ones digit on the } else { // next interrupt. if(dtimer) output_b(digit2); else output_b(0); output_low(tens); output_high(ones); digit = TRUE; } } // This function checks to see if we are seeing daylight or not. We also use // tripdelay so we don't "see" short-term flucuations from headlights, flash- // lights, whatever. void check_light() { if(now_light) output_high(dayled); // now_light is the comparator // output comparing the photocell // to a potentiometer for level setting. else output_low(dayled); if(was_light && !now_light) { // Just got dark, start timer was_light = 0; tripdelay = 5; } else if(!was_light && now_light) { // Just got light, start timer was_light = 1; tripdelay = 10; } else if(!now_light && !tripdelay) { // Been dark 5 minutes or more if(daylight == TRUE) elapsed = 0; daylight = FALSE; } else if(now_light && !tripdelay) { // Been light 10 minutes or more daylight = TRUE; } } // We use this function to turn the lights on or off. We use a short PWM // ramp to turn the lights on and off gently - takes about a second to ramp // up or down. You can use a long variable and longer loops to stretch // this out if you want, but for exaple 750 is a pretty loooong ramp. void lights(char state) { char pulse, width; if((state == 0) && (now_on == TRUE)) { for(width=0;width<255;width++) { for(pulse=0;pulse<255;pulse++) { if(width <= pulse) output_high(MOSFET); else output_low(MOSFET); } } output_low(MOSFET); now_on = FALSE; } else if((state == 1) && (now_on == FALSE)) { for(width=0;width<255;width++) { for(pulse=0;pulse<255;pulse++) { if(pulse <= width) output_high(MOSFET); else output_low(MOSFET); } } output_high(MOSFET); now_on = TRUE; } } // here we check the state of the single button used to set the mode and // lights-on time. Any time the button is pushed we turn on the LEDs for 10 // seconds. Change dtimer value if you want a longer or shorter fuse. If // the button is held ON for half a second, we return an ASCII 'U' character // (historical reasons... an earlier vesion had U and D buttons) to the caller. // If it's a short push just to see the mode setting, there's no need to tell // the caller about it. char get_button(void) { char x = 0; if((input(button))) { // See if the button is pressed dtimer = 10; // If so, turn on LED displays delay_ms(500); // Delay half a second if((input(button))) // See if button is held x = 'U'; // If user hold button 1/2 sec // we assume he wants to change // the lights-on time } return x; } void main() { short changed = 0; port_b_pullups(FALSE); // No need for pullup resistors... setup_counters(RTCC_INTERNAL,RTCC_DIV_32); // This sets the T0 interrupt rate setup_timer_1(T1_DISABLED); setup_timer_2(T2_DISABLED,0,1); setup_vref(FALSE); setup_comparator(NC_NC_A1_A2); // We use one comparator enable_interrupts(INT_RTCC); enable_interrupts(global); clock = 1000000; // One second's worth of interrupts elapsed = 0; // Zero the elapsed time counter on reset output_low(MOSFET); // Start with lamps OFF if(read_eeprom(EE_SAVED) == 'Y') { // See if we have a time saved in EEPROM, set.hours = read_eeprom(EE_ON_HOURS); // if so use it, otherwise use the default. } else { set.hours = DEF_ON_HOURS; } while(1) { // Lather, rinse, repeat... check_light(); // See if it's light or dark out if(set.hours == 24) { // If set for 24 hours, turn lights ON lights(1); } else if(set.hours == 0) { // If set for 00 turn lights OFF lights(0); } else if(!daylight && set.hours == 25) { // Dusk to dawn mode... lights(1); }else if(daylight && set.hours == 25) { lights(0); // Next case is for if it's dark and we have // not yet reached the lights-on elapsed time } else if((!daylight) && (elapsed.hours < set.hours)) { lights(1); // Last case is for daylight OR if lights-on // elapsed time has expired. Note this is // part of an if-else so if we're set for // dusk-to-dawn, always ON or always OFF mode // we will never execute this. } else if((daylight) || (elapsed.hours >= set.hours)) { lights(0); } if(get_button() == 'U') { // See if the user is changing the mode or set.hours++; // delay setting... if(set.hours > 25) set.hours = 0; changed = TRUE; // if it's changed, flag to update EEPROM } if(changed) { // Save new settings in EEPROM. restart_wdt(); write_eeprom(EE_ON_HOURS, set.hours); write_eeprom(EE_SAVED, 'Y'); changed = FALSE; } } }