Project 6: Simple Bootloader

Build a bootloader that initializes hardware, provides a serial interface for firmware updates, validates new firmware (CRC check), and chains to the main application—like a mini U-Boot for your microcontroller.


Quick Reference

Attribute Value
Language ARM Assembly + C
Difficulty Expert
Time 2-4 weeks
Coolness ★★★★★ Pure Magic (Super Cool)
Portfolio Value Service & Support Model
Prerequisites Projects 3-4 (bare-metal + UART), Flash concepts
Key Topics Boot process, Flash programming, XMODEM, firmware updates

Learning Objectives

By completing this project, you will:

  1. Understand the ARM boot sequence: Know exactly what happens from power-on to your first instruction
  2. Master Flash self-programming: Write code that modifies its own Flash memory
  3. Implement serial protocols: Build XMODEM or custom protocols for reliable data transfer
  4. Design update-safe systems: Create bootloaders that can recover from failed updates
  5. Handle memory partitioning: Divide Flash between bootloader, application, and configuration
  6. Implement integrity checking: Use CRC/checksums to validate firmware before execution

The Core Question You’re Answering

“How does a device update its own firmware safely, and what happens between power-on and your main() function?”

This question forces you to understand the earliest stages of program execution—before the C runtime, before main(), even before the stack exists. You’ll learn why bootloaders exist, how they enable field updates, and why production embedded systems always have one.


Concepts You Must Understand First

Concept Why It Matters Where to Learn
ARM reset sequence The CPU starts executing from a specific address ARM TRM, Project 3
Vector table structure First bytes of Flash contain critical pointers ARM Architecture Reference
Flash memory architecture Different from RAM: erase then write, sectors/pages STM32 Flash Programming Manual
Linker scripts How to place code at specific addresses Bare Metal C, Ch. 2
Serial communication UART basics for debug and data transfer Project 4
CRC algorithms Validate data integrity Hacker’s Delight, Ch. 14
Memory-mapped registers How to control Flash controller hardware MCU Reference Manual

Self-Assessment Questions

  1. Boot process: After power-on, what are the first two values the ARM CPU reads from memory?
  2. Flash characteristics: Why can’t you just write to Flash like RAM? What is an erase block?
  3. Vector table offset: How does the application’s vector table differ from the bootloader’s?
  4. Stack initialization: Who sets up the initial stack pointer—hardware or software?
  5. Checksum vs CRC: What’s the difference? Why prefer CRC for firmware validation?

Theoretical Foundation

The ARM Boot Sequence

When power is applied to an ARM Cortex-M processor, a precise sequence occurs:

Power On
   │
   ▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. CPU reads Initial Stack Pointer from address 0x00000000      │
│    (or wherever BOOT pins map Flash to)                         │
└─────────────────────────────────────────────────────────────────┘
   │
   ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. CPU reads Reset Vector from address 0x00000004               │
│    This is the address of the Reset Handler                     │
└─────────────────────────────────────────────────────────────────┘
   │
   ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. CPU sets SP to Initial Stack Pointer value                   │
│    Sets PC to Reset Vector value                                │
│    Execution begins!                                            │
└─────────────────────────────────────────────────────────────────┘
   │
   ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Reset Handler runs:                                          │
│    - Initialize .data (copy from Flash to RAM)                  │
│    - Zero .bss section                                          │
│    - Call SystemInit() (clock configuration)                    │
│    - Call __libc_init_array() (if using C++)                    │
│    - Call main()                                                │
└─────────────────────────────────────────────────────────────────┘

Why Bootloaders Exist

Without a bootloader:

  • Firmware updates require physical programmer (JTAG/SWD)
  • Failed updates brick the device
  • No way to validate firmware before running

With a bootloader:

  • Updates over serial, USB, network, or wireless
  • Recovery from bad updates (bootloader is always safe)
  • Integrity verification before execution
  • Multiple firmware slots for A/B updates
Device without bootloader:         Device with bootloader:
┌────────────────────────┐        ┌────────────────────────┐
│    Application         │        │    Bootloader (safe)   │
│    (entire Flash)      │        ├────────────────────────┤
│                        │        │    Application         │
│                        │        │    (updateable)        │
└────────────────────────┘        └────────────────────────┘
Update = connect programmer        Update = serial/USB/network
Failure = bricked device          Failure = bootloader recovers

