Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

What this course assumes

~45 min

A connected device is first of all a device. It reads a sensor, drives an output and keeps running for months. The network is one more job its firmware does, and it is the job most likely to stall. Every chapter of this course leans on a handful of firmware habits that make that safe. This section rebuilds each of them, in the form the later chapters use.

What the course expects you to bring

This course is about connecting small devices to a network. It starts from the point where you can already make a board do things on its own. That needs a few working habits, and this section goes over each one.

Here is the short list. Your program checks the time instead of pausing. It is built from named situations, called states. It keeps waiting data in a small circular list. It prints notes to your computer over the USB cable. It saves a few settings so they come back after a power cut. It can run two jobs side by side. And it has a guard timer that restarts the board if the program gets stuck.

Why do these matter more now? Talking to a network is slow and unpredictable. A request can take a few seconds, or never finish. A device that simply waits would stop reading its sensor and stop responding to buttons. Each habit is a way to keep the device working while the network is being difficult.

The boards in this course are ESP32 chips, programmed in C++ through the Arduino tools. Every code box on these pages was turned into a program for two of those boards, the ESP32-S3 and the ESP32-C6, when this site was built. None of them was run.

What this course assumes is a reader who can already put firmware on a board. Concretely, that means seven skills, each refreshed in this section:

  1. Timing without blocking: doing several jobs at different rates in one loop() by comparing millis() durations, never by waiting in delay().
  2. State machines: writing a job as named states and the events that move between them.
  3. Ring buffers: a fixed-size queue whose indices wrap around.
  4. Serial debugging: printing timestamped lines over USB to see what the firmware is doing.
  5. Preferences (NVS): keeping a few settings in flash so they survive a restart.
  6. FreeRTOS tasks: running two jobs side by side on an ESP32.
  7. Watchdogs: a timer that restarts the chip when a job stops reporting in.

The boards are ESP32s programmed in C++ with the Arduino-ESP32 core 3.3. Every sketch on these pages is compiled for the ESP32-S3 and the ESP32-C6 when the site is built, and not run. A network adds one new fact to all seven: a call that touches the network can take seconds, or never return. Each habit below is a way to keep that from freezing the rest of the device.

To go deeper on any of them, see embedded-systems, Chapters 12, 13, 15, 21 and 22.

Timing without blocking

millis() is a stopwatch that starts when the board powers up. It counts milliseconds, thousandths of a second. Your program can read it at any moment.

Say you want a reading every 2 seconds. Write down the stopwatch time when you take a reading. On every trip around loop(), the function that runs over and over, look at the stopwatch again. Has 2000 milliseconds passed since the time you wrote down? Then take a reading and write down the new time. If not, carry on with other work. Nothing ever stands still.

delay(2000) is the opposite. It freezes the whole program for 2 seconds. A button press in that time is missed.

There is one catch. The stopwatch is a fixed-size number, so after about 49.7 days it rolls over to zero, like a car's trip meter. Ask "how long since?" by subtracting, now minus then. That subtraction still gives the right answer across the rollover. Asking "is it later than then plus 2 seconds?" goes wrong right at the rollover. Always subtract.

millis() returns the number of milliseconds since the board started, as an unsigned long, a 32-bit unsigned whole number on the ESP32. A job that should run every P milliseconds keeps the time it last ran, last, and on every pass of loop() asks whether \text{now} - \text{last} \ge P. If so, it runs and sets \text{last} = \text{now}. If not, loop() moves straight on to the next job.

Two facts make this the standard pattern.

  1. Nothing waits. delay(2000) holds the processor in place for 2 s. The duration test returns at once, so one loop() can serve a sensor every 2 s, a button every 10 ms and a network client as often as it needs.
  2. The subtraction survives wraparound. A 32-bit counter of milliseconds wraps back to 0 after 2^{32} ms, about 49.7 days. Unsigned subtraction is arithmetic modulo 2^{32}, so now - last is still the true elapsed time across the wrap. Comparing moments, now >= last + P, is not: last + P can wrap past zero and the test fires at once, every pass.

A device meant to run for a year passes that wrap seven times, so only the duration form is acceptable.

