This is Part 8 of my Cortex‑M7 without hardware series.
Part 7 was supposed to be the last post. The infrastructure was complete with identical outputs, and there was nothing left to build. However, an obvious problem remained: most of the code was duplicated. Each of the projects, Renode and the QEMU, carried their own copy of the startup code, the linker script, the syscall stubs, and the C++ runtime shims.
If you know me, I just love to make things as modular as possible while also reducing code repetition. This post extracts the aforementioned shared infrastructure into its own library — cortex-m7-common 1 — and rewires both projects to consume it. The result: each project shrinks to the handful of files that are genuinely project-specific, and any future Cortex-M7 project starts with a solid baseline instead of a copy-paste.
This post will also touch upon several CMake keywords and functions, such as ALIAS, configure_file() and EXCLUDE_FROM_ALL.
The duplication problem
Here is what both projects looked like at the end of Part 7:
| File | cortex-m7-renode | cortex-m7-qemu | Identical? |
|---|---|---|---|
startup.cpp | ✓ | ✓ | ~95 % — same Reset_Handler, same init loops |
syscalls_common.cpp | ✓ | ✓ | 100 % |
syscalls_semihosting.cpp | ✓ | ✓ | ~90 % — same semihosting mechanism, minor backend differences |
cxx_runtime.cpp | ✓ | ✓ | 100 % |
linker.ld | ✓ | ✓ | ~90 % — only the MEMORY block differs |
| CMake flags / post-build | ✓ | ✓ | ~80 % — same compiler flags, same objcopy steps |
vectors.cpp | ✓ | ✓ | No — project-specific vector table entries |
main.cpp | ✓ | ✓ | No — different application logic |
Six of eight files are nearly or fully identical. Imagine a situation when a bug is found in _sbrk, it would have to be fixed in two source files. Or when a new compile and/or linker flag is needed, two CMakeLists.txt files must be updated. The more projects there are, the worse this gets. That is the classic motivation for extracting a library.
Design decisions
Three choices shaped how the shared library was built: the CMake library type, the linker script strategy, and the dependency mechanism.
OBJECT libraries, not STATIC
When building a library, the natural way of going about is to use CMake’s add_library(... STATIC ...). This creates a static library (.a) which is an archive of object (.o) files, and the linker pulls in only those object files that resolve an undefined reference. That is normally a feature — it keeps binaries small. But it breaks embedded startup code.
Reason being that startup.o defines Reset_Handler. The vector table in vectors.cpp references it, so the linker would pull it in. Yet startup.o also calls SystemInit() through a weak symbol, and syscalls_common.o defines _sbrk which malloc calls indirectly through newlib. If the linker does not see a direct reference at the right moment, it may skip the object file entirely — silently dropping weak symbols and leaving the binary broken at runtime.
OBJECT libraries sidestep the problem. CMake passes every .o file directly on the link command line, unconditionally. Every symbol — weak or strong — is visible to the linker:
add_library(
cortex_m7_baremetal OBJECT
${PROJECT_SOURCE_SUBDIRECTORY}/startup.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/default_system.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/cxx_runtime.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/syscalls_common.cpp)
add_library(${PROJECT_NAME}::baremetal ALIAS cortex_m7_baremetal)
While we are creating a library, I have not explained the following anywhere before, so might as well do it here. The ALIAS target with the :: namespace in the above snippet is a CMake convention that makes the consuming side read like a proper package import: target_link_libraries(my_app PRIVATE cortex-m7-common::baremetal). 2
Linker script template
Both projects had a full linker script (linker.ld or memory.ld depending on the project) — some 130 lines each — that differed only in the MEMORY block. The STM32F746 puts flash at 0x08000000 with 1 MB; the MPS2-AN500 puts it at 0x00000000 with 4 MB. Everything else — section layout, alignment, symbol exports, heap/stack boundaries — is identical.
The solution is a template file for CMake. The shared library carries linker.ld.in with placeholder variables:
MEMORY
{
FLASH (rx) : ORIGIN = @CORTEXM7_FLASH_ORIGIN@, LENGTH = @CORTEXM7_FLASH_LENGTH@
RAM (rwx) : ORIGIN = @CORTEXM7_RAM_ORIGIN@, LENGTH = @CORTEXM7_RAM_LENGTH@
}
A CMake module processes the template at configure time and wires the result into the linker flags:
function(cortexm7_configure_linker_script target)
cmake_parse_arguments(ARG "" "FLASH_ORIGIN;FLASH_LENGTH;RAM_ORIGIN;RAM_LENGTH;STACK_RESERVE" "" ${ARGN})
# Set variables for configure_file @ONLY substitution
set(CORTEXM7_FLASH_ORIGIN ${ARG_FLASH_ORIGIN})
set(CORTEXM7_FLASH_LENGTH ${ARG_FLASH_LENGTH})
set(CORTEXM7_RAM_ORIGIN ${ARG_RAM_ORIGIN})
set(CORTEXM7_RAM_LENGTH ${ARG_RAM_LENGTH})
set(CORTEXM7_STACK_RESERVE ${ARG_STACK_RESERVE})
set(_template "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../linker/linker.ld.in")
set(_output "${CMAKE_CURRENT_BINARY_DIR}/linker.ld")
configure_file("${_template}" "${_output}" @ONLY)
target_link_options(${target} PRIVATE "-T${_output}")
endfunction()
configure_file() replaces every @VAR@ with the corresponding CMake variable and writes the result to the build directory. 3 The consuming project just calls:
cortexm7_configure_linker_script(
${PROJECT_NAME}
FLASH_ORIGIN 0x08000000
FLASH_LENGTH 1024K
RAM_ORIGIN 0x20000000
RAM_LENGTH 320K
STACK_RESERVE 0x1000)
Four variables configure a 130-line linker script. Adding a third board in the future requires only these four values. Of course, it is also possible to use per project linker scripts like previously, this just allows quick roll-out for a new project.
FetchContent over submodules
I covered the benefits of FetchContent 4 in detail in my CMake canonical project structure post. To recap: instead of git submodules, each project declares the dependency in CMakeLists.txt and CMake fetches it at configure time:
FetchContent_Declare(
_cortex_m7_common
GIT_REPOSITORY https://gitlab.com/sorhanp/cortex-m7-common.git
GIT_TAG v0.1.0
GIT_SHALLOW TRUE
UPDATE_DISCONNECTED TRUE)
FetchContent_MakeAvailable(_cortex_m7_common)
GIT_TAG v0.1.0 pins the version for reproducible builds. After FetchContent_MakeAvailable, the common library’s targets and CMake modules are available as if they were part of the project.
The library structure
Here is what cortex-m7-common contains:
cortex-m7-common/
├── CMakeLists.txt
├── cortex-m7-common/
│ ├── startup.cpp # Reset_Handler, .data copy, .bss zero, constructors
│ ├── default_system.cpp # Weak SystemInit() and SystemCoreClockUpdate()
│ ├── cxx_runtime.cpp # __cxa_pure_virtual / __cxa_deleted_virtual stubs
│ ├── syscalls_common.cpp # _read, _sbrk, _close, _fstat, _isatty, _lseek
│ ├── syscalls_semihosting.cpp # _write and _exit via ARM semihosting
│ ├── freertos_handlers.cpp # SVC/PendSV/SysTick → FreeRTOS trampolines
│ └── freertos_hooks.cpp # vApplicationMallocFailedHook, StackOverflowHook
├── linker/
│ └── linker.ld.in # Linker script template
└── cmake/
├── CortexM7Flags.cmake # -mcpu, -mthumb, -mfpu, nano.specs, gc-sections
├── CortexM7LinkScript.cmake # configure_file() wrapper for the linker template
├── CortexM7PostBuild.cmake # objcopy hex/bin, size report
└── CortexM7FetchDeps.cmake # FetchContent helpers for CMSIS and FreeRTOS
The source files are grouped into three OBJECT libraries:
# Baremetal core — every Cortex-M7 project needs this
add_library(
cortex_m7_baremetal OBJECT
${PROJECT_SOURCE_SUBDIRECTORY}/startup.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/default_system.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/cxx_runtime.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/syscalls_common.cpp)
add_library(${PROJECT_NAME}::baremetal ALIAS cortex_m7_baremetal)
# Semihosting output backend — optional
add_library(cortex_m7_semihosting OBJECT ${PROJECT_SOURCE_SUBDIRECTORY}/syscalls_semihosting.cpp)
add_library(${PROJECT_NAME}::semihosting ALIAS cortex_m7_semihosting)
# FreeRTOS glue — optional
add_library(cortex_m7_freertos OBJECT ${PROJECT_SOURCE_SUBDIRECTORY}/freertos_handlers.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/freertos_hooks.cpp)
add_library(${PROJECT_NAME}::freertos ALIAS cortex_m7_freertos)
The split is deliberate. A baremetal project that uses UART output and no RTOS links only baremetal. A FreeRTOS project with semihosting links all three. No project pays for code it does not use.
The four CMake modules eliminate build-system duplication:
CortexM7Flags sets
-mcpu=cortex-m7,-mthumb, optional FPU flags, size-optimization options (-ffunction-sections,-fdata-sections,-fno-exceptions,-fno-rtti), and linker options (--specs=nano.specs,-nostartfiles,--gc-sections). Without this module, every project would duplicate roughly 20 lines of compiler and linker flags.CortexM7LinkScript wraps
configure_file()shown above. One function call replaces a per-project linker script.CortexM7PostBuild adds post-build
objcopycommands to generate.hexand.binfiles alongside the ELF, plus asizereport. Three lines replace what used to be a copy-pastedadd_custom_commandblock.CortexM7FetchDeps provides helper functions to fetch CMSIS-Core and FreeRTOS-Kernel via FetchContent with sensible defaults (pinned tags, shallow clones). Projects that need these dependencies call
cortexm7_fetch_cmsis()orcortexm7_fetch_freertos()instead of writing the FetchContent boilerplate themselves. I will cover FreeRTOS on Cortex-M7 in later series.
Integration: the consuming projects
After extraction, each project keeps only what is genuinely project-specific.
cortex-m7-renode (baremetal, STM32F746 on Renode):
cortex-m7-renode/
├── cortex-m7-renode/
│ ├── vectors.cpp # STM32F746 vector table
│ ├── main.cpp # Application code
│ └── syscalls_semihosting.cpp # Renode-specific semihosting backend
├── CMakeLists.txt
├── CMakePresets.json
└── toolchain/
└── arm-none-eabi.cmake
cortex-m7-qemu (FreeRTOS, MPS2-AN500 on QEMU):
cortex-m7-qemu/
├── cortex-m7-qemu/
│ ├── vectors.cpp # MPS2-AN500 vector table
│ ├── main.cpp # FreeRTOS tasks
│ ├── system_qemu_mps2_an500.cpp # Device-specific SystemInit
│ ├── cmsis_device.hpp # CMSIS-Core shim
│ ├── FreeRTOSConfig.h # FreeRTOS configuration
│ └── syscalls_uart.cpp # Optional UART backend
├── CMakeLists.txt
├── CMakePresets.json
└── toolchain/
└── arm-none-eabi.cmake
The Renode project carries its own syscalls_semihosting.cpp because Renode’s SemihostingUart peripheral 5 only implements a subset of the ARM semihosting specification. Specifically, it handles SYS_WRITEC (op 0x03 — write a single character) but does not support SYS_OPEN or SYS_WRITE (open a host file handle and write a buffer to it).
The common library’s semihosting backend uses the SYS_OPEN/SYS_WRITE pair, which is more efficient — one semihosting trap per _write call instead of one per character — but only works on hosts that implement the full protocol, like QEMU.
The Renode-specific backend falls back to a character-at-a-time loop through SYS_WRITEC, which SemihostingUart handles correctly, again demonstrating the flexibility of this system!
The QEMU project has a few extra files — system_qemu_mps2_an500.cpp overrides the weak SystemInit from the common library to set SystemCoreClock to 25 MHz, FreeRTOSConfig.h configures the RTOS, and cmsis_device.hpp provides the CMSIS-Core device shim.
The CMakeLists.txt is where the payoff is most visible. Here is the Renode project’s complete build file, minus the boilerplate at the beginning and Renode launch target:
# Fetch shared Cortex-M7 common library
FetchContent_Declare(
_cortex_m7_common
GIT_REPOSITORY https://gitlab.com/sorhanp/cortex-m7-common.git
GIT_TAG v0.1.0
GIT_SHALLOW TRUE
UPDATE_DISCONNECTED TRUE)
FetchContent_MakeAvailable(_cortex_m7_common)
# Exclude unused OBJECT libraries from the build
set_target_properties(cortex_m7_freertos cortex_m7_semihosting PROPERTIES EXCLUDE_FROM_ALL TRUE)
# Append shared CMake modules to module path
list(APPEND CMAKE_MODULE_PATH "${_cortex_m7_common_SOURCE_DIR}/cmake")
include(CortexM7Flags)
include(CortexM7LinkScript)
include(CortexM7PostBuild)
# Project-specific sources only
set(PROJECT_SOURCES
${PROJECT_SOURCE_SUBDIRECTORY}/vectors.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/main.cpp
${PROJECT_SOURCE_SUBDIRECTORY}/syscalls_semihosting.cpp)
add_executable(${PROJECT_NAME} ${PROJECT_SOURCES})
target_link_libraries(${PROJECT_NAME} PRIVATE cortex_m7_baremetal)
# Apply Cortex-M7 flags
foreach(_target ${PROJECT_NAME} cortex_m7_baremetal)
cortexm7_set_flags(${_target} FPU_ABI hard FPU_TYPE fpv5-sp-d16)
endforeach()
# STM32F746 memory layout
cortexm7_configure_linker_script(
${PROJECT_NAME}
FLASH_ORIGIN 0x08000000
FLASH_LENGTH 1024K
RAM_ORIGIN 0x20000000
RAM_LENGTH 320K
STACK_RESERVE 0x1000)
cortexm7_add_post_build(${PROJECT_NAME})
Compare this to the Part 7 CMakeLists.txt that carried all the compiler flags inline, had a hand-written linker script, and listed every infrastructure source file in add_executable. The project-specific logic — which OBJECT libraries to link, the memory map, and the source files — is all that remains.
Excluding unused libraries
One subtlety: FetchContent_MakeAvailable brings in all targets from the common library, including cortex_m7_freertos which depends on FreeRTOS headers to be present. As the Renode project does not fetch FreeRTOS, attempting to build the cortex_m7_freertos OBJECT library would fail with missing includes.
The fix is EXCLUDE_FROM_ALL:
set_target_properties(cortex_m7_freertos cortex_m7_semihosting PROPERTIES EXCLUDE_FROM_ALL TRUE)
This tells CMake not to include these targets in the default build. They exist in the project graph (so CMake does not error on unknown targets), but they are not compiled unless something explicitly depends on them. 6 The Renode project excludes both cortex_m7_freertos (no FreeRTOS) and cortex_m7_semihosting (Renode needs a project-specific semihosting backend) and simply includes only the cortex_m7_baremetal.
The QEMU project does not exclude anything — it uses all three OBJECT libraries.
Build and run
Renode
For Renode, the behavior is unchanged — this is just a structural refactoring and no features have changed.
git clone \
--depth 1 \
--branch blog-common --single-branch \
https://gitlab.com/sorhanp/cortex-m7-renode.git \
cortex-m7-renode-blog-common
cd cortex-m7-renode-blog-common
cmake --preset arm-none-eabi-debug
cmake --build --preset arm-gcc-debug-build --target run-renode
Expected output:
Hello from Cortex-M7 on Renode!
Value: 42
QEMU
Here we see a different kind of output and also QEMU build creates a FreeRTOS task.
git clone \
--depth 1 \
--branch blog-common --single-branch \
https://gitlab.com/sorhanp/cortex-m7-qemu.git \
cortex-m7-qemu-blog-common
cd cortex-m7-qemu-blog-common
cmake --preset arm-none-eabi-debug
cmake --build --preset arm-gcc-debug-build --target run-qemu
Expected output:
Hello from Cortex-M7 main!
Hello #1 from Cortex-M7 Task
Hello #2 from Cortex-M7 Task
Hello #3 from Cortex-M7 Task
Hello #4 from Cortex-M7 Task
Hello #5 from Cortex-M7 Task
Exiting...
What we built
| What changed | Why it matters |
|---|---|
Common code extracted to cortex-m7-common | Single source of truth for startup, syscalls, runtime stubs |
| OBJECT libraries instead of STATIC | Weak symbols survive linking — overridable handlers work correctly |
Linker script template (linker.ld.in) | One template serves Cortex-M7 boards; four variables configure it |
| CMake modules for flags, linking, post-build | Projects inherit correct flags without duplicating build logic |
| FetchContent integration at a pinned tag | Reproducible builds, no submodules, easy version bumps |
| Each project reduced to vectors + main | Adding a third board means one new project with minimal CMake |
The shared library is now the baseline for any future Cortex-M7 project. Adding a third emulator or board is a matter of writing a new vectors.cpp with the device-specific vector table, a new main.cpp with the application logic, and a short CMakeLists.txt that fetches cortex-m7-common and links the libraries it needs. The infrastructure — startup, syscalls, linker script, build flags — is written once and shared everywhere.
The full source is available on GitLab for each project:
References
Cortex-M7 common library: https://gitlab.com/sorhanp/cortex-m7-common ↩︎ ↩︎
CMake OBJECT libraries: https://cmake.org/cmake/help/latest/command/add_library.html#object-libraries ↩︎
CMake configure_file: https://cmake.org/cmake/help/latest/command/configure_file.html ↩︎
CMake FetchContent: https://cmake.org/cmake/help/latest/module/FetchContent.html ↩︎
Renode SemihostingUart: https://renode.readthedocs.io/en/latest/basic/describing_platforms.html ↩︎
CMake EXCLUDE_FROM_ALL: https://cmake.org/cmake/help/latest/prop_tgt/EXCLUDE_FROM_ALL.html ↩︎
Cortex-M7 Renode project: https://gitlab.com/sorhanp/cortex-m7-renode ↩︎
Cortex-M7 QEMU project: https://gitlab.com/sorhanp/cortex-m7-qemu ↩︎