@@ -15,6 +15,19 @@ enum iwl_dhc_table_id {
* @DHC_TABLE_INTEGRATION: select the integration table
*/
DHC_TABLE_INTEGRATION = 2 << DHC_TABLE_MASK_POS,
+ /**
+ * @DHC_TABLE_TOOLS: select the tools table
+ */
+ DHC_TABLE_TOOLS = 0,
+};
+
+/**
+ * enum iwl_dhc_umac_tools_table - tools operations
+ * @DHC_TOOLS_UMAC_GET_TAS_STATUS: Get TAS status.
+ * See @struct iwl_dhc_tas_status_resp
+ */
+enum iwl_dhc_umac_tools_table {
+ DHC_TOOLS_UMAC_GET_TAS_STATUS = 0,
};
/**
@@ -58,6 +71,90 @@ struct iwl_dhc_cmd {
__le32 data[];
} __packed; /* DHC_CMD_API_S */
+/**
+ * struct iwl_dhc_payload_hdr - DHC payload header
+ * @version: a version of a payload
+ * @reserved: reserved for alignment
+ */
+struct iwl_dhc_payload_hdr {
+ u8 version;
+ u8 reserved[3];
+} __packed; /* DHC_PAYLOAD_HDR_API_S_VER_1 */
+
+/**
+ * struct iwl_dhc_tas_status_per_radio - TAS status per radio
+ * @band: &PHY_BAND_5 for high band, PHY_BAND_24 for low band and
+ * &PHY_BAND_6 for ultra high band.
+ * @static_status: TAS statically enabled or disabled
+ * @static_disable_reason: TAS static disable reason, uses
+ * &enum iwl_tas_statically_disabled_reason
+ * @near_disconnection: is TAS currently near disconnection per radio
+ * @dynamic_status_ant_a: Antenna A current TAS status.
+ * uses &enum iwl_tas_dyna_status
+ * @dynamic_status_ant_b: Antenna B current TAS status.
+ * uses &enum iwl_tas_dyna_status
+ * @max_reg_pwr_limit_ant_a: Antenna A regulatory power limits in dBm
+ * @max_reg_pwr_limit_ant_b: Antenna B regulatory power limits in dBm
+ * @sar_limit_ant_a: Antenna A SAR limit per radio in dBm
+ * @sar_limit_ant_b: Antenna B SAR limit per radio in dBm
+ * @reserved: reserved for alignment
+ */
+struct iwl_dhc_tas_status_per_radio {
+ u8 band;
+ u8 static_status;
+ u8 static_disable_reason;
+ u8 near_disconnection;
+ u8 dynamic_status_ant_a;
+ u8 dynamic_status_ant_b;
+ __le16 max_reg_pwr_limit_ant_a;
+ __le16 max_reg_pwr_limit_ant_b;
+ __le16 sar_limit_ant_a;
+ __le16 sar_limit_ant_b;
+ u8 reserved[2];
+} __packed; /* DHC_TAS_STATUS_PER_RADIO_S_VER_1 */
+
+/**
+ * struct iwl_dhc_tas_status_resp - Response to DHC_TOOLS_UMAC_GET_TAS_STATUS
+ * @header: DHC payload header, uses &struct iwl_dhc_payload_hdr
+ * @tas_config_info: see @struct bios_value_u32
+ * @mcc_block_list: block listed country codes
+ * @tas_status_radio: TAS status, uses &struct iwl_dhc_tas_status_per_radio
+ * @curr_mcc: current mcc
+ * @valid_radio_mask: represent entry in tas_status_per_radio is valid.
+ * @reserved: reserved for alignment
+ */
+struct iwl_dhc_tas_status_resp {
+ struct iwl_dhc_payload_hdr header;
+ struct bios_value_u32 tas_config_info;
+ __le16 mcc_block_list[IWL_WTAS_BLACK_LIST_MAX];
+ struct iwl_dhc_tas_status_per_radio tas_status_radio[2];
+ __le16 curr_mcc;
+ u8 valid_radio_mask;
+ u8 reserved;
+} __packed; /* DHC_TAS_STATUS_RSP_API_S_VER_1 */
+
+/**
+ * struct iwl_dhc_cmd_resp_v1 - debug host command response
+ * @status: status of the command
+ * @data: the response data
+ */
+struct iwl_dhc_cmd_resp_v1 {
+ __le32 status;
+ __le32 data[];
+} __packed; /* DHC_RESP_API_S_VER_1 */
+
+/**
+ * struct iwl_dhc_cmd_resp - debug host command response
+ * @status: status of the command
+ * @descriptor: command descriptor (index_and_mask) returned
+ * @data: the response data
+ */
+struct iwl_dhc_cmd_resp {
+ __le32 status;
+ __le32 descriptor;
+ __le32 data[];
+} __packed; /* DHC_RESP_API_S_VER_2 and DHC_RESP_API_S_VER_3 */
+
/**
* enum iwl_dhc_twt_operation_type - describes the TWT operation type
*
new file mode 100644
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */
+/*
+ * Copyright (C) 2021, 2025 Intel Corporation
+ */
+#ifndef __iwl_fw_dhc_utils_h__
+#define __iwl_fw_dhc_utils_h__
+
+#include <linux/types.h>
+#include "fw/img.h"
+#include "api/commands.h"
+#include "api/dhc.h"
+
+/**
+ * iwl_dhc_resp_status - return status of DHC response
+ * @fw: firwmware image information
+ * @pkt: response packet, must not be %NULL
+ *
+ * Returns: the status value of the DHC command or (u32)-1 if the
+ * response was too short.
+ */
+static inline u32 iwl_dhc_resp_status(const struct iwl_fw *fw,
+ struct iwl_rx_packet *pkt)
+{
+ if (iwl_fw_lookup_notif_ver(fw, IWL_ALWAYS_LONG_GROUP,
+ DEBUG_HOST_COMMAND, 1) >= 2) {
+ struct iwl_dhc_cmd_resp *resp = (void *)pkt->data;
+
+ if (iwl_rx_packet_payload_len(pkt) < sizeof(*resp))
+ return (u32)-1;
+
+ return le32_to_cpu(resp->status);
+ } else {
+ struct iwl_dhc_cmd_resp_v1 *resp = (void *)pkt->data;
+
+ if (iwl_rx_packet_payload_len(pkt) < sizeof(*resp))
+ return (u32)-1;
+
+ return le32_to_cpu(resp->status);
+ }
+}
+
+/**
+ * iwl_dhc_resp_data - return data pointer of DHC response
+ * @fw: firwmware image information
+ * @pkt: response packet, must not be %NULL
+ * @len: where to store the length
+ *
+ * Returns: The data pointer, or an ERR_PTR() if the data was
+ * not valid (too short).
+ */
+static inline void *iwl_dhc_resp_data(const struct iwl_fw *fw,
+ struct iwl_rx_packet *pkt,
+ unsigned int *len)
+{
+ if (iwl_fw_lookup_notif_ver(fw, IWL_ALWAYS_LONG_GROUP,
+ DEBUG_HOST_COMMAND, 1) >= 2) {
+ struct iwl_dhc_cmd_resp *resp = (void *)pkt->data;
+
+ if (iwl_rx_packet_payload_len(pkt) < sizeof(*resp))
+ return ERR_PTR(-EINVAL);
+
+ *len = iwl_rx_packet_payload_len(pkt) - sizeof(*resp);
+ return (void *)&resp->data;
+ } else {
+ struct iwl_dhc_cmd_resp_v1 *resp = (void *)pkt->data;
+
+ if (iwl_rx_packet_payload_len(pkt) < sizeof(*resp))
+ return ERR_PTR(-EINVAL);
+
+ *len = iwl_rx_packet_payload_len(pkt) - sizeof(*resp);
+ return (void *)&resp->data;
+ }
+}
+
+#endif /* __iwl_fw_dhc_utils_h__ */
@@ -21,6 +21,8 @@
#include "fw/api/rs.h"
#include "fw/api/dhc.h"
#include "fw/api/rfi.h"
+#include "fw/dhc-utils.h"
+#include <linux/dmi.h>
#define MLD_DEBUGFS_READ_FILE_OPS(name, bufsz) \
_MLD_DEBUGFS_READ_FILE_OPS(name, bufsz, struct iwl_mld)
@@ -206,11 +208,240 @@ iwl_dbgfs_he_sniffer_params_read(struct iwl_mld *mld, char *buf, size_t count)
#define IWL_RFI_BUF_SIZE (IWL_RFI_DDR_BUF_SIZE + IWL_RFI_DLVR_BUF_SIZE +\
IWL_RFI_DESENSE_BUF_SIZE + 32)
+static size_t iwl_mld_dump_tas_resp(struct iwl_dhc_tas_status_resp *resp,
+ size_t count, u8 *buf)
+{
+ const char * const tas_dis_reason[TAS_DISABLED_REASON_MAX] = {
+ [TAS_DISABLED_DUE_TO_BIOS] =
+ "Due To BIOS",
+ [TAS_DISABLED_DUE_TO_SAR_6DBM] =
+ "Due To SAR Limit Less Than 6 dBm",
+ [TAS_DISABLED_REASON_INVALID] =
+ "N/A",
+ [TAS_DISABLED_DUE_TO_TABLE_SOURCE_INVALID] =
+ "Due to table source invalid"
+ };
+ const char * const tas_current_status[TAS_DYNA_STATUS_MAX] = {
+ [TAS_DYNA_INACTIVE] = "INACTIVE",
+ [TAS_DYNA_INACTIVE_MVM_MODE] =
+ "inactive due to mvm mode",
+ [TAS_DYNA_INACTIVE_TRIGGER_MODE] =
+ "inactive due to trigger mode",
+ [TAS_DYNA_INACTIVE_BLOCK_LISTED] =
+ "inactive due to block listed",
+ [TAS_DYNA_INACTIVE_UHB_NON_US] =
+ "inactive due to uhb non US",
+ [TAS_DYNA_ACTIVE] = "ACTIVE",
+ };
+ ssize_t pos = 0;
+
+ if (resp->header.version != 1) {
+ pos += scnprintf(buf + pos, count - pos,
+ "Unsupported TAS response version:%d",
+ resp->header.version);
+ return pos;
+ }
+
+ pos += scnprintf(buf + pos, count - pos, "TAS Report\n");
+ switch (resp->tas_config_info.table_source) {
+ case BIOS_SOURCE_NONE:
+ pos += scnprintf(buf + pos, count - pos,
+ "BIOS SOURCE NONE ");
+ break;
+ case BIOS_SOURCE_ACPI:
+ pos += scnprintf(buf + pos, count - pos,
+ "BIOS SOURCE ACPI ");
+ break;
+ case BIOS_SOURCE_UEFI:
+ pos += scnprintf(buf + pos, count - pos,
+ "BIOS SOURCE UEFI ");
+ break;
+ default:
+ pos += scnprintf(buf + pos, count - pos,
+ "BIOS SOURCE UNKNOWN (%d) ",
+ resp->tas_config_info.table_source);
+ break;
+ }
+
+ pos += scnprintf(buf + pos, count - pos,
+ "revision is: %d data is: 0x%08x\n",
+ resp->tas_config_info.table_revision,
+ resp->tas_config_info.value);
+ pos += scnprintf(buf + pos, count - pos, "Current MCC: 0x%x\n",
+ le16_to_cpu(resp->curr_mcc));
+
+ pos += scnprintf(buf + pos, count - pos, "Block list entries:");
+ for (int i = 0; i < ARRAY_SIZE(resp->mcc_block_list); i++)
+ pos += scnprintf(buf + pos, count - pos, " 0x%x",
+ le16_to_cpu(resp->mcc_block_list[i]));
+
+ pos += scnprintf(buf + pos, count - pos,
+ "\nDo TAS Support Dual Radio?: %s\n",
+ hweight8(resp->valid_radio_mask) > 1 ?
+ "TRUE" : "FALSE");
+
+ for (int i = 0; i < ARRAY_SIZE(resp->tas_status_radio); i++) {
+ int tmp;
+ unsigned long dynamic_status;
+
+ if (!(resp->valid_radio_mask & BIT(i)))
+ continue;
+
+ pos += scnprintf(buf + pos, count - pos,
+ "TAS report for radio:%d\n", i + 1);
+ pos += scnprintf(buf + pos, count - pos,
+ "Static status: %sabled\n",
+ resp->tas_status_radio[i].static_status ?
+ "En" : "Dis");
+ if (!resp->tas_status_radio[i].static_status) {
+ u8 static_disable_reason =
+ resp->tas_status_radio[i].static_disable_reason;
+
+ pos += scnprintf(buf + pos, count - pos,
+ "\tStatic Disabled Reason: ");
+ if (static_disable_reason >= TAS_DISABLED_REASON_MAX) {
+ pos += scnprintf(buf + pos, count - pos,
+ "unsupported value (%d)\n",
+ static_disable_reason);
+ continue;
+ }
+
+ pos += scnprintf(buf + pos, count - pos,
+ "%s (%d)\n",
+ tas_dis_reason[static_disable_reason],
+ static_disable_reason);
+ continue;
+ }
+
+ pos += scnprintf(buf + pos, count - pos, "\tANT A %s and ",
+ (resp->tas_status_radio[i].dynamic_status_ant_a
+ & BIT(TAS_DYNA_ACTIVE)) ? "ON" : "OFF");
+
+ pos += scnprintf(buf + pos, count - pos, "ANT B %s for ",
+ (resp->tas_status_radio[i].dynamic_status_ant_b
+ & BIT(TAS_DYNA_ACTIVE)) ? "ON" : "OFF");
+
+ switch (resp->tas_status_radio[i].band) {
+ case PHY_BAND_5:
+ pos += scnprintf(buf + pos, count - pos, "HB\n");
+ break;
+ case PHY_BAND_24:
+ pos += scnprintf(buf + pos, count - pos, "LB\n");
+ break;
+ case PHY_BAND_6:
+ pos += scnprintf(buf + pos, count - pos, "UHB\n");
+ break;
+ default:
+ pos += scnprintf(buf + pos, count - pos,
+ "Unsupported band (%d)\n",
+ resp->tas_status_radio[i].band);
+ break;
+ }
+
+ pos += scnprintf(buf + pos, count - pos,
+ "Is near disconnection?: %s\n",
+ resp->tas_status_radio[i].near_disconnection ?
+ "True" : "False");
+
+ pos += scnprintf(buf + pos, count - pos,
+ "Dynamic status antenna A:\n");
+ dynamic_status = resp->tas_status_radio[i].dynamic_status_ant_a;
+ for_each_set_bit(tmp, &dynamic_status, TAS_DYNA_STATUS_MAX) {
+ pos += scnprintf(buf + pos, count - pos, "\t%s (%d)\n",
+ tas_current_status[tmp], tmp);
+ }
+ pos += scnprintf(buf + pos, count - pos,
+ "\nDynamic status antenna B:\n");
+ dynamic_status = resp->tas_status_radio[i].dynamic_status_ant_b;
+ for_each_set_bit(tmp, &dynamic_status, TAS_DYNA_STATUS_MAX) {
+ pos += scnprintf(buf + pos, count - pos, "\t%s (%d)\n",
+ tas_current_status[tmp], tmp);
+ }
+
+ tmp = le16_to_cpu(resp->tas_status_radio[i].max_reg_pwr_limit_ant_a);
+ pos += scnprintf(buf + pos, count - pos,
+ "Max antenna A regulatory pwr limit (dBm): %d.%03d\n",
+ tmp / 8, 125 * (tmp % 8));
+ tmp = le16_to_cpu(resp->tas_status_radio[i].max_reg_pwr_limit_ant_b);
+ pos += scnprintf(buf + pos, count - pos,
+ "Max antenna B regulatory pwr limit (dBm): %d.%03d\n",
+ tmp / 8, 125 * (tmp % 8));
+
+ tmp = le16_to_cpu(resp->tas_status_radio[i].sar_limit_ant_a);
+ pos += scnprintf(buf + pos, count - pos,
+ "Antenna A SAR limit (dBm): %d.%03d\n",
+ tmp / 8, 125 * (tmp % 8));
+ tmp = le16_to_cpu(resp->tas_status_radio[i].sar_limit_ant_b);
+ pos += scnprintf(buf + pos, count - pos,
+ "Antenna B SAR limit (dBm): %d.%03d\n",
+ tmp / 8, 125 * (tmp % 8));
+ }
+
+ return pos;
+}
+
+static ssize_t iwl_dbgfs_tas_get_status_read(struct iwl_mld *mld, char *buf,
+ size_t count)
+{
+ struct iwl_dhc_cmd cmd = {
+ .index_and_mask = cpu_to_le32(DHC_TABLE_TOOLS |
+ DHC_TARGET_UMAC |
+ DHC_TOOLS_UMAC_GET_TAS_STATUS),
+ };
+ struct iwl_host_cmd hcmd = {
+ .id = WIDE_ID(LEGACY_GROUP, DEBUG_HOST_COMMAND),
+ .flags = CMD_WANT_SKB,
+ .len[0] = sizeof(cmd),
+ .data[0] = &cmd,
+ };
+ struct iwl_dhc_tas_status_resp *resp = NULL;
+ ssize_t pos = 0;
+ u32 resp_len;
+ u32 status;
+ int ret;
+
+ if (iwl_mld_dbgfs_fw_cmd_disabled(mld))
+ return -EIO;
+
+ ret = iwl_mld_send_cmd(mld, &hcmd);
+ if (ret)
+ return ret;
+
+ pos += scnprintf(buf + pos, count - pos, "\nOEM name: %s\n",
+ dmi_get_system_info(DMI_SYS_VENDOR) ?: "<unknown>");
+ pos += scnprintf(buf + pos, count - pos,
+ "\tVendor In Approved List: %s\n",
+ iwl_is_tas_approved() ? "YES" : "NO");
+
+ status = iwl_dhc_resp_status(mld->fwrt.fw, hcmd.resp_pkt);
+ if (status != 1) {
+ pos += scnprintf(buf + pos, count - pos,
+ "response status is not success: %d\n",
+ status);
+ goto out;
+ }
+
+ resp = iwl_dhc_resp_data(mld->fwrt.fw, hcmd.resp_pkt, &resp_len);
+ if (IS_ERR(resp) || resp_len != sizeof(*resp)) {
+ pos += scnprintf(buf + pos, count - pos,
+ "Invalid size for TAS response (%u instead of %zd)\n",
+ resp_len, sizeof(*resp));
+ goto out;
+ }
+
+ pos += iwl_mld_dump_tas_resp(resp, count - pos, buf + pos);
+
+out:
+ iwl_free_resp(&hcmd);
+ return pos;
+}
+
WIPHY_DEBUGFS_WRITE_FILE_OPS_MLD(fw_nmi, 10);
WIPHY_DEBUGFS_WRITE_FILE_OPS_MLD(fw_restart, 10);
WIPHY_DEBUGFS_READ_WRITE_FILE_OPS_MLD(he_sniffer_params, 32);
WIPHY_DEBUGFS_WRITE_FILE_OPS_MLD(fw_dbg_clear, 10);
WIPHY_DEBUGFS_WRITE_FILE_OPS_MLD(send_echo_cmd, 8);
+WIPHY_DEBUGFS_READ_FILE_OPS_MLD(tas_get_status, 2048);
static ssize_t iwl_dbgfs_wifi_6e_enable_read(struct iwl_mld *mld,
size_t count, u8 *buf)
@@ -307,6 +538,7 @@ iwl_mld_add_debugfs_files(struct iwl_mld *mld, struct dentry *debugfs_dir)
MLD_DEBUGFS_ADD_FILE(he_sniffer_params, debugfs_dir, 0600);
MLD_DEBUGFS_ADD_FILE(fw_dbg_clear, debugfs_dir, 0200);
MLD_DEBUGFS_ADD_FILE(send_echo_cmd, debugfs_dir, 0200);
+ MLD_DEBUGFS_ADD_FILE(tas_get_status, debugfs_dir, 0400);
#ifdef CONFIG_THERMAL
MLD_DEBUGFS_ADD_FILE(start_ctdp, debugfs_dir, 0200);
MLD_DEBUGFS_ADD_FILE(stop_ctdp, debugfs_dir, 0200);