Skip to main content

2.4 Buzzer Beep

The goal of this example is to make some noise. A passive buzzer is a tiny speaker: feed it a square wave and it vibrates at whatever frequency you send it, so a single buzzer can play any note you like.

In this example the NULA MINI plays a short eight-note scale the moment you run the script.

In this documentation you will learn:

  • How to connect a passive buzzer to the board.
  • How PWM turns a frequency into a sound.
  • What duty_u16() controls, and why 32768 is the number to use.
  • How to keep a melody in a list and step through it with a loop.

Hardware required:

  • 1x Soldered NULA MINI board
  • 1x Breadboard
  • 1x Passive buzzer
  • 3x Jumper wires
  • 1x USB-C cable
ℹ️
Unlike the LED examples, this one needs no resistor. The buzzer sits straight between IO5 and ground. There is nothing to protect it from, because the pin only ever swings between 0 V and 3.3 V.
⚠️
Make sure you are holding the passive buzzer and not an active one. A passive buzzer produces whatever frequency you send it, which is exactly what this example relies on. An active buzzer contains its own oscillator and can only beep at one fixed pitch, so the melody would come out as a single flat tone.

Putting the components together

Follow the five steps below. Each photo is taken from the same position, so you can compare it with the previous one and see exactly what changed.

ℹ️
This example only reaches two pins on the board: IO5 in row 28 and GND in row 30, both on the f–j side. The board body covers columns f–i, so j28 and j30 are the holes you can actually get a wire into.

1. Insert the NULA MINI board on the breadboard

ℹ️
This step assumes you know how a breadboard is wired inside and what its power rails are. For an introduction, see Breadboard Fundamentals documentation page.

Push the board into one end of the breadboard so that its two rows of pins sit on either side of the centre channel, with the chip facing down.

NULA MINI board seated on the breadboard
Step 1: the board seated on the breadboard

2. Place the buzzer and bring ground out to the rail

The buzzer goes on the f–j side, well clear of the board, with its two legs in row 14 and row 15.

Look at the top of the case of the buzzer before you push it in. Next to the HWDZ moulding there is a small + inside a circle, and the leg on that side of the case is the positive one. That leg goes into row 15.

Then run a jumper from j30 (GND) across to the blue rail. That rail is the ground line for the rest of the example.

Passive buzzer in rows 14 and 15, with a jumper from j30 to the negative rail
Step 2: the buzzer bridging rows 14 and 15, and GND wired to the blue − rail
⚠️
The two legs sit in neighbouring rows, so take a moment to check that they really did land in two different rows. If both legs end up in the same row the buzzer is short-circuited and stays silent no matter what the code does.

3. Connect the positive leg to IO5

One jumper from j15 (the row holding the + leg of the buzzer) down to j28 (IO5). This is the wire that will carry the square wave.

Jumper from j15 to j28, connecting the positive leg of the buzzer to IO5
Step 3: the + leg of the buzzer wired to IO5 in row 28

4. Connect the other leg to ground

One more jumper, from j14 (the other leg of the buzzer) across to the blue rail, which is already tied to GND.

Jumper from j14 to the negative rail, completing the circuit
Step 4: the other leg of the buzzer wired to the blue − rail, completing the circuit

The loop is now closed: IO5 → buzzer → GND. Whatever IO5 does, the buzzer feels.

5. Connect the board to your computer

The finished buzzer circuit powered over USB-C
Step 5: the finished circuit, powered over USB-C
ℹ️
Rows 14 and 15 are only the rows the photos happen to use. Any two free rows work, as long as the legs of the buzzer sit in two different rows, IO5 reaches the + row, and the other row reaches ground.

How a passive buzzer makes a note

Sound is nothing more than air being pushed back and forth. Inside the buzzer is a thin disc that moves whenever the voltage across it changes, and moving it quickly enough makes a note you can hear.

That is what PWM does. PWM stands for Pulse Width Modulation: it switches a pin on and off very quickly, and lets us choose both how fast it switches and what fraction of the time it stays on. The number of switches per second is the frequency, measured in hertz, and your ear reads that frequency as pitch:

NoteFrequency (Hz)
C4262
D4294
E4330
F4349
G4392
A4440
B4494
C5523
ℹ️
A higher frequency gives a higher pitch, a lower frequency a deeper one. The eight values above are one octave of a C major scale, which is exactly the melody this script plays.

MicroPython has no tone() function. Instead you create a PWM object on the pin and then drive it with two calls:

buzzer = PWM(Pin(BUZZER_PIN))

buzzer.freq(262) # choose the pitch
buzzer.duty_u16(32768) # start the sound
buzzer.duty_u16(0) # stop the sound

freq() sets the pitch. duty_u16() sets what fraction of the time the pin stays on, as a number from 0 (always off) to 65535 (always on). Half of that, 32768, is an even on-off switching, which gives a buzzer its clearest tone. Writing 0 leaves the pin permanently off, which is silence.

