// SPDX-License-Identifier: GPL-2.0 | |
/* | |
* PM functions for ASR SoC | |
* Copyright (C) 2019 ASR Micro Limited | |
*/ | |
#include <asm/io.h> | |
#include <soc/asr/pm.h> | |
#include <linux/irq.h> | |
#include <asm/compiler.h> | |
#include <linux/arm-smccc.h> | |
#include <linux/kernel.h> | |
#include <linux/pm_qos.h> | |
#include <linux/irq.h> | |
#include <linux/interrupt.h> | |
#include <linux/platform_device.h> | |
#include <linux/mod_devicetable.h> | |
#include <linux/of.h> | |
#include <linux/slab.h> | |
#include <linux/uaccess.h> | |
#include <linux/debugfs.h> | |
#include <linux/regmap.h> | |
#include <linux/mfd/syscon.h> | |
#include <linux/asr_tee_sip.h> | |
#include <linux/irqchip/mmp.h> | |
#include <linux/irqchip/arm-gic.h> | |
#ifdef CONFIG_SMP | |
#include <asm/smp.h> | |
#endif | |
#include <soc/asr/wakeup_defines.h> | |
#include <soc/asr/asrdcstat.h> | |
#include <soc/asr/regs-apmu.h> | |
#include <trace/events/pxa.h> | |
static unsigned int edgew_rer[4]; | |
static void __iomem *mfpr_base; | |
enum { | |
GPIO_ADD, | |
GPIO_REMOVE, | |
}; | |
typedef void (*edge_handler)(int, void *); | |
/* | |
* struct edge_wakeup_desc - edge wakeup source descriptor, used for drivers | |
* whose pin is used to wakeup system. | |
* @list: list control of the descriptors | |
* @gpio: the gpio number of the wakeup source | |
* @dev: the device that gpio attaches to | |
* @data: optional, any kind private data passed to the handler | |
* @handler: optional, the handler for the certain wakeup source detected | |
*/ | |
struct edge_wakeup_desc { | |
struct list_head list; | |
int gpio; | |
struct device *dev; | |
void *data; | |
edge_handler handler; | |
}; | |
struct edge_wakeup { | |
struct list_head list; | |
spinlock_t lock; | |
int num; | |
void __iomem *base; | |
}; | |
static struct edge_wakeup *info; | |
int mfp_edge_wakeup_cb(unsigned long val, int gpio) | |
{ | |
int error = 0; | |
switch (val) { | |
case GPIO_ADD: | |
error = asr_gpio_edge_detect_add(gpio); | |
break; | |
case GPIO_REMOVE: | |
error = asr_gpio_edge_detect_remove(gpio);; | |
break; | |
default: | |
panic("Wrong LC command for GPIO edge wakeup!"); | |
} | |
if (error) | |
pr_warn("GPIO %d %s failed!\n", gpio, (val == GPIO_ADD) ? "add" : "remove"); | |
return error; | |
} | |
int asr_set_wake(struct irq_data *data, unsigned int on) | |
{ | |
int irq = (int)data->hwirq; | |
struct irq_desc *desc = irq_to_desc(data->irq); | |
if (!desc) { | |
pr_err("irq_desc is NULL\n"); | |
return -EINVAL; | |
} | |
if (on) { | |
if (desc->action) | |
desc->action->flags |= IRQF_NO_SUSPEND; | |
} else { | |
if (desc->action) | |
desc->action->flags &= ~IRQF_NO_SUSPEND; | |
} | |
asr_irq_wake_set(irq, on); | |
return 0; | |
} | |
int extern_set_rtc_wkup_disabled(bool flag) | |
{ | |
asr_irq_wake_set((128 + 6), flag); | |
pr_info("rtc_no_wakeup set to: %d\n", flag); | |
return 0; | |
} | |
int extern_set_gpio_wkup_disabled(bool disable, int gpio_num) | |
{ | |
if (gpio_num < 0 || gpio_num > 127) { | |
pr_err("%s error gpio num: %d!\n", __func__, gpio_num); | |
return -ENODEV; | |
} | |
if (disable) | |
asr_gpio_edge_detect_disable(gpio_num); | |
else | |
asr_gpio_edge_detect_enable(gpio_num); | |
return 0; | |
} | |
static void __iomem * get_gpio_vaddr(int gpio) | |
{ | |
#if defined(CONFIG_CPU_ASR18XX) | |
if (gpio <= 54) | |
return mfpr_base + 0xDC + (gpio) * 4; | |
#elif defined(CONFIG_CPU_ASR1901) | |
if (gpio <= 25) | |
return mfpr_base + 0x4 + ((gpio) << 2); | |
else if ((gpio >= 26) && (gpio <= 91)) | |
return mfpr_base + 0x20C + ((gpio-26) << 2); | |
#endif | |
panic("GPIO number %d doesn't exist!\n", gpio); | |
} | |
static struct pm_wakeup_status asr_wkup_sts; | |
void asr_clear_wakeup_event_idx(void) | |
{ | |
asr_wkup_sts.main_wakeup_idx = asr_wkup_sts.gpio_wakeup_idx = asr_wkup_sts.irq_wakeup_idx = 0; | |
} | |
int asr_get_main_wakeup_count(void) | |
{ | |
return asr_wkup_sts.main_wakeup_idx; | |
} | |
u32 asr_get_main_wakeup_event(int idx) | |
{ | |
if (idx < asr_wkup_sts.main_wakeup_idx) { | |
return asr_wkup_sts.sys_main_wakeup_id[idx]; | |
} else { | |
pr_err("%s: error main wakeup idx %d\n", __func__, idx); | |
return 0; | |
} | |
} | |
int asr_get_gpio_wakeup_count(void) | |
{ | |
return asr_wkup_sts.gpio_wakeup_idx; | |
} | |
u32 asr_get_gpio_wakeup_event(int idx) | |
{ | |
if (idx < asr_wkup_sts.gpio_wakeup_idx) { | |
return asr_wkup_sts.sys_gpio_wakeup_id[idx]; | |
} else { | |
pr_err("%s: error gpio wakeup idx %d\n", __func__, idx); | |
return 0; | |
} | |
} | |
int asr_get_irq_wakeup_count(void) | |
{ | |
return asr_wkup_sts.irq_wakeup_idx; | |
} | |
u32 asr_get_irq_wakeup_event(int idx) | |
{ | |
if (idx < asr_wkup_sts.irq_wakeup_idx) { | |
return asr_wkup_sts.sys_irq_wakeup_id[idx]; | |
} else { | |
pr_err("%s: error irq wakeup idx %d\n", __func__, idx); | |
return 0; | |
} | |
} | |
/* MFPR bits */ | |
#define EDGE_CLEAR (1 << 6) | |
#define EDGE_FALL_EN (1 << 5) | |
#define EDGE_RISE_EN (1 << 4) | |
static inline irqreturn_t edge_wakeup_handler(int irq, void *dev_id) | |
{ | |
struct edge_wakeup_desc *e; | |
unsigned int i; | |
void __iomem *addr; | |
unsigned int val; | |
for (i = 0; i < (info->num / 32); i++) { | |
edgew_rer[i] = readl_relaxed(info->base + i * 4); | |
} | |
list_for_each_entry(e, &info->list, list) { | |
if (test_and_clear_bit(e->gpio, (unsigned long *)edgew_rer)) { | |
if (e->handler) | |
e->handler(e->gpio, e->data); | |
addr = get_gpio_vaddr(e->gpio); | |
val = readl_relaxed(addr); | |
val |= EDGE_CLEAR; | |
val &= ~(EDGE_FALL_EN | EDGE_RISE_EN); | |
writel_relaxed(val, addr); | |
} | |
} | |
return IRQ_HANDLED; | |
} | |
/* | |
* mmp_request/remove_edge_wakeup is called by common device driver. | |
* | |
* Drivers use it to set one or several pins as wakeup sources in deep low | |
* power modes. | |
*/ | |
int request_mfp_edge_wakeup(int gpio, edge_handler handler, void *data, struct device *dev) | |
{ | |
struct edge_wakeup_desc *desc, *e; | |
unsigned long flags; | |
if (dev == NULL) { | |
pr_err("error: edge wakeup: unknown device!\n"); | |
return -EINVAL; | |
} | |
if (gpio < 0 || gpio > info->num) { | |
pr_err("error: edge wakeup: add invalid gpio num %d!\n", gpio); | |
return -EINVAL; | |
} | |
desc = kzalloc(sizeof(struct edge_wakeup_desc), GFP_KERNEL); | |
if (!desc) | |
return -ENOMEM; | |
desc->gpio = gpio; | |
desc->dev = dev; | |
desc->data = data; | |
desc->handler = handler; | |
spin_lock_irqsave(&info->lock, flags); | |
list_for_each_entry(e, &info->list, list) { | |
if (e->gpio == gpio) { | |
dev_err(dev, "Adding exist gpio%d to edge wakeup!\n", desc->gpio); | |
spin_unlock_irqrestore(&info->lock, flags); | |
kfree(desc); | |
return -EEXIST; | |
} | |
} | |
list_add(&desc->list, &info->list); | |
spin_unlock_irqrestore(&info->lock, flags); | |
return mfp_edge_wakeup_cb(GPIO_ADD, gpio); | |
} | |
int remove_mfp_edge_wakeup(int gpio) | |
{ | |
struct edge_wakeup_desc *desc, *tmp; | |
unsigned long flags; | |
if (gpio < 0 || gpio > info->num) { | |
pr_err("error: edge wakeup: remove invalid gpio num %d!\n", gpio); | |
return -EINVAL; | |
} | |
spin_lock_irqsave(&info->lock, flags); | |
list_for_each_entry_safe(desc, tmp, &info->list, list) { | |
if (desc->gpio == gpio) { | |
list_del(&desc->list); | |
kfree(desc); | |
break; | |
} | |
} | |
spin_unlock_irqrestore(&info->lock, flags); | |
return mfp_edge_wakeup_cb(GPIO_REMOVE, gpio); | |
} | |
static int edge_wakeup_mfp_probe(struct platform_device *pdev) | |
{ | |
struct resource *res; | |
unsigned int irq; | |
info = devm_kzalloc(&pdev->dev, sizeof(struct edge_wakeup), GFP_KERNEL); | |
if (!info) | |
return -ENOMEM; | |
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0); | |
if (!res) { | |
panic("failed to get irq resource\n"); | |
} | |
irq = res->start; | |
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
if (!res) { | |
panic("failed to get mem resource\n"); | |
} | |
info->base = devm_ioremap_resource(&pdev->dev, res); | |
info->num = resource_size(res) * 8; | |
res = platform_get_resource(pdev, IORESOURCE_MEM, 1); | |
if (!res) | |
panic("failed to get mem resource\n"); | |
mfpr_base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); | |
if (!mfpr_base) | |
panic("failed to map registers\n"); | |
spin_lock_init(&info->lock); | |
INIT_LIST_HEAD(&info->list); | |
platform_set_drvdata(pdev, info); | |
if (request_irq(irq, edge_wakeup_handler, IRQF_NO_SUSPEND, "edge irq", NULL)) | |
panic("failed to enable edge wakeup irq!\n"); | |
return 0; | |
} | |
#ifdef CONFIG_CPU_ASR1901 | |
void asr1901_gic_raise_softirq(const struct cpumask *mask, unsigned int irq) | |
{ | |
unsigned int val = 0; | |
int targ_cpu; | |
/* We shouldn't access any registers when AXI time out occurred */ | |
if (keep_silent) | |
return; | |
/* | |
* Set the wakeup bits to make sure the core(s) can respond to | |
* the IPI interrupt. | |
* If the target core(s) is alive, this operation is ignored by | |
* the APMU. After the core wakes up, these corresponding bits | |
* are clearly automatically by PMU hardware. | |
*/ | |
preempt_disable(); | |
for_each_cpu(targ_cpu, mask) { | |
BUG_ON(targ_cpu >= CONFIG_NR_CPUS); | |
val |= APMU_WAKEUP_CORE(targ_cpu); | |
} | |
__raw_writel(val, APMU_COREn_WAKEUP_CTL(smp_processor_id())); | |
preempt_enable(); | |
} | |
#endif | |
static void find_deepest_state(int *index) | |
{ | |
*index = min(*index, pm_qos_request(PM_QOS_CPUIDLE_BLOCK) - 1); | |
} | |
#ifdef CONFIG_ASR_CLK_DCSTAT | |
static int idle_stat[] = {LPM_C1, LPM_C2, LPM_D1P, LPM_D1}; | |
void asr_cpuidle_enter_dcstat(int index) | |
{ | |
int lpm_mode; | |
int state = index; | |
find_deepest_state(&state); | |
lpm_mode = idle_stat[state]; | |
if ( (lpm_mode >= LPM_C2) && (lpm_mode != LPM_D2_UDR) ) { | |
cpu_dcstat_event(cpu_dcstat_clk, 0, CPU_M2_OR_DEEPER_ENTER, | |
lpm_mode); | |
#ifdef CONFIG_ASR_DVFS | |
vol_dcstat_event(lpm_mode); | |
vol_ledstatus_event(lpm_mode); | |
#endif | |
} | |
cpu_dcstat_event(cpu_dcstat_clk, 0, CPU_IDLE_ENTER, lpm_mode); | |
trace_pxa_cpu_idle(LPM_ENTRY(lpm_mode), 0, 0); | |
} | |
void asr_cpuidle_exit_dcstat(void) | |
{ | |
cpu_dcstat_event(cpu_dcstat_clk, 0, CPU_IDLE_EXIT, MAX_LPM_INDEX); | |
#ifdef CONFIG_ASR_DVFS | |
vol_dcstat_event(MAX_LPM_INDEX); | |
vol_ledstatus_event(MAX_LPM_INDEX); | |
#endif | |
trace_pxa_cpu_idle(LPM_EXIT(0), 0, 0); | |
} | |
#endif | |
static int __init asr_pm_init(void) | |
{ | |
#ifndef CONFIG_CPU_ASR18XX | |
gic_arch_extn.irq_set_wake = asr_set_wake; | |
#else | |
icu_irq_chip.irq_set_wake = asr_set_wake; | |
#endif | |
asr_wake_status_init((u64)virt_to_phys(&asr_wkup_sts)); | |
return 0; | |
} | |
arch_initcall(asr_pm_init); | |
static struct of_device_id edge_wakeup_mfp_dt_ids[] = { | |
{ .compatible = "asr,edge-wakeup", }, | |
{} | |
}; | |
static struct platform_driver edge_wakeup_driver = { | |
.probe = edge_wakeup_mfp_probe, | |
.driver = { | |
.name = "asr-edge-wakeup", | |
.of_match_table = of_match_ptr(edge_wakeup_mfp_dt_ids), | |
}, | |
}; | |
static int __init edge_wakeup_driver_init(void) | |
{ | |
return platform_driver_register(&edge_wakeup_driver); | |
} | |
subsys_initcall(edge_wakeup_driver_init); |