Time management is crucial to many Arduino projects, whether you need to schedule events, measure time intervals, or keep track of real-world time. In Arduino, there are two primary methods for handling time:
Many Arduino applications require precise timing to function correctly. Whether blinking an LED at intervals, creating a real-time clock, or logging sensor data, effectively managing time ensures that tasks run smoothly without unnecessary delays or errors.
Relative Time (millis()): Measures the elapsed time since the Arduino was powered on. This is useful for scheduling tasks but does not retain time after a reset.
Absolute Time (RTC): Uses a Real-Time Clock module to track actual date and time, even when the Arduino is powered off. RTC is ideal for applications that require timekeeping accuracy over long periods.
millis() Use Cases:
Blinking an LED without blocking other processes
Implementing a simple timer for an event
Managing sensor polling intervals
RTC Use Cases:
Data logging with timestamps
Alarm systems that trigger at specific times
Building a digital clock or calendar
This guide will cover both methods, explain when to use each, and provide practical examples.
When working with Arduino, timing functions play a crucial role in controlling tasks efficiently. One of the most important built-in functions for handling time-based operations is millis(). This function returns the number of milliseconds that have elapsed since the Arduino board was powered on or reset. Unlike delay(), millis() does not stop the processor, allowing multiple tasks to run simultaneously. This is critical for applications requiring precise timing, multitasking, or event-driven programming.
Before diving into advanced uses, let’s start by simply printing the millis() value to the Serial Monitor. This helps us understand how millis() continuously increases over time:
void setup() {
Serial.begin(9600); // Initialize Serial Communication
}
void loop() {
Serial.println(millis()); // Print the elapsed time in milliseconds
delay(1000); // Wait for 1 second
}
This code outputs the elapsed time in milliseconds every second, demonstrating how millis() acts as a continuously running timer since the board was powered on.
Arduino provides an example called "Blink Without Delay" that demonstrates using millis() instead of delay(). Here’s the standard example:
const int ledPin = 13; // LED pin
unsigned long previousMillis = 0; // Store last update time
const long interval = 1000; // Blink interval (1 second)
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis; // Save the last time the LED was toggled
digitalWrite(ledPin, !digitalRead(ledPin)); // Toggle LED
}
}
Using delay() halts program execution, making multitasking impossible. For example, calling delay(1000); creates a 1-second pause where nothing else can execute. By using millis(), we can check elapsed time without blocking other code, allowing multiple tasks to run independently.
unsigned long Variables: The millis() function returns an unsigned long value, which can store large numbers (up to ~50 days before overflowing).
Time Checking Condition: if (currentMillis - previousMillis >= interval) ensures the LED toggles at the desired interval without stopping the loop.
Updating previousMillis: This keeps track of when the last toggle happened, preventing repeated execution within the interval.
Non-Blocking Nature: The key advantage is that millis() allows other operations to continue running while time elapses.
By tracking multiple timestamps, millis() enables multitasking on Arduino. Here’s an example where an LED blinks while a buzzer sounds at different intervals:
const int ledPin = 13;
const int buzzerPin = 12;
unsigned long previousLedMillis = 0;
unsigned long previousBuzzerMillis = 0;
const long ledInterval = 1000; // LED blinks every 1s
const long buzzerInterval = 500; // Buzzer beeps every 500ms
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buzzerPin, OUTPUT);
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousLedMillis >= ledInterval) {
previousLedMillis = currentMillis;
digitalWrite(ledPin, !digitalRead(ledPin));
}
if (currentMillis - previousBuzzerMillis >= buzzerInterval) {
previousBuzzerMillis = currentMillis;
digitalWrite(buzzerPin, !digitalRead(buzzerPin));
}
}
This demonstrates how millis() allows multiple tasks (blinking an LED and toggling a buzzer) to run independently. Unlike delay(), which forces one task to finish before another starts, millis() makes non-blocking multitasking possible.
Apart from multitasking, millis() can be used to create absolute timers that trigger events at specific moments without the need for an RTC (Real-Time Clock). This is useful for time-based actions such as data logging, scheduled notifications, or sensor readings.
unsigned long eventStartTime;
const long eventDuration = 10 * 60 * 1000; // 10 minutes
void setup() {
Serial.begin(9600);
eventStartTime = millis(); // Set start time
}
void loop() {
if (millis() - eventStartTime >= eventDuration) {
Serial.println("10 minutes have passed!");
eventStartTime = millis(); // Reset timer
}
}
This technique is helpful when precise timing is needed, such as periodically saving data to EEPROM, turning on a fan for a set duration, or triggering sensor readings at regular intervals.
We can further expand on this concept by defining multiple absolute timers with different durations:
unsigned long event1Start;
unsigned long event2Start;
const long event1Duration = 5000; // 5 seconds
const long event2Duration = 20000; // 20 seconds
void setup() {
Serial.begin(9600);
event1Start = millis();
event2Start = millis();
}
void loop() {
if (millis() - event1Start >= event1Duration) {
Serial.println("Event 1 triggered!");
event1Start = millis();
}
if (millis() - event2Start >= event2Duration) {
Serial.println("Event 2 triggered!");
event2Start = millis();
}
}
This approach allows multiple timed events to execute independently of one another, providing flexibility without needing an RTC module.
millis() Returns Elapsed Time: It continuously counts the milliseconds since the board started.
Replaces delay() for Non-Blocking Code: Allows multiple tasks to run simultaneously.
Used for Multitasking: Multiple timers can be implemented with independent intervals.
Can Create Absolute Timers: Useful for periodic event triggers without RTCs.
Prevents Freezing in Loops: Unlike delay(), which halts execution, millis() lets other code continue running.
Watch Out for Overflow: millis() overflows every ~50 days, but using unsigned long arithmetic ensures it still works correctly.
Real-Time Clock (RTC) modules are essential for keeping accurate time in Arduino projects. Unlike the millis() function, which resets upon power loss or reset, RTC modules retain time even when the Arduino is powered off, thanks to an onboard battery. This makes them ideal for data logging, scheduling events, and real-time automation.
While millis() is excellent for short-term timing and multitasking within a program, it is not reliable for long-term timekeeping because:
millis() resets every time the board powers off or resets.
It overflows approximately every 50 days.
It does not maintain real-world time (e.g., date and clock adjustments).
Use an RTC module when:
You need to keep track of real-world time, even after power loss.
You require precise time-based event scheduling over days, weeks, or months.
Your project involves logging timestamps for sensor data, automation, or scheduling tasks beyond a single runtime session.
If your task only requires interval-based events within a single runtime session, millis() may be a simpler solution.
To use an RTC module like the DS3231 or DS1307, connect it to your Arduino as follows:
To communicate with an RTC module, you need to install an appropriate library. For example, for the DS3231 module:
Open the Arduino IDE.
Go to Sketch > Include Library > Manage Libraries.
Search for RTClib and install it.
Search for DS3231 and install it.
After wiring and installing the library, upload the following code to read the current date and time:
#include <Wire.h>
#include <RTClib.h>
RTC_DS3231 rtc;
void setup() {
Serial.begin(9600);
if (!rtc.begin()) {
Serial.println("Couldn't find RTC");
while (1);
}
if (rtc.lostPower()) {
Serial.println("RTC lost power, setting default time...");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
void loop() {
DateTime now = rtc.now();
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(" ");
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.println(now.second(), DEC);
delay(1000);
}
#include <RTClib.h>
RTC_DS3231 rtc;
#define ALARM_HOUR 12 // Set alarm hour
#define ALARM_MIN 30 // Set alarm minute
#define ALARM_SEC 0 // Set alarm second
void setup() {
Serial.begin(9600);
if (!rtc.begin()) {
Serial.println("RTC not found");
while (true);
}
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
void loop() {
DateTime now = rtc.now();
// Check if the current time matches the alarm time
if (now.hour() == ALARM_HOUR && now.minute() == ALARM_MIN && now.second() == ALARM_SEC) {
Serial.println("Alarm triggered!");
}
delay(1000);
}
This method continuously checks the current time in the loop(), comparing it to a preset alarm time.
It requires the Arduino to keep running and checking manually.
May miss alarms if loop() is busy with other tasks.
Some RTC modules, like the DS3231, support alarms. Here’s an example of how to set an alarm for a specific time:
#include <Wire.h>
#include <RTClib.h>
RTC_DS3231 rtc;
void setup() {
Serial.begin(9600);
rtc.begin();
rtc.adjust(DateTime(2024, 2, 20, 12, 0, 0)); // Set the current time
rtc.setAlarm1(DateTime(2024, 2, 20, 12, 30, 0), DS3231_A1_Hour);
}
void loop() {
if (rtc.alarmFired(1)) {
Serial.println("Alarm triggered!");
rtc.clearAlarm(1);
}
}
This method uses the RTC module’s internal alarm function.
The RTC keeps track of time independently, and the Arduino only checks when the alarm fires.
More efficient because it avoids constant checking.
3.7.1 RTC Returns Incorrect Time
Ensure the backup battery is installed and charged.
Adjust the time manually using rtc.adjust().
3.7.2 RTC Module Not Detected
Check wiring connections, especially SDA and SCL.
Ensure the correct I2C address is being used.
3.7.3 Time Resets on Power Loss
Verify that a coin-cell battery is inserted in the RTC module.
Use an RTC for Real-Time Applications: Unlike millis(), RTC modules keep accurate time even when powered off.
Ideal for Data Logging and Scheduling: Essential for applications requiring timestamps, alarms, and automation.
Simple I2C Communication: Works with libraries like RTClib for easy implementation.
RTC vs. millis(): Use millis() for short-term interval-based events and RTC for long-term timekeeping.
Cool stuff to come