Embedded Systems & IoT
Software that runs on hardware small enough to have no operating system worth the name, where the requirements come from something physical and the resources are fixed at the moment the board was designed. The skill it rewards is reasoning about worst cases rather than averages.
Assumes you know: C, to the level of pointers, storage classes and bit manipulation, A working mental model of memory addresses and integer representation, Willingness to read a reference manual rather than a tutorial, Enough electronics to know what a voltage, a pull-up resistor and ground are
Overview
What this area actually covers
Software whose requirements come from a physical thing and whose resources were fixed before a line of code was written. That is the whole definition, and both halves matter equally.
The physical requirement is what separates embedded work from everything else on this site. A web service that answers in 400 milliseconds instead of 200 has a performance issue. A motor commutation routine that fires 200 microseconds late destroys a motor winding. An airbag controller that decides correctly but 50 milliseconds late has failed completely, and nobody is interested in its average response time. Because of that, the entire discipline is organised around worst cases: worst-case execution time, worst-case stack depth, worst-case interrupt latency, worst-case current draw. Averages, which dominate almost every other kind of engineering interview, are close to useless here.
The fixed resources are what makes it feel unfamiliar. There is a specific amount of RAM on the die and no way to add more without a new board, a new supply chain qualification, and possibly a new certification cycle. There is no swap, usually no memory protection, frequently no operating system, and often no filesystem. When your code overruns its stack, nothing catches it: the write simply lands in a variable belonging to something else, and the bug surfaces three modules away as a value that changes on its own.
Concretely, the area covers writing and structuring firmware in C for microcontrollers; configuring peripherals by writing bits into registers described in a reference manual; handling hardware interrupts correctly and sharing state with them safely; deciding whether you need a real-time operating system and, if so, how tasks and priorities are laid out; the buses that connect chips on a board — UART, SPI, I2C, CAN and their relatives; power management for devices expected to run for years on a cell; and the debugging craft that applies when there is nowhere to print to and the failure happens once a week in a customer's building.
It is worth naming what gets wrongly bundled in. Electronics design is a different profession: reading a schematic and understanding what the hardware requires of your firmware is expected, but designing the board, choosing the passive components and running signal integrity analysis is a hardware engineer's job. Embedded Linux sits at the boundary and is genuinely a different skill set — once you have an MMU, a kernel, a filesystem and a package manager, most of the constraints described above stop applying and the work looks much more like systems programming. Digital signal processing and control theory are the mathematics that often runs on these devices, and they are studied as their own subjects; embedded engineering is how that mathematics gets to run in 8 kilobytes on a fixed deadline. And IoT as a marketing term routinely means cloud back ends and dashboards, which is backend engineering with a device-shaped input.
What sits underneath: Firmware and RTOS
This section currently has a single subsection, which reflects where the interview questions concentrate rather than the breadth of the field.
| Subsection | What it is for |
|---|---|
| Firmware & RTOS | The core of the discipline: bare-metal C, where RAM goes when there is no allocator, interrupt correctness, and the scheduling and reliability mechanisms layered on top |
Firmware & RTOS covers the code that runs directly on the microcontroller and the small operating system some designs put underneath it. Three clusters of material live here, and they are the three things an interviewer will reliably probe.
The first is memory without an operating system. You will find the arithmetic of the link map — what goes to flash, what costs RAM twice, where the stack and the heap sit and why they grow towards each other — along with the reason a shipping device usually has no heap at all, and how a stack allocation is bounded by analysis and then confirmed by measurement rather than chosen by feel.
The second is interrupts and the concurrency they create. An interrupt is preemption you did not schedule, arriving between any two instructions, sharing every global with the code it interrupted. The material here covers what may and may not happen inside a handler, why its duration is charged to the worst-case latency of every other interrupt in the system, what volatile guarantees and — more importantly — what it does not, and the structures that pass data out of a handler without a lock, because a handler cannot take one.
The third is the reliability layer: real-time scheduling and the mechanisms that recover a device nobody can reach. Priorities, rate-monotonic analysis, priority inversion and its remedies sit here, alongside watchdog design — which is mostly a study in how a safety mechanism gets neutered by being made convenient, since a watchdog refreshed from a periodic timer interrupt keeps a wedged device alive indefinitely while reporting perfect health.
It exists as one subsection rather than several because these three clusters are not separable in practice. A stack budget depends on interrupt nesting; a task's priority decides how long a shared buffer sits undrained; a watchdog design is only as good as the check-in evidence the tasks provide. An interviewer who asks about one will follow the thread into the other two, and a candidate who has studied them independently loses the connection.
The four constraints that decide every design
Before any specific technique, there are four numbers that constrain everything, and fluency in an embedded interview largely means being able to state yours.
RAM. Fixed, small, and shared between initialised globals, zeroed globals, any heap you allow, and every stack. On a part with tens of kilobytes, a single carelessly declared buffer is a meaningful fraction of the whole. This constraint is why static allocation is the default, why a const lookup table lives in flash for free, and why adding an RTOS is a decision measured in kilobytes of per-task stack.
Time. Deadlines are external and non-negotiable. A sensor must be sampled at its rate, a bus must be answered within its timeout, a motor must be commutated at its electrical frequency. Everything you add to the system spends time that some deadline was relying on, and the interesting spending is often invisible: a critical section masks interrupts, a flash erase stalls instruction fetch, a long handler delays three other handlers.
Energy. For anything battery-powered, the design is dominated by staying asleep. The arithmetic is unforgiving and counter-intuitive: a radio drawing thousands of times the sleep current for four milliseconds a minute may still be a smaller share of the budget than the sleep current itself, which is why the first optimisation is almost never the code.
Observability. You cannot attach a debugger to a device in a customer's ceiling void, and often you cannot reproduce the failure on the bench. What the device can tell you after the fact — a reset cause, a surviving crash record, a counter — is the entire debugging surface, and it has to be designed in before the failure, not after.
Where it sits in a real system
A modern connected device is a chain, and firmware occupies the first two links. Follow one measurement from the physical world to a dashboard.
flowchart TD
A[Sensor<br/>analogue or digital] --> B[Peripheral<br/>ADC, SPI or I2C]
B --> C[Interrupt handler<br/>capture and enqueue]
C --> D[Application task<br/>filter, decide, act]
D --> E[Actuator<br/>relay or motor]
D --> F[Radio or wired link]
F --> G[Gateway or broker]
G --> H[Cloud storage<br/>and dashboards]The edge worth studying is between C and D. Everything above the handler is bounded by hardware and by your own handler code, and everything below it is bounded by scheduling — so that boundary is where a timing requirement turns into a queue-sizing decision, and where data is lost quietly if the application half falls behind.
The other thing the chain shows is how narrow the embedded engineer's territory is and how much depends on it. Boxes G and H belong to backend and platform engineers, and they can be scaled, redeployed and rolled back in an afternoon. Boxes A through F are on a board that shipped, and changing them means an over-the-air update that must not brick anything. That asymmetry is why firmware culture is conservative in a way that reads as slow to people arriving from web work, and why the review questions are about what happens if power fails halfway.
Inside the device, the structure is a loop wrapped in hardware events. The relationship between the two is the thing to internalise:
flowchart TD
A[Reset vector] --> B[Set clocks<br/>and flash wait states]
B --> C[Copy initialised data<br/>and clear zero data]
C --> D[Configure peripherals]
D --> E[Enable interrupts]
E --> F[Main loop or scheduler]
F --> F
G[Hardware event] -.->|preempts anywhere| FLook at the dotted edge rather than the chain. Every interrupt in the system can arrive between any two instructions of the main loop, so every variable shared between them is a concurrency problem even on a single core with no threads — which is the specific realisation that separates someone who has written firmware from someone who has read about it.
Who does this work
The titles vary more than in web engineering, and the same person often carries two of these responsibilities.
| Role | What the day looks like |
|---|---|
| Firmware engineer | Writing drivers and application logic in C, on a desk with a board, a probe and a logic analyser attached |
| Embedded software engineer | Broader: the application layer, protocol stacks, RTOS structure, occasionally a Linux-based device |
| Real-time software engineer | Scheduling, timing analysis, worst-case execution time, usually in automotive, aerospace, industrial or medical |
| Hardware validation engineer | Bring-up of new boards, writing test firmware, characterising the hardware against its datasheet |
| Systems engineer | Deciding what the device must do, allocating requirements between hardware, firmware and cloud, owning the safety argument |
| Applications engineer at a chip vendor | Supporting customers' firmware, writing reference drivers, explaining errata |
The texture of the day is genuinely different from application development. A meaningful proportion of it happens at a bench with instruments, because the ground truth is electrical: a scope trace showing a pin toggling proves the handler ran, and no amount of reading the source does. Iteration is slower, since a build must be flashed rather than hot-reloaded, so the discipline shifts towards getting it right by reasoning first. And a good deal of the reading is not code at all but reference manuals and errata sheets, where the useful skill is finding the one paragraph in nine hundred pages that says this peripheral needs its clock enabled before its registers respond.
The two groups worth distinguishing are the people who build the firmware and the people who specify or qualify it. In a regulated industry — automotive, medical, aviation, industrial safety — there is a whole discipline around requirements traceability, hazard analysis and evidence that the software does what the safety case claims. Those engineers may write little code and still hold the most consequential decisions, and a firmware engineer in such an industry spends real time producing evidence rather than features.
Demand, adoption and how that is changing
Demand is steady rather than booming, and the reasons are worth stating precisely because both the optimistic and pessimistic versions of this are usually overstated.
The structural demand is durable. The number of devices with a microcontroller in them keeps rising — vehicle electrification alone has multiplied the number of controllers in a car, and industrial equipment, medical devices, building systems and metering all continue to gain connectivity. Every one of those needs firmware, and firmware for a physical product cannot be outsourced to a platform in the way that hosting can. There is also a persistent shortage at the experienced end. The skill takes years to acquire, it is learned largely on hardware rather than from courses, and a generation of engineers who learned it is retiring, so senior firmware roles are frequently hard to fill.
Against that, hiring volume is modest compared with cloud, web and machine learning, and it moves with hardware product cycles rather than with software budgets. A firmware team is sized to the products being developed, so it grows in steps rather than continuously, and a hardware programme cancelled means a team that is not backfilled.
Three shifts are genuinely changing the work. The first is that the boundary with Linux keeps moving: falling silicon costs mean a product that would once have been a microcontroller now often ships an application processor running Linux, which converts part of the role into systems programming. The second is security becoming mandatory rather than optional. Regulation in several jurisdictions now requires that connected devices be updatable and not ship with default credentials, and that has made secure boot, signed updates and key management standard expectations rather than differentiators — which is a real expansion of what a firmware engineer must know. The third is that the tooling is professionalising: continuous integration running on hardware in a rack, unit tests for firmware logic on a host, static analysis and hardware-in-the-loop simulation are now normal in serious teams, where a decade ago the culture was much more artisanal.
What is not happening, despite frequent predictions, is the displacement of C. Rust has real and growing adoption in embedded work and is a credible thing to learn, but the installed base, the vendor toolchains, the certified compilers and the safety evidence are overwhelmingly C, and that inertia is measured in decades rather than years. Preparing for an embedded interview means preparing in C.
What makes it hard
Not the volume of material. Three specific things.
The bug is not where the symptom is. This is the defining difficulty. With no memory protection, an overflowing stack silently overwrites a variable belonging to another module, so the observed fault is a state machine taking an impossible transition or a calibration constant becoming nonsense. Because frame sizes change with optimisation, the symptom moves when you change compiler flags, which sends people looking for a compiler bug. The same displacement happens in time: a handler that runs too long produces a fault in a completely different subsystem whose deadline it stole. Learning to reason backwards from a displaced symptom to a cause in an unrelated module is the skill, and it is not substitutable by experience elsewhere.
Concurrency exists without threads. Application programmers meet concurrency when they create a thread. In firmware, enabling one interrupt creates it, and the second party is code that can arrive between any two instructions and cannot block, cannot take a lock, and cannot wait. So the standard toolkit does not apply: you cannot put a mutex around a variable shared with a handler, because the handler has nothing to wait with. What you can do — mask interrupts briefly, give each side sole ownership of its own indices, use a hardware atomic, or move all real work into task context — is a smaller and less forgiving toolkit, and choosing among it correctly is a senior signal.
Everything is a trade against something you cannot see. Adding a feature costs RAM you may not have, time some deadline was using, and current some battery was budgeting. None of those costs appear when the feature works on the bench. They appear as a stack overflow after eleven days, a missed deadline under a load combination nobody tested, or a battery life of eight months against a specification of two years. That is why the answers in this discipline are numbers with a derivation attached, and why "it works" is not a claim anyone senior accepts.
There is a fourth difficulty that is practical rather than conceptual: the loop is slow. Reproducing a fault may need specific hardware, a specific temperature, or a week of running. So the cost of being careless is much higher than in an environment where you can rerun a test in two seconds, and the culture reflects that.
Why study it
The honest case has three parts and one disclaimer.
First, it makes you materially better at everything else. Working without an allocator, without memory protection and without a scheduler forces a mechanical model of what a program actually is — where the bytes live, what a pointer means, what the compiler is permitted to do to your code, what a preemption costs. Engineers who have written firmware carry that model into higher-level work and debug differently because of it, particularly around concurrency and performance.
Second, the career shape is unusually durable. The knowledge does not churn: interrupt semantics, memory layout and scheduling theory are the same material they were twenty years ago and will be the same in twenty more, so time invested compounds rather than expiring with a framework. That is a genuinely different proposition from web engineering, and it suits people who resent relearning the same concepts under new names.
Third, the work has a satisfying kind of finality. Something physically moves, or measures, or survives on a battery for three years, and when it is right it stays right in a fleet of a hundred thousand units. For people who find distributed systems abstract, the tangibility is the whole appeal.
The disclaimer: if your goal is the fastest route to a well-paid software job, or maximum employer optionality, this is not it. Roles are fewer and geographically clustered around hardware industries, entry-level positions are scarcer than in web development, compensation is typically below the top of the cloud and machine-learning market, and the tooling is less pleasant. If you are drawn to embedded work because you like being close to the machine, that is a good reason. If you are drawn to it because it sounds harder and therefore better paid, the premise is wrong.
Your first hour
Get one pin toggling, then look at it with something that is not the source code. That is the whole first hour, and it teaches more than a week of reading.
Buy or borrow any microcontroller development board with an on-board debug probe — most vendor evaluation boards under about twenty pounds qualify. Install the vendor's toolchain or a GCC cross-compiler with a debug server, and build the blink example that ships with it. Flash it. So far this is a tutorial. The hour becomes useful in the next four steps.
flowchart TD
A[Build and flash<br/>the blink example] --> B[Read the map file<br/>note text, data and bss]
B --> C[Add a 4KB static buffer<br/>rebuild and compare]
C --> D[Move a table to const<br/>watch RAM drop]
D --> E[Set a breakpoint<br/>before main runs]
E --> F[Inspect a global<br/>before it is initialised]The step people skip is B, and it is the one that changes how you think. Look for the middle step's effect: a buffer you never write to still consumes RAM the instant it is declared, which is the constraint of this entire discipline made visible in a diff.
Then do these four things in order.
Open the map file the linker produced and find the sizes of .text, .data and .bss. Write the three numbers down. Now declare a static uint8_t scratch[4096]; at file scope, rebuild, and compare — you have just spent four kilobytes of RAM on a buffer you never touch. Change a large initialised array to static const and watch the RAM figure fall while flash rises. You now understand the linker's arithmetic better than most candidates articulate it.
Next, attach the debugger and set a breakpoint on the reset handler rather than on main. Step through the start-up code and watch the copy loop and the zeroing loop run. Inspect a global variable before the zeroing loop reaches it. This is the concrete version of "there is no runtime here except the one you can read".
Then break the blink deliberately. Remove the volatile from a flag written in an interrupt handler and read in the main loop, turn optimisation up, and observe the loop never noticing the change. Put it back. You have now met the single most-asked question in the discipline as an experiment rather than as a definition.
Finally, toggle a second pin high at the start of your delay and low at the end, and put a logic analyser or a cheap oscilloscope on it — or, failing an instrument, use a second LED and a much longer delay. Measuring rather than assuming is the habit the whole field runs on, and this is the smallest possible version of it.
The artefact at the end of the hour is a board that blinks, three numbers from a map file that you can explain, and a bug you caused and fixed on purpose. That is enough to make the question pages readable.
What this is not
It is not electronics. You will read schematics and you must know what the hardware requires of your code, but component selection, board layout and analogue design belong to a hardware engineer, and claiming otherwise in an interview collapses at the first specific question.
It is not embedded Linux, though the two are constantly conflated. Once there is a kernel, an MMU, processes and a filesystem, most of the constraints that define this area — no allocation, no protection, one flat address space, worst-case stack budgets — stop applying. Embedded Linux work is closer to systems programming, and it is a legitimate and separate specialism with its own interview.
It is not IoT in the sense the term is usually marketed. A great deal of what is sold as IoT engineering is a cloud back end, a message broker and a dashboard, all of which is backend and data work that happens to be fed by devices. The device-side half is what this area covers, and it is the half where a mistake ships in hardware.
It is not obsolete, and it is not a career backwater kept alive by legacy systems. That misconception comes from the tooling looking dated next to modern web development. The volume of new microcontroller-based product development is not falling, and the security and update requirements now attached to connected devices have made the work more demanding rather than less.
Finally, it is not a discipline where being clever is the point. Firmware review culture rewards the boring, provable, statically allocated version over the elegant one, because the elegant one has to be reasoned about at three in the morning by someone who did not write it, on a device that cannot be restarted by hand.
Everything else in this area follows from one habit: state the number and how you got it. This build has 41KB of
.bss, the deepest measured stack use is 3.2KB against 6KB allocated, the worst-case handler runs for 12 microseconds, and here is the measurement that produced each figure.
Where to go next
Now practise it
3 interview questions in Embedded Systems & IoT, each with the rubric the interviewer is scoring against.
- How do you size the stack and the heap on a device with a fixed RAM budget?
- How do you use a watchdog timer to recover a wedged device without hiding the bug that wedged it?
- What belongs inside an interrupt service routine, and what must be kept out of it?