T:     git git://git.kernel.org/pub/scm/linux/kernel/git/pdx86/platform-drivers-x86.git
 F:     drivers/platform/surface/
 
+MICROSOFT SURFACE HID TRANSPORT DRIVER
+M:     Maximilian Luz <luzmaximilian@gmail.com>
+L:     linux-input@vger.kernel.org
+L:     platform-driver-x86@vger.kernel.org
+S:     Maintained
+F:     drivers/hid/surface-hid/
+
 MICROSOFT SURFACE HOT-PLUG DRIVER
 M:     Maximilian Luz <luzmaximilian@gmail.com>
 L:     platform-driver-x86@vger.kernel.org
 
 
 source "drivers/hid/amd-sfh-hid/Kconfig"
 
+source "drivers/hid/surface-hid/Kconfig"
+
 endmenu
 
 obj-$(INTEL_ISH_FIRMWARE_DOWNLOADER)   += intel-ish-hid/
 
 obj-$(CONFIG_AMD_SFH_HID)       += amd-sfh-hid/
+
+obj-$(CONFIG_SURFACE_HID_CORE)  += surface-hid/
 
--- /dev/null
+# SPDX-License-Identifier: GPL-2.0+
+menu "Surface System Aggregator Module HID support"
+       depends on SURFACE_AGGREGATOR
+       depends on INPUT
+
+config SURFACE_HID
+       tristate "HID transport driver for Surface System Aggregator Module"
+       depends on SURFACE_AGGREGATOR_REGISTRY
+       select SURFACE_HID_CORE
+       help
+         Driver to support integrated HID devices on newer Microsoft Surface
+         models.
+
+         This driver provides support for the HID transport protocol provided
+         by the Surface Aggregator Module (i.e. the embedded controller) on
+         7th-generation Microsoft Surface devices, i.e. Surface Book 3 and
+         Surface Laptop 3. On those models, it is mainly used to connect the
+         integrated touchpad and keyboard.
+
+         Say M or Y here, if you want support for integrated HID devices, i.e.
+         integrated touchpad and keyboard, on 7th generation Microsoft Surface
+         models.
+
+endmenu
+
+config SURFACE_HID_CORE
+       tristate
+       select HID
 
