Thread (7 messages) flat view 7 messages, 3 authors, 2d ago
WARM2d

[PATCH v4 4/4] Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072

From: Zijun Hu <hidden>
Date: 2026-09-10 14:35:54
Also in: lkml
Subsystem: bluetooth drivers, the rest · Maintainers: Marcel Holtmann, Luiz Augusto von Dentz, Linus Torvalds

QCC2072 has a Peripheral (PERI) subsystem to support the BT function
unit, the transport wire is owned by PERI and carries both BT-HCI and
PERI-HCI frames, as shown below:

                 _______________________________
       USB I/F	|	   internal link	|
BTHOST --------	|---- PERI -------------- BT	|
		|_______________________________|
                (other on-chip subsystems omitted)

PERI has its own command/event/ACL traffic, memdump, and firmware
(PATCH and NVM), all different from BT's own, and BTHOST needs to
interact with PERI directly.

To support multi-subsystem controllers, add btusb_qcom.c/.h as a new
transport-specific design:

- Multi-subsystem memdump collection.
- Generic PERI event handling, and sending a PERI frame then
  syncing an event sequence.
- Downloading PERI firmware (rampatch and NVM).
- Recovery on command timeout or hardware error event.

BTW, it also supports BT-only chips.

/sys/kernel/debug/usb/devices:
T:  Bus=03 Lev=02 Prnt=02 Port=02 Cnt=01 Dev#=  5 Spd=12   MxCh= 0
D:  Ver= 1.10 Cls=e0(wlcon) Sub=01 Prot=01 MxPS=64 #Cfgs=  1
P:  Vendor=0cf3 ProdID=ea00 Rev= 0.01
C:* #Ifs= 2 Cfg#= 1 Atr=e0 MxPwr=100mA
I:* If#= 0 Alt= 0 #EPs= 3 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=81(I) Atr=03(Int.) MxPS=  64 Ivl=1ms
E:  Ad=82(I) Atr=02(Bulk) MxPS=  64 Ivl=0ms
E:  Ad=02(O) Atr=02(Bulk) MxPS=  64 Ivl=0ms
I:* If#= 1 Alt= 0 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=   0 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=   0 Ivl=1ms
I:  If#= 1 Alt= 1 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=   9 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=   9 Ivl=1ms
I:  If#= 1 Alt= 2 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=  17 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=  17 Ivl=1ms
I:  If#= 1 Alt= 3 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=  25 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=  25 Ivl=1ms
I:  If#= 1 Alt= 4 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=  33 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=  33 Ivl=1ms
I:  If#= 1 Alt= 5 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=  49 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=  49 Ivl=1ms
I:  If#= 1 Alt= 6 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=  63 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=  63 Ivl=1ms
I:  If#= 1 Alt= 7 #EPs= 2 Cls=e0(wlcon) Sub=01 Prot=01 Driver=btusb
E:  Ad=83(I) Atr=01(Isoc) MxPS=  65 Ivl=1ms
E:  Ad=03(O) Atr=01(Isoc) MxPS=  65 Ivl=1ms

Signed-off-by: Zijun Hu <redacted>
---
 drivers/bluetooth/Kconfig      |   14 +
 drivers/bluetooth/Makefile     |    1 +
 drivers/bluetooth/btusb_main.c |  121 ++
 drivers/bluetooth/btusb_qcom.c | 4542 ++++++++++++++++++++++++++++++++++++++++
 drivers/bluetooth/btusb_qcom.h |   99 +
 5 files changed, 4777 insertions(+)
diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig
index 2d6a3117e387..db0189ca3cbe 100644
--- a/drivers/bluetooth/Kconfig
+++ b/drivers/bluetooth/Kconfig
@@ -85,16 +85,30 @@ config BT_HCIBTUSB_RTL
 	select BT_RTL
 	default y
 	help
 	  The Realtek protocol support enables firmware and configuration
 	  download support for Realtek Bluetooth controllers.
 
 	  Say Y here to compile support for Realtek protocol.
 
+config BT_HCIBTUSB_QCOM
+	bool "Qualcomm protocol support"
+	depends on BT_HCIBTUSB
+	select FW_LOADER
+	select GPIOLIB
+	select WANT_DEV_COREDUMP
+	default y
+	help
+	  Enables various required support, such as configuration, firmware
+	  download, and error recovery, for Qualcomm Bluetooth controllers.
+
+	  Say Y here to compile support for Qualcomm protocol.
+	  If unsure, say Y.
+
 config BT_HCIBTSDIO
 	tristate "HCI SDIO driver"
 	depends on MMC
 	help
 	  Bluetooth HCI SDIO driver.
 	  This driver is required if you want to use Bluetooth device with
 	  SDIO interface.
 
diff --git a/drivers/bluetooth/Makefile b/drivers/bluetooth/Makefile
index 8b436c6de8b7..d5eb2568af2e 100644
--- a/drivers/bluetooth/Makefile
+++ b/drivers/bluetooth/Makefile
@@ -47,10 +47,11 @@ hci_uart-$(CONFIG_BT_HCIUART_INTEL)	+= hci_intel.o
 hci_uart-$(CONFIG_BT_HCIUART_BCM)	+= hci_bcm.o
 hci_uart-$(CONFIG_BT_HCIUART_QCA)	+= hci_qca.o
 hci_uart-$(CONFIG_BT_HCIUART_AG6XX)	+= hci_ag6xx.o
 hci_uart-$(CONFIG_BT_HCIUART_MRVL)	+= hci_mrvl.o
 hci_uart-$(CONFIG_BT_HCIUART_AML)	+= hci_aml.o
 hci_uart-objs				:= $(hci_uart-y)
 
 btusb-y				:= btusb_main.o
+btusb-$(CONFIG_BT_HCIBTUSB_QCOM)	+= btusb_qcom.o
 
 CONTEXT_ANALYSIS := y
diff --git a/drivers/bluetooth/btusb_main.c b/drivers/bluetooth/btusb_main.c
index 53010732d9ec..ef5eeb13d4a8 100644
--- a/drivers/bluetooth/btusb_main.c
+++ b/drivers/bluetooth/btusb_main.c
@@ -24,16 +24,17 @@
 #include <net/bluetooth/hci_core.h>
 #include <net/bluetooth/hci_drv.h>
 
 #include "btintel.h"
 #include "btbcm.h"
 #include "btrtl.h"
 #include "btmtk.h"
 #include "hci_uart.h"
+#include "btusb_qcom.h"
 
 #define VERSION "1.0"
 
 static bool disable_scofix;
 static bool force_scofix;
 static bool enable_autosuspend = IS_ENABLED(CONFIG_BT_HCIBTUSB_AUTOSUSPEND);
 static bool enable_poll_sync = IS_ENABLED(CONFIG_BT_HCIBTUSB_POLL_SYNC);
 static bool reset = true;
@@ -65,16 +66,17 @@ static struct usb_driver btusb_driver;
 #define BTUSB_INVALID_LE_STATES		BIT(22)
 #define BTUSB_QCA_WCN6855		BIT(23)
 #define BTUSB_INTEL_BROKEN_SHUTDOWN_LED	BIT(24)
 #define BTUSB_INTEL_BROKEN_INITIAL_NCMD BIT(25)
 #define BTUSB_INTEL_NO_WBS_SUPPORT	BIT(26)
 #define BTUSB_ACTIONS_SEMI		BIT(27)
 #define BTUSB_BARROT			BIT(28)
 #define BTUSB_BROKEN_EXT_SCAN		BIT(29)
+#define BTUSB_QUALCOMM			BIT(30)
 
 static const struct usb_device_id btusb_table[] = {
 	/*
 	 * NXP IW610 (0471:0215): the composite device reports Bluetooth
 	 * class at the whole-device level, so the generic entry below
 	 * would also match this WiFi vendor interface. Ignore it here
 	 * first so mwifiex-nxp can bind it instead.
 	 */
@@ -421,16 +423,20 @@ static const struct usb_device_id quirks_table[] = {
 						     BTUSB_WIDEBAND_SPEECH },
 	{ USB_DEVICE(0x2c7c, 0x0130), .driver_info = BTUSB_QCA_WCN6855 |
 						     BTUSB_WIDEBAND_SPEECH },
 	{ USB_DEVICE(0x2c7c, 0x0131), .driver_info = BTUSB_QCA_WCN6855 |
 						     BTUSB_WIDEBAND_SPEECH },
 	{ USB_DEVICE(0x2c7c, 0x0132), .driver_info = BTUSB_QCA_WCN6855 |
 						     BTUSB_WIDEBAND_SPEECH },
 
+	/* Qualcomm multi-subsystem chipset QCC2072 */
+	{ USB_DEVICE(0x0cf3, 0xea00), .driver_info = BTUSB_QUALCOMM |
+						     BTUSB_WIDEBAND_SPEECH },
+
 	/* Broadcom BCM2035 */
 	{ USB_DEVICE(0x0a5c, 0x2009), .driver_info = BTUSB_BCM92035 },
 	{ USB_DEVICE(0x0a5c, 0x200a), .driver_info = BTUSB_WRONG_SCO_MTU },
 	{ USB_DEVICE(0x0a5c, 0x2035), .driver_info = BTUSB_WRONG_SCO_MTU },
 
 	/* Broadcom BCM2045 */
 	{ USB_DEVICE(0x0a5c, 0x2039), .driver_info = BTUSB_WRONG_SCO_MTU },
 	{ USB_DEVICE(0x0a5c, 0x2101), .driver_info = BTUSB_WRONG_SCO_MTU },
@@ -1391,16 +1397,41 @@ static int btusb_recv_acl(struct hci_dev *hdev, struct sk_buff *skb)
 		return data->recv_acl(hdev, skb);
 
 	skb_queue_tail(&data->acl_q, skb);
 	schedule_delayed_work(&data->rx_work, data->intr_interval);
 
 	return 0;
 }
 
