[PATCH 08/10] HID: apple: Add DockChannel HID transport driver

Michael Reeves via B4 Relay devnull+michael.reeves077.gmail.com at kernel.org
Tue Jun 30 05:54:36 PDT 2026


From: Michael Reeves <michael.reeves077 at gmail.com>

Apple MTP exposes internal keyboard and trackpad interfaces over a HID
transport carried by DockChannel.

Add a transport driver that boots the MTP RTKit coprocessor, exchanges
HID packets through the DockChannel mailbox, and registers child HID
interfaces from devicetree.

Co-developed-by: Hector Martin <marcan at marcan.st>
Signed-off-by: Hector Martin <marcan at marcan.st>
Signed-off-by: Michael Reeves <michael.reeves077 at gmail.com>
---
 MAINTAINERS                         |    1 +
 drivers/hid/Kconfig                 |    2 +
 drivers/hid/Makefile                |    2 +
 drivers/hid/dockchannel/Kconfig     |   15 +
 drivers/hid/dockchannel/Makefile    |    3 +
 drivers/hid/dockchannel/apple-hid.c | 1130 +++++++++++++++++++++++++++++++++++
 6 files changed, 1153 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index ed68452c0ad6..0063276f0349 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2620,6 +2620,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/dockchannel/
 F:	drivers/hwmon/macsmc-hwmon.c
 F:	drivers/pmdomain/apple/
 F:	drivers/i2c/busses/i2c-pasemi-core.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index f9bcaeb66385..f27cda601ede 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1488,6 +1488,8 @@ source "drivers/hid/surface-hid/Kconfig"
 
 source "drivers/hid/intel-thc-hid/Kconfig"
 
