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.
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.
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 Type
Function
Resolution
Max Value
Use Case
millis()
Software timer
1 millisecond
49.7 days
General timing, delays
micros()
Software timer
1 microsecond
71.6 minutes
Precise short intervals
Hardware Timer 0
Internal timer
8-bit
256 counts
millis(), delay()
Hardware Timer 1
Internal timer
16-bit
65536 counts
Servo, interrupts
Hardware Timer 2
Internal timer
8-bit
256 counts
PWM, 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:
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
Feature
millis()/micros()
Hardware Timer Interrupts
Precision
±1ms or ±4μs
Sub-microsecond
Jitter
Depends on loop speed
Minimal
CPU Usage
Negligible
Minimal
Complexity
Simple
Moderate
Multiple instances
Unlimited
Limited by hardware
Best for
General timing
Critical 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) {
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!
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.
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.