luckfox-pico-sdk/sysdrv/drv_ko/wifi/ssv6115/fmac/fmac_tx.c
luckfox-eng29 8f34c2760d project:build.sh: Added fastboot support; custom modifications to U-Boot and kernel implemented using patches.
project:cfg:BoardConfig_IPC: Added fastboot BoardConfig file and firmware post-scripts, distinguishing between
the BoardConfigs for Luckfox Pico Pro and Luckfox Pico Max. project:app: Added fastboot_client and rk_smart_door
for quick boot applications; updated rkipc app to adapt to the latest media library. media:samples: Added more
usage examples. media:rockit: Fixed bugs; removed support for retrieving data frames from VPSS. media:isp:
Updated rkaiq library and related tools to support connection to RKISP_Tuner. sysdrv:Makefile: Added support for
compiling drv_ko on Luckfox Pico Ultra W using Ubuntu; added support for custom root filesystem.
sysdrv:tools:board: Updated Buildroot optional mirror sources, updated some software versions, and stored device
tree files and configuration files that undergo multiple modifications for U-Boot and kernel separately.
sysdrv:source:mcu: Used RISC-V MCU SDK with RT-Thread system, mainly for initializing camera AE during quick
boot. sysdrv:source:uboot: Added support for fastboot; added high baud rate DDR bin for serial firmware upgrades.
sysdrv:source:kernel: Upgraded to version 5.10.160; increased NPU frequency for RV1106G3; added support for
fastboot.

Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
2024-10-14 09:47:04 +08:00

1390 lines
43 KiB
C

