Building firmware for STM32 with LLVM toolchain

Building firmware for STM32 with LLVM toolchain

When we are talking about embedded development for ARM-based microcontrollers the obvious choice is gcc-based toolchain provided by ARM Ltd for many years. It's stable, reliable, and provides mature environment for the developers' needs. But in the "big" world there is another widely used open-source toolchain: LLVM. It's the default toolchain for macOS and FreeBSD, but is it capable to build the firmware for bare metal? Just a few years ago the answer was "it's difficult." While clang by itself supports a lot of target CPU architectures including many ARM embedded cores, it was recommended to build a final firmware using gcc-derived runtime libraries for C. It was much more painful for C++. So while it was possible - at least partially, it was definitely not a viable solution. But time flies like an arrow, and recently I found an official ARM repository on GitHub with the set of scripts to build a complete LLVM+clang toolchain for bare metal: https://github.com/ARM-software/LLVM-embedded-toolchain-for-Arm. And while it's still quite experimental, it works and generate a complete toolchain which can be used as (almost) direct replacement for the arm-none-eabi-gcc. Here I'll share my knowledge how to build the complete toolchain on macOS and use it for a simple embedded project for two different STM32 microcontrollers. It supposed to work with minimal adjustments for Linux/WSL also.

Step 1. Building the toolchain

Prerequisites

There are not too much dependencies required to build LLVM from the source code. It includes:

  • Toolchain. On macOS it could be XCode Command Line Tools or full XCode. I also tested with clang-19 from macports and it works. On Linux/WSL I recommend to install clang 16 or above from packages and set appropriative CC and CXX environment variables.

Tools and libraries are the part of XCode but also could be istalled separately:

  • Git.
  • libtool.
  • Backtrace.
  • Python3 + venv + pip. The ones came from XCode is fine, I also tested with 3.12 from ports

The rest could be installed using package manager:

  • Ninja.
  • CMake
  • ZLIB
  • LibEdit
  • libxml2
  • QEMU with arm target. Only if you want to run tests.

sudo port install ninja cmake zlib libedit libxml2 qemu -x11 +quartz        

Let's start:

wget https://github.com/ARM-software/LLVM-embedded-toolchain-for-Arm/archive/refs/tags/release-19.1.5.tar.gz -q
tar xf release-19.1.5.tar.gz
cd LLVM-embedded-toolchain-for-Arm-release-19.1.5
 ./setup.sh
. venv/bin/activate
pip install pygments meson pyyaml        

Now we have a snapshot of the repo with the latest release (19.1.5) and the python virtual environment configured. Next step is to create a build folder and configure CMake - but before that we need to rename two patch files which names are inconsistent with the build script:

mv patches/llvm-project/0007-PATCH-Remove-ctime.timespec.compile.pass.cpp-xfail.patch patches/llvm-project/0007-Remove-ctime.timespec.compile.pass.cpp-xfail.patch
mv patches/llvm-project/0008-PATCH-libc-AArch64-Add-an-AArch64-setjmp-longjmp.-10.patch patches/llvm-project/0008-libc-AArch64-Add-an-AArch64-setjmp-longjmp-101177.patch        

And finally we can run CMake to configure build:

mkdir build
cd build
cmake .. -GNinja -DFETCHCONTENT_QUIET=OFF        

During the process the sources for LLVM and picolibc will be fetched and configured. Now it's time to run build:

ninja llvm-toolchain        

If you installed QEMU and want to run tests:

ninja check-llvm-toolchain        

It takes quite a long time, and if everything was OK, the next step is to build pack:

ninja package-llvm-toolchain        

It generates Drag-n-Drop DMG image, but it also creates a complete toolchain directory. All you need is to move it into final place. In this example I use /opt/local/arm-none-eabi-llvm:

mv _CPack_Packages/Darwin-arm64/DragNDrop/LLVM-ET-Arm-19.1.5-Darwin-arm64/ALL_IN_ONE/LLVM-ET-Arm-19.1.5-Darwin-arm64 /opt/local/arm-none-eabi-llvm        

Now we can use it to build STM32 firmware!

Step 2. Using the toolchain

I'll show only the steps how to create a project using CubeMX and adapt it to use both arm-none-eabi-gcc and LLVM toolchains. CubeMX can generate project files of multiple types but besides Makefiles and two proprietary IDEs there are two common options:

  • STM32CubeMX which is also used with CLion
  • Common CMake which could be also used with VSCode.

The first option is the STM32CubeIDE targeting STM32F411CE.


Generating project of this type CubeMX first creates CMakeLists_template.txt and after that transforms it into CMakeLists.txt. Each time you press "Generate", the CMakeLists.txt has been regenerating, but the template stays untouched if exists. That means we need to patch the template to obtain a persistent effect. The switch between toolchains will be the LLVM_TOOLCHAIN parameter. This is what needs to be done:

  • Add a switch and second set of CMake parameters for the new toolchain:

F411 has a hardware FPU and it shall be explicitly enabled to use float data types when compiling by clang. I found it would call terminate on first FP operation otherwise. But it's not necessary to uncommit explicit soft FPU ABI for F103.

