wifi: ath12k: Request vdev stats from firmware

Add support to request and print vdev stats from firmware through WMI.

Sample output:
-------------
cat /sys/kernel/debug/ath12k/pci-0000\:06\:00.0/mac0/fw_stats/vdev_stats

             ath12k VDEV stats
             =================

                       VDEV ID 0
              VDEV MAC address 00:03:7f:6c:9c:1a
                    beacon snr 96
                      data snr 255
                 num rx frames 0
                  num rts fail 0
               num rts success 0
                    num rx err 0
                num rx discard 0
              num tx not acked 0
            num tx frames [00] 0
            num tx frames [01] 0
            num tx frames [02] 0
            num tx frames [03] 2
    num tx frames retries [00] 0
    num tx frames retries [01] 0
    num tx frames retries [02] 0
    num tx frames retries [03] 0
   num tx frames failures [00] 0
   num tx frames failures [01] 0
   num tx frames failures [02] 0
   num tx frames failures [03] 0
          tx rate history [00] 0x00000000
          tx rate history [01] 0x00000000
          tx rate history [02] 0x00000000
          tx rate history [03] 0x00000000
          tx rate history [04] 0x00000000
          tx rate history [05] 0x00000000
          tx rate history [06] 0x00000000
          tx rate history [07] 0x00000000
          tx rate history [08] 0x00000000
          tx rate history [09] 0x00000000
      beacon rssi history [00] 0
      beacon rssi history [01] 0
      beacon rssi history [02] 0
      beacon rssi history [03] 0
      beacon rssi history [04] 0
      beacon rssi history [05] 0
      beacon rssi history [06] 0
      beacon rssi history [07] 0
      beacon rssi history [08] 0
      beacon rssi history [09] 0

Tested-on: QCN9274 hw2.0 PCI WLAN.WBE.1.0.1-00029-QCAHKSWPL_SILICONZ-1
Tested-on: WCN7850 hw2.0 PCI WLAN.HMT.1.0.c5-00481-QCAHMTSWPL_V1.0_V2.0_SILICONZ-3

Signed-off-by: Ramya Gnanasekar <ramya.gnanasekar@oss.qualcomm.com>
Reviewed-by: Aditya Kumar Singh <aditya.kumar.singh@oss.qualcomm.com>
Link: https://patch.msgid.link/20250124185330.1244585-2-ramya.gnanasekar@oss.qualcomm.com
Signed-off-by: Jeff Johnson <jeff.johnson@oss.qualcomm.com>
This commit is contained in:
Ramya Gnanasekar 2025-01-25 00:23:28 +05:30 committed by Jeff Johnson
parent 1b24394ed5
commit e367c92476
5 changed files with 394 additions and 2 deletions

View file

