| /* |
| * Copyright (c) 2020 The Linux Foundation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * 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. |
| */ |
| |
| #include <linux/of.h> |
| #include <linux/io.h> |
| #include <linux/init.h> |
| #include <linux/slab.h> |
| #include <linux/list.h> |
| #include <linux/errno.h> |
| #include <linux/delay.h> |
| #include <linux/types.h> |
| #include <linux/kernel.h> |
| #include <linux/device.h> |
| #include <linux/module.h> |
| #include <linux/of_irq.h> |
| #include <linux/kthread.h> |
| #include <linux/debugfs.h> |
| #include <linux/mfd/syscon.h> |
| #include <uapi/linux/major.h> |
| #include <linux/ipc_logging.h> |
| #include <linux/remoteproc.h> |
| #include <linux/clk.h> |
| #include <linux/bt.h> |
| |
| static bool btss_debug; |
| module_param(btss_debug, bool, 0644); |
| |
| static unsigned short btss_boot_delay = 500; |
| module_param(btss_boot_delay, ushort, 0644); |
| |
| unsigned int pid_distinct(struct bt_descriptor *btDesc, enum pid_ops action) |
| { |
| int ret; |
| struct list_head *pid_q = &btDesc->pid_q; |
| pid_t pid = current->tgid; |
| struct pid_n *pid_cursor; |
| struct platform_device *rproc_pdev = btDesc->rproc_pdev; |
| struct rproc *rproc = platform_get_drvdata(rproc_pdev); |
| struct device *dev = &btDesc->pdev->dev; |
| |
| list_for_each_entry(pid_cursor, pid_q, list) { |
| if (pid_cursor->pid == pid) { |
| switch (action) { |
| case ADD: |
| atomic_inc(&pid_cursor->refcnt); |
| break; |
| case REMOVE: |
| atomic_dec(&pid_cursor->refcnt); |
| break; |
| case TERMINATE: |
| ret = atomic_read(&pid_cursor->refcnt); |
| atomic_sub(ret - 1, &rproc->power); |
| list_del(&pid_cursor->list); |
| kfree(pid_cursor); |
| pid_cursor = NULL; |
| break; |
| default: |
| dev_info(dev, "Invalid operation\n"); |
| pr_err("Invalid operation\n"); |
| return -ENOENT; |
| } |
| goto out; |
| } |
| } |
| |
| pid_cursor = kzalloc(sizeof(struct pid_n), GFP_KERNEL); |
| if (!pid_cursor) |
| return -ENOMEM; |
| |
| pid_cursor->pid = pid; |
| atomic_inc(&pid_cursor->refcnt); |
| list_add_tail(&pid_cursor->list, pid_q); |
| |
| if (!atomic_read(&btDesc->state)) { |
| mdelay(btss_boot_delay); |
| enable_irq(btDesc->ipc.irq); |
| } |
| out: |
| return pid_cursor ? atomic_read(&pid_cursor->refcnt) : 0; |
| } |
| EXPORT_SYMBOL(pid_distinct); |
| |
| void pid_show(struct bt_descriptor *btDesc) |
| { |
| struct pid_n *pid_cursor; |
| struct list_head *pid_q = &btDesc->pid_q; |
| struct device *dev = &btDesc->pdev->dev; |
| |
| dev_info(&btDesc->pdev->dev, "Rgistered PIDS:\n"); |
| list_for_each_entry(pid_cursor, pid_q, list) { |
| dev_info(dev, "%d\n", pid_cursor->pid); |
| } |
| } |
| |
| int bt_ipc_avail_size(struct bt_descriptor *btDesc) |
| { |
| return tty_buffer_space_avail(&btDesc->tty_port); |
| } |
| |
| static |
| void bt_read(struct bt_descriptor *btDesc, unsigned char *buf, int len) |
| { |
| tty_insert_flip_string(&btDesc->tty_port, buf, len); |
| tty_flip_buffer_push(&btDesc->tty_port); |
| |
| wake_up(&btDesc->ipc.wait_q); |
| } |
| |
| static |
| int bt_write(struct tty_struct *tty, const unsigned char *buf, int len) |
| { |
| int ret = 0; |
| struct bt_descriptor *btDesc = container_of(tty->port, |
| struct bt_descriptor, tty_port); |
| struct device *dev = &btDesc->pdev->dev; |
| |
| if (btDesc->sendmsg_cb) { |
| ret = btDesc->sendmsg_cb(btDesc, (unsigned char *)buf, len); |
| if (ret < 0 && ret != EAGAIN) |
| dev_err(dev, "failed to send msg, ret = %d\n", ret); |
| } |
| |
| return len; |
| } |
| |
| static int bt_write_room(struct tty_struct *tty) |
| { |
| return 2048; |
| } |
| |
| static |
| void bt_set_termios(struct tty_struct *tty, struct ktermios *old_termios) |
| { |
| } |
| |
| static int bt_tiocmget(struct tty_struct *tty) |
| { |
| return 0; |
| } |
| |
| static |
| int bt_tiocmset(struct tty_struct *tty, unsigned int set, unsigned int clear) |
| { |
| return 0; |
| } |
| |
| static |
| void __user *ioctl_arg_to_cust_msg(unsigned long arg, uint16_t *hdr, uint8_t *len) |
| { |
| int ret = get_user(*hdr, (uint16_t __user *)arg); |
| if (ret) return ERR_PTR(ret); |
| arg += sizeof(*hdr); |
| ret = get_user(*len, (uint8_t __user *)arg); |
| if (ret) return ERR_PTR(ret); |
| arg += sizeof(*len); |
| return (void __user *)arg; |
| } |
| |
| static |
| int bt_ioctl_cust_msg(struct tty_struct *tty, unsigned long arg) |
| { |
| int ret = 0; |
| uint16_t msg_hdr = 0; |
| uint8_t msg_len = 0; |
| struct bt_descriptor *btDesc = container_of(tty->port, |
| struct bt_descriptor, tty_port); |
| struct device *dev = &btDesc->pdev->dev; |
| void *kern_buff = NULL; |
| void __user *user_buff = ioctl_arg_to_cust_msg(arg, &msg_hdr, &msg_len); |
| |
| if (IS_ERR(user_buff)) return PTR_ERR(user_buff); |
| kern_buff = kmalloc(msg_len, GFP_KERNEL); |
| if (!kern_buff) return -ENOMEM; |
| copy_from_user(kern_buff, user_buff, msg_len); |
| |
| if (btDesc->sendmsg_excb) { |
| dev_info(dev, "to send cust msg hdr: %d, len: %d\n", |
| (int)msg_hdr, (int)msg_len); |
| print_hex_dump(KERN_INFO, "msg: ", DUMP_PREFIX_NONE, |
| 16, 1, kern_buff, msg_len, 0); |
| ret = btDesc->sendmsg_excb(btDesc, |
| msg_hdr, (unsigned char *)kern_buff, msg_len); |
| if (ret < 0) |
| dev_err(dev, |
| "failed to send cust msg, ret = %d\n", ret); |
| } |
| kfree(kern_buff); |
| return ret; |
| } |
| |
| static |
| int bt_ioctl(struct tty_struct *tty, unsigned int cmd, unsigned long arg) |
| { |
| int ret = 0; |
| struct bt_descriptor *btDesc = container_of(tty->port, |
| struct bt_descriptor, tty_port); |
| struct device *dev = &btDesc->pdev->dev; |
| struct platform_device *rproc_pdev = btDesc->rproc_pdev; |
| struct rproc *rproc = platform_get_drvdata(rproc_pdev); |
| |
| switch (cmd) { |
| case IOCTL_IPC_BOOT: |
| if (arg) { |
| ret = rproc_boot(rproc); |
| if (ret) |
| dev_err(dev, "m0 boot fail, ret = %d\n", ret); |
| else |
| pid_distinct(btDesc, ADD); |
| } else { |
| pid_distinct(btDesc, tty->closing ? TERMINATE : REMOVE); |
| rproc_shutdown(rproc); |
| } |
| break; |
| case IOCTL_IPC_CUST: |
| return bt_ioctl_cust_msg(tty, arg); |
| default: |
| ret = -ENOIOCTLCMD; |
| } |
| |
| return ret; |
| } |
| |
| static |
| long int bt_compat_ioctl(struct tty_struct *tty, unsigned int cmd, unsigned long arg) |
| { |
| return bt_ioctl(tty, cmd, arg); |
| } |
| |
| void bt_throttle(struct tty_struct *tty) |
| { |
| struct bt_descriptor *btDesc = container_of(tty->port, |
| struct bt_descriptor, tty_port); |
| |
| disable_irq_nosync(btDesc->ipc.irq); |
| } |
| |
| void bt_unthrottle(struct tty_struct *tty) |
| { |
| struct bt_descriptor *btDesc = container_of(tty->port, |
| struct bt_descriptor, tty_port); |
| |
| wake_up(&btDesc->ipc.wait_q); |
| enable_irq(btDesc->ipc.irq); |
| } |
| |
| static int bt_open(struct tty_struct *tty, struct file *file) |
| { |
| int ret = 0; |
| |
| tty->closing = 0; |
| ret = bt_ioctl(tty, IOCTL_IPC_BOOT, 1); |
| return ret; |
| } |
| |
| static void bt_close(struct tty_struct *tty, struct file *file) |
| { |
| struct bt_descriptor *btDesc = container_of(tty->port, |
| struct bt_descriptor, tty_port); |
| |
| tty->closing = 1; |
| bt_ioctl(tty, IOCTL_IPC_BOOT, 0); |
| pid_show(btDesc); |
| } |
| |
| static int bt_tty_activate(struct tty_port *port, struct tty_struct *tty) |
| { |
| return 0; |
| } |
| |
| static void bt_tty_shutdown(struct tty_port *port) |
| { |
| } |
| |
| static const struct tty_port_operations bt_port_ops = { |
| .activate = bt_tty_activate, |
| .shutdown = bt_tty_shutdown |
| }; |
| |
| static const struct tty_operations bt_ops = { |
| .open = bt_open, |
| .close = bt_close, |
| .write = bt_write, |
| .write_room = bt_write_room, |
| .set_termios = bt_set_termios, |
| .tiocmget = bt_tiocmget, |
| .tiocmset = bt_tiocmset, |
| .ioctl = bt_ioctl, |
| .compat_ioctl = bt_compat_ioctl, |
| .throttle = bt_throttle, |
| .unthrottle = bt_unthrottle, |
| }; |
| |
| int bt_tty_init(struct bt_descriptor *btDesc) |
| { |
| int ret; |
| struct device *btdev = &btDesc->pdev->dev; |
| struct device *dev; |
| |
| btDesc->tty_drv = tty_alloc_driver(1, |
| TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV); |
| if (!btDesc->tty_drv) { |
| ret = PTR_ERR(btDesc->tty_drv); |
| dev_err(btdev, "failed to alloc driver, ret %d\n", ret); |
| goto err; |
| } |
| |
| btDesc->tty_drv->driver_name = "bt_tty"; |
| btDesc->tty_drv->name = "ttyBT"; |
| btDesc->tty_drv->major = 0; |
| btDesc->tty_drv->minor_start = 0; |
| btDesc->tty_drv->type = TTY_DRIVER_TYPE_SERIAL; |
| btDesc->tty_drv->subtype = SERIAL_TYPE_NORMAL; |
| |
| btDesc->tty_drv->init_termios = tty_std_termios; |
| btDesc->tty_drv->init_termios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK |
| | ISTRIP | INLCR | IGNCR | ICRNL |
| | IXON); |
| |
| btDesc->tty_drv->init_termios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG |
| | IEXTEN); |
| |
| btDesc->tty_drv->init_termios.c_cflag &= ~(CSIZE | PARENB | CBAUD); |
| btDesc->tty_drv->init_termios.c_oflag &= ~(OPOST); |
| btDesc->tty_drv->init_termios.c_cflag |= CS8; |
| |
| btDesc->tty_drv->init_termios.c_cc[VMIN] = 0; |
| btDesc->tty_drv->init_termios.c_cc[VTIME] = 255; |
| btDesc->tty_drv->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV; |
| |
| tty_set_operations(btDesc->tty_drv, &bt_ops); |
| |
| ret = tty_register_driver(btDesc->tty_drv); |
| if (ret < 0) { |
| dev_err(btdev, "failed to register driver, ret %d\n", ret); |
| goto err_tty_put; |
| } |
| |
| tty_port_init(&btDesc->tty_port); |
| btDesc->tty_port.ops = &bt_port_ops; |
| |
| dev = tty_port_register_device(&btDesc->tty_port, |
| btDesc->tty_drv, 0, NULL); |
| if (IS_ERR(dev)) { |
| ret = PTR_ERR(dev); |
| dev_err(btdev, "failed to register port, ret %d\n", ret); |
| goto err_tty_unreg; |
| } |
| |
| return 0; |
| |
| err_tty_unreg: |
| tty_unregister_driver(btDesc->tty_drv); |
| err_tty_put: |
| put_tty_driver(btDesc->tty_drv); |
| err: |
| return ret; |
| } |
| |
| void bt_tty_deinit(struct bt_descriptor *btDesc) |
| { |
| struct tty_driver *tty_drv = btDesc->tty_drv; |
| |
| tty_unregister_device(tty_drv, 0); |
| tty_unregister_driver(tty_drv); |
| put_tty_driver(tty_drv); |
| } |
| |
| int bt_parse_ipc(struct bt_descriptor *btDesc) |
| { |
| int ret; |
| struct device_node *np; |
| const char *key = "qcom,ipc"; |
| struct bt_ipc *ipc = &btDesc->ipc; |
| struct device *dev = &btDesc->pdev->dev; |
| |
| np = of_parse_phandle(dev->of_node, key, 0); |
| if (!np) { |
| dev_err(dev, "no qcom,ipc node\n"); |
| return -ENODEV; |
| } |
| |
| ipc->regmap = syscon_node_to_regmap(np); |
| if (IS_ERR(ipc->regmap)) |
| return PTR_ERR(btDesc->ipc.regmap); |
| |
| ret = of_property_read_u32_index(dev->of_node, key, 1, &ipc->offset); |
| if (ret < 0) { |
| dev_err(dev, "no offset in %s, ret = %d\n", key, ret); |
| return ret; |
| } |
| |
| ret = of_property_read_u32_index(dev->of_node, key, 2, &ipc->bit); |
| if (ret < 0) { |
| dev_err(dev, "no bit in %s, ret = %d\n", key, ret); |
| return ret; |
| } |
| |
| ipc->irq = platform_get_irq(btDesc->pdev, 0); |
| if (ipc->irq < 0) { |
| dev_err(dev, "err getting IRQ ret = %d\n", ipc->irq); |
| return ipc->irq; |
| } |
| |
| dev_err(dev, "%s\n", __func__); |
| |
| return 0; |
| } |
| |
| int bt_parse_mem(struct bt_descriptor *btDesc) |
| { |
| struct resource *res; |
| struct device_node *np; |
| struct device *dev = &btDesc->pdev->dev; |
| struct reserved_mem *rmem = btDesc->rmem; |
| struct bt_mem *btmem = &btDesc->btmem; |
| |
| res = platform_get_resource_byname(btDesc->pdev, IORESOURCE_MEM, |
| "bt_warm_rst"); |
| btDesc->warm_reset = devm_ioremap_resource(dev, res); |
| if (IS_ERR(btDesc->warm_reset)) { |
| dev_err(dev, "no reset defined\n"); |
| return PTR_ERR(btDesc->warm_reset); |
| } |
| |
| np = of_parse_phandle(dev->of_node, "memory-region", 0); |
| if (!np) { |
| dev_err(dev, "no memory-region node\n"); |
| return -ENODEV; |
| } |
| |
| rmem = of_reserved_mem_lookup(np); |
| if (!rmem) { |
| dev_err(dev, "unable to acquire memory-region\n"); |
| return -EINVAL; |
| } |
| |
| btmem->phys = rmem->base; |
| btmem->reloc = rmem->base; |
| btmem->size = rmem->size; |
| btmem->virt = devm_ioremap_wc(dev, btmem->phys, btmem->size); |
| if (!btmem->virt) { |
| dev_err(dev, "unable to map memory region: %pa+%pa\n", |
| &rmem->base, &rmem->size); |
| return -EBUSY; |
| } |
| |
| dev_err(dev, "%s\n", __func__); |
| |
| return 0; |
| } |
| |
| int bt_parse_clks(struct bt_descriptor *btDesc) |
| { |
| struct device *dev = &btDesc->pdev->dev; |
| |
| btDesc->btss_reset = devm_reset_control_get(dev, "btss_reset"); |
| if (IS_ERR_OR_NULL(btDesc->btss_reset)) { |
| dev_err(dev, "unable to acquire btss_reset\n"); |
| return PTR_ERR(btDesc->btss_reset); |
| } |
| |
| btDesc->lpo_clk = devm_clk_get(dev, "lpo_clk"); |
| if (IS_ERR(btDesc->lpo_clk)) { |
| dev_err(dev, "failed to get lpo_clk\n"); |
| return PTR_ERR(btDesc->lpo_clk); |
| } |
| |
| return 0; |
| } |
| |
| int bt_parse_pinctrl(struct bt_descriptor *btDesc) |
| { |
| struct device *dev = &btDesc->pdev->dev; |
| |
| btDesc->pinctrl = devm_pinctrl_get(dev); |
| if (IS_ERR_OR_NULL(btDesc->pinctrl)) { |
| dev_err(dev, "unable to get pinctrl\n"); |
| return PTR_ERR(btDesc->pinctrl); |
| } |
| |
| return 0; |
| } |
| |
| int bt_parse_dt(struct bt_descriptor *btDesc) |
| { |
| int ret; |
| struct device *dev = &btDesc->pdev->dev; |
| |
| ret = of_property_read_string(dev->of_node, "firmware", |
| &btDesc->fw_name); |
| if (ret < 0) { |
| dev_err(dev, "could not find firmware name, ret = %d\n", ret); |
| return ret; |
| } |
| |
| ret = bt_parse_ipc(btDesc); |
| if (ret < 0) { |
| dev_err(dev, "could not get IPC info, ret = %d\n", ret); |
| return ret; |
| } |
| |
| ret = bt_parse_mem(btDesc); |
| if (ret < 0) { |
| dev_err(dev, "could not get mem info, ret = %d\n", ret); |
| return ret; |
| } |
| |
| ret = bt_parse_clks(btDesc); |
| if (ret < 0) { |
| dev_err(dev, "could not get clk info, ret = %d\n", ret); |
| return ret; |
| } |
| |
| if (btss_debug) { |
| ret = bt_parse_pinctrl(btDesc); |
| if (ret < 0) { |
| dev_err(dev, "could not get pinctrl info, ret = %d\n", |
| ret); |
| return ret; |
| } |
| } |
| |
| btDesc->nosecure = of_property_read_bool(dev->of_node, "qcom,nosecure"); |
| |
| dev_info(dev, "%s operating in %s mode\n", __func__, |
| btDesc->nosecure ? "non secure" : "secure"); |
| |
| return 0; |
| } |
| |
| #if defined(CONFIG_DEBUG_FS) |
| static int bt_dbgfs_open(struct inode *ip, struct file *fp) |
| { |
| return 0; |
| } |
| |
| static ssize_t bt_dbgfs_read(struct file *fp, char __user *buf, |
| size_t count, loff_t *pos) |
| { |
| return count; |
| } |
| |
| static int bt_dbgfs_release(struct inode *ip, struct file *fp) |
| { |
| return 0; |
| } |
| |
| |
| static ssize_t bt_dbgfs_write(struct file *fp, const char __user *buf, |
| size_t count, loff_t *pos) |
| { |
| return count; |
| } |
| |
| static const struct file_operations bt_dbgfs_ops = { |
| .owner = THIS_MODULE, |
| .open = bt_dbgfs_open, |
| .read = bt_dbgfs_read, |
| .write = bt_dbgfs_write, |
| .release = bt_dbgfs_release, |
| }; |
| |
| static int bt_debugfs_init(struct bt_descriptor *btDesc) |
| { |
| void *pret; |
| int index; |
| struct device *dev = &btDesc->pdev->dev; |
| static const char * const dirs[] = {"status", "to_console"}; |
| |
| btDesc->dbgfs = debugfs_create_dir(btDesc->pdev->name, NULL); |
| if (IS_ERR(btDesc->dbgfs)) { |
| dev_err(dev, "debugfs creation failed %s, ret = %ld\n", |
| btDesc->pdev->name, PTR_ERR(btDesc->dbgfs)); |
| return PTR_ERR(btDesc->dbgfs); |
| } |
| |
| for (index = 0; index < ARRAY_SIZE(dirs); index++) { |
| pret = debugfs_create_file(dirs[index], S_IRUSR | S_IWUSR, |
| btDesc->dbgfs, NULL, &bt_dbgfs_ops); |
| if (IS_ERR(pret)) { |
| dev_err(dev, "debugfs creation failed %s, ret = %ld\n", |
| dirs[index], PTR_ERR(pret)); |
| debugfs_remove_recursive(btDesc->dbgfs); |
| return PTR_ERR(pret); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static void bt_debugfs_deinit(struct bt_descriptor *btDesc) |
| { |
| debugfs_remove_recursive(btDesc->dbgfs); |
| } |
| #else |
| static inline int bt_debugfs_init(struct bt_descriptor *btDesc) |
| { |
| return 0; |
| } |
| |
| static inline void bt_debugfs_deinit(struct bt_descriptor *btDesc) |
| { |
| } |
| #endif |
| |
| static int bt_probe(struct platform_device *pdev) |
| { |
| int ret; |
| struct bt_descriptor *btDesc; |
| struct pinctrl_state *pin_state; |
| |
| btDesc = devm_kzalloc(&pdev->dev, sizeof(*btDesc), GFP_KERNEL); |
| if (!btDesc) |
| return -ENOMEM; |
| |
| btDesc->pdev = pdev; |
| |
| ret = bt_parse_dt(btDesc); |
| if (ret < 0) { |
| dev_err(&pdev->dev, "err parsing DT, ret = %d\n", ret); |
| goto err; |
| } |
| |
| ret = bt_debugfs_init(btDesc); |
| if (ret < 0) |
| dev_err(&pdev->dev, "err register debugFs, ret = %d\n", ret); |
| |
| if (!btss_debug) { |
| ret = bt_tty_init(btDesc); |
| if (ret < 0) { |
| dev_err(&pdev->dev, |
| "err initializing TTY, ret = %d\n", ret); |
| goto err; |
| } |
| |
| btDesc->recvmsg_cb = bt_read; |
| } else { |
| btDesc->debug_en = true; |
| |
| pin_state = pinctrl_lookup_state(btDesc->pinctrl, "btss_pins"); |
| if (IS_ERR(pin_state)) { |
| ret = PTR_ERR(pin_state); |
| dev_err(&pdev->dev, |
| "btss pinctrl state err, ret = %d\n", ret); |
| goto err; |
| } |
| pinctrl_select_state(btDesc->pinctrl, pin_state); |
| |
| } |
| |
| btDesc->rproc_pdev = platform_device_register_data(&pdev->dev, |
| "bt_rproc_driver", |
| pdev->id, &btDesc, |
| sizeof(btDesc)); |
| if (IS_ERR(btDesc->rproc_pdev)) { |
| ret = PTR_ERR(btDesc->rproc_pdev); |
| dev_err(&pdev->dev, "err registering rproc, ret = %d\n", ret); |
| goto err_deinit_tty; |
| } |
| |
| INIT_LIST_HEAD(&btDesc->pid_q); |
| |
| platform_set_drvdata(pdev, btDesc); |
| |
| return 0; |
| |
| err_deinit_tty: |
| bt_tty_deinit(btDesc); |
| err: |
| return ret; |
| } |
| |
| static int bt_remove(struct platform_device *pdev) |
| { |
| struct bt_descriptor *btDesc = platform_get_drvdata(pdev); |
| |
| bt_tty_deinit(btDesc); |
| bt_debugfs_deinit(btDesc); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id bt_of_match[] = { |
| {.compatible = "qcom,bt"}, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, bt_of_match); |
| |
| static struct platform_driver bt_driver = { |
| .probe = bt_probe, |
| .remove = bt_remove, |
| .driver = { |
| .name = "bt_driver", |
| .owner = THIS_MODULE, |
| .of_match_table = bt_of_match, |
| }, |
| }; |
| |
| static int __init bt_init(void) |
| { |
| int ret; |
| |
| ret = platform_driver_register(&bt_driver); |
| if (ret) |
| pr_err("%s: plat_driver registeration failed\n", __func__); |
| |
| return ret; |
| } |
| |
| static void __exit bt_exit(void) |
| { |
| platform_driver_unregister(&bt_driver); |
| } |
| |
| module_init(bt_init); |
| module_exit(bt_exit); |
| |
| MODULE_DESCRIPTION("QTI Technologies, Inc."); |
| MODULE_LICENSE("GPL v2"); |