// SPDX-License-Identifier: GPL-2.0 /* * Copyright (c) 2019 MediaTek Inc. */ #include #include #include #include "synaptics_tcm_core.h" #define CHAR_DEVICE_NAME "tcm" #define CONCURRENT true #define DEVICE_IOC_MAGIC 's' #define DEVICE_IOC_RESET _IO(DEVICE_IOC_MAGIC, 0) /* 0x00007300 */ #define DEVICE_IOC_IRQ _IOW(DEVICE_IOC_MAGIC, 1, int) /* 0x40047301 */ #define DEVICE_IOC_RAW _IOW(DEVICE_IOC_MAGIC, 2, int) /* 0x40047302 */ #define DEVICE_IOC_CONCURRENT _IOW(DEVICE_IOC_MAGIC, 3, int) /* 0x40047303 */ struct device_hcd { dev_t dev_num; bool raw_mode; bool concurrent; unsigned int ref_count; struct cdev char_dev; struct class *class; struct device *device; struct syna_tcm_buffer out; struct syna_tcm_buffer resp; struct syna_tcm_buffer report; struct syna_tcm_hcd *tcm_hcd; }; DECLARE_COMPLETION(device_remove_complete); static struct device_hcd *device_hcd; static int rmidev_major_num; static void device_capture_touch_report(unsigned int count) { int retval; unsigned char id; unsigned int idx; unsigned int size; unsigned char *data; struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; static bool report; static unsigned int offset; static unsigned int remaining_size; if (count < 2) return; data = &device_hcd->resp.buf[0]; if (data[0] != MESSAGE_MARKER) return; id = data[1]; size = 0; LOCK_BUFFER(device_hcd->report); switch (id) { case REPORT_TOUCH: if (count >= 4) { remaining_size = le2_to_uint(&data[2]); } else { report = false; goto exit; } retval = syna_tcm_alloc_mem(tcm_hcd, &device_hcd->report, remaining_size); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to allocate memory for device_hcd->report.buf\n"); report = false; goto exit; } idx = 4; size = count - idx; offset = 0; report = true; break; case STATUS_CONTINUED_READ: if (report == false) goto exit; if (count >= 2) { idx = 2; size = count - idx; } break; default: goto exit; } if (size) { size = MIN(size, remaining_size); retval = secure_memcpy(&device_hcd->report.buf[offset], device_hcd->report.buf_size - offset, &data[idx], count - idx, size); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to copy touch report data\n"); report = false; goto exit; } else { offset += size; remaining_size -= size; device_hcd->report.data_length += size; } } if (remaining_size) goto exit; LOCK_BUFFER(tcm_hcd->report.buffer); tcm_hcd->report.buffer.buf = device_hcd->report.buf; tcm_hcd->report.buffer.buf_size = device_hcd->report.buf_size; tcm_hcd->report.buffer.data_length = device_hcd->report.data_length; tcm_hcd->report_touch(); UNLOCK_BUFFER(tcm_hcd->report.buffer); report = false; exit: UNLOCK_BUFFER(device_hcd->report); } static int device_capture_touch_report_config(unsigned int count) { int retval; unsigned int size; unsigned int buf_size; unsigned char *data; struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; if (device_hcd->raw_mode) { if (count < 3) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Invalid write data\n"); return -EINVAL; } size = le2_to_uint(&device_hcd->out.buf[1]); if (count - 3 < size) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Incomplete write data\n"); return -EINVAL; } if (!size) return 0; data = &device_hcd->out.buf[3]; buf_size = device_hcd->out.buf_size - 3; } else { size = count - 1; if (!size) return 0; data = &device_hcd->out.buf[1]; buf_size = device_hcd->out.buf_size - 1; } LOCK_BUFFER(tcm_hcd->config); retval = syna_tcm_alloc_mem(tcm_hcd, &tcm_hcd->config, size); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to allocate memory for tcm_hcd->config.buf\n"); UNLOCK_BUFFER(tcm_hcd->config); return retval; } retval = secure_memcpy(tcm_hcd->config.buf, tcm_hcd->config.buf_size, data, buf_size, size); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to copy touch report config data\n"); UNLOCK_BUFFER(tcm_hcd->config); return retval; } tcm_hcd->config.data_length = size; UNLOCK_BUFFER(tcm_hcd->config); return 0; } #ifdef HAVE_UNLOCKED_IOCTL static long device_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) #else static int device_ioctl(struct inode *inp, struct file *filp, unsigned int cmd, unsigned long arg) #endif { int retval; struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; mutex_lock(&tcm_hcd->extif_mutex); retval = 0; switch (cmd) { case DEVICE_IOC_RESET: retval = tcm_hcd->reset(tcm_hcd, false, true); break; case DEVICE_IOC_IRQ: if (arg == 0) retval = tcm_hcd->enable_irq(tcm_hcd, false, false); else if (arg == 1) retval = tcm_hcd->enable_irq(tcm_hcd, true, NULL); break; case DEVICE_IOC_RAW: if (arg == 0) { device_hcd->raw_mode = false; tcm_hcd->update_watchdog(tcm_hcd, true); } else if (arg == 1) { device_hcd->raw_mode = true; tcm_hcd->update_watchdog(tcm_hcd, false); } break; case DEVICE_IOC_CONCURRENT: if (arg == 0) device_hcd->concurrent = false; else if (arg == 1) device_hcd->concurrent = true; break; default: retval = -ENOTTY; break; } mutex_unlock(&tcm_hcd->extif_mutex); return retval; } static loff_t device_llseek(struct file *filp, loff_t off, int whence) { return -EINVAL; } static ssize_t device_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { int retval; struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; if (count == 0) return 0; mutex_lock(&tcm_hcd->extif_mutex); LOCK_BUFFER(device_hcd->resp); if (device_hcd->raw_mode) { retval = syna_tcm_alloc_mem(tcm_hcd, &device_hcd->resp, count); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to allocate memory for device_hcd->resp.buf\n"); UNLOCK_BUFFER(device_hcd->resp); goto exit; } retval = tcm_hcd->read_message(tcm_hcd, device_hcd->resp.buf, count); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to read message\n"); UNLOCK_BUFFER(device_hcd->resp); goto exit; } } else { if (count != device_hcd->resp.data_length) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Invalid length information\n"); UNLOCK_BUFFER(device_hcd->resp); retval = -EINVAL; goto exit; } } if (copy_to_user(buf, device_hcd->resp.buf, count)) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to copy data to user space\n"); UNLOCK_BUFFER(device_hcd->resp); retval = -EINVAL; goto exit; } if (!device_hcd->concurrent) goto skip_concurrent; if (tcm_hcd->report_touch == NULL) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Unable to report touch\n"); device_hcd->concurrent = false; } if (device_hcd->raw_mode) device_capture_touch_report(count); skip_concurrent: UNLOCK_BUFFER(device_hcd->resp); retval = count; exit: mutex_unlock(&tcm_hcd->extif_mutex); return retval; } static ssize_t device_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { int retval; struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; if (count == 0) return 0; mutex_lock(&tcm_hcd->extif_mutex); LOCK_BUFFER(device_hcd->out); retval = syna_tcm_alloc_mem(tcm_hcd, &device_hcd->out, count == 1 ? count + 1 : count); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to allocate memory for device_hcd->out.buf\n"); UNLOCK_BUFFER(device_hcd->out); goto exit; } if (copy_from_user(device_hcd->out.buf, buf, count)) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to copy data from user space\n"); UNLOCK_BUFFER(device_hcd->out); retval = -EINVAL; goto exit; } LOCK_BUFFER(device_hcd->resp); if (device_hcd->raw_mode) { retval = tcm_hcd->write_message(tcm_hcd, device_hcd->out.buf[0], &device_hcd->out.buf[1], count - 1, NULL, NULL, NULL, NULL, 0); } else { mutex_lock(&tcm_hcd->reset_mutex); retval = tcm_hcd->write_message(tcm_hcd, device_hcd->out.buf[0], &device_hcd->out.buf[1], count - 1, &device_hcd->resp.buf, &device_hcd->resp.buf_size, &device_hcd->resp.data_length, NULL, 0); mutex_unlock(&tcm_hcd->reset_mutex); } if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to write command 0x%02x\n", device_hcd->out.buf[0]); UNLOCK_BUFFER(device_hcd->resp); UNLOCK_BUFFER(device_hcd->out); goto exit; } if (count && device_hcd->out.buf[0] == CMD_SET_TOUCH_REPORT_CONFIG) { retval = device_capture_touch_report_config(count); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to capture touch report config\n"); } } UNLOCK_BUFFER(device_hcd->out); if (device_hcd->raw_mode) retval = count; else retval = device_hcd->resp.data_length; UNLOCK_BUFFER(device_hcd->resp); exit: mutex_unlock(&tcm_hcd->extif_mutex); return retval; } static int device_open(struct inode *inp, struct file *filp) { int retval; struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; mutex_lock(&tcm_hcd->extif_mutex); if (device_hcd->ref_count < 1) { device_hcd->ref_count++; retval = 0; } else { retval = -EACCES; } mutex_unlock(&tcm_hcd->extif_mutex); return retval; } static int device_release(struct inode *inp, struct file *filp) { struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; mutex_lock(&tcm_hcd->extif_mutex); if (device_hcd->ref_count) device_hcd->ref_count--; mutex_unlock(&tcm_hcd->extif_mutex); return 0; } static char *device_devnode(struct device *dev, umode_t *mode) { if (!mode) return NULL; *mode = 0666; return kasprintf(GFP_KERNEL, "%s/%s", PLATFORM_DRIVER_NAME, dev_name(dev)); } static int device_create_class(void) { struct syna_tcm_hcd *tcm_hcd = device_hcd->tcm_hcd; if (device_hcd->class != NULL) return 0; device_hcd->class = class_create(THIS_MODULE, PLATFORM_DRIVER_NAME); if (IS_ERR(device_hcd->class)) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to create class\n"); return -ENODEV; } device_hcd->class->devnode = device_devnode; return 0; } static const struct file_operations device_fops = { .owner = THIS_MODULE, #ifdef HAVE_UNLOCKED_IOCTL .unlocked_ioctl = device_ioctl, #ifdef HAVE_COMPAT_IOCTL .compat_ioctl = device_ioctl, #endif #else .ioctl = device_ioctl, #endif .llseek = device_llseek, .read = device_read, .write = device_write, .open = device_open, .release = device_release, }; static int device_init(struct syna_tcm_hcd *tcm_hcd) { int retval; dev_t dev_num; const struct syna_tcm_board_data *bdata = tcm_hcd->hw_if->bdata; device_hcd = kzalloc(sizeof(*device_hcd), GFP_KERNEL); if (!device_hcd) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to allocate memory for device_hcd\n"); return -ENOMEM; } device_hcd->tcm_hcd = tcm_hcd; device_hcd->concurrent = CONCURRENT; INIT_BUFFER(device_hcd->out, false); INIT_BUFFER(device_hcd->resp, false); INIT_BUFFER(device_hcd->report, false); if (rmidev_major_num) { dev_num = MKDEV(rmidev_major_num, 0); retval = register_chrdev_region(dev_num, 1, PLATFORM_DRIVER_NAME); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to register char device\n"); goto err_register_chrdev_region; } } else { retval = alloc_chrdev_region(&dev_num, 0, 1, PLATFORM_DRIVER_NAME); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to allocate char device\n"); goto err_alloc_chrdev_region; } rmidev_major_num = MAJOR(dev_num); } device_hcd->dev_num = dev_num; cdev_init(&device_hcd->char_dev, &device_fops); retval = cdev_add(&device_hcd->char_dev, dev_num, 1); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to add char device\n"); goto err_add_chardev; } retval = device_create_class(); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to create class\n"); goto err_create_class; } device_hcd->device = device_create(device_hcd->class, NULL, device_hcd->dev_num, NULL, CHAR_DEVICE_NAME"%d", MINOR(device_hcd->dev_num)); if (IS_ERR(device_hcd->device)) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to create device\n"); retval = -ENODEV; goto err_create_device; } if (bdata->irq_gpio >= 0) { retval = gpio_export(bdata->irq_gpio, false); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to export GPIO\n"); } else { retval = gpio_export_link(&tcm_hcd->pdev->dev, "attn", bdata->irq_gpio); if (retval < 0) { LOG_ERR(tcm_hcd->pdev->dev.parent, "Failed to export GPIO link\n"); } } } return 0; err_create_device: class_destroy(device_hcd->class); err_create_class: cdev_del(&device_hcd->char_dev); err_add_chardev: unregister_chrdev_region(dev_num, 1); err_alloc_chrdev_region: err_register_chrdev_region: RELEASE_BUFFER(device_hcd->report); RELEASE_BUFFER(device_hcd->resp); RELEASE_BUFFER(device_hcd->out); kfree(device_hcd); device_hcd = NULL; return retval; } static int device_remove(struct syna_tcm_hcd *tcm_hcd) { if (!device_hcd) goto exit; device_destroy(device_hcd->class, device_hcd->dev_num); class_destroy(device_hcd->class); cdev_del(&device_hcd->char_dev); unregister_chrdev_region(device_hcd->dev_num, 1); RELEASE_BUFFER(device_hcd->report); RELEASE_BUFFER(device_hcd->resp); RELEASE_BUFFER(device_hcd->out); kfree(device_hcd); device_hcd = NULL; exit: complete(&device_remove_complete); return 0; } static int device_reset(struct syna_tcm_hcd *tcm_hcd) { int retval; if (!device_hcd) { retval = device_init(tcm_hcd); return retval; } return 0; } static struct syna_tcm_module_cb device_module = { .type = TCM_DEVICE, .init = device_init, .remove = device_remove, .syncbox = NULL, .asyncbox = NULL, .reset = device_reset, .suspend = NULL, .resume = NULL, .early_suspend = NULL, }; static int __init device_module_init(void) { return syna_tcm_add_module(&device_module, true); } static void __exit device_module_exit(void) { syna_tcm_add_module(&device_module, false); wait_for_completion(&device_remove_complete); } module_init(device_module_init); module_exit(device_module_exit); MODULE_AUTHOR("Synaptics, Inc."); MODULE_DESCRIPTION("Synaptics TCM Device Module"); MODULE_LICENSE("GPL v2");