//-------------------------------------------------------------------------------------
// RampingIOS - simple ramping UI for the Atmel ATTiny85/ATTiny85V
// ==========
//
// Copyright (C) 2017 Tom Elfers and Selene Scriven
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
//
// originally based on STAR_momentary.c v1.6 from JonnyC as of Feb 14th, 2015, Modified by Tom E
//
// Capabilities:
// - full e-switch "only" support with a low standby power mode (low parasitic drain)
// - lock-out feature - uses a special sequence to lock out the e-switch (prevent accidental turn-on's)
// - tactical mode, beacon mode
// - LVP and temperature regulation using the internal temp sensor
// - no voltage divider resistors, very low parasitic drain
//
//#define ATTINY 412
#define F_CPU 3333333UL
#define EEPSIZE 128
#define V_REF REFS1
#define BOGOMIPS 833
#define byte uint8_t
#define word uint16_t
// Ignore a spurious warning, we did the cast on purpose
#pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
//------------- Driver Board Settings -------------------------------------
//
#define VOLTAGE_MON // Comment out to disable - ramp down and eventual shutoff when battery is low
//
//------------- End Driver Board Settings ---------------------------------
//-------------------------------------------------------------------------
// Settings to modify per driver
//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
// v1.1 (in bits: mmm.nnnnn, mm=major @, nnnnn=minor#)
#define FIRMWARE_VERS ((1<<5) | 1)
// Choose one or the other below. Neither set will be normal power-up in OFF state
//#define STARTUP_LIGHT_ON // powerup/startup at max level
#define STARTUP_2BLINKS // enables the powerup/startup two blinks
#define USE_LVP // enables Low Voltage Protection
// Temperature monitoring setup
//-----------------------------
// Temperature Calibration Offset - tiny85 chips vary per-chip
#define TEMP_CAL_OFFSET (0) // err on the safe side for factory production purposes
// -12 is about right on the DEL DDm-L4 board in the UranusFire C818 light
// -11 On the TA22 board in SupFire M2-Z, it's bout 11-12C too high,reads 35C at room temp, 23C=73.4F
// -8 For the Manker U11 - at -11, reads 18C at 71F room temp (22C)
// -2 For the Lumintop SD26 - at -2, reading a solid 19C-20C (66.2F-68F for 67F room temp)
#define MAX_THERM_CEIL 75
#define DEFAULT_THERM_CEIL 57
#define TEMP_CRITICAL (DEFAULT_THERM_CEIL) // Temperature in C to step the light down
// (40C=104F, 45C=113F, 50C=122F, 55C=131F, 60C=140F)
// use 50C for smaller size hosts, or a more conservative level (SD26, U11, etc.)
// use 55C for larger size hosts, maybe C8 and above, or for a more aggressive setting
#define TEMP_ADJ_PERIOD 32 // Thermal regulation frequency: 0.5 sec(s) (in 16 msec ticks)
//-----------------------------
//#define USING_3807135_BANK // (default OFF) sets up ramping for 380 mA 7135's instead of a FET
#define RAMPING_REVERSE // (default ON) reverses ramping direction for every click&hold
#define RAMP_SWITCH_TIMEOUT 75 // make the up/dn ramp switch timeout 1.2 secs (same as IDLE_TIME)
//#define ADV_RAMP_OPTIONS // In ramping, enables "mode set" additional method for lock-out and battery voltage display, comment out to disable
#define TRIPLE_CLICK_BATT // enable a triple-click to display Battery status
#define SHORT_CLICK_DUR 18 // Short click max duration - for 0.288 secs
#define RAMP_MOON_PAUSE 23 // this results in a 0.368 sec delay, paused in moon mode
#define D1 2 // Drop over reverse polarity protection diode, 2 = 0.2 V
// ----- 2/14 TE: One-Click Turn OFF option --------------------------------------------
#define IDLE_TIME 75 // make the time-out 1.2 seconds (Comment out to disable)
// Switch handling:
#define LONG_PRESS_DUR 24 // Prev Mode time - long press, 24=0.384s (1/3s: too fast, 0.5s: too slow)
#define XLONG_PRESS_DUR 68 // strobe mode entry hold time - 68=1.09s (any slower it can happen unintentionally too much)
#define CONFIG_ENTER_DUR 160 // Config mode entry hold time - 160=2.5s, 128=2s
#define DBL_CLICK_TICKS 15 // fast click time for enable/disable of Lock-Out, batt check,
// and double/triple click timing (15=0.240s, was 14=0.224s)
#define BATT_LOW 30 // Cell voltage to step light down = 3.0 V
#define BATT_CRIT 28 // Cell voltage to shut the light off = 2.8 V
#define ADC_DELAY 312 // Delay in ticks between low-batt ramp-downs (312=5secs, 250=4secs, was: 188 ~= 3s)
// output to use for blinks on battery check mode (primary PWM level, alt PWM level)
// Use 20,0 for a single-channel driver or 0,40 for a two-channel driver
#define BLINK_BRIGHTNESS (RAMP_SIZE/5)
#define BEACON_BRIGHTNESS (RAMP_SIZE/4)
// States (in modeState):
#define RAMPING_ST (0)
#define TACTICAL_ST (1)
#define LOCKOUT_ST (2)
#define BEACON_ST (3)
#define THERMAL_REG_ST (4)
#define THERMAL_CAL_ST (5)
#define BATT_READ_ST (6)
#define TEMP_READ_ST (7)
#define FIRM_VERS_MODE 81
//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
/* UT01 notes:
Switch Pin: PA2
PWM Dimming: PA1 (TCA WO-1) ... MOD: PA7 (TCD WO-1)
QX7138 Power: PA6
*/
#define SWITCH_PIN_LEVEL (VPORTA.IN & (1 << 2)) // PA2
inline void Disable7138() { VPORTA.OUT &= ~(1 << 6); }
inline void Enable7138() { VPORTA.OUT |= (1 << 6); }
inline void PCINT_on() { PORTA.PIN2CTRL |= PORT_ISC_BOTHEDGES_gc; }
inline void PCINT_off() { PORTA.PIN2CTRL &= 0b11111000; }
inline void Clear_Switch_Interrupt() { PORTA.INTFLAGS |= PIN2_bm; }
// Having own _delay_ms() saves some bytes AND adds possibility to use variables as input
void _delay_ms(uint16_t n) { while(n-- > 0) _delay_loop_2(BOGOMIPS); }
#define DEBOUNCE_BOTH // Comment out if you don't want to debounce the PRESS along with the RELEASE
// PRESS debounce is only needed in special cases where the switch can experience errant signals
#define DB_PRES_DUR 0b00000001 // time before we consider the switch pressed (after first realizing it was pressed)
#define DB_REL_DUR 0b00001111 // time before we consider the switch released
// each bit of 1 from the right equals 16ms, so 0x0f = 64ms
//---------------------------------------------------------------------------------------
#define RAMP_SIZE 150 //sizeof(ramp_7135) // sizeof only works properly with uint8_t, not uint16_t
#define TURBO_DROP_MIN 115
// min level in ramping the turbo timeout will engage,
// level 115 = 106 PWM, this is ~43%
#define TURBO_DROP_SET 102
// the level turbo timeout will set,
// level 102 = 71 PWM, this is ~32%
//----------------------------------------------------
// Ramping Mode Levels, 150 total entries (2.4 secs)
//
// For FET+1: FET and one 350 mA 7135 (tested/verified):
// level_calc.py 2 150 7135 10 0.3 150 FET 1 1 1500
//
// Note: PWM value of 4 should be ~5.5 mA, but a value of 10 actually measures ~5 ma
//----------------------------------------------------
// 150-entry ramp starting at 1
//PROGMEM const byte ramp_7135[] = {1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,3,3,3,3,3,4,4,4,4,5,5,5,6,6,6,7,7,8,8,8,9,9,10,10,11,11,12,13,13,14,15,15,16,17,18,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,35,36,37,38,40,41,43,44,46,47,49,50,52,53,55,57,59,60,62,64,66,68,70,72,74,76,78,81,83,85,87,90,92,95,97,100,102,105,107,110,113,116,119,121,124,127,130,133,137,140,143,146,150,153,156,160,163,167,171,174,178,182,186,190,194,198,202,206,210,214,219,223,227,232,236,241,246,250,255};
// 150-entry ramp starting at 1 and ending at 1023
// original: PROGMEM const word ramp_7135[] = {1,1,1,2,2,2,3,3,3,4,4,5,5,6,6,7,8,8,9,10,11,12,13,14,15,16,17,19,20,21,23,24,26,27,29,31,33,35,37,39,41,43,45,48,50,53,55,58,61,63,66,69,73,76,79,83,86,90,93,97,101,105,109,113,118,122,127,131,136,141,146,151,157,162,167,173,179,185,191,197,203,209,216,222,229,236,243,250,258,265,273,280,288,296,305,313,321,330,339,348,357,366,376,385,395,405,415,425,436,446,457,468,479,490,502,513,525,537,549,562,574,587,600,613,626,639,653,667,681,695,710,724,739,754,769,785,800,816,832,849,865,882,899,916,933,951,968,986,1005,1023};
// modified to be front-heavy
PROGMEM const word ramp_7135[] = {1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,4,4,4,5,5,6,6,7,8,8,9,10,11,12,13,14,15,16,17,18,20,21,23,24,26,28,29,31,33,35,38,40,42,45,47,50,53,55,58,61,65,68,71,75,79,82,86,90,94,99,103,108,112,117,122,127,132,138,143,149,155,161,167,173,180,186,193,200,207,215,222,230,238,246,254,263,271,280,289,298,308,318,327,337,348,358,369,380,391,402,414,426,438,450,462,475,488,501,515,529,543,557,571,586,601,616,632,648,664,680,696,713,731,748,766,784,802,821,839,859,878,898,918,938,959,980,1001,1023};
// 150-entry ramp starting at 5
//PROGMEM const byte ramp_7135[] = {5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,8,8,8,8,9,9,9,10,10,10,11,11,11,12,12,13,13,14,14,15,15,16,16,17,18,18,19,20,21,21,22,23,24,25,25,26,27,28,29,30,31,32,33,34,36,37,38,39,41,42,43,45,46,47,49,50,52,53,55,57,58,60,62,64,65,67,69,71,73,75,77,79,81,83,86,88,90,92,95,97,100,102,105,107,110,112,115,118,121,124,126,129,132,135,138,142,145,148,151,155,158,161,165,168,172,176,179,183,187,191,195,199,203,207,211,215,219,223,228,232,237,241,246,250,255};
//----------------------------------------------------------------
// Config Settings via UI, with default values:
//----------------------------------------------------------------
byte tacticalSet = 0; // 1: system is in Tactical Mode
//byte tempAdjEnable = 1; // 1: enable active temperature adjustments, 0: disable
// thermal regulation ceiling
byte tempCeil = DEFAULT_THERM_CEIL;
// unused
//byte turboTimeoutMode = 0; // 0=disabled, 1=30s,2=60s,3=90s, 4=120s, 5=3min,6=5min,7=10min (2 mins is good for production)
//----------------------------------------------------------------
// Global state options
//----------------------------------------------------------------
// Ramping :
#define MAX_RAMP_LEVEL (RAMP_SIZE)
#define MAX_7135_LEVEL 65
#define LOCKOUT_RAMP_LEVEL 1
volatile byte rampingLevel = 0; // 0=OFF, 1..MAX_RAMP_LEVEL is the ramping table index, 255=moon mode
volatile byte TargetLevel = 0; // what the user requested (may be higher than actual level, due to thermal regulation)
volatile byte ActualLevel; // current brightness (may differ from rampingLevel or TargetLevel)
byte memorizedLevel = 65; // mode memory (ish, not saved across battery changes)
byte preTurboLevel = 65; // only used to return from double-click turbo
byte rampState = 0; // 0=OFF, 1=in lowest mode (moon) delay, 2=ramping Up, 3=Ramping Down, 4=ramping completed (Up or Dn)
byte rampLastDirState = 0; // last ramping state's direction: 2=up, 3=down
byte dontToggleDir = 0; // flag to not allow the ramping direction to switch//toggle
//volatile byte byDelayRamping = 0; // when the ramping needs to be temporarily disabled
byte rampPauseCntDn; // count down timer for ramping support
// State and count vars:
volatile byte modeState; // Current operating state: 0=Ramping, 1=Tactical, 2=Lock Out, 3=Beacon mode, 4=Battery check, 5=Temp check
word wPressDuration = 0;
volatile byte fastClicks = 0; // fast click counts for dbl-click, triple-click, etc.
volatile word wIdleTicks = 0;
volatile byte LowBattSignal = 0; // a low battery has been detected - trigger/signal it
byte byStartupDelayTime; // Used to delay the WDT from doing anything for a short period at power up
// Keep track of cell voltage in ISRs, 10-bit resolution required for impedance check
volatile int16_t voltageTenths = 0; // in volts * 10
volatile int16_t temperature = 0; // in degC
volatile byte adc_step = 0; // steps 0, 1 read voltage, steps 2, 3 reads temperature - only use steps 1, 3 values
volatile word wTempTicks = 0; // set to TEMP_ADJ_PERIOD, and decremented
byte b10Clicks = 0; // special 10 click state for turning thermal reg ON/OFF
// Configuration settings storage in EEPROM
word eepos = 0; // (0..n) position of mode/settings stored in EEPROM (128/256/512)
/**************************************************************************************
* SetOutput - sets the PWM output value directly
* =========
* Don't use this directly; use SetLevel or SetActualLevel instead.
**************************************************************************************/
void SetOutput(byte pwm1)
{
TCD0.SINGLE.CMP1 = pwm1;
}
/**************************************************************************************
* SetActualLevel - uses the ramping levels to set the PWM output
* ======== (0 is OFF, 1..RAMP_SIZE is the ramping index level)
**************************************************************************************/
void SetActualLevel(byte level)
{
ActualLevel = level;
if (level == 0)
{
SetOutput(0);
Disable7138();
}
else
{
level -= 1; // make it 0 based
SetOutput(pgm_read_byte(ramp_7135 + level));
Enable7138();
}
}
// Use this one if you also want to adjust the maximum brightness it'll step up to when cold.
void SetLevel(byte level)
{
TargetLevel = level;
SetActualLevel(level);
}
#define SetLevelSoft SetLevel
#ifndef SetLevelSoft
// FIXME: this interferes with handling double-clicks
// FIXME: looks bad when turning off, if ramp has repeated values at the bottom
// Like SetLevel, but with a ramp-up / ramp-down transition
void SetLevelSoft(byte level)
{
uint8_t target_level = level;
int8_t shift_amount;
int16_t diff;
do {
diff = target_level - ActualLevel;
shift_amount = (diff >> 2) | (diff!=0);
ActualLevel += shift_amount;
SetLevel(ActualLevel);
_delay_ms(RAMP_SIZE/8); // fast ramp
} while (target_level != ActualLevel);
}
#endif
/**************************************************************************************
* Strobe
* ======
**************************************************************************************/
void Strobe(byte ontime, byte offtime)
{
SetLevel(MAX_RAMP_LEVEL);
_delay_ms(ontime);
SetLevel(0);
_delay_ms(offtime);
}
/**************************************************************************************
* Blink - do a # of blinks with a speed in msecs
* =====
**************************************************************************************/
void Blink(byte val, word speed)
{
for (; val>0; val--)
{
SetLevel(BLINK_BRIGHTNESS);
_delay_ms(speed);
SetLevel(0);
_delay_ms(speed<<2); // 4X delay OFF
}
}
/**************************************************************************************
* delay_unless_modeState_changed - like _delay_ms(), but aborts on mode change
* =========
* ms : milliseconds to delay
* expected : abort if/when modeState doesn't match this value
**************************************************************************************/
uint8_t delay_unless_modeState_changed(uint16_t ms, uint8_t expected)
{
//uint8_t expected = modeState; // automatic mode; buggy
while (ms > 0) {
_delay_ms(1);
ms --;
if (modeState != expected) return 0;
}
return 1;
}
/**************************************************************************************
* DigitBlink - do a # of blinks with a speed in msecs
* =========
**************************************************************************************/
byte DigitBlink(byte val, byte blinkModeState)
{
if (modeState != blinkModeState) // if the mode changed, bail out
return 0;
// zeroes get only a short blink
if (val == 0) {
SetLevel(BLINK_BRIGHTNESS);
delay_unless_modeState_changed(8, blinkModeState);
SetLevel(0);
if(!delay_unless_modeState_changed(375, blinkModeState)) return 0;
}
for (; val>0; val--)
{
SetLevel(BLINK_BRIGHTNESS);
delay_unless_modeState_changed(250, blinkModeState);
SetLevel(0);
if(!delay_unless_modeState_changed(375, blinkModeState)) return 0;
}
return 1; // Ok, not terminated
}
/**************************************************************************************
* BlinkOutNumber - blinks out a 2 digit #
* ==============
**************************************************************************************/
void BlinkOutNumber(byte num, byte blinkModeState)
{
// FIXME: handle 1-digit and 3-digit numbers
// FIXME: don't use "/" or "%"; this MCU doesn't have those
if (!DigitBlink(num / 10, blinkModeState))
return;
_delay_ms(800);
if (!DigitBlink(num % 10, blinkModeState))
return;
_delay_ms(2000);
}
/**************************************************************************************
* BlinkLVP - blinks the specified time for use by LVP
* ========
* Supports both ramping and mode set modes.
**************************************************************************************/
void BlinkLVP(byte BlinkCnt, byte level)
{
int nMsecs = 200;
if (BlinkCnt > 5)
nMsecs = 125;
// Flash 'n' times before lowering
while (BlinkCnt-- > 0)
{
SetLevel(0);
_delay_ms(nMsecs);
SetLevel(level);
_delay_ms(nMsecs<<1);
}
}
/**************************************************************************************
* IsPressed - debounce the switch release, not the switch press
* ==========
**************************************************************************************/
// Debounce switch press value
#ifdef DEBOUNCE_BOTH
int IsPressed()
{
static byte pressed = 0;
// Keep track of last switch values polled
static byte buffer = 0x00;
// Shift over and tack on the latest value, 0 being low for pressed, 1 for pulled-up for released
buffer = (buffer << 1) | (SWITCH_PIN_LEVEL == 0);
if (pressed) {
// Need to look for a release indicator by seeing if the last switch status has been 0 for n number of polls
pressed = (buffer & DB_REL_DUR);
} else {
// Need to look for pressed indicator by seeing if the last switch status was 1 for n number of polls
pressed = ((buffer & DB_PRES_DUR) == DB_PRES_DUR);
}
return pressed;
}
#else
static int IsPressed()
{
// Keep track of last switch values polled
static byte buffer = 0x00;
// Shift over and tack on the latest value, 0 being low for pressed, 1 for pulled-up for released
buffer = (buffer << 1) | (SWITCH_PIN_LEVEL == 0);
return (buffer & DB_REL_DUR);
}
#endif
// Need an interrupt for when pin change is enabled to ONLY wake us from sleep.
// All logic of what to do when we wake up will be handled in the main loop.
EMPTY_INTERRUPT(PORTA_PORT_vect);
/**************************************************************************************
* WDT_on - Setup watchdog timer to only interrupt, not reset, every 16ms
* ======
**************************************************************************************/
void WDT_on()
{
// for the TinyAVR-1 chips, use the RTC instead of WDT
cli();
while (RTC.STATUS > 0) {} // wait for it to be ready
RTC.CTRLA = RTC_PRESCALER_DIV32_gc | RTC_RTCEN_bm; // clock at 1KHz
RTC.PER = 16; // 16ms
RTC.INTCTRL = RTC_OVF_bm; // enable the overflow interrupt
sei();
}
/**************************************************************************************
* WDT_off - turn off the WatchDog timer
* =======
**************************************************************************************/
void WDT_off()
{
// for the TinyAVR-1 chips, use the RTC instead of WDT
RTC.CTRLA &= ~(RTC_RTCEN_bm); // Disable the RTC
}
/**************************************************************************************
* ADC_on - Turn the AtoD Converter ON
* ======
**************************************************************************************/
void ADC_on()
{
VREF.CTRLA |= VREF_ADC0REFSEL_1V1_gc; // Set Vbg ref to 1.1V
ADC0.MUXPOS = ADC_MUXPOS_INTREF_gc; // ADC internal reference, the Vbg
ADC0.CTRLC = ADC_PRESC_DIV64_gc // CLK_PER divided by 64 = 52 KHz
| ADC_REFSEL_VDDREF_gc // Vdd (Vcc) be ADC reference
| 0 << ADC_SAMPCAP_bp; // Sample Capacitance Selection: disabled
ADC0.CTRLA = ADC_ENABLE_bm | ADC_RESSEL_10BIT_gc; // Enabled, 10-bit mode
ADC0.COMMAND |= ADC_STCONV_bm; // Start the ADC conversions
ADC0.CTRLD |= 0b00100000; // INITDLY: delay 16 ADC clock cycles
ADC0.SAMPCTRL |= 0b00000100; // SAMPLEN: 4 ADC clock cycles
ADC0.CTRLC |= ADC_SAMPCAP_bm; // In ADC.CTRLC Select SAMPCAP = 5pf
}
/**************************************************************************************
* ADC_off - Turn the AtoD Converter OFF
* =======
**************************************************************************************/
inline void ADC_off()
{
ADC0.CTRLA &= ~(ADC_ENABLE_bm); // disable the ADC
}
/**************************************************************************************
* SleepUntilSwitchPress - only called with the light OFF
* =====================
**************************************************************************************/
inline void SleepUntilSwitchPress()
{
// This routine takes up a lot of program memory :(
cli(); // Disable interrupts
// Turn the WDT off so it doesn't wake us from sleep. Will also ensure interrupts
// are on or we will never wake up.
//WDT_off();
ADC_off(); // Save more power -- turn the AtoD OFF
// Need to reset press duration since a button release wasn't recorded
wPressDuration = 0;
sei(); // Enable interrupts
// Enable a pin change interrupt to wake us up. However, we have to make sure the switch
// is released otherwise we will wake when the user releases the switch
while (IsPressed()) {
_delay_ms(16);
}
Clear_Switch_Interrupt();
PCINT_on();
//-----------------------------------------
sleep_enable();
sleep_cpu(); // go to sleepy beddy bye
sleep_disable();
//-----------------------------------------
// Hey, to get here, someone must have pressed the switch!!
PCINT_off(); // Disable pin change interrupt because it's only used to wake us up
ADC_on(); // Turn the AtoD back ON
//WDT_on(); // Turn the WDT back on to check for switch presses
} // Go back to main program
/**************************************************************************************
* LoadConfig - gets the configuration settings from EEPROM
* ==========
* config1 - 1st byte of stored configuration settings:
* bits 0: tactical mode
* config2 - 2nd byte of stored configuration settings
* all bits: user-configured thermal ceiling value
*
**************************************************************************************/
inline void LoadConfig()
{
uint8_t found = 0;
uint8_t config1, config2;
// find the config data
for (eepos=0; (!found) && (eepos < 127); eepos++)
{
config1 = eeprom_read_byte((const byte *)eepos);
config2 = eeprom_read_byte((const byte *)eepos+1);
// Only use the data if a valid marker is found
if (config1 < 0x80) {
found = 1;
break;
}
}
// unpack the config data
if (found)
{
tacticalSet = modeState = config1 & 0x01; // ramping or tactical
//tempAdjEnable = (config1 >> 1) & 0x01; // temp regulation
tempCeil = config2; // temperature limit
// make sure nothing crazy in eeprom can cause a fire
if (tempCeil > MAX_THERM_CEIL) {
tempCeil = DEFAULT_THERM_CEIL;
}
}
else
{
tacticalSet = 0;
modeState = RAMPING_ST;
eepos = 0;
}
}
/**************************************************************************************
* SaveConfig - save the current mode with config settings
* ==========
* Central method for writing (with wear leveling)
*
* config1 - 1st byte of stored configuration settings:
* bits 0: tactical mode
* config2 - 2nd byte of stored configuration settings
* all bits: user-configured thermal ceiling value
*
**************************************************************************************/
void SaveConfig()
{
uint8_t config1;
uint8_t config2;
// Pack all settings into one byte
//config1 = (byte)(tacticalSet | (tempAdjEnable << 1));
config1 = (byte)(tacticalSet);
config2 = tempCeil;
byte oldpos = eepos;
eepos = (eepos+1) & 127; // wear leveling, use next cell
cli(); // don't interrupt while writing eeprom; that's bad
// Erase the last settings
eeprom_write_byte((uint8_t *)(oldpos), 0xff);
eeprom_write_byte((uint8_t *)(oldpos+1), 0xff);
// Write the current settings
eeprom_write_byte((uint8_t *)(eepos), config1);
eeprom_write_byte((uint8_t *)(eepos+1), config2);
sei(); // okay, interrupts are fine now
}
/****************************************************************************************
* ADC_vect - ADC done interrupt service routine
* ========
* This should execute every 64ms (every 4 WDT ticks)
* So, voltage and temperature are each measured 4X per second.
****************************************************************************************/
ISR(ADC0_RESRDY_vect)
{
// (probably 4 values, since this happens 4X per second)
#define NUM_TEMP_VALUES 4
static int16_t temperature_values[NUM_TEMP_VALUES]; // for lowpass filter
// Read in the ADC 10 bit value (0..1023)
int16_t adcin = ADC0.RES;
// Voltage Monitoring
if (adc_step == 1) // ignore first ADC value from step 0
{
// Calculate cell voltage from ADC value
// 1.1V / ADC : ADC goes down with voltage
// requires raw battery voltage on VCC pin, so 5.5V or less
//adcin = (11264 + adcin/2)/adcin + D1; // in volts * 10: 10 * 1.1 * 1024 / ADC + D1, rounded
adcin = 11264/adcin + D1; // in volts * 10: 10 * 1.1 * 1024 / ADC + D1, rounded
if (voltageTenths > 0)
{
if (voltageTenths < adcin) // crude low pass filter
++voltageTenths;
if (voltageTenths > adcin)
--voltageTenths;
}
else
voltageTenths = adcin; // prime on first read
}
// Temperature monitoring
if (adc_step == 3) // ignore first ADC value from step 2
{
//----------------------------------------------------------------------------------
// Read the MCU temperature, applying a calibration offset value
//----------------------------------------------------------------------------------
// cancelled: adjust temperature values to fit in 0-255 range
// (should be able to cover about -40 C to 100 C)
// (cancelled because 13.2 fixed-point precision works better)
adcin = adcin * 5 / 8 - 300;
// average the last few values to reduce noise
// (noise will be amplified by predictive thermal algorithm, so noise is bad)
if (temperature != 0)
{
uint8_t i;
uint16_t total=0;
// rotate array
for(i=0; i> 2;
// More precise method: use noise as extra precision
// (values are now basically fixed-point, signed 13.2)
temperature = total;
}
else { // prime on first read
uint8_t i;
for(i=0; i 0)
{
--byStartupDelayTime;
return;
}
if (wTempTicks > 0) // decrement each tick if active
--wTempTicks;
//---------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------
// Button is pressed
//---------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------
if (IsPressed())
{
if (wPressDuration < 2000)
wPressDuration++;
// Enter thermal calibration mode
if (b10Clicks)
{
if (wPressDuration >= 62) // if held more than 1 second
{
b10Clicks = 0;
// enter thermal calibration mode
modeState = THERMAL_REG_ST;
}
}
// For Tactical, turn on MAX while button is depressed
// FIXME: tactical mode acts weird when user fast-clicks 3 times
// (goes to battcheck mode, then next click goes back to tactical mode)
else if ((modeState == TACTICAL_ST) && (wPressDuration == 1) && (fastClicks < 2))
{
rampingLevel = MAX_RAMP_LEVEL;
SetLevel(rampingLevel);
}
// GCH Customization: while in lockout, allow the button to be held down for moon
else if ((modeState == LOCKOUT_ST) && (wPressDuration == 1) && (fastClicks < 2))
{
rampingLevel = LOCKOUT_RAMP_LEVEL;
SetLevel(rampingLevel);
}
// long-press for ramping
else if ((wPressDuration >= SHORT_CLICK_DUR) && (modeState == RAMPING_ST)) // also in there was: && !byDelayRamping
{
{
switch (rampState)
{
case 0: // ramping not initialized yet
if (rampingLevel == 0)
{
rampState = 1;
rampPauseCntDn = RAMP_MOON_PAUSE; // delay a little on moon
// set this to the 1st level
rampingLevel = 1;
SetLevel(1);
dontToggleDir = 0; // clear it in case it got set from a timeout
}
else
{
#ifdef RAMPING_REVERSE
if (dontToggleDir)
{
rampState = rampLastDirState; // keep it in the same
dontToggleDir = 0; // clear it so it can't happen again til another timeout
}
else
rampState = 5 - rampLastDirState; // 2->3, or 3->2
#else
rampState = 2; // lo->hi
#endif
if (rampingLevel == MAX_RAMP_LEVEL)
{
rampState = 3; // hi->lo
SetLevel(MAX_RAMP_LEVEL);
}
else if (rampingLevel == 255) // If stopped in ramping moon mode, start from lowest
{
rampState = 2; // lo->hi
rampingLevel = 1;
SetLevel(rampingLevel);
}
else if (rampingLevel == 1)
rampState = 2; // lo->hi
}
break;
case 1: // in moon mode
if (--rampPauseCntDn == 0)
{
rampState = 2; // lo->hi
}
break;
case 2: // lo->hi
rampLastDirState = 2;
if (rampingLevel < MAX_RAMP_LEVEL)
{
rampingLevel ++;
}
else
{
rampState = 4;
}
if (rampingLevel == MAX_RAMP_LEVEL) {
SetActualLevel(0); // Do a quick blink
_delay_ms(7);
}
SetLevel(rampingLevel);
dontToggleDir = 0;
break;
case 3: // hi->lo
rampLastDirState = 3;
if (rampingLevel > 1)
{
rampingLevel --;
}
else
{
rampState = 4;
}
if (rampingLevel == 1) {
SetActualLevel(0); // Do a quick blink
_delay_ms(7);
}
SetLevel(rampingLevel);
dontToggleDir = 0;
break;
case 4: // ramping ended - 0 or max reached
break;
} // switch on rampState
// ensure turbo toggle will return to ramped level, even if the ramp was at turbo
preTurboLevel = rampingLevel;
} // switch held longer than single click
} // else (not b10Clicks)
//wTurboTicks = 0; // Just always reset turbo timer whenever the button is pressed
#ifdef VOLTAGE_MON
adc_ticks = ADC_DELAY; // Same with the ramp down delay
#endif
}
//---------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------
// Not pressed (debounced qualified)
//---------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------
else
{
// Was previously pressed - button released
if (wPressDuration > 0)
{
rampState = 0; // reset state to not ramping
// user got too hot and let go of button in thermal calibration mode
// (or otherwise aborted that mode)
if ((modeState == THERMAL_REG_ST) || (modeState == THERMAL_CAL_ST)) {
// change modeState to let main() know it can save the calibration
modeState = RAMPING_ST;
}
// Momentary / tactical mode
if (modeState == TACTICAL_ST)
{
rampingLevel = 0; // turn off output as soon as the user lets the switch go
SetLevel(0);
}
// normal short click processing
if (wPressDuration < SHORT_CLICK_DUR)
{
++fastClicks; // track fast clicks in a row from OFF or ON (doesn't matter in ramping)
//-----------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------
switch (modeState)
{
case RAMPING_ST:
if (fastClicks == 1)
{
byModeForMultiClicks = modeState; // save current mode
// if we were off, turn on at memorized level
if (rampingLevel == 0) {
rampingLevel = memorizedLevel;
// bugfix: make off->turbo work when memorizedLevel == turbo
preTurboLevel = memorizedLevel;
// also set up other ramp vars to make sure ramping will work
rampState = 2; // lo->hi
rampLastDirState = 2;
if (rampingLevel == MAX_RAMP_LEVEL) {
rampState = 3; // hi->lo
rampLastDirState = 3;
}
dontToggleDir = 0;
// if we were on, turn off
} else {
memorizedLevel = rampingLevel;
rampingLevel = 0;
}
SetLevelSoft(rampingLevel);
//byDelayRamping = 1; // don't act on ramping button presses
}
else if (fastClicks == 10) // --> 10 clicks: flip thermal regulation control
b10Clicks = 1;
else // --> 2+ clicks: turn off the output
{
// prevent flashes while entering long click sequences
rampingLevel = 0;
SetLevel(rampingLevel);
}
break;
case TACTICAL_ST:
if (fastClicks == 1)
byModeForMultiClicks = modeState; // save current mode
else if (fastClicks == 10) // --> 10 clicks: flip thermal regulation control
b10Clicks = 1;
break;
case BEACON_ST:
case BATT_READ_ST:
case TEMP_READ_ST:
if (fastClicks == 1)
{
byModeForMultiClicks = modeState; // save current mode
modeState = tacticalSet; // set to Ramping or Tactical
rampingLevel = 0;
SetLevel(rampingLevel);
//byDelayRamping = 1; // don't act on ramping button presses
}
break;
} // switch
} // short click
else
{
fastClicks = 0; // reset fast clicks for long holds
b10Clicks = 0; // reset special 10 click flag
}
wPressDuration = 0;
//byDelayRamping = 0; // restore acting on ramping button presses, if disabled
wIdleTicks = 0; // reset idle time
} // previously pressed
else
{
//---------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------
// Not previously pressed
//---------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------
if (++wIdleTicks > 30000)
wIdleTicks = 30000; // max it out at 30,000 (8 minutes)
if (wIdleTicks > DBL_CLICK_TICKS) // Too much delay between clicks - time out the fast clicks
{
//-----------------------------------------------------------------------------------
// Process multi clicks here, after it times out
//-----------------------------------------------------------------------------------
switch (modeState)
{
// FIXME: disable extra states from tactical/momentary mode
// (only do this in ramping mode, or from off)
case RAMPING_ST:
case TACTICAL_ST:
if (fastClicks == 2) // --> double click
{
// --> battcheck to tempcheck
// FIXME: why is this handled here instead of below under battcheck state?
if (byModeForMultiClicks == BATT_READ_ST)
{
modeState = TEMP_READ_ST;
}
// --> ramp direct to MAX/turbo (or back)
else if (modeState == RAMPING_ST)
{
// make double-click toggle turbo, not a one-way trip
// Note: rampingLevel is zero here,
// because double-click turns the light off and back on
if (memorizedLevel == MAX_RAMP_LEVEL) {
rampingLevel = preTurboLevel;
} else {
preTurboLevel = memorizedLevel;
rampingLevel = MAX_RAMP_LEVEL;
}
SetLevelSoft(rampingLevel);
}
}
#ifdef TRIPLE_CLICK_BATT
else if (fastClicks == 3) // --> triple click: display battery check/status
{
modeState = BATT_READ_ST;
//byDelayRamping = 1; // don't act on ramping button presses
}
#endif
// GCH customization: swap Tactical (now 6 clicks) and Lockout (now 4 clicks)
else if (fastClicks == 6) // --> 4 clicks: set toggle Ramping/Tactical Mode
{
if (modeState == RAMPING_ST)
modeState = TACTICAL_ST;
else
modeState = RAMPING_ST;
//byDelayRamping = 1; // don't act on ramping button presses
}
// GCH customization: swap Tactical (now 6 clicks) and Lockout (now 4 clicks)
else if (fastClicks == 4) // --> 6 clicks: set Lockout Mode
{
modeState = LOCKOUT_ST;
//byDelayRamping = 1; // don't act on ramping button presses
}
else if (fastClicks == 8) // --> 8 clicks: set Beacon Mode
{
modeState = BEACON_ST;
//byDelayRamping = 1; // don't act on ramping button presses
}
break;
case LOCKOUT_ST:
// GCH customization: swap Tactical (now 6 clicks) and Lockout (now 4 clicks)
if (fastClicks == 4) // --> 6 clicks: clear Lock Out Mode
{
modeState = tacticalSet; // set to Ramping or Tactical
//byDelayRamping = 1; // don't act on ramping button presses
}
break;
case BEACON_ST:
break;
case BATT_READ_ST:
case TEMP_READ_ST:
break;
}
fastClicks = 0;
}
// For ramping, timeout the direction toggling
if ((rampingLevel > 1) && (rampingLevel < MAX_RAMP_LEVEL))
{
if (wIdleTicks > RAMP_SWITCH_TIMEOUT)
dontToggleDir = 1;
}
//------------------------------------------------------------------------------
// Do voltage monitoring when the switch isn't pressed
//------------------------------------------------------------------------------
#ifdef VOLTAGE_MON
if (adc_ticks > 0)
--adc_ticks;
if (adc_ticks == 0)
{
// See if conversion is done
{
byte atLowLimit;
atLowLimit = ((ActualLevel == 1));
// See if voltage is lower than what we were looking for
if (voltageTenths < (atLowLimit ? BATT_CRIT : BATT_LOW))
++lowbatt_cnt;
else
lowbatt_cnt = 0;
}
// See if it's been low for a while
if (lowbatt_cnt >= 4)
{
LowBattSignal = 1;
lowbatt_cnt = 0;
// If we reach 0 here, main loop will go into sleep mode
// Restart the counter to when we step down again
adc_ticks = ADC_DELAY;
}
}
#endif
} // not previously pressed
wPressDuration = 0;
} // Not pressed
// Schedule an A-D conversion every 4th timer (64 msecs)
if ((++byInitADCTimer & 0x03) == 0) {
ADC0.INTCTRL |= ADC_RESRDY_bm; // enable interrupt
ADC0.COMMAND |= ADC_STCONV_bm; // Start the ADC conversions
}
}
/**************************************************************************************
* main - main program loop. This is where it all happens...
* ====
**************************************************************************************/
int main(void)
{
VPORTA.DIR = 0b11000000;
// enable pullups on the input/unused pins to reduce power
PORTA.PIN0CTRL = PORT_PULLUPEN_bm;
PORTA.PIN1CTRL = PORT_PULLUPEN_bm;
PORTA.PIN2CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc; //PORT_ISC_BOTHEDGES_gc;
PORTA.PIN3CTRL = PORT_PULLUPEN_bm;
// set up the PWM
//TCD0.SINGLE.CTRLB = TCD_SINGLE_CMP1EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // TCA_SINGLE_WGMODE_DSBOTTOM_gc
// At 3.333 Mhz clock, a 10-bit PWM runs at 3.25 kHz
//TCD0.SINGLE.PER = 1023; //255;
//TCD0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm;
// disable change protection
CPU_CCP = CCP_IOREG_gc;
TCD0.FAULTCTRL |= TCD_CMPBEN_bm; // enable channel B
TCD0.CTRLB = TCD_WGMODE_ONERAMP_gc; //TCD_WGMODE_DS_gc;
TCD0.CMPBCLR = 1023; // signal period
TCD0.CMPBSET = 20; // duty cycle
while(!(TCD0.STATUS & TCD_ENRDY_bm)) {;}
TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm;
// Turn features on or off as needed
#ifdef VOLTAGE_MON
ADC_on();
#else
ADC_off();
#endif
AC0.CTRLA &= ~(1<<0); // AC off, though it's probably already off by default
// Enable sleep mode set to Power Down that will be triggered by the sleep_mode() command.
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
// Shut things off now, just in case we come up in a partial power state
cli(); // Disable interrupts
PCINT_off();
//WDT_off();
// Load config settings: tactical mode and temperature regulation
LoadConfig();
// Not really intended to be configurable;
// only #defined for convenience
#define THERM_HISTORY_SIZE 8
// this allows us to measure change over time,
// which allows predicting future state
uint16_t temperatures[THERM_HISTORY_SIZE];
// track whether array has been initialized yet
uint8_t first_temp_reading = 1;
#ifdef STARTUP_LIGHT_ON
rampingLevel = MAX_RAMP_LEVEL;
SetLevel(MAX_RAMP_LEVEL);
#elif defined STARTUP_2BLINKS
Blink(2, 80); // Blink twice upon power-up
#else
SetLevel(0);
#endif
byte byPrevModeState = modeState;
byStartupDelayTime = 25; // 400 msec delay in the WDT interrupt handler
WDT_on(); // Turn it on now (mode can be non-zero on startup)
//------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------
// We will never leave this loop. The WDT will interrupt to check for switch presses
// and will change the mode if needed. If this loop detects that the mode has changed,
// run the logic for that mode while continuing to check for a mode change.
//------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------
while(1) // run forever
{
//---------------------------------------------------------------------------------------
// State Changes and Main Mode Handling
//---------------------------------------------------------------------------------------
if (byPrevModeState != modeState)
{
byte savedPrevMode = byPrevModeState;
byPrevModeState = modeState;
switch (modeState)
{
case RAMPING_ST:
// user released button during thermal calibration:
if ((savedPrevMode == THERMAL_REG_ST) || (savedPrevMode == THERMAL_CAL_ST)) {
// set limit to whatever they requested
SaveConfig();
// ... and show them the new value
SetLevel(0);
_delay_ms(500);
BlinkOutNumber(tempCeil, RAMPING_ST);
}
else if (savedPrevMode == TACTICAL_ST)
{
SetLevel(0);
_delay_ms(250);
Blink(2, 60);
tacticalSet = 0; // Save the state for ramping
SaveConfig();
}
else if (savedPrevMode == LOCKOUT_ST)
{
SetLevel(0);
_delay_ms(250);
Blink(2, 60);
}
break;
case TACTICAL_ST: // Entering Tactical
if (savedPrevMode == RAMPING_ST)
{
SetLevel(0);
_delay_ms(250);
Blink(4, 60);
tacticalSet = 1;
SaveConfig();
}
// FIXME: We can go from lockout to tactical???
// (this code should probably be removed)
else if (savedPrevMode == LOCKOUT_ST)
{
SetLevel(0);
_delay_ms(250);
Blink(2, 60);
}
break;
case LOCKOUT_ST: // Entering Lock Out
SetLevel(0);
_delay_ms(250);
Blink(4, 60);
//byDelayRamping = 0; // restore acting on ramping button presses
break;
case BEACON_ST:
_delay_ms(300); // pause a little initially
while (modeState == BEACON_ST) // Beacon mode
{
SetLevel(memorizedLevel); // allow user to set level
// on for 1/4 second
delay_unless_modeState_changed(250, BEACON_ST);
SetLevel(0);
// 2.5 s per cycle
delay_unless_modeState_changed(2250, BEACON_ST);
}
break;
case THERMAL_REG_ST:
{
// First part of thermal calibration mode:
// check current value, option to set maximum,
// then proceed to actual calibration
// show previous ceiling value
byte oldCeil = tempCeil;
byte aborted = 0;
SetLevel(0); // blink ON/OFF
_delay_ms(500);
BlinkOutNumber(oldCeil, THERMAL_REG_ST);
_delay_ms(500);
aborted = ! IsPressed();
if (aborted) {
// exit with no changes
modeState = RAMPING_ST;
break;
}
// buzz at a low level to give user time to set maximum level
tempCeil = MAX_THERM_CEIL;
for(uint8_t i=0; i<20; i++) {
SetLevel(MAX_RAMP_LEVEL/8);
_delay_ms(50);
SetLevel(MAX_RAMP_LEVEL/7);
_delay_ms(50);
aborted = ! IsPressed();
if (aborted) break;
}
if (aborted) {
// save and exit
modeState = RAMPING_ST;
break;
}
// else calibrate normally
tempCeil = oldCeil;
modeState = THERMAL_CAL_ST;
}
break;
case BATT_READ_ST:
_delay_ms(400); // delay a little here to give the user a chance to see a full blink sequence
//byDelayRamping = 0; // restore acting on ramping button presses
while (modeState == BATT_READ_ST) // Battery Check
{
// blink out volts and tenths
BlinkOutNumber(voltageTenths, BATT_READ_ST);
}
break;
case TEMP_READ_ST:
_delay_ms(400); // delay a little here to give the user a chance to see a full blink sequence
while (modeState == TEMP_READ_ST) // Temperature Check
{
// blink out temperature in 2 digits
// (divide by 4 to get an integer because the value is 13.2 fixed-point)
uint8_t temp = 0;
// don't try to show negative numbers
if (temperature > 0) {
temp = temperature>>2;
}
BlinkOutNumber(temp, TEMP_READ_ST);
}
break;
} // switch
} // mode changed
//---------------------------------------------------------------------
// Perform low battery indicator tests
//---------------------------------------------------------------------
#ifdef USE_LVP
if (LowBattSignal)
{
if (ActualLevel > 0)
{
if (ActualLevel == 1)
{
// Reached critical battery level
BlinkLVP(8, RAMP_SIZE/6); // blink more, brighter, and quicker (to get attention)
ActualLevel = rampingLevel = 0; // Shut it down
rampState = 0;
// TODO: test if it actually shuts off as far as possible after LVP
}
else
{
// Drop the output level when voltage is low
BlinkLVP(3, ActualLevel); // 3 blinks as a warning
// decrease by half current ramp level
ActualLevel = (ActualLevel>>1);
if (ActualLevel < 1) { ActualLevel = 1; }
}
SetLevelSoft(ActualLevel); // soft ramp down
}
LowBattSignal = 0;
}
#endif // ifdef USE_LVP
//---------------------------------------------------------------------
// Temperature monitoring - step it down if too hot!
//---------------------------------------------------------------------
// Init thermal calibration mode, if we're in that mode.
if (modeState == THERMAL_CAL_ST) {
SetLevelSoft(MAX_RAMP_LEVEL);
}
// don't run unless a measurement has completed
// don't run unless the light is actually on!
if ((ActualLevel > 0) && (temperature > 0)) {
if (first_temp_reading) { // initial setup
first_temp_reading = 0;
uint8_t i;
for (i=0; i> 2;
// less aggressive prediction
tempCeil = (temperatures[THERM_HISTORY_SIZE-1]
+ (diff<<(THERM_PREDICTION_STRENGTH-1))) >> 2;
//tempCeil = (temperature + (diff<<(THERM_PREDICTION_STRENGTH-1))) >> 2;
// Don't let user exceed maximum limit
if (tempCeil > MAX_THERM_CEIL) {
tempCeil = MAX_THERM_CEIL;
}
}
// too hot, step down (maybe)
else if (projected >= THERM_CEIL) {
underheat_count = 0; // we're definitely not too cold
if (overheat_count > THERM_LOWPASS) {
overheat_count = 0;
// how far above the ceiling?
int16_t exceed = (projected - THERM_CEIL) >> THERM_DIFF_ATTENUATION;
if (exceed < 1) { exceed = 1; }
uint8_t stepdown = ActualLevel - exceed;
// never go under the floor; zombies in the cellar
if (stepdown < THERM_FLOOR) {
stepdown = THERM_FLOOR;
}
// avoid a bug: stepping "down" from moon to THERM_FLOOR
// if user turned light down to moon during lowpass period
if (stepdown > TargetLevel) {
stepdown = TargetLevel;
}
// really, don't try to regulate below the floor
if (ActualLevel > THERM_FLOOR) {
SetActualLevel(stepdown);
}
} else {
overheat_count ++;
}
}
else { // not too hot
overheat_count = 0; // we're definitely not too hot
// too cold? step back up?
if (projected < (THERM_CEIL - (THERM_WINDOW_SIZE<<2))) {
if (underheat_count > (THERM_LOWPASS/2)) {
underheat_count = 0;
// never go above the user's requested level
if (ActualLevel < TargetLevel) {
SetActualLevel(ActualLevel + 1); // step up slowly
}
} else {
underheat_count ++;
}
}
}
}
} // thermal regulation
//---------------------------------------------------------------------
// Be sure switch is not pressed and light is OFF for at least 5 secs
//---------------------------------------------------------------------
//word wWaitTicks = 300; // ~5 secs
word wWaitTicks = 180; // ~3 secs
if ((ActualLevel == 0) && !IsPressed() && (wIdleTicks > wWaitTicks))
{
wIdleTicks = 0;
_delay_ms(1); // Need this here, maybe instructions for PWM output not getting executed before shutdown?
SleepUntilSwitchPress(); // Go to sleep
}
} // while(1)
return 0; // Standard Return Code
}