summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCheney Ni <cheneyni@google.com>2020-11-15 23:04:58 +0800
committerCheney Ni <cheneyni@google.com>2020-12-03 19:29:35 +0800
commit16a8555679b457dc5336485bc627c66d4345c51d (patch)
tree324929f7bdbd7448297c916b461a24407055fdda
parent7f1dd0af2a16910bffd8fe7631533003c72ef67e (diff)
downloadbroadcom-16a8555679b457dc5336485bc627c66d4345c51d.tar.gz
Nitrous: Copy Nitrous.c from kernel 3.10
Copy from previous kernel. Note this commit will not compile 7804a4c Add Nitrous driver for BT power management. Change-Id: I43aaad986a24cf7cd3b0bc2f7eea23c410babf2f Signed-off-by: Cheney Ni <cheneyni@google.com>
-rw-r--r--nitrous.c569
-rw-r--r--nitrous.txt44
2 files changed, 613 insertions, 0 deletions
diff --git a/nitrous.c b/nitrous.c
new file mode 100644
index 0000000..a7cb521
--- /dev/null
+++ b/nitrous.c
@@ -0,0 +1,569 @@
+/*
+ * Bluetooth low power control via GPIO
+ *
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * The current implementation is very specific to Qualcomm serial driver
+ * with BCM chipset.
+ */
+
+#include <linux/delay.h>
+#include <linux/gpio.h>
+#include <linux/hrtimer.h>
+#include <linux/init.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/module.h>
+#include <linux/of_gpio.h>
+#include <linux/platform_data/msm_serial_hs.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/rfkill.h>
+#include <linux/wakelock.h>
+
+/* Timeout on UART Tx traffic before releasing wakelock. */
+static const int UART_TIMEOUT_SEC = 1;
+
+struct nitrous_bt_lpm {
+ int gpio_dev_wake; /* host -> dev wake gpio */
+ int gpio_host_wake; /* dev -> host wake gpio */
+ int gpio_power; /* gpio to control power */
+ int irq_host_wake; /* IRQ associated with host wake gpio */
+ int dev_wake_pol; /* 0: active low; 1: active high */
+ int host_wake_pol; /* 0: active low; 1: active high */
+
+ struct wake_lock dev_lock; /* Wakelock held during Tx */
+ struct wake_lock host_lock; /* Wakelock held during Rx */
+ struct hrtimer tx_lpm_timer; /* timer for going into LPM during Tx */
+ bool is_suspended; /* driver is in suspend state */
+ bool pending_irq; /* pending host wake IRQ during suspend */
+
+ struct uart_port *uart_port;
+ struct platform_device *pdev;
+ struct rfkill *rfkill;
+ bool rfkill_blocked; /* blocked: off; not blocked: on */
+};
+
+static struct nitrous_bt_lpm *bt_lpm; /* Reference for internal data */
+
+/* Helper to fetch gpio information from the device tree and then register it */
+static int read_and_request_gpio(struct platform_device *pdev,
+ const char *dt_name, int *gpio,
+ unsigned long gpio_flags, const char *label)
+{
+ int rc;
+
+ *gpio = of_get_named_gpio(pdev->dev.of_node, dt_name, 0);
+ if (unlikely(*gpio < 0)) {
+ pr_err("%s: %s not in device tree", __func__, dt_name);
+ return -EINVAL;
+ }
+
+ if (!gpio_is_valid(*gpio)) {
+ pr_err("%s: %s is invalid", __func__, dt_name);
+ return -EINVAL;
+ }
+
+ rc = gpio_request_one(*gpio, gpio_flags, label);
+ if (unlikely(rc < 0)) {
+ pr_err("%s: failed to request gpio: %d (%s), error: %d",
+ __func__, *gpio, label, rc);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+/*
+ * Power up or down UART driver and hold Tx/Rx wakelock.
+ *
+ * Note that the use of pm_runtime_get here is not ideal as the call is not
+ * blocking. By the time the UART driver is powered up, the serial core might
+ * have attempted to do a Tx at the UART driver already. There is currently a
+ * workaround in the MSM serial driver to catch this race. Over time the clock
+ * on and Tx sequence should be made synchronous.
+ */
+static bool nitrous_uart_power(struct uart_port *uart_port,
+ struct wake_lock *lock, bool on)
+{
+ if (wake_lock_active(lock) == on)
+ return false;
+
+ if (on) {
+ wake_lock(lock);
+ pm_runtime_get(uart_port->dev);
+ } else {
+ pm_runtime_put(uart_port->dev);
+ wake_unlock(lock);
+ }
+
+ return true;
+}
+
+/*
+ * Wake up or sleep UART and BT device for Tx.
+ */
+static inline void nitrous_wake_device_locked(struct nitrous_bt_lpm *lpm,
+ bool wake)
+{
+ if (nitrous_uart_power(lpm->uart_port, &lpm->dev_lock, wake)) {
+ /*
+ * If there is a UART power on/off, assert/deassert dev wake
+ * gpio accordingly.
+ */
+ int assert_level = (wake == lpm->dev_wake_pol);
+ gpio_set_value(lpm->gpio_dev_wake, assert_level);
+ }
+}
+
+/*
+ * Wake up or sleep UART for Rx.
+ */
+static inline void nitrous_wake_uart(struct nitrous_bt_lpm *lpm, bool wake)
+{
+ nitrous_uart_power(lpm->uart_port, &lpm->host_lock, wake);
+}
+
+/*
+ * Called when the tx_lpm_timer expires and the last Tx transaction should have
+ * been started about UART_TIMEOUT_SEC second(s) ago. At this time, the Tx
+ * should have been completed.
+ */
+static enum hrtimer_restart nitrous_tx_lpm_handler(struct hrtimer *timer)
+{
+ unsigned long flags;
+
+ if (!bt_lpm) {
+ pr_err("%s: missing bt_lpm\n", __func__);
+ return HRTIMER_NORESTART;
+ }
+
+ pr_debug("%s\n", __func__);
+
+ /* Release UART and BT resources */
+ spin_lock_irqsave(&bt_lpm->uart_port->lock, flags);
+ nitrous_wake_device_locked(bt_lpm, false);
+ spin_unlock_irqrestore(&bt_lpm->uart_port->lock, flags);
+
+ return HRTIMER_NORESTART;
+}
+
+/*
+ * Called before UART driver starts transmitting data out. UART and BT resources
+ * are requested to allow a transmission.
+ *
+ * Note that the calling context from the serial core should have the
+ * uart_port locked.
+ */
+void nitrous_prepare_uart_tx_locked(struct uart_port *port)
+{
+ if (!bt_lpm) {
+ pr_err("%s: missing bt_lpm\n", __func__);
+ return;
+ }
+
+ if (bt_lpm->rfkill_blocked) {
+ pr_err("%s: unexpected Tx when rfkill is blocked\n", __func__);
+ return;
+ }
+
+ hrtimer_cancel(&bt_lpm->tx_lpm_timer);
+ nitrous_wake_device_locked(bt_lpm, true);
+ hrtimer_start(&bt_lpm->tx_lpm_timer, ktime_set(UART_TIMEOUT_SEC, 0),
+ HRTIMER_MODE_REL);
+}
+EXPORT_SYMBOL(nitrous_prepare_uart_tx_locked);
+
+/*
+ * 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 *dev)
+{
+ int host_wake, rc;
+ struct platform_device *pdev = container_of(dev,
+ struct platform_device, dev);
+ struct nitrous_bt_lpm *lpm = platform_get_drvdata(pdev);
+
+ if (!lpm) {
+ pr_err("%s: missing lpm\n", __func__);
+ return IRQ_HANDLED;
+ }
+
+ if (lpm->rfkill_blocked) {
+ pr_err("%s: unexpected host wake IRQ\n", __func__);
+ return IRQ_HANDLED;
+ }
+
+ host_wake = gpio_get_value(lpm->gpio_host_wake);
+ pr_debug("%s: host wake gpio: %d\n", __func__, host_wake);
+
+ /* Invert the interrupt type to catch the next edge */
+ rc = irq_set_irq_type(irq,
+ host_wake ? IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH);
+ if (unlikely(rc))
+ pr_err("%s: error setting irq type %d\n", __func__, rc);
+
+ if (lpm->is_suspended) {
+ /* Mark pending irq flag to delay processing. */
+ lpm->pending_irq = true;
+ } else {
+ /* Wake up UART right the way if not suspended. */
+ bool uart_enable = (host_wake == lpm->host_wake_pol);
+ nitrous_wake_uart(lpm, uart_enable);
+ }
+
+ return IRQ_HANDLED;
+}
+
+static int nitrous_lpm_init(struct nitrous_bt_lpm *lpm)
+{
+ int rc;
+
+ hrtimer_init(&lpm->tx_lpm_timer, CLOCK_MONOTONIC,
+ HRTIMER_MODE_REL);
+ lpm->tx_lpm_timer.function = nitrous_tx_lpm_handler;
+
+ lpm->irq_host_wake = gpio_to_irq(lpm->gpio_host_wake);
+
+ rc = request_irq(lpm->irq_host_wake, nitrous_host_wake_isr,
+ lpm->host_wake_pol ? IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH,
+ "bt_host_wake", &lpm->pdev->dev);
+ if (rc < 0) {
+ pr_err("%s: unable to request IRQ for bt_host_wake GPIO\n",
+ __func__);
+ goto err_request_irq;
+ }
+
+ wake_lock_init(&lpm->dev_lock, WAKE_LOCK_SUSPEND, "bt_dev_tx_wake");
+ wake_lock_init(&lpm->host_lock, WAKE_LOCK_SUSPEND, "bt_host_rx_wake");
+
+ /* Configure wake peer callback to be called at the onset of Tx. */
+ msm_hs_set_wake_peer(lpm->uart_port, nitrous_prepare_uart_tx_locked);
+
+ return 0;
+
+err_request_irq:
+ lpm->irq_host_wake = 0;
+ return rc;
+}
+
+static void nitrous_lpm_cleanup(struct nitrous_bt_lpm *lpm)
+{
+ free_irq(lpm->irq_host_wake, NULL);
+ lpm->irq_host_wake = 0;
+ msm_hs_set_wake_peer(lpm->uart_port, NULL);
+ wake_lock_destroy(&lpm->dev_lock);
+ wake_lock_destroy(&lpm->host_lock);
+}
+
+/*
+ * 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) {
+ pr_err("%s: missing lpm\n", __func__);
+ return -EINVAL;
+ }
+
+ pr_info("%s: %s (blocked=%d)\n", __func__, blocked ? "off" : "on",
+ blocked);
+
+ if (blocked == lpm->rfkill_blocked) {
+ pr_info("%s already in requested state. Ignoring.\n", __func__);
+ return 0;
+ }
+
+ if (!blocked) {
+ int rc;
+
+ /*
+ * Power up the BT chip. Datasheet of BCM4343W suggests at
+ * least a 10ms time delay between consecutive toggles.
+ */
+ gpio_set_value(lpm->gpio_power, 0);
+ msleep(30);
+ gpio_set_value(lpm->gpio_power, 1);
+
+ /* Enable host_wake irq to get ready */
+ rc = irq_set_irq_type(lpm->irq_host_wake,
+ lpm->host_wake_pol ?
+ IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH);
+ if (unlikely(rc))
+ pr_err("%s: error setting irq type %d\n", __func__, rc);
+ enable_irq(lpm->irq_host_wake);
+ } else {
+ /* Disable host wake IRQ and release Rx wakelock*/
+ disable_irq(lpm->irq_host_wake);
+ nitrous_wake_device_locked(lpm, false);
+
+ /* Cancel pending LPM timer and release Tx wakelock*/
+ hrtimer_cancel(&lpm->tx_lpm_timer);
+ nitrous_wake_uart(lpm, false);
+
+ /* Power down the BT chip */
+ gpio_set_value(lpm->gpio_power, 0);
+ }
+
+ lpm->rfkill_blocked = blocked;
+
+ return 0;
+}
+
+static const struct rfkill_ops nitrous_rfkill_ops = {
+ .set_block = nitrous_rfkill_set_power,
+};
+
+static int nitrous_rfkill_init(struct platform_device *pdev,
+ struct nitrous_bt_lpm *lpm)
+{
+ int rc;
+
+ rc = read_and_request_gpio(
+ pdev,
+ "power-gpio",
+ &lpm->gpio_power,
+ GPIOF_OUT_INIT_LOW,
+ "power_gpio"
+ );
+ if (unlikely(rc < 0))
+ goto err_gpio_power_reg;
+
+ lpm->rfkill = rfkill_alloc(
+ "nitrous_bluetooth",
+ &pdev->dev,
+ RFKILL_TYPE_BLUETOOTH,
+ &nitrous_rfkill_ops,
+ lpm
+ );
+ if (unlikely(!lpm->rfkill)) {
+ rc = -ENOMEM;
+ goto err_rfkill_alloc;
+ }
+
+ /* 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;
+err_rfkill_alloc:
+ gpio_free(lpm->gpio_power);
+err_gpio_power_reg:
+ lpm->gpio_power = 0;
+ 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;
+ gpio_free(lpm->gpio_power);
+ lpm->gpio_power = 0;
+}
+
+static int nitrous_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct nitrous_bt_lpm *lpm;
+ struct device_node *np = dev->of_node;
+ u32 port_number;
+ int rc = 0;
+
+ lpm = devm_kzalloc(dev, sizeof(struct nitrous_bt_lpm), GFP_KERNEL);
+ if (!lpm)
+ return -ENOMEM;
+
+ lpm->pdev = pdev;
+
+ if (of_property_read_u32(np, "uart-port", &port_number)) {
+ pr_err("%s: UART port not in dev tree\n", __func__);
+ return -EINVAL;
+ }
+ lpm->uart_port = msm_hs_get_uart_port(port_number);
+
+ if (of_property_read_u32(np, "host-wake-polarity",
+ &lpm->host_wake_pol)) {
+ pr_err("%s: host wake polarity not in dev tree.\n", __func__);
+ return -EINVAL;
+ }
+
+ if (of_property_read_u32(np, "dev-wake-polarity",
+ &lpm->dev_wake_pol)) {
+ pr_err("%s: dev wake polarity not in dev tree\n", __func__);
+ return -EINVAL;
+ }
+
+ rc = read_and_request_gpio(
+ pdev,
+ "dev-wake-gpio",
+ &lpm->gpio_dev_wake,
+ GPIOF_OUT_INIT_LOW,
+ "dev_wake_gpio"
+ );
+ if (unlikely(rc < 0))
+ goto err_gpio_dev_req;
+
+ rc = read_and_request_gpio(
+ pdev,
+ "host-wake-gpio",
+ &lpm->gpio_host_wake,
+ GPIOF_IN,
+ "host_wake_gpio"
+ );
+ if (unlikely(rc < 0))
+ goto err_gpio_host_req;
+
+ device_init_wakeup(dev, true);
+
+ rc = nitrous_lpm_init(lpm);
+ if (unlikely(rc))
+ goto err_lpm_init;
+
+ rc = nitrous_rfkill_init(pdev, lpm);
+ if (unlikely(rc))
+ goto err_rfkill_init;
+
+ platform_set_drvdata(pdev, lpm);
+ bt_lpm = lpm;
+
+ return rc;
+
+err_rfkill_init:
+ nitrous_rfkill_cleanup(lpm);
+err_lpm_init:
+ nitrous_lpm_cleanup(lpm);
+ device_init_wakeup(dev, false);
+ gpio_free(lpm->gpio_host_wake);
+err_gpio_host_req:
+ lpm->gpio_host_wake = 0;
+ gpio_free(lpm->gpio_dev_wake);
+err_gpio_dev_req:
+ lpm->gpio_dev_wake = 0;
+ 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) {
+ pr_err("%s: missing lpm\n", __func__);
+ return -EINVAL;
+ }
+
+ nitrous_rfkill_cleanup(lpm);
+ nitrous_lpm_cleanup(lpm);
+
+ gpio_free(lpm->gpio_dev_wake);
+ gpio_free(lpm->gpio_host_wake);
+ lpm->gpio_dev_wake = 0;
+ lpm->gpio_host_wake = 0;
+ devm_kfree(&pdev->dev, lpm);
+
+ return 0;
+}
+
+static int nitrous_suspend(struct device *dev)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ struct nitrous_bt_lpm *lpm = platform_get_drvdata(pdev);
+
+ if (device_may_wakeup(&pdev->dev) && !lpm->rfkill_blocked) {
+ enable_irq_wake(lpm->irq_host_wake);
+
+ /* Reset flag to capture pending irq before resume */
+ lpm->pending_irq = false;
+ }
+
+ lpm->is_suspended = true;
+
+ return 0;
+}
+
+static int nitrous_resume(struct device *dev)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ struct nitrous_bt_lpm *lpm = platform_get_drvdata(pdev);
+
+ if (device_may_wakeup(&pdev->dev) && !lpm->rfkill_blocked) {
+ disable_irq_wake(lpm->irq_host_wake);
+
+ /* Handle pending host wake irq. */
+ if (lpm->pending_irq) {
+ pr_info("%s: pending host_wake irq\n", __func__);
+ nitrous_wake_uart(lpm, true);
+ lpm->pending_irq = false;
+ }
+ }
+
+ lpm->is_suspended = false;
+
+ 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)
+};
+
+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..cc7fc2d
--- /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
+Broadcom 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>;
+ };