One Codebase, Many Products: Configuration-as-Code for Embedded Firmware at Scale

Containerized Embedded Development: What Docker and CI/CD Actually Change for Firmware Teams

 

An embedded firmware team that started with one product and added variants over three years typically ends up in one of two places. In the first, they have a single codebase with a clean configuration layer, and new variants are added by writing a configuration file and running the build. In the second, they have forked the firmware at each variant boundary, now maintain five or ten or twenty parallel branches that have diverged substantially, and every security patch or bug fix must be cherry-picked into each fork manually. The second state is extremely common. It develops gradually, through individually reasonable decisions: the variant needed a different sensor, the customer required a different communication protocol, the regional SKU needed a different regulatory configuration. Each modification seemed too small to justify the architecture investment that would have kept everything in a single maintainable codebase.

By the time a product line reaches 20 SKUs — let alone 100 — the cost of the forked architecture has compounded enormously. A critical fix takes weeks rather than hours to propagate across all variants. Regression testing must be repeated for every fork. The CI/CD pipeline requires separate jobs for each branch. The team cannot confidently answer which variants are affected by a newly discovered vulnerability. The documentation describing what differs between variants exists in engineering memory rather than in version-controlled files.

Configuration-as-code is the architectural approach that prevents this outcome. It means encoding every dimension of product variation — hardware board layout, peripheral assignments, feature flags, operational parameters, regulatory constraints — as structured, version-controlled, reviewable configuration artifacts that the build system uses to produce variant-specific firmware binaries from a single unified source. A product line with 100 SKUs has 100 configuration files and one firmware codebase, not 100 firmware branches.

The Three Dimensions of Embedded Firmware Variation

Before designing a configuration architecture, it is necessary to understand what actually varies between product SKUs, because different types of variation belong in different configuration layers. Mixing them produces configuration files that are difficult to validate and a codebase that becomes brittle as the product line grows.

Hardware variation describes differences in the physical circuit board: which microcontroller or processor is used, which peripherals are connected, which GPIO pins carry which signals, what the I2C address of each sensor is, which UART is connected to which external interface, and what the power supply topology implies for voltage rail sequencing. These facts are immutable properties of a specific PCB revision and must be described with hardware-level accuracy. They cannot be encoded as feature flags — a sensor that is physically absent on a board cannot be conditionally compiled in by selecting a flag; the driver simply must not exist in that build's binary.

Software feature variation describes which capabilities the firmware presents: whether cellular connectivity is enabled, which communication protocols are supported, whether the diagnostic interface is accessible, which safety interlocks are active, which OTA update mechanism is used. Feature variation is more flexible than hardware variation — the same hardware can run with different feature sets for different market segments — and can change over the product's life through OTA updates if the feature activation is done at runtime rather than compile time.

Operational parameter variation describes numeric and string values that calibrate the firmware's behavior without changing its structure: reporting intervals, threshold values, device identifiers, regional regulatory parameters, manufacturing calibration offsets, server endpoints for different deployment environments. These are the values that most naturally belong in a structured configuration file like JSON or YAML and that the firmware reads at boot to configure its runtime behavior.

A robust configuration-as-code architecture maintains a clean separation between all three variation types and provides an appropriate mechanism for each:

Variation type

Appropriate mechanism

Version controlled artifact

Changed by

Hardware layout

Device tree (DTS) + overlays

.dts, .overlay files

Hardware engineer

Software features

Kconfig symbols, CMake feature flags

defconfig, prj.conf, CMakeLists

Software architect

Operational parameters

JSON/YAML config manifest, NVM partition

config.json, device profile

Product manager, field

Regulatory / regional

Kconfig + parameter overlay

region-specific defconfig

Compliance team

Customer-specific behavior

Combination of feature + parameter layers

Customer config overlay

Account engineer

Hardware Description as Configuration — Device Trees and Overlays

