Bridging the Gap Between Human and Machine Time

In the realm of microcontroller programming, human-scale time is measured in milliseconds. We blink LEDs, debounce buttons, and read serial buffers using the standard delay() function. However, when interfacing with high-frequency sensors, radio frequency (RF) modules, or infrared (IR) protocols, milliseconds are practically an eternity. This is where mastering the Arduino delay microseconds function becomes a critical skill for any serious maker or embedded engineer.

The delayMicroseconds() function pauses the program execution for a specified number of microseconds (one-millionth of a second). While it appears simple on the surface, deploying it effectively requires a deep understanding of your board's clock architecture, interrupt overhead, and hardware limitations. In this comprehensive 2026 guide, we will dissect the mechanics of microsecond timing, explore real-world use cases like generating 38kHz IR carriers and triggering HC-SR04 ultrasonic sensors, and uncover the hidden pitfalls that cause timing drift.

The Physics of Microsecond Timing: Clock Cycles and Overhead

To understand how delayMicroseconds() works, you must look at the silicon. A microsecond (1µs) is 0.000001 seconds. The accuracy of this delay is entirely dependent on the microcontroller's clock speed and the instruction cycles required to execute the delay loop.

Architecture Breakdown

  • Legacy AVR (Arduino Uno R3 / Nano): Running at 16 MHz, the ATmega328P executes 16 million clock cycles per second. Therefore, 1µs equals exactly 16 clock cycles. The Arduino core implements delayMicroseconds() using a highly optimized, inline assembly loop that burns exactly 16 cycles per iteration.
  • Modern ARM (Arduino Uno R4 Minima): Powered by the Renesas RA4M1 running at 48 MHz, 1µs equals 48 clock cycles. The ARM Cortex-M4F architecture handles delays differently, often relying on the hardware SysTick timer or DWT (Data Watchpoint and Trace) cycle counters for sub-microsecond accuracy.
  • Dual-Core Wi-Fi (ESP32-C3 / ESP32-S3): Running at 160 MHz to 240 MHz, the ESP32 maps this function to the underlying FreeRTOS ets_delay_us(). However, because the ESP32 runs a complex RTOS with Wi-Fi/Bluetooth stacks, software-based microsecond delays are highly susceptible to context-switching jitter.

Step-by-Step Implementation: Real-World Scenarios

Let us move from theory to practice by examining two of the most common applications for the Arduino delay microseconds function.

Scenario A: Generating a 38kHz IR Carrier Signal

Standard infrared remote controls use a 38kHz carrier frequency to transmit data. This requires pulsing an IR LED on and off 38,000 times per second.

The Math: 1 second / 38,000 = 26.31µs per full cycle. For a 50% duty cycle, the pin must be HIGH for ~13µs and LOW for ~13µs.

const int IR_LED_PIN = 9;

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

void loop() {
  // Generate a 38kHz burst for 10 milliseconds
  for(int i = 0; i < 380; i++) {
    digitalWrite(IR_LED_PIN, HIGH);
    delayMicroseconds(12); // 13us minus ~1us digitalWrite overhead
    digitalWrite(IR_LED_PIN, LOW);
    delayMicroseconds(12);
  }
  delay(20); // Pause between bursts
}

Expert Insight: Notice we use 12 instead of 13. The digitalWrite() function on an AVR board takes approximately 3 to 4 clock cycles (roughly 0.25µs) to execute. By slightly under-shooting the delay value, we compensate for the instruction overhead, keeping the total period closer to the target 26.3µs.

Scenario B: Triggering an HC-SR04 Ultrasonic Sensor

The ubiquitous HC-SR04 ultrasonic distance sensor (typically costing between $1.50 and $3.00 in 2026) requires a precise 10µs HIGH pulse on its Trigger pin to initiate a measurement.

const int TRIG_PIN = 10;
const int ECHO_PIN = 11;

void setup() {
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  Serial.begin(115200);
}

void loop() {
  // Clear the trigger pin
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  
  // Generate the 10us trigger pulse
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  
  // Read the echo pulse width
  long duration = pulseIn(ECHO_PIN, HIGH);
  float distance_cm = duration * 0.0343 / 2;
  
  Serial.print("Distance: ");
  Serial.println(distance_cm);
  delay(50);
}
Pro-Tip for HC-SR04 Users: The initial delayMicroseconds(2) while the pin is LOW is not optional. It ensures the internal logic of the HC-SR04's EM78P153N microcontroller has fully reset its state machine before receiving the new 10µs trigger command. Skipping this often results in phantom readings or sensor lockups.

