一,编写触摸屏驱动程序的准备知识之一:输入子系统
1.输入子系统简单介绍
linux系统提供了input子系统,按键、触摸屏、鼠标等输入型设备都可以利用input接口函数来实现设备驱动。
2.输入子系统的组成
输入子系统由驱动层(drivers)、输入子系统核心层(input core)和事件处理层(Event Handler)三部分组成。
驱动层:将底层的硬件输入转化为统一的事件型式,向输入核心层汇报。
输入子系统核心层:为驱动层提供输入设备注册与操作接口,如input_register_device ;通知事件处理层对事件进行处理;在/proc下产生相应的设备信息。
事件处理层:将硬件设备上报的事件分发到用户空间和内核。
3.设备描述
在linux内核中,input设备用input_dev结构体描述,使用input子系统实现输入设备驱动编写的时候,驱动的核心工作是向系统报告按键、触摸屏、键盘、鼠标等输入事件,不再需要关心文件操作接口,因为input子系统已经完成了文件操作接口。驱动报告的事件经过输入子系统核心层和事件处理层最终到达用户空间。
4.input设备驱动的编写
4.1 设备注册/注销
注册输入设备的函数为:int input_register_device(struct input_dev*dev)
注销输入设备的函数为: void input_unregister_device(struct input_dev*dev)
4.2 事件支持
设备驱动通过set_bit()或者BIT()告诉input子系统它支持哪些事件,哪些按键。例如下面的例子:
set_bit(EV_KEY,dev->evbit) 也可以写成dev->evbit[0] = BIT(EV_KEY)
表示输入子系统支持按键事件。
struct input_dev 中有两个成员:evbit表示事件类型,Keybit表示按键类型。
输入子系统支持的事件类型:
EV_RST Reset EV_KEY 按键 EV_REL 相对坐标
EV_ABS 绝对坐标 EV_MSC 其它 EV_LED LED
EV_SND 声音 EV_REP Repeat EV_FF 力反馈
EV_SYN 同步事件
当事件类型为EV_KEY时,还需要指明按键类型:
BTN_LEFT 鼠标左键 BTN_0 数字0键
BTN_RIGHT 鼠标右键 BTN_1 数字1键
BTN_MIDDLE 鼠标中键 BTN_TOUCH 触摸屏点击
注:更多的事件类型请看linux源码中的input.h这个头文件。
4.3 报告事件
用于报告EV_KEY、EV_REL和EV_ABS事件的函数分别如下:
void input_report_key(struct input_dev *dev,unsigned int code,int value)
void input_report_rel(struct input_dev *dev,unsigned int code,int value)
void input_report_abs(struct input_dev *dev,unsigned int code,int value)
参数说明:
code 事件的代码,所有事件的代码都在linux源码中的input.h头文件中。
value 事件的值。如果事件的类型是EV_KEY,当按键按下时值为1,松开时值为0。
4.4 完成事件的报告
使用input_sync(struct input_dev *dev)来告诉input core 报告事件已经完成。
二,编写触摸屏驱动程序的准备知识之二:触摸屏驱动的原理
- 触摸屏的工作流程
1.1 设置触摸屏接口为等待中断模式,等待触摸屏被按下。
1.2 如果中断发生(TC中断,即触摸屏被按下),选择X、Y坐标转换模式(X、Y坐标分别转换模式或者X、Y坐标自动转换模式),启动A/D转换。
1.3 当A/D转换完成后,通过中断(AD中断,用来表示X、Y 坐标转换完成)获取X/Y坐标,ADCDAT0 bit[9:0]--X坐标,ADCDAT1 bit[9:0]--Y坐标。
1.4 设置触摸屏接口为等待中断模式,等待触摸笔离开触摸屏。
1.5 返回步骤1,等待下次触摸屏被按下。
- TQ2440触摸屏驱动程序整体流程图
三,TQ2440触摸屏驱动程序源码分析(由于我代码注释已经比较详细了,所以就不逐个函数进行分析了):
#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> #define S3C2410TSVERSION 0x0101 #define WAIT4INT(x) (((x)<<8) | S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | S3C2410_ADCTSC_XY_PST(3)) #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; extern struct semaphore ADC_LOCK; //定义信号量ADC_LOCK static int OwnADC = 0; static void __iomem *base_addr; //用来保存经过映射后的虚拟地址 /*touch_timer_fire函数所做的工作如下: * 读出触摸屏被按下还是被抬起的状态 * 如果触摸屏被按下并且A/D转换完成就报告事件和数据 * 如果触摸屏被按下但是A/D转换还没开始就启动A/D转换 * 如果触摸屏被抬起首先报告事件然后重新进入等待中断模式并释放触摸屏占用的ADC资源 */ static void touch_timer_fire(unsigned long data) { unsigned long data0; unsigned long data1; int updown; //读取A/D转换后的值 data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); //S3C2410_ADCDAT0_UPDOWN =1000000000000000,updown为1时表示触摸屏被按下,updown为0时表示触摸屏没有被按下 updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); //如果触摸屏被按下,并且A/D转换已完成,就报告事件和数据 if (updown) { if (count != 0) { long tmp; tmp = xp; xp = yp; yp = tmp; //由于A/D转换时对X和Y的坐标采样了4次,所以这里X和Y的坐标就取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); } //如果触摸屏被按下,但是A/D转换还没开始,就启动A/D转换 xp = 0; yp = 0; count = 0; //设置触摸屏为自动X/Y轴坐标转换模式 iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC); //启动A/D转换 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); //使触摸屏重新进入等待按下的中断模式 ADCTSC = 011010011 iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); if (OwnADC) //触摸屏被抬起,不应该再占用ADC资源,应释放之 { OwnADC = 0; up(&ADC_LOCK); //释放触摸屏所占用的ADC资源 } } } //定义并初始化定时器touch_timer static struct timer_list touch_timer = TIMER_INITIALIZER(touch_timer_fire, 0, 0); /* TC中断,当触摸屏被按下或松开时执行,此函数主要做的工作如下: * 获得ADC资源,因为在ADC驱动里面也使用了ADC资源 * 判断触摸屏是被按下还是被抬起,并对这两种状态做出相应的处理 */ static irqreturn_t stylus_updown(int irq, void *dev_id) { unsigned long data0; unsigned long data1; int updown;//触摸屏按下或抬起的标示 //尝试获得信号量ADC_LOCK,若down_trylock返回0表示信号量获得成功,触摸屏可以使用ADC资源,否则不能使用ADC资源 if (down_trylock(&ADC_LOCK) == 0) { OwnADC = 1; //表示触摸屏的ADC资源可用 //读取A/D转换后的值 data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); //S3C2410_ADCDAT0_UPDOWN =1000000000000000,updown为1时表示触摸屏被按下,updown为0时表示触摸屏没有被按下 updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); if (updown) //如果触摸屏被按下,那么调用touch_timer_fire函数启动A/D转换 { touch_timer_fire(0); } else //如果触摸屏被松开,那么就释放触摸屏所占用的ADC资源 { OwnADC = 0; up(&ADC_LOCK);//释放ADC_LOCK信号量 } } return IRQ_HANDLED; } /* AD中断,当X,Y坐标转换完成时执行 * 负责取得X和Y的坐标值 */ static irqreturn_t stylus_action(int irq, void *dev_id) { unsigned long data0; unsigned long data1; if (OwnADC) //标示触摸屏资源可用 { //读取ADCDAT0寄存器(读出X轴坐标转换数据值) data0 = ioread32(base_addr+S3C2410_ADCDAT0); //读取ADCDAT1寄存器(读出Y轴坐标转换数据值) data1 = ioread32(base_addr+S3C2410_ADCDAT1); //取得X坐标的值,即读取ADCDAT0寄存器0-9位的值,S3C2410_ADCDAT0_XPDATA_MASK = 0000001111111111 xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK; //取得Y坐标的值,即读取ADCDAT1寄存器0-9位的值,S3C2410_ADCDAT1_YPDATA_MASK = 0000001111111111 yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK; //记录这一次A/D转换的次数 count++; if (count < (1<<2)) //如果转换次数小于4次 { //设置触摸屏为自动X/Y轴坐标转换模式 iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC); //重新启动A/D转换 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; /*设备初始化函数,该函数主要做的工作如下: * 获得ADC时钟以及使能ADC时钟 * 映射ADC的IO内存的地址 * 设置ADCCON寄存器使能预分频,设置ADCDLY寄存器(采样的延时值) * 设置ADCTSC寄存器进入等待中断模式(等待触摸屏被按下) * 设置输入设备支持的事件 * 申请ADC,TC中断 * 注册输入设备 */ static int __init tq2440ts_init(void) { int ret1,ret2; struct input_dev *input_dev; //定义输入设备 adc_clock = clk_get(NULL, "adc"); //获得外设adc的时钟 if (!adc_clock) //如果adc_clock为空 { printk(KERN_ERR "failed to get adc clock source\n"); return -ENOENT; } clk_enable(adc_clock); //使能adc时钟 //映射ADC的I/O内存地址,S3C2410_PA_ADC是ADC控制器的基地址(0x58000000),0x20是虚拟地址长度大小 base_addr=ioremap(S3C2410_PA_ADC,0x20); if (base_addr == NULL) //如果映射ADC的I/O内存地址失败 { printk(KERN_ERR "Failed to remap register block\n"); return -ENOMEM; } //设置ADCCON寄存器使能预分频,预分频系数选择255,ADCCON =0111111111000000 iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(0xFF),base_addr+S3C2410_ADCCON); //ADCDLY= 1111111111111111,采样的延迟值 iowrite32(0xffff, base_addr+S3C2410_ADCDLY); //进入等待按下的中断模式 ADCTSC = 011010011 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; } //初始化输入设备 dev = input_dev; //支持同步事件,按键事件,绝对位移事件 dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); //set_bit(EV_SYN,dev->evbit); //set_bit(EV_KEY,dev->evbit); //set_bit(EV_ABS,dev->evbit); //支持的按键类型为触摸屏点击 dev->keybit[BITS_TO_LONGS(BTN_TOUCH)] = BIT(BTN_TOUCH); //set_bit(BTN_TOUCH,dev->keybit); //设置触摸屏的x坐标,y坐标,压力 input_set_abs_params(dev, ABS_X, 0, 0x3FF, 0, 0); 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中断,X,Y坐标的AD转换完成时即产生此中断 ret1 =request_irq(IRQ_ADC, stylus_action, IRQF_SHARED|IRQF_SAMPLE_RANDOM, tq2440ts_name, dev); if (ret1) //如果申请AD中断失败 { printk(KERN_ERR "tq2440_ts.c: Could not allocate ts IRQ_ADC !\n"); iounmap(base_addr); //解除ADC控制器的内存地址映射 return -EIO; } //申请TC中断,触摸屏被按下或弹起时即产生此中断 ret2 =request_irq(IRQ_TC, stylus_updown, IRQF_SAMPLE_RANDOM, tq2440ts_name, dev); if (ret2) //如果申请TC中断失败 { printk(KERN_ERR "tq2440_ts.c: Could not allocate ts IRQ_ADC !\n"); iounmap(base_addr);//解除ADC控制器的内存地址映射 return -EIO; } printk(KERN_INFO "%s successfully loaded\n", tq2440ts_name); input_register_device(dev);//注册输入设备 return 0; } /*设备退出函数,该函数主要做的工作如下: * 关闭并释放ADC和TC中断 * 关闭并注销ADC时钟 * 注销输入设备 * 解除ADC控制器的内存空间的映射 */ static void __exit tq2440ts_exit(void) { disable_irq(IRQ_ADC);//关闭ADC中断 disable_irq(IRQ_TC); //关闭TC中断 free_irq(IRQ_TC,dev); //释放TC中断 free_irq(IRQ_ADC,dev); //释放ADC中断 if (adc_clock) { clk_disable(adc_clock); //关闭时钟ADC clk_put(adc_clock); //注销ADC时钟 adc_clock = NULL; } input_unregister_device(dev);//注销输入设备 iounmap(base_addr); //解除ADC控制器的内存映射 } module_init(tq2440ts_init); module_exit(tq2440ts_exit); MODULE_LICENSE("GPL");
四,触摸屏驱动的移植
关于触摸屏驱动程序的移植请看我前面写的一篇文章《TQ2440触摸屏驱动程序的移植》 ,至此有关TQ2440触摸屏驱动程序的编写就告一段落了。