If you are searching for a delay_us() function in the Arduino IDE, you have likely hit a wall: the standard Arduino API does not have a function by that exact name. Instead, it uses delayMicroseconds(). When makers copy-paste raw AVR-GCC or STM32 C code into an Arduino sketch, the compiler immediately throws an undeclared scope error. Furthermore, when you actually need sub-microsecond precision for protocols like WS2812B LEDs or high-speed IR, understanding the difference between runtime API calls and compile-time hardware delays is the difference between a working project and a jittery mess.

This guide breaks down exactly how microsecond timing works across different microcontroller cores, provides a complete ultrasonic timing build, and debugs the most common compilation errors you will face.

Microsecond Delay Functions Compared

Not all microsecond delays are created equal. The function you should use depends entirely on your target architecture and whether your delay value is known at compile-time or calculated at runtime. Here is the definitive spec-sheet comparison for the most common cores used in the Arduino ecosystem.

Table 1: Microsecond Delay Methods by Architecture
Function Core / Architecture Max Delay Limit Interrupt Behavior Resolution & Overhead
delayMicroseconds() Arduino API (AVR, ESP32, STM32) 16,383 µs (AVR) Interrupts remain enabled (AVR) ~1 µs resolution. High function-call overhead.
_delay_us() AVR-GCC (<util/delay.h>) 768 µs (at 16 MHz) Interrupts remain enabled (hardware loop) Cycle-accurate. Requires compile-time constant.
ets_delay_us() ESP8266 / ESP32 (ROM/Legacy) Hardware dependent Watchdog timer may trigger if >10ms Very precise, but blocks WiFi/BT tasks.
delay_ns() Teensy (ARM Cortex-M4/M7) ~1000 ns per call Interrupts enabled Nanosecond scale. Inline assembly optimized.
Bench Tip: On a standard 16 MHz Arduino Nano, one clock cycle is 62.5 nanoseconds. If you use delayMicroseconds(1), the actual delay is often closer to 2.5 µs due to the C++ function call overhead (pushing registers to the stack, jumping to the address, and returning). For true 1 µs pulses, use direct port manipulation or _delay_us(1).

Project Build: Precision Ultrasonic Pulse Timing

To see microsecond timing in action, we will build a precision distance sensor using an HC-SR04 ultrasonic module. While most tutorials use the blocking pulseIn() function, we will write a manual timing loop using micros() and delayMicroseconds() to demonstrate exact control over the trigger pulse and echo measurement.

Parts List & Board Variant

  • Microcontroller: Arduino Nano V3 (ATmega328P, 16 MHz, 5V logic)
  • Sensor: HC-SR04 Ultrasonic Distance Sensor (5V tolerant)
  • Wiring: 22 AWG solid core jumper wires, half-size breadboard
  • Note: If you adapt this to an ESP32 (3.3V logic), you must add a 10kΩ/20kΩ voltage divider on the Echo pin to prevent frying the GPIO.

Pin Mapping Table

Arduino Nano V3 Pin HC-SR04 Pin Wire Color (Standard) Notes
5V VCC Red Sensor requires ~15mA peak during ping
GND GND Black Ensure common ground with Nano
D9 (PWM) Trig Yellow Output: 10 µs trigger pulse
D10 Echo Blue Input: Reads 5V HIGH pulse width

Complete Compilable Code

This code targets the Arduino Nano V3 (ATmega328P). It includes explicit timeout error handling to prevent the microcontroller from hanging indefinitely if the ultrasonic sensor fails to return an echo.

// Target: Arduino Nano V3 (ATmega328P, 16MHz)
// Ultrasonic Precision Timing with Manual Microsecond Tracking

#define TRIG_PIN 9
#define ECHO_PIN 10
#define TIMEOUT_US 30000 // ~5 meters max range limit

void setup() {
  Serial.begin(115200);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  
  // Ensure trigger line starts low
  digitalWrite(TRIG_PIN, LOW);
}

void loop() {
  // 1. Send exact 10us trigger pulse using delayMicroseconds
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10); 
  digitalWrite(TRIG_PIN, LOW);

  // 2. Measure echo pulse width manually to show microsecond tracking
  unsigned long start_time = 0;
  unsigned long end_time = 0;
  unsigned long timeout_limit = micros() + TIMEOUT_US;

  // Wait for echo to go HIGH (with timeout error handling)
  while (digitalRead(ECHO_PIN) == LOW) {
    if (micros() > timeout_limit) {
      Serial.println("Error: Timeout waiting for echo start.");
      return; // Exit loop() early to prevent hang
    }
  }
  start_time = micros(); // Capture exact rising edge time

  // Wait for echo to go LOW (with timeout error handling)
  while (digitalRead(ECHO_PIN) == HIGH) {
    if (micros() > timeout_limit) {
      Serial.println("Error: Timeout waiting for echo end.");
      return;
    }
  }
  end_time = micros(); // Capture exact falling edge time

  // Calculate duration and distance
  unsigned long duration = end_time - start_time;
  // Speed of sound is ~343 m/s, or 0.0343 cm/us. Divide by 2 for round trip.
  float distance_cm = (duration * 0.0343) / 2.0;

  Serial.print("Pulse Duration: ");
  Serial.print(duration);
  Serial.print(" us | Distance: ");
  Serial.print(distance_cm);
  Serial.println(" cm");

  delay(100); // 10Hz refresh rate
}