Flash Memory Architecture

Flash memory differs fundamentally from RAM:

RAM:                              Flash:
- Write any byte anytime          - Must erase before write
- No wear concerns                - Limited erase cycles (10K-100K)
- No erase needed                 - Erase is slow (ms)
- Volatile (lost on power off)    - Non-volatile (persists)

Flash write sequence:
1. Unlock Flash (write magic keys)
2. Erase sector (sets all bits to 1)
3. Program word/half-word (changes 1s to 0s)
4. Lock Flash (for safety)

Common Flash organization (STM32F4):
┌──────────────────────────────────────────────────────────────┐
│ Sector 0: 16KB  │ Sector 1: 16KB  │ Sector 2: 16KB  │ ...   │
├──────────────────────────────────────────────────────────────┤
│ Sector 3: 16KB  │ Sector 4: 64KB  │ Sector 5: 128KB │ ...   │
└──────────────────────────────────────────────────────────────┘
Note: Different sizes! Bootloader usually fits in smaller sectors.

Memory Layout for Bootloader Systems

Flash Memory (512KB example):
┌──────────────────────────────────────────────────────────────┐
│ 0x08000000: Bootloader Vector Table                          │
│             - Initial Stack Pointer                          │
│             - Reset Handler → Bootloader code                │
│             - Other exception handlers                       │
├──────────────────────────────────────────────────────────────┤
│ 0x08000100: Bootloader Code                                  │
│             - Hardware init                                  │
│             - UART driver                                    │
│             - Flash programming                              │
│             - XMODEM protocol                                │
│             - Jump to application                            │
├──────────────────────────────────────────────────────────────┤
│ 0x08008000: Application Vector Table (at 32KB boundary)      │
│             - Initial Stack Pointer (for app)                │
│             - Reset Handler → Application code               │
│             - Other exception handlers                       │
├──────────────────────────────────────────────────────────────┤
│ 0x08008100: Application Code                                 │
│             - Your actual application                        │
│             - Can be updated by bootloader                   │
├──────────────────────────────────────────────────────────────┤
│ 0x0807F000: Configuration/Metadata (4KB)                     │
│             - App version                                    │
│             - CRC checksum                                   │
│             - Boot flags                                     │
└──────────────────────────────────────────────────────────────┘

The XMODEM Protocol

XMODEM is a simple, robust protocol for transferring files over serial:

XMODEM Packet Structure:
┌────────┬─────────┬──────────────┬─────────┬────────┐
│  SOH   │ Block # │ ~Block #     │ Data    │ Check  │
│  0x01  │  1-255  │ 255 - Block# │128 bytes│ CRC16  │
└────────┴─────────┴──────────────┴─────────┴────────┘

Transfer sequence:
Receiver                              Sender
   │                                    │
   │────── 'C' (CRC mode request) ─────▶│
   │                                    │
   │◀───── SOH, 01, FE, Data, CRC ─────│  Packet 1
   │────── ACK ────────────────────────▶│
   │                                    │
   │◀───── SOH, 02, FD, Data, CRC ─────│  Packet 2
   │────── ACK ────────────────────────▶│
   │        ...                         │
   │◀───── EOT (End of Transmission) ──│
   │────── ACK ────────────────────────▶│
   │                                    │
   ▼                                    ▼

Error handling:
- Receiver sends NAK on checksum failure
- Sender retransmits packet
- After 10 failures, abort transfer

Jumping to Application

The most critical part of a bootloader—transferring control to the application:

void jump_to_application(uint32_t app_address) {
    // 1. Disable interrupts
    __disable_irq();

    // 2. Read application's stack pointer (first word)
    uint32_t app_stack = *(volatile uint32_t *)app_address;

    // 3. Read application's reset handler (second word)
    uint32_t app_reset = *(volatile uint32_t *)(app_address + 4);

    // 4. Relocate vector table to application's table
    SCB->VTOR = app_address;

    // 5. Set main stack pointer to application's value
    __set_MSP(app_stack);

    // 6. Create function pointer and call (never returns)
    void (*app_entry)(void) = (void (*)(void))app_reset;
    app_entry();

    // Should never reach here
    while (1);
}

CRC for Firmware Validation

CRC-32 is the standard for firmware validation:

