The tone(pin, frequency, duration) function is the standard method for generating audio-frequency square waves on AVR-based microcontrollers. It outputs a 50% duty cycle square wave, which is exactly what a passive piezo buzzer needs to vibrate and produce sound. This guide specifically targets the Arduino Uno R3 (ATmega328P), but we will also cover the critical hardware timer conflicts that trip up most makers, and provide the exact workaround for ESP32 boards where the native function fails.

The Hardware Reality: Timer Conflicts and Board Compatibility

Under the hood, the Arduino tone() reference relies on hardware timers to generate precise frequencies without blocking the main loop. On the ATmega328P (Uno/Nano), tone() hijacks Timer2. It configures the timer's prescaler and uses the OCR2A (Output Compare Register) to toggle the pin state at the exact microsecond interval required for your target frequency.

Because Timer2 is also responsible for hardware PWM on specific pins, calling tone() silently disables analogWrite() on those pins. If you are trying to fade an LED on Pin 3 while playing a tone, the LED will simply snap to full brightness or turn off. The Arduino analogWrite() reference documents this, but it remains one of the most common debugging headaches on the bench.

Furthermore, as makers migrate to 32-bit boards, they quickly discover that tone() is not universally supported. Here is the data-dense compatibility matrix you need before designing your schematic:

Microcontroller / Board Native tone() Support Hardware Timer Used PWM Pins Disabled by tone() Required Alternative Function
ATmega328P (Uno R3 / Nano) Yes Timer2 (8-bit) Pins 3, 11 None (Native)
ATmega2560 (Mega 2560) Yes Timer2 (8-bit) Pins 9, 10 None (Native)
ESP32-WROOM-32 (DevKit V1) No (Compile Error) N/A N/A ledcWriteTone()
RP2040 (Raspberry Pi Pico) Yes (via arduino-pico core) Hardware PWM Slices None (Independent slices) None (Native)
Bench Tip: Never drive a piezo buzzer directly from a microcontroller pin without a current-limiting resistor. A piezo element is essentially a capacitor. When the MCU pin drops to 0V, the piezo dumps its stored charge back into the pin as a voltage spike (kickback). A simple 100Ω resistor in series protects the ATmega328P's internal clamp diodes from degradation over time.

Parts List and Pin Mapping for the Proximity Alarm

To demonstrate tone() in a practical, interactive build, we are wiring up a proximity alarm. As an object gets closer to the ultrasonic sensor, the pitch of the buzzer increases. This requires real-time polling and frequency mapping.

Exact Parts List

  • MCU: Arduino Uno R3 (ATmega328P, 16MHz crystal variant)
  • Sensor: HC-SR04 Ultrasonic Distance Sensor (5V tolerant variant)
  • Output: 5V Passive Piezo Buzzer (e.g., TDK PS1240P02BT or generic 2.7kHz passive module. Do not use an active buzzer.)
  • Protection: 1x 100Ω 1/4W carbon film resistor
  • Wiring: 22 AWG solid core jumper wires, standard 830-point breadboard

Pin Mapping Table

Component Pin Arduino Uno R3 Pin Notes
HC-SR04 VCC 5V Requires stable 5V; do not use 3.3V.
HC-SR04 Trig D8 Configured as OUTPUT.
HC-SR04 Echo D9 Configured as INPUT. 5V logic safe on Uno.
HC-SR04 GND GND Common ground required.
Piezo (+) D10 (via 100Ω resistor) Resistor prevents capacitive kickback damage.
Piezo (-) GND Common ground.

Complete Compilable Code with Error Handling

The following C++ code is fully compilable in the Arduino IDE (1.8.x or 2.x). It includes explicit pin definitions, bounds checking, and error handling for sensor timeouts—a common failure mode when the HC-SR04 misses an echo or is disconnected.

#define TRIG_PIN 8
#define ECHO_PIN 9
#define BUZZER_PIN 10

// Audio bounds
#define MIN_FREQ 200   // Hz (Low hum)
#define MAX_FREQ 2500  // Hz (High whine, near peak resonance for cheap piezos)

// Distance bounds
#define MAX_DISTANCE_CM 200 
#define PULSE_TIMEOUT_US 30000 // 30ms timeout for pulseIn()

void setup() {
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  
  Serial.begin(115200);
  Serial.println("Proximity Alarm Initialized.");
}

