diff options
author | jonerlin <jonerlin@google.com> | 2021-07-10 22:07:40 +0800 |
---|---|---|
committer | jonerlin <jonerlin@google.com> | 2021-07-16 17:11:55 +0800 |
commit | fed170f95b348ede131da0dfc10af0614a62a6e7 (patch) | |
tree | 6243db017d526112b0d49887ece2ba11a07ad19d | |
parent | de15dce79c2fbc4cf82634ccc1fac96ae39896bb (diff) | |
download | synaptics-fed170f95b348ede131da0dfc10af0614a62a6e7.tar.gz |
porting nitrous driver as Synaptic Bluetooth power control and LPM
driver
Bug: 192506914
Bug: 193493268
Test: Build Pass, Bluetooth ON
Change-Id: Ie7faa0defee051fd511dd02800b3eb47d068a27e
Signed-off-by: jonerlin <jonerlin@google.com>
-rw-r--r-- | Kbuild | 3 | ||||
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | nitrous.c | 734 | ||||
-rw-r--r-- | nitrous.txt | 44 |
4 files changed, 788 insertions, 0 deletions
@@ -0,0 +1,3 @@ +obj-$(CONFIG_NITROUS) := nitrous.o + +ccflags-y += -I$(abspath $(KERNEL_SRC)/$(M)) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5a16e0f --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +KERNEL_SRC ?= /lib/modules/$(shell uname -r)/build +M ?= $(shell pwd) + +KBUILD_OPTIONS := CONFIG_NITROUS=m + +modules modules_install clean: + $(MAKE) -C $(KERNEL_SRC) M=$(M) W=1 $(KBUILD_OPTIONS) $(@) diff --git a/nitrous.c b/nitrous.c new file mode 100644 index 0000000..ab01adc --- /dev/null +++ b/nitrous.c @@ -0,0 +1,734 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Bluetooth low power control via GPIO + * + * Copyright 2015-2020 Google LLC. + */ + +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/pinctrl/consumer.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/proc_fs.h> +#include <linux/property.h> +#include <linux/rfkill.h> +#include <linux/rtc.h> +#include <misc/logbuffer.h> +#include <soc/google/exynos-cpupm.h> + +#define STATUS_IDLE 1 +#define STATUS_BUSY 0 + +#define NITROUS_AUTOSUSPEND_DELAY 1000 /* autosleep delay 1000 ms */ + +struct nitrous_lpm_proc; + +struct nitrous_bt_lpm { + struct pinctrl *pinctrls; + struct pinctrl_state *pinctrl_default_state; + struct gpio_desc *gpio_dev_wake; /* Host -> Dev WAKE GPIO */ + struct gpio_desc *gpio_host_wake; /* Dev -> Host WAKE GPIO */ + struct gpio_desc *gpio_power; /* GPIO to control power */ + int irq_host_wake; /* IRQ associated with HOST_WAKE GPIO */ + int wake_polarity; /* 0: active low; 1: active high */ + + bool is_suspended; /* driver is in suspend state */ + bool host_irq_dev_pm_resumed; + + struct device *dev; + struct rfkill *rfkill; + bool rfkill_blocked; /* blocked: OFF; not blocked: ON */ + bool lpm_enabled; + struct nitrous_lpm_proc *proc; + struct logbuffer *log; + int idle_btip_index; +}; + +#define PROC_BTWAKE 0 +#define PROC_LPM 1 +#define PROC_BTWRITE 2 +#define PROC_DIR "bluetooth/sleep" +struct proc_dir_entry *bluetooth_dir, *sleep_dir; + +struct nitrous_lpm_proc { + long operation; + struct nitrous_bt_lpm *lpm; +}; + +/* + * Wake up or sleep BT device for Tx. + */ +static inline void nitrous_wake_controller(struct nitrous_bt_lpm *lpm, bool wake) +{ + int assert_level = (wake == lpm->wake_polarity); + dev_dbg(lpm->dev, "DEV_WAKE: %s", (assert_level ? "Assert" : "Dessert")); + logbuffer_log(lpm->log, "DEV_WAKE: %s", (assert_level ? "Assert" : "Dessert")); + gpiod_set_value_cansleep(lpm->gpio_dev_wake, assert_level); +} + +/* + * Called before UART driver starts transmitting data out. UART and BT resources + * are requested to allow a transmission. + */ +static void nitrous_prepare_uart_tx_locked(struct nitrous_bt_lpm *lpm) +{ + int ret; + + if (lpm->rfkill_blocked) { + dev_err(lpm->dev, "unexpected Tx when rfkill is blocked\n"); + logbuffer_log(lpm->log, "unexpected Tx when rfkill is blocked"); + return; + } + + ret = pm_runtime_get_sync(lpm->dev); + + /* Shall be resumed here */ + logbuffer_log(lpm->log, "uart_tx_locked"); + + if (lpm->is_suspended) { + /* This shouldn't happen. If it does, it will result in a BT crash */ + /* TODO (mullerf): Does this happen? If yes, why? */ + dev_err(lpm->dev,"Tx in device suspended. ret: %d, uc:%d\n", + ret, atomic_read(&lpm->dev->power.usage_count)); + logbuffer_log(lpm->log,"Tx in device suspended. ret: %d, uc:%d", + ret, atomic_read(&lpm->dev->power.usage_count)); + } + + pm_runtime_mark_last_busy(lpm->dev); + pm_runtime_put_autosuspend(lpm->dev); +} + +/* + * ISR to handle host wake line from the BT chip. + * + * If an interrupt is received during system suspend, the handling of the + * interrupt will be delayed until the driver is resumed. This allows the use + * of pm runtime framework to wake the serial driver. + */ +static irqreturn_t nitrous_host_wake_isr(int irq, void *data) +{ + struct nitrous_bt_lpm *lpm = data; + int host_wake; + + host_wake = gpiod_get_value(lpm->gpio_host_wake); + dev_dbg(lpm->dev, "Host wake IRQ: %u\n", host_wake); + + if (lpm->rfkill_blocked) { + dev_err(lpm->dev, "Unexpected Host wake IRQ\n"); + logbuffer_log(lpm->log, "Unexpected Host wake IRQ"); + return IRQ_HANDLED; + } + + /* Check whether host_wake is ACTIVE (== 1) */ + if (host_wake == 1) { + logbuffer_log(lpm->log, "host_wake_isr asserted"); + pm_stay_awake(lpm->dev); + /* Only resume device when needed to keep usage count synced. + This is because some falling edges can be missed by irq. */ + if (!lpm->host_irq_dev_pm_resumed) { + pm_runtime_get(lpm->dev); + lpm->host_irq_dev_pm_resumed = true; + } + } else { + logbuffer_log(lpm->log, "host_wake_isr de-asserted"); + pm_runtime_mark_last_busy(lpm->dev); + /* Only autosuspend device when needed to keep usage count synced. + This is because some rising edges can be missed by irq. */ + if (lpm->host_irq_dev_pm_resumed) { + pm_runtime_put_autosuspend(lpm->dev); + lpm->host_irq_dev_pm_resumed = false; + } + pm_wakeup_dev_event(lpm->dev, NITROUS_AUTOSUSPEND_DELAY, false); + } + + return IRQ_HANDLED; +} + +static int nitrous_lpm_runtime_enable(struct nitrous_bt_lpm *lpm) +{ + int rc; + + if (lpm->irq_host_wake <= 0) + return -EOPNOTSUPP; + + if (lpm->rfkill_blocked) { + dev_err(lpm->dev, "Unexpected LPM request\n"); + logbuffer_log(lpm->log, "Unexpected LPM request"); + return -EINVAL; + } + + if (lpm->lpm_enabled) { + dev_warn(lpm->dev, "Try to request LPM twice\n"); + return 0; + } + + /* Set irq_host_wake as a trigger edge interrupt. */ + rc = devm_request_irq(lpm->dev, lpm->irq_host_wake, nitrous_host_wake_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "bt_host_wake", lpm); + if (unlikely(rc)) { + dev_err(lpm->dev, "Unable to request IRQ for bt_host_wake GPIO\n"); + logbuffer_log(lpm->log, "Unable to request IRQ for bt_host_wake GPIO"); + lpm->irq_host_wake = rc; + return rc; + } + + device_init_wakeup(lpm->dev, true); + pm_runtime_enable(lpm->dev); + pm_runtime_set_autosuspend_delay(lpm->dev, NITROUS_AUTOSUSPEND_DELAY); + pm_runtime_use_autosuspend(lpm->dev); + + /* When LPM is enabled, we resume the device right away. + It will autosuspend automatically if unused. */ + dev_dbg(lpm->dev, "DEV_WAKE: High - LPM enable"); + logbuffer_log(lpm->log, "DEV_WAKE: High - LPM enable"); + pm_runtime_get_sync(lpm->dev); + pm_runtime_mark_last_busy(lpm->dev); + pm_runtime_put_autosuspend(lpm->dev); + + lpm->lpm_enabled = true; + + return rc; +} + +static void nitrous_lpm_runtime_disable(struct nitrous_bt_lpm *lpm) +{ + if (lpm->irq_host_wake <= 0) + return; + + if (!lpm->lpm_enabled) + return; + + devm_free_irq(lpm->dev, lpm->irq_host_wake, lpm); + device_init_wakeup(lpm->dev, false); + pm_relax(lpm->dev); + /* Check whether usage_counter got out of sync */ + if (atomic_read(&lpm->dev->power.usage_count)) { + dev_warn(lpm->dev, "Usage counter went out of sync: %d", + atomic_read(&lpm->dev->power.usage_count)); + logbuffer_log(lpm->log, "Usage counter went out of sync: %d", + atomic_read(&lpm->dev->power.usage_count)); + /* Force set it to 0 */ + atomic_set(&lpm->dev->power.usage_count, 0); + } + dev_dbg(lpm->dev, "DEV_WAKE: Low - LPM disable"); + logbuffer_log(lpm->log, "DEV_WAKE: Low - LPM disable"); + pm_runtime_suspend(lpm->dev); + pm_runtime_disable(lpm->dev); + pm_runtime_set_suspended(lpm->dev); + + /* Host IRQ has been freed, so safe to set this here */ + lpm->host_irq_dev_pm_resumed = false; + + lpm->lpm_enabled = false; +} + +static int nitrous_proc_show(struct seq_file *m, void *v) +{ + struct nitrous_lpm_proc *data = m->private; + struct nitrous_bt_lpm *lpm = data->lpm; + + switch (data->operation) { + case PROC_BTWAKE: + seq_printf(m, "LPM: %s\nPolarity: %s\nHOST_WAKE: %u\nDEV_WAKE: %u\n", + (lpm->lpm_enabled ? "Enabled" : "Disabled"), + (lpm->wake_polarity ? "High" : "Low"), + gpiod_get_value(lpm->gpio_host_wake), + gpiod_get_value(lpm->gpio_dev_wake)); + break; + case PROC_LPM: + case PROC_BTWRITE: + seq_printf(m, "REG_ON: %s\nLPM: %s\nState: %s\n", + (lpm->rfkill_blocked ? "OFF" : "ON"), + (lpm->lpm_enabled ? "Enabled" : "Disabled"), + (lpm->is_suspended ? "asleep" : "awake")); + break; + default: + return 0; + } + return 0; +} + +static int nitrous_proc_open(struct inode *inode, struct file *file) +{ + return single_open(file, nitrous_proc_show, PDE_DATA(inode)); +} + +static ssize_t nitrous_proc_write(struct file *file, const char *buf, + size_t count, loff_t *pos) +{ + struct nitrous_lpm_proc *data = PDE_DATA(file_inode(file)); + struct nitrous_bt_lpm *lpm = data->lpm; + struct timespec64 ts; + char lbuf[4]; + int rc; + + if (count >= sizeof(lbuf)) + count = sizeof(lbuf) - 1; + if (copy_from_user(lbuf, buf, count)) + return -EFAULT; + + switch (data->operation) { + case PROC_LPM: + if (lbuf[0] == '1') { + dev_info(lpm->dev, "LPM enabling\n"); + logbuffer_log(lpm->log, "PROC_LPM: enable"); + rc = nitrous_lpm_runtime_enable(lpm); + if (unlikely(rc)) + return rc; + } else if (lbuf[0] == '0') { + dev_info(lpm->dev, "LPM disabling\n"); + logbuffer_log(lpm->log, "PROC_LPM: disable"); + nitrous_lpm_runtime_disable(lpm); + } else { + dev_warn(lpm->dev, "Unknown LPM operation\n"); + logbuffer_log(lpm->log, "PROC_LPM: unknown"); + return -EFAULT; + } + break; + case PROC_BTWRITE: + if (!lpm->lpm_enabled) { + dev_info(lpm->dev, "LPM not enabled\n"); + logbuffer_log(lpm->log, "PROC_BTWRITE: not enabled"); + return count; + } + dev_dbg(lpm->dev, "LPM waking up for Tx\n"); + ktime_get_real_ts64(&ts); + logbuffer_log(lpm->log, "PROC_BTWRITE: waking up %ptTt", &ts); + nitrous_prepare_uart_tx_locked(lpm); + break; + default: + return 0; + } + return count; +} + +static const struct proc_ops nitrous_proc_read_fops = { + .proc_open = nitrous_proc_open, + .proc_read = seq_read, + .proc_release = single_release, +}; + +static const struct proc_ops nitrous_proc_readwrite_fops = { + .proc_open = nitrous_proc_open, + .proc_read = seq_read, + .proc_write = nitrous_proc_write, + .proc_release = single_release, +}; + +static void nitrous_lpm_remove_proc_entries(struct nitrous_bt_lpm *lpm) +{ + if (bluetooth_dir == NULL) + return; + if (sleep_dir) { + remove_proc_entry("btwrite", sleep_dir); + remove_proc_entry("lpm", sleep_dir); + remove_proc_entry("btwake", sleep_dir); + remove_proc_entry("sleep", bluetooth_dir); + } + remove_proc_entry("bluetooth", 0); + if (lpm->proc) { + devm_kfree(lpm->dev, lpm->proc); + lpm->proc = NULL; + } +} + +static int nitrous_lpm_init(struct nitrous_bt_lpm *lpm) +{ + int rc; + struct proc_dir_entry *entry; + struct nitrous_lpm_proc *data; + + lpm->irq_host_wake = gpiod_to_irq(lpm->gpio_host_wake); + dev_info(lpm->dev, "IRQ: %d active: %s\n", lpm->irq_host_wake, + (lpm->wake_polarity ? "High" : "Low")); + logbuffer_log(lpm->log, "init: IRQ: %d active: %s", lpm->irq_host_wake, + (lpm->wake_polarity ? "High" : "Low")); + + lpm->is_suspended = true; + + data = devm_kzalloc(lpm->dev, sizeof(struct nitrous_lpm_proc) * 3, GFP_KERNEL); + if (data == NULL) { + dev_err(lpm->dev, "Unable to alloc memory"); + logbuffer_log(lpm->log, "Unable to alloc memory"); + return -ENOMEM; + } + lpm->proc = data; + + bluetooth_dir = proc_mkdir("bluetooth", NULL); + if (bluetooth_dir == NULL) { + dev_err(lpm->dev, "Unable to create /proc/bluetooth directory"); + logbuffer_log(lpm->log, "Unable to create /proc/bluetooth directory"); + rc = -ENOMEM; + goto fail; + } + sleep_dir = proc_mkdir("sleep", bluetooth_dir); + if (sleep_dir == NULL) { + dev_err(lpm->dev, "Unable to create /proc/%s directory", PROC_DIR); + logbuffer_log(lpm->log, "Unable to create /proc/%s directory", PROC_DIR); + rc = -ENOMEM; + goto fail; + } + /* Creating read only proc entries "btwake" showing GPIOs state */ + data[0].operation = PROC_BTWAKE; + data[0].lpm = lpm; + entry = proc_create_data("btwake", (S_IRUSR | S_IRGRP), sleep_dir, + &nitrous_proc_read_fops, data); + if (entry == NULL) { + dev_err(lpm->dev, "Unable to create /proc/%s/btwake entry", PROC_DIR); + logbuffer_log(lpm->log, "Unable to create /proc/%s/btwake entry", PROC_DIR); + rc = -ENOMEM; + goto fail; + } + /* read/write proc entries "lpm" */ + data[1].operation = PROC_LPM; + data[1].lpm = lpm; + entry = proc_create_data("lpm", (S_IRUSR | S_IRGRP | S_IWUSR), + sleep_dir, &nitrous_proc_readwrite_fops, data + 1); + if (entry == NULL) { + dev_err(lpm->dev, "Unable to create /proc/%s/lpm entry", PROC_DIR); + logbuffer_log(lpm->log, "Unable to create /proc/%s/lpm entry", PROC_DIR); + rc = -ENOMEM; + goto fail; + } + /* read/write proc entries "btwrite" */ + data[2].operation = PROC_BTWRITE; + data[2].lpm = lpm; + entry = proc_create_data("btwrite", (S_IRUSR | S_IRGRP | S_IWUSR), + sleep_dir, &nitrous_proc_readwrite_fops, data + 2); + if (entry == NULL) { + dev_err(lpm->dev, "Unable to create /proc/%s/btwrite entry", PROC_DIR); + logbuffer_log(lpm->log, "Unable to create /proc/%s/btwrite entry", PROC_DIR); + rc = -ENOMEM; + goto fail; + } + + return 0; + +fail: + nitrous_lpm_remove_proc_entries(lpm); + return rc; +} + +static void nitrous_lpm_cleanup(struct nitrous_bt_lpm *lpm) +{ + nitrous_lpm_runtime_disable(lpm); + lpm->irq_host_wake = 0; + + nitrous_lpm_remove_proc_entries(lpm); +} + +/* + * Set BT power on/off (blocked is true: OFF; blocked is false: ON) + */ +static int nitrous_rfkill_set_power(void *data, bool blocked) +{ + struct nitrous_bt_lpm *lpm = data; + + if (!lpm) { + dev_err(lpm->dev, "rfkill: lpm is NULL\n"); + logbuffer_log(lpm->log, "rfkill: lpm is NULL"); + return -EINVAL; + } + + dev_info(lpm->dev, "rfkill: %s (blocked=%d)\n", blocked ? "off" : "on", + blocked); + logbuffer_log(lpm->log, "rfkill: blocked=%s", blocked ? "off" : "on"); + + if (blocked == lpm->rfkill_blocked) { + dev_info(lpm->dev, "rfkill: already in requested state: %s\n", + blocked ? "off" : "on"); + logbuffer_log(lpm->log, "rfkill: already in requested state: %s", + blocked ? "off" : "on"); + return 0; + } + + /* Reset to make sure LPM is disabled */ + nitrous_lpm_runtime_disable(lpm); + + if (!blocked) { + /* Power up the BT chip. delay between consecutive toggles. */ + logbuffer_log(lpm->log, "Power up BT chip"); + dev_dbg(lpm->dev, "REG_ON: Low"); + gpiod_set_value_cansleep(lpm->gpio_power, false); + msleep(30); + exynos_update_ip_idle_status(lpm->idle_btip_index, STATUS_BUSY); + dev_dbg(lpm->dev, "REG_ON: High"); + gpiod_set_value_cansleep(lpm->gpio_power, true); + + /* Set DEV_WAKE to High as part of the power sequence */ + dev_dbg(lpm->dev, "DEV_WAKE: High - Power sequence"); + gpiod_set_value_cansleep(lpm->gpio_dev_wake, true); + } else { + /* Set DEV_WAKE to Low as part of the power sequence */ + dev_dbg(lpm->dev, "DEV_WAKE: Low - Power sequence"); + gpiod_set_value_cansleep(lpm->gpio_dev_wake, false); + + /* Power down the BT chip */ + logbuffer_log(lpm->log, "Power down BT chip"); + dev_dbg(lpm->dev, "REG_ON: Low"); + gpiod_set_value_cansleep(lpm->gpio_power, false); + exynos_update_ip_idle_status(lpm->idle_btip_index, STATUS_IDLE); + } + lpm->rfkill_blocked = blocked; + + /* wait for device to power cycle and come out of reset */ + usleep_range(10000, 20000); + + return 0; +} + +static const struct rfkill_ops nitrous_rfkill_ops = { + .set_block = nitrous_rfkill_set_power, +}; + +static int nitrous_rfkill_init(struct nitrous_bt_lpm *lpm) +{ + int rc; + + lpm->gpio_power = devm_gpiod_get_optional(lpm->dev, "shutdown", GPIOD_OUT_LOW); + if (IS_ERR(lpm->gpio_power)) + return PTR_ERR(lpm->gpio_power); + + lpm->rfkill = rfkill_alloc( + "nitrous_bluetooth", + lpm->dev, + RFKILL_TYPE_BLUETOOTH, + &nitrous_rfkill_ops, + lpm + ); + if (unlikely(!lpm->rfkill)) + return -ENOMEM; + + /* Make sure rfkill core is initialized to be blocked initially. */ + rfkill_init_sw_state(lpm->rfkill, true); + rc = rfkill_register(lpm->rfkill); + if (unlikely(rc)) + goto err_rfkill_register; + + /* Power off chip at startup. */ + nitrous_rfkill_set_power(lpm, true); + return 0; + +err_rfkill_register: + rfkill_destroy(lpm->rfkill); + lpm->rfkill = NULL; + return rc; +} + +static void nitrous_rfkill_cleanup(struct nitrous_bt_lpm *lpm) +{ + nitrous_rfkill_set_power(lpm, true); + rfkill_unregister(lpm->rfkill); + rfkill_destroy(lpm->rfkill); + lpm->rfkill = NULL; +} + +static int nitrous_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct nitrous_bt_lpm *lpm; + int rc = 0; + + lpm = devm_kzalloc(dev, sizeof(struct nitrous_bt_lpm), GFP_KERNEL); + if (!lpm) + return -ENOMEM; + + lpm->dev = dev; + dev_dbg(lpm->dev, "probe:\n"); + + if (device_property_read_u32(dev, "wake-polarity", &lpm->wake_polarity)) { + dev_warn(lpm->dev, "Wake polarity not in dev tree\n"); + lpm->wake_polarity = 1; + } + + lpm->pinctrls = devm_pinctrl_get(lpm->dev); + if (IS_ERR(lpm->pinctrls)) { + dev_warn(lpm->dev, "Can't get pinctrl\n"); + } else { + lpm->pinctrl_default_state = + pinctrl_lookup_state(lpm->pinctrls, "default"); + if (IS_ERR(lpm->pinctrl_default_state)) + dev_warn(lpm->dev, "Can't get default pinctrl state\n"); + } + + lpm->gpio_dev_wake = devm_gpiod_get_optional(dev, "device-wakeup", GPIOD_OUT_LOW); + if (IS_ERR(lpm->gpio_dev_wake)) + return PTR_ERR(lpm->gpio_dev_wake); + + lpm->gpio_host_wake = devm_gpiod_get_optional(dev, "host-wakeup", GPIOD_IN); + if (IS_ERR(lpm->gpio_host_wake)) + return PTR_ERR(lpm->gpio_host_wake); + + lpm->log = logbuffer_register("btlpm"); + if (IS_ERR_OR_NULL(lpm->log)) { + dev_info(lpm->dev, "logbuffer get failed\n"); + lpm->log = NULL; + } + + rc = nitrous_lpm_init(lpm); + if (unlikely(rc)) + goto err_lpm_init; + + rc = nitrous_rfkill_init(lpm); + if (unlikely(rc)) + goto err_rfkill_init; + + if (!IS_ERR_OR_NULL(lpm->pinctrl_default_state)) { + rc = pinctrl_select_state(lpm->pinctrls, + lpm->pinctrl_default_state); + if (unlikely(rc)) + dev_warn(lpm->dev, "Can't set default pinctrl state\n"); + } + + platform_set_drvdata(pdev, lpm); + + lpm->idle_btip_index = exynos_get_idle_ip_index("bluetooth"); + exynos_update_ip_idle_status(lpm->idle_btip_index, STATUS_IDLE); + + logbuffer_log(lpm->log, "probe: successful"); + + return rc; + +err_rfkill_init: + nitrous_rfkill_cleanup(lpm); +err_lpm_init: + nitrous_lpm_cleanup(lpm); + devm_kfree(dev, lpm); + return rc; +} + +static int nitrous_remove(struct platform_device *pdev) +{ + struct nitrous_bt_lpm *lpm = platform_get_drvdata(pdev); + + if (!lpm) { + dev_err(lpm->dev, "LPM is NULL\n"); + logbuffer_log(lpm->log, "remove: LPM is NULL"); + return -EINVAL; + } + + logbuffer_log(lpm->log, "removing"); + nitrous_rfkill_cleanup(lpm); + nitrous_lpm_cleanup(lpm); + if (!IS_ERR_OR_NULL(lpm->log)) + logbuffer_unregister(lpm->log); + + devm_kfree(&pdev->dev, lpm); + + return 0; +} + +static int nitrous_suspend_device(struct device *dev) +{ + struct nitrous_bt_lpm *lpm = dev_get_drvdata(dev); + + dev_dbg(lpm->dev, "suspend_device from %s\n", + (lpm->is_suspended ? "asleep" : "awake")); + logbuffer_log(lpm->log, "suspend_device from %s", + (lpm->is_suspended ? "asleep" : "awake")); + + nitrous_wake_controller(lpm, false); + exynos_update_ip_idle_status(lpm->idle_btip_index, STATUS_IDLE); + lpm->is_suspended = true; + + return 0; +} + +static int nitrous_resume_device(struct device *dev) +{ + struct nitrous_bt_lpm *lpm = dev_get_drvdata(dev); + + dev_dbg(lpm->dev, "resume_device from %s\n", + (lpm->is_suspended ? "asleep" : "awake")); + logbuffer_log(lpm->log, "resume_device from %s", + (lpm->is_suspended ? "asleep" : "awake")); + + exynos_update_ip_idle_status(lpm->idle_btip_index, STATUS_BUSY); + nitrous_wake_controller(lpm, true); + lpm->is_suspended = false; + + return 0; +} + +static int nitrous_suspend(struct device *dev) +{ + struct nitrous_bt_lpm *lpm = dev_get_drvdata(dev); + + if (lpm->rfkill_blocked) + return 0; + + dev_dbg(lpm->dev, "nitrous_suspend\n"); + logbuffer_log(lpm->log, "nitrous_suspend"); + + if (device_may_wakeup(dev) && lpm->lpm_enabled) { + pm_runtime_force_suspend(lpm->dev); + logbuffer_log(lpm->log, "pm_runtime_force_suspend"); + enable_irq_wake(lpm->irq_host_wake); + dev_dbg(lpm->dev, "Host wake IRQ enabled\n"); + logbuffer_log(lpm->log, "Host wake IRQ enabled"); + } + + return 0; +} + +static int nitrous_resume(struct device *dev) +{ + struct nitrous_bt_lpm *lpm = dev_get_drvdata(dev); + + if (lpm->rfkill_blocked) + return 0; + + dev_dbg(lpm->dev, "nitrous_resume\n"); + logbuffer_log(lpm->log, "nitrous_resume"); + + if (device_may_wakeup(dev) && lpm->lpm_enabled) { + disable_irq_wake(lpm->irq_host_wake); + dev_dbg(lpm->dev, "Host wake IRQ disabled\n"); + logbuffer_log(lpm->log, "Host wake IRQ disabled"); + pm_runtime_force_resume(lpm->dev); + logbuffer_log(lpm->log, "pm_runtime_force_resume"); + } + + return 0; +} + +static struct of_device_id nitrous_match_table[] = { + { .compatible = "goog,nitrous" }, + {} +}; + +static const struct dev_pm_ops nitrous_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(nitrous_suspend, nitrous_resume) + SET_RUNTIME_PM_OPS(nitrous_suspend_device, nitrous_resume_device, NULL) +}; + +static struct platform_driver nitrous_platform_driver = { + .probe = nitrous_probe, + .remove = nitrous_remove, + .driver = { + .name = "nitrous_bluetooth", + .owner = THIS_MODULE, + .of_match_table = nitrous_match_table, + .pm = &nitrous_pm_ops, + }, +}; + +static int __init nitrous_init(void) +{ + return platform_driver_register(&nitrous_platform_driver); +} + +static void __exit nitrous_exit(void) +{ + platform_driver_unregister(&nitrous_platform_driver); +} + +module_init(nitrous_init); +module_exit(nitrous_exit); +MODULE_DESCRIPTION("Nitrous Oxide Driver for Bluetooth"); +MODULE_AUTHOR("Google"); +MODULE_LICENSE("GPL"); diff --git a/nitrous.txt b/nitrous.txt new file mode 100644 index 0000000..84c3d68 --- /dev/null +++ b/nitrous.txt @@ -0,0 +1,44 @@ +* Nitrous Bluetooth Power Management Driver + +The Nitrous BT power management is designed to handle the power management +between a host processor and a BT controller. In its current form, the +driver is specific to the power management between a MSM processor and a +Synaptics BT controller over UART communication. + +The driver handles the following key power management elements: +-rfkill. +-host-wake-gpio/dev-wake-gpio sleep protocol. +-serial driver power management. + +On Tx, the driver registers and receives a callback from the serial core to +toggle the dev-wake-gpio and utilizes the PM Runtime framework to wake up +the serial driver for transmission. + +On Rx, the driver listens to the host-wake GPIO and wakes up the serial driver +to receive incoming data. The host-wake GPIO is also capable of waking up +the host processor from suspend. + +Required Properties: + - compatible: "goog,nitrous" + - uart-port = UART port number for serial communication. + - power-gpio = GPIO number for controller power. + - host-wake-gpio: GPIO number for host wake signal (controller to host). + - host-wake-polarity: Polarity for host wake GPIO + (0: active low; 1: active high). + - dev-wake-gpio: GPIO number for controller wake signal (host to controller). + - dev-wake-polarity: Polarity for dev wake GPIO + (0: active low; 1: active high). + +Optional Properties: + None + +Example: + bt_nitrous { + compatible = "goog,nitrous"; + uart-port = <6>; + power-gpio = <&msmgpio 10 0>; + host-wake-gpio = <&msmgpio 20 0>; + host-wake-polarity = <0>; + dev-wake-gpio = <&msmgpio 30 0>; + dev-wake-polarity = <0>; + }; |