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.
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
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.