Contact Sales & After-Sales Service

Contact & Quotation

  • Inquire: Call 0086-755-23203480, or reach out via the form below/your sales contact to discuss our design, manufacturing, and assembly capabilities.
  • Quote: Email your PCB files to Sales@pcbsync.com (Preferred for large files) or submit online. We will contact you promptly. Please ensure your email is correct.
Drag & Drop Files, Choose Files to Upload You can upload up to 3 files.

Notes:
For PCB fabrication, we require PCB design file in Gerber RS-274X format (most preferred), *.PCB/DDB (Protel, inform your program version) format or *.BRD (Eagle) format. For PCB assembly, we require PCB design file in above mentioned format, drilling file and BOM. Click to download BOM template To avoid file missing, please include all files into one folder and compress it into .zip or .rar format.

Arduino Timers: Precise Timing Without delay

After spending years debugging embedded systems, I’ve learned one crucial lesson: delay() is the enemy of responsive microcontroller programs. In my early days designing control systems, I watched countless projects grind to a halt because a single delay() call blocked everything else from running.

This guide on Arduino timers will show you how to write non-blocking code that handles multiple tasks simultaneously, responds instantly to user input, and maintains precise timing accuracy. Whether you’re building sensor networks, robotics controllers, or interactive displays, mastering Arduino timers is essential.

Why delay() Is Holding Your Projects Back

Before we dive into Arduino timers, let’s understand the problem. The delay() function completely halts your program execution. During a delay(1000) call, your Arduino can’t:

  • Read sensors
  • Check button states
  • Update displays
  • Respond to serial communication
  • Handle interrupts properly

In professional PCB designs, this blocking behavior is unacceptable. Imagine a motor controller that can’t read its limit switches because it’s stuck in a delay loop, or a data logger missing sensor readings because it’s waiting for an LED to blink.

The solution? Time-based programming using Arduino timers.

Understanding Arduino Timer Types

Arduino boards have multiple timing mechanisms available, each suited for different applications:

Timer TypeFunctionResolutionMax ValueUse Case
millis()Software timer1 millisecond49.7 daysGeneral timing, delays
micros()Software timer1 microsecond71.6 minutesPrecise short intervals
Hardware Timer 0Internal timer8-bit256 countsmillis(), delay()
Hardware Timer 1Internal timer16-bit65536 countsServo, interrupts
Hardware Timer 2Internal timer8-bit256 countsPWM, tone()

For most applications, millis() and micros() provide the flexibility we need. Hardware timers are reserved for interrupt-driven applications requiring microsecond precision.

The millis() Method: Your First Non-Blocking Timer

The millis() function returns the number of milliseconds since your Arduino started running. By storing previous time values and comparing them to current time, we create non-blocking delays.

Basic millis() Template

Here’s the fundamental pattern I use in virtually every project:

unsigned long previousMillis = 0;

const long interval = 1000;  // 1 second

void setup() {

  pinMode(LED_BUILTIN, OUTPUT);

}

void loop() {

  unsigned long currentMillis = millis();

  if (currentMillis – previousMillis >= interval) {

    previousMillis = currentMillis;

    // Your timed action here

    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));

  }

  // Other code runs freely here

}

This pattern lets your loop() run continuously, checking the timer and executing code only when the interval has elapsed.

Practical Arduino Timers Examples

Multiple Independent Timers

One of the most powerful aspects of Arduino timers is running multiple tasks independently:

// LED blink timer

unsigned long ledPreviousMillis = 0;

const long ledInterval = 500;

// Sensor read timer

unsigned long sensorPreviousMillis = 0;

const long sensorInterval = 2000;

// Display update timer

unsigned long displayPreviousMillis = 0;

const long displayInterval = 100;

void setup() {

  Serial.begin(9600);

  pinMode(LED_BUILTIN, OUTPUT);

}

