Making a Project Survive Unattended

Draft. This tutorial has not been checked against the physical product yet. Confirm wiring, pin assignments and voltage limits against the product documentation before building it.
DifficultyAdvanced
CategoryFoundations
Supported boardsArduino Uno R3 and compatible ATmega328P boards
Time needed50 minutes
PrerequisitesTiming Without delay()
Tested boardNot yet verified
Tested onNot yet verified
Arduino IDE versionNot yet verified
LibraryNot yet verified

What this covers

A project on your bench has you watching it. A project in a greenhouse, a tank or a workshop does not, and the difference is not a small one: a fault you would have noticed in seconds can run for a week.

This page is about the three things that make an unattended project trustworthy. Recovering when something goes wrong, noticing that it did, and not making things worse while it is broken.

Before you begin

Nothing to wire. An LED on pin 13 and a serial monitor are enough to see everything here.

What actually goes wrong

In rough order of how often you will meet them:

  1. The supply sags and the board browns out. Usually something that moves.
  2. A sensor stops answering. A loose wire, a flat cell, a corroded probe.
  3. A network connection drops and the sketch waits for it for ever.
  4. The sketch locks up. An infinite loop, a library waiting on something that never arrives.
  5. Memory fragments over days of String use and the board freezes.

Notice how few of those are logic errors. Almost everything that fails in the field is physical, or is software waiting patiently for something physical.

Two timelines: a locked-up sketch staying dead, and the same lock-up ended by a watchdog reset eight seconds later
Ten seconds lost on Saturday night, instead of a dead project on Monday.

The watchdog

The ATmega328P has a timer that runs independently of your sketch. If it is not reset within its interval, it resets the whole board.

So a sketch that has locked up stops petting the watchdog, and the board restarts by itself. That is genuinely the difference between a project that is dead on Monday and one that lost ten seconds on Saturday night.

#include <avr/wdt.h>

// Stand-in for whatever your project actually does.
void doTheWork() {
  delay(200);
}

void setup() {
  Serial.begin(9600);
  Serial.println("Siratek: started");

  wdt_enable(WDTO_8S);        // reset if 8 seconds pass without a pet
}

void loop() {
  doTheWork();

  wdt_reset();                // "still here"
}

WDTO_8S is the longest the ATmega328P offers, and it is the right default: long enough that a slow sensor read cannot trip it, short enough to recover quickly.

The rules that stop a watchdog making things worse

Put wdt_reset() in one place. Sprinkling it through your code means a function stuck in a loop can keep petting the watchdog while doing nothing useful. One call, at the end of loop(), means the watchdog measures what you actually want: whether the loop is still going round.

Never pet it inside a while that waits for something. That is precisely the lock-up you want it to catch.

Watch out for a reset loop. If the fault is in setup(), the board resets, runs setup(), hangs again, resets. On some older bootloaders the watchdog stays enabled through the reset and the board becomes very hard to reprogram. Disable it first thing:

void setup() {
  wdt_disable();              // in case we arrived here from a watchdog reset
  // ... set everything up ...
  wdt_enable(WDTO_8S);        // only once the slow work is done
}

Do not enable it until setup has finished. Initialising a display or waiting for a network can easily take longer than eight seconds.

Knowing a reset happened

A board that silently restarts hides the very fault you wanted to find. The chip records why it started, and reading that costs four lines.

#include <avr/wdt.h>

void setup() {
  // MCUSR holds why the chip last started. Read it before anything clears it.
  byte reason = MCUSR;
  MCUSR = 0;

  Serial.begin(9600);
  Serial.print("Siratek: started, reason = ");
  if (reason & _BV(WDRF))  Serial.print("watchdog ");
  if (reason & _BV(BORF))  Serial.print("brown-out ");
  if (reason & _BV(EXTRF)) Serial.print("reset pin ");
  if (reason & _BV(PORF))  Serial.print("power-on ");
  Serial.println();

  wdt_disable();
  wdt_enable(WDTO_8S);
}

“brown-out” means the supply, not the code. That single word saves days, because it tells you to stop reading your sketch and go and measure the rail under load.

Timeouts, which matter more than the watchdog

The watchdog is a last resort. Most lock-ups are avoidable, because they are code waiting for something that is never coming.

This pattern appears in nearly every example in this library and is worth seeing as a principle:

// Never: hangs for ever if the thing never happens.
while (!sensor.ready()) {
  ;
}

// Always: give up, and say so.
unsigned long start = millis();
while (!sensor.ready() && millis() - start < 2000) {
  delay(10);
}
if (!sensor.ready()) {
  Serial.println("sensor did not respond");
  return;
}

