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 | |
23 | enum ssam_hub_state { |
24 | SSAM_HUB_UNINITIALIZED, /* Only set during initialization. */ |
25 | SSAM_HUB_CONNECTED, |
26 | SSAM_HUB_DISCONNECTED, |
27 | }; |
28 | |
29 | enum ssam_hub_flags { |
30 | SSAM_HUB_HOT_REMOVED, |
31 | }; |
32 | |
33 | struct ssam_hub; |
34 | |
35 | struct ssam_hub_ops { |
36 | int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state); |
37 | }; |
38 | |
39 | struct 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 | |
52 | struct 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 | |
67 | static 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 | |
117 | static 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 | |
127 | static 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 | |
146 | static 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 | } |
153 | static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops, NULL, ssam_hub_resume); |
154 | |
155 | static 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 | |
196 | static 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 | |
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, |
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 | |
225 | static 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 | |
244 | static 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 | |
266 | static 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 | |
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, |
296 | .command_id = 0x2c, |
297 | .instance_id = 0x00, |
298 | }); |
299 | |
300 | static 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 | |
315 | static 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 | |
331 | static 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 | |
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 }, |
353 | { } |
354 | }; |
355 | MODULE_DEVICE_TABLE(ssam, ssam_hub_match); |
356 | |
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, |
361 | .driver = { |
362 | .name = "surface_aggregator_subsystem_hub" , |
363 | .probe_type = PROBE_PREFER_ASYNCHRONOUS, |
364 | .pm = &ssam_hub_pm_ops, |
365 | }, |
366 | }; |
367 | module_ssam_device_driver(ssam_subsystem_hub_driver); |
368 | |
369 | MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>" ); |
370 | MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module" ); |
371 | MODULE_LICENSE("GPL" ); |
372 | |