summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjonerlin <jonerlin@google.com>2021-07-10 22:07:40 +0800
committerjonerlin <jonerlin@google.com>2021-07-16 17:11:55 +0800
commitfed170f95b348ede131da0dfc10af0614a62a6e7 (patch)
tree6243db017d526112b0d49887ece2ba11a07ad19d
parentde15dce79c2fbc4cf82634ccc1fac96ae39896bb (diff)
downloadsynaptics-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--Kbuild3
-rw-r--r--Makefile7
-rw-r--r--nitrous.c734
-rw-r--r--nitrous.txt44
4 files changed, 788 insertions, 0 deletions
diff --git a/Kbuild b/Kbuild
new file mode 100644
index 0000000..3f61ad5
--- /dev/null
+++ b/Kbuild
@@ -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>;
+ };