| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * PCI Express Link Bandwidth Notification services driver |
| * Author: Alexandru Gagniuc <mr.nuke.me@gmail.com> |
| * |
| * Copyright (C) 2019, Dell Inc |
| * |
| * The PCIe Link Bandwidth Notification provides a way to notify the |
| * operating system when the link width or data rate changes. This |
| * capability is required for all root ports and downstream ports |
| * supporting links wider than x1 and/or multiple link speeds. |
| * |
| * This service port driver hooks into the bandwidth notification interrupt |
| * and warns when links become degraded in operation. |
| */ |
| |
| #define dev_fmt(fmt) "bw_notification: " fmt |
| |
| #include "../pci.h" |
| #include "portdrv.h" |
| |
| static bool pcie_link_bandwidth_notification_supported(struct pci_dev *dev) |
| { |
| int ret; |
| u32 lnk_cap; |
| |
| ret = pcie_capability_read_dword(dev, PCI_EXP_LNKCAP, &lnk_cap); |
| return (ret == PCIBIOS_SUCCESSFUL) && (lnk_cap & PCI_EXP_LNKCAP_LBNC); |
| } |
| |
| static void pcie_enable_link_bandwidth_notification(struct pci_dev *dev) |
| { |
| u16 lnk_ctl; |
| |
| pcie_capability_write_word(dev, PCI_EXP_LNKSTA, PCI_EXP_LNKSTA_LBMS); |
| |
| pcie_capability_read_word(dev, PCI_EXP_LNKCTL, &lnk_ctl); |
| lnk_ctl |= PCI_EXP_LNKCTL_LBMIE; |
| pcie_capability_write_word(dev, PCI_EXP_LNKCTL, lnk_ctl); |
| } |
| |
| static void pcie_disable_link_bandwidth_notification(struct pci_dev *dev) |
| { |
| u16 lnk_ctl; |
| |
| pcie_capability_read_word(dev, PCI_EXP_LNKCTL, &lnk_ctl); |
| lnk_ctl &= ~PCI_EXP_LNKCTL_LBMIE; |
| pcie_capability_write_word(dev, PCI_EXP_LNKCTL, lnk_ctl); |
| } |
| |
| static irqreturn_t pcie_bw_notification_irq(int irq, void *context) |
| { |
| struct pcie_device *srv = context; |
| struct pci_dev *port = srv->port; |
| u16 link_status, events; |
| int ret; |
| |
| ret = pcie_capability_read_word(port, PCI_EXP_LNKSTA, &link_status); |
| events = link_status & PCI_EXP_LNKSTA_LBMS; |
| |
| if (ret != PCIBIOS_SUCCESSFUL || !events) |
| return IRQ_NONE; |
| |
| pcie_capability_write_word(port, PCI_EXP_LNKSTA, events); |
| pcie_update_link_speed(port->subordinate, link_status); |
| return IRQ_WAKE_THREAD; |
| } |
| |
| static irqreturn_t pcie_bw_notification_handler(int irq, void *context) |
| { |
| struct pcie_device *srv = context; |
| struct pci_dev *port = srv->port; |
| struct pci_dev *dev; |
| |
| /* |
| * Print status from downstream devices, not this root port or |
| * downstream switch port. |
| */ |
| down_read(&pci_bus_sem); |
| list_for_each_entry(dev, &port->subordinate->devices, bus_list) |
| pcie_report_downtraining(dev); |
| up_read(&pci_bus_sem); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int pcie_bandwidth_notification_probe(struct pcie_device *srv) |
| { |
| int ret; |
| |
| /* Single-width or single-speed ports do not have to support this. */ |
| if (!pcie_link_bandwidth_notification_supported(srv->port)) |
| return -ENODEV; |
| |
| ret = request_threaded_irq(srv->irq, pcie_bw_notification_irq, |
| pcie_bw_notification_handler, |
| IRQF_SHARED, "PCIe BW notif", srv); |
| if (ret) |
| return ret; |
| |
| pcie_enable_link_bandwidth_notification(srv->port); |
| pci_info(srv->port, "enabled with IRQ %d\n", srv->irq); |
| |
| return 0; |
| } |
| |
| static void pcie_bandwidth_notification_remove(struct pcie_device *srv) |
| { |
| pcie_disable_link_bandwidth_notification(srv->port); |
| free_irq(srv->irq, srv); |
| } |
| |
| static int pcie_bandwidth_notification_suspend(struct pcie_device *srv) |
| { |
| pcie_disable_link_bandwidth_notification(srv->port); |
| return 0; |
| } |
| |
| static int pcie_bandwidth_notification_resume(struct pcie_device *srv) |
| { |
| pcie_enable_link_bandwidth_notification(srv->port); |
| return 0; |
| } |
| |
| static struct pcie_port_service_driver pcie_bandwidth_notification_driver = { |
| .name = "pcie_bw_notification", |
| .port_type = PCIE_ANY_PORT, |
| .service = PCIE_PORT_SERVICE_BWNOTIF, |
| .probe = pcie_bandwidth_notification_probe, |
| .suspend = pcie_bandwidth_notification_suspend, |
| .resume = pcie_bandwidth_notification_resume, |
| .remove = pcie_bandwidth_notification_remove, |
| }; |
| |
| int __init pcie_bandwidth_notification_init(void) |
| { |
| return pcie_port_service_register(&pcie_bandwidth_notification_driver); |
| } |