Enhance Broadcom protocol with the UART setup, firmware download and power management. Signed-off-by: Ilya Faenson <ifaenson@xxxxxxxxxxxx> --- drivers/bluetooth/hci_bcm.c | 528 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 513 insertions(+), 15 deletions(-) diff --git a/drivers/bluetooth/hci_bcm.c b/drivers/bluetooth/hci_bcm.c index 1ec0b4a..ccd92ed 100644 --- a/drivers/bluetooth/hci_bcm.c +++ b/drivers/bluetooth/hci_bcm.c @@ -1,8 +1,9 @@ /* * - * Bluetooth HCI UART driver for Broadcom devices + * Bluetooth UART H4 protocol for Broadcom devices * * Copyright (C) 2015 Intel Corporation + * Copyright (C) 2015 Broadcom Corporation * * * This program is free software; you can redistribute it and/or modify @@ -21,48 +22,413 @@ * */ +#include <linux/module.h> #include <linux/kernel.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/fcntl.h> +#include <linux/interrupt.h> +#include <linux/ptrace.h> +#include <linux/poll.h> +#include <linux/slab.h> +#include <linux/tty.h> #include <linux/errno.h> +#include <linux/string.h> +#include <linux/signal.h> +#include <linux/ioctl.h> #include <linux/skbuff.h> +#include <linux/gpio/consumer.h> +#include <linux/of_gpio.h> +#include <linux/of_platform.h> #include <net/bluetooth/bluetooth.h> #include <net/bluetooth/hci_core.h> -#include "btbcm.h" #include "hci_uart.h" +#include "btbcm.h" +#include "btbcm_uart.h" struct bcm_data { struct sk_buff *rx_skb; struct sk_buff_head txq; + struct hci_uart *hu; + + bool is_suspended; /* suspend/resume flag */ + + struct timer_list timer; /* idle timer */ + + struct btbcm_uart_parameters pars; /* device parameters */ + void *device_context; /* ACPI/DT device context */ }; +/* Suspend/resume synchronization mutex */ +static DEFINE_MUTEX(plock); + +/* + * Callbacks from the BCMBT_UART device + */ + +/* + * The platform is suspending. Stop UART activity + */ +static void suspend_notification(void *context) +{ + struct ktermios ktermios; + struct hci_uart *hu = (struct hci_uart *)context; + struct bcm_data *h4 = hu->priv; + struct tty_struct *tty = hu->tty; + int status; + unsigned int set = 0; + unsigned int clear = 0; + + BT_DBG("suspend_notification with is_suspended %d", h4->is_suspended); + + if (!h4->pars.configure_sleep) + return; + + if (!h4->is_suspended) { + if (h4->pars.manual_fc) { + /* Disable hardware flow control */ + ktermios = tty->termios; + ktermios.c_cflag &= ~CRTSCTS; + status = tty_set_termios(tty, &ktermios); + if (status) + BT_DBG("suspend_notification dis fc fail %d", + status); + else + BT_DBG("suspend_notification hw fc disabled"); + + /* Clear RTS to prevent the device from sending */ + /* (most PCs need OUT2 to enable interrupts) */ + status = tty->driver->ops->tiocmget(tty); + BT_DBG("suspend_notification cur tiocm 0x%x", status); + set &= ~(TIOCM_OUT2 | TIOCM_RTS); + clear = ~set; + set &= TIOCM_DTR | TIOCM_RTS | TIOCM_OUT1 | + TIOCM_OUT2 | TIOCM_LOOP; + clear &= TIOCM_DTR | TIOCM_RTS | TIOCM_OUT1 | + TIOCM_OUT2 | TIOCM_LOOP; + status = tty->driver->ops->tiocmset(tty, set, clear); + if (status) + BT_DBG("suspend_notification clr RTS fail %d", + status); + else + BT_DBG("suspend_notification RTS cleared"); + status = tty->driver->ops->tiocmget(tty); + BT_DBG("suspend_notification end tiocm 0x%x", status); + } + + /* Once this callback returns, driver suspends BT via GPIO */ + h4->is_suspended = true; + } +} + +/* + * The platform is resuming. Resume UART activity. + */ +static void resume_notification(void *context) +{ + struct ktermios ktermios; + struct hci_uart *hu = (struct hci_uart *)context; + struct bcm_data *h4 = hu->priv; + struct tty_struct *tty = hu->tty; + int status; + unsigned int set = 0; + unsigned int clear = 0; + + BT_DBG("resume_notification with is_suspended %d", h4->is_suspended); + + if (!h4->pars.configure_sleep) + return; + + /* When this callback executes, the device has woken up already */ + if (h4->is_suspended) { + h4->is_suspended = false; + + if (h4->pars.manual_fc) { + status = tty->driver->ops->tiocmget(tty); + BT_DBG("resume_notification cur tiocm 0x%x", status); + set |= (TIOCM_OUT2 | TIOCM_RTS); + clear = ~set; + set &= TIOCM_DTR | TIOCM_RTS | TIOCM_OUT1 | + TIOCM_OUT2 | TIOCM_LOOP; + clear &= TIOCM_DTR | TIOCM_RTS | TIOCM_OUT1 | + TIOCM_OUT2 | TIOCM_LOOP; + status = tty->driver->ops->tiocmset(tty, set, clear); + if (status) + BT_DBG("resume_notification set RTS fail %d", + status); + else + BT_DBG("resume_notification RTS set"); + + /* Re-enable hardware flow control */ + ktermios = tty->termios; + ktermios.c_cflag |= CRTSCTS; + status = tty_set_termios(tty, &ktermios); + if (status) + BT_DBG("resume_notification enable fc fail %d", + status); + else + BT_DBG("resume_notification hw fc re-enabled"); + } + } + + /* If we're resumed, the idle timer must be running */ + status = mod_timer(&h4->timer, jiffies + + msecs_to_jiffies(h4->pars.idle_timeout_in_secs * 1000)); +} + +/* + * The BT device is resuming. Resume UART activity if suspended + */ +static void wakeup_notification(void *context) +{ + struct ktermios ktermios; + struct hci_uart *hu = (struct hci_uart *)context; + struct bcm_data *h4 = hu->priv; + struct tty_struct *tty = hu->tty; + int status; + unsigned int set = 0; + unsigned int clear = 0; + + if (!h4->pars.configure_sleep) + return; + + status = tty->driver->ops->tiocmget(tty); + BT_DBG("wakeup_notification hu %p current tiocm 0x%x", hu, status); + if (h4->is_suspended) { + if (h4->pars.manual_fc) { + set |= (TIOCM_OUT2 | TIOCM_RTS); + clear = ~set; + set &= TIOCM_DTR | TIOCM_RTS | TIOCM_OUT1 | + TIOCM_OUT2 | TIOCM_LOOP; + clear &= TIOCM_DTR | TIOCM_RTS | TIOCM_OUT1 | + TIOCM_OUT2 | TIOCM_LOOP; + status = tty->driver->ops->tiocmset(tty, set, clear); + if (status) + BT_DBG("wakeup_notification set RTS fail %d", + status); + else + BT_DBG("wakeup_notification RTS set"); + + /* Re-enable hardware flow control */ + ktermios = tty->termios; + ktermios.c_cflag |= CRTSCTS; + status = tty_set_termios(tty, &ktermios); + if (status) + BT_DBG("wakeup_notification fc-en failure %d", + status); + else + BT_DBG("wakeup_notification hw fc re-enabled"); + } + + h4->is_suspended = false; + } + + /* If we're resumed, the idle timer must be running */ + status = mod_timer(&h4->timer, jiffies + msecs_to_jiffies( + h4->pars.idle_timeout_in_secs * 1000)); +} + +/* + * Make sure we're awake + * (called when the resumed state is required) + */ +static void bcm_ensure_wakeup(struct hci_uart *hu) +{ + struct bcm_data *h4 = hu->priv; + int status; + + if (!h4->pars.configure_sleep) + return; + + /* Suspend/resume operations are serialized */ + mutex_lock(&plock); + + /* Nothing to do if resumed already */ + if (!h4->is_suspended) { + mutex_unlock(&plock); + + /* Just reset the timer */ + status = mod_timer(&h4->timer, jiffies + msecs_to_jiffies( + h4->pars.idle_timeout_in_secs * 1000)); + return; + } + + /* Wakeup the device */ + status = btbcm_uart_control(BTBCM_UART_ACTION_RESUME, + h4->device_context, NULL, NULL); + if (status) + BT_DBG("bcm_ensure_wakeup failed to resume driver %d", status); + + /* Unflow control the port if configured */ + resume_notification(hu); + + mutex_unlock(&plock); +} + +/* + * Idle timer callback + */ +static void bcm_idle_timeout(unsigned long arg) +{ + struct hci_uart *hu = (struct hci_uart *)arg; + struct bcm_data *h4 = hu->priv; + int status; + + BT_DBG("bcm_idle_timeout hu %p", hu); + + /* Suspend/resume operations are serialized */ + mutex_lock(&plock); + + if (!h4->is_suspended) { + /* Flow control the port if configured */ + suspend_notification(hu); + + /* Suspend the device */ + status = btbcm_uart_control(BTBCM_UART_ACTION_SUSPEND, + h4->device_context, NULL, NULL); + if (status) + BT_DBG("bcm_idle_timeout failed to suspend device %d", + status); + } + + mutex_unlock(&plock); +} + static int bcm_open(struct hci_uart *hu) { - struct bcm_data *bcm; + struct btbcm_uart_callbacks callbacks; + unsigned long callbacks_size = sizeof(callbacks); + int status; + struct bcm_data *h4; + struct tty_struct *tty = hu->tty; - BT_DBG("hu %p", hu); + BT_DBG("bcm_h4_open hu %p", hu); - bcm = kzalloc(sizeof(*bcm), GFP_KERNEL); - if (!bcm) + h4 = kzalloc(sizeof(*h4), GFP_KERNEL); + if (!h4) return -ENOMEM; - skb_queue_head_init(&bcm->txq); + skb_queue_head_init(&h4->txq); + hu->priv = h4; + h4->hu = hu; + h4->is_suspended = false; + + /* Configure callbacks on the driver */ + callbacks.interface_version = BTBCM_UART_INTERFACE_VERSION; + callbacks.context = hu; + strcpy(callbacks.name, tty->name); + callbacks.p_suspend = suspend_notification; + callbacks.p_resume = resume_notification; + callbacks.p_wakeup = wakeup_notification; + status = btbcm_uart_control(BTBCM_UART_ACTION_CONFIGURE_CALLBACKS, + NULL, &callbacks, &callbacks_size); + if (status) { + BT_DBG("bcm_h4_open failed to set driver callbacks %d", status); + return status; + } + if (callbacks_size != sizeof(void *)) { + BT_DBG("bcm_h4_open got back %d bytes from callbacks?!", + (int)callbacks_size); + return -EMSGSIZE; + } + memcpy(&h4->device_context, &callbacks, sizeof(void *)); + BT_DBG("bcm_h4_open callbacks context %p", h4->device_context); + + /* Retrieve device parameters */ + callbacks_size = sizeof(h4->pars); + status = btbcm_uart_control(BTBCM_UART_ACTION_GET_PARAMETERS, + h4->device_context, &h4->pars, + &callbacks_size); + if (status) { + BT_DBG("bcm_h4_open failed to get dev parameters %d", status); + return status; + } + BT_DBG("Pars ver %d csleep %d dalow %d balow %d idle %d", + h4->pars.interface_version, h4->pars.configure_sleep, + h4->pars.dev_wake_active_low, h4->pars.bt_wake_active_low, + h4->pars.idle_timeout_in_secs); + + /* Cycle power to make sure the device is in the known state */ + status = btbcm_uart_control(BTBCM_UART_ACTION_POWER_OFF, + h4->device_context, NULL, NULL); + if (status) { + BT_DBG("bcm_h4_open failed to power off %d", status); + } else { + status = btbcm_uart_control(BTBCM_UART_ACTION_POWER_ON, + h4->device_context, NULL, NULL); + if (status) + BT_DBG("bcm_h4_open failed to power on %d", status); + } + + /* Start the idle timer */ + if (h4->pars.configure_sleep) { + setup_timer(&h4->timer, bcm_idle_timeout, (unsigned long)hu); + if (h4->pars.configure_sleep) + mod_timer(&h4->timer, jiffies + msecs_to_jiffies( + h4->pars.idle_timeout_in_secs * 1000)); + } - hu->priv = bcm; return 0; } static int bcm_close(struct hci_uart *hu) { - struct bcm_data *bcm = hu->priv; + struct btbcm_uart_callbacks callbacks; + unsigned long callbacks_size = sizeof(callbacks); + struct bcm_data *h4 = hu->priv; + int status; - BT_DBG("hu %p", hu); + hu->priv = NULL; - skb_queue_purge(&bcm->txq); - kfree_skb(bcm->rx_skb); - kfree(bcm); + BT_DBG("bcm_h4_close hu %p", hu); + + /* If we're being closed, we must suspend */ + if (h4->pars.configure_sleep) { + mutex_lock(&plock); + + if (!h4->is_suspended) { + /* Flow control the port */ + suspend_notification(hu); + + /* Suspend the device */ + status = btbcm_uart_control(BTBCM_UART_ACTION_SUSPEND, + h4->device_context, NULL, + NULL); + if (status) { + BT_DBG("bcm_h4_close suspend driver fail %d", + status); + } + } + + mutex_unlock(&plock); + + del_timer_sync(&h4->timer); + } + + /* Power off the device if possible */ + status = btbcm_uart_control(BTBCM_UART_ACTION_POWER_OFF, + h4->device_context, NULL, NULL); + if (status) + BT_DBG("bcm_h4_close failed to power off %d", status); + + /* de-configure callbacks on the driver */ + callbacks.interface_version = BTBCM_UART_INTERFACE_VERSION; + callbacks.context = hu; + callbacks.p_suspend = NULL; + callbacks.p_resume = NULL; + callbacks.p_wakeup = NULL; + status = btbcm_uart_control(BTBCM_UART_ACTION_CONFIGURE_CALLBACKS, + h4->device_context, &callbacks, + &callbacks_size); + if (status) + BT_DBG("bcm_h4_close failed to reset drv callbacks %d", status); + skb_queue_purge(&h4->txq); hu->priv = NULL; + kfree(h4); + return 0; } @@ -79,11 +445,137 @@ static int bcm_flush(struct hci_uart *hu) static int bcm_setup(struct hci_uart *hu) { - BT_DBG("hu %p", hu); + struct bcm_data *h4 = hu->priv; + int status; + struct sk_buff *skb; + unsigned char sleep_pars[] = { + 0x01, /* sleep mode 1=UART */ + 0x02, /* idle threshold HOST (value * 300ms) */ + 0x02, /* idle threshold HC (value * 300ms) */ + 0x01, /* BT_WAKE active mode - 1=active high, 0 = active low */ + 0x00, /* HOST_WAKE active mode - 1=active high, 0 = active low */ + 0x01, /* Allow host sleep during SCO - FALSE */ + 0x01, /* combine sleep mode and LPM - 1 == TRUE */ + 0x00, /* enable tristate control of UART TX line - FALSE */ + 0x00, /* USB auto-sleep on USB SUSPEND */ + 0x00, /* USB USB RESUME timeout (seconds) */ + 0x00, /* Pulsed Host Wake */ + 0x00 /* Enable Break To Host */ + }; + unsigned char pcm_int_pars[] = { + 0x00, /* 0=PCM routing, 1=SCO over HCI */ + 0x02, /* 0=128Kbps,1=256Kbps,2=512Kbps,3=1024Kbps,4=2048Kbps */ + 0x00, /* short frame sync 0=short, 1=long */ + 0x00, /* sync mode 0=slave, 1=master */ + 0x00 /* clock mode 0=slave, 1=master */ + }; + unsigned char pcm_format_pars[] = { + 0x00, /* LSB_First 1=TRUE, 0=FALSE */ + 0x00, /* Fill_Value (use 0-7 for fill bits value) */ + 0x02, /* Fill_Method (2=sign extended) */ + 0x03, /* Fill_Num # of fill bits 0-3) */ + 0x01 /* Right_Justify 1=TRUE, 0=FALSE */ + }; + unsigned char time_slot_number = 0; + + BT_DBG("bcm_h4_setup hu %p", hu); + + /* Bring the UART into known default state */ + status = btbcm_init_uart(hu); + if (status) { + BT_DBG("bcm_h4_setup failed to init BT device %d", status); + return status; + } + + /* Basic sanity check */ + skb = __hci_cmd_sync(hu->hdev, HCI_OP_RESET, 0, NULL, HCI_INIT_TIMEOUT); + if (IS_ERR(skb)) { + status = PTR_ERR(skb); + BT_ERR("bcm_h4_setup HCI Reset failed (%d)", status); + return status; + } + kfree_skb(skb); + BT_DBG("bcm_h4_setup HCI Reset succeeded"); + + /* Set the new baud rate */ + status = btbcm_set_baud_rate(hu, + h4->pars.baud_rate_before_config_download); + if (status) { + BT_ERR("bcm_h4_setup set_baud_rate faiilure %d", status); + return status; + } hu->hdev->set_bdaddr = btbcm_set_bdaddr; - return btbcm_setup_patchram(hu->hdev); + /* Download the firmware and reconfigure the UART afterwards */ + status = btbcm_setup_patchram(hu->hdev); + if (status) { + BT_ERR("bcm_h4_setup setup_patchram faiilure %d", status); + return status; + } + + /* Configure SCO PCM parameters */ + if (h4->pars.configure_audio) { + pcm_int_pars[0] = h4->pars.PCMRouting; + pcm_int_pars[1] = h4->pars.PCMInCallBitclock; + pcm_int_pars[2] = h4->pars.PCMShortFrameSync; + pcm_int_pars[3] = h4->pars.PCMSyncMode; + pcm_int_pars[4] = h4->pars.PCMClockMode; + skb = __hci_cmd_sync(hu->hdev, 0xfc1c, sizeof(pcm_int_pars), + pcm_int_pars, HCI_INIT_TIMEOUT); + if (IS_ERR(skb)) { + status = PTR_ERR(skb); + BT_ERR("bcm_h4_setup PCM INT VSC failed (%d)", status); + return status; + } + kfree_skb(skb); + BT_DBG("bcm_h4_setup PCM INT Parameters VSC succeeded"); + + pcm_format_pars[0] = h4->pars.PCMLSBFirst; + pcm_format_pars[1] = h4->pars.PCMFillValue; + pcm_format_pars[2] = h4->pars.PCMFillMethod; + pcm_format_pars[3] = h4->pars.PCMFillNum; + pcm_format_pars[4] = h4->pars.PCMRightJustify; + skb = __hci_cmd_sync(hu->hdev, 0xfc1e, sizeof(pcm_format_pars), + pcm_format_pars, HCI_INIT_TIMEOUT); + if (IS_ERR(skb)) { + status = PTR_ERR(skb); + BT_ERR("bcm_h4_setup PCM Format VSC failed (%d)", + status); + return status; + } + kfree_skb(skb); + BT_DBG("bcm_h4_setup PCM Format VSC succeeded"); + + skb = __hci_cmd_sync(hu->hdev, 0xfc22, sizeof(time_slot_number), + &time_slot_number, HCI_INIT_TIMEOUT); + if (IS_ERR(skb)) { + status = PTR_ERR(skb); + BT_ERR("bcm_h4_setup SCO Time Slot VSC failed (%d)", + status); + return status; + } + kfree_skb(skb); + BT_DBG("bcm_h4_setup SCO Time Slot VSC succeeded"); + } + + /* Configure device's suspend/resume operation */ + if (h4->pars.configure_sleep) { + /* Override the default */ + sleep_pars[3] = (unsigned char)!h4->pars.bt_wake_active_low; + sleep_pars[4] = (unsigned char)!h4->pars.dev_wake_active_low; + skb = __hci_cmd_sync(hu->hdev, 0xfc27, sizeof(sleep_pars), + sleep_pars, HCI_INIT_TIMEOUT); + if (IS_ERR(skb)) { + status = PTR_ERR(skb); + BT_ERR("bcm_h4_setup Sleep VSC failed (%d)", status); + return status; + } + kfree_skb(skb); + BT_DBG("bcm_h4_setup Set Sleep Parameters VSC succeeded"); + } + + return 0; } static const struct h4_recv_pkt bcm_recv_pkts[] = { @@ -99,6 +591,9 @@ static int bcm_recv(struct hci_uart *hu, const void *data, int count) if (!test_bit(HCI_UART_REGISTERED, &hu->flags)) return -EUNATCH; + /* Make sure we're resumed */ + bcm_ensure_wakeup(hu); + bcm->rx_skb = h4_recv_buf(hu->hdev, bcm->rx_skb, data, count, bcm_recv_pkts, ARRAY_SIZE(bcm_recv_pkts)); if (IS_ERR(bcm->rx_skb)) { @@ -116,6 +611,9 @@ static int bcm_enqueue(struct hci_uart *hu, struct sk_buff *skb) BT_DBG("hu %p skb %p", hu, skb); + /* Make sure we're resumed */ + bcm_ensure_wakeup(hu); + /* Prepend skb with frame type */ memcpy(skb_push(skb, 1), &bt_cb(skb)->pkt_type, 1); skb_queue_tail(&bcm->txq, skb); -- 1.9.1 -- To unsubscribe from this list: send the line "unsubscribe linux-bluetooth" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html