added xiao nrf54l15 support for otbr

This commit is contained in:
2026-08-21 16:35:54 +02:00
commit 4c27e81df5
15 changed files with 1167 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Zephyr/west build output
build/
build-*/
# west workspace metadata, if this repo is ever used as a manifest repo
.west/
# Editor / OS droppings
._*
.DS_Store
*.swp

18
CMakeLists.txt Normal file
View File

@@ -0,0 +1,18 @@
#
# Copyright (c) 2020 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
cmake_minimum_required(VERSION 3.20.0)
set(OT_NCP_VENDOR_HOOK_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(openthread_coprocessor)
# NORDIC SDK APP START
target_sources(app PRIVATE src/main.c src/nrf_802154_radio_wrapper.c)
target_sources_ifdef(CONFIG_RCP_SAMPLE_HCI app PRIVATE src/rcp_hci.c)
# NORDIC SDK APP END

22
Kconfig Normal file
View File

@@ -0,0 +1,22 @@
#
# Copyright (c) 2023 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
# Set product string for USB device
config CDC_ACM_SERIAL_PRODUCT_STRING
default "Thread Co-Processor"
depends on USB_DEVICE_STACK_NEXT
menu "Zephyr Kernel"
source "Kconfig.zephyr"
endmenu
module = OT_COPROCESSOR
module-str = ot_coprocessor
source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config"
config RCP_SAMPLE_HCI
bool "Bluetooth HCI USB support for the coprocessor sample"
select EXPERIMENTAL

10
Kconfig.sysbuild Normal file
View File

@@ -0,0 +1,10 @@
#
# Copyright (c) 2026 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
config PARTITION_MANAGER
default n
source "share/sysbuild/Kconfig"

202
README.rst Normal file
View File

@@ -0,0 +1,202 @@
.. _ot_coprocessor_sample:
Thread: Co-processor
####################
.. contents::
:local:
:depth: 2
The :ref:`Thread <ug_thread>` Co-processor sample demonstrates how to implement OpenThread's :ref:`thread_architectures_designs_cp` inside the Zephyr environment.
The sample uses the :ref:`thread_architectures_designs_cp_rcp` architecture.
The sample is based on Zephyr's :zephyr:code-sample:`openthread-coprocessor` sample.
However, it customizes Zephyr's sample to fulfill the |NCS| requirements (for example, by increasing the stack size dedicated for the user application), and also extends it with features such as:
* Increased Mbed TLS heap size.
* Lowered main stack size to increase user application space.
* No obsolete configuration options.
* Vendor hooks for co-processor architecture allowing users to extend handled properties by their own, customized functionalities.
* Thread 1.4 features, including support for Thread 1.3 and Thread 1.2.
This sample supports optional :ref:`logging extension <ot_coprocessor_sample_logging>`, which can be turned on or off independently.
To enable logging extension, use the ``ot-debug`` snippet.
.. _ot_coprocessor_sample_requirements:
Requirements
************
The sample supports the following development kits for testing the network status:
.. table-from-sample-yaml::
To test the sample, you need at least one development kit.
You can use additional development kits programmed with the Co-processor sample for testing network joining.
Moreover, the sample requires a Userspace higher layer process running on your device to communicate with the MCU co-processor part.
This sample uses ``ot-cli`` as reference.
Overview
********
The sample demonstrates using a co-processor target on the MCU to communicate with `ot-cli` on Unix-like operating system.
According to the co-processor architecture, the MCU part must cooperate with user higher layer process to establish the complete full stack application.
The sample shows how to set up the connection between the co-processor and the host.
By default, this sample comes with the :ref:`RCP set of OpenThread functionalities <thread_ug_feature_sets>` enabled (:kconfig:option:`CONFIG_OPENTHREAD_NORDIC_LIBRARY_RCP`).
Application architecture options
================================
.. include:: ../cli/README.rst
:start-after: ot_cli_sample_architecture_options_start
:end-before: ot_cli_sample_architecture_options_end
.. _ot_coprocessor_sample_logging:
Logging extension
=================
By default, this sample uses :ref:`Spinel logging backend <ug_logging_backends_spinel>` for sending log messages to the host device using the Spinel protocol.
This is a useful feature, because it does not require separate interfaces to communicate with the co-processor through the Spinel protocol and collect log messages.
Moreover, using the Spinel logging backend (by setting :kconfig:option:`CONFIG_LOG_BACKEND_SPINEL`) does not exclude using another backend like UART or RTT at the same time.
By default, the log levels for all modules are set to critical to not engage the microprocessor in unnecessary activities.
To make the solution flexible, you can change independently the log levels for your modules, for the whole Zephyr system, and for OpenThread.
Use the ``ot-debug`` snippet as reference for this purpose.
For example:
.. code-block:: none
west build -b nrf54l15dk_nrf54l15_cpuapp -p -- -Dcoprocessor_SNIPPET=ot-debug
User interface
**************
All the interactions with the application are handled using serial communication.
You can interact with the sample through ``ot-daemon`` or ``ot-cli`` with commands listed in `OpenThread CLI Reference`_.
See :ref:`ug_thread_tools_ot_apps` for more information.
You can also use your own application, provided that it supports the Spinel communication protocol.
.. note::
|thread_hwfc_enabled|
In addition, the Co-processor sample reconfigures the baud rate to 1000000 bit/s by default.
Diagnostic module
=================
The Co-processor sample enables a diagnostic module in a similar way as described in the :ref:`ot_cli_sample_diag_module` section of the :ref:`ot_cli_sample` sample documentation.
However, the Co-processor and CLI samples use different commands for the module, as described in the :ref:`ot_coprocessor_testing` section.
Rebooting to bootloader
=======================
The Co-processor sample enables rebooting to bootloader for the ``nrf52840dongle/nrf52840`` board target, similar to what is described in the :ref:`ot_cli_sample_bootloader` section of the :ref:`ot_cli_sample` sample documentation.
However, the Co-processor and CLI samples use different commands, as described in the :ref:`ot_coprocessor_testing` section.
Additionally, the :ref:`ug_thread_tools_ot_apps` should be built with ``-DOT_PLATFORM_BOOTLOADER_MODE=ON`` option.
Configuration
*************
|config|
Check and configure the following library option that is used by the sample:
* :kconfig:option:`CONFIG_OPENTHREAD_COPROCESSOR_RCP` - Selects the RCP architecture for the sample.
.. include:: /includes/sample_fem_support.txt
Building and running
********************
.. |sample path| replace:: :file:`samples/openthread/coprocessor`
|enable_thread_before_testing|
.. include:: /includes/build_and_run.txt
.. _ot_coprocessor_testing:
HCI support
===========
The sample supports Bluetooth® Low Energy (LE) Controller functionality using HCI over UART.
To enable HCI support, complete the following steps:
.. tabs::
.. tab:: nRF54L15, nRF54L10, and nRF54L05 DKs
HCI communication is handled over a dedicated UART peripheral on the nRF54L Series System-on-Chip (SoC).
After connecting the board using the SEGGER J-Link USB port, two serial ports will be created.
The first port is used for HCI and the second for the Thread co-processor (Spinel).
To enable HCI support, run the following command:
.. parsed-literal::
:class: highlight
west build -b *board_target* -p -- -DEXTRA_CONF_FILE=extra_conf/rcp_hci_nrf54l_05_10_15.conf -DEXTRA_DTC_OVERLAY_FILE=extra_conf/rcp_hci_nrf54l_05_10_15.overlay
.. tab:: Other supported boards
For a list of supported boards, see the :ref:`ot_coprocessor_sample_requirements` section.
HCI communication is handled over a USB CDC ACM interface.
After connecting the board using the nRF USB port, two serial ports will be created.
Usually, the first port is used for HCI and the second for the Thread co-processor.
To enable HCI support, run the following command:
.. parsed-literal::
:class: highlight
west build -b *board_target* -p -- -DEXTRA_CONF_FILE=extra_conf/rcp_hci.conf -DEXTRA_DTC_OVERLAY_FILE=extra_conf/rcp_hci.overlay
Vendor hooks
============
The sample supports vendor hooks for co-processor architecture allowing you to extend handled properties by your own, customized functionalities.
To enable vendor hooks, set the :kconfig:option:`CONFIG_OPENTHREAD_COPROCESSOR_VENDOR_HOOK_SOURCE` Kconfig option to the path of the vendor hook source file and run the following command with *board_target* replaced with the board target name:
.. parsed-literal::
:class: highlight
west build -b *board_target* -p -- -DCONFIG_OPENTHREAD_COPROCESSOR_VENDOR_HOOK_SOURCE="/src/user_vendor_hook.cpp"
Testing
=======
After building the sample and programming it to your development kit, complete the following steps to test it:
1. Connect the development kit's SEGGER J-Link USB port to the PC USB port with a USB cable.
If you are using HCI for boards other than nRF54L15, nRF54L10, and nRF54L05 DKs, connect the kit's nRF USB port to the PC USB port instead.
#. Get the kit's serial port name (for example, :file:`/dev/ttyACM0`).
#. Run and configure ot-cli as described in :ref:`ug_thread_tools_ot_apps`.
#. From this point, you can follow the :ref:`ot_cli_sample_testing` instructions in the CLI sample by removing the `ot` prefix for each command.
If you are using HCI, follow the instructions for the :zephyr:code-sample:`bluetooth_hci_uart` sample in the Zephyr documentation.
You can follow these instead of or in addition to the CLI sample instructions.
Dependencies
************
This sample uses the following Zephyr libraries:
* :ref:`zephyr:kernel_api`:
* ``include/kernel.h``
* :ref:`zephyr:thread_protocol_interface`
* :ref:`zephyr:logging_api`:
* ``include/logging/log.h``
* :ref:`zephyr:bluetooth-hci`

View File

@@ -0,0 +1,18 @@
# OpenThread RCP on the Seeed XIAO nRF54L15.
# The XIAO has no external SPI flash populated.
CONFIG_SPI_NOR=n
# Nordic uses this on the nRF54L15 DK to keep the main stack clear of CRACEN
# (the hardware crypto engine) stack usage.
CONFIG_MAIN_STACK_SIZE=2048
# Nothing may write to uart20 except spinel. The boot banner and the UART log
# backend would both land mid-stream and desynchronise otbr-agent's HDLC
# decoder. Logs ride in-band over spinel instead, where otbr-agent decodes them.
CONFIG_BOOT_BANNER=n
CONFIG_LOG=y
CONFIG_LOG_MAX_LEVEL=1
CONFIG_LOG_BACKEND_UART=n
CONFIG_LOG_BACKEND_SPINEL=y
CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048

View File

@@ -0,0 +1,36 @@
/*
* OpenThread RCP on the Seeed XIAO nRF54L15.
*
* uart20 (P1.09 TX / P1.08 RX) is wired to the onboard CMSIS-DAP USB-serial
* bridge, so it is both the board's default console and the only UART that
* reaches the host over USB. The RCP needs that link for spinel, so:
*
* - zephyr,ot-uart -> uart20 (spinel to otbr-agent on the host)
* - zephyr,console -> uart21 (moved off, or console output would corrupt
* the spinel byte stream)
*
* uart21 (P2.08 TX / P2.07 RX) is exposed on the XIAO edge pads. Nothing needs
* to be attached; hook up a USB-UART adapter there if you want boot logs.
*
* Unlike Nordic's nrf54l15dk overlay, hw-flow-control is NOT set: the XIAO's
* pinctrl only defines TX and RX for uart20, and the CMSIS-DAP bridge does not
* carry RTS/CTS. See docs/ for the baud rate this was validated at.
*/
/ {
chosen {
zephyr,ot-uart = &uart20;
zephyr,console = &uart21;
zephyr,shell-uart = &uart21;
};
};
&uart20 {
status = "okay";
current-speed = <1000000>;
};
&uart21 {
status = "okay";
current-speed = <115200>;
};

24
prj.conf Normal file
View File

@@ -0,0 +1,24 @@
#
# Copyright (c) 2020 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
# Enable OpenThread
CONFIG_OPENTHREAD=y
# Set OpenThread NCP architecture
CONFIG_OPENTHREAD_COPROCESSOR=y
CONFIG_OPENTHREAD_COPROCESSOR_RCP=y
# Nordic feature set
CONFIG_OPENTHREAD_NORDIC_LIBRARY_RCP=y
# Increase Settings storage size
CONFIG_PM_PARTITION_SIZE_SETTINGS_STORAGE=0x8000
# Disable RTT
CONFIG_USE_SEGGER_RTT=n
# Disable GPIO
CONFIG_GPIO=n

127
scripts/ncs-toolchain.sh Executable file
View File

@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# nRF Connect SDK toolchain environment.
#
# Normally you do not source this directly. It is wired into Zephyr's own
# entry point via ~/.zephyrrc (see scripts/install-zephyrrc.sh), so that
#
# source /opt/ncs/v3.4.0/zephyr/zephyr-env.sh
#
# sets up ZEPHYR_BASE *and* the toolchain in one step. Sourcing zephyr-env.sh
# without this leaves you on the system cmake and python, and the build fails:
#
# Could NOT find Python3: Found unsuitable version "3.10.6",
# but required is at least "3.12"
#
# because /usr/local/bin/python3.10 (and the host's 3.14) are found before the
# Python 3.12 that the SDK toolchain bundle ships.
#
# Nothing is hardcoded: the bundle directory is discovered, and the variables
# are read from the bundle's own environment.json, so toolchain or SDK updates
# are picked up instead of silently rotting.
#
# Overrides, if the defaults guess wrong:
# NCS_ROOT SDK install root (default /opt/ncs)
# NCS_VERSION version dir under NCS_ROOT (default: newest vX.Y.Z)
# NCS_TOOLCHAIN toolchain bundle directory (default: most recent)
_ncs_warn() { echo "ncs-toolchain: $*" >&2; }
: "${NCS_ROOT:=/opt/ncs}"
if [ ! -d "$NCS_ROOT" ]; then
_ncs_warn "SDK root not found: $NCS_ROOT (set NCS_ROOT)"
else
# --- locate the SDK version directory ---------------------------------------
# Prefer the tree that zephyr-env.sh is actually being sourced from, so the
# toolchain always matches the SDK in use rather than guessing.
if [ -z "${NCS_VERSION:-}" ] && [ -n "${ZEPHYR_BASE:-}" ]; then
case "$ZEPHYR_BASE" in
"$NCS_ROOT"/*) NCS_VERSION="${ZEPHYR_BASE#"$NCS_ROOT"/}"; NCS_VERSION="${NCS_VERSION%%/*}" ;;
esac
fi
if [ -z "${NCS_VERSION:-}" ]; then
NCS_VERSION="$(
for d in "$NCS_ROOT"/v*/; do
[ -d "$d/zephyr" ] && basename "$d"
done | sort -V | tail -1
)"
fi
# --- locate the toolchain bundle --------------------------------------------
if [ -z "${NCS_TOOLCHAIN:-}" ]; then
# Bundles are keyed by an opaque id and manifest.json carries no plain SDK
# version to match on, so take the most recently installed.
NCS_TOOLCHAIN="$(
for d in "$NCS_ROOT"/toolchains/*/; do
[ -f "$d/environment.json" ] && printf '%s %s\n' "$(stat -c %Y "$d")" "${d%/}"
done | sort -n | tail -1 | cut -d' ' -f2-
)"
fi
if [ -z "${NCS_TOOLCHAIN:-}" ] || [ ! -f "$NCS_TOOLCHAIN/environment.json" ]; then
_ncs_warn "no toolchain bundle with environment.json under $NCS_ROOT/toolchains (set NCS_TOOLCHAIN)"
elif [ "${_NCS_TOOLCHAIN_APPLIED:-}" = "$NCS_TOOLCHAIN" ]; then
: # already applied in this shell; re-prepending PATH would just pile up
else
# --- apply the bundle's declared environment --------------------------------
# environment.json declares each variable as either a "string" or a list of
# paths relative to the bundle root, plus how to combine with any existing
# value ("prepend_to" or "overwrite"). Honour what it declares rather than
# copying today's values, so added or changed variables are picked up too.
#
# Parsed with the host python: the bundle's python is only usable once
# PYTHONHOME points at it, and a PYTHONHOME/PYTHONPATH left over from an
# earlier sourcing would break it -- hence `env -u`.
_ncs_env="$(env -u PYTHONHOME -u PYTHONPATH python3 - "$NCS_TOOLCHAIN" <<'PY'
import json, os, shlex, sys
root = sys.argv[1]
with open(os.path.join(root, "environment.json")) as fh:
spec = json.load(fh)
for var in spec.get("env_vars", []):
key = var["key"]
if var["type"] == "string":
value = var["value"]
elif var["type"] == "relative_paths":
value = os.pathsep.join(os.path.join(root, v) for v in var["values"])
else:
continue
if var.get("existing_value_treatment") == "prepend_to":
existing = os.environ.get(key, "")
if existing:
value = value + os.pathsep + existing
print(f"export {key}={shlex.quote(value)}")
PY
)"
if [ -z "$_ncs_env" ]; then
_ncs_warn "failed to read $NCS_TOOLCHAIN/environment.json"
else
eval "$_ncs_env"
# Some bundles declare this as "zephyr/gnu", which the Zephyr build does
# not accept; the SDK variant is plain "zephyr".
case "${ZEPHYR_TOOLCHAIN_VARIANT:-}" in
zephyr/*) export ZEPHYR_TOOLCHAIN_VARIANT=zephyr ;;
esac
export NCS_ROOT NCS_TOOLCHAIN
[ -n "${NCS_VERSION:-}" ] && export NCS_VERSION NCS_WORKSPACE="$NCS_ROOT/$NCS_VERSION"
# west resolves projects from the nearest .west found walking up from the
# current directory. A repo outside the SDK tree has none, so `west build`
# must run from inside the workspace with absolute -d and source paths.
[ -n "${NCS_WORKSPACE:-}" ] && export WEST_TOPDIR="$NCS_WORKSPACE"
export _NCS_TOOLCHAIN_APPLIED="$NCS_TOOLCHAIN"
fi
unset _ncs_env
fi
fi
unset -f _ncs_warn

31
src/main.c Normal file
View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#if defined(CONFIG_RCP_SAMPLE_HCI)
#include "rcp_hci.h"
#endif
LOG_MODULE_REGISTER(coprocessor_sample, CONFIG_OT_COPROCESSOR_LOG_LEVEL);
#define WELCOME_TEXT \
"\n\r" \
"\n\r" \
"=========================================================\n\r" \
"OpenThread Coprocessor application is now running on NCS.\n\r" \
"=========================================================\n\r"
int main(void)
{
LOG_INF(WELCOME_TEXT);
#if defined(CONFIG_RCP_SAMPLE_HCI)
run_hci();
#endif
return 0;
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
#include "nrf_802154_radio_wrapper.h"
#include <nrf_802154.h>
#ifdef CONFIG_NETWORKING
#include <zephyr/device.h>
#include <zephyr/net/ieee802154_radio.h>
static const struct device *const radio_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_ieee802154));
static struct ieee802154_radio_api *radio_api;
#else
static uint16_t get_capabilities(void)
{
nrf_802154_capabilities_t caps = nrf_802154_capabilities_get();
/* Convert nrf_802154_capabilities_t to ieee802154_hw_caps to keep backwards compatibility
* See @ref ieee802154_hw_caps in zephyr/net/ieee802154_radio.h for more details.
*/
return /* IEEE802154_HW_FCS */ BIT(1) |
/* IEEE802154_HW_PROMISC */ BIT(3) |
/* IEEE802154_HW_FILTER */ BIT(2) |
/* IEEE802154_HW_CSMA */ ((caps & NRF_802154_CAPABILITY_CSMA) ? BIT(4) : 0UL) |
/* IEEE802154_HW_TX_RX_ACK */ BIT(5) |
/* IEEE802154_HW_RX_TX_ACK */ BIT(7) |
/* IEEE802154_HW_ENERGY_SCAN */ BIT(0) |
/* IEEE802154_HW_TXTIME */
((caps & NRF_802154_CAPABILITY_DELAYED_TX) ? BIT(8) : 0UL) |
/* IEEE802154_HW_RXTIME */
((caps & NRF_802154_CAPABILITY_DELAYED_RX) ? BIT(10) : 0UL) |
/* IEEE802154_HW_SLEEP_TO_TX */ BIT(9) |
/* IEEE802154_HW_TX_SEC */ BIT(12) |
((caps & NRF_802154_CAPABILITY_SECURITY) ? BIT(11) : 0UL)
#if defined(CONFIG_IEEE802154_NRF5_MULTIPLE_CCA)
| /* IEEE802154_OPENTHREAD_HW_CAPS_BITS_START */ BIT(14)
#endif
#if defined(CONFIG_IEEE802154_SELECTIVE_TXCHANNEL)
| /* IEEE802154_HW_SELECTIVE_TXCHANNEL */ BIT(13)
#endif
#if defined(CONFIG_IEEE802154_NRF5_CST_ENDPOINT)
| /* IEEE802154_OPENTHREAD_HW_CAPS_BITS_START + 1*/ BIT(15)
#endif
;
}
#endif
bool nrf_802154_radio_wrapper_auto_ack_get(void)
{
return nrf_802154_auto_ack_get();
}
void nrf_802154_radio_wrapper_auto_ack_set(bool enabled)
{
nrf_802154_auto_ack_set(enabled);
}
uint16_t nrf_802154_radio_wrapper_hw_capabilities_get(void)
{
#ifdef CONFIG_NETWORKING
__ASSERT_NO_MSG(device_is_ready(radio_dev));
radio_api = (struct ieee802154_radio_api *)radio_dev->api;
__ASSERT_NO_MSG(radio_api != NULL);
return radio_api->get_capabilities(radio_dev);
#else
return get_capabilities();
#endif
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
#ifndef __NRF_802154_RADIO_WRAPPER_H__
#define __NRF_802154_RADIO_WRAPPER_H__
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Gets current automatic acknowledgments mode state.
*
* @return true when auto ACK is set, false otherwise
*/
bool nrf_802154_radio_wrapper_auto_ack_get(void);
/**
* Enables or disables the automatic acknowledgments mode (auto ACK).
*
* @param[in] enabled Value setting (true) or clearing (false)
* auto ACK radio mode.
*/
void nrf_802154_radio_wrapper_auto_ack_set(bool enabled);
/**
* Gets the device hardware capabilities
*
* @return 0 on failure; nonzero hw capabilities value
* on success (@ref ieee802154_hw_caps).
*/
uint16_t nrf_802154_radio_wrapper_hw_capabilities_get(void);
#ifdef __cplusplus
}
#endif
#endif /* __NRF_802154_RADIO_WRAPPER_H__ */

