Building an In-Field Firmware Programmer with Raspberry Pi
May 10, 2026
I share and sell LED badges at when I’m out and about. Sometimes it’s the case that I need to reflash the firmware on some of my creations, for which I don’t necessarily want to bother lugging my laptop around.
I realized there’s fundamentally no reason why I should need a whole 2 pound laptop with a battery with dozens of watt-hours and more transistors than grains of sand on the beach to program an 8 bit microcontroller with 16kb of flash space.
Other attempts have been made at this (1, 2). But I figured the best solution would be to use a raspberry pi zero, because it’s small enough, but it’s also a whole computer, so it should be able to run any kind of flashing software that I could want.
The Hardware
I decided to use a Raspberry Pi Zero W. Not the Zero 2 W – because that additional power comes with the tradeoff of more power, and therefore less battery life.
I considered designing a custom pcb to hold the screen, buttons, or to sandwich underneath a screen for the pi zero that anyone else made with a connector to run the UPDI protocol directly off of the raspberry pi’s GPIO pins. But then I started to think about supporting AVR-ISP, JTAG/SWD, SWIM, TPI, and so on and so on and rabbit-holed on that for a while, ultimately deciding that it would be easier to just use existing USB dongles for each toolchain that I want to support using an OTG adapter on the pi’s sole USB data port.
And for the screen and physical controls I settled on the Adafruit 1.3” Color TFT Bonnet, which gives me a 240x240 color display with a 5-way joystick and two buttons.
Crucially, adafruit claims that you can run their TFT as a real console screen, and see the linux boot process, which I thought was incredibly cool, and gives you something satisfying to watch while the device boots. In practice I found this somewhat buggy and it turned out that adafruit had only really tested it on the Pi Zero 2, and it took some hacking to get it to work manageably.
I used a waveshare UPS HAT for Raspberry Pi Zero designed for the raspberry pi zero as a battery power supply manager. Unlike most pi hats, this one is designed to connect underneath, and interestingly, connects to power via cupped pogo pins without needing a direct solder connection.
I first tried using it with a generic lipo battery that I got off of amazon. I forgot that there is no standard for the polarity of the JST connector of such batteries, and with a whiff of the telltale scent of melting soldermask, quickly learned that the waveshare’s battery hat does not have reverse polarity protection. I purchased a second one.
The waveshare battery pack comes with an IN219 current/voltage sensor on the I2C bus to monitor power consumption and battery level, which is useful.
Furthermore, I took a cheap, generic DS3231 battery-backed RTC, removed and re-arranged the battery connector, and soldered it upside-down onto the bottom of the Adafruit TFT bonnet, to connect to the I2C bus, where it fit reasonably well. This device will not always be connected to the network and without an external RTC timestamps would default to Jan 1, 2000, which I hate, but otherwise makes little practical difference realistically.
Every type of programming will need a dongle, connected over an OTG adapter to the micro USB port. For UPDI I use the excellent SerialUPDI programmer from MCUdude, though I’ve also adapted an Adafruit UPDI Friend to mcudude’s 2x3 pinout) which I use for all of my project. For AVR-ISP I use pololu’s USB AVR Programmer v2.1.
The Software
The entire application is written in Python, running as a systemd service that starts on boot. It takes over /dev/tty1 and presents an ncurses UI — no desktop environment, no X server, just a terminal app on the bare framebuffer.
Project Definitions
Each “project” lives in its own directory under projects/ and contains a definition.yaml that specifies everything needed to flash that target:
- The name of the project
- A glob pattern for finding firmware
.hexfiles - The avrdude (or pymcuprog, or whatever) commands to run, with template parameters for the serial port and firmware path
- Any local support files (like custom fuse configurations)
At startup, the application scans the projects/ directory, loads every valid definition, and presents them as a list. This means I can add a new project by just dropping a folder onto the Pi over WiFi — no code changes needed.
The UI
The UI is a simple ncurses menu system. Main menu gives you four options: Flash Firmware, System Settings, Exit to Shell, and Shutdown. The flow for flashing is: pick a project → pick a firmware file (sorted newest-first by modification time) → confirm → flash.
I designed the input handling around a consistent button convention: Button B and joystick-right always mean “forward/select/confirm”, Button A and joystick-left always mean “back/cancel”. The joystick up/down navigates lists. This sounds obvious but it took a few iterations to get right — early versions had inconsistent mappings that were confusing to use one-handed.
The GPIO input handler runs on its own thread, polling the bonnet’s buttons at 10ms intervals with software debouncing. Events go into a queue that the main loop consumes.
The status bar at the top of every screen shows WiFi signal strength and battery level, both rendered as block-character bar graphs. WiFi signal polls iwconfig and converts dBm to a percentage. Battery level comes from the INA219 over I2C. Charging detection uses a timeout — if I’ve seen positive current in the last 3 seconds, I show the charging indicator. This smooths out the flickering you’d get from noisy current readings.
The Flashing Flow
This was the trickiest part of the UI. When you kick off a flash, the application needs to shell out to avrdude (or whatever tool the project specifies) and stream its output in real time. But ncurses owns the terminal, and avrdude wants to write to stdout.
My solution: temporarily tear down the ncurses display, clear the terminal, and let the subprocess write directly to the TTY. The flash manager runs the command in a background thread, streaming stdout and stderr line-by-line through a queue. The main thread pulls from the queue and prints each line as it arrives.
# Temporarily stop the display to exit ncurses
self.display.stop()
# Clear the terminal and show header
os.system('clear')
print("=" * 70)
print(f"FIRMWARE PROGRAMMER - FLASHING MODE")
print(f"Project: {self.project.name}")
print(f"Firmware: {self.firmware['name']}")
print("=" * 70)
When the flash completes (or fails), I wait for a button press, then restart ncurses and return to the completion screen. From there you can flash again (same firmware, next board) or go back to the menu.
The “flash again” option is important in practice. At an event I might be flashing the same firmware onto 10 boards in a row. I don’t want to navigate back through the menus each time.
Battery Monitoring
The INA219 communicates over I2C and measures both bus voltage and current through a shunt resistor. I calculate battery percentage using a simple linear mapping from the voltage range (3.0V empty to 4.2V full), then smooth it with a basic 50/50 exponential filter to avoid jittery readings.
System control
The UI can shut down the linux OS, toggle wifi on or off, or exit to the console in case I ever need to connect a keyboard and lose SSH access.
System messages
One annoyance I had to deal with kernel and systemd messages writing over the ncurses display for what felt like several minutes after boot. The Pi’s console output was being directed to tty1, which is the same TTY my app uses. The fix was to use the TIOCCONS ioctl to redirect /dev/console output to /dev/null at startup, then restore it on exit. Small thing, but without it the display would randomly get corrupted by boot messages.
Design Decisions and Tradeoffs
Why a Raspberry Pi instead of a microcontroller? Because I wanted to run avrdude, pymcuprog and so forth directly, and those are computer-based tools. I also wanted WiFi so I could scp new firmware files onto the device. A microcontroller-based programmer would need to implement the programming protocols from scratch, and I didn’t want to go down that rabbit hole for a tool I just needed to work, and you need to implement USB somehow or add an SD card reader to copy files onto it. Additionally I wanted one programmer for anything I could ever want to program.
Why ncurses instead of a graphical UI? The Adafruit TFT Bonnet can act as a proper Linux framebuffer display, so I could have used pygame or even a minimal window manager. But ncurses is simpler, uses less memory, is easier to test (I can ssh in and run it over a remote terminal), and frankly looks cool — it has that utilitarian hacker-tool aesthetic that I like.
What I’d Do Differently
Sound feedback. I originally planned to add a piezo buzzer or I2S amplifier for success/failure beeps. I ended up not doing it in an executive decision to reduce project complexity, but it would be genuinely useful — when you’re flashing boards you’re often not looking at the screen.
A 3D-printed case. Right now it’s a sandwich of PCBs held together with standoffs. It works but I worry about it bouncing around in my bag a little.
The programmer has already saved me a bunch of time at events. There’s something satisfying about pulling a little device out of your pocket, selecting a firmware, pressing a button, and watching avrdude do its thing on a tiny color screen. It’s the kind of tool that makes you feel like you have your act together…