e89f45edb7
We have SOF and generic ACP support enabled for Vangogh platform on some machines. Since we have same PCI id used for probing, add check for machine configuration flag to avoid conflict with newer pci drivers. Such machine flag has been initialized via dmi match on few Vangogh based machines. If no flag is specified probe and register older platform device. Signed-off-by: Venkata Prasad Potturu <venkataprasad.potturu@amd.com> Link: https://lore.kernel.org/r/20230530110802.674939-1-venkataprasad.potturu@amd.com Signed-off-by: Mark Brown <broonie@kernel.org>
339 lines
7.9 KiB
C
339 lines
7.9 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
//
|
|
// AMD Vangogh ACP PCI Driver
|
|
//
|
|
// Copyright (C) 2021 Advanced Micro Devices, Inc. All rights reserved.
|
|
|
|
#include <linux/pci.h>
|
|
#include <linux/module.h>
|
|
#include <linux/io.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/pm_runtime.h>
|
|
|
|
#include "acp5x.h"
|
|
|
|
struct acp5x_dev_data {
|
|
void __iomem *acp5x_base;
|
|
bool acp5x_audio_mode;
|
|
struct resource *res;
|
|
struct platform_device *pdev[ACP5x_DEVS];
|
|
};
|
|
|
|
static int acp5x_power_on(void __iomem *acp5x_base)
|
|
{
|
|
u32 val;
|
|
int timeout;
|
|
|
|
val = acp_readl(acp5x_base + ACP_PGFSM_STATUS);
|
|
|
|
if (val == 0)
|
|
return val;
|
|
|
|
if ((val & ACP_PGFSM_STATUS_MASK) !=
|
|
ACP_POWER_ON_IN_PROGRESS)
|
|
acp_writel(ACP_PGFSM_CNTL_POWER_ON_MASK,
|
|
acp5x_base + ACP_PGFSM_CONTROL);
|
|
timeout = 0;
|
|
while (++timeout < 500) {
|
|
val = acp_readl(acp5x_base + ACP_PGFSM_STATUS);
|
|
if ((val & ACP_PGFSM_STATUS_MASK) == ACP_POWERED_ON)
|
|
return 0;
|
|
udelay(1);
|
|
}
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
static int acp5x_reset(void __iomem *acp5x_base)
|
|
{
|
|
u32 val;
|
|
int timeout;
|
|
|
|
acp_writel(1, acp5x_base + ACP_SOFT_RESET);
|
|
timeout = 0;
|
|
while (++timeout < 500) {
|
|
val = acp_readl(acp5x_base + ACP_SOFT_RESET);
|
|
if (val & ACP_SOFT_RESET_SOFTRESET_AUDDONE_MASK)
|
|
break;
|
|
cpu_relax();
|
|
}
|
|
acp_writel(0, acp5x_base + ACP_SOFT_RESET);
|
|
timeout = 0;
|
|
while (++timeout < 500) {
|
|
val = acp_readl(acp5x_base + ACP_SOFT_RESET);
|
|
if (!val)
|
|
return 0;
|
|
cpu_relax();
|
|
}
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
static void acp5x_enable_interrupts(void __iomem *acp5x_base)
|
|
{
|
|
acp_writel(0x01, acp5x_base + ACP_EXTERNAL_INTR_ENB);
|
|
}
|
|
|
|
static void acp5x_disable_interrupts(void __iomem *acp5x_base)
|
|
{
|
|
acp_writel(ACP_EXT_INTR_STAT_CLEAR_MASK, acp5x_base +
|
|
ACP_EXTERNAL_INTR_STAT);
|
|
acp_writel(0x00, acp5x_base + ACP_EXTERNAL_INTR_CNTL);
|
|
acp_writel(0x00, acp5x_base + ACP_EXTERNAL_INTR_ENB);
|
|
}
|
|
|
|
static int acp5x_init(void __iomem *acp5x_base)
|
|
{
|
|
int ret;
|
|
|
|
/* power on */
|
|
ret = acp5x_power_on(acp5x_base);
|
|
if (ret) {
|
|
pr_err("ACP5x power on failed\n");
|
|
return ret;
|
|
}
|
|
acp_writel(0x01, acp5x_base + ACP_CONTROL);
|
|
/* Reset */
|
|
ret = acp5x_reset(acp5x_base);
|
|
if (ret) {
|
|
pr_err("ACP5x reset failed\n");
|
|
return ret;
|
|
}
|
|
acp_writel(0x03, acp5x_base + ACP_CLKMUX_SEL);
|
|
acp5x_enable_interrupts(acp5x_base);
|
|
return 0;
|
|
}
|
|
|
|
static int acp5x_deinit(void __iomem *acp5x_base)
|
|
{
|
|
int ret;
|
|
|
|
acp5x_disable_interrupts(acp5x_base);
|
|
/* Reset */
|
|
ret = acp5x_reset(acp5x_base);
|
|
if (ret) {
|
|
pr_err("ACP5x reset failed\n");
|
|
return ret;
|
|
}
|
|
acp_writel(0x00, acp5x_base + ACP_CLKMUX_SEL);
|
|
acp_writel(0x00, acp5x_base + ACP_CONTROL);
|
|
return 0;
|
|
}
|
|
|
|
static int snd_acp5x_probe(struct pci_dev *pci,
|
|
const struct pci_device_id *pci_id)
|
|
{
|
|
struct acp5x_dev_data *adata;
|
|
struct platform_device_info pdevinfo[ACP5x_DEVS];
|
|
unsigned int irqflags, flag;
|
|
int ret, i;
|
|
u32 addr, val;
|
|
|
|
/* Return if acp config flag is defined */
|
|
flag = snd_amd_acp_find_config(pci);
|
|
if (flag)
|
|
return -ENODEV;
|
|
|
|
irqflags = IRQF_SHARED;
|
|
if (pci->revision != 0x50)
|
|
return -ENODEV;
|
|
|
|
if (pci_enable_device(pci)) {
|
|
dev_err(&pci->dev, "pci_enable_device failed\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
ret = pci_request_regions(pci, "AMD ACP5x audio");
|
|
if (ret < 0) {
|
|
dev_err(&pci->dev, "pci_request_regions failed\n");
|
|
goto disable_pci;
|
|
}
|
|
|
|
adata = devm_kzalloc(&pci->dev, sizeof(struct acp5x_dev_data),
|
|
GFP_KERNEL);
|
|
if (!adata) {
|
|
ret = -ENOMEM;
|
|
goto release_regions;
|
|
}
|
|
addr = pci_resource_start(pci, 0);
|
|
adata->acp5x_base = devm_ioremap(&pci->dev, addr,
|
|
pci_resource_len(pci, 0));
|
|
if (!adata->acp5x_base) {
|
|
ret = -ENOMEM;
|
|
goto release_regions;
|
|
}
|
|
pci_set_master(pci);
|
|
pci_set_drvdata(pci, adata);
|
|
ret = acp5x_init(adata->acp5x_base);
|
|
if (ret)
|
|
goto release_regions;
|
|
|
|
val = acp_readl(adata->acp5x_base + ACP_PIN_CONFIG);
|
|
switch (val) {
|
|
case I2S_MODE:
|
|
adata->res = devm_kzalloc(&pci->dev,
|
|
sizeof(struct resource) * ACP5x_RES,
|
|
GFP_KERNEL);
|
|
if (!adata->res) {
|
|
ret = -ENOMEM;
|
|
goto de_init;
|
|
}
|
|
|
|
adata->res[0].name = "acp5x_i2s_iomem";
|
|
adata->res[0].flags = IORESOURCE_MEM;
|
|
adata->res[0].start = addr;
|
|
adata->res[0].end = addr + (ACP5x_REG_END - ACP5x_REG_START);
|
|
|
|
adata->res[1].name = "acp5x_i2s_sp";
|
|
adata->res[1].flags = IORESOURCE_MEM;
|
|
adata->res[1].start = addr + ACP5x_I2STDM_REG_START;
|
|
adata->res[1].end = addr + ACP5x_I2STDM_REG_END;
|
|
|
|
adata->res[2].name = "acp5x_i2s_hs";
|
|
adata->res[2].flags = IORESOURCE_MEM;
|
|
adata->res[2].start = addr + ACP5x_HS_TDM_REG_START;
|
|
adata->res[2].end = addr + ACP5x_HS_TDM_REG_END;
|
|
|
|
adata->res[3].name = "acp5x_i2s_irq";
|
|
adata->res[3].flags = IORESOURCE_IRQ;
|
|
adata->res[3].start = pci->irq;
|
|
adata->res[3].end = adata->res[3].start;
|
|
|
|
adata->acp5x_audio_mode = ACP5x_I2S_MODE;
|
|
|
|
memset(&pdevinfo, 0, sizeof(pdevinfo));
|
|
pdevinfo[0].name = "acp5x_i2s_dma";
|
|
pdevinfo[0].id = 0;
|
|
pdevinfo[0].parent = &pci->dev;
|
|
pdevinfo[0].num_res = 4;
|
|
pdevinfo[0].res = &adata->res[0];
|
|
pdevinfo[0].data = &irqflags;
|
|
pdevinfo[0].size_data = sizeof(irqflags);
|
|
|
|
pdevinfo[1].name = "acp5x_i2s_playcap";
|
|
pdevinfo[1].id = 0;
|
|
pdevinfo[1].parent = &pci->dev;
|
|
pdevinfo[1].num_res = 1;
|
|
pdevinfo[1].res = &adata->res[1];
|
|
|
|
pdevinfo[2].name = "acp5x_i2s_playcap";
|
|
pdevinfo[2].id = 1;
|
|
pdevinfo[2].parent = &pci->dev;
|
|
pdevinfo[2].num_res = 1;
|
|
pdevinfo[2].res = &adata->res[2];
|
|
|
|
pdevinfo[3].name = "acp5x_mach";
|
|
pdevinfo[3].id = 0;
|
|
pdevinfo[3].parent = &pci->dev;
|
|
for (i = 0; i < ACP5x_DEVS; i++) {
|
|
adata->pdev[i] =
|
|
platform_device_register_full(&pdevinfo[i]);
|
|
if (IS_ERR(adata->pdev[i])) {
|
|
dev_err(&pci->dev, "cannot register %s device\n",
|
|
pdevinfo[i].name);
|
|
ret = PTR_ERR(adata->pdev[i]);
|
|
goto unregister_devs;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
dev_info(&pci->dev, "ACP audio mode : %d\n", val);
|
|
}
|
|
pm_runtime_set_autosuspend_delay(&pci->dev, 2000);
|
|
pm_runtime_use_autosuspend(&pci->dev);
|
|
pm_runtime_put_noidle(&pci->dev);
|
|
pm_runtime_allow(&pci->dev);
|
|
return 0;
|
|
|
|
unregister_devs:
|
|
for (--i; i >= 0; i--)
|
|
platform_device_unregister(adata->pdev[i]);
|
|
de_init:
|
|
if (acp5x_deinit(adata->acp5x_base))
|
|
dev_err(&pci->dev, "ACP de-init failed\n");
|
|
release_regions:
|
|
pci_release_regions(pci);
|
|
disable_pci:
|
|
pci_disable_device(pci);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int __maybe_unused snd_acp5x_suspend(struct device *dev)
|
|
{
|
|
int ret;
|
|
struct acp5x_dev_data *adata;
|
|
|
|
adata = dev_get_drvdata(dev);
|
|
ret = acp5x_deinit(adata->acp5x_base);
|
|
if (ret)
|
|
dev_err(dev, "ACP de-init failed\n");
|
|
else
|
|
dev_dbg(dev, "ACP de-initialized\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int __maybe_unused snd_acp5x_resume(struct device *dev)
|
|
{
|
|
int ret;
|
|
struct acp5x_dev_data *adata;
|
|
|
|
adata = dev_get_drvdata(dev);
|
|
ret = acp5x_init(adata->acp5x_base);
|
|
if (ret) {
|
|
dev_err(dev, "ACP init failed\n");
|
|
return ret;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static const struct dev_pm_ops acp5x_pm = {
|
|
SET_RUNTIME_PM_OPS(snd_acp5x_suspend,
|
|
snd_acp5x_resume, NULL)
|
|
SET_SYSTEM_SLEEP_PM_OPS(snd_acp5x_suspend, snd_acp5x_resume)
|
|
};
|
|
|
|
static void snd_acp5x_remove(struct pci_dev *pci)
|
|
{
|
|
struct acp5x_dev_data *adata;
|
|
int i, ret;
|
|
|
|
adata = pci_get_drvdata(pci);
|
|
if (adata->acp5x_audio_mode == ACP5x_I2S_MODE) {
|
|
for (i = 0; i < ACP5x_DEVS; i++)
|
|
platform_device_unregister(adata->pdev[i]);
|
|
}
|
|
ret = acp5x_deinit(adata->acp5x_base);
|
|
if (ret)
|
|
dev_err(&pci->dev, "ACP de-init failed\n");
|
|
pm_runtime_forbid(&pci->dev);
|
|
pm_runtime_get_noresume(&pci->dev);
|
|
pci_release_regions(pci);
|
|
pci_disable_device(pci);
|
|
}
|
|
|
|
static const struct pci_device_id snd_acp5x_ids[] = {
|
|
{ PCI_DEVICE(PCI_VENDOR_ID_AMD, ACP_DEVICE_ID),
|
|
.class = PCI_CLASS_MULTIMEDIA_OTHER << 8,
|
|
.class_mask = 0xffffff },
|
|
{ 0, },
|
|
};
|
|
MODULE_DEVICE_TABLE(pci, snd_acp5x_ids);
|
|
|
|
static struct pci_driver acp5x_driver = {
|
|
.name = KBUILD_MODNAME,
|
|
.id_table = snd_acp5x_ids,
|
|
.probe = snd_acp5x_probe,
|
|
.remove = snd_acp5x_remove,
|
|
.driver = {
|
|
.pm = &acp5x_pm,
|
|
}
|
|
};
|
|
|
|
module_pci_driver(acp5x_driver);
|
|
|
|
MODULE_AUTHOR("Vijendar.Mukunda@amd.com");
|
|
MODULE_DESCRIPTION("AMD Vangogh ACP PCI driver");
|
|
MODULE_LICENSE("GPL v2");
|