When I was in college, I took a one-semester electronics course, and for a final project, we had a few weeks to build pretty much whatever we wanted. As a computer-science major (the only one in the class), obviously I jumped at the chance to do something with microprocessors and digital logic. I ended up building an electronic music box—a device that plays a preprogrammed song over headphones, using a PIC microcontroller.
It was an interesting journey into the (to me) strange and mysterious world of hardware and embedded software, and I got to do some fun things along the way, like etching and soldering my own circuit board. I came across the files again the other day and thought I’d share it.
The only kind of microcontroller we had available in the lab was a PIC 16F84A, so my whole design was based around it. This is a tiny CPU that has very, very basic processing capabilities. It was quite a fun challenge to write software for this device, given its limitations…but more about that later. :)
The PIC has 18 pins, of which 13 are available for programmable I/O. If we connect some of them to an 8-bit digital-to-analog converter, we could in principle synthesize audio directly from the microcontroller. Of course, the more sophisticated the audio synthesis we want to do, the more processing horsepower we’d need, and the PIC hasn’t got much. In fact, even creating a single sine wave with a controllable frequency in software would be pretty difficult!
I decided to go a different route: do the audio synthesis with a separate chip that’s built for the purpose, and use the PIC to control it. After evaluating various options, I settled on the AD9835, and managed to score a couple of free samples.
The AD9835 is a thing called a “numerically-controlled oscillator”—basically a sine wave generator that lets you set the frequency digitally. It’s actually designed for radio applications, so using it for audio synthesis was a bit overkill—but it has a built-in 10-bit DAC, very precise frequency control, and best yet, it could be programmed using a simple serial interface with only 3 pins. That made it perfect for me—I could attach two of them to my PIC, and still have a few pins left for user controls. Two oscillators means the music can have two voices harmonizing—not much, but certainly more interesting than one!
Here’s the final schematic I arrived at, after much breadboard prototyping:
On the left is the power supply and controls. I used a 4-position dipswitch with the intention of letting the user select one of 16 songs, though this didn’t end up working out—the PIC only had memory enough for a couple short songs at a time. There’s also a play button, and an LED that indicates when the song is done playing.
In the middle is the PIC and the two AD9835s, and on the right is the output block. The analog outputs from the two AD9835s are combined, passed through a couple of RC filters in a (probably unnecessary) attempt to cut any high-frequency noise from the DACs, then amplified and finally sent to the speaker.
The whole thing is clocked at 5 MHz by a crystal oscillator, and powered by 4 AA batteries through a 5-volt regulator. Unfortunately, the TTL-standard 5 volts doesn’t very well match the most common types of batteries, so we have to go slightly over-voltage and use a regulator to dissipate the excess as heat. Kind of like hot dogs and buns.
Probably the coolest part of this project was building my own printed circuit board. After finalizing the design on the breadboard, I learned how to use the EAGLE PCB design software to lay out the traces. EAGLE is handy because it has a built-in database of electronics parts, and automatically lays out the pads at the correct scale and spacing to ensure they’ll match up with the actual part. It also has some autorouting capabilities, but I ended up just laying out my small board by hand (and as I look at it now, I’m already spotting some places where I could’ve done a better job).
The board itself was made using photolithography. We printed the board diagram on a transparent acetate sheet, fixed it to the PCB and exposed it to UV light—i.e. the southern-California sun. The board had a photosensitive coating which, after washing away the exposed parts, formed a mask; then we etched away the unwanted copper using ferric chloride (a fairly nasty chemical). Finally, I drilled all the holes and soldered all the components in place.
The two AD9835s only came in the miniscule surface-mount form factor, which is really designed for machine assembly, not for human fingers. We made a couple of little adapter boards so we could put them in DIP sockets. I still had to solder the AD9835s onto the adapter boards, though, working under a microscope to reduce eye strain from staring at the tiny things!
Here’s the final result:
As you can see, we ended up mounting it in this box that someone found in a spare parts cupboard. On the top is a volume control pot, and a 3.5mm headphone jack for the output. Unfortunately, the dipswitch and play button aren’t very accessible, still being mounted on the PCB. We drilled holes in the top of the box, so you can poke at them with a pencil. Not the user-friendliest design. :)
I mentioned earlier that the PIC microcontroller I used had some serious limitations as a CPU.
First, it’s an 8-bit machine. It has 68 general-purpose registers, plus an accumulator register; the ALU can only talk to the accumulator, so in general all data has to be moved to it before being operated upon (making it more like a machine with one register and 68 bytes of memory, in practice). It has only integer arithmetic, and no hardware multiply or divide; the list of all its instructions fits on one page.
It uses a uniform 14-bit instruction encoding, and has 1024 words of read-only (flash) program memory. Since it’s an 8-bit machine, but has more than 256 program words, the PIC requires a segmented addressing scheme—like the old 8086 real mode. Doing a call or jump across 256-word segment boundaries requires some extra work, and functions can’t straddle a segment boundary, as the processor will not automatically advance to the next segment without an explicit call or jump.
On the plus side, all instructions complete in one cycle—no latency, pipelining, or multi-issue. There’s an 8-deep hardware stack for function calls, plus a configurable timer interrupt, which comes in handy for real-time applications like mine. There are also a few special registers for I/O and various other control tasks.
I programmed the PIC in assembly (MPASM). There do exist C compilers for PICs, but I thought it would be more fun to code closer to the bare metal—and I ended up using some crazy tricks that I doubt a C compiler would have been happy with!
The overall structure of the music box software (or firmware, rather) is simple. It has a main loop that waits for the play button to be pressed, then reads the desired song index from the dipswitch and sets up the timer system to play it at the appropriate tempo. The main loop then waits for the song to finish and repeats.
To manage the playback rate, I made use of the PIC’s built-in hardware timer interrupt.
The timer works very simply: it consists of an 8-bit register that’s incremented every
n clock cycles, where
n is a configurable power of two. When the register wraps around from
255 to 0, an interrupt is generated; the CPU calls a predefined program memory address, at which
you’ve hopefully placed an interrupt service routine.
More precise timer control can be achieved by resetting the timer register to a chosen value on each
interrupt, which controls how long it takes to overflow again. For example, if the register is
reset to 209, the timer will overflow and trigger the interrupt every
47*n cycles. I
precomputed a timer reset value based on each song’s BPM, allowing the remainder of the code to be
The main software-engineering problem I had to solve was how to represent the song data in a form compact enough to fit in the PIC’s very limited storage space. The PIC actually has 64 bytes of flash memory that can be read and written programmatically, but that’s not enough to store a song of any length, especially with two voices. Therefore, I had to cannibalize some of the program memory for song storage. I allocated 512 of the 1024 words to actual code, and the other 512 to song data.
Fortunately, the PIC’s designers anticipated using program memory to store data, and they
retlw instruction, which moves an 8-bit immediate value into the accumulator
register and then returns. Therefore, you can implement a lookup table as a subroutine containing a
sequence of these instructions. At the top of the subroutine, you add the desired table index into
the PC register; this jumps to the
retlw instruction containing the desired data, which
then loads that data and returns from the subroutine. In this way, you can store 8 bits of data per
1 2 3 4 5 6 7 8 9 10 11 12 13 14
; A snippet of the song data lookup table for the Phantom of the Opera theme phantom_theme nop movf data_idx, W addwf PCL, F retlw 0x58 retlw 0xA8 retlw 0x58 retlw 0x8A retlw 0x64 retlw 0xC1 retlw 0x6C retlw 0x38 ; ... etc.
So, with 512 bytes to play with, how to encode something interesting? I took my inspiration from the MIDI file format. While MIDI itself isn’t compact enough for me to use it directly, it still has a useful structure. MIDI files consist of streams of events; each event has a delta-time to wait after the previous event, and a command telling the MIDI device to do something—such as start or stop a note, change instruments, and so on.
Based on this idea, I designed a data format in which each byte encodes one song command. The lower nybble stores a delta-time between this command and the next. The 16 possible values represent musically-meaningful lengths, such as eighth note, quarter note, and so on. During playback, these values are mapped to actual timings by another lookup table, using a standard of 48 ticks per quarter note (divisible by three to allow triplets). As mentioned earlier, the length of a “tick” is set by controlling the timer interrupt rate, based on the song’s BPM.
The upper nybble stores the song command. Of the 16 possible values, 12 represent the pitches of the standard chromatic scale; like the note lengths, these are mapped to the actual frequencies by another lookup table. A 13th command value represents silence—treated as just another pitch, which happens to have frequency zero. Two more command values are used to move up or down an octave, and the last is used to toggle which of the two AD9835s the commands are going to. These last three are stateful—the playback routine remembers which octave and which AD9835 it’s currently on, so state changes affect all following notes until another state change.
The interrupt service routine has the job of decoding all of this. It counts ticks in order to measure out the previous command’s delta-time, then processes the next command—either setting state, or sending out a new frequency to one of the AD9835s. Each frequency is a 32-bit value representing a fraction of the 5 MHz clock rate; the octave is adjusted by bit-shifting.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
; A snippet of the interrupt service routine org 0x04 ; save W and STATUS registers movwf stored_w swapf STATUS, W movwf stored_status ; has tick counter gotten to zero? decfsz tick_counter, F goto ret ; if not, return immediately ; get next song command getcmd movf song_addr, W farcall indirect_song_lookup movwf command incf data_idx, F ; move the pointer to the next item ; process upper nybble andlw 0xF0 sublw REST ; calculate REST - W btfss STATUS, C ; if (W > REST), go to the next section goto cmd ; it's a pitch (or rest) command; look up the frequency swapf command, W andlw 0x0F call freq_lookup ; start playing the new pitch ; send the frequency to the current AD9835 btfsc device, 3 goto todev1 farcall setfreq0 goto settime todev1 farcall setfreq1 goto settime
To generate the song data, I wrote a simple C++ program that converts MIDI files into the compact format for the music box. It requires the MIDI file to work within the restrictions of my format—at most two voices, all note lengths must match one of the 16 predefined values, and so on. The program interleaves the two voices, inserting commands to toggle between AD9835s, and tracks the current octave, inserting commands to adjust it as necessary. I arranged the songs in NoteWorthy Composer, working from publicly-available MIDI files.
If you’ve taken the time to read this far, I thought it would be cruel to leave you without a chance to hear the results of this project! Here are recordings of a couple of short songs played by the music box:
|Harry Potter theme:|
|Phantom of the Opera theme:|
I would recommend any coder to take an electronics class or two, if you have the opportunity. Since I took that electronics class in college in 2006, I haven’t done any more hardware or embedded software projects of my own; nevertheless, I’m quite glad to have had the exposure to that world. Coding on a tiny microcontroller, to the bare metal of a device I designed and built, is so different—and in some ways, more fun—than coding in the carefully sandboxed userland of an OS on a full-featured CPU in a commodity PC. On the other hand, when this project was done, I can’t pretend that I didn’t enjoy getting back to a machine with more than 8-bit registers and 68 bytes of memory. 😂