Michal Kazior michal.kazior at tieto.com
Wed Apr 9 06:48:48 EDT 2014

Each WMI management Tx consumes 1 HTC Tx credit
and doesn't replenish it until the frame is
actually transmitted out of firmware's Tx queues.

If associated client was asleep and has gone out
of range then unicast frames won't be released for
FW/HW queues for a while (10 seconds per
observation). This means that if more management
frames are queued then HTC Tx credits are drained
to 0 and no other commands can be submitted
including beacons and peer removal.

This could in turn result in clients disconnecting
due to their beacon loss and may trigger spurious
sta kickouts because wmi peer removal command may
never reach firmware during disassociation.

This could happen, e.g. when disconnecting
client (hostapd inactivity) that has gone away
while asleep when acting as AP.

As a workaround flush frames that can potentialy
get stuck in FW Tx queues.

Change-Id: I3a80c1ec4bab346197d398fc3bc7376da2e35f15
Signed-off-by: Michal Kazior <michal.kazior at tieto.com>
diff --git a/drivers/net/wireless/ath/ath10k/mac.c b/drivers/net/wireless/ath/ath10k/mac.c
index def3406..1f66057 100644
--- a/drivers/net/wireless/ath/ath10k/mac.c
+++ b/drivers/net/wireless/ath/ath10k/mac.c
@@ -2160,9 +2160,67 @@
 	}
 }
 
+static void ath10k_mgmt_tx_flush(struct ath10k *ar, struct sk_buff *skb)
+{
+	struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+	struct ieee80211_sta *sta;
+	struct ath10k_vif *arvif;
+	u8 *da = ieee80211_get_DA(hdr);
+	u8 vdev_id = ATH10K_SKB_CB(skb)->vdev_id;
+	u32 bcn_intval = 0;
+	unsigned int msecs;
+	int ret;
+
+	lockdep_assert_held(&ar->conf_mutex);
+
+	if (!is_unicast_ether_addr(da))
+		return;
+
+	rcu_read_lock();
+	sta = ieee80211_find_sta_by_ifaddr(ar->hw, da, NULL);
+	rcu_read_unlock();
+
+	/*
+	 * FW Tx queues can be paused only for associated peers. Since this is
+	 * a workaround just assume worst case if station simply entry exists.
+	 */
+	if (!sta)
+		return;
+
+	list_for_each_entry(arvif, &ar->arvifs, list) {
+		if (arvif->vdev_id == vdev_id) {
+			bcn_intval = arvif->beacon_interval;
+			break;
+		}
+	}
+
+	if (!bcn_intval)
+		return;
+
+	/*
+	 * Wait 2 beacon intervals before flushing so stations that are
+	 * asleep but are actually still in range have a chance to see
+	 * updated PVB, wake up and fetch the frame. There's no other way of
+	 * synchronizing other than sleeping because there's no tx completion
+	 * indication event for WMI management tx.
+	 */
+	msecs = 2 * arvif->beacon_interval * 1024 / 1000;
+	ath10k_dbg(ATH10K_DBG_MAC,
+		   "mac flushing peer %pM on vdev %i mgmt tid for unicast mgmt (%d msecs)\n",
+		   da, vdev_id, msecs);
+	msleep(msecs);
+
+	ret = ath10k_wmi_peer_flush(ar, vdev_id, da,
+				    WMI_PEER_TID_MGMT_MASK);
+	if (ret)
+		ath10k_warn("failed to flush peer %pM mgmt tid: %d\n",
+			    da, ret);
+}
+
 void ath10k_mgmt_over_wmi_tx_work(struct work_struct *work)
 {
 	struct ath10k *ar = container_of(work, struct ath10k, wmi_mgmt_tx_work);
+	struct ieee80211_tx_info *info;
 	struct sk_buff *skb;
 	int ret;
 
@@ -2171,12 +2229,50 @@
 		if (!skb)
 			break;
 
+		mutex_lock(&ar->conf_mutex);
+
 		ret = ath10k_wmi_mgmt_tx(ar, skb);
 		if (ret) {
 			ath10k_warn("failed to transmit management frame via WMI: %d\n",
 				    ret);
+			mutex_unlock(&ar->conf_mutex);
 			ieee80211_free_txskb(ar->hw, skb);
+			continue;
 		}
+
+		/*
+		 * Each WMI management Tx consumes 1 HTC Tx credit and doesn't
+		 * replenish it until the frame is actually transmitted out of
+		 * firmware's Tx queues.
+		 *
+		 * If associated client was asleep and has gone out of range
+		 * then unicast frames won't be released for FW/HW queues for a
+		 * while (10 seconds per observation). This means that if more
+		 * management frames are queued then HTC Tx credits are drained
+		 * to 0 and no other commands can be submitted including
+		 * beacons and peer removal.
+		 *
+		 * This could in turn result in clients disconnecting due to
+		 * their beacon loss and may trigger spurious sta kickouts
+		 * because wmi peer removal command may never reach firmware
+		 * during disassociation.
+		 *
+		 * This could happen, e.g. when disconnecting client that has
+		 * gone away while asleep.
+		 *
+		 * As a workaround flush unicast management frames that can
+		 * possibly be buffered.
+		 *
+		 * Note: This is a deficiency in design of WMI_MGMT_TX command.
+		 */
+		ath10k_mgmt_tx_flush(ar, skb);
+
+		mutex_unlock(&ar->conf_mutex);
+
+		/* there's no way to get ACK so just assume it's acked */
+		info = IEEE80211_SKB_CB(skb);
+		info->flags |= IEEE80211_TX_STAT_ACK;
+		ieee80211_tx_status(ar->hw, skb);
 	}
 }
 
diff --git a/drivers/net/wireless/ath/ath10k/wmi.c b/drivers/net/wireless/ath/ath10k/wmi.c
index 7cc4acf..819cac7 100644
--- a/drivers/net/wireless/ath/ath10k/wmi.c
+++ b/drivers/net/wireless/ath/ath10k/wmi.c
@@ -760,7 +760,6 @@
 	struct wmi_mgmt_tx_cmd *cmd;
 	struct ieee80211_hdr *hdr;
 	struct sk_buff *wmi_skb;
-	struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
 	int len;
 	u16 fc;
 
@@ -796,10 +795,6 @@
 	if (ret)
 		return ret;
 
-	/* TODO: report tx status to mac80211 - temporary just ACK */
-	info->flags |= IEEE80211_TX_STAT_ACK;
-	ieee80211_tx_status_irqsafe(ar->hw, skb);
-
 	return ret;
 }
 
diff --git a/drivers/net/wireless/ath/ath10k/wmi.h b/drivers/net/wireless/ath/ath10k/wmi.h
index 288ed56..633cc8c 100644
--- a/drivers/net/wireless/ath/ath10k/wmi.h
+++ b/drivers/net/wireless/ath/ath10k/wmi.h
@@ -4052,6 +4052,9 @@
 	struct wmi_mac_addr peer_macaddr;
 } __packed;
 
+#define WMI_PEER_TID_MGMT 17
+#define WMI_PEER_TID_MGMT_MASK BIT(WMI_PEER_TID_MGMT)
+
 struct wmi_peer_flush_tids_cmd {
 	__le32 vdev_id;
 	struct wmi_mac_addr peer_macaddr;