// ===================================================================================
// Project: NeoCandle - Candle Simulation based on ATtiny25/45/85
// Version: v1.6
// Year: 2019 - 2021
// Author: Stefan Wagner
// Github: https://github.com/wagiminator
// EasyEDA: https://easyeda.com/wagiminator
// License: http://creativecommons.org/licenses/by-sa/3.0/
// ===================================================================================
//
// Description:
// ------------
// Candle simulation using an ATtiny25/45/85 and four NeoPixels. The candle can
// be controlled by an IR remote control. It can be set into an automatic mode
// in which it's been turned on or off by the intensity of the ambient light.
// The code uses sleep mode in order to decrease power consumption.
//
// References:
// -----------
// The Neopixel implementation was inspired by Josh Levine.
// https://wp.josh.com/category/neopixel/
//
// The simulation code is based on the great work of Mark Sherman.
// https://github.com/carangil/candle
//
// The lightweight pseudo random number generator based on Galois linear feedback
// shift register is taken from Łukasz Podkalicki.
// https://blog.podkalicki.com/attiny13-pseudo-random-numbers/
//
// The IR receiver implementation (NEC protocol) is based on TinyDecoder
// https://github.com/wagiminator/ATtiny13-TinyDecoder
//
// Wiring:
// -------
// +-\/-+
// LDR --- RST ADC0 PB5 1|° |8 Vcc
// NEOPIXELS ------- ADC3 PB3 2| |7 PB2 ADC1 --------
// IR RECEIVER ------- ADC2 PB4 3| |6 PB1 AIN1 OC0B ---
// GND 4| |5 PB0 AIN0 OC0A ---
// +----+
//
// Compilation Settings:
// ---------------------
// Core: ATtinyCore (https://github.com/SpenceKonde/ATTinyCore)
// Board: ATtiny25/45/85 (No bootloader)
// Chip: ATtiny25 or 45 or 85 (depending on your chip)
// Clock: 8 MHz (internal)
// B.O.D.: disabled
//
// Leave the rest on default settings. Don't forget to "Burn bootloader"!
// No Arduino core functions or libraries are used. Use the makefile if
// you want to compile without Arduino IDE.
//
// Note: RESET pin is used as a weak analog input for the LDR light sensor.
// You don't need to disable the RESET pin as the voltage won't go
// below 40% of Vcc.
//
// Fuse settings: -U lfuse:w:0xe2:m -U hfuse:w:0xd7:m -U efuse:w:0xff:m
//
// Operating Instructions:
// -----------------------
// Connect a 5V power supply to the Micro-USB socket or a battery to the respective
// board header. The NeoCandle should immediately mimic the flickering of a candle.
// The device is now in TIMER mode and switches off automatically after the time
// programmed in the code has elapsed. You can switch it off at any time by pressing
// the power-off button on the IR remote control and switch it back on with the
// power-on button. If the power-on button on the IR remote control is pressed while
// the NeoCandle is switched on, it changes to LDR mode. This change is indicated by
// a short blue light animation. In LDR mode, NeoCandle switches on and off
// automatically depending on the intensity of the ambient light. Pressing the
// power-on or power-off button on the IR remote control switches back to TIMER mode.
// ===================================================================================
// Libraries, Definitions and Macros
// ===================================================================================
// Libraries
#include <avr/io.h> // for GPIO
#include <avr/wdt.h> // for watchdog timer
#include <avr/sleep.h> // for sleep modes
#include <avr/interrupt.h> // for interrupts
#include <util/delay.h> // for delays
// Pins
#define NEO_PIN PB3 // Pin for neopixels
#define IR_PIN PB4 // Pin for IR receiver
#define LDR_AP 0 // ADC port of LDR
// IR codes
#define IR_ADDR 0xEF00 // IR device address
#define IR_PWRON 0x03 // IR code for power on
#define IR_PWROFF 0x02 // IR code for power off
#define IR_FAIL 0xFF // IR fail code
// LDR parameters
#define LDR_DARK 900 // LDR ADC threshold value for dark
#define LDR_BRIGHT 800 // LDR ADC threshold value for bright
// Auto switch off timer
#define AUTOTIMER (6*3600000) // after 6 hours
// Global variables
uint32_t timermillis = 0; // timer variable for auto switch off
uint8_t ldrmode = 0; // LDR mode flag: "1" = LDR mode on
// ===================================================================================
// Neopixel Implementation for 8MHz MCU Clock and 800kHz Pixels
// ===================================================================================
// Neopixel definitions and macros
#define NEO_PIXELS 4 // number of pixels in the string
#define NEO_init() DDRB |= (1<<NEO_PIN) // set pixel DATA pin as output
#define NEO_latch() _delay_us(251) // delay to show shifted colors
// Send a byte to the pixels string
void NEO_sendByte(uint8_t byte) {
uint8_t count = 8; // 8 bits, MSB first
asm volatile (
"sbi %[port], %[pin] \n\t" // DATA HIGH
"sbrs %[byte], 7 \n\t" // if "1"-bit skip next instruction
"cbi %[port], %[pin] \n\t" // "0"-bit: DATA LOW after 3 cycles
"add %[byte], %[byte] \n\t" // byte <<= 1
"subi %[bit], 0x01 \n\t" // count--
"cbi %[port], %[pin] \n\t" // "1"-bit: DATA LOW after 6 cycles
"brne .-14 \n\t" // while(count)
::
[port] "I" (_SFR_IO_ADDR(PORTB)),
[pin] "I" (NEO_PIN),
[byte] "w" (byte),
[bit] "w" (count)
);
}
// Switch off all pixels
void NEO_clear(void) {
cli();
for(uint8_t i = 3 * NEO_PIXELS; i; i--) NEO_sendByte(0);
sei();
}
// ===================================================================================
// Millis Counter Implementation for Timer0
// ===================================================================================
// Millis counter variable
volatile uint32_t MIL_counter = 0; // millis counter variable
// Init millis counter
void MIL_init(void) {
OCR0A = (F_CPU / 64000UL) - 1; // TOP-value for 1kHz
TCCR0A = (1<<WGM01); // timer0 CTC mode
TCCR0B = (1<<CS01) | (1<<CS00); // start timer0 with prescaler 64
TIMSK |= (1<<OCIE0A); // enable output compare match interrupt
}
// Read millis counter
uint32_t MIL_read(void) {
cli(); // disable interrupt for atomic read
uint32_t result = MIL_counter; // read millis counter
sei(); // enable interrupts
return(result); // return millis counter value
}
// Timer0 compare match A interrupt service routine (every millisecond)
ISR(TIM0_COMPA_vect) {
MIL_counter++; // increase millis counter
}
// ===================================================================================
// IR Receiver Implementation (NEC Protocol) using Timer1
// ===================================================================================
// IR receiver definitions and macros
#define IR_available() (~PINB & (1<<IR_PIN)) // return true if IR line is LOW
#define IR_PRESCALER 512 // prescaler of the timer
#define IR_time(t) ((F_CPU / 1000) * t) / IR_PRESCALER / 1000 // convert us to counts
#define IR_TOP IR_time(12000UL) // TOP value of timer (timeout)
// IR global variables
volatile uint8_t IR_dur; // for storing duration of last burst/pause
volatile uint8_t IR_flag; // gets zero in pin change or time over
// IR initialize the receiver
void IR_init(void) {
PORTB |= (1<<IR_PIN); // pullup on IR pin
PCMSK |= (1<<IR_PIN); // enable interrupt on IR pin
GIMSK |= (1<<PCIE); // enable pin change interrupts
OCR1A = IR_TOP; // timeout causes OCA interrupt
TIMSK |= (1<<OCIE1A); // enable output compare match interrupt
}
// IR wait for signal change
void IR_wait(void) {
IR_flag = 1; // reset flag
while(IR_flag); // wait for pin change or timeout
}
// IR read data according to NEC protocol
uint8_t IR_read(void) {
uint32_t data; // variable for received data
uint16_t addr; // variable for received address
if(!IR_available()) return IR_FAIL; // exit if no signal
IR_wait(); // wait for end of start burst
if(IR_dur < IR_time(8000)) return IR_FAIL; // exit if no start condition
IR_wait(); // wait for end of start pause
if(IR_dur < IR_time(4000)) return IR_FAIL; // exit if no start condition
for(uint8_t i=32; i; i--) { // receive 32 bits
data >>= 1; // LSB first
IR_wait(); // wait for end of burst
if(!IR_dur) return IR_FAIL; // exit if overflow
IR_wait(); // wait for end of pause
if(!IR_dur) return IR_FAIL; // exit if overflow
if(IR_dur > IR_time(1124)) data |= 0x80000000; // bit "0" or "1" depends on pause duration
}
IR_wait(); // wait for end of final burst
uint8_t addr1 = data; // get first address byte
uint8_t addr2 = data >> 8; // get second address byte
uint8_t cmd1 = data >> 16; // get first command byte
uint8_t cmd2 = data >> 24; // get second command byte
if((cmd1 + cmd2) < 255) return IR_FAIL; // if second command byte is not the inverse of the first
if((addr1 + addr2) == 255) addr = addr1; // check if it's extended NEC-protocol ...
else addr = data; // ... and get the correct address
if(addr != IR_ADDR) return IR_FAIL; // wrong address
return cmd1; // return command code
}
// Pin change interrupt service routine
ISR(PCINT0_vect) {
IR_dur = TCNT1; // save timer value
TCNT1 = 0; // reset timer1
TCCR1 = (1<<CS13) | (1<<CS11); // start timer1 with prescaler 512
IR_flag = 0; // raise flag
}
// Timer1 compare match A interrupt service routine (timeout)
ISR(TIM1_COMPA_vect) {
TCCR1 = 0; // stop timer1
TCNT1 = 0; // reset timer1
IR_flag = 0; // raise flag
IR_dur = 0; // set duration value to zero
}
// ===================================================================================
// ADC and LDR Implementation
// ===================================================================================
// Init ADC for LDR light sensor
void LDR_init(void) {
ADCSRA = (1<<ADPS2) | (1<<ADPS1); // set ADC clock prescaler to 64
ADMUX = LDR_AP; // set LDR port against Vcc
}
// Read LDR sensor value
uint16_t LDR_read(void) {
PRR &= ~(1<<PRADC); // power on ADC
ADCSRA |= (1<<ADEN) | (1<<ADSC); // enable ADC and start sampling
while (ADCSRA & (1<<ADSC)); // wait for sampling complete
uint16_t result = ADC; // read ADC value;
ADCSRA &= ~(1<<ADEN); // disable ADC
PRR |= (1<<PRADC); // power off ADC
return result;
}
// Switch on LDR mode
void switchOnLDRmode(void) {
// Show switching animation
for(int i=0; i<4; i++) {
cli();
for(uint8_t j=0; j<3*NEO_PIXELS; j++)
(j == ((i << 1) + i + 2)) ? NEO_sendByte(128) : NEO_sendByte(0);
sei();
_delay_ms(250);
}
// Switch on LDR mode
ldrmode = 1;
}
// ===================================================================================
// Standby, Sleep and Watchdog Implementation
// ===================================================================================
// Reset watchdog timer
void resetWatchdog(void) {
cli(); // timed sequence coming up
wdt_reset(); // reset watchdog
MCUSR = 0; // clear various reset flags
WDTCR = (1<<WDCE) | (1<<WDE) | (1<<WDIF); // allow changes, clear existing interrupt
WDTCR = (1<<WDIE) | (1<<WDP3)| (1<<WDP0); // enable watchdog, set interval to 8 seconds
sei(); // interrupts are required now
}
// Go to sleep in order to save energy, wake up again by watchdog timer or pin change interrupt
void sleep(void) {
GIFR |= (1<<PCIF); // clear any outstanding interrupts
if(ldrmode) resetWatchdog(); // get watchdog ready for LDR mode
sleep_mode(); // sleep
}
// Go to standby mode
void gotoStandby(void) {
NEO_clear(); // turn off NeoPixels
// Sleep until IR remote power on button is pressed
while(1) {
if( (IR_available()) && (IR_read() == IR_PWRON) ) {
ldrmode = 0;
timermillis = MIL_read();
break;
}
if(ldrmode && (LDR_read() > LDR_DARK)) break;
sleep();
}
}
// Watchdog interrupt service routine
ISR(WDT_vect) {
wdt_disable(); // disable watchdog
}
// ===================================================================================
// Pseudo Random Number Generator (adapted from Łukasz Podkalicki)
// ===================================================================================
// Start state (any nonzero value will work)
uint16_t rn = 0xACE1;
// Pseudo random number generator
uint16_t prng(uint16_t maxvalue) {
rn = (rn >> 0x01) ^ (-(rn & 0x01) & 0xB400);
return(rn % maxvalue);
}
// ===================================================================================
// Candle Simulation Implementation (adapted from Mark Sherman)
// ===================================================================================
// Candle simulation parameters
#define MINUNCALM ( 5 * 256)
#define MAXUNCALM (60 * 256)
#define UNCALMINC 10
#define MAXDEV 100
#define CANDLEDELAY 25
// Some variables
int16_t centerx = MAXDEV;
int16_t centery = MAXDEV / 2;
int16_t xvel = 0;
int16_t yvel = 0;
uint16_t uncalm = MINUNCALM;
int16_t uncalmdir = UNCALMINC;
uint8_t cnt = 0;
// Set one candle LED
void setPixel(int val) {
if (val > 255) val = 255;
if (val < 0 ) val = 0;
uint8_t byte = (uint8_t)val;
NEO_sendByte(byte >> 1);
NEO_sendByte(byte >> 2);
NEO_sendByte(byte >> 5);
}
// Candle simulation
void updateCandle(void) {
int movx=0;
int movy=0;
// Random trigger brightness oscillation, if at least half uncalm
if(uncalm > (MAXUNCALM / 2)) {
if(prng(2000) < 5) uncalm = MAXUNCALM * 2; // occasional 'bonus' wind
}
// Random poke, intensity determined by uncalm value (0 is perfectly calm)
movx = prng(uncalm >> 8) - (uncalm >> 9);
movy = prng(uncalm >> 8) - (uncalm >> 9);
// If reach most calm value, start moving towards uncalm
if(uncalm < MINUNCALM) uncalmdir = UNCALMINC;
// If reach most uncalm value, start going towards calm
if(uncalm > MAXUNCALM) uncalmdir = -UNCALMINC;
uncalm += uncalmdir;
// Move center of flame by the current velocity
centerx += movx + (xvel >> 2);
centery += movy + (yvel >> 2);
// Range limits
if(centerx < -MAXDEV) centerx = -MAXDEV;
if(centerx > MAXDEV) centerx = MAXDEV;
if(centery < -MAXDEV) centery = -MAXDEV;
if(centery > MAXDEV) centery = MAXDEV;
// Counter
cnt++;
if(!(cnt & 3)) {
// Attenuate velocity 1/4 clicks
xvel = (xvel * 999) / 1000;
yvel = (yvel * 999) / 1000;
}
// Apply acceleration towards center, proportional to distance from center (spring motion; hooke's law)
xvel -= centerx;
yvel -= centery;
// Set NeoPixels
cli();
setPixel(128 - centerx - centery);
setPixel(128 + centerx - centery);
setPixel(128 + centerx + centery);
setPixel(128 - centerx + centery);
sei();
}
// ===================================================================================
// Main Function
// ===================================================================================
int main(void) {
// Reset watchdog timer
resetWatchdog(); // do this first in case WDT fires
// Local variables
uint32_t lastmillis = 0;
// Disable unused peripherals and set sleep mode to save power
ACSR = (1<<ACD); // disable analog comperator
DIDR0 = ~(1<<IR_PIN) & 0x1F; // disable digital intput buffer except IR pin
PRR = (1<<PRUSI) | (1<<PRADC); // shut down USI and ADC
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // set sleep mode to power down
// Setup
NEO_init(); // init Neopixels
LDR_init(); // init ADC for LDR light sensor
MIL_init(); // init millis counter
IR_init(); // init IR receiver
sei(); // enable global interrupts
// Loop
while(1) {
// Update candle simulation
if((MIL_read() - lastmillis) > CANDLEDELAY) {
updateCandle();
lastmillis = MIL_read();
}
// Check for IR remote signals
if(IR_available()) {
uint8_t keypressed = IR_read();
if(keypressed == IR_PWRON) switchOnLDRmode();
if(keypressed == IR_PWROFF) {
ldrmode = 0;
gotoStandby();
}
}
// Turn off candle in daylight when in LDR mode
if(ldrmode && (LDR_read() < LDR_BRIGHT)) gotoStandby();
// Check auto switch off timer when not in LDR mode
if(!ldrmode && ((MIL_read() - timermillis) > AUTOTIMER)) gotoStandby();
}
}