Aus Raspberry Pi Geek 05/2025

Buzzer-Musik: Einfache Melodien mit dem Pi Pico

© Kundra / Fotolia

Buzz it Baby!

Bernhard Bablok

Per Buzzer lassen Sie Ihre MCU kleine Weckmelodien, Signale oder Musik als Begleitung zu LED-Animationen abspielen. Das ist zwar kein Ohrenschmaus, peppt aber Projekte deutlich auf.

Summer, auf Englisch Buzzer, gibt es für wenig Geld auf den gängigen Marktplätzen. Zwei Typen buhlen um Ihre Gunst: aktive und passive Buzzer. Erstere erzeugen den Ton über einen eingebauten Oszillator und kommen mit zwei Pins aus. Letztere müssen Sie mit einem PWM-Signal füttern; sie benötigen neben Spannung und Masse noch einen weiteren Pin für das Signal.

Die Buzzer selbst sehen aus wie kleine schwarze Töpfchen mit zwei Pins (Abbildung 1, Mitte und rechts). Bei aktiven Buzzern genügt das reine Bauteil völlig. Bei passiven Buzzern greifen Sie am besten gleich zu einem Breakout, das die notwendige Schaltung mitbringt (Abbildung 1 links). Allerdings gibt es auch Breakouts für aktive Buzzer mit drei Pins, jedoch ist einer davon dann aber nicht belegt. Beim Kauf können Sie sich unter anderem an der Bezeichnung orientieren: Passive Buzzer laufen unter KY-006 oder HW-508, die aktive Variante trägt oft den Bestandteil KY-012 im Namen.

Abbildung 1: Neben aktiven Buzzern (drei Pins, links) gibt es auch passive Exemplare (zwei Pins, rechts).

Abbildung 1: Neben aktiven Buzzern (drei Pins, links) gibt es auch passive Exemplare (zwei Pins, rechts).

Da die Teile ausgesprochen preiswert sind, besorgen Sie sich am besten gleich eine Handvoll davon. Für einfache Tonfolgen genügt ein einzelner Buzzer, aber mit mehreren macht es mehr Spaß: Dann können Sie nicht nur Einzeltöne erzeugen, sondern ganze Akkorde.

Pulsweitenmodulation

Bei PWM-Signalen handelt es sich um in festen Abständen an- und ausgeschaltete Pegel, es entsteht eine typische Rechteckkurve (Abbildung 2). Je nach deren Frequenz gibt der Buzzer eine andere Tonhöhe aus. Solche PWM-Signale könnten Sie durch das Schalten eines GPIO-Pins einfach selbst erzeugen, aber das wäre mühsam und obendrein unnötig: Alle Mikrocontroller haben eingebaute Hardwareeinheiten dafür, die Sie nur mit den richtigen Parametern füttern müssen. Neben der für die Tonhöhe entscheidenden Frequenz zählt dazu noch der sogenannte Duty Cycle. So nennt man neuhochdeutsch den Anteil an aktiver Zeit, der die Lautstärke bestimmt.

Abbildung 2: Ein PWM-Signal besteht aus einer Rechteckkurve, deren Frequenz die Tonhöhe bestimmt.

Abbildung 2: Ein PWM-Signal besteht aus einer Rechteckkurve, deren Frequenz die Tonhöhe bestimmt.

Bei manchen Mikrocontrollern unterstützen nur ausgewählte Pins diese Funktion, beim Pi Pico dagegen fast alle. Bei der MCU der Foundation gibt es nur eine einzige Einschränkung: Für ein geradzahliges n nutzen Pin n und Pin n+1 denselben PWM-Kanal. Insgesamt verfügt der Pi Pico über acht Kanäle. Andere MCUs haben andere Einschränkungen und Limits.

Als Basis für unsere Buzzer-Experimente dient ein Pi Pico. Die Verkabelung für vier Buzzer zeigt Abbildung 3. Als Spannungsquelle wählen Sie 5 Volt (VBUS/VUSB).

Abbildung 3: Unser Lo-Fi-Orchester umfasst vier verkabelte aktive Buzzer.

Abbildung 3: Unser Lo-Fi-Orchester umfasst vier verkabelte aktive Buzzer.

Töne erzeugen Sie mit Code wie dem in Listing 1. Das sieht in jeder Programmiersprache etwas anders aus, die Arduino-Welt kennt sogar eine eigene tone()-Funktion dafür. Eine komplette Buzzer-Musik-Bibliothek (für CircuitPython) samt Anwendungsbeispielen erhalten Sie über das Github-Repository [1] des Autors.

Listing 1

PWM-Steuerung

