linux嵌入式系统开发之触摸屏---驱动篇(下/源码分析)

时间:2022-08-31 18:51:57
 

“啥叫旷日持久,你的一篇烂触摸屏的驱动,愣是唧唧歪歪讲了三篇还没到正题,哼”小王啊,小王,总是喜欢在那里撅着个嘴瞎抱怨。

“啊?小姐啊,你是坐着不腰疼吧,你来讲讲试试 ..真是的”一向老好人的我咋也扛不住了呢.”好好,我失礼了,今天就是最后一个小结了,是有关源码分析的”。

“好好,看来不对你来点激将法,你是不会着急的了”小王破怒而笑的说道。

“呵呵..”我苦笑到,其实小王哪里知道,今天是一个值得回忆的日子的啊,今天的课基本上是前面的一个句号,有关字符设备驱动的基本就结束了,后面就要开始有关其它部分的讲解了(比如块设备等),很是伤感,可这些又怎么能跟小王说了。

  罢了,罢了,所有的我自己心里藏,开始今天的课---linux嵌入式系统开发之触摸屏---驱动篇(下/源码分析)

还记得我上节给你讲的input输入设备驱动的架构吗,今天的源码分析就是照着那个步骤来的:

必要的头文件#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 <linux/gpio.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <plat/regs-adc.h>
#include <mach/regs-gpio.h>
static struct input_dev *dev;  //输入设备结构体 
static struct clk *adc_clock; //保存从平台时钟列表中获取的ADC时钟
static void __iomem *base_addr;  //用来保存经过虚拟映射后的内存地址
static long xp;//记录转换后的X坐标值和Y坐标值
static long yp;

static int count; //用于计数对触摸屏压下或抬起时模拟输入转换的次数
static int OwnADC = 0;  //做为一个标签,只有对触摸屏操作后才对X和Y坐标进行转换
//定义一个外部的信号量ADC_LOCK,因为ADC_LOCK在ADC驱动程序中已申明这样就能保证ADC资源在ADC驱动和触摸屏驱动中进行互斥访问
extern struct semaphore ADC_LOCK;
//定义一个WAIT4INT宏,该宏将对ADC触摸屏控制寄存器进行操作S3C2410_ADCTSC_YM_SEN这些宏都定义在regs-adc.h中

#define WAIT4INT(x) (((x)<<8) | \
      S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
      S3C2410_ADCTSC_XY_PST(3))
//定义一个AUTOPST宏,将ADC触摸屏控制寄存器设置成自动转换模式
#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 *s3c2440ts_name = "s3c2440Tscreen";   //设备名
#define S3C2410TSVERSION 0x0101    //版本

