What you will learn
- Verify that a passive sounder matches the piezo reference and 0–5 V excitation.
- Connect the transistor, parallel discharge resistor and series pulse-limiting resistor.
- Map a 12-bit analogue reading into tone frequency.
- Control sound using the pin-based LEDC API and a debounced button state.
Before you start
Course 05: potentiometers and ADC. Course 13: transistor buzzer control, diode polarity and buttons. The reference uses a classic ESP32 and Arduino-ESP32 3.3.12.
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 a simple instrument: hold a button to hear a tone and turn a potentiometer to change its pitch. The ESP32 generates an electrical signal between 200 and 2000 Hz. Releasing the button silences the instrument.
This lesson uses a passive piezoelectric sounder. Its externally supplied waveform determines the tone, letting you connect an analogue reading, a mathematical formula, and a change you can hear immediately.
Learning goals and prerequisites
Complete course 05 for potentiometers and analogue readings, and course 13 for the transistor switch and button. Allow about 90 minutes.
You will select a compatible passive sounder, explain the discharge resistor, map ADC readings into frequency, and use the Arduino-ESP32 LEDC API. A specific goal is to distinguish pitch, controlled by frequency, from loudness.
Equipment and sounder selection
| 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. |
| Passive buzzer — reference piezo transducer | 1 | The photo lists a passive buzzer without its transducer type. This circuit requires a piezo rated for 0–5 V drive; a magnetic transducer needs a separately matched circuit. |
| 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 | 2 | 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. |
| 220 Ω resistor | 1 | R4 is the series resistor between BZ1.MINUS and Q1.C; it limits the piezo charging pulse. |
| Momentary pushbutton | 1 | Use two terminals connected only when pressed; two legs on the same side may already be joined. |
| 10 kΩ potentiometer | 1 | Two end terminals and a wiper W; identify their physical positions on the actual component. |
| Jumper wires | 13 | Use male-to-male or female-to-male leads to suit the board headers. Approximate quantity; depends on the physical layout. |
The PN2222A transistor and 1N4148 diode are mandatory additional parts not shown in the photographed kit. The kit's passive sounder has not been identified as piezoelectric from the photograph alone. This circuit requires a verified passive piezo transducer rated for 0–5 V square-wave excitation.
If markings or documentation do not establish the type and drive rating, identify the part first. An active buzzer has its own oscillator, while a passive magnetic transducer needs a separately matched circuit. Neither is an automatic replacement for this reference piezo element.
Frequency and pitch
Frequency states how many periods occur each second. At 200 Hz a period lasts 5 ms; at 2000 Hz it lasts 0.5 ms. Within this range, a greater frequency generally sounds like a higher pitch.
The LEDC peripheral generates the signal while the main program reads the potentiometer and button. ledcWriteTone() uses a 50% duty cycle. This sets the duration ratio of the waveform's two levels, not equal loudness at every frequency. A real sounder converts some frequencies into sound more efficiently than others.
Why the circuit changes from course 13
A piezo element behaves electrically like a capacitive load. R3, 1 kΩ, is connected across it to discharge it when Q1 turns off. Murata explains this parallel resistor's role. A diode does not replace R3.
R4, 220 Ω, is added in the collector path to limit charging-current pulses. At a nominal 5 V the initial estimate is 5/220 ≈ 23 mA. In the settled on state, R3 and R4 give approximately 5 × 1000/1220 ≈ 4.1 V across the piezo, neglecting transistor voltage. These are circuit calculations; the actual waveform also depends on capacitance and frequency.
Schematic and power checks
Wiring diagram
The reference is a passive piezo transducer rated for 0–5 V excitation; the photo does not establish the kit buzzer type. R3, 1 kΩ, is the parallel discharge path. R4, 220 Ω, is in series between BZ1.MINUS and Q1.C. D1 is across the piezo element, before R4. Q1 and D1 are additional.
| 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 | R4.1 | R4, 220 Ω, is between the piezo network and collector; it limits the charging pulse. |
R4.2 | Q1.C | Other end of series R4 to the collector. |
D1.A | BZ1.MINUS | Diode anode on the piezo MINUS node, before R4. |
BZ1.PLUS | R3.1 | R3, 1 kΩ, is connected across the piezo element. |
R3.2 | BZ1.MINUS | R3 provides a discharge path between PLUS and MINUS. |
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. |
ESP32.3V3 | RV1.END_A | One end of the 10 kΩ potentiometer. |
ESP32.GND | RV1.END_B | The other end terminal. |
RV1.W | ESP32.GPIO34 | Wiper to the ADC1 input. |
B, C and E label Q1's terminal functions. Identify the physical lead order using the datasheet for your exact PN2222A, rather than relying only on its flat face.
Use USB as the board's only power source. The piezo branch uses a documented USB-derived 5 V header; the potentiometer uses 3.3 V. Check the available USB and board current budget. Do not add another 5 V supply in parallel. GPIO34 is an analogue input, so never connect the potentiometer wiper to 5 V.
Wiring step by step
- Disconnect USB. Connect ESP32 GND, Q1's emitter and the common ground rail.
- Connect GPIO26 through R1, 1 kΩ, to the base. Add R2, 10 kΩ, from base to ground.
- Connect BZ1 positive to USB 5 V. Call its negative-terminal connection node X.
- Connect R3, 1 kΩ, between BZ1 positive and X, in parallel with the piezo.
- Connect X through R4, 220 Ω, to Q1's collector. Do not retain a direct wire between BZ1 negative and the collector.
- Add D1: anode to X, banded cathode to positive/5 V. It remains directly across BZ1, before R4.
- Connect the outer terminals of RV1, 10 kΩ, to 3V3 and GND, and its wiper to GPIO34.
- Connect the button's switched contact pair between GPIO27 and GND. Check every connection before reconnecting USB.
Arduino setup
Use Arduino IDE 2.x, esp32 by Espressif Systems 3.3.12, ESP32 Dev Module for the classic ESP32/WROOM-32, and Serial Monitor at 115200 baud. Open passive_synth.ino inside its matching folder. No additional libraries are required.
The sketch uses the Arduino-ESP32 3.x LEDC API, which takes a pin argument. Do not replace these calls with older examples using ledcSetup() from an earlier core version.
Complete program
#include <Arduino.h>
constexpr uint8_t PWM_PIN = 26;
constexpr uint8_t POT_PIN = 34;
constexpr uint8_t BUTTON_PIN = 27;
constexpr uint32_t DEBOUNCE_MS = 30;
constexpr uint32_t SAMPLE_MS = 20;
constexpr uint32_t REPORT_MS = 500;
// 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 pwmReady = false;
bool playing = false;
uint16_t rawValue = 0;
uint32_t targetFrequency = 200;
uint32_t lastSampleAt = 0;
uint32_t lastReportAt = 0;
uint32_t appliedFrequency = 0;
uint32_t frequencyFromRaw(uint16_t raw) {
return 200UL + static_cast<uint32_t>(raw) * 1800UL / 4095UL;
}
void stopOnPwmFailure() {
ledcWriteTone(PWM_PIN, 0);
ledcDetach(PWM_PIN);
pinMode(PWM_PIN, OUTPUT);
digitalWrite(PWM_PIN, LOW);
pwmReady = false;
playing = false;
appliedFrequency = 0;
Serial.println("PWM ERROR: output disabled; reset to retry.");
}
void setup() {
Serial.begin(115200);
pinMode(PWM_PIN, OUTPUT);
digitalWrite(PWM_PIN, LOW);
pinMode(BUTTON_PIN, INPUT_PULLUP);
button.begin(millis());
analogReadResolution(12);
analogSetPinAttenuation(POT_PIN, ADC_11db);
rawValue = analogRead(POT_PIN);
targetFrequency = frequencyFromRaw(rawValue);
pwmReady = ledcAttach(PWM_PIN, 2000, 10);
if (!pwmReady) {
digitalWrite(PWM_PIN, LOW);
Serial.println("PWM ERROR: ledcAttach failed; output OFF; reset to retry.");
return;
}
ledcWriteTone(PWM_PIN, 0);
lastSampleAt = lastReportAt = millis();
Serial.println("Passive piezo: release once after reset, then hold the button to play.");
}
void loop() {
if (!pwmReady) return;
const uint32_t now = millis();
button.update(now);
if (static_cast<uint32_t>(now - lastSampleAt) >= SAMPLE_MS) {
lastSampleAt = now;
rawValue = analogRead(POT_PIN);
targetFrequency = frequencyFromRaw(rawValue);
}
playing = button.armed && !button.stableHigh;
const uint32_t wantedFrequency = playing ? targetFrequency : 0;
if (wantedFrequency != appliedFrequency) {
const uint32_t actualFrequency = ledcWriteTone(PWM_PIN, wantedFrequency);
if (wantedFrequency != 0 && actualFrequency == 0) {
stopOnPwmFailure();
return;
}
appliedFrequency = wantedFrequency;
}
if (static_cast<uint32_t>(now - lastReportAt) >= REPORT_MS) {
lastReportAt = now;
Serial.print("raw="); Serial.print(rawValue);
Serial.print(" target_Hz="); Serial.print(targetFrequency);
Serial.print(" playing="); Serial.println(playing ? "YES" : "NO");
}
}
Upload the original sketch first. Startup should be silent; a valid button press is needed to begin playing.
Mapping the analogue reading
The program reads the potentiometer every 20 ms, using 12-bit resolution and ADC_11db on GPIO34. rawValue ranges from 0 to 4095, and frequencyFromRaw() calculates 200 + raw × 1800 / 4095. Its result is stored in targetFrequency.
| ADC reading | Target frequency |
|---|---|
| 0 | 200 Hz |
| 1024 | 650 Hz |
| 2048 | 1100 Hz |
| 3072 | 1550 Hz |
| 4095 | 2000 Hz |
Integer division discards the fractional remainder. This is not a calibrated voltage measurement. The ADC can reach its maximum before the potentiometer reaches its end stop because its input range is limited; further rotation then leaves the frequency unchanged. Chip-specific ranges are described in the ADC documentation.
Tone control and startup behaviour
ledcAttach(PWM_PIN, 2000, 10) prepares the peripheral; PWM_PIN is 26. While the button is stably pressed, ledcWriteTone() requests the current frequency. Zero frequency stops the output. playing indicates whether a tone is active.
Button debouncing takes 30 ms, including release. A button held during reset must first be released. If PWM attachment fails, pwmReady remains false, the output stays off, and Serial reports an error. A failed nonzero frequency request calls stopOnPwmFailure(), disabling the output until reset. Do not bypass an error by wiring the sounder directly to a GPIO.
Running and experimenting
Hold the button and turn the potentiometer slowly. Expect the pitch to change, possibly together with loudness. Release the button and confirm that sound stops. Serial reports the raw reading, target frequency and playing state approximately every 500 ms.
| Action | Expected result |
|---|---|
| Turn the potentiometer without pressing | Readings change, but there is no sound. |
| Hold the button and turn slowly | Pitch follows the changing target frequency. |
| Release the button | Sound stops after the input stabilises. |
| Reset while holding the button | Silence until release followed by another press. |
Record your own observations. The reported frequency is the software's requested value, not an independently measured acoustic frequency.
Troubleshooting
| Symptom | Check |
|---|---|
| Pitch hardly changes | Confirm a passive piezo part and changing rawValue; an active buzzer is unsuitable. |
| No sound | Check B/C/E, power, button input, PWM error messages, and both R3 and R4 against the schematic. |
| Sound is weak at some positions | A real transducer's response varies with frequency; inspect its datasheet. |
| Frequency jumps | Check the RV1 wiper, ground and contacts; do not leave the analogue input floating. |
| Maximum arrives before full rotation | Compare rawValue with 4095; ADC saturation may explain it. |
Challenge and worked answers
Change the range to 400–2400 Hz. Use 400 + raw × 2000 / 4095, because the span is 2400 − 400 = 2000. A raw reading of 0 gives 400 Hz, 4095 gives 2400 Hz, and 2048 gives 1400 Hz.
Does doubling frequency double loudness? No: frequency controls pitch, while loudness depends on excitation and the transducer's response. Why use both R3 and R4? R3 discharges the capacitive element; R4 limits pulses through the transistor and reduces drive voltage. Why does BZ1's negative terminal rise towards 5 V when the transistor turns off? R3 equalises the two terminal potentials, reducing the voltage across the piezo towards zero.
