| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * ASR built-in touchscreen driver |
| * |
| * Copyright (C) 2023 ASR |
| */ |
| |
| #include <linux/platform_device.h> |
| #include <linux/input.h> |
| #include <linux/interrupt.h> |
| #include <linux/module.h> |
| #include <linux/clk.h> |
| #include <linux/io.h> |
| #include <linux/slab.h> |
| #include <linux/of.h> |
| #include <linux/timer.h> |
| |
| /* |
| * Touchscreen controller register offsets |
| */ |
| #define ASR_TSCR 0x0 |
| #define ASR_TSSR 0x4 |
| #define ASR_XYDR(x) (0x8 + (x) * 0x4) |
| |
| #define ASR_TSSR_DOWN 0 |
| #define ASR_TSSR_UP 1 |
| |
| #define ASR_TSC_MIN_XY_VAL 0 |
| #define ASR_TSC_MAX_XY_VAL 4095 |
| #define ASR_TSC_DOT_CNT_MAX 8 |
| |
| #define MOD_NAME "ts-asr" |
| |
| #define tsc_readl(dev, reg) \ |
| __raw_readl((dev)->tsc_base + (reg)) |
| #define tsc_writel(dev, reg, val) \ |
| __raw_writel((val), (dev)->tsc_base + (reg)) |
| |
| struct asr_tp_info { |
| int x; |
| int y; |
| int last_x; |
| int last_y; |
| }; |
| |
| struct asr_tsc { |
| struct timer_list move_timer; |
| struct input_dev *dev; |
| void __iomem *tsc_base; |
| int rdy_irq; |
| int up_irq; |
| int down_irq; |
| struct clk *clk; |
| struct asr_tp_info tp; |
| int dummpy_points; |
| }; |
| |
| #define ASR_TSC_MOVE_TIMEOUT 10 /* ms */ |
| #define ASR_TSC_DUMMY_POINTS 10 |
| #define ASR_TSC_TP_MARGIN 10 |
| |
| enum { |
| TSC_MODE_IDLE = 0, |
| TSC_MODE_WAIT_PRESSDOWN, |
| TSC_MODE_WAIT_RISEUP, |
| TSC_MODE_WAIT_AUTOMEASURE, |
| }; |
| static void asr_ts_set_mode(struct asr_tsc *tsc, int mode); |
| |
| static void asr_ts_movement_detect(struct timer_list *t) |
| { |
| struct asr_tsc *tsc = from_timer(tsc, t, move_timer); |
| u32 val; |
| val = tsc_readl(tsc, ASR_TSSR); |
| if (val & ASR_TSSR_UP) { |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| return; |
| } |
| |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_AUTOMEASURE); |
| } |
| |
| static void asr_ts_adc_init(void) |
| { |
| void __iomem* apb_spare; |
| u32 val; |
| |
| apb_spare = ioremap(0xD4090000, SZ_4K); |
| if (!apb_spare) { |
| pr_err("%s: error to ioremap apb_spare base\n", __func__); |
| return; |
| } |
| |
| val = readl(apb_spare + 0x12c); |
| val |= (1 << 8) | (1 << 20) | (1 << 22) | (0x4 << 24); |
| writel(val, apb_spare + 0x12c); |
| iounmap(apb_spare); |
| } |
| |
| static void asr_ts_set_mode(struct asr_tsc *tsc, int mode) |
| { |
| u32 val; |
| val = tsc_readl(tsc, ASR_TSCR); |
| val &= ~(0x3 << 1); |
| switch (mode) { |
| case TSC_MODE_WAIT_PRESSDOWN: |
| val &= ~((1 << 8) | (1 << 7)); |
| val |= (1 << 6) | (1 << 9); |
| break; |
| case TSC_MODE_WAIT_RISEUP: |
| val &= ~((1 << 8) | (1 << 6)); |
| val |= (1 << 7) | (1 << 9); |
| break; |
| case TSC_MODE_WAIT_AUTOMEASURE: |
| val &= ~((1 << 6) | (1 << 7) | (1 << 9)); |
| val |= (1 << 8); |
| break; |
| } |
| val |= (mode << 1); |
| tsc_writel(tsc, ASR_TSCR, val); |
| } |
| |
| static void asr_ts_measure(struct asr_tsc *tsc) |
| { |
| int i; |
| u32 val, meas_x, meas_y, sum_x = 0, sum_y = 0; |
| u16 min_x, max_x, min_y, max_y; |
| u16 x[ASR_TSC_DOT_CNT_MAX] = {0}; |
| u16 y[ASR_TSC_DOT_CNT_MAX] = {0}; |
| struct asr_tp_info *tp = (struct asr_tp_info *)&tsc->tp; |
| |
| for (i = 0; i < ASR_TSC_DOT_CNT_MAX; i++) { |
| val = tsc_readl(tsc, ASR_XYDR(i)); |
| x[i] = val & 0xffff; |
| sum_x += x[i]; |
| if (i == 0 ) { |
| min_x = x[0]; |
| max_x = x[0]; |
| } |
| if (min_x > x[i]) |
| min_x = x[i]; |
| if (max_x < x[i]) |
| max_x = x[i]; |
| |
| y[i] = (val >> 16) & 0xffff; |
| sum_y += y[i]; |
| |
| if (i == 0 ) { |
| min_y = y[0]; |
| max_y = y[0]; |
| } |
| if (min_y > y[i]) |
| min_y = y[i]; |
| if(max_y < y[i]) |
| max_y = y[i]; |
| pr_debug(" %s: [%d] (%d, %d)\n", __func__, i, x[i], y[i]); |
| } |
| |
| meas_x = (sum_x - max_x - min_x) / (ASR_TSC_DOT_CNT_MAX - 2); |
| meas_y = (sum_y - max_y - min_y) / (ASR_TSC_DOT_CNT_MAX - 2); |
| |
| pr_debug("meas_x: %d, meas_y: %d\n", meas_x, meas_y); |
| tp->x = meas_x; |
| tp->y = meas_y; |
| } |
| |
| static irqreturn_t asr_ts_down_isr(int irq, void *dev_id) |
| { |
| u32 val; |
| struct asr_tsc *tsc = (struct asr_tsc *)dev_id; |
| val = tsc_readl(tsc, ASR_TSSR); |
| |
| if (val & (1 << 6)) { |
| val |= (1 << 6); |
| tsc_writel(tsc, ASR_TSSR, val); |
| if (val & ASR_TSSR_UP) { |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| return IRQ_HANDLED; |
| } |
| |
| tsc->dummpy_points = 1; |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_AUTOMEASURE); |
| pr_debug("%s: got down irq.\n", __func__); |
| } else { |
| pr_warn("%s: mismatch irq?\n", __func__); |
| } |
| return IRQ_HANDLED; |
| } |
| |
| static irqreturn_t asr_ts_rdy_isr(int irq, void *dev_id) |
| { |
| u32 val; |
| int x, y; |
| struct asr_tsc *tsc = (struct asr_tsc *)dev_id; |
| struct input_dev *input = tsc->dev; |
| |
| val = tsc_readl(tsc, ASR_TSSR); |
| if (val & (1 << 8)) { |
| val |= (1 << 8); |
| tsc_writel(tsc, ASR_TSSR, val); |
| #if 0 |
| if (val & ASR_TSSR_UP) { |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| return IRQ_HANDLED; |
| } |
| #endif |
| asr_ts_measure(tsc); |
| if (tsc->dummpy_points) { |
| tsc->dummpy_points++; |
| if (tsc->dummpy_points > ASR_TSC_DUMMY_POINTS) { |
| tsc->dummpy_points = 0; |
| x = tsc->tp.last_x = tsc->tp.x; |
| y = tsc->tp.last_y = tsc->tp.y; |
| } else |
| goto skip; |
| } else { |
| if (abs(tsc->tp.x - tsc->tp.last_x) < ASR_TSC_TP_MARGIN && |
| abs(tsc->tp.y - tsc->tp.last_y) < ASR_TSC_TP_MARGIN) |
| goto skip; |
| } |
| |
| x = tsc->tp.last_x = tsc->tp.x; |
| y = tsc->tp.last_y = tsc->tp.y; |
| |
| input_report_abs(input, ABS_X, x); |
| input_report_abs(input, ABS_Y, y); |
| input_report_key(input, BTN_TOUCH, 1); |
| input_sync(input); |
| skip: |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_RISEUP); |
| mod_timer(&tsc->move_timer, jiffies + msecs_to_jiffies(ASR_TSC_MOVE_TIMEOUT)); |
| pr_debug("%s: got rdy irq. 0x%x, (%d, %d)\n", __func__, val, tsc->tp.x, tsc->tp.y); |
| } else { |
| pr_warn("%s: mismatch irq?\n", __func__); |
| } |
| return IRQ_HANDLED; |
| } |
| |
| static irqreturn_t asr_ts_up_isr(int irq, void *dev_id) |
| { |
| u32 val; |
| struct asr_tsc *tsc = (struct asr_tsc *)dev_id; |
| struct input_dev *input = tsc->dev; |
| |
| val = tsc_readl(tsc, ASR_TSSR); |
| if (val & (1 << 7)) { |
| val |= (1 << 7); |
| tsc_writel(tsc, ASR_TSSR, val); |
| if ((val & 1) == ASR_TSSR_DOWN) |
| return IRQ_HANDLED; |
| |
| pr_debug("%s: got up irq. (%d, %d)\n", __func__, tsc->tp.x, tsc->tp.y); |
| input_report_key(input, BTN_TOUCH, 0); |
| input_sync(input); |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| } else { |
| pr_warn("%s: mismatch irq?\n", __func__); |
| } |
| return IRQ_HANDLED; |
| } |
| |
| static void asr_stop_tsc(struct asr_tsc *tsc) |
| { |
| tsc_writel(tsc, ASR_TSCR, 0); |
| clk_disable_unprepare(tsc->clk); |
| } |
| |
| static int asr_setup_tsc(struct asr_tsc *tsc) |
| { |
| u32 tmp; |
| int err; |
| asr_ts_adc_init(); |
| |
| err = clk_prepare_enable(tsc->clk); |
| if (err) |
| return err; |
| |
| tmp = tsc_readl(tsc, ASR_TSCR); |
| tmp |= (1 << 0); /* enabled tsc */ |
| tmp |= 0x7 << 16; /* YD ADC channel slection */ |
| tmp |= 0x6 << 11; /* XL ADC channel slection */ |
| tmp |= (1 << 8); /* INT_RDY_EN */ |
| tmp |= (0x20 << 24); |
| tmp |= (0 << 21); /* XL_PU_RES_SEL */ |
| tmp |= ((ASR_TSC_DOT_CNT_MAX - 1) << 3); /* DOT_CNT */ |
| tsc_writel(tsc, ASR_TSCR, tmp); |
| asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| return 0; |
| } |
| |
| static int asr_ts_open(struct input_dev *dev) |
| { |
| struct asr_tsc *tsc = input_get_drvdata(dev); |
| return asr_setup_tsc(tsc); |
| } |
| |
| static void asr_ts_close(struct input_dev *dev) |
| { |
| struct asr_tsc *tsc = input_get_drvdata(dev); |
| asr_stop_tsc(tsc); |
| } |
| |
| static int asr_ts_probe(struct platform_device *pdev) |
| { |
| struct asr_tsc *tsc; |
| struct input_dev *input; |
| struct resource *res; |
| resource_size_t size; |
| int irq0, irq1, irq2; |
| int error; |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| if (!res) { |
| dev_err(&pdev->dev, "Can't get memory resource\n"); |
| return -ENOENT; |
| } |
| |
| irq0 = platform_get_irq(pdev, 0); |
| irq1 = platform_get_irq(pdev, 1); |
| irq2 = platform_get_irq(pdev, 2); |
| if (irq0 < 0 || irq1 < 0 || irq2 < 0) { |
| dev_err(&pdev->dev, "err irq:%d, %d, %d\n", irq0, irq1, irq2); |
| return -ENOENT; |
| } |
| |
| tsc = kzalloc(sizeof(*tsc), GFP_KERNEL); |
| input = input_allocate_device(); |
| if (!tsc || !input) { |
| dev_err(&pdev->dev, "failed allocating memory\n"); |
| error = -ENOMEM; |
| goto err_free_mem; |
| } |
| |
| tsc->dev = input; |
| tsc->down_irq = irq0; |
| tsc->rdy_irq = irq1; |
| tsc->up_irq = irq2; |
| |
| size = resource_size(res); |
| |
| if (!request_mem_region(res->start, size, pdev->name)) { |
| dev_err(&pdev->dev, "TSC registers are not free\n"); |
| error = -EBUSY; |
| goto err_free_mem; |
| } |
| |
| tsc->tsc_base = ioremap(res->start, size); |
| if (!tsc->tsc_base) { |
| dev_err(&pdev->dev, "Can't map memory\n"); |
| error = -ENOMEM; |
| goto err_release_mem; |
| } |
| |
| tsc->clk = clk_get(&pdev->dev, NULL); |
| if (IS_ERR(tsc->clk)) { |
| dev_err(&pdev->dev, "failed getting clock\n"); |
| error = PTR_ERR(tsc->clk); |
| goto err_unmap; |
| } |
| |
| input->name = MOD_NAME; |
| input->phys = "asr/input0"; |
| input->id.bustype = BUS_HOST; |
| input->id.vendor = 0x0001; |
| input->id.product = 0x0002; |
| input->id.version = 0x0100; |
| input->dev.parent = &pdev->dev; |
| input->open = asr_ts_open; |
| input->close = asr_ts_close; |
| |
| __set_bit(EV_ABS, input->evbit); |
| __set_bit(ABS_X, input->absbit); |
| __set_bit(ABS_Y, input->absbit); |
| __set_bit(EV_SYN, input->evbit); |
| __set_bit(EV_KEY, input->evbit); |
| __set_bit(BTN_TOUCH, input->keybit); |
| |
| input_set_abs_params(input, ABS_X, ASR_TSC_MIN_XY_VAL, |
| ASR_TSC_MAX_XY_VAL, 0, 0); |
| input_set_abs_params(input, ABS_Y, ASR_TSC_MIN_XY_VAL, |
| ASR_TSC_MAX_XY_VAL, 0, 0); |
| |
| input_set_drvdata(input, tsc); |
| |
| error = request_irq(tsc->down_irq, asr_ts_down_isr, |
| IRQF_SHARED, pdev->name, tsc); |
| if (error) { |
| dev_err(&pdev->dev, "failed requesting down irq\n"); |
| goto err_put_clock; |
| } |
| |
| error = request_irq(tsc->rdy_irq, asr_ts_rdy_isr, |
| IRQF_SHARED, pdev->name, tsc); |
| if (error) { |
| dev_err(&pdev->dev, "failed requesting rdy irq\n"); |
| goto err_free_irq0; |
| } |
| |
| error = request_irq(tsc->up_irq, asr_ts_up_isr, |
| IRQF_SHARED, pdev->name, tsc); |
| if (error) { |
| dev_err(&pdev->dev, "failed requesting up irq\n"); |
| goto err_free_irq1; |
| } |
| |
| error = input_register_device(input); |
| if (error) { |
| dev_err(&pdev->dev, "failed registering input device\n"); |
| goto err_free_irq2; |
| } |
| |
| platform_set_drvdata(pdev, tsc); |
| device_init_wakeup(&pdev->dev, 1); |
| timer_setup(&tsc->move_timer, asr_ts_movement_detect, 0); |
| dev_info(&pdev->dev, "bringup ok.\n"); |
| return 0; |
| |
| err_free_irq2: |
| free_irq(tsc->up_irq, tsc); |
| err_free_irq1: |
| free_irq(tsc->rdy_irq, tsc); |
| err_free_irq0: |
| free_irq(tsc->down_irq, tsc); |
| err_put_clock: |
| clk_put(tsc->clk); |
| err_unmap: |
| iounmap(tsc->tsc_base); |
| err_release_mem: |
| release_mem_region(res->start, size); |
| err_free_mem: |
| input_free_device(input); |
| kfree(tsc); |
| |
| return error; |
| } |
| |
| static int asr_ts_remove(struct platform_device *pdev) |
| { |
| struct asr_tsc *tsc = platform_get_drvdata(pdev); |
| struct resource *res; |
| |
| free_irq(tsc->down_irq, tsc); |
| free_irq(tsc->rdy_irq, tsc); |
| free_irq(tsc->up_irq, tsc); |
| |
| input_unregister_device(tsc->dev); |
| |
| clk_put(tsc->clk); |
| |
| iounmap(tsc->tsc_base); |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| release_mem_region(res->start, resource_size(res)); |
| |
| kfree(tsc); |
| |
| return 0; |
| } |
| |
| #ifdef CONFIG_PM |
| static int asr_ts_suspend(struct device *dev) |
| { |
| struct asr_tsc *tsc = dev_get_drvdata(dev); |
| struct input_dev *input = tsc->dev; |
| |
| /* |
| * Suspend and resume can be called when the device hasn't been |
| * enabled. If there are no users that have the device open, then |
| * avoid calling the TSC stop and start functions as the TSC |
| * isn't yet clocked. |
| */ |
| mutex_lock(&input->mutex); |
| |
| if (input->users) { |
| if (device_may_wakeup(dev)) { |
| enable_irq_wake(tsc->down_irq); |
| enable_irq_wake(tsc->rdy_irq); |
| enable_irq_wake(tsc->up_irq); |
| } else |
| asr_stop_tsc(tsc); |
| } |
| |
| mutex_unlock(&input->mutex); |
| |
| return 0; |
| } |
| |
| static int asr_ts_resume(struct device *dev) |
| { |
| struct asr_tsc *tsc = dev_get_drvdata(dev); |
| struct input_dev *input = tsc->dev; |
| |
| mutex_lock(&input->mutex); |
| |
| if (input->users) { |
| if (device_may_wakeup(dev)) { |
| disable_irq_wake(tsc->down_irq); |
| disable_irq_wake(tsc->rdy_irq); |
| disable_irq_wake(tsc->up_irq); |
| } else |
| asr_setup_tsc(tsc); |
| } |
| |
| mutex_unlock(&input->mutex); |
| |
| return 0; |
| } |
| |
| static const struct dev_pm_ops asr_ts_pm_ops = { |
| .suspend = asr_ts_suspend, |
| .resume = asr_ts_resume, |
| }; |
| #define ASR_TS_PM_OPS (&asr_ts_pm_ops) |
| #else |
| #define ASR_TS_PM_OPS NULL |
| #endif |
| |
| #ifdef CONFIG_OF |
| static const struct of_device_id asr_tsc_of_match[] = { |
| { .compatible = "asr,tsc", }, |
| { }, |
| }; |
| MODULE_DEVICE_TABLE(of, asr_tsc_of_match); |
| #endif |
| |
| static struct platform_driver asr_ts_driver = { |
| .probe = asr_ts_probe, |
| .remove = asr_ts_remove, |
| .driver = { |
| .name = MOD_NAME, |
| .pm = ASR_TS_PM_OPS, |
| .of_match_table = of_match_ptr(asr_tsc_of_match), |
| }, |
| }; |
| module_platform_driver(asr_ts_driver); |
| |
| MODULE_AUTHOR("Mark Zhang <markzhang@asrmicro.com>"); |
| MODULE_DESCRIPTION("ASR TSC Driver"); |
| MODULE_LICENSE("GPL"); |
| MODULE_ALIAS("platform:asr_ts"); |
| |