381
src/rcp_hci.c Normal file
View File

@@ -0,0 +1,381 @@
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <zephyr/arch/cpu.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/net_buf.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/l2cap.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/buf.h>
#include <zephyr/bluetooth/hci_raw.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(rcp_hci_module, CONFIG_OT_COPROCESSOR_LOG_LEVEL);
static const struct device *const hci_uart_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_bt_c2h_uart));
static K_THREAD_STACK_DEFINE(tx_thread_stack, CONFIG_BT_HCI_TX_STACK_SIZE);
static struct k_thread tx_thread_data;
static K_FIFO_DEFINE(tx_queue);
/* RX in terms of bluetooth communication */
static K_FIFO_DEFINE(uart_tx_queue);
#define H4_CMD 0x01
#define H4_ACL 0x02
#define H4_SCO 0x03
#define H4_EVT 0x04
#define H4_ISO 0x05
/* Receiver states. */
#define ST_IDLE 0 /* Waiting for packet type. */
#define ST_HDR 1 /* Receiving packet header. */
#define ST_PAYLOAD 2 /* Receiving packet payload. */
#define ST_DISCARD 3 /* Dropping packet. */
#define H4_DISCARD_LEN 33
static int h4_read(const struct device *uart, uint8_t *buf, size_t len)
{
int rx = uart_fifo_read(uart, buf, len);
LOG_DBG("read %d req %d", rx, len);
return rx;
}
static bool valid_type(uint8_t type)
{
return (type == H4_CMD) | (type == H4_ACL) | (type == H4_ISO);
}
/* Function expects that type is validated and only CMD, ISO or ACL will be used. */
static uint32_t get_len(const uint8_t *hdr_buf, uint8_t type)
{
switch (type) {
case H4_CMD:
return ((const struct bt_hci_cmd_hdr *)hdr_buf)->param_len;
case H4_ISO:
return bt_iso_hdr_len(
sys_le16_to_cpu(((const struct bt_hci_iso_hdr *)hdr_buf)->len));
case H4_ACL:
return sys_le16_to_cpu(((const struct bt_hci_acl_hdr *)hdr_buf)->len);
default:
LOG_ERR("Invalid type: %u", type);
return 0;
}
}
/* Function expects that type is validated and only CMD, ISO or ACL will be used. */
static int hdr_len(uint8_t type)
{
switch (type) {
case H4_CMD:
return sizeof(struct bt_hci_cmd_hdr);
case H4_ISO:
return sizeof(struct bt_hci_iso_hdr);
case H4_ACL:
return sizeof(struct bt_hci_acl_hdr);
default:
LOG_ERR("Invalid type: %u", type);
return 0;
}
}
static void rx_isr(void)
{
static struct net_buf *buf;
static int remaining;
static uint8_t state;
static uint8_t type;
static uint8_t hdr_buf[MAX(sizeof(struct bt_hci_cmd_hdr), sizeof(struct bt_hci_acl_hdr))];
int read;
do {
switch (state) {
case ST_IDLE:
/* Get packet type */
read = h4_read(hci_uart_dev, &type, sizeof(type));
/* since we read in loop until no data is in the fifo,
* it is possible that read = 0.
*/
if (read) {
if (valid_type(type)) {
/* Get expected header size and switch
* to receiving header.
*/
remaining = hdr_len(type);
state = ST_HDR;
} else {
LOG_WRN("Unknown header %d", type);
}
}
break;
case ST_HDR:
read = h4_read(hci_uart_dev, &hdr_buf[hdr_len(type) - remaining],
remaining);
remaining -= read;
if (remaining == 0) {
/* Header received. Allocate buffer and get
* payload length. If allocation fails leave
* interrupt. On failed allocation state machine
* is reset.
*/
buf = bt_buf_get_tx(bt_buf_type_from_h4(type, BT_BUF_OUT),
K_NO_WAIT, NULL, 0);
if (!buf) {
LOG_ERR("No available command buffers!");
state = ST_IDLE;
return;
}
remaining = get_len(hdr_buf, type);
net_buf_add_mem(buf, hdr_buf, hdr_len(type));
if (remaining > net_buf_tailroom(buf)) {
LOG_ERR("Not enough space in buffer");
net_buf_unref(buf);
state = ST_DISCARD;
} else {
state = ST_PAYLOAD;
}
}
break;
case ST_PAYLOAD:
read = h4_read(hci_uart_dev, net_buf_tail(buf), remaining);
buf->len += read;
remaining -= read;
if (remaining == 0) {
/* Packet received */
LOG_DBG("putting RX packet in queue.");
k_fifo_put(&tx_queue, buf);
state = ST_IDLE;
}
break;
case ST_DISCARD: {
uint8_t discard[H4_DISCARD_LEN];
size_t to_read = MIN(remaining, sizeof(discard));
read = h4_read(hci_uart_dev, discard, to_read);
remaining -= read;
if (remaining == 0) {
state = ST_IDLE;
}
break;
}
default:
read = 0;
__ASSERT_NO_MSG(0);
break;
}
} while (read);
}
static void tx_isr(void)
{
static struct net_buf *buf;
int len;
if (!buf) {
buf = k_fifo_get(&uart_tx_queue, K_NO_WAIT);
if (!buf) {
uart_irq_tx_disable(hci_uart_dev);
return;
}
}
len = uart_fifo_fill(hci_uart_dev, buf->data, buf->len);
net_buf_pull(buf, len);
if (!buf->len) {
net_buf_unref(buf);
buf = NULL;
}
}
static void bt_uart_isr(const struct device *unused, void *user_data)
{
ARG_UNUSED(unused);
ARG_UNUSED(user_data);
if (!(uart_irq_rx_ready(hci_uart_dev) || uart_irq_tx_ready(hci_uart_dev))) {
LOG_DBG("spurious interrupt");
}
if (uart_irq_tx_ready(hci_uart_dev)) {
tx_isr();
}
if (uart_irq_rx_ready(hci_uart_dev)) {
rx_isr();
}
}
static void tx_thread(void *p1, void *p2, void *p3)
{
while (1) {
struct net_buf *buf;
int err;
/* Wait until a buffer is available */
buf = k_fifo_get(&tx_queue, K_FOREVER);
/* Pass buffer to the stack */
err = bt_send(buf);
if (err) {
LOG_ERR("Unable to send (err %d)", err);
net_buf_unref(buf);
}
/* Give other threads a chance to run if tx_queue keeps getting
* new data all the time.
*/
k_yield();
}
}
static int h4_send(struct net_buf *buf)
{
LOG_DBG("buf %p type %u len %u", buf, buf->data[0], buf->len);
k_fifo_put(&uart_tx_queue, buf);
uart_irq_tx_enable(hci_uart_dev);
return 0;
}
#if defined(CONFIG_BT_CTLR_ASSERT_HANDLER)
void bt_ctlr_assert_handle(char *file, uint32_t line)
{
uint32_t len = 0U, pos = 0U;
/* Disable interrupts, this is unrecoverable */
(void)irq_lock();
uart_irq_rx_disable(hci_uart_dev);
uart_irq_tx_disable(hci_uart_dev);
if (file) {
while (file[len] != '\0') {
if (file[len] == '/') {
pos = len + 1;
}
len++;
}
file += pos;
len -= pos;
}
uart_poll_out(hci_uart_dev, H4_EVT);
/* Vendor-Specific debug event */
uart_poll_out(hci_uart_dev, 0xff);
/* 0xAA + strlen + \0 + 32-bit line number */
uart_poll_out(hci_uart_dev, 1 + len + 1 + 4);
uart_poll_out(hci_uart_dev, 0xAA);
if (len) {
while (*file != '\0') {
uart_poll_out(hci_uart_dev, *file);
file++;
}
uart_poll_out(hci_uart_dev, 0x00);
}
uart_poll_out(hci_uart_dev, line >> 0 & 0xff);
uart_poll_out(hci_uart_dev, line >> 8 & 0xff);
uart_poll_out(hci_uart_dev, line >> 16 & 0xff);
uart_poll_out(hci_uart_dev, line >> 24 & 0xff);
while (1) {
}
}
#endif /* CONFIG_BT_CTLR_ASSERT_HANDLER */
static int hci_uart_init(void)
{
LOG_DBG("");
if (!device_is_ready(hci_uart_dev)) {
LOG_ERR("HCI UART %s is not ready", hci_uart_dev->name);
return -EINVAL;
}
uart_irq_rx_disable(hci_uart_dev);
uart_irq_tx_disable(hci_uart_dev);
uart_irq_callback_set(hci_uart_dev, bt_uart_isr);
uart_irq_rx_enable(hci_uart_dev);
return 0;
}
void run_hci(void)
{
int err = 0;
__ASSERT(hci_uart_dev, "UART device is NULL");
/* incoming events and data from the controller */
static K_FIFO_DEFINE(rx_queue);
/* Enable the raw interface, this will in turn open the HCI driver */
bt_enable_raw(&rx_queue);
if (IS_ENABLED(CONFIG_BT_WAIT_NOP)) {
/* Issue a Command Complete with NOP */
int i;
const struct {
const uint8_t h4;
const struct bt_hci_evt_hdr hdr;
const struct bt_hci_evt_cmd_complete cc;
} __packed cc_evt = {
.h4 = H4_EVT,
.hdr = {
.evt = BT_HCI_EVT_CMD_COMPLETE,
.len = sizeof(struct bt_hci_evt_cmd_complete),
},
.cc = {
.ncmd = 1,
.opcode = sys_cpu_to_le16(BT_OP_NOP),
},
};
for (i = 0; i < sizeof(cc_evt); i++) {
uart_poll_out(hci_uart_dev, *(((const uint8_t *)&cc_evt) + i));
}
}
/* Spawn the TX thread and start feeding commands and data to the
* controller
*/
k_thread_create(&tx_thread_data, tx_thread_stack, K_THREAD_STACK_SIZEOF(tx_thread_stack),
tx_thread, NULL, NULL, NULL, K_PRIO_COOP(7), 0, K_NO_WAIT);
k_thread_name_set(&tx_thread_data, "HCI uart TX");
while (1) {
struct net_buf *buf;
buf = k_fifo_get(&rx_queue, K_FOREVER);
err = h4_send(buf);
if (err) {
LOG_ERR("Failed to send");
}
}
}
SYS_INIT(hci_uart_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEVICE);

