Project Walkthrough
This walkthrough takes you from an empty directory to a blinking LED and a
live serial log on a real STM32 Nucleo-F401RE board. We use the
nav CLI to scaffold the
project — it pulls NavHAL in as a dependency, generates a
cross-compile-ready CMake build, and gives you a starter main.c that's
already calling the HAL.
If you don't have the board, the file layout, build flow, and code are
identical for any Cortex-M4 STM32 target — just change the board line in
nav.toml and the CONFIG_BOARD_* flags in .config.
Prerequisites
You need a Linux host (the auto-installer in nav update currently only
targets Debian-family distros) and an STM32 Nucleo-F401RE plugged in over
USB. The toolchain itself is:
cmake≥ 3.20gitarm-none-eabi-gcc(the cross-compiler)stlink-tools(providesst-flash)python3
You don't have to install these by hand — nav check will tell you
what's missing and nav update will pull the rest in via apt. We'll
get to that in a moment.
Step 1 — Install the nav CLI
Grab the latest .deb from the
nav releases page and
install it:
sudo dpkg -i nav-X.X.X-Linux.deb
Or build from source:
git clone https://github.com/ragnar-vallhala/nav.git
cd nav
mkdir build && cd build
cmake .. && make -j$(nproc)
sudo cp nav /usr/local/bin/
Sanity-check the install:
nav --help
You should see the eleven supported verbs: create, build, upload,
monitor, clean, check, update, add, search, login, publish.
Step 2 — Audit the host with nav check
Before scaffolding anything, ask nav what your host is missing:
nav check
Output looks something like this:
✔ Build Orchestrator -> Detected.
✔ VCS Driver -> Detected.
✔ Host Pkg Manager (APT) -> Detected.
Items marked CRITICAL will block the build; optional items only block
specific commands (e.g. st-flash is optional until you actually try to
upload). If anything's missing, run:
nav update
nav update walks the missing list and runs
sudo apt install -y … for each tool it knows how to map (cmake,
git, gcc-arm-none-eabi, stlink-tools, python3). On non-Debian
hosts it bows out gracefully — you'll need to install manually.
Step 3 — Scaffold the project with nav create
Pick a name and let nav do the rest:
nav create my-first-flight
cd my-first-flight
What just happened:
- A folder tree was created —
extern/,include/,src/,lib/,tests/. - Three configuration files dropped in at the root:
nav.toml— the project descriptor (name,version,target,build)..config— Kconfig-style flags consumed by NavHAL itself (which drivers to compile in, which board, which toolchain).CMakeLists.txt— the cross-compile root build, wired to usearm-none-eabi-and includeextern/NavHALas a subdirectory.
- A starter
src/main.cwas written. - NavHAL was cloned into
extern/NavHALfromgithub.com/ragnar-vallhala/NavHAL(branchstable, depth 1). Thesamples/folder is purged immediately to keep the workspace tidy.
Open nav.toml — it's three short tables:
[project]
name = "my-first-flight"
version = "0.1.0"
[target]
arch = "cortex-m4"
vendor = "stm32"
board = "nucleo_f401re"
[build]
backend = "cmake"
Most of the heavy lifting is in .config. The defaults turn on a sensible
set of drivers: GPIO, UART, I²C, SPI, PWM, timers, DMA, the FPU, the CRC
unit, the DWT cycle counter, and SDIO. The flasher is st-flash, and the
flash address is the STM32 default 0x08000000.
If you want a leaner build, comment out the CONFIG_DRV_*=y lines for
peripherals you don't need — anything that's commented out won't be
compiled.
Step 4 — Read the starter main.c
#define CORTEX_M4
#include "navhal.h"
int main(void) {
systick_init(1000);
uart2_init(115200);
uart2_write("System Up.\r\n");
while (1) {
// Main logic loop
}
return 0;
}
Three things to know:
#define CORTEX_M4before the include selects the Cortex-M4 backend inside NavHAL. The header is otherwise architecture-agnostic.systick_init(1000)configures SysTick for a 1 ms tick (1000 Hz). Most HAL routines that count time — includingdelay_ms— depend on this.uart2_init(115200)sets up USART2 at 115 200 baud. On the Nucleo-F401RE, USART2 is wired through the ST-Link's virtual COM port, so it shows up on your host as/dev/ttyACM0over the same USB cable that powers the board.
Step 5 — Build with nav build
nav build
Under the hood this is just two CMake calls:
cmake -S . -B build
cmake --build build --parallel
The first call configures using the toolchain file pinned by NavHAL
(arm-none-eabi-gcc, -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16). The second compiles your main.c, links it against
NavHAL's whole-archive static lib, and drops my-first-flight (an ELF
binary) into build/.
If you ever want to start fresh:
nav clean # rm -rf build/
Step 6 — Flash with nav upload
Plug in the board if you haven't already, then:
nav upload
This runs cmake --build build --target flash, which objcopies the ELF
to a raw .bin and pipes it to st-flash write … 0x08000000. The board
auto-resets when the write completes.
Permission denied on /dev/ttyACM0? Either add yourself to the
dialout group (sudo usermod -aG dialout $USER and re-login) or
prepend sudo.
Step 7 — Watch it run with nav monitor
nav monitor --baud 115200
nav monitor scans /dev for the first ttyACM* or ttyUSB* device,
opens it raw via termios, and streams to stdout. You should see:
System Up.
…and then nothing — because the while (1) loop is empty. Press
Ctrl-C to detach cleanly.
If you have more than one serial device plugged in, pin the port explicitly:
nav monitor -p /dev/ttyACM0 -b 115200
Supported bauds are 9600, 19200, 38400, 57600, 115200 — anything else silently falls back to 9600.
Step 8 — Make the LED blink
The Nucleo-F401RE has an on-board user LED on PA5. Let's drive it from
the HAL. Replace src/main.c with:
#define CORTEX_M4
#include "navhal.h"
int main(void) {
systick_init(1000);
uart2_init(115200);
hal_gpio_setmode(GPIO_PA05, GPIO_OUTPUT, GPIO_PUPD_NONE);
uart2_write("Blink loop armed.\r\n");
while (1) {
hal_gpio_digitalwrite(GPIO_PA05, GPIO_HIGH);
delay_ms(250);
hal_gpio_digitalwrite(GPIO_PA05, GPIO_LOW);
delay_ms(250);
}
}
Three HAL calls do the work:
hal_gpio_setmode(GPIO_PA05, GPIO_OUTPUT, GPIO_PUPD_NONE)— putPA5in push-pull output mode with no pull resistor.hal_gpio_digitalwrite(pin, level)— drive the pin high or low.delay_ms(N)— a SysTick-backed busy-wait. This is not an RTOS delay; it blocks the CPU. For real applications you'd hand timing to VAIOS or a timer ISR.
Re-flash and re-attach:
nav build && nav upload
nav monitor -b 115200
You should see the LED blink at 2 Hz and Blink loop armed. appear once
on the serial output.
What you've built
You now have a complete edit → build → flash → observe loop. Concretely:
- A NavHAL project laid out the way the rest of the stack expects.
- A working cross-compile pipeline pinned to Cortex-M4 with hard-float.
- A board-specific
.configcontrolling which drivers ship in the binary. - Two HAL surfaces in use — GPIO and UART — plus the SysTick timer.
Where to go next
- Browse
extern/NavHAL/include/navhal.hto see the full set of public headers (hal_gpio,hal_uart,hal_timer,hal_pwm,hal_i2c,hal_spi,hal_dma,hal_crc, …). Each one is a thin, architecture- agnostic interface backed by a Cortex-M4 implementation. - Read the NavHAL chapter of the technical report for the design rationale behind the layering, the resource-management model, and the timing guarantees.
- The remaining tutorials in this guide (coming soon) will cover GPIO, UART RX/TX with DMA, I²C and SPI peripherals, and integration with the VAIOS scheduler.