@@ -1,24 +1,46 @@
/*
=============================================================================
FireFly Morse Throwie
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
=============================================================================
FireFly Solar / 日輪蛍 – Nichirin Hotaru / a Solar Morse Throwie
太陽を宿し、夜にモールスを灯す蛍。
Taiyō o yadoshi, yoru ni Mōrusu o tomosu hotaru.
A firefly carrying the sun, lighting Morse in the night.
=============================================================================
Project definitions, sources
-----------------------------------------------------------------------------
Version: 0.3 - ATTiny85, 1 MHz, BOD fuse disabled
Version : 0.5 - ATTiny85, 1 MHz, BOD fuse disabled
used Flash: 1284 bytes
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly
Date : 2026-06 -14
Date : 2026-11 -10
Key changes Version 0.3:
- Removed unnecessary ADC enable/disable from loop()
- Disabled ADC and analog comparator in setup()
- Added INPUT_PULLUP for unused Arduino pins 0, 1, 2 will save power
- Added safer watchdog setup with interrupt protection to save run near brownout
- Added wdt_reset() before sleep for cleaner 8 s timin g
- Added lowercase-to-uppercase handling
- Unsupported characters are now ignored without adding fake timing gaps
Key changes Version 0.5 (goal: lowest possible average current):
- No delay(), no millis(): Timer0 and Timer1 are not needed any more and
are switched off in PRR.
- ALL timing (Morse units, sensing window, 8 s sleep) is done by the watchdo g
in interrupt mode while the CPU is in POWER-DOWN. The CPU runs only for a
few microseconds per wake-up. LED pins keep their state in power-down.
- Darkness sensing by LED no longer polls digitalRead() in a busy loop (that was
~0.3-1 s awake per cycle). The LED junction is charged, then the CPU sleeps
in 16 ms steps and checks the pin: bright light exits after the first step.
- ADC is disabled BEFORE it is gated off in PRR (order matters).
- After a power-on, external, brown-out or watchdog reset the sketch waits
RECOVER_CYCLES cycles before blinking, allowing the supercap to recharge.
Default ~13 min 20 s (nominal); the debug option bypasses recovery.
- Startup code saves MCUSR and disables the watchdog early (.init3), avoiding
a watchdog-reset loop before setup() can execute.
- Optional: BOD disable during sleep (only useful if BOD fuse is enabled).
But found out that also BOD Fuse for 1.8V shorten unnecessary blinking time,
will still work ~1V, meaning ~20 min longer blinking time.
- Field Test: After a sunny day in Winter, outdoor -1 ... 8°C, with a payload
of 10 signs) it blinks for >> 12h.
Timing note: the watchdog oscillator is not precise (changes with Vcc and
temperature). All Morse timing comes from the same clock, so dit/dah/gap
ratios stay correct; only the absolute speed drifts a little.
The HW actual allowes 5V and 3.3V, standard is 3.3V. So don't expec exact timing.
-----------------------------------------------------------------------------
@@ -44,7 +66,9 @@ Inspired by Karl Lunt's FireFly project:
'.', ".-.-.-" ',', "--..--" '?', "..--.."
'!', "-.-.--" ':', "---..." ';', "-.-.-."
'(', "-.--." ')', "-.--.-" '"', ".-..-."
'@', ".--.-." '&', ".-..."
'@', ".--.-." '&', ".-..." '-', "-....-"
'/', "-..-." ''', ".----." '+', ".-.-."
'=', "-...-" '_', "..--.-"
-----------------------------------------------------------------------------
@@ -60,6 +84,12 @@ Inspired by Karl Lunt's FireFly project:
# include <avr/interrupt.h>
# include <string.h>
// ---------------------------------------------------------------------------
// DEBUG_TEST 1 = bench testing: no reset-recovery wait, a blink attempt every
// 8 s, and a short flash when the sketch thinks it is BRIGHT.
// SET TO 0 FOR THE FIELD (40 s idle sleep, ~13m20s reset recovery).
# define DEBUG_TEST 0
# ifndef cbi
# define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
# endif
@@ -68,16 +98,55 @@ Inspired by Karl Lunt's FireFly project:
# endif
// ---------------------------------------------------------------------------
// Timing: unit length in ms.
// At 1 MHz, delay() is accurate when F_CPU=1000000L is set in boards.txt.
// 100 ms gives readable optical Morse; raise to 150 ms if readability is poor.
# define unitLength 100
// Watchdog timeout codes (nominal, at ~5 V; real values vary with Vcc/temp):
// actual HW uses 3.3 (standard) or 5V (for blue LED experimental setup)
// so don't expect exact timing)
// 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms
// 6=1s 7=2s 8=4s 9=8s
//
// One Morse unit = UNIT_WDT_A + UNIT_WDT_B -> 64 ms + 32 ms = ~96 ms
// A=3, B=0 -> ~144 ms (easier to read, more LED time)
// A=3, B=0xFF -> ~128 ms
// A=2, B=0xFF -> ~64 ms (fast, less LED time, harder to read by eye)
// Use 0xFF for B to skip the second sleep.
# define UNIT_WDT_A 2
# define UNIT_WDT_B 1
// ---------------------------------------------------------------------------
// Sleep interval.
// Watchdog setup uses 8 s. For final use, change sleepCycles from 4 to 6
// for about 32s - 48s between sensing/blinking activations. Goal ~ 1 blink/min
const uint8_t sleepCycles = 6 ;
// Sleep interval between two sensing/blink attempts: sleepCycles x 8 s .
// 5 x 8 s = 40 s. Goal ~ 1 blink/min.
# if DEBUG_TEST
const uint8_t sleepCycles = 1 ;
# else
const uint8_t sleepCycles = 5 ;
# endif
// ---------------------------------------------------------------------------
// Darkness window in 16 ms steps (nominal).
// After charging the LED junction the CPU sleeps step by step. If the pin
// reads LOW within the window the LED discharged quickly = bright = no blink.
// Still HIGH after the full window = dark = blink.
// Longer window = triggers in darker conditions
// The window costs almost no energy (CPU sleeps), so it is only a threshold.
// Calibrate at real dusk. Old threshold 17000 loops: measure the old
// sensDarkness() time with micros() at your dusk level and divide it by 16 ms.
const uint8_t darkSteps = 40 ; // ~640 ms
// ---------------------------------------------------------------------------
// After a power-on / brown-out / external / watchdog reset, do not blink for
// this many cycles (each has 40 s of sleep) so the supercap can recharge.
// 20 -> ~13 min 20 s nominal. Set to 0 for bench testing/uploading.
# if DEBUG_TEST
# define RECOVER_CYCLES 0
# else
# define RECOVER_CYCLES 20
# endif
// ---------------------------------------------------------------------------
// Set to 1 only if the BOD fuse is enabled (e.g. hfuse 0xDE = 1.8 V).
// Then BOD is switched off during power-down to save its ~20 uA.
// With BOD fuse disabled this has no effect, leave it 0.
# define BOD_FUSE_ENABLED 0
// ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving).
@@ -95,28 +164,38 @@ const uint8_t sleepCycles = 6;
// ATTiny85 has 512 bytes SRAM; this keeps the Morse text in flash.
// Only uppercase letters, digits, spaces, and the special chars in the switch
// below are emitted. Lowercase letters are converted to uppercase.
// Test setup: 20 x "0" because 0 = "-----" gives maximum LED ON time for test .
// shorter text will better, more text will cost more power.
const char morseText [ ] PROGMEM = " TGO 26 " ;
// Ruler: 0....0 ....1 ...1 ....2
// Ruler: 0....5....0...5....0
// ---------------------------------------------------------------------------
// Darkness threshold.
// Higher value = triggers in brighter conditions.
// Best calibrated at actual dusk/dawn with the chosen LED type.
const unsigned int darknessThreshold = 17000 ;
// Shorter text = less LED time = less energy .
const char morseText [ ] PROGMEM = " TOGO-LAB.IO FIREFLY " ;
// Ruler: 0....0....1....1....2
// Ruler: 01 ...5 ....0 ....5 ....0
// ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR.
volatile bool f_wdt = true ;
// Watchdog interrupt flag - volatile because it is written in an ISR.
volatile bool f_wdt = false ;
// Remaining cycles without blinking after a reset.
static uint8_t recoverCycles = 0 ;
// Preserve the reset flags before Arduino startup and stop a watchdog left
// running by a previous reset. AVR-libc's recommended .init3 pattern.
// .noinit is not cleared by C runtime initialization.
static uint8_t resetCauseEarly __attribute__ ( ( section ( " .noinit " ) ) ) ;
void earlyWatchdogInit ( void ) __attribute__ ( ( used , naked , section ( " .init3 " ) ) ) ;
void earlyWatchdogInit ( void )
{
resetCauseEarly = MCUSR ;
MCUSR = 0 ; // Clear WDRF before disabling WDT.
wdt_disable ( ) ; // Prevent repeated ~15 ms watchdog resets.
}
// ---------------------------------------------------------------------------
// Forward declarations.
void setup_watchdog ( uint8_t ii ) ;
void system_sleep ( ) ;
static void sleepWdt ( uint8_t ii ) ;
static void waitUnits ( uint8_t n ) ;
void configureUnusedPins ( ) ;
void releaseLedPins ( int LED_N , int LED_P ) ;
unsigned int sensDarkness ( int LED_N , int LED_P ) ;
bool isDark ( int LED_N , int LED_P ) ;
void morse ( int LED_N , int LED_P ) ;
void dit ( int LED_P ) ;
void dah ( int LED_P ) ;
@@ -126,22 +205,28 @@ void dah(int LED_P);
// ===========================================================================
void setup ( )
{
// Disable unused peripherals immediately to save power .
// PRTIM1, PRUSI: unus ed, safe to gate off .
// PRADC: ADC is not used; LED sensing is done with digitalRead().
// PRTIM0: must stay ON while awake because delay() depends on Timer0.
PRR = ( 1 < < PRTIM1 ) | ( 1 < < PRUSI ) | ( 1 < < PRADC ) ;
// ADC off. It is not needed for digitalRead()-based LED sensing.
cbi ( ADCSRA , ADEN ) ;
// Analog comparator off. Saves a little sleep current.
ACSR | = ( 1 < < ACD ) ;
// Reset cause was captured before C/Arduino initialization in .init3 .
// Include WDRF: a watchdog reset also ne eds supercap recovery time .
if ( resetCauseEarly & ( ( 1 < < PORF ) | ( 1 < < BORF ) |
( 1 < < EXTRF ) | ( 1 < < WDRF ) ) ) {
recoverCycles = RECOVER_CYCLES ;
}
// Pins first (while the Arduino core can still reach all timer registers
// for its turnOffPWM() handling in digitalWrite()).
configureUnusedPins ( ) ;
releaseLedPins ( LED1_N_SIDE , LED1_P_SIDE ) ;
setup_watchdog ( 9 ) ; // 8-second watchdog interval.
// ADC off FIRST, then gate its clock. With PRADC set, ADC register
// writes are ignored, so the order matters.
cbi ( ADCSRA , ADEN ) ;
// Analog comparator off.
ACSR | = ( 1 < < ACD ) ;
// Power reduction: ADC, USI, Timer0, Timer1 all off.
// Nothing in this sketch uses them: no delay(), no millis(), no analogWrite().
PRR = ( 1 < < PRADC ) | ( 1 < < PRUSI ) | ( 1 < < PRTIM0 ) | ( 1 < < PRTIM1 ) ;
}
// ===========================================================================
@@ -149,22 +234,29 @@ void setup()
// ===========================================================================
void loop ( )
{
if ( f_wdt ) {
f_wdt = false ;
if ( sensDarkness ( LED1_N_SIDE , LED1_P_SIDE ) > darknessThreshold ) {
if ( recoverCycles > 0 ) {
recoverCycles - - ;
} else if ( isDark ( LED1_N_SIDE , LED1_P_SIDE ) ) {
morse ( LED1_N_SIDE , LED1_P_SIDE ) ;
}
# if DEBUG_TEST
else {
// Sketch decided "bright": one short ~128 ms flash so you can see it.
pinMode ( LED1_N_SIDE , OUTPUT ) ;
pinMode ( LED1_P_SIDE , OUTPUT ) ;
digitalWrite ( LED1_N_SIDE , LOW ) ;
digitalWrite ( LED1_P_SIDE , HIGH ) ;
sleepWdt ( 3 ) ;
digitalWrite ( LED1_P_SIDE , LOW ) ;
}
# endif
// Return LED pins to high-Z before sleeping.
// Internal pullups stay disabled for the LED pins.
releaseLedPins ( LED1_N_SIDE , LED1_P_SIDE ) ;
}
// Test setup: 1 x 8 s sleep.
// Final setup: set sleepCycles = 4 for about 32 s.
for ( uint8_t i = 0 ; i < sleepCycles ; i + + ) {
system_sleep ( ) ;
sleepWdt ( 9 ) ; // 8 s each
}
}
@@ -188,87 +280,110 @@ void releaseLedPins(int LED_N, int LED_P)
}
// ===========================================================================
// Sleep helpers
// Sleep helpers (watchdog = only timebase)
// ===========================================================================
void system_sleep ( )
ISR ( WDT_vect )
{
// Reset watchdog counter so each sleep cycle starts with a fresh interval.
wdt_reset ( ) ;
set_sleep_mode ( SLEEP_MODE_PWR_DOWN ) ;
// Atomic sleep entry pattern.
// After sei(), AVR executes the next instruction before servicing interrupts,
// so sleep_cpu() is not skipped by a just-pending interrupt.
cli ( ) ;
sleep_enable ( ) ;
sei ( ) ;
sleep_cpu ( ) ;
// WDT interrupt wakes the CPU here.
sleep_disable ( ) ;
f_wdt = true ;
}
// Watchdog setup - ii selects timeout:
// 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms
// 6=1s 7=2s 8=4s 9=8s
void setup_watchdog ( uint8_t ii )
// Sleep in POWER-DOWN until the watchdog interrupt fires after timeout code ii
// ( 0=16 ms ... 9=8 s). Reconfigures the watchdog on every call, so the full
// interval always starts fresh.
static void sleepWdt ( uint8_t ii )
{
if ( ii > 9 ) ii = 9 ;
uint8_t bb = ii & 7 ;
if ( ii > 7 ) bb | = ( 1 < < WDP3 ) ;
uint8_t oldSREG = SREG ;
// Interrupt-only mode (WDE = 0). WDIF written as 1 clears a stale flag.
const uint8_t cfg = bb | ( 1 < < WDIE ) | ( 1 < < WDIF ) ;
f_wdt = false ;
cli ( ) ;
wdt_reset ( ) ;
MCUSR & = ~ ( 1 < < WDRF ) ;
// Timed sequence: enable configuration change, then set interrupt-only WDT .
WDTCR | = ( 1 < < WDCE ) | ( 1 < < WDE ) ;
WDTCR = bb | ( 1 < < WDIE ) ;
// WDCE/WDE unlock and configuration: two consecutive OUT instructions .
// The second write is guaranteed to occur within the required four cycles.
const uint8_t unlock = _BV ( WDCE ) | _BV ( WDE ) ;
__asm__ __volatile__ (
" out %[wdt], %[unlock] \n \t "
" out %[wdt], %[config] \n \t "
:
: [ wdt ] " I " ( _SFR_IO_ADDR ( WDTCR ) ) ,
[ unlock ] " r " ( unlock ) ,
[ config ] " r " ( cfg )
: " memory "
) ;
SREG = oldSREG ;
set_sleep_mode ( SLEEP_MODE_PWR_DOWN ) ;
// Atomic sleep entry: after sei() the next instruction (sleep_cpu) still
// executes before any interrupt is serviced, so the wake-up cannot be missed.
// Loop again if something else woke the CPU.
while ( ! f_wdt ) {
sleep_enable ( ) ;
# if BOD_FUSE_ENABLED
sleep_bod_disable ( ) ;
# endif
sei ( ) ;
sleep_cpu ( ) ;
sleep_disable ( ) ;
cli ( ) ;
}
sei ( ) ;
}
ISR ( WDT_vect )
// Wait n Morse units in power-down.
static void waitUnits ( uint8_t n )
{
f_wdt = true ;
while ( n - - ) {
sleepWdt ( UNIT_WDT_A ) ;
# if UNIT_WDT_B != 0xFF
sleepWdt ( UNIT_WDT_B ) ;
# endif
}
}
// ===========================================================================
// Light sensor
// ===========================================================================
// Returns a "darkness level": higher = darker .
// Charges the LED junction capacitance, then times how long it takes to bleed
// back through the reverse-biased LED unt il the input reads LOW .
// ~30000 = pitch black, ~0 = bright light
unsigned int sensDarkness ( int LED_N , int LED_P )
// Returns true if it is dark.
// Charges the LED junction capacitance, then lets it bleed through the
// reverse-biased LED wh ile the CPU sleeps. Checks the pin every ~16 ms .
// LOW within the window -> bright (discharged fast)
// still HIGH at the end -> dark
// In power-down the input buffer of the floating pin is disabled by hardware,
// so the slowly falling voltage causes no shoot-through current.
bool isDark ( int LED_N , int LED_P )
{
unsigned int i ;
// Charge the LED junction capacitance.
pinMode ( LED_N , OUTPUT ) ;
pinMode ( LED_P , OUTPUT ) ;
digitalWrite ( LED_N , HIGH ) ;
digitalWrite ( LED_P , LOW ) ;
// Let the N-side float and measure bleed-down time.
// Let the N-side float (no pullup) and measure bleed-down time.
pinMode ( LED_N , INPUT ) ;
digitalWrite ( LED_N , LOW ) ; // disable internal pullup
digitalWrite ( LED_N , LOW ) ;
for ( i = 0 ; i < 30000 ; i + + ) {
if ( digitalRead ( LED_N ) = = LOW ) break ;
bool dark = true ;
for ( uint8_t k = 0 ; k < darkSteps ; k + + ) {
sleepWdt ( 0 ) ; // ~16 ms
if ( digitalRead ( LED_N ) = = LOW ) {
dark = false ; // bright: stop early
break ;
}
}
// Clean up after sensing: both LED pins low, then caller releases them.
// Clean up after sensing: both LED pins low, caller releases them.
pinMode ( LED_N , OUTPUT ) ;
digitalWrite ( LED_N , LOW ) ;
pinMode ( LED_P , OUTPUT ) ;
digitalWrite ( LED_P , LOW ) ;
return i ;
return dark ;
}
// ===========================================================================
@@ -277,17 +392,17 @@ unsigned int sensDarkness(int LED_N, int LED_P)
void dit ( int LED_P )
{
digitalWrite ( LED_P , HIGH ) ;
delay ( unitLength ) ;
waitUnits ( 1 ) ;
digitalWrite ( LED_P , LOW ) ;
delay ( unitLength ) ;
waitUnits ( 1 ) ;
}
void dah ( int LED_P )
{
digitalWrite ( LED_P , HIGH ) ;
delay ( unitLength * 3 ) ;
waitUnits ( 3 ) ;
digitalWrite ( LED_P , LOW ) ;
delay ( unitLength ) ;
waitUnits ( 1 ) ;
}
// ===========================================================================
@@ -354,7 +469,7 @@ void morse(int LED_N, int LED_P)
case ' 9 ' : dah ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // ----.
case ' 0 ' : dah ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; symbolSent = true ; break ; // -----
// ----- Punctuation -----
// ----- Punctuation & Specials -----
case ' . ' : dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; symbolSent = true ; break ; // .-.-.-
case ' , ' : dah ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; symbolSent = true ; break ; // --..--
case ' ? ' : dit ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // ..--..
@@ -366,12 +481,18 @@ void morse(int LED_N, int LED_P)
case ' " ' : dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // .-..-.
case ' @ ' : dit ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // .--.-.
case ' & ' : dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // .-...
case ' - ' : dah ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; symbolSent = true ; break ; // -....-
case ' / ' : dah ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // -..-.
case ' \' ' : dit ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // .----.
case ' + ' : dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; symbolSent = true ; break ; // .-.-.
case ' = ' : dah ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; symbolSent = true ; break ; // -...-
case ' _ ' : dit ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; dah ( LED_P ) ; dit ( LED_P ) ; dah ( LED_P ) ; symbolSent = true ; break ; // ..--.-
// Space handling:
// Previous symbol already ended with 1 unit OFF and then received the
// normal 2 unit character gap. Add 4 units here -> total word gap 7 units.
case ' ' :
delay ( unitLength * 4 ) ;
waitUnits ( 4 ) ;
break ;
// Unsupported chars are ignored without adding an artificial gap.
@@ -382,7 +503,7 @@ void morse(int LED_N, int LED_P)
// Inter-character gap: 3 units total.
// dit()/dah() already trail 1 unit OFF, so add the remaining 2 units.
if ( symbolSent ) {
delay ( unitLength * 2 ) ;
waitUnits ( 2 ) ;
}
}