@ -557,6 +557,7 @@ struct ath12k_fw_stats {
struct list_head pdevs;
struct list_head vdevs;
struct list_head bcn;
bool fw_stats_done;
};
struct ath12k_dbg_htt_stats {
@ -728,6 +729,7 @@ struct ath12k {
struct completion mlo_setup_done;
u32 mlo_setup_status;
u8 ftm_msgref;
struct ath12k_fw_stats fw_stats;
};
struct ath12k_hw {
@ -1078,6 +1080,25 @@ struct ath12k_pdev_map {
u8 pdev_idx;
};
struct ath12k_fw_stats_vdev {
struct list_head list;
u32 vdev_id;
u32 beacon_snr;
u32 data_snr;
u32 num_tx_frames[WLAN_MAX_AC];
u32 num_rx_frames;
u32 num_tx_frames_retries[WLAN_MAX_AC];
u32 num_tx_frames_failures[WLAN_MAX_AC];
u32 num_rts_fail;
u32 num_rts_success;
u32 num_rx_err;
u32 num_rx_discard;
u32 num_tx_not_acked;
u32 tx_rate_history[MAX_TX_RATE_VALUES];
u32 beacon_rssi_history[MAX_TX_RATE_VALUES];
};
int ath12k_core_qmi_firmware_ready(struct ath12k_base *ab);
int ath12k_core_pre_init(struct ath12k_base *ab);
int ath12k_core_init(struct ath12k_base *ath12k);

View file

@ -1,10 +1,11 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
/*
* Copyright (c) 2018-2021 The Linux Foundation. All rights reserved.
* Copyright (c) 2021-2024 Qualcomm Innovation Center, Inc. All rights reserved.
* Copyright (c) 2021-2025 Qualcomm Innovation Center, Inc. All rights reserved.
*/
#include "core.h"
#include "debug.h"
#include "debugfs.h"
#include "debugfs_htt_stats.h"
@ -68,6 +69,199 @@ void ath12k_debugfs_soc_destroy(struct ath12k_base *ab)
*/
}
static void ath12k_fw_stats_vdevs_free(struct list_head *head)
{
struct ath12k_fw_stats_vdev *i, *tmp;
list_for_each_entry_safe(i, tmp, head, list) {
list_del(&i->list);
kfree(i);
}
}
void ath12k_debugfs_fw_stats_reset(struct ath12k *ar)
{
spin_lock_bh(&ar->data_lock);
ar->fw_stats.fw_stats_done = false;
ath12k_fw_stats_vdevs_free(&ar->fw_stats.vdevs);
spin_unlock_bh(&ar->data_lock);
}
static int ath12k_debugfs_fw_stats_request(struct ath12k *ar,
struct ath12k_fw_stats_req_params *param)
{
struct ath12k_base *ab = ar->ab;
unsigned long timeout, time_left;
int ret;
lockdep_assert_wiphy(ath12k_ar_to_hw(ar)->wiphy);
/* FW stats can get split when exceeding the stats data buffer limit.
* In that case, since there is no end marking for the back-to-back
* received 'update stats' event, we keep a 3 seconds timeout in case,
* fw_stats_done is not marked yet
*/
timeout = jiffies + msecs_to_jiffies(3 * 1000);
ath12k_debugfs_fw_stats_reset(ar);
reinit_completion(&ar->fw_stats_complete);
ret = ath12k_wmi_send_stats_request_cmd(ar, param->stats_id,
param->vdev_id, param->pdev_id);
if (ret) {
ath12k_warn(ab, "could not request fw stats (%d)\n",
ret);
return ret;
}
time_left = wait_for_completion_timeout(&ar->fw_stats_complete,
1 * HZ);
/* If the wait timed out, return -ETIMEDOUT */
if (!time_left)
return -ETIMEDOUT;
/* Firmware sends WMI_UPDATE_STATS_EVENTID back-to-back
* when stats data buffer limit is reached. fw_stats_complete
* is completed once host receives first event from firmware, but
* still end might not be marked in the TLV.
* Below loop is to confirm that firmware completed sending all the event
* and fw_stats_done is marked true when end is marked in the TLV
*/
for (;;) {
if (time_after(jiffies, timeout))
break;
spin_lock_bh(&ar->data_lock);
if (ar->fw_stats.fw_stats_done) {
spin_unlock_bh(&ar->data_lock);
break;
}
spin_unlock_bh(&ar->data_lock);
}
return 0;
}
void
ath12k_debugfs_fw_stats_process(struct ath12k *ar,
struct ath12k_fw_stats *stats)
{
struct ath12k_base *ab = ar->ab;
struct ath12k_pdev *pdev;
bool is_end;
static unsigned int num_vdev;
size_t total_vdevs_started = 0;
int i;
if (stats->stats_id == WMI_REQUEST_VDEV_STAT) {
if (list_empty(&stats->vdevs)) {
ath12k_warn(ab, "empty vdev stats");
return;
}
/* FW sends all the active VDEV stats irrespective of PDEV,
* hence limit until the count of all VDEVs started
*/
rcu_read_lock();
for (i = 0; i < ab->num_radios; i++) {
pdev = rcu_dereference(ab->pdevs_active[i]);
if (pdev && pdev->ar)
total_vdevs_started += pdev->ar->num_started_vdevs;
}
rcu_read_unlock();
is_end = ((++num_vdev) == total_vdevs_started);
list_splice_tail_init(&stats->vdevs,
&ar->fw_stats.vdevs);
if (is_end) {
ar->fw_stats.fw_stats_done = true;
num_vdev = 0;
}
return;
}
}
static int ath12k_open_vdev_stats(struct inode *inode, struct file *file)
{
struct ath12k *ar = inode->i_private;
struct ath12k_fw_stats_req_params param;
struct ath12k_hw *ah = ath12k_ar_to_ah(ar);
int ret;
guard(wiphy)(ath12k_ar_to_hw(ar)->wiphy);
if (!ah)
return -ENETDOWN;
if (ah->state != ATH12K_HW_STATE_ON)
return -ENETDOWN;
void *buf __free(kfree) = kzalloc(ATH12K_FW_STATS_BUF_SIZE, GFP_ATOMIC);
if (!buf)
return -ENOMEM;
param.pdev_id = ath12k_mac_get_target_pdev_id(ar);
/* VDEV stats is always sent for all active VDEVs from FW */
param.vdev_id = 0;
param.stats_id = WMI_REQUEST_VDEV_STAT;
ret = ath12k_debugfs_fw_stats_request(ar, &param);
if (ret) {
ath12k_warn(ar->ab, "failed to request fw vdev stats: %d\n", ret);
return ret;
}
ath12k_wmi_fw_stats_dump(ar, &ar->fw_stats, param.stats_id,
buf);
file->private_data = no_free_ptr(buf);
return 0;
}
static int ath12k_release_vdev_stats(struct inode *inode, struct file *file)
{
kfree(file->private_data);
return 0;
}
static ssize_t ath12k_read_vdev_stats(struct file *file,
char __user *user_buf,
size_t count, loff_t *ppos)
{
const char *buf = file->private_data;
size_t len = strlen(buf);
return simple_read_from_buffer(user_buf, count, ppos, buf, len);
}
static const struct file_operations fops_vdev_stats = {
.open = ath12k_open_vdev_stats,
.release = ath12k_release_vdev_stats,
.read = ath12k_read_vdev_stats,
.owner = THIS_MODULE,
.llseek = default_llseek,
};
static
void ath12k_debugfs_fw_stats_register(struct ath12k *ar)
{
struct dentry *fwstats_dir = debugfs_create_dir("fw_stats",
ar->debug.debugfs_pdev);
/* all stats debugfs files created are under "fw_stats" directory
* created per PDEV
*/
debugfs_create_file("vdev_stats", 0600, fwstats_dir, ar,
&fops_vdev_stats);
INIT_LIST_HEAD(&ar->fw_stats.vdevs);
init_completion(&ar->fw_stats_complete);
}
void ath12k_debugfs_register(struct ath12k *ar)
{
struct ath12k_base *ab = ar->ab;
@ -92,6 +286,7 @@ void ath12k_debugfs_register(struct ath12k *ar)
}
ath12k_debugfs_htt_stats_register(ar);
ath12k_debugfs_fw_stats_register(ar);
}
void ath12k_debugfs_unregister(struct ath12k *ar)

