* [PATCH v2 0/4] arm: ras: Add DT frontend support for ARM RAS
From: Umang Chheda @ 2026-07-20 8:19 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
Konrad Dybcio, Ruidong Tian, Tony Luck, Borislav Petkov,
Umang Chheda
Cc: devicetree, linux-kernel, linux-arm-msm, linux-acpi,
linux-arm-kernel, linux-edac, faruque.ansari, avaneesh.dwivedi
This series adds Device Tree support for the ARM64 RAS driver
introduced by Ruidong Tian [1]. The existing driver supports ACPI-based
platforms via the AEST table, this series extends it to DT-based
platforms without any changes to the core driver.
How to test with QEMU
--------------------------
Tian Ruidong's QEMU fork [2] emulates AEST MMIO error records on the
virt machine. To test the DT frontend:
1. Build QEMU:
git clone https://github.com/winterddd/qemu.git
cd qemu
git checkout c5e2d5dec9fd62ba622314c40bff0fbecb4dfb34
./configure --target-list=aarch64-softmmu
make -j$(nproc)
2. Build the kernel with:
CONFIG_ARM64_RAS_EXTN=y
CONFIG_RAS=y
CONFIG_ACPI_AEST=y
CONFIG_ARM64_RAS_DRIVER=y
CONFIG_ARM64_RAS_DT=y
3. Add the following DT node to your virt machine DTB. The QEMU
fork maps GIC error source at 0x017a00000 (SPI 0x0d).
ras-gic-dist@17a00000 {
compatible = "arm,ras-gic";
reg = <0x0 0x17a00000 0x0 0x10000>;
reg-names = "err-group";
arm,group-format = <ARM_RAS_GROUP_4K>;
arm,num-records = <1>;
arm,record-impl = /bits/ 64 <0x1>;
arm,status-reporting = /bits/ 64 <0x0>;
arm,gic-ref = <&gic>;
interrupts = <GIC_SPI 0x0d 0x04>;
interrupt-names = "fhi";
};
4. Boot QEMU with acpi=off:
./qemu-system-aarch64 \
-machine virt,accel=tcg,gic-version=3 \
-cpu cortex-a57 -m 2G -smp 4 \
-kernel Image -dtb virt-aest.dtb \
-append "console=ttyAMA0 acpi=off earlycon" \
-nographic
5. Verify probe:
dmesg | grep "DT RAS"
# Expected: DT RAS: registered 1 RAS error source(s) from DT
ls /sys/kernel/debug/aest/
6. Inject a CE error via the QEMU MMIO fault injection registers.
The QEMU device accepts 64-bit accesses only (use devmem with
the 64-bit width flag):
devmem 0x090e0808 64 0x40000001
This triggers QEMU's error_record_inj_write() which sets
ERR<n>STATUS.V=1 and asserts the IRQ. The kernel driver's
aest_irq_func() fires, reads the status, and logs:
arm64_ras: {2}[Hardware Error]: Hardware error from AEST gic.90e0000
arm64_ras: {2}[Hardware Error]: Error from GIC type 0x0 instance 0x8005
arm64_ras: {2}[Hardware Error]: ERR0FR: 0x48a5
arm64_ras: {2}[Hardware Error]: ERR0CTRL: 0x108
arm64_ras: {2}[Hardware Error]: ERR0STATUS: 0x40000001
Testing
-------
- Validated Processor error nodes/sources for L1-L2 and L3 caches error
on Qualcomm's lemans-evk and monaco-evk boards with DT boot.
- Validated CE and UE injection via debugfs soft_inject.
- Validated GIC and SMMU error sources on QEMU.
[1] https://lore.kernel.org/lkml/20260122094656.73399-1-tianruidong@linux.alibaba.com/
[2] https://github.com/winterddd/qemu/tree/error_record
---
Changes in v2:
- Rebased on top of Ruidong Tian's v7 RAS driver series and reworked
the DT frontend to match the new driver architecture.
- Moved RAS error source nodes to the DT root, removed the arm,aest
container node as suggested by Rob Herring.
- Dropped arm,processor-flags and arm,resource-type properties, these
are now inferred from the interrupt type and cache phandle
respectively.
- Renamed compatible strings from arm,aest-* to arm,ras-* and the
dt-bindings header from aest.h to arm-ras.h to avoid ACPI terminology
in DT bindings.
---
Umang Chheda (4):
dt-bindings: arm: ras: Introduce bindings for ARM RAS error sources
arm64: ras: Add Device Tree frontend
arm64: dts: qcom: monaco: add RAS error source nodes
arm64: dts: qcom: lemans: add RAS error source nodes
.../bindings/arm/arm,ras-error-source.yaml | 330 +++++++++++++++
arch/arm64/boot/dts/qcom/lemans.dtsi | 30 ++
arch/arm64/boot/dts/qcom/monaco.dtsi | 30 ++
drivers/ras/arm64/Kconfig | 25 +-
drivers/ras/arm64/Makefile | 2 +
drivers/ras/arm64/ras-of.c | 383 ++++++++++++++++++
include/dt-bindings/arm/arm-ras.h | 11 +
7 files changed, 806 insertions(+), 5 deletions(-)
create mode 100644 Documentation/devicetree/bindings/arm/arm,ras-error-source.yaml
create mode 100644 drivers/ras/arm64/ras-of.c
create mode 100644 include/dt-bindings/arm/arm-ras.h
--
2.34.1
^ permalink raw reply
* [PATCH 3/3] MAINTAINERS: Add HUSB320 Type-C controller entry
From: Hongyang Zhao @ 2026-07-20 8:17 UTC (permalink / raw)
To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao
In-Reply-To: <20260720-husb320-v1-0-4a1e259d7dc8@163.com>
Add myself as maintainer for the Hynetek HUSB320 binding and driver.
Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 806bd2d80d15..d3390b1daa1a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12056,6 +12056,13 @@ L: linux-kernel@vger.kernel.org
S: Maintained
F: arch/x86/kernel/cpu/hygon.c
+HYNETEK HUSB320 TYPE-C DRIVER
+M: Hongyang Zhao <hongyang.zhao@163.com>
+L: linux-usb@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/usb/hynetek,husb320.yaml
+F: drivers/usb/typec/husb320.c
+
HYNIX HI556 SENSOR DRIVER
M: Sakari Ailus <sakari.ailus@linux.intel.com>
L: linux-media@vger.kernel.org
--
2.43.0
^ permalink raw reply related
* [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver
From: Hongyang Zhao @ 2026-07-20 8:17 UTC (permalink / raw)
To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao
In-Reply-To: <20260720-husb320-v1-0-4a1e259d7dc8@163.com>
The HUSB320 is an autonomous Configuration Channel controller which
supports fixed source or sink operation and dual-role ports without USB
Power Delivery.
Add a Type-C class driver for attach and detach detection, orientation
and Rp current reporting, platform VBUS control, USB role switching,
Try.SRC and Try.SNK, and audio and debug accessories.
Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
drivers/usb/typec/Kconfig | 12 +
drivers/usb/typec/Makefile | 1 +
drivers/usb/typec/husb320.c | 1177 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 1190 insertions(+)
diff --git a/drivers/usb/typec/Kconfig b/drivers/usb/typec/Kconfig
index 2f80c2792dbd..22ef8bacacf7 100644
--- a/drivers/usb/typec/Kconfig
+++ b/drivers/usb/typec/Kconfig
@@ -88,6 +88,18 @@ config TYPEC_HD3SS3220
If you choose to build this driver as a dynamically linked module, the
module will be called hd3ss3220.ko.
+config TYPEC_HUSB320
+ tristate "Hynetek HUSB320 Type-C controller driver"
+ depends on I2C
+ depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
+ select REGMAP_I2C
+ help
+ Say Y or M here if your system has a Hynetek HUSB320 autonomous
+ USB Type-C controller.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called husb320.ko.
+
config TYPEC_STUSB160X
tristate "STMicroelectronics STUSB160x Type-C controller driver"
depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
index 8a6a1c663eb6..1b61c34d6e66 100644
--- a/drivers/usb/typec/Makefile
+++ b/drivers/usb/typec/Makefile
@@ -8,6 +8,7 @@ obj-$(CONFIG_TYPEC_UCSI) += ucsi/
obj-$(CONFIG_TYPEC_TPS6598X) += tipd/
obj-$(CONFIG_TYPEC_ANX7411) += anx7411.o
obj-$(CONFIG_TYPEC_HD3SS3220) += hd3ss3220.o
+obj-$(CONFIG_TYPEC_HUSB320) += husb320.o
obj-$(CONFIG_TYPEC_STUSB160X) += stusb160x.o
obj-$(CONFIG_TYPEC_RT1719) += rt1719.o
obj-$(CONFIG_TYPEC_WUSB3801) += wusb3801.o
diff --git a/drivers/usb/typec/husb320.c b/drivers/usb/typec/husb320.c
new file mode 100644
index 000000000000..fd9f949d9b52
--- /dev/null
+++ b/drivers/usb/typec/husb320.c
@@ -0,0 +1,1177 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hynetek HUSB320 USB Type-C controller driver
+ *
+ * Copyright (C) 2026 Hongyang Zhao <hongyang.zhao@163.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/usb/role.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/typec_altmode.h>
+
+#define HUSB320_REG_DEVICE_ID 0x01
+#define HUSB320_REG_DEVICE_TYPE 0x02
+#define HUSB320_REG_PORT_ROLE 0x03
+#define HUSB320_REG_CONTROL 0x04
+#define HUSB320_REG_CONTROL1 0x05
+#define HUSB320_REG_MANUAL 0x09
+#define HUSB320_REG_RESET 0x0a
+#define HUSB320_REG_MASK 0x0e
+#define HUSB320_REG_MASK1 0x0f
+#define HUSB320_REG_STATUS 0x11
+#define HUSB320_REG_TYPE 0x13
+#define HUSB320_REG_INTERRUPT 0x14
+#define HUSB320_REG_INTERRUPT1 0x15
+
+#define HUSB320_DEVICE_ID_VERSION GENMASK(7, 4)
+#define HUSB320_DEVICE_ID_REVISION GENMASK(3, 0)
+#define HUSB320_DEVICE_ID_VERSION_1 1
+#define HUSB320_DEVICE_TYPE 0x03
+
+#define HUSB320_PORT_ROLE_TRY GENMASK(5, 4)
+#define HUSB320_PORT_ROLE_MODE GENMASK(2, 0)
+#define HUSB320_PORT_ROLE_SOURCE BIT(0)
+#define HUSB320_PORT_ROLE_SINK BIT(1)
+#define HUSB320_PORT_ROLE_DRP BIT(2)
+#define HUSB320_TRY_NONE 0
+#define HUSB320_TRY_SINK 1
+#define HUSB320_TRY_SOURCE 2
+
+#define HUSB320_CONTROL_HOST_CURRENT GENMASK(2, 1)
+#define HUSB320_CONTROL_INT_MASK BIT(0)
+#define HUSB320_HOST_CURRENT_DEFAULT 1
+#define HUSB320_HOST_CURRENT_1_5A 2
+#define HUSB320_HOST_CURRENT_3_0A 3
+
+#define HUSB320_CONTROL1_ENABLE BIT(3)
+
+#define HUSB320_MANUAL_ERROR_RECOVERY BIT(0)
+
+#define HUSB320_RESET_SW BIT(0)
+
+/* The datasheet defines the fault mask, but reserves the matching IRQ bit. */
+#define HUSB320_MASK_FAULT BIT(5)
+#define HUSB320_MASK_AUTOSINK BIT(3)
+#define HUSB320_MASK1_FORCE GENMASK(2, 1)
+
+#define HUSB320_STATUS_ORIENTATION GENMASK(5, 4)
+#define HUSB320_STATUS_ORIENTATION_CC1 1
+#define HUSB320_STATUS_ORIENTATION_CC2 2
+#define HUSB320_STATUS_CURRENT GENMASK(2, 1)
+#define HUSB320_STATUS_ATTACHED BIT(0)
+
+#define HUSB320_TYPE_DEBUG_SOURCE BIT(6)
+#define HUSB320_TYPE_DEBUG_SINK BIT(5)
+#define HUSB320_TYPE_SINK BIT(4)
+#define HUSB320_TYPE_SOURCE BIT(3)
+#define HUSB320_TYPE_AUDIO_VBUS BIT(1)
+#define HUSB320_TYPE_AUDIO BIT(0)
+
+#define HUSB320_INT_ORIENTATION BIT(6)
+#define HUSB320_INT_VBUS_CHANGE BIT(4)
+#define HUSB320_INT_AUTOSINK BIT(3)
+#define HUSB320_INT_CURRENT_CHANGE BIT(2)
+#define HUSB320_INT_DETACH BIT(1)
+#define HUSB320_INT_ATTACH BIT(0)
+#define HUSB320_INT_VALID (HUSB320_INT_ORIENTATION | \
+ HUSB320_INT_VBUS_CHANGE | \
+ HUSB320_INT_AUTOSINK | \
+ HUSB320_INT_CURRENT_CHANGE | \
+ HUSB320_INT_DETACH | \
+ HUSB320_INT_ATTACH)
+#define HUSB320_INT1_VALID GENMASK(2, 1)
+
+enum husb320_state {
+ HUSB320_STATE_UNATTACHED,
+ HUSB320_STATE_SOURCE,
+ HUSB320_STATE_SINK,
+ HUSB320_STATE_DEBUG_SOURCE,
+ HUSB320_STATE_DEBUG_SINK,
+ HUSB320_STATE_AUDIO,
+};
+
+struct husb320 {
+ struct device *dev;
+ struct regmap *regmap;
+ struct gpio_desc *enable_gpio;
+ struct regulator *vbus;
+ struct usb_role_switch *role_sw;
+ struct typec_capability cap;
+ struct typec_port *port;
+ struct typec_partner *partner;
+ struct fwnode_handle *connector;
+ /* Serializes controller access and Type-C state updates. */
+ struct mutex lock;
+ enum typec_port_type port_type;
+ enum typec_role preferred_role;
+ enum typec_pwr_opmode source_opmode;
+ enum husb320_state state;
+ enum usb_role usb_role;
+ int connector_mode;
+ bool has_data;
+ bool mode_valid;
+ bool shutting_down;
+ bool state_valid;
+ bool vbus_on;
+};
+
+static const struct regmap_config husb320_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = HUSB320_REG_INTERRUPT1,
+};
+
+static enum typec_role husb320_default_power_role(struct husb320 *husb)
+{
+ switch (husb->port_type) {
+ case TYPEC_PORT_SRC:
+ return TYPEC_SOURCE;
+ case TYPEC_PORT_SNK:
+ return TYPEC_SINK;
+ case TYPEC_PORT_DRP:
+ default:
+ if (husb->preferred_role == TYPEC_SOURCE)
+ return TYPEC_SOURCE;
+
+ return TYPEC_SINK;
+ }
+}
+
+static enum typec_data_role husb320_default_data_role(struct husb320 *husb)
+{
+ switch (husb->cap.data) {
+ case TYPEC_PORT_DFP:
+ return TYPEC_HOST;
+ case TYPEC_PORT_UFP:
+ return TYPEC_DEVICE;
+ case TYPEC_PORT_DRD:
+ default:
+ return husb320_default_power_role(husb) == TYPEC_SOURCE ?
+ TYPEC_HOST : TYPEC_DEVICE;
+ }
+}
+
+static void husb320_set_unattached_roles(struct husb320 *husb)
+{
+ enum typec_role power_role = husb320_default_power_role(husb);
+
+ typec_set_pwr_role(husb->port, power_role);
+ if (husb->has_data)
+ typec_set_data_role(husb->port,
+ husb320_default_data_role(husb));
+ typec_set_pwr_opmode(husb->port,
+ power_role == TYPEC_SOURCE ?
+ husb->source_opmode : TYPEC_PWR_MODE_USB);
+}
+
+static int husb320_set_vbus(struct husb320 *husb, bool enable)
+{
+ int ret;
+
+ if (!husb->vbus || husb->vbus_on == enable)
+ return 0;
+
+ if (enable)
+ ret = regulator_enable(husb->vbus);
+ else
+ ret = regulator_disable(husb->vbus);
+ if (ret)
+ return ret;
+
+ husb->vbus_on = enable;
+
+ return 0;
+}
+
+static int husb320_set_mode(struct husb320 *husb, int mode)
+{
+ int ret;
+
+ if (husb->mode_valid && husb->connector_mode == mode)
+ return 0;
+
+ ret = typec_set_mode(husb->port, mode);
+ if (ret)
+ return ret;
+
+ husb->connector_mode = mode;
+ husb->mode_valid = true;
+
+ return 0;
+}
+
+static void husb320_unregister_partner(struct husb320 *husb)
+{
+ if (!husb->partner)
+ return;
+
+ typec_unregister_partner(husb->partner);
+ husb->partner = NULL;
+}
+
+static int husb320_register_partner(struct husb320 *husb,
+ enum typec_accessory accessory)
+{
+ struct typec_partner_desc desc = {
+ .accessory = accessory,
+ .usb_pd = false,
+ };
+
+ if (husb->partner)
+ return 0;
+
+ husb->partner = typec_register_partner(husb->port, &desc);
+ if (IS_ERR(husb->partner)) {
+ int ret = PTR_ERR(husb->partner);
+
+ husb->partner = NULL;
+ return ret;
+ }
+
+ return 0;
+}
+
+static int husb320_disconnect(struct husb320 *husb)
+{
+ int err = 0;
+ int ret;
+
+ ret = husb320_set_vbus(husb, false);
+ if (ret)
+ err = ret;
+
+ ret = usb_role_switch_set_role(husb->role_sw, USB_ROLE_NONE);
+ if (ret) {
+ if (!err)
+ err = ret;
+ } else {
+ husb->usb_role = USB_ROLE_NONE;
+ }
+
+ ret = husb320_set_mode(husb, TYPEC_STATE_SAFE);
+ if (ret && !err)
+ err = ret;
+
+ husb320_unregister_partner(husb);
+
+ if (typec_get_orientation(husb->port) != TYPEC_ORIENTATION_NONE) {
+ ret = typec_set_orientation(husb->port, TYPEC_ORIENTATION_NONE);
+ if (ret && !err)
+ err = ret;
+ }
+
+ husb320_set_unattached_roles(husb);
+
+ husb->state = HUSB320_STATE_UNATTACHED;
+ husb->state_valid = !err;
+
+ return err;
+}
+
+static int husb320_hw_disable(struct husb320 *husb);
+
+static void husb320_error_recovery(struct husb320 *husb)
+{
+ int cleanup_ret;
+ int disable_ret;
+ int recovery_ret;
+
+ cleanup_ret = husb320_disconnect(husb);
+ recovery_ret = regmap_write(husb->regmap, HUSB320_REG_MANUAL,
+ HUSB320_MANUAL_ERROR_RECOVERY);
+ if (cleanup_ret)
+ dev_err_ratelimited(husb->dev,
+ "failed to clean up port: %d\n", cleanup_ret);
+ if (recovery_ret)
+ dev_err_ratelimited(husb->dev,
+ "failed to start error recovery: %d\n",
+ recovery_ret);
+
+ if (!cleanup_ret && !recovery_ret)
+ return;
+
+ husb->shutting_down = true;
+ disable_ret = husb320_hw_disable(husb);
+ if (disable_ret)
+ dev_err_ratelimited(husb->dev,
+ "failed to disable controller: %d\n",
+ disable_ret);
+}
+
+static enum typec_pwr_opmode husb320_sink_opmode(unsigned int status)
+{
+ switch (FIELD_GET(HUSB320_STATUS_CURRENT, status)) {
+ case 2:
+ return TYPEC_PWR_MODE_1_5A;
+ case 3:
+ return TYPEC_PWR_MODE_3_0A;
+ case 0:
+ case 1:
+ default:
+ return TYPEC_PWR_MODE_USB;
+ }
+}
+
+static enum typec_role husb320_state_power_role(enum husb320_state state)
+{
+ switch (state) {
+ case HUSB320_STATE_SOURCE:
+ case HUSB320_STATE_DEBUG_SOURCE:
+ case HUSB320_STATE_AUDIO:
+ return TYPEC_SOURCE;
+ case HUSB320_STATE_SINK:
+ case HUSB320_STATE_DEBUG_SINK:
+ case HUSB320_STATE_UNATTACHED:
+ default:
+ return TYPEC_SINK;
+ }
+}
+
+static enum typec_accessory husb320_state_accessory(enum husb320_state state)
+{
+ switch (state) {
+ case HUSB320_STATE_DEBUG_SOURCE:
+ case HUSB320_STATE_DEBUG_SINK:
+ return TYPEC_ACCESSORY_DEBUG;
+ case HUSB320_STATE_AUDIO:
+ return TYPEC_ACCESSORY_AUDIO;
+ default:
+ return TYPEC_ACCESSORY_NONE;
+ }
+}
+
+static enum usb_role husb320_state_usb_role(struct husb320 *husb,
+ enum husb320_state state)
+{
+ if (!husb->has_data)
+ return USB_ROLE_NONE;
+
+ if ((state == HUSB320_STATE_SOURCE ||
+ state == HUSB320_STATE_DEBUG_SOURCE) &&
+ husb->cap.data != TYPEC_PORT_UFP)
+ return USB_ROLE_HOST;
+
+ if ((state == HUSB320_STATE_SINK ||
+ state == HUSB320_STATE_DEBUG_SINK) &&
+ husb->cap.data != TYPEC_PORT_DFP)
+ return USB_ROLE_DEVICE;
+
+ return USB_ROLE_NONE;
+}
+
+static int husb320_state_mode(enum husb320_state state)
+{
+ switch (state) {
+ case HUSB320_STATE_DEBUG_SOURCE:
+ case HUSB320_STATE_DEBUG_SINK:
+ return TYPEC_MODE_DEBUG;
+ case HUSB320_STATE_AUDIO:
+ return TYPEC_MODE_AUDIO;
+ case HUSB320_STATE_SOURCE:
+ case HUSB320_STATE_SINK:
+ return TYPEC_STATE_USB;
+ case HUSB320_STATE_UNATTACHED:
+ default:
+ return TYPEC_STATE_SAFE;
+ }
+}
+
+static int husb320_apply_state(struct husb320 *husb,
+ enum husb320_state state,
+ enum typec_orientation orientation,
+ unsigned int status)
+{
+ enum typec_accessory accessory = husb320_state_accessory(state);
+ enum usb_role usb_role = husb320_state_usb_role(husb, state);
+ enum typec_pwr_opmode opmode;
+ enum typec_role power_role;
+ int ret;
+
+ if (state == HUSB320_STATE_UNATTACHED && husb->state_valid &&
+ husb->state == HUSB320_STATE_UNATTACHED && !husb->partner &&
+ !husb->vbus_on &&
+ husb->usb_role == USB_ROLE_NONE)
+ return 0;
+
+ if (state == HUSB320_STATE_UNATTACHED)
+ return husb320_disconnect(husb);
+
+ if (state != husb->state &&
+ (!husb->state_valid ||
+ husb->state != HUSB320_STATE_UNATTACHED)) {
+ ret = husb320_disconnect(husb);
+ if (ret)
+ return ret;
+ }
+
+ if (typec_get_orientation(husb->port) != orientation) {
+ ret = typec_set_orientation(husb->port, orientation);
+ if (ret)
+ return ret;
+ }
+
+ /* Accessory states do not request the platform VBUS supply. */
+ ret = husb320_set_vbus(husb, state == HUSB320_STATE_SOURCE);
+ if (ret)
+ return ret;
+
+ if (usb_role != husb->usb_role) {
+ ret = usb_role_switch_set_role(husb->role_sw, usb_role);
+ if (ret)
+ return ret;
+ husb->usb_role = usb_role;
+ }
+
+ ret = husb320_set_mode(husb, husb320_state_mode(state));
+ if (ret)
+ return ret;
+
+ power_role = husb320_state_power_role(state);
+ opmode = state == HUSB320_STATE_SINK ?
+ husb320_sink_opmode(status) :
+ (power_role == TYPEC_SOURCE ? husb->source_opmode :
+ TYPEC_PWR_MODE_USB);
+
+ if (usb_role == USB_ROLE_HOST)
+ typec_set_data_role(husb->port, TYPEC_HOST);
+ else if (usb_role == USB_ROLE_DEVICE)
+ typec_set_data_role(husb->port, TYPEC_DEVICE);
+ typec_set_pwr_role(husb->port, power_role);
+ typec_set_pwr_opmode(husb->port, opmode);
+
+ ret = husb320_register_partner(husb, accessory);
+ if (ret) {
+ husb->state_valid = false;
+ dev_err_ratelimited(husb->dev,
+ "failed to register partner: %d\n", ret);
+ return ret;
+ }
+
+ husb->state = state;
+ husb->state_valid = true;
+
+ return 0;
+}
+
+static enum typec_orientation husb320_get_orientation(unsigned int status)
+{
+ switch (FIELD_GET(HUSB320_STATUS_ORIENTATION, status)) {
+ case HUSB320_STATUS_ORIENTATION_CC1:
+ return TYPEC_ORIENTATION_NORMAL;
+ case HUSB320_STATUS_ORIENTATION_CC2:
+ return TYPEC_ORIENTATION_REVERSE;
+ default:
+ return TYPEC_ORIENTATION_NONE;
+ }
+}
+
+static enum husb320_state husb320_get_state(unsigned int status,
+ unsigned int type)
+{
+ if (!(status & HUSB320_STATUS_ATTACHED))
+ return HUSB320_STATE_UNATTACHED;
+
+ if (type & HUSB320_TYPE_DEBUG_SOURCE)
+ return HUSB320_STATE_DEBUG_SOURCE;
+ if (type & HUSB320_TYPE_DEBUG_SINK)
+ return HUSB320_STATE_DEBUG_SINK;
+ if (type & HUSB320_TYPE_SINK)
+ return HUSB320_STATE_SINK;
+ if (type & HUSB320_TYPE_SOURCE)
+ return HUSB320_STATE_SOURCE;
+ if (type & (HUSB320_TYPE_AUDIO_VBUS | HUSB320_TYPE_AUDIO))
+ return HUSB320_STATE_AUDIO;
+
+ return HUSB320_STATE_UNATTACHED;
+}
+
+static int husb320_update_state(struct husb320 *husb)
+{
+ enum typec_orientation orientation;
+ enum husb320_state state;
+ unsigned int status;
+ unsigned int type;
+ int ret;
+
+ ret = regmap_read(husb->regmap, HUSB320_REG_STATUS, &status);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(husb->regmap, HUSB320_REG_TYPE, &type);
+ if (ret)
+ return ret;
+
+ state = husb320_get_state(status, type);
+ orientation = state == HUSB320_STATE_UNATTACHED ?
+ TYPEC_ORIENTATION_NONE : husb320_get_orientation(status);
+
+ ret = husb320_apply_state(husb, state, orientation, status);
+ if (!ret)
+ dev_dbg(husb->dev, "status=%#x type=%#x state=%u\n",
+ status, type, state);
+
+ return ret;
+}
+
+static irqreturn_t husb320_irq_thread(int irq, void *data)
+{
+ struct husb320 *husb = data;
+ unsigned int interrupt;
+ unsigned int interrupt1;
+ int disable_ret;
+ int ret;
+
+ mutex_lock(&husb->lock);
+ if (husb->shutting_down) {
+ disable_irq_nosync(irq);
+ mutex_unlock(&husb->lock);
+ return IRQ_HANDLED;
+ }
+
+ ret = regmap_read(husb->regmap, HUSB320_REG_INTERRUPT, &interrupt);
+ if (ret)
+ goto err;
+
+ ret = regmap_read(husb->regmap, HUSB320_REG_INTERRUPT1, &interrupt1);
+ if (ret)
+ goto err;
+
+ interrupt &= HUSB320_INT_VALID;
+ interrupt1 &= HUSB320_INT1_VALID;
+ if (!interrupt && !interrupt1) {
+ mutex_unlock(&husb->lock);
+ return IRQ_NONE;
+ }
+
+ if (interrupt) {
+ ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT,
+ interrupt);
+ if (ret)
+ goto err;
+ }
+
+ if (interrupt1) {
+ ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT1,
+ interrupt1);
+ if (ret)
+ goto err;
+ }
+
+ if (interrupt & HUSB320_INT_DETACH) {
+ ret = husb320_disconnect(husb);
+ if (ret)
+ dev_err_ratelimited(husb->dev,
+ "failed to disconnect port: %d\n",
+ ret);
+ }
+
+ ret = husb320_update_state(husb);
+ if (ret) {
+ dev_err_ratelimited(husb->dev,
+ "failed to update port state: %d\n", ret);
+ husb320_error_recovery(husb);
+ }
+
+ mutex_unlock(&husb->lock);
+
+ return IRQ_HANDLED;
+
+err:
+ dev_err_ratelimited(husb->dev, "failed to service interrupt: %d\n",
+ ret);
+ husb->shutting_down = true;
+ husb320_error_recovery(husb);
+ disable_ret = husb320_hw_disable(husb);
+ if (disable_ret)
+ dev_err_ratelimited(husb->dev,
+ "failed to disable controller: %d\n",
+ disable_ret);
+ disable_irq_nosync(irq);
+ mutex_unlock(&husb->lock);
+
+ return IRQ_HANDLED;
+}
+
+static unsigned int husb320_port_role(enum typec_port_type type)
+{
+ switch (type) {
+ case TYPEC_PORT_SRC:
+ return HUSB320_PORT_ROLE_SOURCE;
+ case TYPEC_PORT_SNK:
+ return HUSB320_PORT_ROLE_SINK;
+ case TYPEC_PORT_DRP:
+ default:
+ return HUSB320_PORT_ROLE_DRP;
+ }
+}
+
+static unsigned int husb320_try_role(int role)
+{
+ switch (role) {
+ case TYPEC_SINK:
+ return HUSB320_TRY_SINK;
+ case TYPEC_SOURCE:
+ return HUSB320_TRY_SOURCE;
+ case TYPEC_NO_PREFERRED_ROLE:
+ default:
+ return HUSB320_TRY_NONE;
+ }
+}
+
+static int husb320_port_type_set(struct typec_port *port,
+ enum typec_port_type type)
+{
+ struct husb320 *husb = typec_get_drvdata(port);
+ unsigned int port_role;
+ int ret;
+
+ mutex_lock(&husb->lock);
+ if (husb->shutting_down) {
+ ret = -ESHUTDOWN;
+ goto out_unlock;
+ }
+
+ port_role = husb320_port_role(type);
+ if (type == TYPEC_PORT_DRP)
+ port_role |= FIELD_PREP(HUSB320_PORT_ROLE_TRY,
+ husb320_try_role(husb->preferred_role));
+
+ ret = husb320_disconnect(husb);
+ if (ret) {
+ husb320_error_recovery(husb);
+ goto out_unlock;
+ }
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+ HUSB320_PORT_ROLE_TRY |
+ HUSB320_PORT_ROLE_MODE,
+ port_role);
+ if (ret) {
+ husb320_error_recovery(husb);
+ goto out_unlock;
+ }
+
+ husb->port_type = type;
+ husb320_set_unattached_roles(husb);
+out_unlock:
+ mutex_unlock(&husb->lock);
+
+ return ret;
+}
+
+static int husb320_try_role_set(struct typec_port *port, int role)
+{
+ struct husb320 *husb = typec_get_drvdata(port);
+ int ret;
+
+ mutex_lock(&husb->lock);
+ if (husb->shutting_down) {
+ ret = -ESHUTDOWN;
+ goto out_unlock;
+ }
+
+ if (husb->port_type == TYPEC_PORT_DRP)
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+ HUSB320_PORT_ROLE_TRY,
+ FIELD_PREP(HUSB320_PORT_ROLE_TRY,
+ husb320_try_role(role)));
+ else
+ ret = 0;
+ if (!ret)
+ husb->preferred_role = role;
+out_unlock:
+ mutex_unlock(&husb->lock);
+
+ return ret;
+}
+
+static const struct typec_operations husb320_typec_ops = {
+ .port_type_set = husb320_port_type_set,
+ .try_role = husb320_try_role_set,
+};
+
+static unsigned int husb320_host_current(enum typec_pwr_opmode opmode)
+{
+ switch (opmode) {
+ case TYPEC_PWR_MODE_1_5A:
+ return HUSB320_HOST_CURRENT_1_5A;
+ case TYPEC_PWR_MODE_3_0A:
+ return HUSB320_HOST_CURRENT_3_0A;
+ case TYPEC_PWR_MODE_USB:
+ default:
+ return HUSB320_HOST_CURRENT_DEFAULT;
+ }
+}
+
+static int husb320_hw_init(struct husb320 *husb)
+{
+ unsigned int port_role;
+ unsigned int control;
+ int ret;
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_RESET,
+ HUSB320_RESET_SW, HUSB320_RESET_SW);
+ if (ret)
+ return ret;
+
+ /* Wait for stable status and I2C access after the soft reset. */
+ msleep(100);
+
+ control = HUSB320_CONTROL_INT_MASK |
+ FIELD_PREP(HUSB320_CONTROL_HOST_CURRENT,
+ husb320_host_current(husb->source_opmode));
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+ HUSB320_CONTROL_INT_MASK |
+ HUSB320_CONTROL_HOST_CURRENT,
+ control);
+ if (ret)
+ return ret;
+
+ port_role = husb320_port_role(husb->port_type);
+ if (husb->port_type == TYPEC_PORT_DRP)
+ port_role |= FIELD_PREP(HUSB320_PORT_ROLE_TRY,
+ husb320_try_role(husb->preferred_role));
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+ HUSB320_PORT_ROLE_TRY |
+ HUSB320_PORT_ROLE_MODE,
+ port_role);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(husb->regmap, HUSB320_REG_MASK,
+ HUSB320_MASK_FAULT | HUSB320_MASK_AUTOSINK);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(husb->regmap, HUSB320_REG_MASK1,
+ HUSB320_MASK1_FORCE);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
+ HUSB320_CONTROL1_ENABLE,
+ HUSB320_CONTROL1_ENABLE);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT,
+ HUSB320_INT_VALID);
+ if (ret)
+ return ret;
+
+ return regmap_write(husb->regmap, HUSB320_REG_INTERRUPT1,
+ HUSB320_INT1_VALID);
+}
+
+static int husb320_hw_disable(struct husb320 *husb)
+{
+ int err = 0;
+ int ret;
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+ HUSB320_CONTROL_INT_MASK,
+ HUSB320_CONTROL_INT_MASK);
+ if (ret)
+ err = ret;
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
+ HUSB320_CONTROL1_ENABLE, 0);
+ if (ret && !err)
+ err = ret;
+
+ return err;
+}
+
+static int husb320_set_sink_mode(struct husb320 *husb)
+{
+ int ret;
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+ HUSB320_CONTROL_INT_MASK,
+ HUSB320_CONTROL_INT_MASK);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+ HUSB320_PORT_ROLE_TRY |
+ HUSB320_PORT_ROLE_MODE,
+ HUSB320_PORT_ROLE_SINK);
+ if (ret)
+ return ret;
+
+ return regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
+ HUSB320_CONTROL1_ENABLE,
+ HUSB320_CONTROL1_ENABLE);
+}
+
+static int husb320_parse_connector(struct husb320 *husb)
+{
+ const char *opmode;
+ int ret;
+
+ husb->connector = device_get_named_child_node(husb->dev, "connector");
+ if (!husb->connector)
+ return -ENODEV;
+
+ /* The connector is parsed as a fwnode and is not populated as a device. */
+ fw_devlink_purge_absent_suppliers(husb->connector);
+
+ ret = typec_get_fw_cap(&husb->cap, husb->connector);
+ if (ret)
+ return ret;
+
+ ret = fwnode_property_read_string(husb->connector,
+ "typec-power-opmode", &opmode);
+ if (ret)
+ return ret;
+
+ ret = typec_find_pwr_opmode(opmode);
+ if (ret < 0)
+ return ret;
+ if (ret == TYPEC_PWR_MODE_PD)
+ return -EINVAL;
+
+ husb->source_opmode = ret;
+ husb->has_data = fwnode_property_present(husb->connector, "data-role");
+ if (husb->has_data &&
+ ((husb->cap.type == TYPEC_PORT_SRC &&
+ husb->cap.data != TYPEC_PORT_DFP) ||
+ (husb->cap.type == TYPEC_PORT_SNK &&
+ husb->cap.data != TYPEC_PORT_UFP) ||
+ (husb->cap.type == TYPEC_PORT_DRP &&
+ husb->cap.data != TYPEC_PORT_DRD))) {
+ return dev_err_probe(husb->dev, -EINVAL,
+ "power and data roles do not match\n");
+ } else if (!husb->has_data) {
+ if (husb->cap.type == TYPEC_PORT_SNK)
+ husb->cap.data = TYPEC_PORT_UFP;
+ else if (husb->cap.type == TYPEC_PORT_DRP)
+ husb->cap.data = TYPEC_PORT_DRD;
+ }
+
+ husb->cap.revision = USB_TYPEC_REV_2_0;
+ husb->cap.accessory[0] = TYPEC_ACCESSORY_AUDIO;
+ husb->cap.accessory[1] = TYPEC_ACCESSORY_DEBUG;
+ husb->cap.orientation_aware = true;
+ husb->cap.driver_data = husb;
+ husb->cap.ops = &husb320_typec_ops;
+ husb->port_type = husb->cap.type;
+ husb->preferred_role = husb->cap.prefer_role;
+
+ return 0;
+}
+
+static int husb320_get_role_switch(struct husb320 *husb)
+{
+ int ret;
+
+ if (!husb->has_data)
+ return 0;
+
+ husb->role_sw = fwnode_usb_role_switch_get(husb->connector);
+ if (IS_ERR(husb->role_sw)) {
+ ret = PTR_ERR(husb->role_sw);
+ husb->role_sw = NULL;
+ if (ret == -ENODEV && husb->cap.data != TYPEC_PORT_DRD)
+ return 0;
+
+ return dev_err_probe(husb->dev, ret,
+ "failed to get USB role switch\n");
+ }
+
+ if (!husb->role_sw && husb->cap.data == TYPEC_PORT_DRD)
+ return dev_err_probe(husb->dev, -ENODEV,
+ "USB data role has no role switch\n");
+
+ return 0;
+}
+
+static int husb320_get_vbus(struct husb320 *husb)
+{
+ struct device_node *node = to_of_node(husb->connector);
+
+ if (node)
+ husb->vbus = devm_of_regulator_get_optional(husb->dev, node, "vbus");
+ else
+ husb->vbus = devm_regulator_get_optional(husb->dev, "vbus");
+
+ if (IS_ERR(husb->vbus)) {
+ int ret = PTR_ERR(husb->vbus);
+
+ husb->vbus = NULL;
+ if (ret != -ENODEV)
+ return dev_err_probe(husb->dev, ret,
+ "failed to get VBUS supply\n");
+ }
+
+ if (husb->cap.type != TYPEC_PORT_SNK && !husb->vbus)
+ return dev_err_probe(husb->dev, -ENODEV,
+ "source-capable port requires VBUS supply\n");
+
+ return 0;
+}
+
+static void husb320_disable(void *data)
+{
+ struct husb320 *husb = data;
+
+ gpiod_set_value_cansleep(husb->enable_gpio, 0);
+}
+
+static int husb320_power_on(struct husb320 *husb)
+{
+ bool needs_delay = false;
+ int ret;
+
+ ret = devm_regulator_get_enable_optional(husb->dev, "vdd");
+ if (ret == -ENODEV) {
+ ret = 0;
+ } else if (ret) {
+ return dev_err_probe(husb->dev, ret,
+ "failed to enable VDD supply\n");
+ } else {
+ needs_delay = true;
+ }
+
+ husb->enable_gpio = devm_gpiod_get_optional(husb->dev, "enable",
+ GPIOD_OUT_HIGH);
+ if (IS_ERR(husb->enable_gpio))
+ return dev_err_probe(husb->dev, PTR_ERR(husb->enable_gpio),
+ "failed to enable controller\n");
+
+ if (husb->enable_gpio) {
+ ret = devm_add_action_or_reset(husb->dev, husb320_disable, husb);
+ if (ret)
+ return ret;
+ needs_delay = true;
+ }
+
+ /* Wait for the controller to become I2C-accessible after power-on. */
+ if (needs_delay)
+ msleep(100);
+
+ return 0;
+}
+
+static int husb320_check_device(struct husb320 *husb)
+{
+ unsigned int device_id;
+ unsigned int device_type;
+ int ret;
+
+ ret = regmap_read(husb->regmap, HUSB320_REG_DEVICE_ID, &device_id);
+ if (ret)
+ return dev_err_probe(husb->dev, ret,
+ "failed to read device ID\n");
+
+ ret = regmap_read(husb->regmap, HUSB320_REG_DEVICE_TYPE, &device_type);
+ if (ret)
+ return dev_err_probe(husb->dev, ret,
+ "failed to read device type\n");
+
+ if (FIELD_GET(HUSB320_DEVICE_ID_VERSION, device_id) !=
+ HUSB320_DEVICE_ID_VERSION_1 ||
+ device_type != HUSB320_DEVICE_TYPE)
+ return dev_err_probe(husb->dev, -ENODEV,
+ "unsupported device ID %#x type %#x\n",
+ device_id, device_type);
+
+ dev_dbg(husb->dev, "version %lu revision %lu\n",
+ FIELD_GET(HUSB320_DEVICE_ID_VERSION, device_id),
+ FIELD_GET(HUSB320_DEVICE_ID_REVISION, device_id));
+
+ return 0;
+}
+
+static int husb320_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct husb320 *husb;
+ int ret;
+
+ husb = devm_kzalloc(dev, sizeof(*husb), GFP_KERNEL);
+ if (!husb)
+ return -ENOMEM;
+
+ husb->dev = dev;
+ husb->state = HUSB320_STATE_UNATTACHED;
+ husb->usb_role = USB_ROLE_NONE;
+ mutex_init(&husb->lock);
+ i2c_set_clientdata(client, husb);
+
+ ret = husb320_power_on(husb);
+ if (ret)
+ return ret;
+
+ husb->regmap = devm_regmap_init_i2c(client, &husb320_regmap_config);
+ if (IS_ERR(husb->regmap))
+ return dev_err_probe(dev, PTR_ERR(husb->regmap),
+ "failed to initialize regmap\n");
+
+ ret = husb320_check_device(husb);
+ if (ret)
+ return ret;
+
+ if (client->irq <= 0)
+ return dev_err_probe(dev, client->irq ?: -EINVAL,
+ "missing interrupt\n");
+
+ ret = husb320_parse_connector(husb);
+ if (ret)
+ goto err_put_connector;
+
+ ret = husb320_get_role_switch(husb);
+ if (ret)
+ goto err_put_connector;
+
+ ret = husb320_get_vbus(husb);
+ if (ret)
+ goto err_put_role;
+
+ ret = husb320_hw_init(husb);
+ if (ret)
+ goto err_hw_disable;
+
+ husb->port = typec_register_port(dev, &husb->cap);
+ if (IS_ERR(husb->port)) {
+ ret = PTR_ERR(husb->port);
+ goto err_hw_disable;
+ }
+
+ ret = devm_request_threaded_irq(dev, client->irq, NULL,
+ husb320_irq_thread, IRQF_ONESHOT,
+ dev_name(dev), husb);
+ if (ret)
+ goto err_unregister_port;
+
+ mutex_lock(&husb->lock);
+ ret = husb320_update_state(husb);
+ mutex_unlock(&husb->lock);
+ if (ret)
+ goto err_free_irq;
+
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+ HUSB320_CONTROL_INT_MASK, 0);
+ if (ret)
+ goto err_free_irq;
+
+ return 0;
+
+err_free_irq:
+ mutex_lock(&husb->lock);
+ husb->shutting_down = true;
+ mutex_unlock(&husb->lock);
+ devm_free_irq(dev, client->irq, husb);
+err_unregister_port:
+ mutex_lock(&husb->lock);
+ husb->shutting_down = true;
+ husb320_disconnect(husb);
+ mutex_unlock(&husb->lock);
+ typec_unregister_port(husb->port);
+err_hw_disable:
+ husb320_hw_disable(husb);
+err_put_role:
+ usb_role_switch_put(husb->role_sw);
+err_put_connector:
+ fwnode_handle_put(husb->connector);
+
+ return ret;
+}
+
+static void husb320_remove(struct i2c_client *client)
+{
+ struct husb320 *husb = i2c_get_clientdata(client);
+ int ret;
+
+ mutex_lock(&husb->lock);
+ husb->shutting_down = true;
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+ HUSB320_CONTROL_INT_MASK,
+ HUSB320_CONTROL_INT_MASK);
+ mutex_unlock(&husb->lock);
+ if (ret)
+ dev_err(husb->dev, "failed to mask interrupts: %d\n", ret);
+
+ devm_free_irq(husb->dev, client->irq, husb);
+
+ mutex_lock(&husb->lock);
+ ret = husb320_disconnect(husb);
+ if (ret)
+ dev_err(husb->dev, "failed to disconnect port: %d\n", ret);
+ ret = husb320_hw_disable(husb);
+ if (ret)
+ dev_err(husb->dev, "failed to disable controller: %d\n", ret);
+ mutex_unlock(&husb->lock);
+
+ typec_unregister_port(husb->port);
+ usb_role_switch_put(husb->role_sw);
+ fwnode_handle_put(husb->connector);
+}
+
+static void husb320_shutdown(struct i2c_client *client)
+{
+ struct husb320 *husb = i2c_get_clientdata(client);
+ int ret;
+
+ mutex_lock(&husb->lock);
+ husb->shutting_down = true;
+ ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+ HUSB320_CONTROL_INT_MASK,
+ HUSB320_CONTROL_INT_MASK);
+ mutex_unlock(&husb->lock);
+ if (ret)
+ dev_err(husb->dev, "failed to mask interrupts: %d\n", ret);
+
+ disable_irq(client->irq);
+
+ mutex_lock(&husb->lock);
+ ret = husb320_disconnect(husb);
+ if (ret) {
+ dev_err(husb->dev, "failed to disconnect port: %d\n", ret);
+ } else if (husb->cap.type != TYPEC_PORT_SRC) {
+ ret = husb320_set_sink_mode(husb);
+ if (!ret)
+ goto out_unlock;
+
+ dev_err(husb->dev, "failed to configure shutdown mode: %d\n", ret);
+ }
+
+ ret = husb320_hw_disable(husb);
+ if (ret)
+ dev_err(husb->dev, "failed to disable controller: %d\n", ret);
+
+out_unlock:
+ mutex_unlock(&husb->lock);
+}
+
+static const struct of_device_id husb320_of_match[] = {
+ { .compatible = "hynetek,husb320" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, husb320_of_match);
+
+static struct i2c_driver husb320_driver = {
+ .driver = {
+ .name = "husb320",
+ .of_match_table = husb320_of_match,
+ },
+ .probe = husb320_probe,
+ .remove = husb320_remove,
+ .shutdown = husb320_shutdown,
+};
+module_i2c_driver(husb320_driver);
+
+MODULE_AUTHOR("Hongyang Zhao <hongyang.zhao@163.com>");
+MODULE_DESCRIPTION("Hynetek HUSB320 USB Type-C controller driver");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
* [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller
From: Hongyang Zhao @ 2026-07-20 8:17 UTC (permalink / raw)
To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao
In-Reply-To: <20260720-husb320-v1-0-4a1e259d7dc8@163.com>
The HUSB320 is an autonomous USB Type-C Configuration Channel
controller supporting source, sink, and dual-role port operation.
Document its two I2C addresses, active-low interrupt, optional supplies
and enable GPIO, and managed USB-C connector.
Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
.../devicetree/bindings/usb/hynetek,husb320.yaml | 171 +++++++++++++++++++++
1 file changed, 171 insertions(+)
diff --git a/Documentation/devicetree/bindings/usb/hynetek,husb320.yaml b/Documentation/devicetree/bindings/usb/hynetek,husb320.yaml
new file mode 100644
index 000000000000..b84b6b725e84
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/hynetek,husb320.yaml
@@ -0,0 +1,171 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/usb/hynetek,husb320.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Hynetek HUSB320 USB Type-C controller
+
+maintainers:
+ - Hongyang Zhao <hongyang.zhao@163.com>
+
+description:
+ The HUSB320 is an autonomous USB Type-C Configuration Channel controller.
+ It supports source, sink and dual-role port configurations, including
+ optional Try.SRC and Try.SNK role preferences. The controller reports cable
+ attachment, orientation, advertised current and accessory detection over
+ I2C and an active-low interrupt.
+
+properties:
+ compatible:
+ const: hynetek,husb320
+
+ reg:
+ description:
+ 7-bit I2C address selected by the ADDR/ORIENT pin strap. Ground selects
+ 0x21 and VDD selects 0x31. These correspond to the 8-bit write addresses
+ 0x42 and 0x62 shown in the datasheet.
+ enum: [0x21, 0x31]
+
+ interrupts:
+ maxItems: 1
+
+ wakeup-source: true
+
+ vdd-supply:
+ description: Power supply for the controller
+
+ enable-gpios:
+ description: Active-low controller enable
+ maxItems: 1
+
+ connector:
+ type: object
+ $ref: /schemas/connector/usb-connector.yaml#
+ unevaluatedProperties: false
+ description:
+ USB Type-C connector managed by the controller. A dual-role data port
+ requires a High-Speed graph connection to a USB role-switch provider.
+
+ properties:
+ compatible:
+ const: usb-c-connector
+
+ try-power-role:
+ enum: [source, sink]
+
+ required:
+ - compatible
+ - pd-disable
+ - power-role
+ - typec-power-opmode
+
+ allOf:
+ - if:
+ properties:
+ power-role:
+ enum: [source, dual]
+ then:
+ required:
+ - vbus-supply
+
+ - if:
+ properties:
+ power-role:
+ const: source
+ then:
+ properties:
+ data-role:
+ const: host
+ try-power-role: false
+
+ - if:
+ properties:
+ power-role:
+ const: sink
+ then:
+ properties:
+ data-role:
+ const: device
+ try-power-role: false
+
+ - if:
+ properties:
+ power-role:
+ const: dual
+ then:
+ properties:
+ data-role:
+ const: dual
+
+ - if:
+ required:
+ - data-role
+ properties:
+ data-role:
+ const: dual
+ then:
+ anyOf:
+ - required:
+ - port
+ - required:
+ - ports
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - connector
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ typec@31 {
+ compatible = "hynetek,husb320";
+ reg = <0x31>;
+ vdd-supply = <&vreg_3v3>;
+ enable-gpios = <&gpio 54 GPIO_ACTIVE_LOW>;
+ interrupts-extended = <&gpio 55 IRQ_TYPE_LEVEL_LOW>;
+ wakeup-source;
+
+ connector {
+ compatible = "usb-c-connector";
+ label = "USB-C";
+ vbus-supply = <&usb_vbus>;
+ data-role = "dual";
+ power-role = "dual";
+ try-power-role = "sink";
+ typec-power-opmode = "default";
+ pd-disable;
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+
+ endpoint {
+ remote-endpoint = <&usb_role>;
+ };
+ };
+
+ port@1 {
+ reg = <1>;
+
+ endpoint {
+ remote-endpoint = <&usb_ss>;
+ };
+ };
+ };
+ };
+ };
+ };
--
2.43.0
^ permalink raw reply related
* [PATCH 0/3] Add Hynetek HUSB320 Type-C controller support
From: Hongyang Zhao @ 2026-07-20 8:17 UTC (permalink / raw)
To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao
Add a Devicetree binding and Type-C class driver for the Hynetek
HUSB320 autonomous Configuration Channel controller. The device handles
the Type-C state machine without USB Power Delivery.
The driver supports fixed source and sink configurations, dual-role
operation, Try.SRC and Try.SNK, orientation and Rp current reporting,
platform VBUS and USB role control, and audio and debug accessories.
Testing was performed on a Thundercomm I615 Development Kit based on
the Qualcomm QCS615 SoC. USB device mode enumerated at 5 Gbit/s in
both plug orientations. In USB host mode, a mouse worked in both
orientations, and a USB 3.0 flash drive enumerated at 5 Gbit/s in both
plug orientations.
The datasheet is available at:
https://item.szlcsc.com/datasheet/HUSB320_BA000-QN12R/8528239.html
Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
Hongyang Zhao (3):
dt-bindings: usb: Add Hynetek HUSB320 Type-C controller
usb: typec: Add Hynetek HUSB320 Type-C controller driver
MAINTAINERS: Add HUSB320 Type-C controller entry
.../devicetree/bindings/usb/hynetek,husb320.yaml | 171 +++
MAINTAINERS | 7 +
drivers/usb/typec/Kconfig | 12 +
drivers/usb/typec/Makefile | 1 +
drivers/usb/typec/husb320.c | 1177 ++++++++++++++++++++
5 files changed, 1368 insertions(+)
---
base-commit: 80574c40598aedbc1751c528e414d7e224bc6313
change-id: 20260718-husb320-6258aa172ec7
Best regards,
--
Hongyang Zhao <hongyang.zhao@163.com>
^ permalink raw reply
* Re: [PATCH 1/4] drm/rockchip: lvds: propagate bus_flags to the CRTC state
From: Chaoyi Chen @ 2026-07-20 1:27 UTC (permalink / raw)
To: Rok Markovic, Heiko Stuebner, Sandy Huang, Andy Yan,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: dri-devel, devicetree, linux-arm-kernel, linux-rockchip,
linux-kernel, Alibek Omarov
In-Reply-To: <20260717120005.2087386-2-rok@kanardia.eu>
On 7/17/2026 8:00 PM, Rok Markovic wrote:
> rockchip_lvds_encoder_atomic_check() sets output_mode and output_type
> but never copies the connector's bus_flags into the Rockchip CRTC
> state, unlike dw_dp-rockchip.c and dw-mipi-dsi2-rockchip.c which both
> do.
>
> panel-lvds parses pixelclk-active from the DT display timing and
> publishes it on the connector as DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE.
> VOP2 reads that flag back out of the CRTC state to decide
> POLFLAG_DCLK_INV. With the flag dropped here, vcstate->bus_flags is
> always zero, so a panel that asks to be clocked on the falling edge is
> driven on the rising one, and there is no way to express the panel's
> requirement from DT at all.
>
> No functional change for the SoCs currently supported by this driver:
> rk3288 and px30 pair with the VOP1 driver (rockchip_drm_vop.c), which
> never reads bus_flags - only VOP2 consumes it. The flag becomes live
> with the RK3568 support added later in this series.
>
> Signed-off-by: Rok Markovic <rok@kanardia.eu>
> Assisted-by: Claude:claude-opus-4-8
> ---
> drivers/gpu/drm/rockchip/rockchip_lvds.c | 10 +++++++++-
> 1 file changed, 9 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpu/drm/rockchip/rockchip_lvds.c b/drivers/gpu/drm/rockchip/rockchip_lvds.c
> index 75f898a..95fa0a9 100644
> --- a/drivers/gpu/drm/rockchip/rockchip_lvds.c
> +++ b/drivers/gpu/drm/rockchip/rockchip_lvds.c
> @@ -129,7 +129,15 @@ rockchip_lvds_encoder_atomic_check(struct drm_encoder *encoder,
> struct drm_connector_state *conn_state)
> {
> struct rockchip_crtc_state *s = to_rockchip_crtc_state(crtc_state);
> -
> + struct drm_connector *connector = conn_state->connector;
> +
> + /*
> + * The VOP derives the pixel clock polarity from this. Without it a
> + * panel that declares pixelclk-active = <0> is clocked on the wrong
> + * edge. panel-lvds fills the connector's bus_flags in from the DT
> + * display timing.
> + */
> + s->bus_flags = connector->display_info.bus_flags;
> s->output_mode = ROCKCHIP_OUT_MODE_P888;
> s->output_type = DRM_MODE_CONNECTOR_LVDS;
>
Reviewed-by: Chaoyi Chen <chaoyi.chen@rock-chips.com>
--
Best,
Chaoyi
^ permalink raw reply
* Re: [PATCH 2/2] media: i2c: add driver for ITE IT6625/IT6626
From: sashiko-bot @ 2026-07-20 8:11 UTC (permalink / raw)
To: Hermes Wu; +Cc: devicetree, conor+dt, media-ci, robh
In-Reply-To: <20260720-upstream-it6625-it6626-v1-2-475912f74459@ite.com.tw>
Thank you for your contribution! Sashiko AI review found 13 potential issue(s) to consider:
- [High] Premature registration of the V4L2 subdevice exposes uninitialized hardware and control state to the system.
- [High] The probe error path uses non-synchronous `cancel_delayed_work()`, leading to a use-after-free.
- [High] `get_fmt` and `set_fmt` mishandle `V4L2_SUBDEV_FORMAT_TRY` and unsupported formats, violating the API and corrupting active state.
- [High] Banked register reads in the threaded interrupt handler and `get_detected_timings` omit mutex locking, causing data corruption.
- [High] An uninitialized stack variable `rxmsg` is passed to the CEC framework, leaking kernel stack memory to userspace.
- [High] The CEC interrupt handler uses an `else if` block, permanently dropping TX completion interrupts if an RX interrupt fires simultaneously.
- [High] Teardown asymmetry in `it6625_remove` cancels background work before unregistering the subdevice.
- [Medium] The `v4l2_ctrl_handler` memory is leaked on multiple probe error paths.
- [Medium] Incorrect parameters passed to `read_poll_timeout()` severely delay timeouts and defeat the polling mechanism.
- [Medium] EDID capacity querying and clearing fail with `-EINVAL` because `blocks == 0` is strictly rejected.
- [Medium] The debugfs handler `it6625_mipi_reg_write` misuses `simple_write_to_buffer` and parses uninitialized stack memory.
- [Medium] `it6625_cec_adap_transmit` sets the hardware transmit trigger but fails to wait for confirmation, risking aborted transmissions.
- [Low] The `i2c_device_id` array uses positional rather than C99 designated initializers.
--
--- Patch [2]: [PATCH 2/2] media: i2c: add driver for ITE IT6625/IT6626 ---
commit 09bd00239552da9315d71f96392b0c2b3c5769e2
Author: Hermes Wu <Hermes.wu@ite.com.tw>
media: i2c: add driver for ITE IT6625/IT6626
Add a V4L2 subdevice driver for the ITE IT6625/IT6626 HDMI-to-MIPI
CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and IT6626 an
HDMI 2.1 input, converting it to a D-PHY (or C/D-PHY on IT6626)
MIPI CSI-2 output. The bridge is configured over I2C, exposes an
HDMI CEC adapter, and supports EDID read/write, DV timings
detection/configuration, and HPD control via the standard V4L2
subdevice pad and video ops.
diff --git a/drivers/media/i2c/it6625.c b/drivers/media/i2c/it6625.c
new file mode 100644
index 0000000000000..2c1200dd273b4
--- /dev/null
+++ b/drivers/media/i2c/it6625.c
@@ -0,0 +1,2088 @@
[ ... ]
> +static int it6625_wait_for_status(struct it6625 *it6625, u8 reg, u8 val,
> + int timeout_ms)
> +{
> + struct device *dev = it6625->dev;
> + int status;
> + int rval;
> +
> + status = read_poll_timeout(it6625_read_byte, rval, rval == val,
> + 50 * 1000,
> + timeout_ms * 1000,
> + false, it6625, reg);
[Severity: Medium]
Are the parameters to read_poll_timeout() reversed here?
The sleep_us parameter is passed as 50 * 1000 (50ms) while timeout_us is
timeout_ms * 1000 (e.g. 25ms in some callers). Because the sleep duration is
twice the length of the total timeout, if the condition is not met
immediately, won't this sleep for 50ms and unconditionally time out on the
next check, effectively breaking the polling behavior?
[ ... ]
> +static int it6625_read_edid(struct it6625 *it6625, u8 *edid, int start_block,
> + int num_blocks)
> +{
> + int i, bank_ctrl, err = 0;
> + struct device *dev = it6625->dev;
> +
> + if (!edid) {
> + dev_err(dev, "edid buffer is NULL");
> + return -EINVAL;
> + }
> +
> + if (start_block < 0 || num_blocks <= 0 ||
> + start_block + num_blocks > EDID_NUM_BLOCKS_MAX) {
> + dev_err(dev,
> + "invalid block range: start_block=%d, num_blocks=%d",
> + start_block, num_blocks);
> + return -EINVAL;
> + }
[Severity: Medium]
Will rejecting num_blocks <= 0 break standard V4L2 EDID operations?
The V4L2 specification defines VIDIOC_SUBDEV_G_EDID with blocks = 0 as the
command to query the maximum supported blocks. Since this unconditionally
returns -EINVAL for 0, how does userspace query capacity?
[ ... ]
> +static int it6625_write_edid(struct it6625 *it6625, u8 *edid, int start_block,
> + int num_blocks)
> +{
> + int i, bank_ctrl, err = 0;
> + struct device *dev = it6625->dev;
> +
> + if (start_block < 0 || num_blocks <= 0 ||
> + start_block + num_blocks > EDID_NUM_BLOCKS_MAX) {
> + dev_err(dev,
> + "invalid block range: start_block=%d, num_blocks=%d",
> + start_block, num_blocks);
> + return -EINVAL;
> + }
[Severity: Medium]
Will this prevent userspace from clearing the EDID?
VIDIOC_SUBDEV_S_EDID with blocks = 0 is defined as the command to clear the
EDID. Unconditionally rejecting 0 here appears to break that functionality.
[ ... ]
> +static int it6625_get_detected_timings(struct it6625 *it6625,
> + struct v4l2_dv_timings *timings)
> +{
> + struct v4l2_bt_timings *bt = &timings->bt;
> + int val;
> + unsigned int width, height, frame_width, frame_height;
> + u8 buffer[8];
> +
> + if (no_signal(it6625)) {
> + dev_err(it6625->dev, "no signal detected");
> + return -ENOLINK;
> + }
> +
> + memset(timings, 0, sizeof(struct v4l2_dv_timings));
> + timings->type = V4L2_DV_BT_656_1120;
> + val = it6625_read_byte(it6625, REG_VID_INFO);
> + if (val < 0) {
> + dev_err(it6625->dev, "failed to read video info");
> + return -EIO;
> + }
> +
> + bt->interlaced = val & B_INTERLACE ?
> + V4L2_DV_INTERLACED : V4L2_DV_PROGRESSIVE;
> +
> + if (it6625_read_bytes(it6625, REG_H_ACTIVE_1, buffer, 8) < 0)
> + return -EIO;
[Severity: High]
Can this read from the wrong register bank if a concurrent EDID read is in
progress?
The chip appears to use a banked register layout where REG_INFO_BANK_SEL
swaps the meaning of registers. While it6625_read_edid() correctly acquires
it6625_lock to switch the bank, this function reads from REG_VID_INFO (0x68)
without acquiring the mutex.
If a format query runs concurrently with an EDID read, could it silently read
EDID data instead of Video Info?
[ ... ]
> +static int it6625_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
> + u32 signal_free_time, struct cec_msg *msg)
> +{
> + struct it6625 *it6625 = adap->priv;
> +
> + dump_cec_msg(it6625, msg, "CEC_TX");
> +
> + guard(mutex)(&it6625->it6625_lock);
> + it6625_write_bytes(it6625, REG_CEC_TX_DATA, msg->msg, msg->len);
> + it6625_write_byte(it6625, REG_CEC_TX_DATA_LEN, msg->len);
> + it6625_set_bits(it6625, REG_HOST_CTRL_INT, B_CEC_SEND_DATA, B_CEC_SEND_DATA);
> +
> + return 0;
> +}
[Severity: Medium]
Does this command need to wait for hardware acceptance before releasing the
lock?
Other functions using REG_HOST_CTRL_INT wait for the hardware to clear the
trigger bit via it6625_wait_for_status().
If another thread acquires the mutex immediately after this returns and calls
it6625_set_bank(), it will write to REG_HOST_CTRL_INT directly. Since it uses
a bare write rather than set_bits, won't that clear B_CEC_SEND_DATA before
the hardware has latched the command, aborting the transmission?
[ ... ]
> +static void it6625_cec_handler(struct it6625 *it6625, u8 intstatus)
> +{
> + struct cec_msg rxmsg;
> + int val = 0;
[Severity: High]
Could this leak uninitialized kernel stack memory to userspace?
The rxmsg structure is allocated on the stack, but only the msg array and
len field are populated below before passing it to cec_received_msg(). The
CEC core initializes several tracking fields but leaves others (like timeout,
reply, and compiler padding) untouched.
When the CEC framework copies this to userspace via the CEC_RECEIVE ioctl,
won't the uninitialized bytes be leaked?
> +
> + if (intstatus & B_CEC_RX_RECEIVED) {
> + val = it6625_read_byte(it6625, REG_CEC_RX_DATA_LEN);
> + if (val <= 0 || val > 16) {
> + dev_err(it6625->dev, "invalid CEC RX length %d", val);
> + return;
> + }
[Severity: High]
If the RX data length is invalid, won't the early return immediately
terminate processing and guarantee any pending TX interrupt is lost?
> +
> + it6625_read_bytes(it6625, REG_CEC_RX_DATA, &rxmsg.msg[0], val);
[Severity: High]
Can this read the wrong data if an EDID read is happening concurrently?
Similar to it6625_get_detected_timings(), this reads from REG_CEC_RX_DATA
(0x20) without acquiring it6625_lock. If an EDID read has switched the bank,
won't this read EDID bytes instead of the CEC payload?
> + rxmsg.len = val;
> + it6625_write_byte(it6625, REG_CEC_RX_DATA_LEN, 0);
> + cec_received_msg(it6625->cec_adap, &rxmsg);
> + dump_cec_msg(it6625, &rxmsg, "CEC_RX");
> +
> + } else if (intstatus & B_CEC_TX_UPDATE) {
[Severity: High]
Will this logic permanently drop a TX completion interrupt if an RX interrupt
fires at the same time?
The TX and RX interrupt status checks are chained with an 'else if'. If both
B_CEC_RX_RECEIVED and B_CEC_TX_UPDATE are asserted, it looks like the TX
branch is skipped entirely, causing CEC transmission timeouts.
[ ... ]
> +static int it6625_get_fmt(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *sd_state,
> + struct v4l2_subdev_format *format)
> +{
> + struct it6625 *it6625 = sd_to_6625(sd);
> + struct v4l2_dv_timings timings;
> +
> + if (format->pad != 0)
> + return -EINVAL;
> +
> + format->format.code = it6625->mbus_fmt_code;
> + it6625_get_timings(it6625, &timings);
[Severity: High]
Does this ignore format->which and always return the active format instead of
the TRY state when V4L2_SUBDEV_FORMAT_TRY is requested?
[ ... ]
> +static int it6625_set_fmt(struct v4l2_subdev *sd,
> + struct v4l2_subdev_state *sd_state,
> + struct v4l2_subdev_format *format)
> +{
[ ... ]
> + ret = it6625_csi_mbus_code_idx(mbus_fmt_code);
> +
> + if (ret < 0) {
> + v4l2_dbg(1, debug, sd, "%s: unsupported format code 0x%x",
> + __func__, mbus_fmt_code);
> + return ret;
> + }
> +
> + it6625->csi_format = it6625_formats[ret].csi_format;
> +
> + if (format->which == V4L2_SUBDEV_FORMAT_TRY) {
[Severity: High]
Does this unconditionally mutate the active hardware state
(it6625->csi_format) before checking if it is a V4L2_SUBDEV_FORMAT_TRY
request?
It looks like a TRY_FMT ioctl would silently corrupt the active hardware
configuration.
Also, if the requested format code is unsupported, this returns -EINVAL
instead of falling back to a supported format as required by the V4L2 API.
Could this cause issues with generic userspace tools?
[ ... ]
> +static ssize_t it6625_mipi_reg_write(struct file *file,
> + const char __user *user_buf,
> + size_t count, loff_t *ppos)
> +{
> + struct it6625 *it6625 = file_inode(file)->i_private;
> + char buf[32];
> + unsigned int addr, val;
> + ssize_t len;
> +
> + len = simple_write_to_buffer(buf, sizeof(buf) - 1, ppos, user_buf, count);
> + if (len < 0)
> + return len;
> + buf[len] = '\0';
> +
> + if (sscanf(buf, "%X %X", &addr, &val) != 2)
[Severity: Medium]
Is it safe to pass buf to sscanf if *ppos is greater than zero?
simple_write_to_buffer() writes the user data to buf + *ppos. If a user
performs a write with an offset (*ppos > 0), the beginning of the buf array
remains uninitialized stack memory. sscanf() will then parse this random
garbage, potentially leading to unpredictable register writes.
[ ... ]
> +static int it6625_init_v4l2_subdev(struct it6625 *it6625)
> +{
[ ... ]
> + sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS;
> + if (it6625_v4l2_init_controls(sd)) {
> + dev_err(it6625->dev, "Failed to initialize v4l2 controls");
> + return -ENOMEM;
> + }
> +
> + it6625->pad.flags = MEDIA_PAD_FL_SOURCE;
> + sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
> + err = media_entity_pads_init(&sd->entity, 1, &it6625->pad);
> + if (err < 0) {
> + dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err);
> + return err;
> + }
[Severity: Medium]
If media_entity_pads_init() fails, does this leak the v4l2_ctrl_handler
memory?
The controls are successfully initialized via it6625_v4l2_init_controls()
above, but the error path returns directly without freeing them.
> +
> + err = v4l2_async_register_subdev(sd);
> + if (err < 0) {
> + dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err);
> + media_entity_cleanup(&sd->entity);
> + return err;
> + }
[Severity: High]
Is the subdevice registered prematurely here?
v4l2_async_register_subdev() exposes the subdevice to the system. However,
critical setup like v4l2_ctrl_handler_setup(), cec_allocate_adapter(), and
it6625_initial_setup() happens later in probe.
If a host driver binds and calls subdev operations immediately after
registration, couldn't it operate on uninitialized chip state or trigger
NULL pointer dereferences on the CEC adapter?
[ ... ]
> +static int it6625_probe(struct i2c_client *client)
> +{
[ ... ]
> +err_clean_hdl:
> + v4l2_ctrl_handler_free(&it6625->hdl);
> +err_clean_media_entity:
> + v4l2_async_unregister_subdev(sd);
> + v4l2_device_unregister_subdev(sd);
> + media_entity_cleanup(&sd->entity);
> +
> +err_clean_work_queues:
> + if (!client->irq)
> + timer_shutdown_sync(&it6625->timer);
> + cancel_work_sync(&it6625->polling_work);
> + cancel_delayed_work(&it6625->hpd_delayed_work);
> + mutex_destroy(&it6625->it6625_lock);
> + return err;
> +}
[Severity: Medium]
Do the err_clean_media_entity and err_clean_work_queues paths bypass the
control handler cleanup?
If v4l2_ctrl_handler_setup() or CEC setup fail, jumping to these labels
permanently leaks the allocated v4l2_ctrl_handler memory.
[Severity: High]
Does using a non-synchronous cancel here lead to a use-after-free?
If probe fails late in the process, the prematurely registered subdevice
could have already received an s_edid ioctl, which schedules
hpd_delayed_work. If cancel_delayed_work() is used instead of the
synchronous variant, the work function can execute concurrently while the
driver destroys it6625_lock and frees the it6625 structure, causing a crash.
[ ... ]
> +static void it6625_remove(struct i2c_client *client)
> +{
> + struct v4l2_subdev *sd = i2c_get_clientdata(client);
> + struct it6625 *it6625 = sd_to_6625(sd);
> +
> + if (client->irq)
> + disable_irq(client->irq);
> + else
> + timer_shutdown_sync(&it6625->timer);
> +
> + cancel_work_sync(&it6625->polling_work);
> + cancel_delayed_work_sync(&it6625->hpd_delayed_work);
> + debugfs_remove_recursive(it6625->debugfs_dir);
> + cec_unregister_adapter(it6625->cec_adap);
> + v4l2_async_unregister_subdev(sd);
[Severity: High]
Is it safe to cancel the background work before unregistering the subdevice?
Since the subdevice is still actively registered when
cancel_delayed_work_sync() is called, an ioctl like s_edid can immediately
call it6625_enable_hpd() and reschedule the canceled work. When remove()
finishes and frees the it6625 structure, the rescheduled work will
subsequently dereference freed memory.
[ ... ]
> +static const struct i2c_device_id it6625_id[] = {
> + { "it6625", IT6625_CHIP },
> + { "it6626", IT6626_CHIP },
> + {}
> +};
[Severity: Low]
Would it be better to use C99 designated initializers here?
Using positional initialization relies on the exact member order in
struct i2c_device_id.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260720-upstream-it6625-it6626-v1-0-475912f74459@ite.com.tw?part=2
^ permalink raw reply related
* Re: [PATCH v5 2/5] arm64: dts: qcom: sm8550-qrd: add SPMI ADC channels and thermal nodes
From: Neil Armstrong @ 2026-07-20 8:06 UTC (permalink / raw)
To: Konrad Dybcio, Bjorn Andersson, Konrad Dybcio, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: linux-arm-msm, devicetree, linux-kernel
In-Reply-To: <04dfd2a8-a6b2-48d0-bd51-6cdca6250d38@oss.qualcomm.com>
On 7/7/26 12:21, Konrad Dybcio wrote:
> On 7/2/26 11:48 AM, Neil Armstrong wrote:
>> Add the SPMI ADC channels on the PMK8550 SPMI5 ADC3 for the
>> other PMICS on the system.
>>
>> The thermal nodes are sorted by the sensor channel to be
>> coherent with the system thermal nodes ordering.
>>
>> Signed-off-by: Neil Armstrong <neil.armstrong@linaro.org>
>> ---
>
> Reviewed-by: Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>
>
> Konrad
>
>
>> + channel@749 {
>> + reg = <ADC5_GEN3_AMUX6_THM_100K_PU(7)>;
>> + label = "pm8550b_wls_therm";
>> + qcom,ratiometric;
>> + /* use the default settle time */
>
> P.S. grumpy nit - I'd prefer if default was captured explicitly
> anyway
Ack will update,
Thanks,
Neil
>
> Konrad
^ permalink raw reply
* Re: [PATCH 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding
From: sashiko-bot @ 2026-07-20 8:06 UTC (permalink / raw)
To: Hermes Wu; +Cc: robh, conor+dt, devicetree, media-ci
In-Reply-To: <20260720-upstream-it6625-it6626-v1-1-475912f74459@ite.com.tw>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Missing schema constraint for 'bus-type' across IT6625 and IT6626 variants.
--
--- Patch [1]: [PATCH 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding ---
commit 0c73fe488fb2529ba9b003362cc54eefde27f9f8
Author: Hermes Wu <Hermes.wu@ite.com.tw>
dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding
This commit documents the devicetree binding for the ITE IT6625 and IT6626
HDMI to MIPI CSI-2 bridge devices. It defines the required properties,
including up to two selectable CSI-2 output ports.
> diff --git a/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml
> new file mode 100644
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml
[ ... ]
> + port@0:
[ ... ]
> + properties:
> + endpoint:
[ ... ]
> + properties:
> + data-lanes:
> + minItems: 1
> + maxItems: 4
> +
> + bus-type:
> + enum:
> + - 1 # MEDIA_BUS_TYPE_CSI2_CPHY
> + - 4 # MEDIA_BUS_TYPE_CSI2_DPHY
[Severity: Low]
The commit message mentions that IT6625 only supports D-PHY, while IT6626
supports C/D-PHY. Does this unconditional enum allow C-PHY to pass schema
validation for the IT6625? Should there be an allOf / if-then conditional
block restricting the bus-type to 4 for the ite,it6625 compatible?
Additionally, since the IT6626 can use either PHY, should bus-type be made
a required property when ite,it6626 is used to avoid ambiguous driver
configurations?
> +
> + clock-noncontinuous: true
> + link-frequencies: true
[ ... ]
> + port@1:
[ ... ]
> + properties:
> + endpoint:
[ ... ]
> + properties:
> + data-lanes:
> + minItems: 1
> + maxItems: 4
> +
> + bus-type:
> + enum:
> + - 1 # MEDIA_BUS_TYPE_CSI2_CPHY
> + - 4 # MEDIA_BUS_TYPE_CSI2_DPHY
[Severity: Low]
Would the same constraints and required properties for bus-type need to be
applied here for port@1?
> +
> + clock-noncontinuous: true
> + link-frequencies: true
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260720-upstream-it6625-it6626-v1-0-475912f74459@ite.com.tw?part=1
^ permalink raw reply
* [PATCH v5 11/11] arm64: dts: qcom: monaco-arduino-monza: Add QCA2066 M.2 WiFi/BT support
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain, Konrad Dybcio,
Dmitry Baryshkov, Bartosz Golaszewski
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
Add support for the QCA2066 (QCNFA765) WiFi/Bluetooth module on the
Arduino VENTUNO Q board. The module is interfaced via LGA and is
compatible with the M.2 Key E.
Add wireless-lga-connector node using pcie-m2-e-connector binding,
connecting PCIe port 0 to the WiFi interface and UART10 port 3 to
the Bluetooth interface.
Add pcie@1,0 downstream port node with pciclass,0604 compatible so
the pci-pwrctrl driver can acquire the power sequencer and enable
the M.2 slot before PCIe enumeration.
Add nfa725b_default_state pinctrl for the W_DISABLE1/2 GPIOs
(gpio56/gpio55) used by the power sequencer.
Reviewed-by: Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Reviewed-by: Manivannan Sadhasivam <mani@kernel.org>
Reviewed-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
arch/arm64/boot/dts/qcom/monaco-arduino-monza.dts | 64 +++++++++++++++++++++--
1 file changed, 60 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/boot/dts/qcom/monaco-arduino-monza.dts b/arch/arm64/boot/dts/qcom/monaco-arduino-monza.dts
index f67a554adf956696aeac2348e478dbd0b74f0e62..5e41e637c1c705ce0f3847aa71b229261a1e3664 100644
--- a/arch/arm64/boot/dts/qcom/monaco-arduino-monza.dts
+++ b/arch/arm64/boot/dts/qcom/monaco-arduino-monza.dts
@@ -155,6 +155,40 @@ vreg_nvme: regulator-3p3-m2 {
enable-active-high;
startup-delay-us = <20000>;
};
+
+ wireless-lga-connector {
+ compatible = "qcom,pcie-m2-1418-lga-connector",
+ "pcie-m2-e-connector";
+ vpcie3v3-supply = <&vdc_3v3>;
+ vpcie1v8-supply = <&vdc_1v8>;
+ w-disable1-gpios = <&tlmm 56 GPIO_ACTIVE_LOW>;
+ w-disable2-gpios = <&tlmm 55 GPIO_ACTIVE_LOW>;
+ pinctrl-0 = <&nfa725b_default_state>;
+ pinctrl-names = "default";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ /* WiFi/PCIe */
+ port@0 {
+ reg = <0>;
+
+ lga_pcie_ep: endpoint {
+ remote-endpoint = <&pcie_ep>;
+ };
+ };
+
+ /* Bluetooth/UART */
+ port@3 {
+ reg = <3>;
+
+ lga_uart_ep: endpoint {
+ remote-endpoint = <&uart10_ep>;
+ };
+ };
+ };
+ };
};
&cci1 {
@@ -409,6 +443,22 @@ pci@0,0 {
ranges;
reg = <0x010000 0x00 0x00 0x00 0x00>;
+ pcie@1,0 {
+ #address-cells = <3>;
+ #size-cells = <2>;
+ device_type = "pci";
+ compatible = "pciclass,0604";
+ bus-range = <0x00 0xff>;
+ ranges;
+ reg = <0x020800 0x00 0x00 0x00 0x00>;
+
+ port {
+ pcie_ep: endpoint {
+ remote-endpoint = <&lga_pcie_ep>;
+ };
+ };
+ };
+
pci@2,0 {
#address-cells = <3>;
#size-cells = <2>;
@@ -501,6 +551,12 @@ max98091_default: max98091-default-state {
bias-pull-up;
};
+ nfa725b_default_state: nfa725b-default-state {
+ pins = "gpio55", "gpio56";
+ function = "gpio";
+ bias-disable;
+ };
+
pcie1_default_state: pcie1-default-state {
wake-pins {
pins = "gpio21";
@@ -544,10 +600,10 @@ &uart7 {
&uart10 {
status = "okay";
- bluetooth: bluetooth {
- compatible = "qcom,qca2066-bt";
- enable-gpios = <&tlmm 55 GPIO_ACTIVE_HIGH>;
- clocks = <&sleep_clk>;
+ port {
+ uart10_ep: endpoint {
+ remote-endpoint = <&lga_uart_ep>;
+ };
};
};
--
2.34.1
^ permalink raw reply related
* [PATCH v5 10/11] power: sequencing: pcie-m2: Add QCA2066 (QCNFA765) BT serdev ID
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain, Dmitry Baryshkov
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
Add PCI IDs for Qualcomm QCA2066/QCNFA765 to the M.2 serdev ID table,
mapping it to the qcom,qca2066-bt compatible string.
The Subsystem Vendor ID (SVID) 0x0108 distinguishes the QCA2066 from
the WCN6855.
This allows the pwrseq-pcie-m2 driver to automatically create the
Bluetooth serdev device when a QCA2066-based M.2 card is enumerated.
Reviewed-by: Manivannan Sadhasivam <mani@kernel.org>
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
drivers/power/sequencing/pwrseq-pcie-m2.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/drivers/power/sequencing/pwrseq-pcie-m2.c b/drivers/power/sequencing/pwrseq-pcie-m2.c
index 8e4cb778e31f66aa171ad9831d4213e17d2e6145..741bf593d1716d55494cae99d4452600555e53b3 100644
--- a/drivers/power/sequencing/pwrseq-pcie-m2.c
+++ b/drivers/power/sequencing/pwrseq-pcie-m2.c
@@ -216,6 +216,8 @@ static const struct pci_device_id pwrseq_m2_pci_ids[] = {
.driver_data = (kernel_ulong_t)"nxp,88w8987-bt" },
{ PCI_DEVICE(PCI_VENDOR_ID_PHILIPS, 0x3003),
.driver_data = (kernel_ulong_t)"nxp,88w8987-bt" },
+ { PCI_DEVICE_SUB(PCI_VENDOR_ID_QCOM, 0x1103, PCI_VENDOR_ID_QCOM, 0x0108),
+ .driver_data = (kernel_ulong_t)"qcom,qca2066-bt" },
{ PCI_DEVICE(PCI_VENDOR_ID_QCOM, 0x1103),
.driver_data = (kernel_ulong_t)"qcom,wcn6855-bt" },
{ PCI_DEVICE(PCI_VENDOR_ID_QCOM, 0x1107),
--
2.34.1
^ permalink raw reply related
* [PATCH v5 09/11] dt-bindings: connector: pcie-m2-e: Add vendor LGA connector compatible
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain,
Krzysztof Kozlowski, Bartosz Golaszewski
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
Some modules (e.g. the Qualcomm QCA2066/QCNFA765) expose the same M.2
Key E interface signals but are physically soldered as an LGA package
rather than plugged into a real M.2 Key E connector socket. Such designs
are not actual M.2 Key E connectors, so describe them with a dedicated
vendor-specific compatible while keeping "pcie-m2-e-connector" as a
fallback for the shared signal semantics.
Add "qcom,pcie-m2-1418-lga-connector" for the M.2 LGA 1418 found on
Arduino Ventuno Q (Monaco Monza).
Add "qcom,pcie-m2-1620-lga-connector" for the M.2 LGA 1620 found on
The Lenovo Thinkpad T14s.
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Reviewed-by: Manivannan Sadhasivam <mani@kernel.org>
Reviewed-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
.../devicetree/bindings/connector/pcie-m2-e-connector.yaml | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/Documentation/devicetree/bindings/connector/pcie-m2-e-connector.yaml b/Documentation/devicetree/bindings/connector/pcie-m2-e-connector.yaml
index f7859aa9b63469cb43919c0b5719c18694c5364d..ce6173a0ec41cfe8ab4a5780940999c18674ab7e 100644
--- a/Documentation/devicetree/bindings/connector/pcie-m2-e-connector.yaml
+++ b/Documentation/devicetree/bindings/connector/pcie-m2-e-connector.yaml
@@ -17,7 +17,13 @@ description:
properties:
compatible:
- const: pcie-m2-e-connector
+ oneOf:
+ - const: pcie-m2-e-connector
+ - items:
+ - enum:
+ - qcom,pcie-m2-1418-lga-connector
+ - qcom,pcie-m2-1620-lga-connector
+ - const: pcie-m2-e-connector
vpcie3v3-supply:
description: A phandle to the regulator for 3.3v supply.
--
2.34.1
^ permalink raw reply related
* [PATCH v5 08/11] Bluetooth: hci_qca: Support QCA2066 on M.2 connector via pwrseq
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
For QCA2066 (and other QCA chips) on M.2 connectors, the UART enable is
controlled by the W_DISABLE2# signal managed by the pcie-m2 power sequencer
rather than a dedicated BT enable GPIO.
When the serdev controller has an OF graph (indicating it is connected to
an M.2 connector), acquire the 'uart' pwrseq target from the connector's
power sequencer and use it to control BT power instead of the bt-enable
GPIO. This is factored out into qca_serdev_get_m2_pwrseq().
Reviewed-by: Manivannan Sadhasivam <mani@kernel.org>
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
drivers/bluetooth/hci_qca.c | 47 +++++++++++++++++++++++++++++++++++----------
1 file changed, 37 insertions(+), 10 deletions(-)
diff --git a/drivers/bluetooth/hci_qca.c b/drivers/bluetooth/hci_qca.c
index 7e4cb03d3db0722e5f9fa2e7a5a9a0894bf4f18c..8496bd428602c48d093bc76f9810abb9b1c43cfa 100644
--- a/drivers/bluetooth/hci_qca.c
+++ b/drivers/bluetooth/hci_qca.c
@@ -1872,6 +1872,9 @@ static int qca_power_on(struct hci_dev *hdev)
/* Controller needs time to bootup. */
msleep(150);
}
+
+ if (qcadev->bt_power.pwrseq)
+ pwrseq_power_on(qcadev->bt_power.pwrseq);
}
clear_bit(QCA_BT_OFF, &qca->flags);
@@ -2387,6 +2390,34 @@ static int qca_init_regulators(struct qca_power *qca,
return 0;
}
+static void qca_serdev_put_pwrseq(void *data)
+{
+ pwrseq_put(data);
+}
+
+static int qca_serdev_get_m2_pwrseq(struct qca_serdev *qcadev)
+{
+ struct serdev_device *serdev = qcadev->serdev_hu.serdev;
+ struct pwrseq_desc *pwrseq;
+
+ if (!of_graph_is_present(dev_of_node(&serdev->ctrl->dev)))
+ return 0;
+
+ /* The pwrseq is looked up on the serdev controller (which holds the
+ * OF graph to the M.2 connector), but its lifetime must follow this
+ * serdev consumer device, not the controller. So acquire it with the
+ * non-devres pwrseq_get() and release it via a devres action bound to
+ * &serdev->dev instead of using devm_pwrseq_get(&serdev->ctrl->dev).
+ */
+ pwrseq = pwrseq_get(&serdev->ctrl->dev, "uart");
+ if (IS_ERR(pwrseq))
+ return PTR_ERR(pwrseq);
+
+ qcadev->bt_power.pwrseq = pwrseq;
+
+ return devm_add_action_or_reset(&serdev->dev, qca_serdev_put_pwrseq, pwrseq);
+}
+
static int qca_serdev_probe(struct serdev_device *serdev)
{
struct qca_serdev *qcadev;
@@ -2417,6 +2448,10 @@ static int qca_serdev_probe(struct serdev_device *serdev)
else
qcadev->btsoc_type = QCA_ROME;
+ err = qca_serdev_get_m2_pwrseq(qcadev);
+ if (err)
+ return err;
+
switch (qcadev->btsoc_type) {
case QCA_WCN3950:
case QCA_WCN3988:
@@ -2426,17 +2461,9 @@ static int qca_serdev_probe(struct serdev_device *serdev)
case QCA_WCN6750:
case QCA_WCN6855:
case QCA_WCN7850:
- /*
- * OF graph link is only present for BT devices attached through
- * the M.2 Key E connector.
- */
- if (of_graph_is_present(dev_of_node(&serdev->ctrl->dev))) {
- qcadev->bt_power.pwrseq = devm_pwrseq_get(&serdev->ctrl->dev,
- "uart");
- if (IS_ERR(qcadev->bt_power.pwrseq))
- return PTR_ERR(qcadev->bt_power.pwrseq);
+ /* M.2 connector modules are powered by the pwrseq acquired above. */
+ if (qcadev->bt_power.pwrseq)
break;
- }
if (!device_property_present(&serdev->dev, "enable-gpios")) {
/*
--
2.34.1
^ permalink raw reply related
* [PATCH v5 07/11] Bluetooth: hci_qca: Embed bt_power in struct qca_serdev
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain,
Bartosz Golaszewski
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
'bt_power' is allocated dynamically only for a subset of SOC types, using a
switch on the SOC type that mirrors the one that follows it. This is about
to become pointless as the power sequencer path (used by M.2 modules) is
independent of the SOC type and needs 'bt_power' populated for any type.
Embed 'struct qca_power' directly in 'struct qca_serdev' instead of
allocating it separately. This drops the extra allocation and its dedicated
SOC-type switch, and removes the need for NULL checks on the pointer.
No functional change intended.
Reviewed-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
drivers/bluetooth/hci_qca.c | 72 ++++++++++++++++-----------------------------
1 file changed, 26 insertions(+), 46 deletions(-)
diff --git a/drivers/bluetooth/hci_qca.c b/drivers/bluetooth/hci_qca.c
index 81fa41915363ce577ad7c6f25fe1b0a69536d462..7e4cb03d3db0722e5f9fa2e7a5a9a0894bf4f18c 100644
--- a/drivers/bluetooth/hci_qca.c
+++ b/drivers/bluetooth/hci_qca.c
@@ -226,7 +226,7 @@ struct qca_serdev {
struct gpio_desc *sw_ctrl;
struct clk *susclk;
enum qca_btsoc_type btsoc_type;
- struct qca_power *bt_power;
+ struct qca_power bt_power;
u32 init_speed;
u32 oper_speed;
bool bdaddr_property_broken;
@@ -1776,7 +1776,7 @@ static int qca_regulator_init(struct hci_uart *hu)
*/
qcadev = serdev_device_get_drvdata(hu->serdev);
- if (!qcadev->bt_power->vregs_on) {
+ if (!qcadev->bt_power.vregs_on) {
serdev_device_close(hu->serdev);
ret = qca_regulator_enable(qcadev);
if (ret)
@@ -2242,7 +2242,7 @@ static void qca_power_off(struct hci_uart *hu)
return;
qcadev = serdev_device_get_drvdata(hu->serdev);
- power = qcadev->bt_power;
+ power = &qcadev->bt_power;
switch (soc_type) {
case QCA_WCN3988:
@@ -2256,7 +2256,7 @@ static void qca_power_off(struct hci_uart *hu)
break;
}
- if (power && power->pwrseq) {
+ if (power->pwrseq) {
pwrseq_power_off(power->pwrseq);
set_bit(QCA_BT_OFF, &qca->flags);
return;
@@ -2313,7 +2313,7 @@ static int qca_hci_shutdown(struct hci_dev *hdev)
static int qca_regulator_enable(struct qca_serdev *qcadev)
{
- struct qca_power *power = qcadev->bt_power;
+ struct qca_power *power = &qcadev->bt_power;
int ret;
if (power->pwrseq)
@@ -2345,7 +2345,7 @@ static void qca_regulator_disable(struct qca_serdev *qcadev)
if (!qcadev)
return;
- power = qcadev->bt_power;
+ power = &qcadev->bt_power;
/* Already disabled? */
if (!power->vregs_on)
@@ -2417,26 +2417,6 @@ static int qca_serdev_probe(struct serdev_device *serdev)
else
qcadev->btsoc_type = QCA_ROME;
- switch (qcadev->btsoc_type) {
- case QCA_QCA6390:
- case QCA_WCN3950:
- case QCA_WCN3988:
- case QCA_WCN3990:
- case QCA_WCN3991:
- case QCA_WCN3998:
- case QCA_WCN6750:
- case QCA_WCN6855:
- case QCA_WCN7850:
- qcadev->bt_power = devm_kzalloc(&serdev->dev,
- sizeof(struct qca_power),
- GFP_KERNEL);
- if (!qcadev->bt_power)
- return -ENOMEM;
- break;
- default:
- break;
- }
-
switch (qcadev->btsoc_type) {
case QCA_WCN3950:
case QCA_WCN3988:
@@ -2451,10 +2431,10 @@ static int qca_serdev_probe(struct serdev_device *serdev)
* the M.2 Key E connector.
*/
if (of_graph_is_present(dev_of_node(&serdev->ctrl->dev))) {
- qcadev->bt_power->pwrseq = devm_pwrseq_get(&serdev->ctrl->dev,
- "uart");
- if (IS_ERR(qcadev->bt_power->pwrseq))
- return PTR_ERR(qcadev->bt_power->pwrseq);
+ qcadev->bt_power.pwrseq = devm_pwrseq_get(&serdev->ctrl->dev,
+ "uart");
+ if (IS_ERR(qcadev->bt_power.pwrseq))
+ return PTR_ERR(qcadev->bt_power.pwrseq);
break;
}
@@ -2465,8 +2445,8 @@ static int qca_serdev_probe(struct serdev_device *serdev)
* let's use the power sequencer. Otherwise, let's
* drive everything ourselves.
*/
- qcadev->bt_power->pwrseq = devm_pwrseq_get(&serdev->dev,
- "bluetooth");
+ qcadev->bt_power.pwrseq = devm_pwrseq_get(&serdev->dev,
+ "bluetooth");
/*
* Some modules have BT_EN enabled via a hardware pull-up,
@@ -2474,21 +2454,21 @@ static int qca_serdev_probe(struct serdev_device *serdev)
* through the power sequence. In such cases, fall through
* to follow the legacy flow.
*/
- if (IS_ERR(qcadev->bt_power->pwrseq))
- qcadev->bt_power->pwrseq = NULL;
+ if (IS_ERR(qcadev->bt_power.pwrseq))
+ qcadev->bt_power.pwrseq = NULL;
else
break;
}
- qcadev->bt_power->dev = &serdev->dev;
- err = qca_init_regulators(qcadev->bt_power, data->vregs,
+ qcadev->bt_power.dev = &serdev->dev;
+ err = qca_init_regulators(&qcadev->bt_power, data->vregs,
data->num_vregs);
if (err) {
BT_ERR("Failed to init regulators:%d", err);
return err;
}
- qcadev->bt_power->vregs_on = false;
+ qcadev->bt_power.vregs_on = false;
qcadev->bt_en = devm_gpiod_get_optional(&serdev->dev, "enable",
GPIOD_OUT_LOW);
@@ -2522,10 +2502,10 @@ static int qca_serdev_probe(struct serdev_device *serdev)
case QCA_QCA6390:
if (dev_of_node(&serdev->dev)) {
- qcadev->bt_power->pwrseq = devm_pwrseq_get(&serdev->dev,
- "bluetooth");
- if (IS_ERR(qcadev->bt_power->pwrseq))
- return PTR_ERR(qcadev->bt_power->pwrseq);
+ qcadev->bt_power.pwrseq = devm_pwrseq_get(&serdev->dev,
+ "bluetooth");
+ if (IS_ERR(qcadev->bt_power.pwrseq))
+ return PTR_ERR(qcadev->bt_power.pwrseq);
break;
}
fallthrough;
@@ -2549,16 +2529,16 @@ static int qca_serdev_probe(struct serdev_device *serdev)
}
}
- if (qcadev->bt_power->pwrseq) {
- bt_en_available = pwrseq_is_controllable(qcadev->bt_power->pwrseq);
+ if (qcadev->bt_power.pwrseq) {
+ bt_en_available = pwrseq_is_controllable(qcadev->bt_power.pwrseq);
if (!bt_en_available) {
/* The host cannot gate the BT power individually.
* Treat it as always-on and drop the pwrseq handle.
* The descriptor itself is still released by devres,
* so dropping the handle here is not a leak.
*/
- pwrseq_power_on(qcadev->bt_power->pwrseq);
- qcadev->bt_power->pwrseq = NULL;
+ pwrseq_power_on(qcadev->bt_power.pwrseq);
+ qcadev->bt_power.pwrseq = NULL;
}
}
@@ -2596,7 +2576,7 @@ static int qca_serdev_probe(struct serdev_device *serdev)
static void qca_serdev_remove(struct serdev_device *serdev)
{
struct qca_serdev *qcadev = serdev_device_get_drvdata(serdev);
- struct qca_power *power = qcadev->bt_power;
+ struct qca_power *power = &qcadev->bt_power;
switch (qcadev->btsoc_type) {
case QCA_WCN3988:
--
2.34.1
^ permalink raw reply related
* [PATCH v5 06/11] Bluetooth: hci_qca: Set 'bt_en_available' based on pwrseq power controllability
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
A Bluetooth device only needs the non-persistent setup if the host can
gate its power, so that it is actually reset on the next power-on. When the
power is controlled by a power sequencer, whether the host can gate it
depends on the hardware wiring, e.g. the presence of the BT_EN or
W_DISABLE2# line.
Query the generic pwrseq_is_controllable() helper whenever the BT power
comes from a sequencer, regardless of which provider it is (M2, WCN, ...).
If the power is not controllable, clear 'bt_en_available' so that
HCI_QUIRK_NON_PERSISTENT_SETUP is not set.
This is based on Manivannan's original patch that keyed the decision off
the W_DISABLE2# device tree property, it now uses the generic pwrseq helper
and handles the always-on case.
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
drivers/bluetooth/hci_qca.c | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/drivers/bluetooth/hci_qca.c b/drivers/bluetooth/hci_qca.c
index 17f436484e1e74d1dd690ada22134315abb767c4..81fa41915363ce577ad7c6f25fe1b0a69536d462 100644
--- a/drivers/bluetooth/hci_qca.c
+++ b/drivers/bluetooth/hci_qca.c
@@ -2548,7 +2548,20 @@ static int qca_serdev_probe(struct serdev_device *serdev)
return PTR_ERR(qcadev->susclk);
}
}
-
+
+ if (qcadev->bt_power->pwrseq) {
+ bt_en_available = pwrseq_is_controllable(qcadev->bt_power->pwrseq);
+ if (!bt_en_available) {
+ /* The host cannot gate the BT power individually.
+ * Treat it as always-on and drop the pwrseq handle.
+ * The descriptor itself is still released by devres,
+ * so dropping the handle here is not a leak.
+ */
+ pwrseq_power_on(qcadev->bt_power->pwrseq);
+ qcadev->bt_power->pwrseq = NULL;
+ }
+ }
+
err = hci_uart_register_device(&qcadev->serdev_hu, &qca_proto);
if (err) {
BT_ERR("serdev registration failed");
--
2.34.1
^ permalink raw reply related
* [PATCH v5 05/11] power: sequencing: qcom-wcn: Report power controllability
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
The "bluetooth" and "wlan" targets drive the BT_EN and WLAN_EN GPIOs to
gate the respective functions. These GPIOs are optional, on some boards
they are hardwired to an always-on pull-up and not routed to a host GPIO.
In that case the corresponding enable/disable callbacks are no-ops, so the
consumer cannot gate or reset that function on its own.
Implement the .is_controllable() callback on the "bluetooth" and "wlan"
targets so that consumers can query this via the pwrseq API.
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
drivers/power/sequencing/pwrseq-qcom-wcn.c | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/drivers/power/sequencing/pwrseq-qcom-wcn.c b/drivers/power/sequencing/pwrseq-qcom-wcn.c
index b55b4317e21b626e050e022648147770ca7f386e..19b51c94653b7f8e7e1f5a2fe906c0cc1cd46aa8 100644
--- a/drivers/power/sequencing/pwrseq-qcom-wcn.c
+++ b/drivers/power/sequencing/pwrseq-qcom-wcn.c
@@ -184,6 +184,19 @@ static int pwrseq_qcom_wcn_bt_disable(struct pwrseq_device *pwrseq)
return 0;
}
+static bool pwrseq_qcom_wcn_bt_is_controllable(struct pwrseq_device *pwrseq)
+{
+ struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
+
+ /*
+ * The Bluetooth enable is driven through the BT_EN GPIO. When it is not
+ * wired up (e.g. hardwired to an always-on pull-up), the enable/disable
+ * callbacks are no-ops, so the host cannot gate the Bluetooth function
+ * on its own.
+ */
+ return !!ctx->bt_gpio;
+}
+
static const struct pwrseq_unit_data pwrseq_qcom_wcn_bt_unit_data = {
.name = "bluetooth-enable",
.deps = pwrseq_qcom_wcn_unit_deps,
@@ -218,6 +231,19 @@ static int pwrseq_qcom_wcn_wlan_disable(struct pwrseq_device *pwrseq)
return 0;
}
+static bool pwrseq_qcom_wcn_wlan_is_controllable(struct pwrseq_device *pwrseq)
+{
+ struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
+
+ /*
+ * The WLAN enable is driven through the WLAN_EN GPIO. When it is not
+ * wired up (e.g. hardwired to an always-on pull-up), the enable/disable
+ * callbacks are no-ops, so the host cannot gate the WLAN function on
+ * its own.
+ */
+ return !!ctx->wlan_gpio;
+}
+
static const struct pwrseq_unit_data pwrseq_qcom_wcn_wlan_unit_data = {
.name = "wlan-enable",
.deps = pwrseq_qcom_wcn_unit_deps,
@@ -258,12 +284,14 @@ static const struct pwrseq_target_data pwrseq_qcom_wcn_bt_target_data = {
.name = "bluetooth",
.unit = &pwrseq_qcom_wcn_bt_unit_data,
.post_enable = pwrseq_qcom_wcn_pwup_delay,
+ .is_controllable = pwrseq_qcom_wcn_bt_is_controllable,
};
static const struct pwrseq_target_data pwrseq_qcom_wcn_wlan_target_data = {
.name = "wlan",
.unit = &pwrseq_qcom_wcn_wlan_unit_data,
.post_enable = pwrseq_qcom_wcn_pwup_delay,
+ .is_controllable = pwrseq_qcom_wcn_wlan_is_controllable,
};
/* There are no separate BT and WLAN enablement pins */
@@ -281,12 +309,14 @@ static const struct pwrseq_target_data pwrseq_qcom_wcn6855_bt_target_data = {
.name = "bluetooth",
.unit = &pwrseq_qcom_wcn6855_bt_unit_data,
.post_enable = pwrseq_qcom_wcn6855_xo_clk_deassert,
+ .is_controllable = pwrseq_qcom_wcn_bt_is_controllable,
};
static const struct pwrseq_target_data pwrseq_qcom_wcn6855_wlan_target_data = {
.name = "wlan",
.unit = &pwrseq_qcom_wcn6855_wlan_unit_data,
.post_enable = pwrseq_qcom_wcn6855_xo_clk_deassert,
+ .is_controllable = pwrseq_qcom_wcn_wlan_is_controllable,
};
static const struct pwrseq_target_data *pwrseq_qcom_wcn_targets[] = {
--
2.34.1
^ permalink raw reply related
* [PATCH v5 04/11] power: sequencing: pcie-m2: Report power controllability
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
The M.2 Key E connector gates/resets its functions through the
W_DISABLE1# (PCIe/WiFi) and W_DISABLE2# (Bluetooth) signals.
When a signal is not routed to a host GPIO, the corresponding
enable/disable callbacks are no-ops, so the consumer cannot gate
or reset that function on its own.
Implement the .is_controllable() callback on the "uart" and "pcie"
targets so that consumers can query this per instance (based on the
runtime presence of the W_DISABLE2#/W_DISABLE1# GPIOs) via
pwrseq_is_controllable().
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
drivers/power/sequencing/pwrseq-pcie-m2.c | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/drivers/power/sequencing/pwrseq-pcie-m2.c b/drivers/power/sequencing/pwrseq-pcie-m2.c
index e3ba9169144dabbf0c553c0a4302c3b511fcaaa1..8e4cb778e31f66aa171ad9831d4213e17d2e6145 100644
--- a/drivers/power/sequencing/pwrseq-pcie-m2.c
+++ b/drivers/power/sequencing/pwrseq-pcie-m2.c
@@ -84,6 +84,18 @@ static int pwrseq_pci_m2_e_uart_disable(struct pwrseq_device *pwrseq)
return gpiod_set_value_cansleep(ctx->w_disable2_gpio, 1);
}
+static bool pwrseq_pci_m2_e_uart_is_controllable(struct pwrseq_device *pwrseq)
+{
+ struct pwrseq_pcie_m2_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
+
+ /*
+ * The UART enable is driven through the W_DISABLE2# line. When it is not
+ * wired up on this connector the enable/disable callbacks are no-ops, so
+ * the host cannot gate the Bluetooth function on its own.
+ */
+ return !!ctx->w_disable2_gpio;
+}
+
static const struct pwrseq_unit_data pwrseq_pcie_m2_e_uart_unit_data = {
.name = "uart-enable",
.deps = pwrseq_pcie_m2_unit_deps,
@@ -105,6 +117,18 @@ static int pwrseq_pci_m2_e_pcie_disable(struct pwrseq_device *pwrseq)
return gpiod_set_value_cansleep(ctx->w_disable1_gpio, 1);
}
+static bool pwrseq_pci_m2_e_pcie_is_controllable(struct pwrseq_device *pwrseq)
+{
+ struct pwrseq_pcie_m2_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
+
+ /*
+ * The PCIe/WiFi enable is driven through the W_DISABLE1# line. When it
+ * is not wired up on this connector the enable/disable callbacks are
+ * no-ops, so the host cannot gate the PCIe/WiFi function on its own.
+ */
+ return !!ctx->w_disable1_gpio;
+}
+
static const struct pwrseq_unit_data pwrseq_pcie_m2_e_pcie_unit_data = {
.name = "pcie-enable",
.deps = pwrseq_pcie_m2_unit_deps,
@@ -133,12 +157,14 @@ static const struct pwrseq_target_data pwrseq_pcie_m2_e_uart_target_data = {
.name = "uart",
.unit = &pwrseq_pcie_m2_e_uart_unit_data,
.post_enable = pwrseq_pcie_m2_e_pwup_delay,
+ .is_controllable = pwrseq_pci_m2_e_uart_is_controllable,
};
static const struct pwrseq_target_data pwrseq_pcie_m2_e_pcie_target_data = {
.name = "pcie",
.unit = &pwrseq_pcie_m2_e_pcie_unit_data,
.post_enable = pwrseq_pcie_m2_e_pwup_delay,
+ .is_controllable = pwrseq_pci_m2_e_pcie_is_controllable,
};
static const struct pwrseq_target_data pwrseq_pcie_m2_m_pcie_target_data = {
--
2.34.1
^ permalink raw reply related
* [PATCH v5 03/11] power: sequencing: Add pwrseq_is_controllable() API
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
On some boards a power sequencing target has no host-controllable enable
for its function, for instance when the enable line is not wired up to a
GPIO and is hardwired to an always-on level. The pcie-m2 "uart" target is
one such example: when the M.2 connector does not route the W_DISABLE2#
signal to a host GPIO, its enable/disable are no-ops and the consumer
cannot gate the Bluetooth function at all or exclusively.
Add a generic pwrseq_is_controllable() helper. It reports whether the
target's final unit provides a host-controllable dedicated power actuator.
The target can implement a new optional is_controllable() callback,
reporting whether that actuator is effective on this target (for example
depending on GPIO presence). If the target does not provide the callback,
it is assumed to be controllable.
Note this only describes the target's own enable actuator. It does not
imply that a power-off reaches an electrical OFF state, since a target may
have multiple consumers. It also does not mean that power is uncontrolled
for the target's dependencies: those may still be gated on their own. And
it does not restrict consumers from calling pwrseq_power_off() either,
which remains valid to drop this consumer's vote on the (possibly shared)
resources and dependencies of the target.
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
drivers/power/sequencing/core.c | 49 +++++++++++++++++++++++++++++++++++++++++
include/linux/pwrseq/consumer.h | 7 ++++++
include/linux/pwrseq/provider.h | 8 +++++++
3 files changed, 64 insertions(+)
diff --git a/drivers/power/sequencing/core.c b/drivers/power/sequencing/core.c
index 02f42da915985339d3de507fc36dd158b0035a99..cabb3d0885f94d9d70e0631861ed6905d91ad3bc 100644
--- a/drivers/power/sequencing/core.c
+++ b/drivers/power/sequencing/core.c
@@ -182,12 +182,15 @@ static void pwrseq_unit_release(struct kref *ref)
* the state lock has been released. It's useful for implementing
* boot-up delays without blocking other users from powering up
* using the same power sequencer.
+ * @is_controllable: Optional callback reporting whether enabling/disabling
+ * this target actually controls power.
*/
struct pwrseq_target {
struct list_head list;
const char *name;
struct pwrseq_unit *unit;
pwrseq_power_state_func post_enable;
+ pwrseq_is_controllable_func is_controllable;
};
static struct pwrseq_target *
@@ -206,6 +209,7 @@ pwrseq_target_new(const struct pwrseq_target_data *data)
}
target->post_enable = data->post_enable;
+ target->is_controllable = data->is_controllable;
return target;
}
@@ -991,6 +995,51 @@ struct device *pwrseq_to_device(struct pwrseq_desc *desc)
}
EXPORT_SYMBOL_GPL(pwrseq_to_device);
+/**
+ * pwrseq_is_controllable() - Check whether the target provides a
+ * host-controllable power actuator.
+ * @desc: Descriptor referencing the power sequencer.
+ *
+ * Some power sequencing targets provide no host-controllable enable for their
+ * function on a given board, for instance when the enable line is not wired up
+ * and is instead hardwired to an always-on level. For such targets a call to
+ * pwrseq_power_off() is still allowed, so that the consumer can drop its vote
+ * on the (possibly shared) resources, but the host cannot gate the function
+ * on its own.
+ *
+ * Returns:
+ * True if the target provides a host-controllable power actuator, false
+ * otherwise. Also returns false if @desc is NULL.
+ */
+bool pwrseq_is_controllable(struct pwrseq_desc *desc)
+{
+ struct pwrseq_device *pwrseq;
+ struct pwrseq_target *target;
+ struct pwrseq_unit *unit;
+
+ might_sleep();
+
+ if (!desc)
+ return false;
+
+ pwrseq = desc->pwrseq;
+ target = desc->target;
+ unit = target->unit;
+
+ guard(rwsem_read)(&pwrseq->rw_lock);
+ if (!device_is_registered(&pwrseq->dev))
+ return false;
+
+ if (!unit->enable && !unit->disable)
+ return false;
+
+ if (!target->is_controllable)
+ return true;
+
+ return target->is_controllable(pwrseq);
+}
+EXPORT_SYMBOL_GPL(pwrseq_is_controllable);
+
#if IS_ENABLED(CONFIG_DEBUG_FS)
struct pwrseq_debugfs_count_ctx {
diff --git a/include/linux/pwrseq/consumer.h b/include/linux/pwrseq/consumer.h
index 3c907c9e1885dc2958043a9a733fbe20bdf95f6e..f70f694c19ab73f70ccf775aa24902bfcc982a42 100644
--- a/include/linux/pwrseq/consumer.h
+++ b/include/linux/pwrseq/consumer.h
@@ -25,6 +25,8 @@ int pwrseq_power_off(struct pwrseq_desc *desc);
struct device *pwrseq_to_device(struct pwrseq_desc *desc);
+bool pwrseq_is_controllable(struct pwrseq_desc *desc);
+
#else /* CONFIG_POWER_SEQUENCING */
static inline struct pwrseq_desc * __must_check
@@ -58,6 +60,11 @@ static inline struct device *pwrseq_to_device(struct pwrseq_desc *desc)
return NULL;
}
+static inline bool pwrseq_is_controllable(struct pwrseq_desc *desc)
+{
+ return false;
+}
+
#endif /* CONFIG_POWER_SEQUENCING */
#endif /* __POWER_SEQUENCING_CONSUMER_H__ */
diff --git a/include/linux/pwrseq/provider.h b/include/linux/pwrseq/provider.h
index 33b3d2c2e39decafac6c6fca9254ad4329d90e94..7285ad94a2214070a1c27f25c783f51105705b36 100644
--- a/include/linux/pwrseq/provider.h
+++ b/include/linux/pwrseq/provider.h
@@ -6,12 +6,15 @@
#ifndef __POWER_SEQUENCING_PROVIDER_H__
#define __POWER_SEQUENCING_PROVIDER_H__
+#include <linux/types.h>
+
struct device;
struct module;
struct pwrseq_device;
typedef int (*pwrseq_power_state_func)(struct pwrseq_device *);
typedef int (*pwrseq_match_func)(struct pwrseq_device *, struct device *);
+typedef bool (*pwrseq_is_controllable_func)(struct pwrseq_device *);
#define PWRSEQ_NO_MATCH 0
#define PWRSEQ_MATCH_OK 1
@@ -43,11 +46,16 @@ struct pwrseq_unit_data {
* the state lock has been released. It's useful for implementing
* boot-up delays without blocking other users from powering up
* using the same power sequencer.
+ * @is_controllable: Optional callback checking whether enabling/disabling this
+ * target actually controls power (for example when the
+ * controlling GPIO is wired up). If not provided, the
+ * target's power is assumed to be always controllable.
*/
struct pwrseq_target_data {
const char *name;
const struct pwrseq_unit_data *unit;
pwrseq_power_state_func post_enable;
+ pwrseq_is_controllable_func is_controllable;
};
/**
--
2.34.1
^ permalink raw reply related
* [PATCH v5 02/11] Bluetooth: hci_qca: Rename 'power_ctrl_enabled' to 'bt_en_available'
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain, Dmitry Baryshkov,
Bartosz Golaszewski, Wei Deng
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
'power_ctrl_enabled' flag is used to indicate the availability of the BT_EN
GPIO in devicetree. But the naming causes confusion with the new pwrctrl
framework.
So rename it to 'bt_en_available' to make it clear and explicit.
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
Reviewed-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Tested-by: Wei Deng <wei.deng@oss.qualcomm.com>
---
drivers/bluetooth/hci_qca.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/drivers/bluetooth/hci_qca.c b/drivers/bluetooth/hci_qca.c
index 135c79e696aefe2b37ee7a7a668715360a1e2d75..17f436484e1e74d1dd690ada22134315abb767c4 100644
--- a/drivers/bluetooth/hci_qca.c
+++ b/drivers/bluetooth/hci_qca.c
@@ -2393,7 +2393,7 @@ static int qca_serdev_probe(struct serdev_device *serdev)
struct hci_dev *hdev;
const struct qca_device_data *data;
int err;
- bool power_ctrl_enabled = true;
+ bool bt_en_available = true;
qcadev = devm_kzalloc(&serdev->dev, sizeof(*qcadev), GFP_KERNEL);
if (!qcadev)
@@ -2501,7 +2501,7 @@ static int qca_serdev_probe(struct serdev_device *serdev)
(data->soc_type == QCA_WCN6750 ||
data->soc_type == QCA_WCN6855 ||
data->soc_type == QCA_WCN7850))
- power_ctrl_enabled = false;
+ bt_en_available = false;
qcadev->sw_ctrl = devm_gpiod_get_optional(&serdev->dev, "swctrl",
GPIOD_IN);
@@ -2539,7 +2539,7 @@ static int qca_serdev_probe(struct serdev_device *serdev)
}
if (!qcadev->bt_en)
- power_ctrl_enabled = false;
+ bt_en_available = false;
qcadev->susclk = devm_clk_get_optional_enabled_with_rate(
&serdev->dev, NULL, SUSCLK_RATE_32KHZ);
@@ -2557,7 +2557,7 @@ static int qca_serdev_probe(struct serdev_device *serdev)
hdev = qcadev->serdev_hu.hdev;
- if (power_ctrl_enabled) {
+ if (bt_en_available) {
hci_set_quirk(hdev, HCI_QUIRK_NON_PERSISTENT_SETUP);
hdev->shutdown = qca_hci_shutdown;
}
--
2.34.1
^ permalink raw reply related
* [PATCH v5 01/11] Bluetooth: hci_qca: Add M.2 Bluetooth device support using pwrseq
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain,
Bartosz Golaszewski, Dmitry Baryshkov, Wei Deng
In-Reply-To: <20260720-monza-wireless-v5-0-69ec6743543c@oss.qualcomm.com>
From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
Power supply to the M.2 Bluetooth device attached to the host using M.2
connector is controlled using the 'uart' pwrseq device. So add support for
getting the pwrseq device if the OF graph link is present. Once obtained,
the existing pwrseq APIs can be used to control the power supplies of the
M.2 card.
Reviewed-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
Tested-by: Wei Deng <wei.deng@oss.qualcomm.com>
---
drivers/bluetooth/hci_qca.c | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/drivers/bluetooth/hci_qca.c b/drivers/bluetooth/hci_qca.c
index 2444471956197bae62a1c4c3a776c5591780ec02..135c79e696aefe2b37ee7a7a668715360a1e2d75 100644
--- a/drivers/bluetooth/hci_qca.c
+++ b/drivers/bluetooth/hci_qca.c
@@ -26,6 +26,7 @@
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/of.h>
+#include <linux/of_graph.h>
#include <linux/acpi.h>
#include <linux/platform_device.h>
#include <linux/pwrseq/consumer.h>
@@ -2445,6 +2446,18 @@ static int qca_serdev_probe(struct serdev_device *serdev)
case QCA_WCN6750:
case QCA_WCN6855:
case QCA_WCN7850:
+ /*
+ * OF graph link is only present for BT devices attached through
+ * the M.2 Key E connector.
+ */
+ if (of_graph_is_present(dev_of_node(&serdev->ctrl->dev))) {
+ qcadev->bt_power->pwrseq = devm_pwrseq_get(&serdev->ctrl->dev,
+ "uart");
+ if (IS_ERR(qcadev->bt_power->pwrseq))
+ return PTR_ERR(qcadev->bt_power->pwrseq);
+ break;
+ }
+
if (!device_property_present(&serdev->dev, "enable-gpios")) {
/*
* Backward compatibility with old DT sources. If the
--
2.34.1
^ permalink raw reply related
* [PATCH v5 00/11] arm64: dts: monaco-arduino-monza: Add support for LGA WiFi/BT module
From: Loic Poulain @ 2026-07-20 8:04 UTC (permalink / raw)
To: Manivannan Sadhasivam, Bartosz Golaszewski, Marcel Holtmann,
Luiz Augusto von Dentz, Bjorn Andersson, Konrad Dybcio,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: linux-pci, linux-pm, linux-kernel, linux-arm-msm, linux-bluetooth,
devicetree, Manivannan Sadhasivam, Loic Poulain,
Bartosz Golaszewski, Dmitry Baryshkov, Wei Deng,
Krzysztof Kozlowski, Konrad Dybcio
This series describes support for the onboard WiFi/Bluetooth module
present on the Arduino VENTUNO Q (monaco) platform.
The board provides LGA pads for a wireless module. On the VENTUNO Q
these pads are populated with an NFA725B module featuring the
QCA2066 WiFi/BT combo chip. While implemented as an LGA footprint,
the design is functionally compatible with the M.2 Key E.
The NFA725B exposes WiFi over PCIe and Bluetooth over a UART.
Both interfaces are gated through the W_DISABLE1# and W_DISABLE2#
signals, as defined by the M.2 specification and handled here via
the pcie-m2 power sequencer.
This series models the hardware using the existing pwrseq framework
and connector bindings, allowing coordinated PCIe and UART bring-up.
Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
---
Changes in v5:
- hci_qca: comment why the M.2 'uart' pwrseq lifetime is bound to the serdev
consumer device instead of using devm_pwrseq_get() on the controller (Bartosz).
- hci_qca: note that dropping the always-on pwrseq handle is not a leak since
the descriptor is still released by devres (Bartosz).
- Link to v4: https://lore.kernel.org/r/20260716-monza-wireless-v4-0-9b02e6f549d7@oss.qualcomm.com
Changes in v4:
- pwrseq: rename pwrseq_power_is_controllable() to pwrseq_is_controllable() (Bartosz).
- pwrseq: move the is_controllable() callback from the unit to the target (Bartosz).
- pwrseq: add might_sleep() and guard pwrseq_is_controllable() against
concurrent provider removal with rw_lock, matching pwrseq_power_on/off() (Bartosz).
- pwrseq: clarify the pwrseq_is_controllable() doc (Bartosz)
- hci_qca: lifetime follows the Bluetooth device rather than the UART controller (ChenYu).
- dt-bindings: also add the "qcom,pcie-m2-1620-lga-connector" compatible (Mani).
- arm64: dts: monaco-arduino-monza: rename the 'pcie_bridge_ep' endpoint to 'pcie_ep' (Mani).
- pwrseq: reword commmit message (Mani)
- Link to v3: https://lore.kernel.org/r/20260710-monza-wireless-v3-0-46253587af64@oss.qualcomm.com
Changes in v3:
- Embed bt_power in struct qca_serdev instead of allocating it (Dmitry).
- Add generic pwrseq_is_controllable() API + per-target .is_controllable()
callback; use it in hci_qca instead of peeking at the W_DISABLE2# property.
- Implement .is_controllable() in the pcie-m2 and qcom-wcn providers.
- Treat non-controllable BT power as always-on.
- Integrate the pending Bluetooth pwrseq patches from Mani.
- Link to v2: https://lore.kernel.org/r/20260702-monza-wireless-v2-0-7b56e2a6a6d4@oss.qualcomm.com
Changes in v2:
- hci_qca: extract M.2 pwrseq into qca_serdev_get_m2_pwrseq() helper, called
outside the SoC switch; drop duplicated of_graph logic and redundant
bt_power NULL checks (Dmitry/Mani)
- dts/bindings: use qcom,pcie-m2-1418-lga-connector (fallback
pcie-m2-e-connector) instead of the generic M.2 E-key compatible (Mani)
- Link to v1: https://lore.kernel.org/r/20260520-monza-wireless-v1-0-9f6942310653@oss.qualcomm.com
---
Loic Poulain (9):
power: sequencing: Add pwrseq_is_controllable() API
power: sequencing: pcie-m2: Report power controllability
power: sequencing: qcom-wcn: Report power controllability
Bluetooth: hci_qca: Set 'bt_en_available' based on pwrseq power controllability
Bluetooth: hci_qca: Embed bt_power in struct qca_serdev
Bluetooth: hci_qca: Support QCA2066 on M.2 connector via pwrseq
dt-bindings: connector: pcie-m2-e: Add vendor LGA connector compatible
power: sequencing: pcie-m2: Add QCA2066 (QCNFA765) BT serdev ID
arm64: dts: qcom: monaco-arduino-monza: Add QCA2066 M.2 WiFi/BT support
Manivannan Sadhasivam (2):
Bluetooth: hci_qca: Add M.2 Bluetooth device support using pwrseq
Bluetooth: hci_qca: Rename 'power_ctrl_enabled' to 'bt_en_available'
.../bindings/connector/pcie-m2-e-connector.yaml | 8 +-
arch/arm64/boot/dts/qcom/monaco-arduino-monza.dts | 64 ++++++++++-
drivers/bluetooth/hci_qca.c | 117 +++++++++++++--------
drivers/power/sequencing/core.c | 49 +++++++++
drivers/power/sequencing/pwrseq-pcie-m2.c | 28 +++++
drivers/power/sequencing/pwrseq-qcom-wcn.c | 30 ++++++
include/linux/pwrseq/consumer.h | 7 ++
include/linux/pwrseq/provider.h | 8 ++
8 files changed, 264 insertions(+), 47 deletions(-)
---
base-commit: aa61612ab641d7d62b0b6889f2c7c9251489f6e3
change-id: 20260520-monza-wireless-e6ce7f013f38
prerequisite-message-id: <20260507-pwrseq-m2-bt-v2-0-1740bd478539@oss.qualcomm.com>
prerequisite-patch-id: f4a7d1957c1776051608bf3d808b2786606c1ae2
prerequisite-patch-id: 6cd3c33583a9af16b3f6f71517b16b32d8155b7c
prerequisite-patch-id: 0550c57d69cf112fd4830e62f4388db6f8bf397c
prerequisite-patch-id: cc10d8079e37ef0ba0c33d0984c95d76361df9dd
prerequisite-patch-id: d7f4bb2bb4498ac619e67a94f8b59119a5caaf26
prerequisite-patch-id: c00ce9095b2d3a412229796194828b55642d3d96
prerequisite-patch-id: 09600595c2e80b12eda3aae39af192847d0f03d0
prerequisite-patch-id: a6118ed2894c176780ba933750e1068f2819fa4c
prerequisite-patch-id: 1dee41a33e032094e8dda74ac4e0bada928573d7
Best regards,
--
Loic Poulain <loic.poulain@oss.qualcomm.com>
^ permalink raw reply
* [PATCH 2/2] media: i2c: add driver for ITE IT6625/IT6626
From: Hermes Wu via B4 Relay @ 2026-07-20 7:57 UTC (permalink / raw)
To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: linux-media, devicetree, linux-kernel, Hermes Wu
In-Reply-To: <20260720-upstream-it6625-it6626-v1-0-475912f74459@ite.com.tw>
From: Hermes Wu <Hermes.wu@ite.com.tw>
Add a V4L2 subdevice driver for the ITE IT6625/IT6626 HDMI-to-MIPI
CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and IT6626 an
HDMI 2.1 input, converting it to a D-PHY (or C/D-PHY on IT6626)
MIPI CSI-2 output. The bridge is configured over I2C, exposes an
HDMI CEC adapter, and supports EDID read/write, DV timings
detection/configuration, and HPD control via the standard V4L2
subdevice pad and video ops.
Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw>
---
MAINTAINERS | 7 +
drivers/media/i2c/Kconfig | 18 +
drivers/media/i2c/Makefile | 1 +
drivers/media/i2c/it6625.c | 2088 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 2114 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 15011f5752a994cf1b354f490d6c4e411588df88..2d5b0ee107f9e3255a5365ec0a25167a716b1e6e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13847,6 +13847,13 @@ T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
F: Documentation/devicetree/bindings/display/bridge/ite,it66121.yaml
F: drivers/gpu/drm/bridge/ite-it66121.c
+ITE IT6625 HDMI to MIPI MEDIA DRIVER
+M: Hermes Wu <Hermes.Wu@ite.com.tw>
+S: Maintained
+T: git git://linuxtv.org/media.git
+F: Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml
+F: drivers/media/i2c/it6625.c
+
IVTV VIDEO4LINUX DRIVER
M: Andy Walls <awalls@md.metrocast.net>
L: linux-media@vger.kernel.org
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 5d173e0ecf424f2f204f8d426be818e44357f8e4..fb4b24684f7710f90eb399976d11f57701488d26 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -1278,6 +1278,24 @@ config VIDEO_ISL7998X
Support for Intersil ISL7998x analog to MIPI-CSI2 or
BT.656 decoder.
+config VIDEO_IT6625
+ tristate "IT6625 HDMI to MIPI CSI bridge"
+ depends on VIDEO_DEV && I2C
+ depends on OF
+ select CEC_CORE
+ select MEDIA_CONTROLLER
+ select REGMAP_I2C
+ select V4L2_FWNODE
+ select VIDEO_V4L2_SUBDEV_API
+ help
+ V4L2 subdevice driver for the ITE IT6625/IT6626 HDMI to MIPI
+ CSI-2 bridge chips. IT6625 accepts an HDMI 2.0 input and
+ IT6626 an HDMI 2.1 input, converting it to a MIPI CSI-2
+ output. The driver also exposes an HDMI CEC adapter.
+
+ To compile this driver as a module, choose M here: the
+ module will be called it6625.
+
config VIDEO_LT6911UXE
tristate "Lontium LT6911UXE decoder"
depends on ACPI && VIDEO_DEV && I2C
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index e45359efe0e41e13e3c0869e5ead7d6cf4aca3a7..9bbceaa3d07573958edb8a36bc7f0942f1410a39 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -63,6 +63,7 @@ obj-$(CONFIG_VIDEO_IMX412) += imx412.o
obj-$(CONFIG_VIDEO_IMX415) += imx415.o
obj-$(CONFIG_VIDEO_IR_I2C) += ir-kbd-i2c.o
obj-$(CONFIG_VIDEO_ISL7998X) += isl7998x.o
+obj-$(CONFIG_VIDEO_IT6625) += it6625.o
obj-$(CONFIG_VIDEO_KS0127) += ks0127.o
obj-$(CONFIG_VIDEO_LM3560) += lm3560.o
obj-$(CONFIG_VIDEO_LM3646) += lm3646.o
diff --git a/drivers/media/i2c/it6625.c b/drivers/media/i2c/it6625.c
new file mode 100644
index 0000000000000000000000000000000000000000..2c1200dd273b4d924c8f98530eb2c15b11bd5dae
--- /dev/null
+++ b/drivers/media/i2c/it6625.c
@@ -0,0 +1,2088 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * it6625 - ite HDMI to MIPI bridge
+ */
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/debugfs.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/hdmi.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of_graph.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/timer.h>
+#include <linux/v4l2-dv-timings.h>
+#include <linux/videodev2.h>
+#include <linux/workqueue.h>
+
+#include <media/cec.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-dv-timings.h>
+#include <media/v4l2-event.h>
+#include <media/v4l2-fwnode.h>
+
+static int debug = 3;
+module_param(debug, int, 0644);
+MODULE_PARM_DESC(debug, "debug level (0-3)");
+
+#define REG_CHIP_ID_0 0x00
+#define REG_CHIP_ID_1 0x01
+#define REG_FW_VER_MAJOR 0x03
+#define REG_FW_VER_MINOR 0x04
+#define REG_PROTOCOL_VERSION 0x05
+#define B_PVER_MINOR BIT(0)
+#define B_PVER_MAJOR BIT(4)
+
+#define REG_CMD_SET 0x10
+#define CMD_SET_CEC_LA 0xC0
+#define CMD_SET_CEC_ENABLE 0xC1
+
+#define REG_EDID_START 0x20
+#define REG_CEC_RX_DATA 0x20
+#define REG_CEC_TX_DATA 0x30
+
+#define REG_H_ACTIVE_1 0x50
+#define REG_H_ACTIVE_0 0x51
+#define REG_V_ACTIVE_1 0x52
+#define REG_V_ACTIVE_0 0x53
+#define REG_H_TOTAL_1 0x54
+#define REG_H_TOTAL_0 0x55
+#define REG_V_TOTAL_1 0x56
+#define REG_V_TOTAL_0 0x57
+#define REG_VID_PCLK 0x58
+#define REG_H_FP_1 0x5C
+#define REG_H_FP_0 0x5D
+#define REG_H_SW_1 0x5E
+#define REG_H_SW_0 0x5F
+#define REG_H_BP_1 0x60
+#define REG_H_BP_0 0x61
+#define REG_V_FP_1 0x62
+#define REG_V_FP_0 0x63
+#define REG_V_SW_1 0x64
+#define REG_V_SW_0 0x65
+#define REG_V_BP_1 0x66
+#define REG_V_BP_0 0x67
+#define REG_VID_INFO 0x68
+#define B_INTERLACE BIT(0)
+#define B_HSWPOL BIT(1)
+#define B_VSWPOL BIT(2)
+#define B_PIXEL_REP BIT(4)
+
+#define REG_VIC 0x69
+#define REG_HDMI_VIDEO_INFO 0x6A
+#define B_COLOR_MODE BIT(0)
+#define B_COLOR_DEPTH BIT(4)
+#define REG_HDMI_AUDIO_INFO1 0x6B
+#define B_AUD_FS BIT(0)
+
+#define REG_HDMI_AUDIO_INFO2 0x6C
+#define B_AUD_CH BIT(0)
+#define B_AUD_WL BIT(4)
+
+#define REG_HDMI_AUDIO_INFO3 0x6D
+#define B_AUD_TYPE BIT(0)
+#define B_AUD_MS BIT(2)
+#define B_AUD_3D BIT(3)
+
+#define REG_AUDIO_FMT 0x6E
+#define B_I2S_WL BIT(0)
+#define B_I2S_ALN BIT(2)
+#define B_I2S_DLY BIT(3)
+#define B_I2S_LR BIT(4)
+#define B_I2S_SFT BIT(5)
+#define B_AUD_OUT_IF BIT(6)
+
+#define REG_IF_LATCH_HB 0x6F
+#define REG_IF_DATA 0x70
+#define REG_EMP_DATA 0xA0
+#define REG_AVI_DATA 0xAE
+
+#define REG_TX_STATUS 0xE0
+
+#define REG_RX_STATUS 0xE2
+#define B_RX_5V BIT(0)
+#define B_RX_HPD BIT(1)
+#define B_RX_STABLE BIT(2)
+#define B_RX_HDMI BIT(3)
+#define B_RX_AVMUTE BIT(4)
+#define B_RX_AUD_ON BIT(5)
+
+#define REG_RX_HDCP_STS 0xE3
+#define B_HDCP1_AUTH_START BIT(0)
+#define B_HDCP2_AUTH_START BIT(1)
+#define B_HDCP_AUTH_DONE BIT(2)
+#define B_HDCP_ENC BIT(3)
+
+#define REG_CEC_STATUS 0xE4
+#define B_CEC_TX_DONE BIT(0)
+#define B_CEC_TX_NACK BIT(1)
+
+#define REG_CEC_RX_DATA_LEN 0xE5
+#define REG_CEC_TX_DATA_LEN 0xE6
+
+#define REG_SYS_MIPI_INT 0xEB
+#define B_MIPI_OUTPUT_ENABLE BIT(0)
+#define B_MIPI_VIDEO_UNSTABLE BIT(1)
+
+#define REG_RX_INT_STATUS1 0xEC
+#define B_HDMI_5V_CHG BIT(0)
+#define B_HDMI_VID_CHG BIT(1)
+#define B_HDMI_AUD_CHG BIT(2)
+#define B_HDMI_CP_CHG BIT(3)
+#define B_HDMI_IF_LATCH BIT(4)
+#define B_HDMI_NO_IF_LATCH BIT(5)
+#define B_HDMI_EMP BIT(6)
+#define B_HDMI_NO_EMP BIT(7)
+
+#define REG_RX_INT_STATUS2 0xED
+#define B_HDMI_AVI BIT(0)
+#define B_HDMI_NO_AVI BIT(1)
+#define B_HDMI_VSIF BIT(2)
+#define B_HDMI_NO_VSIF BIT(3)
+#define B_HDMI_AVMUTE_CHG BIT(4)
+
+#define REG_CHIP_CONTROL 0xF0
+#define B_HDMI_RESET BIT(5)
+#define B_FW_START BIT(6)
+
+#define REG_MIPI_CFG 0xF1
+#define M_MIPI_LANE 0x03
+#define B_MIPI_SPLIT BIT(2)
+#define B_MIPI_SPLIT_CFG BIT(3)
+#define B_MIPI_DPHY BIT(4)
+#define B_MIPI_USE_DSI BIT(5)
+#define B_MIPI_CONTINU_CLK BIT(6)
+
+#define REG_MIPI_DATA_TYPE 0xF2
+#define CSI_RGB444 0x20
+#define CSI_RGB555 0x21
+#define CSI_RGB565 0x22
+#define CSI_RGB666 0x23
+#define CSI_RGB888 0x24
+#define CSI_YUV420_8b_L 0x1A
+#define CSI_YUV420_8b 0x1C
+#define CSI_YUV420_10b 0x1D
+#define CSI_YUV422_8b 0x1E
+#define CSI_YUV422_10b 0x1F
+#define CSI_RGB_10b 0x30
+#define CSI_RGB_12b 0x31
+#define CSI_YUV422_12b 0x32
+#define CSI_YUV420_10b_L 0x33
+#define CSI_YUV420_12b 0x34
+#define CSI_YUV444_8b 0x35
+#define CSI_YUV444_10b 0x36
+#define CSI_YUV444_12b 0x37
+
+#define REG_MIPI_CONTROL 0xF3
+#define B_MIPI_OUTPUT BIT(0)
+
+#define REG_RX_CFG 0xF4
+#define B_MANUAL_HPD BIT(0)
+#define B_HPD_HIGH BIT(1)
+#define B_HPD_TOGGLE BIT(3)
+
+#define REG_CSC_CFG 0xF5
+#define B_DYNAMIC_RANGE BIT(0)
+
+#define REG_MISC_CFG 0xF6
+#define B_BAUD_RATE BIT(0)
+#define B_DEBUG_MSG BIT(1)
+
+#define REG_HPD_DELAY 0xF7
+#define B_DELAY_COUNT BIT(0)
+#define B_DELAY_UNIT BIT(7)
+
+#define REG_INFO_BANK_SEL 0xFD
+#define CTL_BANK_EDID_READ 1
+#define CTL_BANK_EDID_WRITE 5
+
+#define REG_HOST_CTRL_INT 0xFE
+#define B_CMD_SET BIT(4)
+#define B_CEC_SEND_DATA BIT(5)
+#define B_CONFIG_UPDATE BIT(6)
+#define B_IF_BANK BIT(7)
+
+#define REG_MCU_INTERRUPT 0xFF
+#define B_SYS_INT_ACTIVE BIT(0)
+#define B_CEC_RX_RECEIVED BIT(1)
+#define B_CEC_TX_UPDATE BIT(2)
+
+#define EDID_NUM_BLOCKS_MAX 4
+#define EDID_BLOCK_SIZE 128
+
+#define I2C_MAX_XFER_SIZE 8
+#define POLL_INTERVAL_CEC_MS 10
+#define POLL_INTERVAL_MS 40
+
+#define AUD32K 0x03
+#define AUD44K 0x00
+#define AUD48K 0x02
+#define AUD64K 0x0B
+#define AUD88K 0x08
+#define AUD96K 0x0A
+#define AUD128K 0x2B
+#define AUD176K 0x0C
+#define AUD192K 0x0E
+#define AUD256K 0x1B
+#define AUD352K 0x0D
+#define AUD384K 0x05
+#define AUD512K 0x3B
+#define AUD705K 0x2D
+#define AUD768K 0x09
+#define AUD1024K 0x35
+#define AUD1411K 0x1D
+#define AUD1536K 0x15
+
+enum it6625_chip_type {
+ IT6625_CHIP = 0,
+ IT6626_CHIP = 1,
+};
+
+struct it6625 {
+ struct device *dev;
+ struct i2c_client *i2c_client;
+ struct regmap *it6625_regmap;
+ enum it6625_chip_type chip_type;
+
+ /* protects concurrent access to the chip's registers and state */
+ struct mutex it6625_lock;
+
+ struct v4l2_subdev sd;
+ struct v4l2_mbus_config_mipi_csi2 bus;
+ struct video_device *vdev;
+ struct media_pad pad;
+ struct v4l2_ctrl_handler hdl;
+
+ /* controls */
+ struct v4l2_ctrl *ctrl_5v_detect;
+ struct v4l2_ctrl *ctrl_audio_sampling_rate;
+ struct v4l2_ctrl *ctrl_audio_present;
+ struct v4l2_ctrl *ctrl_link_freq;
+ struct v4l2_ctrl *ctrl_get_hdmi_info;
+ struct v4l2_ctrl *ctrl_mipi_config;
+
+ struct delayed_work hpd_delayed_work;
+
+ struct timer_list timer;
+ struct work_struct polling_work;
+
+ struct v4l2_dv_timings timings;
+
+ u8 csi_lanes;
+ u8 port_num;
+ enum v4l2_mbus_type bus_type;
+ u8 csi_format;
+ u32 mbus_fmt_code;
+
+ struct gpio_desc *reset_gpio;
+
+ struct cec_adapter *cec_adap;
+
+ struct dentry *debugfs_dir;
+};
+
+static const s64 it6625_link_freq[] = { 445500000 };
+
+static const struct v4l2_dv_timings_cap it6625_timings_cap = {
+ .type = V4L2_DV_BT_656_1120,
+ /* keep this initialization for compatibility with GCC < 4.4.6 */
+ .reserved = { 0 },
+
+ V4L2_INIT_BT_TIMINGS(640, 3840, 480, 2160, 27000000, 300000000,
+ V4L2_DV_BT_STD_CEA861 | V4L2_DV_BT_STD_DMT |
+ V4L2_DV_BT_STD_GTF | V4L2_DV_BT_STD_CVT,
+ V4L2_DV_BT_CAP_PROGRESSIVE | V4L2_DV_BT_CAP_INTERLACED |
+ V4L2_DV_BT_CAP_REDUCED_BLANKING | V4L2_DV_BT_CAP_CUSTOM)
+};
+
+static const struct it6625_format_info {
+ u8 csi_format;
+ u32 mbus_fmt_code;
+} it6625_formats[] = {
+ { CSI_YUV422_8b, MEDIA_BUS_FMT_UYVY8_1X16 },
+ { CSI_RGB888, MEDIA_BUS_FMT_RGB888_1X24 },
+ { CSI_YUV444_8b, MEDIA_BUS_FMT_YUV8_1X24 },
+};
+
+static inline int it6625_csi_format_idx(u8 csi_format)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(it6625_formats); i++) {
+ if (it6625_formats[i].csi_format == csi_format)
+ return i;
+ }
+
+ return -EINVAL;
+}
+
+static inline int it6625_csi_mbus_code_idx(u32 mbus_fmt_code)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(it6625_formats); i++) {
+ if (it6625_formats[i].mbus_fmt_code == mbus_fmt_code)
+ return i;
+ }
+
+ return -EINVAL;
+}
+
+static inline struct it6625 *sd_to_6625(struct v4l2_subdev *sd)
+{
+ return container_of(sd, struct it6625, sd);
+}
+
+static inline struct it6625 *hdl_to_6625(struct v4l2_ctrl_handler *hdl)
+{
+ return container_of(hdl, struct it6625, hdl);
+}
+
+static const struct regmap_config it6625_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0xff,
+ .cache_type = REGCACHE_NONE,
+ .max_raw_read = I2C_MAX_XFER_SIZE,
+ .max_raw_write = I2C_MAX_XFER_SIZE,
+};
+
+static int it6625_regmap_i2c_init(struct i2c_client *client,
+ struct it6625 *it6625)
+{
+ it6625->i2c_client = client;
+ it6625->dev = &client->dev;
+
+ it6625->it6625_regmap = devm_regmap_init_i2c(it6625->i2c_client,
+ &it6625_regmap_config);
+ if (IS_ERR(it6625->it6625_regmap))
+ return PTR_ERR(it6625->it6625_regmap);
+
+ return 0;
+}
+
+static int it6625_read_byte(struct it6625 *it6625, u8 reg)
+{
+ unsigned int val;
+ int err;
+ struct device *dev = it6625->dev;
+
+ err = regmap_read(it6625->it6625_regmap, reg, &val);
+ if (err < 0) {
+ dev_err(dev, "reg[0x%x] read failed err: %d", reg, err);
+ return err;
+ }
+
+ return val;
+}
+
+static int it6625_write_byte(struct it6625 *it6625, u8 reg, u8 val)
+{
+ int err;
+ struct device *dev = it6625->dev;
+
+ err = regmap_write(it6625->it6625_regmap, reg, val);
+ if (err < 0) {
+ dev_err(dev, "reg[0x%x] write failed err: %d", reg, err);
+ return err;
+ }
+
+ return 0;
+}
+
+static int it6625_set_bits(struct it6625 *it6625, u8 reg, u8 mask, u8 val)
+{
+ int err;
+ struct device *dev = it6625->dev;
+
+ err = regmap_update_bits(it6625->it6625_regmap, reg, mask, val);
+ if (err < 0) {
+ dev_err(dev, "reg[0x%x] set bits failed err: %d", reg, err);
+ return err;
+ }
+
+ return 0;
+}
+
+static int it6625_read_bytes(struct it6625 *it6625, u8 reg, u8 *buf, int len)
+{
+ int err;
+ struct device *dev = it6625->dev;
+
+ err = regmap_bulk_read(it6625->it6625_regmap, reg, buf, len);
+ if (err < 0) {
+ dev_err(dev, "reg[0x%x] read failed err: %d", reg, err);
+ return err;
+ }
+
+ return 0;
+}
+
+static int it6625_write_bytes(struct it6625 *it6625, u8 reg, u8 *buf, int len)
+{
+ int err;
+ struct device *dev = it6625->dev;
+
+ err = regmap_bulk_write(it6625->it6625_regmap, reg, buf, len);
+ if (err < 0) {
+ dev_err(dev, "reg[0x%x] write failed err: %d", reg, err);
+ return err;
+ }
+
+ return 0;
+}
+
+static int it6625_wait_for_status(struct it6625 *it6625, u8 reg, u8 val,
+ int timeout_ms)
+{
+ struct device *dev = it6625->dev;
+ int status;
+ int rval;
+
+ status = read_poll_timeout(it6625_read_byte, rval, rval == val,
+ 50 * 1000,
+ timeout_ms * 1000,
+ false, it6625, reg);
+
+ dev_info(dev, "%s status = %d %d", __func__, status, (int)rval);
+ if (status < 0) {
+ dev_err(dev, "%s err status = %d", __func__, status);
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+static void it6625_write_command(struct it6625 *it6625, u8 *cmds, int cmd_len)
+{
+ it6625_write_bytes(it6625, REG_CMD_SET, cmds, cmd_len);
+ it6625_write_byte(it6625, REG_HOST_CTRL_INT, B_CMD_SET);
+ it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25);
+}
+
+static int it6625_update_config(struct it6625 *it6625)
+{
+ int err;
+
+ err = it6625_set_bits(it6625, REG_HOST_CTRL_INT,
+ B_CONFIG_UPDATE, B_CONFIG_UPDATE);
+ if (err < 0)
+ return err;
+
+ return it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25);
+}
+
+static int it6625_set_bank(struct it6625 *it6625, u8 bank)
+{
+ int err;
+
+ err = it6625_write_byte(it6625, REG_INFO_BANK_SEL, bank);
+ if (err < 0)
+ return err;
+
+ err = it6625_write_byte(it6625, REG_HOST_CTRL_INT, B_IF_BANK);
+ if (err < 0)
+ return err;
+
+ return it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25);
+}
+
+static inline bool is_hdmi(struct it6625 *it6625)
+{
+ int val;
+
+ val = it6625_read_byte(it6625, REG_RX_STATUS);
+ return (val < 0) ? false : (val & B_RX_HDMI);
+}
+
+static inline bool hdmi_5v_power_present(struct it6625 *it6625)
+{
+ int val;
+
+ val = it6625_read_byte(it6625, REG_RX_STATUS);
+ return (val < 0) ? false : (val & B_RX_5V);
+}
+
+static inline bool no_signal(struct it6625 *it6625)
+{
+ int val;
+
+ val = it6625_read_byte(it6625, REG_RX_STATUS);
+ return (val < 0) ? true : !(val & B_RX_STABLE);
+}
+
+static inline bool audio_present(struct it6625 *it6625)
+{
+ int val;
+
+ val = it6625_read_byte(it6625, REG_RX_STATUS);
+ return (val < 0) ? false : (val & B_RX_AUD_ON);
+}
+
+static int get_audio_sampling_rate(struct it6625 *it6625)
+{
+ u8 fs_id;
+ int i, freq = 0;
+ const struct fs_id_map {
+ u8 fs_id;
+ u16 freq;
+ } s_fsid_map[] = {
+ { AUD32K, 32 },
+ { AUD44K, 44 },
+ { AUD48K, 48 },
+ { AUD64K, 64 },
+ { AUD88K, 88 },
+ { AUD96K, 96 },
+
+ { AUD128K, 128 },
+ { AUD176K, 176 },
+ { AUD192K, 192 },
+ { AUD256K, 256 },
+ { AUD352K, 352 },
+ { AUD384K, 384 },
+
+ { AUD512K, 512 },
+ { AUD705K, 705 },
+ { AUD768K, 768 },
+ { AUD1024K, 1024 },
+ { AUD1411K, 1411 },
+ { AUD1536K, 1536 },
+ };
+
+ if (no_signal(it6625) || !audio_present(it6625))
+ return 0;
+
+ fs_id = it6625_read_byte(it6625, REG_HDMI_AUDIO_INFO1);
+
+ for (i = 0; i < ARRAY_SIZE(s_fsid_map); i++) {
+ if (s_fsid_map[i].fs_id == fs_id) {
+ freq = s_fsid_map[i].freq * 1000;
+ break;
+ }
+ }
+
+ return freq;
+}
+
+static u64 it6625_get_pclk(struct it6625 *it6625)
+{
+ u32 pclk;
+ u8 ck[4];
+ int ret;
+
+ ret = it6625_read_bytes(it6625, REG_VID_PCLK, ck, 4);
+ if (ret < 0) {
+ dev_err(it6625->dev, "failed to read pixel clock");
+ return 0;
+ }
+
+ pclk = ck[0];
+ pclk <<= 8;
+ pclk |= ck[1];
+ pclk <<= 8;
+ pclk |= ck[2];
+ pclk <<= 8;
+ pclk |= ck[3];
+
+ v4l2_dbg(1, debug, &it6625->sd, "%s: pclk=%u (%08x)",
+ __func__, pclk, pclk);
+
+ return (u64)pclk * 1000;
+}
+
+static int it6625_read_edid(struct it6625 *it6625, u8 *edid, int start_block,
+ int num_blocks)
+{
+ int i, bank_ctrl, err = 0;
+ struct device *dev = it6625->dev;
+
+ if (!edid) {
+ dev_err(dev, "edid buffer is NULL");
+ return -EINVAL;
+ }
+
+ if (start_block < 0 || num_blocks <= 0 ||
+ start_block + num_blocks > EDID_NUM_BLOCKS_MAX) {
+ dev_err(dev,
+ "invalid block range: start_block=%d, num_blocks=%d",
+ start_block, num_blocks);
+ return -EINVAL;
+ }
+
+ guard(mutex)(&it6625->it6625_lock);
+ for (i = 0; i < num_blocks; i++) {
+ bank_ctrl = CTL_BANK_EDID_READ + start_block + i;
+ err = it6625_set_bank(it6625, bank_ctrl);
+ if (err < 0)
+ break;
+
+ err = it6625_read_bytes(it6625, REG_EDID_START,
+ edid + (i * 128), 128);
+ if (err < 0)
+ break;
+ }
+
+ it6625_set_bank(it6625, 0);
+
+ return err < 0 ? err : num_blocks;
+}
+
+static int it6625_write_edid(struct it6625 *it6625, u8 *edid, int start_block,
+ int num_blocks)
+{
+ int i, bank_ctrl, err = 0;
+ struct device *dev = it6625->dev;
+
+ if (start_block < 0 || num_blocks <= 0 ||
+ start_block + num_blocks > EDID_NUM_BLOCKS_MAX) {
+ dev_err(dev,
+ "invalid block range: start_block=%d, num_blocks=%d",
+ start_block, num_blocks);
+ return -EINVAL;
+ }
+
+ guard(mutex)(&it6625->it6625_lock);
+ for (i = 0; i < num_blocks; i++) {
+ bank_ctrl = CTL_BANK_EDID_WRITE + start_block + i;
+ err = it6625_set_bank(it6625, bank_ctrl);
+ if (err < 0)
+ break;
+
+ err = it6625_write_bytes(it6625, REG_EDID_START,
+ edid + (i * 128), 128);
+ if (err < 0)
+ break;
+
+ err = it6625_update_config(it6625);
+ if (err < 0)
+ break;
+ }
+
+ it6625_set_bank(it6625, 0);
+
+ return err < 0 ? err : num_blocks;
+}
+
+static void it6625_enable_auto_hpd(struct it6625 *it6625)
+{
+ dev_dbg(it6625->dev, "%s: auto HPD", __func__);
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_set_bits(it6625, REG_RX_CFG, 0x03, 0x00);
+ it6625_update_config(it6625);
+}
+
+static void it6625_disable_hpd(struct it6625 *it6625)
+{
+ cancel_delayed_work_sync(&it6625->hpd_delayed_work);
+
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_set_bits(it6625, REG_RX_CFG, 0x03, 0x01);
+ it6625_update_config(it6625);
+}
+
+static void it6625_enable_hpd(struct it6625 *it6625)
+{
+ schedule_delayed_work(&it6625->hpd_delayed_work,
+ msecs_to_jiffies(100));
+}
+
+static void it6625_hpd_delayed_work(struct work_struct *work)
+{
+ struct it6625 *it6625 = container_of(work,
+ struct it6625, hpd_delayed_work.work);
+ int val = 0;
+
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_set_bits(it6625, REG_RX_CFG, 0x03, val);
+ it6625_update_config(it6625);
+}
+
+static int it6625_get_detected_timings(struct it6625 *it6625,
+ struct v4l2_dv_timings *timings)
+{
+ struct v4l2_bt_timings *bt = &timings->bt;
+ int val;
+ unsigned int width, height, frame_width, frame_height;
+ u8 buffer[8];
+
+ if (no_signal(it6625)) {
+ dev_err(it6625->dev, "no signal detected");
+ return -ENOLINK;
+ }
+
+ memset(timings, 0, sizeof(struct v4l2_dv_timings));
+ timings->type = V4L2_DV_BT_656_1120;
+ val = it6625_read_byte(it6625, REG_VID_INFO);
+ if (val < 0) {
+ dev_err(it6625->dev, "failed to read video info");
+ return -EIO;
+ }
+
+ bt->interlaced = val & B_INTERLACE ?
+ V4L2_DV_INTERLACED : V4L2_DV_PROGRESSIVE;
+
+ if (it6625_read_bytes(it6625, REG_H_ACTIVE_1, buffer, 8) < 0)
+ return -EIO;
+
+ width = ((buffer[0] & 0xff) << 8) + buffer[1];
+ height = ((buffer[2] & 0xff) << 8) + buffer[3];
+ frame_width = ((buffer[4] & 0xff) << 8) + buffer[5];
+ frame_height = ((buffer[6] & 0xff) << 8) + buffer[7];
+
+ bt->width = width;
+ bt->height = height;
+ bt->vsync = frame_height - height;
+ bt->hsync = frame_width - width;
+
+ bt->pixelclock = it6625_get_pclk(it6625);
+ if (bt->interlaced == V4L2_DV_INTERLACED) {
+ bt->height *= 2;
+ bt->il_vsync = bt->vsync + 1;
+ bt->pixelclock /= 2;
+ }
+
+ return 0;
+}
+
+static void it6625_show_avi_infoframe(struct it6625 *it6625)
+{
+ struct device *dev = it6625->dev;
+ union hdmi_infoframe frame;
+ u8 buffer[HDMI_INFOFRAME_SIZE(AVI)];
+ u8 ver, len;
+ int ret;
+
+ if (!is_hdmi(it6625)) {
+ dev_err(dev, "not HDMI signal, skip AVI infoframe log");
+ return;
+ }
+
+ ret = it6625_read_bytes(it6625, REG_AVI_DATA, buffer + 1,
+ HDMI_INFOFRAME_SIZE(AVI) - 1);
+ if (ret < 0) {
+ dev_err(dev, "failed to read AVI infoframe data");
+ return;
+ }
+
+ len = buffer[1];
+ ver = buffer[2];
+
+ buffer[0] = HDMI_INFOFRAME_TYPE_AVI;
+ buffer[1] = ver;
+ buffer[2] = len;
+
+ ret = hdmi_infoframe_unpack(&frame, buffer, sizeof(buffer));
+ if (ret < 0) {
+ dev_err(dev, "unpack of AVI infoframe failed");
+ return;
+ }
+
+ hdmi_infoframe_log(KERN_INFO, dev, &frame);
+}
+
+static int it6625_s_ctrl_detect_hdmi_5v(struct v4l2_subdev *sd)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ return v4l2_ctrl_s_ctrl(it6625->ctrl_5v_detect,
+ hdmi_5v_power_present(it6625));
+}
+
+static int it6625_s_ctrl_audio_sampling_rate(struct v4l2_subdev *sd)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ return v4l2_ctrl_s_ctrl(it6625->ctrl_audio_sampling_rate,
+ get_audio_sampling_rate(it6625));
+}
+
+static int it6625_s_ctrl_audio_present(struct v4l2_subdev *sd)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ return v4l2_ctrl_s_ctrl(it6625->ctrl_audio_present,
+ audio_present(it6625));
+}
+
+static int it6625_v4l2_sd_ctrl_update(struct v4l2_subdev *sd)
+{
+ int ret = 0;
+
+ ret |= it6625_s_ctrl_detect_hdmi_5v(sd);
+ ret |= it6625_s_ctrl_audio_sampling_rate(sd);
+ ret |= it6625_s_ctrl_audio_present(sd);
+
+ return 0;
+}
+
+static void it6625_enable_stream(struct it6625 *it6625, bool enable)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+ int val;
+
+ v4l2_dbg(3, debug, sd, "%s: %sable",
+ __func__, enable ? "en" : "dis");
+
+ val = enable ? B_MIPI_OUTPUT : 0;
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_set_bits(it6625, REG_MIPI_CONTROL, B_MIPI_OUTPUT, val);
+ it6625_update_config(it6625);
+}
+
+static void it6625_set_mipi_config(struct it6625 *it6625, u32 cfg_val)
+{
+ u8 mipi_data_type;
+
+ dev_dbg(it6625->dev, "mipi_data_type = 0x%x", cfg_val);
+
+ mipi_data_type = cfg_val & 0xFF;
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_write_byte(it6625, REG_MIPI_DATA_TYPE, mipi_data_type);
+ it6625_update_config(it6625);
+}
+
+static inline unsigned int fps_from_bt_timings(const struct v4l2_bt_timings *t)
+{
+ if (!V4L2_DV_BT_FRAME_HEIGHT(t) || !V4L2_DV_BT_FRAME_WIDTH(t))
+ return 0;
+
+ return DIV_ROUND_CLOSEST((unsigned int)t->pixelclock,
+ V4L2_DV_BT_FRAME_HEIGHT(t) *
+ V4L2_DV_BT_FRAME_WIDTH(t));
+}
+
+static void it6625_initial_setup(struct it6625 *it6625)
+{
+ int val = 0;
+
+ /*
+ * REG_MIPI_CFG[0:2] lane count field: 1 lane -> 0, 2 lanes -> 1,
+ * 3 lanes (C-PHY only) -> 3, 4 lanes (D-PHY only) -> 3.
+ */
+ switch (it6625->csi_lanes) {
+ case 1:
+ val = FIELD_PREP(M_MIPI_LANE, 0);
+ break;
+ case 2:
+ val = FIELD_PREP(M_MIPI_LANE, 1);
+ break;
+ default:
+ val = FIELD_PREP(M_MIPI_LANE, 3);
+ break;
+ }
+
+ if (it6625->bus_type == V4L2_MBUS_CSI2_DPHY)
+ val |= FIELD_PREP(B_MIPI_DPHY, 1);
+
+ if (it6625->port_num == 2)
+ val |= FIELD_PREP(B_MIPI_SPLIT, 1);
+
+ it6625_write_byte(it6625, REG_MIPI_CFG, val);
+ it6625_write_byte(it6625, REG_MIPI_DATA_TYPE, it6625->csi_format);
+ it6625_write_byte(it6625, REG_MIPI_CONTROL, 0x00);
+ it6625_write_byte(it6625, REG_RX_CFG, 0x00);
+
+ /*trigger FW start*/
+ it6625_set_bits(it6625, REG_CHIP_CONTROL, B_FW_START, B_FW_START);
+ it6625_set_bits(it6625, REG_HOST_CTRL_INT, B_CONFIG_UPDATE, B_CONFIG_UPDATE);
+ it6625_wait_for_status(it6625, REG_HOST_CTRL_INT, 0x00, 25);
+}
+
+static void dump_cec_msg(struct it6625 *it6625, struct cec_msg *msg, char *str)
+{
+ int i;
+
+ dev_dbg(it6625->dev, "%s: from %d to %d len=%d", str,
+ msg->msg[0] >> 4, msg->msg[0] & 0x0f, msg->len);
+ for (i = 1; i < msg->len; i++)
+ dev_dbg(it6625->dev, " %02x", msg->msg[i]);
+}
+
+static int it6625_cec_adap_enable(struct cec_adapter *adap, bool enable)
+{
+ struct it6625 *it6625 = adap->priv;
+ u8 cmds[2];
+
+ cmds[0] = CMD_SET_CEC_ENABLE;
+ cmds[1] = enable ? 1 : 0;
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_write_command(it6625, cmds, sizeof(cmds));
+
+ return 0;
+}
+
+static int it6625_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr)
+{
+ struct it6625 *it6625 = adap->priv;
+
+ dev_dbg(it6625->dev, "%s: la=%d", __func__, log_addr);
+
+ if (log_addr != CEC_LOG_ADDR_INVALID) {
+ u8 cmds[2] = {CMD_SET_CEC_LA, log_addr};
+
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_write_command(it6625, cmds, sizeof(cmds));
+ }
+
+ return 0;
+}
+
+static int it6625_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
+ u32 signal_free_time, struct cec_msg *msg)
+{
+ struct it6625 *it6625 = adap->priv;
+
+ dump_cec_msg(it6625, msg, "CEC_TX");
+
+ guard(mutex)(&it6625->it6625_lock);
+ it6625_write_bytes(it6625, REG_CEC_TX_DATA, msg->msg, msg->len);
+ it6625_write_byte(it6625, REG_CEC_TX_DATA_LEN, msg->len);
+ it6625_set_bits(it6625, REG_HOST_CTRL_INT, B_CEC_SEND_DATA, B_CEC_SEND_DATA);
+
+ return 0;
+}
+
+static const struct cec_adap_ops it6625_cec_adap_ops = {
+ .adap_enable = it6625_cec_adap_enable,
+ .adap_log_addr = it6625_cec_adap_log_addr,
+ .adap_transmit = it6625_cec_adap_transmit,
+};
+
+static void it6625_cec_handler(struct it6625 *it6625, u8 intstatus)
+{
+ struct cec_msg rxmsg;
+ int val = 0;
+
+ if (intstatus & B_CEC_RX_RECEIVED) {
+ val = it6625_read_byte(it6625, REG_CEC_RX_DATA_LEN);
+ if (val <= 0 || val > 16) {
+ dev_err(it6625->dev, "invalid CEC RX length %d", val);
+ return;
+ }
+
+ it6625_read_bytes(it6625, REG_CEC_RX_DATA, &rxmsg.msg[0], val);
+ rxmsg.len = val;
+ it6625_write_byte(it6625, REG_CEC_RX_DATA_LEN, 0);
+ cec_received_msg(it6625->cec_adap, &rxmsg);
+ dump_cec_msg(it6625, &rxmsg, "CEC_RX");
+
+ } else if (intstatus & B_CEC_TX_UPDATE) {
+ val = it6625_read_byte(it6625, REG_CEC_STATUS);
+ if (val < 0) {
+ dev_err(it6625->dev, "read CEC status failed");
+ return;
+ }
+
+ if (val & BIT(0)) {
+ cec_transmit_attempt_done(it6625->cec_adap,
+ CEC_TX_STATUS_OK);
+ } else if (val & BIT(1)) {
+ cec_transmit_attempt_done(it6625->cec_adap,
+ CEC_TX_STATUS_NACK);
+ } else {
+ dev_err(it6625->dev, "unknown CEC status %02X", val);
+ cec_transmit_attempt_done(it6625->cec_adap,
+ CEC_TX_STATUS_NACK);
+ }
+ }
+}
+
+static void it6625_irq_format_change(struct it6625 *it6625)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+ struct v4l2_dv_timings timings;
+ const struct v4l2_event it6625_ev_fmt = {
+ .type = V4L2_EVENT_SOURCE_CHANGE,
+ .u.src_change.changes = V4L2_EVENT_SRC_CH_RESOLUTION,
+ };
+ int ret;
+
+ if (no_signal(it6625))
+ return;
+
+ ret = it6625_get_detected_timings(it6625, &timings);
+ if (ret < 0) {
+ v4l2_dbg(1, debug, sd, "Failed to get detected timings");
+ return;
+ }
+
+ if (sd->devnode)
+ v4l2_subdev_notify_event(sd, &it6625_ev_fmt);
+}
+
+static void it6625_irq_hdmi_audio_change(struct it6625 *it6625)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+
+ it6625_s_ctrl_audio_sampling_rate(sd);
+ it6625_s_ctrl_audio_present(sd);
+}
+
+static void it6625_get_timings(struct it6625 *it6625,
+ struct v4l2_dv_timings *timings)
+{
+ guard(mutex)(&it6625->it6625_lock);
+ *timings = it6625->timings;
+}
+
+static void it6625_clear_timings(struct it6625 *it6625)
+{
+ guard(mutex)(&it6625->it6625_lock);
+ memset(&it6625->timings, 0, sizeof(it6625->timings));
+}
+
+static void it6625_irq_hdmi_5v_change(struct it6625 *it6625)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+
+ it6625_clear_timings(it6625);
+ it6625_v4l2_sd_ctrl_update(sd);
+}
+
+static void it6625_irq_hdcp_change(struct it6625 *it6625)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+ u8 cp_sts;
+
+ cp_sts = it6625_read_byte(it6625, REG_RX_HDCP_STS);
+ v4l2_info(sd, "HDCP change to %02X", cp_sts);
+}
+
+static void it6625_irq_infoframe_latch(struct it6625 *it6625)
+{
+ u8 info_frame[32];
+
+ it6625_read_bytes(it6625, REG_IF_DATA, info_frame, 31);
+}
+
+static void it6625_irq_emata_packet_latch(struct it6625 *it6625)
+{
+ u8 extend_packet[14];
+
+ it6625_read_bytes(it6625, REG_EMP_DATA, extend_packet, 13);
+}
+
+static void it6625_irq_avmute_change(struct it6625 *it6625)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+ int val;
+ u8 avmute;
+
+ val = it6625_read_byte(it6625, REG_RX_STATUS);
+ if (val < 0)
+ return;
+
+ avmute = (val & B_RX_AVMUTE) ? 1 : 0;
+ v4l2_info(sd, "AVMute change to %d", avmute);
+}
+
+static int it6625_interrupt_handler(struct it6625 *it6625)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+ int val, int_sts1 = 0, int_sts2 = 0;
+
+ val = it6625_read_byte(it6625, REG_MCU_INTERRUPT);
+ if (val <= 0)
+ return 0;
+
+ it6625_write_byte(it6625, REG_MCU_INTERRUPT, val);
+
+ v4l2_dbg(1, debug, sd, "%s: INT = 0x%02X", __func__, val);
+
+ if (val & (B_CEC_RX_RECEIVED | B_CEC_TX_UPDATE) && it6625->cec_adap)
+ it6625_cec_handler(it6625, val & (B_CEC_RX_RECEIVED | B_CEC_TX_UPDATE));
+
+ if (val & B_SYS_INT_ACTIVE) {
+ int_sts1 = it6625_read_byte(it6625, REG_RX_INT_STATUS1);
+ int_sts2 = it6625_read_byte(it6625, REG_RX_INT_STATUS2);
+ v4l2_dbg(1, debug, sd, "%s: hdmi_int = 0x%02X 0x%02X",
+ __func__, int_sts1, int_sts2);
+
+ if (int_sts1 >= 0) {
+ it6625_write_byte(it6625, REG_RX_INT_STATUS1, 0x00);
+ if (int_sts1 & B_HDMI_5V_CHG)
+ it6625_irq_hdmi_5v_change(it6625);
+
+ if (int_sts1 & B_HDMI_VID_CHG)
+ it6625_irq_format_change(it6625);
+
+ if (int_sts1 & B_HDMI_AUD_CHG)
+ it6625_irq_hdmi_audio_change(it6625);
+
+ if (int_sts1 & B_HDMI_CP_CHG)
+ it6625_irq_hdcp_change(it6625);
+
+ if (int_sts1 & B_HDMI_IF_LATCH)
+ it6625_irq_infoframe_latch(it6625);
+
+ if (int_sts1 & B_HDMI_EMP)
+ it6625_irq_emata_packet_latch(it6625);
+ }
+
+ if (int_sts2 >= 0) {
+ it6625_write_byte(it6625, REG_RX_INT_STATUS2, 0x00);
+
+ if (int_sts2 & B_HDMI_AVI)
+ it6625_show_avi_infoframe(it6625);
+
+ if (int_sts2 & B_HDMI_NO_AVI)
+ v4l2_info(sd, "AVI-info stopped");
+
+ if (int_sts2 & B_HDMI_AVMUTE_CHG)
+ it6625_irq_avmute_change(it6625);
+ }
+ }
+
+ return 0;
+}
+
+static irqreturn_t it6625_irq_handler(int unused, void *data)
+{
+ struct it6625 *it6625 = data;
+
+ it6625_interrupt_handler(it6625);
+ return IRQ_HANDLED;
+}
+
+static void it6625_irq_poll_timer(struct timer_list *t)
+{
+ struct it6625 *it6625 = timer_container_of(it6625, t, timer);
+ unsigned int msecs;
+
+ schedule_work(&it6625->polling_work);
+ /*
+ * If CEC is present, then we need to poll more frequently,
+ * otherwise we will miss CEC messages.
+ */
+ msecs = it6625->cec_adap ? POLL_INTERVAL_CEC_MS : POLL_INTERVAL_MS;
+ mod_timer(&it6625->timer, jiffies + msecs_to_jiffies(msecs));
+}
+
+static void it6625_polling_work(struct work_struct *work)
+{
+ struct it6625 *it6625 = container_of(work, struct it6625,
+ polling_work);
+
+ it6625_interrupt_handler(it6625);
+}
+
+static int it6625_log_status(struct v4l2_subdev *sd)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ struct v4l2_dv_timings timings, configured_timings;
+
+ if (it6625_get_detected_timings(it6625, &timings))
+ v4l2_info(sd, "No video detected");
+ else
+ v4l2_print_dv_timings(sd->name, "Detected format: ", &timings,
+ true);
+
+ it6625_get_timings(it6625, &configured_timings);
+ v4l2_print_dv_timings(sd->name, "Configured format: ",
+ &configured_timings, true);
+
+ it6625_show_avi_infoframe(it6625);
+
+ return 0;
+}
+
+static int it6625_isr(struct v4l2_subdev *sd, u32 status, bool *handled)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ schedule_work(&it6625->polling_work);
+ *handled = true;
+
+ return 0;
+}
+
+static int it6625_subscribe_event(struct v4l2_subdev *sd, struct v4l2_fh *fh,
+ struct v4l2_event_subscription *sub)
+{
+ switch (sub->type) {
+ case V4L2_EVENT_SOURCE_CHANGE:
+ return v4l2_src_change_event_subdev_subscribe(sd, fh, sub);
+ case V4L2_EVENT_CTRL:
+ return v4l2_ctrl_subdev_subscribe_event(sd, fh, sub);
+ default:
+ return -EINVAL;
+ }
+}
+
+static int it6625_g_input_status(struct v4l2_subdev *sd, u32 *status)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ bool val;
+
+ val = no_signal(it6625);
+ *status = 0;
+ *status |= val ? V4L2_IN_ST_NO_SIGNAL : 0;
+ *status |= val ? V4L2_IN_ST_NO_SYNC : 0;
+
+ v4l2_dbg(1, debug, sd, "%s: status = 0x%x", __func__, *status);
+
+ return 0;
+}
+
+static int
+it6625_update_timings_if_changed(struct it6625 *it6625,
+ const struct v4l2_dv_timings *timings)
+{
+ int ret;
+
+ guard(mutex)(&it6625->it6625_lock);
+ if (v4l2_match_dv_timings(&it6625->timings, timings, 0, false)) {
+ ret = 0;
+ } else if (!v4l2_valid_dv_timings(timings, &it6625_timings_cap,
+ NULL, NULL)) {
+ ret = -ERANGE;
+ } else {
+ it6625->timings = *timings;
+ ret = 1;
+ }
+
+ return ret;
+}
+
+static int it6625_s_dv_timings(struct v4l2_subdev *sd,
+ struct v4l2_dv_timings *timings)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ int ret;
+
+ if (!timings)
+ return -EINVAL;
+
+ if (debug)
+ v4l2_print_dv_timings(sd->name, __func__, timings, false);
+
+ ret = it6625_update_timings_if_changed(it6625, timings);
+ if (ret == -ERANGE) {
+ v4l2_dbg(1, debug, sd, "%s: timings out of range", __func__);
+ return ret;
+ }
+
+ if (ret == 0) {
+ v4l2_dbg(1, debug, sd, "%s: no change", __func__);
+ return 0;
+ }
+
+ it6625_enable_stream(it6625, false);
+
+ return 0;
+}
+
+static int it6625_g_dv_timings(struct v4l2_subdev *sd,
+ struct v4l2_dv_timings *timings)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ it6625_get_timings(it6625, timings);
+
+ return 0;
+}
+
+static int it6625_enum_dv_timings(struct v4l2_subdev *sd,
+ struct v4l2_enum_dv_timings *timings)
+{
+ if (timings->pad != 0)
+ return -EINVAL;
+
+ return v4l2_enum_dv_timings_cap(timings,
+ &it6625_timings_cap, NULL, NULL);
+}
+
+static int it6625_query_dv_timings(struct v4l2_subdev *sd,
+ struct v4l2_dv_timings *timings)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ int ret;
+
+ ret = it6625_get_detected_timings(it6625, timings);
+ if (ret)
+ return ret;
+
+ if (debug)
+ v4l2_print_dv_timings(sd->name, __func__, timings, false);
+
+ if (!v4l2_valid_dv_timings(timings, &it6625_timings_cap, NULL, NULL)) {
+ v4l2_dbg(1, debug, sd, "%s: timings out of range", __func__);
+ return -ERANGE;
+ }
+
+ return 0;
+}
+
+static int it6625_dv_timings_cap(struct v4l2_subdev *sd,
+ struct v4l2_dv_timings_cap *cap)
+{
+ if (cap->pad != 0)
+ return -EINVAL;
+
+ *cap = it6625_timings_cap;
+
+ return 0;
+}
+
+static int it6625_s_stream(struct v4l2_subdev *sd, int enable)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ it6625_enable_stream(it6625, enable);
+ return 0;
+}
+
+static int it6625_enum_mbus_code(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *sd_state,
+ struct v4l2_subdev_mbus_code_enum *code)
+{
+ dev_dbg(sd->dev, "%s: index=%d", __func__, code->index);
+
+ if (code->index >= ARRAY_SIZE(it6625_formats))
+ return -EINVAL;
+
+ dev_dbg(sd->dev, "%s: code=0x%08x", __func__,
+ it6625_formats[code->index].mbus_fmt_code);
+ code->code = it6625_formats[code->index].mbus_fmt_code;
+
+ return 0;
+}
+
+static int it6625_get_mbus_config(struct v4l2_subdev *sd,
+ unsigned int pad,
+ struct v4l2_mbus_config *cfg)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ if (pad != 0)
+ return -EINVAL;
+
+ cfg->type = it6625->bus_type;
+ cfg->bus.mipi_csi2.flags = 0;
+ cfg->bus.mipi_csi2.num_data_lanes = it6625->csi_lanes;
+
+ return 0;
+}
+
+static int it6625_pad_s_dv_timings(struct v4l2_subdev *sd, unsigned int pad,
+ struct v4l2_dv_timings *timings)
+{
+ if (pad != 0)
+ return -EINVAL;
+
+ return it6625_s_dv_timings(sd, timings);
+}
+
+static int it6625_pad_g_dv_timings(struct v4l2_subdev *sd, unsigned int pad,
+ struct v4l2_dv_timings *timings)
+{
+ if (pad != 0)
+ return -EINVAL;
+
+ return it6625_g_dv_timings(sd, timings);
+}
+
+static int it6625_pad_query_dv_timings(struct v4l2_subdev *sd,
+ unsigned int pad,
+ struct v4l2_dv_timings *timings)
+{
+ if (pad != 0)
+ return -EINVAL;
+
+ return it6625_query_dv_timings(sd, timings);
+}
+
+static inline u32 format_to_colorspace(u8 csi_format)
+{
+ switch (csi_format) {
+ case CSI_RGB444:
+ case CSI_RGB555:
+ case CSI_RGB565:
+ case CSI_RGB666:
+ case CSI_RGB888:
+ case CSI_RGB_10b:
+ case CSI_RGB_12b:
+ return V4L2_COLORSPACE_SRGB;
+ case CSI_YUV420_8b_L:
+ case CSI_YUV420_8b:
+ case CSI_YUV420_10b:
+ case CSI_YUV422_8b:
+ case CSI_YUV422_10b:
+ case CSI_YUV422_12b:
+ case CSI_YUV420_10b_L:
+ case CSI_YUV420_12b:
+ case CSI_YUV444_8b:
+ case CSI_YUV444_10b:
+ case CSI_YUV444_12b:
+ return V4L2_COLORSPACE_REC709;
+ default:
+ return 0;
+ }
+}
+
+static int it6625_get_fmt(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *sd_state,
+ struct v4l2_subdev_format *format)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ struct v4l2_dv_timings timings;
+
+ if (format->pad != 0)
+ return -EINVAL;
+
+ format->format.code = it6625->mbus_fmt_code;
+ it6625_get_timings(it6625, &timings);
+ format->format.width = timings.bt.width;
+ format->format.height = timings.bt.height;
+ format->format.colorspace = format_to_colorspace(it6625->csi_format);
+ format->format.field = timings.bt.interlaced == V4L2_DV_INTERLACED ?
+ V4L2_FIELD_INTERLACED : V4L2_FIELD_NONE;
+
+ return 0;
+}
+
+static int it6625_set_fmt(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *sd_state,
+ struct v4l2_subdev_format *format)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ int ret;
+ u32 mbus_fmt_code = format->format.code;
+
+ ret = it6625_get_fmt(sd, sd_state, format);
+ format->format.code = mbus_fmt_code;
+
+ if (ret)
+ return ret;
+
+ ret = it6625_csi_mbus_code_idx(mbus_fmt_code);
+
+ if (ret < 0) {
+ v4l2_dbg(1, debug, sd, "%s: unsupported format code 0x%x",
+ __func__, mbus_fmt_code);
+ return ret;
+ }
+
+ it6625->csi_format = it6625_formats[ret].csi_format;
+
+ if (format->which == V4L2_SUBDEV_FORMAT_TRY) {
+ v4l2_dbg(1, debug, sd, "%s: try format code = 0x%x",
+ __func__, format->format.code);
+ return 0;
+ }
+
+ it6625->mbus_fmt_code = format->format.code;
+ it6625_enable_stream(it6625, false);
+ it6625_set_mipi_config(it6625, it6625->csi_format);
+
+ return 0;
+}
+
+static int it6625_g_edid(struct v4l2_subdev *sd,
+ struct v4l2_subdev_edid *edid)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ int err;
+
+ if (edid->pad != 0)
+ return -EINVAL;
+
+ memset(edid->reserved, 0, sizeof(edid->reserved));
+ err = it6625_read_edid(it6625, edid->edid, edid->start_block,
+ edid->blocks);
+
+ return (err < 0) ? err : 0;
+}
+
+static int it6625_s_edid(struct v4l2_subdev *sd,
+ struct v4l2_subdev_edid *edid)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ int err;
+ u16 pa;
+
+ if (edid->pad != 0) {
+ v4l2_err(sd, "invalid pad %d", edid->pad);
+ return -EINVAL;
+ }
+
+ memset(edid->reserved, 0, sizeof(edid->reserved));
+
+ it6625_disable_hpd(it6625);
+
+ err = it6625_write_edid(it6625, edid->edid,
+ edid->start_block, edid->blocks);
+ if (err) {
+ v4l2_err(sd, "write edid failed");
+ return err;
+ }
+
+ pa = cec_get_edid_phys_addr(edid->edid, edid->blocks * 128, NULL);
+ if (pa == CEC_PHYS_ADDR_INVALID) {
+ v4l2_warn(sd, "invalid CEC physical address in EDID");
+ cec_phys_addr_invalidate(it6625->cec_adap);
+ } else {
+ v4l2_phys_addr_validate(pa, NULL, NULL);
+ cec_s_phys_addr(it6625->cec_adap, pa, false);
+ }
+
+ if (hdmi_5v_power_present(it6625)) {
+ it6625_enable_hpd(it6625);
+ it6625_s_ctrl_detect_hdmi_5v(sd);
+ } else {
+ it6625_enable_auto_hpd(it6625);
+ }
+
+ return 0;
+}
+
+#define V4L2_CID_USER_IT6625_BASE (V4L2_CID_USER_BASE + 0x11b0)
+#define IT6625_CID_AUDIO_SAMPLING_RATE (V4L2_CID_USER_IT6625_BASE + 0)
+#define IT6625_CID_AUDIO_PRESENT (V4L2_CID_USER_IT6625_BASE + 1)
+#define IT6625_CID_HDMI_INFO (V4L2_CID_USER_IT6625_BASE + 2)
+#define IT6625_CID_MIPI_CONFIG (V4L2_CID_USER_IT6625_BASE + 3)
+static int it6625_g_volatile_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct it6625 *it6625 = hdl_to_6625(ctrl->handler);
+ struct v4l2_subdev *sd = &it6625->sd;
+
+ v4l2_dbg(1, debug, sd, "%s, id=0x%x, val=%d",
+ __func__, ctrl->id, ctrl->val);
+
+ switch (ctrl->id) {
+ case IT6625_CID_HDMI_INFO:
+ snprintf(ctrl->p_new.p_char, ctrl->maximum,
+ "RGB444 10bit @ 60Hz");
+ break;
+ case IT6625_CID_MIPI_CONFIG:
+ ctrl->val = it6625_read_byte(it6625, REG_MIPI_DATA_TYPE);
+ break;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static int it6625_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct it6625 *it6625 = hdl_to_6625(ctrl->handler);
+ struct v4l2_subdev *sd = &it6625->sd;
+
+ v4l2_dbg(1, debug, sd, "%s, id=0x%x, val=%d",
+ __func__, ctrl->id, ctrl->val);
+
+ switch (ctrl->id) {
+ case IT6625_CID_MIPI_CONFIG:
+ it6625_set_mipi_config(it6625, ctrl->val);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static const struct v4l2_subdev_core_ops it6625_core_ops = {
+ .log_status = it6625_log_status,
+ .interrupt_service_routine = it6625_isr,
+ .subscribe_event = it6625_subscribe_event,
+ .unsubscribe_event = v4l2_event_subdev_unsubscribe,
+};
+
+static const struct v4l2_subdev_video_ops it6625_video_ops = {
+ .g_input_status = it6625_g_input_status,
+ .s_stream = it6625_s_stream,
+};
+
+static const struct v4l2_subdev_pad_ops it6625_pad_ops = {
+ .enum_mbus_code = it6625_enum_mbus_code,
+ .set_fmt = it6625_set_fmt,
+ .get_fmt = it6625_get_fmt,
+ .get_edid = it6625_g_edid,
+ .set_edid = it6625_s_edid,
+ .enum_dv_timings = it6625_enum_dv_timings,
+ .dv_timings_cap = it6625_dv_timings_cap,
+ .get_mbus_config = it6625_get_mbus_config,
+ .s_dv_timings = it6625_pad_s_dv_timings,
+ .g_dv_timings = it6625_pad_g_dv_timings,
+ .query_dv_timings = it6625_pad_query_dv_timings,
+};
+
+static const struct v4l2_subdev_ops it6625_ops = {
+ .core = &it6625_core_ops,
+ .video = &it6625_video_ops,
+ .pad = &it6625_pad_ops,
+};
+
+static const struct v4l2_ctrl_ops it6625_ctrl_ops = {
+ .g_volatile_ctrl = it6625_g_volatile_ctrl,
+ .s_ctrl = it6625_s_ctrl,
+};
+
+static const struct v4l2_ctrl_config it6625_ctrl_audio_sampling_rate = {
+ .id = IT6625_CID_AUDIO_SAMPLING_RATE,
+ .name = "Audio sampling rate",
+ .type = V4L2_CTRL_TYPE_INTEGER,
+ .min = 0,
+ .max = 768000,
+ .step = 1,
+ .def = 0,
+ .flags = V4L2_CTRL_FLAG_READ_ONLY,
+};
+
+static const struct v4l2_ctrl_config it6625_ctrl_audio_present = {
+ .id = IT6625_CID_AUDIO_PRESENT,
+ .name = "Audio present",
+ .type = V4L2_CTRL_TYPE_BOOLEAN,
+ .min = 0,
+ .max = 1,
+ .step = 1,
+ .def = 0,
+ .flags = V4L2_CTRL_FLAG_READ_ONLY,
+};
+
+static const struct v4l2_ctrl_config it6625_ctrl_get_hdmi_info = {
+ .ops = &it6625_ctrl_ops,
+ .id = IT6625_CID_HDMI_INFO,
+ .name = "HDMI Info",
+ .type = V4L2_CTRL_TYPE_STRING,
+ .max = 128,
+ .step = 1,
+ .flags = V4L2_CTRL_FLAG_READ_ONLY,
+};
+
+static const struct v4l2_ctrl_config it6625_ctrl_mipi_config = {
+ .ops = &it6625_ctrl_ops,
+ .id = IT6625_CID_MIPI_CONFIG,
+ .name = "MIPI config",
+ .type = V4L2_CTRL_TYPE_INTEGER,
+ .min = 0,
+ .max = 0x7FFFFFFF,
+ .step = 1,
+ .def = 0x3E,
+ .flags = V4L2_CTRL_FLAG_VOLATILE,
+};
+
+static int it6625_v4l2_init_controls(struct v4l2_subdev *sd)
+{
+ struct it6625 *it6625 = sd_to_6625(sd);
+ struct v4l2_ctrl_handler *hdl = &it6625->hdl;
+
+ v4l2_ctrl_handler_init(hdl, 4);
+ it6625->ctrl_5v_detect =
+ v4l2_ctrl_new_std(hdl, NULL, V4L2_CID_DV_RX_POWER_PRESENT,
+ 0, 1, 0, 0);
+
+ it6625->ctrl_audio_sampling_rate =
+ v4l2_ctrl_new_custom(hdl,
+ &it6625_ctrl_audio_sampling_rate,
+ NULL);
+ it6625->ctrl_audio_present =
+ v4l2_ctrl_new_custom(hdl, &it6625_ctrl_audio_present, NULL);
+ it6625->ctrl_link_freq =
+ v4l2_ctrl_new_int_menu(hdl, NULL, V4L2_CID_LINK_FREQ,
+ ARRAY_SIZE(it6625_link_freq) - 1,
+ 0, it6625_link_freq);
+ it6625->ctrl_get_hdmi_info =
+ v4l2_ctrl_new_custom(hdl, &it6625_ctrl_get_hdmi_info, NULL);
+ it6625->ctrl_mipi_config =
+ v4l2_ctrl_new_custom(hdl, &it6625_ctrl_mipi_config, NULL);
+ if (hdl->error) {
+ v4l2_err(sd, "Failed to initialize controls");
+ v4l2_ctrl_handler_free(hdl);
+ return hdl->error;
+ }
+
+ sd->ctrl_handler = hdl;
+
+ return 0;
+}
+
+static void it6625_regdump_print(struct seq_file *s, const u8 *reg_buf)
+{
+ int i;
+
+ seq_puts(s, " 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F\n");
+
+ for (i = 0; i < 256; i++) {
+ if (i % 16 == 0)
+ seq_printf(s, "[%02X] ", i & 0xF0);
+ seq_printf(s, "0x%02X ", reg_buf[i]);
+ if (i % 16 == 15)
+ seq_putc(s, '\n');
+ }
+}
+
+static int it6625_edid_ram_show(struct seq_file *s, void *data)
+{
+ struct it6625 *it6625 = s->private;
+ u8 reg_buf[256];
+
+ it6625_read_edid(it6625, reg_buf, 0, 2);
+ it6625_regdump_print(s, reg_buf);
+
+ return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(it6625_edid_ram);
+
+static int it6625_mipi_reg_show(struct seq_file *s, void *data)
+{
+ struct it6625 *it6625 = s->private;
+ u8 reg_buf[256];
+
+ it6625_read_bytes(it6625, 0x00, reg_buf, sizeof(reg_buf));
+ it6625_regdump_print(s, reg_buf);
+
+ return 0;
+}
+
+static int it6625_mipi_reg_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, it6625_mipi_reg_show, inode->i_private);
+}
+
+static ssize_t it6625_mipi_reg_write(struct file *file,
+ const char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ struct it6625 *it6625 = file_inode(file)->i_private;
+ char buf[32];
+ unsigned int addr, val;
+ ssize_t len;
+
+ len = simple_write_to_buffer(buf, sizeof(buf) - 1, ppos, user_buf, count);
+ if (len < 0)
+ return len;
+ buf[len] = '\0';
+
+ if (sscanf(buf, "%X %X", &addr, &val) != 2)
+ return -EINVAL;
+
+ it6625_write_byte(it6625, addr, val);
+
+ return count;
+}
+
+static const struct file_operations it6625_mipi_reg_fops = {
+ .owner = THIS_MODULE,
+ .open = it6625_mipi_reg_open,
+ .read = seq_read,
+ .write = it6625_mipi_reg_write,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static void it6625_debugfs_init(struct it6625 *it6625, struct i2c_client *client)
+{
+ it6625->debugfs_dir = debugfs_create_dir(dev_name(&client->dev), NULL);
+
+ debugfs_create_file("mipi_reg", 0600, it6625->debugfs_dir, it6625,
+ &it6625_mipi_reg_fops);
+ debugfs_create_file("edid_ram", 0400, it6625->debugfs_dir, it6625,
+ &it6625_edid_ram_fops);
+}
+
+static void it6625_init_data(struct it6625 *it6625)
+{
+ static struct v4l2_dv_timings default_timing =
+ V4L2_DV_BT_CEA_1920X1080P60;
+
+ it6625->csi_lanes = 4;
+ it6625->port_num = 1;
+ it6625->bus_type = V4L2_MBUS_CSI2_DPHY;
+ it6625->csi_format = it6625_formats[0].csi_format;
+ it6625->mbus_fmt_code = it6625_formats[0].mbus_fmt_code;
+ it6625->timings = default_timing;
+}
+
+static int it6625_parse_endpoint(struct it6625 *it6625)
+{
+ struct device *dev = it6625->dev;
+ struct v4l2_fwnode_endpoint endpoint = { .bus_type = V4L2_MBUS_CSI2_DPHY };
+ struct device_node *ep = NULL;
+ unsigned int max_lanes;
+ unsigned int port;
+ int ret;
+
+ /*
+ * port@0 and port@1 are the two CSI-2 output ports MIPI0/MIPI1
+ * (port@2 is the HDMI input). This chip series can drive both
+ * simultaneously in split or mirror mode, so port_num counts how
+ * many of MIPI0/MIPI1 have an endpoint wired up. This driver only
+ * wires up a single source pad, so lane/bus-type configuration is
+ * parsed from whichever of the two is found first.
+ */
+ it6625->port_num = 0;
+ for (port = 0; port < 2; port++) {
+ struct device_node *port_ep =
+ of_graph_get_endpoint_by_regs(dev->of_node, port, -1);
+
+ if (!port_ep)
+ continue;
+
+ it6625->port_num++;
+ if (!ep)
+ ep = port_ep;
+ else
+ of_node_put(port_ep);
+ }
+
+ if (!ep) {
+ it6625->port_num = 1;
+ dev_dbg(dev, "no CSI-2 endpoint node found, using default %u CSI lanes",
+ it6625->csi_lanes);
+ return 0;
+ }
+
+ ret = v4l2_fwnode_endpoint_alloc_parse(of_fwnode_handle(ep), &endpoint);
+ of_node_put(ep);
+ if (ret) {
+ dev_err(dev, "failed to parse endpoint: %d", ret);
+ return ret;
+ }
+
+ if (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY &&
+ it6625->chip_type != IT6626_CHIP) {
+ dev_err(dev, "IT6625 does not support C-PHY, only IT6626 does");
+ v4l2_fwnode_endpoint_free(&endpoint);
+ return -EINVAL;
+ }
+
+ max_lanes = (endpoint.bus_type == V4L2_MBUS_CSI2_CPHY) ? 3 : 4;
+
+ if (endpoint.bus.mipi_csi2.num_data_lanes == 0 ||
+ endpoint.bus.mipi_csi2.num_data_lanes > max_lanes) {
+ dev_err(dev,
+ "invalid number of CSI data lanes: %u (max %u for this bus type)",
+ endpoint.bus.mipi_csi2.num_data_lanes, max_lanes);
+ v4l2_fwnode_endpoint_free(&endpoint);
+ return -EINVAL;
+ }
+
+ it6625->csi_lanes = endpoint.bus.mipi_csi2.num_data_lanes;
+ it6625->bus_type = endpoint.bus_type;
+ v4l2_fwnode_endpoint_free(&endpoint);
+
+ return 0;
+}
+
+static int it6625_parse_dt(struct it6625 *it6625)
+{
+ return it6625_parse_endpoint(it6625);
+}
+
+static int it6625_init_v4l2_subdev(struct it6625 *it6625)
+{
+ struct v4l2_subdev *sd = &it6625->sd;
+ int err;
+
+ sd->dev = it6625->dev;
+
+ v4l2_i2c_subdev_init(sd, it6625->i2c_client, &it6625_ops);
+ sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS;
+ if (it6625_v4l2_init_controls(sd)) {
+ dev_err(it6625->dev, "Failed to initialize v4l2 controls");
+ return -ENOMEM;
+ }
+
+ it6625->pad.flags = MEDIA_PAD_FL_SOURCE;
+ sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
+ err = media_entity_pads_init(&sd->entity, 1, &it6625->pad);
+ if (err < 0) {
+ dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err);
+ return err;
+ }
+
+ err = v4l2_async_register_subdev(sd);
+ if (err < 0) {
+ dev_err(it6625->dev, "%s %d err=%d", __func__, __LINE__, err);
+ media_entity_cleanup(&sd->entity);
+ return err;
+ }
+
+ return 0;
+}
+
+static int it6625_check_device(struct it6625 *it6625)
+{
+ static const u8 chip_ids[][2] = {
+ { 0x66, 0x25 },
+ { 0x66, 0x26 },
+ };
+ int chip_id0, chip_id1;
+
+ chip_id0 = it6625_read_byte(it6625, REG_CHIP_ID_0);
+ chip_id1 = it6625_read_byte(it6625, REG_CHIP_ID_1);
+ if (chip_id0 != chip_ids[it6625->chip_type][0] ||
+ chip_id1 != chip_ids[it6625->chip_type][1]) {
+ dev_err(it6625->dev,
+ "chip ID mismatch: got 0x%02x%02x, expected 0x%02x%02x",
+ chip_id0, chip_id1,
+ chip_ids[it6625->chip_type][0],
+ chip_ids[it6625->chip_type][1]);
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
+static int it6625_probe(struct i2c_client *client)
+{
+ struct it6625 *it6625;
+ struct v4l2_subdev *sd;
+ int err;
+
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
+ return -EIO;
+
+ it6625 = devm_kzalloc(&client->dev, sizeof(struct it6625), GFP_KERNEL);
+ if (!it6625)
+ return -ENOMEM;
+
+ it6625->chip_type = (uintptr_t)i2c_get_match_data(client);
+
+ err = it6625_regmap_i2c_init(client, it6625);
+ if (err)
+ return err;
+
+ err = it6625_check_device(it6625);
+ if (err)
+ return err;
+
+ it6625_init_data(it6625);
+
+ err = it6625_parse_dt(it6625);
+ if (err)
+ return err;
+
+ mutex_init(&it6625->it6625_lock);
+ INIT_DELAYED_WORK(&it6625->hpd_delayed_work, it6625_hpd_delayed_work);
+ INIT_WORK(&it6625->polling_work, it6625_polling_work);
+
+ if (client->irq) {
+ err = devm_request_threaded_irq(&client->dev, client->irq,
+ NULL, it6625_irq_handler,
+ IRQF_TRIGGER_LOW |
+ IRQF_ONESHOT |
+ IRQF_NO_AUTOEN,
+ "it6625", it6625);
+ if (err)
+ return err;
+ } else {
+ dev_info(it6625->dev, "no IRQ, falling back to polling");
+ timer_setup(&it6625->timer, it6625_irq_poll_timer, 0);
+ }
+
+ sd = &it6625->sd;
+ err = it6625_init_v4l2_subdev(it6625);
+ if (err)
+ goto err_clean_work_queues;
+
+ err = v4l2_ctrl_handler_setup(sd->ctrl_handler);
+ if (err)
+ goto err_clean_media_entity;
+
+ it6625->cec_adap = cec_allocate_adapter(&it6625_cec_adap_ops,
+ it6625, dev_name(it6625->dev),
+ CEC_CAP_DEFAULTS |
+ CEC_CAP_MONITOR_ALL |
+ CEC_CAP_PHYS_ADDR,
+ CEC_MAX_LOG_ADDRS);
+ if (IS_ERR(it6625->cec_adap)) {
+ err = PTR_ERR(it6625->cec_adap);
+ dev_err(it6625->dev, "%s %d", __func__, __LINE__);
+ goto err_clean_hdl;
+ }
+
+ err = cec_register_adapter(it6625->cec_adap, &client->dev);
+ if (err < 0) {
+ dev_err(it6625->dev, "%s: failed to register the cec device", __func__);
+ cec_delete_adapter(it6625->cec_adap);
+ it6625->cec_adap = NULL;
+ goto err_clean_hdl;
+ }
+
+ it6625_debugfs_init(it6625, client);
+
+ it6625_initial_setup(it6625);
+ it6625_v4l2_sd_ctrl_update(sd);
+
+ if (client->irq)
+ enable_irq(client->irq);
+ else
+ mod_timer(&it6625->timer, jiffies + msecs_to_jiffies(POLL_INTERVAL_MS));
+
+ return 0;
+
+err_clean_hdl:
+ v4l2_ctrl_handler_free(&it6625->hdl);
+err_clean_media_entity:
+ v4l2_async_unregister_subdev(sd);
+ v4l2_device_unregister_subdev(sd);
+ media_entity_cleanup(&sd->entity);
+
+err_clean_work_queues:
+ if (!client->irq)
+ timer_shutdown_sync(&it6625->timer);
+ cancel_work_sync(&it6625->polling_work);
+ cancel_delayed_work(&it6625->hpd_delayed_work);
+ mutex_destroy(&it6625->it6625_lock);
+ return err;
+}
+
+static void it6625_remove(struct i2c_client *client)
+{
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct it6625 *it6625 = sd_to_6625(sd);
+
+ if (client->irq)
+ disable_irq(client->irq);
+ else
+ timer_shutdown_sync(&it6625->timer);
+
+ cancel_work_sync(&it6625->polling_work);
+ cancel_delayed_work_sync(&it6625->hpd_delayed_work);
+ debugfs_remove_recursive(it6625->debugfs_dir);
+ cec_unregister_adapter(it6625->cec_adap);
+ v4l2_async_unregister_subdev(sd);
+ v4l2_device_unregister_subdev(sd);
+ mutex_destroy(&it6625->it6625_lock);
+ media_entity_cleanup(&sd->entity);
+ v4l2_ctrl_handler_free(&it6625->hdl);
+}
+
+static const struct i2c_device_id it6625_id[] = {
+ { "it6625", IT6625_CHIP },
+ { "it6626", IT6626_CHIP },
+ {}
+};
+MODULE_DEVICE_TABLE(i2c, it6625_id);
+
+static const struct of_device_id it6625_of_match[] = {
+ { .compatible = "ite,it6625", .data = (void *)IT6625_CHIP },
+ { .compatible = "ite,it6626", .data = (void *)IT6626_CHIP },
+ {},
+};
+MODULE_DEVICE_TABLE(of, it6625_of_match);
+
+static struct i2c_driver it6625_driver = {
+ .driver = {
+ .name = "it6625",
+ .of_match_table = of_match_ptr(it6625_of_match),
+ },
+ .probe = it6625_probe,
+ .remove = it6625_remove,
+ .id_table = it6625_id,
+};
+module_i2c_driver(it6625_driver);
+
+MODULE_DESCRIPTION("iTE it6625/it6626 HDMI to MIPI CSI bridge driver");
+MODULE_AUTHOR("Hermes Wu <Hermes.wu@ite.com.tw>");
+MODULE_LICENSE("GPL");
--
2.34.1
^ permalink raw reply related
* [PATCH 1/2] dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding
From: Hermes Wu via B4 Relay @ 2026-07-20 7:57 UTC (permalink / raw)
To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: linux-media, devicetree, linux-kernel, Hermes Wu
In-Reply-To: <20260720-upstream-it6625-it6626-v1-0-475912f74459@ite.com.tw>
From: Hermes Wu <Hermes.wu@ite.com.tw>
Document the devicetree binding for the ITE IT6625/IT6626 HDMI to
MIPI CSI-2 bridge. The device exposes three graph ports: port@0
(MIPI0) and port@1 (MIPI1) are the two selectable CSI-2 D-PHY/C-PHY
outputs, and port@2 is the HDMI connector input. Only port@0 is
required, since a board only needs to wire up as many of the bridge's
outputs as it actually uses.
Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw>
---
.../devicetree/bindings/media/i2c/ite,it6625.yaml | 136 +++++++++++++++++++++
1 file changed, 136 insertions(+)
diff --git a/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..102af5dcb380b0ad2462c376e5a87b22d7296f14
--- /dev/null
+++ b/Documentation/devicetree/bindings/media/i2c/ite,it6625.yaml
@@ -0,0 +1,136 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/media/i2c/ite,it6625.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ITE IT6625/IT6626 HDMI to dual MIPI CSI-2 bridge
+
+maintainers:
+ - Hermes Wu <Hermes.wu@ite.com.tw>
+
+description: |-
+ The ITE IT6625 and IT6626 are HDMI to MIPI CSI-2 bridge devices.
+ IT6625 supports an HDMI 2.0 input and converts it to one or two D-PHY CSI
+ outputs, while IT6626 supports an HDMI 2.1 input and converts it to one or
+ two C/D-PHY CSI outputs. The bridges are programmable through I2C and
+ expose one HDMI input port and two selectable CSI-2 output ports. The
+ bridge can operate in split mode or clone mode.
+
+properties:
+ compatible:
+ enum:
+ - ite,it6625
+ - ite,it6626
+
+ reg:
+ maxItems: 1
+
+ reset-gpios:
+ description:
+ GPIO connected to the active-low reset line.
+ maxItems: 1
+
+ ports:
+ $ref: /schemas/graph.yaml#/properties/ports
+ properties:
+ port@0:
+ $ref: /schemas/graph.yaml#/$defs/port-base
+ unevaluatedProperties: false
+ description: CSI-2 output port MIPI0
+
+ properties:
+ endpoint:
+ $ref: /schemas/media/video-interfaces.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ data-lanes:
+ minItems: 1
+ maxItems: 4
+
+ bus-type:
+ enum:
+ - 1 # MEDIA_BUS_TYPE_CSI2_CPHY
+ - 4 # MEDIA_BUS_TYPE_CSI2_DPHY
+
+ clock-noncontinuous: true
+ link-frequencies: true
+
+ required:
+ - data-lanes
+
+ port@1:
+ $ref: /schemas/graph.yaml#/$defs/port-base
+ unevaluatedProperties: false
+ description: CSI-2 output port MIPI1
+
+ properties:
+ endpoint:
+ $ref: /schemas/media/video-interfaces.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ data-lanes:
+ minItems: 1
+ maxItems: 4
+
+ bus-type:
+ enum:
+ - 1 # MEDIA_BUS_TYPE_CSI2_CPHY
+ - 4 # MEDIA_BUS_TYPE_CSI2_DPHY
+
+ clock-noncontinuous: true
+ link-frequencies: true
+
+ required:
+ - data-lanes
+
+ port@2:
+ $ref: /schemas/graph.yaml#/$defs/port-base
+ unevaluatedProperties: false
+ description: HDMI connector input port
+
+ properties:
+ endpoint:
+ $ref: /schemas/graph.yaml#/$defs/endpoint-base
+ unevaluatedProperties: false
+
+ required:
+ - port@0
+required:
+ - compatible
+ - reg
+ - ports
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ hdmi-bridge@4c {
+ compatible = "ite,it6625";
+ reg = <0x4c>;
+
+ reset-gpios = <&gpio 2 GPIO_ACTIVE_LOW>;
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ csi_out0: endpoint {
+ remote-endpoint = <&csi2_rx0>;
+ bus-type = <4>; /* MEDIA_BUS_TYPE_CSI2_DPHY */
+ data-lanes = <1 2 3 4>;
+ };
+ };
+ };
+ };
+ };
--
2.34.1
^ permalink raw reply related
* [PATCH 0/2] media: i2c: add support for ITE IT6625/IT6626 HDMI to MIPI CSI-2 bridge
From: Hermes Wu via B4 Relay @ 2026-07-20 7:57 UTC (permalink / raw)
To: Hermes Wu, Mauro Carvalho Chehab, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: linux-media, devicetree, linux-kernel, Hermes Wu
This series adds support for the ITE IT6625/IT6626 HDMI to MIPI CSI-2
bridge chips. IT6625 accepts an HDMI 2.0 input and IT6626 an HDMI 2.1
input, converting it to a D-PHY (or C/D-PHY on IT6626) MIPI CSI-2
output. The bridge is configured over I2C, exposes an HDMI CEC
adapter, and supports EDID read/write, DV timings detection and
configuration, and HPD control via the standard V4L2 subdevice pad
and video ops.
Patch 1 documents the devicetree binding for the bridge, including
its three graph ports (two selectable CSI-2 outputs and one HDMI
input).
Patch 2 adds the V4L2 subdevice driver itself.
Signed-off-by: Hermes Wu <Hermes.wu@ite.com.tw>
---
Hermes Wu (2):
dt-bindings: media: add ITE IT6625/IT6626 HDMI bridge binding
media: i2c: add driver for ITE IT6625/IT6626
.../devicetree/bindings/media/i2c/ite,it6625.yaml | 136 ++
MAINTAINERS | 7 +
drivers/media/i2c/Kconfig | 18 +
drivers/media/i2c/Makefile | 1 +
drivers/media/i2c/it6625.c | 2088 ++++++++++++++++++++
5 files changed, 2250 insertions(+)
---
base-commit: 8dac27bfa2f994ecb11f01a63641527d17d48fc1
change-id: 20260716-upstream-it6625-it6626-6874ab8a5305
Best regards,
--
Hermes Wu <Hermes.wu@ite.com.tw>
^ permalink raw reply
* Re: [PATCH v3 3/3] tee: add MbedTEE driver
From: sashiko-bot @ 2026-07-20 7:55 UTC (permalink / raw)
To: Xing Loong; +Cc: conor+dt, devicetree, robh
In-Reply-To: <20260720073558.799755-4-xing.xl.loong@gmail.com>
Thank you for your contribution! Sashiko AI review found 13 potential issue(s) to consider:
- [Critical] Unvalidated cmd.size in the rpc_pick_next error path causes kernel stack buffer overflow.
- [High] xa_insert_irq() called with GFP_KERNEL causes sleeping in atomic context.
- [High] Early parameter validation failure in mbedtee_supp_send() hangs kernel worker threads.
- [High] Malicious TEE can deadlock the kernel by sending supplicant requests as async RPCs.
- [High] RPC call object is accessed after being freed when wait_for_completion is interrupted.
- [High] UAF and list corruption in mbedtee_supp_recv due to unprotected access.
- [High] Unvalidated parameter attributes from the TEE allow an attacker to permanently leak shared memory object references.
- [Medium] RISC-V yield calls fail spuriously with -ENOSPC when the REE-to-TEE ring is transiently full.
- [Medium] The required buffer size is not returned to userspace when a supplicant buffer is too small.
- [Medium] Infinite retry loop in workqueue on TEE failure blocks teardown.
- [Medium] Leaked RPC calls on module removal due to xa_destroy without element freeing.
- [Medium] Data race on complete_work_pending between IRQ and workqueue contexts.
- [Medium] Transient memory allocation failure in rpc_pick_next stalls the ring buffer permanently.
--
--- Patch [3]: [PATCH v3 3/3] tee: add MbedTEE driver ---
commit a7be6b832a5cd2c0d320336aea6907717da8717c
Author: Xing Loong <xing.xl.loong@gmail.com>
tee: add MbedTEE driver
This patch adds a Linux TEE driver that communicates with MbedTEE OS via a
fixed shared-memory RPC protocol. It supports ARM TrustZone and RISC-V IMSIC
platforms.
> diff --git a/drivers/tee/mbedtee/core.c b/drivers/tee/mbedtee/core.c
> --- a/drivers/tee/mbedtee/core.c
> +++ b/drivers/tee/mbedtee/core.c
[ ... ]
> @@ -198,16 +198,15 @@ static void mbedtee_remove(struct platform_device *pdev)
> {
> struct mbedtee_device *mbedtee = platform_get_drvdata(pdev);
>
> tee_device_unregister(mbedtee->teedev);
> /*
> * Drain T2R before tearing down R2T: queued RPC work may still need
> * to send COMPLETE_TEE over R2T. Destroy the supplicant mutex only
> * after RPC work can no longer enter mbedtee_supp_handler().
> */
> mbedtee_rpc_uninit(mbedtee);
> mbedtee_r2t_uninit(mbedtee);
> mbedtee_supp_uninit(&mbedtee->supp);
> tee_shm_pool_free(mbedtee->pool);
> xa_destroy(&mbedtee->rpc_calls);
[Severity: Medium]
Are the elements in the xarray leaked on module removal? Calling xa_destroy
frees the internal nodes of the xarray, but it does not iterate over and free
the elements themselves. Any RPC calls left in the MBEDTEE_RPC_CALL_INTERRUPTED
state would be permanently leaked.
> }
> diff --git a/drivers/tee/mbedtee/rpc_callee.c b/drivers/tee/mbedtee/rpc_callee.c
> --- a/drivers/tee/mbedtee/rpc_callee.c
> +++ b/drivers/tee/mbedtee/rpc_callee.c
[ ... ]
> @@ -148,15 +148,15 @@ static void t2r_ring_read(struct mbedtee_t2r_ctx *ctx,
> }
>
> static bool rpc_queue_complete_only(struct mbedtee_t2r_ctx *ctx,
> u64 waiter_id)
> {
> if (ctx->complete_work_pending)
[Severity: Medium]
Is there a data race on complete_work_pending here? This variable is read as a
plain C variable in this IRQ context, but it is modified using WRITE_ONCE in
rpc_routine from a workqueue context. This could allow the compiler to cache
or reorder the read, potentially leaving the driver perpetually thinking a
completion work is pending.
> return false;
>
> ctx->complete_work_pending = true;
> ctx->complete_work.waiter_id = waiter_id;
> queue_work(ctx->rpc_wq, &ctx->complete_work.work);
>
> return true;
> }
[ ... ]
> @@ -215,9 +215,9 @@ static struct rpc_work *rpc_pick_next(struct mbedtee_device *mbedtee,
>
> if (off > ctx->t2r_shm_sz - cmd.size)
> goto skip;
>
> new_work = kzalloc_obj(*new_work, GFP_ATOMIC);
> if (!new_work)
> return NULL;
[Severity: Medium]
Does a transient memory allocation failure here permanently stall the ring
buffer? If kzalloc_obj fails with GFP_ATOMIC, the function returns NULL and
rpc_drain_ring breaks out of its loop without advancing the read pointer.
The TEE thread would block forever since the entry remains unprocessed and no
retry is scheduled.
>
> t2r_ring_advance(ctx, sizeof(cmd));
[ ... ]
> @@ -279,11 +279,11 @@ skip:
> if (cmd.waiter_id != 0 && !rpc_queue_complete_only(ctx, cmd.waiter_id))
> return NULL;
>
> /*
> * Bad or unhandled entry: consume header plus any inline payload
> * (async RPC payloads follow the header in the ring) to keep the
> * ring moving. For sync RPCs payload is in t2r_shm, not the ring.
> */
> t2r_ring_advance(ctx, sizeof(cmd) + (cmd.waiter_id ? 0 : cmd.size));
[Severity: Critical]
Can this advance the read pointer by an unvalidated size and cause a stack
buffer overflow? In rpc_pick_next, if an async RPC jumps to the skip label,
t2r_ring_advance is called with 32 + cmd.size without checking if cmd.size
exceeds the ring buffer capacity.
If a malicious TEE sets a very large cmd.size, the read pointer wraps. On the
next iteration, t2r_available_size underflows and t2r_ring_copy executes
a memcpy of up to 4GB directly into the cmd struct on the kernel stack.
>
> return NULL;
> }
[ ... ]
> @@ -296,14 +296,14 @@ static void rpc_drain_ring(struct mbedtee_device *mbedtee)
> spin_lock_irqsave(&ctx->ring_lock, flags);
> while (READ_ONCE(ctx->t2r_ring_rd) !=
> /* Pair with producer store-release after ring write. */
> smp_load_acquire(&ctx->t2r_ring->wr)) {
> rw.data = ctx->rpc_data;
> c = rpc_pick_next(mbedtee, ctx, &rw);
> if (!c)
> break;
>
> if (c->waiter_id == 0) {
> c->func(mbedtee, c->data, c->size);
[Severity: High]
Is it possible for a malicious TEE to deadlock the kernel by sending
supplicant requests as async RPCs? The TEE-to-REE interrupt handler runs in
hard IRQ context, and rpc_drain_ring relies entirely on cmd.waiter_id == 0
to process a command inline.
If the TEE sends MBEDTEE_RPC_REEFS with waiter_id = 0, the driver invokes
mbedtee_supp_handler inline while holding the ring_lock. That function
allocates memory with GFP_KERNEL and calls wait_for_completion, both of
which sleep in atomic context.
> } else {
> INIT_WORK(&c->work, rpc_routine);
> queue_work(ctx->rpc_wq, &c->work);
> }
> }
> spin_unlock_irqrestore(&ctx->ring_lock, flags);
> }
[ ... ]
> @@ -339,12 +339,12 @@ static void rpc_routine(struct work_struct *work)
> * Keep retrying rather than silently dropping the completion, which
> * would leave the TEE thread blocked in rpc_call_sync forever.
> * On ARM the fastcall is a direct SMC so -ENOSPC never occurs.
> */
> do {
> ret = mbedtee_rpc_fastcall(c->mbedtee, MBEDTEE_RPC_COMPLETE_TEE,
> (unsigned long)c->waiter_id, 0, 0);
> if (ret != -ENOSPC && ret != -ENOMEM)
> break;
> cond_resched();
> } while (1);
[Severity: Medium]
Could this loop infinitely if the ring buffer remains full? If
mbedtee_rpc_fastcall repeatedly returns -ENOSPC (which occurs on RISC-V when
the R2T ring is full), it retries indefinitely. If the TEE crashes and stops
draining the ring, this loop never terminates, and destroy_workqueue during
module removal would block forever waiting for the hung work item.
>
> if (c != &ctx->complete_work) {
> diff --git a/drivers/tee/mbedtee/rpc_caller.c b/drivers/tee/mbedtee/rpc_caller.c
> --- a/drivers/tee/mbedtee/rpc_caller.c
> +++ b/drivers/tee/mbedtee/rpc_caller.c
[ ... ]
> @@ -35,12 +35,12 @@ int mbedtee_rpc_call_alloc(struct mbedtee_device *mbedtee,
> rcall->rpc.ret = -EOPNOTSUPP;
> rcall->rpc_phys = mbedtee_virt_to_phys(&rcall->rpc);
>
> do {
> rpc_index = atomic_long_inc_return(&mbedtee->rpc_call_seq);
> } while (rpc_index == 0);
>
> ret = xa_insert_irq(&mbedtee->rpc_calls, rpc_index, rcall, GFP_KERNEL);
[Severity: High]
Does this code sleep in atomic context? The driver initializes
mbedtee->rpc_calls with XA_FLAGS_ALLOC1. Calling xa_insert_irq with
GFP_KERNEL drops the spinlock but leaves hardware interrupts disabled, then
calls kmem_cache_alloc with GFP_KERNEL which can sleep, violating atomic
context rules.
> if (ret != 0) {
> kfree(rcall);
> return ret;
> }
[ ... ]
> @@ -170,14 +170,14 @@ static int mbedtee_param_decode(struct tee_param *params,
> case TEE_IOCTL_PARAM_ATTR_TYPE_MEMREF_INPUT:
> case TEE_IOCTL_PARAM_ATTR_TYPE_MEMREF_OUTPUT:
> case TEE_IOCTL_PARAM_ATTR_TYPE_MEMREF_INOUT:
> p->u.memref.size = rtp->memref.size;
> break;
> default:
> return -EINVAL;
> }
> p->attr = attr;
[Severity: High]
Can a malicious TEE permanently leak shared memory object references by
returning unvalidated parameter attributes? The kernel decodes the parameter
types returned by the TEE and unconditionally overwrites the local p->attr
with the TEE's value.
If userspace passes a MEMREF, a compromised TEE could return a VALUE type for
that slot, causing the TEE subsystem to skip dropping the reference during
cleanup because tee_param_is_memref will see a VALUE type instead.
> }
> return 0;
> }
[ ... ]
> @@ -415,12 +415,12 @@ int mbedtee_invoke_func(struct tee_context *ctx,
> if (ret) {
> mbedtee_rpc_call_free(mbedtee, call);
> return ret;
> }
>
> ret = mbedtee_rpc_yieldcall(mbedtee, MBEDTEE_RPC_INVOKE_SESSION, call, true);
>
> dev_dbg(mbedtee->dev, "invoke session ret %d gp_ret %d\n", ret,
> call->rpc.ret);
[Severity: High]
Does this introduce a use-after-free if wait_for_completion is interrupted? If
mbedtee_rpc_wait_for_completion is interrupted by a signal, it sets the
state to MBEDTEE_RPC_CALL_INTERRUPTED and returns -ERESTARTSYS. The caller
here then reads call->rpc.ret and calls mbedtee_rpc_call_free.
Concurrently, mbedtee_rpc_complete_call can see the INTERRUPTED state, erase
the XArray entry, and free the call structure, causing the original caller to
perform UAF reads.
>
> if (ret != 0) {
> arg->ret = TEEC_ERROR_COMMUNICATION;
> diff --git a/drivers/tee/mbedtee/rpc_caller_riscv.c b/drivers/tee/mbedtee/rpc_caller_riscv.c
> --- a/drivers/tee/mbedtee/rpc_caller_riscv.c
> +++ b/drivers/tee/mbedtee/rpc_caller_riscv.c
[ ... ]
> @@ -75,10 +75,10 @@ long mbedtee_rpc_yieldcall(struct mbedtee_device *mbedtee,
> call->rpc.id = fn;
>
> spin_lock_irqsave(&ctx->lock, flags);
> if (!rpc_ring_enough(ctx, sizeof(u64))) {
> dev_err_ratelimited(mbedtee->dev, "rpc ring full\n");
> spin_unlock_irqrestore(&ctx->lock, flags);
> return -ENOSPC;
[Severity: Medium]
Should this implement a wait queue or backoff instead of failing immediately?
On RISC-V, mbedtee_rpc_yieldcall returns -ENOSPC if the r2t-ring buffer is
full, which mbedtee_invoke_func translates to TEEC_ERROR_COMMUNICATION.
Without a polling backoff or wait mechanism, high concurrency from userspace
will easily fill the ring and cause spurious, unrecoverable failures.
> }
>
> phys = call->rpc_phys;
> diff --git a/drivers/tee/mbedtee/supp.c b/drivers/tee/mbedtee/supp.c
> --- a/drivers/tee/mbedtee/supp.c
> +++ b/drivers/tee/mbedtee/supp.c
[ ... ]
> @@ -232,18 +232,18 @@ int mbedtee_supp_recv(struct tee_context *ctx,
> if (wait_for_completion_interruptible(&supp->reqs_c)) {
> ret = -ERESTARTSYS;
> goto err;
> }
> }
>
> /* Record which supplicant context owns this request. */
> req->ctx = ctx;
>
> if (req->size > param->u.memref.size) {
[Severity: High]
Can this cause a use-after-free and linked list corruption? After
supp_pop_req removes a request and drops the mutex, the function accesses
req->ctx and req->size locklessly.
If mbedtee_supp_abort_all runs concurrently (e.g. on module removal), it
could remove the request from active_reqs, complete it, and free it, causing
UAF reads here. If the size check fails, it then takes the mutex and
manipulates the list links of the freed node, corrupting the supp->reqs list.
[Severity: Medium]
Does this code fail to return the required buffer size to userspace? When
req->size is larger than param->u.memref.size, it correctly returns -EOVERFLOW
but fails to update param->u.memref.size with the required req->size. The
generic TEE framework relies on this updated value to inform userspace how
much memory to allocate, breaking buffer renegotiation.
> /* Return the request to the queue so it isn't lost */
> mutex_lock(&supp->mutex);
> list_del(&req->node);
> list_add(&req->node, &supp->reqs);
> mutex_unlock(&supp->mutex);
> ret = -EOVERFLOW;
> goto err;
> }
[ ... ]
> @@ -262,11 +262,11 @@ int mbedtee_supp_send(struct tee_context *ctx, u32 ret, u32 num_params,
> struct tee_param *param)
> {
> struct tee_device *teedev = ctx->teedev;
> struct mbedtee_device *mbedtee = tee_get_drvdata(teedev);
> struct mbedtee_supp *supp = &mbedtee->supp;
> struct mbedtee_supp_req *req;
> struct mbedtee_supp_req *_req;
>
> if (num_params != 1)
> return -EINVAL;
[Severity: High]
Could this lead to a permanently hung kernel worker thread? If userspace calls
TEE_IOC_SUPPL_SEND with invalid parameters (like num_params != 1),
mbedtee_supp_send returns -EINVAL early. Because it does not pop the active
request or complete it, the worker thread executing mbedtee_supp_handler
would remain permanently blocked in wait_for_completion.
>
> if (!tee_param_is_memref(param))
> return -EINVAL;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260720073558.799755-1-xing.xl.loong@gmail.com?part=3
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox