Version 3 sleeping behavior bugs, morsehandling bugs and timing bugs especially near BOD

This commit is contained in:
2026-06-14 15:36:48 +02:00
parent 46a2bec551
commit ebf338c97f
2 changed files with 525 additions and 134 deletions

View File

@ -1,18 +1,28 @@
/* /*
============================================================================= =============================================================================
FireFly Morse Throwie FireFly Morse Throwie
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85 - a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
============================================================================= =============================================================================
Project definitions, sources
-----------------------------------------------------------------------------
Version: 0.2 - ATTiny85, 1 MHz, BOD fuse disabled
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly
Date : 2026-06-13
-----------------------------------------------------------------------------
Inspired by Karl Lunt's FireFly project: Project definitions, sources
-----------------------------------------------------------------------------
Version: 0.3 - ATTiny85, 1 MHz, BOD fuse disabled
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly
Date : 2026-06-14
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 timing
- Added lowercase-to-uppercase handling
- Unsupported characters are now ignored without adding fake timing gaps
-----------------------------------------------------------------------------
Inspired by Karl Lunt's FireFly project:
http://www.seanet.com/~karllunt/fireflyLED.html http://www.seanet.com/~karllunt/fireflyLED.html
Morse code reference: Morse code reference:
@ -24,7 +34,7 @@
'P', ".--." 'Q', "--.-" 'R', ".-." 'P', ".--." 'Q', "--.-" 'R', ".-."
'S', "..." 'T', "-" 'U', "..-" 'S', "..." 'T', "-" 'U', "..-"
'V', "...-" 'W', ".--" 'X', "-..-" 'V', "...-" 'W', ".--" 'X', "-..-"
'Y', "-.--" 'Z', "--.." 'Y', "-.--" 'Z', "--.."
'1', ".----" '2', "..---" '3', "...--" '1', ".----" '2', "..---" '3', "...--"
'4', "....-" '5', "....." '6', "-...." '4', "....-" '5', "....." '6', "-...."
@ -36,15 +46,19 @@
'(', "-.--." ')', "-.--.-" '"', ".-..-." '(', "-.--." ')', "-.--.-" '"', ".-..-."
'@', ".--.-." '&', ".-..." '@', ".--.-." '&', ".-..."
-----------------------------------------------------------------------------
Legal stuff / Copyright: Legal stuff / Copyright:
License_-_CC_BY-NC_4.0 License_-_CC_BY-NC_4.0
https://creativecommons.org/licenses/by-nc/4.0/ https://creativecommons.org/licenses/by-nc/4.0/
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
*/ */
#include <avr/sleep.h> #include <avr/sleep.h>
#include <avr/wdt.h> #include <avr/wdt.h>
#include <avr/pgmspace.h> // FIX 3: needed for PROGMEM / pgm_read_byte #include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h>
#ifndef cbi #ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
@ -56,37 +70,52 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Timing: unit length in ms. // Timing: unit length in ms.
// At 1 MHz, delay() is accurate when F_CPU=1000000L is set in boards.txt. // At 1 MHz, delay() is accurate when F_CPU=1000000L is set in boards.txt.
// 100 ms gives readable optical Morse; raise to 150 if readability is poor. // 100 ms gives readable optical Morse; raise to 150 ms if readability is poor.
#define unitLength 100 #define unitLength 100
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving) // Sleep interval.
// Watchdog setup uses 8 s. For final use, change sleepCycles from 1 to 4
// for about 32 s between sensing/blinking activations.
const uint8_t sleepCycles = 4;
// ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving).
#define LED1_N_SIDE 3 #define LED1_N_SIDE 3
#define LED1_P_SIDE 4 #define LED1_P_SIDE 4
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// ATTiny85 has 512 bytes SRAM; this frees all of it for the stack. // ATTiny85 Arduino pins 0, 1, 2 are unused in actual design and physically open.
// Only uppercase letters, digits, and the special chars in the switch below. // Keep them in INPUT_PULLUP so they do not float and waste current.
// Test only: 20 x "0" due 0 = "-----" most energy draining #define UNUSED_PIN_0 0
const char morseText[] PROGMEM = "0000000000000000000"; #define UNUSED_PIN_1 1
// Ruler: 0...0....1...1....2 Attention: more Text #define UNUSED_PIN_2 2
// Ruler: 1...5....0...5....0 will cost more power!
// ---------------------------------------------------------------------------
// 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 = "TOGO LAB";
// Ruler: 0....0....1...1....2
// Ruler: 0....5....0...5....0
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Darkness threshold. // Darkness threshold.
// Higher value = triggers in brighter conditions. // Higher value = triggers in brighter conditions.
// Best calibrated at actual dusk/dawn with the chosen LED type. // Best calibrated at actual dusk/dawn with the chosen LED type.
unsigned int darknessThreshold = 17000; const unsigned int darknessThreshold = 17000;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR // Watchdog interrupt flag — volatile because it is written in an ISR.
volatile boolean f_wdt = 1; volatile bool f_wdt = true;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Forward declarations // Forward declarations.
void setup_watchdog(int ii); void setup_watchdog(uint8_t ii);
void system_sleep(); void system_sleep();
void configureUnusedPins();
void releaseLedPins(int LED_N, int LED_P);
unsigned int sensDarkness(int LED_N, int LED_P); unsigned int sensDarkness(int LED_N, int LED_P);
void morse(int LED_N, int LED_P); void morse(int LED_N, int LED_P);
void dit(int LED_P); void dit(int LED_P);
@ -97,17 +126,22 @@ void dah(int LED_P);
// =========================================================================== // ===========================================================================
void setup() void setup()
{ {
// Disable unused peripherals immediately to save some µ/mA. // Disable unused peripherals immediately to save power.
// PRTIM1, PRUSI: genuinely unused, safe to gate off // PRTIM1, PRUSI: unused, safe to gate off.
// PRADC: controlled manually around sensDarkness() // PRADC: ADC is not used; LED sensing is done with digitalRead().
// PRTIM0: must stay ON — delay() and millis() depend on Timer0 // PRTIM0: must stay ON while awake because delay() depends on Timer0.
PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC); PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC);
// ADC is gated via PRR above; also clear ADEN just in case // ADC off. It is not needed for digitalRead()-based LED sensing.
cbi(ADCSRA, ADEN); cbi(ADCSRA, ADEN);
setup_watchdog(9); // 8-second watchdog interval // Analog comparator off. Saves a little sleep current.
// simple repeat if more delay is need, eg 4 ACSR |= (1 << ACD);
configureUnusedPins();
releaseLedPins(LED1_N_SIDE, LED1_P_SIDE);
setup_watchdog(9); // 8-second watchdog interval.
} }
// =========================================================================== // ===========================================================================
@ -115,117 +149,145 @@ void setup()
// =========================================================================== // ===========================================================================
void loop() void loop()
{ {
if (f_wdt == 1) { if (f_wdt) {
f_wdt = 0; f_wdt = false;
// Enable ADC only for the sensing window, then shut it off again.
cbi(PRR, PRADC); // un-gate ADC clock
sbi(ADCSRA, ADEN); // power ADC on
if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) { if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) {
morse(LED1_N_SIDE, LED1_P_SIDE); morse(LED1_N_SIDE, LED1_P_SIDE);
} }
cbi(ADCSRA, ADEN); // ADC off // Return LED pins to high-Z before sleeping.
sbi(PRR, PRADC); // re-gate ADC clock // Internal pullups stay disabled for the LED pins.
releaseLedPins(LED1_N_SIDE, LED1_P_SIDE);
// Return LED pins to input (high-Z) before sleeping
pinMode(LED1_N_SIDE, INPUT);
pinMode(LED1_P_SIDE, INPUT);
} }
// sleep 4 x 8 s = ~32 s between activations // Test setup: 1 x 8 s sleep.
for (uint8_t i = 0; i < 1; i++) { // Final setup: set sleepCycles = 4 for about 32 s.
for (uint8_t i = 0; i < sleepCycles; i++) {
system_sleep(); system_sleep();
} }
// f_wdt is set to 1 by the WDT ISR when the 8 s expire; no manual clear needed here. }
// ===========================================================================
// 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 // Sleep helpers
// =========================================================================== // ===========================================================================
void system_sleep() void system_sleep()
{ {
// Reset watchdog counter so each sleep cycle starts with a fresh interval.
wdt_reset();
set_sleep_mode(SLEEP_MODE_PWR_DOWN); 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(); sleep_enable();
sleep_mode(); // CPU halts here until WDT fires sei();
sleep_cpu();
// WDT interrupt wakes the CPU here.
sleep_disable(); sleep_disable();
// ADC stays off - re-enable it in loop() only when sensing
} }
// Watchdog setup - ii selects timeout: // Watchdog setup - ii selects timeout:
// 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms // 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms
// 6=1s 7=2s 8=4s 9=8s // 6=1s 7=2s 8=4s 9=8s
void setup_watchdog(int ii) void setup_watchdog(uint8_t ii)
{ {
byte bb;
if (ii > 9) ii = 9; if (ii > 9) ii = 9;
bb = ii & 7;
if (ii > 7) bb |= (1 << 5);
bb |= (1 << WDCE);
uint8_t bb = ii & 7;
if (ii > 7) bb |= (1 << WDP3);
uint8_t oldSREG = SREG;
cli();
wdt_reset();
MCUSR &= ~(1 << WDRF); MCUSR &= ~(1 << WDRF);
WDTCR |= (1 << WDCE) | (1 << WDE); // timed sequence - must not be split
WDTCR = bb; // Timed sequence: enable configuration change, then set interrupt-only WDT.
WDTCR |= _BV(WDIE); WDTCR |= (1 << WDCE) | (1 << WDE);
WDTCR = bb | (1 << WDIE);
SREG = oldSREG;
} }
ISR(WDT_vect) ISR(WDT_vect)
{ {
f_wdt = 1; f_wdt = true;
} }
// =========================================================================== // ===========================================================================
// Light sensor // Light sensor
// =========================================================================== // ===========================================================================
// Returns a "darkness level": higher = darker. // Returns a "darkness level": higher = darker.
// Charges the LED junction capacitor, then times how long it takes to bleed // Charges the LED junction capacitance, then times how long it takes to bleed
// back through the reverse-biased diode to a logic LOW. // back through the reverse-biased LED until the input reads LOW.
// ~30000 = pitch black, ~0 = bright light // ~30000 = pitch black, ~0 = bright light
// return type is unsigned int (counter can reach 30000, fits in 16 bits)
unsigned int sensDarkness(int LED_N, int LED_P) unsigned int sensDarkness(int LED_N, int LED_P)
{ {
unsigned int i; unsigned int i;
// Charge the LED (forward-bias momentarily) // Charge the LED junction capacitance.
pinMode(LED_N, OUTPUT); pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT); pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, HIGH); digitalWrite(LED_N, HIGH);
digitalWrite(LED_P, LOW); digitalWrite(LED_P, LOW);
// Let the N-end float and measure bleed-down time // Let the N-side float and measure bleed-down time.
pinMode(LED_N, INPUT); pinMode(LED_N, INPUT);
digitalWrite(LED_N, LOW); // disable internal pull-up digitalWrite(LED_N, LOW); // disable internal pullup
for (i = 0; i < 30000; i++) { for (i = 0; i < 30000; i++) {
if (digitalRead(LED_N) == 0) break; if (digitalRead(LED_N) == LOW) break;
} }
// clean up after sensing // Clean up after sensing: both LED pins low, then caller releases them.
pinMode(LED_N, OUTPUT); pinMode(LED_N, OUTPUT);
digitalWrite(LED_N, LOW); digitalWrite(LED_N, LOW);
pinMode(LED_P, OUTPUT); pinMode(LED_P, OUTPUT);
digitalWrite(LED_P, LOW); digitalWrite(LED_P, LOW);
return i;
return i;
} }
// =========================================================================== // ===========================================================================
// Morse helpers // Morse helpers
// =========================================================================== // ===========================================================================
void dit(int LED_P) void dit(int LED_P)
{ {
digitalWrite(LED_P, HIGH); delay(unitLength); digitalWrite(LED_P, HIGH);
digitalWrite(LED_P, LOW); delay(unitLength); delay(unitLength);
digitalWrite(LED_P, LOW);
delay(unitLength);
} }
void dah(int LED_P) void dah(int LED_P)
{ {
digitalWrite(LED_P, HIGH); delay(unitLength * 3); digitalWrite(LED_P, HIGH);
digitalWrite(LED_P, LOW); delay(unitLength); delay(unitLength * 3);
digitalWrite(LED_P, LOW);
delay(unitLength);
} }
// =========================================================================== // ===========================================================================
@ -236,76 +298,94 @@ void morse(int LED_N, int LED_P)
pinMode(LED_N, OUTPUT); pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT); pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, LOW); digitalWrite(LED_N, LOW);
digitalWrite(LED_P, LOW);
// read each character from flash with pgm_read_byte() const size_t len = strlen_P(morseText);
// use < morseText length, not <= (avoids reading past the null terminator)
uint8_t len = strlen_P(morseText); for (size_t i = 0; i < len; i++) {
for (uint8_t i = 0; i < len; i++)
{
char c = (char)pgm_read_byte(&morseText[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) switch (c)
{ {
// ----- Letters ----- // ----- Letters -----
case 'A': dit(LED_P); dah(LED_P); break; // .- 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); 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); 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); break; // -.. case 'D': dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -..
case 'E': dit(LED_P); break; // . case 'E': dit(LED_P); symbolSent = true; break; // .
case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); 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); 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); 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); 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); 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); 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); 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); break; // -- case 'M': dah(LED_P); dah(LED_P); symbolSent = true; break; // --
case 'N': dah(LED_P); dit(LED_P); break; // -. case 'N': dah(LED_P); dit(LED_P); symbolSent = true; break; // -.
case 'O': dah(LED_P); dah(LED_P); dah(LED_P); 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); 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); 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); 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); break; // ... case 'S': dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ...
case 'T': dah(LED_P); break; // - case 'T': dah(LED_P); symbolSent = true; break; // -
case 'U': dit(LED_P); dit(LED_P); dah(LED_P); 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); 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); 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); 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); 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); break; // --.. case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // --..
// ----- Digits ----- // ----- Digits -----
case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // .---- 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); 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); 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); 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); 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); 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); 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); 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); 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); break; // ----- case '0': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -----
// ----- Punctuation ----- // ----- Punctuation -----
case ' ': delay(unitLength * 5); break; // word gap (5 + 3 = 8 units total) 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); 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); 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); 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); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); 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); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); 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); dit(LED_P); dah(LED_P); dit(LED_P); 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); break; // -.--. case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.--.-
case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); 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); dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); 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); 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); 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);
break;
// Unsupported chars are ignored without adding an artificial gap.
default:
break;
} }
// Inter-character gap: 3 units (the dit/dah functions already trail 1 unit, // Inter-character gap: 3 units total.
// so this adds the remaining 2 to reach the standard 3-unit gap). // dit()/dah() already trail 1 unit OFF, so add the remaining 2 units.
// Space case already handles word gap - no extra delay needed there. if (symbolSent) {
if (c != ' ') {
delay(unitLength * 2); delay(unitLength * 2);
} }
} }
digitalWrite(LED_P, LOW);
digitalWrite(LED_N, LOW);
} }

