FirmwareMaestro Docs
Dev Guide

Phase 3 — Hardware Abstraction & Drivers

Leverage Nordic's HAL and Zephyr drivers for hardware initialization and peripheral access.

The nRF Connect SDK provides comprehensive hardware abstraction through Zephyr's device driver model and Nordic-specific HAL layers. Using Device Tree for configuration keeps your C code clean and makes porting between boards straightforward.

Steps

Configure Device Tree

Use Device Tree Source (DTS) files and overlays to configure hardware without modifying C code.

// Example overlay file (boards/nrf52840dk_nrf52840.overlay)
// Enable I2C with a BME280 sensor at address 0x76
// Configure GPIO for a status LED on pin P0.13
// Use aliases for hardware-independent code access
  • Use overlays instead of modifying board DTS files
  • Check compatible strings in the Zephyr bindings documentation
  • Use aliases for hardware-independent code
  • Validate overlays with west build before flashing

See the Zephyr Device Tree Guide.

Initialize the clock system

Configure high-frequency and low-frequency clocks for optimal performance and power consumption.

# prj.conf
CONFIG_CLOCK_CONTROL=y
CONFIG_CLOCK_CONTROL_NRF=y
  • HFCLK (64 MHz) is needed for the radio and high-speed peripherals
  • LFCLK (32.768 kHz) runs always for RTC and timers
  • Use an external 32 kHz crystal for best BLE timing accuracy
  • Clocks are managed automatically by Zephyr in most cases

Set up GPIO

Configure GPIO pins for inputs, outputs, and interrupts using Zephyr's GPIO API.

GPIO setup pattern:
1. Get GPIO device from Device Tree using GPIO_DT_SPEC_GET
2. Check gpio_is_ready_dt() before using GPIO
3. Configure with gpio_pin_configure_dt()
4. For buttons: add interrupt with gpio_pin_interrupt_configure_dt()
5. Use gpio_init_callback() and gpio_add_callback() for the ISR
  • Always check gpio_is_ready_dt() before using GPIO
  • Use GPIO_DT_SPEC_GET for compile-time device tree access
  • Configure interrupt polarity based on hardware (pull-up/pull-down)
  • Debounce button inputs in software or hardware

Configure serial interfaces (UART, SPI, I2C)

Set up communication peripherals using Zephyr device drivers.

# prj.conf
CONFIG_UART_ASYNC_API=y
CONFIG_I2C=y
CONFIG_SPI=y
Pattern for I2C:
1. Get device with DEVICE_DT_GET(DT_NODELABEL(i2c0))
2. Check device_is_ready() before use
3. Use i2c_write_read() for register access
  • Use async APIs for non-blocking operations
  • Check max clock frequencies in peripheral datasheets
  • Configure DMA for high-throughput transfers
  • Handle bus errors and implement retry logic

Implement interrupt handlers

Handle hardware interrupts efficiently using Zephyr's ISR management.

ISR best-practice pattern:
1. Keep the ISR short — just submit work with k_work_submit()
2. Use k_work_init() to initialize the work item in main()
3. Do heavy processing in the work handler, not in the ISR
4. Use volatile for data shared between ISR and main

Avoid blocking calls (malloc, printf) in ISRs and use atomic operations for shared data.

Set up power management

Configure power management for optimal battery life using Nordic's PM API.

# prj.conf
CONFIG_PM=y
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y
  • Zephyr automatically enters low-power modes when idle
  • Use pm_device_action_run() to suspend/resume peripherals
  • Use pm_policy_state_lock_get/put() during critical operations
  • Profile power with and without PM enabled

See Zephyr Power Management.

Next

Continue to Phase 4 — Wireless Protocol Integration.

On this page