Why CRC over simple checksum?
┌─────────────────────────────────────────────────────────────┐
│ Simple sum:     Detects ~50% of random errors               │
│ CRC-32:         Detects 99.99999998% of random errors       │
│                 Guaranteed detection of:                    │
│                 - All single-bit errors                     │
│                 - All double-bit errors                     │
│                 - All odd numbers of errors                 │
│                 - All burst errors up to 32 bits            │
└─────────────────────────────────────────────────────────────┘

CRC-32 calculation (simplified):
uint32_t crc32(uint8_t *data, size_t len) {
    uint32_t crc = 0xFFFFFFFF;
    while (len--) {
        crc ^= *data++;
        for (int i = 0; i < 8; i++) {
            if (crc & 1)
                crc = (crc >> 1) ^ 0xEDB88320;
            else
                crc >>= 1;
        }
    }
    return ~crc;
}

Why This Matters

Understanding bootloaders is essential for:

  • Production embedded systems: Every commercial product has one
  • IoT devices: Over-the-air (OTA) updates are standard
  • Automotive/aerospace: Safety-critical update mechanisms
  • Consumer electronics: Firmware updates fix bugs, add features
  • Security: Secure boot chains start here

Project Specification

What You Will Build

A complete bootloader with:

  1. Hardware initialization: Clocks, GPIO, UART
  2. Interactive menu: Serial interface for user commands
  3. XMODEM receiver: Receive firmware over serial
  4. Flash programmer: Write received firmware to Flash
  5. CRC validation: Verify firmware integrity before and after flashing
  6. Application launcher: Jump to main application
  7. Recovery mode: Stay in bootloader if application is invalid

Functional Requirements

  1. Boot Decision Logic:
    • Check if user button held → enter bootloader menu
    • Check if application CRC valid → jump to application
    • Otherwise → stay in bootloader (recovery mode)
  2. Menu System:
    • [1] Boot application
    • [2] Upload new firmware (XMODEM)
    • [3] Verify current firmware
    • [4] Dump flash info
    • [5] Erase application area
    • [6] System reset
  3. Firmware Upload:
    • XMODEM protocol with CRC-16
    • Progress indication
    • Automatic sector erase
    • CRC-32 verification after write
  4. Safety Features:
    • Never erase bootloader itself
    • Validate CRC before marking firmware valid
    • Store metadata (version, CRC, size) separately

Non-Functional Requirements

  • Size: Bootloader fits in first 32KB (or less)
  • Robustness: Survives power loss during update
  • Speed: 115200 baud XMODEM transfer
  • Reliability: CRC verification prevents running bad firmware

Real World Outcome

$ screen /dev/ttyUSB0 115200

========================================
    ARM Custom Bootloader v1.0
========================================
Flash: 512KB (Bootloader: 32KB, App: 480KB)
Current app: CRC 0x1A2B3C4D, Valid
App size: 48,256 bytes
Build date: 2024-01-15

Commands:
  [1] Boot application
  [2] Upload new firmware (XMODEM)
  [3] Verify current firmware
  [4] Dump flash info
  [5] Erase application area
  [6] System reset

Press 1-6, or wait 5s for auto-boot...

> 2
Ready to receive firmware via XMODEM...
Send file now (use: sx -k firmware.bin)

CCCC
Receiving: [====================] 48KB
CRC check: PASS (0x5E6F7A8B)
Erasing sectors 2-5...
Flashing: [====================] Done
Verifying: PASS

Firmware updated successfully!
Rebooting into new application...

> 1
Validating application...
Stack pointer: 0x20010000 (valid)
Reset vector: 0x08008101 (valid)
App CRC: 0x5E6F7A8B (valid)

Jumping to application at 0x08008000...

=== Application Starting ===
Hello from the new firmware!
Version: 2.0.1

Solution Architecture

High-Level Design

┌─────────────────────────────────────────────────────────────────────┐
│                          Bootloader                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐           │
│  │    Boot      │   │    Menu      │   │    Flash     │           │
│  │    Logic     │──▶│   Handler    │──▶│  Programmer  │           │
│  └──────────────┘   └──────────────┘   └──────────────┘           │
│         │                  │                  │                    │
│         │                  ▼                  │                    │
│         │           ┌──────────────┐          │                    │
│         │           │   XMODEM     │──────────┘                    │
│         │           │   Receiver   │                               │
│         │           └──────────────┘                               │
│         │                                                          │
│         ▼                                                          │
│  ┌──────────────┐   ┌──────────────┐                              │
│  │  Application │   │     CRC      │                              │
│  │   Launcher   │──▶│   Validator  │                              │
│  └──────────────┘   └──────────────┘                              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│                        Application                                   │
│  (Lives at 0x08008000, has own vector table and code)              │
└─────────────────────────────────────────────────────────────────────┘