The Linux kernel's device tree mechanism, adopted by Zephyr RTOS and increasingly used in other embedded contexts, provides the most mature infrastructure for encoding hardware variation as a version-controlled text artifact. A device tree source file (.dts) describes the hardware topology: processor, memory map, peripheral addresses, interrupt lines, GPIO assignments, I2C bus topology with device addresses, SPI bus with chip selects, and boot-time configuration for each peripheral. A device tree overlay (.overlay) modifies a base hardware description by adding, removing, or changing nodes and properties — the mechanism that makes variant hardware derivation tractable without duplicating the entire base description.

For a product family built around a common SoC with variant peripheral configurations, the device tree architecture would structure as follows. A common SoC-level .dtsi describes the processor and its on-chip peripherals. A base board .dts includes the SoC .dtsi and describes the hardware common to all variants: power supply regulators, crystal oscillators, fundamental memory. Each variant's .overlay then adds or modifies only what differs: the specific temperature sensor model on variant A versus variant B, the presence or absence of an accelerometer, the GPIO pin assignment for a variant-specific LED indicator, the I2C address of a sensor that changed between board revisions.

Zephyr's Kconfig system, running alongside device tree, handles the software feature dimension. Kconfig uses a hierarchical dependency language to define boolean, integer, and string configuration symbols. A defconfig file for a specific product variant specifies the values of the Kconfig symbols that define that variant's feature set: CONFIG_BT=y to enable Bluetooth, CONFIG_CELLULAR=n to exclude the cellular stack, CONFIG_SENSOR_FUSION=y to include the sensor fusion subsystem. The build system combines the device tree output with the Kconfig output to produce autoconf.h and the devicetree.h generated constants, from which all conditional compilation in the firmware source code derives.

Zephyr explicitly documents the intended division: device tree handles hardware and its boot-time configuration; Kconfig handles which software features are built into the image. This separation is not just organizational preference — it prevents the most common configuration architecture mistake, which is encoding hardware facts (which pin is connected to which device) as software feature flags. When a GPIO assignment is a Kconfig symbol, it can be set to a different value by a different defconfig, producing a firmware binary that compiles correctly but writes to the wrong GPIO pin on actual hardware. When it is a device tree property, the build system enforces that the assigned pin exists in the hardware description, and changing it requires an overlay that is reviewed against the actual schematic.

Kconfig and Defconfig Files as SKU Descriptors

For firmware teams managing 100+ SKUs on platforms that support Kconfig — Zephyr, Linux kernel, or projects that adopt kconfiglib independently — the per-SKU defconfig file is the primary SKU descriptor. It is a text file containing the Kconfig symbol assignments that define that SKU's feature set, committing the complete configuration specification to version control in a form that is human-readable, diff-able, and reviewable.

A defconfig file for an industrial IoT sensor gateway might look like:

CONFIG_NORDIC_NRF5340=y

CONFIG_BT=y

CONFIG_BT_MESH=n

CONFIG_CELLULAR=n

CONFIG_ETHERNET=y

CONFIG_MODBUS_CLIENT=y

CONFIG_OPC_UA_CLIENT=n

CONFIG_OTA_UPDATE=y

CONFIG_DIAGNOSTIC_UART=n

CONFIG_SENSOR_TEMP=y

CONFIG_SENSOR_HUMIDITY=y

CONFIG_SENSOR_VIBRATION=n

CONFIG_WIFI=n

CONFIG_FOTA_MAX_RETRIES=3

CONFIG_REPORTING_INTERVAL_SEC=60

The same product in its industrial vibration monitoring variant would have a defconfig differing primarily in enabling CONFIG_SENSOR_VIBRATION=y and adjusting reporting interval. The device tree overlay for the vibration variant would add the accelerometer node with its I2C address and interrupt GPIO assignment.

This architecture means that adding a new SKU is precisely as complex as the difference between that SKU and the closest existing SKU: write an overlay for any hardware differences, write a defconfig for any feature differences, and add both to the CI build matrix. There is no firmware branch to create, no application code to modify, and no risk of the new variant's configuration accidentally modifying shared code that other variants depend on — because all variant-specific information is in the configuration layer, not in the application code.

