From absolute zero to a blinking LED — reproducible, beginner‑friendly, and ready to extend.
This README shows every step to build and flash a minimal firmware for the NUCLEO‑F446RE on a Raspberry Pi 5 (Pi OS / Linux) using:
- arm-none-eabi-gcc (ARM GNU toolchain)
- CMake + Ninja (portable, fast builds)
- OpenOCD + ST‑LINK (SWD) (program/debug)
- STM32CubeF4 (HAL + CMSIS from ST)
- What You’ll Build
- Prerequisites
- Create Project Folder
- Bring In STM32CubeF4 (HAL/CMSIS)
- Add Required Support Files
- Write the Firmware Code
- CMake Toolchain + Project Files
- Configure & Build
- Flash (Upload) to the Board
- Debug Session (Optional but Recommended)
- VS Code IntelliSense (Optional, Nice to Have)
- Extend the Project (Add Peripherals)
- Use this skeleton (duplicate locally, no history)
- Troubleshooting
- Why This Approach (Industry Rationale)
A minimal blink firmware using the STM32 HAL:
- LED LD2 on the NUCLEO‑F446RE (pin PA5) toggles every 250 ms.
HAL_Delay()works because SysTick interrupt is wired viastm32f4xx_it.c.
You will:
- Set up CMake + Ninja for clean, out‑of‑source builds.
- Use STM32CubeF4 (HAL + CMSIS) as a Git submodule.
- Compile a
.elfand flash it to the MCU with OpenOCD. - Optionally debug with GDB (breakpoints, step, inspect).
Install these on your Raspberry Pi / Linux box:
sudo apt update
sudo apt install -y git cmake ninja-build openocd
# Install ARM GNU Toolchain (arm-none-eabi-*) appropriate for AArch64 (Pi 5).
# If already installed, ensure it's on PATH:
arm-none-eabi-gcc --versionHardware:
- NUCLEO‑F446RE board (USB connected to the Pi; on‑board ST‑LINK provides SWD).
Optional but handy:
- Visual Studio Code with C/C++ and CMake Tools extensions.
Pick a workspace and make the project root:
mkdir -p ~/dev && cd ~/dev
git init stm32f446re_skeleton_code && cd stm32f446re_skeleton_codeCreate the folders we’ll use:
mkdir -p cmake include src system startup linker third_party buildAdd ST’s HAL/CMSIS pack as a submodule so your repo pins the exact version:
git submodule add https://github.com/STMicroelectronics/STM32CubeF4.git third_party/STM32CubeF4
cd third_party/STM32CubeF4
git submodule update --init --recursive
cd ../..Why submodule?
- Reproducible builds: teammates clone with
--recursiveand get the same HAL version you used. - Easy, explicit upgrades later by bumping the submodule commit.
You need four kinds of files in your repo to build an STM32 HAL project:
- HAL config header (project‑local)
- Startup assembly (device‑specific)
- System source (CMSIS system init)
- Linker script (memory layout)
Run these from the project root:
# 1) HAL config: copy the template into your project and drop "_template"
cp third_party/STM32CubeF4/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_conf_template.h \
include/stm32f4xx_hal_conf.h
# 2) Startup assembly for STM32F446
cp third_party/STM32CubeF4/Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/gcc/startup_stm32f446xx.s \
startup/
# 3) System file (shared by F4 devices)
cp third_party/STM32CubeF4/Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/system_stm32f4xx.c \
system/
# 4) Linker script — choose an F446RE one from the Cube examples/templates you have
# Use the "Templates" one (ideal), or any F446RE example's .ld:
cp third_party/STM32CubeF4/Projects/STM32446E-Nucleo/Templates/STM32CubeIDE/STM32F446RETX_FLASH.ld \
linker/If paths differ in your Cube version, discover the linker script with:
find third_party/STM32CubeF4/Projects -type f -iregex '.*STM32F446RE.*FLASH\.ld'
Edit include/stm32f4xx_hal_conf.h as needed (enable modules you use):
#define HAL_MODULE_ENABLED
#define HAL_CORTEX_MODULE_ENABLED
#define HAL_RCC_MODULE_ENABLED
#define HAL_GPIO_MODULE_ENABLED
/* add more as you need later */Create src/main.c:
Create src/stm32f4xx_it.c:
Why src/stm32f4xx_it.c?
This file holds the interrupt service routines (ISRs) for the MCU. The startup file (startup_stm32f446xx.s) defines a vector table with weak default handlers for every IRQ. To make an interrupt actually do something, you override the weak symbol by providing a function with the exact same name in C
Create cmake/arm-gcc-toolchain.cmake:
Create CMakeLists.txt:
Why CMake?
You describe what to build; CMake generates the long arm-none-eabi-gcc -c & -o commands for each file, then Ninja compiles only what changed. Portable, fast, and CI‑friendly.
From the project root:
cmake -S . -B build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-gcc-toolchain.cmake \
-DCMAKE_BUILD_TYPE=Debug
ninja -C build-S .= source dir (hasCMakeLists.txt)-B build= out‑of‑source build dir (clean and disposable)-G Ninja= generate Ninja files (fast incremental builds)- Toolchain file = tells CMake which compiler/toolchain to use
Result: build/nucleo_f446re_blinky.elf
If you change toolchains or want a clean re‑configure:
rm -rf build cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-gcc-toolchain.cmake -DCMAKE_BUILD_TYPE=Debug ninja -C build
Connect the NUCLEO via USB. Two solid choices:
openocd -f board/st_nucleo_f4.cfg \
-c "program build/stm32f446re_skeleton_code.elf verify reset exit"This tells OpenOCD to erase/program/verify, reset the MCU, and exit. If successful, LD2 (PA5) blinks.
Terminal 1:
openocd -f board/st_nucleo_f4.cfgTerminal 2:
arm-none-eabi-gdb build/nucleo_f446re_blinky.elf
(gdb) target extended-remote :3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue- OpenOCD runs a GDB server on port
3333 monitorforwards commands to OpenOCDloadprograms flash via GDB
Logic recap: OpenOCD bridges your PC ↔ ST‑LINK ↔ MCU. The board/st_nucleo_f4.cfg file sets up ST‑LINK + SWD and the target (STM32F4).
- Set a breakpoint in
HAL_Delay()orSysTick_Handler()to see timebase behavior. - Telnet to OpenOCD (optional):
telnet localhost 4444→ commands likereset halt,reg,mdw.
Point IntelliSense to your compiler and compile_commands.json so squiggles disappear and autocompletion is accurate.
Open Command Palette → C/C++: Edit Configurations (UI):
- Compiler path:
/usr/bin/arm-none-eabi-gcc - Compile Commands:
${workspaceFolder}/build/compile_commands.json
Or JSON:
// .vscode/c_cpp_properties.json
{
"version": 4,
"configurations": [{
"name": "stm32",
"compilerPath": "/usr/bin/arm-none-eabi-gcc",
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
"intelliSenseMode": "gcc-arm"
}]
}When you add new modules (e.g., UART, SPI, I²C, ADC+DMA):
- Create source/header files in
src/(or a subfolder). - Update
CMakeLists.txt: add the new.cfile(s) toSRCSand any include paths. - Rebuild:
cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-gcc-toolchain.cmake -DCMAKE_BUILD_TYPE=Debug ninja -C build - Flash again (OpenOCD one‑liner or GDB).
You can also use
file(GLOB CONFIGURE_DEPENDS ...)to auto‑pick new files, but most teams prefer explicit lists for clarity and reproducibility.
This path gives you a clean repo that has the same files/structure as the skeleton, but none of its commit history. It works on GitHub, GitLab, or any Git server.
Summary of what you’ll do
- Copy files from the skeleton (without keeping its history)
- Start fresh Git history on your machine
- Connect your own remote and push
- Initialize any submodules this skeleton uses
# 0) (Recommended) Create an EMPTY repo on your Git host first
# Example: https://github.com/<you>/<my_new_proj>
# Leave it empty (no README/license/gitignore), or it's fine to overwrite later.
# 1) Copy the skeleton files to a new local folder
git clone https://github.com/ka5j/stm32f446re_skeleton_code.git my_new_proj
cd my_new_proj
# 2) Remove the skeleton's Git history and start fresh
rm -rf .git
# Initialize a new repo with 'main' as the default branch (Git ≥ 2.28)
git init -b main
# If your Git is older:
# git init
# git checkout -b main
# 3) Make the first commit in your NEW project
git add .
git commit -m "Start from skeleton (fresh history)"
# 4) Connect your own remote and push
git remote add origin https://github.com/<you>/<my_new_proj>.git
git push -u origin main
# 5) Pull submodule contents (if this skeleton uses any)
git submodule sync --recursive
git submodule update --init --recursiverm -rf .gitwipes the old history so your project starts clean.git init -b maincreates a new repo with main as the default branch (works on Git ≥ 2.28).git remote add origin …ties your local repo to your new server‑side repo.git submodule update --init --recursiveensures all submodules (including nested ones) are fetched at the versions recorded by this skeleton.
- Need to change the remote URL later?
git remote set-url origin https://github.com/<you>/<my_new_proj>.git
- Forgot to create the remote first? You can create it after Step 3, then run Step 4.
- Prefer history mirroring (to copy all commits/branches)? Use your host’s “mirror/duplicate repository” instructions instead—this snippet is specifically for no history.
LED turns on but doesn’t blink
- Likely missing
SysTick_Handler→ ensuresrc/stm32f4xx_it.cexists and is added toSRCS. - Ensure
include/stm32f4xx_hal_conf.hexists (copied from_template) and your include path containsinclude/.
VS Code shows __IO, uint8_t etc. as undefined
- Configure IntelliSense (see section above). The build is correct; the editor just needs the same flags/includes your build uses.
OpenOCD USB permission error
- Install udev rules (see Prerequisites), reload udev, unplug/re‑plug board.
Toolchain warning about CMAKE_TOOLCHAIN_FILE
- That happens if you reconfigure an existing
build/with a different toolchain file. Deletebuild/and re‑run the CMake command.
Submodule folder appears empty
- This happens if submodules weren’t initialized.
Fix:
git submodule sync --recursive
git submodule update --init --recursive- If it’s still empty, confirm it’s a submodule (gitlink) not a normal folder:
git ls-files --stage | grep 160000 || echo "no gitlinks found"- If no gitlink, re-add:
rm -rf third_party/STM32CubeF4
git submodule add https://github.com/STMicroelectronics/STM32CubeF4.git third_party/STM32CubeF4
git submodule update --init --recursive- CMake + Ninja: portable, fast, and the standard way to orchestrate GCC in many embedded teams; works great in CI.
- Git submodule for STM32CubeF4: pins the exact HAL/CMSIS version; reproducible for collaborators.
- OpenOCD + GDB: de‑facto open‑toolchain flow for STM32 + ST‑LINK/SWD; consistent across boards.
- Out‑of‑source builds: keeps your repo clean; a “clean build” is as simple as deleting
build/.