A bootloader is a small program that runs when a microcontroller first powers up and helps load your main application code into memory. Writing your own bootloader gives you more control and customization for your embedded project.
1. Understanding the Boot Process
When a microcontroller first receives power, it begins executing instructions starting at a preset memory address called the reset vector. This is where the bootloader code needs to live. The bootloader’s job is to initialize hardware, set up memory, and ultimately load the main firmware application.
A typical boot sequence looks like this:
- Microcontroller comes out of reset and jumps to the reset vector
- Bootloader code begins executing
- Bootloader sets up hardware like clocks, memory, peripherals
- Bootloader copies application from storage into RAM
- Bootloader jumps to start of application code
Once the application is running, the bootloader is no longer needed. Good bootloader design minimizes its footprint while maximizing flexibility and functionality.
2. Selecting a Bootloader Location
The bootloader needs to live in non-volatile memory so it persists across power cycles. Some options include:
- On-chip flash – Saves space and cost but can’t be modified without an external programmer
- External flash – Allows in-circuit reprogramming of the bootloader
- ROM – Cheapest option but bootloader cannot be changed
The right choice depends on your budget, use case, and how often you expect to modify the bootloader. Using on-chip flash offers convenience while external flash provides more flexibility.
3. Minimal Hardware Initialization
The bootloader only needs to initialize the hardware necessary to load and run the application. This includes:
- Clocks – Set up oscillator, PLL, system clocks
- Memory – Initialize RAM and flash
- Peripherals – Minimal UART, I/O for application loading
Keep the bootloader hardware initialization code lean. Leave more intensive setup for the main application.
4. Implement Bootloader-Application Handoff
To launch the user application, the bootloader must jump to the correct location in memory. This handoff requires:
- Copy application from storage into RAM
- Configure memory protection for bootloader region
- Set stack pointer for application
- Jump to application reset vector in code
Use linker scripts to ensure the application ends up at the expected address in memory. The bootloader can then cleanly jump to that known location.
5. Communication Interfaces
The bootloader needs some interface to load the application binary onto the microcontroller. Common options are:
- UART – Universal asynchronous receiver/transmitter for serial communication
- USB – Host support for external flash drives or host PC
- Ethernet – Network bootloader for centralized application loading
- CAN – Robust automotive bus for embedded networks
Add only the communication necessary for your particular application loading method. UART is simplest, while USB, Ethernet, or CAN enable more advanced use cases.
6. Updating the Bootloader
Sometimes the bootloader itself needs to be upgraded and reflashed. Several techniques allow this:
- External programmer – Full chip erase and reprogram flash
- Bootloader flash section – Separate memory region for just bootloader code
- Application uploading new bootloader – Some interfaces allow self-updating
Allowing field upgrades requires planning to avoid bricking devices. Having at least one backup loading method is wise.
7. Advanced Capabilities
Some additional features to consider adding to bootloaders:
- Encryption – Code signing and authentication to prevent tampering
- Compression – Save bandwidth by compressing application binaries
- Error checking – CRC, checksums to validate application integrity
- Fallback image – Rollback to last working firmware version
These add security, robustness, and flexibility. But they also increase complexity and boot time. Only implement features needed for your use case.
8. Optimizing Size vs Functionality
Design decisions for bootloaders involve tradeoffs between size and functionality. Some guidelines include:
- Use only necessary hardware features to minimize code space
- Disable interrupts and complex peripherals if not needed
- Divide code into separate modules for selective inclusion
- Reuse initialization and functionality from application when possible
A minimal bootloader might be only a few KB. More advanced ones with extensive features can grow to 10-30KB. Find the right balance for each project.
9. Testing the Bootloader
Thoroughly test bootloader operation before relying on it for production. Some key tests:
- Verify bootloader initializes hardware properly
- Test loading applications to memory successfully
- Validate robustness with corrupted application images
- Confirm fallback and recovery mechanisms function
- Try power cycling and resetting device through all states
Catching bootloader bugs late in development is costly. Test rigorously on prototype hardware before finalizing design.
10. Documentation and Release
Don’t forget good documentation on bootloader operation, interfaces, error codes, capabilities, etc. Include:
- In-code comments explaining operation
- Textual guide for usage and features
- Changelog for version history
- Sample code and examples
This allows others to successfully leverage your bootloader in their applications. Consider open sourcing the bootloader code for community benefit.
Writing a custom bootloader from scratch is challenging but very rewarding. Use the techniques outlined here for a robust bootloader tailored to your embedded system needs.