Embedded development is not a static field. Complexity grows, security constraints tighten, tools evolve — sometimes quickly. Early experiences on PIC microcontrollers in assembly feel distant: current projects mix security, connectivity, over-the-air updates and sometimes a full Linux system.

There are two main families of systems, with very different profiles: microcontrollers under an RTOS (Cortex-M, RISC-V 32-bit, AVR…) and application processors under embedded Linux (Cortex-A, x86, RISC-V 64-bit). Both have their rules, their pitfalls, their tools. And they are converging — slowly but surely.

Here is what we have taken away from field experience.

Part 1 — RTOS and microcontrollers

Working without a safety net

A Cortex-M0 microcontroller: 32 kB of Flash, maybe 8 kB of RAM, no MMU, no OS in the traditional sense. It runs at 48 MHz and consumes a few milliamps. This is not a limitation you endure — it is the constraint that defines the product. The battery of an IoT sensor that must last 10 years is sized to the bit.

A few everyday realities in this world:

  • No MMU: a wild pointer overwrites the stack silently. Detection tools (MPU on Cortex-M3+) exist but do not cover everything.
  • RAM counted in kB: every allocation is deliberate. Dynamic heap allocations are often banned in production — too much risk of fragmentation.
  • Determinism: tasks must trigger within a guaranteed delay — for example, motor control or a wireless transmission. This is the purpose of real-time.
  • Direct register access: you talk to hardware without OS abstraction. One wrong byte in a configuration register and the peripheral is in an undefined state.
  • Difficult debugging: printf over UART is still the first tool used in 80% of cases. GDB via SWD/JTAG works, but you quickly lose context the moment you touch interrupts.
Nothing catastrophic here — it is simply the reality of the field. An engineer who only knows Linux will be disoriented at first. But the constraints are predictable, and once internalised, they become a design language in their own right.

RTOS — an overview

An RTOS (Real-Time Operating System) provides preemptive scheduling, task management, synchronisation primitives (mutexes, semaphores, message queues), and sometimes a network stack and filesystem. Several players share the market.

FreeRTOS

Most deployed

Ultra-lightweight: 4 KB RAM minimum, support on virtually anything that has an MCU. The first name that comes up when you say "RTOS".

Strengths: ubiquity, abundant documentation, vendor BSP often delivered ready to go, MIT licence.

Limits: no unified HAL, no Devicetree, configuration via FreeRTOSConfig.h that grows and becomes hard to maintain. Acquired by Amazon in 2017 (AWS FreeRTOS / coreMQTT, etc.) — the cloud focus is real.

NuttX

POSIX on MCU

An ambitious bet: implementing full POSIX on a microcontroller. open(), read(), POSIX threads, BSD sockets — all present.

Strengths: application code portability, used by SpaceX on Dragon, clean and well-thought-out architecture.

Limits: complex integration, steep learning curve, much smaller community. Hard to find someone who knows it well.

RIOT

Low-power IoT

Low-power IoT focus, excellent radio protocol support: IEEE 802.15.4, LoRa, BLE, 6LoWPAN. Designed to run on constrained nodes.

Strengths: well-integrated IoT network stack, active European academic community, modular approach.

Limits: more limited industrial support, vendor BSP often absent — you do part of the work yourself.

Zephyr

Emerging standard

Linux Foundation, launched in 2016. This is where the industry is heading. Architecture mirroring Linux, Devicetree, exceptional commercial support.

Strengths: Nordic, ST, TI, Espressif, NXP, Renesas all maintain upstream BSPs. Rapidly growing ecosystem, serious documentation.

Limits: heavier than FreeRTOS (20–50 KB RAM depending on config), longer build times, non-trivial learning curve. More details below.

Comparison table

RTOS Min RAM Integration Ecosystem Real-time Industrial support Devicetree
FreeRTOS ~4 KB Simple Very rich Yes Excellent No
NuttX ~32 KB Complex Average Yes Limited No
RIOT ~1.5 KB Moderate Decent Yes Weak No
Zephyr ~20 KB Moderate Excellent Yes Excellent Yes

