0 Replies Latest reply on Aug 23, 2011 7:15 PM by wensenyuan

    [资料转发分享]触摸屏驱动程序分析

    Brown Belt

      Linux 下的触摸屏驱动程序主要都在/kernel/drivers/char/s3c2410-ts.c 文件中。

          触摸屏的file_operations 结构定义如下:
      static struct file_operations s3c2410_fops = {
      owner: THIS_MODULE,
      open: s3c2410_ts_open,
      read: s3c2410_ts_read,
      release: s3c2410_ts_release,
      #ifdef USE_ASYNC
      fasync: s3c2410_ts_fasync,
      #endif
      poll: s3c2410_ts_poll,
      };

          在触摸屏设备驱动程序中,全局变量struct TS_DEV tsdev 是很重要的,用来保存触摸屏的相关参数、等待处理的消息队列、当前采样数据、上一次采样数据等信息,数据结构struct TS_DEV 的定义如下:
      typedef struct {
      unsigned int penStatus; /* PEN_UP, PEN_DOWN, PEN_SAMPLE */
      TS_RET buf[MAX_TS_BUF]; /* protect against overrun(环形缓冲区) */
      unsigned int head, tail;/* head and tail for queued events (环形缓冲区的头尾)*/
      wait_queue_head_t wq; //* 等待队列数据结构
      spinlock_t lock; //* 自旋锁
      #ifdef USE_ASYNC
      struct fasync_struct *aq;
      #endif
      #ifdef CONFIG_PM
      struct pm_dev *pm_dev;
      #endif
      } TS_DEV;
      static TS_DEV tsdev;
      在/kernel/include/asm-arm/linuette_ioctl.h 文件中:
      typedef struct {
        unsigned short pressure; //* 压力,这里可定义为笔按下,笔抬起,笔拖曳
        unsigned short x;  //* 横坐标的采样值
        unsigned short y;  //* 纵坐标的采样值
        unsigned short pad;  //* 填充位
      } TS_RET;
      在/kernel/include/linux/wait.h 文件中:
      struct __wait_queue_head {
      wq_lock_t lock;
      struct list_head task_list;
      #if WAITQUEUE_DEBUG
      long __magic;
      long __creator;
      #endif
      };
      typedef struct __wait_queue_head wait_queue_head_t;

          TS_RET结构体中的信息就是驱动程序提供给上层应用程序使用的信息,用来存储触摸屏的返回值。上层应用程序通过读接口,从底层驱动中读取信息,并根据得到的值进行其他方面的操作。
          TS_DEV结构用于记录触摸屏运行的各种状态,PenStatus包括PEN_UP、PEN_DOWN和PEN_FLEETING。buf[MAX_TS_BUF]是用来存放数据信息的事件队列,head、tail分别指向事件队列的头和尾。程序中的笔事件队列是一个环形结构,当有事件加入时,队列头加一,当有事件被取走时,队列尾加一,当头尾位置指针一致时读取笔事件的信息,进程会被安排进入睡眠。wq等待队列,包含一个锁变量和一个正在睡眠进程链表。当有好几个进程都在等待某件事时,Linux会把这些进程记录到这个等待队列。它的作用是当没有笔触事件发生时,阻塞上层的读操作,直到有笔触事件发生。lock使用自旋锁,自旋锁是基于共享变量来工作的,函数可以通过给某个变量设置一个特殊值来获得锁。而其他需要锁的函数则会循环查询锁是否可用。MAX_TS_BUF的值为16,即在没有被读取之前,系统缓冲区中最多可以存放16个笔触数据信息。(关于自旋锁的作用和概念可以参考一篇《Linux内核的同步机制》文章的相关章节。)

      ------------------------------------------------------------------------
          模块初始化函数是调用s3c2410_ts_init 函数来实现的,主要完成触摸屏设备的内核模块加载、初始化系统I/O、中断注册、设备注册,为设备文件系统创建入口等标准的字符设备初始化工作。
      static int __init s3c2410_ts_init(void)
      ret = register_chrdev(0, DEVICE_NAME, &s3c2410_fops);
      tsMajor = ret;
      这里首先对字符设备进行注册,将触摸屏的file_operations 结构中的函数接口传入内核,注册成功后获得系统自动分配的主设备号。
      set_gpio_ctrl(GPIO_YPON);
      set_gpio_ctrl(GPIO_YMON);
      set_gpio_ctrl(GPIO_XPON);
      set_gpio_ctrl(GPIO_XMON);
      在/kernel/include/asm-arm/arch-s3c2410/smdk.h 文件中:
      #define GPIO_YPON (GPIO_MODE_nYPON | GPIO_PULLUP_DIS | GPIO_G15)
      #define GPIO_YMON (GPIO_MODE_YMON | GPIO_PULLUP_EN | GPIO_G14)
      #define GPIO_XPON (GPIO_MODE_nXPON | GPIO_PULLUP_DIS | GPIO_G13)
      #define GPIO_XMON (GPIO_MODE_XMON | GPIO_PULLUP_EN | GPIO_G12)
      GPIO 口的Port G 端口有4个管脚对应触摸屏的控制接口,分别是:
      GPG15 --- nYPON  Y+ 控制信号
      GPG14 --- YMON   Y- 控制信号
      GPG13 --- nXPON  X+ 控制信号
      GPG12 --- XMON   X- 控制信号
      需要通过配置GPGCON 寄存器来设定该端口管脚的输出模式,对应位如下:
      [31:30] [29:28] [27:26] [25:24]
      GPG15   GPG14   GPG13   GPG12   ...
      参考S3C2410 芯片datasheet 的I/O口章节,都要设为11(二进制)。
      ADCDLY = 30000;
      配置ADCDLY 寄存器,对自动转换模式来说是设定ADC 开始转换时的延时时间,以及对X轴和Y轴转换的时间间隔,对正常模式来说仅设定对X轴和Y轴转换的时间间隔。注意该值不能为0。

      在/kernel/arch/arm/kernel/irq.c 文件中:
      /**
      * request_irq - allocate an interrupt line
      * @irq: Interrupt line to allocate
      * @handler: Function to be called when the IRQ occurs
      * @irqflags: Interrupt type flags
      * @devname: An ascii name for the claiming device
      * @dev_id: A cookie passed back to the handler function
      *
      * This call allocates interrupt resources and enables the
      * interrupt line and IRQ handling. From the point this
      * call is made your handler function may be invoked. Since
      * your handler function must clear any interrupt the board
      * raises, you must take care both to initialise your hardware
      * and to set up the interrupt handler in the right order.
      *
      * Dev_id must be globally unique. Normally the address of the
      * device data structure is used as the cookie. Since the handler
      * receives this value it makes sense to use it.
      *
      * If your interrupt is shared you must pass a non NULL dev_id
      * as this is required when freeing the interrupt.
      *
      * Flags:
      *
      * SA_SHIRQ  Interrupt is shared
      *
      * SA_INTERRUPT  Disable local interrupts while processing
      *
      * SA_SAMPLE_RANDOM The interrupt can be used for entropy
      *
      */
      int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *),
         unsigned long irq_flags, const char * devname, void *dev_id)
      其中handler 函数指针的具体参数为:
      void (*handler)(int irq, void *dev_id, struct pt_regs *regs)
           函数request_irq 是Linux 系统中驱动程序注册中断的方法。irq 为所要申请的硬件中断号,handler 为系统所注册的中断处理子程序,irq_flags 为申请时的选项,devname 为指向设备名称的字符指针,dev_id 为申请时告诉系统的设备标识。若中断申请成功则返回0,失败则返回负值。
      ret = request_irq(IRQ_ADC_DONE, s3c2410_isr_adc, SA_INTERRUPT,
           DEVICE_NAME, s3c2410_isr_adc);
          调用该函数来进行A/D转换的中断注册,所要申请的硬件中断号为IRQ_ADC_DONE(62);系统所注册的中断处理子程序为s3c2410_isr_adc 函数;申请中断选项为SA_INTERRUPT,表示中断处理程序是快速处理程序,即快速处理程序运行时,所有中断都被屏蔽;设备名称定义为DEVICE_NAME,即"s3c2410-ts";而设备标识仍然用中断处理子程序代替。

      ret = request_irq(IRQ_TC, s3c2410_isr_tc, SA_INTERRUPT,
           DEVICE_NAME, s3c2410_isr_tc);
          接着继续调用该函数来进行触摸屏触摸的中断注册,所要申请的硬件中断号为IRQ_TC(61);系统所注册的中断处理子程序为s3c2410_isr_tc 函数;申请中断选项为SA_INTERRUPT,表示中断处理程序是快速处理程序,即快速处理程序运行时,所有中断都被屏蔽;设备名称定义为DEVICE_NAME,即"s3c2410-ts";而设备标识仍然用中断处理子程序代替。
      /* Wait for touch screen interrupts */
      wait_down_int();
      调用该宏函数来设置触摸屏为等待中断模式【笔按下产生中断】,具体定义如下:
      #define wait_down_int() { ADCTSC = DOWN_INT | XP_PULL_UP_EN | \
          XP_AIN | XM_HIZ | YP_AIN | YM_GND | \
          XP_PST(WAIT_INT_MODE); }
          用该宏函数来设置ADC 触摸屏控制寄存器,参考S3C2410 芯片datasheet 中关于触摸屏的章节,具体设置参数如下:
      DOWN_INT = 1<<8 * 0  该位保留且应该设为0 【笔按下或笔抬起中断信号控制位,设为0 表示笔按下产生中断信号】
      XP_PULL_UP_EN = 1<<3 * 0  上拉开关使能,设为0 表示XP 引脚上拉使能
      XP_AIN = 1<<4 * 1  选择nXPON 引脚输出值,设为1 表示nXPON 引脚输出1,则XP 引脚连接AIN[7] 引脚
      XM_HIZ = 1<<5 * 0  选择XMON 引脚输出值,设为0 表示XMON 引脚输出0,则XM 引脚为高阻态
      YP_AIN = 1<<6 * 1  选择nYPON 引脚输出值,设为1 表示nYPON 引脚输出1,则YP 引脚连接AIN[5] 引脚
      YM_GND = 1<<7 * 1  选择YMON 引脚输出值,设为1 表示YMON 引脚输出1,则YM 引脚为接地
      XP_PST(WAIT_INT_MODE); = 3  X坐标Y坐标手动测量设置,设为3 表示等待中断模式
      #ifdef CONFIG_DEVFS_FS
      devfs_ts_dir = devfs_mk_dir(NULL, "touchscreen", NULL);
      devfs_tsraw = devfs_register(devfs_ts_dir, "0raw", DEVFS_FL_DEFAULT,
         tsMajor, TSRAW_MINOR, S_IFCHR | S_IRUSR | S_IWUSR,
         &s3c2410_fops, NULL);
      #endif
          以上这两个函数在我总结的一篇《LCD驱动程序分析》中有较详细的介绍。
          这里调用了devfs_mk_dir 函数,在设备文件系统中创建了一个名为touchscreen 的目录,并返回一个带有目录结构的数据结构变量devfs_ts_dir。将该变量作为下一步devfs_register 函数的参数,该参数在调用设备文件系统注册清除函数devfs_unregister 时也要作为参数传入。
          调用devfs_register 函数后,会在刚才创建的touchscreen 目录下再创建一个名为0raw 的设备文件节点。该函数的参数中,DEVFS_FL_DEFAULT 为该函数的标志选项,tsMajor 为注册字符设备时系统自动分配的主设备号,TSRAW_MINOR(1)为次设备号,S_IFCHR | S_IRUSR | S_IWUSR 为默认的文件模式,&s3c2410_fops 为传入内核的触摸屏file_operations 结构中的函数接口,私有数据指针为空。返回一个devfs_handle_t 数据结构的变量devfs_tsraw,这会在调用设备文件系统注册清除函数devfs_unregister 时作为参数传入。

      ------------------------------------------------------------------------
          模块的退出函数为s3c2410_ts_exit,该函数的工作就是清除已注册的字符设备,中断以及设备文件系统。
      #ifdef CONFIG_DEVFS_FS
      devfs_unregister(devfs_tsraw);
      devfs_unregister(devfs_ts_dir);
      #endif
          这里首先清除原先后一步创建设备文件节点0raw 的结构变量devfs_tsraw,然后再清除创建touchscreen 目录的结构变量devfs_ts_dir。
      unregister_chrdev(tsMajor, DEVICE_NAME);
          接下来删除字符设备的注册信息。

      在/kernel/arch/arm/kernel/irq.c 文件中:
      /**
      * free_irq - free an interrupt
      * @irq: Interrupt line to free
      * @dev_id: Device identity to free
      *
      * Remove an interrupt handler. The handler is removed and if the
      * interrupt line is no longer in use by any driver it is disabled.
      * On a shared IRQ the caller must ensure the interrupt is disabled
      * on the card it drives before calling this function.
      *
      * This function may be called from interrupt context.
      */
      void free_irq(unsigned int irq, void *dev_id)
          函数free_irq 与函数request_irq 相对应,通常在模块被卸载时调用,负责注销一个已经申请的中断。

      free_irq(IRQ_ADC_DONE, s3c2410_isr_adc);
      free_irq(IRQ_TC, s3c2410_isr_tc);
          最后依次注销A/D转换和定时器这两个已经申请的中断。

      ------------------------------------------------------------------------
          接下来看一下A/D转换的中断处理函数:
      static void s3c2410_isr_adc(int irq, void *dev_id, struct pt_regs *reg)
          其中参数irq 为中断号,dev_id 为申请中断时告诉系统的设备标识,regs 为中断发生时寄存器内容。该函数在中断产生时由系统来调用,调用时以上参数已经由系统传入。

      在/kernel/include/linux/spinlock.h 文件中:
      /*
      * These are the generic versions of the spinlocks and read-write
      * locks..
      */
      #define spin_lock_irq(lock) do{local_irq_disable();spin_lock(lock);}while (0)
      #define spin_unlock_irq(lock) do{spin_unlock(lock);local_irq_enable();}while(0)

      #define DEBUG_SPINLOCKS 0 /* 0 == no debugging, 1 == maintain lock state, 2 == full debug */
      #if (DEBUG_SPINLOCKS < 1)
        typedef struct { } spinlock_t;
        #define SPIN_LOCK_UNLOCKED (spinlock_t) { }
      #define spin_lock_init(lock) do { } while(0)
      #define spin_lock(lock)  (void)(lock) /* Not "unused variable". */
      #define spin_unlock_wait(lock) do { } while(0)
      #define spin_unlock(lock) do { } while(0)
      可见上面这四个宏函数都是空函数,这样的话spin_lock_irq(lock)和spin_unlock_irq(lock)这两个宏函数就相当于分别只调用了local_irq_disable();和local_irq_enable();两个宏函数。关于自旋锁的作用和概念可以参考一篇《Linux内核的同步机制》文章的相关章节。

      在/kernel/include/asm-arm/system.h 文件中:
      #define local_irq_disable() __cli()
      #define local_irq_enable() __sti()
      在/kernel/include/asm-arm/proc-armo/system.h 文件中:
      /*
      * Enable IRQs
      */
      #define __sti()     \
      do {     \
         unsigned long temp;   \
         __asm__ __volatile__(   \
      " mov %0, pc  @ sti\n" \
      " bic %0, %0, #0x08000000\n"  \
      " teqp %0, #0\n"   \
         : "=r" (temp)    \
         :     \
         : "memory");    \
      } while(0)
      /*
      * Disable IRQs
      */
      #define __cli()     \
      do {     \
         unsigned long temp;   \
         __asm__ __volatile__(   \
      " mov %0, pc  @ cli\n" \
      " orr %0, %0, #0x08000000\n"  \
      " teqp %0, #0\n"   \
         : "=r" (temp)    \
         :     \
         : "memory");    \
      } while(0)

      最后用ARM 汇编指令实现了对IRQ 的使能和禁止。
      spin_lock_irq(&(tsdev.lock));
          这样调用spin_lock_irq 宏函数,实际上只是做了local_irq_disable();一步,就是禁止IRQ 中断。

      if (tsdev.penStatus == PEN_UP)
         s3c2410_get_XY();
          然后根据变量tsdev.penStatus 所处的状态,若为笔抬起则调用s3c2410_get_XY 函数来取得A/D转换得到的坐标值,该函数会在后面说明。
      #ifdef HOOK_FOR_DRAG
      else
         s3c2410_get_XY();
      #endif
          这里表示如果定义了笔拖曳,且在笔没有抬起的情况下,继续调用s3c2410_get_XY 函数来得到最新的坐标值。
      spin_unlock_irq(&(tsdev.lock));
          最后调用spin_unlock_irq 宏函数,相当于只做了local_irq_enable();一步,来重新使能IRQ 中断。最后退出这个中断服务子程序。

      ------------------------------------------------------------------------
          继续来看一下另一个中断处理函数,即触摸屏触摸中断处理函数:
      static void s3c2410_isr_tc(int irq, void *dev_id, struct pt_regs *reg)
          该函数的参数和上面A/D转换中断处理函数的定义一样,不再累赘。
      spin_lock_irq(&(tsdev.lock));

          也同上面的意思一样,首先禁止IRQ 中断。
      if (tsdev.penStatus == PEN_UP) {
         start_ts_adc();
      }
          接着根据变量tsdev.penStatus 的状态值判断是否进行A/D转换。若笔抬起,则调用函数start_ts_adc 来进行A/D转换,该函数会在后面说明。
      else {
         tsdev.penStatus = PEN_UP;
         DPRINTK("PEN UP: x: %08d, y: %08d\n", x, y);
         wait_down_int();
         tsEvent();
      }
          如果变量tsdev.penStatus 的状态值不是笔抬起,则先将该变量状态设为笔抬起,然后调用宏函数wait_down_int()。该宏函数已在前面说明,用来设置触摸屏为等待中断模式。最后调用tsEvent 函数指针所指的函数,在模块初始化函数s3c2410_ts_init 中,tsEvent 指向的是一个空函数tsEvent_dummy,而在打开设备函数s3c2410_ts_open 中,tsEvent 会指向tsEvent_raw 函数,该函数负责填充触摸屏缓冲区,并唤醒等待的进程。该函数也会在后面加以说明。
      spin_unlock_irq(&(tsdev.lock));
          中断处理函数的最后一步都一样,重新使能IRQ 中断。退出中断服务子程序。
      ------------------------------------------------------------------------
          下面先来看启动A/D转换的函数:
      static inline void start_ts_adc(void)
      adc_state = 0;
      mode_x_axis();
      start_adc_x();
          简简单单的3步。
          第一步,对A/D转换的状态变量清零。
          第二步,调用mode_x_axis 宏函数,具体定义如下:
      #define mode_x_axis() { ADCTSC = XP_EXTVLT | XM_GND | YP_AIN | YM_HIZ | \
          XP_PULL_UP_DIS | XP_PST(X_AXIS_MODE); }

      //*******************************************************
      //* 2007.6.27
      //*******************************************************
          该宏函数用来设置ADC触摸屏控制寄存器为测量X坐标模式,参考S3C2410 芯片datasheet 中关于触摸屏的章节,具体设置参数如下:
      XP_EXTVLT = 1<<4 * 0  选择nXPON 引脚输出值,设为0 表示nXPON 引脚输出0,则XP 引脚为接外部电压
      XM_GND = 1<<5 * 1  选择XMON 引脚输出值,设为1 表示XMON 引脚输出1,则XM 引脚为接地
      YP_AIN = 1<<6 * 1  选择nYPON 引脚输出值,设为1 表示nYPON 引脚输出1,则YP 引脚连接AIN[5] 引脚
      YM_HIZ = 1<<7 * 0  选择YMON 引脚输出值,设为0 表示YMON 引脚输出0,则YM 引脚为高阻态
      XP_PULL_UP_DIS = 1<<3 * 1  上拉开关使能,设为1 表示XP 引脚上拉禁止
      XP_PST(X_AXIS_MODE); = 1  X坐标Y坐标手动测量设置,设为1 表示X坐标测量模式
          第三步,调用start_adc_x 宏函数,具体定义如下:
      #define start_adc_x() { ADCCON = PRESCALE_EN | PRSCVL(49) | \
          ADC_INPUT(ADC_IN5) | ADC_START_BY_RD_EN | \
          ADC_NORMAL_MODE; \
           ADCDAT0; }
          该宏函数用来设置ADC控制寄存器启动X坐标的A/D转换,参考S3C2410 芯片datasheet 中关于触摸屏的章节,具体设置参数如下:
      PRESCALE_EN = 1<<14 * 1  A/D转换器使能,设为1 表示使能A/D转换器
      PRSCVL(49) = 49<<6  A/D转换器值,设为49
      ADC_INPUT(ADC_IN5) = 5<<3  选择模拟输入通道,设为5 表示AIN[5] 引脚作为模拟输入通道
      ADC_START_BY_RD_EN = 1<<1 * 1  A/D转换通过读启动,设为1 表示通过读操作启动A/D转换使能
      ADC_NORMAL_MODE; = 1<<2 * 0  选择待命模式,设为0 表示正常操作模式
      ADCDAT0;  读取X坐标的ADC转换数据寄存器
          由于设置了A/D转换通过读启动,则该ADCCON 寄存器的最低位ENABLE_START 启动A/D转换位就无效了。在最后一步读取ADCDAT0 寄存器这一操作时就启动了A/D转换。

      ------------------------------------------------------------------------
      static inline void s3c2410_get_XY(void)
          这就是获取A/D转换所得到的坐标值的函数。
      if (adc_state == 0)
      {
        adc_state = 1;
        disable_ts_adc();
        y = (ADCDAT0 & 0x3ff);
        mode_y_axis();
        start_adc_y();
      }
          这里首先查看A/D转换的状态变量,若为0 表示进行过X坐标的A/D转换,将该变量设为1。然后调用宏函数disable_ts_adc,该宏函数定义如下:
      #define disable_ts_adc() { ADCCON &= ~(ADCCON_READ_START); }
          这个宏函数主要工作就是禁止通过读操作启动A/D转换,参考S3C2410 芯片datasheet 中关于触摸屏的章节,具体设置参数如下:
      ADCCON_READ_START = 1<<1  A/D转换通过读启动,设为0 表示通过读操作启动A/D转换禁止
          然后y = (ADCDAT0 & 0x3ff); 这一步将X坐标的ADC转换数据寄存器的D9~D0 这10为读出到变量y(这里由于是竖屏,参考原理图后知道,硬件连线有过改动,将XP,XM 和YP,YM 进行了对换,这样ADCDAT0 里读出的是YP,YM 方向电阻导通的值,也就是y轴坐标值)。这个mode_y_axis 宏函数定义如下:
      #define mode_y_axis() { ADCTSC = XP_AIN | XM_HIZ | YP_EXTVLT | YM_GND | \
          XP_PULL_UP_DIS | XP_PST(Y_AXIS_MODE); }
          该宏函数用来设置ADC触摸屏控制寄存器为测量Y坐标模式,参考S3C2410 芯片datasheet 中关于触摸屏的章节,具体设置参数如下:
      XP_AIN = 1<<4 * 1  选择nXPON 引脚输出值,设为1 表示nXPON 引脚输出1,则XP 引脚连接AIN[7] 引脚
      XM_HIZ = 1<<5 * 0  选择XMON 引脚输出值,设为0 表示XMON 引脚输出0,则XM 引脚为高阻态
      YP_EXTVLT = 1<<6 * 0  选择nYPON 引脚输出值,设为0 表示nYPON 引脚输出0,则YP 引脚为接外部电压
      YM_GND = 1<<7 * 1  选择YMON 引脚输出值,设为1 表示YMON 引脚输出1,则YM 引脚为接地
      XP_PULL_UP_DIS = 1<<3 * 1  上拉开关使能,设为1 表示XP 引脚上拉禁止
      XP_PST(Y_AXIS_MODE); = 2  X坐标Y坐标手动测量设置,设为2 表示Y坐标测量模式
          最后调用start_adc_y 宏函数,具体定义如下:
      #define start_adc_y() { ADCCON = PRESCALE_EN | PRSCVL(49) | \
          ADC_INPUT(ADC_IN7) | ADC_START_BY_RD_EN | \
          ADC_NORMAL_MODE; \
           ADCDAT1; }
         该宏函数用来设置ADC控制寄存器启动Y坐标的A/D转换,参考S3C2410 芯片datasheet 中关于触摸屏的章节,具体设置参数如下:
      PRESCALE_EN = 1<<14 * 1  A/D转换器使能,设为1 表示使能A/D转换器
      PRSCVL(49) = 49<<6  A/D转换器值,设为49
      ADC_INPUT(ADC_IN7) = 7<<3  选择模拟输入通道,设为7 表示AIN[7] 引脚作为模拟输入通道
      ADC_START_BY_RD_EN = 1<<1 * 1  A/D转换通过读启动,设为1 表示通过读操作启动A/D转换使能
      ADC_NORMAL_MODE; = 1<<2 * 0  选择待命模式,设为0 表示正常操作模式
      ADCDAT1;  读取Y坐标的ADC转换数据寄存器

      else if (adc_state == 1)
      {
        adc_state = 0;
        disable_ts_adc();
        x = (ADCDAT1 & 0x3ff);
        tsdev.penStatus = PEN_DOWN;
        DPRINTK("PEN DOWN: x: %08d, y: %08d\n", x, y);
        wait_up_int();
        tsEvent();
      }
          若查看A/D转换的状态变量,若为1 表示进行过Y坐标的A/D转换,将该变量设为0。然后调用宏函数disable_ts_adc 来禁止通过读操作启动A/D转换。
          接着将x = (ADCDAT1 & 0x3ff); 这一步将Y坐标的ADC转换数据寄存器的D9~D0 这10为读出到变量x(这里由于是竖屏,参考原理图后知道,硬件连线有过改动,将XP,XM 和YP,YM 进行了对换,这样ADCDAT1 里读出的是XP,XM 方向电阻导通的值,也就是x轴坐标值)。
          随后将变量tsdev.penStatus 的状态值改为笔按下,并调用wait_up_int 宏函数来设置触摸屏为等待中断模式【笔抬起产生中断】,具体定义如下:
      #define wait_up_int() { ADCTSC = UP_INT | XP_PULL_UP_EN | XP_AIN | XM_HIZ | \
          YP_AIN | YM_GND | XP_PST(WAIT_INT_MODE); }
          用该宏函数来设置ADC 触摸屏控制寄存器,参考S3C2410 芯片datasheet 中关于触摸屏的章节,具体设置参数如下:
      UP_INT = 1<<8 * 1  该位保留且应该设为0,这里设为1 不知道为什么 【笔按下或笔抬起中断信号控制位,设为1 表示笔抬起产生中断信号】
      XP_PULL_UP_EN = 1<<3 * 0  上拉开关使能,设为0 表示XP 引脚上拉使能
      XP_AIN = 1<<4 * 1  选择nXPON 引脚输出值,设为1 表示nXPON 引脚输出1,则XP 引脚连接AIN[7] 引脚
      XM_HIZ = 1<<5 * 0  选择XMON 引脚输出值,设为0 表示XMON 引脚输出0,则XM 引脚为高阻态
      YP_AIN = 1<<6 * 1  选择nYPON 引脚输出值,设为1 表示nYPON 引脚输出1,则YP 引脚连接AIN[5] 引脚
      YM_GND = 1<<7 * 1  选择YMON 引脚输出值,设为1 表示YMON 引脚输出1,则YM 引脚为接地
      XP_PST(WAIT_INT_MODE); = 3  X坐标Y坐标手动测量设置,设为3 表示等待中断模式
          最后调用函数指针tsEvent 所指向的函数。在s3c2410_get_XY 函数里面,应该表示这个驱动的设备文件已经打开,在打开设备文件函数中,tsEvent 函数指针就指向了tsEvent_raw 这个函数,也就是说,下面执行的是tsEvent_raw 函数。tsEvent_raw 函数负责填充触摸屏缓冲区,并唤醒等待的进程,该函数会在后面说明。

      ------------------------------------------------------------------------
      static void tsEvent_raw(void)
          来看一下tsEvent_raw 这个函数。
      if (tsdev.penStatus == PEN_DOWN)
      {
        BUF_HEAD.x = x;
        BUF_HEAD.y = y;
        BUF_HEAD.pressure = PEN_DOWN;
         
          一上来就根据变量tsdev.penStatus 的状态值进行判断,若为笔按下,将从A/D转换器中采集的x轴y轴坐标以及笔按下的状态存入变量tsdev 中的buf 成员中的相应变量中。

      #ifdef HOOK_FOR_DRAG
        ts_timer.expires = jiffies + TS_TIMER_DELAY;
        add_timer(&ts_timer);
      #endif
          如果定义了笔拖曳,先将定时器的定时溢出值更新,然后调用add_timer 函数重新增加定时器计时,变量ts_timer 为struct timer_list 数据结构。
      }
      else
      {
      #ifdef HOOK_FOR_DRAG
        del_timer(&ts_timer);
      #endif

          如果定义了笔拖曳,调用del_timer 函数来删除定时器,变量ts_timer 为struct timer_list 数据结构。

        BUF_HEAD.x = 0;
        BUF_HEAD.y = 0;
        BUF_HEAD.pressure = PEN_UP;
      }
          若变量tsdev.penStatus 的状态值不是笔按下,则x轴y轴坐标写为0和笔抬起的状态一起存入变量tsdev 中的buf 成员中的相应变量中。
      tsdev.head = INCBUF(tsdev.head, MAX_TS_BUF);
          其中INCBUF 宏函数定义如下:
      #define INCBUF(x,mod)  ((++(x)) & ((mod) - 1))
      由于这里MAX_TS_BUF=16,这样(mod) - 1)就为15(0x0F),所以这个宏函数相当于就是将变量tsdev.head 这个表示buf 头位置的值加1,然后取模16,即指向下一个buf ,形成一个在环形缓冲区上绕环的头指针。
      在/kernel/include/linux/sched.h 文件中:
      #define wake_up_interruptible(x) __wake_up((x),TASK_INTERRUPTIBLE, 1)
      该宏函数定义为__wake_up 函数,参数TASK_INTERRUPTIBLE 为1,表示要唤醒的任务的状态为中断模式,参数1 表示要唤醒的互斥进程数目为1。
      对应的唤醒操作包括wake_up_interruptible和wake_up。wake_up函数不仅可以唤醒状态为TASK_UNINTERRUPTIBLE的进程,而且可以唤醒状态为TASK_INTERRUPTIBLE的进程。wake_up_interruptible只负责唤醒状态为TASK_INTERRUPTIBLE的进程。关于interruptible_sleep_on 和wake_up_interruptible 函数详细的用法可以参考一篇《关于linux内核中等待队列的问题》文档。
      在/kernel/kernel/sched.c 文件中:
      void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr)
      {
      if (q) {
        unsigned long flags;
        wq_read_lock_irqsave(&q->lock, flags);
        __wake_up_common(q, mode, nr, 0);
        wq_read_unlock_irqrestore(&q->lock, flags);
      }
      }
      宏函数wq_read_lock_irqsave 的作用主要就是保存IRQ 和FIQ 的中断使能状态,并禁止IRQ 中断;而宏函数wq_read_unlock_irqrestore 的作用就是恢复IRQ 和FIQ 的中断使能状态。现在可以得知__wake_up 这个函数的作用,它首先保存IRQ 和FIQ 的中断使能状态,并禁止IRQ 中断,接着调用__wake_up_common 函数来唤醒等待q 队列的进程,最后再恢复IRQ 和FIQ 的中断使能状态。
      /*
      * The core wakeup function.  Non-exclusive wakeups (nr_exclusive == 0) just wake everything
      * up.  If it''''s an exclusive wakeup (nr_exclusive == small +ve number) then we wake all the
      * non-exclusive tasks and one exclusive task.
      *
      * There are circumstances in which we can try to wake a task which has already
      * started to run but is not in state TASK_RUNNING.  try_to_wake_up() returns zero
      * in this (rare) case, and we handle it by contonuing to scan the queue.
      */
      static inline void __wake_up_common (wait_queue_head_t *q, unsigned int mode,
                int nr_exclusive, const int sync)
      该函数的作用是唤醒在等待当前等待队列的进程。参数q 表示要操作的等待队列,mode 表示要唤醒任务的状态,如TASK_UNINTERRUPTIBLE 或TASK_INTERRUPTIBLE 等。nr_exclusive 是要唤醒的互斥进程数目,在这之前遇到的非互斥进程将被无条件唤醒。sync表示???

      在/kernel/include/linux/wait.h 文件中:
      struct __wait_queue_head {
      wq_lock_t lock;
      struct list_head task_list;
      #if WAITQUEUE_DEBUG
      long __magic;
      long __creator;
      #endif
      };
      typedef struct __wait_queue_head wait_queue_head_t;
      这是等待队列数据结构。
      # define wq_read_lock_irqsave spin_lock_irqsave
      # define wq_read_unlock_irqrestore spin_unlock_irqrestore
      看到这里可以知道其实宏函数wq_read_lock_irqsave 和wq_read_unlock_irqrestore 等价于宏函数spin_lock_irqsave 和spin_unlock_irqrestore,并直接将自己的参数传了下去。

      在/kernel/include/linux/spinlock.h 文件中:
      /*
      * These are the generic versions of the spinlocks and read-write
      * locks..
      */
      #define spin_lock_irqsave(lock, flags)  do {local_irq_save(flags);spin_lock(lock);}while (0)
      #define spin_unlock_irqrestore(lock, flags)  do {spin_unlock(lock);  local_irq_restore(flags); } while (0)
      在这两个宏函数中,前面已经提到spin_lock 和spin_unlock 其实都为空函数,那么实际只执行了local_irq_save 和local_irq_restore 这两个宏函数。

      在/kernel/include/asm-arm/system.h 文件中:
      /* For spinlocks etc */
      #define local_irq_save(x) __save_flags_cli(x)
      #define local_irq_restore(x) __restore_flags(x)
      这里local_irq_save 和local_irq_restore 这两个宏函数又分别等价于__save_flags_cli 和__restore_flags 这两个宏函数。

      在/kernel/include/asm-arm/proc-armo/system.h 文件中:
      /*
      * A couple of speedups for the ARM
      */
      /*
      * Save the current interrupt enable state & disable IRQs
      */
      #define __save_flags_cli(x)    \
      do {      \
         unsigned long temp;    \
         __asm__ __volatile__(    \
      " mov %0, pc  @ save_flags_cli\n" \
      " orr %1, %0, #0x08000000\n"   \
      " and %0, %0, #0x0c000000\n"   \
      " teqp %1, #0\n"    \
         : "=r" (x), "=r" (temp)   \
         :      \
         : "memory");     \
      } while (0)

      /*
      * restore saved IRQ & FIQ state
      */
      #define __restore_flags(x)    \
      do {      \
         unsigned long temp;    \
         __asm__ __volatile__(    \
      " mov %0, pc  @ restore_flags\n" \
      " bic %0, %0, #0x0c000000\n"   \
      " orr %0, %0, %1\n"    \
      " teqp %0, #0\n"    \
         : "=&r" (temp)    \
         : "r" (x)     \
         : "memory");     \
      } while (0)
      最后用ARM 汇编指令实现了对当前程序状态寄存器CPSR 中的IRQ 和FIQ 中断使能状态的保存和恢复。而且在__save_flags_cli 宏函数中,除了对IRQ 和FIQ 中断使能状态的保存外,还禁止了IRQ 中断。

      wake_up_interruptible(&(tsdev.wq));
          在这个tsEvent_raw 函数最后,调用wake_up_interruptible 函数来以中断模式唤醒等待tsdev.wq 队列的进程。
        
      ------------------------------------------------------------------------
          由于上面这个wake_up_interruptible 函数的作用还不是很明确,所以需要分析一下打开设备文件这个函数。触摸屏打开设备文件函数定义如下:
      static int s3c2410_ts_open(struct inode *inode, struct file *filp)
      tsdev.head = tsdev.tail = 0;
      tsdev.penStatus = PEN_UP;
         
          首先是最简单的将变量tsdev.head 和tsdev.tail 这两个表示buf 头尾位置的值清零,然后将变量tsdev.penStatus 的状态值初始化为笔抬起。
      #ifdef HOOK_FOR_DRAG
      init_timer(&ts_timer);
      ts_timer.function = ts_timer_handler;
      #endif
          如果定义了笔拖曳,先调用init_timer 函数来初始化一个定时器,变量ts_timer 为struct timer_list 数据结构,然后将定时调用的函数指针指向ts_timer_handler 函数。
      tsEvent = tsEvent_raw;
          将函数指针tsEvent 指向tsEvent_raw 函数。

      在/kernel/include/linux/wait.h 文件中:
      static inline void init_waitqueue_head(wait_queue_head_t *q)
      {
      #if WAITQUEUE_DEBUG
      if (!q)
        WQ_BUG();
      #endif
      q->lock = WAITQUEUE_RW_LOCK_UNLOCKED;
      INIT_LIST_HEAD(&q->task_list);
      #if WAITQUEUE_DEBUG
      q->__magic = (long)&q->__magic;
      q->__creator = (long)current_text_addr();
      #endif
      }
      该函数初始化一个已经存在的等待队列头,它将整个队列设置为"未上锁"状态,并将链表指针prev和next指向它自身。
      init_waitqueue_head(&(tsdev.wq));
          在这个s3c2410_ts_open 函数中,调用init_waitqueue_head 函数来初始化一个定义在变量tsdev 中的等待队列头的成员结构。
      MOD_INC_USE_COUNT;
      return 0;
        
          最后调用MOD_INC_USE_COUNT; 来对设备文件计数器加一计数,并返回。

      ------------------------------------------------------------------------
          再来分析一下用户层要调用的读取设备文件的接口函数:
      static ssize_t s3c2410_ts_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
          这个函数实现的任务是将事件队列从设备缓存中读到用户空间的数据缓存中。实现的过程主要是通过一个循环,只有在事件队列的头、尾指针不重合时,才能成功的从tsdev.tail指向的队列尾部读取到一组触摸信息数据,并退出循环。否则调用读取函数的进程就要进入睡眠。

      TS_RET ts_ret;
      retry:
      if (tsdev.head != tsdev.tail)
      {
        int count;
        count = tsRead(&ts_ret);
        if (count) copy_to_user(buffer, (char *)&ts_ret, count);
        return count;
      }
          这个函数中,首先通过变量tsdev.head 和tsdev.tail 是否相等来判断环形缓冲区是否为空。若不相等则表示环形缓冲区中有触摸屏数据,调用tsRead 函数将触摸屏数据读入TS_RET 数据结构的ts_ret 变量中,该函数会在后面说明。
          接下来调用copy_to_user 函数来把内核空间的数据复制到用户空间,在这里就是把驱动程序里的变量ts_ret 中的数据复制到用户程序的buffer 中,然后返回复制的数据长度。

      else
      {
        if (filp->f_flags & O_NONBLOCK)
         return -EAGAIN;
        interruptible_sleep_on(&(tsdev.wq));
        if (signal_pending(current))
         return -ERESTARTSYS;
        goto retry;
      }
          若变量tsdev.head 和tsdev.tail 相等,则表示环形缓冲区为空,首先根据file->f_flags 与上O_NONBLOCK 值来进行判断,若为O_NONBLOCK 值,则表示采用非阻塞的文件IO方法,立即返回,否则才会调用interruptible_sleep_on 函数,调用该函数的进程将会进入睡眠,直到被唤醒。关于O_NONBLOCK 值的含义在我总结的《IIS音频驱动程序分析》一文中有详细说明。
         
      在/kernel/kernel/sched.c 文件中:
      void interruptible_sleep_on(wait_queue_head_t *q)
      {
      SLEEP_ON_VAR
      current->state = TASK_INTERRUPTIBLE;
      SLEEP_ON_HEAD
      schedule();
      SLEEP_ON_TAIL
      }
      常用的睡眠操作有interruptible_sleep_on和sleep_on。两个函数类似,只不过前者将进程的状态从就绪态(TASK_RUNNING)设置为TASK_INTERRUPTIBLE,允许通过发送signal唤醒它(即可中断的睡眠状态);而后者将进程的状态设置为TASK_UNINTERRUPTIBLE,在这种状态下,不接收任何singal。关于interruptible_sleep_on 和wake_up_interruptible 函数详细的用法可以参考一篇《关于linux内核中等待队列的问题》文档。
          如果进程被唤醒,则会继续跳到retry: 循环读取环形缓冲区,直到读取到一组触摸信息数据才会退出。

      ------------------------------------------------------------------------
      static int tsRead(TS_RET * ts_ret)
          这个函数主要将环形缓冲区的x轴y轴坐标数据和笔的状态数据传入该函数形式参数所指的指针变量中。
      spin_lock_irq(&(tsdev.lock));
          上文已经解释过了,调用该宏函数来禁止IRQ 中断。
      ts_ret->x = BUF_TAIL.x;
      ts_ret->y = BUF_TAIL.y;
      ts_ret->pressure = BUF_TAIL.pressure;
      tsdev.tail = INCBUF(tsdev.tail, MAX_TS_BUF);
          接着把变量tsdev 的环形缓冲区中相关数据赋值给该函数形式参数所指的指针变量中,并将表示环形缓冲区队列尾部的变量tsdev.tail 加一,这就意味着从环形队列中读取一组数据,尾指针加一。
      spin_unlock_irq(&(tsdev.lock));
      return sizeof(TS_RET);
          上文已经解释过了,调用该宏函数来重新使能IRQ 中断,然后返回。

      ------------------------------------------------------------------------
          再来看一个定时器定时调用的函数:
      #ifdef HOOK_FOR_DRAG
      static void ts_timer_handler(unsigned long data)
      {
      spin_lock_irq(&(tsdev.lock));
      if (tsdev.penStatus == PEN_DOWN) {
        start_ts_adc();
      }
      spin_unlock_irq(&(tsdev.lock));
      }
      #endif
          这个函数需要定义过笔拖曳才有效。首先调用宏函数spin_lock_irq 来禁止IRQ 中断。
          然后在变量tsdev.penStatus 状态为笔按下的时候,调用宏函数start_ts_adc 来启动A/D转换,转换的是X轴的坐标。
          最后再调用宏函数spin_unlock_irq 来重新使能IRQ 中断。

      //*******************************************************
      //* 2007.6.28
      //*******************************************************
          最后再来看一个释放设备文件的函数:
      static int s3c2410_ts_release(struct inode *inode, struct file *filp)
      #ifdef HOOK_FOR_DRAG
      del_timer(&ts_timer);
      #endif
      MOD_DEC_USE_COUNT;
      return 0;
          其实也很简单,在定义了笔拖曳的情况下,调用del_timer 函数来删除定时器,变量ts_timer 为struct timer_list 数据结构,然后调用MOD_DEC_USE_COUNT; 将设备文件计数器减一计数,并返回。

      ------------------------------------------------------------------------
          经过对整个触摸屏驱动程序的流程,以及S3C2410 芯片数据手册里的相关章节进行分析后,下面来总结一下触摸屏驱动程序的大致流程。
          首先在驱动模块初始化函数中,除了对驱动的字符设备的注册外,还要对中断进行申请。这里申请了两个触摸屏相关的中断,一个是IRQ_TC 中断,查阅了数据手册后了解到,该中断在笔按下时,由XP 管脚产生表示中断的低电平信号,而笔抬起是没有中断信号产生的。另一个是IRQ_ADC_DONE 中断,该中断是当芯片内部A/D转换结束后,通知中断控制器产生中断,这时就可以去读取转换得到的数据。
          当触摸屏按下后,就会出发中断,这时会调用申请中断时附带的s3c2410_isr_tc 中断回调函数,该函数中判断若为笔抬起则启动x轴坐标的A/D转换。当转换完毕后就会产生ADC中断,这时就会调用申请中断时附带的s3c2410_isr_adc 中断回调函数,在该函数中进行判断,若x轴坐标转换结束马上进行y轴坐标的A/D转换转换;若y轴坐标转换结束,则重新回到等待中断模式,然后将坐标值写入环形缓冲区,并环形等待队列中的进程。

      //*******************************************************
      //* 2007.6.29
      //*******************************************************
          昨天晚上经过触摸屏驱动的调试,看出在S3C2410 的datasheet 中有一个问题。关于触摸屏中的ADC 触摸屏控制寄存器ADCTSC 的第8位,在原来的datasheet 中说该为保留,应设为0。而实际调试下来,该位是有功能的,参考了S3C2440 的datasheet 后得知ADCTSC 寄存器的第8位是笔按下或抬起的中断信号控制位,该位设为0,笔按下产生中断信号,该位设为1,笔抬起产生中断信号。经过测试,确实在笔按下和抬起时都会产生中断,并两次调用了s3c2410_isr_tc 中断回调函数。
          现在要对前面涉及到ADCTSC 寄存器配置部分的说明做些改动,用“【 】”加以区分。