Why delay() Freezes Your Arduino and How to Use millis() Instead
For my AI agent project, I wanted an Arduino to show what the AI was doing: a blue LED pulses while it researches, a yellow LED pulses while it debates, and a servo sweeps back and forth while itβs busy. The Python progr
For my AI agent project, I wanted an Arduino to show what the AI was doing: a blue LED pulses while it researches, a yellow LED pulses while it debates, and a servo sweeps back and forth while itβs busy. The Python program sends one letter over USB whenever the state changes. The first version youβd naturally write uses delay(), and it has a frustrating bug: the Arduino ignores new messages for seconds at a time. Hereβs why, and how millis() fixes it.
The Problem: The Arduino Stops Listening
Hereβs the obvious way to blink an LED and sweep a servo:
void loop() {
if (Serial.available() > 0) {
state = Serial.read();
}
if (state == 'B') {
digitalWrite(BLUE_LED, HIGH);
delay(500);
digitalWrite(BLUE_LED, LOW);
delay(500);
for (int angle = 0; angle <= 180; angle++) {
servo.write(angle);
delay(15);
}
}
}
It looks right, but one pass through loop() now takes almost four seconds: one second for the LED and about 2.7 seconds for the sweep. During all that time the Arduino only checks for new messages once. When the AI finishes and Python sends W, the board keeps blinking blue for a few more seconds, and the LED and servo canβt move at the same time either.
Why delay() Causes It
delay(500) doesnβt mean βwait here while doing other thingsβ. It means βdo nothing at all for 500 millisecondsβ. The Arduino canβt read serial, update the servo, or blink another LED while itβs inside delay(). Every delay you add makes the whole board slower to react.
The Fix: Check the Clock with millis()
millis() returns how many milliseconds have passed since the board started. Instead of stopping to wait, each part of the program remembers when it last did something, and on every pass through loop() it checks: βhas enough time passed yet?β If not, it moves on straight away. loop() then runs thousands of times a second, so everything stays responsive.
#include <Servo.h>
const int BLUE_LED = 12;
const int YELLOW_LED = 13;
const int SERVO_PIN = 8;
Servo servo;
char state = '0';
unsigned long lastLedToggle = 0;
unsigned long lastServoStep = 0;
bool pulseOn = false;
int angle = 90;
int step = 2;
// Blink one LED without stopping the program
void pulse(int pin, unsigned long intervalMs) {
unsigned long now = millis();
if (now - lastLedToggle >= intervalMs) {
pulseOn = !pulseOn;
lastLedToggle = now;
}
digitalWrite(pin, pulseOn ? HIGH : LOW);
}
// Move the servo a little, only when it's time to
void sweep(unsigned long intervalMs) {
unsigned long now = millis();
if (now - lastServoStep >= intervalMs) {
lastServoStep = now;
angle += step;
if (angle >= 180 || angle <= 0) {
step = -step; // turn around at the ends
}
servo.write(angle);
}
}
void setup() {
Serial.begin(9600);
pinMode(BLUE_LED, OUTPUT);
pinMode(YELLOW_LED, OUTPUT);
servo.attach(SERVO_PIN);
servo.write(90);
}
void loop() {
if (Serial.available() > 0) {
state = Serial.read();
digitalWrite(BLUE_LED, LOW);
digitalWrite(YELLOW_LED, LOW);
}
if (state == 'B') { // researching
pulse(BLUE_LED, 500);
sweep(15);
} else if (state == 'Y') { // debating: faster pulse
pulse(YELLOW_LED, 200);
sweep(15);
} else { // '0' idle or 'W' done
servo.write(90);
}
}
Now a new letter takes effect within a few milliseconds, and the LED pulses while the servo sweeps. Writing now - lastLedToggle >= intervalMs (instead of comparing against a future time) also keeps working when millis() wraps back to zero after about 49 days.
Two More Gotchas from This Project
The board resets when Python connects. Opening the serial port restarts an Arduino Uno, so the first letters you send get lost. Wait about two seconds after
serial.Serial(...)before sending anything:time.sleep(2).Install pyserial, not serial.
pip install serialinstalls a different package. If Python saysmodule 'serial' has no attribute 'Serial', runpip uninstall serial pyserialand thenpip install pyserial.
A small SG90 servo is fine on the Unoβs 5V pin, but a bigger servo can pull so much power that the board resets mid-sweep. Give it its own 5V supply and connect the grounds together.
See It in the Full Project
The Python side, the wiring steps and how the AI agents decide which letter to send are all in Architecting Autonomous AI Agents with Hardware Integration. If youβre new to agents, start with Creating Agents: A Complete Guide.
What are you building with your Arduino? Tell me in the comments, and I might write a tutorial for it.
Originally published on my blog. I post new C, Python and AI-agent guides there regularly.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.