Time Functions

Lesson 18 - Using micros() for Microsecond Timing

Learn how micros() enables non-blocking timing with microsecond precision for short interval tasks.

Progress indicator

Lesson 18 of 18

Learning Objectives

  • Understand what micros() returns in Arduino/ESP32 programs.
  • Use micros() syntax with unsigned long timing variables.
  • Build a basic non-blocking micros() interval check pattern.
  • Compare millis() and micros() use cases clearly.
  • Handle micros() overflow safely with subtraction logic.
  • Design cleaner timing code by naming units clearly (Us vs Ms).

Concept Explanation

What is micros()

micros() returns the number of microseconds since the board started running.

It is useful when your timing intervals are too short for millis().

This makes micros() helpful for pulse-level timing and short-interval experiments.

micros() Syntax

unsigned long nowUs = micros();

Use unsigned long to store micros values safely.

How micros() Works

The microsecond counter increases continuously in the background. Your code checks elapsed time by subtracting a previous timestamp from current micros.

If elapsed time reaches your interval, run the timed action and update the previous timestamp.

Microseconds Counter Concept

Think of micros() as a faster stopwatch than millis(). It measures much smaller time steps.

You do not reset this counter manually. You only compare current and previous values.

millis() vs micros() (Comparison)

  • millis(): better for millisecond-level tasks (LED blink, UI refresh).
  • micros(): better for short intervals and pulse-level timing.
  • Use micros() only when you need that extra timing granularity.

Overusing micros() in simple projects can make code harder to read without practical benefit.

Basic micros() Timing Pattern

unsigned long currentUs = micros();
if (currentUs - previousUs >= intervalUs) {
  previousUs = currentUs;
  // timed action
}

When to Use micros()

  • Pulse generation and pulse measurement
  • Short protocol timing experiments
  • Tasks where millisecond timing is too coarse

Handling micros() Overflow

micros() eventually wraps around. The subtraction pattern currentUs - previousUs >= intervalUs remains safe with unsigned values.

Avoid direct comparisons like currentUs > previousUs + intervalUs near overflow.

Example Code

This example toggles a pin using non-blocking micros() timing.

const int pulsePin = 2;
unsigned long previousMicros = 0;
const unsigned long intervalUs = 100;
bool pulseState = false;

void setup() {
  pinMode(pulsePin, OUTPUT);
}

void loop() {
  unsigned long currentMicros = micros();

  if (currentMicros - previousMicros >= intervalUs) {
    previousMicros = currentMicros;
    pulseState = !pulseState;
    digitalWrite(pulsePin, pulseState ? HIGH : LOW);
  }
}

What this example teaches

  • How to toggle outputs with microsecond interval checks
  • Why subtraction-based elapsed checks are the standard timing pattern
  • How to keep loop non-blocking while still running timed actions

Example Code Explanation

  1. previousMicros stores the last trigger time.
  2. intervalUs stores target interval in microseconds.
  3. Read current time with currentMicros = micros().
  4. If elapsed time reaches interval, update timestamp and toggle output state.
  5. This pattern avoids blocking waits and keeps loop responsive.

Beginner reading tip

Focus on these three variables first: currentMicros, previousMicros, and intervalUs. They define the full timing rule.

Common Mistakes with micros()

  • Using int instead of unsigned long.
  • Mixing millisecond and microsecond units in the same variable.
  • Using direct absolute comparisons instead of elapsed subtraction.
  • Updating previousMicros every loop even when interval is not reached.

Best Practices for micros()

  • Keep timing variables as unsigned long.
  • Always document units in variable names, like intervalUs.
  • Use micros() only when you actually need microsecond-level timing.
  • Keep interval constants explicit, for example const unsigned long intervalUs = 100;.

Practice Task

  1. Change intervalUs from 100 to 200 and describe output behavior.
  2. Add a second task that uses millis() in the same loop for a slower action.
  3. Explain why both timing systems can coexist without blocking each other.
  4. Write one short note on why subtraction logic is overflow-safe.

Try it now

Open simulator workspace and practice microsecond-level timing patterns.

Run in Simulator