linux input輸入子系統分析《三》:S3C2440的觸摸屏驅動實例

主要講述本人在學習Linux內核input子系統的全部過程,如有分析不當,多謝指正。以下方式均可聯繫,文章歡迎轉載,保留聯繫信息,以便交流。

郵箱:[email protected]

主頁:www.ielife.cn(愛嵌論壇——嵌入式技術學習交流)

博客:blog.csdn.net/ielife


1.1    本節閱讀前提

本節的說明建立在前兩節的基礎之上,需要先閱讀如下兩篇章:

linux input輸入子系統分析《一》:初識input輸入子系統

linux input輸入子系統分析《二》:s3c2440ADC簡單驅動實例分析

1.2    觸摸屏工作原理

S3C2440的觸摸屏接口是4線電阻式觸摸屏接口,可以控制x、y方向上的引腳(XP、XM、YP、YM)的變換,S3C2440觸摸屏的硬件資源包括觸摸屏引腳和ADC轉換接口,可以使用寄存器組中的ADCTSC寄存器來操作觸摸屏引腳資源,ADCCON寄存器來控制AD轉換功能,由linux input輸入子系統分析《二》:s3c2440ADC簡單驅動實例分析中介紹的觸摸屏接口工作模式爲4種,這裏我們只是用x/y方向自動轉換模式和等待中斷模式。自動轉換模式用於轉換x方向和y方向的值到ADCDAT0和ADCDAT1中,等待中斷模式用於檢測觸摸屏的按下和擡起,一般使用上升沿和下降沿觸發獲得觸摸屏事件。

觸摸屏使用引腳的4個引腳XP、XM、YP、YM分別對應S3C2440芯片的AIN7、AIN6、AIN5、AIN4模擬輸入源,其中按照linux input輸入子系統分析《二》:s3c2440ADC簡單驅動實例分析中的圖4和圖5可以看出AIN7接XP,AIN5接YP,由此可得x和y座標的模擬信號由AIN5和AIN7引腳通過ADC轉換器產生,產生的數據保存在ADCDAT0和ADCDAT1中。

1.3    驅動程序組成結構

分析代碼前,有必要了解驅動程序的組成結構。

s3c2440ts_init()完成的功能:

使能adc的PCLK時鐘源

映射操作觸摸屏寄存器的地址

初始化寄存器

初始化輸入設備

填充輸入子系統設備結構體input_dev

申請中斷IRQ_TS和IRQ_ADC

註冊輸入設備到輸入子系統中

s3c2440ts_exit()完成的功能:

註銷使用的系統資源

stylus_updown()完成的功能:

中斷處理程序,完成對觸摸屏按下和釋放的判斷

啓動ADC轉換

stylus_action()完成的功能:

ADC轉換程序

上報事件

touch_timer_fire()完成的功能:

ADC的子功能

1.4    代碼分析

代碼如下:

/*
 * s3c2440 觸摸屏驅動程序
 *
 * Kevin Lee <www.ielife.cn>
 */
 
#include<linux/kernel.h> /* 提供prink等內核特有屬性 */
#include<linux/module.h> /* 提供模塊及符號接口*/
#include<linux/init.h> /* 設置段,如_init、_exit,設置初始化優先級,如__initcall */
#include<linux/wait.h> /* 等待隊列wait_queue */
#include<linux/interrupt.h> /* 中斷方式,如IRQF_SHARED */
#include<linux/fs.h> /* file_operations操作接口等 */
#include<linux/clk.h> /* 時鐘控制接口,如struct clk */
#include<linux/miscdevice.h> /* 雜項設備 */
#include<asm/io.h> /* 提供readl、writel */
#include<linux/irq.h> /* 提供中斷相關宏 */
#include<asm/irq.h> /* 提供中斷號,中斷類型等,如IRQ_ADC中斷號 */
#include<asm/arch/regs-adc.h> /* 提供控制器的寄存器操作,如S3C2410_ADCCON */
#include<asm/uaccess.h> /* 提供copy_to_user等存儲接口 */
#include<linux/input.h> /* 內核輸入子系統操作接口 */
#include<linux/slab.h> /* kzalloc內存分配函數 */
#include<linux/time.h> /* do_gettimeofday時間函數 */
#include<linux/timer.h> /* timer定時器 */
 
