Edge Firmware Updates

Production-grade guide to edge firmware updates covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.

Edge firmware updates are the process of delivering and applying low-level software—device drivers, bootloaders, OS kernels, and runtime components—to edge devices in the field. They matter when a device must run a new version of its firmware without requiring physical access, and when reliability, consistency, and speed are critical: after a network outage, when a sensor fails to report data, or when a new security patch must roll out across 10,000 edge nodes simultaneously.

Managing Firmware Update Lifecycle

Deploying a New Firmware Image

To deploy a new firmware image, begin by building a .fit (Flattened Image Tree) file for a device with a U-Boot bootloader. The image must include a kernel, device tree, and root filesystem, and be signed with a public key.

# Build a FIT image using mkimage
mkimage -f fit.its -k kernel.ub -d rootfs.cpio -T flat_dt -C none -n "Edge Node Firmware" -e 0x40000000 -a 0x40000000 -o edge-firmware.fit

The fit.its file defines the image structure:

/dts-v1/;

/include/ "fit.its";

/ {
    description = "Edge Node Firmware Image";
    #address-cells = <1>;

    images {
        kernel {
            description = "Linux Kernel";
            data = /incbin/("zImage");
            type = "kernel";
            arch = "arm64";
            os = "linux";
            compression = "none";
            load = <0x40000000>;
            entry = <0x40000000>;
            hash = "sha256";
        };

        dtb {
            description = "Device Tree Blob";
            data = /incbin/("am57xx-evm.dtb");
            type = "flat_dt";
            arch = "arm64";
            compression = "none";
            load = <0x40000000>;
            hash = "sha256";
        };

        rootfs {
            description = "Root Filesystem";
            data = /incbin/("rootfs.cpio");
            type = "ramdisk";
            arch = "arm64";
            compression = "none";
            load = <0x41000000>;
            hash = "sha256";
        };
    };

    configurations {
        default = "conf1";
        conf1 {
            description = "Production Configuration";
            kernel = "kernel";
            fdt = "dtb";
            ramdisk = "rootfs";
            loadables = "kernel fdt ramdisk";
            os = "linux";
            arch = "arm64";
            entry = <0x40000000>;
        };
    };
};

After building the FIT image, sign it using openssl and u-boot-tools:

# Sign the FIT image with a private key
openssl dgst -sha256 -sign private.pem -out edge-firmware.fit.sig edge-firmware.fit

# Embed signature in FIT image
mkimage -l edge-firmware.fit
# Then use: fit_image_add_signature() to append .sig

Pushing Updates via Over-the-Air (OTA) Protocol

Use the OTA firmware update protocol defined by the Edge Firmware Update (EFU) standard. The protocol is based on a simple REST API over HTTPS, with a /updates endpoint.

POST /updates HTTP/1.1
Host: firmware.edge.example.com
Content-Type: application/json

{
  "target_device": "edge-node-01",
  "firmware_version": "2.1.4",
  "image_url": "https://firmware.edge.example.com/images/edge-firmware-2.1.4.fit",
  "signature_url": "https://firmware.edge.example.com/images/edge-firmware-2.1.4.fit.sig",
  "checksum": "sha256:abc123...",
  "reboot_after": true,
  "timeout": 300
}

The edge device polls the server every 15 minutes using a lightweight MQTT client. When a new update is available, it downloads the image in chunks and verifies it:

// In device firmware (C code)
int efu_download_and_verify(const char *url, const char *signature_url) {
    int fd = open("/tmp/firmware.fit", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) return -1;

    // Download image in 16KB chunks
    struct http_client client;
    http_client_init(&client, url);
    http_client_start(&client);

    uint8_t buffer[16384];
    size_t total = 0;
    while ((size_t len = http_client_read(&client, buffer, sizeof(buffer))) > 0) {
        write(fd, buffer, len);
        total += len;
    }

    // Verify SHA256 checksum
    uint8_t expected_hash[32];
    read_signature_file(signature_url, expected_hash);
    uint8_t actual_hash[32];
    sha256_file("/tmp/firmware.fit", actual_hash);

    if (memcmp(expected_hash, actual_hash, 32) != 0) {
        close(fd);
        return -1; // Verification failed
    }

    close(fd);
    return 0;
}

Applying the Update with Dual-Partition Strategy

Edge devices must support dual-boot partitions to allow seamless updates. The system uses two boot partitions: boot_a and boot_b, each containing a complete firmware image (including kernel, dtb, rootfs).

During update, the new firmware is written to the inactive partition. The device boots from the active partition, and the update process updates the inactive one.

# After downloading the new image to /mnt/updates/edge-firmware.fit
# Mount the inactive partition (e.g., /dev/mmcblk0p2) as /mnt/boot_b

sudo mount /dev/mmcblk0p2 /mnt/boot_b
sudo cp /tmp/firmware.fit /mnt/boot_b/firmware.fit
sudo cp /tmp/firmware.fit.sig /mnt/boot_b/firmware.fit.sig

# Update U-Boot environment variables
sudo fw_setenv boot_part 2
sudo fw_setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait"
sudo fw_setenv bootcmd "mmc dev 0; bootfit 0:1"

The U-Boot boot sequence now checks for a boot_fit flag and chooses the correct partition:

```u-boot => print bootcmd bootcmd=mmc dev 0; bootfit 0:1 => print bootargs bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait => print boot_part boot_part=2 => print boot_fit boot_fit=1


When the update is applied, the device can be rebooted with `boot_fit=1`, and U-Boot will load the new image from the inactive partition.

### Rolling Back After Failure

A common failure mode: the new firmware boots but fails to initialize a critical sensor, causing the device to stall. The device must roll back to the previous firmware.

Use the **rollback mechanism** triggered by a `rollback` flag in U-Boot:

```u-boot
=> setenv rollback 1
=> saveenv
=> reset

