Clang-LLVM编译STM32

Ninja,CMake,Make

Views: 1 times Updated on March 6, 2025 Posted by elmagnifico on March 6, 2025

Foreword

全网找不到几个LLVM工具链进行交叉编译的例子,如果只是Linux环境,那可能随便弄,但是Windows下要拉哪个库,环境变量什么的要怎么弄都没找到例子。东拼西凑了几个Blog的内容,总算是完整可以编译了

如果直接用Keil的AC6,那没啥难度,如果抛弃他,改用开源又要怎么弄

Clang-LLVM

GNU GCC编译流程

源代码 → GCC前端 → GIMPLE(中间表示) → RTL → 汇编代码 → GNU汇编器 → 目标代码

Clang/LLVM编译流程

源代码 → Clang前端 → LLVM IR → LLVM优化器 → LLVM后端 → 汇编代码 → 目标代码

同时也有各种魔改的类型,使用混合的,Clang的前端+GNU的后端,Clang的前端目前公认是比GNU要快很多的

环境

目前开源可用的LLVM的交叉编译工具链,只看到了这一个

https://github.com/ARM-software/LLVM-embedded-toolchain-for-Arm/releases

官方版本的工具链目前还在建设阶段,根本没有Release可用

https://github.com/arm/arm-toolchain/tree/arm-software

下一个,解压,将bin目录加入到环境变量中

CMake、Make、Ninja(如果需要)也同样需要安装

如果是用MSYS2,可以通过下面的方式安装

安装make

pacman -S make

安装cmake

pacman -S cmake

安装ninja

pacman -S ninja

查看所有安装的包

pacman -Sg

编译

编译工程,以这个为例,他没有说明具体环境怎么弄,而LLVM最麻烦的就是环境问题了

https://github.com/wuwbobo2021/arm-llvm-stm32f103-blinky

他是make构建的整个工程,所以只需要make工具即可,目前LLVM需要人工指定libc、libm的目录或者引用

所以需要这么写

make "ARM_LIB_DIR=I:\llvm\LLVM-ET-Arm-19.1.5-Windows-x86_64\lib\clang-runtimes\arm-none-eabi\armv7m_soft_nofp"

需要指定你的LLVM的armv7m_soft_nofp路径,才能正确找到libc和libm

这样就能正常编译完成了

image-20250306152503416

分析对比

对比一下和arm-gnu-none-eabi的编译有什么区别

# output name
TARGET = arm-llvm-stm32f103-blinky

# debug build?
DEBUG = 0
# optimization
OPT = -O3 -flto

ASM_SOURCES = stm32f10x/startup_stm32f103xb.s
LD_SCRIPT = stm32f103c8tx_flash.ld

C_DIRS = arm \
         stm32f10x \
         .

C_DEFS = -DUSE_STDPERIPH_DRIVER \
         -DSTM32F10X_MD

# 主要这里指定库路径,不知道还没有没有啥办法自动寻找,而不用指定路径
# it must be set for libc and libm, like
# <LLVM Embedded Toolchain for Arm>/lib/clang-runtimes/arm-none-eabi/armv7m_soft_nofp
ARM_LIB_DIR =

# 这个路径却能自动寻找到
# leave empty if the original llvm/clang should be used
ARM_LLVM_PATH =

# 修改替换使用的一些工具
CC = $(ARM_LLVM_PATH)clang
AS = $(ARM_LLVM_PATH)clang
CP = $(ARM_LLVM_PATH)llvm-objcopy
SZ = $(ARM_LLVM_PATH)llvm-size
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S

OPENOCD = openocd \
	      -f /usr/share/openocd/scripts/interface/stlink.cfg \
	      -f /usr/share/openocd/scripts/target/stm32f1x.cfg
GDB = gdb-multiarch

ifeq '$(findstring ;,$(PATH))' ';'
# Windows
RM = del /Q
else
RM = rm -f
endif

# 一些flag的写法或者表示和gnu不太一样了,需要独立区分
FLAGS = -mthumb -mcpu=cortex-m3 --target=thumbv7m-none-unknown-eabi -mfpu=none

