Audible notifications: an active buzzer and transistor

Trigger two short beeps and one long beep with a button. Build a transistor driver and organise the sound pattern using millis().

Sound, relays and interactive projectsBeginner75 min

What you will learn

  • Distinguish an active buzzer from an externally driven passive transducer.
  • Connect a PN2222A, resistors and protection diode by terminal function.
  • Start one timed sequence from a debounced button press.
  • Explain busy-state input handling and the initial off state.

Before you start

Course 03: buttons, INPUT_PULLUP and debouncing. Course 07: reliable digital input reading. You should know how to upload a sketch to a classic ESP32.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What are we building?

Build an audible notification triggered by one button press: two short beeps followed by one longer beep. The ESP32 switches power; the active buzzer generates the tone internally. Later projects could use this pattern to acknowledge an action or announce a completed measurement.

The program continues reading the button and reporting its state during the sequence. Presses received while the sequence is active are ignored. After the final beep, the output remains off until a new press.

Learning goals and prerequisites

You should be able to upload an Arduino sketch and connect a button using an internal pull-up, as covered in course 03. Course 07 provides useful practice with stabilising a digital input.

By the end, you will explain internally generated sound, use a transistor as a switch, identify a protection diode's polarity, and create a timed sequence with millis(). Allow approximately 75 minutes for the workshop, including wiring checks and experiments.

Equipment

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
Active buzzer — 5 V, up to 20 mA reference1The photo lists an active buzzer but does not establish its voltage or current. For this circuit use a documented 5 V model with operating current no higher than 20 mA.
Additional PN2222A NPN transistor1Required extra — not in kit photoADDITIONAL, not listed in the kit. Identify B/C/E for the exact manufacturer; PN2222A and P2N2222A may have different lead orders.

ADDITIONAL: the transistor is not listed in the kit photograph.

Additional 1N4148 protective diode1Required extra — not in kit photoADDITIONAL, not listed in the kit. The band marks cathode K. In the buzzer circuit the cathode faces +5 V.

ADDITIONAL: the diode is not listed in the kit photograph.

1 kΩ resistor1Base resistor; course 14 also uses one across the piezo. The kit quantity at this resistance is unconfirmed.
10 kΩ resistor1R2 pulls Q1 base down to GND, keeping the base low while the GPIO is not driving.
Momentary pushbutton1Use two terminals connected only when pressed; two legs on the same side may already be joined.
Jumper wires10Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity; depends on the physical layout.

In addition to kit parts, you must obtain a PN2222A NPN transistor and a 1N4148 diode; neither is shown in the photographed kit. The reference sounder is a two-terminal active buzzer rated for 5 V with a specified operating current of no more than 20 mA. This is a required compatibility profile, not an identification of the photographed part.

Check the markings, supply voltage and operating current of your actual buzzer. A different voltage or higher current requires a separately matched driver circuit.

Active sound and the current path

An active buzzer contains an oscillator. Applying its specified supply makes it generate a tone. Our software determines when that tone is audible. Changing a 150 ms segment changes the rhythm; it does not select the internal oscillator's musical frequency. Murata's drive circuit overview describes internally and externally driven sound components.

When GPIO26 is HIGH, a small current through R1 turns Q1 on. The load current then flows from 5 V through the buzzer, through Q1's collector and emitter, and to ground. A LOW output interrupts this path by turning Q1 off.

Why the transistor and diode matter

R1, 1 kΩ, limits base current. R2, 10 kΩ, connects the base to ground to hold the transistor off before the GPIO actively drives it. Assuming approximately 0.7 V across the base–emitter junction, current through R1 is about (3.3 − 0.7)/1000 = 2.6 mA. This is a circuit estimate, not a measured value.

The diode is normally reverse-biased: its banded cathode connects to the positive supply. It limits the relevant voltage transient when the load turns off. Q1 carries the normal load current; the buzzer's operating power does not come from the GPIO.

Schematic and pin identification

Wiring diagram

