Fanlab

Robotics · Microcontrollers · Grade 8

millis() and multitasking

Two LEDs at different speeds and a button that reacts at once — why does delay() freeze everything?

Open the experiment

The situation

On a robot car, two LEDs must blink at different speeds, and when the button is pressed the signal must light up at once. The first program is easy to write: on — delay(500) — off — delay(500). But if you press the button, often nothing happens: the program is “asleep”. What does the microcontroller do inside delay()? Is there a clock on the board — how does millis() work, and why does it go back to 0 after 49.7 days? How can we control two LEDs and a button in one loop() with no delay at all?

Topic

millis() and multitasking: a program without delay(). Two LEDs blink at different speeds while a button responds at once — comparing times

Key ideas

  • delay() halts the CPU
  • millis() — a clock
  • State machine

Formulas

Tloop=∑𝚍𝚎𝚕𝚊𝚢(…),tresp≤Tloop

With delay(): the period of loop() is the sum of all the delays; the button is read at only one point, in the worst case the response is late by a whole period, and a short press is lost.

𝚖𝚒𝚕𝚕𝚒𝚜()=t−t0

millis() — the milliseconds since the board was switched on (or RESET): a 32-bit `unsigned long`, counted in the background by the Timer0 interrupt; it does not stop the program.

𝚗𝚘𝚠−tlast≥T⇒toggle,tlast=𝚗𝚘𝚠

State machine: each task remembers its own “last time”; at every pass loop() only checks — it never waits, so the button is read on every pass.

232 ms=4294967296 ms≈49.71 days

Overflow: after 4 294 967 295 an `unsigned long` goes back to 0 — after 49 days 17 hours 2 minutes 47 seconds.

(5−4294967290)mod232=11

Subtraction works correctly: unsigned subtraction is also done modulo 232 — even if the clock has gone back to 0, 𝚗𝚘𝚠−t gives the real elapsed time. But 𝚗𝚘𝚠≥t+T goes wrong at overflow.

Theory

What delay() does

`delay(500)` — for 500 ms the microcontroller only counts time: it neither reads the button, nor controls the other LED, nor writes to Serial. So in a loop() written with delay the button is read on only one line — when its turn to be checked comes. In the sample program loop() lasts 2 seconds: if you press the button for 150 ms, the program will miss it with a probability of 93 %. You can see this in the timeline: grey blocks — the CPU is asleep, a press with a red dashed outline — not noticed.

millis() — the clock on the board

In the ATmega328P Timer0 triggers an interrupt about every 1 ms, and the interrupt increases a 32-bit counter by one — whatever the program is doing, even inside delay(). `millis()` returns this counter: the milliseconds since the board was switched on. It waits for nothing — you simply ask “what time is it?”. The type is `unsigned long` (from 0 to 4 294 967 295); if you store the value in an `int`, it goes wrong after 32 seconds.

State machine: blinking without delay

The idea: we do not wait — we check. For each task we remember the time of “the last time”: `if (millis() - tRed >= 300) { tRed = millis(); red = !red; digitalWrite(RED, red); }`. loop() runs thousands of times a second: on every pass — has the time come for red, for green — and is the button pressed? That is why the two LEDs blink with independent periods (300 and 700 ms — hard to write with delay), while the response to the button comes within one pass — milliseconds. This is a state machine: the program stores a state (red, tRed) and changes it according to the time. The same method lets you combine a servo, a sensor and Serial in a single loop().

49.7 days: overflow

The 32-bit counter goes back to 0 after 232 = 4 294 967 296 ms ≈ 49.71 days. The simple comparison `if (now >= tRed + 250)` goes wrong at that moment: `tRed + 250` overflows too and becomes a small number while `now` is still large — the condition is true on every pass, the LED flickers at the speed of loop until the clock also goes back to 0. The correct way is the difference: `now - tRed >= 250`. Unsigned subtraction is also done modulo 232: if now = 5 and tRed = 4 294 967 290, then 5 − 4 294 967 290 = −4 294 967 285, and in modular arithmetic this is 11 — exactly the elapsed time. In the experiment the “Start 6 s before the clock overflows” mode squeezes 49 days into 6 seconds.

Button: INPUT_PULLUP

`pinMode(BTN, INPUT_PULLUP)` “pulls” the pin up to 5 V through an internal ≈ 20–50 kΩ resistor: HIGH when the button is not pressed, LOW when it is pressed (the pin is connected to GND) — hence the condition `digitalRead(BTN) == LOW`. With a plain `INPUT` the pin is “floating”: like an antenna it picks up noise and shows random 0/1 — the yellow LED flickers on its own (try it). Contact bounce (debounce) and interrupts are covered in the “Button and digital input” and “Interrupts” experiments.

About the model

The program runs on the simulation clock: in each 8.3 ms step loop() runs many times (≈ 13 thousand times a second; on a real Uno, several tens of thousands for this program), and the button state is updated on the same step — so the response looks like 0–8 ms (in reality, microseconds). delay() is exact: 500 ms is exactly 500 ms. millis() counts in 1 ms steps (on a real board the ticks are 1.024 ms, with a small jitter). The floating input is modelled by a random sequence. LED current (5−ULED)/(220+25) Ω: red ≈ 12.7 mA, green ≈ 11.4, yellow ≈ 12.2.

Did you know?

  • Windows 95 and 98 had exactly this bug: the system counted the milliseconds since start-up in a 32-bit number and “froze” after 49.7 days of continuous running; Microsoft fixed it in 1999 (KB216641).
  • In Arduino millis() is driven by the Timer0 interrupt every 1.024 ms (16 MHz / 64 / 256); the core code (wiring.c) counts the accumulated 0.024 ms separately and adds one millisecond every 41–42 ticks — so millis() is accurate on average, but some of its steps are 2 ms.

Inside the experiment

A real board in a hand-inked isometric drawing: turn it, place the parts, wire the circuit, write and upload the code — current and voltage are computed at once, and a wrong connection burns a part. Inside are missions, theory, a quiz and a guided tour.

More experiments in this section