The practice of placing configuration code in separate repositories from application code, as documented in embedded configuration management guidance, reinforces this clean separation at the repository level. The core firmware repository contains application logic, drivers, and the BSP. Separate per-SKU or per-customer configuration repositories contain the defconfig files and device tree overlays for each variant. This structure means a configuration change for variant X cannot accidentally modify the firmware code that variant Y runs — the change to X's configuration repository has no path to Y's firmware unless explicitly propagated.

Runtime Configuration — The Third Layer

Compile-time configuration through Kconfig and device tree handles the variation that is fixed when the firmware binary is built. Runtime configuration handles the variation that must be adjustable after the device is deployed — operational parameters, regional settings, customer-specific thresholds, fleet management metadata.

The architecture for runtime configuration in embedded firmware follows a consistent pattern: a dedicated non-volatile storage partition holds a structured configuration manifest that the firmware reads at startup, validates against a schema, and uses to configure its runtime behavior. The manifest format is typically JSON or a binary-encoded equivalent (CBOR, MessagePack) for constrained environments; on platforms with sufficient storage, JSON is preferable because it is human-readable, schema-validatable, and debuggable with standard tooling.

The configuration manifest as a version-controlled artifact is the key property that justifies the "as code" framing. Each device's configuration manifest is stored in the device management system — alongside the device's firmware version, certificate, and operational status — as a versioned document with a known schema. Provisioning a new device means writing its firmware binary and its configuration manifest to the appropriate partitions. Changing an operational parameter across a fleet means updating the manifest version in the device management system and pushing it to devices through the OTA update channel, distinct from the firmware OTA channel. This separation is important: changing a reporting interval or a server endpoint should not require rebuilding and validating a firmware binary.

The schema enforcement mechanism for the runtime configuration manifest is as important as the manifest itself. A configuration that passes JSON parsing but contains an out-of-range value — a reporting interval of zero, a sensor threshold above the hardware's measurable range, a server endpoint with a malformed URL — can produce subtle failures at runtime that are difficult to diagnose. Schema validation at provisioning time, before the configuration manifest is written to the device, prevents this class of defect. Validation at startup, when the firmware reads the manifest, provides a second line of defense against configuration corruption. Both are necessary, and both require the schema to be version-controlled alongside the firmware and the configuration manifest.

 

docker

 


Build System Integration — Producing All Variants

The CI/CD pipeline must build and test all SKUs on every commit to the firmware codebase, or the configuration-as-code architecture provides version control discipline without the engineering confidence that it is actually working. A bug introduced in a shared subsystem can silently break a subset of SKUs if only a representative subset is built in CI.

For a product family with 100 SKUs, building all 100 on every firmware commit is feasible with parallelization: a build matrix that spawns 100 parallel build jobs takes roughly as long as a single build if the CI infrastructure scales horizontally. The CMake and Kconfig build systems both support this through their command-line configurability — each variant's build is invoked with its defconfig and overlay files as arguments, and all variants share the same compilation cache for source files whose configuration is identical.

The build output for each variant includes not just the firmware binary but the complete configuration record: the .config file showing all resolved Kconfig symbols, the device tree binary (.dtb), and a build manifest capturing the compiler version, the firmware version, and a hash of the configuration inputs. These artifacts are the audit trail that allows answering questions like "which firmware binary does SKU-047 run, and what is its configuration?" from the CI artifact store rather than from engineering memory.

Testing across variants requires thoughtful fixture design. Some tests are configuration-neutral — unit tests of core logic, protocol stack tests, cryptography tests — and run identically on all variants. Others are configuration-specific — tests that verify a sensor driver works, which only run for variants that include that sensor. The test discovery mechanism must map each test to the set of SKUs for which it is relevant, and the CI pipeline must run only relevant tests for each SKU rather than attempting to run all tests on all variants.

