advanced-arduino-techniques

advanced-arduino-techniques

Advanced Arduino Techniques: Beyond the Basics

Phone cases · 15% off first order

Phone Case GiftThey pick the model · 2 minutes Code FIRST15GIFT

You’ve mastered blinking LEDs and reading sensor values with your Arduino. Now it’s time to unlock the true potential of this versatile microcontroller. This guide will delve into three key advanced techniques: interrupts, timers, and EEPROM, offering practical examples and explanations to elevate your Arduino projects from simple to sophisticated. We’ll be using the ubiquitous Arduino Uno Rev3 (A000066)Check on Amazon → as our example platform.

Who is this guide for?

This guide is for intermediate Arduino users who have a solid grasp of basic programming concepts and Arduino syntax. If you’re comfortable with digital and analog I/O, variables, loops, and conditional statements, you’re ready to move on to these more advanced topics. If you’re new to Arduino, we suggest starting with one of our beginner guides first, like our Arduino starter kit recommendationCheck on Amazon →.

Harnessing the Power of Interrupts

Interrupts allow your Arduino to respond to external events in real-time, without constantly polling for changes. This is crucial for applications where timing is critical, such as button presses, encoder readings, or receiving data from a serial port.

Understanding Interrupts

An interrupt is a signal that tells the microcontroller to immediately suspend its current task and execute a special function called an Interrupt Service Routine (ISR). After the ISR is complete, the microcontroller resumes where it left off.

The Arduino Uno has a limited number of external interrupt pins:

  • Pin 2: Interrupt 0
  • Pin 3: Interrupt 1

These pins can be configured to trigger interrupts on different events:

  • LOW: The interrupt is triggered when the pin is LOW.
  • CHANGE: The interrupt is triggered when the pin changes value.
  • RISING: The interrupt is triggered when the pin goes from LOW to HIGH.
  • FALLING: The interrupt is triggered when the pin goes from HIGH to LOW.

Example: Button Debouncing with Interrupts

Button bouncing occurs when a button press generates multiple rapid on/off signals due to mechanical imperfections. Interrupts provide a clean way to debounce a button.

volatile byte buttonState = HIGH; // volatile because it's modified in the ISR

void setup() {
  pinMode(2, INPUT_PULLUP); // Use internal pull-up resistor
  attachInterrupt(digitalPinToInterrupt(2), buttonPress, FALLING); // Interrupt on falling edge
  Serial.begin(9600);
}

void loop() {
  if (buttonState == LOW) {
    Serial.println("Button Pressed!");
    buttonState = HIGH; // Reset the state
    delay(200); // Debounce delay
  }
}

void buttonPress() {
  buttonState = LOW; // Set the state to LOW
}

Explanation:

  1. volatile byte buttonState = HIGH;: Declares a volatile variable to store the button state. volatile is crucial because the variable is modified within the ISR.
  2. pinMode(2, INPUT_PULLUP);: Configures pin 2 as an input with the internal pull-up resistor enabled.
  3. attachInterrupt(digitalPinToInterrupt(2), buttonPress, FALLING);: Attaches an interrupt to pin 2 that triggers the buttonPress function on the falling edge. digitalPinToInterrupt(pin) converts the digital pin number to the corresponding interrupt number.
  4. buttonPress(): This is the Interrupt Service Routine (ISR). It sets buttonState to LOW when the button is pressed.
  5. The loop() function checks the buttonState. If it’s LOW, it prints “Button Pressed!”, resets the state to HIGH, and introduces a 200ms delay for debouncing.

Important Notes about ISRs:

  • Keep ISRs short and fast. Avoid delays or long computations within an ISR.
  • Declare variables shared between the ISR and the main program as volatile.
  • Disable interrupts during critical sections of code using noInterrupts() and re-enable them with interrupts().

Mastering Timers for Precise Timing

Phone cases · 15% off first order

Titan CasePrecision fit · 2,000+ designs Code FIRST15TIT

Timers are essential for tasks that require precise timing, such as PWM control, generating waveforms, or measuring intervals. The Arduino Uno has three timers: Timer0, Timer1, and Timer2. Each timer has different capabilities and configurations.

Understanding Timers

Timers are hardware counters that increment at a specific rate. When the counter reaches a predefined value, an interrupt can be triggered. This allows you to execute code at precise intervals.

Example: Generating a PWM Signal with Timer1

This example demonstrates how to use Timer1 to generate a PWM signal on pin 9.

void setup() {
  pinMode(9, OUTPUT); // PWM output pin
  TCCR1A = _BV(COM1A1) | _BV(WGM11); // Fast PWM mode, non-inverting output on OC1A
  TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS10); // Fast PWM mode, no prescaling
  ICR1 = 399; // Set PWM frequency to approximately 10 kHz (16 MHz / (399 + 1))
  OCR1A = 200; // Set duty cycle to approximately 50% (200 / 399)
}

