diff --git a/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino b/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino index 9bf8241..a0ca8d2 100644 --- a/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino +++ b/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino @@ -1,31 +1,39 @@ /* ============================================================================= - 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.4 - ATTiny85, 1 MHz, BOD fuse disabled - gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly - Date : 2026-11-08 - -Key changes Version 0.4 (goal: lowest possible average current): + Version : 0.5 - ATTiny85, 1 MHz, BOD fuse disabled + used Flash: 1284 bytes + + gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly + Date : 2026-11-10 + + +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 watchdog 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 no longer polls digitalRead() in a busy loop (that was +- 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 reset the sketch waits RECOVER_CYCLES cycles - before it blinks again, so a nearly empty supercap can recharge first. - Default ~16 minutes, therefore added a debug option. +- 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). - Not tested yet. + 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. @@ -58,7 +66,9 @@ Inspired by Karl Lunt's FireFly project: '.', ".-.-.-" ',', "--..--" '?', "..--.." '!', "-.-.--" ':', "---..." ';', "-.-.-." '(', "-.--." ')', "-.--.-" '"', ".-..-." - '@', ".--.-." '&', ".-..." + '@', ".--.-." '&', ".-..." '-', "-....-" + '/', "-..-." ''', ".----." '+', ".-.-." + '=', "-...-" '_', "..--.-" ----------------------------------------------------------------------------- @@ -77,7 +87,7 @@ Inspired by Karl Lunt's FireFly project: // --------------------------------------------------------------------------- // 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 (48 s cycle, 16 min recovery wait after a reset). +// SET TO 0 FOR THE FIELD (40 s idle sleep, ~13m20s reset recovery). #define DEBUG_TEST 0 #ifndef cbi @@ -104,7 +114,7 @@ Inspired by Karl Lunt's FireFly project: // --------------------------------------------------------------------------- // Sleep interval between two sensing/blink attempts: sleepCycles x 8 s. -// 6 x 8 s = 48 s. Goal ~ 1 blink/min. +// 5 x 8 s = 40 s. Goal ~ 1 blink/min. #if DEBUG_TEST const uint8_t sleepCycles = 1; #else @@ -116,16 +126,16 @@ const uint8_t sleepCycles = 5; // 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 brighter conditions (like the old threshold). +// 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 / external reset (e.g. supercap was empty, DC/DC restart) -// do not blink for this many cycles (x ~48 s) so the supercap can recharge. -// 20 -> ~16 min. Set to 0 while testing/uploading (upload = external reset). +// 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 @@ -166,6 +176,19 @@ 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. static void sleepWdt(uint8_t ii); @@ -182,10 +205,10 @@ void dah(int LED_P); // =========================================================================== void setup() { - // Remember why we restarted, then clear the flags. - uint8_t resetCause = MCUSR; - MCUSR = 0; - if (resetCause & ((1 << PORF) | (1 << BORF) | (1 << EXTRF))) { + // Reset cause was captured before C/Arduino initialization in .init3. + // Include WDRF: a watchdog reset also needs supercap recovery time. + if (resetCauseEarly & ((1 << PORF) | (1 << BORF) | + (1 << EXTRF) | (1 << WDRF))) { recoverCycles = RECOVER_CYCLES; } @@ -280,11 +303,19 @@ static void sleepWdt(uint8_t ii) f_wdt = false; cli(); wdt_reset(); - MCUSR &= ~(1 << WDRF); - // Timed sequence: the two writes must follow each other within 4 cycles. - WDTCR = (1 << WDCE) | (1 << WDE); - WDTCR = cfg; + // 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" + ); set_sleep_mode(SLEEP_MODE_PWR_DOWN); @@ -438,18 +469,24 @@ 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 ----- - 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; // ..--.. - case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -.-.-- - case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ---... - case ';': dah(LED_P); 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); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.--. - case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.--.- - 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; // .-... + // ----- 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; // ..--.. + case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -.-.-- + case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ---... + case ';': dah(LED_P); 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); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.--. + case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.--.- + 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 @@ -472,4 +509,4 @@ void morse(int LED_N, int LED_P) digitalWrite(LED_P, LOW); digitalWrite(LED_N, LOW); -} +} \ No newline at end of file