/* Firmware for Ferrero Rocher driver
* and other attiny13a-based e-switch lights.
*
* Copyright (C) 2015 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 .
*
*
* ATTINY13A Diagram
* ----
* -|1 8|- VCC
* E-switch -|2 7|- Voltage ADC
* Red LED -|3 6|- PWM
* GND -|4 5|- Green LED
* ----
*/
#define F_CPU 4800000UL
// PWM Mode
#define PHASE 0b00000001
#define FAST 0b00000011
// Default only; this firmware sets PWM type per mode below in MODE_PWM
#define PWM_MODE FAST // PWM mode/speed: PHASE (9 kHz) or FAST (18 kHz)
// (FAST has side effects when PWM=0, can't
// shut off light without putting the MCU to sleep)
// (PHASE might make audible whining sounds)
// PFM not supported in this firmware, don't uncomment
//#define USE_PFM // comment out to disable pulse frequency modulation
// (makes bottom few modes ramp smoother)
/*
* =========================================================================
* Settings to modify per driver
*/
#define LOW_TO_HIGH 1 // order in fast-tap mode (long-press will go the opposite direction)
#define BLINK_ON_POWER // blink once when power is received
// (helpful on e-switch lights, annoying on dual-switch lights)
#define VOLTAGE_MON // Comment out to disable all voltage-related functions:
// (including ramp down and eventual shutoff when battery is low)
#define REDGREEN_INDICATORS // comment out to disable red/green power indicator LEDs
#define LOWPASS_VOLTAGE // Average the last 4 voltage readings for smoother results
// (comment out to use only one value, saves space)
#define BATTCHECK_ON_LONG // long-press, short-press -> battery check mode
//#define BATTCHECK_ON_SHORT // short-press, long-press -> battery check mode
// // (also short-press quickly from off back to off)
// Switch handling
#define LONG_PRESS_DUR 21 // How many WDT ticks until we consider a press a long press
// 32 is roughly .5 s, 21 is roughly 1/3rd second
#define TICKS_PER_RAMP 21 // How many WDT ticks per step in the ramp (lower == faster ramp)
// Must be low to high, starting with 0
// (and the lowest values are highly device-dependent)
#define MODES 0,1,3,12,40,125,255
//#define ALT_MODES 0,1,3,12,40,125,255 // Must be low to high, and must start with 0, the defines the level for the secondary output. Comment out if no secondary output
#define MODE_PWM 0,PHASE,PHASE,FAST,FAST,FAST,PHASE // Define one per mode above, 0 for phase-correct, 1 for fast-PWM
// (use PHASE for max to avoid a lingering moon-like mode while holding the button, side effect of FAST mode)
#define TURBO // Comment out to disable - full output with a step down after n number of seconds
// If turbo is enabled, it will be where 255 is listed in the modes above
#define TURBO_TIMEOUT 5625 // How many WTD ticks before before dropping down (.016 sec each)
// 30 = 1875
// 90 = 5625
// 120 = 7500
#define ADC_42 184 // the ADC value we expect for 4.20 volts
#define ADC_100 184 // the ADC value for 100% full (4.2V resting)
#define ADC_75 175 // the ADC value for 75% full (4.0V resting)
#define ADC_50 165 // the ADC value for 50% full (3.8V resting)
#define ADC_25 151 // the ADC value for 25% full (3.5V resting)
#define ADC_0 128 // the ADC value for 0% full (3.0V resting)
#define VOLTAGE_FULL 170 // 3.9 V under load
#define VOLTAGE_GREEN 156 // 3.6 V under load
#define VOLTAGE_YELLOW 142 // 3.3 V under load
#define VOLTAGE_RED 128 // 3.0 V under load
#define ADC_LOW 124 // When do we start ramping down (2.9V)
#define ADC_CRIT 114 // When do we shut the light off (2.7V)
// these two are just for testing low-batt behavior w/ a CR123 cell
//#define ADC_LOW 139 // When do we start ramping down
//#define ADC_CRIT 138 // When do we shut the light off
#define ADC_DELAY 188 // Delay in ticks between low-bat rampdowns (188 ~= 3s)
#define OWN_DELAY // replace default _delay_ms() with ours, comment to disable
/*
* =========================================================================
*/
#ifdef OWN_DELAY
#include
// Having own _delay_ms() saves some bytes AND adds possibility to use variables as input
static void _delay_ms(uint16_t n)
{
while(n-- > 0)
_delay_loop_2(950);
}
#else
#include
#endif
#include
#include
#include
#include
#include
#include
//#include
#define SWITCH_PIN PB3 // what pin the switch is connected to, which is Star 4
#define PWM_PIN PB1
#define ALT_PWM_PIN PB0
#define VOLTAGE_PIN PB2
#define RED_PIN PB4 // pin 3
#define GREEN_PIN PB0 // pin 5
#define ADC_CHANNEL 0x01 // MUX 01 corresponds with PB2
#define ADC_DIDR ADC1D // Digital input disable bit corresponding with PB2
#define ADC_PRSCL 0x06 // clk/64
#define PWM_LVL OCR0B // OCR0B is the output compare register for PB1
#define ALT_PWM_LVL OCR0A // OCR0A is the output compare register for PB0
#ifdef USE_PFM
#define CEIL_LVL OCR0A // OCR0A is the number of "frames" per PWM loop
#endif
#define DB_REL_DUR 0b00001111 // time before we consider the switch released after
// each bit of 1 from the right equals 16ms, so 0x0f = 64ms
/*
* The actual program
* =========================================================================
*/
/*
* global variables
*/
const uint8_t modes[] = { MODES };
#ifdef ALT_MODES
const uint8_t alt_modes[] = { ALT_MODES };
#endif
const uint8_t mode_pwm[] = { MODE_PWM };
volatile uint8_t mode_idx = 0;
uint8_t press_duration = 0;
uint8_t voltage_readout = 0;
#ifdef LOWPASS_VOLTAGE
uint8_t voltages[] = { VOLTAGE_FULL, VOLTAGE_FULL, VOLTAGE_FULL, VOLTAGE_FULL };
#endif
PROGMEM const uint8_t voltage_blinks[] = {
ADC_0, // 1 blink for 0%-25%
ADC_25, // 2 blinks for 25%-50%
ADC_50, // 3 blinks for 50%-75%
ADC_75, // 4 blinks for 75%-100%
ADC_100, // 5 blinks for >100%
};
// Debounced switch press value
int is_pressed()
{
// Keep track of last switch values polled
static uint8_t buffer = 0x00;
// Shift over and tack on the latest value, 0 being low for pressed, 1 for pulled-up for released
buffer = (buffer << 1) | ((PINB & (1 << SWITCH_PIN)) == 0);
return (buffer & DB_REL_DUR);
}
void next_mode() {
if (++mode_idx >= sizeof(modes)) {
// Wrap around
mode_idx = 0;
}
}
void prev_mode() {
if (mode_idx == 0) {
// Wrap around
mode_idx = sizeof(modes) - 1;
} else {
--mode_idx;
}
}
inline void PCINT_on() {
// Enable pin change interrupts
GIMSK |= (1 << PCIE);
}
inline void PCINT_off() {
// Disable pin change interrupts
GIMSK &= ~(1 << PCIE);
}
// 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(PCINT0_vect);
inline void WDT_on() {
// Setup watchdog timer to only interrupt, not reset, every 16ms.
cli(); // Disable interrupts
wdt_reset(); // Reset the WDT
WDTCR |= (1< 0 && press_duration < LONG_PRESS_DUR) {
// Short press
#if LOW_TO_HIGH
next_mode();
#else
prev_mode();
#endif
#ifdef BATTCHECK_ON_SHORT
// If the user keeps short-tapping the button from off, reset the
// on-time timer... otherwise, if we've been on for a while, ignore
if (ontime_ticks < (LONG_PRESS_DUR+TICKS_PER_RAMP)) {
ontime_ticks = 1;
// If the user short-tapped all the way through the modes and went
// to "off" again, start the voltage readout mode
// (also happens if they long-press to first mode then
// immediately tap to turn it off again)
if (mode_idx == 0) {
voltage_readout = 4;
}
}
#endif
} else {
#ifdef TURBO
// Only do turbo check when switch isn't pressed
//if (modes[mode_idx] == 255) { // takes more space
if (mode_idx == sizeof(modes)-1) {
turbo_ticks++;
if (turbo_ticks > TURBO_TIMEOUT) {
// Go to the previous mode
prev_mode();
}
}
#endif
// Only do voltage monitoring when the switch isn't pressed
#ifdef VOLTAGE_MON
// See if conversion is done
if (ADCSRA & (1 << ADIF)) {
#ifdef LOWPASS_VOLTAGE
// Get an average of the past few readings
for (i=0;i<3;i++) {
voltages[i] = voltages[i+1];
}
voltages[3] = ADCH;
voltage = (voltages[0]+voltages[1]+voltages[2]+voltages[3]) >> 2;
#else
voltage = ADCH;
#endif
// See if voltage is lower than what we were looking for
if (voltage < ((mode_idx == 1) ? ADC_CRIT : ADC_LOW)) {
++lowbatt_cnt;
} else {
lowbatt_cnt = 0;
}
#ifdef REDGREEN_INDICATORS
if (voltage > VOLTAGE_GREEN) {
// turn on green LED
DDRB = (1 << PWM_PIN) | (1 << GREEN_PIN);
PORTB |= (1 << GREEN_PIN);
PORTB &= 0xff ^ (1 << RED_PIN); // red off
}
else if (voltage > VOLTAGE_YELLOW) {
// turn on red+green LED (yellow)
DDRB = (1 << PWM_PIN) | (1 << GREEN_PIN) | (1 << RED_PIN); // bright green + bright red
//DDRB = (1 << PWM_PIN) | (1 << GREEN_PIN); // bright green + dim red
PORTB |= (1 << GREEN_PIN) | (1 << RED_PIN);
}
else {
// turn on red LED
DDRB = (1 << PWM_PIN) | (1 << RED_PIN);
PORTB |= (1 << RED_PIN);
PORTB &= 0xff ^ (1 << GREEN_PIN); // green off
}
#endif
// allow us to get another voltage reading, not under load
if (voltage_readout > 1) {
PWM_LVL = 0;
voltage_readout --;
} else if (voltage_readout == 1) {
uint8_t blinks = 0;
PWM_LVL = modes[2]; // brief flash at start of measurement
_delay_ms(5);
//voltage = get_voltage();
// turn off and wait one second before showing the value
// (or not, uses extra space)
PWM_LVL = 0;
_delay_ms(1000);
// division takes too much flash space
//voltage = (voltage-ADC_LOW) / (((ADC_42 - 15) - ADC_LOW) >> 2);
// a table uses less space than 5 logic clauses
for (i=0; i pgm_read_byte(voltage_blinks + i)) {
blinks ++;
}
}
// blink up to five times to show voltage
// (~0%, ~25%, ~50%, ~75%, ~100%, >100%)
for(i=0; i= ADC_DELAY) {
prev_mode();
lowbatt_cnt = 0;
}
// Make sure conversion is running for next time through
ADCSRA |= (1 << ADSC);
#endif
}
press_duration = 0;
}
}
int main(void)
{
// Set all ports to input, and turn pull-up resistors on for the inputs we are using
DDRB = 0x00;
PORTB = (1 << SWITCH_PIN);
// Set the switch as an interrupt for when we turn pin change interrupts on
PCMSK = (1 << SWITCH_PIN);
// Set PWM pin to output
#ifdef ALT_MODES
DDRB = (1 << PWM_PIN) | (1 << ALT_PWM_PIN);
#else
DDRB = (1 << PWM_PIN);
#endif
// Set timer to do PWM for correct output pin and set prescaler timing
// PWM is set per-mode in this firmware
TCCR0A = 0x20 | PWM_MODE; // phase corrected PWM is 0x21 for PB1, fast-PWM is 0x23
#ifdef USE_PFM
// 0x08 is for variable-speed PWM
TCCR0B = 0x08 | 0x01; // pre-scaler for timer (1 => 1, 2 => 8, 3 => 64...)
#else
TCCR0B = 0x01; // pre-scaler for timer (1 => 1, 2 => 8, 3 => 64...)
#endif
// Turn features on or off as needed
#ifdef VOLTAGE_MON
ADC_on();
#else
ADC_off();
#endif
ACSR |= (1<<7); //AC off
#ifdef BLINK_ON_POWER
// blink once to let the user know we have power
#ifdef ALT_MODES
TCCR0A = PHASE | 0b10100000; // Use both outputs
#else
TCCR0A = PHASE | 0b00100000; // Only use the normal output
#endif
#ifdef USE_PFM
CEIL_LVL = 255;
#endif
PWM_LVL = 255;
_delay_ms(3);
PWM_LVL = 0;
_delay_ms(1);
#endif
// Enable sleep mode set to Power Down that will be triggered by the sleep_mode() command.
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_until_switch_press();
uint8_t last_mode_idx = 0;
while(1) {
// 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.
if (mode_idx != last_mode_idx) {
// The WDT changed the mode.
if (mode_idx > 0) { // TODO: remove this "if" to save space
// No need to change the mode if we are just turning the light off
// Check if the PWM mode is different
if (mode_pwm[last_mode_idx] != mode_pwm[mode_idx]) {
#ifdef ALT_MODES
TCCR0A = mode_pwm[mode_idx] | 0b10100000; // Use both outputs
#else
TCCR0A = mode_pwm[mode_idx] | 0b00100000; // Only use the normal output
#endif
}
}
PWM_LVL = modes[mode_idx];
#ifdef ALT_MODES
ALT_PWM_LVL = alt_modes[mode_idx];
#endif
last_mode_idx = mode_idx;
#ifdef ALT_MODES
if (modes[mode_idx] == 0 && alt_modes[mode_idx] == 0) {
#else
if (mode_idx == 0) {
#endif
// Finish executing instructions for PWM level change
// and/or voltage readout mode before shutdown.
do {
_delay_ms(1);
} while (voltage_readout);
// Go to sleep
sleep_until_switch_press();
}
}
}
return 0; // Standard Return Code
}