Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions drivers/net/wireless/mediatek/mt76/mt7996/mac.c
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,26 @@ void mt7996_update_channel(struct mt76_phy *mphy)
state->noise = -(phy->noise >> 4);
}

/* True when the PCIe endpoint is no longer reachable: either the driver has
* flagged it removed (PCIe .error_detected handler / unplug) or the AER core
* has taken the channel offline. Any MMIO access past this point would read
* from a dead register window and raise an asynchronous external abort /
* SError on arm64 (mtk-pcie-gen3), which is an unconditional kernel panic.
*/
static bool mt7996_dev_gone(struct mt7996_dev *dev)
{
struct mt76_dev *mdev = &dev->mt76;

if (test_bit(MT76_REMOVED, &dev->mphy.state))
return true;

if (dev_is_pci(mdev->dev) &&
pci_channel_offline(to_pci_dev(mdev->dev)))
return true;

return false;
}

static bool
mt7996_wait_reset_state(struct mt7996_dev *dev, u32 state)
{
Expand Down Expand Up @@ -2469,6 +2489,13 @@ void mt7996_mac_reset_work(struct work_struct *work)
dev = container_of(work, struct mt7996_dev, reset_work);
hw = mt76_hw(dev);

if (mt7996_dev_gone(dev)) {
dev_warn(dev->mt76.dev,
"%s: PCIe endpoint unreachable, skipping MAC reset\n",
wiphy_name(hw->wiphy));
return;
}

/* chip full reset */
if (dev->recovery.restart) {
/* disable WA/WM WDT */
Expand Down Expand Up @@ -2700,6 +2727,14 @@ void mt7996_reset(struct mt7996_dev *dev)
if (dev->recovery.hw_full_reset)
return;

/* Endpoint surprise-removed (e.g. SFP hot-pull browning out the WiFi
* rail on BPI-R4): driving the recovery worker would touch the now-
* unreachable MMIO window and raise an SError, turning a WiFi hiccup
* into a kernel panic. Bail out instead.
*/
if (mt7996_dev_gone(dev))
return;

/* wm/wa exception: do full recovery */
if (READ_ONCE(dev->recovery.state) & MT_MCU_CMD_WDT_MASK) {
dev->recovery.restart = true;
Expand Down
12 changes: 12 additions & 0 deletions drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
#define MT7996_RRO_MSDU_PG_CR_CNT 8
#define MT7996_RRO_MSDU_PG_SIZE_PER_CR 0x10000

struct mt7996_dev;
struct mt7996_vif;
struct mt7996_sta;
struct mt7996_dfs_pulse;
Expand Down Expand Up @@ -313,6 +314,11 @@ struct mt7996_hif {

enum pci_bus_speed speed;
enum pcie_link_width width;

/* Owning device, set when the primary PCIe function claims this hif.
* Lets the hif2 .error_detected handler reach the mt76 device.
*/
struct mt7996_dev *mt7996;
};

#define WED_RRO_ADDR_SIGNATURE_MASK GENMASK(31, 24)
Expand Down Expand Up @@ -413,6 +419,12 @@ struct mt7996_dev {
struct wiphy_radio_freq_range radio_freqs[MT7996_MAX_RADIOS];

struct mt7996_hif *hif2;

/* Optional board GPIO wired to the WiFi card reset/power-enable (in
* parallel with the manual switch), used for hard recovery after a PCIe
* error when PERST# alone does not revive a brownout-wedged MCU.
*/
struct gpio_desc *reset_gpio;
struct mt7996_reg_desc reg;
u8 q_id[MT7996_MAX_QUEUE];
u32 q_int_mask[MT7996_MAX_QUEUE];
Expand Down
154 changes: 154 additions & 0 deletions drivers/net/wireless/mediatek/mt76/mt7996/pci.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Copyright (C) 2022 MediaTek Inc.
*/

#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/pci.h>
Expand Down Expand Up @@ -139,6 +141,14 @@ static int mt7996_pci_probe(struct pci_dev *pdev,
mt7996_wfsys_reset(dev);
hif2 = mt7996_pci_init_hif2(pdev);
dev->hif2 = hif2;
if (hif2)
hif2->mt7996 = dev;

/* Optional reset line for slot_reset() recovery; absent on stock DT. */
dev->reset_gpio = devm_gpiod_get_optional(&pdev->dev, "reset",
GPIOD_OUT_LOW);
if (IS_ERR(dev->reset_gpio))
dev->reset_gpio = NULL;

mt76_npu_init(mdev, pci_resource_start(pdev, 0),
pdev->bus && pci_domain_nr(pdev->bus) ? 3 : 2);
Expand Down Expand Up @@ -196,6 +206,11 @@ static int mt7996_pci_probe(struct pci_dev *pdev,
if (ret)
goto free_hif2_irq;

/* Snapshot config space so slot_reset() can restore BARs and the
* command register after a PCIe bus reset clobbers them.
*/
pci_save_state(pdev);

return 0;

free_hif2_irq:
Expand Down Expand Up @@ -241,18 +256,157 @@ static void mt7996_pci_remove(struct pci_dev *pdev)
mt7996_unregister_device(dev);
}

static void mt7996_pci_mark_removed(struct mt76_dev *mdev)
{
/* Flag the device gone so every liveness check - including
* mt7996_dev_gone() guarding the reset worker - short-circuits before
* it can touch the now-unreachable MMIO window, and wake anyone blocked
* on an MCU response so it fails fast instead of stalling to timeout.
*/
set_bit(MT76_REMOVED, &mdev->phy.state);
wake_up(&mdev->mcu.wait);
}

static pci_ers_result_t
mt7996_pci_error_detected(struct pci_dev *pdev, pci_channel_state_t state)
{
struct mt76_dev *mdev = pci_get_drvdata(pdev);

if (state == pci_channel_io_perm_failure)
return PCI_ERS_RESULT_DISCONNECT;

dev_err(&pdev->dev, "PCIe error detected (state=%u), requesting slot reset\n",
state);

mt7996_pci_mark_removed(mdev);

/* Ask the PCIe core to perform a secondary bus reset (PERST#) and then
* call .slot_reset, where the chip is reinitialised. If recovery there
* fails, slot_reset returns DISCONNECT and we end up in the same safe
* state as before - WiFi down, the rest of the system alive.
*/
return PCI_ERS_RESULT_NEED_RESET;
}

static pci_ers_result_t
mt7996_hif_error_detected(struct pci_dev *pdev, pci_channel_state_t state)
{
struct mt7996_hif *hif = pci_get_drvdata(pdev);
struct mt7996_dev *dev = hif ? hif->mt7996 : NULL;

if (state == pci_channel_io_perm_failure)
return PCI_ERS_RESULT_DISCONNECT;

dev_err(&pdev->dev, "PCIe error detected (state=%u) on hif2\n", state);

/* hif2 is the second PCIe function of the same physical chip; a link
* loss here downs the whole device. Mark the owning mt76 device removed
* so its reset worker bails out too, then request the shared slot reset.
* If the primary function has not claimed this hif yet, there is nothing
* to reinitialise from here - just disconnect this function.
*/
if (!dev)
return PCI_ERS_RESULT_DISCONNECT;

mt7996_pci_mark_removed(&dev->mt76);

return PCI_ERS_RESULT_NEED_RESET;
}

static pci_ers_result_t mt7996_pci_slot_reset(struct pci_dev *pdev)
{
struct mt76_dev *mdev = pci_get_drvdata(pdev);
struct mt7996_dev *dev = container_of(mdev, struct mt7996_dev, mt76);

dev_info(&pdev->dev, "PCIe slot reset, reinitialising device\n");

/* The secondary bus reset clobbered config space; restore BARs and the
* command register before any MMIO, and re-enable bus mastering.
*/
pci_restore_state(pdev);
pci_set_master(pdev);

/* Optional hard power-cycle of the card via a board GPIO wired in
* parallel with the manual power switch. PERST# from the bus reset above
* may not clear an MCU wedged by a supply brownout; cycling the rail
* does. No-op when no reset-gpios is described in DT.
*/
if (dev->reset_gpio) {
gpiod_set_value_cansleep(dev->reset_gpio, 1);
msleep(20);
gpiod_set_value_cansleep(dev->reset_gpio, 0);
msleep(50);
}

/* Confirm the chip is actually back on the bus before promising the
* core a recovery; a dead read returns all-ones.
*/
if (mt76_rr(dev, MT_HW_REV) == 0xffffffff) {
dev_err(&pdev->dev, "device unreachable after slot reset\n");
return PCI_ERS_RESULT_DISCONNECT;
}

/* Config space is valid and the channel is online again, so the
* mt7996_dev_gone() guard must stop tripping for the reinit to run.
*/
clear_bit(MT76_REMOVED, &mdev->phy.state);

/* Reload firmware and rebuild DMA/NAPI/token state through the existing
* full chip reset path (mt7996_mac_reset_work -> mt7996_mac_full_reset).
*/
dev->recovery.restart = true;
queue_work(mdev->wq, &dev->reset_work);

return PCI_ERS_RESULT_RECOVERED;
}

static pci_ers_result_t mt7996_hif_slot_reset(struct pci_dev *pdev)
{
/* hif2 shares the chip with the primary function, which drives the
* actual reinit from its own slot_reset. Here we only need this
* function's own config space restored so the reinit can reach the
* hif2 register window.
*/
pci_restore_state(pdev);
pci_set_master(pdev);

return PCI_ERS_RESULT_RECOVERED;
}

static void mt7996_pci_io_resume(struct pci_dev *pdev)
{
/* The chip reinit queued from slot_reset ends in ieee80211_restart_hw(),
* which restarts the queues, so nothing extra is required here.
*/
dev_info(&pdev->dev, "PCIe recovery complete\n");
}

static const struct pci_error_handlers mt7996_pci_err_handler = {
.error_detected = mt7996_pci_error_detected,
.slot_reset = mt7996_pci_slot_reset,
.resume = mt7996_pci_io_resume,
};

static const struct pci_error_handlers mt7996_hif_err_handler = {
.error_detected = mt7996_hif_error_detected,
.slot_reset = mt7996_hif_slot_reset,
.resume = mt7996_pci_io_resume,
};

struct pci_driver mt7996_hif_driver = {
.name = KBUILD_MODNAME "_hif",
.id_table = mt7996_hif_device_table,
.probe = mt7996_pci_probe,
.remove = mt7996_hif_remove,
.err_handler = &mt7996_hif_err_handler,
};

struct pci_driver mt7996_pci_driver = {
.name = KBUILD_MODNAME,
.id_table = mt7996_pci_device_table,
.probe = mt7996_pci_probe,
.remove = mt7996_pci_remove,
.err_handler = &mt7996_pci_err_handler,
};

MODULE_DEVICE_TABLE(pci, mt7996_pci_device_table);
Expand Down