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.
| 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. |
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:
- 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, beforesetup(). - Missing the Leading Underscore: The actual function name in the AVR Libc documentation is
_delay_us()(with a leading underscore). Standard Arduino usesdelayMicroseconds()(no underscore, camelCase). Check your spelling. - 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'sdelayMicroseconds(my_variable).
<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:
- Push the return address to the stack (2 cycles).
- Jump to the function memory address (3 cycles).
- Execute the delay loop (approx. 10 cycles for 1µs).
- 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)withPINB & (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_usormicros()in your main sketch. - Use Dedicated Libraries: If you must use the HC-SR04, rely on the
NewPinglibrary. It handles the timer interrupts and timeout edge cases natively, preventing the lockups that plague manualwhile()loop implementations.






