STM32 bare metal with CMSIS only
Contract
| Field | Bound contract |
|---|---|
| Trigger | STM32 firmware is started without the Cube HAL, vendor examples are ported to register-level code, a clock or flash-placement problem is being debugged, or a reproducible arm-none-eabi-gcc build layout is needed. |
| Authority | Reversible local: writes only the new project files under the directory the user names (startup file, system file, linker script, main.c, Makefile); rollback is deleting that directory or reverting it in version control. No remote mutation. |
| Side effect | New source, linker, and build files in the project directory. No existing file outside it is modified. |
| Done | The project builds with arm-none-eabi-gcc, SystemInit brings the system clock to the target frequency with bus prescalers and flash wait states inside the reference manual limits, a peripheral clock enable follows the read-back rule, and the flash and RAM map matches the part. |
Inputs
- Exact part (for example STM32F407VG) and its reference manual and datasheet.
- Oscillator: HSE crystal frequency, or HSI only.
- Target system clock and supply voltage range (they set the flash wait states).
- Project directory to create files in.
- Whether a bootloader will launch the application at an offset.
Procedure
Lay out the project. CMSIS-Core (
core_cm4.h) and the device header come from the ST CMSIS device pack or its open-source mirror; the startup file followsbaremetal-startup. Done when: the six files exist and the device header matches the exact part.stm32-bare/ startup_stm32f407xx.s vector table and Reset_Handler system_stm32f4xx.c SystemInit(), SystemCoreClockUpdate() stm32f407xx.h CMSIS device header core_cm4.h CMSIS-Core linker.ld FLASH and RAM regions, section placement main.c MakefileConfigure the clock tree in
SystemInitinside the reference manual limits. For STM32F407 at 168 MHz from an 8 MHz HSE: PLLM = 8, PLLN = 336, PLLP = 2 (00), PLLQ = 7 (48 MHz for USB). APB1 is limited to 42 MHz and APB2 to 84 MHz, so AHB runs undivided, APB1 uses/4, and APB2 uses/2. Set the voltage scale inPWR->CRbefore turning the PLL on, and set flash wait states before switchingSW. Done when:SystemCoreClockUpdatereports 168 MHz and both APB clocks are at or below their limits.void SystemInit(void) { RCC->CR |= RCC_CR_HSEON; while (!(RCC->CR & RCC_CR_HSERDY)) { } RCC->APB1ENR |= RCC_APB1ENR_PWREN; PWR->CR |= PWR_CR_VOS; /* scale 1 for 168 MHz on F407 */ RCC->PLLCFGR = RCC_PLLCFGR_PLLSRC_HSE | (8U << RCC_PLLCFGR_PLLM_Pos) | (336U << RCC_PLLCFGR_PLLN_Pos) | (0U << RCC_PLLCFGR_PLLP_Pos) /* /2 */ | (7U << RCC_PLLCFGR_PLLQ_Pos); RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV4 | RCC_CFGR_PPRE2_DIV2; RCC->CR |= RCC_CR_PLLON; while (!(RCC->CR & RCC_CR_PLLRDY)) { } FLASH->ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_PRFTEN | FLASH_ACR_LATENCY_5WS; RCC->CFGR |= RCC_CFGR_SW_PLL; while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) { } SystemCoreClockUpdate(); }Wait states come from the reference manual table of HCLK against supply voltage: 5 wait states covers 150 to 168 MHz at 2.7 to 3.6 V on STM32F4. Other parts and voltages have their own rows.
Enable a peripheral clock and read the enable register back before the first access. The STM32F4 errata sheet documents a delay after enabling a peripheral clock; the read-back provides it. Done when: every clock enable in the project is followed by the read-back.
static inline void gpioa_clock_enable(void) { RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; (void)RCC->AHB1ENR; /* errata: delay after RCC peripheral clock enable */ }Write the build flags. Cortex-M4F takes
-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard; section flags plus--gc-sectionsdrop unused code;nano.specsselects newlib-nano andnosys.specssupplies stub syscalls. Done when:makeproduces an ELF, andarm-none-eabi-sizeshows text and data within the flash and RAM sizes.MCU = -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard CFLAGS = $(MCU) -std=c23 -Wall -Wextra -ffunction-sections -fdata-sections -g3 -O2 LDFLAGS = $(MCU) -T linker.ld -Wl,--gc-sections --specs=nano.specs --specs=nosys.specsWrite the memory map from the datasheet. Done when:
MEMORYinlinker.ldlists the regions below with the sizes for the exact part.Region Address Notes Flash 0x08000000Vector table at the start; the boot alias at 0x00000000maps hereSRAM1 0x20000000Stack, heap, .data,.bss, DMA buffersCCM 0x10000000STM32F405, F407, F427, F429 only: 64 KB core-coupled, not reachable by DMA An application launched by a bootloader links its flash origin at the application base (for example
0x08010000) and writesSCB->VTORto that base in its startup; seebootloaders-embedded.Choose where to test. QEMU's STM32 machines model STM32F100, F205, F405, and L475 parts and implement USART, SPI, ADC, EXTI, SYSCFG, timers, and RCC reset and enable only; GPIO, DMA, I2C, PWR, RTC, flash interface, and USB are missing (QEMU system documentation, STM32 boards page). Use QEMU for CPU-side and USART logic; validate clocks, GPIO, DMA, and I2C on hardware. Done when: each feature under test is assigned to QEMU or to hardware according to that list.
Build, flash, and confirm: measure the system clock through
MCOon a pin or infer it from a UART baud, and read a known register back. Done when: the measured clock matches the configured one and the UART prints at the configured baud.
Failure and recovery
| Symptom | Cause | Fix |
|---|---|---|
Hang in SystemInit |
HSE crystal absent, or PLL factors out of range | Bring up on HSI first; check the crystal and the PLL factor ranges in the manual. |
| Peripheral dead | Clock gate off, or no read-back after enable | Set the enable bit and read the register back. |
| HardFault at boot | Stack outside RAM, or VTOR wrong for an offset image |
Check _estack and the application VTOR write. |
| Wrong UART baud | SystemCoreClock stale after the PLL switch |
Call SystemCoreClockUpdate() at the end of SystemInit. |
| DMA from a CCM buffer fails | CCM is not on the DMA bus path | Place DMA buffers in SRAM1. |
| Peripheral misbehaves at speed | APB clock above its limit | Use PPRE1_DIV4 and PPRE2_DIV2 at 168 MHz. |
Output
A buildable CMSIS-only project in the named directory: startup file, SystemInit with the clock tree inside the manual's limits, linker script with the part's memory map, main.c, and Makefile, plus a note on which features to validate under QEMU and which on hardware.