欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

7.Linux - 触摸屏(电阻式)驱动程序的实施

最编程 2024-07-15 19:19:22
...

一、触摸屏概述

      触摸屏作为一种输入设备,是目前最简单、方便、自然的一种人机交互方式。按照触摸屏的工作原理和传输信息的介质,可以将触摸屏分为四种:电阻式、电容感应式、红外线式和表面声波式。每一种触摸屏都有其各自的优缺点,要了解哪种触摸屏适用于哪种场合,关键就在要了解每一类触摸屏的工作原理和特点。本节我们主要介绍的是4线电阻式触摸屏。

1.1 电阻式触摸屏

      电阻式触摸屏利用压力感应进行控制。电阻触摸屏的主要部分是一块与显示器表面紧密结合的电阻薄膜屏,这是一种多层的复合薄膜,以一层玻璃或硬塑料平板作为基层,表面涂有一层透明氧化金属(透明的导电电阻)导电层,上面再盖有一层外表面硬化处理、光滑防擦的塑料层、它的内表面也涂有一层涂层、在他们之间有许多细小的的透明隔离点把两层导电层隔开绝缘。所有的电阻式触摸屏都采用分压器原理来产生代表X坐标和Y坐标的电压。分压器是通过将两个电阻进行串联来实现的。电阻R1连接正参考电压VREF,电阻R2接地。两个电阻连接点处的电压测量值与R2的阻值成正比。当手指触摸屏幕时,两层导电层在触摸点位置就有了接触,电阻发生变化,在X和Y两个方向上产生信号,然后送触摸屏控制器。控制器侦测到这一接触并计算出(X,Y)的位置,再根据模拟鼠标的方式运作。这就是电阻技术触摸屏的最基本原理。实际上就是欧姆定律的巧妙运用!

1.2 电阻式触摸屏信号测量

4线触摸屏包含了两个阻性层,如下图所示:


当没有触摸按下时,X层和Y层是分离的,此时就测不到电压。

X坐标方向值测量:
      将XP接到3.3V , XM接0V, YP和YM悬空,我们以按压X坐标的中间位置, X层和Y层便闭合了,此时触摸屏控制器 (ADC)就可测量到YP输出的当前X坐标值1.66V,如下图:

Y坐标方向值测量:
      将YP接3.3V , YM接0V, XP和XM悬空,我们以按压X坐标的中间位置, X层和Y层便闭合了,此时触摸屏控制器 (ADC)就可测量到XP输出的当前Y坐标值1.66V,如下图:

二、触摸屏驱动模型

Linux内核中触摸屏驱动实现基于输入子系统架构,具体请移步 Linux - 输入子系统框架详解 ;


对于触摸屏驱动开发者,核心层和事件处理层由Linux内核提供,主要实现xxx_ts.c,
1)分配一个input_dev结构体
2)设置input_dev的成员
3)注册input_dev 驱动设备到内核中
4)设置触摸屏相关的硬件

触摸屏使用过程简单处理简述如下(流程):

  1. 按下,产生中断;
  2. 在中断服务程序里,启动ADC,转换X,Y坐标;
  3. ADC结束,产生ADC中断;
  4. 在ADC中断处理函数里上报(input_event),启动定时器;
  5. 定时器时间到,执行2再次启动ADC,转换X,Y坐标;(处理长按、滑动)
  6. 松开

二、触摸屏驱动相关的重要数据结构和函数

      在输入子系统架构中,会将设备抽象出一个input_dev结构体;它是驱动的主体。每个struct input_dev代表一个输入设备。

2.1 struct input_dev结构体
struct input_dev {     
       void *private;
       const char *name;  //设备名字
       const char *phys;  //文件路径,比如 input/buttons
       const char *uniq;  
       struct input_id id;

       unsigned long evbit[NBITS(EV_MAX)];  //表示支持哪类事件,常用有以下几种事件(可以多选)
       //EV_SYN      同步事件,当使用input_event()函数后,就要使用这个上报个同步事件
       //EV_KEY       键盘事件
       //EV_REL       (relative)相对坐标事件,比如鼠标
       //EV_ABS       (absolute)绝对坐标事件,比如摇杆、触摸屏感应
       //EV_MSC      其他事件,功能
       //EV_LED       LED灯事件
       //EV_SND      (sound)声音事件
       //EV_REP       重复键盘按键事件
       //(内部会定义一个定时器,若有键盘按键事件一直按下/松开,就重复定时,时间一到就上报事件)  

       //EV_FF         受力事件
       //EV_PWR      电源事件
       //EV_FF_STATUS  受力状态事件

       unsigned long keybit[NBITS(KEY_MAX)];   //存放支持的键盘按键值
       //键盘变量定义在:include/linux/input.h, 比如: KEY_L(按键L)、BTN_TOUCH(触摸屏的按键)

