blob: 373481666483ac4beb11927ea5f5089f0fa6b515 [file] [log] [blame]
/* sound/soc/samsung/lpass.c
*
* Low Power Audio SubSystem driver for Samsung Exynos
*
* Copyright (c) 2013 Samsung Electronics Co. Ltd.
* Yeongman Seo <yman.seo@samsung.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/clk.h>
#include <linux/io.h>
#include <linux/err.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/pm_runtime.h>
#include <linux/pm_qos.h>
#include <linux/fb.h>
#include <linux/iommu.h>
#include <linux/exynos_iovmm.h>
#include <linux/dma-mapping.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/sched.h>
#include <linux/sched/rt.h>
#include <linux/cpu.h>
#include <linux/kthread.h>
#include <linux/mfd/syscon.h>
#include <linux/regmap.h>
#include <asm/tlbflush.h>
#include <sound/exynos.h>
#include <soc/samsung/exynos-pm.h>
#if 0
#include <mach/map.h>
#include <mach/regs-pmu.h>
#include <mach/cpufreq.h>
#endif
#include "lpass.h"
#ifdef USE_EXYNOS_AUD_SCHED
#define AUD_TASK_CPU_UHQ (5)
#ifdef CONFIG_SOC_EXYNOS5422
#define USE_AUD_TASK_RT
#endif
#endif
#ifdef CONFIG_PM_DEVFREQ
#define USE_AUD_DEVFREQ
#ifdef CONFIG_SOC_EXYNOS5422
#define AUD_CPU_FREQ_UHQA (1000000)
#define AUD_KFC_FREQ_UHQA (1300000)
#define AUD_MIF_FREQ_UHQA (413000)
#define AUD_INT_FREQ_UHQA (0)
#define AUD_CPU_FREQ_HIGH (0)
#define AUD_KFC_FREQ_HIGH (1300000)
#define AUD_MIF_FREQ_HIGH (0)
#define AUD_INT_FREQ_HIGH (0)
#define AUD_CPU_FREQ_NORM (0)
#define AUD_KFC_FREQ_NORM (0)
#define AUD_MIF_FREQ_NORM (0)
#define AUD_INT_FREQ_NORM (0)
#elif defined(CONFIG_SOC_EXYNOS5430)
#define AUD_CPU_FREQ_UHQA (1000000)
#define AUD_KFC_FREQ_UHQA (1300000)
#define AUD_MIF_FREQ_UHQA (413000)
#define AUD_INT_FREQ_UHQA (0)
#define AUD_CPU_FREQ_HIGH (0)
#define AUD_KFC_FREQ_HIGH (600000)
#define AUD_MIF_FREQ_HIGH (0)
#define AUD_INT_FREQ_HIGH (0)
#define AUD_CPU_FREQ_NORM (0)
#define AUD_KFC_FREQ_NORM (0)
#define AUD_MIF_FREQ_NORM (0)
#define AUD_INT_FREQ_NORM (0)
#elif defined(CONFIG_SOC_EXYNOS5433)
#define AUD_CPU_FREQ_UHQA (1000000)
#define AUD_KFC_FREQ_UHQA (1300000)
#define AUD_MIF_FREQ_UHQA (413000)
#define AUD_INT_FREQ_UHQA (0)
#define AUD_CPU_FREQ_HIGH (0)
#define AUD_KFC_FREQ_HIGH (500000)
#define AUD_MIF_FREQ_HIGH (0)
#define AUD_INT_FREQ_HIGH (0)
#define AUD_CPU_FREQ_NORM (0)
#define AUD_KFC_FREQ_NORM (0)
#define AUD_MIF_FREQ_NORM (0)
#define AUD_INT_FREQ_NORM (0)
#elif defined(CONFIG_SOC_EXYNOS7420)
#define AUD_CPU_FREQ_UHQA (1000000)
#define AUD_KFC_FREQ_UHQA (1300000)
#define AUD_MIF_FREQ_UHQA (413000)
#define AUD_INT_FREQ_UHQA (0)
#define AUD_CPU_FREQ_HIGH (0)
#define AUD_KFC_FREQ_HIGH (0)
#define AUD_MIF_FREQ_HIGH (416000)
#define AUD_INT_FREQ_HIGH (0)
#define AUD_CPU_FREQ_NORM (0)
#define AUD_KFC_FREQ_NORM (0)
#define AUD_MIF_FREQ_NORM (0)
#define AUD_INT_FREQ_NORM (0)
#else
#define AUD_CPU_FREQ_UHQA (1000000)
#define AUD_KFC_FREQ_UHQA (1300000)
#define AUD_MIF_FREQ_UHQA (413000)
#define AUD_INT_FREQ_UHQA (0)
#define AUD_CPU_FREQ_HIGH (0)
#define AUD_KFC_FREQ_HIGH (500000)
#define AUD_MIF_FREQ_HIGH (0)
#define AUD_INT_FREQ_HIGH (0)
#define AUD_CPU_FREQ_NORM (0)
#define AUD_KFC_FREQ_NORM (0)
#define AUD_MIF_FREQ_NORM (0)
#define AUD_INT_FREQ_NORM (0)
#endif
#endif
/* Default interrupt mask */
#define INTR_CA5_MASK_VAL (LPASS_INTR_SFR)
#define INTR_CPU_MASK_VAL (LPASS_INTR_I2S | \
LPASS_INTR_PCM | LPASS_INTR_SB | \
LPASS_INTR_UART | LPASS_INTR_SFR)
#define INTR_CPU_DMA_VAL (LPASS_INTR_DMA)
#define EXYNOS_PMU_PMU_DEBUG_OFFSET 0x0A00
#define EXYNOS_GPIO_MODE_AUD_SYS_PWR_REG_OFFSET 0x1340
#define EXYNOS_PAD_RETENTION_AUD_OPTION_OFFSET 0x3028
#ifdef CONFIG_SOC_EXYNOS8890
#define SRAM_BASE 0x3000000
#define SRAM_SIZE 0x24000
#endif
/* Audio subsystem version */
enum {
LPASS_VER_000100 = 0, /* pega/carmen */
LPASS_VER_010100, /* hudson */
LPASS_VER_100100 = 16, /* gaia/adonis */
LPASS_VER_110100, /* ares */
LPASS_VER_270120, /* insel-d */
LPASS_VER_370100 = 32, /* rhea/helsinki */
LPASS_VER_370200, /* helsinki prime */
LPASS_VER_370210, /* istor */
LPASS_VER_MAX
};
static struct lpass_info lpass;
struct aud_reg {
void __iomem *reg;
u32 val;
struct list_head node;
};
struct subip_info {
struct device *dev;
const char *name;
void (*cb)(struct device *dev);
atomic_t use_cnt;
struct list_head node;
};
#ifdef CONFIG_SND_SAMSUNG_SEIREN_OFFLOAD
DEFINE_MUTEX(lpass_mutex);
unsigned int cpu_lock[14] =
{0, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000, 1104000, 1200000, 1296000, 1400000};
void lpass_set_cpu_lock(int level)
{
mutex_lock(&lpass_mutex);
#ifdef USE_AUD_DEVFREQ
pm_qos_update_request(&lpass.aud_cluster0_qos, cpu_lock[level]);
#endif
mutex_unlock(&lpass_mutex);
}
#endif
static LIST_HEAD(reg_list);
static LIST_HEAD(subip_list);
extern int check_adma_status(void);
extern int check_fdma_status(void);
extern int check_esa_status(void);
extern int check_eax_dma_status(void);
#ifdef CONFIG_SND_SAMSUNG_SEIREN_OFFLOAD
extern void esa_compr_alpa_notifier(bool on);
extern int check_esa_compr_state(void);
#endif
static void lpass_update_qos(void);
static bool cp_available;
static atomic_t dram_usage_cnt;
void lpass_disable_mif_status(bool on)
{
u32 val = on ? 1 << 2 : 0x0;
writel(val, lpass.regs + LPASS_MIF_POWER);
}
void lpass_inc_dram_usage_count(void)
{
atomic_inc(&dram_usage_cnt);
}
void lpass_dec_dram_usage_count(void)
{
atomic_dec(&dram_usage_cnt);
}
int lpass_get_dram_usage_count(void)
{
return atomic_read(&dram_usage_cnt);
}
bool lpass_i2s_master_mode(void)
{
return lpass.i2s_master_mode;
}
void update_cp_available(bool cpen)
{
cp_available = cpen;
}
bool is_cp_aud_enabled(void)
{
return cp_available;
}
EXPORT_SYMBOL(is_cp_aud_enabled);
/**
* LPASS version for Exynos7580 is 0x270120. The operation of this LPASS is
* closer to the LPASS versions 0x370100 and above, hence it is grouped with
* them.
*/
static inline bool is_old_ass(void)
{
return lpass.ver < LPASS_VER_270120 ? true : false;
}
/**
* LPASS version for Exynos7580 is 0x270120. This LPASS version doesn't support
* CA5, timer and interrupt clocks. Hence keeping this out of this class in
* these cases and the operations are protected by this check.
*/
static inline bool is_new_ass(void)
{
return lpass.ver >= LPASS_VER_370100 ? true : false;
}
static inline bool is_running_only(const char *name)
{
struct subip_info *si;
if (atomic_read(&lpass.use_cnt) != 1)
return false;
list_for_each_entry(si, &subip_list, node) {
if (atomic_read(&si->use_cnt) > 0 &&
!strncmp(name, si->name, strlen(si->name)))
return true;
}
return false;
}
int exynos_check_aud_pwr(void)
{
int dram_used = check_adma_status();
#ifdef CONFIG_SND_SAMSUNG_FAKEDMA
dram_used |= check_fdma_status();
#endif
#if defined(CONFIG_SND_SAMSUNG_SEIREN) || defined(CONFIG_SND_SAMSUNG_SEIREN_OFFLOAD)
dram_used |= check_esa_status();
#endif
dram_used |= check_eax_dma_status();
if (!lpass.enabled)
return AUD_PWR_SLEEP;
else if (is_running_only("aud-uart"))
return AUD_PWR_LPC;
else if (!dram_used)
return AUD_PWR_LPA;
if (is_new_ass())
return AUD_PWR_ALPA;
else
return AUD_PWR_AFTR;
}
void lpass_update_lpclock(u32 ctrlid, bool idle)
{
lpass_update_lpclock_impl(&lpass.pdev->dev, ctrlid, idle);
}
void __iomem *lpass_get_regs(void)
{
return lpass.regs;
}
void __iomem *lpass_get_regs_s(void)
{
return lpass.regs_s;
}
void __iomem *lpass_get_mem(void)
{
return lpass.mem;
}
struct iommu_domain *lpass_get_iommu_domain(void)
{
return lpass.domain;
}
void lpass_set_dma_intr(bool on)
{
u32 cfg = readl(lpass.regs + LPASS_INTR_CPU_MASK);
if (on)
cfg |= INTR_CPU_DMA_VAL;
else
cfg &= ~(INTR_CPU_DMA_VAL);
writel(cfg, lpass.regs + LPASS_INTR_CPU_MASK);
}
void lpass_dma_enable(bool on)
{
spin_lock(&lpass.lock);
if (on) {
atomic_inc(&lpass.dma_use_cnt);
if (atomic_read(&lpass.dma_use_cnt) == 1)
lpass_set_dma_intr(true);
} else {
atomic_dec(&lpass.dma_use_cnt);
if (atomic_read(&lpass.dma_use_cnt) == 0)
lpass_set_dma_intr(false);
}
spin_unlock(&lpass.lock);
}
void ass_reset(int ip, int op)
{
spin_lock(&lpass.lock);
spin_unlock(&lpass.lock);
}
void lpass_reset(int ip, int op)
{
u32 reg, val;
u32 bit = 0;
void __iomem *regs;
if (is_old_ass()) {
ass_reset(ip, op);
return;
}
spin_lock(&lpass.lock);
regs = lpass.regs;
reg = LPASS_CORE_SW_RESET;
switch (ip) {
case LPASS_IP_DMA:
bit = LPASS_SW_RESET_DMA;
break;
case LPASS_IP_MEM:
bit = LPASS_SW_RESET_MEM;
break;
case LPASS_IP_TIMER:
if (is_new_ass())
bit = LPASS_SW_RESET_TIMER;
break;
case LPASS_IP_I2S:
bit = LPASS_SW_RESET_I2S;
break;
case LPASS_IP_PCM:
bit = LPASS_SW_RESET_PCM;
break;
case LPASS_IP_UART:
bit = LPASS_SW_RESET_UART;
break;
case LPASS_IP_SLIMBUS:
if (is_new_ass())
bit = LPASS_SW_RESET_SB;
break;
case LPASS_IP_CA5:
if (is_new_ass()) {
regs = (lpass.ver >= LPASS_VER_370200) ?
lpass.regs_s : regs;
reg = LPASS_CA5_SW_RESET;
bit = LPASS_SW_RESET_CA5;
}
break;
default:
spin_unlock(&lpass.lock);
pr_err("%s: wrong ip type %d!\n", __func__, ip);
return;
}
val = readl(regs + reg);
switch (op) {
case LPASS_OP_RESET:
val &= ~bit;
break;
case LPASS_OP_NORMAL:
val |= bit;
break;
default:
spin_unlock(&lpass.lock);
pr_err("%s: wrong op type %d!\n", __func__, op);
return;
}
writel(val, regs + reg);
spin_unlock(&lpass.lock);
}
void lpass_reset_toggle(int ip)
{
pr_debug("%s: %d\n", __func__, ip);
lpass_reset(ip, LPASS_OP_RESET);
udelay(100);
lpass_reset(ip, LPASS_OP_NORMAL);
}
int lpass_register_subip(struct device *ip_dev, const char *ip_name)
{
struct device *dev = &lpass.pdev->dev;
struct subip_info *si;
si = devm_kzalloc(dev, sizeof(struct subip_info), GFP_KERNEL);
if (!si)
return -1;
si->dev = ip_dev;
si->name = ip_name;
si->cb = NULL;
atomic_set(&si->use_cnt, 0);
list_add(&si->node, &subip_list);
pr_info("%s: %s(%p) registered\n", __func__, ip_name, ip_dev);
return 0;
}
int lpass_set_gpio_cb(struct device *ip_dev, void (*ip_cb)(struct device *dev))
{
struct subip_info *si;
list_for_each_entry(si, &subip_list, node) {
if (si->dev == ip_dev) {
si->cb = ip_cb;
pr_info("%s: %s(cb: %p)\n", __func__,
si->name, si->cb);
return 0;
}
}
return -EINVAL;
}
void lpass_get_sync(struct device *ip_dev)
{
struct subip_info *si;
list_for_each_entry(si, &subip_list, node) {
if (si->dev == ip_dev) {
atomic_inc(&si->use_cnt);
atomic_inc(&lpass.use_cnt);
pr_info("%s: %s (use:%d)\n", __func__,
si->name, atomic_read(&si->use_cnt));
pm_runtime_get_sync(&lpass.pdev->dev);
}
}
lpass_update_qos();
}
void lpass_put_sync(struct device *ip_dev)
{
struct subip_info *si;
list_for_each_entry(si, &subip_list, node) {
if (si->dev == ip_dev) {
atomic_dec(&si->use_cnt);
atomic_dec(&lpass.use_cnt);
pr_info("%s: %s (use:%d)\n", __func__,
si->name, atomic_read(&si->use_cnt));
pm_runtime_put_sync(&lpass.pdev->dev);
}
}
lpass_update_qos();
}
#ifdef USE_EXYNOS_AUD_SCHED
void lpass_set_sched(pid_t pid, int mode)
{
#ifdef USE_AUD_TASK_RT
struct sched_param param_fifo = {.sched_priority = MAX_RT_PRIO >> 1};
struct task_struct *task = find_task_by_vpid(pid);
#endif
switch (mode) {
case AUD_MODE_UHQA:
lpass.uhqa_on = true;
break;
case AUD_MODE_NORM:
lpass.uhqa_on = false;
break;
default:
break;
}
lpass_update_qos();
#ifdef USE_AUD_TASK_RT
if (task) {
sched_setscheduler_nocheck(task,
SCHED_RR | SCHED_RESET_ON_FORK, &param_fifo);
pr_info("%s: [%s] pid = %d, prio = %d\n",
__func__, task->comm, pid, task->prio);
} else {
pr_err("%s: task not found (pid = %d)\n",
__func__, pid);
}
#endif
}
#endif
#ifdef USE_EXYNOS_AUD_CPU_HOTPLUG
void lpass_get_cpu_hotplug(void)
{
pr_debug("%s ++\n", __func__);
cluster0_core1_hotplug_in(true);
}
void lpass_put_cpu_hotplug(void)
{
pr_debug("%s --\n", __func__);
cluster0_core1_hotplug_in(false);
}
#endif
void lpass_add_stream(void)
{
atomic_inc(&lpass.stream_cnt);
lpass_update_qos();
}
void lpass_remove_stream(void)
{
atomic_dec(&lpass.stream_cnt);
lpass_update_qos();
}
static void lpass_reg_save(void)
{
struct aud_reg *ar;
pr_debug("Registers of LPASS are saved\n");
list_for_each_entry(ar, &reg_list, node)
ar->val = readl(ar->reg);
return;
}
static void lpass_reg_restore(void)
{
struct aud_reg *ar;
pr_debug("Registers of LPASS are restore\n");
list_for_each_entry(ar, &reg_list, node)
writel(ar->val, ar->reg);
return;
}
void lpass_retention_pad_reg(void)
{
regmap_update_bits(lpass.pmureg,
EXYNOS_GPIO_MODE_AUD_SYS_PWR_REG_OFFSET,
0x1, 1);
// writel(1, EXYNOS_PMUREG(0x1340)); /* GPIO_MODE_AUD_SYS_PWR_REG */
}
void lpass_release_pad_reg(void)
{
regmap_update_bits(lpass.pmureg,
EXYNOS_PAD_RETENTION_AUD_OPTION_OFFSET,
0x10000000, 1);
regmap_update_bits(lpass.pmureg,
EXYNOS_GPIO_MODE_AUD_SYS_PWR_REG_OFFSET,
0x1, 1);
// writel(1 << 28, EXYNOS_PMUREG(0x3028)); /* PAD_RETENTION_AUD_OPTION */
// writel(1, EXYNOS_PMUREG(0x1340)); /* GPIO_MODE_AUD_SYS_PWR_REG */
}
static void lpass_retention_pad(void)
{
struct subip_info *si;
/* Powerdown mode for gpio */
list_for_each_entry(si, &subip_list, node) {
if (si->cb != NULL)
(*si->cb)(si->dev);
}
/* Set PAD retention */
lpass_retention_pad_reg();
}
static void lpass_release_pad(void)
{
struct subip_info *si;
/* Restore gpio */
list_for_each_entry(si, &subip_list, node) {
if (si->cb != NULL)
(*si->cb)(si->dev);
}
/* Release PAD retention */
lpass_release_pad_reg();
}
static void ass_enable(void)
{
#ifdef CONFIG_SOC_EXYNOS8890
lpass.mem = ioremap_wc(SRAM_BASE, SRAM_SIZE);
if (!lpass.mem) {
pr_err("LPASS driver failed to ioremap sram \n");
return;
}
#endif
/* Enable PLL */
lpass_enable_pll(true);
lpass_reg_restore();
/* PLL path */
lpass_set_mux_pll();
clk_prepare_enable(lpass.clk_dmac);
clk_prepare_enable(lpass.clk_timer);
lpass.enabled = true;
#if 0
ret = iommu_attach_device(lpass.domain, &lpass.pdev->dev);
if (ret) {
dev_err(&lpass.pdev->dev,
"Unable to attach iommu device: %d\n", ret);
} else {
lpass.enabled = true;
}
#endif
}
static void lpass_enable(void)
{
if (!lpass.valid) {
pr_debug("%s: LPASS is not available", __func__);
return;
}
if (is_old_ass()) {
ass_enable();
return;
}
/* Enable PLL */
lpass_enable_pll(true);
lpass_reg_restore();
/* PLL path */
lpass_set_mux_pll();
if (lpass.clk_dmac)
clk_prepare_enable(lpass.clk_dmac);
if (lpass.clk_sramc)
clk_prepare_enable(lpass.clk_sramc);
lpass_reset_toggle(LPASS_IP_MEM);
lpass_reset_toggle(LPASS_IP_I2S);
lpass_reset_toggle(LPASS_IP_DMA);
#ifdef CONFIG_SOC_EXYNOS8890
if (!lpass.mem) {
lpass.mem = ioremap_wc(SRAM_BASE, SRAM_SIZE);
if (!lpass.mem) {
lpass_enable_pll(false);
pr_err("LPASS driver failed to ioremap sram \n");
return;
}
}
#endif
if (lpass.clk_dmac)
clk_disable_unprepare(lpass.clk_dmac);
/* PAD */
lpass_release_pad();
/* Clear memory */
memset(lpass.mem, 0, lpass.mem_size);
lpass.enabled = true;
#if 0
ret = iommu_attach_device(lpass.domain, &lpass.pdev->dev);
if (ret) {
dev_err(&lpass.pdev->dev,
"Unable to attach iommu device: %d\n", ret);
} else {
lpass.enabled = true;
}
#endif
}
static void ass_disable(void)
{
lpass.enabled = false;
if (lpass.clk_dmac)
clk_disable_unprepare(lpass.clk_dmac);
if (lpass.clk_timer)
clk_disable_unprepare(lpass.clk_timer);
lpass_reg_save();
// iommu_detach_device(lpass.domain, &lpass.pdev->dev);
/* OSC path */
lpass_set_mux_osc();
/* Disable PLL */
lpass_enable_pll(false);
#ifdef CONFIG_SOC_EXYNOS8890
iounmap(lpass.mem);
lpass.mem = NULL;
#endif
}
static void lpass_disable(void)
{
#ifdef CONFIG_SOC_EXYNOS8890
unsigned long start;
unsigned long end;
#endif
if (!lpass.valid) {
pr_debug("%s: LPASS is not available", __func__);
return;
}
if (is_old_ass()) {
ass_disable();
return;
}
lpass.enabled = false;
/* PAD */
lpass_retention_pad();
if (lpass.clk_sramc)
clk_disable_unprepare(lpass.clk_sramc);
lpass_reg_save();
// iommu_detach_device(lpass.domain, &lpass.pdev->dev);
/* OSC path */
lpass_set_mux_osc();
/* Enable clocks */
lpass_reset_clk_default();
/* Disable PLL */
lpass_enable_pll(false);
#ifdef CONFIG_SOC_EXYNOS8890
start = (unsigned long)lpass.mem;
end = (unsigned long)lpass.mem + lpass.mem_size;
iounmap(lpass.mem);
flush_tlb_kernel_range(start, end);
lpass.mem = NULL;
#endif
}
#if 0
static int lpass_get_clk_ass(struct device *dev)
{
lpass.clk_dmac = clk_get(dev, "dmac");
if (IS_ERR(lpass.clk_dmac)) {
dev_err(dev, "dmac clk not found\n");
goto err0;
}
lpass.clk_timer = clk_get(dev, "timer");
if (IS_ERR(lpass.clk_timer)) {
dev_err(dev, "timer clk not found\n");
goto err1;
}
return 0;
err1:
clk_put(lpass.clk_dmac);
err0:
return -1;
}
static int lpass_get_clk(struct device *dev)
{
if (is_old_ass())
return lpass_get_clk_ass(dev);
lpass.clk_dmac = clk_get(dev, "dmac");
if (IS_ERR(lpass.clk_dmac)) {
dev_err(dev, "dmac clk not found\n");
goto err0;
}
lpass.clk_sramc = clk_get(dev, "sramc");
if (IS_ERR(lpass.clk_sramc)) {
dev_err(dev, "sramc clk not found\n");
goto err1;
}
/**
* Exynos7580 (LPASS_VER_270120) doesn't have timer and intr clocks,
* hence we need a special check for it.
*/
if (is_new_ass()) {
lpass.clk_intr = clk_get(dev, "intr");
if (IS_ERR(lpass.clk_intr)) {
dev_err(dev, "intr clk not found\n");
goto err2;
}
lpass.clk_timer = clk_get(dev, "timer");
if (IS_ERR(lpass.clk_timer)) {
dev_err(dev, "timer clk not found\n");
goto err3;
}
}
return 0;
err3:
clk_put(lpass.clk_intr);
err2:
clk_put(lpass.clk_sramc);
err1:
clk_put(lpass.clk_dmac);
err0:
return -1;
}
static void clk_put_all_ass(void)
{
clk_put(lpass.clk_dmac);
clk_put(lpass.clk_timer);
}
static void clk_put_all(void)
{
if (is_old_ass()) {
clk_put_all_ass();
return;
}
clk_put(lpass.clk_dmac);
clk_put(lpass.clk_sramc);
/* Exynos7580 (LPASS_VER_270120) doesn't have clk_intr and clk_timer */
if (is_new_ass()) {
clk_put(lpass.clk_intr);
clk_put(lpass.clk_timer);
}
}
#endif
static void lpass_add_suspend_reg(void __iomem *reg)
{
struct device *dev = &lpass.pdev->dev;
struct aud_reg *ar;
ar = devm_kzalloc(dev, sizeof(struct aud_reg), GFP_KERNEL);
if (!ar)
return;
ar->reg = reg;
list_add(&ar->node, &reg_list);
}
static void lpass_init_reg_list(void)
{
int n = 0;
do {
if (lpass_cmu_save[n] == NULL)
break;
lpass_add_suspend_reg(lpass_cmu_save[n]);
} while (++n);
if (is_new_ass())
lpass_add_suspend_reg(lpass.regs + LPASS_INTR_CA5_MASK);
lpass_add_suspend_reg(lpass.regs + LPASS_INTR_CPU_MASK);
if (lpass.sysmmu)
lpass_add_suspend_reg(lpass.sysmmu);
}
static int lpass_proc_show(struct seq_file *m, void *v) {
struct subip_info *si;
int pmode = exynos_check_aud_pwr();
seq_printf(m, "power: %s\n", lpass.enabled ? "on" : "off");
seq_printf(m, "canbe: %s\n",
(pmode == AUD_PWR_SLEEP) ? "sleep" :
(pmode == AUD_PWR_LPA) ? "lpa" :
(pmode == AUD_PWR_ALPA) ? "alpa" :
(pmode == AUD_PWR_AFTR) ? "aftr" : "unknown");
list_for_each_entry(si, &subip_list, node) {
seq_printf(m, "subip: %s (%d)\n",
si->name, atomic_read(&si->use_cnt));
}
seq_printf(m, "strm: %d\n", atomic_read(&lpass.stream_cnt));
seq_printf(m, "uhqa: %s\n", lpass.uhqa_on ? "on" : "off");
#ifdef USE_AUD_DEVFREQ
seq_printf(m, "cpu: %d, kfc: %d\n",
lpass.cpu_qos / 1000, lpass.kfc_qos / 1000);
seq_printf(m, "mif: %d, int: %d\n",
lpass.mif_qos / 1000, lpass.int_qos / 1000);
#endif
return 0;
}
static int lpass_proc_open(struct inode *inode, struct file *file) {
return single_open(file, lpass_proc_show, NULL);
}
static const struct file_operations lpass_proc_fops = {
.owner = THIS_MODULE,
.open = lpass_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
#ifdef CONFIG_PM_SLEEP
static int lpass_suspend(struct device *dev)
{
pr_debug("%s entered\n", __func__);
#ifdef CONFIG_PM
if (atomic_read(&lpass.use_cnt) > 0)
lpass_disable();
#else
lpass_disable();
#endif
return 0;
}
static int lpass_resume(struct device *dev)
{
pr_debug("%s entered\n", __func__);
#ifdef CONFIG_PM
if (atomic_read(&lpass.use_cnt) > 0)
lpass_enable();
#else
lpass_enable();
#endif
return 0;
}
#else
#define lpass_suspend NULL
#define lpass_resume NULL
#endif
#ifdef CONFIG_OF
static const struct of_device_id exynos_lpass_match[];
static int lpass_get_ver(struct device_node *np)
{
const struct of_device_id *match;
int ver;
if (np) {
match = of_match_node(exynos_lpass_match, np);
ver = *(int *)(match->data);
} else {
ver = LPASS_VER_000100;
}
return ver;
}
#else
static int lpass_get_ver(struct device_node *np)
{
return LPASS_VER_000100;
}
#endif
static void lpass_update_qos(void)
{
#ifdef USE_AUD_DEVFREQ
int cpu_qos_new, kfc_qos_new, mif_qos_new, int_qos_new;
if (!lpass.enabled) {
cpu_qos_new = 0;
kfc_qos_new = 0;
mif_qos_new = 0;
int_qos_new = 0;
} else if (lpass.uhqa_on) {
cpu_qos_new = AUD_CPU_FREQ_UHQA;
kfc_qos_new = AUD_KFC_FREQ_UHQA;
mif_qos_new = AUD_MIF_FREQ_UHQA;
int_qos_new = AUD_INT_FREQ_UHQA;
} else if (atomic_read(&lpass.stream_cnt) > 1) {
cpu_qos_new = AUD_CPU_FREQ_HIGH;
kfc_qos_new = AUD_KFC_FREQ_HIGH;
mif_qos_new = AUD_MIF_FREQ_HIGH;
int_qos_new = AUD_INT_FREQ_HIGH;
} else {
cpu_qos_new = AUD_CPU_FREQ_NORM;
kfc_qos_new = AUD_KFC_FREQ_NORM;
mif_qos_new = AUD_MIF_FREQ_NORM;
int_qos_new = AUD_INT_FREQ_NORM;
}
if (lpass.cpu_qos != cpu_qos_new) {
lpass.cpu_qos = cpu_qos_new;
pm_qos_update_request(&lpass.aud_cluster1_qos, lpass.cpu_qos);
pr_debug("%s: cpu_qos = %d\n", __func__, lpass.cpu_qos);
}
if (lpass.kfc_qos != kfc_qos_new) {
lpass.kfc_qos = kfc_qos_new;
pm_qos_update_request(&lpass.aud_cluster0_qos, lpass.kfc_qos);
pr_debug("%s: kfc_qos = %d\n", __func__, lpass.kfc_qos);
}
if (lpass.mif_qos != mif_qos_new) {
lpass.mif_qos = mif_qos_new;
pm_qos_update_request(&lpass.aud_mif_qos, lpass.mif_qos);
pr_debug("%s: mif_qos = %d\n", __func__, lpass.mif_qos);
}
if (lpass.int_qos != int_qos_new) {
lpass.int_qos = int_qos_new;
pm_qos_update_request(&lpass.aud_int_qos, lpass.int_qos);
pr_debug("%s: int_qos = %d\n", __func__, lpass.int_qos);
}
#endif
}
static int lpass_fb_state_chg(struct notifier_block *nb,
unsigned long val, void *data)
{
struct fb_event *evdata = data;
unsigned int blank;
if (val != FB_EVENT_BLANK)
return 0;
blank = *(int *)evdata->data;
switch (blank) {
case FB_BLANK_POWERDOWN:
lpass.display_on = false;
lpass_update_qos();
break;
case FB_BLANK_UNBLANK:
lpass.display_on = true;
lpass_update_qos();
break;
default:
break;
}
return NOTIFY_OK;
}
static struct notifier_block fb_noti_block = {
.notifier_call = lpass_fb_state_chg,
};
static int exynos_aud_alpa_notifier(struct notifier_block *nb,
unsigned long event, void *data)
{
switch (event) {
case SICD_AUD_ENTER:
#ifdef CONFIG_SND_SAMSUNG_SEIREN_OFFLOAD
esa_compr_alpa_notifier(true);
#endif
break;
case SICD_AUD_EXIT:
#ifdef CONFIG_SND_SAMSUNG_SEIREN_OFFLOAD
esa_compr_alpa_notifier(false);
#endif
break;
default:
break;
}
return NOTIFY_DONE;
}
static struct notifier_block lpass_lpa_nb = {
.notifier_call = exynos_aud_alpa_notifier,
};
static char banner[] =
KERN_INFO "Samsung Low Power Audio Subsystem driver, "\
"(c)2013 Samsung Electronics\n";
static int lpass_probe(struct platform_device *pdev)
{
struct resource *res;
struct device *dev = &pdev->dev;
struct device_node *np = pdev->dev.of_node;
int ret = 0;
printk(banner);
lpass.pdev = pdev;
/* LPASS version */
lpass.ver = lpass_get_ver(np);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(dev, "Unable to get LPASS SFRs\n");
return -ENXIO;
}
lpass.regs = ioremap(res->start, resource_size(res));
if (!lpass.regs) {
dev_err(dev, "SFR ioremap failed\n");
return -ENOMEM;
}
pr_info("%s: regs_base = %08X (%08X bytes)\n",
__func__, (u32)res->start, (u32)resource_size(res));
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (!res) {
dev_err(dev, "Unable to get LPASS SRAM\n");
return -ENXIO;
}
#ifndef CONFIG_SOC_EXYNOS8890
lpass.mem = ioremap_wc(res->start, resource_size(res));
if (!lpass.mem) {
dev_err(dev, "SRAM ioremap failed\n");
return -ENOMEM;
}
#else
lpass.mem = NULL;
#endif
lpass.mem_size = resource_size(res);
pr_info("%s: sram_base = %08X (%08X bytes)\n",
__func__, (u32)res->start, (u32)resource_size(res));
if (lpass.ver >= LPASS_VER_370200) {
res = platform_get_resource(pdev, IORESOURCE_MEM, 2);
if (!res) {
dev_err(dev, "Unable to get LPASS-s SFRs\n");
return -ENXIO;
}
lpass.regs_s = ioremap(res->start, resource_size(res));
if (!lpass.regs_s) {
dev_err(dev, "SFR ioremap failed\n");
return -ENOMEM;
}
pr_info("%s: regs-s_base = %08X (%08X bytes)\n",
__func__, (u32)res->start, (u32)resource_size(res));
} else {
lpass.regs_s = lpass.regs;
pr_debug("%s: regs-s_base is set as regs", __func__);
}
ret = lpass_get_clk(&pdev->dev, &lpass);
if (ret) {
dev_err(dev, "failed to get clock\n");
return -ENXIO;
}
ret = lpass_set_clk_heirachy(&pdev->dev);
if (ret) {
dev_err(dev, "failed to set clock hierachy\n");
return -ENXIO;
}
lpass_init_clk_gate();
#ifdef CONFIG_SND_SAMSUNG_IOMMU
lpass.domain = get_domain_from_dev(dev);
if (!lpass.domain) {
dev_err(dev, "Unable to get iommu domain\n");
return -ENOENT;
}
#else
/* Bypass SysMMU */
if (lpass.ver >= LPASS_VER_370210) {
lpass.sysmmu = ioremap(0x114e0000, SZ_32);
writel(0, lpass.sysmmu);
}
#endif
lpass.proc_file = proc_create("driver/lpass", 0,
NULL, &lpass_proc_fops);
if (!lpass.proc_file)
pr_info("Failed to register /proc/driver/lpadd\n");
spin_lock_init(&lpass.lock);
atomic_set(&lpass.dma_use_cnt, 0);
atomic_set(&lpass.use_cnt, 0);
atomic_set(&lpass.stream_cnt, 0);
lpass_init_reg_list();
/* unmask irq source */
if (is_new_ass())
writel(INTR_CA5_MASK_VAL, lpass.regs + LPASS_INTR_CA5_MASK);
writel(INTR_CPU_MASK_VAL, lpass.regs + LPASS_INTR_CPU_MASK);
lpass_reg_save();
lpass.valid = true;
lpass.pmureg = syscon_regmap_lookup_by_phandle(dev->of_node,
"samsung,syscon-phandle");
if (IS_ERR(lpass.pmureg)) {
dev_err(&pdev->dev, "syscon regmap lookup failed.\n");
return PTR_ERR(lpass.pmureg);
}
regmap_update_bits(lpass.pmureg,
EXYNOS_PMU_PMU_DEBUG_OFFSET,
0x1F00, 0x1F00);
regmap_update_bits(lpass.pmureg,
EXYNOS_PMU_PMU_DEBUG_OFFSET,
0x1, 0x0);
ret = iommu_attach_device(lpass.domain, &lpass.pdev->dev);
if (ret) {
dev_err(&lpass.pdev->dev,
"Unable to attach iommu device: %d\n", ret);
return -ENXIO;
}
#ifdef CONFIG_PM
pm_runtime_enable(&lpass.pdev->dev);
if (is_new_ass())
pm_runtime_get_sync(&lpass.pdev->dev);
#else
lpass_enable();
#endif
lpass.display_on = true;
fb_register_client(&fb_noti_block);
lpass_update_lpclock(LPCLK_CTRLID_LEGACY|LPCLK_CTRLID_OFFLOAD, false);
#ifdef USE_AUD_DEVFREQ
lpass.cpu_qos = 0;
lpass.kfc_qos = 0;
lpass.mif_qos = 0;
lpass.int_qos = 0;
pm_qos_add_request(&lpass.aud_cluster1_qos, PM_QOS_CLUSTER1_FREQ_MIN, 0);
pm_qos_add_request(&lpass.aud_cluster0_qos, PM_QOS_CLUSTER0_FREQ_MIN, 0);
pm_qos_add_request(&lpass.aud_mif_qos, PM_QOS_BUS_THROUGHPUT, 0);
pm_qos_add_request(&lpass.aud_int_qos, PM_QOS_DEVICE_THROUGHPUT, 0);
#endif
exynos_pm_register_notifier(&lpass_lpa_nb);
pr_info("%s: LPASS driver was registerd successfully\n", __func__);
return 0;
}
static int lpass_remove(struct platform_device *pdev)
{
#ifdef CONFIG_SND_SAMSUNG_IOMMU
iommu_detach_device(lpass.domain, &pdev->dev);
iommu_domain_free(lpass.domain);
#else
if (lpass.sysmmu)
iounmap(lpass.sysmmu);
#endif
#ifdef CONFIG_PM
pm_runtime_disable(&pdev->dev);
#else
lpass_disable();
#endif
iounmap(lpass.regs);
iounmap(lpass.regs_s);
#ifndef CONFIG_SOC_EXYNOS8890
iounmap(lpass.mem);
#endif
return 0;
}
#ifdef CONFIG_PM
static int lpass_runtime_suspend(struct device *dev)
{
pr_debug("%s entered\n", __func__);
lpass_disable();
return 0;
}
static int lpass_runtime_resume(struct device *dev)
{
pr_debug("%s entered\n", __func__);
lpass_enable();
return 0;
}
#endif
static const int lpass_ver_data[] = {
[LPASS_VER_000100] = LPASS_VER_000100,
[LPASS_VER_010100] = LPASS_VER_010100,
[LPASS_VER_100100] = LPASS_VER_100100,
[LPASS_VER_110100] = LPASS_VER_110100,
[LPASS_VER_270120] = LPASS_VER_270120,
[LPASS_VER_370100] = LPASS_VER_370100,
[LPASS_VER_370200] = LPASS_VER_370200,
[LPASS_VER_370210] = LPASS_VER_370210,
};
static struct platform_device_id lpass_driver_ids[] = {
{
.name = "samsung-lpass",
}, {
.name = "samsung-audss",
}, {},
};
MODULE_DEVICE_TABLE(platform, lpass_driver_ids);
#ifdef CONFIG_OF
static const struct of_device_id exynos_lpass_match[] = {
{
.compatible = "samsung,exynos8890-lpass",
.data = &lpass_ver_data[LPASS_VER_370210],
}, {
.compatible = "samsung,exynos7580-lpass",
.data = &lpass_ver_data[LPASS_VER_270120],
}, {
.compatible = "samsung,exynos7420-lpass",
.data = &lpass_ver_data[LPASS_VER_370210],
}, {
.compatible = "samsung,exynos5433-lpass",
.data = &lpass_ver_data[LPASS_VER_370200],
}, {
.compatible = "samsung,exynos5430-lpass",
.data = &lpass_ver_data[LPASS_VER_370100],
}, {
.compatible = "samsung,exynos5-audss",
.data = &lpass_ver_data[LPASS_VER_110100],
}, {},
};
MODULE_DEVICE_TABLE(of, exynos_lpass_match);
#endif
static const struct dev_pm_ops lpass_pmops = {
SET_SYSTEM_SLEEP_PM_OPS(
lpass_suspend,
lpass_resume
)
SET_RUNTIME_PM_OPS(
lpass_runtime_suspend,
lpass_runtime_resume,
NULL
)
};
static struct platform_driver lpass_driver = {
.probe = lpass_probe,
.remove = lpass_remove,
.id_table = lpass_driver_ids,
.driver = {
.name = "samsung-lpass",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(exynos_lpass_match),
.pm = &lpass_pmops,
},
};
static int __init lpass_driver_init(void)
{
return platform_driver_register(&lpass_driver);
}
fs_initcall(lpass_driver_init);
#ifdef CONFIG_PM
static int lpass_driver_rpm_begin(void)
{
pr_debug("%s entered\n", __func__);
if (is_new_ass())
pm_runtime_put_sync(&lpass.pdev->dev);
return 0;
}
late_initcall(lpass_driver_rpm_begin);
#endif
/* Module information */
MODULE_AUTHOR("Yeongman Seo, <yman.seo@samsung.com>");
MODULE_DESCRIPTION("Samsung Low Power Audio Subsystem Interface");
MODULE_ALIAS("platform:samsung-lpass");
MODULE_LICENSE("GPL");