// 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 = 0 x0d,
.instance_id = 0 x00,
});
#define SSAM_BAS_OPMODE_TABLET 0 x00
#define SSAM_EVENT_BAS_CID_CONNECTION 0 x0c
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 0 x2c
SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state, u8, {
.target_category = SSAM_SSH_TC_KIP,
.target_id = SSAM_SSH_TID_SAM,
.command_id = 0 x2c,
.instance_id = 0 x00,
});
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, 0 x00), (unsigned long )&kip_hub },
{ SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_BAS, 0 x00), (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" );
Messung V0.5 in Prozent C=97 H=100 G=98
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-09-28)
¤
*© Formatika GbR, Deutschland