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"); 372