diff options
-rw-r--r-- | MAINTAINERS | 8 | ||||
-rw-r--r-- | drivers/platform/surface/Kconfig | 2 | ||||
-rw-r--r-- | drivers/platform/surface/Makefile | 1 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/Kconfig | 42 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/Makefile | 10 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/controller.c | 2504 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/controller.h | 276 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/core.c | 787 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/ssh_msgb.h | 205 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/ssh_packet_layer.c | 1710 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/ssh_packet_layer.h | 187 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/ssh_parser.c | 228 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/ssh_parser.h | 154 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/ssh_request_layer.c | 1211 | ||||
-rw-r--r-- | drivers/platform/surface/aggregator/ssh_request_layer.h | 143 | ||||
-rw-r--r-- | include/linux/surface_aggregator/controller.h | 824 | ||||
-rw-r--r-- | include/linux/surface_aggregator/serial_hub.h | 672 |
17 files changed, 8964 insertions, 0 deletions
diff --git a/MAINTAINERS b/MAINTAINERS index 546aa66428c9..845a245f562c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11813,6 +11813,14 @@ L: platform-driver-x86@vger.kernel.org S: Supported F: drivers/platform/surface/surfacepro3_button.c +MICROSOFT SURFACE SYSTEM AGGREGATOR SUBSYSTEM +M: Maximilian Luz <luzmaximilian@gmail.com> +S: Maintained +W: https://github.com/linux-surface/surface-aggregator-module +C: irc://chat.freenode.net/##linux-surface +F: drivers/platform/surface/aggregator/ +F: include/linux/surface_aggregator/ + MICROTEK X6 SCANNER M: Oliver Neukum <oliver@neukum.org> S: Maintained diff --git a/drivers/platform/surface/Kconfig b/drivers/platform/surface/Kconfig index 2c941cdac9ee..2916a47b130e 100644 --- a/drivers/platform/surface/Kconfig +++ b/drivers/platform/surface/Kconfig @@ -56,4 +56,6 @@ config SURFACE_PRO3_BUTTON help This driver handles the power/home/volume buttons on the Microsoft Surface Pro 3/4 tablet. +source "drivers/platform/surface/aggregator/Kconfig" + endif # SURFACE_PLATFORMS diff --git a/drivers/platform/surface/Makefile b/drivers/platform/surface/Makefile index cedfb027ded1..034134fe0264 100644 --- a/drivers/platform/surface/Makefile +++ b/drivers/platform/surface/Makefile @@ -7,5 +7,6 @@ obj-$(CONFIG_SURFACE3_WMI) += surface3-wmi.o obj-$(CONFIG_SURFACE_3_BUTTON) += surface3_button.o obj-$(CONFIG_SURFACE_3_POWER_OPREGION) += surface3_power.o +obj-$(CONFIG_SURFACE_AGGREGATOR) += aggregator/ obj-$(CONFIG_SURFACE_GPE) += surface_gpe.o obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o diff --git a/drivers/platform/surface/aggregator/Kconfig b/drivers/platform/surface/aggregator/Kconfig new file mode 100644 index 000000000000..e9f4ad96e40a --- /dev/null +++ b/drivers/platform/surface/aggregator/Kconfig @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (C) 2019-2020 Maximilian Luz <luzmaximilian@gmail.com> + +menuconfig SURFACE_AGGREGATOR + tristate "Microsoft Surface System Aggregator Module Subsystem and Drivers" + depends on SERIAL_DEV_BUS + select CRC_CCITT + help + The Surface System Aggregator Module (Surface SAM or SSAM) is an + embedded controller (EC) found on 5th- and later-generation Microsoft + Surface devices (i.e. Surface Pro 5, Surface Book 2, Surface Laptop, + and newer, with exception of Surface Go series devices). + + Depending on the device in question, this EC provides varying + functionality, including: + - EC access from ACPI via Surface ACPI Notify (5th- and 6th-generation) + - battery status information (all devices) + - thermal sensor access (all devices) + - performance mode / cooling mode control (all devices) + - clipboard detachment system control (Surface Book 2 and 3) + - HID / keyboard input (Surface Laptops, Surface Book 3) + + This option controls whether the Surface SAM subsystem core will be + built. This includes a driver for the Surface Serial Hub (SSH), which + is the device responsible for the communication with the EC, and a + basic kernel interface exposing the EC functionality to other client + drivers, i.e. allowing them to make requests to the EC and receive + events from it. Selecting this option alone will not provide any + client drivers and therefore no functionality beyond the in-kernel + interface. Said functionality is the responsibility of the respective + client drivers. + + Note: While 4th-generation Surface devices also make use of a SAM EC, + due to a difference in the communication interface of the controller, + only 5th and later generations are currently supported. Specifically, + devices using SAM-over-SSH are supported, whereas devices using + SAM-over-HID, which is used on the 4th generation, are currently not + supported. + + Choose m if you want to build the SAM subsystem core and SSH driver as + module, y if you want to build it into the kernel and n if you don't + want it at all. diff --git a/drivers/platform/surface/aggregator/Makefile b/drivers/platform/surface/aggregator/Makefile new file mode 100644 index 000000000000..faad18d4a7f2 --- /dev/null +++ b/drivers/platform/surface/aggregator/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (C) 2019-2020 Maximilian Luz <luzmaximilian@gmail.com> + +obj-$(CONFIG_SURFACE_AGGREGATOR) += surface_aggregator.o + +surface_aggregator-objs := core.o +surface_aggregator-objs += ssh_parser.o +surface_aggregator-objs += ssh_packet_layer.o +surface_aggregator-objs += ssh_request_layer.o +surface_aggregator-objs += controller.o diff --git a/drivers/platform/surface/aggregator/controller.c b/drivers/platform/surface/aggregator/controller.c new file mode 100644 index 000000000000..488318cf2098 --- /dev/null +++ b/drivers/platform/surface/aggregator/controller.c @@ -0,0 +1,2504 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Main SSAM/SSH controller structure and functionality. + * + * Copyright (C) 2019-2020 Maximilian Luz <luzmaximilian@gmail.com> + */ + +#include <linux/acpi.h> +#include <linux/atomic.h> +#include <linux/completion.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/kref.h> +#include <linux/limits.h> +#include <linux/list.h> +#include <linux/lockdep.h> +#include <linux/mutex.h> +#include <linux/rculist.h> +#include <linux/rbtree.h> +#include <linux/rwsem.h> +#include <linux/serdev.h> +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <linux/srcu.h> +#include <linux/types.h> +#include <linux/workqueue.h> + +#include <linux/surface_aggregator/controller.h> +#include <linux/surface_aggregator/serial_hub.h> + +#include "controller.h" +#include "ssh_msgb.h" +#include "ssh_request_layer.h" + + +/* -- Safe counters. -------------------------------------------------------- */ + +/** + * ssh_seq_reset() - Reset/initialize sequence ID counter. + * @c: The counter to reset. + */ +static void ssh_seq_reset(struct ssh_seq_counter *c) +{ + WRITE_ONCE(c->value, 0); +} + +/** + * ssh_seq_next() - Get next sequence ID. + * @c: The counter providing the sequence IDs. + * + * Return: Returns the next sequence ID of the counter. + */ +static u8 ssh_seq_next(struct ssh_seq_counter *c) +{ + u8 old = READ_ONCE(c->value); + u8 new = old + 1; + u8 ret; + + while (unlikely((ret = cmpxchg(&c->value, old, new)) != old)) { + old = ret; + new = old + 1; + } + + return old; +} + +/** + * ssh_rqid_reset() - Reset/initialize request ID counter. + * @c: The counter to reset. + */ +static void ssh_rqid_reset(struct ssh_rqid_counter *c) +{ + WRITE_ONCE(c->value, 0); +} + +/** + * ssh_rqid_next() - Get next request ID. + * @c: The counter providing the request IDs. + * + * Return: Returns the next request ID of the counter, skipping any reserved + * request IDs. + */ +static u16 ssh_rqid_next(struct ssh_rqid_counter *c) +{ + u16 old = READ_ONCE(c->value); + u16 new = ssh_rqid_next_valid(old); + u16 ret; + + while (unlikely((ret = cmpxchg(&c->value, old, new)) != old)) { + old = ret; + new = ssh_rqid_next_valid(old); + } + + return old; +} + + +/* -- Event notifier/callbacks. --------------------------------------------- */ +/* + * The notifier system is based on linux/notifier.h, specifically the SRCU + * implementation. The difference to that is, that some bits of the notifier + * call return value can be tracked across multiple calls. This is done so + * that handling of events can be tracked and a warning can be issued in case + * an event goes unhandled. The idea of that warning is that it should help + * discover and identify new/currently unimplemented features. + */ + +/** + * ssam_event_matches_notifier() - Test if an event matches a notifier. + * @n: The event notifier to test against. + * @event: The event to test. + * + * Return: Returns %true if the given event matches the given notifier + * according to the rules set in the notifier's event mask, %false otherwise. + */ +static bool ssam_event_matches_notifier(const struct ssam_event_notifier *n, + const struct ssam_event *event) +{ + bool match = n->event.id.target_category == event->target_category; + + if (n->event.mask & SSAM_EVENT_MASK_TARGET) + match &= n->event.reg.target_id == event->target_id; + + if (n->event.mask & SSAM_EVENT_MASK_INSTANCE) + match &= n->event.id.instance == event->instance_id; + + return match; +} + +/** + * ssam_nfblk_call_chain() - Call event notifier callbacks of the given chain. + * @nh: The notifier head for which the notifier callbacks should be called. + * @event: The event data provided to the callbacks. + * + * Call all registered notifier callbacks in order of their priority until + * either no notifier is left or a notifier returns a value with the + * %SSAM_NOTIF_STOP bit set. Note that this bit is automatically set via + * ssam_notifier_from_errno() on any non-zero error value. + * + * Return: Returns the notifier status value, which contains the notifier + * status bits (%SSAM_NOTIF_HANDLED and %SSAM_NOTIF_STOP) as well as a + * potential error value returned from the last executed notifier callback. + * Use ssam_notifier_to_errno() to convert this value to the original error + * value. + */ +static int ssam_nfblk_call_chain(struct ssam_nf_head *nh, struct ssam_event *event) +{ + struct ssam_event_notifier *nf; + int ret = 0, idx; + + idx = srcu_read_lock(&nh->srcu); + + list_for_each_entry_rcu(nf, &nh->head, base.node, + srcu_read_lock_held(&nh->srcu)) { + if (ssam_event_matches_notifier(nf, event)) { + ret = (ret & SSAM_NOTIF_STATE_MASK) | nf->base.fn(nf, event); + if (ret & SSAM_NOTIF_STOP) + break; + } + } + + srcu_read_unlock(&nh->srcu, idx); + return ret; +} + +/** + * ssam_nfblk_insert() - Insert a new notifier block into the given notifier + * list. + * @nh: The notifier head into which the block should be inserted. + * @nb: The notifier block to add. + * + * Note: This function must be synchronized by the caller with respect to other + * insert, find, and/or remove calls by holding ``struct ssam_nf.lock``. + * + * Return: Returns zero on success, %-EEXIST if the notifier block has already + * been registered. + */ +static int ssam_nfblk_insert(struct ssam_nf_head *nh, struct ssam_notifier_block *nb) +{ + struct ssam_notifier_block *p; + struct list_head *h; + + /* Runs under lock, no need for RCU variant. */ + list_for_each(h, &nh->head) { + p = list_entry(h, struct ssam_notifier_block, node); + + if (unlikely(p == nb)) { + WARN(1, "double register detected"); + return -EEXIST; + } + + if (nb->priority > p->priority) + break; + } + + list_add_tail_rcu(&nb->node, h); + return 0; +} + +/** + * ssam_nfblk_find() - Check if a notifier block is registered on the given + * notifier head. + * list. + * @nh: The notifier head on which to search. + * @nb: The notifier block to search for. + * + * Note: This function must be synchronized by the caller with respect to other + * insert, find, and/or remove calls by holding ``struct ssam_nf.lock``. + * + * Return: Returns true if the given notifier block is registered on the given + * notifier head, false otherwise. + */ +static bool ssam_nfblk_find(struct ssam_nf_head *nh, struct ssam_notifier_block *nb) +{ + struct ssam_notifier_block *p; + + /* Runs under lock, no need for RCU variant. */ + list_for_each_entry(p, &nh->head, node) { + if (p == nb) + return true; + } + + return false; +} + +/** + * ssam_nfblk_remove() - Remove a notifier block from its notifier list. + * @nb: The notifier block to be removed. + * + * Note: This function must be synchronized by the caller with respect to + * other insert, find, and/or remove calls by holding ``struct ssam_nf.lock``. + * Furthermore, the caller _must_ ensure SRCU synchronization by calling + * synchronize_srcu() with ``nh->srcu`` after leaving the critical section, to + * ensure that the removed notifier block is not in use any more. + */ +static void ssam_nfblk_remove(struct ssam_notifier_block *nb) +{ + list_del_rcu(&nb->node); +} + +/** + * ssam_nf_head_init() - Initialize the given notifier head. + * @nh: The notifier head to initialize. + */ +static int ssam_nf_head_init(struct ssam_nf_head *nh) +{ + int status; + + status = init_srcu_struct(&nh->srcu); + if (status) + return status; + + INIT_LIST_HEAD(&nh->head); + return 0; +} + +/** + * ssam_nf_head_destroy() - Deinitialize the given notifier head. + * @nh: The notifier head to deinitialize. + */ +static void ssam_nf_head_destroy(struct ssam_nf_head *nh) +{ + cleanup_srcu_struct(&nh->srcu); +} + + +/* -- Event/notification registry. ------------------------------------------ */ + +/** + * struct ssam_nf_refcount_key - Key used for event activation reference + * counting. + * @reg: The registry via which the event is enabled/disabled. + * @id: The ID uniquely describing the event. + */ +struct ssam_nf_refcount_key { + struct ssam_event_registry reg; + struct ssam_event_id id; +}; + +/** + * struct ssam_nf_refcount_entry - RB-tree entry for reference counting event + * activations. + * @node: The node of this entry in the rb-tree. + * @key: The key of the event. + * @refcount: The reference-count of the event. + * @flags: The flags used when enabling the event. + */ +struct ssam_nf_refcount_entry { + struct rb_node node; + struct ssam_nf_refcount_key key; + int refcount; + u8 flags; +}; + +/** + * ssam_nf_refcount_inc() - Increment reference-/activation-count of the given + * event. + * @nf: The notifier system reference. + * @reg: The registry used to enable/disable the event. + * @id: The event ID. + * + * Increments the reference-/activation-count associated with the specified + * event type/ID, allocating a new entry for this event ID if necessary. A + * newly allocated entry will have a refcount of one. + * + * Note: ``nf->lock`` must be held when calling this function. + * + * Return: Returns the refcount entry on success. Returns an error pointer + * with %-ENOSPC if there have already been %INT_MAX events of the specified + * ID and type registered, or %-ENOMEM if the entry could not be allocated. + */ +static struct ssam_nf_refcount_entry * +ssam_nf_refcount_inc(struct ssam_nf *nf, struct ssam_event_registry reg, + struct ssam_event_id id) +{ + struct ssam_nf_refcount_entry *entry; + struct ssam_nf_refcount_key key; + struct rb_node **link = &nf->refcount.rb_node; + struct rb_node *parent = NULL; + int cmp; + + lockdep_assert_held(&nf->lock); + + key.reg = reg; + key.id = id; + + while (*link) { + entry = rb_entry(*link, struct ssam_nf_refcount_entry, node); + parent = *link; + + cmp = memcmp(&key, &entry->key, sizeof(key)); + if (cmp < 0) { + link = &(*link)->rb_left; + } else if (cmp > 0) { + link = &(*link)->rb_right; + } else if (entry->refcount < INT_MAX) { + entry->refcount++; + return entry; + } else { + WARN_ON(1); + return ERR_PTR(-ENOSPC); + } + } + + entry = kzalloc(sizeof(*entry), GFP_KERNEL); + if (!entry) + return ERR_PTR(-ENOMEM); + + entry->key = key; + entry->refcount = 1; + + rb_link_node(&entry->node, parent, link); + rb_insert_color(&entry->node, &nf->refcount); + + return entry; +} + +/** + * ssam_nf_refcount_dec() - Decrement reference-/activation-count of the given + * event. + * @nf: The notifier system reference. + * @reg: The registry used to enable/disable the event. + * @id: The event ID. + * + * Decrements the reference-/activation-count of the specified event, + * returning its entry. If the returned entry has a refcount of zero, the + * caller is responsible for freeing it using kfree(). + * + * Note: ``nf->lock`` must be held when calling this function. + * + * Return: Returns the refcount entry on success or %NULL if the entry has not + * been found. + */ +static struct ssam_nf_refcount_entry * +ssam_nf_refcount_dec(struct ssam_nf *nf, struct ssam_event_registry reg, + struct ssam_event_id id) +{ + struct ssam_nf_refcount_entry *entry; + struct ssam_nf_refcount_key key; + struct rb_node *node = nf->refcount.rb_node; + int cmp; + + lockdep_assert_held(&nf->lock); + + key.reg = reg; + key.id = id; + + while (node) { + entry = rb_entry(node, struct ssam_nf_refcount_entry, node); + + cmp = memcmp(&key, &entry->key, sizeof(key)); + if (cmp < 0) { + node = node->rb_left; + } else if (cmp > 0) { + node = node->rb_right; + } else { + entry->refcount--; + if (entry->refcount == 0) + rb_erase(&entry->node, &nf->refcount); + + return entry; + } + } + + return NULL; +} + +/** + * ssam_nf_refcount_empty() - Test if the notification system has any + * enabled/active events. + * @nf: The notification system. + */ +static bool ssam_nf_refcount_empty(struct ssam_nf *nf) +{ + return RB_EMPTY_ROOT(&nf->refcount); +} + +/** + * ssam_nf_call() - Call notification callbacks for the provided event. + * @nf: The notifier system + * @dev: The associated device, only used for logging. + * @rqid: The request ID of the event. + * @event: The event provided to the callbacks. + * + * Execute registered callbacks in order of their priority until either no + * callback is left or a callback returns a value with the %SSAM_NOTIF_STOP + * bit set. Note that this bit is set automatically when converting non-zero + * error values via ssam_notifier_from_errno() to notifier values. + * + * Also note that any callback that could handle an event should return a value + * with bit %SSAM_NOTIF_HANDLED set, indicating that the event does not go + * unhandled/ignored. In case no registered callback could handle an event, + * this function will emit a warning. + * + * In case a callback failed, this function will emit an error message. + */ +static void ssam_nf_call(struct ssam_nf *nf, struct device *dev, u16 rqid, + struct ssam_event *event) +{ + struct ssam_nf_head *nf_head; + int status, nf_ret; + + if (!ssh_rqid_is_event(rqid)) { + dev_warn(dev, "event: unsupported rqid: %#06x\n", rqid); + return; + } + + nf_head = &nf->head[ssh_rqid_to_event(rqid)]; + nf_ret = ssam_nfblk_call_chain(nf_head, event); + status = ssam_notifier_to_errno(nf_ret); + + if (status < 0) { + dev_err(dev, + "event: error handling event: %d (tc: %#04x, tid: %#04x, cid: %#04x, iid: %#04x)\n", + status, event->target_category, event->target_id, + event->command_id, event->instance_id); + } else if (!(nf_ret & SSAM_NOTIF_HANDLED)) { + dev_warn(dev, + "event: unhandled event (rqid: %#04x, tc: %#04x, tid: %#04x, cid: %#04x, iid: %#04x)\n", + rqid, event->target_category, event->target_id, + event->command_id, event->instance_id); + } +} + +/** + * ssam_nf_init() - Initialize the notifier system. + * @nf: The notifier system to initialize. + */ +static int ssam_nf_init(struct ssam_nf *nf) +{ + int i, status; + + for (i = 0; i < SSH_NUM_EVENTS; i++) { + status = ssam_nf_head_init(&nf->head[i]); + if (status) + break; + } + + if (status) { + while (i--) + ssam_nf_head_destroy(&nf->head[i]); + + return status; + } + + mutex_init(&nf->lock); + return 0; +} + +/** + * ssam_nf_destroy() - Deinitialize the notifier system. + * @nf: The notifier system to deinitialize. + */ +static void ssam_nf_destroy(struct ssam_nf *nf) +{ + int i; + + for (i = 0; i < SSH_NUM_EVENTS; i++) + ssam_nf_head_destroy(&nf->head[i]); + + mutex_destroy(&nf->lock); +} + + +/* -- Event/async request completion system. -------------------------------- */ + +#define SSAM_CPLT_WQ_NAME "ssam_cpltq" + +/* + * SSAM_CPLT_WQ_BATCH - Maximum number of event item completions executed per + * work execution. Used to prevent livelocking of the workqueue. Value chosen + * via educated guess, may be adjusted. + */ +#define SSAM_CPLT_WQ_BATCH 10 + +/** + * ssam_event_item_alloc() - Allocate an event item with the given payload size. + * @len: The event payload length. + * @flags: The flags used for allocation. + * + * Allocate an event item with the given payload size. Sets the item + * operations and payload length values. The item free callback (``ops.free``) + * should not be overwritten after this call. + * + * Return: Returns the newly allocated event item. + */ +static struct ssam_event_item *ssam_event_item_alloc(size_t len, gfp_t flags) +{ + struct ssam_event_item *item; + + item = kzalloc(struct_size(item, event.data, len), flags); + if (!item) + return NULL; + + item->event.length = len; + return item; +} + +/** + * ssam_event_queue_push() - Push an event item to the event queue. + * @q: The event queue. + * @item: The item to add. + */ +static void ssam_event_queue_push(struct ssam_event_queue *q, + struct ssam_event_item *item) +{ + spin_lock(&q->lock); + list_add_tail(&item->node, &q->head); + spin_unlock(&q->lock); +} + +/** + * ssam_event_queue_pop() - Pop the next event item from the event queue. + * @q: The event queue. + * + * Returns and removes the next event item from the queue. Returns %NULL If + * there is no event item left. + */ +static struct ssam_event_item *ssam_event_queue_pop(struct ssam_event_queue *q) +{ + struct ssam_event_item *item; + + spin_lock(&q->lock); + item = list_first_entry_or_null(&q->head, struct ssam_event_item, node); + if (item) + list_del(&item->node); + spin_unlock(&q->lock); + + return item; +} + +/** + * ssam_event_queue_is_empty() - Check if the event queue is empty. + * @q: The event queue. + */ +static bool ssam_event_queue_is_empty(struct ssam_event_queue *q) +{ + bool empty; + + spin_lock(&q->lock); + empty = list_empty(&q->head); + spin_unlock(&q->lock); + + return empty; +} + +/** + * ssam_cplt_get_event_queue() - Get the event queue for the given parameters. + * @cplt: The completion system on which to look for the queue. + * @tid: The target ID of the queue. + * @rqid: The request ID representing the event ID for which to get the queue. + * + * Return: Returns the event queue corresponding to the event type described + * by the given parameters. If the request ID does not represent an event, + * this function returns %NULL. If the target ID is not supported, this + * function will fall back to the default target ID (``tid = 1``). + */ +static +struct ssam_event_queue *ssam_cplt_get_event_queue(struct ssam_cplt *cplt, + u8 tid, u16 rqid) +{ + u16 event = ssh_rqid_to_event(rqid); + u16 tidx = ssh_tid_to_index(tid); + + if (!ssh_rqid_is_event(rqid)) { + dev_err(cplt->dev, "event: unsupported request ID: %#06x\n", rqid); + return NULL; + } + + if (!ssh_tid_is_valid(tid)) { + dev_warn(cplt->dev, "event: unsupported target ID: %u\n", tid); + tidx = 0; + } + + return &cplt->event.target[tidx].queue[event]; +} + +/** + * ssam_cplt_submit() - Submit a work item to the completion system workqueue. + * @cplt: The completion system. + * @work: The work item to submit. + */ +static bool ssam_cplt_submit(struct ssam_cplt *cplt, struct work_struct *work) +{ + return queue_work(cplt->wq, work); +} + +/** + * ssam_cplt_submit_event() - Submit an event to the completion system. + * @cplt: The completion system. + * @item: The event item to submit. + * + * Submits the event to the completion system by queuing it on the event item + * queue and queuing the respective event queue work item on the completion + * workqueue, which will eventually complete the event. + * + * Return: Returns zero on success, %-EINVAL if there is no event queue that + * can handle the given event item. + */ +static int ssam_cplt_submit_event(struct ssam_cplt *cplt, + struct ssam_event_item *item) +{ + struct ssam_event_queue *evq; + + evq = ssam_cplt_get_event_queue(cplt, item->event.target_id, item->rqid); + if (!evq) + return -EINVAL; + + ssam_event_queue_push(evq, item); + ssam_cplt_submit(cplt, &evq->work); + return 0; +} + +/** + * ssam_cplt_flush() - Flush the completion system. + * @cplt: The completion system. + * + * Flush the completion system by waiting until all currently submitted work + * items have been completed. + * + * Note: This function does not guarantee that all events will have been + * handled once this call terminates. In case of a larger number of + * to-be-completed events, the event queue work function may re-schedule its + * work item, which this flush operation will ignore. + * + * This operation is only intended to, during normal operation prior to + * shutdown, try to complete most events and requests to get them out of the + * system while the system is still fully operational. It does not aim to + * provide any guarantee that all of them have been handled. + */ +static void ssam_cplt_flush(struct ssam_cplt *cplt) +{ + flush_workqueue(cplt->wq); +} + +static void ssam_event_queue_work_fn(struct work_struct *work) +{ + struct ssam_event_queue *queue; + struct ssam_event_item *item; + struct ssam_nf *nf; + struct device *dev; + unsigned int iterations = SSAM_CPLT_WQ_BATCH; + + queue = container_of(work, struct ssam_event_queue, work); + nf = &queue->cplt->event.notif; + dev = queue->cplt->dev; + + /* Limit number of processed events to avoid livelocking. */ + do { + item = ssam_event_queue_pop(queue); + if (!item) + return; + + ssam_nf_call(nf, dev, item->rqid, &item->event); + kfree(item); + } while (--iterations); + + if (!ssam_event_queue_is_empty(queue)) + ssam_cplt_submit(queue->cplt, &queue->work); +} + +/** + * ssam_event_queue_init() - Initialize an event queue. + * @cplt: The completion system on which the queue resides. + * @evq: The event queue to initialize. + */ +static void ssam_event_queue_init(struct ssam_cplt *cplt, + struct ssam_event_queue *evq) +{ + evq->cplt = cplt; + spin_lock_init(&evq->lock); + INIT_LIST_HEAD(&evq->head); + INIT_WORK(&evq->work, ssam_event_queue_work_fn); +} + +/** + * ssam_cplt_init() - Initialize completion system. + * @cplt: The completion system to initialize. + * @dev: The device used for logging. + */ +static int ssam_cplt_init(struct ssam_cplt *cplt, struct device *dev) +{ + struct ssam_event_target *target; + int status, c, i; + + cplt->dev = dev; + + cplt->wq = create_workqueue(SSAM_CPLT_WQ_NAME); + if (!cplt->wq) + return -ENOMEM; + + for (c = 0; c < ARRAY_SIZE(cplt->event.target); c++) { + target = &cplt->event.target[c]; + + for (i = 0; i < ARRAY_SIZE(target->queue); i++) + ssam_event_queue_init(cplt, &target->queue[i]); + } + + status = ssam_nf_init(&cplt->event.notif); + if (status) + destroy_workqueue(cplt->wq); + + return status; +} + +/** + * ssam_cplt_destroy() - Deinitialize the completion system. + * @cplt: The completion system to deinitialize. + * + * Deinitialize the given completion system and ensure that all pending, i.e. + * yet-to-be-completed, event items and requests have been handled. + */ +static void ssam_cplt_destroy(struct ssam_cplt *cplt) +{ + /* + * Note: destroy_workqueue ensures that all currently queued work will + * be fully completed and the workqueue drained. This means that this + * call will inherently also free any queued ssam_event_items, thus we + * don't have to take care of that here explicitly. + */ + destroy_workqueue(cplt->wq); + ssam_nf_destroy(&cplt->event.notif); +} + + +/* -- Main SSAM device structures. ------------------------------------------ */ + +/** + * ssam_controller_device() - Get the &struct device associated with this + * controller. + * @c: The controller for which to get the device. + * + * Return: Returns the &struct device associated with this controller, + * providing its lower-level transport. + */ +struct device *ssam_controller_device(struct ssam_controller *c) +{ + return ssh_rtl_get_device(&c->rtl); +} +EXPORT_SYMBOL_GPL(ssam_controller_device); + +static void __ssam_controller_release(struct kref *kref) +{ + struct ssam_controller *ctrl = to_ssam_controller(kref, kref); + + /* + * The lock-call here is to satisfy lockdep. At this point we really + * expect this to be the last remaining reference to the controller. + * Anything else is a bug. + */ + ssam_controller_lock(ctrl); + ssam_controller_destroy(ctrl); + ssam_controller_unlock(ctrl); + + kfree(ctrl); +} + +/** + * ssam_controller_get() - Increment reference count of controller. + * @c: The controller. + * + * Return: Returns the controller provided as input. + */ +struct ssam_controller *ssam_controller_get(struct ssam_controller *c) +{ + if (c) + kref_get(&c->kref); + return c; +} +EXPORT_SYMBOL_GPL(ssam_controller_get); + +/** + * ssam_controller_put() - Decrement reference count of controller. + * @c: The controller. + */ +void ssam_controller_put(struct ssam_controller *c) +{ + if (c) + kref_put(&c->kref, __ssam_controller_release); +} +EXPORT_SYMBOL_GPL(ssam_controller_put); + +/** + * ssam_controller_statelock() - Lock the controller against state transitions. + * @c: The controller to lock. + * + * Lock the controller against state transitions. Holding this lock guarantees + * that the controller will not transition between states, i.e. if the + * controller is in state "started", when this lock has been acquired, it will + * remain in this state at least until the lock has been released. + * + * Multiple clients may concurrently hold this lock. In other words: The + * ``statelock`` functions represent the read-lock part of a r/w-semaphore. + * Actions causing state transitions of the controller must be executed while + * holding the write-part of this r/w-semaphore (see ssam_controller_lock() + * and ssam_controller_unlock() for that). + * + * See ssam_controller_stateunlock() for the corresponding unlock function. + */ +void ssam_controller_statelock(struct ssam_controller *c) +{ + down_read(&c->lock); +} +EXPORT_SYMBOL_GPL(ssam_controller_statelock); + +/** + * ssam_controller_stateunlock() - Unlock controller state transitions. + * @c: The controller to unlock. + * + * See ssam_controller_statelock() for the corresponding lock function. + */ +void ssam_controller_stateunlock(struct ssam_controller *c) +{ + up_read(&c->lock); +} +EXPORT_SYMBOL_GPL(ssam_controller_stateunlock); + +/** + * ssam_controller_lock() - Acquire the main controller lock. + * @c: The controller to lock. + * + * This lock must be held for any state transitions, including transition to |