嵌入式Linux ———触摸屏驱动开发

2019-07-12 15:38发布

声明:本文章是看完韦东山老师的触摸屏驱动视频所写的关于触摸屏的驱动,因此如果有相关内容与其他网友相同,敬请原谅。同时我还是想说本文只是总结自己的学习所得,同时也将自己所学到的知识写下来,所以如果这篇文章对你有帮助,那是我的荣幸。 本文主要介绍韦东山老师视频中所讲的触摸屏驱动,同时还会将s3c2410_ts.c这个驱动程序讲一下。虽然老师讲的驱动是对这个驱动程序的精简和整理,但内核的程序我们还是要自己看懂比较好,这样以后自己写其他内核驱动程序时会很有帮助。 在这里我介绍一篇不错的讲解s3c2410_ts.c的文章:linux嵌入式系统开发之触摸屏---驱动篇(下/源码分析) 而我在自己的上一篇文章中已经翻译了s3c2440数据手册中的ADC与触摸屏这章:S3C2440-ADC触摸屏,所以有硬件方面不清楚的可以看这一章了解触摸屏寄存器的工作模式以及如何设置。 下面我们言归正传开始讲触摸屏的驱动程序: 触摸屏驱动程序是输入子系统中的一类,所以我们按输入子系统的方法写驱动程序,所以步骤如下:  
  1. 分配一个input_dev 结构体
  2. 设置这个结构体用于触摸屏驱动
  3. 注册这个结构体
  4. 做硬件相关的设置
