First, -specs=XYZ.specs asks the linker to look at the file XYZ.specs for additional linker library info. For example, here are some of the lines in the nano.specs file

%rename link nano_link %rename link_gcc_c_sequence nano_link_gcc_c_sequence ... *cpp_unique_options: -isystem =/include/newlib-nano %(nano_cpp_unique_options)

*nano_libc: -lc_nano ...

Basically an internal language to describe actions and what files to include. The content of the specs file would depend on a number of factors including compiler, MCU, etc.

For nano.specs, the main takeaway is that it includes the nanolib.

Moving on to nanolib vs. newlib. The short of it is that newlib is the libc implementation for embedded systems (vs. "regular" glibc for big machines like Linux), and nanolib is a further optimized library for embedded system based on microcontrollers. Nanolib even comes with "small" printf by default to further reduce the output program size. For a bit more background, I have written a blog post here. https://imagecraft.com/blog/2019/04/embedded-gcc-libraries-newlib-vs-nanolib/

To fully answer all your questions (CMSIS, how does GCC compile and link...), it would require a lot more than a simple answer that can fit in here. I would recommend looking up "GNU Embedded ARM" toolchain, and start with its documentation. The basic operations are simple: the compiler compile your files into object files (.o) and the linker combines them with libc into a working program. The major difficulty in building programs for an ARM MCU is which vendor libraries to use to access the peripherals. CMSIS is a way to solve the "generic" ARM MCU requirements, but most programs require much more than that.

STM32 is particular good that ST provides many libraries and even a GUI tool (CubeMX), but is also exceptionally bad that there are no less than 3 major versions (Standard Peripheral Library or SPL, HAL, and Low Level LL) of the libraries, and according to some people, are all broken in many ways.

Answer from Richard at ImageCraft on Stack Overflow
🌐
Klein Embedded
kleinembedded.com › home › stm32 without cubeide (part 1): the bare necessities
STM32 without CubeIDE (Part 1): The bare necessities - Klein Embedded
October 24, 2025 - The STM32 microcontrollers are built around the Arm Cortex-M processor. To convert our code – whether it be C, C++ or assembly – to executable code that the processor understands, we are going to need the Arm GNU Toolchain. This toolchain contains, among other things, a cross-compiler for C, namely arm-none-eabi-gcc...
🌐
Stack Overflow
stackoverflow.com › questions › 70131712 › how-to-quickly-determine-arm-none-eabi-gcc-flags-for-a-stm32-board
How to quickly determine arm-none-eabi-gcc flags for a stm32 board? - Stack Overflow
STLINK=~/stlink.git # Put your source files here (or *.c, etc) SRCS=main.c system_stm32f4xx.c # Binaries will be generated with this name (.elf, .bin, .hex, etc) PROJ_NAME=blinky # Put your STM32F4 library code directory here STM_COMMON=../STM32F4-Discovery_FW_V1.1.0 # Normally you shouldn't need to change anything below this line! ####################################################################################### CC=arm-none-eabi-gcc OBJCOPY=arm-none-eabi-objcopy CFLAGS = -g -O2 -Wall -Tstm32_flash.ld CFLAGS += -mlittle-endian -mthumb -mcpu=cortex-m4 -mthumb-interwork CFLAGS += -mfloat-abi=hard -mfpu=fpv4-sp-d16 CFLAGS += -I.
🌐
GitHub
github.com › stm32duino › arm-none-eabi-gcc › blob › main › README.md
arm-none-eabi-gcc/README.md at main · stm32duino/arm-none-eabi-gcc
The GNU Arm Embedded Toolchain binaries used by STM32duino cores - stm32duino/arm-none-eabi-gcc
Author   stm32duino
🌐
GitHub
github.com › stm32duino › arm-none-eabi-gcc
GitHub - stm32duino/arm-none-eabi-gcc: The GNU Arm Embedded Toolchain binaries used by STM32duino cores
The GNU Arm Embedded Toolchain binaries used by STM32duino cores - stm32duino/arm-none-eabi-gcc
Starred by 10 users
Forked by 5 users
Languages   Makefile 66.1% | Shell 33.9% | Makefile 66.1% | Shell 33.9%
🌐
STMicroelectronics Community
community.st.com › t5 › stm32cubeide-mcus › toolchain-differences-stm32-arm-none-eabi-versus-arm-arm-none › td-p › 122711
Toolchain differences: STM32 arm-none-eabi versus ... - STMicroelectronics Community
August 23, 2022 - Hi I am curious to know about the differences in arm-none-eabi-*** provided by ARM and the same arm-none-eabi-*** that comes with STM32CubeIDE. What/where are the differences? In particular, I am curious about the libc.a and the libc_nano.a I performed a CRC check on those files and compared ...
Top answer
1 of 1
1

