一.、前提知識
1、Linux輸入子系統(Input Subsystem):
在Linux中,輸入子系統是由輸入子系統裝置驅動層、輸入子系統核心層(Input Core)和輸入子系統事件處理層(Event Handler)組成。其中裝置驅動層提供對硬體各寄存器的讀寫訪問和將底層硬體對使用者輸入訪問的響應轉換為標準的輸入事件,再通過核心層提交給事件處理層;而核心層對下提供了裝置驅動層的編程介面,對上又提供了事件處理層的編程介面;而事件處理層就為我們使用者空間的應用程式提供了統一訪問裝置的介面和驅動層提交來的事件處理。所以這使得我們輸入裝置的驅動部分不在用關心對裝置檔案的操作,而是要關心對各硬體寄存器的操作和提交的輸入事件。下面用圖形來描述一下這三者的關係吧!
另外,又找了另一幅圖來說明Linux輸入子系統的結構,可能更加形象容易理解。如下:
2、輸入子系統裝置驅動層實現原理:
在Linux中,Input裝置用input_dev結構體描述,定義在input.h中。裝置的驅動只需按照如下步驟就可實現了。
①、在驅動模組載入函數中設定Input裝置支援input子系統的哪些事件;
②、將Input裝置註冊到input子系統中;
③、在Input裝置發生輸入操作時(如:鍵盤被按下/抬起、觸控螢幕被觸摸/抬起/移動、滑鼠被移動/單擊/抬起時等),提交所發生的事件及對應的索引值/座標等狀態。
Linux中輸入裝置的事件類型有(這裡只列出了常用的一些,更多請看linux/input.h中):
EV_SYN 0x00 同步事件 EV_KEY 0x01 按鍵事件 EV_REL 0x02 相對座標(如:滑鼠移動,報告的是相對最後一次位置的位移) EV_ABS 0x03 絕對座標(如:觸控螢幕和操作杆,報告的是絕對的座標位置) EV_MSC 0x04 其它 EV_LED 0x11 LED EV_SND 0x12 聲音 EV_REP 0x14 Repeat EV_FF 0x15 力反饋 |
用於提交較常用的事件類型給輸入子系統的函數有:
void input_report_key(struct input_dev *dev, unsigned int code, int value); //提交按鍵事件的函數 void input_report_rel(struct input_dev *dev, unsigned int code, int value); //提交相對座標事件的函數 void input_report_abs(struct input_dev *dev, unsigned int code, int value); //提交絕對座標事件的函數 |
注意,在提交輸入裝置的事件後必須用下列方法使事件同步,讓它告知input系統,裝置驅動已經發出了一個完整的報告:
void input_sync(struct input_dev *dev) |
二、觸控螢幕驅動的實現步驟
1、硬體原理圖分析:
S3c2440晶片內部觸控螢幕介面與ADC介面是整合在一起的,硬體結構原理圖請看:S3C2440上ADC驅動執行個體開發講解中的圖,其中通道7(XP或AIN7)作為觸控螢幕介面的X座標輸入,通道5(YP或AIN5)作為觸控螢幕介面的Y座標輸入。在"S3C2440上ADC驅動執行個體開發講解"中,AD轉換的類比訊號是由開發板上的一個電位器產生並通過通道1(AIN0)輸入的,而這裡的類比訊號則是由點觸觸控螢幕所產生的X座標和Y座標兩個類比訊號,並分別通過通道7和通道5輸入。S3c2440提供的觸控螢幕介面有4種處理模式,分別是:正常轉換模式、單獨的X/Y位置轉換模式、自動X/Y位置轉換模式和等待中斷模式,對於在每種模式下工作的要求,請詳細查看資料手冊的描述。本驅動執行個體將採用自動X/Y位置轉換模式和等待中斷模式。
注意:在每步中,為了讓代碼邏輯更加有條理和容易理解,就沒有考慮代碼的順序,比如函數要先定義後調用。如果要編譯此代碼,請嚴格按照C語言的規範來調整代碼的順序。
2、建立觸控螢幕驅動程式my2440_ts.c,首先實現載入和卸載部分,在驅動載入部分,我們主要做的事情是:啟用ADC所需要的時鐘、映射IO口、初始化寄存器、申請中斷、初始化輸入裝置、將輸入裝置註冊到輸入子系統。代碼如下:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/clk.h> #include <linux/init.h> #include <linux/input.h> #include <linux/serio.h> #include <plat/regs-adc.h> #include <asm/irq.h> #include <asm/io.h> /*用於儲存從平台時鐘列表中擷取的ADC時鐘*/ static struct clk *adc_clk; /*定義了一個用來儲存經過虛擬映射後的記憶體位址*/ static void __iomem *adc_base; /*定義一個輸入裝置來表示我們的觸控螢幕裝置*/ static struct input_dev *ts_dev; /*裝置名稱*/ #define DEVICE_NAME "my2440_TouchScreen" /*定義一個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)) static int __init ts_init(void) { int ret; /*從平台時鐘隊列中擷取ADC的時鐘,這裡為什麼要取得這個時鐘,因為ADC的轉換頻率跟時鐘有關。 系統的一些時鐘定義在arch/arm/plat-s3c24xx/s3c2410-clock.c中*/ adc_clk = clk_get(NULL, "adc"); if(!adc_clk) { /*錯誤處理*/ printk(KERN_ERR "falied to find adc clock source\n"); return -ENOENT; } /*時鐘擷取後要使能後才可以使用,clk_enable定義在arch/arm/plat-s3c/clock.c中*/ clk_enable(adc_clk); /*將ADC的IO連接埠佔用的這段IO空間映射到記憶體的虛擬位址,ioremap定義在io.h中。 注意:IO空間要映射後才能使用,以後對虛擬位址的操作就是對IO空間的操作, S3C2410_PA_ADC是ADC控制器的基地址,定義在mach-s3c2410/include/mach/map.h中,0x20是虛擬位址長度大小*/ adc_base = ioremap(S3C2410_PA_ADC, 0x20); if(adc_base == NULL) { /*錯誤處理*/ printk(KERN_ERR "failed to remap register block\n"); ret = -EINVAL; goto err_noclk; } /*初始化ADC控制寄存器和ADC觸控螢幕控制寄存器*/ adc_initialize(); /*申請ADC中斷,AD轉換完成後觸發。這裡使用共用中斷IRQF_SHARED是因為該中斷號在ADC驅動中也使用了, 最後一個參數1是隨便給的一個值,因為如果不給值設為NULL的話,中斷就申請不成功*/ ret = request_irq(IRQ_ADC, adc_irq, IRQF_SHARED | IRQF_SAMPLE_RANDOM, DEVICE_NAME, 1); if(ret) { printk(KERN_ERR "IRQ%d error %d\n", IRQ_ADC, ret); ret = -EINVAL; goto err_nomap; } /*申請觸控螢幕中斷,對觸控螢幕按下或提筆時觸發*/ ret = request_irq(IRQ_TC, tc_irq, IRQF_SAMPLE_RANDOM, DEVICE_NAME, 1); if(ret) { printk(KERN_ERR "IRQ%d error %d\n", IRQ_TC, ret); ret = -EINVAL; goto err_noirq; } /*給輸入裝置申請空間,input_allocate_device定義在input.h中*/ ts_dev = input_allocate_device(); /*下面初始化輸入裝置,即給輸入裝置結構體input_dev的成員設定值。 evbit欄位用於描述支援的事件,這裡支援同步事件、按鍵事件、絕對座標事件, BIT宏實際就是對1進行位操作,定義在linux/bitops.h中*/ ts_dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); /*keybit欄位用於描述按鍵的類型,在input.h中定義了很多,這裡用BTN_TOUCH類型來表示觸控螢幕的點擊*/ ts_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(ts_dev, ABS_X, 0, 0x3FF, 0, 0); input_set_abs_params(ts_dev, ABS_Y, 0, 0x3FF, 0, 0); input_set_abs_params(ts_dev, ABS_PRESSURE, 0, 1, 0, 0); /*以下是設定觸控螢幕輸入裝置的身份資訊,直接在這裡寫死。 這些資訊可以在驅動掛載後在/proc/bus/input/devices中查看到*/ ts_dev->name = DEVICE_NAME; /*裝置名稱*/ ts_dev->id.bustype = BUS_RS232; /*匯流排類型*/ ts_dev->id.vendor = 0xDEAD; /*經銷商ID號*/ ts_dev->id.product = 0xBEEF; /*產品ID號*/ ts_dev->id.version = 0x0101; /*版本ID號*/ /*好了,一些都準備就緒,現在就把ts_dev觸控螢幕裝置註冊到輸入子系統中*/ input_register_device(ts_dev); return 0; /*下面是錯誤跳轉處理*/ err_noclk: clk_disable(adc_clk); clk_put(adc_clk); err_nomap: iounmap(adc_base); err_noirq: free_irq(IRQ_ADC, 1); return ret; } /*初始化ADC控制寄存器和ADC觸控螢幕控制寄存器*/ static void adc_initialize(void) { /*計算結果為(二進位):111111111000000,再根據資料手冊得知 此處是將AD轉換預定標器值設為255、AD轉換預定標器使能有效*/ writel(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(0xFF), adc_base + S3C2410_ADCCON); /*對ADC開始延時寄存器進行設定,延時值為0xffff*/ writel(0xffff, adc_base + S3C2410_ADCDLY); /*WAIT4INT宏計算結果為(二進位):11010011,再根據資料手冊得知 此處是將ADC觸控螢幕控制寄存器設定成等待中斷模式*/ writel(WAIT4INT(0), adc_base + S3C2410_ADCTSC); } static void __exit ts_exit(void) { /*屏蔽和釋放中斷*/ disable_irq(IRQ_ADC); disable_irq(IRQ_TC); free_irq(IRQ_ADC, 1); free_irq(IRQ_TC, 1); /*釋放虛擬位址映射空間*/ iounmap(adc_base); /*屏蔽和銷毀時鐘*/ if(adc_clk) { clk_disable(adc_clk); clk_put(adc_clk); adc_clk = NULL; } /*將觸控螢幕裝置從輸入子系統中登出*/ input_unregister_device(ts_dev); } module_init(ts_init); module_exit(ts_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Huang Gang"); MODULE_DESCRIPTION("My2440 Touch Screen Driver"); |
3、接下來要做的是,在兩個中斷服務程式中實現觸控螢幕狀態和座標的轉換。先看代碼,如下:
/*定義一個外部的訊號量ADC_LOCK,因為ADC_LOCK在ADC驅動程式中已申明 這樣就能保證ADC資源在ADC驅動和觸控螢幕驅動中進行互斥訪問*/ extern struct semaphore ADC_LOCK; /*做為一個標籤,只有對觸控螢幕操作後才對X和Y座標進行轉換*/ static int OwnADC = 0; /*用於記錄轉換後的X座標值和Y座標值*/ static long xp; static long yp; /*用於計數對觸控螢幕壓下或抬起時類比輸入轉換的次數*/ static int count; /*定義一個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 irqreturn_t tc_irq(int irq, void *dev_id) { /*用於記錄這一次AD轉換後的值*/ unsigned long data0; unsigned long data1; /*用於記錄觸控螢幕操作狀態是按下還是抬起*/ int updown; /*ADC資源可以擷取,即上鎖*/ if (down_trylock(&ADC_LOCK) == 0) { /*標識對觸控螢幕進行了操作*/ OwnADC = 1; /*讀取這一次AD轉換後的值,注意這次主要讀的是狀態*/ data0 = readl(adc_base + S3C2410_ADCDAT0); data1 = readl(adc_base + S3C2410_ADCDAT1); /*記錄這一次對觸控螢幕是壓下還是抬起,該狀態儲存在資料寄存器的第15位,所以與上S3C2410_ADCDAT0_UPDOWN*/ updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); /*判斷觸控螢幕的操作狀態*/ if (updown) { /*如果是按下狀態,則調用touch_timer_fire函數來啟動ADC轉換,該函數定義後面再講*/ touch_timer_fire(0); } else { /*如果是抬起狀態,就結束了這一次的操作,所以就釋放ADC資源的佔有*/ OwnADC = 0; up(&ADC_LOCK); } } return IRQ_HANDLED; } static void touch_timer_fire(unsigned long data) { /*用於記錄這一次AD轉換後的值*/ unsigned long data0; unsigned long data1; /*用於記錄觸控螢幕操作狀態是按下還是抬起*/ int updown; /*讀取這一次AD轉換後的值,注意這次主要讀的是狀態*/ data0 = readl(adc_base + S3C2410_ADCDAT0); data1 = readl(adc_base + S3C2410_ADCDAT1); /*記錄這一次對觸控螢幕是壓下還是抬起,該狀態儲存在資料寄存器的第15位,所以與上S3C2410_ADCDAT0_UPDOWN*/ updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN)); /*判斷觸控螢幕的操作狀態*/ if (updown) { /*如果狀態是按下,並且ADC已經轉換了就報告事件和資料*/ if (count != 0) { long tmp; tmp = xp; xp = yp; yp = tmp; xp >>= 2; yp >>= 2; #ifdef CONFIG_TOUCHSCREEN_MY2440_DEBUG /*觸控螢幕調試資訊,編譯核心時選上此項後,點擊觸控螢幕會在終端上列印出座標資訊*/ struct timeval tv; do_gettimeofday(&tv); printk(KERN_DEBUG "T: %06d, X: %03ld, Y: %03ld\n", (int)tv.tv_usec, xp, yp); #endif /*報告X、Y的絕對座標值*/ input_report_abs(ts_dev, ABS_X, xp); input_report_abs(ts_dev, ABS_Y, yp); /*報告觸控螢幕的狀態,1表明觸控螢幕被按下*/ input_report_abs(ts_dev, ABS_PRESSURE, 1); /*報告按鍵事件,索引值為1(代表觸控螢幕對應的按鍵被按下)*/ input_report_key(ts_dev, BTN_TOUCH, 1); /*等待接收方受到資料後回複確認,用於同步*/ input_sync(ts_dev); } /*如果狀態是按下,並且ADC還沒有開始轉換就啟動ADC進行轉換*/ xp = 0; yp = 0; count = 0; /*設定觸控螢幕的模式為自動轉換模式*/ writel(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, adc_base + S3C2410_ADCTSC); /*啟動ADC轉換*/ writel(readl(adc_base + S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, adc_base + S3C2410_ADCCON); } else { /*否則是抬起狀態*/ count = 0; /*報告按鍵事件,索引值為0(代表觸控螢幕對應的按鍵被釋放)*/ input_report_key(ts_dev, BTN_TOUCH, 0); /*報告觸控螢幕的狀態,0表明觸控螢幕沒被按下*/ input_report_abs(ts_dev, ABS_PRESSURE, 0); /*等待接收方受到資料後回複確認,用於同步*/ input_sync(ts_dev); /*將觸控螢幕重新設定為等待中斷狀態*/ writel(WAIT4INT(0), adc_base + S3C2410_ADCTSC); /*如果觸控螢幕抬起,就意味著這一次的操作結束,所以就釋放ADC資源的佔有*/ if (OwnADC) { OwnADC = 0; up(&ADC_LOCK); } } } /*定義並初始化了一個定時器touch_timer,定時器服務程式為touch_timer_fire*/ static struct timer_list touch_timer = TIMER_INITIALIZER(touch_timer_fire, 0, 0); /*ADC中斷服務程式,AD轉換完成後觸發執行*/ static irqreturn_t adc_irq(int irq, void *dev_id) { /*用於記錄這一次AD轉換後的值*/ unsigned long data0; unsigned long data1; if(OwnADC) { /*讀取這一次AD轉換後的值,注意這次主要讀的是座標*/ data0 = readl(adc_base + S3C2410_ADCDAT0); data1 = readl(adc_base + 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; /*計數這一次AD轉換的次數*/ count++; if (count < (1<<2)) { /*如果轉換的次數小於4,則重新啟動ADC轉換*/ writel(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, adc_base + S3C2410_ADCTSC); writel(readl(adc_base + S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, adc_base + S3C2410_ADCCON); } else { /*否則,啟動1個時間滴答的定時器,這是就會去執行定時器服務程式上報事件和資料*/ mod_timer(&touch_timer, jiffies + 1); writel(WAIT4INT(1), adc_base + S3C2410_ADCTSC); } } return IRQ_HANDLED; } |
我們從整體上描述轉換這個的過程:
(1)如果觸控螢幕感覺到觸摸,則觸發觸控螢幕中斷即進入tc_irq,擷取ADC_LOCK後判斷觸控螢幕狀態為按下,則調用touch_timer_fire啟動ADC轉換;
(2)當ADC轉換啟動後,觸發ADC中斷即進入adc_irq,如果這一次轉換的次數小於4,則重新啟動ADC進行轉換,如果4次完畢後,啟動1個時間滴答的定時器,停止ADC轉換,也就是說在這個時間滴答內,ADC轉換是停止的;
(3)這裡為什麼要在1個時間滴答到來之前停止ADC的轉換呢?這是為了防止螢幕抖動。
(4)如果1個時間滴答到來則進入定時器服務程式touch_timer_fire,判斷觸控螢幕仍然處於按下狀態則上報事件和轉換的資料,並重啟ADC轉換,重複第(2)步;
(5)如果觸摸抬起了,則上報釋放事件,並將觸控螢幕重新設定為等待中斷狀態。
四、移植和測試觸控螢幕驅動程式
移植和測試請看Linux-2.6.30.4在2440上的移植之觸控螢幕驅動
if ($ != jQuery) { $ = jQuery.noConflict(); } var isLogined = false; var cb_blogId = 77585; var cb_entryId = 1994233; var cb_blogApp = "hoys"; var cb_blogUserGuid = "1291a735-2c36-df11-ba8f-001cf0cd104b"; var cb_entryCreatedDate = '2011/3/24 18:46:00';