Orange Pi5 kernel

Deprecated Linux kernel 5.10.110 for OrangePi 5/5B/5+ boards

3 Commits   0 Branches   0 Tags
/*
 * kernel/power/wakeup_reason.c
 *
 * Logs the reasons which caused the kernel to resume from
 * the suspend mode.
 *
 * Copyright (C) 2020 Google, Inc.
 * This software is licensed under the terms of the GNU General Public
 * License version 2, as published by the Free Software Foundation, and
 * may be copied, distributed, and modified under those terms.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <linux/wakeup_reason.h>
#include <linux/kernel.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/init.h>
#include <linux/spinlock.h>
#include <linux/notifier.h>
#include <linux/suspend.h>
#include <linux/slab.h>

/*
 * struct wakeup_irq_node - stores data and relationships for IRQs logged as
 * either base or nested wakeup reasons during suspend/resume flow.
 * @siblings - for membership on leaf or parent IRQ lists
 * @irq      - the IRQ number
 * @irq_name - the name associated with the IRQ, or a default if none
 */
struct wakeup_irq_node {
	struct list_head siblings;
	int irq;
	const char *irq_name;
};

enum wakeup_reason_flag {
	RESUME_NONE = 0,
	RESUME_IRQ,
	RESUME_ABORT,
	RESUME_ABNORMAL,
};

static DEFINE_SPINLOCK(wakeup_reason_lock);

static LIST_HEAD(leaf_irqs);   /* kept in ascending IRQ sorted order */
static LIST_HEAD(parent_irqs); /* unordered */

static struct kmem_cache *wakeup_irq_nodes_cache;

static const char *default_irq_name = "(unnamed)";

static struct kobject *kobj;

static bool capture_reasons;
static int wakeup_reason;
static char non_irq_wake_reason[MAX_SUSPEND_ABORT_LEN];

static ktime_t last_monotime; /* monotonic time before last suspend */
static ktime_t curr_monotime; /* monotonic time after last suspend */
static ktime_t last_stime; /* monotonic boottime offset before last suspend */
static ktime_t curr_stime; /* monotonic boottime offset after last suspend */

static void init_node(struct wakeup_irq_node *p, int irq)
{
	struct irq_desc *desc;

	INIT_LIST_HEAD(&p->siblings);

	p->irq = irq;
	desc = irq_to_desc(irq);
	if (desc && desc->action && desc->action->name)
		p->irq_name = desc->action->name;
	else
		p->irq_name = default_irq_name;
}

static struct wakeup_irq_node *create_node(int irq)
{
	struct wakeup_irq_node *result;

	result = kmem_cache_alloc(wakeup_irq_nodes_cache, GFP_ATOMIC);
	if (unlikely(!result))
		pr_warn("Failed to log wakeup IRQ %d\n", irq);
	else
		init_node(result, irq);

	return result;
}

static void delete_list(struct list_head *head)
{
	struct wakeup_irq_node *n;

	while (!list_empty(head)) {
		n = list_first_entry(head, struct wakeup_irq_node, siblings);
		list_del(&n->siblings);
		kmem_cache_free(wakeup_irq_nodes_cache, n);
	}
}

static bool add_sibling_node_sorted(struct list_head *head, int irq)
{
	struct wakeup_irq_node *n = NULL;
	struct list_head *predecessor = head;

	if (unlikely(WARN_ON(!head)))
		return NULL;

	if (!list_empty(head))
		list_for_each_entry(n, head, siblings) {
			if (n->irq < irq)
				predecessor = &n->siblings;
			else if (n->irq == irq)
				return true;
			else
				break;
		}

	n = create_node(irq);
	if (n) {
		list_add(&n->siblings, predecessor);
		return true;
	}

	return false;
}

static struct wakeup_irq_node *find_node_in_list(struct list_head *head,
						 int irq)
{
	struct wakeup_irq_node *n;

	if (unlikely(WARN_ON(!head)))
		return NULL;

	list_for_each_entry(n, head, siblings)
		if (n->irq == irq)
			return n;

	return NULL;
}

void log_irq_wakeup_reason(int irq)
{
	unsigned long flags;

	spin_lock_irqsave(&wakeup_reason_lock, flags);
	if (wakeup_reason == RESUME_ABNORMAL || wakeup_reason == RESUME_ABORT) {
		spin_unlock_irqrestore(&wakeup_reason_lock, flags);
		return;
	}

	if (!capture_reasons) {
		spin_unlock_irqrestore(&wakeup_reason_lock, flags);
		return;
	}

	if (find_node_in_list(&parent_irqs, irq) == NULL)
		add_sibling_node_sorted(&leaf_irqs, irq);

	wakeup_reason = RESUME_IRQ;
	spin_unlock_irqrestore(&wakeup_reason_lock, flags);
}