Debugging: Fixing the 'delay_us' Not Declared Error

If you are porting a library from raw C or an older Atmel Studio project into the Arduino IDE, you will almost certainly encounter this exact compilation failure:

error: 'delay_us' was not declared in this scope

Here are the first three things to check when this fails, ranked from most likely to least likely:

  1. Missing the AVR Utility Header: The Arduino IDE does not automatically include the raw AVR-GCC delay library. You must add #include <util/delay.h> at the very top of your sketch, before setup().
  2. Missing the Leading Underscore: The actual function name in the AVR Libc documentation is _delay_us() (with a leading underscore). Standard Arduino uses delayMicroseconds() (no underscore, camelCase). Check your spelling.
  3. Passing a Runtime Variable: If you fix the spelling and include the header, but pass a variable like _delay_us(my_variable), the compiler will throw a warning or bloat your hex file. _delay_us() relies on inline assembly loops that require a compile-time constant. If you need a variable delay, you must switch back to the Arduino API's delayMicroseconds(my_variable).
ESP32 / STM32 Variant Note: If you are compiling for an ESP32 and get this error, <util/delay.h> does not exist because it is an AVR-specific library. For ESP32, use the Arduino wrapper delayMicroseconds() or the ESP-IDF ROM function ets_delay_us() (found in rom/ets_sys.h).

Hardware Limits: Why Your Microsecond Delay is Actually Longer

A common point of frustration on the workbench is generating a protocol-specific pulse (like the 800ns low / 400ns high required for WS2812B Neopixels) using delayMicroseconds(), only to find the LEDs are flickering or showing the wrong colors. This happens because of function call overhead and interrupt jitter.

When you call delayMicroseconds(1) on a 16 MHz AVR, the microcontroller must:

  1. Push the return address to the stack (2 cycles).
  2. Jump to the function memory address (3 cycles).
  3. Execute the delay loop (approx. 10 cycles for 1µs).
  4. Return from the function (4 cycles).

By the time the function returns, 19 clock cycles have passed. At 62.5ns per cycle, your "1 microsecond" delay actually took ~1.18 microseconds. Furthermore, if a Timer0 interrupt fires during your delay (which Arduino uses for millis()), the Interrupt Service Routine (ISR) will pause your code for another 5 to 10 microseconds, completely destroying your timing window.

For strict timing protocols, you must disable interrupts using noInterrupts() before the pulse sequence, and re-enable them with interrupts() immediately after. Alternatively, use hardware-driven peripherals like the SPI bus or the Timer1 Input Capture Unit (ICP1) to handle microsecond tracking entirely in hardware.

Extending and Simplifying the Build

Depending on your project requirements, you will eventually need to either push the precision of this build further or abandon software timing altogether.

How to Extend for Higher Precision

If you are building a high-precision anemometer or a time-of-flight LiDAR receiver, software polling loops (like the while(digitalRead() == HIGH) used above) are too slow. digitalRead() takes roughly 3.5 µs to execute on an AVR. To extend this build for sub-microsecond accuracy:

  • Use Direct Port Manipulation: Replace digitalRead(ECHO_PIN) with PINB & (1 << PB2) (assuming D10 maps to PB2 on the Nano). This reads the pin state in a single clock cycle (62.5ns).
  • Implement Hardware Input Capture: Configure Timer1 to route the Echo pin into the ICP1 (Input Capture Pin) hardware register. The microcontroller will automatically timestamp the exact clock cycle the pin changed state, completely independent of code execution or interrupt latency.

How to Simplify the Build

If you are struggling with jitter, compilation errors, and timing math, the best engineering decision is often to remove the problem entirely. If your goal is simply to measure distance or detect proximity:

  • Switch to I2C Time-of-Flight: Replace the HC-SR04 and manual microsecond tracking with a VL53L0X or VL53L1X I2C Time-of-Flight sensor. These modules contain an internal laser, SPAD array, and dedicated timing microcontroller. You simply request the distance over I2C, completely bypassing the need for delay_us or micros() in your main sketch.
  • Use Dedicated Libraries: If you must use the HC-SR04, rely on the NewPing library. It handles the timer interrupts and timeout edge cases natively, preventing the lockups that plague manual while() loop implementations.