C_INCLUDES = $(foreach d, $(C_DIRS), -I$(d)) -I$(ARM_LIB_DIR)/include
C_SOURCES = $(foreach d, $(C_DIRS), $(foreach c, $(wildcard $(d)/*.c), $(c)))
CFLAGS = $(C_DEFS) $(C_INCLUDES) $(FLAGS) -std=c99 -fdata-sections -ffunction-sections

OBJS = $(C_SOURCES:.c=.o) $(ASM_SOURCES:.s=.o)
LD_FLAGS = $(FLAGS) -Wl,--gc-sections -T$(LD_SCRIPT) -nostdlib -lc -lm -L $(ARM_LIB_DIR)/lib

ifeq ($(DEBUG), 1)
C_DEFS += -DDEBUG
CFLAGS += -g -gdwarf-2
else
CFLAGS += $(OPT)
LD_FLAGS += $(OPT)
endif

$(TARGET).bin: $(TARGET).elf
	$(BIN) $< $@

$(TARGET).elf: $(OBJS)
	$(CC) $(LD_FLAGS) $(OBJS) -o $@
	$(SZ) $@

%.o: %.c
	$(CC) -c $(CFLAGS) $< -o $@

%.o: %.s
	$(AS) -c $(FLAGS) $< -o $@

.PHONY: clean flash debug

clean:
	$(RM) $(foreach d, $(C_DIRS), $(d)/*.o) *.elf *.bin

flash: $(TARGET).bin
	$(OPENOCD) -c "program $(TARGET).bin preverify verify reset exit 0x08000000"

debug: $(TARGET).elf
	$(GDB) -iex "target extended | $(OPENOCD) -c 'gdb_port pipe'" \
	-iex 'monitor reset halt' -ex 'break main' -ex 'c' -ex '-' $<

这个的写法是与GNU最大的不同点,他的组合比较固定,没有GNU那么灵活

--target=thumbv7m-none-unknown-eabi -mfpu=none
--target=thumbv7m-unknown-none-eabihf -mfpu=fpv5-d16

如果是在CMakeLists中需要额外指定库文件位置,否则还是会提示问题

# Link directories setup
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE
    # Add user defined library search paths
    "I:/llvm/LLVM-ET-Arm-19.1.5-Windows-x86_64/lib/clang-runtimes/arm-none-eabi/armv7m_soft_nofp/include"
)

# Add linked libraries
target_link_libraries(${CMAKE_PROJECT_NAME}
    # Add user defined libraries
    -l:libc.a    
    -l:libm.a      
)

常见问题

类似这种通不过cmake的test程序,这种都是库没定义好,缺少必要的库,导致连cmake内部的检测程序都无法正常调用编译器完成编译,进而提出错误

LLVM-ET-Arm-19.1.5-Windows-x86_64/bin/clang.exe"
is not able to compile a simple test program.

网上一般有两种解决方案,一种是直接修改对应报错的cmake的测试程序,一种是传递一个宏,让这个检测程序不运行,建议都别用。还是正常解决库路径的问题,否则后面还会有更多问题的

经典问题_exit没有定义,这个是GNU里,可以使用--specs=nosys.specs直接靠编译器帮我们实现一个替代,我们就不用写了

ld.lld: error: undefined symbol: _exit
>>> referenced by signal.c:151 ...
>>> referenced by abort.c:63 ...

但是似乎LLVM里没有这个东西,那就得手动实现一个

#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

extern "C" {
    // 系统调用实现
    void _exit(int status) { 
        while(1) {
            // 一个断点,方便调试
            __asm__ volatile("bkpt #0");
        }
    }
    
    int _close(int file) { return -1; }
    int _fstat(int file, struct stat *st) { 
        st->st_mode = S_IFCHR;
        return 0; 
    }
    int _isatty(int file) { return 1; }
    int _lseek(int file, int ptr, int dir) { return 0; }
    int _read(int file, char *ptr, int len) { return 0; }
    int _write(int file, char *ptr, int len) { return len; }
    
    // C++ 运行时支持
    void __cxa_pure_virtual() { while (1); }
    int __cxa_atexit(void (*destructor) (void *), void *arg, void *dso) { return 0; }
    void __cxa_finalize(void *f) {}
    void *__dso_handle = nullptr;
}

大致类似这样的一个文件即可,如果没有c++就去掉c++部分,其他的适配c

Summary

目前GNU和LLVM都可以正常编译完成整个工程了

对比两种工程编译时间:

LLVM Build completed: 00:04:27.449

GNU Build completed: 00:02:19.536

不知道哪里有问题,导致LLVM实际反而比GNU要慢,甚至慢了一倍

Quote

https://hustlei.github.io/2018/11/msys2-pacman.html

https://github.com/ARM-software/LLVM-embedded-toolchain-for-Arm/blob/main/docs/migrating.md

https://github.com/astronmax/stm32-llvm

https://www.eet-china.com/mp/a103863.html