/* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. See the file COPYING * * 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. * * Copyright (C) 2010 Frank van Maarseveen */ /* * NOTES * - At power-on, it is important to pull the switching FET gate connected * to OC1A low asap to avoid high current spikes. To give an idea: in * normal operation it is never driven high for more than about 4 usec * (D=0.5 at freq 125kHz). The ZXMN2F30FH input capacitance (452pF) is * about 9 times the reverse transfer capacitance so this might not be * an issue after all but it is the right thing to do. * - A pathological state exists where the fet is permanently ON due to a * charged gate while at the same time the attiny BOD is active. To avoid * this connect a 22k resistor between gate and source (500pF*22k = 11usec). * - Attiny at 2V seems powerful enough to drive the FET: no measurable * efficiency difference with 4V. * - Just because the schottky diode leaks a lot the circuit starts up * quite close to 1.8V! It dies below 0.5V! * - The differential amplifier might not work for the first couple of mV. * - 1.44V = FULL, 1.37V = 95% reset, 1.2V = enable 1.37V reset again. * * UNREGULATED TEST DATA * Setup: Cree 3.5W LED with 0.1 ohm series. Attiny VCC connected to Uout. * OCR1A Uin Iin Uout Iout % remarks * 64 2 0.13 2.92 0.081 90 * 64 2.5 0.286 3.11 0.221 96 * 64 3 1.5 3.64 1.2 97? very sensitive to Uin changes * 64 3 1.616 3.62 1.29 96? ditto, hot LED * * TYPICAL 3.5W WHITE LED * Iled Uled (@25C) * 1.0 3.56 * 0.35 3.25 * 0.1 2.93 * 0.02 2.75 * 0.00002 2.25 (dark point) */ #include #include #include #include #include #include "common.h" #include "hardware.h" #include "types.h" #include "bits.h" #include "timer0.h" #include "pwm1.h" #include "adc.h" #include "receive.h" #include "eeprom.h" #include "serial.h" #include "boot.h" enum runlevel { RUNLEVEL_OFF, /* powerdown */ RUNLEVEL_OFF_FORCE, /* forced powerdown with switch = on */ RUNLEVEL_ON, RUNLEVEL_REPROGRAM /* WDT resets hardware before calling bootloader */ }; static enum runlevel runlevel; /* * The runlevel must survive a watchdog reset. RAM won't work because it's shared * with a bootloader so use eeprom. The eeprom.[ch] driver is not really suited in * this case because: * - watchdog reset code must be minimalistic and not use interrupts. * - cannot bring system up when we're just polling a reed switch every 125ms * to save battery power. */ static enum runlevel get_runlevel(void) { EEAR = EEAR_RUNLEVEL; BIT_SET(EECR, EERE); return EEDR; } __attribute__((noinline)) static void set_runlevel(enum runlevel new) { EECR = 0; /* atomic byte programming */ EEAR = EEAR_RUNLEVEL; EEDR = new; BIT_SET(EECR, EEMPE); BIT_SET(EECR, EEPE); while (BIT_TST(EECR, EEPE)) ; } static void sync(void) { static uint32_t lasttime; if (t0_ms(lasttime) > 10 * 1000) { ee_sync(); lasttime = t0_ms(0); } } static uint16_t iled_target; static uint16_t iled_actual; static uint16_t iled_thermal_throttle = 0xffff; static uint8_t iled_dim_shift; static uint16_t iled_dim_throttle; static uint16_t iled_final; static uint16_t iled_min; static uint16_t iled_max; static uint16_t iled_mintime; static uint16_t iled_maxtime; static int16_t iled_err; static uint16_t iled_minerr; static uint16_t iled_maxerr; static uint16_t iled_maxadj; static uint16_t iled_adjustments; static uint16_t iled_stable; static uint16_t pwm1_max; static uint8_t pwm1_kickstart; static uint16_t vbat_val; static uint16_t vbat_max; static uint16_t temp_val; static uint16_t temp_min; static uint16_t temp_max; static uint32_t on_max; static bool_t reed; static uint32_t reed_timestamp; static void tx_variables(void) { if (reed) { // #include "write-serial-variables.h" } } /* * The voltage threshold for detecting a closed reed switch is somewhere * between 0V and VBAT_MIN. However, vbat_val itself is an average of * possibly a number of 0V samples just before the switch-on occurred. * To provide a reliable battery voltage for reacting upon an "on" event * the change to "on" is delayed for one sample. This is important for * kickstarting pwm1 and for avoiding a false vbat-too-low trigger. */ static void reed_check(void) { static bool_t was_on; uint32_t t; if (vbat_val >= VBAT_REED_THRESHOLD) { if (!reed && was_on) { t = t0_ms(reed_timestamp); if (t > 3 * 1000 || iled_dim_shift == 3) iled_dim_shift = 0; else if (t > 100) ++iled_dim_shift; iled_dim_throttle = ILED_MAX >> iled_dim_shift; iled_target = iled_dim_throttle; reed = true; reed_timestamp = t0_ms(0); } was_on = true; } else { if (reed) { iled_target = 0; reed = false; reed_timestamp = t0_ms(0); } was_on = false; } } /* * Temperature readings are slow and a missing one right after switch-on * translates to a very cold value (waay below zero): not a problem. Actually, * the flashy behavior of switch-on when hot could be considered a feature. */ static void temp_check(void) { uint16_t thmax; static uint32_t timestamp; if (temp_val < TEMP(55)) thmax = ILED_MAX; else if (temp_val < TEMP(58)) thmax = ILED_MAX - (ILED_MAX >> 2); /* 75% */ else if (temp_val < TEMP(61)) thmax = ILED_MAX >> 1; /* 50% */ else if (temp_val < TEMP(64)) thmax = ILED_MAX >> 2; /* 25% */ else if (temp_val < TEMP(67)) thmax = ILED_MAX >> 3; else thmax = 0; /* something else must be wrong */ if (thmax <= iled_thermal_throttle) { timestamp = t0_ms(0); iled_thermal_throttle = thmax; } else if (t0_ms(timestamp) >= 5 * 1000) { iled_thermal_throttle = thmax; } if (iled_target > iled_thermal_throttle) iled_target = iled_thermal_throttle; } /* * Note that many AA NiMH cells have a too high impedance to deliver the * 3.5-4W we actually need from 2 cells. To avoid damage and to get the * most out of the battery pack its voltage must be part of the regulator * loop. * Best cells so far seem to be the ready2use ones from Varta (2100mAh) but * any other brand of Low Self Discharge NiMH cells will probably do as well. */ static void vbat_check(void) { uint16_t vbat_min; if (reed) { vbat_min = VBAT_MIN; /* * Compensate 0.3V at most for resistance in wiring et.al. * ILED_MAX is 745, VBAT(0.3) is 87: 745 >> 3 is 93 which is * close enough. See also hardware.h */ vbat_min -= iled_actual >> 3; if (vbat_val < vbat_min) { if (iled_target > iled_actual) { /* * vbat is collapsing, e.g. due to a too high * kickstart at startup and current follows. * Speed-up the response. */ iled_target = iled_actual; } if (iled_target) { /* * A lower vbat should always result in a lower * current but apparently it is equal or higher now. * Increase the current error by lowering the target. */ --iled_target; } } /* * After reducing the load, the battery may recover somewhat. * This is an issue after kickstart on poor batteries. */ if (vbat_val >= VBAT_MIN + VBAT(0.1) && iled_target < iled_thermal_throttle && iled_target < iled_dim_throttle) ++iled_target; } } /* * Kickstarting the boost converter is not that easy but when tuned well it * improves the perceptible response upon a switch-on event. This function * assumes loss due to resistance (battery impedance and wiring) of max. 0.2V. */ static void pwm1_kickstart_check(void) { uint16_t iled, boostv; uint8_t pwm1_kickstart_max; if (pwm1_kickstart) { /* * The battery may have recovered a bit since switch-off. Limit * the duty cycle kickstart value using the actual vbat_val to avoid * excessive peak currents. Also, avoid a voltage collapse when * the battery is empty using iled_final as an indicator. */ boostv = VBAT(3.5 + 0.2) - vbat_val; /* fits in 10 bits */ iled = ILED_MAX * 0.7; /* ignore the first 30% drop */ while (iled > iled_final && boostv >= VBAT(0.30)) { iled >>= 1; boostv -= VBAT(0.30); } pwm1_kickstart_max = (boostv << 6) / (vbat_val >> 2); if (pwm1_kickstart > pwm1_kickstart_max) pwm1_kickstart = pwm1_kickstart_max; } else { /* * Speed up initial switch-on. */ if (!BIT_TST(MCUSR, BORF)) pwm1_kickstart = ((VBAT(3.2 + 0.2) - vbat_val) << 6) / (vbat_val >> 2); } } /* * Electrical response times are all sub-millisecond. We're called every 5-6 * millisecond after iled_actual has been set to the last 16 samples, averaged. * Essentially we're regulating output voltage trying to make the LED current * approximately the same as iled_target so it's highly nonlinear and PWM deltas * have to be kept minimal (-1..1 currently). * * To increase response to a "switch-on" event the last known good duty cycle * is saved. */ static void iled_regulate(void) { static uint8_t wobble_count; static uint16_t unlock_threshold; static bool_t last_reed; int16_t err; uint8_t dc; reed_check(); vbat_check(); temp_check(); dc = pwm1_read(); if (!dc || !iled_target) { unlock_threshold = 0; wobble_count = 0; } if (reed == last_reed) { err = iled_actual - iled_target; if (unlock_threshold) { /* * Both iled_actual and iled_target may decrease together (see * vbat_check()) so make sure that unlock_threshold is reachable. * This also makes sure that the threshold is sane. */ if (unlock_threshold > (iled_actual >> 1)) unlock_threshold = iled_actual >> 1; if (abs(err) > unlock_threshold) { unlock_threshold = 0; wobble_count = 0; ee_max16(EE_ILED_MAXADJ, &iled_maxadj, iled_adjustments); iled_adjustments = 0; } } else { if ((err > 0 && iled_err > 0) || (err < 0 && iled_err < 0)) wobble_count = 0; else ++wobble_count; if (wobble_count >= 3 && err <= 0) { if (iled_err > -err) unlock_threshold = (iled_err << 1) + 1; else unlock_threshold = (-err << 1) + 1; ++iled_stable; if (reed) iled_final = iled_actual; } } iled_err = err; if (unlock_threshold) { if (err < 0) ee_max16(EE_ILED_MINERR, &iled_minerr, -err); else ee_max16(EE_ILED_MAXERR, &iled_maxerr, err); if (dc) pwm1_kickstart = dc; } else if (err) { if (err < 0 && dc < 255) ++dc; else if (err > 0 && dc > 0) --dc; } } else { last_reed = reed; if (reed) { pwm1_kickstart_check(); dc = pwm1_kickstart; } else { dc = 0; } } if (dc != pwm1_read()) { pwm1_write(dc); ee_max16(EE_PWM1_MAX, &pwm1_max, dc); if (!dc) ee_sync(); ++iled_adjustments; } } void adc_iled(uint16_t value) { static uint32_t start; uint16_t t; if (start) { t = t0_ms(start); ee_min16(EE_ILED_MINTIME, &iled_mintime, t); ee_max16(EE_ILED_MAXTIME, &iled_maxtime, t); } start = t0_ms(0); ee_min16(EE_ILED_MIN, &iled_min, value); ee_max16(EE_ILED_MAX, &iled_max, value); iled_actual = value; iled_regulate(); } void adc_vbat(uint16_t value) { vbat_val = value; ee_max16(EE_VBAT_MAX, &vbat_max, value); } void adc_temp(uint16_t value) { temp_val = value; ee_min16(EE_TEMP_MIN, &temp_min, value); ee_max16(EE_TEMP_MAX, &temp_max, value); } static void ee_initall(void) { ee_init(31, 0); ee_init16(EE_PWM1_MAX, &pwm1_max, 0x0000); ee_init16(EE_ILED_MIN, &iled_min, 0xffff); ee_init16(EE_ILED_MAX, &iled_max, 0x0000); ee_init16(EE_ILED_MINTIME, &iled_mintime, 0xffff); ee_init16(EE_ILED_MAXTIME, &iled_maxtime, 0x0000); ee_init16(EE_ILED_MINERR, &iled_minerr, 0); ee_init16(EE_ILED_MAXERR, &iled_maxerr, 0); ee_init16(EE_ILED_MAXADJ, &iled_maxadj, 0); ee_init16(EE_VBAT_MAX, &vbat_max, 0x0000); ee_init16(EE_TEMP_MIN, &temp_min, 0xffff); ee_init16(EE_TEMP_MAX, &temp_max, 0x0000); ee_init32(EE_ON_MAX, &on_max, 0); } /* * Output: * - LED on or off depending of the final current is above or below 0.5A respectively * - 1s steady. * - blink 0-6 times: count down from 1A or up from 0A respectively. * * 0 1 2 3 4 5 6 * 0.04A 0.13A 0.21A 0.29A 0.38A 0.46A * * - 1s steady * - dump state, mostly eeprom variables. Repeat this last step. */ static void tx_dump(void) { static uint32_t start; static uint8_t tick; static uint16_t iled; if (reed) tick = 0; else if (t0_ms(start) > 100) { start = t0_ms(0); if (tick == 3) { iled = iled_final; OUTPUT(LED); CLR(LED); if (iled > ILED_MAX / 2) { SET(LED); if (iled > ILED_MAX) iled = ILED_MAX; iled = ILED_MAX - iled; } } else if (tick >= 13 && tick < 40) { if (iled > ILED_MAX / 24) { if ((tick & 3) == 1) /* 13,17,21,25,29 */ TOGGLE(LED); else if ((tick & 3) == 2) { /* 14,18,22,26,30 */ TOGGLE(LED); if (iled >= ILED_MAX / 12) iled -= ILED_MAX / 12; else iled = 0; } } } else if (tick == 40) { #include "write-serial-dump.h" /* >100ms */ tick = 39; } ++tick; } } /* * Cannot use the adc.[ch] driver, see get_runlevel() comment. */ static void adc_sample_vbat(void) { ADMUX = VBAT_ADMUX; ADCSRA = _BV(ADEN) | _BV(ADSC) | BIN8(101); /* enable at 4MHz/32 = 125kHz and start conversion */ while (BIT_TST(ADCSRA, ADSC)) ; vbat_val = ADCW; ADCSRA = 0; ADMUX = 0; } #define WDT_ASAP WDT_16MS #define WDT_16MS 0 #define WDT_125MS ( _BV(WDP1) | _BV(WDP0)) #define WDT_2S ( _BV(WDP2) | _BV(WDP1) | _BV(WDP0)) #define WDT_4S (_BV(WDP3) ) #define WDT_8S (_BV(WDP3) | _BV(WDP0)) static void wdt_setup(uint8_t prescaler) { do_wdr(); WDTCR = _BV(WDE) | prescaler; } static void wdt_cleanup(void) { BIT_CLR(MCUSR, WDRF); /* WDRF overrules WDE so must clear it */ WDTCR = _BV(WDCE) | _BV(WDE); WDTCR = 0; } __attribute__((noreturn, noinline)) static void powerdown(void) { MCUCR = _BV(SE) | _BV(SM1); /* sleep enable to powerdown mode */ MCUSR = 0; /* clear anything else such as BORF */ do_sleep(); __builtin_unreachable(); } static bool_t loop(void) { uint8_t sreg; static uint32_t bad_iled_timestamp; uint32_t t; #if LED_REPROGRAM_TRIGGER bool_t led; #endif do_wdr(); tx_variables(); #if LED_REPROGRAM_TRIGGER led = ISSET(LED); if (!ee_busy() && rx_input()) runlevel = RUNLEVEL_REPROGRAM; if (led) { OUTPUT(LED); SET(LED); } #endif /* * interrupts will be switched off for sanity when accessing all the * global cruft which also gets updated by interrupt handlers but * the races don't seem to be really interesting. */ saveicli(sreg); t = t0_ms(reed_timestamp); if (reed) { CLR(LED); if (iled_actual >= ILED_MIN || t < 3 * 1000) bad_iled_timestamp = t0_ms(0); else if (t0_ms(bad_iled_timestamp) > 20 * 1000) runlevel = RUNLEVEL_OFF_FORCE; } else { if (t > 60U * 1000) runlevel = RUNLEVEL_OFF; } restorei(sreg); tx_dump(); sync(); return runlevel == RUNLEVEL_ON; } __attribute__((noreturn)) void main(void) { uint8_t sreg; hw_init(); if (BIT_TST(MCUSR, WDRF)) wdt_cleanup(); /* * The boot-loader passes PORF down for informational purposes (e.g. * for deciding how much of eeprom should be reset). We should clear * this bit before any reset (e.g. watchdog) could confuse the boot-loader. */ BIT_CLR(MCUSR, PORF); /* * Handle non-ON runlevels and runlevel changes. */ adc_sample_vbat(); runlevel = get_runlevel(); switch (runlevel) { case RUNLEVEL_OFF_FORCE: if (vbat_val < VBAT_REED_THRESHOLD) { runlevel = RUNLEVEL_OFF; set_runlevel(runlevel); } wdt_setup(WDT_125MS); /* we'll be back */ powerdown(); default: runlevel = RUNLEVEL_OFF; /* fall through */ case RUNLEVEL_OFF: if (vbat_val < VBAT_REED_THRESHOLD) { wdt_setup(WDT_125MS); /* we'll be back */ powerdown(); } runlevel = RUNLEVEL_ON; set_runlevel(runlevel); /* fall through */ case RUNLEVEL_ON: break; case RUNLEVEL_REPROGRAM: /* * Hardware has been reset just to be sure about proper * bootloader operation. Jump to original reset vector * after clearing MCUSR so the bootloader knows it has been * called by software. Reset flags cannot be written to '1'. */ set_runlevel(RUNLEVEL_OFF); MCUSR = 0; /* clear BORF for example */ exec_bootloader(); } wdt_setup(WDT_4S); OUTPUT(LED); SET(LED); sei(); t0_init(); t0_mswait(50); /* let power stabilize */ ee_initall(); saveicli(sreg); pwm1_init(); adc_init(); restorei(sreg); adc_start(); CLR(LED); while (loop()) ; CLR(LED); adc_cleanup(); /* remove iled_regulate() call source */ pwm1_cleanup(); ee_max32(EE_ON_MAX, &on_max, t0_ms(0)); ee_sync(); ee_cleanup(); /* can take considerable time */ t0_cleanup(); SREG = 0; /* interrupts off */ set_runlevel(runlevel); wdt_setup(WDT_ASAP); powerdown(); }