First, -specs=XYZ.specs asks the linker to look at the file XYZ.specs for additional linker library info. For example, here are some of the lines in the nano.specs file

%rename link nano_link %rename link_gcc_c_sequence nano_link_gcc_c_sequence ... *cpp_unique_options: -isystem =/include/newlib-nano %(nano_cpp_unique_options)

*nano_libc: -lc_nano ...

Basically an internal language to describe actions and what files to include. The content of the specs file would depend on a number of factors including compiler, MCU, etc.

For nano.specs, the main takeaway is that it includes the nanolib.

Moving on to nanolib vs. newlib. The short of it is that newlib is the libc implementation for embedded systems (vs. "regular" glibc for big machines like Linux), and nanolib is a further optimized library for embedded system based on microcontrollers. Nanolib even comes with "small" printf by default to further reduce the output program size. For a bit more background, I have written a blog post here. https://imagecraft.com/blog/2019/04/embedded-gcc-libraries-newlib-vs-nanolib/

To fully answer all your questions (CMSIS, how does GCC compile and link...), it would require a lot more than a simple answer that can fit in here. I would recommend looking up "GNU Embedded ARM" toolchain, and start with its documentation. The basic operations are simple: the compiler compile your files into object files (.o) and the linker combines them with libc into a working program. The major difficulty in building programs for an ARM MCU is which vendor libraries to use to access the peripherals. CMSIS is a way to solve the "generic" ARM MCU requirements, but most programs require much more than that.

STM32 is particular good that ST provides many libraries and even a GUI tool (CubeMX), but is also exceptionally bad that there are no less than 3 major versions (Standard Peripheral Library or SPL, HAL, and Low Level LL) of the libraries, and according to some people, are all broken in many ways.

🌐
Jeremyherbert
jeremyherbert.net › get › stm32f4_getting_started
Getting Started with the STM32F4 and GCC - jeremyherbert.net
arm-none-eabi-gcc (Linaro GCC 4.6-2011.10) 4.6.2 20111004 (prerelease) Copyright (C) 2011 Free Software Foundation, Inc. This is free software; see the source for copying conditions.
Find elsewhere
🌐
GitHub
github.com › ryanbekabe › arm-none-eabi-gcc
GitHub - ryanbekabe/arm-none-eabi-gcc: The GNU Arm Embedded Toolchain binaries used by STM32duino cores
The GNU Arm Embedded Toolchain binaries used by STM32duino cores - ryanbekabe/arm-none-eabi-gcc
Author   ryanbekabe
🌐
Stack Exchange
electronics.stackexchange.com › questions › 651331 › how-to-successfully-build-for-arm-cortex-m0-using-arm-gcc-none-eabi
stm32 - How to successfully build for ARM Cortex-M0 using arm-gcc-none-eabi? - Electrical Engineering Stack Exchange
January 25, 2023 - -mthumb -mcpu=cortex-m0 -u Default_Handler --specs=rdimon.specs -L/usr/lib/gcc/arm-none-eabi/12.2.1/ -L/opt/arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi/arm-none-eabi/lib/thumb/v6-m/nofp/ -lc -Tstm32f030x6.ld arm-none-eabi-gcc -mthumb -mcpu=cortex-m0 -c -O0 -Wall -fmessage-length=0 3rd-src/STM32-libs/Drivers/CMSIS/Device/ST/STM32F0xx/Source/Templates/gcc/startup_stm32f030x6.s -o build/startup_stm32f030x6.o arm-none-eabi-gcc -mthumb -mcpu=cortex-m0 -c -O0 -Wall -fmessage-length=0 3rd-src/STM32-libs/Drivers/CMSIS/Device/ST/STM32F0xx/Source/Templates/gcc/startup_stm32f030x6.s -o build/test.o
🌐
GitHub
github.com › topics › arm-none-eabi-gcc
arm-none-eabi-gcc · GitHub Topics · GitHub
toolchain-arm-none-eabi-gcc is a CMake toolchain for cross compiling for Arm Cortex-M and Cortex-R microcontrollers using arm-none-eabi-gcc. ... music-player nim stm32 mbed discovery wav wave sampling sd-card stm32f4 stm32f0 pwm nim-lang 16bit ...
Top answer
1 of 2
3

