diff --git a/Documentation/devicetree/bindings/extcon/multitracks,stm32-spi-ipc.yaml b/Documentation/devicetree/bindings/extcon/multitracks,stm32-spi-ipc.yaml new file mode 100644 index 0000000000000..5d23c67d39c12 --- /dev/null +++ b/Documentation/devicetree/bindings/extcon/multitracks,stm32-spi-ipc.yaml @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/extcon/multitracks,stm32-spi-ipc.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: MultiTracks STM32 SPI IPC USB extcon provider + +maintainers: + - Samuel Morris + +description: | + An SPI client that talks to an external STM32 co-processor. The STM32 reports + USB cable/role events (disconnected, peripheral or host) for each physical USB + port over SPI, signalled by a dedicated attention (interrupt) line. + + Each USB port is described by a child connector node that acts as an extcon + provider (EXTCON_USB / EXTCON_USB_HOST) for the USB OTG controller wired to + that port, allowing it to switch between host and peripheral roles. + +properties: + compatible: + const: multitracks,stm32-spi-ipc + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + description: + Attention line, driven by the STM32 co-processor to signal that a new + event packet is pending on the SPI bus. + +patternProperties: + '^connector-usb[0-9]+$': + type: object + description: + A virtual USB connector exposed as an extcon provider. Reference it from + the "extcon" property of the corresponding USB OTG controller node. + + properties: + compatible: + const: multitracks,stm32-usb-connector + + port-id: + $ref: /schemas/types.yaml#/definitions/uint32 + maximum: 255 + description: + Identifier of the USB port on the STM32 co-processor that this + connector represents. It is matched against the "port" field of the + incoming SPI event packet. + + required: + - compatible + - port-id + + additionalProperties: false + +required: + - compatible + - reg + - interrupts + +allOf: + - $ref: /schemas/spi/spi-peripheral-props.yaml# + +unevaluatedProperties: false + +examples: + - | + #include + + spi { + #address-cells = <1>; + #size-cells = <0>; + + ipc@0 { + compatible = "multitracks,stm32-spi-ipc"; + reg = <0>; + spi-max-frequency = <10000000>; + interrupt-parent = <&gpio2>; + interrupts = <11 IRQ_TYPE_EDGE_FALLING>; + + connector-usb1 { + compatible = "multitracks,stm32-usb-connector"; + port-id = <0>; + }; + + connector-usb2 { + compatible = "multitracks,stm32-usb-connector"; + port-id = <1>; + }; + }; + }; +... diff --git a/arch/arm64/boot/dts/freescale/mt-connect.dts b/arch/arm64/boot/dts/freescale/mt-connect.dts index 49b02c88cba9d..9e7dbcf3bfa4d 100644 --- a/arch/arm64/boot/dts/freescale/mt-connect.dts +++ b/arch/arm64/boot/dts/freescale/mt-connect.dts @@ -495,15 +495,29 @@ }; &ecspi2 { - status = "okay"; + #address-cells = <1>; + #size-cells = <0>; pinctrl-names = "default"; - pinctrl-0 = <&pinctrl_ecspi2>; + pinctrl-0 = <&pinctrl_ecspi2 &pinctrl_stm_ipc>; cs-gpios = <&gpio5 13 GPIO_ACTIVE_LOW>; + status = "okay"; - spidev1: spi@0 { + stm_ipc: stm32-ipc@0 { + compatible = "multitracks,stm32-spi-ipc"; reg = <0>; - compatible = "rohm,dh2228fv"; - spi-max-frequency = <500000>; + spi-max-frequency = <10000000>; + interrupt-parent = <&gpio4>; + interrupts = <29 IRQ_TYPE_EDGE_FALLING>; + + usb1_connector: connector-usb1 { + compatible = "multitracks,stm32-usb-connector"; + port-id = <0>; + }; + + usb2_connector: connector-usb2 { + compatible = "multitracks,stm32-usb-connector"; + port-id = <1>; + }; }; }; @@ -520,6 +534,14 @@ }; &usbotg1 { + /* + * ChipIdea reads extcon phandle 0 as the VBUS (EXTCON_USB) source and + * phandle 1 as the ID (EXTCON_USB_HOST) source. The connector provides + * both cable types, so point both indices at it; otherwise the ID/host + * notifier is never registered by ci_extcon_register() and host-role + * events are dropped. + */ + extcon = <&usb1_connector>, <&usb1_connector>; hnp-disable; srp-disable; adp-disable; @@ -533,6 +555,8 @@ }; &usbotg2 { + /* Both extcon indices point at the connector; see &usbotg1. */ + extcon = <&usb2_connector>, <&usb2_connector>; hnp-disable; srp-disable; adp-disable; @@ -710,9 +734,9 @@ >; }; - pinctrl_typec1: typec1grp { + pinctrl_stm_ipc: stmipcgrp { fsl,pins = < - MX8MM_IOMUXC_SD1_STROBE_GPIO2_IO11 0x159 + MX8MM_IOMUXC_SAI3_RXC_GPIO4_IO29 0x82 /* STM IPC attention (ECSPI2 INT) */ >; }; @@ -735,9 +759,7 @@ MX8MM_IOMUXC_ECSPI2_MISO_ECSPI2_MISO 0x82 MX8MM_IOMUXC_ECSPI2_MOSI_ECSPI2_MOSI 0x82 MX8MM_IOMUXC_ECSPI2_SCLK_ECSPI2_SCLK 0x82 - MX8MM_IOMUXC_ECSPI2_SS0_ECSPI2_SS0 0x82 - MX8MM_IOMUXC_ECSPI2_SS0_GPIO5_IO13 0x140 /* ECSPI2 NSS */ - MX8MM_IOMUXC_SAI3_RXC_GPIO4_IO29 0x82 /* ECSPI2 INT */ + MX8MM_IOMUXC_ECSPI2_SS0_GPIO5_IO13 0x140 /* ECSPI2 NSS (cs-gpios) */ >; }; diff --git a/arch/arm64/configs/mt_connect_defconfig b/arch/arm64/configs/mt_connect_defconfig index dd15ba66e2408..31e80d4eeb68d 100644 --- a/arch/arm64/configs/mt_connect_defconfig +++ b/arch/arm64/configs/mt_connect_defconfig @@ -654,6 +654,7 @@ CONFIG_FSL_QIXIS=y CONFIG_SOC_TI=y CONFIG_EXTCON_PTN5150=m CONFIG_EXTCON_USB_GPIO=y +CONFIG_EXTCON_STM32_SPI_IPC=y CONFIG_IIO=y CONFIG_FXLS8962AF_I2C=m CONFIG_IIO_ST_ACCEL_3AXIS=m diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig index cf472e44c5ff9..2681b3465c1b1 100644 --- a/drivers/extcon/Kconfig +++ b/drivers/extcon/Kconfig @@ -176,6 +176,15 @@ config EXTCON_USB_GPIO Say Y here to enable GPIO based USB cable detection extcon support. Used typically if GPIO is used for USB ID pin detection. +config EXTCON_STM32_SPI_IPC + tristate "STM32 SPI IPC and Virtual Extcon support" + depends on SPI && OF + select CRC8 + help + Say Y here to enable the STM32 SPI IPC and Virtual Extcon driver, + which handles USB connection/cable event notifications reported + by an external STM32 co-processor over SPI. + config EXTCON_USBC_CROS_EC tristate "ChromeOS Embedded Controller EXTCON support" depends on CROS_EC diff --git a/drivers/extcon/Makefile b/drivers/extcon/Makefile index 1b390d934ca92..b962d043fd804 100644 --- a/drivers/extcon/Makefile +++ b/drivers/extcon/Makefile @@ -23,5 +23,6 @@ obj-$(CONFIG_EXTCON_QCOM_SPMI_MISC) += extcon-qcom-spmi-misc.o obj-$(CONFIG_EXTCON_RT8973A) += extcon-rt8973a.o obj-$(CONFIG_EXTCON_SM5502) += extcon-sm5502.o obj-$(CONFIG_EXTCON_USB_GPIO) += extcon-usb-gpio.o +obj-$(CONFIG_EXTCON_STM32_SPI_IPC) += extcon-stm32-ipc.o obj-$(CONFIG_EXTCON_USBC_CROS_EC) += extcon-usbc-cros-ec.o obj-$(CONFIG_EXTCON_USBC_TUSB320) += extcon-usbc-tusb320.o diff --git a/drivers/extcon/extcon-stm32-ipc.c b/drivers/extcon/extcon-stm32-ipc.c new file mode 100644 index 0000000000000..a43d8a06e3916 --- /dev/null +++ b/drivers/extcon/extcon-stm32-ipc.c @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * STM32 SPI IPC and virtual extcon driver + * + * Copyright (C) 2026 MultiTracks + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_NAME "stm-spi-ipc" + +/* Protocol constants */ +#define STM_IPC_MSG_MAGIC 0x5A +#define MSG_TYPE_USB_EVENT 0x01 +#define STM_IPC_MSG_LEN_USB_EVENT 2 +#define STM_IPC_CRC8_POLYNOMIAL 0x07 + +enum stm_ipc_usb_state { + STM_IPC_USB_STATE_DISCONNECTED = 0, + STM_IPC_USB_STATE_PERIPHERAL = 1, + STM_IPC_USB_STATE_HOST = 2, +}; + +struct stm_ipc_packet { + u8 magic; + u8 type; + u8 length; + u8 port; + u8 state; /* enum stm_ipc_usb_state */ + u8 crc; +} __packed; + +/* Module-wide CRC-8 table and debugfs parent, shared by all instances */ +DECLARE_CRC8_TABLE(stm_crc8_table); +static struct dentry *stm_ipc_debugfs_dir; + +/* Parent SPI controller private structure */ +struct stm_ipc_priv { + struct spi_device *spi; + struct mutex lock; /* serialises SPI packet transfers */ + struct dentry *debugfs_root; +}; + +/* Child connector platform device private structure */ +struct stm_connector_priv { + struct device *dev; + struct extcon_dev *edev; + u8 port_id; +}; + +/* Payload handed to the device_for_each_child() match callback */ +struct stm_ipc_port_event { + u8 port; + u8 state; + bool handled; +}; + +static const unsigned int stm_usb_cable[] = { + EXTCON_USB, + EXTCON_USB_HOST, + EXTCON_NONE, +}; + +/* Helper to update extcon state based on STM32 reporting */ +static void stm_ipc_update_state(struct device *dev, struct extcon_dev *edev, u8 state) +{ + int usb_state = extcon_get_state(edev, EXTCON_USB); + int host_state = extcon_get_state(edev, EXTCON_USB_HOST); + bool want_usb = false; + bool want_host = false; + + if (usb_state < 0 || host_state < 0) { + dev_err_ratelimited(dev, "Failed to get current extcon state\n"); + return; + } + + switch (state) { + case STM_IPC_USB_STATE_DISCONNECTED: + want_usb = false; + want_host = false; + break; + case STM_IPC_USB_STATE_PERIPHERAL: + want_usb = true; + want_host = false; + break; + case STM_IPC_USB_STATE_HOST: + want_usb = false; + want_host = true; + break; + default: + /* Fallback to safe disconnected state on protocol error */ + want_usb = false; + want_host = false; + dev_warn_ratelimited(dev, "Invalid USB connector state: %u\n", state); + break; + } + + /* Only update states if they have actually changed, clearing opposite first */ + if (want_usb != usb_state || want_host != host_state) { + if (!want_usb && usb_state) + extcon_set_state_sync(edev, EXTCON_USB, false); + if (!want_host && host_state) + extcon_set_state_sync(edev, EXTCON_USB_HOST, false); + + if (want_usb && !usb_state) + extcon_set_state_sync(edev, EXTCON_USB, true); + if (want_host && !host_state) + extcon_set_state_sync(edev, EXTCON_USB_HOST, true); + } +} + +/* Callback used to find and update state of correct child platform device */ +static int match_and_update_state(struct device *dev, void *data) +{ + struct stm_connector_priv *priv = dev_get_drvdata(dev); + struct stm_ipc_port_event *event = data; + + if (priv && priv->port_id == event->port) { + stm_ipc_update_state(dev, priv->edev, event->state); + event->handled = true; + } + + /* + * Always return 0 so the return value of device_for_each_child() stays + * reserved for genuine errors; "was a matching connector found" is + * reported through event->handled instead. + */ + return 0; +} + +/* Threaded IRQ Handler (safe to perform sleeping SPI transfers) */ +static irqreturn_t stm_ipc_threaded_irq(int irq, void *dev_id) +{ + struct stm_ipc_priv *priv = dev_id; + struct stm_ipc_packet tx_buf = {0}; + struct stm_ipc_packet rx_buf = {0}; + struct spi_transfer t = { + .tx_buf = &tx_buf, + .rx_buf = &rx_buf, + .len = sizeof(struct stm_ipc_packet), + }; + struct spi_message m; + int ret; + u8 calc_crc; + + mutex_lock(&priv->lock); + + spi_message_init(&m); + spi_message_add_tail(&t, &m); + ret = spi_sync(priv->spi, &m); + + mutex_unlock(&priv->lock); + + if (ret < 0) { + dev_err_ratelimited(&priv->spi->dev, "SPI sync transfer failed: %d\n", ret); + return IRQ_HANDLED; + } + + /* Validate magic byte */ + if (rx_buf.magic != STM_IPC_MSG_MAGIC) { + dev_warn_ratelimited(&priv->spi->dev, "Invalid magic byte: 0x%02x\n", rx_buf.magic); + return IRQ_HANDLED; + } + + /* Validate packet CRC */ + calc_crc = crc8(stm_crc8_table, (const u8 *)&rx_buf, + offsetof(struct stm_ipc_packet, crc), 0); + if (calc_crc != rx_buf.crc) { + dev_warn_ratelimited(&priv->spi->dev, "CRC mismatch: read 0x%02x, calculated 0x%02x\n", + rx_buf.crc, calc_crc); + return IRQ_HANDLED; + } + + /* Validate packet payload length for USB events */ + if (rx_buf.type == MSG_TYPE_USB_EVENT && rx_buf.length != STM_IPC_MSG_LEN_USB_EVENT) { + dev_warn_ratelimited(&priv->spi->dev, "Invalid packet length: %u (expected %d)\n", + rx_buf.length, STM_IPC_MSG_LEN_USB_EVENT); + return IRQ_HANDLED; + } + + /* Process USB event */ + if (rx_buf.type == MSG_TYPE_USB_EVENT) { + struct stm_ipc_port_event event = { + .port = rx_buf.port, + .state = rx_buf.state, + }; + + dev_dbg(&priv->spi->dev, "USB Event from STM32 on Port %d: State %d\n", + rx_buf.port, rx_buf.state); + ret = device_for_each_child(&priv->spi->dev, &event, match_and_update_state); + if (ret < 0) + dev_warn_ratelimited(&priv->spi->dev, "Failed to dispatch event for Port %d: %d\n", + rx_buf.port, ret); + else if (!event.handled) + dev_warn_ratelimited(&priv->spi->dev, "No matching USB connector found for Port %d\n", + rx_buf.port); + } + + return IRQ_HANDLED; +} + +/* Debugfs Simulation Entry (for mocking events) */ +static int stm_ipc_sim_write(void *data, u64 val) +{ + struct stm_connector_priv *priv = data; + + if (val > STM_IPC_USB_STATE_HOST) + return -EINVAL; + + stm_ipc_update_state(priv->dev, priv->edev, (u8)val); + return 0; +} +DEFINE_DEBUGFS_ATTRIBUTE(stm_ipc_sim_fops, NULL, stm_ipc_sim_write, "%llu\n"); + +static void stm_ipc_debugfs_cleanup(void *data) +{ + struct dentry *dentry = data; + + debugfs_remove_recursive(dentry); +} + +/* Child Connector Platform Device Probe */ +static int stm_usb_connector_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct stm_connector_priv *priv; + struct stm_ipc_priv *parent_priv; + char name[32]; + u32 val; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->dev = dev; + + ret = of_property_read_u32(dev->of_node, "port-id", &val); + if (ret) { + dev_err(dev, "Missing 'port-id' property\n"); + return ret; + } + if (val > 255) { + dev_err(dev, "Invalid 'port-id' value: %u (must be <= 255)\n", val); + return -EINVAL; + } + priv->port_id = (u8)val; + + priv->edev = devm_extcon_dev_allocate(dev, stm_usb_cable); + if (IS_ERR(priv->edev)) + return PTR_ERR(priv->edev); + + ret = devm_extcon_dev_register(dev, priv->edev); + if (ret) { + dev_err(dev, "Failed to register extcon device: %d\n", ret); + return ret; + } + + platform_set_drvdata(pdev, priv); + + /* Set up DebugFS for this virtual connector for runtime simulation */ + parent_priv = dev_get_drvdata(dev->parent); + if (parent_priv && parent_priv->debugfs_root) { + struct dentry *sim_file; + + snprintf(name, sizeof(name), "usb%d_sim", priv->port_id + 1); + sim_file = debugfs_create_file(name, 0200, parent_priv->debugfs_root, + priv, &stm_ipc_sim_fops); + if (!IS_ERR_OR_NULL(sim_file)) { + ret = devm_add_action_or_reset(dev, stm_ipc_debugfs_cleanup, sim_file); + if (ret) { + dev_err(dev, "Failed to register debugfs cleanup action: %d\n", + ret); + return ret; + } + } + } + + dev_info(dev, "Registered virtual USB connector on Port %d\n", priv->port_id); + return 0; +} + +static const struct of_device_id stm_usb_connector_of_match[] = { + { .compatible = "multitracks,stm32-usb-connector" }, + { } +}; +MODULE_DEVICE_TABLE(of, stm_usb_connector_of_match); + +static struct platform_driver stm_usb_connector_driver = { + .probe = stm_usb_connector_probe, + .driver = { + .name = "stm32-usb-connector", + .of_match_table = stm_usb_connector_of_match, + }, +}; + +/* Parent SPI Driver Probe */ +static int stm_ipc_probe(struct spi_device *spi) +{ + struct stm_ipc_priv *priv; + int ret; + + priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->spi = spi; + mutex_init(&priv->lock); + spi_set_drvdata(spi, priv); + + /* Initialize DebugFS subdirectory under /sys/kernel/debug/stm_ipc/ */ + if (stm_ipc_debugfs_dir) { + priv->debugfs_root = debugfs_create_dir(dev_name(&spi->dev), stm_ipc_debugfs_dir); + if (IS_ERR_OR_NULL(priv->debugfs_root)) { + dev_warn(&spi->dev, "Failed to create debugfs root directory\n"); + priv->debugfs_root = NULL; + } else { + /* + * Tear the directory down via devres. Registering the + * action *before* devm_of_platform_populate() means it + * runs *after* the child connectors are depopulated (LIFO + * devres order), so each connector removes its own sim + * file first and this only reaps the now-empty directory. + * That avoids the double-free that a direct + * debugfs_remove_recursive() here would cause against the + * connectors' per-file cleanup. + */ + ret = devm_add_action_or_reset(&spi->dev, + stm_ipc_debugfs_cleanup, + priv->debugfs_root); + if (ret) + return ret; + } + } else { + priv->debugfs_root = NULL; + } + + /* Populate subnodes as platform devices (this probes the connector sub-drivers) */ + ret = devm_of_platform_populate(&spi->dev); + if (ret) { + dev_err(&spi->dev, "Failed to populate child connectors: %d\n", ret); + return ret; + } + + /* + * The attention line is mandatory: without it no STM32 events are ever + * read and the IPC path is silently dead, so refuse to probe rather than + * come up looking healthy. The trigger type is taken from the device tree + * ("interrupts"); only IRQF_ONESHOT is requested here to keep the line + * masked until the threaded handler completes. + */ + if (spi->irq <= 0) { + dev_err(&spi->dev, "Missing attention interrupt\n"); + return spi->irq < 0 ? spi->irq : -ENXIO; + } + + ret = devm_request_threaded_irq(&spi->dev, spi->irq, NULL, + stm_ipc_threaded_irq, + IRQF_ONESHOT, + DRIVER_NAME, priv); + if (ret) { + dev_err(&spi->dev, "Failed to request threaded IRQ: %d\n", ret); + return ret; + } + + dev_info(&spi->dev, "STM32 SPI IPC Core initialized\n"); + return 0; +} + +static const struct of_device_id stm_ipc_of_match[] = { + { .compatible = "multitracks,stm32-spi-ipc" }, + { } +}; +MODULE_DEVICE_TABLE(of, stm_ipc_of_match); + +static struct spi_driver stm_ipc_driver = { + .driver = { + .name = DRIVER_NAME, + .of_match_table = stm_ipc_of_match, + }, + .probe = stm_ipc_probe, +}; + +static int __init stm_ipc_init(void) +{ + int ret; + + /* Setup CRC8 lookup table (polynomial 0x07) once */ + crc8_populate_msb(stm_crc8_table, STM_IPC_CRC8_POLYNOMIAL); + + /* Setup global debugfs parent directory */ + stm_ipc_debugfs_dir = debugfs_create_dir("stm_ipc", NULL); + if (IS_ERR_OR_NULL(stm_ipc_debugfs_dir)) + stm_ipc_debugfs_dir = NULL; + + ret = platform_driver_register(&stm_usb_connector_driver); + if (ret) + goto err_debugfs; + + ret = spi_register_driver(&stm_ipc_driver); + if (ret) { + platform_driver_unregister(&stm_usb_connector_driver); + goto err_debugfs; + } + + return 0; + +err_debugfs: + debugfs_remove_recursive(stm_ipc_debugfs_dir); + return ret; +} + +static void __exit stm_ipc_exit(void) +{ + spi_unregister_driver(&stm_ipc_driver); + platform_driver_unregister(&stm_usb_connector_driver); + debugfs_remove_recursive(stm_ipc_debugfs_dir); +} + +module_init(stm_ipc_init); +module_exit(stm_ipc_exit); + +MODULE_AUTHOR("Samuel Morris "); +MODULE_DESCRIPTION("STM32 SPI IPC and virtual Extcon Driver"); +MODULE_LICENSE("GPL v2");