Embedded HandbookApps
All sections › I. Basics
STM32F103, STM32F407 · level basic · 22 min read
Open in the interactive handbook

4в. Building without an environment: GCC, a Makefile, startup code and your own linker script

A development environment is convenient but not mandatory. Firmware for an STM32 can be written in any editor and built with one command in a terminal. For this four files are needed: the program, the startup code, the linker script and a Makefile. This section shows all four from scratch — without HAL, without the C library and without a single line of assembly. The main thing here is the *.ld linker script: how to write it yourself and how to check that it did what was intended.

Everything is checked by building: arm-none-eabi-gcc 12.2, GNU Make 4.3, a run in QEMU 7.2 on the netduinoplus2 board (STM32F405 — the core and memory as on the F407), debugging with GDB (_verify/bare4v/). For the F103 only the memory sizes in the script and the core option change — more on this in 4в.9.

Terms used in the documentation

Term Meaning
toolchain the set of tools: compiler, assembler, linker, utilities
linker script where each memory is and what goes where
MEMORY, SECTIONS the description of memory regions and the section layout
output section, input section a section of the image and the sections from object files
VMA (virtual memory address) the address at which a section runs
LMA (load memory address) the address at which a section is stored in the image
location counter (.) the current address inside a section
KEEP, --gc-sections "do not throw away" and the removal of unused sections
NOLOAD a section with no contents in the image
vector table, reset handler the table of exception addresses, the code run after reset
weak alias a name that can be overridden
map file the memory map after linking
freestanding a program without an operating system and without the full C library

4в.1 What happens between the source and the firmware

Building without an environment: gcc turns main.c and startup.c into object files, ld lays out the sections according to linker.ld — vectors and code into flash memory, .data runs in RAM but is stored in flash memory, .bss has no bytes in the image, the stack is at the top of RAM

One arm-none-eabi-gcc command actually runs a chain of programs:

Step Program From what Into what
preprocessor cpp (inside gcc) .c with #include and #define plain C
compiler cc1 C assembly
assembler as assembly an object file .o
linker ld (via collect2) all .o + the .ld script app.elf
conversion objcopy app.elf app.bin or app.hex for flashing

app.elf is the full image with addresses, symbols and debug data: the debugger loads it. app.bin is only the bytes of flash memory in a row: the programmer writes it. The environment calls the same programs and does nothing more.


4в.2 A program without HAL

The program prints a line to USART1 and checks that the startup code did its job: a variable with an initial value must receive it, and an array without a value must turn out to be zeros:

static uint32_t counter = 0x12345678;                  /* .data */
uint32_t zeros[4];                                     /* .bss; не static — иначе компилятор подставит 0 и секция опустеет */
static const char greeting[] = "bare metal: hello\n";  /* .rodata */

The registers — straight by address, as in section 8:

#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR   (*(volatile uint32_t *)0x40011000)
#define USART1_DR   (*(volatile uint32_t *)0x40011004)
#define USART1_BRR  (*(volatile uint32_t *)0x40011008)
#define USART1_CR1  (*(volatile uint32_t *)0x4001100C)
    RCC_APB2ENR |= 1u << 4;                            /* такт USART1 */
    USART1_BRR = 0x008B;                               /* 115200 бод при 16 МГц: 16 000 000 / 115 200 = 138,9 */
    USART1_CR1 = (1u << 13) | (1u << 3);               /* UE — USART включён, TE — передатчик */

The emulator does not need pins, but on a board USART1 is silent without PA9 configured. Before it the port is enabled: the GPIOA clock is bit 0 in RCC_AHB1ENR (0x40023830); in GPIOA_MODER (0x40020000) for PA9 bits 19:18 = 10 — alternate function; in GPIOA_AFRH (0x40020024) bits 7:4 = 7 — AF7, which is USART1_TX (section 8).

