Skip to content

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

  1. Preprocessing

    • The preprocessor handles directives like #include and #define in your C/C++ source files.
    • Produces expanded source files (often named .i or .ii).
  2. Compilation

    • Converts the preprocessed source code into assembly (internally or explicitly).
    • Produces object files (.o, .obj) containing machine code for each module.
  3. 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.
  4. 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.
  5. Hex/Bin Generation

    • Many embedded tools produce a .hex, .bin, or similar file to be loaded into the microcontroller’s non-volatile memory (Flash).
  6. 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).
  7. 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

  1. The build system checks file timestamps or hashes (e.g., Make, CMake, vendor IDE projects).
  2. 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

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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.

References

← Previous Section | Next Section →