Embedded Firmware Development: What Changes When the Code Runs on the Device
Firmware fails differently from application software. A web service that leaks memory gets restarted at 3am by the platform and nobody finds out. A wearable that leaks memory stops recording on day three of a seven day session, and those four days do not come back.
Everything below follows from that.
What embedded firmware development is
Embedded firmware is code that runs directly on a microcontroller, usually with no operating system under it or a small real-time one. It reads the sensors, drives the radio, manages power, and decides what gets written to flash. There is no browser. No package manager at runtime. No shipping a hotfix and watching a dashboard for ten minutes to see whether it worked.
Embedded firmware development is writing and maintaining that code inside a hardware budget you cannot change. A wrist-worn sensor might give you 512KB of flash, 64KB of RAM, and an average current draw under 100 microamps, because the battery has to survive a week of continuous recording between charges. Those three numbers are the brief. Everything else negotiates around them.
The languages, and why C is still the answer
C is the default and has been since the 1980s. It maps closely to what the processor actually does, every silicon vendor ships a compiler for it, and the static analysis tooling that regulated industries depend on was built around it.
When we ported three movement disorder algorithms to run on Empatica's wrist device, C was not a preference. It was what the hardware and the certification path both accepted.
C++ turns up on larger parts, normally a subset with exceptions and RTTI switched off. Rust is real in embedded work now, though the certified toolchain story is younger than C's, which slows adoption in medical and automotive. MicroPython is good for prototypes and internal test rigs and almost never ships. Assembly survives in startup code, interrupt vectors, and the occasional hand-tuned loop, and nowhere else.
Choosing today for a constrained sensor device with a compliance requirement attached, pick C.
What the embedded firmware development process looks like
It starts with the datasheet, not with code. You read the reference manual for the microcontroller and every peripheral hanging off it, work out what the part can do, and size the software against the flash, RAM and power you have been given. Teams that draw the architecture first and check the budget later end up rewriting a working system because it will not fit in the part the hardware engineer already committed to.
Then bring-up. Getting a board to blink an LED, answer a debugger, and print over UART sounds like an afternoon and routinely takes a week. Until bring-up works you cannot tell a software bug from a bad solder joint, and you will waste days assuming the wrong one.
Driver layer next, then the application logic on top of it, then integration on real hardware rather than a simulator. Testing runs through all of it, on the desktop where the code is portable and on the device where it is not.
Field testing is last. That is where the failures nobody modelled show up: a user who wears the device upside down, a Karachi summer the thermal design never saw, a hospital corridor full of radio interference.
The development environment is the part nobody warns you about
Application developers moving into embedded work lose tools they had stopped noticing they had.
The compiler runs on your machine and produces code for a different processor, so the toolchain is a cross-compiler, and configuring it correctly is a job in itself. Debugging happens through a hardware probe over JTAG or SWD rather than by attaching to a running process. You do not have a log aggregator. You have a UART line, a small buffer to hold messages in, and now and then a logic analyser clipped to a pin because the bug disappears the moment logging is switched on.
Continuous integration is possible and harder than it sounds. Unit tests for portable logic run fine on a build server — the discipline behind that is the same one behind our software testing and QA work generally. Anything touching hardware needs a physical device wired to the CI machine, which most teams only set up after the third regression reaches a customer.
The constraints that catch application developers out
Dynamic allocation is discouraged and often banned outright. Call malloc on a part with 64KB of RAM and no memory management unit and the heap will fragment eventually, then fail at a moment you cannot reproduce. Most firmware allocates everything statically at compile time so the worst case is knowable before the device ships.
Floating point costs real time when the part has no FPU, because every operation becomes a software routine. Fixed point is the usual answer, and it is where the porting effort concentrates. Our Python reference implementation used doubles freely. The C version could not, and reconciling the two took longer than writing the C did — the mechanics of that particular port are covered in more depth in how we moved Parkinson's algorithms from Python to embedded C.
Timing is the other one. Interrupts fire while the main loop is halfway through something, so shared state needs protecting and handlers need to be short. Stack depth is finite and nothing warns you before you run past the end of it. Power management means the device sleeps most of its life, so the firmware is structured around wake events rather than a loop that runs forever.
None of this is exotic. It is a different set of rules, and application experience transfers less cleanly than people expect.
What changes when the device is regulated
On a medical device the question stops being whether the firmware works. It becomes whether you can show it does the same thing as the version that was cleared.
That inverts the usual test relationship. Instead of writing tests that check behaviour against a specification, you write tests that check output against a reference implementation, session by session, across real recorded data. A result that is close is a failure. IEC 62304 then wants the paper trail: requirements traced to design, design traced to code, code traced to tests, and a written rationale for every deviation you allowed.
We spent about six months on the C port for the Empatica device. Most of that was validation, not implementation. The algorithms already worked in Python. Showing that the embedded version matched them was the actual job — this is the same methodology we apply across clinical algorithm development generally.
When outsourcing embedded firmware development makes sense
Not always, and it is worth being blunt about when it does not. If firmware is your core product and you ship it continuously, the knowledge belongs in your building and an external team will always be a step behind the hardware.
It works when the work has edges. A port from one architecture to another. A validation effort against an existing reference implementation. A driver for a peripheral your team has not touched before. Each of those has a definition of done and a clean handover point.
We do not take firmware work where the hardware is still moving. If the board is on revision C and the pinout changes again next month, an outside team burns the budget chasing it, and the client pays twice for the same driver.
The thing to check in any vendor is whether they have worked under a compliance regime before. Firmware that runs is a much lower bar than firmware that survives an audit, and teams who have only done the first tend to underestimate the second by a factor of two or three.
If you are weighing up a port, a validation effort, or firmware for a constrained sensor device, our embedded firmware development page covers how we work. The clinical algorithm development page and the PKG Health case study go further into the validation side. If you want to talk through a specific board or algorithm, get in touch.