Buzz It, Baby!
Use the buzzer to have your MCU play small wake-up melodies, signals, or music to accompany LED animations. The sound might not be a feast for the ears, but it does give your projects a great retro feel.
Every PC has at least a small beeper for signal tones. In the case of microcontrollers, you need to retrofit a buzzer to do this, but buzzers are too good for just outputting beeps. They are available for a small amount from the typical marketplaces, and you can choose between two basic types: active and passive. The active relies on a built-in oscillator to generate sound and only require two pins, while you need to feed a PWM signal to the passive. Besides voltage and earth, they require another pin for the signal.
The buzzers themselves look like small black pots with two pins (Figure 1, center and right). For active buzzers, you just need the component itself, but for passive buzzers, it makes sense to use a breakout with the required circuitry (Figure 1, left). There are also breakouts for active buzzers with three pins, but one of them is not assigned in this case. When purchasing, you can use the designation as a guide: Passive buzzers will be known as KY-006 or HW-508, while an active variant often has KY-012 in its name.

Because the parts are inexpensive, it’s a good idea to grab a handful of them. A single buzzer is fine for simple tone sequences, but it’s more fun if you have several of them so that you can create entire chords instead of just individual tones.
Pulse Width Modulation
PWM signals are levels that are switched on and off at fixed intervals, resulting in a typical square wave curve (Figure 2). The pitch of the buzzer output depends on the frequency. You could simply generate these PWM signals by switching a GPIO pin, but that would be painstaking and unnecessary. All microcontrollers have built-in hardware units for this; you just have to feed them the right parameters. In addition to the frequency, which is decisive for the pitch, this also includes the duty cycle, i.e., the proportion of active time that determines the volume.

With some microcontrollers, only selected pins support this function, but on the Pi Pico almost all of them do. There is only one restriction with the Foundation MCU: Pin n and pin n+1 use the same PWM channel for an even n. The Pi Pico has a total of eight channels. Other MCUs have other restrictions and limits.
We will be using a Pi Pico as the basis for our buzzer experiments. The wiring for four buzzers is shown in Figure 3; use 5V as the voltage source (VBUS/VUSB).