The following validation hierarchy scales gracefully to 100+ SKUs:

  • Build validation: all SKUs, every commit — confirms all configurations compile without error
  • Unit test suite: configuration-neutral tests, all SKUs — confirms shared logic is correct
  • Integration tests: configuration-specific tests, relevant SKU subset — confirms driver and subsystem behavior for each configuration
  • Hardware regression: representative SKU sample on actual hardware — confirms hardware interaction is correct for the configuration combinations most likely to diverge
  • Full hardware regression: all SKUs on hardware — run on release candidates, not on every commit

Managing Configuration Debt and Variant Proliferation

Configuration-as-code architecture does not prevent variant proliferation — it makes it visible and manageable. A product line that has grown to 100 SKUs over five years likely has some SKUs that are minor variants of other SKUs, some that are customer-specific customizations that could be generalized, and some that have configuration combinations that were valid when created but are now inconsistent with subsequent architectural decisions.

The discipline that prevents configuration debt from accumulating is requiring that every new SKU be created by composing existing configuration primitives rather than by introducing new ones. A new hardware variant should be described by a new overlay that modifies an existing board description rather than by a new board description written from scratch. A new feature variant should be described by a new defconfig that differs from an existing defconfig by the minimum required delta. When a new SKU requires a configuration primitive that does not exist — a new hardware interface, a new feature flag — that primitive is added to the shared configuration infrastructure through the standard code review process, not added inline to the SKU's configuration file in a way that cannot be reused.

The observable metric for configuration health is the overlap between any two SKU configurations. A mature product line where SKUs are properly composed from shared primitives should show that 90 percent or more of any two adjacent-variant configurations are identical and only the defining differences between those variants are distinct. A product line where SKU configurations have diverged through independent modification should show high cross-SKU delta — a symptom of the same entropy that produces forked firmware branches when no configuration architecture is in place.

Quick Overview

Configuration-as-code for embedded firmware product variants encodes every dimension of product variation — hardware layout in device tree overlays, software feature selection in Kconfig defconfig files, operational parameters in structured runtime configuration manifests — as version-controlled, reviewable text artifacts that the build system uses to produce variant-specific firmware binaries from a single application codebase. The alternative — forking the firmware at each variant boundary — creates maintenance burdens that compound with every additional SKU and make security patch propagation, regression testing, and variant auditing exponentially more expensive. With clean separation between hardware description (device tree), software feature flags (Kconfig), and runtime parameters (JSON/YAML manifest), adding a new SKU requires writing configuration artifacts rather than modifying application code, and the CI/CD build matrix confirms that all variants build and test correctly on every commit.

Key Applications

Industrial IoT sensor gateway families where regional SKUs differ in communication protocol and regulatory parameters while sharing a common hardware platform, EV charging equipment product lines where consumer, commercial, and fleet variants differ in power level, payment interface, and management protocol, medical device families where regional regulatory clearance determines which features are enabled, smart meter product families where utility customers require customer-specific operational parameters and reporting formats, and any embedded product family where variant proliferation has made branch-per-variant firmware maintenance prohibitively expensive.

Benefits

Adding a new SKU requires writing configuration files rather than branching firmware, keeping the test and validation burden proportional to the actual delta between SKUs. Security patches and bug fixes propagate to all variants automatically because they exist in one codebase, eliminating the weeks-long cherry-pick process that branch-per-variant architecture requires. The CI/CD build matrix confirms that every commit builds and tests correctly for all 100+ SKUs, providing confidence that a change to shared code has not silently broken a subset of variants. The configuration manifest as a version-controlled artifact answers the question "what configuration is device X running?" precisely and auditably.

Challenges

Maintaining the hardware/software/parameter layer separation requires discipline: encoding hardware facts as Kconfig symbols or parameter values, rather than as device tree properties, is a common error that produces firmware that compiles correctly but behaves incorrectly on hardware where the fact differs. Variant proliferation in the configuration layer — SKUs that could be expressed as a composition of existing primitives but instead introduce new configuration symbols — creates configuration debt that is less visible than firmware branch debt but equally limiting over time. Runtime configuration schema versioning across a fleet with devices on different firmware versions requires careful backward compatibility design; a schema change that breaks parsing on older firmware versions can corrupt devices that receive the new configuration manifest through an OTA update.