Key Components

Component Responsibility Key Functions
Boot Logic Decide bootloader vs app check_boot_button, validate_app
Menu Handler User interface show_menu, handle_command
XMODEM Receiver Serial file transfer xmodem_receive, crc16
Flash Programmer Write to Flash flash_unlock, flash_erase, flash_write
CRC Validator Integrity checking crc32, validate_firmware
App Launcher Jump to application jump_to_app, set_vtor

Data Structures

// Firmware metadata stored at fixed location
typedef struct {
    uint32_t magic;           // 0xDEADC0DE if valid
    uint32_t size;            // Firmware size in bytes
    uint32_t crc;             // CRC-32 of firmware
    uint32_t version;         // Version number
    uint32_t build_date;      // Unix timestamp
    uint8_t  reserved[12];    // Future use
} firmware_metadata_t;

#define METADATA_MAGIC    0xDEADC0DE
#define METADATA_ADDR     0x0807F000  // Last 4KB

// XMODEM packet
typedef struct {
    uint8_t header;           // SOH (0x01) or STX (0x02)
    uint8_t block_num;
    uint8_t block_num_inv;
    uint8_t data[128];        // Or 1024 for XMODEM-1K
    uint16_t crc;             // CRC-16-CCITT
} xmodem_packet_t;

// Flash sector info
typedef struct {
    uint32_t start_addr;
    uint32_t size;
    uint8_t  sector_num;
} flash_sector_t;

Memory Map

Linker script memory regions:

MEMORY
{
    BOOTLOADER (rx)  : ORIGIN = 0x08000000, LENGTH = 32K
    APPLICATION (rx) : ORIGIN = 0x08008000, LENGTH = 476K
    METADATA (r)     : ORIGIN = 0x0807F000, LENGTH = 4K
    RAM (rwx)        : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } >BOOTLOADER

    .text :
    {
        *(.text*)
        *(.rodata*)
    } >BOOTLOADER

    /* ... data and bss in RAM ... */
}

Boot Flow Algorithm

boot_main():
    1. Initialize hardware (clocks, GPIO, UART)

    2. Check boot mode:
       if (button_pressed() || serial_break_detected()):
           goto bootloader_menu

    3. Validate application:
       metadata = read_metadata()
       if (metadata.magic != MAGIC):
           print("No valid application")
           goto bootloader_menu

       calculated_crc = crc32(APP_START, metadata.size)
       if (calculated_crc != metadata.crc):
           print("Application CRC mismatch")
           goto bootloader_menu

    4. Quick boot (no button, valid app):
       if (auto_boot_enabled):
           countdown(5)  // 5 second delay for interrupt
           jump_to_application()

bootloader_menu:
    5. Show menu and handle commands:
       while (1):
           show_menu()
           cmd = get_command()
           switch (cmd):
               case BOOT:     jump_to_application(); break;
               case UPLOAD:   xmodem_receive(); flash_program(); break;
               case VERIFY:   verify_firmware(); break;
               case DUMP:     dump_flash_info(); break;
               case ERASE:    erase_application(); break;
               case RESET:    NVIC_SystemReset(); break;

Implementation Guide

Development Environment Setup

# Required tools
sudo apt-get install gcc-arm-none-eabi gdb-multiarch lrzsz

# lrzsz provides sx/rx for XMODEM transfers
sx --version  # Send via XMODEM
rx --version  # Receive via XMODEM

# Create project structure
mkdir -p bootloader/{src,include,app}
cd bootloader

Project Structure

bootloader/
├── src/
│   ├── startup.s          # Vector table, reset handler
│   ├── main.c             # Boot logic, menu
│   ├── flash.c            # Flash programming
│   ├── xmodem.c           # XMODEM protocol
│   ├── crc.c              # CRC-32 and CRC-16
│   ├── uart.c             # Serial I/O (from Project 4)
│   └── jump.s             # Assembly for app jump
├── include/
│   ├── flash.h
│   ├── xmodem.h
│   ├── crc.h
│   └── config.h           # Memory addresses, settings
├── linker_bootloader.ld   # Bootloader linker script
├── linker_application.ld  # Application linker script
├── app/
│   └── main.c             # Simple test application
└── Makefile

