| /* |
| * Bluetooth TI wl18xx rfkill power control via GPIO |
| * |
| * Copyright (C) 2014 Motorola, Inc. |
| * Copyright (C) 2008 Texas Instruments |
| * Initial code: Pavan Savoy <pavan.savoy@gmail.com> (wl18xx_power.c) |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * 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. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| * |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/init.h> |
| #include <linux/gpio.h> |
| #include <linux/rfkill.h> |
| #include <linux/platform_device.h> |
| #include <linux/rfkill-wl18xx.h> |
| #include <linux/delay.h> |
| #include <linux/of_gpio.h> |
| #include <linux/slab.h> |
| |
| enum wl18xx_devices { |
| WL18XX_BLUETOOTH = 0, |
| WL18XX_FM, |
| WL18XX_MAX_DEV, |
| }; |
| |
| struct wl18xx_rfkill_driver_data { |
| struct wl18xx_rfkill_platform_data *pdata; |
| struct rfkill *rfkill[WL18XX_MAX_DEV]; |
| }; |
| |
| #define WL18XX_BT_SUPPORTED(x) ((x)->bt_enable_gpio >= 0) |
| #define WL18XX_FM_SUPPORTED(x) ((x)->fm_enable_gpio >= 0) |
| |
| static void wl18xx_rfkill_destroy(struct wl18xx_rfkill_driver_data *p_drvdata); |
| |
| static int wl18xx_bt_rfkill_set_power(void *data, bool blocked) |
| { |
| struct wl18xx_rfkill_platform_data *pdata = |
| (struct wl18xx_rfkill_platform_data *)data; |
| |
| if (blocked) |
| gpio_set_value(pdata->bt_enable_gpio, 0); |
| else |
| gpio_set_value(pdata->bt_enable_gpio, 1); |
| |
| return 0; |
| } |
| |
| static int wl18xx_fm_rfkill_set_power(void *data, bool blocked) |
| { |
| struct wl18xx_rfkill_platform_data *pdata = |
| (struct wl18xx_rfkill_platform_data *)data; |
| |
| if (blocked) |
| gpio_set_value(pdata->fm_enable_gpio, 0); |
| else |
| gpio_set_value(pdata->fm_enable_gpio, 1); |
| |
| return 0; |
| } |
| |
| static const struct rfkill_ops wl18xx_bt_rfkill_ops = { |
| .set_block = wl18xx_bt_rfkill_set_power, |
| }; |
| |
| static const struct rfkill_ops wl18xx_fm_rfkill_ops = { |
| .set_block = wl18xx_fm_rfkill_set_power, |
| }; |
| |
| /** |
| Added to reset the BT chip after Ram download |
| */ |
| static ssize_t reset_wl18xx_chip(struct device *dev, |
| struct device_attribute |
| *attr, const char *buf, size_t size) |
| { |
| struct wl18xx_rfkill_platform_data *pd = dev->platform_data; |
| |
| BUG_ON(!pd); |
| if (WL18XX_BT_SUPPORTED(pd)) { |
| gpio_set_value(pd->bt_enable_gpio, 0); |
| msleep(5); |
| gpio_set_value(pd->bt_enable_gpio, 1); |
| } |
| return size; |
| } |
| static DEVICE_ATTR(reset_vio, 0200, NULL, reset_wl18xx_chip); |
| |
| #ifdef CONFIG_OF |
| static int wl18xx_rfkill_of_init( |
| struct wl18xx_rfkill_platform_data *pdata, |
| struct device_node *dt_node) |
| { |
| int rc; |
| int bt_fm_support = 0; |
| int gpio; |
| |
| /* Set default values */ |
| pdata->bt_enable_gpio = -1; |
| pdata->fm_enable_gpio = -1; |
| |
| /* Set default values */ |
| rc = of_property_read_u32(dt_node, "mot,bt_fm", &bt_fm_support); |
| if (!rc) { |
| if (bt_fm_support & 0x01) { |
| gpio = of_get_named_gpio_flags(dt_node, |
| "mot,bt_en-gpio", 0, NULL); |
| pdata->bt_enable_gpio = (gpio < 0) ? -1 : gpio; |
| } else { |
| pr_err("wl18xx_rfkill: BT support disabled"); |
| } |
| |
| if (bt_fm_support & 0x02) { |
| gpio = of_get_named_gpio_flags(dt_node, |
| "mot,fm_en-gpio", 0, NULL); |
| pdata->fm_enable_gpio = (gpio < 0) ? -1 : gpio; |
| } else { |
| pr_err("wl18xx_rfkill: FM support disabled"); |
| } |
| } else { |
| pr_err("%s: failed to read device tree", __func__); |
| } |
| |
| return rc; |
| } |
| #endif |
| |
| static int wl18xx_rfkill_gpio_init(struct wl18xx_rfkill_platform_data *pdata) |
| { |
| int rc = 0; |
| struct gpio wl18xx_rfkill_bt_gpio[] = { |
| {pdata->bt_enable_gpio, GPIOF_OUT_INIT_LOW, "wl18xx_bt_en"}, |
| }; |
| struct gpio wl18xx_rfkill_fm_gpio[] = { |
| {pdata->fm_enable_gpio, GPIOF_OUT_INIT_LOW, "wl18xx_fm_en"}, |
| }; |
| |
| if (WL18XX_BT_SUPPORTED(pdata)) { |
| rc = gpio_request_array(wl18xx_rfkill_bt_gpio, |
| ARRAY_SIZE(wl18xx_rfkill_bt_gpio)); |
| if (rc) |
| pr_err("%s: Failed to request BT GPIOs\n", __func__); |
| } |
| |
| if (WL18XX_FM_SUPPORTED(pdata)) { |
| rc = gpio_request_array(wl18xx_rfkill_fm_gpio, |
| ARRAY_SIZE(wl18xx_rfkill_fm_gpio)); |
| if (rc) |
| pr_err("%s: Failed to request FM GPIOs\n", __func__); |
| } |
| |
| return rc; |
| } |
| |
| static int wl18xx_rfkill_probe(struct platform_device *pdev) |
| { |
| int rc = 0; |
| struct wl18xx_rfkill_driver_data *dd = NULL; |
| struct wl18xx_rfkill_platform_data *pd = NULL; |
| |
| pr_info("%s\n", __func__); |
| |
| dd = kzalloc(sizeof(struct wl18xx_rfkill_driver_data), GFP_KERNEL); |
| if (dd == NULL) { |
| pr_err("%s: memory allocation failure\n", __func__); |
| rc = -ENOMEM; |
| goto wl18xx_probe_fail; |
| } |
| |
| /* set platform data */ |
| if (pdev->dev.of_node) { |
| pd = devm_kzalloc(&pdev->dev, |
| sizeof(struct wl18xx_rfkill_platform_data), |
| GFP_KERNEL); |
| if (!pd) { |
| pr_err("%s: memory allocation failure\n", __func__); |
| rc = -ENOMEM; |
| goto wl18xx_probe_fail; |
| } |
| |
| rc = wl18xx_rfkill_of_init(pd, pdev->dev.of_node); |
| if (rc) |
| goto wl18xx_probe_fail; |
| pdev->dev.platform_data = pd; |
| } else { |
| pd = pdev->dev.platform_data; |
| } |
| dd->pdata = pd; |
| |
| /* set driver data */ |
| dev_set_drvdata(&pdev->dev, dd); |
| |
| /* verify pdata */ |
| if (!WL18XX_BT_SUPPORTED(pd) && WL18XX_FM_SUPPORTED(pd)) { |
| pr_err("%s: invalid configuration\n", __func__); |
| rc = -EINVAL; |
| goto wl18xx_probe_fail; |
| } |
| |
| /* requesting gpios */ |
| rc = wl18xx_rfkill_gpio_init(pd); |
| if (rc) |
| goto gpio_err; |
| |
| if (WL18XX_BT_SUPPORTED(pd)) { |
| /* init rfkill for BT */ |
| dd->rfkill[WL18XX_BLUETOOTH] = rfkill_alloc( |
| "wl18xx Bluetooth", |
| &pdev->dev, |
| RFKILL_TYPE_BLUETOOTH, |
| &wl18xx_bt_rfkill_ops, |
| (void *)pd); |
| if (unlikely(!dd->rfkill[WL18XX_BLUETOOTH])) { |
| pr_err("%s: memory allocation failure\n", __func__); |
| rc = -ENOMEM; |
| goto err_rfkill_register; |
| } |
| |
| rfkill_set_states(dd->rfkill[WL18XX_BLUETOOTH], |
| true, false); |
| rc = rfkill_register(dd->rfkill[WL18XX_BLUETOOTH]); |
| if (unlikely(rc)) { |
| pr_err("%s: failed to register BT rfkill\n", __func__); |
| goto err_rfkill_register; |
| } |
| } |
| |
| if (WL18XX_FM_SUPPORTED(pd)) { |
| /* init rfkill for FM */ |
| dd->rfkill[WL18XX_FM] = rfkill_alloc( |
| "wl18xx FM Radio", &pdev->dev, |
| RFKILL_TYPE_FM, &wl18xx_fm_rfkill_ops, |
| (void *)pd); |
| if (unlikely(!dd->rfkill[WL18XX_FM])) { |
| pr_err("%s: memory allocation failure\n", __func__); |
| rc = -ENOMEM; |
| goto err_rfkill_register; |
| } |
| |
| rfkill_set_states(dd->rfkill[WL18XX_FM], |
| true, false); |
| rc = rfkill_register(dd->rfkill[WL18XX_FM]); |
| if (unlikely(rc)) { |
| pr_err("%s: failed to register BT rfkill\n", __func__); |
| goto err_rfkill_register; |
| } |
| } |
| |
| /* Create device file to expose interface to user space to |
| reset the vio of BT, this should be done independent of whether |
| BT/FM is initialised as both run on the same w18xx chip |
| */ |
| rc = device_create_file(&pdev->dev, &dev_attr_reset_vio); |
| if (rc) { |
| pr_err("%s: failed to create sys entry\n", __func__); |
| goto err_rfkill_register; |
| } |
| |
| goto done; |
| |
| /* Clean up for BT generic registration */ |
| err_rfkill_register: |
| wl18xx_rfkill_destroy(dd); |
| |
| gpio_err: |
| if (pd->bt_enable_gpio > 0) |
| gpio_free(pd->bt_enable_gpio); |
| if (pd->fm_enable_gpio > 0) |
| gpio_free(pd->fm_enable_gpio); |
| |
| wl18xx_probe_fail: |
| kfree(dd); |
| |
| pr_err("%s: failed with error = %d\n", __func__, rc); |
| |
| done: |
| return rc; |
| } |
| |
| static void wl18xx_rfkill_destroy(struct wl18xx_rfkill_driver_data *p_drvdata) |
| { |
| BUG_ON(!p_drvdata); |
| |
| if (p_drvdata->rfkill[WL18XX_BLUETOOTH]) { |
| rfkill_unregister(p_drvdata->rfkill[WL18XX_BLUETOOTH]); |
| rfkill_destroy(p_drvdata->rfkill[WL18XX_BLUETOOTH]); |
| } |
| |
| if (p_drvdata->rfkill[WL18XX_FM]) { |
| rfkill_unregister(p_drvdata->rfkill[WL18XX_FM]); |
| rfkill_destroy(p_drvdata->rfkill[WL18XX_FM]); |
| } |
| } |
| |
| static int wl18xx_rfkill_remove(struct platform_device *pdev) |
| { |
| struct wl18xx_rfkill_driver_data *dd; |
| struct wl18xx_rfkill_platform_data *pd; |
| |
| dd = dev_get_drvdata(&pdev->dev); |
| pd = pdev->dev.platform_data; |
| |
| /* remove the sys fs file created as part of probe*/ |
| device_remove_file(&pdev->dev, &dev_attr_reset_vio); |
| |
| if (pd->bt_enable_gpio > 0) |
| gpio_free(pd->bt_enable_gpio); |
| if (pd->fm_enable_gpio > 0) |
| gpio_free(pd->fm_enable_gpio); |
| kfree(pd); |
| |
| return 0; |
| } |
| |
| #ifdef CONFIG_OF |
| static struct of_device_id mot_wl18xx_rfkill_table[] = { |
| { .compatible = "mot,wl18xx-rfkill",}, |
| { }, |
| }; |
| #else |
| #define mot_wl18xx_rfkill_table NULL |
| #endif |
| |
| static struct platform_driver wl18xx_rfkill_platform_driver = { |
| .probe = wl18xx_rfkill_probe, |
| .remove = wl18xx_rfkill_remove, |
| .driver = { |
| .name = "wl18xx-rfkill", |
| .owner = THIS_MODULE, |
| .of_match_table = mot_wl18xx_rfkill_table, |
| }, |
| }; |
| |
| static int __init wl18xx_rfkill_init(void) |
| { |
| return platform_driver_register(&wl18xx_rfkill_platform_driver); |
| } |
| |
| static void __exit wl18xx_rfkill_exit(void) |
| { |
| platform_driver_unregister(&wl18xx_rfkill_platform_driver); |
| } |
| |
| module_init(wl18xx_rfkill_init); |
| module_exit(wl18xx_rfkill_exit); |
| |
| MODULE_ALIAS("platform:wl18xx"); |
| MODULE_DESCRIPTION("wl18xx-rfkill"); |
| MODULE_AUTHOR("Motorola"); |
| MODULE_LICENSE("GPL"); |