void loop() {

  unsigned long currentMillis = millis();

  // LED task (every 500ms)

  if (currentMillis – ledPreviousMillis >= ledInterval) {

    ledPreviousMillis = currentMillis;

    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));

  }

  // Sensor task (every 2 seconds)

  if (currentMillis – sensorPreviousMillis >= sensorInterval) {

    sensorPreviousMillis = currentMillis;

    int sensorValue = analogRead(A0);

    Serial.print(“Sensor: “);

    Serial.println(sensorValue);

  }

  // Display task (every 100ms)

  if (currentMillis – displayPreviousMillis >= displayInterval) {

    displayPreviousMillis = currentMillis;

    // Update display code here

  }

}

This approach lets you manage multiple timing-critical tasks without any blocking. The Arduino continuously cycles through all checks, executing each when its time comes.

Button Debouncing with Arduino Timers

Physical buttons bounce—they create multiple signal transitions during a single press. Here’s a reliable debouncing technique using timers:

const int buttonPin = 2;

const int ledPin = 13;

int buttonState = HIGH;

int lastButtonState = HIGH;

unsigned long lastDebounceTime = 0;

const unsigned long debounceDelay = 50;

void setup() {

  pinMode(buttonPin, INPUT_PULLUP);

  pinMode(ledPin, OUTPUT);

}

void loop() {

  int reading = digitalRead(buttonPin);

  if (reading != lastButtonState) {

    lastDebounceTime = millis();

  }

  if ((millis() – lastDebounceTime) > debounceDelay) {

    if (reading != buttonState) {

      buttonState = reading;

      if (buttonState == LOW) {

        digitalWrite(ledPin, !digitalRead(ledPin));

      }

    }

  }

  lastButtonState = reading;

}

I’ve used this pattern in production control panels where button reliability is critical. The 50ms debounce delay filters out noise without making the interface feel sluggish.

State Machine with Timing

For complex sequences, combining Arduino timers with state machines creates predictable, maintainable code:

enum SystemState {

  IDLE,

  HEATING,

  WAITING,

  COOLING

};

SystemState currentState = IDLE;

unsigned long stateStartTime = 0;

const int heaterPin = 9;

void setup() {

  pinMode(heaterPin, OUTPUT);

  Serial.begin(9600);

}

void loop() {

  unsigned long currentMillis = millis();

  unsigned long elapsedTime = currentMillis – stateStartTime;

  switch(currentState) {

    case IDLE:

      digitalWrite(heaterPin, LOW);

      if (/* start condition */) {

        currentState = HEATING;

        stateStartTime = currentMillis;

        Serial.println(“Starting heating cycle”);

      }

      break;

    case HEATING:

      digitalWrite(heaterPin, HIGH);

      if (elapsedTime >= 5000) {  // Heat for 5 seconds

        currentState = WAITING;

        stateStartTime = currentMillis;

        Serial.println(“Heating complete, waiting”);

      }

      break;

    case WAITING:

      digitalWrite(heaterPin, LOW);

      if (elapsedTime >= 3000) {  // Wait 3 seconds

        currentState = COOLING;

        stateStartTime = currentMillis;

        Serial.println(“Starting cooling”);

      }

      break;

    case COOLING:

      // Cooling code here

      if (elapsedTime >= 10000) {  // Cool for 10 seconds

        currentState = IDLE;

        Serial.println(“Cycle complete”);

      }

      break;

  }

}

This pattern is invaluable for automation projects, manufacturing equipment, or any system requiring timed sequences.

Handling millis() Overflow

The millis() counter overflows after approximately 49.7 days, resetting to zero. Surprisingly, the standard subtraction method handles this automatically due to unsigned integer arithmetic:

unsigned long previousMillis = 4294967000;  // Near overflow

unsigned long currentMillis = 1000;          // After overflow

unsigned long elapsed = currentMillis – previousMillis;

// elapsed correctly calculates to ~1296 milliseconds

However, if you need to store absolute time values for comparison, handle overflow explicitly:

