/* ============================================================================= 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.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 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. ----------------------------------------------------------------------------- Inspired by Karl Lunt's FireFly project: http://www.seanet.com/~karllunt/fireflyLED.html Morse code reference: 'A', ".-" 'B', "-..." 'C', "-.-." 'D', "-.." 'E', "." 'F', "..-." 'G', "--." 'H', "...." 'I', ".." 'J', ".---" 'K', "-.-" 'L', ".-.." 'M', "--" 'N', "-." 'O', "---" 'P', ".--." 'Q', "--.-" 'R', ".-." 'S', "..." 'T', "-" 'U', "..-" 'V', "...-" 'W', ".--" 'X', "-..-" 'Y', "-.--" 'Z', "--.." '1', ".----" '2', "..---" '3', "...--" '4', "....-" '5', "....." '6', "-...." '7', "--..." '8', "---.." '9', "----." '0', "-----" '.', ".-.-.-" ',', "--..--" '?', "..--.." '!', "-.-.--" ':', "---..." ';', "-.-.-." '(', "-.--." ')', "-.--.-" '"', ".-..-." '@', ".--.-." '&', ".-..." '-', "-....-" '/', "-..-." ''', ".----." '+', ".-.-." '=', "-...-" '_', "..--.-" ----------------------------------------------------------------------------- Legal stuff / Copyright: License_-_CC_BY-NC_4.0 https://creativecommons.org/licenses/by-nc/4.0/ ----------------------------------------------------------------------------- */ #include #include #include #include #include // --------------------------------------------------------------------------- // 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 #ifndef sbi #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) #endif // --------------------------------------------------------------------------- // 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 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). #define LED1_N_SIDE 3 #define LED1_P_SIDE 4 // --------------------------------------------------------------------------- // ATTiny85 Arduino pins 0, 1, 2 are unused in actual design and physically open. // Keep them in INPUT_PULLUP so they do not float and waste current. #define UNUSED_PIN_0 0 #define UNUSED_PIN_1 1 #define UNUSED_PIN_2 2 // --------------------------------------------------------------------------- // 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. // 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 = 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); static void waitUnits(uint8_t n); void configureUnusedPins(); void releaseLedPins(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); // =========================================================================== // Setup // =========================================================================== void setup() { // 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; } // 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); // 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); } // =========================================================================== // Main loop // =========================================================================== void loop() { 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); for (uint8_t i = 0; i < sleepCycles; i++) { sleepWdt(9); // 8 s each } } // =========================================================================== // Pin helpers // =========================================================================== void configureUnusedPins() { pinMode(UNUSED_PIN_0, INPUT_PULLUP); pinMode(UNUSED_PIN_1, INPUT_PULLUP); pinMode(UNUSED_PIN_2, INPUT_PULLUP); } void releaseLedPins(int LED_N, int LED_P) { // digitalWrite LOW before INPUT disables the internal pullup on Arduino cores. digitalWrite(LED_N, LOW); digitalWrite(LED_P, LOW); pinMode(LED_N, INPUT); pinMode(LED_P, INPUT); } // =========================================================================== // Sleep helpers (watchdog = only timebase) // =========================================================================== ISR(WDT_vect) { f_wdt = true; } // 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); // 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(); // 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); // 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(); } // Wait n Morse units in power-down. static void waitUnits(uint8_t n) { while (n--) { sleepWdt(UNIT_WDT_A); #if UNIT_WDT_B != 0xFF sleepWdt(UNIT_WDT_B); #endif } } // =========================================================================== // Light sensor // =========================================================================== // Returns true if it is dark. // Charges the LED junction capacitance, then lets it bleed through the // reverse-biased LED while 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) { // 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 (no pullup) and measure bleed-down time. pinMode(LED_N, INPUT); digitalWrite(LED_N, LOW); 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, caller releases them. pinMode(LED_N, OUTPUT); digitalWrite(LED_N, LOW); pinMode(LED_P, OUTPUT); digitalWrite(LED_P, LOW); return dark; } // =========================================================================== // Morse helpers // =========================================================================== void dit(int LED_P) { digitalWrite(LED_P, HIGH); waitUnits(1); digitalWrite(LED_P, LOW); waitUnits(1); } void dah(int LED_P) { digitalWrite(LED_P, HIGH); waitUnits(3); digitalWrite(LED_P, LOW); waitUnits(1); } // =========================================================================== // Morse sender // =========================================================================== void morse(int LED_N, int LED_P) { pinMode(LED_N, OUTPUT); pinMode(LED_P, OUTPUT); digitalWrite(LED_N, LOW); digitalWrite(LED_P, LOW); const size_t len = strlen_P(morseText); for (size_t i = 0; i < len; i++) { char c = (char)pgm_read_byte(&morseText[i]); // Allow lowercase source text without silently breaking timing. if (c >= 'a' && c <= 'z') { c = c - 'a' + 'A'; } bool symbolSent = false; switch (c) { // ----- Letters ----- case 'A': dit(LED_P); dah(LED_P); symbolSent = true; break; // .- case 'B': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -... case 'C': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.-. case 'D': dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -.. case 'E': dit(LED_P); symbolSent = true; break; // . case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // ..-. case 'G': dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // --. case 'H': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .... case 'I': dit(LED_P); dit(LED_P); symbolSent = true; break; // .. case 'J': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .--- case 'K': dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.- case 'L': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .-.. case 'M': dah(LED_P); dah(LED_P); symbolSent = true; break; // -- case 'N': dah(LED_P); dit(LED_P); symbolSent = true; break; // -. case 'O': dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // --- case 'P': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .--. case 'Q': dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // --.- case 'R': dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .-. case 'S': dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ... case 'T': dah(LED_P); symbolSent = true; break; // - case 'U': dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ..- case 'V': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ...- case 'W': dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .-- case 'X': dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -..- case 'Y': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -.-- case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // --.. // ----- Digits ----- case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .---- case '2': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ..--- case '3': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ...-- case '4': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ....- case '5': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ..... case '6': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -.... case '7': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // --... case '8': dah(LED_P); dah(LED_P); dah(LED_P); dit(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; // ----- // ----- 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 // normal 2 unit character gap. Add 4 units here -> total word gap 7 units. case ' ': waitUnits(4); break; // Unsupported chars are ignored without adding an artificial gap. default: break; } // Inter-character gap: 3 units total. // dit()/dah() already trail 1 unit OFF, so add the remaining 2 units. if (symbolSent) { waitUnits(2); } } digitalWrite(LED_P, LOW); digitalWrite(LED_N, LOW); }