Fix ATtiny85 watchdog register timing with atomic ASM writes; handle WDRF resets with early watchdog disable and recovery delay.

This commit is contained in:
tgohle committed 2026-11-10 21:01:27 +01:00
1 parent 28f49e8463
commit 80e42f1952
1 file changed
+65 -28
@@ -1,31 +1,39 @@
/* /*
============================================================================= =============================================================================
FireFly Morse Throwie FireFly Solar / 日輪蛍 – Nichirin Hotaru / a Solar Morse Throwie
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
=============================================================================
太陽を宿し、夜にモールスを灯す蛍。
Taiyō o yadoshi, yoru ni Mōrusu o tomosu hotaru.
A firefly carrying the sun, lighting Morse in the night.
=============================================================================
Project definitions, sources Project definitions, sources
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
Version: 0.4 - ATTiny85, 1 MHz, BOD fuse disabled Version : 0.5 - ATTiny85, 1 MHz, BOD fuse disabled
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly used Flash: 1284 bytes
Date : 2026-11-08
Key changes Version 0.4 (goal: lowest possible average current): 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 - No delay(), no millis(): Timer0 and Timer1 are not needed any more and
are switched off in PRR. are switched off in PRR.
- ALL timing (Morse units, sensing window, 8 s sleep) is done by the watchdog - 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 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. 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 ~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. 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). - ADC is disabled BEFORE it is gated off in PRR (order matters).
- After a power-on / external reset the sketch waits RECOVER_CYCLES cycles - After a power-on, external, brown-out or watchdog reset the sketch waits
before it blinks again, so a nearly empty supercap can recharge first. RECOVER_CYCLES cycles before blinking, allowing the supercap to recharge.
Default ~16 minutes, therefore added a debug option. 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). - 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 - Field Test: After a sunny day in Winter, outdoor -1 ... 8°C, with a payload
of 10 signs) it blinks for >> 12h. 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 // 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. // 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 #define DEBUG_TEST 0
#ifndef cbi #ifndef cbi
@@ -104,7 +114,7 @@ Inspired by Karl Lunt's FireFly project:
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Sleep interval between two sensing/blink attempts: sleepCycles x 8 s. // 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 #if DEBUG_TEST
const uint8_t sleepCycles = 1; const uint8_t sleepCycles = 1;
#else #else
@@ -116,16 +126,16 @@ const uint8_t sleepCycles = 5;
// After charging the LED junction the CPU sleeps step by step. If the pin // 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. // reads LOW within the window the LED discharged quickly = bright = no blink.
// Still HIGH after the full window = dark = 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. // 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 // 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. // sensDarkness() time with micros() at your dusk level and divide it by 16 ms.
const uint8_t darkSteps = 40; // ~640 ms const uint8_t darkSteps = 40; // ~640 ms
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// After a power-on / external reset (e.g. supercap was empty, DC/DC restart) // After a power-on / brown-out / external / watchdog reset, do not blink for
// do not blink for this many cycles (x ~48 s) so the supercap can recharge. // this many cycles (each has 40 s of sleep) so the supercap can recharge.
// 20 -> ~16 min. Set to 0 while testing/uploading (upload = external reset). // 20 -> ~13 min 20 s nominal. Set to 0 for bench testing/uploading.
#if DEBUG_TEST #if DEBUG_TEST
#define RECOVER_CYCLES 0 #define RECOVER_CYCLES 0
#else #else
@@ -166,6 +176,19 @@ volatile bool f_wdt = false;
// Remaining cycles without blinking after a reset. // Remaining cycles without blinking after a reset.
static uint8_t recoverCycles = 0; 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. // Forward declarations.
static void sleepWdt(uint8_t ii); static void sleepWdt(uint8_t ii);
@@ -182,10 +205,10 @@ void dah(int LED_P);
// =========================================================================== // ===========================================================================
void setup() void setup()
{ {
// Remember why we restarted, then clear the flags. // Reset cause was captured before C/Arduino initialization in .init3.
uint8_t resetCause = MCUSR; // Include WDRF: a watchdog reset also needs supercap recovery time.
MCUSR = 0; if (resetCauseEarly & ((1 << PORF) | (1 << BORF) |
if (resetCause & ((1 << PORF) | (1 << BORF) | (1 << EXTRF))) { (1 << EXTRF) | (1 << WDRF))) {
recoverCycles = RECOVER_CYCLES; recoverCycles = RECOVER_CYCLES;
} }
@@ -280,11 +303,19 @@ static void sleepWdt(uint8_t ii)
f_wdt = false; f_wdt = false;
cli(); cli();
wdt_reset(); wdt_reset();
MCUSR &= ~(1 << WDRF);
// Timed sequence: the two writes must follow each other within 4 cycles. // WDCE/WDE unlock and configuration: two consecutive OUT instructions.
WDTCR = (1 << WDCE) | (1 << WDE); // The second write is guaranteed to occur within the required four cycles.
WDTCR = cfg; 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); set_sleep_mode(SLEEP_MODE_PWR_DOWN);
@@ -438,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 '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; // ----- 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 '.': 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 ',': 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 '?': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ..--..
@@ -450,6 +481,12 @@ 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); 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); 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 '&': 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: // Space handling:
// Previous symbol already ended with 1 unit OFF and then received the // Previous symbol already ended with 1 unit OFF and then received the