Skip to main content

7.2 Mini Piano

This project turns four buttons and a passive buzzer into a small piano. Each button gets a note of its own, and holding it down plays that note for as long as you keep your finger there.

Nothing here is new. The buttons are read exactly as in 2.1 Button Counter, and the buzzer is driven exactly as in 2.4 Buzzer Beep. What makes it a project is the scale: four inputs and one output on the breadboard at once, and the first build in this kit that uses pins on both sides of the board.

In this documentation you will learn:

  • How to wire four buttons at once, each to its own pin
  • How to join the two blue rails into a single shared ground
  • How to write your own small functions to keep a loop readable
  • Why an elif chain means only one note can sound at a time

Hardware required:

  • 1x Soldered NULA MINI board
  • 1x Breadboard
  • 4x Push buttons
  • 1x Passive buzzer
  • 12x Jumper wires
  • 1x USB-C cable
ℹ️
This example needs no resistors at all. Not for the buttons: they use the resistors built into the chip, switched on in code with Pin.PULL_UP. And not for the buzzer: the pin only ever swings between 0 V and 3.3 V, so there is nothing to protect it from. If you have seen a piano circuit drawn with four 10 kΩ resistors, that is a different way of wiring buttons and it is not what this script expects.
⚠️
Make sure you are holding the passive buzzer and not an active one. A passive buzzer plays whatever frequency you send it, which is the whole point here. An active buzzer contains its own oscillator and can only beep at one fixed pitch, so every button would sound identical.

Putting the components together

This build uses five pins on the board, and they are split across both sides of it:

PinRowSideWhat it does
IO225f–jButton 1, note C4
IO326f–jButton 2, note D4
IO427f–jButton 3, note E4
IO528f–jButton 4, note F4
IO1828a–eBuzzer
GND30f–jGround for everything
⚠️
Row 28 appears twice in that table, and the two entries are different pins. On the f–j side row 28 is IO5, one of the buttons. On the a–e side row 28 is IO18, the buzzer. They sit at opposite ends of the same row number and are not connected to each other. Read the name printed on the board, never the row number on its own.

The board body covers the middle columns, so beside it only column a on the a–e side and column j on the f–j side are free. Every wire that touches the board therefore goes into an a… or a j… hole.

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

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. Press it in evenly until all the pins are seated.

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

In the photos that follow, the board occupies rows 25 to 30.

ℹ️
Your board may sit in different rows than the one in the photos. What matters is the row each pin lands in. Read the names printed along the edges of the board rather than copying the numbers.

2. Bring ground out to the blue − rail

One short jumper from j30 (the row holding GND) across to the blue rail on the f–j edge.

Jumper from j30 out to the blue negative rail on the f-j edge
Step 2: GND wired out to the blue − rail
ℹ️
The wire has to go into the hole row running alongside the blue line. Each edge of the breadboard carries two rail rows, and the one beside the red line is a separate strip that is not connected to it.

3. Join the two blue − rails together

The buttons and the buzzer will take their ground from the a–e edge, but GND came out on the f–j edge. So the two blue rails have to be joined into one.

Run a jumper from the blue rail on the f–j edge, around the far end of the breadboard, to the blue rail on the a–e edge. Put it down near row 1, well clear of everything else.

Jumper bridging the two blue negative rails at the row 1 end of the breadboard
Step 3: the two blue − rails joined into a single ground line
ℹ️
The rails along the two edges of a breadboard are not connected inside it: each strip runs the length of one edge and stops there. This one wire is what turns them into a single ground line that both sides of the board can reach. Every ground connection from here on simply goes to the nearer blue rail.

4. Place the four push buttons

Push the four buttons into the middle of the breadboard so that each one straddles the centre channel, a few rows clear of the board. Each button spans two rows.

In the photos they sit in rows 18 and 20, 15 and 17, 12 and 14, and 9 and 11, nearest the board first, three rows apart, which is comfortable spacing for four fingers.

Four push buttons placed across the centre channel
Step 4: the four buttons straddling the centre channel
⚠️
A push button has four legs, but only two of them matter. The two legs on the same side of the button are permanently joined together inside it. Pressing the button connects one side to the other. So for each button always use one leg from the a–e side and one from the f–j side, in its two different rows. If you take both wires from the same side, the two are already connected and the board will behave as if that button is held down forever.

5. Ground each button

Four short jumpers, one per button, from the a–e side of the button out to the blue rail on that edge. In the photo those are rows 20, 17, 14 and 11, the higher row of each pair.

Four jumpers connecting each button to the blue negative rail on the a-e edge
Step 5: all four buttons grounded to the blue − rail

6. Connect each button to its own pin

Now the four signal wires, all on the f–j side. Each runs from a board pin to the lower row of the pair of one button:

FromToButtonNote
j25 (IO2)j181C4, 262 Hz
j26 (IO3)j152D4, 294 Hz
j27 (IO4)j123E4, 330 Hz
j28 (IO5)j94F4, 349 Hz
Four signal jumpers running from the board pins out to the four buttons
Step 6: each button wired to its own pin, IO2 to the button nearest the board

Notice that the four wires run in parallel and never cross. IO2, the pin nearest the buttons, goes to the nearest button; IO5, the furthest pin, goes to the furthest button. Keeping that order is what makes the notes climb as your hand moves away from the board, and it is worth double-checking, because all four wires look alike once they are in.

ℹ️
Each button now has ground on one side of the centre channel and its own pin on the other. That is the same diagonal pair used for the single button in 2.1 Button Counter, just repeated four times.

7. Place the buzzer and wire it up

The buzzer goes on the a–e side, at the far end of the breadboard, with its legs in two neighbouring rows, rows 3 and 4 in the photo.

Look at the top of its case 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 4.

Then two wires:

  • From a4, the row holding the + leg, to a28, which is IO18. This is the wire that carries the square wave.
  • From a3, the other leg of the buzzer, to the blue rail on the same edge.
Passive buzzer in rows 3 and 4, one wire to IO18 and one to the negative rail
Step 7: the buzzer wired between IO18 and ground
⚠️
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.

The loop is now closed: IO18 → buzzer → GND. The 12 mm case hides the legs once the buzzer is pushed in, so if you need to check the wiring later, read the two jumpers instead.

8. Connect the board to your computer

Plug the USB-C cable into the board. The PWR LED lights up as soon as it has power.

The finished mini piano circuit powered over USB-C
Step 8: the finished circuit, powered over USB-C
ℹ️
All the row numbers above are only the rows the photos happen to use. Any free rows work, as long as each button straddles the channel in two different rows, each button's own pin reaches one side of it and ground reaches the other, and the two legs of the buzzer sit in two different rows with IO18 on the + one.

How four buttons share one buzzer

Reading a button. Each of the four pins is created with Pin.IN and Pin.PULL_UP, which switches on a resistor inside the chip that gently ties the pin to 3.3 V. The pin therefore reads 1 while its button is released, and the job of the button is only to connect that pin to GND, which drags it down to 0.

btn1 = Pin(BTN1, Pin.IN, Pin.PULL_UP)
btn2 = Pin(BTN2, Pin.IN, Pin.PULL_UP)
btn3 = Pin(BTN3, Pin.IN, Pin.PULL_UP)
btn4 = Pin(BTN4, Pin.IN, Pin.PULL_UP)
ℹ️
This is why a pressed button reads 0. That is the opposite of what most people expect. A button wired this way is called active low, and it is the standard way to wire a button to any microcontroller. 2.1 Button Counter covers it in detail.

Making a note. PWM switches IO18 between 3.3 V and 0 V over and over. That is a square wave. The number of switches per second is the frequency, and your ear reads frequency as pitch. Four frequencies, four notes:

NoteFrequency (Hz)
C4262
D4294
E4330
F4349

These are the first four notes of the C major scale, starting from the C in the middle of a piano keyboard. Change the numbers and you change the tuning. There is nothing special about these four values, and looking up the frequency of any other note is enough to build your own scale.

Two small functions keep the loop readable. Where an Arduino sketch has tone() and noTone() ready-made, MicroPython has neither, so the script defines its own pair:

def play_note(frequency):
buzzer.freq(frequency)
buzzer.duty_u16(SOUND_ON)


def stop_note():
buzzer.duty_u16(SOUND_OFF)

Both are two lines long, and neither does anything you have not already seen in 2.4 Buzzer Beep. What they buy you is a loop that reads as play this note rather than as two PWM calls repeated four times over.

ℹ️
This is worth copying as a habit. A def costs almost nothing and turns a chunk of detail into a name, which is the difference between a loop you can read at a glance and one you have to decode.

Only one note at a time. The four checks are joined into a single if / elif chain, so the board stops at the first button it finds pressed and never looks at the rest. Press two keys together and you hear only the one that comes earlier in the chain, which is the lower note. A real piano plays chords; this one cannot, and the elif chain is the reason.

Letting go. When none of the four is pressed the chain falls through to its else and calls stop_note(). That is what makes a note last exactly as long as you hold the key down.

ℹ️

The time.sleep_ms(50) at the end of the loop sets how often the board looks at the keys: 20 times a second. That is what the pause really does, and you can just about feel it: up to 50 ms can pass between your finger landing and the note starting, which takes the edge off fast playing.

The comment in the code calls this debouncing, but that idea is borrowed from 2.2 Button Debounce and does not really apply here. Debouncing matters when a program counts presses or toggles something on an edge, because there one noisy press can register as two. This loop counts nothing and remembers nothing: on every pass it simply asks whether a key is down right now, and plays or stops accordingly. There is no press for the board to read twice.