ℹ️
Creating the PWM object already claims the pin, so unlike a plain Pin you do not set a direction for it. There is no Pin.OUT in this example at all.
⚠️
Do not use 65535 to make it louder. A duty of 65535 means the pin is on all the time and never switches, so the disc inside the buzzer stops moving and you hear nothing at all, the same as a duty of 0. Loudness is not what this number controls.

How the melody is stored

A melody is really two lists that travel together: which note, and how long it lasts. The script keeps them in two lists of the same length:

  • melody holds the frequency of each note in hertz.
  • note_duration holds the length of each note, written the way sheet music writes it: 4 for a quarter note, 2 for a half note.

A for loop then walks through both lists one position at a time, plays the note and waits out its duration.

for i in range(len(melody)):
ℹ️
len(melody) is the number of notes in the list, and range() counts from 0 up to just below that number. So i takes the value of each position in turn, and we use it to read the matching entry out of both lists. Written this way the loop stays correct even after you add a note of your own. There is no equivalent of the Arduino sizeof() arithmetic to keep in step.

When the loop finishes we release the pin:

buzzer.deinit()
ℹ️
deinit() hands the pin back and leaves the buzzer silent for good. Without it the PWM hardware stays claimed even after the program ends, which can leave the buzzer humming after the script has stopped.

Code

# PWM stands for Pulse Width Modulation. It switches a pin on and off very quickly, and we can choose both how fast
# it switches and what fraction of the time it stays on. Feeding that fast switching into a buzzer is what makes it
# produce a sound, and the switching speed is what we hear as the pitch.
from machine import Pin, PWM
import time

# This is a variable to which we assign the number of the pin that we connected the buzzer to.
# The NULA board has a pin naming logic as follows: IO5, where 5 is the number that we give to the variable.
BUZZER_PIN = 5

# We will use two lists - one for note frequencies (in Hertz) and one for note durations.
# A frequency is how many times per second the buzzer moves back and forth, and our ears hear it as the pitch of
# the note: the higher the number, the higher the note.
melody = [262, 294, 330, 349, 392, 440, 494, 523] # C4 to C5
note_duration = [4, 4, 4, 4, 4, 4, 4, 2] # Quarter notes (last one is half note)

# Here we create our PWM object, which we named "buzzer". Creating it already claims the pin, so unlike a plain Pin
# we do not set a direction for it.
buzzer = PWM(Pin(BUZZER_PIN))

# duty_u16() sets what fraction of the time the pin stays on, as a number from 0 (always off) to 65535 (always on).
# Half of that, 32768, is the even on-off switching that gives a buzzer its clearest tone.
SOUND_ON = 32768
SOUND_OFF = 0

# Start with the buzzer silent, so it does not sound before the melody begins.
buzzer.duty_u16(SOUND_OFF)

print("Playing melody...")

# A for loop repeats a block of code once for every item it is given. range(len(melody)) counts from 0 up to the
# number of notes in our list, so "i" ends up being the position of each note in turn.
for i in range(len(melody)):

# Musical note lengths are written as fractions: a quarter note is a quarter of a whole note. Here we turn that
# fraction into milliseconds by dividing one second by the number in the list, so a 4 becomes 250 ms and a 2
# becomes 500 ms.
duration = int(1000 / note_duration[i])

# freq() sets how fast the pin switches, which is the pitch of the note, and duty_u16() then starts the sound.
buzzer.freq(melody[i])
buzzer.duty_u16(SOUND_ON)

# Play the note for its full length.
time.sleep_ms(duration)

# Now we stop the sound and wait a little longer before the next note. That extra silence keeps the notes from
# running into each other. Feel free to experiment with the 0.3.
buzzer.duty_u16(SOUND_OFF)
time.sleep_ms(int(duration * 0.3))

# deinit() releases the pin once we are finished with it, leaving the buzzer silent for good.
buzzer.deinit()
print("Melody finished!")

What you should see

Press Run and the buzzer plays a rising scale of eight notes, just under three seconds in all. Then it goes quiet.

ℹ️
That length is worth checking against the lists, because it is the two of them added together. The seven quarter notes are 250 ms each and the final half note is 500 ms, which is 2250 ms of sound; the extra silence after each note adds another 30 % on top, so the whole melody takes 2930 ms on the board. If you shorten the 0.3 the scale runs faster without any of the notes changing pitch.

There is no endless loop in this script, so the melody plays once and the program ends. To hear it again, press Run again.

The Shell shows the same thing in text:

Playing melody...
Melody finished!
ℹ️
If both messages appear but you hear nothing, the code is fine and the wiring is not. Check that the legs of the buzzer are in two different rows, and that the jumper from IO5 lands in the same row as one of them.

Try it yourself: change one of the numbers in melody and run it again. Because the loop counts with len(melody), you can also add notes to both lists and it will pick them up with no other change.


Full example

Check out the full example code on the link below:

2.4_Buzzer_Beep.py

Example that shows how to use PWM to play a simple melody on a passive buzzer connected to IO5.