From 5399769982ef375ed621d50894968968400b3d61 Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Mon, 15 Feb 2021 21:30:40 +0100 Subject: [PATCH] Update v5.4 patches Changes: - SAM: - Move and split drivers to closer reflect upstreaming process - Various updates to HID and battery/AC drivers for upstreaming - Hotplug: - various fixes and improvements - GPE: - Fix Kconfig dependency Links: - kernel: https://github.com/linux-surface/kernel/commit/531e6519a892711c74e9aa7ffa3f83cb2050f22f - SAM: https://github.com/linux-surface/surface-aggregator-module/commit/bee2add45fb668a794406ce6a648cece4fd00d51 - GPE: https://github.com/linux-surface/surface-gpe/commit/6ecfdb39050129bf17e5f1fff784e4df7aa56171 - Hotplug: https://github.com/linux-surface/surface-hotplug/commit/595ed62f24417b2ac97f4658cfc30776c9b888cf --- configs/surface-5.4.config | 9 +- patches/5.4/0001-surface3-power.patch | 4 +- patches/5.4/0002-surface3-oemb.patch | 4 +- patches/5.4/0003-wifi.patch | 44 +- patches/5.4/0004-ipts.patch | 12 +- patches/5.4/0005-surface-gpe.patch | 15 +- patches/5.4/0006-surface-sam-over-hid.patch | 12 +- patches/5.4/0007-surface-sam.patch | 10334 +++++++++--------- patches/5.4/0008-surface-hotplug.patch | 160 +- patches/5.4/0009-surface-typecover.patch | 4 +- patches/5.4/0010-surface-sensors.patch | 4 +- 11 files changed, 5391 insertions(+), 5211 deletions(-) diff --git a/configs/surface-5.4.config b/configs/surface-5.4.config index 13fade958..6f4df156f 100644 --- a/configs/surface-5.4.config +++ b/configs/surface-5.4.config @@ -6,12 +6,17 @@ CONFIG_SURFACE_AGGREGATOR_ERROR_INJECTION=n CONFIG_SURFACE_AGGREGATOR_BUS=y CONFIG_SURFACE_AGGREGATOR_CDEV=m CONFIG_SURFACE_AGGREGATOR_REGISTRY=m + CONFIG_SURFACE_ACPI_NOTIFY=m -CONFIG_SURFACE_BATTERY=m CONFIG_SURFACE_DTX=m -CONFIG_SURFACE_HID=m CONFIG_SURFACE_PERFMODE=m +CONFIG_SURFACE_HID=m +CONFIG_SURFACE_KBD=m + +CONFIG_BATTERY_SURFACE=m +CONFIG_CHARGER_SURFACE=m + # # Surface Hotplug # diff --git a/patches/5.4/0001-surface3-power.patch b/patches/5.4/0001-surface3-power.patch index 99d8ec502..4732f5558 100644 --- a/patches/5.4/0001-surface3-power.patch +++ b/patches/5.4/0001-surface3-power.patch @@ -1,4 +1,4 @@ -From 97796e7d29cf461df923c61fe70f6ca236c4997f Mon Sep 17 00:00:00 2001 +From 5848e2fba8c859e7a4cc8e7663fb5371997770f5 Mon Sep 17 00:00:00 2001 From: qzed Date: Tue, 17 Sep 2019 17:17:56 +0200 Subject: [PATCH] platform/x86: Surface 3 battery platform operation region @@ -653,5 +653,5 @@ index 000000000000..e0af01a60302 +MODULE_DESCRIPTION("mshw0011 driver"); +MODULE_LICENSE("GPL v2"); -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0002-surface3-oemb.patch b/patches/5.4/0002-surface3-oemb.patch index 1f001edb9..a67f3c187 100644 --- a/patches/5.4/0002-surface3-oemb.patch +++ b/patches/5.4/0002-surface3-oemb.patch @@ -1,4 +1,4 @@ -From 37fd7de62d1aa0a910f97dc4eb37cecb7c28393e Mon Sep 17 00:00:00 2001 +From b2d676c08fd273e13731ce67d2836855ba6ebf0f Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Sun, 18 Oct 2020 16:42:44 +0900 Subject: [PATCH] (surface3-oemb) add DMI matches for Surface 3 with broken DMI @@ -97,5 +97,5 @@ index d0fb43c2b9f6..0e938713cb13 100644 }; -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0003-wifi.patch b/patches/5.4/0003-wifi.patch index c2fd3dd69..adce1d358 100644 --- a/patches/5.4/0003-wifi.patch +++ b/patches/5.4/0003-wifi.patch @@ -1,4 +1,4 @@ -From 06e17723ae88ae7030681b499b965eeffb7337a5 Mon Sep 17 00:00:00 2001 +From 582e2708b267722ff0b91a71307f321e2c840159 Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Thu, 24 Sep 2020 18:02:06 +0900 Subject: [PATCH] mwifiex: pcie: skip cancel_work_sync() on reset failure path @@ -154,9 +154,9 @@ index f7ce9b6db6b4..72d0c01ff359 100644 static inline int -- -2.30.0 +2.30.1 -From d27c246316c2c8d8615b43fc68e1a620d1998940 Mon Sep 17 00:00:00 2001 +From ab13f94d3cde1dd9e1e86546ed6469f76018207b Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Mon, 28 Sep 2020 17:46:49 +0900 Subject: [PATCH] mwifiex: pcie: add DMI-based quirk impl for Surface devices @@ -362,9 +362,9 @@ index 000000000000..5326ae7e5671 + +void mwifiex_initialize_quirks(struct pcie_service_card *card); -- -2.30.0 +2.30.1 -From c2ac862bd2dd1183e158a76950500a7560218987 Mon Sep 17 00:00:00 2001 +From 90eedd60f6dfb6578f0f9c09520b6169adc87d74 Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Tue, 29 Sep 2020 17:25:22 +0900 Subject: [PATCH] mwifiex: pcie: add reset_d3cold quirk for Surface gen4+ @@ -563,9 +563,9 @@ index 5326ae7e5671..8b9dcb5070d8 100644 void mwifiex_initialize_quirks(struct pcie_service_card *card); +int mwifiex_pcie_reset_d3cold_quirk(struct pci_dev *pdev); -- -2.30.0 +2.30.1 -From b8ae7bac392c8a965e1d4d2d6d1ef550597ef47f Mon Sep 17 00:00:00 2001 +From d4c3664053583b97dbcdeea4dc37da854215f471 Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Tue, 29 Sep 2020 17:32:22 +0900 Subject: [PATCH] mwifiex: pcie: add reset_wsid quirk for Surface 3 @@ -742,9 +742,9 @@ index 8b9dcb5070d8..3ef7440418e3 100644 int mwifiex_pcie_reset_d3cold_quirk(struct pci_dev *pdev); +int mwifiex_pcie_reset_wsid_quirk(struct pci_dev *pdev); -- -2.30.0 +2.30.1 -From e9db9491f97d658cf5c000a4ab07f1272e3bee59 Mon Sep 17 00:00:00 2001 +From ff7424694a69509c694c0ff5a968e6e43fe9315a Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Wed, 30 Sep 2020 18:08:24 +0900 Subject: [PATCH] mwifiex: pcie: (OEMB) add quirk for Surface 3 with broken DMI @@ -804,9 +804,9 @@ index f0a6fa0a7ae5..34dcd84f02a6 100644 .ident = "Surface Pro 3", .matches = { -- -2.30.0 +2.30.1 -From 053f2f3cc88d60a2978004704395dc8c914671fe Mon Sep 17 00:00:00 2001 +From 861079133a6881425e234bdc62da525f37c60386 Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Thu, 24 Sep 2020 01:56:34 +0900 Subject: [PATCH] mwifiex: pcie: use shutdown_sw()/reinit_sw() on @@ -946,9 +946,9 @@ index 263d918767bd..bd6791dc3a0f 100644 return 0; } -- -2.30.0 +2.30.1 -From d002bafb6f43d694650f1a910edb3672c8de641f Mon Sep 17 00:00:00 2001 +From e5b5d7b27a53fcff871603a3c7fb4f505d029c00 Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Mon, 24 Aug 2020 17:11:35 +0900 Subject: [PATCH] mwifiex: pcie: add enable_device_dump module parameter @@ -995,9 +995,9 @@ index bd6791dc3a0f..d7ff898c1767 100644 if (!adapter->devdump_data) { mwifiex_dbg(adapter, ERROR, -- -2.30.0 +2.30.1 -From 55b4bf63af39b2422dac63d89253e66a9a87e0f2 Mon Sep 17 00:00:00 2001 +From d27add3beb83d75f4e20559ace0341946533ef17 Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Sun, 4 Oct 2020 00:11:49 +0900 Subject: [PATCH] mwifiex: pcie: disable bridge_d3 for Surface gen4+ @@ -1150,9 +1150,9 @@ index 3ef7440418e3..a95ebac06e13 100644 void mwifiex_initialize_quirks(struct pcie_service_card *card); int mwifiex_pcie_reset_d3cold_quirk(struct pci_dev *pdev); -- -2.30.0 +2.30.1 -From ea3c4820fc14f520366a5d61fb11637a8d09519a Mon Sep 17 00:00:00 2001 +From b19749fd1a6563a4d7d76bcc503b383bd112665d Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Sun, 4 Oct 2020 00:25:48 +0900 Subject: [PATCH] mwifiex: add allow_ps_mode module parameter @@ -1212,9 +1212,9 @@ index 9e6dc289ec3e..20f5ee3fe7e3 100644 } -- -2.30.0 +2.30.1 -From 6282406f43441ff1c45e9f326380f62cfd1154cb Mon Sep 17 00:00:00 2001 +From c86b4bca63e8d57ddab8e484c158e0dde4044e5e Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Sun, 4 Oct 2020 00:38:48 +0900 Subject: [PATCH] mwifiex: print message when changing ps_mode @@ -1247,9 +1247,9 @@ index 20f5ee3fe7e3..8020a2929069 100644 } -- -2.30.0 +2.30.1 -From d68427cdc69ec1683b85f6502be6fd977ef9863d Mon Sep 17 00:00:00 2001 +From 0bffe54a88dab69db359641bec117adf8a8dc7ab Mon Sep 17 00:00:00 2001 From: Tsuchiya Yuto Date: Sun, 4 Oct 2020 00:59:37 +0900 Subject: [PATCH] mwifiex: disable ps_mode explicitly by default instead @@ -1295,5 +1295,5 @@ index 4ed10cf82f9a..ed0fffb9eba6 100644 if (drcs) { -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0004-ipts.patch b/patches/5.4/0004-ipts.patch index 462165287..d5200c0e2 100644 --- a/patches/5.4/0004-ipts.patch +++ b/patches/5.4/0004-ipts.patch @@ -1,4 +1,4 @@ -From 863c231e1cc6ec12c47819a1c1121db9d80ec88b Mon Sep 17 00:00:00 2001 +From 964a397cb3e5ed3c8887f4203c27c8a28c2f166d Mon Sep 17 00:00:00 2001 From: Dorian Stoll Date: Mon, 27 Jan 2020 21:16:20 +0100 Subject: [PATCH] mei: Add IPTS device IDs @@ -51,9 +51,9 @@ index 75ab2ffbf235..78790904d77c 100644 {MEI_PCI_DEVICE(MEI_DEV_ID_TGP_LP, MEI_ME_PCH12_CFG)}, -- -2.30.0 +2.30.1 -From e5db54a618d5f4c7325e52abaae5d302cfc446e8 Mon Sep 17 00:00:00 2001 +From 749e05593270579c5ac9c692204f1e104be42b67 Mon Sep 17 00:00:00 2001 From: Dorian Stoll Date: Fri, 20 Dec 2019 23:15:58 +0100 Subject: [PATCH] uapi: Add MEI bus ID @@ -77,9 +77,9 @@ index 9a61c28ed3ae..47fc20975245 100644 /* * MT_TOOL types -- -2.30.0 +2.30.1 -From e402b004f197426819ed17bb3e1ec89fe1001157 Mon Sep 17 00:00:00 2001 +From 73dcca12dbedbe0ca69958d2997a7fec4a64f139 Mon Sep 17 00:00:00 2001 From: Dorian Stoll Date: Mon, 27 Jan 2020 21:22:42 +0100 Subject: [PATCH] input: Add support for Intel Precise Touch & Stylus @@ -2085,5 +2085,5 @@ index 000000000000..5b93add1eac2 + +#endif /* _IPTS_STYLUS_H_ */ -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0005-surface-gpe.patch b/patches/5.4/0005-surface-gpe.patch index 1f6d6fc76..deb0f87c2 100644 --- a/patches/5.4/0005-surface-gpe.patch +++ b/patches/5.4/0005-surface-gpe.patch @@ -1,4 +1,4 @@ -From 8a49d9d3c28983fcacbb4dd0d7e8113a100d2b54 Mon Sep 17 00:00:00 2001 +From 12089e42e9edf09773bf3236cb9a6267b268a556 Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Sun, 16 Aug 2020 23:39:56 +0200 Subject: [PATCH] platform/x86: Add Driver to set up lid GPEs on MS Surface @@ -31,23 +31,24 @@ declared via the _PRW field. Signed-off-by: Maximilian Luz Patchset: surface-gpe --- - drivers/platform/x86/Kconfig | 9 + + drivers/platform/x86/Kconfig | 10 + drivers/platform/x86/Makefile | 1 + drivers/platform/x86/surface_gpe.c | 321 +++++++++++++++++++++++++++++ - 3 files changed, 331 insertions(+) + 3 files changed, 332 insertions(+) create mode 100644 drivers/platform/x86/surface_gpe.c diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig -index 56b1cf96ff57..12cf0d723f21 100644 +index 56b1cf96ff57..5d3110a7a7fa 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig -@@ -1217,6 +1217,15 @@ config SURFACE_3_POWER_OPREGION +@@ -1217,6 +1217,16 @@ config SURFACE_3_POWER_OPREGION Select this option to enable support for ACPI operation region of the Surface 3 battery platform driver. +config SURFACE_GPE -+ tristate "Surface GPE/Lid Driver" ++ tristate "Surface GPE/Lid Support Driver" + depends on ACPI ++ depends on DMI + help + This driver marks the GPEs related to the ACPI lid device found on + Microsoft Surface devices as wakeup sources and prepares them @@ -397,5 +398,5 @@ index 000000000000..86f6991b1215 +MODULE_LICENSE("GPL"); +MODULE_ALIAS("dmi:*:svnMicrosoftCorporation:pnSurface*:*"); -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0006-surface-sam-over-hid.patch b/patches/5.4/0006-surface-sam-over-hid.patch index 381fd0205..54f0cd36a 100644 --- a/patches/5.4/0006-surface-sam-over-hid.patch +++ b/patches/5.4/0006-surface-sam-over-hid.patch @@ -1,4 +1,4 @@ -From 026ad3f65be8a3f3a5dffad423a2dde7251a01b0 Mon Sep 17 00:00:00 2001 +From 578d687c64724fa8b16c5c9fd2a2a37e7a968da7 Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Sat, 25 Jul 2020 17:19:53 +0200 Subject: [PATCH] i2c: acpi: Implement RawBytes read access @@ -107,9 +107,9 @@ index c70983780ae7..1c90651161a6 100644 dev_warn(&adapter->dev, "protocol 0x%02x not supported for client 0x%02x\n", accessor_type, client->addr); -- -2.30.0 +2.30.1 -From 0a1661bae14793c84a3d9c440bcf825ffb64e370 Mon Sep 17 00:00:00 2001 +From 36ea4af952dfd9f8ae04030dc4fe6334bd034669 Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Sun, 6 Sep 2020 04:01:19 +0200 Subject: [PATCH] platform/x86: Add driver for Surface Book 1 dGPU switch @@ -132,10 +132,10 @@ Patchset: surface-sam-over-hid create mode 100644 drivers/platform/x86/sb1_dgpu_sw.c diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig -index 12cf0d723f21..f06a0bf92890 100644 +index 5d3110a7a7fa..8e95839341e8 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig -@@ -1226,6 +1226,13 @@ config SURFACE_GPE +@@ -1227,6 +1227,13 @@ config SURFACE_GPE accordingly. It is required on those devices to allow wake-ups from suspend by opening the lid. @@ -330,5 +330,5 @@ index 000000000000..8c66ed5110fd +MODULE_DESCRIPTION("Discrete GPU Power-Switch for Surface Book 1"); +MODULE_LICENSE("GPL"); -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0007-surface-sam.patch b/patches/5.4/0007-surface-sam.patch index ef5c241df..78d90d141 100644 --- a/patches/5.4/0007-surface-sam.patch +++ b/patches/5.4/0007-surface-sam.patch @@ -1,4 +1,4 @@ -From f3005b47f052eb94cfab78f38aafa57acb30fbcd Mon Sep 17 00:00:00 2001 +From 6ca525df793fe8516ee1f057367d6f2b67ea9f13 Mon Sep 17 00:00:00 2001 From: qzed Date: Mon, 26 Aug 2019 01:11:08 +0200 Subject: [PATCH] ACPI: Fix buffer/integer type mismatch @@ -58,9 +58,9 @@ index d3d2dbfba680..0b7f617a6e9b 100644 buffer_desc = acpi_ut_create_buffer_object(buffer_length); if (!buffer_desc) { -- -2.30.0 +2.30.1 -From 892dfdea075d24abf3e1eb5b0f32662890a58b92 Mon Sep 17 00:00:00 2001 +From 285f87c08d31cb1f662af1ec8fed4525a8e227b0 Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Tue, 24 Sep 2019 17:38:12 +0200 Subject: [PATCH] serdev: Add ACPI devices by ResourceSource field @@ -246,13 +246,12 @@ index a9719858c950..ce5309d00280 100644 if (!ctrl->serdev) return -ENODEV; -- -2.30.0 +2.30.1 -From 96bbe52d5ea5308d776bee60ace244f05cb480a4 Mon Sep 17 00:00:00 2001 +From d1e41cb2a20d8803ecbdc15f7daf6c7ee889a531 Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Mon, 17 Aug 2020 01:23:20 +0200 -Subject: [PATCH] misc: surface_sam: Add file2alias support for Surface SAM - devices +Subject: [PATCH] Add file2alias support for Surface System Aggregator devices Implement file2alias support for Surface System Aggregator Module (SSAM) devices. This allows modules to be auto-loaded for specific devices via @@ -348,12 +347,13 @@ index c91eba751804..bc06f7631200 100644 /* Create MODULE_ALIAS() statements. -- -2.30.0 +2.30.1 -From 1c5e72241965f1a32d98b190e04cfbbaf0c8ab09 Mon Sep 17 00:00:00 2001 +From 153d93a08d3260fdbef16f980d5878d833d12a7c Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Mon, 17 Aug 2020 01:44:30 +0200 -Subject: [PATCH] misc: Add support for Surface System Aggregator Module +Subject: [PATCH] platform/x86: Add support for Surface System Aggregator + Module Add support for the Surface System Aggregator Module (SSAM), an embedded controller that can be found on 5th and later generation Microsoft @@ -379,32 +379,40 @@ Patchset: surface-sam .../surface_aggregator/internal.rst | 577 ++++ .../surface_aggregator/overview.rst | 77 + .../driver-api/surface_aggregator/ssh.rst | 344 +++ - drivers/misc/Kconfig | 1 + - drivers/misc/Makefile | 1 + - drivers/misc/surface_aggregator/Kconfig | 67 + - drivers/misc/surface_aggregator/Makefile | 18 + - drivers/misc/surface_aggregator/bus.c | 416 +++ - drivers/misc/surface_aggregator/bus.h | 27 + - .../misc/surface_aggregator/clients/Kconfig | 134 + - .../misc/surface_aggregator/clients/Makefile | 10 + - .../clients/surface_acpi_notify.c | 886 ++++++ - .../clients/surface_aggregator_cdev.c | 322 ++ - .../clients/surface_aggregator_registry.c | 616 ++++ - .../clients/surface_battery.c | 1168 ++++++++ - .../surface_aggregator/clients/surface_dtx.c | 1277 ++++++++ - .../surface_aggregator/clients/surface_hid.c | 924 ++++++ - .../clients/surface_perfmode.c | 122 + - drivers/misc/surface_aggregator/controller.c | 2579 +++++++++++++++++ - drivers/misc/surface_aggregator/controller.h | 285 ++ - drivers/misc/surface_aggregator/core.c | 851 ++++++ - drivers/misc/surface_aggregator/ssh_msgb.h | 205 ++ - .../surface_aggregator/ssh_packet_layer.c | 2074 +++++++++++++ - .../surface_aggregator/ssh_packet_layer.h | 190 ++ - drivers/misc/surface_aggregator/ssh_parser.c | 228 ++ - drivers/misc/surface_aggregator/ssh_parser.h | 155 + + drivers/hid/Kconfig | 2 + + drivers/hid/Makefile | 2 + + drivers/hid/surface-hid/Kconfig | 42 + + drivers/hid/surface-hid/Makefile | 7 + + drivers/hid/surface-hid/surface_hid.c | 256 ++ + drivers/hid/surface-hid/surface_hid_core.c | 272 ++ + drivers/hid/surface-hid/surface_hid_core.h | 77 + + drivers/hid/surface-hid/surface_kbd.c | 303 ++ + drivers/platform/x86/Kconfig | 101 + + drivers/platform/x86/Makefile | 6 + + drivers/platform/x86/surface_acpi_notify.c | 886 ++++++ + .../platform/x86/surface_aggregator/Kconfig | 69 + + .../platform/x86/surface_aggregator/Makefile | 17 + + drivers/platform/x86/surface_aggregator/bus.c | 415 +++ + drivers/platform/x86/surface_aggregator/bus.h | 27 + + .../x86/surface_aggregator/controller.c | 2579 +++++++++++++++++ + .../x86/surface_aggregator/controller.h | 285 ++ + .../platform/x86/surface_aggregator/core.c | 850 ++++++ + .../x86/surface_aggregator/ssh_msgb.h | 205 ++ + .../x86/surface_aggregator/ssh_packet_layer.c | 2074 +++++++++++++ + .../x86/surface_aggregator/ssh_packet_layer.h | 190 ++ + .../x86/surface_aggregator/ssh_parser.c | 228 ++ + .../x86/surface_aggregator/ssh_parser.h | 154 + .../surface_aggregator/ssh_request_layer.c | 1263 ++++++++ .../surface_aggregator/ssh_request_layer.h | 143 + - drivers/misc/surface_aggregator/trace.h | 632 ++++ + .../platform/x86/surface_aggregator/trace.h | 632 ++++ + .../platform/x86/surface_aggregator_cdev.c | 322 ++ + .../x86/surface_aggregator_registry.c | 615 ++++ + drivers/platform/x86/surface_dtx.c | 1289 ++++++++ + drivers/platform/x86/surface_perfmode.c | 122 + + drivers/power/supply/Kconfig | 32 + + drivers/power/supply/Makefile | 2 + + drivers/power/supply/surface_battery.c | 901 ++++++ + drivers/power/supply/surface_charger.c | 296 ++ include/linux/mod_devicetable.h | 5 +- include/linux/surface_acpi_notify.h | 39 + include/linux/surface_aggregator/controller.h | 824 ++++++ @@ -414,7 +422,7 @@ Patchset: surface-sam include/uapi/linux/surface_aggregator/dtx.h | 146 + scripts/mod/devicetable-offsets.c | 3 +- scripts/mod/file2alias.c | 10 +- - 47 files changed, 19170 insertions(+), 7 deletions(-) + 55 files changed, 19240 insertions(+), 7 deletions(-) create mode 100644 Documentation/driver-api/surface_aggregator/client-api.rst create mode 100644 Documentation/driver-api/surface_aggregator/client.rst create mode 100644 Documentation/driver-api/surface_aggregator/clients/cdev.rst @@ -426,30 +434,34 @@ Patchset: surface-sam create mode 100644 Documentation/driver-api/surface_aggregator/internal.rst create mode 100644 Documentation/driver-api/surface_aggregator/overview.rst create mode 100644 Documentation/driver-api/surface_aggregator/ssh.rst - create mode 100644 drivers/misc/surface_aggregator/Kconfig - create mode 100644 drivers/misc/surface_aggregator/Makefile - create mode 100644 drivers/misc/surface_aggregator/bus.c - create mode 100644 drivers/misc/surface_aggregator/bus.h - create mode 100644 drivers/misc/surface_aggregator/clients/Kconfig - create mode 100644 drivers/misc/surface_aggregator/clients/Makefile - create mode 100644 drivers/misc/surface_aggregator/clients/surface_acpi_notify.c - create mode 100644 drivers/misc/surface_aggregator/clients/surface_aggregator_cdev.c - create mode 100644 drivers/misc/surface_aggregator/clients/surface_aggregator_registry.c - create mode 100644 drivers/misc/surface_aggregator/clients/surface_battery.c - create mode 100644 drivers/misc/surface_aggregator/clients/surface_dtx.c - create mode 100644 drivers/misc/surface_aggregator/clients/surface_hid.c - create mode 100644 drivers/misc/surface_aggregator/clients/surface_perfmode.c - create mode 100644 drivers/misc/surface_aggregator/controller.c - create mode 100644 drivers/misc/surface_aggregator/controller.h - create mode 100644 drivers/misc/surface_aggregator/core.c - create mode 100644 drivers/misc/surface_aggregator/ssh_msgb.h - create mode 100644 drivers/misc/surface_aggregator/ssh_packet_layer.c - create mode 100644 drivers/misc/surface_aggregator/ssh_packet_layer.h - create mode 100644 drivers/misc/surface_aggregator/ssh_parser.c - create mode 100644 drivers/misc/surface_aggregator/ssh_parser.h - create mode 100644 drivers/misc/surface_aggregator/ssh_request_layer.c - create mode 100644 drivers/misc/surface_aggregator/ssh_request_layer.h - create mode 100644 drivers/misc/surface_aggregator/trace.h + create mode 100644 drivers/hid/surface-hid/Kconfig + create mode 100644 drivers/hid/surface-hid/Makefile + create mode 100644 drivers/hid/surface-hid/surface_hid.c + create mode 100644 drivers/hid/surface-hid/surface_hid_core.c + create mode 100644 drivers/hid/surface-hid/surface_hid_core.h + create mode 100644 drivers/hid/surface-hid/surface_kbd.c + create mode 100644 drivers/platform/x86/surface_acpi_notify.c + create mode 100644 drivers/platform/x86/surface_aggregator/Kconfig + create mode 100644 drivers/platform/x86/surface_aggregator/Makefile + create mode 100644 drivers/platform/x86/surface_aggregator/bus.c + create mode 100644 drivers/platform/x86/surface_aggregator/bus.h + create mode 100644 drivers/platform/x86/surface_aggregator/controller.c + create mode 100644 drivers/platform/x86/surface_aggregator/controller.h + create mode 100644 drivers/platform/x86/surface_aggregator/core.c + create mode 100644 drivers/platform/x86/surface_aggregator/ssh_msgb.h + create mode 100644 drivers/platform/x86/surface_aggregator/ssh_packet_layer.c + create mode 100644 drivers/platform/x86/surface_aggregator/ssh_packet_layer.h + create mode 100644 drivers/platform/x86/surface_aggregator/ssh_parser.c + create mode 100644 drivers/platform/x86/surface_aggregator/ssh_parser.h + create mode 100644 drivers/platform/x86/surface_aggregator/ssh_request_layer.c + create mode 100644 drivers/platform/x86/surface_aggregator/ssh_request_layer.h + create mode 100644 drivers/platform/x86/surface_aggregator/trace.h + create mode 100644 drivers/platform/x86/surface_aggregator_cdev.c + create mode 100644 drivers/platform/x86/surface_aggregator_registry.c + create mode 100644 drivers/platform/x86/surface_dtx.c + create mode 100644 drivers/platform/x86/surface_perfmode.c + create mode 100644 drivers/power/supply/surface_battery.c + create mode 100644 drivers/power/supply/surface_charger.c create mode 100644 include/linux/surface_acpi_notify.h create mode 100644 include/linux/surface_aggregator/controller.h create mode 100644 include/linux/surface_aggregator/device.h @@ -2917,629 +2929,1031 @@ index 000000000000..bf007d6c9873 +(per party, effectively leading to synchronous communication regarding +frames) and at most three pending commands. The limit to synchronous frame +transfers seems to be consistent with behavior observed on Windows. -diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig -index c55b63750757..b8626bef683c 100644 ---- a/drivers/misc/Kconfig -+++ b/drivers/misc/Kconfig -@@ -481,4 +481,5 @@ source "drivers/misc/cxl/Kconfig" - source "drivers/misc/ocxl/Kconfig" - source "drivers/misc/cardreader/Kconfig" - source "drivers/misc/habanalabs/Kconfig" -+source "drivers/misc/surface_aggregator/Kconfig" +diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig +index 1ecb5124421c..5aa898189afe 100644 +--- a/drivers/hid/Kconfig ++++ b/drivers/hid/Kconfig +@@ -1154,4 +1154,6 @@ source "drivers/hid/i2c-hid/Kconfig" + + source "drivers/hid/intel-ish-hid/Kconfig" + ++source "drivers/hid/surface-hid/Kconfig" ++ endmenu -diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile -index c1860d35dc7e..986da863df27 100644 ---- a/drivers/misc/Makefile -+++ b/drivers/misc/Makefile -@@ -57,3 +57,4 @@ obj-y += cardreader/ - obj-$(CONFIG_PVPANIC) += pvpanic.o - obj-$(CONFIG_HABANA_AI) += habanalabs/ - obj-$(CONFIG_XILINX_SDFEC) += xilinx_sdfec.o -+obj-$(CONFIG_SURFACE_AGGREGATOR) += surface_aggregator/ -diff --git a/drivers/misc/surface_aggregator/Kconfig b/drivers/misc/surface_aggregator/Kconfig +diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile +index 0c03308cfb08..3a023848fe12 100644 +--- a/drivers/hid/Makefile ++++ b/drivers/hid/Makefile +@@ -138,3 +138,5 @@ obj-$(CONFIG_I2C_HID) += i2c-hid/ + + obj-$(CONFIG_INTEL_ISH_HID) += intel-ish-hid/ + obj-$(INTEL_ISH_FIRMWARE_DOWNLOADER) += intel-ish-hid/ ++ ++obj-$(CONFIG_SURFACE_HID_CORE) += surface-hid/ +diff --git a/drivers/hid/surface-hid/Kconfig b/drivers/hid/surface-hid/Kconfig new file mode 100644 -index 000000000000..47dd8fdffac3 +index 000000000000..7ce9b5d641eb --- /dev/null -+++ b/drivers/misc/surface_aggregator/Kconfig -@@ -0,0 +1,67 @@ ++++ b/drivers/hid/surface-hid/Kconfig +@@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-2.0+ -+# Copyright (C) 2019-2020 Maximilian Luz -+ -+menuconfig SURFACE_AGGREGATOR -+ tristate "Microsoft Surface System Aggregator Module Subsystem and Drivers" -+ depends on SERIAL_DEV_BUS -+ depends on ACPI -+ select CRC_CCITT -+ help -+ The Surface System Aggregator Module (Surface SAM or SSAM) is an -+ embedded controller (EC) found on 5th- and later-generation Microsoft -+ Surface devices (i.e. Surface Pro 5, Surface Book 2, Surface Laptop, -+ and newer, with exception of Surface Go series devices). -+ -+ Depending on the device in question, this EC provides varying -+ functionality, including: -+ - EC access from ACPI via Surface ACPI Notify (5th- and 6th-generation) -+ - battery status information (all devices) -+ - thermal sensor access (all devices) -+ - performance mode / cooling mode control (all devices) -+ - clipboard detachment system control (Surface Book 2 and 3) -+ - HID / keyboard input (Surface Laptops, Surface Book 3) -+ -+ This option controls whether the Surface SAM subsystem core will be -+ built. This includes a driver for the Surface Serial Hub (SSH), which -+ is the device responsible for the communication with the EC, and a -+ basic kernel interface exposing the EC functionality to other client -+ drivers, i.e. allowing them to make requests to the EC and receive -+ events from it. Selecting this option alone will not provide any -+ client drivers and therefore no functionality beyond the in-kernel -+ interface. Said functionality is the repsonsibility of the respective -+ client drivers. -+ -+ Note: While 4th-generation Surface devices also make use of a SAM EC, -+ due to a difference in the communication interface of the controller, -+ only 5th and later generations are currently supported. Specifically, -+ devices using SAM-over-SSH are supported, whereas devices using -+ SAM-over-HID, which is used on the 4th generation, are currently not -+ supported. -+ -+config SURFACE_AGGREGATOR_BUS -+ bool "Surface System Aggregator Module Bus" ++menu "Surface System Aggregator Module HID support" + depends on SURFACE_AGGREGATOR -+ default y ++ 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 -+ Expands the Surface System Aggregator Module (SSAM) core driver by -+ providing a dedicated bus and client-device type. ++ Driver to support integrated HID devices on newer Microsoft Surface ++ models. + -+ This bus and device type are intended to provide and simplify support -+ for non-platform and non-ACPI SSAM devices, i.e. SSAM devices that are -+ not auto-detectable via the conventional means (e.g. ACPI). ++ 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. + -+config SURFACE_AGGREGATOR_ERROR_INJECTION -+ bool "Surface System Aggregator Module Error Injection Capabilities" -+ depends on SURFACE_AGGREGATOR -+ depends on FUNCTION_ERROR_INJECTION ++ 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. ++ ++config SURFACE_KBD ++ tristate "HID keyboard transport driver for Surface System Aggregator Module" ++ select SURFACE_HID_CORE + help -+ Provides error-injection capabilities for the Surface System -+ Aggregator Module subsystem and Surface Serial Hub driver. ++ Driver to support HID keyboards on Surface Laptop 1 and 2 devices. + -+ Specifically, exports error injection hooks to be used with the -+ kernel's function error injection capabilities to simulate underlying -+ transport and communication problems, such as invalid data sent to or -+ received from the EC, dropped data, and communication timeouts. -+ Intended for development and debugging. ++ This driver provides support for the HID transport protocol provided ++ by the Surface Aggregator Module (i.e. the embedded controller) on ++ Microsoft Surface Laptops 1 and 2. It is used to connect the ++ integrated keyboard on those devices. + -+source "drivers/misc/surface_aggregator/clients/Kconfig" -diff --git a/drivers/misc/surface_aggregator/Makefile b/drivers/misc/surface_aggregator/Makefile ++ Say M or Y here, if you want support for the integrated keyboard on ++ Microsoft Surface Laptops 1 and 2. ++ ++endmenu ++ ++config SURFACE_HID_CORE ++ tristate ++ select HID +diff --git a/drivers/hid/surface-hid/Makefile b/drivers/hid/surface-hid/Makefile new file mode 100644 -index 000000000000..b48ffc37ab52 +index 000000000000..4ae11cf09b25 --- /dev/null -+++ b/drivers/misc/surface_aggregator/Makefile -@@ -0,0 +1,18 @@ ++++ b/drivers/hid/surface-hid/Makefile +@@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0+ -+# Copyright (C) 2019-2020 Maximilian Luz -+ -+# For include/trace/define_trace.h to include trace.h -+CFLAGS_core.o = -I$(src) -+ -+obj-$(CONFIG_SURFACE_AGGREGATOR) += surface_aggregator.o -+obj-$(CONFIG_SURFACE_AGGREGATOR) += clients/ -+ -+surface_aggregator-objs := core.o -+surface_aggregator-objs += ssh_parser.o -+surface_aggregator-objs += ssh_packet_layer.o -+surface_aggregator-objs += ssh_request_layer.o -+surface_aggregator-objs += controller.o -+ -+ifeq ($(CONFIG_SURFACE_AGGREGATOR_BUS),y) -+surface_aggregator-objs += bus.o -+endif -diff --git a/drivers/misc/surface_aggregator/bus.c b/drivers/misc/surface_aggregator/bus.c ++# ++# 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 ++obj-$(CONFIG_SURFACE_KBD) += surface_kbd.o +diff --git a/drivers/hid/surface-hid/surface_hid.c b/drivers/hid/surface-hid/surface_hid.c new file mode 100644 -index 000000000000..fecfa1b6a140 +index 000000000000..e4477c328536 --- /dev/null -+++ b/drivers/misc/surface_aggregator/bus.c -@@ -0,0 +1,416 @@ ++++ b/drivers/hid/surface-hid/surface_hid.c +@@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* -+ * Surface System Aggregator Module bus and device integration. ++ * 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-2020 Maximilian Luz ++ * Copyright (C) 2019-2021 Blaž Hrastnik , ++ * Maximilian Luz + */ + -+#include -+#include ++#include ++#include ++#include ++#include ++#include + +#include +#include + -+#include "bus.h" -+#include "controller.h" ++#include "surface_hid_core.h" + -+static ssize_t modalias_show(struct device *dev, struct device_attribute *attr, -+ char *buf) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); + -+ // FIXME: we should use sysfs_emit here, but that's not available on < 5.10 -+ return scnprintf(buf, PAGE_SIZE, "ssam:d%02Xc%02Xt%02Xi%02Xf%02X\n", -+ sdev->uid.domain, sdev->uid.category, sdev->uid.target, -+ sdev->uid.instance, sdev->uid.function); -+} -+static DEVICE_ATTR_RO(modalias); ++/* -- SAM interface. -------------------------------------------------------- */ + -+static struct attribute *ssam_device_attrs[] = { -+ &dev_attr_modalias.attr, -+ NULL, ++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, +}; -+ATTRIBUTE_GROUPS(ssam_device); + -+static int ssam_device_uevent(struct device *dev, struct kobj_uevent_env *env) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); -+ -+ return add_uevent_var(env, "MODALIAS=ssam:d%02Xc%02Xt%02Xi%02Xf%02X", -+ sdev->uid.domain, sdev->uid.category, -+ sdev->uid.target, sdev->uid.instance, -+ sdev->uid.function); -+} -+ -+static void ssam_device_release(struct device *dev) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); -+ -+ ssam_controller_put(sdev->ctrl); -+ kfree(sdev); -+} -+ -+const struct device_type ssam_device_type = { -+ .name = "surface_aggregator_device", -+ .groups = ssam_device_groups, -+ .uevent = ssam_device_uevent, -+ .release = ssam_device_release, -+}; -+EXPORT_SYMBOL_GPL(ssam_device_type); -+ -+/** -+ * ssam_device_alloc() - Allocate and initialize a SSAM client device. -+ * @ctrl: The controller under which the device should be added. -+ * @uid: The UID of the device to be added. -+ * -+ * Allocates and initializes a new client device. The parent of the device -+ * will be set to the controller device and the name will be set based on the -+ * UID. Note that the device still has to be added via ssam_device_add(). -+ * Refer to that function for more details. -+ * -+ * Return: Returns the newly allocated and initialized SSAM client device, or -+ * %NULL if it could not be allocated. -+ */ -+struct ssam_device *ssam_device_alloc(struct ssam_controller *ctrl, -+ struct ssam_device_uid uid) -+{ -+ struct ssam_device *sdev; -+ -+ sdev = kzalloc(sizeof(*sdev), GFP_KERNEL); -+ if (!sdev) -+ return NULL; -+ -+ device_initialize(&sdev->dev); -+ sdev->dev.bus = &ssam_bus_type; -+ sdev->dev.type = &ssam_device_type; -+ sdev->dev.parent = ssam_controller_device(ctrl); -+ sdev->ctrl = ssam_controller_get(ctrl); -+ sdev->uid = uid; -+ -+ dev_set_name(&sdev->dev, "%02x:%02x:%02x:%02x:%02x", -+ sdev->uid.domain, sdev->uid.category, sdev->uid.target, -+ sdev->uid.instance, sdev->uid.function); -+ -+ return sdev; -+} -+EXPORT_SYMBOL_GPL(ssam_device_alloc); -+ -+/** -+ * ssam_device_add() - Add a SSAM client device. -+ * @sdev: The SSAM client device to be added. -+ * -+ * Added client devices must be guaranteed to always have a valid and active -+ * controller. Thus, this function will fail with %-ENODEV if the controller -+ * of the device has not been initialized yet, has been suspended, or has been -+ * shut down. -+ * -+ * The caller of this function should ensure that the corresponding call to -+ * ssam_device_remove() is issued before the controller is shut down. If the -+ * added device is a direct child of the controller device (default), it will -+ * be automatically removed when the controller is shut down. -+ * -+ * By default, the controller device will become the parent of the newly -+ * created client device. The parent may be changed before ssam_device_add is -+ * called, but care must be taken that a) the correct suspend/resume ordering -+ * is guaranteed and b) the client device does not outlive the controller, -+ * i.e. that the device is removed before the controller is being shut down. -+ * In case these guarantees have to be manually enforced, please refer to the -+ * ssam_client_link() and ssam_client_bind() functions, which are intended to -+ * set up device-links for this purpose. -+ * -+ * Return: Returns zero on success, a negative error code on failure. -+ */ -+int ssam_device_add(struct ssam_device *sdev) ++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; + + /* -+ * Ensure that we can only add new devices to a controller if it has -+ * been started and is not going away soon. This works in combination -+ * with ssam_controller_remove_clients to ensure driver presence for the -+ * controller device, i.e. it ensures that the controller (sdev->ctrl) -+ * is always valid and can be used for requests as long as the client -+ * device we add here is registered as child under it. This essentially -+ * guarantees that the client driver can always expect the preconditions -+ * for functions like ssam_request_sync (controller has to be started -+ * and is not suspended) to hold and thus does not have to check for -+ * them. -+ * -+ * Note that for this to work, the controller has to be a parent device. -+ * If it is not a direct parent, care has to be taken that the device is -+ * removed via ssam_device_remove(), as device_unregister does not -+ * remove child devices recursively. ++ * 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. + */ -+ ssam_controller_statelock(sdev->ctrl); + -+ if (sdev->ctrl->state != SSAM_CONTROLLER_STARTED) { -+ ssam_controller_stateunlock(sdev->ctrl); -+ return -ENODEV; ++ 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; + } + -+ status = device_add(&sdev->dev); ++ if (offset != len) { ++ dev_err(shid->dev, "unexpected descriptor length: got %u, expected %zu\n", ++ offset, len); ++ return -EPROTO; ++ } + -+ ssam_controller_stateunlock(sdev->ctrl); -+ return status; -+} -+EXPORT_SYMBOL_GPL(ssam_device_add); -+ -+/** -+ * ssam_device_remove() - Remove a SSAM client device. -+ * @sdev: The device to remove. -+ * -+ * Removes and unregisters the provided SSAM client device. -+ */ -+void ssam_device_remove(struct ssam_device *sdev) -+{ -+ device_unregister(&sdev->dev); -+} -+EXPORT_SYMBOL_GPL(ssam_device_remove); -+ -+/** -+ * ssam_device_id_compatible() - Check if a device ID matches a UID. -+ * @id: The device ID as potential match. -+ * @uid: The device UID matching against. -+ * -+ * Check if the given ID is a match for the given UID, i.e. if a device with -+ * the provided UID is compatible to the given ID following the match rules -+ * described in its &ssam_device_id.match_flags member. -+ * -+ * Return: Returns %true if the given UID is compatible to the match rule -+ * described by the given ID, %false otherwise. -+ */ -+static bool ssam_device_id_compatible(const struct ssam_device_id *id, -+ struct ssam_device_uid uid) -+{ -+ if (id->domain != uid.domain || id->category != uid.category) -+ return false; -+ -+ if ((id->match_flags & SSAM_MATCH_TARGET) && id->target != uid.target) -+ return false; -+ -+ if ((id->match_flags & SSAM_MATCH_INSTANCE) && id->instance != uid.instance) -+ return false; -+ -+ if ((id->match_flags & SSAM_MATCH_FUNCTION) && id->function != uid.function) -+ return false; -+ -+ return true; ++ return 0; +} + -+/** -+ * ssam_device_id_is_null() - Check if a device ID is null. -+ * @id: The device ID to check. -+ * -+ * Check if a given device ID is null, i.e. all zeros. Used to check for the -+ * end of ``MODULE_DEVICE_TABLE(ssam, ...)`` or similar lists. -+ * -+ * Return: Returns %true if the given ID represents a null ID, %false -+ * otherwise. -+ */ -+static bool ssam_device_id_is_null(const struct ssam_device_id *id) ++static int ssam_hid_set_raw_report(struct surface_hid_device *shid, u8 rprt_id, bool feature, ++ u8 *buf, size_t len) +{ -+ return id->match_flags == 0 && -+ id->domain == 0 && -+ id->category == 0 && -+ id->target == 0 && -+ id->instance == 0 && -+ id->function == 0 && -+ id->driver_data == 0; ++ 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); +} + -+/** -+ * ssam_device_id_match() - Find the matching ID table entry for the given UID. -+ * @table: The table to search in. -+ * @uid: The UID to matched against the individual table entries. -+ * -+ * Find the first match for the provided device UID in the provided ID table -+ * and return it. Returns %NULL if no match could be found. -+ */ -+const struct ssam_device_id *ssam_device_id_match(const struct ssam_device_id *table, -+ const struct ssam_device_uid uid) ++static int ssam_hid_get_raw_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) +{ -+ const struct ssam_device_id *id; ++ struct ssam_request rqst; ++ struct ssam_response rsp; + -+ for (id = table; !ssam_device_id_is_null(id); ++id) -+ if (ssam_device_id_compatible(id, uid)) -+ return id; ++ 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; + -+ return NULL; ++ rsp.capacity = len; ++ rsp.length = 0; ++ rsp.pointer = buf; ++ ++ return ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(rprt_id)); +} -+EXPORT_SYMBOL_GPL(ssam_device_id_match); + -+/** -+ * ssam_device_get_match() - Find and return the ID matching the device in the -+ * ID table of the bound driver. -+ * @dev: The device for which to get the matching ID table entry. -+ * -+ * Find the fist match for the UID of the device in the ID table of the -+ * currently bound driver and return it. Returns %NULL if the device does not -+ * have a driver bound to it, the driver does not have match_table (i.e. it is -+ * %NULL), or there is no match in the driver's match_table. -+ * -+ * This function essentially calls ssam_device_id_match() with the ID table of -+ * the bound device driver and the UID of the device. -+ * -+ * Return: Returns the first match for the UID of the device in the device -+ * driver's match table, or %NULL if no such match could be found. -+ */ -+const struct ssam_device_id *ssam_device_get_match(const struct ssam_device *dev) ++static u32 ssam_hid_event_fn(struct ssam_event_notifier *nf, const struct ssam_event *event) +{ -+ const struct ssam_device_driver *sdrv; ++ struct surface_hid_device *shid = container_of(nf, struct surface_hid_device, notif); ++ int status; + -+ sdrv = to_ssam_device_driver(dev->dev.driver); -+ if (!sdrv) -+ return NULL; -+ -+ if (!sdrv->match_table) -+ return NULL; -+ -+ return ssam_device_id_match(sdrv->match_table, dev->uid); -+} -+EXPORT_SYMBOL_GPL(ssam_device_get_match); -+ -+/** -+ * ssam_device_get_match_data() - Find the ID matching the device in the -+ * ID table of the bound driver and return its ``driver_data`` member. -+ * @dev: The device for which to get the match data. -+ * -+ * Find the fist match for the UID of the device in the ID table of the -+ * corresponding driver and return its driver_data. Returns %NULL if the -+ * device does not have a driver bound to it, the driver does not have -+ * match_table (i.e. it is %NULL), there is no match in the driver's -+ * match_table, or the match does not have any driver_data. -+ * -+ * This function essentially calls ssam_device_get_match() and, if any match -+ * could be found, returns its ``struct ssam_device_id.driver_data`` member. -+ * -+ * Return: Returns the driver data associated with the first match for the UID -+ * of the device in the device driver's match table, or %NULL if no such match -+ * could be found. -+ */ -+const void *ssam_device_get_match_data(const struct ssam_device *dev) -+{ -+ const struct ssam_device_id *id; -+ -+ id = ssam_device_get_match(dev); -+ if (!id) -+ return NULL; -+ -+ return (const void *)id->driver_data; -+} -+EXPORT_SYMBOL_GPL(ssam_device_get_match_data); -+ -+static int ssam_bus_match(struct device *dev, struct device_driver *drv) -+{ -+ struct ssam_device_driver *sdrv = to_ssam_device_driver(drv); -+ struct ssam_device *sdev = to_ssam_device(dev); -+ -+ if (!is_ssam_device(dev)) ++ if (event->command_id != 0x00) + return 0; + -+ return !!ssam_device_id_match(sdrv->match_table, sdev->uid); ++ status = hid_input_report(shid->hid, HID_INPUT_REPORT, (u8 *)&event->data[0], ++ event->length, 0); ++ ++ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; +} + -+static int ssam_bus_probe(struct device *dev) ++ ++/* -- Transport driver. ----------------------------------------------------- */ ++ ++static int shid_output_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) +{ -+ return to_ssam_device_driver(dev->driver) -+ ->probe(to_ssam_device(dev)); ++ int status; ++ ++ status = ssam_hid_set_raw_report(shid, rprt_id, false, buf, len); ++ return status >= 0 ? len : status; +} + -+static int ssam_bus_remove(struct device *dev) ++static int shid_get_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) +{ -+ struct ssam_device_driver *sdrv = to_ssam_device_driver(dev->driver); ++ int status; + -+ if (sdrv->remove) -+ sdrv->remove(to_ssam_device(dev)); -+ -+ return 0; ++ status = ssam_hid_get_raw_report(shid, rprt_id, buf, len); ++ return status >= 0 ? len : status; +} + -+struct bus_type ssam_bus_type = { -+ .name = "surface_aggregator", -+ .match = ssam_bus_match, -+ .probe = ssam_bus_probe, -+ .remove = ssam_bus_remove, ++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) }, ++ { }, +}; -+EXPORT_SYMBOL_GPL(ssam_bus_type); ++MODULE_DEVICE_TABLE(ssam, surface_hid_match); + -+/** -+ * __ssam_device_driver_register() - Register a SSAM client device driver. -+ * @sdrv: The driver to register. -+ * @owner: The module owning the provided driver. -+ * -+ * Please refer to the ssam_device_driver_register() macro for the normal way -+ * to register a driver from inside its owning module. -+ */ -+int __ssam_device_driver_register(struct ssam_device_driver *sdrv, -+ struct module *owner) -+{ -+ sdrv->driver.owner = owner; -+ sdrv->driver.bus = &ssam_bus_type; ++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); + -+ /* force drivers to async probe so I/O is possible in probe */ -+ sdrv->driver.probe_type = PROBE_PREFER_ASYNCHRONOUS; -+ -+ return driver_register(&sdrv->driver); -+} -+EXPORT_SYMBOL_GPL(__ssam_device_driver_register); -+ -+/** -+ * ssam_device_driver_unregister - Unregister a SSAM device driver. -+ * @sdrv: The driver to unregister. -+ */ -+void ssam_device_driver_unregister(struct ssam_device_driver *sdrv) -+{ -+ driver_unregister(&sdrv->driver); -+} -+EXPORT_SYMBOL_GPL(ssam_device_driver_unregister); -+ -+static int ssam_remove_device(struct device *dev, void *_data) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); -+ -+ if (is_ssam_device(dev)) -+ ssam_device_remove(sdev); -+ -+ return 0; -+} -+ -+/** -+ * ssam_controller_remove_clients() - Remove SSAM client devices registered as -+ * direct children under the given controller. -+ * @ctrl: The controller to remove all direct clients for. -+ * -+ * Remove all SSAM client devices registered as direct children under the -+ * given controller. Note that this only accounts for direct children of the -+ * controller device. This does not take care of any client devices where the -+ * parent device has been manually set before calling ssam_device_add. Refer -+ * to ssam_device_add()/ssam_device_remove() for more details on those cases. -+ * -+ * To avoid new devices being added in parallel to this call, the main -+ * controller lock (not statelock) must be held during this (and if -+ * necessary, any subsequent deinitialization) call. -+ */ -+void ssam_controller_remove_clients(struct ssam_controller *ctrl) -+{ -+ struct device *dev; -+ -+ dev = ssam_controller_device(ctrl); -+ device_for_each_child_reverse(dev, NULL, ssam_remove_device); -+} -+ -+/** -+ * ssam_bus_register() - Register and set-up the SSAM client device bus. -+ */ -+int ssam_bus_register(void) -+{ -+ return bus_register(&ssam_bus_type); -+} -+ -+/** -+ * ssam_bus_unregister() - Unregister the SSAM client device bus. -+ */ -+void ssam_bus_unregister(void) -+{ -+ return bus_unregister(&ssam_bus_type); -+} -diff --git a/drivers/misc/surface_aggregator/bus.h b/drivers/misc/surface_aggregator/bus.h ++MODULE_AUTHOR("Blaž Hrastnik "); ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("HID transport driver for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/hid/surface-hid/surface_hid_core.c b/drivers/hid/surface-hid/surface_hid_core.c new file mode 100644 -index 000000000000..7712baaed6a5 +index 000000000000..7b27ec392232 --- /dev/null -+++ b/drivers/misc/surface_aggregator/bus.h -@@ -0,0 +1,27 @@ -+/* SPDX-License-Identifier: GPL-2.0+ */ ++++ b/drivers/hid/surface-hid/surface_hid_core.c +@@ -0,0 +1,272 @@ ++// SPDX-License-Identifier: GPL-2.0+ +/* -+ * Surface System Aggregator Module bus and device integration. ++ * 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-2020 Maximilian Luz ++ * Copyright (C) 2019-2021 Maximilian Luz + */ + -+#ifndef _SURFACE_AGGREGATOR_BUS_H -+#define _SURFACE_AGGREGATOR_BUS_H ++#include ++#include ++#include ++#include ++#include ++#include + +#include + -+#ifdef CONFIG_SURFACE_AGGREGATOR_BUS ++#include "surface_hid_core.h" + -+void ssam_controller_remove_clients(struct ssam_controller *ctrl); + -+int ssam_bus_register(void); -+void ssam_bus_unregister(void); ++/* -- Device descriptor access. --------------------------------------------- */ + -+#else /* CONFIG_SURFACE_AGGREGATOR_BUS */ ++static int surface_hid_load_hid_descriptor(struct surface_hid_device *shid) ++{ ++ int status; + -+static inline void ssam_controller_remove_clients(struct ssam_controller *ctrl) {} -+static inline int ssam_bus_register(void) { return 0; } -+static inline void ssam_bus_unregister(void) {} ++ status = shid->ops.get_descriptor(shid, SURFACE_HID_DESC_HID, ++ (u8 *)&shid->hid_desc, sizeof(shid->hid_desc)); ++ if (status) ++ return status; + -+#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ -+#endif /* _SURFACE_AGGREGATOR_BUS_H */ -diff --git a/drivers/misc/surface_aggregator/clients/Kconfig b/drivers/misc/surface_aggregator/clients/Kconfig ++ 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 "); ++MODULE_DESCRIPTION("HID transport driver core for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/hid/surface-hid/surface_hid_core.h b/drivers/hid/surface-hid/surface_hid_core.h new file mode 100644 -index 000000000000..4d2b0528ca2e +index 000000000000..4b1a7b57e035 --- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/Kconfig -@@ -0,0 +1,134 @@ -+# SPDX-License-Identifier: GPL-2.0+ -+# Copyright (C) 2019-2020 Maximilian Luz ++++ b/drivers/hid/surface-hid/surface_hid_core.h +@@ -0,0 +1,77 @@ ++/* 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 ++ */ + -+config SURFACE_AGGREGATOR_CDEV -+ tristate "Surface System Aggregator Module User-Space Interface" -+ depends on SURFACE_AGGREGATOR -+ help -+ Provides a misc-device interface to the Surface System Aggregator -+ Module (SSAM) controller. ++#ifndef SURFACE_HID_CORE_H ++#define SURFACE_HID_CORE_H + -+ This option provides a module (called surface_aggregator_cdev), that, -+ when loaded, will add a client device (and its respective driver) to -+ the SSAM controller. Said client device manages a misc-device -+ interface (/dev/surface/aggregator), which can be used by user-space -+ tools to directly communicate with the SSAM EC by sending requests and -+ receiving the correspondign responses. ++#include ++#include ++#include + -+ The provided interface is intended for debugging and development only, -+ and should not be used otherwise. ++#include ++#include + -+config SURFACE_AGGREGATOR_REGISTRY -+ tristate "Surface System Aggregator Module Device Registry" -+ depends on SURFACE_AGGREGATOR_BUS -+ default m -+ help -+ Device-registry and device-hubs for Surface System Aggregator Module -+ (SSAM) devices. ++enum surface_hid_descriptor_entry { ++ SURFACE_HID_DESC_HID = 0, ++ SURFACE_HID_DESC_REPORT = 1, ++ SURFACE_HID_DESC_ATTRS = 2, ++}; + -+ Provides a module and driver which act as device-registry for SSAM -+ client devices that cannot be detected automatically, e.g. via ACPI. -+ Such devices are instead provided via this registry and attached via -+ device hubs, also provided in this module. ++struct surface_hid_descriptor { ++ __u8 desc_len; /* = 9 */ ++ __u8 desc_type; /* = HID_DT_HID */ ++ __le16 hid_version; ++ __u8 country_code; ++ __u8 num_descriptors; /* = 1 */ + -+ Devices provided via this registry are: -+ - performance / cooling mode device (all generations) -+ - battery/AC devices (7th generation) -+ - HID input devices (7th generation) ++ __u8 report_desc_type; /* = HID_DT_REPORT */ ++ __le16 report_desc_len; ++} __packed; + -+ Note that this module only provides the respective client devices. -+ Drivers for these devices still need to be selected via the other -+ options. ++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 */ +diff --git a/drivers/hid/surface-hid/surface_kbd.c b/drivers/hid/surface-hid/surface_kbd.c +new file mode 100644 +index 000000000000..e72baac952ec +--- /dev/null ++++ b/drivers/hid/surface-hid/surface_kbd.c +@@ -0,0 +1,303 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++/* ++ * Surface System Aggregator Module (SSAM) HID transport driver for the legacy ++ * keyboard interface (KBD/TC=0x08 subsystem). Provides support for the ++ * integrated HID keyboard on Surface Laptops 1 and 2. ++ * ++ * Copyright (C) 2019-2021 Maximilian Luz ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include "surface_hid_core.h" ++ ++ ++/* -- SAM interface (KBD). -------------------------------------------------- */ ++ ++#define KBD_FEATURE_REPORT_SIZE 7 /* 6 + report ID */ ++ ++enum surface_kbd_cid { ++ SURFACE_KBD_CID_GET_DESCRIPTOR = 0x00, ++ SURFACE_KBD_CID_SET_CAPSLOCK_LED = 0x01, ++ SURFACE_KBD_CID_EVT_INPUT_GENERIC = 0x03, ++ SURFACE_KBD_CID_EVT_INPUT_HOTKEYS = 0x04, ++ SURFACE_KBD_CID_GET_FEATURE_REPORT = 0x0b, ++}; ++ ++static int ssam_kbd_get_descriptor(struct surface_hid_device *shid, u8 entry, u8 *buf, size_t len) ++{ ++ struct ssam_request rqst; ++ struct ssam_response rsp; ++ int status; ++ ++ rqst.target_category = shid->uid.category; ++ rqst.target_id = shid->uid.target; ++ rqst.command_id = SURFACE_KBD_CID_GET_DESCRIPTOR; ++ rqst.instance_id = shid->uid.instance; ++ rqst.flags = SSAM_REQUEST_HAS_RESPONSE; ++ rqst.length = sizeof(entry); ++ rqst.payload = &entry; ++ ++ rsp.capacity = len; ++ rsp.length = 0; ++ rsp.pointer = buf; ++ ++ status = ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(entry)); ++ if (status) ++ return status; ++ ++ if (rsp.length != len) { ++ dev_err(shid->dev, "invalid descriptor length: got %zu, expected, %zu\n", ++ rsp.length, len); ++ return -EPROTO; ++ } ++ ++ return 0; ++} ++ ++static int ssam_kbd_set_caps_led(struct surface_hid_device *shid, bool value) ++{ ++ struct ssam_request rqst; ++ u8 value_u8 = value; ++ ++ rqst.target_category = shid->uid.category; ++ rqst.target_id = shid->uid.target; ++ rqst.command_id = SURFACE_KBD_CID_SET_CAPSLOCK_LED; ++ rqst.instance_id = shid->uid.instance; ++ rqst.flags = 0; ++ rqst.length = sizeof(value_u8); ++ rqst.payload = &value_u8; ++ ++ return ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, NULL, sizeof(value_u8)); ++} ++ ++static int ssam_kbd_get_feature_report(struct surface_hid_device *shid, u8 *buf, size_t len) ++{ ++ struct ssam_request rqst; ++ struct ssam_response rsp; ++ u8 payload = 0; ++ int status; ++ ++ rqst.target_category = shid->uid.category; ++ rqst.target_id = shid->uid.target; ++ rqst.command_id = SURFACE_KBD_CID_GET_FEATURE_REPORT; ++ rqst.instance_id = shid->uid.instance; ++ rqst.flags = SSAM_REQUEST_HAS_RESPONSE; ++ rqst.length = sizeof(payload); ++ rqst.payload = &payload; ++ ++ rsp.capacity = len; ++ rsp.length = 0; ++ rsp.pointer = buf; ++ ++ status = ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(payload)); ++ if (status) ++ return status; ++ ++ if (rsp.length != len) { ++ dev_err(shid->dev, "invalid feature report length: got %zu, expected, %zu\n", ++ rsp.length, len); ++ return -EPROTO; ++ } ++ ++ return 0; ++} ++ ++static bool ssam_kbd_is_input_event(const struct ssam_event *event) ++{ ++ if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_GENERIC) ++ return true; ++ ++ if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_HOTKEYS) ++ return true; ++ ++ return false; ++} ++ ++static u32 ssam_kbd_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); ++ int status; ++ ++ /* ++ * Check against device UID manually, as registry and device target ++ * category doesn't line up. ++ */ ++ ++ if (shid->uid.category != event->target_category) ++ return 0; ++ ++ if (shid->uid.target != event->target_id) ++ return 0; ++ ++ if (shid->uid.instance != event->instance_id) ++ return 0; ++ ++ if (!ssam_kbd_is_input_event(event)) ++ return 0; ++ ++ status = hid_input_report(shid->hid, HID_INPUT_REPORT, (u8 *)&event->data[0], ++ event->length, 0); ++ ++ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; ++} ++ ++ ++/* -- Transport driver (KBD). ----------------------------------------------- */ ++ ++static int skbd_get_caps_led_value(struct hid_device *hid, u8 rprt_id, u8 *buf, size_t len) ++{ ++ struct hid_field *field; ++ unsigned int offset, size; ++ int i; ++ ++ /* Get LED field. */ ++ field = hidinput_get_led_field(hid); ++ if (!field) ++ return -ENOENT; ++ ++ /* Check if we got the correct report. */ ++ if (len != hid_report_len(field->report)) ++ return -ENOENT; ++ ++ if (rprt_id != field->report->id) ++ return -ENOENT; ++ ++ /* Get caps lock LED index. */ ++ for (i = 0; i < field->report_count; i++) ++ if ((field->usage[i].hid & 0xffff) == 0x02) ++ break; ++ ++ if (i == field->report_count) ++ return -ENOENT; ++ ++ /* Extract value. */ ++ size = field->report_size; ++ offset = field->report_offset + i * size; ++ return !!hid_field_extract(hid, buf + 1, size, offset); ++} ++ ++static int skbd_output_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) ++{ ++ int caps_led; ++ int status; ++ ++ caps_led = skbd_get_caps_led_value(shid->hid, rprt_id, buf, len); ++ if (caps_led < 0) ++ return -EIO; /* Only caps LED output reports are supported. */ ++ ++ status = ssam_kbd_set_caps_led(shid, caps_led); ++ if (status < 0) ++ return status; ++ ++ return len; ++} ++ ++static int skbd_get_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) ++{ ++ u8 report[KBD_FEATURE_REPORT_SIZE]; ++ int status; ++ ++ /* ++ * The keyboard only has a single hard-coded read-only feature report ++ * of size KBD_FEATURE_REPORT_SIZE. Try to load it and compare its ++ * report ID against the requested one. ++ */ ++ ++ if (len < ARRAY_SIZE(report)) ++ return -ENOSPC; ++ ++ status = ssam_kbd_get_feature_report(shid, report, ARRAY_SIZE(report)); ++ if (status < 0) ++ return status; ++ ++ if (rprt_id != report[0]) ++ return -ENOENT; ++ ++ memcpy(buf, report, ARRAY_SIZE(report)); ++ return len; ++} ++ ++static int skbd_set_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) ++{ ++ /* Not supported. See skbd_get_feature_report() for details. */ ++ return -EIO; ++} ++ ++ ++/* -- Driver setup. --------------------------------------------------------- */ ++ ++static int surface_kbd_probe(struct platform_device *pdev) ++{ ++ struct ssam_controller *ctrl; ++ struct surface_hid_device *shid; ++ ++ /* Add device link to EC. */ ++ ctrl = ssam_client_bind(&pdev->dev); ++ if (IS_ERR(ctrl)) ++ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); ++ ++ shid = devm_kzalloc(&pdev->dev, sizeof(*shid), GFP_KERNEL); ++ if (!shid) ++ return -ENOMEM; ++ ++ shid->dev = &pdev->dev; ++ shid->ctrl = ctrl; ++ ++ shid->uid.domain = SSAM_DOMAIN_SERIALHUB; ++ shid->uid.category = SSAM_SSH_TC_KBD; ++ shid->uid.target = 2; ++ shid->uid.instance = 0; ++ shid->uid.function = 0; ++ ++ shid->notif.base.priority = 1; ++ shid->notif.base.fn = ssam_kbd_event_fn; ++ shid->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; ++ shid->notif.event.id.target_category = shid->uid.category; ++ shid->notif.event.id.instance = shid->uid.instance; ++ shid->notif.event.mask = SSAM_EVENT_MASK_NONE; ++ shid->notif.event.flags = 0; ++ ++ shid->ops.get_descriptor = ssam_kbd_get_descriptor; ++ shid->ops.output_report = skbd_output_report; ++ shid->ops.get_feature_report = skbd_get_feature_report; ++ shid->ops.set_feature_report = skbd_set_feature_report; ++ ++ platform_set_drvdata(pdev, shid); ++ return surface_hid_device_add(shid); ++} ++ ++static int surface_kbd_remove(struct platform_device *pdev) ++{ ++ surface_hid_device_destroy(platform_get_drvdata(pdev)); ++ return 0; ++} ++ ++static const struct acpi_device_id surface_kbd_match[] = { ++ { "MSHW0096" }, ++ { }, ++}; ++MODULE_DEVICE_TABLE(acpi, surface_kbd_match); ++ ++static struct platform_driver surface_kbd_driver = { ++ .probe = surface_kbd_probe, ++ .remove = surface_kbd_remove, ++ .driver = { ++ .name = "surface_keyboard", ++ .acpi_match_table = surface_kbd_match, ++ .pm = &surface_hid_pm_ops, ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++module_platform_driver(surface_kbd_driver); ++ ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("HID legacy transport driver for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig +index 8e95839341e8..b5a103d0ccd2 100644 +--- a/drivers/platform/x86/Kconfig ++++ b/drivers/platform/x86/Kconfig +@@ -1234,6 +1234,106 @@ config SURFACE_BOOK1_DGPU_SWITCH + This driver provides a sysfs switch to set the power-state of the + discrete GPU found on the Microsoft Surface Book 1. + +config SURFACE_ACPI_NOTIFY + tristate "Surface ACPI Notify Driver" + depends on SURFACE_AGGREGATOR -+ default m + help + Surface ACPI Notify (SAN) driver for Microsoft Surface devices. + @@ -3556,26 +3970,54 @@ index 000000000000..4d2b0528ca2e + thermal sensor access, and real-time clock information, depending on + the Surface device in question. + -+config SURFACE_BATTERY -+ tristate "Surface Battery Driver" -+ depends on SURFACE_AGGREGATOR_BUS -+ select POWER_SUPPLY -+ default m ++config SURFACE_AGGREGATOR_CDEV ++ tristate "Surface System Aggregator Module User-Space Interface" ++ depends on SURFACE_AGGREGATOR + help -+ Driver for battery and AC-adapter devices connected/managed via the -+ Surface System Aggregator Module (SSAM) EC. ++ Provides a misc-device interface to the Surface System Aggregator ++ Module (SSAM) controller. + -+ This driver provides battery-/AC-information and -status support for -+ Surface devices where said data is not exposed via the standard ACPI -+ devices. On those models (7th-generation, i.e. Surface pro 7, Surface -+ Laptop 3, and Surface Book 3), battery-/AC-status and -information is -+ instead handled directly via SSAM client devices. ++ This option provides a module (called surface_aggregator_cdev), that, ++ when loaded, will add a client device (and its respective driver) to ++ the SSAM controller. Said client device manages a misc-device ++ interface (/dev/surface/aggregator), which can be used by user-space ++ tools to directly communicate with the SSAM EC by sending requests and ++ receiving the corresponding responses. ++ ++ The provided interface is intended for debugging and development only, ++ and should not be used otherwise. ++ ++config SURFACE_AGGREGATOR_REGISTRY ++ tristate "Surface System Aggregator Module Device Registry" ++ depends on SURFACE_AGGREGATOR ++ depends on SURFACE_AGGREGATOR_BUS ++ help ++ Device-registry and device-hubs for Surface System Aggregator Module ++ (SSAM) devices. ++ ++ Provides a module and driver which act as a device-registry for SSAM ++ client devices that cannot be detected automatically, e.g. via ACPI. ++ Such devices are instead provided via this registry and attached via ++ device hubs, also provided in this module. ++ ++ Devices provided via this registry are: ++ - Platform profile (performance-/cooling-mode) device (5th- and later ++ generations). ++ - Battery/AC devices (7th-generation). ++ - HID input devices (7th-generation). ++ ++ Select M (recommended) or Y here if you want support for the above ++ mentioned devices on the corresponding Surface models. Without this ++ module, the respective devices will not be instantiated and thus any ++ functionality provided by them will be missing, even when drivers for ++ these devices are present. In other words, this module only provides ++ the respective client devices. Drivers for these devices still need to ++ be selected via the other options. + +config SURFACE_DTX -+ tristate "Surface Detachment System Driver" ++ tristate "Surface DTX (Detachment System) Driver" + depends on SURFACE_AGGREGATOR + depends on INPUT -+ default m + help + Driver for the Surface Book clipboard detachment system (DTX). + @@ -3592,30 +4034,10 @@ index 000000000000..4d2b0528ca2e + Aggregator Bus (i.e. CONFIG_SURFACE_AGGREGATOR_BUS=n). In that case, + some devices, specifically the Surface Book 3, will not be supported. + -+config SURFACE_HID -+ tristate "Surface HID Transport Driver" -+ depends on SURFACE_AGGREGATOR_BUS -+ depends on HID -+ default m -+ help -+ Transport driver for HID devices connected via the Surface System -+ Aggregator Module (SSAM). -+ -+ This driver provides support for HID input devices (e.g. touchpad and -+ keyboard) connected via SSAM. It is required for keyboard input on the -+ Surface Laptop 1 and 2, as well as keyboard and touchpad input on the -+ Surface Laptop 3 and Surface Book 3. -+ -+ Note that this module can be built without support for the Surface -+ Aggregator Bus (i.e. CONFIG_SURFACE_AGGREGATOR_BUS=n). In that case, -+ some devices, specifically the Surface Book 3 and Surface Laptop 3, -+ will not be supported. -+ +config SURFACE_PERFMODE + tristate "Surface Performance-Mode Driver" + depends on SURFACE_AGGREGATOR_BUS + depends on SYSFS -+ default m + help + Driver for the performance-/cooling-mode interface of Microsoft + Surface devices. @@ -3628,27 +4050,40 @@ index 000000000000..4d2b0528ca2e + + This driver provides a user-space interface (via sysfs) for + controlling said mode via the corresponding client device. -diff --git a/drivers/misc/surface_aggregator/clients/Makefile b/drivers/misc/surface_aggregator/clients/Makefile -new file mode 100644 -index 000000000000..4249af06d738 ---- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/Makefile -@@ -0,0 +1,10 @@ -+# SPDX-License-Identifier: GPL-2.0+ -+# Copyright (C) 2019-2020 Maximilian Luz + -+obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV) += surface_aggregator_cdev.o -+obj-$(CONFIG_SURFACE_AGGREGATOR_REGISTRY) += surface_aggregator_registry.o -+obj-$(CONFIG_SURFACE_ACPI_NOTIFY) += surface_acpi_notify.o -+obj-$(CONFIG_SURFACE_BATTERY) += surface_battery.o -+obj-$(CONFIG_SURFACE_DTX) += surface_dtx.o -+obj-$(CONFIG_SURFACE_HID) += surface_hid.o -+obj-$(CONFIG_SURFACE_PERFMODE) += surface_perfmode.o -diff --git a/drivers/misc/surface_aggregator/clients/surface_acpi_notify.c b/drivers/misc/surface_aggregator/clients/surface_acpi_notify.c + config INTEL_PUNIT_IPC + tristate "Intel P-Unit IPC Driver" + ---help--- +@@ -1360,6 +1460,7 @@ config PCENGINES_APU2 + will be called pcengines-apuv2. + + source "drivers/platform/x86/intel_speed_select_if/Kconfig" ++source "drivers/platform/x86/surface_aggregator/Kconfig" + + endif # X86_PLATFORM_DEVICES + +diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile +index a1e70973257c..328830619b21 100644 +--- a/drivers/platform/x86/Makefile ++++ b/drivers/platform/x86/Makefile +@@ -83,6 +83,12 @@ obj-$(CONFIG_INTEL_SMARTCONNECT) += intel-smartconnect.o + obj-$(CONFIG_ALIENWARE_WMI) += alienware-wmi.o + obj-$(CONFIG_INTEL_PMC_IPC) += intel_pmc_ipc.o + obj-$(CONFIG_TOUCHSCREEN_DMI) += touchscreen_dmi.o ++obj-$(CONFIG_SURFACE_AGGREGATOR) += surface_aggregator/ ++obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV) += surface_aggregator_cdev.o ++obj-$(CONFIG_SURFACE_AGGREGATOR_REGISTRY) += surface_aggregator_registry.o ++obj-$(CONFIG_SURFACE_ACPI_NOTIFY) += surface_acpi_notify.o ++obj-$(CONFIG_SURFACE_DTX) += surface_dtx.o ++obj-$(CONFIG_SURFACE_PERFMODE) += surface_perfmode.o + obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o + obj-$(CONFIG_SURFACE_3_BUTTON) += surface3_button.o + obj-$(CONFIG_SURFACE_3_POWER_OPREGION) += surface3_power.o +diff --git a/drivers/platform/x86/surface_acpi_notify.c b/drivers/platform/x86/surface_acpi_notify.c new file mode 100644 index 000000000000..ef9c1f8e8336 --- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/surface_acpi_notify.c ++++ b/drivers/platform/x86/surface_acpi_notify.c @@ -0,0 +1,886 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* @@ -4536,4476 +4971,563 @@ index 000000000000..ef9c1f8e8336 +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Surface ACPI Notify driver for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/clients/surface_aggregator_cdev.c b/drivers/misc/surface_aggregator/clients/surface_aggregator_cdev.c +diff --git a/drivers/platform/x86/surface_aggregator/Kconfig b/drivers/platform/x86/surface_aggregator/Kconfig new file mode 100644 -index 000000000000..79e28fab7e40 +index 000000000000..44c2493706bc --- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/surface_aggregator_cdev.c -@@ -0,0 +1,322 @@ ++++ b/drivers/platform/x86/surface_aggregator/Kconfig +@@ -0,0 +1,69 @@ ++# SPDX-License-Identifier: GPL-2.0+ ++# Copyright (C) 2019-2020 Maximilian Luz ++ ++menuconfig SURFACE_AGGREGATOR ++ tristate "Microsoft Surface System Aggregator Module Subsystem and Drivers" ++ depends on SERIAL_DEV_BUS ++ depends on ACPI ++ select CRC_CCITT ++ help ++ The Surface System Aggregator Module (Surface SAM or SSAM) is an ++ embedded controller (EC) found on 5th- and later-generation Microsoft ++ Surface devices (i.e. Surface Pro 5, Surface Book 2, Surface Laptop, ++ and newer, with exception of Surface Go series devices). ++ ++ Depending on the device in question, this EC provides varying ++ functionality, including: ++ - EC access from ACPI via Surface ACPI Notify (5th- and 6th-generation) ++ - battery status information (all devices) ++ - thermal sensor access (all devices) ++ - performance mode / cooling mode control (all devices) ++ - clipboard detachment system control (Surface Book 2 and 3) ++ - HID / keyboard input (Surface Laptops, Surface Book 3) ++ ++ This option controls whether the Surface SAM subsystem core will be ++ built. This includes a driver for the Surface Serial Hub (SSH), which ++ is the device responsible for the communication with the EC, and a ++ basic kernel interface exposing the EC functionality to other client ++ drivers, i.e. allowing them to make requests to the EC and receive ++ events from it. Selecting this option alone will not provide any ++ client drivers and therefore no functionality beyond the in-kernel ++ interface. Said functionality is the responsibility of the respective ++ client drivers. ++ ++ Note: While 4th-generation Surface devices also make use of a SAM EC, ++ due to a difference in the communication interface of the controller, ++ only 5th and later generations are currently supported. Specifically, ++ devices using SAM-over-SSH are supported, whereas devices using ++ SAM-over-HID, which is used on the 4th generation, are currently not ++ supported. ++ ++ Choose m if you want to build the SAM subsystem core and SSH driver as ++ module, y if you want to build it into the kernel and n if you don't ++ want it at all. ++ ++config SURFACE_AGGREGATOR_BUS ++ bool "Surface System Aggregator Module Bus" ++ depends on SURFACE_AGGREGATOR ++ default y ++ help ++ Expands the Surface System Aggregator Module (SSAM) core driver by ++ providing a dedicated bus and client-device type. ++ ++ This bus and device type are intended to provide and simplify support ++ for non-platform and non-ACPI SSAM devices, i.e. SSAM devices that are ++ not auto-detectable via the conventional means (e.g. ACPI). ++ ++config SURFACE_AGGREGATOR_ERROR_INJECTION ++ bool "Surface System Aggregator Module Error Injection Capabilities" ++ depends on SURFACE_AGGREGATOR ++ depends on FUNCTION_ERROR_INJECTION ++ help ++ Provides error-injection capabilities for the Surface System ++ Aggregator Module subsystem and Surface Serial Hub driver. ++ ++ Specifically, exports error injection hooks to be used with the ++ kernel's function error injection capabilities to simulate underlying ++ transport and communication problems, such as invalid data sent to or ++ received from the EC, dropped data, and communication timeouts. ++ Intended for development and debugging. +diff --git a/drivers/platform/x86/surface_aggregator/Makefile b/drivers/platform/x86/surface_aggregator/Makefile +new file mode 100644 +index 000000000000..c112e2c7112b +--- /dev/null ++++ b/drivers/platform/x86/surface_aggregator/Makefile +@@ -0,0 +1,17 @@ ++# SPDX-License-Identifier: GPL-2.0+ ++# Copyright (C) 2019-2020 Maximilian Luz ++ ++# For include/trace/define_trace.h to include trace.h ++CFLAGS_core.o = -I$(src) ++ ++obj-$(CONFIG_SURFACE_AGGREGATOR) += surface_aggregator.o ++ ++surface_aggregator-objs := core.o ++surface_aggregator-objs += ssh_parser.o ++surface_aggregator-objs += ssh_packet_layer.o ++surface_aggregator-objs += ssh_request_layer.o ++surface_aggregator-objs += controller.o ++ ++ifeq ($(CONFIG_SURFACE_AGGREGATOR_BUS),y) ++surface_aggregator-objs += bus.o ++endif +diff --git a/drivers/platform/x86/surface_aggregator/bus.c b/drivers/platform/x86/surface_aggregator/bus.c +new file mode 100644 +index 000000000000..b77927b278ca +--- /dev/null ++++ b/drivers/platform/x86/surface_aggregator/bus.c +@@ -0,0 +1,415 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* -+ * Provides user-space access to the SSAM EC via the /dev/surface/aggregator -+ * misc device. Intended for debugging and development. ++ * Surface System Aggregator Module bus and device integration. + * -+ * Copyright (C) 2020 Maximilian Luz ++ * Copyright (C) 2019-2020 Maximilian Luz + */ + -+#include -+#include -+#include -+#include -+#include -+#include -+#include ++#include +#include -+#include -+ -+#include -+#include -+ -+#define SSAM_CDEV_DEVICE_NAME "surface_aggregator_cdev" -+ -+struct ssam_cdev { -+ struct kref kref; -+ struct rw_semaphore lock; -+ struct ssam_controller *ctrl; -+ struct miscdevice mdev; -+}; -+ -+static void __ssam_cdev_release(struct kref *kref) -+{ -+ kfree(container_of(kref, struct ssam_cdev, kref)); -+} -+ -+static struct ssam_cdev *ssam_cdev_get(struct ssam_cdev *cdev) -+{ -+ if (cdev) -+ kref_get(&cdev->kref); -+ -+ return cdev; -+} -+ -+static void ssam_cdev_put(struct ssam_cdev *cdev) -+{ -+ if (cdev) -+ kref_put(&cdev->kref, __ssam_cdev_release); -+} -+ -+static int ssam_cdev_device_open(struct inode *inode, struct file *filp) -+{ -+ struct miscdevice *mdev = filp->private_data; -+ struct ssam_cdev *cdev = container_of(mdev, struct ssam_cdev, mdev); -+ -+ filp->private_data = ssam_cdev_get(cdev); -+ return stream_open(inode, filp); -+} -+ -+static int ssam_cdev_device_release(struct inode *inode, struct file *filp) -+{ -+ ssam_cdev_put(filp->private_data); -+ return 0; -+} -+ -+static long ssam_cdev_request(struct ssam_cdev *cdev, unsigned long arg) -+{ -+ struct ssam_cdev_request __user *r; -+ struct ssam_cdev_request rqst; -+ struct ssam_request spec = {}; -+ struct ssam_response rsp = {}; -+ const void __user *plddata; -+ void __user *rspdata; -+ int status = 0, ret = 0, tmp; -+ -+ r = (struct ssam_cdev_request __user *)arg; -+ ret = copy_struct_from_user(&rqst, sizeof(rqst), r, sizeof(*r)); -+ if (ret) -+ goto out; -+ -+ plddata = u64_to_user_ptr(rqst.payload.data); -+ rspdata = u64_to_user_ptr(rqst.response.data); -+ -+ /* Setup basic request fields. */ -+ spec.target_category = rqst.target_category; -+ spec.target_id = rqst.target_id; -+ spec.command_id = rqst.command_id; -+ spec.instance_id = rqst.instance_id; -+ spec.flags = 0; -+ spec.length = rqst.payload.length; -+ spec.payload = NULL; -+ -+ if (rqst.flags & SSAM_CDEV_REQUEST_HAS_RESPONSE) -+ spec.flags |= SSAM_REQUEST_HAS_RESPONSE; -+ -+ if (rqst.flags & SSAM_CDEV_REQUEST_UNSEQUENCED) -+ spec.flags |= SSAM_REQUEST_UNSEQUENCED; -+ -+ rsp.capacity = rqst.response.length; -+ rsp.length = 0; -+ rsp.pointer = NULL; -+ -+ /* Get request payload from user-space. */ -+ if (spec.length) { -+ if (!plddata) { -+ ret = -EINVAL; -+ goto out; -+ } -+ -+ /* -+ * Note: spec.length is limited to U16_MAX bytes via struct -+ * ssam_cdev_request. This is slightly larger than the -+ * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the -+ * underlying protocol (note that nothing remotely this size -+ * should ever be allocated in any normal case). This size is -+ * validated later in ssam_request_sync(), for allocation the -+ * bound imposed by u16 should be enough. -+ */ -+ spec.payload = kzalloc(spec.length, GFP_KERNEL); -+ if (!spec.payload) { -+ ret = -ENOMEM; -+ goto out; -+ } -+ -+ if (copy_from_user((void *)spec.payload, plddata, spec.length)) { -+ ret = -EFAULT; -+ goto out; -+ } -+ } -+ -+ /* Allocate response buffer. */ -+ if (rsp.capacity) { -+ if (!rspdata) { -+ ret = -EINVAL; -+ goto out; -+ } -+ -+ /* -+ * Note: rsp.capacity is limited to U16_MAX bytes via struct -+ * ssam_cdev_request. This is slightly larger than the -+ * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the -+ * underlying protocol (note that nothing remotely this size -+ * should ever be allocated in any normal case). In later use, -+ * this capacity does not have to be strictly bounded, as it -+ * is only used as an output buffer to be written to. For -+ * allocation the bound imposed by u16 should be enough. -+ */ -+ rsp.pointer = kzalloc(rsp.capacity, GFP_KERNEL); -+ if (!rsp.pointer) { -+ ret = -ENOMEM; -+ goto out; -+ } -+ } -+ -+ /* Perform request. */ -+ status = ssam_request_sync(cdev->ctrl, &spec, &rsp); -+ if (status) -+ goto out; -+ -+ /* Copy response to user-space. */ -+ if (rsp.length && copy_to_user(rspdata, rsp.pointer, rsp.length)) -+ ret = -EFAULT; -+ -+out: -+ /* Always try to set response-length and status. */ -+ tmp = put_user(rsp.length, &r->response.length); -+ if (tmp) -+ ret = tmp; -+ -+ tmp = put_user(status, &r->status); -+ if (tmp) -+ ret = tmp; -+ -+ /* Cleanup. */ -+ kfree(spec.payload); -+ kfree(rsp.pointer); -+ -+ return ret; -+} -+ -+static long __ssam_cdev_device_ioctl(struct ssam_cdev *cdev, unsigned int cmd, -+ unsigned long arg) -+{ -+ switch (cmd) { -+ case SSAM_CDEV_REQUEST: -+ return ssam_cdev_request(cdev, arg); -+ -+ default: -+ return -ENOTTY; -+ } -+} -+ -+static long ssam_cdev_device_ioctl(struct file *file, unsigned int cmd, -+ unsigned long arg) -+{ -+ struct ssam_cdev *cdev = file->private_data; -+ long status; -+ -+ /* Ensure that controller is valid for as long as we need it. */ -+ if (down_read_killable(&cdev->lock)) -+ return -ERESTARTSYS; -+ -+ if (!cdev->ctrl) { -+ up_read(&cdev->lock); -+ return -ENODEV; -+ } -+ -+ status = __ssam_cdev_device_ioctl(cdev, cmd, arg); -+ -+ up_read(&cdev->lock); -+ return status; -+} -+ -+static const struct file_operations ssam_controller_fops = { -+ .owner = THIS_MODULE, -+ .open = ssam_cdev_device_open, -+ .release = ssam_cdev_device_release, -+ .unlocked_ioctl = ssam_cdev_device_ioctl, -+ .compat_ioctl = ssam_cdev_device_ioctl, -+ .llseek = noop_llseek, -+}; -+ -+static int ssam_dbg_device_probe(struct platform_device *pdev) -+{ -+ struct ssam_controller *ctrl; -+ struct ssam_cdev *cdev; -+ int status; -+ -+ ctrl = ssam_client_bind(&pdev->dev); -+ if (IS_ERR(ctrl)) -+ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); -+ -+ cdev = kzalloc(sizeof(*cdev), GFP_KERNEL); -+ if (!cdev) -+ return -ENOMEM; -+ -+ kref_init(&cdev->kref); -+ init_rwsem(&cdev->lock); -+ cdev->ctrl = ctrl; -+ -+ cdev->mdev.parent = &pdev->dev; -+ cdev->mdev.minor = MISC_DYNAMIC_MINOR; -+ cdev->mdev.name = "surface_aggregator"; -+ cdev->mdev.nodename = "surface/aggregator"; -+ cdev->mdev.fops = &ssam_controller_fops; -+ -+ status = misc_register(&cdev->mdev); -+ if (status) { -+ kfree(cdev); -+ return status; -+ } -+ -+ platform_set_drvdata(pdev, cdev); -+ return 0; -+} -+ -+static int ssam_dbg_device_remove(struct platform_device *pdev) -+{ -+ struct ssam_cdev *cdev = platform_get_drvdata(pdev); -+ -+ misc_deregister(&cdev->mdev); -+ -+ /* -+ * The controller is only guaranteed to be valid for as long as the -+ * driver is bound. Remove controller so that any lingering open files -+ * cannot access it any more after we're gone. -+ */ -+ down_write(&cdev->lock); -+ cdev->ctrl = NULL; -+ up_write(&cdev->lock); -+ -+ ssam_cdev_put(cdev); -+ return 0; -+} -+ -+static struct platform_device *ssam_cdev_device; -+ -+static struct platform_driver ssam_cdev_driver = { -+ .probe = ssam_dbg_device_probe, -+ .remove = ssam_dbg_device_remove, -+ .driver = { -+ .name = SSAM_CDEV_DEVICE_NAME, -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+static int __init ssam_debug_init(void) -+{ -+ int status; -+ -+ ssam_cdev_device = platform_device_alloc(SSAM_CDEV_DEVICE_NAME, -+ PLATFORM_DEVID_NONE); -+ if (!ssam_cdev_device) -+ return -ENOMEM; -+ -+ status = platform_device_add(ssam_cdev_device); -+ if (status) -+ goto err_device; -+ -+ status = platform_driver_register(&ssam_cdev_driver); -+ if (status) -+ goto err_driver; -+ -+ return 0; -+ -+err_driver: -+ platform_device_del(ssam_cdev_device); -+err_device: -+ platform_device_put(ssam_cdev_device); -+ return status; -+} -+module_init(ssam_debug_init); -+ -+static void __exit ssam_debug_exit(void) -+{ -+ platform_driver_unregister(&ssam_cdev_driver); -+ platform_device_unregister(ssam_cdev_device); -+} -+module_exit(ssam_debug_exit); -+ -+MODULE_AUTHOR("Maximilian Luz "); -+MODULE_DESCRIPTION("User-space interface for Surface System Aggregator Module"); -+MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/clients/surface_aggregator_registry.c b/drivers/misc/surface_aggregator/clients/surface_aggregator_registry.c -new file mode 100644 -index 000000000000..eae9be09a6a3 ---- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/surface_aggregator_registry.c -@@ -0,0 +1,616 @@ -+// SPDX-License-Identifier: GPL-2.0+ -+/* -+ * Surface System Aggregator Module (SSAM) client device registry. -+ * -+ * Registry for non-platform/non-ACPI SSAM client devices, i.e. devices that -+ * cannot be auto-detected. Provides device-hubs for these devices. -+ * -+ * Copyright (C) 2020 Maximilian Luz -+ */ -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include + +#include +#include + ++#include "bus.h" ++#include "controller.h" + -+/* -- Device registry structures. ------------------------------------------- */ -+ -+struct ssam_hub_cell { -+ struct ssam_device_uid uid; -+ void *data; -+}; -+ -+struct ssam_hub_desc { -+ const struct ssam_hub_cell *cells; -+ unsigned int num_cells; -+}; -+ -+ -+#define SSAM_DUID(cat, tid, iid, fun) \ -+ ((struct ssam_device_uid) { \ -+ .domain = SSAM_DOMAIN_SERIALHUB, \ -+ .category = SSAM_SSH_TC_##cat, \ -+ .target = (tid), \ -+ .instance = (iid), \ -+ .function = (fun) \ -+ }) -+ -+#define SSAM_VDUID(cat, tid, iid, fun) \ -+ ((struct ssam_device_uid) { \ -+ .domain = SSAM_DOMAIN_VIRTUAL, \ -+ .category = SSAM_VIRTUAL_TC_##cat, \ -+ .target = (tid), \ -+ .instance = (iid), \ -+ .function = (fun) \ -+ }) -+ -+#define SSAM_DUID_HUB_MAIN SSAM_VDUID(HUB, 0x01, 0x00, 0x00) -+#define SSAM_DUID_HUB_BASE SSAM_VDUID(HUB, 0x02, 0x00, 0x00) -+ -+#define SSAM_DEFINE_HUB_DESC(__name, __cells) \ -+ struct ssam_hub_desc __name = { \ -+ .cells = __cells, \ -+ .num_cells = ARRAY_SIZE(__cells), \ -+ }; -+ -+#define SSAM_DEFINE_PLATFORM_HUB(__suffix) \ -+ static const SSAM_DEFINE_HUB_DESC(ssam_device_hub_##__suffix, \ -+ ssam_devices_##__suffix); \ -+ static const struct ssam_hub_cell ssam_platform_hubs_##__suffix[] = { \ -+ { SSAM_DUID_HUB_MAIN, (void *)&ssam_device_hub_##__suffix }, \ -+ }; \ -+ static const SSAM_DEFINE_HUB_DESC(ssam_platform_hub_##__suffix, \ -+ ssam_platform_hubs_##__suffix); \ -+ -+#define SSAM_DEFINE_PLATFORM_HUB_WITH_BASE(__suffix) \ -+ static const SSAM_DEFINE_HUB_DESC(ssam_device_hub_##__suffix, \ -+ ssam_devices_##__suffix); \ -+ static const SSAM_DEFINE_HUB_DESC(ssam_device_hub_##__suffix##_base, \ -+ ssam_devices_##__suffix##_base); \ -+ static const struct ssam_hub_cell ssam_platform_hubs_##__suffix[] = { \ -+ { SSAM_DUID_HUB_MAIN, (void *)&ssam_device_hub_##__suffix }, \ -+ { SSAM_DUID_HUB_BASE, (void *)&ssam_device_hub_##__suffix##_base },\ -+ }; \ -+ static const SSAM_DEFINE_HUB_DESC(ssam_platform_hub_##__suffix, \ -+ ssam_platform_hubs_##__suffix); \ -+ -+ -+/* -- Device registry. ------------------------------------------------------ */ -+ -+#define SSAM_DUID_BAT_AC SSAM_DUID(BAT, 0x01, 0x01, 0x01) -+#define SSAM_DUID_BAT_MAIN SSAM_DUID(BAT, 0x01, 0x01, 0x00) -+#define SSAM_DUID_BAT_SB3BASE SSAM_DUID(BAT, 0x02, 0x01, 0x00) -+ -+#define SSAM_DUID_TMP_PERF SSAM_DUID(TMP, 0x01, 0x00, 0x01) -+ -+#define SSAM_DUID_BAS_DTX SSAM_DUID(BAS, 0x01, 0x00, 0x00) -+ -+#define SSAM_DUID_HID_KEYBOARD SSAM_DUID(HID, 0x02, 0x01, 0x00) -+#define SSAM_DUID_HID_TOUCHPAD SSAM_DUID(HID, 0x02, 0x03, 0x00) -+#define SSAM_DUID_HID_IID5 SSAM_DUID(HID, 0x02, 0x05, 0x00) -+#define SSAM_DUID_HID_IID6 SSAM_DUID(HID, 0x02, 0x06, 0x00) -+ -+ -+static const struct ssam_hub_cell ssam_devices_sb2[] = { -+ { SSAM_DUID_TMP_PERF }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sb3[] = { -+ { SSAM_DUID_TMP_PERF }, -+ { SSAM_DUID_BAT_AC }, -+ { SSAM_DUID_BAT_MAIN }, -+ { SSAM_DUID_BAS_DTX }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sb3_base[] = { -+ { SSAM_DUID_BAT_SB3BASE }, -+ { SSAM_DUID_HID_KEYBOARD }, -+ { SSAM_DUID_HID_TOUCHPAD }, -+ { SSAM_DUID_HID_IID5 }, -+ { SSAM_DUID_HID_IID6 }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sl1[] = { -+ { SSAM_DUID_TMP_PERF }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sl2[] = { -+ { SSAM_DUID_TMP_PERF }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sl3[] = { -+ { SSAM_DUID_TMP_PERF }, -+ { SSAM_DUID_BAT_AC }, -+ { SSAM_DUID_BAT_MAIN }, -+ { SSAM_DUID_HID_KEYBOARD }, -+ { SSAM_DUID_HID_TOUCHPAD }, -+ { SSAM_DUID_HID_IID5 }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_slg1[] = { -+ { SSAM_DUID_TMP_PERF }, -+ { SSAM_DUID_BAT_AC }, -+ { SSAM_DUID_BAT_MAIN }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sp5[] = { -+ { SSAM_DUID_TMP_PERF }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sp6[] = { -+ { SSAM_DUID_TMP_PERF }, -+}; -+ -+static const struct ssam_hub_cell ssam_devices_sp7[] = { -+ { SSAM_DUID_TMP_PERF }, -+ { SSAM_DUID_BAT_AC }, -+ { SSAM_DUID_BAT_MAIN }, -+}; -+ -+SSAM_DEFINE_PLATFORM_HUB(sb2); -+SSAM_DEFINE_PLATFORM_HUB_WITH_BASE(sb3); -+SSAM_DEFINE_PLATFORM_HUB(sl1); -+SSAM_DEFINE_PLATFORM_HUB(sl2); -+SSAM_DEFINE_PLATFORM_HUB(sl3); -+SSAM_DEFINE_PLATFORM_HUB(slg1); -+SSAM_DEFINE_PLATFORM_HUB(sp5); -+SSAM_DEFINE_PLATFORM_HUB(sp6); -+SSAM_DEFINE_PLATFORM_HUB(sp7); -+ -+ -+/* -- Device registry helper functions. ------------------------------------- */ -+ -+static int ssam_hub_remove_devices_fn(struct device *dev, void *data) ++static ssize_t modalias_show(struct device *dev, struct device_attribute *attr, ++ char *buf) +{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ ++ return scnprintf(buf, PAGE_SIZE, "ssam:d%02Xc%02Xt%02Xi%02Xf%02X\n", ++ sdev->uid.domain, sdev->uid.category, sdev->uid.target, ++ sdev->uid.instance, sdev->uid.function); ++} ++static DEVICE_ATTR_RO(modalias); ++ ++static struct attribute *ssam_device_attrs[] = { ++ &dev_attr_modalias.attr, ++ NULL, ++}; ++ATTRIBUTE_GROUPS(ssam_device); ++ ++static int ssam_device_uevent(struct device *dev, struct kobj_uevent_env *env) ++{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ ++ return add_uevent_var(env, "MODALIAS=ssam:d%02Xc%02Xt%02Xi%02Xf%02X", ++ sdev->uid.domain, sdev->uid.category, ++ sdev->uid.target, sdev->uid.instance, ++ sdev->uid.function); ++} ++ ++static void ssam_device_release(struct device *dev) ++{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ ++ ssam_controller_put(sdev->ctrl); ++ kfree(sdev); ++} ++ ++const struct device_type ssam_device_type = { ++ .name = "surface_aggregator_device", ++ .groups = ssam_device_groups, ++ .uevent = ssam_device_uevent, ++ .release = ssam_device_release, ++}; ++EXPORT_SYMBOL_GPL(ssam_device_type); ++ ++/** ++ * ssam_device_alloc() - Allocate and initialize a SSAM client device. ++ * @ctrl: The controller under which the device should be added. ++ * @uid: The UID of the device to be added. ++ * ++ * Allocates and initializes a new client device. The parent of the device ++ * will be set to the controller device and the name will be set based on the ++ * UID. Note that the device still has to be added via ssam_device_add(). ++ * Refer to that function for more details. ++ * ++ * Return: Returns the newly allocated and initialized SSAM client device, or ++ * %NULL if it could not be allocated. ++ */ ++struct ssam_device *ssam_device_alloc(struct ssam_controller *ctrl, ++ struct ssam_device_uid uid) ++{ ++ struct ssam_device *sdev; ++ ++ sdev = kzalloc(sizeof(*sdev), GFP_KERNEL); ++ if (!sdev) ++ return NULL; ++ ++ device_initialize(&sdev->dev); ++ sdev->dev.bus = &ssam_bus_type; ++ sdev->dev.type = &ssam_device_type; ++ sdev->dev.parent = ssam_controller_device(ctrl); ++ sdev->ctrl = ssam_controller_get(ctrl); ++ sdev->uid = uid; ++ ++ dev_set_name(&sdev->dev, "%02x:%02x:%02x:%02x:%02x", ++ sdev->uid.domain, sdev->uid.category, sdev->uid.target, ++ sdev->uid.instance, sdev->uid.function); ++ ++ return sdev; ++} ++EXPORT_SYMBOL_GPL(ssam_device_alloc); ++ ++/** ++ * ssam_device_add() - Add a SSAM client device. ++ * @sdev: The SSAM client device to be added. ++ * ++ * Added client devices must be guaranteed to always have a valid and active ++ * controller. Thus, this function will fail with %-ENODEV if the controller ++ * of the device has not been initialized yet, has been suspended, or has been ++ * shut down. ++ * ++ * The caller of this function should ensure that the corresponding call to ++ * ssam_device_remove() is issued before the controller is shut down. If the ++ * added device is a direct child of the controller device (default), it will ++ * be automatically removed when the controller is shut down. ++ * ++ * By default, the controller device will become the parent of the newly ++ * created client device. The parent may be changed before ssam_device_add is ++ * called, but care must be taken that a) the correct suspend/resume ordering ++ * is guaranteed and b) the client device does not outlive the controller, ++ * i.e. that the device is removed before the controller is being shut down. ++ * In case these guarantees have to be manually enforced, please refer to the ++ * ssam_client_link() and ssam_client_bind() functions, which are intended to ++ * set up device-links for this purpose. ++ * ++ * Return: Returns zero on success, a negative error code on failure. ++ */ ++int ssam_device_add(struct ssam_device *sdev) ++{ ++ int status; ++ ++ /* ++ * Ensure that we can only add new devices to a controller if it has ++ * been started and is not going away soon. This works in combination ++ * with ssam_controller_remove_clients to ensure driver presence for the ++ * controller device, i.e. it ensures that the controller (sdev->ctrl) ++ * is always valid and can be used for requests as long as the client ++ * device we add here is registered as child under it. This essentially ++ * guarantees that the client driver can always expect the preconditions ++ * for functions like ssam_request_sync (controller has to be started ++ * and is not suspended) to hold and thus does not have to check for ++ * them. ++ * ++ * Note that for this to work, the controller has to be a parent device. ++ * If it is not a direct parent, care has to be taken that the device is ++ * removed via ssam_device_remove(), as device_unregister does not ++ * remove child devices recursively. ++ */ ++ ssam_controller_statelock(sdev->ctrl); ++ ++ if (sdev->ctrl->state != SSAM_CONTROLLER_STARTED) { ++ ssam_controller_stateunlock(sdev->ctrl); ++ return -ENODEV; ++ } ++ ++ status = device_add(&sdev->dev); ++ ++ ssam_controller_stateunlock(sdev->ctrl); ++ return status; ++} ++EXPORT_SYMBOL_GPL(ssam_device_add); ++ ++/** ++ * ssam_device_remove() - Remove a SSAM client device. ++ * @sdev: The device to remove. ++ * ++ * Removes and unregisters the provided SSAM client device. ++ */ ++void ssam_device_remove(struct ssam_device *sdev) ++{ ++ device_unregister(&sdev->dev); ++} ++EXPORT_SYMBOL_GPL(ssam_device_remove); ++ ++/** ++ * ssam_device_id_compatible() - Check if a device ID matches a UID. ++ * @id: The device ID as potential match. ++ * @uid: The device UID matching against. ++ * ++ * Check if the given ID is a match for the given UID, i.e. if a device with ++ * the provided UID is compatible to the given ID following the match rules ++ * described in its &ssam_device_id.match_flags member. ++ * ++ * Return: Returns %true if the given UID is compatible to the match rule ++ * described by the given ID, %false otherwise. ++ */ ++static bool ssam_device_id_compatible(const struct ssam_device_id *id, ++ struct ssam_device_uid uid) ++{ ++ if (id->domain != uid.domain || id->category != uid.category) ++ return false; ++ ++ if ((id->match_flags & SSAM_MATCH_TARGET) && id->target != uid.target) ++ return false; ++ ++ if ((id->match_flags & SSAM_MATCH_INSTANCE) && id->instance != uid.instance) ++ return false; ++ ++ if ((id->match_flags & SSAM_MATCH_FUNCTION) && id->function != uid.function) ++ return false; ++ ++ return true; ++} ++ ++/** ++ * ssam_device_id_is_null() - Check if a device ID is null. ++ * @id: The device ID to check. ++ * ++ * Check if a given device ID is null, i.e. all zeros. Used to check for the ++ * end of ``MODULE_DEVICE_TABLE(ssam, ...)`` or similar lists. ++ * ++ * Return: Returns %true if the given ID represents a null ID, %false ++ * otherwise. ++ */ ++static bool ssam_device_id_is_null(const struct ssam_device_id *id) ++{ ++ return id->match_flags == 0 && ++ id->domain == 0 && ++ id->category == 0 && ++ id->target == 0 && ++ id->instance == 0 && ++ id->function == 0 && ++ id->driver_data == 0; ++} ++ ++/** ++ * ssam_device_id_match() - Find the matching ID table entry for the given UID. ++ * @table: The table to search in. ++ * @uid: The UID to matched against the individual table entries. ++ * ++ * Find the first match for the provided device UID in the provided ID table ++ * and return it. Returns %NULL if no match could be found. ++ */ ++const struct ssam_device_id *ssam_device_id_match(const struct ssam_device_id *table, ++ const struct ssam_device_uid uid) ++{ ++ const struct ssam_device_id *id; ++ ++ for (id = table; !ssam_device_id_is_null(id); ++id) ++ if (ssam_device_id_compatible(id, uid)) ++ return id; ++ ++ return NULL; ++} ++EXPORT_SYMBOL_GPL(ssam_device_id_match); ++ ++/** ++ * ssam_device_get_match() - Find and return the ID matching the device in the ++ * ID table of the bound driver. ++ * @dev: The device for which to get the matching ID table entry. ++ * ++ * Find the fist match for the UID of the device in the ID table of the ++ * currently bound driver and return it. Returns %NULL if the device does not ++ * have a driver bound to it, the driver does not have match_table (i.e. it is ++ * %NULL), or there is no match in the driver's match_table. ++ * ++ * This function essentially calls ssam_device_id_match() with the ID table of ++ * the bound device driver and the UID of the device. ++ * ++ * Return: Returns the first match for the UID of the device in the device ++ * driver's match table, or %NULL if no such match could be found. ++ */ ++const struct ssam_device_id *ssam_device_get_match(const struct ssam_device *dev) ++{ ++ const struct ssam_device_driver *sdrv; ++ ++ sdrv = to_ssam_device_driver(dev->dev.driver); ++ if (!sdrv) ++ return NULL; ++ ++ if (!sdrv->match_table) ++ return NULL; ++ ++ return ssam_device_id_match(sdrv->match_table, dev->uid); ++} ++EXPORT_SYMBOL_GPL(ssam_device_get_match); ++ ++/** ++ * ssam_device_get_match_data() - Find the ID matching the device in the ++ * ID table of the bound driver and return its ``driver_data`` member. ++ * @dev: The device for which to get the match data. ++ * ++ * Find the fist match for the UID of the device in the ID table of the ++ * corresponding driver and return its driver_data. Returns %NULL if the ++ * device does not have a driver bound to it, the driver does not have ++ * match_table (i.e. it is %NULL), there is no match in the driver's ++ * match_table, or the match does not have any driver_data. ++ * ++ * This function essentially calls ssam_device_get_match() and, if any match ++ * could be found, returns its ``struct ssam_device_id.driver_data`` member. ++ * ++ * Return: Returns the driver data associated with the first match for the UID ++ * of the device in the device driver's match table, or %NULL if no such match ++ * could be found. ++ */ ++const void *ssam_device_get_match_data(const struct ssam_device *dev) ++{ ++ const struct ssam_device_id *id; ++ ++ id = ssam_device_get_match(dev); ++ if (!id) ++ return NULL; ++ ++ return (const void *)id->driver_data; ++} ++EXPORT_SYMBOL_GPL(ssam_device_get_match_data); ++ ++static int ssam_bus_match(struct device *dev, struct device_driver *drv) ++{ ++ struct ssam_device_driver *sdrv = to_ssam_device_driver(drv); ++ struct ssam_device *sdev = to_ssam_device(dev); ++ + if (!is_ssam_device(dev)) + return 0; + -+ ssam_device_remove(to_ssam_device(dev)); -+ return 0; ++ return !!ssam_device_id_match(sdrv->match_table, sdev->uid); +} + -+static void ssam_hub_remove_devices(struct device *parent) ++static int ssam_bus_probe(struct device *dev) +{ -+ device_for_each_child_reverse(parent, NULL, ssam_hub_remove_devices_fn); ++ return to_ssam_device_driver(dev->driver) ++ ->probe(to_ssam_device(dev)); +} + -+static int ssam_hub_add_device(struct device *parent, -+ struct ssam_controller *ctrl, -+ const struct ssam_hub_cell *cell) ++static int ssam_bus_remove(struct device *dev) +{ -+ struct ssam_device *sdev; -+ int status; ++ struct ssam_device_driver *sdrv = to_ssam_device_driver(dev->driver); + -+ sdev = ssam_device_alloc(ctrl, cell->uid); -+ if (!sdev) -+ return -ENOMEM; -+ -+ sdev->dev.parent = parent; -+ sdev->dev.platform_data = cell->data; -+ -+ status = ssam_device_add(sdev); -+ if (status) -+ ssam_device_put(sdev); -+ -+ return status; -+} -+ -+static int ssam_hub_add_devices(struct device *parent, -+ struct ssam_controller *ctrl, -+ const struct ssam_hub_desc *desc) -+{ -+ int status, i; -+ -+ for (i = 0; i < desc->num_cells; i++) { -+ status = ssam_hub_add_device(parent, ctrl, &desc->cells[i]); -+ if (status) -+ goto err; -+ } -+ -+ return 0; -+err: -+ ssam_hub_remove_devices(parent); -+ return status; -+} -+ -+ -+/* -- SSAM main-hub driver. ------------------------------------------------- */ -+ -+static int ssam_hub_probe(struct ssam_device *sdev) -+{ -+ const struct ssam_hub_desc *desc = dev_get_platdata(&sdev->dev); -+ -+ if (!desc) -+ return -ENODEV; -+ -+ return ssam_hub_add_devices(&sdev->dev, sdev->ctrl, desc); -+} -+ -+static void ssam_hub_remove(struct ssam_device *sdev) -+{ -+ ssam_hub_remove_devices(&sdev->dev); -+} -+ -+static const struct ssam_device_id ssam_hub_match[] = { -+ { SSAM_VDEV(HUB, 0x01, 0x00, 0x00) }, -+ { }, -+}; -+ -+static struct ssam_device_driver ssam_hub_driver = { -+ .probe = ssam_hub_probe, -+ .remove = ssam_hub_remove, -+ .match_table = ssam_hub_match, -+ .driver = { -+ .name = "surface_aggregator_device_hub", -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+ -+/* -- SSAM base-hub driver. ------------------------------------------------- */ -+ -+enum ssam_base_hub_state { -+ SSAM_BASE_HUB_UNINITIALIZED, -+ SSAM_BASE_HUB_CONNECTED, -+ SSAM_BASE_HUB_DISCONNECTED, -+}; -+ -+struct ssam_base_hub { -+ struct ssam_device *sdev; -+ const struct ssam_hub_desc *devices; -+ -+ struct mutex lock; -+ enum ssam_base_hub_state state; -+ -+ struct ssam_event_notifier notif; -+}; -+ -+static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x0d, -+ .instance_id = 0x00, -+}); -+ -+#define SSAM_BAS_OPMODE_TABLET 0x00 -+#define SSAM_EVENT_BAS_CID_CONNECTION 0x0c -+ -+static int ssam_base_hub_query_state(struct ssam_device *sdev, -+ enum ssam_base_hub_state *state) -+{ -+ u8 opmode; -+ int status; -+ -+ status = ssam_retry(ssam_bas_query_opmode, sdev->ctrl, &opmode); -+ if (status < 0) { -+ dev_err(&sdev->dev, "failed to query base state: %d\n", status); -+ return status; -+ } -+ -+ if (opmode != SSAM_BAS_OPMODE_TABLET) -+ *state = SSAM_BASE_HUB_CONNECTED; -+ else -+ *state = SSAM_BASE_HUB_DISCONNECTED; ++ if (sdrv->remove) ++ sdrv->remove(to_ssam_device(dev)); + + return 0; +} + -+static ssize_t ssam_base_hub_state_show(struct device *dev, -+ struct device_attribute *attr, -+ char *buf) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); -+ struct ssam_base_hub *hub = ssam_device_get_drvdata(sdev); -+ bool connected; -+ -+ mutex_lock(&hub->lock); -+ connected = hub->state == SSAM_BASE_HUB_CONNECTED; -+ mutex_unlock(&hub->lock); -+ -+ // FIXME: we should use sysfs_emit here, but that's not available on < 5.10 -+ return scnprintf(buf, PAGE_SIZE, "%d\n", connected); -+} -+ -+static struct device_attribute ssam_base_hub_attr_state = -+ __ATTR(state, S_IRUGO, ssam_base_hub_state_show, NULL); -+ -+static struct attribute *ssam_base_hub_attrs[] = { -+ &ssam_base_hub_attr_state.attr, -+ NULL, ++struct bus_type ssam_bus_type = { ++ .name = "surface_aggregator", ++ .match = ssam_bus_match, ++ .probe = ssam_bus_probe, ++ .remove = ssam_bus_remove, +}; ++EXPORT_SYMBOL_GPL(ssam_bus_type); + -+const struct attribute_group ssam_base_hub_group = { -+ .attrs = ssam_base_hub_attrs, -+}; -+ -+static int ssam_base_hub_update(struct ssam_device *sdev, -+ enum ssam_base_hub_state new) -+{ -+ struct ssam_base_hub *hub = ssam_device_get_drvdata(sdev); -+ int status = 0; -+ -+ mutex_lock(&hub->lock); -+ if (hub->state == new) { -+ mutex_unlock(&hub->lock); -+ return 0; -+ } -+ hub->state = new; -+ -+ if (hub->state == SSAM_BASE_HUB_CONNECTED) -+ status = ssam_hub_add_devices(&sdev->dev, sdev->ctrl, hub->devices); -+ -+ if (hub->state != SSAM_BASE_HUB_CONNECTED || status) -+ ssam_hub_remove_devices(&sdev->dev); -+ -+ mutex_unlock(&hub->lock); -+ -+ if (status) { -+ dev_err(&sdev->dev, "failed to update base-hub devices: %d\n", -+ status); -+ } -+ -+ return status; -+} -+ -+static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, -+ const struct ssam_event *event) -+{ -+ struct ssam_base_hub *hub; -+ struct ssam_device *sdev; -+ enum ssam_base_hub_state new; -+ -+ hub = container_of(nf, struct ssam_base_hub, notif); -+ sdev = hub->sdev; -+ -+ if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION) -+ return 0; -+ -+ if (event->length < 1) { -+ dev_err(&sdev->dev, "unexpected payload size: %u\n", -+ event->length); -+ return 0; -+ } -+ -+ if (event->data[0]) -+ new = SSAM_BASE_HUB_CONNECTED; -+ else -+ new = SSAM_BASE_HUB_DISCONNECTED; -+ -+ ssam_base_hub_update(sdev, new); -+ -+ /* -+ * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and -+ * consumed by the detachment system driver. We're just a (more or less) -+ * silent observer. -+ */ -+ return 0; -+} -+ -+static int __maybe_unused ssam_base_hub_resume(struct device *dev) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); -+ enum ssam_base_hub_state state; -+ int status; -+ -+ status = ssam_base_hub_query_state(sdev, &state); -+ if (status) -+ return status; -+ -+ return ssam_base_hub_update(sdev, state); -+} -+static SIMPLE_DEV_PM_OPS(ssam_base_hub_pm_ops, NULL, ssam_base_hub_resume); -+ -+static int ssam_base_hub_probe(struct ssam_device *sdev) -+{ -+ const struct ssam_hub_desc *desc = dev_get_platdata(&sdev->dev); -+ const struct ssam_device_id *match; -+ enum ssam_base_hub_state state; -+ struct ssam_base_hub *hub; -+ int status; -+ -+ if (!desc) -+ return -ENODEV; -+ -+ match = ssam_device_get_match(sdev); -+ if (!match) -+ return -ENODEV; -+ -+ hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL); -+ if (!hub) -+ return -ENOMEM; -+ -+ mutex_init(&hub->lock); -+ -+ hub->sdev = sdev; -+ hub->devices = desc; -+ hub->state = SSAM_BASE_HUB_UNINITIALIZED; -+ -+ hub->notif.base.priority = 1000; /* This notifier should run first. */ -+ hub->notif.base.fn = ssam_base_hub_notif; -+ hub->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; -+ hub->notif.event.id.target_category = SSAM_SSH_TC_BAS, -+ hub->notif.event.id.instance = 0, -+ hub->notif.event.mask = SSAM_EVENT_MASK_NONE; -+ hub->notif.event.flags = SSAM_EVENT_SEQUENCED; -+ -+ status = ssam_notifier_register(sdev->ctrl, &hub->notif); -+ if (status) -+ return status; -+ -+ ssam_device_set_drvdata(sdev, hub); -+ -+ status = ssam_base_hub_query_state(sdev, &state); -+ if (status) { -+ ssam_notifier_unregister(sdev->ctrl, &hub->notif); -+ return status; -+ } -+ -+ status = ssam_base_hub_update(sdev, state); -+ if (status) { -+ ssam_notifier_unregister(sdev->ctrl, &hub->notif); -+ return status; -+ } -+ -+ status = sysfs_create_group(&sdev->dev.kobj, &ssam_base_hub_group); -+ if (status) { -+ ssam_notifier_unregister(sdev->ctrl, &hub->notif); -+ ssam_hub_remove_devices(&sdev->dev); -+ } -+ -+ return status; -+} -+ -+static void ssam_base_hub_remove(struct ssam_device *sdev) -+{ -+ struct ssam_base_hub *hub = ssam_device_get_drvdata(sdev); -+ -+ sysfs_remove_group(&sdev->dev.kobj, &ssam_base_hub_group); -+ -+ ssam_notifier_unregister(sdev->ctrl, &hub->notif); -+ ssam_hub_remove_devices(&sdev->dev); -+ -+ mutex_destroy(&hub->lock); -+} -+ -+static const struct ssam_device_id ssam_base_hub_match[] = { -+ { SSAM_VDEV(HUB, 0x02, 0x00, 0x00) }, -+ { }, -+}; -+ -+static struct ssam_device_driver ssam_base_hub_driver = { -+ .probe = ssam_base_hub_probe, -+ .remove = ssam_base_hub_remove, -+ .match_table = ssam_base_hub_match, -+ .driver = { -+ .name = "surface_aggregator_base_hub", -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ .pm = &ssam_base_hub_pm_ops, -+ }, -+}; -+ -+ -+/* -- SSAM platform/meta-hub driver. ---------------------------------------- */ -+ -+static const struct acpi_device_id ssam_platform_hub_match[] = { -+ /* Surface Pro 4, 5, and 6 */ -+ { "MSHW0081", (unsigned long)&ssam_platform_hub_sp5 }, -+ -+ /* Surface Pro 6 (OMBR >= 0x10) */ -+ { "MSHW0111", (unsigned long)&ssam_platform_hub_sp6 }, -+ -+ /* Surface Pro 7 */ -+ { "MSHW0116", (unsigned long)&ssam_platform_hub_sp7 }, -+ -+ /* Surface Book 2 */ -+ { "MSHW0107", (unsigned long)&ssam_platform_hub_sb2 }, -+ -+ /* Surface Book 3 */ -+ { "MSHW0117", (unsigned long)&ssam_platform_hub_sb3 }, -+ -+ /* Surface Laptop 1 */ -+ { "MSHW0086", (unsigned long)&ssam_platform_hub_sl1 }, -+ -+ /* Surface Laptop 2 */ -+ { "MSHW0112", (unsigned long)&ssam_platform_hub_sl2 }, -+ -+ /* Surface Laptop 3 (13", Intel) */ -+ { "MSHW0114", (unsigned long)&ssam_platform_hub_sl3 }, -+ -+ /* Surface Laptop 3 (15", AMD) */ -+ { "MSHW0110", (unsigned long)&ssam_platform_hub_sl3 }, -+ -+ /* Surface Laptop Go 1 */ -+ { "MSHW0118", (unsigned long)&ssam_platform_hub_slg1 }, -+ -+ { }, -+}; -+MODULE_DEVICE_TABLE(acpi, ssam_platform_hub_match); -+ -+static int ssam_platform_hub_probe(struct platform_device *pdev) -+{ -+ const struct ssam_hub_desc *desc; -+ struct ssam_controller *ctrl; -+ -+ desc = acpi_device_get_match_data(&pdev->dev); -+ if (!desc) -+ return -ENODEV; -+ -+ /* -+ * As we're adding the SSAM client devices as children under this device -+ * and not the SSAM controller, we need to add a device link to the -+ * controller to ensure that we remove all of our devices before the -+ * controller is removed. This also guarantees proper ordering for -+ * suspend/resume of the devices on this hub. -+ */ -+ ctrl = ssam_client_bind(&pdev->dev); -+ if (IS_ERR(ctrl)) -+ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); -+ -+ return ssam_hub_add_devices(&pdev->dev, ctrl, desc); -+} -+ -+static int ssam_platform_hub_remove(struct platform_device *pdev) -+{ -+ ssam_hub_remove_devices(&pdev->dev); -+ return 0; -+} -+ -+static struct platform_driver ssam_platform_hub_driver = { -+ .probe = ssam_platform_hub_probe, -+ .remove = ssam_platform_hub_remove, -+ .driver = { -+ .name = "surface_aggregator_platform_hub", -+ .acpi_match_table = ssam_platform_hub_match, -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+ -+/* -- Module initialization. ------------------------------------------------ */ -+ -+static int __init ssam_device_hub_init(void) -+{ -+ int status; -+ -+ status = platform_driver_register(&ssam_platform_hub_driver); -+ if (status) -+ goto err_platform; -+ -+ status = ssam_device_driver_register(&ssam_hub_driver); -+ if (status) -+ goto err_main; -+ -+ status = ssam_device_driver_register(&ssam_base_hub_driver); -+ if (status) -+ goto err_base; -+ -+ return 0; -+ -+err_base: -+ ssam_device_driver_unregister(&ssam_hub_driver); -+err_main: -+ platform_driver_unregister(&ssam_platform_hub_driver); -+err_platform: -+ return status; -+} -+ -+static void __exit ssam_device_hub_exit(void) -+{ -+ ssam_device_driver_unregister(&ssam_base_hub_driver); -+ ssam_device_driver_unregister(&ssam_hub_driver); -+ platform_driver_unregister(&ssam_platform_hub_driver); -+} -+ -+module_init(ssam_device_hub_init); -+module_exit(ssam_device_hub_exit); -+ -+MODULE_AUTHOR("Maximilian Luz "); -+MODULE_DESCRIPTION("Device-registry for Surface System Aggregator Module"); -+MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/clients/surface_battery.c b/drivers/misc/surface_aggregator/clients/surface_battery.c -new file mode 100644 -index 000000000000..77f7842e05a4 ---- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/surface_battery.c -@@ -0,0 +1,1168 @@ -+// SPDX-License-Identifier: GPL-2.0+ -+/* -+ * Surface battery and AC device driver. ++/** ++ * __ssam_device_driver_register() - Register a SSAM client device driver. ++ * @sdrv: The driver to register. ++ * @owner: The module owning the provided driver. + * -+ * Provides support for battery and AC devices connected via the Surface -+ * System Aggregator Module. ++ * Please refer to the ssam_device_driver_register() macro for the normal way ++ * to register a driver from inside its owning module. ++ */ ++int __ssam_device_driver_register(struct ssam_device_driver *sdrv, ++ struct module *owner) ++{ ++ sdrv->driver.owner = owner; ++ sdrv->driver.bus = &ssam_bus_type; ++ ++ /* force drivers to async probe so I/O is possible in probe */ ++ sdrv->driver.probe_type = PROBE_PREFER_ASYNCHRONOUS; ++ ++ return driver_register(&sdrv->driver); ++} ++EXPORT_SYMBOL_GPL(__ssam_device_driver_register); ++ ++/** ++ * ssam_device_driver_unregister - Unregister a SSAM device driver. ++ * @sdrv: The driver to unregister. ++ */ ++void ssam_device_driver_unregister(struct ssam_device_driver *sdrv) ++{ ++ driver_unregister(&sdrv->driver); ++} ++EXPORT_SYMBOL_GPL(ssam_device_driver_unregister); ++ ++static int ssam_remove_device(struct device *dev, void *_data) ++{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ ++ if (is_ssam_device(dev)) ++ ssam_device_remove(sdev); ++ ++ return 0; ++} ++ ++/** ++ * ssam_controller_remove_clients() - Remove SSAM client devices registered as ++ * direct children under the given controller. ++ * @ctrl: The controller to remove all direct clients for. ++ * ++ * Remove all SSAM client devices registered as direct children under the ++ * given controller. Note that this only accounts for direct children of the ++ * controller device. This does not take care of any client devices where the ++ * parent device has been manually set before calling ssam_device_add. Refer ++ * to ssam_device_add()/ssam_device_remove() for more details on those cases. ++ * ++ * To avoid new devices being added in parallel to this call, the main ++ * controller lock (not statelock) must be held during this (and if ++ * necessary, any subsequent deinitialization) call. ++ */ ++void ssam_controller_remove_clients(struct ssam_controller *ctrl) ++{ ++ struct device *dev; ++ ++ dev = ssam_controller_device(ctrl); ++ device_for_each_child_reverse(dev, NULL, ssam_remove_device); ++} ++ ++/** ++ * ssam_bus_register() - Register and set-up the SSAM client device bus. ++ */ ++int ssam_bus_register(void) ++{ ++ return bus_register(&ssam_bus_type); ++} ++ ++/** ++ * ssam_bus_unregister() - Unregister the SSAM client device bus. ++ */ ++void ssam_bus_unregister(void) ++{ ++ return bus_unregister(&ssam_bus_type); ++} +diff --git a/drivers/platform/x86/surface_aggregator/bus.h b/drivers/platform/x86/surface_aggregator/bus.h +new file mode 100644 +index 000000000000..7712baaed6a5 +--- /dev/null ++++ b/drivers/platform/x86/surface_aggregator/bus.h +@@ -0,0 +1,27 @@ ++/* SPDX-License-Identifier: GPL-2.0+ */ ++/* ++ * Surface System Aggregator Module bus and device integration. + * + * Copyright (C) 2019-2020 Maximilian Luz + */ + -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+#include -+ -+#define SPWR_AC_BAT_UPDATE_DELAY msecs_to_jiffies(5000) -+ -+ -+/* -- Module parameters. ---------------------------------------------------- */ -+ -+static unsigned int cache_time = 1000; -+module_param(cache_time, uint, 0644); -+MODULE_PARM_DESC(cache_time, "battery state caching time in milliseconds [default: 1000]"); -+ -+ -+/* -- SAM Interface. -------------------------------------------------------- */ -+ -+enum sam_event_cid_bat { -+ SAM_EVENT_CID_BAT_BIX = 0x15, -+ SAM_EVENT_CID_BAT_BST = 0x16, -+ SAM_EVENT_CID_BAT_ADP = 0x17, -+ SAM_EVENT_CID_BAT_PROT = 0x18, -+ SAM_EVENT_CID_BAT_DPTF = 0x53, -+}; -+ -+enum sam_battery_sta { -+ SAM_BATTERY_STA_OK = 0x0f, -+ SAM_BATTERY_STA_PRESENT = 0x10, -+}; -+ -+enum sam_battery_state { -+ SAM_BATTERY_STATE_DISCHARGING = BIT(0), -+ SAM_BATTERY_STATE_CHARGING = BIT(1), -+ SAM_BATTERY_STATE_CRITICAL = BIT(2), -+}; -+ -+enum sam_battery_power_unit { -+ SAM_BATTERY_POWER_UNIT_mW = 0, -+ SAM_BATTERY_POWER_UNIT_mA = 1, -+}; -+ -+/* Equivalent to data returned in ACPI _BIX method, revision 0 */ -+struct spwr_bix { -+ u8 revision; -+ __le32 power_unit; -+ __le32 design_cap; -+ __le32 last_full_charge_cap; -+ __le32 technology; -+ __le32 design_voltage; -+ __le32 design_cap_warn; -+ __le32 design_cap_low; -+ __le32 cycle_count; -+ __le32 measurement_accuracy; -+ __le32 max_sampling_time; -+ __le32 min_sampling_time; -+ __le32 max_avg_interval; -+ __le32 min_avg_interval; -+ __le32 bat_cap_granularity_1; -+ __le32 bat_cap_granularity_2; -+ __u8 model[21]; -+ __u8 serial[11]; -+ __u8 type[5]; -+ __u8 oem_info[21]; -+} __packed; -+ -+static_assert(sizeof(struct spwr_bix) == 119); -+ -+#define SPWR_BIX_REVISION 0 -+ -+/* Equivalent to data returned in ACPI _BST method */ -+struct spwr_bst { -+ __le32 state; -+ __le32 present_rate; -+ __le32 remaining_cap; -+ __le32 present_voltage; -+} __packed; -+ -+static_assert(sizeof(struct spwr_bst) == 16); -+ -+#define SPWR_BATTERY_VALUE_UNKNOWN 0xffffffff -+ -+/* Get battery status (_STA) */ -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_sta, __le32, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x01, -+}); -+ -+/* Get battery static information (_BIX) */ -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_bix, struct spwr_bix, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x02, -+}); -+ -+/* Get battery dynamic information (_BST) */ -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_bst, struct spwr_bst, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x03, -+}); -+ -+/* Set battery trip point (_BTP) */ -+static SSAM_DEFINE_SYNC_REQUEST_CL_W(ssam_bat_set_btp, __le32, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x04, -+}); -+ -+/* Get platform power source for battery (DPTF PSRC). */ -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_psrc, __le32, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x0d, -+}); -+ -+/* -+ * The following requests are currently unused. They are nevertheless included -+ * for documentation of the SAM interface. -+ */ -+ -+/* Get maximum platform power for battery (DPTF PMAX) */ -+__always_unused -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_pmax, __le32, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x0b, -+}); -+ -+/* Get adapter rating (DPTF ARTG) */ -+__always_unused -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_artg, __le32, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x0f, -+}); -+ -+/* Unknown (DPTF PSOC) */ -+__always_unused -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_psoc, __le32, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x0c, -+}); -+ -+/* Unknown (DPTF CHGI/ INT3403 SPPC) */ -+__always_unused -+static SSAM_DEFINE_SYNC_REQUEST_CL_W(ssam_bat_set_chgi, __le32, { -+ .target_category = SSAM_SSH_TC_BAT, -+ .command_id = 0x0e, -+}); -+ -+ -+/* -- Common power-subsystem interface. ------------------------------------- */ -+ -+struct spwr_psy_properties { -+ const char *name; -+ struct ssam_event_registry registry; -+}; -+ -+struct spwr_battery_device { -+ struct ssam_device *sdev; -+ -+ char name[32]; -+ struct power_supply *psy; -+ struct power_supply_desc psy_desc; -+ -+ struct delayed_work update_work; -+ -+ struct ssam_event_notifier notif; -+ -+ struct mutex lock; -+ unsigned long timestamp; -+ -+ __le32 sta; -+ struct spwr_bix bix; -+ struct spwr_bst bst; -+ u32 alarm; -+}; -+ -+struct spwr_ac_device { -+ struct ssam_device *sdev; -+ -+ char name[32]; -+ struct power_supply *psy; -+ struct power_supply_desc psy_desc; -+ -+ struct ssam_event_notifier notif; -+ -+ struct mutex lock; -+ -+ __le32 state; -+}; -+ -+static enum power_supply_property spwr_ac_props[] = { -+ POWER_SUPPLY_PROP_ONLINE, -+}; -+ -+static enum power_supply_property spwr_battery_props_chg[] = { -+ POWER_SUPPLY_PROP_STATUS, -+ POWER_SUPPLY_PROP_PRESENT, -+ POWER_SUPPLY_PROP_TECHNOLOGY, -+ POWER_SUPPLY_PROP_CYCLE_COUNT, -+ POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, -+ POWER_SUPPLY_PROP_VOLTAGE_NOW, -+ POWER_SUPPLY_PROP_CURRENT_NOW, -+ POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, -+ POWER_SUPPLY_PROP_CHARGE_FULL, -+ POWER_SUPPLY_PROP_CHARGE_NOW, -+ POWER_SUPPLY_PROP_CAPACITY, -+ POWER_SUPPLY_PROP_CAPACITY_LEVEL, -+ POWER_SUPPLY_PROP_MODEL_NAME, -+ POWER_SUPPLY_PROP_MANUFACTURER, -+ POWER_SUPPLY_PROP_SERIAL_NUMBER, -+}; -+ -+static enum power_supply_property spwr_battery_props_eng[] = { -+ POWER_SUPPLY_PROP_STATUS, -+ POWER_SUPPLY_PROP_PRESENT, -+ POWER_SUPPLY_PROP_TECHNOLOGY, -+ POWER_SUPPLY_PROP_CYCLE_COUNT, -+ POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, -+ POWER_SUPPLY_PROP_VOLTAGE_NOW, -+ POWER_SUPPLY_PROP_POWER_NOW, -+ POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, -+ POWER_SUPPLY_PROP_ENERGY_FULL, -+ POWER_SUPPLY_PROP_ENERGY_NOW, -+ POWER_SUPPLY_PROP_CAPACITY, -+ POWER_SUPPLY_PROP_CAPACITY_LEVEL, -+ POWER_SUPPLY_PROP_MODEL_NAME, -+ POWER_SUPPLY_PROP_MANUFACTURER, -+ POWER_SUPPLY_PROP_SERIAL_NUMBER, -+}; -+ -+static bool spwr_battery_present(struct spwr_battery_device *bat) -+{ -+ return le32_to_cpu(bat->sta) & SAM_BATTERY_STA_PRESENT; -+} -+ -+static int spwr_battery_load_sta(struct spwr_battery_device *bat) -+{ -+ return ssam_retry(ssam_bat_get_sta, bat->sdev, &bat->sta); -+} -+ -+static int spwr_battery_load_bix(struct spwr_battery_device *bat) -+{ -+ int status; -+ -+ if (!spwr_battery_present(bat)) -+ return 0; -+ -+ status = ssam_retry(ssam_bat_get_bix, bat->sdev, &bat->bix); -+ -+ /* Enforce NULL terminated strings in case anything goes wrong... */ -+ bat->bix.model[ARRAY_SIZE(bat->bix.model) - 1] = 0; -+ bat->bix.serial[ARRAY_SIZE(bat->bix.serial) - 1] = 0; -+ bat->bix.type[ARRAY_SIZE(bat->bix.type) - 1] = 0; -+ bat->bix.oem_info[ARRAY_SIZE(bat->bix.oem_info) - 1] = 0; -+ -+ return status; -+} -+ -+static int spwr_battery_load_bst(struct spwr_battery_device *bat) -+{ -+ if (!spwr_battery_present(bat)) -+ return 0; -+ -+ return ssam_retry(ssam_bat_get_bst, bat->sdev, &bat->bst); -+} -+ -+static int spwr_battery_set_alarm_unlocked(struct spwr_battery_device *bat, -+ u32 value) -+{ -+ __le32 value_le = cpu_to_le32(value); -+ -+ bat->alarm = value; -+ return ssam_retry(ssam_bat_set_btp, bat->sdev, &value_le); -+} -+ -+static int spwr_battery_set_alarm(struct spwr_battery_device *bat, u32 value) -+{ -+ int status; -+ -+ mutex_lock(&bat->lock); -+ status = spwr_battery_set_alarm_unlocked(bat, value); -+ mutex_unlock(&bat->lock); -+ -+ return status; -+} -+ -+static int spwr_battery_update_bst_unlocked(struct spwr_battery_device *bat, -+ bool cached) -+{ -+ unsigned long cache_deadline; -+ int status; -+ -+ cache_deadline = bat->timestamp + msecs_to_jiffies(cache_time); -+ if (cached && bat->timestamp && time_is_after_jiffies(cache_deadline)) -+ return 0; -+ -+ status = spwr_battery_load_sta(bat); -+ if (status) -+ return status; -+ -+ status = spwr_battery_load_bst(bat); -+ if (status) -+ return status; -+ -+ bat->timestamp = jiffies; -+ return 0; -+} -+ -+static int spwr_battery_update_bst(struct spwr_battery_device *bat, bool cached) -+{ -+ int status; -+ -+ mutex_lock(&bat->lock); -+ status = spwr_battery_update_bst_unlocked(bat, cached); -+ mutex_unlock(&bat->lock); -+ -+ return status; -+} -+ -+static int spwr_battery_update_bix_unlocked(struct spwr_battery_device *bat) -+{ -+ int status; -+ -+ status = spwr_battery_load_sta(bat); -+ if (status) -+ return status; -+ -+ status = spwr_battery_load_bix(bat); -+ if (status) -+ return status; -+ -+ status = spwr_battery_load_bst(bat); -+ if (status) -+ return status; -+ -+ if (bat->bix.revision != SPWR_BIX_REVISION) { -+ dev_warn(&bat->sdev->dev, "unsupported battery revision: %u\n", -+ bat->bix.revision); -+ } -+ -+ bat->timestamp = jiffies; -+ return 0; -+} -+ -+static int spwr_ac_update_unlocked(struct spwr_ac_device *ac) -+{ -+ int status; -+ u32 old = ac->state; -+ -+ status = ssam_retry(ssam_bat_get_psrc, ac->sdev, &ac->state); -+ if (status < 0) -+ return status; -+ -+ return old != ac->state; -+} -+ -+static int spwr_ac_update(struct spwr_ac_device *ac) -+{ -+ int status; -+ -+ mutex_lock(&ac->lock); -+ status = spwr_ac_update_unlocked(ac); -+ mutex_unlock(&ac->lock); -+ -+ return status; -+} -+ -+static u32 sprw_battery_get_full_cap_safe(struct spwr_battery_device *bat) -+{ -+ u32 full_cap = get_unaligned_le32(&bat->bix.last_full_charge_cap); -+ -+ if (full_cap == 0 || full_cap == SPWR_BATTERY_VALUE_UNKNOWN) -+ full_cap = get_unaligned_le32(&bat->bix.design_cap); -+ -+ return full_cap; -+} -+ -+static bool spwr_battery_is_full(struct spwr_battery_device *bat) -+{ -+ u32 state = get_unaligned_le32(&bat->bst.state); -+ u32 full_cap = sprw_battery_get_full_cap_safe(bat); -+ u32 remaining_cap = get_unaligned_le32(&bat->bst.remaining_cap); -+ -+ return full_cap != SPWR_BATTERY_VALUE_UNKNOWN && full_cap != 0 && -+ remaining_cap != SPWR_BATTERY_VALUE_UNKNOWN && -+ remaining_cap >= full_cap && -+ state == 0; -+} -+ -+static int spwr_battery_recheck_full(struct spwr_battery_device *bat) -+{ -+ bool present; -+ u32 unit; -+ int status; -+ -+ mutex_lock(&bat->lock); -+ unit = get_unaligned_le32(&bat->bix.power_unit); -+ present = spwr_battery_present(bat); -+ -+ status = spwr_battery_update_bix_unlocked(bat); -+ if (status) -+ goto out; -+ -+ /* If battery has been attached, (re-)initialize alarm. */ -+ if (!present && spwr_battery_present(bat)) { -+ u32 cap_warn = get_unaligned_le32(&bat->bix.design_cap_warn); -+ -+ status = spwr_battery_set_alarm_unlocked(bat, cap_warn); -+ if (status) -+ goto out; -+ } -+ -+ /* -+ * Warn if the unit has changed. This is something we genuinely don't -+ * expect to happen, so make this a big warning. If it does, we'll -+ * need to add support for it. -+ */ -+ WARN_ON(unit != get_unaligned_le32(&bat->bix.power_unit)); -+ -+out: -+ mutex_unlock(&bat->lock); -+ -+ if (!status) -+ power_supply_changed(bat->psy); -+ -+ return status; -+} -+ -+static int spwr_battery_recheck_status(struct spwr_battery_device *bat) -+{ -+ int status; -+ -+ status = spwr_battery_update_bst(bat, false); -+ if (!status) -+ power_supply_changed(bat->psy); -+ -+ return status; -+} -+ -+static int spwr_battery_recheck_adapter(struct spwr_battery_device *bat) -+{ -+ /* -+ * Handle battery update quirk: When the battery is fully charged (or -+ * charged up to the limit imposed by the UEFI battery limit) and the -+ * adapter is plugged in or removed, the EC does not send a separate -+ * event for the state (charging/discharging) change. Furthermore it -+ * may take some time until the state is updated on the battery. -+ * Schedule an update to solve this. -+ */ -+ -+ schedule_delayed_work(&bat->update_work, SPWR_AC_BAT_UPDATE_DELAY); -+ return 0; -+} -+ -+static int spwr_ac_recheck(struct spwr_ac_device *ac) -+{ -+ int status; -+ -+ status = spwr_ac_update(ac); -+ if (status > 0) -+ power_supply_changed(ac->psy); -+ -+ return status >= 0 ? 0 : status; -+} -+ -+static u32 spwr_notify_bat(struct ssam_event_notifier *nf, -+ const struct ssam_event *event) -+{ -+ struct spwr_battery_device *bat; -+ int status; -+ -+ bat = container_of(nf, struct spwr_battery_device, notif); -+ -+ dev_dbg(&bat->sdev->dev, "power event (cid = %#04x, iid = %#04x, tid = %#04x)\n", -+ event->command_id, event->instance_id, event->target_id); -+ -+ /* Handled here, needs to be handled for all targets/instances. */ -+ if (event->command_id == SAM_EVENT_CID_BAT_ADP) { -+ status = spwr_battery_recheck_adapter(bat); -+ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; -+ } -+ -+ if (bat->sdev->uid.target != event->target_id) -+ return 0; -+ -+ if (bat->sdev->uid.instance != event->instance_id) -+ return 0; -+ -+ switch (event->command_id) { -+ case SAM_EVENT_CID_BAT_BIX: -+ status = spwr_battery_recheck_full(bat); -+ break; -+ -+ case SAM_EVENT_CID_BAT_BST: -+ status = spwr_battery_recheck_status(bat); -+ break; -+ -+ case SAM_EVENT_CID_BAT_PROT: -+ /* -+ * TODO: Implement support for battery protection status change -+ * event. -+ */ -+ status = 0; -+ break; -+ -+ case SAM_EVENT_CID_BAT_DPTF: -+ /* -+ * TODO: Implement support for DPTF event. -+ */ -+ status = 0; -+ break; -+ -+ default: -+ return 0; -+ } -+ -+ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; -+} -+ -+static u32 spwr_notify_ac(struct ssam_event_notifier *nf, -+ const struct ssam_event *event) -+{ -+ struct spwr_ac_device *ac; -+ int status; -+ -+ ac = container_of(nf, struct spwr_ac_device, notif); -+ -+ dev_dbg(&ac->sdev->dev, "power event (cid = %#04x, iid = %#04x, tid = %#04x)\n", -+ event->command_id, event->instance_id, event->target_id); -+ -+ /* -+ * Allow events of all targets/instances here. Global adapter status -+ * seems to be handled via target=1 and instance=1, but events are -+ * reported on all targets/instances in use. -+ * -+ * While it should be enough to just listen on 1/1, listen everywhere to -+ * make sure we don't miss anything. -+ */ -+ -+ switch (event->command_id) { -+ case SAM_EVENT_CID_BAT_ADP: -+ status = spwr_ac_recheck(ac); -+ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; -+ -+ default: -+ return 0; -+ } -+} -+ -+static void spwr_battery_update_bst_workfn(struct work_struct *work) -+{ -+ struct delayed_work *dwork = to_delayed_work(work); -+ struct spwr_battery_device *bat; -+ int status; -+ -+ bat = container_of(dwork, struct spwr_battery_device, update_work); -+ -+ status = spwr_battery_update_bst(bat, false); -+ if (!status) -+ power_supply_changed(bat->psy); -+ -+ if (status) { -+ dev_err(&bat->sdev->dev, "failed to update battery state: %d\n", -+ status); -+ } -+} -+ -+static int spwr_battery_prop_status(struct spwr_battery_device *bat) -+{ -+ u32 state = get_unaligned_le32(&bat->bst.state); -+ u32 present_rate = get_unaligned_le32(&bat->bst.present_rate); -+ -+ if (state & SAM_BATTERY_STATE_DISCHARGING) -+ return POWER_SUPPLY_STATUS_DISCHARGING; -+ -+ if (state & SAM_BATTERY_STATE_CHARGING) -+ return POWER_SUPPLY_STATUS_CHARGING; -+ -+ if (spwr_battery_is_full(bat)) -+ return POWER_SUPPLY_STATUS_FULL; -+ -+ if (present_rate == 0) -+ return POWER_SUPPLY_STATUS_NOT_CHARGING; -+ -+ return POWER_SUPPLY_STATUS_UNKNOWN; -+} -+ -+static int spwr_battery_prop_technology(struct spwr_battery_device *bat) -+{ -+ if (!strcasecmp("NiCd", bat->bix.type)) -+ return POWER_SUPPLY_TECHNOLOGY_NiCd; -+ -+ if (!strcasecmp("NiMH", bat->bix.type)) -+ return POWER_SUPPLY_TECHNOLOGY_NiMH; -+ -+ if (!strcasecmp("LION", bat->bix.type)) -+ return POWER_SUPPLY_TECHNOLOGY_LION; -+ -+ if (!strncasecmp("LI-ION", bat->bix.type, 6)) -+ return POWER_SUPPLY_TECHNOLOGY_LION; -+ -+ if (!strcasecmp("LiP", bat->bix.type)) -+ return POWER_SUPPLY_TECHNOLOGY_LIPO; -+ -+ return POWER_SUPPLY_TECHNOLOGY_UNKNOWN; -+} -+ -+static int spwr_battery_prop_capacity(struct spwr_battery_device *bat) -+{ -+ u32 full_cap = sprw_battery_get_full_cap_safe(bat); -+ u32 remaining_cap = get_unaligned_le32(&bat->bst.remaining_cap); -+ -+ if (full_cap == 0 || full_cap == SPWR_BATTERY_VALUE_UNKNOWN) -+ return -ENODEV; -+ -+ if (remaining_cap == SPWR_BATTERY_VALUE_UNKNOWN) -+ return -ENODEV; -+ -+ return remaining_cap * 100 / full_cap; -+} -+ -+static int spwr_battery_prop_capacity_level(struct spwr_battery_device *bat) -+{ -+ u32 state = get_unaligned_le32(&bat->bst.state); -+ u32 remaining_cap = get_unaligned_le32(&bat->bst.remaining_cap); -+ -+ if (state & SAM_BATTERY_STATE_CRITICAL) -+ return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; -+ -+ if (spwr_battery_is_full(bat)) -+ return POWER_SUPPLY_CAPACITY_LEVEL_FULL; -+ -+ if (remaining_cap <= bat->alarm) -+ return POWER_SUPPLY_CAPACITY_LEVEL_LOW; -+ -+ return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; -+} -+ -+static int spwr_ac_get_property(struct power_supply *psy, -+ enum power_supply_property psp, -+ union power_supply_propval *val) -+{ -+ struct spwr_ac_device *ac = power_supply_get_drvdata(psy); -+ int status; -+ -+ mutex_lock(&ac->lock); -+ -+ status = spwr_ac_update_unlocked(ac); -+ if (status) -+ goto out; -+ -+ switch (psp) { -+ case POWER_SUPPLY_PROP_ONLINE: -+ val->intval = !!le32_to_cpu(ac->state); -+ break; -+ -+ default: -+ status = -EINVAL; -+ goto out; -+ } -+ -+out: -+ mutex_unlock(&ac->lock); -+ return status; -+} -+ -+static int spwr_battery_get_property(struct power_supply *psy, -+ enum power_supply_property psp, -+ union power_supply_propval *val) -+{ -+ struct spwr_battery_device *bat = power_supply_get_drvdata(psy); -+ u32 value; -+ int status; -+ -+ mutex_lock(&bat->lock); -+ -+ status = spwr_battery_update_bst_unlocked(bat, true); -+ if (status) -+ goto out; -+ -+ /* Abort if battery is not present. */ -+ if (!spwr_battery_present(bat) && psp != POWER_SUPPLY_PROP_PRESENT) { -+ status = -ENODEV; -+ goto out; -+ } -+ -+ switch (psp) { -+ case POWER_SUPPLY_PROP_STATUS: -+ val->intval = spwr_battery_prop_status(bat); -+ break; -+ -+ case POWER_SUPPLY_PROP_PRESENT: -+ val->intval = spwr_battery_present(bat); -+ break; -+ -+ case POWER_SUPPLY_PROP_TECHNOLOGY: -+ val->intval = spwr_battery_prop_technology(bat); -+ break; -+ -+ case POWER_SUPPLY_PROP_CYCLE_COUNT: -+ value = get_unaligned_le32(&bat->bix.cycle_count); -+ if (value != SPWR_BATTERY_VALUE_UNKNOWN) -+ val->intval = value; -+ else -+ status = -ENODEV; -+ break; -+ -+ case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: -+ value = get_unaligned_le32(&bat->bix.design_voltage); -+ if (value != SPWR_BATTERY_VALUE_UNKNOWN) -+ val->intval = value * 1000; -+ else -+ status = -ENODEV; -+ break; -+ -+ case POWER_SUPPLY_PROP_VOLTAGE_NOW: -+ value = get_unaligned_le32(&bat->bst.present_voltage); -+ if (value != SPWR_BATTERY_VALUE_UNKNOWN) -+ val->intval = value * 1000; -+ else -+ status = -ENODEV; -+ break; -+ -+ case POWER_SUPPLY_PROP_CURRENT_NOW: -+ case POWER_SUPPLY_PROP_POWER_NOW: -+ value = get_unaligned_le32(&bat->bst.present_rate); -+ if (value != SPWR_BATTERY_VALUE_UNKNOWN) -+ val->intval = value * 1000; -+ else -+ status = -ENODEV; -+ break; -+ -+ case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: -+ case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: -+ value = get_unaligned_le32(&bat->bix.design_cap); -+ if (value != SPWR_BATTERY_VALUE_UNKNOWN) -+ val->intval = value * 1000; -+ else -+ status = -ENODEV; -+ break; -+ -+ case POWER_SUPPLY_PROP_CHARGE_FULL: -+ case POWER_SUPPLY_PROP_ENERGY_FULL: -+ value = get_unaligned_le32(&bat->bix.last_full_charge_cap); -+ if (value != SPWR_BATTERY_VALUE_UNKNOWN) -+ val->intval = value * 1000; -+ else -+ status = -ENODEV; -+ break; -+ -+ case POWER_SUPPLY_PROP_CHARGE_NOW: -+ case POWER_SUPPLY_PROP_ENERGY_NOW: -+ value = get_unaligned_le32(&bat->bst.remaining_cap); -+ if (value != SPWR_BATTERY_VALUE_UNKNOWN) -+ val->intval = value * 1000; -+ else -+ status = -ENODEV; -+ break; -+ -+ case POWER_SUPPLY_PROP_CAPACITY: -+ val->intval = spwr_battery_prop_capacity(bat); -+ break; -+ -+ case POWER_SUPPLY_PROP_CAPACITY_LEVEL: -+ val->intval = spwr_battery_prop_capacity_level(bat); -+ break; -+ -+ case POWER_SUPPLY_PROP_MODEL_NAME: -+ val->strval = bat->bix.model; -+ break; -+ -+ case POWER_SUPPLY_PROP_MANUFACTURER: -+ val->strval = bat->bix.oem_info; -+ break; -+ -+ case POWER_SUPPLY_PROP_SERIAL_NUMBER: -+ val->strval = bat->bix.serial; -+ break; -+ -+ default: -+ status = -EINVAL; -+ break; -+ } -+ -+out: -+ mutex_unlock(&bat->lock); -+ return status; -+} -+ -+static ssize_t spwr_battery_alarm_show(struct device *dev, -+ struct device_attribute *attr, -+ char *buf) -+{ -+ struct power_supply *psy = dev_get_drvdata(dev); -+ struct spwr_battery_device *bat = power_supply_get_drvdata(psy); -+ -+ return sprintf(buf, "%d\n", bat->alarm * 1000); -+} -+ -+static ssize_t spwr_battery_alarm_store(struct device *dev, -+ struct device_attribute *attr, -+ const char *buf, size_t count) -+{ -+ struct power_supply *psy = dev_get_drvdata(dev); -+ struct spwr_battery_device *bat = power_supply_get_drvdata(psy); -+ unsigned long value; -+ int status; -+ -+ status = kstrtoul(buf, 0, &value); -+ if (status) -+ return status; -+ -+ if (!spwr_battery_present(bat)) -+ return -ENODEV; -+ -+ status = spwr_battery_set_alarm(bat, value / 1000); -+ if (status) -+ return status; -+ -+ return count; -+} -+ -+static const struct device_attribute alarm_attr = { -+ .attr = {.name = "alarm", .mode = 0644}, -+ .show = spwr_battery_alarm_show, -+ .store = spwr_battery_alarm_store, -+}; -+ -+static void spwr_ac_init(struct spwr_ac_device *ac, struct ssam_device *sdev, -+ struct ssam_event_registry registry, const char *name) -+{ -+ mutex_init(&ac->lock); -+ strncpy(ac->name, name, ARRAY_SIZE(ac->name) - 1); -+ -+ ac->sdev = sdev; -+ -+ ac->notif.base.priority = 1; -+ ac->notif.base.fn = spwr_notify_ac; -+ ac->notif.event.reg = registry; -+ ac->notif.event.id.target_category = sdev->uid.category; -+ ac->notif.event.id.instance = 0; -+ ac->notif.event.mask = SSAM_EVENT_MASK_NONE; -+ ac->notif.event.flags = SSAM_EVENT_SEQUENCED; -+ -+ ac->psy_desc.name = ac->name; -+ ac->psy_desc.type = POWER_SUPPLY_TYPE_MAINS; -+ ac->psy_desc.properties = spwr_ac_props; -+ ac->psy_desc.num_properties = ARRAY_SIZE(spwr_ac_props); -+ ac->psy_desc.get_property = spwr_ac_get_property; -+} -+ -+static void spwr_ac_destroy(struct spwr_ac_device *ac) -+{ -+ mutex_destroy(&ac->lock); -+} -+ -+static int spwr_ac_register(struct spwr_ac_device *ac) -+{ -+ struct power_supply_config psy_cfg = {}; -+ __le32 sta; -+ int status; -+ -+ /* Make sure the device is there and functioning properly. */ -+ status = ssam_retry(ssam_bat_get_sta, ac->sdev, &sta); -+ if (status) -+ return status; -+ -+ if ((le32_to_cpu(sta) & SAM_BATTERY_STA_OK) != SAM_BATTERY_STA_OK) -+ return -ENODEV; -+ -+ psy_cfg.drv_data = ac; -+ ac->psy = power_supply_register(&ac->sdev->dev, &ac->psy_desc, &psy_cfg); -+ if (IS_ERR(ac->psy)) -+ return PTR_ERR(ac->psy); -+ -+ status = ssam_notifier_register(ac->sdev->ctrl, &ac->notif); -+ if (status) -+ power_supply_unregister(ac->psy); -+ -+ return status; -+} -+ -+static int spwr_ac_unregister(struct spwr_ac_device *ac) -+{ -+ ssam_notifier_unregister(ac->sdev->ctrl, &ac->notif); -+ power_supply_unregister(ac->psy); -+ return 0; -+} -+ -+static void spwr_battery_init(struct spwr_battery_device *bat, -+ struct ssam_device *sdev, -+ struct ssam_event_registry registry, -+ const char *name) -+{ -+ mutex_init(&bat->lock); -+ strncpy(bat->name, name, ARRAY_SIZE(bat->name) - 1); -+ -+ bat->sdev = sdev; -+ -+ bat->notif.base.priority = 1; -+ bat->notif.base.fn = spwr_notify_bat; -+ bat->notif.event.reg = registry; -+ bat->notif.event.id.target_category = sdev->uid.category; -+ bat->notif.event.id.instance = 0; -+ bat->notif.event.mask = SSAM_EVENT_MASK_NONE; -+ bat->notif.event.flags = SSAM_EVENT_SEQUENCED; -+ -+ bat->psy_desc.name = bat->name; -+ bat->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY; -+ bat->psy_desc.get_property = spwr_battery_get_property; -+ -+ INIT_DELAYED_WORK(&bat->update_work, spwr_battery_update_bst_workfn); -+} -+ -+static void spwr_battery_destroy(struct spwr_battery_device *bat) -+{ -+ mutex_destroy(&bat->lock); -+} -+ -+static int spwr_battery_register(struct spwr_battery_device *bat) -+{ -+ struct power_supply_config psy_cfg = {}; -+ __le32 sta; -+ int status; -+ -+ /* Make sure the device is there and functioning properly. */ -+ status = ssam_retry(ssam_bat_get_sta, bat->sdev, &sta); -+ if (status) -+ return status; -+ -+ if ((le32_to_cpu(sta) & SAM_BATTERY_STA_OK) != SAM_BATTERY_STA_OK) -+ return -ENODEV; -+ -+ status = spwr_battery_update_bix_unlocked(bat); -+ if (status) -+ return status; -+ -+ if (spwr_battery_present(bat)) { -+ u32 cap_warn = get_unaligned_le32(&bat->bix.design_cap_warn); -+ -+ status = spwr_battery_set_alarm_unlocked(bat, cap_warn); -+ if (status) -+ return status; -+ } -+ -+ switch (get_unaligned_le32(&bat->bix.power_unit)) { -+ case SAM_BATTERY_POWER_UNIT_mW: -+ bat->psy_desc.properties = spwr_battery_props_eng; -+ bat->psy_desc.num_properties = ARRAY_SIZE(spwr_battery_props_eng); -+ break; -+ -+ case SAM_BATTERY_POWER_UNIT_mA: -+ bat->psy_desc.properties = spwr_battery_props_chg; -+ bat->psy_desc.num_properties = ARRAY_SIZE(spwr_battery_props_chg); -+ break; -+ -+ default: -+ dev_err(&bat->sdev->dev, "unsupported battery power unit: %u\n", -+ get_unaligned_le32(&bat->bix.power_unit)); -+ return -ENOTSUPP; -+ } -+ -+ psy_cfg.drv_data = bat; -+ bat->psy = power_supply_register(&bat->sdev->dev, &bat->psy_desc, &psy_cfg); -+ if (IS_ERR(bat->psy)) -+ return PTR_ERR(bat->psy); -+ -+ status = ssam_notifier_register(bat->sdev->ctrl, &bat->notif); -+ if (status) -+ goto err_notif; -+ -+ status = device_create_file(&bat->psy->dev, &alarm_attr); -+ if (status) -+ goto err_file; -+ -+ return 0; -+ -+err_file: -+ ssam_notifier_unregister(bat->sdev->ctrl, &bat->notif); -+err_notif: -+ power_supply_unregister(bat->psy); -+ return status; -+} -+ -+static void spwr_battery_unregister(struct spwr_battery_device *bat) -+{ -+ ssam_notifier_unregister(bat->sdev->ctrl, &bat->notif); -+ cancel_delayed_work_sync(&bat->update_work); -+ device_remove_file(&bat->psy->dev, &alarm_attr); -+ power_supply_unregister(bat->psy); -+} -+ -+ -+/* -- Power management. ----------------------------------------------------- */ -+ -+static int __maybe_unused surface_battery_resume(struct device *dev) -+{ -+ return spwr_battery_recheck_full(dev_get_drvdata(dev)); -+} -+SIMPLE_DEV_PM_OPS(surface_battery_pm_ops, NULL, surface_battery_resume); -+ -+static int __maybe_unused surface_ac_resume(struct device *dev) -+{ -+ return spwr_ac_recheck(dev_get_drvdata(dev)); -+} -+SIMPLE_DEV_PM_OPS(surface_ac_pm_ops, NULL, surface_ac_resume); -+ -+ -+/* -- Battery driver. ------------------------------------------------------- */ -+ -+static int surface_battery_probe(struct ssam_device *sdev) -+{ -+ const struct spwr_psy_properties *p; -+ struct spwr_battery_device *bat; -+ int status; -+ -+ p = ssam_device_get_match_data(sdev); -+ if (!p) -+ return -ENODEV; -+ -+ bat = devm_kzalloc(&sdev->dev, sizeof(*bat), GFP_KERNEL); -+ if (!bat) -+ return -ENOMEM; -+ -+ spwr_battery_init(bat, sdev, p->registry, p->name); -+ ssam_device_set_drvdata(sdev, bat); -+ -+ status = spwr_battery_register(bat); -+ if (status) -+ spwr_battery_destroy(bat); -+ -+ return status; -+} -+ -+static void surface_battery_remove(struct ssam_device *sdev) -+{ -+ struct spwr_battery_device *bat = ssam_device_get_drvdata(sdev); -+ -+ spwr_battery_unregister(bat); -+ spwr_battery_destroy(bat); -+} -+ -+static const struct spwr_psy_properties spwr_psy_props_bat1 = { -+ .name = "BAT1", -+ .registry = SSAM_EVENT_REGISTRY_SAM, -+}; -+ -+static const struct spwr_psy_properties spwr_psy_props_bat2_sb3 = { -+ .name = "BAT2", -+ .registry = SSAM_EVENT_REGISTRY_KIP, -+}; -+ -+static const struct ssam_device_id surface_battery_match[] = { -+ { SSAM_SDEV(BAT, 0x01, 0x01, 0x00), (unsigned long)&spwr_psy_props_bat1 }, -+ { SSAM_SDEV(BAT, 0x02, 0x01, 0x00), (unsigned long)&spwr_psy_props_bat2_sb3 }, -+ { }, -+}; -+MODULE_DEVICE_TABLE(ssam, surface_battery_match); -+ -+static struct ssam_device_driver surface_battery_driver = { -+ .probe = surface_battery_probe, -+ .remove = surface_battery_remove, -+ .match_table = surface_battery_match, -+ .driver = { -+ .name = "surface_battery", -+ .pm = &surface_battery_pm_ops, -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+ -+/* -- AC driver. ------------------------------------------------------------ */ -+ -+static int surface_ac_probe(struct ssam_device *sdev) -+{ -+ const struct spwr_psy_properties *p; -+ struct spwr_ac_device *ac; -+ int status; -+ -+ p = ssam_device_get_match_data(sdev); -+ if (!p) -+ return -ENODEV; -+ -+ ac = devm_kzalloc(&sdev->dev, sizeof(*ac), GFP_KERNEL); -+ if (!ac) -+ return -ENOMEM; -+ -+ spwr_ac_init(ac, sdev, p->registry, p->name); -+ ssam_device_set_drvdata(sdev, ac); -+ -+ status = spwr_ac_register(ac); -+ if (status) -+ spwr_ac_destroy(ac); -+ -+ return status; -+} -+ -+static void surface_ac_remove(struct ssam_device *sdev) -+{ -+ struct spwr_ac_device *ac = ssam_device_get_drvdata(sdev); -+ -+ spwr_ac_unregister(ac); -+ spwr_ac_destroy(ac); -+} -+ -+static const struct spwr_psy_properties spwr_psy_props_adp1 = { -+ .name = "ADP1", -+ .registry = SSAM_EVENT_REGISTRY_SAM, -+}; -+ -+static const struct ssam_device_id surface_ac_match[] = { -+ { SSAM_SDEV(BAT, 0x01, 0x01, 0x01), (unsigned long)&spwr_psy_props_adp1 }, -+ { }, -+}; -+MODULE_DEVICE_TABLE(ssam, surface_ac_match); -+ -+static struct ssam_device_driver surface_ac_driver = { -+ .probe = surface_ac_probe, -+ .remove = surface_ac_remove, -+ .match_table = surface_ac_match, -+ .driver = { -+ .name = "surface_ac", -+ .pm = &surface_ac_pm_ops, -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+ -+/* -- Module setup. --------------------------------------------------------- */ -+ -+static int __init surface_battery_init(void) -+{ -+ int status; -+ -+ status = ssam_device_driver_register(&surface_battery_driver); -+ if (status) -+ return status; -+ -+ status = ssam_device_driver_register(&surface_ac_driver); -+ if (status) -+ ssam_device_driver_unregister(&surface_battery_driver); -+ -+ return status; -+} -+module_init(surface_battery_init); -+ -+static void __exit surface_battery_exit(void) -+{ -+ ssam_device_driver_unregister(&surface_battery_driver); -+ ssam_device_driver_unregister(&surface_ac_driver); -+} -+module_exit(surface_battery_exit); -+ -+MODULE_AUTHOR("Maximilian Luz "); -+MODULE_DESCRIPTION("Battery/AC driver for Surface System Aggregator Module"); -+MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/clients/surface_dtx.c b/drivers/misc/surface_aggregator/clients/surface_dtx.c -new file mode 100644 -index 000000000000..b7a3b8ce7726 ---- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/surface_dtx.c -@@ -0,0 +1,1277 @@ -+// SPDX-License-Identifier: GPL-2.0+ -+/* -+ * Surface Book (gen. 2 and later) detachment system (DTX) driver. -+ * -+ * Provides a user-space interface to properly handle clipboard/tablet -+ * (containing screen and processor) detachment from the base of the device -+ * (containing the keyboard and optionally a discrete GPU). Allows to -+ * acknowledge (to speed things up), abort (e.g. in case the dGPU is still in -+ * use), or request detachment via user-space. -+ * -+ * Copyright (C) 2019-2020 Maximilian Luz -+ */ -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include ++#ifndef _SURFACE_AGGREGATOR_BUS_H ++#define _SURFACE_AGGREGATOR_BUS_H + +#include -+#include -+#include -+ -+ -+/* -- SSAM interface. ------------------------------------------------------- */ -+ -+enum sam_event_cid_bas { -+ SAM_EVENT_CID_DTX_CONNECTION = 0x0c, -+ SAM_EVENT_CID_DTX_REQUEST = 0x0e, -+ SAM_EVENT_CID_DTX_CANCEL = 0x0f, -+ SAM_EVENT_CID_DTX_LATCH_STATUS = 0x11, -+}; -+ -+enum ssam_bas_base_state { -+ SSAM_BAS_BASE_STATE_DETACH_SUCCESS = 0x00, -+ SSAM_BAS_BASE_STATE_ATTACHED = 0x01, -+ SSAM_BAS_BASE_STATE_NOT_FEASIBLE = 0x02, -+}; -+ -+enum ssam_bas_latch_status { -+ SSAM_BAS_LATCH_STATUS_CLOSED = 0x00, -+ SSAM_BAS_LATCH_STATUS_OPENED = 0x01, -+ SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN = 0x02, -+ SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN = 0x03, -+ SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE = 0x04, -+}; -+ -+enum ssam_bas_cancel_reason { -+ SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE = 0x00, /* Low battery. */ -+ SSAM_BAS_CANCEL_REASON_TIMEOUT = 0x02, -+ SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN = 0x03, -+ SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN = 0x04, -+ SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE = 0x05, -+}; -+ -+struct ssam_bas_base_info { -+ u8 state; -+ u8 base_id; -+} __packed; -+ -+static_assert(sizeof(struct ssam_bas_base_info) == 2); -+ -+static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_lock, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x06, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_unlock, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x07, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_request, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x08, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_confirm, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x09, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_heartbeat, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x0a, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_cancel, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x0b, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_base, struct ssam_bas_base_info, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x0c, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_device_mode, u8, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x0d, -+ .instance_id = 0x00, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_latch_status, u8, { -+ .target_category = SSAM_SSH_TC_BAS, -+ .target_id = 0x01, -+ .command_id = 0x11, -+ .instance_id = 0x00, -+}); -+ -+ -+/* -- Main structures. ------------------------------------------------------ */ -+ -+enum sdtx_device_state { -+ SDTX_DEVICE_SHUTDOWN_BIT = BIT(0), -+ SDTX_DEVICE_DIRTY_BASE_BIT = BIT(1), -+ SDTX_DEVICE_DIRTY_MODE_BIT = BIT(2), -+ SDTX_DEVICE_DIRTY_LATCH_BIT = BIT(3), -+}; -+ -+struct sdtx_device { -+ struct kref kref; -+ struct rw_semaphore lock; -+ -+ struct device *dev; -+ struct ssam_controller *ctrl; -+ unsigned long flags; -+ -+ struct miscdevice mdev; -+ wait_queue_head_t waitq; -+ struct mutex write_lock; -+ struct rw_semaphore client_lock; -+ struct list_head client_list; -+ -+ struct delayed_work state_work; -+ struct { -+ struct ssam_bas_base_info base; -+ u8 device_mode; -+ u8 latch_status; -+ } state; -+ -+ struct delayed_work mode_work; -+ struct input_dev *mode_switch; -+ -+ struct ssam_event_notifier notif; -+}; -+ -+enum sdtx_client_state { -+ SDTX_CLIENT_EVENTS_ENABLED_BIT = BIT(0), -+}; -+ -+struct sdtx_client { -+ struct sdtx_device *ddev; -+ struct list_head node; -+ unsigned long flags; -+ -+ struct fasync_struct *fasync; -+ -+ struct mutex read_lock; -+ DECLARE_KFIFO(buffer, u8, 512); -+}; -+ -+static void __sdtx_device_release(struct kref *kref) -+{ -+ struct sdtx_device *ddev = container_of(kref, struct sdtx_device, kref); -+ -+ mutex_destroy(&ddev->write_lock); -+ kfree(ddev); -+} -+ -+static struct sdtx_device *sdtx_device_get(struct sdtx_device *ddev) -+{ -+ if (ddev) -+ kref_get(&ddev->kref); -+ -+ return ddev; -+} -+ -+static void sdtx_device_put(struct sdtx_device *ddev) -+{ -+ if (ddev) -+ kref_put(&ddev->kref, __sdtx_device_release); -+} -+ -+ -+/* -- Firmware value translations. ------------------------------------------ */ -+ -+static u16 sdtx_translate_base_state(struct sdtx_device *ddev, u8 state) -+{ -+ switch (state) { -+ case SSAM_BAS_BASE_STATE_ATTACHED: -+ return SDTX_BASE_ATTACHED; -+ -+ case SSAM_BAS_BASE_STATE_DETACH_SUCCESS: -+ return SDTX_BASE_DETACHED; -+ -+ case SSAM_BAS_BASE_STATE_NOT_FEASIBLE: -+ return SDTX_DETACH_NOT_FEASIBLE; -+ -+ default: -+ dev_err(ddev->dev, "unknown base state: %#04x\n", state); -+ return SDTX_UNKNOWN(state); -+ } -+} -+ -+static u16 sdtx_translate_latch_status(struct sdtx_device *ddev, u8 status) -+{ -+ switch (status) { -+ case SSAM_BAS_LATCH_STATUS_CLOSED: -+ return SDTX_LATCH_CLOSED; -+ -+ case SSAM_BAS_LATCH_STATUS_OPENED: -+ return SDTX_LATCH_OPENED; -+ -+ case SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN: -+ return SDTX_ERR_FAILED_TO_OPEN; -+ -+ case SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN: -+ return SDTX_ERR_FAILED_TO_REMAIN_OPEN; -+ -+ case SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE: -+ return SDTX_ERR_FAILED_TO_CLOSE; -+ -+ default: -+ dev_err(ddev->dev, "unknown latch status: %#04x\n", status); -+ return SDTX_UNKNOWN(status); -+ } -+} -+ -+static u16 sdtx_translate_cancel_reason(struct sdtx_device *ddev, u8 reason) -+{ -+ switch (reason) { -+ case SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE: -+ return SDTX_DETACH_NOT_FEASIBLE; -+ -+ case SSAM_BAS_CANCEL_REASON_TIMEOUT: -+ return SDTX_DETACH_TIMEDOUT; -+ -+ case SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN: -+ return SDTX_ERR_FAILED_TO_OPEN; -+ -+ case SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN: -+ return SDTX_ERR_FAILED_TO_REMAIN_OPEN; -+ -+ case SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE: -+ return SDTX_ERR_FAILED_TO_CLOSE; -+ -+ default: -+ dev_err(ddev->dev, "unknown cancel reason: %#04x\n", reason); -+ return SDTX_UNKNOWN(reason); -+ } -+} -+ -+ -+/* -- IOCTLs. --------------------------------------------------------------- */ -+ -+static int sdtx_ioctl_get_base_info(struct sdtx_device *ddev, -+ struct sdtx_base_info __user *buf) -+{ -+ struct ssam_bas_base_info raw; -+ struct sdtx_base_info info; -+ int status; -+ -+ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &raw); -+ if (status < 0) -+ return status; -+ -+ info.state = sdtx_translate_base_state(ddev, raw.state); -+ info.base_id = SDTX_BASE_TYPE_SSH(raw.base_id); -+ -+ if (copy_to_user(buf, &info, sizeof(info))) -+ return -EFAULT; -+ -+ return 0; -+} -+ -+static int sdtx_ioctl_get_device_mode(struct sdtx_device *ddev, u16 __user *buf) -+{ -+ u8 mode; -+ int status; -+ -+ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); -+ if (status < 0) -+ return status; -+ -+ return put_user(mode, buf); -+} -+ -+static int sdtx_ioctl_get_latch_status(struct sdtx_device *ddev, u16 __user *buf) -+{ -+ u8 latch; -+ int status; -+ -+ status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); -+ if (status < 0) -+ return status; -+ -+ return put_user(sdtx_translate_latch_status(ddev, latch), buf); -+} -+ -+static long __surface_dtx_ioctl(struct sdtx_client *client, unsigned int cmd, -+ unsigned long arg) -+{ -+ struct sdtx_device *ddev = client->ddev; -+ -+ switch (cmd) { -+ case SDTX_IOCTL_EVENTS_ENABLE: -+ set_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); -+ return 0; -+ -+ case SDTX_IOCTL_EVENTS_DISABLE: -+ clear_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); -+ return 0; -+ -+ case SDTX_IOCTL_LATCH_LOCK: -+ return ssam_retry(ssam_bas_latch_lock, ddev->ctrl); -+ -+ case SDTX_IOCTL_LATCH_UNLOCK: -+ return ssam_retry(ssam_bas_latch_unlock, ddev->ctrl); -+ -+ case SDTX_IOCTL_LATCH_REQUEST: -+ return ssam_retry(ssam_bas_latch_request, ddev->ctrl); -+ -+ case SDTX_IOCTL_LATCH_CONFIRM: -+ return ssam_retry(ssam_bas_latch_confirm, ddev->ctrl); -+ -+ case SDTX_IOCTL_LATCH_HEARTBEAT: -+ return ssam_retry(ssam_bas_latch_heartbeat, ddev->ctrl); -+ -+ case SDTX_IOCTL_LATCH_CANCEL: -+ return ssam_retry(ssam_bas_latch_cancel, ddev->ctrl); -+ -+ case SDTX_IOCTL_GET_BASE_INFO: -+ return sdtx_ioctl_get_base_info(ddev, -+ (struct sdtx_base_info __user *)arg); -+ -+ case SDTX_IOCTL_GET_DEVICE_MODE: -+ return sdtx_ioctl_get_device_mode(ddev, (u16 __user *)arg); -+ -+ case SDTX_IOCTL_GET_LATCH_STATUS: -+ return sdtx_ioctl_get_latch_status(ddev, (u16 __user *)arg); -+ -+ default: -+ return -EINVAL; -+ } -+} -+ -+static long surface_dtx_ioctl(struct file *file, unsigned int cmd, -+ unsigned long arg) -+{ -+ struct sdtx_client *client = file->private_data; -+ long status; -+ -+ if (down_read_killable(&client->ddev->lock)) -+ return -ERESTARTSYS; -+ -+ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) { -+ up_read(&client->ddev->lock); -+ return -ENODEV; -+ } -+ -+ status = __surface_dtx_ioctl(client, cmd, arg); -+ -+ up_read(&client->ddev->lock); -+ return status; -+} -+ -+ -+/* -- File operations. ------------------------------------------------------ */ -+ -+static int surface_dtx_open(struct inode *inode, struct file *file) -+{ -+ struct sdtx_device *ddev; -+ struct sdtx_client *client; -+ -+ ddev = container_of(file->private_data, struct sdtx_device, mdev); -+ -+ /* Initialize client. */ -+ client = kzalloc(sizeof(*client), GFP_KERNEL); -+ if (!client) -+ return -ENOMEM; -+ -+ client->ddev = sdtx_device_get(ddev); -+ -+ INIT_LIST_HEAD(&client->node); -+ -+ mutex_init(&client->read_lock); -+ INIT_KFIFO(client->buffer); -+ -+ file->private_data = client; -+ -+ /* Attach client. */ -+ down_write(&ddev->client_lock); -+ -+ /* Do not add a new client if the device has been shut down. */ -+ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { -+ up_write(&ddev->client_lock); -+ sdtx_device_put(client->ddev); -+ kfree(client); -+ return -ENODEV; -+ } -+ -+ list_add_tail(&client->node, &ddev->client_list); -+ up_write(&ddev->client_lock); -+ -+ stream_open(inode, file); -+ return 0; -+} -+ -+static int surface_dtx_release(struct inode *inode, struct file *file) -+{ -+ struct sdtx_client *client = file->private_data; -+ -+ /* Detach client. */ -+ down_write(&client->ddev->client_lock); -+ list_del(&client->node); -+ up_write(&client->ddev->client_lock); -+ -+ sdtx_device_put(client->ddev); -+ mutex_destroy(&client->read_lock); -+ kfree(client); -+ -+ return 0; -+} -+ -+static ssize_t surface_dtx_read(struct file *file, char __user *buf, -+ size_t count, loff_t *offs) -+{ -+ struct sdtx_client *client = file->private_data; -+ struct sdtx_device *ddev = client->ddev; -+ unsigned int copied; -+ int status = 0; -+ -+ if (down_read_killable(&ddev->lock)) -+ return -ERESTARTSYS; -+ -+ /* Make sure we're not shut down. */ -+ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { -+ up_read(&ddev->lock); -+ return -ENODEV; -+ } -+ -+ do { -+ /* Check availability, wait if necessary. */ -+ if (kfifo_is_empty(&client->buffer)) { -+ up_read(&ddev->lock); -+ -+ if (file->f_flags & O_NONBLOCK) -+ return -EAGAIN; -+ -+ status = wait_event_interruptible(ddev->waitq, -+ !kfifo_is_empty(&client->buffer) || -+ test_bit(SDTX_DEVICE_SHUTDOWN_BIT, -+ &ddev->flags)); -+ if (status < 0) -+ return status; -+ -+ if (down_read_killable(&client->ddev->lock)) -+ return -ERESTARTSYS; -+ -+ /* Need to check that we're not shut down again. */ -+ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { -+ up_read(&ddev->lock); -+ return -ENODEV; -+ } -+ } -+ -+ /* Try to read from FIFO. */ -+ if (mutex_lock_interruptible(&client->read_lock)) { -+ up_read(&ddev->lock); -+ return -ERESTARTSYS; -+ } -+ -+ status = kfifo_to_user(&client->buffer, buf, count, &copied); -+ mutex_unlock(&client->read_lock); -+ -+ if (status < 0) { -+ up_read(&ddev->lock); -+ return status; -+ } -+ -+ /* We might not have gotten anything, check this here. */ -+ if (copied == 0 && (file->f_flags & O_NONBLOCK)) { -+ up_read(&ddev->lock); -+ return -EAGAIN; -+ } -+ } while (copied == 0); -+ -+ up_read(&ddev->lock); -+ return copied; -+} -+ -+static __poll_t surface_dtx_poll(struct file *file, struct poll_table_struct *pt) -+{ -+ struct sdtx_client *client = file->private_data; -+ __poll_t events = 0; -+ -+ if (down_read_killable(&client->ddev->lock)) -+ return -ERESTARTSYS; -+ -+ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) { -+ up_read(&client->ddev->lock); -+ return EPOLLHUP | EPOLLERR; -+ } -+ -+ poll_wait(file, &client->ddev->waitq, pt); -+ -+ if (!kfifo_is_empty(&client->buffer)) -+ events |= EPOLLIN | EPOLLRDNORM; -+ -+ up_read(&client->ddev->lock); -+ return events; -+} -+ -+static int surface_dtx_fasync(int fd, struct file *file, int on) -+{ -+ struct sdtx_client *client = file->private_data; -+ -+ return fasync_helper(fd, file, on, &client->fasync); -+} -+ -+static const struct file_operations surface_dtx_fops = { -+ .owner = THIS_MODULE, -+ .open = surface_dtx_open, -+ .release = surface_dtx_release, -+ .read = surface_dtx_read, -+ .poll = surface_dtx_poll, -+ .fasync = surface_dtx_fasync, -+ .unlocked_ioctl = surface_dtx_ioctl, -+ .compat_ioctl = surface_dtx_ioctl, -+ .llseek = no_llseek, -+}; -+ -+ -+/* -- Event handling/forwarding. -------------------------------------------- */ -+ -+/* -+ * The device operation mode is not immediately updated on the EC when the -+ * base has been connected, i.e. querying the device mode inside the -+ * connection event callback yields an outdated value. Thus, we can only -+ * determine the new tablet-mode switch and device mode values after some -+ * time. -+ * -+ * These delays have been chosen by experimenting. We first delay on connect -+ * events, then check and validate the device mode against the base state and -+ * if invalid delay again by the "recheck" delay. -+ */ -+#define SDTX_DEVICE_MODE_DELAY_CONNECT msecs_to_jiffies(100) -+#define SDTX_DEVICE_MODE_DELAY_RECHECK msecs_to_jiffies(100) -+ -+static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay); -+ -+struct sdtx_status_event { -+ struct sdtx_event e; -+ __u16 v; -+} __packed; -+ -+struct sdtx_base_info_event { -+ struct sdtx_event e; -+ struct sdtx_base_info v; -+} __packed; -+ -+union sdtx_generic_event { -+ struct sdtx_event common; -+ struct sdtx_status_event status; -+ struct sdtx_base_info_event base; -+}; -+ -+/* Must be executed with ddev->write_lock held. */ -+static void sdtx_push_event(struct sdtx_device *ddev, struct sdtx_event *evt) -+{ -+ const size_t len = sizeof(struct sdtx_event) + evt->length; -+ struct sdtx_client *client; -+ -+ down_read(&ddev->client_lock); -+ list_for_each_entry(client, &ddev->client_list, node) { -+ if (!test_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags)) -+ continue; -+ -+ if (likely(kfifo_avail(&client->buffer) >= len)) -+ kfifo_in(&client->buffer, (const u8 *)evt, len); -+ else -+ dev_warn(ddev->dev, "event buffer overrun\n"); -+ -+ kill_fasync(&client->fasync, SIGIO, POLL_IN); -+ } -+ up_read(&ddev->client_lock); -+ -+ wake_up_interruptible(&ddev->waitq); -+} -+ -+static u32 sdtx_notifier(struct ssam_event_notifier *nf, -+ const struct ssam_event *in) -+{ -+ struct sdtx_device *ddev = container_of(nf, struct sdtx_device, notif); -+ union sdtx_generic_event event; -+ size_t len; -+ -+ /* Validate event payload length. */ -+ switch (in->command_id) { -+ case SAM_EVENT_CID_DTX_CONNECTION: -+ len = 2; -+ break; -+ -+ case SAM_EVENT_CID_DTX_REQUEST: -+ len = 0; -+ break; -+ -+ case SAM_EVENT_CID_DTX_CANCEL: -+ len = 1; -+ break; -+ -+ case SAM_EVENT_CID_DTX_LATCH_STATUS: -+ len = 1; -+ break; -+ -+ default: -+ return 0; -+ }; -+ -+ if (in->length != len) { -+ dev_err(ddev->dev, -+ "unexpected payload size for event %#04x: got %u, expected %zu\n", -+ in->command_id, in->length, len); -+ return 0; -+ } -+ -+ mutex_lock(&ddev->write_lock); -+ -+ /* Translate event. */ -+ switch (in->command_id) { -+ case SAM_EVENT_CID_DTX_CONNECTION: -+ clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); -+ -+ /* If state has not changed: do not send new event. */ -+ if (ddev->state.base.state == in->data[0] && -+ ddev->state.base.base_id == in->data[1]) -+ goto out; -+ -+ ddev->state.base.state = in->data[0]; -+ ddev->state.base.base_id = in->data[1]; -+ -+ event.base.e.length = sizeof(struct sdtx_base_info); -+ event.base.e.code = SDTX_EVENT_BASE_CONNECTION; -+ event.base.v.state = sdtx_translate_base_state(ddev, in->data[0]); -+ event.base.v.base_id = SDTX_BASE_TYPE_SSH(in->data[1]); -+ break; -+ -+ case SAM_EVENT_CID_DTX_REQUEST: -+ event.common.code = SDTX_EVENT_REQUEST; -+ event.common.length = 0; -+ break; -+ -+ case SAM_EVENT_CID_DTX_CANCEL: -+ event.status.e.length = sizeof(u16); -+ event.status.e.code = SDTX_EVENT_CANCEL; -+ event.status.v = sdtx_translate_cancel_reason(ddev, in->data[0]); -+ break; -+ -+ case SAM_EVENT_CID_DTX_LATCH_STATUS: -+ clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); -+ -+ /* If state has not changed: do not send new event. */ -+ if (ddev->state.latch_status == in->data[0]) -+ goto out; -+ -+ ddev->state.latch_status = in->data[0]; -+ -+ event.status.e.length = sizeof(u16); -+ event.status.e.code = SDTX_EVENT_LATCH_STATUS; -+ event.status.v = sdtx_translate_latch_status(ddev, in->data[0]); -+ break; -+ } -+ -+ sdtx_push_event(ddev, &event.common); -+ -+ /* Update device mode on base connection change. */ -+ if (in->command_id == SAM_EVENT_CID_DTX_CONNECTION) { -+ unsigned long delay; -+ -+ delay = in->data[0] ? SDTX_DEVICE_MODE_DELAY_CONNECT : 0; -+ sdtx_update_device_mode(ddev, delay); -+ } -+ -+out: -+ mutex_unlock(&ddev->write_lock); -+ return SSAM_NOTIF_HANDLED; -+} -+ -+ -+/* -- State update functions. ----------------------------------------------- */ -+ -+static bool sdtx_device_mode_invalid(u8 mode, u8 base_state) -+{ -+ return ((base_state == SSAM_BAS_BASE_STATE_ATTACHED) && -+ (mode == SDTX_DEVICE_MODE_TABLET)) || -+ ((base_state == SSAM_BAS_BASE_STATE_DETACH_SUCCESS) && -+ (mode != SDTX_DEVICE_MODE_TABLET)); -+} -+ -+static void sdtx_device_mode_workfn(struct work_struct *work) -+{ -+ struct sdtx_device *ddev; -+ struct sdtx_status_event event; -+ struct ssam_bas_base_info base; -+ int status, tablet; -+ u8 mode; -+ -+ ddev = container_of(work, struct sdtx_device, mode_work.work); -+ -+ /* Get operation mode. */ -+ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); -+ if (status) { -+ dev_err(ddev->dev, "failed to get device mode: %d\n", status); -+ return; -+ } -+ -+ /* Get base info. */ -+ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); -+ if (status) { -+ dev_err(ddev->dev, "failed to get base info: %d\n", status); -+ return; -+ } -+ -+ /* -+ * In some cases (specifically when attaching the base), the device -+ * mode isn't updated right away. Thus we check if the device mode -+ * makes sense for the given base state and try again later if it -+ * doesn't. -+ */ -+ if (sdtx_device_mode_invalid(mode, base.state)) { -+ dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); -+ sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); -+ return; -+ } -+ -+ mutex_lock(&ddev->write_lock); -+ clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); -+ -+ /* Avoid sending duplicate device-mode events. */ -+ if (ddev->state.device_mode == mode) { -+ mutex_unlock(&ddev->write_lock); -+ return; -+ } -+ -+ ddev->state.device_mode = mode; -+ -+ event.e.length = sizeof(u16); -+ event.e.code = SDTX_EVENT_DEVICE_MODE; -+ event.v = mode; -+ -+ sdtx_push_event(ddev, &event.e); -+ -+ /* Send SW_TABLET_MODE event. */ -+ tablet = mode != SDTX_DEVICE_MODE_LAPTOP; -+ input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); -+ input_sync(ddev->mode_switch); -+ -+ mutex_unlock(&ddev->write_lock); -+} -+ -+static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay) -+{ -+ schedule_delayed_work(&ddev->mode_work, delay); -+} -+ -+static void __sdtx_device_state_update_base(struct sdtx_device *ddev, -+ struct ssam_bas_base_info info) -+{ -+ struct sdtx_base_info_event event; -+ -+ /* Prevent duplicate events. */ -+ if (ddev->state.base.state == info.state && -+ ddev->state.base.base_id == info.base_id) -+ return; -+ -+ ddev->state.base = info; -+ -+ event.e.length = sizeof(struct sdtx_base_info); -+ event.e.code = SDTX_EVENT_BASE_CONNECTION; -+ event.v.state = sdtx_translate_base_state(ddev, info.state); -+ event.v.base_id = SDTX_BASE_TYPE_SSH(info.base_id); -+ -+ sdtx_push_event(ddev, &event.e); -+} -+ -+static void __sdtx_device_state_update_mode(struct sdtx_device *ddev, u8 mode) -+{ -+ struct sdtx_status_event event; -+ int tablet; -+ -+ /* -+ * Note: This function must be called after updating the base state -+ * via __sdtx_device_state_update_base(), as we rely on the updated -+ * base state value in the validity check below. -+ */ -+ -+ if (sdtx_device_mode_invalid(mode, ddev->state.base.state)) { -+ dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); -+ sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); -+ return; -+ } -+ -+ /* Prevent duplicate events. */ -+ if (ddev->state.device_mode == mode) -+ return; -+ -+ ddev->state.device_mode = mode; -+ -+ /* Send event. */ -+ event.e.length = sizeof(u16); -+ event.e.code = SDTX_EVENT_DEVICE_MODE; -+ event.v = mode; -+ -+ sdtx_push_event(ddev, &event.e); -+ -+ /* Send SW_TABLET_MODE event. */ -+ tablet = mode != SDTX_DEVICE_MODE_LAPTOP; -+ input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); -+ input_sync(ddev->mode_switch); -+} -+ -+static void __sdtx_device_state_update_latch(struct sdtx_device *ddev, u8 status) -+{ -+ struct sdtx_status_event event; -+ -+ /* Prevent duplicate events. */ -+ if (ddev->state.latch_status == status) -+ return; -+ -+ ddev->state.latch_status = status; -+ -+ event.e.length = sizeof(struct sdtx_base_info); -+ event.e.code = SDTX_EVENT_BASE_CONNECTION; -+ event.v = sdtx_translate_latch_status(ddev, status); -+ -+ sdtx_push_event(ddev, &event.e); -+} -+ -+static void sdtx_device_state_workfn(struct work_struct *work) -+{ -+ struct sdtx_device *ddev; -+ struct ssam_bas_base_info base; -+ u8 mode, latch; -+ int status; -+ -+ ddev = container_of(work, struct sdtx_device, state_work.work); -+ -+ /* Mark everything as dirty. */ -+ set_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); -+ set_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); -+ set_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); -+ -+ /* -+ * Ensure that the state gets marked as dirty before continuing to -+ * query it. Necessary to ensure that clear_bit() calls in -+ * sdtx_notifier() and sdtx_device_mode_workfn() actually clear these -+ * bits if an event is received while updating the state here. -+ */ -+ smp_mb__after_atomic(); -+ -+ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); -+ if (status) { -+ dev_err(ddev->dev, "failed to get base state: %d\n", status); -+ return; -+ } -+ -+ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); -+ if (status) { -+ dev_err(ddev->dev, "failed to get device mode: %d\n", status); -+ return; -+ } -+ -+ status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); -+ if (status) { -+ dev_err(ddev->dev, "failed to get latch status: %d\n", status); -+ return; -+ } -+ -+ mutex_lock(&ddev->write_lock); -+ -+ /* -+ * If the respective dirty-bit has been cleared, an event has been -+ * received, updating this state. The queried state may thus be out of -+ * date. At this point, we can safely assume that the state provided -+ * by the event is either up to date, or we're about to receive -+ * another event updating it. -+ */ -+ -+ if (test_and_clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags)) -+ __sdtx_device_state_update_base(ddev, base); -+ -+ if (test_and_clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags)) -+ __sdtx_device_state_update_mode(ddev, mode); -+ -+ if (test_and_clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags)) -+ __sdtx_device_state_update_latch(ddev, latch); -+ -+ mutex_unlock(&ddev->write_lock); -+} -+ -+static void sdtx_update_device_state(struct sdtx_device *ddev, unsigned long delay) -+{ -+ schedule_delayed_work(&ddev->state_work, delay); -+} -+ -+ -+/* -- Common device initialization. ----------------------------------------- */ -+ -+static int sdtx_device_init(struct sdtx_device *ddev, struct device *dev, -+ struct ssam_controller *ctrl) -+{ -+ int status, tablet_mode; -+ -+ /* Basic initialization. */ -+ kref_init(&ddev->kref); -+ ddev->dev = dev; -+ ddev->ctrl = ctrl; -+ -+ ddev->mdev.minor = MISC_DYNAMIC_MINOR; -+ ddev->mdev.name = "surface_dtx"; -+ ddev->mdev.nodename = "surface/dtx"; -+ ddev->mdev.fops = &surface_dtx_fops; -+ -+ ddev->notif.base.priority = 1; -+ ddev->notif.base.fn = sdtx_notifier; -+ ddev->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; -+ ddev->notif.event.id.target_category = SSAM_SSH_TC_BAS; -+ ddev->notif.event.id.instance = 0; -+ ddev->notif.event.mask = SSAM_EVENT_MASK_NONE; -+ ddev->notif.event.flags = SSAM_EVENT_SEQUENCED; -+ -+ init_waitqueue_head(&ddev->waitq); -+ mutex_init(&ddev->write_lock); -+ init_rwsem(&ddev->client_lock); -+ INIT_LIST_HEAD(&ddev->client_list); -+ -+ INIT_DELAYED_WORK(&ddev->mode_work, sdtx_device_mode_workfn); -+ INIT_DELAYED_WORK(&ddev->state_work, sdtx_device_state_workfn); -+ -+ /* -+ * Get current device state. We want to guarantee that events are only -+ * sent when state actually changes. Thus we cannot use special -+ * "uninitialized" values, as that would cause problems when manually -+ * querying the state in surface_dtx_pm_complete(). I.e. we would not -+ * be able to detect state changes there if no change event has been -+ * received between driver initialization and first device suspension. -+ * -+ * Note that we also need to do this before registering the event -+ * notifier, as that may access the state values. -+ */ -+ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &ddev->state.base); -+ if (status) -+ return status; -+ -+ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, -+ &ddev->state.device_mode); -+ if (status) -+ return status; -+ -+ status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, -+ &ddev->state.latch_status); -+ if (status) -+ return status; -+ -+ /* Set up tablet mode switch. */ -+ ddev->mode_switch = input_allocate_device(); -+ if (!ddev->mode_switch) -+ return -ENOMEM; -+ -+ ddev->mode_switch->name = "Microsoft Surface DTX Device Mode Switch"; -+ ddev->mode_switch->phys = "ssam/01:11:01:00:00/input0"; -+ ddev->mode_switch->id.bustype = BUS_HOST; -+ ddev->mode_switch->dev.parent = ddev->dev; -+ -+ tablet_mode = (ddev->state.device_mode != SDTX_DEVICE_MODE_LAPTOP); -+ input_set_capability(ddev->mode_switch, EV_SW, SW_TABLET_MODE); -+ input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet_mode); -+ -+ status = input_register_device(ddev->mode_switch); -+ if (status) { -+ input_free_device(ddev->mode_switch); -+ return status; -+ } -+ -+ /* Set up event notifier. */ -+ status = ssam_notifier_register(ddev->ctrl, &ddev->notif); -+ if (status) -+ goto err_notif; -+ -+ /* Register miscdevice. */ -+ status = misc_register(&ddev->mdev); -+ if (status) -+ goto err_mdev; -+ -+ /* -+ * Update device state in case it has changed between getting the -+ * initial mode and registering the event notifier. -+ */ -+ sdtx_update_device_state(ddev, 0); -+ return 0; -+ -+err_notif: -+ ssam_notifier_unregister(ddev->ctrl, &ddev->notif); -+ cancel_delayed_work_sync(&ddev->mode_work); -+err_mdev: -+ input_unregister_device(ddev->mode_switch); -+ return status; -+} -+ -+static struct sdtx_device *sdtx_device_setup(struct device *dev, -+ struct ssam_controller *ctrl) -+{ -+ struct sdtx_device *ddev; -+ int status; -+ -+ ddev = kzalloc(sizeof(*ddev), GFP_KERNEL); -+ if (!ddev) -+ return ERR_PTR(-ENOMEM); -+ -+ status = sdtx_device_init(ddev, dev, ctrl); -+ if (status) { -+ kfree(ddev); -+ return ERR_PTR(status); -+ } -+ -+ return ddev; -+} -+ -+static void sdtx_device_destroy(struct sdtx_device *ddev) -+{ -+ struct sdtx_client *client; -+ -+ /* Disable notifiers, prevent new events from arriving. */ -+ ssam_notifier_unregister(ddev->ctrl, &ddev->notif); -+ -+ /* Stop mode_work, prevent access to mode_switch. */ -+ cancel_delayed_work_sync(&ddev->mode_work); -+ -+ /* Stop state_work. */ -+ cancel_delayed_work_sync(&ddev->state_work); -+ -+ /* With mode_work canceled, we can unregister the mode_switch. */ -+ input_unregister_device(ddev->mode_switch); -+ -+ /* -+ * Mark device as shut-down. Prevent new clients from being added and -+ * new operations from being executed. -+ */ -+ set_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags); -+ -+ /* Wake up async clients. */ -+ down_write(&ddev->client_lock); -+ list_for_each_entry(client, &ddev->client_list, node) { -+ kill_fasync(&client->fasync, SIGIO, POLL_HUP); -+ } -+ up_write(&ddev->client_lock); -+ -+ /* Wake up blocking clients. */ -+ wake_up_interruptible(&ddev->waitq); -+ -+ /* -+ * Wait for clients to finish their current operation. After this, the -+ * controller and device references are guaranteed to be no longer in -+ * use. -+ */ -+ down_write(&ddev->lock); -+ ddev->dev = NULL; -+ ddev->ctrl = NULL; -+ up_write(&ddev->lock); -+ -+ /* Finally remove the misc-device. */ -+ misc_deregister(&ddev->mdev); -+ -+ /* -+ * We're now guaranteed that sdtx_device_open() won't be called any -+ * more, so we can now drop out reference. -+ */ -+ sdtx_device_put(ddev); -+} -+ -+ -+/* -- PM ops. --------------------------------------------------------------- */ -+ -+#ifdef CONFIG_PM_SLEEP -+ -+static void surface_dtx_pm_complete(struct device *dev) -+{ -+ struct sdtx_device *ddev = dev_get_drvdata(dev); -+ -+ /* -+ * Normally, the EC will store events while suspended (i.e. in -+ * display-off state) and release them when resumed (i.e. transitioned -+ * to display-on state). During hibernation, however, the EC will be -+ * shut down and does not store events. Furthermore, events might be -+ * dropped during prolonged suspension (it is currently unknown how -+ * big this event buffer is and how it behaves on overruns). -+ * -+ * To prevent any problems, we update the device state here. We do -+ * this delayed to ensure that any events sent by the EC directly -+ * after resuming will be handled first. The delay below has been -+ * chosen (experimentally), so that there should be ample time for -+ * these events to be handled, before we check and, if necessary, -+ * update the state. -+ */ -+ sdtx_update_device_state(ddev, msecs_to_jiffies(1000)); -+} -+ -+static const struct dev_pm_ops surface_dtx_pm_ops = { -+ .complete = surface_dtx_pm_complete, -+}; -+ -+#else /* CONFIG_PM_SLEEP */ -+ -+static const struct dev_pm_ops surface_dtx_pm_ops = {}; -+ -+#endif /* CONFIG_PM_SLEEP */ -+ -+ -+/* -- Platform driver. ------------------------------------------------------ */ -+ -+static int surface_dtx_platform_probe(struct platform_device *pdev) -+{ -+ struct ssam_controller *ctrl; -+ struct sdtx_device *ddev; -+ -+ /* Link to EC. */ -+ ctrl = ssam_client_bind(&pdev->dev); -+ if (IS_ERR(ctrl)) -+ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); -+ -+ ddev = sdtx_device_setup(&pdev->dev, ctrl); -+ if (IS_ERR(ddev)) -+ return PTR_ERR(ddev); -+ -+ platform_set_drvdata(pdev, ddev); -+ return 0; -+} -+ -+static int surface_dtx_platform_remove(struct platform_device *pdev) -+{ -+ sdtx_device_destroy(platform_get_drvdata(pdev)); -+ return 0; -+} -+ -+static const struct acpi_device_id surface_dtx_acpi_match[] = { -+ { "MSHW0133", 0 }, -+ { }, -+}; -+MODULE_DEVICE_TABLE(acpi, surface_dtx_acpi_match); -+ -+static struct platform_driver surface_dtx_platform_driver = { -+ .probe = surface_dtx_platform_probe, -+ .remove = surface_dtx_platform_remove, -+ .driver = { -+ .name = "surface_dtx_pltf", -+ .acpi_match_table = surface_dtx_acpi_match, -+ .pm = &surface_dtx_pm_ops, -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+ -+/* -- SSAM device driver. --------------------------------------------------- */ + +#ifdef CONFIG_SURFACE_AGGREGATOR_BUS + -+static int surface_dtx_ssam_probe(struct ssam_device *sdev) -+{ -+ struct sdtx_device *ddev; ++void ssam_controller_remove_clients(struct ssam_controller *ctrl); + -+ ddev = sdtx_device_setup(&sdev->dev, sdev->ctrl); -+ if (IS_ERR(ddev)) -+ return PTR_ERR(ddev); -+ -+ ssam_device_set_drvdata(sdev, ddev); -+ return 0; -+} -+ -+static void surface_dtx_ssam_remove(struct ssam_device *sdev) -+{ -+ sdtx_device_destroy(ssam_device_get_drvdata(sdev)); -+} -+ -+static const struct ssam_device_id surface_dtx_ssam_match[] = { -+ { SSAM_SDEV(BAS, 0x01, 0x00, 0x00) }, -+ { }, -+}; -+MODULE_DEVICE_TABLE(ssam, surface_dtx_ssam_match); -+ -+static struct ssam_device_driver surface_dtx_ssam_driver = { -+ .probe = surface_dtx_ssam_probe, -+ .remove = surface_dtx_ssam_remove, -+ .match_table = surface_dtx_ssam_match, -+ .driver = { -+ .name = "surface_dtx", -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+static int ssam_dtx_driver_register(void) -+{ -+ return ssam_device_driver_register(&surface_dtx_ssam_driver); -+} -+ -+static void ssam_dtx_driver_unregister(void) -+{ -+ ssam_device_driver_unregister(&surface_dtx_ssam_driver); -+} ++int ssam_bus_register(void); ++void ssam_bus_unregister(void); + +#else /* CONFIG_SURFACE_AGGREGATOR_BUS */ + -+static int ssam_dtx_driver_register(void) -+{ -+ return 0; -+} -+ -+static void ssam_dtx_driver_unregister(void) -+{ -+} ++static inline void ssam_controller_remove_clients(struct ssam_controller *ctrl) {} ++static inline int ssam_bus_register(void) { return 0; } ++static inline void ssam_bus_unregister(void) {} + +#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ -+ -+ -+/* -- Module setup. --------------------------------------------------------- */ -+ -+static int __init surface_dtx_init(void) -+{ -+ int status; -+ -+ status = ssam_dtx_driver_register(); -+ if (status) -+ return status; -+ -+ status = platform_driver_register(&surface_dtx_platform_driver); -+ if (status) -+ ssam_dtx_driver_unregister(); -+ -+ return status; -+} -+module_init(surface_dtx_init); -+ -+static void __exit surface_dtx_exit(void) -+{ -+ platform_driver_unregister(&surface_dtx_platform_driver); -+ ssam_dtx_driver_unregister(); -+} -+module_exit(surface_dtx_exit); -+ -+MODULE_AUTHOR("Maximilian Luz "); -+MODULE_DESCRIPTION("Detachment-system driver for Surface System Aggregator Module"); -+MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/clients/surface_hid.c b/drivers/misc/surface_aggregator/clients/surface_hid.c -new file mode 100644 -index 000000000000..7d0362ae31d4 ---- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/surface_hid.c -@@ -0,0 +1,924 @@ -+// SPDX-License-Identifier: GPL-2.0+ -+/* -+ * Surface System Aggregator Module (SSAM) HID device driver. -+ * -+ * Provides support for HID input devices connected via the Surface System -+ * Aggregator Module. -+ * -+ * Copyright (C) 2019-2020 Blaž Hrastnik , -+ * Maximilian Luz -+ */ -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+#include -+#include -+ -+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 report_id, -+ u8 *data, size_t len); -+ int (*get_feature_report)(struct surface_hid_device *shid, u8 report_id, -+ u8 *data, size_t len); -+ int (*set_feature_report)(struct surface_hid_device *shid, u8 report_id, -+ u8 *data, 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; -+}; -+ -+ -+/* -- SAM interface (HID). -------------------------------------------------- */ -+ -+#ifdef CONFIG_SURFACE_AGGREGATOR_BUS -+ -+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 report_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] = report_id; -+ -+ return ssam_retry(ssam_request_sync, shid->ctrl, &rqst, NULL); -+} -+ -+static int ssam_hid_get_raw_report(struct surface_hid_device *shid, -+ u8 report_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(report_id); -+ rqst.payload = &report_id; -+ -+ rsp.capacity = len; -+ rsp.length = 0; -+ rsp.pointer = buf; -+ -+ return ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp, -+ sizeof(report_id)); -+} -+ -+static u32 ssam_hid_event_fn(struct ssam_event_notifier *nf, -+ const struct ssam_event *event) -+{ -+ struct surface_hid_device *shid; -+ int status; -+ -+ shid = container_of(nf, struct surface_hid_device, notif); -+ -+ if (event->command_id != 0x00) -+ return 0; -+ -+ status = hid_input_report(shid->hid, HID_INPUT_REPORT, -+ (u8 *)&event->data[0], event->length, 0); -+ -+ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; -+} -+ -+ -+/* -- Transport driver (HID). ----------------------------------------------- */ -+ -+static int shid_output_report(struct surface_hid_device *shid, u8 report_id, -+ u8 *data, size_t len) -+{ -+ int status; -+ -+ status = ssam_hid_set_raw_report(shid, report_id, false, data, len); -+ return status >= 0 ? len : status; -+} -+ -+static int shid_get_feature_report(struct surface_hid_device *shid, -+ u8 report_id, u8 *data, size_t len) -+{ -+ int status; -+ -+ status = ssam_hid_get_raw_report(shid, report_id, data, len); -+ return status >= 0 ? len : status; -+} -+ -+static int shid_set_feature_report(struct surface_hid_device *shid, -+ u8 report_id, u8 *data, size_t len) -+{ -+ int status; -+ -+ status = ssam_hid_set_raw_report(shid, report_id, true, data, len); -+ return status >= 0 ? len : status; -+} -+ -+#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ -+ -+ -+/* -- SAM interface (KBD). -------------------------------------------------- */ -+ -+#define KBD_FEATURE_REPORT_SIZE 7 /* 6 + report ID */ -+ -+enum surface_kbd_cid { -+ SURFACE_KBD_CID_GET_DESCRIPTOR = 0x00, -+ SURFACE_KBD_CID_SET_CAPSLOCK_LED = 0x01, -+ SURFACE_KBD_CID_EVT_INPUT_GENERIC = 0x03, -+ SURFACE_KBD_CID_EVT_INPUT_HOTKEYS = 0x04, -+ SURFACE_KBD_CID_GET_FEATURE_REPORT = 0x0b, -+}; -+ -+static int ssam_kbd_get_descriptor(struct surface_hid_device *shid, u8 entry, -+ u8 *buf, size_t len) -+{ -+ struct ssam_request rqst; -+ struct ssam_response rsp; -+ int status; -+ -+ rqst.target_category = shid->uid.category; -+ rqst.target_id = shid->uid.target; -+ rqst.command_id = SURFACE_KBD_CID_GET_DESCRIPTOR; -+ rqst.instance_id = shid->uid.instance; -+ rqst.flags = SSAM_REQUEST_HAS_RESPONSE; -+ rqst.length = sizeof(entry); -+ rqst.payload = &entry; -+ -+ rsp.capacity = len; -+ rsp.length = 0; -+ rsp.pointer = buf; -+ -+ status = ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp, -+ sizeof(entry)); -+ if (status) -+ return status; -+ -+ if (rsp.length != len) { -+ dev_err(shid->dev, -+ "invalid descriptor length: got %zu, expected, %zu\n", -+ rsp.length, len); -+ return -EPROTO; -+ } -+ -+ return 0; -+} -+ -+static int ssam_kbd_set_caps_led(struct surface_hid_device *shid, bool value) -+{ -+ struct ssam_request rqst; -+ u8 value_u8 = value; -+ -+ rqst.target_category = shid->uid.category; -+ rqst.target_id = shid->uid.target; -+ rqst.command_id = SURFACE_KBD_CID_SET_CAPSLOCK_LED; -+ rqst.instance_id = shid->uid.instance; -+ rqst.flags = 0; -+ rqst.length = sizeof(value_u8); -+ rqst.payload = &value_u8; -+ -+ return ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, NULL, -+ sizeof(value_u8)); -+} -+ -+static int ssam_kbd_get_feature_report(struct surface_hid_device *shid, u8 *buf, -+ size_t len) -+{ -+ struct ssam_request rqst; -+ struct ssam_response rsp; -+ u8 payload = 0; -+ int status; -+ -+ rqst.target_category = shid->uid.category; -+ rqst.target_id = shid->uid.target; -+ rqst.command_id = SURFACE_KBD_CID_GET_FEATURE_REPORT; -+ rqst.instance_id = shid->uid.instance; -+ rqst.flags = SSAM_REQUEST_HAS_RESPONSE; -+ rqst.length = sizeof(payload); -+ rqst.payload = &payload; -+ -+ rsp.capacity = len; -+ rsp.length = 0; -+ rsp.pointer = buf; -+ -+ status = ssam_retry(ssam_request_sync_onstack, shid->ctrl, &rqst, &rsp, -+ sizeof(payload)); -+ if (status) -+ return status; -+ -+ if (rsp.length != len) { -+ dev_err(shid->dev, -+ "invalid feature report length: got %zu, expected, %zu\n", -+ rsp.length, len); -+ return -EPROTO; -+ } -+ -+ return 0; -+} -+ -+static bool ssam_kbd_is_input_event(const struct ssam_event *event) -+{ -+ if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_GENERIC) -+ return true; -+ -+ if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_HOTKEYS) -+ return true; -+ -+ return false; -+} -+ -+static u32 ssam_kbd_event_fn(struct ssam_event_notifier *nf, -+ const struct ssam_event *event) -+{ -+ struct surface_hid_device *shid; -+ int status; -+ -+ shid = container_of(nf, struct surface_hid_device, notif); -+ -+ /* -+ * Check against device UID manually, as registry and device target -+ * category doesn't line up. -+ */ -+ -+ if (shid->uid.category != event->target_category) -+ return 0; -+ -+ if (shid->uid.target != event->target_id) -+ return 0; -+ -+ if (shid->uid.instance != event->instance_id) -+ return 0; -+ -+ if (!ssam_kbd_is_input_event(event)) -+ return 0; -+ -+ status = hid_input_report(shid->hid, HID_INPUT_REPORT, -+ (u8 *)&event->data[0], event->length, 0); -+ -+ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; -+} -+ -+ -+/* -- Transport driver (KBD). ----------------------------------------------- */ -+ -+static int skbd_get_caps_led_value(struct hid_device *hid, u8 report_id, -+ u8 *data, size_t len) -+{ -+ struct hid_field *field; -+ unsigned int offset, size; -+ int i; -+ -+ /* Get led field. */ -+ field = hidinput_get_led_field(hid); -+ if (!field) -+ return -ENOENT; -+ -+ /* Check if we got the correct report. */ -+ if (len != hid_report_len(field->report)) -+ return -ENOENT; -+ -+ if (report_id != field->report->id) -+ return -ENOENT; -+ -+ /* Get caps lock led index. */ -+ for (i = 0; i < field->report_count; i++) -+ if ((field->usage[i].hid & 0xffff) == 0x02) -+ break; -+ -+ if (i == field->report_count) -+ return -ENOENT; -+ -+ /* Extract value. */ -+ size = field->report_size; -+ offset = field->report_offset + i * size; -+ return !!hid_field_extract(hid, data + 1, size, offset); -+} -+ -+static int skbd_output_report(struct surface_hid_device *shid, u8 report_id, -+ u8 *data, size_t len) -+{ -+ int caps_led; -+ int status; -+ -+ caps_led = skbd_get_caps_led_value(shid->hid, report_id, data, len); -+ if (caps_led < 0) -+ return -EIO; /* Only caps output reports are supported. */ -+ -+ status = ssam_kbd_set_caps_led(shid, caps_led); -+ if (status < 0) -+ return status; -+ -+ return len; -+} -+ -+static int skbd_get_feature_report(struct surface_hid_device *shid, -+ u8 report_id, u8 *data, size_t len) -+{ -+ u8 report[KBD_FEATURE_REPORT_SIZE]; -+ int status; -+ -+ /* -+ * The keyboard only has a single hard-coded read-only feature report -+ * of size KBD_FEATURE_REPORT_SIZE. Try to load it and compare its -+ * report ID against the requested one. -+ */ -+ -+ if (len < ARRAY_SIZE(report)) -+ return -ENOSPC; -+ -+ status = ssam_kbd_get_feature_report(shid, report, ARRAY_SIZE(report)); -+ if (status < 0) -+ return status; -+ -+ if (report_id != report[0]) -+ return -ENOENT; -+ -+ memcpy(data, report, ARRAY_SIZE(report)); -+ return len; -+} -+ -+static int skbd_set_feature_report(struct surface_hid_device *shid, -+ u8 report_id, u8 *data, size_t len) -+{ -+ return -EIO; -+} -+ -+ -+/* -- 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. -------------------------------------------------- */ -+ -+static 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; // TODO: BUS_SURFACE -+ 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); -+ -+ strlcpy(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; -+} -+ -+static void surface_hid_device_destroy(struct surface_hid_device *shid) -+{ -+ hid_destroy_device(shid->hid); -+} -+ -+ -+/* -- 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, -+}; -+ -+#else /* CONFIG_PM_SLEEP */ -+ -+const struct dev_pm_ops surface_hid_pm_ops = { }; -+ -+#endif /* CONFIG_PM_SLEEP */ -+ -+ -+/* -- Driver setup (HID). --------------------------------------------------- */ -+ -+#ifdef CONFIG_SURFACE_AGGREGATOR_BUS -+ -+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, -+ }, -+}; -+ -+static int surface_hid_driver_register(void) -+{ -+ return ssam_device_driver_register(&surface_hid_driver); -+} -+ -+static void surface_hid_driver_unregister(void) -+{ -+ ssam_device_driver_unregister(&surface_hid_driver); -+} -+ -+#else /* CONFIG_SURFACE_AGGREGATOR_BUS */ -+ -+static int surface_hid_driver_register(void) -+{ -+ return 0; -+} -+ -+static void surface_hid_driver_unregister(void) -+{ -+} -+ -+#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ -+ -+ -+/* -- Driver setup (KBD). --------------------------------------------------- */ -+ -+static int surface_kbd_probe(struct platform_device *pdev) -+{ -+ struct ssam_controller *ctrl; -+ struct surface_hid_device *shid; -+ -+ /* Add device link to EC. */ -+ ctrl = ssam_client_bind(&pdev->dev); -+ if (IS_ERR(ctrl)) -+ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); -+ -+ shid = devm_kzalloc(&pdev->dev, sizeof(*shid), GFP_KERNEL); -+ if (!shid) -+ return -ENOMEM; -+ -+ shid->dev = &pdev->dev; -+ shid->ctrl = ctrl; -+ -+ shid->uid.domain = SSAM_DOMAIN_SERIALHUB; -+ shid->uid.category = SSAM_SSH_TC_KBD; -+ shid->uid.target = 2; -+ shid->uid.instance = 0; -+ shid->uid.function = 0; -+ -+ shid->notif.base.priority = 1; -+ shid->notif.base.fn = ssam_kbd_event_fn; -+ shid->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; -+ shid->notif.event.id.target_category = shid->uid.category; -+ shid->notif.event.id.instance = shid->uid.instance; -+ shid->notif.event.mask = SSAM_EVENT_MASK_NONE; -+ shid->notif.event.flags = 0; -+ -+ shid->ops.get_descriptor = ssam_kbd_get_descriptor; -+ shid->ops.output_report = skbd_output_report; -+ shid->ops.get_feature_report = skbd_get_feature_report; -+ shid->ops.set_feature_report = skbd_set_feature_report; -+ -+ platform_set_drvdata(pdev, shid); -+ return surface_hid_device_add(shid); -+} -+ -+static int surface_kbd_remove(struct platform_device *pdev) -+{ -+ surface_hid_device_destroy(platform_get_drvdata(pdev)); -+ return 0; -+} -+ -+static const struct acpi_device_id surface_kbd_match[] = { -+ { "MSHW0096" }, -+ { }, -+}; -+MODULE_DEVICE_TABLE(acpi, surface_kbd_match); -+ -+static struct platform_driver surface_kbd_driver = { -+ .probe = surface_kbd_probe, -+ .remove = surface_kbd_remove, -+ .driver = { -+ .name = "surface_keyboard", -+ .acpi_match_table = surface_kbd_match, -+ .pm = &surface_hid_pm_ops, -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+ -+ -+/* -- Module setup. --------------------------------------------------------- */ -+ -+static int __init surface_hid_init(void) -+{ -+ int status; -+ -+ status = surface_hid_driver_register(); -+ if (status) -+ return status; -+ -+ status = platform_driver_register(&surface_kbd_driver); -+ if (status) -+ surface_hid_driver_unregister(); -+ -+ return status; -+} -+module_init(surface_hid_init); -+ -+static void __exit surface_hid_exit(void) -+{ -+ platform_driver_unregister(&surface_kbd_driver); -+ surface_hid_driver_unregister(); -+} -+module_exit(surface_hid_exit); -+ -+MODULE_AUTHOR("Blaž Hrastnik "); -+MODULE_AUTHOR("Maximilian Luz "); -+MODULE_DESCRIPTION("HID transport-/device-driver for Surface System Aggregator Module"); -+MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/clients/surface_perfmode.c b/drivers/misc/surface_aggregator/clients/surface_perfmode.c -new file mode 100644 -index 000000000000..cac7227f27ea ---- /dev/null -+++ b/drivers/misc/surface_aggregator/clients/surface_perfmode.c -@@ -0,0 +1,122 @@ -+// SPDX-License-Identifier: GPL-2.0+ -+/* -+ * Surface performance-mode driver. -+ * -+ * Provides a user-space interface for the performance mode control provided -+ * by the Surface System Aggregator Module (SSAM), influencing cooling -+ * behavior of the device and potentially managing power limits. -+ * -+ * Copyright (C) 2019-2020 Maximilian Luz -+ */ -+ -+#include -+#include -+#include -+#include -+#include -+ -+#include -+ -+enum sam_perf_mode { -+ SAM_PERF_MODE_NORMAL = 1, -+ SAM_PERF_MODE_BATTERY = 2, -+ SAM_PERF_MODE_PERF1 = 3, -+ SAM_PERF_MODE_PERF2 = 4, -+ -+ __SAM_PERF_MODE__MIN = 1, -+ __SAM_PERF_MODE__MAX = 4, -+}; -+ -+struct ssam_perf_info { -+ __le32 mode; -+ __le16 unknown1; -+ __le16 unknown2; -+} __packed; -+ -+static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_tmp_perf_mode_get, struct ssam_perf_info, { -+ .target_category = SSAM_SSH_TC_TMP, -+ .command_id = 0x02, -+}); -+ -+static SSAM_DEFINE_SYNC_REQUEST_CL_W(__ssam_tmp_perf_mode_set, __le32, { -+ .target_category = SSAM_SSH_TC_TMP, -+ .command_id = 0x03, -+}); -+ -+static int ssam_tmp_perf_mode_set(struct ssam_device *sdev, u32 mode) -+{ -+ __le32 mode_le = cpu_to_le32(mode); -+ -+ if (mode < __SAM_PERF_MODE__MIN || mode > __SAM_PERF_MODE__MAX) -+ return -EINVAL; -+ -+ return ssam_retry(__ssam_tmp_perf_mode_set, sdev, &mode_le); -+} -+ -+static ssize_t perf_mode_show(struct device *dev, struct device_attribute *attr, -+ char *data) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); -+ struct ssam_perf_info info; -+ int status; -+ -+ status = ssam_retry(ssam_tmp_perf_mode_get, sdev, &info); -+ if (status) { -+ dev_err(dev, "failed to get current performance mode: %d\n", -+ status); -+ return -EIO; -+ } -+ -+ return sprintf(data, "%d\n", le32_to_cpu(info.mode)); -+} -+ -+static ssize_t perf_mode_store(struct device *dev, struct device_attribute *attr, -+ const char *data, size_t count) -+{ -+ struct ssam_device *sdev = to_ssam_device(dev); -+ int perf_mode; -+ int status; -+ -+ status = kstrtoint(data, 0, &perf_mode); -+ if (status < 0) -+ return status; -+ -+ status = ssam_tmp_perf_mode_set(sdev, perf_mode); -+ if (status < 0) -+ return status; -+ -+ return count; -+} -+ -+static const DEVICE_ATTR_RW(perf_mode); -+ -+static int surface_sam_sid_perfmode_probe(struct ssam_device *sdev) -+{ -+ return sysfs_create_file(&sdev->dev.kobj, &dev_attr_perf_mode.attr); -+} -+ -+static void surface_sam_sid_perfmode_remove(struct ssam_device *sdev) -+{ -+ sysfs_remove_file(&sdev->dev.kobj, &dev_attr_perf_mode.attr); -+} -+ -+static const struct ssam_device_id ssam_perfmode_match[] = { -+ { SSAM_SDEV(TMP, 0x01, 0x00, 0x01) }, -+ { }, -+}; -+MODULE_DEVICE_TABLE(ssam, ssam_perfmode_match); -+ -+static struct ssam_device_driver surface_sam_sid_perfmode = { -+ .probe = surface_sam_sid_perfmode_probe, -+ .remove = surface_sam_sid_perfmode_remove, -+ .match_table = ssam_perfmode_match, -+ .driver = { -+ .name = "surface_performance_mode", -+ .probe_type = PROBE_PREFER_ASYNCHRONOUS, -+ }, -+}; -+module_ssam_device_driver(surface_sam_sid_perfmode); -+ -+MODULE_AUTHOR("Maximilian Luz "); -+MODULE_DESCRIPTION("Performance mode interface for Surface System Aggregator Module"); -+MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/controller.c b/drivers/misc/surface_aggregator/controller.c ++#endif /* _SURFACE_AGGREGATOR_BUS_H */ +diff --git a/drivers/platform/x86/surface_aggregator/controller.c b/drivers/platform/x86/surface_aggregator/controller.c new file mode 100644 index 000000000000..5bcb59ed579d --- /dev/null -+++ b/drivers/misc/surface_aggregator/controller.c ++++ b/drivers/platform/x86/surface_aggregator/controller.c @@ -0,0 +1,2579 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* @@ -11586,11 +8108,11 @@ index 000000000000..5bcb59ed579d + } + disable_irq(ctrl->irq.num); +} -diff --git a/drivers/misc/surface_aggregator/controller.h b/drivers/misc/surface_aggregator/controller.h +diff --git a/drivers/platform/x86/surface_aggregator/controller.h b/drivers/platform/x86/surface_aggregator/controller.h new file mode 100644 index 000000000000..8297d34e7489 --- /dev/null -+++ b/drivers/misc/surface_aggregator/controller.h ++++ b/drivers/platform/x86/surface_aggregator/controller.h @@ -0,0 +1,285 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* @@ -11877,12 +8399,12 @@ index 000000000000..8297d34e7489 +void ssam_event_item_cache_destroy(void); + +#endif /* _SURFACE_AGGREGATOR_CONTROLLER_H */ -diff --git a/drivers/misc/surface_aggregator/core.c b/drivers/misc/surface_aggregator/core.c +diff --git a/drivers/platform/x86/surface_aggregator/core.c b/drivers/platform/x86/surface_aggregator/core.c new file mode 100644 -index 000000000000..0b5807e88565 +index 000000000000..1b35f3e284da --- /dev/null -+++ b/drivers/misc/surface_aggregator/core.c -@@ -0,0 +1,851 @@ ++++ b/drivers/platform/x86/surface_aggregator/core.c +@@ -0,0 +1,850 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Surface Serial Hub (SSH) driver for communication with the Surface/System @@ -12165,7 +8687,6 @@ index 000000000000..0b5807e88565 + b = ((version >> 8) & 0xffff); + c = version & 0xff; + -+ // FIXME: we should use sysfs_emit here, but that's not available on < 5.10 + return scnprintf(buf, PAGE_SIZE, "%u.%u.%u\n", a, b, c); +} +static DEVICE_ATTR_RO(firmware_version); @@ -12734,11 +9255,11 @@ index 000000000000..0b5807e88565 +MODULE_AUTHOR("Maximilian Luz "); +MODULE_DESCRIPTION("Subsystem and Surface Serial Hub driver for Surface System Aggregator Module"); +MODULE_LICENSE("GPL"); -diff --git a/drivers/misc/surface_aggregator/ssh_msgb.h b/drivers/misc/surface_aggregator/ssh_msgb.h +diff --git a/drivers/platform/x86/surface_aggregator/ssh_msgb.h b/drivers/platform/x86/surface_aggregator/ssh_msgb.h new file mode 100644 index 000000000000..1221f642dda1 --- /dev/null -+++ b/drivers/misc/surface_aggregator/ssh_msgb.h ++++ b/drivers/platform/x86/surface_aggregator/ssh_msgb.h @@ -0,0 +1,205 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* @@ -12945,11 +9466,11 @@ index 000000000000..1221f642dda1 +} + +#endif /* _SURFACE_AGGREGATOR_SSH_MSGB_H */ -diff --git a/drivers/misc/surface_aggregator/ssh_packet_layer.c b/drivers/misc/surface_aggregator/ssh_packet_layer.c +diff --git a/drivers/platform/x86/surface_aggregator/ssh_packet_layer.c b/drivers/platform/x86/surface_aggregator/ssh_packet_layer.c new file mode 100644 -index 000000000000..583315db8b02 +index 000000000000..15d96eac6811 --- /dev/null -+++ b/drivers/misc/surface_aggregator/ssh_packet_layer.c ++++ b/drivers/platform/x86/surface_aggregator/ssh_packet_layer.c @@ -0,0 +1,2074 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* @@ -14727,7 +11248,7 @@ index 000000000000..583315db8b02 + break; + } + -+ return aligned.ptr - source->ptr + SSH_MESSAGE_LENGTH(frame->len); ++ return aligned.ptr - source->ptr + SSH_MESSAGE_LENGTH(payload.len); +} + +static int ssh_ptl_rx_threadfn(void *data) @@ -15025,11 +11546,11 @@ index 000000000000..583315db8b02 + kfifo_free(&ptl->rx.fifo); + sshp_buf_free(&ptl->rx.buf); +} -diff --git a/drivers/misc/surface_aggregator/ssh_packet_layer.h b/drivers/misc/surface_aggregator/ssh_packet_layer.h +diff --git a/drivers/platform/x86/surface_aggregator/ssh_packet_layer.h b/drivers/platform/x86/surface_aggregator/ssh_packet_layer.h new file mode 100644 index 000000000000..e8757d03f279 --- /dev/null -+++ b/drivers/misc/surface_aggregator/ssh_packet_layer.h ++++ b/drivers/platform/x86/surface_aggregator/ssh_packet_layer.h @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* @@ -15221,11 +11742,11 @@ index 000000000000..e8757d03f279 +void ssh_ctrl_packet_cache_destroy(void); + +#endif /* _SURFACE_AGGREGATOR_SSH_PACKET_LAYER_H */ -diff --git a/drivers/misc/surface_aggregator/ssh_parser.c b/drivers/misc/surface_aggregator/ssh_parser.c +diff --git a/drivers/platform/x86/surface_aggregator/ssh_parser.c b/drivers/platform/x86/surface_aggregator/ssh_parser.c new file mode 100644 index 000000000000..e2dead8de94a --- /dev/null -+++ b/drivers/misc/surface_aggregator/ssh_parser.c ++++ b/drivers/platform/x86/surface_aggregator/ssh_parser.c @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* @@ -15455,12 +11976,12 @@ index 000000000000..e2dead8de94a + + return 0; +} -diff --git a/drivers/misc/surface_aggregator/ssh_parser.h b/drivers/misc/surface_aggregator/ssh_parser.h +diff --git a/drivers/platform/x86/surface_aggregator/ssh_parser.h b/drivers/platform/x86/surface_aggregator/ssh_parser.h new file mode 100644 -index 000000000000..395c61ef890b +index 000000000000..63c38d350988 --- /dev/null -+++ b/drivers/misc/surface_aggregator/ssh_parser.h -@@ -0,0 +1,155 @@ ++++ b/drivers/platform/x86/surface_aggregator/ssh_parser.h +@@ -0,0 +1,154 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * SSH message parser. @@ -15478,7 +11999,6 @@ index 000000000000..395c61ef890b + +#include + -+ +/** + * struct sshp_buf - Parser buffer for SSH messages. + * @ptr: Pointer to the beginning of the buffer. @@ -15616,11 +12136,11 @@ index 000000000000..395c61ef890b + struct ssam_span *command_data); + +#endif /* _SURFACE_AGGREGATOR_SSH_PARSER_h */ -diff --git a/drivers/misc/surface_aggregator/ssh_request_layer.c b/drivers/misc/surface_aggregator/ssh_request_layer.c +diff --git a/drivers/platform/x86/surface_aggregator/ssh_request_layer.c b/drivers/platform/x86/surface_aggregator/ssh_request_layer.c new file mode 100644 index 000000000000..52a83a8fcf82 --- /dev/null -+++ b/drivers/misc/surface_aggregator/ssh_request_layer.c ++++ b/drivers/platform/x86/surface_aggregator/ssh_request_layer.c @@ -0,0 +1,1263 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* @@ -16885,11 +13405,11 @@ index 000000000000..52a83a8fcf82 + ssh_request_put(r); + } +} -diff --git a/drivers/misc/surface_aggregator/ssh_request_layer.h b/drivers/misc/surface_aggregator/ssh_request_layer.h +diff --git a/drivers/platform/x86/surface_aggregator/ssh_request_layer.h b/drivers/platform/x86/surface_aggregator/ssh_request_layer.h new file mode 100644 index 000000000000..cb35815858d1 --- /dev/null -+++ b/drivers/misc/surface_aggregator/ssh_request_layer.h ++++ b/drivers/platform/x86/surface_aggregator/ssh_request_layer.h @@ -0,0 +1,143 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* @@ -17034,11 +13554,11 @@ index 000000000000..cb35815858d1 + const struct ssh_request_ops *ops); + +#endif /* _SURFACE_AGGREGATOR_SSH_REQUEST_LAYER_H */ -diff --git a/drivers/misc/surface_aggregator/trace.h b/drivers/misc/surface_aggregator/trace.h +diff --git a/drivers/platform/x86/surface_aggregator/trace.h b/drivers/platform/x86/surface_aggregator/trace.h new file mode 100644 index 000000000000..eb332bb53ae4 --- /dev/null -+++ b/drivers/misc/surface_aggregator/trace.h ++++ b/drivers/platform/x86/surface_aggregator/trace.h @@ -0,0 +1,632 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* @@ -17672,6 +14192,3638 @@ index 000000000000..eb332bb53ae4 +#define TRACE_INCLUDE_FILE trace + +#include +diff --git a/drivers/platform/x86/surface_aggregator_cdev.c b/drivers/platform/x86/surface_aggregator_cdev.c +new file mode 100644 +index 000000000000..79e28fab7e40 +--- /dev/null ++++ b/drivers/platform/x86/surface_aggregator_cdev.c +@@ -0,0 +1,322 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++/* ++ * Provides user-space access to the SSAM EC via the /dev/surface/aggregator ++ * misc device. Intended for debugging and development. ++ * ++ * Copyright (C) 2020 Maximilian Luz ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++ ++#define SSAM_CDEV_DEVICE_NAME "surface_aggregator_cdev" ++ ++struct ssam_cdev { ++ struct kref kref; ++ struct rw_semaphore lock; ++ struct ssam_controller *ctrl; ++ struct miscdevice mdev; ++}; ++ ++static void __ssam_cdev_release(struct kref *kref) ++{ ++ kfree(container_of(kref, struct ssam_cdev, kref)); ++} ++ ++static struct ssam_cdev *ssam_cdev_get(struct ssam_cdev *cdev) ++{ ++ if (cdev) ++ kref_get(&cdev->kref); ++ ++ return cdev; ++} ++ ++static void ssam_cdev_put(struct ssam_cdev *cdev) ++{ ++ if (cdev) ++ kref_put(&cdev->kref, __ssam_cdev_release); ++} ++ ++static int ssam_cdev_device_open(struct inode *inode, struct file *filp) ++{ ++ struct miscdevice *mdev = filp->private_data; ++ struct ssam_cdev *cdev = container_of(mdev, struct ssam_cdev, mdev); ++ ++ filp->private_data = ssam_cdev_get(cdev); ++ return stream_open(inode, filp); ++} ++ ++static int ssam_cdev_device_release(struct inode *inode, struct file *filp) ++{ ++ ssam_cdev_put(filp->private_data); ++ return 0; ++} ++ ++static long ssam_cdev_request(struct ssam_cdev *cdev, unsigned long arg) ++{ ++ struct ssam_cdev_request __user *r; ++ struct ssam_cdev_request rqst; ++ struct ssam_request spec = {}; ++ struct ssam_response rsp = {}; ++ const void __user *plddata; ++ void __user *rspdata; ++ int status = 0, ret = 0, tmp; ++ ++ r = (struct ssam_cdev_request __user *)arg; ++ ret = copy_struct_from_user(&rqst, sizeof(rqst), r, sizeof(*r)); ++ if (ret) ++ goto out; ++ ++ plddata = u64_to_user_ptr(rqst.payload.data); ++ rspdata = u64_to_user_ptr(rqst.response.data); ++ ++ /* Setup basic request fields. */ ++ spec.target_category = rqst.target_category; ++ spec.target_id = rqst.target_id; ++ spec.command_id = rqst.command_id; ++ spec.instance_id = rqst.instance_id; ++ spec.flags = 0; ++ spec.length = rqst.payload.length; ++ spec.payload = NULL; ++ ++ if (rqst.flags & SSAM_CDEV_REQUEST_HAS_RESPONSE) ++ spec.flags |= SSAM_REQUEST_HAS_RESPONSE; ++ ++ if (rqst.flags & SSAM_CDEV_REQUEST_UNSEQUENCED) ++ spec.flags |= SSAM_REQUEST_UNSEQUENCED; ++ ++ rsp.capacity = rqst.response.length; ++ rsp.length = 0; ++ rsp.pointer = NULL; ++ ++ /* Get request payload from user-space. */ ++ if (spec.length) { ++ if (!plddata) { ++ ret = -EINVAL; ++ goto out; ++ } ++ ++ /* ++ * Note: spec.length is limited to U16_MAX bytes via struct ++ * ssam_cdev_request. This is slightly larger than the ++ * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the ++ * underlying protocol (note that nothing remotely this size ++ * should ever be allocated in any normal case). This size is ++ * validated later in ssam_request_sync(), for allocation the ++ * bound imposed by u16 should be enough. ++ */ ++ spec.payload = kzalloc(spec.length, GFP_KERNEL); ++ if (!spec.payload) { ++ ret = -ENOMEM; ++ goto out; ++ } ++ ++ if (copy_from_user((void *)spec.payload, plddata, spec.length)) { ++ ret = -EFAULT; ++ goto out; ++ } ++ } ++ ++ /* Allocate response buffer. */ ++ if (rsp.capacity) { ++ if (!rspdata) { ++ ret = -EINVAL; ++ goto out; ++ } ++ ++ /* ++ * Note: rsp.capacity is limited to U16_MAX bytes via struct ++ * ssam_cdev_request. This is slightly larger than the ++ * theoretical maximum (SSH_COMMAND_MAX_PAYLOAD_SIZE) of the ++ * underlying protocol (note that nothing remotely this size ++ * should ever be allocated in any normal case). In later use, ++ * this capacity does not have to be strictly bounded, as it ++ * is only used as an output buffer to be written to. For ++ * allocation the bound imposed by u16 should be enough. ++ */ ++ rsp.pointer = kzalloc(rsp.capacity, GFP_KERNEL); ++ if (!rsp.pointer) { ++ ret = -ENOMEM; ++ goto out; ++ } ++ } ++ ++ /* Perform request. */ ++ status = ssam_request_sync(cdev->ctrl, &spec, &rsp); ++ if (status) ++ goto out; ++ ++ /* Copy response to user-space. */ ++ if (rsp.length && copy_to_user(rspdata, rsp.pointer, rsp.length)) ++ ret = -EFAULT; ++ ++out: ++ /* Always try to set response-length and status. */ ++ tmp = put_user(rsp.length, &r->response.length); ++ if (tmp) ++ ret = tmp; ++ ++ tmp = put_user(status, &r->status); ++ if (tmp) ++ ret = tmp; ++ ++ /* Cleanup. */ ++ kfree(spec.payload); ++ kfree(rsp.pointer); ++ ++ return ret; ++} ++ ++static long __ssam_cdev_device_ioctl(struct ssam_cdev *cdev, unsigned int cmd, ++ unsigned long arg) ++{ ++ switch (cmd) { ++ case SSAM_CDEV_REQUEST: ++ return ssam_cdev_request(cdev, arg); ++ ++ default: ++ return -ENOTTY; ++ } ++} ++ ++static long ssam_cdev_device_ioctl(struct file *file, unsigned int cmd, ++ unsigned long arg) ++{ ++ struct ssam_cdev *cdev = file->private_data; ++ long status; ++ ++ /* Ensure that controller is valid for as long as we need it. */ ++ if (down_read_killable(&cdev->lock)) ++ return -ERESTARTSYS; ++ ++ if (!cdev->ctrl) { ++ up_read(&cdev->lock); ++ return -ENODEV; ++ } ++ ++ status = __ssam_cdev_device_ioctl(cdev, cmd, arg); ++ ++ up_read(&cdev->lock); ++ return status; ++} ++ ++static const struct file_operations ssam_controller_fops = { ++ .owner = THIS_MODULE, ++ .open = ssam_cdev_device_open, ++ .release = ssam_cdev_device_release, ++ .unlocked_ioctl = ssam_cdev_device_ioctl, ++ .compat_ioctl = ssam_cdev_device_ioctl, ++ .llseek = noop_llseek, ++}; ++ ++static int ssam_dbg_device_probe(struct platform_device *pdev) ++{ ++ struct ssam_controller *ctrl; ++ struct ssam_cdev *cdev; ++ int status; ++ ++ ctrl = ssam_client_bind(&pdev->dev); ++ if (IS_ERR(ctrl)) ++ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); ++ ++ cdev = kzalloc(sizeof(*cdev), GFP_KERNEL); ++ if (!cdev) ++ return -ENOMEM; ++ ++ kref_init(&cdev->kref); ++ init_rwsem(&cdev->lock); ++ cdev->ctrl = ctrl; ++ ++ cdev->mdev.parent = &pdev->dev; ++ cdev->mdev.minor = MISC_DYNAMIC_MINOR; ++ cdev->mdev.name = "surface_aggregator"; ++ cdev->mdev.nodename = "surface/aggregator"; ++ cdev->mdev.fops = &ssam_controller_fops; ++ ++ status = misc_register(&cdev->mdev); ++ if (status) { ++ kfree(cdev); ++ return status; ++ } ++ ++ platform_set_drvdata(pdev, cdev); ++ return 0; ++} ++ ++static int ssam_dbg_device_remove(struct platform_device *pdev) ++{ ++ struct ssam_cdev *cdev = platform_get_drvdata(pdev); ++ ++ misc_deregister(&cdev->mdev); ++ ++ /* ++ * The controller is only guaranteed to be valid for as long as the ++ * driver is bound. Remove controller so that any lingering open files ++ * cannot access it any more after we're gone. ++ */ ++ down_write(&cdev->lock); ++ cdev->ctrl = NULL; ++ up_write(&cdev->lock); ++ ++ ssam_cdev_put(cdev); ++ return 0; ++} ++ ++static struct platform_device *ssam_cdev_device; ++ ++static struct platform_driver ssam_cdev_driver = { ++ .probe = ssam_dbg_device_probe, ++ .remove = ssam_dbg_device_remove, ++ .driver = { ++ .name = SSAM_CDEV_DEVICE_NAME, ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++ ++static int __init ssam_debug_init(void) ++{ ++ int status; ++ ++ ssam_cdev_device = platform_device_alloc(SSAM_CDEV_DEVICE_NAME, ++ PLATFORM_DEVID_NONE); ++ if (!ssam_cdev_device) ++ return -ENOMEM; ++ ++ status = platform_device_add(ssam_cdev_device); ++ if (status) ++ goto err_device; ++ ++ status = platform_driver_register(&ssam_cdev_driver); ++ if (status) ++ goto err_driver; ++ ++ return 0; ++ ++err_driver: ++ platform_device_del(ssam_cdev_device); ++err_device: ++ platform_device_put(ssam_cdev_device); ++ return status; ++} ++module_init(ssam_debug_init); ++ ++static void __exit ssam_debug_exit(void) ++{ ++ platform_driver_unregister(&ssam_cdev_driver); ++ platform_device_unregister(ssam_cdev_device); ++} ++module_exit(ssam_debug_exit); ++ ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("User-space interface for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/platform/x86/surface_aggregator_registry.c b/drivers/platform/x86/surface_aggregator_registry.c +new file mode 100644 +index 000000000000..bfd99743da53 +--- /dev/null ++++ b/drivers/platform/x86/surface_aggregator_registry.c +@@ -0,0 +1,615 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++/* ++ * Surface System Aggregator Module (SSAM) client device registry. ++ * ++ * Registry for non-platform/non-ACPI SSAM client devices, i.e. devices that ++ * cannot be auto-detected. Provides device-hubs for these devices. ++ * ++ * Copyright (C) 2020 Maximilian Luz ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++ ++ ++/* -- Device registry structures. ------------------------------------------- */ ++ ++struct ssam_hub_cell { ++ struct ssam_device_uid uid; ++ void *data; ++}; ++ ++struct ssam_hub_desc { ++ const struct ssam_hub_cell *cells; ++ unsigned int num_cells; ++}; ++ ++ ++#define SSAM_DUID(cat, tid, iid, fun) \ ++ ((struct ssam_device_uid) { \ ++ .domain = SSAM_DOMAIN_SERIALHUB, \ ++ .category = SSAM_SSH_TC_##cat, \ ++ .target = (tid), \ ++ .instance = (iid), \ ++ .function = (fun) \ ++ }) ++ ++#define SSAM_VDUID(cat, tid, iid, fun) \ ++ ((struct ssam_device_uid) { \ ++ .domain = SSAM_DOMAIN_VIRTUAL, \ ++ .category = SSAM_VIRTUAL_TC_##cat, \ ++ .target = (tid), \ ++ .instance = (iid), \ ++ .function = (fun) \ ++ }) ++ ++#define SSAM_DUID_HUB_MAIN SSAM_VDUID(HUB, 0x01, 0x00, 0x00) ++#define SSAM_DUID_HUB_BASE SSAM_VDUID(HUB, 0x02, 0x00, 0x00) ++ ++#define SSAM_DEFINE_HUB_DESC(__name, __cells) \ ++ struct ssam_hub_desc __name = { \ ++ .cells = __cells, \ ++ .num_cells = ARRAY_SIZE(__cells), \ ++ }; ++ ++#define SSAM_DEFINE_PLATFORM_HUB(__suffix) \ ++ static const SSAM_DEFINE_HUB_DESC(ssam_device_hub_##__suffix, \ ++ ssam_devices_##__suffix); \ ++ static const struct ssam_hub_cell ssam_platform_hubs_##__suffix[] = { \ ++ { SSAM_DUID_HUB_MAIN, (void *)&ssam_device_hub_##__suffix }, \ ++ }; \ ++ static const SSAM_DEFINE_HUB_DESC(ssam_platform_hub_##__suffix, \ ++ ssam_platform_hubs_##__suffix); \ ++ ++#define SSAM_DEFINE_PLATFORM_HUB_WITH_BASE(__suffix) \ ++ static const SSAM_DEFINE_HUB_DESC(ssam_device_hub_##__suffix, \ ++ ssam_devices_##__suffix); \ ++ static const SSAM_DEFINE_HUB_DESC(ssam_device_hub_##__suffix##_base, \ ++ ssam_devices_##__suffix##_base); \ ++ static const struct ssam_hub_cell ssam_platform_hubs_##__suffix[] = { \ ++ { SSAM_DUID_HUB_MAIN, (void *)&ssam_device_hub_##__suffix }, \ ++ { SSAM_DUID_HUB_BASE, (void *)&ssam_device_hub_##__suffix##_base },\ ++ }; \ ++ static const SSAM_DEFINE_HUB_DESC(ssam_platform_hub_##__suffix, \ ++ ssam_platform_hubs_##__suffix); \ ++ ++ ++/* -- Device registry. ------------------------------------------------------ */ ++ ++#define SSAM_DUID_BAT_AC SSAM_DUID(BAT, 0x01, 0x01, 0x01) ++#define SSAM_DUID_BAT_MAIN SSAM_DUID(BAT, 0x01, 0x01, 0x00) ++#define SSAM_DUID_BAT_SB3BASE SSAM_DUID(BAT, 0x02, 0x01, 0x00) ++ ++#define SSAM_DUID_TMP_PERF SSAM_DUID(TMP, 0x01, 0x00, 0x01) ++ ++#define SSAM_DUID_BAS_DTX SSAM_DUID(BAS, 0x01, 0x00, 0x00) ++ ++#define SSAM_DUID_HID_KEYBOARD SSAM_DUID(HID, 0x02, 0x01, 0x00) ++#define SSAM_DUID_HID_TOUCHPAD SSAM_DUID(HID, 0x02, 0x03, 0x00) ++#define SSAM_DUID_HID_IID5 SSAM_DUID(HID, 0x02, 0x05, 0x00) ++#define SSAM_DUID_HID_IID6 SSAM_DUID(HID, 0x02, 0x06, 0x00) ++ ++ ++static const struct ssam_hub_cell ssam_devices_sb2[] = { ++ { SSAM_DUID_TMP_PERF }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sb3[] = { ++ { SSAM_DUID_TMP_PERF }, ++ { SSAM_DUID_BAT_AC }, ++ { SSAM_DUID_BAT_MAIN }, ++ { SSAM_DUID_BAS_DTX }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sb3_base[] = { ++ { SSAM_DUID_BAT_SB3BASE }, ++ { SSAM_DUID_HID_KEYBOARD }, ++ { SSAM_DUID_HID_TOUCHPAD }, ++ { SSAM_DUID_HID_IID5 }, ++ { SSAM_DUID_HID_IID6 }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sl1[] = { ++ { SSAM_DUID_TMP_PERF }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sl2[] = { ++ { SSAM_DUID_TMP_PERF }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sl3[] = { ++ { SSAM_DUID_TMP_PERF }, ++ { SSAM_DUID_BAT_AC }, ++ { SSAM_DUID_BAT_MAIN }, ++ { SSAM_DUID_HID_KEYBOARD }, ++ { SSAM_DUID_HID_TOUCHPAD }, ++ { SSAM_DUID_HID_IID5 }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_slg1[] = { ++ { SSAM_DUID_TMP_PERF }, ++ { SSAM_DUID_BAT_AC }, ++ { SSAM_DUID_BAT_MAIN }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sp5[] = { ++ { SSAM_DUID_TMP_PERF }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sp6[] = { ++ { SSAM_DUID_TMP_PERF }, ++}; ++ ++static const struct ssam_hub_cell ssam_devices_sp7[] = { ++ { SSAM_DUID_TMP_PERF }, ++ { SSAM_DUID_BAT_AC }, ++ { SSAM_DUID_BAT_MAIN }, ++}; ++ ++SSAM_DEFINE_PLATFORM_HUB(sb2); ++SSAM_DEFINE_PLATFORM_HUB_WITH_BASE(sb3); ++SSAM_DEFINE_PLATFORM_HUB(sl1); ++SSAM_DEFINE_PLATFORM_HUB(sl2); ++SSAM_DEFINE_PLATFORM_HUB(sl3); ++SSAM_DEFINE_PLATFORM_HUB(slg1); ++SSAM_DEFINE_PLATFORM_HUB(sp5); ++SSAM_DEFINE_PLATFORM_HUB(sp6); ++SSAM_DEFINE_PLATFORM_HUB(sp7); ++ ++ ++/* -- Device registry helper functions. ------------------------------------- */ ++ ++static int ssam_hub_remove_devices_fn(struct device *dev, void *data) ++{ ++ if (!is_ssam_device(dev)) ++ return 0; ++ ++ ssam_device_remove(to_ssam_device(dev)); ++ return 0; ++} ++ ++static void ssam_hub_remove_devices(struct device *parent) ++{ ++ device_for_each_child_reverse(parent, NULL, ssam_hub_remove_devices_fn); ++} ++ ++static int ssam_hub_add_device(struct device *parent, ++ struct ssam_controller *ctrl, ++ const struct ssam_hub_cell *cell) ++{ ++ struct ssam_device *sdev; ++ int status; ++ ++ sdev = ssam_device_alloc(ctrl, cell->uid); ++ if (!sdev) ++ return -ENOMEM; ++ ++ sdev->dev.parent = parent; ++ sdev->dev.platform_data = cell->data; ++ ++ status = ssam_device_add(sdev); ++ if (status) ++ ssam_device_put(sdev); ++ ++ return status; ++} ++ ++static int ssam_hub_add_devices(struct device *parent, ++ struct ssam_controller *ctrl, ++ const struct ssam_hub_desc *desc) ++{ ++ int status, i; ++ ++ for (i = 0; i < desc->num_cells; i++) { ++ status = ssam_hub_add_device(parent, ctrl, &desc->cells[i]); ++ if (status) ++ goto err; ++ } ++ ++ return 0; ++err: ++ ssam_hub_remove_devices(parent); ++ return status; ++} ++ ++ ++/* -- SSAM main-hub driver. ------------------------------------------------- */ ++ ++static int ssam_hub_probe(struct ssam_device *sdev) ++{ ++ const struct ssam_hub_desc *desc = dev_get_platdata(&sdev->dev); ++ ++ if (!desc) ++ return -ENODEV; ++ ++ return ssam_hub_add_devices(&sdev->dev, sdev->ctrl, desc); ++} ++ ++static void ssam_hub_remove(struct ssam_device *sdev) ++{ ++ ssam_hub_remove_devices(&sdev->dev); ++} ++ ++static const struct ssam_device_id ssam_hub_match[] = { ++ { SSAM_VDEV(HUB, 0x01, 0x00, 0x00) }, ++ { }, ++}; ++ ++static struct ssam_device_driver ssam_hub_driver = { ++ .probe = ssam_hub_probe, ++ .remove = ssam_hub_remove, ++ .match_table = ssam_hub_match, ++ .driver = { ++ .name = "surface_aggregator_device_hub", ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++ ++ ++/* -- SSAM base-hub driver. ------------------------------------------------- */ ++ ++enum ssam_base_hub_state { ++ SSAM_BASE_HUB_UNINITIALIZED, ++ SSAM_BASE_HUB_CONNECTED, ++ SSAM_BASE_HUB_DISCONNECTED, ++}; ++ ++struct ssam_base_hub { ++ struct ssam_device *sdev; ++ const struct ssam_hub_desc *devices; ++ ++ struct mutex lock; ++ enum ssam_base_hub_state state; ++ ++ struct ssam_event_notifier notif; ++}; ++ ++static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x0d, ++ .instance_id = 0x00, ++}); ++ ++#define SSAM_BAS_OPMODE_TABLET 0x00 ++#define SSAM_EVENT_BAS_CID_CONNECTION 0x0c ++ ++static int ssam_base_hub_query_state(struct ssam_device *sdev, ++ enum ssam_base_hub_state *state) ++{ ++ u8 opmode; ++ int status; ++ ++ status = ssam_retry(ssam_bas_query_opmode, sdev->ctrl, &opmode); ++ if (status < 0) { ++ dev_err(&sdev->dev, "failed to query base state: %d\n", status); ++ return status; ++ } ++ ++ if (opmode != SSAM_BAS_OPMODE_TABLET) ++ *state = SSAM_BASE_HUB_CONNECTED; ++ else ++ *state = SSAM_BASE_HUB_DISCONNECTED; ++ ++ return 0; ++} ++ ++static ssize_t ssam_base_hub_state_show(struct device *dev, ++ struct device_attribute *attr, ++ char *buf) ++{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ struct ssam_base_hub *hub = ssam_device_get_drvdata(sdev); ++ bool connected; ++ ++ mutex_lock(&hub->lock); ++ connected = hub->state == SSAM_BASE_HUB_CONNECTED; ++ mutex_unlock(&hub->lock); ++ ++ return scnprintf(buf, PAGE_SIZE, "%d\n", connected); ++} ++ ++static struct device_attribute ssam_base_hub_attr_state = ++ __ATTR(state, S_IRUGO, ssam_base_hub_state_show, NULL); ++ ++static struct attribute *ssam_base_hub_attrs[] = { ++ &ssam_base_hub_attr_state.attr, ++ NULL, ++}; ++ ++const struct attribute_group ssam_base_hub_group = { ++ .attrs = ssam_base_hub_attrs, ++}; ++ ++static int ssam_base_hub_update(struct ssam_device *sdev, ++ enum ssam_base_hub_state new) ++{ ++ struct ssam_base_hub *hub = ssam_device_get_drvdata(sdev); ++ int status = 0; ++ ++ mutex_lock(&hub->lock); ++ if (hub->state == new) { ++ mutex_unlock(&hub->lock); ++ return 0; ++ } ++ hub->state = new; ++ ++ if (hub->state == SSAM_BASE_HUB_CONNECTED) ++ status = ssam_hub_add_devices(&sdev->dev, sdev->ctrl, hub->devices); ++ ++ if (hub->state != SSAM_BASE_HUB_CONNECTED || status) ++ ssam_hub_remove_devices(&sdev->dev); ++ ++ mutex_unlock(&hub->lock); ++ ++ if (status) { ++ dev_err(&sdev->dev, "failed to update base-hub devices: %d\n", ++ status); ++ } ++ ++ return status; ++} ++ ++static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, ++ const struct ssam_event *event) ++{ ++ struct ssam_base_hub *hub; ++ struct ssam_device *sdev; ++ enum ssam_base_hub_state new; ++ ++ hub = container_of(nf, struct ssam_base_hub, notif); ++ sdev = hub->sdev; ++ ++ if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION) ++ return 0; ++ ++ if (event->length < 1) { ++ dev_err(&sdev->dev, "unexpected payload size: %u\n", ++ event->length); ++ return 0; ++ } ++ ++ if (event->data[0]) ++ new = SSAM_BASE_HUB_CONNECTED; ++ else ++ new = SSAM_BASE_HUB_DISCONNECTED; ++ ++ ssam_base_hub_update(sdev, new); ++ ++ /* ++ * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and ++ * consumed by the detachment system driver. We're just a (more or less) ++ * silent observer. ++ */ ++ return 0; ++} ++ ++static int __maybe_unused ssam_base_hub_resume(struct device *dev) ++{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ enum ssam_base_hub_state state; ++ int status; ++ ++ status = ssam_base_hub_query_state(sdev, &state); ++ if (status) ++ return status; ++ ++ return ssam_base_hub_update(sdev, state); ++} ++static SIMPLE_DEV_PM_OPS(ssam_base_hub_pm_ops, NULL, ssam_base_hub_resume); ++ ++static int ssam_base_hub_probe(struct ssam_device *sdev) ++{ ++ const struct ssam_hub_desc *desc = dev_get_platdata(&sdev->dev); ++ const struct ssam_device_id *match; ++ enum ssam_base_hub_state state; ++ struct ssam_base_hub *hub; ++ int status; ++ ++ if (!desc) ++ return -ENODEV; ++ ++ match = ssam_device_get_match(sdev); ++ if (!match) ++ return -ENODEV; ++ ++ hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL); ++ if (!hub) ++ return -ENOMEM; ++ ++ mutex_init(&hub->lock); ++ ++ hub->sdev = sdev; ++ hub->devices = desc; ++ hub->state = SSAM_BASE_HUB_UNINITIALIZED; ++ ++ hub->notif.base.priority = 1000; /* This notifier should run first. */ ++ hub->notif.base.fn = ssam_base_hub_notif; ++ hub->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; ++ hub->notif.event.id.target_category = SSAM_SSH_TC_BAS, ++ hub->notif.event.id.instance = 0, ++ hub->notif.event.mask = SSAM_EVENT_MASK_NONE; ++ hub->notif.event.flags = SSAM_EVENT_SEQUENCED; ++ ++ status = ssam_notifier_register(sdev->ctrl, &hub->notif); ++ if (status) ++ return status; ++ ++ ssam_device_set_drvdata(sdev, hub); ++ ++ status = ssam_base_hub_query_state(sdev, &state); ++ if (status) { ++ ssam_notifier_unregister(sdev->ctrl, &hub->notif); ++ return status; ++ } ++ ++ status = ssam_base_hub_update(sdev, state); ++ if (status) { ++ ssam_notifier_unregister(sdev->ctrl, &hub->notif); ++ return status; ++ } ++ ++ status = sysfs_create_group(&sdev->dev.kobj, &ssam_base_hub_group); ++ if (status) { ++ ssam_notifier_unregister(sdev->ctrl, &hub->notif); ++ ssam_hub_remove_devices(&sdev->dev); ++ } ++ ++ return status; ++} ++ ++static void ssam_base_hub_remove(struct ssam_device *sdev) ++{ ++ struct ssam_base_hub *hub = ssam_device_get_drvdata(sdev); ++ ++ sysfs_remove_group(&sdev->dev.kobj, &ssam_base_hub_group); ++ ++ ssam_notifier_unregister(sdev->ctrl, &hub->notif); ++ ssam_hub_remove_devices(&sdev->dev); ++ ++ mutex_destroy(&hub->lock); ++} ++ ++static const struct ssam_device_id ssam_base_hub_match[] = { ++ { SSAM_VDEV(HUB, 0x02, 0x00, 0x00) }, ++ { }, ++}; ++ ++static struct ssam_device_driver ssam_base_hub_driver = { ++ .probe = ssam_base_hub_probe, ++ .remove = ssam_base_hub_remove, ++ .match_table = ssam_base_hub_match, ++ .driver = { ++ .name = "surface_aggregator_base_hub", ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ .pm = &ssam_base_hub_pm_ops, ++ }, ++}; ++ ++ ++/* -- SSAM platform/meta-hub driver. ---------------------------------------- */ ++ ++static const struct acpi_device_id ssam_platform_hub_match[] = { ++ /* Surface Pro 4, 5, and 6 */ ++ { "MSHW0081", (unsigned long)&ssam_platform_hub_sp5 }, ++ ++ /* Surface Pro 6 (OMBR >= 0x10) */ ++ { "MSHW0111", (unsigned long)&ssam_platform_hub_sp6 }, ++ ++ /* Surface Pro 7 */ ++ { "MSHW0116", (unsigned long)&ssam_platform_hub_sp7 }, ++ ++ /* Surface Book 2 */ ++ { "MSHW0107", (unsigned long)&ssam_platform_hub_sb2 }, ++ ++ /* Surface Book 3 */ ++ { "MSHW0117", (unsigned long)&ssam_platform_hub_sb3 }, ++ ++ /* Surface Laptop 1 */ ++ { "MSHW0086", (unsigned long)&ssam_platform_hub_sl1 }, ++ ++ /* Surface Laptop 2 */ ++ { "MSHW0112", (unsigned long)&ssam_platform_hub_sl2 }, ++ ++ /* Surface Laptop 3 (13", Intel) */ ++ { "MSHW0114", (unsigned long)&ssam_platform_hub_sl3 }, ++ ++ /* Surface Laptop 3 (15", AMD) */ ++ { "MSHW0110", (unsigned long)&ssam_platform_hub_sl3 }, ++ ++ /* Surface Laptop Go 1 */ ++ { "MSHW0118", (unsigned long)&ssam_platform_hub_slg1 }, ++ ++ { }, ++}; ++MODULE_DEVICE_TABLE(acpi, ssam_platform_hub_match); ++ ++static int ssam_platform_hub_probe(struct platform_device *pdev) ++{ ++ const struct ssam_hub_desc *desc; ++ struct ssam_controller *ctrl; ++ ++ desc = acpi_device_get_match_data(&pdev->dev); ++ if (!desc) ++ return -ENODEV; ++ ++ /* ++ * As we're adding the SSAM client devices as children under this device ++ * and not the SSAM controller, we need to add a device link to the ++ * controller to ensure that we remove all of our devices before the ++ * controller is removed. This also guarantees proper ordering for ++ * suspend/resume of the devices on this hub. ++ */ ++ ctrl = ssam_client_bind(&pdev->dev); ++ if (IS_ERR(ctrl)) ++ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); ++ ++ return ssam_hub_add_devices(&pdev->dev, ctrl, desc); ++} ++ ++static int ssam_platform_hub_remove(struct platform_device *pdev) ++{ ++ ssam_hub_remove_devices(&pdev->dev); ++ return 0; ++} ++ ++static struct platform_driver ssam_platform_hub_driver = { ++ .probe = ssam_platform_hub_probe, ++ .remove = ssam_platform_hub_remove, ++ .driver = { ++ .name = "surface_aggregator_platform_hub", ++ .acpi_match_table = ssam_platform_hub_match, ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++ ++ ++/* -- Module initialization. ------------------------------------------------ */ ++ ++static int __init ssam_device_hub_init(void) ++{ ++ int status; ++ ++ status = platform_driver_register(&ssam_platform_hub_driver); ++ if (status) ++ goto err_platform; ++ ++ status = ssam_device_driver_register(&ssam_hub_driver); ++ if (status) ++ goto err_main; ++ ++ status = ssam_device_driver_register(&ssam_base_hub_driver); ++ if (status) ++ goto err_base; ++ ++ return 0; ++ ++err_base: ++ ssam_device_driver_unregister(&ssam_hub_driver); ++err_main: ++ platform_driver_unregister(&ssam_platform_hub_driver); ++err_platform: ++ return status; ++} ++ ++static void __exit ssam_device_hub_exit(void) ++{ ++ ssam_device_driver_unregister(&ssam_base_hub_driver); ++ ssam_device_driver_unregister(&ssam_hub_driver); ++ platform_driver_unregister(&ssam_platform_hub_driver); ++} ++ ++module_init(ssam_device_hub_init); ++module_exit(ssam_device_hub_exit); ++ ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("Device-registry for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/platform/x86/surface_dtx.c b/drivers/platform/x86/surface_dtx.c +new file mode 100644 +index 000000000000..4bb5d286bf95 +--- /dev/null ++++ b/drivers/platform/x86/surface_dtx.c +@@ -0,0 +1,1289 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++/* ++ * Surface Book (gen. 2 and later) detachment system (DTX) driver. ++ * ++ * Provides a user-space interface to properly handle clipboard/tablet ++ * (containing screen and processor) detachment from the base of the device ++ * (containing the keyboard and optionally a discrete GPU). Allows to ++ * acknowledge (to speed things up), abort (e.g. in case the dGPU is still in ++ * use), or request detachment via user-space. ++ * ++ * Copyright (C) 2019-2021 Maximilian Luz ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++ ++/* -- SSAM interface. ------------------------------------------------------- */ ++ ++enum sam_event_cid_bas { ++ SAM_EVENT_CID_DTX_CONNECTION = 0x0c, ++ SAM_EVENT_CID_DTX_REQUEST = 0x0e, ++ SAM_EVENT_CID_DTX_CANCEL = 0x0f, ++ SAM_EVENT_CID_DTX_LATCH_STATUS = 0x11, ++}; ++ ++enum ssam_bas_base_state { ++ SSAM_BAS_BASE_STATE_DETACH_SUCCESS = 0x00, ++ SSAM_BAS_BASE_STATE_ATTACHED = 0x01, ++ SSAM_BAS_BASE_STATE_NOT_FEASIBLE = 0x02, ++}; ++ ++enum ssam_bas_latch_status { ++ SSAM_BAS_LATCH_STATUS_CLOSED = 0x00, ++ SSAM_BAS_LATCH_STATUS_OPENED = 0x01, ++ SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN = 0x02, ++ SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN = 0x03, ++ SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE = 0x04, ++}; ++ ++enum ssam_bas_cancel_reason { ++ SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE = 0x00, /* Low battery. */ ++ SSAM_BAS_CANCEL_REASON_TIMEOUT = 0x02, ++ SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN = 0x03, ++ SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN = 0x04, ++ SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE = 0x05, ++}; ++ ++struct ssam_bas_base_info { ++ u8 state; ++ u8 base_id; ++} __packed; ++ ++static_assert(sizeof(struct ssam_bas_base_info) == 2); ++ ++static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_lock, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x06, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_unlock, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x07, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_request, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x08, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_confirm, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x09, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_heartbeat, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x0a, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_N(ssam_bas_latch_cancel, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x0b, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_base, struct ssam_bas_base_info, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x0c, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_device_mode, u8, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x0d, ++ .instance_id = 0x00, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_get_latch_status, u8, { ++ .target_category = SSAM_SSH_TC_BAS, ++ .target_id = 0x01, ++ .command_id = 0x11, ++ .instance_id = 0x00, ++}); ++ ++ ++/* -- Main structures. ------------------------------------------------------ */ ++ ++enum sdtx_device_state { ++ SDTX_DEVICE_SHUTDOWN_BIT = BIT(0), ++ SDTX_DEVICE_DIRTY_BASE_BIT = BIT(1), ++ SDTX_DEVICE_DIRTY_MODE_BIT = BIT(2), ++ SDTX_DEVICE_DIRTY_LATCH_BIT = BIT(3), ++}; ++ ++struct sdtx_device { ++ struct kref kref; ++ struct rw_semaphore lock; /* Guards device and controller reference. */ ++ ++ struct device *dev; ++ struct ssam_controller *ctrl; ++ unsigned long flags; ++ ++ struct miscdevice mdev; ++ wait_queue_head_t waitq; ++ struct mutex write_lock; /* Guards order of events/notifications. */ ++ struct rw_semaphore client_lock; /* Guards client list. */ ++ struct list_head client_list; ++ ++ struct delayed_work state_work; ++ struct { ++ struct ssam_bas_base_info base; ++ u8 device_mode; ++ u8 latch_status; ++ } state; ++ ++ struct delayed_work mode_work; ++ struct input_dev *mode_switch; ++ ++ struct ssam_event_notifier notif; ++}; ++ ++enum sdtx_client_state { ++ SDTX_CLIENT_EVENTS_ENABLED_BIT = BIT(0), ++}; ++ ++struct sdtx_client { ++ struct sdtx_device *ddev; ++ struct list_head node; ++ unsigned long flags; ++ ++ struct fasync_struct *fasync; ++ ++ struct mutex read_lock; /* Guards FIFO buffer read access. */ ++ DECLARE_KFIFO(buffer, u8, 512); ++}; ++ ++static void __sdtx_device_release(struct kref *kref) ++{ ++ struct sdtx_device *ddev = container_of(kref, struct sdtx_device, kref); ++ ++ mutex_destroy(&ddev->write_lock); ++ kfree(ddev); ++} ++ ++static struct sdtx_device *sdtx_device_get(struct sdtx_device *ddev) ++{ ++ if (ddev) ++ kref_get(&ddev->kref); ++ ++ return ddev; ++} ++ ++static void sdtx_device_put(struct sdtx_device *ddev) ++{ ++ if (ddev) ++ kref_put(&ddev->kref, __sdtx_device_release); ++} ++ ++ ++/* -- Firmware value translations. ------------------------------------------ */ ++ ++static u16 sdtx_translate_base_state(struct sdtx_device *ddev, u8 state) ++{ ++ switch (state) { ++ case SSAM_BAS_BASE_STATE_ATTACHED: ++ return SDTX_BASE_ATTACHED; ++ ++ case SSAM_BAS_BASE_STATE_DETACH_SUCCESS: ++ return SDTX_BASE_DETACHED; ++ ++ case SSAM_BAS_BASE_STATE_NOT_FEASIBLE: ++ return SDTX_DETACH_NOT_FEASIBLE; ++ ++ default: ++ dev_err(ddev->dev, "unknown base state: %#04x\n", state); ++ return SDTX_UNKNOWN(state); ++ } ++} ++ ++static u16 sdtx_translate_latch_status(struct sdtx_device *ddev, u8 status) ++{ ++ switch (status) { ++ case SSAM_BAS_LATCH_STATUS_CLOSED: ++ return SDTX_LATCH_CLOSED; ++ ++ case SSAM_BAS_LATCH_STATUS_OPENED: ++ return SDTX_LATCH_OPENED; ++ ++ case SSAM_BAS_LATCH_STATUS_FAILED_TO_OPEN: ++ return SDTX_ERR_FAILED_TO_OPEN; ++ ++ case SSAM_BAS_LATCH_STATUS_FAILED_TO_REMAIN_OPEN: ++ return SDTX_ERR_FAILED_TO_REMAIN_OPEN; ++ ++ case SSAM_BAS_LATCH_STATUS_FAILED_TO_CLOSE: ++ return SDTX_ERR_FAILED_TO_CLOSE; ++ ++ default: ++ dev_err(ddev->dev, "unknown latch status: %#04x\n", status); ++ return SDTX_UNKNOWN(status); ++ } ++} ++ ++static u16 sdtx_translate_cancel_reason(struct sdtx_device *ddev, u8 reason) ++{ ++ switch (reason) { ++ case SSAM_BAS_CANCEL_REASON_NOT_FEASIBLE: ++ return SDTX_DETACH_NOT_FEASIBLE; ++ ++ case SSAM_BAS_CANCEL_REASON_TIMEOUT: ++ return SDTX_DETACH_TIMEDOUT; ++ ++ case SSAM_BAS_CANCEL_REASON_FAILED_TO_OPEN: ++ return SDTX_ERR_FAILED_TO_OPEN; ++ ++ case SSAM_BAS_CANCEL_REASON_FAILED_TO_REMAIN_OPEN: ++ return SDTX_ERR_FAILED_TO_REMAIN_OPEN; ++ ++ case SSAM_BAS_CANCEL_REASON_FAILED_TO_CLOSE: ++ return SDTX_ERR_FAILED_TO_CLOSE; ++ ++ default: ++ dev_err(ddev->dev, "unknown cancel reason: %#04x\n", reason); ++ return SDTX_UNKNOWN(reason); ++ } ++} ++ ++ ++/* -- IOCTLs. --------------------------------------------------------------- */ ++ ++static int sdtx_ioctl_get_base_info(struct sdtx_device *ddev, ++ struct sdtx_base_info __user *buf) ++{ ++ struct ssam_bas_base_info raw; ++ struct sdtx_base_info info; ++ int status; ++ ++ lockdep_assert_held_read(&ddev->lock); ++ ++ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &raw); ++ if (status < 0) ++ return status; ++ ++ info.state = sdtx_translate_base_state(ddev, raw.state); ++ info.base_id = SDTX_BASE_TYPE_SSH(raw.base_id); ++ ++ if (copy_to_user(buf, &info, sizeof(info))) ++ return -EFAULT; ++ ++ return 0; ++} ++ ++static int sdtx_ioctl_get_device_mode(struct sdtx_device *ddev, u16 __user *buf) ++{ ++ u8 mode; ++ int status; ++ ++ lockdep_assert_held_read(&ddev->lock); ++ ++ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); ++ if (status < 0) ++ return status; ++ ++ return put_user(mode, buf); ++} ++ ++static int sdtx_ioctl_get_latch_status(struct sdtx_device *ddev, u16 __user *buf) ++{ ++ u8 latch; ++ int status; ++ ++ lockdep_assert_held_read(&ddev->lock); ++ ++ status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); ++ if (status < 0) ++ return status; ++ ++ return put_user(sdtx_translate_latch_status(ddev, latch), buf); ++} ++ ++static long __surface_dtx_ioctl(struct sdtx_client *client, unsigned int cmd, unsigned long arg) ++{ ++ struct sdtx_device *ddev = client->ddev; ++ ++ lockdep_assert_held_read(&ddev->lock); ++ ++ switch (cmd) { ++ case SDTX_IOCTL_EVENTS_ENABLE: ++ set_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); ++ return 0; ++ ++ case SDTX_IOCTL_EVENTS_DISABLE: ++ clear_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags); ++ return 0; ++ ++ case SDTX_IOCTL_LATCH_LOCK: ++ return ssam_retry(ssam_bas_latch_lock, ddev->ctrl); ++ ++ case SDTX_IOCTL_LATCH_UNLOCK: ++ return ssam_retry(ssam_bas_latch_unlock, ddev->ctrl); ++ ++ case SDTX_IOCTL_LATCH_REQUEST: ++ return ssam_retry(ssam_bas_latch_request, ddev->ctrl); ++ ++ case SDTX_IOCTL_LATCH_CONFIRM: ++ return ssam_retry(ssam_bas_latch_confirm, ddev->ctrl); ++ ++ case SDTX_IOCTL_LATCH_HEARTBEAT: ++ return ssam_retry(ssam_bas_latch_heartbeat, ddev->ctrl); ++ ++ case SDTX_IOCTL_LATCH_CANCEL: ++ return ssam_retry(ssam_bas_latch_cancel, ddev->ctrl); ++ ++ case SDTX_IOCTL_GET_BASE_INFO: ++ return sdtx_ioctl_get_base_info(ddev, (struct sdtx_base_info __user *)arg); ++ ++ case SDTX_IOCTL_GET_DEVICE_MODE: ++ return sdtx_ioctl_get_device_mode(ddev, (u16 __user *)arg); ++ ++ case SDTX_IOCTL_GET_LATCH_STATUS: ++ return sdtx_ioctl_get_latch_status(ddev, (u16 __user *)arg); ++ ++ default: ++ return -EINVAL; ++ } ++} ++ ++static long surface_dtx_ioctl(struct file *file, unsigned int cmd, unsigned long arg) ++{ ++ struct sdtx_client *client = file->private_data; ++ long status; ++ ++ if (down_read_killable(&client->ddev->lock)) ++ return -ERESTARTSYS; ++ ++ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) { ++ up_read(&client->ddev->lock); ++ return -ENODEV; ++ } ++ ++ status = __surface_dtx_ioctl(client, cmd, arg); ++ ++ up_read(&client->ddev->lock); ++ return status; ++} ++ ++ ++/* -- File operations. ------------------------------------------------------ */ ++ ++static int surface_dtx_open(struct inode *inode, struct file *file) ++{ ++ struct sdtx_device *ddev = container_of(file->private_data, struct sdtx_device, mdev); ++ struct sdtx_client *client; ++ ++ /* Initialize client. */ ++ client = kzalloc(sizeof(*client), GFP_KERNEL); ++ if (!client) ++ return -ENOMEM; ++ ++ client->ddev = sdtx_device_get(ddev); ++ ++ INIT_LIST_HEAD(&client->node); ++ ++ mutex_init(&client->read_lock); ++ INIT_KFIFO(client->buffer); ++ ++ file->private_data = client; ++ ++ /* Attach client. */ ++ down_write(&ddev->client_lock); ++ ++ /* ++ * Do not add a new client if the device has been shut down. Note that ++ * it's enough to hold the client_lock here as, during shutdown, we ++ * only acquire that lock and remove clients after marking the device ++ * as shut down. ++ */ ++ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { ++ up_write(&ddev->client_lock); ++ sdtx_device_put(client->ddev); ++ kfree(client); ++ return -ENODEV; ++ } ++ ++ list_add_tail(&client->node, &ddev->client_list); ++ up_write(&ddev->client_lock); ++ ++ stream_open(inode, file); ++ return 0; ++} ++ ++static int surface_dtx_release(struct inode *inode, struct file *file) ++{ ++ struct sdtx_client *client = file->private_data; ++ ++ /* Detach client. */ ++ down_write(&client->ddev->client_lock); ++ list_del(&client->node); ++ up_write(&client->ddev->client_lock); ++ ++ /* Free client. */ ++ sdtx_device_put(client->ddev); ++ mutex_destroy(&client->read_lock); ++ kfree(client); ++ ++ return 0; ++} ++ ++static ssize_t surface_dtx_read(struct file *file, char __user *buf, size_t count, loff_t *offs) ++{ ++ struct sdtx_client *client = file->private_data; ++ struct sdtx_device *ddev = client->ddev; ++ unsigned int copied; ++ int status = 0; ++ ++ if (down_read_killable(&ddev->lock)) ++ return -ERESTARTSYS; ++ ++ /* Make sure we're not shut down. */ ++ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { ++ up_read(&ddev->lock); ++ return -ENODEV; ++ } ++ ++ do { ++ /* Check availability, wait if necessary. */ ++ if (kfifo_is_empty(&client->buffer)) { ++ up_read(&ddev->lock); ++ ++ if (file->f_flags & O_NONBLOCK) ++ return -EAGAIN; ++ ++ status = wait_event_interruptible(ddev->waitq, ++ !kfifo_is_empty(&client->buffer) || ++ test_bit(SDTX_DEVICE_SHUTDOWN_BIT, ++ &ddev->flags)); ++ if (status < 0) ++ return status; ++ ++ if (down_read_killable(&client->ddev->lock)) ++ return -ERESTARTSYS; ++ ++ /* Need to check that we're not shut down again. */ ++ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags)) { ++ up_read(&ddev->lock); ++ return -ENODEV; ++ } ++ } ++ ++ /* Try to read from FIFO. */ ++ if (mutex_lock_interruptible(&client->read_lock)) { ++ up_read(&ddev->lock); ++ return -ERESTARTSYS; ++ } ++ ++ status = kfifo_to_user(&client->buffer, buf, count, &copied); ++ mutex_unlock(&client->read_lock); ++ ++ if (status < 0) { ++ up_read(&ddev->lock); ++ return status; ++ } ++ ++ /* We might not have gotten anything, check this here. */ ++ if (copied == 0 && (file->f_flags & O_NONBLOCK)) { ++ up_read(&ddev->lock); ++ return -EAGAIN; ++ } ++ } while (copied == 0); ++ ++ up_read(&ddev->lock); ++ return copied; ++} ++ ++static __poll_t surface_dtx_poll(struct file *file, struct poll_table_struct *pt) ++{ ++ struct sdtx_client *client = file->private_data; ++ __poll_t events = 0; ++ ++ if (down_read_killable(&client->ddev->lock)) ++ return -ERESTARTSYS; ++ ++ if (test_bit(SDTX_DEVICE_SHUTDOWN_BIT, &client->ddev->flags)) { ++ up_read(&client->ddev->lock); ++ return EPOLLHUP | EPOLLERR; ++ } ++ ++ poll_wait(file, &client->ddev->waitq, pt); ++ ++ if (!kfifo_is_empty(&client->buffer)) ++ events |= EPOLLIN | EPOLLRDNORM; ++ ++ up_read(&client->ddev->lock); ++ return events; ++} ++ ++static int surface_dtx_fasync(int fd, struct file *file, int on) ++{ ++ struct sdtx_client *client = file->private_data; ++ ++ return fasync_helper(fd, file, on, &client->fasync); ++} ++ ++static const struct file_operations surface_dtx_fops = { ++ .owner = THIS_MODULE, ++ .open = surface_dtx_open, ++ .release = surface_dtx_release, ++ .read = surface_dtx_read, ++ .poll = surface_dtx_poll, ++ .fasync = surface_dtx_fasync, ++ .unlocked_ioctl = surface_dtx_ioctl, ++ .compat_ioctl = surface_dtx_ioctl, ++ .llseek = no_llseek, ++}; ++ ++ ++/* -- Event handling/forwarding. -------------------------------------------- */ ++ ++/* ++ * The device operation mode is not immediately updated on the EC when the ++ * base has been connected, i.e. querying the device mode inside the ++ * connection event callback yields an outdated value. Thus, we can only ++ * determine the new tablet-mode switch and device mode values after some ++ * time. ++ * ++ * These delays have been chosen by experimenting. We first delay on connect ++ * events, then check and validate the device mode against the base state and ++ * if invalid delay again by the "recheck" delay. ++ */ ++#define SDTX_DEVICE_MODE_DELAY_CONNECT msecs_to_jiffies(100) ++#define SDTX_DEVICE_MODE_DELAY_RECHECK msecs_to_jiffies(100) ++ ++struct sdtx_status_event { ++ struct sdtx_event e; ++ __u16 v; ++} __packed; ++ ++struct sdtx_base_info_event { ++ struct sdtx_event e; ++ struct sdtx_base_info v; ++} __packed; ++ ++union sdtx_generic_event { ++ struct sdtx_event common; ++ struct sdtx_status_event status; ++ struct sdtx_base_info_event base; ++}; ++ ++static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay); ++ ++/* Must be executed with ddev->write_lock held. */ ++static void sdtx_push_event(struct sdtx_device *ddev, struct sdtx_event *evt) ++{ ++ const size_t len = sizeof(struct sdtx_event) + evt->length; ++ struct sdtx_client *client; ++ ++ lockdep_assert_held(&ddev->write_lock); ++ ++ down_read(&ddev->client_lock); ++ list_for_each_entry(client, &ddev->client_list, node) { ++ if (!test_bit(SDTX_CLIENT_EVENTS_ENABLED_BIT, &client->flags)) ++ continue; ++ ++ if (likely(kfifo_avail(&client->buffer) >= len)) ++ kfifo_in(&client->buffer, (const u8 *)evt, len); ++ else ++ dev_warn(ddev->dev, "event buffer overrun\n"); ++ ++ kill_fasync(&client->fasync, SIGIO, POLL_IN); ++ } ++ up_read(&ddev->client_lock); ++ ++ wake_up_interruptible(&ddev->waitq); ++} ++ ++static u32 sdtx_notifier(struct ssam_event_notifier *nf, const struct ssam_event *in) ++{ ++ struct sdtx_device *ddev = container_of(nf, struct sdtx_device, notif); ++ union sdtx_generic_event event; ++ size_t len; ++ ++ /* Validate event payload length. */ ++ switch (in->command_id) { ++ case SAM_EVENT_CID_DTX_CONNECTION: ++ len = 2 * sizeof(u8); ++ break; ++ ++ case SAM_EVENT_CID_DTX_REQUEST: ++ len = 0; ++ break; ++ ++ case SAM_EVENT_CID_DTX_CANCEL: ++ len = sizeof(u8); ++ break; ++ ++ case SAM_EVENT_CID_DTX_LATCH_STATUS: ++ len = sizeof(u8); ++ break; ++ ++ default: ++ return 0; ++ }; ++ ++ if (in->length != len) { ++ dev_err(ddev->dev, ++ "unexpected payload size for event %#04x: got %u, expected %zu\n", ++ in->command_id, in->length, len); ++ return 0; ++ } ++ ++ mutex_lock(&ddev->write_lock); ++ ++ /* Translate event. */ ++ switch (in->command_id) { ++ case SAM_EVENT_CID_DTX_CONNECTION: ++ clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); ++ ++ /* If state has not changed: do not send new event. */ ++ if (ddev->state.base.state == in->data[0] && ++ ddev->state.base.base_id == in->data[1]) ++ goto out; ++ ++ ddev->state.base.state = in->data[0]; ++ ddev->state.base.base_id = in->data[1]; ++ ++ event.base.e.length = sizeof(struct sdtx_base_info); ++ event.base.e.code = SDTX_EVENT_BASE_CONNECTION; ++ event.base.v.state = sdtx_translate_base_state(ddev, in->data[0]); ++ event.base.v.base_id = SDTX_BASE_TYPE_SSH(in->data[1]); ++ break; ++ ++ case SAM_EVENT_CID_DTX_REQUEST: ++ event.common.code = SDTX_EVENT_REQUEST; ++ event.common.length = 0; ++ break; ++ ++ case SAM_EVENT_CID_DTX_CANCEL: ++ event.status.e.length = sizeof(u16); ++ event.status.e.code = SDTX_EVENT_CANCEL; ++ event.status.v = sdtx_translate_cancel_reason(ddev, in->data[0]); ++ break; ++ ++ case SAM_EVENT_CID_DTX_LATCH_STATUS: ++ clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); ++ ++ /* If state has not changed: do not send new event. */ ++ if (ddev->state.latch_status == in->data[0]) ++ goto out; ++ ++ ddev->state.latch_status = in->data[0]; ++ ++ event.status.e.length = sizeof(u16); ++ event.status.e.code = SDTX_EVENT_LATCH_STATUS; ++ event.status.v = sdtx_translate_latch_status(ddev, in->data[0]); ++ break; ++ } ++ ++ sdtx_push_event(ddev, &event.common); ++ ++ /* Update device mode on base connection change. */ ++ if (in->command_id == SAM_EVENT_CID_DTX_CONNECTION) { ++ unsigned long delay; ++ ++ delay = in->data[0] ? SDTX_DEVICE_MODE_DELAY_CONNECT : 0; ++ sdtx_update_device_mode(ddev, delay); ++ } ++ ++out: ++ mutex_unlock(&ddev->write_lock); ++ return SSAM_NOTIF_HANDLED; ++} ++ ++ ++/* -- State update functions. ----------------------------------------------- */ ++ ++static bool sdtx_device_mode_invalid(u8 mode, u8 base_state) ++{ ++ return ((base_state == SSAM_BAS_BASE_STATE_ATTACHED) && ++ (mode == SDTX_DEVICE_MODE_TABLET)) || ++ ((base_state == SSAM_BAS_BASE_STATE_DETACH_SUCCESS) && ++ (mode != SDTX_DEVICE_MODE_TABLET)); ++} ++ ++static void sdtx_device_mode_workfn(struct work_struct *work) ++{ ++ struct sdtx_device *ddev = container_of(work, struct sdtx_device, mode_work.work); ++ struct sdtx_status_event event; ++ struct ssam_bas_base_info base; ++ int status, tablet; ++ u8 mode; ++ ++ /* Get operation mode. */ ++ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); ++ if (status) { ++ dev_err(ddev->dev, "failed to get device mode: %d\n", status); ++ return; ++ } ++ ++ /* Get base info. */ ++ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); ++ if (status) { ++ dev_err(ddev->dev, "failed to get base info: %d\n", status); ++ return; ++ } ++ ++ /* ++ * In some cases (specifically when attaching the base), the device ++ * mode isn't updated right away. Thus we check if the device mode ++ * makes sense for the given base state and try again later if it ++ * doesn't. ++ */ ++ if (sdtx_device_mode_invalid(mode, base.state)) { ++ dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); ++ sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); ++ return; ++ } ++ ++ mutex_lock(&ddev->write_lock); ++ clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); ++ ++ /* Avoid sending duplicate device-mode events. */ ++ if (ddev->state.device_mode == mode) { ++ mutex_unlock(&ddev->write_lock); ++ return; ++ } ++ ++ ddev->state.device_mode = mode; ++ ++ event.e.length = sizeof(u16); ++ event.e.code = SDTX_EVENT_DEVICE_MODE; ++ event.v = mode; ++ ++ sdtx_push_event(ddev, &event.e); ++ ++ /* Send SW_TABLET_MODE event. */ ++ tablet = mode != SDTX_DEVICE_MODE_LAPTOP; ++ input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); ++ input_sync(ddev->mode_switch); ++ ++ mutex_unlock(&ddev->write_lock); ++} ++ ++static void sdtx_update_device_mode(struct sdtx_device *ddev, unsigned long delay) ++{ ++ schedule_delayed_work(&ddev->mode_work, delay); ++} ++ ++/* Must be executed with ddev->write_lock held. */ ++static void __sdtx_device_state_update_base(struct sdtx_device *ddev, ++ struct ssam_bas_base_info info) ++{ ++ struct sdtx_base_info_event event; ++ ++ lockdep_assert_held(&ddev->write_lock); ++ ++ /* Prevent duplicate events. */ ++ if (ddev->state.base.state == info.state && ++ ddev->state.base.base_id == info.base_id) ++ return; ++ ++ ddev->state.base = info; ++ ++ event.e.length = sizeof(struct sdtx_base_info); ++ event.e.code = SDTX_EVENT_BASE_CONNECTION; ++ event.v.state = sdtx_translate_base_state(ddev, info.state); ++ event.v.base_id = SDTX_BASE_TYPE_SSH(info.base_id); ++ ++ sdtx_push_event(ddev, &event.e); ++} ++ ++/* Must be executed with ddev->write_lock held. */ ++static void __sdtx_device_state_update_mode(struct sdtx_device *ddev, u8 mode) ++{ ++ struct sdtx_status_event event; ++ int tablet; ++ ++ /* ++ * Note: This function must be called after updating the base state ++ * via __sdtx_device_state_update_base(), as we rely on the updated ++ * base state value in the validity check below. ++ */ ++ ++ lockdep_assert_held(&ddev->write_lock); ++ ++ if (sdtx_device_mode_invalid(mode, ddev->state.base.state)) { ++ dev_dbg(ddev->dev, "device mode is invalid, trying again\n"); ++ sdtx_update_device_mode(ddev, SDTX_DEVICE_MODE_DELAY_RECHECK); ++ return; ++ } ++ ++ /* Prevent duplicate events. */ ++ if (ddev->state.device_mode == mode) ++ return; ++ ++ ddev->state.device_mode = mode; ++ ++ /* Send event. */ ++ event.e.length = sizeof(u16); ++ event.e.code = SDTX_EVENT_DEVICE_MODE; ++ event.v = mode; ++ ++ sdtx_push_event(ddev, &event.e); ++ ++ /* Send SW_TABLET_MODE event. */ ++ tablet = mode != SDTX_DEVICE_MODE_LAPTOP; ++ input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet); ++ input_sync(ddev->mode_switch); ++} ++ ++/* Must be executed with ddev->write_lock held. */ ++static void __sdtx_device_state_update_latch(struct sdtx_device *ddev, u8 status) ++{ ++ struct sdtx_status_event event; ++ ++ lockdep_assert_held(&ddev->write_lock); ++ ++ /* Prevent duplicate events. */ ++ if (ddev->state.latch_status == status) ++ return; ++ ++ ddev->state.latch_status = status; ++ ++ event.e.length = sizeof(struct sdtx_base_info); ++ event.e.code = SDTX_EVENT_BASE_CONNECTION; ++ event.v = sdtx_translate_latch_status(ddev, status); ++ ++ sdtx_push_event(ddev, &event.e); ++} ++ ++static void sdtx_device_state_workfn(struct work_struct *work) ++{ ++ struct sdtx_device *ddev = container_of(work, struct sdtx_device, state_work.work); ++ struct ssam_bas_base_info base; ++ u8 mode, latch; ++ int status; ++ ++ /* Mark everything as dirty. */ ++ set_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags); ++ set_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags); ++ set_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags); ++ ++ /* ++ * Ensure that the state gets marked as dirty before continuing to ++ * query it. Necessary to ensure that clear_bit() calls in ++ * sdtx_notifier() and sdtx_device_mode_workfn() actually clear these ++ * bits if an event is received while updating the state here. ++ */ ++ smp_mb__after_atomic(); ++ ++ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &base); ++ if (status) { ++ dev_err(ddev->dev, "failed to get base state: %d\n", status); ++ return; ++ } ++ ++ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &mode); ++ if (status) { ++ dev_err(ddev->dev, "failed to get device mode: %d\n", status); ++ return; ++ } ++ ++ status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &latch); ++ if (status) { ++ dev_err(ddev->dev, "failed to get latch status: %d\n", status); ++ return; ++ } ++ ++ mutex_lock(&ddev->write_lock); ++ ++ /* ++ * If the respective dirty-bit has been cleared, an event has been ++ * received, updating this state. The queried state may thus be out of ++ * date. At this point, we can safely assume that the state provided ++ * by the event is either up to date, or we're about to receive ++ * another event updating it. ++ */ ++ ++ if (test_and_clear_bit(SDTX_DEVICE_DIRTY_BASE_BIT, &ddev->flags)) ++ __sdtx_device_state_update_base(ddev, base); ++ ++ if (test_and_clear_bit(SDTX_DEVICE_DIRTY_MODE_BIT, &ddev->flags)) ++ __sdtx_device_state_update_mode(ddev, mode); ++ ++ if (test_and_clear_bit(SDTX_DEVICE_DIRTY_LATCH_BIT, &ddev->flags)) ++ __sdtx_device_state_update_latch(ddev, latch); ++ ++ mutex_unlock(&ddev->write_lock); ++} ++ ++static void sdtx_update_device_state(struct sdtx_device *ddev, unsigned long delay) ++{ ++ schedule_delayed_work(&ddev->state_work, delay); ++} ++ ++ ++/* -- Common device initialization. ----------------------------------------- */ ++ ++static int sdtx_device_init(struct sdtx_device *ddev, struct device *dev, ++ struct ssam_controller *ctrl) ++{ ++ int status, tablet_mode; ++ ++ /* Basic initialization. */ ++ kref_init(&ddev->kref); ++ init_rwsem(&ddev->lock); ++ ddev->dev = dev; ++ ddev->ctrl = ctrl; ++ ++ ddev->mdev.minor = MISC_DYNAMIC_MINOR; ++ ddev->mdev.name = "surface_dtx"; ++ ddev->mdev.nodename = "surface/dtx"; ++ ddev->mdev.fops = &surface_dtx_fops; ++ ++ ddev->notif.base.priority = 1; ++ ddev->notif.base.fn = sdtx_notifier; ++ ddev->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; ++ ddev->notif.event.id.target_category = SSAM_SSH_TC_BAS; ++ ddev->notif.event.id.instance = 0; ++ ddev->notif.event.mask = SSAM_EVENT_MASK_NONE; ++ ddev->notif.event.flags = SSAM_EVENT_SEQUENCED; ++ ++ init_waitqueue_head(&ddev->waitq); ++ mutex_init(&ddev->write_lock); ++ init_rwsem(&ddev->client_lock); ++ INIT_LIST_HEAD(&ddev->client_list); ++ ++ INIT_DELAYED_WORK(&ddev->mode_work, sdtx_device_mode_workfn); ++ INIT_DELAYED_WORK(&ddev->state_work, sdtx_device_state_workfn); ++ ++ /* ++ * Get current device state. We want to guarantee that events are only ++ * sent when state actually changes. Thus we cannot use special ++ * "uninitialized" values, as that would cause problems when manually ++ * querying the state in surface_dtx_pm_complete(). I.e. we would not ++ * be able to detect state changes there if no change event has been ++ * received between driver initialization and first device suspension. ++ * ++ * Note that we also need to do this before registering the event ++ * notifier, as that may access the state values. ++ */ ++ status = ssam_retry(ssam_bas_get_base, ddev->ctrl, &ddev->state.base); ++ if (status) ++ return status; ++ ++ status = ssam_retry(ssam_bas_get_device_mode, ddev->ctrl, &ddev->state.device_mode); ++ if (status) ++ return status; ++ ++ status = ssam_retry(ssam_bas_get_latch_status, ddev->ctrl, &ddev->state.latch_status); ++ if (status) ++ return status; ++ ++ /* Set up tablet mode switch. */ ++ ddev->mode_switch = input_allocate_device(); ++ if (!ddev->mode_switch) ++ return -ENOMEM; ++ ++ ddev->mode_switch->name = "Microsoft Surface DTX Device Mode Switch"; ++ ddev->mode_switch->phys = "ssam/01:11:01:00:00/input0"; ++ ddev->mode_switch->id.bustype = BUS_HOST; ++ ddev->mode_switch->dev.parent = ddev->dev; ++ ++ tablet_mode = (ddev->state.device_mode != SDTX_DEVICE_MODE_LAPTOP); ++ input_set_capability(ddev->mode_switch, EV_SW, SW_TABLET_MODE); ++ input_report_switch(ddev->mode_switch, SW_TABLET_MODE, tablet_mode); ++ ++ status = input_register_device(ddev->mode_switch); ++ if (status) { ++ input_free_device(ddev->mode_switch); ++ return status; ++ } ++ ++ /* Set up event notifier. */ ++ status = ssam_notifier_register(ddev->ctrl, &ddev->notif); ++ if (status) ++ goto err_notif; ++ ++ /* Register miscdevice. */ ++ status = misc_register(&ddev->mdev); ++ if (status) ++ goto err_mdev; ++ ++ /* ++ * Update device state in case it has changed between getting the ++ * initial mode and registering the event notifier. ++ */ ++ sdtx_update_device_state(ddev, 0); ++ return 0; ++ ++err_notif: ++ ssam_notifier_unregister(ddev->ctrl, &ddev->notif); ++ cancel_delayed_work_sync(&ddev->mode_work); ++err_mdev: ++ input_unregister_device(ddev->mode_switch); ++ return status; ++} ++ ++static struct sdtx_device *sdtx_device_create(struct device *dev, struct ssam_controller *ctrl) ++{ ++ struct sdtx_device *ddev; ++ int status; ++ ++ ddev = kzalloc(sizeof(*ddev), GFP_KERNEL); ++ if (!ddev) ++ return ERR_PTR(-ENOMEM); ++ ++ status = sdtx_device_init(ddev, dev, ctrl); ++ if (status) { ++ sdtx_device_put(ddev); ++ return ERR_PTR(status); ++ } ++ ++ return ddev; ++} ++ ++static void sdtx_device_destroy(struct sdtx_device *ddev) ++{ ++ struct sdtx_client *client; ++ ++ /* ++ * Mark device as shut-down. Prevent new clients from being added and ++ * new operations from being executed. ++ */ ++ set_bit(SDTX_DEVICE_SHUTDOWN_BIT, &ddev->flags); ++ ++ /* Disable notifiers, prevent new events from arriving. */ ++ ssam_notifier_unregister(ddev->ctrl, &ddev->notif); ++ ++ /* Stop mode_work, prevent access to mode_switch. */ ++ cancel_delayed_work_sync(&ddev->mode_work); ++ ++ /* Stop state_work. */ ++ cancel_delayed_work_sync(&ddev->state_work); ++ ++ /* With mode_work canceled, we can unregister the mode_switch. */ ++ input_unregister_device(ddev->mode_switch); ++ ++ /* Wake up async clients. */ ++ down_write(&ddev->client_lock); ++ list_for_each_entry(client, &ddev->client_list, node) { ++ kill_fasync(&client->fasync, SIGIO, POLL_HUP); ++ } ++ up_write(&ddev->client_lock); ++ ++ /* Wake up blocking clients. */ ++ wake_up_interruptible(&ddev->waitq); ++ ++ /* ++ * Wait for clients to finish their current operation. After this, the ++ * controller and device references are guaranteed to be no longer in ++ * use. ++ */ ++ down_write(&ddev->lock); ++ ddev->dev = NULL; ++ ddev->ctrl = NULL; ++ up_write(&ddev->lock); ++ ++ /* Finally remove the misc-device. */ ++ misc_deregister(&ddev->mdev); ++ ++ /* ++ * We're now guaranteed that sdtx_device_open() won't be called any ++ * more, so we can now drop out reference. ++ */ ++ sdtx_device_put(ddev); ++} ++ ++ ++/* -- PM ops. --------------------------------------------------------------- */ ++ ++#ifdef CONFIG_PM_SLEEP ++ ++static void surface_dtx_pm_complete(struct device *dev) ++{ ++ struct sdtx_device *ddev = dev_get_drvdata(dev); ++ ++ /* ++ * Normally, the EC will store events while suspended (i.e. in ++ * display-off state) and release them when resumed (i.e. transitioned ++ * to display-on state). During hibernation, however, the EC will be ++ * shut down and does not store events. Furthermore, events might be ++ * dropped during prolonged suspension (it is currently unknown how ++ * big this event buffer is and how it behaves on overruns). ++ * ++ * To prevent any problems, we update the device state here. We do ++ * this delayed to ensure that any events sent by the EC directly ++ * after resuming will be handled first. The delay below has been ++ * chosen (experimentally), so that there should be ample time for ++ * these events to be handled, before we check and, if necessary, ++ * update the state. ++ */ ++ sdtx_update_device_state(ddev, msecs_to_jiffies(1000)); ++} ++ ++static const struct dev_pm_ops surface_dtx_pm_ops = { ++ .complete = surface_dtx_pm_complete, ++}; ++ ++#else /* CONFIG_PM_SLEEP */ ++ ++static const struct dev_pm_ops surface_dtx_pm_ops = {}; ++ ++#endif /* CONFIG_PM_SLEEP */ ++ ++ ++/* -- Platform driver. ------------------------------------------------------ */ ++ ++static int surface_dtx_platform_probe(struct platform_device *pdev) ++{ ++ struct ssam_controller *ctrl; ++ struct sdtx_device *ddev; ++ ++ /* Link to EC. */ ++ ctrl = ssam_client_bind(&pdev->dev); ++ if (IS_ERR(ctrl)) ++ return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); ++ ++ ddev = sdtx_device_create(&pdev->dev, ctrl); ++ if (IS_ERR(ddev)) ++ return PTR_ERR(ddev); ++ ++ platform_set_drvdata(pdev, ddev); ++ return 0; ++} ++ ++static int surface_dtx_platform_remove(struct platform_device *pdev) ++{ ++ sdtx_device_destroy(platform_get_drvdata(pdev)); ++ return 0; ++} ++ ++static const struct acpi_device_id surface_dtx_acpi_match[] = { ++ { "MSHW0133", 0 }, ++ { }, ++}; ++MODULE_DEVICE_TABLE(acpi, surface_dtx_acpi_match); ++ ++static struct platform_driver surface_dtx_platform_driver = { ++ .probe = surface_dtx_platform_probe, ++ .remove = surface_dtx_platform_remove, ++ .driver = { ++ .name = "surface_dtx_pltf", ++ .acpi_match_table = surface_dtx_acpi_match, ++ .pm = &surface_dtx_pm_ops, ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++ ++ ++/* -- SSAM device driver. --------------------------------------------------- */ ++ ++#ifdef CONFIG_SURFACE_AGGREGATOR_BUS ++ ++static int surface_dtx_ssam_probe(struct ssam_device *sdev) ++{ ++ struct sdtx_device *ddev; ++ ++ ddev = sdtx_device_create(&sdev->dev, sdev->ctrl); ++ if (IS_ERR(ddev)) ++ return PTR_ERR(ddev); ++ ++ ssam_device_set_drvdata(sdev, ddev); ++ return 0; ++} ++ ++static void surface_dtx_ssam_remove(struct ssam_device *sdev) ++{ ++ sdtx_device_destroy(ssam_device_get_drvdata(sdev)); ++} ++ ++static const struct ssam_device_id surface_dtx_ssam_match[] = { ++ { SSAM_SDEV(BAS, 0x01, 0x00, 0x00) }, ++ { }, ++}; ++MODULE_DEVICE_TABLE(ssam, surface_dtx_ssam_match); ++ ++static struct ssam_device_driver surface_dtx_ssam_driver = { ++ .probe = surface_dtx_ssam_probe, ++ .remove = surface_dtx_ssam_remove, ++ .match_table = surface_dtx_ssam_match, ++ .driver = { ++ .name = "surface_dtx", ++ .pm = &surface_dtx_pm_ops, ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++ ++static int ssam_dtx_driver_register(void) ++{ ++ return ssam_device_driver_register(&surface_dtx_ssam_driver); ++} ++ ++static void ssam_dtx_driver_unregister(void) ++{ ++ ssam_device_driver_unregister(&surface_dtx_ssam_driver); ++} ++ ++#else /* CONFIG_SURFACE_AGGREGATOR_BUS */ ++ ++static int ssam_dtx_driver_register(void) ++{ ++ return 0; ++} ++ ++static void ssam_dtx_driver_unregister(void) ++{ ++} ++ ++#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */ ++ ++ ++/* -- Module setup. --------------------------------------------------------- */ ++ ++static int __init surface_dtx_init(void) ++{ ++ int status; ++ ++ status = ssam_dtx_driver_register(); ++ if (status) ++ return status; ++ ++ status = platform_driver_register(&surface_dtx_platform_driver); ++ if (status) ++ ssam_dtx_driver_unregister(); ++ ++ return status; ++} ++module_init(surface_dtx_init); ++ ++static void __exit surface_dtx_exit(void) ++{ ++ platform_driver_unregister(&surface_dtx_platform_driver); ++ ssam_dtx_driver_unregister(); ++} ++module_exit(surface_dtx_exit); ++ ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("Detachment-system driver for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/platform/x86/surface_perfmode.c b/drivers/platform/x86/surface_perfmode.c +new file mode 100644 +index 000000000000..3b92a43f8606 +--- /dev/null ++++ b/drivers/platform/x86/surface_perfmode.c +@@ -0,0 +1,122 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++/* ++ * Surface performance-mode driver. ++ * ++ * Provides a user-space interface for the performance mode control provided ++ * by the Surface System Aggregator Module (SSAM), influencing cooling ++ * behavior of the device and potentially managing power limits. ++ * ++ * Copyright (C) 2019-2021 Maximilian Luz ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++enum sam_perf_mode { ++ SAM_PERF_MODE_NORMAL = 1, ++ SAM_PERF_MODE_BATTERY = 2, ++ SAM_PERF_MODE_PERF1 = 3, ++ SAM_PERF_MODE_PERF2 = 4, ++ ++ __SAM_PERF_MODE__MIN = 1, ++ __SAM_PERF_MODE__MAX = 4, ++}; ++ ++struct ssam_perf_info { ++ __le32 mode; ++ __le16 unknown1; ++ __le16 unknown2; ++} __packed; ++ ++static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_tmp_perf_mode_get, struct ssam_perf_info, { ++ .target_category = SSAM_SSH_TC_TMP, ++ .command_id = 0x02, ++}); ++ ++static SSAM_DEFINE_SYNC_REQUEST_CL_W(__ssam_tmp_perf_mode_set, __le32, { ++ .target_category = SSAM_SSH_TC_TMP, ++ .command_id = 0x03, ++}); ++ ++static int ssam_tmp_perf_mode_set(struct ssam_device *sdev, u32 mode) ++{ ++ __le32 mode_le = cpu_to_le32(mode); ++ ++ if (mode < __SAM_PERF_MODE__MIN || mode > __SAM_PERF_MODE__MAX) ++ return -EINVAL; ++ ++ return ssam_retry(__ssam_tmp_perf_mode_set, sdev, &mode_le); ++} ++ ++static ssize_t perf_mode_show(struct device *dev, struct device_attribute *attr, ++ char *data) ++{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ struct ssam_perf_info info; ++ int status; ++ ++ status = ssam_retry(ssam_tmp_perf_mode_get, sdev, &info); ++ if (status) { ++ dev_err(dev, "failed to get current performance mode: %d\n", ++ status); ++ return -EIO; ++ } ++ ++ return sprintf(data, "%d\n", le32_to_cpu(info.mode)); ++} ++ ++static ssize_t perf_mode_store(struct device *dev, struct device_attribute *attr, ++ const char *data, size_t count) ++{ ++ struct ssam_device *sdev = to_ssam_device(dev); ++ int perf_mode; ++ int status; ++ ++ status = kstrtoint(data, 0, &perf_mode); ++ if (status < 0) ++ return status; ++ ++ status = ssam_tmp_perf_mode_set(sdev, perf_mode); ++ if (status < 0) ++ return status; ++ ++ return count; ++} ++ ++static const DEVICE_ATTR_RW(perf_mode); ++ ++static int surface_sam_sid_perfmode_probe(struct ssam_device *sdev) ++{ ++ return sysfs_create_file(&sdev->dev.kobj, &dev_attr_perf_mode.attr); ++} ++ ++static void surface_sam_sid_perfmode_remove(struct ssam_device *sdev) ++{ ++ sysfs_remove_file(&sdev->dev.kobj, &dev_attr_perf_mode.attr); ++} ++ ++static const struct ssam_device_id ssam_perfmode_match[] = { ++ { SSAM_SDEV(TMP, 0x01, 0x00, 0x01) }, ++ { }, ++}; ++MODULE_DEVICE_TABLE(ssam, ssam_perfmode_match); ++ ++static struct ssam_device_driver surface_sam_sid_perfmode = { ++ .probe = surface_sam_sid_perfmode_probe, ++ .remove = surface_sam_sid_perfmode_remove, ++ .match_table = ssam_perfmode_match, ++ .driver = { ++ .name = "surface_performance_mode", ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++module_ssam_device_driver(surface_sam_sid_perfmode); ++ ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("Performance mode interface for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig +index d6fdc10c29f0..205a7c781093 100644 +--- a/drivers/power/supply/Kconfig ++++ b/drivers/power/supply/Kconfig +@@ -710,4 +710,36 @@ config CHARGER_WILCO + information can be found in + Documentation/ABI/testing/sysfs-class-power-wilco + ++config BATTERY_SURFACE ++ tristate "Battery driver for 7th-generation Microsoft Surface devices" ++ depends on SURFACE_AGGREGATOR_REGISTRY ++ help ++ Driver for battery devices connected via/managed by the Surface System ++ Aggregator Module (SSAM). ++ ++ This driver provides battery-information and -status support for ++ Surface devices where said data is not exposed via the standard ACPI ++ devices. On those models (7th-generation), battery-information is ++ instead handled directly via SSAM client devices and this driver. ++ ++ Say M or Y here to include battery status support for 7th-generation ++ Microsoft Surface devices, i.e. Surface Pro 7, Surface Laptop 3, ++ Surface Book 3, and Surface Laptop Go. ++ ++config CHARGER_SURFACE ++ tristate "AC driver for 7th-generation Microsoft Surface devices" ++ depends on SURFACE_AGGREGATOR_REGISTRY ++ help ++ Driver for AC devices connected via/managed by the Surface System ++ Aggregator Module (SSAM). ++ ++ This driver provides AC-information and -status support for Surface ++ devices where said data is not exposed via the standard ACPI devices. ++ On those models (7th-generation), AC-information is instead handled ++ directly via a SSAM client device and this driver. ++ ++ Say M or Y here to include AC status support for 7th-generation ++ Microsoft Surface devices, i.e. Surface Pro 7, Surface Laptop 3, ++ Surface Book 3, and Surface Laptop Go. ++ + endif # POWER_SUPPLY +diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile +index 6c7da920ea83..84cf59901c73 100644 +--- a/drivers/power/supply/Makefile ++++ b/drivers/power/supply/Makefile +@@ -92,3 +92,5 @@ obj-$(CONFIG_FUEL_GAUGE_SC27XX) += sc27xx_fuel_gauge.o + obj-$(CONFIG_CHARGER_UCS1002) += ucs1002_power.o + obj-$(CONFIG_CHARGER_BD70528) += bd70528-charger.o + obj-$(CONFIG_CHARGER_WILCO) += wilco-charger.o ++obj-$(CONFIG_BATTERY_SURFACE) += surface_battery.o ++obj-$(CONFIG_CHARGER_SURFACE) += surface_charger.o +diff --git a/drivers/power/supply/surface_battery.c b/drivers/power/supply/surface_battery.c +new file mode 100644 +index 000000000000..013d6dc94769 +--- /dev/null ++++ b/drivers/power/supply/surface_battery.c +@@ -0,0 +1,901 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++/* ++ * Battery driver for 7th-generation Microsoft Surface devices via Surface ++ * System Aggregator Module (SSAM). ++ * ++ * Copyright (C) 2019-2021 Maximilian Luz ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++ ++/* -- SAM interface. -------------------------------------------------------- */ ++ ++enum sam_event_cid_bat { ++ SAM_EVENT_CID_BAT_BIX = 0x15, ++ SAM_EVENT_CID_BAT_BST = 0x16, ++ SAM_EVENT_CID_BAT_ADP = 0x17, ++ SAM_EVENT_CID_BAT_PROT = 0x18, ++ SAM_EVENT_CID_BAT_DPTF = 0x53, ++}; ++ ++enum sam_battery_sta { ++ SAM_BATTERY_STA_OK = 0x0f, ++ SAM_BATTERY_STA_PRESENT = 0x10, ++}; ++ ++enum sam_battery_state { ++ SAM_BATTERY_STATE_DISCHARGING = BIT(0), ++ SAM_BATTERY_STATE_CHARGING = BIT(1), ++ SAM_BATTERY_STATE_CRITICAL = BIT(2), ++}; ++ ++enum sam_battery_power_unit { ++ SAM_BATTERY_POWER_UNIT_mW = 0, ++ SAM_BATTERY_POWER_UNIT_mA = 1, ++}; ++ ++/* Equivalent to data returned in ACPI _BIX method, revision 0. */ ++struct spwr_bix { ++ u8 revision; ++ __le32 power_unit; ++ __le32 design_cap; ++ __le32 last_full_charge_cap; ++ __le32 technology; ++ __le32 design_voltage; ++ __le32 design_cap_warn; ++ __le32 design_cap_low; ++ __le32 cycle_count; ++ __le32 measurement_accuracy; ++ __le32 max_sampling_time; ++ __le32 min_sampling_time; ++ __le32 max_avg_interval; ++ __le32 min_avg_interval; ++ __le32 bat_cap_granularity_1; ++ __le32 bat_cap_granularity_2; ++ __u8 model[21]; ++ __u8 serial[11]; ++ __u8 type[5]; ++ __u8 oem_info[21]; ++} __packed; ++ ++static_assert(sizeof(struct spwr_bix) == 119); ++ ++/* Equivalent to data returned in ACPI _BST method. */ ++struct spwr_bst { ++ __le32 state; ++ __le32 present_rate; ++ __le32 remaining_cap; ++ __le32 present_voltage; ++} __packed; ++ ++static_assert(sizeof(struct spwr_bst) == 16); ++ ++#define SPWR_BIX_REVISION 0 ++#define SPWR_BATTERY_VALUE_UNKNOWN 0xffffffff ++ ++/* Get battery status (_STA) */ ++static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_sta, __le32, { ++ .target_category = SSAM_SSH_TC_BAT, ++ .command_id = 0x01, ++}); ++ ++/* Get battery static information (_BIX). */ ++static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_bix, struct spwr_bix, { ++ .target_category = SSAM_SSH_TC_BAT, ++ .command_id = 0x02, ++}); ++ ++/* Get battery dynamic information (_BST). */ ++static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_bst, struct spwr_bst, { ++ .target_category = SSAM_SSH_TC_BAT, ++ .command_id = 0x03, ++}); ++ ++/* Set battery trip point (_BTP). */ ++static SSAM_DEFINE_SYNC_REQUEST_CL_W(ssam_bat_set_btp, __le32, { ++ .target_category = SSAM_SSH_TC_BAT, ++ .command_id = 0x04, ++}); ++ ++ ++/* -- Device structures. ---------------------------------------------------- */ ++ ++struct spwr_psy_properties { ++ const char *name; ++ struct ssam_event_registry registry; ++}; ++ ++struct spwr_battery_device { ++ struct ssam_device *sdev; ++ ++ char name[32]; ++ struct power_supply *psy; ++ struct power_supply_desc psy_desc; ++ ++ struct delayed_work update_work; ++ ++ struct ssam_event_notifier notif; ++ ++ struct mutex lock; /* Guards access to state data below. */ ++ unsigned long timestamp; ++ ++ __le32 sta; ++ struct spwr_bix bix; ++ struct spwr_bst bst; ++ u32 alarm; ++}; ++ ++ ++/* -- Module parameters. ---------------------------------------------------- */ ++ ++static unsigned int cache_time = 1000; ++module_param(cache_time, uint, 0644); ++MODULE_PARM_DESC(cache_time, "battery state caching time in milliseconds [default: 1000]"); ++ ++ ++/* -- State management. ----------------------------------------------------- */ ++ ++/* ++ * Delay for battery update quirk. See spwr_battery_recheck_adapter() below ++ * for more details. ++ */ ++#define SPWR_AC_BAT_UPDATE_DELAY msecs_to_jiffies(5000) ++ ++static bool spwr_battery_present(struct spwr_battery_device *bat) ++{ ++ lockdep_assert_held(&bat->lock); ++ ++ return le32_to_cpu(bat->sta) & SAM_BATTERY_STA_PRESENT; ++} ++ ++static int spwr_battery_load_sta(struct spwr_battery_device *bat) ++{ ++ lockdep_assert_held(&bat->lock); ++ ++ return ssam_retry(ssam_bat_get_sta, bat->sdev, &bat->sta); ++} ++ ++static int spwr_battery_load_bix(struct spwr_battery_device *bat) ++{ ++ int status; ++ ++ lockdep_assert_held(&bat->lock); ++ ++ if (!spwr_battery_present(bat)) ++ return 0; ++ ++ status = ssam_retry(ssam_bat_get_bix, bat->sdev, &bat->bix); ++ ++ /* Enforce NULL terminated strings in case anything goes wrong... */ ++ bat->bix.model[ARRAY_SIZE(bat->bix.model) - 1] = 0; ++ bat->bix.serial[ARRAY_SIZE(bat->bix.serial) - 1] = 0; ++ bat->bix.type[ARRAY_SIZE(bat->bix.type) - 1] = 0; ++ bat->bix.oem_info[ARRAY_SIZE(bat->bix.oem_info) - 1] = 0; ++ ++ return status; ++} ++ ++static int spwr_battery_load_bst(struct spwr_battery_device *bat) ++{ ++ lockdep_assert_held(&bat->lock); ++ ++ if (!spwr_battery_present(bat)) ++ return 0; ++ ++ return ssam_retry(ssam_bat_get_bst, bat->sdev, &bat->bst); ++} ++ ++static int spwr_battery_set_alarm_unlocked(struct spwr_battery_device *bat, u32 value) ++{ ++ __le32 value_le = cpu_to_le32(value); ++ ++ lockdep_assert_held(&bat->lock); ++ ++ bat->alarm = value; ++ return ssam_retry(ssam_bat_set_btp, bat->sdev, &value_le); ++} ++ ++static int spwr_battery_update_bst_unlocked(struct spwr_battery_device *bat, bool cached) ++{ ++ unsigned long cache_deadline = bat->timestamp + msecs_to_jiffies(cache_time); ++ int status; ++ ++ lockdep_assert_held(&bat->lock); ++ ++ if (cached && bat->timestamp && time_is_after_jiffies(cache_deadline)) ++ return 0; ++ ++ status = spwr_battery_load_sta(bat); ++ if (status) ++ return status; ++ ++ status = spwr_battery_load_bst(bat); ++ if (status) ++ return status; ++ ++ bat->timestamp = jiffies; ++ return 0; ++} ++ ++static int spwr_battery_update_bst(struct spwr_battery_device *bat, bool cached) ++{ ++ int status; ++ ++ mutex_lock(&bat->lock); ++ status = spwr_battery_update_bst_unlocked(bat, cached); ++ mutex_unlock(&bat->lock); ++ ++ return status; ++} ++ ++static int spwr_battery_update_bix_unlocked(struct spwr_battery_device *bat) ++{ ++ int status; ++ ++ lockdep_assert_held(&bat->lock); ++ ++ status = spwr_battery_load_sta(bat); ++ if (status) ++ return status; ++ ++ status = spwr_battery_load_bix(bat); ++ if (status) ++ return status; ++ ++ status = spwr_battery_load_bst(bat); ++ if (status) ++ return status; ++ ++ if (bat->bix.revision != SPWR_BIX_REVISION) ++ dev_warn(&bat->sdev->dev, "unsupported battery revision: %u\n", bat->bix.revision); ++ ++ bat->timestamp = jiffies; ++ return 0; ++} ++ ++static u32 sprw_battery_get_full_cap_safe(struct spwr_battery_device *bat) ++{ ++ u32 full_cap = get_unaligned_le32(&bat->bix.last_full_charge_cap); ++ ++ lockdep_assert_held(&bat->lock); ++ ++ if (full_cap == 0 || full_cap == SPWR_BATTERY_VALUE_UNKNOWN) ++ full_cap = get_unaligned_le32(&bat->bix.design_cap); ++ ++ return full_cap; ++} ++ ++static bool spwr_battery_is_full(struct spwr_battery_device *bat) ++{ ++ u32 state = get_unaligned_le32(&bat->bst.state); ++ u32 full_cap = sprw_battery_get_full_cap_safe(bat); ++ u32 remaining_cap = get_unaligned_le32(&bat->bst.remaining_cap); ++ ++ lockdep_assert_held(&bat->lock); ++ ++ return full_cap != SPWR_BATTERY_VALUE_UNKNOWN && full_cap != 0 && ++ remaining_cap != SPWR_BATTERY_VALUE_UNKNOWN && ++ remaining_cap >= full_cap && ++ state == 0; ++} ++ ++static int spwr_battery_recheck_full(struct spwr_battery_device *bat) ++{ ++ bool present; ++ u32 unit; ++ int status; ++ ++ mutex_lock(&bat->lock); ++ unit = get_unaligned_le32(&bat->bix.power_unit); ++ present = spwr_battery_present(bat); ++ ++ status = spwr_battery_update_bix_unlocked(bat); ++ if (status) ++ goto out; ++ ++ /* If battery has been attached, (re-)initialize alarm. */ ++ if (!present && spwr_battery_present(bat)) { ++ u32 cap_warn = get_unaligned_le32(&bat->bix.design_cap_warn); ++ ++ status = spwr_battery_set_alarm_unlocked(bat, cap_warn); ++ if (status) ++ goto out; ++ } ++ ++ /* ++ * Warn if the unit has changed. This is something we genuinely don't ++ * expect to happen, so make this a big warning. If it does, we'll ++ * need to add support for it. ++ */ ++ WARN_ON(unit != get_unaligned_le32(&bat->bix.power_unit)); ++ ++out: ++ mutex_unlock(&bat->lock); ++ ++ if (!status) ++ power_supply_changed(bat->psy); ++ ++ return status; ++} ++ ++static int spwr_battery_recheck_status(struct spwr_battery_device *bat) ++{ ++ int status; ++ ++ status = spwr_battery_update_bst(bat, false); ++ if (!status) ++ power_supply_changed(bat->psy); ++ ++ return status; ++} ++ ++static int spwr_battery_recheck_adapter(struct spwr_battery_device *bat) ++{ ++ /* ++ * Handle battery update quirk: When the battery is fully charged (or ++ * charged up to the limit imposed by the UEFI battery limit) and the ++ * adapter is plugged in or removed, the EC does not send a separate ++ * event for the state (charging/discharging) change. Furthermore it ++ * may take some time until the state is updated on the battery. ++ * Schedule an update to solve this. ++ */ ++ ++ schedule_delayed_work(&bat->update_work, SPWR_AC_BAT_UPDATE_DELAY); ++ return 0; ++} ++ ++static u32 spwr_notify_bat(struct ssam_event_notifier *nf, const struct ssam_event *event) ++{ ++ struct spwr_battery_device *bat = container_of(nf, struct spwr_battery_device, notif); ++ int status; ++ ++ dev_dbg(&bat->sdev->dev, "power event (cid = %#04x, iid = %#04x, tid = %#04x)\n", ++ event->command_id, event->instance_id, event->target_id); ++ ++ /* Handled here, needs to be handled for all targets/instances. */ ++ if (event->command_id == SAM_EVENT_CID_BAT_ADP) { ++ status = spwr_battery_recheck_adapter(bat); ++ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; ++ } ++ ++ if (bat->sdev->uid.target != event->target_id) ++ return 0; ++ ++ if (bat->sdev->uid.instance != event->instance_id) ++ return 0; ++ ++ switch (event->command_id) { ++ case SAM_EVENT_CID_BAT_BIX: ++ status = spwr_battery_recheck_full(bat); ++ break; ++ ++ case SAM_EVENT_CID_BAT_BST: ++ status = spwr_battery_recheck_status(bat); ++ break; ++ ++ case SAM_EVENT_CID_BAT_PROT: ++ /* ++ * TODO: Implement support for battery protection status change ++ * event. ++ */ ++ status = 0; ++ break; ++ ++ case SAM_EVENT_CID_BAT_DPTF: ++ /* ++ * TODO: Implement support for DPTF event. ++ */ ++ status = 0; ++ break; ++ ++ default: ++ return 0; ++ } ++ ++ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; ++} ++ ++static void spwr_battery_update_bst_workfn(struct work_struct *work) ++{ ++ struct delayed_work *dwork = to_delayed_work(work); ++ struct spwr_battery_device *bat; ++ int status; ++ ++ bat = container_of(dwork, struct spwr_battery_device, update_work); ++ ++ status = spwr_battery_update_bst(bat, false); ++ if (!status) ++ power_supply_changed(bat->psy); ++ ++ if (status) ++ dev_err(&bat->sdev->dev, "failed to update battery state: %d\n", status); ++} ++ ++ ++/* -- Properties. ----------------------------------------------------------- */ ++ ++static enum power_supply_property spwr_battery_props_chg[] = { ++ POWER_SUPPLY_PROP_STATUS, ++ POWER_SUPPLY_PROP_PRESENT, ++ POWER_SUPPLY_PROP_TECHNOLOGY, ++ POWER_SUPPLY_PROP_CYCLE_COUNT, ++ POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, ++ POWER_SUPPLY_PROP_VOLTAGE_NOW, ++ POWER_SUPPLY_PROP_CURRENT_NOW, ++ POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, ++ POWER_SUPPLY_PROP_CHARGE_FULL, ++ POWER_SUPPLY_PROP_CHARGE_NOW, ++ POWER_SUPPLY_PROP_CAPACITY, ++ POWER_SUPPLY_PROP_CAPACITY_LEVEL, ++ POWER_SUPPLY_PROP_MODEL_NAME, ++ POWER_SUPPLY_PROP_MANUFACTURER, ++ POWER_SUPPLY_PROP_SERIAL_NUMBER, ++}; ++ ++static enum power_supply_property spwr_battery_props_eng[] = { ++ POWER_SUPPLY_PROP_STATUS, ++ POWER_SUPPLY_PROP_PRESENT, ++ POWER_SUPPLY_PROP_TECHNOLOGY, ++ POWER_SUPPLY_PROP_CYCLE_COUNT, ++ POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, ++ POWER_SUPPLY_PROP_VOLTAGE_NOW, ++ POWER_SUPPLY_PROP_POWER_NOW, ++ POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, ++ POWER_SUPPLY_PROP_ENERGY_FULL, ++ POWER_SUPPLY_PROP_ENERGY_NOW, ++ POWER_SUPPLY_PROP_CAPACITY, ++ POWER_SUPPLY_PROP_CAPACITY_LEVEL, ++ POWER_SUPPLY_PROP_MODEL_NAME, ++ POWER_SUPPLY_PROP_MANUFACTURER, ++ POWER_SUPPLY_PROP_SERIAL_NUMBER, ++}; ++ ++static int spwr_battery_prop_status(struct spwr_battery_device *bat) ++{ ++ u32 state = get_unaligned_le32(&bat->bst.state); ++ u32 present_rate = get_unaligned_le32(&bat->bst.present_rate); ++ ++ lockdep_assert_held(&bat->lock); ++ ++ if (state & SAM_BATTERY_STATE_DISCHARGING) ++ return POWER_SUPPLY_STATUS_DISCHARGING; ++ ++ if (state & SAM_BATTERY_STATE_CHARGING) ++ return POWER_SUPPLY_STATUS_CHARGING; ++ ++ if (spwr_battery_is_full(bat)) ++ return POWER_SUPPLY_STATUS_FULL; ++ ++ if (present_rate == 0) ++ return POWER_SUPPLY_STATUS_NOT_CHARGING; ++ ++ return POWER_SUPPLY_STATUS_UNKNOWN; ++} ++ ++static int spwr_battery_prop_technology(struct spwr_battery_device *bat) ++{ ++ lockdep_assert_held(&bat->lock); ++ ++ if (!strcasecmp("NiCd", bat->bix.type)) ++ return POWER_SUPPLY_TECHNOLOGY_NiCd; ++ ++ if (!strcasecmp("NiMH", bat->bix.type)) ++ return POWER_SUPPLY_TECHNOLOGY_NiMH; ++ ++ if (!strcasecmp("LION", bat->bix.type)) ++ return POWER_SUPPLY_TECHNOLOGY_LION; ++ ++ if (!strncasecmp("LI-ION", bat->bix.type, 6)) ++ return POWER_SUPPLY_TECHNOLOGY_LION; ++ ++ if (!strcasecmp("LiP", bat->bix.type)) ++ return POWER_SUPPLY_TECHNOLOGY_LIPO; ++ ++ return POWER_SUPPLY_TECHNOLOGY_UNKNOWN; ++} ++ ++static int spwr_battery_prop_capacity(struct spwr_battery_device *bat) ++{ ++ u32 full_cap = sprw_battery_get_full_cap_safe(bat); ++ u32 remaining_cap = get_unaligned_le32(&bat->bst.remaining_cap); ++ ++ lockdep_assert_held(&bat->lock); ++ ++ if (full_cap == 0 || full_cap == SPWR_BATTERY_VALUE_UNKNOWN) ++ return -ENODEV; ++ ++ if (remaining_cap == SPWR_BATTERY_VALUE_UNKNOWN) ++ return -ENODEV; ++ ++ return remaining_cap * 100 / full_cap; ++} ++ ++static int spwr_battery_prop_capacity_level(struct spwr_battery_device *bat) ++{ ++ u32 state = get_unaligned_le32(&bat->bst.state); ++ u32 remaining_cap = get_unaligned_le32(&bat->bst.remaining_cap); ++ ++ lockdep_assert_held(&bat->lock); ++ ++ if (state & SAM_BATTERY_STATE_CRITICAL) ++ return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; ++ ++ if (spwr_battery_is_full(bat)) ++ return POWER_SUPPLY_CAPACITY_LEVEL_FULL; ++ ++ if (remaining_cap <= bat->alarm) ++ return POWER_SUPPLY_CAPACITY_LEVEL_LOW; ++ ++ return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; ++} ++ ++static int spwr_battery_get_property(struct power_supply *psy, enum power_supply_property psp, ++ union power_supply_propval *val) ++{ ++ struct spwr_battery_device *bat = power_supply_get_drvdata(psy); ++ u32 value; ++ int status; ++ ++ mutex_lock(&bat->lock); ++ ++ status = spwr_battery_update_bst_unlocked(bat, true); ++ if (status) ++ goto out; ++ ++ /* Abort if battery is not present. */ ++ if (!spwr_battery_present(bat) && psp != POWER_SUPPLY_PROP_PRESENT) { ++ status = -ENODEV; ++ goto out; ++ } ++ ++ switch (psp) { ++ case POWER_SUPPLY_PROP_STATUS: ++ val->intval = spwr_battery_prop_status(bat); ++ break; ++ ++ case POWER_SUPPLY_PROP_PRESENT: ++ val->intval = spwr_battery_present(bat); ++ break; ++ ++ case POWER_SUPPLY_PROP_TECHNOLOGY: ++ val->intval = spwr_battery_prop_technology(bat); ++ break; ++ ++ case POWER_SUPPLY_PROP_CYCLE_COUNT: ++ value = get_unaligned_le32(&bat->bix.cycle_count); ++ if (value != SPWR_BATTERY_VALUE_UNKNOWN) ++ val->intval = value; ++ else ++ status = -ENODEV; ++ break; ++ ++ case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: ++ value = get_unaligned_le32(&bat->bix.design_voltage); ++ if (value != SPWR_BATTERY_VALUE_UNKNOWN) ++ val->intval = value * 1000; ++ else ++ status = -ENODEV; ++ break; ++ ++ case POWER_SUPPLY_PROP_VOLTAGE_NOW: ++ value = get_unaligned_le32(&bat->bst.present_voltage); ++ if (value != SPWR_BATTERY_VALUE_UNKNOWN) ++ val->intval = value * 1000; ++ else ++ status = -ENODEV; ++ break; ++ ++ case POWER_SUPPLY_PROP_CURRENT_NOW: ++ case POWER_SUPPLY_PROP_POWER_NOW: ++ value = get_unaligned_le32(&bat->bst.present_rate); ++ if (value != SPWR_BATTERY_VALUE_UNKNOWN) ++ val->intval = value * 1000; ++ else ++ status = -ENODEV; ++ break; ++ ++ case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: ++ case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: ++ value = get_unaligned_le32(&bat->bix.design_cap); ++ if (value != SPWR_BATTERY_VALUE_UNKNOWN) ++ val->intval = value * 1000; ++ else ++ status = -ENODEV; ++ break; ++ ++ case POWER_SUPPLY_PROP_CHARGE_FULL: ++ case POWER_SUPPLY_PROP_ENERGY_FULL: ++ value = get_unaligned_le32(&bat->bix.last_full_charge_cap); ++ if (value != SPWR_BATTERY_VALUE_UNKNOWN) ++ val->intval = value * 1000; ++ else ++ status = -ENODEV; ++ break; ++ ++ case POWER_SUPPLY_PROP_CHARGE_NOW: ++ case POWER_SUPPLY_PROP_ENERGY_NOW: ++ value = get_unaligned_le32(&bat->bst.remaining_cap); ++ if (value != SPWR_BATTERY_VALUE_UNKNOWN) ++ val->intval = value * 1000; ++ else ++ status = -ENODEV; ++ break; ++ ++ case POWER_SUPPLY_PROP_CAPACITY: ++ val->intval = spwr_battery_prop_capacity(bat); ++ break; ++ ++ case POWER_SUPPLY_PROP_CAPACITY_LEVEL: ++ val->intval = spwr_battery_prop_capacity_level(bat); ++ break; ++ ++ case POWER_SUPPLY_PROP_MODEL_NAME: ++ val->strval = bat->bix.model; ++ break; ++ ++ case POWER_SUPPLY_PROP_MANUFACTURER: ++ val->strval = bat->bix.oem_info; ++ break; ++ ++ case POWER_SUPPLY_PROP_SERIAL_NUMBER: ++ val->strval = bat->bix.serial; ++ break; ++ ++ default: ++ status = -EINVAL; ++ break; ++ } ++ ++out: ++ mutex_unlock(&bat->lock); ++ return status; ++} ++ ++ ++/* -- Alarm attribute. ------------------------------------------------------ */ ++ ++static ssize_t spwr_battery_alarm_show(struct device *dev, struct device_attribute *attr, char *buf) ++{ ++ struct power_supply *psy = dev_get_drvdata(dev); ++ struct spwr_battery_device *bat = power_supply_get_drvdata(psy); ++ int status; ++ ++ mutex_lock(&bat->lock); ++ status = scnprintf(buf, PAGE_SIZE, "%d\n", bat->alarm * 1000); ++ mutex_unlock(&bat->lock); ++ ++ return status; ++} ++ ++static ssize_t spwr_battery_alarm_store(struct device *dev, struct device_attribute *attr, ++ const char *buf, size_t count) ++{ ++ struct power_supply *psy = dev_get_drvdata(dev); ++ struct spwr_battery_device *bat = power_supply_get_drvdata(psy); ++ unsigned long value; ++ int status; ++ ++ status = kstrtoul(buf, 0, &value); ++ if (status) ++ return status; ++ ++ mutex_lock(&bat->lock); ++ ++ if (!spwr_battery_present(bat)) { ++ mutex_unlock(&bat->lock); ++ return -ENODEV; ++ } ++ ++ status = spwr_battery_set_alarm_unlocked(bat, value / 1000); ++ if (status) { ++ mutex_unlock(&bat->lock); ++ return status; ++ } ++ ++ mutex_unlock(&bat->lock); ++ return count; ++} ++ ++static const struct device_attribute alarm_attr = { ++ .attr = {.name = "alarm", .mode = 0644}, ++ .show = spwr_battery_alarm_show, ++ .store = spwr_battery_alarm_store, ++}; ++ ++ ++/* -- Device setup. --------------------------------------------------------- */ ++ ++static void spwr_battery_init(struct spwr_battery_device *bat, struct ssam_device *sdev, ++ struct ssam_event_registry registry, const char *name) ++{ ++ mutex_init(&bat->lock); ++ strncpy(bat->name, name, ARRAY_SIZE(bat->name) - 1); ++ ++ bat->sdev = sdev; ++ ++ bat->notif.base.priority = 1; ++ bat->notif.base.fn = spwr_notify_bat; ++ bat->notif.event.reg = registry; ++ bat->notif.event.id.target_category = sdev->uid.category; ++ bat->notif.event.id.instance = 0; ++ bat->notif.event.mask = SSAM_EVENT_MASK_NONE; ++ bat->notif.event.flags = SSAM_EVENT_SEQUENCED; ++ ++ bat->psy_desc.name = bat->name; ++ bat->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY; ++ bat->psy_desc.get_property = spwr_battery_get_property; ++ ++ INIT_DELAYED_WORK(&bat->update_work, spwr_battery_update_bst_workfn); ++} ++ ++static void spwr_battery_destroy(struct spwr_battery_device *bat) ++{ ++ mutex_destroy(&bat->lock); ++} ++ ++static int spwr_battery_register(struct spwr_battery_device *bat) ++{ ++ struct power_supply_config psy_cfg = {}; ++ __le32 sta; ++ int status; ++ ++ /* Make sure the device is there and functioning properly. */ ++ status = ssam_retry(ssam_bat_get_sta, bat->sdev, &sta); ++ if (status) ++ return status; ++ ++ if ((le32_to_cpu(sta) & SAM_BATTERY_STA_OK) != SAM_BATTERY_STA_OK) ++ return -ENODEV; ++ ++ /* Satisfy lockdep although we are in an exclusive context here. */ ++ mutex_lock(&bat->lock); ++ ++ status = spwr_battery_update_bix_unlocked(bat); ++ if (status) { ++ mutex_unlock(&bat->lock); ++ return status; ++ } ++ ++ if (spwr_battery_present(bat)) { ++ u32 cap_warn = get_unaligned_le32(&bat->bix.design_cap_warn); ++ ++ status = spwr_battery_set_alarm_unlocked(bat, cap_warn); ++ if (status) { ++ mutex_unlock(&bat->lock); ++ return status; ++ } ++ } ++ ++ mutex_unlock(&bat->lock); ++ ++ switch (get_unaligned_le32(&bat->bix.power_unit)) { ++ case SAM_BATTERY_POWER_UNIT_mW: ++ bat->psy_desc.properties = spwr_battery_props_eng; ++ bat->psy_desc.num_properties = ARRAY_SIZE(spwr_battery_props_eng); ++ break; ++ ++ case SAM_BATTERY_POWER_UNIT_mA: ++ bat->psy_desc.properties = spwr_battery_props_chg; ++ bat->psy_desc.num_properties = ARRAY_SIZE(spwr_battery_props_chg); ++ break; ++ ++ default: ++ dev_err(&bat->sdev->dev, "unsupported battery power unit: %u\n", ++ get_unaligned_le32(&bat->bix.power_unit)); ++ return -EINVAL; ++ } ++ ++ psy_cfg.drv_data = bat; ++ bat->psy = power_supply_register(&bat->sdev->dev, &bat->psy_desc, &psy_cfg); ++ if (IS_ERR(bat->psy)) ++ return PTR_ERR(bat->psy); ++ ++ status = ssam_notifier_register(bat->sdev->ctrl, &bat->notif); ++ if (status) ++ goto err_notif; ++ ++ status = device_create_file(&bat->psy->dev, &alarm_attr); ++ if (status) ++ goto err_file; ++ ++ return 0; ++ ++err_file: ++ ssam_notifier_unregister(bat->sdev->ctrl, &bat->notif); ++err_notif: ++ power_supply_unregister(bat->psy); ++ return status; ++} ++ ++static void spwr_battery_unregister(struct spwr_battery_device *bat) ++{ ++ ssam_notifier_unregister(bat->sdev->ctrl, &bat->notif); ++ cancel_delayed_work_sync(&bat->update_work); ++ device_remove_file(&bat->psy->dev, &alarm_attr); ++ power_supply_unregister(bat->psy); ++} ++ ++ ++/* -- Driver setup. --------------------------------------------------------- */ ++ ++static int __maybe_unused surface_battery_resume(struct device *dev) ++{ ++ return spwr_battery_recheck_full(dev_get_drvdata(dev)); ++} ++SIMPLE_DEV_PM_OPS(surface_battery_pm_ops, NULL, surface_battery_resume); ++ ++static int surface_battery_probe(struct ssam_device *sdev) ++{ ++ const struct spwr_psy_properties *p; ++ struct spwr_battery_device *bat; ++ int status; ++ ++ p = ssam_device_get_match_data(sdev); ++ if (!p) ++ return -ENODEV; ++ ++ bat = devm_kzalloc(&sdev->dev, sizeof(*bat), GFP_KERNEL); ++ if (!bat) ++ return -ENOMEM; ++ ++ spwr_battery_init(bat, sdev, p->registry, p->name); ++ ssam_device_set_drvdata(sdev, bat); ++ ++ status = spwr_battery_register(bat); ++ if (status) ++ spwr_battery_destroy(bat); ++ ++ return status; ++} ++ ++static void surface_battery_remove(struct ssam_device *sdev) ++{ ++ struct spwr_battery_device *bat = ssam_device_get_drvdata(sdev); ++ ++ spwr_battery_unregister(bat); ++ spwr_battery_destroy(bat); ++} ++ ++static const struct spwr_psy_properties spwr_psy_props_bat1 = { ++ .name = "BAT1", ++ .registry = SSAM_EVENT_REGISTRY_SAM, ++}; ++ ++static const struct spwr_psy_properties spwr_psy_props_bat2_sb3 = { ++ .name = "BAT2", ++ .registry = SSAM_EVENT_REGISTRY_KIP, ++}; ++ ++static const struct ssam_device_id surface_battery_match[] = { ++ { SSAM_SDEV(BAT, 0x01, 0x01, 0x00), (unsigned long)&spwr_psy_props_bat1 }, ++ { SSAM_SDEV(BAT, 0x02, 0x01, 0x00), (unsigned long)&spwr_psy_props_bat2_sb3 }, ++ { }, ++}; ++MODULE_DEVICE_TABLE(ssam, surface_battery_match); ++ ++static struct ssam_device_driver surface_battery_driver = { ++ .probe = surface_battery_probe, ++ .remove = surface_battery_remove, ++ .match_table = surface_battery_match, ++ .driver = { ++ .name = "surface_battery", ++ .pm = &surface_battery_pm_ops, ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++module_ssam_device_driver(surface_battery_driver); ++ ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("Battery driver for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); +diff --git a/drivers/power/supply/surface_charger.c b/drivers/power/supply/surface_charger.c +new file mode 100644 +index 000000000000..982f9b9ef6f5 +--- /dev/null ++++ b/drivers/power/supply/surface_charger.c +@@ -0,0 +1,296 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++/* ++ * AC driver for 7th-generation Microsoft Surface devices via Surface System ++ * Aggregator Module (SSAM). ++ * ++ * Copyright (C) 2019-2021 Maximilian Luz ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++ ++/* -- SAM interface. -------------------------------------------------------- */ ++ ++enum sam_event_cid_bat { ++ SAM_EVENT_CID_BAT_ADP = 0x17, ++}; ++ ++enum sam_battery_sta { ++ SAM_BATTERY_STA_OK = 0x0f, ++ SAM_BATTERY_STA_PRESENT = 0x10, ++}; ++ ++/* Get battery status (_STA). */ ++static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_sta, __le32, { ++ .target_category = SSAM_SSH_TC_BAT, ++ .command_id = 0x01, ++}); ++ ++/* Get platform power source for battery (_PSR / DPTF PSRC). */ ++static SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_psrc, __le32, { ++ .target_category = SSAM_SSH_TC_BAT, ++ .command_id = 0x0d, ++}); ++ ++ ++/* -- Device structures. ---------------------------------------------------- */ ++ ++struct spwr_psy_properties { ++ const char *name; ++ struct ssam_event_registry registry; ++}; ++ ++struct spwr_ac_device { ++ struct ssam_device *sdev; ++ ++ char name[32]; ++ struct power_supply *psy; ++ struct power_supply_desc psy_desc; ++ ++ struct ssam_event_notifier notif; ++ ++ struct mutex lock; /* Guards access to state below. */ ++ ++ __le32 state; ++}; ++ ++ ++/* -- State management. ----------------------------------------------------- */ ++ ++static int spwr_ac_update_unlocked(struct spwr_ac_device *ac) ++{ ++ u32 old = ac->state; ++ int status; ++ ++ lockdep_assert_held(&ac->lock); ++ ++ status = ssam_retry(ssam_bat_get_psrc, ac->sdev, &ac->state); ++ if (status < 0) ++ return status; ++ ++ return old != ac->state; ++} ++ ++static int spwr_ac_update(struct spwr_ac_device *ac) ++{ ++ int status; ++ ++ mutex_lock(&ac->lock); ++ status = spwr_ac_update_unlocked(ac); ++ mutex_unlock(&ac->lock); ++ ++ return status; ++} ++ ++static int spwr_ac_recheck(struct spwr_ac_device *ac) ++{ ++ int status; ++ ++ status = spwr_ac_update(ac); ++ if (status > 0) ++ power_supply_changed(ac->psy); ++ ++ return status >= 0 ? 0 : status; ++} ++ ++static u32 spwr_notify_ac(struct ssam_event_notifier *nf, const struct ssam_event *event) ++{ ++ struct spwr_ac_device *ac; ++ int status; ++ ++ ac = container_of(nf, struct spwr_ac_device, notif); ++ ++ dev_dbg(&ac->sdev->dev, "power event (cid = %#04x, iid = %#04x, tid = %#04x)\n", ++ event->command_id, event->instance_id, event->target_id); ++ ++ /* ++ * Allow events of all targets/instances here. Global adapter status ++ * seems to be handled via target=1 and instance=1, but events are ++ * reported on all targets/instances in use. ++ * ++ * While it should be enough to just listen on 1/1, listen everywhere to ++ * make sure we don't miss anything. ++ */ ++ ++ switch (event->command_id) { ++ case SAM_EVENT_CID_BAT_ADP: ++ status = spwr_ac_recheck(ac); ++ return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED; ++ ++ default: ++ return 0; ++ } ++} ++ ++ ++/* -- Properties. ----------------------------------------------------------- */ ++ ++static enum power_supply_property spwr_ac_props[] = { ++ POWER_SUPPLY_PROP_ONLINE, ++}; ++ ++static int spwr_ac_get_property(struct power_supply *psy, enum power_supply_property psp, ++ union power_supply_propval *val) ++{ ++ struct spwr_ac_device *ac = power_supply_get_drvdata(psy); ++ int status; ++ ++ mutex_lock(&ac->lock); ++ ++ status = spwr_ac_update_unlocked(ac); ++ if (status) ++ goto out; ++ ++ switch (psp) { ++ case POWER_SUPPLY_PROP_ONLINE: ++ val->intval = !!le32_to_cpu(ac->state); ++ break; ++ ++ default: ++ status = -EINVAL; ++ goto out; ++ } ++ ++out: ++ mutex_unlock(&ac->lock); ++ return status; ++} ++ ++ ++/* -- Device setup. --------------------------------------------------------- */ ++ ++static void spwr_ac_init(struct spwr_ac_device *ac, struct ssam_device *sdev, ++ struct ssam_event_registry registry, const char *name) ++{ ++ mutex_init(&ac->lock); ++ strncpy(ac->name, name, ARRAY_SIZE(ac->name) - 1); ++ ++ ac->sdev = sdev; ++ ++ ac->notif.base.priority = 1; ++ ac->notif.base.fn = spwr_notify_ac; ++ ac->notif.event.reg = registry; ++ ac->notif.event.id.target_category = sdev->uid.category; ++ ac->notif.event.id.instance = 0; ++ ac->notif.event.mask = SSAM_EVENT_MASK_NONE; ++ ac->notif.event.flags = SSAM_EVENT_SEQUENCED; ++ ++ ac->psy_desc.name = ac->name; ++ ac->psy_desc.type = POWER_SUPPLY_TYPE_MAINS; ++ ac->psy_desc.properties = spwr_ac_props; ++ ac->psy_desc.num_properties = ARRAY_SIZE(spwr_ac_props); ++ ac->psy_desc.get_property = spwr_ac_get_property; ++} ++ ++static void spwr_ac_destroy(struct spwr_ac_device *ac) ++{ ++ mutex_destroy(&ac->lock); ++} ++ ++static int spwr_ac_register(struct spwr_ac_device *ac) ++{ ++ struct power_supply_config psy_cfg = {}; ++ __le32 sta; ++ int status; ++ ++ /* Make sure the device is there and functioning properly. */ ++ status = ssam_retry(ssam_bat_get_sta, ac->sdev, &sta); ++ if (status) ++ return status; ++ ++ if ((le32_to_cpu(sta) & SAM_BATTERY_STA_OK) != SAM_BATTERY_STA_OK) ++ return -ENODEV; ++ ++ psy_cfg.drv_data = ac; ++ ac->psy = power_supply_register(&ac->sdev->dev, &ac->psy_desc, &psy_cfg); ++ if (IS_ERR(ac->psy)) ++ return PTR_ERR(ac->psy); ++ ++ status = ssam_notifier_register(ac->sdev->ctrl, &ac->notif); ++ if (status) ++ power_supply_unregister(ac->psy); ++ ++ return status; ++} ++ ++static int spwr_ac_unregister(struct spwr_ac_device *ac) ++{ ++ ssam_notifier_unregister(ac->sdev->ctrl, &ac->notif); ++ power_supply_unregister(ac->psy); ++ return 0; ++} ++ ++ ++/* -- Driver setup. --------------------------------------------------------- */ ++ ++static int __maybe_unused surface_ac_resume(struct device *dev) ++{ ++ return spwr_ac_recheck(dev_get_drvdata(dev)); ++} ++SIMPLE_DEV_PM_OPS(surface_ac_pm_ops, NULL, surface_ac_resume); ++ ++static int surface_ac_probe(struct ssam_device *sdev) ++{ ++ const struct spwr_psy_properties *p; ++ struct spwr_ac_device *ac; ++ int status; ++ ++ p = ssam_device_get_match_data(sdev); ++ if (!p) ++ return -ENODEV; ++ ++ ac = devm_kzalloc(&sdev->dev, sizeof(*ac), GFP_KERNEL); ++ if (!ac) ++ return -ENOMEM; ++ ++ spwr_ac_init(ac, sdev, p->registry, p->name); ++ ssam_device_set_drvdata(sdev, ac); ++ ++ status = spwr_ac_register(ac); ++ if (status) ++ spwr_ac_destroy(ac); ++ ++ return status; ++} ++ ++static void surface_ac_remove(struct ssam_device *sdev) ++{ ++ struct spwr_ac_device *ac = ssam_device_get_drvdata(sdev); ++ ++ spwr_ac_unregister(ac); ++ spwr_ac_destroy(ac); ++} ++ ++static const struct spwr_psy_properties spwr_psy_props_adp1 = { ++ .name = "ADP1", ++ .registry = SSAM_EVENT_REGISTRY_SAM, ++}; ++ ++static const struct ssam_device_id surface_ac_match[] = { ++ { SSAM_SDEV(BAT, 0x01, 0x01, 0x01), (unsigned long)&spwr_psy_props_adp1 }, ++ { }, ++}; ++MODULE_DEVICE_TABLE(ssam, surface_ac_match); ++ ++static struct ssam_device_driver surface_ac_driver = { ++ .probe = surface_ac_probe, ++ .remove = surface_ac_remove, ++ .match_table = surface_ac_match, ++ .driver = { ++ .name = "surface_ac", ++ .pm = &surface_ac_pm_ops, ++ .probe_type = PROBE_PREFER_ASYNCHRONOUS, ++ }, ++}; ++module_ssam_device_driver(surface_ac_driver); ++ ++MODULE_AUTHOR("Maximilian Luz "); ++MODULE_DESCRIPTION("AC driver for Surface System Aggregator Module"); ++MODULE_LICENSE("GPL"); diff --git a/include/linux/mod_devicetable.h b/include/linux/mod_devicetable.h index 33c0eb774f3e..3e2fe1c6631c 100644 --- a/include/linux/mod_devicetable.h @@ -19960,5 +20112,5 @@ index bc06f7631200..c86de633d7ea 100644 ADD(alias, "f", match_flags & SSAM_MATCH_FUNCTION, function); -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0008-surface-hotplug.patch b/patches/5.4/0008-surface-hotplug.patch index ab1978083..1f37c3dc7 100644 --- a/patches/5.4/0008-surface-hotplug.patch +++ b/patches/5.4/0008-surface-hotplug.patch @@ -1,4 +1,4 @@ -From d3f60896ae2a2fb6c6b610e230916d2a81b24e2f Mon Sep 17 00:00:00 2001 +From d60ac44982cb1b4c8555a30e411ffec54056e0dc Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Mon, 9 Nov 2020 14:23:00 +0100 Subject: [PATCH] PCI: Run platform power transition on initial D0 entry @@ -58,9 +58,9 @@ index 89dece8a4132..fd34c8743cbc 100644 bridge = pci_upstream_bridge(dev); if (bridge) -- -2.30.0 +2.30.1 -From 32bc35a7ad400888222f5cf083d080e646f5e9ef Mon Sep 17 00:00:00 2001 +From 79034c754cd06ff12003b5ba38ef24706feb2b7c Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Sat, 31 Oct 2020 20:46:33 +0100 Subject: [PATCH] PCI: Add sysfs attribute for PCI device power state @@ -131,9 +131,9 @@ index e401f040f157..418927872ae6 100644 &dev_attr_vendor.attr, &dev_attr_device.attr, -- -2.30.0 +2.30.1 -From 6ebd60f882ad789a2dc4b8c7d681b16bb3e227f1 Mon Sep 17 00:00:00 2001 +From 14b245813cd398b61cf0507f7fdd6b54acdb5530 Mon Sep 17 00:00:00 2001 From: Maximilian Luz Date: Mon, 14 Dec 2020 20:50:59 +0100 Subject: [PATCH] platform/x86: Add Surface Hotplug driver @@ -146,40 +146,47 @@ appropriate signal to the PCIe hot-plug driver core. Signed-off-by: Maximilian Luz Patchset: surface-hotplug --- - drivers/platform/x86/Kconfig | 12 ++ + drivers/platform/x86/Kconfig | 19 ++ drivers/platform/x86/Makefile | 1 + - drivers/platform/x86/surface_hotplug.c | 267 +++++++++++++++++++++++++ - 3 files changed, 280 insertions(+) + drivers/platform/x86/surface_hotplug.c | 282 +++++++++++++++++++++++++ + 3 files changed, 302 insertions(+) create mode 100644 drivers/platform/x86/surface_hotplug.c diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig -index f06a0bf92890..3353aa9a9617 100644 +index b5a103d0ccd2..f0c17b65bb5b 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig -@@ -756,6 +756,18 @@ config ACPI_WMI +@@ -756,6 +756,25 @@ config ACPI_WMI It is safe to enable this driver even if your DSDT doesn't define any ACPI-WMI devices. +config SURFACE_HOTPLUG -+ tristate "Surface Hot-Plug System Driver" -+ depends on ACPI -+ default m ++ tristate "Surface Hot-Plug Driver" ++ depends on GPIOLIB + help -+ Driver for the Surface discrete GPU (dGPU) hot-plug system. ++ Driver for out-of-band hot-plug event signaling on Microsoft Surface ++ devices with hot-pluggable PCIe cards. + -+ This driver provides support for out-of-band hot-plug event signaling -+ on Surface Book 2 and 3 devices. This out-of-band signaling is -+ required to notify the kernel of any hot-plug events when the dGPU is -+ powered off, i.e. in D3cold. ++ This driver is used on Surface Book (2 and 3) devices with a ++ hot-pluggable discrete GPU (dGPU). When not in use, the dGPU on those ++ devices can enter D3cold, which prevents in-band (standard) PCIe ++ hot-plug signaling. Thus, without this driver, detaching the base ++ containing the dGPU will not correctly update the state of the ++ corresponding PCIe device if it is in D3cold. This driver adds support ++ for out-of-band hot-plug notifications, ensuring that the device state ++ is properly updated even when the device in question is in D3cold. ++ ++ Select M or Y here, if you want to (fully) support hot-plugging of ++ dGPU devices on the Surface Book 2 and/or 3 during D3cold. + config WMI_BMOF tristate "WMI embedded Binary MOF driver" depends on ACPI_WMI diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile -index a1e70973257c..7a6b5ce38cc0 100644 +index 328830619b21..1bdad1722521 100644 --- a/drivers/platform/x86/Makefile +++ b/drivers/platform/x86/Makefile -@@ -87,6 +87,7 @@ obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o +@@ -93,6 +93,7 @@ obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o obj-$(CONFIG_SURFACE_3_BUTTON) += surface3_button.o obj-$(CONFIG_SURFACE_3_POWER_OPREGION) += surface3_power.o obj-$(CONFIG_SURFACE_GPE) += surface_gpe.o @@ -189,13 +196,13 @@ index a1e70973257c..7a6b5ce38cc0 100644 obj-$(CONFIG_INTEL_BXTWC_PMIC_TMU) += intel_bxtwc_tmu.o diff --git a/drivers/platform/x86/surface_hotplug.c b/drivers/platform/x86/surface_hotplug.c new file mode 100644 -index 000000000000..572fba30cd77 +index 000000000000..cfcc15cfbacb --- /dev/null +++ b/drivers/platform/x86/surface_hotplug.c -@@ -0,0 +1,267 @@ +@@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* -+ * Surface Book (gen. 2 and later) hot-plug driver. ++ * Surface Book (2 and later) hot-plug driver. + * + * Surface Book devices (can) have a hot-pluggable discrete GPU (dGPU). This + * driver is responsible for out-of-band hot-plug event signaling on these @@ -205,7 +212,7 @@ index 000000000000..572fba30cd77 + * Event signaling is handled via ACPI, which will generate the appropriate + * device-check notifications to be picked up by the PCIe hot-plug driver. + * -+ * Copyright (C) 2019-2020 Maximilian Luz ++ * Copyright (C) 2019-2021 Maximilian Luz + */ + +#include @@ -235,8 +242,7 @@ index 000000000000..572fba30cd77 + +/* 5515a847-ed55-4b27-8352-cd320e10360a */ +static const guid_t shps_dsm_guid = -+ GUID_INIT(0x5515a847, 0xed55, 0x4b27, 0x83, 0x52, 0xcd, -+ 0x32, 0x0e, 0x10, 0x36, 0x0a); ++ GUID_INIT(0x5515a847, 0xed55, 0x4b27, 0x83, 0x52, 0xcd, 0x32, 0x0e, 0x10, 0x36, 0x0a); + +#define SHPS_DSM_REVISION 1 + @@ -249,11 +255,10 @@ index 000000000000..572fba30cd77 +}; + +enum shps_irq_type { -+ /* NOTE: Must be in order of DSM function */ ++ /* NOTE: Must be in order of enum shps_dsm_fn above. */ + SHPS_IRQ_TYPE_BASE_PRESENCE = 0, + SHPS_IRQ_TYPE_DEVICE_POWER = 1, + SHPS_IRQ_TYPE_DEVICE_PRESENCE = 2, -+ + SHPS_NUM_IRQS, +}; + @@ -264,15 +269,19 @@ index 000000000000..572fba30cd77 +}; + +struct shps_device { -+ struct mutex lock[SHPS_NUM_IRQS]; ++ struct mutex lock[SHPS_NUM_IRQS]; /* Protects update in shps_dsm_notify_irq() */ + struct gpio_desc *gpio[SHPS_NUM_IRQS]; + unsigned int irq[SHPS_NUM_IRQS]; +}; + +#define SHPS_IRQ_NOT_PRESENT ((unsigned int)-1) + -+static void shps_dsm_notify_irq(struct platform_device *pdev, -+ enum shps_irq_type type) ++static enum shps_dsm_fn shps_dsm_fn_for_irq(enum shps_irq_type type) ++{ ++ return SHPS_DSM_FN_IRQ_BASE_PRESENCE + type; ++} ++ ++static void shps_dsm_notify_irq(struct platform_device *pdev, enum shps_irq_type type) +{ + struct shps_device *sdev = platform_get_drvdata(pdev); + acpi_handle handle = ACPI_HANDLE(&pdev->dev); @@ -285,42 +294,37 @@ index 000000000000..572fba30cd77 + value = gpiod_get_value_cansleep(sdev->gpio[type]); + if (value < 0) { + mutex_unlock(&sdev->lock[type]); -+ dev_err(&pdev->dev, "failed to get gpio: %d (irq=%d)\n", -+ type, value); ++ dev_err(&pdev->dev, "failed to get gpio: %d (irq=%d)\n", type, value); + return; + } + -+ dev_dbg(&pdev->dev, "IRQ notification via DSM (irq=%d, value=%d)\n", -+ type, value); ++ dev_dbg(&pdev->dev, "IRQ notification via DSM (irq=%d, value=%d)\n", type, value); + + param.type = ACPI_TYPE_INTEGER; + param.integer.value = value; + + result = acpi_evaluate_dsm(handle, &shps_dsm_guid, SHPS_DSM_REVISION, -+ SHPS_DSM_FN_IRQ_BASE_PRESENCE + type, ¶m); ++ shps_dsm_fn_for_irq(type), ¶m); + + if (!result) { -+ mutex_unlock(&sdev->lock[type]); -+ dev_err(&pdev->dev, -+ "IRQ notification via DSM failed (irq=%d, gpio=%d)\n", ++ dev_err(&pdev->dev, "IRQ notification via DSM failed (irq=%d, gpio=%d)\n", + type, value); -+ return; -+ } + -+ if (result->type != ACPI_TYPE_BUFFER) { ++ } else if (result->type != ACPI_TYPE_BUFFER) { + dev_err(&pdev->dev, + "IRQ notification via DSM failed: unexpected result type (irq=%d, gpio=%d)\n", + type, value); -+ } + -+ if (result->buffer.length != 1 || result->buffer.pointer[0] != 0) { ++ } else if (result->buffer.length != 1 || result->buffer.pointer[0] != 0) { + dev_err(&pdev->dev, + "IRQ notification via DSM failed: unexpected result value (irq=%d, gpio=%d)\n", + type, value); + } + + mutex_unlock(&sdev->lock[type]); -+ ACPI_FREE(result); ++ ++ if (result) ++ ACPI_FREE(result); +} + +static irqreturn_t shps_handle_irq(int irq, void *data) @@ -350,17 +354,16 @@ index 000000000000..572fba30cd77 + struct gpio_desc *gpiod; + acpi_handle handle = ACPI_HANDLE(&pdev->dev); + const char *irq_name; -+ const int dsm = SHPS_DSM_FN_IRQ_BASE_PRESENCE + type; ++ const int dsm = shps_dsm_fn_for_irq(type); + int status, irq; + -+ /* Initialize as "not present". */ -+ sdev->gpio[type] = NULL; -+ sdev->irq[type] = SHPS_IRQ_NOT_PRESENT; -+ -+ /* Only set up interrupts that we actually need. */ ++ /* ++ * Only set up interrupts that we actually need: The Surface Book 3 ++ * does not have a DSM for base presence, so don't set up an interrupt ++ * for that. ++ */ + if (!acpi_check_dsm(handle, &shps_dsm_guid, SHPS_DSM_REVISION, BIT(dsm))) { -+ dev_dbg(&pdev->dev, "IRQ notification via DSM not present (irq=%d)\n", -+ type); ++ dev_dbg(&pdev->dev, "IRQ notification via DSM not present (irq=%d)\n", type); + return 0; + } + @@ -389,11 +392,33 @@ index 000000000000..572fba30cd77 + return 0; +} + ++static int surface_hotplug_remove(struct platform_device *pdev) ++{ ++ struct shps_device *sdev = platform_get_drvdata(pdev); ++ int i; ++ ++ /* Ensure that IRQs have been fully handled and won't trigger any more. */ ++ for (i = 0; i < SHPS_NUM_IRQS; i++) { ++ if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT) ++ disable_irq(sdev->irq[i]); ++ ++ mutex_destroy(&sdev->lock[i]); ++ } ++ ++ return 0; ++} ++ +static int surface_hotplug_probe(struct platform_device *pdev) +{ + struct shps_device *sdev; + int status, i; + ++ /* ++ * The MSHW0153 device is also present on the Surface Laptop 3, ++ * however that doesn't have a hot-pluggable PCIe device. It also ++ * doesn't have any GPIO interrupts/pins under the MSHW0153, so filter ++ * it out here. ++ */ + if (gpiod_count(&pdev->dev, NULL) < 0) + return -ENODEV; + @@ -407,15 +432,21 @@ index 000000000000..572fba30cd77 + + platform_set_drvdata(pdev, sdev); + ++ /* ++ * Initialize IRQs so that we can safely call surface_hotplug_remove() ++ * on errors. ++ */ ++ for (i = 0; i < SHPS_NUM_IRQS; i++) ++ sdev->irq[i] = SHPS_IRQ_NOT_PRESENT; ++ + /* Set up IRQs. */ + for (i = 0; i < SHPS_NUM_IRQS; i++) { + mutex_init(&sdev->lock[i]); + + status = shps_setup_irq(pdev, i); + if (status) { -+ dev_err(&pdev->dev, "failed to set up IRQ %d: %d\n", -+ i, status); -+ return status; ++ dev_err(&pdev->dev, "failed to set up IRQ %d: %d\n", i, status); ++ goto err; + } + } + @@ -425,19 +456,10 @@ index 000000000000..572fba30cd77 + shps_dsm_notify_irq(pdev, i); + + return 0; -+} + -+static int surface_hotplug_remove(struct platform_device *pdev) -+{ -+ struct shps_device *sdev = platform_get_drvdata(pdev); -+ int i; -+ -+ /* Ensure that IRQs have been fully handled and won't trigger any more. */ -+ for (i = 0; i < SHPS_NUM_IRQS; i++) -+ if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT) -+ disable_irq(sdev->irq[i]); -+ -+ return 0; ++err: ++ surface_hotplug_remove(pdev); ++ return status; +} + +static const struct acpi_device_id surface_hotplug_acpi_match[] = { @@ -461,5 +483,5 @@ index 000000000000..572fba30cd77 +MODULE_DESCRIPTION("Surface Hot-Plug Signaling Driver for Surface Book Devices"); +MODULE_LICENSE("GPL"); -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0009-surface-typecover.patch b/patches/5.4/0009-surface-typecover.patch index 89a05d12f..c1866b7a2 100644 --- a/patches/5.4/0009-surface-typecover.patch +++ b/patches/5.4/0009-surface-typecover.patch @@ -1,4 +1,4 @@ -From 8a77d6cf83747db2b3de6e84eec497f07a6d49a6 Mon Sep 17 00:00:00 2001 +From 5aa632a7f8b50ef883c5f12703a9787b1afa8b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= Date: Thu, 5 Nov 2020 13:09:45 +0100 Subject: [PATCH] hid/multitouch: Turn off Type Cover keyboard backlight when @@ -229,5 +229,5 @@ index d91e6679afb1..11bf0d87523d 100644 { .driver_data = MT_CLS_GOOGLE, HID_DEVICE(HID_BUS_ANY, HID_GROUP_ANY, USB_VENDOR_ID_GOOGLE, -- -2.30.0 +2.30.1 diff --git a/patches/5.4/0010-surface-sensors.patch b/patches/5.4/0010-surface-sensors.patch index 377061d27..26d5363d6 100644 --- a/patches/5.4/0010-surface-sensors.patch +++ b/patches/5.4/0010-surface-sensors.patch @@ -1,4 +1,4 @@ -From da8e015edd4236fd6994164c01ecae7916d64aab Mon Sep 17 00:00:00 2001 +From 0da844f153e038ebf1dc3da125f8f5f37f8e9786 Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Sat, 19 Dec 2020 17:50:55 -0800 Subject: [PATCH] iio:light:apds9960 add detection for MSHW0184 ACPI device in @@ -50,5 +50,5 @@ index c5dfb9a6b5a1..ee4a576dc62c 100644 .probe = apds9960_probe, .remove = apds9960_remove, -- -2.30.0 +2.30.1