Listing 1 shows an example for the kind of code you can use to generate tones. It will look slightly different in other programming languages; the Arduino world even has its own tone()
function for this. A complete buzzer music library (for CircuitPython) including examples is available from my Github repository. The action happens in lines 7 to 10 of Listing 1. The tone()
method first sets the pitch (frequency
) and the volume (duty_cycle
). Then the function goes to sleep for the duration
of the sound before setting the volume to zero (DUTY_OFF
).
Listing 1: PWM control
01 DUTY_ON = 65535
02 DUTY_OFF = 0
03 VOL_DIV = [200, 100, 67, 50, 40, 33, 29, 22, 11, 2]
04 class AsyncBuzzer:
05 async def tone(self, pitch, duration, volume=10):
06 await self._lock.acquire()
07 self._pwm.frequency = PITCH[pitch]
08 self._pwm.duty_cycle = int(DUTY_ON / VOLDIV[volume‑1])
09 await asyncio.sleep(duration)
10 self._pwm.duty_cycle = DUTY_OFF
11 self._lock.release()
The rest of the method (including async
, await
, and locks
) is additional icing that enables asynchronous execution. The Python and Asyncio box gives you a crash course on this. Experts will argue that asynchronous routines do not guarantee the sound duration; but, with just a little due care and attention, it works surprisingly well for our modest lo-fi requirements.
Asyncio is a framework for cooperative multitasking. In contrast to operating systems, where a scheduler assigns time slices to complete programs, in an asynchronous program it is the individual program parts themselves that stop at a suitable point and give priority to other code sections.
Co-routines, which you define with the
async
keyword (line 5 in Listing 1, lines 8 and 14 in Listing 2), are the central elements. Calling a method of this type does not execute any code; instead it returns a co-routine object. The method only runs when it is wrapped in a task. This often happens implicitly, for example, with the await
keyword (Listing 2, line 11) or with asyncio.gather()
(line 17) for several co-routines at the same time.If it encounters an
await
statement during execution, the Asyncio scheduler can stop the method and send something else. In Listing 1, this happens in lines 6 and 9. In the first case, the method waits for a lock, in the second case it sleeps for the duration of the sound. The specified sleep duration is a minimum value: If other parts of the program do not release the processor in time, the nap can also last (slightly) longer.This whole idea is particularly useful on microcontrollers which typically lack an operating system and execute code sequentially. Parallel processes – such as an LED animation while
asyncio.gather()
plays music – are far easier to program with the Asyncio framework. Asyncio works particularly well whenever different parts of the program need to sleep or wait for something (such as data).First Chords
On the basis of the code module from Listing 1, playing around with chords is no problem at all. Listing 2 plays a C major chord on one (lines 8 to 12) or four buzzers (lines 14 to 19).
Listing 2: Playing Chords
01 import board
02 import asyncio
03 from buzzer_music.async_buzzer import AsyncBuzzer
04 # notes: (pitch, duration, volume),...
05 NOTES = [('C4', 1 , 10), ('E4' , 1, 10), ('G4', 1, 10), ('C5', 1, 10)]
06 PINS = [board.GP18, board.GP17, board.GP15, board.GP13]
07
08 async def mono():
09 buzzer = AsyncBuzzer(PINS[0])
10 for note in NOTES:
11 await buzzer.tone(*note)
12 buzzer.deinit()
13
14 async def poly():
15 buzzers = [AsyncBuzzer(pin) for pin in PINS]
16 coros = [buzzer.tone(*note) for buzzer,note in zip(buzzers, NOTES)]
17 await asyncio.gather(*coros)
18 for buzzer in buzzers:
19 buzzer.deinit()
20
21 asyncio.run(mono())
22 asyncio.run(poly())
Using the code from the mono()
method, you could actually play complete melodies – but there would be no rests. And the notation is very tedious. Writing down entire melodies as a list of tuples, as shown in line 5, may be just about acceptable for “Happy Birthday” or other short snippets, but it’s not exactly convenient.
Fortunately, there is an online source from which you can download music in a (more or less) suitable format. The Onlinesequencer.net website provides a whole range of stuff from tone sequences to complete musical works. If, for example, you’re looking for the old Nokia ringtone, you’ve come to the right place. A search for “Telekom” will reveal the minimalist original version with five tones, but also more creative arrangements with up to 1,157 tones. Note that not everything that you find online is in the public domain.
You can access the search via Sequences (the first tab in Figure 4). Clicking on one of the match tiles takes you to playback mode where you can listen to the piece of music. On the right there is a download link to the matching MIDI file. This should actually contain all the relevant information, but I have not yet managed to reliably extract the data.

Fortunately, there is an alternative approach. Start by switching to edit mode in the Online Sequencer (pencil symbol, Most Notes), select all the notes using the Select All button (Figure 5) and then copy them by pressing Ctrl+C. Then paste the notes into a text file or directly into your program. The complete piece of music consists of a long string in the format …;start pitch duration instrument;… for the individual notes.

The raw data is not suitable for direct playback, but there is no need to convert the tracks manually. All you need is the helper script from my own Buzzer Music project. A Python class is used for playback; it reads the notes and outputs them on one or more buzzers.
Tips & Tricks
A few practical tips and tricks: Not every buzzer sounds the same, and not every piece of music is suitable for buzzer playback. This is primarily due to the fact that the frequency range of these small components is limited. Buzzers tend to hum at low frequencies and beep at high frequencies.
In general, chords sound worse than melodies because the buzzers don’t hit the pitch perfectly. Simpler versions of a piece are likely to work better than more complex ones. You’ll also run into problems when you try to play more simultaneous notes than there are buzzers available: The routine then has the choice between dropping notes or waiting for a free buzzer – and neither sounds good.
Conclusions
Music played on buzzers is not a feast for the ears, but it can spice up your projects with signals, wake-up melodies, or as an accompaniment to LED animations. Singing birthday cards work on the same principle. So why not decorate a gift so that the recipient’s favorite melody plays when they open it or make a kitchen alarm clock that can do more than just beep?