// Two jobs at two rates in one loop(), with no delay().
const unsigned long READ_EVERY_MS = 2000;
const unsigned long BLINK_EVERY_MS = 250;
const int LED_PIN = 2;

unsigned long lastRead = 0;
unsigned long lastBlink = 0;
bool ledOn = false;

void setup() {
    Serial.begin(115200);
    pinMode(LED_PIN, OUTPUT);
}

void loop() {
    unsigned long now = millis();
    if (now - lastRead >= READ_EVERY_MS) {    // a duration: safe across wraparound
        lastRead = now;
        Serial.println("read the sensor");
    }
    if (now - lastBlink >= BLINK_EVERY_MS) {
        lastBlink = now;
        ledOn = !ledOn;
        digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
    }
    // a network client gets its turn here, on every pass
}

On a board this prints read the sensor every 2 seconds while the LED blinks four times a second. Pin 2 is an example; use the pin your LED is on.

Worked example

The subtraction across the wrap

  1. 1

    A sketch stored last = 4294966296, which is 2^{32} - 1000, just before the counter wrapped. Now millis() returns 1000. Is a 2000 ms job due?

  2. 2

    Do the unsigned subtraction. 1000 - 4294966296 is negative in ordinary arithmetic. Unsigned 32-bit subtraction adds 2^{32}: 1000 - 4294966296 + 4294967296 = 2000.

  3. 3

    Compare. 2000 \ge 2000, so the job runs. Exactly 2000 ms passed: 1000 before the wrap and 1000 after.

  4. 4

    Try the moment form. last + 2000 wraps to 1000, and now >= 1000 is true. It happens to work here, but one pass earlier, at now = 4294966300, it also says true, because last + 2000 wrapped to 1000. The job would fire 1996 ms early.

Quick review

Practice from earlier coursework, pulled in for this topic.

From Embedded Systems and the Internet of Things

On the Uno R4 Minima, int elapsed = millis() - start; is fine for a while. When does it start giving wrong answers?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

That's a 16-bit int, as on the Uno R3. On the Uno R4 an int is 32 bits.

It is 32 bits, but signed: its largest value is half of uint32_t's.

It can hold smaller values; the trouble starts when they get large.

Why

A signed 32-bit int holds up to 2,147,483,647 ms, about 24.8 days. A longer elapsed time comes out negative. Keep it in uint32_t.

A signed 32-bit int's largest value is 2^31 - 1 ms.

From Embedded Systems and the Internet of Things

uint32_t start = 4294967200; and later uint32_t now = 300;. What is now - start, in milliseconds?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

From 4,294,967,200 to the wrap is 96 ms; then 300 more: 396 ms.

Correct answer

Also accepted:

Why

How far is 4,294,967,200 from the wrap at 4,294,967,296?

State machines

A state machine is a way of describing a job as a short list of situations, called states, and the things that move you between them.

Take a motorized vent in a greenhouse. It can be closed, opening, open or closing. Those are its four states. It is always in exactly one of them. Things happen to it: someone asks it to open, someone asks it to close, or the motor finishes its travel. Those are events. A small table says what each event does in each state. "Open" while closed starts the motor and moves to opening. "Open" while already open does nothing.

In code, the current state is one variable. Each trip around loop() looks at it and checks whether anything should change. Nothing waits.

Connected devices use this pattern everywhere. A Wi-Fi link can be joining, connected or waiting to try again. Writing it as a state machine makes sure the device always has a way out of every situation, even when nobody is there to press reset.

A state machine describes a job as a fixed set of named states, the events that can happen, and a table that says, for each state and event, which state comes next and what to do on the way. The machine is always in exactly one state. In C++ the state is usually an enum class, and a switch on it runs once per pass of loop(), so the machine never waits either.

Connected firmware is full of these. A Wi-Fi link is joining, connected or waiting to retry. A command is received, being carried out, or done. An update is downloading, verifying or ready to boot. Writing each one as an explicit machine has two payoffs. Every state has a way out that doesn't depend on a person. And an event that makes no sense in the current state, such as "travel finished" while the vent is already open, is ignored instead of corrupting the job.

// A greenhouse vent as a state machine. updateVent() runs on every pass.
enum class Vent { Closed, Opening, Open, Closing };

