// SPDX-License-Identifier: GPL-2.0+
/*
* PCI Hotplug Driver for PowerPC PowerNV platform.
*
* Copyright Gavin Shan, IBM Corporation 2016.
*/
#include <linux/libfdt.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/pci_hotplug.h>
#include <asm/opal.h>
#include <asm/pnv-pci.h>
#include <asm/ppc-pci.h>
#define DRIVER_VERSION "0.1"
#define DRIVER_AUTHOR "Gavin Shan, IBM Corporation"
#define DRIVER_DESC "PowerPC PowerNV PCI Hotplug Driver"
#define SLOT_WARN(sl, x...) \
((sl)->pdev ? pci_warn((sl)->pdev, x) : dev_warn(&(sl)->bus->dev, x))
struct pnv_php_event {
bool added;
struct pnv_php_slot *php_slot;
struct work_struct work;
};
static LIST_HEAD(pnv_php_slot_list);
static DEFINE_SPINLOCK(pnv_php_lock);
static void pnv_php_register(struct device_node *dn);
static void pnv_php_unregister_one(struct device_node *dn);
static void pnv_php_unregister(struct device_node *dn);
static void pnv_php_disable_irq(struct pnv_php_slot *php_slot,
bool disable_device)
{
struct pci_dev *pdev = php_slot->pdev;
int irq = php_slot->irq;
u16 ctrl;
if (php_slot->irq > 0) {
pcie_capability_read_word(pdev, PCI_EXP_SLTCTL, &ctrl);
ctrl &= ~(PCI_EXP_SLTCTL_HPIE |
PCI_EXP_SLTCTL_PDCE |
PCI_EXP_SLTCTL_DLLSCE);
pcie_capability_write_word(pdev, PCI_EXP_SLTCTL, ctrl);
free_irq(php_slot->irq, php_slot);
php_slot->irq = 0;
}
if (php_slot->wq) {
destroy_workqueue(php_slot->wq);
php_slot->wq = NULL;
}
if (disable_device || irq > 0) {
if (pdev->msix_enabled)
pci_disable_msix(pdev);
else if (pdev->msi_enabled)
pci_disable_msi(pdev);
pci_disable_device(pdev);
}
}
static void pnv_php_free_slot(struct kref *kref)
{
struct pnv_php_slot *php_slot = container_of(kref,
struct pnv_php_slot, kref);
WARN_ON(!list_empty(&php_slot->children));
pnv_php_disable_irq(php_slot, false);
kfree(php_slot->name);
kfree(php_slot);
}
static inline void pnv_php_put_slot(struct pnv_php_slot *php_slot)
{
if (!php_slot)
return;
kref_put(&php_slot->kref, pnv_php_free_slot);
}
static struct pnv_php_slot *pnv_php_match(struct device_node *dn,
struct pnv_php_slot *php_slot)
{
struct pnv_php_slot *target, *tmp;
if (php_slot->dn == dn) {
kref_get(&php_slot->kref);
return php_slot;
}
list_for_each_entry(tmp, &php_slot->children, link) {
target = pnv_php_match(dn, tmp);
if (target)
return target;
}
return NULL;
}
struct pnv_php_slot *pnv_php_find_slot(struct device_node *dn)
{
struct pnv_php_slot *php_slot, *tmp;
unsigned long flags;
spin_lock_irqsave(&pnv_php_lock, flags);
list_for_each_entry(tmp, &pnv_php_slot_list, link) {
php_slot = pnv_php_match(dn, tmp);
if (php_slot) {
spin_unlock_irqrestore(&pnv_php_lock, flags);
return php_slot;
}
}
spin_unlock_irqrestore(&pnv_php_lock, flags);
return NULL;
}
EXPORT_SYMBOL_GPL(pnv_php_find_slot);
/*
* Remove pdn for all children of the indicated device node.
* The function should remove pdn in a depth-first manner.
*/
static void pnv_php_rmv_pdns(struct device_node *dn)
{
struct device_node *child;
for_each_child_of_node(dn, child) {
pnv_php_rmv_pdns(child);
pci_remove_device_node_info(child);
}
}
/*
* Detach all child nodes of the indicated device nodes. The
* function should handle device nodes in depth-first manner.
*
* We should not invoke of_node_release() as the memory for
* individual device node is part of large memory block. The
* large block is allocated from memblock (system bootup) or
* kmalloc() when unflattening the device tree by OF changeset.
* We can not free the large block allocated from memblock. For
* later case, it should be released at once.
*/
static void pnv_php_detach_device_nodes(struct device_node *parent)
{
struct device_node *dn;
for_each_child_of_node(parent, dn) {
pnv_php_detach_device_nodes(dn);
of_node_put(dn);
of_detach_node(dn);
}
}
static void pnv_php_rmv_devtree(struct pnv_php_slot *php_slot)
{
pnv_php_rmv_pdns(php_slot->dn);
/*
* Decrease the refcount if the device nodes were created
* through OF changeset before detaching them.
*/
if (php_slot->fdt)
of_changeset_destroy(&php_slot->ocs);
pnv_php_detach_device_nodes(php_slot->dn);
if (php_slot->fdt) {
kfree(php_slot->dt);
kfree(php_slot->fdt);
php_slot->dt = NULL;
php_slot->dn->child = NULL;
php_slot->fdt = NULL;
}
}
/*
* As the nodes in OF changeset are applied in reverse order, we
* need revert the nodes in advance so that we have correct node
* order after the changeset is applied.
*/
static void pnv_php_reverse_nodes(struct device_node *parent)
{
struct device_node *child, *next;
/* In-depth first */
for_each_child_of_node(parent, child)
pnv_php_reverse_nodes(child);
/* Reverse the nodes in the child list */
child = parent->child;
parent->child = NULL;
while (child) {
next = child->sibling;