12
src/rcp_hci.h Normal file
View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
#ifndef __RCP_HCI_H__
#define __RCP_HCI_H__
void run_hci(void);
#endif

157
src/user_vendor_hook.cpp Normal file
View File

@@ -0,0 +1,157 @@
/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/**
* @file
* This file shows how to implement the NCP vendor hook.
*/
#if OPENTHREAD_ENABLE_NCP_VENDOR_HOOK
#include "nrf_802154_radio_wrapper.h"
#include <zephyr/logging/log.h>
#include <ncp_base.hpp>
#include <ncp_hdlc.hpp>
#include <common/new.hpp>
#define VENDOR_SPINEL_PROP_VENDOR_NAME SPINEL_PROP_VENDOR__BEGIN
#define VENDOR_SPINEL_PROP_AUTO_ACK_ENABLED SPINEL_PROP_VENDOR__BEGIN + 1
#define VENDOR_SPINEL_PROP_HW_CAPABILITIES SPINEL_PROP_VENDOR__BEGIN + 2
LOG_MODULE_REGISTER(ncp_sample_vendor_hook, CONFIG_OT_COPROCESSOR_LOG_LEVEL);
namespace ot {
namespace Ncp {
otError NcpBase::VendorCommandHandler(uint8_t aHeader, unsigned int aCommand)
{
otError error = OT_ERROR_NONE;
LOG_DBG("VendorCommandHandler");
switch (aCommand)
{
// TODO: Implement your command handlers here.
default:
error = PrepareLastStatusResponse(aHeader, SPINEL_STATUS_INVALID_COMMAND);
}
return error;
}
void NcpBase::VendorHandleFrameRemovedFromNcpBuffer(Spinel::Buffer::FrameTag aFrameTag)
{
// This method is a callback which mirrors `NcpBase::HandleFrameRemovedFromNcpBuffer()`.
// It is called when a spinel frame is sent and removed from NCP buffer.
//
// (a) This can be used to track and verify that a vendor spinel frame response is
// delivered to the host (tracking the frame using its tag).
//
// (b) It indicates that NCP buffer space is now available (since a spinel frame is
// removed). This can be used to implement reliability mechanisms to re-send
// a failed spinel command response (or an async spinel frame) transmission
// (failed earlier due to NCP buffer being full).
OT_UNUSED_VARIABLE(aFrameTag);
}
otError NcpBase::VendorGetPropertyHandler(spinel_prop_key_t aPropKey)
{
otError error = OT_ERROR_NONE;
switch (aPropKey)
{
case VENDOR_SPINEL_PROP_VENDOR_NAME:
LOG_DBG("Got NAME get property request");
error = mEncoder.WriteUtf8("Nordic Seminconductor");
break;
case VENDOR_SPINEL_PROP_AUTO_ACK_ENABLED:
LOG_DBG("Got VENDOR_SPINEL_PROP_AUTO_ACK_ENABLED get property request");
error = mEncoder.WriteUint8(nrf_802154_radio_wrapper_auto_ack_get());
break;
case VENDOR_SPINEL_PROP_HW_CAPABILITIES:
LOG_DBG("Got VENDOR_SPINEL_PROP_HW_CAPABILITIES get property request");
error = mEncoder.WriteUint16(nrf_802154_radio_wrapper_hw_capabilities_get());
break;
// TODO: Implement your get properties handlers here.
default:
error = OT_ERROR_NOT_FOUND;
break;
}
return error;
}
otError NcpBase::VendorSetPropertyHandler(spinel_prop_key_t aPropKey)
{
otError error = OT_ERROR_NONE;
switch (aPropKey)
{
case VENDOR_SPINEL_PROP_AUTO_ACK_ENABLED: {
LOG_DBG("Got VENDOR_SPINEL_PROP_AUTO_ACK_ENABLED set property request");
uint8_t mode = 0;
SuccessOrExit(error = mDecoder.ReadUint8(mode));
nrf_802154_radio_wrapper_auto_ack_set(mode);
}
break;
// TODO: Implement your set properties handlers here.
default:
error = OT_ERROR_NOT_FOUND;
break;
}
exit:
return error;
}
} // namespace Ncp
} // namespace ot
//------------------------------------------------------------------
// When OPENTHREAD_ENABLE_NCP_VENDOR_HOOK is enabled, vendor code is
// expected to provide the `otNcpInit()` function. The reason behind
// this is to enable vendor code to define its own sub-class of
// `NcpBase` or `NcpHdlc`/`NcpSpi`.
//
// Example below show how to add a vendor sub-class over `NcpHdlc`.
class NcpVendorUart : public ot::Ncp::NcpHdlc
{
static int SendHdlc(const uint8_t *aBuf, uint16_t aBufLength)
{
return mSendCallback(aBuf, aBufLength);
}
public:
NcpVendorUart(ot::Instance *aInstance, otNcpHdlcSendCallback aSendCallback)
: ot::Ncp::NcpHdlc(aInstance, &NcpVendorUart::SendHdlc)
{
OT_ASSERT(aSendCallback);
mSendCallback = aSendCallback;
}
// Add public/private methods or member variables
private:
static otNcpHdlcSendCallback mSendCallback;
};
otNcpHdlcSendCallback NcpVendorUart::mSendCallback;
static OT_DEFINE_ALIGNED_VAR(sNcpVendorRaw, sizeof(NcpVendorUart), uint64_t);
extern "C" void otNcpHdlcInit(otInstance *aInstance, otNcpHdlcSendCallback aSendCallback)
{
NcpVendorUart *ncpVendor = NULL;
ot::Instance *instance = static_cast<ot::Instance *>(aInstance);
ncpVendor = new (&sNcpVendorRaw) NcpVendorUart(instance, aSendCallback);
}
#endif