MusicBox Project Introduction

July 2024

Welcome, I am David! I am a husband, Dad, and engineer. I think that computer programming is fun, especially when I get to write code "close to the metal" (directly interacting with electronics).

My MusicBox project is about using the electronics parts I had laying around in my office to build a small device that would play music and sound effects on small speakers. I'm doing this for fun and learning, and also to share my experiences with other people who might be interested.

These are the main parts of the website:

  • Construction - instructions for how to build my MusicBox device. Mostly for future me, but hopefully helpful to others as well.
  • Programming - the software I wrote to power the MusicBox
  • Design - explanation of how it works and why I made some of the design choices. This is intended to be somewhat educational for those interested.

Enjoy!

This is my little workshop when I was developing the Music Box device: My Handy Workshop

QR code for sharing this website:

QR code for this website

Construction

Parts List

The amount of soldering I did on this project is extremely minimal. Mainly I just soldered two wires onto the speaker so I had some way to connect it to the breadboard. Your battery holder will need wires soldered to it if it doesn't already have them.

Part substitutions:

Batteries

I don't think I needed D-sized batteries. AA batteries might work. They have to supply about 200mA current at 3V. The batteries just supply the current through the speaker. The microncontroller is powered via a USB connector.

Transistors

The 2N4403 and 2N4401 BJT transistors are listed as "obsolete" on some websites. I'm using some left over from my Electrical Engineering student days from the 1990s. The main thing is to use complementary PNP / NPN BJT transistors that can are rated at 600mA and don't have a turnoff time much higher than 225 ns. I use the BJTs in full-saturation switching mode in an H-bridge configuration. The software I wrote assumes a 225 ns turn-off time to avoid shoot-through on the H-bridge. If you substitute transistors that turn off much more slowly than that, then you will need to tweak my software to have a longer dead time when switching, or your circuit will burn up.

I am using BJTs instead of MOSFETs because I could not find MOSFETs that met my requirements:

  • Must be breadboard-friendly with pins at 0.1" / 2.54mm pitch
  • Must quickly switch on and off up to 200mA current
  • Must turn fully on with only 3.3V Vgs

Everything I found when searching for MOSFETs either needed 5V Vgs or higher to turn fully on, had a large gate capacitance such that the microcontroller couldn't switch them quickly, or were surface-mounted parts unusable on a breadboard. You can buy MOSFET drivers to handle the high gate capacitance and level shift from 3.3v to the required gate voltage. But with that additional cost and space and complexity, I might as well just buy an audio amplifier - defeating the whole purpose of this project, which is to build my own!

So I'm using my old BJTs as switches and they work just fine.

Wire Connections

Speaker Driver Schematics

Speaker Driver Schematic

Raspberry Pi Pico pin diagram

(link to the original) Raspberry Pi Pico pin diagram

Connections to make on Raspberry Pi Pico

Pin 1 (UART0 TX): white wire on USB-to-serial adapter (host RX)

Pin 2 (UART0 RX): green wire on USB-to-serial adapter (host TX)

Pin 3 (GND): black wire on USB-to-serial adapter (host ground)

Pin 6 (GPIO4): to resistor driving Q1 (see schematic)

Pin 7 (GPIO5): to resistor driving Q2 (see schematic)

Pin 8 (GND): battery ground

Pin 16 (GPIO12): to resistor driving Q3 (see schematic)

Pin 17 (GPIO13): to resistor driving Q4 (see schematic)

Pin 18 (GND): battery ground

Pin 28 (GND): battery ground

Pin 29 (GPIO22): app pushbutton. Connect other side of pushbutton to ground.

Pin 30 (RUN): reset pushbutton. Connect other side pushbutton to ground.

If you are going to program the Pico, connect it to a host PC via USB cable.

If the Pico is already programmed, then you can connect it to either a PC or to a power adapter. Either way, the Pico gets its power via its USB connection.

Other connections

The battery holder holds 2 D batteries in series. The black wire is ground, the red wire is +3 volts. (assuming your battery holder uses standard wire colors.)

The single-pole double-throw (SPDT) switch is used as a on-off switch for the battery. It has 3 pins that I'll call top, middle, and bottom. Connect top to battery red, middle to the red power row the breadboard, and bottom to nothing.