Vent vent = Vent::Closed;
unsigned long moveStart = 0;
const unsigned long TRAVEL_MS = 8000;      // the motor takes 8 s end to end

void request(bool wantOpen, unsigned long now) {
    if (wantOpen && vent == Vent::Closed) { vent = Vent::Opening; moveStart = now; }
    if (!wantOpen && vent == Vent::Open)  { vent = Vent::Closing; moveStart = now; }
    // any other combination is ignored
}

void updateVent(unsigned long now) {
    switch (vent) {
        case Vent::Opening:
            if (now - moveStart >= TRAVEL_MS) vent = Vent::Open;
            break;
        case Vent::Closing:
            if (now - moveStart >= TRAVEL_MS) vent = Vent::Closed;
            break;
        default:
            break;                          // Closed and Open wait for a request
    }
}

void setup() {}

void loop() {
    updateVent(millis());
}

The vent's four states

Open the vent, then ask it to open again while it is still moving. Predict first: does the second request do anything? Then let the travel finish and close it.

Loading interactive…

Worked example

Tracing a vent through four events

  1. 1

    The vent starts CLOSED. These events arrive in order: open requested, open requested, travel done, travel done. Where does it end up?

  2. 2

    Open requested while CLOSED: the table has a row for it. Move to OPENING and start the motor.

  3. 3

    Open requested again while OPENING: no row. Ignored. The motor keeps running and its start time is not reset.

  4. 4

    Travel done while OPENING: move to OPEN.

  5. 5

    Travel done while OPEN: no row. Ignored. The vent ends OPEN, and a stray or repeated event never knocked it into a wrong state.

Quick review

Practice from earlier coursework, pulled in for this topic.

From Embedded Systems and the Internet of Things

Why use enum class State { Green, Yellow, Walk, Clear }; rather than plain numbers 0 to 3 for the states?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

The variable still takes memory; here, one byte or more.

Speed is the same; the gain is in catching mistakes.

switch works on plain integers too, as Chapter 10 showed.

Why

An enum class names each state and limits a variable to those names. Writing State::Wlak is a compile error; writing 5 for an unused number isn't.

From Embedded Systems and the Internet of Things

In a table-driven machine, an event arrives for which the current state has no row. What does the lookup in this chapter do?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Nothing resets the machine. The lookup returns the state it was given.

A missing row isn't an error; it's a decision to ignore that event.

Rows are matched by (state, event), not taken in order.

Why

next() returns the same state when no row matches. Leaving a row out is how a designer says an event means nothing in that state.

Look at what next() returns after its loop finds no matching row.

Ring buffers

A ring buffer is a waiting line with a fixed number of seats, say 8. New items join at the back. The oldest item leaves from the front.

The trick is how the seats are reused. Picture the 8 seats in a circle. When the line reaches seat 7, the next newcomer sits in seat 0, as long as it's empty. Nothing is ever moved along. The program just remembers two numbers: which seat holds the oldest item, and how many are waiting.

Because the seats are fixed, a ring buffer never asks for more memory. That matters on a small chip that must run for months. The cost is that it can fill up. Then you must pick: turn the new item away, or throw out the oldest to make room. A device that buffers readings while its network is down faces exactly that choice.

A ring buffer is a queue stored in a fixed array of capacity N. Two numbers describe it: head, the index of the oldest item, and count, how many items it holds. A push writes at index (\text{head} + \text{count}) \bmod N and adds 1 to count. A pop reads at head, then sets \text{head} = (\text{head} + 1) \bmod N and takes 1 from count. The \bmod N is what makes it a ring: after index N - 1 the next index is 0.

The buffer never allocates memory, and push and pop take the same short time whatever it holds. When it is full (\text{count} = N), something has to give: refuse the new item, or overwrite the oldest. That choice is the firmware's to make, deliberately. Chapter 8 uses exactly this structure to hold readings while the network is down.

Some ring buffers keep a tail index instead of a count and leave one slot always empty to tell full from empty; that version holds N - 1 items. The one below holds N.