Q1 and D1 are additional components. Use an active buzzer verified for 5 V and no more than 20 mA operating current. The diode band goes to +5 V; identify Q1 B/C/E from its actual datasheet. USB5V is the verified board supply rail, not a GPIO.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.GPIO26R1.1R1, 1 kΩ, limits base current.
R1.2Q1.BBase of the additional PN2222A transistor.
Q1.BR2.1R2, 10 kΩ, pulls the base low while the GPIO is not driving.
R2.2ESP32.GNDOther end of the pull-down resistor.
Q1.EESP32.GNDEmitter to common ground; verify actual B/C/E pin order.
ESP32.USB5VBZ1.PLUSVerified USB 5 V rail to the buzzer positive terminal.
D1.KESP32.USB5VCathode of the protective 1N4148, marked with a band, to +5 V.
BZ1.MINUSQ1.CCollector switches the active buzzer current.
D1.ABZ1.MINUSDiode anode to buzzer negative and collector.
ESP32.GPIO27SW1.CONTACT_AButton input configured with INPUT_PULLUP.
SW1.CONTACT_BESP32.GNDOther terminal of the selected contact pair, connected when pressed.

The schematic labels transistor terminals by function: B, C and E. Determine their physical order from the datasheet for your exact manufacturer and package. Another member of the 2222 family can have a different lead arrangement. Consult the PN2222A datasheet and match its package drawing to your part.

Power the board through USB. Use a header documented for your specific board as providing USB-derived 5 V. Check the available USB and board current budget. Do not connect another 5 V source in parallel with USB. All ESP32 GPIO control signals remain at 3.3 V.

Wiring step by step

  1. Disconnect USB. Place Q1 with its B, C and E leads in separate breadboard rows.
  2. Connect ESP32 GND to the common ground rail and Q1's emitter to that rail.
  3. Connect GPIO26 through R1, 1 kΩ, to the base. Add R2, 10 kΩ, between the base and ground.
  4. Connect BZ1 negative to Q1's collector and BZ1 positive to the verified USB 5 V header.
  5. Connect D1 across the buzzer: anode to negative/collector, banded cathode to positive/5 V.
  6. Connect GPIO27 through a switched button contact pair to GND. On a four-leg button, identify which contacts actually connect when pressed.
  7. Compare every connection with the table. Confirm that neither GPIO26 nor GPIO27 connects to 5 V, then reconnect USB.

Arduino setup

Use Arduino IDE 2.x, esp32 by Espressif Systems 3.3.12, ESP32 Dev Module, and the correct serial port. The reference is a classic ESP32/WROOM-32 board; the pin selection does not automatically apply to every ESP32 variant. Set Serial Monitor to 115200 baud.

No additional Arduino libraries are required. Open active_buzzer.ino inside its matching folder, verify the board selection, and upload. The buzzer should remain off after startup.

Complete program

active_buzzer.ino Arduino / C++
Download .ino

#include <Arduino.h>

constexpr uint8_t BUZZER_PIN = 26;
constexpr uint8_t BUTTON_PIN = 27;
constexpr uint32_t DEBOUNCE_MS = 30;
constexpr uint32_t REPORT_MS = 500;
constexpr uint32_t STEP_MS[] = {150, 150, 150, 150, 600};
constexpr uint8_t STEP_COUNT = sizeof(STEP_MS) / sizeof(STEP_MS[0]);
// Each event is accepted once after 30 ms of stable input.
// If held during reset, a stable release is required before the first press.
struct DebouncedButton {
  bool rawHigh = true;
  bool stableHigh = true;
  bool armed = true;
  bool pressed = false;
  bool released = false;
  uint32_t rawChangedAt = 0;
  uint32_t pressStartedAt = 0;
  void begin(uint32_t now) {
    rawHigh = stableHigh = digitalRead(BUTTON_PIN) == HIGH;
    armed = rawHigh;
    rawChangedAt = now;
    pressed = released = false;
  }
  void update(uint32_t now) {
    pressed = released = false;
    const bool readingHigh = digitalRead(BUTTON_PIN) == HIGH;
    if (readingHigh != rawHigh) {
      rawHigh = readingHigh;
      rawChangedAt = now;
    }
    if (rawHigh != stableHigh && static_cast<uint32_t>(now - rawChangedAt) >= DEBOUNCE_MS) {
      stableHigh = rawHigh;
      if (stableHigh) {
        armed = true;
        released = true;
      } else if (armed) {
        pressed = true;
        pressStartedAt = rawChangedAt;
      }
    }
  }
};
DebouncedButton button;

