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 buildbefore 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_GETfor 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=yPattern 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 mainAvoid 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
Next
Continue to Phase 4 — Wireless Protocol Integration.