       unsigned long relbit[NBITS(REL_MAX)];    //存放支持的相对坐标值
       unsigned long absbit[NBITS(ABS_MAX)];   //存放支持的绝对坐标值,存放下面4个absxxx[]
       unsigned long mscbit[NBITS(MSC_MAX)];   //存放支持的其它事件,也就是功能
       unsigned long ledbit[NBITS(LED_MAX)];    //存放支持的各种状态LED
       unsigned long sndbit[NBITS(SND_MAX)];    //存放支持的各种声音
       unsigned long ffbit[NBITS(FF_MAX)];       //存放支持的受力设备
       unsigned long swbit[NBITS(SW_MAX)];     //存放支持的开关功能
         ... ...

 
/*以下4个数组都会保存在上面成员absbit[]里,数组号为:ABS_xx ,位于include/linux/input.h */
/*比如数组0,标志就是ABS_X,以下4个的absXXX[0]就是表示绝对位移X方向的最大值、最小值... */
/*对于触摸屏常用的标志有:
ABS_X(X坐标方向), ABS_Y(Y坐标方向), ABS_PRESSURE(压力方向,比如绘图,越用力线就越粗)* / 
       int absmax[ABS_MAX + 1];      //绝对坐标的最大值
       int absmin[ABS_MAX + 1];      //绝对坐标的最小值
       int absfuzz[ABS_MAX + 1];     //绝对坐标的干扰值,默认为0,
       int absflat[ABS_MAX + 1];     //绝对坐标的平焊位置,默认为0
... ...
2.2 分配/释放input_dev结构体

函数原型:struct input_dev *input_allocate_device(void)
参数说明 :无
返回值 : 分配到的input_dev结构体

函数原型:input_free_device(struct input_dev dev)
参数说明 :
dev 待释放的input_dev结构体

2.3 注册/注销input_dev设备

函数原型:input_register_device(struct input_dev *dev)
参数说明 :*dev 注册的input_dev结构体
说       明 :注册一个input_dev,若有对应的驱动事件,则在/sys/class/input下创建这个类设备。

函数原型:input_unregister_device(struct input_dev *dev)
参数说明 :*dev 注销的input_dev结构体
说       明 :卸载/sys/class/input目录下的input_dev这个类设备;

2.4 事件支持(初始化)

函数原型:int set_bit(int nr,long * addr)
参数说明 :设置某个结构体成员 * addr里面的某位等于nr,支持这个功能
说       明 :告诉input输入子系统支持哪些事件,哪些按键;例如:
set_bit(EV_KEY,button_dev.evbit) (其中button_dev是struct input_dev类型)
struct input_dev中有两个成员为:
evbit:
事件类型(EV_RST,EV_REL,EV_MSC,EV_KEY,EV_ABS,EV_REP等)
keybit:
按键类型(当事件类型为EV_KEY时包括BTN_LEFT,BTN_0,BTN_1,BTN_MIDDLE等)

2.5 设置绝对位移的支持参数

函数原型:input_set_abs_params(struct input_dev *dev, int axis, int min, int max, int fuzz, int flat)
参数说明 :*dev : 需要设置的input_dev结构体
                    axis : 需要设置的数组号,常用的有: ABS_X(X坐标方向), ABS_Y(Y坐标方向), ABS_PRESSURE(压力方向)
                    min : axis方向的最小值
                    max : axis方向的最大值
                    fuzz : axis方向的干扰值
                    flat : axis方向的平焊位置

2.5 上报EV_ABS事件

函数原型:input_report_abs(struct input_dev *dev, unsigned int code, int value);
参数说明 :*dev : 要上报哪个input_dev驱动设备的事件
                    code : EV_ABS事件里支持的哪个方向,比如X坐标方向则填入: ABS_X
                    value : 对应的方向的值,比如X坐标126
说       明 :该函数实际就是调用的input_event(dev, EV_ABS, code, value);

2.6 上报EV_KEY事件

函数原型:input_report_key(struct input_dev *dev, unsigned int code, int value);

2.7 同步事件通知,通知系统有事件上报

函数原型:input_sync(struct input_dev *dev) ;
参数说明 :*dev : 要上报哪个input_dev驱动设备的事件
说       明 :同步用于告诉input core子系统报告结束

三、触摸屏驱动实现

3.1 在init入口函数中:

1)分配一个input_dev结构体
2)设置input_dev的成员
-> 2.1)设置input_dev->evbit支持按键事件,绝对位移事件
(触摸屏:通过按键BTN_TOUCH获取按下/松开,通过绝对位移获取坐标)
-> 2.2)设置input_dev-> keybit支持BTN_TOUCH触摸屏笔尖按下
-> 2.3)设置input_dev-> absbit 支持ABS_X、ABS_Y、 ABS_PRESSURE
input_set_abs_params(ts.dev, ABS_X, 0, 0x3FF, 0, 0);
  input_set_abs_params(ts.dev, ABS_Y, 0, 0x3FF, 0, 0); // 0x3FF:最大值为10位ADC,
  input_set_abs_params(ts.dev, ABS_PRESSURE, 0, 1, 0, 0); //压力最多就是1
3)注册input_dev 驱动设备到内核中
4)设置触摸屏相关的硬件
-> 4.1)开启ADC时钟,使用clk_get ()和clk_enable()函数
-> 4.2) ioremap获取寄存器地址,设置寄存器ADCCON =(1<<14)|(49<<6),分频
->4.3)设置寄存器ADCDLY=0xffff,ADC启动延时时间设为最大值,使触摸按压更加稳定
->4.4)开启IRQ_TC笔尖中断、开启IRQ_ADC中断获取XY坐标
-> 4.5)初始化定时器,增加触摸滑动功能
->4.6)最后设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断

3.2 在出口函数中:

1)注销内核里的input_dev、
2)释放中断、删除定时器、iounmap注销地址、
3)释放input_dev、

3.3 在IRQ_TC中断函数中:

1)若判断笔尖为松开,设置寄存器ADCTSC =0XD3(按下中断)
2)若判断笔尖按下,设置为XY自动转换模式,启动一次ADC转换,ADC转换成功,会进入ADC中断

3.4 在IRQ_ADC中断函数中:

1)获取ADCDAT0的位[9:0],来算出XY方向坐标值
2)测量n次值保存在数组中,然后再次设置为XY自动转换模式,启动ADC
(PS:要启动ADC转换之前必须设置一次XY为自动转换模式,不然获取的数据会不准)
3)采集完毕,使用快速排序将n次值排序后,以最小值为基准,如有误差非常大的数,则舍弃,如果没有则打印数组的中间值,实现中值滤波。
(PS: 使用快速排序,比冒泡更快,详解 )
4)打印数据后,必须设置寄存器ADCTSC =0X1D3(松开中断IRQ_TC)
(PS:在ADC采样模式下是判断不到ADCDAT0的bit15位的,因为ADCDAT0已被自动设置为X坐标的采样值)
5)设置定时器10ms超时时间

3.5 在定时器超时函数中:

1)判断ADCDAT0的bit15位,若还在按下再次启动ADC转换(实现触摸滑动功能)
2)若松开,设置寄存器ADCTSC =0XD3(按下中断)

代码如下:

#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 <asm/plat-s3c24xx/ts.h> 
#include <asm/arch/regs-adc.h>
#include <asm/arch/regs-gpio.h>
 
 

static struct input_dev  *ts_dev;
static struct clk    *ADC_CLK;                    //adc时钟
static struct timer_list    ts_timer;             //定时器

struct adc_regs{
       unsigned long   adccon;
       unsigned long   adctsc;
       unsigned long   adcdly;
       unsigned long  adcdat0;
       unsigned long  adcdat1;
       unsigned long  adcupdn;
};

static volatile struct adc_regs  *adc_regs;

/*启动TC 函数*/
static void set_pen_down(void)
{
       /* 设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断*/
          adc_regs->adctsc = 0xd3;
}

static void set_pen_up(void)
{
     /* 设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断*/
         adc_regs->adctsc = 0x1d3;
}



/*启动ADC  转换函数*/
static void adc_start(void)
{
   adc_regs->adctsc= (1<<3)| (1<<2);     //启动XY自动转换
   adc_regs->adccon|=(1<<0);                         //启动1次ADC转换
}

 

/*快速排序,比冒泡更快*/
/*快速排序详解:http://www.cnblogs.com/lifexy/p/7597276.html*/
static void find_frst(int *s,int lift,int right)
{
    int i=lift,j=right,temp;  //(1)初始化i、j   
    if(lift>=right)
     return ;
    temp=s[i];                //(2)以第一个数组为比较值,保存到temp中
    while(i<j)
    {   
      while(j>i&&s[j]>=temp)  //(3)j--,找小值
      j--;
      s[i]= s[j];             //保存小值,到s[i]上  
      while(i<j&&s[i]<=temp)  //(4)i++,找大值
      i++;
      s[j--]=s[i];            //保存大值 到s[j]上
    }

    s[i]=temp;             //(5)将比较值放在s[i]上       

  /*(6)拆分成两个数组 s[0,i-1]、s[i+1,n-1]又开始排序 */
  find_frst(s,lift,i-1);         //左
  find_frst(s,i+1,right);        //右    
}


/*查找X Y坐标偏移值是否太大*/
/*return:  0误差大,   1误差小            */
static int  find_xy_offset(int x[], int      y[],int n)
{
       int i;
       for(i=n;i>=1;i--)
       {
       if(x[i]-x[i-1]>10)    //判断是否大于误差10, 
       return 0;
             
       if(y[i]-y[i-1]>10)//判断是否大于误差10,
       return 0;          
       }
      
       return 1;
}

 
       /*定时器函数,实现触摸滑动功能    */
void pen_updown_timer(unsigned long  cnt)
{
   if((adc_regs->adcdat0>>15)&0x01)       //此时笔尖已经抬起
   {
       set_pen_down();                   //设置TC中断
   }
   else
   {
       adc_start();                          //启动一次ADC转换
   }

}
   