Implementation Phases

Phase 1: Skeleton and Menu (Days 1-3)

Goals:

  • Set up dual linker scripts (bootloader + app)
  • Implement basic UART menu
  • Test build and flash for both

Tasks:

  1. Create bootloader linker script (32KB at 0x08000000)
  2. Create application linker script (starts at 0x08008000)
  3. Port UART code from Project 4
  4. Implement text menu system
  5. Build and flash bootloader, verify it runs

Checkpoint: Bootloader shows menu, responds to commands (stubs only).

Phase 2: Flash Programming (Days 4-7)

Goals:

  • Implement Flash unlock/lock
  • Implement sector erase
  • Implement word programming
  • Test with manual data

Tasks:

  1. Write flash_unlock() using key sequence
  2. Write flash_erase_sector()
  3. Write flash_write_word()
  4. Add error checking (busy flag, error flags)
  5. Test by manually writing patterns

Key code (STM32F4):

#define FLASH_KEY1  0x45670123
#define FLASH_KEY2  0xCDEF89AB

void flash_unlock(void) {
    if (FLASH->CR & FLASH_CR_LOCK) {
        FLASH->KEYR = FLASH_KEY1;
        FLASH->KEYR = FLASH_KEY2;
    }
}

void flash_lock(void) {
    FLASH->CR |= FLASH_CR_LOCK;
}

bool flash_erase_sector(uint8_t sector) {
    // Wait for not busy
    while (FLASH->SR & FLASH_SR_BSY);

    // Set sector and erase bit
    FLASH->CR &= ~(FLASH_CR_PSIZE | FLASH_CR_SNB);
    FLASH->CR |= FLASH_CR_PSIZE_1;  // 32-bit parallelism
    FLASH->CR |= (sector << 3);      // Sector number
    FLASH->CR |= FLASH_CR_SER;       // Sector erase

    // Start erase
    FLASH->CR |= FLASH_CR_STRT;

    // Wait for completion
    while (FLASH->SR & FLASH_SR_BSY);

    // Check for errors
    if (FLASH->SR & (FLASH_SR_PGSERR | FLASH_SR_PGPERR | FLASH_SR_PGAERR | FLASH_SR_WRPERR)) {
        return false;
    }

    return true;
}

bool flash_write_word(uint32_t address, uint32_t data) {
    while (FLASH->SR & FLASH_SR_BSY);

    FLASH->CR &= ~(FLASH_CR_PSIZE);
    FLASH->CR |= FLASH_CR_PSIZE_1;  // 32-bit
    FLASH->CR |= FLASH_CR_PG;        // Program mode

    *(volatile uint32_t *)address = data;

    while (FLASH->SR & FLASH_SR_BSY);

    FLASH->CR &= ~FLASH_CR_PG;

    return (*(volatile uint32_t *)address == data);
}

Checkpoint: Can erase and write Flash sectors programmatically.

Phase 3: XMODEM Protocol (Days 8-12)

Goals:

  • Implement XMODEM receiver
  • Handle CRC-16 calculation
  • Robust error recovery

Tasks:

  1. Implement CRC-16-CCITT
  2. Implement packet receiving with timeout
  3. Implement NAK/ACK responses
  4. Handle retransmissions
  5. Test with sx command from Linux host

Key code:

uint16_t crc16_ccitt(uint8_t *data, size_t len) {
    uint16_t crc = 0;
    while (len--) {
        crc ^= (uint16_t)*data++ << 8;
        for (int i = 0; i < 8; i++) {
            if (crc & 0x8000)
                crc = (crc << 1) ^ 0x1021;
            else
                crc <<= 1;
        }
    }
    return crc;
}