DUTY_ON = 65535
DUTY_OFF = 0
VOL_DIV = [200, 100, 67, 50, 40, 33, 29, 22, 11, 2]
class AsyncBuzzer:
  async def tone(self, pitch, duration, volume=10):
    await self._lock.acquire()
    self._pwm.frequency = PITCH[pitch]
    self._pwm.duty_cycle = int(DUTY_ON / VOLDIV[volume-1])
    await asyncio.sleep(duration)
    self._pwm.duty_cycle = DUTY_OFF
    self._lock.release()

Das Entscheidende passiert in den Zeilen 7 bis 10 von Listing 1. Zuerst setzt die Methode die Tonhöhe (frequency) und Lautstärke (duty_cycle). Danach schläft die Funktion für die Tondauer (duration) und schaltet danach die Lautstärke auf null (DUTY_OFF).

Der Rest der Methode (mit async, await und Locks) ist zusätzlicher Zucker, der die asynchrone Ausführung ermöglicht. Einen Crashkurs dazu gibt der Kasten “Python und Asyncio”. Experten werden einwenden, dass asynchrone Routinen die Tondauer nicht garantieren, aber mit etwas Sorgfalt funktioniert das für unsere bescheidenen Lo-Fi-Ansprüche überraschend gut.

Python und Asyncio

Asyncio ist ein Framework für kooperatives Multitasking. Im Gegensatz zu Betriebssystemen, bei denen ein Scheduler Zeitscheiben an komplette Programme vergibt, sind es innerhalb eines asynchronen Programms die einzelnen Programmteile selbst, die an geeigneter Stelle anhalten und anderen Codestellen den Vorrang geben.

Als zentrales Element dienen Coroutinen, die Sie mit dem Schlüsselwort async definieren (Zeile 5 in Listing 1, Zeile 8 und 14 in Listing 2). Der Aufruf einer solchen Methode führt dann keinen Code aus, sondern liefert nur ein Coroutinen-Objekt zurück. Erst eingepackt in einen Task läuft die Methode los. Das passiert oft implizit, etwa mit dem Schlüsselwort await (Listing 2, Zeile 11) oder für mehrere Coroutinen auf einmal mit asyncio.gather() (Zeile 17).

Stößt er bei der Ausführung auf ein await-Statement, kann der Asyncio-Scheduler die Methode anhalten und etwas anderes losschicken. In Listing 1 passiert das in den Zeilen 6 und 9. Im ersten Fall wartet die Methode auf einen Lock, im zweiten Fall schläft sie kooperativ für die Dauer des Tons. Bei der angegebenen Schlafdauer handelt es sich um einen Mindestwert: Geben andere Programmteile den Prozessor nicht rechtzeitig frei, kann das Nickerchen auch (etwas) länger dauern.

Das ganze Konzept ist insbesondere für Mikroprozessoren nützlich: Die haben kein Betriebssystem und arbeiten stur eine Anweisung nach der anderen ab. Parallele Prozesse – etwa eine LED-Animation, während asyncio.gather() Musik spielt – lassen sich mit dem Asyncio-Framework sehr viel einfacher programmieren. Besonders gut funktioniert Asyncio immer dann, wenn verschiedene Programmteile immer wieder schlafen sollen oder auf etwas warten müssen (etwa Daten).

Erste Akkorde

Auf Basis des Codebausteins aus Listing 1 stellen erste Spielereien mit Akkorden keinerlei Problem dar. Listing 2 spielt einen C-Dur-Akkord auf einem (Zeilen 8 bis 12) beziehungsweise auf vier Buzzern (Zeile 14 bis 19).

Listing 2

Akkorde abspielen

import board
import asyncio
from buzzer_music.async_buzzer import AsyncBuzzer
# notes: (pitch, duration, volume),...
NOTES = [('C4', 1 , 10), ('E4' , 1, 10), ('G4', 1, 10), ('C5', 1, 10)]
PINS = [board.GP18, board.GP17, board.GP15, board.GP13]
async def mono():
  buzzer = AsyncBuzzer(PINS[0])
  for note in NOTES:
    await buzzer.tone(*note)
  buzzer.deinit()
async def poly():
  buzzers = [AsyncBuzzer(pin) for pin in PINS]
  coros = [buzzer.tone(*note) for buzzer,note in zip(buzzers, NOTES)]
  await asyncio.gather(*coros)
  for buzzer in buzzers:
    buzzer.deinit()
asyncio.run(mono())
asyncio.run(poly())
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
RASPBERRY PI GEEK KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Raspberry Pi Geek bei Google Play Readly Logo
Nach oben