• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ssam_hub_update_workfn(struct work_struct * work)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 
ssam_hub_mark_hot_removed(struct device * dev,void * _data)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 
ssam_hub_update(struct ssam_hub * hub,bool connected)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 
ssam_hub_resume(struct device * dev)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 
ssam_hub_probe(struct ssam_device * sdev)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 
ssam_hub_remove(struct ssam_device * sdev)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 
ssam_base_hub_query_state(struct ssam_hub * hub,enum ssam_hub_state * state)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 
ssam_base_hub_notif(struct ssam_event_notifier * nf,const struct ssam_event * event)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 
ssam_kip_hub_query_state(struct ssam_hub * hub,enum ssam_hub_state * state)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 
ssam_kip_hub_notif(struct ssam_event_notifier * nf,const struct ssam_event * event)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");
372