Browse Source
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
5 changed files with 544 additions and 0 deletions
@ -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 |
@ -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); |
@ -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…
Reference in new issue