#include <linux/wait.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
+#include <linux/acpi.h>
#include <linux/unaligned.h>
#include <linux/devcoredump.h>
BTINTEL_PCIE_D3
};
+enum {
+ BTINTEL_PCIE_DSM_SET_RESET_TIMING = 1,
+ BTINTEL_PCIE_DSM_GET_RESET_TIMING = 2,
+ BTINTEL_PCIE_DSM_BT_PLDR_CONFIG = 3,
+ BTINTEL_PCIE_DSM_GET_RESET_TYPE = 4,
+ BTINTEL_PCIE_DSM_DYNAMIC_PLDR = 5,
+ BTINTEL_PCIE_DSM_GET_RESET_METHOD = 6,
+ BTINTEL_PCIE_DSM_SET_PLDR_DELAY = 7,
+};
+
+enum btintel_dsm_internal_product_reset_mode {
+ BTINTEL_PCIE_DSM_PLDR_MODE_EN_PROD_RESET = BIT(0),
+ BTINTEL_PCIE_DSM_PLDR_MODE_EN_WIFI_FLR = BIT(1),
+ BTINTEL_PCIE_DSM_PLDR_MODE_EN_BT_OFF_ON = BIT(2),
+};
+
/* Structure for dbgc fragment buffer
* @buf_addr_lsb: LSB of the buffer's physical address
* @buf_addr_msb: MSB of the buffer's physical address
struct btintel_pcie_dbgc_ctxt_buf bufs[BTINTEL_PCIE_DBGC_BUFFER_COUNT];
};
-struct btintel_pcie_removal {
- struct pci_dev *pdev;
- struct work_struct work;
-};
-
static LIST_HEAD(btintel_pcie_recovery_list);
static DEFINE_SPINLOCK(btintel_pcie_recovery_lock);
}
static int btintel_pcie_setup_hdev(struct btintel_pcie_data *data);
+static void btintel_pcie_reset(struct hci_dev *hdev);
-static void btintel_pcie_removal_work(struct work_struct *wk)
+static int btintel_pcie_acpi_reset_method(struct btintel_pcie_data *data)
{
- struct btintel_pcie_removal *removal =
- container_of(wk, struct btintel_pcie_removal, work);
- struct pci_dev *pdev = removal->pdev;
- struct btintel_pcie_data *data;
+ union acpi_object *obj, argv4;
+ acpi_handle handle;
+ int ret;
+ struct pldr_mode {
+ __le16 cmd_type;
+ __le16 cmd_payload;
+ } __packed;
+
+ /* set 1 for _PRR mode
+ * Product Reset (PLDR Abort flow)
+ */
+ static const struct pldr_mode mode = {
+ .cmd_type = cpu_to_le16(1),
+ .cmd_payload = cpu_to_le16(BTINTEL_PCIE_DSM_PLDR_MODE_EN_PROD_RESET |
+ BTINTEL_PCIE_DSM_PLDR_MODE_EN_WIFI_FLR),
+ };
+ struct hci_dev *hdev = data->hdev;
+
+ handle = ACPI_HANDLE(GET_HCIDEV_DEV(data->hdev));
+ if (!handle) {
+ bt_dev_err(data->hdev, "No support for bluetooth device in ACPI firmware");
+ return -EACCES;
+ }
+
+ if (!acpi_has_method(handle, "_PRR")) {
+ bt_dev_err(data->hdev, "No support for _PRR ACPI method, cold boot");
+ return -ENODEV;
+ }
+
+ argv4.buffer.type = ACPI_TYPE_BUFFER;
+ argv4.buffer.length = sizeof(mode);
+ argv4.buffer.pointer = (void *)&mode;
+
+ obj = acpi_evaluate_dsm(handle, &btintel_guid_dsm, 0,
+ BTINTEL_PCIE_DSM_DYNAMIC_PLDR, &argv4);
+ if (!obj) {
+ bt_dev_err(data->hdev, "Failed to call dsm to set reset method");
+ return -EIO;
+ }
+ ACPI_FREE(obj);
+
+ pci_dev_lock(data->pdev);
+ pci_save_state(data->pdev);
+ ret = btintel_acpi_reset_method(hdev);
+ if (ret)
+ bt_dev_err(data->hdev, "ACPI _PRR reset failed (%d), PLDR incomplete",
+ ret);
+ pci_restore_state(data->pdev);
+ pci_dev_unlock(data->pdev);
+ return ret;
+}
+
+static void btintel_pcie_perform_pldr(struct btintel_pcie_data *data)
+{
+ struct pci_dev *pdev = data->pdev;
+ struct pci_dev *wifi = NULL;
+ struct pci_bus *bus;
+ int ret;
+ /* on integrated we have to look up by ID (same bus) */
+ static const struct pci_device_id wifi_device_ids[] = {
+ #define WIFI_DEV(_id) { PCI_DEVICE(PCI_VENDOR_ID_INTEL, _id) }
+ WIFI_DEV(0xA840), /* LNL */
+ WIFI_DEV(0xE440), /* PTL-P */
+ WIFI_DEV(0xE340), /* PTL-H */
+ WIFI_DEV(0xD340), /* NVL-H */
+ WIFI_DEV(0x6E70), /* NVL-S */
+ WIFI_DEV(0x4D40), /* WCL */
+ {}
+ };
+ struct pci_dev *tmp = NULL;
+
+ bus = pdev->bus;
+ if (!bus)
+ return;
+
+ list_for_each_entry(tmp, &bus->devices, bus_list) {
+ if (pci_match_id(wifi_device_ids, tmp)) {
+ wifi = pci_dev_get(tmp);
+ break;
+ }
+ }
+
+ if (wifi)
+ device_release_driver(&wifi->dev);
+
+ /* Wi-Fi is fully unbound before the reset and fully reprobed after
+ * the normal PCI probe path handles all state setup from scratch.
+ * BT needs pci_save_state()/pci_restore_state() because the BT driver
+ * is still partially attached when the _PRR runs (it hasn't been unbound yet).
+ * The PCI device needs to remain minimally functional so that
+ * device_reprobe(&pdev->dev) can work afterward
+ */
+ ret = btintel_pcie_acpi_reset_method(data);
+
+ if (wifi) {
+ if (device_reprobe(&wifi->dev))
+ BT_ERR("WiFi reprobe failed for BDF:%s", pci_name(wifi));
+ pci_dev_put(wifi);
+ }
+
+ if (!ret) {
+ if (device_reprobe(&pdev->dev))
+ BT_ERR("BT reprobe failed for BDF:%s", pci_name(pdev));
+ }
+}
+
+static void btintel_pcie_reset_work(struct work_struct *wk)
+{
+ struct btintel_pcie_data *data =
+ container_of(wk, struct btintel_pcie_data, reset_work);
+ struct pci_dev *pdev = data->pdev;
int err;
pci_lock_rescan_remove();
if (!pdev->bus)
- goto error;
+ goto out;
- data = pci_get_drvdata(pdev);
+ if (!data)
+ goto out;
btintel_pcie_disable_interrupts(data);
btintel_pcie_synchronize_irqs(data);
flush_work(&data->rx_work);
bt_dev_dbg(data->hdev, "Release bluetooth interface");
+ if (data->reset_type == BTINTEL_PCIE_IOSF_PRR_PLDR) {
+ /* This function holds pci_lock_rescan_remove(), which acquires
+ * pci_rescan_remove_lock. This mutex serializes against PCI device
+ * addition/removal (hotplug), so no device can be added to or
+ * removed from the bus list while this code runs.
+ */
+ btintel_pcie_perform_pldr(data);
+ goto out;
+ }
btintel_pcie_release_hdev(data);
err = pci_reset_function(pdev);
if (err) {
BT_ERR("Failed resetting the pcie device (%d)", err);
- goto error;
+ goto out;
}
btintel_pcie_enable_interrupts(data);
if (err) {
BT_ERR("Failed to enable bluetooth hardware after reset (%d)",
err);
- goto error;
+ goto out;
}
btintel_pcie_reset_ia(data);
err = btintel_pcie_setup_hdev(data);
if (err) {
BT_ERR("Failed registering hdev (%d)", err);
- goto error;
+ goto out;
}
-error:
+out:
pci_dev_put(pdev);
pci_unlock_rescan_remove();
- kfree(removal);
}
static void btintel_pcie_reset(struct hci_dev *hdev)
{
- struct btintel_pcie_removal *removal;
struct btintel_pcie_data *data;
data = hci_get_drvdata(hdev);
if (test_and_set_bit(BTINTEL_PCIE_RECOVERY_IN_PROGRESS, &data->flags))
return;
- removal = kzalloc_obj(*removal, GFP_ATOMIC);
- if (!removal)
- return;
-
- removal->pdev = data->pdev;
- INIT_WORK(&removal->work, btintel_pcie_removal_work);
- pci_dev_get(removal->pdev);
- schedule_work(&removal->work);
+ pci_dev_get(data->pdev);
+ schedule_work(&data->reset_work);
}
static void btintel_pcie_hw_error(struct hci_dev *hdev, u8 code)
struct pci_dev *pdev = dev_data->pdev;
time64_t retry_window;
- if (code == 0x13) {
- bt_dev_err(hdev, "Encountered top exception");
- return;
- }
+ btintel_pcie_dump_debug_registers(hdev);
data = btintel_pcie_get_recovery(pdev, &hdev->dev);
if (!data)
return;
+ if (code == 0x13)
+ dev_data->reset_type = BTINTEL_PCIE_IOSF_PRR_PLDR;
+ else
+ dev_data->reset_type = BTINTEL_PCIE_IOSF_PRR_FLR;
+
+ bt_dev_err(hdev, "Encountered exception err:0x%x triggering: %s", code,
+ dev_data->reset_type == BTINTEL_PCIE_IOSF_PRR_PLDR ? "PLDR" : "FLR");
retry_window = ktime_get_boottime_seconds() - data->last_error;
if (retry_window < BTINTEL_PCIE_RESET_WINDOW_SECS &&
skb_queue_head_init(&data->rx_skb_q);
INIT_WORK(&data->rx_work, btintel_pcie_rx_work);
+ INIT_WORK(&data->reset_work, btintel_pcie_reset_work);
data->boot_stage_cache = 0x00;
data->img_resp_cache = 0x00;
-
+ /* FLR can be invoked by echoing to debugfs path, so explicitly
+ * initialized
+ */
+ data->reset_type = BTINTEL_PCIE_IOSF_PRR_FLR;
err = btintel_pcie_config_pcie(pdev, data);
if (err)
goto exit_error;
struct btintel_pcie_data *data;
data = pci_get_drvdata(pdev);
+ if (!data) {
+ BT_WARN("PCI driver data is NULL, aborting remove");
+ return;
+ }
+
+ /* Cancel pending reset work. Skip only when remove() is called from
+ * within the reset work itself (PLDR device_reprobe path) to avoid
+ * deadlock. current_work() returns the work_struct of the caller if
+ * we are in a workqueue context.
+ */
+ if (current_work() != &data->reset_work)
+ cancel_work_sync(&data->reset_work);
btintel_pcie_disable_interrupts(data);
if (data->pm_sx_event == PM_EVENT_FREEZE ||
data->pm_sx_event == PM_EVENT_HIBERNATE) {
set_bit(BTINTEL_PCIE_CORE_HALTED, &data->flags);
+ data->reset_type = BTINTEL_PCIE_IOSF_PRR_FLR;
btintel_pcie_reset(data->hdev);
return 0;
}