Your main() terminates. What happens next depends on what follows the __main call in startup_stm32f100xb.s; possibly a forced reset, or an endless loop (until watchdog reset, if enables).

In embedded systems, it is not normal for main() to return.

The "Enable HSI" is perhaps unnecessary or even incorrect. The core clock is HSI by default. A clock must already have been established for the code to be executing at all, and enabling the HSI will either not have any effect, or disable a previously established clock. The system clocking will have been set up in system_stm32f10x.c, and if you have specific clocking requirements, you need to modify that to suit your board and/or application. Using the HSI directly allows 8 MHz operation, feeding it to the PLL supports up-to 64 MHz, and the chip will run at up to 72 MHz when the PLL is fed from an external 4-16 MHz oscillator (HSE).

If you are not establishing the clock correctly in system_stm32f10x.c (or leaving it running HSI-direct), then it is possible that the execution is stalled in SystemInit() in system_stm32f10x.c waiting for PLL lock, or if you have it really wrong is not running at all.

To be working on any bare-metal processor at the board-bring-up level, you should ideally have a hardware debugger (JTAG/SWI), so you can use your tool-chain's debugger to determine exactly what your code is doing and where it is falling over. You can at least determine whether it is even running as far as main(). There is a significant initialisation implemented by startup_stm32f100xb.s and system_stm32f10x.c before main() run. The latter normally requires modification to suit your target.

2 of 2
2

Here is a complete example derived from one that blinks pc13 (untested at pc8, but worked at pc13, can test it if you have issues)

flash.s

@.cpu cortex-m0
@.cpu cortex-m3
.thumb


.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang
.thumb_func
hang:   b .

.thumb_func
.globl PUT32
PUT32:
    str r1,[r0]
    bx lr

.thumb_func
.globl GET32
GET32:
    ldr r0,[r0]
    bx lr

.thumb_func
.globl dummy
dummy:
    bx lr

blinker01.c

void PUT32 ( unsigned int, unsigned int );
unsigned int GET32 ( unsigned int );
void dummy ( unsigned int );

#define GPIOCBASE 0x40011000
#define RCCBASE   0x40021000

int notmain ( void )
{
    unsigned int ra;

    ra=GET32(RCCBASE+0x18);
    ra|=1<<4; //enable port c
    PUT32(RCCBASE+0x18,ra);
    //config
    ra=GET32(GPIOCBASE+0x04);
    ra&=~(0xF<<0);   //PC8
    ra|= (0x1<<0);   //PC8
    PUT32(GPIOCBASE+0x04,ra);
    while(1)
    {
        PUT32(GPIOCBASE+0x10,1<<(8+0));
        for(ra=0;ra<200000;ra++) dummy(ra);
        PUT32(GPIOCBASE+0x10,1<<(8+16));
        for(ra=0;ra<200000;ra++) dummy(ra);
    }
    return(0);
}

