ATTiny85: Mysterious delay() bug: Why does my Arduino sketch suddenly fail?

Hello everyone,

I'm currently working on a project where an LED should blink based on data received via I2C. I’ve noticed that the delay() function works correctly in the setup() function, but not as expected in the loop() function. To achieve a 1-second delay, I need to set the delay to approximately 8000 ms instead of the usual 1000 ms.

Interestingly, the delay works as expected in setup(), but not in the blinkNumber() function, which is called in the loop() function. I have already checked the fuses, but the issue persists.

Here’s a snippet of my code:

void blinkNumber(unsigned long number) {
  for (int i = 0; i < 5; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay(8000);  // Sollte 1 Sekunde leuchten
    digitalWrite(LED_PIN, LOW);
    delay(8000);  // Sollte 1 Sekunde ausgeschaltet bleiben
  }
}

Does anyone have an idea what could be causing this? Why does delay() work correctly in setup() but not in loop()? Could it have something to do with the sleep mode or the watchdog settings?

I’m looking forward to your tips and ideas!

Thanks in advance!

Here’s the complete code:

#include <Arduino.h>
#include "TinyWire.h"
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/power.h>

#define LED_PIN 4                   // Definiert den Pin für die LED
#define MOSFET_ENABLE 3             // Definiert den Pin zur Steuerung des MOSFETs
#define SHORT_ESP_POWER_BUTTON 1    // Definiert den Pin für den ESP Power-Button

volatile unsigned long receivedData = 0;    // Speichert die über I2C empfangenen Daten
volatile unsigned long totalSleepTime = 0;  // Gesamtschlafzeit in Sekunden
const unsigned long MAX_SLEEP_TIME = 72000; // Maximale Schlafzeit (12 Stunden in Sekunden) => auf 20h geändert
const byte MAX_ON_TIME = 90;        // Maximale Zeit für Modemverbindung in Sekunden
const byte RETRY_TIME = 8;          // Wartezeit vor erneutem Verbindungsversuch in Minuten
volatile byte onTimeCounter = 0;    // Zähler für die Zeit, die das Modem eingeschaltet ist
volatile byte sleepPrepared = 0;    // Flag, ob der Schlafmodus vorbereitet ist
volatile bool blinkDone = false;  // Flag zum Überprüfen, ob das Blinken abgeschlossen ist

byte saveADCSRA;                    // Speichert den ADCSRA-Wert für den Schlafmodus
volatile unsigned long counterWD = 0; // Zähler für den Watchdog-Timer

void resetWatchDog() {
  cli();                            // Deaktiviert globale Interrupts
  MCUSR = 0;                        // Setzt das MCU Status Register zurück
  WDTCR = bit(WDCE) | bit(WDE) | bit(WDIF);  // Aktiviert Watchdog-Änderungen
  WDTCR = bit(WDIE) | bit(WDP3) | bit(WDP0); // Setzt Watchdog auf 8 Sekunden
  sei();                            // Aktiviert globale Interrupts
  wdt_reset();                      // Setzt den Watchdog-Timer zurück
}

void sleepNow() {
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // Setzt den tiefsten Schlafmodus
  saveADCSRA = ADCSRA;               // Speichert den aktuellen ADCSRA-Wert
  ADCSRA = 0;                        // Deaktiviert den ADC
  power_all_disable();               // Schaltet alle Peripheriegeräte aus
  
  noInterrupts();                    // Deaktiviert Interrupts
  resetWatchDog();                   // Setzt den Watchdog zurück
  sleep_enable();                    // Aktiviert den Schlafmodus
  interrupts();                      // Aktiviert Interrupts wieder
  
  sleep_cpu();                       // Geht in den Schlafmodus
  
  sleep_disable();                   // CPU wacht auf, deaktiviert Schlafmodus
  power_all_enable();                // Schaltet alle Peripheriegeräte wieder ein
  ADCSRA = saveADCSRA;               // Stellt den ursprünglichen ADCSRA-Wert wieder her
}

// ######################

 void blinkNumber(unsigned long number) {
  for (int i = 0; i < 5; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay(1000);
    digitalWrite(LED_PIN, LOW);
    delay(1000);
    }
  }