bool isTimeoutExpired(unsigned long startTime, unsigned long timeout) {

  return (millis() – startTime) >= timeout;

}

This function works correctly across overflow boundaries.

Hardware Timer Interrupts for Precise Timing

When you need microsecond precision or guaranteed execution timing, hardware timer interrupts are the solution. These interrupt your main program at exact intervals.

Timer1 Interrupt Example

Timer1 on Arduino Uno is a 16-bit timer perfect for precise timing:

#include <TimerOne.h>

volatile bool toggleFlag = false;

const int ledPin = 13;

void setup() {

  pinMode(ledPin, OUTPUT);

  Timer1.initialize(1000000);  // 1 second = 1,000,000 microseconds

  Timer1.attachInterrupt(timerISR);

}

void timerISR() {

  toggleFlag = true;

}

void loop() {

  if (toggleFlag) {

    toggleFlag = false;

    digitalWrite(ledPin, !digitalRead(ledPin));

  }

  // Other non-time-critical code runs here

}

Critical interrupt rules from my experience:

  • Keep ISR (Interrupt Service Routine) functions extremely short
  • Don’t use delay(), Serial.print(), or complex calculations in ISRs
  • Use volatile keyword for variables shared between ISR and main code
  • Set flags in ISR, handle actions in loop()

Comparison: Software vs Hardware Timers

Featuremillis()/micros()Hardware Timer Interrupts
Precision±1ms or ±4μsSub-microsecond
JitterDepends on loop speedMinimal
CPU UsageNegligibleMinimal
ComplexitySimpleModerate
Multiple instancesUnlimitedLimited by hardware
Best forGeneral timingCritical timing

For PCB designs with tight timing requirements (communication protocols, sensor sampling, waveform generation), hardware timers are essential.

Advanced Timer Patterns

Elapsed Time Tracking

Sometimes you need to track how long something has been running:

class ElapsedTime {

  private:

    unsigned long startTime;

    bool running;

  public:

    ElapsedTime() : startTime(0), running(false) {}

    void start() {

      startTime = millis();

      running = true;

    }

    void stop() {

      running = false;

    }

    unsigned long elapsed() {

      if (running) {

        return millis() – startTime;

      }

      return 0;

    }

    bool isRunning() {

      return running;

    }

};

ElapsedTime processTimer;

void setup() {

  Serial.begin(9600);

  processTimer.start();

}

void loop() {

  if (processTimer.elapsed() > 5000) {

    Serial.println(“Process took more than 5 seconds”);

    processTimer.stop();

  }

}

Rate Limiting

Limiting how often a function executes prevents system overload:

class RateLimiter {

  private:

    unsigned long lastExecution;

    unsigned long minInterval;

  public:

    RateLimiter(unsigned long interval) : lastExecution(0), minInterval(interval) {}

    bool canExecute() {

      unsigned long now = millis();

      if (now – lastExecution >= minInterval) {

        lastExecution = now;

        return true;

      }

      return false;

    }

};

RateLimiter serialLimiter(1000);  // Max once per second

void loop() {

  int sensorValue = analogRead(A0);

  if (serialLimiter.canExecute()) {

    Serial.println(sensorValue);

  }

  // Sensor read continuously, serial output limited

}

This pattern protects communication buses from flooding and prevents overwhelming slower peripherals.

Common Arduino Timers Pitfalls and Solutions

Problem 1: Timer Drift in Long-Running Applications

Issue: Using previousMillis = millis() inside the if statement causes drift.

// WRONG – Accumulates drift

if (currentMillis – previousMillis >= interval) {

  previousMillis = millis();  // Could be microseconds after the check

  // Do work

}

// CORRECT – No drift accumulation

if (currentMillis – previousMillis >= interval) {

  previousMillis = currentMillis;  // Use the checked value

  // Do work

}

In a PCB design I worked on for industrial logging, this drift caused data timestamps to shift by seconds over weeks of operation.

Problem 2: Integer Overflow in Calculations

// DANGEROUS – May overflow before comparison