--- /dev/null
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Makefile - Surface System Aggregator Module (SSAM) HID transport driver.
+#
+obj-$(CONFIG_SURFACE_HID_CORE) += surface_hid_core.o
+obj-$(CONFIG_SURFACE_HID)      += surface_hid.o
 
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Surface System Aggregator Module (SSAM) HID transport driver for the
+ * generic HID interface (HID/TC=0x15 subsystem). Provides support for
+ * integrated HID devices on Surface Laptop 3, Book 3, and later.
+ *
+ * Copyright (C) 2019-2021 Blaž Hrastnik <blaz@mxxn.io>,
+ *                         Maximilian Luz <luzmaximilian@gmail.com>
+ */
+
+#include <asm/unaligned.h>
+#include <linux/hid.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/types.h>
+
+#include <linux/surface_aggregator/controller.h>
+#include <linux/surface_aggregator/device.h>
+
+#include "surface_hid_core.h"
+
+
+/* -- SAM interface. -------------------------------------------------------- */
+
+struct surface_hid_buffer_slice {
+       __u8 entry;
+       __le32 offset;
+       __le32 length;
+       __u8 end;
+       __u8 data[];
+} __packed;
+
+static_assert(sizeof(struct surface_hid_buffer_slice) == 10);
+
+enum surface_hid_cid {
+       SURFACE_HID_CID_OUTPUT_REPORT      = 0x01,
+       SURFACE_HID_CID_GET_FEATURE_REPORT = 0x02,
+       SURFACE_HID_CID_SET_FEATURE_REPORT = 0x03,
+       SURFACE_HID_CID_GET_DESCRIPTOR     = 0x04,
+};
+
+static int ssam_hid_get_descriptor(struct surface_hid_device *shid, u8 entry, u8 *buf, size_t len)
+{
+       u8 buffer[sizeof(struct surface_hid_buffer_slice) + 0x76];
+       struct surface_hid_buffer_slice *slice;
+       struct ssam_request rqst;
+       struct ssam_response rsp;
+       u32 buffer_len, offset, length;
+       int status;
+
+       /*
+        * Note: The 0x76 above has been chosen because that's what's used by
+        * the Windows driver. Together with the header, this leads to a 128
+        * byte payload in total.
+        */
+
+       buffer_len = ARRAY_SIZE(buffer) - sizeof(struct surface_hid_buffer_slice);
+
+       rqst.target_category = shid->uid.category;
+       rqst.target_id = shid->uid.target;
+       rqst.command_id = SURFACE_HID_CID_GET_DESCRIPTOR;
+       rqst.instance_id = shid->uid.instance;
+       rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
+       rqst.length = sizeof(struct surface_hid_buffer_slice);
+       rqst.payload = buffer;
+
+       rsp.capacity = ARRAY_SIZE(buffer);
+       rsp.pointer = buffer;
+
+       slice = (struct surface_hid_buffer_slice *)buffer;
+       slice->entry = entry;
+       slice->end = 0;
+
+       offset = 0;
+       length = buffer_len;
+
+       while (!slice->end && offset < len) {
+               put_unaligned_le32(offset, &slice->offset);
+               put_unaligned_le32(length, &slice->length);
+
+               rsp.length = 0;
+
+               status = ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp,
+                                   sizeof(*slice));
+               if (status)
+                       return status;
+
+               offset = get_unaligned_le32(&slice->offset);
+               length = get_unaligned_le32(&slice->length);
+
+               /* Don't mess stuff up in case we receive garbage. */
+               if (length > buffer_len || offset > len)
+                       return -EPROTO;
+
+               if (offset + length > len)
+                       length = len - offset;
+
+               memcpy(buf + offset, &slice->data[0], length);
+
+               offset += length;
+               length = buffer_len;
+       }
+
+       if (offset != len) {
+               dev_err(shid->dev, "unexpected descriptor length: got %u, expected %zu\n",
+                       offset, len);
+               return -EPROTO;
+       }
+
+       return 0;
+}
+
+static int ssam_hid_set_raw_report(struct surface_hid_device *shid, u8 rprt_id, bool feature,
+                                  u8 *buf, size_t len)
+{
+       struct ssam_request rqst;
+       u8 cid;
+
+       if (feature)
+               cid = SURFACE_HID_CID_SET_FEATURE_REPORT;
+       else
+               cid = SURFACE_HID_CID_OUTPUT_REPORT;
+
+       rqst.target_category = shid->uid.category;
+       rqst.target_id = shid->uid.target;
+       rqst.instance_id = shid->uid.instance;
+       rqst.command_id = cid;
+       rqst.flags = 0;
+       rqst.length = len;
+       rqst.payload = buf;
+
+       buf[0] = rprt_id;
+
+       return ssam_retry(ssam_request_sync, shid->ctrl, &rqst, NULL);
+}
+
+static int ssam_hid_get_raw_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
+{
+       struct ssam_request rqst;
+       struct ssam_response rsp;
+
+       rqst.target_category = shid->uid.category;
+       rqst.target_id = shid->uid.target;
+       rqst.instance_id = shid->uid.instance;
+       rqst.command_id = SURFACE_HID_CID_GET_FEATURE_REPORT;
+       rqst.flags = 0;
+       rqst.length = sizeof(rprt_id);
+       rqst.payload = &rprt_id;
+
+       rsp.capacity = len;
+       rsp.length = 0;
+       rsp.pointer = buf;
+
+       return ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(rprt_id));
+}
+
+static u32 ssam_hid_event_fn(struct ssam_event_notifier *nf, const struct ssam_event *event)
+{
+       struct surface_hid_device *shid = container_of(nf, struct surface_hid_device, notif);
+
+       if (event->command_id != 0x00)
+               return 0;
+
+       hid_input_report(shid->hid, HID_INPUT_REPORT, (u8 *)&event->data[0], event->length, 0);
+       return SSAM_NOTIF_HANDLED;
+}
+
+
+/* -- Transport driver. ----------------------------------------------------- */
+
+static int shid_output_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
+{
+       int status;
+
+       status = ssam_hid_set_raw_report(shid, rprt_id, false, buf, len);
+       return status >= 0 ? len : status;
+}
+
+static int shid_get_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
+{
+       int status;
+
+       status = ssam_hid_get_raw_report(shid, rprt_id, buf, len);
+       return status >= 0 ? len : status;
+}
+
+static int shid_set_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
+{
+       int status;
+
+       status = ssam_hid_set_raw_report(shid, rprt_id, true, buf, len);
+       return status >= 0 ? len : status;
+}
+
+
+/* -- Driver setup. --------------------------------------------------------- */
+
+static int surface_hid_probe(struct ssam_device *sdev)
+{
+       struct surface_hid_device *shid;
+
+       shid = devm_kzalloc(&sdev->dev, sizeof(*shid), GFP_KERNEL);
+       if (!shid)
+               return -ENOMEM;
+
+       shid->dev = &sdev->dev;
+       shid->ctrl = sdev->ctrl;
+       shid->uid = sdev->uid;
+
+       shid->notif.base.priority = 1;
+       shid->notif.base.fn = ssam_hid_event_fn;
+       shid->notif.event.reg = SSAM_EVENT_REGISTRY_REG;
+       shid->notif.event.id.target_category = sdev->uid.category;
+       shid->notif.event.id.instance = sdev->uid.instance;
+       shid->notif.event.mask = SSAM_EVENT_MASK_STRICT;
+       shid->notif.event.flags = 0;
+
+       shid->ops.get_descriptor = ssam_hid_get_descriptor;
+       shid->ops.output_report = shid_output_report;
+       shid->ops.get_feature_report = shid_get_feature_report;
+       shid->ops.set_feature_report = shid_set_feature_report;
+
+       ssam_device_set_drvdata(sdev, shid);
+       return surface_hid_device_add(shid);
+}
+
+static void surface_hid_remove(struct ssam_device *sdev)
+{
+       surface_hid_device_destroy(ssam_device_get_drvdata(sdev));
+}
+
+static const struct ssam_device_id surface_hid_match[] = {
+       { SSAM_SDEV(HID, 0x02, SSAM_ANY_IID, 0x00) },
+       { },
+};
+MODULE_DEVICE_TABLE(ssam, surface_hid_match);
+
+static struct ssam_device_driver surface_hid_driver = {
+       .probe = surface_hid_probe,
+       .remove = surface_hid_remove,
+       .match_table = surface_hid_match,
+       .driver = {
+               .name = "surface_hid",
+               .pm = &surface_hid_pm_ops,
+               .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+       },
+};
+module_ssam_device_driver(surface_hid_driver);
+
+MODULE_AUTHOR("Blaž Hrastnik <blaz@mxxn.io>");
+MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
+MODULE_DESCRIPTION("HID transport driver for Surface System Aggregator Module");
+MODULE_LICENSE("GPL");
 
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Common/core components for the Surface System Aggregator Module (SSAM) HID
+ * transport driver. Provides support for integrated HID devices on Microsoft
+ * Surface models.
+ *
+ * Copyright (C) 2019-2021 Maximilian Luz <luzmaximilian@gmail.com>
+ */
+
+#include <asm/unaligned.h>
+#include <linux/hid.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/types.h>
+#include <linux/usb/ch9.h>
+
+#include <linux/surface_aggregator/controller.h>
+
+#include "surface_hid_core.h"
+
+
+/* -- Device descriptor access. --------------------------------------------- */
+
+static int surface_hid_load_hid_descriptor(struct surface_hid_device *shid)
+{
+       int status;
+
+       status = shid->ops.get_descriptor(shid, SURFACE_HID_DESC_HID,
+                       (u8 *)&shid->hid_desc, sizeof(shid->hid_desc));
+       if (status)
+               return status;
+
+       if (shid->hid_desc.desc_len != sizeof(shid->hid_desc)) {
+               dev_err(shid->dev, "unexpected HID descriptor length: got %u, expected %zu\n",
+                       shid->hid_desc.desc_len, sizeof(shid->hid_desc));
+               return -EPROTO;
+       }
+
+       if (shid->hid_desc.desc_type != HID_DT_HID) {
+               dev_err(shid->dev, "unexpected HID descriptor type: got %#04x, expected %#04x\n",
+                       shid->hid_desc.desc_type, HID_DT_HID);
+               return -EPROTO;
+       }
+
+       if (shid->hid_desc.num_descriptors != 1) {
+               dev_err(shid->dev, "unexpected number of descriptors: got %u, expected 1\n",
+                       shid->hid_desc.num_descriptors);
+               return -EPROTO;
+       }
+
+       if (shid->hid_desc.report_desc_type != HID_DT_REPORT) {
+               dev_err(shid->dev, "unexpected report descriptor type: got %#04x, expected %#04x\n",
+                       shid->hid_desc.report_desc_type, HID_DT_REPORT);
+               return -EPROTO;
+       }
+
+       return 0;
+}
+
+static int surface_hid_load_device_attributes(struct surface_hid_device *shid)
+{
+       int status;
+
+       status = shid->ops.get_descriptor(shid, SURFACE_HID_DESC_ATTRS,
+                       (u8 *)&shid->attrs, sizeof(shid->attrs));
+       if (status)
+               return status;
+
+       if (get_unaligned_le32(&shid->attrs.length) != sizeof(shid->attrs)) {
+               dev_err(shid->dev, "unexpected attribute length: got %u, expected %zu\n",
+                       get_unaligned_le32(&shid->attrs.length), sizeof(shid->attrs));
+               return -EPROTO;
+       }
+
+       return 0;
+}
+
+
+/* -- Transport driver (common). -------------------------------------------- */
+
+static int surface_hid_start(struct hid_device *hid)
+{
+       struct surface_hid_device *shid = hid->driver_data;
+
+       return ssam_notifier_register(shid->ctrl, &shid->notif);
+}
+
+static void surface_hid_stop(struct hid_device *hid)
+{
+       struct surface_hid_device *shid = hid->driver_data;
+
+       /* Note: This call will log errors for us, so ignore them here. */
+       ssam_notifier_unregister(shid->ctrl, &shid->notif);
+}
+
+static int surface_hid_open(struct hid_device *hid)
+{
+       return 0;
+}
+
+static void surface_hid_close(struct hid_device *hid)
+{
+}
+
+static int surface_hid_parse(struct hid_device *hid)
+{
+       struct surface_hid_device *shid = hid->driver_data;
+       size_t len = get_unaligned_le16(&shid->hid_desc.report_desc_len);
+       u8 *buf;
+       int status;
+
+       buf = kzalloc(len, GFP_KERNEL);
+       if (!buf)
+               return -ENOMEM;
+
+       status = shid->ops.get_descriptor(shid, SURFACE_HID_DESC_REPORT, buf, len);
+       if (!status)
+               status = hid_parse_report(hid, buf, len);
+
+       kfree(buf);
+       return status;
+}
+
+static int surface_hid_raw_request(struct hid_device *hid, unsigned char reportnum, u8 *buf,
+                                  size_t len, unsigned char rtype, int reqtype)
+{
+       struct surface_hid_device *shid = hid->driver_data;
+
+       if (rtype == HID_OUTPUT_REPORT && reqtype == HID_REQ_SET_REPORT)
+               return shid->ops.output_report(shid, reportnum, buf, len);
+
+       else if (rtype == HID_FEATURE_REPORT && reqtype == HID_REQ_GET_REPORT)
+               return shid->ops.get_feature_report(shid, reportnum, buf, len);
+
+       else if (rtype == HID_FEATURE_REPORT && reqtype == HID_REQ_SET_REPORT)
+               return shid->ops.set_feature_report(shid, reportnum, buf, len);
+
+       return -EIO;
+}
+
+static struct hid_ll_driver surface_hid_ll_driver = {
+       .start       = surface_hid_start,
+       .stop        = surface_hid_stop,
+       .open        = surface_hid_open,
+       .close       = surface_hid_close,
+       .parse       = surface_hid_parse,
+       .raw_request = surface_hid_raw_request,
+};
+
+
+/* -- Common device setup. -------------------------------------------------- */
+
+int surface_hid_device_add(struct surface_hid_device *shid)
+{
+       int status;
+
+       status = surface_hid_load_hid_descriptor(shid);
+       if (status)
+               return status;
+
+       status = surface_hid_load_device_attributes(shid);
+       if (status)
+               return status;
+
+       shid->hid = hid_allocate_device();
+       if (IS_ERR(shid->hid))
+               return PTR_ERR(shid->hid);
+
+       shid->hid->dev.parent = shid->dev;
+       shid->hid->bus = BUS_HOST;
+       shid->hid->vendor = cpu_to_le16(shid->attrs.vendor);
+       shid->hid->product = cpu_to_le16(shid->attrs.product);
+       shid->hid->version = cpu_to_le16(shid->hid_desc.hid_version);
+       shid->hid->country = shid->hid_desc.country_code;
+
+       snprintf(shid->hid->name, sizeof(shid->hid->name), "Microsoft Surface %04X:%04X",
+                shid->hid->vendor, shid->hid->product);
+
+       strscpy(shid->hid->phys, dev_name(shid->dev), sizeof(shid->hid->phys));
+
+       shid->hid->driver_data = shid;
+       shid->hid->ll_driver = &surface_hid_ll_driver;
+
+       status = hid_add_device(shid->hid);
+       if (status)
+               hid_destroy_device(shid->hid);
+
+       return status;
+}
+EXPORT_SYMBOL_GPL(surface_hid_device_add);
+
+void surface_hid_device_destroy(struct surface_hid_device *shid)
+{
+       hid_destroy_device(shid->hid);
+}
+EXPORT_SYMBOL_GPL(surface_hid_device_destroy);
+
+
+/* -- PM ops. --------------------------------------------------------------- */
+
+#ifdef CONFIG_PM_SLEEP
+
+static int surface_hid_suspend(struct device *dev)
+{
+       struct surface_hid_device *d = dev_get_drvdata(dev);
+
+       if (d->hid->driver && d->hid->driver->suspend)
+               return d->hid->driver->suspend(d->hid, PMSG_SUSPEND);
+
+       return 0;
+}
+
+static int surface_hid_resume(struct device *dev)
+{
+       struct surface_hid_device *d = dev_get_drvdata(dev);
+
+       if (d->hid->driver && d->hid->driver->resume)
+               return d->hid->driver->resume(d->hid);
+
+       return 0;
+}
+
+static int surface_hid_freeze(struct device *dev)
+{
+       struct surface_hid_device *d = dev_get_drvdata(dev);
+
+       if (d->hid->driver && d->hid->driver->suspend)
+               return d->hid->driver->suspend(d->hid, PMSG_FREEZE);
+
+       return 0;
+}
+
+static int surface_hid_poweroff(struct device *dev)
+{
+       struct surface_hid_device *d = dev_get_drvdata(dev);
+
+       if (d->hid->driver && d->hid->driver->suspend)
+               return d->hid->driver->suspend(d->hid, PMSG_HIBERNATE);
+
+       return 0;
+}
+
+static int surface_hid_restore(struct device *dev)
+{
+       struct surface_hid_device *d = dev_get_drvdata(dev);
+
+       if (d->hid->driver && d->hid->driver->reset_resume)
+               return d->hid->driver->reset_resume(d->hid);
+
+       return 0;
+}
+
+const struct dev_pm_ops surface_hid_pm_ops = {
+       .freeze   = surface_hid_freeze,
+       .thaw     = surface_hid_resume,
+       .suspend  = surface_hid_suspend,
+       .resume   = surface_hid_resume,
+       .poweroff = surface_hid_poweroff,
+       .restore  = surface_hid_restore,
+};
+EXPORT_SYMBOL_GPL(surface_hid_pm_ops);
+
+#else /* CONFIG_PM_SLEEP */
+
+const struct dev_pm_ops surface_hid_pm_ops = { };
+EXPORT_SYMBOL_GPL(surface_hid_pm_ops);
+
+#endif /* CONFIG_PM_SLEEP */
+
+MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
+MODULE_DESCRIPTION("HID transport driver core for Surface System Aggregator Module");
+MODULE_LICENSE("GPL");
 