View file

@ -1,7 +1,7 @@
/* SPDX-License-Identifier: BSD-3-Clause-Clear */
/*
* Copyright (c) 2018-2021 The Linux Foundation. All rights reserved.
* Copyright (c) 2021-2024 Qualcomm Innovation Center, Inc. All rights reserved.
* Copyright (c) 2021-2025 Qualcomm Innovation Center, Inc. All rights reserved.
*/
#ifndef _ATH12K_DEBUGFS_H_
@ -12,6 +12,9 @@ void ath12k_debugfs_soc_create(struct ath12k_base *ab);
void ath12k_debugfs_soc_destroy(struct ath12k_base *ab);
void ath12k_debugfs_register(struct ath12k *ar);
void ath12k_debugfs_unregister(struct ath12k *ar);
void ath12k_debugfs_fw_stats_process(struct ath12k *ar,
struct ath12k_fw_stats *stats);
void ath12k_debugfs_fw_stats_reset(struct ath12k *ar);
#else
static inline void ath12k_debugfs_soc_create(struct ath12k_base *ab)
{
@ -29,6 +32,14 @@ static inline void ath12k_debugfs_unregister(struct ath12k *ar)
{
}
static inline void ath12k_debugfs_fw_stats_process(struct ath12k *ar,
struct ath12k_fw_stats *stats)
{
}
static inline void ath12k_debugfs_fw_stats_reset(struct ath12k *ar)
{
}
#endif /* CONFIG_ATH12K_DEBUGFS */
#endif /* _ATH12K_DEBUGFS_H_ */

View file

@ -15,6 +15,7 @@
#include <linux/time.h>
#include <linux/of.h>
#include "core.h"
#include "debugfs.h"
#include "debug.h"
#include "mac.h"
#include "hw.h"
@ -6853,12 +6854,156 @@ static void ath12k_peer_assoc_conf_event(struct ath12k_base *ab, struct sk_buff
rcu_read_unlock();
}
static void
ath12k_wmi_fw_vdev_stats_dump(struct ath12k *ar,
struct ath12k_fw_stats *fw_stats,
char *buf, u32 *length)
{
const struct ath12k_fw_stats_vdev *vdev;
u32 buf_len = ATH12K_FW_STATS_BUF_SIZE;
struct ath12k_link_vif *arvif;
u32 len = *length;
u8 *vif_macaddr;
int i;
len += scnprintf(buf + len, buf_len - len, "\n");
len += scnprintf(buf + len, buf_len - len, "%30s\n",
"ath12k VDEV stats");
len += scnprintf(buf + len, buf_len - len, "%30s\n\n",
"=================");
list_for_each_entry(vdev, &fw_stats->vdevs, list) {
arvif = ath12k_mac_get_arvif(ar, vdev->vdev_id);
if (!arvif)
continue;
vif_macaddr = arvif->ahvif->vif->addr;
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"VDEV ID", vdev->vdev_id);
len += scnprintf(buf + len, buf_len - len, "%30s %pM\n",
"VDEV MAC address", vif_macaddr);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"beacon snr", vdev->beacon_snr);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"data snr", vdev->data_snr);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"num rx frames", vdev->num_rx_frames);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"num rts fail", vdev->num_rts_fail);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"num rts success", vdev->num_rts_success);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"num rx err", vdev->num_rx_err);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"num rx discard", vdev->num_rx_discard);
len += scnprintf(buf + len, buf_len - len, "%30s %u\n",
"num tx not acked", vdev->num_tx_not_acked);
for (i = 0 ; i < WLAN_MAX_AC; i++)
len += scnprintf(buf + len, buf_len - len,
"%25s [%02d] %u\n",
"num tx frames", i,
vdev->num_tx_frames[i]);
for (i = 0 ; i < WLAN_MAX_AC; i++)
len += scnprintf(buf + len, buf_len - len,
"%25s [%02d] %u\n",
"num tx frames retries", i,
vdev->num_tx_frames_retries[i]);
for (i = 0 ; i < WLAN_MAX_AC; i++)
len += scnprintf(buf + len, buf_len - len,
"%25s [%02d] %u\n",
"num tx frames failures", i,
vdev->num_tx_frames_failures[i]);
for (i = 0 ; i < MAX_TX_RATE_VALUES; i++)
len += scnprintf(buf + len, buf_len - len,
"%25s [%02d] 0x%08x\n",
"tx rate history", i,
vdev->tx_rate_history[i]);
for (i = 0 ; i < MAX_TX_RATE_VALUES; i++)
len += scnprintf(buf + len, buf_len - len,
"%25s [%02d] %u\n",
"beacon rssi history", i,
vdev->beacon_rssi_history[i]);
len += scnprintf(buf + len, buf_len - len, "\n");
*length = len;
}
}
void ath12k_wmi_fw_stats_dump(struct ath12k *ar,
struct ath12k_fw_stats *fw_stats,
u32 stats_id, char *buf)
{
u32 len = 0;
u32 buf_len = ATH12K_FW_STATS_BUF_SIZE;
spin_lock_bh(&ar->data_lock);
switch (stats_id) {
case WMI_REQUEST_VDEV_STAT:
ath12k_wmi_fw_vdev_stats_dump(ar, fw_stats, buf, &len);
break;
default:
break;
}
spin_unlock_bh(&ar->data_lock);
if (len >= buf_len)
buf[len - 1] = 0;
else
buf[len] = 0;
ath12k_debugfs_fw_stats_reset(ar);
}
static void
ath12k_wmi_pull_vdev_stats(const struct wmi_vdev_stats_params *src,
struct ath12k_fw_stats_vdev *dst)
{
int i;
dst->vdev_id = le32_to_cpu(src->vdev_id);
dst->beacon_snr = le32_to_cpu(src->beacon_snr);
dst->data_snr = le32_to_cpu(src->data_snr);
dst->num_rx_frames = le32_to_cpu(src->num_rx_frames);
dst->num_rts_fail = le32_to_cpu(src->num_rts_fail);
dst->num_rts_success = le32_to_cpu(src->num_rts_success);
dst->num_rx_err = le32_to_cpu(src->num_rx_err);
dst->num_rx_discard = le32_to_cpu(src->num_rx_discard);
dst->num_tx_not_acked = le32_to_cpu(src->num_tx_not_acked);
for (i = 0; i < WLAN_MAX_AC; i++)
dst->num_tx_frames[i] =
le32_to_cpu(src->num_tx_frames[i]);
for (i = 0; i < WLAN_MAX_AC; i++)
dst->num_tx_frames_retries[i] =
le32_to_cpu(src->num_tx_frames_retries[i]);
for (i = 0; i < WLAN_MAX_AC; i++)
dst->num_tx_frames_failures[i] =
le32_to_cpu(src->num_tx_frames_failures[i]);
for (i = 0; i < MAX_TX_RATE_VALUES; i++)
dst->tx_rate_history[i] =
le32_to_cpu(src->tx_rate_history[i]);
for (i = 0; i < MAX_TX_RATE_VALUES; i++)
dst->beacon_rssi_history[i] =
le32_to_cpu(src->beacon_rssi_history[i]);
}
static int ath12k_wmi_tlv_fw_stats_data_parse(struct ath12k_base *ab,
struct wmi_tlv_fw_stats_parse *parse,
const void *ptr,
u16 len)
{
const struct wmi_stats_event *ev = parse->ev;
struct ath12k_fw_stats stats = {0};
struct ath12k *ar;
struct ath12k_link_vif *arvif;
struct ieee80211_sta *sta;
@ -6867,6 +7012,8 @@ static int ath12k_wmi_tlv_fw_stats_data_parse(struct ath12k_base *ab,
int i, ret = 0;
const void *data = ptr;
INIT_LIST_HEAD(&stats.vdevs);
if (!ev) {
ath12k_warn(ab, "failed to fetch update stats ev");
return -EPROTO;
@ -6884,6 +7031,7 @@ static int ath12k_wmi_tlv_fw_stats_data_parse(struct ath12k_base *ab,
for (i = 0; i < le32_to_cpu(ev->num_vdev_stats); i++) {
const struct wmi_vdev_stats_params *src;
struct ath12k_fw_stats_vdev *dst;
src = data;
if (len < sizeof(*src)) {
@ -6912,9 +7060,16 @@ static int ath12k_wmi_tlv_fw_stats_data_parse(struct ath12k_base *ab,
data += sizeof(*src);
len -= sizeof(*src);
dst = kzalloc(sizeof(*dst), GFP_ATOMIC);
if (!dst)
continue;
ath12k_wmi_pull_vdev_stats(src, dst);
stats.stats_id = WMI_REQUEST_VDEV_STAT;
list_add_tail(&dst->list, &stats.vdevs);
}
complete(&ar->fw_stats_complete);
ath12k_debugfs_fw_stats_process(ar, &stats);
exit:
rcu_read_unlock();
return ret;

View file

@ -25,6 +25,7 @@
struct ath12k_base;
struct ath12k;
struct ath12k_link_vif;
struct ath12k_fw_stats;
/* There is no signed version of __le32, so for a temporary solution come
* up with our own version. The idea is from fs/ntfs/endian.h.
@ -5695,6 +5696,12 @@ struct wmi_vdev_stats_params {
__le32 beacon_rssi_history[MAX_TX_RATE_VALUES];
} __packed;
struct ath12k_fw_stats_req_params {
u32 stats_id;
u32 vdev_id;
u32 pdev_id;
};
void ath12k_wmi_init_qcn9274(struct ath12k_base *ab,
struct ath12k_wmi_resource_config_arg *config);
void ath12k_wmi_init_wcn7850(struct ath12k_base *ab,
@ -5876,5 +5883,8 @@ int ath12k_wmi_sta_keepalive(struct ath12k *ar,
int ath12k_wmi_mlo_setup(struct ath12k *ar, struct wmi_mlo_setup_arg *mlo_params);
int ath12k_wmi_mlo_ready(struct ath12k *ar);
int ath12k_wmi_mlo_teardown(struct ath12k *ar);
void ath12k_wmi_fw_stats_dump(struct ath12k *ar,
struct ath12k_fw_stats *fw_stats, u32 stats_id,
char *buf);
#endif