1. 程式人生 > >Llinux-觸控式螢幕驅動(詳解)

Llinux-觸控式螢幕驅動(詳解)

https://www.cnblogs.com/lifexy/p/7628889.html

https://forum.qt.io/topic/57756/solved-proper-configuration-of-capacitive-touchscreen-with-qt5-and-eglfs/3 

1.先來回憶之前第12節分析的輸入子系統

其中輸入子系統層次如下圖所示,

 

其中事件處理層的函式都是通過input_register_handler()函式註冊到input_handler_list連結串列中

搜尋input_register_handler註冊函式,就可以看到都是事件處理層裡的函式:

所以最終如下圖所示:

 

 

 

右邊的驅動事件處理,核心是已經寫好了的,所以我們的觸控式螢幕只需要寫具體的驅動裝置,然後核心會與觸控式螢幕驅動tsdev.c自動連線    

2.本節需要用到的結構體成員如下:

複製程式碼
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 ... ...
複製程式碼

 

3.本節需要用到的函式:

複製程式碼
struct input_dev *input_allocate_device(void);  //向記憶體中分配input_dev結構體

input_free_device(struct input_dev *dev);   //釋放記憶體中的input_dev結構體

input_register_device(struct input_dev *dev);   //註冊一個input_dev,若有對應的驅動事件,
則在/sys/class/input下建立這個類裝置

input_unregister_device(struct input_dev *dev);   //解除安裝/sys/class/input目錄下的
input_dev這個類裝置

 
set_bit(nr,p);                  //設定某個結構體成員p裡面的某位等於nr,支援這個功能
/* 比如:
set_bit(EV_KEY,buttons_dev->evbit);   //設定input_dev結構體buttons_dev->evbit支援EV_KEY
set_bit(KEY_S,buttons_dev->keybit);  //設定input_dev結構體buttons_dev->keybit支援按鍵”S”

*/

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方向的平焊位置
 

input_report_abs(struct input_dev *dev, unsigned int code, int value);   
//上報EV_ABS事件
//該函式實際就是呼叫的input_event(dev, EV_ABS, code, value);
//*dev :要上報哪個input_dev驅動裝置的事件
// code: EV_ABS事件裡支援的哪個方向,比如X座標方向則填入: ABS_X
//value:對應的方向的值,比如X座標126

input_report_key(struct input_dev *dev, unsigned int code, int value);  
//上報EV_KEY事件 
 
input_sync(struct input_dev *dev); //同步事件通知,通知系統有事件上報
 
struct  clk *clk_get(struct device *dev, const char *id);    
//獲得*id模組的時鐘,返回一個clk結構體
//*dev:填0即可,     *id:模組名字, 比如"adc","i2c"等,名字定義在clock.c中

clk_enable(struct clk *clk);   
//開啟clk_get()到的模組時鐘,就是使能CLKCON暫存器的某個模組的位
複製程式碼

4.電阻式觸控式螢幕介紹:

如下圖所示,2440開發板使用的是4線觸控式螢幕,該4線連線在2440的AIN4~AIN7引腳上,該引腳專門是用來接收模擬輸入訊號.

 

引腳說明:

YM: (Y Minus)觸控式螢幕的Y座標的負線,也可以用Y -表示

YP : (Y Power)觸控式螢幕的Y座標的正線, 也可以用Y+表示

XM: (Y Minus)觸控式螢幕的Y座標的負線, 也可以用X-表示

XP : (Y Power)觸控式螢幕的Y座標的正線, 也可以用X+表示

4.1  4線觸控式螢幕包含了兩個阻性層,如下圖所示:

 

 

當沒有觸控按下時,X層和Y層是分離的,此時就測不到電壓

4.2 測X座標方向時:

如下圖,  把XP接3.3V , XM接0V, YP和YM懸空,我們以按壓X座標的中間位置, X層和Y層便閉合了,此時YP就會輸出當前X座標值的1.66V給CPU 

 

4.3 測Y座標方向時:

如下圖, 把YP接3.3V , YM接0V, XP和XM懸空,我們以按壓X座標的中間位置, X層和Y層便閉合了,此時XP就會輸出當前X座標值的1.66V給CPU 

 

 

5.接下來開始看2440手冊

如下圖,2440的ADC解析度為10位(0~0X3FFF)

 

如下圖,若工作在普通ADC模式,則通過暫存器ADCCON->SEL_MUX來選擇轉換哪個引腳的模擬訊號

當設定為ADC等待中斷模式時,測到有螢幕筆尖觸控,就會產生INT_TC中斷

其中ADC的工作頻率最大為2.5MHZ,需要設定暫存器ADCCON->PRSCVL更改分頻係數

 

5.1 獲取筆尖觸控按下/鬆開使用的是ADC等待中斷模式:
當筆尖落下時觸控式螢幕控制器產生中斷(INT_TC)訊號。需要設定暫存器ADCTSC=0xd3/0x1d3 

設定暫存器ADCTSC=0x0d3/0x1d3 (X 1101 0011)時(如下圖):

開啟 YM開關,使能XP上拉, 開啟等待中斷模式

當有筆尖按下時,X層和Y層閉合,然後會拉低XP和XM電平,輸出低電平

