From e0cfe0bf598094787b36bc6cec5253386d91015d Mon Sep 17 00:00:00 2001 From: Sasha Finkelstein Date: Tue, 21 Jul 2026 00:20:01 +0200 Subject: [PATCH 01/12] dt-bindings: spmi: apple,spmi: Add t603x Add t603x compatibles and interrupt support Signed-off-by: Sasha Finkelstein --- .../devicetree/bindings/spmi/apple,spmi.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml index f7920dd3a59b62..fceed47b2cd17f 100644 --- a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml +++ b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml @@ -20,6 +20,8 @@ properties: - items: - enum: - apple,t6020-spmi + - apple,t6030-spmi + - apple,t6031-spmi - apple,t8012-spmi - apple,t8015-spmi - apple,t8122-spmi @@ -35,6 +37,18 @@ properties: reg: maxItems: 1 + interrupts: + maxItems: 1 + description: Optional, operates in polled mode if not present + + interrupt-controller: true + + "#interrupt-cells": + const: 2 + + power-domains: + maxItems: 1 + required: - compatible - reg From f7a15b638c85ce6d0be7ca72f9b949c12db97193 Mon Sep 17 00:00:00 2001 From: Alba Mendez Date: Sun, 22 Jun 2025 15:29:38 +0200 Subject: [PATCH 02/12] spmi: apple: Validate FIFO state Check for data before reading the body of a reply, and check for end of data afterwards. Signed-off-by: Alba Mendez Signed-off-by: Sasha Finkelstein --- drivers/spmi/spmi-apple-controller.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index 87e3ee9d4f2aa5..ac5a85212f2759 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -79,6 +79,10 @@ static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, /* Read SPMI data reply */ while (len_read < len) { + if (readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY) { + dev_err(&ctrl->dev, "FIFO lacks reply data, controller stuck?\n"); + return -EIO; + } rsp = readl(spmi->regs + SPMI_RSP_REG); i = 0; while ((len_read < len) && (i < 4)) { @@ -87,6 +91,9 @@ static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, } } + if (!(readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY)) + dev_warn(&ctrl->dev, "FIFO has extra data\n"); + return 0; } @@ -116,6 +123,9 @@ static int spmi_write_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, /* Discard */ readl(spmi->regs + SPMI_RSP_REG); + if (!(readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY)) + dev_warn(&ctrl->dev, "FIFO has extra data\n"); + return 0; } From c327d228e8f3f6ba89fee8a93a844e10b766667f Mon Sep 17 00:00:00 2001 From: Alba Mendez Date: Sun, 22 Jun 2025 18:20:14 +0200 Subject: [PATCH 03/12] spmi: apple: check transaction status Check for parity errors and missing command ACKs Signed-off-by: Alba Mendez Signed-off-by: Sasha Finkelstein --- drivers/spmi/spmi-apple-controller.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index ac5a85212f2759..837dbdc797a2de 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -23,6 +23,12 @@ #define SPMI_CMD_REG 0x4 #define SPMI_RSP_REG 0x8 +/* SPMI_RSP_REG reply word */ +#define SPMI_REPLY_FRAME_PARITY_OFFSET 16 +#define SPMI_REPLY_ACK BIT(15) +#define SPMI_REPLY_SLAVE_ID GENMASK(14, 8) +#define SPMI_REPLY_CMD GENMASK(7, 0) + #define SPMI_RX_FIFO_EMPTY BIT(24) #define REG_POLL_INTERVAL_US 10000 @@ -63,7 +69,7 @@ static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, { struct apple_spmi *spmi = spmi_controller_get_drvdata(ctrl); u32 spmi_cmd = apple_spmi_pack_cmd(opc, sid, saddr, len); - u32 rsp; + u32 reply, rsp; size_t len_read = 0; u8 i; int ret; @@ -74,8 +80,7 @@ static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, if (ret) return ret; - /* Discard SPMI reply status */ - readl(spmi->regs + SPMI_RSP_REG); + reply = readl(spmi->regs + SPMI_RSP_REG); /* Read SPMI data reply */ while (len_read < len) { @@ -94,6 +99,10 @@ static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, if (!(readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY)) dev_warn(&ctrl->dev, "FIFO has extra data\n"); + if ((~reply >> SPMI_REPLY_FRAME_PARITY_OFFSET) & ((1 << len) - 1)) { + dev_err(&ctrl->dev, "some frames failed parity check\n"); + return -EIO; + } return 0; } @@ -102,6 +111,7 @@ static int spmi_write_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, { struct apple_spmi *spmi = spmi_controller_get_drvdata(ctrl); u32 spmi_cmd = apple_spmi_pack_cmd(opc, sid, saddr, len); + u32 reply; size_t i = 0, j; int ret; @@ -120,12 +130,15 @@ static int spmi_write_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, if (ret) return ret; - /* Discard */ - readl(spmi->regs + SPMI_RSP_REG); + reply = readl(spmi->regs + SPMI_RSP_REG); if (!(readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY)) dev_warn(&ctrl->dev, "FIFO has extra data\n"); + if (!(reply & SPMI_REPLY_ACK)) { + dev_err(&ctrl->dev, "command not acknowledged\n"); + return -EIO; + } return 0; } From 9e79d0350b329ce571634e5b54dbc782e54d0062 Mon Sep 17 00:00:00 2001 From: Alba Mendez Date: Sun, 14 Sep 2025 14:49:37 +0200 Subject: [PATCH 04/12] spmi: apple: Implement remaining commands Add support for zero write and power management commands Signed-off-by: Alba Mendez Signed-off-by: Sasha Finkelstein --- drivers/spmi/spmi-apple-controller.c | 106 +++++++++++++++++---------- 1 file changed, 68 insertions(+), 38 deletions(-) diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index 837dbdc797a2de..cda306d3a074f7 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -42,9 +42,9 @@ struct apple_spmi { readl_poll_timeout((spmi)->regs + (reg), (val), (cond), \ REG_POLL_INTERVAL_US, REG_POLL_TIMEOUT_US) -static inline u32 apple_spmi_pack_cmd(u8 opc, u8 sid, u16 saddr, size_t len) +static inline u32 apple_spmi_pack_cmd(u8 opc, u8 sid, u16 param) { - return opc | sid << 8 | saddr << 16 | (len - 1) | (1 << 15); + return opc | sid << 8 | param << 16 | (1 << 15); } /* Wait for Rx FIFO to have something */ @@ -64,18 +64,27 @@ static int apple_spmi_wait_rx_not_empty(struct spmi_controller *ctrl) return 0; } -static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, - u16 saddr, u8 *buf, size_t len) +static int spmi_raw_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, + u16 param, const u8 *buf, size_t len, u8 *ibuf, size_t ilen) { struct apple_spmi *spmi = spmi_controller_get_drvdata(ctrl); - u32 spmi_cmd = apple_spmi_pack_cmd(opc, sid, saddr, len); + u32 spmi_cmd = apple_spmi_pack_cmd(opc, sid, param); u32 reply, rsp; size_t len_read = 0; - u8 i; + size_t i = 0, j; int ret; writel(spmi_cmd, spmi->regs + SPMI_CMD_REG); + while (i < len) { + j = 0; + spmi_cmd = 0; + while ((j < 4) & (i < len)) + spmi_cmd |= buf[i++] << (j++ * 8); + + writel(spmi_cmd, spmi->regs + SPMI_CMD_REG); + } + ret = apple_spmi_wait_rx_not_empty(ctrl); if (ret) return ret; @@ -83,15 +92,15 @@ static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, reply = readl(spmi->regs + SPMI_RSP_REG); /* Read SPMI data reply */ - while (len_read < len) { + while (len_read < ilen) { if (readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY) { dev_err(&ctrl->dev, "FIFO lacks reply data, controller stuck?\n"); return -EIO; } rsp = readl(spmi->regs + SPMI_RSP_REG); i = 0; - while ((len_read < len) && (i < 4)) { - buf[len_read++] = ((0xff << (8 * i)) & rsp) >> (8 * i); + while ((len_read < ilen) && (i < 4)) { + ibuf[len_read++] = ((0xff << (8 * i)) & rsp) >> (8 * i); i += 1; } } @@ -99,47 +108,67 @@ static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, if (!(readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY)) dev_warn(&ctrl->dev, "FIFO has extra data\n"); - if ((~reply >> SPMI_REPLY_FRAME_PARITY_OFFSET) & ((1 << len) - 1)) { + if (!ilen && !(reply & SPMI_REPLY_ACK)) { + dev_err(&ctrl->dev, "command not acknowledged\n"); + return -EIO; + } + if ((~reply >> SPMI_REPLY_FRAME_PARITY_OFFSET) & ((1 << ilen) - 1)) { dev_err(&ctrl->dev, "some frames failed parity check\n"); return -EIO; } return 0; } -static int spmi_write_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, - u16 saddr, const u8 *buf, size_t len) +/* Send a raw command with 1..16 input data frames */ +static int spmi_raw_cmd_input(struct spmi_controller *ctrl, u8 opc, u8 sid, + u16 param, u8 *buf, size_t len) { - struct apple_spmi *spmi = spmi_controller_get_drvdata(ctrl); - u32 spmi_cmd = apple_spmi_pack_cmd(opc, sid, saddr, len); - u32 reply; - size_t i = 0, j; - int ret; - - writel(spmi_cmd, spmi->regs + SPMI_CMD_REG); + return spmi_raw_cmd(ctrl, opc, sid, param, NULL, 0, buf, len); +} - while (i < len) { - j = 0; - spmi_cmd = 0; - while ((j < 4) & (i < len)) - spmi_cmd |= buf[i++] << (j++ * 8); +/* Send a raw command with (optional) body and an input ACK */ +static int spmi_raw_cmd_ack(struct spmi_controller *ctrl, u8 opc, u8 sid, + u16 param, const u8 *buf, size_t len) +{ + return spmi_raw_cmd(ctrl, opc, sid, param, buf, len, NULL, 0); +} - writel(spmi_cmd, spmi->regs + SPMI_CMD_REG); +static int spmi_read_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, + u16 saddr, u8 *buf, size_t len) +{ + switch (opc) { + case SPMI_CMD_EXT_READ: + case SPMI_CMD_EXT_READL: + return spmi_raw_cmd_input(ctrl, opc | (len - 1), sid, saddr, buf, len); + case SPMI_CMD_READ: + return spmi_raw_cmd_input(ctrl, opc | saddr, sid, saddr, buf, len); } + return -EINVAL; +} - ret = apple_spmi_wait_rx_not_empty(ctrl); - if (ret) - return ret; - - reply = readl(spmi->regs + SPMI_RSP_REG); - - if (!(readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY)) - dev_warn(&ctrl->dev, "FIFO has extra data\n"); - - if (!(reply & SPMI_REPLY_ACK)) { - dev_err(&ctrl->dev, "command not acknowledged\n"); - return -EIO; +static int spmi_write_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, + u16 saddr, const u8 *buf, size_t len) +{ + switch (opc) { + case SPMI_CMD_WRITE: + return spmi_raw_cmd_ack(ctrl, opc | saddr, sid, buf[0] << 8 | saddr, NULL, 0); + case SPMI_CMD_ZERO_WRITE: + return spmi_raw_cmd_ack(ctrl, opc | buf[0], sid, buf[0] << 8 | saddr, NULL, 0); + case SPMI_CMD_EXT_WRITE: + case SPMI_CMD_EXT_WRITEL: + return spmi_raw_cmd_ack(ctrl, opc | (len - 1), sid, saddr, buf, len); } - return 0; + return -EINVAL; +} + +static int spmi_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid) +{ + if ( + opc == SPMI_CMD_RESET || opc == SPMI_CMD_SLEEP || + opc == SPMI_CMD_SHUTDOWN || opc == SPMI_CMD_WAKEUP + ) + return spmi_raw_cmd_ack(ctrl, opc, sid, 0, NULL, 0); + return -EINVAL; } static int apple_spmi_probe(struct platform_device *pdev) @@ -162,6 +191,7 @@ static int apple_spmi_probe(struct platform_device *pdev) ctrl->read_cmd = spmi_read_cmd; ctrl->write_cmd = spmi_write_cmd; + ctrl->cmd = spmi_cmd; ret = devm_spmi_controller_add(&pdev->dev, ctrl); if (ret) From 7f403a76a1c5826dabddbde8f58ecf250fb3ec3e Mon Sep 17 00:00:00 2001 From: Alba Mendez Date: Sun, 21 Sep 2025 12:28:14 +0200 Subject: [PATCH 05/12] spmi: apple: lock around FIFOs The driver was missing locking around register interactions Signed-off-by: Alba Mendez Signed-off-by: Sasha Finkelstein --- drivers/spmi/spmi-apple-controller.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index cda306d3a074f7..ad3d8af09d7445 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -17,6 +17,7 @@ #include #include #include +#include /* SPMI Controller Registers */ #define SPMI_STATUS_REG 0 @@ -36,6 +37,7 @@ struct apple_spmi { void __iomem *regs; + struct mutex fifo_lock; }; #define poll_reg(spmi, reg, val, cond) \ @@ -74,6 +76,10 @@ static int spmi_raw_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, size_t i = 0, j; int ret; + ret = mutex_lock_interruptible(&spmi->fifo_lock); + if (ret) + return ret; + writel(spmi_cmd, spmi->regs + SPMI_CMD_REG); while (i < len) { @@ -87,7 +93,7 @@ static int spmi_raw_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, ret = apple_spmi_wait_rx_not_empty(ctrl); if (ret) - return ret; + goto out; reply = readl(spmi->regs + SPMI_RSP_REG); @@ -95,7 +101,8 @@ static int spmi_raw_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, while (len_read < ilen) { if (readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY) { dev_err(&ctrl->dev, "FIFO lacks reply data, controller stuck?\n"); - return -EIO; + ret = -EIO; + goto out; } rsp = readl(spmi->regs + SPMI_RSP_REG); i = 0; @@ -108,6 +115,8 @@ static int spmi_raw_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, if (!(readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY)) dev_warn(&ctrl->dev, "FIFO has extra data\n"); + mutex_unlock(&spmi->fifo_lock); + if (!ilen && !(reply & SPMI_REPLY_ACK)) { dev_err(&ctrl->dev, "command not acknowledged\n"); return -EIO; @@ -117,6 +126,10 @@ static int spmi_raw_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, return -EIO; } return 0; + +out: + mutex_unlock(&spmi->fifo_lock); + return ret; } /* Send a raw command with 1..16 input data frames */ @@ -182,6 +195,7 @@ static int apple_spmi_probe(struct platform_device *pdev) return -ENOMEM; spmi = spmi_controller_get_drvdata(ctrl); + mutex_init(&spmi->fifo_lock); spmi->regs = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(spmi->regs)) From c6b431ac60a113c98a8dc9970577af28059660e0 Mon Sep 17 00:00:00 2001 From: Alba Mendez Date: Sun, 21 Sep 2025 17:43:30 +0200 Subject: [PATCH 06/12] spmi: apple: use IRQ for RX FIFO if possible The IRQ fires as soon as the reply is available, which is usually takes a few us instead of the 10ms sleep interval for polling Signed-off-by: Alba Mendez Signed-off-by: Sasha Finkelstein --- drivers/spmi/spmi-apple-controller.c | 97 +++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index ad3d8af09d7445..6f4ddc5e602f1a 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -18,12 +18,20 @@ #include #include #include +#include +#include /* SPMI Controller Registers */ #define SPMI_STATUS_REG 0 #define SPMI_CMD_REG 0x4 #define SPMI_RSP_REG 0x8 +#define SPMI_IRQ_MASK_BASE 0x20 +#define SPMI_IRQ_ACK_BASE 0x60 +#define SPMI_IRQ_USER_SIZE 0x20 + +#define SPMI_IRQ_FIFO_RX 0 + /* SPMI_RSP_REG reply word */ #define SPMI_REPLY_FRAME_PARITY_OFFSET 16 #define SPMI_REPLY_ACK BIT(15) @@ -38,6 +46,8 @@ struct apple_spmi { void __iomem *regs; struct mutex fifo_lock; + bool fifo_rx_irq; + struct completion fifo_rx; }; #define poll_reg(spmi, reg, val, cond) \ @@ -56,7 +66,19 @@ static int apple_spmi_wait_rx_not_empty(struct spmi_controller *ctrl) int ret; u32 status; - ret = poll_reg(spmi, SPMI_STATUS_REG, status, !(status & SPMI_RX_FIFO_EMPTY)); + if (spmi->fifo_rx_irq) { + ret = wait_for_completion_timeout(&spmi->fifo_rx, + usecs_to_jiffies(REG_POLL_TIMEOUT_US)); + if (!ret) + ret = -ETIMEDOUT; + else if (readl(spmi->regs + SPMI_STATUS_REG) & SPMI_RX_FIFO_EMPTY) + ret = -EIO; + else + ret = 0; + } else { + ret = poll_reg(spmi, SPMI_STATUS_REG, status, !(status & SPMI_RX_FIFO_EMPTY)); + } + if (ret) { dev_err(&ctrl->dev, "failed to wait for RX FIFO not empty\n"); @@ -80,6 +102,8 @@ static int spmi_raw_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid, if (ret) return ret; + reinit_completion(&spmi->fifo_rx); + writel(spmi_cmd, spmi->regs + SPMI_CMD_REG); while (i < len) { @@ -184,10 +208,68 @@ static int spmi_cmd(struct spmi_controller *ctrl, u8 opc, u8 sid) return -EINVAL; } +static void apple_spmi_irq_ack_raw(struct apple_spmi *spmi, u32 irq) +{ + u32 __iomem *reg = spmi->regs + SPMI_IRQ_ACK_BASE + (irq / 32) * 4; + + writel(BIT(irq % 32), reg); +} + +static void apple_spmi_irq_mask_raw(struct apple_spmi *spmi, u32 irq) +{ + u32 __iomem *reg = spmi->regs + SPMI_IRQ_MASK_BASE + (irq / 32) * 4; + + writel(readl(reg) & ~BIT(irq % 32), reg); +} + +static void apple_spmi_irq_unmask_raw(struct apple_spmi *spmi, u32 irq) +{ + u32 __iomem *reg = spmi->regs + SPMI_IRQ_MASK_BASE + (irq / 32) * 4; + + writel(readl(reg) | BIT(irq % 32), reg); +} + +static irqreturn_t apple_spmi_irq_handler(int irq, void *dev_id) +{ + struct apple_spmi *spmi = dev_id; + bool handled = false; + u32 val, offset, bit; + + val = readl(spmi->regs + SPMI_IRQ_ACK_BASE + SPMI_IRQ_USER_SIZE); + if (val & BIT(SPMI_IRQ_FIFO_RX)) { + complete(&spmi->fifo_rx); + apple_spmi_irq_ack_raw(spmi, SPMI_IRQ_USER_SIZE * 8 + SPMI_IRQ_FIFO_RX); + handled = true; + } + + return handled ? IRQ_HANDLED : IRQ_NONE; +} + +static int apple_spmi_init_irq(struct platform_device *pdev, + struct apple_spmi *spmi, int irq) +{ + int ret; + + for (size_t offset = 0; offset < SPMI_IRQ_USER_SIZE + 4; offset += 4) { + writel(0, spmi->regs + SPMI_IRQ_MASK_BASE + offset); + writel(U32_MAX, spmi->regs + SPMI_IRQ_ACK_BASE + offset); + } + + spmi->fifo_rx_irq = true; + apple_spmi_irq_unmask_raw(spmi, SPMI_IRQ_USER_SIZE * 8 + SPMI_IRQ_FIFO_RX); + + ret = devm_request_any_context_irq(&pdev->dev, irq, apple_spmi_irq_handler, 0, NULL, spmi); + if (ret < 0) + return dev_err_probe(&pdev->dev, ret, "failed to request IRQ\n"); + + return 0; +} + static int apple_spmi_probe(struct platform_device *pdev) { struct apple_spmi *spmi; struct spmi_controller *ctrl; + int irq; int ret; ctrl = devm_spmi_controller_alloc(&pdev->dev, sizeof(*spmi)); @@ -196,6 +278,7 @@ static int apple_spmi_probe(struct platform_device *pdev) spmi = spmi_controller_get_drvdata(ctrl); mutex_init(&spmi->fifo_lock); + init_completion(&spmi->fifo_rx); spmi->regs = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(spmi->regs)) @@ -207,11 +290,23 @@ static int apple_spmi_probe(struct platform_device *pdev) ctrl->write_cmd = spmi_write_cmd; ctrl->cmd = spmi_cmd; + irq = platform_get_irq_optional(pdev, 0); + if (irq < 0 && irq != -ENXIO) + return irq; + if (irq >= 0) { + ret = apple_spmi_init_irq(pdev, spmi, irq); + if (ret) + return ret; + } + ret = devm_spmi_controller_add(&pdev->dev, ctrl); if (ret) return dev_err_probe(&pdev->dev, ret, "spmi_controller_add failed\n"); + dev_info(&pdev->dev, irq >= 0 ? "Initialized with IRQ" : + "Initialized without IRQ, falling back to polling"); + return 0; } From 2f282ea5c1b712b44967e8e7e38bee4adbafc3ee Mon Sep 17 00:00:00 2001 From: Alba Mendez Date: Sun, 21 Sep 2025 17:49:28 +0200 Subject: [PATCH 07/12] spmi: apple: interrupt controller functionality Add support for interrupts sent by slave devices. Signed-off-by: Alba Mendez Signed-off-by: Sasha Finkelstein --- drivers/spmi/Kconfig | 1 + drivers/spmi/spmi-apple-controller.c | 149 ++++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/drivers/spmi/Kconfig b/drivers/spmi/Kconfig index a80cf4047b86e3..c9eff3d470a104 100644 --- a/drivers/spmi/Kconfig +++ b/drivers/spmi/Kconfig @@ -13,6 +13,7 @@ if SPMI config SPMI_APPLE tristate "Apple SoC SPMI Controller platform driver" + select IRQ_DOMAIN_HIERARCHY depends on ARCH_APPLE || COMPILE_TEST help If you say yes to this option, support will be included for the diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index 6f4ddc5e602f1a..3d35d858025a23 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -20,6 +20,9 @@ #include #include #include +#include +#include +#include /* SPMI Controller Registers */ #define SPMI_STATUS_REG 0 @@ -48,6 +51,9 @@ struct apple_spmi { struct mutex fifo_lock; bool fifo_rx_irq; struct completion fifo_rx; + struct irq_domain *irqd; + raw_spinlock_t irq_mask_lock; + u64 irq_mask_cache[SPMI_IRQ_USER_SIZE / sizeof(u64)]; }; #define poll_reg(spmi, reg, val, cond) \ @@ -229,11 +235,123 @@ static void apple_spmi_irq_unmask_raw(struct apple_spmi *spmi, u32 irq) writel(readl(reg) | BIT(irq % 32), reg); } +static void apple_spmi_irq_ack(struct irq_data *d) +{ + struct apple_spmi *spmi = irq_data_get_irq_chip_data(d); + + apple_spmi_irq_ack_raw(spmi, d->hwirq); +} + +static void apple_spmi_irq_mask(struct irq_data *d) +{ + struct apple_spmi *spmi = irq_data_get_irq_chip_data(d); + unsigned long flags; + + raw_spin_lock_irqsave(&spmi->irq_mask_lock, flags); + apple_spmi_irq_mask_raw(spmi, d->hwirq); + spmi->irq_mask_cache[d->hwirq / 64] &= ~BIT(d->hwirq % 64); + raw_spin_unlock_irqrestore(&spmi->irq_mask_lock, flags); +} + +static void apple_spmi_irq_unmask(struct irq_data *d) +{ + struct apple_spmi *spmi = irq_data_get_irq_chip_data(d); + unsigned long flags; + + raw_spin_lock_irqsave(&spmi->irq_mask_lock, flags); + apple_spmi_irq_unmask_raw(spmi, d->hwirq); + spmi->irq_mask_cache[d->hwirq / 64] |= BIT(d->hwirq % 64); + raw_spin_unlock_irqrestore(&spmi->irq_mask_lock, flags); +} + +static int apple_spmi_irq_set_type(struct irq_data *d, unsigned int type) +{ + /* all interrupts have MSI semantics */ + return type == IRQ_TYPE_EDGE_RISING ? 0 : -EINVAL; +} + +static struct irq_chip apple_spmi_irq_chip = { + .name = "apple_spmi", + .irq_mask = apple_spmi_irq_mask, + .irq_unmask = apple_spmi_irq_unmask, + .irq_ack = apple_spmi_irq_ack, + .irq_set_type = apple_spmi_irq_set_type, + .flags = IRQCHIP_ONESHOT_SAFE, +}; + +static int apple_spmi_irq_domain_map(struct irq_domain *irqd, + unsigned int irq, irq_hw_number_t hw) +{ + irq_domain_set_info(irqd, irq, hw, &apple_spmi_irq_chip, irqd->host_data, + handle_edge_irq, NULL, NULL); + return 0; +} + +static int apple_spmi_irq_domain_translate(struct irq_domain *irqd, + struct irq_fwspec *fwspec, + unsigned long *hwirq, + unsigned int *type) +{ + u32 *args = fwspec->param; + + if (fwspec->param_count != 2) + return -EINVAL; + + if (args[0] >= SPMI_IRQ_USER_SIZE * 8) + return -EINVAL; + *hwirq = args[0]; + *type = args[1] & IRQ_TYPE_SENSE_MASK; + return 0; +} + +static int apple_spmi_irq_domain_alloc(struct irq_domain *irqd, unsigned int virq, + unsigned int nr_irqs, void *arg) +{ + unsigned int type = IRQ_TYPE_NONE; + struct irq_fwspec *fwspec = arg; + irq_hw_number_t hwirq; + int i, ret; + + ret = apple_spmi_irq_domain_translate(irqd, fwspec, &hwirq, &type); + if (ret) + return ret; + + if (hwirq + nr_irqs > SPMI_IRQ_USER_SIZE * 8) + return -EINVAL; + + for (i = 0; i < nr_irqs; i++) { + ret = apple_spmi_irq_domain_map(irqd, virq + i, hwirq + i); + if (ret) + return ret; + } + + return 0; +} + +static void apple_spmi_irq_domain_free(struct irq_domain *irqd, unsigned int virq, + unsigned int nr_irqs) +{ + int i; + + for (i = 0; i < nr_irqs; i++) { + struct irq_data *d = irq_domain_get_irq_data(irqd, virq + i); + + irq_set_handler(virq + i, NULL); + irq_domain_reset_irq_data(d); + } +} + +static const struct irq_domain_ops apple_spmi_irq_domain_ops = { + .translate = apple_spmi_irq_domain_translate, + .alloc = apple_spmi_irq_domain_alloc, + .free = apple_spmi_irq_domain_free, +}; + static irqreturn_t apple_spmi_irq_handler(int irq, void *dev_id) { struct apple_spmi *spmi = dev_id; bool handled = false; - u32 val, offset, bit; + u64 val, offset, bit; val = readl(spmi->regs + SPMI_IRQ_ACK_BASE + SPMI_IRQ_USER_SIZE); if (val & BIT(SPMI_IRQ_FIFO_RX)) { @@ -242,6 +360,23 @@ static irqreturn_t apple_spmi_irq_handler(int irq, void *dev_id) handled = true; } + for (offset = 0; offset < SPMI_IRQ_USER_SIZE; offset += sizeof(val)) { + val = readq(spmi->regs + SPMI_IRQ_ACK_BASE + offset); + /** + * because of other masters in the bus, we're going to get a multitude of + * interrupts we're not interested in. irq_resolve_mapping isn't very + * optimized for the nonexistent path, so instead we mask with (a locally + * cached version of) the IRQ mask + */ + val &= spmi->irq_mask_cache[offset / sizeof(val)]; + while (val) { + bit = __builtin_ctzll(val); + generic_handle_domain_irq(spmi->irqd, offset * 8 + bit); + handled = true; + val &= ~BIT(bit); + } + } + return handled ? IRQ_HANDLED : IRQ_NONE; } @@ -249,12 +384,24 @@ static int apple_spmi_init_irq(struct platform_device *pdev, struct apple_spmi *spmi, int irq) { int ret; + struct irq_domain_info info = { + .fwnode = pdev->dev.fwnode, + .hwirq_max = ~0U, + .ops = &apple_spmi_irq_domain_ops, + .host_data = spmi, + }; + + raw_spin_lock_init(&spmi->irq_mask_lock); for (size_t offset = 0; offset < SPMI_IRQ_USER_SIZE + 4; offset += 4) { writel(0, spmi->regs + SPMI_IRQ_MASK_BASE + offset); writel(U32_MAX, spmi->regs + SPMI_IRQ_ACK_BASE + offset); } + spmi->irqd = devm_irq_domain_instantiate(&pdev->dev, &info); + if (IS_ERR(spmi->irqd)) + return PTR_ERR(spmi->irqd); + spmi->fifo_rx_irq = true; apple_spmi_irq_unmask_raw(spmi, SPMI_IRQ_USER_SIZE * 8 + SPMI_IRQ_FIFO_RX); From 30c2f6fabb07ecf9a649663e09a2d80925ae356e Mon Sep 17 00:00:00 2001 From: Sasha Finkelstein Date: Tue, 21 Jul 2026 00:35:54 +0200 Subject: [PATCH 08/12] dts: apple: t603x: Add usb-related spmi controllers Signed-off-by: Sasha Finkelstein --- arch/arm64/boot/dts/apple/t6030.dtsi | 26 +++++++++++++++++++++ arch/arm64/boot/dts/apple/t6031-die0.dtsi | 28 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6030.dtsi b/arch/arm64/boot/dts/apple/t6030.dtsi index 2c2c388524317d..0bc566d99deef6 100644 --- a/arch/arm64/boot/dts/apple/t6030.dtsi +++ b/arch/arm64/boot/dts/apple/t6030.dtsi @@ -808,6 +808,8 @@ reg = <0x3 0x64714000 0x0 0x100>; #address-cells = <2>; #size-cells = <0>; + interrupt-parent = <&aic>; + interrupts = ; pmic1: pmic@b { compatible = "apple,chapel-pmic", "apple,spmi-nvmem"; @@ -860,6 +862,30 @@ }; }; + nub_spmi_a0: spmi@364908000 { + compatible = "apple,t6030-spmi", "apple,t8103-spmi"; + reg = <0x3 0x64908000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + #interrupt-cells = <2>; + interrupt-controller; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_nub_spmi_a0>; + }; + + nub_spmi_a1: spmi@364a08000 { + compatible = "apple,t6030-spmi", "apple,t8103-spmi"; + reg = <0x3 0x64a08000 0x0 0x100>; + #address-cells = <2>; + #size-cells = <0>; + #interrupt-cells = <2>; + interrupt-controller; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_nub_spmi_a1>; + }; + smc_mbox: mbox@36c408000 { compatible = "apple,t6030-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x3 0x6c408000 0x0 0x4000>; diff --git a/arch/arm64/boot/dts/apple/t6031-die0.dtsi b/arch/arm64/boot/dts/apple/t6031-die0.dtsi index 9591bace819be7..41abb97d7f9e3e 100644 --- a/arch/arm64/boot/dts/apple/t6031-die0.dtsi +++ b/arch/arm64/boot/dts/apple/t6031-die0.dtsi @@ -37,6 +37,8 @@ reg = <0x2 0xa1014000 0x0 0x100>; #address-cells = <2>; #size-cells = <0>; + interrupt-parent = <&aic>; + interrupts = ; pmic1: pmic@b { compatible = "apple,chapel-pmic", "apple,spmi-nvmem"; @@ -89,6 +91,32 @@ }; }; + nub_spmi_a0: spmi@2a1188000 { + compatible = "apple,t6031-spmi", "apple,t8103-spmi"; + #address-cells = <2>; + #size-cells = <0>; + #interrupt-cells = <2>; + interrupt-controller; + + reg = <0x2 0xa1188000 0x0 0x100>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_nub_spmi_a0>; + }; + + nub_spmi_a1: spmi@2a1198000 { + compatible = "apple,t6031-spmi", "apple,t8103-spmi"; + #address-cells = <2>; + #size-cells = <0>; + #interrupt-cells = <2>; + interrupt-controller; + + reg = <0x2 0xa1198000 0x0 0x100>; + interrupt-parent = <&aic>; + interrupts = ; + power-domains = <&ps_nub_spmi_a1>; + }; + smc_mbox: mbox@2a4408000 { compatible = "apple,t6031-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x2 0xa4408000 0x0 0x4000>; From 8307d3b5e67331978e46a5bfa0cefe6aa6cdf616 Mon Sep 17 00:00:00 2001 From: Sasha Finkelstein Date: Wed, 22 Jul 2026 19:46:16 +0200 Subject: [PATCH 09/12] dt-bindings: usb: tps6598x: Add sn201202x/ACE3 A variant of tps6598x that is attached to a SPMI bus and is found on Apple devices starting from the M3 series Signed-off-by: Sasha Finkelstein --- .../bindings/usb/apple,sn201202x.yaml | 78 +++++++++++++++++++ MAINTAINERS | 1 + 2 files changed, 79 insertions(+) create mode 100644 Documentation/devicetree/bindings/usb/apple,sn201202x.yaml diff --git a/Documentation/devicetree/bindings/usb/apple,sn201202x.yaml b/Documentation/devicetree/bindings/usb/apple,sn201202x.yaml new file mode 100644 index 00000000000000..84d230ccccc7f5 --- /dev/null +++ b/Documentation/devicetree/bindings/usb/apple,sn201202x.yaml @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/usb/apple,sn201202x.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Texas Instruments sn201202x Type-C Power Delivery controller + +maintainers: + - Sasha Finkelstein + +description: | + Texas Instruments sn201202x Type-C Power Delivery controller + + A variant of tps6598x controller also known as Apple ACE3 + present on hardware with Apple SoCs starting from the M3. + +properties: + compatible: + enum: + - apple,sn201202x + + reg: + minItems: 1 + + interrupts: + items: + - description: Primary irq used for tps6598x events + - description: Logical register selection completed + - description: Standby command completed + - description: Wakeup command completed + + interrupt-names: + items: + - const: irq + - const: select + - const: sleep + - const: wake + + connector: + $ref: /schemas/connector/usb-connector.yaml# + +required: + - compatible + - reg + - interrupts + - interrupt-names + +additionalProperties: false + +examples: + - | + #include + #include + spmi { + #address-cells = <2>; + #size-cells = <0>; + usb-pd@c { + compatible = "apple,sn201202x"; + reg = <0xc SPMI_USID>; + interrupts = <11 IRQ_TYPE_EDGE_RISING>, + <13 IRQ_TYPE_EDGE_RISING>, + <17 IRQ_TYPE_EDGE_RISING>, + <19 IRQ_TYPE_EDGE_RISING>; + interrupt-names = "irq", "select", "sleep", "wake"; + + connector { + compatible = "usb-c-connector"; + label = "USB-C"; + + port { + typec_ep: endpoint { + remote-endpoint = <&otg_ep>; + }; + }; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index cde843b4f4d966..fc0de1b96cb6ac 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2555,6 +2555,7 @@ F: Documentation/devicetree/bindings/rtc/apple,smc-rtc.yaml F: Documentation/devicetree/bindings/spi/apple,spi.yaml F: Documentation/devicetree/bindings/spmi/apple,spmi.yaml F: Documentation/devicetree/bindings/usb/apple,dwc3.yaml +F: Documentation/devicetree/bindings/usb/apple,sn201202x.yaml F: Documentation/devicetree/bindings/watchdog/apple,wdt.yaml F: Documentation/hwmon/macsmc-hwmon.rst F: arch/arm64/boot/dts/apple/ From cb9363ef13601b1882bfbb2ad2ff25bbf10b2380 Mon Sep 17 00:00:00 2001 From: Alyssa Milburn Date: Sat, 30 May 2026 16:28:22 +0200 Subject: [PATCH 10/12] usb: typec: tipd: Factor out i2c specifics Make the core driver more bus-agnostic to prepare for SPMI variants of the tipd chip Signed-off-by: Alyssa Milburn Signed-off-by: Sasha Finkelstein --- drivers/usb/typec/tipd/Makefile | 2 +- drivers/usb/typec/tipd/{core.c => core.h} | 109 ++++++---------------- drivers/usb/typec/tipd/i2c.c | 86 +++++++++++++++++ 3 files changed, 115 insertions(+), 82 deletions(-) rename drivers/usb/typec/tipd/{core.c => core.h} (95%) create mode 100644 drivers/usb/typec/tipd/i2c.c diff --git a/drivers/usb/typec/tipd/Makefile b/drivers/usb/typec/tipd/Makefile index aa439f80a889ec..ba38daee9e7251 100644 --- a/drivers/usb/typec/tipd/Makefile +++ b/drivers/usb/typec/tipd/Makefile @@ -2,5 +2,5 @@ CFLAGS_trace.o := -I$(src) obj-$(CONFIG_TYPEC_TPS6598X) += tps6598x.o -tps6598x-y := core.o +tps6598x-y := i2c.o tps6598x-$(CONFIG_TRACING) += trace.o diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.h similarity index 95% rename from drivers/usb/typec/tipd/core.c rename to drivers/usb/typec/tipd/core.h index d0b769333bd998..1ba29e439e01c0 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -167,6 +166,7 @@ struct tps6598x { struct device *dev; struct regmap *regmap; struct mutex lock; /* device lock */ + int irq; u8 i2c_protocol:1; struct gpio_desc *reset; @@ -231,6 +231,13 @@ static const char *tps6598x_psy_name_prefix = "tps6598x-source-psy-"; */ #define TPS_MAX_LEN 64 +static struct tps6598x *tps6598x_from_device(struct device *dev) +{ + struct i2c_client *client = i2c_verify_client(dev); + struct tps6598x *tps = i2c_get_clientdata(client); + return tps; +} + static int tps6598x_block_read(struct tps6598x *tps, u8 reg, void *val, size_t len) { @@ -1738,27 +1745,13 @@ static void cd321x_remove(struct tps6598x *tps) cancel_delayed_work_sync(&cd321x->update_work); } -static int tps6598x_probe(struct i2c_client *client) +static int tps6598x_probe(struct tps6598x *tps) { - const struct tipd_data *data; - struct tps6598x *tps; struct fwnode_handle *fwnode; u32 status; u32 vid; int ret; - data = i2c_get_match_data(client); - if (!data) - return -EINVAL; - - tps = devm_kzalloc(&client->dev, data->tps_struct_size, GFP_KERNEL); - if (!tps) - return -ENOMEM; - - mutex_init(&tps->lock); - tps->dev = &client->dev; - tps->data = data; - tps->reset = devm_gpiod_get_optional(tps->dev, "reset", GPIOD_OUT_LOW); if (IS_ERR(tps->reset)) return dev_err_probe(tps->dev, PTR_ERR(tps->reset), @@ -1766,23 +1759,12 @@ static int tps6598x_probe(struct i2c_client *client) if (tps->reset) msleep(TPS_SETUP_MS); - tps->regmap = devm_regmap_init_i2c(client, &tps6598x_regmap_config); - if (IS_ERR(tps->regmap)) - return PTR_ERR(tps->regmap); - if (!device_is_compatible(tps->dev, "ti,tps25750")) { ret = tps6598x_read32(tps, TPS_REG_VID, &vid); if (ret < 0 || !vid) return -ENODEV; } - /* - * Checking can the adapter handle SMBus protocol. If it can not, the - * driver needs to take care of block reads separately. - */ - if (i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) - tps->i2c_protocol = true; - if (tps->data->switch_power_state) { ret = tps->data->switch_power_state(tps, TPS_SYSTEM_POWER_STATE_S0); if (ret) @@ -1816,7 +1798,7 @@ static int tps6598x_probe(struct i2c_client *client) * with existing DT files, we work around this by deleting any * fwnode_links to/from this fwnode. */ - fwnode = device_get_named_child_node(&client->dev, "connector"); + fwnode = device_get_named_child_node(tps->dev, "connector"); if (fwnode) fw_devlink_purge_absent_suppliers(fwnode); @@ -1842,14 +1824,14 @@ static int tps6598x_probe(struct i2c_client *client) goto err_unregister_port; ret = tps->data->connect(tps, status); if (ret) - dev_err(&client->dev, "failed to register partner\n"); + dev_err(tps->dev, "failed to register partner\n"); } - if (client->irq) { - ret = devm_request_threaded_irq(&client->dev, client->irq, NULL, + if (tps->irq) { + ret = devm_request_threaded_irq(tps->dev, tps->irq, NULL, tps->data->irq_handler, IRQF_SHARED | IRQF_ONESHOT, - dev_name(&client->dev), tps); + dev_name(tps->dev), tps); } else { dev_warn(tps->dev, "Unable to find the interrupt, switching to polling\n"); INIT_DELAYED_WORK(&tps->wq_poll, tps6598x_poll_work); @@ -1860,13 +1842,12 @@ static int tps6598x_probe(struct i2c_client *client) if (ret) goto err_disconnect; - i2c_set_clientdata(client, tps); fwnode_handle_put(fwnode); tps->wakeup = device_property_read_bool(tps->dev, "wakeup-source"); - if (tps->wakeup && client->irq) { - devm_device_init_wakeup(&client->dev); - enable_irq_wake(client->irq); + if (tps->wakeup && tps->irq) { + devm_device_init_wakeup(tps->dev); + enable_irq_wake(tps->irq); } return 0; @@ -1888,14 +1869,12 @@ static int tps6598x_probe(struct i2c_client *client) return ret; } -static void tps6598x_remove(struct i2c_client *client) +static void tps6598x_remove(struct tps6598x *tps) { - struct tps6598x *tps = i2c_get_clientdata(client); - - if (!client->irq) + if (!tps->irq) cancel_delayed_work_sync(&tps->wq_poll); else - devm_free_irq(tps->dev, client->irq, tps); + devm_free_irq(tps->dev, tps->irq, tps); if (tps->data->remove) tps->data->remove(tps); @@ -1913,17 +1892,16 @@ static void tps6598x_remove(struct i2c_client *client) static int __maybe_unused tps6598x_suspend(struct device *dev) { - struct i2c_client *client = to_i2c_client(dev); - struct tps6598x *tps = i2c_get_clientdata(client); + struct tps6598x *tps = tps6598x_from_device(dev); if (tps->wakeup) { - disable_irq(client->irq); - enable_irq_wake(client->irq); + disable_irq(tps->irq); + enable_irq_wake(tps->irq); } else if (tps->reset) { gpiod_set_value_cansleep(tps->reset, 1); } - if (!client->irq) + if (!tps->irq) cancel_delayed_work_sync(&tps->wq_poll); return 0; @@ -1931,8 +1909,7 @@ static int __maybe_unused tps6598x_suspend(struct device *dev) static int __maybe_unused tps6598x_resume(struct device *dev) { - struct i2c_client *client = to_i2c_client(dev); - struct tps6598x *tps = i2c_get_clientdata(client); + struct tps6598x *tps = tps6598x_from_device(dev); int ret; ret = tps6598x_check_mode(tps); @@ -1946,14 +1923,14 @@ static int __maybe_unused tps6598x_resume(struct device *dev) } if (tps->wakeup) { - disable_irq_wake(client->irq); - enable_irq(client->irq); + disable_irq_wake(tps->irq); + enable_irq(tps->irq); } else if (tps->reset) { gpiod_set_value_cansleep(tps->reset, 0); msleep(TPS_SETUP_MS); } - if (!client->irq) + if (!tps->irq) queue_delayed_work(system_power_efficient_wq, &tps->wq_poll, msecs_to_jiffies(POLL_INTERVAL)); @@ -2018,33 +1995,3 @@ static const struct tipd_data tps25750_data = { .reset = tps25750_reset, .connect = tps6598x_connect, }; - -static const struct of_device_id tps6598x_of_match[] = { - { .compatible = "ti,tps6598x", &tps6598x_data}, - { .compatible = "apple,cd321x", &cd321x_data}, - { .compatible = "ti,tps25750", &tps25750_data}, - {} -}; -MODULE_DEVICE_TABLE(of, tps6598x_of_match); - -static const struct i2c_device_id tps6598x_id[] = { - { "tps6598x", (kernel_ulong_t)&tps6598x_data }, - { } -}; -MODULE_DEVICE_TABLE(i2c, tps6598x_id); - -static struct i2c_driver tps6598x_i2c_driver = { - .driver = { - .name = "tps6598x", - .pm = &tps6598x_pm_ops, - .of_match_table = tps6598x_of_match, - }, - .probe = tps6598x_probe, - .remove = tps6598x_remove, - .id_table = tps6598x_id, -}; -module_i2c_driver(tps6598x_i2c_driver); - -MODULE_AUTHOR("Heikki Krogerus "); -MODULE_LICENSE("GPL v2"); -MODULE_DESCRIPTION("TI TPS6598x USB Power Delivery Controller Driver"); diff --git a/drivers/usb/typec/tipd/i2c.c b/drivers/usb/typec/tipd/i2c.c new file mode 100644 index 00000000000000..6b244c5f84a6fc --- /dev/null +++ b/drivers/usb/typec/tipd/i2c.c @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for TI TPS6598x USB Power Delivery controller family + * + * Copyright (C) 2017, Intel Corporation + * Author: Heikki Krogerus + */ + +#include + +#include "core.h" + +static int tps6598x_probe_i2c(struct i2c_client *client) +{ + const struct tipd_data *data; + struct tps6598x *tps; + int ret; + + data = i2c_get_match_data(client); + if (!data) + return -EINVAL; + + tps = devm_kzalloc(&client->dev, data->tps_struct_size, GFP_KERNEL); + if (!tps) + return -ENOMEM; + + mutex_init(&tps->lock); + tps->dev = &client->dev; + tps->data = data; + tps->irq = client->irq; + + tps->regmap = devm_regmap_init_i2c(client, &tps6598x_regmap_config); + if (IS_ERR(tps->regmap)) + return PTR_ERR(tps->regmap); + + /* + * Checking can the adapter handle SMBus protocol. If it can not, the + * driver needs to take care of block reads separately. + */ + if (i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) + tps->i2c_protocol = true; + + ret = tps6598x_probe(tps); + + if (ret == 0) + i2c_set_clientdata(client, tps); + + return ret; +} + +static void tps6598x_remove_i2c(struct i2c_client *client) +{ + struct tps6598x *tps = i2c_get_clientdata(client); + + tps6598x_remove(tps); +} + +static const struct of_device_id tps6598x_of_match[] = { + { .compatible = "ti,tps6598x", &tps6598x_data}, + { .compatible = "apple,cd321x", &cd321x_data}, + { .compatible = "ti,tps25750", &tps25750_data}, + {} +}; +MODULE_DEVICE_TABLE(of, tps6598x_of_match); + +static const struct i2c_device_id tps6598x_id[] = { + { "tps6598x", (kernel_ulong_t)&tps6598x_data }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tps6598x_id); + +static struct i2c_driver tps6598x_i2c_driver = { + .driver = { + .name = "tps6598x", + .pm = &tps6598x_pm_ops, + .of_match_table = tps6598x_of_match, + }, + .probe = tps6598x_probe_i2c, + .remove = tps6598x_remove_i2c, + .id_table = tps6598x_id, +}; +module_i2c_driver(tps6598x_i2c_driver); + +MODULE_AUTHOR("Heikki Krogerus "); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("TI TPS6598x USB Power Delivery Controller Driver"); From e983e3bcf0fd2867c7a945b7c1950a9a1c293896 Mon Sep 17 00:00:00 2001 From: Sasha Finkelstein Date: Wed, 22 Jul 2026 18:51:20 +0200 Subject: [PATCH 11/12] usb: typec: tipd: Add sn201202x support Add support for sn201202x (aka ACE3), a tipd variant that uses a very similar register map, that is exposed over a "logical register" interface on the SPMI bus. Co-developed-by: Alyssa Milburn Signed-off-by: Alyssa Milburn Signed-off-by: Sasha Finkelstein --- MAINTAINERS | 1 + drivers/usb/typec/tipd/Kconfig | 14 ++ drivers/usb/typec/tipd/Makefile | 12 +- drivers/usb/typec/tipd/core.h | 14 +- drivers/usb/typec/tipd/spmi.c | 297 ++++++++++++++++++++++++++++++++ 5 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 drivers/usb/typec/tipd/spmi.c diff --git a/MAINTAINERS b/MAINTAINERS index fc0de1b96cb6ac..6aae4c2c46f332 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2587,6 +2587,7 @@ F: drivers/soc/apple/* F: drivers/spi/spi-apple.c F: drivers/spmi/spmi-apple-controller.c F: drivers/usb/dwc3/dwc3-apple.c +F: drivers/usb/typec/tipd/spmi.c F: drivers/video/backlight/apple_dwi_bl.c F: drivers/watchdog/apple_wdt.c F: include/dt-bindings/interrupt-controller/apple-aic.h diff --git a/drivers/usb/typec/tipd/Kconfig b/drivers/usb/typec/tipd/Kconfig index b8271529307286..6aa2fa439fddcd 100644 --- a/drivers/usb/typec/tipd/Kconfig +++ b/drivers/usb/typec/tipd/Kconfig @@ -10,3 +10,17 @@ config TYPEC_TPS6598X If you choose to build this driver as a dynamically linked module, the module will be called tps6598x.ko. + +config TYPEC_SN201202X + tristate "TI SN201202x USB Power Delivery controller support" + select POWER_SUPPLY + select REGMAP_I2C + select USB_ROLE_SWITCH + select REGMAP_SPMI + help + Say Y here to enable support for SN201202x, a TPS6598x variant + that uses SPMI as a host bus, and is found on Apple devices starting + from the M3 series. + + If you choose to build this driver as a dynamically linked module, the + module will be called sn201202x.ko. diff --git a/drivers/usb/typec/tipd/Makefile b/drivers/usb/typec/tipd/Makefile index ba38daee9e7251..9a547bb14831b0 100644 --- a/drivers/usb/typec/tipd/Makefile +++ b/drivers/usb/typec/tipd/Makefile @@ -1,6 +1,10 @@ # SPDX-License-Identifier: GPL-2.0 -CFLAGS_trace.o := -I$(src) +CFLAGS_trace.o := -I$(src) -obj-$(CONFIG_TYPEC_TPS6598X) += tps6598x.o -tps6598x-y := i2c.o -tps6598x-$(CONFIG_TRACING) += trace.o +obj-$(CONFIG_TYPEC_TPS6598X) += tps6598x.o +tps6598x-y := i2c.o +tps6598x-$(CONFIG_TRACING) += trace.o + +obj-$(CONFIG_TYPEC_SN201202X) += sn201202x.o +sn201202x-y := spmi.o +sn201202x-$(CONFIG_TRACING) += trace.o diff --git a/drivers/usb/typec/tipd/core.h b/drivers/usb/typec/tipd/core.h index 1ba29e439e01c0..fed326a9788d73 100644 --- a/drivers/usb/typec/tipd/core.h +++ b/drivers/usb/typec/tipd/core.h @@ -25,6 +25,10 @@ #include "tps6598x.h" #include "trace.h" +#ifdef CONFIG_TYPEC_SN201202X +#include +#endif + /* Register offsets */ #define TPS_REG_VID 0x00 #define TPS_REG_MODE 0x03 @@ -234,8 +238,14 @@ static const char *tps6598x_psy_name_prefix = "tps6598x-source-psy-"; static struct tps6598x *tps6598x_from_device(struct device *dev) { struct i2c_client *client = i2c_verify_client(dev); - struct tps6598x *tps = i2c_get_clientdata(client); - return tps; +#ifdef CONFIG_TYPEC_SN201202X + if (!client) { + struct spmi_device *device = to_spmi_device(dev); + + return spmi_device_get_drvdata(device); + } +#endif + return i2c_get_clientdata(client); } static int diff --git a/drivers/usb/typec/tipd/spmi.c b/drivers/usb/typec/tipd/spmi.c new file mode 100644 index 00000000000000..f94453131ea294 --- /dev/null +++ b/drivers/usb/typec/tipd/spmi.c @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include + +#include "core.h" + +struct sn201202x { + struct cd321x cd; + struct completion select_completion; + struct completion sleep_completion; + struct completion wake_completion; + struct spmi_device *sdev; +}; + +#define tps_to_sn(tps) container_of_const((tps), struct sn201202x, cd.tps) + +static int regmap_sn201202x_select_reg(struct spmi_device *sdev, u8 reg) +{ + int err; + u8 val; + bool warned = false; + struct tps6598x *tps = spmi_device_get_drvdata(sdev); + struct sn201202x *sn = tps_to_sn(tps); + + reinit_completion(&sn->select_completion); + err = spmi_register_zero_write(sdev, reg); + if (err) + return err; + + if (!wait_for_completion_timeout(&sn->select_completion, msecs_to_jiffies(100))) + return -ETIMEDOUT; + + while (1) { + err = spmi_register_read(sdev, 0, &val); + if (err) + return err; + if (val == (reg | 0x80)) { + if (!warned) { + dev_warn(tps->dev, + "Got interrupt but selection not complete?\n"); + warned = true; + } + msleep(20); + continue; + } + if (val == reg) + break; + return -EIO; + } + + return 0; +} + +static int regmap_sn201202x_read(void *context, + const void *reg, size_t reg_size, + void *val, size_t val_size) +{ + int err; + unsigned int offset = 0x20; + size_t len; + u8 addr; + + WARN_ON(reg_size != 1); + WARN_ON(val_size > 0x40); + + addr = *(u8 *)reg; + + err = regmap_sn201202x_select_reg(context, addr); + if (err) + return err; + + while (val_size) { + len = min_t(size_t, val_size, 16); + err = spmi_ext_register_read(context, offset, val, len); + if (err) + return err; + offset += len; + val += len; + val_size -= len; + } + + return 0; +} + +static int regmap_sn201202x_write(void *context, const void *data, + size_t count) +{ + int err = 0; + unsigned int offset = 0xa0; + size_t len; + u8 addr; + + WARN_ON(count < 1); + + addr = *(u8 *)data; + data += 1; + count -= 1; + + WARN_ON(count > 0x40); + + err = regmap_sn201202x_select_reg(context, addr); + if (err) + return err; + + while (count) { + len = min_t(size_t, count, 16); + err = spmi_ext_register_write(context, offset, data, len); + if (err) + return err; + offset += len; + data += len; + count -= len; + } + + return err; +} + +static irqreturn_t sn201202x_irq(int irq, void *data) +{ + struct completion *c = data; + + complete(c); + return IRQ_HANDLED; +} + +static const struct regmap_bus regmap_sn201202x = { + .read = regmap_sn201202x_read, + .write = regmap_sn201202x_write, + .reg_format_endian_default = REGMAP_ENDIAN_NATIVE, + .val_format_endian_default = REGMAP_ENDIAN_NATIVE, +}; + +static struct regmap *__devm_regmap_init_sn201202x(struct spmi_device *sdev, + const struct regmap_config *config, + struct lock_class_key *lock_key, + const char *lock_name) +{ + return __devm_regmap_init(&sdev->dev, ®map_sn201202x, sdev, config, + lock_key, lock_name); +} + +#define devm_regmap_init_sn201202x(dev, config) \ + __regmap_lockdep_wrapper(__devm_regmap_init_sn201202x, #config, \ + dev, config) + +static const struct tipd_data sn201202x_data = { + .irq_handler = cd321x_interrupt, + .irq_mask1 = APPLE_CD_REG_INT_POWER_STATUS_UPDATE | + APPLE_CD_REG_INT_DATA_STATUS_UPDATE | + APPLE_CD_REG_INT_PLUG_EVENT, + .tps_struct_size = sizeof(struct sn201202x), + .remove = cd321x_remove, + .register_port = cd321x_register_port, + .unregister_port = cd321x_unregister_port, + .trace_data_status = trace_cd321x_data_status, + .trace_power_status = trace_tps6598x_power_status, + .trace_status = trace_tps6598x_status, + .init = cd321x_init, + .read_data_status = cd321x_read_data_status, + .reset = cd321x_reset, + .switch_power_state = cd321x_switch_power_state, + .connect = cd321x_connect, +}; + +static const struct of_device_id sn201202x_of_match[] = { + { .compatible = "apple,sn201202x", &sn201202x_data}, + {} +}; + +static int sn201202x_probe(struct spmi_device *device) +{ + const struct of_device_id *match; + const struct tipd_data *data; + struct sn201202x *sn; + struct tps6598x *tps; + int irq_select, irq_sleep, irq_wake; + int ret; + + match = of_match_device(sn201202x_of_match, &device->dev); + if (!match) + return -EINVAL; + data = match->data; + + sn = devm_kzalloc(&device->dev, data->tps_struct_size, GFP_KERNEL); + if (!sn) + return -ENOMEM; + sn->sdev = device; + tps = &sn->cd.tps; + + mutex_init(&tps->lock); + tps->dev = &device->dev; + tps->data = data; + + tps->irq = of_irq_get_byname(device->dev.of_node, "irq"); + if (tps->irq < 0) + return tps->irq; + irq_select = of_irq_get_byname(device->dev.of_node, "select"); + if (irq_select < 0) + return irq_select; + irq_sleep = of_irq_get_byname(device->dev.of_node, "sleep"); + if (irq_sleep < 0) + return irq_sleep; + irq_wake = of_irq_get_byname(device->dev.of_node, "wake"); + if (irq_wake < 0) + return irq_wake; + + init_completion(&sn->select_completion); + init_completion(&sn->sleep_completion); + init_completion(&sn->wake_completion); + + ret = devm_request_threaded_irq(&device->dev, irq_select, NULL, sn201202x_irq, + IRQF_ONESHOT, NULL, &sn->select_completion); + if (ret) + return ret; + ret = devm_request_threaded_irq(&device->dev, irq_sleep, NULL, sn201202x_irq, + IRQF_ONESHOT, NULL, &sn->sleep_completion); + if (ret) + return ret; + ret = devm_request_threaded_irq(&device->dev, irq_wake, NULL, sn201202x_irq, + IRQF_ONESHOT, NULL, &sn->wake_completion); + if (ret) + return ret; + + tps->regmap = devm_regmap_init_sn201202x(device, &tps6598x_regmap_config); + if (IS_ERR(tps->regmap)) + return PTR_ERR(tps->regmap); + + ret = spmi_command_wakeup(device); + if (ret) + return ret; + if (!wait_for_completion_timeout(&sn->wake_completion, msecs_to_jiffies(100))) + return -ETIMEDOUT; + + spmi_device_set_drvdata(device, tps); + return tps6598x_probe(tps); +} + +static void sn201202x_remove(struct spmi_device *device) +{ + struct tps6598x *tps = spmi_device_get_drvdata(device); + + tps6598x_remove(tps); +} + +static int __maybe_unused sn201202x_resume(struct device *dev) +{ + struct tps6598x *tps = dev_get_drvdata(dev); + struct sn201202x *sn = tps_to_sn(tps); + int err; + + err = spmi_command_wakeup(sn->sdev); + if (err) + return err; + if (!wait_for_completion_timeout(&sn->wake_completion, msecs_to_jiffies(100))) + return -ETIMEDOUT; + return tps6598x_resume(dev); +} + +static int __maybe_unused sn201202x_suspend(struct device *dev) +{ + struct tps6598x *tps = dev_get_drvdata(dev); + struct sn201202x *sn = tps_to_sn(tps); + int err; + + err = tps6598x_suspend(dev); + if (err) + return err; + err = spmi_command_sleep(sn->sdev); + if (err) + return err; + if (!wait_for_completion_timeout(&sn->sleep_completion, msecs_to_jiffies(100))) + return -ETIMEDOUT; + return 0; +} + +MODULE_DEVICE_TABLE(of, sn201202x_of_match); + +static const struct dev_pm_ops sn201202x_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(sn201202x_suspend, sn201202x_resume) +}; + +static struct spmi_driver sn201202x_driver = { + .driver = { + .name = "sn201202x", + .pm = &sn201202x_pm_ops, + .of_match_table = sn201202x_of_match, + }, + .probe = sn201202x_probe, + .remove = sn201202x_remove, +}; +module_spmi_driver(sn201202x_driver); + +MODULE_AUTHOR("Heikki Krogerus "); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("TI SN201202x USB Power Delivery Controller Driver"); From 7eeab09175074656bdee26971d84795e8676c7bb Mon Sep 17 00:00:00 2001 From: Sasha Finkelstein Date: Wed, 22 Jul 2026 20:31:45 +0200 Subject: [PATCH 12/12] dts: apple: t603x: Add ACE3 pd controllers Signed-off-by: Sasha Finkelstein --- .../arm64/boot/dts/apple/t603x-j514-j516.dtsi | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t603x-j514-j516.dtsi b/arch/arm64/boot/dts/apple/t603x-j514-j516.dtsi index f868eeb8ace8d9..b2ff8864b323e3 100644 --- a/arch/arm64/boot/dts/apple/t603x-j514-j516.dtsi +++ b/arch/arm64/boot/dts/apple/t603x-j514-j516.dtsi @@ -288,6 +288,50 @@ }; }; +&nub_spmi_a0 { + hpm0: usb-pd@c { + compatible = "apple,sn201202x"; + reg = <0xc SPMI_USID>; + interrupts = <11 IRQ_TYPE_EDGE_RISING>, + <13 IRQ_TYPE_EDGE_RISING>, + <17 IRQ_TYPE_EDGE_RISING>, + <19 IRQ_TYPE_EDGE_RISING>; + interrupt-names = "irq", "select", "sleep", "wake"; + }; + + hpm1: usb-pd@a { + compatible = "apple,sn201202x"; + reg = <0xa SPMI_USID>; + interrupts = <37 IRQ_TYPE_EDGE_RISING>, + <39 IRQ_TYPE_EDGE_RISING>, + <43 IRQ_TYPE_EDGE_RISING>, + <45 IRQ_TYPE_EDGE_RISING>; + interrupt-names = "irq", "select", "sleep", "wake"; + }; + + hpm5: usb-pd@6 { + compatible = "apple,sn201202x"; + reg = <0x6 SPMI_USID>; + interrupts = <89 IRQ_TYPE_EDGE_RISING>, + <91 IRQ_TYPE_EDGE_RISING>, + <95 IRQ_TYPE_EDGE_RISING>, + <97 IRQ_TYPE_EDGE_RISING>; + interrupt-names = "irq", "select", "sleep", "wake"; + }; +}; + +&nub_spmi_a1 { + hpm2: usb-pd@8 { + compatible = "apple,sn201202x"; + reg = <0x8 SPMI_USID>; + interrupts = <63 IRQ_TYPE_EDGE_RISING>, + <65 IRQ_TYPE_EDGE_RISING>, + <69 IRQ_TYPE_EDGE_RISING>, + <71 IRQ_TYPE_EDGE_RISING>; + interrupt-names = "irq", "select", "sleep", "wake"; + }; +}; + #ifndef NO_ISP_YET &isp { apple,platform-id = <9>;