XL LED/Using it, and when it will not light/08. Blinking without stopping
Using it, and when it will not light · 08 of 9

Blinking without stopping

delay() blinks the LED by stopping the whole sketch, so anything else the sketch should notice while it waits is missed. Checking the clock with millis() blinks it exactly the same and leaves the sketch free.

Deaf while it waits

Blinking without stopping
Presses so far
0
delay() saw
0
millis() saw
0
Green bars are a button being pressed for a tenth of a second. Run it and watch which lane notices.

Both lanes blink the LED identically: half a second on, half a second off. The difference is what else each sketch can do.

The first blink sketch spends its whole life inside delay(). It sets the pin, waits 500 ms, sets it again and waits another 500 ms. If it also read a button, it could only do that in the moment between the second delay and the first, once a second. A press that begins and ends inside a delay is never seen at all. In the figure, one press out of four happens to be held at that moment, and it is caught by luck.

Look at the clock instead

millis() returns the number of milliseconds since the board started. Instead of waiting half a second, the sketch asks, every time round loop(), whether half a second has passed since the LED last changed. If it has, it changes the LED and notes the time. If not, it carries on at once.

So loop() comes round thousands of times a second, and every pass is a chance to read a button, a sensor or the serial port. The LED keeps perfect time, and nothing else waits for it.

Three details that matter

  • unsigned long, for every variable that holds a time. millis() outgrows an int in about half a minute on an Uno.
  • now - lastChange >= INTERVAL_MS, written as a subtraction. After about 49 days millis() wraps back to zero, and the subtraction still gives the right answer across the wrap. Comparing against a future time does not.
  • Nothing else in loop() may block. One delay() elsewhere brings the problem back.

The LED as a signal from your code

This is how the LED becomes useful beyond blinking. A sketch can light it when a reading crosses a threshold, flash it while the Wi-Fi connects, or blink a count to report an error, all without stopping to do it. Most of the TinkerBlock lessons after this one use it that way, as a light that says what the code is doing.

The code

Blinks the LED once a second and counts loop() passes, to show the sketch never stops. No library. Change LED_PIN to the pin you wired SIGNAL to.

xl_led_blink_without_delay.ino
/*
  XL LED - blinking without delay()                       TK01 / /p/tk01

  Wiring. Count from the square pad on the TinkerBlock board, LED side
  up, header at the bottom:

    GND    -> GND
    NC     -> nothing   (both NC pins are unconnected on the board)
    SIGNAL -> D9 on an Uno, GPIO 4 on an ESP32 or ESP32-S3,
              GP15 on a Raspberry Pi Pico

  Arduino IDE
    Tools > Board                 your board, e.g. ESP32S3 Dev Module
    Tools > Port                  the one that appears when you plug in
    Tools > USB CDC On Boot       Enabled   (ESP32-S3 only)
    No library needed.
*/

// The GPIO number SIGNAL is wired to. Uno: 9. ESP32, ESP32-S3: 4. Pico: 15.
const int LED_PIN = 4;
const unsigned long INTERVAL_MS = 500;   // on for 500 ms, off for 500 ms

unsigned long lastChange = 0;
bool ledOn = false;
unsigned long passes = 0;

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

void loop() {
  unsigned long now = millis();

  // Has half a second gone by since the LED last changed?
  if (now - lastChange >= INTERVAL_MS) {
    lastChange = now;
    ledOn = !ledOn;
    digitalWrite(LED_PIN, ledOn ? HIGH : LOW);

    if (ledOn) {                    // once a second, report and reset
      Serial.print("loop() ran ");
      Serial.print(passes);
      Serial.println(" times in the last second");
      passes = 0;
    }
  }

  passes++;
  // Anything else goes here: read a button, a sensor, the serial port.
  // None of it waits for the LED.
}

The count printed each second is how many times loop() ran in that second: tens of thousands on an Uno, more on an ESP32. With delay() it would be one. Every one of those passes is a chance to read a button or a sensor, which is the whole point.

When it does not work

Why unsigned long, and not int?

millis() counts milliseconds since the board started, and it passes 32,767 after about half a minute. On an Uno an int would overflow and the blink would stop. An unsigned long lasts about 49 days, and the subtraction in the sketch still works when it wraps round.

What happens after 49 days?

millis() wraps back to zero, and the sketch keeps working. now - lastChange is computed in unsigned arithmetic, which gives the right answer across the wrap. Comparing millis() with a future time, such as millis() > nextChange, is the version that breaks.

The LED blinks unevenly when I add more code to loop().

Something else in loop() is blocking: another delay(), a long Serial print at low baud, or a library call that waits. The millis() pattern only keeps time if loop() comes round often. Find the blocking call and give it the same treatment.

Can I blink two LEDs at different rates?

Yes, that is what this pattern is for. Give each LED its own lastChange variable and interval, and its own if block in loop(). Two TK01 blocks on two pins will blink independently without either waiting for the other.

Where this goes next

The short list of reasons, in the order they are usually the answer.

When it stays dark

Edit this page — content/books/xl-led/blinking-without-stopping.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

XL LED

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum