/*****************************************************************
DECORATIVE NIGHT LIGHT USING SHIFT REGISTER
This implements firmware for a decorative night light shaped
as a house with two floors, each having 3 windows. When it
becomes dark, the windows light up, changing the pattern from
time to time. The light also has a button for changing settings.
If the light level drops slowly, windows also light up slowly
one by one, until reaching the configured amount. If electric
light is switched off suddenly, all windows are lit very rapidly,
and then slowly decay to the configured amount.
If the button is pressed, all windows light up exactly like in
the case of sudden darkness.
A long button press enters the settings menu. In the menu, the
top row of windows blinks, showing the binary setting index.
The bottom row shows the setting value. A long press switches
to the next setting, or exits the menu after the last one.
A short press switches to the next value.
The settings are:
1 (001): Window Brightness
Selects the brightness of the windows out of 8 levels.
2 (010): Number of Lit Windows (1 to 5)
Selects the number of windows to be lit in the steady state.
The actual number steps between this value and one above,
as the pattern changes.
3 (011): Update Rate
Delay between changes in the pattern, one of:
0.5, 1, 2, 5, 10, 15, 30, 60 minutes
4 (100): Darkness Threshold
This setting allows calibrating the threshold where the transition
between light and dark should be detected. Unlike other settings,
the value is not controlled directly. Instead, short presses light
up the bottom windows one by one, and when all 3 are lit, the current
light value is recorded as the threshold.
Holding the button for 5 seconds when power is applied enters
the voltage calibration mode, which allows tuning LED brightness
compensation for varying battery voltage - see calibrate().
*****************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>
#include <avr/wdt.h>
/************* WOKWI simulator details *************/
#define WOKWI
#ifndef WOKWI
#define USE_USI
#endif
#ifdef WOKWI
#include <TinyDebug.h>
#define WOKWI_DEBUG_PRINT(...) Debug.print(__VA_ARGS__);
#define WOKWI_DEBUG_PRINTLN(...) Debug.println(__VA_ARGS__);
#else
#define WOKWI_DEBUG_PRINT(...)
#define WOKWI_DEBUG_PRINTLN(...)
#endif
/* Digital pins */
#define PWM_PIN PB0
#define DATA_PIN PB1
#define CLOCK_PIN PB2
#define BUTTON_PIN PB4
#define LIGHT_ADC_PIN PB3
#define LIGHT_ADC_CHANNEL 3
#define PORT_PULLUP_MASK (_BV(BUTTON_PIN) | _BV(CLOCK_PIN) | _BV(PWM_PIN))
#define PORT_OUTPUT_MASK (_BV(DATA_PIN) | _BV(CLOCK_PIN) | _BV(PWM_PIN))
#define PORT_DISABLE_MASK _BV(ADC3D)
/* Misc constants */
#define BTN_DEBOUNCE_DELAY 20
#define MCUCR_SLEEP_IDLE _BV(SE)
#define MCUCR_SLEEP_POFF (_BV(SE) | _BV(SM1))
#define MCUCR_SLEEP_ADC (_BV(SE) | _BV(SM0))
/************* Global State *************/
/* The high frequency hardware timer is active. */
bool timer_on;
void start_timer();
void end_timer();
/* Enable the high frequency hardware timer interrupt. */
void enable_timer_interrupt();
/************* Simple Random Generator *************/
class Random
{
uint16_t random_seed;
void next_random(int8_t cnt)
{
// Every LFSR step only generates one new unique bit.
while (cnt-- > 0)
random_seed = (random_seed >> 1) ^ (-(random_seed & 1) & 0xB400);
}
public:
inline uint8_t bits(int8_t cnt) __attribute__((always_inline))
{
/* Retrieve the necessary number of bits, and replenish them. */
uint8_t rv = random_seed & ((1 << cnt) - 1);
next_random(cnt);
return rv;
}
void init(uint8_t perturbation)
{
random_seed = 0xACE1u ^ perturbation;
next_random(16);
}
} rng;
/************* Settings *************/
#define EEPROM_VERSION 2
enum UserSettings {
SettingBrightess = 0,
SettingNumWindows,
SettingUpdateDelay,
// Special setting
SettingLightThreshold
};
// EEPROM memory layout - CHANGES REQUIRE EEPROM_VERSION UPDATE
struct Settings {
uint8_t version;
/* Settings directly available through the user menu. */
struct {
uint8_t brightness;
uint8_t num_windows;
uint8_t update_delay;
uint8_t reserved1;
uint8_t reserved2;
uint8_t reserved3;
} user;
/* Threshold for the light vs dark detection. */
uint8_t light_threshold;
/* PWM and voltage calibration data (see calibrate() for the meaning) */
uint8_t low_voltage_threshold;
uint8_t volt_coeff_p;
uint16_t volt_coeff_s;
uint16_t volt_coeff_q;
};
Settings settings;
const Settings PROGMEM default_settings = {
.version = EEPROM_VERSION,
.user = {
.brightness = 4,
.num_windows = 3,
.update_delay = 1
},
.light_threshold = 128,
// Uncalibrated guess: 3.0V threshold (reading 94) and a 2.0V LED drop (reading 70)
.low_voltage_threshold = 94,
.volt_coeff_p = 94 - 70,
.volt_coeff_s = 2*70 - 94,
.volt_coeff_q = 94 * 70,
};
/* Value limits of the plain single byte user menu settings. */
const uint8_t PROGMEM setting_limits[][2] = {
{ 0, 7 }, // SettingBrightess: 1/8 .. 8/8 of the compensated duty cycle
{ 1, 5 }, // SettingNumWindows
{ 0, 7 }, // SettingUpdateDelay: index into update_delay_values
};
void save_settings()
{
eeprom_update_block(&settings, (void*)0, sizeof(Settings));
}
void load_settings(bool reset = false)
{
eeprom_read_block(&settings, (void*)0, sizeof(Settings));
if (!reset && settings.version == EEPROM_VERSION)
return;
uint8_t *pd = (uint8_t*)&settings, *ps = (uint8_t*)&default_settings;
for (int8_t i = sizeof(Settings); i > 0; i--, pd++, ps++)
{
*pd = pgm_read_byte(ps);
}
}
/************* Async Events *************/
#define EVENT_BTN_PRESSED (1 << 0)
#define EVENT_BTN_LONG (1 << 1)
#define EVENT_BTN_RELEASED_SHORT (1 << 2)
#define EVENT_BTN_RELEASED_LONG (1 << 3)
#define EVENT_BTN_RELEASED (EVENT_BTN_RELEASED_SHORT | EVENT_BTN_RELEASED_LONG)
#define EVENT_BTN_MASK (EVENT_BTN_PRESSED | EVENT_BTN_LONG | EVENT_BTN_RELEASED)
#define EVENT_SLOW_STEP (1 << 4)
#define EVENT_SLOW_COUNT (1 << 5)
#define EVENT_SLOW_MASK (EVENT_SLOW_STEP | EVENT_SLOW_COUNT)
#define EVENT_ADC_LIGHT (1 << 6)
#define EVENT_ADC_VOLTAGE (1 << 7)
#define EVENT_ADC_MASK (EVENT_ADC_LIGHT | EVENT_ADC_VOLTAGE)
/* Handles signaling of events from interrupts to the main code flow. */
class AsyncEvent
{
volatile uint8_t mask;
public:
/* Set event flags with interrupts blocked (e.g. inside interrupt). */
void signal(uint8_t ev) __attribute__((always_inline))
{
mask |= ev;
}
/* Set and unset event flags with interrupts blocked. */
void signal(uint8_t ev, uint8_t unset) __attribute__((always_inline))
{
mask = (mask & ~unset) | ev;
}
/* Test if interrupt flags are set */
bool test(uint8_t ev) __attribute__((always_inline))
{
return (mask & ev) != 0;
}
/* Clear interrupt flags with active interrupts. */
void clear(uint8_t ev)
{
cli();
mask &= ~ev;
sei();
}
/* Test if interrupt flags are set, and clear if true. */
bool test_and_clear(uint8_t ev) __attribute__((always_inline))
{
return test_and_clear(ev, ev);
}
/* Test if interrupt flags are set, and clear some if true. */
bool test_and_clear(uint8_t ev, uint8_t clr)
{
cli();
uint8_t m = mask;
bool rv = (m & ev) != 0;
if (rv)
mask = m & ~clr;
sei();
return rv;
}
/* Sleep until any of the flags are set. */
void await_any(uint8_t events)
{
cli();
while ((mask & events) == 0)
{
sei();
sleep_cpu();
cli();
}
sei();
}
} async_event;
/************* ADC *************/
#define VOLTAGE_PERIOD 20 // Sample voltage once per every N light level checks
#define ADC_VOLT_VCC 0
#define VOLTAGE_ADC_CHANNEL 12
#define ADC_MUX_COMMON (_BV(ADLAR) | ADC_VOLT_VCC)
#define ADC_CONVERT (_BV(ADEN) | _BV(ADSC) | _BV(ADIE) | _BV(ADIF))
class ADCController {
enum Task {
ADC_IDLE, // ADC is disabled
ADC_RESTART, // ADC started in a non-idle state
ADC_SAMPLE_LIGHT, // ADC is sampling light level
ADC_DELAY_VOLTAGE, // ADC is waiting for voltage to stabilize
ADC_SAMPLE_VOLTAGE, // ADC is sampling voltage
};
volatile uint8_t task;
volatile uint8_t voltage_period_timer, voltage_delay_timer;
void stop() {
ADCSRA = 0;
PRR |= _BV(PRADC);
task = ADC_IDLE;
if (!timer_on)
MCUCR = MCUCR_SLEEP_POFF;
}
void on_timer_stop()
{
if (task == ADC_DELAY_VOLTAGE)
stop();
MCUCR = (task == ADC_IDLE) ? MCUCR_SLEEP_POFF : MCUCR_SLEEP_ADC;
}
friend void stop_timer();
public:
/* Disables starting new ADC conversions. */
volatile bool paused;
/* Measurement of the brightness level (0 - 255) */
volatile uint8_t light;
/* Measurement of the input voltage (1.1V / VCC * 255) */
volatile uint8_t voltage;
void request_voltage()
{
cli();
voltage_period_timer = 0;
sei();
}
void start() {
if (paused)
return;
task = (task == ADC_IDLE) ? ADC_SAMPLE_LIGHT : ADC_RESTART;
PRR &= ~_BV(PRADC);
ADMUX = ADC_MUX_COMMON | LIGHT_ADC_CHANNEL;
ADCSRA = ADC_CONVERT;
if (!timer_on)
MCUCR = MCUCR_SLEEP_ADC;
}
bool on_tick() __attribute__((always_inline))
{
if (task != ADC_DELAY_VOLTAGE)
return false;
if (voltage_delay_timer == 0)
{
voltage_period_timer = VOLTAGE_PERIOD;
task = ADC_SAMPLE_VOLTAGE;
ADCSRA = ADC_CONVERT;
return false;
}
--voltage_delay_timer;
return true;
}
void on_done() __attribute__((always_inline))
{
switch (task) {
case ADC_IDLE:
return;
case ADC_RESTART:
task = ADC_SAMPLE_LIGHT;
break;
case ADC_SAMPLE_LIGHT:
light = ADCH;
async_event.signal(EVENT_ADC_LIGHT);
if (voltage_period_timer > 0) {
voltage_period_timer--;
goto done;
}
/* According to the datasheet, switching to the voltage channel requires at least 1ms
stabilization delay, which is timed using the PWM timer; if disabled, can't be done. */
else if (!timer_on)
goto done;
else {
task = ADC_DELAY_VOLTAGE;
ADMUX = ADC_MUX_COMMON | VOLTAGE_ADC_CHANNEL;
voltage_delay_timer = 1;
enable_timer_interrupt();
return;
}
case ADC_DELAY_VOLTAGE:
// Shouldn't happen, ignore
return;
case ADC_SAMPLE_VOLTAGE:
voltage = ADCH;
async_event.signal(EVENT_ADC_VOLTAGE);
goto done;
done:
default:
stop();
return;
}
ADCSRA = ADC_CONVERT;
}
} adc;
ISR(ADC_vect)
{
adc.on_done();
}
/************* LED Display Controller *************/
#define TIMSK_SEND _BV(OCIE0A)
#define TIMSK_TIMER _BV(TOIE0)
#define TIFR_CLEAR_SEND _BV(OCF0A)
#define TIFR_CLEAR_TIMER _BV(TOV0)
/*
* This class manages sending the LED mask to the external shift register, as
* well as the ~500Hz hardware timer that controls PWM dimming of the register.
*
* The PWM signal is also used to latch the shift register outputs upon the
* ON->OFF edge, so updates must be synced to the timer via an interrupt.
*
* Finally, the fast timer is necessary for timing and debouncing button
* presses, so it must be kept active while the button is pressed.
*/
class LedController
{
volatile uint8_t led_mask;
volatile bool send_led;
/* Transmit the new led mask to the shift register chip. */
void transmit_led_mask()
{
uint8_t bits = led_mask;
#ifndef USE_USI
uint8_t base = PORTB & ~(_BV(DATA_PIN) | _BV(CLOCK_PIN));
for (uint8_t m = 0x80; m != 0; m >>= 1)
{
uint8_t val = base | ((bits & m) ? _BV(DATA_PIN) : 0);
PORTB = val;
PORTB = val | _BV(CLOCK_PIN);
}
#else
const uint8_t usi_mode = _BV(USIWM0); // 3 wire, manual clock
const uint8_t clk_up = usi_mode | _BV(USITC); // Toggle clock
const uint8_t clk_dn = usi_mode | _BV(USITC) | _BV(USICLK); // Toggle clock and shift data
PORTB &= ~_BV(CLOCK_PIN);
PRR &= ~_BV(PRUSI);
USIDR = bits;
USICR = usi_mode;
USICR = clk_up;
USICR = clk_dn;
USICR = clk_up;
USICR = clk_dn;
USICR = clk_up;
USICR = clk_dn;
USICR = clk_up;
USICR = clk_dn;
USICR = clk_up;
USICR = clk_dn;
USICR = clk_up;
USICR = clk_dn;
USICR = clk_up;
USICR = clk_dn;
USICR = clk_up;
USICR = 0;
PRR |= _BV(PRUSI);
#endif
}
public:
friend void start_timer();
/* Change the PWM duty cycle. */
void set_pwm_duty(uint8_t val)
{
/* Make sure the PWM duty cycle is not 0% or 100%, as toggling
states is necessary for shift register latching. */
if (val == 0xff)
val = 0xfe;
else if (val == 0)
val = 1;
OCR0A = val;
}
/* Set a new LED mask to be transmitted (timer assumed to be running) */
void queue_mask(uint8_t mask)
{
cli();
led_mask = mask;
send_led = true;
// Enable send timer interrupt
TIFR = TIFR_CLEAR_SEND;
TIMSK |= TIMSK_SEND;
sei();
}
/* Check that all LEDs are off, i.e. a zero mask has been fully transmitted. */
bool is_off()
{
return led_mask == 0 && !send_led;
}
void on_send_mask() __attribute__((always_inline))
{
/* Shift register latch clock is tied to output enable, so transmission needs
to be synced to the PWM timing. This interrupt transmits immediately after
an up edge of the clock, to maximize available time until next update.
Due to low clock speed, there are only 256 CPU cycles per PWM cycle. */
if (send_led)
{
send_led = false;
transmit_led_mask();
}
// Disable button timer interrupt
TIMSK &= ~TIMSK_SEND;
}
} led;
ISR(TIM0_COMPA_vect)
{
led.on_send_mask();
}
void start_timer()
{
if (timer_on)
return;
WOKWI_DEBUG_PRINTLN("start_timer")
// Set up the timer to fire ~500 times a second
timer_on = true;
led.send_led = false;
TCCR0A = _BV(COM0A1) | _BV(COM0A0) | _BV(WGM01) | _BV(WGM00); // Fast PWM with top 0xff and inverted A
OCR0B = 0;
TCNT0 = 0; // Set counter 0 to zero
TCCR0B = _BV(CS00); // clock frequency
MCUCR = MCUCR_SLEEP_IDLE;
}
void stop_timer()
{
if (!timer_on)
return;
WOKWI_DEBUG_PRINTLN("stop_timer")
cli();
TCCR0A = TCCR0B = 0;
adc.on_timer_stop();
timer_on = false;
sei();
}
void enable_timer_interrupt()
{
// Enable button timer interrupt
uint8_t msk = TIMSK;
if (!(msk & TIMSK_TIMER))
{
TIFR = TIFR_CLEAR_TIMER;
TIMSK = msk | TIMSK_TIMER;
}
}
/************* Button Press Detector *************/
/*
* This class manages detecting button presses with debounce
* and distinguishing long and short presses.
*/
class Button
{
volatile bool down;
volatile uint8_t up_timer = 0, down_timer = 0;
public:
bool is_down() {
return down;
}
/* Fast timer interrupt driven by the LedController hardware timer. */
bool on_tick() __attribute__((always_inline))
{
if (!down)
return false;
uint8_t up = up_timer;
if (up) {
/* Timeout upon physical release of the button in order to debounce
the signal by ignoring very short releases followed by resumed hold. */
if (--up == 0)
{
down = false;
async_event.signal(down_timer ? EVENT_BTN_RELEASED_SHORT : EVENT_BTN_RELEASED_LONG);
down_timer = 0;
return false;
}
up_timer = up;
}
else
{
/* The button is held down, so measure time and upgrade to long press if necessary. */
uint8_t dn = down_timer;
if (dn) {
if (--dn == 0) {
async_event.signal(EVENT_BTN_LONG);
}
down_timer = dn;
}
}
return true;
}
bool is_up_hardware() __attribute__((always_inline))
{
return (PINB & _BV(BUTTON_PIN)) != 0;
}
/* Hardware pin state change detector interrupt. */
void on_toggle() __attribute__((always_inline))
{
if (is_up_hardware())
{
if (down) {
/* Start a timer to ignore releases that are too short. */
up_timer = BTN_DEBOUNCE_DELAY;
}
}
else
{
if (!down)
{
down = true;
down_timer = 250;
async_event.signal(EVENT_BTN_PRESSED, EVENT_BTN_MASK);
enable_timer_interrupt();
}
up_timer = 0;
}
}
} btn;
ISR(PCINT0_vect)
{
btn.on_toggle();
}
ISR(TIMER0_OVF_vect)
{
bool keep = false;
keep = btn.on_tick() || keep;
keep = adc.on_tick() || keep;
if (!keep)
{
// Disable button timer interrupt
TIMSK &= ~TIMSK_TIMER;
}
}
/************* Slow Timer *************/
#define SLOW_TIMER_NORMAL (_BV(WDIE) | _BV(WDP2) | _BV(WDP0)) // 0.5 sec interrupt (2 Hz)
#define SLOW_TIMER_FAST (_BV(WDIE) | _BV(WDP1)) // 0.06 sec interrupt (15.6 Hz)
/* A timer for slow rate events, normally running at 2Hz using the WDT interrupt. */
class SlowTimer
{
/* A count down timer for delayed actions. */
volatile uint16_t count;
public:
void init(uint8_t wdtcr = SLOW_TIMER_NORMAL)
{
wdt_reset();
MCUSR = 0x00; // Clear WDRF in MCUSR
WDTCR = _BV(WDCE) | _BV(WDE);
WDTCR = wdtcr;
count = 0;
}
void reconfigure(uint8_t wdtcr = SLOW_TIMER_NORMAL)
{
cli();
init(wdtcr);
// Clear all pending timer events since the timer was reinitialized
async_event.signal(0, EVENT_SLOW_MASK);
sei();
}
void start_count(uint16_t val)
{
cli();
count = val;
async_event.signal(0, EVENT_SLOW_COUNT);
sei();
}
void stop_count() __attribute__((always_inline))
{
start_count(0);
}
void on_step() __attribute__((always_inline))
{
uint16_t cnt = count;
if (cnt != 0)
{
count = --cnt;
if (cnt == 0)
{
async_event.signal(EVENT_SLOW_STEP | EVENT_SLOW_COUNT);
return;
}
}
async_event.signal(EVENT_SLOW_STEP);
}
} slow_timer;
ISR(WDT_vect)
{
slow_timer.on_step();
adc.start();
}
/************* LED Pattern Randomizer *************/
#define MAX_LEDS 6
/* Window rows within the led mask; bits read left to right, top to bottom. */
#define ALL_LEDS_MASK 0x3f
#define TOP_ROW_MASK 0x38
#define TOP_ROW_SHIFT 3
#define BOTTOM_ROW_MASK 0x07
/*
* Manages turning a single random LED on or off.
*/
class RandomLeds
{
uint8_t active_mask = 0, active_leds = 0;
public:
bool is_max() { return active_leds >= MAX_LEDS; }
bool is_zero() { return active_leds == 0; }
uint8_t get_mask() { return active_mask; }
uint8_t num_active() { return active_leds; }
void reset()
{
active_mask = active_leds = 0;
led.queue_mask(0);
}
bool change(bool on)
{
uint8_t gap = on ? (MAX_LEDS - active_leds) : active_leds;
if (gap == 0)
return false;
WOKWI_DEBUG_PRINTLN(on ? "led ON" : "led OFF")
uint8_t idx = 0;
if (gap > 1)
{
uint8_t mask = gap > 4 ? 7 : gap > 2 ? 3 : 1;
do {
idx = rng.bits(3) & mask;
}
while (idx >= gap);
}
uint8_t mask = active_mask;
uint8_t bit_mask = 1;
for (; (bit_mask & ALL_LEDS_MASK) != 0; bit_mask <<= 1)
{
if (bool(mask & bit_mask) != on)
{
if (idx == 0) {
mask ^= bit_mask;
active_leds += (on ? 1 : -1);
active_mask = mask;
led.queue_mask(mask);
return true;
}
idx--;
}
}
return false;
}
} random_leds;
/************* Main Logic *************/
/* Menu timeout without action (1 minute using 2 Hz timer). */
#define MENU_TIMEOUT_DELAY (2*60)
/* Fastest period of the window by window transitions (30 seconds at the 2 Hz slow rate). */
#ifdef WOKWI
#define WINDOW_STEP_PERIOD (2*5) /* Reduce the timer in the simulator to 5 seconds. */
#else
#define WINDOW_STEP_PERIOD (2*30)
#endif
/* User-selectable window update delay values (0.5, 1, 2, 5, 10, 15, 30, 60 min) */
const uint16_t PROGMEM update_delay_values[8] = {
WINDOW_STEP_PERIOD, 2*60*1, 2*60*2, 2*60*5, 2*60*10, 2*60*15, 2*60*30, 2*60*60
};
/* Extra margin applied to the light threshold once darkness has been detected. */
#define LIGHT_HYSTERESIS 16
/* A light level drop bigger than this within one sample is considered abrupt. */
#define ABRUPT_DROP_DELTA 24
/* Number of times the windows blink to report a low battery. */
#define LOW_BATTERY_BLINKS 3
/* Number of bottom row steps of the light threshold progress bar. */
#define THRESHOLD_BAR_MAX 3
/* Button hold time that enters calibration from power up (5 seconds at 2 Hz). */
#define CALIBRATION_HOLD_TIME (2*5)
/* Duty cycles of the two calibration steps; exactly 2:1 to each other. */
#define CALIBRATION_DUTY_FULL 0xfc
#define CALIBRATION_DUTY_HALF 0x7e
#define CALIBRATION_DUTY_QUARTER 0x3f
#define CALIBRATION_5V_VOLTAGE 56 /* 1.1/5*255 */
/* Voltage compensated PWM duty cycle, before the user brightness setting. */
uint8_t compensated_voltage = 0xff;
uint8_t compensated_duty = 0xff;
/* The most recent light level sample, used to detect abrupt changes. */
uint8_t last_light;
/* Rapidly light all leds one by one with a gradually slowing down rate. */
void ramp_on(uint8_t abort_on_event = 0)
{
// Switch the watchdog timer to a faster rate temporarily (~15.6 Hz)
adc.paused = true;
slow_timer.reconfigure(SLOW_TIMER_FAST);
WOKWI_DEBUG_PRINTLN("ramp_on")
uint8_t delay_count = 0;
slow_timer.start_count(++delay_count);
random_leds.change(true);
while (!random_leds.is_max())
{
async_event.await_any(EVENT_SLOW_COUNT | abort_on_event);
if (async_event.test(abort_on_event))
break;
if (async_event.test(EVENT_SLOW_COUNT))
{
slow_timer.start_count(++delay_count);
random_leds.change(true);
}
}
WOKWI_DEBUG_PRINTLN("ramp_on end")
// Return back to the normal 2 Hz rate
slow_timer.reconfigure();
adc.paused = false;
}
/* Wait until the button is physically released, discarding the press. */
void wait_button_release()
{
for (;;)
{
cli();
bool down = btn.is_down();
async_event.clear(EVENT_BTN_MASK); // sei() inside
if (down)
async_event.await_any(EVENT_BTN_RELEASED);
else
break;
}
}
/* Sleep until the next slow timer step. */
void wait_slow_step()
{
async_event.clear(EVENT_SLOW_STEP);
async_event.await_any(EVENT_SLOW_STEP);
async_event.clear(EVENT_SLOW_STEP);
}
/*
* Determine the led forward voltage drop indirectly, by asking the user to
* find the supply voltage at which halving the duty cycle keeps the average
* current unchanged.
*
* With an ideal led and a series resistor the average current is
* duty * (V - Vf) / R, so equal current at (V0, 1) and (V1, 1/2) means that
* Vf = 2*V0 - V1, and the compensated duty cycle at voltage V becomes
*
* duty = (V0 - Vf) / (V - Vf)
*
* The ADC measures the 1.1V reference against VCC, i.e. r = K / V, which
* turns the above into a form where the reference voltage cancels out:
*
* duty = P * r / (Q - S * r)
* P = r0 - r1, Q = r0 * r1, S = 2*r1 - r0
*/
void compute_calibration(uint8_t r0, uint8_t r1, uint8_t factor /* 2..4 */)
{
/* Keep bogus input from producing a division by zero later on. */
if (r1 >= r0)
r1 = r0 - 1;
settings.volt_coeff_p = r0 - r1;
settings.volt_coeff_q = uint16_t(r0) * r1 * (factor - 1);
/* A negative S would mean a negative voltage drop, so clamp it to zero. */
uint16_t s1 = uint16_t(r1) * factor;
uint16_t s2 = uint16_t(r0);
settings.volt_coeff_s = (s1 > s2) ? s1 - s2 : 0;
}
/* Recompute the voltage compensation from the last voltage measurement. */
uint8_t compute_compensation(uint8_t v)
{
/* Note that the ADC measures the 1.1V reference against VCC, so the
value is inversely proportional to the actual supply voltage. */
if (v < settings.low_voltage_threshold)
{
/* duty = 255 * P * r / (Q - S * r), see compute_calibration() for the derivation.
Runs at most once per voltage sample, i.e. every 10 seconds, so the
wide arithmetic here is not a concern. */
uint16_t den = settings.volt_coeff_q - settings.volt_coeff_s * v;
if (den != 0) {
uint32_t val = uint32_t(uint16_t(settings.volt_coeff_p) * v) * 255u / den;
if (val < 0xff)
return uint8_t(val);
}
}
return 0xff;
}
/* Update voltage compensation if the voltage changed. */
void update_compensation()
{
uint8_t v = adc.voltage;
if (v != compensated_voltage)
{
compensated_duty = compute_compensation(v);
compensated_voltage = v;
}
}
/* Apply the user brightness setting on top of the voltage compensation. */
void apply_pwm_duty()
{
uint16_t duty = uint16_t(compensated_duty) * ((settings.user.brightness & 7) + 1);
led.set_pwm_duty(duty >> 3);
}
/* Compare the current light level to the threshold, applying hysteresis. */
bool is_dark(bool was_dark)
{
uint16_t threshold = settings.light_threshold;
if (was_dark)
threshold += LIGHT_HYSTERESIS;
return adc.light < threshold;
}
/* Number of windows to keep lit in steady operation. */
uint8_t window_target()
{
uint8_t val = settings.user.num_windows;
/* One window must stay free so that the pattern can keep changing between target and target+1. */
return (val < MAX_LEDS) ? val : MAX_LEDS - 1;
}
/* Delay between random window updates in steady operation. */
uint16_t window_update_delay()
{
return pgm_read_word(&update_delay_values[settings.user.update_delay & 7]);
}
/* Report a low battery by blinking the lit windows off and on via PWM. */
void low_battery_blink()
{
WOKWI_DEBUG_PRINTLN("low battery")
for (int8_t i = LOW_BATTERY_BLINKS; i > 0; i--)
{
led.set_pwm_duty(0);
wait_slow_step();
apply_pwm_duty();
wait_slow_step();
}
}
/************* User Settings Menu *************/
/* The plain user settings are consecutive single bytes, so index them directly. */
uint8_t *menu_value_ptr(int8_t index)
{
return (uint8_t*)&settings.user + index;
}
/* Increase a plain setting by one, wrapping back to its lowest value. */
void menu_increment(int8_t index)
{
uint8_t val = *menu_value_ptr(index) + 1;
if (val > pgm_read_byte(&setting_limits[index][1]))
val = pgm_read_byte(&setting_limits[index][0]);
*menu_value_ptr(index) = val;
}
/*
* Run the settings menu until a long press walks past the last setting.
* The top row blinks the setting number, while the bottom row shows the value.
*/
void run_menu()
{
WOKWI_DEBUG_PRINTLN("menu")
uint8_t index = SettingBrightess;
uint8_t progress = 0;
bool blink = true;
/* Start a timer in order to automatically exit the menu upon inaction. */
async_event.clear(EVENT_SLOW_MASK);
slow_timer.start_count(MENU_TIMEOUT_DELAY);
for (;;)
{
/* Update the display. */
uint8_t display_index = blink ? index + 1 : 0;
uint8_t display_value;
if (index == SettingLightThreshold)
display_value = BOTTOM_ROW_MASK << (THRESHOLD_BAR_MAX - progress);
else
display_value = *menu_value_ptr(index);
led.queue_mask(((display_index << TOP_ROW_SHIFT) & TOP_ROW_MASK) | (display_value & BOTTOM_ROW_MASK));
/* Wait for events. */
async_event.await_any(EVENT_BTN_LONG | EVENT_BTN_RELEASED_SHORT | EVENT_SLOW_STEP | EVENT_SLOW_COUNT | EVENT_ADC_VOLTAGE);
/* Long press switches to the next menu setting. */
if (async_event.test_and_clear(EVENT_BTN_LONG, EVENT_BTN_MASK))
{
if (++index > SettingLightThreshold)
break;
WOKWI_DEBUG_PRINT("setting ")
WOKWI_DEBUG_PRINTLN(index)
progress = 0;
blink = true;
slow_timer.start_count(MENU_TIMEOUT_DELAY);
continue;
}
/* Short press changes the setting value. */
if (async_event.test_and_clear(EVENT_BTN_RELEASED_SHORT, EVENT_BTN_MASK))
{
if (index != SettingLightThreshold)
{
menu_increment(index);
apply_pwm_duty();
}
/* Light threshold is sampled from the actual sensor after 3 presses to avoid accidental changes. */
else if (progress < THRESHOLD_BAR_MAX)
{
if (++progress == THRESHOLD_BAR_MAX)
{
/* Adjust the measured light level to center the hysterisis zone on it,
and ensure both transitions can happen. */
uint8_t val = adc.light;
val = (val <= LIGHT_HYSTERESIS/2) ? 1 : val - LIGHT_HYSTERESIS/2;
if (val >= 255 - LIGHT_HYSTERESIS)
val = 255 - LIGHT_HYSTERESIS;
settings.light_threshold = val;
WOKWI_DEBUG_PRINT("threshold ")
WOKWI_DEBUG_PRINTLN(settings.light_threshold)
}
}
slow_timer.start_count(MENU_TIMEOUT_DELAY);
continue;
}
if (async_event.test_and_clear(EVENT_ADC_VOLTAGE))
{
update_compensation();
apply_pwm_duty();
}
if (async_event.test_and_clear(EVENT_SLOW_STEP))
blink = !blink;
if (async_event.test_and_clear(EVENT_SLOW_COUNT))
break;
}
save_settings();
/* Restore the window pattern of the interrupted operation mode. */
led.queue_mask(random_leds.get_mask());
slow_timer.stop_count();
wait_button_release();
}
/************* Calibration *************/
/* Wait for a button press and take a fresh voltage measurement. */
uint8_t calibration_measure()
{
wait_button_release();
async_event.await_any(EVENT_BTN_PRESSED);
async_event.clear(EVENT_BTN_MASK);
async_event.clear(EVENT_ADC_VOLTAGE);
adc.request_voltage();
adc.paused = false;
async_event.await_any(EVENT_ADC_VOLTAGE);
async_event.clear(EVENT_ADC_VOLTAGE);
return adc.voltage;
}
void calibrate()
{
WOKWI_DEBUG_PRINTLN("calibrate")
/* Light everything to signal the start of calibration, and wait for button release. */
led.set_pwm_duty(CALIBRATION_DUTY_HALF);
led.queue_mask(ALL_LEDS_MASK);
// Pause ADC to reduce overhead noise
adc.paused = true;
wait_button_release();
/* First phase: all LEDs off, but PWM active.
* User: Measure and record overhead current at different voltages.
* Press the button to proceed. */
led.queue_mask(0);
async_event.await_any(EVENT_BTN_PRESSED);
/* Second phase: light everything at the full duty cycle.
* User: Set voltage to the low battery threshold, and record the current.
* Press the button to proceed. */
led.set_pwm_duty(CALIBRATION_DUTY_FULL);
led.queue_mask(ALL_LEDS_MASK);
uint8_t r0 = calibration_measure();
if (r0 < 2)
r0 = 2;
// Record the low battery threshold
settings.low_voltage_threshold = r0;
/* Second phase: halve the duty cycle.
* User: Raise voltage until the LED current is restored, taking overhead
* current variance with voltage into account if necessary.
* Press the button to proceed. */
led.set_pwm_duty(CALIBRATION_DUTY_HALF);
adc.paused = true;
uint8_t r1 = calibration_measure();
compute_calibration(r0, r1, 2);
/* If at 5V duty is <= 25%, recalibrate at 25% duty to improve precision. */
if (compute_compensation(56) <= 64)
{
/* Third phase: Halve the duty cycle again to 25%.
* User: Raise voltage again, same as second phase.
* Press the button to proceed. */
led.set_pwm_duty(CALIBRATION_DUTY_QUARTER);
adc.paused = true;
uint8_t r2 = calibration_measure();
compute_calibration(r0, r2, 4);
}
save_settings();
/* Testing phase: Duty is recalculated at high rate to allow checking
* how well the obtained calibration parameters work at different voltages.
* Press the button to finish. */
adc.request_voltage();
update_compensation();
led.set_pwm_duty(compensated_duty);
wait_button_release();
slow_timer.reconfigure(SLOW_TIMER_FAST);
for(;;)
{
adc.request_voltage();
async_event.await_any(EVENT_BTN_PRESSED | EVENT_ADC_VOLTAGE);
if (async_event.test_and_clear(EVENT_BTN_PRESSED))
break;
if (async_event.test_and_clear(EVENT_ADC_VOLTAGE))
{
update_compensation();
led.set_pwm_duty(compensated_duty);
}
}
slow_timer.reconfigure();
led.queue_mask(0);
}
void initialize()
{
// Clock divider = 64 (125 kHz) for very low power consumption
CLKPR = _BV(CLKPCE);
CLKPR = 6;
PRR = _BV(PRTIM1) | _BV(PRUSI) | _BV(PRADC);
load_settings();
// Set up the watchdog timer to 2 Hz interrupt rate
slow_timer.init();
// Set up button change interrupt
GIMSK |= _BV(PCIE);
PCMSK |= _BV(PCINT4);
// First 4 pins to output, PB4 pull up
PORTB = PORT_PULLUP_MASK;
DDRB = PORT_OUTPUT_MASK;
DIDR0 = PORT_DISABLE_MASK;
apply_pwm_duty();
start_timer();
// Register a button that is already held down at power up
btn.on_toggle();
// Send a zero led mask; also enables interrupts
random_leds.reset();
}
void stop_timer_if_idle()
{
if (!timer_on)
return;
cli();
if (led.is_off() && !btn.is_down())
stop_timer();
sei();
}
int main(void)
{
bool dark = false, abrupt = false, battery_pending = false;
initialize();
/* Calibration mode is entered by holding the button from power up. */
if (btn.is_down())
{
slow_timer.start_count(CALIBRATION_HOLD_TIME);
async_event.await_any(EVENT_SLOW_COUNT | EVENT_BTN_RELEASED);
if (btn.is_down())
calibrate();
/* Discard the press, so that it does not act as a normal one. */
wait_button_release();
}
/* Compute the initial voltage compensation. */
adc.request_voltage();
async_event.await_any(EVENT_ADC_VOLTAGE);
async_event.clear(EVENT_ADC_VOLTAGE);
update_compensation();
apply_pwm_duty();
rng.init(adc.light);
/* Standby: everything off, waking up only to sample the light level. */
standby:
WOKWI_DEBUG_PRINTLN("standby")
dark = false;
random_leds.reset();
wait_button_release();
for (;;)
{
async_event.await_any(EVENT_BTN_PRESSED | EVENT_ADC_LIGHT);
if (async_event.test_and_clear(EVENT_BTN_PRESSED))
{
abrupt = true;
goto operational;
}
if (async_event.test_and_clear(EVENT_ADC_LIGHT))
{
uint8_t light = adc.light, prev = last_light;
last_light = light;
if (is_dark(dark))
{
/* A drop this sharp within half a second means that electric
lighting was switched off rather than the sun going down. */
abrupt = uint16_t(light) + ABRUPT_DROP_DELTA < prev;
goto operational;
}
}
/* The fast timer is only needed once the zero mask has been sent. */
stop_timer_if_idle();
}
/* Operation: windows lit and slowly changing, PWM dimming active. */
operational:
WOKWI_DEBUG_PRINTLN("operational")
start_timer();
apply_pwm_duty();
async_event.clear(EVENT_SLOW_MASK | EVENT_ADC_VOLTAGE);
adc.request_voltage();
/* An abrupt drop lights everything at once, and the excess windows
are then switched off one by one just like in the gradual case.
Otherwise just one window is lit immediately. */
if (abrupt)
ramp_on(EVENT_BTN_LONG);
else
random_leds.change(true);
dark = is_dark(true);
battery_pending = dark || abrupt;
abrupt = false;
slow_timer.start_count(WINDOW_STEP_PERIOD);
for (;;)
{
async_event.await_any(EVENT_BTN_LONG | EVENT_BTN_PRESSED | EVENT_SLOW_COUNT | EVENT_ADC_MASK);
if (async_event.test_and_clear(EVENT_BTN_LONG, EVENT_BTN_MASK))
{
run_menu();
random_leds.reset();
slow_timer.start_count(2);
continue;
}
if (async_event.test_and_clear(EVENT_BTN_PRESSED))
{
/* Same as entering operation after an abrupt light drop. */
ramp_on(EVENT_BTN_LONG);
slow_timer.start_count(WINDOW_STEP_PERIOD);
continue;
}
if (async_event.test_and_clear(EVENT_ADC_VOLTAGE))
{
WOKWI_DEBUG_PRINT("VOLTAGE ")
WOKWI_DEBUG_PRINTLN(adc.voltage)
update_compensation();
apply_pwm_duty();
/* Report the low battery state once. */
if (battery_pending)
{
battery_pending = false;
#ifndef WOKWI /* The simulator always reports voltage as 5V. */
if (adc.voltage >= settings.low_voltage_threshold)
#endif
low_battery_blink();
}
}
if (async_event.test_and_clear(EVENT_ADC_LIGHT))
{
last_light = adc.light;
bool now_dark = is_dark(dark);
if (now_dark != dark)
{
WOKWI_DEBUG_PRINTLN(now_dark ? "dark" : "bright")
dark = now_dark;
slow_timer.start_count(WINDOW_STEP_PERIOD);
}
}
if (async_event.test_and_clear(EVENT_SLOW_COUNT))
{
if (!dark)
{
/* Shutting down because it is bright now. */
random_leds.change(false);
if (random_leds.is_zero())
{
if (!btn.is_down())
goto standby;
slow_timer.start_count(1);
}
else
slow_timer.start_count(WINDOW_STEP_PERIOD);
}
else
{
uint8_t target = window_target();
/* Oscillating around the target keeps the pattern changing. */
random_leds.change(random_leds.num_active() <= target);
/* Update as configured by the user close to the target, and faster otherwise. */
if (uint8_t(random_leds.num_active() - target) <= 1)
slow_timer.start_count(window_update_delay());
else
slow_timer.start_count(WINDOW_STEP_PERIOD);
}
}
}
}
Ambient Light Level
PWM ON