/* 
 | 
 * 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> 
 | 
#if defined(CONFIG_ROCKCHIP_SIP) 
 | 
#include <linux/rockchip/rockchip_sip.h> 
 | 
#endif 
 | 
  
 | 
/* 
 | 
 * 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; 
 | 
}; 
 | 
  
 | 
static DEFINE_RAW_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 bool suspend_abort; 
 | 
static bool abnormal_wake; 
 | 
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; 
 | 
  
 | 
    raw_spin_lock_irqsave(&wakeup_reason_lock, flags); 
 | 
  
 | 
    if (!capture_reasons) { 
 | 
        raw_spin_unlock_irqrestore(&wakeup_reason_lock, flags); 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    if (find_node_in_list(&parent_irqs, irq) == NULL) 
 | 
        add_sibling_node_sorted(&leaf_irqs, irq); 
 | 
  
 | 
    raw_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; 
 | 
  
 | 
    raw_spin_lock_irqsave(&wakeup_reason_lock, flags); 
 | 
  
 | 
    if (!capture_reasons || (find_node_in_list(&leaf_irqs, irq) != NULL)) { 
 | 
        raw_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); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    raw_spin_unlock_irqrestore(&wakeup_reason_lock, flags); 
 | 
} 
 | 
  
 | 
static void __log_abort_or_abnormal_wake(bool abort, const char *fmt, 
 | 
                     va_list args) 
 | 
{ 
 | 
    unsigned long flags; 
 | 
  
 | 
    raw_spin_lock_irqsave(&wakeup_reason_lock, flags); 
 | 
  
 | 
    /* Suspend abort or abnormal wake reason has already been logged. */ 
 | 
    if (suspend_abort || abnormal_wake) { 
 | 
        raw_spin_unlock_irqrestore(&wakeup_reason_lock, flags); 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    suspend_abort = abort; 
 | 
    abnormal_wake = !abort; 
 | 
    vsnprintf(non_irq_wake_reason, MAX_SUSPEND_ABORT_LEN, fmt, args); 
 | 
  
 | 
    raw_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); 
 | 
} 
 | 
  
 | 
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); 
 | 
} 
 | 
  
 | 
void clear_wakeup_reasons(void) 
 | 
{ 
 | 
    unsigned long flags; 
 | 
  
 | 
    raw_spin_lock_irqsave(&wakeup_reason_lock, flags); 
 | 
  
 | 
    delete_list(&leaf_irqs); 
 | 
    delete_list(&parent_irqs); 
 | 
    suspend_abort = false; 
 | 
    abnormal_wake = false; 
 | 
    capture_reasons = true; 
 | 
  
 | 
    raw_spin_unlock_irqrestore(&wakeup_reason_lock, flags); 
 | 
} 
 | 
  
 | 
static void print_wakeup_sources(void) 
 | 
{ 
 | 
    struct wakeup_irq_node *n; 
 | 
    unsigned long flags; 
 | 
  
 | 
    raw_spin_lock_irqsave(&wakeup_reason_lock, flags); 
 | 
  
 | 
    capture_reasons = false; 
 | 
  
 | 
    if (suspend_abort) { 
 | 
        pr_info("Abort: %s\n", non_irq_wake_reason); 
 | 
        raw_spin_unlock_irqrestore(&wakeup_reason_lock, flags); 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    if (!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 (abnormal_wake) 
 | 
        pr_info("Resume caused by %s\n", non_irq_wake_reason); 
 | 
    else 
 | 
        pr_info("Resume cause unknown\n"); 
 | 
  
 | 
    raw_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; 
 | 
  
 | 
    raw_spin_lock_irqsave(&wakeup_reason_lock, flags); 
 | 
  
 | 
    if (suspend_abort) { 
 | 
        buf_offset = scnprintf(buf, PAGE_SIZE, "Abort: %s", 
 | 
                       non_irq_wake_reason); 
 | 
        raw_spin_unlock_irqrestore(&wakeup_reason_lock, flags); 
 | 
        return buf_offset; 
 | 
    } 
 | 
  
 | 
    if (!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 (abnormal_wake) 
 | 
        buf_offset = scnprintf(buf, PAGE_SIZE, "-1 %s", 
 | 
                       non_irq_wake_reason); 
 | 
  
 | 
    raw_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); 
 | 
} 
 | 
  
 | 
#if defined(CONFIG_ROCKCHIP_SIP) 
 | 
static ssize_t total_suspend_wfi_time_show(struct kobject *kobj, 
 | 
                       struct kobj_attribute *attr, 
 | 
                       char *buf) 
 | 
{ 
 | 
    struct arm_smccc_res res; 
 | 
    unsigned long wfi_time_ms; 
 | 
  
 | 
    res = sip_smc_get_suspend_info(SUSPEND_WFI_TIME_MS); 
 | 
    if (res.a0) 
 | 
        wfi_time_ms = 0; 
 | 
    else 
 | 
        wfi_time_ms = res.a1; 
 | 
  
 | 
    return sprintf(buf, "%lu.%02lu\n", wfi_time_ms / MSEC_PER_SEC, 
 | 
               (wfi_time_ms % MSEC_PER_SEC) / 10); 
 | 
} 
 | 
#endif 
 | 
  
 | 
static struct kobj_attribute resume_reason = __ATTR_RO(last_resume_reason); 
 | 
static struct kobj_attribute suspend_time = __ATTR_RO(last_suspend_time); 
 | 
#if defined(CONFIG_ROCKCHIP_SIP) 
 | 
static struct kobj_attribute suspend_wfi_time = __ATTR_RO(total_suspend_wfi_time); 
 | 
#endif 
 | 
  
 | 
static struct attribute *attrs[] = { 
 | 
    &resume_reason.attr, 
 | 
    &suspend_time.attr, 
 | 
#if defined(CONFIG_ROCKCHIP_SIP) 
 | 
    &suspend_wfi_time.attr, 
 | 
#endif 
 | 
    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); 
 |