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,SECTIONSthe 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 NOLOADa 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
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.
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
- The chain: preprocessor → compiler → assembler → linker →
objcopy; the environment calls the same programs. - The script:
ENTRY,MEMORY,SECTIONS;.data—> RAM AT > FLASHandLOADADDR;.bss—NOLOAD;KEEPfor the vectors;ASSERTfor the space. - Registers at fixed addresses only through
volatile, otherwise a flag wait reads the flag once. - Startup code in C: copy
.data, zero.bss, callmain; the vectors are an array in the.isr_vectorsection, the handlers are weak aliases. - The example: flash memory 480 bytes (476 + 4), RAM 20 bytes (4 + 16),
app.bin480 bytes. .data: VMA0x20000000, LMA0x080001dc; the first words of the image are0x20020000and0x08000045(the Thumb bit).- Without
KEEPandENTRYthe image is empty without a single error; without room in RAM theASSERTstops the build.
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.