int xmodem_receive(uint8_t *buffer, size_t max_size) {
    size_t received = 0;
    uint8_t expected_block = 1;
    int retries = 0;

    // Send 'C' to request CRC mode
    for (int i = 0; i < 10; i++) {
        uart_putc('C');
        if (uart_getc_timeout(1000) == SOH) {
            break;
        }
    }

    while (1) {
        xmodem_packet_t pkt;

        // Read packet
        pkt.header = SOH;  // Already received
        pkt.block_num = uart_getc_timeout(1000);
        pkt.block_num_inv = uart_getc_timeout(1000);

        for (int i = 0; i < 128; i++) {
            pkt.data[i] = uart_getc_timeout(1000);
        }

        pkt.crc = (uart_getc_timeout(1000) << 8) | uart_getc_timeout(1000);

        // Validate block number
        if (pkt.block_num != expected_block ||
            pkt.block_num_inv != (255 - expected_block)) {
            uart_putc(NAK);
            retries++;
            continue;
        }

        // Validate CRC
        uint16_t calc_crc = crc16_ccitt(pkt.data, 128);
        if (calc_crc != pkt.crc) {
            uart_putc(NAK);
            retries++;
            continue;
        }

        // Good packet - copy data
        memcpy(buffer + received, pkt.data, 128);
        received += 128;
        expected_block++;
        retries = 0;

        uart_putc(ACK);

        // Check for EOT
        int next = uart_getc_timeout(1000);
        if (next == EOT) {
            uart_putc(ACK);
            break;
        }
        // Otherwise, it's the next SOH
    }

    return received;
}

Checkpoint: Can receive files via XMODEM and store in RAM buffer.

Phase 4: Integration (Days 13-17)

Goals:

  • Integrate XMODEM with Flash programming
  • Implement CRC-32 validation
  • Write and verify firmware metadata

Tasks:

  1. Connect XMODEM receive to Flash write
  2. Implement CRC-32 for firmware validation
  3. Write firmware metadata after successful flash
  4. Implement verify_firmware() command
  5. Test full upload cycle

Checkpoint: Full firmware upload, flash, and verification works.

Phase 5: Application Jump (Days 18-21)

Goals:

  • Implement safe jump to application
  • Handle vector table relocation
  • Build test application

Tasks:

  1. Implement jump_to_application() in assembly
  2. Create simple test application with own vector table
  3. Test boot sequence: bootloader → application
  4. Implement “return to bootloader” from application

Key code:

// In C
void jump_to_application(void) {
    uint32_t app_address = APPLICATION_START;

    // Validate stack pointer (should be in RAM)
    uint32_t sp = *(volatile uint32_t *)app_address;
    if (sp < RAM_START || sp > RAM_END) {
        uart_puts("Invalid stack pointer!\n");
        return;
    }

    // Validate reset vector (should be in Flash)
    uint32_t reset = *(volatile uint32_t *)(app_address + 4);
    if (reset < app_address || reset > FLASH_END) {
        uart_puts("Invalid reset vector!\n");
        return;
    }

    uart_puts("Jumping to application...\n");
    delay_ms(10);  // Let UART finish

    // Disable all interrupts
    __disable_irq();

    // Clear pending interrupts
    for (int i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPR[i] = 0xFFFFFFFF;
    }

    // Set vector table offset
    SCB->VTOR = app_address;

    // Set stack pointer and jump
    __set_MSP(sp);
    ((void (*)(void))reset)();

    // Never reached
    while (1);
}

Application linker script key section:

/* Application must have vector table at start */
.isr_vector :
{
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
} >APPLICATION

Checkpoint: Bootloader successfully launches application.

Phase 6: Polish and Safety (Days 22-28)

Goals:

  • Add safety features
  • Handle edge cases
  • Test recovery scenarios

Tasks:

  1. Add auto-boot with countdown
  2. Handle power loss during update (metadata written last)
  3. Add “factory reset” option
  4. Test various failure scenarios
  5. Add status LED indication
  6. Write user documentation

Checkpoint: Robust bootloader survives power loss and bad firmware.


Hints in Layers

Hint 1: Flash Unlock Sequence

STM32 Flash is locked by default. You must write specific keys in sequence:

// These keys must be written in order, with no intervening writes
FLASH->KEYR = 0x45670123;  // KEY1
FLASH->KEYR = 0xCDEF89AB;  // KEY2

// After this, FLASH_CR.LOCK should be 0

If you write the wrong key or interrupt the sequence, Flash stays locked until reset.

Hint 2: Erase Before Write

Flash can only change bits from 1 to 0. To change 0 back to 1, you must erase:

// This will NOT work (trying to set bits that are 0)
flash_write_word(addr, 0x12345678);
flash_write_word(addr, 0xFFFFFFFF);  // Can't set bits back to 1!

