The Embedded 12-Factor App (E12FA)

The 12 factor application, https://12factor.net/ is a great checklist to building Applications. I have found it to be a greate guideline that I believe is a good checklist for most software projects. As such I started to aaply it to my applications. Then I thought that I could apply it to embedded systems. But there were a number of differences which I thought I could air out with this blog post defining the E12FA according to how I think they can help in Embedded software projects.

This blog was written with assistance from Gemini by Google.


1. Codebase: One codebase tracked in revision control, many deploys.

Embedded System Interpretation (E12FA I)

The core principle remains: one application (the firmware) corresponds to one repository.

  • Version Control is Paramount: All source code, configuration files, build scripts, and HAL definitions must live in a single Git repository.
  • The “Deploy” is the Firmware Image: A “deploy” isn’t spinning up a new container; it’s generating a firmware binary (e.g., hex, .bin, .elf) designed for a specific hardware target.
  • Targeting Multiple Boards: If you have closely related hardware versions (e.g., v1.0 vs. v1.1 of the same product), they should generally share the codebase, using conditional compilation or build flags to manage minor differences in MCU peripherals or pin assignments. For fundamentally different products, consider separate repositories.

Key Takeaway

Use tags in your VCS (Version control system) to precisely mark the SHA used to generate every released firmware image. This ties the binary back to the exact source code state, which is crucial for debugging and compliance.


2. Dependencies: Explicitly declare and isolate dependencies.

Embedded System Interpretation (E12FA II)

This factor is about achieving reproducible builds. When you can’t rely on operating system packages, you must explicitly manage the entire development environment.

  • Toolchain and RTOS: The most critical dependencies are the exact versions of your compiler (e.g., GCC for ARM), linker, debugger, and any chosen RTOS or foundational framework (like Zephyr, FreeRTOS, or Mbed).
  • Submodules and Vendors: If using third-party libraries or vendor-provided hardware abstraction layers (HALs), they should be managed via mechanisms like Git submodules** or a dedicated dependency manager (e.g., West, Conan).
  • Isolation: The build process should never rely on libraries installed system-wide. All dependencies must be either vendored (copied into the project) or referenced relative to the project root.

Key Takeaway

Use a containerized environment (e.g., Docker) or a dedicated virtual machine to host your entire toolchain and build environment. This guarantees that your CI/CD server, your colleague, and your own machine all produce the identical binary from the same source code.


3. Config: Store config in the environment.

Embedded System Interpretation (E12FA III)

This factor splits into three distinct types of configuration, because true “environment variables” aren’t a practical execution model for an MCU:

Config TypeWhere it’s StoredExample DataPrinciple
A. Compile-Time (Static)Source Code/Build SystemMCU memory map, peripheral addresses, max buffer size.Stays with the firmware image (Factor V). Changes require a re-flash.
B. Deployment (Semi-Dynamic)Provisioning/Flash DataDevice ID, initial network keys, calibration factors.Injected once during the factory provisioning phase (after Factor V). Distinct from the code.
C. Runtime (Dynamic)Non-Volatile StorageWi-Fi SSID/Password, MQTT endpoint URL, user settings.Can be changed over-the-air or by the user. Stored in a dedicated Flash sector (e.g., EEPROM emulation).

Key Takeaway

Never bundle runtime configuration (C) into the firmware binary (A). Separate your configuration from your code. Use a struct or key-value storage API to abstract access to runtime configuration, allowing the underlying non-volatile storage to be swapped without changing the application logic.


4. Backing Services: Treat attached resources as backing services.

Embedded System Interpretation (E12FA IV)

In the cloud, a database or a queue is accessed over the network. In an embedded system, this factor applies to any resource the application consumes but does not manage itself, whether local hardware or remote services.

  • Internal Backing Services: These are local peripherals like I2C sensors, SPI flash chips, ADCs, DACs, Timers, and DMA controllers.
    • Principle: Access them through well-defined, reusable HAL or driver APIs. The application should only know the interface, not the implementation (e.g., it asks for “temperature,” not “read register 0x1A on I2C address 0x48”). This allows you to swap out hardware (e.g., a BME280 for a SHT31) with minimal code change.
  • External Backing Services: These are cloud resources like MQTT brokers, REST APIs, or OTA update servers.
    • Principle: Connection details must be configurable at runtime (Factor III) and the application must be designed to handle their absence gracefully (e.g., retry logic, offline caching).

Key Takeaway

Abstract hardware access. If a backing service (internal or external) becomes unavailable, the core application logic should not crash. This is crucial for robustness and hardware independence.


5. Build, Release, Run: Strictly separate build and run stages.

Embedded System Interpretation (E12FA V)

This is a critical factor for OTA (Over-The-Air) updates and production quality. The goal is an immutable artifact that is tested once and never changed.

