Levi Puckett

firmware engineer

Hey! If you landed here, you probably clicked through from my resume and are looking for more details about how I work.

Most of the interesting things I've built are covered by NDAs. I can't provide details about the products or show the code (and screenshots of firmware aren't exactly riveting anyway!)

What I can do is walk through a couple of the interesting technical problems I've faced, and how I solved them.

edge inference

The challenge was to fit an AI algorithm onto a disposable device. This constrains the design: a smaller MCU means a lower bill-of-materials cost. The memory budget was as tight as it could be while still running the inference. We had 128kB of RAM and five sensor channels sampling at 250Hz, which we needed to process in near-real-time. The inference pipeline included a 10-second-wide Fourier Transform and a neural network to output a confidence score.

sensor
5ch @ 250Hz
──▶
DMA
ring buffer
──▶
Fourier
1s window
10s window
──▶
Neural Net
feed-forward

reference → implementation

The algorithm existed first as a Python reference model. I then worked to produce a C implementation that ran on the device with exactly the same outputs.

I used a unit test framework to validate the C implementation incrementally: each stage of the pipeline tested in isolation against the Python reference. Once the tests passed, I built a hardware-in-the-loop jig: pipe canned sensor data into the physical device over serial, collect the output, and assert it matches what the Python model produces for the same input.

PC Host
reference model
── canned data ──▶
◀───── result ─────
MCU
device

fitting in memory

The first major hurdle was that the large FFT window didn't fit in RAM. The solution was to split it into several smaller FFTs and average the results — a reasonable approximation which reduced memory usage significantly.

We were still running into space and time challenges, so I went further and replaced the FFT with the Goertzel algorithm — a specialized DFT that computes only the specific bins you care about. The unit test setup showed that it was equivalent to a full DFT implementation, but it reduced RAM usage and CPU cycles for the long DFT.

reclaiming cpu

With the algorithm fitting in memory, the remaining target was CPU headroom. I migrated all the peripherals to use DMA to reduce software polling loops, and squeezed other optimizations out of the application via the compiler options and reducing idle processes. This freed roughly 20% of CPU cycles which could now be used to run the inference pipeline closer to real-time.

FFT · DMA · unit testing · hardware-in-the-loop

protecting trade secrets

A battery-powered wearable meant to be taken home and used for several weeks at a time. The device relied on a proprietary algorithm that the client considered too sensitive to share with anyone, including us.

The challenge: integrate this algorithm into firmware we'd own and maintain, without any visibility into what it was doing.

co-located binaries

After considering several alternatives (the MPU, or a second MCU), the solution we landed on was co-located binaries: two independent processes sharing a single MCU. A “frontend” (our code) and a “backend” (the blob) were each given their own memory regions, peripherals, and interrupt vectors, with a strictly defined interface between them.

MCU
frontend
our code
▸ ADC/timer IRQs
▸ UI peripherals
▸ comms stack
─── call ───▶
API boundary
◀─── result ──
⚠ watchdog
backend
secret blob
▸ timer IRQs
▸ algo processing

the interface contract

We defined a limited set of API calls the frontend could make into the backend, each with an explicit timing allowance — the frontend could not block indefinitely on a response from an opaque binary.

Watchdog timers enforced those contracts at runtime: if the CPU spent too long inside the backend blob, the watchdog would fire. Peripherals and interrupt vectors were partitioned between the two sides so neither could interfere with the other's hardware.

Making all of this work meant getting very comfortable with the linking process. I wrote a custom linker script to lay out both binaries in memory — assigning address ranges, aligning sections, and keeping the frontend and backend regions cleanly separated.

flash
frontend
backend
sram
frontend
backend

dev infrastructure

Two independent binaries quickly became awkward to work with. I built automation to independently flash either side to allow for quick iteration when only the frontend was changing.

Before the real backend arrived, I built test stubs: stub implementations of the backend interface with known, predictable behaviour. These let us develop and validate the frontend in isolation, and gave us confidence in the API contract itself before the opaque binary was ever in the picture.

custom linker scripts · watchdog timers · peripheral partitioning · test stubs

always learning

My hobby work is available on GitHub. When I spend time building something, it's usually to get to know a technology better.

wyagreimplemented Git in Python to understand how it works under the hood
feedmecreatures with small neural-network brains forage for food in a simulated environment. Each generation, parent pairs are chosen with a bias toward creatures that found more food; a child’s "genes" are just its parents’ network weights and biases, mixed together. No goal directly drives the creatures: they become fitter naturally over generations.
cryptographyexplored a cryptanalysis attack on a toy cipher
sorting visualizerC++ sorting algorithms with a visualization component