Every wait gets a deadline. A Wi-Fi connection, a card initialising, a sensor converting, a character arriving on a serial port. The version without a timeout works perfectly until the day it does not, and then it does nothing at all, for ever.

Carrying on when a part fails

A logger that refuses to run because the card is missing is worse than one that keeps measuring and says the card is missing. Decide, for each part, what should happen without it.

bool cardReady = false;
bool clockReady = false;

void setup() {
  Serial.begin(9600);

  cardReady = SD.begin(10);
  if (!cardReady) {
    Serial.println("no card: logging disabled, serial only");
  }

  clockReady = rtc.begin();
  if (!clockReady) {
    Serial.println("no clock: timestamps will be elapsed time");
  }
}

The reading still happens. The serial output still works. One capability is gone and the project says which, which is far more useful than a blank screen.

Retrying, without hammering

A sensor that failed once may work in a minute. One that has failed a hundred times will not, and retrying it every loop wastes time and fills the log.

unsigned long nextRetry = 0;
int failures = 0;

bool tryTheSensor() {
  if (millis() < nextRetry) {
    return false;
  }

  if (sensor.read()) {
    failures = 0;
    return true;
  }

  failures++;
  // Back off: 1 s, 2 s, 4 s, 8 s, up to a minute.
  unsigned long wait = 1000UL << min(failures, 6);
  nextRetry = millis() + min(wait, 60000UL);
  return false;
}

Backing off like this is what the networked world does, and the reasoning is the same: a failing thing should be asked less often, not more.

A heartbeat, so you can tell at a glance

An LED that blinks once a second from your main loop tells you more than any amount of serial output, because you can see it from the doorway.

if (millis() - lastBlink >= 1000) {
  lastBlink = millis();
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

Steady blinking: the loop is going round. Stopped: it is not. Blinking faster than it should: it restarted and your interval variable went back to zero.

For a project you will not see, write a line to the SD card every hour whether anything happened or not. A log with a gap in it tells you exactly when something went wrong, which a log that only records events cannot.

What this does not fix

Being honest about the limits:

  • A watchdog cannot fix a supply that is too small. It will reset, and reset, and reset.
  • It cannot recover a corrupted SD card, which is why the logging tutorial closes the file after every line.
  • It does not help if the fault is in setup(), unless you disable it there first.
  • Nothing here notices a sensor that is lying. A probe reading a plausible but wrong value passes every check on this page. Sanity-check values against what is physically possible: a temperature of 300 degrees in a greenhouse means a sensor fault, not weather.

Check yourself

  1. Why should wdt_reset() appear only once in a sketch?
  2. The board restarts and prints “brown-out”. Where do you look?
  3. Why enable the watchdog at the end of setup() rather than the start?
  4. A sensor has failed fifty times in a row. What should the sketch do?

Answers in order: so a function stuck in a loop cannot keep petting it; the watchdog should measure whether the loop is going round. At the power supply, under load, not in the code. Because initialising a display or a network can take longer than the watchdog interval. Back off and retry less often, while continuing to do everything else it can.

Troubleshooting

Symptom Likely cause Check or fix
Board resets every 8 seconds wdt_reset() not reached It must run every pass of loop()
Resets during startup Watchdog enabled before slow setup Enable it at the end of setup()
Board hard to reprogram Watchdog reset loop wdt_disable() first thing in setup()
Locks up despite the watchdog Something pets it inside a stuck loop One call, at the end of loop()
Restarts, no clue why Reset reason not read Read MCUSR at startup
Reason says brown-out Supply, not software Measure the rail under load
Hangs for ever on a sensor A wait with no timeout Give every wait a deadline
Stops working when the card is removed No graceful degradation Carry on; report the missing part
Log has no gap, but data is wrong A sensor lying plausibly Range-check against what is possible
Heartbeat blinking too fast The board restarted Read the reset reason

Next steps

  • Easier first: Timing Without delay(), which every pattern here is built on.
  • Related: Troubleshooting by Symptom, for faults you are present to see.
  • Next project: a data logger left running for a week, with a heartbeat line every hour.

Sources and attribution

All text and code in this tutorial are original Siratek work.

  • ATmega328P datasheet for the watchdog timer, its interval options and the MCUSR reset-source flags.
  • avr/wdt.h is part of the AVR libc shipped with the Arduino IDE.

The caution about watchdog reset loops on older bootloaders is a known property of some ATmega328P bootloader versions rather than of any particular board. No third-party tutorial text, diagrams, photographs or code has been reproduced here.

Shop
Sidebar
0 Wishlist
0 Cart