18.Llinux-觸摸屏驅動(詳解)
本節的觸摸屏驅動也是使用之前的輸入子系統
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: 為觸摸屏驅動獲得的采樣提供諸如濾波、去抖、校準等功能,通常作為觸摸屏驅動的適配層,為上層的應用提供了一個統一的接口。
校驗界面如下圖所示:
運行測試如下圖所示,能隨意畫圖:
最終,觸摸屏驅動測試成功
18.Llinux-觸摸屏驅動(詳解)