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
| Component | Quantity | Specification and notes |
|---|---|---|
| Development board with a classic ESP32-WROOM-32 module | 1 | Match the reference GPIO labels; check physical header positions on the actual board. |
| USB data and power cable | 1 | Use the connector fitted to your board; the pictured kit lists Micro-USB. |
| Solderless breadboard | 1 | The kit lists 830 tie points. Check whether the power rails are split. |
| Active buzzer — 5 V, up to 20 mA reference | 1 | The 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 transistor | 1Required extra — not in kit photo | ADDITIONAL, 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 diode | 1Required extra — not in kit photo | ADDITIONAL, 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Ω resistor | 1 | Base resistor; course 14 also uses one across the piezo. The kit quantity at this resistance is unconfirmed. |
| 10 kΩ resistor | 1 | R2 pulls Q1 base down to GND, keeping the base low while the GPIO is not driving. |
| Momentary pushbutton | 1 | Use two terminals connected only when pressed; two legs on the same side may already be joined. |
| Jumper wires | 10 | Use 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.
| From | To | Connection |
|---|---|---|
ESP32.GPIO26 | R1.1 | R1, 1 kΩ, limits base current. |
R1.2 | Q1.B | Base of the additional PN2222A transistor. |
Q1.B | R2.1 | R2, 10 kΩ, pulls the base low while the GPIO is not driving. |
R2.2 | ESP32.GND | Other end of the pull-down resistor. |
Q1.E | ESP32.GND | Emitter to common ground; verify actual B/C/E pin order. |
ESP32.USB5V | BZ1.PLUS | Verified USB 5 V rail to the buzzer positive terminal. |
D1.K | ESP32.USB5V | Cathode of the protective 1N4148, marked with a band, to +5 V. |
BZ1.MINUS | Q1.C | Collector switches the active buzzer current. |
D1.A | BZ1.MINUS | Diode anode to buzzer negative and collector. |
ESP32.GPIO27 | SW1.CONTACT_A | Button input configured with INPUT_PULLUP. |
SW1.CONTACT_B | ESP32.GND | Other 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
- Disconnect USB. Place Q1 with its B, C and E leads in separate breadboard rows.
- Connect ESP32 GND to the common ground rail and Q1's emitter to that rail.
- Connect GPIO26 through R1, 1 kΩ, to the base. Add R2, 10 kΩ, between the base and ground.
- Connect BZ1 negative to Q1's collector and BZ1 positive to the verified USB 5 V header.
- Connect D1 across the buzzer: anode to negative/collector, banded cathode to positive/5 V.
- Connect GPIO27 through a switched button contact pair to GND. On a four-leg button, identify which contacts actually connect when pressed.
- 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
#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.
| Segment | Buzzer | Duration | Time since start |
|---|---|---|---|
| 1 | On | 150 ms | 150 ms |
| 2 | Off | 150 ms | 300 ms |
| 3 | On | 150 ms | 450 ms |
| 4 | Off | 150 ms | 600 ms |
| 5 | On | 600 ms | 1200 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
| Action | Expected result | What it demonstrates |
|---|---|---|
| Press briefly once | One sequence, then silence | Event-triggered operation |
| Hold the button for three seconds | No repeating sequence | State versus a new press |
| Press again while sound is active | No additional queued sequence | Handling a busy system |
| Hold the button while resetting | Silence until release and a new press | Defined startup behaviour |
Record each result. When testing another press, release the button long enough for the release itself to become stable.
Troubleshooting
| Symptom | Check and correction |
|---|---|
| Serial works but there is no sound | Check active type, BZ1 polarity, USB 5 V, common ground, and exact B/C/E identification. |
| Only a click is audible | Verify that the part is not a passive transducer requiring an alternating drive. |
| Sound is continuous | Disconnect power; check for a negative terminal wired directly to ground or an incorrectly connected Q1. |
| The button has no effect | Check the switched contact pair, GPIO27, and the release requirement after reset. |
| The ESP32 resets when sound starts | Check 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.