unsigned long timeout = 50 * 60 * 1000;  // 50 minutes in milliseconds

// SAFE – Use UL suffix for long constants

unsigned long timeout = 50UL * 60UL * 1000UL;

Problem 3: Blocking Code Mixed with Timers

Even with Arduino timers, adding delay() anywhere blocks the entire system:

// BAD – delay() negates timer benefits

void loop() {

  if (currentMillis – previousMillis >= interval) {

    previousMillis = currentMillis;

    digitalWrite(ledPin, HIGH);

    delay(100);  // BLOCKS EVERYTHING

    digitalWrite(ledPin, LOW);

  }

}

// GOOD – Use additional timer for LED duration

void loop() {

  if (currentMillis – ledPreviousMillis >= ledInterval) {

    ledPreviousMillis = currentMillis;

    digitalWrite(ledPin, HIGH);

    ledOnTime = currentMillis;

    ledIsOn = true;

  }

  if (ledIsOn && (currentMillis – ledOnTime >= 100)) {

    digitalWrite(ledPin, LOW);

    ledIsOn = false;

  }

}

Real-World Project: Multi-Sensor Data Logger

Here’s a complete example combining multiple Arduino timers concepts:

#include <SD.h>

#include <Wire.h>

// Timer variables

unsigned long sensorReadTime = 0;

unsigned long sdWriteTime = 0;

unsigned long statusBlinkTime = 0;

unsigned long serialOutputTime = 0;

// Intervals

const unsigned long SENSOR_INTERVAL = 100;    // Read every 100ms

const unsigned long SD_INTERVAL = 1000;        // Write every 1 second

const unsigned long BLINK_INTERVAL = 500;      // Status LED

const unsigned long SERIAL_INTERVAL = 5000;    // Serial output every 5s

// Data buffer

const int BUFFER_SIZE = 10;

int sensorBuffer[BUFFER_SIZE];

int bufferIndex = 0;

void setup() {

  Serial.begin(115200);

  pinMode(LED_BUILTIN, OUTPUT);

  if (!SD.begin(10)) {

    Serial.println(“SD initialization failed!”);

    while(1);

  }

}

void loop() {

  unsigned long currentMillis = millis();

  // Sensor reading task

  if (currentMillis – sensorReadTime >= SENSOR_INTERVAL) {

    sensorReadTime = currentMillis;

    int value = analogRead(A0);

    sensorBuffer[bufferIndex++] = value;

    if (bufferIndex >= BUFFER_SIZE) {

      bufferIndex = 0;

    }

  }

  // SD card writing task

  if (currentMillis – sdWriteTime >= SD_INTERVAL) {

    sdWriteTime = currentMillis;

    File dataFile = SD.open(“datalog.txt”, FILE_WRITE);

    if (dataFile) {

      dataFile.print(currentMillis);

      dataFile.print(“,”);

      // Write buffer average

      long sum = 0;

      for (int i = 0; i < BUFFER_SIZE; i++) {

        sum += sensorBuffer[i];

      }

      dataFile.println(sum / BUFFER_SIZE);

      dataFile.close();

    }

  }

  // Status LED blink

  if (currentMillis – statusBlinkTime >= BLINK_INTERVAL) {

    statusBlinkTime = currentMillis;

    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));

  }

  // Serial output for monitoring

  if (currentMillis – serialOutputTime >= SERIAL_INTERVAL) {

    serialOutputTime = currentMillis;

    Serial.print(“Logging… Buffer: “);

    Serial.println(bufferIndex);

  }

}

This data logger performs four independent tasks with different timing requirements, something impossible with delay()-based code.

Popular Arduino Timers Libraries

While the millis() approach works excellently, libraries can simplify complex timing scenarios:

SimpleTimer Library

#include <SimpleTimer.h>

SimpleTimer timer;

void setup() {

  Serial.begin(9600);

  timer.setInterval(1000, sendData);      // Every 1 second

  timer.setInterval(5000, checkSensor);   // Every 5 seconds

  timer.setTimeout(30000, shutdownMode);  // Once after 30 seconds

}