StageDescriptionArtifact CreatedEmbedded Action
BuildConverts code into a ready-to-flash binary.Immutable Firmware Binary (.bin, .hex)Compile C/C++ code, link against dependencies (Factor II).
ReleaseCombines the binary with deployment configuration.Release Package (OTA file, factory image)Add metadata (version, target ID), sign the binary, and bundle it with the initial configuration data (Factor III.B). This package is what is deployed.
RunExecutes the release package on the hardware.Running SystemThe Bootloader verifies the signature and flashes the binary; the application loads its runtime config (Factor III.C) and starts executing.

Key Takeaway

Once the Release Package is created, it is frozen. Any change, even a single bit in the configuration, requires a new Release to be created, ensuring traceability and a consistent QA process.


6. Processes: Execute the app as one or more stateless processes.

Embedded System Interpretation (E12FA VI)

This factor demands the most adaptation, as an MCU managing hardware registers is inherently stateful.

  • Stateless Component Isolation: While the device as a whole holds state (e.g., the current position of a motor), individual RTOS Tasks or Threads should be as stateless as possible. Tasks should ideally only work on data passed to them and then output results, avoiding shared global state wherever possible.
  • Externalize Volatile State: Persistent state (like user settings) goes into non-volatile storage (Factor III). Volatile state (e.g., sensor readings, connection status) should be externalized from the functional tasks into a centralized State Manager module or a set of well-defined data structures protected by mutexes or accessed via message queues.
  • Disposability and Resilience: If a task crashes or a watch dog resets the MCU, the state should be quickly recoverable (Factor IX). The application must never depend on the local memory or process ID of any task for critical operations.

Key Takeaway

Embrace the RTOS task model for concurrency (Factor VIII), but enforce strict APIs between tasks. Think of a Wi-Fi connection task—it shouldn’t care about the application logic; it just focuses on connecting and reporting its status to the central state manager.


7. Port Binding: Export services via port binding.

Embedded System Interpretation (E12FA VII)

For cloud apps, this means exposing an HTTP port. For embedded systems, the “port” represents the physical or logical interface used for communication, configuration, or management.

  • Self-Contained Access: The application must be designed to be self-contained and expose its services through well-known interfaces defined by the hardware and networking stack.
  • The “Ports”:
    • Network: TCP port for MQTT or HTTPS, BLE advertising channels, LoRaWAN duty cycles.
    • Local: UART for debugging/local control, USB enumeration endpoints, physical pins for GPIO interaction.
  • Decoupling: The core firmware logic should not hardcode the network stack’s management. It should assume the operating environment (the RTOS and network drivers) handles the actual binding and allows the application to focus on the data and protocol.

Key Takeaway

Define your device’s external interface contract clearly. The device is a “server” to the outside world, whether it’s serving a web page on port 80 or accepting configuration via a BLE characteristic.


8. Concurrency: Scale out via the process model.

Embedded System Interpretation (E12FA VIII)

Unlike cloud apps that scale horizontally by adding more servers, embedded systems scale vertically through concurrency management and maximizing single-chip efficiency.The core principle of E12FA VIII is not just “use an RTOS,” but to structure the application to maximize predictability and isolation of different functional components, regardless of the underlying scheduler.

A. The RTOS Model (Task-Based)

  • Task/Thread Model: Concurrency is achieved using RTOS tasks or threads. Each functional component (e.g., Sensor Reader, Network Sender, User Interface) should ideally be its own task.
  • Prioritization: Utilize RTOS scheduling to assign appropriate priorities to tasks (e.g., control loops get high priority; logging gets low priority) to ensure real-time constraints are met.
  • Communication: Tasks must communicate cleanly using RTOS primitives like queues, semaphores, and mailboxes rather than shared memory alone. This limits contention and race conditions.

B. The State Machine/Super-Loop Model (Execution Isolation)

When an RTOS is not present, we must enforce isolation and concurrency through meticulous code design.

Execution MechanismConcurrency StrategyIsolation & Predictability
Super-LoopTime Slicing & Polling: The main loop repeatedly calls functions. Concurrency is achieved by ensuring each function executes quickly and non-blockingly.Non-Blocking Calls: Every component function (e.g., read_sensor(), send_network_packet()) must return quickly. Never use long delays (sleep()).
Finite State Machine (FSM)State-based Sequencing: The entire application (or a major subsystem) progresses through discrete, well-defined states based on events.Explicit State Transitions: Isolation is achieved by ensuring code for one state does not directly interfere with another. Data is passed between states using the central FSM state data structure (Factor VI).
Interrupt Service Routines (ISRs)Event-Driven: ISRs handle high-priority, time-critical events (e.g., GPIO change, timer expiry).Minimal Work & Hand-off: ISRs should do the absolute minimum required and immediately hand off complex processing to the main loop or FSM using flags or message buffers to maintain predictability.

Key Takeaway for Non-RTOS Systems

Concurrency is replaced by execution isolation and temporal predictability. The principle is:

  1. Break functionality into independent functions.
  2. Ensure these functions are non-blocking (run quickly and yield control).
  3. Use a single, centralized mechanism (the state structure or a central dispatcher) to manage the flow of control and shared data.

This ensures that a slow operation in one area (e.g., waiting for I/O) doesn’t completely starve other critical functions, maintaining the goal of robust, reliable execution that E12FA VIII requires.