static void touch_timer_fire(unsigned long data)
{
   unsigned long data0; //用于记录这一次AD转换后的值
   unsigned long data1;
   int updown;   //记录触摸屏操作状态是按下还是抬起
    
   data0 = ioread32(base_addr + S3C2410_ADCDAT0);  //读取这一次AD转换后的值,注意这次主要读的是状态
   data1 = ioread32(base_addr + S3C2410_ADCDAT1);
   //记录这一次对触摸屏是压下还是抬起,该状态保存在数据寄存器的第15位,所以与上S3C2410_ADCDAT0_UPDOWN
   updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
   if(updown){  //判断触摸屏的操作状态
        if(count != 0){  //如果是按下,并且ADC已经转换了报告事件和数据
            long tmp;
            
      tmp = xp;
      xp = yp;
      yp = tmp;
            
      xp >>= 2;
      yp >>= 2;
            
      input_report_abs(dev, ABS_X, xp);  //报告X,Y的绝对坐标值
            input_report_abs(dev, ABS_Y, yp);
            
      input_report_key(dev, BTN_TOUCH, 1);  //报告按键事件,键值为1(代表触摸屏对应的按键被按下)
      input_report_abs(dev, ABS_PRESSURE, 1);   //报告触摸屏的状态,1表明触摸屏被按下
            input_sync(dev);   //等待接收方受到数据后回复确认,用于同步
      }
      xp = 0;   //如果状态是按下,并且ADC还没有开始转换就启动ADC进行转换
      yp = 0;
      count = 0;
      //设置触摸屏的模式为自动转换模式
      iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC);
      //启动ADC转换
      iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON);
   }else{    //状态是抬起状态
            count = 0;
            input_report_key(dev, BTN_TOUCH, 0);   //报告按键事件,键值为0(代表触摸屏对应的按键被释放)
            input_report_abs(dev, ABS_PRESSURE, 0);   //报告触摸屏的状态,0表明触摸屏没被按下
            input_sync(dev);   //等待接收方受到数据后回复确认,用于同步
        
            iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);    //将触摸屏重新设置为等待中断状态
            if(OwnADC){   //如果触摸屏抬起,就意味着这一次的操作结束,所以就释放ADC资源的占有
                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_action(int irq, void *dev_id)
{ //ADC中断服务程序,AD转换完成后触发执行
      unsigned long data0;  //记录这一次AD转换后的值
      unsigned long data1;

      if(OwnADC){   //读取这一次AD转换后的值,注意这次主要读的是坐标
            data0 = ioread32(base_addr+S3C2410_ADCDAT0);
            data1 = ioread32(base_addr+S3C2410_ADCDAT1);
            //记录这一次通过AD转换后的X坐标值和Y坐标值,根据数据手册可知,X和Y坐标转换数值
            //分别保存在数据寄存器0和1的第0-9位,所以这里与上S3C2410_ADCDAT0_XPDATA_MASK就是取0-9位的值
            xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;
            yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;
            count++;  //记数这一次AD转换的次数
		
            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个时间滴答的定时器,这是就会去执行定时器服务程序上报事件和数据
           	  mod_timer(&touch_timer, jiffies+1);  //修改定时器的expire
	          iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC); 
            }
      }
      return IRQ_HANDLED;
}
static irqreturn_t stylus_updown(int irq, void *dev_id)
{//触摸屏中断服务程序,对触摸屏按下或提笔时触发执行
 unsigned long data0;   //用于记录这一次AD转换后的值
      unsigned long data1;
      int updown;  //用于记录触摸屏操作状态是按下还是抬起
      if(down_trylock(&ADC_LOCK) == 0) {   //ADC资源可以获取,即上锁
	  OwnADC = 1;   //标识对触摸屏进行了操作
	  data0 = ioread32(base_addr+S3C2410_ADCDAT0);//读取这一次AD转换后的值,注意这次主要读的是状态
	  data1 = ioread32(base_addr+S3C2410_ADCDAT1);
          //记录这一次对触摸屏是压下还是抬起,该状态保存在数据寄存器的第15位,所以与上S3C2410_ADCDAT0_UPDOWN
	  updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
          if (updown) {   //如果是按下状态
		touch_timer_fire(0);   //如果是按下状态,则调用touch_timer_fire函数来启动ADC转换
	  } else {
	        OwnADC = 0;   //如果是抬起状态,就结束了这一次的操作,释放ADC资源的占有
		up(&ADC_LOCK);
  	  }
      }
      return IRQ_HANDLED;
}
static inline void s3c2440_ts_connect(void)
{
    s3c2410_gpio_cfgpin(S3C2410_GPG(12), S3C2410_GPG12_XMON);
    s3c2410_gpio_cfgpin(S3C2410_GPG(13), S3C2410_GPG13_nXPON);
    s3c2410_gpio_cfgpin(S3C2410_GPG(14), S3C2410_GPG14_YMON);
    s3c2410_gpio_cfgpin(S3C2410_GPG(15), S3C2410_GPG15_nYPON);
}
static int __init s3c2440ts_init(void)
{
    struct input_dev *input_dev;
    //从平台时钟队列中获取ADC的时钟,这里之所以要获得这个时钟是因为ADC的转换频率
        //跟时钟有关。系统的一些时钟定义在arch/arm/plat-s3c24xx/s3c2410-clock.c中
    adc_clock = clk_get(NULL, "adc");
    if(!adc_clock){
        printk(KERN_ERR "failed to get adc lock source\n");
        return -ENOENT;
    }
    //使能时钟,就预开启,clk_enable定义在arch/arm/plat-s3c/clock.c
    clk_enable(adc_clock);
    //将ADC的IO端口占用的这段IO空间映射到内存的虚拟地址S3C2410_PA_ADC
    //是ADC控制器的基地址,定义在mach-s3c2410/include/mach/map.h中,0x20是虚拟地址长度大小
    base_addr = ioremap(S3C2410_PA_ADC, 0x20);
    if(base_addr == NULL){
        printk(KERN_ERR "Failed to remap register block\n");
        return -ENOMEM;
    }    
    //配置GPIOS
    s3c2440_ts_connect();
    iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(0xFF),base_addr+S3C2410_ADCCON); //这些的函数都是系统提供的
    iowrite32(0xffff, base_addr+S3C2410_ADCDLY);
    iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
    
    input_dev = input_allocate_device();
    
    if(!input_dev){
        printk(KERN_ERR "Unable to allocate the input device!!\n");
        return -ENOMEM;
    }
    
    dev = input_dev;
    //设置输入设备的相关信息,evbit字段用于描述支持的事件,这里支持
    //同步事件、按键事件、绝对坐标事件,BIT宏实际就是对1进行位操作,定义在linux/bitops.h中
    dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS);
    //keybit字段用于描述按键的类型,在input.h中定义了很多,这里用BTN_TOUCH类型来表示触摸屏的点击
    dev->keybit[BITS_TO_LONGS(BTN_TOUCH)] = BIT(BTN_TOUCH);
    //触摸屏使用的是绝对坐标系统。这里设置该坐标系统中X和Y坐标的最小值和最大值(0-1023范围)
    //ABS_X和ABS_Y就表示X坐标和Y坐标,ABS_PRESSURE就表示触摸屏是按下还是抬起状态
    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);
    
    dev->name = s3c2440ts_name;
    dev->id.bustype = BUS_RS232;
    dev->id.vendor = 0xDEAD; /*经销商ID号*/
    dev->id.product = 0xBEEF; /*产品ID号*/
    dev->id.version = S3C2410TSVERSION;/*版本ID号*/
    
    //申请ADC中断,AD转换完成后触发。这里使用共享中断IRQF_SHARED
    //是因为该中断号在ADC驱动中也使用了,
    if(request_irq(IRQ_ADC, stylus_action, IRQF_SHARED|IRQF_SAMPLE_RANDOM, "s3c2440_action", dev)){
        printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_ADC !\n");
        iounmap(base_addr);
        return -EIO;
    }
    /*申请触摸屏中断,对触摸屏按下或提笔时触发*/
    if(request_irq(IRQ_TC, stylus_updown, IRQF_SAMPLE_RANDOM|IRQF_SHARED, "s3c2410_action", dev)){
        printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_TC !\n");
        iounmap(base_addr);
        return -EIO;
    }
    printk(KERN_ERR "%s successfully loaded\n", s3c2440ts_name);
    //一些都准备就绪,现在就把ts_dev触摸屏设备注册到输入子系统中
    input_register_device(dev);
    return 0;
}
static void __exit s3c2440ts_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(s3c2440ts_init);
module_exit(s3c2440ts_exit);
说明:这后来的很多驱动,慢慢就需要编译内核了,因为买的板子带的内核都包含了驱动,所以为了保证我们的驱动不受它的干扰,所以..不说你也明白是吧。

“小王,洋洋洒洒一大堆,我再来给你捋捋顺序吧”

1)如果触摸屏感觉到触摸,则触发触摸屏中断即进入stylus_updown,获取ADC_LOCK后判断触摸屏状态为按下,则调用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)如果触摸抬起了,则上报释放事件,并将触摸屏重新设置为等待中断状态。

“小王呢,有关触摸屏的相关部分经过三次专题讲解,终于讲完了。总体来说,前边的有关字符设备驱动的相关理论和实例也都讲完了。从下次起,我们就要开始讲解有关设备驱动的其他几个大的环节了,比如块设备啦, 音频设备,网络设备等等”我如释重负而心里遗憾的说道。

“嗯,好好,后面难度大吗?”小王似乎充满期待而又满脸担心。

“放心吧,驱动相关的理论基础都差不多了,后面的其他大块,无非是设备本身的复杂性要求我们更多更巧妙的利用前边学习的理论基础,再说啦,有我呢”我啊,胸脯一排。

“那行,有你我就不怕….”