Linux bluetooth development
 help / color / mirror / Atom feed
* [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072
@ 2026-08-31  5:03 Zijun Hu
  2026-08-31  5:03 ` [PATCH v2 1/4] Bluetooth: btusb: Add recv_intr() hook to btusb_data Zijun Hu
                   ` (4 more replies)
  0 siblings, 5 replies; 9+ messages in thread
From: Zijun Hu @ 2026-08-31  5:03 UTC (permalink / raw)
  To: Marcel Holtmann, Luiz Augusto von Dentz
  Cc: Zijun Hu, linux-bluetooth, linux-kernel, Zijun Hu

Hi Luiz,

This revision addresses issues below you raised in our previous discussion.
Could you take a look at this new revision when you get a chance, and let
me know if you have any further comments?

- Pre-process the PERI frame
- Don't hci_send_to_monitor() for PERI frame
- Don't use the safer skb_pull_data() helper

A malformed PERI frame, once identified for what it is, is sometimes still
valuable, so don't drop it early via a min_len/max_len check.
For example, a malformed memdump frame needs to reach its final handler to
abort an in-progress memdump procedure.

This patch series is to support Qualcomm multi-subsystem QCC2072, the
first 3 commits are simple dependencies of the last one, previously
sent separately but now combined into this series, each commit includes
its own changelog below the commit message.

Signed-off-by: Zijun Hu <zijun.hu@oss.qualcomm.com>
---
Zijun Hu (4):
      Bluetooth: btusb: Add recv_intr() hook to btusb_data
      Bluetooth: Add generic support for vendor HCI packets
      Bluetooth: btusb: Build the driver from multiple source files
      Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072

 drivers/bluetooth/Kconfig                   |   14 +
 drivers/bluetooth/Makefile                  |    3 +
 drivers/bluetooth/{btusb.c => btusb_main.c} |   77 +-
 drivers/bluetooth/btusb_qcom.c              | 4513 +++++++++++++++++++++++++++
 drivers/bluetooth/btusb_qcom.h              |   91 +
 include/net/bluetooth/bluetooth.h           |    6 +
 include/net/bluetooth/hci.h                 |    1 +
 include/net/bluetooth/hci_core.h            |    5 +
 include/net/bluetooth/hci_mon.h             |    2 +
 net/bluetooth/hci_core.c                    |   44 +
 net/bluetooth/hci_sock.c                    |   53 +-
 11 files changed, 4802 insertions(+), 7 deletions(-)
---
base-commit: bcb621272aa04a116a74ab5fcc9324231ffa3acb
change-id: 20260824-btusb_qcc2072-abb918ab4220

Best regards,
--  
Zijun Hu <zijun.hu@oss.qualcomm.com>


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 1/4] Bluetooth: btusb: Add recv_intr() hook to btusb_data
  2026-08-31  5:03 [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 Zijun Hu
@ 2026-08-31  5:03 ` Zijun Hu
  2026-08-31  6:41   ` Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 bluez.test.bot
  2026-08-31  5:03 ` [PATCH v2 2/4] Bluetooth: Add generic support for vendor HCI packets Zijun Hu
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 9+ messages in thread
From: Zijun Hu @ 2026-08-31  5:03 UTC (permalink / raw)
  To: Marcel Holtmann, Luiz Augusto von Dentz
  Cc: Zijun Hu, linux-bluetooth, linux-kernel, Zijun Hu

To allow a vendor driver to assemble and handle its non-BT frames
from the interrupt endpoint.

Used by Qualcomm QCC2072 support.

Signed-off-by: Zijun Hu <zijun.hu@oss.qualcomm.com>
---
Previous version:
https://lore.kernel.org/all/20260713-btusb_prep_qcc2072-v2-3-bbcb651285f2@oss.qualcomm.com

Changes since previous version:
- Improve the commit title and message
---
 drivers/bluetooth/btusb.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/drivers/bluetooth/btusb.c b/drivers/bluetooth/btusb.c
index 84614e60d142..79dc6b15c5f5 100644
--- a/drivers/bluetooth/btusb.c
+++ b/drivers/bluetooth/btusb.c
@@ -1034,16 +1034,17 @@ struct btusb_data {
 	unsigned int air_mode;
 	bool usb_alt6_packet_flow;
 	int isoc_altsetting;
 	int suspend_count;
 	const struct usb_device_id *match_id;
 
 	int (*recv_event)(struct hci_dev *hdev, struct sk_buff *skb);
 	int (*recv_acl)(struct hci_dev *hdev, struct sk_buff *skb);
+	int (*recv_intr)(struct btusb_data *data, void *buffer, int count);
 	int (*recv_bulk)(struct btusb_data *data, void *buffer, int count);
 
 	int (*setup_on_usb)(struct hci_dev *hdev);
 
 	int (*suspend)(struct hci_dev *hdev);
 	int (*resume)(struct hci_dev *hdev);
 	int (*disconnect)(struct hci_dev *hdev);
 
@@ -1508,17 +1509,17 @@ static void btusb_intr_complete(struct urb *urb)
 	       urb->actual_length);
 
 	if (!test_bit(HCI_RUNNING, &hdev->flags))
 		return;
 
 	if (urb->status == 0) {
 		hdev->stat.byte_rx += urb->actual_length;
 
-		if (btusb_recv_intr(data, urb->transfer_buffer,
+		if (data->recv_intr(data, urb->transfer_buffer,
 				    urb->actual_length) < 0) {
 			bt_dev_err(hdev, "corrupted event packet");
 			hdev->stat.err_rx++;
 		}
 	} else if (urb->status == -ENOENT) {
 		/* Avoid suspend failed when usb_kill_urb */
 		return;
 	}
@@ -2723,17 +2724,17 @@ static int btusb_recv_bulk_intel(struct btusb_data *data, void *buffer,
 {
 	struct hci_dev *hdev = data->hdev;
 
 	/* When the device is in bootloader mode, then it can send
 	 * events via the bulk endpoint. These events are treated the
 	 * same way as the ones received from the interrupt endpoint.
 	 */
 	if (btintel_test_flag(hdev, INTEL_BOOTLOADER))
-		return btusb_recv_intr(data, buffer, count);
+		return data->recv_intr(data, buffer, count);
 
 	return btusb_recv_bulk(data, buffer, count);
 }
 
 static int btusb_send_frame_intel(struct hci_dev *hdev, struct sk_buff *skb)
 {
 	struct urb *urb;
 
@@ -4175,16 +4176,17 @@ static int btusb_probe(struct usb_interface *intf,
 	init_usb_anchor(&data->intr_anchor);
 	init_usb_anchor(&data->bulk_anchor);
 	init_usb_anchor(&data->isoc_anchor);
 	init_usb_anchor(&data->diag_anchor);
 	init_usb_anchor(&data->ctrl_anchor);
 	spin_lock_init(&data->rxlock);
 
 	data->recv_event = hci_recv_frame;
+	data->recv_intr = btusb_recv_intr;
 	data->recv_bulk = btusb_recv_bulk;
 
 	if (id->driver_info & BTUSB_INTEL_COMBINED) {
 		/* Allocate extra space for Intel device */
 		priv_size += sizeof(struct btintel_data);
 
 		/* Override the rx handlers */
 		data->recv_event = btintel_recv_event;

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v2 2/4] Bluetooth: Add generic support for vendor HCI packets
  2026-08-31  5:03 [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 Zijun Hu
  2026-08-31  5:03 ` [PATCH v2 1/4] Bluetooth: btusb: Add recv_intr() hook to btusb_data Zijun Hu
@ 2026-08-31  5:03 ` Zijun Hu
  2026-08-31 14:45   ` Luiz Augusto von Dentz
  2026-08-31  5:03 ` [PATCH v2 3/4] Bluetooth: btusb: Build the driver from multiple source files Zijun Hu
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 9+ messages in thread
From: Zijun Hu @ 2026-08-31  5:03 UTC (permalink / raw)
  To: Marcel Holtmann, Luiz Augusto von Dentz
  Cc: Zijun Hu, linux-bluetooth, linux-kernel, Zijun Hu

The virtual HCI_VENDOR_PKT (0xff) has been defined but never used by
the BT core. Now, there is a requirement to implement it, as follows:

For Qualcomm multi-subsystem BT chips, the transport wire carries both
BT-HCI and PERI-HCI frames, where PERI is a subsystem in the chip, take
the upcoming QCC2072 as an example:

    Packet type                 BT-HCI indicator   PERI-HCI indicator
    -----------------------------------------------------------------
    CMD (Host -> Controller)    0x01               0x31
    ACL Data (bidirectional)    0x02               0x32
    EVENT (Controller -> Host)  0x04               0x34

Implement HCI_VENDOR_PKT to generically support vendor HCI packets:

- Log them in btmon as they appear on the wire.
- Allow userspace to send/receive them over HCI_CHANNEL_USER, gate RX
  with the new socket option BT_RECV_VENDOR_PKT, off by default to
  avoid regressions for existing applications.
- Add hdev->recv_vendor_pkt() to handle them in hci_rx_work().
- Add hci_send_vendor_frame() to send an HCI_VENDOR_PKT frame.

Signed-off-by: Zijun Hu <zijun.hu@oss.qualcomm.com>
---
Previous version:
https://lore.kernel.org/all/20260722-support_vendor_hci-v2-2-132b506460a4@oss.qualcomm.com

Changes since previous version:
- Change BT_RECV_VENDOR_PKT type from int to u32
- Rename macro and enum from *_RCV_* to *_RECV_*
- Improve the commit title and message
---
 include/net/bluetooth/bluetooth.h |  6 +++++
 include/net/bluetooth/hci.h       |  1 +
 include/net/bluetooth/hci_core.h  |  5 ++++
 include/net/bluetooth/hci_mon.h   |  2 ++
 net/bluetooth/hci_core.c          | 44 ++++++++++++++++++++++++++++++++
 net/bluetooth/hci_sock.c          | 53 +++++++++++++++++++++++++++++++++++----
 6 files changed, 106 insertions(+), 5 deletions(-)

diff --git a/include/net/bluetooth/bluetooth.h b/include/net/bluetooth/bluetooth.h
index b624da5026f5..f9b380060976 100644
--- a/include/net/bluetooth/bluetooth.h
+++ b/include/net/bluetooth/bluetooth.h
@@ -251,16 +251,22 @@ struct bt_codecs {
 #define BT_ISO_BASE		20
 
 /* Socket option value 21 reserved */
 
 #define BT_PKT_SEQNUM		22
 
 #define BT_SCM_PKT_SEQNUM	0x05
 
+/*
+ * Control receiving HCI_VENDOR_PKT over HCI_CHANNEL_USER, off
+ * by default to avoid regressions for existing applications.
+ */
+#define BT_RECV_VENDOR_PKT	23
+
 __printf(1, 2)
 void bt_info(const char *fmt, ...);
 __printf(1, 2)
 void bt_warn(const char *fmt, ...);
 __printf(1, 2)
 void bt_err(const char *fmt, ...);
 #if IS_ENABLED(CONFIG_BT_FEATURE_DEBUG)
 void bt_dbg_set(bool enable);
diff --git a/include/net/bluetooth/hci.h b/include/net/bluetooth/hci.h
index 1641d879dbda..0145f629cdd5 100644
--- a/include/net/bluetooth/hci.h
+++ b/include/net/bluetooth/hci.h
@@ -393,16 +393,17 @@ enum {
 	HCI_RAW,
 
 	HCI_RESET,
 };
 
 /* HCI socket flags */
 enum {
 	HCI_SOCK_TRUSTED,
+	HCI_SOCK_RECV_VENDOR_PKT,
 	HCI_MGMT_INDEX_EVENTS,
 	HCI_MGMT_UNCONF_INDEX_EVENTS,
 	HCI_MGMT_EXT_INDEX_EVENTS,
 	HCI_MGMT_EXT_INFO_EVENTS,
 	HCI_MGMT_OPTION_EVENTS,
 	HCI_MGMT_SETTING_EVENTS,
 	HCI_MGMT_DEV_CLASS_EVENTS,
 	HCI_MGMT_LOCAL_NAME_EVENTS,
diff --git a/include/net/bluetooth/hci_core.h b/include/net/bluetooth/hci_core.h
index c12cd6873f65..b26004a05368 100644
--- a/include/net/bluetooth/hci_core.h
+++ b/include/net/bluetooth/hci_core.h
@@ -23,16 +23,17 @@
 #ifndef __HCI_CORE_H
 #define __HCI_CORE_H
 
 #include <linux/idr.h>
 #include <linux/leds.h>
 #include <linux/rculist.h>
 #include <linux/spinlock.h>
 #include <linux/srcu.h>
+#include <linux/uio.h>
 
 #include <net/bluetooth/hci.h>
 #include <net/bluetooth/hci_drv.h>
 #include <net/bluetooth/hci_sync.h>
 #include <net/bluetooth/hci_sock.h>
 #include <net/bluetooth/coredump.h>
 
 /* HCI priority */
@@ -641,16 +642,18 @@ struct hci_dev {
 #endif
 
 	int (*open)(struct hci_dev *hdev);
 	int (*close)(struct hci_dev *hdev);
 	int (*flush)(struct hci_dev *hdev);
 	int (*setup)(struct hci_dev *hdev);
 	int (*shutdown)(struct hci_dev *hdev);
 	int (*send)(struct hci_dev *hdev, struct sk_buff *skb);
+	/* Receive HCI_VENDOR_PKT */
+	void (*recv_vendor_pkt)(struct hci_dev *hdev, struct sk_buff *skb);
 	/* Handle HCI_EV_VENDOR; return true if handled, false otherwise */
 	bool (*handle_ev_vendor)(struct hci_dev *hdev, struct sk_buff *skb);
 	void (*notify)(struct hci_dev *hdev, unsigned int evt);
 	void (*hw_error)(struct hci_dev *hdev, u8 code);
 	int (*post_init)(struct hci_dev *hdev);
 	int (*set_diag)(struct hci_dev *hdev, bool enable);
 	int (*set_bdaddr)(struct hci_dev *hdev, const bdaddr_t *bdaddr);
 	void (*reset)(struct hci_dev *hdev);
@@ -2395,16 +2398,18 @@ static inline int hci_check_conn_params(u16 min, u16 max, u16 latency,
 	}
 
 	return 0;
 }
 
 int hci_register_cb(struct hci_cb *hcb);
 int hci_unregister_cb(struct hci_cb *hcb);
 
+int hci_send_vendor_frame(struct hci_dev *hdev, struct iov_iter *iter);
+
 int __hci_cmd_send(struct hci_dev *hdev, u16 opcode, u32 plen,
 		   const void *param);
 
 int hci_send_cmd(struct hci_dev *hdev, __u16 opcode, __u32 plen,
 		 const void *param);
 void hci_send_acl(struct hci_chan *chan, struct sk_buff *skb, __u16 flags);
 void hci_send_sco(struct hci_conn *conn, struct sk_buff *skb);
 void hci_send_iso(struct hci_conn *conn, struct sk_buff *skb);
diff --git a/include/net/bluetooth/hci_mon.h b/include/net/bluetooth/hci_mon.h
index 4b2a0af4ed58..7710688c0d30 100644
--- a/include/net/bluetooth/hci_mon.h
+++ b/include/net/bluetooth/hci_mon.h
@@ -45,16 +45,18 @@ struct hci_mon_hdr {
 #define HCI_MON_CTRL_OPEN	14
 #define HCI_MON_CTRL_CLOSE	15
 #define HCI_MON_CTRL_COMMAND	16
 #define HCI_MON_CTRL_EVENT	17
 #define HCI_MON_ISO_TX_PKT	18
 #define HCI_MON_ISO_RX_PKT	19
 #define HCI_MON_DRV_TX_PKT	20
 #define HCI_MON_DRV_RX_PKT	21
+#define HCI_MON_VENDOR_TX_PKT	22
+#define HCI_MON_VENDOR_RX_PKT	23
 
 struct hci_mon_new_index {
 	__u8		type;
 	__u8		bus;
 	bdaddr_t	bdaddr;
 	char		name[8] __nonstring;
 } __packed;
 #define HCI_MON_NEW_INDEX_SIZE 16
diff --git a/net/bluetooth/hci_core.c b/net/bluetooth/hci_core.c
index 88df159d3393..2663654cdf60 100644
--- a/net/bluetooth/hci_core.c
+++ b/net/bluetooth/hci_core.c
@@ -2911,16 +2911,18 @@ int hci_recv_frame(struct hci_dev *hdev, struct sk_buff *skb)
 			    type == PA_LINK)
 				hci_skb_pkt_type(skb) = HCI_ISODATA_PKT;
 		}
 		break;
 	case HCI_SCODATA_PKT:
 		break;
 	case HCI_ISODATA_PKT:
 		break;
+	case HCI_VENDOR_PKT:
+		break;
 	case HCI_DRV_PKT:
 		break;
 	default:
 		kfree_skb(skb);
 		return -EINVAL;
 	}
 
 	/* Incoming skb */
@@ -3047,16 +3049,50 @@ static int hci_send_frame(struct hci_dev *hdev, struct sk_buff *skb)
 
 static int hci_send_conn_frame(struct hci_dev *hdev, struct hci_conn *conn,
 			       struct sk_buff *skb)
 {
 	hci_conn_tx_queue(conn, skb);
 	return hci_send_frame(hdev, skb);
 }
 
+/**
+ * hci_send_vendor_frame - Send an HCI_VENDOR_PKT frame to the HCI driver
+ * @hdev: The HCI device
+ * @iter: iov_iter carrying the frame
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+int hci_send_vendor_frame(struct hci_dev *hdev, struct iov_iter *iter)
+{
+	struct sk_buff *skb;
+	size_t len;
+
+	if (WARN_ON(!iov_iter_is_kvec(iter)))
+		return -EINVAL;
+
+	len = iov_iter_count(iter);
+	if (!len)
+		return -EINVAL;
+
+	skb = bt_skb_alloc(len, GFP_KERNEL);
+	if (!skb)
+		return -ENOMEM;
+
+	if (!copy_from_iter_full(skb_put(skb, len), len, iter)) {
+		kfree_skb(skb);
+		return -EFAULT;
+	}
+
+	hci_skb_pkt_type(skb) = HCI_VENDOR_PKT;
+
+	return hci_send_frame(hdev, skb);
+}
+EXPORT_SYMBOL(hci_send_vendor_frame);
+
 /* Send HCI command */
 int hci_send_cmd(struct hci_dev *hdev, __u16 opcode, __u32 plen,
 		 const void *param)
 {
 	struct sk_buff *skb;
 
 	BT_DBG("%s opcode 0x%4.4x plen %d", hdev->name, opcode, plen);
 
@@ -4051,16 +4087,24 @@ static void hci_rx_work(struct work_struct *work)
 			hci_scodata_packet(hdev, skb);
 			break;
 
 		case HCI_ISODATA_PKT:
 			BT_DBG("%s ISO data packet", hdev->name);
 			hci_isodata_packet(hdev, skb);
 			break;
 
+		case HCI_VENDOR_PKT:
+			BT_DBG("%s Vendor packet", hdev->name);
+			if (hdev->recv_vendor_pkt)
+				hdev->recv_vendor_pkt(hdev, skb);
+			else
+				kfree_skb(skb);
+			break;
+
 		default:
 			kfree_skb(skb);
 			break;
 		}
 	}
 }
 
 static int hci_send_cmd_sync(struct hci_dev *hdev, struct sk_buff *skb)
diff --git a/net/bluetooth/hci_sock.c b/net/bluetooth/hci_sock.c
index 070ca388f9ac..4b328fa8dff4 100644
--- a/net/bluetooth/hci_sock.c
+++ b/net/bluetooth/hci_sock.c
@@ -224,22 +224,26 @@ void hci_send_to_sock(struct hci_dev *hdev, struct sk_buff *skb)
 			    hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
 			    hci_skb_pkt_type(skb) != HCI_ISODATA_PKT)
 				continue;
 			if (is_filtered_packet(sk, skb))
 				continue;
 		} else if (hci_pi(sk)->channel == HCI_CHANNEL_USER) {
 			if (!bt_cb(skb)->incoming)
 				continue;
-			if (hci_skb_pkt_type(skb) != HCI_EVENT_PKT &&
-			    hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
-			    hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
-			    hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
-			    hci_skb_pkt_type(skb) != HCI_DRV_PKT)
+			if (hci_skb_pkt_type(skb) == HCI_VENDOR_PKT) {
+				if (!hci_sock_test_flag(sk, HCI_SOCK_RECV_VENDOR_PKT))
+					continue;
+			} else if (hci_skb_pkt_type(skb) != HCI_EVENT_PKT &&
+				   hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
+				   hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
+				   hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
+				   hci_skb_pkt_type(skb) != HCI_DRV_PKT) {
 				continue;
+			}
 		} else {
 			/* Don't send frame to other channel types */
 			continue;
 		}
 
 		if (!skb_copy) {
 			/* Create a private copy with headroom */
 			skb_copy = __pskb_copy_fclone(skb, 1, GFP_ATOMIC, true);
@@ -385,16 +389,22 @@ void hci_send_to_monitor(struct hci_dev *hdev, struct sk_buff *skb)
 			opcode = cpu_to_le16(HCI_MON_SCO_TX_PKT);
 		break;
 	case HCI_ISODATA_PKT:
 		if (bt_cb(skb)->incoming)
 			opcode = cpu_to_le16(HCI_MON_ISO_RX_PKT);
 		else
 			opcode = cpu_to_le16(HCI_MON_ISO_TX_PKT);
 		break;
+	case HCI_VENDOR_PKT:
+		if (bt_cb(skb)->incoming)
+			opcode = cpu_to_le16(HCI_MON_VENDOR_RX_PKT);
+		else
+			opcode = cpu_to_le16(HCI_MON_VENDOR_TX_PKT);
+		break;
 	case HCI_DRV_PKT:
 		if (bt_cb(skb)->incoming)
 			opcode = cpu_to_le16(HCI_MON_DRV_RX_PKT);
 		else
 			opcode = cpu_to_le16(HCI_MON_DRV_TX_PKT);
 		break;
 	case HCI_DIAG_PKT:
 		opcode = cpu_to_le16(HCI_MON_VENDOR_DIAG);
@@ -1863,16 +1873,17 @@ static int hci_sock_sendmsg(struct socket *sock, struct msghdr *msg,
 		 * since that gets enforced when binding the socket.
 		 *
 		 * However check that the packet type is valid.
 		 */
 		if (hci_skb_pkt_type(skb) != HCI_COMMAND_PKT &&
 		    hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
 		    hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
 		    hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
+		    hci_skb_pkt_type(skb) != HCI_VENDOR_PKT &&
 		    hci_skb_pkt_type(skb) != HCI_DRV_PKT) {
 			err = -EINVAL;
 			goto drop;
 		}
 
 		skb_queue_tail(&hdev->raw_q, skb);
 		queue_work(hdev->workqueue, &hdev->tx_work);
 	} else if (hci_skb_pkt_type(skb) == HCI_COMMAND_PKT) {
@@ -2012,16 +2023,17 @@ static int hci_sock_setsockopt_old(struct socket *sock, int level, int optname,
 	return err;
 }
 
 static int hci_sock_setsockopt(struct socket *sock, int level, int optname,
 			       sockptr_t optval, unsigned int optlen)
 {
 	struct sock *sk = sock->sk;
 	int err = 0;
+	u32 opt_u32;
 	u16 opt;
 
 	BT_DBG("sk %p, opt %d", sk, optname);
 
 	if (level == SOL_HCI)
 		return hci_sock_setsockopt_old(sock, level, optname, optval,
 					       optlen);
 
@@ -2045,16 +2057,33 @@ static int hci_sock_setsockopt(struct socket *sock, int level, int optname,
 
 		err = copy_safe_from_sockptr(&opt, sizeof(opt), optval, optlen);
 		if (err)
 			break;
 
 		hci_pi(sk)->mtu = opt;
 		break;
 
+	case BT_RECV_VENDOR_PKT:
+		if (hci_pi(sk)->channel != HCI_CHANNEL_USER) {
+			err = -ENOPROTOOPT;
+			break;
+		}
+
+		err = copy_safe_from_sockptr(&opt_u32, sizeof(opt_u32),
+					     optval, optlen);
+		if (err)
+			break;
+
+		if (opt_u32)
+			hci_sock_set_flag(sk, HCI_SOCK_RECV_VENDOR_PKT);
+		else
+			hci_sock_clear_flag(sk, HCI_SOCK_RECV_VENDOR_PKT);
+		break;
+
 	default:
 		err = -ENOPROTOOPT;
 		break;
 	}
 
 done:
 	release_sock(sk);
 	return err;
@@ -2127,16 +2156,17 @@ static int hci_sock_getsockopt_old(struct socket *sock, int level, int optname,
 	return err;
 }
 
 static int hci_sock_getsockopt(struct socket *sock, int level, int optname,
 			       sockopt_t *sopt)
 {
 	struct sock *sk = sock->sk;
 	int err = 0;
+	u32 opt_u32;
 	u16 mtu;
 
 	BT_DBG("sk %p, opt %d", sk, optname);
 
 	if (level == SOL_HCI)
 		return hci_sock_getsockopt_old(sock, level, optname, sopt);
 
 	if (level != SOL_BLUETOOTH)
@@ -2148,16 +2178,29 @@ static int hci_sock_getsockopt(struct socket *sock, int level, int optname,
 	case BT_SNDMTU:
 	case BT_RCVMTU:
 		mtu = hci_pi(sk)->mtu;
 		if (copy_to_iter(&mtu, sizeof(mtu), &sopt->iter_out) !=
 		    sizeof(mtu))
 			err = -EFAULT;
 		break;
 
+	case BT_RECV_VENDOR_PKT:
+		if (hci_pi(sk)->channel != HCI_CHANNEL_USER) {
+			err = -ENOPROTOOPT;
+			break;
+		}
+
+		opt_u32 = hci_sock_test_flag(sk, HCI_SOCK_RECV_VENDOR_PKT) ?
+			  1 : 0;
+		if (copy_to_iter(&opt_u32, sizeof(opt_u32),
+				 &sopt->iter_out) != sizeof(opt_u32))
+			err = -EFAULT;
+		break;
+
 	default:
 		err = -ENOPROTOOPT;
 		break;
 	}
 
 	release_sock(sk);
 	return err;
 }

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v2 3/4] Bluetooth: btusb: Build the driver from multiple source files
  2026-08-31  5:03 [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 Zijun Hu
  2026-08-31  5:03 ` [PATCH v2 1/4] Bluetooth: btusb: Add recv_intr() hook to btusb_data Zijun Hu
  2026-08-31  5:03 ` [PATCH v2 2/4] Bluetooth: Add generic support for vendor HCI packets Zijun Hu
@ 2026-08-31  5:03 ` Zijun Hu
  2026-08-31  5:03 ` [PATCH v2 4/4] Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072 Zijun Hu
  2026-08-31 14:34 ` [PATCH v2 0/4] Bluetooth: btusb: Support " Luiz Augusto von Dentz
  4 siblings, 0 replies; 9+ messages in thread
From: Zijun Hu @ 2026-08-31  5:03 UTC (permalink / raw)
  To: Marcel Holtmann, Luiz Augusto von Dentz
  Cc: Zijun Hu, linux-bluetooth, linux-kernel, Zijun Hu

Allow the driver to include vendor USB-transport-specific source files by:
- renaming btusb.c to btusb_main.c
- making btusb.o a composite object

Used by Qualcomm QCC2072 support.

Signed-off-by: Zijun Hu <zijun.hu@oss.qualcomm.com>
---
Previous version:
https://lore.kernel.org/all/20260714-btusb_main-v2-1-fd32cf15c26e@oss.qualcomm.com

Changes since previous version:
- Improve commit message
---
 drivers/bluetooth/Makefile                  | 2 ++
 drivers/bluetooth/{btusb.c => btusb_main.c} | 0
 2 files changed, 2 insertions(+)

diff --git a/drivers/bluetooth/Makefile b/drivers/bluetooth/Makefile
index e6b1c1180d1d..8b436c6de8b7 100644
--- a/drivers/bluetooth/Makefile
+++ b/drivers/bluetooth/Makefile
@@ -46,9 +46,11 @@ hci_uart-$(CONFIG_BT_HCIUART_3WIRE)	+= hci_h5.o
 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
+
 CONTEXT_ANALYSIS := y
diff --git a/drivers/bluetooth/btusb.c b/drivers/bluetooth/btusb_main.c
similarity index 100%
rename from drivers/bluetooth/btusb.c
rename to drivers/bluetooth/btusb_main.c

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH v2 4/4] Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072
  2026-08-31  5:03 [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 Zijun Hu
                   ` (2 preceding siblings ...)
  2026-08-31  5:03 ` [PATCH v2 3/4] Bluetooth: btusb: Build the driver from multiple source files Zijun Hu
@ 2026-08-31  5:03 ` Zijun Hu
  2026-08-31 14:34 ` [PATCH v2 0/4] Bluetooth: btusb: Support " Luiz Augusto von Dentz
  4 siblings, 0 replies; 9+ messages in thread
From: Zijun Hu @ 2026-08-31  5:03 UTC (permalink / raw)
  To: Marcel Holtmann, Luiz Augusto von Dentz
  Cc: Zijun Hu, linux-bluetooth, linux-kernel, Zijun Hu

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 <zijun.hu@oss.qualcomm.com>
---
Previous version:
https://lore.kernel.org/all/20260713-btusb_qcom-v1-1-39cabc0dd601@oss.qualcomm.com

Changes since previous version:
- Add CONFIG_BT_HCIBTUSB_QCOM to gate building btusb_qcom.c
- Don't pre-process PERI frames, so they reach btmon
- Use the safer skb_pull_data() helper
- Change btusb_qcom_memdump_hdr() to generate a static instead of
   dynamic devcoredump header
- Improve PERI TX-sync event sequence with reference to hci_sync.c
- Support BTC recovery by triggering a memdump on command timeout,
   which also aids debugging
- Sort sections, with "BTC supported ..." first
- Improve the commit title and message
---
 drivers/bluetooth/Kconfig      |   14 +
 drivers/bluetooth/Makefile     |    1 +
 drivers/bluetooth/btusb_main.c |   71 +
 drivers/bluetooth/btusb_qcom.c | 4513 ++++++++++++++++++++++++++++++++++++++++
 drivers/bluetooth/btusb_qcom.h |   91 +
 5 files changed, 4690 insertions(+)

diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig
index 4e8c24d757e9..5bd0f82bbe67 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 79dc6b15c5f5..4031405b41c3 100644
--- a/drivers/bluetooth/btusb_main.c
+++ b/drivers/bluetooth/btusb_main.c
@@ -22,16 +22,17 @@
 #include <net/bluetooth/bluetooth.h>
 #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 "btusb_qcom.h"
 
 #define VERSION "0.8"
 
 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;
@@ -63,16 +64,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[] = {
 	/* Generic Bluetooth USB device */
 	{ USB_DEVICE_INFO(0xe0, 0x01, 0x01) },
 
 	/* Generic Bluetooth AMP device */
 	{ USB_DEVICE_INFO(0xe0, 0x01, 0x04), .driver_info = BTUSB_AMP },
 
@@ -410,16 +412,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 },
@@ -4097,16 +4103,54 @@ 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;
+}
+
+/*
+ * ============================================================================
+ * Qualcomm support end
+ * ============================================================================
+ */
+
 static int btusb_probe(struct usb_interface *intf,
 		       const struct usb_device_id *id)
 {
 	struct gpio_desc *reset_gpio;
 	struct btusb_data *data;
 	struct hci_dev *hdev;
 	unsigned ifnum_base;
 	int err, priv_size = 0;
@@ -4197,16 +4241,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;
@@ -4355,16 +4402,40 @@ 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->send_frame = btusb_send_frame;
+		xport_data->recv_acl   = btusb_recv_acl;
+		xport_data->recv_event = btusb_recv_event;
+
+		data->recv_intr = btusb_recv_intr_qcom;
+		data->recv_bulk = btusb_recv_bulk_qcom;
+		data->disconnect = btusb_qcom_disconnect;
+
+		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 {
 		/* Interface orders are hardcoded in the specification */
 		data->isoc = usb_ifnum_to_if(data->udev, ifnum_base + 1);
 		data->isoc_ifnum = ifnum_base + 1;
 	}
diff --git a/drivers/bluetooth/btusb_qcom.c b/drivers/bluetooth/btusb_qcom.c
new file mode 100644
index 000000000000..8a17eda6c7c5
--- /dev/null
+++ b/drivers/bluetooth/btusb_qcom.c
@@ -0,0 +1,4513 @@
+// 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
+ * @xport_tx_mutex: serializes sending frames to the transport
+ * @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 mutex xport_tx_mutex;
+	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))
+#define QPERI_COMMAND_PKT	0x31
+
+struct qperi_event_hdr {
+	u8	host_id;
+	u8	evt;
+	u8	plen;
+} __packed;
+#define QPERI_EVENT_HDR_SIZE	(sizeof(struct qperi_event_hdr))
+#define QPERI_EVENT_PKT	0x34
+#define QPERI_MAX_EVENT_SIZE	(HCI_MAX_EVENT_SIZE + 1)
+
+struct qperi_acl_hdr {
+	u8	host_id;
+	__le16	handle;
+	__le16	dlen;
+} __packed;
+#define QPERI_ACL_HDR_SIZE	(sizeof(struct qperi_acl_hdr))
+#define QPERI_ACLDATA_PKT	0x32
+#define QPERI_MAX_FRAME_SIZE	(HCI_MAX_FRAME_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;
+}
+
+/*
+ * ============================================================================
+ * 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;
+
+	memset(buf, 'P', sizeof(buf));
+	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);
+
+	/*
+	 * 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;
+}
+
+/* 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)
+		goto out;
+
+	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;
+	}
+
+out:
+	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;
+
+		scoped_guard(spinlock, &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;
+		}
+
+		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;
+
+	/* Not an unbalanced PM refcount — the device will reset. */
+	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;
+	}
+
+	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);
+	}
+
+	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;
+
+	if (!qbt_data->inited)
+		return;
+
+	mutex_destroy(&qbt_data->req_mutex);
+	mutex_destroy(&qbt_data->xport_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 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;
+
+	hdev->manufacturer = 0x1d;
+	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(&qbt_data->req_mutex);
+	init_waitqueue_head(&qbt_data->req_wait_q);
+	spin_lock_init(&qbt_data->req_spinlock);
+	mutex_init(&qbt_data->xport_tx_mutex);
+
+	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 buf[512];
+	int len = 0;
+
+	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];
+
+		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);
+	}
+	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);
+	u8 pkt_type;
+	int ret;
+
+	if (qbt_data->category != QBTC_CAT_MSUBSYS)
+		return xport_data->send_frame(hdev, skb);
+
+	pkt_type = hci_skb_pkt_type(skb);
+	if (pkt_type == HCI_VENDOR_PKT)
+		hci_skb_pkt_type(skb) = *(const u8 *)skb_pull_data(skb, 1);
+
+	pkt_type = hci_skb_pkt_type(skb);
+	if (pkt_type == QPERI_COMMAND_PKT)
+		hci_skb_pkt_type(skb) = HCI_COMMAND_PKT;
+	else if (pkt_type == QPERI_ACLDATA_PKT)
+		hci_skb_pkt_type(skb) = HCI_ACLDATA_PKT;
+
+	pkt_type = hci_skb_pkt_type(skb);
+	if (pkt_type != HCI_COMMAND_PKT)
+		return xport_data->send_frame(hdev, skb);
+
+	mutex_lock(&qbt_data->xport_tx_mutex);
+	ret = xport_data->send_frame(hdev, skb);
+	mutex_unlock(&qbt_data->xport_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_{event,acl}() (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;
+		ret = hci_recv_frame(hdev, skb);
+		break;
+	case HCI_EVENT_PKT:
+		ret = xport_data->recv_event(hdev, skb);
+		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;
+			ret = hci_recv_frame(hdev, skb);
+		} else {
+			ret = xport_data->recv_acl(hdev, skb);
+		}
+		break;
+	default:
+		dev_kfree_skb_irq(skb);
+		ret = -EINVAL;
+		break;
+	}
+
+	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..200c5e923dba
--- /dev/null
+++ b/drivers/bluetooth/btusb_qcom.h
@@ -0,0 +1,91 @@
+/* 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/skbuff.h>
+#include <linux/usb.h>
+
+#include <net/bluetooth/hci_core.h>
+
+struct btusb_qcom {
+	__u16                 idVendor;
+	__u16                 idProduct;
+	struct usb_device    *udev;
+	struct usb_interface *intf;
+	struct gpio_desc *reset_gpio;
+
+	void (*prepare_reset)(struct hci_dev *hdev);
+	int (*send_frame)(struct hci_dev *hdev, struct sk_buff *skb);
+	int (*recv_acl)(struct hci_dev *hdev, struct sk_buff *skb);
+	int (*recv_event)(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


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* RE: Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072
  2026-08-31  5:03 ` [PATCH v2 1/4] Bluetooth: btusb: Add recv_intr() hook to btusb_data Zijun Hu
@ 2026-08-31  6:41   ` bluez.test.bot
  0 siblings, 0 replies; 9+ messages in thread
From: bluez.test.bot @ 2026-08-31  6:41 UTC (permalink / raw)
  To: linux-bluetooth, zijun.hu

[-- Attachment #1: Type: text/plain, Size: 4254 bytes --]

This is automated email and please do not reply to this email!

Dear submitter,

Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1154112

---Test result---

Test Summary:
CheckPatch                    FAIL      7.74 seconds
VerifyFixes                   PASS      0.08 seconds
VerifySignedoff               PASS      0.08 seconds
GitLint                       FAIL      0.92 seconds
SubjectPrefix                 PASS      0.32 seconds
BuildKernel                   PASS      28.98 seconds
CheckAllWarning               PASS      31.99 seconds
CheckSparse                   PASS      30.60 seconds
BuildKernel32                 PASS      27.89 seconds
CheckKernelLLVM               SKIP      0.00 seconds
TestRunnerSetup               PASS      527.19 seconds
TestRunner_l2cap-tester       PASS      71.10 seconds
TestRunner_iso-tester         PASS      108.50 seconds
TestRunner_bnep-tester        PASS      20.19 seconds
TestRunner_mgmt-tester        FAIL      238.88 seconds
TestRunner_rfcomm-tester      PASS      27.38 seconds
TestRunner_sco-tester         PASS      33.32 seconds
TestRunner_ioctl-tester       PASS      27.83 seconds
TestRunner_mesh-tester        FAIL      27.18 seconds
TestRunner_smp-tester         PASS      25.21 seconds
TestRunner_userchan-tester    PASS      20.77 seconds
TestRunner_6lowpan-tester     PASS      24.84 seconds
IncrementalBuild              PASS      53.54 seconds

Details
##############################
Test: CheckPatch - FAIL
Desc: Run checkpatch.pl script
Output:
[v2,3/4] Bluetooth: btusb: Build the driver from multiple source files
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#174: 
 drivers/bluetooth/{btusb.c => btusb_main.c} | 0

total: 0 errors, 1 warnings, 11 lines checked

NOTE: For some of the reported defects, checkpatch may be able to
      mechanically convert to the typical style using --fix or --fix-inplace.

/github/workspace/src/patch/14776343.patch has style problems, please review.

NOTE: Ignored message types: UNKNOWN_COMMIT_ID

NOTE: If any of the errors are false positives, please report
      them to the maintainer, see CHECKPATCH in MAINTAINERS.


[v2,4/4] Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#471: 
new file mode 100644

total: 0 errors, 1 warnings, 4812 lines checked

NOTE: For some of the reported defects, checkpatch may be able to
      mechanically convert to the typical style using --fix or --fix-inplace.

/github/workspace/src/patch/14776345.patch has style problems, please review.

NOTE: Ignored message types: UNKNOWN_COMMIT_ID

NOTE: If any of the errors are false positives, please report
      them to the maintainer, see CHECKPATCH in MAINTAINERS.


##############################
Test: GitLint - FAIL
Desc: Run gitlint
Output:
[v2,4/4] Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072

8: B3 Line contains hard tab characters (\t): "       USB I/F	|	   internal link	|"
9: B3 Line contains hard tab characters (\t): "BTHOST --------	|---- PERI -------------- BT	|"
10: B3 Line contains hard tab characters (\t): "		|_______________________________|"
##############################
Test: CheckKernelLLVM - SKIP
Desc: Build kernel with LLVM + context analysis
Output:
Clang not found
##############################
Test: TestRunner_mgmt-tester - FAIL
Desc: Run mgmt-tester with test-runner
Output:
Total: 501, Passed: 495 (98.8%), Failed: 2, Not Run: 4

Failed Test Cases
Read Exp Feature - Success                           Failed       0.276 seconds
LL Privacy - Unpair 1                                Timed out    1.960 seconds
##############################
Test: TestRunner_mesh-tester - FAIL
Desc: Run mesh-tester with test-runner
Output:
Total: 10, Passed: 8 (80.0%), Failed: 2, Not Run: 0

Failed Test Cases
Mesh - Send cancel - 1                               Timed out    2.064 seconds
Mesh - Send cancel - 2                               Timed out    1.992 seconds


https://github.com/bluez/bluetooth-next/pull/670

---
Regards,
Linux Bluetooth


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072
  2026-08-31  5:03 [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 Zijun Hu
                   ` (3 preceding siblings ...)
  2026-08-31  5:03 ` [PATCH v2 4/4] Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072 Zijun Hu
@ 2026-08-31 14:34 ` Luiz Augusto von Dentz
  4 siblings, 0 replies; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-08-31 14:34 UTC (permalink / raw)
  To: Zijun Hu; +Cc: Marcel Holtmann, Zijun Hu, linux-bluetooth, linux-kernel

Hi Zijun,

On Mon, Aug 31, 2026 at 1:04 AM Zijun Hu <zijun.hu@oss.qualcomm.com> wrote:
>
> Hi Luiz,
>
> This revision addresses issues below you raised in our previous discussion.
> Could you take a look at this new revision when you get a chance, and let
> me know if you have any further comments?
>
> - Pre-process the PERI frame
> - Don't hci_send_to_monitor() for PERI frame
> - Don't use the safer skb_pull_data() helper
>
> A malformed PERI frame, once identified for what it is, is sometimes still
> valuable, so don't drop it early via a min_len/max_len check.
> For example, a malformed memdump frame needs to reach its final handler to
> abort an in-progress memdump procedure.
>
> This patch series is to support Qualcomm multi-subsystem QCC2072, the
> first 3 commits are simple dependencies of the last one, previously
> sent separately but now combined into this series, each commit includes
> its own changelog below the commit message.
>
> Signed-off-by: Zijun Hu <zijun.hu@oss.qualcomm.com>
> ---
> Zijun Hu (4):
>       Bluetooth: btusb: Add recv_intr() hook to btusb_data
>       Bluetooth: Add generic support for vendor HCI packets
>       Bluetooth: btusb: Build the driver from multiple source files
>       Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072
>
>  drivers/bluetooth/Kconfig                   |   14 +
>  drivers/bluetooth/Makefile                  |    3 +
>  drivers/bluetooth/{btusb.c => btusb_main.c} |   77 +-
>  drivers/bluetooth/btusb_qcom.c              | 4513 +++++++++++++++++++++++++++
>  drivers/bluetooth/btusb_qcom.h              |   91 +
>  include/net/bluetooth/bluetooth.h           |    6 +
>  include/net/bluetooth/hci.h                 |    1 +
>  include/net/bluetooth/hci_core.h            |    5 +
>  include/net/bluetooth/hci_mon.h             |    2 +
>  net/bluetooth/hci_core.c                    |   44 +
>  net/bluetooth/hci_sock.c                    |   53 +-
>  11 files changed, 4802 insertions(+), 7 deletions(-)
> ---
> base-commit: bcb621272aa04a116a74ab5fcc9324231ffa3acb
> change-id: 20260824-btusb_qcc2072-abb918ab4220
>
> Best regards,
> --
> Zijun Hu <zijun.hu@oss.qualcomm.com>

Sashiko found several issues with this series:

https://sashiko.dev/#/patchset/20260830-btusb_qcc2072-v2-0-5c0e0c9dd98b%40oss.qualcomm.com

-- 
Luiz Augusto von Dentz

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 2/4] Bluetooth: Add generic support for vendor HCI packets
  2026-08-31  5:03 ` [PATCH v2 2/4] Bluetooth: Add generic support for vendor HCI packets Zijun Hu
@ 2026-08-31 14:45   ` Luiz Augusto von Dentz
  2026-09-02 10:22     ` Zijun Hu
  0 siblings, 1 reply; 9+ messages in thread
From: Luiz Augusto von Dentz @ 2026-08-31 14:45 UTC (permalink / raw)
  To: Zijun Hu; +Cc: Marcel Holtmann, Zijun Hu, linux-bluetooth, linux-kernel

Hi Zijun,

On Mon, Aug 31, 2026 at 1:04 AM Zijun Hu <zijun.hu@oss.qualcomm.com> wrote:
>
> The virtual HCI_VENDOR_PKT (0xff) has been defined but never used by
> the BT core. Now, there is a requirement to implement it, as follows:
>
> For Qualcomm multi-subsystem BT chips, the transport wire carries both
> BT-HCI and PERI-HCI frames, where PERI is a subsystem in the chip, take
> the upcoming QCC2072 as an example:
>
>     Packet type                 BT-HCI indicator   PERI-HCI indicator
>     -----------------------------------------------------------------
>     CMD (Host -> Controller)    0x01               0x31
>     ACL Data (bidirectional)    0x02               0x32
>     EVENT (Controller -> Host)  0x04               0x34
>
> Implement HCI_VENDOR_PKT to generically support vendor HCI packets:
>
> - Log them in btmon as they appear on the wire.
> - Allow userspace to send/receive them over HCI_CHANNEL_USER, gate RX
>   with the new socket option BT_RECV_VENDOR_PKT, off by default to
>   avoid regressions for existing applications.
> - Add hdev->recv_vendor_pkt() to handle them in hci_rx_work().
> - Add hci_send_vendor_frame() to send an HCI_VENDOR_PKT frame.

I don't find this one very useful, as btmon already supports handling
vendor events:

{ 0xff, "Vendor", vendor_evt, 0, false },

This will complicate things because some drivers might label packets
as vendor while others might inline them as an HCI vendor event. From
the spec's point of view there is no generic HCI vendor packet, so the
PERI approach really stands out as a non-compliant solution because it
uses reserved opcodes.

> Signed-off-by: Zijun Hu <zijun.hu@oss.qualcomm.com>
> ---
> Previous version:
> https://lore.kernel.org/all/20260722-support_vendor_hci-v2-2-132b506460a4@oss.qualcomm.com
>
> Changes since previous version:
> - Change BT_RECV_VENDOR_PKT type from int to u32
> - Rename macro and enum from *_RCV_* to *_RECV_*
> - Improve the commit title and message
> ---
>  include/net/bluetooth/bluetooth.h |  6 +++++
>  include/net/bluetooth/hci.h       |  1 +
>  include/net/bluetooth/hci_core.h  |  5 ++++
>  include/net/bluetooth/hci_mon.h   |  2 ++
>  net/bluetooth/hci_core.c          | 44 ++++++++++++++++++++++++++++++++
>  net/bluetooth/hci_sock.c          | 53 +++++++++++++++++++++++++++++++++++----
>  6 files changed, 106 insertions(+), 5 deletions(-)
>
> diff --git a/include/net/bluetooth/bluetooth.h b/include/net/bluetooth/bluetooth.h
> index b624da5026f5..f9b380060976 100644
> --- a/include/net/bluetooth/bluetooth.h
> +++ b/include/net/bluetooth/bluetooth.h
> @@ -251,16 +251,22 @@ struct bt_codecs {
>  #define BT_ISO_BASE            20
>
>  /* Socket option value 21 reserved */
>
>  #define BT_PKT_SEQNUM          22
>
>  #define BT_SCM_PKT_SEQNUM      0x05
>
> +/*
> + * Control receiving HCI_VENDOR_PKT over HCI_CHANNEL_USER, off
> + * by default to avoid regressions for existing applications.
> + */
> +#define BT_RECV_VENDOR_PKT     23
> +
>  __printf(1, 2)
>  void bt_info(const char *fmt, ...);
>  __printf(1, 2)
>  void bt_warn(const char *fmt, ...);
>  __printf(1, 2)
>  void bt_err(const char *fmt, ...);
>  #if IS_ENABLED(CONFIG_BT_FEATURE_DEBUG)
>  void bt_dbg_set(bool enable);
> diff --git a/include/net/bluetooth/hci.h b/include/net/bluetooth/hci.h
> index 1641d879dbda..0145f629cdd5 100644
> --- a/include/net/bluetooth/hci.h
> +++ b/include/net/bluetooth/hci.h
> @@ -393,16 +393,17 @@ enum {
>         HCI_RAW,
>
>         HCI_RESET,
>  };
>
>  /* HCI socket flags */
>  enum {
>         HCI_SOCK_TRUSTED,
> +       HCI_SOCK_RECV_VENDOR_PKT,
>         HCI_MGMT_INDEX_EVENTS,
>         HCI_MGMT_UNCONF_INDEX_EVENTS,
>         HCI_MGMT_EXT_INDEX_EVENTS,
>         HCI_MGMT_EXT_INFO_EVENTS,
>         HCI_MGMT_OPTION_EVENTS,
>         HCI_MGMT_SETTING_EVENTS,
>         HCI_MGMT_DEV_CLASS_EVENTS,
>         HCI_MGMT_LOCAL_NAME_EVENTS,
> diff --git a/include/net/bluetooth/hci_core.h b/include/net/bluetooth/hci_core.h
> index c12cd6873f65..b26004a05368 100644
> --- a/include/net/bluetooth/hci_core.h
> +++ b/include/net/bluetooth/hci_core.h
> @@ -23,16 +23,17 @@
>  #ifndef __HCI_CORE_H
>  #define __HCI_CORE_H
>
>  #include <linux/idr.h>
>  #include <linux/leds.h>
>  #include <linux/rculist.h>
>  #include <linux/spinlock.h>
>  #include <linux/srcu.h>
> +#include <linux/uio.h>
>
>  #include <net/bluetooth/hci.h>
>  #include <net/bluetooth/hci_drv.h>
>  #include <net/bluetooth/hci_sync.h>
>  #include <net/bluetooth/hci_sock.h>
>  #include <net/bluetooth/coredump.h>
>
>  /* HCI priority */
> @@ -641,16 +642,18 @@ struct hci_dev {
>  #endif
>
>         int (*open)(struct hci_dev *hdev);
>         int (*close)(struct hci_dev *hdev);
>         int (*flush)(struct hci_dev *hdev);
>         int (*setup)(struct hci_dev *hdev);
>         int (*shutdown)(struct hci_dev *hdev);
>         int (*send)(struct hci_dev *hdev, struct sk_buff *skb);
> +       /* Receive HCI_VENDOR_PKT */
> +       void (*recv_vendor_pkt)(struct hci_dev *hdev, struct sk_buff *skb);
>         /* Handle HCI_EV_VENDOR; return true if handled, false otherwise */
>         bool (*handle_ev_vendor)(struct hci_dev *hdev, struct sk_buff *skb);
>         void (*notify)(struct hci_dev *hdev, unsigned int evt);
>         void (*hw_error)(struct hci_dev *hdev, u8 code);
>         int (*post_init)(struct hci_dev *hdev);
>         int (*set_diag)(struct hci_dev *hdev, bool enable);
>         int (*set_bdaddr)(struct hci_dev *hdev, const bdaddr_t *bdaddr);
>         void (*reset)(struct hci_dev *hdev);
> @@ -2395,16 +2398,18 @@ static inline int hci_check_conn_params(u16 min, u16 max, u16 latency,
>         }
>
>         return 0;
>  }
>
>  int hci_register_cb(struct hci_cb *hcb);
>  int hci_unregister_cb(struct hci_cb *hcb);
>
> +int hci_send_vendor_frame(struct hci_dev *hdev, struct iov_iter *iter);
> +
>  int __hci_cmd_send(struct hci_dev *hdev, u16 opcode, u32 plen,
>                    const void *param);
>
>  int hci_send_cmd(struct hci_dev *hdev, __u16 opcode, __u32 plen,
>                  const void *param);
>  void hci_send_acl(struct hci_chan *chan, struct sk_buff *skb, __u16 flags);
>  void hci_send_sco(struct hci_conn *conn, struct sk_buff *skb);
>  void hci_send_iso(struct hci_conn *conn, struct sk_buff *skb);
> diff --git a/include/net/bluetooth/hci_mon.h b/include/net/bluetooth/hci_mon.h
> index 4b2a0af4ed58..7710688c0d30 100644
> --- a/include/net/bluetooth/hci_mon.h
> +++ b/include/net/bluetooth/hci_mon.h
> @@ -45,16 +45,18 @@ struct hci_mon_hdr {
>  #define HCI_MON_CTRL_OPEN      14
>  #define HCI_MON_CTRL_CLOSE     15
>  #define HCI_MON_CTRL_COMMAND   16
>  #define HCI_MON_CTRL_EVENT     17
>  #define HCI_MON_ISO_TX_PKT     18
>  #define HCI_MON_ISO_RX_PKT     19
>  #define HCI_MON_DRV_TX_PKT     20
>  #define HCI_MON_DRV_RX_PKT     21
> +#define HCI_MON_VENDOR_TX_PKT  22
> +#define HCI_MON_VENDOR_RX_PKT  23
>
>  struct hci_mon_new_index {
>         __u8            type;
>         __u8            bus;
>         bdaddr_t        bdaddr;
>         char            name[8] __nonstring;
>  } __packed;
>  #define HCI_MON_NEW_INDEX_SIZE 16
> diff --git a/net/bluetooth/hci_core.c b/net/bluetooth/hci_core.c
> index 88df159d3393..2663654cdf60 100644
> --- a/net/bluetooth/hci_core.c
> +++ b/net/bluetooth/hci_core.c
> @@ -2911,16 +2911,18 @@ int hci_recv_frame(struct hci_dev *hdev, struct sk_buff *skb)
>                             type == PA_LINK)
>                                 hci_skb_pkt_type(skb) = HCI_ISODATA_PKT;
>                 }
>                 break;
>         case HCI_SCODATA_PKT:
>                 break;
>         case HCI_ISODATA_PKT:
>                 break;
> +       case HCI_VENDOR_PKT:
> +               break;
>         case HCI_DRV_PKT:
>                 break;
>         default:
>                 kfree_skb(skb);
>                 return -EINVAL;
>         }
>
>         /* Incoming skb */
> @@ -3047,16 +3049,50 @@ static int hci_send_frame(struct hci_dev *hdev, struct sk_buff *skb)
>
>  static int hci_send_conn_frame(struct hci_dev *hdev, struct hci_conn *conn,
>                                struct sk_buff *skb)
>  {
>         hci_conn_tx_queue(conn, skb);
>         return hci_send_frame(hdev, skb);
>  }
>
> +/**
> + * hci_send_vendor_frame - Send an HCI_VENDOR_PKT frame to the HCI driver
> + * @hdev: The HCI device
> + * @iter: iov_iter carrying the frame
> + *
> + * Return: 0 on success, or a negative errno on failure.
> + */
> +int hci_send_vendor_frame(struct hci_dev *hdev, struct iov_iter *iter)
> +{
> +       struct sk_buff *skb;
> +       size_t len;
> +
> +       if (WARN_ON(!iov_iter_is_kvec(iter)))
> +               return -EINVAL;
> +
> +       len = iov_iter_count(iter);
> +       if (!len)
> +               return -EINVAL;
> +
> +       skb = bt_skb_alloc(len, GFP_KERNEL);
> +       if (!skb)
> +               return -ENOMEM;
> +
> +       if (!copy_from_iter_full(skb_put(skb, len), len, iter)) {
> +               kfree_skb(skb);
> +               return -EFAULT;
> +       }
> +
> +       hci_skb_pkt_type(skb) = HCI_VENDOR_PKT;
> +
> +       return hci_send_frame(hdev, skb);
> +}
> +EXPORT_SYMBOL(hci_send_vendor_frame);
> +
>  /* Send HCI command */
>  int hci_send_cmd(struct hci_dev *hdev, __u16 opcode, __u32 plen,
>                  const void *param)
>  {
>         struct sk_buff *skb;
>
>         BT_DBG("%s opcode 0x%4.4x plen %d", hdev->name, opcode, plen);
>
> @@ -4051,16 +4087,24 @@ static void hci_rx_work(struct work_struct *work)
>                         hci_scodata_packet(hdev, skb);
>                         break;
>
>                 case HCI_ISODATA_PKT:
>                         BT_DBG("%s ISO data packet", hdev->name);
>                         hci_isodata_packet(hdev, skb);
>                         break;
>
> +               case HCI_VENDOR_PKT:
> +                       BT_DBG("%s Vendor packet", hdev->name);
> +                       if (hdev->recv_vendor_pkt)
> +                               hdev->recv_vendor_pkt(hdev, skb);
> +                       else
> +                               kfree_skb(skb);
> +                       break;
> +
>                 default:
>                         kfree_skb(skb);
>                         break;
>                 }
>         }
>  }
>
>  static int hci_send_cmd_sync(struct hci_dev *hdev, struct sk_buff *skb)
> diff --git a/net/bluetooth/hci_sock.c b/net/bluetooth/hci_sock.c
> index 070ca388f9ac..4b328fa8dff4 100644
> --- a/net/bluetooth/hci_sock.c
> +++ b/net/bluetooth/hci_sock.c
> @@ -224,22 +224,26 @@ void hci_send_to_sock(struct hci_dev *hdev, struct sk_buff *skb)
>                             hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
>                             hci_skb_pkt_type(skb) != HCI_ISODATA_PKT)
>                                 continue;
>                         if (is_filtered_packet(sk, skb))
>                                 continue;
>                 } else if (hci_pi(sk)->channel == HCI_CHANNEL_USER) {
>                         if (!bt_cb(skb)->incoming)
>                                 continue;
> -                       if (hci_skb_pkt_type(skb) != HCI_EVENT_PKT &&
> -                           hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
> -                           hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
> -                           hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
> -                           hci_skb_pkt_type(skb) != HCI_DRV_PKT)
> +                       if (hci_skb_pkt_type(skb) == HCI_VENDOR_PKT) {
> +                               if (!hci_sock_test_flag(sk, HCI_SOCK_RECV_VENDOR_PKT))
> +                                       continue;
> +                       } else if (hci_skb_pkt_type(skb) != HCI_EVENT_PKT &&
> +                                  hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
> +                                  hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
> +                                  hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
> +                                  hci_skb_pkt_type(skb) != HCI_DRV_PKT) {
>                                 continue;
> +                       }
>                 } else {
>                         /* Don't send frame to other channel types */
>                         continue;
>                 }
>
>                 if (!skb_copy) {
>                         /* Create a private copy with headroom */
>                         skb_copy = __pskb_copy_fclone(skb, 1, GFP_ATOMIC, true);
> @@ -385,16 +389,22 @@ void hci_send_to_monitor(struct hci_dev *hdev, struct sk_buff *skb)
>                         opcode = cpu_to_le16(HCI_MON_SCO_TX_PKT);
>                 break;
>         case HCI_ISODATA_PKT:
>                 if (bt_cb(skb)->incoming)
>                         opcode = cpu_to_le16(HCI_MON_ISO_RX_PKT);
>                 else
>                         opcode = cpu_to_le16(HCI_MON_ISO_TX_PKT);
>                 break;
> +       case HCI_VENDOR_PKT:
> +               if (bt_cb(skb)->incoming)
> +                       opcode = cpu_to_le16(HCI_MON_VENDOR_RX_PKT);
> +               else
> +                       opcode = cpu_to_le16(HCI_MON_VENDOR_TX_PKT);
> +               break;
>         case HCI_DRV_PKT:
>                 if (bt_cb(skb)->incoming)
>                         opcode = cpu_to_le16(HCI_MON_DRV_RX_PKT);
>                 else
>                         opcode = cpu_to_le16(HCI_MON_DRV_TX_PKT);
>                 break;
>         case HCI_DIAG_PKT:
>                 opcode = cpu_to_le16(HCI_MON_VENDOR_DIAG);
> @@ -1863,16 +1873,17 @@ static int hci_sock_sendmsg(struct socket *sock, struct msghdr *msg,
>                  * since that gets enforced when binding the socket.
>                  *
>                  * However check that the packet type is valid.
>                  */
>                 if (hci_skb_pkt_type(skb) != HCI_COMMAND_PKT &&
>                     hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
>                     hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
>                     hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
> +                   hci_skb_pkt_type(skb) != HCI_VENDOR_PKT &&
>                     hci_skb_pkt_type(skb) != HCI_DRV_PKT) {
>                         err = -EINVAL;
>                         goto drop;
>                 }
>
>                 skb_queue_tail(&hdev->raw_q, skb);
>                 queue_work(hdev->workqueue, &hdev->tx_work);
>         } else if (hci_skb_pkt_type(skb) == HCI_COMMAND_PKT) {
> @@ -2012,16 +2023,17 @@ static int hci_sock_setsockopt_old(struct socket *sock, int level, int optname,
>         return err;
>  }
>
>  static int hci_sock_setsockopt(struct socket *sock, int level, int optname,
>                                sockptr_t optval, unsigned int optlen)
>  {
>         struct sock *sk = sock->sk;
>         int err = 0;
> +       u32 opt_u32;
>         u16 opt;
>
>         BT_DBG("sk %p, opt %d", sk, optname);
>
>         if (level == SOL_HCI)
>                 return hci_sock_setsockopt_old(sock, level, optname, optval,
>                                                optlen);
>
> @@ -2045,16 +2057,33 @@ static int hci_sock_setsockopt(struct socket *sock, int level, int optname,
>
>                 err = copy_safe_from_sockptr(&opt, sizeof(opt), optval, optlen);
>                 if (err)
>                         break;
>
>                 hci_pi(sk)->mtu = opt;
>                 break;
>
> +       case BT_RECV_VENDOR_PKT:
> +               if (hci_pi(sk)->channel != HCI_CHANNEL_USER) {
> +                       err = -ENOPROTOOPT;
> +                       break;
> +               }
> +
> +               err = copy_safe_from_sockptr(&opt_u32, sizeof(opt_u32),
> +                                            optval, optlen);
> +               if (err)
> +                       break;
> +
> +               if (opt_u32)
> +                       hci_sock_set_flag(sk, HCI_SOCK_RECV_VENDOR_PKT);
> +               else
> +                       hci_sock_clear_flag(sk, HCI_SOCK_RECV_VENDOR_PKT);
> +               break;
> +
>         default:
>                 err = -ENOPROTOOPT;
>                 break;
>         }
>
>  done:
>         release_sock(sk);
>         return err;
> @@ -2127,16 +2156,17 @@ static int hci_sock_getsockopt_old(struct socket *sock, int level, int optname,
>         return err;
>  }
>
>  static int hci_sock_getsockopt(struct socket *sock, int level, int optname,
>                                sockopt_t *sopt)
>  {
>         struct sock *sk = sock->sk;
>         int err = 0;
> +       u32 opt_u32;
>         u16 mtu;
>
>         BT_DBG("sk %p, opt %d", sk, optname);
>
>         if (level == SOL_HCI)
>                 return hci_sock_getsockopt_old(sock, level, optname, sopt);
>
>         if (level != SOL_BLUETOOTH)
> @@ -2148,16 +2178,29 @@ static int hci_sock_getsockopt(struct socket *sock, int level, int optname,
>         case BT_SNDMTU:
>         case BT_RCVMTU:
>                 mtu = hci_pi(sk)->mtu;
>                 if (copy_to_iter(&mtu, sizeof(mtu), &sopt->iter_out) !=
>                     sizeof(mtu))
>                         err = -EFAULT;
>                 break;
>
> +       case BT_RECV_VENDOR_PKT:
> +               if (hci_pi(sk)->channel != HCI_CHANNEL_USER) {
> +                       err = -ENOPROTOOPT;
> +                       break;
> +               }
> +
> +               opt_u32 = hci_sock_test_flag(sk, HCI_SOCK_RECV_VENDOR_PKT) ?
> +                         1 : 0;
> +               if (copy_to_iter(&opt_u32, sizeof(opt_u32),
> +                                &sopt->iter_out) != sizeof(opt_u32))
> +                       err = -EFAULT;
> +               break;
> +
>         default:
>                 err = -ENOPROTOOPT;
>                 break;
>         }
>
>         release_sock(sk);
>         return err;
>  }
>
> --
> 2.34.1
>


-- 
Luiz Augusto von Dentz

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 2/4] Bluetooth: Add generic support for vendor HCI packets
  2026-08-31 14:45   ` Luiz Augusto von Dentz
@ 2026-09-02 10:22     ` Zijun Hu
  0 siblings, 0 replies; 9+ messages in thread
From: Zijun Hu @ 2026-09-02 10:22 UTC (permalink / raw)
  To: Luiz Augusto von Dentz
  Cc: Marcel Holtmann, Zijun Hu, linux-bluetooth, linux-kernel

On 8/31/2026 10:45 PM, Luiz Augusto von Dentz wrote:
>>     Packet type                 BT-HCI indicator   PERI-HCI indicator
>>     -----------------------------------------------------------------
>>     CMD (Host -> Controller)    0x01               0x31
>>     ACL Data (bidirectional)    0x02               0x32
>>     EVENT (Controller -> Host)  0x04               0x34
>>
>> Implement HCI_VENDOR_PKT to generically support vendor HCI packets:
>>
>> - Log them in btmon as they appear on the wire.
>> - Allow userspace to send/receive them over HCI_CHANNEL_USER, gate RX
>>   with the new socket option BT_RECV_VENDOR_PKT, off by default to
>>   avoid regressions for existing applications.
>> - Add hdev->recv_vendor_pkt() to handle them in hci_rx_work().
>> - Add hci_send_vendor_frame() to send an HCI_VENDOR_PKT frame.
> I don't find this one very useful, as btmon already supports handling
> vendor events:
> 
> { 0xff, "Vendor", vendor_evt, 0, false },
> 

This commit actually implements your guidance concluded at 2026-07-20
after discussion:

https://lore.kernel.org/all/CABBYNZL_H2iRAshF-RCX41jbt0jots5i5W=aUUJC_wEN6wzp=w@mail.gmail.com

"I'm fine adding code to the likes of btmon to decode vendor packets, we already have
something similar for Intel although that uses a vendor event not a
vendor packet (both use 0xff, causing the confusion). The user of the
user channel shall be able to read/write starting with 0xff then
decode/encode the next byte as the actual vendor opcode."

Sorry that this patchset comes too late due to refining the last
commit, so it may no longer be top of mind.

vendor_evt() handles HCI_EV_VENDOR, which is very different and
easily distinguished from HCI_VENDOR_PKT, as table below:

+----------------+------------------------+------------------------+
|                | HCI_VENDOR_PKT (0xff)  | HCI_EV_VENDOR (0xff)   |
+----------------+------------------------+------------------------+
| Defined by     | Vendor                 | BT SIG                 |
+----------------+------------------------+------------------------+
| Hierarchy      | Parallel to            | Nested inside          |
|                | HCI_EVENT_PKT(0x04)    | HCI_EVENT_PKT(0x04)    |
+----------------+------------------------+------------------------+
| Direction      | RX and TX              | RX only                |
+----------------+------------------------+------------------------+
| kernel <->     | HCI_VENDOR_PKT, ...    | 0x04, HCI_EV_VENDOR,   |
| userspace      |                        | ...                    |
+----------------+------------------------+------------------------+
| kernel <->     | TX: 22, its payload    | RX: 3, HCI_EV_VENDOR,  |
| monitor        | RX: 23, its payload    | ...                    |
+----------------+------------------------+------------------------+

> This will complicate things because some drivers might label packets
> as vendor while others might inline them as an HCI vendor event. From
> the spec's point of view there is no generic HCI vendor packet, so the
> PERI approach really stands out as a non-compliant solution because it
> uses reserved opcodes.

Vendors sometimes define various HCI_VENDOR_PKTs for their H/W
controller based on their own considerations or requirements, such as
Qualcomm's PERI frames, and BT ACL with vendor-reserved handles, as in
the table below:
+--------+----------+------------------------------------------+
| Vendor | Coredump | Other                                    |
+--------+----------+------------------------------------------+
| QCOM   | 0xEDD    | Enhanced Logging (0xEDC)                 |
+--------+----------+------------------------------------------+
| MTK    | 0xFC6F   | Firmware debug logging (0x05FF, 0x05FE)  |
+--------+----------+------------------------------------------+
| NXP    | 0xFFF    | -                                        |
+--------+----------+------------------------------------------+
These chips have been BT SIG certified and are commercially deployed.

This commit supports them, and also fixes issues similar to what the
commit below fixes for HCI_EV_VENDOR:

0bd606b31d40 ("Bluetooth: hci_event: Introduce handle_ev_vendor()
for HCI_EV_VENDOR")

It is also required infrastructure for supporting Qualcomm's
multi-subsystem chips.

May we address your concern by improving the naming and adding
comments to make each packet type's concept clearer?

May we address your concern by improving the naming and adding
comments to make each packet type's concept clearer in the BlueZ
patchset below:
https://lore.kernel.org/all/20260830-vendor_hci-v2-0-9903760957ab@oss.qualcomm.com

Please take a look when you get a chance, and let me know if you
have any further comments.



^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-09-02 10:22 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31  5:03 [PATCH v2 0/4] Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 Zijun Hu
2026-08-31  5:03 ` [PATCH v2 1/4] Bluetooth: btusb: Add recv_intr() hook to btusb_data Zijun Hu
2026-08-31  6:41   ` Bluetooth: btusb: Support Qualcomm multi-subsystem QCC2072 bluez.test.bot
2026-08-31  5:03 ` [PATCH v2 2/4] Bluetooth: Add generic support for vendor HCI packets Zijun Hu
2026-08-31 14:45   ` Luiz Augusto von Dentz
2026-09-02 10:22     ` Zijun Hu
2026-08-31  5:03 ` [PATCH v2 3/4] Bluetooth: btusb: Build the driver from multiple source files Zijun Hu
2026-08-31  5:03 ` [PATCH v2 4/4] Bluetooth: btusb: Add support for Qualcomm multi-subsystem QCC2072 Zijun Hu
2026-08-31 14:34 ` [PATCH v2 0/4] Bluetooth: btusb: Support " Luiz Augusto von Dentz

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox