Integrating a third-party UWB chip

This guide explains how to integrate a UWB radio from a vendor other than Qorvo into the nRF Door Lock and Access Control Add-on and the Aliro stack. The Qorvo QM35825 (QM35) implementation shipped in this repository is an example integration — it shows one way to map Aliro session semantics onto a vendor SDK, but it is not the only supported hardware path.

For the reference QM35825 architecture and stack interaction, see UWB integration in the reference applications.

Prerequisites

Before starting your port, make sure you understand the scope of the work and have the required components in place.

This guide assumes familiarity with the following:

  • Aliro specification — In particular, the Bluetooth LE message layout (Protocol Header, Message ID, Length, Payload) and the ranging session setup flow (M1-M4 exchange). Several integration points require you to forward complete Aliro messages unchanged, so a working understanding of this layout is essential. See Aliro application interactions for the relevant sequence diagrams.

  • Facade pattern used in the add-on — The platform code separates a stable public API (UltraWideBand) from a vendor-specific implementation (UltraWideBandImpl). You implement only the private methods; you do not modify the facade or the stack interface wiring. See Integration with the door lock add-on for details.

  • nRF Connect SDK and Zephyr build system — You configure the port through Kconfig options and adapt one or two CMake files.

Vendor SDK requirements

Your UWB silicon and its driver or SDK must be able to provide the following capabilities, which the Aliro stack and the reference AccessManager rely on:

Capability

Required

Notes

Ranging session configuration from URSK, session ID, and protocol version

Yes

The stack extracts these values from the Bluetooth LE session and passes them to ConfigureRangingSession(). Your SDK must accept an externally supplied URSK rather than generating its own key material.

Distance reporting

Yes

You deliver distance samples through the mRangingData callback. The reference AccessManager expects a 16-bit big-endian value in centimeters, but you can convert in your implementation or override the extraction logic.

Session lifecycle events

Yes

Your driver must signal session state transitions (for example, ranging started, suspended, or destroyed) so you can forward them through mRangingSessionStateChanged.

Forwarding opaque UWB setup messages

Yes

During setup, the vendor stack must accept and produce the UWB-related payloads carried inside Aliro Bluetooth LE messages, without requiring direct access to the link.

Multiple concurrent sessions

Recommended

If your hardware supports more than one User Device at a time, your implementation must map each ConnectionHandle to a separate vendor session context.

Bluetooth LE/UWB time synchronization

Optional

Implement BleTimeSync() only if your vendor requires explicit time alignment after the Bluetooth LE connection.

If your SDK cannot satisfy one of the required capabilities, the corresponding Aliro ranging flow cannot complete, and the stack may terminate the Bluetooth LE session.

Starting point

The subsys/aliro/uwb/custom_impl/ directory is the starting point for your port. It contains skeleton uwb_impl.h and uwb_impl.cpp files whose methods return -ENOSYS.

When the QM35 Kconfig options are disabled, the build system selects this directory automatically, so you can build, link, and run the reference application before writing any vendor code. This is the basis for the first step of the porting workflow.

To confirm your environment is ready, build a reference application with the specific options set and verify that it links and logs UWB is not implemented.

Solution architecture

Your code sits in UltraWideBandImpl, which translates between the Aliro-facing API and your vendor driver or SDK. To learn more about the interactions between the Aliro stack and the application, see the Aliro application interactions documentation page.

The table below summarizes each layer, its location in the codebase, and its responsibility. The sections that follow divide the port into stack integration and application integration so you can implement and test each part independently.

Component

Location

Responsibility

Aliro stack state machine

nRF Connect SDK Aliro stack (precompiled binary library)

Enters UwbRanging state, delegates UWB Ranging Service and Notification/Ranging Bluetooth LE messages to Interface::Uwb::HandleBleMessage() (full Aliro message buffers), and requests ranging session creation through Interface::Session::StartRangingSession().

Interface::Uwb

include/aliro/interface.h

Single stack entry point for inbound Bluetooth LE/UWB traffic. The stack forwards only UWB Ranging Service and Notification/Ranging messages as complete Aliro messages.

Interface::Session

include/aliro/interface.h

Calls AccessManager::StartRangingSession() when the stack has URSK and session ID and needs the platform to configure hardware ranging. As a result, AccessManager calls UltraWideBandInstance().ConfigureRangingSession().

UltraWideBand facade

subsys/aliro/uwb/uwb.h, subsys/aliro/uwb/uwb.cpp

Stable public API used by the application; forwards to private UltraWideBandImpl methods.

UltraWideBandImpl

subsys/aliro/uwb/custom_impl/ (your code) or qm35_impl/ (an example)

