Merge tag 'scsi-fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/jejb/scsi
[platform/kernel/linux-starfive.git] / drivers / platform / surface / surface_aggregator_hub.c
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(SSAM_HUB_HOT_REMOVED, &hub->flags)) {
98                 if (state == SSAM_HUB_CONNECTED)
99                         schedule_delayed_work(&hub->update_work, 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(hub->sdev);
110         else
111                 ssam_remove_clients(&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(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(SSAM_HUB_HOT_REMOVED, &hub->flags);
134                 device_for_each_child_reverse(&hub->sdev->dev, NULL, 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(&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(&hub->update_work, 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(sdev);
162         if (!desc) {
163                 WARN(1, "no driver match data specified");
164                 return -EINVAL;
165         }
166
167         hub = devm_kzalloc(&sdev->dev, 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(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, hub);
187
188         status = ssam_device_notifier_register(sdev, &hub->notif);
189         if (status)
190                 return status;
191
192         schedule_delayed_work(&hub->update_work, 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, &hub->notif);
201         cancel_delayed_work_sync(&hub->update_work);
202         ssam_remove_clients(&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       = 0x01,
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, 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       = 0x01,
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, 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, 0x01, SSAM_SSH_TC_KIP, 0x00), (unsigned long)&kip_hub  },
352         { SSAM_VDEV(HUB, 0x02, 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");