1 // SPDX-License-Identifier: GPL-2.0+
3 * Driver for Surface System Aggregator Module (SSAM) subsystem device hubs.
5 * Provides a driver for SSAM subsystems device hubs. This driver performs
6 * instantiation of the devices managed by said hubs and takes care of
9 * Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
12 #include <linux/kernel.h>
13 #include <linux/limits.h>
14 #include <linux/module.h>
15 #include <linux/types.h>
16 #include <linux/workqueue.h>
18 #include <linux/surface_aggregator/device.h>
21 /* -- SSAM generic subsystem hub driver framework. -------------------------- */
24 SSAM_HUB_UNINITIALIZED
, /* Only set during initialization. */
26 SSAM_HUB_DISCONNECTED
,
36 int (*get_state
)(struct ssam_hub
*hub
, enum ssam_hub_state
*state
);
40 struct ssam_device
*sdev
;
42 enum ssam_hub_state state
;
45 struct delayed_work update_work
;
46 unsigned long connect_delay
;
48 struct ssam_event_notifier notif
;
49 struct ssam_hub_ops ops
;
52 struct ssam_hub_desc
{
54 struct ssam_event_registry reg
;
55 struct ssam_event_id id
;
56 enum ssam_event_mask mask
;
60 u32 (*notify
)(struct ssam_event_notifier
*nf
, const struct ssam_event
*event
);
61 int (*get_state
)(struct ssam_hub
*hub
, enum ssam_hub_state
*state
);
64 unsigned long connect_delay_ms
;
67 static void ssam_hub_update_workfn(struct work_struct
*work
)
69 struct ssam_hub
*hub
= container_of(work
, struct ssam_hub
, update_work
.work
);
70 enum ssam_hub_state state
;
73 status
= hub
->ops
.get_state(hub
, &state
);
78 * There is a small possibility that hub devices were hot-removed and
79 * re-added before we were able to remove them here. In that case, both
80 * the state returned by get_state() and the state of the hub will
81 * equal SSAM_HUB_CONNECTED and we would bail early below, which would
82 * leave child devices without proper (re-)initialization and the
83 * hot-remove flag set.
85 * Therefore, we check whether devices have been hot-removed via an
86 * additional flag on the hub and, in this case, override the returned
87 * hub state. In case of a missed disconnect (i.e. get_state returned
88 * "connected"), we further need to re-schedule this work (with the
89 * appropriate delay) as the actual connect work submission might have
90 * been merged with this one.
92 * This then leads to one of two cases: Either we submit an unnecessary
93 * work item (which will get ignored via either the queue or the state
94 * checks) or, in the unlikely case that the work is actually required,
95 * double the normal connect delay.
97 if (test_and_clear_bit(SSAM_HUB_HOT_REMOVED
, &hub
->flags
)) {
98 if (state
== SSAM_HUB_CONNECTED
)
99 schedule_delayed_work(&hub
->update_work
, hub
->connect_delay
);
101 state
= SSAM_HUB_DISCONNECTED
;
104 if (hub
->state
== state
)
108 if (hub
->state
== SSAM_HUB_CONNECTED
)
109 status
= ssam_device_register_clients(hub
->sdev
);
111 ssam_remove_clients(&hub
->sdev
->dev
);
114 dev_err(&hub
->sdev
->dev
, "failed to update hub child devices: %d\n", status
);
117 static int ssam_hub_mark_hot_removed(struct device
*dev
, void *_data
)
119 struct ssam_device
*sdev
= to_ssam_device(dev
);
121 if (is_ssam_device(dev
))
122 ssam_device_mark_hot_removed(sdev
);
127 static void ssam_hub_update(struct ssam_hub
*hub
, bool connected
)
131 /* Mark devices as hot-removed before we remove any. */
133 set_bit(SSAM_HUB_HOT_REMOVED
, &hub
->flags
);
134 device_for_each_child_reverse(&hub
->sdev
->dev
, NULL
, ssam_hub_mark_hot_removed
);
138 * Delay update when the base/keyboard cover is being connected to give
139 * devices/EC some time to set up.
141 delay
= connected
? hub
->connect_delay
: 0;
143 schedule_delayed_work(&hub
->update_work
, delay
);
146 static int __maybe_unused
ssam_hub_resume(struct device
*dev
)
148 struct ssam_hub
*hub
= dev_get_drvdata(dev
);
150 schedule_delayed_work(&hub
->update_work
, 0);
153 static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops
, NULL
, ssam_hub_resume
);
155 static int ssam_hub_probe(struct ssam_device
*sdev
)
157 const struct ssam_hub_desc
*desc
;
158 struct ssam_hub
*hub
;
161 desc
= ssam_device_get_match_data(sdev
);
163 WARN(1, "no driver match data specified");
167 hub
= devm_kzalloc(&sdev
->dev
, sizeof(*hub
), GFP_KERNEL
);
172 hub
->state
= SSAM_HUB_UNINITIALIZED
;
174 hub
->notif
.base
.priority
= INT_MAX
; /* This notifier should run first. */
175 hub
->notif
.base
.fn
= desc
->ops
.notify
;
176 hub
->notif
.event
.reg
= desc
->event
.reg
;
177 hub
->notif
.event
.id
= desc
->event
.id
;
178 hub
->notif
.event
.mask
= desc
->event
.mask
;
179 hub
->notif
.event
.flags
= SSAM_EVENT_SEQUENCED
;
181 hub
->connect_delay
= msecs_to_jiffies(desc
->connect_delay_ms
);
182 hub
->ops
.get_state
= desc
->ops
.get_state
;
184 INIT_DELAYED_WORK(&hub
->update_work
, ssam_hub_update_workfn
);
186 ssam_device_set_drvdata(sdev
, hub
);
188 status
= ssam_device_notifier_register(sdev
, &hub
->notif
);
192 schedule_delayed_work(&hub
->update_work
, 0);
196 static void ssam_hub_remove(struct ssam_device
*sdev
)
198 struct ssam_hub
*hub
= ssam_device_get_drvdata(sdev
);
200 ssam_device_notifier_unregister(sdev
, &hub
->notif
);
201 cancel_delayed_work_sync(&hub
->update_work
);
202 ssam_remove_clients(&sdev
->dev
);
206 /* -- SSAM base-subsystem hub driver. --------------------------------------- */
209 * Some devices (especially battery) may need a bit of time to be fully usable
210 * after being (re-)connected. This delay has been determined via
213 #define SSAM_BASE_UPDATE_CONNECT_DELAY 2500
215 SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode
, u8
, {
216 .target_category
= SSAM_SSH_TC_BAS
,
217 .target_id
= SSAM_SSH_TID_SAM
,
222 #define SSAM_BAS_OPMODE_TABLET 0x00
223 #define SSAM_EVENT_BAS_CID_CONNECTION 0x0c
225 static int ssam_base_hub_query_state(struct ssam_hub
*hub
, enum ssam_hub_state
*state
)
230 status
= ssam_retry(ssam_bas_query_opmode
, hub
->sdev
->ctrl
, &opmode
);
232 dev_err(&hub
->sdev
->dev
, "failed to query base state: %d\n", status
);
236 if (opmode
!= SSAM_BAS_OPMODE_TABLET
)
237 *state
= SSAM_HUB_CONNECTED
;
239 *state
= SSAM_HUB_DISCONNECTED
;
244 static u32
ssam_base_hub_notif(struct ssam_event_notifier
*nf
, const struct ssam_event
*event
)
246 struct ssam_hub
*hub
= container_of(nf
, struct ssam_hub
, notif
);
248 if (event
->command_id
!= SSAM_EVENT_BAS_CID_CONNECTION
)
251 if (event
->length
< 1) {
252 dev_err(&hub
->sdev
->dev
, "unexpected payload size: %u\n", event
->length
);
256 ssam_hub_update(hub
, event
->data
[0]);
259 * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and
260 * consumed by the detachment system driver. We're just a (more or less)
266 static const struct ssam_hub_desc base_hub
= {
268 .reg
= SSAM_EVENT_REGISTRY_SAM
,
270 .target_category
= SSAM_SSH_TC_BAS
,
273 .mask
= SSAM_EVENT_MASK_NONE
,
276 .notify
= ssam_base_hub_notif
,
277 .get_state
= ssam_base_hub_query_state
,
279 .connect_delay_ms
= SSAM_BASE_UPDATE_CONNECT_DELAY
,
283 /* -- SSAM KIP-subsystem hub driver. ---------------------------------------- */
286 * Some devices may need a bit of time to be fully usable after being
287 * (re-)connected. This delay has been determined via experimentation.
289 #define SSAM_KIP_UPDATE_CONNECT_DELAY 250
291 #define SSAM_EVENT_KIP_CID_CONNECTION 0x2c
293 SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state
, u8
, {
294 .target_category
= SSAM_SSH_TC_KIP
,
295 .target_id
= SSAM_SSH_TID_SAM
,
300 static int ssam_kip_hub_query_state(struct ssam_hub
*hub
, enum ssam_hub_state
*state
)
305 status
= ssam_retry(__ssam_kip_query_state
, hub
->sdev
->ctrl
, &connected
);
307 dev_err(&hub
->sdev
->dev
, "failed to query KIP connection state: %d\n", status
);
311 *state
= connected
? SSAM_HUB_CONNECTED
: SSAM_HUB_DISCONNECTED
;
315 static u32
ssam_kip_hub_notif(struct ssam_event_notifier
*nf
, const struct ssam_event
*event
)
317 struct ssam_hub
*hub
= container_of(nf
, struct ssam_hub
, notif
);
319 if (event
->command_id
!= SSAM_EVENT_KIP_CID_CONNECTION
)
320 return 0; /* Return "unhandled". */
322 if (event
->length
< 1) {
323 dev_err(&hub
->sdev
->dev
, "unexpected payload size: %u\n", event
->length
);
327 ssam_hub_update(hub
, event
->data
[0]);
328 return SSAM_NOTIF_HANDLED
;
331 static const struct ssam_hub_desc kip_hub
= {
333 .reg
= SSAM_EVENT_REGISTRY_SAM
,
335 .target_category
= SSAM_SSH_TC_KIP
,
338 .mask
= SSAM_EVENT_MASK_TARGET
,
341 .notify
= ssam_kip_hub_notif
,
342 .get_state
= ssam_kip_hub_query_state
,
344 .connect_delay_ms
= SSAM_KIP_UPDATE_CONNECT_DELAY
,
348 /* -- Driver registration. -------------------------------------------------- */
350 static const struct ssam_device_id ssam_hub_match
[] = {
351 { SSAM_VDEV(HUB
, SAM
, SSAM_SSH_TC_KIP
, 0x00), (unsigned long)&kip_hub
},
352 { SSAM_VDEV(HUB
, SAM
, SSAM_SSH_TC_BAS
, 0x00), (unsigned long)&base_hub
},
355 MODULE_DEVICE_TABLE(ssam
, ssam_hub_match
);
357 static struct ssam_device_driver ssam_subsystem_hub_driver
= {
358 .probe
= ssam_hub_probe
,
359 .remove
= ssam_hub_remove
,
360 .match_table
= ssam_hub_match
,
362 .name
= "surface_aggregator_subsystem_hub",
363 .probe_type
= PROBE_PREFER_ASYNCHRONOUS
,
364 .pm
= &ssam_hub_pm_ops
,
367 module_ssam_device_driver(ssam_subsystem_hub_driver
);
369 MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
370 MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module");
371 MODULE_LICENSE("GPL");