Commit 76015021 authored by John W. Linville's avatar John W. Linville
parents f7a01cac fded313e
...@@ -430,7 +430,7 @@ static int iwlagn_mac_suspend(struct ieee80211_hw *hw, ...@@ -430,7 +430,7 @@ static int iwlagn_mac_suspend(struct ieee80211_hw *hw,
iwl_write32(priv->trans, CSR_UCODE_DRV_GP1_SET, iwl_write32(priv->trans, CSR_UCODE_DRV_GP1_SET,
CSR_UCODE_DRV_GP1_BIT_D3_CFG_COMPLETE); CSR_UCODE_DRV_GP1_BIT_D3_CFG_COMPLETE);
iwl_trans_d3_suspend(priv->trans); iwl_trans_d3_suspend(priv->trans, false);
goto out; goto out;
...@@ -504,7 +504,7 @@ static int iwlagn_mac_resume(struct ieee80211_hw *hw) ...@@ -504,7 +504,7 @@ static int iwlagn_mac_resume(struct ieee80211_hw *hw)
/* we'll clear ctx->vif during iwlagn_prepare_restart() */ /* we'll clear ctx->vif during iwlagn_prepare_restart() */
vif = ctx->vif; vif = ctx->vif;
ret = iwl_trans_d3_resume(priv->trans, &d3_status); ret = iwl_trans_d3_resume(priv->trans, &d3_status, false);
if (ret) if (ret)
goto out_unlock; goto out_unlock;
......
...@@ -428,8 +428,9 @@ struct iwl_trans_ops { ...@@ -428,8 +428,9 @@ struct iwl_trans_ops {
void (*fw_alive)(struct iwl_trans *trans, u32 scd_addr); void (*fw_alive)(struct iwl_trans *trans, u32 scd_addr);
void (*stop_device)(struct iwl_trans *trans); void (*stop_device)(struct iwl_trans *trans);
void (*d3_suspend)(struct iwl_trans *trans); void (*d3_suspend)(struct iwl_trans *trans, bool test);
int (*d3_resume)(struct iwl_trans *trans, enum iwl_d3_status *status); int (*d3_resume)(struct iwl_trans *trans, enum iwl_d3_status *status,
bool test);
int (*send_cmd)(struct iwl_trans *trans, struct iwl_host_cmd *cmd); int (*send_cmd)(struct iwl_trans *trans, struct iwl_host_cmd *cmd);
...@@ -588,17 +589,18 @@ static inline void iwl_trans_stop_device(struct iwl_trans *trans) ...@@ -588,17 +589,18 @@ static inline void iwl_trans_stop_device(struct iwl_trans *trans)
trans->state = IWL_TRANS_NO_FW; trans->state = IWL_TRANS_NO_FW;
} }
static inline void iwl_trans_d3_suspend(struct iwl_trans *trans) static inline void iwl_trans_d3_suspend(struct iwl_trans *trans, bool test)
{ {
might_sleep(); might_sleep();
trans->ops->d3_suspend(trans); trans->ops->d3_suspend(trans, test);
} }
static inline int iwl_trans_d3_resume(struct iwl_trans *trans, static inline int iwl_trans_d3_resume(struct iwl_trans *trans,
enum iwl_d3_status *status) enum iwl_d3_status *status,
bool test)
{ {
might_sleep(); might_sleep();
return trans->ops->d3_resume(trans, status); return trans->ops->d3_resume(trans, status, test);
} }
static inline int iwl_trans_send_cmd(struct iwl_trans *trans, static inline int iwl_trans_send_cmd(struct iwl_trans *trans,
......
...@@ -174,7 +174,7 @@ static const __le32 iwl_tight_lookup[BT_COEX_LUT_SIZE] = { ...@@ -174,7 +174,7 @@ static const __le32 iwl_tight_lookup[BT_COEX_LUT_SIZE] = {
static const __le32 iwl_loose_lookup[BT_COEX_LUT_SIZE] = { static const __le32 iwl_loose_lookup[BT_COEX_LUT_SIZE] = {
cpu_to_le32(0xaaaaaaaa), cpu_to_le32(0xaaaaaaaa),
cpu_to_le32(0xaaaaaaaa), cpu_to_le32(0xaaaaaaaa),
cpu_to_le32(0xaeaaaaaa), cpu_to_le32(0xaaaaaaaa),
cpu_to_le32(0xaaaaaaaa), cpu_to_le32(0xaaaaaaaa),
cpu_to_le32(0xcc00ff28), cpu_to_le32(0xcc00ff28),
cpu_to_le32(0x0000aaaa), cpu_to_le32(0x0000aaaa),
......
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
#include <linux/etherdevice.h> #include <linux/etherdevice.h>
#include <linux/ip.h> #include <linux/ip.h>
#include <linux/fs.h>
#include <net/cfg80211.h> #include <net/cfg80211.h>
#include <net/ipv6.h> #include <net/ipv6.h>
#include <net/tcp.h> #include <net/tcp.h>
...@@ -756,7 +757,9 @@ static int iwl_mvm_d3_reprogram(struct iwl_mvm *mvm, struct ieee80211_vif *vif, ...@@ -756,7 +757,9 @@ static int iwl_mvm_d3_reprogram(struct iwl_mvm *mvm, struct ieee80211_vif *vif,
return 0; return 0;
} }
int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan) static int __iwl_mvm_suspend(struct ieee80211_hw *hw,
struct cfg80211_wowlan *wowlan,
bool test)
{ {
struct iwl_mvm *mvm = IWL_MAC80211_GET_MVM(hw); struct iwl_mvm *mvm = IWL_MAC80211_GET_MVM(hw);
struct iwl_d3_iter_data suspend_iter_data = { struct iwl_d3_iter_data suspend_iter_data = {
...@@ -769,7 +772,7 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan) ...@@ -769,7 +772,7 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan)
struct iwl_wowlan_config_cmd wowlan_config_cmd = {}; struct iwl_wowlan_config_cmd wowlan_config_cmd = {};
struct iwl_wowlan_kek_kck_material_cmd kek_kck_cmd = {}; struct iwl_wowlan_kek_kck_material_cmd kek_kck_cmd = {};
struct iwl_wowlan_tkip_params_cmd tkip_cmd = {}; struct iwl_wowlan_tkip_params_cmd tkip_cmd = {};
struct iwl_d3_manager_config d3_cfg_cmd = { struct iwl_d3_manager_config d3_cfg_cmd_data = {
/* /*
* Program the minimum sleep time to 10 seconds, as many * Program the minimum sleep time to 10 seconds, as many
* platforms have issues processing a wakeup signal while * platforms have issues processing a wakeup signal while
...@@ -777,17 +780,30 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan) ...@@ -777,17 +780,30 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan)
*/ */
.min_sleep_time = cpu_to_le32(10 * 1000 * 1000), .min_sleep_time = cpu_to_le32(10 * 1000 * 1000),
}; };
struct iwl_host_cmd d3_cfg_cmd = {
.id = D3_CONFIG_CMD,
.flags = CMD_SYNC | CMD_WANT_SKB,
.data[0] = &d3_cfg_cmd_data,
.len[0] = sizeof(d3_cfg_cmd_data),
};
struct wowlan_key_data key_data = { struct wowlan_key_data key_data = {
.use_rsc_tsc = false, .use_rsc_tsc = false,
.tkip = &tkip_cmd, .tkip = &tkip_cmd,
.use_tkip = false, .use_tkip = false,
}; };
int ret, i; int ret, i;
int len __maybe_unused;
u16 seq; u16 seq;
u8 old_aux_sta_id, old_ap_sta_id = IWL_MVM_STATION_COUNT; u8 old_aux_sta_id, old_ap_sta_id = IWL_MVM_STATION_COUNT;
if (WARN_ON(!wowlan)) if (!wowlan) {
/*
* mac80211 shouldn't get here, but for D3 test
* it doesn't warrant a warning
*/
WARN_ON(!test);
return -EINVAL; return -EINVAL;
}
key_data.rsc_tsc = kzalloc(sizeof(*key_data.rsc_tsc), GFP_KERNEL); key_data.rsc_tsc = kzalloc(sizeof(*key_data.rsc_tsc), GFP_KERNEL);
if (!key_data.rsc_tsc) if (!key_data.rsc_tsc)
...@@ -1012,14 +1028,26 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan) ...@@ -1012,14 +1028,26 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan)
goto out; goto out;
/* must be last -- this switches firmware state */ /* must be last -- this switches firmware state */
ret = iwl_mvm_send_cmd_pdu(mvm, D3_CONFIG_CMD, CMD_SYNC, ret = iwl_mvm_send_cmd(mvm, &d3_cfg_cmd);
sizeof(d3_cfg_cmd), &d3_cfg_cmd);
if (ret) if (ret)
goto out; goto out;
#ifdef CONFIG_IWLWIFI_DEBUGFS
len = le32_to_cpu(d3_cfg_cmd.resp_pkt->len_n_flags) &
FH_RSCSR_FRAME_SIZE_MSK;
if (len >= sizeof(u32) * 2) {
mvm->d3_test_pme_ptr =
le32_to_cpup((__le32 *)d3_cfg_cmd.resp_pkt->data);
} else if (test) {
/* in test mode we require the pointer */
ret = -EIO;
goto out;
}
#endif
iwl_free_resp(&d3_cfg_cmd);
clear_bit(IWL_MVM_STATUS_IN_HW_RESTART, &mvm->status); clear_bit(IWL_MVM_STATUS_IN_HW_RESTART, &mvm->status);
iwl_trans_d3_suspend(mvm->trans); iwl_trans_d3_suspend(mvm->trans, test);
out: out:
mvm->aux_sta.sta_id = old_aux_sta_id; mvm->aux_sta.sta_id = old_aux_sta_id;
mvm_ap_sta->sta_id = old_ap_sta_id; mvm_ap_sta->sta_id = old_ap_sta_id;
...@@ -1034,6 +1062,11 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan) ...@@ -1034,6 +1062,11 @@ int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan)
return ret; return ret;
} }
int iwl_mvm_suspend(struct ieee80211_hw *hw, struct cfg80211_wowlan *wowlan)
{
return __iwl_mvm_suspend(hw, wowlan, false);
}
static void iwl_mvm_query_wakeup_reasons(struct iwl_mvm *mvm, static void iwl_mvm_query_wakeup_reasons(struct iwl_mvm *mvm,
struct ieee80211_vif *vif) struct ieee80211_vif *vif)
{ {
...@@ -1238,9 +1271,8 @@ static void iwl_mvm_read_d3_sram(struct iwl_mvm *mvm) ...@@ -1238,9 +1271,8 @@ static void iwl_mvm_read_d3_sram(struct iwl_mvm *mvm)
#endif #endif
} }
int iwl_mvm_resume(struct ieee80211_hw *hw) static int __iwl_mvm_resume(struct iwl_mvm *mvm, bool test)
{ {
struct iwl_mvm *mvm = IWL_MAC80211_GET_MVM(hw);
struct iwl_d3_iter_data resume_iter_data = { struct iwl_d3_iter_data resume_iter_data = {
.mvm = mvm, .mvm = mvm,
}; };
...@@ -1260,7 +1292,7 @@ int iwl_mvm_resume(struct ieee80211_hw *hw) ...@@ -1260,7 +1292,7 @@ int iwl_mvm_resume(struct ieee80211_hw *hw)
vif = resume_iter_data.vif; vif = resume_iter_data.vif;
ret = iwl_trans_d3_resume(mvm->trans, &d3_status); ret = iwl_trans_d3_resume(mvm->trans, &d3_status, test);
if (ret) if (ret)
goto out_unlock; goto out_unlock;
...@@ -1277,7 +1309,7 @@ int iwl_mvm_resume(struct ieee80211_hw *hw) ...@@ -1277,7 +1309,7 @@ int iwl_mvm_resume(struct ieee80211_hw *hw)
out_unlock: out_unlock:
mutex_unlock(&mvm->mutex); mutex_unlock(&mvm->mutex);
if (vif) if (!test && vif)
ieee80211_resume_disconnect(vif); ieee80211_resume_disconnect(vif);
/* return 1 to reconfigure the device */ /* return 1 to reconfigure the device */
...@@ -1285,9 +1317,106 @@ int iwl_mvm_resume(struct ieee80211_hw *hw) ...@@ -1285,9 +1317,106 @@ int iwl_mvm_resume(struct ieee80211_hw *hw)
return 1; return 1;
} }
int iwl_mvm_resume(struct ieee80211_hw *hw)
{
struct iwl_mvm *mvm = IWL_MAC80211_GET_MVM(hw);
return __iwl_mvm_resume(mvm, false);
}
void iwl_mvm_set_wakeup(struct ieee80211_hw *hw, bool enabled) void iwl_mvm_set_wakeup(struct ieee80211_hw *hw, bool enabled)
{ {
struct iwl_mvm *mvm = IWL_MAC80211_GET_MVM(hw); struct iwl_mvm *mvm = IWL_MAC80211_GET_MVM(hw);
device_set_wakeup_enable(mvm->trans->dev, enabled); device_set_wakeup_enable(mvm->trans->dev, enabled);
} }
#ifdef CONFIG_IWLWIFI_DEBUGFS
static int iwl_mvm_d3_test_open(struct inode *inode, struct file *file)
{
struct iwl_mvm *mvm = inode->i_private;
int err;
if (mvm->d3_test_active)
return -EBUSY;
file->private_data = inode->i_private;
ieee80211_stop_queues(mvm->hw);
synchronize_net();
/* start pseudo D3 */
rtnl_lock();
err = __iwl_mvm_suspend(mvm->hw, mvm->hw->wiphy->wowlan_config, true);
rtnl_unlock();
if (err > 0)
err = -EINVAL;
if (err) {
ieee80211_wake_queues(mvm->hw);
return err;
}
mvm->d3_test_active = true;
return 0;
}
static ssize_t iwl_mvm_d3_test_read(struct file *file, char __user *user_buf,
size_t count, loff_t *ppos)
{
struct iwl_mvm *mvm = file->private_data;
u32 pme_asserted;
while (true) {
pme_asserted = iwl_trans_read_mem32(mvm->trans,
mvm->d3_test_pme_ptr);
if (pme_asserted)
break;
if (msleep_interruptible(100))
break;
}
return 0;
}
static void iwl_mvm_d3_test_disconn_work_iter(void *_data, u8 *mac,
struct ieee80211_vif *vif)
{
if (vif->type == NL80211_IFTYPE_STATION)
ieee80211_connection_loss(vif);
}
static int iwl_mvm_d3_test_release(struct inode *inode, struct file *file)
{
struct iwl_mvm *mvm = inode->i_private;
int remaining_time = 10;
mvm->d3_test_active = false;
__iwl_mvm_resume(mvm, true);
iwl_abort_notification_waits(&mvm->notif_wait);
ieee80211_restart_hw(mvm->hw);
/* wait for restart and disconnect all interfaces */
while (test_bit(IWL_MVM_STATUS_IN_HW_RESTART, &mvm->status) &&
remaining_time > 0) {
remaining_time--;
msleep(1000);
}
if (remaining_time == 0)
IWL_ERR(mvm, "Timed out waiting for HW restart to finish!\n");
ieee80211_iterate_active_interfaces_atomic(
mvm->hw, IEEE80211_IFACE_ITER_NORMAL,
iwl_mvm_d3_test_disconn_work_iter, NULL);
ieee80211_wake_queues(mvm->hw);
return 0;
}
const struct file_operations iwl_dbgfs_d3_test_ops = {
.llseek = no_llseek,
.open = iwl_mvm_d3_test_open,
.read = iwl_mvm_d3_test_read,
.release = iwl_mvm_d3_test_release,
};
#endif
...@@ -938,6 +938,7 @@ int iwl_mvm_dbgfs_register(struct iwl_mvm *mvm, struct dentry *dbgfs_dir) ...@@ -938,6 +938,7 @@ int iwl_mvm_dbgfs_register(struct iwl_mvm *mvm, struct dentry *dbgfs_dir)
MVM_DEBUGFS_ADD_FILE(fw_restart, mvm->debugfs_dir, S_IWUSR); MVM_DEBUGFS_ADD_FILE(fw_restart, mvm->debugfs_dir, S_IWUSR);
#ifdef CONFIG_PM_SLEEP #ifdef CONFIG_PM_SLEEP
MVM_DEBUGFS_ADD_FILE(d3_sram, mvm->debugfs_dir, S_IRUSR | S_IWUSR); MVM_DEBUGFS_ADD_FILE(d3_sram, mvm->debugfs_dir, S_IRUSR | S_IWUSR);
MVM_DEBUGFS_ADD_FILE(d3_test, mvm->debugfs_dir, S_IRUSR);
#endif #endif
/* /*
......
...@@ -71,7 +71,13 @@ ...@@ -71,7 +71,13 @@
#define MAC_INDEX_MIN_DRIVER 0 #define MAC_INDEX_MIN_DRIVER 0
#define NUM_MAC_INDEX_DRIVER MAC_INDEX_AUX #define NUM_MAC_INDEX_DRIVER MAC_INDEX_AUX
#define AC_NUM 4 /* Number of access categories */ enum iwl_ac {
AC_BK,
AC_BE,
AC_VI,
AC_VO,
AC_NUM,
};
/** /**
* enum iwl_mac_protection_flags - MAC context flags * enum iwl_mac_protection_flags - MAC context flags
......
...@@ -365,7 +365,7 @@ int iwl_mvm_mac_ctxt_init(struct iwl_mvm *mvm, struct ieee80211_vif *vif) ...@@ -365,7 +365,7 @@ int iwl_mvm_mac_ctxt_init(struct iwl_mvm *mvm, struct ieee80211_vif *vif)
break; break;
case NL80211_IFTYPE_AP: case NL80211_IFTYPE_AP:
iwl_trans_ac_txq_enable(mvm->trans, vif->cab_queue, iwl_trans_ac_txq_enable(mvm->trans, vif->cab_queue,
IWL_MVM_TX_FIFO_VO); IWL_MVM_TX_FIFO_MCAST);
/* fall through */ /* fall through */
default: default:
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
...@@ -553,6 +553,10 @@ static void iwl_mvm_mac_ctxt_cmd_common(struct iwl_mvm *mvm, ...@@ -553,6 +553,10 @@ static void iwl_mvm_mac_ctxt_cmd_common(struct iwl_mvm *mvm,
cmd->ac[i].fifos_mask = BIT(iwl_mvm_ac_to_tx_fifo[i]); cmd->ac[i].fifos_mask = BIT(iwl_mvm_ac_to_tx_fifo[i]);
} }
/* in AP mode, the MCAST FIFO takes the EDCA params from VO */
if (vif->type == NL80211_IFTYPE_AP)
cmd->ac[AC_VO].fifos_mask |= BIT(IWL_MVM_TX_FIFO_MCAST);
if (vif->bss_conf.qos) if (vif->bss_conf.qos)
cmd->qos_flags |= cpu_to_le32(MAC_QOS_FLG_UPDATE_EDCA); cmd->qos_flags |= cpu_to_le32(MAC_QOS_FLG_UPDATE_EDCA);
......
...@@ -609,21 +609,11 @@ static int iwl_mvm_mac_add_interface(struct ieee80211_hw *hw, ...@@ -609,21 +609,11 @@ static int iwl_mvm_mac_add_interface(struct ieee80211_hw *hw,
mvmvif->phy_ctxt = NULL; mvmvif->phy_ctxt = NULL;
iwl_mvm_mac_ctxt_remove(mvm, vif); iwl_mvm_mac_ctxt_remove(mvm, vif);
out_release: out_release:
/*
* TODO: remove this temporary code.
* Currently MVM FW supports power management only on single MAC.
* Check if only one additional interface remains after releasing
* current one. Update power mode on the remaining interface.
*/
if (vif->type != NL80211_IFTYPE_P2P_DEVICE) if (vif->type != NL80211_IFTYPE_P2P_DEVICE)
mvm->vif_count--; mvm->vif_count--;
IWL_DEBUG_MAC80211(mvm, "Currently %d interfaces active\n", ieee80211_iterate_active_interfaces(
mvm->vif_count); mvm->hw, IEEE80211_IFACE_ITER_NORMAL,
if (mvm->vif_count == 1) { iwl_mvm_power_update_iterator, mvm);
ieee80211_iterate_active_interfaces(
mvm->hw, IEEE80211_IFACE_ITER_NORMAL,
iwl_mvm_power_update_iterator, mvm);
}
iwl_mvm_mac_ctxt_release(mvm, vif); iwl_mvm_mac_ctxt_release(mvm, vif);
out_unlock: out_unlock:
mutex_unlock(&mvm->mutex); mutex_unlock(&mvm->mutex);
...@@ -785,6 +775,9 @@ static void iwl_mvm_bss_info_changed_station(struct iwl_mvm *mvm, ...@@ -785,6 +775,9 @@ static void iwl_mvm_bss_info_changed_station(struct iwl_mvm *mvm,
if (ret) if (ret)
IWL_ERR(mvm, "failed to update quotas\n"); IWL_ERR(mvm, "failed to update quotas\n");
} }
ret = iwl_mvm_power_update_mode(mvm, vif);
if (ret)
IWL_ERR(mvm, "failed to update power mode\n");
} else if (changes & BSS_CHANGED_DTIM_PERIOD) { } else if (changes & BSS_CHANGED_DTIM_PERIOD) {
/* /*
* We received a beacon _after_ association so * We received a beacon _after_ association so
...@@ -793,19 +786,9 @@ static void iwl_mvm_bss_info_changed_station(struct iwl_mvm *mvm, ...@@ -793,19 +786,9 @@ static void iwl_mvm_bss_info_changed_station(struct iwl_mvm *mvm,
iwl_mvm_remove_time_event(mvm, mvmvif, iwl_mvm_remove_time_event(mvm, mvmvif,
&mvmvif->time_event_data); &mvmvif->time_event_data);
} else if (changes & BSS_CHANGED_PS) { } else if (changes & BSS_CHANGED_PS) {
/* ret = iwl_mvm_power_update_mode(mvm, vif);
* TODO: remove this temporary code. if (ret)
* Currently MVM FW supports power management only on single IWL_ERR(mvm, "failed to update power mode\n");
* MAC. Avoid power mode update if more than one interface
* is active.
*/
IWL_DEBUG_MAC80211(mvm, "Currently %d interfaces active\n",
mvm->vif_count);
if (mvm->vif_count == 1) {
ret = iwl_mvm_power_update_mode(mvm, vif);
if (ret)
IWL_ERR(mvm, "failed to update power mode\n");
}
} }
} }
......
...@@ -88,6 +88,7 @@ enum iwl_mvm_tx_fifo { ...@@ -88,6 +88,7 @@ enum iwl_mvm_tx_fifo {
IWL_MVM_TX_FIFO_BE, IWL_MVM_TX_FIFO_BE,
IWL_MVM_TX_FIFO_VI, IWL_MVM_TX_FIFO_VI,
IWL_MVM_TX_FIFO_VO, IWL_MVM_TX_FIFO_VO,
IWL_MVM_TX_FIFO_MCAST = 5,
}; };
extern struct ieee80211_ops iwl_mvm_hw_ops; extern struct ieee80211_ops iwl_mvm_hw_ops;
...@@ -459,8 +460,10 @@ struct iwl_mvm { ...@@ -459,8 +460,10 @@ struct iwl_mvm {
#ifdef CONFIG_PM_SLEEP #ifdef CONFIG_PM_SLEEP
int gtk_ivlen, gtk_icvlen, ptk_ivlen, ptk_icvlen; int gtk_ivlen, gtk_icvlen, ptk_ivlen, ptk_icvlen;
#ifdef CONFIG_IWLWIFI_DEBUGFS #ifdef CONFIG_IWLWIFI_DEBUGFS
bool d3_test_active;
bool store_d3_resume_sram; bool store_d3_resume_sram;
void *d3_resume_sram; void *d3_resume_sram;
u32 d3_test_pme_ptr;
#endif #endif
#endif #endif
...@@ -669,6 +672,7 @@ void iwl_mvm_ipv6_addr_change(struct ieee80211_hw *hw, ...@@ -669,6 +672,7 @@ void iwl_mvm_ipv6_addr_change(struct ieee80211_hw *hw,
struct inet6_dev *idev); struct inet6_dev *idev);
void iwl_mvm_set_default_unicast_key(struct ieee80211_hw *hw, void iwl_mvm_set_default_unicast_key(struct ieee80211_hw *hw,
struct ieee80211_vif *vif, int idx); struct ieee80211_vif *vif, int idx);
extern const struct file_operations iwl_dbgfs_d3_test_ops;
/* BT Coex */ /* BT Coex */
int iwl_send_bt_prio_tbl(struct iwl_mvm *mvm); int iwl_send_bt_prio_tbl(struct iwl_mvm *mvm);
......
...@@ -215,15 +215,16 @@ static const struct iwl_rx_handlers iwl_mvm_rx_handlers[] = { ...@@ -215,15 +215,16 @@ static const struct iwl_rx_handlers iwl_mvm_rx_handlers[] = {
RX_HANDLER(REPLY_RX_PHY_CMD, iwl_mvm_rx_rx_phy_cmd, false), RX_HANDLER(REPLY_RX_PHY_CMD, iwl_mvm_rx_rx_phy_cmd, false),
RX_HANDLER(TX_CMD, iwl_mvm_rx_tx_cmd, false), RX_HANDLER(TX_CMD, iwl_mvm_rx_tx_cmd, false),
RX_HANDLER(BA_NOTIF, iwl_mvm_rx_ba_notif, false), RX_HANDLER(BA_NOTIF, iwl_mvm_rx_ba_notif, false),
RX_HANDLER(TIME_EVENT_NOTIFICATION, iwl_mvm_rx_time_event_notif, false),
RX_HANDLER(SCAN_REQUEST_CMD, iwl_mvm_rx_scan_response, false),
RX_HANDLER(SCAN_COMPLETE_NOTIFICATION, iwl_mvm_rx_scan_complete, false),
RX_HANDLER(BT_PROFILE_NOTIFICATION, iwl_mvm_rx_bt_coex_notif, true), RX_HANDLER(BT_PROFILE_NOTIFICATION, iwl_mvm_rx_bt_coex_notif, true),
RX_HANDLER(BEACON_NOTIFICATION, iwl_mvm_rx_beacon_notif, false), RX_HANDLER(BEACON_NOTIFICATION, iwl_mvm_rx_beacon_notif, false),
RX_HANDLER(STATISTICS_NOTIFICATION, iwl_mvm_rx_statistics, true), RX_HANDLER(STATISTICS_NOTIFICATION, iwl_mvm_rx_statistics, true),
RX_HANDLER(TIME_EVENT_NOTIFICATION, iwl_mvm_rx_time_event_notif, false),
RX_HANDLER(SCAN_REQUEST_CMD, iwl_mvm_rx_scan_response, false),
RX_HANDLER(SCAN_COMPLETE_NOTIFICATION, iwl_mvm_rx_scan_complete, false),
RX_HANDLER(RADIO_VERSION_NOTIFICATION, iwl_mvm_rx_radio_ver, false), RX_HANDLER(RADIO_VERSION_NOTIFICATION, iwl_mvm_rx_radio_ver, false),
RX_HANDLER(CARD_STATE_NOTIFICATION, iwl_mvm_rx_card_state_notif, false), RX_HANDLER(CARD_STATE_NOTIFICATION, iwl_mvm_rx_card_state_notif, false),
......
...@@ -204,7 +204,8 @@ int iwl_mvm_phy_ctxt_add(struct iwl_mvm *mvm, struct iwl_mvm_phy_ctxt *ctxt, ...@@ -204,7 +204,8 @@ int iwl_mvm_phy_ctxt_add(struct iwl_mvm *mvm, struct iwl_mvm_phy_ctxt *ctxt,
{ {
int ret; int ret;
WARN_ON(ctxt->ref); WARN_ON(!test_bit(IWL_MVM_STATUS_IN_HW_RESTART, &mvm->status) &&
ctxt->ref);
lockdep_assert_held(&mvm->mutex); lockdep_assert_held(&mvm->mutex);
ctxt->channel = chandef->chan; ctxt->channel = chandef->chan;
......
...@@ -168,6 +168,8 @@ void iwl_mvm_power_build_cmd(struct iwl_mvm *mvm, struct ieee80211_vif *vif, ...@@ -168,6 +168,8 @@ void iwl_mvm_power_build_cmd(struct iwl_mvm *mvm, struct ieee80211_vif *vif,
return; return;
cmd->flags |= cpu_to_le16(POWER_FLAGS_POWER_SAVE_ENA_MSK); cmd->flags |= cpu_to_le16(POWER_FLAGS_POWER_SAVE_ENA_MSK);
if (!vif->bss_conf.assoc)
cmd->flags |= cpu_to_le16(POWER_FLAGS_POWER_MANAGEMENT_ENA_MSK);
#ifdef CONFIG_IWLWIFI_DEBUGFS #ifdef CONFIG_IWLWIFI_DEBUGFS
if (mvmvif->dbgfs_pm.mask & MVM_DEBUGFS_PM_DISABLE_POWER_OFF && if (mvmvif->dbgfs_pm.mask & MVM_DEBUGFS_PM_DISABLE_POWER_OFF &&
...@@ -246,6 +248,17 @@ int iwl_mvm_power_update_mode(struct iwl_mvm *mvm, struct ieee80211_vif *vif) ...@@ -246,6 +248,17 @@ int iwl_mvm_power_update_mode(struct iwl_mvm *mvm, struct ieee80211_vif *vif)
if (vif->type != NL80211_IFTYPE_STATION || vif->p2p) if (vif->type != NL80211_IFTYPE_STATION || vif->p2p)
return 0; return 0;
/*
* TODO: The following vif_count verification is temporary condition.
* Avoid power mode update if more than one interface is currently
* active. Remove this condition when FW will support power management
* on multiple MACs.
*/
IWL_DEBUG_POWER(mvm, "Currently %d interfaces active\n",
mvm->vif_count);
if (mvm->vif_count > 1)
return 0;
iwl_mvm_power_build_cmd(mvm, vif, &cmd); iwl_mvm_power_build_cmd(mvm, vif, &cmd);
iwl_mvm_power_log(mvm, &cmd); iwl_mvm_power_log(mvm, &cmd);
......
...@@ -169,27 +169,34 @@ int iwl_mvm_update_quotas(struct iwl_mvm *mvm, struct ieee80211_vif *newvif) ...@@ -169,27 +169,34 @@ int iwl_mvm_update_quotas(struct iwl_mvm *mvm, struct ieee80211_vif *newvif)
num_active_bindings++; num_active_bindings++;
} }
if (!num_active_bindings) quota = 0;
goto send_cmd; quota_rem = 0;
if (num_active_bindings) {
quota = IWL_MVM_MAX_QUOTA / num_active_bindings; quota = IWL_MVM_MAX_QUOTA / num_active_bindings;
quota_rem = IWL_MVM_MAX_QUOTA % num_active_bindings; quota_rem = IWL_MVM_MAX_QUOTA % num_active_bindings;
}
for (idx = 0, i = 0; i < MAX_BINDINGS; i++) { for (idx = 0, i = 0; i < MAX_BINDINGS; i++) {
if (data.n_interfaces[i] <= 0) if (data.colors[i] < 0)
continue; continue;
cmd.quotas[idx].id_and_color = cmd.quotas[idx].id_and_color =
cpu_to_le32(FW_CMD_ID_AND_COLOR(i, data.colors[i])); cpu_to_le32(FW_CMD_ID_AND_COLOR(i, data.colors[i]));
cmd.quotas[idx].quota = cpu_to_le32(quota);
cmd.quotas[idx].max_duration = cpu_to_le32(IWL_MVM_MAX_QUOTA); if (data.n_interfaces[i] <= 0) {
cmd.quotas[idx].quota = cpu_to_le32(0);
cmd.quotas[idx].max_duration = cpu_to_le32(0);
} else {
cmd.quotas[idx].quota = cpu_to_le32(quota);
cmd.quotas[idx].max_duration =
cpu_to_le32(IWL_MVM_MAX_QUOTA);
}
idx++; idx++;
} }
/* Give the remainder of the session to the first binding */ /* Give the remainder of the session to the first binding */
le32_add_cpu(&cmd.quotas[0].quota, quota_rem); le32_add_cpu(&cmd.quotas[0].quota, quota_rem);
send_cmd:
ret = iwl_mvm_send_cmd_pdu(mvm, TIME_QUOTA_CMD, CMD_SYNC, ret = iwl_mvm_send_cmd_pdu(mvm, TIME_QUOTA_CMD, CMD_SYNC,
sizeof(cmd), &cmd); sizeof(cmd), &cmd);
if (ret) if (ret)
......
...@@ -401,6 +401,17 @@ static int rs_tl_turn_on_agg_for_tid(struct iwl_mvm *mvm, ...@@ -401,6 +401,17 @@ static int rs_tl_turn_on_agg_for_tid(struct iwl_mvm *mvm,
load = rs_tl_get_load(lq_data, tid); load = rs_tl_get_load(lq_data, tid);
/*
* Don't create TX aggregation sessions when in high
* BT traffic, as they would just be disrupted by BT.
*/
if (BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD) >= 2) {
IWL_DEBUG_COEX(mvm, "BT traffic (%d), no aggregation allowed\n",
BT_MBOX_MSG(&mvm->last_bt_notif,
3, TRAFFIC_LOAD));
return ret;
}
if ((iwlwifi_mod_params.auto_agg) || (load > IWL_AGG_LOAD_THRESHOLD)) { if ((iwlwifi_mod_params.auto_agg) || (load > IWL_AGG_LOAD_THRESHOLD)) {
IWL_DEBUG_HT(mvm, "Starting Tx agg: STA: %pM tid: %d\n", IWL_DEBUG_HT(mvm, "Starting Tx agg: STA: %pM tid: %d\n",
sta->addr, tid); sta->addr, tid);
...@@ -1519,6 +1530,29 @@ static int rs_move_siso_to_other(struct iwl_mvm *mvm, ...@@ -1519,6 +1530,29 @@ static int rs_move_siso_to_other(struct iwl_mvm *mvm,
u8 update_search_tbl_counter = 0; u8 update_search_tbl_counter = 0;
int ret; int ret;
switch (BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD)) {
case IWL_BT_COEX_TRAFFIC_LOAD_NONE:
/* nothing */
break;
case IWL_BT_COEX_TRAFFIC_LOAD_LOW:
/* avoid antenna B unless MIMO */
if (tbl->action == IWL_SISO_SWITCH_ANTENNA2)
tbl->action = IWL_SISO_SWITCH_MIMO2_AB;
break;
case IWL_BT_COEX_TRAFFIC_LOAD_HIGH:
case IWL_BT_COEX_TRAFFIC_LOAD_CONTINUOUS:
/* avoid antenna B and MIMO */
valid_tx_ant =
first_antenna(iwl_fw_valid_tx_ant(mvm->fw));
if (tbl->action != IWL_SISO_SWITCH_ANTENNA1)
tbl->action = IWL_SISO_SWITCH_ANTENNA1;
break;
default:
IWL_ERR(mvm, "Invalid BT load %d",
BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD));
break;
}
start_action = tbl->action; start_action = tbl->action;
while (1) { while (1) {
lq_sta->action_counter++; lq_sta->action_counter++;
...@@ -1532,7 +1566,9 @@ static int rs_move_siso_to_other(struct iwl_mvm *mvm, ...@@ -1532,7 +1566,9 @@ static int rs_move_siso_to_other(struct iwl_mvm *mvm,
tx_chains_num <= 2)) tx_chains_num <= 2))
break; break;
if (window->success_ratio >= IWL_RS_GOOD_RATIO) if (window->success_ratio >= IWL_RS_GOOD_RATIO &&
BT_MBOX_MSG(&mvm->last_bt_notif, 3,
TRAFFIC_LOAD) == 0)
break; break;
memcpy(search_tbl, tbl, sz); memcpy(search_tbl, tbl, sz);
...@@ -1654,6 +1690,28 @@ static int rs_move_mimo2_to_other(struct iwl_mvm *mvm, ...@@ -1654,6 +1690,28 @@ static int rs_move_mimo2_to_other(struct iwl_mvm *mvm,
u8 update_search_tbl_counter = 0; u8 update_search_tbl_counter = 0;
int ret; int ret;
switch (BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD)) {
case IWL_BT_COEX_TRAFFIC_LOAD_NONE:
/* nothing */
break;
case IWL_BT_COEX_TRAFFIC_LOAD_HIGH:
case IWL_BT_COEX_TRAFFIC_LOAD_CONTINUOUS:
/* avoid antenna B and MIMO */
if (tbl->action != IWL_MIMO2_SWITCH_SISO_A)
tbl->action = IWL_MIMO2_SWITCH_SISO_A;
break;
case IWL_BT_COEX_TRAFFIC_LOAD_LOW:
/* avoid antenna B unless MIMO */
if (tbl->action == IWL_MIMO2_SWITCH_SISO_B ||
tbl->action == IWL_MIMO2_SWITCH_SISO_C)
tbl->action = IWL_MIMO2_SWITCH_SISO_A;
break;
default:
IWL_ERR(mvm, "Invalid BT load %d",
BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD));
break;
}
start_action = tbl->action; start_action = tbl->action;
while (1) { while (1) {
lq_sta->action_counter++; lq_sta->action_counter++;
...@@ -1791,6 +1849,28 @@ static int rs_move_mimo3_to_other(struct iwl_mvm *mvm, ...@@ -1791,6 +1849,28 @@ static int rs_move_mimo3_to_other(struct iwl_mvm *mvm,
int ret; int ret;
u8 update_search_tbl_counter = 0; u8 update_search_tbl_counter = 0;
switch (BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD)) {
case IWL_BT_COEX_TRAFFIC_LOAD_NONE:
/* nothing */
break;
case IWL_BT_COEX_TRAFFIC_LOAD_HIGH:
case IWL_BT_COEX_TRAFFIC_LOAD_CONTINUOUS:
/* avoid antenna B and MIMO */
if (tbl->action != IWL_MIMO3_SWITCH_SISO_A)
tbl->action = IWL_MIMO3_SWITCH_SISO_A;
break;
case IWL_BT_COEX_TRAFFIC_LOAD_LOW:
/* avoid antenna B unless MIMO */
if (tbl->action == IWL_MIMO3_SWITCH_SISO_B ||
tbl->action == IWL_MIMO3_SWITCH_SISO_C)
tbl->action = IWL_MIMO3_SWITCH_SISO_A;
break;
default:
IWL_ERR(mvm, "Invalid BT load %d",
BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD));
break;
}
start_action = tbl->action; start_action = tbl->action;
while (1) { while (1) {
lq_sta->action_counter++; lq_sta->action_counter++;
...@@ -2302,6 +2382,32 @@ static void rs_rate_scale_perform(struct iwl_mvm *mvm, ...@@ -2302,6 +2382,32 @@ static void rs_rate_scale_perform(struct iwl_mvm *mvm,
(current_tpt > (100 * tbl->expected_tpt[low])))) (current_tpt > (100 * tbl->expected_tpt[low]))))
scale_action = 0; scale_action = 0;
if ((BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD) >=
IWL_BT_COEX_TRAFFIC_LOAD_HIGH) &&
(is_mimo2(tbl->lq_type) || is_mimo3(tbl->lq_type))) {
if (lq_sta->last_bt_traffic >
BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD)) {
/*
* don't set scale_action, don't want to scale up if
* the rate scale doesn't otherwise think that is a
* good idea.
*/
} else if (lq_sta->last_bt_traffic <=
BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD)) {
scale_action = -1;
}
}
lq_sta->last_bt_traffic =
BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD);
if ((BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD) >=
IWL_BT_COEX_TRAFFIC_LOAD_HIGH) &&
(is_mimo2(tbl->lq_type) || is_mimo3(tbl->lq_type))) {
/* search for a new modulation */
rs_stay_in_table(lq_sta, true);
goto lq_update;
}
switch (scale_action) { switch (scale_action) {
case -1: case -1:
/* Decrease starting rate, update uCode's rate table */ /* Decrease starting rate, update uCode's rate table */
...@@ -2782,6 +2888,13 @@ static void rs_fill_link_cmd(struct iwl_mvm *mvm, ...@@ -2782,6 +2888,13 @@ static void rs_fill_link_cmd(struct iwl_mvm *mvm,
lq_cmd->agg_time_limit = lq_cmd->agg_time_limit =
cpu_to_le16(LINK_QUAL_AGG_TIME_LIMIT_DEF); cpu_to_le16(LINK_QUAL_AGG_TIME_LIMIT_DEF);
/*
* overwrite if needed, pass aggregation time limit
* to uCode in uSec - This is racy - but heh, at least it helps...
*/
if (mvm && BT_MBOX_MSG(&mvm->last_bt_notif, 3, TRAFFIC_LOAD) >= 2)
lq_cmd->agg_time_limit = cpu_to_le16(1200);
} }
static void *rs_alloc(struct ieee80211_hw *hw, struct dentry *debugfsdir) static void *rs_alloc(struct ieee80211_hw *hw, struct dentry *debugfsdir)
......
...@@ -358,6 +358,18 @@ struct iwl_lq_sta { ...@@ -358,6 +358,18 @@ struct iwl_lq_sta {
u8 last_bt_traffic; u8 last_bt_traffic;
}; };
enum iwl_bt_coex_profile_traffic_load {
IWL_BT_COEX_TRAFFIC_LOAD_NONE = 0,
IWL_BT_COEX_TRAFFIC_LOAD_LOW = 1,
IWL_BT_COEX_TRAFFIC_LOAD_HIGH = 2,
IWL_BT_COEX_TRAFFIC_LOAD_CONTINUOUS = 3,
/*
* There are no more even though below is a u8, the
* indication from the BT device only has two bits.
*/
};
static inline u8 num_of_ant(u8 mask) static inline u8 num_of_ant(u8 mask)
{ {
return !!((mask) & ANT_A) + return !!((mask) & ANT_A) +
......
...@@ -371,6 +371,9 @@ static void iwl_mvm_tt_smps_iterator(void *_data, u8 *mac, ...@@ -371,6 +371,9 @@ static void iwl_mvm_tt_smps_iterator(void *_data, u8 *mac,
else else
smps_mode = IEEE80211_SMPS_AUTOMATIC; smps_mode = IEEE80211_SMPS_AUTOMATIC;
if (vif->type != NL80211_IFTYPE_STATION)
return;
iwl_mvm_update_smps(mvm, vif, IWL_MVM_SMPS_REQ_TT, smps_mode); iwl_mvm_update_smps(mvm, vif, IWL_MVM_SMPS_REQ_TT, smps_mode);
} }
......
...@@ -175,7 +175,7 @@ static void iwl_mvm_set_tx_cmd_rate(struct iwl_mvm *mvm, ...@@ -175,7 +175,7 @@ static void iwl_mvm_set_tx_cmd_rate(struct iwl_mvm *mvm,
* table is controlled by LINK_QUALITY commands * table is controlled by LINK_QUALITY commands
*/ */
if (ieee80211_is_data(fc)) { if (ieee80211_is_data(fc) && sta) {
tx_cmd->initial_rate_index = 0; tx_cmd->initial_rate_index = 0;
tx_cmd->tx_flags |= cpu_to_le32(TX_CMD_FLG_STA_RATE); tx_cmd->tx_flags |= cpu_to_le32(TX_CMD_FLG_STA_RATE);
return; return;
......
...@@ -76,6 +76,11 @@ int iwl_mvm_send_cmd(struct iwl_mvm *mvm, struct iwl_host_cmd *cmd) ...@@ -76,6 +76,11 @@ int iwl_mvm_send_cmd(struct iwl_mvm *mvm, struct iwl_host_cmd *cmd)
{ {
int ret; int ret;
#if defined(CONFIG_IWLWIFI_DEBUGFS) && defined(CONFIG_PM_SLEEP)
if (WARN_ON(mvm->d3_test_active))
return -EIO;
#endif
/* /*
* Synchronous commands from this op-mode must hold * Synchronous commands from this op-mode must hold
* the mutex, this ensures we don't try to send two * the mutex, this ensures we don't try to send two
...@@ -125,6 +130,11 @@ int iwl_mvm_send_cmd_status(struct iwl_mvm *mvm, struct iwl_host_cmd *cmd, ...@@ -125,6 +130,11 @@ int iwl_mvm_send_cmd_status(struct iwl_mvm *mvm, struct iwl_host_cmd *cmd,
lockdep_assert_held(&mvm->mutex); lockdep_assert_held(&mvm->mutex);
#if defined(CONFIG_IWLWIFI_DEBUGFS) && defined(CONFIG_PM_SLEEP)
if (WARN_ON(mvm->d3_test_active))
return -EIO;
#endif
/* /*
* Only synchronous commands can wait for status, * Only synchronous commands can wait for status,
* we use WANT_SKB so the caller can't. * we use WANT_SKB so the caller can't.
......
...@@ -578,9 +578,17 @@ static void iwl_trans_pcie_stop_device(struct iwl_trans *trans) ...@@ -578,9 +578,17 @@ static void iwl_trans_pcie_stop_device(struct iwl_trans *trans)
clear_bit(STATUS_RFKILL, &trans_pcie->status); clear_bit(STATUS_RFKILL, &trans_pcie->status);
} }
static void iwl_trans_pcie_d3_suspend(struct iwl_trans *trans) static void iwl_trans_pcie_d3_suspend(struct iwl_trans *trans, bool test)
{ {
iwl_disable_interrupts(trans); iwl_disable_interrupts(trans);
/*
* in testing mode, the host stays awake and the
* hardware won't be reset (not even partially)
*/
if (test)
return;
iwl_pcie_disable_ict(trans); iwl_pcie_disable_ict(trans);
iwl_clear_bit(trans, CSR_GP_CNTRL, iwl_clear_bit(trans, CSR_GP_CNTRL,
...@@ -599,11 +607,18 @@ static void iwl_trans_pcie_d3_suspend(struct iwl_trans *trans) ...@@ -599,11 +607,18 @@ static void iwl_trans_pcie_d3_suspend(struct iwl_trans *trans)
} }
static int iwl_trans_pcie_d3_resume(struct iwl_trans *trans, static int iwl_trans_pcie_d3_resume(struct iwl_trans *trans,
enum iwl_d3_status *status) enum iwl_d3_status *status,
bool test)
{ {
u32 val; u32 val;
int ret; int ret;
if (test) {
iwl_enable_interrupts(trans);
*status = IWL_D3_STATUS_ALIVE;
return 0;
}
iwl_pcie_set_pwr(trans, false); iwl_pcie_set_pwr(trans, false);
val = iwl_read32(trans, CSR_RESET); val = iwl_read32(trans, CSR_RESET);
......
...@@ -1527,7 +1527,8 @@ static int iwl_pcie_send_hcmd_sync(struct iwl_trans *trans, ...@@ -1527,7 +1527,8 @@ static int iwl_pcie_send_hcmd_sync(struct iwl_trans *trans,
goto cancel; goto cancel;
} }
if (test_bit(STATUS_RFKILL, &trans_pcie->status)) { if (!(cmd->flags & CMD_SEND_IN_RFKILL) &&
test_bit(STATUS_RFKILL, &trans_pcie->status)) {
IWL_DEBUG_RF_KILL(trans, "RFKILL in SYNC CMD... no rsp\n"); IWL_DEBUG_RF_KILL(trans, "RFKILL in SYNC CMD... no rsp\n");
ret = -ERFKILL; ret = -ERFKILL;
goto cancel; goto cancel;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment