android_kernel_samsung_on5x.../sound/soc/samsung/lpass-exynos7570.c
2018-06-19 23:16:04 +02:00

758 lines
16 KiB
C

/*
* Audio SubSystem driver for Samsung Exynos7570
*
* Copyright (c) 2015 Samsung Electronics Co. Ltd.
* Tushar Behera <tushar.b@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/of.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/proc_fs.h>
#include <linux/mfd/syscon.h>
#include <linux/regmap.h>
#include <soc/samsung/exynos-powermode.h>
#include <sound/exynos.h>
#include <sound/exynos-audmixer.h>
#ifdef CONFIG_SND_SOC_SAMSUNG_VERBOSE_DEBUG
#ifdef dev_dbg
#undef dev_dbg
#endif
#define dev_dbg dev_err
#endif
#define AUD_PLL_FREQ (98304020U)
#define AUD_MI2S_FREQ (110000000U + 100)
#define AUD_MIXER_FREQ (49152000U + 100)
#define EXYNOS7570_DISPAUD_CFG 0x1000
#define EXYNOS7570_DISPAUD_INTMASK 0x101C
#define DISPAUD_CFG_ADMA_SWRST_BIT 3
#define DISPAUD_CFG_AMP_SWRST_BIT 2
#define DISPAUD_CFG_MI2S_SWRST_BIT 1
#define DISPAUD_CFG_MIXER_SWRST_BIT 0
#define EXYNOS_GPIO_MODE_AUD_SYS_PWR_REG_OFFSET 0x1340
#define EXYNOS_PAD_RETENTION_AUD_OPTION_OFFSET 0x3028
static struct lpass_cmu_info {
struct clk *clk_fout_aud_pll;
struct clk *clk_dout_sclk_mi2s;
struct clk *clk_dout_sclk_mixer;
struct clk *clk_mi2s_aud_bclk;
struct clk *clk_aclk_aud;
struct clk *clk_dispaud_decon;
} lpass_cmu;
void __iomem *lpass_cmu_save[] = {
NULL, /* endmark */
};
/* Audio subsystem version */
enum {
LPASS_VER_7570, /* Java */
LPASS_VER_MAX
};
static struct lpass_info {
spinlock_t lock;
bool valid;
bool enabled;
struct platform_device *pdev;
void __iomem *regs;
struct proc_dir_entry *proc_file;
atomic_t use_cnt;
atomic_t stream_cnt;
struct regmap *pmureg;
bool display_on;
bool uhqa_on;
int idle_ip_index;
} 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;
};
static LIST_HEAD(reg_list);
static LIST_HEAD(subip_list);
static void lpass_update_qos(void);
static void lpass_enable_pll(bool on)
{
if (on) {
clk_prepare_enable(lpass_cmu.clk_dispaud_decon);
clk_prepare_enable(lpass_cmu.clk_fout_aud_pll);
clk_prepare_enable(lpass_cmu.clk_dout_sclk_mi2s);
} else {
clk_disable_unprepare(lpass_cmu.clk_fout_aud_pll);
clk_disable_unprepare(lpass_cmu.clk_dout_sclk_mi2s);
clk_disable_unprepare(lpass_cmu.clk_dispaud_decon);
}
}
/*
* lpass_set_clk_hierarchy(): Define clock settings for audio
*
* This configures the default state of the MUX/DIV clocks used in Audio
* sub-system. Since this called from driver probe function only, it is safe to
* use devm_clk_get() APIs and remove the goto statements.
*
* Arguments:
* 1. dev: 'struct device *', pointer to LPASS device
*
* Return value:
* 0 on success, error code on failure.
*/
static int lpass_set_clk_hierarchy(struct device *dev)
{
lpass_cmu.clk_fout_aud_pll = devm_clk_get(dev, "fout_aud_pll");
if (IS_ERR(lpass_cmu.clk_fout_aud_pll)) {
dev_err(dev, "fout_aud_pll clk not found\n");
return PTR_ERR(lpass_cmu.clk_fout_aud_pll);
}
lpass_cmu.clk_dout_sclk_mi2s = devm_clk_get(dev, "dout_sclk_mi2s");
if (IS_ERR(lpass_cmu.clk_dout_sclk_mi2s)) {
dev_err(dev, "dout_sclk_mi2s clk not found\n");
return PTR_ERR(lpass_cmu.clk_dout_sclk_mi2s);
}
lpass_cmu.clk_dispaud_decon = devm_clk_get(dev, "dispaud_decon");
if (IS_ERR(lpass_cmu.clk_dispaud_decon)) {
dev_err(dev, "clk_dispaud_decon clk not found\n");
return PTR_ERR(lpass_cmu.clk_dispaud_decon);
}
lpass_enable_pll(true);
clk_set_rate(lpass_cmu.clk_fout_aud_pll, AUD_PLL_FREQ);
dev_info(dev, "PLL rate = %lu\n",
clk_get_rate(lpass_cmu.clk_fout_aud_pll));
clk_set_rate(lpass_cmu.clk_dout_sclk_mi2s, AUD_MI2S_FREQ);
dev_info(dev, "sclk_mi2s clock rate = %lu\n",
clk_get_rate(lpass_cmu.clk_dout_sclk_mi2s));
return 0;
}
/**
* AUD_PLL_USER Mux is defined as USERMUX. Enabling the USERMUX selects
* the underlying PLL as the parent of this MUX and disabling sets the
* oscillator clock as the parent of this clock.
*/
static void lpass_set_mux_pll(void)
{
/* Already taken care in lpass_enable_pll() */
}
static void lpass_set_mux_osc(void)
{
/* Already taken care in lpass_enable_pll() */
}
static void lpass_retention_pad_reg(void)
{
regmap_update_bits(lpass.pmureg,
EXYNOS_GPIO_MODE_AUD_SYS_PWR_REG_OFFSET,
0x1, 1);
}
void lpass_release_pad_reg(void)
{
regmap_update_bits(lpass.pmureg,
EXYNOS_PAD_RETENTION_AUD_OPTION_OFFSET,
0x10000000, 0x10000000);
regmap_update_bits(lpass.pmureg,
EXYNOS_GPIO_MODE_AUD_SYS_PWR_REG_OFFSET,
0x1, 1);
}
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)
{
/* TODO: Implement later */
return 0;
}
void lpass_set_dma_intr(bool on)
{
u32 reg, val;
void __iomem *regs;
spin_lock(&lpass.lock);
regs = lpass.regs;
reg = EXYNOS7570_DISPAUD_CFG;
val = readl(regs + reg);
val |= 0x1;
writel(val, regs + reg);
spin_unlock(&lpass.lock);
}
void lpass_reset(int ip, int op)
{
u32 reg, val, bit;
void __iomem *regs;
spin_lock(&lpass.lock);
regs = lpass.regs;
reg = EXYNOS7570_DISPAUD_CFG;
switch (ip) {
case LPASS_IP_DMA:
bit = DISPAUD_CFG_ADMA_SWRST_BIT;
break;
case LPASS_IP_AMP:
bit = DISPAUD_CFG_AMP_SWRST_BIT;
break;
case LPASS_IP_I2S:
bit = DISPAUD_CFG_MI2S_SWRST_BIT;
break;
case LPASS_IP_MIXER:
bit = DISPAUD_CFG_MIXER_SWRST_BIT;
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_RESET_BIT_UNSET:
val &= ~BIT(bit);
break;
case LPASS_RESET_BIT_SET:
val |= BIT(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)
{
lpass_reset(ip, LPASS_RESET_BIT_SET);
udelay(100);
lpass_reset(ip, LPASS_RESET_BIT_UNSET);
}
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);
dev_dbg(ip_dev, "%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);
dev_dbg(ip_dev, "%s: %s (use:%d)\n", __func__,
si->name, atomic_read(&si->use_cnt));
pm_runtime_put_sync(&lpass.pdev->dev);
}
}
lpass_update_qos();
}
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;
list_for_each_entry(ar, &reg_list, node)
ar->val = readl(ar->reg);
return;
}
static void lpass_reg_restore(void)
{
struct aud_reg *ar;
list_for_each_entry(ar, &reg_list, node)
writel(ar->val, ar->reg);
return;
}
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 lpass_enable(struct device *dev)
{
static unsigned int count;
dev_dbg(dev, "%s (count = %d)\n", __func__, ++count);
if (!lpass.valid) {
dev_err(dev, "%s: LPASS is not available", __func__);
return;
}
/* Enable PLL */
lpass_enable_pll(true);
lpass_reg_restore();
/* PLL path */
lpass_set_mux_pll();
/* Reset blocks inside audio sub-system as they are just powered-on */
lpass_reset_toggle(LPASS_IP_MIXER);
lpass_reset_toggle(LPASS_IP_AMP);
lpass_reset_toggle(LPASS_IP_I2S);
lpass_reset_toggle(LPASS_IP_DMA);
/* PAD */
lpass_release_pad();
/* ToDo: Check With AP Dev. */
//lpass_set_dma_intr(true);
lpass.enabled = true;
}
static void lpass_disable(struct device *dev)
{
static unsigned int count;
dev_dbg(dev, "%s (count = %d)\n", __func__, ++count);
if (!lpass.valid) {
dev_err(dev, "%s: LPASS is not available", __func__);
return;
}
lpass.enabled = false;
/* PAD */
lpass_retention_pad();
lpass_reg_save();
/* OSC path */
lpass_set_mux_osc();
/* Disable PLL */
lpass_enable_pll(false);
}
void lpass_dma_enable(bool on)
{
}
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);
}
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");
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)
{
/*
* LPASS should be on during call, need to supply clocks to
* codec and mixer, so if call is in progress exit from suspend.
*/
if (is_cp_aud_enabled()) {
dev_info(dev, "Audio block active, don't suspend\n");
return 0;
}
#ifdef CONFIG_PM_RUNTIME
if (atomic_read(&lpass.use_cnt) > 0)
lpass_disable(dev);
#else
lpass_disable(dev);
#endif
exynos_update_ip_idle_status(lpass.idle_ip_index, 1);
return 0;
}
static int lpass_resume(struct device *dev)
{
/*
* LPASS was left enabled during CP call. If it is already on, no need
* to do anything here.
*/
if (lpass.enabled)
return 0;
exynos_update_ip_idle_status(lpass.idle_ip_index, 0);
#ifdef CONFIG_PM_RUNTIME
if (atomic_read(&lpass.use_cnt) > 0)
lpass_enable(dev);
#else
lpass_enable(dev);
#endif
return 0;
}
#else
#define lpass_suspend NULL
#define lpass_resume NULL
#endif
static void lpass_update_qos(void)
{
}
static char banner[] = KERN_INFO "Samsung Audio Subsystem driver\n";
static int lpass_probe(struct platform_device *pdev)
{
struct resource *res;
struct device *dev = &pdev->dev;
int ret = 0;
printk(banner);
lpass.pdev = pdev;
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, res->end);
if (!lpass.regs) {
dev_err(dev, "SFR ioremap failed\n");
return -ENOMEM;
}
ret = lpass_set_clk_hierarchy(&pdev->dev);
if (ret) {
dev_err(dev, "failed to set clock hierachy\n");
return -ENXIO;
}
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.use_cnt, 0);
atomic_set(&lpass.stream_cnt, 0);
lpass_init_reg_list();
lpass.idle_ip_index = exynos_get_idle_ip_index(dev_name(&pdev->dev));
if (lpass.idle_ip_index < 0)
dev_err(dev, "Idle ip index is not provided for Audio.\n");
#ifdef CONFIG_PM_RUNTIME
pm_runtime_enable(&lpass.pdev->dev);
pm_runtime_get_sync(&lpass.pdev->dev);
#else
exynos_update_ip_idle_status(lpass.idle_ip_index, 0);
lpass_enable(&lpass.pdev->dev);
#endif
lpass_reg_save();
lpass.valid = true;
lpass.display_on = 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);
}
dev_dbg(dev, "%s Completed\n", __func__);
return 0;
}
static int lpass_remove(struct platform_device *pdev)
{
#ifdef CONFIG_PM_RUNTIME
pm_runtime_disable(&pdev->dev);
#else
lpass_disable(&pdev->dev);
exynos_update_ip_idle_status(lpass.idle_ip_index, 1);
#endif
iounmap(lpass.regs);
return 0;
}
#ifdef CONFIG_PM_RUNTIME
static int lpass_runtime_suspend(struct device *dev)
{
lpass_disable(dev);
exynos_update_ip_idle_status(lpass.idle_ip_index, 1);
return 0;
}
static int lpass_runtime_resume(struct device *dev)
{
exynos_update_ip_idle_status(lpass.idle_ip_index, 0);
lpass_enable(dev);
return 0;
}
#endif
static const int lpass_ver_data[] = {
[LPASS_VER_7570] = LPASS_VER_7570,
};
static struct platform_device_id lpass_driver_ids[] = {
{
.name = "samsung-lpass",
}, {},
};
MODULE_DEVICE_TABLE(platform, lpass_driver_ids);
#ifdef CONFIG_OF
static const struct of_device_id exynos_lpass_match[] = {
{
.compatible = "samsung,exynos7570-lpass",
.data = &lpass_ver_data[LPASS_VER_7570],
}, {},
};
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);
}
subsys_initcall(lpass_driver_init);
#ifdef CONFIG_PM_RUNTIME
static int lpass_driver_rpm_begin(void)
{
pr_debug("%s entered\n", __func__);
#ifndef CONFIG_SND_SOC_SAMSUNG_DUMMY_CODEC
pm_runtime_put_sync(&lpass.pdev->dev);
#endif
return 0;
}
late_initcall(lpass_driver_rpm_begin);
#endif
/* Module information */
MODULE_AUTHOR("Divya Jaiswal <divya.jswl@samsung.com>");
MODULE_AUTHOR("Chandrasekar R <rcsekar@samsung.com>");
MODULE_LICENSE("GPL");