flash.ld

MEMORY
{
    rom : ORIGIN = 0x08000000, LENGTH = 0x1000
    ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > rom
    .rodata : { *(.rodata*) } > rom
    .bss : { *(.bss*) } > ram
}

Makefile

ARMGNU = arm-none-eabi
#ARMGNU = arm-linux-gnueabi

AOPS = --warn --fatal-warnings -mcpu=cortex-m0
AOPS3 = --warn --fatal-warnings -mcpu=cortex-m3
COPS = -Wall -Werror -O2 -nostdlib -nostartfiles -ffreestanding -mcpu=cortex-m0 -march=armv6-m
COPS32 = -Wall -Werror -O2 -nostdlib -nostartfiles -ffreestanding -mcpu=cortex-m3 -march=armv7-m

all : blinker01.bin

clean:
    rm -f *.bin
    rm -f *.o
    rm -f *.elf
    rm -f *.list

flash.o : flash.s
    $(ARMGNU)-as $(AOPS) flash.s -o flash.o

blinker01.o : blinker01.c
    $(ARMGNU)-gcc $(COPS) -mthumb -c blinker01.c -o blinker01.o

blinker01.bin : flash.ld flash.o blinker01.o
    $(ARMGNU)-ld -o blinker01.elf -T flash.ld flash.o blinker01.o
    $(ARMGNU)-objdump -D blinker01.elf > blinker01.list
    $(ARMGNU)-objcopy blinker01.elf blinker01.bin -O binary

or

arm-none-eabi-as --warn --fatal-warnings -mcpu=cortex-m0 flash.s -o flash.o
arm-none-eabi-gcc -Wall -Werror -O2 -nostdlib -nostartfiles -ffreestanding -mcpu=cortex-m0 -march=armv6-m -mthumb -c blinker01.c -o blinker01.o
arm-none-eabi-ld -o blinker01.elf -T flash.ld flash.o blinker01.o
arm-none-eabi-objdump -D blinker01.elf > blinker01.list
arm-none-eabi-objcopy blinker01.elf blinker01.bin -O binary

You dont need to mess with the clock initially it comes out of reset using the internal oscillator. So unless you have your own firmware that sets it to something else you wont need to set it to HSI. A clock init would be needed to change it to use an external clock and/or to use the PLL.

GPIOC->CRH &= (uint32_t)(0x00000002);

This changes the register from 0x44444444 to 0x44444444 (sorry see edit below). which is another way of saying you didnt do anything. You want 0x44444442 for the 2mhz if you want to read-modify-write which is cleaner, but not necessary since we know the power on reset state you should do Like I did above read the value mask off the bits to zero then set the bits you want, the whole pattern. Or you can just shove the 0x44444442 or 0x44444441 or 0x44444443.

had you done an or equal then it would have made 0x44444446 which is an open drain output. So if there is a pull up then it would light the led perhaps, depends, but you would want to use open drain for sinking more than sourcing. Which end of the led is the gpio pin connected to do you need to source it or sink it to turn it on? You set the output to 1 to set the output high, which on open drain just means dont sink. Although for an input setting odr pulls it high for input pull up/down, but as an open drain output not sure if setting odr pulls it up. easier to try push-pull first and if you just want it on then BSRR makes it easy to set or reset an individual pin in the port rather than touching the whole port with odr.

Open Drain Mode: A “0” in the Output register activates the N-MOS while a “1” in the Output register leaves the port in Hi-Z. (the P-MOS is never activated)

So first step change your code to set the port to a push-pull output, and then if you want to really be from scratch you can replace the rest of the code you borrowed and own all of it. Dont have to use the put32/get32 assembly I use, I have many reasons and tons of experience but that is the beauty of bare metal, do your own thing, you can use the volatile pointer approach as well which is likely what the header files you linked do, and simplify the bootstrap. If you make sure that your entry point function never exits and you dont rely on .data nor .bss then you can reduce your bootstrap to