// A ring buffer of 8 readings that refuses new items when full.
const unsigned CAP = 8;
float items[CAP];
unsigned head = 0;      // index of the oldest item
unsigned count = 0;     // how many items are stored

bool push(float x) {
    if (count == CAP) return false;           // full: the caller decides what to do
    items[(head + count) % CAP] = x;
    count = count + 1;
    return true;
}

bool pop(float &out) {
    if (count == 0) return false;              // empty
    out = items[head];
    head = (head + 1) % CAP;
    count = count - 1;
    return true;
}

void setup() {}
void loop() {}

Worked example

Where the fifth push lands

  1. 1

    A ring buffer has N = 4. It starts empty with head = 0. Push A, B, C; pop one; push D and E. Where is each item, and what are head and count?

  2. 2

    Three pushes. A goes to index (0 + 0) \bmod 4 = 0, B to 1, C to 2. Now count = 3.

  3. 3

    One pop returns A from index 0. Now head = 1 and count = 2.

  4. 4

    Push D at (1 + 2) \bmod 4 = 3. Now count = 3.

  5. 5

    Push E at (1 + 3) \bmod 4 = 0: it wraps into the slot A left. Now count = 4, the buffer is full, and the next push is refused. Reading from head = 1 gives B, C, D, E: oldest first.

Quick review

Practice from earlier coursework, pulled in for this topic.

From Embedded Systems and the Internet of Things

In the single-producer, single-consumer ring buffer, why does the interrupt handler store the value before it moves head?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Both orders take the same two stores. The order is about correctness.

The handler is the only writer of head; nothing else changes it.

With the opposite order the consumer can read a slot before its value arrives.

Why

Moving head is what makes the slot visible to the consumer. Store first, then publish, so the consumer never reads a stale slot.

Which line makes the slot visible to the consumer?

From Embedded Systems and the Internet of Things

A 16-slot ring buffer has head = 3 and tail = 12. How many values are waiting?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

Slots 12, 13, 14, 15, 0, 1 and 2 hold waiting values: (3 − 12 + 16) % 16 = 7.

Correct answer

Also accepted:

Why

Count from tail up to 15, then wrap to 0 and count up to just before head.

Serial output as a debugging tool

Serial is the board's way of talking to your computer through the USB cable. Whatever it prints shows up in a window on your screen, such as the Arduino IDE's Serial Monitor. Your sketch starts it once with Serial.begin(115200). The number is the speed, and the window must be set to the same one.

With a connected device, the most useful thing to print is a short note whenever something changes, with the time in front: "at 61 seconds, Wi-Fi connected." Later, when two machines disagree about what happened, those times let you match up the two stories.

Two warnings. Print when something changes, not on every trip around the loop, or the printing itself slows the board down. And never print a password. People paste these logs into help forums.

Serial sends text from the board to your computer over the USB cable, where the Arduino IDE's Serial Monitor or any terminal program shows it. Serial.begin(115200) in setup() opens it at 115 200 baud; the monitor must be set to the same rate.

For connected firmware, print events with a timestamp, not values in a tight loop. A line such as [ 61234] wifi: connected tells you what happened and when, so you can line it up against a broker's log or a packet capture later. Two rules keep logging from causing the bugs it is meant to find. Print on state changes, not on every pass, or the serial port becomes the bottleneck. And never print a password, key or token: serial logs get pasted into forums and issue trackers.

// Timestamped event lines, printed only when something changes.
bool lastButton = false;
#if CONFIG_IDF_TARGET_ESP32C6
const int BUTTON_PIN = 9;    // the BOOT button on the ESP32-C6-DevKitC-1
#else
const int BUTTON_PIN = 0;    // the BOOT button on the ESP32-S3-DevKitC-1
#endif

void logEvent(const char *what) {
    Serial.printf("[%8lu] %s\n", millis(), what);
}

void setup() {
    Serial.begin(115200);
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    logEvent("boot");
}

void loop() {
    bool pressed = (digitalRead(BUTTON_PIN) == LOW);
    if (pressed != lastButton) {             // a change, not a state
        lastButton = pressed;
        logEvent(pressed ? "button: down" : "button: up");
    }
}

Quick review

Practice from earlier coursework, pulled in for this topic.

