1// SPDX-License-Identifier: GPL-2.0+
2/*
3 * Driver for Surface System Aggregator Module (SSAM) subsystem device hubs.
4 *
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
7 * (hot-)removal.
8 *
9 * Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
10 */
11
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>
17
18#include <linux/surface_aggregator/device.h>
19
20
21/* -- SSAM generic subsystem hub driver framework. -------------------------- */
22
23enum ssam_hub_state {
24 SSAM_HUB_UNINITIALIZED, /* Only set during initialization. */
25 SSAM_HUB_CONNECTED,
26 SSAM_HUB_DISCONNECTED,
27};
28
29enum ssam_hub_flags {
30 SSAM_HUB_HOT_REMOVED,
31};
32
33struct ssam_hub;
34
35struct ssam_hub_ops {
36 int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);
37};
38
39struct ssam_hub {
40 struct ssam_device *sdev;
41
42 enum ssam_hub_state state;
43 unsigned long flags;
44
45 struct delayed_work update_work;
46 unsigned long connect_delay;
47
48 struct ssam_event_notifier notif;
49 struct ssam_hub_ops ops;
50};
51
52struct ssam_hub_desc {
53 struct {
54 struct ssam_event_registry reg;
55 struct ssam_event_id id;
56 enum ssam_event_mask mask;
57 } event;
58
59 struct {
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);
62 } ops;
63
64 unsigned long connect_delay_ms;
65};
66
67static void ssam_hub_update_workfn(struct work_struct *work)
68{
69 struct ssam_hub *hub = container_of(work, struct ssam_hub, update_work.work);
70 enum ssam_hub_state state;
71 int status = 0;
72
73 status = hub->ops.get_state(hub, &state);
74 if (status)
75 return;
76
77 /*
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.
84 *
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.
91 *
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.
96 */
97 if (test_and_clear_bit(nr: SSAM_HUB_HOT_REMOVED, addr: &hub->flags)) {
98 if (state == SSAM_HUB_CONNECTED)
99 schedule_delayed_work(dwork: &hub->update_work, delay: hub->connect_delay);
100
101 state = SSAM_HUB_DISCONNECTED;
102 }
103
104 if (hub->state == state)
105 return;
106 hub->state = state;
107
108 if (hub->state == SSAM_HUB_CONNECTED)
109 status = ssam_device_register_clients(sdev: hub->sdev);
110 else
111 ssam_remove_clients(dev: &hub->sdev->dev);
112
113 if (status)
114 dev_err(&hub->sdev->dev, "failed to update hub child devices: %d\n", status);
115}
116
117static int ssam_hub_mark_hot_removed(struct device *dev, void *_data)
118{
119 struct ssam_device *sdev = to_ssam_device(dev);
120
121 if (is_ssam_device(d: dev))
122 ssam_device_mark_hot_removed(sdev);
123
124 return 0;
125}
126
127static void ssam_hub_update(struct ssam_hub *hub, bool connected)
128{
129 unsigned long delay;
130
131 /* Mark devices as hot-removed before we remove any. */
132 if (!connected) {
133 set_bit(nr: SSAM_HUB_HOT_REMOVED, addr: &hub->flags);
134 device_for_each_child_reverse(dev: &hub->sdev->dev, NULL, fn: ssam_hub_mark_hot_removed);
135 }
136
137 /*
138 * Delay update when the base/keyboard cover is being connected to give
139 * devices/EC some time to set up.
140 */
141 delay = connected ? hub->connect_delay : 0;
142
143 schedule_delayed_work(dwork: &hub->update_work, delay);
144}
145
146static int __maybe_unused ssam_hub_resume(struct device *dev)
147{
148 struct ssam_hub *hub = dev_get_drvdata(dev);
149
150 schedule_delayed_work(dwork: &hub->update_work, delay: 0);
151 return 0;
152}
153static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops, NULL, ssam_hub_resume);
154
155static int ssam_hub_probe(struct ssam_device *sdev)
156{
157 const struct ssam_hub_desc *desc;
158 struct ssam_hub *hub;
159 int status;
160
161 desc = ssam_device_get_match_data(dev: sdev);
162 if (!desc) {
163 WARN(1, "no driver match data specified");
164 return -EINVAL;
165 }
166
167 hub = devm_kzalloc(dev: &sdev->dev, size: sizeof(*hub), GFP_KERNEL);
168 if (!hub)
169 return -ENOMEM;
170
171 hub->sdev = sdev;
172 hub->state = SSAM_HUB_UNINITIALIZED;
173
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;
180
181 hub->connect_delay = msecs_to_jiffies(m: desc->connect_delay_ms);
182 hub->ops.get_state = desc->ops.get_state;
183
184 INIT_DELAYED_WORK(&hub->update_work, ssam_hub_update_workfn);
185
186 ssam_device_set_drvdata(sdev, data: hub);
187
188 status = ssam_device_notifier_register(sdev, n: &hub->notif);
189 if (status)
190 return status;
191
192 schedule_delayed_work(dwork: &hub->update_work, delay: 0);
193 return 0;
194}
195
196static void ssam_hub_remove(struct ssam_device *sdev)
197{
198 struct ssam_hub *hub = ssam_device_get_drvdata(sdev);
199
200 ssam_device_notifier_unregister(sdev, n: &hub->notif);
201 cancel_delayed_work_sync(dwork: &hub->update_work);
202 ssam_remove_clients(dev: &sdev->dev);
203}
204
205
206/* -- SSAM base-subsystem hub driver. --------------------------------------- */
207
208/*
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
211 * experimentation.
212 */
213#define SSAM_BASE_UPDATE_CONNECT_DELAY 2500
214
215SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, {
216 .target_category = SSAM_SSH_TC_BAS,
217 .target_id = SSAM_SSH_TID_SAM,
218 .command_id = 0x0d,
219 .instance_id = 0x00,
220});
221
222#define SSAM_BAS_OPMODE_TABLET 0x00
223#define SSAM_EVENT_BAS_CID_CONNECTION 0x0c
224
225static int ssam_base_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)
226{
227 u8 opmode;
228 int status;
229
230 status = ssam_retry(ssam_bas_query_opmode, hub->sdev->ctrl, &opmode);
231 if (status < 0) {
232 dev_err(&hub->sdev->dev, "failed to query base state: %d\n", status);
233 return status;
234 }
235
236 if (opmode != SSAM_BAS_OPMODE_TABLET)
237 *state = SSAM_HUB_CONNECTED;
238 else
239 *state = SSAM_HUB_DISCONNECTED;
240
241 return 0;
242}
243
244static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
245{
246 struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
247
248 if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION)
249 return 0;
250
251 if (event->length < 1) {
252 dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
253 return 0;
254 }
255
256 ssam_hub_update(hub, connected: event->data[0]);
257
258 /*
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)
261 * silent observer.
262 */
263 return 0;
264}
265
266static const struct ssam_hub_desc base_hub = {
267 .event = {
268 .reg = SSAM_EVENT_REGISTRY_SAM,
269 .id = {
270 .target_category = SSAM_SSH_TC_BAS,
271 .instance = 0,
272 },
273 .mask = SSAM_EVENT_MASK_NONE,
274 },
275 .ops = {
276 .notify = ssam_base_hub_notif,
277 .get_state = ssam_base_hub_query_state,
278 },
279 .connect_delay_ms = SSAM_BASE_UPDATE_CONNECT_DELAY,
280};
281
282
283/* -- SSAM KIP-subsystem hub driver. ---------------------------------------- */
284
285/*
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.
288 */
289#define SSAM_KIP_UPDATE_CONNECT_DELAY 250
290
291#define SSAM_EVENT_KIP_CID_CONNECTION 0x2c
292
293SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state, u8, {
294 .target_category = SSAM_SSH_TC_KIP,
295 .target_id = SSAM_SSH_TID_SAM,
296 .command_id = 0x2c,
297 .instance_id = 0x00,
298});
299
300static int ssam_kip_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)
301{
302 int status;
303 u8 connected;
304
305 status = ssam_retry(__ssam_kip_query_state, hub->sdev->ctrl, &connected);
306 if (status < 0) {
307 dev_err(&hub->sdev->dev, "failed to query KIP connection state: %d\n", status);
308 return status;
309 }
310
311 *state = connected ? SSAM_HUB_CONNECTED : SSAM_HUB_DISCONNECTED;
312 return 0;
313}
314
315static u32 ssam_kip_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
316{
317 struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
318
319 if (event->command_id != SSAM_EVENT_KIP_CID_CONNECTION)
320 return 0; /* Return "unhandled". */
321
322 if (event->length < 1) {
323 dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
324 return 0;
325 }
326
327 ssam_hub_update(hub, connected: event->data[0]);
328 return SSAM_NOTIF_HANDLED;
329}
330
331static const struct ssam_hub_desc kip_hub = {
332 .event = {
333 .reg = SSAM_EVENT_REGISTRY_SAM,
334 .id = {
335 .target_category = SSAM_SSH_TC_KIP,
336 .instance = 0,
337 },
338 .mask = SSAM_EVENT_MASK_TARGET,
339 },
340 .ops = {
341 .notify = ssam_kip_hub_notif,
342 .get_state = ssam_kip_hub_query_state,
343 },
344 .connect_delay_ms = SSAM_KIP_UPDATE_CONNECT_DELAY,
345};
346
347
348/* -- Driver registration. -------------------------------------------------- */
349
350static 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 },
353 { }
354};
355MODULE_DEVICE_TABLE(ssam, ssam_hub_match);
356
357static struct ssam_device_driver ssam_subsystem_hub_driver = {
358 .probe = ssam_hub_probe,
359 .remove = ssam_hub_remove,
360 .match_table = ssam_hub_match,
361 .driver = {
362 .name = "surface_aggregator_subsystem_hub",
363 .probe_type = PROBE_PREFER_ASYNCHRONOUS,
364 .pm = &ssam_hub_pm_ops,
365 },
366};
367module_ssam_device_driver(ssam_subsystem_hub_driver);
368
369MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
370MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module");
371MODULE_LICENSE("GPL");
372

source code of linux/drivers/platform/surface/surface_aggregator_hub.c