// This works
flash_erase_sector(sector);           // All bits now 1
flash_write_word(addr, 0x12345678);   // Set some to 0
// But now you can't write again without another erase!
Hint 3: XMODEM Timeout Handling

XMODEM requires careful timeout handling:

int uart_getc_timeout(uint32_t timeout_ms) {
    uint32_t start = get_tick();
    while ((get_tick() - start) < timeout_ms) {
        if (uart_available()) {
            return uart_getc();
        }
    }
    return -1;  // Timeout
}

// In receiver loop:
int byte = uart_getc_timeout(1000);  // 1 second timeout
if (byte < 0) {
    // Timeout - send NAK and retry
    uart_putc(NAK);
}
Hint 4: Vector Table Alignment

The vector table must be aligned to a power of 2 boundary (at least 128 words):

// SCB->VTOR has alignment requirements
// For Cortex-M3/M4: bits [6:0] must be 0 (128-byte aligned)
// For practical use: align to sector boundary (e.g., 0x8000)

// Application at 0x08008000 is safe (32KB boundary)
SCB->VTOR = 0x08008000;

// But 0x08004123 would be WRONG (not aligned)
Hint 5: Safe Metadata Update

To survive power loss, update metadata LAST:

// WRONG - metadata written before firmware is complete
write_metadata(crc, size);
for (each block) {
    flash_write(block);  // Power loss here = corrupt metadata
}

// RIGHT - metadata written after firmware is verified
for (each block) {
    flash_write(block);
}
verify_flash(start, size);  // Re-read and check
write_metadata(crc, size);  // Only after verified
Hint 6: Returning to Bootloader

Application can request bootloader mode on next boot:

// In application:
#define MAGIC_BOOTLOADER_REQUEST 0xDEADBEEF
#define BACKUP_REG (*(volatile uint32_t *)0x40002850)  // RTC backup register

void request_bootloader(void) {
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;  // Enable PWR clock
    PWR->CR |= PWR_CR_DBP;               // Enable backup domain write
    BACKUP_REG = MAGIC_BOOTLOADER_REQUEST;
    NVIC_SystemReset();
}

// In bootloader:
if (BACKUP_REG == MAGIC_BOOTLOADER_REQUEST) {
    BACKUP_REG = 0;  // Clear flag
    goto bootloader_menu;  // Don't auto-boot
}

Testing Strategy

Test Categories

Category Purpose Examples
Boot Logic Verify decision making Button detect, CRC check
Flash Operations Verify programming Erase, write, read-back
XMODEM Protocol correctness Good packets, retries, errors
Integration Full workflow Upload → flash → boot
Recovery Failure handling Power loss, bad firmware

Critical Test Cases

// Test 1: Flash erase and write
void test_flash(void) {
    flash_unlock();

    // Erase sector
    assert(flash_erase_sector(TEST_SECTOR));

    // Verify erased (all 0xFF)
    for (int i = 0; i < SECTOR_SIZE; i += 4) {
        assert(*(uint32_t *)(SECTOR_ADDR + i) == 0xFFFFFFFF);
    }

    // Write pattern
    assert(flash_write_word(SECTOR_ADDR, 0x12345678));
    assert(*(uint32_t *)SECTOR_ADDR == 0x12345678);

    flash_lock();
}

// Test 2: CRC calculation
void test_crc(void) {
    uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
    uint32_t crc = crc32(data, 4);
    assert(crc == 0xB63CFBCD);  // Known value
}

// Test 3: XMODEM receive (requires host interaction)
void test_xmodem(void) {
    uart_puts("Send test file via XMODEM...\n");
    uint8_t buffer[4096];
    int size = xmodem_receive(buffer, sizeof(buffer));
    assert(size > 0);
    uart_printf("Received %d bytes\n", size);
}

// Test 4: Application validation
void test_validate_app(void) {
    // Write valid app image
    flash_program_app(test_app_bin, sizeof(test_app_bin));
    write_metadata(crc32(test_app_bin, sizeof(test_app_bin)),
                   sizeof(test_app_bin));

    assert(validate_application() == true);

    // Corrupt one byte
    flash_write_word(APPLICATION_START + 100, 0xDEADDEAD);
    assert(validate_application() == false);  // CRC mismatch
}

