From a540ee6143e227f5f3388e7e5f4f2e2cfd86a2d1 Mon Sep 17 00:00:00 2001 From: Pieter De Gendt Date: Wed, 7 May 2025 14:29:48 +0200 Subject: [PATCH] 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 --- drivers/hdlc_rcp_if/CMakeLists.txt | 1 + drivers/hdlc_rcp_if/Kconfig | 1 + drivers/hdlc_rcp_if/Kconfig.spi | 34 ++ drivers/hdlc_rcp_if/hdlc_rcp_if_spi.c | 473 ++++++++++++++++++ dts/bindings/hdlc_rcp_if/spi,hdlc-rcp-if.yaml | 35 ++ 5 files changed, 544 insertions(+) create mode 100644 drivers/hdlc_rcp_if/Kconfig.spi create mode 100644 drivers/hdlc_rcp_if/hdlc_rcp_if_spi.c create mode 100644 dts/bindings/hdlc_rcp_if/spi,hdlc-rcp-if.yaml diff --git a/drivers/hdlc_rcp_if/CMakeLists.txt b/drivers/hdlc_rcp_if/CMakeLists.txt index eb325d2550e..1f4fcece2c2 100644 --- a/drivers/hdlc_rcp_if/CMakeLists.txt +++ b/drivers/hdlc_rcp_if/CMakeLists.txt @@ -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) diff --git a/drivers/hdlc_rcp_if/Kconfig b/drivers/hdlc_rcp_if/Kconfig index b16886a9def..b94b8d8b99b 100644 --- a/drivers/hdlc_rcp_if/Kconfig +++ b/drivers/hdlc_rcp_if/Kconfig @@ -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 diff --git a/drivers/hdlc_rcp_if/Kconfig.spi b/drivers/hdlc_rcp_if/Kconfig.spi new file mode 100644 index 00000000000..8ef9ca32013 --- /dev/null +++ b/drivers/hdlc_rcp_if/Kconfig.spi @@ -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 diff --git a/drivers/hdlc_rcp_if/hdlc_rcp_if_spi.c b/drivers/hdlc_rcp_if/hdlc_rcp_if_spi.c new file mode 100644 index 00000000000..5b4c905b22d --- /dev/null +++ b/drivers/hdlc_rcp_if/hdlc_rcp_if_spi.c @@ -0,0 +1,473 @@ +/** + * Copyright (c) 2025 Basalte bv + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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); diff --git a/dts/bindings/hdlc_rcp_if/spi,hdlc-rcp-if.yaml b/dts/bindings/hdlc_rcp_if/spi,hdlc-rcp-if.yaml new file mode 100644 index 00000000000..d9c0a3b5b43 --- /dev/null +++ b/dts/bindings/hdlc_rcp_if/spi,hdlc-rcp-if.yaml @@ -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.