Key Takeaway

Concurrency is your primary tool for managing complexity and meeting performance deadlines. Structure your firmware as a set of cooperating, isolated tasks governed by the RTOS scheduler.


9. Disposability: Maximize robustness with fast startup and graceful shutdown.

Embedded System Interpretation (E12FA IX)

In embedded systems, “disposability” means the device can handle unexpected failure or planned power cycles without data loss or corruption.

  • Fast Startup (Crucial): Minimize the time from power-on (POR) to being operational (e.g., reading sensors, ready for network connection). This means deferring non-essential initialization until after core functionality is running.
  • Graceful Shutdown: On a planned power-off or restart command (if supported), the firmware must:
    1. Halt new operations.
    2. Flush buffers (logs, data) to non-volatile storage (Factor XI).
    3. Perform cleanup (e.g., release locks, unmount file systems).
  • Resilience to Failure: The system must treat unhandled exceptions or watchdog resets as a normal part of life. The startup routine must check the last shutdown state and be able to recover state from non-volatile storage (Factor VI) to resume operation quickly and safely.

Key Takeaway

Design the system to be failure-tolerant. The moment the device boots, it should assume the last power-down was ungraceful and perform checks to ensure data integrity before proceeding.


10. Dev/Prod Parity: Keep development, staging, and production as similar as possible.

Embedded System Interpretation (E12FA X)

This factor is about minimizing the gap between where you write the code and where it runs. The famous phrase “it worked on my machine” is a disaster in embedded systems.

  • Toolchain Parity: Use the exact same compiler, linker, and build scripts (Factor II) for local development, CI (Continuous Integration), and final production builds. A difference in compiler flags or linker versions can change the resulting binary and introduce subtle bugs.
  • Target Parity:
    • Development: Use the actual target hardware for all final testing. If you must use a simulator (e.g., QEMU), ensure it is only for unit-testing the highest-level logic, not for validating HAL or I/O behavior.
    • Testing: Set up Hardware-in-the-Loop (HIL) testing environments that accurately simulate the external world (sensors, power events, network conditions) to maximize confidence before deployment.
  • Configuration Parity: Use the Release Package method (Factor V) to ensure the only difference between environments is the specific runtime configuration (e.g., test MQTT broker vs. production MQTT broker), which is stored separately (Factor III).

Key Takeaway

Build once, test everywhere. The binary running in production should be the identical binary that passed your QA and HIL tests.


11. Logs: Treat logs as event streams.

Embedded System Interpretation (E12FA XI)

Logging is the primary form of observability for devices in the field, which are often disconnected and lack a standard operating system for log collection.

  • Event-Driven Logging: Logs should be treated as a time-ordered stream of discrete events, not just random print statements. Each log message should include timestamp, log level, source task/module (Factor VIII), and the message payload.
  • Storage Tiers:
    1. Volatile Buffer: High-volume, short-term logs stored in a fast RAM buffer. Lost on reset.
    2. Persistent Storage: Critical error logs, warnings, and history persisted to non-volatile memory (Flash, SD card) for post-mortem analysis (Factor IX).
    3. Transmission: For connected devices, logs should be efficiently streamed over the network (e.g., batched MQTT messages or secure log-forwarding protocol) to a central server for long-term aggregation and analysis.
  • Controllable Verbosity: The logging level (DEBUG, INFO, WARN, ERROR) must be configurable at runtime (Factor III) to manage resource usage and network bandwidth.

Key Takeaway

Implement a robust, tiered logging mechanism that ensures you can retrieve post-mortem logs from a failed device, even if it was offline.


12. Admin Processes: Run admin/management tasks as one-off processes.

Embedded System Interpretation (E12FA XII)

Admin tasks are specific maintenance, diagnostic, or provisioning operations that run outside the main loop of the application. They are distinct from the continuous, core functionality.

  • Isolated Execution: Admin tasks must run as separate RTOS tasks (Factor VIII) or be triggered by specific, secure events (e.g., a maintenance GPIO pin is pulled low, a password-protected CLI command is entered). They must clean up their resources upon completion.
  • Common Admin Tasks:
    • Factory Provisioning: Writing unique IDs and initial keys to non-volatile config (Factor III.B).
    • Diagnostics: Running sensor self-tests or memory integrity checks.
    • Firmware Update: The OTA update reception and flashing process.
    • Secure Reset: Triggering a factory reset that wipes runtime config (Factor III.C) but leaves the firmware intact.
  • Security: These processes must be strictly permissioned and secure, as they often have privileged access to the hardware or configuration.

Key Takeaway

Separate maintenance tasks from core product features. Ensure these powerful, one-off tools are only accessible via a secure, designated interface.


Summary of the Embedded 12-Factor App (E12FA)

By adapting the 12 Factors to the reality of constrained resources, stateful execution, and custom hardware, we shift the focus from horizontal scaling and environment variables to reproducible builds, reliable state management, and robust, observable field operation. Try it out. Give me feedback Or come back for updates to my opinion on the E12FA

Leave a Reply

Your email address will not be published. Required fields are marked *