mirror of
https://github.com/torvalds/linux.git
synced 2024-11-12 07:01:57 +00:00
7d12e780e0
Maintain a per-CPU global "struct pt_regs *" variable which can be used instead of passing regs around manually through all ~1800 interrupt handlers in the Linux kernel. The regs pointer is used in few places, but it potentially costs both stack space and code to pass it around. On the FRV arch, removing the regs parameter from all the genirq function results in a 20% speed up of the IRQ exit path (ie: from leaving timer_interrupt() to leaving do_IRQ()). Where appropriate, an arch may override the generic storage facility and do something different with the variable. On FRV, for instance, the address is maintained in GR28 at all times inside the kernel as part of general exception handling. Having looked over the code, it appears that the parameter may be handed down through up to twenty or so layers of functions. Consider a USB character device attached to a USB hub, attached to a USB controller that posts its interrupts through a cascaded auxiliary interrupt controller. A character device driver may want to pass regs to the sysrq handler through the input layer which adds another few layers of parameter passing. I've build this code with allyesconfig for x86_64 and i386. I've runtested the main part of the code on FRV and i386, though I can't test most of the drivers. I've also done partial conversion for powerpc and MIPS - these at least compile with minimal configurations. This will affect all archs. Mostly the changes should be relatively easy. Take do_IRQ(), store the regs pointer at the beginning, saving the old one: struct pt_regs *old_regs = set_irq_regs(regs); And put the old one back at the end: set_irq_regs(old_regs); Don't pass regs through to generic_handle_irq() or __do_IRQ(). In timer_interrupt(), this sort of change will be necessary: - update_process_times(user_mode(regs)); - profile_tick(CPU_PROFILING, regs); + update_process_times(user_mode(get_irq_regs())); + profile_tick(CPU_PROFILING); I'd like to move update_process_times()'s use of get_irq_regs() into itself, except that i386, alone of the archs, uses something other than user_mode(). Some notes on the interrupt handling in the drivers: (*) input_dev() is now gone entirely. The regs pointer is no longer stored in the input_dev struct. (*) finish_unlinks() in drivers/usb/host/ohci-q.c needs checking. It does something different depending on whether it's been supplied with a regs pointer or not. (*) Various IRQ handler function pointers have been moved to type irq_handler_t. Signed-Off-By: David Howells <dhowells@redhat.com> (cherry picked from 1b16e7ac850969f38b375e511e3fa2f474a33867 commit)
655 lines
14 KiB
C
655 lines
14 KiB
C
/*
|
|
*
|
|
* Digianswer Bluetooth USB driver
|
|
*
|
|
* Copyright (C) 2004-2005 Marcel Holtmann <marcel@holtmann.org>
|
|
*
|
|
*
|
|
* 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
|
|
*
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
|
|
#include <linux/kernel.h>
|
|
#include <linux/init.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/types.h>
|
|
#include <linux/errno.h>
|
|
|
|
#include <linux/usb.h>
|
|
|
|
#include <net/bluetooth/bluetooth.h>
|
|
#include <net/bluetooth/hci_core.h>
|
|
|
|
#ifndef CONFIG_BT_HCIBPA10X_DEBUG
|
|
#undef BT_DBG
|
|
#define BT_DBG(D...)
|
|
#endif
|
|
|
|
#define VERSION "0.8"
|
|
|
|
static int ignore = 0;
|
|
|
|
static struct usb_device_id bpa10x_table[] = {
|
|
/* Tektronix BPA 100/105 (Digianswer) */
|
|
{ USB_DEVICE(0x08fd, 0x0002) },
|
|
|
|
{ } /* Terminating entry */
|
|
};
|
|
|
|
MODULE_DEVICE_TABLE(usb, bpa10x_table);
|
|
|
|
#define BPA10X_CMD_EP 0x00
|
|
#define BPA10X_EVT_EP 0x81
|
|
#define BPA10X_TX_EP 0x02
|
|
#define BPA10X_RX_EP 0x82
|
|
|
|
#define BPA10X_CMD_BUF_SIZE 252
|
|
#define BPA10X_EVT_BUF_SIZE 16
|
|
#define BPA10X_TX_BUF_SIZE 384
|
|
#define BPA10X_RX_BUF_SIZE 384
|
|
|
|
struct bpa10x_data {
|
|
struct hci_dev *hdev;
|
|
struct usb_device *udev;
|
|
|
|
rwlock_t lock;
|
|
|
|
struct sk_buff_head cmd_queue;
|
|
struct urb *cmd_urb;
|
|
struct urb *evt_urb;
|
|
struct sk_buff *evt_skb;
|
|
unsigned int evt_len;
|
|
|
|
struct sk_buff_head tx_queue;
|
|
struct urb *tx_urb;
|
|
struct urb *rx_urb;
|
|
};
|
|
|
|
#define HCI_VENDOR_HDR_SIZE 5
|
|
|
|
struct hci_vendor_hdr {
|
|
__u8 type;
|
|
__le16 snum;
|
|
__le16 dlen;
|
|
} __attribute__ ((packed));
|
|
|
|
static void bpa10x_recv_bulk(struct bpa10x_data *data, unsigned char *buf, int count)
|
|
{
|
|
struct hci_acl_hdr *ah;
|
|
struct hci_sco_hdr *sh;
|
|
struct hci_vendor_hdr *vh;
|
|
struct sk_buff *skb;
|
|
int len;
|
|
|
|
while (count) {
|
|
switch (*buf++) {
|
|
case HCI_ACLDATA_PKT:
|
|
ah = (struct hci_acl_hdr *) buf;
|
|
len = HCI_ACL_HDR_SIZE + __le16_to_cpu(ah->dlen);
|
|
skb = bt_skb_alloc(len, GFP_ATOMIC);
|
|
if (skb) {
|
|
memcpy(skb_put(skb, len), buf, len);
|
|
skb->dev = (void *) data->hdev;
|
|
bt_cb(skb)->pkt_type = HCI_ACLDATA_PKT;
|
|
hci_recv_frame(skb);
|
|
}
|
|
break;
|
|
|
|
case HCI_SCODATA_PKT:
|
|
sh = (struct hci_sco_hdr *) buf;
|
|
len = HCI_SCO_HDR_SIZE + sh->dlen;
|
|
skb = bt_skb_alloc(len, GFP_ATOMIC);
|
|
if (skb) {
|
|
memcpy(skb_put(skb, len), buf, len);
|
|
skb->dev = (void *) data->hdev;
|
|
bt_cb(skb)->pkt_type = HCI_SCODATA_PKT;
|
|
hci_recv_frame(skb);
|
|
}
|
|
break;
|
|
|
|
case HCI_VENDOR_PKT:
|
|
vh = (struct hci_vendor_hdr *) buf;
|
|
len = HCI_VENDOR_HDR_SIZE + __le16_to_cpu(vh->dlen);
|
|
skb = bt_skb_alloc(len, GFP_ATOMIC);
|
|
if (skb) {
|
|
memcpy(skb_put(skb, len), buf, len);
|
|
skb->dev = (void *) data->hdev;
|
|
bt_cb(skb)->pkt_type = HCI_VENDOR_PKT;
|
|
hci_recv_frame(skb);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
len = count - 1;
|
|
break;
|
|
}
|
|
|
|
buf += len;
|
|
count -= (len + 1);
|
|
}
|
|
}
|
|
|
|
static int bpa10x_recv_event(struct bpa10x_data *data, unsigned char *buf, int size)
|
|
{
|
|
BT_DBG("data %p buf %p size %d", data, buf, size);
|
|
|
|
if (data->evt_skb) {
|
|
struct sk_buff *skb = data->evt_skb;
|
|
|
|
memcpy(skb_put(skb, size), buf, size);
|
|
|
|
if (skb->len == data->evt_len) {
|
|
data->evt_skb = NULL;
|
|
data->evt_len = 0;
|
|
hci_recv_frame(skb);
|
|
}
|
|
} else {
|
|
struct sk_buff *skb;
|
|
struct hci_event_hdr *hdr;
|
|
unsigned char pkt_type;
|
|
int pkt_len = 0;
|
|
|
|
if (size < HCI_EVENT_HDR_SIZE + 1) {
|
|
BT_ERR("%s event packet block with size %d is too short",
|
|
data->hdev->name, size);
|
|
return -EILSEQ;
|
|
}
|
|
|
|
pkt_type = *buf++;
|
|
size--;
|
|
|
|
if (pkt_type != HCI_EVENT_PKT) {
|
|
BT_ERR("%s unexpected event packet start byte 0x%02x",
|
|
data->hdev->name, pkt_type);
|
|
return -EPROTO;
|
|
}
|
|
|
|
hdr = (struct hci_event_hdr *) buf;
|
|
pkt_len = HCI_EVENT_HDR_SIZE + hdr->plen;
|
|
|
|
skb = bt_skb_alloc(pkt_len, GFP_ATOMIC);
|
|
if (!skb) {
|
|
BT_ERR("%s no memory for new event packet",
|
|
data->hdev->name);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
skb->dev = (void *) data->hdev;
|
|
bt_cb(skb)->pkt_type = pkt_type;
|
|
|
|
memcpy(skb_put(skb, size), buf, size);
|
|
|
|
if (pkt_len == size) {
|
|
hci_recv_frame(skb);
|
|
} else {
|
|
data->evt_skb = skb;
|
|
data->evt_len = pkt_len;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void bpa10x_wakeup(struct bpa10x_data *data)
|
|
{
|
|
struct urb *urb;
|
|
struct sk_buff *skb;
|
|
int err;
|
|
|
|
BT_DBG("data %p", data);
|
|
|
|
urb = data->cmd_urb;
|
|
if (urb->status == -EINPROGRESS)
|
|
skb = NULL;
|
|
else
|
|
skb = skb_dequeue(&data->cmd_queue);
|
|
|
|
if (skb) {
|
|
struct usb_ctrlrequest *cr;
|
|
|
|
if (skb->len > BPA10X_CMD_BUF_SIZE) {
|
|
BT_ERR("%s command packet with size %d is too big",
|
|
data->hdev->name, skb->len);
|
|
kfree_skb(skb);
|
|
return;
|
|
}
|
|
|
|
cr = (struct usb_ctrlrequest *) urb->setup_packet;
|
|
cr->wLength = __cpu_to_le16(skb->len);
|
|
|
|
memcpy(urb->transfer_buffer, skb->data, skb->len);
|
|
urb->transfer_buffer_length = skb->len;
|
|
|
|
err = usb_submit_urb(urb, GFP_ATOMIC);
|
|
if (err < 0 && err != -ENODEV) {
|
|
BT_ERR("%s submit failed for command urb %p with error %d",
|
|
data->hdev->name, urb, err);
|
|
skb_queue_head(&data->cmd_queue, skb);
|
|
} else
|
|
kfree_skb(skb);
|
|
}
|
|
|
|
urb = data->tx_urb;
|
|
if (urb->status == -EINPROGRESS)
|
|
skb = NULL;
|
|
else
|
|
skb = skb_dequeue(&data->tx_queue);
|
|
|
|
if (skb) {
|
|
memcpy(urb->transfer_buffer, skb->data, skb->len);
|
|
urb->transfer_buffer_length = skb->len;
|
|
|
|
err = usb_submit_urb(urb, GFP_ATOMIC);
|
|
if (err < 0 && err != -ENODEV) {
|
|
BT_ERR("%s submit failed for command urb %p with error %d",
|
|
data->hdev->name, urb, err);
|
|
skb_queue_head(&data->tx_queue, skb);
|
|
} else
|
|
kfree_skb(skb);
|
|
}
|
|
}
|
|
|
|
static void bpa10x_complete(struct urb *urb)
|
|
{
|
|
struct bpa10x_data *data = urb->context;
|
|
unsigned char *buf = urb->transfer_buffer;
|
|
int err, count = urb->actual_length;
|
|
|
|
BT_DBG("data %p urb %p buf %p count %d", data, urb, buf, count);
|
|
|
|
read_lock(&data->lock);
|
|
|
|
if (!test_bit(HCI_RUNNING, &data->hdev->flags))
|
|
goto unlock;
|
|
|
|
if (urb->status < 0 || !count)
|
|
goto resubmit;
|
|
|
|
if (usb_pipein(urb->pipe)) {
|
|
data->hdev->stat.byte_rx += count;
|
|
|
|
if (usb_pipetype(urb->pipe) == PIPE_INTERRUPT)
|
|
bpa10x_recv_event(data, buf, count);
|
|
|
|
if (usb_pipetype(urb->pipe) == PIPE_BULK)
|
|
bpa10x_recv_bulk(data, buf, count);
|
|
} else {
|
|
data->hdev->stat.byte_tx += count;
|
|
|
|
bpa10x_wakeup(data);
|
|
}
|
|
|
|
resubmit:
|
|
if (usb_pipein(urb->pipe)) {
|
|
err = usb_submit_urb(urb, GFP_ATOMIC);
|
|
if (err < 0 && err != -ENODEV) {
|
|
BT_ERR("%s urb %p type %d resubmit status %d",
|
|
data->hdev->name, urb, usb_pipetype(urb->pipe), err);
|
|
}
|
|
}
|
|
|
|
unlock:
|
|
read_unlock(&data->lock);
|
|
}
|
|
|
|
static inline struct urb *bpa10x_alloc_urb(struct usb_device *udev, unsigned int pipe,
|
|
size_t size, gfp_t flags, void *data)
|
|
{
|
|
struct urb *urb;
|
|
struct usb_ctrlrequest *cr;
|
|
unsigned char *buf;
|
|
|
|
BT_DBG("udev %p data %p", udev, data);
|
|
|
|
urb = usb_alloc_urb(0, flags);
|
|
if (!urb)
|
|
return NULL;
|
|
|
|
buf = kmalloc(size, flags);
|
|
if (!buf) {
|
|
usb_free_urb(urb);
|
|
return NULL;
|
|
}
|
|
|
|
switch (usb_pipetype(pipe)) {
|
|
case PIPE_CONTROL:
|
|
cr = kmalloc(sizeof(*cr), flags);
|
|
if (!cr) {
|
|
kfree(buf);
|
|
usb_free_urb(urb);
|
|
return NULL;
|
|
}
|
|
|
|
cr->bRequestType = USB_TYPE_VENDOR;
|
|
cr->bRequest = 0;
|
|
cr->wIndex = 0;
|
|
cr->wValue = 0;
|
|
cr->wLength = __cpu_to_le16(0);
|
|
|
|
usb_fill_control_urb(urb, udev, pipe, (void *) cr, buf, 0, bpa10x_complete, data);
|
|
break;
|
|
|
|
case PIPE_INTERRUPT:
|
|
usb_fill_int_urb(urb, udev, pipe, buf, size, bpa10x_complete, data, 1);
|
|
break;
|
|
|
|
case PIPE_BULK:
|
|
usb_fill_bulk_urb(urb, udev, pipe, buf, size, bpa10x_complete, data);
|
|
break;
|
|
|
|
default:
|
|
kfree(buf);
|
|
usb_free_urb(urb);
|
|
return NULL;
|
|
}
|
|
|
|
return urb;
|
|
}
|
|
|
|
static inline void bpa10x_free_urb(struct urb *urb)
|
|
{
|
|
BT_DBG("urb %p", urb);
|
|
|
|
if (!urb)
|
|
return;
|
|
|
|
kfree(urb->setup_packet);
|
|
kfree(urb->transfer_buffer);
|
|
|
|
usb_free_urb(urb);
|
|
}
|
|
|
|
static int bpa10x_open(struct hci_dev *hdev)
|
|
{
|
|
struct bpa10x_data *data = hdev->driver_data;
|
|
struct usb_device *udev = data->udev;
|
|
unsigned long flags;
|
|
int err;
|
|
|
|
BT_DBG("hdev %p data %p", hdev, data);
|
|
|
|
if (test_and_set_bit(HCI_RUNNING, &hdev->flags))
|
|
return 0;
|
|
|
|
data->cmd_urb = bpa10x_alloc_urb(udev, usb_sndctrlpipe(udev, BPA10X_CMD_EP),
|
|
BPA10X_CMD_BUF_SIZE, GFP_KERNEL, data);
|
|
if (!data->cmd_urb) {
|
|
err = -ENOMEM;
|
|
goto done;
|
|
}
|
|
|
|
data->evt_urb = bpa10x_alloc_urb(udev, usb_rcvintpipe(udev, BPA10X_EVT_EP),
|
|
BPA10X_EVT_BUF_SIZE, GFP_KERNEL, data);
|
|
if (!data->evt_urb) {
|
|
bpa10x_free_urb(data->cmd_urb);
|
|
err = -ENOMEM;
|
|
goto done;
|
|
}
|
|
|
|
data->rx_urb = bpa10x_alloc_urb(udev, usb_rcvbulkpipe(udev, BPA10X_RX_EP),
|
|
BPA10X_RX_BUF_SIZE, GFP_KERNEL, data);
|
|
if (!data->rx_urb) {
|
|
bpa10x_free_urb(data->evt_urb);
|
|
bpa10x_free_urb(data->cmd_urb);
|
|
err = -ENOMEM;
|
|
goto done;
|
|
}
|
|
|
|
data->tx_urb = bpa10x_alloc_urb(udev, usb_sndbulkpipe(udev, BPA10X_TX_EP),
|
|
BPA10X_TX_BUF_SIZE, GFP_KERNEL, data);
|
|
if (!data->rx_urb) {
|
|
bpa10x_free_urb(data->rx_urb);
|
|
bpa10x_free_urb(data->evt_urb);
|
|
bpa10x_free_urb(data->cmd_urb);
|
|
err = -ENOMEM;
|
|
goto done;
|
|
}
|
|
|
|
write_lock_irqsave(&data->lock, flags);
|
|
|
|
err = usb_submit_urb(data->evt_urb, GFP_ATOMIC);
|
|
if (err < 0) {
|
|
BT_ERR("%s submit failed for event urb %p with error %d",
|
|
data->hdev->name, data->evt_urb, err);
|
|
} else {
|
|
err = usb_submit_urb(data->rx_urb, GFP_ATOMIC);
|
|
if (err < 0) {
|
|
BT_ERR("%s submit failed for rx urb %p with error %d",
|
|
data->hdev->name, data->evt_urb, err);
|
|
usb_kill_urb(data->evt_urb);
|
|
}
|
|
}
|
|
|
|
write_unlock_irqrestore(&data->lock, flags);
|
|
|
|
done:
|
|
if (err < 0)
|
|
clear_bit(HCI_RUNNING, &hdev->flags);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int bpa10x_close(struct hci_dev *hdev)
|
|
{
|
|
struct bpa10x_data *data = hdev->driver_data;
|
|
unsigned long flags;
|
|
|
|
BT_DBG("hdev %p data %p", hdev, data);
|
|
|
|
if (!test_and_clear_bit(HCI_RUNNING, &hdev->flags))
|
|
return 0;
|
|
|
|
write_lock_irqsave(&data->lock, flags);
|
|
|
|
skb_queue_purge(&data->cmd_queue);
|
|
usb_kill_urb(data->cmd_urb);
|
|
usb_kill_urb(data->evt_urb);
|
|
usb_kill_urb(data->rx_urb);
|
|
usb_kill_urb(data->tx_urb);
|
|
|
|
write_unlock_irqrestore(&data->lock, flags);
|
|
|
|
bpa10x_free_urb(data->cmd_urb);
|
|
bpa10x_free_urb(data->evt_urb);
|
|
bpa10x_free_urb(data->rx_urb);
|
|
bpa10x_free_urb(data->tx_urb);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int bpa10x_flush(struct hci_dev *hdev)
|
|
{
|
|
struct bpa10x_data *data = hdev->driver_data;
|
|
|
|
BT_DBG("hdev %p data %p", hdev, data);
|
|
|
|
skb_queue_purge(&data->cmd_queue);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int bpa10x_send_frame(struct sk_buff *skb)
|
|
{
|
|
struct hci_dev *hdev = (struct hci_dev *) skb->dev;
|
|
struct bpa10x_data *data;
|
|
|
|
BT_DBG("hdev %p skb %p type %d len %d", hdev, skb, bt_cb(skb)->pkt_type, skb->len);
|
|
|
|
if (!hdev) {
|
|
BT_ERR("Frame for unknown HCI device");
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (!test_bit(HCI_RUNNING, &hdev->flags))
|
|
return -EBUSY;
|
|
|
|
data = hdev->driver_data;
|
|
|
|
/* Prepend skb with frame type */
|
|
memcpy(skb_push(skb, 1), &bt_cb(skb)->pkt_type, 1);
|
|
|
|
switch (bt_cb(skb)->pkt_type) {
|
|
case HCI_COMMAND_PKT:
|
|
hdev->stat.cmd_tx++;
|
|
skb_queue_tail(&data->cmd_queue, skb);
|
|
break;
|
|
|
|
case HCI_ACLDATA_PKT:
|
|
hdev->stat.acl_tx++;
|
|
skb_queue_tail(&data->tx_queue, skb);
|
|
break;
|
|
|
|
case HCI_SCODATA_PKT:
|
|
hdev->stat.sco_tx++;
|
|
skb_queue_tail(&data->tx_queue, skb);
|
|
break;
|
|
};
|
|
|
|
read_lock(&data->lock);
|
|
|
|
bpa10x_wakeup(data);
|
|
|
|
read_unlock(&data->lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void bpa10x_destruct(struct hci_dev *hdev)
|
|
{
|
|
struct bpa10x_data *data = hdev->driver_data;
|
|
|
|
BT_DBG("hdev %p data %p", hdev, data);
|
|
|
|
kfree(data);
|
|
}
|
|
|
|
static int bpa10x_probe(struct usb_interface *intf, const struct usb_device_id *id)
|
|
{
|
|
struct usb_device *udev = interface_to_usbdev(intf);
|
|
struct hci_dev *hdev;
|
|
struct bpa10x_data *data;
|
|
int err;
|
|
|
|
BT_DBG("intf %p id %p", intf, id);
|
|
|
|
if (ignore)
|
|
return -ENODEV;
|
|
|
|
if (intf->cur_altsetting->desc.bInterfaceNumber > 0)
|
|
return -ENODEV;
|
|
|
|
data = kzalloc(sizeof(*data), GFP_KERNEL);
|
|
if (!data) {
|
|
BT_ERR("Can't allocate data structure");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
data->udev = udev;
|
|
|
|
rwlock_init(&data->lock);
|
|
|
|
skb_queue_head_init(&data->cmd_queue);
|
|
skb_queue_head_init(&data->tx_queue);
|
|
|
|
hdev = hci_alloc_dev();
|
|
if (!hdev) {
|
|
BT_ERR("Can't allocate HCI device");
|
|
kfree(data);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
data->hdev = hdev;
|
|
|
|
hdev->type = HCI_USB;
|
|
hdev->driver_data = data;
|
|
SET_HCIDEV_DEV(hdev, &intf->dev);
|
|
|
|
hdev->open = bpa10x_open;
|
|
hdev->close = bpa10x_close;
|
|
hdev->flush = bpa10x_flush;
|
|
hdev->send = bpa10x_send_frame;
|
|
hdev->destruct = bpa10x_destruct;
|
|
|
|
hdev->owner = THIS_MODULE;
|
|
|
|
err = hci_register_dev(hdev);
|
|
if (err < 0) {
|
|
BT_ERR("Can't register HCI device");
|
|
kfree(data);
|
|
hci_free_dev(hdev);
|
|
return err;
|
|
}
|
|
|
|
usb_set_intfdata(intf, data);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void bpa10x_disconnect(struct usb_interface *intf)
|
|
{
|
|
struct bpa10x_data *data = usb_get_intfdata(intf);
|
|
struct hci_dev *hdev = data->hdev;
|
|
|
|
BT_DBG("intf %p", intf);
|
|
|
|
if (!hdev)
|
|
return;
|
|
|
|
usb_set_intfdata(intf, NULL);
|
|
|
|
if (hci_unregister_dev(hdev) < 0)
|
|
BT_ERR("Can't unregister HCI device %s", hdev->name);
|
|
|
|
hci_free_dev(hdev);
|
|
}
|
|
|
|
static struct usb_driver bpa10x_driver = {
|
|
.name = "bpa10x",
|
|
.probe = bpa10x_probe,
|
|
.disconnect = bpa10x_disconnect,
|
|
.id_table = bpa10x_table,
|
|
};
|
|
|
|
static int __init bpa10x_init(void)
|
|
{
|
|
int err;
|
|
|
|
BT_INFO("Digianswer Bluetooth USB driver ver %s", VERSION);
|
|
|
|
err = usb_register(&bpa10x_driver);
|
|
if (err < 0)
|
|
BT_ERR("Failed to register USB driver");
|
|
|
|
return err;
|
|
}
|
|
|
|
static void __exit bpa10x_exit(void)
|
|
{
|
|
usb_deregister(&bpa10x_driver);
|
|
}
|
|
|
|
module_init(bpa10x_init);
|
|
module_exit(bpa10x_exit);
|
|
|
|
module_param(ignore, bool, 0644);
|
|
MODULE_PARM_DESC(ignore, "Ignore devices from the matching table");
|
|
|
|
MODULE_AUTHOR("Marcel Holtmann <marcel@holtmann.org>");
|
|
MODULE_DESCRIPTION("Digianswer Bluetooth USB driver ver " VERSION);
|
|
MODULE_VERSION(VERSION);
|
|
MODULE_LICENSE("GPL");
|