Critical Limitations and Edge Cases

While delayMicroseconds() is powerful, it is not a silver bullet. Makers frequently encounter two major pitfalls when pushing the limits of this function.

The 16383 Microsecond Ceiling (AVR Boards)

If you consult the official Arduino reference documentation, you will find a strict warning: on 16 MHz AVR boards, the largest value that will produce an accurate delay is 16383 microseconds.

This limitation exists because the internal assembly loop uses a 16-bit integer counter, and the mathematical prescaler overflows beyond this threshold. If you need a delay of 20,000µs (20ms), you must either use the standard delay(20) function or chain multiple microsecond delays. Attempting to pass delayMicroseconds(20000) on an Arduino Uno will result in an unpredictable, truncated delay.

The Interrupt Trap

Unlike some custom assembly routines, the standard Arduino delayMicroseconds() function does not disable interrupts. This is generally a good thing, as it prevents the disruption of critical background tasks like Serial communication and the millis() timer (Timer0).

However, if an interrupt service routine (ISR) fires during your microsecond delay, the delay will be stretched by the execution time of the ISR. For example, the Timer0 overflow interrupt (which updates millis()) takes roughly 5µs to execute. If this fires during your 10µs HC-SR04 trigger pulse, your pulse becomes 15µs. While the HC-SR04 will tolerate this, a strict RF protocol might reject the data packet entirely.

The Solution: For ultra-critical timing windows (under 50µs), temporarily disable interrupts:

noInterrupts();
digitalWrite(PIN, HIGH);
delayMicroseconds(12);
digitalWrite(PIN, LOW);
interrupts();

Comparison Matrix: Timing Functions in the Arduino Ecosystem

Choosing the right timing mechanism is crucial for non-blocking, efficient firmware. Below is a comparison of the primary timing tools available.

Function / MethodResolutionBlocking?Max Reliable ValueBest Use Case
delay()1 msYes~49.7 daysSimple setups, human-scale pauses
delayMicroseconds()1 µsYes16383 µs (AVR)Sensor triggers, IR carriers
millis()1 msNo~49.7 daysState machines, multitasking
micros()4 µs (AVR) / 1 µs (ARM)No~70 minutesNon-blocking precision loops
Hardware Timers (e.g., Timer1)0.0625 µs (AVR)NoVaries by prescalerPWM generation, encoders

Advanced: Moving Beyond Software Delays

As projects scale in complexity, relying on the CPU to count microseconds via software loops becomes inefficient. In 2026, modern embedded design heavily favors hardware peripherals for microsecond timing.

AVR Hardware Timers

For legacy boards like the Uno R3, utilizing Timer1 allows you to generate exact microsecond pulses without CPU intervention. As detailed in Nick Gammon's authoritative guide on AVR timers, configuring the Output Compare Register (OCR1A) can toggle a pin at exact frequencies, freeing the main loop to handle sensor fusion or Wi-Fi logic.

ESP32 Remote Control (RMT) Peripheral

If you have migrated to the ESP32 ecosystem, using delayMicroseconds() for protocols like WS2812B addressable LEDs or IR transmission is considered bad practice due to RTOS jitter. Instead, the ESP32 features the Remote Control (RMT) peripheral. The RMT module operates entirely in hardware, pulling pulse-duration data from RAM and transmitting it with nanosecond precision, completely immune to Wi-Fi stack interrupts.

Summary Checklist for Makers

  • Always account for digitalWrite() overhead (approx. 0.25µs on AVR) when calculating tight timing loops.
  • Never pass a value greater than 16383 to delayMicroseconds() on 16MHz AVR boards.
  • Use noInterrupts() and interrupts() to protect critical sub-50µs timing windows from ISR jitter.
  • Transition to hardware timers (Timer1) or dedicated peripherals (ESP32 RMT) when generating continuous high-frequency signals.
  • Remember that micros() on a 16MHz Arduino Uno resolves in steps of 4µs, not 1µs, due to the Timer0 prescaler configuration.

By understanding the silicon-level mechanics of the Arduino delay microseconds function, you can eliminate timing drift, ensure reliable sensor communication, and write firmware that operates with professional-grade precision.