.thumb
.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word main
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
hang: b hang

Or define a C function for the other vectors or a dumping ground for the unused ones. Note that I didnt put all the vectors in the bootstrap, depends on the chip and core but could be hundreds of vectors (up to 128 or 256)

And match the amount of ram you have for the stack pointer initialization value (0x20001000 in the example above).

EDIT, actually, sorry 0x44444444&=0x00000002 = 0x00000000, which sets all the ports to analog inputs. you want it to be 0x44444441 or 0x44444442 or 0x44444443

🌐
Openstm32
openstm32.org › forumthread1112
OpenSTM32 Community Site | Program "arm-none-eabi-g++" not found in PATH
Program “arm-none-eabi-g++” not found in PATH testf7 Project Properties, C++ Preprocessor Include.../Providers, Ac6 SW4 STM32 MCU Built-in Compiler Settings options C/C++ Scanner Discovery Problem Program “arm-none-eabi-gcc” not found in PATH testf7 Project Properties, C++ Preprocessor ...
🌐
Stack Overflow
stackoverflow.com › questions › 64131523 › arm-none-eabi-gcc-not-found-on-windows-10-stm32ide
c - arm-none-eabi-gcc: not found on Windows 10 STM32IDE - Stack Overflow
arm-none-eabi-gcc is the name of the cross-compiler itself. You will need to install it. Unfortunately, I don't know the Windows 10 STM32IDE, or where you can obtain the compiler.
🌐
GitHub
github.com › ryankurte › stm32-base › blob › master › toolchain › arm-gcc.cmake
stm32-base/toolchain/arm-gcc.cmake at master · ryankurte/stm32-base
set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER arm-none-eabi-g++) · # Set other tools ·
Author   ryankurte
🌐
STMicroelectronics Community
community.st.com › t5 › other-tools-mcus › toolchains-for-stm32 › td-p › 273328
Toolchains for STM32 - STMicroelectronics Community
January 6, 2025 - There is a make.exe for Windows, and a arm-none-eabi-gcc toolchain. Don't know about 32-bit. cygwin is a *nix like enviroment for windows. Check their website for the supported tools. Haven't needed it for a decade. If you think of hardware debugging, you want more tools, maybe openOCD, gdb. It will give you a hard time. ... Configuring Peripherals and Code Generation in System Workbench for STM32 in STM32CubeIDE (MCUs) 2026-03-19
🌐
STMicroelectronics Community
community.st.com › t5 › stm32-mpus-products-and-hardware › recipe-for-gcc-arm-none-eabi-native-fails-download-checksum › td-p › 57611
Recipe for gcc-arm-none-eabi-native fails. Downloa... - STMicroelectronics Community
May 5, 2023 - NOTE: Resolving any missing task queue dependencies Build Configuration: BB_VERSION = "2.0.0" BUILD_SYS = "x86_64-linux" NATIVELSBSTRING = "ubuntu-22.04" TARGET_SYS = "arm-ostl-linux-gnueabi" MACHINE = "stm32mp1-nexio" DISTRO = "openstlinux-weston" DISTRO_VERSION = "4.0.4-snapshot-20230504" TUNE_FEATURES = "arm vfp cortexa7 neon vfpv4 thumb callconvention-hard" TARGET_FPU = "hard" DISTRO_CODENAME = "kirkstone" GCCVERSION = "11.%" PREFERRED_PROVIDER_virtual/kernel = "linux-stm32mp" meta-swupdate-nexio = "<unknown>:<unknown>" meta-swupdate = "kirkstone:fb01170b98cf9541d286ba5a890fb85d83949b91" m
🌐
GitHub
github.com › stm32duino › arm-none-eabi-gcc › releases
Releases · stm32duino/arm-none-eabi-gcc
The GNU Arm Embedded Toolchain binaries used by STM32duino cores - stm32duino/arm-none-eabi-gcc
Author   stm32duino