Every register is declared with volatile (section 3.4). Without it the wait for the TXE flag at -Os reads the register once: arm-none-eabi-gcc 12.2 turned the loop into ldr r1, [r2], lsls r1, r1, #24, bmi and a branch to itself. It is enough to read TXE = 0 — and the program hangs forever, and on a board the output breaks off after the first characters. QEMU does not show this: its transmitter is always free, and a run without volatile passes too.

zeros without static is not an accident. With static the compiler at -Os sees the whole file, understands that nobody writes to the array, and substitutes zero for the read: the .bss section turns out empty, and there is nothing to check. For a global variable other files may write to it, and the compiler is obliged to read memory.


4в.3 A linker script from scratch

The script answers three questions: what memory there is, what to put where, and where the startup code takes the initial values from. Below it comes in four pieces in a row — together they are the whole linker.ld, with all the braces. First the memory and the entry point:

ENTRY(Reset_Handler)                     /* с чего начинается программа — для отладчика и ELF */

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

_estack     = ORIGIN(RAM) + LENGTH(RAM); /* вершина стека: стек растёт вниз от конца ОЗУ */
_stack_size = 0x800;                     /* сколько ОЗУ оставить под стек — проверяется в ASSERT */

MEMORY sets the regions: the name, the permissions (r — read, w — write, x — execute), the start and the size. The suffix K means kilobytes. _estack is an ordinary script symbol: it has an address but no memory, and the program sees it as extern.

Next come the sections. The vector table must be the first thing in flash memory: after reset the core reads the initial SP and the address of Reset_Handler from the first two words at address 0x00000000. With BOOT0 = 0 the Flash from 0x08000000 is mapped there (section 1.4), so the table is placed at its start:

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))                 /* таблица векторов — первой, с адреса 0x08000000 */
  } > FLASH

  .text :
  {
    *(.text .text.*)                     /* код */
    *(.rodata .rodata.*)                 /* константы и строки */
    . = ALIGN(4);
    _etext = .;
  } > FLASH

