450 lines
11 KiB
C
450 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
|
|
/*
|
|
* Copyright (C) 2023, 2025-2026 Intel Corporation
|
|
*/
|
|
#include <linux/dmi.h>
|
|
#include "iwl-drv.h"
|
|
#include "iwl-debug.h"
|
|
#include "regulatory.h"
|
|
#include "fw/runtime.h"
|
|
#include "fw/uefi.h"
|
|
|
|
#define GET_BIOS_TABLE(__name, ...) \
|
|
do { \
|
|
int ret = -ENOENT; \
|
|
if (fwrt->uefi_tables_lock_status > UEFI_WIFI_GUID_UNLOCKED) \
|
|
ret = iwl_uefi_get_ ## __name(__VA_ARGS__); \
|
|
if (ret < 0) \
|
|
ret = iwl_acpi_get_ ## __name(__VA_ARGS__); \
|
|
return ret; \
|
|
} while (0)
|
|
|
|
#define IWL_BIOS_TABLE_LOADER(__name) \
|
|
int iwl_bios_get_ ## __name(struct iwl_fw_runtime *fwrt) \
|
|
{GET_BIOS_TABLE(__name, fwrt); } \
|
|
IWL_EXPORT_SYMBOL(iwl_bios_get_ ## __name)
|
|
|
|
#define IWL_BIOS_TABLE_LOADER_DATA(__name, data_type) \
|
|
int iwl_bios_get_ ## __name(struct iwl_fw_runtime *fwrt, \
|
|
data_type * data) \
|
|
{GET_BIOS_TABLE(__name, fwrt, data); } \
|
|
IWL_EXPORT_SYMBOL(iwl_bios_get_ ## __name)
|
|
|
|
IWL_BIOS_TABLE_LOADER(wrds_table);
|
|
IWL_BIOS_TABLE_LOADER(ewrd_table);
|
|
IWL_BIOS_TABLE_LOADER(wgds_table);
|
|
IWL_BIOS_TABLE_LOADER(ppag_table);
|
|
IWL_BIOS_TABLE_LOADER(phy_filters);
|
|
IWL_BIOS_TABLE_LOADER_DATA(tas_table, struct iwl_tas_data);
|
|
IWL_BIOS_TABLE_LOADER_DATA(pwr_limit, u64);
|
|
IWL_BIOS_TABLE_LOADER_DATA(mcc, char);
|
|
IWL_BIOS_TABLE_LOADER_DATA(eckv, u32);
|
|
IWL_BIOS_TABLE_LOADER_DATA(wbem, u32);
|
|
IWL_BIOS_TABLE_LOADER_DATA(dsbr, u32);
|
|
|
|
|
|
static const struct dmi_system_id dmi_ppag_approved_list[] = {
|
|
{ .ident = "HP",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "HP"),
|
|
},
|
|
},
|
|
{ .ident = "SAMSUNG",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG ELECTRONICS CO., LTD"),
|
|
},
|
|
},
|
|
{ .ident = "MSFT",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
|
|
},
|
|
},
|
|
{ .ident = "ASUSTEK",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."),
|
|
},
|
|
},
|
|
{ .ident = "ASUS",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "ASUS"),
|
|
},
|
|
},
|
|
{ .ident = "GOOGLE-HP",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Google"),
|
|
DMI_MATCH(DMI_BOARD_VENDOR, "HP"),
|
|
},
|
|
},
|
|
{ .ident = "GOOGLE-ASUS",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Google"),
|
|
DMI_MATCH(DMI_BOARD_VENDOR, "ASUSTek COMPUTER INC."),
|
|
},
|
|
},
|
|
{ .ident = "GOOGLE-SAMSUNG",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Google"),
|
|
DMI_MATCH(DMI_BOARD_VENDOR, "SAMSUNG ELECTRONICS CO., LTD"),
|
|
},
|
|
},
|
|
{ .ident = "DELL",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."),
|
|
},
|
|
},
|
|
{ .ident = "DELL",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Alienware"),
|
|
},
|
|
},
|
|
{ .ident = "RAZER",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Razer"),
|
|
},
|
|
},
|
|
{ .ident = "Honor",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "HONOR"),
|
|
},
|
|
},
|
|
{ .ident = "WIKO",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "WIKO"),
|
|
},
|
|
},
|
|
{ .ident = "XIAOMI",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "XIAOMI"),
|
|
},
|
|
},
|
|
{}
|
|
};
|
|
|
|
static const struct dmi_system_id dmi_tas_approved_list[] = {
|
|
{ .ident = "HP",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "HP"),
|
|
},
|
|
},
|
|
{ .ident = "SAMSUNG",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "SAMSUNG ELECTRONICS CO., LTD"),
|
|
},
|
|
},
|
|
{ .ident = "LENOVO",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
|
|
},
|
|
},
|
|
{ .ident = "DELL",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."),
|
|
},
|
|
},
|
|
{ .ident = "MSFT",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation"),
|
|
},
|
|
},
|
|
{ .ident = "Acer",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Acer"),
|
|
},
|
|
},
|
|
{ .ident = "ASUSTEK",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."),
|
|
},
|
|
},
|
|
{ .ident = "ASUS",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "ASUS"),
|
|
},
|
|
},
|
|
{ .ident = "GOOGLE-HP",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Google"),
|
|
DMI_MATCH(DMI_BOARD_VENDOR, "HP"),
|
|
},
|
|
},
|
|
{ .ident = "MSI",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."),
|
|
},
|
|
},
|
|
{ .ident = "Honor",
|
|
.matches = {
|
|
DMI_MATCH(DMI_SYS_VENDOR, "HONOR"),
|
|
},
|
|
},
|
|
/* keep last */
|
|
{}
|
|
};
|
|
|
|
bool iwl_sar_geo_support(struct iwl_fw_runtime *fwrt)
|
|
{
|
|
/*
|
|
* The PER_CHAIN_LIMIT_OFFSET_CMD command is not supported on
|
|
* earlier firmware versions. Unfortunately, we don't have a
|
|
* TLV API flag to rely on, so rely on the major version which
|
|
* is in the first byte of ucode_ver. This was implemented
|
|
* initially on version 38 and then backported to 17. It was
|
|
* also backported to 29, but only for 7265D devices. The
|
|
* intention was to have it in 36 as well, but not all 8000
|
|
* family got this feature enabled. The 8000 family is the
|
|
* only one using version 36, so skip this version entirely.
|
|
*/
|
|
return IWL_UCODE_SERIAL(fwrt->fw->ucode_ver) >= 38 ||
|
|
(IWL_UCODE_SERIAL(fwrt->fw->ucode_ver) == 17 &&
|
|
fwrt->trans->info.hw_rev != CSR_HW_REV_TYPE_3160) ||
|
|
(IWL_UCODE_SERIAL(fwrt->fw->ucode_ver) == 29 &&
|
|
((fwrt->trans->info.hw_rev & CSR_HW_REV_TYPE_MSK) ==
|
|
CSR_HW_REV_TYPE_7265D));
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_sar_geo_support);
|
|
|
|
int iwl_sar_geo_fill_table(struct iwl_fw_runtime *fwrt,
|
|
struct iwl_per_chain_offset *table,
|
|
u32 n_bands, u32 n_profiles)
|
|
{
|
|
int i, j;
|
|
|
|
if (!fwrt->geo_enabled)
|
|
return -ENODATA;
|
|
|
|
if (!iwl_sar_geo_support(fwrt))
|
|
return -EOPNOTSUPP;
|
|
|
|
for (i = 0; i < n_profiles; i++) {
|
|
for (j = 0; j < n_bands; j++) {
|
|
struct iwl_per_chain_offset *chain =
|
|
&table[i * n_bands + j];
|
|
|
|
chain->max_tx_power =
|
|
cpu_to_le16(fwrt->geo_profiles[i].bands[j].max);
|
|
chain->chain_a =
|
|
fwrt->geo_profiles[i].bands[j].chains[0];
|
|
chain->chain_b =
|
|
fwrt->geo_profiles[i].bands[j].chains[1];
|
|
IWL_DEBUG_RADIO(fwrt,
|
|
"SAR geographic profile[%d] Band[%d]: chain A = %d chain B = %d max_tx_power = %d\n",
|
|
i, j,
|
|
fwrt->geo_profiles[i].bands[j].chains[0],
|
|
fwrt->geo_profiles[i].bands[j].chains[1],
|
|
fwrt->geo_profiles[i].bands[j].max);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_sar_geo_fill_table);
|
|
|
|
static int iwl_sar_fill_table(struct iwl_fw_runtime *fwrt,
|
|
__le16 *per_chain, u32 n_subbands,
|
|
int prof_a, int prof_b)
|
|
{
|
|
int profs[BIOS_SAR_NUM_CHAINS] = { prof_a, prof_b };
|
|
int i, j;
|
|
|
|
if (WARN_ON_ONCE(n_subbands >
|
|
ARRAY_SIZE(fwrt->sar_profiles[0].chains[0].subbands)))
|
|
return -EINVAL;
|
|
|
|
for (i = 0; i < BIOS_SAR_NUM_CHAINS; i++) {
|
|
struct iwl_sar_profile *prof;
|
|
|
|
/* don't allow SAR to be disabled (profile 0 means disable) */
|
|
if (profs[i] == 0)
|
|
return -EPERM;
|
|
|
|
/* we are off by one, so allow up to BIOS_SAR_MAX_PROFILE_NUM */
|
|
if (profs[i] > BIOS_SAR_MAX_PROFILE_NUM)
|
|
return -EINVAL;
|
|
|
|
/* profiles go from 1 to 4, so decrement to access the array */
|
|
prof = &fwrt->sar_profiles[profs[i] - 1];
|
|
|
|
/* if the profile is disabled, do nothing */
|
|
if (!prof->enabled) {
|
|
IWL_DEBUG_RADIO(fwrt, "SAR profile %d is disabled.\n",
|
|
profs[i]);
|
|
/*
|
|
* if one of the profiles is disabled, we
|
|
* ignore all of them and return 1 to
|
|
* differentiate disabled from other failures.
|
|
*/
|
|
return 1;
|
|
}
|
|
|
|
IWL_DEBUG_INFO(fwrt,
|
|
"SAR EWRD: chain %d profile index %d\n",
|
|
i, profs[i]);
|
|
IWL_DEBUG_RADIO(fwrt, " Chain[%d]:\n", i);
|
|
for (j = 0; j < n_subbands; j++) {
|
|
per_chain[i * n_subbands + j] =
|
|
cpu_to_le16(prof->chains[i].subbands[j]);
|
|
IWL_DEBUG_RADIO(fwrt, " Band[%d] = %d * .125dBm\n",
|
|
j, prof->chains[i].subbands[j]);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int iwl_sar_fill_profile(struct iwl_fw_runtime *fwrt,
|
|
__le16 *per_chain, u32 n_tables, u32 n_subbands,
|
|
int prof_a, int prof_b)
|
|
{
|
|
int i, ret = 0;
|
|
|
|
for (i = 0; i < n_tables; i++) {
|
|
ret = iwl_sar_fill_table(fwrt,
|
|
&per_chain[i * n_subbands * BIOS_SAR_NUM_CHAINS],
|
|
n_subbands, prof_a, prof_b);
|
|
if (ret)
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_sar_fill_profile);
|
|
|
|
bool iwl_is_ppag_approved(struct iwl_fw_runtime *fwrt)
|
|
{
|
|
if (!dmi_check_system(dmi_ppag_approved_list)) {
|
|
IWL_DEBUG_RADIO(fwrt,
|
|
"System vendor '%s' is not in the approved list, disabling PPAG.\n",
|
|
dmi_get_system_info(DMI_SYS_VENDOR) ?: "<unknown>");
|
|
fwrt->ppag_flags = 0;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_is_ppag_approved);
|
|
|
|
/* Print the PPAG table as read from BIOS */
|
|
void iwl_bios_print_ppag(struct iwl_fw_runtime *fwrt, int n_subbands)
|
|
{
|
|
int i, j;
|
|
|
|
IWL_DEBUG_RADIO(fwrt, "PPAG table as read from BIOS:\n");
|
|
IWL_DEBUG_RADIO(fwrt, "PPAG revision = %d\n", fwrt->ppag_bios_rev);
|
|
IWL_DEBUG_RADIO(fwrt, "PPAG flags = 0x%x\n", fwrt->ppag_flags);
|
|
|
|
if (WARN_ON_ONCE(n_subbands >
|
|
ARRAY_SIZE(fwrt->ppag_chains[0].subbands)))
|
|
return;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(fwrt->ppag_chains); i++)
|
|
for (j = 0; j < n_subbands; j++)
|
|
IWL_DEBUG_RADIO(fwrt,
|
|
"ppag_chains[%d].subbands[%d] = %d\n",
|
|
i, j,
|
|
fwrt->ppag_chains[i].subbands[j]);
|
|
}
|
|
|
|
bool iwl_is_tas_approved(void)
|
|
{
|
|
return dmi_check_system(dmi_tas_approved_list);
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_is_tas_approved);
|
|
|
|
struct iwl_tas_selection_data
|
|
iwl_parse_tas_selection(const u32 tas_selection_in, const u8 tbl_rev)
|
|
{
|
|
struct iwl_tas_selection_data tas_selection_out = {};
|
|
u8 override_iec = u32_get_bits(tas_selection_in,
|
|
IWL_WTAS_OVERRIDE_IEC_MSK);
|
|
u8 canada_tas_uhb = u32_get_bits(tas_selection_in,
|
|
IWL_WTAS_CANADA_UHB_MSK);
|
|
u8 enabled_iec = u32_get_bits(tas_selection_in,
|
|
IWL_WTAS_ENABLE_IEC_MSK);
|
|
u8 usa_tas_uhb = u32_get_bits(tas_selection_in,
|
|
IWL_WTAS_USA_UHB_MSK);
|
|
|
|
if (tbl_rev > 0) {
|
|
tas_selection_out.usa_tas_uhb_allowed = usa_tas_uhb;
|
|
tas_selection_out.override_tas_iec = override_iec;
|
|
tas_selection_out.enable_tas_iec = enabled_iec;
|
|
}
|
|
|
|
if (tbl_rev > 1)
|
|
tas_selection_out.canada_tas_uhb_allowed = canada_tas_uhb;
|
|
|
|
return tas_selection_out;
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_parse_tas_selection);
|
|
|
|
bool iwl_add_mcc_to_tas_block_list(u16 *list, u8 *size, u16 mcc)
|
|
{
|
|
for (int i = 0; i < *size; i++) {
|
|
if (list[i] == mcc)
|
|
return true;
|
|
}
|
|
|
|
/* Verify that there is room for another country
|
|
* If *size == IWL_WTAS_BLACK_LIST_MAX, then the table is full.
|
|
*/
|
|
if (*size >= IWL_WTAS_BLACK_LIST_MAX)
|
|
return false;
|
|
|
|
list[(*size)++] = mcc;
|
|
return true;
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_add_mcc_to_tas_block_list);
|
|
|
|
int iwl_bios_get_dsm(struct iwl_fw_runtime *fwrt, enum iwl_dsm_funcs func,
|
|
u32 *value)
|
|
{
|
|
GET_BIOS_TABLE(dsm, fwrt, func, value);
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_bios_get_dsm);
|
|
|
|
bool iwl_puncturing_is_allowed_in_bios(u32 puncturing, u16 mcc)
|
|
{
|
|
/* Some kind of regulatory mess means we need to currently disallow
|
|
* puncturing in the US and Canada unless enabled in BIOS.
|
|
*/
|
|
switch (mcc) {
|
|
case IWL_MCC_US:
|
|
return puncturing & IWL_UEFI_CNV_PUNCTURING_USA_EN_MSK;
|
|
case IWL_MCC_CANADA:
|
|
return puncturing & IWL_UEFI_CNV_PUNCTURING_CANADA_EN_MSK;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_puncturing_is_allowed_in_bios);
|
|
|
|
bool iwl_rfi_is_enabled_in_bios(struct iwl_fw_runtime *fwrt)
|
|
{
|
|
/* default behaviour is disabled */
|
|
u32 value = 0;
|
|
int ret = iwl_bios_get_dsm(fwrt, DSM_FUNC_RFI_CONFIG, &value);
|
|
|
|
if (ret < 0) {
|
|
IWL_DEBUG_RADIO(fwrt, "Failed to get DSM RFI, ret=%d\n", ret);
|
|
return false;
|
|
}
|
|
|
|
value &= DSM_VALUE_RFI_DISABLE;
|
|
/* RFI BIOS CONFIG value can be 0 or 3 only.
|
|
* i.e 0 means DDR and DLVR enabled. 3 means DDR and DLVR disabled.
|
|
* 1 and 2 are invalid BIOS configurations, So, it's not possible to
|
|
* disable ddr/dlvr separately.
|
|
*/
|
|
if (!value) {
|
|
IWL_DEBUG_RADIO(fwrt, "DSM RFI is evaluated to enable\n");
|
|
return true;
|
|
} else if (value == DSM_VALUE_RFI_DISABLE) {
|
|
IWL_DEBUG_RADIO(fwrt, "DSM RFI is evaluated to disable\n");
|
|
} else {
|
|
IWL_DEBUG_RADIO(fwrt,
|
|
"DSM RFI got invalid value, value=%d\n", value);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
IWL_EXPORT_SYMBOL(iwl_rfi_is_enabled_in_bios);
|