Software Development Flow (Embedded Systems)
This page details a typical build and deployment process in embedded software development, including the roles of linker files and incremental compilation. While many examples use the GNU toolchain, the concepts below apply to a wide variety of compilers and IDEs (e.g., ARM Compiler, Keil, IAR, XC8, etc.) across vendors like STM32, Cypress (Infineon), and Microchip.
Overview of the Build Process
-
Preprocessing
- The preprocessor handles directives like
#include
and#define
in your C/C++ source files. - Produces expanded source files (often named
.i
or.ii
).
- The preprocessor handles directives like
-
Compilation
- Converts the preprocessed source code into assembly (internally or explicitly).
- Produces object files (
.o
,.obj
) containing machine code for each module.
-
Assembly (if separate)
- If you have raw assembly code or if your build toolchain treats assembly as a distinct step, the assembler converts
.s
files into object files.
- If you have raw assembly code or if your build toolchain treats assembly as a distinct step, the assembler converts
-
Linking
- Combines all object files into a single executable (or firmware image) using linker files (also called linker scripts, scatter files, or linker command files, depending on the toolchain).
- Resolves symbol references (e.g., function names, global variables) and places code/data in specific memory sections.
-
Hex/Bin Generation
- Many embedded tools produce a
.hex
,.bin
, or similar file to be loaded into the microcontroller’s non-volatile memory (Flash).
- Many embedded tools produce a
-
Flashing/Programming
- The final image is written to your device’s memory (e.g., Flash) using a hardware programmer/debugger (e.g., ST-Link for STM32, PSoC Programmer for Cypress, MPLAB IPE for Microchip, or generic JTAG/SWD).
-
Debugging
- Tools like GDB, Arm Debugger, IAR C-SPY, or Keil µVision allow you to step through code, set breakpoints, and inspect memory/registers on the target hardware.
Linker Files (Linker Scripts / Scatter Files)
In embedded systems, linker configuration files are crucial for defining how compiled code and data map into the microcontroller’s memory. Different toolchains have different naming conventions:
- GNU ld:
linker.ld
or.ld
scripts - Arm Compiler (Keil): Scatter files (
.sct
) - IAR:
.icf
(IAR Linker Configuration File) - Microchip: Linker scripts for XC compilers (e.g.,
.gld
)
Memory Regions
Most linker files specify the sizes and addresses of Flash (program memory) and RAM (data memory). For example:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
Sections
Code and data sections (e.g., .text, .data, .bss, .rodata) are placed in the corresponding memory regions. For instance:
SECTIONS
{
.text : {
*(.text)
*(.text*)
...
} > FLASH
.data : {
*(.data)
*(.data*)
...
} > RAM AT > FLASH
...
}
By customizing these files, you control where code and variables reside—critical for ensuring the program fits within the microcontroller’s memory constraints and meets performance requirements (e.g., running time-critical code from RAM).
Incremental Compilation
Incremental compilation is a build strategy where only changed source files (and their dependents) are recompiled, rather than recompiling the entire codebase. This concept applies across a variety of toolchains and IDEs:
How It Works
- The build system checks file timestamps or hashes (e.g., Make, CMake, vendor IDE projects).
- If only a single
.c
file is modified, only that file (and any files depending on it) are recompiled.
Why It’s Important for Embedded
-
Faster Iterations: Large projects (e.g., STM32, PSoC, or Microchip-based) can be slow to compile from scratch.
Incremental builds let you test changes more rapidly. -
Efficiency: Reduces CPU usage and the time you spend waiting in the edit-compile-flash-debug cycle.
Potential Pitfalls
-
Dependency Tracking: Misconfigured or missing dependencies can result in stale object files.
-
Linker Script Changes: Altering memory layouts or section placements often requires a full rebuild to ensure everything is placed correctly.
Best Practices for Embedded Builds
-
Keep Code Modular
- Break your application into multiple modules, each with its own responsibility.
- This helps the build system isolate changes and recompile only what’s necessary.
-
Use a Robust Build System
- CMake, Meson, Makefiles, or IDE-based solutions (Keil, IAR Embedded Workbench, MPLAB X) all support incremental builds.
- Ensure your system is correctly configured so unchanged modules are skipped.
-
Maintain a Clean Linker Configuration
- Document memory regions and section mappings clearly.
- Update if you switch MCUs (e.g., going from an STM32F4 to STM32F7, or from PSoC 4 to PSoC 6).
-
Enable Compiler Optimizations Wisely
- For Debug: Use lower optimization (e.g.,
-O0
,-Og
) to preserve debugging symbols and structure. - For Release: Use higher optimization (e.g.,
-O2
,-Os
) or vendor-specific flags for performance/size gains.
- For Debug: Use lower optimization (e.g.,
-
Test Early and Often
- Automated tests, static analysis, and continuous integration help catch bugs before they hit hardware.
- Regular flashing and real-device tests ensure performance and reliability in actual operating conditions.