blob: cd11cce4ea9d79c784359b48f310cca49b14d930 [file] [log] [blame]
/*
* drivers/misc/bluedroid_pm.c
*
* Copyright (c) 2014, NVIDIA Corporation. All rights reserved.
*
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* ldisc support based on drivers/bluetooth/bcmbt_lpm_ldisc.c
* Copyright 2003 - 2011 Broadcom Corporation.
*
*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/regulator/consumer.h>
#include <linux/gpio/consumer.h>
#include <linux/rfkill.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/wakelock.h>
#include <linux/serial_core.h>
#include <linux/tty.h>
#define BCM_SHARED_UART_MAGIC 0x80
#define TIO_ASSERT_BT_WAKE _IO(BCM_SHARED_UART_MAGIC, 3)
#define TIO_DEASSERT_BT_WAKE _IO(BCM_SHARED_UART_MAGIC, 4)
#define TIO_GET_BT_WAKE_STATE _IO(BCM_SHARED_UART_MAGIC, 5)
/* BT may not be ready to sleep, these parameters configure retries */
#define TX_RETRY_INTERVAL 5
#define MAX_SLEEP_RETRIES 5
struct bluedroid_pm_data {
struct gpio_desc *gpio_shutdown;
struct gpio_desc *host_wake;
struct gpio_desc *ext_wake;
bool is_blocked;
unsigned host_wake_irq;
bool wake_on_irq;
int retries;
struct regulator *vdd_3v3;
struct regulator *vdd_1v8;
struct rfkill *rfkill;
struct wake_lock wake_lock;
struct device *dev;
struct delayed_work retry_work;
};
static struct bluedroid_pm_data *bluedroid_pm;
static struct tty_ldisc_ops bluedroid_pm_ldisc_ops;
static irqreturn_t bluedroid_pm_hostwake_isr(int irq, void *dev_id)
{
/* This handler is empty, but allows us to register with pm_suspend */
return IRQ_HANDLED;
}
/**
* Handles bluedroid_pm retries
* @param data: bluedroid_pm delayed work structure.
*/
static void bluedroid_pm_work(struct work_struct *work)
{
struct bluedroid_pm_data *bluedroid_pm =
container_of(work, struct bluedroid_pm_data, retry_work.work);
if (++bluedroid_pm->retries >= MAX_SLEEP_RETRIES)
dev_err(bluedroid_pm->dev, "Couldn't sleep after %d tries",
bluedroid_pm->retries);
if (!gpiod_get_value_cansleep(bluedroid_pm->host_wake)) {
/* BT can sleep */
dev_dbg(bluedroid_pm->dev, "Tx and Rx are idle, BT sleeping");
gpiod_set_value_cansleep(bluedroid_pm->ext_wake, 1);
wake_unlock(&bluedroid_pm->wake_lock);
} else {
/* Try again later */
dev_dbg(bluedroid_pm->dev, "Rx is busy, try again later");
schedule_delayed_work(&bluedroid_pm->retry_work,
(TX_RETRY_INTERVAL * HZ));
}
}
static int bluedroid_pm_tty_ioctl(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg)
{
if (!tty || tty->magic != TTY_MAGIC)
return -ENODEV;
if (!bluedroid_pm)
return -ENODEV;
switch (cmd) {
case TIO_ASSERT_BT_WAKE:
dev_dbg(bluedroid_pm->dev, "TIO_ASSERT_BT_WAKE\n");
gpiod_set_value_cansleep(bluedroid_pm->ext_wake, 0);
wake_lock(&bluedroid_pm->wake_lock);
cancel_delayed_work_sync(&bluedroid_pm->retry_work);
return 0;
case TIO_DEASSERT_BT_WAKE:
dev_dbg(bluedroid_pm->dev, "TIO_DEASSERT_BT_WAKE\n");
bluedroid_pm->retries = 0;
schedule_delayed_work(&bluedroid_pm->retry_work, 0);
return 0;
case TIO_GET_BT_WAKE_STATE:
dev_dbg(bluedroid_pm->dev, "TIO_GET_BT_WAKE_STATE\n");
return gpiod_get_value_cansleep(bluedroid_pm->ext_wake);
default:
return n_tty_ioctl_helper(tty, file, cmd, arg);
}
}
static long bluedroid_pm_tty_compat_ioctl(struct tty_struct *tty,
struct file *file,
unsigned int cmd, unsigned long arg)
{
return bluedroid_pm_tty_ioctl(tty, file, cmd, arg);
}
static int bluedroid_pm_tty_init(void)
{
int err;
/* Inherit the N_TTY's ops */
n_tty_inherit_ops(&bluedroid_pm_ldisc_ops);
bluedroid_pm_ldisc_ops.owner = THIS_MODULE;
bluedroid_pm_ldisc_ops.name = "bluedroid_pm_tty";
bluedroid_pm_ldisc_ops.ioctl = bluedroid_pm_tty_ioctl;
bluedroid_pm_ldisc_ops.compat_ioctl = bluedroid_pm_tty_compat_ioctl;
err = tty_register_ldisc(N_BRCM_HCI, &bluedroid_pm_ldisc_ops);
if (err)
pr_err("can't register N_BRCM_HCI line discipline\n");
else
pr_info("N_BRCM_HCI line discipline registered\n");
return err;
}
static void bluedroid_pm_tty_cleanup(void)
{
int err;
err = tty_unregister_ldisc(N_BRCM_HCI);
if (err)
pr_err("can't unregister N_BRCM_HCI line discipline\n");
else
pr_info("N_BRCM_HCI line discipline removed\n");
}
static int bluedroid_pm_rfkill_set_power(void *data, bool blocked)
{
struct bluedroid_pm_data *bluedroid_pm = data;
/*
* check if BT gpio_shutdown line status and current request are same.
* If same, then return, else perform requested operation.
*/
if (gpiod_get_value_cansleep(bluedroid_pm->gpio_shutdown) == blocked)
return 0;
if (blocked) {
gpiod_set_value_cansleep(bluedroid_pm->gpio_shutdown, 1);
regulator_disable(bluedroid_pm->vdd_3v3);
regulator_disable(bluedroid_pm->vdd_1v8);
wake_unlock(&bluedroid_pm->wake_lock);
} else {
int ret = 0;
ret = regulator_enable(bluedroid_pm->vdd_3v3);
if (ret) {
dev_err(bluedroid_pm->dev, "vdd_3v3 not enabled\n");
return ret;
}
ret = regulator_enable(bluedroid_pm->vdd_1v8);
if (ret) {
dev_err(bluedroid_pm->dev, "vdd_1v8 not enabled\n");
regulator_disable(bluedroid_pm->vdd_3v3);
return ret;
}
gpiod_set_value_cansleep(bluedroid_pm->gpio_shutdown, 0);
}
bluedroid_pm->is_blocked = blocked;
return 0;
}
static const struct rfkill_ops bluedroid_pm_rfkill_ops = {
.set_block = bluedroid_pm_rfkill_set_power,
};
static const struct of_device_id bluedroid_match[] = {
{ .compatible = "bcm,bt-bcm4354" },
{}
};
MODULE_DEVICE_TABLE(of, bluedroid_match);
static int bluedroid_pm_probe(struct platform_device *pdev)
{
int ret;
bluedroid_pm = devm_kzalloc(&pdev->dev, sizeof(*bluedroid_pm),
GFP_KERNEL);
if (!bluedroid_pm)
return -ENOMEM;
bluedroid_pm->dev = &pdev->dev;
bluedroid_pm->vdd_3v3 = devm_regulator_get(&pdev->dev, "avdd");
if (IS_ERR(bluedroid_pm->vdd_3v3)) {
dev_err(bluedroid_pm->dev, "regulator avdd not available\n");
return PTR_ERR(bluedroid_pm->vdd_3v3);
}
bluedroid_pm->vdd_1v8 = devm_regulator_get(&pdev->dev, "dvdd");
if (IS_ERR(bluedroid_pm->vdd_1v8)) {
dev_err(bluedroid_pm->dev, "regulator dvdd not available\n");
return PTR_ERR(bluedroid_pm->vdd_1v8);
}
bluedroid_pm->gpio_shutdown = devm_gpiod_get(bluedroid_pm->dev,
"bt_reg_on", GPIOD_OUT_HIGH);
if (IS_ERR(bluedroid_pm->gpio_shutdown)) {
dev_err(bluedroid_pm->dev, "shutdown gpio not registered\n");
return PTR_ERR(bluedroid_pm->gpio_shutdown);
}
bluedroid_pm->ext_wake = devm_gpiod_get(bluedroid_pm->dev,
"bt_ext_wake", GPIOD_OUT_LOW);
if (IS_ERR(bluedroid_pm->ext_wake)) {
dev_err(bluedroid_pm->dev, "ext_wake gpio not registered\n");
return PTR_ERR(bluedroid_pm->ext_wake);
}
bluedroid_pm->host_wake = devm_gpiod_get(bluedroid_pm->dev,
"bt_host_wake", GPIOD_IN);
if (IS_ERR(bluedroid_pm->host_wake)) {
dev_err(bluedroid_pm->dev, "host_wake gpio not registered\n");
return PTR_ERR(bluedroid_pm->host_wake);
}
bluedroid_pm->host_wake_irq = gpiod_to_irq(bluedroid_pm->host_wake);
ret = devm_request_irq(bluedroid_pm->dev,
bluedroid_pm->host_wake_irq,
bluedroid_pm_hostwake_isr,
IRQF_TRIGGER_RISING,
"bluetooth hostwake", bluedroid_pm);
if (ret) {
dev_err(bluedroid_pm->dev, "Failed to get host_wake irq\n");
return ret;
}
/*
* At this point the GPIOs and regulators avaiable to
* register with rfkill are defined
*/
bluedroid_pm->rfkill = rfkill_alloc("bluedroid_pm", &pdev->dev,
RFKILL_TYPE_BLUETOOTH, &bluedroid_pm_rfkill_ops,
bluedroid_pm);
if (unlikely(!bluedroid_pm->rfkill))
return -ENOMEM;
bluedroid_pm->is_blocked = true;
rfkill_set_states(bluedroid_pm->rfkill,
bluedroid_pm->is_blocked, false);
ret = rfkill_register(bluedroid_pm->rfkill);
if (unlikely(ret)) {
rfkill_destroy(bluedroid_pm->rfkill);
dev_err(bluedroid_pm->dev, "Couldn't register rfkill");
return ret;
}
wake_lock_init(&bluedroid_pm->wake_lock, WAKE_LOCK_SUSPEND,
"bluedroid_pm");
INIT_DELAYED_WORK(&bluedroid_pm->retry_work, bluedroid_pm_work);
ret = bluedroid_pm_tty_init();
if (ret) {
dev_err(bluedroid_pm->dev, "tty_init failed");
cancel_delayed_work_sync(&bluedroid_pm->retry_work);
wake_lock_destroy(&bluedroid_pm->wake_lock);
rfkill_unregister(bluedroid_pm->rfkill);
rfkill_destroy(bluedroid_pm->rfkill);
return ret;
}
platform_set_drvdata(pdev, bluedroid_pm);
dev_dbg(bluedroid_pm->dev, "driver successfully registered");
return 0;
}
static int bluedroid_pm_remove(struct platform_device *pdev)
{
struct bluedroid_pm_data *bluedroid_pm = platform_get_drvdata(pdev);
bluedroid_pm_tty_cleanup();
cancel_delayed_work_sync(&bluedroid_pm->retry_work);
wake_lock_destroy(&bluedroid_pm->wake_lock);
rfkill_unregister(bluedroid_pm->rfkill);
rfkill_destroy(bluedroid_pm->rfkill);
return 0;
}
static int __maybe_unused bluedroid_pm_suspend(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct bluedroid_pm_data *bluedroid_pm = platform_get_drvdata(pdev);
if (!bluedroid_pm->is_blocked)
bluedroid_pm->wake_on_irq =
(enable_irq_wake(bluedroid_pm->host_wake_irq) == 0);
return 0;
}
static int __maybe_unused bluedroid_pm_resume(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct bluedroid_pm_data *bluedroid_pm = platform_get_drvdata(pdev);
if (bluedroid_pm->wake_on_irq) {
disable_irq_wake(bluedroid_pm->host_wake_irq);
bluedroid_pm->wake_on_irq = false;
}
return 0;
}
static SIMPLE_DEV_PM_OPS(bluedroid_pm_ops,
bluedroid_pm_suspend, bluedroid_pm_resume);
static struct platform_driver bluedroid_pm_driver = {
.probe = bluedroid_pm_probe,
.remove = bluedroid_pm_remove,
.driver = {
.name = "bluedroid_pm",
.pm = &bluedroid_pm_ops,
.of_match_table = bluedroid_match,
},
};
module_platform_driver(bluedroid_pm_driver);
MODULE_ALIAS_LDISC(N_BRCM_HCI);
MODULE_DESCRIPTION("bluedroid PM");
MODULE_AUTHOR("NVIDIA");
MODULE_LICENSE("GPL");