Files
linux/drivers/net/wireless/silabs/wfx/main.c
Marco Crivellari 2112519bdf wifi: wfx: add WQ_PERCPU to alloc_workqueue users
Currently if a user enqueues a work item using schedule_delayed_work() the
used wq is "system_wq" (per-cpu wq) while queue_delayed_work() use
WORK_CPU_UNBOUND (used when a cpu is not specified). The same applies to
schedule_work() that is using system_wq and queue_work(), that makes use
again of WORK_CPU_UNBOUND.
This lack of consistency cannot be addressed without refactoring the API.
For more details see the Link tag below.

alloc_workqueue() treats all queues as per-CPU by default, while unbound
workqueues must opt-in via WQ_UNBOUND.

This default is suboptimal: most workloads benefit from unbound queues,
allowing the scheduler to place worker threads where they’re needed and
reducing noise when CPUs are isolated.

This continues the effort to refactor workqueue APIs, which began with
the introduction of new workqueues and a new alloc_workqueue flag in:

commit 128ea9f6cc ("workqueue: Add system_percpu_wq and system_dfl_wq")
commit 930c2ea566 ("workqueue: Add new WQ_PERCPU flag")

This change adds a new WQ_PERCPU flag to explicitly request
alloc_workqueue() to be per-cpu when WQ_UNBOUND has not been specified.

With the introduction of the WQ_PERCPU flag (equivalent to !WQ_UNBOUND),
any alloc_workqueue() caller that doesn’t explicitly specify WQ_UNBOUND
must now use WQ_PERCPU.

Once migration is complete, WQ_UNBOUND can be removed and unbound will
become the implicit default.

Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Marco Crivellari <marco.crivellari@suse.com>
Link: https://lore.kernel.org/all/20250221112003.1dSuoGyc@linutronix.de/
Reviewed-by: Jérôme Pouiller <jerome.pouiller@silabs.com>
Link: https://patch.msgid.link/20251113160825.383883-1-marco.crivellari@suse.com
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
2025-11-20 10:25:46 +01:00

526 lines
16 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Device probe and register.
*
* Copyright (c) 2017-2020, Silicon Laboratories, Inc.
* Copyright (c) 2010, ST-Ericsson
* Copyright (c) 2008, Johannes Berg <johannes@sipsolutions.net>
* Copyright (c) 2008 Nokia Corporation and/or its subsidiary(-ies).
* Copyright (c) 2007-2009, Christian Lamparter <chunkeey@web.de>
* Copyright (c) 2006, Michael Wu <flamingice@sourmilk.net>
* Copyright (c) 2004-2006 Jean-Baptiste Note <jbnote@gmail.com>, et al.
*/
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_net.h>
#include <linux/gpio/consumer.h>
#include <linux/mmc/sdio_func.h>
#include <linux/spi/spi.h>
#include <linux/etherdevice.h>
#include <linux/firmware.h>
#include "main.h"
#include "wfx.h"
#include "fwio.h"
#include "hwio.h"
#include "bus.h"
#include "bh.h"
#include "sta.h"
#include "key.h"
#include "scan.h"
#include "debug.h"
#include "data_tx.h"
#include "hif_tx_mib.h"
#include "hif_api_cmd.h"
#define WFX_PDS_TLV_TYPE 0x4450 // "PD" (Platform Data) in ascii little-endian
#define WFX_PDS_MAX_CHUNK_SIZE 1500
MODULE_DESCRIPTION("Silicon Labs 802.11 Wireless LAN driver for WF200");
MODULE_AUTHOR("Jérôme Pouiller <jerome.pouiller@silabs.com>");
MODULE_LICENSE("GPL");
#define RATETAB_ENT(_rate, _rateid, _flags) { \
.bitrate = (_rate), \
.hw_value = (_rateid), \
.flags = (_flags), \
}
static struct ieee80211_rate wfx_rates[] = {
RATETAB_ENT(10, 0, 0),
RATETAB_ENT(20, 1, IEEE80211_RATE_SHORT_PREAMBLE),
RATETAB_ENT(55, 2, IEEE80211_RATE_SHORT_PREAMBLE),
RATETAB_ENT(110, 3, IEEE80211_RATE_SHORT_PREAMBLE),
RATETAB_ENT(60, 6, 0),
RATETAB_ENT(90, 7, 0),
RATETAB_ENT(120, 8, 0),
RATETAB_ENT(180, 9, 0),
RATETAB_ENT(240, 10, 0),
RATETAB_ENT(360, 11, 0),
RATETAB_ENT(480, 12, 0),
RATETAB_ENT(540, 13, 0),
};
#define CHAN2G(_channel, _freq, _flags) { \
.band = NL80211_BAND_2GHZ, \
.center_freq = (_freq), \
.hw_value = (_channel), \
.flags = (_flags), \
.max_antenna_gain = 0, \
.max_power = 30, \
}
static struct ieee80211_channel wfx_2ghz_chantable[] = {
CHAN2G(1, 2412, 0),
CHAN2G(2, 2417, 0),
CHAN2G(3, 2422, 0),
CHAN2G(4, 2427, 0),
CHAN2G(5, 2432, 0),
CHAN2G(6, 2437, 0),
CHAN2G(7, 2442, 0),
CHAN2G(8, 2447, 0),
CHAN2G(9, 2452, 0),
CHAN2G(10, 2457, 0),
CHAN2G(11, 2462, 0),
CHAN2G(12, 2467, 0),
CHAN2G(13, 2472, 0),
CHAN2G(14, 2484, 0),
};
static const struct ieee80211_supported_band wfx_band_2ghz = {
.channels = wfx_2ghz_chantable,
.n_channels = ARRAY_SIZE(wfx_2ghz_chantable),
.bitrates = wfx_rates,
.n_bitrates = ARRAY_SIZE(wfx_rates),
.ht_cap = {
/* Receive caps */
.cap = IEEE80211_HT_CAP_GRN_FLD | IEEE80211_HT_CAP_SGI_20 |
IEEE80211_HT_CAP_MAX_AMSDU | (1 << IEEE80211_HT_CAP_RX_STBC_SHIFT),
.ht_supported = 1,
.ampdu_factor = IEEE80211_HT_MAX_AMPDU_16K,
.ampdu_density = IEEE80211_HT_MPDU_DENSITY_NONE,
.mcs = {
.rx_mask = { 0xFF }, /* MCS0 to MCS7 */
.rx_highest = cpu_to_le16(72),
.tx_params = IEEE80211_HT_MCS_TX_DEFINED,
},
},
};
static const struct ieee80211_iface_limit wdev_iface_limits[] = {
{ .max = 1, .types = BIT(NL80211_IFTYPE_STATION) },
{ .max = 1, .types = BIT(NL80211_IFTYPE_AP) },
};
static const struct ieee80211_iface_combination wfx_iface_combinations[] = {
{
.num_different_channels = 2,
.max_interfaces = 2,
.limits = wdev_iface_limits,
.n_limits = ARRAY_SIZE(wdev_iface_limits),
}
};
#ifdef CONFIG_PM
static const struct wiphy_wowlan_support wfx_wowlan_support = {
.flags = WIPHY_WOWLAN_ANY | WIPHY_WOWLAN_DISCONNECT,
};
#endif
static const struct ieee80211_ops wfx_ops = {
.start = wfx_start,
.stop = wfx_stop,
.add_interface = wfx_add_interface,
.remove_interface = wfx_remove_interface,
.config = wfx_config,
.tx = wfx_tx,
.wake_tx_queue = ieee80211_handle_wake_tx_queue,
.join_ibss = wfx_join_ibss,
.leave_ibss = wfx_leave_ibss,
.conf_tx = wfx_conf_tx,
.hw_scan = wfx_hw_scan,
.cancel_hw_scan = wfx_cancel_hw_scan,
.start_ap = wfx_start_ap,
.stop_ap = wfx_stop_ap,
.sta_add = wfx_sta_add,
.sta_remove = wfx_sta_remove,
.set_tim = wfx_set_tim,
.set_key = wfx_set_key,
.set_rts_threshold = wfx_set_rts_threshold,
.set_default_unicast_key = wfx_set_default_unicast_key,
.bss_info_changed = wfx_bss_info_changed,
.configure_filter = wfx_configure_filter,
.ampdu_action = wfx_ampdu_action,
.flush = wfx_flush,
.add_chanctx = wfx_add_chanctx,
.remove_chanctx = wfx_remove_chanctx,
.change_chanctx = wfx_change_chanctx,
.assign_vif_chanctx = wfx_assign_vif_chanctx,
.unassign_vif_chanctx = wfx_unassign_vif_chanctx,
.remain_on_channel = wfx_remain_on_channel,
.cancel_remain_on_channel = wfx_cancel_remain_on_channel,
#ifdef CONFIG_PM
.suspend = wfx_suspend,
.resume = wfx_resume,
.set_wakeup = wfx_set_wakeup,
#endif
};
bool wfx_api_older_than(struct wfx_dev *wdev, int major, int minor)
{
if (wdev->hw_caps.api_version_major < major)
return true;
if (wdev->hw_caps.api_version_major > major)
return false;
if (wdev->hw_caps.api_version_minor < minor)
return true;
return false;
}
/* The device needs data about the antenna configuration. This information in provided by PDS
* (Platform Data Set, this is the wording used in WF200 documentation) files. For hardware
* integrators, the full process to create PDS files is described here:
* https://github.com/SiliconLabs/wfx-firmware/blob/master/PDS/README.md
*
* The PDS file is an array of Time-Length-Value structs.
*/
int wfx_send_pds(struct wfx_dev *wdev, u8 *buf, size_t len)
{
int ret, chunk_type, chunk_len, chunk_num = 0;
if (*buf == '{') {
dev_err(wdev->dev, "PDS: malformed file (legacy format?)\n");
return -EINVAL;
}
while (len > 0) {
chunk_type = get_unaligned_le16(buf + 0);
chunk_len = get_unaligned_le16(buf + 2);
if (chunk_len < 4 || chunk_len > len) {
dev_err(wdev->dev, "PDS:%d: corrupted file\n", chunk_num);
return -EINVAL;
}
if (chunk_type != WFX_PDS_TLV_TYPE) {
dev_info(wdev->dev, "PDS:%d: skip unknown data\n", chunk_num);
goto next;
}
if (chunk_len > WFX_PDS_MAX_CHUNK_SIZE)
dev_warn(wdev->dev, "PDS:%d: unexpectedly large chunk\n", chunk_num);
if (buf[4] != '{' || buf[chunk_len - 1] != '}')
dev_warn(wdev->dev, "PDS:%d: unexpected content\n", chunk_num);
ret = wfx_hif_configuration(wdev, buf + 4, chunk_len - 4);
if (ret > 0) {
dev_err(wdev->dev, "PDS:%d: invalid data (unsupported options?)\n", chunk_num);
return -EINVAL;
}
if (ret == -ETIMEDOUT) {
dev_err(wdev->dev, "PDS:%d: chip didn't reply (corrupted file?)\n", chunk_num);
return ret;
}
if (ret) {
dev_err(wdev->dev, "PDS:%d: chip returned an unknown error\n", chunk_num);
return -EIO;
}
next:
chunk_num++;
len -= chunk_len;
buf += chunk_len;
}
return 0;
}
static int wfx_send_pdata_pds(struct wfx_dev *wdev)
{
int ret = 0;
const struct firmware *pds;
u8 *tmp_buf;
ret = request_firmware(&pds, wdev->pdata.file_pds, wdev->dev);
if (ret) {
dev_err(wdev->dev, "can't load antenna parameters (PDS file %s). The device may be unstable.\n",
wdev->pdata.file_pds);
return ret;
}
tmp_buf = kmemdup(pds->data, pds->size, GFP_KERNEL);
if (!tmp_buf) {
ret = -ENOMEM;
goto release_fw;
}
ret = wfx_send_pds(wdev, tmp_buf, pds->size);
kfree(tmp_buf);
release_fw:
release_firmware(pds);
return ret;
}
static void wfx_free_common(void *data)
{
struct wfx_dev *wdev = data;
mutex_destroy(&wdev->tx_power_loop_info_lock);
mutex_destroy(&wdev->rx_stats_lock);
mutex_destroy(&wdev->scan_lock);
mutex_destroy(&wdev->conf_mutex);
ieee80211_free_hw(wdev->hw);
}
struct wfx_dev *wfx_init_common(struct device *dev, const struct wfx_platform_data *pdata,
const struct wfx_hwbus_ops *hwbus_ops, void *hwbus_priv)
{
struct ieee80211_hw *hw;
struct wfx_dev *wdev;
hw = ieee80211_alloc_hw(sizeof(struct wfx_dev), &wfx_ops);
if (!hw)
return NULL;
SET_IEEE80211_DEV(hw, dev);
ieee80211_hw_set(hw, TX_AMPDU_SETUP_IN_HW);
ieee80211_hw_set(hw, AMPDU_AGGREGATION);
ieee80211_hw_set(hw, CONNECTION_MONITOR);
ieee80211_hw_set(hw, REPORTS_TX_ACK_STATUS);
ieee80211_hw_set(hw, SUPPORTS_DYNAMIC_PS);
ieee80211_hw_set(hw, SIGNAL_DBM);
ieee80211_hw_set(hw, SUPPORTS_PS);
ieee80211_hw_set(hw, MFP_CAPABLE);
hw->vif_data_size = sizeof(struct wfx_vif);
hw->sta_data_size = sizeof(struct wfx_sta_priv);
hw->queues = 4;
hw->max_rates = 8;
hw->max_rate_tries = 8;
hw->extra_tx_headroom = sizeof(struct wfx_hif_msg) + sizeof(struct wfx_hif_req_tx) +
4 /* alignment */ + 8 /* TKIP IV */;
hw->wiphy->interface_modes = BIT(NL80211_IFTYPE_STATION) |
BIT(NL80211_IFTYPE_ADHOC) |
BIT(NL80211_IFTYPE_AP);
hw->wiphy->probe_resp_offload = NL80211_PROBE_RESP_OFFLOAD_SUPPORT_WPS |
NL80211_PROBE_RESP_OFFLOAD_SUPPORT_WPS2 |
NL80211_PROBE_RESP_OFFLOAD_SUPPORT_P2P |
NL80211_PROBE_RESP_OFFLOAD_SUPPORT_80211U;
hw->wiphy->features |= NL80211_FEATURE_AP_SCAN;
#ifdef CONFIG_PM
hw->wiphy->wowlan = &wfx_wowlan_support;
#endif
hw->wiphy->flags |= WIPHY_FLAG_AP_PROBE_RESP_OFFLOAD;
hw->wiphy->flags |= WIPHY_FLAG_AP_UAPSD;
hw->wiphy->max_remain_on_channel_duration = 5000;
hw->wiphy->max_ap_assoc_sta = HIF_LINK_ID_MAX;
hw->wiphy->max_scan_ssids = 2;
hw->wiphy->max_scan_ie_len = IEEE80211_MAX_DATA_LEN;
hw->wiphy->n_iface_combinations = ARRAY_SIZE(wfx_iface_combinations);
hw->wiphy->iface_combinations = wfx_iface_combinations;
/* FIXME: also copy wfx_rates and wfx_2ghz_chantable */
hw->wiphy->bands[NL80211_BAND_2GHZ] = devm_kmemdup(dev, &wfx_band_2ghz,
sizeof(wfx_band_2ghz), GFP_KERNEL);
if (!hw->wiphy->bands[NL80211_BAND_2GHZ])
goto err;
wdev = hw->priv;
wdev->hw = hw;
wdev->dev = dev;
wdev->hwbus_ops = hwbus_ops;
wdev->hwbus_priv = hwbus_priv;
memcpy(&wdev->pdata, pdata, sizeof(*pdata));
of_property_read_string(dev->of_node, "silabs,antenna-config-file", &wdev->pdata.file_pds);
wdev->pdata.gpio_wakeup = devm_gpiod_get_optional(dev, "wakeup", GPIOD_OUT_LOW);
if (IS_ERR(wdev->pdata.gpio_wakeup))
goto err;
if (wdev->pdata.gpio_wakeup)
gpiod_set_consumer_name(wdev->pdata.gpio_wakeup, "wfx wakeup");
mutex_init(&wdev->conf_mutex);
mutex_init(&wdev->scan_lock);
mutex_init(&wdev->rx_stats_lock);
mutex_init(&wdev->tx_power_loop_info_lock);
init_completion(&wdev->firmware_ready);
INIT_DELAYED_WORK(&wdev->cooling_timeout_work, wfx_cooling_timeout_work);
skb_queue_head_init(&wdev->tx_pending);
init_waitqueue_head(&wdev->tx_dequeue);
wfx_init_hif_cmd(&wdev->hif_cmd);
if (devm_add_action_or_reset(dev, wfx_free_common, wdev))
return NULL;
return wdev;
err:
ieee80211_free_hw(hw);
return NULL;
}
int wfx_probe(struct wfx_dev *wdev)
{
int i;
int err;
struct gpio_desc *gpio_saved;
/* During first part of boot, gpio_wakeup cannot yet been used. So prevent bh() to touch
* it.
*/
gpio_saved = wdev->pdata.gpio_wakeup;
wdev->pdata.gpio_wakeup = NULL;
wdev->poll_irq = true;
wdev->bh_wq = alloc_workqueue("wfx_bh_wq", WQ_HIGHPRI | WQ_PERCPU, 0);
if (!wdev->bh_wq)
return -ENOMEM;
wfx_bh_register(wdev);
err = wfx_init_device(wdev);
if (err)
goto bh_unregister;
wfx_bh_poll_irq(wdev);
err = wait_for_completion_timeout(&wdev->firmware_ready, 1 * HZ);
if (err == 0) {
dev_err(wdev->dev, "timeout while waiting for startup indication\n");
err = -ETIMEDOUT;
goto bh_unregister;
}
/* FIXME: fill wiphy::hw_version */
dev_info(wdev->dev, "started firmware %d.%d.%d \"%s\" (API: %d.%d, keyset: %02X, caps: 0x%.8X)\n",
wdev->hw_caps.firmware_major, wdev->hw_caps.firmware_minor,
wdev->hw_caps.firmware_build, wdev->hw_caps.firmware_label,
wdev->hw_caps.api_version_major, wdev->hw_caps.api_version_minor,
wdev->keyset, wdev->hw_caps.link_mode);
snprintf(wdev->hw->wiphy->fw_version,
sizeof(wdev->hw->wiphy->fw_version),
"%d.%d.%d",
wdev->hw_caps.firmware_major,
wdev->hw_caps.firmware_minor,
wdev->hw_caps.firmware_build);
if (wfx_api_older_than(wdev, 1, 0)) {
dev_err(wdev->dev, "unsupported firmware API version (expect 1 while firmware returns %d)\n",
wdev->hw_caps.api_version_major);
err = -EOPNOTSUPP;
goto bh_unregister;
}
if (wdev->hw_caps.link_mode == SEC_LINK_ENFORCED) {
dev_err(wdev->dev, "chip require secure_link, but can't negotiate it\n");
goto bh_unregister;
}
if (wdev->hw_caps.region_sel_mode) {
wdev->hw->wiphy->regulatory_flags |= REGULATORY_DISABLE_BEACON_HINTS;
wdev->hw->wiphy->bands[NL80211_BAND_2GHZ]->channels[11].flags |=
IEEE80211_CHAN_NO_IR;
wdev->hw->wiphy->bands[NL80211_BAND_2GHZ]->channels[12].flags |=
IEEE80211_CHAN_NO_IR;
wdev->hw->wiphy->bands[NL80211_BAND_2GHZ]->channels[13].flags |=
IEEE80211_CHAN_DISABLED;
}
dev_dbg(wdev->dev, "sending configuration file %s\n", wdev->pdata.file_pds);
err = wfx_send_pdata_pds(wdev);
if (err < 0 && err != -ENOENT)
goto bh_unregister;
wdev->poll_irq = false;
err = wdev->hwbus_ops->irq_subscribe(wdev->hwbus_priv);
if (err)
goto bh_unregister;
err = wfx_hif_use_multi_tx_conf(wdev, true);
if (err)
dev_err(wdev->dev, "misconfigured IRQ?\n");
wdev->pdata.gpio_wakeup = gpio_saved;
if (wdev->pdata.gpio_wakeup) {
dev_dbg(wdev->dev, "enable 'quiescent' power mode with wakeup GPIO and PDS file %s\n",
wdev->pdata.file_pds);
gpiod_set_value_cansleep(wdev->pdata.gpio_wakeup, 1);
wfx_control_reg_write(wdev, 0);
wfx_hif_set_operational_mode(wdev, HIF_OP_POWER_MODE_QUIESCENT);
} else {
wfx_hif_set_operational_mode(wdev, HIF_OP_POWER_MODE_DOZE);
}
for (i = 0; i < ARRAY_SIZE(wdev->addresses); i++) {
eth_zero_addr(wdev->addresses[i].addr);
err = of_get_mac_address(wdev->dev->of_node, wdev->addresses[i].addr);
if (!err)
wdev->addresses[i].addr[ETH_ALEN - 1] += i;
else
ether_addr_copy(wdev->addresses[i].addr, wdev->hw_caps.mac_addr[i]);
if (!is_valid_ether_addr(wdev->addresses[i].addr)) {
dev_warn(wdev->dev, "using random MAC address\n");
eth_random_addr(wdev->addresses[i].addr);
}
dev_info(wdev->dev, "MAC address %d: %pM\n", i, wdev->addresses[i].addr);
}
wdev->hw->wiphy->n_addresses = ARRAY_SIZE(wdev->addresses);
wdev->hw->wiphy->addresses = wdev->addresses;
if (!wfx_api_older_than(wdev, 3, 8))
wdev->hw->wiphy->flags |= WIPHY_FLAG_SUPPORTS_TDLS;
err = ieee80211_register_hw(wdev->hw);
if (err)
goto irq_unsubscribe;
err = wfx_debug_init(wdev);
if (err)
goto ieee80211_unregister;
return 0;
ieee80211_unregister:
ieee80211_unregister_hw(wdev->hw);
irq_unsubscribe:
wdev->hwbus_ops->irq_unsubscribe(wdev->hwbus_priv);
bh_unregister:
wfx_bh_unregister(wdev);
destroy_workqueue(wdev->bh_wq);
return err;
}
void wfx_release(struct wfx_dev *wdev)
{
ieee80211_unregister_hw(wdev->hw);
wfx_hif_shutdown(wdev);
wdev->hwbus_ops->irq_unsubscribe(wdev->hwbus_priv);
wfx_bh_unregister(wdev);
destroy_workqueue(wdev->bh_wq);
}
static int __init wfx_core_init(void)
{
int ret = 0;
if (IS_ENABLED(CONFIG_SPI)) {
ret = spi_register_driver(&wfx_spi_driver);
if (ret)
goto out;
}
if (IS_ENABLED(CONFIG_MMC)) {
ret = sdio_register_driver(&wfx_sdio_driver);
if (ret)
goto unregister_spi;
}
return 0;
unregister_spi:
if (IS_ENABLED(CONFIG_SPI))
spi_unregister_driver(&wfx_spi_driver);
out:
return ret;
}
module_init(wfx_core_init);
static void __exit wfx_core_exit(void)
{
if (IS_ENABLED(CONFIG_MMC))
sdio_unregister_driver(&wfx_sdio_driver);
if (IS_ENABLED(CONFIG_SPI))
spi_unregister_driver(&wfx_spi_driver);
}
module_exit(wfx_core_exit);