Why Zephyr is establishing itself as the reference RTOS

Four concrete reasons, not marketing arguments.

  • 1
    Devicetree — hardware is declarative, not hard-coded. You describe your circuit in a .dts file, the firmware adapts at compile time. Changing a board, adding a sensor — it becomes a text file edit, not a hunt through #define statements in ten files. And engineers coming from the Linux world are immediately at home.

A typical overlay for an I2C sensor:

DTS overlay — I2C sensor
/* boards/my_board.overlay */
&i2c0 {
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;  /* 400 kHz */

    bme280: bme280@76 {
        compatible = "bosch,bme280";
        reg = <0x76>;
        label = "BME280";
    };
};

/* In C code, retrieve the device cleanly: */
/* const struct device *dev =
       DEVICE_DT_GET(DT_NODELABEL(bme280)); */
  • 2
    CMake + Kconfig — the same system as the Linux kernel. Build options are configured in a human-readable prj.conf. No esoteric configuration GUI, no sprawling Makefile.
prj.conf — Zephyr configuration
# Enable Bluetooth LE
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Codium Sensor"

# Network stack + TLS
CONFIG_NET_SOCKETS=y
CONFIG_MBEDTLS=y

# Logging
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3  # INFO

# MPU for invalid memory access detection
CONFIG_ARM_MPU=y
  • 3
    Upstream hardware support. Nordic Semiconductor, STMicroelectronics, Texas Instruments, Espressif (ESP32), Microchip, NXP, Renesas — they all maintain their BSPs directly in the main Zephyr repository. This is not a symbolic contribution: it is code tested in CI on every commit, with actual hardware runners for some vendors. When you target an nRF9160 or an STM32U5, you have a solid starting point, not a fork that is three years old.
  • 4
    west — the multi-repository workspace manager. A YAML manifest describes all your dependencies (Zephyr itself, third-party modules, your own code). Guaranteed reproducibility between developers and in CI.
west.yml — minimal manifest
manifest:
  defaults:
    remote: upstream

  remotes:
    - name: upstream
      url-base: https://github.com/zephyrproject-rtos
    - name: codium
      url-base: https://github.com/codium-electronique

  projects:
    - name: zephyr
      remote: upstream
      revision: v3.7.0
      import: true

    - name: firmware-drivers
      remote: codium
      path: modules/codium-drivers
      revision: main

Part 2 — Embedded Linux

Scaling up — but constraints remain

We move to an application processor: Cortex-A53, i.MX8, RK3568, or x86 Atom. MMU, L1/L2 cache, multiple cores, hundreds of MB or even GB of RAM. Linux runs on it with its full ecosystem.

But "Linux" does not mean "easy". A few realities:

  • Boot < 10 s: in many industrial products, this is a hard requirement. The kernel takes time. You must optimise the initrd, disable unused subsystems, sometimes patch the bootloader.
  • Real-time: the standard kernel is not real-time. PREEMPT_RT (now integrated into mainline since 6.12) drastically reduces maximum latency — you can get below 100 µs on a well-configured Cortex-A53. Sufficient for medium-term motion control, not for microsecond-level command loops.
  • Certifications: IEC 62443 for industry, IEC 61508 for functional safety — mainstream Linux is not certifiable as-is. Variants like RTEMS or heavily reduced and validated Linux configurations exist for these cases.
  • Kernel rules: no stable internal ABI, no memory allocation in interrupt context (GFP_ATOMIC only), demanding code review for anything going upstream. If you write an in-house driver, you alone maintain it — that is a real cost.
An out-of-tree Linux driver is technical debt from the first commit. Every new kernel version can break your internal ABI. The decision between maintaining out-of-tree and contributing upstream is a real trade-off: upstream requires significant preparation and review effort, out-of-tree is faster short-term but accumulates.

Structure of a Linux driver

