Sound can be a great addition to your product: alert users to problems or give feedback from actions like a button press. Sometimes, it’s also just great for demos; triggering a device to play the mario theme over cellular brings a certain joy for me, despite the number of times I’ve done it.
Today I want to add sound to the Aludel Elixir, our internal development platform.
Making the code more portable
The source of the PWM buzzer driver code started in the thingy91_golioth
repo. I’ve written about this on the blog before, including how we created a framework for writing “songs”. Just recently I added another tune (“Ode To Joy”) without much hassle, a testament to building some structure into your code so you don’t have to replicate work each time you need to make a change.
Previously the thread that plays the notes was directly in app_rpc.c
(and .h) because we were triggering all of the songs from a Remote Procedure Call. Mike had already broken out the beeper code into a separate file when he revised the thingy91_golioth
repo to support the Thingy91X (more on how those changes work below). Now the code lives inside app_buzzer.c
(and .h). That required that we add the new .c file to the CMakeLists.txt
and have any relevant functions or variables in the header file that need to be accessed outside of those functions. We call the play_beep_once()
function from main.c when a button is pressed, for instance.
I moved the beeper code into the RD template, including pulling over the broken out files that contain the thread that controls the PWM. I also needed to move any calls I wanted into main.c
, including calling a buzzer init function. Then I modified the logic in app_rpc.c
that handles incoming commands so that we can call the correct function (like play_mario_once()
). Lastly, I needed to make sure that the devicetree was set up properly. In the case of the Aludel Elixir, the PWM was set up in the board files (aludel_elixir_common.dtsi
) but the alias for the PWM pin that was in the board files did not match what I was calling when I initialize the function. A quick change to the aludel_elixir_ns.overlay
fixed the alias for my build and everything was plumbed together properly.
Only compile for certain boards
One new thing to me was encapsulating code based on which board we’re building for. KConfigs are not new, nor is the fact that we can selectively compile code based on that KConfig, including a module or library being included (foreshadowing!).
You might consider this for your Zephyr repo if you’re targeting different revisions of hardware, or even entirely different boards. The latter is less common, but we have written about setting up board definitions for hardware revisions and it’s likely that a sensor or a hardware element might live on one board vs another. For instance, the Thingy91 has a buzzer, while the Thingy91X does not! (Mario, we hardly knew ye)
Future work: Making it a module or library
If you’re really going to reuse code, you should standardize it for your various projects. There are two levels of possibility here.
First, you can just turn it into a module and pull in the code as a Zephyr module. Primarily this helps with revision control, keeping the module’s version tracked in west.yml
and placing the code in a known location in your project like modules/lib/<your_module>
. Keen eyes will note this is also the directory where the Golioth Firmware SDK resides.
Taking it further, we can write a device driver with a custom API. This is a more involved approach, but one that newcomers to Zephyr often ask about. It involves writing code to crawl the devicetree and initialize your module. It will instantiate more than one device in the event there are multiple matching nodes in the devicetree entry. You present a unified set of calls that are accessible to different functions; in the case of the buzzer, we’d only really need to create a play_song(<songchoice>)
interface.
Both methods clean up your code because you’re not copying and pasting into your design, you’re calling into external functions and APIs. They also both implement KConfig symbols to help selective code compilation in other parts of your code.
We’re always updating our Reference Designs
At Golioth, we are continually refining our hardware, firmware, and Cloud solutions to enable our clients’ IoT projects. This includes rolling new functionality into our Golioth Firmware SDK and our Reference Design Template. If you’d like to see everything in one place, check out our recent Reference Designs or consider picking up an Aludel Elixir and experience of end-to-end IoT connectivity on your bench.
No comments yet! Start the discussion at forum.golioth.io