CMake will try to find modules if C++20 is enabled and it doesn't work in case of LLVM toolchain, so CMAKE_CXX_SCAN_FOR_MODULES=OFF is required.

  • Adjust the specific compiler anl likner options: clang prefers -mtune instead of -mcpu, doesn't require -mthumb for Cortex-M cores and rejects -mthumb-interwork. It also requires full triplet. While clang accept -Ofast it emits warning so would by good to adjust the release flags also:

Generate project again from CubeMX and these changes will be propagated to real CMakeLists.txt. Now we need to modify linker script, in this case STM32F411CEUX_FLASH.ld

  • Move MEMORY definition up:


  • Remove all (READONLY) specifiers from sections:


The last two things:

  • Core/Src/sysmem.c - just add #include <stddefs.h>

  • Core/Src/syscalls.c - add void as a parameter list for initialise_monitor_handles function:


These files are not necessary with LLVM+picolibc at all, but it's easier to modify them rather than to exclude them conditionally from build.

That's all! Now we have a project's frame usable with both GCC and LLVM toolchains. The first one is used by default, for the second it's necessary to add LLVM_TOOLCHAIN parameter:

-DLLVM_TOOLCHAIN=/opt/local/arm-none-eabi-llvm        

Here is an example if my CLion targets:



I tested this toolchain on the small C++ project gathering data from particle meter and temperature/humidity sensor and displaying it on a small LCD. The firmware works in the same way in all cases, and there are the size differences between toolchains:

  • GCC Debug:

  • LLVM DEBUG:

  • GCC MinRelSize:

  • LLVM MinRelSize:

CLang generates smaller debug builds but slightly bigger release once - the difference here is negligable though.

The second option is the pure CMake targeting STM32103C8.


In this case CubeMX generate CMakeLists.txt only once so we have to modify it directly.

We use the same LLVM_TOOLCHAIN based switch. CMake prefer enable_language() before project() so it's better to move it up.

CubeMX also generates CMakePresets.json. To reuse it we need to remove a hardcoded toolchain from there since it's already being included from CMakeLists.txt:


Next we need to provide a toolchain file for llvm: cmake/llvm-arm-none-eabi.cmake

set(CMAKE_SYSTEM_NAME               Generic)
set(CMAKE_SYSTEM_PROCESSOR          arm)

set(CMAKE_C_COMPILER_FORCED TRUE)
set(CMAKE_CXX_COMPILER_FORCED TRUE)

set(TOOLCHAIN_PREFIX                llvm-)
set(CMAKE_C_COMPILER                ${LLVM_TOOLCHAIN}/bin/clang)
set(CMAKE_ASM_COMPILER              ${CMAKE_C_COMPILER})
set(CMAKE_CXX_COMPILER              ${CMAKE_C_COMPILER})
set(CMAKE_AR                        ${LLVM_TOOLCHAIN}/bin/${TOOLCHAIN_PREFIX}ar)
set(CMAKE_OBJDUMP                   ${LLVM_TOOLCHAIN}/bin/${TOOLCHAIN_PREFIX}objdump)
set(CMAKE_OBJCOPY                   ${LLVM_TOOLCHAIN}/bin/${TOOLCHAIN_PREFIX}objcopy)
set(CMAKE_SIZE                      ${LLVM_TOOLCHAIN}/bin/${TOOLCHAIN_PREFIX}size)
set(CMAKE_CXX_SCAN_FOR_MODULES      OFF)

add_link_options(--ld-path=${LLVM_TOOLCHAIN}/bin/ld.lld)

set(CMAKE_EXECUTABLE_SUFFIX_ASM     ".elf")
set(CMAKE_EXECUTABLE_SUFFIX_C       ".elf")
set(CMAKE_EXECUTABLE_SUFFIX_CXX     ".elf")

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# MCU specific flags
set(TARGET_FLAGS "--target=armv7m-none-eabi -mtune=cortex-m3 ")

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${TARGET_FLAGS}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wpedantic -fdata-sections -ffunction-sections")
if(CMAKE_BUILD_TYPE MATCHES Debug)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g3")
endif()
if(CMAKE_BUILD_TYPE MATCHES Release)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os -g0")
endif()

set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-rtti -fno-exceptions -fno-threadsafe-statics")

set(CMAKE_C_LINK_FLAGS "${TARGET_FLAGS}")
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/stm32f103c8tx_flash.ld\"")
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections")
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group -lc -lm -Wl,--end-group")
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--print-memory-usage")

set(CMAKE_CXX_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group -lc++ -lc++abi -Wl,--end-group")        

We also need to modify .ld script, syscalls.c and sysmem.c files as it's described above - and the project is ready to use. A configuration example I used for testing:

Final words

While it still looks quite experimental and use unstable libc++ ABI, the current state of this toolchain and general direction look very promising. I hope quite soon the monopoly of GCC in the embedded domain will finally end as it already happened in a big system's world.

要查看或添加评论,请登录

Alexander Sopov的更多文章

其他会员也浏览了