Собираем проект для STM32 с помощью Clang/LLVM

Небольшое описание процесса сборки проекта для микроконтроллера STM32 с помощью clang/llvm.

Cover Image

Казалось бы зачем использовать Clang/LLVM для проектов ориентированных на микроконтроллеры, лучше ли он GCC?

Сразу скажу, здесь это не рассматривается, мы будем учиться использовать, а не разбираться зачем это и почему. Раз уж вы это читаете, значит у вас есть свои причины.

Первое с чего стоит начать это то, что у clang параметры командной строки почти аналогичны gcc. Почти - это большинство параметров, исключение лишь указание архитектуры для которой будет генерироваться бинарник, но это скорее уже к бэкенду, нежели к самомму clang.

Ну и сам Си код совместим c GCC.

Компилируем сишный исходник в объектный файл

Типичная строка для компиляции кода с помощью GCC под Cortex-M0 будет выглядеть следующим образом:

arm-none-eabi-gcc.exe -c -std=c2x -mcpu=cortex-m0 -mthumb $(C_DEFS) $(C_INCLUDES) -O2 -Wall -fdata-sections -ffunction-sections -Wno-implicit-fallthrough -fshort-enums file.c -o file.o

Папки с заголовками и предопределенные макросы специально нес тал указывать, и ... да-да, использую язык будущего стандарта для Си - -std=c2x, об изменениях можно почитать подробнее здесь: https://en.cppreference.com/w/c/language/history

А теперь посмотрим как это будет выглядеть для clang

clang.exe -c -std=c2x --target=thumbv6m-none-none-eabi $(C_DEFS) $(C_INCLUDES)  -Ic:/gcc-arm/arm-none-eabi/include -Ic:/gcc-arm/lib/gcc/arm-none-eabi/9.2.1/include -O2 -Wall -fdata-sections -ffunction-sections -Wno-implicit-fallthrough -fshort-enums file.c -o file.o

Вот тут уже побольше, давайте разбираться, что же у нас тут добавилось:

  • --target=thumbv6m-none-none-eabi

    Указание цели для которой будет генерироваться код, нужно выбрать из следующих вариантов:

    • thumbv6m-none-none-eabi для ARM Cortex-M0 и Cortex-M0+;
    • thumbv7m-none-none-eabi для ARM Cortex-M3;
    • thumbv7em-none-none-eabi для ARM Cortex-M4 и Cortex-M7 (без FPU);
    • thumbv7em-none-none-eabihf для ARM Cortex-M4F и Cortex-M7F (с FPU).
  • -Ic:/gcc-arm/arm-none-eabi/include и -Ic:/gcc-arm/lib/gcc/arm-none-eabi/9.2.1/include

    А это пути (в вашем случае они могут, и скорее всего будут другие) к заголовочным файлам и файлам библиотек из пакета GCC. Да его тоже нужно будет установить. Это, по крайней мере до тех пор, пока не появятся соответсвующие файлы в самом LLVM.

В принципе ничего сложного, добавились несколько параметров, с этим можно жить.

Компилируем ассемблерный исходник в объектный файл

Как это выглядит в gcc:

arm-none-eabi-gcc.exe -c -x assembler-with-cpp -mcpu=cortex-m0 -mthumb $(ASM_DEFS) $(ASM_INCLUDES) -O2 -Wall -fdata-sections -ffunction-sections file.s -o file.o

А теперь как это будет выглядеть в clang - а никак, ну по крайней мере я не нашел как это сделать, он ругается на каждую инструкцию, каждую запятую и всё остальное в ассемблерном файле. Вроде должен быть компилятор для ассемблера llvm-as.exe, но не в моем установочном пакете, собирать из исходников не стал, не думаю, что это будет интересно.

Так что будем жить без ассемблерных исходников. Можно, конечно, обернуть все ассемблерные функции в сишные и спокойно пользоваться встроеным ассемблером - здесь всё отлично работает.

Что делать с "startup.s" файлом

Ответ ожидаем, переписать его на Си, и выглядеть он будет примерно так:

#define NULL    (void*)0

extern void * _estack;
extern void *_sidata, *_sdata, *_edata, *_sbss, *_ebss;

void Reset_Handler();
void NMI_Handler();
void HardFault_Handler();
void SysTick_Handler();
/* Тут прототипы для остальных векторов
* ...
*/

extern void SystemInit ();
extern int main();

void * vectors[] __attribute__((section(".isr_vector"), used)) =
{
    &_estack
,   &Reset_Handler
,   &NMI_Handler
,   &HardFault_Handler
,   NULL
    /* тут остальные векторы прерываний
    *  ...
    * */
};

void Reset_Handler()
{
    // копирование и инициализация данных в оперативке
    void **pSource, **pDest;
    for (pSource = &_sidata, pDest = &_sdata; pDest != &_edata; pSource++, pDest++) *pDest = *pSource;
    for (pDest = &_sbss; pDest != &_ebss; pDest++) *pDest = 0;
    // полезные функции
    SystemInit ();
    main();
}

void __attribute__((weak))  NMI_Handler()       { for(;;){}; }
void __attribute__((weak))  HardFault_Handler() { for(;;){}; }
void __attribute__((weak))  SysTick_Handler()   {             }
/* Тут остальные функции обработки прерываний, помеченные как "weak"
* ...
*/

Всё что написано выше - это немного переделанный ассемблерный код, при большом желании можно сделать конвертер с помощью питона или ещё чего-нибудь.

Линковка нескольких объектных файлов

Компилировать мы уже научились, теперь необходимо слинковать несколько файлов и получить заветный HEX, ну точнее сначала ELF файл, ну а затем уже HEX.

Как выглядит линковка с использованием GCC:

arm-none-eabi-gcc.exe $(CFLAGS) file1.o file2.o -specs=nano.specs -TSTM32F042K6Tx_FLASH.ld -lc -lm -lnosys -Wl,-Map=target.map -Wl,--gc-sections -nostdlib --output target.elf

Ничего примечательного, указываются объектные файлы, скрипт для линкера, используемые библиотеки, путь для MAP файла (очень полезная штука) и имя готового ELF файла.

Теперь посмотрим, что же у нас происходит при использовании clang:

clang.exe $(CFLAGS) file1.o file2.o -TSTM32F042K6Tx_FLASH.ld -lc -lm -lgcc -Lc:/gcc-arm/lib/gcc/arm-none-eabi/9.2.1/thumb/v6-m/nofp -Lc:/gcc-arm/arm-none-eabi/lib/thumb/v6-m/nofp -Wl,-Map=target.map -Wl,--gc-sections -nostdlib --output target.elf

Почти всё осталось без изменений:

  • -specs=nano.specs удалено;
  • -Lc:/gcc-arm/lib/gcc/arm-none-eabi/9.2.1/thumb/v6-m/nofp и -Lc:/gcc-arm/arm-none-eabi/lib/thumb/v6-m/nofp, - это пути до библиотек, их обязательно нужно указывать, так как сам LLVM ничего о них не знает. С этими путями не всё так просто, они индивидуальны под каждую архитектуру.

В принципе всё.

Что изменилось

  • Время сборки проекта уменьшилось раза в два;
  • Можно делать так: enum NAME : uint8_t { VAL1, VAL2, VAL3 };, то есть указывать какому типу будет соответсвовать перечисление, в gcc это делается не так просто.
  • Можно использовать [[nodiscard]]
  • Можно использовать [[fallthrough]]
  • Можно использовать [[clang::optnone]]
  • ...

По сути я и не знаю чем один лучше другого, и даже не хочу сравнивать, мне просто нравиться clang.

Ссылки для скачивания всего-всего