Recovery Testing

  1. Power loss during erase: Sector may be partially erased (not safe to read)
  2. Power loss during write: Some bytes written, CRC will fail
  3. Power loss during metadata write: Old metadata intact (won’t boot corrupt app)

Common Pitfalls & Debugging

Frequent Mistakes

Pitfall Symptom Solution
Erasing bootloader Device bricked! Never erase sectors containing bootloader
Wrong vector table offset Hard fault on first interrupt VTOR must point to app’s vector table
Stack pointer invalid Crash immediately after jump Validate SP is within RAM
Not clearing interrupts Spurious interrupt in app Disable and clear all before jump
Flash not unlocked Programming fails silently Check LOCK bit after unlock sequence
Metadata sector erased Boot loop Write metadata to separate sector

Debugging Strategies

  1. Use a debugger: SWD connection to inspect registers during boot
  2. LED indicators: Blink codes for different stages
  3. UART logging: Print progress at each step
  4. Checkpoints: Store boot stage in backup register, check on restart

Quick Debug Checks

void debug_boot_state(void) {
    uart_printf("VTOR: 0x%08X\n", SCB->VTOR);
    uart_printf("App SP: 0x%08X\n", *(uint32_t *)APPLICATION_START);
    uart_printf("App Reset: 0x%08X\n", *(uint32_t *)(APPLICATION_START + 4));
    uart_printf("Flash SR: 0x%08X\n", FLASH->SR);
    uart_printf("Flash CR: 0x%08X\n", FLASH->CR);
}

Extensions & Challenges

Beginner Extensions

  • LED status: Different blink patterns for boot stages
  • Multiple baud rates: Auto-detect or menu selection
  • Boot count: Track how many times device has booted

Intermediate Extensions

  • A/B partitions: Two app slots, always have fallback
  • Compressed images: Decompress firmware during flash
  • Secure boot: Verify firmware signature before execution
  • OTA updates: Receive firmware over network instead of serial

Advanced Extensions

  • Encrypted firmware: Decrypt on-the-fly during flash
  • Hardware security: Use STM32 RDP/WRP for protection
  • Rollback protection: Version counters to prevent downgrades
  • Differential updates: Only transfer changed blocks

The Interview Questions They’ll Ask

  1. “What happens when an ARM processor comes out of reset?”
    • Load SP from 0x0, load PC from 0x4, start execution
  2. “How does a bootloader update firmware safely?”
    • Erase app area, program new firmware, verify CRC, update metadata last
  3. “What is the Vector Table Offset Register (VTOR)?”
    • Tells CPU where to find exception vectors (allows relocation)
  4. “How do you handle a failed firmware update?”
    • CRC validation, don’t update metadata until verified, A/B partitions
  5. “Why can’t you just write to Flash like RAM?”
    • Flash bits can only go 1→0, need erase to set back to 1
  6. “What’s the difference between a hard reset and NVIC_SystemReset?”
    • Hardware reset reinitializes everything, NVIC reset is software-triggered

Books That Will Help

Topic Book Chapter
ARM boot sequence Making Embedded Systems Ch. 10
Flash programming STM32 Flash Programming Manual All
XMODEM protocol (Chuck Forsberg specification) -
CRC algorithms Hacker’s Delight Ch. 14
Vector table ARM Cortex-M TRM Vector Table section
Linker scripts Bare Metal C Ch. 2

Self-Assessment Checklist

Understanding

  • I can explain what happens at power-on before main() runs
  • I understand why Flash requires erase before write
  • I can describe XMODEM packet structure and error handling
  • I know why VTOR must be set before jumping to application
  • I understand how to make updates safe against power loss

Implementation

  • Bootloader shows menu and accepts commands
  • Flash erase and write work correctly
  • XMODEM receives files reliably
  • Application boots after successful update
  • Invalid applications are detected and rejected

Testing

  • All menu commands work
  • Recovery works after bad firmware
  • Power loss during update is handled safely
  • Multiple update cycles work correctly

Learning Milestones

  1. Bootloader boots and shows menu → Basic structure works
  2. You can receive data over XMODEM → Protocol implementation works
  3. Flash programming succeeds → You can modify your own Flash
  4. Application boots from bootloader → Vector table and jump work
  5. System recovers from bad update → Production-ready robustness

This guide was expanded from LEARN_ARM_DEEP_DIVE.md. For the complete learning path, see the project index.