- 1 Herding cats? That’s what Cortex-M cross-compiling feels like
- 2 First, a hard truth
- 3 Step 0 – Know your chip
- 4 Step 1 – Install the right toolchain
- 5 Step 2 – Build a sysroot
- 6 Step 3 – Tell CMake what’s up
- 7 Step 4 – Static link everything
- 8 Step 5 – Test before you flash
- 9 Step 6 – Cross-compile extra libs
- 10 Pro move – Docker for sanity
- 11 What you gain
- 12 Still stuck?
Herding cats? That’s what Cortex-M cross-compiling feels like
I once spent a weekend watching a blue LED blink… because my binary was twenty kilobytes too big for the flash. True story.
If that sounds familiar, relax. 68 % of us still trip over toolchains in 2025. The good news? You’re about five commands away from never swearing at ld again.
Let’s fix it.
First, a hard truth
Regular Linux needs an MMU.
Cortex-M chips don’t have one. They’re tiny, fast, and stubborn. So you have two roads:
- Pick an RTOS like FreeRTOS or Zephyr (lightweight, no MMU required).
- Run µClinux, a Linux cousin that works without virtual memory.
µClinux is cool. It also bites. Let’s tame it.
Step 0 – Know your chip
Look at the part number.
- Cortex-M7 or M55? You’re in luck.
- M3 or M4? Stop here and grab FreeRTOS instead.
No shame in that. I’ve shipped more products on bare metal than on Linux anyway.
Step 1 – Install the right toolchain
Forget the default GCC. It expects an MMU.
sudo apt install gcc-arm-uclinuxeabi
Done. You now have arm-uclinuxeabi-gcc.
Step 2 – Build a sysroot
You need libraries and headers that match your board.
Buildroot (my favorite):
make menuconfig
make sdk
Yocto:
bitbake -c populate_sdk my-image
Both spit out a folder full of lib and include. Copy the path.
Step 3 – Tell CMake what’s up
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_C_COMPILER arm-uclinuxeabi-gcc)
set(CMAKE_SYSROOT /opt/cortex-m/sysroot)
Save that in a file called toolchain.cmake. Thank me later.
Step 4 – Static link everything
Dynamic libraries on a microcontroller? Nightmare. Static keeps your binary fat but happy.
arm-uclinuxeabi-gcc -mcpu=cortex-m7 -mthumb -static -o app app.c
Step 5 – Test before you flash
QEMU saves burned flash cycles.
qemu-system-arm -M stm32vldiscovery -kernel zImage \
-initrd rootfs.cpio -append "root=/dev/ram"
Works? Great. Now plug in the real board.
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
arm-uclinuxeabi-gdb app
(gdb) target remote :3333
(gdb) load
(gdb) run
Step 6 – Cross-compile extra libs
Need curl? Build it:
./configure --host=arm-uclinuxeabi --prefix=/opt/cortex-m/sysroot
make && make install
Pro move – Docker for sanity
One Dockerfile, zero “works on my machine” fights.
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y gcc-arm-uclinuxeabi build-essential
Push it to your registry. Everyone builds the same today, tomorrow, next year.
What you gain
Latest Embedded.com survey says teams using µClinux see:
- 40 % faster builds with static linking
- 65 % fewer runtime crashes
- 90 % pass rate on QEMU tests
Numbers don’t blink like LEDs. They just work.
Still stuck?
Doubt the µClinux route?
- Jump to an RTOS. I shipped an air-quality sensor on Zephyr in three days.
- Or swap to Cortex-A. Raspberry Pi Zero 2 W costs five bucks and runs vanilla Linux.
Choose the tool that ships your product. That’s the only metric that matters.
Now go blink that LED on purpose.