--- /dev/null
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Common/core components for the Surface System Aggregator Module (SSAM) HID
+ * transport driver. Provides support for integrated HID devices on Microsoft
+ * Surface models.
+ *
+ * Copyright (C) 2019-2021 Maximilian Luz <luzmaximilian@gmail.com>
+ */
+
+#ifndef SURFACE_HID_CORE_H
+#define SURFACE_HID_CORE_H
+
+#include <linux/hid.h>
+#include <linux/pm.h>
+#include <linux/types.h>
+
+#include <linux/surface_aggregator/controller.h>
+#include <linux/surface_aggregator/device.h>
+
+enum surface_hid_descriptor_entry {
+       SURFACE_HID_DESC_HID    = 0,
+       SURFACE_HID_DESC_REPORT = 1,
+       SURFACE_HID_DESC_ATTRS  = 2,
+};
+
+struct surface_hid_descriptor {
+       __u8 desc_len;                  /* = 9 */
+       __u8 desc_type;                 /* = HID_DT_HID */
+       __le16 hid_version;
+       __u8 country_code;
+       __u8 num_descriptors;           /* = 1 */
+
+       __u8 report_desc_type;          /* = HID_DT_REPORT */
+       __le16 report_desc_len;
+} __packed;
+
+static_assert(sizeof(struct surface_hid_descriptor) == 9);
+
+struct surface_hid_attributes {
+       __le32 length;
+       __le16 vendor;
+       __le16 product;
+       __le16 version;
+       __u8 _unknown[22];
+} __packed;
+
+static_assert(sizeof(struct surface_hid_attributes) == 32);
+
+struct surface_hid_device;
+
+struct surface_hid_device_ops {
+       int (*get_descriptor)(struct surface_hid_device *shid, u8 entry, u8 *buf, size_t len);
+       int (*output_report)(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len);
+       int (*get_feature_report)(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len);
+       int (*set_feature_report)(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len);
+};
+
+struct surface_hid_device {
+       struct device *dev;
+       struct ssam_controller *ctrl;
+       struct ssam_device_uid uid;
+
+       struct surface_hid_descriptor hid_desc;
+       struct surface_hid_attributes attrs;
+
+       struct ssam_event_notifier notif;
+       struct hid_device *hid;
+
+       struct surface_hid_device_ops ops;
+};
+
+int surface_hid_device_add(struct surface_hid_device *shid);
+void surface_hid_device_destroy(struct surface_hid_device *shid);
+
+extern const struct dev_pm_ops surface_hid_pm_ops;
+
+#endif /* SURFACE_HID_CORE_H */