On boot, U-Boot checks the rollback variable. If set, it:

  1. Copies the current boot_a to boot_b
  2. Sets boot_part=1
  3. Clears rollback
  4. Reboots

The rollback logic is implemented in a U-Boot script (rollback.scr):

```u-boot

rollback.scr

setenv boot_part 1 setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p1 rw rootwait" setenv bootcmd "mmc dev 0; bootfit 0:1" saveenv reset


To trigger a rollback from within the Linux kernel:

```c
void efu_trigger_rollback(const char *reason) {
    FILE *f = fopen("/etc/efu/rollback.reason", "w");
    fprintf(f, "Update failed: %s\n", reason);
    fclose(f);

    // Write to U-Boot environment
    system("fw_setenv rollback 1");
    system("fw_saveenv");
    system("reboot");
}

Managing Update Metadata and State

Each update is tracked via a metadata store. The device maintains a JSON file at /etc/efu/updates.json:

{
  "current": {
    "version": "2.1.3",
    "partition": "boot_a",
    "installed_at": "2024-03-15T12:00:00Z",
    "checksum": "sha256:ef4567..."
  },
  "pending": {
    "version": "2.1.4",
    "partition": "boot_b",
    "downloaded_at": "2024-03-15T12:15:30Z",
    "signature_verified": true,
    "applied": true
  },
  "history": [
    {
      "version": "2.1.2",
      "status": "success",
      "timestamp": "2024-03-01T08:00:00Z",
      "rollback_reason": "Sensor calibration failed"
    },
    {
      "version": "2.1.1",
      "status": "failed",
      "timestamp": "2024-02-28T14:22:10Z",
      "rollback_reason": "Network timeout during boot"
    }
  ]
}

The metadata is updated by a efu-update daemon:

# efu-update.py
import json
import os
import subprocess
import time

UPDATE_PATH = "/etc/efu/updates.json"

def read_metadata():
    with open(UPDATE_PATH, 'r') as f:
        return json.load(f)

def write_metadata(metadata):
    with open(UPDATE_PATH, 'w') as f:
        json.dump(metadata, f, indent=2)

def apply_update(version, partition, success=True):
    meta = read_metadata()
    meta["pending"]["version"] = version
    meta["pending"]["partition"] = partition
    meta["pending"]["applied"] = True

    if success:
        meta["current"] = meta["pending"]
        meta["history"].insert(0, {
            "version": version,
            "status": "success",
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ")
        })
    else:
        meta["history"].insert(0, {
            "version": version,
            "status": "failed",
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ")
        })

    write_metadata(meta)

Handling Update Failures

The most silent failure: a device boots with the new firmware but never reports back to the update server.

Key error codes:

The device logs these errors and waits 15 minutes before retrying. After three failed attempts, it enters update recovery mode:

# In recovery mode
cd /tmp/recovery
wget http://firmware.edge.example.com/recovery.tar.gz
tar -xzf recovery.tar.gz
./recovery.sh

The recovery script:

#!/bin/sh
# recovery.sh
set -e

echo "Starting edge firmware recovery..."

# Check for network
if ! ping -c 3 firmware.edge.example.com; then
    echo "Network unreachable. Trying to configure static IP..."
    ip addr add 192.168.1.100/24 dev eth0
    ip route add default via 192.168.1.1
fi

# Download latest firmware from server
wget -O /tmp/latest.fit http://firmware.edge.example.com/images/latest.fit
wget -O /tmp/latest.fit.sig http://firmware.edge.example.com/images/latest.fit.sig

# Verify and apply
if ! sha256sum -c /tmp/latest.fit.sha256; then
    echo "Checksum mismatch. Rebooting to recovery environment."
    fw_setenv boot_part 3
    fw_setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p3 rw rootwait"
    fw_saveenv
    reboot
fi

# Copy to boot_a
mount /dev/mmcblk0p1 /mnt/boot_a
cp /tmp/latest.fit /mnt/boot_a/firmware.fit
cp /tmp/latest.fit.sig /mnt/boot_a/firmware.fit.sig
umount /mnt/boot_a

# Finalize
fw_setenv boot_part 1
fw_setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p1 rw rootwait"
fw_saveenv
reboot

Common Pitfalls and Confusing Options

Verifying Update Success

To verify that an update succeeded, check the device logs:

journalctl -u efu-update.service --since "2024-03-15 12:00:00"

Expected output:

Mar 15 12:15:30 edge-node-01 efu-update[123]: Downloaded firmware.fit from https://firmware.edge.example.com/images/edge-firmware-2.1.4.fit
Mar 15 12:16:15 edge-node-01 efu-update[123]: Verified SHA256 checksum: abc123...
Mar 15 12:16:20 edge-node-01 efu-update[123]: Applied firmware to boot_b
Mar 15 12:16:25 edge-node-01 efu-update[123]: Update applied successfully
Mar 15 12:16:30 edge-node-01 efu-update[123]: Rebooting in 5 seconds...

The update service also emits a systemd event:

systemctl --state=active --property=SubState efu-update.service

Returns:

SubState=running

When the device reboots, the efu-update.service logs status=applied to the server via:

PATCH /updates/123 HTTP/1.1
Host: firmware.edge.example.com
Content-Type: application/json

{
  "status": "applied",
  "timestamp": "2024-03-15T12:16:30Z"
}

If the server receives this, the update is marked as complete.

This page was rewritten on 10 October 2026. It replaced a templated version whose text was largely shared with other pages in this section and was not specific to its own title. The new text was drafted with a locally run language model, checked by a separate reviewer model for specificity and for invented figures, and measured against its sibling pages for duplication before publication. If anything here is wrong, tell us at [email protected] and we will correct it.