Connect the battery black wire to the ground row on both sides of the breadboard.

Connect one of the speaker wires to the H-bridge side 1 spkr1 output (see schematic) and the other speaker wire to the H-bridge side 2 spkr2 output (see schematic).

Final Assembly

Don't worry that the "host RX" and "host TX" wires shown in the picture below are both blue instead of white and green, respectively, as described in the connection list above. The picture below was taken of a MusicBox device connected to a Raspberry Pi single-board computer instead of to a PC, so no USB-serial adapter was needed. Instead I just connected directly to the UART RX/TX pins on the Raspberry Pi GPIO header.

Final Assembly

Programming

I wrote the code for the MusicBox project in my favorite programming language: Rust!

Rust Logo

Writing programs for microcontrollers is often called embedded development because microcontroller chips are embedded in other devices such as microwave ovens, watches, clocks, refrigerators, automobiles, airplanes, tractors, drones, and just about any other machine made in the past 20 years.

See: Embedded development with Rust

The MusicBox project uses the RP2040 microcontroller chip in the Raspberry Pi Pico board.

Software I wrote for MusicBox

Sequence diagram showing driver initialization and startup

sequenceDiagram
    pico-rtic-sound-effects->>sonic_ng: init_pacer
    pico-rtic-sound-effects->>sonic_ng: init_driver_t4_voice1
    create participant T4AudioDriver
    sonic_ng ->> T4AudioDriver: new
    sonic_ng -->> pico-rtic-sound-effects: T4SonicVoice
    pico-rtic-sound-effects->>sonic_ng: start
    activate T4AudioDriver

wavegen16 is a Rust module I wrote to efficiently generate audio samples with different waveforms (square, triangle, sine). It takes the sample rate as a generic compile-time parameter and produces 16-bit signed amplitude values.

sonic_ng is a Rust module I wrote to take 16-bit signed audio samples and turn them into PWM driving signals that control my custom A/D converter and audio amplifier.

pico-rtic-sound-effects is a program I wrote to showcase the abilities of my MusicBox device. It builds on my wavegen16 and sonic_ng modules. Connect it to a terminal program running on a PC and press keys, and it makes sounds!

Screenshot of pico-rtic-sound-effects

Here is a 25-second recording of my sound effects demo in action: sound effects demo

Credits

Open-source software libraries I used for my MusicBox project:

  • rp-hal is an open-source project with APIs for accessing the RP2040 peripherals (UART, I2C, SPI, ADC, etc) from Rust.
  • RTIC Real-Time Interrupt-Driven Concurrency - kind of like a real-time OS in Rust for embedded systems.

Design

The goal of my MusicBox project is to have fun creating my own microcontroller-based hardware and software to play sound effects and music on small speakers. I wanted to use the smallest number of parts possible while still getting reasonably good audio quality. Preferably I wanted to use parts I already had on hand in my toolbox.

Turning digital signals into sound

A microcontroller is a digital device. Its output pins only produce binary 1's and 0's, represented as 3.3 volts for a 1 and ground for 0. Sound is analog, continuous in nature, not binary. A speaker is driven by electrical currents that need to vary in proportion to the sounds it is trying to produce. To bridge the divide between digital and analog, we need to do digital-to-analog conversion. The RP2040 microcontroller chip I'm using is really good at PWM (Pulse Width Modulation), so that was what I used for to drive the speaker.

What is PWM?

PWM stands for Pulse Width Modulation. It means rapidly turning a digital output signal on and off for varying amounts of time (pulse widths).

For example, most light dimmers use PWM to control the brightness of a lamp or LED light. The PWM signal turns the light on and off 60 times per second or faster, quicker than the eye can notice. The percent of the time the PWM signal is turned on is called the duty cycle. If the PWM signal is on 10% of the time and off 90% of the time, it has a 10% duty cycle. Applied to the light dimmer, it means the light shines at 10% of its maximum brightness.

See: What is PWM: Pulse Width Modulation

How did I use PWM to drive a speaker?

The Suggested RP2040 PWM Audio Design

I looked at the RP2040 hardware design notes section 3.4.1 PWM Audio. Their suggested design uses a PWM output signal connected to a filter circuit consisting of two capacitors and three resistors:

RP2040 recommended PWM audio design

This design does not meet my requirements:

  • It produces a signal too weak to drive a speaker
  • It therefore needs to be connected to an audio amplifier
  • Buying an amplifier doesn't give me as much DIY joy as making my own
  • Buying an amplifier doesn't make use of the parts I already have

My PWM Speaker Driver Design

I researched audio amplifiers to find out how to build my own audio amplifier to turn a weak, filtered PWM signal into a strong one that could drive a speaker. I found that modern audio amplifiers use a class D amplifier design. The class D design has benefits over the older class A and AB designs because it doesn't experience crossover distortion.

See: What is a a Class-D Amplifier, and What Are They Useful For?

This example class D amplifier design comes from an Analog Devices technical article Class D Amplifiers: Fundamentals of Operation and Recent Developments:

Basic half-bridge Class D amplifier

When I looked at this diagram, that's when I had my "aha!" moment. A class D amplifier turns an analog input signal into a PWM signal, and then uses that PWM signal to rapidly switch a speaker "off" and "on", just like a light switch dimmer or a PWM variable-speed motor controller.

Why use a capacitor filter to turn my PWM output signal into a weak analog signal just so a class D amplifier can turn it back into a PWM signal to drive a speaker? I already have a PWM signal!

My design combines analog to digital conversion and signal amplification in one step, by using the classic H-bridge technique used for controlling motors.

This diagram comes from an article titled "What is an H-Bridge?" and shows the basic layout of an H-bridge motor controller circuit:

H-bridge motor controller with BJT transistors

An H-bridge circuit lets you run a motor backward or forward by changing the direction of current through the motor. By using PWM signals as the inputs to the H-bridge you can control not only the direction but the speed of the motor, by varying the PWM duty cycle.

A motor and a speaker have a lot in common: they are inductive in nature because they basically involve putting a electrical current through a coil of wire. In my case instead of making a motor run backwards and forwards I'm sending current in alternating directions through a speaker to make the speaker diaphram move forwards or backwards. I vary the duty cycle to vary the strength of the current and therefore the power of the audio signal.

Here is my speaker driver circuit design: Speaker Driver Schematic

This design only requires 4 transistors, 4 diodes, 4 resistors, 2 1.5V batteries, and a speaker. I already had these parts on hand.

Why are there diodes in the circuit? As I mentioned above, the speaker has an inductive coil and therefore I need diodes to protect the circuit from inductive kickback.

Avoiding the Hazard of Transistor Burnout

People experienced in electronics might notice that this design requires very careful timing to ensure that for example transistors Q1 and Q2 are never both turned on at the same time. That hazard is called "shoot through" and will cause your transistors to overheat and burn out.

These particular BJT transistors take 225 ns to turn off but only 15 ns to turn on. I programmed the microcontroller to include "dead zone" timing to make sure that for each pair of transistors, one transistor was turned off for 225 ns before the other transistor starts turning on. I carefully checked the timing of the PWM signals with an oscilloscope before connecting the transistor outputs together.

H-bridge deadzone check 1

In the oscilloscope screenshot above:

  • Yellow (channel 1) is Pico pin 6 controlling transistor Q1
  • Red/violet (channel 2) is Pico pin 7 controlling transistor Q2
  • Blue (channel 3) is Pico pin 16 controlling transistor Q3
  • Green (channel 4) is Pico pin 17 controlling transistor Q4

Q1 and Q3 are PNP transistors, which means that a low signal turns them on.

Q2 and Q4 are NPN transistors, which means that a high signal turns them on.

In this oscilloscope trace you can see that the yellow and red signals start out high, meaning Q1 is off and Q1 is on. (If you are confused please go back and read the previous two paragraphs about PNP vs NPN transistors.) At the left vertical dashed line the red signal goes low, meaning Q2 starts to turn off. At the right vertical dotted line, you see that the yellow signal goes low, meaning Q1 starts to turn on. The time difference between the two vertical dashed lines is the "dead zone" when both are turned off. The oscilloscope shows that the dead zone is 230 ns, which meets the requirement that it be at least 225 ns. (N.B. ns = nanosecond, one billionth of a second. These are precise timing requirements!)