Embedded systems rely on a bootloader and startup code to initialize the hardware and software components and get the system up and running. The bootloader is a small piece of code that runs when the system first powers on and gets things started. The startup code then takes over and completes the system initialization before the main application code runs.
The primary job of the bootloader is to load the main program into memory and start executing it. This requires a few key tasks:
- Initialize critical hardware components like CPU, RAM, clocks, and I/O peripherals
- Detect and initialize external memory if present
- Copy the main program from non-volatile storage (flash or ROM) into RAM
- Pass control to the startup code of the main program
The bootloader runs from internal ROM or flash memory and has access to limited hardware resources. It needs to set up just enough of the system to be able to read the main program into RAM and start it running. The bootloader typically runs in a special CPU mode with minimal stack space.
Several design decisions go into implementing an embedded bootloader:
- Integrated vs Separated: The bootloader code can be linked into the same executable as the main program or exist as a completely separate program.
- Boot Medium: The bootloader needs to access the medium containing the main program, such as flash memory, SD card, or external EEPROM.
- Communication: A communication protocol may be needed for the bootloader to download the main program over a network link.
- Security: Some bootloaders incorporate security features like digital signatures to verify the authenticity of the main firmware image before booting it.
- Updatability: The bootloader may need to support field upgrades of the main program image.
Bootloader code often lives in a separate partition of internal flash memory. This reserves space for the bootloader while keeping it isolated from the main code which may need to be updated. The bootloader partition will also need to be protected against accidental writes or erasure.
After the bootloader passes control to the main program, the startup code completes the hardware and software initialization. This enables the full functionality required by the main application. Typical startup tasks include:
- Enable protected/privileged CPU modes as needed
- Set up memory regions and segments for code, data, stack, heap, etc.
- Initialize CPU registers and critical variables
- Set up the stack pointer and stack overflow checks
- Initialize system clocks, bus speeds, and power management
- Configure peripheral devices drivers like GPIO, I2C, SPI, UARTs, etc.
- Test and initialize external RAM and memories
- Load initialized data from ROM to RAM as needed
- Clear or initialize RAM contents as desired
- Call constructors for global C++ objects
- Initialize operating system and device drivers as needed
The startup code culminates in a call to the
main() function of the application. At this point the hardware is fully operational and all OS, libraries, and drivers are ready for use. The application main loop will run until the system is rebooted.
Startup Code Organization
The startup code may be implemented across multiple files and modules:
- Low-level CPU and hardware initialization code
- Board/SoC specific system initialization
- OS kernel and thread initialization
- Device driver initialization functions
- C/C++ runtime and library initialization like heap allocation
- Constructors for global objects
- Main function call
The order of operations is very important in the startup sequence. Things like the stack pointer, exceptions, and interrupts must be set up before any substantive initialization code can run. There may also be ordering dependencies like initializing the system clocks before SPI peripherals.
Bootloader vs Startup Code
The bootloader and startup code serve related but distinct purposes:
- The bootloader is a standalone program that runs upfront to load the main firmware.
- The startup code is part of the main program that runs initialization before the main loop.
- Bootloader focuses on minimal viable hardware for loading firmware.
- Startup enables full functionality for main application needs.
- Bootloader runs in a limited standalone environment.
- Startup runs as part of main program with all resources available.
- Bootloader code is buried in internal ROM or flash.
- Startup code is linked into each main firmware image.
The bootloader hands off control to the startup code of the main program once it has copied the firmware image to RAM and can start executing it. The startup code then picks up where the bootloader left off and initializes everything needed for the main application code to run.
Developing an embedded bootloader requires attention to some unique aspects:
- Restricted execution environment – Limited memory, stack, interrupts, etc.
- May need to support multiple CPU architectures or hardware configurations.
- Requires access to non-volatile storage like flash memory or external memories.
- Needs fault tolerance for recovery from invalid firmware images.
- Security features to prevent tampering with firmware images.
- Methods for updating the bootloader itself as needed.
As a result bootloaders are usually written in Assembly or C languages for low level access, speed, small size, and avoidance of runtime dependencies. The code needs to be thoroughly tested under various failure conditions like power cycles during firmware updates. Secure bootloaders require significant expertise in cryptographic functions, key storage, and authentication techniques.
Some processors have dedicated boot ROM functionality or standardized interfaces that simplify bootloader development. This helps handle the processor and hardware initialization while the bootloader focuses on loading the desired firmware image from external memories and starting it up properly.
Rigorous testing helps ensure the bootloader is robust and fault-tolerant:
- Try corrupt firmware images or partially erased flashes
- Introduce faulty image headers or invalid signatures
- Simulate unsuccessful flash erase/write operations
- Cut power during firmware image updates
- Try overflowing fixed bootloader buffers
- Manipulate CPU modes and privilege levels
- Check fallback recovery modes like default images
Hardware tools like logic analyzers help capture the detailed boot up sequences. Bootloader code will be some of the most heavily exercised in the entire system – it runs on every reset and power cycle.
Startup Code Development
On the startup code side, key aspects include:
- Ordering initialization steps properly
- Tuning performance of startup sequence
- Setting up memory regions and segments
- Early initialization of core peripherals
- Late initialization of optional drivers/features
- Calling constructors in right order
- InitializingVariables before code access
- Error handling if initialization fails
The startup code executes in a more flexible environment than the bootloader but still must follow strict ordering rules. Complex hardware, OS, and drivers can make the startup sequence quite lengthy. The startup code also needs to play nicely with bootloader limitations like avoiding its reserved memory regions.
Startup time directly impacts user experience on resets and wakeups. Some optimization techniques include:
- Initialize hardware asynchronously or on-demand if possible
- Break startup into required and optional modules
- Initialize RAM in bulk instead of bytewise clears
- Order code to avoid unnecessary jumps
- Tune compiler settings for smaller code size
- Initialize global variables on first use
Faster startup gets the main application running quicker at the cost of some added complexity. The optimal tradeoff depends on the application needs. Latencies may range from milliseconds for simple microcontrollers to seconds for complex embedded OS boots.
The bootloader and startup code form the crucial foundation needed to transition an embedded system from power on to full operation. The compact bootloader initializes just enough hardware to load the main program into memory and begin executing it. The flexible startup code then brings up all the capabilities required for the main application code to run successfully. Together they allow an embedded system to efficiently move from reset to real world functioning.