Vendor-specific driver and SDK glue, session bookkeeping, and callback invocation.

AccessManager

applications/*/src/aliro/access_manager/

Applies access policy by evaluating distance thresholds, deciding unlock and lock actions, and handling UWB session lifecycle.

Application main

applications/*/src/main.cpp

Registers UWB callbacks and initializes the module before AliroInit().

Integration steps

Integration of a third-party UWB chip consists of the following steps:

  1. Integration with the Aliro stack

  2. Integration with the door lock add-on

After completing both steps, follow the recommended bring-up sequence to implement and verify the port incrementally.

Integration with the Aliro stack

The Aliro stack remains vendor-agnostic. It never includes specific UWB driver headers. All hardware access goes through two interface functions implemented in the application tree.

Stack interface contract

When the CONFIG_NCS_ALIRO_BLE_UWB Kconfig option is enabled (automatically enabled when the CONFIG_DOOR_LOCK_BLE_UWB Kconfig option is selected), the stack expects the following implementations (guarded by the same Kconfig symbol):

Function

When it is called

Contract and behavior

Aliro::Interface::Uwb::HandleBleMessage()

On UWB Ranging Service messages and Notification messages with the Ranging Message ID.

The data buffer is a complete Aliro message, not a bare payload. It must include, in order: Protocol Header (1 byte), Message ID (1 byte), Length (16-bit, big-endian, payload size), and Payload — the Aliro Bluetooth LE message layout from the Aliro specification. The length argument is the total byte count of that complete message (four-byte header plus payload). Your implementation should pass this buffer unchanged to the vendor stack (the Qorvo example copies the full message into aliro_uwb_session_message_handle()). Return 0 on success or a negative errno value on failure. Depending on the internal state of the Aliro session machine state, any error returned by this function may eventually cause the stack to terminate the Bluetooth LE session.

Aliro::Interface::Session::StartRangingSession()

When the stack enters the UwbRanging HSM state, after sessionId, ursk, and protocolVersion are extracted from the Bluetooth LE session.

Delegates to AccessManager, which ultimately calls UltraWideBandInstance().ConfigureRangingSession().

