diff options
author | Cheney Ni <cheneyni@google.com> | 2020-11-15 23:04:58 +0800 |
---|---|---|
committer | Cheney Ni <cheneyni@google.com> | 2020-12-03 19:29:35 +0800 |
commit | 16a8555679b457dc5336485bc627c66d4345c51d (patch) | |
tree | 324929f7bdbd7448297c916b461a24407055fdda | |
parent | 7f1dd0af2a16910bffd8fe7631533003c72ef67e (diff) | |
download | broadcom-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.c | 569 | ||||
-rw-r--r-- | nitrous.txt | 44 |
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>; + }; |