void log_threaded_irq_wakeup_reason(int irq, int parent_irq)
{
	struct wakeup_irq_node *parent;
	unsigned long flags;

	/*
	 * Intentionally unsynchronized.  Calls that come in after we have
	 * resumed should have a fast exit path since there's no work to be
	 * done, any any coherence issue that could cause a wrong value here is
	 * both highly improbable - given the set/clear timing - and very low
	 * impact (parent IRQ gets logged instead of the specific child).
	 */
	if (!capture_reasons)
		return;

	spin_lock_irqsave(&wakeup_reason_lock, flags);

	if (wakeup_reason == RESUME_ABNORMAL || wakeup_reason == RESUME_ABORT) {
		spin_unlock_irqrestore(&wakeup_reason_lock, flags);
		return;
	}

	if (!capture_reasons || (find_node_in_list(&leaf_irqs, irq) != NULL)) {
		spin_unlock_irqrestore(&wakeup_reason_lock, flags);
		return;
	}

	parent = find_node_in_list(&parent_irqs, parent_irq);
	if (parent != NULL)
		add_sibling_node_sorted(&leaf_irqs, irq);
	else {
		parent = find_node_in_list(&leaf_irqs, parent_irq);
		if (parent != NULL) {
			list_del_init(&parent->siblings);
			list_add_tail(&parent->siblings, &parent_irqs);
			add_sibling_node_sorted(&leaf_irqs, irq);
		}
	}

	spin_unlock_irqrestore(&wakeup_reason_lock, flags);
}
EXPORT_SYMBOL_GPL(log_threaded_irq_wakeup_reason);

static void __log_abort_or_abnormal_wake(bool abort, const char *fmt,
					 va_list args)
{
	unsigned long flags;

	spin_lock_irqsave(&wakeup_reason_lock, flags);

	/* Suspend abort or abnormal wake reason has already been logged. */
	if (wakeup_reason != RESUME_NONE) {
		spin_unlock_irqrestore(&wakeup_reason_lock, flags);
		return;
	}

	if (abort)
		wakeup_reason = RESUME_ABORT;
	else
		wakeup_reason = RESUME_ABNORMAL;

	vsnprintf(non_irq_wake_reason, MAX_SUSPEND_ABORT_LEN, fmt, args);

	spin_unlock_irqrestore(&wakeup_reason_lock, flags);
}

void log_suspend_abort_reason(const char *fmt, ...)
{
	va_list args;

	va_start(args, fmt);
	__log_abort_or_abnormal_wake(true, fmt, args);
	va_end(args);
}
EXPORT_SYMBOL_GPL(log_suspend_abort_reason);

void log_abnormal_wakeup_reason(const char *fmt, ...)
{
	va_list args;

	va_start(args, fmt);
	__log_abort_or_abnormal_wake(false, fmt, args);
	va_end(args);
}
EXPORT_SYMBOL_GPL(log_abnormal_wakeup_reason);

void clear_wakeup_reasons(void)
{
	unsigned long flags;

	spin_lock_irqsave(&wakeup_reason_lock, flags);

	delete_list(&leaf_irqs);
	delete_list(&parent_irqs);
	wakeup_reason = RESUME_NONE;
	capture_reasons = true;

	spin_unlock_irqrestore(&wakeup_reason_lock, flags);
}

static void print_wakeup_sources(void)
{
	struct wakeup_irq_node *n;
	unsigned long flags;

	spin_lock_irqsave(&wakeup_reason_lock, flags);

	capture_reasons = false;

	if (wakeup_reason == RESUME_ABORT) {
		pr_info("Abort: %s\n", non_irq_wake_reason);
		spin_unlock_irqrestore(&wakeup_reason_lock, flags);
		return;
	}

	if (wakeup_reason == RESUME_IRQ && !list_empty(&leaf_irqs))
		list_for_each_entry(n, &leaf_irqs, siblings)
			pr_info("Resume caused by IRQ %d, %s\n", n->irq,
				n->irq_name);
	else if (wakeup_reason == RESUME_ABNORMAL)
		pr_info("Resume caused by %s\n", non_irq_wake_reason);
	else
		pr_info("Resume cause unknown\n");

	spin_unlock_irqrestore(&wakeup_reason_lock, flags);
}

static ssize_t last_resume_reason_show(struct kobject *kobj,
				       struct kobj_attribute *attr, char *buf)
{
	ssize_t buf_offset = 0;
	struct wakeup_irq_node *n;
	unsigned long flags;

	spin_lock_irqsave(&wakeup_reason_lock, flags);