void loop() {

  timer.run();

}

void sendData() {

  Serial.println(“Sending data…”);

}

void checkSensor() {

  Serial.println(“Checking sensor…”);

}

void shutdownMode() {

  Serial.println(“Entering low power mode”);

}

Metro Library (Adafruit)

#include <Metro.h>

Metro ledMetro = Metro(500);

Metro sensorMetro = Metro(2000);

void loop() {

  if (ledMetro.check()) {

    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));

  }

  if (sensorMetro.check()) {

    int value = analogRead(A0);

    Serial.println(value);

  }

}

Timer Comparison Table

LibraryProsConsBest For
millis() (native)No dependencies, lightweightManual managementProduction code
SimpleTimerEasy multiple timersBlocking callbacksPrototypes
MetroSimple API, Adafruit supportLimited featuresSimple projects
TimerOne/TimerThreeHardware precisionComplex setupCritical timing
TaskSchedulerFull RTOS-lite featuresOverheadComplex systems

Useful Resources and Downloads

Official Documentation:

  • Arduino Time Functions Reference: https://www.arduino.cc/reference/en/language/functions/time/millis/
  • ATmega328P Timer Documentation: Available in Microchip’s datasheet

Essential Libraries:

  • SimpleTimer: Available in Arduino Library Manager
  • Metro Library: Adafruit’s GitHub repository
  • TimerOne: Arduino Library Manager or https://github.com/PaulStoffregen/TimerOne
  • TaskScheduler: https://github.com/arkhipenko/TaskScheduler

Development Tools:

  • Arduino IDE: Free download from arduino.cc
  • PlatformIO: Advanced IDE with library management
  • Logic Analyzer: For debugging timing issues in hardware

Learning Resources:

  • Blink Without Delay Tutorial: arduino.cc official tutorial
  • Gammon Forum Timer Tutorials: Detailed hardware timer explanations
  • Nick Gammon’s Interrupts Tutorial: Comprehensive ISR guide

Code Examples Repository:

  • Arduino Playground: playground.arduino.cc
  • GitHub Arduino Tag: Search “arduino timers examples”

Hardware Tools:

  • Oscilloscope: Essential for verifying timer accuracy
  • Logic Analyzer: Multi-channel timing visualization
  • Multimeter with Frequency Counter: Quick timing verification

Debugging Arduino Timers Issues

Verify Timing Accuracy

void setup() {

  Serial.begin(115200);

}

void loop() {

  static unsigned long lastReport = 0;

  static unsigned long count = 0;

  count++;

  if (millis() – lastReport >= 1000) {

    Serial.print(“Loop iterations per second: “);

    Serial.println(count);

    count = 0;

    lastReport = millis();

  }

}

This diagnostic shows your effective loop speed, crucial for understanding if your code can meet timing requirements.

Timer Monitoring Class

class TimerMonitor {

  private:

    unsigned long lastCheck;

    unsigned long interval;

    String name;

  public:

    TimerMonitor(String n, unsigned long i) : name(n), interval(i), lastCheck(0) {}

    void check() {

      unsigned long now = millis();

      unsigned long actualInterval = now – lastCheck;

      if (abs((long)(actualInterval – interval)) > 10) {

        Serial.print(name);

        Serial.print(” timing drift: “);

        Serial.print(actualInterval);

        Serial.print(“ms (expected “);

        Serial.print(interval);

        Serial.println(“ms)”);

      }

      lastCheck = now;

    }

};

Frequently Asked Questions

Q: How accurate are Arduino timers compared to Real-Time Clocks (RTCs)?

A: Arduino timers using millis() are reasonably accurate for short to medium durations but drift over time. The internal oscillator typically has ±10% tolerance without calibration. For my embedded projects requiring timekeeping over hours or days, I always add an external RTC module (DS3231 or DS1307). These maintain accuracy within seconds per month. Use millis() for intervals and control timing; use RTCs for actual timekeeping and timestamps.

