diff --git a/drivers/net/wireless/ath/wil6210/debugfs.c b/drivers/net/wireless/ath/wil6210/debugfs.c
index f2eab39..51c3330 100644
--- a/drivers/net/wireless/ath/wil6210/debugfs.c
+++ b/drivers/net/wireless/ath/wil6210/debugfs.c
@@ -1922,6 +1922,223 @@ static ssize_t wil_tx_latency_write(struct file *file, const char __user *buf,
 	.llseek		= seq_lseek,
 };
 
+static void wil_link_stats_print_basic(struct wil6210_vif *vif,
+				       struct seq_file *s,
+				       struct wmi_link_stats_basic *basic)
+{
+	char per[5] = "?";
+
+	if (basic->per_average != 0xff)
+		snprintf(per, sizeof(per), "%d%%", basic->per_average);
+
+	seq_printf(s, "CID %d {\n"
+		   "\tTxMCS %d TxTpt %d\n"
+		   "\tGoodput(rx:tx) %d:%d\n"
+		   "\tRxBcastFrames %d\n"
+		   "\tRSSI %d SQI %d SNR %d PER %s\n"
+		   "\tRx RFC %d Ant num %d\n"
+		   "\tSectors(rx:tx) my %d:%d peer %d:%d\n"
+		   "}\n",
+		   basic->cid,
+		   basic->bf_mcs, le32_to_cpu(basic->tx_tpt),
+		   le32_to_cpu(basic->rx_goodput),
+		   le32_to_cpu(basic->tx_goodput),
+		   le32_to_cpu(basic->rx_bcast_frames),
+		   basic->rssi, basic->sqi, basic->snr, per,
+		   basic->selected_rfc, basic->rx_effective_ant_num,
+		   basic->my_rx_sector, basic->my_tx_sector,
+		   basic->other_rx_sector, basic->other_tx_sector);
+}
+
+static void wil_link_stats_print_global(struct wil6210_priv *wil,
+					struct seq_file *s,
+					struct wmi_link_stats_global *global)
+{
+	seq_printf(s, "Frames(rx:tx) %d:%d\n"
+		   "BA Frames(rx:tx) %d:%d\n"
+		   "Beacons %d\n"
+		   "Rx Errors (MIC:CRC) %d:%d\n"
+		   "Tx Errors (no ack) %d\n",
+		   le32_to_cpu(global->rx_frames),
+		   le32_to_cpu(global->tx_frames),
+		   le32_to_cpu(global->rx_ba_frames),
+		   le32_to_cpu(global->tx_ba_frames),
+		   le32_to_cpu(global->tx_beacons),
+		   le32_to_cpu(global->rx_mic_errors),
+		   le32_to_cpu(global->rx_crc_errors),
+		   le32_to_cpu(global->tx_fail_no_ack));
+}
+
+static void wil_link_stats_debugfs_show_vif(struct wil6210_vif *vif,
+					    struct seq_file *s)
+{
+	struct wil6210_priv *wil = vif_to_wil(vif);
+	struct wmi_link_stats_basic *stats;
+	int i;
+
+	if (!vif->fw_stats_ready) {
+		seq_puts(s, "no statistics\n");
+		return;
+	}
+
+	seq_printf(s, "TSF %lld\n", vif->fw_stats_tsf);
+	for (i = 0; i < ARRAY_SIZE(wil->sta); i++) {
+		if (wil->sta[i].status == wil_sta_unused)
+			continue;
+		if (wil->sta[i].mid != vif->mid)
+			continue;
+
+		stats = &wil->sta[i].fw_stats_basic;
+		wil_link_stats_print_basic(vif, s, stats);
+	}
+}
+
+static int wil_link_stats_debugfs_show(struct seq_file *s, void *data)
+{
+	struct wil6210_priv *wil = s->private;
+	struct wil6210_vif *vif;
+	int i, rc;
+
+	rc = mutex_lock_interruptible(&wil->vif_mutex);
+	if (rc)
+		return rc;
+
+	/* iterate over all MIDs and show per-cid statistics. Then show the
+	 * global statistics
+	 */
+	for (i = 0; i < wil->max_vifs; i++) {
+		vif = wil->vifs[i];
+
+		seq_printf(s, "MID %d ", i);
+		if (!vif) {
+			seq_puts(s, "unused\n");
+			continue;
+		}
+
+		wil_link_stats_debugfs_show_vif(vif, s);
+	}
+
+	mutex_unlock(&wil->vif_mutex);
+
+	return 0;
+}
+
+static int wil_link_stats_seq_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, wil_link_stats_debugfs_show, inode->i_private);
+}
+
+static ssize_t wil_link_stats_write(struct file *file, const char __user *buf,
+				    size_t len, loff_t *ppos)
+{
+	struct seq_file *s = file->private_data;
+	struct wil6210_priv *wil = s->private;
+	int cid, interval, rc, i;
+	struct wil6210_vif *vif;
+	char *kbuf = kmalloc(len + 1, GFP_KERNEL);
+
+	if (!kbuf)
+		return -ENOMEM;
+
+	rc = simple_write_to_buffer(kbuf, len, ppos, buf, len);
+	if (rc != len) {
+		kfree(kbuf);
+		return rc >= 0 ? -EIO : rc;
+	}
+
+	kbuf[len] = '\0';
+	/* specify cid (use -1 for all cids) and snapshot interval in ms */
+	rc = sscanf(kbuf, "%d %d", &cid, &interval);
+	kfree(kbuf);
+	if (rc < 0)
+		return rc;
+	if (rc < 2 || interval < 0)
+		return -EINVAL;
+
+	wil_info(wil, "request link statistics, cid %d interval %d\n",
+		 cid, interval);
+
+	rc = mutex_lock_interruptible(&wil->vif_mutex);
+	if (rc)
+		return rc;
+
+	for (i = 0; i < wil->max_vifs; i++) {
+		vif = wil->vifs[i];
+		if (!vif)
+			continue;
+
+		rc = wmi_link_stats_cfg(vif, WMI_LINK_STATS_TYPE_BASIC,
+					(cid == -1 ? 0xff : cid), interval);
+		if (rc)
+			wil_err(wil, "link statistics failed for mid %d\n", i);
+	}
+	mutex_unlock(&wil->vif_mutex);
+
+	return len;
+}
+
+static const struct file_operations fops_link_stats = {
+	.open		= wil_link_stats_seq_open,
+	.release	= single_release,
+	.read		= seq_read,
+	.write		= wil_link_stats_write,
+	.llseek		= seq_lseek,
+};
+
+static int
+wil_link_stats_global_debugfs_show(struct seq_file *s, void *data)
+{
+	struct wil6210_priv *wil = s->private;
+
+	if (!wil->fw_stats_global.ready)
+		return 0;
+
+	seq_printf(s, "TSF %lld\n", wil->fw_stats_global.tsf);
+	wil_link_stats_print_global(wil, s, &wil->fw_stats_global.stats);
+
+	return 0;
+}
+
+static int
+wil_link_stats_global_seq_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, wil_link_stats_global_debugfs_show,
+			   inode->i_private);
+}
+
+static ssize_t
+wil_link_stats_global_write(struct file *file, const char __user *buf,
+			    size_t len, loff_t *ppos)
+{
+	struct seq_file *s = file->private_data;
+	struct wil6210_priv *wil = s->private;
+	int interval, rc;
+	struct wil6210_vif *vif = ndev_to_vif(wil->main_ndev);
+
+	/* specify snapshot interval in ms */
+	rc = kstrtoint_from_user(buf, len, 0, &interval);
+	if (rc || interval < 0) {
+		wil_err(wil, "Invalid argument\n");
+		return -EINVAL;
+	}
+
+	wil_info(wil, "request global link stats, interval %d\n", interval);
+
+	rc = wmi_link_stats_cfg(vif, WMI_LINK_STATS_TYPE_GLOBAL, 0, interval);
+	if (rc)
+		wil_err(wil, "global link stats failed %d\n", rc);
+
+	return rc ? rc : len;
+}
+
+static const struct file_operations fops_link_stats_global = {
+	.open		= wil_link_stats_global_seq_open,
+	.release	= single_release,
+	.read		= seq_read,
+	.write		= wil_link_stats_global_write,
+	.llseek		= seq_lseek,
+};
+
 static ssize_t wil_read_file_led_cfg(struct file *file, char __user *user_buf,
 				     size_t count, loff_t *ppos)
 {
@@ -2256,6 +2473,8 @@ static void wil6210_debugfs_init_blobs(struct wil6210_priv *wil,
 	{"status_msg",	0444,		&fops_status_msg},
 	{"rx_buff_mgmt",	0444,	&fops_rx_buff_mgmt},
 	{"tx_latency",	0644,		&fops_tx_latency},
+	{"link_stats",	0644,		&fops_link_stats},
+	{"link_stats_global",	0644,	&fops_link_stats_global},
 };
 
 static void wil6210_debugfs_init_files(struct wil6210_priv *wil,
diff --git a/drivers/net/wireless/ath/wil6210/wil6210.h b/drivers/net/wireless/ath/wil6210/wil6210.h
index 379f7ce..e87c889 100644
--- a/drivers/net/wireless/ath/wil6210/wil6210.h
+++ b/drivers/net/wireless/ath/wil6210/wil6210.h
@@ -728,6 +728,7 @@ struct wil_sta_info {
 	 * tx_latency_res is configured from "tx_latency" debug-fs.
 	 */
 	u64 *tx_latency_bins;
+	struct wmi_link_stats_basic fw_stats_basic;
 	/* Rx BACK */
 	struct wil_tid_ampdu_rx *tid_rx[WIL_STA_TID_NUM];
 	spinlock_t tid_rx_lock; /* guarding tid_rx array */
@@ -842,6 +843,8 @@ struct wil6210_vif {
 	struct mutex probe_client_mutex; /* protect @probe_client_pending */
 	struct work_struct probe_client_worker;
 	int net_queue_stopped; /* netif_tx_stop_all_queues invoked */
+	bool fw_stats_ready; /* per-cid statistics are ready inside sta_info */
+	u64 fw_stats_tsf; /* measurement timestamp */
 };
 
 /**
@@ -869,6 +872,12 @@ struct wil_rx_buff_mgmt {
 	unsigned long free_list_empty_cnt; /* statistics */
 };
 
+struct wil_fw_stats_global {
+	bool ready;
+	u64 tsf; /* measurement timestamp */
+	struct wmi_link_stats_global stats;
+};
+
 struct wil6210_priv {
 	struct pci_dev *pdev;
 	u32 bar_size;
@@ -1002,6 +1011,8 @@ struct wil6210_priv {
 	bool use_rx_hw_reordering;
 	bool secured_boot;
 	u8 boot_config;
+
+	struct wil_fw_stats_global fw_stats_global;
 };
 
 #define wil_to_wiphy(i) (i->wiphy)
@@ -1201,6 +1212,7 @@ int wmi_ps_dev_profile_cfg(struct wil6210_priv *wil,
 int wmi_port_allocate(struct wil6210_priv *wil, u8 mid,
 		      const u8 *mac, enum nl80211_iftype iftype);
 int wmi_port_delete(struct wil6210_priv *wil, u8 mid);
+int wmi_link_stats_cfg(struct wil6210_vif *vif, u32 type, u8 cid, u32 interval);
 int wil_addba_rx_request(struct wil6210_priv *wil, u8 mid,
 			 u8 cidxtid, u8 dialog_token, __le16 ba_param_set,
 			 __le16 ba_timeout, __le16 ba_seq_ctrl);
diff --git a/drivers/net/wireless/ath/wil6210/wmi.c b/drivers/net/wireless/ath/wil6210/wmi.c
index 71056c8..45a71fd 100644
--- a/drivers/net/wireless/ath/wil6210/wmi.c
+++ b/drivers/net/wireless/ath/wil6210/wmi.c
@@ -464,6 +464,8 @@ static const char *cmdid2name(u16 cmdid)
 		return "WMI_BCAST_DESC_RING_ADD_CMD";
 	case WMI_CFG_DEF_RX_OFFLOAD_CMDID:
 		return "WMI_CFG_DEF_RX_OFFLOAD_CMD";
+	case WMI_LINK_STATS_CMDID:
+		return "WMI_LINK_STATS_CMD";
 	default:
 		return "Untracked CMD";
 	}
@@ -598,6 +600,10 @@ static const char *eventid2name(u16 eventid)
 		return "WMI_RX_DESC_RING_CFG_DONE_EVENT";
 	case WMI_CFG_DEF_RX_OFFLOAD_DONE_EVENTID:
 		return "WMI_CFG_DEF_RX_OFFLOAD_DONE_EVENT";
+	case WMI_LINK_STATS_CONFIG_DONE_EVENTID:
+		return "WMI_LINK_STATS_CONFIG_DONE_EVENT";
+	case WMI_LINK_STATS_EVENTID:
+		return "WMI_LINK_STATS_EVENT";
 	default:
 		return "Untracked EVENT";
 	}
@@ -1329,6 +1335,130 @@ static void wmi_evt_delba(struct wil6210_vif *vif, int id, void *d, int len)
 	cfg80211_sched_scan_results(wiphy, 0);
 }
 
+static void wil_link_stats_store_basic(struct wil6210_vif *vif,
+				       struct wmi_link_stats_basic *basic)
+{
+	struct wil6210_priv *wil = vif_to_wil(vif);
+	u8 cid = basic->cid;
+	struct wil_sta_info *sta;
+
+	if (cid < 0 || cid >= WIL6210_MAX_CID) {
+		wil_err(wil, "invalid cid %d\n", cid);
+		return;
+	}
+
+	sta = &wil->sta[cid];
+	sta->fw_stats_basic = *basic;
+}
+
+static void wil_link_stats_store_global(struct wil6210_vif *vif,
+					struct wmi_link_stats_global *global)
+{
+	struct wil6210_priv *wil = vif_to_wil(vif);
+
+	wil->fw_stats_global.stats = *global;
+}
+
+static void wmi_link_stats_parse(struct wil6210_vif *vif, u64 tsf,
+				 bool has_next, void *payload,
+				 size_t payload_size)
+{
+	struct wil6210_priv *wil = vif_to_wil(vif);
+	size_t hdr_size = sizeof(struct wmi_link_stats_record);
+	size_t stats_size, record_size, expected_size;
+	struct wmi_link_stats_record *hdr;
+
+	if (payload_size < hdr_size) {
+		wil_err(wil, "link stats wrong event size %zu\n", payload_size);
+		return;
+	}
+
+	while (payload_size >= hdr_size) {
+		hdr = payload;
+		stats_size = le16_to_cpu(hdr->record_size);
+		record_size = hdr_size + stats_size;
+
+		if (payload_size < record_size) {
+			wil_err(wil, "link stats payload ended unexpectedly, size %zu < %zu\n",
+				payload_size, record_size);
+			return;
+		}
+
+		switch (hdr->record_type_id) {
+		case WMI_LINK_STATS_TYPE_BASIC:
+			expected_size = sizeof(struct wmi_link_stats_basic);
+			if (stats_size < expected_size) {
+				wil_err(wil, "link stats invalid basic record size %zu < %zu\n",
+					stats_size, expected_size);
+				return;
+			}
+			if (vif->fw_stats_ready) {
+				/* clean old statistics */
+				vif->fw_stats_tsf = 0;
+				vif->fw_stats_ready = 0;
+			}
+
+			wil_link_stats_store_basic(vif, payload + hdr_size);
+
+			if (!has_next) {
+				vif->fw_stats_tsf = tsf;
+				vif->fw_stats_ready = 1;
+			}
+
+			break;
+		case WMI_LINK_STATS_TYPE_GLOBAL:
+			expected_size = sizeof(struct wmi_link_stats_global);
+			if (stats_size < sizeof(struct wmi_link_stats_global)) {
+				wil_err(wil, "link stats invalid global record size %zu < %zu\n",
+					stats_size, expected_size);
+				return;
+			}
+
+			if (wil->fw_stats_global.ready) {
+				/* clean old statistics */
+				wil->fw_stats_global.tsf = 0;
+				wil->fw_stats_global.ready = 0;
+			}
+
+			wil_link_stats_store_global(vif, payload + hdr_size);
+
+			if (!has_next) {
+				wil->fw_stats_global.tsf = tsf;
+				wil->fw_stats_global.ready = 1;
+			}
+
+			break;
+		default:
+			break;
+		}
+
+		/* skip to next record */
+		payload += record_size;
+		payload_size -= record_size;
+	}
+}
+
+static void
+wmi_evt_link_stats(struct wil6210_vif *vif, int id, void *d, int len)
+{
+	struct wil6210_priv *wil = vif_to_wil(vif);
+	struct wmi_link_stats_event *evt = d;
+	size_t payload_size;
+
+	if (len < offsetof(struct wmi_link_stats_event, payload)) {
+		wil_err(wil, "stats event way too short %d\n", len);
+		return;
+	}
+	payload_size = le16_to_cpu(evt->payload_size);
+	if (len < sizeof(struct wmi_link_stats_event) + payload_size) {
+		wil_err(wil, "stats event too short %d\n", len);
+		return;
+	}
+
+	wmi_link_stats_parse(vif, le64_to_cpu(evt->tsf), evt->has_next,
+			     evt->payload, payload_size);
+}
+
 /**
  * Some events are ignored for purpose; and need not be interpreted as
  * "unhandled events"
@@ -1359,6 +1489,7 @@ static void wmi_evt_ignore(struct wil6210_vif *vif, int id, void *d, int len)
 	{WMI_RING_EN_EVENTID,		wmi_evt_ring_en},
 	{WMI_DATA_PORT_OPEN_EVENTID,		wmi_evt_ignore},
 	{WMI_SCHED_SCAN_RESULT_EVENTID,		wmi_evt_sched_scan_result},
+	{WMI_LINK_STATS_EVENTID,		wmi_evt_link_stats},
 };
 
 /*
@@ -3242,3 +3373,37 @@ int wil_wmi_bcast_desc_ring_add(struct wil6210_vif *vif, int ring_id)
 
 	return 0;
 }
+
+int wmi_link_stats_cfg(struct wil6210_vif *vif, u32 type, u8 cid, u32 interval)
+{
+	struct wil6210_priv *wil = vif_to_wil(vif);
+	struct wmi_link_stats_cmd cmd = {
+		.record_type_mask = cpu_to_le32(type),
+		.cid = cid,
+		.action = WMI_LINK_STATS_SNAPSHOT,
+		.interval_msec = cpu_to_le32(interval),
+	};
+	struct {
+		struct wmi_cmd_hdr wmi;
+		struct wmi_link_stats_config_done_event evt;
+	} __packed reply = {
+		.evt = {.status = WMI_FW_STATUS_FAILURE},
+	};
+	int rc;
+
+	rc = wmi_call(wil, WMI_LINK_STATS_CMDID, vif->mid, &cmd, sizeof(cmd),
+		      WMI_LINK_STATS_CONFIG_DONE_EVENTID, &reply,
+		      sizeof(reply), WIL_WMI_CALL_GENERAL_TO_MS);
+	if (rc) {
+		wil_err(wil, "WMI_LINK_STATS_CMDID failed, rc %d\n", rc);
+		return rc;
+	}
+
+	if (reply.evt.status != WMI_FW_STATUS_SUCCESS) {
+		wil_err(wil, "Link statistics config failed, status %d\n",
+			reply.evt.status);
+		return -EINVAL;
+	}
+
+	return 0;
+}
diff --git a/drivers/net/wireless/ath/wil6210/wmi.h b/drivers/net/wireless/ath/wil6210/wmi.h
index 13f6f621..00cf3c4 100644
--- a/drivers/net/wireless/ath/wil6210/wmi.h
+++ b/drivers/net/wireless/ath/wil6210/wmi.h
@@ -1715,9 +1715,7 @@ enum wmi_link_stats_action {
 /* WMI_LINK_STATS_EVENT record identifiers */
 enum wmi_link_stats_record_type {
 	WMI_LINK_STATS_TYPE_BASIC	= 0x01,
-	WMI_LINK_STATS_TYPE_MAC		= 0x02,
-	WMI_LINK_STATS_TYPE_PHY		= 0x04,
-	WMI_LINK_STATS_TYPE_OTA		= 0x08,
+	WMI_LINK_STATS_TYPE_GLOBAL	= 0x02,
 };
 
 /* WMI_LINK_STATS_CMDID */
@@ -1731,7 +1729,7 @@ struct wmi_link_stats_cmd {
 	/* wmi_link_stats_action_e */
 	u8 action;
 	u8 reserved[6];
-	/* for WMI_LINK_STATS_PERIODIC */
+	/* range = 100 - 10000 */
 	__le32 interval_msec;
 } __packed;
 
@@ -3779,32 +3777,59 @@ struct wmi_link_stats_config_done_event {
 
 /* WMI_LINK_STATS_EVENTID */
 struct wmi_link_stats_event {
+	__le64 tsf;
 	__le16 payload_size;
 	u8 has_next;
 	u8 reserved[5];
-	/* a stream of records, e.g. wmi_link_stats_basic_s */
+	/* a stream of wmi_link_stats_record_s */
 	u8 payload[0];
 } __packed;
 
-/* WMI_LINK_STATS_EVENT record struct */
-struct wmi_link_stats_basic {
-	/* WMI_LINK_STATS_TYPE_BASIC */
+/* WMI_LINK_STATS_EVENT */
+struct wmi_link_stats_record {
+	/* wmi_link_stats_record_type_e */
 	u8 record_type_id;
+	u8 reserved;
+	__le16 record_size;
+	u8 record[0];
+} __packed;
+
+/* WMI_LINK_STATS_TYPE_BASIC */
+struct wmi_link_stats_basic {
 	u8 cid;
-	/* 0: fail; 1: OK; 2: retrying */
-	u8 bf_status;
 	s8 rssi;
 	u8 sqi;
+	u8 bf_mcs;
+	u8 per_average;
 	u8 selected_rfc;
-	__le16 bf_mcs;
+	u8 rx_effective_ant_num;
+	u8 my_rx_sector;
+	u8 my_tx_sector;
+	u8 other_rx_sector;
+	u8 other_tx_sector;
+	u8 reserved[7];
+	/* 1/4 Db units */
+	__le16 snr;
 	__le32 tx_tpt;
 	__le32 tx_goodput;
 	__le32 rx_goodput;
-	__le16 my_rx_sector;
-	__le16 my_tx_sector;
-	__le16 other_rx_sector;
-	__le16 other_tx_sector;
-	__le32 reserved[2];
+	__le32 bf_count;
+	__le32 rx_bcast_frames;
+} __packed;
+
+/* WMI_LINK_STATS_TYPE_GLOBAL */
+struct wmi_link_stats_global {
+	/* all ack-able frames */
+	__le32 rx_frames;
+	/* all ack-able frames */
+	__le32 tx_frames;
+	__le32 rx_ba_frames;
+	__le32 tx_ba_frames;
+	__le32 tx_beacons;
+	__le32 rx_mic_errors;
+	__le32 rx_crc_errors;
+	__le32 tx_fail_no_ack;
+	u8 reserved[8];
 } __packed;
 
 /* WMI_SET_GRANT_MCS_EVENTID */
