mirror of
https://mirrors.bfsu.edu.cn/git/linux.git
synced 2025-01-12 08:45:11 +08:00
b430e1d65e
Disabling events silently fails due to the wrong command ID being used.
Instead of the command ID for the disable call, the command ID for the
enable call was being used. This causes the disable call to enable the
event instead. As the event is already enabled when we call this
function, the EC silently drops this command and does nothing.
Use the correct command ID for disabling the event to fix this.
Fixes: c167b9c7e3
("platform/surface: Add Surface Aggregator subsystem")
Signed-off-by: Maximilian Luz <luzmaximilian@gmail.com>
Link: https://lore.kernel.org/r/20210603000636.568846-1-luzmaximilian@gmail.com
Signed-off-by: Hans de Goede <hdegoede@redhat.com>
2579 lines
76 KiB
C
2579 lines
76 KiB
C
// 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"
|
|
|
|
#include "trace.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_CACHE_PAYLOAD_LEN - Maximum payload length for a cached
|
|
* &struct ssam_event_item.
|
|
*
|
|
* This length has been chosen to be accommodate standard touchpad and
|
|
* keyboard input events. Events with larger payloads will be allocated
|
|
* separately.
|
|
*/
|
|
#define SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN 32
|
|
|
|
static struct kmem_cache *ssam_event_item_cache;
|
|
|
|
/**
|
|
* ssam_event_item_cache_init() - Initialize the event item cache.
|
|
*/
|
|
int ssam_event_item_cache_init(void)
|
|
{
|
|
const unsigned int size = sizeof(struct ssam_event_item)
|
|
+ SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN;
|
|
const unsigned int align = __alignof__(struct ssam_event_item);
|
|
struct kmem_cache *cache;
|
|
|
|
cache = kmem_cache_create("ssam_event_item", size, align, 0, NULL);
|
|
if (!cache)
|
|
return -ENOMEM;
|
|
|
|
ssam_event_item_cache = cache;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_event_item_cache_destroy() - Deinitialize the event item cache.
|
|
*/
|
|
void ssam_event_item_cache_destroy(void)
|
|
{
|
|
kmem_cache_destroy(ssam_event_item_cache);
|
|
ssam_event_item_cache = NULL;
|
|
}
|
|
|
|
static void __ssam_event_item_free_cached(struct ssam_event_item *item)
|
|
{
|
|
kmem_cache_free(ssam_event_item_cache, item);
|
|
}
|
|
|
|
static void __ssam_event_item_free_generic(struct ssam_event_item *item)
|
|
{
|
|
kfree(item);
|
|
}
|
|
|
|
/**
|
|
* ssam_event_item_free() - Free the provided event item.
|
|
* @item: The event item to free.
|
|
*/
|
|
static void ssam_event_item_free(struct ssam_event_item *item)
|
|
{
|
|
trace_ssam_event_item_free(item);
|
|
item->ops.free(item);
|
|
}
|
|
|
|
/**
|
|
* 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, preferring allocation
|
|
* from the event item cache if the payload is small enough (i.e. smaller than
|
|
* %SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN). 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;
|
|
|
|
if (len <= SSAM_EVENT_ITEM_CACHE_PAYLOAD_LEN) {
|
|
item = kmem_cache_alloc(ssam_event_item_cache, flags);
|
|
if (!item)
|
|
return NULL;
|
|
|
|
item->ops.free = __ssam_event_item_free_cached;
|
|
} else {
|
|
item = kzalloc(struct_size(item, event.data, len), flags);
|
|
if (!item)
|
|
return NULL;
|
|
|
|
item->ops.free = __ssam_event_item_free_generic;
|
|
}
|
|
|
|
item->event.length = len;
|
|
|
|
trace_ssam_event_item_alloc(item, 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);
|
|
ssam_event_item_free(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
|
|
* suspend/resumed states and during shutdown. See ssam_controller_statelock()
|
|
* for more details on controller locking.
|
|
*
|
|
* See ssam_controller_unlock() for the corresponding unlock function.
|
|
*/
|
|
void ssam_controller_lock(struct ssam_controller *c)
|
|
{
|
|
down_write(&c->lock);
|
|
}
|
|
|
|
/*
|
|
* ssam_controller_unlock() - Release the main controller lock.
|
|
* @c: The controller to unlock.
|
|
*
|
|
* See ssam_controller_lock() for the corresponding lock function.
|
|
*/
|
|
void ssam_controller_unlock(struct ssam_controller *c)
|
|
{
|
|
up_write(&c->lock);
|
|
}
|
|
|
|
static void ssam_handle_event(struct ssh_rtl *rtl,
|
|
const struct ssh_command *cmd,
|
|
const struct ssam_span *data)
|
|
{
|
|
struct ssam_controller *ctrl = to_ssam_controller(rtl, rtl);
|
|
struct ssam_event_item *item;
|
|
|
|
item = ssam_event_item_alloc(data->len, GFP_KERNEL);
|
|
if (!item)
|
|
return;
|
|
|
|
item->rqid = get_unaligned_le16(&cmd->rqid);
|
|
item->event.target_category = cmd->tc;
|
|
item->event.target_id = cmd->tid_in;
|
|
item->event.command_id = cmd->cid;
|
|
item->event.instance_id = cmd->iid;
|
|
memcpy(&item->event.data[0], data->ptr, data->len);
|
|
|
|
if (WARN_ON(ssam_cplt_submit_event(&ctrl->cplt, item)))
|
|
ssam_event_item_free(item);
|
|
}
|
|
|
|
static const struct ssh_rtl_ops ssam_rtl_ops = {
|
|
.handle_event = ssam_handle_event,
|
|
};
|
|
|
|
static bool ssam_notifier_is_empty(struct ssam_controller *ctrl);
|
|
static void ssam_notifier_unregister_all(struct ssam_controller *ctrl);
|
|
|
|
#define SSAM_SSH_DSM_REVISION 0
|
|
|
|
/* d5e383e1-d892-4a76-89fc-f6aaae7ed5b5 */
|
|
static const guid_t SSAM_SSH_DSM_GUID =
|
|
GUID_INIT(0xd5e383e1, 0xd892, 0x4a76,
|
|
0x89, 0xfc, 0xf6, 0xaa, 0xae, 0x7e, 0xd5, 0xb5);
|
|
|
|
enum ssh_dsm_fn {
|
|
SSH_DSM_FN_SSH_POWER_PROFILE = 0x05,
|
|
SSH_DSM_FN_SCREEN_ON_SLEEP_IDLE_TIMEOUT = 0x06,
|
|
SSH_DSM_FN_SCREEN_OFF_SLEEP_IDLE_TIMEOUT = 0x07,
|
|
SSH_DSM_FN_D3_CLOSES_HANDLE = 0x08,
|
|
SSH_DSM_FN_SSH_BUFFER_SIZE = 0x09,
|
|
};
|
|
|
|
static int ssam_dsm_get_functions(acpi_handle handle, u64 *funcs)
|
|
{
|
|
union acpi_object *obj;
|
|
u64 mask = 0;
|
|
int i;
|
|
|
|
*funcs = 0;
|
|
|
|
/*
|
|
* The _DSM function is only present on newer models. It is not
|
|
* present on 5th and 6th generation devices (i.e. up to and including
|
|
* Surface Pro 6, Surface Laptop 2, Surface Book 2).
|
|
*
|
|
* If the _DSM is not present, indicate that no function is supported.
|
|
* This will result in default values being set.
|
|
*/
|
|
if (!acpi_has_method(handle, "_DSM"))
|
|
return 0;
|
|
|
|
obj = acpi_evaluate_dsm_typed(handle, &SSAM_SSH_DSM_GUID,
|
|
SSAM_SSH_DSM_REVISION, 0, NULL,
|
|
ACPI_TYPE_BUFFER);
|
|
if (!obj)
|
|
return -EIO;
|
|
|
|
for (i = 0; i < obj->buffer.length && i < 8; i++)
|
|
mask |= (((u64)obj->buffer.pointer[i]) << (i * 8));
|
|
|
|
if (mask & BIT(0))
|
|
*funcs = mask;
|
|
|
|
ACPI_FREE(obj);
|
|
return 0;
|
|
}
|
|
|
|
static int ssam_dsm_load_u32(acpi_handle handle, u64 funcs, u64 func, u32 *ret)
|
|
{
|
|
union acpi_object *obj;
|
|
u64 val;
|
|
|
|
if (!(funcs & BIT_ULL(func)))
|
|
return 0; /* Not supported, leave *ret at its default value */
|
|
|
|
obj = acpi_evaluate_dsm_typed(handle, &SSAM_SSH_DSM_GUID,
|
|
SSAM_SSH_DSM_REVISION, func, NULL,
|
|
ACPI_TYPE_INTEGER);
|
|
if (!obj)
|
|
return -EIO;
|
|
|
|
val = obj->integer.value;
|
|
ACPI_FREE(obj);
|
|
|
|
if (val > U32_MAX)
|
|
return -ERANGE;
|
|
|
|
*ret = val;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_controller_caps_load_from_acpi() - Load controller capabilities from
|
|
* ACPI _DSM.
|
|
* @handle: The handle of the ACPI controller/SSH device.
|
|
* @caps: Where to store the capabilities in.
|
|
*
|
|
* Initializes the given controller capabilities with default values, then
|
|
* checks and, if the respective _DSM functions are available, loads the
|
|
* actual capabilities from the _DSM.
|
|
*
|
|
* Return: Returns zero on success, a negative error code on failure.
|
|
*/
|
|
static
|
|
int ssam_controller_caps_load_from_acpi(acpi_handle handle,
|
|
struct ssam_controller_caps *caps)
|
|
{
|
|
u32 d3_closes_handle = false;
|
|
u64 funcs;
|
|
int status;
|
|
|
|
/* Set defaults. */
|
|
caps->ssh_power_profile = U32_MAX;
|
|
caps->screen_on_sleep_idle_timeout = U32_MAX;
|
|
caps->screen_off_sleep_idle_timeout = U32_MAX;
|
|
caps->d3_closes_handle = false;
|
|
caps->ssh_buffer_size = U32_MAX;
|
|
|
|
/* Pre-load supported DSM functions. */
|
|
status = ssam_dsm_get_functions(handle, &funcs);
|
|
if (status)
|
|
return status;
|
|
|
|
/* Load actual values from ACPI, if present. */
|
|
status = ssam_dsm_load_u32(handle, funcs, SSH_DSM_FN_SSH_POWER_PROFILE,
|
|
&caps->ssh_power_profile);
|
|
if (status)
|
|
return status;
|
|
|
|
status = ssam_dsm_load_u32(handle, funcs,
|
|
SSH_DSM_FN_SCREEN_ON_SLEEP_IDLE_TIMEOUT,
|
|
&caps->screen_on_sleep_idle_timeout);
|
|
if (status)
|
|
return status;
|
|
|
|
status = ssam_dsm_load_u32(handle, funcs,
|
|
SSH_DSM_FN_SCREEN_OFF_SLEEP_IDLE_TIMEOUT,
|
|
&caps->screen_off_sleep_idle_timeout);
|
|
if (status)
|
|
return status;
|
|
|
|
status = ssam_dsm_load_u32(handle, funcs, SSH_DSM_FN_D3_CLOSES_HANDLE,
|
|
&d3_closes_handle);
|
|
if (status)
|
|
return status;
|
|
|
|
caps->d3_closes_handle = !!d3_closes_handle;
|
|
|
|
status = ssam_dsm_load_u32(handle, funcs, SSH_DSM_FN_SSH_BUFFER_SIZE,
|
|
&caps->ssh_buffer_size);
|
|
if (status)
|
|
return status;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_controller_init() - Initialize SSAM controller.
|
|
* @ctrl: The controller to initialize.
|
|
* @serdev: The serial device representing the underlying data transport.
|
|
*
|
|
* Initializes the given controller. Does neither start receiver nor
|
|
* transmitter threads. After this call, the controller has to be hooked up to
|
|
* the serdev core separately via &struct serdev_device_ops, relaying calls to
|
|
* ssam_controller_receive_buf() and ssam_controller_write_wakeup(). Once the
|
|
* controller has been hooked up, transmitter and receiver threads may be
|
|
* started via ssam_controller_start(). These setup steps need to be completed
|
|
* before controller can be used for requests.
|
|
*/
|
|
int ssam_controller_init(struct ssam_controller *ctrl,
|
|
struct serdev_device *serdev)
|
|
{
|
|
acpi_handle handle = ACPI_HANDLE(&serdev->dev);
|
|
int status;
|
|
|
|
init_rwsem(&ctrl->lock);
|
|
kref_init(&ctrl->kref);
|
|
|
|
status = ssam_controller_caps_load_from_acpi(handle, &ctrl->caps);
|
|
if (status)
|
|
return status;
|
|
|
|
dev_dbg(&serdev->dev,
|
|
"device capabilities:\n"
|
|
" ssh_power_profile: %u\n"
|
|
" ssh_buffer_size: %u\n"
|
|
" screen_on_sleep_idle_timeout: %u\n"
|
|
" screen_off_sleep_idle_timeout: %u\n"
|
|
" d3_closes_handle: %u\n",
|
|
ctrl->caps.ssh_power_profile,
|
|
ctrl->caps.ssh_buffer_size,
|
|
ctrl->caps.screen_on_sleep_idle_timeout,
|
|
ctrl->caps.screen_off_sleep_idle_timeout,
|
|
ctrl->caps.d3_closes_handle);
|
|
|
|
ssh_seq_reset(&ctrl->counter.seq);
|
|
ssh_rqid_reset(&ctrl->counter.rqid);
|
|
|
|
/* Initialize event/request completion system. */
|
|
status = ssam_cplt_init(&ctrl->cplt, &serdev->dev);
|
|
if (status)
|
|
return status;
|
|
|
|
/* Initialize request and packet transport layers. */
|
|
status = ssh_rtl_init(&ctrl->rtl, serdev, &ssam_rtl_ops);
|
|
if (status) {
|
|
ssam_cplt_destroy(&ctrl->cplt);
|
|
return status;
|
|
}
|
|
|
|
/*
|
|
* Set state via write_once even though we expect to be in an
|
|
* exclusive context, due to smoke-testing in
|
|
* ssam_request_sync_submit().
|
|
*/
|
|
WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_INITIALIZED);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_controller_start() - Start the receiver and transmitter threads of the
|
|
* controller.
|
|
* @ctrl: The controller.
|
|
*
|
|
* Note: When this function is called, the controller should be properly
|
|
* hooked up to the serdev core via &struct serdev_device_ops. Please refer
|
|
* to ssam_controller_init() for more details on controller initialization.
|
|
*
|
|
* This function must be called with the main controller lock held (i.e. by
|
|
* calling ssam_controller_lock()).
|
|
*/
|
|
int ssam_controller_start(struct ssam_controller *ctrl)
|
|
{
|
|
int status;
|
|
|
|
lockdep_assert_held_write(&ctrl->lock);
|
|
|
|
if (ctrl->state != SSAM_CONTROLLER_INITIALIZED)
|
|
return -EINVAL;
|
|
|
|
status = ssh_rtl_start(&ctrl->rtl);
|
|
if (status)
|
|
return status;
|
|
|
|
/*
|
|
* Set state via write_once even though we expect to be locked/in an
|
|
* exclusive context, due to smoke-testing in
|
|
* ssam_request_sync_submit().
|
|
*/
|
|
WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_STARTED);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* SSAM_CTRL_SHUTDOWN_FLUSH_TIMEOUT - Timeout for flushing requests during
|
|
* shutdown.
|
|
*
|
|
* Chosen to be larger than one full request timeout, including packets timing
|
|
* out. This value should give ample time to complete any outstanding requests
|
|
* during normal operation and account for the odd package timeout.
|
|
*/
|
|
#define SSAM_CTRL_SHUTDOWN_FLUSH_TIMEOUT msecs_to_jiffies(5000)
|
|
|
|
/**
|
|
* ssam_controller_shutdown() - Shut down the controller.
|
|
* @ctrl: The controller.
|
|
*
|
|
* Shuts down the controller by flushing all pending requests and stopping the
|
|
* transmitter and receiver threads. All requests submitted after this call
|
|
* will fail with %-ESHUTDOWN. While it is discouraged to do so, this function
|
|
* is safe to use in parallel with ongoing request submission.
|
|
*
|
|
* In the course of this shutdown procedure, all currently registered
|
|
* notifiers will be unregistered. It is, however, strongly recommended to not
|
|
* rely on this behavior, and instead the party registering the notifier
|
|
* should unregister it before the controller gets shut down, e.g. via the
|
|
* SSAM bus which guarantees client devices to be removed before a shutdown.
|
|
*
|
|
* Note that events may still be pending after this call, but, due to the
|
|
* notifiers being unregistered, these events will be dropped when the
|
|
* controller is subsequently destroyed via ssam_controller_destroy().
|
|
*
|
|
* This function must be called with the main controller lock held (i.e. by
|
|
* calling ssam_controller_lock()).
|
|
*/
|
|
void ssam_controller_shutdown(struct ssam_controller *ctrl)
|
|
{
|
|
enum ssam_controller_state s = ctrl->state;
|
|
int status;
|
|
|
|
lockdep_assert_held_write(&ctrl->lock);
|
|
|
|
if (s == SSAM_CONTROLLER_UNINITIALIZED || s == SSAM_CONTROLLER_STOPPED)
|
|
return;
|
|
|
|
/*
|
|
* Try to flush pending events and requests while everything still
|
|
* works. Note: There may still be packets and/or requests in the
|
|
* system after this call (e.g. via control packets submitted by the
|
|
* packet transport layer or flush timeout / failure, ...). Those will
|
|
* be handled with the ssh_rtl_shutdown() call below.
|
|
*/
|
|
status = ssh_rtl_flush(&ctrl->rtl, SSAM_CTRL_SHUTDOWN_FLUSH_TIMEOUT);
|
|
if (status) {
|
|
ssam_err(ctrl, "failed to flush request transport layer: %d\n",
|
|
status);
|
|
}
|
|
|
|
/* Try to flush all currently completing requests and events. */
|
|
ssam_cplt_flush(&ctrl->cplt);
|
|
|
|
/*
|
|
* We expect all notifiers to have been removed by the respective client
|
|
* driver that set them up at this point. If this warning occurs, some
|
|
* client driver has not done that...
|
|
*/
|
|
WARN_ON(!ssam_notifier_is_empty(ctrl));
|
|
|
|
/*
|
|
* Nevertheless, we should still take care of drivers that don't behave
|
|
* well. Thus disable all enabled events, unregister all notifiers.
|
|
*/
|
|
ssam_notifier_unregister_all(ctrl);
|
|
|
|
/*
|
|
* Cancel remaining requests. Ensure no new ones can be queued and stop
|
|
* threads.
|
|
*/
|
|
ssh_rtl_shutdown(&ctrl->rtl);
|
|
|
|
/*
|
|
* Set state via write_once even though we expect to be locked/in an
|
|
* exclusive context, due to smoke-testing in
|
|
* ssam_request_sync_submit().
|
|
*/
|
|
WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_STOPPED);
|
|
ctrl->rtl.ptl.serdev = NULL;
|
|
}
|
|
|
|
/**
|
|
* ssam_controller_destroy() - Destroy the controller and free its resources.
|
|
* @ctrl: The controller.
|
|
*
|
|
* Ensures that all resources associated with the controller get freed. This
|
|
* function should only be called after the controller has been stopped via
|
|
* ssam_controller_shutdown(). In general, this function should not be called
|
|
* directly. The only valid place to call this function directly is during
|
|
* initialization, before the controller has been fully initialized and passed
|
|
* to other processes. This function is called automatically when the
|
|
* reference count of the controller reaches zero.
|
|
*
|
|
* This function must be called with the main controller lock held (i.e. by
|
|
* calling ssam_controller_lock()).
|
|
*/
|
|
void ssam_controller_destroy(struct ssam_controller *ctrl)
|
|
{
|
|
lockdep_assert_held_write(&ctrl->lock);
|
|
|
|
if (ctrl->state == SSAM_CONTROLLER_UNINITIALIZED)
|
|
return;
|
|
|
|
WARN_ON(ctrl->state != SSAM_CONTROLLER_STOPPED);
|
|
|
|
/*
|
|
* Note: New events could still have been received after the previous
|
|
* flush in ssam_controller_shutdown, before the request transport layer
|
|
* has been shut down. At this point, after the shutdown, we can be sure
|
|
* that no new events will be queued. The call to ssam_cplt_destroy will
|
|
* ensure that those remaining are being completed and freed.
|
|
*/
|
|
|
|
/* Actually free resources. */
|
|
ssam_cplt_destroy(&ctrl->cplt);
|
|
ssh_rtl_destroy(&ctrl->rtl);
|
|
|
|
/*
|
|
* Set state via write_once even though we expect to be locked/in an
|
|
* exclusive context, due to smoke-testing in
|
|
* ssam_request_sync_submit().
|
|
*/
|
|
WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_UNINITIALIZED);
|
|
}
|
|
|
|
/**
|
|
* ssam_controller_suspend() - Suspend the controller.
|
|
* @ctrl: The controller to suspend.
|
|
*
|
|
* Marks the controller as suspended. Note that display-off and D0-exit
|
|
* notifications have to be sent manually before transitioning the controller
|
|
* into the suspended state via this function.
|
|
*
|
|
* See ssam_controller_resume() for the corresponding resume function.
|
|
*
|
|
* Return: Returns %-EINVAL if the controller is currently not in the
|
|
* "started" state.
|
|
*/
|
|
int ssam_controller_suspend(struct ssam_controller *ctrl)
|
|
{
|
|
ssam_controller_lock(ctrl);
|
|
|
|
if (ctrl->state != SSAM_CONTROLLER_STARTED) {
|
|
ssam_controller_unlock(ctrl);
|
|
return -EINVAL;
|
|
}
|
|
|
|
ssam_dbg(ctrl, "pm: suspending controller\n");
|
|
|
|
/*
|
|
* Set state via write_once even though we're locked, due to
|
|
* smoke-testing in ssam_request_sync_submit().
|
|
*/
|
|
WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_SUSPENDED);
|
|
|
|
ssam_controller_unlock(ctrl);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_controller_resume() - Resume the controller from suspend.
|
|
* @ctrl: The controller to resume.
|
|
*
|
|
* Resume the controller from the suspended state it was put into via
|
|
* ssam_controller_suspend(). This function does not issue display-on and
|
|
* D0-entry notifications. If required, those have to be sent manually after
|
|
* this call.
|
|
*
|
|
* Return: Returns %-EINVAL if the controller is currently not suspended.
|
|
*/
|
|
int ssam_controller_resume(struct ssam_controller *ctrl)
|
|
{
|
|
ssam_controller_lock(ctrl);
|
|
|
|
if (ctrl->state != SSAM_CONTROLLER_SUSPENDED) {
|
|
ssam_controller_unlock(ctrl);
|
|
return -EINVAL;
|
|
}
|
|
|
|
ssam_dbg(ctrl, "pm: resuming controller\n");
|
|
|
|
/*
|
|
* Set state via write_once even though we're locked, due to
|
|
* smoke-testing in ssam_request_sync_submit().
|
|
*/
|
|
WRITE_ONCE(ctrl->state, SSAM_CONTROLLER_STARTED);
|
|
|
|
ssam_controller_unlock(ctrl);
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* -- Top-level request interface ------------------------------------------- */
|
|
|
|
/**
|
|
* ssam_request_write_data() - Construct and write SAM request message to
|
|
* buffer.
|
|
* @buf: The buffer to write the data to.
|
|
* @ctrl: The controller via which the request will be sent.
|
|
* @spec: The request data and specification.
|
|
*
|
|
* Constructs a SAM/SSH request message and writes it to the provided buffer.
|
|
* The request and transport counters, specifically RQID and SEQ, will be set
|
|
* in this call. These counters are obtained from the controller. It is thus
|
|
* only valid to send the resulting message via the controller specified here.
|
|
*
|
|
* For calculation of the required buffer size, refer to the
|
|
* SSH_COMMAND_MESSAGE_LENGTH() macro.
|
|
*
|
|
* Return: Returns the number of bytes used in the buffer on success. Returns
|
|
* %-EINVAL if the payload length provided in the request specification is too
|
|
* large (larger than %SSH_COMMAND_MAX_PAYLOAD_SIZE) or if the provided buffer
|
|
* is too small.
|
|
*/
|
|
ssize_t ssam_request_write_data(struct ssam_span *buf,
|
|
struct ssam_controller *ctrl,
|
|
const struct ssam_request *spec)
|
|
{
|
|
struct msgbuf msgb;
|
|
u16 rqid;
|
|
u8 seq;
|
|
|
|
if (spec->length > SSH_COMMAND_MAX_PAYLOAD_SIZE)
|
|
return -EINVAL;
|
|
|
|
if (SSH_COMMAND_MESSAGE_LENGTH(spec->length) > buf->len)
|
|
return -EINVAL;
|
|
|
|
msgb_init(&msgb, buf->ptr, buf->len);
|
|
seq = ssh_seq_next(&ctrl->counter.seq);
|
|
rqid = ssh_rqid_next(&ctrl->counter.rqid);
|
|
msgb_push_cmd(&msgb, seq, rqid, spec);
|
|
|
|
return msgb_bytes_used(&msgb);
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_request_write_data);
|
|
|
|
static void ssam_request_sync_complete(struct ssh_request *rqst,
|
|
const struct ssh_command *cmd,
|
|
const struct ssam_span *data, int status)
|
|
{
|
|
struct ssh_rtl *rtl = ssh_request_rtl(rqst);
|
|
struct ssam_request_sync *r;
|
|
|
|
r = container_of(rqst, struct ssam_request_sync, base);
|
|
r->status = status;
|
|
|
|
if (r->resp)
|
|
r->resp->length = 0;
|
|
|
|
if (status) {
|
|
rtl_dbg_cond(rtl, "rsp: request failed: %d\n", status);
|
|
return;
|
|
}
|
|
|
|
if (!data) /* Handle requests without a response. */
|
|
return;
|
|
|
|
if (!r->resp || !r->resp->pointer) {
|
|
if (data->len)
|
|
rtl_warn(rtl, "rsp: no response buffer provided, dropping data\n");
|
|
return;
|
|
}
|
|
|
|
if (data->len > r->resp->capacity) {
|
|
rtl_err(rtl,
|
|
"rsp: response buffer too small, capacity: %zu bytes, got: %zu bytes\n",
|
|
r->resp->capacity, data->len);
|
|
r->status = -ENOSPC;
|
|
return;
|
|
}
|
|
|
|
r->resp->length = data->len;
|
|
memcpy(r->resp->pointer, data->ptr, data->len);
|
|
}
|
|
|
|
static void ssam_request_sync_release(struct ssh_request *rqst)
|
|
{
|
|
complete_all(&container_of(rqst, struct ssam_request_sync, base)->comp);
|
|
}
|
|
|
|
static const struct ssh_request_ops ssam_request_sync_ops = {
|
|
.release = ssam_request_sync_release,
|
|
.complete = ssam_request_sync_complete,
|
|
};
|
|
|
|
/**
|
|
* ssam_request_sync_alloc() - Allocate a synchronous request.
|
|
* @payload_len: The length of the request payload.
|
|
* @flags: Flags used for allocation.
|
|
* @rqst: Where to store the pointer to the allocated request.
|
|
* @buffer: Where to store the buffer descriptor for the message buffer of
|
|
* the request.
|
|
*
|
|
* Allocates a synchronous request with corresponding message buffer. The
|
|
* request still needs to be initialized ssam_request_sync_init() before
|
|
* it can be submitted, and the message buffer data must still be set to the
|
|
* returned buffer via ssam_request_sync_set_data() after it has been filled,
|
|
* if need be with adjusted message length.
|
|
*
|
|
* After use, the request and its corresponding message buffer should be freed
|
|
* via ssam_request_sync_free(). The buffer must not be freed separately.
|
|
*
|
|
* Return: Returns zero on success, %-ENOMEM if the request could not be
|
|
* allocated.
|
|
*/
|
|
int ssam_request_sync_alloc(size_t payload_len, gfp_t flags,
|
|
struct ssam_request_sync **rqst,
|
|
struct ssam_span *buffer)
|
|
{
|
|
size_t msglen = SSH_COMMAND_MESSAGE_LENGTH(payload_len);
|
|
|
|
*rqst = kzalloc(sizeof(**rqst) + msglen, flags);
|
|
if (!*rqst)
|
|
return -ENOMEM;
|
|
|
|
buffer->ptr = (u8 *)(*rqst + 1);
|
|
buffer->len = msglen;
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_request_sync_alloc);
|
|
|
|
/**
|
|
* ssam_request_sync_free() - Free a synchronous request.
|
|
* @rqst: The request to be freed.
|
|
*
|
|
* Free a synchronous request and its corresponding buffer allocated with
|
|
* ssam_request_sync_alloc(). Do not use for requests allocated on the stack
|
|
* or via any other function.
|
|
*
|
|
* Warning: The caller must ensure that the request is not in use any more.
|
|
* I.e. the caller must ensure that it has the only reference to the request
|
|
* and the request is not currently pending. This means that the caller has
|
|
* either never submitted the request, request submission has failed, or the
|
|
* caller has waited until the submitted request has been completed via
|
|
* ssam_request_sync_wait().
|
|
*/
|
|
void ssam_request_sync_free(struct ssam_request_sync *rqst)
|
|
{
|
|
kfree(rqst);
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_request_sync_free);
|
|
|
|
/**
|
|
* ssam_request_sync_init() - Initialize a synchronous request struct.
|
|
* @rqst: The request to initialize.
|
|
* @flags: The request flags.
|
|
*
|
|
* Initializes the given request struct. Does not initialize the request
|
|
* message data. This has to be done explicitly after this call via
|
|
* ssam_request_sync_set_data() and the actual message data has to be written
|
|
* via ssam_request_write_data().
|
|
*
|
|
* Return: Returns zero on success or %-EINVAL if the given flags are invalid.
|
|
*/
|
|
int ssam_request_sync_init(struct ssam_request_sync *rqst,
|
|
enum ssam_request_flags flags)
|
|
{
|
|
int status;
|
|
|
|
status = ssh_request_init(&rqst->base, flags, &ssam_request_sync_ops);
|
|
if (status)
|
|
return status;
|
|
|
|
init_completion(&rqst->comp);
|
|
rqst->resp = NULL;
|
|
rqst->status = 0;
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_request_sync_init);
|
|
|
|
/**
|
|
* ssam_request_sync_submit() - Submit a synchronous request.
|
|
* @ctrl: The controller with which to submit the request.
|
|
* @rqst: The request to submit.
|
|
*
|
|
* Submit a synchronous request. The request has to be initialized and
|
|
* properly set up, including response buffer (may be %NULL if no response is
|
|
* expected) and command message data. This function does not wait for the
|
|
* request to be completed.
|
|
*
|
|
* If this function succeeds, ssam_request_sync_wait() must be used to ensure
|
|
* that the request has been completed before the response data can be
|
|
* accessed and/or the request can be freed. On failure, the request may
|
|
* immediately be freed.
|
|
*
|
|
* This function may only be used if the controller is active, i.e. has been
|
|
* initialized and not suspended.
|
|
*/
|
|
int ssam_request_sync_submit(struct ssam_controller *ctrl,
|
|
struct ssam_request_sync *rqst)
|
|
{
|
|
int status;
|
|
|
|
/*
|
|
* This is only a superficial check. In general, the caller needs to
|
|
* ensure that the controller is initialized and is not (and does not
|
|
* get) suspended during use, i.e. until the request has been completed
|
|
* (if _absolutely_ necessary, by use of ssam_controller_statelock/
|
|
* ssam_controller_stateunlock, but something like ssam_client_link
|
|
* should be preferred as this needs to last until the request has been
|
|
* completed).
|
|
*
|
|
* Note that it is actually safe to use this function while the
|
|
* controller is in the process of being shut down (as ssh_rtl_submit
|
|
* is safe with regards to this), but it is generally discouraged to do
|
|
* so.
|
|
*/
|
|
if (WARN_ON(READ_ONCE(ctrl->state) != SSAM_CONTROLLER_STARTED)) {
|
|
ssh_request_put(&rqst->base);
|
|
return -ENODEV;
|
|
}
|
|
|
|
status = ssh_rtl_submit(&ctrl->rtl, &rqst->base);
|
|
ssh_request_put(&rqst->base);
|
|
|
|
return status;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_request_sync_submit);
|
|
|
|
/**
|
|
* ssam_request_sync() - Execute a synchronous request.
|
|
* @ctrl: The controller via which the request will be submitted.
|
|
* @spec: The request specification and payload.
|
|
* @rsp: The response buffer.
|
|
*
|
|
* Allocates a synchronous request with its message data buffer on the heap
|
|
* via ssam_request_sync_alloc(), fully initializes it via the provided
|
|
* request specification, submits it, and finally waits for its completion
|
|
* before freeing it and returning its status.
|
|
*
|
|
* Return: Returns the status of the request or any failure during setup.
|
|
*/
|
|
int ssam_request_sync(struct ssam_controller *ctrl,
|
|
const struct ssam_request *spec,
|
|
struct ssam_response *rsp)
|
|
{
|
|
struct ssam_request_sync *rqst;
|
|
struct ssam_span buf;
|
|
ssize_t len;
|
|
int status;
|
|
|
|
status = ssam_request_sync_alloc(spec->length, GFP_KERNEL, &rqst, &buf);
|
|
if (status)
|
|
return status;
|
|
|
|
status = ssam_request_sync_init(rqst, spec->flags);
|
|
if (status)
|
|
return status;
|
|
|
|
ssam_request_sync_set_resp(rqst, rsp);
|
|
|
|
len = ssam_request_write_data(&buf, ctrl, spec);
|
|
if (len < 0) {
|
|
ssam_request_sync_free(rqst);
|
|
return len;
|
|
}
|
|
|
|
ssam_request_sync_set_data(rqst, buf.ptr, len);
|
|
|
|
status = ssam_request_sync_submit(ctrl, rqst);
|
|
if (!status)
|
|
status = ssam_request_sync_wait(rqst);
|
|
|
|
ssam_request_sync_free(rqst);
|
|
return status;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_request_sync);
|
|
|
|
/**
|
|
* ssam_request_sync_with_buffer() - Execute a synchronous request with the
|
|
* provided buffer as back-end for the message buffer.
|
|
* @ctrl: The controller via which the request will be submitted.
|
|
* @spec: The request specification and payload.
|
|
* @rsp: The response buffer.
|
|
* @buf: The buffer for the request message data.
|
|
*
|
|
* Allocates a synchronous request struct on the stack, fully initializes it
|
|
* using the provided buffer as message data buffer, submits it, and then
|
|
* waits for its completion before returning its status. The
|
|
* SSH_COMMAND_MESSAGE_LENGTH() macro can be used to compute the required
|
|
* message buffer size.
|
|
*
|
|
* This function does essentially the same as ssam_request_sync(), but instead
|
|
* of dynamically allocating the request and message data buffer, it uses the
|
|
* provided message data buffer and stores the (small) request struct on the
|
|
* heap.
|
|
*
|
|
* Return: Returns the status of the request or any failure during setup.
|
|
*/
|
|
int ssam_request_sync_with_buffer(struct ssam_controller *ctrl,
|
|
const struct ssam_request *spec,
|
|
struct ssam_response *rsp,
|
|
struct ssam_span *buf)
|
|
{
|
|
struct ssam_request_sync rqst;
|
|
ssize_t len;
|
|
int status;
|
|
|
|
status = ssam_request_sync_init(&rqst, spec->flags);
|
|
if (status)
|
|
return status;
|
|
|
|
ssam_request_sync_set_resp(&rqst, rsp);
|
|
|
|
len = ssam_request_write_data(buf, ctrl, spec);
|
|
if (len < 0)
|
|
return len;
|
|
|
|
ssam_request_sync_set_data(&rqst, buf->ptr, len);
|
|
|
|
status = ssam_request_sync_submit(ctrl, &rqst);
|
|
if (!status)
|
|
status = ssam_request_sync_wait(&rqst);
|
|
|
|
return status;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_request_sync_with_buffer);
|
|
|
|
|
|
/* -- Internal SAM requests. ------------------------------------------------ */
|
|
|
|
SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_get_firmware_version, __le32, {
|
|
.target_category = SSAM_SSH_TC_SAM,
|
|
.target_id = 0x01,
|
|
.command_id = 0x13,
|
|
.instance_id = 0x00,
|
|
});
|
|
|
|
SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_off, u8, {
|
|
.target_category = SSAM_SSH_TC_SAM,
|
|
.target_id = 0x01,
|
|
.command_id = 0x15,
|
|
.instance_id = 0x00,
|
|
});
|
|
|
|
SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_display_on, u8, {
|
|
.target_category = SSAM_SSH_TC_SAM,
|
|
.target_id = 0x01,
|
|
.command_id = 0x16,
|
|
.instance_id = 0x00,
|
|
});
|
|
|
|
SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_exit, u8, {
|
|
.target_category = SSAM_SSH_TC_SAM,
|
|
.target_id = 0x01,
|
|
.command_id = 0x33,
|
|
.instance_id = 0x00,
|
|
});
|
|
|
|
SSAM_DEFINE_SYNC_REQUEST_R(ssam_ssh_notif_d0_entry, u8, {
|
|
.target_category = SSAM_SSH_TC_SAM,
|
|
.target_id = 0x01,
|
|
.command_id = 0x34,
|
|
.instance_id = 0x00,
|
|
});
|
|
|
|
/**
|
|
* struct ssh_notification_params - Command payload to enable/disable SSH
|
|
* notifications.
|
|
* @target_category: The target category for which notifications should be
|
|
* enabled/disabled.
|
|
* @flags: Flags determining how notifications are being sent.
|
|
* @request_id: The request ID that is used to send these notifications.
|
|
* @instance_id: The specific instance in the given target category for
|
|
* which notifications should be enabled.
|
|
*/
|
|
struct ssh_notification_params {
|
|
u8 target_category;
|
|
u8 flags;
|
|
__le16 request_id;
|
|
u8 instance_id;
|
|
} __packed;
|
|
|
|
static_assert(sizeof(struct ssh_notification_params) == 5);
|
|
|
|
static int __ssam_ssh_event_request(struct ssam_controller *ctrl,
|
|
struct ssam_event_registry reg, u8 cid,
|
|
struct ssam_event_id id, u8 flags)
|
|
{
|
|
struct ssh_notification_params params;
|
|
struct ssam_request rqst;
|
|
struct ssam_response result;
|
|
int status;
|
|
|
|
u16 rqid = ssh_tc_to_rqid(id.target_category);
|
|
u8 buf = 0;
|
|
|
|
/* Only allow RQIDs that lie within the event spectrum. */
|
|
if (!ssh_rqid_is_event(rqid))
|
|
return -EINVAL;
|
|
|
|
params.target_category = id.target_category;
|
|
params.instance_id = id.instance;
|
|
params.flags = flags;
|
|
put_unaligned_le16(rqid, ¶ms.request_id);
|
|
|
|
rqst.target_category = reg.target_category;
|
|
rqst.target_id = reg.target_id;
|
|
rqst.command_id = cid;
|
|
rqst.instance_id = 0x00;
|
|
rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
|
|
rqst.length = sizeof(params);
|
|
rqst.payload = (u8 *)¶ms;
|
|
|
|
result.capacity = sizeof(buf);
|
|
result.length = 0;
|
|
result.pointer = &buf;
|
|
|
|
status = ssam_retry(ssam_request_sync_onstack, ctrl, &rqst, &result,
|
|
sizeof(params));
|
|
|
|
return status < 0 ? status : buf;
|
|
}
|
|
|
|
/**
|
|
* ssam_ssh_event_enable() - Enable SSH event.
|
|
* @ctrl: The controller for which to enable the event.
|
|
* @reg: The event registry describing what request to use for enabling and
|
|
* disabling the event.
|
|
* @id: The event identifier.
|
|
* @flags: The event flags.
|
|
*
|
|
* Enables the specified event on the EC. This function does not manage
|
|
* reference counting of enabled events and is basically only a wrapper for
|
|
* the raw EC request. If the specified event is already enabled, the EC will
|
|
* ignore this request.
|
|
*
|
|
* Return: Returns the status of the executed SAM request (zero on success and
|
|
* negative on direct failure) or %-EPROTO if the request response indicates a
|
|
* failure.
|
|
*/
|
|
static int ssam_ssh_event_enable(struct ssam_controller *ctrl,
|
|
struct ssam_event_registry reg,
|
|
struct ssam_event_id id, u8 flags)
|
|
{
|
|
int status;
|
|
|
|
status = __ssam_ssh_event_request(ctrl, reg, reg.cid_enable, id, flags);
|
|
|
|
if (status < 0 && status != -EINVAL) {
|
|
ssam_err(ctrl,
|
|
"failed to enable event source (tc: %#04x, iid: %#04x, reg: %#04x)\n",
|
|
id.target_category, id.instance, reg.target_category);
|
|
}
|
|
|
|
if (status > 0) {
|
|
ssam_err(ctrl,
|
|
"unexpected result while enabling event source: %#04x (tc: %#04x, iid: %#04x, reg: %#04x)\n",
|
|
status, id.target_category, id.instance, reg.target_category);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* ssam_ssh_event_disable() - Disable SSH event.
|
|
* @ctrl: The controller for which to disable the event.
|
|
* @reg: The event registry describing what request to use for enabling and
|
|
* disabling the event (must be same as used when enabling the event).
|
|
* @id: The event identifier.
|
|
* @flags: The event flags (likely ignored for disabling of events).
|
|
*
|
|
* Disables the specified event on the EC. This function does not manage
|
|
* reference counting of enabled events and is basically only a wrapper for
|
|
* the raw EC request. If the specified event is already disabled, the EC will
|
|
* ignore this request.
|
|
*
|
|
* Return: Returns the status of the executed SAM request (zero on success and
|
|
* negative on direct failure) or %-EPROTO if the request response indicates a
|
|
* failure.
|
|
*/
|
|
static int ssam_ssh_event_disable(struct ssam_controller *ctrl,
|
|
struct ssam_event_registry reg,
|
|
struct ssam_event_id id, u8 flags)
|
|
{
|
|
int status;
|
|
|
|
status = __ssam_ssh_event_request(ctrl, reg, reg.cid_disable, id, flags);
|
|
|
|
if (status < 0 && status != -EINVAL) {
|
|
ssam_err(ctrl,
|
|
"failed to disable event source (tc: %#04x, iid: %#04x, reg: %#04x)\n",
|
|
id.target_category, id.instance, reg.target_category);
|
|
}
|
|
|
|
if (status > 0) {
|
|
ssam_err(ctrl,
|
|
"unexpected result while disabling event source: %#04x (tc: %#04x, iid: %#04x, reg: %#04x)\n",
|
|
status, id.target_category, id.instance, reg.target_category);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
/* -- Wrappers for internal SAM requests. ----------------------------------- */
|
|
|
|
/**
|
|
* ssam_get_firmware_version() - Get the SAM/EC firmware version.
|
|
* @ctrl: The controller.
|
|
* @version: Where to store the version number.
|
|
*
|
|
* Return: Returns zero on success or the status of the executed SAM request
|
|
* if that request failed.
|
|
*/
|
|
int ssam_get_firmware_version(struct ssam_controller *ctrl, u32 *version)
|
|
{
|
|
__le32 __version;
|
|
int status;
|
|
|
|
status = ssam_retry(ssam_ssh_get_firmware_version, ctrl, &__version);
|
|
if (status)
|
|
return status;
|
|
|
|
*version = le32_to_cpu(__version);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_ctrl_notif_display_off() - Notify EC that the display has been turned
|
|
* off.
|
|
* @ctrl: The controller.
|
|
*
|
|
* Notify the EC that the display has been turned off and the driver may enter
|
|
* a lower-power state. This will prevent events from being sent directly.
|
|
* Rather, the EC signals an event by pulling the wakeup GPIO high for as long
|
|
* as there are pending events. The events then need to be manually released,
|
|
* one by one, via the GPIO callback request. All pending events accumulated
|
|
* during this state can also be released by issuing the display-on
|
|
* notification, e.g. via ssam_ctrl_notif_display_on(), which will also reset
|
|
* the GPIO.
|
|
*
|
|
* On some devices, specifically ones with an integrated keyboard, the keyboard
|
|
* backlight will be turned off by this call.
|
|
*
|
|
* This function will only send the display-off notification command if
|
|
* display notifications are supported by the EC. Currently all known devices
|
|
* support these notifications.
|
|
*
|
|
* Use ssam_ctrl_notif_display_on() to reverse the effects of this function.
|
|
*
|
|
* Return: Returns zero on success or if no request has been executed, the
|
|
* status of the executed SAM request if that request failed, or %-EPROTO if
|
|
* an unexpected response has been received.
|
|
*/
|
|
int ssam_ctrl_notif_display_off(struct ssam_controller *ctrl)
|
|
{
|
|
int status;
|
|
u8 response;
|
|
|
|
ssam_dbg(ctrl, "pm: notifying display off\n");
|
|
|
|
status = ssam_retry(ssam_ssh_notif_display_off, ctrl, &response);
|
|
if (status)
|
|
return status;
|
|
|
|
if (response != 0) {
|
|
ssam_err(ctrl, "unexpected response from display-off notification: %#04x\n",
|
|
response);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_ctrl_notif_display_on() - Notify EC that the display has been turned on.
|
|
* @ctrl: The controller.
|
|
*
|
|
* Notify the EC that the display has been turned back on and the driver has
|
|
* exited its lower-power state. This notification is the counterpart to the
|
|
* display-off notification sent via ssam_ctrl_notif_display_off() and will
|
|
* reverse its effects, including resetting events to their default behavior.
|
|
*
|
|
* This function will only send the display-on notification command if display
|
|
* notifications are supported by the EC. Currently all known devices support
|
|
* these notifications.
|
|
*
|
|
* See ssam_ctrl_notif_display_off() for more details.
|
|
*
|
|
* Return: Returns zero on success or if no request has been executed, the
|
|
* status of the executed SAM request if that request failed, or %-EPROTO if
|
|
* an unexpected response has been received.
|
|
*/
|
|
int ssam_ctrl_notif_display_on(struct ssam_controller *ctrl)
|
|
{
|
|
int status;
|
|
u8 response;
|
|
|
|
ssam_dbg(ctrl, "pm: notifying display on\n");
|
|
|
|
status = ssam_retry(ssam_ssh_notif_display_on, ctrl, &response);
|
|
if (status)
|
|
return status;
|
|
|
|
if (response != 0) {
|
|
ssam_err(ctrl, "unexpected response from display-on notification: %#04x\n",
|
|
response);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_ctrl_notif_d0_exit() - Notify EC that the driver/device exits the D0
|
|
* power state.
|
|
* @ctrl: The controller
|
|
*
|
|
* Notifies the EC that the driver prepares to exit the D0 power state in
|
|
* favor of a lower-power state. Exact effects of this function related to the
|
|
* EC are currently unknown.
|
|
*
|
|
* This function will only send the D0-exit notification command if D0-state
|
|
* notifications are supported by the EC. Only newer Surface generations
|
|
* support these notifications.
|
|
*
|
|
* Use ssam_ctrl_notif_d0_entry() to reverse the effects of this function.
|
|
*
|
|
* Return: Returns zero on success or if no request has been executed, the
|
|
* status of the executed SAM request if that request failed, or %-EPROTO if
|
|
* an unexpected response has been received.
|
|
*/
|
|
int ssam_ctrl_notif_d0_exit(struct ssam_controller *ctrl)
|
|
{
|
|
int status;
|
|
u8 response;
|
|
|
|
if (!ctrl->caps.d3_closes_handle)
|
|
return 0;
|
|
|
|
ssam_dbg(ctrl, "pm: notifying D0 exit\n");
|
|
|
|
status = ssam_retry(ssam_ssh_notif_d0_exit, ctrl, &response);
|
|
if (status)
|
|
return status;
|
|
|
|
if (response != 0) {
|
|
ssam_err(ctrl, "unexpected response from D0-exit notification: %#04x\n",
|
|
response);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_ctrl_notif_d0_entry() - Notify EC that the driver/device enters the D0
|
|
* power state.
|
|
* @ctrl: The controller
|
|
*
|
|
* Notifies the EC that the driver has exited a lower-power state and entered
|
|
* the D0 power state. Exact effects of this function related to the EC are
|
|
* currently unknown.
|
|
*
|
|
* This function will only send the D0-entry notification command if D0-state
|
|
* notifications are supported by the EC. Only newer Surface generations
|
|
* support these notifications.
|
|
*
|
|
* See ssam_ctrl_notif_d0_exit() for more details.
|
|
*
|
|
* Return: Returns zero on success or if no request has been executed, the
|
|
* status of the executed SAM request if that request failed, or %-EPROTO if
|
|
* an unexpected response has been received.
|
|
*/
|
|
int ssam_ctrl_notif_d0_entry(struct ssam_controller *ctrl)
|
|
{
|
|
int status;
|
|
u8 response;
|
|
|
|
if (!ctrl->caps.d3_closes_handle)
|
|
return 0;
|
|
|
|
ssam_dbg(ctrl, "pm: notifying D0 entry\n");
|
|
|
|
status = ssam_retry(ssam_ssh_notif_d0_entry, ctrl, &response);
|
|
if (status)
|
|
return status;
|
|
|
|
if (response != 0) {
|
|
ssam_err(ctrl, "unexpected response from D0-entry notification: %#04x\n",
|
|
response);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* -- Top-level event registry interface. ----------------------------------- */
|
|
|
|
/**
|
|
* ssam_notifier_register() - Register an event notifier.
|
|
* @ctrl: The controller to register the notifier on.
|
|
* @n: The event notifier to register.
|
|
*
|
|
* Register an event notifier and increment the usage counter of the
|
|
* associated SAM event. If the event was previously not enabled, it will be
|
|
* enabled during this call.
|
|
*
|
|
* Return: Returns zero on success, %-ENOSPC if there have already been
|
|
* %INT_MAX notifiers for the event ID/type associated with the notifier block
|
|
* registered, %-ENOMEM if the corresponding event entry could not be
|
|
* allocated. If this is the first time that a notifier block is registered
|
|
* for the specific associated event, returns the status of the event-enable
|
|
* EC-command.
|
|
*/
|
|
int ssam_notifier_register(struct ssam_controller *ctrl,
|
|
struct ssam_event_notifier *n)
|
|
{
|
|
u16 rqid = ssh_tc_to_rqid(n->event.id.target_category);
|
|
struct ssam_nf_refcount_entry *entry;
|
|
struct ssam_nf_head *nf_head;
|
|
struct ssam_nf *nf;
|
|
int status;
|
|
|
|
if (!ssh_rqid_is_event(rqid))
|
|
return -EINVAL;
|
|
|
|
nf = &ctrl->cplt.event.notif;
|
|
nf_head = &nf->head[ssh_rqid_to_event(rqid)];
|
|
|
|
mutex_lock(&nf->lock);
|
|
|
|
entry = ssam_nf_refcount_inc(nf, n->event.reg, n->event.id);
|
|
if (IS_ERR(entry)) {
|
|
mutex_unlock(&nf->lock);
|
|
return PTR_ERR(entry);
|
|
}
|
|
|
|
ssam_dbg(ctrl, "enabling event (reg: %#04x, tc: %#04x, iid: %#04x, rc: %d)\n",
|
|
n->event.reg.target_category, n->event.id.target_category,
|
|
n->event.id.instance, entry->refcount);
|
|
|
|
status = ssam_nfblk_insert(nf_head, &n->base);
|
|
if (status) {
|
|
entry = ssam_nf_refcount_dec(nf, n->event.reg, n->event.id);
|
|
if (entry->refcount == 0)
|
|
kfree(entry);
|
|
|
|
mutex_unlock(&nf->lock);
|
|
return status;
|
|
}
|
|
|
|
if (entry->refcount == 1) {
|
|
status = ssam_ssh_event_enable(ctrl, n->event.reg, n->event.id,
|
|
n->event.flags);
|
|
if (status) {
|
|
ssam_nfblk_remove(&n->base);
|
|
kfree(ssam_nf_refcount_dec(nf, n->event.reg, n->event.id));
|
|
mutex_unlock(&nf->lock);
|
|
synchronize_srcu(&nf_head->srcu);
|
|
return status;
|
|
}
|
|
|
|
entry->flags = n->event.flags;
|
|
|
|
} else if (entry->flags != n->event.flags) {
|
|
ssam_warn(ctrl,
|
|
"inconsistent flags when enabling event: got %#04x, expected %#04x (reg: %#04x, tc: %#04x, iid: %#04x)\n",
|
|
n->event.flags, entry->flags, n->event.reg.target_category,
|
|
n->event.id.target_category, n->event.id.instance);
|
|
}
|
|
|
|
mutex_unlock(&nf->lock);
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_notifier_register);
|
|
|
|
/**
|
|
* ssam_notifier_unregister() - Unregister an event notifier.
|
|
* @ctrl: The controller the notifier has been registered on.
|
|
* @n: The event notifier to unregister.
|
|
*
|
|
* Unregister an event notifier and decrement the usage counter of the
|
|
* associated SAM event. If the usage counter reaches zero, the event will be
|
|
* disabled.
|
|
*
|
|
* Return: Returns zero on success, %-ENOENT if the given notifier block has
|
|
* not been registered on the controller. If the given notifier block was the
|
|
* last one associated with its specific event, returns the status of the
|
|
* event-disable EC-command.
|
|
*/
|
|
int ssam_notifier_unregister(struct ssam_controller *ctrl,
|
|
struct ssam_event_notifier *n)
|
|
{
|
|
u16 rqid = ssh_tc_to_rqid(n->event.id.target_category);
|
|
struct ssam_nf_refcount_entry *entry;
|
|
struct ssam_nf_head *nf_head;
|
|
struct ssam_nf *nf;
|
|
int status = 0;
|
|
|
|
if (!ssh_rqid_is_event(rqid))
|
|
return -EINVAL;
|
|
|
|
nf = &ctrl->cplt.event.notif;
|
|
nf_head = &nf->head[ssh_rqid_to_event(rqid)];
|
|
|
|
mutex_lock(&nf->lock);
|
|
|
|
if (!ssam_nfblk_find(nf_head, &n->base)) {
|
|
mutex_unlock(&nf->lock);
|
|
return -ENOENT;
|
|
}
|
|
|
|
entry = ssam_nf_refcount_dec(nf, n->event.reg, n->event.id);
|
|
if (WARN_ON(!entry)) {
|
|
/*
|
|
* If this does not return an entry, there's a logic error
|
|
* somewhere: The notifier block is registered, but the event
|
|
* refcount entry is not there. Remove the notifier block
|
|
* anyways.
|
|
*/
|
|
status = -ENOENT;
|
|
goto remove;
|
|
}
|
|
|
|
ssam_dbg(ctrl, "disabling event (reg: %#04x, tc: %#04x, iid: %#04x, rc: %d)\n",
|
|
n->event.reg.target_category, n->event.id.target_category,
|
|
n->event.id.instance, entry->refcount);
|
|
|
|
if (entry->flags != n->event.flags) {
|
|
ssam_warn(ctrl,
|
|
"inconsistent flags when disabling event: got %#04x, expected %#04x (reg: %#04x, tc: %#04x, iid: %#04x)\n",
|
|
n->event.flags, entry->flags, n->event.reg.target_category,
|
|
n->event.id.target_category, n->event.id.instance);
|
|
}
|
|
|
|
if (entry->refcount == 0) {
|
|
status = ssam_ssh_event_disable(ctrl, n->event.reg, n->event.id,
|
|
n->event.flags);
|
|
kfree(entry);
|
|
}
|
|
|
|
remove:
|
|
ssam_nfblk_remove(&n->base);
|
|
mutex_unlock(&nf->lock);
|
|
synchronize_srcu(&nf_head->srcu);
|
|
|
|
return status;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ssam_notifier_unregister);
|
|
|
|
/**
|
|
* ssam_notifier_disable_registered() - Disable events for all registered
|
|
* notifiers.
|
|
* @ctrl: The controller for which to disable the notifiers/events.
|
|
*
|
|
* Disables events for all currently registered notifiers. In case of an error
|
|
* (EC command failing), all previously disabled events will be restored and
|
|
* the error code returned.
|
|
*
|
|
* This function is intended to disable all events prior to hibernation entry.
|
|
* See ssam_notifier_restore_registered() to restore/re-enable all events
|
|
* disabled with this function.
|
|
*
|
|
* Note that this function will not disable events for notifiers registered
|
|
* after calling this function. It should thus be made sure that no new
|
|
* notifiers are going to be added after this call and before the corresponding
|
|
* call to ssam_notifier_restore_registered().
|
|
*
|
|
* Return: Returns zero on success. In case of failure returns the error code
|
|
* returned by the failed EC command to disable an event.
|
|
*/
|
|
int ssam_notifier_disable_registered(struct ssam_controller *ctrl)
|
|
{
|
|
struct ssam_nf *nf = &ctrl->cplt.event.notif;
|
|
struct rb_node *n;
|
|
int status;
|
|
|
|
mutex_lock(&nf->lock);
|
|
for (n = rb_first(&nf->refcount); n; n = rb_next(n)) {
|
|
struct ssam_nf_refcount_entry *e;
|
|
|
|
e = rb_entry(n, struct ssam_nf_refcount_entry, node);
|
|
status = ssam_ssh_event_disable(ctrl, e->key.reg,
|
|
e->key.id, e->flags);
|
|
if (status)
|
|
goto err;
|
|
}
|
|
mutex_unlock(&nf->lock);
|
|
|
|
return 0;
|
|
|
|
err:
|
|
for (n = rb_prev(n); n; n = rb_prev(n)) {
|
|
struct ssam_nf_refcount_entry *e;
|
|
|
|
e = rb_entry(n, struct ssam_nf_refcount_entry, node);
|
|
ssam_ssh_event_enable(ctrl, e->key.reg, e->key.id, e->flags);
|
|
}
|
|
mutex_unlock(&nf->lock);
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* ssam_notifier_restore_registered() - Restore/re-enable events for all
|
|
* registered notifiers.
|
|
* @ctrl: The controller for which to restore the notifiers/events.
|
|
*
|
|
* Restores/re-enables all events for which notifiers have been registered on
|
|
* the given controller. In case of a failure, the error is logged and the
|
|
* function continues to try and enable the remaining events.
|
|
*
|
|
* This function is intended to restore/re-enable all registered events after
|
|
* hibernation. See ssam_notifier_disable_registered() for the counter part
|
|
* disabling the events and more details.
|
|
*/
|
|
void ssam_notifier_restore_registered(struct ssam_controller *ctrl)
|
|
{
|
|
struct ssam_nf *nf = &ctrl->cplt.event.notif;
|
|
struct rb_node *n;
|
|
|
|
mutex_lock(&nf->lock);
|
|
for (n = rb_first(&nf->refcount); n; n = rb_next(n)) {
|
|
struct ssam_nf_refcount_entry *e;
|
|
|
|
e = rb_entry(n, struct ssam_nf_refcount_entry, node);
|
|
|
|
/* Ignore errors, will get logged in call. */
|
|
ssam_ssh_event_enable(ctrl, e->key.reg, e->key.id, e->flags);
|
|
}
|
|
mutex_unlock(&nf->lock);
|
|
}
|
|
|
|
/**
|
|
* ssam_notifier_is_empty() - Check if there are any registered notifiers.
|
|
* @ctrl: The controller to check on.
|
|
*
|
|
* Return: Returns %true if there are currently no notifiers registered on the
|
|
* controller, %false otherwise.
|
|
*/
|
|
static bool ssam_notifier_is_empty(struct ssam_controller *ctrl)
|
|
{
|
|
struct ssam_nf *nf = &ctrl->cplt.event.notif;
|
|
bool result;
|
|
|
|
mutex_lock(&nf->lock);
|
|
result = ssam_nf_refcount_empty(nf);
|
|
mutex_unlock(&nf->lock);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* ssam_notifier_unregister_all() - Unregister all currently registered
|
|
* notifiers.
|
|
* @ctrl: The controller to unregister the notifiers on.
|
|
*
|
|
* Unregisters all currently registered notifiers. This function is used to
|
|
* ensure that all notifiers will be unregistered and associated
|
|
* entries/resources freed when the controller is being shut down.
|
|
*/
|
|
static void ssam_notifier_unregister_all(struct ssam_controller *ctrl)
|
|
{
|
|
struct ssam_nf *nf = &ctrl->cplt.event.notif;
|
|
struct ssam_nf_refcount_entry *e, *n;
|
|
|
|
mutex_lock(&nf->lock);
|
|
rbtree_postorder_for_each_entry_safe(e, n, &nf->refcount, node) {
|
|
/* Ignore errors, will get logged in call. */
|
|
ssam_ssh_event_disable(ctrl, e->key.reg, e->key.id, e->flags);
|
|
kfree(e);
|
|
}
|
|
nf->refcount = RB_ROOT;
|
|
mutex_unlock(&nf->lock);
|
|
}
|
|
|
|
|
|
/* -- Wakeup IRQ. ----------------------------------------------------------- */
|
|
|
|
static irqreturn_t ssam_irq_handle(int irq, void *dev_id)
|
|
{
|
|
struct ssam_controller *ctrl = dev_id;
|
|
|
|
ssam_dbg(ctrl, "pm: wake irq triggered\n");
|
|
|
|
/*
|
|
* Note: Proper wakeup detection is currently unimplemented.
|
|
* When the EC is in display-off or any other non-D0 state, it
|
|
* does not send events/notifications to the host. Instead it
|
|
* signals that there are events available via the wakeup IRQ.
|
|
* This driver is responsible for calling back to the EC to
|
|
* release these events one-by-one.
|
|
*
|
|
* This IRQ should not cause a full system resume by its own.
|
|
* Instead, events should be handled by their respective subsystem
|
|
* drivers, which in turn should signal whether a full system
|
|
* resume should be performed.
|
|
*
|
|
* TODO: Send GPIO callback command repeatedly to EC until callback
|
|
* returns 0x00. Return flag of callback is "has more events".
|
|
* Each time the command is sent, one event is "released". Once
|
|
* all events have been released (return = 0x00), the GPIO is
|
|
* re-armed. Detect wakeup events during this process, go back to
|
|
* sleep if no wakeup event has been received.
|
|
*/
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
/**
|
|
* ssam_irq_setup() - Set up SAM EC wakeup-GPIO interrupt.
|
|
* @ctrl: The controller for which the IRQ should be set up.
|
|
*
|
|
* Set up an IRQ for the wakeup-GPIO pin of the SAM EC. This IRQ can be used
|
|
* to wake the device from a low power state.
|
|
*
|
|
* Note that this IRQ can only be triggered while the EC is in the display-off
|
|
* state. In this state, events are not sent to the host in the usual way.
|
|
* Instead the wakeup-GPIO gets pulled to "high" as long as there are pending
|
|
* events and these events need to be released one-by-one via the GPIO
|
|
* callback request, either until there are no events left and the GPIO is
|
|
* reset, or all at once by transitioning the EC out of the display-off state,
|
|
* which will also clear the GPIO.
|
|
*
|
|
* Not all events, however, should trigger a full system wakeup. Instead the
|
|
* driver should, if necessary, inspect and forward each event to the
|
|
* corresponding subsystem, which in turn should decide if the system needs to
|
|
* be woken up. This logic has not been implemented yet, thus wakeup by this
|
|
* IRQ should be disabled by default to avoid spurious wake-ups, caused, for
|
|
* example, by the remaining battery percentage changing. Refer to comments in
|
|
* this function and comments in the corresponding IRQ handler for more
|
|
* details on how this should be implemented.
|
|
*
|
|
* See also ssam_ctrl_notif_display_off() and ssam_ctrl_notif_display_off()
|
|
* for functions to transition the EC into and out of the display-off state as
|
|
* well as more details on it.
|
|
*
|
|
* The IRQ is disabled by default and has to be enabled before it can wake up
|
|
* the device from suspend via ssam_irq_arm_for_wakeup(). On teardown, the IRQ
|
|
* should be freed via ssam_irq_free().
|
|
*/
|
|
int ssam_irq_setup(struct ssam_controller *ctrl)
|
|
{
|
|
struct device *dev = ssam_controller_device(ctrl);
|
|
struct gpio_desc *gpiod;
|
|
int irq;
|
|
int status;
|
|
|
|
/*
|
|
* The actual GPIO interrupt is declared in ACPI as TRIGGER_HIGH.
|
|
* However, the GPIO line only gets reset by sending the GPIO callback
|
|
* command to SAM (or alternatively the display-on notification). As
|
|
* proper handling for this interrupt is not implemented yet, leaving
|
|
* the IRQ at TRIGGER_HIGH would cause an IRQ storm (as the callback
|
|
* never gets sent and thus the line never gets reset). To avoid this,
|
|
* mark the IRQ as TRIGGER_RISING for now, only creating a single
|
|
* interrupt, and let the SAM resume callback during the controller
|
|
* resume process clear it.
|
|
*/
|
|
const int irqf = IRQF_ONESHOT | IRQF_TRIGGER_RISING | IRQF_NO_AUTOEN;
|
|
|
|
gpiod = gpiod_get(dev, "ssam_wakeup-int", GPIOD_ASIS);
|
|
if (IS_ERR(gpiod))
|
|
return PTR_ERR(gpiod);
|
|
|
|
irq = gpiod_to_irq(gpiod);
|
|
gpiod_put(gpiod);
|
|
|
|
if (irq < 0)
|
|
return irq;
|
|
|
|
status = request_threaded_irq(irq, NULL, ssam_irq_handle, irqf,
|
|
"ssam_wakeup", ctrl);
|
|
if (status)
|
|
return status;
|
|
|
|
ctrl->irq.num = irq;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_irq_free() - Free SAM EC wakeup-GPIO interrupt.
|
|
* @ctrl: The controller for which the IRQ should be freed.
|
|
*
|
|
* Free the wakeup-GPIO IRQ previously set-up via ssam_irq_setup().
|
|
*/
|
|
void ssam_irq_free(struct ssam_controller *ctrl)
|
|
{
|
|
free_irq(ctrl->irq.num, ctrl);
|
|
ctrl->irq.num = -1;
|
|
}
|
|
|
|
/**
|
|
* ssam_irq_arm_for_wakeup() - Arm the EC IRQ for wakeup, if enabled.
|
|
* @ctrl: The controller for which the IRQ should be armed.
|
|
*
|
|
* Sets up the IRQ so that it can be used to wake the device. Specifically,
|
|
* this function enables the irq and then, if the device is allowed to wake up
|
|
* the system, calls enable_irq_wake(). See ssam_irq_disarm_wakeup() for the
|
|
* corresponding function to disable the IRQ.
|
|
*
|
|
* This function is intended to arm the IRQ before entering S2idle suspend.
|
|
*
|
|
* Note: calls to ssam_irq_arm_for_wakeup() and ssam_irq_disarm_wakeup() must
|
|
* be balanced.
|
|
*/
|
|
int ssam_irq_arm_for_wakeup(struct ssam_controller *ctrl)
|
|
{
|
|
struct device *dev = ssam_controller_device(ctrl);
|
|
int status;
|
|
|
|
enable_irq(ctrl->irq.num);
|
|
if (device_may_wakeup(dev)) {
|
|
status = enable_irq_wake(ctrl->irq.num);
|
|
if (status) {
|
|
ssam_err(ctrl, "failed to enable wake IRQ: %d\n", status);
|
|
disable_irq(ctrl->irq.num);
|
|
return status;
|
|
}
|
|
|
|
ctrl->irq.wakeup_enabled = true;
|
|
} else {
|
|
ctrl->irq.wakeup_enabled = false;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* ssam_irq_disarm_wakeup() - Disarm the wakeup IRQ.
|
|
* @ctrl: The controller for which the IRQ should be disarmed.
|
|
*
|
|
* Disarm the IRQ previously set up for wake via ssam_irq_arm_for_wakeup().
|
|
*
|
|
* This function is intended to disarm the IRQ after exiting S2idle suspend.
|
|
*
|
|
* Note: calls to ssam_irq_arm_for_wakeup() and ssam_irq_disarm_wakeup() must
|
|
* be balanced.
|
|
*/
|
|
void ssam_irq_disarm_wakeup(struct ssam_controller *ctrl)
|
|
{
|
|
int status;
|
|
|
|
if (ctrl->irq.wakeup_enabled) {
|
|
status = disable_irq_wake(ctrl->irq.num);
|
|
if (status)
|
|
ssam_err(ctrl, "failed to disable wake IRQ: %d\n", status);
|
|
|
|
ctrl->irq.wakeup_enabled = false;
|
|
}
|
|
disable_irq(ctrl->irq.num);
|
|
}
|