       /*触摸中断IRQ_TC  */
static irqreturn_t tc_handler(int irq, void *dev_id)
{

       if((adc_regs->adcdat0>>15)&0x01)     //此时笔尖已经抬起
       {      
       set_pen_down();
       }

       else
       {
       adc_start();     //启动一次ADC转换
       }
       return IRQ_HANDLED;
}


       /*ADC中断IRQ_ADC:测XY坐标  */
static irqreturn_t adc_handler(int irq, void *dev_id)
{
       static int                      adc_x[5],adc_y[5];          //保存XY坐标
       static unsigned char     xy_cnt=0;                            //计数   

       adc_y[xy_cnt]  =adc_regs->adcdat1&0x3ff;               //10位ADC
       adc_x[xy_cnt]  =adc_regs->adcdat0&0x3ff;               //10位ADC     

       if (adc_regs->adcdat0 & (1<<15))
              {
                     /* 已经松开 */
                     xy_cnt = 0;
                     set_pen_down();
              }
       else{
        xy_cnt++;
        if(xy_cnt>=5)
              {
              xy_cnt=0;
                find_frst(adc_x,0,4);                      // 快速排序X
                find_frst(adc_y,0,4);                      // 快速排序y         
                     if(find_xy_offset(adc_x,adc_y,4))                         
                     {
                            printk("X:  %04d,y:  %04d \n",adc_x[2],adc_y[2]);      //中值滤波                  
                     }

                     set_pen_up();
                     mod_timer(&ts_timer    ,jiffies+HZ/100);            //启动定时10ms
              }
         else     //在测一次
            {
              adc_start();    //启动 ADC
            }
          }     
       return IRQ_HANDLED;
}

 

 

/*入口函数*/
static int myts_init(void)
{
       /*1. 申请input_dev   */
       ts_dev=input_allocate_device();
       /*2. 设置input_dev*/
       set_bit(EV_ABS, ts_dev->evbit);
       set_bit(EV_KEY, ts_dev->evbit);
       set_bit(BTN_TOUCH, ts_dev->keybit);
       input_set_abs_params(ts_dev, ABS_X , 0 , 0x3ff , 0 , 0);       //adc是个10位的,所以为0X3FF
       input_set_abs_params(ts_dev, ABS_Y , 0 , 0x3ff , 0 , 0);       //adc是个10位的,所以为0X3FF
       input_set_abs_params(ts_dev, ABS_PRESSURE, 0 , 1 , 0 , 0);       //adc是个10位的,所以为0X3FF   

       /*3.注册input_dev 驱动设备到内核中*/
       input_register_device(ts_dev);   

       /*4.设置触摸屏相关的硬件*/
       /*4.1 开启ADC时钟 */
       ADC_CLK        =clk_get(0,"adc");
       clk_enable(ADC_CLK);
      
       /*4.2 设置寄存器ADCCON分频,*/
       adc_regs=ioremap(0x58000000, sizeof(struct adc_regs));
       adc_regs->adccon=(1<<14)|(49<<6);        //50Mhz/(49+1)=1Mhz
  

       /*4.3 设置中断IRQ_TC   IRQ_ADC  */
       request_irq(IRQ_TC       , tc_handler, IRQF_SAMPLE_RANDOM, "pen_updown", 0);
       request_irq(IRQ_ADC     , adc_handler, IRQF_SAMPLE_RANDOM, "adc"    , 0);


       /*4.4设置寄存器ADCDLY=0xffff */
       adc_regs->adcdly  =0xffff;   

       /*4.5 初始化定时器*/
       init_timer(&ts_timer);
       ts_timer.function    =pen_updown_timer;
       add_timer(&ts_timer);

       /*4.6设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断*/
       set_pen_down();

       return 0;
}

/*出口函数*/
static void myts_exit(void)
{
       /*1.注销内核里的input_dev、*/
       input_unregister_device(ts_dev);
       /*2.释放中断、删除定时器、iounmap注销地址、*/
       free_irq(IRQ_TC, NULL);
       free_irq(IRQ_ADC, NULL);    

       del_timer(&ts_timer);
       iounmap(adc_regs);

       /*3.释放input_dev、*/
       input_free_device(ts_dev);
}

module_init(myts_init);
module_exit(myts_exit); 
MODULE_LICENSE("GPL");