The Linux kernel organises drivers around the Device Model: each driver is bound to a bus (I2C, SPI, platform, PCI…), registers with a functional subsystem (IIO for sensors, input for input devices, DRM for display, V4L2 for video…), and exposes a standard interface to userspace.

Userspace ───────────────────────────────────────────────── /dev/iio:device0 /dev/input/event0 │ │ ─── Kernel subsystem ─────────────────────────────── IIO subsystem input subsystem │ │ ─── Driver logic ──────────────────────────────────── iio_driver input_handler (read/write regs) (IRQ → event) │ │ ─── Bus layer ───────────────────────────────────── i2c_driver i2c_driver │ │ ─── Hardware ────────────────────────────────────── I2C sensor 0x40 Touch controller 0x38

This layered architecture has a concrete advantage: the subsystem handles the generic part (event management, sysfs interface, buffers) and your driver does only what is strictly necessary — reading registers, handling interrupts, translating raw data. This decoupling simplifies maintenance and enables reuse across products.

In practice, a driver always starts with a probe() (called on device detection) and a remove() (cleanup). In between, you configure the hardware, register resources (IRQ, regulator, clock…) via kernel APIs, and connect to the functional subsystem.

Custom BSP on a Debian/Fedora base

When porting Linux to a custom board, you build a BSP (Board Support Package) — the combination of kernel + bootloader + root filesystem adapted to the hardware. The main steps:

  1. 1
    LTS kernel + SoC patches Start from an LTS kernel (6.6, 6.12…), integrate the SoC vendor patches (Rockchip, NXP, ST…) — usually a public git repository maintained by the vendor. Enable only the necessary subsystems. A production .config typically has 200–400 options, not 4000.
  2. 2
    Custom Device Tree Source (DTS) Start from the SoC reference DTS, extend it with the board's specific peripherals: GPIO, power regulators, display panels, sensors. The overlay is maintained in the kernel sources or project sources, alongside the rest of the BSP.
  3. 3
    U-Boot The bootloader. It initialises RAM, loads the kernel and DTS into memory, hands over control. Configuration via Kconfig, identical to the kernel. A few environment scripts for fallback (failed OTA update → automatic rollback).
  4. 4
    Root filesystem Start from stable Debian (debootstrap) or Fedora, strip aggressively, add business packages. Result: a 300 MB to 1.2 GB system depending on needs. Updates go through the native package manager — .deb on Debian, .rpm on Fedora — guaranteeing reproducibility, clean rollback and traceability without third-party dependencies.
  5. 5
    Proprietary packages GPU firmware (blobs), video codec, certified cryptography stack, business daemon. Integrated into the rootfs via signed Debian packages — reproducibility and traceability guaranteed.

Concrete example: MIPI DSI display driver + I2C touch controller

A concrete and recurring example in our projects: a custom board with a MIPI DSI display and an I2C touch controller. Two separate drivers, two kernel subsystems, one link in the Device Tree.

The MIPI DSI panel — DRM subsystem

A DRM panel implements a few callbacks: prepare() powers up the panel, enable() comes out of reset and sends the DCS initialisation commands. Conversely, disable() and unprepare() handle clean shutdown.

panel driver — DRM excerpt (simplified)
static int my_panel_prepare(struct drm_panel *panel)
{
    struct my_panel *ctx = to_my_panel(panel);

    /* Power-on sequence: regulator then GPIO reset */
    regulator_enable(ctx->vdd);
    usleep_range(5000, 6000);

    gpiod_set_value_cansleep(ctx->reset_gpio, 1);
    usleep_range(10000, 12000);
    gpiod_set_value_cansleep(ctx->reset_gpio, 0);
    msleep(120);  /* Delay specified by datasheet */

    return 0;
}

static int my_panel_enable(struct drm_panel *panel)
{
    struct my_panel *ctx = to_my_panel(panel);
    struct mipi_dsi_device *dsi = ctx->dsi;

    /* DCS init sequence — exit sleep, display on */
    mipi_dsi_dcs_exit_sleep_mode(dsi);
    msleep(120);
    mipi_dsi_dcs_set_display_on(dsi);

    return 0;
}

