/* ============================================================================= FireFly Morse Throwie - a light controlled (LED as Sensor) morse blinker throwie with ATTiny85 ============================================================================= 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): - 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 ~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. - Optional: BOD disable during sleep (only useful if BOD fuse is enabled). Not tested yet. - 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 (48 s cycle, 16 min recovery wait after a reset). #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. // 6 x 8 s = 48 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 brighter conditions (like the old 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 // 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). #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; // --------------------------------------------------------------------------- // 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() { // Remember why we restarted, then clear the flags. uint8_t resetCause = MCUSR; MCUSR = 0; if (resetCause & ((1 << PORF) | (1 << BORF) | (1 << EXTRF))) { 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(); MCUSR &= ~(1 << WDRF); // Timed sequence: the two writes must follow each other within 4 cycles. WDTCR = (1 << WDCE) | (1 << WDE); WDTCR = cfg; 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 ----- 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; // .-... // 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); }