A reliable embedded system does not start with a microcontroller board or a code editor. It starts with a set of requirements that can be tested. The hardware, firmware, interfaces, power architecture, timing, and validation plan should all trace back to those requirements.
This workflow is deliberately practical: define what the product must do, turn those needs into measurable engineering constraints, select hardware from those constraints, design firmware around explicit interfaces, then validate the complete system under normal and fault conditions.
1. Turn the idea into measurable requirements
“Build a smart sensor” is an idea, not a requirement. A useful requirement has an observable result and, where relevant, a limit.
| Weak statement | Engineering requirement |
|---|---|
| The device should be low power. | The device shall operate for at least 30 days from the specified battery under the defined duty cycle. |
| It should react quickly. | The output shall respond within 50 ms of a valid input event. |
| It should store data. | The device shall preserve at least 10,000 timestamped records across a power cycle. |
| It should be reliable. | The system shall recover automatically after loss and restoration of network connectivity without manual reset. |
Capture functional requirements, timing, accuracy, power, environmental range, interfaces, safety constraints, storage, communications, startup/recovery behavior, update strategy, physical size, cost target, and serviceability before choosing parts.
2. Build a system block diagram before the schematic
Draw the major blocks and the signals between them. A typical embedded product may contain:
- power input, regulation, protection, and battery management;
- microcontroller or processor;
- sensors and analog front ends;
- actuators or loads;
- non-volatile storage;
- wired interfaces such as UART, I2C, SPI, CAN, USB, or Ethernet;
- wireless interfaces such as Wi-Fi, Bluetooth, or cellular;
- debug/programming interface;
- user interface;
- external connectors and protection.
For every connection, write the voltage domain, protocol, direction, maximum rate, connector/pin, ownership, and failure behavior. Many “random” embedded bugs are actually interface-contract mistakes: wrong voltage, wrong pull-up, timing assumptions, missing common ground, conflicting bus ownership, or incompatible data formats.
3. Make a resource budget before selecting the MCU
Choose the MCU or processor from the actual workload, not from familiarity alone. Build a budget for:
- GPIO count and electrical requirements;
- ADC/DAC resolution and sampling rate;
- timers, PWM channels, capture/compare;
- UART/SPI/I2C/CAN/USB/Ethernet peripherals;
- RAM and flash, including margin for future features;
- CPU load and worst-case interrupt rate;
- low-power modes and wake sources;
- package, temperature range, availability, and cost;
- debug features such as SWD/JTAG and trace support.
Do not design to 100% resource utilization. Leave engineering margin for bootloader space, logging, diagnostics, library growth, stack usage, and requirements that appear later.
4. Design power as a system, not a regulator symbol
Create a power tree showing every rail and load. Estimate average and peak current, startup/inrush current, radio peaks, actuator transients, battery behavior, regulator efficiency, and thermal dissipation.
Then define startup and shutdown behavior. Ask what happens if the supply rises slowly, dips during radio transmission, or is removed during a flash write. Add brownout/reset supervision, bulk capacitance, decoupling, reverse-polarity or transient protection where the product requires them.
A firmware feature cannot repair an unstable power rail.
5. Partition firmware into explicit responsibilities
Even a small bare-metal project benefits from clear layers. One practical structure is:
- Hardware abstraction: GPIO, ADC, timers, UART/SPI/I2C, flash.
- Device drivers: sensor, display, modem, motor driver, external memory.
- Services: logging, storage, communications, timekeeping, diagnostics.
- Application logic: states, decisions, user behavior, control algorithm.
Keep application logic from depending directly on register details everywhere. This makes it easier to test logic, replace hardware, and isolate faults.
6. Define the system states
Write the major states explicitly. For example:
BOOT
SELF_TEST
IDLE
ACQUIRE
PROCESS
TRANSMIT
LOW_POWER
FAULT
For each state define the entry condition, allowed events, timeout behavior, outputs, and next states. Decide what happens after watchdog reset, sensor timeout, corrupt configuration, lost network, low battery, full storage, and failed firmware update.
A system that only defines the happy path is not finished.
7. Establish timing and concurrency rules
List all periodic work and deadlines. Example:
| Activity | Rate/deadline | Priority concern |
|---|---|---|
| Motor control loop | 1 kHz | Deterministic execution |
| Sensor acquisition | 100 Hz | Timestamp consistency |
| Telemetry publish | 1 Hz | Must not block control loop |
| UI refresh | 10 Hz | Lower priority |
| Flash logging | Event-driven | Avoid long blocking writes in critical paths |
If a superloop can meet the deadlines cleanly, use it. If the system has multiple independent activities, blocking I/O, or more complex timing, an RTOS may improve structure—but an RTOS does not remove the need for a timing design.
8. Design observability before debugging starts
Make the product explain what it is doing. Useful mechanisms include:
- structured serial logs with severity and timestamps;
- fault codes stored across reset;
- watchdog-reset reason reporting;
- test points on critical power and communication signals;
- runtime counters for overruns, dropped packets, retries, and queue depth;
- version/build identifiers visible in logs or UI.
Without observability, a field failure becomes guesswork.
9. Build hardware and firmware in controlled increments
Do not wait for a fully populated custom PCB before proving every subsystem. A safer sequence is:
- prove the highest-risk sensors/interfaces on development hardware;
- verify timing, throughput, and software architecture;
- freeze the electrical interfaces;
- design and review the custom PCB;
- bring up the PCB rail-by-rail and interface-by-interface;
- integrate the application incrementally;
- run system-level fault and endurance tests.
This does not mean staying on development boards forever. It means retiring technical risks before combining them.
10. Bring up a new PCB safely
Before applying full power, inspect polarity, solder bridges, connector orientation, resistance between major rails and ground, and obvious assembly defects.
On first power-up, use current limiting when practical. Verify each rail before connecting expensive loads. Confirm reset and clock behavior, then establish the debug connection and run the smallest firmware that proves MCU execution.
Next test peripherals one at a time. A useful bring-up order is:
- power rails and reset;
- programming/debug interface;
- clock and basic GPIO;
- serial debug output;
- external buses;
- sensors/storage;
- communications;
- high-current loads/actuators.
11. Test requirements, not just features
Create a verification matrix that maps each requirement to a test and evidence.
| Requirement | Test | Evidence |
|---|---|---|
| Boot in <2 s | 50 cold boots across supply range | Boot-time log |
| Recover after Wi-Fi loss | Disable AP for 5 min, restore | Reconnect time + retained data count |
| 30-day battery target | Measure current by operating state and duty cycle | Power budget + measured profile |
| Preserve configuration | Power-cycle during defined write scenarios | Configuration integrity results |
Repeat tests after firmware changes that touch shared drivers, timing, memory, storage, or power behavior. Regression testing matters because embedded changes often have system-wide side effects.
12. Deliberately test failure paths
Force the system into conditions it will eventually experience:
- sensor disconnected or returning invalid data;
- communication timeout;
- network loss and recovery;
- nearly full flash/storage;
- unexpected reset;
- rapid power cycling;
- low supply voltage;
- maximum expected data rate;
- long-duration operation.
Record the expected recovery behavior before running the test. “It recovered eventually” is not a measurable acceptance criterion.
13. Freeze a reproducible release
A release should connect firmware, hardware, and configuration. Record:
- PCB revision and manufacturing package;
- BOM revision;
- firmware Git commit/tag;
- compiler/toolchain version;
- configuration/calibration format version;
- programming procedure;
- verification results and known limitations.
If another engineer cannot reproduce the build, program a fresh board, and run the validation from your documentation, the project is not yet production-ready.
Practical architecture checklist
- Requirements are measurable.
- Interfaces have voltage, timing, ownership, and failure rules.
- MCU resources have margin.
- Power budget includes peak and startup behavior.
- Firmware responsibilities are separated.
- System states and timeouts are explicit.
- Critical paths have timing budgets.
- Logs, test points, and reset reasons provide observability.
- PCB bring-up sequence is defined.
- Every critical requirement has a verification method.
- Fault recovery is tested deliberately.
- Hardware/firmware/configuration versions are linked in the release record.
Next, see why embedded projects fail after deployment and the embedded engineer skill stack.