/* * Linux cfg80211 driver - Android related functions * * Copyright (C) 1999-2011, Broadcom Corporation * * Unless you and Broadcom execute a separate written software license * agreement governing use of this software, this software is licensed to you * under the terms of the GNU General Public License version 2 (the "GPL"), * available at http://www.broadcom.com/licenses/GPLv2.php, with the * following added to such license: * * As a special exception, the copyright holders of this software give you * permission to link this software with independent modules, and to copy and * distribute the resulting executable under terms of your choice, provided that * you also meet, for each linked independent module, the terms and conditions of * the license of that module. An independent module is a module which is not * derived from this software. The special exception does not apply to any * modifications of the software. * * Notwithstanding the above, under no circumstances may you combine this * software in any way with any other Broadcom software provided under a license * other than the GPL, without Broadcom's express prior written consent. * * $Id: wl_android.c,v 1.1.4.1.2.14 2011/02/09 01:40:07 Exp $ */ #include #include #include #include #include #include #include #include #include #include #include #if defined(CONFIG_WIFI_CONTROL_FUNC) #include #if (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 35)) #include #else #include #endif #endif /* CONFIG_WIFI_CONTROL_FUNC */ /* * Android private command strings, PLEASE define new private commands here * so they can be updated easily in the future (if needed) */ #define CMD_START "START" #define CMD_STOP "STOP" #define CMD_SCAN_ACTIVE "SCAN-ACTIVE" #define CMD_SCAN_PASSIVE "SCAN-PASSIVE" #define CMD_RSSI "RSSI" #define CMD_LINKSPEED "LINKSPEED" #define CMD_RXFILTER_START "RXFILTER-START" #define CMD_RXFILTER_STOP "RXFILTER-STOP" #define CMD_RXFILTER_ADD "RXFILTER-ADD" #define CMD_RXFILTER_REMOVE "RXFILTER-REMOVE" #define CMD_BTCOEXSCAN_START "BTCOEXSCAN-START" #define CMD_BTCOEXSCAN_STOP "BTCOEXSCAN-STOP" #define CMD_BTCOEXMODE "BTCOEXMODE" #define CMD_SETSUSPENDOPT "SETSUSPENDOPT" #define CMD_P2P_DEV_ADDR "P2P_DEV_ADDR" #define CMD_SETFWPATH "SETFWPATH" #define CMD_SETBAND "SETBAND" #define CMD_GETBAND "GETBAND" #define CMD_COUNTRY "COUNTRY" #ifdef PNO_SUPPORT #define CMD_PNOSSIDCLR_SET "PNOSSIDCLR" #define CMD_PNOSETUP_SET "PNOSETUP " #define CMD_PNOENABLE_SET "PNOFORCE" #define CMD_PNODEBUG_SET "PNODEBUG" #define PNO_TLV_PREFIX 'S' #define PNO_TLV_VERSION '1' #define PNO_TLV_SUBVERSION '2' #define PNO_TLV_RESERVED '0' #define PNO_TLV_TYPE_SSID_IE 'S' #define PNO_TLV_TYPE_TIME 'T' #define PNO_TLV_FREQ_REPEAT 'R' #define PNO_TLV_FREQ_EXPO_MAX 'M' typedef struct cmd_tlv { char prefix; char version; char subver; char reserved; } cmd_tlv_t; #endif /* PNO_SUPPORT */ typedef struct android_wifi_priv_cmd { char *buf; int used_len; int total_len; } android_wifi_priv_cmd; /** * Extern function declarations (TODO: move them to dhd_linux.h) */ void dhd_customer_gpio_wlan_ctrl(int onoff); uint dhd_dev_reset(struct net_device *dev, uint8 flag); void dhd_dev_init_ioctl(struct net_device *dev); #ifdef WL_CFG80211 int wl_cfg80211_get_p2p_dev_addr(struct net_device *net, struct ether_addr *p2pdev_addr); #else int wl_cfg80211_get_p2p_dev_addr(struct net_device *net, struct ether_addr *p2pdev_addr) { return 0; } #endif extern bool ap_fw_loaded; #ifdef CUSTOMER_HW2 extern char iface_name[IFNAMSIZ]; #endif /** * Local (static) functions and variables */ /* Initialize g_wifi_on to 1 so dhd_bus_start will be called for the first * time (only) in dhd_open, subsequential wifi on will be handled by * wl_android_wifi_on */ static int g_wifi_on = TRUE; /** * Local (static) function definitions */ static int wl_android_get_link_speed(struct net_device *net, char *command, int total_len) { int link_speed; int bytes_written; int error; error = wldev_get_link_speed(net, &link_speed); if (error) return -1; /* Convert Kbps to Android Mbps */ link_speed = link_speed / 1000; bytes_written = snprintf(command, total_len, "LinkSpeed %d", link_speed); DHD_INFO(("%s: command result is %s\n", __FUNCTION__, command)); return bytes_written; } static int wl_android_get_rssi(struct net_device *net, char *command, int total_len) { wlc_ssid_t ssid = {0}; int rssi; int bytes_written = 0; int error; error = wldev_get_rssi(net, &rssi); if (error) return -1; error = wldev_get_ssid(net, &ssid); if (error) return -1; if ((ssid.SSID_len == 0) || (ssid.SSID_len > DOT11_MAX_SSID_LEN)) { DHD_ERROR(("%s: wldev_get_ssid failed\n", __FUNCTION__)); } else { memcpy(command, ssid.SSID, ssid.SSID_len); bytes_written = ssid.SSID_len; } bytes_written += snprintf(&command[bytes_written], total_len, " rssi %d", rssi); DHD_INFO(("%s: command result is %s (%d)\n", __FUNCTION__, command, bytes_written)); return bytes_written; } static int wl_android_set_suspendopt(struct net_device *dev, char *command, int total_len) { int suspend_flag; int ret_now; int ret = 0; suspend_flag = *(command + strlen(CMD_SETSUSPENDOPT) + 1) - '0'; if (suspend_flag != 0) suspend_flag = 1; ret_now = net_os_set_suspend_disable(dev, suspend_flag); if (ret_now != suspend_flag) { if (!(ret = net_os_set_suspend(dev, ret_now))) DHD_INFO(("%s: Suspend Flag %d -> %d\n", __FUNCTION__, ret_now, suspend_flag)); else DHD_ERROR(("%s: failed %d\n", __FUNCTION__, ret)); } return ret; } static int wl_android_get_band(struct net_device *dev, char *command, int total_len) { uint band; int bytes_written; int error; error = wldev_get_band(dev, &band); if (error) return -1; bytes_written = snprintf(command, total_len, "Band %d", band); return bytes_written; } #ifdef PNO_SUPPORT static int wl_android_set_pno_setup(struct net_device *dev, char *command, int total_len) { wlc_ssid_t ssids_local[MAX_PFN_LIST_COUNT]; int res = -1; int nssid = 0; cmd_tlv_t *cmd_tlv_temp; char *str_ptr; int tlv_size_left; int pno_time = 0; int pno_repeat = 0; int pno_freq_expo_max = 0; DHD_INFO(("%s: command=%s, len=%d\n", __FUNCTION__, command, total_len)); if (total_len < (strlen(CMD_PNOSETUP_SET) + sizeof(cmd_tlv_t))) { DHD_ERROR(("%s argument=%d less min size\n", __FUNCTION__, total_len)); goto exit_proc; } str_ptr = command + strlen(CMD_PNOSETUP_SET); tlv_size_left = total_len - strlen(CMD_PNOSETUP_SET); cmd_tlv_temp = (cmd_tlv_t *)str_ptr; memset(ssids_local, 0, sizeof(ssids_local)); if ((cmd_tlv_temp->prefix == PNO_TLV_PREFIX) && (cmd_tlv_temp->version == PNO_TLV_VERSION) && (cmd_tlv_temp->subver == PNO_TLV_SUBVERSION)) { str_ptr += sizeof(cmd_tlv_t); tlv_size_left -= sizeof(cmd_tlv_t); if ((nssid = wl_iw_parse_ssid_list_tlv(&str_ptr, ssids_local, MAX_PFN_LIST_COUNT, &tlv_size_left)) <= 0) { DHD_ERROR(("SSID is not presented or corrupted ret=%d\n", nssid)); goto exit_proc; } else { if ((str_ptr[0] != PNO_TLV_TYPE_TIME) || (tlv_size_left <= 1)) { DHD_ERROR(("%s scan duration corrupted field size %d\n", __FUNCTION__, tlv_size_left)); goto exit_proc; } str_ptr++; pno_time = simple_strtoul(str_ptr, &str_ptr, 16); DHD_INFO(("%s: pno_time=%d\n", __FUNCTION__, pno_time)); if (str_ptr[0] != 0) { if ((str_ptr[0] != PNO_TLV_FREQ_REPEAT)) { DHD_ERROR(("%s pno repeat : corrupted field\n", __FUNCTION__)); goto exit_proc; } str_ptr++; pno_repeat = simple_strtoul(str_ptr, &str_ptr, 16); DHD_INFO(("%s :got pno_repeat=%d\n", __FUNCTION__, pno_repeat)); if (str_ptr[0] != PNO_TLV_FREQ_EXPO_MAX) { DHD_ERROR(("%s FREQ_EXPO_MAX corrupted field size\n", __FUNCTION__)); goto exit_proc; } str_ptr++; pno_freq_expo_max = simple_strtoul(str_ptr, &str_ptr, 16); DHD_INFO(("%s: pno_freq_expo_max=%d\n", __FUNCTION__, pno_freq_expo_max)); } } } else { DHD_ERROR(("%s get wrong TLV command\n", __FUNCTION__)); goto exit_proc; } res = dhd_dev_pno_set(dev, ssids_local, nssid, pno_time, pno_repeat, pno_freq_expo_max); exit_proc: return res; } #endif /* PNO_SUPPORT */ static int wl_android_get_p2p_dev_addr(struct net_device *ndev, char *command, int total_len) { int ret; int bytes_written = 0; ret = wl_cfg80211_get_p2p_dev_addr(ndev, (struct ether_addr*)command); if (ret) return 0; bytes_written = sizeof(struct ether_addr); return bytes_written; } /** * Global function definitions (declared in wl_android.h) */ int wl_android_wifi_on(struct net_device *dev) { int ret = 0; printk("%s in\n", __FUNCTION__); if (!dev) { DHD_ERROR(("%s: dev is null\n", __FUNCTION__)); return -EINVAL; } dhd_net_if_lock(dev); if (!g_wifi_on) { dhd_customer_gpio_wlan_ctrl(WLAN_RESET_ON); sdioh_start(NULL, 0); ret = dhd_dev_reset(dev, FALSE); sdioh_start(NULL, 1); dhd_dev_init_ioctl(dev); g_wifi_on = 1; } dhd_net_if_unlock(dev); return ret; } int wl_android_wifi_off(struct net_device *dev) { int ret = 0; printk("%s in\n", __FUNCTION__); if (!dev) { DHD_TRACE(("%s: dev is null\n", __FUNCTION__)); return -EINVAL; } dhd_net_if_lock(dev); if (g_wifi_on) { dhd_dev_reset(dev, 1); dhd_customer_gpio_wlan_ctrl(WLAN_RESET_OFF); sdioh_stop(NULL); /* clean up dtim_skip setting */ net_os_set_dtim_skip(dev, TRUE); g_wifi_on = 0; } dhd_net_if_unlock(dev); return ret; } static int wl_android_set_fwpath(struct net_device *net, char *command, int total_len) { if ((strlen(command) - strlen(CMD_SETFWPATH)) > MOD_PARAM_PATHLEN) return -1; bcm_strncpy_s(fw_path, sizeof(fw_path), command + strlen(CMD_SETFWPATH) + 1, MOD_PARAM_PATHLEN - 1); if (strstr(fw_path, "apsta") != NULL) { DHD_INFO(("GOT APSTA FIRMWARE\n")); ap_fw_loaded = TRUE; } else { DHD_INFO(("GOT STA FIRMWARE\n")); ap_fw_loaded = FALSE; } return 0; } int wl_android_priv_cmd(struct net_device *net, struct ifreq *ifr, int cmd) { int ret = 0; char *command = NULL; int bytes_written = 0; android_wifi_priv_cmd priv_cmd; net_os_wake_lock(net); if (!ifr->ifr_data) { ret = -EINVAL; goto exit; } if (copy_from_user(&priv_cmd, ifr->ifr_data, sizeof(android_wifi_priv_cmd))) { ret = -EFAULT; goto exit; } command = kmalloc(priv_cmd.total_len, GFP_KERNEL); if (!command) { DHD_ERROR(("%s: failed to allocate memory\n", __FUNCTION__)); ret = -ENOMEM; goto exit; } if (copy_from_user(command, priv_cmd.buf, priv_cmd.total_len)) { ret = -EFAULT; goto exit; } DHD_INFO(("%s: Android private cmd \"%s\" on %s\n", __FUNCTION__, command, ifr->ifr_name)); if (strnicmp(command, CMD_START, strlen(CMD_START)) == 0) { DHD_INFO(("%s, Received regular START command\n", __FUNCTION__)); bytes_written = wl_android_wifi_on(net); } else if (strnicmp(command, CMD_SETFWPATH, strlen(CMD_SETFWPATH)) == 0) { bytes_written = wl_android_set_fwpath(net, command, priv_cmd.total_len); } if (!g_wifi_on) { DHD_ERROR(("%s: Ignore private cmd \"%s\" - iface %s is down\n", __FUNCTION__, command, ifr->ifr_name)); ret = 0; goto exit; } if (strnicmp(command, CMD_STOP, strlen(CMD_STOP)) == 0) { bytes_written = wl_android_wifi_off(net); } else if (strnicmp(command, CMD_SCAN_ACTIVE, strlen(CMD_SCAN_ACTIVE)) == 0) { /* TBD: SCAN-ACTIVE */ } else if (strnicmp(command, CMD_SCAN_PASSIVE, strlen(CMD_SCAN_PASSIVE)) == 0) { /* TBD: SCAN-PASSIVE */ } else if (strnicmp(command, CMD_RSSI, strlen(CMD_RSSI)) == 0) { bytes_written = wl_android_get_rssi(net, command, priv_cmd.total_len); } else if (strnicmp(command, CMD_LINKSPEED, strlen(CMD_LINKSPEED)) == 0) { bytes_written = wl_android_get_link_speed(net, command, priv_cmd.total_len); } else if (strnicmp(command, CMD_RXFILTER_START, strlen(CMD_RXFILTER_START)) == 0) { bytes_written = net_os_set_packet_filter(net, 1); } else if (strnicmp(command, CMD_RXFILTER_STOP, strlen(CMD_RXFILTER_STOP)) == 0) { bytes_written = net_os_set_packet_filter(net, 0); } else if (strnicmp(command, CMD_RXFILTER_ADD, strlen(CMD_RXFILTER_ADD)) == 0) { int filter_num = *(command + strlen(CMD_RXFILTER_ADD) + 1) - '0'; bytes_written = net_os_rxfilter_add_remove(net, TRUE, filter_num); } else if (strnicmp(command, CMD_RXFILTER_REMOVE, strlen(CMD_RXFILTER_REMOVE)) == 0) { int filter_num = *(command + strlen(CMD_RXFILTER_REMOVE) + 1) - '0'; bytes_written = net_os_rxfilter_add_remove(net, FALSE, filter_num); } else if (strnicmp(command, CMD_BTCOEXSCAN_START, strlen(CMD_BTCOEXSCAN_START)) == 0) { /* TBD: BTCOEXSCAN-START */ } else if (strnicmp(command, CMD_BTCOEXSCAN_STOP, strlen(CMD_BTCOEXSCAN_STOP)) == 0) { /* TBD: BTCOEXSCAN-STOP */ } else if (strnicmp(command, CMD_BTCOEXMODE, strlen(CMD_BTCOEXMODE)) == 0) { /* TBD: BTCOEXMODE */ } else if (strnicmp(command, CMD_SETSUSPENDOPT, strlen(CMD_SETSUSPENDOPT)) == 0) { bytes_written = wl_android_set_suspendopt(net, command, priv_cmd.total_len); } else if (strnicmp(command, CMD_SETBAND, strlen(CMD_SETBAND)) == 0) { uint band = *(command + strlen(CMD_SETBAND) + 1) - '0'; bytes_written = wldev_set_band(net, band); } else if (strnicmp(command, CMD_GETBAND, strlen(CMD_GETBAND)) == 0) { bytes_written = wl_android_get_band(net, command, priv_cmd.total_len); } else if (strnicmp(command, CMD_COUNTRY, strlen(CMD_COUNTRY)) == 0) { char *country_code = command + strlen(CMD_COUNTRY) + 1; bytes_written = wldev_set_country(net, country_code); } #ifdef PNO_SUPPORT else if (strnicmp(command, CMD_PNOSSIDCLR_SET, strlen(CMD_PNOSSIDCLR_SET)) == 0) { bytes_written = dhd_dev_pno_reset(net); } else if (strnicmp(command, CMD_PNOSETUP_SET, strlen(CMD_PNOSETUP_SET)) == 0) { bytes_written = wl_android_set_pno_setup(net, command, priv_cmd.total_len); } else if (strnicmp(command, CMD_PNOENABLE_SET, strlen(CMD_PNOENABLE_SET)) == 0) { uint pfn_enabled = *(command + strlen(CMD_PNOENABLE_SET) + 1) - '0'; bytes_written = dhd_dev_pno_enable(net, pfn_enabled); } #endif else if (strnicmp(command, CMD_P2P_DEV_ADDR, strlen(CMD_P2P_DEV_ADDR)) == 0) { bytes_written = wl_android_get_p2p_dev_addr(net, command, priv_cmd.total_len); } else { DHD_ERROR(("Unknown PRIVATE command %s - ignored\n", command)); snprintf(command, 3, "OK"); bytes_written = strlen("OK"); } if (bytes_written > 0) { if (bytes_written > priv_cmd.total_len) { DHD_ERROR(("%s: bytes_written = %d\n", __FUNCTION__, bytes_written)); bytes_written = priv_cmd.total_len; } else { bytes_written++; } priv_cmd.used_len = bytes_written; if (copy_to_user(priv_cmd.buf, command, bytes_written)) { DHD_ERROR(("%s: failed to copy data to user buffer\n", __FUNCTION__)); ret = -EFAULT; } } else { ret = bytes_written; } exit: net_os_wake_unlock(net); if (command) { kfree(command); } return ret; } int wl_android_init(void) { int ret = 0; dhd_msg_level = DHD_ERROR_VAL; #ifdef ENABLE_INSMOD_NO_FW_LOAD dhd_download_fw_on_driverload = FALSE; #endif /* ENABLE_INSMOD_NO_FW_LOAD */ #ifdef CUSTOMER_HW2 if (!iface_name[0]) bcm_strncpy_s(iface_name, IFNAMSIZ, "wlan", IFNAMSIZ); #endif /* CUSTOMER_HW2 */ return ret; } int wl_android_exit(void) { int ret = 0; return ret; } int wl_android_post_init(void) { int ret = 0; if (!dhd_download_fw_on_driverload) { /* Call customer gpio to turn off power with WL_REG_ON signal */ dhd_customer_gpio_wlan_ctrl(WLAN_RESET_OFF); g_wifi_on = 0; } return ret; } /** * Functions for Android WiFi card detection */ #if defined(CONFIG_WIFI_CONTROL_FUNC) static int g_wifidev_registered = 0; static struct semaphore wifi_control_sem; static struct wifi_platform_data *wifi_control_data = NULL; static struct resource *wifi_irqres = NULL; static int wifi_add_dev(void); static void wifi_del_dev(void); int wl_android_wifictrl_func_add(void) { int ret = 0; sema_init(&wifi_control_sem, 0); ret = wifi_add_dev(); if (ret) { DHD_ERROR(("%s: platform_driver_register failed\n", __FUNCTION__)); return ret; } g_wifidev_registered = 1; /* Waiting callback after platform_driver_register is done or exit with error */ if (down_timeout(&wifi_control_sem, msecs_to_jiffies(1000)) != 0) { ret = -EINVAL; DHD_ERROR(("%s: platform_driver_register timeout\n", __FUNCTION__)); } return ret; } void wl_android_wifictrl_func_del(void) { if (g_wifidev_registered) { wifi_del_dev(); g_wifidev_registered = 0; } } void* wl_android_prealloc(int section, unsigned long size) { void *alloc_ptr = NULL; if (wifi_control_data && wifi_control_data->mem_prealloc) { alloc_ptr = wifi_control_data->mem_prealloc(section, size); if (alloc_ptr) { DHD_INFO(("success alloc section %d\n", section)); bzero(alloc_ptr, size); return alloc_ptr; } } DHD_ERROR(("can't alloc section %d\n", section)); return 0; } int wifi_get_irq_number(unsigned long *irq_flags_ptr) { if (wifi_irqres) { *irq_flags_ptr = wifi_irqres->flags & IRQF_TRIGGER_MASK; return (int)wifi_irqres->start; } #ifdef CUSTOM_OOB_GPIO_NUM return CUSTOM_OOB_GPIO_NUM; #else return -1; #endif } int wifi_set_power(int on, unsigned long msec) { DHD_ERROR(("%s = %d\n", __FUNCTION__, on)); if (wifi_control_data && wifi_control_data->set_power) { wifi_control_data->set_power(on); } if (msec) mdelay(msec); return 0; } #if (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 35)) int wifi_get_mac_addr(unsigned char *buf) { DHD_ERROR(("%s\n", __FUNCTION__)); if (!buf) return -EINVAL; if (wifi_control_data && wifi_control_data->get_mac_addr) { return wifi_control_data->get_mac_addr(buf); } return -EOPNOTSUPP; } #endif /* (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 35)) */ #if (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 39)) void *wifi_get_country_code(char *ccode) { DHD_TRACE(("%s\n", __FUNCTION__)); if (!ccode) return NULL; if (wifi_control_data && wifi_control_data->get_country_code) { return wifi_control_data->get_country_code(ccode); } return NULL; } #endif /* (LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 39)) */ static int wifi_set_carddetect(int on) { DHD_ERROR(("%s = %d\n", __FUNCTION__, on)); if (wifi_control_data && wifi_control_data->set_carddetect) { wifi_control_data->set_carddetect(on); } return 0; } static int wifi_probe(struct platform_device *pdev) { struct wifi_platform_data *wifi_ctrl = (struct wifi_platform_data *)(pdev->dev.platform_data); DHD_ERROR(("## %s\n", __FUNCTION__)); wifi_irqres = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "bcmdhd_wlan_irq"); if (wifi_irqres == NULL) wifi_irqres = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "bcm4329_wlan_irq"); wifi_control_data = wifi_ctrl; wifi_set_power(1, 0); /* Power On */ wifi_set_carddetect(1); /* CardDetect (0->1) */ up(&wifi_control_sem); return 0; } static int wifi_remove(struct platform_device *pdev) { struct wifi_platform_data *wifi_ctrl = (struct wifi_platform_data *)(pdev->dev.platform_data); DHD_ERROR(("## %s\n", __FUNCTION__)); wifi_control_data = wifi_ctrl; wifi_set_power(0, 0); /* Power Off */ wifi_set_carddetect(0); /* CardDetect (1->0) */ up(&wifi_control_sem); return 0; } static int wifi_suspend(struct platform_device *pdev, pm_message_t state) { DHD_TRACE(("##> %s\n", __FUNCTION__)); #if defined(OOB_INTR_ONLY) bcmsdh_oob_intr_set(0); #endif /* (OOB_INTR_ONLY) */ return 0; } static int wifi_resume(struct platform_device *pdev) { DHD_TRACE(("##> %s\n", __FUNCTION__)); #if defined(OOB_INTR_ONLY) bcmsdh_oob_intr_set(1); #endif /* (OOB_INTR_ONLY) */ return 0; } static struct platform_driver wifi_device = { .probe = wifi_probe, .remove = wifi_remove, .suspend = wifi_suspend, .resume = wifi_resume, .driver = { .name = "bcmdhd_wlan", } }; static struct platform_driver wifi_device_legacy = { .probe = wifi_probe, .remove = wifi_remove, .suspend = wifi_suspend, .resume = wifi_resume, .driver = { .name = "bcm4329_wlan", } }; static int wifi_add_dev(void) { DHD_TRACE(("## Calling platform_driver_register\n")); platform_driver_register(&wifi_device); platform_driver_register(&wifi_device_legacy); return 0; } static void wifi_del_dev(void) { DHD_TRACE(("## Unregister platform_driver_register\n")); platform_driver_unregister(&wifi_device); platform_driver_unregister(&wifi_device_legacy); } #endif /* defined(CONFIG_WIFI_CONTROL_FUNC) */