+source "drivers/hid/dockchannel/Kconfig"
+
 endif # HID
 
 # USB support may be used with HID disabled
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 23e6e3dd0c56..c9b4b1aff247 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -182,3 +182,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)	+= dockchannel/
diff --git a/drivers/hid/dockchannel/Kconfig b/drivers/hid/dockchannel/Kconfig
new file mode 100644
index 000000000000..fca09ef74403
--- /dev/null
+++ b/drivers/hid/dockchannel/Kconfig
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+
+config APPLE_DOCKCHANNEL_HID
+	tristate "HID over Apple DockChannel"
+	depends on APPLE_DOCKCHANNEL
+	depends on APPLE_RTKIT
+	depends on HID
+	depends on INPUT
+	depends on OF
+	help
+	  This provides a HID transport layer over the Apple DockChannel
+	  mailbox interface. It 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 Apple MacBook.
diff --git a/drivers/hid/dockchannel/Makefile b/drivers/hid/dockchannel/Makefile
new file mode 100644
index 000000000000..d1a82aa57a69
--- /dev/null
+++ b/drivers/hid/dockchannel/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+
+obj-$(CONFIG_APPLE_DOCKCHANNEL_HID)	+= apple-hid.o
diff --git a/drivers/hid/dockchannel/apple-hid.c b/drivers/hid/dockchannel/apple-hid.c
new file mode 100644
index 000000000000..162fcfb5ab1c
--- /dev/null
+++ b/drivers/hid/dockchannel/apple-hid.c
@@ -0,0 +1,1130 @@
+// SPDX-License-Identifier: GPL-2.0-only OR MIT
+/*
+ * Apple DockChannel HID transport driver
+ *
+ * Copyright The Asahi Linux Contributors
+ */
+
+#include <linux/bitfield.h>
+#include <linux/completion.h>
+#include <linux/ctype.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/hid.h>
+#include <linux/mailbox/apple-dockchannel.h>
+#include <linux/mailbox_client.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/soc/apple/rtkit.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#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
+
+#define DCHID_MAX_PAYLOAD	0xffff
+#define DCHID_CHECKSUM_LEN	4
+#define DCHID_RX_BUF_SIZE	(sizeof(struct dchid_hdr) + DCHID_MAX_PAYLOAD + \
+				 DCHID_CHECKSUM_LEN)
+
+#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
+
+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;
+	/* Serial follows, but we grab it with a different report. */
+} __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 mbox_client dc_mbox_client;
+	struct mbox_chan *dc_mbox;
+
+	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];
+
+	u8 *rx_buf;
+	size_t rx_len;
+
+	struct dchid_iface *comm;
+	struct mutex ifaces_lock; /* protects ifaces array */
+	struct dchid_iface *ifaces[MAX_INTERFACES];
+
+	/* Workqueue to asynchronously create HID devices */
+	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 void dchid_free_mbox(void *data)
+{
+	mbox_free_channel(data);
+}
+
+static u32 dchid_checksum(const void *data, size_t len)
+{
+	const u8 *p = data;
+	u32 sum = 0;
+	int i;
+
+	while (len >= sizeof(u32)) {
+		sum += get_unaligned_le32(p);
+		p += sizeof(u32);
+		len -= sizeof(u32);
+	}
+
+	if (len) {
+		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);
+
+	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")) {
+		/* Comm is not a HID subdevice */
+		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;
+}
+
+static int dchid_send(struct dchid_iface *iface, u32 flags, const void *msg,
+		      size_t size)
+{
+	struct dchid_dev *dchid = iface->dchid;
+	size_t payload_padded = round_up(size, sizeof(u32));
+	size_t total_len = sizeof(struct dchid_hdr) + sizeof(struct dchid_subhdr) +
+			   payload_padded + DCHID_CHECKSUM_LEN;
+	struct apple_dockchannel_msg dc_msg;
+	struct dchid_hdr *hdr;
+	struct dchid_subhdr *sub;
+	u32 *checksum_ptr;
+	u8 *buf;
+	int ret;
+
+	if (total_len > DCHID_RX_BUF_SIZE)
+		return -EINVAL;
+
+	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 - DCHID_CHECKSUM_LEN);
+
+	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 - DCHID_CHECKSUM_LEN);
+
+	dc_msg.data = buf;
+	dc_msg.len = total_len;
+	ret = mbox_send_message(dchid->dc_mbox, &dc_msg);
+	kfree(buf);
+
+	return ret < 0 ? ret : 0;
+}
+
+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->retcode = 0;
+	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) {
+		ret = -ETIMEDOUT;
+	} else if (iface->retcode) {
+		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 {
+		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;
+	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);
+}
+
+/* Note: buf excludes report number. */
+static int dchid_get_report_cmd(struct dchid_iface *iface, u8 reportnum,
+				void *buf, size_t len)
+{
+	int ret;
+
+	ret = dchid_cmd(iface, HID_FEATURE_REPORT, REQ_GET_REPORT, &reportnum, 1,
+			buf, len);
+
+	return ret <= 0 ? ret : ret - 1;
+}
+
+/* Note: buf includes report number. */
+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:
+		if (len < 1)
+			return -EINVAL;
+
+		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;
+	char cap_name[16];
+	int ret;
+
+	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);
+	hid->type = HID_TYPE_OTHER;
+	if (!strcmp(iface->name, "keyboard")) {
+		u32 country_code;
+
+		hid->group = HID_GROUP_APPLE_DOCKCHANNEL;
+
+		/*
+		 * The device provides no reliable way to get the keyboard
+		 * country code, so board devicetrees provide it instead,
+		 * filled by the bootloader.
+		 */
+		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)
+{
+	u8 *rdesc;
+	int i;
+
+	if (iface->hid)
+		return;
+
+	rdesc = devm_kmemdup(iface->dchid->dev, hid_desc, desc_len,
+			     GFP_KERNEL);
+	if (!rdesc)
+		return;
+
+	/* Fix up oversized report sizes in DockChannel report descriptors */
+	if (desc_len >= 5) {
+		for (i = 0; i <= (int)desc_len - 5; i++) {
+			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) {
+					dev_info(iface->dchid->dev,
+						 "fixing up interface %s (%d) report size\n",
+						 iface->name, iface->index);
+					rdesc[i] = 0x75;
+					rdesc[i + 1] = 0x08;
+					rdesc[i + 2] = 0x96;
+					rdesc[i + 3] = 0x00;
+					rdesc[i + 4] = count * 8;
+				}
+			}
+		}
+	}
+
+	iface->hid_desc = rdesc;
+	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;
+	int 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);
+
+	/* When STM is ready, grab global device info */
+	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)) {
+			dev_warn(iface->dchid->dev, "failed to get device ID from STM\n");
+			/* Fake it and keep going. Things might still work. */
+			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) {
+			dev_warn(iface->dchid->dev, "failed to get serial from STM\n");
+			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_init_block_hdr *blk;
+	struct dchid_iface *iface;
+	u8 *p = data;
+
+	if (length < sizeof(*hdr))
+		return;
+
+	iface = dchid_get_interface(dchid, hdr->iface, hdr->name);
+	if (!iface)
+		return;
+
+	p += sizeof(*hdr);
+	length -= sizeof(*hdr);
+
+	while (length >= sizeof(*blk)) {
+		u16 blk_len;
+
+		blk = (struct dchid_init_block_hdr *)p;
+		p += 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, p, blk_len);
+			break;
+		case INIT_PRODUCT_NAME:
+			if (blk_len > 0 && p[blk_len - 1] != 0)
+				dev_warn(dchid->dev, "unterminated product name for %s\n",
+					 iface->name);
+			break;
+		}
+
+		p += blk_len;
+		length -= blk_len;
+
+		if (le16_to_cpu(blk->type) == INIT_TERMINATOR)
+			break;
+	}
+
+	if (hdr->more_packets)
+		return;
+
+	/*
+	 * Prefer to enable STM first, since it provides device IDs. Some
+	 * firmware versions do not expose STM, so let the keyboard start
+	 * without it.
+	 */
+	if (iface->dchid->id_ready || !strcmp(iface->name, "stm") ||
+	    !strcmp(iface->name, "keyboard"))
+		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;
+
+	if (!length)
+		return;
+
+	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;
+	u16 hdr_len = le16_to_cpu(work->hdr.length);
+	u16 sub_len;
+	int type;
+	u8 *payload;
+
+	if (hdr_len < sizeof(*shdr)) {
+		dev_err(dchid->dev, "bad subheader length\n");
+		goto done;
+	}
+
+	sub_len = le16_to_cpu(shdr->length);
+	if (sub_len > hdr_len - sizeof(*shdr)) {
+		dev_err(dchid->dev, "bad subheader length\n");
+		goto done;
+	}
+
+	type = FIELD_GET(FLAGS_GROUP, shdr->flags);
+	payload = work->data + sizeof(*shdr);
+
+	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 = data;
+	u8 *payload = data + sizeof(*shdr);
+	u16 hdr_len = le16_to_cpu(hdr->length);
+	u16 sub_len = le16_to_cpu(shdr->length);
+	unsigned long flags;
+	bool complete_cmd = false;
+
+	if (hdr_len < sizeof(*shdr) || sub_len > hdr_len - sizeof(*shdr) ||
+	    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_process_packet(struct dchid_dev *dchid, struct dchid_hdr *hdr,
+				 u8 *payload, size_t payload_len, u8 *packet,
+				 size_t packet_len)
+{
+	struct dchid_work *work;
+
+	if (dchid_checksum(packet, packet_len) != DCHID_CHECKSUM_SEED) {
+		dev_err_ratelimited(dchid->dev, "checksum error\n");
+		return;
+	}
+
+	if (payload_len < sizeof(struct dchid_subhdr))
+		return;
+
+	if (hdr->iface >= MAX_INTERFACES || !dchid->ifaces[hdr->iface])
+		return;
+
+	if (hdr->channel == DCHID_CHANNEL_CMD) {
+		dchid_handle_ack(dchid->ifaces[hdr->iface], hdr, payload);
+		return;
+	}
+
+	if (hdr->channel != DCHID_CHANNEL_REPORT)
+		return;
+
+	work = kzalloc(sizeof(*work) + payload_len, GFP_ATOMIC);
+	if (!work)
+		return;
+
+	work->hdr = *hdr;
+	work->iface = dchid->ifaces[hdr->iface];
+	memcpy(work->data, payload, payload_len);
+	INIT_WORK(&work->work, dchid_packet_work);
+
+	queue_work(work->iface->wq, &work->work);
+}
+
+static void dchid_consume_rx(struct dchid_dev *dchid)
+{
+	while (dchid->rx_len >= sizeof(struct dchid_hdr)) {
+		struct dchid_hdr *hdr = (struct dchid_hdr *)dchid->rx_buf;
+		size_t payload_len;
+		size_t packet_len;
+
+		if (hdr->hdr_len != sizeof(*hdr)) {
+			dev_err_ratelimited(dchid->dev, "bad header length %u\n",
+					    hdr->hdr_len);
+			dchid->rx_len = 0;
+			return;
+		}
+
+		payload_len = le16_to_cpu(hdr->length);
+		packet_len = sizeof(*hdr) + payload_len + DCHID_CHECKSUM_LEN;
+		if (packet_len > DCHID_RX_BUF_SIZE) {
+			dev_err_ratelimited(dchid->dev, "oversized packet %zu\n",
+					    packet_len);
+			dchid->rx_len = 0;
+			return;
+		}
+
+		if (dchid->rx_len < packet_len)
+			return;
+
+		dchid_process_packet(dchid, hdr, dchid->rx_buf + sizeof(*hdr),
+				     payload_len, dchid->rx_buf, packet_len);
+
+		dchid->rx_len -= packet_len;
+		memmove(dchid->rx_buf, dchid->rx_buf + packet_len, dchid->rx_len);
+	}
+}
+
+static void dchid_rx_callback(struct mbox_client *cl, void *mssg)
+{
+	struct dchid_dev *dchid = container_of(cl, struct dchid_dev, dc_mbox_client);
+	struct apple_dockchannel_msg *msg = mssg;
+
+	if (!msg || !msg->data || !msg->len)
+		return;
+
+	if (msg->len > DCHID_RX_BUF_SIZE - dchid->rx_len) {
+		dev_err_ratelimited(dchid->dev, "RX buffer overflow\n");
+		dchid->rx_len = 0;
+		return;
+	}
+
+	memcpy(dchid->rx_buf + dchid->rx_len, msg->data, msg->len);
+	dchid->rx_len += msg->len;
+
+	dchid_consume_rx(dchid);
+}
+
+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;
+
+	dchid->asc_base = devm_platform_ioremap_resource_byname(pdev, "coproc-asc");
+	if (IS_ERR(dchid->asc_base))
+		return PTR_ERR(dchid->asc_base);
+
+	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "coproc-sram");
+	if (!res)
+		return -EINVAL;
+
+	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->rx_buf = devm_kmalloc(dev, DCHID_RX_BUF_SIZE, GFP_KERNEL);
+	if (!dchid->rx_buf)
+		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->dc_mbox_client.dev = dev;
+	dchid->dc_mbox_client.tx_block = true;
+	dchid->dc_mbox_client.rx_callback = dchid_rx_callback;
+
+	dchid->dc_mbox = mbox_request_channel_byname(&dchid->dc_mbox_client,
+						     "dockchannel");
+	if (IS_ERR(dchid->dc_mbox))
+		return dev_err_probe(dev, PTR_ERR(dchid->dc_mbox),
+				     "failed to request DockChannel mailbox\n");
+
+	ret = devm_add_action_or_reset(dev, dchid_free_mbox, dchid->dc_mbox);
+	if (ret)
+		return ret;
+
+	dchid->rtk = devm_apple_rtkit_init(dev, dchid, "asc", 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 coprocessor\n");
+
+	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");
+
+	return 0;
+}
+
+static void dchid_remove(struct platform_device *pdev)
+{
+	struct dchid_dev *dchid = platform_get_drvdata(pdev);
+	int i;
+
+	if (dchid->dc_mbox) {
+		devm_release_action(&pdev->dev, dchid_free_mbox, dchid->dc_mbox);
+		dchid->dc_mbox = NULL;
+	}
+
+	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);
+
+	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,t8122-dockchannel-hid" },
+	{ .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 <marcan at marcan.st>");
+MODULE_AUTHOR("Michael Reeves <michael.reeves077 at gmail.com>");
+MODULE_LICENSE("Dual MIT/GPL");

-- 
2.51.2





More information about the linux-arm-kernel mailing list