	if (wakeup_reason == RESUME_ABORT) {
		buf_offset = scnprintf(buf, PAGE_SIZE, "Abort: %s",
				       non_irq_wake_reason);
		spin_unlock_irqrestore(&wakeup_reason_lock, flags);
		return buf_offset;
	}

	if (wakeup_reason == RESUME_IRQ && !list_empty(&leaf_irqs))
		list_for_each_entry(n, &leaf_irqs, siblings)
			buf_offset += scnprintf(buf + buf_offset,
						PAGE_SIZE - buf_offset,
						"%d %s\n", n->irq, n->irq_name);
	else if (wakeup_reason == RESUME_ABNORMAL)
		buf_offset = scnprintf(buf, PAGE_SIZE, "-1 %s",
				       non_irq_wake_reason);

	spin_unlock_irqrestore(&wakeup_reason_lock, flags);

	return buf_offset;
}

static ssize_t last_suspend_time_show(struct kobject *kobj,
			struct kobj_attribute *attr, char *buf)
{
	struct timespec64 sleep_time;
	struct timespec64 total_time;
	struct timespec64 suspend_resume_time;

	/*
	 * total_time is calculated from monotonic bootoffsets because
	 * unlike CLOCK_MONOTONIC it include the time spent in suspend state.
	 */
	total_time = ktime_to_timespec64(ktime_sub(curr_stime, last_stime));

	/*
	 * suspend_resume_time is calculated as monotonic (CLOCK_MONOTONIC)
	 * time interval before entering suspend and post suspend.
	 */
	suspend_resume_time =
		ktime_to_timespec64(ktime_sub(curr_monotime, last_monotime));

	/* sleep_time = total_time - suspend_resume_time */
	sleep_time = timespec64_sub(total_time, suspend_resume_time);

	/* Export suspend_resume_time and sleep_time in pair here. */
	return sprintf(buf, "%llu.%09lu %llu.%09lu\n",
		       (unsigned long long)suspend_resume_time.tv_sec,
		       suspend_resume_time.tv_nsec,
		       (unsigned long long)sleep_time.tv_sec,
		       sleep_time.tv_nsec);
}

static struct kobj_attribute resume_reason = __ATTR_RO(last_resume_reason);
static struct kobj_attribute suspend_time = __ATTR_RO(last_suspend_time);

static struct attribute *attrs[] = {
	&resume_reason.attr,
	&suspend_time.attr,
	NULL,
};
static struct attribute_group attr_group = {
	.attrs = attrs,
};

/* Detects a suspend and clears all the previous wake up reasons*/
static int wakeup_reason_pm_event(struct notifier_block *notifier,
		unsigned long pm_event, void *unused)
{
	switch (pm_event) {
	case PM_SUSPEND_PREPARE:
		/* monotonic time since boot */
		last_monotime = ktime_get();
		/* monotonic time since boot including the time spent in suspend */
		last_stime = ktime_get_boottime();
		clear_wakeup_reasons();
		break;
	case PM_POST_SUSPEND:
		/* monotonic time since boot */
		curr_monotime = ktime_get();
		/* monotonic time since boot including the time spent in suspend */
		curr_stime = ktime_get_boottime();
		print_wakeup_sources();
		break;
	default:
		break;
	}
	return NOTIFY_DONE;
}

static struct notifier_block wakeup_reason_pm_notifier_block = {
	.notifier_call = wakeup_reason_pm_event,
};

static int __init wakeup_reason_init(void)
{
	if (register_pm_notifier(&wakeup_reason_pm_notifier_block)) {
		pr_warn("[%s] failed to register PM notifier\n", __func__);
		goto fail;
	}

	kobj = kobject_create_and_add("wakeup_reasons", kernel_kobj);
	if (!kobj) {
		pr_warn("[%s] failed to create a sysfs kobject\n", __func__);
		goto fail_unregister_pm_notifier;
	}

	if (sysfs_create_group(kobj, &attr_group)) {
		pr_warn("[%s] failed to create a sysfs group\n", __func__);
		goto fail_kobject_put;
	}

	wakeup_irq_nodes_cache =
		kmem_cache_create("wakeup_irq_node_cache",
				  sizeof(struct wakeup_irq_node), 0, 0, NULL);
	if (!wakeup_irq_nodes_cache)
		goto fail_remove_group;

	return 0;

fail_remove_group:
	sysfs_remove_group(kobj, &attr_group);
fail_kobject_put:
	kobject_put(kobj);
fail_unregister_pm_notifier:
	unregister_pm_notifier(&wakeup_reason_pm_notifier_block);
fail:
	return 1;
}

late_initcall(wakeup_reason_init);