而上面步骤1,2,3所对应的程序为: /* 1. 分配一个input_dev结构体 */ s3c_ts_dev = input_allocate_device(); /* 1.1 初始化定时器 */ init_timer(&ts_timer); ts_timer.function = s3c_ts_timer_func; //这个为定时器的处理函数 add_timer(&ts_timer); /* 2. 设置这个结构体 */ /* 2.1 设置他产生哪类事件 */ set_bit(EV_KEY, s3c_ts_dev->evbit); //按键类事件 set_bit(EV_ABS,s3c_ts_dev->evbit); //绝对位移事件 /* 2.2 设置其产生这类事件中的那个具体事件 */ set_bit(BTN_TOUCH,s3c_ts_dev->keybit); //按键类事件中的触摸屏按键,此处将触摸屏抽象为按键,笔按下时为 //键被按下,笔抬起时为键被抬起 /     input_set_abs_params(s3c_ts_dev,ABS_X,0,0x3ff,0,0); //绝对位移中的x方向,此处的0x3ff是因为触摸屏为10位ADC input_set_abs_params(s3c_ts_dev,ABS_Y,0,0x3ff,0,0); //绝对位移中的y方向,此处的0x3ff是因为触摸屏为10位ADC input_set_abs_params(s3c_ts_dev,ABS_PRESSURE,0,1,0,0);//绝对位移中的压力方向,此处只有抬起和按下两种情况 /* 3. 注册这个结构体 */ input_register_device(s3c_ts_dev);   现在已将input_dev结构体设置好了,接下来就是对触摸屏硬件的设置了,而在设置硬件之前要先向大家介绍一下触摸屏的使用过程:
按下触摸屏产生中断
在触摸屏中断处理程序中,将通过ADC采集的电压值转化为XY方向的坐标值,然后启动ADC。
AD转化结束产生ADC中断。
在ADC中断处理函数中上报事件,并启动定时器(用于处理长按和滑动事件)
定时器时间到,跳到步骤2继续往下执行
直到松开触摸屏
下面是硬件寄存器和中断的设置,为上面触摸屏的操作做准备,程序为:   /* 4. 硬件相关 */ /* 4.1 使能时钟(CLKCON[15])使能ADC模块 */ clk = clk_get(NULL,"adc"); clk_enable(clk); /* 4.2 设置S3c的ADC寄存器 */ s3c_ts_regs = ioremap(0x58000000, sizeof(struct s3c_ts_reg)); /* *ADCCON: *bit[14] PRSCEN :A/D converter 预分频使能 * 1 = Enable *bit[13:6] PRSCVL :预分频系数 * 49: PLCK/(PRSCVL+1) = 50MHZ/(49+1) = 1MHZ *bit[0] ENABLE_START :ADC开始转化使能 * 此处先设为0 */ s3c_ts_regs->ADCCON = (1<<14) | (49<<6) | (0<<0); /* ADCDLY *bit[15:0] DELAY :ADC转化开始延迟值 * =0xffff; */ s3c_ts_regs->ADCDLY = 0xffff; /* 4.3 注册中断,按下触摸屏产生中断 */ request_irq(IRQ_TC,pen_down_up_irq,IRQF_SAMPLE_RANDOM,"ts_pen",NULL); /* 4.5 ADC转化结束,产生ADC中断 */ request_irq(IRQ_ADC,adc_irq,IRQF_SAMPLE_RANDOM,"ts_adc",NULL); enter_wait_pen_down_mode();   上面程序中的s3c_ts_regs是触摸屏寄存器的一个总的集合,并将其放入一个结构体中,代码为: struct s3c_ts_reg { unsigned long ADCCON; unsigned long ADCTSC; unsigned long ADCDLY; unsigned long ADCDAT0; unsigned long ADCDAT1; unsigned long ADCUPDN; };
上面是准备代码,下面我们将模仿触摸屏按下的过程来分析代码:
假设我们程序已经写好,并且在开发版上运行正常,当你拿起笔按在触摸屏上的一点,因为在之前我们已经注册了IRQ_TC中断:   request_irq(IRQ_TC,pen_down_up_irq,IRQF_SAMPLE_RANDOM,"ts_pen",NULL); 所以当你按下屏幕时触发中断,从而处理相应的中断处理函数:pen_down_up_irq,详细的代码为: static irqreturn_t pen_down_up_irq(int irq,void *dev_id) { if(s3c_ts_regs->ADCDAT0 & (1<<15)){ //判断屏幕是否还是处于按下状态 //printk("pen up "); //处于抬起状态,上报抬起事件 input_report_abs(s3c_ts_dev,ABS_PRESSURE,0); input_report_key(s3c_ts_dev,BTN_TOUCH,0); input_sync(s3c_ts_dev); enter_wait_pen_down_mode(); //进入等待按下模式 }else{ //此时屏幕还处于按下状态 /* 4.4 在中断中启动ADC,转化XY坐标 */ // printk("pen down "); // enter_wait_pen_up_mode(); enter_measure_xy_mode(); start_adc(); } return IRQ_HANDLED; }   如上面程序所示,此时如果你抬起了你的笔,程序将上报事件,而如果你还处于按下状态,我们将继续跟着程序走:
由于你已经注册了ADC中断:     request_irq(IRQ_ADC,adc_irq,IRQF_SAMPLE_RANDOM,"ts_adc",NULL); 而上面的程序已经开启了ADC:   start_adc()   ,所以我们将进入ADC的中断处理函数adc_irq,详细的代码:     static irqreturn_t adc_irq(int irq,void *dev_id) { static int cnt; static int x[4],y[4]; x[cnt] = ((s3c_ts_regs->ADCDAT0)&0x3ff); y[cnt] = ((s3c_ts_regs->ADCDAT1)&0x3ff); cnt++; if(s3c_ts_regs->ADCDAT0 & (1<<15)){ //printk("pen up "); input_report_abs(s3c_ts_dev,ABS_PRESSURE,0); input_report_key(s3c_ts_dev,BTN_TOUCH,0); input_sync(s3c_ts_dev); enter_wait_pen_down_mode(); }else{ if(cnt == 4){ //printk("adc_irq x= %d, y=%d ",(x[0]+x[1]+x[2]+x[3])/4,(y[0]+y[1]+y[2]+y[3])/4); input_report_abs(s3c_ts_dev,ABS_X,(x[0]+x[1]+x[2]+x[3])/4); input_report_abs(s3c_ts_dev,ABS_Y,(y[0]+y[1]+y[2]+y[3])/4); input_report_abs(s3c_ts_dev,ABS_PRESSURE,1); input_report_key(s3c_ts_dev,BTN_TOUCH,1); input_sync(s3c_ts_dev); cnt = 0; /* 4.6 在ADC中断处理函数中上报(input_event),启动定时器 */ mod_timer(&ts_timer,jiffies + HZ/100); enter_wait_pen_up_mode(); }else{ enter_measure_xy_mode(); start_adc(); } } mod_timer(&ts_timer,jiffies + HZ/100); return IRQ_HANDLED;}           而通过上面的程序,我们知道了触摸屏的处理过程,此时,触摸屏驱动已经基本写好了。而下面就是对特殊的处理,如当长按或者滑动时,此时我们将通过定时器来处理这样的问题,加入此时你还没有松手,触摸屏还处于按下状态。(你可能觉得怎么这么长时间了笔还没抬起啊,其实上面分析的可能麻烦,但在程序中上面的代码还是跑的很快的)。那么,我们接下来就是对定时器的分析了,因为在上文中我们启动了定时器:mod_timer(&ts_timer,jiffies + HZ/100);
当定时时间(此处我们设置定时时间为10ms)到时,进入定时器处理函数:     void s3c_ts_timer_func(unsigned long data) { /* 4.7 定时器时间到,在定时器处理函数中,重复4.4的步骤(用于处理长按和滑动) */ if(s3c_ts_regs->ADCDAT0 & (1<<15)){ //printk("pen up "); input_report_abs(s3c_ts_dev,ABS_PRESSURE,0); input_report_key(s3c_ts_dev,BTN_TOUCH,0); input_sync(s3c_ts_dev); enter_wait_pen_down_mode(); }else{ /* 4.4 在中断中启动ADC,转化XY坐标 */ // printk("pen down "); // enter_wait_pen_up_mode(); enter_measure_xy_mode(); start_adc(); } }           在上面的程序中,可以看出定时时间到后定时器又会启动ADC然后将接着步骤2继续执行,一直这样循环,直到松开,而这样也就可以处理长按或者滑动了,当你长按或者滑动时,定时时间到后会采集你所在的位置的XY坐标,然后上报,然后继续进入定时器,继续进入ADC,继续采集上报,直到你松开触摸屏。

好了,上面就是触摸屏驱动的大致流程和代码分析了。

下面我们将要按着前面所讲的方法和步骤去分析s3c2410_ts.c这个驱动文件。
首先分析一个驱动文件,第一个要看的就是他的入口函数:     platform_driver_register(&s3c2410ts_driver);
这个入口函数中注册了一个platform_driver的结构体,而这个结构体的probe函数就是当配对成功时,首先进入的函数:
下面我们进probe中分析:           static int __init s3c2410ts_probe(struct platform_device *pdev)
在s3c2410_ts.c中首先对硬件的操作:           adc_clock = clk_get(NULL, "adc"); if (!adc_clock) { printk(KERN_ERR "failed to get adc clock source "); return -ENOENT; } clk_enable(adc_clock);
这个是对CLKCON的寄存器中ADC模块的使能。         base_addr=ioremap(S3C2410_PA_ADC,0x20); //对s3c2410z中寄存器的remap if (base_addr == NULL) { printk(KERN_ERR "Failed to remap register block "); return -ENOMEM; } /* 对触摸屏寄存器的操作 */ s3c2410_ts_connect(); if ((info->presc&0xff) > 0) iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(info->presc&0xFF), base_addr+S3C2410_ADCCON); else iowrite32(0,base_addr+S3C2410_ADCCON); /* 初始化寄存器 */ if ((info->delay&0xffff) > 0) iowrite32(info->delay & 0xffff, base_addr+S3C2410_ADCDLY); iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
通过对上面的代码的分析我们可以看出,上面主要是对硬件相关的操作,如寄存器。
而下面的代码就是将触摸屏注册到输入子系统中:           /* Initialise input stuff */ memset(&ts, 0, sizeof(struct s3c2410ts)); input_dev = input_allocate_device(); //分配一个input_dev结构体 if (!input_dev) { printk(KERN_ERR "Unable to allocate the input device !! "); return -ENOMEM; } ts.dev = input_dev; ts.dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); //设置产生同步事件,按键类事件和绝对位移事件 ts.dev->keybit[LONG(BTN_TOUCH)] = BIT(BTN_TOUCH); //设置产生按键中的触摸屏按键 input_set_abs_params(ts.dev, ABS_X, 0, 0x3FF, 0, 0); //绝对位移在X方向的事件 input_set_abs_params(ts.dev, ABS_Y, 0, 0x3FF, 0, 0); //绝对位移在Y方向的事件 input_set_abs_params(ts.dev, ABS_PRESSURE, 0, 1, 0, 0); //绝对位移在压力方向的事件 ts.dev->private = &ts; ts.dev->name = s3c2410ts_name; ts.dev->id.bustype = BUS_RS232; ts.dev->id.vendor = 0xDEAD; ts.dev->id.product = 0xBEEF; ts.dev->id.version = S3C2410TSVERSION; ts.shift = info->oversampling_shift;     而接下来就是对中断的设置了:   if (request_irq(IRQ_ADC, stylus_action, IRQF_SAMPLE_RANDOM | SA_SHIRQ, "s3c2410_action", ts.dev)) { printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_ADC ! "); iounmap(base_addr); return -EIO; } if (request_irq(IRQ_TC, stylus_updown, IRQF_SAMPLE_RANDOM, "s3c2410_action", ts.dev)) { printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_TC ! "); iounmap(base_addr); return -EIO; }     当上面所有的工作做完后我们就可以注册这个设备了:   /* All went ok, so register to the input system */ input_register_device(ts.dev);
而接下来就是和上面一样对触摸屏按下和抬起过程进行分析了:
我们同样先将整个过程写下来,让大家有个大致过程的认识:




1)按下触摸屏,触发触摸屏中断然后进入stylus_updown,如果触摸屏状态为按下,则调用touch_timer_fire启动ADC转换;
2)当ADC启动后,触发ADC中断即进入stylus_action,如果转换的次数小于4,则重新启动ADC进行转换,如果4次完毕后,启动1个时间滴答的定时器,
   停止ADC转换,也就是说在这个时间滴答内,ADC转换是停止的;这里为什么要在1个时间滴答到来之前停止ADC的转换呢?这是为了防止屏幕抖动。
3)如果1个时间滴答到来则进入定时器服务程序touch_timer_fire,判断触摸屏仍然处于按下状态则上报事件和转换的数据,并重启ADC转换,重复第(2)步;
4)如果触摸抬起了,则上报释放事件,并将触摸屏重新设置为等待中断状态。
上面的步骤是从linux嵌入式系统开发之触摸屏---驱动篇(下/源码分析),中抄来的。
下面就带着这个过程用代码分析:
首先触摸屏按下,触发触摸屏中断处理函数:       static irqreturn_t stylus_updown(int irq, void *dev_id) { unsigned long data0; unsigned long data1; int updown; data0 = ioread32(base_addr+S3C2410_ADCDAT0); //读出x方向的值 data1 = ioread32(base_addr+S3C2410_ADCDAT1); //读出y方向的值 /* 判断触摸屏是否按下 */ updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); /* TODO we should never get an interrupt with updown set while * the timer is running, but maybe we ought to verify that the * timer isn't running anyways. */ if (updown) //如果处于按下状态 touch_timer_fire(0); //调用touch_timer_fire函数 return IRQ_HANDLED; }
我们假设现在还处于按下状态,所以将继续调用touch_timer_fire函数:       static void touch_timer_fire(unsigned long data) { unsigned long data0; unsigned long data1; int updown; data0 = ioread32(base_addr+S3C2410_ADCDAT0); //读出x方向的值 data1 = ioread32(base_addr+S3C2410_ADCDAT1); //读出y方向的值 /* 判断触摸屏是否按下 */ updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); if (updown) { //屏幕被按下 if (ts.count != 0) { //判断是否采集的次数为0 long tmp; /* add by www.100ask.net */ tmp = ts.xp; ts.xp = ts.yp; ts.yp = tmp; /* 多次采集求平均值,这里整除的数就是要采集的次数 */ ts.xp >>= ts.shift; ts.yp >>= ts.shift; /* 将采集的到的平均值上报 */ input_report_abs(ts.dev, ABS_X, ts.xp); input_report_abs(ts.dev, ABS_Y, ts.yp); input_report_key(ts.dev, BTN_TOUCH, 1); input_report_abs(ts.dev, ABS_PRESSURE, 1); input_sync(ts.dev); } /* 如果还没有开启ADC采集,则设置XP,yp,count为0,并开启ADC */ ts.xp = 0; ts.yp = 0; ts.count = 0; /* 开启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 { //如果松手,将count设为0,并上报事件。 ts.count = 0; input_report_key(ts.dev, BTN_TOUCH, 0); input_report_abs(ts.dev, ABS_PRESSURE, 0); input_sync(ts.dev); iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); } }     从上面我们知道想要得到XY的坐标值要开启ADC后在ADC中断处理函数中才能采集XY的值,所以下面分析ADC中断函数:

    static irqreturn_t stylus_action(int irq, void *dev_id) { unsigned long data0; unsigned long data1; data0 = ioread32(base_addr+S3C2410_ADCDAT0); data1 = ioread32(base_addr+S3C2410_ADCDAT1); /* 采集XY的坐标值,并多次测量,而在后面求平均值 */ ts.xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK; //因为ADC为10位,所以要使用有效位 ts.yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK; ts.count++; //此处count就是采集的次数 if (ts.count < (1<
通过上面的分析我们可以看出,老师所讲的方法和内核所提供的方法,他们大致的内容是一样的,不同的地方只是具体内容在不同的位置设定。