View File

@ -0,0 +1,311 @@
/*
=============================================================================
FireFly Morse Throwie
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
=============================================================================
Project definitions, sources
-----------------------------------------------------------------------------
Version: 0.2 - ATTiny85, 1 MHz, BOD fuse disabled
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly
Date : 2026-06-13
-----------------------------------------------------------------------------
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 <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/pgmspace.h> // FIX 3: needed for PROGMEM / pgm_read_byte
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#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 if readability is poor.
#define unitLength 100
// ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving)
#define LED1_N_SIDE 3
#define LED1_P_SIDE 4
// ---------------------------------------------------------------------------
// ATTiny85 has 512 bytes SRAM; this frees all of it for the stack.
// Only uppercase letters, digits, and the special chars in the switch below.
// Test only: 20 x "0" due 0 = "-----" most energy draining
const char morseText[] PROGMEM = "0000000000000000000";
// Ruler: 0...0....1...1....2 Attention: more Text
// Ruler: 1...5....0...5....0 will cost more power!
// ---------------------------------------------------------------------------
// Darkness threshold.
// Higher value = triggers in brighter conditions.
// Best calibrated at actual dusk/dawn with the chosen LED type.
unsigned int darknessThreshold = 17000;
// ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR
volatile boolean f_wdt = 1;
// ---------------------------------------------------------------------------
// Forward declarations
void setup_watchdog(int ii);
void system_sleep();
unsigned int sensDarkness(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()
{
// Disable unused peripherals immediately to save some µ/mA.
// PRTIM1, PRUSI: genuinely unused, safe to gate off
// PRADC: controlled manually around sensDarkness()
// PRTIM0: must stay ON — delay() and millis() depend on Timer0
PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC);
// ADC is gated via PRR above; also clear ADEN just in case
cbi(ADCSRA, ADEN);
setup_watchdog(9); // 8-second watchdog interval
// simple repeat if more delay is need, eg 4
}
// ===========================================================================
// Main loop
// ===========================================================================
void loop()
{
if (f_wdt == 1) {
f_wdt = 0;
// Enable ADC only for the sensing window, then shut it off again.
cbi(PRR, PRADC); // un-gate ADC clock
sbi(ADCSRA, ADEN); // power ADC on
if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) {
morse(LED1_N_SIDE, LED1_P_SIDE);
}
cbi(ADCSRA, ADEN); // ADC off
sbi(PRR, PRADC); // re-gate ADC clock
// Return LED pins to input (high-Z) before sleeping
pinMode(LED1_N_SIDE, INPUT);
pinMode(LED1_P_SIDE, INPUT);
}
// sleep 4 x 8 s = ~32 s between activations
for (uint8_t i = 0; i < 1; i++) {
system_sleep();
}
// f_wdt is set to 1 by the WDT ISR when the 8 s expire; no manual clear needed here.
}
// ===========================================================================
// Sleep helpers
// ===========================================================================
void system_sleep()
{
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode(); // CPU halts here until WDT fires
sleep_disable();
// ADC stays off - re-enable it in loop() only when sensing
}
// 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(int ii)
{
byte bb;
if (ii > 9) ii = 9;
bb = ii & 7;
if (ii > 7) bb |= (1 << 5);
bb |= (1 << WDCE);
MCUSR &= ~(1 << WDRF);
WDTCR |= (1 << WDCE) | (1 << WDE); // timed sequence - must not be split
WDTCR = bb;
WDTCR |= _BV(WDIE);
}
ISR(WDT_vect)
{
f_wdt = 1;
}
// ===========================================================================
// Light sensor
// ===========================================================================
// Returns a "darkness level": higher = darker.
// Charges the LED junction capacitor, then times how long it takes to bleed
// back through the reverse-biased diode to a logic LOW.
// ~30000 = pitch black, ~0 = bright light
// return type is unsigned int (counter can reach 30000, fits in 16 bits)
unsigned int sensDarkness(int LED_N, int LED_P)
{
unsigned int i;
// Charge the LED (forward-bias momentarily)
pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, HIGH);
digitalWrite(LED_P, LOW);
// Let the N-end float and measure bleed-down time
pinMode(LED_N, INPUT);
digitalWrite(LED_N, LOW); // disable internal pull-up
for (i = 0; i < 30000; i++) {
if (digitalRead(LED_N) == 0) break;
}
// clean up after sensing
pinMode(LED_N, OUTPUT);
digitalWrite(LED_N, LOW);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_P, LOW);
return i;
}
// ===========================================================================
// Morse helpers
// ===========================================================================
void dit(int LED_P)
{
digitalWrite(LED_P, HIGH); delay(unitLength);
digitalWrite(LED_P, LOW); delay(unitLength);
}
void dah(int LED_P)
{
digitalWrite(LED_P, HIGH); delay(unitLength * 3);
digitalWrite(LED_P, LOW); delay(unitLength);
}
// ===========================================================================
// Morse sender
// ===========================================================================
void morse(int LED_N, int LED_P)
{
pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, LOW);
// read each character from flash with pgm_read_byte()
// use < morseText length, not <= (avoids reading past the null terminator)
uint8_t len = strlen_P(morseText);
for (uint8_t i = 0; i < len; i++)
{
char c = (char)pgm_read_byte(&morseText[i]);
switch (c)
{
// ----- Letters -----
case 'A': dit(LED_P); dah(LED_P); break; // .-
case 'B': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // -...
case 'C': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // -.-.
case 'D': dah(LED_P); dit(LED_P); dit(LED_P); break; // -..
case 'E': dit(LED_P); break; // .
case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // ..-.
case 'G': dah(LED_P); dah(LED_P); dit(LED_P); break; // --.
case 'H': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // ....
case 'I': dit(LED_P); dit(LED_P); break; // ..
case 'J': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // .---
case 'K': dah(LED_P); dit(LED_P); dah(LED_P); break; // -.-
case 'L': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // .-..
case 'M': dah(LED_P); dah(LED_P); break; // --
case 'N': dah(LED_P); dit(LED_P); break; // -.
case 'O': dah(LED_P); dah(LED_P); dah(LED_P); break; // ---
case 'P': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // .--.
case 'Q': dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // --.-
case 'R': dit(LED_P); dah(LED_P); dit(LED_P); break; // .-.
case 'S': dit(LED_P); dit(LED_P); dit(LED_P); break; // ...
case 'T': dah(LED_P); break; // -
case 'U': dit(LED_P); dit(LED_P); dah(LED_P); break; // ..-
case 'V': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // ...-
case 'W': dit(LED_P); dah(LED_P); dah(LED_P); break; // .--
case 'X': dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // -..-
case 'Y': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // -.--
case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // --..
// ----- Digits -----
case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // .----
case '2': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // ..---
case '3': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // ...--
case '4': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // ....-
case '5': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // .....
case '6': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // -....
case '7': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // --...
case '8': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // ---.
case '9': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // ----.
case '0': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // -----
// ----- Punctuation -----
case ' ': delay(unitLength * 5); break; // word gap (5 + 3 = 8 units total)
case '.': dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // .-.-.-
case ',': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // --..--
case '?': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // ..--..
case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // -.-.--
case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // ---...
case ';': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // -.-.-.
case '(': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // -.--.
case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // -.--.-
case '"': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // .-..-.
case '@': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // .--.-.
case '&': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // .-...
}
// Inter-character gap: 3 units (the dit/dah functions already trail 1 unit,
// so this adds the remaining 2 to reach the standard 3-unit gap).
// Space case already handles word gap - no extra delay needed there.
if (c != ' ') {
delay(unitLength * 2);
}
}
}