Both functions have reference implementations under the applications/*/src/aliro/interface_impl/ directory. The following example shows the reference HandleBleMessage() implementation in interface_impl/uwb.cpp:

return ::Aliro::Uwb::UltraWideBandInstance()
    .HandleBleMessage(data, length, sessionContext);

Note

In most ports you only adapt uwb_impl.cpp (and uwb_impl.h). You normally do not modify interface_impl/uwb.cpp or interface_impl/session.cpp. Keep that wiring as-is and implement ranging setup in UltraWideBandImpl::_ConfigureRangingSession().

Outbound path: UWB and Bluetooth LE inter-operation

When your UWB stack needs to send a response to the User Device (for example M2 after M1), it must not call Bluetooth LE APIs directly. Instead, invoke the mBleMessageTransmit callback registered during UltraWideBand::Init().

The reference application code wires this callback to the Aliro stack public API:

.mBleMessageTransmit = [](UltraWideBand::SessionContextHandle sessionContext,
                           const uint8_t *data, size_t length) {
    Aliro::AliroStack::Instance().SendBleMessage(sessionContext, data, length);
},

SendBleMessage() accepts the same full Aliro message layout as HandleBleMessage() (Protocol Header, Message ID, Length, Payload). The stack parses the buffer, extracts the protocol type and message ID, encrypts the payload, and transmits it over Bluetooth LE through Interface::Session::Send(). Messages you send through this API should therefore use either UWB Ranging Service or Notification with Ranging Message ID — the same types the stack forwards inbound through HandleBleMessage(). Passing only a payload without the Aliro header is invalid and will be rejected by the stack.

Session context handle

Both interface functions receive an Aliro::ConnectionHandle (typedef UltraWideBand::SessionContextHandle). This opaque handle identifies the Aliro Bluetooth LE session and must be stored in your implementation so that:

  • Incoming Bluetooth LE payloads from HandleBleMessage() can be routed to the correct UWB session object.

  • Outbound mBleMessageTransmit calls can be directed to the Bluetooth LE session the stack is expecting.

  • Ranging callbacks (mRangingData, mRangingSessionStateChanged) correlate measurements with the correct access-control context.

Inbound path: UWB to the access policy layer

Your implementation reports two event streams through the Callbacks structure defined in the subsys/aliro/uwb/uwb.h file:

Callback

Payload type

Purpose

mRangingSessionStateChanged

Aliro::RangingSessionState

Report session lifecycle transitions.

mRangingData

Aliro::UwbRangingData

Deliver distance samples.

mRangingSessionStateChanged

Notify state transitions using Aliro::RangingSessionState (include/aliro/types.h):

enum class RangingSessionState : uint8_t {
    Uninitialized = 0x00,
    Initialized,
    Idle,
    Ranging,
    RangingSuspended,
    RangingResumed,
    Destroyed,
};

The AccessManager acts on Destroyed and RangingSuspended to update reader state and prevent unwanted unlock toggling.

mRangingData

Delivers distance samples as Aliro::UwbRangingData (a data structure that contains a pointer to the data and its length).

The reference AccessManager reads a 16-bit big-endian distance in centimeters from the first two bytes (see ExtractDistanceFromUwbData() in the access_manager_impl.cpp file).

If your vendor reports distance in another format, do one of the following:

  • Convert in UltraWideBandImpl before invoking the callback.

  • Override ExtractDistanceFromUwbData() in your AccessManagerImpl.

Integration with the door lock add-on

Platform UWB code is located under the subsys/aliro/uwb/ directory. The build selects the implementation as follows (subsys/aliro/uwb/CMakeLists.txt):

if(CONFIG_QM35_UWB_ALIRO_ZEPHYR)
  add_subdirectory(qm35_impl)    # Qorvo example implementation
else()
  add_subdirectory(custom_impl)  # Your starting point
endif()

To use a third-party chip:

  • Do not enable the CONFIG_QM35_UWB_ALIRO_ZEPHYR Kconfig option.

  • Do not apply the uwb_qm35 snippet.

Enable only the following options:

CONFIG_DOOR_LOCK_BLE_UWB=y
CONFIG_DOOR_LOCK_ALIRO_UWB=y

The UltraWideBand facade pattern

Following the same design as AccessManager, the add-on exposes a singleton facade:

  • UltraWideBand — Declares the public methods (Init(), HandleBleMessage(), …).

  • UltraWideBandImpl — Implements the private methods (_Init(), _HandleBleMessage(), …).

  • UltraWideBandInstance() — Returns the facade reference used throughout the application.

External code should call UltraWideBandInstance() and should not call UltraWideBandImpl methods directly.

The public UltraWideBand API in subsys/aliro/uwb/uwb.h defines the contract between the reference application and your implementation.

To use it:

  • Implement UltraWideBandImpl in custom_impl/uwb_impl.h and custom_impl/uwb_impl.cpp, using the files in that directory as a skeleton. Replace each -ENOSYS stub with real driver logic.

  • Implement only the methods declared in the facade. Keep any additional chip-specific types and helpers private to UltraWideBandImpl.

The implementation methods table lists every method in the contract and the behavior each one must provide.

Implementation methods

Each row below maps a public UltraWideBand method to the private UltraWideBandImpl method you implement, and describes the behavior it must provide. Methods marked Optional can remain no-op or -ENOSYS stubs if your hardware does not need them.

Public method (via facade)

Private implementation method

Expected behavior

Init(callbacks)

_Init()

Initialize hardware, store callbacks, set _IsInitialized() to true when ready. Return 0 on success. The reference main.cpp treats -ENOSYS as “UWB not implemented” and continues without ranging.

Deinit()

_Deinit()

Release sessions and shut down the radio.

BleTimeSync()

_BleTimeSync()

Optional: trigger CCC Procedure 0 Bluetooth LE/UWB time sync. Implement if your UWB vendor requires explicit time alignment after Bluetooth LE connection.

HandleBleMessage(data, length, handle)

_HandleBleMessage()

Pass the full decrypted Aliro message (header + payload) to your vendor stack. This is the critical path during session setup (M1-M4 exchange over Bluetooth LE). Outbound replies must use the same message layout when invoking mBleMessageTransmit / SendBleMessage().

ConfigureRangingSession(id, ursk, version, handle)

_ConfigureRangingSession()

Create a per-session context keyed by handle, program URSK and protocol version into the vendor stack. Called from AccessManager when the Aliro stack enters ranging.

InitiateRangingSession(handle)

_InitiateRangingSession()

Start or arm ranging after configuration. Currently not used by the reference applications.

TerminateRangingSession(handle)

_TerminateRangingSession()

Tear down the UWB session when the Bluetooth LE session ends or access process completes.

SuspendRangingSession(handle)

_SuspendRangingSession()

Pause active ranging (Aliro suspend and resume flow). Currently not used by the reference applications.

ResumeRangingSession(handle)

_ResumeRangingSession()

Resume after suspend. Currently not used by the reference applications.

GetFirmwareVersion()

_GetFirmwareVersion()

Optional: human-readable firmware string for logging or shell commands. Return nullptr if unavailable.

IsInitialized()

_IsInitialized()

Return whether Init() completed successfully.

StopRadarSession()

_StopRadarSession()

Optional: stop any background sensing your hardware uses. The custom_impl stub leaves this as a no-op.

Note

Currently, UWB setup and suspend/resume in the reference applications are driven by Bluetooth LE messages from the User Device (relayed through the Aliro stack and HandleBleMessage()), not by explicit calls to InitiateRangingSession(), SuspendRangingSession(), or ResumeRangingSession(). Those methods remain part of the public API for integrators who need the application to initiate setup or control ranging directly.

Using the Qorvo QM35 implementation as an example

The QM35 port (subsys/aliro/uwb/qm35_impl/) demonstrates a complete production-style integration. Study it for patterns, not as a mandatory structure:

  • Vendor SDK boundary — Qorvo host driver libraries and APIs wrap the communication with the QM35825 firmware. Your port replaces this layer with your vendor’s equivalent while keeping the same UltraWideBandImpl method signatures.

  • Session list — QM35 keeps a Zephyr sys_slist_t of SessionContext objects mapping ConnectionHandle to vendor session pointers. Replicate this pattern if your driver supports multiple concurrent sessions.

  • Callback translation — SessionHandlerCallback() maps vendor session events to RangingSessionState and encodes distance into two big-endian bytes before calling mRangingData.

  • Bluetooth LE relay — TransmitBleMessage() invokes mBleMessageTransmit with raw bytes from the vendor stack.

UWB integration in the reference applications

The reference applications already contain the glue code that is used for the integration with the Aliro stack, the Access Manager, and the UWB implementation. The entries follow the data path, from initialization through to the unlock decision.

Initialization — applications/*/src/main.cpp

