Programming · 55 pages
- C language — 15 lessonstutorial
- Lesson 1 — Hello, C
- Lesson 2 — Types & literals
- Lesson 3 — Expressions & operators
- Lesson 4 — Control flow
- Lesson 5 — Functions
- Lesson 6 — Arrays
- Lesson 7 — Pointers
- Lesson 8 — Strings
- Lesson 9 — Structs & unions
- Lesson 10 — Dynamic memory
- Lesson 11 — Preprocessor
- Lesson 12 — Multi-file projects
- Lesson 13 — Standard library
- Lesson 14 — Undefined behavior
- Lesson 15 — Modern C (C11/17/23)
- C++ programming — 15 lessonstutorial
- Lesson 1 — Hello, C++
- Lesson 2 — References & namespaces
- Lesson 3 — Classes & RAII
- Lesson 4 — Inheritance & polymorphism
- Lesson 5 — Templates
- Lesson 6 — STL containers
- Lesson 7 — Iterators & algorithms
- Lesson 8 — Smart pointers
- Lesson 9 — Move semantics
- Lesson 10 — Lambdas
- Lesson 11 — Error handling
- Lesson 12 — Concurrency
- Lesson 13 — Modules (C++20)
- Lesson 14 — Modern C++23
- Lesson 15 — C++ on Cortex-M
- Bare-metal C tutorial — 10 lessonsembedded
- Lesson 1 — Toolchain setup
- Lesson 2 — Bare-metal blink
- Lesson 3 — Registers in C
- Lesson 4 — Memory map
- Lesson 5 — Linker script
- Lesson 6 — Interrupts & ISRs
- Lesson 7 — Bit patterns
- Lesson 8 — Fixed-point math
- Lesson 9 — State machines
- Lesson 10 — Ring buffers + DMA
- Embedded C patterns
- volatile & memory-mapped IO
- CMSIS-Core
- CMSIS-DSP
- Linker scripts
- Startup & vector table
- C++ on Cortex-M
- Rust on Cortex-Mmodern
- Inline assembly
- Bare-metal libc & newlib-nano
- Bootloaders & DFU
- OTA update flow
Startup & vector table
Writing the code that runs before main — vector table layout, Reset_Handler, .data/.bss init, C++ static constructors, and what newlib needs.
When the processor comes out of reset, none of your C code has run. The stack pointer is uninitialised. The C runtime doesn't exist. The .bss is full of random bits. Every Cortex-M startup file is the bridge between "raw silicon" and "main() is about to be called" — typically 100–300 lines that you'll write once and rarely touch again.
The vector table
The first two words of the vector table are special:
- Word 0 — initial Main Stack Pointer.
- Word 1 — Reset_Handler address.
The hardware reads these at power-on, sets MSP from word 0, and branches to word 1.
__attribute__((section(".isr_vector"), used))
void (* const vector_table[])(void) = {
(void(*)(void))(&_estack), // 0: MSP
Reset_Handler, // 1: reset
NMI_Handler, // 2: NMI
HardFault_Handler, // 3: HardFault
MemManage_Handler, // 4
BusFault_Handler, // 5
UsageFault_Handler, // 6
0, 0, 0, 0, // 7-10 reserved
SVC_Handler, // 11
DebugMon_Handler, // 12
0, // 13 reserved
PendSV_Handler, // 14
SysTick_Handler, // 15
// External IRQs — vendor-specific from here
WWDG_IRQHandler, // 16
PVD_IRQHandler, // 17
// ... usually ~80–240 more
};The vector table must be placed at the start of flash (0x08000000 on STM32, etc.), pinned by the linker script's .isr_vector section. The KEEP(*(.isr_vector)) in the linker script prevents the linker's --gc-sections from discarding it.
Reset_Handler
extern uint32_t _sidata; // .data load address (in flash)
extern uint32_t _sdata, _edata; // .data destination range (in RAM)
extern uint32_t _sbss, _ebss; // .bss range (in RAM)
extern void SystemInit(void);
extern void __libc_init_array(void);
extern int main(void);
__attribute__((naked, noreturn))
void Reset_Handler(void) {
// 1. Copy .data from flash to RAM
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) *dst++ = *src++;
// 2. Zero .bss
dst = &_sbss;
while (dst < &_ebss) *dst++ = 0;
// 3. Vendor-specific clock setup (PLL, flash wait states, voltage scaling)
SystemInit();
// 4. C++ static constructors (.preinit_array, .init_array)
__libc_init_array();
// 5. Hand off to main
main();
// 6. main() should never return, but if it does:
while (1) {}
}__attribute__((naked)) is only needed if you're writing this in assembly within a C function — for a pure C handler that does what's above, you can drop it. The Cortex-M exception entry already saves caller-saved registers; Reset_Handler is called as if it were an exception, so it has the same calling convention as a C function.
What SystemInit does
The default CMSIS SystemInit for most STM32 chips:
- Enables the FPU (if present) — clears
CPACR.CP10/CP11to permit FPU access. - Configures
VTORto the start of flash (in case the bootloader left it elsewhere). - Doesn't typically configure clocks — that's left to your
main()or a separateSystemClock_Config().
void SystemInit(void) {
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2)); // full access to CP10, CP11
#endif
SCB->VTOR = (uint32_t)&vector_table;
}Some vendor versions also bring up the watchdog, configure the cache (on M7), or set up the MPU.
C++ static constructors
If you compile C++ code with global objects, those objects need their constructors run before main(). The linker collects all the constructor function pointers into .init_array, and __libc_init_array walks the array calling each:
extern void (*__preinit_array_start[])(void);
extern void (*__preinit_array_end[])(void);
extern void (*__init_array_start[])(void);
extern void (*__init_array_end[])(void);
void __libc_init_array(void) {
for (void (**f)(void) = __preinit_array_start; f < __preinit_array_end; f++) (*f)();
for (void (**f)(void) = __init_array_start; f < __init_array_end; f++) (*f)();
}newlib ships this. If you're not using C++ and want a smaller binary, you can replace it with an empty stub.
The linker script must provide the symbols:
.preinit_array :
{
PROVIDE_HIDDEN(__preinit_array_start = .);
KEEP(*(.preinit_array*))
PROVIDE_HIDDEN(__preinit_array_end = .);
} > FLASH
.init_array :
{
PROVIDE_HIDDEN(__init_array_start = .);
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array*))
PROVIDE_HIDDEN(__init_array_end = .);
} > FLASHnewlib syscalls
If you link against newlib (the standard libc for embedded), it expects certain syscall stubs:
_write(fd, buf, len)— used byprintf. Forward to UART._read(fd, buf, len)— used bygetchar. Block until UART byte._sbrk(incr)— used bymallocto grow the heap. Return current heap top, increment byincr._exit(code)— used byexit(). Halt._kill,_getpid,_close,_lseek,_fstat,_isatty— stub implementations returning errors.
A typical _sbrk:
extern char _end[]; /* defined by linker script — end of .bss */
extern char _heap_end[]; /* also from linker script */
static char *heap = _end;
void *_sbrk(int incr) {
char *prev = heap;
if (heap + incr > _heap_end) {
errno = ENOMEM;
return (void *) -1;
}
heap += incr;
return prev;
}If you avoid malloc, you can stub _sbrk to return -1 and save a few hundred bytes.
Customising for your application
The startup file is the place to:
- Disable the watchdog if your bootloader enabled it (some vendors do).
- Set the cache configuration on M7.
- Configure the MPU before any code that depends on protection runs.
- Relocate the vector table if you're chained from a bootloader.
void Reset_Handler(void) {
// Run from a bootloader? Relocate vector table.
SCB->VTOR = (uint32_t)&vector_table;
__DSB(); __ISB();
// ... rest of init ...
main();
}See also
- Linker scripts — paired companion.
- Exception model — what the vector table actually does.
- CMSIS —
SystemInitand friends.