372 lines · 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 performs6 * instantiation of the devices managed by said hubs and takes care of7 * (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 and79 * re-added before we were able to remove them here. In that case, both80 * the state returned by get_state() and the state of the hub will81 * equal SSAM_HUB_CONNECTED and we would bail early below, which would82 * leave child devices without proper (re-)initialization and the83 * hot-remove flag set.84 *85 * Therefore, we check whether devices have been hot-removed via an86 * additional flag on the hub and, in this case, override the returned87 * hub state. In case of a missed disconnect (i.e. get_state returned88 * "connected"), we further need to re-schedule this work (with the89 * appropriate delay) as the actual connect work submission might have90 * been merged with this one.91 *92 * This then leads to one of two cases: Either we submit an unnecessary93 * work item (which will get ignored via either the queue or the state94 * 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 else111 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 give139 * 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 usable210 * after being (re-)connected. This delay has been determined via211 * experimentation.212 */213#define SSAM_BASE_UPDATE_CONNECT_DELAY 2500214 215SSAM_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 0x00223#define SSAM_EVENT_BAS_CID_CONNECTION 0x0c224 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 else239 *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 and260 * 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 being287 * (re-)connected. This delay has been determined via experimentation.288 */289#define SSAM_KIP_UPDATE_CONNECT_DELAY 250290 291#define SSAM_EVENT_KIP_CID_CONNECTION 0x2c292 293SSAM_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 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, 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};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");372