static const struct drm_panel_funcs my_panel_funcs = {
    .prepare   = my_panel_prepare,
    .enable    = my_panel_enable,
    .disable   = my_panel_disable,
    .unprepare = my_panel_unprepare,
    .get_modes = my_panel_get_modes,
};

The touch controller — input subsystem

The touch controller sends an interrupt on each detected contact. The driver reads coordinates via I2C and reports events to the input subsystem using the multi-touch API (type B).

touch driver — IRQ handler excerpt (simplified)
static irqreturn_t my_ts_irq_handler(int irq, void *data)
{
    struct my_ts *ts = data;
    struct touch_point points[MY_TS_MAX_FINGERS];
    int count, i;

    count = my_ts_read_touch_data(ts->client, points);
    if (count < 0)
        return IRQ_HANDLED;

    for (i = 0; i < MY_TS_MAX_FINGERS; i++) {
        input_mt_slot(ts->input, i);

        if (i < count) {
            input_mt_report_slot_state(ts->input,
                                       MT_TOOL_FINGER, true);
            input_report_abs(ts->input, ABS_MT_POSITION_X,
                             points[i].x);
            input_report_abs(ts->input, ABS_MT_POSITION_Y,
                             points[i].y);
        } else {
            input_mt_report_slot_state(ts->input,
                                       MT_TOOL_FINGER, false);
        }
    }

    input_mt_report_pointer_emulation(ts->input, true);
    input_sync(ts->input);

    return IRQ_HANDLED;
}

The Device Tree binding

The DTS links both drivers and describes the hardware: which GPIO for the panel reset, which IRQ for the touch, which regulators.

DTS — panel + touch controller
&mipi_dsi {
    status = "okay";

    panel@0 {
        compatible = "vendor,my-panel";
        reg = <0>;
        reset-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;
        vdd-supply = <&vcc_lcd>;

        /* MIPI DSI timing */
        dsi,lanes = <4>;
        dsi,format = <MIPI_DSI_FMT_RGB888>;
        dsi,flags = <MIPI_DSI_MODE_VIDEO |
                     MIPI_DSI_MODE_VIDEO_BURST>;

        /* Reference to touch controller (for DRM link) */
        touchscreen = <&my_touch>;
    };
};

&i2c4 {
    status = "okay";

    my_touch: touchscreen@38 {
        compatible = "vendor,my-touch";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <RK_PA7 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio3 RK_PB7 GPIO_ACTIVE_LOW>;
        vcc-supply = <&vcc_tp>;
        touchscreen-size-x = <1280>;
        touchscreen-size-y = <800>;
    };
};
The kernel does not know about the display. It does not know about the touch. It knows DTS nodes with compatible strings, and it looks for a registered driver that matches. This is the Device Model in action — complete decoupling between hardware description and driver code.

Conclusion

These two worlds remain distinct. A microcontroller specialist and an embedded Linux engineer do not share the same instincts, the same tools, the same problems. And that is normal — the fundamental constraints are different.

What is changing is the progressive convergence of tools. Zephyr deliberately copied Linux's architecture: Devicetree, Kconfig, CMake, west. An engineer writing kernel code in the morning can understand a Zephyr project in the afternoon without being completely lost. That is new, and it is useful.

The fundamental challenges remain. RAM still matters. Determinism cannot be improvised. A mishandled pointer is still a silent bug. An allocation in an IRQ handler is still incorrect code. These rules do not change because the tools become more comfortable.

But the field evolves quickly — Zephyr 3.x, PREEMPT_RT in mainline, Matter on microcontrollers, eBPF beginning to appear in embedded. That is precisely what makes it interesting.

A project with a firmware constraint?

From LTE-M PSM/eDRX configuration to a custom Linux driver, we handle the software side of your embedded systems. Zephyr, Linux kernel, BSP — we know the terrain.

Contact us Design Office