Q: Can I use Arduino timers in interrupt service routines?

A: Yes, but with critical limitations. You can read millis() and micros() in ISRs, but the values only update while interrupts are enabled. Never call delay() in an ISR—it won’t work and will hang your program. I learned this debugging a motor controller where the ISR deadlocked. Keep ISRs extremely short (microseconds), set flags, and handle processing in the main loop. For hardware timer ISRs, you’re already in interrupt context, so standard timer rules apply.

Q: What happens to Arduino timers during sleep modes?

A: Timer behavior depends on sleep mode depth. In idle mode, timers continue running normally. In power-down mode, Timer2 continues with an external crystal, but Timer0 and Timer1 stop, meaning millis() stops updating. When I design low-power sensor nodes, I use watchdog timer interrupts or external RTCs for wake-up timing. The ATmega328P datasheet details which peripherals remain active in each sleep mode—essential reading for battery-powered projects.

Q: How do I synchronize multiple Arduinos’ timers?

A: Synchronizing independent Arduino timers requires external timing signals. I’ve implemented this using GPS modules (1PPS signals), radio time broadcasts, or wired synchronization pulses. For looser coordination (within seconds), send timestamp messages over Serial, I2C, or wireless. One Arduino acts as master, broadcasting its millis() value periodically. Slaves offset their local timers accordingly. True microsecond synchronization needs hardware support—GPS is most practical for distributed systems.

Q: Why does my timer code work in testing but fail in production?

A: The most common cause is loop blocking you didn’t notice during testing. Adding Serial.println() for debugging, waiting for sensor responses, or processing complex calculations can delay your loop execution enough that timer checks miss their intervals. I always measure actual loop execution time in production conditions. Another culprit: timer overflow edge cases that only appear after 49 days. Test overflow scenarios explicitly by setting millis() near overflow in your test code (though this requires hardware access or simulation).

Conclusion

Mastering Arduino timers transforms how you write embedded code. The journey from blocking delay() calls to elegant, non-blocking timer-based programs represents a fundamental shift in thinking about microcontroller programming.

Throughout my PCB design career, implementing proper Arduino timers has solved countless problems: motors that respond instantly to limit switches, sensors that log data without missing readings, and user interfaces that never feel sluggish or unresponsive.

Start with the basic millis() pattern for simple projects. As your requirements grow, add multiple independent timers, implement state machines, and explore hardware timer interrupts for precision timing. Remember these key principles:

  • Never use delay() in production code
  • Store previousMillis using the checked currentMillis value to avoid drift
  • Keep ISRs short and use flags for complex processing
  • Test timer overflow scenarios for long-running applications
  • Use appropriate timer resolution (millis vs micros) for your requirements

Whether you’re building data loggers, control systems, or interactive displays, non-blocking Arduino timers are essential tools in your embedded programming toolkit. The patterns shown here scale from simple LED blinkers to complex industrial automation systems.

Now go eliminate those delay() calls and build truly responsive Arduino projects!

Leave a Reply

Your email address will not be published. Required fields are marked *

Contact Sales & After-Sales Service

Contact & Quotation

  • Inquire: Call 0086-755-23203480, or reach out via the form below/your sales contact to discuss our design, manufacturing, and assembly capabilities.

  • Quote: Email your PCB files to Sales@pcbsync.com (Preferred for large files) or submit online. We will contact you promptly. Please ensure your email is correct.

Drag & Drop Files, Choose Files to Upload You can upload up to 3 files.

Notes:
For PCB fabrication, we require PCB design file in Gerber RS-274X format (most preferred), *.PCB/DDB (Protel, inform your program version) format or *.BRD (Eagle) format. For PCB assembly, we require PCB design file in above mentioned format, drilling file and BOM. Click to download BOM template To avoid file missing, please include all files into one folder and compress it into .zip or .rar format.