/* * Copyright (C) 2017 Spreadtrum Communications Inc. * * This software is licensed under the terms of the GNU General Public * License version 2, as published by the Free Software Foundation, and * may be copied, distributed, and modified under those terms. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gnss_firmware_bin.h" #include "marlin_firmware_bin.h" #include "wcn_glb.h" #include "wcn_glb_reg.h" #include "wcn_log.h" #include "wcn_misc.h" #include "wcn_procfs.h" #include "wcn_txrx.h" struct wcn_device_manage s_wcn_device; static void wcn_global_source_init(void) { wcn_boot_init(); WCN_INFO("init finish!\n"); } #ifdef CONFIG_PM_SLEEP static int wcn_resume(struct device *dev) { WCN_INFO("enter\n"); #if SUSPEND_RESUME_ENABLE slp_mgr_resume(); #endif WCN_INFO("ok\n"); return 0; } static int wcn_suspend(struct device *dev) { WCN_INFO("enter\n"); #if SUSPEND_RESUME_ENABLE slp_mgr_suspend(); #endif WCN_INFO("ok\n"); return 0; } #endif /* CONFIG_PM_SLEEP */ #if WCN_INTEGRATE_PLATFORM_DEBUG static u32 s_wcn_debug_case; static struct task_struct *s_thead_wcn_codes_debug; static int wcn_codes_debug_thread(void *data) { u32 i; static u32 is_first_time = 1; while (!kthread_should_stop()) { switch (s_wcn_debug_case) { case WCN_START_MARLIN_DEBUG: for (i = 0; i < 16; i++) start_integrate_wcn(i); s_wcn_debug_case = 0; break; case WCN_STOP_MARLIN_DEBUG: for (i = 0; i < 16; i++) stop_integrate_wcn(i); s_wcn_debug_case = 0; break; case WCN_START_MARLIN_DDR_FIRMWARE_DEBUG: for (i = 0; i < 16; i++) start_integrate_wcn(i); s_wcn_debug_case = 0; break; case WCN_START_GNSS_DEBUG: for (i = 16; i < 32; i++) start_integrate_wcn(i); s_wcn_debug_case = 0; break; case WCN_STOP_GNSS_DEBUG: for (i = 16; i < 32; i++) stop_integrate_wcn(i); s_wcn_debug_case = 0; break; case WCN_START_GNSS_DDR_FIRMWARE_DEBUG: for (i = 16; i < 32; i++) start_integrate_wcn(i); s_wcn_debug_case = 0; break; case WCN_PRINT_INFO: WCN_INFO( "cali[data=%p flag=%p]efuse=%p status=%p gnss=%p\n", &s_wssm_phy_offset_p->wifi.calibration_data, &s_wssm_phy_offset_p->wifi.calibration_flag, &s_wssm_phy_offset_p->wifi.efuse[0], &s_wssm_phy_offset_p->marlin.init_status, &s_wssm_phy_offset_p->include_gnss); break; case WCN_BRINGUP_DEBUG: if (is_first_time) { msleep(100000); is_first_time = 0; } for (i = 0; i < 16; i++) { msleep(5000); start_integrate_wcn(i); } for (i = 0; i < 16; i++) { msleep(5000); stop_integrate_wcn(i); } break; default: msleep(5000); break; } } kthread_stop(s_thead_wcn_codes_debug); return 0; } static void wcn_codes_debug(void) { /* Check reg read */ s_thead_wcn_codes_debug = kthread_create(wcn_codes_debug_thread, NULL, "wcn_codes_debug"); wake_up_process(s_thead_wcn_codes_debug); } #endif static void wcn_config_ctrlreg(struct wcn_device *wcn_dev, u32 start, u32 end) { u32 reg_read, type, i, val, utemp_val; for (i = start; i < end; i++) { val = 0; type = wcn_dev->ctrl_type[i]; reg_read = wcn_dev->ctrl_reg[i] - wcn_dev->ctrl_rw_offset[i]; wcn_regmap_read(wcn_dev->rmap[type], reg_read, &val); WCN_INFO("ctrl_reg[%d]=0x%x,read=0x%x, set=%x\n", i, reg_read, val, wcn_dev->ctrl_value[i]); utemp_val = wcn_dev->ctrl_value[i]; if (wcn_platform_chip_type() == WCN_PLATFORM_TYPE_PIKE2) { if (wcn_dev->ctrl_rw_offset[i] == 0x00) utemp_val = val | wcn_dev->ctrl_value[i]; } WCN_INFO("rmap[%d]=%p,ctrl_reg[i]=\n", type, wcn_dev->rmap[type]); wcn_regmap_raw_write_bit(wcn_dev->rmap[type], wcn_dev->ctrl_reg[i], utemp_val); if (wcn_dev->ctrl_us_delay[i] >= 10) usleep_range(wcn_dev->ctrl_us_delay[i], wcn_dev->ctrl_us_delay[i] + 40); else udelay(wcn_dev->ctrl_us_delay[i]); wcn_regmap_read(wcn_dev->rmap[type], reg_read, &val); WCN_INFO("ctrl_reg[%d] = 0x%x, val=0x%x\n", i, reg_read, val); } } void wcn_cpu_bootup(struct wcn_device *wcn_dev) { u32 reg_nr; if (!wcn_dev) return; reg_nr = wcn_dev->reg_nr < REG_CTRL_CNT_MAX ? wcn_dev->reg_nr : REG_CTRL_CNT_MAX; wcn_config_ctrlreg(wcn_dev, wcn_dev->ctrl_probe_num, reg_nr); } static struct wcn_proc_data g_proc_data; static const struct of_device_id wcn_match_table[] = { { .compatible = "sprd,integrate_marlin", .data = &g_proc_data}, { .compatible = "sprd,integrate_gnss", .data = &g_proc_data}, { }, }; static int wcn_parse_dt(struct platform_device *pdev, struct wcn_device *wcn_dev) { struct device_node *np = pdev->dev.of_node; u32 cr_num; int index, ret; u32 i; struct resource res; const struct of_device_id *of_id = of_match_node(wcn_match_table, np); struct wcn_proc_data *pcproc_data; WCN_INFO("start!\n"); if (of_id) pcproc_data = (struct wcn_proc_data *)of_id->data; else { WCN_ERR("not find matched id!"); return -EINVAL; } if (!wcn_dev) { WCN_ERR("wcn_dev NULL\n"); return -EINVAL; } /* get the wcn chip name */ ret = of_property_read_string(np, "sprd,name", (const char **)&wcn_dev->name); /* get apb reg handle */ wcn_dev->rmap[REGMAP_AON_APB] = syscon_regmap_lookup_by_phandle(np, "sprd,syscon-ap-apb"); if (IS_ERR(wcn_dev->rmap[REGMAP_AON_APB])) { WCN_ERR("failed to find sprd,syscon-ap-apb\n"); return -EINVAL; } wcn_parse_platform_chip_id(wcn_dev); /* get pmu reg handle */ wcn_dev->rmap[REGMAP_PMU_APB] = syscon_regmap_lookup_by_phandle(np, "sprd,syscon-ap-pmu"); if (IS_ERR(wcn_dev->rmap[REGMAP_PMU_APB])) { WCN_ERR("failed to find sprd,syscon-ap-pmu\n"); return -EINVAL; } /* get pub apb reg handle:SHARKLE has it, but PIKE2 hasn't */ if (wcn_platform_chip_type() == WCN_PLATFORM_TYPE_SHARKLE) { wcn_dev->rmap[REGMAP_PUB_APB] = syscon_regmap_lookup_by_phandle(np, "sprd,syscon-ap-pub-apb"); if (IS_ERR(wcn_dev->rmap[REGMAP_PUB_APB])) { WCN_ERR("failed to find sprd,syscon-ap-pub-apb\n"); return -EINVAL; } } /* get anlg wrap wcn reg handle */ wcn_dev->rmap[REGMAP_ANLG_WRAP_WCN] = syscon_regmap_lookup_by_phandle( np, "sprd,syscon-anlg-wrap-wcn"); if (IS_ERR(wcn_dev->rmap[REGMAP_ANLG_WRAP_WCN])) { WCN_ERR("failed to find sprd,anlg-wrap-wcn\n"); return -EINVAL; } if (wcn_platform_chip_type() == WCN_PLATFORM_TYPE_SHARKLE) { /* get anlg wrap wcn reg handle */ wcn_dev->rmap[REGMAP_ANLG_PHY_G6] = syscon_regmap_lookup_by_phandle( np, "sprd,syscon-anlg-phy-g6"); if (IS_ERR(wcn_dev->rmap[REGMAP_ANLG_PHY_G6])) { WCN_ERR("failed to find sprd,anlg-phy-g6\n"); return -EINVAL; } } /* SharkL3:The base Reg changed which used by AP read CP2 Regs */ if (wcn_platform_chip_type() == WCN_PLATFORM_TYPE_SHARKL3) { /* get anlg wrap wcn reg handle */ wcn_dev->rmap[REGMAP_WCN_REG] = syscon_regmap_lookup_by_phandle(np, "sprd,syscon-wcn-reg"); if (IS_ERR(wcn_dev->rmap[REGMAP_WCN_REG])) { WCN_ERR("failed to find sprd,wcn-reg\n"); return -EINVAL; } WCN_INFO("success to find sprd,wcn-reg for SharkL3 %p\n", wcn_dev->rmap[REGMAP_WCN_REG]); } if (wcn_platform_chip_type() == WCN_PLATFORM_TYPE_SHARKL3) { /* get anlg wrap wcn reg handle */ wcn_dev->rmap[REGMAP_ANLG_PHY_G5] = syscon_regmap_lookup_by_phandle(np, "sprd,syscon-anlg-phy-g5"); if (IS_ERR(wcn_dev->rmap[REGMAP_ANLG_PHY_G5])) WCN_ERR("failed to find sprd,anlg-phy-g5\n"); } ret = of_property_read_u32(np, "sprd,ctrl-probe-num", &wcn_dev->ctrl_probe_num); if (ret) { WCN_ERR("failed to find sprd,ctrl-probe-num\n"); return -EINVAL; } /* * get ctrl_reg offset, the ctrl-reg variable number, so need * to start reading from the largest until success */ cr_num = of_property_count_elems_of_size(np, "sprd,ctrl-reg", 4); if (cr_num > REG_CTRL_CNT_MAX) { WCN_ERR("DTS config err. cr_num=%d\n", cr_num); return -EINVAL; } do { ret = of_property_read_u32_array(np, "sprd,ctrl-reg", (u32 *)wcn_dev->ctrl_reg, cr_num); if (ret) cr_num--; if (!cr_num) return -EINVAL; } while (ret); wcn_dev->reg_nr = cr_num; for (i = 0; i < cr_num; i++) WCN_INFO("ctrl_reg[%d] = 0x%x\n", i, wcn_dev->ctrl_reg[i]); /* get ctrl_mask */ ret = of_property_read_u32_array(np, "sprd,ctrl-mask", (u32 *)wcn_dev->ctrl_mask, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) WCN_INFO("ctrl_mask[%d] = 0x%08x\n", i, wcn_dev->ctrl_mask[i]); /* get ctrl_value */ ret = of_property_read_u32_array(np, "sprd,ctrl-value", (u32 *)wcn_dev->ctrl_value, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) WCN_INFO("ctrl_value[%d] = 0x%08x\n", i, wcn_dev->ctrl_value[i]); /* get ctrl_rw_offset */ ret = of_property_read_u32_array(np, "sprd,ctrl-rw-offset", (u32 *)wcn_dev->ctrl_rw_offset, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) WCN_INFO("ctrl_rw_offset[%d] = 0x%08x\n", i, wcn_dev->ctrl_rw_offset[i]); /* get ctrl_us_delay */ ret = of_property_read_u32_array(np, "sprd,ctrl-us-delay", (u32 *)wcn_dev->ctrl_us_delay, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) WCN_INFO("ctrl_us_delay[%d] = 0x%08x\n", i, wcn_dev->ctrl_us_delay[i]); /* get ctrl_type */ ret = of_property_read_u32_array(np, "sprd,ctrl-type", (u32 *)wcn_dev->ctrl_type, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) WCN_INFO("ctrl_type[%d] = 0x%08x\n", i, wcn_dev->ctrl_type[i]); /* * Add a new group to control shut down WCN * get ctrl_reg offset, the ctrl-reg variable number, so need * to start reading from the largest until success */ cr_num = of_property_count_elems_of_size(np, "sprd,ctrl-shutdown-reg", 4); if (cr_num > REG_CTRL_CNT_MAX) { WCN_ERR("DTS config err. cr_num=%d\n", cr_num); return -EINVAL; } do { ret = of_property_read_u32_array(np, "sprd,ctrl-shutdown-reg", (u32 *)wcn_dev->ctrl_shutdown_reg, cr_num); if (ret) cr_num--; if (!cr_num) return -EINVAL; } while (ret); wcn_dev->reg_shutdown_nr = cr_num; for (i = 0; i < cr_num; i++) { WCN_INFO("ctrl_shutdown_reg[%d] = 0x%x\n", i, wcn_dev->ctrl_shutdown_reg[i]); } /* get ctrl_shutdown_mask */ ret = of_property_read_u32_array(np, "sprd,ctrl-shutdown-mask", (u32 *)wcn_dev->ctrl_shutdown_mask, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) { WCN_INFO("ctrl_shutdown_mask[%d] = 0x%08x\n", i, wcn_dev->ctrl_shutdown_mask[i]); } /* get ctrl_shutdown_value */ ret = of_property_read_u32_array(np, "sprd,ctrl-shutdown-value", (u32 *)wcn_dev->ctrl_shutdown_value, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) { WCN_INFO("ctrl_shutdown_value[%d] = 0x%08x\n", i, wcn_dev->ctrl_shutdown_value[i]); } /* get ctrl_shutdown_rw_offset */ ret = of_property_read_u32_array(np, "sprd,ctrl-shutdown-rw-offset", (u32 *)wcn_dev->ctrl_shutdown_rw_offset, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) { WCN_INFO("ctrl_shutdown_rw_offset[%d] = 0x%08x\n", i, wcn_dev->ctrl_shutdown_rw_offset[i]); } /* get ctrl_shutdown_us_delay */ ret = of_property_read_u32_array(np, "sprd,ctrl-shutdown-us-delay", (u32 *)wcn_dev->ctrl_shutdown_us_delay, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) { WCN_INFO("ctrl_shutdown_us_delay[%d] = 0x%08x\n", i, wcn_dev->ctrl_shutdown_us_delay[i]); } /* get ctrl_shutdown_type */ ret = of_property_read_u32_array(np, "sprd,ctrl-shutdown-type", (u32 *)wcn_dev->ctrl_shutdown_type, cr_num); if (ret) return -EINVAL; for (i = 0; i < cr_num; i++) WCN_INFO("ctrl_shutdown_type[%d] = 0x%08x\n", i, wcn_dev->ctrl_shutdown_type[i]); /* get vddwcn */ if (s_wcn_device.vddwcn == NULL) { s_wcn_device.vddwcn = devm_regulator_get(&pdev->dev, "vddwcn"); if (IS_ERR(s_wcn_device.vddwcn)) { WCN_ERR("Get regulator of vddwcn error!\n"); return -EINVAL; } } /* get vddwifipa: only MARLIN has it */ if (strcmp(wcn_dev->name, WCN_MARLIN_DEV_NAME) == 0) { wcn_dev->vddwifipa = devm_regulator_get(&pdev->dev, "vddwifipa"); if (IS_ERR(wcn_dev->vddwifipa)) { WCN_ERR("Get regulator of vddwifipa error!\n"); return -EINVAL; } } /* get cp base */ index = 0; ret = of_address_to_resource(np, index, &res); if (ret) return -EINVAL; wcn_dev->base_addr = res.start; wcn_dev->maxsz = res.end - res.start + 1; WCN_INFO("cp base = %llu, size = 0x%x\n", (u64)wcn_dev->base_addr, wcn_dev->maxsz); ret = of_property_read_string(np, "sprd,file-name", (const char **)&wcn_dev->file_path); if (!ret) WCN_INFO("firmware name:%s\n", wcn_dev->file_path); ret = of_property_read_string(np, "sprd,file-name-ext", (const char **)&wcn_dev->file_path_ext); if (!ret) WCN_INFO("firmware name ext:%s\n", wcn_dev->file_path_ext); /* get cp source file length */ ret = of_property_read_u32_index(np, "sprd,file-length", 0, &wcn_dev->file_length); WCN_INFO("wcn_dev->file_length:%d\n", wcn_dev->file_length); if (ret) return -EINVAL; wcn_dev->start = pcproc_data->start; wcn_dev->stop = pcproc_data->stop; return 0; } static struct wcn_proc_data g_proc_data = { .start = wcn_proc_native_start, .stop = wcn_proc_native_stop, }; static int wcn_platform_open(struct inode *inode, struct file *filp) { struct platform_proc_file_entry *entry = (struct platform_proc_file_entry *)PDE_DATA(inode); WCN_INFO("entry name:%s\n!", entry->name); filp->private_data = entry; return 0; } static ssize_t wcn_platform_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { return 0; } static ssize_t wcn_platform_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct platform_proc_file_entry *entry = (struct platform_proc_file_entry *)filp->private_data; struct wcn_device *wcn_dev = entry->wcn_dev; char *type = entry->name; unsigned int flag; char str[WCN_PROC_FILE_LENGTH_MAX + 1]; u32 sub_sys = 0; flag = entry->flag; WCN_INFO("type = %s flag = 0x%x\n", type, flag); if ((flag & BE_WRONLY) == 0) return -EPERM; memset(&str[0], 0, WCN_PROC_FILE_LENGTH_MAX + 1); if (copy_from_user(&str[0], buf, WCN_PROC_FILE_LENGTH_MAX) == 0) { if (strncmp(str, "gnss", strlen("gnss")) == 0) sub_sys = WCN_GNSS; else sub_sys = str[0] - '0'; } else { WCN_ERR("copy_from_user too length %s!\n", buf); return -EINVAL; } if ((flag & BE_CTRL_ON) != 0) { start_integrate_wcn(sub_sys); wcn_dev->status = CP_NORMAL_STATUS; WCN_INFO("start, str=%s!\n", str); return count; } else if ((flag & BE_CTRL_OFF) != 0) { stop_integrate_wcn(sub_sys); wcn_dev->status = CP_STOP_STATUS; WCN_INFO("stop, str=%s!\n", str); return count; } return 0; } static const struct file_operations wcn_platform_fs_fops = { .open = wcn_platform_open, .read = wcn_platform_read, .write = wcn_platform_write, }; static inline void wcn_platform_fs_init(struct wcn_device *wcn_dev) { u8 i, ucnt; unsigned int flag; umode_t mode = 0; wcn_dev->platform_fs.platform_proc_dir_entry = proc_mkdir(wcn_dev->name, NULL); memset(wcn_dev->platform_fs.entrys, 0, sizeof(wcn_dev->platform_fs.entrys)); for (flag = 0, ucnt = 0, i = 0; i < MAX_PLATFORM_ENTRY_NUM; i++, flag = 0, mode = 0) { switch (i) { case 0: wcn_dev->platform_fs.entrys[i].name = "start"; flag |= (BE_WRONLY | BE_CTRL_ON); ucnt++; break; case 1: wcn_dev->platform_fs.entrys[i].name = "stop"; flag |= (BE_WRONLY | BE_CTRL_OFF); ucnt++; break; case 2: wcn_dev->platform_fs.entrys[i].name = "status"; flag |= (BE_RDONLY | BE_RDWDTS); ucnt++; break; default: return; /* we didn't use it until now */ } wcn_dev->platform_fs.entrys[i].flag = flag; mode |= (0600); if (flag & (BE_CPDUMP | BE_MNDUMP)) mode |= 0004; WCN_INFO("entry name is %s type 0x%x addr: 0x%p\n", wcn_dev->platform_fs.entrys[i].name, wcn_dev->platform_fs.entrys[i].flag, &wcn_dev->platform_fs.entrys[i]); wcn_dev->platform_fs.entrys[i].platform_proc_dir_entry = proc_create_data( wcn_dev->platform_fs.entrys[i].name, mode, wcn_dev->platform_fs.platform_proc_dir_entry, &wcn_platform_fs_fops, &wcn_dev->platform_fs.entrys[i]); wcn_dev->platform_fs.entrys[i].wcn_dev = wcn_dev; } } static inline void wcn_platform_fs_exit(struct wcn_device *wcn_dev) { u8 i = 0; for (i = 0; i < MAX_PLATFORM_ENTRY_NUM; i++) { if (!wcn_dev->platform_fs.entrys[i].name) break; if (wcn_dev->platform_fs.entrys[i].flag != 0) { remove_proc_entry(wcn_dev->platform_fs.entrys[i].name, wcn_dev->platform_fs.platform_proc_dir_entry); } } remove_proc_entry(wcn_dev->name, NULL); } static int wcn_probe(struct platform_device *pdev) { struct wcn_device *wcn_dev; static int first = 1; WCN_INFO("start!\n"); wcn_dev = kzalloc(sizeof(struct wcn_device), GFP_KERNEL); if (!wcn_dev) return -ENOMEM; if (wcn_parse_dt(pdev, wcn_dev) < 0) { WCN_ERR("wcn_parse_dt Failed!\n"); kfree(wcn_dev); return -EINVAL; } /* init the regs which can be init in the driver probe */ wcn_config_ctrlreg(wcn_dev, 0, wcn_dev->ctrl_probe_num); mutex_init(&(wcn_dev->power_lock)); wcn_platform_fs_init(wcn_dev); platform_set_drvdata(pdev, (void *)wcn_dev); if (strcmp(wcn_dev->name, WCN_MARLIN_DEV_NAME) == 0) s_wcn_device.btwf_device = wcn_dev; else if (strcmp(wcn_dev->name, WCN_GNSS_DEV_NAME) == 0) s_wcn_device.gnss_device = wcn_dev; /* default vddcon is 1.6V, we should set it to 1.2v */ if (s_wcn_device.vddcon_voltage_setted == false) { s_wcn_device.vddcon_voltage_setted = true; wcn_power_set_vddcon(WCN_VDDCON_WORK_VOLTAGE); mutex_init(&(s_wcn_device.vddwcn_lock)); } if (strcmp(wcn_dev->name, WCN_MARLIN_DEV_NAME) == 0) { mutex_init(&(wcn_dev->vddwifipa_lock)); if (wcn_platform_chip_id() == AON_CHIP_ID_AA) wcn_power_set_vddwifipa(WCN_VDDWIFIPA_WORK_VOLTAGE); wcn_global_source_init(); /* register ops */ wcn_bus_init(); proc_fs_init(); log_dev_init(); mdbg_atcmd_owner_init(); wcn_marlin_write_efuse(); } else if (strcmp(wcn_dev->name, WCN_GNSS_DEV_NAME) == 0) gnss_write_efuse_data(); INIT_DELAYED_WORK(&wcn_dev->power_wq, wcn_power_wq); if (first) { /* Transceiver can't get into LP, so force deep sleep */ if ((wcn_platform_chip_type() == WCN_PLATFORM_TYPE_SHARKLE) || (wcn_platform_chip_type() == WCN_PLATFORM_TYPE_SHARKL3)) { wcn_sys_soft_release(); wcn_sys_deep_sleep_en(); } first = 0; } #if WCN_INTEGRATE_PLATFORM_DEBUG wcn_codes_debug(); #endif WCN_INFO("finish!\n"); return 0; } static int wcn_remove(struct platform_device *pdev) { struct wcn_device *wcn_dev = platform_get_drvdata(pdev); if (wcn_dev) WCN_INFO("dev name %s\n", wcn_dev->name); wcn_platform_fs_exit(wcn_dev); kfree(wcn_dev); wcn_dev = NULL; return 0; } static void wcn_shutdown(struct platform_device *pdev) { struct wcn_device *wcn_dev = platform_get_drvdata(pdev); if (wcn_dev && wcn_dev->wcn_open_status) { /* CPU hold on */ wcn_proc_native_stop(wcn_dev); /* wifipa power off */ if (strcmp(wcn_dev->name, WCN_MARLIN_DEV_NAME) == 0) { wcn_marlin_power_enable_vddwifipa(false); /* ASIC: disable vddcon, wifipa interval time > 1ms */ usleep_range(VDDWIFIPA_VDDCON_MIN_INTERVAL_TIME, VDDWIFIPA_VDDCON_MAX_INTERVAL_TIME); } /* vddcon power off */ wcn_power_enable_vddcon(false); wcn_sys_soft_reset(); wcn_sys_soft_release(); wcn_sys_deep_sleep_en(); WCN_INFO("dev name %s\n", wcn_dev->name); } } static SIMPLE_DEV_PM_OPS(wcn_pm_ops, wcn_suspend, wcn_resume); static struct platform_driver wcn_driver = { .driver = { .name = "wcn_integrate_platform", .pm = &wcn_pm_ops, .of_match_table = wcn_match_table, }, .probe = wcn_probe, .remove = wcn_remove, .shutdown = wcn_shutdown, }; static int __init wcn_init(void) { WCN_INFO("entry!\n"); return platform_driver_register(&wcn_driver); } late_initcall(wcn_init); static void __exit wcn_exit(void) { platform_driver_unregister(&wcn_driver); } module_exit(wcn_exit); MODULE_LICENSE("GPL v2"); MODULE_DESCRIPTION("Spreadtrum WCN Integrate Platform Driver"); MODULE_AUTHOR("YaoGuang Chen ");