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 |
Distance reporting |
Yes |
You deliver distance samples through the |
Session lifecycle events |
Yes |
Your driver must signal session state transitions (for example, ranging started, suspended, or destroyed) so you can forward them through |
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 |
Bluetooth LE/UWB time synchronization |
Optional |
Implement |
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 |
|
|
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. |
|
|
Calls |
|
|
Stable public API used by the application; forwards to private |
|
|
Vendor-specific driver and SDK glue, session bookkeeping, and callback invocation. |
|
|
Applies access policy by evaluating distance thresholds, deciding unlock and lock actions, and handling UWB session lifecycle. |
Application |
|
Registers UWB callbacks and initializes the module before |
Integration steps
Integration of a third-party UWB chip consists of the following steps:
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 |
|---|---|---|
|
On UWB Ranging Service messages and Notification messages with the Ranging Message ID. |
The |
|
When the stack enters the |
Delegates to |
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
mBleMessageTransmitcalls 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 |
|---|---|---|
|
|
Report session lifecycle transitions. |
|
|
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
UltraWideBandImplbefore invoking the callback.Override
ExtractDistanceFromUwbData()in yourAccessManagerImpl.
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_ZEPHYRKconfig option.Do not apply the
uwb_qm35snippet.
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
UltraWideBandImplincustom_impl/uwb_impl.handcustom_impl/uwb_impl.cpp, using the files in that directory as a skeleton. Replace each-ENOSYSstub 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 |
|---|---|---|
|
|
Initialize hardware, store |
|
|
Release sessions and shut down the radio. |
|
|
Optional: trigger CCC Procedure 0 Bluetooth LE/UWB time sync. Implement if your UWB vendor requires explicit time alignment after Bluetooth LE connection. |
|
|
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 |
|
|
Create a per-session context keyed by |
|
|
Start or arm ranging after configuration. Currently not used by the reference applications. |
|
|
Tear down the UWB session when the Bluetooth LE session ends or access process completes. |
|
|
Pause active ranging (Aliro suspend and resume flow). Currently not used by the reference applications. |
|
|
Resume after suspend. Currently not used by the reference applications. |
|
|
Optional: human-readable firmware string for logging or shell commands.
Return |
|
|
Return whether |
|
|
Optional: stop any background sensing your hardware uses.
The |
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
UltraWideBandImplmethod signatures.Session list — QM35 keeps a Zephyr
sys_slist_tofSessionContextobjects mappingConnectionHandleto vendor session pointers. Replicate this pattern if your driver supports multiple concurrent sessions.Callback translation —
SessionHandlerCallback()maps vendor session events toRangingSessionStateand encodes distance into two big-endian bytes before callingmRangingData.Bluetooth LE relay —
TransmitBleMessage()invokesmBleMessageTransmitwith 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()beforeAliroInit():#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:
mRangingData→AccessManagerInstance().HandleRangingSessionData()mRangingSessionStateChanged→AccessManagerInstance().HandleRangingSessionStateChanged()mBleMessageTransmit→AliroStack::Instance().SendBleMessage()
- Aliro stack interface —
applications/*/src/aliro/interface_impl/uwb.cpp Implements
Interface::Uwb::HandleBleMessage()by forwarding toUltraWideBandInstance().HandleBleMessage().- Session lifecycle —
applications/*/src/aliro/interface_impl/session.cpp Forwards stack session events to
AccessManager: ranging start onStartRangingSession(), cleanup (including UWB teardown) onHandleTermination().- Access policy and ranging sessions —
applications/*/src/aliro/access_manager/access_manager_impl.cpp AddRangingSession()callsUltraWideBandInstance().ConfigureRangingSession()RemoveRangingSession()callsUltraWideBandInstance().TerminateRangingSession()EvaluateUwbOpenAllowed()andExtractDistanceFromUwbData()define the distance-based unlock policy
CMake integration
The build is wired across three places:
Subsystem library —
subsys/aliro/uwb/custom_impl/CMakeLists.txtcompiles youruwb_impl.cpp. The parentsubsys/aliro/uwb/CMakeLists.txtalways builds the shared facadeuwb.cpp.Application interface —
applications/*/src/aliro/interface_impl/CMakeLists.txtaddsuwb.cppwhen theCONFIG_NCS_ALIRO_BLE_UWBKconfig option is set.Include path — Application code includes
uwb_impl.h. With the custom implementation selected, Zephyr resolves that header fromcustom_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 |
|---|---|---|
|
Yes (via |
Enables stack UWB HSM and interface symbols. |
|
Yes |
Application-level Bluetooth with UWB integration. |
|
Yes |
Builds |
|
No |
Selects Qorvo example implementation instead of |
|
No |
QM35-only features (DFU, disambiguation, radar). |
Recommended bring-up sequence
Implement the methods in the order below. The first stages — initializing the hardware, configuring sessions, and relaying Bluetooth LE messages — form the minimum viable port, the smallest set of methods needed for an end-to-end ranging session. With these in place, the Aliro stack can drive a complete UWB setup exchange through your implementation.
The later stages refine reporting and validate the integration. Each stage adds a verification point, so you can confirm progress before moving on.
Build with stubs — Enable
CONFIG_DOOR_LOCK_BLE_UWBwithout QM35 snippets, as described in Prerequisites. Confirm the application links and logsUWB is not implemented(-ENOSYSfrom_Init()).Initialize the hardware — Implement
_Init(),_Deinit(), and_IsInitialized(). VerifyIsInitialized()returns true after init, and optionally surfaceGetFirmwareVersion()in a log or shell command.Configure and tear down sessions — Implement
_ConfigureRangingSession()and_TerminateRangingSession(). Confirm thatStartRangingSession()from the stack succeeds and that a session is cleanly removed on Bluetooth LE disconnect.Relay Bluetooth LE messages — Implement
_HandleBleMessage()and wiremBleMessageTransmit. Use logging to confirm M1–M4 traffic flows through your module and back to the User Device.Deliver ranging reports — Fire
mRangingDatawith a 16-bit big-endian centimeter distance. TuneAccessManagerthresholds throughSetMaxAllowedDistance()and confirm the distance reaches the access policy layer.Emit state notifications — Call
mRangingSessionStateChangedsoAccessManagerupdates reader state on suspend and destroy.Regression test — Run an Aliro Bluetooth LE + UWB session against a certified User Device or Aliro Test Harness.