diff --git a/arch/arm64/boot/dts/freescale/mt-connect.dts b/arch/arm64/boot/dts/freescale/mt-connect.dts index 49b02c88cba9d..949475cda4227 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 = <&gpio2>; + interrupts = <11 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,7 @@ }; &usbotg1 { + extcon = <&usb1_connector>; hnp-disable; srp-disable; adp-disable; @@ -533,6 +548,7 @@ }; &usbotg2 { + extcon = <&usb2_connector>; hnp-disable; srp-disable; adp-disable; @@ -710,7 +726,7 @@ >; }; - pinctrl_typec1: typec1grp { + pinctrl_stm_ipc: stmipcgrp { fsl,pins = < MX8MM_IOMUXC_SD1_STROBE_GPIO2_IO11 0x159 >; 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..6f3940402a067 --- /dev/null +++ b/drivers/extcon/extcon-stm32-ipc.c @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * drivers/extcon/extcon-stm32-ipc.c - 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; + +/* Parent SPI controller private structure */ +DECLARE_CRC8_TABLE(stm_crc8_table); +static struct dentry *stm_ipc_debugfs_dir; + +struct stm_ipc_priv { + struct spi_device *spi; + struct mutex lock; + struct dentry *debugfs_root; +}; + +/* Child connector platform device private structure */ +struct stm_connector_priv { + struct device *dev; + struct extcon_dev *edev; + u8 port_id; +}; + +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); + u8 *port_info = data; + u8 port_id = port_info[0]; + u8 state = port_info[1]; + + if (priv && priv->port_id == port_id) { + stm_ipc_update_state(dev, priv->edev, state); + return 1; /* Found and processed, stop iteration */ + } + 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); + if (ret < 0) { + dev_err_ratelimited(&priv->spi->dev, "SPI sync transfer failed: %d\n", ret); + goto out; + } + + /* 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); + goto out; + } + + /* 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); + goto out; + } + + /* 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); + goto out; + } + + /* Process USB event */ + if (rx_buf.type == MSG_TYPE_USB_EVENT) { + u8 port_info[2] = { rx_buf.port, 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, port_info, match_and_update_state); + if (ret == 0) { + dev_warn_ratelimited(&priv->spi->dev, "No matching USB connector found for Port %d\n", rx_buf.port); + } + } + +out: + mutex_unlock(&priv->lock); + 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(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 { + 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); + goto err_debugfs; + } + + /* Request Attention Pin Interrupt */ + if (spi->irq > 0) { + ret = devm_request_threaded_irq(&spi->dev, spi->irq, NULL, + stm_ipc_threaded_irq, + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + DRIVER_NAME, priv); + if (ret) { + dev_err(&spi->dev, "Failed to request threaded IRQ: %d\n", ret); + goto err_debugfs; + } + } + + dev_info(&spi->dev, "STM32 SPI IPC Core initialized\n"); + return 0; + +err_debugfs: + debugfs_remove_recursive(priv->debugfs_root); + return ret; +} + +static void stm_ipc_remove(struct spi_device *spi) +{ + struct stm_ipc_priv *priv = spi_get_drvdata(spi); + debugfs_remove_recursive(priv->debugfs_root); +} + +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, + .remove = stm_ipc_remove, +}; + +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");