From e246d5f233d820c34ca39c0331eeb69031ce0bf8 Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sat, 10 Jan 2026 21:56:49 +1100 Subject: [PATCH 1/9] dt-bindings: soc: apple: Add Dockchannel and MTP bindings This adds device tree bindings for the DockChannel, DockChannel HID and MTP nodes required to enable internal keyboard/trackpad support on M2 and later Apple Silicon laptops. Signed-off-by: Michael Reeves --- .../bindings/soc/apple/apple,dockchannel.yaml | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 Documentation/devicetree/bindings/soc/apple/apple,dockchannel.yaml diff --git a/Documentation/devicetree/bindings/soc/apple/apple,dockchannel.yaml b/Documentation/devicetree/bindings/soc/apple/apple,dockchannel.yaml new file mode 100644 index 00000000000000..f345183ca4f2b8 --- /dev/null +++ b/Documentation/devicetree/bindings/soc/apple/apple,dockchannel.yaml @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +# Copyright (c) 2025 The Asahi Linux contributors +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/soc/apple/apple,dockchannel.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple DockChannel FIFO + +maintainers: + - Michael Reeves + +description: | + DockChannel is a hardware FIFO and interrupt controller used on Apple SoCs + for low-latency communication with co-processors. + +properties: + compatible: + oneOf: + - items: + - const: apple,t8112-dockchannel + - items: + - const: apple,t6020-dockchannel + - const: apple,t8112-dockchannel + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + interrupt-controller: true + + "#interrupt-cells": + const: 2 + + ranges: true + + nonposted-mmio: true + + "#address-cells": + const: 2 + + "#size-cells": + const: 2 + +patternProperties: + "^input@[0-9a-f]+$": + $ref: /schemas/input/apple,dockchannel-hid.yaml + +required: + - compatible + - reg + - interrupts + - interrupt-controller + - "#interrupt-cells" + - ranges + - "#address-cells" + - "#size-cells" + +additionalProperties: false + +examples: + - | + #include + #include + + soc { + #address-cells = <2>; + #size-cells = <2>; + + fifo@2a9b14000 { + compatible = "apple,t6020-dockchannel", "apple,t8112-dockchannel"; + reg = <0x2 0xa9b14000 0x0 0x4000>; + + interrupt-parent = <&aic>; + interrupts = ; + + ranges; + #address-cells = <2>; + #size-cells = <2>; + + interrupt-controller; + #interrupt-cells = <2>; + }; + }; From 59116edf18bdb8f73066ff6a815d4e46660f31cc Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sat, 10 Jan 2026 21:58:19 +1100 Subject: [PATCH 2/9] dt-bindings: input: apple: Add Dockchannel HID bindings This adds device tree bindings for the DockChannel HID node required to enable internal keyboard/trackpad support on Apple Silicon M2 and later laptops. Signed-off-by: Michael Reeves --- .../bindings/input/apple,dockchannel-hid.yaml | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml diff --git a/Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml b/Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml new file mode 100644 index 00000000000000..bc7972ff6d74bd --- /dev/null +++ b/Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/input/apple,dockchannel-hid.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple DockChannel HID Transport + +maintainers: + - Michael Reeves + +description: | + Transport layer for HID devices (keyboard, trackpad) connected via the + DockChannel FIFO interface on Apple Silicon SoCs. Contains a minimal + coprocessor called the MTP (Multi-Touch Coprocessor) which assists with + this and must be booted for it to work. + +properties: + compatible: + oneOf: + - items: + - const: apple,t8112-dockchannel-hid + - items: + - const: apple,t6020-dockchannel-hid + - const: apple,t8112-dockchannel-hid + + reg: + items: + - description: Config interface registers + - description: Data interface registers + - description: Coprocessor ASC registers + - description: Coprocessor SRAM/mailbox registers + + reg-names: + items: + - const: config + - const: data + - const: coproc-asc + - const: coproc-sram + + interrupts: + items: + - description: TX interrupt + - description: RX interrupt + + interrupt-names: + items: + - const: tx + - const: rx + + mboxes: + maxItems: 1 + + iommus: + maxItems: 1 + + keyboard: + type: object + description: | + Optional properties for the internal keyboard. + properties: + hid-country-code: + $ref: /schemas/types.yaml#/definitions/uint32 + description: The USB HID country code for the keyboard layout. + additionalProperties: false + +required: + - compatible + - reg + - reg-names + - interrupts + - interrupt-names + - mboxes + - iommus + +additionalProperties: false + +examples: + - | + #include + + soc { + #address-cells = <2>; + #size-cells = <2>; + + input@8000 { + compatible = "apple,t8112-dockchannel-hid"; + reg = <0x2 0xfab30000 0x0 0x4000>, + <0x2 0xfab34000 0x0 0x4000>, + <0x2 0xfa400000 0x0 0x4000>, + <0x2 0xfa050000 0x0 0x100000>; + reg-names = "config", "data", "coproc-asc", "coproc-sram"; + mboxes = <&mtp_mbox>; + iommus = <&mtp_dart 1>; + interrupts = <2 IRQ_TYPE_LEVEL_HIGH>, <3 IRQ_TYPE_LEVEL_HIGH>; + interrupt-names = "tx", "rx"; + + keyboard { + hid-country-code = <0>; + }; + }; + }; From 5faeda0d6702279a1ebd24ff070417cc5b78f946 Mon Sep 17 00:00:00 2001 From: Hector Martin Date: Sat, 10 Jan 2026 21:58:50 +1100 Subject: [PATCH 3/9] soc: apple: Add DockChannel driver This patch adds a driver for Apple's DockChannel protocol which is a hardware FIFO found on Apple Silicon devices. Notably, it used to enable communication with the internal trackpad and keyboard on MacBooks with an M2 and later chipset. Signed-off-by: Hector Martin Co-developed-by: Michael Reeves Signed-off-by: Michael Reeves --- drivers/soc/apple/Kconfig | 11 + drivers/soc/apple/Makefile | 3 + drivers/soc/apple/dockchannel.c | 522 ++++++++++++++++++++++++++ include/linux/soc/apple/dockchannel.h | 53 +++ 4 files changed, 589 insertions(+) create mode 100644 drivers/soc/apple/dockchannel.c create mode 100644 include/linux/soc/apple/dockchannel.h diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig index ad67368892311b..77ccb50412bb98 100644 --- a/drivers/soc/apple/Kconfig +++ b/drivers/soc/apple/Kconfig @@ -4,6 +4,17 @@ if ARCH_APPLE || COMPILE_TEST menu "Apple SoC drivers" +config APPLE_DOCKCHANNEL + tristate "Apple DockChannel FIFO" + depends on ARCH_APPLE || COMPILE_TEST + help + DockChannel is a hardware FIFO used on Apple Silicon SoCs for + communication between the application processor and various + coprocessors. It is the primary transport for the internal + keyboard and trackpad on M2 and later MacBook models. + + Say Y here if you have an M2 or later Apple MacBook. + config APPLE_MAILBOX tristate "Apple SoC mailboxes" depends on PM diff --git a/drivers/soc/apple/Makefile b/drivers/soc/apple/Makefile index 4d9ab8f3037b71..0b6a9f92bbbbf8 100644 --- a/drivers/soc/apple/Makefile +++ b/drivers/soc/apple/Makefile @@ -1,5 +1,8 @@ # SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_APPLE_DOCKCHANNEL) += apple-dockchannel.o +apple-dockchannel-y = dockchannel.o + obj-$(CONFIG_APPLE_MAILBOX) += apple-mailbox.o apple-mailbox-y = mailbox.o diff --git a/drivers/soc/apple/dockchannel.c b/drivers/soc/apple/dockchannel.c new file mode 100644 index 00000000000000..2d1600c174cbd5 --- /dev/null +++ b/drivers/soc/apple/dockchannel.c @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Apple DockChannel FIFO driver + * Copyright The Asahi Linux Contributors + * + * This driver uses PIO to transfer data to/from the FIFO. + * Because all data is moved by the CPU and no DMA is involved, + * we do not require the expensive memory barriers provided by + * the non-relaxed MMIO accessors. Internal ordering to the + * same peripheral is maintained. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define DOCKCHANNEL_FIFO_SIZE 0x800 + +#define DOCKCHANNEL_MAX_IRQ 32 + +#define DOCKCHANNEL_TX_TIMEOUT_MS 1000 +#define DOCKCHANNEL_RX_TIMEOUT_MS 1000 + +#define IRQ_MASK 0x0 +#define IRQ_FLAG 0x4 + +#define IRQ_TX BIT(0) +#define IRQ_RX BIT(1) + +#define CONFIG_TX_THRESH 0x0 +#define CONFIG_RX_THRESH 0x4 + +#define DATA_TX8 0x4 +#define DATA_TX16 0x8 +#define DATA_TX24 0xc +#define DATA_TX32 0x10 +#define DATA_TX_FREE 0x14 +#define DATA_RX8 0x1c +#define DATA_RX16 0x20 +#define DATA_RX24 0x24 +#define DATA_RX32 0x28 +#define DATA_RX_COUNT 0x2c + +struct dockchannel { + struct device *dev; + struct mutex lock; /* protects send/recv/await serialization */ + + int tx_irq; + int rx_irq; + + void __iomem *config_base; + void __iomem *data_base; + + bool awaiting; + struct completion tx_comp; + struct completion rx_comp; + + void *cookie; + void (*data_available)(void *cookie, size_t avail); +}; + +struct dockchannel_common { + struct device *dev; + struct irq_domain *domain; + int irq; + + void __iomem *irq_base; + struct raw_spinlock lock; /* protects IRQ mask RMW */ +}; + +static irqreturn_t dockchannel_tx_irq(int irq, void *data) +{ + struct dockchannel *dockchannel = data; + + disable_irq_nosync(irq); + complete(&dockchannel->tx_comp); + + return IRQ_HANDLED; +} + +static irqreturn_t dockchannel_rx_irq(int irq, void *data) +{ + struct dockchannel *dockchannel = data; + + disable_irq_nosync(irq); + + if (dockchannel->awaiting) + return IRQ_WAKE_THREAD; + + complete(&dockchannel->rx_comp); + return IRQ_HANDLED; +} + +static irqreturn_t dockchannel_rx_irq_thread(int irq, void *data) +{ + struct dockchannel *dockchannel = data; + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + size_t avail = readl_relaxed(dockchannel->data_base + DATA_RX_COUNT); + + dockchannel->awaiting = false; + if (dockchannel->data_available) + dockchannel->data_available(dockchannel->cookie, avail); + + return IRQ_HANDLED; +} + +/** + * dockchannel_send - Send data via DockChannel + * @dockchannel: The dockchannel instance + * @buf: The buffer to send + * @count: Number of bytes to send + * + * This function blocks until all data is written to the FIFO. It handles + * waiting for space if the FIFO fills up. + * + * Return: Number of bytes sent or negative error code. + */ +int dockchannel_send(struct dockchannel *dockchannel, const void *buf, size_t count) +{ + size_t left = count; + const u8 *p = buf; + + mutex_lock(&dockchannel->lock); + + while (left > 0) { + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + size_t avail = readl_relaxed(dockchannel->data_base + DATA_TX_FREE); + size_t block = min(left, avail); + + if (avail == 0) { + size_t threshold = min_t(size_t, DOCKCHANNEL_FIFO_SIZE / 2, left); + + writel_relaxed(threshold, dockchannel->config_base + CONFIG_TX_THRESH); + reinit_completion(&dockchannel->tx_comp); + enable_irq(dockchannel->tx_irq); + + unsigned long timeout; + + timeout = msecs_to_jiffies(DOCKCHANNEL_TX_TIMEOUT_MS); + if (!wait_for_completion_timeout(&dockchannel->tx_comp, + timeout)) { + /* + * If we timed out, we must ensure the IRQ is disabled. + * However, we must check if the handler ran *just* before + * we called disable_irq, which would leave depth at 2. + */ + disable_irq(dockchannel->tx_irq); + if (try_wait_for_completion(&dockchannel->tx_comp)) { + /* Handler ran, depth is 2. Restore to 1 (disabled). */ + enable_irq(dockchannel->tx_irq); + } else { + /* Genuine timeout. Depth is 1. */ + mutex_unlock(&dockchannel->lock); + return -ETIMEDOUT; + } + } + + continue; + } + + while (block >= 4) { + writel_relaxed(get_unaligned_le32(p), dockchannel->data_base + DATA_TX32); + p += 4; + left -= 4; + block -= 4; + } + while (block > 0) { + writeb_relaxed(*p++, dockchannel->data_base + DATA_TX8); + left--; + block--; + } + } + + mutex_unlock(&dockchannel->lock); + return count; +} +EXPORT_SYMBOL_GPL(dockchannel_send); + +/** + * dockchannel_recv - Receive data via DockChannel + * @dockchannel: The dockchannel instance + * @buf: Buffer to receive data into + * @count: Number of bytes to receive + * + * This function blocks until the requested number of bytes are read. + * + * Return: Number of bytes received or negative error code. + */ +int dockchannel_recv(struct dockchannel *dockchannel, void *buf, size_t count) +{ + size_t left = count; + u8 *p = buf; + + mutex_lock(&dockchannel->lock); + + while (left > 0) { + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + size_t avail = readl_relaxed(dockchannel->data_base + DATA_RX_COUNT); + size_t block = min(left, avail); + + if (avail == 0) { + size_t threshold = min_t(size_t, DOCKCHANNEL_FIFO_SIZE / 2, left); + + writel_relaxed(threshold, dockchannel->config_base + CONFIG_RX_THRESH); + reinit_completion(&dockchannel->rx_comp); + enable_irq(dockchannel->rx_irq); + + unsigned long timeout; + + timeout = msecs_to_jiffies(DOCKCHANNEL_RX_TIMEOUT_MS); + if (!wait_for_completion_timeout(&dockchannel->rx_comp, + timeout)) { + disable_irq(dockchannel->rx_irq); + if (try_wait_for_completion(&dockchannel->rx_comp)) { + /* Handler ran, depth 2 -> 1 */ + enable_irq(dockchannel->rx_irq); + } else { + mutex_unlock(&dockchannel->lock); + return -ETIMEDOUT; + } + } + + continue; + } + + while (block >= 4) { + put_unaligned_le32(readl_relaxed(dockchannel->data_base + DATA_RX32), p); + p += 4; + left -= 4; + block -= 4; + } + while (block > 0) { + /* + * From testing, the hardware specifically puts the byte + * in the second byte of a 32-bit word, so the shift is + * required. + */ + *p++ = readl_relaxed(dockchannel->data_base + DATA_RX8) >> 8; + left--; + block--; + } + } + + mutex_unlock(&dockchannel->lock); + return count; +} +EXPORT_SYMBOL_GPL(dockchannel_recv); + +/** + * dockchannel_await - Register a callback for incoming data + * @dockchannel: The dockchannel instance + * @callback: Function to call when data is available + * @cookie: Context pointer passed to the callback + * @count: Threshold of bytes to trigger the callback + * + * This sets up an asynchronous notification. When data is available, + * the callback is invoked from a threaded IRQ context. + * + * Return: Threshold value set, or 0 if disabled. + */ +int dockchannel_await(struct dockchannel *dockchannel, + void (*callback)(void *cookie, size_t avail), + void *cookie, size_t count) +{ + size_t threshold = min_t(size_t, DOCKCHANNEL_FIFO_SIZE, count); + + mutex_lock(&dockchannel->lock); + + if (!count) { + dockchannel->awaiting = false; + disable_irq(dockchannel->rx_irq); + mutex_unlock(&dockchannel->lock); + return 0; + } + + dockchannel->data_available = callback; + dockchannel->cookie = cookie; + dockchannel->awaiting = true; + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + writel_relaxed(threshold, dockchannel->config_base + CONFIG_RX_THRESH); + + enable_irq(dockchannel->rx_irq); + + mutex_unlock(&dockchannel->lock); + return threshold; +} +EXPORT_SYMBOL_GPL(dockchannel_await); + +/** + * dockchannel_init - Initialize a dockchannel interface + * @pdev: The platform device of the child (consumer) + * + * Initializes the I/O, IRQs and locks for a dockchannel interface. + * + * Return: Pointer to initialized dockchannel struct or ERR_PTR. + */ +struct dockchannel *dockchannel_init(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct dockchannel *dockchannel; + int ret; + + dockchannel = devm_kzalloc(dev, sizeof(*dockchannel), GFP_KERNEL); + if (!dockchannel) + return ERR_PTR(-ENOMEM); + + dockchannel->dev = dev; + mutex_init(&dockchannel->lock); + + dockchannel->config_base = devm_platform_ioremap_resource_byname(pdev, "config"); + if (IS_ERR(dockchannel->config_base)) + return ERR_CAST(dockchannel->config_base); + + dockchannel->data_base = devm_platform_ioremap_resource_byname(pdev, "data"); + if (IS_ERR(dockchannel->data_base)) + return ERR_CAST(dockchannel->data_base); + + init_completion(&dockchannel->tx_comp); + init_completion(&dockchannel->rx_comp); + + dockchannel->tx_irq = platform_get_irq_byname(pdev, "tx"); + if (dockchannel->tx_irq <= 0) + return ERR_PTR(dev_err_probe(dev, dockchannel->tx_irq, "Failed to get TX IRQ")); + + dockchannel->rx_irq = platform_get_irq_byname(pdev, "rx"); + if (dockchannel->rx_irq <= 0) + return ERR_PTR(dev_err_probe(dev, dockchannel->rx_irq, "Failed to get RX IRQ")); + + ret = devm_request_irq(dev, dockchannel->tx_irq, dockchannel_tx_irq, IRQF_NO_AUTOEN, + "apple-dockchannel-tx", dockchannel); + if (ret) + return ERR_PTR(dev_err_probe(dev, ret, "Failed to request TX IRQ")); + + ret = devm_request_threaded_irq(dev, dockchannel->rx_irq, dockchannel_rx_irq, + dockchannel_rx_irq_thread, IRQF_NO_AUTOEN, + "apple-dockchannel-rx", dockchannel); + if (ret) + return ERR_PTR(dev_err_probe(dev, ret, "Failed to request RX IRQ")); + + return dockchannel; +} +EXPORT_SYMBOL_GPL(dockchannel_init); + +/* Dockchannel IRQchip */ + +static void dockchannel_irq(struct irq_desc *desc) +{ + unsigned int irq = irq_desc_get_irq(desc); + struct irq_chip *chip = irq_desc_get_chip(desc); + struct dockchannel_common *dcc = irq_get_handler_data(irq); + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + unsigned long flags = readl_relaxed(dcc->irq_base + IRQ_FLAG); + + int bit; + + chained_irq_enter(chip, desc); + + for_each_set_bit(bit, &flags, DOCKCHANNEL_MAX_IRQ) + generic_handle_domain_irq(dcc->domain, bit); + + chained_irq_exit(chip, desc); +} + +static void dockchannel_irq_ack(struct irq_data *data) +{ + struct dockchannel_common *dcc = irq_data_get_irq_chip_data(data); + unsigned int hwirq = data->hwirq; + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + writel_relaxed(BIT(hwirq), dcc->irq_base + IRQ_FLAG); +} + +static void dockchannel_irq_mask(struct irq_data *data) +{ + struct dockchannel_common *dcc = irq_data_get_irq_chip_data(data); + unsigned int hwirq = data->hwirq; + unsigned long flags; + u32 val; + + raw_spin_lock_irqsave(&dcc->lock, flags); + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + val = readl_relaxed(dcc->irq_base + IRQ_MASK); + writel_relaxed(val & ~BIT(hwirq), dcc->irq_base + IRQ_MASK); + + raw_spin_unlock_irqrestore(&dcc->lock, flags); +} + +static void dockchannel_irq_unmask(struct irq_data *data) +{ + struct dockchannel_common *dcc = irq_data_get_irq_chip_data(data); + unsigned int hwirq = data->hwirq; + unsigned long flags; + u32 val; + + raw_spin_lock_irqsave(&dcc->lock, flags); + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + val = readl_relaxed(dcc->irq_base + IRQ_MASK); + writel_relaxed(val | BIT(hwirq), dcc->irq_base + IRQ_MASK); + + raw_spin_unlock_irqrestore(&dcc->lock, flags); +} + +static const struct irq_chip dockchannel_irqchip = { + .name = "dockchannel-irqc", + .irq_ack = dockchannel_irq_ack, + .irq_mask = dockchannel_irq_mask, + .irq_unmask = dockchannel_irq_unmask, +}; + +static int dockchannel_irq_domain_map(struct irq_domain *d, unsigned int virq, + irq_hw_number_t hw) +{ + irq_set_chip_data(virq, d->host_data); + irq_set_chip_and_handler(virq, &dockchannel_irqchip, handle_level_irq); + + return 0; +} + +static const struct irq_domain_ops dockchannel_irq_domain_ops = { + .xlate = irq_domain_xlate_twocell, + .map = dockchannel_irq_domain_map, +}; + +static int dockchannel_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct dockchannel_common *dcc; + + dcc = devm_kzalloc(dev, sizeof(*dcc), GFP_KERNEL); + if (!dcc) + return -ENOMEM; + + dcc->dev = dev; + raw_spin_lock_init(&dcc->lock); + platform_set_drvdata(pdev, dcc); + + dcc->irq_base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(dcc->irq_base)) + return PTR_ERR(dcc->irq_base); + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + writel_relaxed(0, dcc->irq_base + IRQ_MASK); + writel_relaxed(~0, dcc->irq_base + IRQ_FLAG); + + dcc->domain = irq_domain_add_linear(dev->of_node, DOCKCHANNEL_MAX_IRQ, + &dockchannel_irq_domain_ops, dcc); + if (!dcc->domain) + return -ENOMEM; + + dcc->irq = platform_get_irq(pdev, 0); + if (dcc->irq <= 0) { + irq_domain_remove(dcc->domain); + return dev_err_probe(dev, dcc->irq, "Failed to get IRQ"); + } + + irq_set_handler_data(dcc->irq, dcc); + irq_set_chained_handler(dcc->irq, dockchannel_irq); + + return devm_of_platform_populate(dev); +} + +static void dockchannel_remove(struct platform_device *pdev) +{ + struct dockchannel_common *dcc = platform_get_drvdata(pdev); + int hwirq; + + /* + * Children are automatically removed by devm_of_platform_populate + * mechanism before this function is called. + */ + + irq_set_chained_handler_and_data(dcc->irq, NULL, NULL); + + for (hwirq = 0; hwirq < DOCKCHANNEL_MAX_IRQ; hwirq++) + irq_dispose_mapping(irq_find_mapping(dcc->domain, hwirq)); + + irq_domain_remove(dcc->domain); + + /* Relaxed is safe here as there is no DMA, as explained at top of file */ + writel_relaxed(0, dcc->irq_base + IRQ_MASK); + writel_relaxed(~0, dcc->irq_base + IRQ_FLAG); +} + +static const struct of_device_id dockchannel_of_match[] = { + { .compatible = "apple,t8112-dockchannel" }, + {}, +}; +MODULE_DEVICE_TABLE(of, dockchannel_of_match); + +static struct platform_driver dockchannel_driver = { + .driver = { + .name = "dockchannel", + .of_match_table = dockchannel_of_match, + }, + .probe = dockchannel_probe, + .remove = dockchannel_remove, +}; +module_platform_driver(dockchannel_driver); + +MODULE_AUTHOR("Hector Martin "); +MODULE_AUTHOR("Michael Reeves "); +MODULE_LICENSE("Dual MIT/GPL"); +MODULE_DESCRIPTION("Apple DockChannel driver"); diff --git a/include/linux/soc/apple/dockchannel.h b/include/linux/soc/apple/dockchannel.h new file mode 100644 index 00000000000000..63eeae53be6b7d --- /dev/null +++ b/include/linux/soc/apple/dockchannel.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* + * Apple DockChannel definitions + * Copyright (C) The Asahi Linux Contributors + */ + +#ifndef _LINUX_SOC_APPLE_DOCKCHANNEL_H_ +#define _LINUX_SOC_APPLE_DOCKCHANNEL_H_ + +#include +#include +#include + +struct dockchannel; + +#if IS_ENABLED(CONFIG_APPLE_DOCKCHANNEL) + +struct dockchannel *dockchannel_init(struct platform_device *pdev); + +int dockchannel_send(struct dockchannel *dockchannel, const void *buf, size_t count); +int dockchannel_recv(struct dockchannel *dockchannel, void *buf, size_t count); +int dockchannel_await(struct dockchannel *dockchannel, + void (*callback)(void *cookie, size_t avail), + void *cookie, size_t count); + +#else + +static inline struct dockchannel *dockchannel_init(struct platform_device *pdev) +{ + return ERR_PTR(-ENODEV); +} + +static inline int dockchannel_send(struct dockchannel *dockchannel, + const void *buf, size_t count) +{ + return -ENODEV; +} + +static inline int dockchannel_recv(struct dockchannel *dockchannel, + void *buf, size_t count) +{ + return -ENODEV; +} + +static inline int dockchannel_await(struct dockchannel *dockchannel, + void (*callback)(void *cookie, size_t avail), + void *cookie, size_t count) +{ + return -ENODEV; +} + +#endif +#endif From ae611b049125bc2408c8f82a8290308db78137ff Mon Sep 17 00:00:00 2001 From: Sasha Finkelstein Date: Sat, 10 Jan 2026 22:09:07 +1100 Subject: [PATCH 4/9] soc: apple: rtkit: Add tracekit endpoint This commit adds recognition to the Apple RTKit driver for the TraceKit endpoint, found in the Multi-Touch Processor (MTP), Always-On Processor (AOP), and potentially other coprocessors. Without it, warning messages may occur when communicating with these processors. Signed-off-by: Sasha Finkelstein Signed-off-by: Michael Reeves --- drivers/soc/apple/rtkit.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index b8d4da147d23f7..d833f847c7d437 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -22,6 +22,7 @@ enum { APPLE_RTKIT_EP_DEBUG = 3, APPLE_RTKIT_EP_IOREPORT = 4, APPLE_RTKIT_EP_OSLOG = 8, + APPLE_RTKIT_EP_TRACEKIT = 0xa, }; #define APPLE_RTKIT_MGMT_TYPE GENMASK_ULL(59, 52) @@ -191,6 +192,7 @@ static void apple_rtkit_management_rx_epmap(struct apple_rtkit *rtk, u64 msg) case APPLE_RTKIT_EP_DEBUG: case APPLE_RTKIT_EP_IOREPORT: case APPLE_RTKIT_EP_OSLOG: + case APPLE_RTKIT_EP_TRACEKIT: dev_dbg(rtk->dev, "RTKit: Starting system endpoint 0x%02x\n", ep); apple_rtkit_start_ep(rtk, ep); From aa79b6c1f1753f3da9cc88ac65f9e70447947e28 Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sat, 17 Jan 2026 18:55:06 +1100 Subject: [PATCH 5/9] hid: apple: Add support for DockChannel HID devices Modify hid-apple to recognize internal DockChannel HID devices. This adds support for the 2021/2024 FN-key tables on the Host bus and includes a report descriptor fixup to handle the oversized bitfields used by the DockChannel transport. Signed-off-by: Michael Reeves --- drivers/hid/hid-apple.c | 135 ++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 46 deletions(-) diff --git a/drivers/hid/hid-apple.c b/drivers/hid/hid-apple.c index 57da4f86a9fa7f..4b0129c7037b8c 100644 --- a/drivers/hid/hid-apple.c +++ b/drivers/hid/hid-apple.c @@ -473,53 +473,61 @@ static int hidinput_apple_event(struct hid_device *hid, struct input_dev *input, asc->fn_on = !!value; if (real_fnmode) { - switch (hid->product) { - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ANSI: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ISO: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_JIS: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ANSI: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ISO: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_JIS: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ANSI: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ISO: - case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_JIS: - table = magic_keyboard_alu_fn_keys; - break; - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2015: - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2015: - table = magic_keyboard_2015_fn_keys; - break; - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2021: - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2021: - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2021: - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2024: - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2024: - case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2024: + /* + * If it's on the Host bus, it's a modern internal keyboard. + * For other bus types, check the legacy USB product IDs. + */ + if (hid->bus == BUS_HOST) { table = magic_keyboard_2021_and_2024_fn_keys; - break; - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J132: - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J213: - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680: - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680_ALT: - table = macbookpro_no_esc_fn_keys; - break; - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J152F: - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J214K: - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J223: - table = macbookpro_dedicated_esc_fn_keys; - break; - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J140K: - case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J230K: - table = apple_fn_keys; - break; - default: - if (hid->product >= USB_DEVICE_ID_APPLE_WELLSPRING4_ANSI && - hid->product <= USB_DEVICE_ID_APPLE_WELLSPRING4A_JIS) - table = macbookair_fn_keys; - else if (hid->product < 0x21d || hid->product >= 0x300) - table = powerbook_fn_keys; - else + } else { + switch (hid->product) { + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ANSI: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_ISO: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_JIS: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ANSI: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_ISO: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2009_JIS: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ANSI: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_ISO: + case USB_DEVICE_ID_APPLE_ALU_WIRELESS_2011_JIS: + table = magic_keyboard_alu_fn_keys; + break; + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2015: + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2015: + table = magic_keyboard_2015_fn_keys; + break; + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2021: + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2021: + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2021: + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_2024: + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_FINGERPRINT_2024: + case USB_DEVICE_ID_APPLE_MAGIC_KEYBOARD_NUMPAD_2024: + table = magic_keyboard_2021_and_2024_fn_keys; + break; + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J132: + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J213: + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680: + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J680_ALT: + table = macbookpro_no_esc_fn_keys; + break; + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J152F: + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J214K: + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J223: + table = macbookpro_dedicated_esc_fn_keys; + break; + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J140K: + case USB_DEVICE_ID_APPLE_WELLSPRINGT2_J230K: table = apple_fn_keys; + break; + default: + if (hid->product >= USB_DEVICE_ID_APPLE_WELLSPRING4_ANSI && + hid->product <= USB_DEVICE_ID_APPLE_WELLSPRING4A_JIS) + table = macbookair_fn_keys; + else if (hid->product < 0x21d || hid->product >= 0x300) + table = powerbook_fn_keys; + else + table = apple_fn_keys; + } } trans = apple_find_translation(table, code); @@ -653,6 +661,7 @@ static void apple_battery_timer_tick(struct timer_list *t) /* * MacBook JIS keyboard has wrong logical maximum * Magic Keyboard JIS has wrong logical maximum + * Internal DockChannel HIDs on MacBook M2+ have wrong report sizes */ static const __u8 *apple_report_fixup(struct hid_device *hdev, __u8 *rdesc, unsigned int *rsize) @@ -695,6 +704,38 @@ static const __u8 *apple_report_fixup(struct hid_device *hdev, __u8 *rdesc, rdesc[3] = 0x06; } + /* + * Fix up DockChannel descriptors that use oversized report sizes (16384 bits). + * The HID parser cannot handle such large sizes, so we redistribute the bits + * into a standard 8-bit report size with a proportionally increased count + * to maintain the same total data length in a byte-aligned format. + */ + if (*rsize >= 5) { + int i; + + for (i = 0; i <= *rsize - 5; i++) { + /* Look for Report Size 0x4000 (16384) followed by Report Count */ + if (rdesc[i] == 0x76 && rdesc[i + 1] == 0x00 && + rdesc[i + 2] == 0x40 && rdesc[i + 3] == 0x95) { + u8 count = rdesc[i + 4]; + + if (count > 0 && count < 32) { + hid_info(hdev, "fixing up DockChannel report size (Count %d)\n", + count); + + /* + * Replace with Report Size 8 and + * Report Count (2048 * count) + */ + rdesc[i] = 0x75; + rdesc[i + 1] = 0x08; + rdesc[i + 2] = 0x96; + rdesc[i + 3] = 0x00; + rdesc[i + 4] = count * 8; + } + } + } + } return rdesc; } @@ -759,7 +800,7 @@ static int apple_input_configured(struct hid_device *hdev, struct apple_sc *asc = hid_get_drvdata(hdev); if (((asc->quirks & APPLE_HAS_FN) && !asc->fn_found) || apple_is_omoton_kb066(hdev)) { - hid_info(hdev, "Fn key not found (Apple Wireless Keyboard clone?), disabling Fn key handling\n"); + hid_info(hdev, "Disabling function quirk for device without function key\n"); asc->quirks &= ~APPLE_HAS_FN; } @@ -1218,6 +1259,8 @@ static const struct hid_device_id apple_devices[] = { .driver_data = APPLE_HAS_FN | APPLE_ISO_TILDE_QUIRK }, { HID_USB_DEVICE(USB_VENDOR_ID_APPLE, USB_DEVICE_ID_APPLE_TOUCHBAR_BACKLIGHT), .driver_data = APPLE_MAGIC_BACKLIGHT }, + { HID_DEVICE(BUS_HOST, HID_GROUP_ANY, USB_VENDOR_ID_APPLE, HID_ANY_ID), + .driver_data = APPLE_HAS_FN | APPLE_ISO_TILDE_QUIRK }, { } }; From 04265253b4c8c4f1892f2fe61ac5a967c8f01aaa Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sat, 17 Jan 2026 19:12:33 +1100 Subject: [PATCH 6/9] hid: apple: Add DockChannel HID transport driver This adds the transport layer required to communicate with the internal keyboard and trackpad on Apple M2 and later MacBooks. It facilitates HID communication via the DockChannel FIFO and the "MTP" coprocessor. Signed-off-by: Michael Reeves --- drivers/hid/Kconfig | 2 + drivers/hid/Makefile | 2 + drivers/hid/apple-dockchannel-hid/Kconfig | 15 + drivers/hid/apple-dockchannel-hid/Makefile | 6 + .../apple_dockchannel_hid.c | 1004 +++++++++++++++++ 5 files changed, 1029 insertions(+) create mode 100644 drivers/hid/apple-dockchannel-hid/Kconfig create mode 100644 drivers/hid/apple-dockchannel-hid/Makefile create mode 100644 drivers/hid/apple-dockchannel-hid/apple_dockchannel_hid.c diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 920a64b66b25b3..68244803f79cc3 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -1442,6 +1442,8 @@ source "drivers/hid/surface-hid/Kconfig" source "drivers/hid/intel-thc-hid/Kconfig" +source "drivers/hid/apple-dockchannel-hid/Kconfig" + endif # HID # USB support may be used with HID disabled diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 361a7daedeb854..4677b9c46a0399 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -176,3 +176,5 @@ obj-$(CONFIG_AMD_SFH_HID) += amd-sfh-hid/ obj-$(CONFIG_SURFACE_HID_CORE) += surface-hid/ obj-$(CONFIG_INTEL_THC_HID) += intel-thc-hid/ + +obj-$(CONFIG_APPLE_DOCKCHANNEL_HID) += apple-dockchannel-hid/ diff --git a/drivers/hid/apple-dockchannel-hid/Kconfig b/drivers/hid/apple-dockchannel-hid/Kconfig new file mode 100644 index 00000000000000..1eb2e6f9146b46 --- /dev/null +++ b/drivers/hid/apple-dockchannel-hid/Kconfig @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +menu "Apple DockChannel HID support" + depends on APPLE_DOCKCHANNEL + +config APPLE_DOCKCHANNEL_HID + tristate "HID over DockChannel transport layer for Apple Silicon SoCs" + depends on APPLE_DOCKCHANNEL && INPUT && OF && HID + help + This provides a HID transport layer over the Apple DockChannel + interface. This is required to support the internal keyboard + and trackpad on M2 and later MacBook models. + + Say Y here if you have an M2 or later MacBook. + +endmenu diff --git a/drivers/hid/apple-dockchannel-hid/Makefile b/drivers/hid/apple-dockchannel-hid/Makefile new file mode 100644 index 00000000000000..289cc75bc44e60 --- /dev/null +++ b/drivers/hid/apple-dockchannel-hid/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# +# Makefile for DockChannel HID transport drivers +# + +obj-$(CONFIG_APPLE_DOCKCHANNEL_HID) += apple_dockchannel_hid.o diff --git a/drivers/hid/apple-dockchannel-hid/apple_dockchannel_hid.c b/drivers/hid/apple-dockchannel-hid/apple_dockchannel_hid.c new file mode 100644 index 00000000000000..c54ee0783fc6df --- /dev/null +++ b/drivers/hid/apple-dockchannel-hid/apple_dockchannel_hid.c @@ -0,0 +1,1004 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Apple DockChannel HID transport driver + * + * Copyright The Asahi Linux Contributors + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define APPLE_ASC_CPU_CONTROL 0x44 +#define APPLE_ASC_CPU_CONTROL_RUN BIT(4) + +#define COMMAND_TIMEOUT_MS 1000 +#define START_TIMEOUT_MS 2000 + +#define MAX_INTERFACES 16 + +/* Data + checksum */ +#define MAX_PKT_SIZE (0xffff + 4) + +#define DCHID_CHANNEL_CMD 0x11 +#define DCHID_CHANNEL_REPORT 0x12 +#define DCHID_CHECKSUM_SEED 0xffffffff + +struct dchid_hdr { + u8 hdr_len; + u8 channel; + __le16 length; + u8 seq; + u8 iface; + __le16 pad; +} __packed; + +#define IFACE_COMM 0 + +#define FLAGS_GROUP GENMASK(7, 6) +#define FLAGS_REQ GENMASK(5, 0) + +#define REQ_SET_REPORT 0 +#define REQ_GET_REPORT 1 + +struct dchid_subhdr { + u8 flags; + u8 unk; + __le16 length; + __le32 retcode; +} __packed; + +#define EVENT_INIT 0xf0 +#define EVENT_READY 0xf1 + +struct dchid_init_hdr { + u8 type; + u8 unk1; + u8 unk2; + u8 iface; + char name[16]; + u8 more_packets; + u8 unkpad; +} __packed; + +#define INIT_HID_DESCRIPTOR 0 +#define INIT_TERMINATOR 2 +#define INIT_PRODUCT_NAME 7 + +#define CMD_RESET_INTERFACE 0x40 +#define CMD_RESET_INTERFACE_SUB 1 +#define CMD_ENABLE_INTERFACE 0xb4 + +struct dchid_init_block_hdr { + __le16 type; + __le16 length; +} __packed; + +#define STM_REPORT_ID 0x10 +#define STM_REPORT_SERIAL 0x11 +#define STM_REPORT_KEYBTYPE 0x14 + +struct dchid_stm_id { + u8 unk; + __le16 vendor_id; + __le16 product_id; + __le16 version_number; + u8 unk2; + u8 unk3; + u8 keyboard_type; + u8 serial_length; +} __packed; + +struct dchid_work { + struct work_struct work; + struct dchid_iface *iface; + + struct dchid_hdr hdr; + u8 data[]; +}; + +struct dchid_iface { + struct dchid_dev *dchid; + struct hid_device *hid; + struct workqueue_struct *wq; + + bool creating; + struct work_struct create_work; + + int index; + const char *name; + struct fwnode_handle *fwnode; + + u8 tx_seq; + bool deferred; + bool starting; + bool open; + struct completion ready; + + void *hid_desc; + size_t hid_desc_len; + + /* Lock for command submission state below */ + spinlock_t out_lock; + u32 out_flags; + int out_report; + u32 retcode; + void *resp_buf; + size_t resp_size; + struct completion out_complete; +}; + +struct dchid_dev { + struct device *dev; + struct dockchannel *dc; + + struct apple_rtkit *rtk; + void __iomem *asc_base; + void __iomem *sram_base; + struct resource sram_res; + + bool id_ready; + struct dchid_stm_id device_id; + char serial[64]; + + struct dchid_iface *comm; + struct mutex ifaces_lock; /* protects ifaces array */ + struct dchid_iface *ifaces[MAX_INTERFACES]; + + struct workqueue_struct *new_iface_wq; +}; + +static void dchid_destroy_wq(void *data) +{ + struct workqueue_struct *wq = data; + + destroy_workqueue(wq); +} + +static void dchid_fwnode_release(void *data) +{ + fwnode_handle_put(data); +} + +static u32 dchid_checksum(const void *data, size_t len) +{ + const u8 *p = data; + u32 sum = 0; + int i; + + while (len >= 4) { + sum += get_unaligned_le32(p); + p += 4; + len -= 4; + } + + if (len > 0) { + u32 tmp = 0; + + for (i = 0; i < len; i++) + tmp |= p[i] << (i * 8); + sum += tmp; + } + + return sum; +} + +static struct dchid_iface * +dchid_get_interface(struct dchid_dev *dchid, int index, const char *name) +{ + struct dchid_iface *iface; + struct fwnode_handle *fwnode; + int ret; + + if (index >= MAX_INTERFACES) { + dev_err(dchid->dev, "Interface index %d out of range\n", index); + return NULL; + } + + mutex_lock(&dchid->ifaces_lock); + if (dchid->ifaces[index]) { + iface = dchid->ifaces[index]; + mutex_unlock(&dchid->ifaces_lock); + return iface; + } + + iface = devm_kzalloc(dchid->dev, sizeof(*iface), GFP_KERNEL); + if (!iface) { + mutex_unlock(&dchid->ifaces_lock); + return NULL; + } + + iface->index = index; + iface->name = devm_kstrdup(dchid->dev, name, GFP_KERNEL); + if (!iface->name) { + mutex_unlock(&dchid->ifaces_lock); + return NULL; + } + + iface->dchid = dchid; + iface->out_report = -1; + init_completion(&iface->out_complete); + init_completion(&iface->ready); + spin_lock_init(&iface->out_lock); + + /* Input events don't involve memory reclaim */ + iface->wq = alloc_ordered_workqueue("dchid-%s", 0, iface->name); + if (!iface->wq) { + mutex_unlock(&dchid->ifaces_lock); + return NULL; + } + + ret = devm_add_action_or_reset(dchid->dev, dchid_destroy_wq, iface->wq); + if (ret) { + mutex_unlock(&dchid->ifaces_lock); + return NULL; + } + + if (!strcmp(name, "comm")) { + dchid->ifaces[index] = iface; + mutex_unlock(&dchid->ifaces_lock); + return iface; + } + + fwnode = device_get_named_child_node(dchid->dev, name); + if (fwnode) { + iface->fwnode = fwnode; + ret = devm_add_action_or_reset(dchid->dev, dchid_fwnode_release, iface->fwnode); + if (ret) { + mutex_unlock(&dchid->ifaces_lock); + return NULL; + } + } else { + iface->fwnode = dev_fwnode(dchid->dev); + } + + dchid->ifaces[index] = iface; + mutex_unlock(&dchid->ifaces_lock); + return iface; +} + +/* + * dchid_send must send the entire packet as one DockChannel transaction + * to ensure atomicity for the coprocessor hardware. + */ +static int dchid_send(struct dchid_iface *iface, u32 flags, const void *msg, size_t size) +{ + size_t payload_padded = round_up(size, 4); + size_t total_len = sizeof(struct dchid_hdr) + sizeof(struct dchid_subhdr) + + payload_padded + 4; + struct dchid_hdr *hdr; + struct dchid_subhdr *sub; + u32 *checksum_ptr; + u8 *buf; + int ret; + + buf = kzalloc(total_len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + hdr = (struct dchid_hdr *)buf; + sub = (struct dchid_subhdr *)(buf + sizeof(*hdr)); + checksum_ptr = (u32 *)(buf + total_len - 4); + + hdr->hdr_len = sizeof(*hdr); + hdr->channel = DCHID_CHANNEL_CMD; + hdr->length = cpu_to_le16(payload_padded + sizeof(*sub)); + hdr->seq = iface->tx_seq; + hdr->iface = iface->index; + + sub->flags = (u8)flags; + sub->length = cpu_to_le16(size); + + memcpy(buf + sizeof(*hdr) + sizeof(*sub), msg, size); + + *checksum_ptr = 0xffffffff - dchid_checksum(buf, total_len - 4); + + ret = dockchannel_send(iface->dchid->dc, buf, total_len); + kfree(buf); + + return (ret == total_len) ? 0 : -EIO; +} + +static int dchid_cmd(struct dchid_iface *iface, u32 type, u32 req, + void *data, size_t size, void *resp_buf, size_t resp_size) +{ + unsigned long flags; + int ret; + int report_id; + bool timed_out = false; + u32 out_flags; + + if (size < 1) + return -EINVAL; + + report_id = *(u8 *)data; + out_flags = FIELD_PREP(FLAGS_GROUP, type) | FIELD_PREP(FLAGS_REQ, req); + + spin_lock_irqsave(&iface->out_lock, flags); + + /* Only one command can be in flight per interface */ + if (WARN_ON(iface->out_report != -1)) { + spin_unlock_irqrestore(&iface->out_lock, flags); + return -EBUSY; + } + + iface->out_report = report_id; + iface->out_flags = out_flags; + iface->resp_buf = resp_buf; + iface->resp_size = resp_size; + reinit_completion(&iface->out_complete); + + spin_unlock_irqrestore(&iface->out_lock, flags); + + ret = dchid_send(iface, out_flags, data, size); + if (ret < 0) { + spin_lock_irqsave(&iface->out_lock, flags); + iface->out_report = -1; + iface->resp_buf = NULL; + iface->resp_size = 0; + spin_unlock_irqrestore(&iface->out_lock, flags); + return ret; + } + + if (!wait_for_completion_timeout(&iface->out_complete, + msecs_to_jiffies(COMMAND_TIMEOUT_MS))) { + dev_err(iface->dchid->dev, "command 0x%x to iface %d (%s) timed out\n", + report_id, iface->index, iface->name); + timed_out = true; + } + + spin_lock_irqsave(&iface->out_lock, flags); + + if (timed_out && iface->out_report == report_id) { + /* Truly timed out; the response handler never ran */ + ret = -ETIMEDOUT; + } else if (iface->retcode) { + /* Response received, but coprocessor reported an error */ + dev_err(iface->dchid->dev, + "command 0x%x to iface %d (%s) failed with err 0x%x\n", + report_id, iface->index, iface->name, iface->retcode); + ret = -EIO; + } else { + /* Command succeeded; return bytes written to resp_buf */ + ret = iface->resp_size; + } + + iface->tx_seq++; + iface->out_report = -1; + iface->resp_buf = NULL; + iface->resp_size = 0; + spin_unlock_irqrestore(&iface->out_lock, flags); + + return ret; +} + +static int dchid_comm_cmd(struct dchid_dev *dchid, void *cmd, size_t size) +{ + return dchid_cmd(dchid->comm, HID_FEATURE_REPORT, REQ_SET_REPORT, cmd, size, NULL, 0); +} + +static int dchid_enable_interface(struct dchid_iface *iface) +{ + u8 cmd[] = { CMD_ENABLE_INTERFACE, iface->index }; + + return dchid_comm_cmd(iface->dchid, cmd, sizeof(cmd)); +} + +static int dchid_reset_interface(struct dchid_iface *iface, int state) +{ + u8 cmd[] = { CMD_RESET_INTERFACE, CMD_RESET_INTERFACE_SUB, iface->index, (u8)state }; + + return dchid_comm_cmd(iface->dchid, cmd, sizeof(cmd)); +} + +static int dchid_start_interface(struct dchid_iface *iface) +{ + if (iface->starting) + return -EINPROGRESS; + + dev_dbg(iface->dchid->dev, "Starting interface %s\n", iface->name); + + iface->starting = true; + + /* + * Removed FW loading logic. + * Just reset the interface to clean state. + */ + dev_dbg(iface->dchid->dev, "Resetting %s\n", iface->name); + dchid_reset_interface(iface, 0); + dchid_reset_interface(iface, 2); + + return 0; +} + +static int dchid_start(struct hid_device *hdev) +{ + return 0; +} + +static int dchid_open(struct hid_device *hdev) +{ + struct dchid_iface *iface = hdev->driver_data; + int ret; + + if (!completion_done(&iface->ready)) { + ret = dchid_start_interface(iface); + if (ret < 0) + return ret; + + if (!wait_for_completion_timeout(&iface->ready, + msecs_to_jiffies(START_TIMEOUT_MS))) { + dev_err(iface->dchid->dev, "iface %s start timed out\n", iface->name); + return -ETIMEDOUT; + } + } + + iface->open = true; + return 0; +} + +static void dchid_close(struct hid_device *hdev) +{ + struct dchid_iface *iface = hdev->driver_data; + + iface->open = false; +} + +static int dchid_parse(struct hid_device *hdev) +{ + struct dchid_iface *iface = hdev->driver_data; + + return hid_parse_report(hdev, iface->hid_desc, iface->hid_desc_len); +} + +static int dchid_get_report_cmd(struct dchid_iface *iface, u8 reportnum, void *buf, size_t len) +{ + int ret = dchid_cmd(iface, HID_FEATURE_REPORT, REQ_GET_REPORT, &reportnum, 1, buf, len); + + return ret <= 0 ? ret : ret - 1; +} + +static int dchid_set_report(struct dchid_iface *iface, void *buf, size_t len) +{ + return dchid_cmd(iface, HID_OUTPUT_REPORT, REQ_SET_REPORT, buf, len, NULL, 0); +} + +static int dchid_raw_request(struct hid_device *hdev, + unsigned char reportnum, __u8 *buf, size_t len, + unsigned char rtype, int reqtype) +{ + struct dchid_iface *iface = hdev->driver_data; + + switch (reqtype) { + case HID_REQ_GET_REPORT: + buf[0] = reportnum; + return dchid_cmd(iface, rtype, REQ_GET_REPORT, &reportnum, 1, buf + 1, len - 1); + case HID_REQ_SET_REPORT: + return dchid_set_report(iface, buf, len); + default: + return -EIO; + } +} + +static const struct hid_ll_driver dchid_ll = { + .start = &dchid_start, + .open = &dchid_open, + .close = &dchid_close, + .parse = &dchid_parse, + .raw_request = &dchid_raw_request, +}; + +static void dchid_create_interface_work(struct work_struct *ws) +{ + struct dchid_iface *iface = container_of(ws, struct dchid_iface, create_work); + struct dchid_dev *dchid = iface->dchid; + struct hid_device *hid; + int ret; + char cap_name[16]; + + if (iface->hid) { + dev_warn(dchid->dev, "Interface %s already created!\n", + iface->name); + goto done; + } + + ret = dchid_enable_interface(iface); + if (ret < 0) { + dev_warn(dchid->dev, "Failed to enable %s: %d\n", iface->name, ret); + goto done; + } + + iface->deferred = false; + + hid = hid_allocate_device(); + if (IS_ERR(hid)) + goto done; + + strscpy(cap_name, iface->name, sizeof(cap_name)); + if (cap_name[0]) + cap_name[0] = toupper(cap_name[0]); + snprintf(hid->name, sizeof(hid->name), "Apple DockChannel %s", cap_name); + + snprintf(hid->phys, sizeof(hid->phys), "%s.%d", + dev_name(dchid->dev), iface->index); + strscpy(hid->uniq, dchid->serial, sizeof(hid->uniq)); + + hid->ll_driver = &dchid_ll; + hid->bus = BUS_HOST; + hid->vendor = le16_to_cpu(dchid->device_id.vendor_id); + hid->product = le16_to_cpu(dchid->device_id.product_id); + hid->version = le16_to_cpu(dchid->device_id.version_number); + + if (!strcmp(iface->name, "keyboard")) { + u32 country_code = 0; + + if (!fwnode_property_read_u32(iface->fwnode, "hid-country-code", &country_code)) + hid->country = country_code; + } + + hid->dev.parent = iface->dchid->dev; + hid->driver_data = iface; + + iface->hid = hid; + + ret = hid_add_device(hid); + if (ret < 0) { + iface->hid = NULL; + hid_destroy_device(hid); + dev_warn(iface->dchid->dev, "Failed to register hid device %s\n", iface->name); + } + +done: + iface->creating = false; +} + +static int dchid_create_interface(struct dchid_iface *iface) +{ + if (iface->creating) + return -EBUSY; + + iface->creating = true; + INIT_WORK(&iface->create_work, dchid_create_interface_work); + return queue_work(iface->dchid->new_iface_wq, &iface->create_work); +} + +static void dchid_handle_descriptor(struct dchid_iface *iface, void *hid_desc, size_t desc_len) +{ + if (iface->hid) + return; + + iface->hid_desc = devm_kmemdup(iface->dchid->dev, hid_desc, desc_len, GFP_KERNEL); + if (iface->hid_desc) + iface->hid_desc_len = desc_len; +} + +static void dchid_handle_ready(struct dchid_dev *dchid, void *data, size_t length) +{ + struct dchid_iface *iface; + u8 *pkt = data; + u8 index; + int i, ret; + + if (length < 2) + return; + + index = pkt[1]; + + if (index >= MAX_INTERFACES) + return; + + iface = dchid->ifaces[index]; + if (!iface) + return; + + dev_dbg(dchid->dev, "Interface %s is now ready\n", iface->name); + complete_all(&iface->ready); + + if (!strcmp(iface->name, "stm")) { + ret = dchid_get_report_cmd(iface, STM_REPORT_ID, &dchid->device_id, + sizeof(dchid->device_id)); + if (ret < (int)sizeof(dchid->device_id)) + memset(&dchid->device_id, 0, sizeof(dchid->device_id)); + + ret = dchid_get_report_cmd(iface, STM_REPORT_SERIAL, dchid->serial, + sizeof(dchid->serial) - 1); + if (ret < 0) + dchid->serial[0] = 0; + + dchid->id_ready = true; + for (i = 0; i < MAX_INTERFACES; i++) { + if (!dchid->ifaces[i] || !dchid->ifaces[i]->deferred) + continue; + dchid_create_interface(dchid->ifaces[i]); + } + } +} + +static void dchid_handle_init(struct dchid_dev *dchid, void *data, size_t length) +{ + struct dchid_init_hdr *hdr = data; + struct dchid_iface *iface; + struct dchid_init_block_hdr *blk; + + if (length < sizeof(*hdr)) + return; + + iface = dchid_get_interface(dchid, hdr->iface, hdr->name); + if (!iface) + return; + + data += sizeof(*hdr); + length -= sizeof(*hdr); + + while (length >= sizeof(*blk)) { + u16 blk_len; + + blk = data; + data += sizeof(*blk); + length -= sizeof(*blk); + + blk_len = le16_to_cpu(blk->length); + + if (blk_len > length) + break; + + switch (le16_to_cpu(blk->type)) { + case INIT_HID_DESCRIPTOR: + dchid_handle_descriptor(iface, data, blk_len); + break; + + case INIT_PRODUCT_NAME: { + char *product = data; + + if (blk_len > 0 && product[blk_len - 1] != 0) + dev_warn(dchid->dev, "Unterminated product name for %s\n", + iface->name); + break; + } + } + + data += blk_len; + length -= blk_len; + + if (le16_to_cpu(blk->type) == INIT_TERMINATOR) + break; + } + + if (hdr->more_packets) + return; + + if (iface->dchid->id_ready || !strcmp(iface->name, "stm")) + dchid_create_interface(iface); + else + iface->deferred = true; +} + +static void dchid_handle_event(struct dchid_dev *dchid, void *data, size_t length) +{ + u8 *p = data; + + switch (*p) { + case EVENT_INIT: + dchid_handle_init(dchid, data, length); + break; + case EVENT_READY: + dchid_handle_ready(dchid, data, length); + break; + } +} + +static void dchid_handle_report(struct dchid_iface *iface, void *data, size_t length) +{ + if (!iface->hid || !iface->open) + return; + + hid_input_report(iface->hid, HID_INPUT_REPORT, data, length, 1); +} + +static void dchid_packet_work(struct work_struct *ws) +{ + struct dchid_work *work = container_of(ws, struct dchid_work, work); + struct dchid_subhdr *shdr = (void *)work->data; + struct dchid_dev *dchid = work->iface->dchid; + int type = FIELD_GET(FLAGS_GROUP, shdr->flags); + u8 *payload = work->data + sizeof(*shdr); + u16 sub_len = le16_to_cpu(shdr->length); + u16 hdr_len = le16_to_cpu(work->hdr.length); + + if (sub_len + sizeof(*shdr) > hdr_len) { + dev_err(dchid->dev, "Bad sub header length\n"); + goto done; + } + + switch (type) { + case HID_INPUT_REPORT: + if (work->hdr.iface == IFACE_COMM) + dchid_handle_event(dchid, payload, sub_len); + else + dchid_handle_report(work->iface, payload, sub_len); + break; + } + +done: + kfree(work); +} + +static void dchid_handle_ack(struct dchid_iface *iface, struct dchid_hdr *hdr, void *data) +{ + struct dchid_subhdr *shdr = (void *)data; + u8 *payload = data + sizeof(*shdr); + u16 sub_len = le16_to_cpu(shdr->length); + u16 hdr_len = le16_to_cpu(hdr->length); + unsigned long flags; + bool complete_cmd = false; + + if (sub_len + sizeof(*shdr) > hdr_len || sub_len < 1) + return; + + spin_lock_irqsave(&iface->out_lock, flags); + + if (shdr->flags == iface->out_flags && iface->tx_seq == hdr->seq && + iface->out_report == payload[0]) { + if (iface->resp_buf && iface->resp_size) + memcpy(iface->resp_buf, payload + 1, + min_t(size_t, sub_len - 1, iface->resp_size)); + + iface->resp_size = sub_len; + iface->out_report = -1; + iface->retcode = le32_to_cpu(shdr->retcode); + complete_cmd = true; + } + + spin_unlock_irqrestore(&iface->out_lock, flags); + + if (complete_cmd) + complete(&iface->out_complete); +} + +static void dchid_handle_packet(void *cookie, size_t avail) +{ + struct dchid_dev *dchid = cookie; + struct dchid_hdr hdr; + struct dchid_work *work = NULL; + u8 *tmp_buf = NULL; + u32 checksum; + u16 payload_len; + size_t total_payload_size; + + if (dockchannel_recv(dchid->dc, &hdr, sizeof(hdr)) != sizeof(hdr)) + return; + + if (hdr.hdr_len != sizeof(hdr)) + goto reschedule; + + payload_len = le16_to_cpu(hdr.length); + total_payload_size = payload_len + 4; // Payload + Checksum + + tmp_buf = kzalloc(total_payload_size, GFP_ATOMIC); + if (!tmp_buf) + goto reschedule; + + if (dockchannel_recv(dchid->dc, tmp_buf, total_payload_size) != total_payload_size) + goto out_free; + + checksum = dchid_checksum(&hdr, sizeof(hdr)); + checksum += dchid_checksum(tmp_buf, total_payload_size); + + if (checksum != DCHID_CHECKSUM_SEED) { + dev_err_ratelimited(dchid->dev, "Checksum error\n"); + goto out_free; + } + + if (hdr.iface >= MAX_INTERFACES || !dchid->ifaces[hdr.iface]) + goto out_free; + + if (hdr.channel == DCHID_CHANNEL_CMD) { + dchid_handle_ack(dchid->ifaces[hdr.iface], &hdr, tmp_buf); + goto out_free; + } + + work = kzalloc(sizeof(*work) + payload_len, GFP_ATOMIC); + if (!work) + goto out_free; + + work->hdr = hdr; + work->iface = dchid->ifaces[hdr.iface]; + memcpy(work->data, tmp_buf, payload_len); + INIT_WORK(&work->work, dchid_packet_work); + + queue_work(work->iface->wq, &work->work); + +out_free: + kfree(tmp_buf); +reschedule: + dockchannel_await(dchid->dc, dchid_handle_packet, dchid, sizeof(struct dchid_hdr)); +} + +static int dchid_rtkit_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr) +{ + struct dchid_dev *dchid = cookie; + struct resource res = { + .start = bfr->iova, + .end = bfr->iova + bfr->size - 1, + .name = "rtkit_map", + }; + + if (!bfr->iova) { + bfr->buffer = dma_alloc_coherent(dchid->dev, bfr->size, + &bfr->iova, GFP_KERNEL); + if (!bfr->buffer) + return -ENOMEM; + return 0; + } + + if (!dchid->sram_res.start) + return -EFAULT; + + res.flags = dchid->sram_res.flags; + + if (res.end < res.start || !resource_contains(&dchid->sram_res, &res)) + return -EFAULT; + + bfr->iomem = dchid->sram_base + (res.start - dchid->sram_res.start); + bfr->is_mapped = true; + + return 0; +} + +static void dchid_rtkit_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr) +{ + struct dchid_dev *dchid = cookie; + + if (bfr->buffer) + dma_free_coherent(dchid->dev, bfr->size, bfr->buffer, bfr->iova); +} + +static const struct apple_rtkit_ops dchid_rtkit_ops = { + .shmem_setup = dchid_rtkit_shmem_setup, + .shmem_destroy = dchid_rtkit_shmem_destroy, +}; + +static int dchid_map_helper_cpu(struct platform_device *pdev, struct dchid_dev *dchid) +{ + struct resource *res; + + /* Map ASC (Co-processor CPU control) */ + dchid->asc_base = devm_platform_ioremap_resource_byname(pdev, "coproc-asc"); + if (IS_ERR(dchid->asc_base)) + return PTR_ERR(dchid->asc_base); + + /* Map SRAM (Shared memory) */ + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "coproc-sram"); + if (!res) + return -EINVAL; + + /* Store resource copy for RTKit shmem setup */ + dchid->sram_res = *res; + + dchid->sram_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(dchid->sram_base)) + return PTR_ERR(dchid->sram_base); + + return 0; +} + +static int dchid_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct dchid_dev *dchid; + int ret; + + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(44)); + if (ret) + return dev_err_probe(dev, ret, "Failed to set DMA mask\n"); + + dchid = devm_kzalloc(dev, sizeof(*dchid), GFP_KERNEL); + if (!dchid) + return -ENOMEM; + + dchid->dev = dev; + mutex_init(&dchid->ifaces_lock); + platform_set_drvdata(pdev, dchid); + + ret = dchid_map_helper_cpu(pdev, dchid); + if (ret) + return dev_err_probe(dev, ret, "Failed to map helper CPU\n"); + + dchid->rtk = devm_apple_rtkit_init(dev, dchid, NULL, 0, &dchid_rtkit_ops); + if (IS_ERR(dchid->rtk)) + return dev_err_probe(dev, PTR_ERR(dchid->rtk), "Failed to init RTKit\n"); + + writel_relaxed(APPLE_ASC_CPU_CONTROL_RUN, dchid->asc_base + APPLE_ASC_CPU_CONTROL); + + ret = apple_rtkit_wake(dchid->rtk); + if (ret) + return dev_err_probe(dev, ret, "Failed to wake up coprocessor\n"); + + dchid->dc = dockchannel_init(pdev); + if (IS_ERR(dchid->dc)) + return dev_err_probe(dev, PTR_ERR(dchid->dc), "Failed to init DockChannel\n"); + + /* Create workqueue for dynamic interface addition */ + dchid->new_iface_wq = alloc_ordered_workqueue("dchid-new", 0); + if (!dchid->new_iface_wq) + return dev_err_probe(dev, -ENOMEM, "Failed to allocate workqueue\n"); + + ret = devm_add_action_or_reset(dev, dchid_destroy_wq, dchid->new_iface_wq); + if (ret) + return ret; + + dchid->comm = dchid_get_interface(dchid, IFACE_COMM, "comm"); + if (!dchid->comm) + return dev_err_probe(dev, -EIO, "Failed to init comm interface\n"); + + dockchannel_await(dchid->dc, dchid_handle_packet, dchid, sizeof(struct dchid_hdr)); + + return 0; +} + +static void dchid_remove(struct platform_device *pdev) +{ + struct dchid_dev *dchid = platform_get_drvdata(pdev); + int i; + + if (dchid->rtk && apple_rtkit_is_running(dchid->rtk)) + apple_rtkit_quiesce(dchid->rtk); + + if (dchid->asc_base) + writel_relaxed(0, dchid->asc_base + APPLE_ASC_CPU_CONTROL); + + dockchannel_await(dchid->dc, NULL, NULL, 0); + + for (i = 0; i < MAX_INTERFACES; i++) { + struct dchid_iface *iface = dchid->ifaces[i]; + + if (!iface) + continue; + + cancel_work_sync(&iface->create_work); + flush_workqueue(iface->wq); + + if (iface->hid) + hid_destroy_device(iface->hid); + } + + if (dchid->new_iface_wq) + flush_workqueue(dchid->new_iface_wq); +} + +static const struct of_device_id dchid_of_match[] = { + { .compatible = "apple,t8112-dockchannel-hid" }, + {}, +}; +MODULE_DEVICE_TABLE(of, dchid_of_match); + +static struct platform_driver dchid_platform_driver = { + .driver = { + .name = "dockchannel-hid", + .of_match_table = dchid_of_match, + }, + .probe = dchid_probe, + .remove = dchid_remove, +}; +module_platform_driver(dchid_platform_driver); + +MODULE_DESCRIPTION("Apple DockChannel HID transport driver"); +MODULE_AUTHOR("Hector Martin "); +MODULE_AUTHOR("Michael Reeves "); +MODULE_LICENSE("Dual MIT/GPL"); From 6cf8f1346e99735c964faf4de8eed9886690770c Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sat, 10 Jan 2026 22:12:48 +1100 Subject: [PATCH 7/9] arm64: dts: apple: Add DockChannel HID nodes Add the required nodes for DockChannel, DockChannel HID and MTP (a coprocessor that facilitates DockChannel HID communication) to t602x and t8112 (the only currently in-tree DTs which support DockChannel HID). They are disabled by default, to be enabled only on the laptop variants of these chipsets which actually have the internal HID hardware. Co-developed-by: Hector Martin Signed-off-by: Hector Martin Signed-off-by: Michael Reeves --- arch/arm64/boot/dts/apple/t602x-die0.dtsi | 59 +++++++++++++++++++++++ arch/arm64/boot/dts/apple/t8112.dtsi | 59 +++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t602x-die0.dtsi b/arch/arm64/boot/dts/apple/t602x-die0.dtsi index 2e7d2bf08ddc82..19a6ab3cff689b 100644 --- a/arch/arm64/boot/dts/apple/t602x-die0.dtsi +++ b/arch/arm64/boot/dts/apple/t602x-die0.dtsi @@ -144,6 +144,65 @@ ; }; + mtp_mbox: mbox@2a9408000 { + compatible = "apple,t6020-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x2 0xa9408000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + + status = "disabled"; + }; + + mtp_dart: iommu@2a9808000 { + compatible = "apple,t6020-dart", "apple,t8110-dart"; + reg = <0x2 0xa9808000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + + status = "disabled"; + }; + + mtp_dockchannel: fifo@2a9b14000 { + compatible = "apple,t6020-dockchannel", "apple,t8112-dockchannel"; + reg = <0x2 0xa9b14000 0x0 0x4000>; + + interrupt-parent = <&aic>; + interrupts = ; + + ranges; + #address-cells = <2>; + #size-cells = <2>; + + interrupt-controller; + #interrupt-cells = <2>; + + status = "disabled"; + + mtp_hid: input@2a9b30000 { + compatible = "apple,t6020-dockchannel-hid", "apple,t8112-dockchannel-hid"; + reg = <0x2 0xa9b30000 0x0 0x4000>, + <0x2 0xa9b34000 0x0 0x4000>, + <0x2 0xa9400000 0x0 0x4000>, + <0x2 0xa9c00000 0x0 0x100000>; + reg-names = "config", "data", "coproc-asc", "coproc-sram"; + + mboxes = <&mtp_mbox>; + iommus = <&mtp_dart 1>; + + interrupt-parent = <&mtp_dockchannel>; + interrupts = <2 IRQ_TYPE_LEVEL_HIGH>, + <3 IRQ_TYPE_LEVEL_HIGH>; + interrupt-names = "tx", "rx"; + }; + }; + sio_dart: iommu@39b008000 { compatible = "apple,t6020-dart", "apple,t8110-dart"; reg = <0x3 0x9b008000 0x0 0x8000>; diff --git a/arch/arm64/boot/dts/apple/t8112.dtsi b/arch/arm64/boot/dts/apple/t8112.dtsi index 3f79878b25af1f..917e96a174f235 100644 --- a/arch/arm64/boot/dts/apple/t8112.dtsi +++ b/arch/arm64/boot/dts/apple/t8112.dtsi @@ -976,6 +976,65 @@ ; }; + mtp_mbox: mbox@24e408000 { + compatible = "apple,t8112-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x2 0x4e408000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + + status = "disabled"; + }; + + mtp_dart: iommu@24e808000 { + compatible = "apple,t8110-dart"; + reg = <0x2 0x4e808000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = ; + #iommu-cells = <1>; + + status = "disabled"; + }; + + mtp_dockchannel: fifo@24eb14000 { + compatible = "apple,t8112-dockchannel"; + reg = <0x2 0x4eb14000 0x0 0x4000>; + + interrupt-parent = <&aic>; + interrupts = ; + + ranges; + #address-cells = <2>; + #size-cells = <2>; + + interrupt-controller; + #interrupt-cells = <2>; + + status = "disabled"; + + mtp_hid: input@24eb30000 { + compatible = "apple,t8112-dockchannel-hid"; + reg = <0x2 0x4eb30000 0x0 0x4000>, + <0x2 0x4eb34000 0x0 0x4000>, + <0x2 0x4e400000 0x0 0x4000>, + <0x2 0x4ec00000 0x0 0x100000>; + reg-names = "config", "data", "coproc-asc", "coproc-sram"; + + mboxes = <&mtp_mbox>; + iommus = <&mtp_dart 1>; + + interrupt-parent = <&mtp_dockchannel>; + interrupts = <2 IRQ_TYPE_LEVEL_HIGH>, + <3 IRQ_TYPE_LEVEL_HIGH>; + interrupt-names = "tx", "rx"; + }; + }; + ans_mbox: mbox@277408000 { compatible = "apple,t8112-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x2 0x77408000 0x0 0x4000>; From 275d6457a1c94b6147432a5690cbf19726d8721d Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sat, 10 Jan 2026 22:14:29 +1100 Subject: [PATCH 8/9] arm64: dts: apple: Enable DockChannel HID on M2 and later laptops On M2 and later chipsets, the nodes required to enable communication with the internal keyboard and trackpad are disabled by default, this patch enables them only on laptop (MacBook) variants where this hardware is actually present. Co-developed-by: Hector Martin Signed-off-by: Hector Martin Signed-off-by: Michael Reeves --- .../arm64/boot/dts/apple/t602x-j414-j416.dtsi | 25 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8112-j413.dts | 20 +++++++++++++++ arch/arm64/boot/dts/apple/t8112-j415.dts | 20 +++++++++++++++ arch/arm64/boot/dts/apple/t8112-j493.dts | 22 +++++++++++++++- 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t602x-j414-j416.dtsi b/arch/arm64/boot/dts/apple/t602x-j414-j416.dtsi index 0e806d8ddf81b1..fceff4224894c8 100644 --- a/arch/arm64/boot/dts/apple/t602x-j414-j416.dtsi +++ b/arch/arm64/boot/dts/apple/t602x-j414-j416.dtsi @@ -16,6 +16,12 @@ #include "t600x-j314-j316.dtsi" +/ { + aliases { + keyboard = &keyboard; + }; +}; + &framebuffer0 { power-domains = <&ps_disp0_cpu0>, <&ps_dptx_phy_ps>; }; @@ -43,3 +49,22 @@ &bluetooth0 { compatible = "pci14e4,5f72"; }; + +&mtp_mbox { + status = "okay"; +}; + +&mtp_dart { + status = "okay"; +}; + +&mtp_dockchannel { + status = "okay"; +}; + +&mtp_hid { + keyboard: keyboard { + /* Filled by bootloader */ + hid-country-code = <0>; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8112-j413.dts b/arch/arm64/boot/dts/apple/t8112-j413.dts index 6f69658623bf89..b3755befd82556 100644 --- a/arch/arm64/boot/dts/apple/t8112-j413.dts +++ b/arch/arm64/boot/dts/apple/t8112-j413.dts @@ -20,6 +20,7 @@ aliases { bluetooth0 = &bluetooth0; wifi0 = &wifi0; + keyboard = &keyboard; }; led-controller { @@ -78,3 +79,22 @@ &fpwm1 { status = "okay"; }; + +&mtp_mbox { + status = "okay"; +}; + +&mtp_dart { + status = "okay"; +}; + +&mtp_dockchannel { + status = "okay"; +}; + +&mtp_hid { + keyboard: keyboard { + /* Filled by bootloader */ + hid-country-code = <0>; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8112-j415.dts b/arch/arm64/boot/dts/apple/t8112-j415.dts index b54e218e5384ca..cee13511f2b9aa 100644 --- a/arch/arm64/boot/dts/apple/t8112-j415.dts +++ b/arch/arm64/boot/dts/apple/t8112-j415.dts @@ -20,6 +20,7 @@ aliases { bluetooth0 = &bluetooth0; wifi0 = &wifi0; + keyboard = &keyboard; }; led-controller { @@ -78,3 +79,22 @@ &fpwm1 { status = "okay"; }; + +&mtp_mbox { + status = "okay"; +}; + +&mtp_dart { + status = "okay"; +}; + +&mtp_dockchannel { + status = "okay"; +}; + +&mtp_hid { + keyboard: keyboard { + /* Filled by bootloader */ + hid-country-code = <0>; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8112-j493.dts b/arch/arm64/boot/dts/apple/t8112-j493.dts index fb8ad7d4c65a8f..781d841a0dd324 100644 --- a/arch/arm64/boot/dts/apple/t8112-j493.dts +++ b/arch/arm64/boot/dts/apple/t8112-j493.dts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0+ OR MIT /* - * Apple MacBook Pro (13-inch, M1, 2022) + * Apple MacBook Pro (13-inch, M2, 2022) * * target-type: J493 * @@ -25,6 +25,7 @@ bluetooth0 = &bluetooth0; touchbar0 = &touchbar0; wifi0 = &wifi0; + keyboard = &keyboard; }; led-controller { @@ -133,3 +134,22 @@ touchscreen-inverted-y; }; }; + +&mtp_mbox { + status = "okay"; +}; + +&mtp_dart { + status = "okay"; +}; + +&mtp_dockchannel { + status = "okay"; +}; + +&mtp_hid { + keyboard: keyboard { + /* Filled by bootloader */ + hid-country-code = <0>; + }; +}; From a8a69b12a2d4ac24a3ab6ebc7c6147c88527eccb Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sun, 11 Jan 2026 00:00:30 +1100 Subject: [PATCH 9/9] MAINTAINERS: Add Apple DockChannel entries This patch adds the new directories and files created while adding support for Keyboard HID over Apple DockChannel to the block in MAINTAINERS for the Asahi Linux (Linux on Mac devices with Apple Silicon chipset) project. Signed-off-by: Michael Reeves --- MAINTAINERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 5b11839cba9de1..a95603c06f5092 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2459,6 +2459,7 @@ F: Documentation/devicetree/bindings/dma/apple,admac.yaml F: Documentation/devicetree/bindings/gpio/apple,smc-gpio.yaml F: Documentation/devicetree/bindings/gpu/apple,agx.yaml F: Documentation/devicetree/bindings/i2c/apple,i2c.yaml +F: Documentation/devicetree/bindings/input/apple,dockchannel-hid.yaml F: Documentation/devicetree/bindings/input/touchscreen/apple,z2-multitouch.yaml F: Documentation/devicetree/bindings/interrupt-controller/apple,* F: Documentation/devicetree/bindings/iommu/apple,dart.yaml @@ -2476,6 +2477,7 @@ F: Documentation/devicetree/bindings/power/apple* F: Documentation/devicetree/bindings/power/reset/apple,smc-reboot.yaml F: Documentation/devicetree/bindings/pwm/apple,s5l-fpwm.yaml F: Documentation/devicetree/bindings/rtc/apple,smc-rtc.yaml +F: Documentation/devicetree/bindings/soc/apple/* F: Documentation/devicetree/bindings/spi/apple,spi.yaml F: Documentation/devicetree/bindings/spmi/apple,spmi.yaml F: Documentation/devicetree/bindings/usb/apple,dwc3.yaml @@ -2487,6 +2489,7 @@ F: drivers/clk/clk-apple-nco.c F: drivers/cpufreq/apple-soc-cpufreq.c F: drivers/dma/apple-admac.c F: drivers/gpio/gpio-macsmc.c +F: drivers/hid/apple-dockchannel-hid/* F: drivers/hwmon/macsmc-hwmon.c F: drivers/pmdomain/apple/ F: drivers/i2c/busses/i2c-pasemi-core.c