Outlook

The software-defined product trend across automotive, industrial, and consumer electronics is increasing the number of firmware variants that product teams must maintain simultaneously, because software differentiation has replaced hardware differentiation as the primary SKU creation mechanism. This makes configuration-as-code architecture a prerequisite for product line scalability rather than an optional best practice. Zephyr's board variant system — introduced with the hardware model v2 in recent releases, supporting SoC variants and board qualifiers as first-class concepts in the board YAML descriptor — is formalizing what mature product teams have implemented informally for years, and is providing a reference architecture that teams can adopt rather than design from first principles.

Related Terms

configuration as code, Kconfig, device tree, DTS, overlay, defconfig, prj.conf, hardware abstraction layer, BSP, board support package, product variant, SKU, feature flag, conditional compilation, CMake, West, Zephyr, product family firmware, build matrix, CI/CD firmware, configuration manifest, JSON schema, CBOR, YAML, NVM partition, runtime configuration, OTA configuration update, configuration repository, firmware branching, variant proliferation, configuration debt, hardware model v2, board qualifier, defconfig composition, schema validation, firmware audit trail, build artifact, configuration record

 

Contact us

 

 

Our Case Studies

 

FAQ

What is the difference between device tree overlays and Kconfig defconfig files for firmware product variants?

 

Device tree overlays modify hardware descriptions: they add or change nodes describing physical hardware — sensor addresses, GPIO pin assignments, peripheral configurations — without duplicating the base hardware description. They encode the immutable physical facts about a specific PCB revision. Kconfig defconfig files specify which software features are compiled into the firmware binary: boolean, integer, and string configuration symbols that enable or disable subsystems, set buffer sizes, and configure software behavior. Zephyr explicitly documents this separation: device tree for hardware and its boot-time configuration, Kconfig for software feature selection. Encoding hardware facts as Kconfig symbols is a design error that produces firmware that compiles but behaves incorrectly on hardware where the hardware fact differs from the symbol value.
 

Why should the configuration repository be separate from the firmware application repository?

 

Placing SKU configuration in a separate repository from application code enforces that a configuration change for one SKU cannot accidentally modify firmware code that other SKUs run. It also enables different access and review workflows for configuration changes — which may be performed by hardware engineers, product managers, or account managers — versus firmware code changes, which require software engineering review. Configuration repositories can be versioned independently of the firmware, allowing the same firmware binary to be deployed with different configurations across a fleet, and allowing a configuration update to be deployed without rebuilding the firmware.
 

How does a build matrix scale to 100+ product SKUs without making CI prohibitively slow?

 

A build matrix that spawns one build job per SKU in parallel takes approximately the same wall-clock time as a single build if the CI infrastructure provides sufficient parallel workers. All SKUs share the same compiler cache for source files whose configuration is identical, which is the majority of the codebase in a well-structured product family. Testing scales by separating configuration-neutral tests — which run identically on all SKUs — from configuration-specific tests — which run only on SKUs where the tested feature or hardware is present. Build validation runs on every commit; full hardware regression runs on release candidates only.
 

What format should the runtime configuration manifest use on a constrained embedded device?

 

On devices with sufficient storage and processing, JSON is preferable because it is human-readable, widely supported for schema validation, and debuggable with standard tools. On constrained devices where JSON parsing overhead is unacceptable, CBOR, Concise Binary Object Representation, provides a binary-encoded equivalent with the same data model and is supported by multiple embedded parsing libraries. The critical property of either format is that the firmware validates the manifest against a schema at startup and rejects or defaults invalid values rather than proceeding with potentially dangerous configuration. The schema must be version-controlled alongside the firmware and the manifest, with a schema version field in the manifest enabling forward and backward compatibility handling.