diff options
author | Beomho Seo <beomho.seo@samsung.com> | 2015-02-26 17:08:59 +0900 |
---|---|---|
committer | Seung-Woo Kim <sw0312.kim@samsung.com> | 2016-12-14 13:41:19 +0900 |
commit | 6a79cb9da08db9001019adf6d21e680ebb0f556b (patch) | |
tree | 793cc4b566619307bf9d6637de1a4730918afbd1 /net | |
parent | 70f96eded713e27345c123de1b587f42d0d15853 (diff) | |
download | linux-exynos-6a79cb9da08db9001019adf6d21e680ebb0f556b.tar.gz linux-exynos-6a79cb9da08db9001019adf6d21e680ebb0f556b.tar.bz2 linux-exynos-6a79cb9da08db9001019adf6d21e680ebb0f556b.zip |
LOCAL / net: rfkill: add rfkill-bcm driver
This patch add rfkill-bcm driver for specific control Broadcom bluetooth
chip. This is based rfkill-gpio driver. This driver control on/off
Broadcom bluetooth chip, handle interrupt and notify.
Signed-off-by: Beomho Seo <beomho.seo@samsung.com>
Diffstat (limited to 'net')
-rw-r--r-- | net/rfkill/Kconfig | 9 | ||||
-rw-r--r-- | net/rfkill/Makefile | 1 | ||||
-rw-r--r-- | net/rfkill/rfkill-bcm.c | 371 |
3 files changed, 381 insertions, 0 deletions
diff --git a/net/rfkill/Kconfig b/net/rfkill/Kconfig index 4c10e7e6c9f6..b246efddb3aa 100644 --- a/net/rfkill/Kconfig +++ b/net/rfkill/Kconfig @@ -42,3 +42,12 @@ config RFKILL_GPIO If you say yes here you get support of a generic gpio RFKILL driver. The platform should fill in the appropriate fields in the rfkill_gpio_platform_data structure and pass that to the driver. + +config RFKILL_BCM + tristate "BCM RFKILL driver" + depends on RFKILL && GPIOLIB + default n + help + If you say yes here you get support of a Broadcom RFKILL + driver. The platform should fill in the appropriate fields in the + rfkill_gpio_platform_data structure and pass that to the driver. diff --git a/net/rfkill/Makefile b/net/rfkill/Makefile index 311768783f4a..430ce8578775 100644 --- a/net/rfkill/Makefile +++ b/net/rfkill/Makefile @@ -7,3 +7,4 @@ rfkill-$(CONFIG_RFKILL_INPUT) += input.o obj-$(CONFIG_RFKILL) += rfkill.o obj-$(CONFIG_RFKILL_REGULATOR) += rfkill-regulator.o obj-$(CONFIG_RFKILL_GPIO) += rfkill-gpio.o +obj-$(CONFIG_RFKILL_BCM) += rfkill-bcm.o diff --git a/net/rfkill/rfkill-bcm.c b/net/rfkill/rfkill-bcm.c new file mode 100644 index 000000000000..b520eab041b7 --- /dev/null +++ b/net/rfkill/rfkill-bcm.c @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2015 Samsung Electronics + * + * 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. + */ + +#include <linux/gpio.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/rfkill.h> +#include <linux/platform_device.h> +#include <linux/clk.h> +#include <linux/slab.h> +#include <linux/gpio/consumer.h> +#include <linux/of_gpio.h> +#include <linux/irq.h> +#include <linux/interrupt.h> +#include <linux/notifier.h> +#include <linux/hrtimer.h> +#include <net/bluetooth/bluetooth.h> +#include <net/bluetooth/hci_core.h> + +#include <linux/rfkill-gpio.h> + +struct bt_lpm_timer { + struct hrtimer htimer; + ktime_t sleep_delay; +}; + +#define BT_PM_WAKE_DELAY (3) /* 3 sec */ + +struct rfkill_bcm_data { + const char *name; + const char *clk_name; + enum rfkill_type type; + struct gpio_desc *reset_gpio; + struct gpio_desc *shutdown_gpio; + struct gpio_desc *wake_gpio; + struct gpio_desc *host_wake_gpio; + + struct rfkill *rfkill_dev; + struct clk *clk; + + bool clk_enabled; + int irq; + struct device *dev; + struct bt_lpm_timer timer; +}; + +static struct rfkill_bcm_data *bt_data; + +static enum hrtimer_restart bt_lpm_sleep(struct hrtimer *htimer) +{ + struct bt_lpm_timer *timer = container_of(htimer, struct bt_lpm_timer, + htimer); + struct rfkill_bcm_data *rfkill = container_of(timer, + struct rfkill_bcm_data, timer); + + gpiod_set_value(rfkill->wake_gpio, 0); + pm_wakeup_event(rfkill->dev, HZ / 2); + dev_dbg(rfkill->dev, "HCI Tx may be finished\n"); + + return HRTIMER_NORESTART; +} + +static void bt_lpm_wake(struct rfkill_bcm_data *rfkill) +{ + struct hrtimer *htimer = &rfkill->timer.htimer; + + hrtimer_try_to_cancel(htimer); + pm_stay_awake(rfkill->dev); + if (rfkill->clk_enabled) + gpiod_set_value(rfkill->wake_gpio, 1); + hrtimer_start(htimer, rfkill->timer.sleep_delay, HRTIMER_MODE_REL); +} + +static int bt_lpm_init(struct rfkill_bcm_data *rfkill) +{ + struct hrtimer *timer = &rfkill->timer.htimer; + + hrtimer_init(timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + timer->function = bt_lpm_sleep; + rfkill->timer.sleep_delay = ktime_set(BT_PM_WAKE_DELAY, 0); + + return 0; +} + +static int rfkill_bcm_hci_event(struct notifier_block *this, + unsigned long event, void *data) +{ + struct hci_dev *hdev = (struct hci_dev *)data; + struct rfkill_bcm_data *rfkill = bt_data; + + if (!hdev) + return NOTIFY_DONE; + + switch (event) { + case HCI_DEV_REG: + dev_dbg(rfkill->dev, "HCI device is registered\n"); + break; + case HCI_DEV_UNREG: + dev_dbg(rfkill->dev, "HCI device is unregistered\n"); + break; + case HCI_DEV_WRITE: + dev_dbg(rfkill->dev, "HCI Tx is on going\n"); + bt_lpm_wake(rfkill); + break; + default: + break; + } + + return NOTIFY_DONE; +} + +static struct notifier_block hci_event_nblock = { + .notifier_call = rfkill_bcm_hci_event, +}; + +static int rfkill_bcm_set_power(void *data, bool blocked) +{ + struct rfkill_bcm_data *rfkill = data; + + if (!blocked && !IS_ERR(rfkill->clk) && !rfkill->clk_enabled) + clk_prepare_enable(rfkill->clk); + + gpiod_set_value_cansleep(rfkill->shutdown_gpio, !blocked); + gpiod_set_value_cansleep(rfkill->reset_gpio, !blocked); + gpiod_set_value_cansleep(rfkill->wake_gpio, !blocked); + + if (blocked && !IS_ERR(rfkill->clk) && rfkill->clk_enabled) + clk_disable_unprepare(rfkill->clk); + + rfkill->clk_enabled = !blocked; + + if (!blocked) + msleep(100); + + return 0; +} + +static const struct rfkill_ops rfkill_bcm_ops = { + .set_block = rfkill_bcm_set_power, +}; + +static int rfkill_bcm_dt_probe(struct device *dev, + struct rfkill_bcm_data *rfkill) +{ + struct device_node *np = dev->of_node; + struct gpio_desc *gpio; + int ret; + + rfkill->name = np->name; + of_property_read_string(np, "rfkill-name", &rfkill->name); + of_property_read_u32(np, "rfkill-type", &rfkill->type); + of_property_read_string(np, "clock-names", &rfkill->clk_name); + + gpio = devm_gpiod_get_index(dev, "wake", 0); + if (!IS_ERR(gpio)) { + ret = gpiod_direction_output(gpio, 0); + if (ret) + return ret; + rfkill->wake_gpio = gpio; + } + + gpio = devm_gpiod_get_index(dev, "host-wake", 0); + if (!IS_ERR(gpio)) { + ret = gpiod_direction_input(gpio); + if (ret) + return ret; + rfkill->host_wake_gpio = gpio; + } + + return 0; +} + +static irqreturn_t rfkill_bcm_irq_handler(int irq, void *data) +{ + struct rfkill_bcm_data *rfkill = data; + int host_wake; + unsigned int type; + + host_wake = gpiod_get_value(rfkill->host_wake_gpio); + if (host_wake) { + /* host is waked until Rx is finished */ + pm_stay_awake(rfkill->dev); + dev_dbg(rfkill->dev, "HCI Rx is started\n"); + } else { + struct hrtimer *htimer = &rfkill->timer.htimer; + /* host is sleeped after guard time if there is no Tx*/ + if (!hrtimer_active(htimer)) + pm_wakeup_event(rfkill->dev, HZ / 2); + dev_dbg(rfkill->dev, "HCI Rx is finished\n"); + } + + type = (host_wake ? IRQF_TRIGGER_FALLING : IRQF_TRIGGER_RISING) | + IRQF_NO_SUSPEND; + irq_set_irq_type(rfkill->irq, type); + + return IRQ_HANDLED; +} + +static int rfkill_bcm_probe(struct platform_device *pdev) +{ + struct rfkill_gpio_platform_data *pdata = pdev->dev.platform_data; + struct rfkill_bcm_data *rfkill; + struct gpio_desc *gpio; + int ret; + + rfkill = devm_kzalloc(&pdev->dev, sizeof(*rfkill), GFP_KERNEL); + if (!rfkill) + return -ENOMEM; + + rfkill->dev = &pdev->dev; + + if (&pdev->dev.of_node) { + ret = rfkill_bcm_dt_probe(&pdev->dev, rfkill); + if (ret) + return ret; + } else if (pdata) { + rfkill->name = pdata->name; + rfkill->type = pdata->type; + } else { + return -ENODEV; + } + + rfkill->clk = devm_clk_get(&pdev->dev, rfkill->clk_name); + + gpio = devm_gpiod_get_index(&pdev->dev, "reset", 0); + if (!IS_ERR(gpio)) { + ret = gpiod_direction_output(gpio, 0); + if (ret) + return ret; + rfkill->reset_gpio = gpio; + } + + gpio = devm_gpiod_get_index(&pdev->dev, "shutdown", 0); + if (!IS_ERR(gpio)) { + ret = gpiod_direction_output(gpio, 0); + if (ret) + return ret; + rfkill->shutdown_gpio = gpio; + } + + /* Make sure at-least one of the GPIO is defined and that + * a name is specified for this instance + */ + if ((!rfkill->reset_gpio && !rfkill->shutdown_gpio) || !rfkill->name) { + dev_err(&pdev->dev, "invalid platform data\n"); + return -EINVAL; + } + + if (rfkill->host_wake_gpio) { + rfkill->irq = gpiod_to_irq(rfkill->host_wake_gpio); + ret = devm_request_irq(&pdev->dev, rfkill->irq, + rfkill_bcm_irq_handler, + IRQF_TRIGGER_HIGH | IRQF_NO_SUSPEND, + "rfkill-gpio-irq", rfkill); + if (ret) { + dev_err(&pdev->dev, "failed to request irq\n"); + return ret; + } + ret = irq_set_irq_wake(rfkill->irq, 1); + if (ret) + return ret; + } + + rfkill->rfkill_dev = rfkill_alloc(rfkill->name, &pdev->dev, + rfkill->type, &rfkill_bcm_ops, + rfkill); + if (!rfkill->rfkill_dev) + return -ENOMEM; + + rfkill_init_sw_state(rfkill->rfkill_dev, false); + + ret = rfkill_register(rfkill->rfkill_dev); + if (ret < 0) + return ret; + + platform_set_drvdata(pdev, rfkill); + + rfkill_set_sw_state(rfkill->rfkill_dev, true); + + device_init_wakeup(&pdev->dev, true); + + /* bluetooth hci event registration */ + if (rfkill->type == 2) { + bt_data = rfkill; + bt_lpm_init(rfkill); + hci_register_notifier(&hci_event_nblock); + } + + dev_info(&pdev->dev, "%s device registered.\n", rfkill->name); + + return 0; +} + +static int rfkill_bcm_remove(struct platform_device *pdev) +{ + struct rfkill_bcm_data *rfkill = platform_get_drvdata(pdev); + + /* bluetooth hci event unregistration */ + if (rfkill->type == 2) { + hci_unregister_notifier(&hci_event_nblock); + bt_data = NULL; + } + + rfkill_unregister(rfkill->rfkill_dev); + rfkill_destroy(rfkill->rfkill_dev); + + return 0; +} + +#ifdef CONFIG_PM +static int rfkill_bcm_suspend(struct device *dev) +{ + struct platform_device *pdev = container_of(dev, + struct platform_device, dev); + struct rfkill_bcm_data *rfkill = platform_get_drvdata(pdev); + + if (device_may_wakeup(dev)) + enable_irq_wake(rfkill->irq); + + return 0; +} + +static int rfkill_bcm_resume(struct device *dev) +{ + struct platform_device *pdev = container_of(dev, + struct platform_device, dev); + struct rfkill_bcm_data *rfkill = platform_get_drvdata(pdev); + + if (device_may_wakeup(dev)) + disable_irq_wake(rfkill->irq); + + return 0; +} +#else +#define rfkill_bcm_suspend NULL +#define rfkill_bcm_resume NULL +#endif /* CONFIG_PM */ + +const struct dev_pm_ops rfkill_bcm_pm = { + .suspend = rfkill_bcm_suspend, + .resume = rfkill_bcm_resume, +}; + +static const struct of_device_id rfkill_of_match[] = { + { .compatible = "rfkill-gpio", }, + {}, +}; + +static struct platform_driver rfkill_bcm_driver = { + .probe = rfkill_bcm_probe, + .remove = rfkill_bcm_remove, + .driver = { + .name = "rfkill_bcm", + .owner = THIS_MODULE, + .pm = &rfkill_bcm_pm, + .of_match_table = of_match_ptr(rfkill_of_match), + }, +}; + +module_platform_driver(rfkill_bcm_driver); + +MODULE_DESCRIPTION("bcm rfkill"); +MODULE_AUTHOR("beomho.seo@samsung.com"); +MODULE_LICENSE("GPL"); |