The Definitive Quick Reference for Timing with Arduino

Whether you are building a simple blinking LED circuit or a complex multi-sensor data logger, mastering timing with Arduino is the dividing line between amateur sketches and professional-grade firmware. In modern maker projects—especially those utilizing Wi-Fi-enabled boards like the ESP32 or high-speed sensor arrays on the ATmega4809—blocking code can cause buffer overflows, watchdog resets, and missed sensor readings.

This quick reference guide and FAQ cuts through the fluff, providing exact mathematical formulas, hardware register details, and architectural insights to help you manage time precisely on 8-bit and 32-bit microcontrollers.

Timing Methods Comparison Matrix

Before diving into the FAQs, review this quick-reference matrix to select the correct timing mechanism for your specific hardware architecture.

Method / Function Resolution (16MHz AVR) Blocking? Max Duration Best Use Case
delay() ~1ms Yes ~49.7 days Setup routines, simple hardware initialization
millis() 1ms No ~49.7 days State machines, multitasking, non-blocking UI
micros() 4µs No ~70 minutes PID control loops, high-speed encoder polling
Hardware Timers (CTC) Clock-dependent No (Interrupt) Infinite PWM generation, precise RTOS ticks, audio synthesis
RTOS vTaskDelay() 1ms (Tick rate) Yields Core Infinite ESP32 multi-core task management, Wi-Fi stack

Core FAQs: Software Timing Functions

1. Why is delay() considered bad practice in production firmware?

The delay() function halts the microcontroller's main execution loop. While the AVR architecture still processes background interrupts (like Serial RX buffering or Timer0 incrementing), your application logic is entirely paralyzed. In a 2026 IoT context, using delay() on an ESP8266 or ESP32 will starve the underlying Wi-Fi/Bluetooth stack, leading to WDT_RESET (Watchdog Timer) crashes or dropped network connections. Always reserve delay() strictly for initial hardware settling times (e.g., waiting 50ms for an I2C sensor to boot).

2. How do I correctly handle the 49.7-day millis() rollover?

The millis() function returns an unsigned long (32-bit integer), which maxes out at 4,294,967,295 milliseconds (roughly 49.7 days). When it overflows, it resets to zero. Many beginners write flawed logic that breaks upon rollover. The mathematically bulletproof method relies on unsigned integer underflow arithmetic.

The Golden Rule of millis(): Never add the interval to the current time. Always subtract the previous time from the current time.

Correct Implementation:

unsigned long previousMillis = 0;
const long interval = 1000;

void loop() {
  unsigned long currentMillis = millis();
  // This subtraction handles the rollover automatically
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    // Execute timed action here
  }
}

For a deeper dive into the binary arithmetic behind this, consult the Arduino Official Language Reference, which explicitly outlines the BlinkWithoutDelay paradigm.

3. What is the actual resolution of micros() on a standard 16MHz Arduino Uno?

Despite the name, micros() does not increment by exactly 1 microsecond on a 16MHz ATmega328P. Because of the clock cycles required to execute the timer overflow interrupt service routine (ISR), the resolution is exactly 4 microseconds. Therefore, micros() will return values like 4, 8, 12, 16, skipping the numbers in between. If your project requires true 1µs resolution (such as ultrasonic time-of-flight measurements), you must read the hardware timer registers directly or use a 32-bit board like the Arduino Due or ESP32, which offer finer timer granularity.

Advanced FAQs: Hardware Timers & Interrupts

4. How do I configure ATmega328P Timer1 for precise 1-second interrupts?

When software timing isn't accurate enough due to loop execution variance, you must use hardware timers. Timer1 is a 16-bit timer capable of Clear Timer on Compare (CTC) mode. To trigger an interrupt exactly every second on a 16MHz clock:

  1. Set the prescaler to 256 (yielding a 62,500 Hz timer clock).
  2. Set the Output Compare Register (OCR1A) to 62,499 (since it counts from zero).
  3. Enable the CTC mode bit and the Timer1 Compare Match A interrupt.
void setupTimer1() {
  noInterrupts();
  TCCR1A = 0; // Clear timer control registers
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A = 62499;            // (16,000,000 / (256 * 1)) - 1
  TCCR1B |= (1 << WGM12);   // Turn on CTC mode
  TCCR1B |= (1 << CS12);    // Set CS12 bit for 256 prescaler
  TIMSK1 |= (1 << OCIE1A);  // Enable timer compare interrupt
  interrupts();
}

ISR(TIMER1_COMPA_vect) {
  // Executes precisely every 1.000 seconds
}

Nick Gammon's extensive Microcontroller Timers Tutorial remains the industry-standard reference for mapping AVR prescalers and bitwise register operations.

5. How does timing differ on the ESP32 (FreeRTOS Architecture)?

The ESP32 operates on a dual-core FreeRTOS architecture. Using standard Arduino delay() or blocking millis() loops on the ESP32 is highly discouraged because it prevents the Idle Task from running, which is responsible for feeding the watchdog and managing background Wi-Fi/BLE cleanup.

  • Task Delay: Use vTaskDelay(pdMS_TO_TICKS(1000)); to yield the core to other RTOS tasks.
  • Hardware Timers: The ESP32 Arduino Core provides a dedicated Hardware Timer API. Use timerBegin(), timerAttachInterrupt(), and timerAlarm() instead of manipulating raw AVR registers. Refer to the Espressif ESP32 Arduino Core Timer API for the latest 3.x core syntax.

Troubleshooting Edge Cases & Pitfalls

6. Why does my I2C sensor freeze when I use hardware timer interrupts?

A frequent issue in advanced timing with Arduino involves the I2C (Wire) library hanging indefinitely. The Wire library relies heavily on background interrupts to clock in data from sensors. If your custom hardware timer ISR (Interrupt Service Routine) takes too long to execute, or if you inadvertently call noInterrupts() globally without restoring them, the I2C bus will lock up waiting for an interrupt that will never fire.

Solution: Keep ISRs under 5 microseconds. Never call Wire.requestFrom() or Serial.print() inside an ISR. Instead, set a volatile boolean flag inside the ISR, and handle the I2C polling inside the main loop() when the flag is true.

7. My millis() timing is drifting by 2% over 24 hours. Why?

Software timing is only as accurate as the physical crystal oscillator or ceramic resonator on your board. Standard Arduino Uno clones often use cheap ceramic resonators with a tolerance of ±0.5% to ±1.0%. Over 24 hours, a 1% drift equals roughly 14.4 minutes of lost time. Furthermore, ambient temperature fluctuations affect oscillator frequency.

Fixes for Precision Timing:

  • Upgrade to a board with a Temperature Compensated Crystal Oscillator (TCXO).
  • Implement an NTP (Network Time Protocol) sync routine if using an ESP32.
  • Add a dedicated DS3231 Real Time Clock (RTC) module, which features an internal TCXO accurate to ±2ppm (roughly 1 minute per year).

Summary Checklist for Firmware Developers

  • Never use delay() in the main loop of a network-connected or multi-sensor device.
  • Always use unsigned subtraction for millis() rollover safety.
  • Remember the 4µs hardware limitation of micros() on 16MHz AVR boards.
  • Keep ISRs brief to prevent I2C and Serial buffer lockups.
  • Use RTOS delays (vTaskDelay) when programming ESP32 or ARM Cortex-M boards.