bool patternActive = false;
uint8_t patternStep = 0;
uint32_t stepStartedAt = 0;
uint32_t lastReportAt = 0;

void startPattern(uint32_t now) {
  patternActive = true;
  patternStep = 0;
  stepStartedAt = now;
  digitalWrite(BUZZER_PIN, HIGH);
}

void updatePattern(uint32_t now) {
  while (patternActive && static_cast<uint32_t>(now - stepStartedAt) >= STEP_MS[patternStep]) {
    stepStartedAt += STEP_MS[patternStep];
    ++patternStep;
    if (patternStep >= STEP_COUNT) {
      patternActive = false;
      digitalWrite(BUZZER_PIN, LOW);
    } else {
      digitalWrite(BUZZER_PIN, (patternStep % 2 == 0) ? HIGH : LOW);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  button.begin(millis());
  lastReportAt = millis();
  Serial.println("Active buzzer: press for one 1.2 s pattern; presses during the pattern are ignored.");
}

void loop() {
  const uint32_t now = millis();
  button.update(now);
  if (button.pressed && !patternActive) startPattern(now);
  updatePattern(now);
  if (static_cast<uint32_t>(now - lastReportAt) >= REPORT_MS) {
    lastReportAt = now;
    Serial.print("Pattern="); Serial.println(patternActive ? "PLAYING" : "IDLE");
  }
}

Run the unmodified sketch first. This gives you a clear baseline before changing the notification pattern.

How the timing works

The button uses INPUT_PULLUP: released is HIGH and pressed is LOW. A changed input must remain stable for 30 ms before being accepted. If the button is held during reset, release it and press again; the initial LOW level does not start a sequence.

STEP_MS stores five durations; patternStep selects the current segment. startPattern() starts the sequence, and updatePattern() advances it when the segment time expires. patternActive distinguishes playing from idle. Subtracting unsigned timestamps keeps comparisons valid across millis() rollover. There is no blocking delay() between segments.

SegmentBuzzerDurationTime since start
1On150 ms150 ms
2Off150 ms300 ms
3On150 ms450 ms
4Off150 ms600 ms
5On600 ms1200 ms

Running the experiment

Press and release the button. Expect two short beeps and one longer beep, followed by silence. A further press during the pattern neither extends it nor queues another sequence.

Serial status is reported approximately every 500 ms. This helps distinguish idle and active operation, but an entire 150 ms segment can occur between reports. Do not use the printed samples as precise measurements of every beep. These are expected observations for you to verify on your own hardware.

Experiment table

ActionExpected resultWhat it demonstrates
Press briefly onceOne sequence, then silenceEvent-triggered operation
Hold the button for three secondsNo repeating sequenceState versus a new press
Press again while sound is activeNo additional queued sequenceHandling a busy system
Hold the button while resettingSilence until release and a new pressDefined startup behaviour

Record each result. When testing another press, release the button long enough for the release itself to become stable.

Troubleshooting

SymptomCheck and correction
Serial works but there is no soundCheck active type, BZ1 polarity, USB 5 V, common ground, and exact B/C/E identification.
Only a click is audibleVerify that the part is not a passive transducer requiring an alternating drive.
Sound is continuousDisconnect power; check for a negative terminal wired directly to ground or an incorrectly connected Q1.
The button has no effectCheck the switched contact pair, GPIO27, and the release requirement after reset.
The ESP32 resets when sound startsCheck for shorts, excessive load current, a poor USB cable, or an inadequate power path.

Challenge and worked answers

In STEP_MS, change only the final beep from 600 to 300 ms. Calculate the new duration and predict what happens when the button remains held.

The answer is 150 + 150 + 150 + 150 + 300 = 900 ms. The first four segments are unchanged. Holding the button still produces only one sequence because you have not changed the event handling.

Why does a shorter segment not produce a higher pitch? You change the active oscillator's powered duration, not its frequency. Why is R2 present? It defines the base state when the GPIO is not yet controlling the circuit. Why do additional presses not play later? The program processes them while busy and deliberately stores no request for another cycle.

Primary references