void blinkNumber(unsigned long number) {
  blinkDone = false;  // Setzt das Flag vor dem Start des Blinkens

  // Flackern am Anfang
  for (int i = 0; i < 5; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay(400);
    digitalWrite(LED_PIN, LOW);
    delay(400);
  }

 // Pause nach dem Flackern
  delay(2000);
  
  
  
  
  
  // Jede Ziffer einzeln blinken
  unsigned long divisor = 1;
  while (number / divisor > 9) {
    divisor *= 10;

      digitalWrite(LED_PIN, HIGH);
      delay(10000);
      digitalWrite(LED_PIN, LOW);
      delay(80);
      

  }
  
  while (divisor > 0) {
    int digit = (number / divisor) % 10;
    
    // Blinke die Ziffer
    for (int i = 0; i < digit; i++) {
      digitalWrite(LED_PIN, HIGH);
      delay(8000);
      digitalWrite(LED_PIN, LOW);
      delay(8000);
    }
    
    // Pause zwischen den Ziffern
    delay(30000);
    
    divisor /= 10;
  }
  
  // Flackern am Ende
  for (int i = 0; i < 5; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay(1000);
    digitalWrite(LED_PIN, LOW);
    delay(1000);
  }
  
  // Abschließende Pause
  delay(2000);

  blinkDone = true;  // Setzt das Flag, wenn das Blinken abgeschlossen ist
}

// ################


void receiveEvent(int num) {
  byte rxbyte[3] = {0};              // Array zur Speicherung der empfangenen Bytes
  int i = 0;
  while (TinyWire.available() && i < 3) {
    rxbyte[i++] = TinyWire.read();   // Liest bis zu 3 Bytes ein
  }
  
  // Kombiniert die 3 Bytes zu einer Zahl
  receivedData = ((unsigned long)rxbyte[0] << 16) | ((unsigned long)rxbyte[1] << 8) | rxbyte[2];
  
  if (receivedData >= 1 && receivedData <= 9999) {
    totalSleepTime = receivedData * 60; // Umrechnung von Minuten in Sekunden
    counterWD = 0;                   // Setzt den Watchdog-Zähler zurück
    sleepPrepared = 0;               // Setzt das Schlafvorbereitungs-Flag zurück
    
    // Bestätigung durch LED-Blinken
    // digitalWrite(LED_PIN, HIGH);
    // delay(100);
    // digitalWrite(LED_PIN, LOW);

   // Anzeigen des empfangenen Werts durch LED-Blinken
    blinkNumber(receivedData);

  }
}

void requestEvent() {
  // Teilt receivedData in 3 Bytes auf
  byte highB = (receivedData >> 16) & 0xFF;
  byte midB = (receivedData >> 8) & 0xFF;
  byte lowB = receivedData & 0xFF;
  
  // Sendet die 3 Bytes über I2C
  TinyWire.send(highB);
  TinyWire.send(midB);
  TinyWire.send(lowB);
  
  receivedData = 0;                  // Setzt receivedData zurück
}

void setup() {

// // Debug-Schleife für Zeitkalibrierung
//   for (int i = 0; i < 10; i++) {  // 10 Wiederholungen für eine einfache Messung
//     digitalWrite(LED_PIN, HIGH);
//     delay(2000);  // Sollte 1 Sekunde sein
//     digitalWrite(LED_PIN, LOW);
//     delay(2000);  // Sollte 1 Sekunde sein
//   }

  resetWatchDog();                   // Initialisiert den Watchdog-Timer
  
  pinMode(LED_PIN, OUTPUT);
  pinMode(SHORT_ESP_POWER_BUTTON, OUTPUT);
  pinMode(MOSFET_ENABLE, OUTPUT);
  
  digitalWrite(MOSFET_ENABLE, HIGH); // Aktiviert den MOSFET
  
  TinyWire.begin(0x08);              // Initialisiert I2C mit Adresse 0x08
  TinyWire.onReceive(receiveEvent);
  TinyWire.onRequest(requestEvent);
  
  // Initialisierung anzeigen durch LED-Blinken ( Flackern)
  for (int i = 0; i < 5; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay(50);
    digitalWrite(LED_PIN, LOW);
    delay(50);
  }
  
  // ESP starten
  digitalWrite(SHORT_ESP_POWER_BUTTON, HIGH);
  delay(1000);
  digitalWrite(SHORT_ESP_POWER_BUTTON, LOW);
}