/* 用於代碼的調試 */
#define CONFIG_S3C2440_TOUCHSCREEN__DEBUG 1
 
/* 用於處理位操作 */
#define BITS_PER_LONG           32
#define BIT_MASK(nr)         (1UL << ((nr) %BITS_PER_LONG))
#define BIT_WORD(nr)        ((nr) / BITS_PER_LONG)
 
/* 定義一個宏WAIT4INT,用於對ADCTSC觸摸屏控制寄存器進行操作,
 * S3C2410_ADCTSC_YM_SEN等在內核include/asm/arch/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))
 
/* 定義一個宏AUTOPST,用於設置ADCTSC觸摸屏控制寄存器爲自動轉換模式 */
#define AUTOPST    (S3C2410_ADCTSC_YM_SEN |S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
                S3C2410_ADCTSC_AUTO_PST |S3C2410_ADCTSC_XY_PST(0))
 
/* 觸摸屏數據結構體 */
struct ts_event {
       short pressure;              /* 是否按下 */
       short xp;        /*觸摸屏x座標值 */
       short yp;        /*觸摸屏y座標值 */
};
 
/* 觸摸屏設備結構體 */
struct s3c2440_ts {
       struct input_dev *input;/* 輸入子系統設備結構體 */
       struct timer_list timer;/* 定時器,用於ADC轉換操作 */
       struct ts_event tc;  /* ADC轉換的值的保存位置,也用於上報input子系統的數據 */
       int pendown;          /* 判斷是否有按下 */
       int count;        /* 用於驅動去抖的計數 */
       int shift;         /* 用於驅動去抖的基數 */
};
 
/* 定義觸摸屏設備結構體 */
static struct s3c2440_ts *s3c2440_ts;
 
/* 定義虛擬地址訪問硬件寄存器,__iomem只是用於表示指針將指向I/O內存 */
static void __iomem *base_addr;
 
/* 定義adc時鐘,通過adc_clock接口獲得adc輸入時鐘,adc轉換器需要 */
static struct clk *adc_clock;
 
/* 申明外部定義的信號量,adc.c中定義,處理IRQ_ADC共享中斷引起的資源互斥 */
extern struct semaphore adc_lock;
//DECLARE_MUTEX(adc_lock);
 