SECTIONS { opens here and closes in the last piece, after .bss.

*(.text .text.*) means "the .text and .text.anything sections from all object files". Names like .text.main come from the -ffunction-sections option: each function is in its own section, and the linker can throw away the unused ones (--gc-sections). KEEP forbids throwing away the vector table: not a single function refers to it. The dot . is the current address; . = ALIGN(4) aligns it to a word, _etext = . remembers the end of the code.

The most important part is .data. A variable with an initial value has two addresses: it runs in RAM, but the value is stored in flash memory until power is applied:

  .data :
  {
    _sdata = .;                          /* где .data работает — адрес в ОЗУ (VMA) */
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH                     /* а хранится во флеш-памяти сразу за кодом (LMA) */
  _sidata = LOADADDR(.data);             /* откуда стартовый код копирует начальные значения */

> RAM is the run address (VMA), AT > FLASH the storage address (LMA). LOADADDR(.data) gives the second. .bss need not be stored — it is all zeros:

  .bss (NOLOAD) :
  {
    _sbss = .;                           /* нули: во флеш-памяти не хранятся вовсе */
    *(.bss .bss.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM

  ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}

NOLOAD means the section has addresses but no bytes in the image. The line *(.bss .bss.*) is mandatory: without it variables without an initial value go into orphan sections, and the startup code will not zero them — the same trap as with .data in 4в.10. ASSERT stops the build if the data and the stack do not fit into RAM: the error is seen at link time, not when the board hangs.


4в.4 Startup code in C

The startup file does not have to be in assembly. The Cortex-M itself loads SP from the vector table, so Reset_Handler can be an ordinary C function:

#include <stdint.h>

extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;   /* символы из linker.ld */
int main(void);

void Reset_Handler(void)
{
    uint32_t *src = &_sidata;
    uint32_t *dst = &_sdata;
    while (dst < &_edata)                /* .data: начальные значения из флеш-памяти в ОЗУ */
        *dst++ = *src++;
    for (dst = &_sbss; dst < &_ebss;)    /* .bss: обнулить */
        *dst++ = 0;
    main();
    for (;;) {
    }
}

Before these two loops not a single global variable has the correct value. That is why Reset_Handler reads no global variables and takes the script symbols only by address — &_sdata.

There is nowhere to return to from Reset_Handler: after reset LR holds 0xFFFFFFFF. If main returns, the core will jump to this address — into a region from which instructions are not executed — and end up in a HardFault. In the experiment without the endless loop that is exactly what happened: HFSR = 0x40000000, CFSR = 0x00000001 — an instruction fetch violation escalated to a HardFault, the address in the frame is 0xFFFFFFFE. That is why there is an endless loop after the call.

Handlers that the program has not written are weak aliases of one trap function. Define a SysTick_Handler function in the program, and the linker will take it rather than the alias:

void Default_Handler(void)
{
    for (;;) {                           /* сюда попадает любое прерывание без своего обработчика */
    }
}

void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));

The vector table is an array of pointers in its own section:

typedef void (*vector_t)(void);

__attribute__((section(".isr_vector"), used))
const vector_t vectors[16] = {
    [0] = (vector_t)&_estack,            /* начальное значение SP — не функция, а адрес */
    [1] = Reset_Handler,
    [3] = HardFault_Handler,
    [15] = SysTick_Handler,
};

The three listings in a row are already a complete startup file: it builds and runs with the program from 4в.2. Here there are 16 core vectors, four of them filled; in startup.c there are ten: SP, Reset_Handler and eight core exceptions. A real F407 has 82 more peripheral vectors after them — ST's startup file has 98 of them (section 4б).


4в.5 The Makefile

CC      = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE    = arm-none-eabi-size

CFLAGS  = -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
          -Os -g3 -Wall -Wextra -ffreestanding \
          -ffunction-sections -fdata-sections
LDFLAGS = -T linker.ld -nostdlib -Wl,--gc-sections -Wl,-Map=build/app.map -Wl,--print-memory-usage

build/app.bin: build/app.elf
    $(OBJCOPY) -O binary $< $@

build/app.elf: startup.c main.c linker.ld | build
    $(CC) $(CFLAGS) startup.c main.c $(LDFLAGS) -o $@
    $(SIZE) $@

build:
    mkdir -p build

clean:
    rm -rf build

.PHONY: clean
Option What for
-mcpu=cortex-m4 -mthumb the core and the Thumb instruction set — the Cortex-M has no other
-mfpu=fpv4-sp-d16 -mfloat-abi=hard the F4 FPU and passing floats in its registers
-ffreestanding no OS: the compiler does not treat main and the library as special
-ffunction-sections -fdata-sections each function and variable in its own section
-T linker.ld your own linker script
-nostdlib without the C library and without the compiler's startup files
-Wl,--gc-sections throw away unused sections
-Wl,-Map=…, --print-memory-usage the memory map and the region fill line

linker.ld is in the target's dependencies: editing the script rebuilds the image. | build is an order-only dependency: the directory is created before the build, but its date does not rebuild the image. make clean deletes everything built. Commands in a Makefile start with a tab character, not spaces — in the listing there is exactly a tab before each command. Spaces instead of it are a common mistake when typing in a simple editor.


4в.6 Build and check: the map, the sections, the image

Memory region         Used Size  Region Size  %age Used
FLASH:         480 B         1 MB      0.05%
RAM:          20 B       128 KB      0.02%
text       data     bss     dec     hex filename
476       4      16     496     1f0 build/app.elf

Flash memory — 480 bytes: 476 of code with constants and 4 bytes of the initial value of counter. RAM — 20 bytes: 4 bytes of .data and 16 bytes of .bss. The sections with their run (VMA) and storage (LMA) addresses — objdump -h:

.isr_vector 00000040 08000000 08000000
.text 0000019c 08000040 08000040
.data 00000004 20000000 080001dc
.bss 00000010 20000004 080001e0

.data has different addresses: it runs from 0x20000000 and is stored at 0x080001dc — right after the code. The script symbols agree:

08000044 T Reset_Handler
080001dc A _sidata
20000000 D _sdata
20000004 D _edata
20000014 B _ebss
20020000 R _estack

The vector table is 0x40 = 64 bytes: 16 words. The first two words of the image:

000000 20020000 08000045

The initial SP is 0x20020000, the end of RAM. The address of Reset_Handler is 0x08000045, although the function lies at 0x08000044: the lowest bit 1 means Thumb. The linker sets it itself, because the table holds a pointer to a function. Write the address as the number 0x08000044, and the core will fall into a HardFault on the first instruction.

The ELF program headers show what exactly will get into memory:

Entry point 0x8000045
LOAD 0x001000 0x08000000 0x08000000 0x001dc 0x001dc R E 0x1000
LOAD 0x002000 0x20000000 0x080001dc 0x00004 0x00014 RW 0x1000

The second segment is RAM: 4 bytes in the file (.data) and 0x14 = 20 bytes in memory (.data + .bss). The difference, 16 bytes of zeros, is filled by the startup code. app.bin is 480 bytes, exactly the flash memory size from the report.

The sections of app.elf: what is stored in Flash and what the startup code copies and zeroes in RAM


4в.7 Running and debugging without an environment

Running in the emulator and the program output:

qemu-system-arm -M netduinoplus2 -nographic -kernel build/app.elf -serial stdio
bare metal: hello
counter (.data) = 0x12345678
zeros (.bss) = 0x00000000
&counter = 0x20000000, greeting = 0x080001c6
done

The startup code copied 0x12345678 and zeroed the array. The variable runs in RAM, the string stayed in flash memory.

Debugging: the emulator with the options -S -gdb tcp::1234 waits for a debugger, and gdb connects with the command target remote :1234. With a board it is the same, only instead of QEMU it is openocd or st-util, and the port is theirs:

Reset_Handler () at startup.c:11
sp 0x20020000 0x20020000
pc 0x8000044 0x8000044 <Reset_Handler>
0x8000000 <vectors>:    0x20020000  0x08000045  0x08000041  0x08000041
Breakpoint 1, main () at main.c:38
0x20000000 <counter>:   0x12345678
$1 = {0x0, 0x0, 0x0, 0x0}
sp 0x2001fff8 0x2001fff8

Before the first instruction the core itself took SP and PC from the table. On entering main counter already holds its value, zeros holds zeros, and the stack has gone down by 8 bytes. Vectors 2 and 3 are 0x08000041: NMI_Handler and HardFault_Handler are one and the same Default_Handler.


4в.8 Two errors in the script: one stays silent, one shouts

Too little memory — it shouts. The same project with a script where RAM is 1 KB:

RAM too small for .data + .bss + stack
collect2: error: ld returned 1 exit status

The ASSERT fired: 20 bytes of data and 0x800 bytes of stack do not fit into a kilobyte. Without this line the build would have passed, and the program would have overwritten its variables with the stack.

No KEEP and ENTRY — it stays silent. Remove ENTRY(Reset_Handler) and the KEEP on the vector table from the script — and build with --gc-sections:

без KEEP и ENTRY: text 0

Not a single error and an empty image. The linker looks for what can be reached by references from the entry point. There is no entry point, nobody refers to the vector table — and it threw everything away. A 0-byte firmware will be flashed onto the board without objections. That is why every script has both lines, and after the build the memory report is looked at.


4в.9 The F103 and other chips

For the F103C8 (BluePill) more changes than it seems: the core options -mcpu=cortex-m3 -mthumb without an FPU, the sizes in MEMORY (FLASH 64K, RAM 20K), the register addresses (USART1 — 0x40013800, RCC_APB2ENR — 0x40021018, the USART1 clock — bit 14, port A — bit 2), the baud rate divider (HSI on the F1 is 8 MHz: 8 000 000 / 115 200 = 69.4, BRR = 0x45) and the configuration of PA9: in GPIOA_CRH (0x40010804) bits 7:4 = 1011 — an alternate function push-pull output at 50 MHz. If you need the C library, printf and malloc, instead of -nostdlib you add --specs=nano.specs and write _sbrk and _write yourself — this is how sysmem.c and syscalls.c are organised in a CubeIDE project (section 4б). Then the script gets .init_array sections for constructors and an end symbol for the heap.


4в.10 Pitfalls

Symptom Cause
the firmware is flashed, the board is silent, the image is 0 bytes there is no KEEP(*(.isr_vector)) or ENTRY, while --gc-sections is on
a HardFault on the first instruction the table holds an address without the lowest bit: 0x08000044 instead of a pointer to a function
a global variable without its initial value .data without AT > FLASH, or the startup code does not copy it
garbage in .bss the startup code does not zero from _sbss to _ebss
undefined reference to memcpy or __aeabi_… -nostdlib removed both the library and libgcc; add -lgcc or your own functions
variables get corrupted in deep calls the stack ran into the data; there is no ASSERT with a margin for the stack
make: *** missing separator spaces instead of a tab in a Makefile command
in QEMU everything prints, but on a board the output breaks off after the first characters the registers are declared without volatile: the TXE wait read the flag once and spins forever
editing the .ld does not rebuild the image the script is not listed in the target's dependencies
.bss is empty although the array is declared the compiler replaced a static array that nobody writes to with zero
a variable with an initial value reads as zero or garbage the script has *(.data) without .data.*, while the build used -fdata-sections: the variable went into an orphan section
a HardFault on the first float operation, CFSR = 0x00080000 the FPU is not enabled in CPACR: the -mfpu options do not switch the block on

The last two rows — by a run in QEMU. A script where only *(.data) was taken into .data built with -fdata-sections without a single warning. The variable counter = 0x12345678 went into a separate section .data.counter; the linker placed it in RAM at address 0x20000000, but it did not become part of .data — _sdata and _edata coincided, and the startup code copied nothing. The program printed counter = 0x00000000: in QEMU RAM is zeroed, on a board there will be whatever remained after power-up. The linker option --orphan-handling=warn shows such sections, but among a dozen service ones.

A program with a float without the write to CPACR printed "before float" and crashed: CFSR = 0x00080000 — the NOCP bit, the coprocessor is not available, HFSR = 0x40000000 — the fault was escalated to a HardFault. With the write CPACR |= 0xF << 20 and the DSB/ISB barriers before the first float it got to "after float: 3". The options -mfpu=fpv4-sp-d16 -mfloat-abi=hard only allow the compiler to use FPU instructions; the block is enabled by the startup code — in CMSIS this is done by SystemInit.


4в.11 Summary

Full sources

linker.ld — a linker script from scratch for the STM32F405/F407.

/* Раздел 4в: скрипт компоновщика с нуля для STM32F405/F407 — 1 МБ флеш-памяти, 128 КБ ОЗУ.
 * Каждая строка отвечает на один вопрос: где лежит, где работает, откуда берутся начальные значения. */
ENTRY(Reset_Handler)                     /* с чего начинается программа — для отладчика и ELF */

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

_estack     = ORIGIN(RAM) + LENGTH(RAM); /* вершина стека: стек растёт вниз от конца ОЗУ */
_stack_size = 0x800;                     /* сколько ОЗУ оставить под стек — проверяется в ASSERT */

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))                 /* таблица векторов — первой, с адреса 0x08000000 */
  } > FLASH

  .text :
  {
    *(.text .text.*)                     /* код */
    *(.rodata .rodata.*)                 /* константы и строки */
    . = ALIGN(4);
    _etext = .;
  } > FLASH

  .data :
  {
    _sdata = .;                          /* где .data работает — адрес в ОЗУ (VMA) */
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH                     /* а хранится во флеш-памяти сразу за кодом (LMA) */
  _sidata = LOADADDR(.data);             /* откуда стартовый код копирует начальные значения */

  .bss (NOLOAD) :
  {
    _sbss = .;                           /* нули: во флеш-памяти не хранятся вовсе */
    *(.bss .bss.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM

  ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}

startup.c — startup code in C: the vector table and Reset_Handler.

/* Раздел 4в: стартовый код на C — таблица векторов и Reset_Handler без единой строки ассемблера. */
#include <stdint.h>

extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;   /* символы из linker.ld */
int main(void);

void Reset_Handler(void)
{
    uint32_t *src = &_sidata;
    uint32_t *dst = &_sdata;
    while (dst < &_edata)                /* .data: начальные значения из флеш-памяти в ОЗУ */
        *dst++ = *src++;
    for (dst = &_sbss; dst < &_ebss;)    /* .bss: обнулить */
        *dst++ = 0;
    main();
    for (;;) {
    }
}

void Default_Handler(void)
{
    for (;;) {                           /* сюда попадает любое прерывание без своего обработчика */
    }
}

void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void) __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));

