177 lines
4.6 KiB
C
177 lines
4.6 KiB
C
/*
|
|
* Copyright (C) 2009 Google, 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 <linux/module.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/hrtimer.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/serial_core.h>
|
|
#include <mach/bcm_bt_lpm.h>
|
|
#include <asm/gpio.h>
|
|
|
|
/*
|
|
* Manage WAKE and HOST_WAKE low power mode signals for Broadcom
|
|
* Bluetooth chipsets.
|
|
*
|
|
* This driver needs to be tightly coupled with a uart driver that supports
|
|
* request_clock_off_locked() and request_clock_on_locked(), to clock off and
|
|
* on the uart indepdently of Linux suspend/resume.
|
|
*
|
|
* The uart driver needs to call bcm_bt_lpm_exit_lpm_locked() every time it
|
|
* begins TX, to ensure this driver keeps WAKE asserted during TX.
|
|
*
|
|
* The callbacks and hijacking of the uart_port struct are not a clean API,
|
|
* but the Linux tty and serial core layers do not have a better alternative
|
|
* right now: there is no good way to plumb uart clock control through these
|
|
* layers. See http://lkml.org/lkml/2008/12/19/213 for more background.
|
|
*/
|
|
|
|
struct bcm_bt_lpm {
|
|
unsigned int gpio_wake;
|
|
unsigned int gpio_host_wake;
|
|
|
|
int wake;
|
|
int host_wake;
|
|
|
|
struct hrtimer enter_lpm_timer;
|
|
ktime_t enter_lpm_delay;
|
|
|
|
struct uart_port *uport;
|
|
|
|
void (*request_clock_off_locked)(struct uart_port *uport);
|
|
void (*request_clock_on_locked)(struct uart_port *uport);
|
|
} bt_lpm;
|
|
|
|
static void set_wake_locked(int wake)
|
|
{
|
|
if (wake == bt_lpm.wake)
|
|
return;
|
|
bt_lpm.wake = wake;
|
|
|
|
if (wake || bt_lpm.host_wake)
|
|
bt_lpm.request_clock_on_locked(bt_lpm.uport);
|
|
else
|
|
bt_lpm.request_clock_off_locked(bt_lpm.uport);
|
|
|
|
gpio_set_value(bt_lpm.gpio_wake, wake);
|
|
}
|
|
|
|
static enum hrtimer_restart enter_lpm(struct hrtimer *timer) {
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&bt_lpm.uport->lock, flags);
|
|
set_wake_locked(0);
|
|
spin_unlock_irqrestore(&bt_lpm.uport->lock, flags);
|
|
|
|
return HRTIMER_NORESTART;
|
|
}
|
|
|
|
void bcm_bt_lpm_exit_lpm_locked(struct uart_port *uport) {
|
|
bt_lpm.uport = uport;
|
|
|
|
hrtimer_try_to_cancel(&bt_lpm.enter_lpm_timer);
|
|
|
|
set_wake_locked(1);
|
|
|
|
hrtimer_start(&bt_lpm.enter_lpm_timer, bt_lpm.enter_lpm_delay,
|
|
HRTIMER_MODE_REL);
|
|
}
|
|
EXPORT_SYMBOL(bcm_bt_lpm_exit_lpm_locked);
|
|
|
|
static void update_host_wake_locked(int host_wake)
|
|
{
|
|
if (host_wake == bt_lpm.host_wake)
|
|
return;
|
|
bt_lpm.host_wake = host_wake;
|
|
|
|
if (bt_lpm.wake || host_wake)
|
|
bt_lpm.request_clock_on_locked(bt_lpm.uport);
|
|
else
|
|
bt_lpm.request_clock_off_locked(bt_lpm.uport);
|
|
}
|
|
|
|
static irqreturn_t host_wake_isr(int irq, void *dev)
|
|
{
|
|
int host_wake;
|
|
unsigned long flags;
|
|
|
|
host_wake = gpio_get_value(bt_lpm.gpio_host_wake);
|
|
set_irq_type(irq, host_wake ? IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH);
|
|
|
|
if (!bt_lpm.uport) {
|
|
bt_lpm.host_wake = host_wake;
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
spin_lock_irqsave(&bt_lpm.uport->lock, flags);
|
|
|
|
update_host_wake_locked(host_wake);
|
|
|
|
spin_unlock_irqrestore(&bt_lpm.uport->lock, flags);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static int bcm_bt_lpm_probe(struct platform_device *pdev)
|
|
{
|
|
int irq;
|
|
int ret;
|
|
struct bcm_bt_lpm_platform_data *pdata = pdev->dev.platform_data;
|
|
|
|
if (bt_lpm.request_clock_off_locked != NULL) {
|
|
printk(KERN_ERR "Cannot register two bcm_bt_lpm drivers\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
bt_lpm.gpio_wake = pdata->gpio_wake;
|
|
bt_lpm.gpio_host_wake = pdata->gpio_host_wake;
|
|
bt_lpm.request_clock_off_locked = pdata->request_clock_off_locked;
|
|
bt_lpm.request_clock_on_locked = pdata->request_clock_on_locked;
|
|
|
|
hrtimer_init(&bt_lpm.enter_lpm_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
|
|
bt_lpm.enter_lpm_delay = ktime_set(1, 0); /* 1 sec */
|
|
bt_lpm.enter_lpm_timer.function = enter_lpm;
|
|
|
|
gpio_set_value(bt_lpm.gpio_wake, 0);
|
|
bt_lpm.host_wake = 0;
|
|
|
|
irq = gpio_to_irq(bt_lpm.gpio_host_wake);
|
|
ret = request_irq(irq, host_wake_isr, IRQF_TRIGGER_HIGH,
|
|
"bt host_wake", NULL);
|
|
if (ret)
|
|
return ret;
|
|
ret = set_irq_wake(irq, 1);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct platform_driver bcm_bt_lpm_driver = {
|
|
.probe = bcm_bt_lpm_probe,
|
|
.driver = {
|
|
.name = "bcm_bt_lpm",
|
|
.owner = THIS_MODULE,
|
|
},
|
|
};
|
|
|
|
static int __init bcm_bt_lpm_init(void)
|
|
{
|
|
return platform_driver_register(&bcm_bt_lpm_driver);
|
|
}
|
|
|
|
module_init(bcm_bt_lpm_init);
|
|
MODULE_DESCRIPTION("Broadcom Bluetooth low power mode driver");
|
|
MODULE_AUTHOR("Nick Pelly <npelly@google.com>");
|
|
MODULE_LICENSE("GPL");
|