| /* |
| * TI Touch Screen driver |
| * |
| * Copyright (C) 2011 Texas Instruments Incorporated - http://www.ti.com/ |
| * |
| * 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 version 2. |
| * |
| * This program is distributed "as is" WITHOUT ANY WARRANTY of any |
| * kind, whether express or implied; without even the implied warranty |
| * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| |
| #include <linux/init.h> |
| #include <linux/kernel.h> |
| #include <linux/err.h> |
| #include <linux/module.h> |
| #include <linux/input.h> |
| #include <linux/slab.h> |
| #include <linux/interrupt.h> |
| #include <linux/clk.h> |
| #include <linux/platform_device.h> |
| #include <linux/io.h> |
| #include <linux/input/ti_tscadc.h> |
| #include <linux/delay.h> |
| |
| #define REG_IRQEOI 0x020 |
| #define REG_RAWIRQSTATUS 0x024 |
| #define REG_IRQSTATUS 0x028 |
| #define REG_IRQENABLE 0x02C |
| #define REG_IRQWAKEUP 0x034 |
| #define REG_CTRL 0x040 |
| #define REG_ADCFSM 0x044 |
| #define REG_CLKDIV 0x04C |
| #define REG_SE 0x054 |
| #define REG_IDLECONFIG 0x058 |
| #define REG_CHARGECONFIG 0x05C |
| #define REG_CHARGEDELAY 0x060 |
| #define REG_STEPCONFIG(n) (0x64 + ((n - 1) * 8)) |
| #define REG_STEPDELAY(n) (0x68 + ((n - 1) * 8)) |
| #define REG_STEPCONFIG13 0x0C4 |
| #define REG_STEPDELAY13 0x0C8 |
| #define REG_STEPCONFIG14 0x0CC |
| #define REG_STEPDELAY14 0x0D0 |
| #define REG_FIFO0CNT 0xE4 |
| #define REG_FIFO1THR 0xF4 |
| #define REG_FIFO0 0x100 |
| #define REG_FIFO1 0x200 |
| |
| /* Register Bitfields */ |
| #define IRQWKUP_ENB BIT(0) |
| #define STPENB_STEPENB 0x7FFF |
| #define IRQENB_FIFO1THRES BIT(5) |
| #define IRQENB_PENUP BIT(9) |
| #define STEPCONFIG_MODE_HWSYNC 0x2 |
| #define STEPCONFIG_SAMPLES_AVG (1 << 4) |
| #define STEPCONFIG_XPP (1 << 5) |
| #define STEPCONFIG_XNN (1 << 6) |
| #define STEPCONFIG_YPP (1 << 7) |
| #define STEPCONFIG_YNN (1 << 8) |
| #define STEPCONFIG_XNP (1 << 9) |
| #define STEPCONFIG_YPN (1 << 10) |
| #define STEPCONFIG_INM (1 << 18) |
| #define STEPCONFIG_INP (1 << 20) |
| #define STEPCONFIG_INP_5 (1 << 21) |
| #define STEPCONFIG_FIFO1 (1 << 26) |
| #define STEPCONFIG_OPENDLY 0xff |
| #define STEPCONFIG_Z1 (3 << 19) |
| #define STEPIDLE_INP (1 << 22) |
| #define STEPCHARGE_RFP (1 << 12) |
| #define STEPCHARGE_INM (1 << 15) |
| #define STEPCHARGE_INP (1 << 19) |
| #define STEPCHARGE_RFM (1 << 23) |
| #define STEPCHARGE_DELAY 0x1 |
| #define CNTRLREG_TSCSSENB (1 << 0) |
| #define CNTRLREG_STEPID (1 << 1) |
| #define CNTRLREG_STEPCONFIGWRT (1 << 2) |
| #define CNTRLREG_4WIRE (1 << 5) |
| #define CNTRLREG_5WIRE (1 << 6) |
| #define CNTRLREG_8WIRE (3 << 5) |
| #define CNTRLREG_TSCENB (1 << 7) |
| #define ADCFSM_STEPID 0x10 |
| |
| #define SEQ_SETTLE 275 |
| #define ADC_CLK 3000000 |
| #define MAX_12BIT ((1 << 12) - 1) |
| #define TSCADC_DELTA_X 15 |
| #define TSCADC_DELTA_Y 15 |
| |
| struct tscadc { |
| struct input_dev *input; |
| struct clk *tsc_ick; |
| void __iomem *tsc_base; |
| unsigned int irq; |
| unsigned int wires; |
| unsigned int x_plate_resistance; |
| bool pen_down; |
| }; |
| |
| static unsigned int tscadc_readl(struct tscadc *ts, unsigned int reg) |
| { |
| return readl(ts->tsc_base + reg); |
| } |
| |
| static void tscadc_writel(struct tscadc *tsc, unsigned int reg, |
| unsigned int val) |
| { |
| writel(val, tsc->tsc_base + reg); |
| } |
| |
| static void tscadc_step_config(struct tscadc *ts_dev) |
| { |
| unsigned int config; |
| int i; |
| |
| /* Configure the Step registers */ |
| |
| config = STEPCONFIG_MODE_HWSYNC | |
| STEPCONFIG_SAMPLES_AVG | STEPCONFIG_XPP; |
| switch (ts_dev->wires) { |
| case 4: |
| config |= STEPCONFIG_INP | STEPCONFIG_XNN; |
| break; |
| case 5: |
| config |= STEPCONFIG_YNN | |
| STEPCONFIG_INP_5 | STEPCONFIG_XNN | |
| STEPCONFIG_YPP; |
| break; |
| case 8: |
| config |= STEPCONFIG_INP | STEPCONFIG_XNN; |
| break; |
| } |
| |
| for (i = 1; i < 7; i++) { |
| tscadc_writel(ts_dev, REG_STEPCONFIG(i), config); |
| tscadc_writel(ts_dev, REG_STEPDELAY(i), STEPCONFIG_OPENDLY); |
| } |
| |
| config = 0; |
| config = STEPCONFIG_MODE_HWSYNC | |
| STEPCONFIG_SAMPLES_AVG | STEPCONFIG_YNN | |
| STEPCONFIG_INM | STEPCONFIG_FIFO1; |
| switch (ts_dev->wires) { |
| case 4: |
| config |= STEPCONFIG_YPP; |
| break; |
| case 5: |
| config |= STEPCONFIG_XPP | STEPCONFIG_INP_5 | |
| STEPCONFIG_XNP | STEPCONFIG_YPN; |
| break; |
| case 8: |
| config |= STEPCONFIG_YPP; |
| break; |
| } |
| |
| for (i = 7; i < 13; i++) { |
| tscadc_writel(ts_dev, REG_STEPCONFIG(i), config); |
| tscadc_writel(ts_dev, REG_STEPDELAY(i), STEPCONFIG_OPENDLY); |
| } |
| |
| config = 0; |
| /* Charge step configuration */ |
| config = STEPCONFIG_XPP | STEPCONFIG_YNN | |
| STEPCHARGE_RFP | STEPCHARGE_RFM | |
| STEPCHARGE_INM | STEPCHARGE_INP; |
| |
| tscadc_writel(ts_dev, REG_CHARGECONFIG, config); |
| tscadc_writel(ts_dev, REG_CHARGEDELAY, STEPCHARGE_DELAY); |
| |
| config = 0; |
| /* Configure to calculate pressure */ |
| config = STEPCONFIG_MODE_HWSYNC | |
| STEPCONFIG_SAMPLES_AVG | STEPCONFIG_YPP | |
| STEPCONFIG_XNN | STEPCONFIG_INM; |
| tscadc_writel(ts_dev, REG_STEPCONFIG13, config); |
| tscadc_writel(ts_dev, REG_STEPDELAY13, STEPCONFIG_OPENDLY); |
| |
| config |= STEPCONFIG_Z1 | STEPCONFIG_FIFO1; |
| tscadc_writel(ts_dev, REG_STEPCONFIG14, config); |
| tscadc_writel(ts_dev, REG_STEPDELAY14, STEPCONFIG_OPENDLY); |
| |
| tscadc_writel(ts_dev, REG_SE, STPENB_STEPENB); |
| } |
| |
| static void tscadc_idle_config(struct tscadc *ts_config) |
| { |
| unsigned int idleconfig; |
| |
| idleconfig = STEPCONFIG_YNN | |
| STEPCONFIG_INM | |
| STEPCONFIG_YPN | STEPIDLE_INP; |
| tscadc_writel(ts_config, REG_IDLECONFIG, idleconfig); |
| } |
| |
| static void tscadc_read_coordinates(struct tscadc *ts_dev, |
| unsigned int *x, unsigned int *y) |
| { |
| unsigned int fifocount = tscadc_readl(ts_dev, REG_FIFO0CNT); |
| unsigned int prev_val_x = ~0, prev_val_y = ~0; |
| unsigned int prev_diff_x = ~0, prev_diff_y = ~0; |
| unsigned int read, diff; |
| unsigned int i; |
| |
| /* |
| * Delta filter is used to remove large variations in sampled |
| * values from ADC. The filter tries to predict where the next |
| * coordinate could be. This is done by taking a previous |
| * coordinate and subtracting it form current one. Further the |
| * algorithm compares the difference with that of a present value, |
| * if true the value is reported to the sub system. |
| */ |
| for (i = 0; i < fifocount - 1; i++) { |
| read = tscadc_readl(ts_dev, REG_FIFO0) & 0xfff; |
| diff = abs(read - prev_val_x); |
| if (diff < prev_diff_x) { |
| prev_diff_x = diff; |
| *x = read; |
| } |
| prev_val_x = read; |
| |
| read = tscadc_readl(ts_dev, REG_FIFO1) & 0xfff; |
| diff = abs(read - prev_val_y); |
| if (diff < prev_diff_y) { |
| prev_diff_y = diff; |
| *y = read; |
| } |
| prev_val_y = read; |
| } |
| } |
| |
| static irqreturn_t tscadc_irq(int irq, void *dev) |
| { |
| struct tscadc *ts_dev = dev; |
| struct input_dev *input_dev = ts_dev->input; |
| unsigned int status, irqclr = 0; |
| unsigned int x = 0, y = 0; |
| unsigned int z1, z2, z; |
| unsigned int fsm; |
| |
| status = tscadc_readl(ts_dev, REG_IRQSTATUS); |
| if (status & IRQENB_FIFO1THRES) { |
| tscadc_read_coordinates(ts_dev, &x, &y); |
| |
| z1 = tscadc_readl(ts_dev, REG_FIFO0) & 0xfff; |
| z2 = tscadc_readl(ts_dev, REG_FIFO1) & 0xfff; |
| |
| if (ts_dev->pen_down && z1 != 0 && z2 != 0) { |
| /* |
| * Calculate pressure using formula |
| * Resistance(touch) = x plate resistance * |
| * x postion/4096 * ((z2 / z1) - 1) |
| */ |
| z = z2 - z1; |
| z *= x; |
| z *= ts_dev->x_plate_resistance; |
| z /= z1; |
| z = (z + 2047) >> 12; |
| |
| if (z <= MAX_12BIT) { |
| input_report_abs(input_dev, ABS_X, x); |
| input_report_abs(input_dev, ABS_Y, y); |
| input_report_abs(input_dev, ABS_PRESSURE, z); |
| input_report_key(input_dev, BTN_TOUCH, 1); |
| input_sync(input_dev); |
| } |
| } |
| irqclr |= IRQENB_FIFO1THRES; |
| } |
| |
| /* |
| * Time for sequencer to settle, to read |
| * correct state of the sequencer. |
| */ |
| udelay(SEQ_SETTLE); |
| |
| status = tscadc_readl(ts_dev, REG_RAWIRQSTATUS); |
| if (status & IRQENB_PENUP) { |
| /* Pen up event */ |
| fsm = tscadc_readl(ts_dev, REG_ADCFSM); |
| if (fsm == ADCFSM_STEPID) { |
| ts_dev->pen_down = false; |
| input_report_key(input_dev, BTN_TOUCH, 0); |
| input_report_abs(input_dev, ABS_PRESSURE, 0); |
| input_sync(input_dev); |
| } else { |
| ts_dev->pen_down = true; |
| } |
| irqclr |= IRQENB_PENUP; |
| } |
| |
| tscadc_writel(ts_dev, REG_IRQSTATUS, irqclr); |
| /* check pending interrupts */ |
| tscadc_writel(ts_dev, REG_IRQEOI, 0x0); |
| |
| tscadc_writel(ts_dev, REG_SE, STPENB_STEPENB); |
| return IRQ_HANDLED; |
| } |
| |
| /* |
| * The functions for inserting/removing driver as a module. |
| */ |
| |
| static int __devinit tscadc_probe(struct platform_device *pdev) |
| { |
| const struct tsc_data *pdata = pdev->dev.platform_data; |
| struct resource *res; |
| struct tscadc *ts_dev; |
| struct input_dev *input_dev; |
| struct clk *clk; |
| int err; |
| int clk_value, ctrl, irq; |
| |
| if (!pdata) { |
| dev_err(&pdev->dev, "missing platform data.\n"); |
| return -EINVAL; |
| } |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| if (!res) { |
| dev_err(&pdev->dev, "no memory resource defined.\n"); |
| return -EINVAL; |
| } |
| |
| irq = platform_get_irq(pdev, 0); |
| if (irq < 0) { |
| dev_err(&pdev->dev, "no irq ID is specified.\n"); |
| return -EINVAL; |
| } |
| |
| /* Allocate memory for device */ |
| ts_dev = kzalloc(sizeof(struct tscadc), GFP_KERNEL); |
| input_dev = input_allocate_device(); |
| if (!ts_dev || !input_dev) { |
| dev_err(&pdev->dev, "failed to allocate memory.\n"); |
| err = -ENOMEM; |
| goto err_free_mem; |
| } |
| |
| ts_dev->input = input_dev; |
| ts_dev->irq = irq; |
| ts_dev->wires = pdata->wires; |
| ts_dev->x_plate_resistance = pdata->x_plate_resistance; |
| |
| res = request_mem_region(res->start, resource_size(res), pdev->name); |
| if (!res) { |
| dev_err(&pdev->dev, "failed to reserve registers.\n"); |
| err = -EBUSY; |
| goto err_free_mem; |
| } |
| |
| ts_dev->tsc_base = ioremap(res->start, resource_size(res)); |
| if (!ts_dev->tsc_base) { |
| dev_err(&pdev->dev, "failed to map registers.\n"); |
| err = -ENOMEM; |
| goto err_release_mem_region; |
| } |
| |
| err = request_irq(ts_dev->irq, tscadc_irq, |
| 0, pdev->dev.driver->name, ts_dev); |
| if (err) { |
| dev_err(&pdev->dev, "failed to allocate irq.\n"); |
| goto err_unmap_regs; |
| } |
| |
| ts_dev->tsc_ick = clk_get(&pdev->dev, "adc_tsc_ick"); |
| if (IS_ERR(ts_dev->tsc_ick)) { |
| dev_err(&pdev->dev, "failed to get TSC ick\n"); |
| goto err_free_irq; |
| } |
| clk_enable(ts_dev->tsc_ick); |
| |
| clk = clk_get(&pdev->dev, "adc_tsc_fck"); |
| if (IS_ERR(clk)) { |
| dev_err(&pdev->dev, "failed to get TSC fck\n"); |
| err = PTR_ERR(clk); |
| goto err_disable_clk; |
| } |
| |
| clk_value = clk_get_rate(clk) / ADC_CLK; |
| clk_put(clk); |
| |
| if (clk_value < 7) { |
| dev_err(&pdev->dev, "clock input less than min clock requirement\n"); |
| goto err_disable_clk; |
| } |
| /* CLKDIV needs to be configured to the value minus 1 */ |
| tscadc_writel(ts_dev, REG_CLKDIV, clk_value - 1); |
| |
| /* Enable wake-up of the SoC using touchscreen */ |
| tscadc_writel(ts_dev, REG_IRQWAKEUP, IRQWKUP_ENB); |
| |
| ctrl = CNTRLREG_STEPCONFIGWRT | |
| CNTRLREG_TSCENB | |
| CNTRLREG_STEPID; |
| switch (ts_dev->wires) { |
| case 4: |
| ctrl |= CNTRLREG_4WIRE; |
| break; |
| case 5: |
| ctrl |= CNTRLREG_5WIRE; |
| break; |
| case 8: |
| ctrl |= CNTRLREG_8WIRE; |
| break; |
| } |
| tscadc_writel(ts_dev, REG_CTRL, ctrl); |
| |
| tscadc_idle_config(ts_dev); |
| tscadc_writel(ts_dev, REG_IRQENABLE, IRQENB_FIFO1THRES); |
| tscadc_step_config(ts_dev); |
| tscadc_writel(ts_dev, REG_FIFO1THR, 6); |
| |
| ctrl |= CNTRLREG_TSCSSENB; |
| tscadc_writel(ts_dev, REG_CTRL, ctrl); |
| |
| input_dev->name = "ti-tsc-adc"; |
| input_dev->dev.parent = &pdev->dev; |
| |
| input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS); |
| input_dev->keybit[BIT_WORD(BTN_TOUCH)] = BIT_MASK(BTN_TOUCH); |
| |
| input_set_abs_params(input_dev, ABS_X, 0, MAX_12BIT, 0, 0); |
| input_set_abs_params(input_dev, ABS_Y, 0, MAX_12BIT, 0, 0); |
| input_set_abs_params(input_dev, ABS_PRESSURE, 0, MAX_12BIT, 0, 0); |
| |
| /* register to the input system */ |
| err = input_register_device(input_dev); |
| if (err) |
| goto err_disable_clk; |
| |
| platform_set_drvdata(pdev, ts_dev); |
| return 0; |
| |
| err_disable_clk: |
| clk_disable(ts_dev->tsc_ick); |
| clk_put(ts_dev->tsc_ick); |
| err_free_irq: |
| free_irq(ts_dev->irq, ts_dev); |
| err_unmap_regs: |
| iounmap(ts_dev->tsc_base); |
| err_release_mem_region: |
| release_mem_region(res->start, resource_size(res)); |
| err_free_mem: |
| input_free_device(input_dev); |
| kfree(ts_dev); |
| return err; |
| } |
| |
| static int __devexit tscadc_remove(struct platform_device *pdev) |
| { |
| struct tscadc *ts_dev = platform_get_drvdata(pdev); |
| struct resource *res; |
| |
| free_irq(ts_dev->irq, ts_dev); |
| |
| input_unregister_device(ts_dev->input); |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| iounmap(ts_dev->tsc_base); |
| release_mem_region(res->start, resource_size(res)); |
| |
| clk_disable(ts_dev->tsc_ick); |
| clk_put(ts_dev->tsc_ick); |
| |
| kfree(ts_dev); |
| |
| platform_set_drvdata(pdev, NULL); |
| return 0; |
| } |
| |
| static struct platform_driver ti_tsc_driver = { |
| .probe = tscadc_probe, |
| .remove = __devexit_p(tscadc_remove), |
| .driver = { |
| .name = "tsc", |
| .owner = THIS_MODULE, |
| }, |
| }; |
| module_platform_driver(ti_tsc_driver); |
| |
| MODULE_DESCRIPTION("TI touchscreen controller driver"); |
| MODULE_AUTHOR("Rachna Patil <rachna@ti.com>"); |
| MODULE_LICENSE("GPL"); |