typedef void (*vector_t)(void);

__attribute__((section(".isr_vector"), used))
const vector_t vectors[16] = {
    [0] = (vector_t)&_estack,            /* начальное значение SP — не функция, а адрес */
    [1] = Reset_Handler,
    [2] = NMI_Handler,
    [3] = HardFault_Handler,
    [4] = MemManage_Handler,
    [5] = BusFault_Handler,
    [6] = UsageFault_Handler,
    [11] = SVC_Handler,
    [14] = PendSV_Handler,
    [15] = SysTick_Handler,
};

main.c — a program without HAL: printing to USART1 and checking .data and .bss.

/* Раздел 4в: программа без HAL и без библиотеки C — печать в USART1 и проверка,
 * что стартовый код скопировал .data и обнулил .bss. */
#include <stdint.h>

#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR   (*(volatile uint32_t *)0x40011000)
#define USART1_DR   (*(volatile uint32_t *)0x40011004)
#define USART1_BRR  (*(volatile uint32_t *)0x40011008)
#define USART1_CR1  (*(volatile uint32_t *)0x4001100C)

static uint32_t counter = 0x12345678;                  /* .data */
uint32_t zeros[4];                                     /* .bss; не static — иначе компилятор подставит 0 и секция опустеет */
static const char greeting[] = "bare metal: hello\n";  /* .rodata */