設定為0x0d3是檢測觸控低電平, 設定為0x1d3是檢測觸控上拉電平

(PS:  ADCDAT0的bit15位用來標誌筆尖是按下還是鬆開)

 

5.2 獲取XY座標時使用的是自動 X/Y 方向轉換模式

當ADC轉換成功,  X 座標值到 ADCDAT0 和 Y 座標值到ADCDAT1 後,就會產生INT_ADC中斷

自動獲取XY座標時(如下圖):

設定暫存器ADCTSC=0X0C (關閉XP上拉、啟動自動XY方向轉換)

設定暫存器ADCCON的位[0]=1(開啟一次ADC轉換,當ADC轉換成功該位清0)

 

6.編寫程式碼

步驟如下:

6.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中斷

6.2 在出口函式中:

1)登出核心裡的input_dev、

2)釋放中斷、刪除定時器、iounmap登出地址、

3)釋放input_dev、

6.3 在IRQ_TC中斷函式中:

1)若判斷筆尖為鬆開,設定暫存器ADCTSC =0XD3(按下中斷)

2)若判斷筆尖按下,設定為XY自動轉換模式,啟動一次ADC轉換,ADC轉換成功,會進入ADC中斷   

6.4 在IRQ_ADC中斷函式中:

1)獲取ADCDAT0的位[9:0],來算出XY方向座標值

2)測量n次值儲存在陣列中,然後再次設定為XY自動轉換模式,啟動ADC

(PS:要啟動ADC轉換之前必須設定一次XY為自動轉換模式,不然獲取的資料會不準)

3)採集完畢,使用快速排序將n次值排序後,以最小值為基準,如有誤差非常大的數,則捨棄,如果沒有則列印陣列的中間值,實現中值濾波。

(PS: 使用快速排序,比冒泡更快,詳解:http://www.cnblogs.com/lifexy/p/7597276.html )

4)列印資料後,必須設定暫存器ADCTSC =0X1D3(鬆開中斷IRQ_TC)

(PS:在ADC取樣模式下是判斷不到ADCDAT0的bit15位的,因為ADCDAT0已被自動設定為X座標的取樣值)

5)設定定時器10ms超時時間

6.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");
複製程式碼

 

7.測試執行

7.1 重新設定編譯核心(去掉預設的觸控式螢幕驅動)

make menuconfig ,進入menu選單重新設定核心引數:

進入Device Drivers-> Input device support -> Touchscreens ->  

< >   S3C2410/S3C2440 touchscreens     //將自帶的觸控式螢幕驅動去掉, 不編進核心和模組

然後make uImage 編譯核心

將新的觸控式螢幕驅動模組放入nfs檔案系統目錄中

7.2然後燒寫核心,裝載觸控式螢幕驅動模組

如下圖, 通過 ls -l /dev/event* 命令可以看到我們的觸控式螢幕驅動的裝置為event0

 

7.3 測試執行:

如下圖所示,可以看到在同一個點按下時,變化都一致,沒有誤差

 

有了定時器後,也能支援滑動功能, 如下圖滑動Y方向:

 

此時的驅動只是列印資料,並沒有上報EV_KYE事件和EV_ABS事件

8.新增上報事件

設定上報事件之前還要刪除printk列印資訊,步驟如下:

8.1 在IRQ_TC中斷函式濾波處,新增:

input_report_abs(ts_dev, ABS_X, adc_x[2]);   //上報X方向值
input_report_abs(ts_dev, ABS_X, adc_y[2]);   //上報Y方向值
input_report_abs(ts_dev, ABS_PRESSURE, 1);  //上報壓力方向值
input_report_key(ts_dev,BTN_TOUCH,1);      //上報BTN_TOUCH按鍵值按下
input_sync(ts_dev);                                   //上報同步事件,通知系統有事件上報

8.2 在(ADCDAT0的bit15位==1)觸控鬆開處,新增:

input_report_abs(ts_dev, ABS_PRESSURE, 0);  //上報壓力值為0
//(PS:必須要上報一次壓力值,否則壓力值會一直為1,會影響tslib的測試執行)

input_report_key(ts_dev,BTN_TOUCH,0);       //上報BTN_TOUCH按鍵值鬆開
input_sync(ts_dev);              //上報同步事件,通知系統有事件上報

9.測試執行:

如下圖, 通過 ls -l /dev/event* 命令可以看到我們的觸控式螢幕驅動的裝置為event0

 

9.1使用hexdump命令來除錯程式碼

(hexdump命令除錯程式碼詳解地址:http://www.cnblogs.com/lifexy/p/7553550.html)

測試效果如下:

 

(PS:必須要保證有ABS_X、ABS_Y、壓力、觸控按鍵上傳,不然TSLIB測試會失敗)

9.2 使用TSLIB應用程式測試

(TSLIB安裝以及使用詳解地址: http://www.cnblogs.com/lifexy/p/7628780.html)

TSLIB: 為觸控式螢幕驅動獲得的取樣提供諸如濾波、去抖、校準等功能,通常作為觸控式螢幕驅動的適配層,為上層的應用提供了一個統一的介面。

校驗介面如下圖所示:

 

執行測試如下圖所示,能隨意畫圖:

 

 

最終,觸控式螢幕驅動測試成功