/*
* Copyright (c) 2021 iComm-semi Ltd.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/**
* @file fmac_tx.c
* @brief
*/
/*******************************************************************************
* Include Files
******************************************************************************/
#include <linux/version.h>
#include <linux/types.h>
#include <linux/dma-mapping.h>
#include <linux/if_arp.h>
#include <linux/etherdevice.h>
#include <net/ieee80211_radiotap.h>
#include "ssv_cfg.h"
#include "fmac.h"
#include "hci/drv_hci_ops.h"
#include "fmac_defs.h"
#include "fmac_tx.h"
#include "fmac_msg_tx.h"
#include "ipc_msg.h"
#include "hwif/hwif.h"
#include "hci/ssv_hci.h"
#include "hci/hctrl.h"
#include "ssv_debug.h"
#ifdef FMAC_BRIDGE
#include "fmac_bridge.h"
#endif
/*******************************************************************************
* Local Defines
******************************************************************************/
#define PRIO_STA_NULL 0xAA
#define TX_BMU_PAGE_SIZE 512
extern void ssv_hex_dump(unsigned char *data, int len);
extern u32 ssv_get_free_tx_page(struct ssv_softc *sc);
enum data_frame_types ssv_get_data_frame_type(struct sk_buff *skb);
static struct ssv_sta *ssv_get_tx_info(struct ssv_vif *ssv_vif, struct sk_buff *skb, u8 *tid);
/*******************************************************************************
* Local Enumerations
******************************************************************************/
/*******************************************************************************
* Local Structures
******************************************************************************/
/*******************************************************************************
* Local Variables
******************************************************************************/
static u16 g_pkt_sn = 0;
static const int ssv_down_hwq2tid[3] = {
[SSV_HWQ_BK] = 2,
[SSV_HWQ_BE] = 3,
[SSV_HWQ_VI] = 5,
};
// static int tx_pkt_cnt;
/*******************************************************************************
* Global Variables
******************************************************************************/
extern struct ssv6xxx_cfg ssv_cfg;
/*******************************************************************************
* Local Functions
******************************************************************************/
#if 0
static void _ssv_hex_dump(unsigned char *data, int len)
{
int i = 0;
SSV_LOG_DBG("\n");
for (i = 1; i <= len; i++)
{
if (i % 16 == 1)
{
SSV_LOG_DBG("0x%p : ", &data[i - 1]);
}
SSV_LOG_DBG("%02x ", data[i - 1]);
if (i % 16 == 0)
SSV_LOG_DBG("\n");
}
SSV_LOG_DBG("\n");
}
#endif
/**
* ssv_get_tx_info - Get STA and tid for one skb
*
* @ssv_vif: vif ptr
* @skb: skb
* @tid: pointer updated with the tid to use for this skb
*
* @return: pointer on the destination STA (may be NULL)
*
*/
static struct ssv_sta *ssv_get_tx_info(struct ssv_vif *ssv_vif,
struct sk_buff *skb,
u8 *tid)
{
struct ssv_softc *sc = ssv_vif->sc;
struct wireless_dev *wdev = &ssv_vif->wdev;
struct ssv_sta *sta = NULL;
switch (wdev->iftype) {
case NL80211_IFTYPE_STATION:
case NL80211_IFTYPE_P2P_CLIENT:
{
struct ethhdr *eth;
eth = (struct ethhdr *)skb->data;
sta = ssv_vif->sta.ap;
break;
}
case NL80211_IFTYPE_AP_VLAN:
{
struct ssv_sta *cur;
struct ethhdr *eth = (struct ethhdr *)skb->data;
if (ssv_vif->ap_vlan.sta_4a) {
sta = ssv_vif->ap_vlan.sta_4a;
break;
}
/* AP_VLAN interface is not used for a 4A STA,
fallback searching sta amongs all AP's clients */
ssv_vif = ssv_vif->ap_vlan.master;
if (is_multicast_ether_addr(eth->h_dest)) {
sta = &sc->sta_table[ssv_vif->ap.bcmc_index];
} else {
list_for_each_entry(cur, &ssv_vif->ap.sta_list, list) {
if (!memcmp(cur->mac_addr, eth->h_dest, ETH_ALEN)) {
sta = cur;
break;
}
}
}
break;
}
case NL80211_IFTYPE_AP:
case NL80211_IFTYPE_P2P_GO:
{
struct ssv_sta *cur;
struct ethhdr *eth = (struct ethhdr *)skb->data;
if (is_multicast_ether_addr(eth->h_dest)) {
sta = &sc->sta_table[ssv_vif->ap.bcmc_index];
} else {
list_for_each_entry(cur, &ssv_vif->ap.sta_list, list) {
if (!memcmp(cur->mac_addr, eth->h_dest, ETH_ALEN)) {
sta = cur;
break;
}
}
}
break;
}
default:
break;
} /* end of switch (wdev->iftype) */
if (sta && sta->qos)
{
/* use the data classifier to determine what 802.1d tag the data frame has */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 14, 0)
skb->priority = cfg80211_classify8021d(skb, NULL) & IEEE80211_QOS_CTL_TAG1D_MASK;
#else
skb->priority = cfg80211_classify8021d(skb) & IEEE80211_QOS_CTL_TAG1D_MASK;
#endif
if (sta->acm)
ssv_downgrade_ac(sta, skb);
}
else if (sta)
{
skb->priority = 0xFF;
}
else
{
/* This packet will be dropped in xmit function, still need to select
an active queue for xmit to be called. As it most likely to happen
for AP interface, select BCMC queue
(TODO: select another queue if BCMC queue is stopped) */
skb->priority = PRIO_STA_NULL;
}
*tid = skb->priority;
return sta;
}
static void _ssv_tx_mib(struct ssv_softc *sc, u8 *data, bool is_80211, u8 sta_idx, u8 data_type)
{
if(0xFF == sta_idx){
sta_idx = NX_REMOTE_STA_MAX + 1; //BROADCAST/GROUP DATA TX STA Index for virtual AP
}
sc->tx.tx_count++;
sc->tx_bysta[sta_idx].tx_count++;
if(false == is_80211) {
sc->tx.tx_data_count++; //802.3 packet is treat as data frame
sc->tx_bysta[sta_idx].tx_data_count++;
switch(data_type){
case SSV_ARP_REPLY:
sc->tx.tx_arp_reply_count++;
sc->tx_bysta[sta_idx].tx_arp_reply_count++;
break;
case SSV_ARP_REQUEST:
sc->tx.tx_arp_req_count++;
sc->tx_bysta[sta_idx].tx_arp_req_count++;
break;
case SSV_ICMP_ECHO:
sc->tx.tx_icmp_echo++;
sc->tx_bysta[sta_idx].tx_icmp_echo++;
break;
case SSV_ICMP_ECHOREPLY:
sc->tx.tx_icmp_echoreply++;
sc->tx_bysta[sta_idx].tx_icmp_echoreply++;
break;
case SSV_DHCP_DISCOVER:
sc->tx.tx_dhcp_discv++;
sc->tx_bysta[sta_idx].tx_dhcp_discv++;
SSV_LOG_DBG("SSV_DHCP_DISCOVER\n");
break;
case SSV_DHCP_OFFER:
sc->tx.tx_dhcp_offer++;
sc->tx_bysta[sta_idx].tx_dhcp_offer++;
break;
case SSV_DHCP_REQUEST:
sc->tx.tx_dhcp_req++;
sc->tx_bysta[sta_idx].tx_dhcp_req++;
SSV_LOG_DBG("SSV_DHCP_REQUEST\n");
break;
case SSV_DHCP_ACK:
sc->tx.tx_dhcp_ack++;
sc->tx_bysta[sta_idx].tx_dhcp_ack++;
break;
case SSV_EAPOL:
sc->tx.tx_eapol++;
sc->tx_bysta[sta_idx].tx_eapol++;
SSV_LOG_DBG("SSV_EAPOL TX Packet to STA%d\n", sta_idx);
break;
default:
break;
}
}
else {
struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)data;
if (ieee80211_is_data(hdr->frame_control)) {
sc->tx.tx_data_count++;
sc->tx_bysta[sta_idx].tx_data_count++;
} else if (ieee80211_is_mgmt(hdr->frame_control)) {
sc->tx.tx_mgmt_count++;
sc->tx_bysta[sta_idx].tx_mgmt_count++;
if (ieee80211_is_auth(hdr->frame_control)) {
sc->tx.tx_auth_count++;
sc->tx_bysta[sta_idx].tx_auth_count++;
} else if (ieee80211_is_deauth(hdr->frame_control)) {
sc->tx.tx_deauth_count++;
sc->tx_bysta[sta_idx].tx_deauth_count++;
} else if (ieee80211_is_assoc_req(hdr->frame_control)) {
sc->tx.tx_assoc_req_count++;
sc->tx_bysta[sta_idx].tx_assoc_req_count++;
} else if (ieee80211_is_assoc_resp(hdr->frame_control)) {
sc->tx.tx_assoc_resp_count++;
sc->tx_bysta[sta_idx].tx_assoc_resp_count++;
} else if (ieee80211_is_probe_req(hdr->frame_control)) {
sc->tx.tx_probe_req_count++;
sc->tx_bysta[sta_idx].tx_probe_req_count++;
} else if (ieee80211_is_probe_resp(hdr->frame_control)) {
sc->tx.tx_probe_resp_count++;
sc->tx_bysta[sta_idx].tx_probe_resp_count++;
}
}
}
}
/*******************************************************************************
* Global Functions
******************************************************************************/
/******************************************************************************
* Power Save functions
*****************************************************************************/
/**
* ssv_set_traffic_status - Inform FW if traffic is available for STA in PS
*
* @sc: Driver main data
* @sta: Sta in PS mode
* @available: whether traffic is buffered for the STA
* @ps_id: type of PS data requested (@LEGACY_PS_ID or @UAPSD_ID)
*/
void ssv_set_traffic_status(struct ssv_softc *sc,
struct ssv_sta *sta,
bool available,
u8 ps_id)
{
bool uapsd = (ps_id != LEGACY_PS_ID);
ssv_send_me_traffic_ind(sc, sta->sta_idx, uapsd, available);
// trace_ps_traffic_update(sta->sta_idx, available, uapsd);
}
/**
* ssv_ps_bh_enable - Enable/disable PS mode for one STA
*
* @sc: Driver main data
* @sta: Sta which enters/leaves PS mode
* @enable: PS mode status
*
* This function will enable/disable PS mode for one STA.
* When enabling PS mode:
* - Stop all STA's txq for SSV_TXQ_STOP_STA_PS reason
* - Count how many buffers are already ready for this STA
* - For BC/MC sta, update all queued SKB to use hw_queue BCMC
* - Update TIM if some packet are ready
*
* When disabling PS mode:
* - Start all STA's txq for SSV_TXQ_STOP_STA_PS reason
* - For BC/MC sta, update all queued SKB to use hw_queue AC_BE
* - Update TIM if some packet are ready (otherwise fw will not update TIM
* in beacon for this STA)
*
* NOTE: _bh_ in function name indicates that this function is called
* from a bottom_half tasklet.
*/
void ssv_ps_bh_enable(struct ssv_softc *sc, struct ssv_sta *sta,
bool enable)
{
if(enable)
{
ssv_drv_hci_tx_pause_by_sta(sc->hci_priv, sc->hci_ops, sta->sta_idx);
sta->ps.active = true;
sta->ps.sp_cnt[LEGACY_PS_ID] = 0;
sta->ps.sp_cnt[UAPSD_ID] = 0;
//SSV_LOG_DBG("sta idx = %d, vif idx = %d\n", sta->sta_idx, sta->vif_idx);
if(_ssv_is_multicast_sta(sta->sta_idx))
{
bool inactive;
bool paused;
int locked;
u32 qsize;
u32 pkt_cnt;
ssv_drv_hci_txq_st(sc->hci_priv, sc->hci_ops, sta->sta_idx, &inactive, &paused, &locked, &qsize, &pkt_cnt);
sta->ps.pkt_ready[LEGACY_PS_ID] = qsize;
sta->ps.pkt_ready[UAPSD_ID] = 0;
}
else
{
bool inactive;
bool paused;
int locked;
u32 qsize;
u32 pkt_cnt;
ssv_drv_hci_txq_st(sc->hci_priv, sc->hci_ops, sta->sta_idx, &inactive, &paused, &locked, &qsize, &pkt_cnt);
sta->ps.pkt_ready[LEGACY_PS_ID] = qsize;
sta->ps.pkt_ready[UAPSD_ID] = 0;
}
if(sta->ps.pkt_ready[LEGACY_PS_ID])
ssv_set_traffic_status(sc, sta, true, LEGACY_PS_ID);
if(sta->ps.pkt_ready[UAPSD_ID])
ssv_set_traffic_status(sc, sta, true, UAPSD_ID);
}
else
{
sta->ps.active = false;
ssv_drv_hci_tx_resume_by_sta(sc->hci_priv, sc->hci_ops, sta->sta_idx);
if(sta->ps.pkt_ready[LEGACY_PS_ID])
ssv_set_traffic_status(sc, sta, false, LEGACY_PS_ID);
if(sta->ps.pkt_ready[UAPSD_ID])
ssv_set_traffic_status(sc, sta, false, UAPSD_ID);
}
}
/**
* ssv_ps_bh_traffic_req - Handle traffic request for STA in PS mode
*
* @sc: Driver main data
* @sta: Sta which enters/leaves PS mode
* @pkt_req: number of pkt to push
* @ps_id: type of PS data requested (@LEGACY_PS_ID or @UAPSD_ID)
*
* This function will make sure that @pkt_req are pushed to fw
* whereas the STA is in PS mode.
* If request is 0, send all traffic
* If request is greater than available pkt, reduce request
* Note: request will also be reduce if txq credits are not available
*
* NOTE: _bh_ in function name indicates that this function is called
* from the bottom_half tasklet.
*/
void ssv_ps_bh_traffic_req(struct ssv_softc *sc, struct ssv_sta *sta,
u16 pkt_req, u8 ps_id)
{
int pkt_ready_all;
if (WARN(!sta->ps.active, "sta %pM is not in Power Save mode",
sta->mac_addr))
return;
// trace_ps_traffic_req(sta, pkt_req, ps_id);
pkt_ready_all = (sta->ps.pkt_ready[ps_id] - sta->ps.sp_cnt[ps_id]);
/* Don't start SP until previous one is finished or we don't have
packet ready (which must not happen for U-APSD) */
if (sta->ps.sp_cnt[ps_id] || pkt_ready_all <= 0) {
goto done;
}
/* Adapt request to what is available. */
if (pkt_req == 0 || pkt_req > pkt_ready_all) {
pkt_req = pkt_ready_all;
}
/* Reset the SP counter */
sta->ps.sp_cnt[ps_id] = 0;
/* "dispatch" the request*/
if (_ssv_is_multicast_sta(sta->sta_idx)) {
sta->ps.sp_cnt[ps_id] = pkt_req;
} else {
sta->ps.sp_cnt[ps_id] += pkt_req;
}
done:
return;
}
void ssv_downgrade_ac(struct ssv_sta *sta, struct sk_buff *skb)
{
int8_t ac = ssv_tid2hwq[skb->priority];
if (WARN((ac > SSV_HWQ_VO),
"Unexepcted ac %d for skb before downgrade", ac))
ac = SSV_HWQ_VO;
while (sta->acm & BIT(ac)) {
if (ac == SSV_HWQ_BK) {
skb->priority = 1;
return;
}
ac--;
skb->priority = ssv_down_hwq2tid[ac];
}
}
void ssv_build_tx_bmu_header(struct tx_bmu_desc *tx_bmu_hdr, int tx_len)
{
int total_len;
total_len = tx_len + RESERVED_TX_SIZE + TX_OFFSET_ALIGN;
tx_bmu_hdr->upatterntx = 0xCAFEFADE;
tx_bmu_hdr->mpudu_len = total_len;
tx_bmu_hdr->bufctrlinfo = 0;
tx_bmu_hdr->bufctrlinfo = (total_len - sizeof(struct tx_bmu_desc)) << 8;
tx_bmu_hdr->dw7_reserve = 0x12345678;
}
int ssv_requeue_multicast_skb(struct sk_buff *skb, struct ssv_vif *ssv_vif)
{
struct net_device *ndev = skb->dev;
return ssv_start_xmit(skb, ndev);
}
void ssv_tx_flow_control(struct ssv_softc *sc, bool fc_en)
{
int i = 0;
int tx_frame = 0;
if (ssv_cfg.flowctl) {
tx_frame = ssv_drv_hci_txq_len(sc->hci_priv, sc->hci_ops);
if (fc_en) {
if (tx_frame > ssv_cfg.flowctl_high_threshold) {
for(i = 0; i < NX_VIRT_DEV_MAX; i++)
{
struct net_device *ndev = NULL;
if(NULL != sc->vif_table[i]) {
ndev = sc->vif_table[i]->ndev;
}
if(NULL != ndev) {
// SSV_LOG_DBG("to call netif_tx_stop_all_queues\n");
netif_tx_stop_all_queues(ndev);
sc->flowctl_txq_stop = true;
sc->tx.flowctl_stop_count++;
}
}
}
} else {
if (tx_frame < ssv_cfg.flowctl_low_threshold) {
for(i = 0; i < NX_VIRT_DEV_MAX; i++)
{
struct net_device *ndev = NULL;
if(NULL != sc->vif_table[i]) {
ndev = sc->vif_table[i]->ndev;
}
if(NULL != ndev) {
//SSV_LOG_DBG("to call netif_tx_wake_all_queues\n");
netif_tx_wake_all_queues(ndev);
sc->flowctl_txq_stop = false;
sc->tx.flowctl_wake_count++;
}
}
}
}
}
}
extern void _ssv_hex_dump(unsigned char *data, int len);
static netdev_tx_t _ssv_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct ssv_vif *ssv_vif = netdev_priv(dev);
struct ssv_softc *sc = ssv_vif->sc;
struct tx_bmu_desc *tx_bmu_hdr;
struct sdio_hdr *type_hdr;
struct ethhdr *eth;
struct ethhdr tmp_eth;
struct txdesc_api *desc;
struct ssv_sta *sta;
int max_headroom;
int hdr_pads;
u16 frame_len;
u8 tid;
struct sk_buff *copy_skb = NULL;
enum data_frame_types data_type = SSV_DATA_UNKNOW;
#ifdef SSV_PERFORMANCE_WATCH
struct ssv6xxx_hci_ctrl *hci_ctrl = (struct ssv6xxx_hci_ctrl *)sc->hci_priv;
unsigned long tx_now_time = jiffies;
static unsigned long tx_prev_runtime = 0;
static unsigned long tx_next_runtime = 0;
int q_num;
struct ssv_sw_txq *sw_txq;
u32 max_count = 0;
if(tx_prev_runtime == 0)
tx_prev_runtime = tx_now_time;
if(tx_next_runtime == 0)
tx_next_runtime = tx_now_time + msecs_to_jiffies(SSV_WATCH_CPU_THROUGHT_TIME_MS);
if (time_after(tx_now_time, tx_prev_runtime)) {
sc->tx_run_time += jiffies_to_msecs(tx_now_time - tx_prev_runtime);
tx_prev_runtime = tx_now_time;
}
sc->tx_run_cnt++;
sc->tx_run_size += skb->len;
if (time_after(tx_now_time, tx_next_runtime)) {
sc->tx_run_cnt = 1;
sc->tx_run_size = skb->len;
sc->tx_run_time = jiffies_to_msecs(tx_now_time - tx_next_runtime);
memset(sc->sw_txq_res, 0, SSV_SW_TXQ_NUM*6*4);
tx_next_runtime = tx_now_time + msecs_to_jiffies(SSV_WATCH_CPU_THROUGHT_TIME_MS);
}
for (q_num = (SSV_SW_TXQ_NUM - 1); q_num >= 0; q_num--) {
sw_txq = &hci_ctrl->sw_txq[q_num];
max_count = (u32)skb_queue_len(&sw_txq->qhead);
if(max_count == 0)
sc->sw_txq_res[q_num][0]++;
else if(max_count <= 32)
sc->sw_txq_res[q_num][1]++;
else if(max_count <= 64)
sc->sw_txq_res[q_num][2]++;
else if(max_count <= 128)
sc->sw_txq_res[q_num][3]++;
else if(max_count <= 256)
sc->sw_txq_res[q_num][4]++;
else
sc->sw_txq_res[q_num][5]++;
}
if(sc->skip_fmac_to_hci == true) {
dev_kfree_skb_any(skb);
skb = NULL;
return NETDEV_TX_OK;
}
#endif
ssv_vif->tx_total_cnt++;
ssv_vif->tx_total_byte += skb->len;
// _ssv_tx_mib(sc, (u8 *)skb->data, false);
#ifdef FMAC_BRIDGE
ssv_vif = ssv_bridge_tx_change(skb, dev);
//SSV_LOG_DBG("%s %d,name [%s]\n",__func__,__LINE__,ssv_vif->ndev->name);
#endif /* FMAC_BRIDGE */
ssv_tx_flow_control(sc, true);
if (ssv_cfg.directly_ack_ctrl) {
// create a copy of tx skb
copy_skb = skb_copy(skb, GFP_ATOMIC);
if (!copy_skb) {
SSV_LOG_DBG("create TX skb copy failed!\n");
} else {
dev_kfree_skb_any(skb);
skb = copy_skb;
}
}
max_headroom = SSV_TX_HDR_SIZE;
/* check whether the current skb can be used */
if (skb_shared(skb) || (skb_headroom(skb) < max_headroom)) {
struct sk_buff *newskb = skb_copy_expand(skb, max_headroom, 0,
GFP_ATOMIC);
if (unlikely(newskb == NULL))
goto free;
dev_kfree_skb_any(skb);
skb = newskb;
}
/* Get the STA id and TID information */
sta = ssv_get_tx_info(ssv_vif, skb, &tid);
if (!sta)
goto free;
data_type = ssv_get_data_frame_type(skb);
_ssv_tx_mib(sc, (u8 *)skb->data, false, sta->sta_idx, data_type);
/* Retrieve the pointer to the Ethernet data */
eth = (struct ethhdr *)skb->data;
if((sta->port_control==true)&&(htons(ETH_P_PAE)!=eth->h_proto))
{
goto free;
}
memcpy(tmp_eth.h_dest, eth->h_dest, ETH_ALEN);
memcpy(tmp_eth.h_source, eth->h_source, ETH_ALEN);
tmp_eth.h_proto = eth->h_proto;
hdr_pads = sizeof(struct ethhdr);
/* the frame len contains actual skb->data */
/* only MSDU frame*/
frame_len = (u16)skb->len;
/* | |
* |<---------- send to HCI ----------------->|
* | |
* +-------------+----------+------------+--------------------------+
* | tx_bmu_desc | sdio_hdr | txdesc_api | MSDU Frame |
* +-------------+----------+------------+--------------------------+
*
* |<--- skb->data
* |<----------- skb->len ---------------------------------------->|
* |
*
*/
/* 1st. reserve the tx descriptor */
skb_push(skb, sizeof(struct txdesc_api));
desc = (struct txdesc_api *)skb->data;
/* 2nd. reserve the type header*/
skb_push(skb, sizeof(struct sdio_hdr));
type_hdr = (struct sdio_hdr *)skb->data;
/* 3rd. reserve the tx_bum header */
skb_push(skb, sizeof(struct tx_bmu_desc));
tx_bmu_hdr = (struct tx_bmu_desc *)skb->data;
type_hdr->type = E_IPC_TYPE_DATA;
type_hdr->len = skb->len;
if (tid >= NX_NB_TXQ_PER_STA)
tid = 0;
type_hdr->queue_idx = ssv_tid2hwq[tid];
type_hdr->reserved = g_pkt_sn;
//SSV_DBG("xmit_pkt_sn: hw_idx=%d, frame_len=%u, sn=%u\n",txq->hwq->id, frame_len, g_pkt_sn);
g_pkt_sn++;
// build tx_bmu descriptor
ssv_build_tx_bmu_header(tx_bmu_hdr, type_hdr->len);
// debug - remvoe later
tid = skb->priority;
// Fill-in the descriptor
memcpy(&desc->host.eth_dest_addr, tmp_eth.h_dest, ETH_ALEN);
memcpy(&desc->host.eth_src_addr, tmp_eth.h_source, ETH_ALEN);
desc->host.ethertype = tmp_eth.h_proto;
desc->host.staid = sta->sta_idx;
desc->host.tid = tid;
//desc->host.host_hdr_pads = hdr_pads;
if (unlikely(ssv_vif->wdev.iftype == NL80211_IFTYPE_AP_VLAN)) {
desc->host.vif_idx = ssv_vif->ap_vlan.master->drv_vif_index;
} else {
desc->host.vif_idx = ssv_vif->drv_vif_index;
}
if (ssv_vif->use_4addr && (sta->sta_idx < NX_REMOTE_STA_MAX))
desc->host.flags = TXU_CNTRL_USE_4ADDR;
else
desc->host.flags = 0;
desc->host.packet_len = frame_len;
#if 0 //Debug
SSV_LOG_DBG("[%s][%d]\n", __FUNCTION__, __LINE__);
_ssv_hex_dump(skb->data, skb->len);
#endif
//for PS mode
{
u8 ps_id = (sta->uapsd_tids & (1 << tid)) ? UAPSD_ID : LEGACY_PS_ID;
if (unlikely(sta->ps.active)) {
sta->ps.pkt_ready[ps_id]++;
if (sta->ps.pkt_ready[ps_id] == 1) {
//SSV_LOG_DBG("unlikely, sta was in ps mode!\n, txq->ps_id=%d\n", txq->ps_id);
ssv_set_traffic_status(sc, sta, true, ps_id);
}
}
}
if(-1 == ssv_fmac_hci_tx(sc, skb, sta->sta_idx, false, 0))
{
goto free;
}
return NETDEV_TX_OK;
free:
if(skb)
{
dev_kfree_skb_any(skb);
skb = NULL;
}
return NETDEV_TX_OK;
}
static int ssv_get_radiotap_len(unsigned char *data)
{
struct ieee80211_radiotap_header *hdr =
(struct ieee80211_radiotap_header *)data;
return get_unaligned_le16(&hdr->it_len);
}
static netdev_tx_t ssv_mon_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct ssv_vif *ssv_vif = netdev_priv(dev);
struct ssv_softc *sc = ssv_vif->sc;
struct ieee80211_radiotap_header *prthdr =
(struct ieee80211_radiotap_header *)skb->data;
struct ieee80211_hdr *hdr;
u16 len_rthdr;
int hdrlen;
struct ssv_vif *tmp_vif = NULL;
struct ssv_sta *ssv_sta = NULL;
struct ssv_mgmt_tx_params ssv_params = {0};
struct ssv_chanctx *ctxt;
bool found = false;
u64 cookie = 0;
int error = 0;
if (!skb)
return NETDEV_TX_OK;
/* check for not even having the fixed radiotap header part */
if (unlikely(skb->len < sizeof(struct ieee80211_radiotap_header)))
goto fail; /* too short to be possibly valid */
/* is it a header version we can trust to find length from? */
if (unlikely(prthdr->it_version))
goto fail; /* only version 0 is supported */
/* then there must be a radiotap header with a length we can use */
len_rthdr = ssv_get_radiotap_len(skb->data);
/* does the skb contain enough to deliver on the alleged length? */
if (unlikely(skb->len < len_rthdr))
goto fail; /* skb too short for claimed rt header extent */
/*
* fix up the pointers accounting for the radiotap
* header still being in there. We are being given
* a precooked IEEE80211 header so no need for normal processing
*/
skb_set_mac_header(skb, len_rthdr);
/*
* these are just fixed to the end of the rt area since we
* don't have any better information and at this point, nobody cares
*/
skb_set_network_header(skb, len_rthdr);
skb_set_transport_header(skb, len_rthdr);
if (skb->len < len_rthdr + 2)
goto fail;
hdr = (struct ieee80211_hdr *)(skb->data + len_rthdr);
hdrlen = ieee80211_hdrlen(hdr->frame_control);
if (skb->len < len_rthdr + hdrlen)
goto fail;
/* remove radiotap header */
skb_pull(skb, len_rthdr);
hdr = (struct ieee80211_hdr *)(skb->data);
// find correct vif with softap mode
list_for_each_entry(tmp_vif, &sc->vifs, list) {
if (!tmp_vif->up)
continue;
if (NL80211_IFTYPE_AP != SSV_VIF_TYPE(tmp_vif))
continue;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 8, 0)
if (ether_addr_equal(tmp_vif->ndev->dev_addr, hdr->addr2))
#else
if (!compare_ether_addr(tmp_vif->ndev->dev_addr, hdr->addr2))
#endif
{
ssv_vif = tmp_vif;
found = true;
break;
}
}
if (false == found)
goto fail;
/* Get STA through list of STAs linked with the provided VIF */
found = false;
list_for_each_entry(ssv_sta, &ssv_vif->ap.sta_list, list)
{
if (ssv_sta->valid &&
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 8, 0)
ether_addr_equal(ssv_sta->mac_addr, hdr->addr1)
#else
!compare_ether_addr(ssv_sta->mac_addr, hdr->addr1)
#endif
)
{
/* the found STA */
found = true;
break;
}
}
if (false == found)
ssv_sta = NULL;
/* Get channel context */
ctxt = &sc->chanctx_table[ssv_vif->ch_index];
// fill mgmt params
ssv_params.chan = ctxt->chan_def.chan;
ssv_params.offchan = false;
ssv_params.wait = 0;
ssv_params.buf = skb->data;
ssv_params.len = skb->len;
ssv_params.no_cck = false;
ssv_params.dont_wait_for_ack = false;
error = ssv_start_mgmt_xmit(ssv_vif, ssv_sta, &ssv_params, ssv_params.offchan, &cookie);
if (error) {
SSV_LOG_DBG("Fail to send 80211 frame from mon.wlan\n");
}
fail:
dev_kfree_skb_any(skb);
return NETDEV_TX_OK;
}
/**
* netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,
* struct net_device *dev);
* Called when a packet needs to be transmitted.
* Must return NETDEV_TX_OK , NETDEV_TX_BUSY.
* (can also return NETDEV_TX_LOCKED if NETIF_F_LLTX)
*
* - Initialize the desciptor for this pkt (stored in skb before data)
* - Push the pkt in the corresponding Txq
* - If possible (i.e. credit available and not in PS) the pkt is pushed
* to fw
*/
netdev_tx_t ssv_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct ssv_vif *ssv_vif = netdev_priv(dev);
if (dev->type == ARPHRD_IEEE80211_RADIOTAP) {
/* Just handle softap with use monitor */
if (ssv_vif->use_monitor)
return ssv_mon_start_xmit(skb, dev);
else {
dev_kfree_skb_any(skb);
return NETDEV_TX_OK;
}
} else {
return _ssv_start_xmit(skb, dev);
}
}
void ssv_update_mgmt_txdesc(struct ssv_vif *vif, struct ssv_sta *sta,
struct sk_buff *skb, bool robust, bool no_cck)
{
struct tx_bmu_desc *tx_bmu_hdr;
struct sdio_hdr *type_hdr;
struct txdesc_api *desc;
struct mac_hdr *mac_hdr;
int frame_len = skb->len;
u8 *data = skb->data;
/* | |
* |<---------- send to HCI ----------------->|
* | |
* +-------------+----------+------------+--------------------------+
* | tx_bmu_desc | sdio_hdr | txdesc_api | MSDU Frame |
* +-------------+----------+------------+--------------------------+
*
* |<--- skb->data
* |<----------- skb->len ---------------------------------------->|
* |
*
*/
/* first we increase the area for tx descriptor */
skb_push(skb, sizeof(struct txdesc_api));
desc = (struct txdesc_api *)skb->data;
/* then we increase the area for sdio header*/
skb_push(skb, sizeof(struct sdio_hdr));
type_hdr = (struct sdio_hdr *)skb->data;
/* 3rd. reserve the tx_bum header */
skb_push(skb, sizeof(struct tx_bmu_desc));
tx_bmu_hdr = (struct tx_bmu_desc *)skb->data;
/*Fill the SDIO Header*/
type_hdr->type = E_IPC_TYPE_DATA;
type_hdr->len = skb->len;
type_hdr->queue_idx = ssv_tid2hwq[8];
type_hdr->reserved = g_pkt_sn++;
g_pkt_sn++;
// build tx_bmu descriptor
ssv_build_tx_bmu_header(tx_bmu_hdr, type_hdr->len);
/* Fill the Descriptor to be provided to the MAC SW */
mac_hdr = (struct mac_hdr *)data;
mac_hdr->seq = 0;
desc->host.eth_dest_addr = mac_hdr->addr1;
desc->host.eth_src_addr = mac_hdr->addr2;
desc->host.staid = (sta) ? sta->sta_idx : 0xFF;
desc->host.vif_idx = vif->drv_vif_index;
desc->host.tid = TID_MGT;
desc->host.flags = TXU_CNTRL_MGMT;
// desc->host.ethertype = 12345;
if (robust)
desc->host.flags |= TXU_CNTRL_MGMT_ROBUST;
desc->host.packet_len = frame_len;
if (no_cck)
{
desc->host.flags |= TXU_CNTRL_MGMT_NO_CCK;
}
}
static int _ssv_start_probe_pkt_xmit(struct ssv_vif *vif, struct ssv_sta *sta, struct sk_buff *skb)
{
struct ssv_softc *sc = vif->sc;
struct sk_buff *ack_skb;
int mgmt_txq = (0 == vif->drv_vif_index) ? SSV_SW_TXQ_ID_MNG0 : SSV_SW_TXQ_ID_MNG1;
struct txdesc_api *desc;
ssv_update_mgmt_txdesc(vif, sta, skb, false, false);
desc = (struct txdesc_api *)((u8 *)skb->data + sizeof(struct sdio_hdr) + sizeof(struct tx_bmu_desc));
desc->host.sw_ack = 0x1;
desc->host.sw_seq = sc->now_ack_seq++;
ack_skb = skb_clone(skb, GFP_KERNEL);
if(!ack_skb)
{
SSV_LOG_DBG("create TX ack skb failed!\n");
}
else
{
/*
* Use skb->cb to make tag
* If sw ack, it drop the probe tx with the tag directly.
*/
ack_skb->cb[0] = 'p';
ack_skb->cb[1] = 'r';
ack_skb->cb[2] = 'o';
ack_skb->cb[3] = 'b';
ack_skb->cb[4] = 'e';
ack_skb->cb[5] = 't';
ack_skb->cb[6] = 'x';
skb_queue_tail(&sc->wait_sw_ack_q, ack_skb);
}
if(-1 == ssv_fmac_hci_tx(sc, skb, mgmt_txq, false, 0))
{
return -EINVAL;
}
return 0;
}
void ssv_start_probe_pkt_xmit(struct ssv_vif *vif, struct ssv_sta *sta, bool fromds)
{
#define MAX_STATION_PROBE_PERIOD (5*1000)
struct net_device *dev = vif->ndev;
struct sk_buff *skb;
unsigned long tick = jiffies;
if (time_before(tick, (sta->probe_timestamp + msecs_to_jiffies(MAX_STATION_PROBE_PERIOD)))) {
return;
}
if (fromds) {
skb = ssv_build_nulldata_frame(sta->mac_addr, dev->dev_addr, dev->dev_addr, sta->qos, true);
} else {
skb = ssv_build_nulldata_frame(sta->mac_addr, dev->dev_addr, sta->mac_addr, sta->qos, false);
}
if (NULL == skb) {
SSV_LOG_DBG("fail to build nulldata\n");
}
if (0 == _ssv_start_probe_pkt_xmit(vif, sta, skb)) {
sta->probe_timestamp = jiffies;
}
}
#ifdef SEND_KEEP_ALIVE
void ssv_send_keep_alive(struct ssv_softc *sc)
{
struct ssv_vif *vif = NULL;
// Look for VIF entry
list_for_each_entry(vif, &sc->vifs, list) {
if (vif->up) {
if (NL80211_IFTYPE_STATION == SSV_VIF_TYPE(vif)) {
if (NULL != vif->sta.ap) {
ssv_start_probe_pkt_xmit(vif, vif->sta.ap, false);
}
}
}
}
}
#endif
void ssv_probe_client(struct ssv_softc *sc)
{
struct ssv_vif *ap_vif = NULL;
struct ssv_vif *vif = NULL;
struct ssv_sta *ssv_sta;
// Look for VIF entry
list_for_each_entry(vif, &sc->vifs, list) {
if (vif->up) {
if (NL80211_IFTYPE_AP == vif->wdev.iftype)
ap_vif = vif;
}
}
if (NULL == ap_vif)
return;
list_for_each_entry(ssv_sta, &ap_vif->ap.sta_list, list)
{
if (ssv_sta->valid) {
ssv_start_probe_pkt_xmit(ap_vif, ssv_sta, true);
}
}
}
static int _ssv_start_mgmt_xmit(struct ssv_vif *vif, struct ssv_sta *sta,
struct ssv_mgmt_tx_params *params, bool offchan,
u64 *cookie)
{
struct ssv_softc *sc = vif->sc;
struct txdesc_api *desc;
struct sk_buff *skb;
int max_headroom;
u16 frame_len;
u8 *data;
bool robust = false;
bool no_cck = params->no_cck;
int mgmt_txq = (0 == vif->drv_vif_index) ? SSV_SW_TXQ_ID_MNG0 : SSV_SW_TXQ_ID_MNG1;
struct ssv_sta *cur = NULL;
u8 sta_num = 0;
//u8 sta_idx = (sta) ? sta->sta_idx : 0xFF;
//int hdr_pads;
struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)params->buf;
/*
softap mode:
1.if packet is unicast probe rsp, sta = NULL, use list_for_each_entry for search sta.
2.if packet is broadcast, sta_idx = 11
*/
if(0 != vif->drv_vif_index){
if (SSV_VIF_TYPE(vif) == NL80211_IFTYPE_AP || SSV_VIF_TYPE(vif) == NL80211_IFTYPE_P2P_GO || SSV_VIF_TYPE(vif) == NL80211_IFTYPE_MESH_POINT) {
list_for_each_entry(cur, &vif->ap.sta_list, list){
if (!memcmp(cur->mac_addr, hdr->addr1, ETH_ALEN)){
_ssv_tx_mib(sc, (u8 *)params->buf, true, cur->sta_idx, 0);
sta_num++;
break;
}
}
} else if (SSV_VIF_TYPE(vif) == NL80211_IFTYPE_STATION || SSV_VIF_TYPE(vif) == NL80211_IFTYPE_P2P_CLIENT) {
if (vif->sta.ap != NULL) {
if (!memcmp(vif->sta.ap->mac_addr, hdr->addr1, ETH_ALEN)){
_ssv_tx_mib(sc, (u8 *)params->buf, true, vif->sta.ap->sta_idx, 0);
sta_num++;
}
}
}
if(0 == sta_num){
_ssv_tx_mib(sc, (u8 *)params->buf, true, 11, 0);
}
}
//SSV_DBG(SSV_FN_ENTRY_STR);
// max_headroom = sizeof(struct ssv_txhdr) + SSV_TX_HDR_SIZE;
max_headroom = SSV_TX_HDR_SIZE;
frame_len = params->len;
/* Create a SK Buff object that will contain the provided data */
skb = dev_alloc_skb(max_headroom + frame_len);
if (!skb)
{
return -ENOMEM;
}
/*
* Move skb->data pointer in order to reserve room for ssv_txhdr
* headroom value will be equal to sizeof(struct ssv_txhdr)
*/
skb_reserve(skb, max_headroom);
/*
* Extend the buffer data area in order to contain the provided packet
* len value (for skb) will be equal to param->len
*/
data = skb_put(skb, frame_len);
/* Copy the provided data */
memcpy(data, params->buf, frame_len);
robust = ieee80211_is_robust_mgmt_frame((void *)skb); //robust = 0;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 16, 0)
/* Update CSA counter if present */
if (unlikely(params->n_csa_offsets) &&
vif->wdev.iftype == NL80211_IFTYPE_AP &&
vif->ap.csa)
{
int i;
data = skb->data;
for (i = 0; i < params->n_csa_offsets; i++)
{
data[params->csa_offsets[i]] = vif->ap.csa->count;
}
}
#endif
ssv_update_mgmt_txdesc(vif, sta, skb, robust, no_cck);
desc = (struct txdesc_api *)((u8 *)skb->data + sizeof(struct sdio_hdr) + sizeof(struct tx_bmu_desc));
if(!params->dont_wait_for_ack)
{
struct sk_buff *ack_skb;
desc->host.sw_ack = 0x1;
desc->host.sw_seq = sc->now_ack_seq++;
ack_skb = skb_clone(skb, GFP_KERNEL);
if(!ack_skb)
{
SSV_LOG_DBG("create TX ack skb failed!\n");
}
else
{
*cookie = (unsigned long)ack_skb;
//SSV_LOG_DBG("enqueue ack_skb, seq = %d, vif idx === %d\n", desc->host.sw_seq, desc->host.vif_idx);
skb_queue_tail(&sc->wait_sw_ack_q, ack_skb);
}
}
else
{
desc->host.sw_ack = 0;
}
#if 0 //Debug
SSV_LOG_DBG("[%s][%d]\n", __FUNCTION__, __LINE__);
_ssv_hex_dump(skb->data, skb->len);
#endif
if (unlikely(sta && sta->ps.active)) {
u8 tid = skb->priority;
u8 ps_id = (sta->uapsd_tids & (1 << tid)) ? UAPSD_ID : LEGACY_PS_ID;
sta->ps.pkt_ready[ps_id]++;
if (sta->ps.pkt_ready[ps_id] == 1) {
//SSV_LOG_DBG("unlikely, sta was in ps mode!\n, ps_id=%d\n", ps_id);
ssv_set_traffic_status(sc, sta, true, ps_id);
}
}
if(-1 == ssv_fmac_hci_tx(sc, skb, mgmt_txq, false, 0))
{
return -EINVAL;
}
return 0;
}
/**
* ssv_start_mgmt_xmit - Transmit a management frame
*
* @vif: Vif that send the frame
* @sta: Destination of the frame. May be NULL if the destiantion is unknown
* to the AP.
* @params: Mgmt frame parameters
* @offchan: Indicate whether the frame must be send via the offchan TXQ.
* (is is redundant with params->offchan ?)
* @cookie: updated with a unique value to identify the frame with upper layer
*
*/
int ssv_start_mgmt_xmit(struct ssv_vif *vif, struct ssv_sta *sta,
struct ssv_mgmt_tx_params *params, bool offchan,
u64 *cookie)
{
return _ssv_start_mgmt_xmit(vif, sta, params, offchan, cookie);
}
/* return value
* < 0, hw error or no operation struct
* 0, success
* 1, no hw resource
*/
int ssv_tx_check_resource(void *app_param, struct sk_buff_head *qhead,
int reserved_page, int *p_max_count)
{
#define TX_MAX_NUM (16) ///@TBD: Where to define this value???
struct ssv_softc *sc = (struct ssv_softc *)app_param;
struct ssv6xxx_hci_ctrl *hci_ctrl = (struct ssv6xxx_hci_ctrl *)sc->hci_priv;
struct sk_buff *skb = NULL;
int tx_req_cnt =0;
u32 free_page_cnt = 0;
u16 tx_hdr_len = SSV_TX_HDR_SIZE;
int data_len = 0;
int page_count = 0;
int tx_count = 0;
int ret = -1;
bool b_no_tx_resource = false;
HCI_HWIF_GET_TX_REQ_CNT(hci_ctrl,&tx_req_cnt);
// HCI_TX_BMU_FREE_PAGE_COUNT(0x08c10050)
// b[8:0]: TX_BMU_FREE_PAGE_CNT
// b[15:9]: reserved
// b[23:16]: SD_TX_BMU_FREE_PAGE_CNT
// b[29:24]: TX_INT_FIFO_CNT_RO_REG2
// b[31:30]: reseved
if(NULL != sc)
{
if ((ret = ssv_drv_hci_read_word(sc->hci_priv, sc->hci_ops, 0x08c10050, &free_page_cnt)) != 0)
{
goto END;
}
}
else
{
ret = -EIO;
goto END;
}
#ifdef CONFIG_USB_TX_MULTI_URB
tx_req_cnt = tx_req_cnt *4;
#endif
free_page_cnt = (free_page_cnt & 0x00FF0000) >> 16;
#ifdef CONFIG_USB_TX_MULTI_URB
free_page_cnt = (free_page_cnt > tx_req_cnt)?free_page_cnt-tx_req_cnt:0;
#endif
//SSV_LOG_DBG("free page = %u, reserved_page = %d\n", free_page_cnt, reserved_page);
free_page_cnt = (free_page_cnt > reserved_page)?free_page_cnt-reserved_page:0;
if(0 == free_page_cnt)
{
b_no_tx_resource = true;
ret = 1;
goto END;
}
//Limit TX maximum number for one time.
(*p_max_count) = (TX_MAX_NUM >= (*p_max_count))?(*p_max_count):TX_MAX_NUM;
// SSV_LOG_DBG("[%s][%d] *p_max_count = %d\n", __FUNCTION__, __LINE__, *p_max_count);
//Check tx resource
{
#define HW_MMU_PAGE_SHIFT (9) //9bit, 512 bytes
#define HW_MMU_PAGE_MASK (0x1ff) //512 bytes
for(tx_count = 0; tx_count < (*p_max_count); tx_count++) {
if (0 == tx_count) {
skb = skb_peek(qhead);
} else {
struct sk_buff *next = skb->next;
if (next == (struct sk_buff *)qhead)
{
next = NULL;
}
skb = next;
// skb = ssv6xxx_hci_skb_peek_next(skb, qhead);
}
if (!skb){
break;
}
data_len = skb->len-tx_hdr_len; //this should be greater than zero
page_count = ((0 < data_len)?data_len:0);
if (page_count & HW_MMU_PAGE_MASK)
page_count = (page_count >> HW_MMU_PAGE_SHIFT) + 1;
else
page_count = page_count >> HW_MMU_PAGE_SHIFT;
page_count += 1; ///must add 1 for headroom uses
if ((free_page_cnt < page_count)) {
if(0 == tx_count)
{
b_no_tx_resource = true;
}
break;
}
// SSV_LOG_DBG("page_count = %u\n", page_count);
free_page_cnt -= page_count;
}
(*p_max_count) = tx_count;
}
//SSV_LOG_DBG("tx_count = %u, b_no_tx_resource = %u, free_page_cnt = %u\n", tx_count, b_no_tx_resource, free_page_cnt);
ret = (b_no_tx_resource ? 1 : 0);
END:
#ifdef SSV_PERFORMANCE_WATCH
if(b_no_tx_resource == 0) {
if(free_page_cnt == 0)
hci_ctrl->zero_resource_cnt++;
else if(free_page_cnt <= 12)
hci_ctrl->less_resource_10percent_cnt++;
else
hci_ctrl->over_resource_10percent_cnt++;
} else {
hci_ctrl->no_tx_resource_cnt++;
}
#endif
return ret;
}