This guide walks through integrating wolfHAL into a bare-metal embedded project.
A typical project using wolfHAL looks like this:
my_project/
wolfHAL/ wolfHAL repository (submodule, copy, etc.)
boards/
<board_name>/
board.h Peripheral externs and board constants
board.c Device instances, configuration, and Board_Init
ivt.c Interrupt vector table and Reset_Handler
linker.ld Linker script for your MCU
board.mk Toolchain and source list
src/
main.c Application entry point
... Additional application sources
Makefile
The key idea is that your project provides the board-level glue (device instances, pin assignments, clock config, startup code) and wolfHAL provides the driver implementations and API.
wolfHAL is a source-level library with no external dependencies beyond a C
compiler and standard headers (stdint.h, stddef.h). To use it:
- Add the wolfHAL repository root to your include path (e.g.,
-I/path/to/wolfHAL) - Compile the generic dispatch sources for the modules you need:
src/gpio/gpio.c
src/uart/uart.c
src/flash/flash.c
...
- Compile the platform-specific driver sources for your target:
src/clock/<platform>_clock.c
src/gpio/<platform>_gpio.c
src/uart/<platform>_uart.c
src/uart/<platform>_flash.c
...
You only need to include the modules and platform drivers your project actually uses.
wolfHAL has three driver categories — platform, peripheral, and board-level
(see docs/writing_a_driver.md). Their device structs have slightly
different shapes.
Platform drivers operate directly on SoC registers. The device struct has three fields:
struct whal_Gpio {
const whal_Regmap regmap; /* base address and size */
const whal_GpioDriver *driver; /* vtable of function pointers */
const void *cfg; /* platform-specific configuration */
};- regmap — identifies the peripheral's memory-mapped register block
- driver — points to the driver implementation (the vtable)
- cfg — points to a driver-specific configuration struct that the driver reads during Init
Platform headers provide _REGMAP and _DRIVER macros for each peripheral,
so you only need to fill in the cfg:
#include <wolfHAL/platform/st/stm32wb55xx.h>
whal_Gpio g_whalGpio = {
.regmap = { WHAL_STM32WB55_GPIO_REGMAP },
.driver = WHAL_STM32WB55_GPIO_DRIVER,
.cfg = &gpioConfig,
};When direct API mapping is active for a device type, the .driver field is
omitted since calls go directly to the driver implementation.
Peripheral drivers talk to external chips over a bus (SPI, I2C, MDIO). The device struct shape mirrors platform drivers — same vtable + cfg pattern — but instead of a register block the device carries the bus handle and any chip-specific addressing inside its cfg:
whal_Flash g_whalSpiFlash = {
.driver = WHAL_SPI_NOR_W25Q64_FLASH_DRIVER,
.cfg = &(whal_SpiNor_W25q64_Cfg) {
.spi = &g_whalSpi, /* underlying bus driver */
.csPin = SPI_FLASH_CS_PIN,
/* ...chip-specific fields... */
},
};The application calls the same generic API (whal_Flash_Read(&g_whalSpiFlash, ...))
whether the flash is on-chip (platform driver) or external SPI NOR (peripheral
driver) — the device pointer determines which implementation runs.
Board-level drivers (clock, power) only expose chip-specific helpers — no
vtable, no generic whal_<Type>_* API. The device struct is just a regmap:
whal_Clock g_whalClock = {
.regmap = { WHAL_STM32WB55_RCC_REGMAP },
};There is no .driver or .cfg field to fill in. Boards bring up the clock
tree imperatively in Board_Init by calling chip-specific helpers like
whal_Stm32wb_Rcc_EnableOsc(), EnablePll(), SetSysClock(), etc., in the
required order. Applications that need to trigger board-level behavior at
runtime (e.g., enter low-power mode) call a board-provided wrapper such as
Board_Sleep() rather than a generic whal_X function.
Each platform driver defines its own configuration struct with the parameters
it needs. For example, a GPIO driver takes a pin configuration table; the
platform usually provides a WHAL_<PLATFORM>_GPIO_PIN(...) macro to populate
each entry compactly:
whal_Gpio g_whalGpio = {
.regmap = { WHAL_STM32WB55_GPIO_REGMAP },
/* .driver: direct API mapping */
.cfg = &(whal_Stm32wb_Gpio_Cfg) {
.pinCfg = (whal_Stm32wb_Gpio_PinCfg[PIN_COUNT]) {
[LED_PIN] = WHAL_STM32WB_GPIO_PIN(
WHAL_STM32WB_GPIO_PORT_B, 5, WHAL_STM32WB_GPIO_MODE_OUT,
WHAL_STM32WB_GPIO_OUTTYPE_PUSHPULL, WHAL_STM32WB_GPIO_SPEED_LOW,
WHAL_STM32WB_GPIO_PULL_UP, 0),
},
.pinCount = PIN_COUNT,
},
};A UART driver typically takes a pre-computed baud rate register value and a timeout:
whal_Uart g_whalUart = {
.regmap = { WHAL_STM32WB55_UART1_REGMAP },
/* .driver: direct API mapping */
.cfg = &(whal_Stm32wb_Uart_Cfg) {
.brr = WHAL_STM32WB_UART_BRR(64000000, 115200),
.timeout = &g_whalTimeout,
},
};The .driver: direct API mapping comment indicates this peripheral is using
the optimization path described above — calls to whal_Uart_* link directly
to the chip-specific implementation, no vtable dispatch.
See the platform-specific headers in wolfHAL/<device_type>/ for the full set
of configuration options for each driver, and the example boards in boards/
for full instantiations.
The board is responsible for initializing peripherals in dependency order. Drivers do not enable their own clocks or power supplies — the board must handle these prerequisites explicitly before calling a driver's Init.
A typical initialization sequence:
- Do any pre-clock-controller initialization (e.g., flash wait states, power supplies)
- Bring up the clock tree (oscillators, optional PLL, sysclk source)
- Enable peripheral clocks
- Initialize peripheral drivers
- Start timers
The chip's clock driver exposes Enable*/Disable*/Set* helpers that
the board calls in order. There is no generic whal_Clock_Init walker —
clock-tree shape varies too much across vendors to abstract.
static const whal_Myplatform_Clock_PeriphClk g_periphClks[] = {
{MY_PLATFORM_GPIOB_CLOCK},
{MY_PLATFORM_UART1_CLOCK},
};
#define PERIPH_CLK_COUNT \
(sizeof(g_periphClks) / sizeof(g_periphClks[0]))
whal_Error Board_Init(void)
{
whal_Error err;
/* Bring up clocks (chip-specific helpers, called in order) */
err = whal_Myplatform_Clock_EnableOsc(&g_whalClock,
&(whal_Myplatform_Clock_OscCfg){WHAL_MYPLATFORM_CLOCK_OSC0_CFG});
if (err)
return err;
err = whal_Myplatform_Clock_SetSysClock(&g_whalClock,
WHAL_MYPLATFORM_CLOCK_SYSCLK_SRC_OSC0);
if (err)
return err;
/* Enable peripheral clocks */
for (size_t i = 0; i < PERIPH_CLK_COUNT; i++) {
err = whal_Myplatform_Clock_EnablePeriphClk(&g_whalClock,
&g_periphClks[i]);
if (err)
return err;
}
/* Initialize peripherals */
err = whal_Gpio_Init(&g_whalGpio);
if (err)
return err;
err = whal_Uart_Init(&g_whalUart);
if (err)
return err;
err = whal_Timer_Init(&g_whalTimer);
if (err)
return err;
err = whal_Timer_Start(&g_whalTimer);
if (err)
return err;
return WHAL_SUCCESS;
}See the board examples in boards/ for complete initialization sequences
including platform-specific steps.
After initialization, use the wolfHAL API to interact with peripherals:
#include <wolfHAL/wolfHAL.h>
#include "board.h"
void main(void)
{
if (Board_Init() != WHAL_SUCCESS)
while (1);
while (1) {
whal_Gpio_Set(&g_whalGpio, BOARD_LED_PIN, 1);
whal_Uart_Send(&g_whalUart, "Hello!\r\n", 8);
Board_WaitMs(1000);
whal_Gpio_Set(&g_whalGpio, BOARD_LED_PIN, 0);
Board_WaitMs(1000);
}
}All API functions return whal_Error. Check for WHAL_SUCCESS to confirm the
operation completed. The error codes are:
| Code | Meaning |
|---|---|
WHAL_SUCCESS |
Operation completed successfully |
WHAL_EINVAL |
Invalid argument (null device pointer, null data pointer) |
WHAL_ENOTREADY |
Resource is busy or not yet available |
WHAL_EHARDWARE |
Hardware error (e.g., RNG entropy failure) |
WHAL_ETIMEOUT |
Operation timed out waiting for hardware |
WHAL_ENOTSUP |
Operation or parameter not supported by this driver/hardware |
wolfHAL gives you several ways to reduce code size depending on how much control you want.
Each driver source (platform or peripheral) provides an #ifdef block that
renames its driver functions to the top-level API names. When the
corresponding WHAL_CFG_<TYPE>_API_MAPPING_<VARIANT> flag is defined, the
driver file itself provides the definition of the top-level API — no
wrapper, no vtable indirection, no runtime null-check.
For example, -DWHAL_CFG_UART_API_MAPPING_STM32WB causes
src/uart/stm32wb_uart.c to emit external symbols named whal_Uart_Init,
whal_Uart_Deinit, whal_Uart_Send, and whal_Uart_Recv, each bound to
the polled STM32WB UART driver body. Application code calling
whal_Uart_Send(&dev, buf, sz) links directly to the driver.
The same pattern works for peripheral drivers: enabling
WHAL_CFG_SDHC_SPI_BLOCK_DIRECT_API_MAPPING, for instance, makes the
external SD-card-over-SPI driver provide the top-level whal_Block_* API
symbols directly.
Direct API mapping is only safe when a single driver of that type is present in the build. Boards that combine multiple drivers of the same type (e.g., on-chip flash + external SPI NOR flash) cannot enable mapping for that type — they keep the vtable dispatch so both drivers can be linked simultaneously.
The dispatch source src/<type>/<type>.c must not be compiled when the
corresponding mapping flag is active. Both the dispatch source and the
mapped driver source provide the same top-level symbols, which would cause
a multiple-definition link error. Exclude the dispatch source from the
board's source list.
Only one mapping flag may be active per device type per build.
The platform drivers provide a pre-built vtable with all operations populated. If you only use a subset of a driver's functionality, you can define your own vtable that only includes the functions you need:
static const whal_GpioDriver myGpioDriver = {
.Init = whal_Stm32wb_Gpio_Init,
.Deinit = whal_Stm32wb_Gpio_Deinit,
.Set = whal_Stm32wb_Gpio_Set,
/* Get left as NULL — calls return WHAL_ENOTSUP, saves pulling in that code */
};
whal_Gpio g_whalGpio = {
.regmap = { .base = 0x48000000, .size = 0x2000 },
.driver = &myGpioDriver,
.cfg = &gpioConfig,
};With link-time optimization (-flto) or garbage collection (-ffunction-sections
-Wl,--gc-sections), any driver functions not referenced through the vtable will be stripped from the final binary.
For maximum control, you can skip the vtable entirely and call the underlying platform driver functions directly:
#include <wolfHAL/gpio/stm32wb_gpio.h>
whal_Stm32wb_Gpio_Init(&g_whalGpio);
whal_Stm32wb_Gpio_Set(&g_whalGpio, BOARD_LED_PIN, 1);This eliminates the vtable indirection and lets the compiler inline or optimize the calls more aggressively.
Register-level drivers do not call other drivers internally, so this works without any caveats. Peripheral drivers (e.g., SPI flash) still call their bus driver through the vtable.
- See
boards/for complete board configuration examples - See Writing a Driver for how to add support for a new platform
- See Adding a Board for how to create a board configuration for your hardware