void loop() {
  // 1. Trigger the HC-SR04
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  // 2. Read the echo with a strict timeout to prevent loop blocking
  unsigned long duration = pulseIn(ECHO_PIN, HIGH, PULSE_TIMEOUT_US);

  // 3. Error Handling: Sensor timeout or disconnected
  if (duration == 0) {
    noTone(BUZZER_PIN); // Mute on error
    Serial.println("Error: Sensor timeout or out of range.");
    delay(100);
    return; 
  }

  // 4. Calculate distance (Speed of sound = 343 m/s -> 0.0343 cm/us)
  float distance_cm = (duration * 0.0343) / 2.0;

  // 5. Map distance to frequency and drive the buzzer
  if (distance_cm > MAX_DISTANCE_CM) {
    noTone(BUZZER_PIN); // Silent when far away
  } else {
    // Closer object = higher pitch. Map 0-200cm to 2500Hz-200Hz.
    int freq = map((long)distance_cm, 0, MAX_DISTANCE_CM, MAX_FREQ, MIN_FREQ);
    
    // Constrain to prevent out-of-bounds timer values
    freq = constrain(freq, MIN_FREQ, MAX_FREQ);
    
    tone(BUZZER_PIN, freq);
  }

  // Small delay to stabilize sensor readings (HC-SR04 needs ~60ms cycle time)
  delay(60); 
}

Debugging: First Three Things to Check When It Fails

If you upload the code and the buzzer remains silent, emits a faint click, or throws a compilation error, run through these three diagnostic steps in order.

1. Active vs. Passive Buzzer Mismatch

This is the number one hardware mistake. An active buzzer has an internal oscillator circuit; it only requires a steady DC voltage (usually 5V) to make sound. If you feed an active buzzer a square wave via tone(), it will either stay silent or emit a distorted, sputtering noise because you are rapidly turning its internal oscillator on and off. tone() strictly requires a passive buzzer, which is just a raw piezo crystal that relies on the MCU's square wave to vibrate. Test it by connecting the buzzer directly to 5V and GND: if it beeps continuously, it's active. If it just clicks once, it's passive.

2. The Timer2 PWM Conflict

If your buzzer works, but an LED on Pin 3 or Pin 11 stops fading when you call analogWrite(), you have hit the Timer2 conflict. tone() reconfigures the Timer2 prescaler and compare registers, completely overriding the PWM configuration on those specific pins. Fix: Move your PWM-driven LEDs to Pins 5 or 6 (which use Timer0) or Pins 9 and 10 (which use Timer1).

3. The ESP32 Compilation Error

If you port this exact code to an ESP32 DevKit V1, the compiler will halt with this exact string:

error: 'tone' was not declared in this scope

The ESP32 does not use the AVR timer architecture, so the standard tone() function doesn't exist in its core. To fix this, you must use the ESP32's LED Control (LEDC) peripheral, which handles PWM and tone generation natively. You can review the Espressif LEDC API for the full documentation, but here is the exact drop-in replacement for the ESP32:

// ESP32 tone() replacement using LEDC
#define BUZZER_PIN 10
#define LEDC_CHANNEL 0
#define LEDC_RESOLUTION 8

void setup() {
  // Configure LEDC channel, frequency (placeholder), and resolution
  ledcSetup(LEDC_CHANNEL, 1000, LEDC_RESOLUTION);
  ledcAttachPin(BUZZER_PIN, LEDC_CHANNEL);
}

void playTone(int freq) {
  if (freq > 0) {
    ledcWriteTone(LEDC_CHANNEL, freq);
  } else {
    ledcWriteTone(LEDC_CHANNEL, 0); // Mute
  }
}

Extending and Simplifying the Build

Depending on your project constraints, you may need to scale this circuit up for better audio fidelity, or strip it down for a simpler user interface.

How to Simplify: The Potentiometer Theremin

If the HC-SR04 ultrasonic sensor is causing jitter or you just want a manual testing tool, strip the sensor out entirely. Wire a 10kΩ potentiometer with the wiper to Analog Pin A0, and the outer legs to 5V and GND. Replace the distance mapping logic in the loop with a direct analog read:

int potVal = analogRead(A0);
int freq = map(potVal, 0, 1023, 31, 8000);
tone(BUZZER_PIN, freq);

This gives you a smooth, manual sweep across the entire audible spectrum supported by the ATmega328P's Timer2 limits.

How to Extend: True Audio and Sine Waves

The tone() function only generates square waves. Square waves are rich in odd harmonics, which is why cheap buzzers sound harsh and grating. If your project requires pleasant chimes, voice synthesis, or true sine waves, you must abandon the MCU's internal timers and offload audio to a dedicated I2S DAC.

For high-quality extension, use an Adafruit MAX98357A I2S Amplifier paired with a 3W 4Ω speaker. This requires an ESP32 (since the Uno lacks native I2S hardware). The ESP32's I2S peripheral streams buffered audio data directly to the DAC without touching the CPU's general-purpose timers, allowing you to play actual .wav files or generate mathematically pure sine waves while keeping your main loop completely free for sensor polling and WiFi communication.