Register callbacks and call Init() before AliroInit():

#ifdef CONFIG_DOOR_LOCK_BLE_UWB
constexpr Aliro::Uwb::UltraWideBand::Callbacks uwbCallbacks = { /* ... */ };
int uwbError = Aliro::Uwb::UltraWideBandInstance().Init(uwbCallbacks);
#endif

The reference applications connect callbacks as follows:

  • mRangingDataAccessManagerInstance().HandleRangingSessionData()

  • mRangingSessionStateChangedAccessManagerInstance().HandleRangingSessionStateChanged()

  • mBleMessageTransmitAliroStack::Instance().SendBleMessage()

Aliro stack interface — applications/*/src/aliro/interface_impl/uwb.cpp

Implements Interface::Uwb::HandleBleMessage() by forwarding to UltraWideBandInstance().HandleBleMessage().

Session lifecycle — applications/*/src/aliro/interface_impl/session.cpp

Forwards stack session events to AccessManager: ranging start on StartRangingSession(), cleanup (including UWB teardown) on HandleTermination().

Access policy and ranging sessions — applications/*/src/aliro/access_manager/access_manager_impl.cpp
  • AddRangingSession() calls UltraWideBandInstance().ConfigureRangingSession()

  • RemoveRangingSession() calls UltraWideBandInstance().TerminateRangingSession()

  • EvaluateUwbOpenAllowed() and ExtractDistanceFromUwbData() define the distance-based unlock policy

CMake integration

The build is wired across three places:

  • Subsystem library — subsys/aliro/uwb/custom_impl/CMakeLists.txt compiles your uwb_impl.cpp. The parent subsys/aliro/uwb/CMakeLists.txt always builds the shared facade uwb.cpp.

  • Application interface — applications/*/src/aliro/interface_impl/CMakeLists.txt adds uwb.cpp when the CONFIG_NCS_ALIRO_BLE_UWB Kconfig option is set.

  • Include path — Application code includes uwb_impl.h. With the custom implementation selected, Zephyr resolves that header from custom_impl/.

To use your own directory instead of custom_impl, either change the else() branch in subsys/aliro/uwb/CMakeLists.txt, or edit the files in place.

Kconfig checklist

Option

Required for custom UWB

Notes

CONFIG_NCS_ALIRO_BLE_UWB

Yes (via CONFIG_DOOR_LOCK_BLE_UWB)

Enables stack UWB HSM and interface symbols.

CONFIG_DOOR_LOCK_BLE_UWB

Yes

Application-level Bluetooth with UWB integration.

CONFIG_DOOR_LOCK_ALIRO_UWB

Yes

Builds subsys/aliro/uwb/ platform module.

CONFIG_QM35_UWB_ALIRO_ZEPHYR

No

Selects Qorvo example implementation instead of custom_impl.

CONFIG_DOOR_LOCK_ALIRO_UWB_QM35_*

No

QM35-only features (DFU, disambiguation, radar).