Browse Source

drivers: hdlc_rcp_if: Add HDLC SPI adapter driver

Implement the HDLC over SPI adapter driver to support the Openthread RCP
with SPI communication.

Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
pull/89798/head
Pieter De Gendt 2 months ago committed by Anas Nashif
parent
commit
a540ee6143
  1. 1
      drivers/hdlc_rcp_if/CMakeLists.txt
  2. 1
      drivers/hdlc_rcp_if/Kconfig
  3. 34
      drivers/hdlc_rcp_if/Kconfig.spi
  4. 473
      drivers/hdlc_rcp_if/hdlc_rcp_if_spi.c
  5. 35
      dts/bindings/hdlc_rcp_if/spi,hdlc-rcp-if.yaml

1
drivers/hdlc_rcp_if/CMakeLists.txt

@ -3,4 +3,5 @@ @@ -3,4 +3,5 @@
zephyr_library()
zephyr_library_sources_ifdef(CONFIG_HDLC_RCP_IF_NXP hdlc_rcp_if_nxp.c)
zephyr_library_sources_ifdef(CONFIG_HDLC_RCP_IF_SPI hdlc_rcp_if_spi.c)
zephyr_library_sources_ifdef(CONFIG_HDLC_RCP_IF_UART hdlc_rcp_if_uart.c)

1
drivers/hdlc_rcp_if/Kconfig

@ -15,6 +15,7 @@ menuconfig HDLC_RCP_IF @@ -15,6 +15,7 @@ menuconfig HDLC_RCP_IF
if HDLC_RCP_IF
source "drivers/hdlc_rcp_if/Kconfig.nxp"
source "drivers/hdlc_rcp_if/Kconfig.spi"
source "drivers/hdlc_rcp_if/Kconfig.uart"
config HDLC_RCP_IF_DRV_NAME

34
drivers/hdlc_rcp_if/Kconfig.spi

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
# Configuration options for NXP HDLC RCP SPI communication Interface
# Copyright (c) 2025 Basalte bv
# SPDX-License-Identifier: Apache-2.0
config HDLC_RCP_IF_SPI
bool "SPI HDLC interface for Zephyr Openthread RCP host"
default y
depends on DT_HAS_SPI_HDLC_RCP_IF_ENABLED
depends on SPI
if HDLC_RCP_IF_SPI
config HDLC_RCP_IF_SPI_MAX_FRAME_SIZE
int "HDLC RCP IF SPI maximum frame size"
default 2048
help
Maximum frame size for the OpenThread HDLC host SPI.
config HDLC_RCP_IF_SPI_ALIGN_ALLOWANCE
int "HDLC RCP IF SPI RX align allowance"
default 0
range 0 16
help
Specify the maximum number of 0xFF bytes to clip from the start of MISO frames.
config HDLC_RCP_IF_SPI_SMALL_PACKET_SIZE
int "HDLC RCP IF SPI small packet size"
default 32
help
Specify the smallest packet we can receive in a single transaction.
Larger packets will require multiple transactions.
endif

473
drivers/hdlc_rcp_if/hdlc_rcp_if_spi.c

