一.、前提知識
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';