/* 處理觸摸屏的數據及事件上報,屬於中斷調用的函數,不能睡眠 */
static void touch_timer_fire(unsigned long data)
{
    /* 保存ADCDAT0及ADCDAT1的x,y座標值 */
    unsigned long data0;
    unsigned long data1;
   
    /* 禁止中斷,處理完數據再打開中斷 */
    set_irq_type(IRQ_TC, IRQT_NOEDGE);
    set_irq_type(IRQ_ADC, IRQT_NOEDGE);
 
    /* 讀取ADCDAT0和ADCDAT1寄存器,提取ADC轉換的x,y座標值 */
    data0 = readl(base_addr + S3C2410_ADCDAT0);
    data1 = readl(base_addr + S3C2410_ADCDAT1);
 
    /* 判斷ADCDAT0和ADCDAT1中的[15]位,[15]位爲等待中斷模式下用於判斷筆尖是否有擡起或落下,0=落下 */
    s3c2440_ts->pendown = (!(data0 &S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
 
    /* 當觸摸屏處於被按下狀態,執行下面代碼 */
    if (s3c2440_ts->pendown) {
           /*count不爲0,說明正在轉換,xp和yp往右移動shift(2)位是爲了去抖
            * 需要結合stylus_action中斷函數來看,當count=4時,才能認爲轉換結束,
            * 即最後所得的xp和yp是被計算4次的,最終上報時需要除以4,因此這裏預先
            * 右移2位,相當於乘以4
            */
           if(s3c2440_ts->count != 0) {
                  s3c2440_ts->tc.xp>>= s3c2440_ts->shift;
                  s3c2440_ts->tc.yp>>= s3c2440_ts->shift;
 
/* 終端打印調試信息 */
#ifdef CONFIG_S3C2440_TOUCHSCREEN__DEBUG
            {
                   struct timeval tv;
                   do_gettimeofday(&tv);
                   printk(KERN_INFO"T: %06d, X: %03x, Y: %03x\n", (int)tv.tv_usec, s3c2440_ts->tc.xp,s3c2440_ts->tc.yp);
            }
#endif
              /*報告X、Y的絕對座標值*/
              input_report_abs(s3c2440_ts->input,ABS_X, s3c2440_ts->tc.xp);
              input_report_abs(s3c2440_ts->input,ABS_Y, s3c2440_ts->tc.yp);
             
              /*報告觸摸屏的狀態,1表明觸摸屏被按下*/
              input_report_abs(s3c2440_ts->input,ABS_PRESSURE, s3c2440_ts->tc.pressure);
             
              /*報告按鍵事件,鍵值爲1(代表觸摸屏對應的按鍵被按下)*/
              input_report_key(s3c2440_ts->input,BTN_TOUCH, s3c2440_ts->pendown);
             
              /*等待接收方受到數據後回覆確認,用於同步*/
              input_sync(s3c2440_ts->input);
       }
 
        /* count=0執行這裏面的代碼,ADC還沒有開始轉換 */
         s3c2440_ts->tc.xp = 0;
         s3c2440_ts->tc.yp = 0;
         s3c2440_ts->count = 0;
         s3c2440_ts->tc.pressure = 0;
 
        /* 因爲觸摸屏是按下狀態,ADC還沒有轉換,需要啓動ADC開始轉換
         * 本句代碼是設置觸摸屏爲自動轉換模式
         */
         writel(S3C2410_ADCTSC_PULL_UP_DISABLE| AUTOPST, base_addr + S3C2410_ADCTSC);
 
        /* 啓動ADC轉換 */
         writel(readl(base_addr +S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr + S3C2410_ADCCON);
     } else {
        /* 執行到這裏說明按鍵沒有被按下或按鍵擡起,count清0 */
        s3c2440_ts->count = 0;
 
        /* 報告按鍵事件,給0值說明按鍵擡起 */
        input_report_key(s3c2440_ts->input,BTN_TOUCH, 0);
 
        /* 報告按鍵事件,給0值說明按鍵擡起 */
        input_report_abs(s3c2440_ts->input,ABS_PRESSURE, 0);
 
        /* 用於同步事件處理層同步上報的按鍵和觸摸事件 */
        input_sync(s3c2440_ts->input);
 
        /* 將觸摸屏重新設置爲等待中斷狀態,等待觸摸屏被按下 */
        writel(WAIT4INT(0), base_addr +S3C2410_ADCTSC);
       
        /* 觸摸屏擡起了,可以釋放信號量了 */
        up(&adc_lock);
     }
    
     /* 使能中斷觸發條件,IRQT_BOTHEDGE爲使能上升沿和下降沿觸發 */
     set_irq_type(IRQ_TC, IRQT_BOTHEDGE);
     set_irq_type(IRQ_ADC, IRQT_BOTHEDGE);
}
 
/* 觸摸屏中斷服務程序,觸摸屏按下或擡起時觸發執行 */
static irqreturn_t stylus_updown(int irq, void *dev_id)
{
    /* 用於記錄ADC轉換後的值 */
    unsigned long data0;
    unsigned long data1;
 
    /* 由於是中斷程序,所以不能使用down和down_interruptible,會導致睡眠 */
    if (down_trylock(&adc_lock) == 0)
    {
        /* 讀取ADCDAT0和ADCDAT1,用於判斷觸摸屏是否被按下 */
        data0 = readl(base_addr +S3C2410_ADCDAT0);
        data1 = readl(base_addr +S3C2410_ADCDAT1);
 
        /* 判斷按鍵是否被按下 */
        s3c2440_ts->pendown = (!(data0 &S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
 
        /* 按鍵已經按下 */
        if (s3c2440_ts->pendown)
            /* 啓動ADC開始轉換數據 */
            touch_timer_fire(0);
    }
 
    return IRQ_RETVAL(IRQ_HANDLED);
}
 
/*ADC中斷服務程序,ADC啓動後被執行 */
static irqreturn_t stylus_action(int irq, void *dev_id)
{
    /* 同上函數 */
    unsigned long data0;
    unsigned long data1;
 
#ifdef CONFIG_S3C2440_TOUCHSCREEN__DEBUG
       printk(KERN_ERR "%s() No.%dline:\n\r",__FUNCTION__,__LINE__);
#endif
 
    /* 獲取觸摸屏的x,y座標值 */
    data0 = readl(base_addr + S3C2410_ADCDAT0);
    data1 = readl(base_addr + S3C2410_ADCDAT1);
   
    /* 既然按鍵按下了,執行到此ADC轉換也開始了,應該取得x,y座標值了
     * x=ADCDAT0[9:0],y=ADCDAT1[9:0],count++,presssure設置爲1
     */
    s3c2440_ts->tc.xp += data0 &S3C2410_ADCDAT0_XPDATA_MASK;
    s3c2440_ts->tc.yp += data1 &S3C2410_ADCDAT1_YPDATA_MASK;
    s3c2440_ts->count++;
    s3c2440_ts->tc.pressure = 1;
   
    /* 如果count小於4,需要重啓設置自動轉換模式,並進行ADC轉換,用於去抖 */
    if (s3c2440_ts->count <(1<<s3c2440_ts->shift)) {
           writel(S3C2410_ADCTSC_PULL_UP_DISABLE| AUTOPST, base_addr + S3C2410_ADCTSC);
           writel(readl(base_addr+ S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr + S3C2410_ADCCON);
    } else {
           /*到這裏說明count=4,啓動定時器去執行touch_timer_fire函數上報按鍵和觸摸事件*/
           mod_timer(&s3c2440_ts->timer,jiffies + HZ / 100);
           /*檢測觸摸屏擡起的中斷信號*/
           writel(WAIT4INT(1),base_addr + S3C2410_ADCTSC);
    }
 
    return IRQ_HANDLED;
}
 
/* 初始化ADC控制寄存器和ADC觸摸屏控制寄存器 */
static void adc_init(void)
{
    /* S3C2410_ADCCON_PRSCEN設置ADCCON的位[14]=1爲使能A/D預分頻器
     * S3C2410_ADCCON_PRSCVL設置ADCCON的位[13:6]=32表示設置的分頻值,
     * ADC的轉換頻率需要在2.5MHZ以下,我們使用的ADC輸入時鐘爲PCLK=50MHZ,50MHZ/(49+1)=1MHZ,滿足條件
     */
    writel(S3C2410_ADCCON_PRSCEN |S3C2410_ADCCON_PRSCVL(49), base_addr + S3C2410_ADCCON);
 
    /* 初始化ADC啓動或延時寄存器,ADC轉換啓動延時值設置爲0xffff */
    writel(0xffff, base_addr + S3C2410_ADCDLY);
 
    /* 初始化ADC觸摸屏控制寄存器ADCTSC,
     * WAIT4INT(0)在上面定義,引腳YM、YP、XM、XP的使能位位於ADCTSC的[7:4]位,
     * 設置觸摸屏的工作狀態在[1:0]位,WAIT4INT(0)=11010011,
     * 1101代表筆尖按下時發生觸摸屏中斷信號IRQ_TS給CPU,0011表示XP上拉使能,
     * 使用正常ADC轉換,轉換的方式爲等待中斷模式
     * 原理圖見linux input輸入子系統分析《二》:s3c2440的ADC簡單驅動實例分析
     */
    writel(WAIT4INT(0), base_addr +S3C2410_ADCTSC);
}
 
/*s3c2440觸摸屏驅動模塊加載程序,做了以下工作
 * 獲得時鐘源、設置訪問觸摸屏控制器的虛擬地址並初始化觸摸屏控制器、初始化中斷定時器、
 * 填充input_dev結構體(設備基本信息及事件信息)、註冊中斷、註冊設備到input子系統
 */
static int __init s3c2440ts_init(void)
{
    struct input_dev *input_dev;
    int err = -ENOMEM;
   
    s3c2440_ts = kzalloc(sizeof(structs3c2440_ts), GFP_KERNEL);
    /*給輸入設備申請空間,input_allocate_device定義在input.h中*/
    input_dev = input_allocate_device();
    if (!s3c2440_ts || !input_dev)
           gotofail1;
 
    /* 獲得adc的時鐘源,通過arch/arm/mach-s3c2410/clock.c獲得提供的時鐘源爲PCLK */
    adc_clock = clk_get(NULL, "adc");
    if (!adc_clock)
    {
        printk(KERN_ERR "failed to get adcclock source\n");
        return -ENOENT;
    }
 
    /* 在時鐘控制器中給adc提供輸入時鐘,ADC轉換需要輸入時鐘 */
    clk_enable(adc_clock);
 
    /* 使用ioremap獲得操作ADC控制器的虛擬地址
     * S3C2410_PA_ADC=ADCCON,是ADC控制器的基地址,寄存器組的長度=0x1c
     */
    base_addr = ioremap(S3C2410_PA_ADC, 0x1c);
    if (base_addr == NULL)
    {
        printk(KERN_ERR "Failed to remapregister block\n");
        return -ENOMEM;
        goto fail1;
    }
 
    /*初始化ADC控制寄存器和ADC觸摸屏控制寄存器*/
    adc_init();
   
    /* 初始化定時器 */
    init_timer(&s3c2440_ts->timer);
    s3c2440_ts->timer.data = 1;
    s3c2440_ts->timer.function =touch_timer_fire;
      
    /* 設置觸摸屏輸入設備的標誌,註冊輸入設備成功進入根文件系統,可以cat /proc/bus/input/devices查看其內容*/
    input_dev->name          = "s3c2410Touchscreen";  /* 設備名稱 */
    input_dev->phys         ="s3c2440ts/input0"; /*  */
    input_dev->id.bustype    = BUS_HOST;                    /*總線類型 */   
    input_dev->id.vendor     = 0x1;                /*經銷商ID */
    input_dev->id.product    = 0x2;                /*產品ID */
    input_dev->id.version    = 0x0100;                   /*版本ID */
   
    s3c2440_ts->shift = 2;
   
    /*下面初始化輸入設備,即給輸入設備結構體input_dev的成員設置值。
    evbit字段用於描述支持的事件,這裏支持同步事件、按鍵事件、絕對座標事件,
    BIT宏實際就是對1進行位操作,定義在linux/bitops.h中*/
    input_dev->evbit[0] = BIT_MASK(EV_SYN) |BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS);
   
    /*keybit字段用於描述按鍵的類型,在input.h中定義了很多,這裏用BTN_TOUCH類型來表示觸摸屏的點擊*/
    input_dev->keybit[BIT_WORD(BTN_TOUCH)] =BIT_MASK(BTN_TOUCH);
 
    /*對於觸摸屏來說,使用的是絕對座標系統。這裏設置該座標系統中X和Y座標的最小值和最大值(0-1023範圍)
    ABS_X和ABS_Y就表示X座標和Y座標,ABS_PRESSURE就表示觸摸屏是按下還是擡起狀態*/
    input_set_abs_params(input_dev, ABS_X, 0,0x3FF, 0, 0);
    input_set_abs_params(input_dev, ABS_Y, 0,0x3FF, 0, 0);
    input_set_abs_params(input_dev,ABS_PRESSURE, 0, 1, 0, 0);
 
    /* 申請ADC中斷,AD轉換完成後觸發。這裏使用共享中斷IRQF_SHARED是因爲該中斷號在ADC驅動中也使用了,
    最後一個參數1是隨便給的一個值,因爲如果不給值設爲NULL的話,中斷就申請不成功*/
    if(request_irq(IRQ_ADC, stylus_action,IRQF_SHARED | IRQF_SAMPLE_RANDOM, input_dev->name, s3c2440_ts))
    {
        printk(KERN_ERR "s3c2440_ts.c:Could not allocate ts IRQ_ADC !\n");
       err = -EBUSY;
       goto fail2;
    }
 
    /*申請觸摸屏中斷,對觸摸屏按下或提筆時觸發*/
    if(request_irq(IRQ_TC, stylus_updown,IRQF_SAMPLE_RANDOM, input_dev->name, s3c2440_ts))
    {
        printk(KERN_ERR "s3c2440_ts.c:Could not allocate ts IRQ_TC !\n");
       err = -EBUSY;
       goto fail2;
    }
 
    /* 初始化完畢,註冊輸入子系統 */
    s3c2440_ts->input = input_dev;
    err =input_register_device(s3c2440_ts->input);
    if(err)
           gotofail3;
   
    /* 設置中斷觸發條件,IRQT_BOTHEDGE爲使能上升沿和下降沿觸發 */
    set_irq_type(IRQ_TC, IRQT_BOTHEDGE);
    set_irq_type(IRQ_ADC, IRQT_BOTHEDGE);
   
    return 0;
 
fail3:
       free_irq(IRQ_TC, (void *)s3c2440_ts);
       free_irq(IRQ_ADC, (void *)s3c2440_ts);
fail2:
       iounmap(base_addr);
fail1:
       input_free_device(input_dev);
       kfree(s3c2440_ts);
 
    return err;
}
 
static void __exit s3c2440ts_exit(void)
{
    /* 屏蔽並釋放中斷 */
    disable_irq(IRQ_TC);
    disable_irq(IRQ_ADC);
    free_irq(IRQ_TC, (void *)s3c2440_ts);
    free_irq(IRQ_ADC, (void *)s3c2440_ts);
   
    /* 註銷定時器 */
    del_timer_sync(&s3c2440_ts->timer);
 
    /* 屏蔽和禁止adc時鐘 */
    if(adc_clock)
    {
        clk_disable(adc_clock);
        clk_put(adc_clock);
        adc_clock = NULL;
    }
 
    /* 註銷觸摸屏輸入子系統 */
   input_unregister_device(s3c2440_ts->input);
   
    /* 釋放虛擬地址 */
    iounmap(base_addr);
   
    /* 釋放觸摸屏設備結構體 */
    kfree(s3c2440_ts);
}
 
module_init(s3c2440ts_init);
module_exit(s3c2440ts_exit);
 
MODULE_AUTHOR("KevinLee <www.ielife.cn>");
MODULE_DESCRIPTION("S3c2440TouchScreen Device Driver");
MODULE_VERSION("S3C2440TOUCHSCREEN 1.0");
MODULE_LICENSE("GPL");

1.5    添加Makefile及編譯模塊

Mkaefile腳本如下:

MODULENAME:= s3c2440_ts.o
 
ifneq($(KERNELRELEASE),)
#call from kernel build system
obj-m      := $(MODULENAME)
 
else
#KERNELDIR?= /lib/modules/$(shell uname -r)/build
KERNELDIR?= /work/system/linux-2.6.22.6
PWD       := $(shell pwd)
default:
       $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
 
clean:
       rm -rf *.o *~ core .depend .*.cmd *.ko*.mod.c .tmp_versions module* Module* $(APPNAME)
 
depend.depend dep:
       $(CC) $(CFLAGS) -M *.c > .depend
 
ifeq(.depend,$(wildcard .depend))
include.depend
endif

直接執行make,獲得s3c2440_ts.ko文件,insmod進入內核,點擊觸摸屏可以看到驅動中打印的信息。

insmod驅動模塊s3c2440_ts.ko之後,還可以通過cat /proc/bus/input/devices來查看輸入設備在輸入子系統中的信息:

I:Bus=0019 Vendor=0001 Product=0002 Version=0100

N:Name="s3c2410 Touchscreen"

P:Phys=s3c2440ts/input0

S:Sysfs=/class/input/input0

U:Uniq=

H:Handlers=mouse0 event0 evbug

B:EV=b

B:KEY=400 0 0 0 0 0 0 0 0 0 0

B:ABS=1000003

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章