PETI gets PWM Audio
Last Updated: 2025-02-12 00:00:00 -0600
For the last few weeks, particular during our scheduled lab livestreams, I’ve been working on the 0.5.0 firmware update for PETI, our house-developed virtual pet. While the main focus of the update is going to be the Bathing and Bathroom gameplay features, the update also includes some window-dressing changes and an important change to support the Revision D development kit, which should be coming out sometime this spring. This last bit is the piece we’ve just implemented: the use of Pulse-Width Modulation to drive an audio signal on the pet.
Current (0.4.1) Audio Method
The Revision C PETI development kit, which was being used up until now to do all development work, as well as the prototypes that predated it, all used the same WST-1206UX magnetic buzzer to make sounds. The main issue with this part is that it’s a self-oscillating buzzer, not a speaker. When supplied its rated power reqirement, it produced a 2.3 kHz tone at about 83dB until you shut the power back off. This made coding for the buzzer really simple. In 0.4.1
and earlier versions of the firmware, the AUDIO_pulse
function just started a timer for an argued interval, and turned the P3.4 output pin to Logic High, turning the buzzer on. When the timer threw its interrupt, the ISR for that interrupt just shut the signal back off. This was great because you could use longer or shorter “pulse” intervals to create distinct audio messages. What wasn’t great about it was that they were always at the same pitch, and that actually made the difference between the different pulses hard to hear.
New (0.5.0 and Future) Audio Method
Though I initially avoided making this decision because “music” is well outside my comfort zone and skillsets, it was becoming clear from the play-testing that followed the 0.4.0
and 0.4.1
releases that I was ultimately going to want a more flexible and robust audio solution; the ability to play entire small jingles. While that’s not supposed to be baked into the firmware in full until the 1.0
milestone, realizing that it was coming one way or the other meant that I wanted it to be baked into the Revision D development kits (which aren’t out yet, at the time of writing). The hardware change coming sooner than later also meant that the matching firmware changes needed to be made, at least in their most primitive sense, in time for the Revision D release, and that meant that the absolute latest they should be included was the next planned version, 0.5.0
.
Swapping in a Speaker
If you had a copy produced today (or earlier) of the service manual for the PETI Virtual Pet Development Kit, you would see that revisions C and D of the development kit supposedly use the same WST-1206UX part for a speaker. That’s because the service manuals are still somewhat outdated and need to be brought up to speed. This became an official change during a January 22 Live Stream, when I changed the specified part to a AST-02308MR-R speaker, which is now part of the Revision D spec. This isn’t quite a 1:1 replacement though; this speaker is a bit bigger, and being a speaker required some special accomodation in the form of adding a transistor to the board as well; this was needed to act as a sort of primitive amplifier to make sure that the speaker produced an audible signal across the full spread of possible PWM paramters.
Changing the Signals
So, for the uninitiated, while a buzzer lets you produce a sound just by turning it on or off, a speaker needs to be driven by a waveform signal. It’s not enough anymore to just take pin p3.4
and turn it on for a fraction of a second then turn it back off. We need to turn it on and off hundreds or thousands of times per second. In a rich audio system, like a proper digital audio player, or a modular synthesizer, this signal sent to the speaker would even be analog: not “on” or “off”, but a specific positive or negative voltage at any given instant. With the MSP430FR5994, that’s not strictly possible on pin 3.4 without additional hardware, and it’s also asking a lot from a processing power perspective.
There is, however, a suitably retro midway solution to this problem: pulse-width modulation. At sufficiently high clock speeds, a digital signal can be confused by the human ear for an analogue signal. This is such a common solution to such a large number of problems that the MSP430 driver library includes a convenient helper function that makes it easier to configure the timers to output a PWM signal (ie: turn the pin on and off in a certain way) on a given pin based on which timer you’re using.
And as it happens, extremely conveniently, the CCR3 register on Timer B0 outputs to the same physical pin as GPIO Pin P3.4
. This provides a couple of important advantages for our purposes:
- Assuming a suitable configuration of the PWM signal, the PWM signal can be used to drive the Revision C board’s buzzer, admittedly at a slightly lower volume than normal, and;
- By hardcoding certain parameters, we can make the code changes completely transparant to existing scenes/modules which use the
AUDIO_pulse
alert primative.
This is possible because as long as the produced period (roughly: the frequency of the signal) is relatively high and the duty cycle (percentage of the period) is relatively high, the output PWM signal triggered by AUDIO_pulse
is enough to drive the buzzer, but also an appropriate sound to call a “beep” when it comes out of the speaker.
Building on These Signals
Longer term, it would be pretty easy to take the function which is currently AUDIO_pulse
and turn it into something such as AUDIO_produceNote
by simply adding arguments. Currently, Pulse just takes a pulse_length
variable and uses it to set up an interrupt flag on Timer A2 CCR0. When this interrupt flags, the corresponding ISR sets a flag to run another function on the next gameplay loop, which:
- Stops the Timer B0 from running (stopping the PWM signal),
- Stops the Timer A2 from running (why run a timer that’s expired), and
- Stops the output on
P3.4
.
If you wanted to play interesting tunes, you’d only need two conceptual changes:
- An enhancement like
AUDIO_produceNote
which lets you set an arbitrary duty cycle and period for the PWM signal, thus producing an arbitrary sound, and; - A change to the ISR that would run something akin to
AUDIO_nextNote
instead ofAUDIO_stopNote
.
I haven’t fully architected it yet, but what you would essentially do is build increasingly abstracted systems so that a struct or array that defines a “tune” could be passed to some convenience function, and that convenience function replaces individual calls of AUDIO_pulse()
in actual scenes or other places you’d place alerts.
This would obfuscate the need to interface with the timers directly from the developer, who could simply define sequences of notes and then pass them to some imagined AUDIO_playTune()
function. It’s this obfuscation that is later slated for the 1.0 update.
The Rest of 0.5.0
The rest of the 0.5.0 update is actually still under active development - fixing the Revision D hardware changes in place and making this audio code change was essentially a side-quest to the main update.
As a quick reminder, these are the features intended to be added to 0.5.0
:
- The PWM audio change described in this post;
- The titular
Hygine
system, which will include the features to allow the pet to poop and the user to clean the pet up (likely to take the bulk of the upgrade development time); - An alert to trigger when the pet falls asleep;
- An improved system for drawing the status balloon based on the position of the Pet’s sprite on the screen, and;
- An idle check that reverts the user to the main game screen if the game is left in a menu for too long without interaction.
Since the Hygine system will involve both sprite development work and new animations, I’m curently loosely targeting roughly May 2025 for the software release.
If you wanted to show your support financially for Arcana Labs projects like PETI, but don’t need a virtual pet development kit, your best avenue is through the pathways detailed on our support page.