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