static void putc1(char c)
{
    while (!(USART1_SR & (1u << 7))) {                 /* TXE: передатчик свободен */
    }
    USART1_DR = (uint32_t)c;
}

static void puts1(const char *s)
{
    while (*s)
        putc1(*s++);
}

static void puthex(uint32_t v)
{
    static const char hex[] = "0123456789abcdef";
    puts1("0x");
    for (int i = 28; i >= 0; i -= 4)
        putc1(hex[(v >> i) & 0xF]);
}

int main(void)
{
    RCC_APB2ENR |= 1u << 4;                            /* такт USART1 */
    USART1_BRR = 0x008B;                               /* 115200 бод при 16 МГц: 16 000 000 / 115 200 = 138,9 */
    USART1_CR1 = (1u << 13) | (1u << 3);               /* UE — USART включён, TE — передатчик */
    puts1(greeting);
    puts1("counter (.data) = ");
    puthex(counter);
    puts1("\nzeros (.bss) = ");
    puthex(zeros[0] | zeros[1] | zeros[2] | zeros[3]);
    puts1("\n&counter = ");
    puthex((uint32_t)&counter);
    puts1(", greeting = ");
    puthex((uint32_t)greeting);
    puts1("\ndone\n");
    for (;;) {
    }
}

data_main.c — the experiment from "Pitfalls": a variable in the orphan section .data.counter.

/* Подраздел 4в.10: переменная с начальным значением и -fdata-sections попадает в секцию
 * .data.counter. Скрипт с «*(.data)» её не берёт — она становится сиротой. */
#include <stdint.h>

#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR   (*(volatile uint32_t *)0x40011000)
#define USART1_DR   (*(volatile uint32_t *)0x40011004)
#define USART1_BRR  (*(volatile uint32_t *)0x40011008)
#define USART1_CR1  (*(volatile uint32_t *)0x4001100C)

uint32_t counter = 0x12345678;                  /* .data.counter при -fdata-sections */

static void putc1(char c)
{
    while (!(USART1_SR & (1u << 7))) {
    }
    USART1_DR = (uint32_t)c;
}

static void puthex(uint32_t v)
{
    static const char hex[] = "0123456789abcdef";
    putc1('0');
    putc1('x');
    for (int i = 28; i >= 0; i -= 4)
        putc1(hex[(v >> i) & 0xF]);
}

int main(void)
{
    RCC_APB2ENR |= 1u << 4;
    USART1_BRR = 0x008B;
    USART1_CR1 = (1u << 13) | (1u << 3);
    const char *s = "counter = ";
    while (*s)
        putc1(*s++);
    puthex(counter);
    putc1('\n');
    for (;;) {
    }
}

