Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,234 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Microsoft Surface Platform-Specific Drivers
#
menuconfig SURFACE_PLATFORMS
bool "Microsoft Surface Platform-Specific Device Drivers"
depends on ARM64 || X86 || COMPILE_TEST
default y if ARM64 || X86
help
Say Y here to get to see options for platform-specific device drivers
for Microsoft Surface devices. This option alone does not add any
kernel code.
If you say N, all options in this submenu will be skipped and disabled.
if SURFACE_PLATFORMS
config SURFACE3_WMI
tristate "Surface 3 WMI Driver"
depends on ACPI_WMI
depends on DMI
depends on INPUT
depends on SPI
help
Say Y here if you have a Surface 3.
To compile this driver as a module, choose M here: the module will
be called surface3-wmi.
config SURFACE_3_POWER_OPREGION
tristate "Surface 3 battery platform operation region support"
depends on ACPI
depends on I2C
help
This driver provides support for ACPI operation
region of the Surface 3 battery platform driver.
config SURFACE_ACPI_NOTIFY
tristate "Surface ACPI Notify Driver"
depends on SURFACE_AGGREGATOR
help
Surface ACPI Notify (SAN) driver for Microsoft Surface devices.
This driver provides support for the ACPI interface (called SAN) of
the Surface System Aggregator Module (SSAM) EC. This interface is used
on 5th- and 6th-generation Microsoft Surface devices (including
Surface Pro 5 and 6, Surface Book 2, Surface Laptops 1 and 2, and in
reduced functionality on the Surface Laptop 3) to execute SSAM
requests directly from ACPI code, as well as receive SSAM events and
turn them into ACPI notifications. It essentially acts as a
translation layer between the SSAM controller and ACPI.
Specifically, this driver may be needed for battery status reporting,
thermal sensor access, and real-time clock information, depending on
the Surface device in question.
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.
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_HUB
tristate "Surface System Aggregator Module Subsystem Device Hubs"
depends on SURFACE_AGGREGATOR
depends on SURFACE_AGGREGATOR_BUS
help
Device-hub drivers for Surface System Aggregator Module (SSAM) subsystem
devices.
Provides subsystem hub drivers which manage client devices on various
SSAM subsystems. In some subsystems, notably the BAS subsystem managing
devices contained in the base of the Surface Book 3 and the KIP subsystem
managing type-cover devices in the Surface Pro 8 and Surface Pro X,
devices can be (hot-)removed. Hub devices and drivers are required to
manage these subdevices.
Devices managed via these hubs are:
- Battery/AC devices (Surface Book 3).
- HID input devices (7th-generation and later models with detachable
input devices).
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 mentioned above will not be instantiated
and thus any functionality provided by them will be missing, even when
drivers for these devices are present. This module only provides the
respective subsystem hubs. Both drivers and device specification (e.g.
via the Surface Aggregator Registry) for these devices still need to be
selected via other options.
config SURFACE_AGGREGATOR_REGISTRY
tristate "Surface System Aggregator Module Device Registry"
depends on SURFACE_AGGREGATOR
depends on SURFACE_AGGREGATOR_BUS
help
Device-registry 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 and managed via this registry.
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_AGGREGATOR_TABLET_SWITCH
tristate "Surface Aggregator Generic Tablet-Mode Switch Driver"
depends on SURFACE_AGGREGATOR
depends on SURFACE_AGGREGATOR_BUS
depends on INPUT
help
Provides a tablet-mode switch input device on Microsoft Surface models
using the KIP subsystem for detachable keyboards (e.g. keyboard covers)
or the POS subsystem for device/screen posture changes.
The KIP subsystem is used on newer Surface generations to handle
detachable input peripherals, specifically the keyboard cover (containing
keyboard and touchpad) on the Surface Pro 8 and Surface Pro X. The POS
subsystem is used for device posture change notifications on the Surface
Laptop Studio. This module provides a driver to let user-space know when
the device should be considered in tablet-mode due to the keyboard cover
being detached or folded back (essentially signaling when the keyboard is
not available for input). It does so by creating a tablet-mode switch
input device, sending the standard SW_TABLET_MODE event on mode change.
Select M or Y here, if you want to provide tablet-mode switch input
events on the Surface Pro 8, Surface Pro X, and Surface Laptop Studio.
config SURFACE_DTX
tristate "Surface DTX (Detachment System) Driver"
depends on SURFACE_AGGREGATOR
depends on INPUT
help
Driver for the Surface Book clipboard detachment system (DTX).
On the Surface Book series devices, the display part containing the
CPU (called the clipboard) can be detached from the base (containing a
battery, the keyboard, and, optionally, a discrete GPU) by (if
necessary) unlocking and opening the latch connecting both parts.
This driver provides a user-space interface that can influence the
behavior of this process, which includes the option to abort it in
case the base is still in use or speed it up in case it is not.
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, will not be supported.
config SURFACE_GPE
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
accordingly. It is required on those devices to allow wake-ups from
suspend by opening the lid.
config SURFACE_HOTPLUG
tristate "Surface Hot-Plug Driver"
depends on ACPI
depends on GPIOLIB
help
Driver for out-of-band hot-plug event signaling on Microsoft Surface
devices with hot-pluggable PCIe cards.
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 SURFACE_PLATFORM_PROFILE
tristate "Surface Platform Profile Driver"
depends on ACPI
depends on SURFACE_AGGREGATOR_REGISTRY
select ACPI_PLATFORM_PROFILE
help
Provides support for the ACPI platform profile on 5th- and later
generation Microsoft Surface devices.
More specifically, this driver provides ACPI platform profile support
on Microsoft Surface devices with a Surface System Aggregator Module
(SSAM) connected via the Surface Serial Hub (SSH / SAM-over-SSH). In
other words, this driver provides platform profile support on the
Surface Pro 5, Surface Book 2, Surface Laptop, Surface Laptop Go and
later. On those devices, the platform profile can significantly
influence cooling behavior, e.g. setting it to 'quiet' (default) or
'low-power' can significantly limit performance of the discrete GPU on
Surface Books, while in turn leading to lower power consumption and/or
less fan noise.
Select M or Y here, if you want to include ACPI platform profile
support on the above mentioned devices.
config SURFACE_PRO3_BUTTON
tristate "Power/home/volume buttons driver for Microsoft Surface Pro 3/4 tablet"
depends on ACPI
depends on INPUT
help
This driver handles the power/home/volume buttons on the Microsoft Surface Pro 3/4 tablet.
source "drivers/platform/surface/aggregator/Kconfig"
endif # SURFACE_PLATFORMS
@@ -0,0 +1,19 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for linux/drivers/platform/surface
# Microsoft Surface Platform-Specific Drivers
#
obj-$(CONFIG_SURFACE3_WMI) += surface3-wmi.o
obj-$(CONFIG_SURFACE_3_POWER_OPREGION) += surface3_power.o
obj-$(CONFIG_SURFACE_ACPI_NOTIFY) += surface_acpi_notify.o
obj-$(CONFIG_SURFACE_AGGREGATOR) += aggregator/
obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV) += surface_aggregator_cdev.o
obj-$(CONFIG_SURFACE_AGGREGATOR_HUB) += surface_aggregator_hub.o
obj-$(CONFIG_SURFACE_AGGREGATOR_REGISTRY) += surface_aggregator_registry.o
obj-$(CONFIG_SURFACE_AGGREGATOR_TABLET_SWITCH) += surface_aggregator_tabletsw.o
obj-$(CONFIG_SURFACE_DTX) += surface_dtx.o
obj-$(CONFIG_SURFACE_GPE) += surface_gpe.o
obj-$(CONFIG_SURFACE_HOTPLUG) += surface_hotplug.o
obj-$(CONFIG_SURFACE_PLATFORM_PROFILE) += surface_platform_profile.o
obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o
@@ -0,0 +1,69 @@
# SPDX-License-Identifier: GPL-2.0+
# Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
menuconfig SURFACE_AGGREGATOR
tristate "Microsoft Surface System Aggregator Module Subsystem and Drivers"
depends on SERIAL_DEV_BUS
depends on ACPI && !RISCV
select CRC_ITU_T
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.
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: GPL-2.0+
# Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
# For include/trace/define_trace.h to include trace.h
CFLAGS_core.o = -I$(src)
obj-$(CONFIG_SURFACE_AGGREGATOR) += surface_aggregator.o
surface_aggregator-y := core.o
surface_aggregator-y += ssh_parser.o
surface_aggregator-y += ssh_packet_layer.o
surface_aggregator-y += ssh_request_layer.o
surface_aggregator-$(CONFIG_SURFACE_AGGREGATOR_BUS) += bus.o
surface_aggregator-y += controller.o
@@ -0,0 +1,527 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Surface System Aggregator Module bus and device integration.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/device.h>
#include <linux/of.h>
#include <linux/property.h>
#include <linux/slab.h>
#include <linux/surface_aggregator/controller.h>
#include <linux/surface_aggregator/device.h>
#include "bus.h"
#include "controller.h"
/* -- Device and bus functions. --------------------------------------------- */
static ssize_t modalias_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct ssam_device *sdev = to_ssam_device(dev);
return sysfs_emit(buf, "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 const struct bus_type ssam_bus_type;
static int ssam_device_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
const 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);
fwnode_handle_put(sdev->dev.fwnode);
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_obj(*sdev);
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_do_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, const struct device_driver *drv)
{
const 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;
return !!ssam_device_id_match(sdrv->match_table, sdev->uid);
}
static int ssam_bus_probe(struct device *dev)
{
return to_ssam_device_driver(dev->driver)
->probe(to_ssam_device(dev));
}
static void ssam_bus_remove(struct device *dev)
{
struct ssam_device_driver *sdrv = to_ssam_device_driver(dev->driver);
if (sdrv->remove)
sdrv->remove(to_ssam_device(dev));
}
static const struct bus_type ssam_bus_type = {
.name = "surface_aggregator",
.match = ssam_bus_match,
.probe = ssam_bus_probe,
.remove = ssam_bus_remove,
};
/**
* __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;
/* 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);
/* -- Bus registration. ----------------------------------------------------- */
/**
* 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);
}
/* -- Helpers for controller and hub devices. ------------------------------- */
static int ssam_device_uid_from_string(const char *str, struct ssam_device_uid *uid)
{
u8 d, tc, tid, iid, fn;
int n;
n = sscanf(str, "%hhx:%hhx:%hhx:%hhx:%hhx", &d, &tc, &tid, &iid, &fn);
if (n != 5)
return -EINVAL;
uid->domain = d;
uid->category = tc;
uid->target = tid;
uid->instance = iid;
uid->function = fn;
return 0;
}
static int ssam_get_uid_for_node(struct fwnode_handle *node, struct ssam_device_uid *uid)
{
const char *str = fwnode_get_name(node);
/*
* To simplify definitions of firmware nodes, we set the device name
* based on the UID of the device, prefixed with "ssam:".
*/
if (strncmp(str, "ssam:", strlen("ssam:")) != 0)
return -ENODEV;
str += strlen("ssam:");
return ssam_device_uid_from_string(str, uid);
}
static int ssam_add_client_device(struct device *parent, struct ssam_controller *ctrl,
struct fwnode_handle *node)
{
struct ssam_device_uid uid;
struct ssam_device *sdev;
int status;
status = ssam_get_uid_for_node(node, &uid);
if (status)
return status;
sdev = ssam_device_alloc(ctrl, uid);
if (!sdev)
return -ENOMEM;
sdev->dev.parent = parent;
sdev->dev.fwnode = fwnode_handle_get(node);
sdev->dev.of_node = to_of_node(node);
status = ssam_device_add(sdev);
if (status)
ssam_device_put(sdev);
return status;
}
/**
* __ssam_register_clients() - Register client devices defined under the
* given firmware node as children of the given device.
* @parent: The parent device under which clients should be registered.
* @ctrl: The controller with which client should be registered.
* @node: The firmware node holding definitions of the devices to be added.
*
* Register all clients that have been defined as children of the given root
* firmware node as children of the given parent device. The respective child
* firmware nodes will be associated with the correspondingly created child
* devices.
*
* The given controller will be used to instantiate the new devices. See
* ssam_device_add() for details.
*
* Note that, generally, the use of either ssam_device_register_clients() or
* ssam_register_clients() should be preferred as they directly use the
* firmware node and/or controller associated with the given device. This
* function is only intended for use when different device specifications (e.g.
* ACPI and firmware nodes) need to be combined (as is done in the platform hub
* of the device registry).
*
* Return: Returns zero on success, nonzero on failure.
*/
int __ssam_register_clients(struct device *parent, struct ssam_controller *ctrl,
struct fwnode_handle *node)
{
struct fwnode_handle *child;
int status;
fwnode_for_each_child_node(node, child) {
/*
* Try to add the device specified in the firmware node. If
* this fails with -ENODEV, the node does not specify any SSAM
* device, so ignore it and continue with the next one.
*/
status = ssam_add_client_device(parent, ctrl, child);
if (status && status != -ENODEV) {
fwnode_handle_put(child);
goto err;
}
}
return 0;
err:
ssam_remove_clients(parent);
return status;
}
EXPORT_SYMBOL_GPL(__ssam_register_clients);
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_remove_clients() - Remove SSAM client devices registered as direct
* children under the given parent device.
* @dev: The (parent) device to remove all direct clients for.
*
* Remove all SSAM client devices registered as direct children under the given
* device. Note that this only accounts for direct children of the device.
* Refer to ssam_device_add()/ssam_device_remove() for more details.
*/
void ssam_remove_clients(struct device *dev)
{
device_for_each_child_reverse(dev, NULL, ssam_remove_device);
}
EXPORT_SYMBOL_GPL(ssam_remove_clients);
@@ -0,0 +1,24 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Surface System Aggregator Module bus and device integration.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#ifndef _SURFACE_AGGREGATOR_BUS_H
#define _SURFACE_AGGREGATOR_BUS_H
#include <linux/surface_aggregator/controller.h>
#ifdef CONFIG_SURFACE_AGGREGATOR_BUS
int ssam_bus_register(void);
void ssam_bus_unregister(void);
#else /* CONFIG_SURFACE_AGGREGATOR_BUS */
static inline int ssam_bus_register(void) { return 0; }
static inline void ssam_bus_unregister(void) {}
#endif /* CONFIG_SURFACE_AGGREGATOR_BUS */
#endif /* _SURFACE_AGGREGATOR_BUS_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,285 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Main SSAM/SSH controller structure and functionality.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#ifndef _SURFACE_AGGREGATOR_CONTROLLER_H
#define _SURFACE_AGGREGATOR_CONTROLLER_H
#include <linux/kref.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/rwsem.h>
#include <linux/serdev.h>
#include <linux/spinlock.h>
#include <linux/srcu.h>
#include <linux/types.h>
#include <linux/workqueue.h>
#include <linux/surface_aggregator/controller.h>
#include <linux/surface_aggregator/serial_hub.h>
#include "ssh_request_layer.h"
/* -- Safe counters. -------------------------------------------------------- */
/**
* struct ssh_seq_counter - Safe counter for SSH sequence IDs.
* @value: The current counter value.
*/
struct ssh_seq_counter {
u8 value;
};
/**
* struct ssh_rqid_counter - Safe counter for SSH request IDs.
* @value: The current counter value.
*/
struct ssh_rqid_counter {
u16 value;
};
/* -- Event/notification system. -------------------------------------------- */
/**
* struct ssam_nf_head - Notifier head for SSAM events.
* @srcu: The SRCU struct for synchronization.
* @head: List-head for notifier blocks registered under this head.
*/
struct ssam_nf_head {
struct srcu_struct srcu;
struct list_head head;
};
/**
* struct ssam_nf - Notifier callback- and activation-registry for SSAM events.
* @lock: Lock guarding (de-)registration of notifier blocks. Note: This
* lock does not need to be held for notifier calls, only
* registration and deregistration.
* @refcount: The root of the RB-tree used for reference-counting enabled
* events/notifications.
* @head: The list of notifier heads for event/notification callbacks.
*/
struct ssam_nf {
struct mutex lock;
struct rb_root refcount;
struct ssam_nf_head head[SSH_NUM_EVENTS];
};
/* -- Event/async request completion system. -------------------------------- */
struct ssam_cplt;
/**
* struct ssam_event_item - Struct for event queuing and completion.
* @node: The node in the queue.
* @rqid: The request ID of the event.
* @ops: Instance specific functions.
* @ops.free: Callback for freeing this event item.
* @event: Actual event data.
*/
struct ssam_event_item {
struct list_head node;
u16 rqid;
struct {
void (*free)(struct ssam_event_item *event);
} ops;
struct ssam_event event; /* must be last */
};
/**
* struct ssam_event_queue - Queue for completing received events.
* @cplt: Reference to the completion system on which this queue is active.
* @lock: The lock for any operation on the queue.
* @head: The list-head of the queue.
* @work: The &struct work_struct performing completion work for this queue.
*/
struct ssam_event_queue {
struct ssam_cplt *cplt;
spinlock_t lock;
struct list_head head;
struct work_struct work;
};
/**
* struct ssam_event_target - Set of queues for a single SSH target ID.
* @queue: The array of queues, one queue per event ID.
*/
struct ssam_event_target {
struct ssam_event_queue queue[SSH_NUM_EVENTS];
};
/**
* struct ssam_cplt - SSAM event/async request completion system.
* @dev: The device with which this system is associated. Only used
* for logging.
* @wq: The &struct workqueue_struct on which all completion work
* items are queued.
* @event: Event completion management.
* @event.target: Array of &struct ssam_event_target, one for each target.
* @event.notif: Notifier callbacks and event activation reference counting.
*/
struct ssam_cplt {
struct device *dev;
struct workqueue_struct *wq;
struct {
struct ssam_event_target target[SSH_NUM_TARGETS];
struct ssam_nf notif;
} event;
};
/* -- Main SSAM device structures. ------------------------------------------ */
/**
* enum ssam_controller_state - State values for &struct ssam_controller.
* @SSAM_CONTROLLER_UNINITIALIZED:
* The controller has not been initialized yet or has been deinitialized.
* @SSAM_CONTROLLER_INITIALIZED:
* The controller is initialized, but has not been started yet.
* @SSAM_CONTROLLER_STARTED:
* The controller has been started and is ready to use.
* @SSAM_CONTROLLER_STOPPED:
* The controller has been stopped.
* @SSAM_CONTROLLER_SUSPENDED:
* The controller has been suspended.
*/
enum ssam_controller_state {
SSAM_CONTROLLER_UNINITIALIZED,
SSAM_CONTROLLER_INITIALIZED,
SSAM_CONTROLLER_STARTED,
SSAM_CONTROLLER_STOPPED,
SSAM_CONTROLLER_SUSPENDED,
};
/**
* struct ssam_controller_caps - Controller device capabilities.
* @ssh_power_profile: SSH power profile.
* @ssh_buffer_size: SSH driver UART buffer size.
* @screen_on_sleep_idle_timeout: SAM UART screen-on sleep idle timeout.
* @screen_off_sleep_idle_timeout: SAM UART screen-off sleep idle timeout.
* @d3_closes_handle: SAM closes UART handle in D3.
*
* Controller and SSH device capabilities found in ACPI.
*/
struct ssam_controller_caps {
u32 ssh_power_profile;
u32 ssh_buffer_size;
u32 screen_on_sleep_idle_timeout;
u32 screen_off_sleep_idle_timeout;
u32 d3_closes_handle:1;
};
/**
* struct ssam_controller - SSAM controller device.
* @kref: Reference count of the controller.
* @lock: Main lock for the controller, used to guard state changes.
* @state: Controller state.
* @rtl: Request transport layer for SSH I/O.
* @cplt: Completion system for SSH/SSAM events and asynchronous requests.
* @counter: Safe SSH message ID counters.
* @counter.seq: Sequence ID counter.
* @counter.rqid: Request ID counter.
* @irq: Wakeup IRQ resources.
* @irq.num: The wakeup IRQ number.
* @irq.wakeup_enabled: Whether wakeup by IRQ is enabled during suspend.
* @caps: The controller device capabilities.
*/
struct ssam_controller {
struct kref kref;
struct rw_semaphore lock;
enum ssam_controller_state state;
struct ssh_rtl rtl;
struct ssam_cplt cplt;
struct {
struct ssh_seq_counter seq;
struct ssh_rqid_counter rqid;
} counter;
struct {
int num;
bool wakeup_enabled;
} irq;
struct ssam_controller_caps caps;
};
#define to_ssam_controller(ptr, member) \
container_of(ptr, struct ssam_controller, member)
#define ssam_dbg(ctrl, fmt, ...) rtl_dbg(&(ctrl)->rtl, fmt, ##__VA_ARGS__)
#define ssam_info(ctrl, fmt, ...) rtl_info(&(ctrl)->rtl, fmt, ##__VA_ARGS__)
#define ssam_warn(ctrl, fmt, ...) rtl_warn(&(ctrl)->rtl, fmt, ##__VA_ARGS__)
#define ssam_err(ctrl, fmt, ...) rtl_err(&(ctrl)->rtl, fmt, ##__VA_ARGS__)
/**
* ssam_controller_receive_buf() - Provide input-data to the controller.
* @ctrl: The controller.
* @buf: The input buffer.
* @n: The number of bytes in the input buffer.
*
* Provide input data to be evaluated by the controller, which has been
* received via the lower-level transport.
*
* Return: Returns the number of bytes consumed, or, if the packet transport
* layer of the controller has been shut down, %-ESHUTDOWN.
*/
static inline
ssize_t ssam_controller_receive_buf(struct ssam_controller *ctrl, const u8 *buf,
size_t n)
{
return ssh_ptl_rx_rcvbuf(&ctrl->rtl.ptl, buf, n);
}
/**
* ssam_controller_write_wakeup() - Notify the controller that the underlying
* device has space available for data to be written.
* @ctrl: The controller.
*/
static inline void ssam_controller_write_wakeup(struct ssam_controller *ctrl)
{
ssh_ptl_tx_wakeup_transfer(&ctrl->rtl.ptl);
}
int ssam_controller_init(struct ssam_controller *ctrl, struct serdev_device *s);
int ssam_controller_start(struct ssam_controller *ctrl);
void ssam_controller_shutdown(struct ssam_controller *ctrl);
void ssam_controller_destroy(struct ssam_controller *ctrl);
int ssam_notifier_disable_registered(struct ssam_controller *ctrl);
void ssam_notifier_restore_registered(struct ssam_controller *ctrl);
int ssam_irq_setup(struct ssam_controller *ctrl);
void ssam_irq_free(struct ssam_controller *ctrl);
int ssam_irq_arm_for_wakeup(struct ssam_controller *ctrl);
void ssam_irq_disarm_wakeup(struct ssam_controller *ctrl);
void ssam_controller_lock(struct ssam_controller *c);
void ssam_controller_unlock(struct ssam_controller *c);
int ssam_get_firmware_version(struct ssam_controller *ctrl, u32 *version);
int ssam_ctrl_notif_display_off(struct ssam_controller *ctrl);
int ssam_ctrl_notif_display_on(struct ssam_controller *ctrl);
int ssam_ctrl_notif_d0_exit(struct ssam_controller *ctrl);
int ssam_ctrl_notif_d0_entry(struct ssam_controller *ctrl);
int ssam_controller_suspend(struct ssam_controller *ctrl);
int ssam_controller_resume(struct ssam_controller *ctrl);
int ssam_event_item_cache_init(void);
void ssam_event_item_cache_destroy(void);
#endif /* _SURFACE_AGGREGATOR_CONTROLLER_H */
@@ -0,0 +1,894 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Surface Serial Hub (SSH) driver for communication with the Surface/System
* Aggregator Module (SSAM/SAM).
*
* Provides access to a SAM-over-SSH connected EC via a controller device.
* Handles communication via requests as well as enabling, disabling, and
* relaying of events.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/acpi.h>
#include <linux/atomic.h>
#include <linux/completion.h>
#include <linux/gpio/consumer.h>
#include <linux/kernel.h>
#include <linux/kref.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/serdev.h>
#include <linux/sysfs.h>
#include <linux/units.h>
#include <linux/surface_aggregator/controller.h>
#include <linux/surface_aggregator/device.h>
#include "bus.h"
#include "controller.h"
#define CREATE_TRACE_POINTS
#include "trace.h"
/* -- Static controller reference. ------------------------------------------ */
/*
* Main controller reference. The corresponding lock must be held while
* accessing (reading/writing) the reference.
*/
static struct ssam_controller *__ssam_controller;
static DEFINE_SPINLOCK(__ssam_controller_lock);
/**
* ssam_get_controller() - Get reference to SSAM controller.
*
* Returns a reference to the SSAM controller of the system or %NULL if there
* is none, it hasn't been set up yet, or it has already been unregistered.
* This function automatically increments the reference count of the
* controller, thus the calling party must ensure that ssam_controller_put()
* is called when it doesn't need the controller any more.
*/
struct ssam_controller *ssam_get_controller(void)
{
struct ssam_controller *ctrl;
spin_lock(&__ssam_controller_lock);
ctrl = __ssam_controller;
if (!ctrl)
goto out;
if (WARN_ON(!kref_get_unless_zero(&ctrl->kref)))
ctrl = NULL;
out:
spin_unlock(&__ssam_controller_lock);
return ctrl;
}
EXPORT_SYMBOL_GPL(ssam_get_controller);
/**
* ssam_try_set_controller() - Try to set the main controller reference.
* @ctrl: The controller to which the reference should point.
*
* Set the main controller reference to the given pointer if the reference
* hasn't been set already.
*
* Return: Returns zero on success or %-EEXIST if the reference has already
* been set.
*/
static int ssam_try_set_controller(struct ssam_controller *ctrl)
{
int status = 0;
spin_lock(&__ssam_controller_lock);
if (!__ssam_controller)
__ssam_controller = ctrl;
else
status = -EEXIST;
spin_unlock(&__ssam_controller_lock);
return status;
}
/**
* ssam_clear_controller() - Remove/clear the main controller reference.
*
* Clears the main controller reference, i.e. sets it to %NULL. This function
* should be called before the controller is shut down.
*/
static void ssam_clear_controller(void)
{
spin_lock(&__ssam_controller_lock);
__ssam_controller = NULL;
spin_unlock(&__ssam_controller_lock);
}
/**
* ssam_client_link() - Link an arbitrary client device to the controller.
* @c: The controller to link to.
* @client: The client device.
*
* Link an arbitrary client device to the controller by creating a device link
* between it as consumer and the controller device as provider. This function
* can be used for non-SSAM devices (or SSAM devices not registered as child
* under the controller) to guarantee that the controller is valid for as long
* as the driver of the client device is bound, and that proper suspend and
* resume ordering is guaranteed.
*
* The device link does not have to be destructed manually. It is removed
* automatically once the driver of the client device unbinds.
*
* Return: Returns zero on success, %-ENODEV if the controller is not ready or
* going to be removed soon, or %-ENOMEM if the device link could not be
* created for other reasons.
*/
int ssam_client_link(struct ssam_controller *c, struct device *client)
{
const u32 flags = DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOREMOVE_CONSUMER;
struct device_link *link;
struct device *ctrldev;
ssam_controller_statelock(c);
if (c->state != SSAM_CONTROLLER_STARTED) {
ssam_controller_stateunlock(c);
return -ENODEV;
}
ctrldev = ssam_controller_device(c);
if (!ctrldev) {
ssam_controller_stateunlock(c);
return -ENODEV;
}
link = device_link_add(client, ctrldev, flags);
if (!link) {
ssam_controller_stateunlock(c);
return -ENOMEM;
}
/*
* Return -ENODEV if supplier driver is on its way to be removed. In
* this case, the controller won't be around for much longer and the
* device link is not going to save us any more, as unbinding is
* already in progress.
*/
if (READ_ONCE(link->status) == DL_STATE_SUPPLIER_UNBIND) {
ssam_controller_stateunlock(c);
return -ENODEV;
}
ssam_controller_stateunlock(c);
return 0;
}
EXPORT_SYMBOL_GPL(ssam_client_link);
/**
* ssam_client_bind() - Bind an arbitrary client device to the controller.
* @client: The client device.
*
* Link an arbitrary client device to the controller by creating a device link
* between it as consumer and the main controller device as provider. This
* function can be used for non-SSAM devices to guarantee that the controller
* returned by this function is valid for as long as the driver of the client
* device is bound, and that proper suspend and resume ordering is guaranteed.
*
* This function does essentially the same as ssam_client_link(), except that
* it first fetches the main controller reference, then creates the link, and
* finally returns this reference. Note that this function does not increment
* the reference counter of the controller, as, due to the link, the
* controller lifetime is assured as long as the driver of the client device
* is bound.
*
* It is not valid to use the controller reference obtained by this method
* outside of the driver bound to the client device at the time of calling
* this function, without first incrementing the reference count of the
* controller via ssam_controller_get(). Even after doing this, care must be
* taken that requests are only submitted and notifiers are only
* (un-)registered when the controller is active and not suspended. In other
* words: The device link only lives as long as the client driver is bound and
* any guarantees enforced by this link (e.g. active controller state) can
* only be relied upon as long as this link exists and may need to be enforced
* in other ways afterwards.
*
* The created device link does not have to be destructed manually. It is
* removed automatically once the driver of the client device unbinds.
*
* Return: Returns the controller on success, an error pointer with %-ENODEV
* if the controller is not present, not ready or going to be removed soon, or
* %-ENOMEM if the device link could not be created for other reasons.
*/
struct ssam_controller *ssam_client_bind(struct device *client)
{
struct ssam_controller *c;
int status;
c = ssam_get_controller();
if (!c)
return ERR_PTR(-ENODEV);
status = ssam_client_link(c, client);
/*
* Note that we can drop our controller reference in both success and
* failure cases: On success, we have bound the controller lifetime
* inherently to the client driver lifetime, i.e. it the controller is
* now guaranteed to outlive the client driver. On failure, we're not
* going to use the controller any more.
*/
ssam_controller_put(c);
return status >= 0 ? c : ERR_PTR(status);
}
EXPORT_SYMBOL_GPL(ssam_client_bind);
/* -- Glue layer (serdev_device -> ssam_controller). ------------------------ */
static size_t ssam_receive_buf(struct serdev_device *dev, const u8 *buf,
size_t n)
{
struct ssam_controller *ctrl;
int ret;
ctrl = serdev_device_get_drvdata(dev);
ret = ssam_controller_receive_buf(ctrl, buf, n);
return ret < 0 ? 0 : ret;
}
static void ssam_write_wakeup(struct serdev_device *dev)
{
ssam_controller_write_wakeup(serdev_device_get_drvdata(dev));
}
static const struct serdev_device_ops ssam_serdev_ops = {
.receive_buf = ssam_receive_buf,
.write_wakeup = ssam_write_wakeup,
};
/* -- SysFS and misc. ------------------------------------------------------- */
static int ssam_log_firmware_version(struct ssam_controller *ctrl)
{
u32 version, a, b, c;
int status;
status = ssam_get_firmware_version(ctrl, &version);
if (status)
return status;
a = (version >> 24) & 0xff;
b = ((version >> 8) & 0xffff);
c = version & 0xff;
ssam_info(ctrl, "SAM firmware version: %u.%u.%u\n", a, b, c);
return 0;
}
static ssize_t firmware_version_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct ssam_controller *ctrl = dev_get_drvdata(dev);
u32 version, a, b, c;
int status;
status = ssam_get_firmware_version(ctrl, &version);
if (status < 0)
return status;
a = (version >> 24) & 0xff;
b = ((version >> 8) & 0xffff);
c = version & 0xff;
return sysfs_emit(buf, "%u.%u.%u\n", a, b, c);
}
static DEVICE_ATTR_RO(firmware_version);
static struct attribute *ssam_sam_attrs[] = {
&dev_attr_firmware_version.attr,
NULL
};
static const struct attribute_group ssam_sam_group = {
.name = "sam",
.attrs = ssam_sam_attrs,
};
/* -- Serial device setup. -------------------------------------------------- */
static acpi_status ssam_serdev_setup_via_acpi_crs(struct acpi_resource *rsc,
void *ctx)
{
struct serdev_device *serdev = ctx;
struct acpi_resource_uart_serialbus *uart;
bool flow_control;
int status = 0;
if (!serdev_acpi_get_uart_resource(rsc, &uart))
return AE_OK;
/* Set up serdev device. */
serdev_device_set_baudrate(serdev, uart->default_baud_rate);
/* serdev currently only supports RTSCTS flow control. */
if (uart->flow_control & (~((u8)ACPI_UART_FLOW_CONTROL_HW))) {
dev_warn(&serdev->dev, "setup: unsupported flow control (value: %#04x)\n",
uart->flow_control);
}
/* Set RTSCTS flow control. */
flow_control = uart->flow_control & ACPI_UART_FLOW_CONTROL_HW;
serdev_device_set_flow_control(serdev, flow_control);
/* serdev currently only supports EVEN/ODD parity. */
switch (uart->parity) {
case ACPI_UART_PARITY_NONE:
status = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
break;
case ACPI_UART_PARITY_EVEN:
status = serdev_device_set_parity(serdev, SERDEV_PARITY_EVEN);
break;
case ACPI_UART_PARITY_ODD:
status = serdev_device_set_parity(serdev, SERDEV_PARITY_ODD);
break;
default:
dev_warn(&serdev->dev, "setup: unsupported parity (value: %#04x)\n",
uart->parity);
break;
}
if (status) {
dev_err(&serdev->dev, "setup: failed to set parity (value: %#04x, error: %d)\n",
uart->parity, status);
return AE_ERROR;
}
/* We've found the resource and are done. */
return AE_CTRL_TERMINATE;
}
static int ssam_serdev_setup_via_acpi(struct serdev_device *serdev, acpi_handle handle)
{
acpi_status status;
status = acpi_walk_resources(handle, METHOD_NAME__CRS,
ssam_serdev_setup_via_acpi_crs, serdev);
return status ? -ENXIO : 0;
}
static int ssam_serdev_setup(struct acpi_device *ssh, struct serdev_device *serdev)
{
if (ssh)
return ssam_serdev_setup_via_acpi(serdev, ssh->handle);
/* TODO: these values may differ per board/implementation */
serdev_device_set_baudrate(serdev, 4 * HZ_PER_MHZ);
serdev_device_set_flow_control(serdev, true);
serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
return 0;
}
/* -- Power management. ----------------------------------------------------- */
static void ssam_serial_hub_shutdown(struct serdev_device *serdev)
{
struct ssam_controller *c = dev_get_drvdata(&serdev->dev);
int status;
/*
* Try to disable notifiers, signal display-off and D0-exit, ignore any
* errors.
*
* Note: It has not been established yet if this is actually
* necessary/useful for shutdown.
*/
status = ssam_notifier_disable_registered(c);
if (status) {
ssam_err(c, "pm: failed to disable notifiers for shutdown: %d\n",
status);
}
status = ssam_ctrl_notif_display_off(c);
if (status)
ssam_err(c, "pm: display-off notification failed: %d\n", status);
status = ssam_ctrl_notif_d0_exit(c);
if (status)
ssam_err(c, "pm: D0-exit notification failed: %d\n", status);
}
#ifdef CONFIG_PM_SLEEP
static int ssam_serial_hub_pm_prepare(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
/*
* Try to signal display-off, This will quiesce events.
*
* Note: Signaling display-off/display-on should normally be done from
* some sort of display state notifier. As that is not available,
* signal it here.
*/
status = ssam_ctrl_notif_display_off(c);
if (status)
ssam_err(c, "pm: display-off notification failed: %d\n", status);
return status;
}
static void ssam_serial_hub_pm_complete(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
/*
* Try to signal display-on. This will restore events.
*
* Note: Signaling display-off/display-on should normally be done from
* some sort of display state notifier. As that is not available,
* signal it here.
*/
status = ssam_ctrl_notif_display_on(c);
if (status)
ssam_err(c, "pm: display-on notification failed: %d\n", status);
}
static int ssam_serial_hub_pm_suspend(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
/*
* Try to signal D0-exit, enable IRQ wakeup if specified. Abort on
* error.
*/
status = ssam_ctrl_notif_d0_exit(c);
if (status) {
ssam_err(c, "pm: D0-exit notification failed: %d\n", status);
goto err_notif;
}
status = ssam_irq_arm_for_wakeup(c);
if (status)
goto err_irq;
WARN_ON(ssam_controller_suspend(c));
return 0;
err_irq:
ssam_ctrl_notif_d0_entry(c);
err_notif:
ssam_ctrl_notif_display_on(c);
return status;
}
static int ssam_serial_hub_pm_resume(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
WARN_ON(ssam_controller_resume(c));
/*
* Try to disable IRQ wakeup (if specified) and signal D0-entry. In
* case of errors, log them and try to restore normal operation state
* as far as possible.
*
* Note: Signaling display-off/display-on should normally be done from
* some sort of display state notifier. As that is not available,
* signal it here.
*/
ssam_irq_disarm_wakeup(c);
status = ssam_ctrl_notif_d0_entry(c);
if (status)
ssam_err(c, "pm: D0-entry notification failed: %d\n", status);
return 0;
}
static int ssam_serial_hub_pm_freeze(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
/*
* During hibernation image creation, we only have to ensure that the
* EC doesn't send us any events. This is done via the display-off
* and D0-exit notifications. Note that this sets up the wakeup IRQ
* on the EC side, however, we have disabled it by default on our side
* and won't enable it here.
*
* See ssam_serial_hub_poweroff() for more details on the hibernation
* process.
*/
status = ssam_ctrl_notif_d0_exit(c);
if (status) {
ssam_err(c, "pm: D0-exit notification failed: %d\n", status);
ssam_ctrl_notif_display_on(c);
return status;
}
WARN_ON(ssam_controller_suspend(c));
return 0;
}
static int ssam_serial_hub_pm_thaw(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
WARN_ON(ssam_controller_resume(c));
status = ssam_ctrl_notif_d0_entry(c);
if (status)
ssam_err(c, "pm: D0-exit notification failed: %d\n", status);
return status;
}
static int ssam_serial_hub_pm_poweroff(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
/*
* When entering hibernation and powering off the system, the EC, at
* least on some models, may disable events. Without us taking care of
* that, this leads to events not being enabled/restored when the
* system resumes from hibernation, resulting SAM-HID subsystem devices
* (i.e. keyboard, touchpad) not working, AC-plug/AC-unplug events being
* gone, etc.
*
* To avoid these issues, we disable all registered events here (this is
* likely not actually required) and restore them during the drivers PM
* restore callback.
*
* Wakeup from the EC interrupt is not supported during hibernation,
* so don't arm the IRQ here.
*/
status = ssam_notifier_disable_registered(c);
if (status) {
ssam_err(c, "pm: failed to disable notifiers for hibernation: %d\n",
status);
return status;
}
status = ssam_ctrl_notif_d0_exit(c);
if (status) {
ssam_err(c, "pm: D0-exit notification failed: %d\n", status);
ssam_notifier_restore_registered(c);
return status;
}
WARN_ON(ssam_controller_suspend(c));
return 0;
}
static int ssam_serial_hub_pm_restore(struct device *dev)
{
struct ssam_controller *c = dev_get_drvdata(dev);
int status;
/*
* Ignore but log errors, try to restore state as much as possible in
* case of failures. See ssam_serial_hub_poweroff() for more details on
* the hibernation process.
*/
WARN_ON(ssam_controller_resume(c));
status = ssam_ctrl_notif_d0_entry(c);
if (status)
ssam_err(c, "pm: D0-entry notification failed: %d\n", status);
ssam_notifier_restore_registered(c);
return 0;
}
static const struct dev_pm_ops ssam_serial_hub_pm_ops = {
.prepare = ssam_serial_hub_pm_prepare,
.complete = ssam_serial_hub_pm_complete,
.suspend = ssam_serial_hub_pm_suspend,
.resume = ssam_serial_hub_pm_resume,
.freeze = ssam_serial_hub_pm_freeze,
.thaw = ssam_serial_hub_pm_thaw,
.poweroff = ssam_serial_hub_pm_poweroff,
.restore = ssam_serial_hub_pm_restore,
};
#else /* CONFIG_PM_SLEEP */
static const struct dev_pm_ops ssam_serial_hub_pm_ops = { };
#endif /* CONFIG_PM_SLEEP */
/* -- Device/driver setup. -------------------------------------------------- */
static const struct acpi_gpio_params gpio_ssam_wakeup_int = { 0, 0, false };
static const struct acpi_gpio_params gpio_ssam_wakeup = { 1, 0, false };
static const struct acpi_gpio_mapping ssam_acpi_gpios[] = {
{ "ssam_wakeup-int-gpio", &gpio_ssam_wakeup_int, 1 },
{ "ssam_wakeup-gpio", &gpio_ssam_wakeup, 1 },
{ },
};
static int ssam_serial_hub_probe(struct serdev_device *serdev)
{
struct device *dev = &serdev->dev;
struct acpi_device *ssh = ACPI_COMPANION(dev);
struct ssam_controller *ctrl;
int status;
if (ssh) {
status = gpiod_count(dev, NULL);
if (status < 0)
return dev_err_probe(dev, status, "no GPIO found\n");
status = devm_acpi_dev_add_driver_gpios(dev, ssam_acpi_gpios);
if (status)
return status;
}
/* Allocate controller. */
ctrl = kzalloc_obj(*ctrl);
if (!ctrl)
return -ENOMEM;
/* Initialize controller. */
status = ssam_controller_init(ctrl, serdev);
if (status) {
dev_err_probe(dev, status, "failed to initialize ssam controller\n");
goto err_ctrl_init;
}
ssam_controller_lock(ctrl);
/* Set up serdev device. */
serdev_device_set_drvdata(serdev, ctrl);
serdev_device_set_client_ops(serdev, &ssam_serdev_ops);
status = serdev_device_open(serdev);
if (status) {
dev_err_probe(dev, status, "failed to open serdev device\n");
goto err_devopen;
}
status = ssam_serdev_setup(ssh, serdev);
if (status) {
dev_err_probe(dev, status, "failed to setup serdev\n");
goto err_devinit;
}
/* Start controller. */
status = ssam_controller_start(ctrl);
if (status)
goto err_devinit;
ssam_controller_unlock(ctrl);
/*
* Initial SAM requests: Log version and notify default/init power
* states.
*/
status = ssam_log_firmware_version(ctrl);
if (status) {
dev_err_probe(dev, status, "failed to get firmware version\n");
goto err_initrq;
}
status = ssam_ctrl_notif_d0_entry(ctrl);
if (status) {
dev_err_probe(dev, status, "D0-entry notification failed\n");
goto err_initrq;
}
status = ssam_ctrl_notif_display_on(ctrl);
if (status) {
dev_err_probe(dev, status, "display-on notification failed\n");
goto err_initrq;
}
status = sysfs_create_group(&dev->kobj, &ssam_sam_group);
if (status)
goto err_initrq;
/* Set up IRQ. */
status = ssam_irq_setup(ctrl);
if (status) {
dev_err_probe(dev, status, "failed to setup IRQ\n");
goto err_irq;
}
/* Finally, set main controller reference. */
status = ssam_try_set_controller(ctrl);
if (WARN_ON(status)) /* Currently, we're the only provider. */
goto err_mainref;
/*
* TODO: The EC can wake up the system via the associated GPIO interrupt
* in multiple situations. One of which is the remaining battery
* capacity falling below a certain threshold. Normally, we should
* use the device_init_wakeup function, however, the EC also seems
* to have other reasons for waking up the system and it seems
* that Windows has additional checks whether the system should be
* resumed. In short, this causes some spurious unwanted wake-ups.
* For now let's thus default power/wakeup to false.
*/
device_set_wakeup_capable(dev, true);
/*
* When using DT, we have to register the platform hub driver manually,
* as it can't be matched based on top-level board compatible (like it
* does the ACPI case).
*/
if (!ssh) {
struct platform_device *ph_pdev =
platform_device_register_simple("surface_aggregator_platform_hub",
0, NULL, 0);
if (IS_ERR(ph_pdev))
return dev_err_probe(dev, PTR_ERR(ph_pdev),
"Failed to register the platform hub driver\n");
}
if (ssh)
acpi_dev_clear_dependencies(ssh);
return 0;
err_mainref:
ssam_irq_free(ctrl);
err_irq:
sysfs_remove_group(&dev->kobj, &ssam_sam_group);
err_initrq:
ssam_controller_lock(ctrl);
ssam_controller_shutdown(ctrl);
err_devinit:
serdev_device_close(serdev);
err_devopen:
ssam_controller_destroy(ctrl);
ssam_controller_unlock(ctrl);
err_ctrl_init:
kfree(ctrl);
return status;
}
static void ssam_serial_hub_remove(struct serdev_device *serdev)
{
struct ssam_controller *ctrl = serdev_device_get_drvdata(serdev);
int status;
/* Clear static reference so that no one else can get a new one. */
ssam_clear_controller();
/* Disable and free IRQ. */
ssam_irq_free(ctrl);
sysfs_remove_group(&serdev->dev.kobj, &ssam_sam_group);
ssam_controller_lock(ctrl);
/* Remove all client devices. */
ssam_remove_clients(&serdev->dev);
/* Act as if suspending to silence events. */
status = ssam_ctrl_notif_display_off(ctrl);
if (status) {
dev_err(&serdev->dev, "display-off notification failed: %d\n",
status);
}
status = ssam_ctrl_notif_d0_exit(ctrl);
if (status) {
dev_err(&serdev->dev, "D0-exit notification failed: %d\n",
status);
}
/* Shut down controller and remove serdev device reference from it. */
ssam_controller_shutdown(ctrl);
/* Shut down actual transport. */
serdev_device_wait_until_sent(serdev, 0);
serdev_device_close(serdev);
/* Drop our controller reference. */
ssam_controller_unlock(ctrl);
ssam_controller_put(ctrl);
device_set_wakeup_capable(&serdev->dev, false);
}
static const struct acpi_device_id ssam_serial_hub_acpi_match[] = {
{ "MSHW0084", 0 },
{ },
};
MODULE_DEVICE_TABLE(acpi, ssam_serial_hub_acpi_match);
#ifdef CONFIG_OF
static const struct of_device_id ssam_serial_hub_of_match[] = {
{ .compatible = "microsoft,surface-sam", },
{ },
};
MODULE_DEVICE_TABLE(of, ssam_serial_hub_of_match);
#endif
static struct serdev_device_driver ssam_serial_hub = {
.probe = ssam_serial_hub_probe,
.remove = ssam_serial_hub_remove,
.shutdown = ssam_serial_hub_shutdown,
.driver = {
.name = "surface_serial_hub",
.acpi_match_table = ACPI_PTR(ssam_serial_hub_acpi_match),
.of_match_table = of_match_ptr(ssam_serial_hub_of_match),
.pm = &ssam_serial_hub_pm_ops,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
/* -- Module setup. --------------------------------------------------------- */
static int __init ssam_core_init(void)
{
int status;
status = ssam_bus_register();
if (status)
goto err_bus;
status = ssh_ctrl_packet_cache_init();
if (status)
goto err_cpkg;
status = ssam_event_item_cache_init();
if (status)
goto err_evitem;
status = serdev_device_driver_register(&ssam_serial_hub);
if (status)
goto err_register;
return 0;
err_register:
ssam_event_item_cache_destroy();
err_evitem:
ssh_ctrl_packet_cache_destroy();
err_cpkg:
ssam_bus_unregister();
err_bus:
return status;
}
subsys_initcall(ssam_core_init);
static void __exit ssam_core_exit(void)
{
serdev_device_driver_unregister(&ssam_serial_hub);
ssam_event_item_cache_destroy();
ssh_ctrl_packet_cache_destroy();
ssam_bus_unregister();
}
module_exit(ssam_core_exit);
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Subsystem and Surface Serial Hub driver for Surface System Aggregator Module");
MODULE_LICENSE("GPL");
@@ -0,0 +1,205 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* SSH message builder functions.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#ifndef _SURFACE_AGGREGATOR_SSH_MSGB_H
#define _SURFACE_AGGREGATOR_SSH_MSGB_H
#include <linux/unaligned.h>
#include <linux/types.h>
#include <linux/surface_aggregator/controller.h>
#include <linux/surface_aggregator/serial_hub.h>
/**
* struct msgbuf - Buffer struct to construct SSH messages.
* @begin: Pointer to the beginning of the allocated buffer space.
* @end: Pointer to the end (one past last element) of the allocated buffer
* space.
* @ptr: Pointer to the first free element in the buffer.
*/
struct msgbuf {
u8 *begin;
u8 *end;
u8 *ptr;
};
/**
* msgb_init() - Initialize the given message buffer struct.
* @msgb: The buffer struct to initialize
* @ptr: Pointer to the underlying memory by which the buffer will be backed.
* @cap: Size of the underlying memory.
*
* Initialize the given message buffer struct using the provided memory as
* backing.
*/
static inline void msgb_init(struct msgbuf *msgb, u8 *ptr, size_t cap)
{
msgb->begin = ptr;
msgb->end = ptr + cap;
msgb->ptr = ptr;
}
/**
* msgb_bytes_used() - Return the current number of bytes used in the buffer.
* @msgb: The message buffer.
*/
static inline size_t msgb_bytes_used(const struct msgbuf *msgb)
{
return msgb->ptr - msgb->begin;
}
static inline void __msgb_push_u8(struct msgbuf *msgb, u8 value)
{
*msgb->ptr = value;
msgb->ptr += sizeof(u8);
}
static inline void __msgb_push_u16(struct msgbuf *msgb, u16 value)
{
put_unaligned_le16(value, msgb->ptr);
msgb->ptr += sizeof(u16);
}
/**
* msgb_push_u16() - Push a u16 value to the buffer.
* @msgb: The message buffer.
* @value: The value to push to the buffer.
*/
static inline void msgb_push_u16(struct msgbuf *msgb, u16 value)
{
if (WARN_ON(msgb->ptr + sizeof(u16) > msgb->end))
return;
__msgb_push_u16(msgb, value);
}
/**
* msgb_push_syn() - Push SSH SYN bytes to the buffer.
* @msgb: The message buffer.
*/
static inline void msgb_push_syn(struct msgbuf *msgb)
{
msgb_push_u16(msgb, SSH_MSG_SYN);
}
/**
* msgb_push_buf() - Push raw data to the buffer.
* @msgb: The message buffer.
* @buf: The data to push to the buffer.
* @len: The length of the data to push to the buffer.
*/
static inline void msgb_push_buf(struct msgbuf *msgb, const u8 *buf, size_t len)
{
msgb->ptr = memcpy(msgb->ptr, buf, len) + len;
}
/**
* msgb_push_crc() - Compute CRC and push it to the buffer.
* @msgb: The message buffer.
* @buf: The data for which the CRC should be computed.
* @len: The length of the data for which the CRC should be computed.
*/
static inline void msgb_push_crc(struct msgbuf *msgb, const u8 *buf, size_t len)
{
msgb_push_u16(msgb, ssh_crc(buf, len));
}
/**
* msgb_push_frame() - Push a SSH message frame header to the buffer.
* @msgb: The message buffer
* @ty: The type of the frame.
* @len: The length of the payload of the frame.
* @seq: The sequence ID of the frame/packet.
*/
static inline void msgb_push_frame(struct msgbuf *msgb, u8 ty, u16 len, u8 seq)
{
u8 *const begin = msgb->ptr;
if (WARN_ON(msgb->ptr + sizeof(struct ssh_frame) > msgb->end))
return;
__msgb_push_u8(msgb, ty); /* Frame type. */
__msgb_push_u16(msgb, len); /* Frame payload length. */
__msgb_push_u8(msgb, seq); /* Frame sequence ID. */
msgb_push_crc(msgb, begin, msgb->ptr - begin);
}
/**
* msgb_push_ack() - Push a SSH ACK frame to the buffer.
* @msgb: The message buffer
* @seq: The sequence ID of the frame/packet to be ACKed.
*/
static inline void msgb_push_ack(struct msgbuf *msgb, u8 seq)
{
/* SYN. */
msgb_push_syn(msgb);
/* ACK-type frame + CRC. */
msgb_push_frame(msgb, SSH_FRAME_TYPE_ACK, 0x00, seq);
/* Payload CRC (ACK-type frames do not have a payload). */
msgb_push_crc(msgb, msgb->ptr, 0);
}
/**
* msgb_push_nak() - Push a SSH NAK frame to the buffer.
* @msgb: The message buffer
*/
static inline void msgb_push_nak(struct msgbuf *msgb)
{
/* SYN. */
msgb_push_syn(msgb);
/* NAK-type frame + CRC. */
msgb_push_frame(msgb, SSH_FRAME_TYPE_NAK, 0x00, 0x00);
/* Payload CRC (ACK-type frames do not have a payload). */
msgb_push_crc(msgb, msgb->ptr, 0);
}
/**
* msgb_push_cmd() - Push a SSH command frame with payload to the buffer.
* @msgb: The message buffer.
* @seq: The sequence ID (SEQ) of the frame/packet.
* @rqid: The request ID (RQID) of the request contained in the frame.
* @rqst: The request to wrap in the frame.
*/
static inline void msgb_push_cmd(struct msgbuf *msgb, u8 seq, u16 rqid,
const struct ssam_request *rqst)
{
const u8 type = SSH_FRAME_TYPE_DATA_SEQ;
u8 *cmd;
/* SYN. */
msgb_push_syn(msgb);
/* Command frame + CRC. */
msgb_push_frame(msgb, type, sizeof(struct ssh_command) + rqst->length, seq);
/* Frame payload: Command struct + payload. */
if (WARN_ON(msgb->ptr + sizeof(struct ssh_command) > msgb->end))
return;
cmd = msgb->ptr;
__msgb_push_u8(msgb, SSH_PLD_TYPE_CMD); /* Payload type. */
__msgb_push_u8(msgb, rqst->target_category); /* Target category. */
__msgb_push_u8(msgb, rqst->target_id); /* Target ID. */
__msgb_push_u8(msgb, SSAM_SSH_TID_HOST); /* Source ID. */
__msgb_push_u8(msgb, rqst->instance_id); /* Instance ID. */
__msgb_push_u16(msgb, rqid); /* Request ID. */
__msgb_push_u8(msgb, rqst->command_id); /* Command ID. */
/* Command payload. */
msgb_push_buf(msgb, rqst->payload, rqst->length);
/* CRC for command struct + payload. */
msgb_push_crc(msgb, cmd, msgb->ptr - cmd);
}
#endif /* _SURFACE_AGGREGATOR_SSH_MSGB_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,190 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* SSH packet transport layer.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#ifndef _SURFACE_AGGREGATOR_SSH_PACKET_LAYER_H
#define _SURFACE_AGGREGATOR_SSH_PACKET_LAYER_H
#include <linux/atomic.h>
#include <linux/kfifo.h>
#include <linux/ktime.h>
#include <linux/list.h>
#include <linux/serdev.h>
#include <linux/spinlock.h>
#include <linux/types.h>
#include <linux/wait.h>
#include <linux/workqueue.h>
#include <linux/surface_aggregator/serial_hub.h>
#include "ssh_parser.h"
/**
* enum ssh_ptl_state_flags - State-flags for &struct ssh_ptl.
*
* @SSH_PTL_SF_SHUTDOWN_BIT:
* Indicates that the packet transport layer has been shut down or is
* being shut down and should not accept any new packets/data.
*/
enum ssh_ptl_state_flags {
SSH_PTL_SF_SHUTDOWN_BIT,
};
/**
* struct ssh_ptl_ops - Callback operations for packet transport layer.
* @data_received: Function called when a data-packet has been received. Both,
* the packet layer on which the packet has been received and
* the packet's payload data are provided to this function.
*/
struct ssh_ptl_ops {
void (*data_received)(struct ssh_ptl *p, const struct ssam_span *data);
};
/**
* struct ssh_ptl - SSH packet transport layer.
* @serdev: Serial device providing the underlying data transport.
* @state: State(-flags) of the transport layer.
* @queue: Packet submission queue.
* @queue.lock: Lock for modifying the packet submission queue.
* @queue.head: List-head of the packet submission queue.
* @pending: Set/list of pending packets.
* @pending.lock: Lock for modifying the pending set.
* @pending.head: List-head of the pending set/list.
* @pending.count: Number of currently pending packets.
* @tx: Transmitter subsystem.
* @tx.running: Flag indicating (desired) transmitter thread state.
* @tx.thread: Transmitter thread.
* @tx.thread_cplt_tx: Completion for transmitter thread waiting on transfer.
* @tx.thread_cplt_pkt: Completion for transmitter thread waiting on packets.
* @tx.packet_wq: Waitqueue-head for packet transmit completion.
* @rx: Receiver subsystem.
* @rx.thread: Receiver thread.
* @rx.wq: Waitqueue-head for receiver thread.
* @rx.fifo: Buffer for receiving data/pushing data to receiver thread.
* @rx.buf: Buffer for evaluating data on receiver thread.
* @rx.blocked: List of recent/blocked sequence IDs to detect retransmission.
* @rx.blocked.seqs: Array of blocked sequence IDs.
* @rx.blocked.offset: Offset indicating where a new ID should be inserted.
* @rtx_timeout: Retransmission timeout subsystem.
* @rtx_timeout.lock: Lock for modifying the retransmission timeout reaper.
* @rtx_timeout.timeout: Timeout interval for retransmission.
* @rtx_timeout.expires: Time specifying when the reaper work is next scheduled.
* @rtx_timeout.reaper: Work performing timeout checks and subsequent actions.
* @ops: Packet layer operations.
*/
struct ssh_ptl {
struct serdev_device *serdev;
unsigned long state;
struct {
spinlock_t lock;
struct list_head head;
} queue;
struct {
spinlock_t lock;
struct list_head head;
atomic_t count;
} pending;
struct {
atomic_t running;
struct task_struct *thread;
struct completion thread_cplt_tx;
struct completion thread_cplt_pkt;
struct wait_queue_head packet_wq;
} tx;
struct {
struct task_struct *thread;
struct wait_queue_head wq;
struct kfifo fifo;
struct sshp_buf buf;
struct {
u16 seqs[8];
u16 offset;
} blocked;
} rx;
struct {
spinlock_t lock;
ktime_t timeout;
ktime_t expires;
struct delayed_work reaper;
} rtx_timeout;
struct ssh_ptl_ops ops;
};
#define __ssam_prcond(func, p, fmt, ...) \
do { \
typeof(p) __p = (p); \
\
if (__p) \
func(__p, fmt, ##__VA_ARGS__); \
} while (0)
#define ptl_dbg(p, fmt, ...) dev_dbg(&(p)->serdev->dev, fmt, ##__VA_ARGS__)
#define ptl_info(p, fmt, ...) dev_info(&(p)->serdev->dev, fmt, ##__VA_ARGS__)
#define ptl_warn(p, fmt, ...) dev_warn(&(p)->serdev->dev, fmt, ##__VA_ARGS__)
#define ptl_err(p, fmt, ...) dev_err(&(p)->serdev->dev, fmt, ##__VA_ARGS__)
#define ptl_dbg_cond(p, fmt, ...) __ssam_prcond(ptl_dbg, p, fmt, ##__VA_ARGS__)
#define to_ssh_ptl(ptr, member) \
container_of(ptr, struct ssh_ptl, member)
int ssh_ptl_init(struct ssh_ptl *ptl, struct serdev_device *serdev,
struct ssh_ptl_ops *ops);
void ssh_ptl_destroy(struct ssh_ptl *ptl);
/**
* ssh_ptl_get_device() - Get device associated with packet transport layer.
* @ptl: The packet transport layer.
*
* Return: Returns the device on which the given packet transport layer builds
* upon.
*/
static inline struct device *ssh_ptl_get_device(struct ssh_ptl *ptl)
{
return ptl->serdev ? &ptl->serdev->dev : NULL;
}
int ssh_ptl_tx_start(struct ssh_ptl *ptl);
int ssh_ptl_tx_stop(struct ssh_ptl *ptl);
int ssh_ptl_rx_start(struct ssh_ptl *ptl);
int ssh_ptl_rx_stop(struct ssh_ptl *ptl);
void ssh_ptl_shutdown(struct ssh_ptl *ptl);
int ssh_ptl_submit(struct ssh_ptl *ptl, struct ssh_packet *p);
void ssh_ptl_cancel(struct ssh_packet *p);
ssize_t ssh_ptl_rx_rcvbuf(struct ssh_ptl *ptl, const u8 *buf, size_t n);
/**
* ssh_ptl_tx_wakeup_transfer() - Wake up packet transmitter thread for
* transfer.
* @ptl: The packet transport layer.
*
* Wakes up the packet transmitter thread, notifying it that the underlying
* transport has more space for data to be transmitted. If the packet
* transport layer has been shut down, calls to this function will be ignored.
*/
static inline void ssh_ptl_tx_wakeup_transfer(struct ssh_ptl *ptl)
{
if (test_bit(SSH_PTL_SF_SHUTDOWN_BIT, &ptl->state))
return;
complete(&ptl->tx.thread_cplt_tx);
}
void ssh_packet_init(struct ssh_packet *packet, unsigned long type,
u8 priority, const struct ssh_packet_ops *ops);
int ssh_ctrl_packet_cache_init(void);
void ssh_ctrl_packet_cache_destroy(void);
#endif /* _SURFACE_AGGREGATOR_SSH_PACKET_LAYER_H */
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* SSH message parser.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/unaligned.h>
#include <linux/compiler.h>
#include <linux/device.h>
#include <linux/types.h>
#include <linux/surface_aggregator/serial_hub.h>
#include "ssh_parser.h"
/**
* sshp_validate_crc() - Validate a CRC in raw message data.
* @src: The span of data over which the CRC should be computed.
* @crc: The pointer to the expected u16 CRC value.
*
* Computes the CRC of the provided data span (@src), compares it to the CRC
* stored at the given address (@crc), and returns the result of this
* comparison, i.e. %true if equal. This function is intended to run on raw
* input/message data.
*
* Return: Returns %true if the computed CRC matches the stored CRC, %false
* otherwise.
*/
static bool sshp_validate_crc(const struct ssam_span *src, const u8 *crc)
{
u16 actual = ssh_crc(src->ptr, src->len);
u16 expected = get_unaligned_le16(crc);
return actual == expected;
}
/**
* sshp_starts_with_syn() - Check if the given data starts with SSH SYN bytes.
* @src: The data span to check the start of.
*/
static bool sshp_starts_with_syn(const struct ssam_span *src)
{
return src->len >= 2 && get_unaligned_le16(src->ptr) == SSH_MSG_SYN;
}
/**
* sshp_find_syn() - Find SSH SYN bytes in the given data span.
* @src: The data span to search in.
* @rem: The span (output) indicating the remaining data, starting with SSH
* SYN bytes, if found.
*
* Search for SSH SYN bytes in the given source span. If found, set the @rem
* span to the remaining data, starting with the first SYN bytes and capped by
* the source span length, and return %true. This function does not copy any
* data, but rather only sets pointers to the respective start addresses and
* length values.
*
* If no SSH SYN bytes could be found, set the @rem span to the zero-length
* span at the end of the source span and return %false.
*
* If partial SSH SYN bytes could be found at the end of the source span, set
* the @rem span to cover these partial SYN bytes, capped by the end of the
* source span, and return %false. This function should then be re-run once
* more data is available.
*
* Return: Returns %true if a complete SSH SYN sequence could be found,
* %false otherwise.
*/
bool sshp_find_syn(const struct ssam_span *src, struct ssam_span *rem)
{
size_t i;
for (i = 0; i < src->len - 1; i++) {
if (likely(get_unaligned_le16(src->ptr + i) == SSH_MSG_SYN)) {
rem->ptr = src->ptr + i;
rem->len = src->len - i;
return true;
}
}
if (unlikely(src->ptr[src->len - 1] == (SSH_MSG_SYN & 0xff))) {
rem->ptr = src->ptr + src->len - 1;
rem->len = 1;
return false;
}
rem->ptr = src->ptr + src->len;
rem->len = 0;
return false;
}
/**
* sshp_parse_frame() - Parse SSH frame.
* @dev: The device used for logging.
* @source: The source to parse from.
* @frame: The parsed frame (output).
* @payload: The parsed payload (output).
* @maxlen: The maximum supported message length.
*
* Parses and validates a SSH frame, including its payload, from the given
* source. Sets the provided @frame pointer to the start of the frame and
* writes the limits of the frame payload to the provided @payload span
* pointer.
*
* This function does not copy any data, but rather only validates the message
* data and sets pointers (and length values) to indicate the respective parts.
*
* If no complete SSH frame could be found, the frame pointer will be set to
* the %NULL pointer and the payload span will be set to the null span (start
* pointer %NULL, size zero).
*
* Return: Returns zero on success or if the frame is incomplete, %-ENOMSG if
* the start of the message is invalid, %-EBADMSG if any (frame-header or
* payload) CRC is invalid, or %-EMSGSIZE if the SSH message is bigger than
* the maximum message length specified in the @maxlen parameter.
*/
int sshp_parse_frame(const struct device *dev, const struct ssam_span *source,
struct ssh_frame **frame, struct ssam_span *payload,
size_t maxlen)
{
struct ssam_span sf;
struct ssam_span sp;
/* Initialize output. */
*frame = NULL;
payload->ptr = NULL;
payload->len = 0;
if (!sshp_starts_with_syn(source)) {
dev_warn(dev, "rx: parser: invalid start of frame\n");
return -ENOMSG;
}
/* Check for minimum packet length. */
if (unlikely(source->len < SSH_MESSAGE_LENGTH(0))) {
dev_dbg(dev, "rx: parser: not enough data for frame\n");
return 0;
}
/* Pin down frame. */
sf.ptr = source->ptr + sizeof(u16);
sf.len = sizeof(struct ssh_frame);
/* Validate frame CRC. */
if (unlikely(!sshp_validate_crc(&sf, sf.ptr + sf.len))) {
dev_warn(dev, "rx: parser: invalid frame CRC\n");
return -EBADMSG;
}
/* Ensure packet does not exceed maximum length. */
sp.len = get_unaligned_le16(&((struct ssh_frame *)sf.ptr)->len);
if (unlikely(SSH_MESSAGE_LENGTH(sp.len) > maxlen)) {
dev_warn(dev, "rx: parser: frame too large: %llu bytes\n",
SSH_MESSAGE_LENGTH(sp.len));
return -EMSGSIZE;
}
/* Pin down payload. */
sp.ptr = sf.ptr + sf.len + sizeof(u16);
/* Check for frame + payload length. */
if (source->len < SSH_MESSAGE_LENGTH(sp.len)) {
dev_dbg(dev, "rx: parser: not enough data for payload\n");
return 0;
}
/* Validate payload CRC. */
if (unlikely(!sshp_validate_crc(&sp, sp.ptr + sp.len))) {
dev_warn(dev, "rx: parser: invalid payload CRC\n");
return -EBADMSG;
}
*frame = (struct ssh_frame *)sf.ptr;
*payload = sp;
dev_dbg(dev, "rx: parser: valid frame found (type: %#04x, len: %u)\n",
(*frame)->type, (*frame)->len);
return 0;
}
/**
* sshp_parse_command() - Parse SSH command frame payload.
* @dev: The device used for logging.
* @source: The source to parse from.
* @command: The parsed command (output).
* @command_data: The parsed command data/payload (output).
*
* Parses and validates a SSH command frame payload. Sets the @command pointer
* to the command header and the @command_data span to the command data (i.e.
* payload of the command). This will result in a zero-length span if the
* command does not have any associated data/payload. This function does not
* check the frame-payload-type field, which should be checked by the caller
* before calling this function.
*
* The @source parameter should be the complete frame payload, e.g. returned
* by the sshp_parse_frame() command.
*
* This function does not copy any data, but rather only validates the frame
* payload data and sets pointers (and length values) to indicate the
* respective parts.
*
* Return: Returns zero on success or %-ENOMSG if @source does not represent a
* valid command-type frame payload, i.e. is too short.
*/
int sshp_parse_command(const struct device *dev, const struct ssam_span *source,
struct ssh_command **command,
struct ssam_span *command_data)
{
/* Check for minimum length. */
if (unlikely(source->len < sizeof(struct ssh_command))) {
*command = NULL;
command_data->ptr = NULL;
command_data->len = 0;
dev_err(dev, "rx: parser: command payload is too short\n");
return -ENOMSG;
}
*command = (struct ssh_command *)source->ptr;
command_data->ptr = source->ptr + sizeof(struct ssh_command);
command_data->len = source->len - sizeof(struct ssh_command);
dev_dbg(dev, "rx: parser: valid command found (tc: %#04x, cid: %#04x)\n",
(*command)->tc, (*command)->cid);
return 0;
}
@@ -0,0 +1,154 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* SSH message parser.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#ifndef _SURFACE_AGGREGATOR_SSH_PARSER_H
#define _SURFACE_AGGREGATOR_SSH_PARSER_H
#include <linux/device.h>
#include <linux/kfifo.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/surface_aggregator/serial_hub.h>
/**
* struct sshp_buf - Parser buffer for SSH messages.
* @ptr: Pointer to the beginning of the buffer.
* @len: Number of bytes used in the buffer.
* @cap: Maximum capacity of the buffer.
*/
struct sshp_buf {
u8 *ptr;
size_t len;
size_t cap;
};
/**
* sshp_buf_init() - Initialize a SSH parser buffer.
* @buf: The buffer to initialize.
* @ptr: The memory backing the buffer.
* @cap: The length of the memory backing the buffer, i.e. its capacity.
*
* Initializes the buffer with the given memory as backing and set its used
* length to zero.
*/
static inline void sshp_buf_init(struct sshp_buf *buf, u8 *ptr, size_t cap)
{
buf->ptr = ptr;
buf->len = 0;
buf->cap = cap;
}
/**
* sshp_buf_alloc() - Allocate and initialize a SSH parser buffer.
* @buf: The buffer to initialize/allocate to.
* @cap: The desired capacity of the buffer.
* @flags: The flags used for allocating the memory.
*
* Allocates @cap bytes and initializes the provided buffer struct with the
* allocated memory.
*
* Return: Returns zero on success and %-ENOMEM if allocation failed.
*/
static inline int sshp_buf_alloc(struct sshp_buf *buf, size_t cap, gfp_t flags)
{
u8 *ptr;
ptr = kzalloc(cap, flags);
if (!ptr)
return -ENOMEM;
sshp_buf_init(buf, ptr, cap);
return 0;
}
/**
* sshp_buf_free() - Free a SSH parser buffer.
* @buf: The buffer to free.
*
* Frees a SSH parser buffer by freeing the memory backing it and then
* resetting its pointer to %NULL and length and capacity to zero. Intended to
* free a buffer previously allocated with sshp_buf_alloc().
*/
static inline void sshp_buf_free(struct sshp_buf *buf)
{
kfree(buf->ptr);
buf->ptr = NULL;
buf->len = 0;
buf->cap = 0;
}
/**
* sshp_buf_drop() - Drop data from the beginning of the buffer.
* @buf: The buffer to drop data from.
* @n: The number of bytes to drop.
*
* Drops the first @n bytes from the buffer. Re-aligns any remaining data to
* the beginning of the buffer.
*/
static inline void sshp_buf_drop(struct sshp_buf *buf, size_t n)
{
memmove(buf->ptr, buf->ptr + n, buf->len - n);
buf->len -= n;
}
/**
* sshp_buf_read_from_fifo() - Transfer data from a fifo to the buffer.
* @buf: The buffer to write the data into.
* @fifo: The fifo to read the data from.
*
* Transfers the data contained in the fifo to the buffer, removing it from
* the fifo. This function will try to transfer as much data as possible,
* limited either by the remaining space in the buffer or by the number of
* bytes available in the fifo.
*
* Return: Returns the number of bytes transferred.
*/
static inline size_t sshp_buf_read_from_fifo(struct sshp_buf *buf,
struct kfifo *fifo)
{
size_t n;
n = kfifo_out(fifo, buf->ptr + buf->len, buf->cap - buf->len);
buf->len += n;
return n;
}
/**
* sshp_buf_span_from() - Initialize a span from the given buffer and offset.
* @buf: The buffer to create the span from.
* @offset: The offset in the buffer at which the span should start.
* @span: The span to initialize (output).
*
* Initializes the provided span to point to the memory at the given offset in
* the buffer, with the length of the span being capped by the number of bytes
* used in the buffer after the offset (i.e. bytes remaining after the
* offset).
*
* Warning: This function does not validate that @offset is less than or equal
* to the number of bytes used in the buffer or the buffer capacity. This must
* be guaranteed by the caller.
*/
static inline void sshp_buf_span_from(struct sshp_buf *buf, size_t offset,
struct ssam_span *span)
{
span->ptr = buf->ptr + offset;
span->len = buf->len - offset;
}
bool sshp_find_syn(const struct ssam_span *src, struct ssam_span *rem);
int sshp_parse_frame(const struct device *dev, const struct ssam_span *source,
struct ssh_frame **frame, struct ssam_span *payload,
size_t maxlen);
int sshp_parse_command(const struct device *dev, const struct ssam_span *source,
struct ssh_command **command,
struct ssam_span *command_data);
#endif /* _SURFACE_AGGREGATOR_SSH_PARSER_h */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* SSH request transport layer.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#ifndef _SURFACE_AGGREGATOR_SSH_REQUEST_LAYER_H
#define _SURFACE_AGGREGATOR_SSH_REQUEST_LAYER_H
#include <linux/atomic.h>
#include <linux/ktime.h>
#include <linux/list.h>
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <linux/surface_aggregator/serial_hub.h>
#include <linux/surface_aggregator/controller.h>
#include "ssh_packet_layer.h"
/**
* enum ssh_rtl_state_flags - State-flags for &struct ssh_rtl.
*
* @SSH_RTL_SF_SHUTDOWN_BIT:
* Indicates that the request transport layer has been shut down or is
* being shut down and should not accept any new requests.
*/
enum ssh_rtl_state_flags {
SSH_RTL_SF_SHUTDOWN_BIT,
};
/**
* struct ssh_rtl_ops - Callback operations for request transport layer.
* @handle_event: Function called when a SSH event has been received. The
* specified function takes the request layer, received command
* struct, and corresponding payload as arguments. If the event
* has no payload, the payload span is empty (not %NULL).
*/
struct ssh_rtl_ops {
void (*handle_event)(struct ssh_rtl *rtl, const struct ssh_command *cmd,
const struct ssam_span *data);
};
/**
* struct ssh_rtl - SSH request transport layer.
* @ptl: Underlying packet transport layer.
* @state: State(-flags) of the transport layer.
* @queue: Request submission queue.
* @queue.lock: Lock for modifying the request submission queue.
* @queue.head: List-head of the request submission queue.
* @pending: Set/list of pending requests.
* @pending.lock: Lock for modifying the request set.
* @pending.head: List-head of the pending set/list.
* @pending.count: Number of currently pending requests.
* @tx: Transmitter subsystem.
* @tx.work: Transmitter work item.
* @rtx_timeout: Retransmission timeout subsystem.
* @rtx_timeout.lock: Lock for modifying the retransmission timeout reaper.
* @rtx_timeout.timeout: Timeout interval for retransmission.
* @rtx_timeout.expires: Time specifying when the reaper work is next scheduled.
* @rtx_timeout.reaper: Work performing timeout checks and subsequent actions.
* @ops: Request layer operations.
*/
struct ssh_rtl {
struct ssh_ptl ptl;
unsigned long state;
struct {
spinlock_t lock;
struct list_head head;
} queue;
struct {
spinlock_t lock;
struct list_head head;
atomic_t count;
} pending;
struct {
struct work_struct work;
} tx;
struct {
spinlock_t lock;
ktime_t timeout;
ktime_t expires;
struct delayed_work reaper;
} rtx_timeout;
struct ssh_rtl_ops ops;
};
#define rtl_dbg(r, fmt, ...) ptl_dbg(&(r)->ptl, fmt, ##__VA_ARGS__)
#define rtl_info(p, fmt, ...) ptl_info(&(p)->ptl, fmt, ##__VA_ARGS__)
#define rtl_warn(r, fmt, ...) ptl_warn(&(r)->ptl, fmt, ##__VA_ARGS__)
#define rtl_err(r, fmt, ...) ptl_err(&(r)->ptl, fmt, ##__VA_ARGS__)
#define rtl_dbg_cond(r, fmt, ...) __ssam_prcond(rtl_dbg, r, fmt, ##__VA_ARGS__)
#define to_ssh_rtl(ptr, member) \
container_of(ptr, struct ssh_rtl, member)
/**
* ssh_rtl_get_device() - Get device associated with request transport layer.
* @rtl: The request transport layer.
*
* Return: Returns the device on which the given request transport layer
* builds upon.
*/
static inline struct device *ssh_rtl_get_device(struct ssh_rtl *rtl)
{
return ssh_ptl_get_device(&rtl->ptl);
}
/**
* ssh_request_rtl() - Get request transport layer associated with request.
* @rqst: The request to get the request transport layer reference for.
*
* Return: Returns the &struct ssh_rtl associated with the given SSH request.
*/
static inline struct ssh_rtl *ssh_request_rtl(struct ssh_request *rqst)
{
struct ssh_ptl *ptl;
ptl = READ_ONCE(rqst->packet.ptl);
return likely(ptl) ? to_ssh_rtl(ptl, ptl) : NULL;
}
int ssh_rtl_submit(struct ssh_rtl *rtl, struct ssh_request *rqst);
bool ssh_rtl_cancel(struct ssh_request *rqst, bool pending);
int ssh_rtl_init(struct ssh_rtl *rtl, struct serdev_device *serdev,
const struct ssh_rtl_ops *ops);
int ssh_rtl_start(struct ssh_rtl *rtl);
int ssh_rtl_flush(struct ssh_rtl *rtl, unsigned long timeout);
void ssh_rtl_shutdown(struct ssh_rtl *rtl);
void ssh_rtl_destroy(struct ssh_rtl *rtl);
int ssh_request_init(struct ssh_request *rqst, enum ssam_request_flags flags,
const struct ssh_request_ops *ops);
#endif /* _SURFACE_AGGREGATOR_SSH_REQUEST_LAYER_H */
@@ -0,0 +1,703 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Trace points for SSAM/SSH.
*
* Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#undef TRACE_SYSTEM
#define TRACE_SYSTEM surface_aggregator
#if !defined(_SURFACE_AGGREGATOR_TRACE_H) || defined(TRACE_HEADER_MULTI_READ)
#define _SURFACE_AGGREGATOR_TRACE_H
#include <linux/surface_aggregator/serial_hub.h>
#include <linux/unaligned.h>
#include <linux/tracepoint.h>
TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_DATA_SEQ);
TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_DATA_NSQ);
TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_ACK);
TRACE_DEFINE_ENUM(SSH_FRAME_TYPE_NAK);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_LOCKED_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_QUEUED_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_PENDING_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_TRANSMITTING_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_TRANSMITTED_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_ACKED_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_CANCELED_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_SF_COMPLETED_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_TY_FLUSH_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_TY_SEQUENCED_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_TY_BLOCKING_BIT);
TRACE_DEFINE_ENUM(SSH_PACKET_FLAGS_SF_MASK);
TRACE_DEFINE_ENUM(SSH_PACKET_FLAGS_TY_MASK);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_LOCKED_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_QUEUED_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_PENDING_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_TRANSMITTING_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_TRANSMITTED_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_RSPRCVD_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_CANCELED_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_SF_COMPLETED_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_TY_FLUSH_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_TY_HAS_RESPONSE_BIT);
TRACE_DEFINE_ENUM(SSH_REQUEST_FLAGS_SF_MASK);
TRACE_DEFINE_ENUM(SSH_REQUEST_FLAGS_TY_MASK);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SAM);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_BAT);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_TMP);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_PMC);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_FAN);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_PoM);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_DBG);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_KBD);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_FWU);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_UNI);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_LPC);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_TCL);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SFL);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_KIP);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_EXT);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_BLD);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_BAS);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SEN);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SRQ);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_MCU);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_HID);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_TCH);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_BKL);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_TAM);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_ACC0);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_UFI);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_USC);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_PEN);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_VID);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_AUD);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SMC);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_KPD);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_REG);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SPT);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SYS);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_ACC1);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_SHB);
TRACE_DEFINE_ENUM(SSAM_SSH_TC_POS);
#define SSAM_PTR_UID_LEN 9
#define SSAM_U8_FIELD_NOT_APPLICABLE ((u16)-1)
#define SSAM_SEQ_NOT_APPLICABLE ((u16)-1)
#define SSAM_RQID_NOT_APPLICABLE ((u32)-1)
#define SSAM_SSH_TC_NOT_APPLICABLE 0
#define SSAM_SSH_TID_NOT_APPLICABLE ((u8)-1)
#ifndef _SURFACE_AGGREGATOR_TRACE_HELPERS
#define _SURFACE_AGGREGATOR_TRACE_HELPERS
/**
* ssam_trace_ptr_uid() - Convert the pointer to a non-pointer UID string.
* @ptr: The pointer to convert.
* @uid_str: A buffer of length SSAM_PTR_UID_LEN where the UID will be stored.
*
* Converts the given pointer into a UID string that is safe to be shared
* with userspace and logs, i.e. doesn't give away the real memory location.
*/
static inline void ssam_trace_ptr_uid(const void *ptr, char *uid_str)
{
char buf[2 * sizeof(void *) + 1];
BUILD_BUG_ON(ARRAY_SIZE(buf) < SSAM_PTR_UID_LEN);
snprintf(buf, ARRAY_SIZE(buf), "%p", ptr);
memcpy(uid_str, &buf[ARRAY_SIZE(buf) - SSAM_PTR_UID_LEN],
SSAM_PTR_UID_LEN);
}
/**
* ssam_trace_get_packet_seq() - Read the packet's sequence ID.
* @p: The packet.
*
* Return: Returns the packet's sequence ID (SEQ) field if present, or
* %SSAM_SEQ_NOT_APPLICABLE if not (e.g. flush packet).
*/
static inline u16 ssam_trace_get_packet_seq(const struct ssh_packet *p)
{
if (!p->data.ptr || p->data.len < SSH_MESSAGE_LENGTH(0))
return SSAM_SEQ_NOT_APPLICABLE;
return p->data.ptr[SSH_MSGOFFSET_FRAME(seq)];
}
/**
* ssam_trace_get_request_id() - Read the packet's request ID.
* @p: The packet.
*
* Return: Returns the packet's request ID (RQID) field if the packet
* represents a request with command data, or %SSAM_RQID_NOT_APPLICABLE if not
* (e.g. flush request, control packet).
*/
static inline u32 ssam_trace_get_request_id(const struct ssh_packet *p)
{
if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0))
return SSAM_RQID_NOT_APPLICABLE;
return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(rqid)]);
}
/**
* ssam_trace_get_request_tid() - Read the packet's request target ID.
* @p: The packet.
*
* Return: Returns the packet's request target ID (TID) field if the packet
* represents a request with command data, or %SSAM_SSH_TID_NOT_APPLICABLE
* if not (e.g. flush request, control packet).
*/
static inline u32 ssam_trace_get_request_tid(const struct ssh_packet *p)
{
if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0))
return SSAM_SSH_TID_NOT_APPLICABLE;
return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(tid)]);
}
/**
* ssam_trace_get_request_sid() - Read the packet's request source ID.
* @p: The packet.
*
* Return: Returns the packet's request source ID (SID) field if the packet
* represents a request with command data, or %SSAM_SSH_TID_NOT_APPLICABLE
* if not (e.g. flush request, control packet).
*/
static inline u32 ssam_trace_get_request_sid(const struct ssh_packet *p)
{
if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0))
return SSAM_SSH_TID_NOT_APPLICABLE;
return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(sid)]);
}
/**
* ssam_trace_get_request_tc() - Read the packet's request target category.
* @p: The packet.
*
* Return: Returns the packet's request target category (TC) field if the
* packet represents a request with command data, or %SSAM_SSH_TC_NOT_APPLICABLE
* if not (e.g. flush request, control packet).
*/
static inline u32 ssam_trace_get_request_tc(const struct ssh_packet *p)
{
if (!p->data.ptr || p->data.len < SSH_COMMAND_MESSAGE_LENGTH(0))
return SSAM_SSH_TC_NOT_APPLICABLE;
return get_unaligned_le16(&p->data.ptr[SSH_MSGOFFSET_COMMAND(tc)]);
}
#endif /* _SURFACE_AGGREGATOR_TRACE_HELPERS */
#define ssam_trace_get_command_field_u8(packet, field) \
((!(packet) || (packet)->data.len < SSH_COMMAND_MESSAGE_LENGTH(0)) \
? 0 : (packet)->data.ptr[SSH_MSGOFFSET_COMMAND(field)])
#define ssam_show_generic_u8_field(value) \
__print_symbolic(value, \
{ SSAM_U8_FIELD_NOT_APPLICABLE, "N/A" } \
)
#define ssam_show_frame_type(ty) \
__print_symbolic(ty, \
{ SSH_FRAME_TYPE_DATA_SEQ, "DSEQ" }, \
{ SSH_FRAME_TYPE_DATA_NSQ, "DNSQ" }, \
{ SSH_FRAME_TYPE_ACK, "ACK" }, \
{ SSH_FRAME_TYPE_NAK, "NAK" } \
)
#define ssam_show_packet_type(type) \
__print_flags(flags & SSH_PACKET_FLAGS_TY_MASK, "", \
{ BIT(SSH_PACKET_TY_FLUSH_BIT), "F" }, \
{ BIT(SSH_PACKET_TY_SEQUENCED_BIT), "S" }, \
{ BIT(SSH_PACKET_TY_BLOCKING_BIT), "B" } \
)
#define ssam_show_packet_state(state) \
__print_flags(flags & SSH_PACKET_FLAGS_SF_MASK, "", \
{ BIT(SSH_PACKET_SF_LOCKED_BIT), "L" }, \
{ BIT(SSH_PACKET_SF_QUEUED_BIT), "Q" }, \
{ BIT(SSH_PACKET_SF_PENDING_BIT), "P" }, \
{ BIT(SSH_PACKET_SF_TRANSMITTING_BIT), "S" }, \
{ BIT(SSH_PACKET_SF_TRANSMITTED_BIT), "T" }, \
{ BIT(SSH_PACKET_SF_ACKED_BIT), "A" }, \
{ BIT(SSH_PACKET_SF_CANCELED_BIT), "C" }, \
{ BIT(SSH_PACKET_SF_COMPLETED_BIT), "F" } \
)
#define ssam_show_packet_seq(seq) \
__print_symbolic(seq, \
{ SSAM_SEQ_NOT_APPLICABLE, "N/A" } \
)
#define ssam_show_request_type(flags) \
__print_flags((flags) & SSH_REQUEST_FLAGS_TY_MASK, "", \
{ BIT(SSH_REQUEST_TY_FLUSH_BIT), "F" }, \
{ BIT(SSH_REQUEST_TY_HAS_RESPONSE_BIT), "R" } \
)
#define ssam_show_request_state(flags) \
__print_flags((flags) & SSH_REQUEST_FLAGS_SF_MASK, "", \
{ BIT(SSH_REQUEST_SF_LOCKED_BIT), "L" }, \
{ BIT(SSH_REQUEST_SF_QUEUED_BIT), "Q" }, \
{ BIT(SSH_REQUEST_SF_PENDING_BIT), "P" }, \
{ BIT(SSH_REQUEST_SF_TRANSMITTING_BIT), "S" }, \
{ BIT(SSH_REQUEST_SF_TRANSMITTED_BIT), "T" }, \
{ BIT(SSH_REQUEST_SF_RSPRCVD_BIT), "A" }, \
{ BIT(SSH_REQUEST_SF_CANCELED_BIT), "C" }, \
{ BIT(SSH_REQUEST_SF_COMPLETED_BIT), "F" } \
)
#define ssam_show_request_id(rqid) \
__print_symbolic(rqid, \
{ SSAM_RQID_NOT_APPLICABLE, "N/A" } \
)
#define ssam_show_ssh_tid(tid) \
__print_symbolic(tid, \
{ SSAM_SSH_TID_NOT_APPLICABLE, "N/A" }, \
{ SSAM_SSH_TID_HOST, "Host" }, \
{ SSAM_SSH_TID_SAM, "SAM" }, \
{ SSAM_SSH_TID_KIP, "KIP" }, \
{ SSAM_SSH_TID_DEBUG, "Debug" }, \
{ SSAM_SSH_TID_SURFLINK, "SurfLink" } \
)
#define ssam_show_ssh_tc(tc) \
__print_symbolic(tc, \
{ SSAM_SSH_TC_NOT_APPLICABLE, "N/A" }, \
{ SSAM_SSH_TC_SAM, "SAM" }, \
{ SSAM_SSH_TC_BAT, "BAT" }, \
{ SSAM_SSH_TC_TMP, "TMP" }, \
{ SSAM_SSH_TC_PMC, "PMC" }, \
{ SSAM_SSH_TC_FAN, "FAN" }, \
{ SSAM_SSH_TC_PoM, "PoM" }, \
{ SSAM_SSH_TC_DBG, "DBG" }, \
{ SSAM_SSH_TC_KBD, "KBD" }, \
{ SSAM_SSH_TC_FWU, "FWU" }, \
{ SSAM_SSH_TC_UNI, "UNI" }, \
{ SSAM_SSH_TC_LPC, "LPC" }, \
{ SSAM_SSH_TC_TCL, "TCL" }, \
{ SSAM_SSH_TC_SFL, "SFL" }, \
{ SSAM_SSH_TC_KIP, "KIP" }, \
{ SSAM_SSH_TC_EXT, "EXT" }, \
{ SSAM_SSH_TC_BLD, "BLD" }, \
{ SSAM_SSH_TC_BAS, "BAS" }, \
{ SSAM_SSH_TC_SEN, "SEN" }, \
{ SSAM_SSH_TC_SRQ, "SRQ" }, \
{ SSAM_SSH_TC_MCU, "MCU" }, \
{ SSAM_SSH_TC_HID, "HID" }, \
{ SSAM_SSH_TC_TCH, "TCH" }, \
{ SSAM_SSH_TC_BKL, "BKL" }, \
{ SSAM_SSH_TC_TAM, "TAM" }, \
{ SSAM_SSH_TC_ACC0, "ACC0" }, \
{ SSAM_SSH_TC_UFI, "UFI" }, \
{ SSAM_SSH_TC_USC, "USC" }, \
{ SSAM_SSH_TC_PEN, "PEN" }, \
{ SSAM_SSH_TC_VID, "VID" }, \
{ SSAM_SSH_TC_AUD, "AUD" }, \
{ SSAM_SSH_TC_SMC, "SMC" }, \
{ SSAM_SSH_TC_KPD, "KPD" }, \
{ SSAM_SSH_TC_REG, "REG" }, \
{ SSAM_SSH_TC_SPT, "SPT" }, \
{ SSAM_SSH_TC_SYS, "SYS" }, \
{ SSAM_SSH_TC_ACC1, "ACC1" }, \
{ SSAM_SSH_TC_SHB, "SMB" }, \
{ SSAM_SSH_TC_POS, "POS" } \
)
DECLARE_EVENT_CLASS(ssam_frame_class,
TP_PROTO(const struct ssh_frame *frame),
TP_ARGS(frame),
TP_STRUCT__entry(
__field(u8, type)
__field(u8, seq)
__field(u16, len)
),
TP_fast_assign(
__entry->type = frame->type;
__entry->seq = frame->seq;
__entry->len = get_unaligned_le16(&frame->len);
),
TP_printk("ty=%s, seq=%#04x, len=%u",
ssam_show_frame_type(__entry->type),
__entry->seq,
__entry->len
)
);
#define DEFINE_SSAM_FRAME_EVENT(name) \
DEFINE_EVENT(ssam_frame_class, ssam_##name, \
TP_PROTO(const struct ssh_frame *frame), \
TP_ARGS(frame) \
)
DECLARE_EVENT_CLASS(ssam_command_class,
TP_PROTO(const struct ssh_command *cmd, u16 len),
TP_ARGS(cmd, len),
TP_STRUCT__entry(
__field(u16, rqid)
__field(u16, len)
__field(u8, tid)
__field(u8, sid)
__field(u8, tc)
__field(u8, cid)
__field(u8, iid)
),
TP_fast_assign(
__entry->rqid = get_unaligned_le16(&cmd->rqid);
__entry->tid = cmd->tid;
__entry->sid = cmd->sid;
__entry->tc = cmd->tc;
__entry->cid = cmd->cid;
__entry->iid = cmd->iid;
__entry->len = len;
),
TP_printk("rqid=%#06x, tid=%s, sid=%s, tc=%s, cid=%#04x, iid=%#04x, len=%u",
__entry->rqid,
ssam_show_ssh_tid(__entry->tid),
ssam_show_ssh_tid(__entry->sid),
ssam_show_ssh_tc(__entry->tc),
__entry->cid,
__entry->iid,
__entry->len
)
);
#define DEFINE_SSAM_COMMAND_EVENT(name) \
DEFINE_EVENT(ssam_command_class, ssam_##name, \
TP_PROTO(const struct ssh_command *cmd, u16 len), \
TP_ARGS(cmd, len) \
)
DECLARE_EVENT_CLASS(ssam_packet_class,
TP_PROTO(const struct ssh_packet *packet),
TP_ARGS(packet),
TP_STRUCT__entry(
__field(unsigned long, state)
__array(char, uid, SSAM_PTR_UID_LEN)
__field(u8, priority)
__field(u16, length)
__field(u16, seq)
),
TP_fast_assign(
__entry->state = READ_ONCE(packet->state);
ssam_trace_ptr_uid(packet, __entry->uid);
__entry->priority = READ_ONCE(packet->priority);
__entry->length = packet->data.len;
__entry->seq = ssam_trace_get_packet_seq(packet);
),
TP_printk("uid=%s, seq=%s, ty=%s, pri=%#04x, len=%u, sta=%s",
__entry->uid,
ssam_show_packet_seq(__entry->seq),
ssam_show_packet_type(__entry->state),
__entry->priority,
__entry->length,
ssam_show_packet_state(__entry->state)
)
);
#define DEFINE_SSAM_PACKET_EVENT(name) \
DEFINE_EVENT(ssam_packet_class, ssam_##name, \
TP_PROTO(const struct ssh_packet *packet), \
TP_ARGS(packet) \
)
DECLARE_EVENT_CLASS(ssam_packet_status_class,
TP_PROTO(const struct ssh_packet *packet, int status),
TP_ARGS(packet, status),
TP_STRUCT__entry(
__field(unsigned long, state)
__field(int, status)
__array(char, uid, SSAM_PTR_UID_LEN)
__field(u8, priority)
__field(u16, length)
__field(u16, seq)
),
TP_fast_assign(
__entry->state = READ_ONCE(packet->state);
__entry->status = status;
ssam_trace_ptr_uid(packet, __entry->uid);
__entry->priority = READ_ONCE(packet->priority);
__entry->length = packet->data.len;
__entry->seq = ssam_trace_get_packet_seq(packet);
),
TP_printk("uid=%s, seq=%s, ty=%s, pri=%#04x, len=%u, sta=%s, status=%d",
__entry->uid,
ssam_show_packet_seq(__entry->seq),
ssam_show_packet_type(__entry->state),
__entry->priority,
__entry->length,
ssam_show_packet_state(__entry->state),
__entry->status
)
);
#define DEFINE_SSAM_PACKET_STATUS_EVENT(name) \
DEFINE_EVENT(ssam_packet_status_class, ssam_##name, \
TP_PROTO(const struct ssh_packet *packet, int status), \
TP_ARGS(packet, status) \
)
DECLARE_EVENT_CLASS(ssam_request_class,
TP_PROTO(const struct ssh_request *request),
TP_ARGS(request),
TP_STRUCT__entry(
__field(unsigned long, state)
__field(u32, rqid)
__array(char, uid, SSAM_PTR_UID_LEN)
__field(u8, tc)
__field(u16, cid)
__field(u16, iid)
__field(u8, tid)
__field(u8, sid)
),
TP_fast_assign(
const struct ssh_packet *p = &request->packet;
/* Use packet for UID so we can match requests to packets. */
__entry->state = READ_ONCE(request->state);
__entry->rqid = ssam_trace_get_request_id(p);
ssam_trace_ptr_uid(p, __entry->uid);
__entry->tid = ssam_trace_get_request_tid(p);
__entry->sid = ssam_trace_get_request_sid(p);
__entry->tc = ssam_trace_get_request_tc(p);
__entry->cid = ssam_trace_get_command_field_u8(p, cid);
__entry->iid = ssam_trace_get_command_field_u8(p, iid);
),
TP_printk("uid=%s, rqid=%s, ty=%s, sta=%s, tid=%s, sid=%s, tc=%s, cid=%s, iid=%s",
__entry->uid,
ssam_show_request_id(__entry->rqid),
ssam_show_request_type(__entry->state),
ssam_show_request_state(__entry->state),
ssam_show_ssh_tid(__entry->tid),
ssam_show_ssh_tid(__entry->sid),
ssam_show_ssh_tc(__entry->tc),
ssam_show_generic_u8_field(__entry->cid),
ssam_show_generic_u8_field(__entry->iid)
)
);
#define DEFINE_SSAM_REQUEST_EVENT(name) \
DEFINE_EVENT(ssam_request_class, ssam_##name, \
TP_PROTO(const struct ssh_request *request), \
TP_ARGS(request) \
)
DECLARE_EVENT_CLASS(ssam_request_status_class,
TP_PROTO(const struct ssh_request *request, int status),
TP_ARGS(request, status),
TP_STRUCT__entry(
__field(unsigned long, state)
__field(u32, rqid)
__field(int, status)
__array(char, uid, SSAM_PTR_UID_LEN)
__field(u8, tc)
__field(u16, cid)
__field(u16, iid)
__field(u8, tid)
__field(u8, sid)
),
TP_fast_assign(
const struct ssh_packet *p = &request->packet;
/* Use packet for UID so we can match requests to packets. */
__entry->state = READ_ONCE(request->state);
__entry->rqid = ssam_trace_get_request_id(p);
__entry->status = status;
ssam_trace_ptr_uid(p, __entry->uid);
__entry->tid = ssam_trace_get_request_tid(p);
__entry->sid = ssam_trace_get_request_sid(p);
__entry->tc = ssam_trace_get_request_tc(p);
__entry->cid = ssam_trace_get_command_field_u8(p, cid);
__entry->iid = ssam_trace_get_command_field_u8(p, iid);
),
TP_printk("uid=%s, rqid=%s, ty=%s, sta=%s, tid=%s, sid=%s, tc=%s, cid=%s, iid=%s, status=%d",
__entry->uid,
ssam_show_request_id(__entry->rqid),
ssam_show_request_type(__entry->state),
ssam_show_request_state(__entry->state),
ssam_show_ssh_tid(__entry->tid),
ssam_show_ssh_tid(__entry->sid),
ssam_show_ssh_tc(__entry->tc),
ssam_show_generic_u8_field(__entry->cid),
ssam_show_generic_u8_field(__entry->iid),
__entry->status
)
);
#define DEFINE_SSAM_REQUEST_STATUS_EVENT(name) \
DEFINE_EVENT(ssam_request_status_class, ssam_##name, \
TP_PROTO(const struct ssh_request *request, int status),\
TP_ARGS(request, status) \
)
DECLARE_EVENT_CLASS(ssam_alloc_class,
TP_PROTO(void *ptr, size_t len),
TP_ARGS(ptr, len),
TP_STRUCT__entry(
__field(size_t, len)
__array(char, uid, SSAM_PTR_UID_LEN)
),
TP_fast_assign(
__entry->len = len;
ssam_trace_ptr_uid(ptr, __entry->uid);
),
TP_printk("uid=%s, len=%zu", __entry->uid, __entry->len)
);
#define DEFINE_SSAM_ALLOC_EVENT(name) \
DEFINE_EVENT(ssam_alloc_class, ssam_##name, \
TP_PROTO(void *ptr, size_t len), \
TP_ARGS(ptr, len) \
)
DECLARE_EVENT_CLASS(ssam_free_class,
TP_PROTO(void *ptr),
TP_ARGS(ptr),
TP_STRUCT__entry(
__array(char, uid, SSAM_PTR_UID_LEN)
),
TP_fast_assign(
ssam_trace_ptr_uid(ptr, __entry->uid);
),
TP_printk("uid=%s", __entry->uid)
);
#define DEFINE_SSAM_FREE_EVENT(name) \
DEFINE_EVENT(ssam_free_class, ssam_##name, \
TP_PROTO(void *ptr), \
TP_ARGS(ptr) \
)
DECLARE_EVENT_CLASS(ssam_pending_class,
TP_PROTO(unsigned int pending),
TP_ARGS(pending),
TP_STRUCT__entry(
__field(unsigned int, pending)
),
TP_fast_assign(
__entry->pending = pending;
),
TP_printk("pending=%u", __entry->pending)
);
#define DEFINE_SSAM_PENDING_EVENT(name) \
DEFINE_EVENT(ssam_pending_class, ssam_##name, \
TP_PROTO(unsigned int pending), \
TP_ARGS(pending) \
)
DECLARE_EVENT_CLASS(ssam_data_class,
TP_PROTO(size_t length),
TP_ARGS(length),
TP_STRUCT__entry(
__field(size_t, length)
),
TP_fast_assign(
__entry->length = length;
),
TP_printk("length=%zu", __entry->length)
);
#define DEFINE_SSAM_DATA_EVENT(name) \
DEFINE_EVENT(ssam_data_class, ssam_##name, \
TP_PROTO(size_t length), \
TP_ARGS(length) \
)
DEFINE_SSAM_FRAME_EVENT(rx_frame_received);
DEFINE_SSAM_COMMAND_EVENT(rx_response_received);
DEFINE_SSAM_COMMAND_EVENT(rx_event_received);
DEFINE_SSAM_PACKET_EVENT(packet_release);
DEFINE_SSAM_PACKET_EVENT(packet_submit);
DEFINE_SSAM_PACKET_EVENT(packet_resubmit);
DEFINE_SSAM_PACKET_EVENT(packet_timeout);
DEFINE_SSAM_PACKET_EVENT(packet_cancel);
DEFINE_SSAM_PACKET_STATUS_EVENT(packet_complete);
DEFINE_SSAM_PENDING_EVENT(ptl_timeout_reap);
DEFINE_SSAM_REQUEST_EVENT(request_submit);
DEFINE_SSAM_REQUEST_EVENT(request_timeout);
DEFINE_SSAM_REQUEST_EVENT(request_cancel);
DEFINE_SSAM_REQUEST_STATUS_EVENT(request_complete);
DEFINE_SSAM_PENDING_EVENT(rtl_timeout_reap);
DEFINE_SSAM_PACKET_EVENT(ei_tx_drop_ack_packet);
DEFINE_SSAM_PACKET_EVENT(ei_tx_drop_nak_packet);
DEFINE_SSAM_PACKET_EVENT(ei_tx_drop_dsq_packet);
DEFINE_SSAM_PACKET_STATUS_EVENT(ei_tx_fail_write);
DEFINE_SSAM_PACKET_EVENT(ei_tx_corrupt_data);
DEFINE_SSAM_DATA_EVENT(ei_rx_corrupt_syn);
DEFINE_SSAM_FRAME_EVENT(ei_rx_corrupt_data);
DEFINE_SSAM_REQUEST_EVENT(ei_rx_drop_response);
DEFINE_SSAM_ALLOC_EVENT(ctrl_packet_alloc);
DEFINE_SSAM_FREE_EVENT(ctrl_packet_free);
DEFINE_SSAM_ALLOC_EVENT(event_item_alloc);
DEFINE_SSAM_FREE_EVENT(event_item_free);
#endif /* _SURFACE_AGGREGATOR_TRACE_H */
/* This part must be outside protection */
#undef TRACE_INCLUDE_PATH
#undef TRACE_INCLUDE_FILE
#define TRACE_INCLUDE_PATH .
#define TRACE_INCLUDE_FILE trace
#include <trace/define_trace.h>
@@ -0,0 +1,286 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Driver for the LID cover switch of the Surface 3
*
* Copyright (c) 2016 Red Hat Inc.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/input.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/spi/spi.h>
MODULE_AUTHOR("Benjamin Tissoires <benjamin.tissoires@redhat.com>");
MODULE_DESCRIPTION("Surface 3 platform driver");
MODULE_LICENSE("GPL");
#define ACPI_BUTTON_HID_LID "PNP0C0D"
#define SPI_CTL_OBJ_NAME "SPI"
#define SPI_TS_OBJ_NAME "NTRG"
#define SURFACE3_LID_GUID "F7CC25EC-D20B-404C-8903-0ED4359C18AE"
MODULE_ALIAS("wmi:" SURFACE3_LID_GUID);
static const struct dmi_system_id surface3_dmi_table[] = {
#if defined(CONFIG_X86)
{
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_MATCH(DMI_PRODUCT_NAME, "Surface 3"),
},
},
#endif
{ }
};
struct surface3_wmi {
struct acpi_device *touchscreen_adev;
struct acpi_device *pnp0c0d_adev;
struct acpi_hotplug_context hp;
struct input_dev *input;
};
static struct platform_device *s3_wmi_pdev;
static struct surface3_wmi s3_wmi;
static DEFINE_MUTEX(s3_wmi_lock);
static int s3_wmi_query_block(const char *guid, int instance, int *ret)
{
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
union acpi_object *obj = NULL;
acpi_status status;
int error = 0;
mutex_lock(&s3_wmi_lock);
status = wmi_query_block(guid, instance, &output);
if (ACPI_FAILURE(status)) {
error = -EIO;
goto out_free_unlock;
}
obj = output.pointer;
if (!obj || obj->type != ACPI_TYPE_INTEGER) {
if (obj) {
pr_err("query block returned object type: %d - buffer length:%d\n",
obj->type,
obj->type == ACPI_TYPE_BUFFER ?
obj->buffer.length : 0);
}
error = -EINVAL;
goto out_free_unlock;
}
*ret = obj->integer.value;
out_free_unlock:
kfree(obj);
mutex_unlock(&s3_wmi_lock);
return error;
}
static inline int s3_wmi_query_lid(int *ret)
{
return s3_wmi_query_block(SURFACE3_LID_GUID, 0, ret);
}
static int s3_wmi_send_lid_state(void)
{
int ret, lid_sw;
ret = s3_wmi_query_lid(&lid_sw);
if (ret)
return ret;
input_report_switch(s3_wmi.input, SW_LID, lid_sw);
input_sync(s3_wmi.input);
return 0;
}
static int s3_wmi_hp_notify(struct acpi_device *adev, u32 value)
{
return s3_wmi_send_lid_state();
}
static acpi_status s3_wmi_attach_spi_device(acpi_handle handle,
u32 level,
void *data,
void **return_value)
{
struct acpi_device *adev = acpi_fetch_acpi_dev(handle);
struct acpi_device **ts_adev = data;
if (!adev || strncmp(acpi_device_bid(adev), SPI_TS_OBJ_NAME,
strlen(SPI_TS_OBJ_NAME)))
return AE_OK;
if (*ts_adev) {
pr_err("duplicate entry %s\n", SPI_TS_OBJ_NAME);
return AE_OK;
}
*ts_adev = adev;
return AE_OK;
}
static int s3_wmi_check_platform_device(struct device *dev, void *data)
{
struct acpi_device *adev = ACPI_COMPANION(dev);
struct acpi_device *ts_adev = NULL;
acpi_status status;
/* ignore non ACPI devices */
if (!adev)
return 0;
/* check for LID ACPI switch */
if (!strcmp(ACPI_BUTTON_HID_LID, acpi_device_hid(adev))) {
s3_wmi.pnp0c0d_adev = adev;
return 0;
}
/* ignore non SPI controllers */
if (strncmp(acpi_device_bid(adev), SPI_CTL_OBJ_NAME,
strlen(SPI_CTL_OBJ_NAME)))
return 0;
status = acpi_walk_namespace(ACPI_TYPE_DEVICE, adev->handle, 1,
s3_wmi_attach_spi_device, NULL,
&ts_adev, NULL);
if (ACPI_FAILURE(status))
dev_warn(dev, "failed to enumerate SPI slaves\n");
if (!ts_adev)
return 0;
s3_wmi.touchscreen_adev = ts_adev;
return 0;
}
static int s3_wmi_create_and_register_input(struct platform_device *pdev)
{
struct input_dev *input;
int error;
input = devm_input_allocate_device(&pdev->dev);
if (!input)
return -ENOMEM;
input->name = "Lid Switch";
input->phys = "button/input0";
input->id.bustype = BUS_HOST;
input->id.product = 0x0005;
input_set_capability(input, EV_SW, SW_LID);
error = input_register_device(input);
if (error)
return error;
s3_wmi.input = input;
return 0;
}
static int __init s3_wmi_probe(struct platform_device *pdev)
{
int error;
if (!dmi_check_system(surface3_dmi_table))
return -ENODEV;
memset(&s3_wmi, 0, sizeof(s3_wmi));
bus_for_each_dev(&platform_bus_type, NULL, NULL,
s3_wmi_check_platform_device);
if (!s3_wmi.touchscreen_adev)
return -ENODEV;
acpi_bus_trim(s3_wmi.pnp0c0d_adev);
error = s3_wmi_create_and_register_input(pdev);
if (error)
goto restore_acpi_lid;
acpi_initialize_hp_context(s3_wmi.touchscreen_adev, &s3_wmi.hp,
s3_wmi_hp_notify, NULL);
s3_wmi_send_lid_state();
return 0;
restore_acpi_lid:
acpi_bus_scan(s3_wmi.pnp0c0d_adev->handle);
return error;
}
static void s3_wmi_remove(struct platform_device *device)
{
/* remove the hotplug context from the acpi device */
s3_wmi.touchscreen_adev->hp = NULL;
/* reinstall the actual PNPC0C0D LID default handle */
acpi_bus_scan(s3_wmi.pnp0c0d_adev->handle);
}
static int __maybe_unused s3_wmi_resume(struct device *dev)
{
s3_wmi_send_lid_state();
return 0;
}
static SIMPLE_DEV_PM_OPS(s3_wmi_pm, NULL, s3_wmi_resume);
static struct platform_driver s3_wmi_driver = {
.driver = {
.name = "surface3-wmi",
.pm = &s3_wmi_pm,
},
.remove = s3_wmi_remove,
};
static int __init s3_wmi_init(void)
{
int error;
s3_wmi_pdev = platform_device_alloc("surface3-wmi", -1);
if (!s3_wmi_pdev)
return -ENOMEM;
error = platform_device_add(s3_wmi_pdev);
if (error)
goto err_device_put;
error = platform_driver_probe(&s3_wmi_driver, s3_wmi_probe);
if (error)
goto err_device_del;
pr_info("Surface 3 WMI Extras loaded\n");
return 0;
err_device_del:
platform_device_del(s3_wmi_pdev);
err_device_put:
platform_device_put(s3_wmi_pdev);
return error;
}
static void __exit s3_wmi_exit(void)
{
platform_device_unregister(s3_wmi_pdev);
platform_driver_unregister(&s3_wmi_driver);
}
module_init(s3_wmi_init);
module_exit(s3_wmi_exit);
@@ -0,0 +1,586 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Supports for the power IC on the Surface 3 tablet.
*
* (C) Copyright 2016-2018 Red Hat, Inc
* (C) Copyright 2016-2018 Benjamin Tissoires <benjamin.tissoires@gmail.com>
* (C) Copyright 2016 Stephen Just <stephenjust@gmail.com>
*
* This driver has been reverse-engineered by parsing the DSDT of the Surface 3
* and looking at the registers of the chips.
*
* The DSDT allowed to find out that:
* - the driver is required for the ACPI BAT0 device to communicate to the chip
* through an operation region.
* - the various defines for the operation region functions to communicate with
* this driver
* - the DSM 3f99e367-6220-4955-8b0f-06ef2ae79412 allows to trigger ACPI
* events to BAT0 (the code is all available in the DSDT).
*
* Further findings regarding the 2 chips declared in the MSHW0011 are:
* - there are 2 chips declared:
* . 0x22 seems to control the ADP1 line status (and probably the charger)
* . 0x55 controls the battery directly
* - the battery chip uses a SMBus protocol (using plain SMBus allows non
* destructive commands):
* . the commands/registers used are in the range 0x00..0x7F
* . if bit 8 (0x80) is set in the SMBus command, the returned value is the
* same as when it is not set. There is a high chance this bit is the
* read/write
* . the various registers semantic as been deduced by observing the register
* dumps.
*/
#include <linux/acpi.h>
#include <linux/bits.h>
#include <linux/freezer.h>
#include <linux/i2c.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/uuid.h>
#include <linux/unaligned.h>
#define SURFACE_3_POLL_INTERVAL (2 * HZ)
#define SURFACE_3_STRLEN 10
struct mshw0011_data {
struct i2c_client *adp1;
struct i2c_client *bat0;
unsigned short notify_mask;
struct task_struct *poll_task;
bool kthread_running;
bool charging;
bool bat_charging;
u8 trip_point;
s32 full_capacity;
};
struct mshw0011_handler_data {
struct acpi_connection_info info;
struct i2c_client *client;
};
struct bix {
u32 revision;
u32 power_unit;
u32 design_capacity;
u32 last_full_charg_capacity;
u32 battery_technology;
u32 design_voltage;
u32 design_capacity_of_warning;
u32 design_capacity_of_low;
u32 cycle_count;
u32 measurement_accuracy;
u32 max_sampling_time;
u32 min_sampling_time;
u32 max_average_interval;
u32 min_average_interval;
u32 battery_capacity_granularity_1;
u32 battery_capacity_granularity_2;
char model[SURFACE_3_STRLEN];
char serial[SURFACE_3_STRLEN];
char type[SURFACE_3_STRLEN];
char OEM[SURFACE_3_STRLEN];
} __packed;
struct bst {
u32 battery_state;
s32 battery_present_rate;
u32 battery_remaining_capacity;
u32 battery_present_voltage;
} __packed;
struct gsb_command {
u8 arg0;
u8 arg1;
u8 arg2;
} __packed;
struct gsb_buffer {
u8 status;
u8 len;
u8 ret;
union {
struct gsb_command cmd;
struct bst bst;
struct bix bix;
} __packed;
} __packed;
#define ACPI_BATTERY_STATE_DISCHARGING BIT(0)
#define ACPI_BATTERY_STATE_CHARGING BIT(1)
#define ACPI_BATTERY_STATE_CRITICAL BIT(2)
#define MSHW0011_CMD_DEST_BAT0 0x01
#define MSHW0011_CMD_DEST_ADP1 0x03
#define MSHW0011_CMD_BAT0_STA 0x01
#define MSHW0011_CMD_BAT0_BIX 0x02
#define MSHW0011_CMD_BAT0_BCT 0x03
#define MSHW0011_CMD_BAT0_BTM 0x04
#define MSHW0011_CMD_BAT0_BST 0x05
#define MSHW0011_CMD_BAT0_BTP 0x06
#define MSHW0011_CMD_ADP1_PSR 0x07
#define MSHW0011_CMD_BAT0_PSOC 0x09
#define MSHW0011_CMD_BAT0_PMAX 0x0a
#define MSHW0011_CMD_BAT0_PSRC 0x0b
#define MSHW0011_CMD_BAT0_CHGI 0x0c
#define MSHW0011_CMD_BAT0_ARTG 0x0d
#define MSHW0011_NOTIFY_GET_VERSION 0x00
#define MSHW0011_NOTIFY_ADP1 0x01
#define MSHW0011_NOTIFY_BAT0_BST 0x02
#define MSHW0011_NOTIFY_BAT0_BIX 0x05
#define MSHW0011_ADP1_REG_PSR 0x04
#define MSHW0011_BAT0_REG_CAPACITY 0x0c
#define MSHW0011_BAT0_REG_FULL_CHG_CAPACITY 0x0e
#define MSHW0011_BAT0_REG_DESIGN_CAPACITY 0x40
#define MSHW0011_BAT0_REG_VOLTAGE 0x08
#define MSHW0011_BAT0_REG_RATE 0x14
#define MSHW0011_BAT0_REG_OEM 0x45
#define MSHW0011_BAT0_REG_TYPE 0x4e
#define MSHW0011_BAT0_REG_SERIAL_NO 0x56
#define MSHW0011_BAT0_REG_CYCLE_CNT 0x6e
#define MSHW0011_EV_2_5_MASK GENMASK(8, 0)
/* 3f99e367-6220-4955-8b0f-06ef2ae79412 */
static const guid_t mshw0011_guid =
GUID_INIT(0x3F99E367, 0x6220, 0x4955, 0x8B, 0x0F, 0x06, 0xEF,
0x2A, 0xE7, 0x94, 0x12);
static int
mshw0011_notify(struct mshw0011_data *cdata, u8 arg1, u8 arg2,
unsigned int *ret_value)
{
union acpi_object *obj;
acpi_handle handle;
unsigned int i;
handle = ACPI_HANDLE(&cdata->adp1->dev);
if (!handle)
return -ENODEV;
obj = acpi_evaluate_dsm_typed(handle, &mshw0011_guid, arg1, arg2, NULL,
ACPI_TYPE_BUFFER);
if (!obj) {
dev_err(&cdata->adp1->dev, "device _DSM execution failed\n");
return -ENODEV;
}
*ret_value = 0;
for (i = 0; i < obj->buffer.length; i++)
*ret_value |= obj->buffer.pointer[i] << (i * 8);
ACPI_FREE(obj);
return 0;
}
static const struct bix default_bix = {
.revision = 0x00,
.power_unit = 0x01,
.design_capacity = 0x1dca,
.last_full_charg_capacity = 0x1dca,
.battery_technology = 0x01,
.design_voltage = 0x10df,
.design_capacity_of_warning = 0x8f,
.design_capacity_of_low = 0x47,
.cycle_count = 0xffffffff,
.measurement_accuracy = 0x00015f90,
.max_sampling_time = 0x03e8,
.min_sampling_time = 0x03e8,
.max_average_interval = 0x03e8,
.min_average_interval = 0x03e8,
.battery_capacity_granularity_1 = 0x45,
.battery_capacity_granularity_2 = 0x11,
.model = "P11G8M",
.serial = "",
.type = "LION",
.OEM = "",
};
static int mshw0011_bix(struct mshw0011_data *cdata, struct bix *bix)
{
struct i2c_client *client = cdata->bat0;
char buf[SURFACE_3_STRLEN];
int ret;
*bix = default_bix;
/* get design capacity */
ret = i2c_smbus_read_word_data(client,
MSHW0011_BAT0_REG_DESIGN_CAPACITY);
if (ret < 0) {
dev_err(&client->dev, "Error reading design capacity: %d\n",
ret);
return ret;
}
bix->design_capacity = ret;
/* get last full charge capacity */
ret = i2c_smbus_read_word_data(client,
MSHW0011_BAT0_REG_FULL_CHG_CAPACITY);
if (ret < 0) {
dev_err(&client->dev,
"Error reading last full charge capacity: %d\n", ret);
return ret;
}
bix->last_full_charg_capacity = ret;
/*
* Get serial number, on some devices (with unofficial replacement
* battery?) reading any of the serial number range addresses gets
* nacked in this case just leave the serial number empty.
*/
ret = i2c_smbus_read_i2c_block_data(client, MSHW0011_BAT0_REG_SERIAL_NO,
sizeof(buf), buf);
if (ret == -EREMOTEIO) {
/* no serial number available */
} else if (ret != sizeof(buf)) {
dev_err(&client->dev, "Error reading serial no: %d\n", ret);
return ret;
} else {
snprintf(bix->serial, ARRAY_SIZE(bix->serial), "%3pE%6pE", buf + 7, buf);
}
/* get cycle count */
ret = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_CYCLE_CNT);
if (ret < 0) {
dev_err(&client->dev, "Error reading cycle count: %d\n", ret);
return ret;
}
bix->cycle_count = ret;
/* get OEM name */
ret = i2c_smbus_read_i2c_block_data(client, MSHW0011_BAT0_REG_OEM,
4, buf);
if (ret != 4) {
dev_err(&client->dev, "Error reading cycle count: %d\n", ret);
return ret;
}
snprintf(bix->OEM, ARRAY_SIZE(bix->OEM), "%3pE", buf);
return 0;
}
static int mshw0011_bst(struct mshw0011_data *cdata, struct bst *bst)
{
struct i2c_client *client = cdata->bat0;
int rate, capacity, voltage, state;
s16 tmp;
rate = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_RATE);
if (rate < 0)
return rate;
capacity = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_CAPACITY);
if (capacity < 0)
return capacity;
voltage = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_VOLTAGE);
if (voltage < 0)
return voltage;
tmp = rate;
bst->battery_present_rate = abs((s32)tmp);
state = 0;
if ((s32) tmp > 0)
state |= ACPI_BATTERY_STATE_CHARGING;
else if ((s32) tmp < 0)
state |= ACPI_BATTERY_STATE_DISCHARGING;
bst->battery_state = state;
bst->battery_remaining_capacity = capacity;
bst->battery_present_voltage = voltage;
return 0;
}
static int mshw0011_adp_psr(struct mshw0011_data *cdata)
{
return i2c_smbus_read_byte_data(cdata->adp1, MSHW0011_ADP1_REG_PSR);
}
static int mshw0011_isr(struct mshw0011_data *cdata)
{
struct bst bst;
struct bix bix;
int ret;
bool status, bat_status;
ret = mshw0011_adp_psr(cdata);
if (ret < 0)
return ret;
status = ret;
if (status != cdata->charging)
mshw0011_notify(cdata, cdata->notify_mask,
MSHW0011_NOTIFY_ADP1, &ret);
cdata->charging = status;
ret = mshw0011_bst(cdata, &bst);
if (ret < 0)
return ret;
bat_status = bst.battery_state;
if (bat_status != cdata->bat_charging)
mshw0011_notify(cdata, cdata->notify_mask,
MSHW0011_NOTIFY_BAT0_BST, &ret);
cdata->bat_charging = bat_status;
ret = mshw0011_bix(cdata, &bix);
if (ret < 0)
return ret;
if (bix.last_full_charg_capacity != cdata->full_capacity)
mshw0011_notify(cdata, cdata->notify_mask,
MSHW0011_NOTIFY_BAT0_BIX, &ret);
cdata->full_capacity = bix.last_full_charg_capacity;
return 0;
}
static int mshw0011_poll_task(void *data)
{
struct mshw0011_data *cdata = data;
int ret = 0;
cdata->kthread_running = true;
set_freezable();
while (!kthread_should_stop()) {
schedule_timeout_interruptible(SURFACE_3_POLL_INTERVAL);
try_to_freeze();
ret = mshw0011_isr(data);
if (ret)
break;
}
cdata->kthread_running = false;
return ret;
}
static acpi_status
mshw0011_space_handler(u32 function, acpi_physical_address command,
u32 bits, u64 *value64,
void *handler_context, void *region_context)
{
struct gsb_buffer *gsb = (struct gsb_buffer *)value64;
struct mshw0011_handler_data *data = handler_context;
struct acpi_connection_info *info = &data->info;
struct acpi_resource_i2c_serialbus *sb;
struct i2c_client *client = data->client;
struct mshw0011_data *cdata = i2c_get_clientdata(client);
struct acpi_resource *ares;
u32 accessor_type = function >> 16;
acpi_status ret;
int status = 1;
ret = acpi_buffer_to_resource(info->connection, info->length, &ares);
if (ACPI_FAILURE(ret))
return ret;
if (!value64 || !i2c_acpi_get_i2c_resource(ares, &sb)) {
ret = AE_BAD_PARAMETER;
goto err;
}
if (accessor_type != ACPI_GSB_ACCESS_ATTRIB_RAW_PROCESS) {
ret = AE_BAD_PARAMETER;
goto err;
}
if (gsb->cmd.arg0 == MSHW0011_CMD_DEST_ADP1 &&
gsb->cmd.arg1 == MSHW0011_CMD_ADP1_PSR) {
status = mshw0011_adp_psr(cdata);
if (status >= 0) {
ret = AE_OK;
goto out;
} else {
ret = AE_ERROR;
goto err;
}
}
if (gsb->cmd.arg0 != MSHW0011_CMD_DEST_BAT0) {
ret = AE_BAD_PARAMETER;
goto err;
}
switch (gsb->cmd.arg1) {
case MSHW0011_CMD_BAT0_STA:
break;
case MSHW0011_CMD_BAT0_BIX:
ret = mshw0011_bix(cdata, &gsb->bix);
break;
case MSHW0011_CMD_BAT0_BTP:
cdata->trip_point = gsb->cmd.arg2;
break;
case MSHW0011_CMD_BAT0_BST:
ret = mshw0011_bst(cdata, &gsb->bst);
break;
default:
dev_info(&cdata->bat0->dev, "command(0x%02x) is not supported.\n", gsb->cmd.arg1);
ret = AE_BAD_PARAMETER;
goto err;
}
out:
gsb->ret = status;
gsb->status = 0;
err:
ACPI_FREE(ares);
return ret;
}
static int mshw0011_install_space_handler(struct i2c_client *client)
{
struct acpi_device *adev;
struct mshw0011_handler_data *data;
acpi_status status;
adev = ACPI_COMPANION(&client->dev);
if (!adev)
return -ENODEV;
data = kzalloc_obj(struct mshw0011_handler_data);
if (!data)
return -ENOMEM;
data->client = client;
status = acpi_bus_attach_private_data(adev->handle, (void *)data);
if (ACPI_FAILURE(status)) {
kfree(data);
return -ENOMEM;
}
status = acpi_install_address_space_handler(adev->handle,
ACPI_ADR_SPACE_GSBUS,
&mshw0011_space_handler,
NULL,
data);
if (ACPI_FAILURE(status)) {
dev_err(&client->dev, "Error installing i2c space handler\n");
acpi_bus_detach_private_data(adev->handle);
kfree(data);
return -ENOMEM;
}
acpi_dev_clear_dependencies(adev);
return 0;
}
static void mshw0011_remove_space_handler(struct i2c_client *client)
{
struct mshw0011_handler_data *data;
acpi_handle handle;
acpi_status status;
handle = ACPI_HANDLE(&client->dev);
if (!handle)
return;
acpi_remove_address_space_handler(handle,
ACPI_ADR_SPACE_GSBUS,
&mshw0011_space_handler);
status = acpi_bus_get_private_data(handle, (void **)&data);
if (ACPI_SUCCESS(status))
kfree(data);
acpi_bus_detach_private_data(handle);
}
static int mshw0011_probe(struct i2c_client *client)
{
struct i2c_board_info board_info;
struct device *dev = &client->dev;
struct i2c_client *bat0;
struct mshw0011_data *data;
int error, mask;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->adp1 = client;
i2c_set_clientdata(client, data);
memset(&board_info, 0, sizeof(board_info));
strscpy(board_info.type, "MSHW0011-bat0", I2C_NAME_SIZE);
bat0 = i2c_acpi_new_device(dev, 1, &board_info);
if (IS_ERR(bat0))
return PTR_ERR(bat0);
data->bat0 = bat0;
i2c_set_clientdata(bat0, data);
error = mshw0011_notify(data, 1, MSHW0011_NOTIFY_GET_VERSION, &mask);
if (error)
goto out_err;
data->notify_mask = mask == MSHW0011_EV_2_5_MASK;
data->poll_task = kthread_run(mshw0011_poll_task, data, "mshw0011_adp");
if (IS_ERR(data->poll_task)) {
error = PTR_ERR(data->poll_task);
dev_err(&client->dev, "Unable to run kthread err %d\n", error);
goto out_err;
}
error = mshw0011_install_space_handler(client);
if (error)
goto out_err;
return 0;
out_err:
if (data->kthread_running)
kthread_stop(data->poll_task);
i2c_unregister_device(data->bat0);
return error;
}
static void mshw0011_remove(struct i2c_client *client)
{
struct mshw0011_data *cdata = i2c_get_clientdata(client);
mshw0011_remove_space_handler(client);
if (cdata->kthread_running)
kthread_stop(cdata->poll_task);
i2c_unregister_device(cdata->bat0);
}
static const struct acpi_device_id mshw0011_acpi_match[] = {
{ "MSHW0011", 0 },
{ }
};
MODULE_DEVICE_TABLE(acpi, mshw0011_acpi_match);
static struct i2c_driver mshw0011_driver = {
.probe = mshw0011_probe,
.remove = mshw0011_remove,
.driver = {
.name = "mshw0011",
.acpi_match_table = mshw0011_acpi_match,
},
};
module_i2c_driver(mshw0011_driver);
MODULE_AUTHOR("Benjamin Tissoires <benjamin.tissoires@gmail.com>");
MODULE_DESCRIPTION("mshw0011 driver");
MODULE_LICENSE("GPL v2");
@@ -0,0 +1,884 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Driver for the Surface ACPI Notify (SAN) interface/shim.
*
* Translates communication from ACPI to Surface System Aggregator Module
* (SSAM/SAM) requests and back, specifically SAM-over-SSH. Translates SSAM
* events back to ACPI notifications. Allows handling of discrete GPU
* notifications sent from ACPI via the SAN interface by providing them to any
* registered external driver.
*
* Copyright (C) 2019-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/unaligned.h>
#include <linux/acpi.h>
#include <linux/delay.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/rwsem.h>
#include <linux/surface_aggregator/controller.h>
#include <linux/surface_acpi_notify.h>
struct san_data {
struct device *dev;
struct ssam_controller *ctrl;
struct acpi_connection_info info;
struct ssam_event_notifier nf_bat;
struct ssam_event_notifier nf_tmp;
};
#define to_san_data(ptr, member) \
container_of(ptr, struct san_data, member)
static struct workqueue_struct *san_wq;
/* -- dGPU notifier interface. ---------------------------------------------- */
struct san_rqsg_if {
struct rw_semaphore lock;
struct device *dev;
struct blocking_notifier_head nh;
};
static struct san_rqsg_if san_rqsg_if = {
.lock = __RWSEM_INITIALIZER(san_rqsg_if.lock),
.dev = NULL,
.nh = BLOCKING_NOTIFIER_INIT(san_rqsg_if.nh),
};
static int san_set_rqsg_interface_device(struct device *dev)
{
int status = 0;
down_write(&san_rqsg_if.lock);
if (!san_rqsg_if.dev && dev)
san_rqsg_if.dev = dev;
else
status = -EBUSY;
up_write(&san_rqsg_if.lock);
return status;
}
/**
* san_client_link() - Link client as consumer to SAN device.
* @client: The client to link.
*
* Sets up a device link between the provided client device as consumer and
* the SAN device as provider. This function can be used to ensure that the
* SAN interface has been set up and will be set up for as long as the driver
* of the client device is bound. This guarantees that, during that time, all
* dGPU events will be received by any registered notifier.
*
* The link will be automatically removed once the client device's driver is
* unbound.
*
* Return: Returns zero on success, %-ENXIO if the SAN interface has not been
* set up yet, and %-ENOMEM if device link creation failed.
*/
int san_client_link(struct device *client)
{
const u32 flags = DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOREMOVE_CONSUMER;
struct device_link *link;
down_read(&san_rqsg_if.lock);
if (!san_rqsg_if.dev) {
up_read(&san_rqsg_if.lock);
return -ENXIO;
}
link = device_link_add(client, san_rqsg_if.dev, flags);
if (!link) {
up_read(&san_rqsg_if.lock);
return -ENOMEM;
}
if (READ_ONCE(link->status) == DL_STATE_SUPPLIER_UNBIND) {
up_read(&san_rqsg_if.lock);
return -ENXIO;
}
up_read(&san_rqsg_if.lock);
return 0;
}
EXPORT_SYMBOL_GPL(san_client_link);
/**
* san_dgpu_notifier_register() - Register a SAN dGPU notifier.
* @nb: The notifier-block to register.
*
* Registers a SAN dGPU notifier, receiving any new SAN dGPU events sent from
* ACPI. The registered notifier will be called with &struct san_dgpu_event
* as notifier data and the command ID of that event as notifier action.
*/
int san_dgpu_notifier_register(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&san_rqsg_if.nh, nb);
}
EXPORT_SYMBOL_GPL(san_dgpu_notifier_register);
/**
* san_dgpu_notifier_unregister() - Unregister a SAN dGPU notifier.
* @nb: The notifier-block to unregister.
*/
int san_dgpu_notifier_unregister(struct notifier_block *nb)
{
return blocking_notifier_chain_unregister(&san_rqsg_if.nh, nb);
}
EXPORT_SYMBOL_GPL(san_dgpu_notifier_unregister);
static int san_dgpu_notifier_call(struct san_dgpu_event *evt)
{
int ret;
ret = blocking_notifier_call_chain(&san_rqsg_if.nh, evt->command, evt);
return notifier_to_errno(ret);
}
/* -- ACPI _DSM event relay. ------------------------------------------------ */
#define SAN_DSM_REVISION 0
/* 93b666c5-70c6-469f-a215-3d487c91ab3c */
static const guid_t SAN_DSM_UUID =
GUID_INIT(0x93b666c5, 0x70c6, 0x469f, 0xa2, 0x15, 0x3d,
0x48, 0x7c, 0x91, 0xab, 0x3c);
enum san_dsm_event_fn {
SAN_DSM_EVENT_FN_BAT1_STAT = 0x03,
SAN_DSM_EVENT_FN_BAT1_INFO = 0x04,
SAN_DSM_EVENT_FN_ADP1_STAT = 0x05,
SAN_DSM_EVENT_FN_ADP1_INFO = 0x06,
SAN_DSM_EVENT_FN_BAT2_STAT = 0x07,
SAN_DSM_EVENT_FN_BAT2_INFO = 0x08,
SAN_DSM_EVENT_FN_THERMAL = 0x09,
SAN_DSM_EVENT_FN_DPTF = 0x0a,
};
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 = 0x4f,
};
enum sam_event_cid_tmp {
SAM_EVENT_CID_TMP_TRIP = 0x0b,
};
struct san_event_work {
struct delayed_work work;
struct device *dev;
struct ssam_event event; /* must be last */
};
static int san_acpi_notify_event(struct device *dev, u64 func,
union acpi_object *param)
{
acpi_handle san = ACPI_HANDLE(dev);
union acpi_object *obj;
int status = 0;
if (!acpi_check_dsm(san, &SAN_DSM_UUID, SAN_DSM_REVISION, BIT_ULL(func)))
return 0;
dev_dbg(dev, "notify event %#04llx\n", func);
obj = acpi_evaluate_dsm_typed(san, &SAN_DSM_UUID, SAN_DSM_REVISION,
func, param, ACPI_TYPE_BUFFER);
if (!obj)
return -EFAULT;
if (obj->buffer.length != 1 || obj->buffer.pointer[0] != 0) {
dev_err(dev, "got unexpected result from _DSM\n");
status = -EPROTO;
}
ACPI_FREE(obj);
return status;
}
static int san_evt_bat_adp(struct device *dev, const struct ssam_event *event)
{
int status;
status = san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_ADP1_STAT, NULL);
if (status)
return status;
/*
* Ensure that the battery states get updated correctly. When the
* battery is fully charged and an adapter is plugged in, it sometimes
* is not updated correctly, instead showing it as charging.
* Explicitly trigger battery updates to fix this.
*/
status = san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_BAT1_STAT, NULL);
if (status)
return status;
return san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_BAT2_STAT, NULL);
}
static int san_evt_bat_bix(struct device *dev, const struct ssam_event *event)
{
enum san_dsm_event_fn fn;
if (event->instance_id == 0x02)
fn = SAN_DSM_EVENT_FN_BAT2_INFO;
else
fn = SAN_DSM_EVENT_FN_BAT1_INFO;
return san_acpi_notify_event(dev, fn, NULL);
}
static int san_evt_bat_bst(struct device *dev, const struct ssam_event *event)
{
enum san_dsm_event_fn fn;
if (event->instance_id == 0x02)
fn = SAN_DSM_EVENT_FN_BAT2_STAT;
else
fn = SAN_DSM_EVENT_FN_BAT1_STAT;
return san_acpi_notify_event(dev, fn, NULL);
}
static int san_evt_bat_dptf(struct device *dev, const struct ssam_event *event)
{
union acpi_object payload;
/*
* The Surface ACPI expects a buffer and not a package. It specifically
* checks for ObjectType (Arg3) == 0x03. This will cause a warning in
* acpica/nsarguments.c, but that warning can be safely ignored.
*/
payload.type = ACPI_TYPE_BUFFER;
payload.buffer.length = event->length;
payload.buffer.pointer = (u8 *)&event->data[0];
return san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_DPTF, &payload);
}
static unsigned long san_evt_bat_delay(u8 cid)
{
switch (cid) {
case SAM_EVENT_CID_BAT_ADP:
/*
* Wait for battery state to update before signaling adapter
* change.
*/
return msecs_to_jiffies(5000);
case SAM_EVENT_CID_BAT_BST:
/* Ensure we do not miss anything important due to caching. */
return msecs_to_jiffies(2000);
default:
return 0;
}
}
static bool san_evt_bat(const struct ssam_event *event, struct device *dev)
{
int status;
switch (event->command_id) {
case SAM_EVENT_CID_BAT_BIX:
status = san_evt_bat_bix(dev, event);
break;
case SAM_EVENT_CID_BAT_BST:
status = san_evt_bat_bst(dev, event);
break;
case SAM_EVENT_CID_BAT_ADP:
status = san_evt_bat_adp(dev, event);
break;
case SAM_EVENT_CID_BAT_PROT:
/*
* TODO: Implement support for battery protection status change
* event.
*/
return true;
case SAM_EVENT_CID_BAT_DPTF:
status = san_evt_bat_dptf(dev, event);
break;
default:
return false;
}
if (status) {
dev_err(dev, "error handling power event (cid = %#04x)\n",
event->command_id);
}
return true;
}
static void san_evt_bat_workfn(struct work_struct *work)
{
struct san_event_work *ev;
ev = container_of(work, struct san_event_work, work.work);
san_evt_bat(&ev->event, ev->dev);
kfree(ev);
}
static u32 san_evt_bat_nf(struct ssam_event_notifier *nf,
const struct ssam_event *event)
{
struct san_data *d = to_san_data(nf, nf_bat);
struct san_event_work *work;
unsigned long delay = san_evt_bat_delay(event->command_id);
if (delay == 0)
return san_evt_bat(event, d->dev) ? SSAM_NOTIF_HANDLED : 0;
work = kzalloc(sizeof(*work) + event->length, GFP_KERNEL);
if (!work)
return ssam_notifier_from_errno(-ENOMEM);
INIT_DELAYED_WORK(&work->work, san_evt_bat_workfn);
work->dev = d->dev;
work->event = *event;
memcpy(work->event.data, event->data, event->length);
queue_delayed_work(san_wq, &work->work, delay);
return SSAM_NOTIF_HANDLED;
}
static int san_evt_tmp_trip(struct device *dev, const struct ssam_event *event)
{
union acpi_object param;
/*
* The Surface ACPI expects an integer and not a package. This will
* cause a warning in acpica/nsarguments.c, but that warning can be
* safely ignored.
*/
param.type = ACPI_TYPE_INTEGER;
param.integer.value = event->instance_id;
return san_acpi_notify_event(dev, SAN_DSM_EVENT_FN_THERMAL, &param);
}
static bool san_evt_tmp(const struct ssam_event *event, struct device *dev)
{
int status;
switch (event->command_id) {
case SAM_EVENT_CID_TMP_TRIP:
status = san_evt_tmp_trip(dev, event);
break;
default:
return false;
}
if (status) {
dev_err(dev, "error handling thermal event (cid = %#04x)\n",
event->command_id);
}
return true;
}
static u32 san_evt_tmp_nf(struct ssam_event_notifier *nf,
const struct ssam_event *event)
{
struct san_data *d = to_san_data(nf, nf_tmp);
return san_evt_tmp(event, d->dev) ? SSAM_NOTIF_HANDLED : 0;
}
/* -- ACPI GSB OperationRegion handler -------------------------------------- */
struct gsb_data_in {
u8 cv;
} __packed;
struct gsb_data_rqsx {
u8 cv; /* Command value (san_gsb_request_cv). */
u8 tc; /* Target category. */
u8 tid; /* Target ID. */
u8 iid; /* Instance ID. */
u8 snc; /* Expect-response-flag. */
u8 cid; /* Command ID. */
u16 cdl; /* Payload length. */
u8 pld[]; /* Payload. */
} __packed;
struct gsb_data_etwl {
u8 cv; /* Command value (should be 0x02). */
u8 etw3; /* Unknown. */
u8 etw4; /* Unknown. */
u8 msg[]; /* Error message (ASCIIZ). */
} __packed;
struct gsb_data_out {
u8 status; /* _SSH communication status. */
u8 len; /* _SSH payload length. */
u8 pld[]; /* _SSH payload. */
} __packed;
union gsb_buffer_data {
struct gsb_data_in in; /* Common input. */
struct gsb_data_rqsx rqsx; /* RQSX input. */
struct gsb_data_etwl etwl; /* ETWL input. */
struct gsb_data_out out; /* Output. */
};
struct gsb_buffer {
u8 status; /* GSB AttribRawProcess status. */
u8 len; /* GSB AttribRawProcess length. */
union gsb_buffer_data data;
} __packed;
#define SAN_GSB_MAX_RQSX_PAYLOAD (U8_MAX - 2 - sizeof(struct gsb_data_rqsx))
#define SAN_GSB_MAX_RESPONSE (U8_MAX - 2 - sizeof(struct gsb_data_out))
#define SAN_GSB_COMMAND 0
enum san_gsb_request_cv {
SAN_GSB_REQUEST_CV_RQST = 0x01,
SAN_GSB_REQUEST_CV_ETWL = 0x02,
SAN_GSB_REQUEST_CV_RQSG = 0x03,
};
#define SAN_REQUEST_NUM_TRIES 5
static acpi_status san_etwl(struct san_data *d, struct gsb_buffer *b)
{
struct gsb_data_etwl *etwl = &b->data.etwl;
if (b->len < sizeof(struct gsb_data_etwl)) {
dev_err(d->dev, "invalid ETWL package (len = %d)\n", b->len);
return AE_OK;
}
dev_err(d->dev, "ETWL(%#04x, %#04x): %.*s\n", etwl->etw3, etwl->etw4,
(unsigned int)(b->len - sizeof(struct gsb_data_etwl)),
(char *)etwl->msg);
/* Indicate success. */
b->status = 0x00;
b->len = 0x00;
return AE_OK;
}
static
struct gsb_data_rqsx *san_validate_rqsx(struct device *dev, const char *type,
struct gsb_buffer *b)
{
struct gsb_data_rqsx *rqsx = &b->data.rqsx;
if (b->len < sizeof(struct gsb_data_rqsx)) {
dev_err(dev, "invalid %s package (len = %d)\n", type, b->len);
return NULL;
}
if (get_unaligned(&rqsx->cdl) != b->len - sizeof(struct gsb_data_rqsx)) {
dev_err(dev, "bogus %s package (len = %d, cdl = %d)\n",
type, b->len, get_unaligned(&rqsx->cdl));
return NULL;
}
if (get_unaligned(&rqsx->cdl) > SAN_GSB_MAX_RQSX_PAYLOAD) {
dev_err(dev, "payload for %s package too large (cdl = %d)\n",
type, get_unaligned(&rqsx->cdl));
return NULL;
}
return rqsx;
}
static void gsb_rqsx_response_error(struct gsb_buffer *gsb, int status)
{
gsb->status = 0x00;
gsb->len = 0x02;
gsb->data.out.status = (u8)(-status);
gsb->data.out.len = 0x00;
}
static void gsb_rqsx_response_success(struct gsb_buffer *gsb, u8 *ptr, size_t len)
{
gsb->status = 0x00;
gsb->len = len + 2;
gsb->data.out.status = 0x00;
gsb->data.out.len = len;
if (len)
memcpy(&gsb->data.out.pld[0], ptr, len);
}
static acpi_status san_rqst_fixup_suspended(struct san_data *d,
struct ssam_request *rqst,
struct gsb_buffer *gsb)
{
if (rqst->target_category == SSAM_SSH_TC_BAS && rqst->command_id == 0x0D) {
u8 base_state = 1;
/* Base state quirk:
* The base state may be queried from ACPI when the EC is still
* suspended. In this case it will return '-EPERM'. This query
* will only be triggered from the ACPI lid GPE interrupt, thus
* we are either in laptop or studio mode (base status 0x01 or
* 0x02). Furthermore, we will only get here if the device (and
* EC) have been suspended.
*
* We now assume that the device is in laptop mode (0x01). This
* has the drawback that it will wake the device when unfolding
* it in studio mode, but it also allows us to avoid actively
* waiting for the EC to wake up, which may incur a notable
* delay.
*/
dev_dbg(d->dev, "rqst: fixup: base-state quirk\n");
gsb_rqsx_response_success(gsb, &base_state, sizeof(base_state));
return AE_OK;
}
gsb_rqsx_response_error(gsb, -ENXIO);
return AE_OK;
}
static acpi_status san_rqst(struct san_data *d, struct gsb_buffer *buffer)
{
u8 rspbuf[SAN_GSB_MAX_RESPONSE];
struct gsb_data_rqsx *gsb_rqst;
struct ssam_request rqst;
struct ssam_response rsp;
int status = 0;
gsb_rqst = san_validate_rqsx(d->dev, "RQST", buffer);
if (!gsb_rqst)
return AE_OK;
rqst.target_category = gsb_rqst->tc;
rqst.target_id = gsb_rqst->tid;
rqst.command_id = gsb_rqst->cid;
rqst.instance_id = gsb_rqst->iid;
rqst.flags = gsb_rqst->snc ? SSAM_REQUEST_HAS_RESPONSE : 0;
rqst.length = get_unaligned(&gsb_rqst->cdl);
rqst.payload = &gsb_rqst->pld[0];
rsp.capacity = ARRAY_SIZE(rspbuf);
rsp.length = 0;
rsp.pointer = &rspbuf[0];
/* Handle suspended device. */
if (d->dev->power.is_suspended) {
dev_warn(d->dev, "rqst: device is suspended, not executing\n");
return san_rqst_fixup_suspended(d, &rqst, buffer);
}
status = __ssam_retry(ssam_request_do_sync_onstack, SAN_REQUEST_NUM_TRIES,
d->ctrl, &rqst, &rsp, SAN_GSB_MAX_RQSX_PAYLOAD);
if (!status) {
gsb_rqsx_response_success(buffer, rsp.pointer, rsp.length);
} else {
dev_err(d->dev, "rqst: failed with error %d\n", status);
gsb_rqsx_response_error(buffer, status);
}
return AE_OK;
}
static acpi_status san_rqsg(struct san_data *d, struct gsb_buffer *buffer)
{
struct gsb_data_rqsx *gsb_rqsg;
struct san_dgpu_event evt;
int status;
gsb_rqsg = san_validate_rqsx(d->dev, "RQSG", buffer);
if (!gsb_rqsg)
return AE_OK;
evt.category = gsb_rqsg->tc;
evt.target = gsb_rqsg->tid;
evt.command = gsb_rqsg->cid;
evt.instance = gsb_rqsg->iid;
evt.length = get_unaligned(&gsb_rqsg->cdl);
evt.payload = &gsb_rqsg->pld[0];
status = san_dgpu_notifier_call(&evt);
if (!status) {
gsb_rqsx_response_success(buffer, NULL, 0);
} else {
dev_err(d->dev, "rqsg: failed with error %d\n", status);
gsb_rqsx_response_error(buffer, status);
}
return AE_OK;
}
static acpi_status san_opreg_handler(u32 function, acpi_physical_address command,
u32 bits, u64 *value64, void *opreg_context,
void *region_context)
{
struct san_data *d = to_san_data(opreg_context, info);
struct gsb_buffer *buffer = (struct gsb_buffer *)value64;
int accessor_type = (function & 0xFFFF0000) >> 16;
if (command != SAN_GSB_COMMAND) {
dev_warn(d->dev, "unsupported command: %#04llx\n", command);
return AE_OK;
}
if (accessor_type != ACPI_GSB_ACCESS_ATTRIB_RAW_PROCESS) {
dev_err(d->dev, "invalid access type: %#04x\n", accessor_type);
return AE_OK;
}
/* Buffer must have at least contain the command-value. */
if (buffer->len == 0) {
dev_err(d->dev, "request-package too small\n");
return AE_OK;
}
switch (buffer->data.in.cv) {
case SAN_GSB_REQUEST_CV_RQST:
return san_rqst(d, buffer);
case SAN_GSB_REQUEST_CV_ETWL:
return san_etwl(d, buffer);
case SAN_GSB_REQUEST_CV_RQSG:
return san_rqsg(d, buffer);
default:
dev_warn(d->dev, "unsupported SAN0 request (cv: %#04x)\n",
buffer->data.in.cv);
return AE_OK;
}
}
/* -- Driver setup. --------------------------------------------------------- */
static int san_events_register(struct platform_device *pdev)
{
struct san_data *d = platform_get_drvdata(pdev);
int status;
d->nf_bat.base.priority = 1;
d->nf_bat.base.fn = san_evt_bat_nf;
d->nf_bat.event.reg = SSAM_EVENT_REGISTRY_SAM;
d->nf_bat.event.id.target_category = SSAM_SSH_TC_BAT;
d->nf_bat.event.id.instance = 0;
d->nf_bat.event.mask = SSAM_EVENT_MASK_TARGET;
d->nf_bat.event.flags = SSAM_EVENT_SEQUENCED;
d->nf_tmp.base.priority = 1;
d->nf_tmp.base.fn = san_evt_tmp_nf;
d->nf_tmp.event.reg = SSAM_EVENT_REGISTRY_SAM;
d->nf_tmp.event.id.target_category = SSAM_SSH_TC_TMP;
d->nf_tmp.event.id.instance = 0;
d->nf_tmp.event.mask = SSAM_EVENT_MASK_TARGET;
d->nf_tmp.event.flags = SSAM_EVENT_SEQUENCED;
status = ssam_notifier_register(d->ctrl, &d->nf_bat);
if (status)
return status;
status = ssam_notifier_register(d->ctrl, &d->nf_tmp);
if (status)
ssam_notifier_unregister(d->ctrl, &d->nf_bat);
return status;
}
static void san_events_unregister(struct platform_device *pdev)
{
struct san_data *d = platform_get_drvdata(pdev);
ssam_notifier_unregister(d->ctrl, &d->nf_bat);
ssam_notifier_unregister(d->ctrl, &d->nf_tmp);
}
#define san_consumer_printk(level, dev, handle, fmt, ...) \
do { \
char *path = "<error getting consumer path>"; \
struct acpi_buffer buffer = { \
.length = ACPI_ALLOCATE_BUFFER, \
.pointer = NULL, \
}; \
\
if (ACPI_SUCCESS(acpi_get_name(handle, ACPI_FULL_PATHNAME, &buffer))) \
path = buffer.pointer; \
\
dev_##level(dev, "[%s]: " fmt, path, ##__VA_ARGS__); \
kfree(buffer.pointer); \
} while (0)
#define san_consumer_dbg(dev, handle, fmt, ...) \
san_consumer_printk(dbg, dev, handle, fmt, ##__VA_ARGS__)
#define san_consumer_warn(dev, handle, fmt, ...) \
san_consumer_printk(warn, dev, handle, fmt, ##__VA_ARGS__)
static acpi_status san_consumer_setup(acpi_handle handle, u32 lvl,
void *context, void **rv)
{
const u32 flags = DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOREMOVE_SUPPLIER;
struct platform_device *pdev = context;
struct acpi_device *adev;
struct device_link *link;
if (!acpi_device_dep(handle, ACPI_HANDLE(&pdev->dev)))
return AE_OK;
/* Ignore ACPI devices that are not present. */
adev = acpi_fetch_acpi_dev(handle);
if (!adev)
return AE_OK;
san_consumer_dbg(&pdev->dev, handle, "creating device link\n");
/* Try to set up device links, ignore but log errors. */
link = device_link_add(&adev->dev, &pdev->dev, flags);
if (!link) {
san_consumer_warn(&pdev->dev, handle, "failed to create device link\n");
return AE_OK;
}
return AE_OK;
}
static int san_consumer_links_setup(struct platform_device *pdev)
{
acpi_status status;
status = acpi_walk_namespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT,
ACPI_UINT32_MAX, san_consumer_setup, NULL,
pdev, NULL);
return status ? -EFAULT : 0;
}
static int san_probe(struct platform_device *pdev)
{
struct acpi_device *san = ACPI_COMPANION(&pdev->dev);
struct ssam_controller *ctrl;
struct san_data *data;
acpi_status astatus;
int status;
ctrl = ssam_client_bind(&pdev->dev);
if (IS_ERR(ctrl))
return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl);
status = san_consumer_links_setup(pdev);
if (status)
return status;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->dev = &pdev->dev;
data->ctrl = ctrl;
platform_set_drvdata(pdev, data);
astatus = acpi_install_address_space_handler(san->handle,
ACPI_ADR_SPACE_GSBUS,
&san_opreg_handler, NULL,
&data->info);
if (ACPI_FAILURE(astatus))
return -ENXIO;
status = san_events_register(pdev);
if (status)
goto err_enable_events;
status = san_set_rqsg_interface_device(&pdev->dev);
if (status)
goto err_install_dev;
acpi_dev_clear_dependencies(san);
return 0;
err_install_dev:
san_events_unregister(pdev);
err_enable_events:
acpi_remove_address_space_handler(san, ACPI_ADR_SPACE_GSBUS,
&san_opreg_handler);
return status;
}
static void san_remove(struct platform_device *pdev)
{
acpi_handle san = ACPI_HANDLE(&pdev->dev);
san_set_rqsg_interface_device(NULL);
acpi_remove_address_space_handler(san, ACPI_ADR_SPACE_GSBUS,
&san_opreg_handler);
san_events_unregister(pdev);
/*
* We have unregistered our event sources. Now we need to ensure that
* all delayed works they may have spawned are run to completion.
*/
flush_workqueue(san_wq);
}
static const struct acpi_device_id san_match[] = {
{ "MSHW0091" },
{ },
};
MODULE_DEVICE_TABLE(acpi, san_match);
static struct platform_driver surface_acpi_notify = {
.probe = san_probe,
.remove = san_remove,
.driver = {
.name = "surface_acpi_notify",
.acpi_match_table = san_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
static int __init san_init(void)
{
int ret;
san_wq = alloc_workqueue("san_wq", WQ_PERCPU, 0);
if (!san_wq)
return -ENOMEM;
ret = platform_driver_register(&surface_acpi_notify);
if (ret)
destroy_workqueue(san_wq);
return ret;
}
module_init(san_init);
static void __exit san_exit(void)
{
platform_driver_unregister(&surface_acpi_notify);
destroy_workqueue(san_wq);
}
module_exit(san_exit);
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Surface ACPI Notify driver for Surface System Aggregator Module");
MODULE_LICENSE("GPL");
@@ -0,0 +1,808 @@
// 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-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/kernel.h>
#include <linux/kfifo.h>
#include <linux/kref.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/poll.h>
#include <linux/rwsem.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/vmalloc.h>
#include <linux/surface_aggregator/cdev.h>
#include <linux/surface_aggregator/controller.h>
#include <linux/surface_aggregator/serial_hub.h>
#define SSAM_CDEV_DEVICE_NAME "surface_aggregator_cdev"
/* -- Main structures. ------------------------------------------------------ */
enum ssam_cdev_device_state {
SSAM_CDEV_DEVICE_SHUTDOWN_BIT = BIT(0),
};
struct ssam_cdev {
struct kref kref;
struct rw_semaphore lock;
struct device *dev;
struct ssam_controller *ctrl;
struct miscdevice mdev;
unsigned long flags;
struct rw_semaphore client_lock; /* Guards client list. */
struct list_head client_list;
};
struct ssam_cdev_client;
struct ssam_cdev_notifier {
struct ssam_cdev_client *client;
struct ssam_event_notifier nf;
};
struct ssam_cdev_client {
struct ssam_cdev *cdev;
struct list_head node;
struct mutex notifier_lock; /* Guards notifier access for registration */
struct ssam_cdev_notifier *notifier[SSH_NUM_EVENTS];
struct mutex read_lock; /* Guards FIFO buffer read access */
struct mutex write_lock; /* Guards FIFO buffer write access */
DECLARE_KFIFO(buffer, u8, 4096);
wait_queue_head_t waitq;
struct fasync_struct *fasync;
};
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);
}
/* -- Notifier handling. ---------------------------------------------------- */
static u32 ssam_cdev_notifier(struct ssam_event_notifier *nf, const struct ssam_event *in)
{
struct ssam_cdev_notifier *cdev_nf = container_of(nf, struct ssam_cdev_notifier, nf);
struct ssam_cdev_client *client = cdev_nf->client;
struct ssam_cdev_event event;
size_t n = struct_size(&event, data, in->length);
/* Translate event. */
event.target_category = in->target_category;
event.target_id = in->target_id;
event.command_id = in->command_id;
event.instance_id = in->instance_id;
event.length = in->length;
mutex_lock(&client->write_lock);
/* Make sure we have enough space. */
if (kfifo_avail(&client->buffer) < n) {
dev_warn(client->cdev->dev,
"buffer full, dropping event (tc: %#04x, tid: %#04x, cid: %#04x, iid: %#04x)\n",
in->target_category, in->target_id, in->command_id, in->instance_id);
mutex_unlock(&client->write_lock);
return 0;
}
/* Copy event header and payload. */
kfifo_in(&client->buffer, (const u8 *)&event, struct_size(&event, data, 0));
kfifo_in(&client->buffer, &in->data[0], in->length);
mutex_unlock(&client->write_lock);
/* Notify waiting readers. */
kill_fasync(&client->fasync, SIGIO, POLL_IN);
wake_up_interruptible(&client->waitq);
/*
* Don't mark events as handled, this is the job of a proper driver and
* not the debugging interface.
*/
return 0;
}
static int ssam_cdev_notifier_register(struct ssam_cdev_client *client, u8 tc, int priority)
{
const u16 rqid = ssh_tc_to_rqid(tc);
const u16 event = ssh_rqid_to_event(rqid);
struct ssam_cdev_notifier *nf;
int status;
lockdep_assert_held_read(&client->cdev->lock);
/* Validate notifier target category. */
if (!ssh_rqid_is_event(rqid))
return -EINVAL;
mutex_lock(&client->notifier_lock);
/* Check if the notifier has already been registered. */
if (client->notifier[event]) {
mutex_unlock(&client->notifier_lock);
return -EEXIST;
}
/* Allocate new notifier. */
nf = kzalloc_obj(*nf);
if (!nf) {
mutex_unlock(&client->notifier_lock);
return -ENOMEM;
}
/*
* Create a dummy notifier with the minimal required fields for
* observer registration. Note that we can skip fully specifying event
* and registry here as we do not need any matching and use silent
* registration, which does not enable the corresponding event.
*/
nf->client = client;
nf->nf.base.fn = ssam_cdev_notifier;
nf->nf.base.priority = priority;
nf->nf.event.id.target_category = tc;
nf->nf.event.mask = 0; /* Do not do any matching. */
nf->nf.flags = SSAM_EVENT_NOTIFIER_OBSERVER;
/* Register notifier. */
status = ssam_notifier_register(client->cdev->ctrl, &nf->nf);
if (status)
kfree(nf);
else
client->notifier[event] = nf;
mutex_unlock(&client->notifier_lock);
return status;
}
static int ssam_cdev_notifier_unregister(struct ssam_cdev_client *client, u8 tc)
{
const u16 rqid = ssh_tc_to_rqid(tc);
const u16 event = ssh_rqid_to_event(rqid);
int status;
lockdep_assert_held_read(&client->cdev->lock);
/* Validate notifier target category. */
if (!ssh_rqid_is_event(rqid))
return -EINVAL;
mutex_lock(&client->notifier_lock);
/* Check if the notifier is currently registered. */
if (!client->notifier[event]) {
mutex_unlock(&client->notifier_lock);
return -ENOENT;
}
/* Unregister and free notifier. */
status = ssam_notifier_unregister(client->cdev->ctrl, &client->notifier[event]->nf);
kfree(client->notifier[event]);
client->notifier[event] = NULL;
mutex_unlock(&client->notifier_lock);
return status;
}
static void ssam_cdev_notifier_unregister_all(struct ssam_cdev_client *client)
{
int i;
down_read(&client->cdev->lock);
/*
* This function may be used during shutdown, thus we need to test for
* cdev->ctrl instead of the SSAM_CDEV_DEVICE_SHUTDOWN_BIT bit.
*/
if (client->cdev->ctrl) {
for (i = 0; i < SSH_NUM_EVENTS; i++)
ssam_cdev_notifier_unregister(client, i + 1);
} else {
int count = 0;
/*
* Device has been shut down. Any notifier remaining is a bug,
* so warn about that as this would otherwise hardly be
* noticeable. Nevertheless, free them as well.
*/
mutex_lock(&client->notifier_lock);
for (i = 0; i < SSH_NUM_EVENTS; i++) {
count += !!(client->notifier[i]);
kfree(client->notifier[i]);
client->notifier[i] = NULL;
}
mutex_unlock(&client->notifier_lock);
WARN_ON(count > 0);
}
up_read(&client->cdev->lock);
}
/* -- IOCTL functions. ------------------------------------------------------ */
static long ssam_cdev_request(struct ssam_cdev_client *client, 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;
lockdep_assert_held_read(&client->cdev->lock);
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_do_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_do_sync(client->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_notif_register(struct ssam_cdev_client *client,
const struct ssam_cdev_notifier_desc __user *d)
{
struct ssam_cdev_notifier_desc desc;
long ret;
lockdep_assert_held_read(&client->cdev->lock);
ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));
if (ret)
return ret;
return ssam_cdev_notifier_register(client, desc.target_category, desc.priority);
}
static long ssam_cdev_notif_unregister(struct ssam_cdev_client *client,
const struct ssam_cdev_notifier_desc __user *d)
{
struct ssam_cdev_notifier_desc desc;
long ret;
lockdep_assert_held_read(&client->cdev->lock);
ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));
if (ret)
return ret;
return ssam_cdev_notifier_unregister(client, desc.target_category);
}
static long ssam_cdev_event_enable(struct ssam_cdev_client *client,
const struct ssam_cdev_event_desc __user *d)
{
struct ssam_cdev_event_desc desc;
struct ssam_event_registry reg;
struct ssam_event_id id;
long ret;
lockdep_assert_held_read(&client->cdev->lock);
/* Read descriptor from user-space. */
ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));
if (ret)
return ret;
/* Translate descriptor. */
reg.target_category = desc.reg.target_category;
reg.target_id = desc.reg.target_id;
reg.cid_enable = desc.reg.cid_enable;
reg.cid_disable = desc.reg.cid_disable;
id.target_category = desc.id.target_category;
id.instance = desc.id.instance;
/* Disable event. */
return ssam_controller_event_enable(client->cdev->ctrl, reg, id, desc.flags);
}
static long ssam_cdev_event_disable(struct ssam_cdev_client *client,
const struct ssam_cdev_event_desc __user *d)
{
struct ssam_cdev_event_desc desc;
struct ssam_event_registry reg;
struct ssam_event_id id;
long ret;
lockdep_assert_held_read(&client->cdev->lock);
/* Read descriptor from user-space. */
ret = copy_struct_from_user(&desc, sizeof(desc), d, sizeof(*d));
if (ret)
return ret;
/* Translate descriptor. */
reg.target_category = desc.reg.target_category;
reg.target_id = desc.reg.target_id;
reg.cid_enable = desc.reg.cid_enable;
reg.cid_disable = desc.reg.cid_disable;
id.target_category = desc.id.target_category;
id.instance = desc.id.instance;
/* Disable event. */
return ssam_controller_event_disable(client->cdev->ctrl, reg, id, desc.flags);
}
/* -- File operations. ------------------------------------------------------ */
static int ssam_cdev_device_open(struct inode *inode, struct file *filp)
{
struct miscdevice *mdev = filp->private_data;
struct ssam_cdev_client *client;
struct ssam_cdev *cdev = container_of(mdev, struct ssam_cdev, mdev);
/* Initialize client */
client = vzalloc(sizeof(*client));
if (!client)
return -ENOMEM;
client->cdev = ssam_cdev_get(cdev);
INIT_LIST_HEAD(&client->node);
mutex_init(&client->notifier_lock);
mutex_init(&client->read_lock);
mutex_init(&client->write_lock);
INIT_KFIFO(client->buffer);
init_waitqueue_head(&client->waitq);
filp->private_data = client;
/* Attach client. */
down_write(&cdev->client_lock);
if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags)) {
up_write(&cdev->client_lock);
mutex_destroy(&client->write_lock);
mutex_destroy(&client->read_lock);
mutex_destroy(&client->notifier_lock);
ssam_cdev_put(client->cdev);
vfree(client);
return -ENODEV;
}
list_add_tail(&client->node, &cdev->client_list);
up_write(&cdev->client_lock);
stream_open(inode, filp);
return 0;
}
static int ssam_cdev_device_release(struct inode *inode, struct file *filp)
{
struct ssam_cdev_client *client = filp->private_data;
/* Force-unregister all remaining notifiers of this client. */
ssam_cdev_notifier_unregister_all(client);
/* Detach client. */
down_write(&client->cdev->client_lock);
list_del(&client->node);
up_write(&client->cdev->client_lock);
/* Free client. */
mutex_destroy(&client->write_lock);
mutex_destroy(&client->read_lock);
mutex_destroy(&client->notifier_lock);
ssam_cdev_put(client->cdev);
vfree(client);
return 0;
}
static long __ssam_cdev_device_ioctl(struct ssam_cdev_client *client, unsigned int cmd,
unsigned long arg)
{
lockdep_assert_held_read(&client->cdev->lock);
switch (cmd) {
case SSAM_CDEV_REQUEST:
return ssam_cdev_request(client, (struct ssam_cdev_request __user *)arg);
case SSAM_CDEV_NOTIF_REGISTER:
return ssam_cdev_notif_register(client,
(struct ssam_cdev_notifier_desc __user *)arg);
case SSAM_CDEV_NOTIF_UNREGISTER:
return ssam_cdev_notif_unregister(client,
(struct ssam_cdev_notifier_desc __user *)arg);
case SSAM_CDEV_EVENT_ENABLE:
return ssam_cdev_event_enable(client, (struct ssam_cdev_event_desc __user *)arg);
case SSAM_CDEV_EVENT_DISABLE:
return ssam_cdev_event_disable(client, (struct ssam_cdev_event_desc __user *)arg);
default:
return -ENOTTY;
}
}
static long ssam_cdev_device_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct ssam_cdev_client *client = file->private_data;
long status;
/* Ensure that controller is valid for as long as we need it. */
if (down_read_killable(&client->cdev->lock))
return -ERESTARTSYS;
if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &client->cdev->flags)) {
up_read(&client->cdev->lock);
return -ENODEV;
}
status = __ssam_cdev_device_ioctl(client, cmd, arg);
up_read(&client->cdev->lock);
return status;
}
static ssize_t ssam_cdev_read(struct file *file, char __user *buf, size_t count, loff_t *offs)
{
struct ssam_cdev_client *client = file->private_data;
struct ssam_cdev *cdev = client->cdev;
unsigned int copied;
int status = 0;
if (down_read_killable(&cdev->lock))
return -ERESTARTSYS;
/* Make sure we're not shut down. */
if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags)) {
up_read(&cdev->lock);
return -ENODEV;
}
do {
/* Check availability, wait if necessary. */
if (kfifo_is_empty(&client->buffer)) {
up_read(&cdev->lock);
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
status = wait_event_interruptible(client->waitq,
!kfifo_is_empty(&client->buffer) ||
test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT,
&cdev->flags));
if (status < 0)
return status;
if (down_read_killable(&cdev->lock))
return -ERESTARTSYS;
/* Need to check that we're not shut down again. */
if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags)) {
up_read(&cdev->lock);
return -ENODEV;
}
}
/* Try to read from FIFO. */
if (mutex_lock_interruptible(&client->read_lock)) {
up_read(&cdev->lock);
return -ERESTARTSYS;
}
status = kfifo_to_user(&client->buffer, buf, count, &copied);
mutex_unlock(&client->read_lock);
if (status < 0) {
up_read(&cdev->lock);
return status;
}
/* We might not have gotten anything, check this here. */
if (copied == 0 && (file->f_flags & O_NONBLOCK)) {
up_read(&cdev->lock);
return -EAGAIN;
}
} while (copied == 0);
up_read(&cdev->lock);
return copied;
}
static __poll_t ssam_cdev_poll(struct file *file, struct poll_table_struct *pt)
{
struct ssam_cdev_client *client = file->private_data;
__poll_t events = 0;
if (test_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &client->cdev->flags))
return EPOLLHUP | EPOLLERR;
poll_wait(file, &client->waitq, pt);
if (!kfifo_is_empty(&client->buffer))
events |= EPOLLIN | EPOLLRDNORM;
return events;
}
static int ssam_cdev_fasync(int fd, struct file *file, int on)
{
struct ssam_cdev_client *client = file->private_data;
return fasync_helper(fd, file, on, &client->fasync);
}
static const struct file_operations ssam_controller_fops = {
.owner = THIS_MODULE,
.open = ssam_cdev_device_open,
.release = ssam_cdev_device_release,
.read = ssam_cdev_read,
.poll = ssam_cdev_poll,
.fasync = ssam_cdev_fasync,
.unlocked_ioctl = ssam_cdev_device_ioctl,
.compat_ioctl = ssam_cdev_device_ioctl,
};
/* -- Device and driver setup ----------------------------------------------- */
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_obj(*cdev);
if (!cdev)
return -ENOMEM;
kref_init(&cdev->kref);
init_rwsem(&cdev->lock);
cdev->ctrl = ctrl;
cdev->dev = &pdev->dev;
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;
init_rwsem(&cdev->client_lock);
INIT_LIST_HEAD(&cdev->client_list);
status = misc_register(&cdev->mdev);
if (status) {
kfree(cdev);
return status;
}
platform_set_drvdata(pdev, cdev);
return 0;
}
static void ssam_dbg_device_remove(struct platform_device *pdev)
{
struct ssam_cdev *cdev = platform_get_drvdata(pdev);
struct ssam_cdev_client *client;
/*
* Mark device as shut-down. Prevent new clients from being added and
* new operations from being executed.
*/
set_bit(SSAM_CDEV_DEVICE_SHUTDOWN_BIT, &cdev->flags);
down_write(&cdev->client_lock);
/* Remove all notifiers registered by us. */
list_for_each_entry(client, &cdev->client_list, node) {
ssam_cdev_notifier_unregister_all(client);
}
/* Wake up async clients. */
list_for_each_entry(client, &cdev->client_list, node) {
kill_fasync(&client->fasync, SIGIO, POLL_HUP);
}
/* Wake up blocking clients. */
list_for_each_entry(client, &cdev->client_list, node) {
wake_up_interruptible(&client->waitq);
}
up_write(&cdev->client_lock);
/*
* 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;
cdev->dev = NULL;
up_write(&cdev->lock);
misc_deregister(&cdev->mdev);
ssam_cdev_put(cdev);
}
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 <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("User-space interface for Surface System Aggregator Module");
MODULE_LICENSE("GPL");
@@ -0,0 +1,371 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Driver for Surface System Aggregator Module (SSAM) subsystem device hubs.
*
* Provides a driver for SSAM subsystems device hubs. This driver performs
* instantiation of the devices managed by said hubs and takes care of
* (hot-)removal.
*
* Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/kernel.h>
#include <linux/limits.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/workqueue.h>
#include <linux/surface_aggregator/device.h>
/* -- SSAM generic subsystem hub driver framework. -------------------------- */
enum ssam_hub_state {
SSAM_HUB_UNINITIALIZED, /* Only set during initialization. */
SSAM_HUB_CONNECTED,
SSAM_HUB_DISCONNECTED,
};
enum ssam_hub_flags {
SSAM_HUB_HOT_REMOVED,
};
struct ssam_hub;
struct ssam_hub_ops {
int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);
};
struct ssam_hub {
struct ssam_device *sdev;
enum ssam_hub_state state;
unsigned long flags;
struct delayed_work update_work;
unsigned long connect_delay;
struct ssam_event_notifier notif;
struct ssam_hub_ops ops;
};
struct ssam_hub_desc {
struct {
struct ssam_event_registry reg;
struct ssam_event_id id;
enum ssam_event_mask mask;
} event;
struct {
u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event);
int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);
} ops;
unsigned long connect_delay_ms;
};
static void ssam_hub_update_workfn(struct work_struct *work)
{
struct ssam_hub *hub = container_of(work, struct ssam_hub, update_work.work);
enum ssam_hub_state state;
int status = 0;
status = hub->ops.get_state(hub, &state);
if (status)
return;
/*
* There is a small possibility that hub devices were hot-removed and
* re-added before we were able to remove them here. In that case, both
* the state returned by get_state() and the state of the hub will
* equal SSAM_HUB_CONNECTED and we would bail early below, which would
* leave child devices without proper (re-)initialization and the
* hot-remove flag set.
*
* Therefore, we check whether devices have been hot-removed via an
* additional flag on the hub and, in this case, override the returned
* hub state. In case of a missed disconnect (i.e. get_state returned
* "connected"), we further need to re-schedule this work (with the
* appropriate delay) as the actual connect work submission might have
* been merged with this one.
*
* This then leads to one of two cases: Either we submit an unnecessary
* work item (which will get ignored via either the queue or the state
* checks) or, in the unlikely case that the work is actually required,
* double the normal connect delay.
*/
if (test_and_clear_bit(SSAM_HUB_HOT_REMOVED, &hub->flags)) {
if (state == SSAM_HUB_CONNECTED)
schedule_delayed_work(&hub->update_work, hub->connect_delay);
state = SSAM_HUB_DISCONNECTED;
}
if (hub->state == state)
return;
hub->state = state;
if (hub->state == SSAM_HUB_CONNECTED)
status = ssam_device_register_clients(hub->sdev);
else
ssam_remove_clients(&hub->sdev->dev);
if (status)
dev_err(&hub->sdev->dev, "failed to update hub child devices: %d\n", status);
}
static int ssam_hub_mark_hot_removed(struct device *dev, void *_data)
{
struct ssam_device *sdev = to_ssam_device(dev);
if (is_ssam_device(dev))
ssam_device_mark_hot_removed(sdev);
return 0;
}
static void ssam_hub_update(struct ssam_hub *hub, bool connected)
{
unsigned long delay;
/* Mark devices as hot-removed before we remove any. */
if (!connected) {
set_bit(SSAM_HUB_HOT_REMOVED, &hub->flags);
device_for_each_child_reverse(&hub->sdev->dev, NULL, ssam_hub_mark_hot_removed);
}
/*
* Delay update when the base/keyboard cover is being connected to give
* devices/EC some time to set up.
*/
delay = connected ? hub->connect_delay : 0;
schedule_delayed_work(&hub->update_work, delay);
}
static int __maybe_unused ssam_hub_resume(struct device *dev)
{
struct ssam_hub *hub = dev_get_drvdata(dev);
schedule_delayed_work(&hub->update_work, 0);
return 0;
}
static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops, NULL, ssam_hub_resume);
static int ssam_hub_probe(struct ssam_device *sdev)
{
const struct ssam_hub_desc *desc;
struct ssam_hub *hub;
int status;
desc = ssam_device_get_match_data(sdev);
if (!desc) {
WARN(1, "no driver match data specified");
return -EINVAL;
}
hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL);
if (!hub)
return -ENOMEM;
hub->sdev = sdev;
hub->state = SSAM_HUB_UNINITIALIZED;
hub->notif.base.priority = INT_MAX; /* This notifier should run first. */
hub->notif.base.fn = desc->ops.notify;
hub->notif.event.reg = desc->event.reg;
hub->notif.event.id = desc->event.id;
hub->notif.event.mask = desc->event.mask;
hub->notif.event.flags = SSAM_EVENT_SEQUENCED;
hub->connect_delay = msecs_to_jiffies(desc->connect_delay_ms);
hub->ops.get_state = desc->ops.get_state;
INIT_DELAYED_WORK(&hub->update_work, ssam_hub_update_workfn);
ssam_device_set_drvdata(sdev, hub);
status = ssam_device_notifier_register(sdev, &hub->notif);
if (status)
return status;
schedule_delayed_work(&hub->update_work, 0);
return 0;
}
static void ssam_hub_remove(struct ssam_device *sdev)
{
struct ssam_hub *hub = ssam_device_get_drvdata(sdev);
ssam_device_notifier_unregister(sdev, &hub->notif);
cancel_delayed_work_sync(&hub->update_work);
ssam_remove_clients(&sdev->dev);
}
/* -- SSAM base-subsystem hub driver. --------------------------------------- */
/*
* Some devices (especially battery) may need a bit of time to be fully usable
* after being (re-)connected. This delay has been determined via
* experimentation.
*/
#define SSAM_BASE_UPDATE_CONNECT_DELAY 2500
SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, {
.target_category = SSAM_SSH_TC_BAS,
.target_id = SSAM_SSH_TID_SAM,
.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_hub *hub, enum ssam_hub_state *state)
{
u8 opmode;
int status;
status = ssam_retry(ssam_bas_query_opmode, hub->sdev->ctrl, &opmode);
if (status < 0) {
dev_err(&hub->sdev->dev, "failed to query base state: %d\n", status);
return status;
}
if (opmode != SSAM_BAS_OPMODE_TABLET)
*state = SSAM_HUB_CONNECTED;
else
*state = SSAM_HUB_DISCONNECTED;
return 0;
}
static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
{
struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION)
return 0;
if (event->length < 1) {
dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
return 0;
}
ssam_hub_update(hub, event->data[0]);
/*
* 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 const struct ssam_hub_desc base_hub = {
.event = {
.reg = SSAM_EVENT_REGISTRY_SAM,
.id = {
.target_category = SSAM_SSH_TC_BAS,
.instance = 0,
},
.mask = SSAM_EVENT_MASK_NONE,
},
.ops = {
.notify = ssam_base_hub_notif,
.get_state = ssam_base_hub_query_state,
},
.connect_delay_ms = SSAM_BASE_UPDATE_CONNECT_DELAY,
};
/* -- SSAM KIP-subsystem hub driver. ---------------------------------------- */
/*
* Some devices may need a bit of time to be fully usable after being
* (re-)connected. This delay has been determined via experimentation.
*/
#define SSAM_KIP_UPDATE_CONNECT_DELAY 250
#define SSAM_EVENT_KIP_CID_CONNECTION 0x2c
SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state, u8, {
.target_category = SSAM_SSH_TC_KIP,
.target_id = SSAM_SSH_TID_SAM,
.command_id = 0x2c,
.instance_id = 0x00,
});
static int ssam_kip_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)
{
int status;
u8 connected;
status = ssam_retry(__ssam_kip_query_state, hub->sdev->ctrl, &connected);
if (status < 0) {
dev_err(&hub->sdev->dev, "failed to query KIP connection state: %d\n", status);
return status;
}
*state = connected ? SSAM_HUB_CONNECTED : SSAM_HUB_DISCONNECTED;
return 0;
}
static u32 ssam_kip_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
{
struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
if (event->command_id != SSAM_EVENT_KIP_CID_CONNECTION)
return 0; /* Return "unhandled". */
if (event->length < 1) {
dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
return 0;
}
ssam_hub_update(hub, event->data[0]);
return SSAM_NOTIF_HANDLED;
}
static const struct ssam_hub_desc kip_hub = {
.event = {
.reg = SSAM_EVENT_REGISTRY_SAM,
.id = {
.target_category = SSAM_SSH_TC_KIP,
.instance = 0,
},
.mask = SSAM_EVENT_MASK_TARGET,
},
.ops = {
.notify = ssam_kip_hub_notif,
.get_state = ssam_kip_hub_query_state,
},
.connect_delay_ms = SSAM_KIP_UPDATE_CONNECT_DELAY,
};
/* -- Driver registration. -------------------------------------------------- */
static const struct ssam_device_id ssam_hub_match[] = {
{ SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_KIP, 0x00), (unsigned long)&kip_hub },
{ SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_BAS, 0x00), (unsigned long)&base_hub },
{ }
};
MODULE_DEVICE_TABLE(ssam, ssam_hub_match);
static struct ssam_device_driver ssam_subsystem_hub_driver = {
.probe = ssam_hub_probe,
.remove = ssam_hub_remove,
.match_table = ssam_hub_match,
.driver = {
.name = "surface_aggregator_subsystem_hub",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.pm = &ssam_hub_pm_ops,
},
};
module_ssam_device_driver(ssam_subsystem_hub_driver);
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module");
MODULE_LICENSE("GPL");
@@ -0,0 +1,592 @@
// 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 and performs instantiation
* for these devices.
*
* Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/acpi.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/property.h>
#include <linux/types.h>
#include <linux/surface_aggregator/device.h>
/* -- Device registry. ------------------------------------------------------ */
/*
* SSAM device names follow the SSAM module alias, meaning they are prefixed
* with 'ssam:', followed by domain, category, target ID, instance ID, and
* function, each encoded as two-digit hexadecimal, separated by ':'. In other
* words, it follows the scheme
*
* ssam:dd:cc:tt:ii:ff
*
* Where, 'dd', 'cc', 'tt', 'ii', and 'ff' are the two-digit hexadecimal
* values mentioned above, respectively.
*/
/* Root node. */
static const struct software_node ssam_node_root = {
.name = "ssam_platform_hub",
};
/* KIP device hub (connects keyboard cover devices on Surface Pro 8). */
static const struct software_node ssam_node_hub_kip = {
.name = "ssam:00:00:01:0e:00",
.parent = &ssam_node_root,
};
/* Base device hub (devices attached to Surface Book 3 base). */
static const struct software_node ssam_node_hub_base = {
.name = "ssam:00:00:01:11:00",
.parent = &ssam_node_root,
};
/* AC adapter. */
static const struct software_node ssam_node_bat_ac = {
.name = "ssam:01:02:01:01:01",
.parent = &ssam_node_root,
};
/* Primary battery. */
static const struct software_node ssam_node_bat_main = {
.name = "ssam:01:02:01:01:00",
.parent = &ssam_node_root,
};
/* Secondary battery (Surface Book 3). */
static const struct software_node ssam_node_bat_sb3base = {
.name = "ssam:01:02:02:01:00",
.parent = &ssam_node_hub_base,
};
/* Platform profile / performance-mode device without a fan. */
static const struct software_node ssam_node_tmp_perf_profile = {
.name = "ssam:01:03:01:00:01",
.parent = &ssam_node_root,
};
/* Platform profile / performance-mode device with a fan, such that
* the fan controller profile can also be switched.
*/
static const struct property_entry ssam_node_tmp_perf_profile_has_fan[] = {
PROPERTY_ENTRY_BOOL("has_fan"),
{ }
};
static const struct software_node ssam_node_tmp_perf_profile_with_fan = {
.name = "ssam:01:03:01:00:01",
.parent = &ssam_node_root,
.properties = ssam_node_tmp_perf_profile_has_fan,
};
/* Thermal sensors. */
static const struct software_node ssam_node_tmp_sensors = {
.name = "ssam:01:03:01:00:02",
.parent = &ssam_node_root,
};
/* Fan speed function. */
static const struct software_node ssam_node_fan_speed = {
.name = "ssam:01:05:01:01:01",
.parent = &ssam_node_root,
};
/* Tablet-mode switch via KIP subsystem. */
static const struct software_node ssam_node_kip_tablet_switch = {
.name = "ssam:01:0e:01:00:01",
.parent = &ssam_node_root,
};
/* DTX / detachment-system device (Surface Book 3). */
static const struct software_node ssam_node_bas_dtx = {
.name = "ssam:01:11:01:00:00",
.parent = &ssam_node_root,
};
/* HID keyboard (SAM, TID=1). */
static const struct software_node ssam_node_hid_sam_keyboard = {
.name = "ssam:01:15:01:01:00",
.parent = &ssam_node_root,
};
/* HID pen stash (SAM, TID=1; pen taken / stashed away evens). */
static const struct software_node ssam_node_hid_sam_penstash = {
.name = "ssam:01:15:01:02:00",
.parent = &ssam_node_root,
};
/* HID touchpad (SAM, TID=1). */
static const struct software_node ssam_node_hid_sam_touchpad = {
.name = "ssam:01:15:01:03:00",
.parent = &ssam_node_root,
};
/* HID device instance 6 (SAM, TID=1, HID sensor collection). */
static const struct software_node ssam_node_hid_sam_sensors = {
.name = "ssam:01:15:01:06:00",
.parent = &ssam_node_root,
};
/* HID device instance 7 (SAM, TID=1, UCM UCSI HID client). */
static const struct software_node ssam_node_hid_sam_ucm_ucsi = {
.name = "ssam:01:15:01:07:00",
.parent = &ssam_node_root,
};
/* HID system controls (SAM, TID=1). */
static const struct software_node ssam_node_hid_sam_sysctrl = {
.name = "ssam:01:15:01:08:00",
.parent = &ssam_node_root,
};
/* HID keyboard. */
static const struct software_node ssam_node_hid_main_keyboard = {
.name = "ssam:01:15:02:01:00",
.parent = &ssam_node_root,
};
/* HID touchpad. */
static const struct software_node ssam_node_hid_main_touchpad = {
.name = "ssam:01:15:02:03:00",
.parent = &ssam_node_root,
};
/* HID device instance 5 (unknown HID device). */
static const struct software_node ssam_node_hid_main_iid5 = {
.name = "ssam:01:15:02:05:00",
.parent = &ssam_node_root,
};
/* HID keyboard (base hub). */
static const struct software_node ssam_node_hid_base_keyboard = {
.name = "ssam:01:15:02:01:00",
.parent = &ssam_node_hub_base,
};
/* HID touchpad (base hub). */
static const struct software_node ssam_node_hid_base_touchpad = {
.name = "ssam:01:15:02:03:00",
.parent = &ssam_node_hub_base,
};
/* HID device instance 5 (unknown HID device, base hub). */
static const struct software_node ssam_node_hid_base_iid5 = {
.name = "ssam:01:15:02:05:00",
.parent = &ssam_node_hub_base,
};
/* HID device instance 6 (unknown HID device, base hub). */
static const struct software_node ssam_node_hid_base_iid6 = {
.name = "ssam:01:15:02:06:00",
.parent = &ssam_node_hub_base,
};
/* HID keyboard (KIP hub). */
static const struct software_node ssam_node_hid_kip_keyboard = {
.name = "ssam:01:15:02:01:00",
.parent = &ssam_node_hub_kip,
};
/* HID pen stash (KIP hub; pen taken / stashed away evens). */
static const struct software_node ssam_node_hid_kip_penstash = {
.name = "ssam:01:15:02:02:00",
.parent = &ssam_node_hub_kip,
};
/* HID touchpad (KIP hub). */
static const struct software_node ssam_node_hid_kip_touchpad = {
.name = "ssam:01:15:02:03:00",
.parent = &ssam_node_hub_kip,
};
/* HID device instance 5 (KIP hub, type-cover firmware update). */
static const struct software_node ssam_node_hid_kip_fwupd = {
.name = "ssam:01:15:02:05:00",
.parent = &ssam_node_hub_kip,
};
/* Tablet-mode switch via POS subsystem. */
static const struct software_node ssam_node_pos_tablet_switch = {
.name = "ssam:01:26:01:00:01",
.parent = &ssam_node_root,
};
/*
* Devices for 5th- and 6th-generations models:
* - Surface Book 2,
* - Surface Laptop 1 and 2,
* - Surface Pro 5 and 6.
*/
static const struct software_node *ssam_node_group_gen5[] = {
&ssam_node_root,
&ssam_node_tmp_perf_profile,
NULL,
};
/* Devices for Surface Book 3. */
static const struct software_node *ssam_node_group_sb3[] = {
&ssam_node_root,
&ssam_node_hub_base,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_bat_sb3base,
&ssam_node_tmp_perf_profile,
&ssam_node_bas_dtx,
&ssam_node_hid_base_keyboard,
&ssam_node_hid_base_touchpad,
&ssam_node_hid_base_iid5,
&ssam_node_hid_base_iid6,
NULL,
};
/* Devices for Surface Laptop 3 and 4. */
static const struct software_node *ssam_node_group_sl3[] = {
&ssam_node_root,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile,
&ssam_node_hid_main_keyboard,
&ssam_node_hid_main_touchpad,
&ssam_node_hid_main_iid5,
NULL,
};
/* Devices for Surface Laptop 5. */
static const struct software_node *ssam_node_group_sl5[] = {
&ssam_node_root,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile_with_fan,
&ssam_node_tmp_sensors,
&ssam_node_fan_speed,
&ssam_node_hid_main_keyboard,
&ssam_node_hid_main_touchpad,
&ssam_node_hid_main_iid5,
&ssam_node_hid_sam_ucm_ucsi,
NULL,
};
/* Devices for Surface Laptop 6. */
static const struct software_node *ssam_node_group_sl6[] = {
&ssam_node_root,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile_with_fan,
&ssam_node_tmp_sensors,
&ssam_node_fan_speed,
&ssam_node_hid_main_keyboard,
&ssam_node_hid_main_touchpad,
&ssam_node_hid_main_iid5,
&ssam_node_hid_sam_sensors,
&ssam_node_hid_sam_ucm_ucsi,
NULL,
};
/* Devices for Surface Laptop 7. */
static const struct software_node *ssam_node_group_sl7[] = {
&ssam_node_root,
&ssam_node_tmp_perf_profile_with_fan,
&ssam_node_fan_speed,
&ssam_node_hid_sam_keyboard,
/* TODO: evaluate thermal sensors devices when we get a driver for that */
NULL,
};
/* Devices for Surface Laptop Studio 1. */
static const struct software_node *ssam_node_group_sls1[] = {
&ssam_node_root,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile,
&ssam_node_pos_tablet_switch,
&ssam_node_hid_sam_keyboard,
&ssam_node_hid_sam_penstash,
&ssam_node_hid_sam_touchpad,
&ssam_node_hid_sam_sensors,
&ssam_node_hid_sam_ucm_ucsi,
&ssam_node_hid_sam_sysctrl,
NULL,
};
/* Devices for Surface Laptop Studio 2. */
static const struct software_node *ssam_node_group_sls2[] = {
&ssam_node_root,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile_with_fan,
&ssam_node_tmp_sensors,
&ssam_node_fan_speed,
&ssam_node_pos_tablet_switch,
&ssam_node_hid_sam_keyboard,
&ssam_node_hid_sam_penstash,
&ssam_node_hid_sam_sensors,
&ssam_node_hid_sam_ucm_ucsi,
NULL,
};
/* Devices for Surface Laptop Go. */
static const struct software_node *ssam_node_group_slg1[] = {
&ssam_node_root,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile,
NULL,
};
/* Devices for Surface Pro 7 and Surface Pro 7+. */
static const struct software_node *ssam_node_group_sp7[] = {
&ssam_node_root,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile,
NULL,
};
/* Devices for Surface Pro 8 */
static const struct software_node *ssam_node_group_sp8[] = {
&ssam_node_root,
&ssam_node_hub_kip,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile,
&ssam_node_kip_tablet_switch,
&ssam_node_hid_kip_keyboard,
&ssam_node_hid_kip_penstash,
&ssam_node_hid_kip_touchpad,
&ssam_node_hid_kip_fwupd,
&ssam_node_hid_sam_sensors,
&ssam_node_hid_sam_ucm_ucsi,
NULL,
};
/* Devices for Surface Pro 9, 10 and 11 (Intel/x86) */
static const struct software_node *ssam_node_group_sp9[] = {
&ssam_node_root,
&ssam_node_hub_kip,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_perf_profile_with_fan,
&ssam_node_tmp_sensors,
&ssam_node_fan_speed,
&ssam_node_pos_tablet_switch,
&ssam_node_hid_kip_keyboard,
&ssam_node_hid_kip_penstash,
&ssam_node_hid_kip_touchpad,
&ssam_node_hid_kip_fwupd,
&ssam_node_hid_sam_sensors,
&ssam_node_hid_sam_ucm_ucsi,
NULL,
};
/* Devices for Surface Pro 9 5G (ARM/QCOM) */
static const struct software_node *ssam_node_group_sp9_5g[] = {
&ssam_node_root,
&ssam_node_hub_kip,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_sensors,
&ssam_node_hid_kip_keyboard,
&ssam_node_hid_kip_penstash,
&ssam_node_hid_kip_touchpad,
&ssam_node_hid_kip_fwupd,
&ssam_node_hid_sam_sensors,
&ssam_node_kip_tablet_switch,
NULL,
};
/* Devices for Surface Pro 11 (ARM/QCOM) */
static const struct software_node *ssam_node_group_sp11[] = {
&ssam_node_root,
&ssam_node_hub_kip,
&ssam_node_bat_ac,
&ssam_node_bat_main,
&ssam_node_tmp_sensors,
&ssam_node_hid_kip_keyboard,
&ssam_node_hid_kip_penstash,
&ssam_node_hid_kip_touchpad,
&ssam_node_hid_kip_fwupd,
&ssam_node_hid_sam_sensors,
&ssam_node_kip_tablet_switch,
NULL,
};
/* Devices for Surface Pro 12" first edition (ARM/QCOM) */
static const struct software_node *ssam_node_group_sp12in[] = {
&ssam_node_root,
&ssam_node_hub_kip,
&ssam_node_tmp_sensors,
&ssam_node_hid_kip_keyboard,
&ssam_node_hid_sam_penstash,
&ssam_node_hid_kip_touchpad,
&ssam_node_hid_kip_fwupd,
&ssam_node_pos_tablet_switch,
NULL,
};
/* -- SSAM platform/meta-hub driver. ---------------------------------------- */
static const struct acpi_device_id ssam_platform_hub_acpi_match[] = {
/* Surface Pro 4, 5, and 6 (OMBR < 0x10) */
{ "MSHW0081", (unsigned long)ssam_node_group_gen5 },
/* Surface Pro 6 (OMBR >= 0x10) */
{ "MSHW0111", (unsigned long)ssam_node_group_gen5 },
/* Surface Pro 7 */
{ "MSHW0116", (unsigned long)ssam_node_group_sp7 },
/* Surface Pro 7+ */
{ "MSHW0119", (unsigned long)ssam_node_group_sp7 },
/* Surface Pro 8 */
{ "MSHW0263", (unsigned long)ssam_node_group_sp8 },
/* Surface Pro 9 */
{ "MSHW0343", (unsigned long)ssam_node_group_sp9 },
/* Surface Pro 10 */
{ "MSHW0510", (unsigned long)ssam_node_group_sp9 },
/* Surface Pro 11 */
{ "MSHW0583", (unsigned long)ssam_node_group_sp9 },
/* Surface Book 2 */
{ "MSHW0107", (unsigned long)ssam_node_group_gen5 },
/* Surface Book 3 */
{ "MSHW0117", (unsigned long)ssam_node_group_sb3 },
/* Surface Laptop 1 */
{ "MSHW0086", (unsigned long)ssam_node_group_gen5 },
/* Surface Laptop 2 */
{ "MSHW0112", (unsigned long)ssam_node_group_gen5 },
/* Surface Laptop 3 (13", Intel) */
{ "MSHW0114", (unsigned long)ssam_node_group_sl3 },
/* Surface Laptop 3 (15", AMD) and 4 (15", AMD) */
{ "MSHW0110", (unsigned long)ssam_node_group_sl3 },
/* Surface Laptop 4 (13", Intel) */
{ "MSHW0250", (unsigned long)ssam_node_group_sl3 },
/* Surface Laptop 5 */
{ "MSHW0350", (unsigned long)ssam_node_group_sl5 },
/* Surface Laptop 6 */
{ "MSHW0530", (unsigned long)ssam_node_group_sl6 },
/* Surface Laptop Go 1 */
{ "MSHW0118", (unsigned long)ssam_node_group_slg1 },
/* Surface Laptop Go 2 */
{ "MSHW0290", (unsigned long)ssam_node_group_slg1 },
/* Surface Laptop Go 3 */
{ "MSHW0440", (unsigned long)ssam_node_group_slg1 },
/* Surface Laptop Studio 1 */
{ "MSHW0123", (unsigned long)ssam_node_group_sls1 },
/* Surface Laptop Studio 2 */
{ "MSHW0360", (unsigned long)ssam_node_group_sls2 },
{ },
};
MODULE_DEVICE_TABLE(acpi, ssam_platform_hub_acpi_match);
static const struct of_device_id ssam_platform_hub_of_match[] __maybe_unused = {
/* Surface Pro 9 5G (ARM/QCOM) */
{ .compatible = "microsoft,arcata", (void *)ssam_node_group_sp9_5g },
/* Surface Pro 11 (ARM/QCOM) */
{ .compatible = "microsoft,denali", (void *)ssam_node_group_sp11 },
/* Surface Pro 12in First Edition (ARM/QCOM) */
{ .compatible = "microsoft,surface-pro-12in", (void *)ssam_node_group_sp12in },
/* Surface Laptop 7 */
{ .compatible = "microsoft,romulus13", (void *)ssam_node_group_sl7 },
{ .compatible = "microsoft,romulus15", (void *)ssam_node_group_sl7 },
{ },
};
static int ssam_platform_hub_probe(struct platform_device *pdev)
{
const struct software_node **nodes;
struct ssam_controller *ctrl;
struct fwnode_handle *root;
int status;
nodes = (const struct software_node **)acpi_device_get_match_data(&pdev->dev);
if (!nodes) {
nodes = (const struct software_node **)of_machine_get_match_data(ssam_platform_hub_of_match);
if (!nodes)
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);
status = software_node_register_node_group(nodes);
if (status)
return status;
root = software_node_fwnode(&ssam_node_root);
if (!root) {
software_node_unregister_node_group(nodes);
return -ENOENT;
}
set_secondary_fwnode(&pdev->dev, root);
status = __ssam_register_clients(&pdev->dev, ctrl, root);
if (status) {
set_secondary_fwnode(&pdev->dev, NULL);
software_node_unregister_node_group(nodes);
}
platform_set_drvdata(pdev, nodes);
return status;
}
static void ssam_platform_hub_remove(struct platform_device *pdev)
{
const struct software_node **nodes = platform_get_drvdata(pdev);
ssam_remove_clients(&pdev->dev);
set_secondary_fwnode(&pdev->dev, NULL);
software_node_unregister_node_group(nodes);
}
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_acpi_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
module_platform_driver(ssam_platform_hub_driver);
MODULE_ALIAS("platform:surface_aggregator_platform_hub");
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Device-registry for Surface System Aggregator Module");
MODULE_LICENSE("GPL");
@@ -0,0 +1,645 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Surface System Aggregator Module (SSAM) tablet mode switch driver.
*
* Copyright (C) 2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/unaligned.h>
#include <linux/input.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/workqueue.h>
#include <linux/surface_aggregator/controller.h>
#include <linux/surface_aggregator/device.h>
/* -- SSAM generic tablet switch driver framework. -------------------------- */
struct ssam_tablet_sw;
struct ssam_tablet_sw_state {
u32 source;
u32 state;
};
struct ssam_tablet_sw_ops {
int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state);
const char *(*state_name)(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state);
bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state);
};
struct ssam_tablet_sw {
struct ssam_device *sdev;
struct ssam_tablet_sw_state state;
struct work_struct update_work;
struct input_dev *mode_switch;
struct ssam_tablet_sw_ops ops;
struct ssam_event_notifier notif;
};
struct ssam_tablet_sw_desc {
struct {
const char *name;
const char *phys;
} dev;
struct {
u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event);
int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state);
const char *(*state_name)(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state);
bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state);
} ops;
struct {
struct ssam_event_registry reg;
struct ssam_event_id id;
enum ssam_event_mask mask;
u8 flags;
} event;
};
static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct ssam_tablet_sw *sw = dev_get_drvdata(dev);
const char *state = sw->ops.state_name(sw, &sw->state);
return sysfs_emit(buf, "%s\n", state);
}
static DEVICE_ATTR_RO(state);
static struct attribute *ssam_tablet_sw_attrs[] = {
&dev_attr_state.attr,
NULL,
};
static const struct attribute_group ssam_tablet_sw_group = {
.attrs = ssam_tablet_sw_attrs,
};
static void ssam_tablet_sw_update_workfn(struct work_struct *work)
{
struct ssam_tablet_sw *sw = container_of(work, struct ssam_tablet_sw, update_work);
struct ssam_tablet_sw_state state;
int tablet, status;
status = sw->ops.get_state(sw, &state);
if (status)
return;
if (sw->state.source == state.source && sw->state.state == state.state)
return;
sw->state = state;
/* Send SW_TABLET_MODE event. */
tablet = sw->ops.state_is_tablet_mode(sw, &state);
input_report_switch(sw->mode_switch, SW_TABLET_MODE, tablet);
input_sync(sw->mode_switch);
}
static int __maybe_unused ssam_tablet_sw_resume(struct device *dev)
{
struct ssam_tablet_sw *sw = dev_get_drvdata(dev);
schedule_work(&sw->update_work);
return 0;
}
static SIMPLE_DEV_PM_OPS(ssam_tablet_sw_pm_ops, NULL, ssam_tablet_sw_resume);
static int ssam_tablet_sw_probe(struct ssam_device *sdev)
{
const struct ssam_tablet_sw_desc *desc;
struct ssam_tablet_sw *sw;
int tablet, status;
desc = ssam_device_get_match_data(sdev);
if (!desc) {
WARN(1, "no driver match data specified");
return -EINVAL;
}
sw = devm_kzalloc(&sdev->dev, sizeof(*sw), GFP_KERNEL);
if (!sw)
return -ENOMEM;
sw->sdev = sdev;
sw->ops.get_state = desc->ops.get_state;
sw->ops.state_name = desc->ops.state_name;
sw->ops.state_is_tablet_mode = desc->ops.state_is_tablet_mode;
INIT_WORK(&sw->update_work, ssam_tablet_sw_update_workfn);
ssam_device_set_drvdata(sdev, sw);
/* Get initial state. */
status = sw->ops.get_state(sw, &sw->state);
if (status)
return status;
/* Set up tablet mode switch. */
sw->mode_switch = devm_input_allocate_device(&sdev->dev);
if (!sw->mode_switch)
return -ENOMEM;
sw->mode_switch->name = desc->dev.name;
sw->mode_switch->phys = desc->dev.phys;
sw->mode_switch->id.bustype = BUS_HOST;
sw->mode_switch->dev.parent = &sdev->dev;
tablet = sw->ops.state_is_tablet_mode(sw, &sw->state);
input_set_capability(sw->mode_switch, EV_SW, SW_TABLET_MODE);
input_report_switch(sw->mode_switch, SW_TABLET_MODE, tablet);
status = input_register_device(sw->mode_switch);
if (status)
return status;
/* Set up notifier. */
sw->notif.base.priority = 0;
sw->notif.base.fn = desc->ops.notify;
sw->notif.event.reg = desc->event.reg;
sw->notif.event.id = desc->event.id;
sw->notif.event.mask = desc->event.mask;
sw->notif.event.flags = SSAM_EVENT_SEQUENCED;
status = ssam_device_notifier_register(sdev, &sw->notif);
if (status)
return status;
status = sysfs_create_group(&sdev->dev.kobj, &ssam_tablet_sw_group);
if (status)
goto err;
/* We might have missed events during setup, so check again. */
schedule_work(&sw->update_work);
return 0;
err:
ssam_device_notifier_unregister(sdev, &sw->notif);
cancel_work_sync(&sw->update_work);
return status;
}
static void ssam_tablet_sw_remove(struct ssam_device *sdev)
{
struct ssam_tablet_sw *sw = ssam_device_get_drvdata(sdev);
sysfs_remove_group(&sdev->dev.kobj, &ssam_tablet_sw_group);
ssam_device_notifier_unregister(sdev, &sw->notif);
cancel_work_sync(&sw->update_work);
}
/* -- SSAM KIP tablet switch implementation. -------------------------------- */
#define SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED 0x1d
enum ssam_kip_cover_state {
SSAM_KIP_COVER_STATE_DISCONNECTED = 0x01,
SSAM_KIP_COVER_STATE_CLOSED = 0x02,
SSAM_KIP_COVER_STATE_LAPTOP = 0x03,
SSAM_KIP_COVER_STATE_FOLDED_CANVAS = 0x04,
SSAM_KIP_COVER_STATE_FOLDED_BACK = 0x05,
SSAM_KIP_COVER_STATE_BOOK = 0x06,
};
static const char *ssam_kip_cover_state_name(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state)
{
switch (state->state) {
case SSAM_KIP_COVER_STATE_DISCONNECTED:
return "disconnected";
case SSAM_KIP_COVER_STATE_CLOSED:
return "closed";
case SSAM_KIP_COVER_STATE_LAPTOP:
return "laptop";
case SSAM_KIP_COVER_STATE_FOLDED_CANVAS:
return "folded-canvas";
case SSAM_KIP_COVER_STATE_FOLDED_BACK:
return "folded-back";
case SSAM_KIP_COVER_STATE_BOOK:
return "book";
default:
dev_warn(&sw->sdev->dev, "unknown KIP cover state: %u\n", state->state);
return "<unknown>";
}
}
static bool ssam_kip_cover_state_is_tablet_mode(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state)
{
switch (state->state) {
case SSAM_KIP_COVER_STATE_DISCONNECTED:
case SSAM_KIP_COVER_STATE_FOLDED_CANVAS:
case SSAM_KIP_COVER_STATE_FOLDED_BACK:
case SSAM_KIP_COVER_STATE_BOOK:
return true;
case SSAM_KIP_COVER_STATE_CLOSED:
case SSAM_KIP_COVER_STATE_LAPTOP:
return false;
default:
dev_warn(&sw->sdev->dev, "unknown KIP cover state: %d\n", state->state);
return true;
}
}
SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_get_cover_state, u8, {
.target_category = SSAM_SSH_TC_KIP,
.target_id = SSAM_SSH_TID_SAM,
.command_id = 0x1d,
.instance_id = 0x00,
});
static int ssam_kip_get_cover_state(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state)
{
int status;
u8 raw;
status = ssam_retry(__ssam_kip_get_cover_state, sw->sdev->ctrl, &raw);
if (status < 0) {
dev_err(&sw->sdev->dev, "failed to query KIP lid state: %d\n", status);
return status;
}
state->source = 0; /* Unused for KIP switch. */
state->state = raw;
return 0;
}
static u32 ssam_kip_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
{
struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif);
if (event->command_id != SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED)
return 0; /* Return "unhandled". */
if (event->length < 1)
dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length);
schedule_work(&sw->update_work);
return SSAM_NOTIF_HANDLED;
}
static const struct ssam_tablet_sw_desc ssam_kip_sw_desc = {
.dev = {
.name = "Microsoft Surface KIP Tablet Mode Switch",
.phys = "ssam/01:0e:01:00:01/input0",
},
.ops = {
.notify = ssam_kip_sw_notif,
.get_state = ssam_kip_get_cover_state,
.state_name = ssam_kip_cover_state_name,
.state_is_tablet_mode = ssam_kip_cover_state_is_tablet_mode,
},
.event = {
.reg = SSAM_EVENT_REGISTRY_SAM,
.id = {
.target_category = SSAM_SSH_TC_KIP,
.instance = 0,
},
.mask = SSAM_EVENT_MASK_TARGET,
},
};
/* -- SSAM POS tablet switch implementation. -------------------------------- */
static bool tablet_mode_in_slate_state = true;
module_param(tablet_mode_in_slate_state, bool, 0644);
MODULE_PARM_DESC(tablet_mode_in_slate_state, "Enable tablet mode in slate device posture, default is 'true'");
#define SSAM_EVENT_POS_CID_POSTURE_CHANGED 0x03
#define SSAM_POS_MAX_SOURCES 4
enum ssam_pos_source_id {
SSAM_POS_SOURCE_COVER = 0x00,
SSAM_POS_SOURCE_SLS = 0x03,
};
enum ssam_pos_state_cover {
SSAM_POS_COVER_DISCONNECTED = 0x01,
SSAM_POS_COVER_CLOSED = 0x02,
SSAM_POS_COVER_LAPTOP = 0x03,
SSAM_POS_COVER_FOLDED_CANVAS = 0x04,
SSAM_POS_COVER_FOLDED_BACK = 0x05,
SSAM_POS_COVER_BOOK = 0x06,
};
enum ssam_pos_state_sls {
SSAM_POS_SLS_LID_CLOSED = 0x00,
SSAM_POS_SLS_LAPTOP = 0x01,
SSAM_POS_SLS_SLATE = 0x02,
SSAM_POS_SLS_TABLET = 0x03,
};
struct ssam_sources_list {
__le32 count;
__le32 id[SSAM_POS_MAX_SOURCES];
} __packed;
static const char *ssam_pos_state_name_cover(struct ssam_tablet_sw *sw, u32 state)
{
switch (state) {
case SSAM_POS_COVER_DISCONNECTED:
return "disconnected";
case SSAM_POS_COVER_CLOSED:
return "closed";
case SSAM_POS_COVER_LAPTOP:
return "laptop";
case SSAM_POS_COVER_FOLDED_CANVAS:
return "folded-canvas";
case SSAM_POS_COVER_FOLDED_BACK:
return "folded-back";
case SSAM_POS_COVER_BOOK:
return "book";
default:
dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n", state);
return "<unknown>";
}
}
static const char *ssam_pos_state_name_sls(struct ssam_tablet_sw *sw, u32 state)
{
switch (state) {
case SSAM_POS_SLS_LID_CLOSED:
return "closed";
case SSAM_POS_SLS_LAPTOP:
return "laptop";
case SSAM_POS_SLS_SLATE:
return "slate";
case SSAM_POS_SLS_TABLET:
return "tablet";
default:
dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n", state);
return "<unknown>";
}
}
static const char *ssam_pos_state_name(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state)
{
switch (state->source) {
case SSAM_POS_SOURCE_COVER:
return ssam_pos_state_name_cover(sw, state->state);
case SSAM_POS_SOURCE_SLS:
return ssam_pos_state_name_sls(sw, state->state);
default:
dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n", state->source);
return "<unknown>";
}
}
static bool ssam_pos_state_is_tablet_mode_cover(struct ssam_tablet_sw *sw, u32 state)
{
switch (state) {
case SSAM_POS_COVER_DISCONNECTED:
case SSAM_POS_COVER_FOLDED_CANVAS:
case SSAM_POS_COVER_FOLDED_BACK:
case SSAM_POS_COVER_BOOK:
return true;
case SSAM_POS_COVER_CLOSED:
case SSAM_POS_COVER_LAPTOP:
return false;
default:
dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n", state);
return true;
}
}
static bool ssam_pos_state_is_tablet_mode_sls(struct ssam_tablet_sw *sw, u32 state)
{
switch (state) {
case SSAM_POS_SLS_LAPTOP:
case SSAM_POS_SLS_LID_CLOSED:
return false;
case SSAM_POS_SLS_SLATE:
return tablet_mode_in_slate_state;
case SSAM_POS_SLS_TABLET:
return true;
default:
dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n", state);
return true;
}
}
static bool ssam_pos_state_is_tablet_mode(struct ssam_tablet_sw *sw,
const struct ssam_tablet_sw_state *state)
{
switch (state->source) {
case SSAM_POS_SOURCE_COVER:
return ssam_pos_state_is_tablet_mode_cover(sw, state->state);
case SSAM_POS_SOURCE_SLS:
return ssam_pos_state_is_tablet_mode_sls(sw, state->state);
default:
dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n", state->source);
return true;
}
}
static int ssam_pos_get_sources_list(struct ssam_tablet_sw *sw, struct ssam_sources_list *sources)
{
struct ssam_request rqst;
struct ssam_response rsp;
int status;
rqst.target_category = SSAM_SSH_TC_POS;
rqst.target_id = SSAM_SSH_TID_SAM;
rqst.command_id = 0x01;
rqst.instance_id = 0x00;
rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
rqst.length = 0;
rqst.payload = NULL;
rsp.capacity = sizeof(*sources);
rsp.length = 0;
rsp.pointer = (u8 *)sources;
status = ssam_retry(ssam_request_do_sync_onstack, sw->sdev->ctrl, &rqst, &rsp, 0);
if (status)
return status;
/* We need at least the 'sources->count' field. */
if (rsp.length < sizeof(__le32)) {
dev_err(&sw->sdev->dev, "received source list response is too small\n");
return -EPROTO;
}
/* Make sure 'sources->count' matches with the response length. */
if (get_unaligned_le32(&sources->count) * sizeof(__le32) + sizeof(__le32) != rsp.length) {
dev_err(&sw->sdev->dev, "mismatch between number of sources and response size\n");
return -EPROTO;
}
return 0;
}
static int ssam_pos_get_source(struct ssam_tablet_sw *sw, u32 *source_id)
{
struct ssam_sources_list sources = {};
int status;
status = ssam_pos_get_sources_list(sw, &sources);
if (status)
return status;
if (get_unaligned_le32(&sources.count) == 0) {
dev_err(&sw->sdev->dev, "no posture sources found\n");
return -ENODEV;
}
/*
* We currently don't know what to do with more than one posture
* source. At the moment, only one source seems to be used/provided.
* The WARN_ON() here should hopefully let us know quickly once there
* is a device that provides multiple sources, at which point we can
* then try to figure out how to handle them.
*/
WARN_ON(get_unaligned_le32(&sources.count) > 1);
*source_id = get_unaligned_le32(&sources.id[0]);
return 0;
}
SSAM_DEFINE_SYNC_REQUEST_WR(__ssam_pos_get_posture_for_source, __le32, __le32, {
.target_category = SSAM_SSH_TC_POS,
.target_id = SSAM_SSH_TID_SAM,
.command_id = 0x02,
.instance_id = 0x00,
});
static int ssam_pos_get_posture_for_source(struct ssam_tablet_sw *sw, u32 source_id, u32 *posture)
{
__le32 source_le = cpu_to_le32(source_id);
__le32 rspval_le = 0;
int status;
status = ssam_retry(__ssam_pos_get_posture_for_source, sw->sdev->ctrl,
&source_le, &rspval_le);
if (status)
return status;
*posture = le32_to_cpu(rspval_le);
return 0;
}
static int ssam_pos_get_posture(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state)
{
u32 source_id;
u32 source_state;
int status;
status = ssam_pos_get_source(sw, &source_id);
if (status) {
dev_err(&sw->sdev->dev, "failed to get posture source ID: %d\n", status);
return status;
}
status = ssam_pos_get_posture_for_source(sw, source_id, &source_state);
if (status) {
dev_err(&sw->sdev->dev, "failed to get posture value for source %u: %d\n",
source_id, status);
return status;
}
state->source = source_id;
state->state = source_state;
return 0;
}
static u32 ssam_pos_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
{
struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif);
if (event->command_id != SSAM_EVENT_POS_CID_POSTURE_CHANGED)
return 0; /* Return "unhandled". */
if (event->length != sizeof(__le32) * 3)
dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length);
schedule_work(&sw->update_work);
return SSAM_NOTIF_HANDLED;
}
static const struct ssam_tablet_sw_desc ssam_pos_sw_desc = {
.dev = {
.name = "Microsoft Surface POS Tablet Mode Switch",
.phys = "ssam/01:26:01:00:01/input0",
},
.ops = {
.notify = ssam_pos_sw_notif,
.get_state = ssam_pos_get_posture,
.state_name = ssam_pos_state_name,
.state_is_tablet_mode = ssam_pos_state_is_tablet_mode,
},
.event = {
.reg = SSAM_EVENT_REGISTRY_SAM,
.id = {
.target_category = SSAM_SSH_TC_POS,
.instance = 0,
},
.mask = SSAM_EVENT_MASK_TARGET,
},
};
/* -- Driver registration. -------------------------------------------------- */
static const struct ssam_device_id ssam_tablet_sw_match[] = {
{ SSAM_SDEV(KIP, SAM, 0x00, 0x01), (unsigned long)&ssam_kip_sw_desc },
{ SSAM_SDEV(POS, SAM, 0x00, 0x01), (unsigned long)&ssam_pos_sw_desc },
{ },
};
MODULE_DEVICE_TABLE(ssam, ssam_tablet_sw_match);
static struct ssam_device_driver ssam_tablet_sw_driver = {
.probe = ssam_tablet_sw_probe,
.remove = ssam_tablet_sw_remove,
.match_table = ssam_tablet_sw_match,
.driver = {
.name = "surface_aggregator_tablet_mode_switch",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
.pm = &ssam_tablet_sw_pm_ops,
},
};
module_ssam_device_driver(ssam_tablet_sw_driver);
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Tablet mode switch driver for Surface devices using the Surface Aggregator Module");
MODULE_LICENSE("GPL");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,352 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Surface GPE/Lid driver to enable wakeup from suspend via the lid by
* properly configuring the respective GPEs. Required for wakeup via lid on
* newer Intel-based Microsoft Surface devices.
*
* Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
/*
* Note: The GPE numbers for the lid devices found below have been obtained
* from ACPI/the DSDT table, specifically from the GPE handler for the
* lid.
*/
static const struct property_entry lid_device_props_l17[] = {
PROPERTY_ENTRY_U32("gpe", 0x17),
{},
};
static const struct property_entry lid_device_props_l4B[] = {
PROPERTY_ENTRY_U32("gpe", 0x4B),
{},
};
static const struct property_entry lid_device_props_l4D[] = {
PROPERTY_ENTRY_U32("gpe", 0x4D),
{},
};
static const struct property_entry lid_device_props_l4F[] = {
PROPERTY_ENTRY_U32("gpe", 0x4F),
{},
};
static const struct property_entry lid_device_props_l57[] = {
PROPERTY_ENTRY_U32("gpe", 0x57),
{},
};
/*
* Note: When changing this, don't forget to check that the MODULE_ALIAS below
* still fits.
*/
static const struct dmi_system_id dmi_lid_device_table[] = {
{
.ident = "Surface Pro 4",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 4"),
},
.driver_data = (void *)lid_device_props_l17,
},
{
.ident = "Surface Pro 5",
.matches = {
/*
* We match for SKU here due to generic product name
* "Surface Pro".
*/
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Pro_1796"),
},
.driver_data = (void *)lid_device_props_l4F,
},
{
.ident = "Surface Pro 5 (LTE)",
.matches = {
/*
* We match for SKU here due to generic product name
* "Surface Pro"
*/
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Pro_1807"),
},
.driver_data = (void *)lid_device_props_l4F,
},
{
.ident = "Surface Pro 6",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 6"),
},
.driver_data = (void *)lid_device_props_l4F,
},
{
.ident = "Surface Pro 7",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 7"),
},
.driver_data = (void *)lid_device_props_l4D,
},
{
.ident = "Surface Pro 8",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 8"),
},
.driver_data = (void *)lid_device_props_l4B,
},
{
.ident = "Surface Book 1",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book"),
},
.driver_data = (void *)lid_device_props_l17,
},
{
.ident = "Surface Book 2",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book 2"),
},
.driver_data = (void *)lid_device_props_l17,
},
{
.ident = "Surface Book 3",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book 3"),
},
.driver_data = (void *)lid_device_props_l4D,
},
{
.ident = "Surface Laptop 1",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop"),
},
.driver_data = (void *)lid_device_props_l57,
},
{
.ident = "Surface Laptop 2",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop 2"),
},
.driver_data = (void *)lid_device_props_l57,
},
{
.ident = "Surface Laptop 3 (Intel 13\")",
.matches = {
/*
* We match for SKU here due to different variants: The
* AMD (15") version does not rely on GPEs.
*/
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_3_1867:1868"),
},
.driver_data = (void *)lid_device_props_l4D,
},
{
.ident = "Surface Laptop 3 (Intel 15\")",
.matches = {
/*
* We match for SKU here due to different variants: The
* AMD (15") version does not rely on GPEs.
*/
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_3_1872"),
},
.driver_data = (void *)lid_device_props_l4D,
},
{
.ident = "Surface Laptop 4 (Intel 13\")",
.matches = {
/*
* We match for SKU here due to different variants: The
* AMD (15") version does not rely on GPEs.
*/
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_4_1950:1951"),
},
.driver_data = (void *)lid_device_props_l4B,
},
{
.ident = "Surface Laptop Studio",
.matches = {
DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop Studio"),
},
.driver_data = (void *)lid_device_props_l4B,
},
{ }
};
struct surface_lid_device {
u32 gpe_number;
};
static int surface_lid_enable_wakeup(struct device *dev, bool enable)
{
const struct surface_lid_device *lid = dev_get_drvdata(dev);
int action = enable ? ACPI_GPE_ENABLE : ACPI_GPE_DISABLE;
acpi_status status;
status = acpi_set_gpe_wake_mask(NULL, lid->gpe_number, action);
if (ACPI_FAILURE(status)) {
dev_err(dev, "failed to set GPE wake mask: %s\n",
acpi_format_exception(status));
return -EINVAL;
}
return 0;
}
static int __maybe_unused surface_gpe_suspend(struct device *dev)
{
return surface_lid_enable_wakeup(dev, true);
}
static int __maybe_unused surface_gpe_resume(struct device *dev)
{
return surface_lid_enable_wakeup(dev, false);
}
static SIMPLE_DEV_PM_OPS(surface_gpe_pm, surface_gpe_suspend, surface_gpe_resume);
static int surface_gpe_probe(struct platform_device *pdev)
{
struct surface_lid_device *lid;
u32 gpe_number;
acpi_status status;
int ret;
ret = device_property_read_u32(&pdev->dev, "gpe", &gpe_number);
if (ret) {
dev_err(&pdev->dev, "failed to read 'gpe' property: %d\n", ret);
return ret;
}
lid = devm_kzalloc(&pdev->dev, sizeof(*lid), GFP_KERNEL);
if (!lid)
return -ENOMEM;
lid->gpe_number = gpe_number;
platform_set_drvdata(pdev, lid);
status = acpi_mark_gpe_for_wake(NULL, gpe_number);
if (ACPI_FAILURE(status)) {
dev_err(&pdev->dev, "failed to mark GPE for wake: %s\n",
acpi_format_exception(status));
return -EINVAL;
}
status = acpi_enable_gpe(NULL, gpe_number);
if (ACPI_FAILURE(status)) {
dev_err(&pdev->dev, "failed to enable GPE: %s\n",
acpi_format_exception(status));
return -EINVAL;
}
ret = surface_lid_enable_wakeup(&pdev->dev, false);
if (ret)
acpi_disable_gpe(NULL, gpe_number);
return ret;
}
static void surface_gpe_remove(struct platform_device *pdev)
{
struct surface_lid_device *lid = dev_get_drvdata(&pdev->dev);
/* restore default behavior without this module */
surface_lid_enable_wakeup(&pdev->dev, false);
acpi_disable_gpe(NULL, lid->gpe_number);
}
static struct platform_driver surface_gpe_driver = {
.probe = surface_gpe_probe,
.remove = surface_gpe_remove,
.driver = {
.name = "surface_gpe",
.pm = &surface_gpe_pm,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
static struct platform_device *surface_gpe_device;
static int __init surface_gpe_init(void)
{
const struct dmi_system_id *match;
struct platform_device *pdev;
struct fwnode_handle *fwnode;
int status;
match = dmi_first_match(dmi_lid_device_table);
if (!match) {
pr_info("no compatible Microsoft Surface device found, exiting\n");
return -ENODEV;
}
status = platform_driver_register(&surface_gpe_driver);
if (status)
return status;
fwnode = fwnode_create_software_node(match->driver_data, NULL);
if (IS_ERR(fwnode)) {
status = PTR_ERR(fwnode);
goto err_node;
}
pdev = platform_device_alloc("surface_gpe", PLATFORM_DEVID_NONE);
if (!pdev) {
status = -ENOMEM;
goto err_alloc;
}
pdev->dev.fwnode = fwnode;
status = platform_device_add(pdev);
if (status)
goto err_add;
surface_gpe_device = pdev;
return 0;
err_add:
platform_device_put(pdev);
err_alloc:
fwnode_remove_software_node(fwnode);
err_node:
platform_driver_unregister(&surface_gpe_driver);
return status;
}
module_init(surface_gpe_init);
static void __exit surface_gpe_exit(void)
{
struct fwnode_handle *fwnode = surface_gpe_device->dev.fwnode;
platform_device_unregister(surface_gpe_device);
platform_driver_unregister(&surface_gpe_driver);
fwnode_remove_software_node(fwnode);
}
module_exit(surface_gpe_exit);
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Surface GPE/Lid Driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("dmi:*:svnMicrosoftCorporation:pnSurface*:*");
@@ -0,0 +1,273 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* 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
* devices. It is specifically required when the hot-plug device is in D3cold
* and can thus not generate PCIe hot-plug events itself.
*
* 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-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/acpi.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
static const struct acpi_gpio_params shps_base_presence_int = { 0, 0, false };
static const struct acpi_gpio_params shps_base_presence = { 1, 0, false };
static const struct acpi_gpio_params shps_device_power_int = { 2, 0, false };
static const struct acpi_gpio_params shps_device_power = { 3, 0, false };
static const struct acpi_gpio_params shps_device_presence_int = { 4, 0, false };
static const struct acpi_gpio_params shps_device_presence = { 5, 0, false };
static const struct acpi_gpio_mapping shps_acpi_gpios[] = {
{ "base_presence-int-gpio", &shps_base_presence_int, 1 },
{ "base_presence-gpio", &shps_base_presence, 1 },
{ "device_power-int-gpio", &shps_device_power_int, 1 },
{ "device_power-gpio", &shps_device_power, 1 },
{ "device_presence-int-gpio", &shps_device_presence_int, 1 },
{ "device_presence-gpio", &shps_device_presence, 1 },
{ },
};
/* 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);
#define SHPS_DSM_REVISION 1
enum shps_dsm_fn {
SHPS_DSM_FN_PCI_NUM_ENTRIES = 0x01,
SHPS_DSM_FN_PCI_GET_ENTRIES = 0x02,
SHPS_DSM_FN_IRQ_BASE_PRESENCE = 0x03,
SHPS_DSM_FN_IRQ_DEVICE_POWER = 0x04,
SHPS_DSM_FN_IRQ_DEVICE_PRESENCE = 0x05,
};
enum shps_irq_type {
/* 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,
};
static const char *const shps_gpio_names[] = {
[SHPS_IRQ_TYPE_BASE_PRESENCE] = "base_presence",
[SHPS_IRQ_TYPE_DEVICE_POWER] = "device_power",
[SHPS_IRQ_TYPE_DEVICE_PRESENCE] = "device_presence",
};
struct shps_device {
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 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);
union acpi_object *result;
union acpi_object param;
int value;
mutex_lock(&sdev->lock[type]);
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);
return;
}
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_typed(handle, &shps_dsm_guid, SHPS_DSM_REVISION,
shps_dsm_fn_for_irq(type), &param, ACPI_TYPE_BUFFER);
if (!result) {
dev_err(&pdev->dev, "IRQ notification via DSM failed (irq=%d, gpio=%d)\n",
type, value);
} 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);
}
static irqreturn_t shps_handle_irq(int irq, void *data)
{
struct platform_device *pdev = data;
struct shps_device *sdev = platform_get_drvdata(pdev);
int type;
/* Figure out which IRQ we're handling. */
for (type = 0; type < SHPS_NUM_IRQS; type++)
if (irq == sdev->irq[type])
break;
/* We should have found our interrupt, if not: this is a bug. */
if (WARN(type >= SHPS_NUM_IRQS, "invalid IRQ number: %d\n", irq))
return IRQ_HANDLED;
/* Forward interrupt to ACPI via DSM. */
shps_dsm_notify_irq(pdev, type);
return IRQ_HANDLED;
}
static int shps_setup_irq(struct platform_device *pdev, enum shps_irq_type type)
{
unsigned long flags = IRQF_ONESHOT | IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
struct shps_device *sdev = platform_get_drvdata(pdev);
struct gpio_desc *gpiod;
acpi_handle handle = ACPI_HANDLE(&pdev->dev);
const char *irq_name;
const int dsm = shps_dsm_fn_for_irq(type);
int status, irq;
/*
* 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);
return 0;
}
gpiod = devm_gpiod_get(&pdev->dev, shps_gpio_names[type], GPIOD_ASIS);
if (IS_ERR(gpiod))
return PTR_ERR(gpiod);
irq = gpiod_to_irq(gpiod);
if (irq < 0)
return irq;
irq_name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "shps-irq-%d", type);
if (!irq_name)
return -ENOMEM;
status = devm_request_threaded_irq(&pdev->dev, irq, NULL, shps_handle_irq,
flags, irq_name, pdev);
if (status)
return status;
dev_dbg(&pdev->dev, "set up irq %d as type %d\n", irq, type);
sdev->gpio[type] = gpiod;
sdev->irq[type] = irq;
return 0;
}
static void 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]);
}
}
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;
status = devm_acpi_dev_add_driver_gpios(&pdev->dev, shps_acpi_gpios);
if (status)
return status;
sdev = devm_kzalloc(&pdev->dev, sizeof(*sdev), GFP_KERNEL);
if (!sdev)
return -ENOMEM;
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);
goto err;
}
}
/* Ensure everything is up-to-date. */
for (i = 0; i < SHPS_NUM_IRQS; i++)
if (sdev->irq[i] != SHPS_IRQ_NOT_PRESENT)
shps_dsm_notify_irq(pdev, i);
return 0;
err:
surface_hotplug_remove(pdev);
return status;
}
static const struct acpi_device_id surface_hotplug_acpi_match[] = {
{ "MSHW0153", 0 },
{ },
};
MODULE_DEVICE_TABLE(acpi, surface_hotplug_acpi_match);
static struct platform_driver surface_hotplug_driver = {
.probe = surface_hotplug_probe,
.remove = surface_hotplug_remove,
.driver = {
.name = "surface_hotplug",
.acpi_match_table = surface_hotplug_acpi_match,
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
module_platform_driver(surface_hotplug_driver);
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Surface Hot-Plug Signaling Driver for Surface Book Devices");
MODULE_LICENSE("GPL");
@@ -0,0 +1,257 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Surface Platform Profile / Performance Mode driver for Surface System
* Aggregator Module (thermal and fan subsystem).
*
* Copyright (C) 2021-2022 Maximilian Luz <luzmaximilian@gmail.com>
*/
#include <linux/unaligned.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_profile.h>
#include <linux/types.h>
#include <linux/surface_aggregator/device.h>
// Enum for the platform performance profile sent to the TMP module.
enum ssam_tmp_profile {
SSAM_TMP_PROFILE_NORMAL = 1,
SSAM_TMP_PROFILE_BATTERY_SAVER = 2,
SSAM_TMP_PROFILE_BETTER_PERFORMANCE = 3,
SSAM_TMP_PROFILE_BEST_PERFORMANCE = 4,
};
// Enum for the fan profile sent to the FAN module. This fan profile is
// only sent to the EC if the 'has_fan' property is set. The integers are
// not a typo, they differ from the performance profile indices.
enum ssam_fan_profile {
SSAM_FAN_PROFILE_NORMAL = 2,
SSAM_FAN_PROFILE_BATTERY_SAVER = 1,
SSAM_FAN_PROFILE_BETTER_PERFORMANCE = 3,
SSAM_FAN_PROFILE_BEST_PERFORMANCE = 4,
};
struct ssam_tmp_profile_info {
__le32 profile;
__le16 unknown1;
__le16 unknown2;
} __packed;
struct ssam_platform_profile_device {
struct ssam_device *sdev;
struct device *ppdev;
bool has_fan;
};
SSAM_DEFINE_SYNC_REQUEST_CL_R(__ssam_tmp_profile_get, struct ssam_tmp_profile_info, {
.target_category = SSAM_SSH_TC_TMP,
.command_id = 0x02,
});
SSAM_DEFINE_SYNC_REQUEST_CL_W(__ssam_tmp_profile_set, __le32, {
.target_category = SSAM_SSH_TC_TMP,
.command_id = 0x03,
});
SSAM_DEFINE_SYNC_REQUEST_W(__ssam_fan_profile_set, u8, {
.target_category = SSAM_SSH_TC_FAN,
.target_id = SSAM_SSH_TID_SAM,
.command_id = 0x0e,
.instance_id = 0x01,
});
static int ssam_tmp_profile_get(struct ssam_device *sdev, enum ssam_tmp_profile *p)
{
struct ssam_tmp_profile_info info;
int status;
status = ssam_retry(__ssam_tmp_profile_get, sdev, &info);
if (status < 0)
return status;
*p = le32_to_cpu(info.profile);
return 0;
}
static int ssam_tmp_profile_set(struct ssam_device *sdev, enum ssam_tmp_profile p)
{
const __le32 profile_le = cpu_to_le32(p);
return ssam_retry(__ssam_tmp_profile_set, sdev, &profile_le);
}
static int ssam_fan_profile_set(struct ssam_device *sdev, enum ssam_fan_profile p)
{
const u8 profile = p;
return ssam_retry(__ssam_fan_profile_set, sdev->ctrl, &profile);
}
static int convert_ssam_tmp_to_profile(struct ssam_device *sdev, enum ssam_tmp_profile p)
{
switch (p) {
case SSAM_TMP_PROFILE_NORMAL:
return PLATFORM_PROFILE_BALANCED;
case SSAM_TMP_PROFILE_BATTERY_SAVER:
return PLATFORM_PROFILE_LOW_POWER;
case SSAM_TMP_PROFILE_BETTER_PERFORMANCE:
return PLATFORM_PROFILE_BALANCED_PERFORMANCE;
case SSAM_TMP_PROFILE_BEST_PERFORMANCE:
return PLATFORM_PROFILE_PERFORMANCE;
default:
dev_err(&sdev->dev, "invalid performance profile: %d", p);
return -EINVAL;
}
}
static int convert_profile_to_ssam_tmp(struct ssam_device *sdev, enum platform_profile_option p)
{
switch (p) {
case PLATFORM_PROFILE_LOW_POWER:
return SSAM_TMP_PROFILE_BATTERY_SAVER;
case PLATFORM_PROFILE_BALANCED:
return SSAM_TMP_PROFILE_NORMAL;
case PLATFORM_PROFILE_BALANCED_PERFORMANCE:
return SSAM_TMP_PROFILE_BETTER_PERFORMANCE;
case PLATFORM_PROFILE_PERFORMANCE:
return SSAM_TMP_PROFILE_BEST_PERFORMANCE;
default:
/* This should have already been caught by platform_profile_store(). */
WARN(true, "unsupported platform profile");
return -EOPNOTSUPP;
}
}
static int convert_profile_to_ssam_fan(struct ssam_device *sdev, enum platform_profile_option p)
{
switch (p) {
case PLATFORM_PROFILE_LOW_POWER:
return SSAM_FAN_PROFILE_BATTERY_SAVER;
case PLATFORM_PROFILE_BALANCED:
return SSAM_FAN_PROFILE_NORMAL;
case PLATFORM_PROFILE_BALANCED_PERFORMANCE:
return SSAM_FAN_PROFILE_BETTER_PERFORMANCE;
case PLATFORM_PROFILE_PERFORMANCE:
return SSAM_FAN_PROFILE_BEST_PERFORMANCE;
default:
/* This should have already been caught by platform_profile_store(). */
WARN(true, "unsupported platform profile");
return -EOPNOTSUPP;
}
}
static int ssam_platform_profile_get(struct device *dev,
enum platform_profile_option *profile)
{
struct ssam_platform_profile_device *tpd;
enum ssam_tmp_profile tp;
int status;
tpd = dev_get_drvdata(dev);
status = ssam_tmp_profile_get(tpd->sdev, &tp);
if (status)
return status;
status = convert_ssam_tmp_to_profile(tpd->sdev, tp);
if (status < 0)
return status;
*profile = status;
return 0;
}
static int ssam_platform_profile_set(struct device *dev,
enum platform_profile_option profile)
{
struct ssam_platform_profile_device *tpd;
int tp;
tpd = dev_get_drvdata(dev);
tp = convert_profile_to_ssam_tmp(tpd->sdev, profile);
if (tp < 0)
return tp;
tp = ssam_tmp_profile_set(tpd->sdev, tp);
if (tp < 0)
return tp;
if (tpd->has_fan) {
tp = convert_profile_to_ssam_fan(tpd->sdev, profile);
if (tp < 0)
return tp;
tp = ssam_fan_profile_set(tpd->sdev, tp);
}
return tp;
}
static int ssam_platform_profile_probe(void *drvdata, unsigned long *choices)
{
set_bit(PLATFORM_PROFILE_LOW_POWER, choices);
set_bit(PLATFORM_PROFILE_BALANCED, choices);
set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices);
set_bit(PLATFORM_PROFILE_PERFORMANCE, choices);
return 0;
}
static const struct platform_profile_ops ssam_platform_profile_ops = {
.probe = ssam_platform_profile_probe,
.profile_get = ssam_platform_profile_get,
.profile_set = ssam_platform_profile_set,
};
static int surface_platform_profile_probe(struct ssam_device *sdev)
{
struct ssam_platform_profile_device *tpd;
tpd = devm_kzalloc(&sdev->dev, sizeof(*tpd), GFP_KERNEL);
if (!tpd)
return -ENOMEM;
tpd->sdev = sdev;
ssam_device_set_drvdata(sdev, tpd);
tpd->has_fan = device_property_read_bool(&sdev->dev, "has_fan");
tpd->ppdev = devm_platform_profile_register(&sdev->dev, "Surface Platform Profile",
tpd, &ssam_platform_profile_ops);
return PTR_ERR_OR_ZERO(tpd->ppdev);
}
static const struct ssam_device_id ssam_platform_profile_match[] = {
{ SSAM_SDEV(TMP, SAM, 0x00, 0x01) },
{ },
};
MODULE_DEVICE_TABLE(ssam, ssam_platform_profile_match);
static struct ssam_device_driver surface_platform_profile = {
.probe = surface_platform_profile_probe,
.match_table = ssam_platform_profile_match,
.driver = {
.name = "surface_platform_profile",
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
module_ssam_device_driver(surface_platform_profile);
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Platform Profile Support for Surface System Aggregator Module");
MODULE_LICENSE("GPL");
@@ -0,0 +1,277 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* power/home/volume button support for
* Microsoft Surface Pro 3/4 tablet.
*
* Copyright (c) 2015 Intel Corporation.
* All rights reserved.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/input.h>
#include <linux/acpi.h>
#include <linux/platform_device.h>
#include <acpi/button.h>
#define SURFACE_PRO3_BUTTON_HID "MSHW0028"
#define SURFACE_PRO4_BUTTON_HID "MSHW0040"
#define SURFACE_BUTTON_OBJ_NAME "VGBI"
#define SURFACE_BUTTON_DEVICE_NAME "Surface Pro 3/4 Buttons"
#define MSHW0040_DSM_REVISION 0x01
#define MSHW0040_DSM_GET_OMPR 0x02 // get OEM Platform Revision
static const guid_t MSHW0040_DSM_UUID =
GUID_INIT(0x6fd05c69, 0xcde3, 0x49f4, 0x95, 0xed, 0xab, 0x16, 0x65,
0x49, 0x80, 0x35);
#define SURFACE_BUTTON_NOTIFY_TABLET_MODE 0xc8
#define SURFACE_BUTTON_NOTIFY_PRESS_POWER 0xc6
#define SURFACE_BUTTON_NOTIFY_RELEASE_POWER 0xc7
#define SURFACE_BUTTON_NOTIFY_PRESS_HOME 0xc4
#define SURFACE_BUTTON_NOTIFY_RELEASE_HOME 0xc5
#define SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_UP 0xc0
#define SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_UP 0xc1
#define SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_DOWN 0xc2
#define SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_DOWN 0xc3
MODULE_AUTHOR("Chen Yu");
MODULE_DESCRIPTION("Surface Pro3 Button Driver");
MODULE_LICENSE("GPL v2");
/*
* Power button, Home button, Volume buttons support is supposed to
* be covered by drivers/input/misc/soc_button_array.c, which is implemented
* according to "Windows ACPI Design Guide for SoC Platforms".
* However surface pro3 seems not to obey the specs, instead it uses
* device VGBI(MSHW0028) for dispatching the events.
* We choose acpi_driver rather than platform_driver/i2c_driver because
* although VGBI has an i2c resource connected to i2c controller, it
* is not embedded in any i2c controller's scope, thus neither platform_device
* will be created, nor i2c_client will be enumerated, we have to use
* acpi_driver.
*/
static const struct acpi_device_id surface_button_device_ids[] = {
{SURFACE_PRO3_BUTTON_HID, 0},
{SURFACE_PRO4_BUTTON_HID, 0},
{"", 0},
};
MODULE_DEVICE_TABLE(acpi, surface_button_device_ids);
struct surface_button {
unsigned int type;
struct input_dev *input;
char phys[32]; /* for input device */
unsigned long pushed;
bool suspended;
};
static void surface_button_notify(acpi_handle handle, u32 event, void *data)
{
struct device *dev = data;
struct surface_button *button = dev_get_drvdata(dev);
struct input_dev *input;
int key_code = KEY_RESERVED;
bool pressed = false;
switch (event) {
/* Power button press,release handle */
case SURFACE_BUTTON_NOTIFY_PRESS_POWER:
pressed = true;
fallthrough;
case SURFACE_BUTTON_NOTIFY_RELEASE_POWER:
key_code = KEY_POWER;
break;
/* Home button press,release handle */
case SURFACE_BUTTON_NOTIFY_PRESS_HOME:
pressed = true;
fallthrough;
case SURFACE_BUTTON_NOTIFY_RELEASE_HOME:
key_code = KEY_LEFTMETA;
break;
/* Volume up button press,release handle */
case SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_UP:
pressed = true;
fallthrough;
case SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_UP:
key_code = KEY_VOLUMEUP;
break;
/* Volume down button press,release handle */
case SURFACE_BUTTON_NOTIFY_PRESS_VOLUME_DOWN:
pressed = true;
fallthrough;
case SURFACE_BUTTON_NOTIFY_RELEASE_VOLUME_DOWN:
key_code = KEY_VOLUMEDOWN;
break;
case SURFACE_BUTTON_NOTIFY_TABLET_MODE:
dev_warn_once(dev, "Tablet mode is not supported\n");
break;
default:
dev_info_ratelimited(dev, "Unsupported event [0x%x]\n", event);
break;
}
input = button->input;
if (key_code == KEY_RESERVED)
return;
if (pressed)
pm_wakeup_dev_event(dev, 0, button->suspended);
if (button->suspended)
return;
input_report_key(input, key_code, pressed?1:0);
input_sync(input);
}
#ifdef CONFIG_PM_SLEEP
static int surface_button_suspend(struct device *dev)
{
struct surface_button *button = dev_get_drvdata(dev);
button->suspended = true;
return 0;
}
static int surface_button_resume(struct device *dev)
{
struct surface_button *button = dev_get_drvdata(dev);
button->suspended = false;
return 0;
}
#endif
/*
* Surface Pro 4 and Surface Book 2 / Surface Pro 2017 use the same device
* ID (MSHW0040) for the power/volume buttons. Make sure this is the right
* device by checking for the _DSM method and OEM Platform Revision.
*
* Returns true if the driver should bind to this device, i.e. the device is
* either MSWH0028 (Pro 3) or MSHW0040 on a Pro 4 or Book 1.
*/
static bool surface_button_check_MSHW0040(struct device *dev, acpi_handle handle)
{
union acpi_object *result;
u64 oem_platform_rev = 0; // valid revisions are nonzero
// get OEM platform revision
result = acpi_evaluate_dsm_typed(handle, &MSHW0040_DSM_UUID,
MSHW0040_DSM_REVISION,
MSHW0040_DSM_GET_OMPR,
NULL, ACPI_TYPE_INTEGER);
/*
* If evaluating the _DSM fails, the method is not present. This means
* that we have either MSHW0028 or MSHW0040 on Pro 4 or Book 1, so we
* should use this driver. We use revision 0 indicating it is
* unavailable.
*/
if (result) {
oem_platform_rev = result->integer.value;
ACPI_FREE(result);
}
dev_dbg(dev, "OEM Platform Revision %llu\n", oem_platform_rev);
return oem_platform_rev == 0;
}
static int surface_button_probe(struct platform_device *pdev)
{
struct surface_button *button;
struct acpi_device *device;
struct input_dev *input;
int error;
device = ACPI_COMPANION(&pdev->dev);
if (!device)
return -ENODEV;
if (strncmp(acpi_device_bid(device), SURFACE_BUTTON_OBJ_NAME,
strlen(SURFACE_BUTTON_OBJ_NAME)))
return -ENODEV;
if (!surface_button_check_MSHW0040(&pdev->dev, device->handle))
return -ENODEV;
button = kzalloc_obj(struct surface_button);
if (!button)
return -ENOMEM;
platform_set_drvdata(pdev, button);
button->input = input = input_allocate_device();
if (!input) {
error = -ENOMEM;
goto err_free_button;
}
strscpy(acpi_device_name(device), SURFACE_BUTTON_DEVICE_NAME);
snprintf(button->phys, sizeof(button->phys), "%s/buttons",
acpi_device_hid(device));
input->name = acpi_device_name(device);
input->phys = button->phys;
input->id.bustype = BUS_HOST;
input->dev.parent = &pdev->dev;
input_set_capability(input, EV_KEY, KEY_POWER);
input_set_capability(input, EV_KEY, KEY_LEFTMETA);
input_set_capability(input, EV_KEY, KEY_VOLUMEUP);
input_set_capability(input, EV_KEY, KEY_VOLUMEDOWN);
error = input_register_device(input);
if (error)
goto err_free_input;
device_init_wakeup(&pdev->dev, true);
error = acpi_dev_install_notify_handler(device, ACPI_DEVICE_NOTIFY,
surface_button_notify, &pdev->dev);
if (error) {
device_init_wakeup(&pdev->dev, false);
input_unregister_device(input);
goto err_free_button;
}
dev_info(&pdev->dev, "%s [%s]\n", acpi_device_name(device),
acpi_device_bid(device));
return 0;
err_free_input:
input_free_device(input);
err_free_button:
kfree(button);
return error;
}
static void surface_button_remove(struct platform_device *pdev)
{
struct surface_button *button = platform_get_drvdata(pdev);
acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev),
ACPI_DEVICE_NOTIFY, surface_button_notify);
device_init_wakeup(&pdev->dev, false);
input_unregister_device(button->input);
kfree(button);
}
static SIMPLE_DEV_PM_OPS(surface_button_pm,
surface_button_suspend, surface_button_resume);
static struct platform_driver surface_button_driver = {
.probe = surface_button_probe,
.remove = surface_button_remove,
.driver = {
.name = "surface_pro3_button",
.acpi_match_table = surface_button_device_ids,
.pm = &surface_button_pm,
},
};
module_platform_driver(surface_button_driver);