@ -0,0 +1,473 @@ @@ -0,0 +1,473 @@
/**
* Copyright (c) 2025 Basalte bv
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <openthread/platform/radio.h>
#include <zephyr/init.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/hdlc_rcp_if/hdlc_rcp_if.h>
#include <zephyr/net/ieee802154_radio.h>
#include <zephyr/net/openthread.h>
#include <zephyr/sys/crc.h>
#define DT_DRV_COMPAT spi_hdlc_rcp_if
LOG_MODULE_REGISTER(hdlc_rcp_if_spi, CONFIG_HDLC_RCP_IF_DRIVER_LOG_LEVEL);
#define SPI_HEADER_LEN 5
#define SPI_HEADER_RESET_FLAG 0x80
#define SPI_HEADER_CRC_FLAG 0x40
#define SPI_HEADER_PATTERN_VALUE 0x02
#define SPI_HEADER_PATTERN_MASK 0x03
#define HDLC_BYTE_FLAG 0x7E
#define HDLC_BYTE_ESC 0x7D
#define HDLC_BYTE_XON 0x11
#define HDLC_BYTE_XOFF 0x13
#define HDLC_BYTE_VENDOR 0xF8
#define HDLC_ESC_XOR 0x20
#define FCS_RESET 0xffff
#define FCS_CHECK 0xf0b8
#define BUFFER_SIZE \
(SPI_HEADER_LEN + CONFIG_HDLC_RCP_IF_SPI_MAX_FRAME_SIZE + \
CONFIG_HDLC_RCP_IF_SPI_ALIGN_ALLOWANCE)
struct hdlc_rcp_if_spi_config {
struct spi_dt_spec bus;
struct gpio_dt_spec int_gpio;
struct gpio_dt_spec rst_gpio;
uint16_t reset_time;
uint16_t reset_delay;
};
struct hdlc_rcp_if_spi_data {
const struct device *self;
struct k_work work;
struct gpio_callback int_gpio_cb;
hdlc_rx_callback_t rx_cb;
void *rx_param;
uint8_t rx_buf[BUFFER_SIZE];
uint16_t rx_len;
uint8_t tx_buf[BUFFER_SIZE];
uint16_t tx_len;
bool tx_escaped;
uint16_t tx_fcs;
bool tx_ready;
bool frame_sent;
};
BUILD_ASSERT(CONFIG_HDLC_RCP_IF_SPI_SMALL_PACKET_SIZE <=
CONFIG_HDLC_RCP_IF_SPI_MAX_FRAME_SIZE - SPI_HEADER_LEN,
"HDLC IF SPI small packet size larger than maximum frame size");
static bool hdlc_byte_needs_escape(uint8_t byte)
{
switch (byte) {
case HDLC_BYTE_VENDOR:
case HDLC_BYTE_ESC:
case HDLC_BYTE_FLAG:
case HDLC_BYTE_XOFF:
case HDLC_BYTE_XON:
return true;
default:
break;
}
return false;
}
static int hdlc_rcp_if_spi_reset(const struct device *dev)
{
const struct hdlc_rcp_if_spi_config *cfg = dev->config;
if (cfg->rst_gpio.port != NULL) {
int ret;
if (!gpio_is_ready_dt(&cfg->rst_gpio)) {
return -ENODEV;
}
ret = gpio_pin_configure_dt(&cfg->rst_gpio, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
return ret;
}
k_msleep(cfg->reset_time);
ret = gpio_pin_set_dt(&cfg->rst_gpio, 0);
if (ret < 0) {
return ret;
}
k_msleep(cfg->reset_delay);
}
return 0;
}
static void hdlc_rcp_if_rx_cb(struct hdlc_rcp_if_spi_data *data, uint8_t *buf, uint16_t len)
{
uint8_t esc_buf[] = {HDLC_BYTE_ESC, 0x00};
uint16_t idx = 0;
/* Optimize a bit by sending un-escaped chunks at once */
while (len > 0 && idx < len) {
if (!hdlc_byte_needs_escape(buf[idx])) {
idx++;
continue;
}
if (idx > 0) {
data->rx_cb(buf, idx, data->rx_param);
}
esc_buf[1] = buf[idx] ^ HDLC_ESC_XOR;
data->rx_cb(esc_buf, sizeof(esc_buf), data->rx_param);
len -= idx + 1;
buf += idx + 1;
idx = 0;
}
/* Remainder chunk */
if (len > 0) {
data->rx_cb(buf, len, data->rx_param);
}
}
static void hdlc_rcp_if_push_pull_spi(struct k_work *work)
{
struct hdlc_rcp_if_spi_data *data = CONTAINER_OF(work, struct hdlc_rcp_if_spi_data, work);
const struct hdlc_rcp_if_spi_config *cfg = data->self->config;
struct spi_buf tx_frame = {
.buf = data->tx_buf,
.len = data->tx_len + SPI_HEADER_LEN,
};
const struct spi_buf_set tx_set = {
.buffers = &tx_frame,
.count = 1U,
};
struct spi_buf rx_frame = {
.buf = data->rx_buf,
.len = SPI_HEADER_LEN + CONFIG_HDLC_RCP_IF_SPI_ALIGN_ALLOWANCE,
};
const struct spi_buf_set rx_set = {
.buffers = &rx_frame,
.count = 1U,
};
uint8_t *rx_buf;
uint16_t peer_max_rx;
uint16_t fcs;
int ret;
data->tx_buf[0] = SPI_HEADER_PATTERN_VALUE;
if (!data->frame_sent) {
data->tx_buf[0] |= SPI_HEADER_RESET_FLAG;
}
sys_put_le16(0U, &data->tx_buf[1]);
sys_put_le16(data->tx_len, &data->tx_buf[3]);
if (data->rx_len > 0U) {
rx_frame.len += data->rx_len;
sys_put_le16(data->rx_len, &data->tx_buf[1]);
} else {
rx_frame.len += CONFIG_HDLC_RCP_IF_SPI_SMALL_PACKET_SIZE;
sys_put_le16(CONFIG_HDLC_RCP_IF_SPI_SMALL_PACKET_SIZE, &data->tx_buf[1]);
}
LOG_HEXDUMP_DBG(data->tx_buf, SPI_HEADER_LEN, "TX Header");
LOG_HEXDUMP_DBG(&data->tx_buf[SPI_HEADER_LEN], data->tx_len, "TX Data");
ret = spi_transceive_dt(&cfg->bus, &tx_set, &rx_set);
if (ret < 0) {
LOG_ERR("Failed to push/pull frames (%d)", ret);
goto end;
}
/* Find the real start of the frame */
rx_buf = data->rx_buf;
for (uint8_t i = 0U; i < CONFIG_HDLC_RCP_IF_SPI_ALIGN_ALLOWANCE && rx_frame.len > 0U; ++i) {
if (rx_buf[0] != 0xff) {
break;
}
rx_buf++;
rx_frame.len--;
}
if ((rx_buf[0] & SPI_HEADER_PATTERN_MASK) != SPI_HEADER_PATTERN_VALUE) {
LOG_HEXDUMP_WRN(rx_buf, SPI_HEADER_LEN, "Invalid header data");
goto end;
}
data->frame_sent = true;
peer_max_rx = sys_get_le16(&rx_buf[1]);
data->rx_len = sys_get_le16(&rx_buf[3]);
if (data->tx_len > peer_max_rx) {
LOG_WRN("Peer not ready to receive our frame (%u > %u)", data->tx_len, peer_max_rx);
}
LOG_HEXDUMP_DBG(rx_buf, SPI_HEADER_LEN, "RX Header");
if (data->rx_len + SPI_HEADER_LEN > rx_frame.len || data->rx_len == 0U) {
/* Frame empty or incomplete */
goto end;
}
LOG_HEXDUMP_DBG(&rx_buf[SPI_HEADER_LEN], data->rx_len, "RX Data");
if (peer_max_rx > CONFIG_HDLC_RCP_IF_SPI_MAX_FRAME_SIZE ||
data->rx_len > CONFIG_HDLC_RCP_IF_SPI_MAX_FRAME_SIZE) {
LOG_HEXDUMP_WRN(rx_buf, SPI_HEADER_LEN, "Invalid accept/data lengths");
data->rx_len = 0;
goto end;
}
if ((rx_buf[0] & SPI_HEADER_RESET_FLAG) != 0U) {
LOG_DBG("Peer did reset");
if (data->rx_cb != NULL) {
uint8_t rst_buf[] = {HDLC_BYTE_FLAG, 0x13, 0x11, HDLC_BYTE_FLAG};
data->rx_cb(rst_buf, sizeof(rst_buf), data->rx_param);
}
}
fcs = crc16_ccitt(FCS_RESET, &rx_buf[SPI_HEADER_LEN], data->rx_len);
fcs ^= FCS_RESET;
if ((rx_buf[0] & SPI_HEADER_CRC_FLAG) != 0U) {
if (fcs != sys_get_le16(&rx_buf[SPI_HEADER_LEN + data->rx_len])) {
LOG_WRN("Invalid CRC");
data->rx_len = 0U;
goto end;
}
}
if (data->rx_cb != NULL) {
uint8_t tmp[2] = {HDLC_BYTE_FLAG, 0x00};
/* Start HDLC */
data->rx_cb(tmp, 1, data->rx_param);
hdlc_rcp_if_rx_cb(data, &rx_buf[SPI_HEADER_LEN], data->rx_len);
sys_put_le16(fcs, tmp);
hdlc_rcp_if_rx_cb(data, tmp, sizeof(tmp));
/* End HDLC */
tmp[0] = HDLC_BYTE_FLAG;
data->rx_cb(tmp, 1, data->rx_param);
}
data->rx_len = 0;
end:
data->tx_ready = true;
data->tx_len = 0;
}
static void hdlc_rcp_if_spi_isr(const struct device *port, struct gpio_callback *cb,
gpio_port_pins_t pins)
{
struct hdlc_rcp_if_spi_data *data =
CONTAINER_OF(cb, struct hdlc_rcp_if_spi_data, int_gpio_cb);
ARG_UNUSED(port);
ARG_UNUSED(pins);
k_work_submit(&data->work);
}
static void hdlc_iface_init(struct net_if *iface)
{
otExtAddress eui64;
__ASSERT_NO_MSG(DEVICE_DT_INST_GET(0) == net_if_get_device(iface));
ieee802154_init(iface);
otPlatRadioGetIeeeEui64(openthread_get_default_instance(), eui64.m8);
net_if_set_link_addr(iface, eui64.m8, OT_EXT_ADDRESS_SIZE, NET_LINK_IEEE802154);
}
static int hdlc_register_rx_cb(hdlc_rx_callback_t hdlc_rx_callback, void *param)
{
const struct device *dev = DEVICE_DT_INST_GET(0);
struct hdlc_rcp_if_spi_data *data = dev->data;
data->rx_cb = hdlc_rx_callback;
data->rx_param = param;
return 0;
}
static int hdlc_send(const uint8_t *frame, uint16_t length)
{
const struct device *dev = DEVICE_DT_INST_GET(0);
struct hdlc_rcp_if_spi_data *data = dev->data;
if (frame == NULL) {
return -EINVAL;
}
if (!data->tx_ready) {
return -EBUSY;
}
for (uint16_t i = 0; i < length; ++i) {
uint8_t byte = frame[i];
if (data->tx_len >= BUFFER_SIZE - SPI_HEADER_LEN) {
data->tx_escaped = false;
data->tx_len = 0;
data->tx_fcs = FCS_RESET;
return -ENOMEM;
}
if (byte == HDLC_BYTE_FLAG) {
if (data->tx_len <= 2) {
/* Start of frame */
data->tx_escaped = false;
data->tx_len = 0;
data->tx_fcs = FCS_RESET;
continue;
}
if (data->tx_fcs != FCS_CHECK) {
LOG_ERR("Invalid HDLC CRC 0x%04x for length %u", data->tx_fcs,
data->tx_len);
data->tx_escaped = false;
data->tx_len = 0;
data->tx_fcs = FCS_RESET;
continue;
}
if (i != length - 1) {
LOG_WRN("Dropped %d bytes", length - i);
}
/* Frame ready (strip CRC length) */
data->tx_len -= 2U;
data->tx_ready = false;
/* Reset for next */
data->tx_fcs = FCS_RESET;
data->tx_escaped = false;
break;
}
if (byte == HDLC_BYTE_ESC) {
data->tx_escaped = true;
continue;
}
if (hdlc_byte_needs_escape(byte)) {
continue;
}
if (data->tx_escaped) {
byte ^= HDLC_ESC_XOR;
data->tx_escaped = false;
}
data->tx_fcs = crc16_ccitt(data->tx_fcs, &byte, 1);
data->tx_buf[SPI_HEADER_LEN + data->tx_len++] = byte;
}
k_work_submit(&data->work);
return 0;
}
static int hdlc_deinit(void)
{
const struct device *dev = DEVICE_DT_INST_GET(0);
struct hdlc_rcp_if_spi_data *data = dev->data;
data->frame_sent = false;
return 0;
}
static int hdlc_rcp_if_spi_init(const struct device *dev)
{
const struct hdlc_rcp_if_spi_config *cfg = dev->config;
struct hdlc_rcp_if_spi_data *data = dev->data;
int ret;
data->self = dev;
data->tx_ready = true;
k_work_init(&data->work, hdlc_rcp_if_push_pull_spi);
if (!spi_is_ready_dt(&cfg->bus)) {
LOG_ERR("SPI bus not ready");
return -ENODEV;
}
if (!gpio_is_ready_dt(&cfg->int_gpio)) {
LOG_ERR("Interrupt GPIO not ready");
return -ENODEV;
}
ret = gpio_pin_configure_dt(&cfg->int_gpio, GPIO_INPUT);
if (ret < 0) {
LOG_ERR("Failed to configure interrupt GPIO pin (%d)", ret);
return ret;
}
ret = gpio_pin_interrupt_configure_dt(&cfg->int_gpio, GPIO_INT_EDGE_TO_ACTIVE);
if (ret < 0) {
LOG_ERR("Failed to configure interrupt GPIO (%d)", ret);
return ret;
}
gpio_init_callback(&data->int_gpio_cb, hdlc_rcp_if_spi_isr, BIT(cfg->int_gpio.pin));
ret = gpio_add_callback(cfg->int_gpio.port, &data->int_gpio_cb);
if (ret < 0) {
LOG_ERR("Failed to add interrupt GPIO callback (%d)", ret);
return ret;
}
ret = hdlc_rcp_if_spi_reset(dev);
if (ret < 0) {
LOG_ERR("Failed to reset HDLC SPI device (%d)", ret);
}
return 0;
}
static const struct hdlc_api spi_hdlc_api = {
.iface_api.init = hdlc_iface_init,
.register_rx_cb = hdlc_register_rx_cb,
.send = hdlc_send,
.deinit = hdlc_deinit,
};
#define L2_CTX_TYPE NET_L2_GET_CTX_TYPE(OPENTHREAD_L2)
#define MTU 1280
static const struct hdlc_rcp_if_spi_config ot_hdlc_rcp_cfg = {
.bus = SPI_DT_SPEC_INST_GET(0, SPI_OP_MODE_MASTER | SPI_WORD_SET(8),
DT_INST_PROP(0, cs_delay)),
.int_gpio = GPIO_DT_SPEC_INST_GET(0, int_gpios),
.rst_gpio = GPIO_DT_SPEC_INST_GET_OR(0, reset_gpios, {}),
.reset_time = DT_INST_PROP(0, reset_assert_time),
.reset_delay = DT_INST_PROP(0, reset_delay),
};
static struct hdlc_rcp_if_spi_data ot_hdlc_rcp_data;
NET_DEVICE_DT_INST_DEFINE(0, hdlc_rcp_if_spi_init, NULL, &ot_hdlc_rcp_data, &ot_hdlc_rcp_cfg,
CONFIG_KERNEL_INIT_PRIORITY_DEVICE, &spi_hdlc_api, OPENTHREAD_L2,
NET_L2_GET_CTX_TYPE(OPENTHREAD_L2), MTU);

35
dts/bindings/hdlc_rcp_if/spi,hdlc-rcp-if.yaml

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
# Copyright (c) 2025 Basalte bv
# SPDX-License-Identifier: Apache-2.0
description: SPI HDLC RCP interface node
compatible: "spi,hdlc-rcp-if"
include: spi-device.yaml
properties:
int-gpios:
type: phandle-array
required: true
description: |
Interrupt GPIO. Used by the RCP to signal data is available. Active low.
reset-gpios:
type: phandle-array
description: |
Reset GPIO. Used to assert a hardware reset for the RCP. Active low.
reset-assert-time:
type: int
default: 30
description: The time (in ms) to keep the RCP radio reset active.
reset-delay:
type: int
default: 0
description: The delay (in ms) after reset de-assertion.
cs-delay:
type: int
default: 0
description: The delay (in ms) for the RCP radio chip select.
Loading…
Cancel
Save