Expanding Into Embedded: A Backend Engineer Gets Close to the Metal
Jul 27, 2026 - ⧖ 6.0 minWhy embedded, and why now
My day job is backend and infrastructure. I write Python and Rust, I own the production systems that keep a platform running, and I spend most of my time thinking about services, deployments, and things that fail at 3 a.m.
But my first real contact with programming was not a web server. Back in 2018 I started a Control and Automation Engineering course, and that is where I first touched Python, C/C++, and an Arduino. I did not finish the course, life took a different route, but the itch never went away: the idea of software that does not just move data around, but reaches out and changes something in the physical world.
So I decided to scratch it. Not as a career pivot, but as a deliberate way to expand my knowledge toward systems that run close to the hardware, which is where I want to grow in the long run. This is the first project of that effort, and this post is both a write-up and a note to myself about how I want to approach the rest.
The project: a pedestrian crossing
The first project is small on purpose: a pedestrian crossing controller running on an ATmega328P (a Miuzei board, an Arduino Uno R3 clone).
The behaviour is simple to describe and surprisingly good at forcing you to think in states:
- The traffic light stays green for cars by default.
- A pedestrian presses a button to request a crossing.
- The light transitions to yellow, then red.
- While red, a countdown runs on a 7-segment display and a buzzer beeps.
- When the countdown ends, it returns to green, and the cycle can start again.
A quick confession on that 7-segment display: I have the countdown working in the logic, but I have not driven the physical display on the bench yet, because I killed my first one. While probing to find the ground pin, I sent it where it should not have gone, and that was the end of it. Consider it my initiation fee into hardware. A replacement is on the way, so the next iteration gets tested for real and not only in code. It is also a quiet argument for the testing strategy below: getting the countdown logic wrong costs me nothing, I just re-run the tests, while the same kind of mistake on the bench charged me one display.
It is a state machine with a hardware body: a button as input, and LEDs, a display, and a buzzer as outputs. Trivial as a concept, but it is exactly the kind of thing that teaches you the fundamentals of embedded work: timing without blocking, debouncing an input, and driving several outputs that all have to stay in sync.
Treating a toy project like production code
Here is the part I actually care about, and the reason I am writing this down.
It would have been easy to open the Arduino IDE, paste everything into a single .ino file, and call it done. That is how most hobby projects look. I wanted to find out whether the discipline I apply to backend systems, testing, CI, clean structure, translates to a microcontroller with 2 KB of SRAM. It does, and the constraints make it more interesting, not less.
PlatformIO instead of the Arduino IDE
The whole repository is built around PlatformIO. Each project is a self-contained PlatformIO project with its own platformio.ini, built and tested independently, with no shared build at the root. PlatformIO provides the cross-compiler, the framework, and the upload tool, so the only real prerequisite is Python.
I install it the same way I install any Python tool these days, isolated, so it never touches the system Python:
uv tool install platformio
The testing strategy: separate logic from hardware
This is the decision I am most happy with, and it is a direct import from backend engineering.
The trap with embedded code is that everything ends up tangled together: the crossing logic, the timing, and the calls that flip physical pins all live in the same function, and the only way to test any of it is to flash the board and watch the LEDs. That is slow, and it is not really testing.
So I split it in two:
- Logic that does not touch hardware lives in libraries that include nothing from the Arduino framework. The state machine, the transitions, the countdown, none of it knows a pin exists. Because it depends on nothing hardware-specific, it compiles with the host compiler and runs as native unit tests in seconds, with no board attached.
- Code that does drive pins is kept thin, thin enough to verify by inspection and on the bench.
In practice that means I can iterate on the crossing behaviour on my laptop, at the speed of a normal test suite, and only reach for the physical board when I am integrating. It is the same principle as keeping business logic out of your HTTP handlers: push the part you want to test away from the part you cannot.
Running it looks like this:
pio test -d pedestrian-crossing -e native # host unit tests, no board needed
pio run -d pedestrian-crossing -e uno # cross-compile firmware
pio run -d pedestrian-crossing -e uno -t upload # flash the board
CI on an 8-bit target
Every push runs the host tests and the firmware build in CI. There is something satisfying about a green checkmark verifying code destined for a chip with less memory than a single HTTP request buffer. It also keeps me honest: the moment the native tests or the cross-compilation break, I know, without having the board plugged in.
What is next
This is deliberately the beginning of a track, not a one-off. The natural next steps I am eyeing:
- More projects in the same repository, each self-contained, growing in complexity from the pedestrian crossing toward things with real timing and communication concerns.
- Doing some of them in Rust on AVR (via
avr-hal), because bridging the language I love with bare-metal targets is exactly the kind of overlap I want to live in. - Eventually, getting close to the domain that started all of this for me: systems where correctness and reliability are not negotiable, the space and aerospace software world I keep gravitating toward.
For now, the lesson is smaller and more useful: good engineering habits are not a luxury you earn once the project is big enough. Testable structure, a real build system, and CI paid off on a breadboard traffic light. They will pay off on whatever comes next.
The code is on GitHub: vmagueta/embedded.