From Embedded Systems and the Internet of Things

What's the difference between Serial.print(x) and Serial.println(x)?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

The monitor shows plain text. There are no fonts.

Both send x as readable text.

Neither adds a time. You print millis() yourself for that.

Why

Both send x as text. println then ends the line, so the next print starts on a new one.

From Embedded Systems and the Internet of Things

Where does Serial.begin(115200); normally go in a sketch?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Starting the port once is enough. Restarting it on every pass only wastes time.

begin starts the port; it isn't needed per print.

Serial must be started with begin before printing.

Why

Serial.begin starts the serial port. Like pinMode, it's one-time preparation, so it belongs in setup().

Settings that survive a restart

When an ESP32 loses power, everything in its working memory is gone. Some things must come back after a restart: the device's name, a correction for a sensor that reads high, or the Wi-Fi password someone typed in during setup. The chip keeps those in NVS, short for non-volatile storage. Non-volatile just means it survives a power cut.

NVS works like a small labelled filing box inside the chip's flash memory. Each value has a short label, called a key. The Arduino Preferences library lets you open a drawer, read a value by its label, and write a new one. When you read a key that was never saved, you get a default you chose instead.

Flash memory wears out a little each time it is rewritten. So save settings that change rarely. Don't save a temperature every few seconds.

An ESP32 keeps small settings in NVS (non-volatile storage), a key-value store in a partition of its flash memory. The Arduino Preferences library is the usual way in: begin(namespace, readOnly) opens a group of keys, getUInt(key, default) reads one and returns the default if it is missing, putUInt(key, value) writes one, and end() closes the group. Strings, floats and byte arrays have their own get and put functions.

Flash wears out with erasing, so NVS is for values that change rarely: a device name, a calibration offset, a count of crash restarts, the Wi-Fi credentials a user entered during setup (Chapter 4). A reading taken every few seconds does not belong there. A value that must change often is kept in RAM and saved only when it matters.

#include <Preferences.h>

Preferences prefs;

void setup() {
    Serial.begin(115200);
    prefs.begin("node", false);                       // namespace "node", read-write
    uint32_t boots = prefs.getUInt("boots", 0) + 1;   // 0 on the very first start
    prefs.putUInt("boots", boots);                    // one write per restart
    String name = prefs.getString("name", "bench1");
    prefs.end();
    Serial.printf("boot %lu as %s\n", (unsigned long)boots, name.c_str());
}

void loop() {}

Quick review

Practice from earlier coursework, pulled in for this topic.

From Embedded Systems and the Internet of Things

Why does NVS on the ESP32-S3 wear its flash far less than rewriting the same bytes in place would?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

NVS values survive power cuts, so they must be in flash.

It uses the same SPI flash as the sketch, rated 100,000 cycles.

Size isn't the point; erases per page are.

Why

NVS is log-structured: up to 126 entries share a 4 KB page before it needs an erase, which Espressif says cuts erases relative to writes by a factor of 126.

Does NVS change a stored value in place?

From Embedded Systems and the Internet of Things

This program runs on a computer and models programming a flash byte, where programming leaves stored AND new. What does it print?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

What this task checks

Structure checked automatically. Whether your ideas are convincing isn't graded, so reread with the checklist.

The compiler says

A model answer

Why

0x3C AND 0xC3 = 0x00: the second program can't restore the bits the first one cleared. After an erase to 0xFF, programming 0xC3 works.

Correct answer

Also accepted:

Why

0x3C and 0xC3 have no 1 bits in common.

Tasks and the watchdog

On an ESP32, your program can be split into separate jobs that run side by side. Each one is called a task. A small built-in system called FreeRTOS decides which task gets the processor at each moment. Even loop() is a task. A job that is waiting, for example for a reply from a slow server, steps aside. The other jobs keep running.

So the device can put its sensor reading in one task and its network talking in another. When the network is slow, the sensor task carries on.

The task watchdog is a guard timer. Each task it watches must check in now and then, like a night guard pressing a button on each round. If a task stops checking in, the program is probably stuck. After 5 seconds, the watchdog restarts the whole board. That is crude, but for a device in a field or on a roof, a restart is far better than sitting frozen until someone drives out to it.

