On WCN3990, we are seeing a rare scenario where copy engine hardware is sending a copy complete interrupt to the host driver while still processing the buffer that the driver has sent, this is leading into an SMMU fault triggering kernel panic. This is happening on copy engine channel 3 (CE3) where the driver normally enqueues WMI commands to the firmware. Upon receiving a copy complete interrupt, host driver will immediately unmap and frees the buffer presuming that hardware has processed the buffer. In the issue case, upon receiving copy complete interrupt, host driver will unmap and free the buffer but since hardware is still accessing the buffer (which in this case got unmapped in parallel), SMMU hardware will trigger an SMMU fault resulting in a kernel panic. In order to avoid this, as a work around, add a delay before unmapping the copy engine source DMA buffer. This is conditionally done for WCN3990 and only for the CE3 channel where issue is seen. Below is the crash signature: wifi smmu error: kernel: [ 10.120965] arm-smmu 15000000.iommu: Unhandled context fault: fsr=0x402, iova=0x7fdfd8ac0, fsynr=0x500003,cbfrsynra=0xc1, cb=6 arm-smmu 15000000.iommu: Unhandled context fault:fsr=0x402, iova=0x7fe06fdc0, fsynr=0x710003, cbfrsynra=0xc1, cb=6 qcom-q6v5-mss 4080000.remoteproc: fatal error received: err_qdi.c:1040:EF:wlan_process:0x1:WLAN RT:0x2091: cmnos_thread.c:3998:Asserted in copy_engine.c:AXI_ERROR_DETECTED:2149 remoteproc remoteproc0: crash detected in 4080000.remoteproc: type fatal error <3> remoteproc remoteproc0: handling crash #1 in 4080000.remoteproc pc : __arm_lpae_unmap+0x500/0x514 lr : __arm_lpae_unmap+0x4bc/0x514 sp : ffffffc011ffb530 x29: ffffffc011ffb590 x28: 0000000000000000 x27: 0000000000000000 x26: 0000000000000004 x25: 0000000000000003 x24: ffffffc011ffb890 x23: ffffffa762ef9be0 x22: ffffffa77244ef00 x21: 0000000000000009 x20: 00000007fff7c000 x19: 0000000000000003 x18: 0000000000000000 x17: 0000000000000004 x16: ffffffd7a357d9f0 x15: 0000000000000000 x14: 00fd5d4fa7ffffff x13: 000000000000000e x12: 0000000000000000 x11: 00000000ffffffff x10: 00000000fffffe00 x9 : 000000000000017c x8 : 000000000000000c x7 : 0000000000000000 x6 : ffffffa762ef9000 x5 : 0000000000000003 x4 : 0000000000000004 x3 : 0000000000001000 x2 : 00000007fff7c000 x1 : ffffffc011ffb890 x0 : 0000000000000000 Call trace: __arm_lpae_unmap+0x500/0x514 __arm_lpae_unmap+0x4bc/0x514 __arm_lpae_unmap+0x4bc/0x514 arm_lpae_unmap_pages+0x78/0xa4 arm_smmu_unmap_pages+0x78/0x104 __iommu_unmap+0xc8/0x1e4 iommu_unmap_fast+0x38/0x48 __iommu_dma_unmap+0x84/0x104 iommu_dma_free+0x34/0x50 dma_free_attrs+0xa4/0xd0 ath10k_htt_rx_free+0xc4/0xf4 [ath10k_core] ath10k_core_stop+0x64/0x7c [ath10k_core] ath10k_halt+0x11c/0x180 [ath10k_core] ath10k_stop+0x54/0x94 [ath10k_core] drv_stop+0x48/0x1c8 [mac80211] ieee80211_do_open+0x638/0x77c [mac80211] ieee80211_open+0x48/0x5c [mac80211] __dev_open+0xb4/0x174 __dev_change_flags+0xc4/0x1dc dev_change_flags+0x3c/0x7c devinet_ioctl+0x2b4/0x580 inet_ioctl+0xb0/0x1b4 sock_do_ioctl+0x4c/0x16c compat_ifreq_ioctl+0x1cc/0x35c compat_sock_ioctl+0x110/0x2ac __arm64_compat_sys_ioctl+0xf4/0x3e0 el0_svc_common+0xb4/0x17c el0_svc_compat_handler+0x2c/0x58 el0_svc_compat+0x8/0x2c Tested-on: WCN3990 hw1.0 SNOC WLAN.HL.2.0-01387-QCAHLSWMTPLZ-1 Tested-by: Douglas Anderson <dianders@chromium.org> Signed-off-by: Youghandhar Chintala <quic_youghand@quicinc.com> Signed-off-by: Kalle Valo <quic_kvalo@quicinc.com> Link: https://lore.kernel.org/r/20221012142733.32420-1-quic_youghand@quicinc.com
1320 lines
34 KiB
C
1320 lines
34 KiB
C
// SPDX-License-Identifier: ISC
|
|
/*
|
|
* Copyright (c) 2005-2011 Atheros Communications Inc.
|
|
* Copyright (c) 2011-2017 Qualcomm Atheros, Inc.
|
|
*/
|
|
|
|
#include "core.h"
|
|
#include "hif.h"
|
|
#include "debug.h"
|
|
|
|
/********/
|
|
/* Send */
|
|
/********/
|
|
|
|
static void ath10k_htc_control_tx_complete(struct ath10k *ar,
|
|
struct sk_buff *skb)
|
|
{
|
|
kfree_skb(skb);
|
|
}
|
|
|
|
static struct sk_buff *ath10k_htc_build_tx_ctrl_skb(void *ar)
|
|
{
|
|
struct sk_buff *skb;
|
|
struct ath10k_skb_cb *skb_cb;
|
|
|
|
skb = dev_alloc_skb(ATH10K_HTC_CONTROL_BUFFER_SIZE);
|
|
if (!skb)
|
|
return NULL;
|
|
|
|
skb_reserve(skb, 20); /* FIXME: why 20 bytes? */
|
|
WARN_ONCE((unsigned long)skb->data & 3, "unaligned skb");
|
|
|
|
skb_cb = ATH10K_SKB_CB(skb);
|
|
memset(skb_cb, 0, sizeof(*skb_cb));
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "%s: skb %pK\n", __func__, skb);
|
|
return skb;
|
|
}
|
|
|
|
static inline void ath10k_htc_restore_tx_skb(struct ath10k_htc *htc,
|
|
struct sk_buff *skb)
|
|
{
|
|
struct ath10k_skb_cb *skb_cb = ATH10K_SKB_CB(skb);
|
|
|
|
if (htc->ar->bus_param.dev_type != ATH10K_DEV_TYPE_HL)
|
|
dma_unmap_single(htc->ar->dev, skb_cb->paddr, skb->len, DMA_TO_DEVICE);
|
|
skb_pull(skb, sizeof(struct ath10k_htc_hdr));
|
|
}
|
|
|
|
void ath10k_htc_notify_tx_completion(struct ath10k_htc_ep *ep,
|
|
struct sk_buff *skb)
|
|
{
|
|
struct ath10k *ar = ep->htc->ar;
|
|
struct ath10k_htc_hdr *hdr;
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "%s: ep %d skb %pK\n", __func__,
|
|
ep->eid, skb);
|
|
|
|
/* A corner case where the copy completion is reaching to host but still
|
|
* copy engine is processing it due to which host unmaps corresponding
|
|
* memory and causes SMMU fault, hence as workaround adding delay
|
|
* the unmapping memory to avoid SMMU faults.
|
|
*/
|
|
if (ar->hw_params.delay_unmap_buffer &&
|
|
ep->ul_pipe_id == 3)
|
|
mdelay(2);
|
|
|
|
hdr = (struct ath10k_htc_hdr *)skb->data;
|
|
ath10k_htc_restore_tx_skb(ep->htc, skb);
|
|
|
|
if (!ep->ep_ops.ep_tx_complete) {
|
|
ath10k_warn(ar, "no tx handler for eid %d\n", ep->eid);
|
|
dev_kfree_skb_any(skb);
|
|
return;
|
|
}
|
|
|
|
if (hdr->flags & ATH10K_HTC_FLAG_SEND_BUNDLE) {
|
|
dev_kfree_skb_any(skb);
|
|
return;
|
|
}
|
|
|
|
ep->ep_ops.ep_tx_complete(ep->htc->ar, skb);
|
|
}
|
|
EXPORT_SYMBOL(ath10k_htc_notify_tx_completion);
|
|
|
|
static void ath10k_htc_prepare_tx_skb(struct ath10k_htc_ep *ep,
|
|
struct sk_buff *skb)
|
|
{
|
|
struct ath10k_htc_hdr *hdr;
|
|
|
|
hdr = (struct ath10k_htc_hdr *)skb->data;
|
|
memset(hdr, 0, sizeof(struct ath10k_htc_hdr));
|
|
|
|
hdr->eid = ep->eid;
|
|
hdr->len = __cpu_to_le16(skb->len - sizeof(*hdr));
|
|
hdr->flags = 0;
|
|
if (ep->tx_credit_flow_enabled && !ep->bundle_tx)
|
|
hdr->flags |= ATH10K_HTC_FLAG_NEED_CREDIT_UPDATE;
|
|
|
|
spin_lock_bh(&ep->htc->tx_lock);
|
|
hdr->seq_no = ep->seq_no++;
|
|
spin_unlock_bh(&ep->htc->tx_lock);
|
|
}
|
|
|
|
static int ath10k_htc_consume_credit(struct ath10k_htc_ep *ep,
|
|
unsigned int len,
|
|
bool consume)
|
|
{
|
|
struct ath10k_htc *htc = ep->htc;
|
|
struct ath10k *ar = htc->ar;
|
|
enum ath10k_htc_ep_id eid = ep->eid;
|
|
int credits, ret = 0;
|
|
|
|
if (!ep->tx_credit_flow_enabled)
|
|
return 0;
|
|
|
|
credits = DIV_ROUND_UP(len, ep->tx_credit_size);
|
|
spin_lock_bh(&htc->tx_lock);
|
|
|
|
if (ep->tx_credits < credits) {
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"htc insufficient credits ep %d required %d available %d consume %d\n",
|
|
eid, credits, ep->tx_credits, consume);
|
|
ret = -EAGAIN;
|
|
goto unlock;
|
|
}
|
|
|
|
if (consume) {
|
|
ep->tx_credits -= credits;
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"htc ep %d consumed %d credits total %d\n",
|
|
eid, credits, ep->tx_credits);
|
|
}
|
|
|
|
unlock:
|
|
spin_unlock_bh(&htc->tx_lock);
|
|
return ret;
|
|
}
|
|
|
|
static void ath10k_htc_release_credit(struct ath10k_htc_ep *ep, unsigned int len)
|
|
{
|
|
struct ath10k_htc *htc = ep->htc;
|
|
struct ath10k *ar = htc->ar;
|
|
enum ath10k_htc_ep_id eid = ep->eid;
|
|
int credits;
|
|
|
|
if (!ep->tx_credit_flow_enabled)
|
|
return;
|
|
|
|
credits = DIV_ROUND_UP(len, ep->tx_credit_size);
|
|
spin_lock_bh(&htc->tx_lock);
|
|
ep->tx_credits += credits;
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"htc ep %d reverted %d credits back total %d\n",
|
|
eid, credits, ep->tx_credits);
|
|
spin_unlock_bh(&htc->tx_lock);
|
|
|
|
if (ep->ep_ops.ep_tx_credits)
|
|
ep->ep_ops.ep_tx_credits(htc->ar);
|
|
}
|
|
|
|
int ath10k_htc_send(struct ath10k_htc *htc,
|
|
enum ath10k_htc_ep_id eid,
|
|
struct sk_buff *skb)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
struct ath10k_htc_ep *ep = &htc->endpoint[eid];
|
|
struct ath10k_skb_cb *skb_cb = ATH10K_SKB_CB(skb);
|
|
struct ath10k_hif_sg_item sg_item;
|
|
struct device *dev = htc->ar->dev;
|
|
int ret;
|
|
unsigned int skb_len;
|
|
|
|
if (htc->ar->state == ATH10K_STATE_WEDGED)
|
|
return -ECOMM;
|
|
|
|
if (eid >= ATH10K_HTC_EP_COUNT) {
|
|
ath10k_warn(ar, "Invalid endpoint id: %d\n", eid);
|
|
return -ENOENT;
|
|
}
|
|
|
|
skb_push(skb, sizeof(struct ath10k_htc_hdr));
|
|
|
|
skb_len = skb->len;
|
|
ret = ath10k_htc_consume_credit(ep, skb_len, true);
|
|
if (ret)
|
|
goto err_pull;
|
|
|
|
ath10k_htc_prepare_tx_skb(ep, skb);
|
|
|
|
skb_cb->eid = eid;
|
|
if (ar->bus_param.dev_type != ATH10K_DEV_TYPE_HL) {
|
|
skb_cb->paddr = dma_map_single(dev, skb->data, skb->len,
|
|
DMA_TO_DEVICE);
|
|
ret = dma_mapping_error(dev, skb_cb->paddr);
|
|
if (ret) {
|
|
ret = -EIO;
|
|
goto err_credits;
|
|
}
|
|
}
|
|
|
|
sg_item.transfer_id = ep->eid;
|
|
sg_item.transfer_context = skb;
|
|
sg_item.vaddr = skb->data;
|
|
sg_item.paddr = skb_cb->paddr;
|
|
sg_item.len = skb->len;
|
|
|
|
ret = ath10k_hif_tx_sg(htc->ar, ep->ul_pipe_id, &sg_item, 1);
|
|
if (ret)
|
|
goto err_unmap;
|
|
|
|
return 0;
|
|
|
|
err_unmap:
|
|
if (ar->bus_param.dev_type != ATH10K_DEV_TYPE_HL)
|
|
dma_unmap_single(dev, skb_cb->paddr, skb->len, DMA_TO_DEVICE);
|
|
err_credits:
|
|
ath10k_htc_release_credit(ep, skb_len);
|
|
err_pull:
|
|
skb_pull(skb, sizeof(struct ath10k_htc_hdr));
|
|
return ret;
|
|
}
|
|
|
|
void ath10k_htc_tx_completion_handler(struct ath10k *ar, struct sk_buff *skb)
|
|
{
|
|
struct ath10k_htc *htc = &ar->htc;
|
|
struct ath10k_skb_cb *skb_cb;
|
|
struct ath10k_htc_ep *ep;
|
|
|
|
if (WARN_ON_ONCE(!skb))
|
|
return;
|
|
|
|
skb_cb = ATH10K_SKB_CB(skb);
|
|
ep = &htc->endpoint[skb_cb->eid];
|
|
|
|
ath10k_htc_notify_tx_completion(ep, skb);
|
|
/* the skb now belongs to the completion handler */
|
|
}
|
|
EXPORT_SYMBOL(ath10k_htc_tx_completion_handler);
|
|
|
|
/***********/
|
|
/* Receive */
|
|
/***********/
|
|
|
|
static void
|
|
ath10k_htc_process_credit_report(struct ath10k_htc *htc,
|
|
const struct ath10k_htc_credit_report *report,
|
|
int len,
|
|
enum ath10k_htc_ep_id eid)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
struct ath10k_htc_ep *ep;
|
|
int i, n_reports;
|
|
|
|
if (len % sizeof(*report))
|
|
ath10k_warn(ar, "Uneven credit report len %d", len);
|
|
|
|
n_reports = len / sizeof(*report);
|
|
|
|
spin_lock_bh(&htc->tx_lock);
|
|
for (i = 0; i < n_reports; i++, report++) {
|
|
if (report->eid >= ATH10K_HTC_EP_COUNT)
|
|
break;
|
|
|
|
ep = &htc->endpoint[report->eid];
|
|
ep->tx_credits += report->credits;
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "htc ep %d got %d credits (total %d)\n",
|
|
report->eid, report->credits, ep->tx_credits);
|
|
|
|
if (ep->ep_ops.ep_tx_credits) {
|
|
spin_unlock_bh(&htc->tx_lock);
|
|
ep->ep_ops.ep_tx_credits(htc->ar);
|
|
spin_lock_bh(&htc->tx_lock);
|
|
}
|
|
}
|
|
spin_unlock_bh(&htc->tx_lock);
|
|
}
|
|
|
|
static int
|
|
ath10k_htc_process_lookahead(struct ath10k_htc *htc,
|
|
const struct ath10k_htc_lookahead_report *report,
|
|
int len,
|
|
enum ath10k_htc_ep_id eid,
|
|
void *next_lookaheads,
|
|
int *next_lookaheads_len)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
|
|
/* Invalid lookahead flags are actually transmitted by
|
|
* the target in the HTC control message.
|
|
* Since this will happen at every boot we silently ignore
|
|
* the lookahead in this case
|
|
*/
|
|
if (report->pre_valid != ((~report->post_valid) & 0xFF))
|
|
return 0;
|
|
|
|
if (next_lookaheads && next_lookaheads_len) {
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"htc rx lookahead found pre_valid 0x%x post_valid 0x%x\n",
|
|
report->pre_valid, report->post_valid);
|
|
|
|
/* look ahead bytes are valid, copy them over */
|
|
memcpy((u8 *)next_lookaheads, report->lookahead, 4);
|
|
|
|
*next_lookaheads_len = 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
ath10k_htc_process_lookahead_bundle(struct ath10k_htc *htc,
|
|
const struct ath10k_htc_lookahead_bundle *report,
|
|
int len,
|
|
enum ath10k_htc_ep_id eid,
|
|
void *next_lookaheads,
|
|
int *next_lookaheads_len)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
int bundle_cnt = len / sizeof(*report);
|
|
|
|
if (!bundle_cnt || (bundle_cnt > htc->max_msgs_per_htc_bundle)) {
|
|
ath10k_warn(ar, "Invalid lookahead bundle count: %d\n",
|
|
bundle_cnt);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (next_lookaheads && next_lookaheads_len) {
|
|
int i;
|
|
|
|
for (i = 0; i < bundle_cnt; i++) {
|
|
memcpy(((u8 *)next_lookaheads) + 4 * i,
|
|
report->lookahead, 4);
|
|
report++;
|
|
}
|
|
|
|
*next_lookaheads_len = bundle_cnt;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int ath10k_htc_process_trailer(struct ath10k_htc *htc,
|
|
u8 *buffer,
|
|
int length,
|
|
enum ath10k_htc_ep_id src_eid,
|
|
void *next_lookaheads,
|
|
int *next_lookaheads_len)
|
|
{
|
|
struct ath10k_htc_lookahead_bundle *bundle;
|
|
struct ath10k *ar = htc->ar;
|
|
int status = 0;
|
|
struct ath10k_htc_record *record;
|
|
u8 *orig_buffer;
|
|
int orig_length;
|
|
size_t len;
|
|
|
|
orig_buffer = buffer;
|
|
orig_length = length;
|
|
|
|
while (length > 0) {
|
|
record = (struct ath10k_htc_record *)buffer;
|
|
|
|
if (length < sizeof(record->hdr)) {
|
|
status = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
if (record->hdr.len > length) {
|
|
/* no room left in buffer for record */
|
|
ath10k_warn(ar, "Invalid record length: %d\n",
|
|
record->hdr.len);
|
|
status = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
switch (record->hdr.id) {
|
|
case ATH10K_HTC_RECORD_CREDITS:
|
|
len = sizeof(struct ath10k_htc_credit_report);
|
|
if (record->hdr.len < len) {
|
|
ath10k_warn(ar, "Credit report too long\n");
|
|
status = -EINVAL;
|
|
break;
|
|
}
|
|
ath10k_htc_process_credit_report(htc,
|
|
record->credit_report,
|
|
record->hdr.len,
|
|
src_eid);
|
|
break;
|
|
case ATH10K_HTC_RECORD_LOOKAHEAD:
|
|
len = sizeof(struct ath10k_htc_lookahead_report);
|
|
if (record->hdr.len < len) {
|
|
ath10k_warn(ar, "Lookahead report too long\n");
|
|
status = -EINVAL;
|
|
break;
|
|
}
|
|
status = ath10k_htc_process_lookahead(htc,
|
|
record->lookahead_report,
|
|
record->hdr.len,
|
|
src_eid,
|
|
next_lookaheads,
|
|
next_lookaheads_len);
|
|
break;
|
|
case ATH10K_HTC_RECORD_LOOKAHEAD_BUNDLE:
|
|
bundle = record->lookahead_bundle;
|
|
status = ath10k_htc_process_lookahead_bundle(htc,
|
|
bundle,
|
|
record->hdr.len,
|
|
src_eid,
|
|
next_lookaheads,
|
|
next_lookaheads_len);
|
|
break;
|
|
default:
|
|
ath10k_warn(ar, "Unhandled record: id:%d length:%d\n",
|
|
record->hdr.id, record->hdr.len);
|
|
break;
|
|
}
|
|
|
|
if (status)
|
|
break;
|
|
|
|
/* multiple records may be present in a trailer */
|
|
buffer += sizeof(record->hdr) + record->hdr.len;
|
|
length -= sizeof(record->hdr) + record->hdr.len;
|
|
}
|
|
|
|
if (status)
|
|
ath10k_dbg_dump(ar, ATH10K_DBG_HTC, "htc rx bad trailer", "",
|
|
orig_buffer, orig_length);
|
|
|
|
return status;
|
|
}
|
|
EXPORT_SYMBOL(ath10k_htc_process_trailer);
|
|
|
|
void ath10k_htc_rx_completion_handler(struct ath10k *ar, struct sk_buff *skb)
|
|
{
|
|
int status = 0;
|
|
struct ath10k_htc *htc = &ar->htc;
|
|
struct ath10k_htc_hdr *hdr;
|
|
struct ath10k_htc_ep *ep;
|
|
u16 payload_len;
|
|
u32 trailer_len = 0;
|
|
size_t min_len;
|
|
u8 eid;
|
|
bool trailer_present;
|
|
|
|
hdr = (struct ath10k_htc_hdr *)skb->data;
|
|
skb_pull(skb, sizeof(*hdr));
|
|
|
|
eid = hdr->eid;
|
|
|
|
if (eid >= ATH10K_HTC_EP_COUNT) {
|
|
ath10k_warn(ar, "HTC Rx: invalid eid %d\n", eid);
|
|
ath10k_dbg_dump(ar, ATH10K_DBG_HTC, "htc bad header", "",
|
|
hdr, sizeof(*hdr));
|
|
goto out;
|
|
}
|
|
|
|
ep = &htc->endpoint[eid];
|
|
if (ep->service_id == ATH10K_HTC_SVC_ID_UNUSED) {
|
|
ath10k_warn(ar, "htc rx endpoint %d is not connected\n", eid);
|
|
goto out;
|
|
}
|
|
|
|
payload_len = __le16_to_cpu(hdr->len);
|
|
|
|
if (payload_len + sizeof(*hdr) > ATH10K_HTC_MAX_LEN) {
|
|
ath10k_warn(ar, "HTC rx frame too long, len: %zu\n",
|
|
payload_len + sizeof(*hdr));
|
|
ath10k_dbg_dump(ar, ATH10K_DBG_HTC, "htc bad rx pkt len", "",
|
|
hdr, sizeof(*hdr));
|
|
goto out;
|
|
}
|
|
|
|
if (skb->len < payload_len) {
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"HTC Rx: insufficient length, got %d, expected %d\n",
|
|
skb->len, payload_len);
|
|
ath10k_dbg_dump(ar, ATH10K_DBG_HTC, "htc bad rx pkt len",
|
|
"", hdr, sizeof(*hdr));
|
|
goto out;
|
|
}
|
|
|
|
/* get flags to check for trailer */
|
|
trailer_present = hdr->flags & ATH10K_HTC_FLAG_TRAILER_PRESENT;
|
|
if (trailer_present) {
|
|
u8 *trailer;
|
|
|
|
trailer_len = hdr->trailer_len;
|
|
min_len = sizeof(struct ath10k_ath10k_htc_record_hdr);
|
|
|
|
if ((trailer_len < min_len) ||
|
|
(trailer_len > payload_len)) {
|
|
ath10k_warn(ar, "Invalid trailer length: %d\n",
|
|
trailer_len);
|
|
goto out;
|
|
}
|
|
|
|
trailer = (u8 *)hdr;
|
|
trailer += sizeof(*hdr);
|
|
trailer += payload_len;
|
|
trailer -= trailer_len;
|
|
status = ath10k_htc_process_trailer(htc, trailer,
|
|
trailer_len, hdr->eid,
|
|
NULL, NULL);
|
|
if (status)
|
|
goto out;
|
|
|
|
skb_trim(skb, skb->len - trailer_len);
|
|
}
|
|
|
|
if (((int)payload_len - (int)trailer_len) <= 0)
|
|
/* zero length packet with trailer data, just drop these */
|
|
goto out;
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "htc rx completion ep %d skb %pK\n",
|
|
eid, skb);
|
|
ep->ep_ops.ep_rx_complete(ar, skb);
|
|
|
|
/* skb is now owned by the rx completion handler */
|
|
skb = NULL;
|
|
out:
|
|
kfree_skb(skb);
|
|
}
|
|
EXPORT_SYMBOL(ath10k_htc_rx_completion_handler);
|
|
|
|
static void ath10k_htc_control_rx_complete(struct ath10k *ar,
|
|
struct sk_buff *skb)
|
|
{
|
|
struct ath10k_htc *htc = &ar->htc;
|
|
struct ath10k_htc_msg *msg = (struct ath10k_htc_msg *)skb->data;
|
|
|
|
switch (__le16_to_cpu(msg->hdr.message_id)) {
|
|
case ATH10K_HTC_MSG_READY_ID:
|
|
case ATH10K_HTC_MSG_CONNECT_SERVICE_RESP_ID:
|
|
/* handle HTC control message */
|
|
if (completion_done(&htc->ctl_resp)) {
|
|
/* this is a fatal error, target should not be
|
|
* sending unsolicited messages on the ep 0
|
|
*/
|
|
ath10k_warn(ar, "HTC rx ctrl still processing\n");
|
|
complete(&htc->ctl_resp);
|
|
goto out;
|
|
}
|
|
|
|
htc->control_resp_len =
|
|
min_t(int, skb->len,
|
|
ATH10K_HTC_MAX_CTRL_MSG_LEN);
|
|
|
|
memcpy(htc->control_resp_buffer, skb->data,
|
|
htc->control_resp_len);
|
|
|
|
complete(&htc->ctl_resp);
|
|
break;
|
|
case ATH10K_HTC_MSG_SEND_SUSPEND_COMPLETE:
|
|
htc->htc_ops.target_send_suspend_complete(ar);
|
|
break;
|
|
default:
|
|
ath10k_warn(ar, "ignoring unsolicited htc ep0 event\n");
|
|
break;
|
|
}
|
|
|
|
out:
|
|
kfree_skb(skb);
|
|
}
|
|
|
|
/***************/
|
|
/* Init/Deinit */
|
|
/***************/
|
|
|
|
static const char *htc_service_name(enum ath10k_htc_svc_id id)
|
|
{
|
|
switch (id) {
|
|
case ATH10K_HTC_SVC_ID_RESERVED:
|
|
return "Reserved";
|
|
case ATH10K_HTC_SVC_ID_RSVD_CTRL:
|
|
return "Control";
|
|
case ATH10K_HTC_SVC_ID_WMI_CONTROL:
|
|
return "WMI";
|
|
case ATH10K_HTC_SVC_ID_WMI_DATA_BE:
|
|
return "DATA BE";
|
|
case ATH10K_HTC_SVC_ID_WMI_DATA_BK:
|
|
return "DATA BK";
|
|
case ATH10K_HTC_SVC_ID_WMI_DATA_VI:
|
|
return "DATA VI";
|
|
case ATH10K_HTC_SVC_ID_WMI_DATA_VO:
|
|
return "DATA VO";
|
|
case ATH10K_HTC_SVC_ID_NMI_CONTROL:
|
|
return "NMI Control";
|
|
case ATH10K_HTC_SVC_ID_NMI_DATA:
|
|
return "NMI Data";
|
|
case ATH10K_HTC_SVC_ID_HTT_DATA_MSG:
|
|
return "HTT Data";
|
|
case ATH10K_HTC_SVC_ID_HTT_DATA2_MSG:
|
|
return "HTT Data";
|
|
case ATH10K_HTC_SVC_ID_HTT_DATA3_MSG:
|
|
return "HTT Data";
|
|
case ATH10K_HTC_SVC_ID_TEST_RAW_STREAMS:
|
|
return "RAW";
|
|
case ATH10K_HTC_SVC_ID_HTT_LOG_MSG:
|
|
return "PKTLOG";
|
|
}
|
|
|
|
return "Unknown";
|
|
}
|
|
|
|
static void ath10k_htc_reset_endpoint_states(struct ath10k_htc *htc)
|
|
{
|
|
struct ath10k_htc_ep *ep;
|
|
int i;
|
|
|
|
for (i = ATH10K_HTC_EP_0; i < ATH10K_HTC_EP_COUNT; i++) {
|
|
ep = &htc->endpoint[i];
|
|
ep->service_id = ATH10K_HTC_SVC_ID_UNUSED;
|
|
ep->max_ep_message_len = 0;
|
|
ep->max_tx_queue_depth = 0;
|
|
ep->eid = i;
|
|
ep->htc = htc;
|
|
ep->tx_credit_flow_enabled = true;
|
|
}
|
|
}
|
|
|
|
static u8 ath10k_htc_get_credit_allocation(struct ath10k_htc *htc,
|
|
u16 service_id)
|
|
{
|
|
u8 allocation = 0;
|
|
|
|
/* The WMI control service is the only service with flow control.
|
|
* Let it have all transmit credits.
|
|
*/
|
|
if (service_id == ATH10K_HTC_SVC_ID_WMI_CONTROL)
|
|
allocation = htc->total_transmit_credits;
|
|
|
|
return allocation;
|
|
}
|
|
|
|
static int ath10k_htc_send_bundle(struct ath10k_htc_ep *ep,
|
|
struct sk_buff *bundle_skb,
|
|
struct sk_buff_head *tx_save_head)
|
|
{
|
|
struct ath10k_hif_sg_item sg_item;
|
|
struct ath10k_htc *htc = ep->htc;
|
|
struct ath10k *ar = htc->ar;
|
|
struct sk_buff *skb;
|
|
int ret, cn = 0;
|
|
unsigned int skb_len;
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "bundle skb len %d\n", bundle_skb->len);
|
|
skb_len = bundle_skb->len;
|
|
ret = ath10k_htc_consume_credit(ep, skb_len, true);
|
|
|
|
if (!ret) {
|
|
sg_item.transfer_id = ep->eid;
|
|
sg_item.transfer_context = bundle_skb;
|
|
sg_item.vaddr = bundle_skb->data;
|
|
sg_item.len = bundle_skb->len;
|
|
|
|
ret = ath10k_hif_tx_sg(htc->ar, ep->ul_pipe_id, &sg_item, 1);
|
|
if (ret)
|
|
ath10k_htc_release_credit(ep, skb_len);
|
|
}
|
|
|
|
if (ret)
|
|
dev_kfree_skb_any(bundle_skb);
|
|
|
|
for (cn = 0; (skb = skb_dequeue_tail(tx_save_head)); cn++) {
|
|
if (ret) {
|
|
skb_pull(skb, sizeof(struct ath10k_htc_hdr));
|
|
skb_queue_head(&ep->tx_req_head, skb);
|
|
} else {
|
|
skb_queue_tail(&ep->tx_complete_head, skb);
|
|
}
|
|
}
|
|
|
|
if (!ret)
|
|
queue_work(ar->workqueue_tx_complete, &ar->tx_complete_work);
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"bundle tx status %d eid %d req count %d count %d len %d\n",
|
|
ret, ep->eid, skb_queue_len(&ep->tx_req_head), cn, skb_len);
|
|
return ret;
|
|
}
|
|
|
|
static void ath10k_htc_send_one_skb(struct ath10k_htc_ep *ep, struct sk_buff *skb)
|
|
{
|
|
struct ath10k_htc *htc = ep->htc;
|
|
struct ath10k *ar = htc->ar;
|
|
int ret;
|
|
|
|
ret = ath10k_htc_send(htc, ep->eid, skb);
|
|
|
|
if (ret)
|
|
skb_queue_head(&ep->tx_req_head, skb);
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "tx one status %d eid %d len %d pending count %d\n",
|
|
ret, ep->eid, skb->len, skb_queue_len(&ep->tx_req_head));
|
|
}
|
|
|
|
static int ath10k_htc_send_bundle_skbs(struct ath10k_htc_ep *ep)
|
|
{
|
|
struct ath10k_htc *htc = ep->htc;
|
|
struct sk_buff *bundle_skb, *skb;
|
|
struct sk_buff_head tx_save_head;
|
|
struct ath10k_htc_hdr *hdr;
|
|
u8 *bundle_buf;
|
|
int ret = 0, credit_pad, credit_remainder, trans_len, bundles_left = 0;
|
|
|
|
if (htc->ar->state == ATH10K_STATE_WEDGED)
|
|
return -ECOMM;
|
|
|
|
if (ep->tx_credit_flow_enabled &&
|
|
ep->tx_credits < ATH10K_MIN_CREDIT_PER_HTC_TX_BUNDLE)
|
|
return 0;
|
|
|
|
bundles_left = ATH10K_MAX_MSG_PER_HTC_TX_BUNDLE * ep->tx_credit_size;
|
|
bundle_skb = dev_alloc_skb(bundles_left);
|
|
|
|
if (!bundle_skb)
|
|
return -ENOMEM;
|
|
|
|
bundle_buf = bundle_skb->data;
|
|
skb_queue_head_init(&tx_save_head);
|
|
|
|
while (true) {
|
|
skb = skb_dequeue(&ep->tx_req_head);
|
|
if (!skb)
|
|
break;
|
|
|
|
credit_pad = 0;
|
|
trans_len = skb->len + sizeof(*hdr);
|
|
credit_remainder = trans_len % ep->tx_credit_size;
|
|
|
|
if (credit_remainder != 0) {
|
|
credit_pad = ep->tx_credit_size - credit_remainder;
|
|
trans_len += credit_pad;
|
|
}
|
|
|
|
ret = ath10k_htc_consume_credit(ep,
|
|
bundle_buf + trans_len - bundle_skb->data,
|
|
false);
|
|
if (ret) {
|
|
skb_queue_head(&ep->tx_req_head, skb);
|
|
break;
|
|
}
|
|
|
|
if (bundles_left < trans_len) {
|
|
bundle_skb->len = bundle_buf - bundle_skb->data;
|
|
ret = ath10k_htc_send_bundle(ep, bundle_skb, &tx_save_head);
|
|
|
|
if (ret) {
|
|
skb_queue_head(&ep->tx_req_head, skb);
|
|
return ret;
|
|
}
|
|
|
|
if (skb_queue_len(&ep->tx_req_head) == 0) {
|
|
ath10k_htc_send_one_skb(ep, skb);
|
|
return ret;
|
|
}
|
|
|
|
if (ep->tx_credit_flow_enabled &&
|
|
ep->tx_credits < ATH10K_MIN_CREDIT_PER_HTC_TX_BUNDLE) {
|
|
skb_queue_head(&ep->tx_req_head, skb);
|
|
return 0;
|
|
}
|
|
|
|
bundles_left =
|
|
ATH10K_MAX_MSG_PER_HTC_TX_BUNDLE * ep->tx_credit_size;
|
|
bundle_skb = dev_alloc_skb(bundles_left);
|
|
|
|
if (!bundle_skb) {
|
|
skb_queue_head(&ep->tx_req_head, skb);
|
|
return -ENOMEM;
|
|
}
|
|
bundle_buf = bundle_skb->data;
|
|
skb_queue_head_init(&tx_save_head);
|
|
}
|
|
|
|
skb_push(skb, sizeof(struct ath10k_htc_hdr));
|
|
ath10k_htc_prepare_tx_skb(ep, skb);
|
|
|
|
memcpy(bundle_buf, skb->data, skb->len);
|
|
hdr = (struct ath10k_htc_hdr *)bundle_buf;
|
|
hdr->flags |= ATH10K_HTC_FLAG_SEND_BUNDLE;
|
|
hdr->pad_len = __cpu_to_le16(credit_pad);
|
|
bundle_buf += trans_len;
|
|
bundles_left -= trans_len;
|
|
skb_queue_tail(&tx_save_head, skb);
|
|
}
|
|
|
|
if (bundle_buf != bundle_skb->data) {
|
|
bundle_skb->len = bundle_buf - bundle_skb->data;
|
|
ret = ath10k_htc_send_bundle(ep, bundle_skb, &tx_save_head);
|
|
} else {
|
|
dev_kfree_skb_any(bundle_skb);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void ath10k_htc_bundle_tx_work(struct work_struct *work)
|
|
{
|
|
struct ath10k *ar = container_of(work, struct ath10k, bundle_tx_work);
|
|
struct ath10k_htc_ep *ep;
|
|
struct sk_buff *skb;
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ar->htc.endpoint); i++) {
|
|
ep = &ar->htc.endpoint[i];
|
|
|
|
if (!ep->bundle_tx)
|
|
continue;
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "bundle tx work eid %d count %d\n",
|
|
ep->eid, skb_queue_len(&ep->tx_req_head));
|
|
|
|
if (skb_queue_len(&ep->tx_req_head) >=
|
|
ATH10K_MIN_MSG_PER_HTC_TX_BUNDLE) {
|
|
ath10k_htc_send_bundle_skbs(ep);
|
|
} else {
|
|
skb = skb_dequeue(&ep->tx_req_head);
|
|
|
|
if (!skb)
|
|
continue;
|
|
ath10k_htc_send_one_skb(ep, skb);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ath10k_htc_tx_complete_work(struct work_struct *work)
|
|
{
|
|
struct ath10k *ar = container_of(work, struct ath10k, tx_complete_work);
|
|
struct ath10k_htc_ep *ep;
|
|
enum ath10k_htc_ep_id eid;
|
|
struct sk_buff *skb;
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ar->htc.endpoint); i++) {
|
|
ep = &ar->htc.endpoint[i];
|
|
eid = ep->eid;
|
|
if (ep->bundle_tx && eid == ar->htt.eid) {
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "bundle tx complete eid %d pending complete count%d\n",
|
|
ep->eid, skb_queue_len(&ep->tx_complete_head));
|
|
|
|
while (true) {
|
|
skb = skb_dequeue(&ep->tx_complete_head);
|
|
if (!skb)
|
|
break;
|
|
ath10k_htc_notify_tx_completion(ep, skb);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int ath10k_htc_send_hl(struct ath10k_htc *htc,
|
|
enum ath10k_htc_ep_id eid,
|
|
struct sk_buff *skb)
|
|
{
|
|
struct ath10k_htc_ep *ep = &htc->endpoint[eid];
|
|
struct ath10k *ar = htc->ar;
|
|
|
|
if (sizeof(struct ath10k_htc_hdr) + skb->len > ep->tx_credit_size) {
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "tx exceed max len %d\n", skb->len);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "htc send hl eid %d bundle %d tx count %d len %d\n",
|
|
eid, ep->bundle_tx, skb_queue_len(&ep->tx_req_head), skb->len);
|
|
|
|
if (ep->bundle_tx) {
|
|
skb_queue_tail(&ep->tx_req_head, skb);
|
|
queue_work(ar->workqueue, &ar->bundle_tx_work);
|
|
return 0;
|
|
} else {
|
|
return ath10k_htc_send(htc, eid, skb);
|
|
}
|
|
}
|
|
|
|
void ath10k_htc_setup_tx_req(struct ath10k_htc_ep *ep)
|
|
{
|
|
if (ep->htc->max_msgs_per_htc_bundle >= ATH10K_MIN_MSG_PER_HTC_TX_BUNDLE &&
|
|
!ep->bundle_tx) {
|
|
ep->bundle_tx = true;
|
|
skb_queue_head_init(&ep->tx_req_head);
|
|
skb_queue_head_init(&ep->tx_complete_head);
|
|
}
|
|
}
|
|
|
|
void ath10k_htc_stop_hl(struct ath10k *ar)
|
|
{
|
|
struct ath10k_htc_ep *ep;
|
|
int i;
|
|
|
|
cancel_work_sync(&ar->bundle_tx_work);
|
|
cancel_work_sync(&ar->tx_complete_work);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ar->htc.endpoint); i++) {
|
|
ep = &ar->htc.endpoint[i];
|
|
|
|
if (!ep->bundle_tx)
|
|
continue;
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "stop tx work eid %d count %d\n",
|
|
ep->eid, skb_queue_len(&ep->tx_req_head));
|
|
|
|
skb_queue_purge(&ep->tx_req_head);
|
|
}
|
|
}
|
|
|
|
int ath10k_htc_wait_target(struct ath10k_htc *htc)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
int i, status = 0;
|
|
unsigned long time_left;
|
|
struct ath10k_htc_msg *msg;
|
|
u16 message_id;
|
|
|
|
time_left = wait_for_completion_timeout(&htc->ctl_resp,
|
|
ATH10K_HTC_WAIT_TIMEOUT_HZ);
|
|
if (!time_left) {
|
|
/* Workaround: In some cases the PCI HIF doesn't
|
|
* receive interrupt for the control response message
|
|
* even if the buffer was completed. It is suspected
|
|
* iomap writes unmasking PCI CE irqs aren't propagated
|
|
* properly in KVM PCI-passthrough sometimes.
|
|
*/
|
|
ath10k_warn(ar, "failed to receive control response completion, polling..\n");
|
|
|
|
for (i = 0; i < CE_COUNT; i++)
|
|
ath10k_hif_send_complete_check(htc->ar, i, 1);
|
|
|
|
time_left =
|
|
wait_for_completion_timeout(&htc->ctl_resp,
|
|
ATH10K_HTC_WAIT_TIMEOUT_HZ);
|
|
|
|
if (!time_left)
|
|
status = -ETIMEDOUT;
|
|
}
|
|
|
|
if (status < 0) {
|
|
ath10k_err(ar, "ctl_resp never came in (%d)\n", status);
|
|
return status;
|
|
}
|
|
|
|
if (htc->control_resp_len < sizeof(msg->hdr) + sizeof(msg->ready)) {
|
|
ath10k_err(ar, "Invalid HTC ready msg len:%d\n",
|
|
htc->control_resp_len);
|
|
return -ECOMM;
|
|
}
|
|
|
|
msg = (struct ath10k_htc_msg *)htc->control_resp_buffer;
|
|
message_id = __le16_to_cpu(msg->hdr.message_id);
|
|
|
|
if (message_id != ATH10K_HTC_MSG_READY_ID) {
|
|
ath10k_err(ar, "Invalid HTC ready msg: 0x%x\n", message_id);
|
|
return -ECOMM;
|
|
}
|
|
|
|
if (ar->hw_params.use_fw_tx_credits)
|
|
htc->total_transmit_credits = __le16_to_cpu(msg->ready.credit_count);
|
|
else
|
|
htc->total_transmit_credits = 1;
|
|
|
|
htc->target_credit_size = __le16_to_cpu(msg->ready.credit_size);
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"Target ready! transmit resources: %d size:%d actual credits:%d\n",
|
|
htc->total_transmit_credits,
|
|
htc->target_credit_size,
|
|
msg->ready.credit_count);
|
|
|
|
if ((htc->total_transmit_credits == 0) ||
|
|
(htc->target_credit_size == 0)) {
|
|
ath10k_err(ar, "Invalid credit size received\n");
|
|
return -ECOMM;
|
|
}
|
|
|
|
/* The only way to determine if the ready message is an extended
|
|
* message is from the size.
|
|
*/
|
|
if (htc->control_resp_len >=
|
|
sizeof(msg->hdr) + sizeof(msg->ready_ext)) {
|
|
htc->alt_data_credit_size =
|
|
__le16_to_cpu(msg->ready_ext.reserved) &
|
|
ATH10K_HTC_MSG_READY_EXT_ALT_DATA_MASK;
|
|
htc->max_msgs_per_htc_bundle =
|
|
min_t(u8, msg->ready_ext.max_msgs_per_htc_bundle,
|
|
HTC_HOST_MAX_MSG_PER_RX_BUNDLE);
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"Extended ready message RX bundle size %d alt size %d\n",
|
|
htc->max_msgs_per_htc_bundle,
|
|
htc->alt_data_credit_size);
|
|
}
|
|
|
|
INIT_WORK(&ar->bundle_tx_work, ath10k_htc_bundle_tx_work);
|
|
INIT_WORK(&ar->tx_complete_work, ath10k_htc_tx_complete_work);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void ath10k_htc_change_tx_credit_flow(struct ath10k_htc *htc,
|
|
enum ath10k_htc_ep_id eid,
|
|
bool enable)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
struct ath10k_htc_ep *ep = &ar->htc.endpoint[eid];
|
|
|
|
ep->tx_credit_flow_enabled = enable;
|
|
}
|
|
|
|
int ath10k_htc_connect_service(struct ath10k_htc *htc,
|
|
struct ath10k_htc_svc_conn_req *conn_req,
|
|
struct ath10k_htc_svc_conn_resp *conn_resp)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
struct ath10k_htc_msg *msg;
|
|
struct ath10k_htc_conn_svc *req_msg;
|
|
struct ath10k_htc_conn_svc_response resp_msg_dummy;
|
|
struct ath10k_htc_conn_svc_response *resp_msg = &resp_msg_dummy;
|
|
enum ath10k_htc_ep_id assigned_eid = ATH10K_HTC_EP_COUNT;
|
|
struct ath10k_htc_ep *ep;
|
|
struct sk_buff *skb;
|
|
unsigned int max_msg_size = 0;
|
|
int length, status;
|
|
unsigned long time_left;
|
|
bool disable_credit_flow_ctrl = false;
|
|
u16 message_id, service_id, flags = 0;
|
|
u8 tx_alloc = 0;
|
|
|
|
/* special case for HTC pseudo control service */
|
|
if (conn_req->service_id == ATH10K_HTC_SVC_ID_RSVD_CTRL) {
|
|
disable_credit_flow_ctrl = true;
|
|
assigned_eid = ATH10K_HTC_EP_0;
|
|
max_msg_size = ATH10K_HTC_MAX_CTRL_MSG_LEN;
|
|
memset(&resp_msg_dummy, 0, sizeof(resp_msg_dummy));
|
|
goto setup;
|
|
}
|
|
|
|
tx_alloc = ath10k_htc_get_credit_allocation(htc,
|
|
conn_req->service_id);
|
|
if (!tx_alloc)
|
|
ath10k_dbg(ar, ATH10K_DBG_BOOT,
|
|
"boot htc service %s does not allocate target credits\n",
|
|
htc_service_name(conn_req->service_id));
|
|
|
|
skb = ath10k_htc_build_tx_ctrl_skb(htc->ar);
|
|
if (!skb) {
|
|
ath10k_err(ar, "Failed to allocate HTC packet\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
length = sizeof(msg->hdr) + sizeof(msg->connect_service);
|
|
skb_put(skb, length);
|
|
memset(skb->data, 0, length);
|
|
|
|
msg = (struct ath10k_htc_msg *)skb->data;
|
|
msg->hdr.message_id =
|
|
__cpu_to_le16(ATH10K_HTC_MSG_CONNECT_SERVICE_ID);
|
|
|
|
flags |= SM(tx_alloc, ATH10K_HTC_CONN_FLAGS_RECV_ALLOC);
|
|
|
|
/* Only enable credit flow control for WMI ctrl service */
|
|
if (conn_req->service_id != ATH10K_HTC_SVC_ID_WMI_CONTROL) {
|
|
flags |= ATH10K_HTC_CONN_FLAGS_DISABLE_CREDIT_FLOW_CTRL;
|
|
disable_credit_flow_ctrl = true;
|
|
}
|
|
|
|
req_msg = &msg->connect_service;
|
|
req_msg->flags = __cpu_to_le16(flags);
|
|
req_msg->service_id = __cpu_to_le16(conn_req->service_id);
|
|
|
|
reinit_completion(&htc->ctl_resp);
|
|
|
|
status = ath10k_htc_send(htc, ATH10K_HTC_EP_0, skb);
|
|
if (status) {
|
|
kfree_skb(skb);
|
|
return status;
|
|
}
|
|
|
|
/* wait for response */
|
|
time_left = wait_for_completion_timeout(&htc->ctl_resp,
|
|
ATH10K_HTC_CONN_SVC_TIMEOUT_HZ);
|
|
if (!time_left) {
|
|
ath10k_err(ar, "Service connect timeout\n");
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
/* we controlled the buffer creation, it's aligned */
|
|
msg = (struct ath10k_htc_msg *)htc->control_resp_buffer;
|
|
resp_msg = &msg->connect_service_response;
|
|
message_id = __le16_to_cpu(msg->hdr.message_id);
|
|
service_id = __le16_to_cpu(resp_msg->service_id);
|
|
|
|
if ((message_id != ATH10K_HTC_MSG_CONNECT_SERVICE_RESP_ID) ||
|
|
(htc->control_resp_len < sizeof(msg->hdr) +
|
|
sizeof(msg->connect_service_response))) {
|
|
ath10k_err(ar, "Invalid resp message ID 0x%x", message_id);
|
|
return -EPROTO;
|
|
}
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC,
|
|
"HTC Service %s connect response: status: 0x%x, assigned ep: 0x%x\n",
|
|
htc_service_name(service_id),
|
|
resp_msg->status, resp_msg->eid);
|
|
|
|
conn_resp->connect_resp_code = resp_msg->status;
|
|
|
|
/* check response status */
|
|
if (resp_msg->status != ATH10K_HTC_CONN_SVC_STATUS_SUCCESS) {
|
|
ath10k_err(ar, "HTC Service %s connect request failed: 0x%x)\n",
|
|
htc_service_name(service_id),
|
|
resp_msg->status);
|
|
return -EPROTO;
|
|
}
|
|
|
|
assigned_eid = (enum ath10k_htc_ep_id)resp_msg->eid;
|
|
max_msg_size = __le16_to_cpu(resp_msg->max_msg_size);
|
|
|
|
setup:
|
|
|
|
if (assigned_eid >= ATH10K_HTC_EP_COUNT)
|
|
return -EPROTO;
|
|
|
|
if (max_msg_size == 0)
|
|
return -EPROTO;
|
|
|
|
ep = &htc->endpoint[assigned_eid];
|
|
ep->eid = assigned_eid;
|
|
|
|
if (ep->service_id != ATH10K_HTC_SVC_ID_UNUSED)
|
|
return -EPROTO;
|
|
|
|
/* return assigned endpoint to caller */
|
|
conn_resp->eid = assigned_eid;
|
|
conn_resp->max_msg_len = __le16_to_cpu(resp_msg->max_msg_size);
|
|
|
|
/* setup the endpoint */
|
|
ep->service_id = conn_req->service_id;
|
|
ep->max_tx_queue_depth = conn_req->max_send_queue_depth;
|
|
ep->max_ep_message_len = __le16_to_cpu(resp_msg->max_msg_size);
|
|
ep->tx_credits = tx_alloc;
|
|
ep->tx_credit_size = htc->target_credit_size;
|
|
|
|
if (conn_req->service_id == ATH10K_HTC_SVC_ID_HTT_DATA_MSG &&
|
|
htc->alt_data_credit_size != 0)
|
|
ep->tx_credit_size = htc->alt_data_credit_size;
|
|
|
|
/* copy all the callbacks */
|
|
ep->ep_ops = conn_req->ep_ops;
|
|
|
|
status = ath10k_hif_map_service_to_pipe(htc->ar,
|
|
ep->service_id,
|
|
&ep->ul_pipe_id,
|
|
&ep->dl_pipe_id);
|
|
if (status) {
|
|
ath10k_dbg(ar, ATH10K_DBG_BOOT, "unsupported HTC service id: %d\n",
|
|
ep->service_id);
|
|
return status;
|
|
}
|
|
|
|
ath10k_dbg(ar, ATH10K_DBG_BOOT,
|
|
"boot htc service '%s' ul pipe %d dl pipe %d eid %d ready\n",
|
|
htc_service_name(ep->service_id), ep->ul_pipe_id,
|
|
ep->dl_pipe_id, ep->eid);
|
|
|
|
if (disable_credit_flow_ctrl && ep->tx_credit_flow_enabled) {
|
|
ep->tx_credit_flow_enabled = false;
|
|
ath10k_dbg(ar, ATH10K_DBG_BOOT,
|
|
"boot htc service '%s' eid %d TX flow control disabled\n",
|
|
htc_service_name(ep->service_id), assigned_eid);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
struct sk_buff *ath10k_htc_alloc_skb(struct ath10k *ar, int size)
|
|
{
|
|
struct sk_buff *skb;
|
|
|
|
skb = dev_alloc_skb(size + sizeof(struct ath10k_htc_hdr));
|
|
if (!skb)
|
|
return NULL;
|
|
|
|
skb_reserve(skb, sizeof(struct ath10k_htc_hdr));
|
|
|
|
/* FW/HTC requires 4-byte aligned streams */
|
|
if (!IS_ALIGNED((unsigned long)skb->data, 4))
|
|
ath10k_warn(ar, "Unaligned HTC tx skb\n");
|
|
|
|
return skb;
|
|
}
|
|
|
|
static void ath10k_htc_pktlog_process_rx(struct ath10k *ar, struct sk_buff *skb)
|
|
{
|
|
trace_ath10k_htt_pktlog(ar, skb->data, skb->len);
|
|
dev_kfree_skb_any(skb);
|
|
}
|
|
|
|
static int ath10k_htc_pktlog_connect(struct ath10k *ar)
|
|
{
|
|
struct ath10k_htc_svc_conn_resp conn_resp;
|
|
struct ath10k_htc_svc_conn_req conn_req;
|
|
int status;
|
|
|
|
memset(&conn_req, 0, sizeof(conn_req));
|
|
memset(&conn_resp, 0, sizeof(conn_resp));
|
|
|
|
conn_req.ep_ops.ep_tx_complete = NULL;
|
|
conn_req.ep_ops.ep_rx_complete = ath10k_htc_pktlog_process_rx;
|
|
conn_req.ep_ops.ep_tx_credits = NULL;
|
|
|
|
/* connect to control service */
|
|
conn_req.service_id = ATH10K_HTC_SVC_ID_HTT_LOG_MSG;
|
|
status = ath10k_htc_connect_service(&ar->htc, &conn_req, &conn_resp);
|
|
if (status) {
|
|
ath10k_warn(ar, "failed to connect to PKTLOG service: %d\n",
|
|
status);
|
|
return status;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool ath10k_htc_pktlog_svc_supported(struct ath10k *ar)
|
|
{
|
|
u8 ul_pipe_id;
|
|
u8 dl_pipe_id;
|
|
int status;
|
|
|
|
status = ath10k_hif_map_service_to_pipe(ar, ATH10K_HTC_SVC_ID_HTT_LOG_MSG,
|
|
&ul_pipe_id,
|
|
&dl_pipe_id);
|
|
if (status) {
|
|
ath10k_dbg(ar, ATH10K_DBG_BOOT, "unsupported HTC pktlog service id: %d\n",
|
|
ATH10K_HTC_SVC_ID_HTT_LOG_MSG);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int ath10k_htc_start(struct ath10k_htc *htc)
|
|
{
|
|
struct ath10k *ar = htc->ar;
|
|
struct sk_buff *skb;
|
|
int status = 0;
|
|
struct ath10k_htc_msg *msg;
|
|
|
|
skb = ath10k_htc_build_tx_ctrl_skb(htc->ar);
|
|
if (!skb)
|
|
return -ENOMEM;
|
|
|
|
skb_put(skb, sizeof(msg->hdr) + sizeof(msg->setup_complete_ext));
|
|
memset(skb->data, 0, skb->len);
|
|
|
|
msg = (struct ath10k_htc_msg *)skb->data;
|
|
msg->hdr.message_id =
|
|
__cpu_to_le16(ATH10K_HTC_MSG_SETUP_COMPLETE_EX_ID);
|
|
|
|
if (ar->hif.bus == ATH10K_BUS_SDIO) {
|
|
/* Extra setup params used by SDIO */
|
|
msg->setup_complete_ext.flags =
|
|
__cpu_to_le32(ATH10K_HTC_SETUP_COMPLETE_FLAGS_RX_BNDL_EN);
|
|
msg->setup_complete_ext.max_msgs_per_bundled_recv =
|
|
htc->max_msgs_per_htc_bundle;
|
|
}
|
|
ath10k_dbg(ar, ATH10K_DBG_HTC, "HTC is using TX credit flow control\n");
|
|
|
|
status = ath10k_htc_send(htc, ATH10K_HTC_EP_0, skb);
|
|
if (status) {
|
|
kfree_skb(skb);
|
|
return status;
|
|
}
|
|
|
|
if (ath10k_htc_pktlog_svc_supported(ar)) {
|
|
status = ath10k_htc_pktlog_connect(ar);
|
|
if (status) {
|
|
ath10k_err(ar, "failed to connect to pktlog: %d\n", status);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* registered target arrival callback from the HIF layer */
|
|
int ath10k_htc_init(struct ath10k *ar)
|
|
{
|
|
int status;
|
|
struct ath10k_htc *htc = &ar->htc;
|
|
struct ath10k_htc_svc_conn_req conn_req;
|
|
struct ath10k_htc_svc_conn_resp conn_resp;
|
|
|
|
spin_lock_init(&htc->tx_lock);
|
|
|
|
ath10k_htc_reset_endpoint_states(htc);
|
|
|
|
htc->ar = ar;
|
|
|
|
/* setup our pseudo HTC control endpoint connection */
|
|
memset(&conn_req, 0, sizeof(conn_req));
|
|
memset(&conn_resp, 0, sizeof(conn_resp));
|
|
conn_req.ep_ops.ep_tx_complete = ath10k_htc_control_tx_complete;
|
|
conn_req.ep_ops.ep_rx_complete = ath10k_htc_control_rx_complete;
|
|
conn_req.max_send_queue_depth = ATH10K_NUM_CONTROL_TX_BUFFERS;
|
|
conn_req.service_id = ATH10K_HTC_SVC_ID_RSVD_CTRL;
|
|
|
|
/* connect fake service */
|
|
status = ath10k_htc_connect_service(htc, &conn_req, &conn_resp);
|
|
if (status) {
|
|
ath10k_err(ar, "could not connect to htc service (%d)\n",
|
|
status);
|
|
return status;
|
|
}
|
|
|
|
init_completion(&htc->ctl_resp);
|
|
|
|
return 0;
|
|
}
|