void loop() {
  // You can adjust the duty cycle in the loop if needed
  // For example:
  // OCR1A = analogRead(A0) / 5; // Map analog input to duty cycle
}

Explanation:

  1. pinMode(9, OUTPUT);: Configures pin 9 as an output pin.
  2. TCCR1A and TCCR1B: These registers configure Timer1’s control registers.
    • COM1A1: Enables non-inverting PWM output on OC1A (pin 9).
    • WGM11, WGM12, WGM13: Sets Timer1 to Fast PWM mode with ICR1 as the TOP value.
    • CS10: Sets the prescaler to no prescaling (clock frequency = 16 MHz).
  3. ICR1 = 399;: Sets the Input Capture Register 1 (ICR1) to 399. This determines the PWM frequency. The formula is: Frequency = Clock Frequency / (ICR1 + 1). In this case, 16 MHz / (399 + 1) ≈ 40 kHz.
  4. OCR1A = 200;: Sets the Output Compare Register 1A (OCR1A) to 200. This determines the duty cycle. The formula is: Duty Cycle = OCR1A / ICR1. In this case, 200 / 399 ≈ 50%.

Important Notes about Timers:

  • Directly manipulating timer registers can be complex. Use libraries like <TimerOne.h> for easier timer management in some cases. However, understanding the underlying registers provides more control.
  • Timer0 is used by the millis() and delay() functions. Modifying Timer0 can affect these functions.
  • Each timer has different modes of operation. Refer to the ATmega328P datasheet for detailed information.

Utilizing EEPROM for Persistent Data Storage

EEPROM (Electrically Erasable Programmable Read-Only Memory) allows you to store data that persists even when the Arduino is powered off. This is useful for storing configuration settings, calibration data, or other important information that needs to be retained between sessions.

Understanding EEPROM

The Arduino Uno has 1KB (1024 bytes) of EEPROM. You can read from and write to EEPROM using the <EEPROM.h> library.

Example: Storing and Retrieving a Calibration Value

This example demonstrates how to store a calibration value in EEPROM and retrieve it on startup.

#include <EEPROM.h>

const int calibrationAddress = 0; // EEPROM address to store the calibration value
float calibrationValue = 1.0;     // Default calibration value

void setup() {
  Serial.begin(9600);
  delay(1000);

  // Read the calibration value from EEPROM
  EEPROM.get(calibrationAddress, calibrationValue);

  Serial.print("Calibration Value: ");
  Serial.println(calibrationValue);

  // Check if the EEPROM value is valid (e.g., not all 0xFF)
  if (calibrationValue > 1000 || calibrationValue < -1000) {
    Serial.println("Invalid calibration value in EEPROM. Using default.");
    calibrationValue = 1.0;
  }

  // Simulate a calibration process (replace with your actual calibration)
  calibrationValue = 1.23;

  // Store the new calibration value in EEPROM
  EEPROM.put(calibrationAddress, calibrationValue);

  Serial.println("New Calibration Value stored in EEPROM.");
}

void loop() {
  // Use the calibration value in your application
  // For example:
  // float sensorValue = analogRead(A0) * calibrationValue;
  delay(1000);
}

Explanation:

  1. #include <EEPROM.h>: Includes the EEPROM library.
  2. const int calibrationAddress = 0;: Defines the EEPROM address where the calibration value will be stored.
  3. float calibrationValue = 1.0;: Defines a variable to store the calibration value with a default value.
  4. EEPROM.get(calibrationAddress, calibrationValue);: Reads the calibration value from EEPROM at the specified address and stores it in the calibrationValue variable.
  5. The code checks if the read value is within a reasonable range. This helps prevent using corrupt data if the EEPROM is uninitialized.
  6. EEPROM.put(calibrationAddress, calibrationValue);: Writes the new calibration value to EEPROM.

Important Notes about EEPROM:

  • EEPROM has a limited number of write cycles (typically 100,000). Avoid writing to EEPROM frequently.
  • Use EEPROM.update() instead of EEPROM.write() to avoid unnecessary writes. EEPROM.update() only writes to the EEPROM if the value has changed.
  • Be careful not to overwrite critical system data stored in EEPROM.
  • EEPROM is byte-oriented. You need to handle data types larger than a byte (e.g., int, float) using functions like EEPROM.get() and EEPROM.put().

By mastering interrupts, timers, and EEPROM, you’ll significantly expand your Arduino programming capabilities and be able to create more complex and responsive projects. Remember to consult the ATmega328P datasheet and Arduino reference documentation for more in-depth information.

Marcus Webb

By Marcus Webb · Editor, GymLedger

Published June 6, 2026 · Last reviewed June 6, 2026

Before you leave

Maker staples worth buying in bulk

Workshop refills we track on TinkerBench — partner links, no extra cost to you.

As an Amazon Associate, TinkerBench earns from qualifying purchases. Full disclosure · All tracked reviews