In 2021 I joined Blues as a software developer that was completely new to the world of firmware. In the intervening years I’ve gone from being happy to make an LED blink, to being a part of an ambitious set of sample projects we’ve created here at Blues. While I’m still far from a firmware expert, I thought I’d take a moment to share some of the things I’ve learned through the process.
Hopefully some of these tips inspire you, especially if you’re new to firmware development. And if you have your own tips, let me know in the comments.
Tip #1: Learn C
When I started writing firmware I did everything I could to avoid C. I built projects with Python, CircuitPython, and even tried to make JavaScript work for a few weeks. And although many of those projects worked (not the JavaScript-based ones, unfortunately), I consistently ran into roadblocks.
For example, consider the first serious project I built, a pest detector that sent me text messages when it spotted animals in my yard.
The firmware for this project was written in Python and deployed on a Raspberry Pi. At a high level the firmware needed to capture images from the Raspberry Pi’s camera, analyze those images using a freely available machine learning model that looked for animals, and send me a text message when it successfully detected an animal.
And technically the firmware did work—but in a laughably inefficient way. Processing the images with the ML model took many seconds (sometimes minutes!), meaning the pest detector wasn’t especially good at, well, detecting.
To be fair Python isn’t that much slower than C, and the problems with my pest detector were more about how I set up my ML model than with programming language choice. But as I’ve worked on more projects I’ve found that in the firmware world milliseconds matter, and you set yourself up for success if you’re using the fastest language possible from the start.
There are auxillary benefits as well. For example look at the hardware I used for my first project again.
The Raspberry Pi is not small and it needs a lot of power to run. For my pest detector that meant I needed to buy a bulky enclosure to hold my hardware, and also run an extension cord throughout my yard to detect animals in multiple locations (which my significant other loved).
Because C is so efficient, it can run on the smallest hardware and run in the most power-efficient way—making battery power and solar power more plausible for your projects.
Fast forward to 2022 and the next big project I built was a tracker for my kids.
Because of the nature of this project (a tracker) the hardware had to run on battery power, and had to stay powered while maintaining a cellular and GPS/GNSS connection. With these constraints I decided to write the firmware in C, and although the process took significantly longer (I remember the code I wrote to dynamically build a Google Maps URL taking forever), ultimately I was happier with the result. And now that I’ve went through the learning process I’ve carried a lot of that knowledge forward.
Plus, writing code in an unfamiliar language has gotten significantly easier thanks to my second tip.
Tip #2: Use AI
Artificial intelligence is the biggest change that’s happened to software development in my lifetime. Whereas I used to learn new languages or features in books, courses, or online articles, today the majority of my learning takes place while talking to an AI chatbot.
Although I wrestle with the moral implications of using AI chatbots that have scraped countless courses and guides to build their models, I have a hard time ignoring a tool that makes me an order of magnitude more productive—especially when I work in new environments.
Let me give you an example. I spent my day yesterday making some updates to my Kid Tracker’s firmware, and one of the things I wanted to tweak was the format of the text message I sent out. Here’s the code to do that.
// http://maps.google.com/maps?q=<lat>,<lon>
char buffer[100];
snprintf(
buffer,
sizeof(buffer),
"Your kids are requesting you. https://maps.google.com/maps?q=%.12lf,%.12lf",
lat,
lon
);
It’s been about a year since I last touched this code, and I completely forgot what %.12lf
does in snprintf
, so I decided to ask GitHub Copilot Chat (my preferred AI tool). Here’s what that looked like (see the left column for the answer).
Copilot’s answer is shockingly good. It tells me what I need to know about the code, and it does so right along the code itself so there’s no back and forth. Compare that to the results I get from doing a Google search.
The best results from Google are general guides and reference pages about sprintf
. And while I’m sure those guides contain valuable information, it would take me a lot of time to learn enough to apply that generic knowledge to my specific situation. Meanwhile, with GitHub Copilot I knew what I needed in seconds and was on to my next task.
But this is just the start. What makes AI even more valuable is you can have a conversation to further refine your requests. For example, one feature I’ve been thinking about adding to my Kid Tracker is the ability to choose between Apple Maps or Google Maps links. Instead of going to Google to look up if Apple Maps provides the ability to link to a location by latitude and longitude, I just asked Copilot Chat and it worked—first try.
AI models aren’t perfect, and occasionally they give an answer that doesn’t compile or isn’t what I want. But as I’ve used GitHub Copilot over the last year I’ve seen it get consistently better, and I can live with an answer or two being wrong if the others answers are saving me an incredible amount of time.
GitHub Copilot isn’t cheap ($19/month), but they do offer a free trial if you want to try it out. There are also a number of competitors in this space. Amazon has a similar tool named CodeWhisperer, and Cody is a free (for now at least) tool that also offers editor integration.
Although I don’t think the specific AI tool you use matters a ton I will make one recommendation: try using an AI tool that integrates into an editor if you haven’t already. General-purpose AI tools like ChatGPT can help you code some, but having a model that’s optimized for coding and embedded into your editor is life changing.
But no matter how good your AI-assisted code is, things will still go wrong. And that’s where my third tip comes in.
Tip #3: Log Everything
Debugging firmware is harder than debugging code in almost any other environment. If you write code that runs in the cloud you’re writing code for a shockingly consistent environment; outside of an outage that code can happily run until the heat death of the universe.
Meanwhile, writing firmware is writing for chaos. Your code might stop running because your host loses power. Your code might rely on external sensors, lights, or buttons that break, stop functioning, or fall off. Your code might be on a device that is put in unexpected weather conditions, unexpected locations, or unexpected positions.
Firmware developers learn to trust nothing, and to make their lives as easy as possible to debug devices that die or misbehave in the field. Although there are a variety of things you can do to make your devices easier to debug, I’ve found that some simple logging goes a long ways.
The first thing I do in my firmware is set up a RELEASE
flag that allows me to toggle whether to enable debug logging. This allows me to aggressively use Serial logging while developing my project, while maintaining the ability to turn off Serial logs for production builds.
// Set this to 1 to disable debugging logs
#define RELEASE 0
// This is for using the Notecard. More on that momentarily.
Notecard notecard;
void setup() {
#if !RELEASE
static const size_t MAX_SERIAL_WAIT_MS = 5000;
size_t begin_serial_wait_ms = ::millis();
// Wait for the serial port to become available
while (!serialDebug && (MAX_SERIAL_WAIT_MS > (::millis() - begin_serial_wait_ms)));
serialDebug.begin(115200);
notecard.setDebugOutputStream(serialDebug);
#endif
notecard.begin();
...
notecard.logDebug("Setup complete\n");
}
From there I include logging calls through my code, especially in places where things can potentially go wrong. Here are a few examples from my Kid Tracker where I handle error conditions related to location.
// Check for a timeout, and if enough time has passed, break out of the loop
// to avoid looping forever
if (::millis() >= (start_ms + (timeout_s * 1000))) {
notecard.logDebug("Timed out looking for a location\n");
locationRequested = false;
break;
}
// If a "stop" field is on the card.location response, it means the Notecard
// cannot locate a GPS/GNSS signal, so we break out of the loop to avoid looping
// endlessly
if (JGetObjectItem(rsp, "stop")) {
notecard.logDebug("Found a stop flag, cannot find location\n");
locationRequested = false;
break;
}
With ample logging in place my code is much easier to follow when using a Serial Monitor. For example, my Kid Tracker includes an SOS button that the user can press to immediately send an SMS message to a designated phone number.
Here’s what I see in my Serial Monitor when I press the SOS button.
Button pressed
[INFO] {"req":"card.location","crc":"0003:F0AE3444"}
[INFO] {"status":"GPS updated (4 sec, 26/27 dB SNR, 4/8 sats, HDOP 0.90) {gps-active} {gps-signal} {gps-sats} {gps}","mode":"periodic","lat":42.xxxxxxxxxx,"lon":-84.xxxxxxxxxx,"dop":0.9,"time":1702933023}
[INFO] {"req":"card.location.mode","mode":"continuous","crc":"0004:B7C156B1"}
[INFO] {"seconds":180,"mode":"continuous"}
[INFO] {"req":"card.location","crc":"0005:F0AE3444"}
[INFO] {"status":"GPS waiting to start {gps-starting} {gps-active} {gps}","mode":"continuous","lat":42.763580216666669,"lon":-84.648001783333346,"dop":0.9,"time":1702933023}
[INFO] {"req":"card.location","crc":"0006:F0AE3444"}
[INFO] {"status":"GPS waiting to start {gps-starting} {gps-active} {gps}","mode":"continuous","lat":42.763580216666669,"lon":-84.648001783333346,"dop":0.9,"time":1702933023}
[INFO] {"req":"card.location","crc":"0007:F0AE3444"}
[INFO] {"status":"GPS waiting to start {gps-starting} {gps-active} {gps}","mode":"continuous","lat":42.763580216666669,"lon":-84.648001783333346,"dop":0.9,"time":1702933023}
[INFO] {"req":"card.location","crc":"0008:F0AE3444"}
[INFO] {"status":"GPS inactive {gps-inactive} {gps}","mode":"continuous","lat":42.763580216666669,"lon":-84.648001783333346,"dop":0.9,"time":1702933023}
...
[INFO] {"req":"card.location","crc":"0023:F0AE3444"}
[INFO] {"status":"GPS search (35 sec, 32/33 dB SNR, 0/2 sats, HDOP 0.00) {gps-active} {gps-signal} {gps-sats} {gps}","mode":"continuous","lat":42.xxxxxxxxxx,"lon":-84.xxxxxxxxxxx,"dop":0.9,"time":1702933023}
[INFO] {"req":"card.location","crc":"0024:F0AE3444"}
[INFO] {"status":"GPS updated (37 sec, 33/33 dB SNR, 5/8 sats, HDOP 0.80) {gps-active} {gps-signal} {gps-sats} {gps}","mode":"continuous","lat":42.xxxxxxxxxx,"lon":-84.xxxxxxxxxx,"dop":1,"time":1702933090}
Location: 42.xxxxxxxxxx, -84.xxxxxxxxxx
Your kids are requesting you. https://maps.google.com/maps?q=42.xxxxxxxxxx,-84.xxxxxxxxxx",[INFO] {"req":"note.add","file":"twilio.qo","sync":true,"body":{"message":"Your kids are requesting you. https://maps.google.com/maps?q=42.xxxxxxxxxx,-84.xxxxxxxxxx"},"crc":"0025:CED8FECD"}
[INFO] {"total":1}
Location sent successfully.
These log messages help me debug the functionality when testing my project at my desk. For my Kid Tracker specifically I sometimes struggle to get a clear GPS/GNSS signal from my office, and having log messages that give me updates on the Notecard’s attempts to gather a signal let me know my problem is signal related, or if something in my code is problematic.
While Serial logging works great for local development, oftentimes the most valuable debugging comes from devices running in the field. And for this I’ve found the Notecard we make at Blues invaluable.
The Notecard makes connectivity simple, which allows me to gain insights on my deployed devices. For example, as I use my Kid Tracker I not only get information on the device’s location, but also voltage readings, timestamps, temperature data, and a whole more. All of this information can help me debug problems when things inevitably go wrong.
I’ve found that knowing your device’s voltage can be particularly valuable, both when testing the Kid Tracker as well as another project we have here at Blues called Airnote. The Airnote uses a solar charger, and I’ve been using data from the Notecard to help me find the ideal location for my device to get the most sunlight possible. Here’s a chart of the voltage levels of my Airnote over the last two weeks (as I write this) so you can see what I mean.
My Airnote’s data is public, and you can view the full dashboard here if you’re curious about the air quality in Lansing, Michigan, and/or how well my solar charging experiments are going.
Overall, what you choose to log depends on exactly what you’re building. But having data flowing to the cloud at a regular cadence—even if that data is as simple as a heartbeat so you know a device is still online—can be invaluable when you need to debug device problems in the field. And the Notecard makes the process of getting data to the cloud really simple.
Tip #4: Find Tools That Work For You
Finally, my last tip is to find tools that help you write firmware and to learn them really well. This tip is hard to make specific suggestions for, as oftentimes the tools you use in the firmware world are tied to the specific hardware you use. Nevertheless, I thought I’d share what I use for development as you might find some of this information interesting or valuable.
I try to write as much of my code as possible in Visual Studio Code (VS Code). VS Code is free, shockingly fast, and completely customizable through extensions. Two of those extensions are fundamental to how I write firmware: GitHub Copilot Chat and PlatformIO.
I discussed GitHub Copilot Chat earlier in this article, but I’ve having the tool directly in my editor is incredibly valuable for asking questions about my code as I’m writing it.
The other extension I can’t live without is PlatformIO. PlatformIO helps automate the messy parts of embedded development, such as configuring a project for a board, building the code using the appropriate tools, and flashing the built code to your device.
With PlatformIO your configuration lives in an easy-to-read platformio.ini
file that sits at the root of your project.
All PlatformIO projects have built-in tasks for building your code, uploading your code, and launching a Serial Monitor for monitoring your project in action. My favorite part—all of these commands are available through keyboard shortcuts, which are 100% worth memorizing if you perform these tasks repeatedly.
Oh, and did I mention that Visual Studio Code has a built-in terminal? So while I’m monitoring my Serial Monitor I can easily check on the status of my project through git
, or do any other terminal-y things I need to take care of.
Overall, I’m productive in VS Code because it does everything I need, and I’ve invested the time to thorough learn parts of the tool I use on a daily basis. Regardless of what tool you use to write your firmware, it’s worth putting in the effort to learn the tool well because it’ll make you more productive, and also less frustrated as you inevitably have to debug difficult problems.
Wrapping Up
There you have it—a very random set of tips that I’ve found useful as I’ve become more accustomed to writing firmware over the last handful of years. I’m always looking to learn more, so if you have any tips that have worked for you feel free to drop them in the comments.