fpu_main.c — the experiment from "Pitfalls": a float without enabling the FPU in CPACR.

/* Подраздел 4в.10: FPU включён ключами компилятора, но не разрешён в CPACR.
 * Собирается со стартовым кодом и скриптом раздела 4в; с -DENABLE_FPU перед первым float
 * пишет CPACR. HardFault_Handler здесь свой: печатает CFSR и HFSR и останавливается. */
#include <stdint.h>

#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844)
#define USART1_SR   (*(volatile uint32_t *)0x40011000)
#define USART1_DR   (*(volatile uint32_t *)0x40011004)
#define USART1_BRR  (*(volatile uint32_t *)0x40011008)
#define USART1_CR1  (*(volatile uint32_t *)0x4001100C)
#define SCB_CPACR   (*(volatile uint32_t *)0xE000ED88)
#define SCB_CFSR    (*(volatile uint32_t *)0xE000ED28)
#define SCB_HFSR    (*(volatile uint32_t *)0xE000ED2C)

static void putc1(char c)
{
    while (!(USART1_SR & (1u << 7))) {
    }
    USART1_DR = (uint32_t)c;
}

static void puts1(const char *s)
{
    while (*s)
        putc1(*s++);
}

static void puthex(uint32_t v)
{
    static const char hex[] = "0123456789abcdef";
    puts1("0x");
    for (int i = 28; i >= 0; i -= 4)
        putc1(hex[(v >> i) & 0xF]);
}

void HardFault_Handler(void)
{
    puts1("HardFault CFSR=");
    puthex(SCB_CFSR);
    puts1(" HFSR=");
    puthex(SCB_HFSR);
    puts1("\n");
    for (;;) {
    }
}

volatile float k = 1.5f;

int main(void)
{
    RCC_APB2ENR |= 1u << 4;
    USART1_BRR = 0x008B;
    USART1_CR1 = (1u << 13) | (1u << 3);
#ifdef ENABLE_FPU
    SCB_CPACR |= 0xFu << 20;                     /* CP10 и CP11 — полный доступ */
    __asm volatile("dsb\n isb");
#endif
    puts1("before float\n");
    volatile float x = k * 2.0f;                 /* первая команда FPU */
    puts1("after float: ");
    putc1((char)('0' + (int)x));
    puts1("\n");
    for (;;) {
    }
}

Next: section 5 "ST's three levels of abstraction" — CMSIS, HAL and LL.

Open in the interactive handbook