The ESP32 Arduino core runs on FreeRTOS, a small real-time operating system. loop() itself runs inside a FreeRTOS task, an independent job with its own stack and a priority. xTaskCreate(fn, name, stackBytes, arg, priority, handle) starts another one. The scheduler runs the highest-priority task that is ready, and a task that calls vTaskDelay() or waits on a queue gives the processor up until it is ready again. So a sensor task can keep its schedule while a network task sits waiting for a slow server.

The task watchdog is a timer that each subscribed task must reset regularly. A task that stops resetting it, because it is stuck in a loop or waiting forever, makes the watchdog restart the chip, by default after 5 s. A task joins with esp_task_wdt_add(NULL) and reports in with esp_task_wdt_reset(). loop() is not watched until setup() calls enableLoopWDT(); after that the core resets the watchdog every time loop() returns. A restart is a blunt recovery, but for a device on a shelf far away it beats hanging until someone visits.

#include <esp_task_wdt.h>

void sensorTask(void *) {
    esp_task_wdt_add(NULL);                  // this task joins the task watchdog
    for (;;) {
        // read the sensor and hand the reading on
        esp_task_wdt_reset();                // "still alive"
        vTaskDelay(pdMS_TO_TICKS(1000));     // waits without holding the processor
    }
}

void setup() {
    xTaskCreate(sensorTask, "sensor", 4096, nullptr, 2, nullptr);
    enableLoopWDT();                         // watch loop() as well
}

void loop() {
    // the network code lives here; if it hangs for 5 s, the board restarts
}

A slow network task beside a sensor task

The sensor task (higher priority) needs 1 ms every 10 ms. The network task needs 25 ms of work every 40 ms. Predict first: does the sensor task ever miss its 10 ms deadline? Then read the lanes.

Loading interactive…

Worked example

What the watchdog catches, and what it can't

  1. 1

    A sketch subscribes loop() with enableLoopWDT(). Its network code calls a function that waits for a server reply with no timeout, and the server never answers. What happens?

  2. 2

    loop() never returns, so the core never resets the watchdog for it.

  3. 3

    After 5 s the task watchdog fires and the chip restarts. The device comes back and tries again.

  4. 4

    What it can't fix. If the network code returned quickly but with the wrong answer every time, loop() would keep returning and the watchdog would stay happy. The watchdog detects a stuck program, not a wrong one. The real fix for the hang is a timeout on the wait, which Chapter 8 adds.

Quick review

Practice from earlier coursework, pulled in for this topic.

From Embedded Systems and the Internet of Things

A sketch crashes during start-up because of a bad sensor driver, and the watchdog restarts it every few seconds. What stops this boot loop?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

A longer timeout only makes each loop of the crash slower.

That hides the crash instead of recovering from it.

Then the device hangs or crashes with no recovery at all.

Why

esp_reset_reason() says the last reset was a crash; a streak stored in NVS lets start-up switch the suspect features off and report.

The reset reason tells start-up that the last run crashed.

From Embedded Systems and the Internet of Things

esp_reset_reason() returns ESP_RST_TASK_WDT at start-up. What happened before this start?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

A reset pin or power-on has its own value.

That would be ESP_RST_BROWNOUT.

That would be ESP_RST_DEEPSLEEP.

Why

ESP_RST_TASK_WDT means the task watchdog caused the last reset.

Each reset cause has its own named value.

Which habit fixes it?

Each card is a problem in a connected device's firmware. Sort it under the habit that fixes it. Decide before you drag, then check.

Loading interactive…

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

What this task checks

Structure checked automatically. Whether your ideas are convincing isn't graded, so reread with the checklist.

The compiler says

A model answer

Correct answer

Also accepted:

Why

Why

A blinking LED that freezes is a delay() problem, and the 49-day misfire is a moment comparison instead of a duration: both are millis() fixes. A forgotten name needs NVS. Readings during an outage need a queue to wait in. A request that should be ignored needs a state machine. A hang needs a watchdog (and, better still, a timeout).

Check your understanding (counts toward your progress)

A sensor task (priority 2) runs 1 ms of work every 10 ms. A network task (priority 1) waits up to 3 s for a slow server inside vTaskDelay() and blocking reads. Does the sensor task keep its schedule?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

