Linux下的觸控式螢幕驅動
一.觸控式螢幕理論概述
對於觸控式螢幕驅動,我們主要需要掌握觸控式螢幕驅動程式碼和應用層測試程式碼。下面講的是基於Mini2440的觸控式螢幕驅動,現在的驅動我們都將裝置和驅動分離,掛在平臺裝置總線上,讓裝置和驅動去匹配。而我們在linu2.6.32.2核心版本中的觸控式螢幕驅動仍然沒有將裝置和驅動分離,這樣就不存在匹配問題,這種現象其實我們並不陌生,在我們學習驅動的前期,都會研究簡單字元驅動代表LED驅動,那個驅動就是把裝置和驅動寫在了一起。總結下,驅動和裝置可以分離也可以不分離,建議分離,而本觸控式螢幕驅動沒有分離裝置和驅動,有興趣可以將裝置和驅動進行分離。
先說明下觸控式螢幕的工作原理,當有人在觸控式螢幕上按下觸筆時,觸控式螢幕的四個引腳會產生不同的電壓值,這樣觸控式螢幕控制器就能檢測到這種變化,從而產生INT_TC
二.觸控式螢幕驅動分析
本驅動分析很有特點,我對觸控式螢幕驅動的分析是按照整個觸控事件的執行順序進行程式碼分析的,有的函式由於每次被執行完成的任務不同,所以需要多次分析。同時,我把整個觸控事件的來龍去脈也都說的很清楚了。
驅動分析/driver/input/touchscreen/s3c2410_ts.c
static int __init s3c2410ts_init(void)
{
struct input_dev *input_dev;
adc_clock = clk_get(NULL, "adc"); //獲取時鐘
if (!adc_clock) {
printk(KERN_ERR "failed to get adc clock source\n");
return -ENOENT;
}
clk_enable(adc_clock); //使能時鐘
base_addr=ioremap(S3C2410_PA_ADC,0x20); //實體地址轉為虛擬地址
if (base_addr == NULL) {
printk(KERN_ERR "Failed to remap register block\n");
return -ENOMEM;
}
s3c2410_ts_connect(); //觸控式螢幕埠配置
//使能預分頻,分頻係數為0xff
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(); //分配input裝置
if (!input_dev) {
printk(KERN_ERR "Unable to allocate the input device !!\n");
return -ENOMEM;
}
dev = input_dev;
//支援按鍵事件、座標事件
dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS);
dev->keybit[BITS_TO_LONGS(BTN_TOUCH)] = BIT(BTN_TOUCH);
//對於X軸範圍是0-ox3ff,資料誤差是0,中心平滑位置是0
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 = s3c2410ts_name;
dev->id.bustype = BUS_RS232;
dev->id.vendor = 0xDEAD;
dev->id.product = 0xBEEF;
dev->id.version = S3C2410TSVERSION;
//申請AD轉換中斷
if(request_irq(IRQ_ADC,stylus_action,IRQF_SHARED|IRQF_SAMPLE_RANDOM,"s3c2410_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,
"s3c2410_action", dev)) {
printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_TC !\n");
iounmap(base_addr);
return -EIO;
}
printk(KERN_INFO "%s successfully loaded\n", s3c2410ts_name);
input_register_device(dev);
return 0;
}
下面是這個模組載入函式中呼叫的一個配置埠函式
static inline void s3c2410_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 struct timer_list touch_timer =
TIMER_INITIALIZER(touch_timer_fire, 0, 0);
因為這個定時器的期限時間設定為0,這表示當驅動載入後就會執行一次定時函式touch_timer_fire
static void touch_timer_fire(unsigned long data)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0); //讀取X座標
data1 = ioread32(base_addr+S3C2410_ADCDAT1); //讀取Y座標
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); //觸控式螢幕是否被按下,如果按下updowm=1
if (updown) {
if (count != 0) {
long tmp;
tmp = xp;
xp = yp;
yp = tmp;
xp >>= 2;
yp >>= 2;
input_report_abs(dev, ABS_X, xp);
input_report_abs(dev, ABS_Y, yp);
input_report_key(dev, BTN_TOUCH, 1);
input_report_abs(dev, ABS_PRESSURE, 1);
input_sync(dev);
}
xp = 0;
yp = 0;
count = 0;
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 { //沒有被按下
count = 0; //初始化count為0,表示當前AD轉換沒發生
input_report_key(dev, BTN_TOUCH, 0); //向input子系統報告未按下
input_report_abs(dev, ABS_PRESSURE, 0);
input_sync(dev);
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); //等待按鍵中斷
if (OwnADC) { //OwnADC是獲取一把鎖標示,在此為0
OwnADC = 0;
up(&ADC_LOCK);
}
}
}
第二種情況,如果觸控式螢幕被按下,首先觸發觸控中斷,執行stylus_updown函式
static irqreturn_t stylus_updown(int irq, void *dev_id)
{
unsigned long data0;
unsigned long data1;
int updown;
if (down_trylock(&ADC_LOCK) == 0) { //獲取一把鎖
OwnADC = 1; //表示獲得鎖
data0 = ioread32(base_addr+S3C2410_ADCDAT0); //讀取X軸資料
data1 = ioread32(base_addr+S3C2410_ADCDAT1); //讀取Y軸資料
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); //觸控式螢幕是否被按下,按下updowm=1
if (updown) {
touch_timer_fire(0); // 如果觸控式螢幕被按下,執行touch_timer_fire
} else { //去抖動操作,釋放鎖
OwnADC = 0;
up(&ADC_LOCK);
}
}
return IRQ_HANDLED;
}
下面我們第二次分析touch_timer_fire,而這次主要是因為觸控中斷中呼叫了這個函式,假設當前觸控式螢幕被按下後,座標值還沒進行AD轉換
static void touch_timer_fire(unsigned long data)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
if (updown) { //觸控式螢幕被按下
if (count != 0) { //count是全域性變數,統計AD轉換次數,目前未AD轉換
long tmp;
tmp = xp;
xp = yp;
yp = tmp;
xp >>= 2;
yp >>= 2;
input_report_abs(dev, ABS_X, xp);
input_report_abs(dev, ABS_Y, yp);
input_report_key(dev, BTN_TOUCH, 1);
input_report_abs(dev, ABS_PRESSURE, 1);
input_sync(dev);
}
xp = 0; //雖然觸控式螢幕被按下,但是未完成AD轉換
yp = 0;
count = 0;
//自動連續測量X和Y座標
iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC);
//AD轉換開始且該位在開始後清零
iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON);
} else {
count = 0;
input_report_key(dev, BTN_TOUCH, 0);
input_report_abs(dev, ABS_PRESSURE, 0);
input_sync(dev);
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
if (OwnADC) {
OwnADC = 0;
up(&ADC_LOCK);
}
}
}
現在我們知道,如果觸控式螢幕被按下,但是AD還沒轉換完畢,那麼我們會開啟AD轉換,自動測量X和Y座標,這樣就會觸發AD轉換中斷,執行AD轉換的中斷處理程式。其實當我們的觸控式螢幕被按下,當X和Y軸獲取電壓值,然後就會進行AD轉換,執行AD轉換的中斷處理程式。好了,我們該看看AD轉換的中斷程式碼了。
static irqreturn_t stylus_action(int irq, void *dev_id)
{
unsigned long data0;
unsigned long data1;
if (OwnADC) { //只有觸控式螢幕被按下,相應了觸控中斷該標誌才為1
data0 = ioread32(base_addr+S3C2410_ADCDAT0); //讀取X座標
data1 = ioread32(base_addr+S3C2410_ADCDAT1); //讀取Y座標
xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;//疊加X座標
yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;//疊加Y座標
count++; //統計AD轉換次數
if (count < (1<<2)) { //如果AD轉換次數不足4次
//自動連續測量X和Y座標
iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC);
//AD轉換開始且該位在開始後清零
iowrite32(ioread32(base_addr+S3C2410_ADCCON) |S3C2410_ADCCON_ENABLE_START,base_addr+S3C2410_ADCCON);
} else {
mod_timer(&touch_timer, jiffies+1); //四次AD轉換後,修改定時時間
iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC);//等待釋放
}
}
return IRQ_HANDLED;
}
在這個AD轉換的中斷程式中,有一個全域性變數count令人費解,count是什麼作用呢?我們做AD轉換時,其實是對一個點進行了四次取樣,然後把四次取樣結果進行疊加然後取平均。這樣提高AD轉換的精確度。在上面這個AD轉換的中斷處理程式中,我們知道,當count不足4次時,會繼續進行自動連續測量X和Y座標並開啟AD轉換,只有當AD轉換次數達到四次後,就會修改定時器的時間,使得下一個節拍到來時執行定時器處理程式,並且等待按鍵被釋放。當AD轉換完畢,我們在下一個節拍到來後,會執行一次定時器程式touch_timer_fire
static void touch_timer_fire(unsigned long data)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
if (updown) { //觸控式螢幕被按下
if (count != 0) { //count是全域性變數,統計AD轉換次數,目前已經4次
long tmp;
tmp = xp;
xp = yp;
yp = tmp; // X和Y軸資料交換
xp >>= 2; //因為對同一個點取樣四次,這裡對X軸取平均
yp >>= 2; //因為對同一個點取樣四次,這裡對Y軸取平均
input_report_abs(dev, ABS_X, xp); //報告X座標
input_report_abs(dev, ABS_Y, yp); //報告Y座標
input_report_key(dev, BTN_TOUCH, 1); //報告觸控事件
input_report_abs(dev, ABS_PRESSURE, 1);
input_sync(dev); //同步
}
xp = 0; //清零
yp = 0;
count = 0;
//自動連續測量X和Y座標
iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC);
//AD轉換開始且該位在開始後清零
iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON);
} else {
count = 0;
input_report_key(dev, BTN_TOUCH, 0);
input_report_abs(dev, ABS_PRESSURE, 0);
input_sync(dev);
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
if (OwnADC) {
OwnADC = 0;
up(&ADC_LOCK);
}
}
}
這裡首先解釋下,為什麼要對X和Y座標值交換,因為我們的X35LCD屏是240*320的,為了滿足使用者“X軸長Y軸短”的習慣,在此進行X和Y座標值交換,當然在此不交換,不會影響觸控式螢幕驅動的執行。
我們這次分析這個定時器touch_timer_fire,最終發現我們不但向input子系統報告了我們的座標值,還對座標變數和統計AD轉換次數變數清零,最後還打開了AD轉換開關,既然這次觸控事件已經結束,那麼這裡怎麼還開啟AD轉換開關呢?其實,如果你足夠細心,你會發現,雖然AD開關打開了,然後會執行AD轉換的中斷處理程式,一旦進入AD轉換,肯定也是轉換四次,然後在下一個節拍到來時,執行定時程式,最關鍵的是,此時我們的觸控式螢幕按鍵已經釋放了,在這一的背景下,我們再次跟蹤定時函式touch_timer_fire
static void touch_timer_fire(unsigned long data)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0); //讀取X座標
data1 = ioread32(base_addr+S3C2410_ADCDAT1); //讀取Y座標
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); //觸控式螢幕是否被按下,如果按下updowm=1
if (updown) {
if (count != 0) {
long tmp;
tmp = xp;
xp = yp;
yp = tmp;
xp >>= 2;
yp >>= 2;
input_report_abs(dev, ABS_X, xp);
input_report_abs(dev, ABS_Y, yp);
input_report_key(dev, BTN_TOUCH, 1);
input_report_abs(dev, ABS_PRESSURE, 1);
input_sync(dev);
}
xp = 0;
yp = 0;
count = 0;
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 { //沒有被按下
count = 0; //初始化count為0,表示當前AD轉換沒發生
input_report_key(dev, BTN_TOUCH, 0); //向input子系統報告未按下
input_report_abs(dev, ABS_PRESSURE, 0); //觸控式螢幕是擡起狀態
input_sync(dev);
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); //等待按鍵中斷
if (OwnADC) { //OwnADC是獲取一把鎖標示,在此為1
OwnADC = 0; //該鎖標誌為0,表示釋放該鎖
up(&ADC_LOCK); //釋放鎖
}
}
}
好了,一旦釋放了鎖,那麼我們觸控式螢幕事件的一個生命週期才真正算分析結束了,不過在此還有一個問題沒有解決,就是當我們觸控式螢幕按鍵被釋放後,其實也會產生一個按鍵中斷,執行觸控中斷程式,不過在這個程式中起初是為了獲得一個訊號量,由於訊號量是等待鎖,所以,程式一直在試圖獲取這個訊號量,當我們的訊號量在上面這個touch_timer_fire中被釋放後,我們就會再次獲取這個訊號量,繼續跟蹤這個觸控中斷函式stylus_updown
static irqreturn_t stylus_updown(int irq, void *dev_id)
{
unsigned long data0;
unsigned long data1;
int updown;
if (down_trylock(&ADC_LOCK) == 0) { //再次獲取鎖
OwnADC = 1;
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
if (updown) { //因為是釋放操作,所以updowm=0
touch_timer_fire(0);
} else {
OwnADC = 0;
up(&ADC_LOCK); //釋放鎖
}
}
return IRQ_HANDLED;
}
好了,這樣我們就真正結束了一次觸控時間的週期。
總結下觸控式螢幕控制的整個執行過程:
Step1:軟體開啟INT_ADC和INT_TS中斷,設定ADCCON以確定AD轉換需要的時鐘頻率,設定ADCDLY以確定從得到命令到開始轉換座標的延時時長,設定ADCTSC使得觸控式螢幕處於等待觸筆按下狀態
Step2:當觸筆按下,產生INT_TC中斷,執行stylus_updown,stylus_updown首先判斷中斷產生的原因是不是觸筆按下,是的話就呼叫定時函式touch_timer_fire,在touch_timer_fire中設定ADCTSC使得觸控式螢幕準備進行自動X/Y軸轉換狀態,然後設定ADCCON啟動座標轉換,結束stylus_updown。
Step3:觸控式螢幕在延遲指定時間後開始轉換X/Y座標,並將轉換的結果儲存到ADCDAT0和ADADAT1中,完成後發出INT_ADC中斷,表示轉換完成。
Step4:進入INT_ADC中斷處理程式stylus_action,獲取X/Y軸座標,然後進行四次座標轉換以求平均值,最後設定ADCTSC使得觸控式螢幕處於等待觸筆釋放狀態,同時當下一個節拍到來時,呼叫touch_timer_fire向input子系統報告座標。
Step5:當觸筆釋放,還會產生INT_TC中斷,進入其中斷處理程式,得到觸筆釋放的訊息,最後設定ADCTSC使得觸控式螢幕處於下一次等待觸筆按下狀態。
三.觸控式螢幕驅動測試
由於mini2440的觸控式螢幕驅動是基於input子系統的,而input子系統給使用者層提供的是input_event結構體,我們主要是在應用層接收這個結構體,然後對其型別進行分類,取出我們需要的數值。
struct input_event { struct timeval time;
unsigned short type; //支援的型別,如EV_ABS
unsigned short code; //支援的具體事件,如座標事件的ABS_X
unsigned int value; //值
};
測試觸控式螢幕驅動的應用層程式碼如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/input.h>
#include <sys/fcntl.h>
int main(int argc, char *argv[])
{
int fd = -1;
int num;
size_t rb;
int version;
char name[20];
struct input_event ev;
int i=0;
if ((fd = open("/dev/input/event0", O_RDONLY)) < 0) //開啟裝置
{
perror("open error");
exit(1);
}
while(1)
{
rb = read(fd, &ev, sizeof(struct input_event)); //讀取裝置
if (rb < (int)sizeof(struct input_event)) //讀取錯誤
{
perror("read error");
exit(1);
}
if (EV_ABS==ev.type) //讀取按鍵內容
{
printf("event=%s,value=%d\n",ev.code==ABS_X?"ABS_X":ev.code==ABS_Y?"ABS_Y":ev.code==ABS_PRESSURE?"ABS_PRESSURE":"UNKNOWEN",ev.value);
}else{
printf("not ev_abs\n");
}
}
close(fd);
return 0;
}
編譯測試程式test.c
arm-linux-gcc test.c –o test
超級終端:
./test
測試結果:(觸筆按下觸控式螢幕)
event=ABS_X, value=505
event=ABS_Y, value=334
event=ABS_PRESSURE, value=1