void loop() {
  if (digitalRead(MOSFET_ENABLE) == HIGH) {
    onTimeCounter++;
    if (onTimeCounter >= MAX_ON_TIME) {
      digitalWrite(MOSFET_ENABLE, LOW);  // Schaltet MOSFET aus
      totalSleepTime = (unsigned long)RETRY_TIME * 60; // Setzt Schlafzeit auf RETRY_TIME
      onTimeCounter = 0;
      sleepPrepared = 0;
      counterWD = 0;
    }
  } else {
    onTimeCounter = 0;
  }

 if (totalSleepTime > 0 && blinkDone) {  // Überprüft, ob das Blinken abgeschlossen ist
    if (!sleepPrepared) {
      digitalWrite(MOSFET_ENABLE, LOW);  // Schaltet MOSFET aus
      sleepPrepared = 1;
    }
    
    if (counterWD >= totalSleepTime || counterWD >= MAX_SLEEP_TIME) {
      // Aufwachen
      digitalWrite(LED_PIN, HIGH);
      delay(10);
      digitalWrite(LED_PIN, LOW);
      
      counterWD = 0;
      digitalWrite(MOSFET_ENABLE, HIGH); // Schaltet MOSFET ein
      totalSleepTime = 0;
      sleepPrepared = 0;
      
      // ESP neu starten => Hack um TTGO zu staren ist ein zus Knopfdruck notwendig
      digitalWrite(SHORT_ESP_POWER_BUTTON, HIGH);
      delay(500);
      digitalWrite(SHORT_ESP_POWER_BUTTON, LOW);
    } else {
      sleepNow();                    // Geht in den Schlafmodus
    }
  }
  
  delay(1000);                       // Kurze Pause, um Energie zu sparen
}

ISR(WDT_vect) {
  wdt_disable();                     // Deaktiviert den Watchdog
  counterWD += 8;                    // Erhöht den Zähler um 8 Sekunden
}

Fuse EInstellungen:
Low-Fuse: 0xE2
High-Fuse: 0xDF
Extended-Fuse: 0xFF

I've not looked into the code precisely but I've seen this

The onReceive and onRequest functions are triggered by I2C events and they run in the context of an interrupt (where interrupts are deactivated, meaning millis() does not tick any more, delay will struggle etc).

➜ you should be cautious about what you do inside these functions and calling blinkNumber() might not be a wise idea.

that's where I would dig first.

PS/ I took the liberty to edit your title as you need to use English if you post in the general forum. There is a Deutsch specific category if you prefer writing in German. (but don't double post).

Thank you so much, J-M-L, for your insightful answer! Your explanation really helped me understand the underlying issue. Here's what I ended up doing based on your advice:

Relocation of blinkNumber(): I moved the function from the ISR to the main loop (loop()), which prevented the timing issues I was experiencing.

Introduction of Flags: I added a blinkExecuted flag to ensure that blinkNumber() only runs once per cycle. I also used a dataReceivedFlag to make sure blinkNumber() is only called after the data is fully received.

Visual Feedback: I incorporated a flickering effect at both the beginning and the end of the blink sequence, which provides a nice visual confirmation.

Controlled Transition to Deep Sleep: Now, Deep Sleep is safely triggered after the single execution of blinkNumber() when the conditions are met.

These changes have made my code much more stable and functional. I truly appreciate your help—thanks again for pointing me in the right direction!

Instead of using delay(), you can implement your own timing logic using micros() or millis() which would be less affected by the sleep mode, though still sensitive to power management settings.

great to hear ! have fun

(make sure that if you have variables shared between an ISR context and the loop then they are declared volatile and you use a critical section in the loop() to access/modify the shared variables.)