That is what a single loop() with a blocking call does. Separate tasks are the fix.

Even on the single-core ESP32-C6 the scheduler switches tasks; a second core isn't needed.

In a FreeRTOS task, delay() is vTaskDelay() underneath. It changes nothing here.

Why

The scheduler runs the highest-priority ready task. The sensor task is ready every 10 ms and outranks the network task, which spends its wait blocked anyway.

A task that is waiting is not running.

A sketch must read a sensor every 2 s and check a button every 10 ms. Which line belongs inside loop()?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

During that delay(2000) the button goes unchecked for two whole seconds.

An empty while is a delay written by hand: nothing else runs until it ends.

This compares moments. When lastRead + 2000 wraps past zero, the test is true on every pass.

Why

Subtracting the stored time from now gives the elapsed duration, which unsigned arithmetic keeps correct across wraparound, and the test returns immediately when nothing is due.

Which line returns at once when the reading isn't due yet, and stays right after day 49?

A ring buffer with capacity 8 has head = 6 and count = 3. At which index does the next push write?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

(6 + 3) mod 8 = 9 mod 8 = 1. The items sit at 6, 7 and 0, so the next one wraps to 1.

Correct answer

Also accepted:

Why

The next free slot is (head + count) mod capacity.

Which Serial line is most useful when a device's Wi-Fi drops now and then?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Thousands of identical lines a second hide the one change you're looking for, and can slow the board down.

Never print a secret. Logs get pasted into forums.

Printing on changes costs almost nothing and is how you find out what happened.

Why

A timestamped event line, printed on the change, tells you what happened and when, without flooding the port.

What would you want to line up against another machine's log later?

Practice: Firmware habits connected devices lean on (5 problems · doesn’t affect your progress)

Why do the firmware habits in this section matter more once a device talks to a network?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Memory matters, but these habits are about not stalling.

No law is involved: it's about keeping the device responsive.

A single non-blocking loop() can run a network client too.

Why

Each habit keeps the rest of the device working while the slowest, least predictable job is waiting.

A network call can stall for seconds, and nothing else may stop

A sketch stored last = 4294966796 (2^32 − 500). Now millis() returns 1500. How many milliseconds does now - last give?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

1500 − 4294966796 + 4294967296 = 2000: 500 ms before the wrap and 1500 after.

Correct answer

Also accepted:

Why

Add 2^32 when the subtraction would go negative.

2000

A ring buffer of capacity 4 holds 4 items with head = 2. Two pops happen. What is head now?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

2 → 3 → (3 + 1) mod 4 = 0.

Correct answer

Also accepted:

Why

Each pop moves head one step, wrapping after 3.

0

prefs.getUInt("boots", 0) runs on a brand-new board that has never saved boots. What does it return?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

A missing key isn't an error; that is what the default is for.

The library returns the default for a missing key.

The sketch adds 1 afterwards; the call itself returns the default.

Why

A missing key returns the default you pass, so first-boot code needs no special case.

0, the default given in the call

A task calls esp_task_wdt_add(NULL) and then waits on a server with no timeout. The server never replies. What is the better long-term fix than relying on the watchdog's restart?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Then the device hangs forever with nothing to recover it.

That just hangs longer before the same restart.

A blocked task can't run any code inside its own wait.

Why

The watchdog is the last line of defence. A timeout lets the code notice the failure and carry on without a restart.

Give the wait a timeout and handle the failure

Where this goes next

Every habit here has a home later in the course. Chapter 4 saves the Wi-Fi credentials a user enters during setup in NVS. Chapter 8 runs the connection as a state machine, schedules reconnect attempts with millis() durations, and keeps readings in a ring buffer while the link is down. Chapter 9 §8 runs an MQTT client from loop() without blocking, and logs its events over Serial. Chapter 18 moves the same ideas to Rust tasks on the ESP32-C6, and Chapter 21 leans on the watchdog to roll back a firmware update that hangs.

Feeling good about this section?

Take the section quiz

Every answer gets a full explanation at the end.

Sign in to save highlights

Insert an equation

  • No matches.