Code

# PWM switches a pin on and off very quickly, and feeding that into a buzzer is what makes it produce a sound. The
# switching speed is what we hear as the pitch.
from machine import Pin, PWM
import time

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

# These are the variables to which we pass the numbers of pins that we had connected the four BUTTONS to. Each
# button gets a pin of its own, because the board has to be able to tell them apart.
BTN1 = 2
BTN2 = 3
BTN3 = 4
BTN4 = 5

# These are the frequencies of the four notes, in Hertz. A frequency is how many times per second the buzzer moves
# back and forth, and it is what our ears hear as the pitch of the sound: the higher the number, the higher the note.
# The names come from the musical scale, where C4 is the C in the middle of a piano keyboard.
NOTE_C4 = 262
NOTE_D4 = 294
NOTE_E4 = 330
NOTE_F4 = 349

# Here we create our four Pin objects for the buttons. Pin.IN tells the board that these pins should read a value
# instead of writing one, and Pin.PULL_UP switches on a resistor inside the chip that ties each pin to 3.3V while
# its button is released. That means a pin reads high (1) when its button is up and low (0) while it is pressed.
btn1 = Pin(BTN1, Pin.IN, Pin.PULL_UP)
btn2 = Pin(BTN2, Pin.IN, Pin.PULL_UP)
btn3 = Pin(BTN3, Pin.IN, Pin.PULL_UP)
btn4 = Pin(BTN4, Pin.IN, Pin.PULL_UP)

# Here we create our PWM object for the buzzer. 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 is the even on-off switching that gives a buzzer its
# clearest tone.
buzzer = PWM(Pin(BUZZER_PIN))
SOUND_ON = 32768
SOUND_OFF = 0

# Start with the buzzer silent, so it makes no noise before any button is pressed.
buzzer.duty_u16(SOUND_OFF)


def play_note(frequency):
# This is a function we wrote ourselves. It starts a note and leaves it playing.
# freq() sets how fast the pin switches, which is the pitch, and duty_u16() then starts the sound.
buzzer.freq(frequency)
buzzer.duty_u16(SOUND_ON)


def stop_note():
# This function stops whatever sound the buzzer was making, which is what makes the note stop as soon as you let
# go of the button.
buzzer.duty_u16(SOUND_OFF)


while True:

# value() is a function that reads the value from a pin, either 1 (high) or 0 (low). Because of the pull-up
# resistors, a pressed button reads 0.
# Notice the "elif" chain: the board checks the buttons in order and stops at the first one it finds pressed, so
# pressing two buttons at once plays only the note that comes first in this list.
if btn1.value() == 0:
play_note(NOTE_C4)
elif btn2.value() == 0:
play_note(NOTE_D4)
elif btn3.value() == 0:
play_note(NOTE_E4)
elif btn4.value() == 0:
play_note(NOTE_F4)
else:

# If none of the buttons is pressed we end up here, and the buzzer goes silent.
stop_note()

# time.sleep_ms() pauses the program for the given number of milliseconds. This very short pause gives the
# button contacts a moment to settle, which stops a single press from being read as several.
time.sleep_ms(50)

What you should see

Press Run. Nothing happens at all until you press a key. The buzzer is silent, and nothing is printed, because this script never calls print(). The only output is sound.

Press the button nearest the board. You should hear a steady tone that lasts exactly as long as you hold it, and stops the moment you let go. Work your way outwards and the pitch climbs: C4, D4, E4, F4.

A finger pressing one of the four buttons on the finished mini piano
Holding a key down: the note sounds for as long as your finger stays there

Things worth trying once it works:

  • Hold two keys at once. Only the lower of the two sounds, because of the elif chain.
  • Play quickly. The short pause in the loop means the board only looks at the keys 20 times a second, so very fast playing feels slightly soft-edged.
  • Change a frequency. Edit one of the NOTE_ values, run it again, and that key is retuned.
ℹ️
No sound from any key? Check the two ground wires first: j30 out to the blue rail, and the wire joining the two blue rails. Without that second wire nothing on the a–e edge is grounded, so no button can pull its pin low and the buzzer has no return path. One key silent while the others work? The two wires of that button are probably on the same side of the centre channel, or its legs landed in one row instead of two.
ℹ️
Notes in the wrong order? Two of the four signal wires have been swapped. They are all the same colour once fitted, so trace them back to the board and check that IO2 reaches the button nearest the board and IO5 the furthest.
ℹ️
A note that keeps sounding after you press Stop? Stopping the script leaves the PWM hardware exactly as it was, so a note that was playing carries on. Run it again and let go of the keys, or reset the board.

Full example

Check out the full example code on the link below:

7.2_Mini_piano.py

Project that creates a simple piano using 4 buttons and a passive buzzer. Each button produces a unique note.