一、输入子系统情景回忆ING......
在Linux中,输入子系统是由输入子系统设备驱动层、输入子系统核心层(Input Core)和输入子系统事件处理层(Event Handler)组成。其中设备驱动层提供对硬件各寄存器的读写访问和将底层硬件对用户输入访问的响应转换为标准的输入事件,再通过核心层提交给事件处理层;而核心层对下提供了设备驱动层的编程接口,对上又提供了事件处理层的编程接口;而事件处理层就为我们用户空间的应用程序提供了统一访问设备的接口和驱动层提交来的事件处理。所以这使得我们输入设备的驱动部分不在用关心对设备文件的操作,而是要关心对各硬件寄存器的操作和提交的输入事件。下面用图形来描述一下这三者的关系吧!
二、S3C2440触摸屏接口预热ING......
S3C2440提供的触摸屏接口有4种处理模式,分别是:正常转换模式、单独的X/Y位置转换模式、自动X/Y位置转换模式
和等待中断模式,对于在每种模式下工作的要求,请详细查看数据手册的描述。本驱动实例将采用自动X/Y位置转换
模式和等待中断模式。
三、tq2440_ts.c源码分析
1.入口函数,为了方便分析,清晰架构,这里省去了返回值的判断,最详细的还请参考后面的源码。
static int __init tq2440ts_init(void) { struct input_dev *input_dev; /* 获取ADC时钟,使能时钟(CLKCON[15]) * ADC(&Touch Screen) [15], Control PCLK into ADC block. */ adc_clock = clk_get(NULL, "adc"); clk_enable(adc_clock); /* ADCCON寄存器地址:0x58000000 */ base_addr=ioremap(S3C2410_PA_ADC,0x20); /* 使能预分频,预分频系数PRSCVL为0xff */ iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(0xFF),base_addr+S3C2410_ADCCON); /* ADCDLY = 0xffff */ iowrite32(0xffff, base_addr+S3C2410_ADCDLY); /* 进入等待按下中断模式 */ iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); /* 分配一个input_dev结构体 */ input_dev = input_allocate_device(); /* 初始化输入设备,即input_dev成员 */ dev = input_dev; /* 支持同步事件、按键事件、绝对位移事件 */ dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); /* 支持按键类中的触摸屏点击 */ dev->keybit[BITS_TO_LONGS(BTN_TOUCH)] = BIT(BTN_TOUCH); /* 设置触摸屏的X坐标、Y坐标、压力 */ input_set_abs_params(dev, ABS_X, 0, 0x3FF, 0, 0); /* Resolution: 10-bit,0x3ff即10位 */ input_set_abs_params(dev, ABS_Y, 0, 0x3FF, 0, 0); input_set_abs_params(dev, ABS_PRESSURE, 0, 1, 0, 0); /* 下面这些信息在驱动挂载后在/proc/bus/input/devices中看到 */ dev->name = tq2440ts_name; dev->id.bustype = BUS_RS232; dev->id.vendor = 0xDEAD; dev->id.product = 0xBEEF; dev->id.version = S3C2410TSVERSION; /* 分别申请ADC、TC中断,在ADC中断里使用了IRQF_SHARED * 共享中断标志,因为在ADC驱动里也使用了ADC中断 */ request_irq(IRQ_ADC, stylus_action, IRQF_SHARED|IRQF_SAMPLE_RANDOM, tq2440ts_name, dev) request_irq(IRQ_TC, stylus_updown, IRQF_SAMPLE_RANDOM, tq2440ts_name, dev) printk(KERN_INFO "%s successfully loaded\n", tq2440ts_name); /* 将触摸屏输入设备注册到输入子系统 */ input_register_device(dev); return 0; }
驱动加载函数tq2440ts_init主要做了以下一些事情:获取ADC时钟,使能ADC时钟,映射ADC的IO地址,使能预分频、设ADCDLY寄存器,进入等待按下中断模式,分配一个input_dev结构体,初始化input_dev结构体成员,如:支持哪类事件、支持这类事件的哪些事件,申请ADC、TC中断,最后注册一个触摸屏输入设备。万事具备,只欠东风。加载函数准备好一切条件后,就当你触摸触摸屏了,当触摸屏被触摸后,即被按下后,会进入触摸屏中断处理函数stylus_updown
/* 触摸屏中断服务程序,当触摸屏按下或抬起时触发执行 */ static irqreturn_t stylus_updown(int irq, void *dev_id) { unsigned long data0; unsigned long data1; /* 用于判断触摸屏是按下还是抬起 */ int updown; /* ADC资源可以获取,即上锁 */ if (down_trylock(&ADC_LOCK) == 0) { /* 标识触摸屏资源可用 */ OwnADC = 1; /* 读取AD转换后的值,注意这次重点是读出状态 */ data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); if (updown) { /* ADCDAT0[15] = 0,则updown = 1,此时表示按下状态 * 那么调用touch_timer_fire函数启动ADC转换 */ touch_timer_fire(0); } else { /* 如果是抬起状态,就结束这次的操作,并释放ADC资源的占用,即解锁 */ OwnADC = 0; up(&ADC_LOCK); } } return IRQ_HANDLED; }stylus_updown函数首先获得ADC资源,因为在ADC驱动里也有可能使用了ADC资源,然后获得触摸屏状态,判断触摸屏是被按下还是被抬起,如果是被按下,那么调用touch_timer_fire函数启动ADC转换;如果是抬起状态,就结束这次的操作,并释放ADC资源的占用。
touch_timer_fire函数分析
static void touch_timer_fire(unsigned long data) { unsigned long data0; unsigned long data1; int updown; /* 读取AD转换后的值,注意这次重点是读出状态 */ data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); if (updown) { /* 如果是按下状态,并且ADC已经转换,就报告事件和数据 */ if (count != 0) { long tmp; tmp = xp; xp = yp; yp = tmp; /* stylus_action函数是4次采样,所以要除以4 */ xp >>= 2; yp >>= 2; /* 上报X/Y坐标数据 */ input_report_abs(dev, ABS_X, xp); input_report_abs(dev, ABS_Y, yp); /* 报告按键事件,键值为1表示触摸点被按下 */ input_report_key(dev, BTN_TOUCH, 1); /* 报告触摸屏状态,键值为1表示触摸屏被按下 */ input_report_abs(dev, ABS_PRESSURE, 1); /* 报告完后,上报同步事件 */ input_sync(dev); } /* 如果是按下状态,但ADC还没开始转换,就启动ADC转换 */ xp = 0; yp = 0; count = 0; /* 设置触摸屏模式为自动X/Y位置转换模式 */ iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC); /* 启动ADC转换,转换完成后进入stylus_action(ADC中断处理函数) */ iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON); } else { /* 如果是抬起状态 */ count = 0; /* 报告按键事件,键值为0表示触摸点被释放 */ input_report_key(dev, BTN_TOUCH, 0); /* 报告触摸屏绝对位移事件,键值为0表示触摸屏没有被按下 */ input_report_abs(dev, ABS_PRESSURE, 0); /* 报告完后,上报同步事件 */ input_sync(dev); /* 将触摸屏重设置为等待按下中断模式 */ iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); /* 如果触摸屏抬起,意味着这一次的操作结束,所以应该释放ADC资源的占用 */ if (OwnADC) { OwnADC = 0; up(&ADC_LOCK); } } }touch_timer_fire函数首先读出触摸屏状态,如果是按下状态,并且ADC已经转换,就报告事件和数据;如果是按下状态,但ADC还没开始转换,就启动ADC转换;如果是抬起状态,报告事件后,将触摸屏重设置为等待按下中断模式;如果触摸屏抬起,意味着这一次的操作结束,所以应该释放ADC资源的占用。
当ADC转换完成后触发ADC中断,就会进入ADC中断处理函数stylus_action
/* ADC中断处理程序,AD转换完成后触发执行 */ static irqreturn_t stylus_action(int irq, void *dev_id) { unsigned long data0; unsigned long data1; /* 标识触摸屏资源可用 */ if (OwnADC) { /* 读取AD转换后的值,注意这次重点读X/Y坐标数据 */ data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); /* 取出bit[9:0],即X/Y坐标数据 */ xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK; yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK; /* 记录这一次AD转换次数 */ count++; if (count < (1<<2)) { /* 如果转换次数小于4,则重启动ADC转换 */ iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC); iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON); } else { /* 否则,启动1个时间滴答的定时器,这就会去执行 * touch_timer_fire定时器超时函数的上报事件和数据 */ mod_timer(&touch_timer, jiffies+1); /* 停止ADC转换,防止屏幕抖动 */ iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC); } } return IRQ_HANDLED; }在stylus_action函数里,首先读取ADC转换后的数据,判断是否连续转换够4次,如果没有则重新启动ADC转换;否则,启动1个时间滴答的定时器,这就会去执行touch_timer_fire定时器超时函数的上报事件和数据。
在本驱动的定时器的定义和初始化与以往不一样,这里使用了简易的方法
/* 定义并初始化了一个touch_timer定时器,超时处理函数为touch_timer_fire */ static struct timer_list touch_timer = TIMER_INITIALIZER(touch_timer_fire, 0, 0);在这里总结一下触摸的工作流程:
(1)如果触摸屏感觉到触摸,则触发触摸屏中断即进入stylus_updown,获取ADC_LOCK后判断触摸屏状态为按下,
则调用touch_timer_fire启动ADC转换;
(2)当ADC转换启动后,触发ADC中断即进入stylus_action,如果这一次转换的次数小于4,则重新启动ADC进行转换,
如果4次完毕后,启动1个时间滴答的定时器,停止ADC转换,也就是说在这个时间滴答内,ADC转换是停止的;
(3)这里为什么要在1个时间滴答到来之前停止ADC的转换呢?这是为了防止屏幕抖动。
(4)如果1个时间滴答到来则进入定时器服务程序touch_timer_fire,判断触摸屏仍然处于按下状态则上报事件和
转换的数据,并重启ADC转换,重复第(2)步;
(5)如果触摸抬起了,则上报释放事件,并将触摸屏重新设置为等待中断状态。
触摸屏驱动参考源码
#include <linux/errno.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/input.h> #include <linux/init.h> #include <linux/serio.h> #include <linux/delay.h> #include <linux/platform_device.h> #include <linux/clk.h> #include <asm/io.h> #include <asm/irq.h> #include <plat/regs-adc.h> #include <mach/regs-gpio.h> /* For ts.dev.id.version */ #define S3C2410TSVERSION 0x0101 /* x = 0:等待按下中断模式,即ADCTSC = 0xd3 * x = 1:等待松开中断模式, 即ADCTSC = 0x1d3 */ #define WAIT4INT(x) (((x)<<8) | S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | S3C2410_ADCTSC_XY_PST(3)) /* 自动X/Y位置转换模式 */ #define AUTOPST (S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | S3C2410_ADCTSC_AUTO_PST | S3C2410_ADCTSC_XY_PST(0)) static char *tq2440ts_name = "TQ2440 TouchScreen"; static struct input_dev *dev; static long xp; static long yp; static int count; /* 外部信号量ADC_LOCK,在ADC驱动中有定义 */ extern struct semaphore ADC_LOCK; static int OwnADC = 0; static void __iomem *base_addr; /* static inline void tq2440_ts_connect(void) { s3c2410_gpio_cfgpin(S3C2410_GPG12, S3C2410_GPG12_XMON); s3c2410_gpio_cfgpin(S3C2410_GPG13, S3C2410_GPG13_nXPON); s3c2410_gpio_cfgpin(S3C2410_GPG14, S3C2410_GPG14_YMON); s3c2410_gpio_cfgpin(S3C2410_GPG15, S3C2410_GPG15_nYPON); } */ static void touch_timer_fire(unsigned long data) { unsigned long data0; unsigned long data1; int updown; /* 读取AD转换后的值,注意这次重点是读出状态 */ data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); if (updown) { /* 如果是按下状态,并且ADC已经转换,就报告事件和数据 */ if (count != 0) { long tmp; tmp = xp; xp = yp; yp = tmp; /* stylus_action函数是4次采样,所以要除以4 */ xp >>= 2; yp >>= 2; /* 上报X/Y坐标数据 */ input_report_abs(dev, ABS_X, xp); input_report_abs(dev, ABS_Y, yp); /* 报告按键事件,键值为1表示触摸点被按下 */ input_report_key(dev, BTN_TOUCH, 1); /* 报告触摸屏状态,键值为1表示触摸屏被按下 */ input_report_abs(dev, ABS_PRESSURE, 1); /* 报告完后,上报同步事件 */ input_sync(dev); } /* 如果是按下状态,但ADC还没开始转换,就启动ADC转换 */ xp = 0; yp = 0; count = 0; /* 设置触摸屏模式为自动X/Y位置转换模式 */ iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC); /* 启动ADC转换,转换完成后进入stylus_action(ADC中断处理函数) */ iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON); } else { /* 如果是抬起状态 */ count = 0; /* 报告按键事件,键值为0表示触摸点被释放 */ input_report_key(dev, BTN_TOUCH, 0); /* 报告触摸屏绝对位移事件,键值为0表示触摸屏没有被按下 */ input_report_abs(dev, ABS_PRESSURE, 0); /* 报告完后,上报同步事件 */ input_sync(dev); /* 将触摸屏重设置为等待按下中断模式 */ iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); /* 如果触摸屏抬起,意味着这一次的操作结束,所以应该释放ADC资源的占用 */ if (OwnADC) { OwnADC = 0; up(&ADC_LOCK); } } } /* 定义并初始化了一个touch_timer定时器,超时处理函数为touch_timer_fire */ static struct timer_list touch_timer = TIMER_INITIALIZER(touch_timer_fire, 0, 0); /* 触摸屏中断服务程序,当触摸屏按下或抬起时触发执行 */ static irqreturn_t stylus_updown(int irq, void *dev_id) { unsigned long data0; unsigned long data1; /* 用于判断触摸屏是按下还是抬起 */ int updown; /* ADC资源可以获取,即上锁 */ if (down_trylock(&ADC_LOCK) == 0) { /* 标识触摸屏资源可用 */ OwnADC = 1; /* 读取AD转换后的值,注意这次重点是读出状态 */ data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); if (updown) { /* ADCDAT0[15] = 0,则updown = 1,此时表示按下状态 * 那么调用touch_timer_fire函数启动ADC转换 */ touch_timer_fire(0); } else { /* 如果是抬起状态,就结束这次的操作,并释放ADC资源的占用,即解锁 */ OwnADC = 0; up(&ADC_LOCK); } } return IRQ_HANDLED; } /* ADC中断处理程序,AD转换完成后触发执行 */ static irqreturn_t stylus_action(int irq, void *dev_id) { unsigned long data0; unsigned long data1; /* 标识触摸屏资源可用 */ if (OwnADC) { /* 读取AD转换后的值,注意这次重点读X/Y坐标数据 */ data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); /* 取出bit[9:0],即X/Y坐标数据 */ xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK; yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK; /* 记录这一次AD转换次数 */ count++; if (count < (1<<2)) { /* 如果转换次数小于4,则重启动ADC转换 */ iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC); iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON); } else { /* 否则,启动1个时间滴答的定时器,这就会去执行 * touch_timer_fire定时器超时函数的上报事件和数据 */ mod_timer(&touch_timer, jiffies+1); /* 停止ADC转换,防止屏幕抖动 */ iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC); } } return IRQ_HANDLED; } static struct clk *adc_clock; static int __init tq2440ts_init(void) { struct input_dev *input_dev; /* 获取ADC时钟,使能时钟(CLKCON[15]) * ADC(&Touch Screen) [15], Control PCLK into ADC block. */ adc_clock = clk_get(NULL, "adc"); if (!adc_clock) { printk(KERN_ERR "failed to get adc clock source\n"); return -ENOENT; } clk_enable(adc_clock); /* ADCCON寄存器地址:0x58000000 */ base_addr=ioremap(S3C2410_PA_ADC,0x20); if (base_addr == NULL) { printk(KERN_ERR "Failed to remap register block\n"); return -ENOMEM; } /* Configure GPIOs */ // tq2440_ts_connect(); /* 使能预分频,预分频系数PRSCVL为0xff */ iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(0xFF),base_addr+S3C2410_ADCCON); /* ADCDLY = 0xffff */ iowrite32(0xffff, base_addr+S3C2410_ADCDLY); /* 进入等待按下中断模式 */ iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); /* 分配一个input_dev结构体 */ input_dev = input_allocate_device(); if (!input_dev) { printk(KERN_ERR "Unable to allocate the input device !!\n"); return -ENOMEM; } /* 初始化输入设备,即input_dev成员 */ dev = input_dev; /* 支持同步事件、按键事件、绝对位移事件 */ dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); /* 支持按键类中的触摸屏点击 */ dev->keybit[BITS_TO_LONGS(BTN_TOUCH)] = BIT(BTN_TOUCH); /* 设置触摸屏的X坐标、Y坐标、压力 */ input_set_abs_params(dev, ABS_X, 0, 0x3FF, 0, 0); /* Resolution: 10-bit,0x3ff即10位 */ input_set_abs_params(dev, ABS_Y, 0, 0x3FF, 0, 0); input_set_abs_params(dev, ABS_PRESSURE, 0, 1, 0, 0); /* 下面这些信息在驱动挂载后在/proc/bus/input/devices中看到 */ dev->name = tq2440ts_name; dev->id.bustype = BUS_RS232; dev->id.vendor = 0xDEAD; dev->id.product = 0xBEEF; dev->id.version = S3C2410TSVERSION; /* 分别申请ADC、TC中断,在ADC中断里使用了IRQF_SHARED * 共享中断标志,因为在ADC驱动里也使用了ADC中断 */ if (request_irq(IRQ_ADC, stylus_action, IRQF_SHARED|IRQF_SAMPLE_RANDOM, tq2440ts_name, dev)) { printk(KERN_ERR "tq2440_ts.c: Could not allocate ts IRQ_ADC !\n"); iounmap(base_addr); return -EIO; } if (request_irq(IRQ_TC, stylus_updown, IRQF_SAMPLE_RANDOM, tq2440ts_name, dev)) { printk(KERN_ERR "tq2440_ts.c: Could not allocate ts IRQ_ADC !\n"); iounmap(base_addr); return -EIO; } printk(KERN_INFO "%s successfully loaded\n", tq2440ts_name); /* 将触摸屏输入设备注册到输入子系统 */ input_register_device(dev); return 0; } static void __exit tq2440ts_exit(void) { disable_irq(IRQ_ADC); disable_irq(IRQ_TC); free_irq(IRQ_TC,dev); free_irq(IRQ_ADC,dev); if (adc_clock) { clk_disable(adc_clock); clk_put(adc_clock); adc_clock = NULL; } input_unregister_device(dev); iounmap(base_addr); } module_init(tq2440ts_init); module_exit(tq2440ts_exit);
测试,使用tslib库来测试,关于如何测试请参考韦东山视频的“第16课第3节 触摸屏驱动程序之使用TSLIB测试_P”,在这里再说显得有点多余。
2014-01-20补充
[WJ2440]# cat proc/bus/input/devices I: Bus=0013 Vendor=dead Product=beef Version=0101 N: Name="TQ2440 TouchScreen" P: Phys= S: Sysfs=/devices/virtual/input/input0 U: Uniq= H: Handlers=event0 B: EV=b B: KEY=0 B: ABS=1000003与源码里的信息是一致的
dev->name = tq2440ts_name; dev->id.bustype = BUS_RS232; dev->id.vendor = 0xDEAD; dev->id.product = 0xBEEF; dev->id.version = S3C2410TSVERSION;