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:
- Understand the ARM boot sequence: Know exactly what happens from power-on to your first instruction
- Master Flash self-programming: Write code that modifies its own Flash memory
- Implement serial protocols: Build XMODEM or custom protocols for reliable data transfer
- Design update-safe systems: Create bootloaders that can recover from failed updates
- Handle memory partitioning: Divide Flash between bootloader, application, and configuration
- 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
- Boot process: After power-on, what are the first two values the ARM CPU reads from memory?
- Flash characteristics: Why can’t you just write to Flash like RAM? What is an erase block?
- Vector table offset: How does the application’s vector table differ from the bootloader’s?
- Stack initialization: Who sets up the initial stack pointer—hardware or software?
- 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:
- Hardware initialization: Clocks, GPIO, UART
- Interactive menu: Serial interface for user commands
- XMODEM receiver: Receive firmware over serial
- Flash programmer: Write received firmware to Flash
- CRC validation: Verify firmware integrity before and after flashing
- Application launcher: Jump to main application
- Recovery mode: Stay in bootloader if application is invalid
Functional Requirements
- 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)
- 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
- Firmware Upload:
- XMODEM protocol with CRC-16
- Progress indication
- Automatic sector erase
- CRC-32 verification after write
- 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:
- Create bootloader linker script (32KB at 0x08000000)
- Create application linker script (starts at 0x08008000)
- Port UART code from Project 4
- Implement text menu system
- 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:
- Write flash_unlock() using key sequence
- Write flash_erase_sector()
- Write flash_write_word()
- Add error checking (busy flag, error flags)
- 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:
- Implement CRC-16-CCITT
- Implement packet receiving with timeout
- Implement NAK/ACK responses
- Handle retransmissions
- Test with
sxcommand 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:
- Connect XMODEM receive to Flash write
- Implement CRC-32 for firmware validation
- Write firmware metadata after successful flash
- Implement verify_firmware() command
- 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:
- Implement jump_to_application() in assembly
- Create simple test application with own vector table
- Test boot sequence: bootloader → application
- 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:
- Add auto-boot with countdown
- Handle power loss during update (metadata written last)
- Add “factory reset” option
- Test various failure scenarios
- Add status LED indication
- 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
- Power loss during erase: Sector may be partially erased (not safe to read)
- Power loss during write: Some bytes written, CRC will fail
- 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
- Use a debugger: SWD connection to inspect registers during boot
- LED indicators: Blink codes for different stages
- UART logging: Print progress at each step
- 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
- “What happens when an ARM processor comes out of reset?”
- Load SP from 0x0, load PC from 0x4, start execution
- “How does a bootloader update firmware safely?”
- Erase app area, program new firmware, verify CRC, update metadata last
- “What is the Vector Table Offset Register (VTOR)?”
- Tells CPU where to find exception vectors (allows relocation)
- “How do you handle a failed firmware update?”
- CRC validation, don’t update metadata until verified, A/B partitions
- “Why can’t you just write to Flash like RAM?”
- Flash bits can only go 1→0, need erase to set back to 1
- “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
- Bootloader boots and shows menu → Basic structure works
- You can receive data over XMODEM → Protocol implementation works
- Flash programming succeeds → You can modify your own Flash
- Application boots from bootloader → Vector table and jump work
- 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.