Skip to content

Robotics

How to write Arduino robot code without delay()

To write Arduino robot code without delay(), stop waiting and start checking the clock. Remember when the current step began in a variable, and on every pass of loop() ask whether millis() minus that time has reached the step's length. Nothing ever waits, so loop() runs thousands of times a second and the robot reads its sensors on every pass.

Deaf while it waits

A simple patrol drives straight for a second, then pivots for half a second:

analogWrite(9, 255); analogWrite(10, 255);
delay(1000);                       // straight
analogWrite(10, 0);
delay(500);                        // pivot
bool bumped = (digitalRead(2) == LOW);   // read once a lap

The bumper is only read once a lap, after both delays. Bump the robot 0.2 s into the straight leg and it grinds on for another 0.8 s, pivots for 0.5 s, and only then notices: 1.3 s late. A quick tap that's over by then is never seen at all.

Glance at the clock

Replace the waits with a timestamp, since, and a flag, turning, that says which leg the robot is on:

if (!turning && millis() - since >= 1000) {   // straight leg over
  turning = true;
  since = millis();
}
if (turning && millis() - since >= 500) {     // pivot over
  turning = false;
  since = millis();
}

Each if is checked every pass, and only one can fire. Resetting since starts the clock for the new leg; forget it and the next leg ends the moment it starts.

The bumper comes first

Now drive from the flag, with the bumper checked first so it beats any leg:

if (digitalRead(2) == LOW) {        // bumped: freeze
  analogWrite(9, 0);   analogWrite(10, 0);
} else if (turning) {
  analogWrite(9, 255); analogWrite(10, 0);
} else {
  analogWrite(9, 255); analogWrite(10, 255);
}

The first matching branch wins, so a press stops both wheels whatever the patrol is doing, within a millisecond.

Live experiment

Poke the bumper mid-patrol

The rover patrols: straight for 1000 ms, pivot for 500 ms. Each poke holds the bumper for 0.3 s. Timed with delay(), the program reads pin 2 only once a lap, so a poke in the middle of a leg goes unheard. Timed with millis(), it reads the bumper every pass and stops the moment it's pressed.

Try it yourself: free lesson

“Patrol without waiting” is lesson 5 of 6 in Rover Ridge, Robotics World 1. Free with an account. Lessons unlock in order, so you’ll do the 4 before it first.

A state machine in miniature

turning is a two-state state machine: each state has its own wheel speeds and its own way out. Add more states, backing up or searching, and an enum with a switch keeps them tidy. The pattern underneath stays the same: check inputs first, check the clock, act, and never wait.

Free lesson

Now build it yourself

Each lesson comes in three short parts: see the idea, try it on a live board, then build it yourself, with quick checks as you go.

Try it yourself: free lesson

“Patrol without waiting” is lesson 5 of 6 in Rover Ridge, Robotics World 1. Free with an account. Lessons unlock in order, so you’ll do the 4 before it first.

Or start from the very first lesson, no account needed

Practice questions

Three quick questions from the lessons. Pick an answer to check it.

  1. Question 1 of 3A patrol drives straight with delay(1000), pivots with delay(500), then reads the bumper. The bumper is pressed 0.2 s into the straight leg and held. The program notices…
    Show the answer

    Answer: 1.3 s later, once both delays are over. delay() stops everything, including reading the bumper, until it's over: 0.8 s left on the straight, then 0.5 s of pivot. A quick tap that's over by then is never seen at all.

    • Why not “Never”? The press is held, so it's still there when the program finally reads pin 2 after the pivot.
    • Why not “At once”? delay() blocks the whole program, so the bumper can't be read until both delays end.
    • Why not “0.2 s later”? Nothing is read during a delay. The rest of the straight leg, 0.8 s, and the 0.5 s pivot both run first.
  2. Question 2 of 3A patrol timed with delay() has its bumper poked for 0.3 s in the middle of a straight leg. The rover…
    Show the answer

    Answer: Never notices: it only reads pin 2 after both delays, and the poke is over by then. The delay() version reads the bumper once a lap, after the pivot. A quick poke comes and goes in between and is never seen. The millis() version reads it every pass and stops the same moment.

    • Why not “Stops when the poke ends”? When the poke ends the bumper reads HIGH again, so even the late read sees nothing.
    • Why not “Turns the other way”? Nothing in the program reacts by turning. The poke simply goes unseen.
    • Why not “Stops at once”? Stopping at once needs the bumper read during the leg, which delay() prevents.
  3. Question 3 of 3if (!turning && millis() - since >= 1000) { turning = true; since = millis(); } What does resetting since do?
    Show the answer

    Answer: Starts the clock for the new leg, so the turn is timed from now. since marks when the current leg began. Setting it to now when a leg changes means the next comparison measures the new leg's length, not the total time.

    • Why not “Stops the clock”? millis() keeps counting whatever happens. since just marks when the new leg began.
    • Why not “Nothing”? Without the reset, the next leg's test measures time since the old leg began and ends almost at once.
    • Why not “Restarts loop()”? loop() runs again on its own every pass. since is only a number the comparisons use.
Keep practising: free lesson

“Patrol without waiting” is lesson 5 of 6 in Rover Ridge, Robotics World 1. Free with an account. Lessons unlock in order, so you’ll do the 4 before it first.

Questions people ask

Why does my Arduino robot ignore its sensors?

Usually a delay() somewhere in loop(). While it runs, the program does nothing else: no digitalRead, no analogRead, no reaction. A one-second delay makes the robot blind for a second at a time. Time things with millis() instead.

How do I time two different things with millis()?

Give each one its own timestamp, or use one timestamp and a variable that says which step you're on. Each pass, compare millis() minus the timestamp with that step's length; when it's reached, change step and reset the timestamp.

What happens when millis() overflows?

It wraps back to 0 after about 49.7 days. If you store times as unsigned long and always test millis() − since against a length, the subtraction still gives the right answer across the wrap.

Every topic