I still remember the anxiety of pushing my first over-the-air firmware update to a device already deployed in the field, with no easy way to physically get to it if something went wrong. That fear is exactly why firmware update mechanisms in embedded systems are designed so carefully — a bad update to a desktop app is annoying; a bad update to a device you can’t physically reach can brick it permanently. In this article, I want to explain how embedded systems actually handle software updates and patches, from simple wired reprogramming up through robust, fail-safe OTA (Over-The-Air) update architectures.
Why Firmware Updates Are Harder Than Regular Software Updates
Unlike a desktop or mobile app update, where a failed install just leaves the previous version running, a failed embedded firmware update can leave a device completely unresponsive — a state commonly called “bricking.” This is because firmware isn’t just an application running on top of an operating system; on many embedded systems, it is the entire operating environment. If the update process is interrupted mid-write (power loss, communication failure), the device can be left with corrupted, unbootable code and no fallback.
flowchart TB
A[Update Challenge] --> B[No Fallback OS<br/>if Firmware Corrupted]
A --> C[Limited/No Physical Access<br/>Once Deployed]
A --> D[Power Loss Mid-Update<br/>Risk]
A --> E[Communication Failure<br/>Mid-Transfer]
B --> F[Requires Careful<br/>Update Architecture]
C --> F
D --> F
E --> F
Wired/Local Firmware Updates
The simplest update method is direct, wired reprogramming via a debug interface (JTAG/SWD) or a dedicated bootloader over UART/USB, typically used during development or for products serviced in person.
sequenceDiagram
participant PC as Development PC
participant TOOL as Flash Tool<br/>(ST-Link Utility/dfu-util)
participant MCU as Target MCU
PC->>TOOL: Load firmware.bin
TOOL->>MCU: Halt CPU via debug interface
TOOL->>MCU: Erase flash sectors
TOOL->>MCU: Write new firmware
TOOL->>MCU: Verify written data
TOOL->>MCU: Reset and run
# Example: Flashing firmware via ST-Link command line tool
st-flash write firmware.bin 0x08000000
# Example: Flashing via DFU (Device Firmware Update) over USB
dfu-util -a 0 -s 0x08000000:leave -D firmware.bin
This method is reliable because the flashing tool has full, direct control over the target and can verify each step, but it obviously doesn’t scale to thousands of deployed field devices.
Bootloader Architecture: The Foundation of Field-Updatable Systems
Any embedded system designed for field updates needs a bootloader — a small, separate piece of firmware that runs before the main application, responsible for deciding whether to boot the main application or enter update mode, and for safely writing new firmware into flash memory.
flowchart TB
A[Power-On / Reset] --> B[Bootloader Starts]
B --> C{Update Request<br/>Detected?}
C -->|Yes - button held/flag set| D[Enter Update Mode]
C -->|No| E[Verify Application Integrity<br/>CRC/Signature Check]
D --> F[Receive New Firmware<br/>via UART/USB/Network]
F --> G[Write to Flash]
G --> H[Verify Written Firmware]
H --> E
E -->|Valid| I[Jump to Application]
E -->|Invalid| J[Stay in Bootloader<br/>Await Recovery]
// Simplified bootloader logic (conceptual)
#define APP_START_ADDRESS 0x08008000
#define BOOTLOADER_FLAG_ADDR 0x0800FFF0
typedef void (*app_entry_t)(void);
void bootloader_main(void) {
if (should_enter_update_mode()) {
run_update_receiver();
}
if (!verify_application_integrity(APP_START_ADDRESS)) {
// Application is corrupt or missing - stay in bootloader,
// signal error state, wait for recovery firmware
indicate_recovery_needed();
run_update_receiver();
}
jump_to_application(APP_START_ADDRESS);
}
void jump_to_application(uint32_t address) {
uint32_t app_stack = *(volatile uint32_t*)address;
uint32_t app_entry = *(volatile uint32_t*)(address + 4);
__set_MSP(app_stack); // Set application's stack pointer
((app_entry_t)app_entry)(); // Jump to application reset vector
}
uint8_t verify_application_integrity(uint32_t address) {
uint32_t stored_crc = *(volatile uint32_t*)(address + APP_SIZE - 4);
uint32_t calculated_crc = calculate_crc32((uint8_t*)address, APP_SIZE - 4);
return (stored_crc == calculated_crc);
}
Dual-Bank (A/B) Firmware Update Strategy
The most robust approach used in modern embedded and IoT products is the dual-bank or A/B update scheme, where flash memory is partitioned into two application slots. The device always runs from one slot while updates are written to the other, inactive slot — meaning a failed or interrupted update never touches the currently running, known-good firmware.
flowchart TB
subgraph Flash Memory
BL[Bootloader]
A[App Slot A - Active/Running]
B[App Slot B - Inactive/Update Target]
end
NEW[New Firmware Received] --> B
B --> VERIFY{Verify Slot B<br/>CRC/Signature}
VERIFY -->|Valid| SWITCH[Mark Slot B as Active]
VERIFY -->|Invalid| KEEP[Keep Running Slot A]
SWITCH --> REBOOT[Reboot into Slot B]
REBOOT --> CONFIRM{Slot B Boots<br/>Successfully?}
CONFIRM -->|Yes| DONE[Update Complete]
CONFIRM -->|No - Rollback| REVERT[Bootloader Reverts to Slot A]
// Example: Bootloader logic for A/B slot selection with rollback
typedef struct {
uint8_t active_slot; // 0 = Slot A, 1 = Slot B
uint8_t boot_attempts;
uint8_t confirmed; // Set by application after successful boot
} BootConfig_t;
void bootloader_select_slot(BootConfig_t *config) {
uint32_t target_address = (config->active_slot == 0) ? SLOT_A_ADDR : SLOT_B_ADDR;
if (!config->confirmed && config->boot_attempts >= MAX_BOOT_ATTEMPTS) {
// New firmware failed to confirm itself as healthy after N attempts
// Roll back to the previous known-good slot
config->active_slot = !config->active_slot;
config->boot_attempts = 0;
target_address = (config->active_slot == 0) ? SLOT_A_ADDR : SLOT_B_ADDR;
save_boot_config(config);
}
config->boot_attempts++;
save_boot_config(config);
jump_to_application(target_address);
}
// Application-side: confirming successful boot after an update
// This must run only after verifying core functionality is working
void application_confirm_healthy_boot(void) {
if (self_test_passed() && communication_established()) {
BootConfig_t config;
load_boot_config(&config);
config.confirmed = 1;
config.boot_attempts = 0;
save_boot_config(&config);
}
}
This “confirm after boot” pattern is critical — the new firmware must actively prove it’s healthy (successfully initializing peripherals, establishing network connectivity, passing self-tests) before the bootloader commits to it permanently. If the new firmware crashes or fails those checks repeatedly, the bootloader automatically reverts to the previous, known-good slot.
Over-The-Air (OTA) Update Process
For connected embedded and IoT devices, updates are typically delivered wirelessly via Wi-Fi, cellular, LoRa, or Bluetooth, coordinated with a cloud-based update server.
sequenceDiagram
participant Cloud as Update Server
participant Device as IoT Device
participant BL as Bootloader
Device->>Cloud: Check for update (version query)
Cloud->>Device: New firmware available (version, size, signature)
Device->>Cloud: Request firmware binary
Cloud->>Device: Stream firmware in chunks
Device->>Device: Write chunks to inactive flash slot
Device->>Device: Verify complete image (hash/signature)
Device->>BL: Mark inactive slot for next boot
Device->>Device: Reboot
BL->>Device: Boot new firmware, run self-test
Device->>Cloud: Report update success/failure
// Simplified OTA chunk-writing logic (ESP32-style flow, conceptual)
Status_t ota_write_chunk(uint8_t *data, size_t len, uint32_t offset) {
if (offset + len > OTA_PARTITION_SIZE) {
return STATUS_ERROR_INVALID_PARAM;
}
if (flash_write(OTA_INACTIVE_PARTITION_ADDR + offset, data, len) != FLASH_OK) {
return STATUS_ERROR_HARDWARE_FAULT;
}
running_sha256_update(&ota_hash_ctx, data, len);
return STATUS_OK;
}
Status_t ota_finalize(uint8_t *expected_hash) {
uint8_t calculated_hash[32];
running_sha256_final(&ota_hash_ctx, calculated_hash);
if (memcmp(calculated_hash, expected_hash, 32) != 0) {
return STATUS_ERROR_CRC_MISMATCH; // Reject corrupted/tampered image
}
mark_ota_partition_valid();
return STATUS_OK;
}
Security in Firmware Updates
Firmware update mechanisms are a high-value target for attackers, since compromising the update path can let an attacker install malicious firmware permanently. Secure update design typically includes:
- Cryptographic signature verification — the device verifies the new firmware is signed by a trusted private key before accepting it, preventing installation of unauthorized firmware.
- Encrypted transport — using TLS for network-delivered updates to prevent interception or tampering in transit.
- Rollback/version protection — preventing an attacker from “downgrading” a device to an older, known-vulnerable firmware version.
- Secure boot chain — the bootloader itself is verified by an immutable root of trust (often stored in one-time-programmable memory or a hardware security module), so even the bootloader can’t be tampered with undetected.
flowchart TB
ROT[Hardware Root of Trust<br/>OTP/Secure Element] --> BL[Verify Bootloader Signature]
BL --> APP[Verify Application Signature]
APP --> RUN[Run Verified Application]
ROT -.->|Any Verification Fails| HALT[Halt/Recovery Mode]
BL -.-> HALT
APP -.-> HALT
// Signature verification before accepting new firmware (conceptual, using ECDSA)
Status_t verify_firmware_signature(uint8_t *firmware, size_t len, uint8_t *signature) {
uint8_t hash[32];
sha256(firmware, len, hash);
if (ecdsa_verify(hash, signature, PUBLIC_KEY_TRUSTED_ROOT) != VERIFY_OK) {
log_error("Firmware signature verification FAILED - rejecting update");
return STATUS_ERROR_INVALID_PARAM;
}
return STATUS_OK;
}
Delta/Incremental Updates for Bandwidth-Constrained Devices
For devices on low-bandwidth or metered connections (cellular IoT, LoRa), transmitting a full firmware image for every update can be impractical. Delta updates transmit only the binary difference between the old and new firmware, then reconstruct the full image on-device.
| Update Type | Bandwidth Usage | Device Complexity | Best For |
|---|---|---|---|
| Full image OTA | High (entire firmware size) | Low | Wi-Fi/Ethernet connected devices |
| Delta/incremental update | Low (only changed bytes) | Higher (patch reconstruction logic) | Cellular/LoRa/bandwidth-constrained devices |
| Wired/local update | N/A (direct connection) | Lowest | Development, in-person servicing |
Real-World Example: OTA Update Strategy for a Fleet of IoT Sensors
For a fleet of remotely deployed environmental sensors communicating over cellular, a realistic update strategy looks like:
- Devices check in periodically with an update server, reporting current firmware version.
- If an update is available, the device downloads it in the background during idle time, writing to an inactive flash partition — the device continues normal operation uninterrupted during download.
- Once fully downloaded, the device verifies the cryptographic signature and hash before accepting the image.
- The device schedules a reboot during a low-activity window (e.g., overnight) to minimize disruption.
- After rebooting into the new firmware, it runs self-tests (sensor readings valid, network connectivity established) and only then reports success back to the server — if self-tests fail, the bootloader automatically rolls back to the previous firmware on the next boot attempt.
- The server tracks rollout success rates across the fleet, halting a rollout automatically if failure rates spike, preventing a bad update from bricking the entire deployed fleet.
Performance, Reliability, and Security Considerations
- Performance: Writing to flash memory during an update consumes CPU time and can briefly affect real-time responsiveness — many designs throttle update-chunk writing or schedule it during known idle periods.
- Reliability: Never overwrite the only copy of working firmware directly — always use a dual-bank/A/B scheme or a dedicated, separately verified bootloader so a failed or interrupted update can’t leave the device unbootable.
- Security: Always verify cryptographic signatures before accepting new firmware; an update mechanism without signature verification is effectively an open door for anyone who can reach the update channel to install arbitrary code on the device.
Frequently Asked Questions
Q: What happens if power is lost in the middle of a firmware update? With a properly designed dual-bank/A/B update scheme, the currently running firmware is untouched during the update process, so a power loss simply results in an incomplete update in the inactive slot — the device reboots into the still-intact, previously running firmware.
Q: Why do some devices need a “confirm boot” step after an update? This lets the bootloader distinguish between “new firmware installed successfully and is working” versus “new firmware installed but is crashing or hanging” — without an explicit confirmation from a healthy-running application, the bootloader assumes failure and rolls back automatically.
Q: Are delta updates always better than full image updates? Not always — delta updates reduce bandwidth usage but add complexity and risk (patch reconstruction bugs), so they’re generally reserved for genuinely bandwidth-constrained connections like cellular or LoRa, while Wi-Fi-connected devices often just use simpler full-image updates.
Q: How do embedded systems prevent malicious firmware from being installed? Through cryptographic signature verification — the device only accepts firmware images signed by a trusted private key held by the manufacturer, checked against a public key embedded in the device’s secure boot chain.
Summary
Handling software updates in embedded systems requires far more care than typical application updates, since a failed update can permanently brick a device with no easy recovery path. Robust designs rely on a dedicated bootloader, dual-bank (A/B) flash partitioning so updates never overwrite the currently working firmware, cryptographic signature verification to prevent malicious firmware installation, and a “confirm after boot” mechanism that allows automatic rollback if new firmware proves unhealthy. For connected devices, OTA update pipelines extend this architecture across an entire fleet, with careful attention to bandwidth usage, staged rollouts, and monitoring to catch problems before they affect every deployed unit. Getting this right is what allows embedded products to be maintained and improved for years after deployment, without requiring a truck roll to fix every bug.