tegrakernel/kernel/nvidia/drivers/platform/tegra/tegra186-hsp.c

649 lines
16 KiB
C
Raw Permalink Normal View History

2022-02-16 09:13:02 -06:00
/*
* Copyright (c) 2016-2018, NVIDIA CORPORATION. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <linux/kernel.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/reset.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/rculist.h>
#include <linux/tegra-hsp.h>
#define NV(p) "nvidia," #p
struct tegra_hsp {
void __iomem *base;
struct reset_control *reset;
spinlock_t lock;
u8 n_sm;
u8 n_as;
u8 n_ss;
u8 n_db;
u8 n_si;
bool mbox_ie;
};
struct tegra_hsp_irq {
int irq;
u8 si_index;
u8 ie_shift;
u8 index;
u8 per_sm_ie;
};
struct tegra_hsp_sm_pair {
tegra_hsp_sm_full_fn notify_full;
struct tegra_hsp_irq full;
tegra_hsp_sm_empty_fn notify_empty;
struct tegra_hsp_irq empty;
struct device dev;
};
#define TEGRA_HSP_IR (0x304)
#define TEGRA_HSP_IE(si) (0x100 + (4 * (si)))
#define TEGRA_HSP_IE_SM_EMPTY(sm) (0x1u << (sm))
#define TEGRA_HSP_IE_SM_FULL(sm) (0x100u << (sm))
#define TEGRA_HSP_IE_DB(db) (0x10000u << (db))
#define TEGRA_HSP_IE_AS(as) (0x1000000u << (as))
#define TEGRA_HSP_DIMENSIONING 0x380
#define TEGRA_HSP_SM(sm) (0x10000 + (0x8000 * (sm)))
#define TEGRA_HSP_SM_FULL 0x80000000u
#define TEGRA_HSP_SM_IE_FULL 0x4u
#define TEGRA_HSP_SM_IE_EMPTY 0x8u
static void __iomem *tegra_hsp_reg(struct device *dev, u32 offset)
{
struct tegra_hsp *hsp = dev_get_drvdata(dev);
return hsp->base + offset;
}
static void __iomem *tegra_hsp_ie_reg(struct device *dev, u8 si)
{
return tegra_hsp_reg(dev, TEGRA_HSP_IE(si));
}
static void __iomem *tegra_hsp_ir_reg(struct device *dev)
{
return tegra_hsp_reg(dev, TEGRA_HSP_IR);
}
static inline bool tegra_hsp_irq_is_shared(struct tegra_hsp_irq *hi)
{
return (hi->si_index != 0xff);
}
static void tegra_hsp_irq_suspend(struct device *dev, struct tegra_hsp_irq *hi)
{
unsigned long flags;
if (tegra_hsp_irq_is_shared(hi)) {
struct tegra_hsp *hsp = dev_get_drvdata(dev);
void __iomem *reg = tegra_hsp_ie_reg(dev, hi->si_index);
spin_lock_irqsave(&hsp->lock, flags);
writel(readl(reg) & ~(1u << hi->ie_shift), reg);
spin_unlock_irqrestore(&hsp->lock, flags);
}
}
static void tegra_hsp_irq_resume(struct device *dev, struct tegra_hsp_irq *hi)
{
unsigned long flags;
if (tegra_hsp_irq_is_shared(hi)) {
struct tegra_hsp *hsp = dev_get_drvdata(dev);
void __iomem *reg = tegra_hsp_ie_reg(dev, hi->si_index);
spin_lock_irqsave(&hsp->lock, flags);
writel(readl(reg) | (1u << hi->ie_shift), reg);
spin_unlock_irqrestore(&hsp->lock, flags);
}
}
static void __iomem *tegra_hsp_sm_reg(struct device *dev, u32 sm)
{
return tegra_hsp_reg(dev, TEGRA_HSP_SM(sm));
}
static void tegra_hsp_enable_per_sm_irq(struct device *dev,
struct tegra_hsp_irq *hi,
int irq)
{
if (hi->per_sm_ie != 0)
writel(1, tegra_hsp_sm_reg(dev, hi->index) + hi->per_sm_ie);
else if (tegra_hsp_irq_is_shared(hi))
tegra_hsp_irq_resume(dev, (struct tegra_hsp_irq *)hi);
else if (!(irq < 0))
enable_irq(irq); /* APE HSP uses internal interrupts */
}
static void tegra_hsp_disable_per_sm_irq(struct device *dev,
struct tegra_hsp_irq *hi)
{
if (hi->per_sm_ie != 0)
writel(0, tegra_hsp_sm_reg(dev, hi->index) + hi->per_sm_ie);
else if (tegra_hsp_irq_is_shared(hi))
tegra_hsp_irq_suspend(dev, (struct tegra_hsp_irq *)hi);
else
disable_irq_nosync(hi->irq);
}
static inline bool tegra_hsp_irq_is_set(struct device *dev,
struct tegra_hsp_irq *hi)
{
return tegra_hsp_irq_is_shared(hi) ?
((readl(tegra_hsp_ir_reg(dev)) &
readl(tegra_hsp_ie_reg(dev, hi->si_index))) &
(1u << hi->ie_shift)) : true;
}
static irqreturn_t tegra_hsp_full_isr(int irq, void *data)
{
struct tegra_hsp_irq *hi = data;
struct tegra_hsp_sm_pair *pair =
container_of(hi, struct tegra_hsp_sm_pair, full);
struct device *dev = pair->dev.parent;
void __iomem *reg = tegra_hsp_sm_reg(dev, hi->index);
u32 value;
void *drv_data;
if (!tegra_hsp_irq_is_set(dev, hi))
return IRQ_NONE;
value = readl(reg);
if (!(value & TEGRA_HSP_SM_FULL))
return IRQ_NONE;
/* Empty the mailbox and clear the interrupt */
writel(0, reg);
if (pair->notify_full != NULL) {
drv_data = dev_get_drvdata(&pair->dev);
pair->notify_full(drv_data, value & ~TEGRA_HSP_SM_FULL);
}
return IRQ_HANDLED;
}
static irqreturn_t tegra_hsp_empty_isr(int irq, void *data)
{
struct tegra_hsp_irq *hi = data;
struct tegra_hsp_sm_pair *pair =
container_of(hi, struct tegra_hsp_sm_pair, empty);
struct device *dev = pair->dev.parent;
void __iomem *reg = tegra_hsp_sm_reg(dev, hi->index);
u32 value;
if (!tegra_hsp_irq_is_set(dev, hi))
return IRQ_NONE;
value = readl(reg);
if ((value & TEGRA_HSP_SM_FULL))
return IRQ_NONE;
tegra_hsp_disable_per_sm_irq(dev, &pair->empty);
pair->notify_empty(dev_get_drvdata(&pair->dev), value);
return IRQ_HANDLED;
}
static int tegra_hsp_get_shared_irq(struct device *dev, irq_handler_t handler,
unsigned long flags,
bool empty,
struct tegra_hsp_irq *hi)
{
struct platform_device *pdev = to_platform_device(dev);
struct tegra_hsp *hsp = dev_get_drvdata(dev);
u8 ie_shift;
int ret = -ENODEV;
unsigned i;
if (empty)
ie_shift = hi->index;
else
ie_shift = hi->index + 8;
flags |= IRQF_PROBE_SHARED;
for (i = 0; i < hsp->n_si; i++) {
char irqname[8];
sprintf(irqname, "shared%X", i);
hi->irq = platform_get_irq_byname(pdev, irqname);
if (hi->irq < 0)
continue;
hi->si_index = i;
hi->ie_shift = ie_shift;
ret = request_threaded_irq(hi->irq, NULL, handler, flags,
dev_name(dev), hi);
if (ret)
continue;
dev_dbg(&pdev->dev, "using shared IRQ %u (%d)\n", i, hi->irq);
tegra_hsp_enable_per_sm_irq(dev, hi, -EPERM);
/* Update interrupt masks (for shared interrupts only) */
tegra_hsp_irq_resume(dev, hi);
return 0;
}
if (ret != -EPROBE_DEFER)
dev_err(dev, "cannot get shared IRQ: %d\n", ret);
return ret;
}
static int tegra_hsp_get_sm_irq(struct device *dev, bool empty,
struct tegra_hsp_irq *hi)
{
struct platform_device *pdev = to_platform_device(dev);
irq_handler_t handler = empty ? tegra_hsp_empty_isr
: tegra_hsp_full_isr;
unsigned long flags = IRQF_ONESHOT;
char name[7];
flags |= IRQF_SHARED;
/* Look for dedicated internal IRQ */
sprintf(name, empty ? "empty%X" : "full%X", hi->index);
hi->irq = platform_get_irq_byname(pdev, name);
if (!(hi->irq < 0)) {
hi->si_index = 0xff;
if (request_threaded_irq(hi->irq, NULL, handler, flags,
dev_name(dev), hi) == 0) {
tegra_hsp_enable_per_sm_irq(dev, hi, -EPERM);
return 0;
}
}
/* Look for a free shared IRQ */
return tegra_hsp_get_shared_irq(dev, handler, flags, empty, hi);
}
static void tegra_hsp_irq_free(struct device *dev, struct tegra_hsp_irq *hi)
{
tegra_hsp_irq_suspend(dev, hi);
free_irq(hi->irq, hi);
}
static int tegra_hsp_sm_suspend(struct device *dev)
{
struct tegra_hsp_sm_pair *pair =
container_of(dev, struct tegra_hsp_sm_pair, dev);
tegra_hsp_irq_suspend(dev->parent, &pair->full);
if (pair->notify_empty != NULL)
tegra_hsp_irq_suspend(dev->parent, &pair->empty);
return 0;
}
static int tegra_hsp_sm_resume(struct device *dev)
{
struct tegra_hsp_sm_pair *pair =
container_of(dev, struct tegra_hsp_sm_pair, dev);
if (pair->notify_empty != NULL)
tegra_hsp_irq_resume(dev->parent, &pair->empty);
tegra_hsp_irq_resume(dev->parent, &pair->full);
return 0;
}
static const struct dev_pm_ops tegra_hsp_sm_pm_ops = {
.suspend_noirq = tegra_hsp_sm_suspend,
.resume_noirq = tegra_hsp_sm_resume,
};
static const struct device_type tegra_hsp_sm_dev_type = {
.name = "tegra-hsp-shared-mailbox-pair",
.pm = &tegra_hsp_sm_pm_ops,
};
static void tegra_hsp_sm_dev_release(struct device *dev)
{
struct tegra_hsp_sm_pair *pair =
container_of(dev, struct tegra_hsp_sm_pair, dev);
kfree(pair);
}
static struct tegra_hsp_sm_pair *tegra_hsp_sm_pair_request(
struct device *dev, u32 index,
tegra_hsp_sm_full_fn full, tegra_hsp_sm_empty_fn empty, void *data)
{
struct tegra_hsp *hsp = dev_get_drvdata(dev);
struct tegra_hsp_sm_pair *pair;
int err;
if (hsp == NULL)
return ERR_PTR(-EPROBE_DEFER);
if (index >= hsp->n_sm)
return ERR_PTR(-ENODEV);
pair = kzalloc(sizeof(*pair), GFP_KERNEL);
if (unlikely(pair == NULL))
return ERR_PTR(-ENOMEM);
pair->notify_full = full;
pair->notify_empty = empty;
pair->full.index = index;
pair->full.per_sm_ie = hsp->mbox_ie ? TEGRA_HSP_SM_IE_FULL : 0;
pair->empty.index = index ^ 1;
pair->empty.per_sm_ie = hsp->mbox_ie ? TEGRA_HSP_SM_IE_EMPTY : 0;
pair->dev.parent = dev;
pair->dev.type = &tegra_hsp_sm_dev_type;
pair->dev.release = tegra_hsp_sm_dev_release;
dev_set_name(&pair->dev, "%s:sm%u", dev_name(dev), index);
dev_set_drvdata(&pair->dev, data);
err = device_register(&pair->dev);
if (err) {
put_device(&pair->dev);
return ERR_PTR(err);
}
/* Get empty interrupt if necessary */
if (pair->notify_empty != NULL) {
err = tegra_hsp_get_sm_irq(dev, true, &pair->empty);
if (err)
goto error;
}
/* Get full interrupt */
err = tegra_hsp_get_sm_irq(dev, false, &pair->full);
if (err) {
if (pair->notify_empty != NULL)
tegra_hsp_irq_free(dev, &pair->empty);
goto error;
}
return pair;
error:
put_device(&pair->dev);
return ERR_PTR(err);
}
/**
* of_tegra_sm_pair_request - request a Tegra HSP shared mailbox pair from DT.
*
* @np: device node
* @index: mailbox pair entry offset in the DT property
*
* Looks up a shared mailbox pair in device tree by index. The device node
* needs a nvidia,hsp-shared-mailbox property, containing pairs of
* OF phandle and mailbox number. The OF phandle points to the Tegra HSP
* platform device. The mailbox number refers to the consumer side mailbox.
* The producer side mailbox is the other one in the same (even-odd) pair.
*/
struct tegra_hsp_sm_pair *of_tegra_hsp_sm_pair_request(
const struct device_node *np, u32 index,
tegra_hsp_sm_full_fn full, tegra_hsp_sm_empty_fn empty, void *data)
{
struct platform_device *pdev;
struct tegra_hsp_sm_pair *pair;
struct of_phandle_args smspec;
int err;
err = of_parse_phandle_with_fixed_args(np, NV(hsp-shared-mailbox), 1,
index, &smspec);
if (err)
return ERR_PTR(err);
pdev = of_find_device_by_node(smspec.np);
index = smspec.args[0];
of_node_put(smspec.np);
if (pdev == NULL)
return ERR_PTR(-EPROBE_DEFER);
pair = tegra_hsp_sm_pair_request(&pdev->dev, index, full, empty, data);
platform_device_put(pdev);
return pair;
}
EXPORT_SYMBOL(of_tegra_hsp_sm_pair_request);
/**
* of_tegra_sm_pair_by_name - request a Tegra HSP shared mailbox pair from DT.
*
* @np: device node
* @name: mailbox pair entry name
*
* Looks up a shared mailbox pair in device tree by name. The device node needs
* nvidia,hsp-shared-mailbox and nvidia-hsp-shared-mailbox-names properties.
*/
struct tegra_hsp_sm_pair *of_tegra_hsp_sm_pair_by_name(
struct device_node *np, char const *name,
tegra_hsp_sm_full_fn full, tegra_hsp_sm_empty_fn empty, void *data)
{
/* If match fails, index will be -1 and parse_phandles fails */
int index = of_property_match_string(np,
NV(hsp-shared-mailbox-names), name);
return of_tegra_hsp_sm_pair_request(np, index, full, empty, data);
}
EXPORT_SYMBOL(of_tegra_hsp_sm_pair_by_name);
/**
* tegra_hsp_sm_pair_free - free a Tegra HSP shared mailbox pair.
*/
void tegra_hsp_sm_pair_free(struct tegra_hsp_sm_pair *pair)
{
struct device *dev;
struct tegra_hsp *hsp;
if (IS_ERR_OR_NULL(pair))
return;
dev = pair->dev.parent;
hsp = dev_get_drvdata(dev);
/* Make sure that the structure is no longer referenced.
* This also implies that callbacks are no longer pending. */
tegra_hsp_irq_free(dev, &pair->full);
if (pair->notify_empty != NULL)
tegra_hsp_irq_free(dev, &pair->empty);
device_unregister(&pair->dev);
}
EXPORT_SYMBOL(tegra_hsp_sm_pair_free);
/**
* tegra_hsp_sm_pair_write - fill a Tegra HSP shared mailbox
*
* @pair: shared mailbox pair
* @value: value to fill mailbox with (only 31-bits low order bits are used)
*
* This writes a value to the producer side mailbox of a mailbox pair.
* The mailbox must be empty (especially if notify_empty callback is non-nul).
*/
void tegra_hsp_sm_pair_write(struct tegra_hsp_sm_pair *pair,
u32 value)
{
struct device *dev = pair->dev.parent;
void __iomem *reg = tegra_hsp_sm_reg(dev, pair->empty.index);
/* Ensure any pending empty ISR invocation has disabled the IRQ */
if (pair->notify_empty != NULL) {
might_sleep();
synchronize_irq(pair->empty.irq);
}
writel(TEGRA_HSP_SM_FULL | value, reg);
if (pair->notify_empty != NULL)
tegra_hsp_enable_per_sm_irq(dev, &pair->empty, pair->empty.irq);
}
EXPORT_SYMBOL(tegra_hsp_sm_pair_write);
bool tegra_hsp_sm_pair_is_empty(const struct tegra_hsp_sm_pair *pair)
{
struct device *dev = pair->dev.parent;
u32 cvalue, pvalue;
/* Ensure any pending full ISR invocation has emptied the mailbox */
synchronize_irq(pair->full.irq);
pvalue = readl(tegra_hsp_sm_reg(dev, pair->empty.index));
cvalue = readl(tegra_hsp_sm_reg(dev, pair->full.index));
return ((pvalue|cvalue) & TEGRA_HSP_SM_FULL) == 0;
}
EXPORT_SYMBOL(tegra_hsp_sm_pair_is_empty);
static int tegra_hsp_suspend(struct device *dev)
{
struct tegra_hsp *hsp = dev_get_drvdata(dev);
int ret = 0;
if (!IS_ERR(hsp->reset))
ret = reset_control_assert(hsp->reset);
return ret;
}
static int tegra_hsp_resume(struct device *dev)
{
struct tegra_hsp *hsp = dev_get_drvdata(dev);
int ret = 0;
if (!IS_ERR(hsp->reset))
ret = reset_control_deassert(hsp->reset);
return ret;
}
static const struct dev_pm_ops tegra_hsp_pm_ops = {
.suspend_noirq = tegra_hsp_suspend,
.resume_noirq = tegra_hsp_resume,
};
static const struct of_device_id tegra_hsp_of_match[] = {
{ .compatible = NV(tegra186-hsp), },
{ },
};
static int tegra_hsp_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct resource *r;
struct tegra_hsp *hsp;
u32 reg;
if (np == NULL)
return -ENXIO;
hsp = devm_kzalloc(&pdev->dev, sizeof(*hsp), GFP_KERNEL);
if (unlikely(hsp == NULL))
return -ENOMEM;
platform_set_drvdata(pdev, hsp);
spin_lock_init(&hsp->lock);
r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (r == NULL)
return -EINVAL;
if (resource_size(r) < 0x10000) {
dev_err(&pdev->dev, "memory range too short\n");
return -EINVAL;
}
hsp->base = devm_ioremap(&pdev->dev, r->start, resource_size(r));
if (hsp->base == NULL)
return -ENOMEM;
/* devm_reset_control_get() fails indistinctly with -EPROBE_DEFER */
hsp->reset = of_reset_control_get(pdev->dev.of_node, "hsp");
if (hsp->reset == ERR_PTR(-EPROBE_DEFER))
return -EPROBE_DEFER;
pm_runtime_enable(&pdev->dev);
pm_runtime_get_sync(&pdev->dev);
reg = readl(tegra_hsp_reg(&pdev->dev, TEGRA_HSP_DIMENSIONING));
hsp->n_sm = reg & 0xf;
hsp->n_ss = (reg >> 4) & 0xf;
hsp->n_as = (reg >> 8) & 0xf;
hsp->n_db = (reg >> 12) & 0xf;
hsp->n_si = (reg >> 16) & 0xf;
hsp->mbox_ie = of_property_read_bool(pdev->dev.of_node, NV(mbox-ie));
pm_runtime_put(&pdev->dev);
if ((resource_size(r) >> 16) < (1 + (hsp->n_sm / 2) + hsp->n_ss +
hsp->n_as + (hsp->n_db > 0))) {
dev_err(&pdev->dev, "memory range too short\n");
return -EINVAL;
}
return 0;
}
static __exit int tegra_hsp_remove(struct platform_device *pdev)
{
struct tegra_hsp *hsp = platform_get_drvdata(pdev);
pm_runtime_disable(&pdev->dev);
reset_control_put(hsp->reset);
return 0;
}
static struct platform_driver tegra_hsp_driver = {
.probe = tegra_hsp_probe,
.remove = __exit_p(tegra_hsp_remove),
.driver = {
.name = "tegra186-hsp",
.owner = THIS_MODULE,
.suppress_bind_attrs = true,
.of_match_table = of_match_ptr(tegra_hsp_of_match),
.pm = &tegra_hsp_pm_ops,
},
};
static int __init tegra18_hsp_init(void)
{
int ret;
ret = platform_driver_register(&tegra_hsp_driver);
if (ret)
return ret;
return 0;
}
subsys_initcall(tegra18_hsp_init);
static void __exit tegra18_hsp_exit(void)
{
}
module_exit(tegra18_hsp_exit);
MODULE_AUTHOR("Remi Denis-Courmont <remid@nvidia.com>");
MODULE_DESCRIPTION("NVIDIA Tegra 186 HSP driver");
MODULE_LICENSE("GPL");