+static int btusb_recv_frame(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	u8 pkt_type = hci_skb_pkt_type(skb);
+	int ret;
+
+	switch (pkt_type) {
+	case HCI_EVENT_PKT:
+		ret = btusb_recv_event(hdev, skb);
+		break;
+	case HCI_ACLDATA_PKT:
+		ret = btusb_recv_acl(hdev, skb);
+		break;
+	case HCI_SCODATA_PKT:
+	case HCI_ISODATA_PKT:
+		ret = hci_recv_frame(hdev, skb);
+		break;
+	default:
+		dev_kfree_skb_irq(skb);
+		ret = -EINVAL;
+		break;
+	}
+
+	return ret;
+}
+
 /* Dispatch through the btusb_recv_* wrappers so that vendor specific
  * handling (data->recv_event, data->recv_acl) is preserved in H:4 mode.
  */
 static const struct h4_recv_pkt btusb_recv_pkts[] = {
 	{ H4_RECV_ACL,          .recv = btusb_recv_acl },
 	{ H4_RECV_SCO,          .recv = hci_recv_frame },
 	{ H4_RECV_EVENT,        .recv = btusb_recv_event },
 	{ H4_RECV_ISO,          .recv = hci_recv_frame },
@@ -4279,16 +4310,78 @@ static const struct hci_drv_handler btusb_hci_drv_specific_handlers[] = {
 
 static struct hci_drv btusb_hci_drv = {
 	.common_handler_count	= ARRAY_SIZE(btusb_hci_drv_common_handlers),
 	.common_handlers	= btusb_hci_drv_common_handlers,
 	.specific_handler_count	= ARRAY_SIZE(btusb_hci_drv_specific_handlers),
 	.specific_handlers	= btusb_hci_drv_specific_handlers,
 };
 
+/*
+ * ============================================================================
+ * Qualcomm support
+ * ============================================================================
+ */
+
+static int btusb_recv_intr_qcom(struct btusb_data *data, void *buffer, int count)
+{
+	unsigned long flags;
+	int err = 0;
+
+	spin_lock_irqsave(&data->rxlock, flags);
+	data->evt_skb = btusb_qcom_recv_intr(data->hdev, data->evt_skb, buffer, count,
+					     &err);
+	spin_unlock_irqrestore(&data->rxlock, flags);
+
+	return err;
+}
+
+static int btusb_recv_bulk_qcom(struct btusb_data *data, void *buffer, int count)
+{
+	unsigned long flags;
+	int err = 0;
+
+	spin_lock_irqsave(&data->rxlock, flags);
+	data->acl_skb = btusb_qcom_recv_bulk(data->hdev, data->acl_skb, buffer, count,
+					     &err);
+	spin_unlock_irqrestore(&data->rxlock, flags);
+
+	return err;
+}
+
+static int btusb_send_vendor_frame_qcom(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	u8 pkt_type = hci_skb_pkt_type(skb);
+	struct urb *urb;
+
+	switch (pkt_type) {
+	case QPERI_COMMAND_PKT:
+		urb = alloc_ctrl_urb(hdev, skb);
+		break;
+
+	case QPERI_ACLDATA_PKT:
+		urb = alloc_bulk_urb(hdev, skb);
+		break;
+
+	default:
+		return -EILSEQ;
+	}
+
+	if (IS_ERR(urb))
+		return PTR_ERR(urb);
+
+	return submit_or_queue_tx_urb(hdev, urb);
+}
+
+/*
+ * ============================================================================
+ * Qualcomm support end
+ * ============================================================================
+ */
+
 static int btusb_probe(struct usb_interface *intf,
 		       const struct usb_device_id *id)
 {
 	struct gpio_desc *reset_gpio;
 	struct usb_host_interface *alt;
 	struct usb_endpoint_descriptor *bulk_rx_ep, *bulk_tx_ep, *intr_ep;
 	struct btusb_data *data;
 	struct hci_dev *hdev;
@@ -4409,16 +4502,19 @@ static int btusb_probe(struct usb_interface *intf,
 
 		data->recv_event = btusb_recv_event_realtek;
 	} else if (id->driver_info & BTUSB_MEDIATEK) {
 		/* Allocate extra space for Mediatek device */
 		priv_size += sizeof(struct btmtk_data);
 	} else if (id->driver_info & BTUSB_QCA_WCN6855) {
 		/* Allocate extra space for QCA WCN6855 device */
 		priv_size += sizeof(struct btqca_data);
+	} else if (id->driver_info & BTUSB_QUALCOMM) {
+		/* Allocate extra space for Qualcomm device */
+		priv_size += btusb_qcom_hdev_priv_size();
 	}
 
 	data->recv_acl = hci_recv_frame;
 
 	hdev = hci_alloc_dev_priv(priv_size);
 	if (!hdev) {
 		err = -ENOMEM;
 		goto err_free_data;
@@ -4567,16 +4663,41 @@ static int btusb_probe(struct usb_interface *intf,
 		hdev->classify_pkt_type = btusb_classify_qca_pkt_type;
 		hdev->shutdown = btusb_shutdown_qca;
 		hdev->set_bdaddr = btusb_set_bdaddr_wcn6855;
 		hdev->reset = btusb_qca_reset;
 		hci_set_quirk(hdev, HCI_QUIRK_SIMULTANEOUS_DISCOVERY);
 		hci_set_msft_opcode(hdev, 0xFD70);
 	}
 
+	if (id->driver_info & BTUSB_QUALCOMM) {
+		struct btusb_qcom *xport_data;
+
+		if (!IS_ENABLED(CONFIG_BT_HCIBTUSB_QCOM)) {
+			err = -ENODEV;
+			bt_dev_err(hdev, "CONFIG_BT_HCIBTUSB_QCOM not enabled");
+			goto err_kill_tx_urbs;
+		}
+
+		xport_data = btusb_qcom_xport_data(hdev);
+		xport_data->reset_gpio = data->reset_gpio;
+		xport_data->prepare_reset = btusb_prepare_reset;
+		xport_data->recv_bt_frame = btusb_recv_frame;
+		xport_data->send_bt_frame = btusb_send_frame;
+		xport_data->send_vendor_frame = btusb_send_vendor_frame_qcom;
+
+		data->recv_intr = btusb_recv_intr_qcom;
+		data->recv_bulk = btusb_recv_bulk_qcom;
+		data->disconnect = btusb_qcom_disconnect;
+
+		hdev->manufacturer = 0x1d;
+		hdev->send  = btusb_qcom_send_frame;
+		hdev->setup = btusb_qcom_setup;
+	}
+
 	if (id->driver_info & BTUSB_AMP) {
 		/* AMP controllers do not support SCO packets */
 		data->isoc = NULL;
 	} else if (data->proto == BTUSB_PROTO_H4) {
 		/* In H:4 mode every packet, including SCO/ISO, is carried over
 		 * the bulk endpoints, so the isochronous interface must not be
 		 * claimed nor have its alternate settings switched.
 		 */
diff --git a/drivers/bluetooth/btusb_qcom.c b/drivers/bluetooth/btusb_qcom.c
new file mode 100644
index 000000000000..50da211cd400
--- /dev/null
+++ b/drivers/bluetooth/btusb_qcom.c
@@ -0,0 +1,4542 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Qualcomm Bluetooth USB transport-specific support
+ *
+ * Abbreviations:
+ *   BTC   - BT controller
+ *   PERI  - Peripheral subsystem of a multi-subsys BTC
+ *   TME-L - Trust Management Engine Lite subsystem of a multi-subsys BTC
+ *   DFU   - Device Firmware Update
+ *   EDL   - Embedded Downloader
+ *   TLV   - Type-Length-Value, a firmware file format
+ *   VSC   - Vendor-Specific Command
+ *   VSE   - Vendor-Specific Event
+ *   CCE   - Command Complete Event
+ *   CSE   - Command Status Event
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include <linux/args.h>
+#include <linux/bitfield.h>
+#include <linux/firmware.h>
+#include <linux/gpio/consumer.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/sizes.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+
+#include <net/bluetooth/bluetooth.h>
+#include <net/bluetooth/coredump.h>
+#include <net/bluetooth/hci_core.h>
+
+#include "btusb_qcom.h"
+
+/*
+ * ============================================================================
+ * Supported BTCs and their configuration
+ * ============================================================================
+ */
+
+/*
+ * enum qbtc_category - driver-perspective BTC category
+ * @QBTC_CAT_LEGACY: legacy
+ * @QBTC_CAT_UNIFIED: unified HCI, VSC gets CCE as response
+ * @QBTC_CAT_MSUBSYS: multi-subsys, always has PERI
+ * @QBTC_CAT_MAX: number of categories
+ */
+enum qbtc_category {
+	QBTC_CAT_LEGACY,
+	QBTC_CAT_UNIFIED,
+	QBTC_CAT_MSUBSYS,
+	QBTC_CAT_MAX,
+};
+
+static const char *qbtc_category_name(int category)
+{
+	const char *category_name = "unknown";
+
+	switch (category) {
+	case QBTC_CAT_LEGACY:
+		category_name = "legacy";
+		break;
+	case QBTC_CAT_UNIFIED:
+		category_name = "unified";
+		break;
+	case QBTC_CAT_MSUBSYS:
+		category_name = "multi-subsys";
+		break;
+	default:
+		break;
+	}
+
+	return category_name;
+}
+
+/* flags for BTC info and default configuration */
+
+/* BTC has PERI / TME-L subsystem */
+#define QBT_FLAG_HAS_PERI		BIT(0)
+#define QBT_FLAG_HAS_TMEL		BIT(1)
+/* BTC supports AOSP / MSFT vendor extension */
+#define QBT_FLAG_AOSP_EXT		BIT(4)
+#define QBT_FLAG_MSFT_EXT		BIT(5)
+/* BTC supports software reset */
+#define QBT_FLAG_SW_RESET		BIT(6)
+/* BTC supports memdump */
+#define QBT_FLAG_MEMDUMP		BIT(7)
+/* take foundry as a factor to select NVM */
+#define QBT_FLAG_FOUNDRY_NVM		BIT(8)
+/* select NVM based on board ID to download */
+#define QBT_FLAG_BID_NVM		BIT(9)
+/* fall back to the default NVM if no board-ID-specific NVM exists */
+#define QBT_FLAG_NVM_FALLBACK		BIT(10)
+/* reset PERI HCI by QHCI instead of QDFU */
+#define QBT_FLAG_RESET_PERI_HCI		BIT(11)
+/* trigger memdump on command timeout */
+#define QBT_FLAG_CMD_TIMEOUT_MEMDUMP	BIT(12)
+/* reserve bits [31:24] for board-level flags */
+#define QBT_FLAG_BTC_CFG_MASK		GENMASK(23, 0)
+
+/* rare board ID to custom firmware directroy map */
+struct qbtc_bid_fwdir {
+	u16 board_id;
+	const char *fw_dir;
+};
+
+/*
+ * struct qbtc_info - BTC information and default configuration
+ * @category: driver-perspective BTC category (legacy/unified/multi-subsys)
+ * @flags: QBT_FLAG_* — BTC attributes and default configuration
+ * @fw_dir: firmware folder, "qca" if NULL
+ * @custom_fw_table: {}-terminated array or NULL
+ */
+struct qbtc_info {
+	enum qbtc_category category;
+	unsigned long flags;
+	const char *fw_dir;
+	const struct qbtc_bid_fwdir *custom_fw_table;
+};
+
+/*
+ * struct qbtc_id - BTC ID entry in qbtc_id_table[] below
+ * @rom_version: ID to match against rom_version read from BTC
+ * @name: human-readable BTC name
+ * @btc_info: BTC information and default configuration, != NULL
+ */
+struct qbtc_id {
+	u32		rom_version;
+	const char	*name;
+	const struct qbtc_info *btc_info;
+};
+
+/* multi-subsys BTC here */
+#define QBTC_INFO_FLAGS_MSUBSYS	(QBT_FLAG_HAS_PERI | QBT_FLAG_AOSP_EXT | \
+				 QBT_FLAG_MSFT_EXT | QBT_FLAG_SW_RESET | \
+				 QBT_FLAG_MEMDUMP | QBT_FLAG_BID_NVM | \
+				 QBT_FLAG_NVM_FALLBACK | QBT_FLAG_CMD_TIMEOUT_MEMDUMP)
+
+static const struct qbtc_info qbtc_msubsys_qcc2072 = {
+	.category = QBTC_CAT_MSUBSYS,
+	.flags    = QBTC_INFO_FLAGS_MSUBSYS,
+	.fw_dir   = "qca/QCC2072",
+};
+
+static const struct qbtc_id qbtc_id_table[] = {
+	{ 0x00220100, "QCC2072 1.x", &qbtc_msubsys_qcc2072 },
+	{ }
+};
+
+/*
+ * ============================================================================
+ * Qualcomm DFU for rampatch/NVM download
+ * ============================================================================
+ */
+
+/* QDFU USB vendor command codes (bRequest) */
+#define QDFU_BT_CMD_VSC_REQ_DOWNLOAD_LOCAL	0x01
+#define QDFU_BT_CMD_CHECK_TARGET_STATE	0x05
+#define QDFU_BT_CMD_GET_TARGET_VERSION	0x09
+#define QDFU_BT_CMD_VSC_REQ_DOWNLOAD_REMOTE	0x10
+#define QDFU_BT_CMD_RESET_PERI_HCI			0x11
+#define QDFU_BT_CMD_ACTIVATE_REMOTE_BTSS	0x12
+#define QDFU_BT_CMD_BT_ENABLE_RESET		0x13
+
+/* QDFU target status bits (u8 bitmask from QDFU_BT_CMD_CHECK_TARGET_STATE) */
+#define QDFU_BT_STATE_LOADING_LOCAL		BIT(3)
+#define QDFU_BT_STATE_PATCHED_REMOTE	BIT(4)
+#define QDFU_BT_STATE_NVMED_REMOTE		BIT(5)
+#define QDFU_BT_STATE_NVMED_LOCAL		BIT(6)
+#define QDFU_BT_STATE_PATCHED_LOCAL		BIT(7)
+
+/* QDFU_BT_CMD_GET_TARGET_VERSION response */
+struct qdfu_bt_version {
+	__le32	rom_version;
+	__le32	patch_version;
+	__le32	soc_ver;
+	__be16	board_id;
+	__le16	flag;
+	u8	reserved[4];
+} __packed;
+
+struct qdfu_bt_id {
+	u32 rom_version;
+	u32 patch_version;
+	u32 soc_id;
+	u16 board_id;
+};
+
+static inline int qdfu_recv_vendor_req(struct hci_dev *hdev, u8 request,
+				       u16 value, void *buf, u16 size)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+
+	return usb_control_msg_recv(xport_data->udev, 0, request,
+				    USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_ENDPOINT,
+				    value, 0, buf, size,
+				    USB_CTRL_GET_TIMEOUT, GFP_KERNEL);
+}
+
+static inline int qdfu_send_vendor_req(struct hci_dev *hdev, u8 request,
+				       u16 value, const void *buf, u16 size)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+
+	return usb_control_msg_send(xport_data->udev, 0, request,
+				    USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_ENDPOINT,
+				    value, 0, buf, size,
+				    USB_CTRL_SET_TIMEOUT, GFP_KERNEL);
+}
+
+static int qdfu_vsc_req_download(struct hci_dev *hdev, u8 request,
+				 const void *buf, u16 size)
+{
+	int ret;
+
+	if (request != QDFU_BT_CMD_VSC_REQ_DOWNLOAD_LOCAL &&
+	    request != QDFU_BT_CMD_VSC_REQ_DOWNLOAD_REMOTE) {
+		bt_dev_err(hdev,
+			   "QDFU download invalid request code 0x%02x", request);
+		return -EINVAL;
+	}
+
+	ret = qdfu_send_vendor_req(hdev, request, 0, buf, size);
+	if (ret)
+		bt_dev_err(hdev,
+			   "QDFU download request 0x%02x failed: %pe",
+			   request, ERR_PTR(ret));
+
+	return ret;
+}
+
+static int qdfu_get_target_state(struct hci_dev *hdev, u8 *state_ptr)
+{
+	u8 state;
+	int ret;
+
+	ret = qdfu_recv_vendor_req(hdev, QDFU_BT_CMD_CHECK_TARGET_STATE, 0,
+				   &state, sizeof(state));
+	if (!ret)
+		*state_ptr = state;
+	else
+		bt_dev_err(hdev,
+			   "QDFU get target state failed: %pe",
+			   ERR_PTR(ret));
+
+	return ret;
+}
+
+static int qdfu_get_target_version(struct hci_dev *hdev, struct qdfu_bt_id *id_info)
+{
+	struct qdfu_bt_version dfu_ver;
+	u16 board_id = 0;
+	int ret;
+
+	ret = qdfu_recv_vendor_req(hdev, QDFU_BT_CMD_GET_TARGET_VERSION, 0,
+				   &dfu_ver, sizeof(dfu_ver));
+	if (ret) {
+		bt_dev_err(hdev,
+			   "QDFU get target version failed: %pe",
+			   ERR_PTR(ret));
+		return ret;
+	}
+
+	id_info->rom_version = le32_to_cpu(dfu_ver.rom_version);
+	id_info->patch_version = le32_to_cpu(dfu_ver.patch_version);
+	id_info->soc_id = le32_to_cpu(dfu_ver.soc_ver);
+
+	if ((le16_to_cpu(dfu_ver.flag) >> 8) == 0x80)
+		board_id = be16_to_cpu(dfu_ver.board_id);
+
+	/* Take 0xffff as invalid board ID */
+	if (board_id == 0xffff)
+		board_id = 0;
+
+	id_info->board_id = board_id;
+
+	return 0;
+}
+
+static int qdfu_activate_remote_btss(struct hci_dev *hdev, bool on,
+				     unsigned int wait_us)
+{
+	u8 status = 0;
+	int ret;
+
+	ret = qdfu_recv_vendor_req(hdev, QDFU_BT_CMD_ACTIVATE_REMOTE_BTSS, on,
+				   &status, sizeof(status));
+	if (ret) {
+		bt_dev_err(hdev, "QDFU turn %s remote BTSS failed: %pe",
+			   str_on_off(on), ERR_PTR(ret));
+		return ret;
+	}
+
+	switch (status) {
+	case 0:
+		bt_dev_dbg(hdev, "QDFU Remote BTSS turned %s", str_on_off(on));
+		fsleep(wait_us);
+		break;
+	case 0x17:
+		bt_dev_dbg(hdev, "QDFU Remote BTSS already %s", str_on_off(on));
+		break;
+	default:
+		bt_dev_err(hdev,
+			   "QDFU turn remote BTSS %s failed, unexpected status 0x%02x",
+			   str_on_off(on), status);
+		ret = -ENODEV;
+	}
+
+	return ret;
+}
+
+static int qdfu_reset_peri_hci(struct hci_dev *hdev)
+{
+	int ret;
+
+	ret = qdfu_send_vendor_req(hdev, QDFU_BT_CMD_RESET_PERI_HCI, 0, NULL, 0);
+	if (ret) {
+		bt_dev_err(hdev, "PERI HCI reset via QDFU failed: %pe",
+			   ERR_PTR(ret));
+		return ret;
+	}
+	bt_dev_info(hdev, "PERI HCI reset via QDFU succeeded");
+	fsleep(20 * 1000);
+
+	return 0;
+}
+
+static int qdfu_sw_reset(struct hci_dev *hdev)
+{
+	int ret;
+
+	ret = qdfu_send_vendor_req(hdev, QDFU_BT_CMD_BT_ENABLE_RESET, 0, NULL, 0);
+	/* SW reset succeeds even if the request returns an error code */
+	if (ret)
+		bt_dev_dbg(hdev, "QDFU SW reset failed: %pe", ERR_PTR(ret));
+
+	bt_dev_info(hdev, "QDFU SW reset succeeded");
+
+	return 0;
+}
+
+/*
+ * qdfu_poll_state - wait for DFU target status flags to reach a wanted state
+ * @hdev: the HCI device to poll
+ * @state_ptr: optional storage for the last status read; may be NULL
+ * @set: true to wait until @flags are all set, false until all cleared
+ * @flags: the status flag bits to wait on
+ *
+ * Repeatedly reads the DFU target status until @flags reach the requested
+ * state, a status read fails, or the overall timeout expires.
+ *
+ * Return: 0 on success, -ETIMEDOUT on timeout, or a negative errno on read
+ *	failure.
+ */
+static int qdfu_poll_state(struct hci_dev *hdev, u8 *state_ptr, bool set, u8 flags)
+{
+	int err, ret;
+	u8 dfu_state;
+
+	if (!state_ptr)
+		state_ptr = &dfu_state;
+
+	ret = read_poll_timeout(qdfu_get_target_state, err,
+				err || (set ? (*state_ptr & flags) == flags
+					    : !(*state_ptr & flags)),
+				5 * 1000,
+				3000 * 1000, true,
+				hdev, state_ptr);
+	if (ret) {
+		bt_dev_err(hdev,
+			   "Timed out waiting for QDFU state flags 0x%02x to be %s: %pe",
+			   flags, set ? "set" : "cleared", ERR_PTR(ret));
+		return ret;
+	}
+
+	return err;
+}
+
+static inline int qdfu_reset_msubsys_bt(struct hci_dev *hdev)
+{
+	int ret;
+
+	ret = qdfu_activate_remote_btss(hdev, false, 100 * 1000);
+	if (!ret)
+		ret = qdfu_activate_remote_btss(hdev, true, 100 * 1000);
+	if (!ret)
+		bt_dev_info(hdev, "QDFU reset msubsys BT succeeded");
+
+	return ret;
+}
+
+/*
+ * ============================================================================
+ * Common definitions, per-device struct btqcom_data, and PERI frame format
+ * ============================================================================
+ */
+
+/* BTC version, read out via an HCI command */
+struct qhci_btc_ver {
+	u32 product_id;
+	u32 soc_ver;
+	u16 rom_ver;
+	u16 patch_ver;
+	u8  sec_ver;
+};
+
+/* Subsystem field appears in a PERI command/response/event */
+enum qhci_subsys {
+	QHCI_SUBSYS_PERI,
+	QHCI_SUBSYS_BT,
+	QHCI_SUBSYS_UWB,
+	QHCI_SUBSYS_TMEL,
+	QHCI_SUBSYS_MAX,
+};
+
+/*
+ * The pseudo QHCI subsys: for BT-only BTC, or when the memdump comes
+ * from the BT channel (HCI_EVENT_PKT or HCI_ACLDATA_PKT) on a msubsys
+ * BTC, there is no real subsys — it stands in for these cases.
+ */
+#define QHCI_SUBSYS_INVALID	QHCI_SUBSYS_MAX
+
+static const char *qhci_subsys_name(int subsys)
+{
+	const char *subsys_name = "unknown";
+
+	switch (subsys) {
+	case QHCI_SUBSYS_PERI:
+		subsys_name = "PERI";
+		break;
+	case QHCI_SUBSYS_BT:
+		subsys_name = "PERI_BT";
+		break;
+	case QHCI_SUBSYS_UWB:
+		subsys_name = "UWB";
+		break;
+	case QHCI_SUBSYS_TMEL:
+		subsys_name = "TME-L";
+		break;
+	case QHCI_SUBSYS_INVALID:
+		subsys_name = "BT";
+		break;
+	default:
+		break;
+	}
+
+	return subsys_name;
+}
+
+/* BTC subsystems we care about that support the BT function unit */
+enum qbtc_subsys {
+	QBTC_SUBSYS_PERI,
+	QBTC_SUBSYS_TMEL,
+	QBTC_SUBSYS_MAX,
+};
+
+/*
+ * The pseudo BTC subsys stands in for the BT function unit, not a
+ * real subsys, when building a firmware file path.
+ */
+#define QBTC_SUBSYS_INVALID QBTC_SUBSYS_MAX
+
+static const char *qbtc_subsys_name(int subsys)
+{
+	const char *subsys_name = "unknown";
+
+	switch (subsys) {
+	case QBTC_SUBSYS_PERI:
+		subsys_name = "PERI";
+		break;
+	case QBTC_SUBSYS_TMEL:
+		subsys_name = "TME-L";
+		break;
+	case QBTC_SUBSYS_INVALID:
+		subsys_name = "BT";
+		break;
+	default:
+		break;
+	}
+
+	return subsys_name;
+}
+
+/*
+ * struct qbtc_subsys_data - per-subsys data
+ * @ver: subsys version
+ * @board_id: subsys board ID
+ * @build_info: the subsys firmware's build info string
+ */
+struct qbtc_subsys_data {
+	struct qhci_btc_ver ver;
+	u16	board_id;
+	char	build_info[160];
+};
+
+/* simple command payload */
+struct qhci_cp_simple {
+	u8	sub_opcode;
+} __packed;
+
+/* simple CCE(response) payload */
+struct qhci_rp_simple {
+	u8	status;
+} __packed;
+
+/* generic CCE(response) payload */
+struct qhci_rp_generic {
+	u8		status;
+	u8		sub_opcode;
+} __packed;
+
+/* common VSE payload */
+struct qhci_vse_comm {
+	u8   ev_class;
+	u8   ev_type;
+} __packed;
+#define QHCI_VSE_COMM_SIZE	(sizeof(struct qhci_vse_comm))
+
+#define QHCI_MEMDUMP_SEQ_LAST		0xFFFF
+#define QBT_EV_CLASS_DATALOG 0x01
+#define QBT_EV_TYPE_MEMDUMP  0x08
+
+/*
+ * struct qhci_vse_memdump - one memdump segment
+ * @seqno: segment sequence number; 0 = first, QHCI_MEMDUMP_SEQ_LAST = last
+ * @subsys: subsystem this segment belongs to (see enum qhci_subsys)
+ * @segdata: payload of a middle or last segment
+ * @dump_size: total dump size, valid only in the first segment (@seqno == 0)
+ * @first_segdata: payload of the first segment, following @dump_size
+ */
+struct qhci_vse_memdump {
+	__le16 seqno;
+	u8     subsys;
+	union {
+		u8 segdata[0];
+		struct {
+			__le32 dump_size;
+			u8 first_segdata[];
+		} __packed;
+	};
+} __packed;
+
+#define QHCI_MEMDUMP_SEGDATA_GAP \
+	(offsetof(struct qhci_vse_memdump, first_segdata) - \
+	 offsetof(struct qhci_vse_memdump, segdata))
+
+enum qhci_req_state {
+	QHCI_REQ_DONE,
+	QHCI_REQ_PEND,
+	QHCI_REQ_CANCELED,
+	/* unused for now */
+	QHCI_REQ_STOP,
+};
+
+#define QHCI_OPCODE_INVALID		HCI_OP_NOP
+#define QHCI_SUB_OPCODE_INVALID		0xff
+#define QHCI_REQ_EVENT_MAX		2
+
+/*
+ * struct qhci_req_spec - request/response specification
+ * @opcode: command opcode, ignored if invalid
+ * @sub_opcode: command sub-opcode, ignored if invalid
+ * @event: a sequence of events, ending with an invalid marker
+ */
+struct qhci_req_spec {
+	u16	opcode;
+	u8	sub_opcode;
+	u8	event[QHCI_REQ_EVENT_MAX + 1];
+};
+
+/*
+ * struct qhci_req - per-request data, often stack allocated by the requester
+ * @pkt_type: the packet type to sync
+ * @spec: the request spec to sync
+ * @req_rsp: response skb on success, NULL on no data, or ERR_PTR() on failure
+ * @req_result: >= 0 an HCI status code, or a negative errno on failure
+ * @event_idx: cursor into @spec->event
+ */
+struct qhci_req {
+	u8				pkt_type;
+	const struct qhci_req_spec	*spec;
+	struct sk_buff			*req_rsp;
+	int				req_result;
+	u8				event_idx;
+};
+
+/* board has a BT_EN pin to reset BTC */
+#define QBT_FLAG_HW_RESET		BIT(24)
+
+/*
+ * enum qbt_work_bit - bits in btqcom_data.work_flags
+ * @QBT_WORK_RESET_HDEV: request to reset hdev
+ */
+enum qbt_work_bit {
+	QBT_WORK_RESET_HDEV,
+};
+
+/*
+ * enum qbt_misc_bit - bits in btqcom_data.misc_flags
+ * @QBT_MISC_MEMDUMP_PERI: PERI memdump
+ * @QBT_MISC_MEMDUMP_BT: BT memdump
+ * @QBT_MISC_MEMDUMP_UWB: UWB memdump, never happens here
+ * @QBT_MISC_MEMDUMP_TMEL: TME-L memdump
+ * @QBT_MISC_MEMDUMP_INCOMING: memdump is arriving from the BTC
+ * @QBT_MISC_RESET_ACTIVE: a reset is in progress
+ * @QBT_MISC_HWERR_PERI: PERI hardware error event happens
+ * @QBT_MISC_HWERR_BT: BT hardware error event happens
+ * @QBT_MISC_CMD_TIMEOUT: command timeout happens
+ *
+ * The memdump bits are indexed by enum qhci_subsys, so a subsystem's bit can
+ * be derived from its QHCI subsys value.
+ */
+enum qbt_misc_bit {
+	QBT_MISC_MEMDUMP_PERI = QHCI_SUBSYS_PERI,
+	QBT_MISC_MEMDUMP_BT = QHCI_SUBSYS_BT,
+	QBT_MISC_MEMDUMP_UWB = QHCI_SUBSYS_UWB,
+	QBT_MISC_MEMDUMP_TMEL = QHCI_SUBSYS_TMEL,
+	QBT_MISC_MEMDUMP_INCOMING,
+	QBT_MISC_RESET_ACTIVE,
+	QBT_MISC_HWERR_PERI,
+	QBT_MISC_HWERR_BT,
+	QBT_MISC_CMD_TIMEOUT,
+};
+
+/* each subsys's memdump pending flag in btqcom_data.md_pending_flags */
+#define QMD_FLAG_PENDING_PERI	BIT(QBT_MISC_MEMDUMP_PERI)
+#define QMD_FLAG_PENDING_BT	BIT(QBT_MISC_MEMDUMP_BT)
+#define QMD_FLAG_PENDING_TMEL	BIT(QBT_MISC_MEMDUMP_TMEL)
+#define QMD_FLAG_PENDING_MASK	(QMD_FLAG_PENDING_PERI | \
+				 QMD_FLAG_PENDING_BT | \
+				 QMD_FLAG_PENDING_TMEL)
+
+static inline int qmd_subsys_to_bit(int qhci_subsys)
+{
+	return qhci_subsys == QHCI_SUBSYS_INVALID ?
+	       QBT_MISC_MEMDUMP_BT : qhci_subsys;
+}
+
+static inline unsigned long qmd_subsys_to_flag(int qhci_subsys)
+{
+	return BIT(qmd_subsys_to_bit(qhci_subsys));
+}
+
+/*
+ * struct btqcom_memdump - collect memdump from a QHCI subsys
+ * @size: total memdump size to collect
+ * @subsys: QHCI subsys this memdump belongs to
+ * @seqno: next expected sequence number
+ * @from: which channel this memdump comes from, marked by hci_skb_pkt_type()
+ * @submit_size: bytes submitted to HCI devcoredump for the subsys
+ */
+struct btqcom_memdump {
+	u32 size;
+	int subsys;
+	u16 seqno;
+	u8 from;
+
+	u32 submit_size;
+};
+
+/*
+ * struct btqcom_data - transport-independent per-device common data, allocated as hci priv
+ * @drv_name: driver name
+ * @btc_name: human-readable BTC name
+ * @category: driver-perspective BTC category (legacy/unified/multi-subsys)
+ * @flags: QBT_FLAG_* flags for BTC/board and default config
+ * @hdev: the owning HCI device
+ * @xport_data: opaque transport-specific data (e.g. struct btusb_qcom for USB)
+ * @inited: whether the hdev-lifetime fields are initialized
+ * @board_id: BT board ID
+ * @ver: BT version
+ * @build_info: BT firmware build info string
+ * @fw_dir: firmware directory configured by user via sysfs
+ * @fw_logging: firmware logging configured by user via sysfs
+ * @misc_flags: flags made from QBT_MISC_* bits defined above
+ * @work_flags: flags made from QBT_WORK_* bits defined above
+ * @dwork: delayed work to handle @work_flags
+ * @md_ready: memdump functionality is ready
+ * @md_state: devcoredump state as notified by the HCI devcoredump core
+ * @md_submit_err: first error submitting to HCI devcoredump, 0 if none
+ * @md_submit_size: total bytes submitted to HCI devcoredump, headers included
+ * @md_pending_flags: QMD_FLAG_PENDING_* flags tracking per-subsystem memdump
+ * @md: per-subsystem memdump collection track
+ * @req_mutex: serializes sync requesters
+ * @req_wait_q: wait queue for the requester
+ * @req_spinlock: protects @req_state, @req, and @*req
+ * @req_state: request state, see enum qhci_req_state
+ * @req: the in-flight request, valid while @req_state is QHCI_REQ_PEND
+ * @subsys: per-subsystem info for MSUBSYS chips
+ */
+struct btqcom_data {
+	const char *drv_name;
+	const char *btc_name;
+	enum qbtc_category category;
+	unsigned long flags;
+
+	struct hci_dev *hdev;
+	void *xport_data;
+
+	bool inited;
+
+	u16	board_id;
+	struct qhci_btc_ver ver;
+	char	build_info[160];
+
+	const char *fw_dir;
+	u8 fw_logging;
+
+	unsigned long misc_flags;
+	unsigned long		work_flags;
+	struct delayed_work	dwork;
+
+	bool md_ready;
+	enum devcoredump_state md_state;
+	int md_submit_err;
+	u32 md_submit_size;
+	unsigned long md_pending_flags;
+	struct btqcom_memdump md;
+
+	struct mutex		req_mutex;
+	wait_queue_head_t	req_wait_q;
+	spinlock_t		req_spinlock;
+	int			req_state;
+	struct qhci_req		*req;
+
+	struct qbtc_subsys_data subsys[QBTC_SUBSYS_MAX];
+};
+
+/* BT host ID in PERI command/event/ACL */
+#define QHCI_HOST_ID_BT 0
+
+struct qperi_command_hdr {
+	u8	host_id;
+	__le16	opcode;
+	u8	plen;
+} __packed;
+#define QPERI_COMMAND_HDR_SIZE (sizeof(struct qperi_command_hdr))
+
+struct qperi_acl_hdr {
+	u8	host_id;
+	__le16	handle;
+	__le16	dlen;
+} __packed;
+#define QPERI_ACL_HDR_SIZE	(sizeof(struct qperi_acl_hdr))
+#define QPERI_MAX_FRAME_SIZE	(HCI_MAX_FRAME_SIZE + 1)
+
+struct qperi_event_hdr {
+	u8	host_id;
+	u8	evt;
+	u8	plen;
+} __packed;
+#define QPERI_EVENT_HDR_SIZE	(sizeof(struct qperi_event_hdr))
+#define QPERI_MAX_EVENT_SIZE	(HCI_MAX_EVENT_SIZE + 1)
+
+static inline struct qperi_event_hdr *qperi_event_header(const struct sk_buff *skb)
+{
+	return (struct qperi_event_hdr *)skb->data;
+}
+
+static inline struct qperi_acl_hdr *qperi_acl_header(const struct sk_buff *skb)
+{
+	return (struct qperi_acl_hdr *)skb->data;
+}
+
+static inline __u16 qperi_acl_handle(const struct sk_buff *skb)
+{
+	struct qperi_acl_hdr *hdr = qperi_acl_header(skb);
+
+	return hci_handle(__le16_to_cpu(hdr->handle));
+}
+
+static inline __u16 qperi_acl_dlen(const struct sk_buff *skb)
+{
+	return __le16_to_cpu(qperi_acl_header(skb)->dlen);
+}
+
+static const char *qhci_pkt_type_name(u8 pkt_type)
+{
+	const char *pkt_type_name = "Unknown";
+
+	switch (pkt_type) {
+	case HCI_EVENT_PKT:
+		pkt_type_name = "BT EVT";
+		break;
+	case QPERI_EVENT_PKT:
+		pkt_type_name = "PERI EVT";
+		break;
+	case HCI_ACLDATA_PKT:
+		pkt_type_name = "BT ACL";
+		break;
+	case QPERI_ACLDATA_PKT:
+		pkt_type_name = "PERI ACL";
+		break;
+	default:
+		break;
+	}
+
+	return pkt_type_name;
+}
+
+static void btusb_qcom_reset_sync(struct hci_dev *hdev);
+
+/*
+ * ============================================================================
+ * Multi-subsys memdump design
+ * ============================================================================
+ */
+
+/*
+ * Design compatible with BT-only BTC as well:
+ *
+ * Abbreviations: SS - Subsystem, MD - Memdump, HDR - Header
+ *
+ * +--------------+-------------+---------+     +-------------+---------+
+ * | file HDR     | SS_A MD HDR | SS_A    | ... | SS_N MD HDR | SS_N    |
+ * | 512B         | 512B        | MD data |     | 512B        | MD data |
+ * | by dmp_hdr() | appended as |         |     | appended as |         |
+ * |              | MD data     |         |     | MD data     |         |
+ * +--------------+-------------+---------+     +-------------+---------+
+ *
+ * 1) All SS MDs are collected into a SINGLE file.
+ * 2) Each SS_X MD HDR records the size of its own MD data.
+ * 3) On the 1st segment of the 1st SS: call hci_devcd_init() with
+ *    dump_size — for a BT-only BTC, a 512B MD HDR plus the size carried
+ *    in the segment itself; for a multi-subsys BTC, the sum of (a 512B
+ *    MD HDR plus the max MD data size) over every SS. The file HDR is
+ *    not counted, since the HCI devcoredump adds it on top of dump_size.
+ * 4) On the 1st segment of every SS: call hci_devcd_append() to append
+ *    its 512B MD HDR as ordinary MD data.
+ * 5) On the last segment of a SS: call hci_devcd_complete() only if
+ *    no other SS's MD is still pending.
+ * 6) On the hardware error event (either PERI or BT), which always
+ *    comes after all MDs have been reported by the BTC: call
+ *    hci_devcd_complete() there if it has not been called yet, since a
+ *    SS's MD is optional.
+ */
+
+static const u32 btqcom_memdump_maxsize[] = {
+	[QHCI_SUBSYS_PERI]	= SZ_256K,
+	[QHCI_SUBSYS_BT]	= SZ_1M,
+	[QHCI_SUBSYS_UWB]	= 0,
+	[QHCI_SUBSYS_TMEL]	= SZ_256K,
+	[QHCI_SUBSYS_INVALID]	= SZ_1M,
+};
+
+/*
+ * btqcom_memdump_hdr_room - helper to fill a memdump header up to 512 bytes
+ * @skb: the devcoredump header skb to fill
+ *
+ * Return: bytes safe to write into @skb.
+ */
+static int btqcom_memdump_hdr_room(struct sk_buff *skb)
+{
+	int end_marker_len = sizeof(HCI_DEVCD_HDR_END_MARKER) - 1;
+	/*
+	 * don't use skb_tailroom(): observed to differ from requested
+	 * alloc_size - skb->len
+	 */
+	int avail_room = HCI_DEVCD_HDR_SIZE_MAX - skb->len;
+	u8 *ptr;
+
+	/* 1 char + '\n' at least */
+	if (avail_room >= end_marker_len + 2)
+		return avail_room - end_marker_len;
+
+	if (avail_room == end_marker_len)
+		return 0;
+
+	/* Avoid a blank line ('\n\n') in the header */
+	skb_trim(skb, HCI_DEVCD_HDR_SIZE_MAX - end_marker_len - 1);
+	ptr = skb_tail_pointer(skb) - 1;
+	if (*ptr == '\n')
+		*ptr = ' ';
+	skb_put_u8(skb, '\n');
+
+	return 0;
+}
+
+/*
+ * btqcom_memdump_hdr - build the 512B memdump header
+ * @hdev: the HCI device
+ * @skb: the skb to build the header into
+ *
+ * Its 'Memdump Size' field is the size of the following payload.
+ */
+static void btqcom_memdump_hdr(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	int avail_room = btqcom_memdump_hdr_room(skb);
+	struct btqcom_memdump *md = &qbt_data->md;
+	struct qbtc_subsys_data *subsys_data;
+	char buf[HCI_DEVCD_HDR_SIZE_MAX];
+	struct qhci_btc_ver *btc_ver;
+	const char *build_info;
+	u16 board_id;
+	int len = 0;
+
+	if (!avail_room)
+		return;
+
+	len += scnprintf(buf + len, sizeof(buf) - len, "Header Type: memdump\n");
+	len += scnprintf(buf + len, sizeof(buf) - len, "Header Size: %d\n",
+			 HCI_DEVCD_HDR_SIZE_MAX);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Memdump Size: %u\n",
+			 md->size);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Channel: %s\n",
+			 qhci_pkt_type_name(md->from));
+	len += scnprintf(buf + len, sizeof(buf) - len, "Owner: %s\n",
+			 qhci_subsys_name(md->subsys));
+
+	switch (md->subsys) {
+	case QHCI_SUBSYS_PERI:
+		subsys_data = &qbt_data->subsys[QBTC_SUBSYS_PERI];
+		btc_ver = &subsys_data->ver;
+		board_id = subsys_data->board_id;
+		build_info = subsys_data->build_info;
+		break;
+	case QHCI_SUBSYS_TMEL:
+		subsys_data = &qbt_data->subsys[QBTC_SUBSYS_TMEL];
+		btc_ver = &subsys_data->ver;
+		board_id = subsys_data->board_id;
+		build_info = subsys_data->build_info;
+		break;
+	default:
+		btc_ver = &qbt_data->ver;
+		board_id = qbt_data->board_id;
+		build_info = qbt_data->build_info;
+		break;
+	}
+
+	len += scnprintf(buf + len, sizeof(buf) - len, "SoC Version: 0x%08x\n",
+			 btc_ver->soc_ver);
+	len += scnprintf(buf + len, sizeof(buf) - len, "ROM Version: 0x%04x\n",
+			 btc_ver->rom_ver);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Patch Version: 0x%04x\n",
+			 btc_ver->patch_ver);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Board ID: 0x%04x\n",
+			 board_id);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Firmware Version: %s\n",
+			 build_info);
+
+	if (len > avail_room) {
+		bt_dev_warn(hdev, "memdump header truncated (%d -> %d bytes)",
+			    len, avail_room);
+		len = avail_room;
+		if (buf[len - 2] == '\n')
+			buf[len - 2] = ' ';
+		buf[len - 1] = '\n';
+	}
+
+	skb_put_data(skb, buf, len);
+}
+
+/*
+ * btusb_qcom_memdump_hdr - build the 512B file header
+ * @hdev: the HCI device
+ * @skb: the skb to build the header into
+ *
+ * Implements the dmp_hdr_t for hci_devcd_register().
+ */
+static void btusb_qcom_memdump_hdr(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	char buf[HCI_DEVCD_HDR_SIZE_MAX];
+	const char *vendor = "Qualcomm";
+	int avail_room;
+	int len = 0;
+
+	avail_room = btqcom_memdump_hdr_room(skb);
+	if (!avail_room)
+		return;
+
+	len += scnprintf(buf + len, sizeof(buf) - len, "Header Type: file\n");
+	len += scnprintf(buf + len, sizeof(buf) - len, "Header Size: %d\n",
+			 HCI_DEVCD_HDR_SIZE_MAX);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Driver: %s\n",
+			 qbt_data->drv_name);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Vendor: %s\n", vendor);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Controller Name: %s\n",
+			 qbt_data->btc_name);
+	len += scnprintf(buf + len, sizeof(buf) - len, "Controller Category: %s\n",
+			 qbtc_category_name(qbt_data->category));
+
+	if (qbt_data->category == QBTC_CAT_MSUBSYS) {
+		if (qbt_data->flags & QBT_FLAG_HAS_TMEL)
+			len += scnprintf(buf + len, sizeof(buf) - len,
+					 "Controller Subsys: PERI, TME-L\n");
+		else
+			len += scnprintf(buf + len, sizeof(buf) - len,
+					 "Controller Subsys: PERI\n");
+	}
+	len += scnprintf(buf + len, sizeof(buf) - len, "VID: 0x%04x\n",
+			 xport_data->idVendor);
+	len += scnprintf(buf + len, sizeof(buf) - len, "PID: 0x%04x\n",
+			 xport_data->idProduct);
+
+	if (len > avail_room)
+		bt_dev_warn(hdev, "dump header truncated (%d -> %d bytes)",
+			    len, avail_room);
+
+	memset(buf + len, 'P', sizeof(buf) - len);
+
+	/*
+	 * Deliberately overfill to skb's full 512 bytes, then leverage
+	 * btqcom_memdump_hdr_room() to trim it back to the expected boundary.
+	 */
+	skb_put_data(skb, buf, HCI_DEVCD_HDR_SIZE_MAX - skb->len);
+	btqcom_memdump_hdr_room(skb);
+}
+
+/* Used as the notify_change_t callback for hci_devcd_register(). */
+static void btusb_qcom_notify_memdump(struct hci_dev *hdev, int state)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	enum devcoredump_state old_state;
+
+	old_state = qbt_data->md_state;
+
+	bt_dev_dbg(hdev, "memdump notify state: %s -> %s",
+		   hci_devcd_state_name(old_state),
+		   hci_devcd_state_name(state));
+	bt_dev_dbg(hdev, "misc_flags: 0x%lx", READ_ONCE(qbt_data->misc_flags));
+	bt_dev_dbg(hdev, "md_pending_flags: 0x%lx, md_submit_err: %d",
+		   READ_ONCE(qbt_data->md_pending_flags),
+		   READ_ONCE(qbt_data->md_submit_err));
+
+	switch (state) {
+	case HCI_DEVCOREDUMP_IDLE:
+		break;
+	case HCI_DEVCOREDUMP_ACTIVE:
+		usb_autopm_get_interface_no_resume(xport_data->intf);
+		bt_dev_dbg(hdev, "memdump notify: get autopm refcount");
+		break;
+	case HCI_DEVCOREDUMP_TIMEOUT:
+		bt_dev_err(hdev, "memdump notify: %s", hci_devcd_state_name(state));
+		qbt_data->md_submit_err = -ETIMEDOUT;
+		qbt_data->md_pending_flags = 0;
+		test_and_clear_bit(QBT_MISC_MEMDUMP_INCOMING,
+				   &qbt_data->misc_flags);
+		fallthrough;
+	case HCI_DEVCOREDUMP_DONE:
+	case HCI_DEVCOREDUMP_ABORT:
+		usb_autopm_put_interface_no_suspend(xport_data->intf);
+		bt_dev_dbg(hdev, "memdump notify: put autopm refcount");
+		break;
+	}
+
+	qbt_data->md_state = state;
+
+	if (state == HCI_DEVCOREDUMP_TIMEOUT)
+		btusb_qcom_reset_sync(hdev);
+}
+
+/* handle received memdump frame here */
+
+static inline void btqcom_reset_memdump(struct btqcom_memdump *md)
+{
+	memset(md, 0x00, sizeof(*md));
+	md->subsys  = QHCI_SUBSYS_INVALID;
+}
+
+/*
+ * btqcom_submit_memdump - submit one memdump segment to HCI devcoredump
+ * @hdev: the HCI device the memdump comes from
+ * @skb: the memdump segment payload
+ *
+ * See the "Multi-subsys memdump design" block above for the overall scheme
+ * this implements.
+ *
+ * Return: 0 on success,
+ *	1 if the memdump ended normally,
+ *	a negative errno on failure.
+ */
+static int btqcom_submit_memdump(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct btqcom_memdump *md = &qbt_data->md;
+	unsigned int dump_size, seg_len;
+	bool is_first_subsys = false;
+	int ret = 0;
+
+	dump_size = 0;
+	seg_len = skb->len;
+
+	if (md->seqno == 0) {
+		if (!test_and_set_bit(QBT_MISC_MEMDUMP_INCOMING,
+				      &qbt_data->misc_flags)) {
+			/* wake the waiter in btusb_do_cmd_timeout_work() */
+			wake_up_var(&qbt_data->misc_flags);
+			is_first_subsys = true;
+			qbt_data->md_submit_err = 0;
+			qbt_data->md_submit_size = 0;
+			if (qbt_data->category == QBTC_CAT_MSUBSYS) {
+				qbt_data->md_pending_flags = QMD_FLAG_PENDING_BT |
+							     QMD_FLAG_PENDING_PERI;
+				dump_size = HCI_DEVCD_HDR_SIZE_MAX +
+					    btqcom_memdump_maxsize[QHCI_SUBSYS_BT] +
+					    HCI_DEVCD_HDR_SIZE_MAX +
+					    btqcom_memdump_maxsize[QHCI_SUBSYS_PERI];
+
+				if (qbt_data->flags & QBT_FLAG_HAS_TMEL) {
+					qbt_data->md_pending_flags |= QMD_FLAG_PENDING_TMEL;
+					dump_size += HCI_DEVCD_HDR_SIZE_MAX +
+						     btqcom_memdump_maxsize[QHCI_SUBSYS_TMEL];
+				}
+			} else {
+				qbt_data->md_pending_flags = QMD_FLAG_PENDING_BT;
+				dump_size = HCI_DEVCD_HDR_SIZE_MAX + md->size;
+			}
+		}
+
+		bt_dev_info(hdev, "%s memdump incoming: %u bytes",
+			    qhci_subsys_name(md->subsys), md->size);
+		bt_dev_dbg(hdev, "md_pending_flags: 0x%lx",
+			   qbt_data->md_pending_flags);
+
+		if (is_first_subsys) {
+			ret = hci_devcd_init(hdev, dump_size);
+			if (ret) {
+				kfree_skb(skb);
+				bt_dev_err(hdev, "init memdump failed: %pe",
+					   ERR_PTR(ret));
+				return ret;
+			}
+			qbt_data->md_submit_size += HCI_DEVCD_HDR_SIZE_MAX;
+			bt_dev_info(hdev, "file header: %u bytes",
+				    HCI_DEVCD_HDR_SIZE_MAX);
+		}
+
+		if (!qbt_data->md_submit_err) {
+			struct sk_buff *hdr_skb;
+
+			hdr_skb = alloc_skb(HCI_DEVCD_HDR_SIZE_MAX, GFP_KERNEL);
+			if (hdr_skb) {
+				btqcom_memdump_hdr(hdev, hdr_skb);
+				if (hdr_skb->len < HCI_DEVCD_HDR_SIZE_MAX)
+					skb_put_zero(hdr_skb,
+						     HCI_DEVCD_HDR_SIZE_MAX - hdr_skb->len);
+				ret = hci_devcd_append(hdev, hdr_skb);
+			} else {
+				ret = -ENOMEM;
+			}
+			if (ret) {
+				hci_devcd_abort(hdev);
+				kfree_skb(skb);
+				bt_dev_err(hdev, "append memdump header failed: %pe",
+					   ERR_PTR(ret));
+				return ret;
+			}
+			qbt_data->md_submit_size += HCI_DEVCD_HDR_SIZE_MAX;
+			bt_dev_info(hdev, "%s memdump header appended: %u bytes",
+				    qhci_subsys_name(md->subsys), HCI_DEVCD_HDR_SIZE_MAX);
+		}
+	}
+
+	ret = qbt_data->md_submit_err;
+	if (ret) {
+		kfree_skb(skb);
+	} else {
+		ret = hci_devcd_append(hdev, skb);
+		if (unlikely(ret)) {
+			hci_devcd_abort(hdev);
+			bt_dev_err(hdev, "append memdump failed: %pe", ERR_PTR(ret));
+		} else {
+			md->submit_size += seg_len;
+			qbt_data->md_submit_size += seg_len;
+		}
+	}
+
+	if (md->seqno != QHCI_MEMDUMP_SEQ_LAST)
+		return ret;
+
+	if (md->submit_size == md->size)
+		bt_dev_info(hdev, "%s memdump fully collected",
+			    qhci_subsys_name(md->subsys));
+	else
+		bt_dev_err(hdev, "%s memdump partially collected %u/%u bytes",
+			   qhci_subsys_name(md->subsys), md->submit_size, md->size);
+
+	qbt_data->md_pending_flags &= ~qmd_subsys_to_flag(md->subsys);
+	if (qbt_data->md_pending_flags) {
+		bt_dev_dbg(hdev, "md_pending_flags: 0x%lx",
+			   qbt_data->md_pending_flags);
+		return ret;
+	}
+
+	if (test_and_clear_bit(QBT_MISC_MEMDUMP_INCOMING, &qbt_data->misc_flags))
+		bt_dev_dbg(hdev, "clear MEMDUMP_INCOMING on memdump completion");
+
+	if (ret) {
+		bt_dev_err(hdev, "memdump complete failed: %pe", ERR_PTR(ret));
+		return ret;
+	}
+
+	ret = hci_devcd_complete(hdev);
+	if (ret) {
+		hci_devcd_abort(hdev);
+		bt_dev_err(hdev, "memdump complete failed: %pe", ERR_PTR(ret));
+	} else {
+		ret = 1;
+		bt_dev_info(hdev, "memdump completed: %u bytes",
+			    qbt_data->md_submit_size);
+	}
+	return ret;
+}
+
+/*
+ * btqcom_handle_memdump - handle one memdump segment
+ * @hdev: the HCI device the @skb comes from
+ * @skb: the memdump segment to handle
+ *
+ * Return: 0 on success,
+ *	1 if the memdump ended normally,
+ *	a negative errno on failure.
+ */
+static int btqcom_handle_memdump(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct btqcom_memdump *md = &qbt_data->md;
+	const struct qhci_vse_memdump *md_vse;
+	u8 pkt_type = hci_skb_pkt_type(skb);
+	int subsys = QHCI_SUBSYS_INVALID;
+	u16 seq_no = 0;
+	u32 dump_size;
+	int ret = 0;
+
+	md_vse = skb_pull_data(skb, offsetof(struct qhci_vse_memdump, segdata));
+	if (!md_vse) {
+		ret = -EMSGSIZE;
+		bt_dev_err(hdev, "bad memdump segment: too short (%u bytes)", skb->len);
+		goto out_abort_md;
+	}
+
+	if (pkt_type == QPERI_EVENT_PKT || pkt_type == QPERI_ACLDATA_PKT) {
+		subsys = md_vse->subsys;
+		if (subsys >= QHCI_SUBSYS_MAX || !btqcom_memdump_maxsize[subsys] ||
+		    (subsys == QHCI_SUBSYS_TMEL &&
+		     !(qbt_data->flags & QBT_FLAG_HAS_TMEL))) {
+			kfree_skb(skb);
+			bt_dev_warn_ratelimited(hdev,
+						"drop memdump of unsupported subsys %d",
+						subsys);
+			return 0;
+		}
+	}
+
+	seq_no = le16_to_cpu(md_vse->seqno);
+	if (seq_no == 0) {
+		set_bit(qmd_subsys_to_bit(subsys), &qbt_data->misc_flags);
+
+		if (!skb_pull(skb, QHCI_MEMDUMP_SEGDATA_GAP)) {
+			ret = -EMSGSIZE;
+			bt_dev_err(hdev, "bad first %s memdump segment: too short for dump_size",
+				   qhci_subsys_name(subsys));
+			goto out_abort_md;
+		}
+
+		dump_size = le32_to_cpu(md_vse->dump_size);
+		if (!dump_size || dump_size > btqcom_memdump_maxsize[subsys]) {
+			ret = -EILSEQ;
+			bt_dev_err(hdev, "wrong %s memdump dump_size: %u",
+				   qhci_subsys_name(subsys), dump_size);
+			goto out_abort_md;
+		}
+
+		btqcom_reset_memdump(md);
+		md->from   = pkt_type;
+		md->subsys = subsys;
+		md->size   = dump_size;
+	} else {
+		if (md->subsys != subsys) {
+			ret = -EILSEQ;
+			bt_dev_err_ratelimited(hdev,
+					       "wrong memdump subsys: expected(%d), coming(%d)",
+					       md->subsys, subsys);
+			goto out_abort_md;
+		}
+
+		if (seq_no == QHCI_MEMDUMP_SEQ_LAST) {
+			md->seqno = QHCI_MEMDUMP_SEQ_LAST;
+		} else if (seq_no != md->seqno) {
+			ret = -EILSEQ;
+			bt_dev_err_ratelimited(hdev,
+					       "wrong memdump seqno: expected(%u), coming(%u)",
+					       md->seqno, seq_no);
+			goto out_abort_md;
+		}
+	}
+
+	/* @skb is consumed here */
+	ret = btqcom_submit_memdump(hdev, skb);
+	if (seq_no != QHCI_MEMDUMP_SEQ_LAST)
+		md->seqno = seq_no + 1;
+
+	goto out_reset_md;
+
+out_abort_md:
+	kfree_skb(skb);
+	if (qbt_data->md_pending_flags && !qbt_data->md_submit_err) {
+		hci_devcd_abort(hdev);
+		bt_dev_err(hdev, "abort memdump");
+	}
+
+out_reset_md:
+	if (ret < 0 && !qbt_data->md_submit_err)
+		qbt_data->md_submit_err = ret;
+
+	if (seq_no == QHCI_MEMDUMP_SEQ_LAST)
+		btqcom_reset_memdump(md);
+
+	return ret;
+}
+
+/*
+ * ============================================================================
+ * BT-HCI vendor-specific command/response
+ * ============================================================================
+ */
+
+/* reserved BT ACL handle for enhanced logging */
+#define QBT_HANDLE_ENHANCED_LOGGING 0xEDC
+/* reserved BT ACL handle for memdump */
+#define QBT_HANDLE_MEMDUMP 0xEDD
+
+/*
+ * Unless otherwise noted, for the VSCs below:
+ * - command payload: struct qhci_cp_simple
+ * - response payload: struct qhci_rp_generic
+ *
+ * Grouped by { opcode, sub_opcode, cp, rp }.
+ */
+
+/* BT EDL opcode and its sub-opcodes */
+#define QBT_OP_EDL		0xFC00
+
+#define BEDL_PATCH_GETVER	0x19
+struct qbt_rp_patch_getver {
+	u8	status;
+	u8	sub_opcode;
+	u8	dlen;
+	__le32	product_id;
+	__le16	patch_ver;
+	__le16	rom_ver;
+	__le32	soc_ver;
+} __packed;
+
+#define	BEDL_GET_BUILD_INFO 0x20
+struct qbt_rp_get_build_info {
+	u8	status;
+	u8	sub_opcode;
+	u8	dlen;
+	u8	data[];
+} __packed;
+
+#define BEDL_GET_BOARD_ID	0x23
+struct qbt_rp_get_board_id {
+	u8	status;
+	u8	sub_opcode;
+	u8	dlen;
+	__be16	board_id;
+} __packed;
+
+/* BT DEBUG opcode and its sub-opcodes */
+#define QBT_OP_DEBUG		0xFC0C
+
+#define BDBG_ERROR_FATAL_CMD	0x26
+/* no response */
+
+/* BT write BD_ADDR opcode, no sub-opcode */
+#define QBT_OP_WRITE_BD_ADDR	0xFC14
+struct qbt_cp_write_bd_addr {
+	bdaddr_t bdaddr;
+} __packed;
+
+/* BT firmware logging opcode and its sub-opcodes */
+#define QBT_OP_HOST_LOG		0xFC17
+
+#define BHL_ENH_ENABLE_LOG	0x14
+struct qbt_cp_config_fw_logging {
+	u8	sub_opcode;
+	u8	flags;
+} __packed;
+
+/*
+ * QBT_CHECK_RP_GENERIC - sanity check rp @_skb against type @_rp_type
+ * @_skb: the rp to check
+ * @_rp_type: the type to interpret @_skb as
+ * @_sub_opcode: sub_opcode to check @_skb against
+ *
+ * It is safe to evaluate @_skb and @_sub_opcode more than once for its
+ * usages.
+ *
+ * Returns a negative errno on failure, 0 on success, or error status code
+ * otherwise.
+ */
+#define QBT_CHECK_RP_GENERIC(_skb, _rp_type, _sub_opcode)		\
+({									\
+	_rp_type *_rp_ptr;						\
+	int _err = 0;							\
+	do {								\
+		if ((_skb)->len < sizeof(_rp_type)) {			\
+			_err = -EBADMSG;				\
+			break;						\
+		}							\
+		_rp_ptr = (void *)(_skb)->data;				\
+		if (_rp_ptr->sub_opcode != (_sub_opcode)) {		\
+			_err = -EILSEQ;					\
+			break;						\
+		}							\
+		if (_rp_ptr->status) {					\
+			_err = _rp_ptr->status;				\
+			break;						\
+		}							\
+	} while (0);							\
+	_err;								\
+})
+
+/* check the @dlen field on top of QBT_CHECK_RP_GENERIC() */
+#define QBT_CHECK_RP_DLEN(_skb, _rp_type, _sub_opcode)			\
+({									\
+	_rp_type *_rp_ptr;						\
+	int _err = 0;							\
+	do {								\
+		_err = QBT_CHECK_RP_GENERIC(_skb, _rp_type, _sub_opcode);\
+		if (_err)						\
+			break;						\
+		_rp_ptr = (void *)(_skb)->data;				\
+		if ((_skb)->len != offsetofend(_rp_type, dlen) +	\
+				   _rp_ptr->dlen) {			\
+			_err = -EMSGSIZE;				\
+			break;						\
+		}							\
+	} while (0);							\
+	_err;								\
+})
+
+/*
+ * qbt_edl_patch_getver - read BTC version info
+ * @hdev: the HCI device to query
+ * @ver: output version info, filled on success
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qbt_edl_patch_getver(struct hci_dev *hdev, struct qhci_btc_ver *ver)
+{
+	const struct qhci_cp_simple cp = { .sub_opcode = BEDL_PATCH_GETVER };
+	struct qbt_rp_patch_getver *rp;
+	struct sk_buff *skb;
+	int err;
+
+	skb = __hci_cmd_sync_ev(hdev, QBT_OP_EDL, sizeof(cp),
+				&cp, 0, HCI_INIT_TIMEOUT);
+	if (IS_ERR(skb)) {
+		err = PTR_ERR(skb);
+		goto out;
+	}
+
+	err = QBT_CHECK_RP_DLEN(skb, struct qbt_rp_patch_getver, cp.sub_opcode);
+	if (err)
+		goto out_free_skb;
+
+	rp = (void *)skb->data;
+	ver->product_id = le32_to_cpu(rp->product_id);
+	ver->soc_ver    = le32_to_cpu(rp->soc_ver);
+	ver->rom_ver    = le16_to_cpu(rp->rom_ver);
+	ver->patch_ver  = le16_to_cpu(rp->patch_ver);
+
+	bt_dev_dbg(hdev, "Product ID   :0x%08x", ver->product_id);
+	bt_dev_dbg(hdev, "SOC Version  :0x%08x", ver->soc_ver);
+	bt_dev_dbg(hdev, "ROM Version  :0x%04x", ver->rom_ver);
+	bt_dev_dbg(hdev, "Patch Version:0x%04x", ver->patch_ver);
+
+	if (ver->soc_ver == 0 || ver->rom_ver == 0)
+		err = -EILSEQ;
+
+out_free_skb:
+	kfree_skb(skb);
+
+	if (err > 0) {
+		bt_dev_dbg(hdev, "get BT version status error: 0x%02x",
+			   err);
+		err = -bt_to_errno(err);
+	}
+out:
+	if (err)
+		bt_dev_err(hdev, "get BT version failed: %pe", ERR_PTR(err));
+
+	return err;
+}
+
+/*
+ * qbt_edl_get_build_info - get BTC build info string
+ * @hdev: the HCI device to query
+ * @build_info: output build info string, allocated on success; caller frees
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qbt_edl_get_build_info(struct hci_dev *hdev, char **build_info)
+{
+	const struct qhci_cp_simple cp = { .sub_opcode = BEDL_GET_BUILD_INFO };
+	struct qbt_rp_get_build_info *rp;
+	struct sk_buff *skb;
+	int err;
+
+	skb = __hci_cmd_sync_ev(hdev, QBT_OP_EDL, sizeof(cp),
+				&cp, 0, HCI_INIT_TIMEOUT);
+	if (IS_ERR(skb)) {
+		err = PTR_ERR(skb);
+		goto out;
+	}
+
+	err = QBT_CHECK_RP_DLEN(skb, struct qbt_rp_get_build_info, cp.sub_opcode);
+	if (err)
+		goto out_free_skb;
+
+	rp = (void *)skb->data;
+	*build_info = kmemdup_nul(rp->data, rp->dlen, GFP_KERNEL);
+	if (!*build_info) {
+		err = -ENOMEM;
+		goto out_free_skb;
+	}
+
+	bt_dev_dbg(hdev, "BT build info: %s", *build_info);
+
+out_free_skb:
+	kfree_skb(skb);
+
+	if (err > 0) {
+		bt_dev_err(hdev, "get BT build info status error: 0x%02x",
+			   err);
+		err = -bt_to_errno(err);
+	}
+out:
+	if (err)
+		bt_dev_err(hdev, "get BT build info failed: %pe", ERR_PTR(err));
+
+	return err;
+}
+
+/*
+ * qbt_edl_get_board_id - get BTC board ID
+ * @hdev: the HCI device to query
+ * @board_id: output board ID, filled on success
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qbt_edl_get_board_id(struct hci_dev *hdev, u16 *board_id)
+{
+	const struct qhci_cp_simple cp = { .sub_opcode = BEDL_GET_BOARD_ID };
+	struct qbt_rp_get_board_id *rp;
+	struct sk_buff *skb;
+	int err;
+
+	skb = __hci_cmd_sync_ev(hdev, QBT_OP_EDL, sizeof(cp),
+				&cp, 0, HCI_INIT_TIMEOUT);
+	if (IS_ERR(skb)) {
+		err = PTR_ERR(skb);
+		goto out;
+	}
+
+	err = QBT_CHECK_RP_DLEN(skb, struct qbt_rp_get_board_id, cp.sub_opcode);
+	if (err)
+		goto out_free_skb;
+
+	rp = (void *)skb->data;
+	*board_id = be16_to_cpu(rp->board_id);
+	/* Take 0xffff as invalid board ID */
+	if (*board_id == 0xffff)
+		*board_id = 0;
+
+	bt_dev_dbg(hdev, "BT Board ID: 0x%04x", *board_id);
+
+out_free_skb:
+	kfree_skb(skb);
+
+	if (err > 0) {
+		bt_dev_dbg(hdev, "get BT board ID status error: 0x%02x",
+			   err);
+		err = -bt_to_errno(err);
+	}
+out:
+	if (err)
+		bt_dev_err(hdev, "get BT board ID failed: %pe", ERR_PTR(err));
+
+	return err;
+}
+
+/*
+ * qbt_error_fatal_cmd - trigger a controller-side fatal error for debugging
+ * @hdev: the HCI device to command
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qbt_error_fatal_cmd(struct hci_dev *hdev)
+{
+	const struct qhci_cp_simple cp = { .sub_opcode = BDBG_ERROR_FATAL_CMD };
+	int err;
+
+	err = __hci_cmd_send(hdev, QBT_OP_DEBUG, sizeof(cp), &cp);
+	if (err < 0)
+		bt_dev_err(hdev, "send error fatal cmd failed: %d", err);
+	else
+		bt_dev_info(hdev, "send error fatal cmd succeeded");
+
+	return err;
+}
+
+/*
+ * qbt_write_bda - set the controller's BD address
+ * @hdev: the HCI device to configure
+ * @bdaddr: the address to set
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qbt_write_bda(struct hci_dev *hdev, const bdaddr_t *bdaddr)
+{
+	struct qbt_cp_write_bd_addr cp;
+	int err;
+
+	/* The controller expects the address in reversed byte order. */
+	baswap(&cp.bdaddr, bdaddr);
+
+	err = __hci_cmd_sync_status(hdev, QBT_OP_WRITE_BD_ADDR,
+				    sizeof(cp), &cp, HCI_INIT_TIMEOUT);
+	if (err < 0) {
+		bt_dev_err(hdev, "set BT address failed: %pe",
+			   ERR_PTR(err));
+		return err;
+	}
+	if (err > 0) {
+		bt_dev_err(hdev, "set BT address status error: 0x%02x",
+			   err);
+		return -bt_to_errno(err);
+	}
+
+	bt_dev_info(hdev, "BT address set to %pMR", bdaddr);
+
+	return 0;
+}
+
+/*
+ * qbt_config_fw_logging - configure enhanced firmware logging
+ * @hdev: the HCI device to configure
+ * @flags: logging configuration flags, normally 1 (enable) or 0 (disable)
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qbt_config_fw_logging(struct hci_dev *hdev, u8 flags)
+{
+	const struct qbt_cp_config_fw_logging cp = {
+		.sub_opcode = BHL_ENH_ENABLE_LOG,
+		.flags      = flags,
+	};
+	struct sk_buff *skb;
+	int err;
+
+	skb = __hci_cmd_sync_ev(hdev, QBT_OP_HOST_LOG, sizeof(cp),
+				&cp, 0, HCI_INIT_TIMEOUT);
+	if (IS_ERR(skb)) {
+		err = PTR_ERR(skb);
+		goto out;
+	}
+
+	err = QBT_CHECK_RP_GENERIC(skb, struct qhci_rp_generic, cp.sub_opcode);
+	if (err)
+		goto out_free_skb;
+
+	bt_dev_dbg(hdev, "firmware logging configured (flags=0x%02x)", flags);
+
+out_free_skb:
+	kfree_skb(skb);
+
+	if (err > 0) {
+		bt_dev_dbg(hdev, "firmware logging config status error: 0x%02x", err);
+		err = -bt_to_errno(err);
+	}
+out:
+	if (err)
+		bt_dev_err(hdev, "firmware logging config failed: %pe", ERR_PTR(err));
+
+	return err;
+}
+
+/*
+ * ============================================================================
+ * qperi_handle_evt(): per-event handler and TX sync event sequence
+ * ============================================================================
+ */
+
+#define QPERI_EV_CLASS_PERI	0xF0
+#define QPERI_EV_TYPE_INVALID	0xff
+
+/* Pass QPERI_EV_TYPE_INVALID as event if no events to sync */
+#define DEFINE_QPERI_REQ_SPEC(specname, _opcode, _sub_opcode, _events...) \
+	const struct qhci_req_spec specname = {			\
+		.opcode     = (_opcode),				\
+		.sub_opcode = (_sub_opcode),				\
+		.event      = { _events, QPERI_EV_TYPE_INVALID },	\
+	}
+
+/*
+ * struct qperi_evt - per-event helper for qperi_handle_evt()
+ *
+ * @handler: event handler, executed before the TX sync event check.
+ *	Return: true if @skb was consumed, false otherwise.
+ *
+ * @verify_wakeup: extra verify if @skb belongs to the event sequence being synced.
+ *
+ *	Pull @skb empty if all its own checks pass and nothing is left for the
+ *	requester to verify, and the requester will get NULL as response, e.g. a
+ *	CSE, or a CCE whose payload has only status, or only status and
+ *	sub_opcode.
+ *
+ *	Return: 0 if @skb doesn't belong, 1 if it does, or a negative errno if
+ *		@skb is malformed.
+ */
+struct qperi_evt {
+	bool (*handler)(struct hci_dev *hdev, struct sk_buff *skb);
+	int (*verify_wakeup)(struct hci_dev *hdev, struct qhci_req *req,
+			     struct sk_buff *skb);
+};
+
+#define PERI_EV_CMD_STATUS	0x00
+struct peri_ev_cmd_status {
+	u8	status;
+	u8	ncmd;
+	__le16	opcode;
+} __packed;
+
+#define PERI_EV_CMD_COMPLETE	0x01
+struct peri_ev_cmd_complete {
+	u8	ncmd;
+	__le16	opcode;
+} __packed;
+
+#define PERI_EV_SUBSYS_ACTIVATE_COMPLETE	0x02
+struct peri_ev_subsys_activate_complete {
+	u8	subsys;
+	u8	action;
+} __packed;
+
+#define PERI_EV_SUBSYS_PATCH_NOTIFICATION	0x03
+struct peri_ev_subsys_patch_notification {
+	u8	status;
+	u8	subsys;
+} __packed;
+
+#define PERI_EV_CRASH_DUMP_MEMDUMP 0x04
+/* struct qhci_vse_memdump */
+
+#define PERI_EV_HARDWARE_ERROR	0x06
+struct peri_ev_hardware_error {
+	u8	code;
+} __packed;
+
+/* As qperi_evt::verify_wakeup() for PERI_EV_CMD_STATUS */
+static int peri_verify_cmd_status(struct hci_dev *hdev __maybe_unused,
+				  struct qhci_req *req, struct sk_buff *skb)
+{
+	const struct peri_ev_cmd_status *ev;
+
+	if (skb->len != sizeof(*ev))
+		return -EMSGSIZE;
+
+	ev = (const void *)skb->data;
+
+	if (le16_to_cpu(ev->opcode) != req->spec->opcode)
+		return 0;
+
+	req->req_result = ev->status;
+	if (!req->req_result)
+		skb_pull(skb, sizeof(*ev));
+
+	return 1;
+}
+
+/* CCE rp payload: [0] status, [1] sub_opcode if present, then rp-specific data if any */
+static int peri_verify_rp_payload(struct hci_dev *hdev __maybe_unused,
+				  struct qhci_req *req, struct sk_buff *skb)
+{
+	if (!skb->len)
+		return -EMSGSIZE;
+
+	if (req->spec->sub_opcode != QHCI_SUB_OPCODE_INVALID) {
+		if (skb->len < 2)
+			return -EMSGSIZE;
+
+		if (skb->data[1] != req->spec->sub_opcode)
+			return 0;
+	}
+
+	req->req_result = skb->data[0];
+	if (!req->req_result && skb->len <= 2)
+		skb_pull(skb, skb->len);
+
+	return 1;
+}
+
+/* As qperi_evt::verify_wakeup() for PERI_EV_CMD_COMPLETE */
+static int peri_verify_cmd_complete(struct hci_dev *hdev, struct qhci_req *req,
+				    struct sk_buff *skb)
+{
+	const struct peri_ev_cmd_complete *ev;
+	u16 opcode;
+
+	ev = skb_pull_data(skb, sizeof(*ev));
+	if (!ev)
+		return -EMSGSIZE;
+
+	opcode = le16_to_cpu(ev->opcode);
+	if (opcode != req->spec->opcode)
+		return 0;
+
+	hci_skb_opcode(skb) = opcode;
+
+	return peri_verify_rp_payload(hdev, req, skb);
+}
+
+/* As qperi_evt::verify_wakeup() for PERI_EV_SUBSYS_ACTIVATE_COMPLETE */
+static int peri_verify_subsys_activate_complete(struct hci_dev *hdev __maybe_unused,
+						struct qhci_req *req __maybe_unused,
+						struct sk_buff *skb)
+{
+	if (skb->len != sizeof(struct peri_ev_subsys_activate_complete))
+		return -EMSGSIZE;
+
+	return 1;
+}
+
+/* As qperi_evt::verify_wakeup() for PERI_EV_SUBSYS_PATCH_NOTIFICATION */
+static int peri_verify_subsys_patch_notification(struct hci_dev *hdev __maybe_unused,
+						 struct qhci_req *req,
+						 struct sk_buff *skb)
+{
+	const struct peri_ev_subsys_patch_notification *ev;
+
+	if (skb->len != sizeof(*ev))
+		return -EMSGSIZE;
+
+	ev = (const void *)skb->data;
+	req->req_result = ev->status;
+
+	return 1;
+}
+
+/* As qperi_evt::handler() for PERI_EV_CRASH_DUMP_MEMDUMP */
+static bool peri_crash_dump_memdump(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	btqcom_handle_memdump(hdev, skb);
+
+	return true;
+}
+
+/* As qperi_evt::handler() for PERI_EV_HARDWARE_ERROR */
+static bool peri_hardware_error(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	const struct peri_ev_hardware_error *ev;
+	u8 code = HCI_ERROR_UNSPECIFIED;
+
+	ev = skb_pull_data(skb, sizeof(*ev));
+	if (ev)
+		code = ev->code;
+	else
+		bt_dev_err(hdev, "malformed PERI hardware error event");
+
+	set_bit(QBT_MISC_HWERR_PERI, &qbt_data->misc_flags);
+	__hci_reset_dev(hdev, code);
+
+	return false;
+}
+
+static const struct qperi_evt qperi_evt_table[] = {
+	/* [0x00 = PERI_EV_CMD_STATUS] */
+	[PERI_EV_CMD_STATUS] = {
+		.verify_wakeup = peri_verify_cmd_status,
+	},
+	/* [0x01 = PERI_EV_CMD_COMPLETE] */
+	[PERI_EV_CMD_COMPLETE] = {
+		.verify_wakeup = peri_verify_cmd_complete,
+	},
+	/* [0x02 = PERI_EV_SUBSYS_ACTIVATE_COMPLETE] */
+	[PERI_EV_SUBSYS_ACTIVATE_COMPLETE] = {
+		.verify_wakeup = peri_verify_subsys_activate_complete,
+	},
+	/* [0x03 = PERI_EV_SUBSYS_PATCH_NOTIFICATION] */
+	[PERI_EV_SUBSYS_PATCH_NOTIFICATION] = {
+		.verify_wakeup = peri_verify_subsys_patch_notification,
+	},
+	/* [0x04 = PERI_EV_CRASH_DUMP_MEMDUMP] */
+	[PERI_EV_CRASH_DUMP_MEMDUMP] = {
+		.handler = peri_crash_dump_memdump,
+	},
+	/* [0x06 = PERI_EV_HARDWARE_ERROR] */
+	[PERI_EV_HARDWARE_ERROR] = {
+		.handler = peri_hardware_error,
+	},
+};
+
+/*
+ * qperi_try_wakeup - check if event @skb with type @ev_type can wake up the requester
+ * @hdev: the HCI device the event comes from
+ * @ev_type: the event type
+ * @skb: the event payload, or NULL on skb clone failure
+ *
+ * Note: the caller only cares about the return value rather than the wakeup result.
+ *
+ * Return: true if @skb was consumed, false otherwise.
+ */
+static bool qperi_try_wakeup(struct hci_dev *hdev, u8 ev_type,
+			     struct sk_buff *skb)
+{
+	const struct qperi_evt *evt = &qperi_evt_table[ev_type];
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct sk_buff *req_rsp = skb;
+	struct qhci_req *req;
+	int res;
+
+	/* fast path */
+	if (READ_ONCE(qbt_data->req_state) != QHCI_REQ_PEND)
+		return false;
+
+	guard(spinlock)(&qbt_data->req_spinlock);
+
+	if (qbt_data->req_state != QHCI_REQ_PEND)
+		return false;
+
+	req = qbt_data->req;
+	if (WARN_ON_ONCE(!req) || WARN_ON_ONCE(!req->spec))
+		return false;
+
+	if (req->spec->event[req->event_idx] != ev_type)
+		return false;
+
+	/* skb clone failure */
+	if (!skb) {
+		req->req_result = -ENOMEM;
+		goto out_wakeup;
+	}
+
+	/* malformed frame */
+	if (!skb->len) {
+		req->req_result = -EMSGSIZE;
+		goto out_wakeup;
+	}
+
+	req->req_result = 0;
+	if (evt->verify_wakeup) {
+		res = evt->verify_wakeup(hdev, req, skb);
+		if (!res)
+			return false;
+
+		if (res < 0) {
+			req->req_result = res;
+			goto out_wakeup;
+		}
+
+		/* HCI status error */
+		if (req->req_result)
+			goto out_wakeup;
+
+		/* nothing left for the requester to verify */
+		if (!req_rsp->len)
+			req_rsp = NULL;
+	}
+
+	if (req->spec->event[req->event_idx + 1] != QPERI_EV_TYPE_INVALID) {
+		req->event_idx++;
+		return false;
+	}
+
+out_wakeup:
+	if (req->req_result < 0)
+		req_rsp = NULL;
+
+	req->req_rsp = req_rsp;
+	if (req->req_result)
+		bt_dev_err(hdev, "PERI cmd opcode(0x%04x) sub_opcode(0x%02x) failed on ev_type(0x%02x): %d",
+			   req->spec->opcode, req->spec->sub_opcode, ev_type, req->req_result);
+	qbt_data->req_state = QHCI_REQ_DONE;
+	wake_up_interruptible(&qbt_data->req_wait_q);
+	/* test if the requester now owns @skb */
+	return req_rsp == skb;
+}
+
+/*
+ * __qperi_tx_sync_evt - send a frame and sync its event sequence
+ * @hdev: the HCI device
+ * @iter: the frame to send, NULL to sync only
+ * @spec: the request spec to sync, NULL to send only
+ * @timeout: timeout in jiffies for the sync
+ * @evt_idx: index of the waking event
+ * @status: HCI status code if it has one, 0 otherwise
+ *
+ * Requires the caller holds qbt_data->req_mutex.
+ *
+ * Return: ERR_PTR() on failure,
+ *	NULL on success for send-only or nothing left for the caller to verify,
+ *	the waking event otherwise.
+ */
+static struct sk_buff *__qperi_tx_sync_evt(struct hci_dev *hdev,
+					   struct iov_iter *iter,
+					   const struct qhci_req_spec *spec,
+					   unsigned int timeout,
+					   u8 *evt_idx, u8 *status)
+{
+	struct qhci_req req = { .pkt_type = QPERI_EVENT_PKT, .spec = spec };
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct sk_buff *ret = NULL;
+	int res = 0, wait_res = 0;
+	int req_state;
+
+	if (!iter && !spec)
+		return ERR_PTR(-EINVAL);
+
+	/* send only, no event to sync */
+	if (!spec) {
+		res = hci_send_vendor_frame(hdev, iter);
+		if (res < 0) {
+			bt_dev_err(hdev, "send-only PERI frame failed: %pe", ERR_PTR(res));
+			return ERR_PTR(res);
+		}
+		return NULL;
+	}
+
+	scoped_guard(spinlock, &qbt_data->req_spinlock) {
+		qbt_data->req = &req;
+		qbt_data->req_state = QHCI_REQ_PEND;
+	}
+
+	if (iter)
+		res = hci_send_vendor_frame(hdev, iter);
+
+	if (!res)
+		wait_res = wait_event_interruptible_timeout(qbt_data->req_wait_q,
+							    READ_ONCE(qbt_data->req_state) !=
+							    QHCI_REQ_PEND,
+							    timeout);
+
+	scoped_guard(spinlock, &qbt_data->req_spinlock) {
+		req_state = qbt_data->req_state;
+		qbt_data->req = NULL;
+		qbt_data->req_state = QHCI_REQ_DONE;
+	}
+
+	if (res >= 0) {
+		switch (req_state) {
+		case QHCI_REQ_DONE:
+			res = req.req_result;
+			break;
+		case QHCI_REQ_CANCELED:
+			if (req.req_result < 0)
+				bt_dev_info(hdev, "PERI cmd opcode(0x%04x) sub_opcode(0x%02x) canceled: %pe",
+					    spec->opcode, spec->sub_opcode,
+					    ERR_PTR(req.req_result));
+			else
+				bt_dev_info(hdev, "PERI cmd opcode(0x%04x) sub_opcode(0x%02x) canceled",
+					    spec->opcode, spec->sub_opcode);
+			res = req.req_result < 0 ? req.req_result : -ECANCELED;
+			break;
+		case QHCI_REQ_PEND:
+		default:
+			res = wait_res < 0 ? -EINTR : -ETIMEDOUT;
+			break;
+		}
+	}
+
+	ret = req.req_rsp;
+	if (res < 0) {
+		/* log unless qperi_try_wakeup() already did */
+		if (res != req.req_result)
+			bt_dev_err(hdev, "PERI cmd opcode(0x%04x) sub_opcode(0x%02x) failed: %pe",
+				   spec->opcode, spec->sub_opcode, ERR_PTR(res));
+
+		if (WARN_ON_ONCE(req.req_rsp))
+			kfree_skb(req.req_rsp);
+
+		ret = ERR_PTR(res);
+	}
+
+	if (evt_idx)
+		*evt_idx = req.event_idx;
+	if (status)
+		*status = (u8)req.req_result;
+	return ret;
+}
+
+/* synchronously cancel pending request with error code @err */
+static void qperi_tx_sync_cancel_sync(struct hci_dev *hdev, int err)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct qhci_req *req;
+
+	guard(spinlock)(&qbt_data->req_spinlock);
+
+	if (qbt_data->req_state != QHCI_REQ_PEND)
+		return;
+
+	req = qbt_data->req;
+	if (WARN_ON_ONCE(!req))
+		return;
+
+	req->req_result = err;
+	qbt_data->req = NULL;
+	qbt_data->req_state = QHCI_REQ_CANCELED;
+
+	wake_up_interruptible(&qbt_data->req_wait_q);
+}
+
+/*
+ * __qperi_cmd_sync_evt - send a command and sync its event sequence
+ * @hdev: the HCI device
+ * @opcode: command opcode, QHCI_OPCODE_INVALID to sync only
+ * @plen: length of @cp
+ * @cp: the command payload
+ * @spec: the request spec to sync, NULL to send only
+ * @evt_idx: index of the waking event
+ * @status: HCI status code if it has one, 0 otherwise
+ *
+ * Requires the caller holds qbt_data->req_mutex.
+ *
+ * Return: ERR_PTR() on failure,
+ *	NULL on success for send-only or nothing left for the caller to verify,
+ *	the waking event otherwise.
+ */
+static struct sk_buff *__qperi_cmd_sync_evt(struct hci_dev *hdev,
+					    u16 opcode, u32 plen, const void *cp,
+					    const struct qhci_req_spec *spec,
+					    u8 *evt_idx, u8 *status)
+{
+	u8 pkt_type = QPERI_COMMAND_PKT;
+	struct qperi_command_hdr cmd_hdr = {
+		.host_id = QHCI_HOST_ID_BT,
+		.opcode  = cpu_to_le16(opcode),
+		.plen    = (u8)plen,
+	};
+	struct kvec cmd_kv[] = {
+		{ .iov_base = &pkt_type,       .iov_len = 1 },
+		{ .iov_base = &cmd_hdr,        .iov_len = sizeof(cmd_hdr) },
+		{ .iov_base = (void *)cp,      .iov_len = plen },
+	};
+	struct iov_iter iter;
+
+	/* no command to send, sync only */
+	if (opcode == QHCI_OPCODE_INVALID)
+		return __qperi_tx_sync_evt(hdev, NULL, spec, HCI_INIT_TIMEOUT, evt_idx, status);
+
+	iov_iter_kvec(&iter, ITER_SOURCE, cmd_kv, plen ? 3 : 2,
+		      1 + sizeof(cmd_hdr) + plen);
+
+	return __qperi_tx_sync_evt(hdev, &iter, spec, HCI_INIT_TIMEOUT, evt_idx, status);
+}
+
+static inline struct sk_buff *qperi_cmd_sync_evt(struct hci_dev *hdev,
+						 u16 opcode, u32 plen, const void *cp,
+						 const struct qhci_req_spec *spec,
+						 u8 *evt_idx, u8 *status)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+
+	guard(mutex)(&qbt_data->req_mutex);
+
+	return __qperi_cmd_sync_evt(hdev, opcode, plen, cp, spec, evt_idx, status);
+}
+
+/*
+ * send a command and sync an event with type @ev_type;
+ * @cp's first byte is sub_opcode for all commands so far
+ *
+ * Requires the caller holds qbt_data->req_mutex.
+ */
+static struct sk_buff *__qperi_cmd_sync_one_evt(struct hci_dev *hdev,
+						u16 opcode, u32 plen, const void *cp,
+						u8 ev_type, u8 *status)
+{
+	u8 sub_opcode = plen ? *(const u8 *)cp : QHCI_SUB_OPCODE_INVALID;
+	DEFINE_QPERI_REQ_SPEC(spec, opcode, sub_opcode, ev_type);
+
+	return __qperi_cmd_sync_evt(hdev, opcode, plen, cp, &spec, NULL, status);
+}
+
+static inline struct sk_buff *qperi_cmd_sync_one_evt(struct hci_dev *hdev,
+						     u16 opcode, u32 plen, const void *cp,
+						     u8 ev_type, u8 *status)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+
+	guard(mutex)(&qbt_data->req_mutex);
+
+	return __qperi_cmd_sync_one_evt(hdev, opcode, plen, cp, ev_type, status);
+}
+
+/*
+ * only wait for an unsolicited event with type @ev_type
+ *
+ * Requires the caller holds qbt_data->req_mutex.
+ */
+static struct sk_buff __maybe_unused *__qperi_wait_one_evt(struct hci_dev *hdev,
+							   u8 ev_type, u8 *status)
+{
+	DEFINE_QPERI_REQ_SPEC(spec, QHCI_OPCODE_INVALID, QHCI_SUB_OPCODE_INVALID, ev_type);
+
+	return __qperi_tx_sync_evt(hdev, NULL, &spec, HCI_INIT_TIMEOUT, NULL, status);
+}
+
+/*
+ * handle a received PERI event @skb, called from hdev->recv_vendor_pkt();
+ * generic processing first, then the TX sync event, without interfering
+ */
+static void qperi_handle_evt(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	const struct qhci_vse_comm *vse_comm;
+	const struct qperi_event_hdr *hdr;
+	const struct qperi_evt *evt_entry;
+	/* for qperi_try_wakeup() call below */
+	struct sk_buff *orig_skb = NULL;
+	u8 ev_type;
+	int res;
+
+	ev_type = QPERI_EV_TYPE_INVALID;
+	res = -EBADMSG;
+	hdr = skb_pull_data(skb, QPERI_EVENT_HDR_SIZE);
+	if (!hdr || hdr->evt != HCI_EV_VENDOR)
+		goto out_free_skb;
+
+	res = -EILSEQ;
+	vse_comm = skb_pull_data(skb, QHCI_VSE_COMM_SIZE);
+	if (!vse_comm || vse_comm->ev_class != QPERI_EV_CLASS_PERI)
+		goto out_free_skb;
+
+	ev_type = vse_comm->ev_type;
+	res = -ENOENT;
+	if (ev_type >= ARRAY_SIZE(qperi_evt_table))
+		goto out_free_skb;
+
+	res = 0;
+	hci_skb_event(skb) = ev_type;
+	evt_entry = &qperi_evt_table[ev_type];
+
+	if (evt_entry->handler) {
+		const struct qhci_req *req;
+		bool may_wakeup;
+
+		spin_lock(&qbt_data->req_spinlock);
+		req = qbt_data->req;
+		may_wakeup = qbt_data->req_state == QHCI_REQ_PEND &&
+			req && req->spec &&
+			req->spec->event[req->event_idx] == ev_type;
+		spin_unlock(&qbt_data->req_spinlock);
+
+		if (may_wakeup) {
+			orig_skb = skb_clone(skb, GFP_KERNEL);
+			if (!orig_skb)
+				res = -ENOMEM;
+		}
+
+		if (evt_entry->handler(hdev, skb))
+			skb = NULL;
+	} else {
+		orig_skb = skb;
+		skb = NULL;
+	}
+
+	if (qperi_try_wakeup(hdev, ev_type, orig_skb))
+		orig_skb = NULL;
+
+out_free_skb:
+	kfree_skb(orig_skb);
+	kfree_skb(skb);
+	if (res < 0)
+		bt_dev_err(hdev, "fails to handle PERI event with type 0x%02x: %pe",
+			   ev_type, ERR_PTR(res));
+}
+
+/*
+ * ============================================================================
+ * PERI-HCI vendor-specific command/response
+ * ============================================================================
+ */
+
+/* reserved PERI ACL handle for enhanced logging */
+#define QPERI_HANDLE_ENHANCED_LOGGING 0xEC0
+/* reserved PERI ACL handle for memdump */
+#define QPERI_HANDLE_MEMDUMP 0xEC1
+
+/*
+ * Unless otherwise noted, for the VSCs below:
+ * - command payload: struct qhci_cp_simple
+ * - response payload: struct qhci_rp_generic
+ *
+ * Grouped by { opcode, sub_opcode, cp, rp }.
+ */
+
+/* PERI EDL opcode and its sub-opcodes */
+#define QPERI_OP_EDL		0xFFF0
+
+#define PEDL_GET_BUILD_INFO	0x09
+struct qperi_cp_generic {
+	u8	sub_opcode;
+	u8	subsys;
+} __packed;
+struct qperi_rp_get_build_info {
+	u8	status;
+	u8	sub_opcode;
+	u8	subsys;
+	u8	dlen;
+	u8	data[];
+} __packed;
+
+/* PERI Generic opcode and its sub-opcodes */
+#define QPERI_OP_GENERIC	0xFFF1
+
+#define PGEN_PERI_RESET		0x03
+/* struct qhci_rp_generic */
+/* struct peri_ev_subsys_patch_notification */
+
+#define PGEN_INITIATE_BT_CRASH	0x05
+/* struct peri_ev_cmd_status */
+
+#define __QPERI_CP_INIT_SUBSYS_0(...)
+#define __QPERI_CP_INIT_SUBSYS_1(_subsys, ...)	.subsys = (_subsys),
+
+#define QPERI_CP_INITIALIZER(_sub_opcode, _subsys...) {			\
+	.sub_opcode = (_sub_opcode),					\
+	CONCATENATE(__QPERI_CP_INIT_SUBSYS_, COUNT_ARGS(_subsys))(_subsys)	\
+}
+
+/*
+ * QPERI_CHECK_RP_LEN - check rp @_skb's length and its @dlen
+ * @_skb: the rp to check
+ * @_rp_type: the type to interpret @_skb as
+ *
+ * It is safe to evaluate @_skb more than once for its usages.
+ *
+ * Returns a negative errno on failure, or 0 on success.
+ */
+#define QPERI_CHECK_RP_LEN(_skb, _rp_type)				\
+({									\
+	_rp_type *_rp_ptr;						\
+	int _err = 0;							\
+	do {								\
+		if (!(_skb) || (_skb)->len < sizeof(_rp_type)) {	\
+			_err = -EBADMSG;				\
+			break;						\
+		}							\
+		_rp_ptr = (void *)(_skb)->data;				\
+		if ((_skb)->len != offsetofend(_rp_type, dlen) +	\
+				   _rp_ptr->dlen) {			\
+			_err = -EMSGSIZE;				\
+			break;						\
+		}							\
+	} while (0);							\
+	_err;								\
+})
+
+/*
+ * qperi_get_subsys_build_info - get subsystem build info string
+ * @hdev: the HCI device to query
+ * @subsys: the subsystem to query
+ * @build_info: output build info string, allocated on success; caller frees
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qperi_get_subsys_build_info(struct hci_dev *hdev, u8 subsys,
+				       char **build_info)
+{
+	struct qperi_cp_generic cp = QPERI_CP_INITIALIZER(PEDL_GET_BUILD_INFO, subsys);
+	struct qperi_rp_get_build_info *rp;
+	struct sk_buff *skb;
+	u8 status = 0;
+	int ret;
+
+	skb = qperi_cmd_sync_one_evt(hdev, QPERI_OP_EDL, sizeof(cp), &cp,
+				     PERI_EV_CMD_COMPLETE, &status);
+	if (IS_ERR(skb))
+		return PTR_ERR(skb);
+
+	ret = QPERI_CHECK_RP_LEN(skb, struct qperi_rp_get_build_info);
+	if (ret)
+		goto out_free_skb;
+	rp = (void *)skb->data;
+
+	if (status) {
+		ret = -bt_to_errno(status);
+		goto out_free_skb;
+	}
+
+	if (rp->subsys != subsys) {
+		ret = -EILSEQ;
+		goto out_free_skb;
+	}
+
+	*build_info = kmemdup_nul(rp->data, rp->dlen, GFP_KERNEL);
+	ret = *build_info ? 0 : -ENOMEM;
+
+out_free_skb:
+	kfree_skb(skb);
+
+	if (ret)
+		bt_dev_err(hdev, "get %s build info failed: %pe",
+			   qhci_subsys_name(subsys), ERR_PTR(ret));
+	else
+		bt_dev_dbg(hdev, "%s build info: %s",
+			   qhci_subsys_name(subsys), *build_info);
+
+	return ret;
+}
+
+/*
+ * qperi_hci_reset - reset PERI HCI
+ * @hdev: the HCI device to reset
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qperi_hci_reset(struct hci_dev *hdev)
+{
+	DEFINE_QPERI_REQ_SPEC(spec, QPERI_OP_GENERIC, PGEN_PERI_RESET,
+			      PERI_EV_CMD_COMPLETE, PERI_EV_SUBSYS_PATCH_NOTIFICATION);
+	struct qhci_cp_simple cp = QPERI_CP_INITIALIZER(PGEN_PERI_RESET);
+	struct sk_buff *skb;
+	u8 evt_idx = 0;
+	u8 status = 0;
+	int ret = 0;
+
+	skb = qperi_cmd_sync_evt(hdev, QPERI_OP_GENERIC, sizeof(cp), &cp,
+				 &spec, &evt_idx, &status);
+	if (IS_ERR(skb))
+		return PTR_ERR(skb);
+
+	if (status) {
+		ret = -bt_to_errno(status);
+	} else if (evt_idx == 1) {
+		const struct peri_ev_subsys_patch_notification *ev;
+
+		ev = skb_pull_data(skb, sizeof(*ev));
+		if (ev->subsys != QHCI_SUBSYS_PERI) {
+			ret = -EILSEQ;
+			bt_dev_err(hdev, "PERI HCI reset via QHCI failed: wrong subsys %d",
+				   ev->subsys);
+		}
+	}
+
+	kfree_skb(skb);
+
+	if (!ret)
+		bt_dev_info(hdev, "PERI HCI reset via QHCI succeeded");
+
+	return ret;
+}
+
+/*
+ * qperi_initiate_bt_crash - initiate a BT subsystem crash
+ * @hdev: the HCI device
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int qperi_initiate_bt_crash(struct hci_dev *hdev)
+{
+	struct qhci_cp_simple cp = QPERI_CP_INITIALIZER(PGEN_INITIATE_BT_CRASH);
+	struct sk_buff *skb;
+	u8 status = 0;
+	int ret = 0;
+
+	skb = qperi_cmd_sync_one_evt(hdev, QPERI_OP_GENERIC, sizeof(cp), &cp,
+				     PERI_EV_CMD_STATUS, &status);
+	if (IS_ERR(skb))
+		return PTR_ERR(skb);
+
+	if (skb) {
+		ret = -bt_to_errno(status);
+		kfree_skb(skb);
+	}
+
+	if (!ret)
+		bt_dev_info(hdev, "PERI initiate BT crash succeeded");
+
+	return ret;
+}
+
+/*
+ * ============================================================================
+ * hdev callbacks
+ * ============================================================================
+ */
+
+/* test if a HW or SW reset is available */
+static inline bool btqcom_has_btc_reset(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+
+	return qbt_data->flags & (QBT_FLAG_HW_RESET | QBT_FLAG_SW_RESET);
+}
+
+/*
+ * btqcom_set_bdaddr - set BD_ADDR
+ * @hdev: the HCI device
+ * @bdaddr: the BD_ADDR to set
+ *
+ * Implements hdev->set_bdaddr().
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btqcom_set_bdaddr(struct hci_dev *hdev, const bdaddr_t *bdaddr)
+{
+	int ret;
+
+	ret = __hci_reset_sync(hdev);
+	if (ret) {
+		bt_dev_err(hdev, "HCI reset before setting BD_ADDR failed: %pe",
+			   ERR_PTR(ret));
+		return ret;
+	}
+
+	ret = qbt_write_bda(hdev, bdaddr);
+
+	return ret;
+}
+
+/*
+ * btqcom_handle_ev_vendor - handle @skb if interested
+ * @hdev: the HCI device @skb comes from
+ * @skb: the VSE payload
+ *
+ * Implements hdev->handle_ev_vendor().
+ *
+ * Return: true if @skb was handled, false otherwise.
+ */
+static bool btqcom_handle_ev_vendor(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	const struct qhci_vse_comm *vse_comm;
+	u16 vse_id;
+
+	if (skb->len < QHCI_VSE_COMM_SIZE)
+		return false;
+
+	vse_comm = (const void *)skb->data;
+	vse_id = vse_comm->ev_class << 8 | vse_comm->ev_type;
+
+	switch (vse_id) {
+	case (QBT_EV_CLASS_DATALOG << 8) | QBT_EV_TYPE_MEMDUMP:
+		skb_pull_data(skb, QHCI_VSE_COMM_SIZE);
+		btqcom_handle_memdump(hdev, skb_get(skb));
+		return true;
+	default:
+		return false;
+	}
+}
+
+/* handle a received BT vendor ACL frame */
+static void handle_acl_vendor(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	const struct qhci_vse_comm *vse_comm;
+	const struct hci_event_hdr *evt_hdr;
+	u16 vse_id;
+
+	if (hci_acl_handle(skb) != QBT_HANDLE_MEMDUMP)
+		goto out_free_skb;
+
+	skb_pull(skb, HCI_ACL_HDR_SIZE);
+
+	evt_hdr = skb_pull_data(skb, HCI_EVENT_HDR_SIZE);
+	if (!evt_hdr || evt_hdr->evt != HCI_EV_VENDOR)
+		goto out_free_skb;
+
+	vse_comm = skb_pull_data(skb, QHCI_VSE_COMM_SIZE);
+	if (!vse_comm)
+		goto out_free_skb;
+
+	vse_id = vse_comm->ev_class << 8 | vse_comm->ev_type;
+
+	switch (vse_id) {
+	case (QBT_EV_CLASS_DATALOG << 8) | QBT_EV_TYPE_MEMDUMP:
+		btqcom_handle_memdump(hdev, skb);
+		return;
+	default:
+		break;
+	}
+
+out_free_skb:
+	kfree_skb(skb);
+}
+
+/* handle a received PERI ACL frame */
+static void qperi_handle_acl(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	const struct hci_event_hdr *bt_evt_hdr;
+	const struct qhci_vse_comm *vse_comm;
+	u8 ev_type;
+
+	if (qperi_acl_handle(skb) != QPERI_HANDLE_MEMDUMP)
+		goto out_free_skb;
+
+	skb_pull(skb, QPERI_ACL_HDR_SIZE);
+
+	bt_evt_hdr = skb_pull_data(skb, HCI_EVENT_HDR_SIZE);
+	if (!bt_evt_hdr || bt_evt_hdr->evt != HCI_EV_VENDOR)
+		goto out_free_skb;
+
+	vse_comm = skb_pull_data(skb, sizeof(*vse_comm));
+	if (!vse_comm || vse_comm->ev_class != QPERI_EV_CLASS_PERI)
+		goto out_free_skb;
+
+	ev_type = vse_comm->ev_type;
+	switch (ev_type) {
+	case PERI_EV_CRASH_DUMP_MEMDUMP:
+		btqcom_handle_memdump(hdev, skb);
+		return;
+	default:
+		break;
+	}
+
+out_free_skb:
+	kfree_skb(skb);
+}
+
+/* Implements hdev->recv_vendor_pkt(). */
+static void btqcom_recv_vendor_pkt(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	hci_skb_pkt_type(skb) = *(const u8 *)skb_pull_data(skb, 1);
+
+	switch (hci_skb_pkt_type(skb)) {
+	case QPERI_EVENT_PKT:
+		qperi_handle_evt(hdev, skb);
+		break;
+	case QPERI_ACLDATA_PKT:
+		qperi_handle_acl(hdev, skb);
+		break;
+	case HCI_ACLDATA_PKT:
+		handle_acl_vendor(hdev, skb);
+		break;
+	default:
+		bt_dev_err(hdev, "unexpected vendor HCI frame with pkt_type 0x%02x",
+			   hci_skb_pkt_type(skb));
+		kfree_skb(skb);
+		break;
+	}
+}
+
+/* Implements hdev->dump.coredump() for hci_devcd_register(). */
+static void btusb_qcom_trigger_memdump(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+
+	if (qbt_data->category == QBTC_CAT_MSUBSYS)
+		qperi_initiate_bt_crash(hdev);
+	else
+		qbt_error_fatal_cmd(hdev);
+}
+
+/* return 0 on success, or a negative errno on failure */
+static int btusb_qcom_deactivate_msubsys_bt(struct hci_dev *hdev)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	int ret;
+
+	ret = usb_autopm_get_interface(xport_data->intf);
+	if (ret) {
+		bt_dev_err(hdev, "get autopm for deactivate BT failed: %pe", ERR_PTR(ret));
+		return ret;
+	}
+
+	ret = qdfu_activate_remote_btss(hdev, false, 100 * 1000);
+
+	usb_autopm_put_interface(xport_data->intf);
+
+	if (!ret)
+		bt_dev_info(hdev, "deactivate BT succeeded");
+
+	return ret;
+}
+
+/* return 0 on success, or a negative errno on failure */
+static int btusb_qcom_sw_reset(struct hci_dev *hdev)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	int ret = -EOPNOTSUPP;
+
+	if (!(qbt_data->flags & QBT_FLAG_SW_RESET))
+		return ret;
+
+	ret = usb_autopm_get_interface(xport_data->intf);
+	if (ret) {
+		bt_dev_err(hdev, "get autopm for SW reset failed: %pe", ERR_PTR(ret));
+		return ret;
+	}
+
+	ret = qdfu_sw_reset(hdev);
+
+	usb_autopm_put_interface(xport_data->intf);
+
+	return ret;
+}
+
+#define QBT_RESET_TYPE_SYNC	"sync"
+#define QBT_RESET_TYPE_ASYNC	"async"
+#define QBT_RESET_TYPE_DIRECT	"direct"
+
+/*
+ * btusb_do_reset_work - reset the BTC
+ * @hdev: the HCI device
+ * @type: QBT_RESET_TYPE_* above, for logging
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_do_reset_work(struct hci_dev *hdev, void *type)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	struct gpio_desc *reset_gpio = xport_data->reset_gpio;
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	int ret;
+
+	bt_dev_dbg(hdev, "%s reset: misc_flags(0x%lx)", (char *)type,
+		   READ_ONCE(qbt_data->misc_flags));
+	bt_dev_dbg(hdev, "md_state: %s, md_submit_err: %d",
+		   hci_devcd_state_name(READ_ONCE(qbt_data->md_state)),
+		   READ_ONCE(qbt_data->md_submit_err));
+
+	if (test_and_set_bit(QBT_MISC_RESET_ACTIVE, &qbt_data->misc_flags)) {
+		bt_dev_info(hdev, "reset already in progress");
+		return 0;
+	}
+
+	if (xport_data->prepare_reset)
+		xport_data->prepare_reset(hdev);
+
+	if (reset_gpio) {
+		bt_dev_info(hdev, "hardware reset");
+		gpiod_set_value_cansleep(reset_gpio, 0);
+		fsleep(200 * 1000);
+		gpiod_set_value_cansleep(reset_gpio, 1);
+		return 0;
+	}
+
+	ret = btusb_qcom_sw_reset(hdev);
+	if (!ret)
+		return 0;
+
+	ret = usb_autopm_get_interface(xport_data->intf);
+	if (ret) {
+		bt_dev_err(hdev, "reset: autopm get failed: %pe", ERR_PTR(ret));
+		clear_bit(QBT_MISC_RESET_ACTIVE, &qbt_data->misc_flags);
+		return ret;
+	}
+	usb_autopm_put_interface_no_suspend(xport_data->intf);
+
+	bt_dev_info(hdev, "usb reset");
+	/* Clear it here since usb reset isn't guaranteed to succeed. */
+	clear_bit(QBT_MISC_RESET_ACTIVE, &qbt_data->misc_flags);
+	usb_queue_reset_device(xport_data->intf);
+
+	return 0;
+}
+
+static void btusb_qcom_reset_sync(struct hci_dev *hdev)
+{
+	int res;
+
+	res = hci_cmd_sync_queue_once(hdev, btusb_do_reset_work, QBT_RESET_TYPE_SYNC, NULL);
+	if (res)
+		bt_dev_dbg(hdev, "queue reset work failed: %pe", ERR_PTR(res));
+	if (!res || res == -EEXIST || res == -ENODEV)
+		return;
+
+	btusb_do_reset_work(hdev, QBT_RESET_TYPE_DIRECT);
+}
+
+static void btqcom_reset_async(struct hci_dev *hdev, unsigned int delay_ms)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+
+	set_bit(QBT_WORK_RESET_HDEV, &qbt_data->work_flags);
+	schedule_delayed_work(&qbt_data->dwork, msecs_to_jiffies(delay_ms));
+}
+
+/*
+ * btusb_do_cmd_timeout_work - HCI cmd sync work function to handle command timeout
+ * @hdev: the HCI device
+ * @data: unused
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_do_cmd_timeout_work(struct hci_dev *hdev,
+				     void *data __maybe_unused)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	unsigned int wait_ms = 0;
+	int ret = 0;
+
+	if (!(qbt_data->flags & QBT_FLAG_MEMDUMP) ||
+	    !(qbt_data->flags & QBT_FLAG_CMD_TIMEOUT_MEMDUMP))
+		goto out_reset;
+
+	switch (qbt_data->category) {
+	case QBTC_CAT_LEGACY:
+		return -EOPNOTSUPP;
+	case QBTC_CAT_UNIFIED:
+		wait_ms = 800;
+		ret = qbt_error_fatal_cmd(hdev);
+		break;
+	case QBTC_CAT_MSUBSYS:
+		wait_ms = 1200;
+		ret = qperi_initiate_bt_crash(hdev);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (ret)
+		goto out_reset;
+
+	ret = wait_var_event_timeout(&qbt_data->misc_flags,
+				     test_bit(QBT_MISC_MEMDUMP_INCOMING,
+					      &qbt_data->misc_flags) ||
+				     !test_bit(QBT_MISC_CMD_TIMEOUT,
+					       &qbt_data->misc_flags),
+				     msecs_to_jiffies(wait_ms));
+	if (!test_bit(QBT_MISC_CMD_TIMEOUT, &qbt_data->misc_flags)) {
+		bt_dev_dbg(hdev, "Flag CMD_TIMEOUT cleared");
+		return 0;
+	}
+	if (ret) {
+		bt_dev_dbg(hdev, "trigger memdump on command timeout succeeded");
+		return 0;
+	}
+	bt_dev_err(hdev, "trigger memdump on command timeout failed");
+
+out_reset:
+	btusb_do_reset_work(hdev, QBT_RESET_TYPE_SYNC);
+
+	return 0;
+}
+
+/* Implements hdev->reset(). */
+static void btusb_qcom_reset(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	bool has_cmd_timeout;
+	int res;
+
+	has_cmd_timeout = current_work() == &hdev->cmd_timer.work;
+	bt_dev_info(hdev, "reset triggered by %s",
+		    has_cmd_timeout ? "command timeout" : "user");
+
+	if (test_bit(QBT_MISC_MEMDUMP_PERI, &qbt_data->misc_flags) ||
+	    test_bit(QBT_MISC_MEMDUMP_BT, &qbt_data->misc_flags) ||
+	    test_bit(QBT_MISC_MEMDUMP_TMEL, &qbt_data->misc_flags)) {
+		bt_dev_info(hdev, "reset will happen after memdump");
+		return;
+	}
+
+	if (!has_cmd_timeout) {
+		btusb_qcom_reset_sync(hdev);
+		return;
+	}
+
+	if (test_and_set_bit(QBT_MISC_CMD_TIMEOUT, &qbt_data->misc_flags)) {
+		bt_dev_info(hdev, "handling command timeout is in progress");
+		return;
+	}
+
+	res = hci_cmd_sync_queue_once(hdev, btusb_do_cmd_timeout_work, NULL, NULL);
+	if (res)
+		bt_dev_dbg(hdev, "queue command timeout work failed: %pe", ERR_PTR(res));
+	if (!res || res == -EEXIST || res == -ENODEV)
+		return;
+
+	btqcom_reset_async(hdev, 0);
+}
+
+/* Implements hdev->hw_error(). */
+static void btqcom_hw_error(struct hci_dev *hdev, u8 code)
+{
+	const char *subsys_name = qhci_subsys_name(QHCI_SUBSYS_PERI);
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	int res;
+
+	if (!test_bit(QBT_MISC_HWERR_PERI, &qbt_data->misc_flags)) {
+		set_bit(QBT_MISC_HWERR_BT, &qbt_data->misc_flags);
+		subsys_name = qhci_subsys_name(QHCI_SUBSYS_INVALID);
+	}
+
+	bt_dev_info(hdev, "%s hardware error (0x%02x)", subsys_name, code);
+	bt_dev_dbg(hdev, "md_pending_flags: 0x%lx", qbt_data->md_pending_flags);
+	bt_dev_dbg(hdev, "md_submit_err: %d", qbt_data->md_submit_err);
+
+	if (qbt_data->md_pending_flags && !qbt_data->md_submit_err) {
+		res = hci_devcd_complete(hdev);
+		if (res)
+			bt_dev_err(hdev, "memdump complete on %s hardware error failed: %pe",
+				   subsys_name, ERR_PTR(res));
+		else
+			bt_dev_info(hdev, "memdump completed on %s hardware error: %u bytes",
+				    subsys_name, qbt_data->md_submit_size);
+	}
+
+	qbt_data->md_pending_flags = 0;
+
+	if (test_and_clear_bit(QBT_MISC_MEMDUMP_INCOMING, &qbt_data->misc_flags))
+		bt_dev_dbg(hdev, "clear MEMDUMP_INCOMING on %s hardware error", subsys_name);
+}
+
+static int btusb_qcom_shutdown_unified(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	bool had_hwerr, had_memdump;
+	unsigned int delay_ms = 10;
+	int ret;
+
+	had_memdump = test_bit(QBT_MISC_MEMDUMP_BT, &qbt_data->misc_flags);
+	had_hwerr = test_bit(QBT_MISC_HWERR_BT, &qbt_data->misc_flags);
+
+	bt_dev_dbg(hdev, "shutdown: had_memdump %d, had_hwerr %d",
+		   had_memdump, had_hwerr);
+
+	if (!had_hwerr && !had_memdump) {
+		ret = __hci_reset_sync(hdev);
+		if (ret)
+			bt_dev_err(hdev, "shutdown: HCI reset failed: %pe",
+				   ERR_PTR(ret));
+		else
+			bt_dev_info(hdev, "shutdown: HCI reset succeeded");
+		goto out;
+	}
+
+	if (!had_memdump) {
+		ret = __hci_cmd_sync_status(hdev, HCI_OP_RESET, 0, NULL,
+					    HCI_CMD_TIMEOUT);
+		if (!ret) {
+			clear_bit(QBT_MISC_HWERR_BT, &qbt_data->misc_flags);
+			bt_dev_info(hdev, "shutdown: recovered from hardware error via HCI reset");
+			goto out;
+		}
+
+		bt_dev_warn(hdev, "shutdown: HCI reset failed to recover from hardware error: %pe",
+			    ERR_PTR(ret));
+	}
+
+	ret = -EIO;
+	if (had_memdump && !btqcom_has_btc_reset(hdev)) {
+		bt_dev_info(hdev, "shutdown: BTC will self-recover via re-enumeration");
+		return ret;
+	}
+
+	bt_dev_info(hdev, "shutdown: reset BTC after %u msec", delay_ms);
+	btqcom_reset_async(hdev, delay_ms);
+
+	return ret;
+out:
+	if (test_and_clear_bit(QBT_MISC_CMD_TIMEOUT, &qbt_data->misc_flags))
+		bt_dev_dbg(hdev, "clear CMD_TIMEOUT on shutdown");
+
+	return ret;
+}
+
+static int btusb_qcom_shutdown_msubsys(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	bool had_peri_hwerr, had_peri_memdump;
+	bool had_bt_hwerr, had_bt_memdump;
+	bool had_tmel_memdump;
+	unsigned int delay_ms;
+	int ret;
+
+	delay_ms = 10;
+	had_peri_memdump = test_bit(QBT_MISC_MEMDUMP_PERI, &qbt_data->misc_flags);
+	had_peri_hwerr = test_bit(QBT_MISC_HWERR_PERI, &qbt_data->misc_flags);
+	had_bt_memdump = test_bit(QBT_MISC_MEMDUMP_BT, &qbt_data->misc_flags);
+	had_bt_hwerr = test_bit(QBT_MISC_HWERR_BT, &qbt_data->misc_flags);
+	had_tmel_memdump = test_bit(QBT_MISC_MEMDUMP_TMEL, &qbt_data->misc_flags);
+
+	bt_dev_dbg(hdev, "shutdown: had_peri_memdump %d, had_peri_hwerr %d",
+		   had_peri_memdump, had_peri_hwerr);
+	bt_dev_dbg(hdev, "shutdown: had_bt_memdump %d, had_bt_hwerr %d",
+		   had_bt_memdump, had_bt_hwerr);
+	bt_dev_dbg(hdev, "shutdown: had_tmel_memdump %d", had_tmel_memdump);
+
+	ret = -EIO;
+	if (had_peri_memdump || had_peri_hwerr || had_tmel_memdump)
+		goto out_reset;
+
+	ret = btusb_qcom_deactivate_msubsys_bt(hdev);
+	if (ret && (had_bt_memdump || had_bt_hwerr))
+		goto out_reset;
+
+	clear_bit(QBT_MISC_HWERR_BT, &qbt_data->misc_flags);
+	clear_bit(QBT_MISC_MEMDUMP_BT, &qbt_data->misc_flags);
+	if (test_and_clear_bit(QBT_MISC_CMD_TIMEOUT, &qbt_data->misc_flags))
+		bt_dev_dbg(hdev, "clear CMD_TIMEOUT on shutdown");
+
+	return ret;
+
+out_reset:
+	bt_dev_info(hdev, "shutdown: reset BTC after %u msec", delay_ms);
+	btqcom_reset_async(hdev, delay_ms);
+	return ret;
+}
+
+/* Implements hdev->shutdown(). */
+static int btusb_qcom_shutdown(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	int ret;
+
+	switch (qbt_data->category) {
+	case QBTC_CAT_LEGACY:
+		ret = -EOPNOTSUPP;
+		break;
+	case QBTC_CAT_UNIFIED:
+		ret = btusb_qcom_shutdown_unified(hdev);
+		break;
+	case QBTC_CAT_MSUBSYS:
+		ret = btusb_qcom_shutdown_msubsys(hdev);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	return ret;
+}
+
+/*
+ * ============================================================================
+ * QDFU firmware downloading
+ * ============================================================================
+ */
+
+#define QBT_FW_PATH_MAX  96
+
+enum qbt_fw_type {
+	QBT_FW_TYPE_PATCH,
+	QBT_FW_TYPE_NVM,
+	QBT_FW_TYPE_MAX,
+};
+
+#define QBT_PATCH_TYPE_TLV		0x01
+#define QBT_NVM_TYPE_TLV		0x02
+struct qbt_tlv_file_hdr {
+	u8	type;
+	u8	length[3];
+} __packed;
+
+struct qbt_patch_tlv_hdr {
+	struct  qbt_tlv_file_hdr tlv_hdr;
+	__le32	total_len;
+	__le32	patch_data_len;
+	u8	sign_ver;
+	u8	sign_algo;
+	u8	download_cfg;
+	u8	image_type;
+	__le16	product_id;
+	__le16	rom_ver;
+	__le16	patch_ver;
+	u8	reserved1[2];
+	__le32	anti_rollback_ver;
+	__le32	serial_low;
+	__le16	serial_high;
+	u8	debug_option;
+	u8	reserved2;
+	__le32	entry_addr;
+} __packed;
+
+struct qbt_nvm_tlv_hdr {
+	struct  qbt_tlv_file_hdr tlv_hdr;
+} __packed;
+
+/*
+ * struct qdfu_fw_cfg - QDFU firmware download configuration
+ * @desc: short description for logging (e.g. "PERI patch", "PERI NVM")
+ * @format: QBT_PATCH_TYPE_TLV or QBT_NVM_TYPE_TLV
+ * @hdr_size: TLV header size for @format
+ * @request: QDFU_BT_CMD_VSC_REQ_DOWNLOAD_(LOCAL|REMOTE)
+ * @loaded_flag: QDFU_BT_STATE_* flag that marks fw downloaded or not
+ * @settle_us: default time to wait after download for the BTC to settle
+ * @get_path: build firmware file path, return number of paths or error
+ * @check: check firmware, return 0 on success or a negative errno
+ * @download: download firmware, return 0 on success or a negative errno
+ * @get_settle_us: get the time to wait for BTC to settle
+ * @post_download: run after download, return 0 on success or a negative errno
+ */
+struct qdfu_fw_cfg {
+	const char	*desc;
+	u8		format;
+	u8		hdr_size;
+	u8		request;
+	u8		loaded_flag;
+	u32		settle_us;
+
+	int (*get_path)(struct hci_dev *hdev, const struct qdfu_fw_cfg *fw_cfg,
+			const struct qbtc_info *info, const struct qdfu_bt_id *ctrl_id,
+			char path[], size_t size);
+
+	int (*check)(struct hci_dev *hdev, const struct qdfu_fw_cfg *fw_cfg,
+		     const struct qbtc_info *info, const struct qdfu_bt_id *ctrl_id,
+		     const struct firmware *fw);
+
+	int (*download)(struct hci_dev *hdev, const struct qdfu_fw_cfg *fw_cfg,
+			const struct qbtc_info *info, const struct qdfu_bt_id *ctrl_id,
+			const void *data, size_t size);
+
+	u32 (*get_settle_us)(struct hci_dev *hdev, const struct qdfu_fw_cfg *fw_cfg,
+			     const struct qbtc_info *info, const struct qdfu_bt_id *ctrl_id);
+
+	int (*post_download)(struct hci_dev *hdev, const struct qdfu_fw_cfg *fw_cfg,
+			     const struct qbtc_info *info, const struct qdfu_bt_id *ctrl_id);
+};
+
+static int get_qbtc_subsys(const struct qdfu_fw_cfg *fw_cfg);
+
+static void qhci_to_qdfu_id(struct qdfu_bt_id *dfu_id,
+			    const struct qhci_btc_ver *hci_ver, u16 board_id)
+{
+	dfu_id->rom_version   = (u32)hci_ver->product_id << 16 | hci_ver->rom_ver;
+	dfu_id->patch_version = hci_ver->patch_ver;
+	dfu_id->soc_id        = hci_ver->soc_ver;
+	dfu_id->board_id      = board_id;
+}
+
+static void qdfu_to_qhci_id(struct qhci_btc_ver *hci_ver, u16 *board_id,
+			    const struct qdfu_bt_id *dfu_id)
+{
+	hci_ver->product_id = upper_16_bits(dfu_id->rom_version);
+	hci_ver->rom_ver    = lower_16_bits(dfu_id->rom_version);
+	hci_ver->patch_ver  = (u16)dfu_id->patch_version;
+	hci_ver->soc_ver    = dfu_id->soc_id;
+	*board_id           = dfu_id->board_id;
+}
+
+static int get_qdfu_id_via_hci(struct hci_dev *hdev,
+			       struct qdfu_bt_id *dfu_id)
+{
+	struct qhci_btc_ver hci_ver;
+	u16 board_id;
+	int ret;
+
+	ret = qbt_edl_patch_getver(hdev, &hci_ver);
+	if (ret)
+		return ret;
+
+	ret = qbt_edl_get_board_id(hdev, &board_id);
+	if (ret)
+		return ret;
+
+	qhci_to_qdfu_id(dfu_id, &hci_ver, board_id);
+
+	return 0;
+}
+
+static const char *get_fw_directory(struct hci_dev *hdev, const struct qbtc_info *info,
+				    const struct qdfu_bt_id *ctrl_id)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	const struct qbtc_bid_fwdir *custom_fwdir;
+
+	if (qbt_data->fw_dir)
+		return qbt_data->fw_dir;
+
+	if (!info->custom_fw_table)
+		goto info_fw_dir;
+
+	for (custom_fwdir = info->custom_fw_table; custom_fwdir->board_id; custom_fwdir++) {
+		if (custom_fwdir->board_id == ctrl_id->board_id)
+			return custom_fwdir->fw_dir;
+	}
+
+info_fw_dir:
+	if (info->fw_dir)
+		return info->fw_dir;
+
+	return "qca";
+}
+
+/*
+ * btusb_get_patch_path - build the patch firmware file path
+ * @hdev: the HCI device to build the path for
+ * @fw_cfg: the firmware download config
+ * @info: BTC info for this hdev
+ * @ctrl_id: BTC QDFU ID
+ * @path: output buffer for the built path
+ * @size: size of @path
+ *
+ * Used as the qdfu_fw_cfg ->get_path() callback.
+ *
+ * Return: 1 path written into @path, or a negative errno on failure.
+ */
+static int btusb_get_patch_path(struct hci_dev *hdev,
+				const struct qdfu_fw_cfg *fw_cfg,
+				const struct qbtc_info *info,
+				const struct qdfu_bt_id *ctrl_id,
+				char path[], size_t size)
+{
+	const char *fw_dir = get_fw_directory(hdev, info, ctrl_id);
+	int qbtc_subsys;
+	int len;
+
+	qbtc_subsys = get_qbtc_subsys(fw_cfg);
+
+	if (qbtc_subsys == QBTC_SUBSYS_INVALID) {
+		/* BT function unit. */
+		len = snprintf(path, size, "%s/rampatch_usb_%08x.bin",
+			       fw_dir, ctrl_id->rom_version);
+		goto out;
+	}
+
+	switch (qbtc_subsys) {
+	case QBTC_SUBSYS_PERI:
+		len = snprintf(path, size, "%s/peripatch_usb_%08x.bin",
+			       fw_dir, ctrl_id->rom_version);
+		break;
+	case QBTC_SUBSYS_TMEL:
+		return -EOPNOTSUPP;
+	default:
+		return -EINVAL;
+	}
+
+out:
+	if (len >= size)
+		return -ENAMETOOLONG;
+
+	return 1;
+}
+
+/*
+ * btusb_get_nvm_path - build the NVM firmware file path(s)
+ * @hdev: the HCI device to build the path for
+ * @fw_cfg: the firmware download config
+ * @info: BTC info for this hdev
+ * @ctrl_id: BTC QDFU ID
+ * @path: output buffer for the built path(s)
+ * @size: size of @path
+ *
+ * @path may hold two NUL-terminated paths back-to-back: a board-ID path
+ * followed by a fallback path, when NVM fallback is enabled.
+ *
+ * Used as the qdfu_fw_cfg ->get_path() callback.
+ *
+ * Return: number of paths (1 or 2) written into @path, or a negative
+ *	errno on failure.
+ */
+static int btusb_get_nvm_path(struct hci_dev *hdev,
+			      const struct qdfu_fw_cfg *fw_cfg,
+			      const struct qbtc_info *info,
+			      const struct qdfu_bt_id *ctrl_id,
+			      char path[], size_t size)
+{
+	const char *fw_dir = get_fw_directory(hdev, info, ctrl_id);
+	const char *prefix = NULL, *foundry_str = NULL;
+	bool has_bid = false, need_fb = false;
+	char fw_fb[QBT_FW_PATH_MAX] = { 0 };
+	int qbtc_subsys;
+	int len = 0;
+
+	qbtc_subsys = get_qbtc_subsys(fw_cfg);
+	if (qbtc_subsys == QBTC_SUBSYS_INVALID) {
+		/* BT function unit. */
+		prefix = "nvm_usb";
+	} else {
+		switch (qbtc_subsys) {
+		case QBTC_SUBSYS_PERI:
+			prefix = "perinvm_usb";
+			break;
+		case QBTC_SUBSYS_TMEL:
+			return -EOPNOTSUPP;
+		default:
+			return -EINVAL;
+		}
+	}
+
+	if (info->flags & QBT_FLAG_FOUNDRY_NVM) {
+		u8 foundry = FIELD_GET(GENMASK(15, 12), ctrl_id->soc_id);
+
+		switch (foundry) {
+		/* GlobalFoundries */
+		case 0x01:
+			foundry_str = "_gf";
+			break;
+		default:
+			break;
+		}
+	}
+
+	if ((info->flags & QBT_FLAG_BID_NVM) && ctrl_id->board_id) {
+		has_bid = true;
+		if (info->flags & QBT_FLAG_NVM_FALLBACK)
+			need_fb = true;
+	}
+
+	len = snprintf(path, size, "%s/%s_%08x", fw_dir, prefix, ctrl_id->rom_version);
+	if (len >= size)
+		return -ENAMETOOLONG;
+
+	if (foundry_str) {
+		len += snprintf(path + len, size - len, "%s", foundry_str);
+		if (len >= size)
+			return -ENAMETOOLONG;
+	}
+
+	if (!has_bid) {
+		len += snprintf(path + len, size - len, ".bin");
+		if (len >= size)
+			return -ENAMETOOLONG;
+		return 1;
+	}
+
+	if (need_fb) {
+		int fb_len = snprintf(fw_fb, sizeof(fw_fb), "%s.bin", path);
+
+		if (fb_len >= sizeof(fw_fb))
+			return -ENAMETOOLONG;
+
+		len += snprintf(path + len, size - len, "_%04x.bin",
+				ctrl_id->board_id);
+		if (len >= size)
+			return -ENAMETOOLONG;
+
+		/* path holds two NUL-terminated strings back-to-back:
+		 * [board-id path '\0'][fallback path '\0']
+		 * Verify the buffer fits both before writing.
+		 */
+		if (len + fb_len + 2 > size)
+			return -ENAMETOOLONG;
+
+		len += 1;
+		snprintf(path + len, size - len, "%s", fw_fb);
+		return 2;
+	}
+
+	len += snprintf(path + len, size - len, "_%04x.bin", ctrl_id->board_id);
+	if (len >= size)
+		return -ENAMETOOLONG;
+
+	return 1;
+}
+
+/* check the TLV file header; return 0 on success, or a negative errno */
+static int qbt_check_tlv_file(struct hci_dev *hdev, const struct firmware *fw, u8 format)
+{
+	const struct qbt_tlv_file_hdr *tlv_hdr = (const struct qbt_tlv_file_hdr *)fw->data;
+	size_t file_len;
+
+	if (fw->size < sizeof(*tlv_hdr))
+		return -ENODATA;
+
+	if (tlv_hdr->type != format)
+		return -EINVAL;
+
+	file_len = get_unaligned_le24(tlv_hdr->length) + sizeof(*tlv_hdr);
+	if (file_len != fw->size)
+		return -EBADF;
+
+	return 0;
+}
+
+/*
+ * btusb_check_patch_tlv - validate a patch TLV file against the BTC
+ *	before download
+ * @hdev: the HCI device to validate the file for
+ * @fw_cfg: the firmware download config (unused)
+ * @info: BTC info for this hdev (unused)
+ * @ctrl_id: BTC QDFU ID
+ * @fw: the firmware to validate
+ *
+ * Used as the qdfu_fw_cfg ->check() callback.
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_check_patch_tlv(struct hci_dev *hdev,
+				 const struct qdfu_fw_cfg *fw_cfg,
+				 const struct qbtc_info *info __maybe_unused,
+				 const struct qdfu_bt_id *ctrl_id,
+				 const struct firmware *fw)
+{
+	const struct qbt_patch_tlv_hdr *patch_hdr = (const void *)fw->data;
+	u32 fw_rom_version, fw_patch_version;
+	int ret;
+
+	ret = qbt_check_tlv_file(hdev, fw, QBT_PATCH_TYPE_TLV);
+	if (ret) {
+		bt_dev_err(hdev, "Check %s TLV file header failed: %pe",
+			   fw_cfg->desc, ERR_PTR(ret));
+		return ret;
+	}
+
+	if (fw->size < sizeof(*patch_hdr)) {
+		bt_dev_err(hdev, "%s header truncated (%zu bytes, header needs %zu)",
+			   fw_cfg->desc, fw->size, sizeof(*patch_hdr));
+		return -ENODATA;
+	}
+
+	if (likely(upper_16_bits(ctrl_id->rom_version))) {
+		fw_rom_version = (le16_to_cpu(patch_hdr->product_id) << 16) |
+				 le16_to_cpu(patch_hdr->rom_ver);
+	} else {
+		bt_dev_warn(hdev, "check %s: invalid rom_version 0x%08x",
+			    fw_cfg->desc, ctrl_id->rom_version);
+		fw_rom_version = le16_to_cpu(patch_hdr->rom_ver);
+	}
+
+	fw_patch_version = le16_to_cpu(patch_hdr->patch_ver);
+
+	if (fw_rom_version != ctrl_id->rom_version) {
+		bt_dev_err(hdev,
+			   "%s ROM version 0x%08x does not match controller 0x%08x",
+			   fw_cfg->desc, fw_rom_version, ctrl_id->rom_version);
+		return -EINVAL;
+	}
+
+	if (fw_patch_version < ctrl_id->patch_version) {
+		bt_dev_err(hdev,
+			   "%s version 0x%x older than controller 0x%x, anti-rollback",
+			   fw_cfg->desc, fw_patch_version, ctrl_id->patch_version);
+		return -EPERM;
+	}
+
+	return 0;
+}
+
+/*
+ * validate a NVM TLV file against the BTC
+ * Used as the qdfu_fw_cfg ->check() callback.
+ */
+static int btusb_check_nvm_tlv(struct hci_dev *hdev,
+			       const struct qdfu_fw_cfg *fw_cfg,
+			       const struct qbtc_info *info __maybe_unused,
+			       const struct qdfu_bt_id *ctrl_id __maybe_unused,
+			       const struct firmware *fw)
+{
+	int ret;
+
+	ret = qbt_check_tlv_file(hdev, fw, QBT_NVM_TYPE_TLV);
+	if (ret) {
+		bt_dev_err(hdev, "Check %s TLV file header failed: %pe",
+			   fw_cfg->desc, ERR_PTR(ret));
+		return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * btusb_download_via_qdfu - download a firmware image via QDFU to BTC
+ * @hdev: the HCI device to send the firmware to
+ * @fw_cfg: the firmware download config
+ * @info: BTC info for this hdev (unused)
+ * @ctrl_id: BTC QDFU ID (unused)
+ * @fw_data: the firmware image to send, header included
+ * @fw_size: size of @fw_data
+ *
+ * Used as the qdfu_fw_cfg ->download() callback.
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_download_via_qdfu(struct hci_dev *hdev,
+				   const struct qdfu_fw_cfg *fw_cfg,
+				   const struct qbtc_info *info __maybe_unused,
+				   const struct qdfu_bt_id *ctrl_id __maybe_unused,
+				   const void *fw_data, size_t fw_size)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	unsigned int pipe = usb_sndbulkpipe(xport_data->udev, 0x02);
+	const unsigned int xfer_timeout_ms = 3000;
+	const size_t xfer_size = SZ_4K;
+	const u8 *base = fw_data;
+	const u8 *ptr = fw_data;
+	size_t seg_size, size;
+	int snd_len, ret;
+	u8 *buf;
+
+	ret = qdfu_vsc_req_download(hdev, fw_cfg->request, ptr, fw_cfg->hdr_size);
+	if (ret)
+		return ret;
+
+	ptr += fw_cfg->hdr_size;
+	size = fw_size - fw_cfg->hdr_size;
+
+	/* ep2 needs time to switch from ACL to DFU function mode. */
+	fsleep(20 * 1000);
+
+	buf = kmalloc(xfer_size, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	while (size) {
+		seg_size = min_t(size_t, size, xfer_size);
+		memcpy(buf, ptr, seg_size);
+
+		snd_len = 0;
+		ret = usb_bulk_msg(xport_data->udev, pipe, buf, seg_size, &snd_len,
+				   xfer_timeout_ms);
+		if (ret < 0) {
+			bt_dev_err(hdev,
+				   "QDFU bulk send failed at offset %zu: %pe",
+				   ptr - base, ERR_PTR(ret));
+			break;
+		}
+
+		if (seg_size != snd_len) {
+			bt_dev_err(hdev,
+				   "QDFU bulk short write (%d of %zu bytes) at offset %zu",
+				   snd_len, seg_size, ptr - base);
+			ret = -EIO;
+			break;
+		}
+
+		ptr += seg_size;
+		size -= seg_size;
+	}
+
+	kfree(buf);
+	return ret;
+}
+
+/*
+ * Used as the qdfu_fw_cfg ->get_settle_us() callback.
+ * Kept here for future per-BTC settle time use.
+ */
+static u32 btusb_get_settle_us(struct hci_dev *hdev __maybe_unused,
+			       const struct qdfu_fw_cfg *fw_cfg,
+			       const struct qbtc_info *info __maybe_unused,
+			       const struct qdfu_bt_id *ctrl_id __maybe_unused)
+{
+	return fw_cfg->settle_us;
+}
+
+/* Used as the qdfu_fw_cfg ->post_download() callback. */
+static int btusb_post_download_unified_bt(struct hci_dev *hdev,
+					  const struct qdfu_fw_cfg *fw_cfg __maybe_unused,
+					  const struct qbtc_info *info __maybe_unused,
+					  const struct qdfu_bt_id *ctrl_id __maybe_unused)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	char *build_info = NULL;
+	int ret;
+
+	ret = qbt_edl_get_build_info(hdev, &build_info);
+	if (!ret) {
+		strscpy(qbt_data->build_info, build_info, sizeof(qbt_data->build_info));
+		kfree(build_info);
+	}
+
+	ret = __hci_reset_sync(hdev);
+	if (ret)
+		bt_dev_err(hdev, "HCI reset after BT NVM download failed: %pe", ERR_PTR(ret));
+	else
+		bt_dev_info(hdev, "HCI reset after BT NVM download succeeded");
+
+	return ret;
+}
+
+/* Used as the qdfu_fw_cfg ->post_download() callback. */
+static int btusb_post_download_msubsys_peri(struct hci_dev *hdev,
+					    const struct qdfu_fw_cfg *fw_cfg __maybe_unused,
+					    const struct qbtc_info *info __maybe_unused,
+					    const struct qdfu_bt_id *ctrl_id __maybe_unused)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct qbtc_subsys_data *subsys_data;
+	char *build_info = NULL;
+	int ret;
+
+	subsys_data = &qbt_data->subsys[QBTC_SUBSYS_PERI];
+
+	ret = qperi_get_subsys_build_info(hdev, QHCI_SUBSYS_PERI, &build_info);
+	if (!ret) {
+		strscpy(subsys_data->build_info, build_info, sizeof(subsys_data->build_info));
+		kfree(build_info);
+	}
+
+	/* Resetting PERI HCI is only allowed after downloading PERI NVM. */
+	if (qbt_data->flags & QBT_FLAG_RESET_PERI_HCI)
+		ret = qperi_hci_reset(hdev);
+	else
+		ret = qdfu_reset_peri_hci(hdev);
+
+	return ret;
+}
+
+static const struct qdfu_fw_cfg qdfu_fw_cfgs_bt[QBTC_CAT_MAX][QBT_FW_TYPE_MAX] = {
+	[QBTC_CAT_UNIFIED][QBT_FW_TYPE_PATCH] = {
+		.desc          = "patch",
+		.format        = QBT_PATCH_TYPE_TLV,
+		.hdr_size      = sizeof(struct qbt_patch_tlv_hdr),
+		.request       = QDFU_BT_CMD_VSC_REQ_DOWNLOAD_LOCAL,
+		.loaded_flag   = QDFU_BT_STATE_PATCHED_LOCAL,
+		.settle_us     = 10 * 1000,
+		.get_path      = btusb_get_patch_path,
+		.check         = btusb_check_patch_tlv,
+		.download      = btusb_download_via_qdfu,
+		.get_settle_us = btusb_get_settle_us,
+		.post_download = NULL,
+	},
+	[QBTC_CAT_UNIFIED][QBT_FW_TYPE_NVM] = {
+		.desc          = "NVM",
+		.format        = QBT_NVM_TYPE_TLV,
+		.hdr_size      = sizeof(struct qbt_nvm_tlv_hdr),
+		.request       = QDFU_BT_CMD_VSC_REQ_DOWNLOAD_LOCAL,
+		.loaded_flag   = QDFU_BT_STATE_NVMED_LOCAL,
+		.settle_us     = 40 * 1000,
+		.get_path      = btusb_get_nvm_path,
+		.check         = btusb_check_nvm_tlv,
+		.download      = btusb_download_via_qdfu,
+		.get_settle_us = btusb_get_settle_us,
+		.post_download = btusb_post_download_unified_bt,
+	},
+	[QBTC_CAT_MSUBSYS][QBT_FW_TYPE_PATCH] = {
+		.desc          = "BT patch",
+		.format        = QBT_PATCH_TYPE_TLV,
+		.hdr_size      = sizeof(struct qbt_patch_tlv_hdr),
+		.request       = QDFU_BT_CMD_VSC_REQ_DOWNLOAD_REMOTE,
+		.loaded_flag   = QDFU_BT_STATE_PATCHED_REMOTE,
+		.settle_us     = 30 * 1000,
+		.get_path      = btusb_get_patch_path,
+		.check         = btusb_check_patch_tlv,
+		.download      = btusb_download_via_qdfu,
+		.get_settle_us = btusb_get_settle_us,
+		.post_download = NULL,
+	},
+	[QBTC_CAT_MSUBSYS][QBT_FW_TYPE_NVM] = {
+		.desc          = "BT NVM",
+		.format        = QBT_NVM_TYPE_TLV,
+		.hdr_size      = sizeof(struct qbt_nvm_tlv_hdr),
+		.request       = QDFU_BT_CMD_VSC_REQ_DOWNLOAD_REMOTE,
+		.loaded_flag   = QDFU_BT_STATE_NVMED_REMOTE,
+		.settle_us     = 60 * 1000,
+		.get_path      = btusb_get_nvm_path,
+		.check         = btusb_check_nvm_tlv,
+		.download      = btusb_download_via_qdfu,
+		.get_settle_us = btusb_get_settle_us,
+		.post_download = btusb_post_download_unified_bt,
+	},
+};
+
+static const struct qdfu_fw_cfg qdfu_fw_cfgs_subsys[QBTC_SUBSYS_MAX][QBT_FW_TYPE_MAX] = {
+	[QBTC_SUBSYS_PERI][QBT_FW_TYPE_PATCH] = {
+		.desc          = "PERI patch",
+		.format        = QBT_PATCH_TYPE_TLV,
+		.hdr_size      = sizeof(struct qbt_patch_tlv_hdr),
+		.request       = QDFU_BT_CMD_VSC_REQ_DOWNLOAD_LOCAL,
+		.loaded_flag   = QDFU_BT_STATE_PATCHED_LOCAL,
+		.settle_us     = 10 * 1000,
+		.get_path      = btusb_get_patch_path,
+		.check         = btusb_check_patch_tlv,
+		.download      = btusb_download_via_qdfu,
+		.get_settle_us = btusb_get_settle_us,
+		.post_download = NULL,
+	},
+	[QBTC_SUBSYS_PERI][QBT_FW_TYPE_NVM] = {
+		.desc          = "PERI NVM",
+		.format        = QBT_NVM_TYPE_TLV,
+		.hdr_size      = sizeof(struct qbt_nvm_tlv_hdr),
+		.request       = QDFU_BT_CMD_VSC_REQ_DOWNLOAD_LOCAL,
+		.loaded_flag   = QDFU_BT_STATE_NVMED_LOCAL,
+		.settle_us     = 20 * 1000,
+		.get_path      = btusb_get_nvm_path,
+		.check         = btusb_check_nvm_tlv,
+		.download      = btusb_download_via_qdfu,
+		.get_settle_us = btusb_get_settle_us,
+		.post_download = btusb_post_download_msubsys_peri,
+	},
+};
+
+static int get_qbtc_subsys(const struct qdfu_fw_cfg *fw_cfg)
+{
+	int qbtc_subsys;
+	int fw_type;
+
+	for (qbtc_subsys = 0; qbtc_subsys < QBTC_SUBSYS_MAX; qbtc_subsys++) {
+		for (fw_type = 0; fw_type < QBT_FW_TYPE_MAX; fw_type++) {
+			if (&qdfu_fw_cfgs_subsys[qbtc_subsys][fw_type] == fw_cfg)
+				return qbtc_subsys;
+		}
+	}
+
+	return QBTC_SUBSYS_INVALID;
+}
+
+/*
+ * btusb_download_fw - download a firmware image
+ * @hdev: the HCI device to download the firmware to
+ * @fw_cfg: the firmware download config
+ * @info: BTC info for this hdev
+ * @ctrl_id: BTC ID
+ *
+ * Return: 1 if downloaded, 0 if already downloaded, or a negative errno
+ *	on failure.
+ */
+static int btusb_download_fw(struct hci_dev *hdev,
+			     const struct qdfu_fw_cfg *fw_cfg,
+			     const struct qbtc_info *info,
+			     const struct qdfu_bt_id *ctrl_id)
+{
+	const struct firmware *fw = NULL;
+	char paths[QBT_FW_PATH_MAX * 2];
+	const char *loaded_name;
+	u8 state, state_new;
+	u32 settle_us;
+	int n_paths;
+	int ret;
+
+	bt_dev_dbg(hdev, "download %s", fw_cfg->desc);
+	ret = qdfu_get_target_state(hdev, &state);
+	if (ret)
+		goto out;
+
+	if (state & fw_cfg->loaded_flag) {
+		bt_dev_info(hdev, "%s already downloaded", fw_cfg->desc);
+		goto out;
+	}
+
+	n_paths = fw_cfg->get_path(hdev, fw_cfg, info, ctrl_id, paths, sizeof(paths));
+	if (n_paths < 0) {
+		bt_dev_err(hdev, "Failed to get %s path: %pe",
+			   fw_cfg->desc, ERR_PTR(n_paths));
+		ret = n_paths;
+		goto out;
+	}
+	bt_dev_dbg(hdev, "%s path: %s", fw_cfg->desc, paths);
+	if (n_paths == 2)
+		bt_dev_dbg(hdev, "%s fallback path: %s", fw_cfg->desc,
+			   paths + strlen(paths) + 1);
+
+	loaded_name = paths;
+	ret = request_firmware(&fw, paths, &hdev->dev);
+	if (ret == -ENOENT && n_paths == 2) {
+		bt_dev_err(hdev, "Failed to request %s %s: %pe",
+			   fw_cfg->desc, loaded_name, ERR_PTR(ret));
+		loaded_name = paths + strlen(paths) + 1;
+		ret = request_firmware(&fw, loaded_name, &hdev->dev);
+	}
+	if (ret) {
+		bt_dev_err(hdev, "Failed to request %s %s: %pe",
+			   fw_cfg->desc, loaded_name, ERR_PTR(ret));
+		goto out;
+	}
+	bt_dev_dbg(hdev, "%s request succeeded: %s", fw_cfg->desc, loaded_name);
+
+	ret = fw_cfg->check(hdev, fw_cfg, info, ctrl_id, fw);
+	if (ret)
+		goto out_free_fw;
+
+	ret = fw_cfg->download(hdev, fw_cfg, info, ctrl_id, fw->data, fw->size);
+	if (ret)
+		goto out_free_fw;
+	bt_dev_info(hdev, "%s download succeeded: %s", fw_cfg->desc, loaded_name);
+
+	ret = qdfu_poll_state(hdev, &state_new, true, fw_cfg->loaded_flag);
+	if (ret)
+		goto out_free_fw;
+	if (state_new != state)
+		bt_dev_dbg(hdev, "%s state: 0x%02x -> 0x%02x",
+			   fw_cfg->desc, state, state_new);
+
+	if (fw_cfg->get_settle_us)
+		settle_us = fw_cfg->get_settle_us(hdev, fw_cfg, info, ctrl_id);
+	else
+		settle_us = fw_cfg->settle_us;
+	fsleep(settle_us);
+
+	if (fw_cfg->post_download) {
+		ret = fw_cfg->post_download(hdev, fw_cfg, info, ctrl_id);
+		if (ret)
+			bt_dev_err(hdev, "%s post-download failed: %pe",
+				   fw_cfg->desc, ERR_PTR(ret));
+		else
+			bt_dev_dbg(hdev, "%s post-download succeeded", fw_cfg->desc);
+	}
+
+	if (!ret) {
+		bt_dev_dbg(hdev, "download %s succeeded", fw_cfg->desc);
+		ret = 1;
+	}
+
+out_free_fw:
+	release_firmware(fw);
+out:
+	if (ret < 0)
+		bt_dev_err(hdev, "download %s failed: %pe", fw_cfg->desc, ERR_PTR(ret));
+	return ret;
+}
+
+/*
+ * ============================================================================
+ * btusb_qcom.h APIs
+ * ============================================================================
+ */
+
+/* size of the hci priv area */
+int btusb_qcom_hdev_priv_size(void)
+{
+	return sizeof(struct btqcom_data) + sizeof(struct btusb_qcom);
+}
+
+/* get @hdev's transport-specific data */
+struct btusb_qcom *btusb_qcom_xport_data(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+
+	return (struct btusb_qcom *)(qbt_data + 1);
+}
+
+/* work function for qbt_data->dwork */
+static void btqcom_work(struct work_struct *work)
+{
+	struct btqcom_data *qbt_data;
+	struct hci_dev *hdev;
+
+	qbt_data = container_of(to_delayed_work(work), struct btqcom_data, dwork);
+	hdev = qbt_data->hdev;
+
+	if (test_and_clear_bit(QBT_WORK_RESET_HDEV, &qbt_data->work_flags)) {
+		hci_dev_hold(hdev);
+		hci_req_sync_lock(hdev);
+
+		btusb_do_reset_work(hdev, QBT_RESET_TYPE_ASYNC);
+
+		hci_req_sync_unlock(hdev);
+		hci_dev_put(hdev);
+	}
+}
+
+static void devm_btusb_qcom_deinit(void *data)
+{
+	struct btqcom_data *qbt_data = data;
+	struct btusb_qcom *xport_data;
+
+	if (!qbt_data->inited)
+		return;
+
+	xport_data = qbt_data->xport_data;
+	mutex_destroy(&qbt_data->req_mutex);
+	mutex_destroy(&xport_data->tx_mutex);
+
+	qbt_data->inited = false;
+	bt_dev_dbg(qbt_data->hdev, "btusb qcom deinited");
+}
+
+static void __maybe_unused btusb_qcom_deinit(struct btqcom_data *qbt_data)
+{
+	devm_release_action(&qbt_data->hdev->dev, devm_btusb_qcom_deinit,
+			    qbt_data);
+}
+
+/*
+ * btusb_qcom_init - initialize hdev and per-device btqcom_data
+ * @qbt_data: the btqcom_data to initialize
+ *
+ * Doesn't touch btqcom_data fields that live for the hdev's lifetime,
+ * e.g. @fw_dir and @fw_logging.
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_qcom_init(struct btqcom_data *qbt_data)
+{
+	struct btusb_qcom *xport_data = qbt_data->xport_data;
+	struct hci_dev *hdev = qbt_data->hdev;
+	int res;
+
+	WRITE_ONCE(qbt_data->misc_flags, 0);
+	WRITE_ONCE(qbt_data->work_flags, 0);
+
+	qbt_data->md_state = HCI_DEVCOREDUMP_IDLE;
+	qbt_data->md_submit_err = 0;
+	qbt_data->md_submit_size = 0;
+	qbt_data->md_pending_flags = 0;
+	btqcom_reset_memdump(&qbt_data->md);
+
+	WRITE_ONCE(qbt_data->req, NULL);
+	WRITE_ONCE(qbt_data->req_state, QHCI_REQ_DONE);
+
+	if (qbt_data->inited)
+		return 0;
+
+	hci_set_quirk(hdev, HCI_QUIRK_NON_PERSISTENT_SETUP);
+	hci_set_quirk(hdev, HCI_QUIRK_SIMULTANEOUS_DISCOVERY);
+	hci_set_quirk(hdev, HCI_QUIRK_WIDEBAND_SPEECH_SUPPORTED);
+
+	if (qbt_data->flags & QBT_FLAG_AOSP_EXT)
+		hci_set_aosp_capable(hdev);
+	if (qbt_data->flags & QBT_FLAG_MSFT_EXT)
+		hci_set_msft_opcode(hdev, 0xFD70);
+
+	res = devm_add_action(&hdev->dev, devm_btusb_qcom_deinit, qbt_data);
+	if (res) {
+		bt_dev_err(hdev, "Add btusb qcom deinit action failed: %pe",
+			   ERR_PTR(res));
+		return res;
+	}
+
+	INIT_DELAYED_WORK(&qbt_data->dwork, btqcom_work);
+	mutex_init(&xport_data->tx_mutex);
+	mutex_init(&qbt_data->req_mutex);
+	init_waitqueue_head(&qbt_data->req_wait_q);
+	spin_lock_init(&qbt_data->req_spinlock);
+
+	hdev->set_bdaddr        = btqcom_set_bdaddr;
+	hdev->reset             = btusb_qcom_reset;
+	hdev->shutdown          = btusb_qcom_shutdown;
+	hdev->hw_error          = btqcom_hw_error;
+	hdev->handle_ev_vendor  = btqcom_handle_ev_vendor;
+	hdev->recv_vendor_pkt   = btqcom_recv_vendor_pkt;
+
+	qbt_data->md_ready = false;
+	if (qbt_data->flags & QBT_FLAG_MEMDUMP) {
+		res = hci_devcd_register(hdev, btusb_qcom_trigger_memdump,
+					 btusb_qcom_memdump_hdr,
+					 btusb_qcom_notify_memdump);
+		if (!res)
+			qbt_data->md_ready = true;
+		else if (res == -EOPNOTSUPP)
+			bt_dev_warn(hdev, "CONFIG_DEV_COREDUMP not enabled");
+		else
+			bt_dev_err(hdev, "register devcoredump failed: %pe",
+				   ERR_PTR(res));
+	}
+
+	/* pairs with smp_load_acquire() in btusb_qcom_disconnect() */
+	smp_store_release(&qbt_data->inited, true);
+
+	bt_dev_dbg(hdev, "btusb qcom inited");
+	return 0;
+}
+
+/*
+ * btusb_qcom_setup_unified - setup for a unified BTC
+ * @hdev: the HCI device to set up
+ * @info: BTC info for this hdev
+ * @ctrl_id: BTC QDFU ID
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_qcom_setup_unified(struct hci_dev *hdev,
+				    const struct qbtc_info *info,
+				    const struct qdfu_bt_id *ctrl_id)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	enum qbtc_category btc_cat = info->category;
+	const struct qdfu_fw_cfg *cfgs_bt;
+	struct qdfu_bt_id bt_id;
+	int ret;
+
+	cfgs_bt = qdfu_fw_cfgs_bt[btc_cat];
+
+	ret = btusb_download_fw(hdev, &cfgs_bt[QBT_FW_TYPE_PATCH], info, ctrl_id);
+	if (ret < 0)
+		return ret;
+
+	ret = qdfu_get_target_version(hdev, &bt_id);
+	if (ret)
+		return ret;
+	qdfu_to_qhci_id(&qbt_data->ver, &qbt_data->board_id, &bt_id);
+
+	ret = btusb_download_fw(hdev, &cfgs_bt[QBT_FW_TYPE_NVM], info, &bt_id);
+	if (ret)
+		return ret < 0 ? ret : 0;
+
+	ret = __hci_reset_sync(hdev);
+	if (ret)
+		bt_dev_err(hdev, "HCI reset failed: %pe", ERR_PTR(ret));
+	else
+		bt_dev_dbg(hdev, "HCI reset succeeded");
+
+	return ret;
+}
+
+/*
+ * btusb_qcom_setup_msubsys - setup for a multi-subsystem BTC
+ * @hdev: the HCI device to set up
+ * @info: BTC info for this hdev
+ * @ctrl_id: BTC QDFU ID
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_qcom_setup_msubsys(struct hci_dev *hdev,
+				    const struct qbtc_info *info,
+				    const struct qdfu_bt_id *ctrl_id)
+{
+	const struct qdfu_fw_cfg *cfgs_peri = qdfu_fw_cfgs_subsys[QBTC_SUBSYS_PERI];
+	const struct qdfu_fw_cfg *cfgs_bt = qdfu_fw_cfgs_bt[QBTC_CAT_MSUBSYS];
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct qbtc_subsys_data *subsys_peri;
+	struct qdfu_bt_id peri_id, bt_id;
+	int ret;
+
+	subsys_peri = &qbt_data->subsys[QBTC_SUBSYS_PERI];
+
+	ret = btusb_download_fw(hdev, &cfgs_peri[QBT_FW_TYPE_PATCH], info, ctrl_id);
+	if (ret < 0)
+		return ret;
+
+	ret = qdfu_get_target_version(hdev, &peri_id);
+	if (ret)
+		return ret;
+	qdfu_to_qhci_id(&subsys_peri->ver, &subsys_peri->board_id, &peri_id);
+
+	ret = btusb_download_fw(hdev, &cfgs_peri[QBT_FW_TYPE_NVM], info, &peri_id);
+	if (ret < 0)
+		return ret;
+
+	ret = qdfu_reset_msubsys_bt(hdev);
+	if (ret)
+		return ret;
+
+	ret = get_qdfu_id_via_hci(hdev, &bt_id);
+	if (ret)
+		return ret;
+
+	if (bt_id.rom_version != peri_id.rom_version ||
+	    bt_id.soc_id != peri_id.soc_id ||
+	    bt_id.board_id != peri_id.board_id) {
+		bt_dev_info(hdev, "BT ROM Version: 0x%08x",
+			    bt_id.rom_version);
+		bt_dev_info(hdev, "BT Patch Version: 0x%08x",
+			    bt_id.patch_version);
+		bt_dev_info(hdev, "BT SoC Version: 0x%08x",
+			    bt_id.soc_id);
+		bt_dev_info(hdev, "BT Board ID: 0x%04x",
+			    bt_id.board_id);
+	}
+
+	ret = btusb_download_fw(hdev, &cfgs_bt[QBT_FW_TYPE_PATCH], info, &bt_id);
+	if (ret < 0)
+		return ret;
+
+	ret = get_qdfu_id_via_hci(hdev, &bt_id);
+	if (ret)
+		return ret;
+	qdfu_to_qhci_id(&qbt_data->ver, &qbt_data->board_id, &bt_id);
+
+	ret = btusb_download_fw(hdev, &cfgs_bt[QBT_FW_TYPE_NVM], info, &bt_id);
+	if (ret < 0)
+		return ret;
+
+	return 0;
+}
+
+static int btqcom_post_setup(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	struct qbtc_subsys_data *subsys_data;
+	char *build_info = NULL;
+	char buf[512];
+	int len = 0;
+	int res;
+
+	bt_dev_dbg(hdev, "flags: 0x%lx", qbt_data->flags);
+	bt_dev_info(hdev, "memdump ready? %s", str_yes_no(qbt_data->md_ready));
+
+	qbt_config_fw_logging(hdev, qbt_data->fw_logging);
+
+	hci_set_hw_info(hdev, "%s", qbt_data->btc_name);
+	if (qbt_data->category == QBTC_CAT_MSUBSYS) {
+		subsys_data = &qbt_data->subsys[QBTC_SUBSYS_PERI];
+		if (subsys_data->build_info[0] == '\0') {
+			res = qperi_get_subsys_build_info(hdev, QHCI_SUBSYS_PERI,
+							  &build_info);
+			if (!res) {
+				strscpy(subsys_data->build_info, build_info,
+					sizeof(subsys_data->build_info));
+				kfree(build_info);
+			}
+		}
+
+		bt_dev_info(hdev, "%s build info: %s",
+			    qbtc_subsys_name(QBTC_SUBSYS_PERI), subsys_data->build_info);
+		len = scnprintf(buf, sizeof(buf), "%s: %s\n",
+				qbtc_subsys_name(QBTC_SUBSYS_PERI), subsys_data->build_info);
+	}
+
+	if (qbt_data->build_info[0] == '\0') {
+		build_info = NULL;
+		res = qbt_edl_get_build_info(hdev, &build_info);
+		if (!res) {
+			strscpy(qbt_data->build_info, build_info,
+				sizeof(qbt_data->build_info));
+			kfree(build_info);
+		}
+	}
+
+	bt_dev_info(hdev, "BT build info: %s", qbt_data->build_info);
+	len += snprintf(buf + len, sizeof(buf) - len, "BT: %s", qbt_data->build_info);
+	hci_set_fw_info(hdev, "%s", buf);
+
+	return 0;
+}
+
+/*
+ * btusb_qcom_setup - setup a BTC
+ * @hdev: the HCI device
+ *
+ * Identifies the BTC, then dispatches setup based on its category.
+ * Implements hdev->setup().
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+int btusb_qcom_setup(struct hci_dev *hdev)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	bool is_first_setup = hci_dev_test_flag(hdev, HCI_SETUP);
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	const struct qbtc_id *id_entry = qbtc_id_table;
+	struct qdfu_bt_id ctrl_id;
+	int ret;
+
+	qbt_data->hdev = hdev;
+	xport_data->intf = to_usb_interface(hdev->dev.parent);
+	xport_data->udev = interface_to_usbdev(xport_data->intf);
+	xport_data->idVendor  = le16_to_cpu(xport_data->udev->descriptor.idVendor);
+	xport_data->idProduct = le16_to_cpu(xport_data->udev->descriptor.idProduct);
+	qbt_data->xport_data = xport_data;
+
+	ret = usb_autopm_get_interface(xport_data->intf);
+	if (ret) {
+		bt_dev_err(hdev, "get autopm for QCOM BTUSB setup failed: %pe",
+			   ERR_PTR(ret));
+		return ret;
+	}
+
+	ret = qdfu_get_target_version(hdev, &ctrl_id);
+	if (ret)
+		goto out;
+
+	while (id_entry->rom_version) {
+		if (id_entry->rom_version == ctrl_id.rom_version)
+			break;
+		id_entry++;
+	}
+
+	if (!id_entry->rom_version) {
+		ret = -ENODEV;
+		bt_dev_warn(hdev, "Detected unsupported BT controller:");
+	} else {
+		bt_dev_info(hdev, "%s QCOM %s BT controller: %s",
+			    is_first_setup ? "Detected" : "Setup",
+			    qbtc_category_name(id_entry->btc_info->category),
+			    id_entry->name);
+	}
+
+	bt_dev_info(hdev, "ROM Version  : 0x%08x", ctrl_id.rom_version);
+	bt_dev_info(hdev, "Patch Version: 0x%08x", ctrl_id.patch_version);
+	bt_dev_info(hdev, "SoC Version  : 0x%08x", ctrl_id.soc_id);
+	bt_dev_info(hdev, "Board ID     : 0x%04x", ctrl_id.board_id);
+
+	if (ret)
+		goto out;
+
+	if (!qbt_data->drv_name)
+		qbt_data->drv_name = dev_driver_string(&xport_data->intf->dev);
+	qbt_data->btc_name = id_entry->name;
+	qbt_data->category = id_entry->btc_info->category;
+	qbt_data->flags    = id_entry->btc_info->flags & QBT_FLAG_BTC_CFG_MASK;
+	if (xport_data->reset_gpio)
+		qbt_data->flags |= QBT_FLAG_HW_RESET;
+	ret = btusb_qcom_init(qbt_data);
+	if (ret)
+		goto out;
+
+	switch (qbt_data->category) {
+	case QBTC_CAT_LEGACY:
+		ret = -EOPNOTSUPP;
+		break;
+	case QBTC_CAT_UNIFIED:
+		hci_set_quirk(hdev, HCI_QUIRK_BROKEN_ENHANCED_SETUP_SYNC_CONN);
+		ret = btusb_qcom_setup_unified(hdev, id_entry->btc_info, &ctrl_id);
+		break;
+	case QBTC_CAT_MSUBSYS:
+		ret = btusb_qcom_setup_msubsys(hdev, id_entry->btc_info, &ctrl_id);
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	if (!ret)
+		ret = btqcom_post_setup(hdev);
+
+out:
+	usb_autopm_put_interface(xport_data->intf);
+	if (!ret)
+		bt_dev_info(hdev, "QCOM BTUSB setup succeeded (^_^)");
+	else
+		bt_dev_err(hdev, "QCOM BTUSB setup failed: %pe", ERR_PTR(ret));
+	return ret;
+}
+
+/*
+ * btusb_qcom_disconnect - clean up on USB disconnect
+ * @hdev: the HCI device being disconnected
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+int btusb_qcom_disconnect(struct hci_dev *hdev)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+
+	/* pairs with smp_store_release() in btusb_qcom_init() */
+	if (!smp_load_acquire(&qbt_data->inited))
+		return 0;
+
+	bt_dev_dbg(hdev, "disconnection: misc_flags(0x%lx)",
+		   READ_ONCE(qbt_data->misc_flags));
+
+	if (test_bit(QBT_MISC_MEMDUMP_INCOMING, &qbt_data->misc_flags) &&
+	    !qbt_data->md_submit_err)
+		bt_dev_warn(hdev, "memdump collection interrupted by disconnection");
+
+	qperi_tx_sync_cancel_sync(hdev, -ENODEV);
+	if (test_and_clear_bit(QBT_MISC_CMD_TIMEOUT, &qbt_data->misc_flags)) {
+		wake_up_var(&qbt_data->misc_flags);
+		bt_dev_dbg(hdev, "wake command-timeout waiter on disconnection");
+	}
+	disable_delayed_work_sync(&qbt_data->dwork);
+
+	return 0;
+}
+
+/*
+ * btusb_qcom_send_frame - send a frame for BT or PERI
+ * @hdev: the HCI device
+ * @skb: the frame to send
+ *
+ * Implements hdev->send().
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+int btusb_qcom_send_frame(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	bool is_vendor;
+	bool need_lock;
+	u8 pkt_type;
+	int ret;
+
+	if (qbt_data->category != QBTC_CAT_MSUBSYS)
+		return xport_data->send_bt_frame(hdev, skb);
+
+	pkt_type = hci_skb_pkt_type(skb);
+	is_vendor = pkt_type == HCI_VENDOR_PKT;
+	/* an empty vendor packet has no path to reach here */
+	if (is_vendor)
+		hci_skb_pkt_type(skb) = *(const u8 *)skb_pull_data(skb, 1);
+
+	pkt_type = hci_skb_pkt_type(skb);
+	need_lock = pkt_type == HCI_COMMAND_PKT ||
+		    pkt_type == QPERI_COMMAND_PKT;
+
+	if (need_lock)
+		mutex_lock(&xport_data->tx_mutex);
+
+	if (is_vendor)
+		ret = xport_data->send_vendor_frame(hdev, skb);
+	else
+		ret = xport_data->send_bt_frame(hdev, skb);
+
+	if (need_lock)
+		mutex_unlock(&xport_data->tx_mutex);
+
+	return ret;
+}
+
+/*
+ * RX flow:
+ *
+ * USB intr/bulk endpoint (byte stream)
+ *  -> btusb_qcom_recv_{intr,bulk}() (reassemble to frame)
+ *      -> btqcom_recv_frame()
+ *          -> btusb_upward_frame() (dispatch frame upwards)
+ *              -> hci_recv_frame() (PERI frame as HCI_VENDOR_PKT)
+ *              -> xport_data->recv_bt_frame() (BT, back to btusb_main.c)
+ */
+
+/*
+ * btusb_upward_frame - dispatch a received frame upwards
+ * @hdev: the HCI device the @skb comes from
+ * @skb: the frame to dispatch upwards
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+static int btusb_upward_frame(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	struct btusb_qcom *xport_data = btusb_qcom_xport_data(hdev);
+	u8 pkt_type = hci_skb_pkt_type(skb);
+	u16 handle;
+	int ret;
+
+	switch (pkt_type) {
+	case QPERI_EVENT_PKT:
+	case QPERI_ACLDATA_PKT:
+		*(u8 *)skb_push(skb, 1) = pkt_type;
+		hci_skb_pkt_type(skb) = HCI_VENDOR_PKT;
+		break;
+	case HCI_EVENT_PKT:
+		break;
+	case HCI_ACLDATA_PKT:
+		handle = hci_acl_handle(skb);
+		/* reroute vendor ACLs as HCI_VENDOR_PKT */
+		if (handle == QBT_HANDLE_ENHANCED_LOGGING ||
+		    handle == QBT_HANDLE_MEMDUMP) {
+			*(u8 *)skb_push(skb, 1) = pkt_type;
+			hci_skb_pkt_type(skb) = HCI_VENDOR_PKT;
+		}
+		break;
+	default:
+		dev_kfree_skb_irq(skb);
+		ret = -EINVAL;
+		goto out;
+	}
+
+	if (hci_skb_pkt_type(skb) == HCI_VENDOR_PKT)
+		ret = hci_recv_frame(hdev, skb);
+	else
+		ret = xport_data->recv_bt_frame(hdev, skb);
+
+out:
+	if (ret)
+		bt_dev_err(hdev, "upward frame with type (0x%02x) failed: %pe",
+			   pkt_type, ERR_PTR(ret));
+
+	return ret;
+}
+
+/* Every frame lands here first — the ideal spot for pre-processing. */
+static int btqcom_recv_frame(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	u8 pkt_type = hci_skb_pkt_type(skb);
+
+	switch (pkt_type) {
+	case HCI_EVENT_PKT:
+	case QPERI_EVENT_PKT:
+	case HCI_ACLDATA_PKT:
+	case QPERI_ACLDATA_PKT:
+		break;
+	default:
+		dev_kfree_skb_irq(skb);
+		bt_dev_err_ratelimited(hdev, "unexpected pkt type 0x%02x", pkt_type);
+		return -EINVAL;
+	}
+
+	return btusb_upward_frame(hdev, skb);
+}
+
+/*
+ * RX reassembly state, stashed in hci_skb_pkt_seqnum(skb) between calls:
+ * QRX_STATE_INDICATOR - determining the packet type
+ * QRX_STATE_HEADER    - receiving the packet header
+ * QRX_STATE_PAYLOAD  - receiving the payload
+ */
+enum {
+	QRX_STATE_INDICATOR,
+	QRX_STATE_HEADER,
+	QRX_STATE_PAYLOAD,
+};
+
+/*
+ * btusb_qcom_recv_intr - reassemble byte stream from the intr endpoint to frame
+ * @hdev: the HCI device
+ * @skb: the in-progress frame, or NULL to start a new one
+ * @buffer: bytes received
+ * @count: bytes count
+ * @err: output error code
+ *
+ * See the RX flow diagram above for further dispatch.
+ *
+ * Return: the in-progress frame to resume on the next call, or NULL
+ *	otherwise.
+ */
+struct sk_buff *btusb_qcom_recv_intr(struct hci_dev *hdev, struct sk_buff *skb,
+				     void *buffer, int count, int *err)
+{
+	struct btqcom_data *qbt_data = hci_get_priv(hdev);
+	enum qbtc_category btc_cat = qbt_data->category;
+	int rx_state;
+	u8 pkt_type;
+	int len;
+	int res;
+
+	*err = 0;
+	while (count) {
+		if (!skb) {
+			u8 host_id = *(u8 *)buffer;
+
+			skb = bt_skb_alloc(QPERI_MAX_EVENT_SIZE, GFP_ATOMIC);
+			if (!skb) {
+				*err = -ENOMEM;
+				break;
+			}
+
+			/* see struct qperi_event_hdr for PERI event format */
+			if (host_id == QHCI_HOST_ID_BT) {
+				hci_skb_pkt_type(skb) = QPERI_EVENT_PKT;
+				hci_skb_pkt_seqnum(skb) = QRX_STATE_HEADER;
+				hci_skb_expect(skb) = QPERI_EVENT_HDR_SIZE;
+			} else {
+				hci_skb_pkt_type(skb) = HCI_EVENT_PKT;
+				hci_skb_pkt_seqnum(skb) = QRX_STATE_HEADER;
+				hci_skb_expect(skb) = HCI_EVENT_HDR_SIZE;
+			}
+		}
+
+		len = min_t(uint, hci_skb_expect(skb), count);
+		skb_put_data(skb, buffer, len);
+
+		count -= len;
+		buffer += len;
+		hci_skb_expect(skb) -= len;
+
+		if (hci_skb_expect(skb))
+			continue;
+
+		rx_state = hci_skb_pkt_seqnum(skb);
+		if (rx_state == QRX_STATE_PAYLOAD)
+			goto frame_done;
+
+		hci_skb_pkt_seqnum(skb) = QRX_STATE_PAYLOAD;
+		pkt_type = hci_skb_pkt_type(skb);
+		if (pkt_type == QPERI_EVENT_PKT)
+			hci_skb_expect(skb) = qperi_event_header(skb)->plen;
+		else if (pkt_type == HCI_EVENT_PKT)
+			hci_skb_expect(skb) = hci_event_hdr(skb)->plen;
+
+		if (hci_skb_expect(skb))
+			continue;
+
+frame_done:
+		if (count && count < HCI_EVENT_HDR_SIZE) {
+			bt_dev_warn(hdev,
+				    "Unexpected continuation: %d bytes",
+				    count);
+			if (btc_cat != QBTC_CAT_MSUBSYS)
+				count = 0;
+		}
+
+		hci_skb_pkt_seqnum(skb) = 0;
+		res = btqcom_recv_frame(hdev, skb);
+		if (res)
+			bt_dev_err_ratelimited(hdev, "recv intr frame failed: %pe",
+					       ERR_PTR(res));
+		skb = NULL;
+	}
+
+	return skb;
+}
+
+/*
+ * btusb_qcom_recv_bulk - reassemble byte stream from the bulk endpoint to frame
+ * @hdev: the HCI device
+ * @skb: the in-progress frame, or NULL to start a new one
+ * @buffer: bytes received
+ * @count: bytes count
+ * @err: output error code
+ *
+ * See the RX flow diagram above for further dispatch.
+ *
+ * Return: the in-progress frame to resume on the next call, or NULL
+ *	otherwise.
+ */
+struct sk_buff *btusb_qcom_recv_bulk(struct hci_dev *hdev, struct sk_buff *skb,
+				     void *buffer, int count, int *err)
+{
+	struct qperi_acl_hdr *peri_hdr;
+	u16 peri_handle;
+	int rx_state;
+	u8 pkt_type;
+	int res;
+	int len;
+
+	*err = 0;
+	while (count) {
+		if (!skb) {
+			skb = bt_skb_alloc(QPERI_MAX_FRAME_SIZE, GFP_ATOMIC);
+			if (!skb) {
+				*err = -ENOMEM;
+				break;
+			}
+			hci_skb_pkt_seqnum(skb) = QRX_STATE_INDICATOR;
+			hci_skb_expect(skb) = HCI_ACL_HDR_SIZE;
+		}
+
+		len = min_t(uint, hci_skb_expect(skb), count);
+		skb_put_data(skb, buffer, len);
+
+		count -= len;
+		buffer += len;
+		hci_skb_expect(skb) -= len;
+
+		if (hci_skb_expect(skb))
+			continue;
+
+		rx_state = hci_skb_pkt_seqnum(skb);
+		switch (rx_state) {
+		case QRX_STATE_INDICATOR:
+			hci_skb_pkt_seqnum(skb) = QRX_STATE_HEADER;
+			peri_hdr = qperi_acl_header(skb);
+			peri_handle = qperi_acl_handle(skb);
+			if (peri_hdr->host_id == QHCI_HOST_ID_BT &&
+			    peri_handle >= 0xEC0 && peri_handle <= 0xECF) {
+				hci_skb_pkt_type(skb) = QPERI_ACLDATA_PKT;
+				hci_skb_expect(skb) = QPERI_ACL_HDR_SIZE - HCI_ACL_HDR_SIZE;
+				continue;
+			}
+			hci_skb_pkt_type(skb) = HCI_ACLDATA_PKT;
+			fallthrough;
+
+		case QRX_STATE_HEADER:
+			pkt_type = hci_skb_pkt_type(skb);
+			if (pkt_type == QPERI_ACLDATA_PKT)
+				hci_skb_expect(skb) = qperi_acl_dlen(skb);
+			else if (pkt_type == HCI_ACLDATA_PKT)
+				hci_skb_expect(skb) = hci_acl_dlen(skb);
+
+			if (hci_skb_expect(skb) > QPERI_MAX_FRAME_SIZE - skb->len) {
+				dev_kfree_skb_irq(skb);
+				skb = NULL;
+				*err = -EILSEQ;
+				return NULL;
+			}
+			hci_skb_pkt_seqnum(skb) = QRX_STATE_PAYLOAD;
+			if (hci_skb_expect(skb))
+				continue;
+			fallthrough;
+
+		case QRX_STATE_PAYLOAD:
+			break;
+		}
+
+		hci_skb_pkt_seqnum(skb) = 0;
+		res = btqcom_recv_frame(hdev, skb);
+		if (res)
+			bt_dev_err_ratelimited(hdev, "recv bulk frame failed: %pe",
+					       ERR_PTR(res));
+		skb = NULL;
+	}
+
+	return skb;
+}
+
+MODULE_AUTHOR("Zijun Hu <zijun.hu@oss.qualcomm.com>");
diff --git a/drivers/bluetooth/btusb_qcom.h b/drivers/bluetooth/btusb_qcom.h
new file mode 100644
index 000000000000..8fbc105f4eaf
--- /dev/null
+++ b/drivers/bluetooth/btusb_qcom.h
@@ -0,0 +1,99 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Qualcomm Bluetooth USB transport-specific support
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifndef __BTUSB_QCOM_H
+#define __BTUSB_QCOM_H
+
+#include <linux/types.h>
+#include <linux/mutex.h>
+#include <linux/skbuff.h>
+#include <linux/usb.h>
+
+#include <net/bluetooth/hci_core.h>
+
+#define QPERI_COMMAND_PKT	0x31
+#define QPERI_ACLDATA_PKT	0x32
+#define QPERI_EVENT_PKT		0x34
+
+struct btusb_qcom {
+	__u16                 idVendor;
+	__u16                 idProduct;
+	struct usb_device    *udev;
+	struct usb_interface *intf;
+	struct gpio_desc     *reset_gpio;
+
+	/* serializes sending BT and vendor frames to the transport */
+	struct mutex          tx_mutex;
+
+	void (*prepare_reset)(struct hci_dev *hdev);
+	int (*recv_bt_frame)(struct hci_dev *hdev, struct sk_buff *skb);
+	int (*send_bt_frame)(struct hci_dev *hdev, struct sk_buff *skb);
+	int (*send_vendor_frame)(struct hci_dev *hdev, struct sk_buff *skb);
+};
+
+#if IS_ENABLED(CONFIG_BT_HCIBTUSB_QCOM)
+
+int btusb_qcom_hdev_priv_size(void);
+struct btusb_qcom *btusb_qcom_xport_data(struct hci_dev *hdev);
+
+int btusb_qcom_setup(struct hci_dev *hdev);
+int btusb_qcom_disconnect(struct hci_dev *hdev);
+int btusb_qcom_send_frame(struct hci_dev *hdev, struct sk_buff *skb);
+
+struct sk_buff *btusb_qcom_recv_intr(struct hci_dev *hdev, struct sk_buff *skb,
+				     void *buffer, int count, int *err);
+
+struct sk_buff *btusb_qcom_recv_bulk(struct hci_dev *hdev, struct sk_buff *skb,
+				     void *buffer, int count, int *err);
+
+#else
+
+static inline int btusb_qcom_hdev_priv_size(void)
+{
+	return 0;
+}
+
+static inline struct btusb_qcom *btusb_qcom_xport_data(struct hci_dev *hdev)
+{
+	return NULL;
+}
+
+static inline int btusb_qcom_setup(struct hci_dev *hdev)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int btusb_qcom_disconnect(struct hci_dev *hdev)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int btusb_qcom_send_frame(struct hci_dev *hdev, struct sk_buff *skb)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline struct sk_buff *btusb_qcom_recv_intr(struct hci_dev *hdev,
+						   struct sk_buff *skb,
+						   void *buffer, int count,
+						   int *err)
+{
+	*err = -EOPNOTSUPP;
+	return NULL;
+}
+
+static inline struct sk_buff *btusb_qcom_recv_bulk(struct hci_dev *hdev,
+						   struct sk_buff *skb,
+						   void *buffer, int count,
+						   int *err)
+{
+	*err = -EOPNOTSUPP;
+	return NULL;
+}
+
+#endif
+#endif
-- 
2.34.1
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help