Linux Audio

Check our new training course

Loading...
Note: File does not exist in v3.1.
  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(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
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(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(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
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(&hub->update_work, 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(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
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, &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
215SSAM_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
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, 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       = 0x01,
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, 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, 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};
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");