From b37219c61cbd50440d7c154d4208188fe3e5c12b Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Mon, 9 Nov 2026 14:45:03 -0500 Subject: [PATCH] Bluetooth: bcm4377: restore private PCI windows before J713 resume Shared radio PERST resets private BAR windows that generic PCI saved state does not cover. Restore the windows and subsystem control in resume_noirq, before interrupts or UNQUIESCE access the retained device. Without this, Wi-Fi returns after sleep but Bluetooth HCI commands time out. Restoring the windows permits Read Local Version to complete. Coordinate QUIESCE and recovery work with the managed host transaction, leaving other controllers on their existing path. Cover config failures and retained-path skips with a fault-injection fixture. Signed-off-by: Erik Bourget --- MAINTAINERS | 1 + drivers/bluetooth/hci_bcm4377.c | 75 ++++++++++++-- tools/asahi/bcm4377-sleep-config-tests.c | 120 ++++++++++++++++++++++ tools/asahi/bcm4377-sleep-config-tests.py | 36 +++++++ 4 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 tools/asahi/bcm4377-sleep-config-tests.c create mode 100644 tools/asahi/bcm4377-sleep-config-tests.py diff --git a/MAINTAINERS b/MAINTAINERS index 82291a41d6f20..e3386f4aea0a3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2769,6 +2769,7 @@ F: Documentation/hwmon/macsmc-hwmon.rst F: Documentation/power/apple-s2idle.rst F: arch/arm64/boot/dts/apple/ F: drivers/bluetooth/hci_bcm4377.c +F: tools/asahi/bcm4377-sleep-config-tests.* F: drivers/clk/clk-apple-nco.c F: drivers/cpufreq/apple-soc-cpufreq.c F: drivers/dma/apple-admac.c diff --git a/drivers/bluetooth/hci_bcm4377.c b/drivers/bluetooth/hci_bcm4377.c index dc24c3e5057a0..02cc8fccbc27c 100644 --- a/drivers/bluetooth/hci_bcm4377.c +++ b/drivers/bluetooth/hci_bcm4377.c @@ -19,8 +19,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -3098,6 +3100,15 @@ static int bcm4377_probe(struct pci_dev *pdev, const struct pci_device_id *id) hdev); } +/* Final release runs after every device resume, including reversible unwind. */ +static void bcm4377_sleep_finish(struct device *dev) +{ + struct bcm4377_data *bcm4377 = pci_get_drvdata(to_pci_dev(dev)); + + WRITE_ONCE(bcm4377->setup_retry_blocked, false); + bcm4377_queue_setup_retry(bcm4377); +} + static int bcm4377_resume(struct device *dev); static int bcm4377_suspend(struct device *dev) @@ -3106,6 +3117,12 @@ static int bcm4377_suspend(struct device *dev) struct bcm4377_data *bcm4377 = pci_get_drvdata(pdev); int ret; + ret = apple_radio_sleep_endpoint_prepare(dev, bcm4377_sleep_finish); + if (ret) + return ret; + if (apple_radio_sleep_owned(dev) && device_may_wakeup(dev)) + return -EBUSY; + bcm4377_resume_recovery_stop(bcm4377); WRITE_ONCE(bcm4377->setup_retry_blocked, true); cancel_delayed_work_sync(&bcm4377->setup_retry_work); @@ -3116,8 +3133,10 @@ static int bcm4377_suspend(struct device *dev) ret = hci_suspend_dev(bcm4377->hdev); if (ret) { bcm4377->resume_powered = false; - WRITE_ONCE(bcm4377->setup_retry_blocked, false); - bcm4377_queue_setup_retry(bcm4377); + if (!apple_radio_sleep_owned(dev)) { + WRITE_ONCE(bcm4377->setup_retry_blocked, false); + bcm4377_queue_setup_retry(bcm4377); + } return ret; } @@ -3152,8 +3171,11 @@ static int bcm4377_suspend(struct device *dev) /* Flush the posted write before the system goes to sleep. */ ioread32(bcm4377->bar0 + BCM4377_BAR0_SLEEP_CONTROL); bcm4377->quiesced = true; - - return 0; + /* This records the command/readflush, not a firmware idle acknowledgment. */ + ret = apple_radio_sleep_endpoint_suspended(dev); + if (ret) + bcm4377_resume(dev); + return ret; } static int bcm4377_resume(struct device *dev) @@ -3163,6 +3185,10 @@ static int bcm4377_resume(struct device *dev) unsigned long flags; int ret; + ret = apple_radio_sleep_endpoint_resume(dev); + if (ret) + return ret; + if (bcm4377->wake_armed) { disable_irq_wake(pdev->irq); bcm4377->wake_armed = false; @@ -3186,13 +3212,46 @@ static int bcm4377_resume(struct device *dev) bcm4377->resume_powered = false; ret = hci_resume_dev(bcm4377->hdev); - WRITE_ONCE(bcm4377->setup_retry_blocked, false); - if (!ret) - bcm4377_queue_setup_retry(bcm4377); + apple_radio_sleep_endpoint_resumed(dev, ret); + if (!apple_radio_sleep_owned(dev)) { + WRITE_ONCE(bcm4377->setup_retry_blocked, false); + if (!ret) + bcm4377_queue_setup_retry(bcm4377); + } return ret; } -static DEFINE_SIMPLE_DEV_PM_OPS(bcm4377_ops, bcm4377_suspend, bcm4377_resume); +static int bcm4377_resume_noirq(struct device *dev) +{ + struct bcm4377_data *bcm4377 = pci_get_drvdata(to_pci_dev(dev)); + u32 before, after; + int ret; + + if (!apple_radio_sleep_owned(dev) || !pm_resume_via_firmware()) + return 0; + + /* + * PERST resets the private BAR windows, which PCI's standard saved state + * does not cover. Restore them before the IRQ handler or UNQUIESCE can + * access the retained controller through those windows. + */ + ret = pci_read_config_dword(bcm4377->pdev, BCM4377_PCIECFG_BAR0_WINDOW1, &before); + if (!ret) + ret = bcm4377_init_cfg(bcm4377); + if (!ret) + ret = pci_read_config_dword(bcm4377->pdev, BCM4377_PCIECFG_BAR0_WINDOW1, + &after); + if (ret || after != bcm4377->hw->bar0_window1) + panic("J713 Bluetooth PCI window restore failed: %d", ret); + dev_info(dev, "private PCI windows restored after sleep (%#x -> %#x)\n", + before, after); + return 0; +} + +static const struct dev_pm_ops bcm4377_ops = { + SYSTEM_SLEEP_PM_OPS(bcm4377_suspend, bcm4377_resume) + .resume_noirq = bcm4377_resume_noirq, +}; static const struct dmi_system_id bcm4377_dmi_board_table[] = { { diff --git a/tools/asahi/bcm4377-sleep-config-tests.c b/tools/asahi/bcm4377-sleep-config-tests.c new file mode 100644 index 0000000000000..8740031cc6bd3 --- /dev/null +++ b/tools/asahi/bcm4377-sleep-config-tests.c @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U << (n)) +#define dev_info(...) ((void)0) +struct device { + int unused; +}; +struct pci_dev { + int unused; +}; +struct hardware { + u32 bar0_window1, bar0_window2, bar0_core2_window2; + bool has_bar0_core2_window2, clear_pciecfg_subsystem_ctrl_bit19; +}; +struct bcm4377_data { + struct pci_dev *pdev; + struct hardware *hw; +}; +static struct pci_dev pci; +static struct hardware hw = { 0x18002000, 0x18109000, 0x18106000, true, false }; +static struct bcm4377_data data = { &pci, &hw }; +static bool owned, physical, ignore_window; +static unsigned int ops, writes, fail; +static u32 cfg[64]; +static jmp_buf fatal; +static struct pci_dev *to_pci_dev(struct device *d) +{ + (void)d; + return &pci; +} +static struct bcm4377_data *pci_get_drvdata(struct pci_dev *d) +{ + (void)d; + return &data; +} +static bool apple_radio_sleep_owned(struct device *d) +{ + (void)d; + return owned; +} +static bool pm_resume_via_firmware(void) +{ + return physical; +} +static int pci_read_config_dword(struct pci_dev *d, unsigned int off, u32 *v) +{ + (void)d; + if (++ops == fail) + return 0x87; + *v = cfg[off / 4]; + return 0; +} +static int pci_write_config_dword(struct pci_dev *d, unsigned int off, u32 v) +{ + (void)d; + writes++; + if (++ops == fail) + return 0x87; + if (!(ignore_window && off == 0x80)) + cfg[off / 4] = v; + return 0; +} +#define panic(...) longjmp(fatal, 1) +/* ACTUAL */ +static void fresh(void) +{ + owned = physical = true; + ignore_window = false; + ops = writes = fail = 0; + memset(cfg, 0, sizeof(cfg)); + cfg[0x88 / 4] = BIT(19) | 0x80; +} +int main(void) +{ + struct device dev = {}; + + fresh(); + + assert(!bcm4377_resume_noirq(&dev)); + assert(cfg[0x80 / 4] == hw.bar0_window1); + assert(cfg[0x70 / 4] == hw.bar0_window2); + assert(cfg[0x74 / 4] == 0x18011000); + assert(cfg[0x78 / 4] == hw.bar0_core2_window2); + assert(cfg[0x84 / 4] == 0x19000000); + assert(cfg[0x88 / 4] == (BIT(19) | BIT(16) | 0x80)); + unsigned int count = ops; + + fresh(); + + owned = false; + assert(!bcm4377_resume_noirq(&dev)); + assert(!ops); + fresh(); + physical = false; + assert(!bcm4377_resume_noirq(&dev)); + assert(!ops); + for (unsigned int i = 1; i <= count; i++) { + fresh(); + fail = i; + if (!setjmp(fatal)) { + bcm4377_resume_noirq(&dev); + assert(0); + } + assert(ops == i); + } + fresh(); + ignore_window = true; + if (!setjmp(fatal)) { + bcm4377_resume_noirq(&dev); + assert(0); + } + puts("PASS: private PCI windows, retained-path skip and restore failures"); + return 0; +} diff --git a/tools/asahi/bcm4377-sleep-config-tests.py b/tools/asahi/bcm4377-sleep-config-tests.py new file mode 100644 index 0000000000000..cdbdf0f7e9054 --- /dev/null +++ b/tools/asahi/bcm4377-sleep-config-tests.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise private PCI-window restoration before Bluetooth resume IRQs.""" + +from pathlib import Path +import re +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/bluetooth/hci_bcm4377.c").read_text() +functions = [] +for name in ("bcm4377_init_cfg", "bcm4377_resume_noirq"): + match = re.search( + r"^static int " + name + r"\([^;]+?\{.*?\n\}", source, re.M | re.S + ) + assert match, name + functions.append(match.group()) +constants = "\n".join( + line for line in source.splitlines() if line.startswith("#define BCM4377_PCIECFG_") +) +code = ( + Path(__file__) + .with_suffix(".c") + .read_text() + .replace("/* ACTUAL */", constants + "\n" + "\n".join(functions)) +) +with tempfile.TemporaryDirectory() as tmp: + c = Path(tmp) / "test.c" + c.write_text(code) + exe = Path(tmp) / "test" + subprocess.run( + ["cc", "-std=gnu11", "-Wall", "-Wextra", "-Werror", str(c), "-o", str(exe)], + check=True, + ) + subprocess.run([str(exe)], check=True)