基于Linux2.6.22.6内核的S3C2440的触摸屏驱动程序分析

本文来简单的分析一下Linux2.6.22.6内核自带的S3C2440的触摸屏驱动程序。

驱动程序在内核中的路径与文件名为:drivers\input\touchscreen\s3c2410_ts.c,这个文件的完整代码实现如下所示:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 

#include 
#include 

/* For ts.dev.id.version */
#define S3C2410TSVERSION	0x0101

#define WAIT4INT(x)  (((x)<<8) | \
		     S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
		     S3C2410_ADCTSC_XY_PST(3))

#define AUTOPST	     (S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
		     S3C2410_ADCTSC_AUTO_PST | S3C2410_ADCTSC_XY_PST(0))

#define DEBUG_LVL    KERN_DEBUG

MODULE_AUTHOR("Arnaud Patard ");
MODULE_DESCRIPTION("s3c2410 touchscreen driver");
MODULE_LICENSE("GPL");

/*
 * Definitions & global arrays.
 */

//static volatile int bADCForTS;
//struct semaphore gADClock;
//EXPORT_SYMBOL(gADClock);

static char *s3c2410ts_name = "s3c2410 TouchScreen";

/*
 * Per-touchscreen data.
 */

struct s3c2410ts {
	struct input_dev *dev;
	long xp;
	long yp;
	int count;
	int shift;
};

static struct s3c2410ts ts;
static void __iomem *base_addr;

static inline void s3c2410_ts_connect(void)
{
	s3c2410_gpio_cfgpin(S3C2410_GPG12, S3C2410_GPG12_XMON);
	s3c2410_gpio_cfgpin(S3C2410_GPG13, S3C2410_GPG13_nXPON);
	s3c2410_gpio_cfgpin(S3C2410_GPG14, S3C2410_GPG14_YMON);
	s3c2410_gpio_cfgpin(S3C2410_GPG15, S3C2410_GPG15_nYPON);
}

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 (ts.count != 0) {
			long tmp;  /* add by www.100ask.net */

			tmp = ts.xp;
			ts.xp = ts.yp;
			ts.yp = tmp;

 			ts.xp >>= ts.shift;
 			ts.yp >>= ts.shift;

#ifdef CONFIG_TOUCHSCREEN_S3C2410_DEBUG
 			{
 				struct timeval tv;
 				do_gettimeofday(&tv);
 				printk(DEBUG_LVL "T: %06d, X: %03ld, Y: %03ld\n", (int)tv.tv_usec, ts.xp, ts.yp);
 			}
#endif

 			input_report_abs(ts.dev, ABS_X, ts.xp);
 			input_report_abs(ts.dev, ABS_Y, ts.yp);

 			input_report_key(ts.dev, BTN_TOUCH, 1);
 			input_report_abs(ts.dev, ABS_PRESSURE, 1);
 			input_sync(ts.dev);
 		}

 		ts.xp = 0;
 		ts.yp = 0;
 		ts.count = 0;

//		if (!down_trylock(&gADClock)) {
//			bADCForTS = 1;
  		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 {
 		ts.count = 0;

 		input_report_key(ts.dev, BTN_TOUCH, 0);
 		input_report_abs(ts.dev, ABS_PRESSURE, 0);
 		input_sync(ts.dev);

 		iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
 	}
}

static struct timer_list touch_timer =
		TIMER_INITIALIZER(touch_timer_fire, 0, 0);

static irqreturn_t stylus_updown(int irq, void *dev_id)
{
	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));

	/* TODO we should never get an interrupt with updown set while
	 * the timer is running, but maybe we ought to verify that the
	 * timer isn't running anyways. */

	if (updown)
		touch_timer_fire(0);

	return IRQ_HANDLED;
}


static irqreturn_t stylus_action(int irq, void *dev_id)
{
	unsigned long data0;
	unsigned long data1;

//	if (bADCForTS) {
	
		data0 = ioread32(base_addr+S3C2410_ADCDAT0);
		data1 = ioread32(base_addr+S3C2410_ADCDAT1);
	
		ts.xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;
		ts.yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;
		ts.count++;

//		bADCForTS = 0;
//		up(&gADClock);
			
	    if (ts.count < (1<dev.platform_data;

	if (!info)
	{
		printk(KERN_ERR "Hm... too bad : no platform data for ts\n");
		return -EINVAL;
	}

#ifdef CONFIG_TOUCHSCREEN_S3C2410_DEBUG
	printk(DEBUG_LVL "Entering s3c2410ts_init\n");
#endif

	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);

#ifdef CONFIG_TOUCHSCREEN_S3C2410_DEBUG
	printk(DEBUG_LVL "got and enabled clock\n");
#endif

	base_addr=ioremap(S3C2410_PA_ADC,0x20);
	if (base_addr == NULL) {
		printk(KERN_ERR "Failed to remap register block\n");
		return -ENOMEM;
	}

	/* Configure GPIOs */
	s3c2410_ts_connect();

	if ((info->presc&0xff) > 0)	// 对ADC进行时钟分频
		iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(info->presc&0xFF),\
			     base_addr+S3C2410_ADCCON);
	else
		iowrite32(0,base_addr+S3C2410_ADCCON);


	/* Initialise registers */
	if ((info->delay&0xffff) > 0)	// 设置ADC开始转换的延时
		iowrite32(info->delay & 0xffff,  base_addr+S3C2410_ADCDLY);

	// 等待触摸屏按下
	iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);

	/* Initialise input stuff */
	memset(&ts, 0, sizeof(struct s3c2410ts));
	input_dev = input_allocate_device();

	if (!input_dev) {
		printk(KERN_ERR "Unable to allocate the input device !!\n");
		return -ENOMEM;
	}

	ts.dev = input_dev;
	ts.dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS);
	ts.dev->keybit[LONG(BTN_TOUCH)] = BIT(BTN_TOUCH);
	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);

	ts.dev->private = &ts;
	ts.dev->name = s3c2410ts_name;
	ts.dev->id.bustype = BUS_RS232;
	ts.dev->id.vendor = 0xDEAD;
	ts.dev->id.product = 0xBEEF;
	ts.dev->id.version = S3C2410TSVERSION;

	ts.shift = info->oversampling_shift;

	/* Get irqs */
	if (request_irq(IRQ_ADC, stylus_action, IRQF_SAMPLE_RANDOM | SA_SHIRQ,
		"s3c2410_action", ts.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", ts.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);

	/* All went ok, so register to the input system */
	input_register_device(ts.dev);

	return 0;
}

static int s3c2410ts_remove(struct platform_device *pdev)
{
	disable_irq(IRQ_ADC);
	disable_irq(IRQ_TC);
	free_irq(IRQ_TC,ts.dev);
	free_irq(IRQ_ADC,ts.dev);

	if (adc_clock) {
		clk_disable(adc_clock);
		clk_put(adc_clock);
		adc_clock = NULL;
	}

	input_unregister_device(ts.dev);
	iounmap(base_addr);

	return 0;
}

static struct platform_driver s3c2410ts_driver = {
       .driver         = {
	       .name   = "s3c2410-ts",
	       .owner  = THIS_MODULE,
       },
       .probe          = s3c2410ts_probe,
       .remove         = s3c2410ts_remove,
};


static int __init s3c2410ts_init(void)
{
//	init_MUTEX(&gADClock);
	return platform_driver_register(&s3c2410ts_driver);
}

static void __exit s3c2410ts_exit(void)
{
	platform_driver_unregister(&s3c2410ts_driver);
}

module_init(s3c2410ts_init);
module_exit(s3c2410ts_exit);


为了更好地对这个驱动程序文件进行理解,我把整个驱动程序分成了三个部分:

1、平台、总线、设备驱动模型

2、Linux输入子系统和硬件相关

3、触摸屏和ADC中断事件的处理


下面按照上面提出的三个部分逐渐分析:

1、平台、总线、设备驱动模型

看一个驱动程序,一般从入口和出口的地方开始看,本驱动的入口函数如下所示:

static int __init s3c2410ts_init(void)
{
//	init_MUTEX(&gADClock);
	return platform_driver_register(&s3c2410ts_driver);
}
从里面可以看出它注册了一个平台驱动,这个平台驱动的原型如下所示:
static struct platform_driver s3c2410ts_driver = {
       .driver         = {
	       .name   = "s3c2410-ts",
	       .owner  = THIS_MODULE,
       },
       .probe          = s3c2410ts_probe,
       .remove         = s3c2410ts_remove,
};
有了这个平台驱动,要想让这个驱动程序能够发挥作用,必须定义一个同名的平台设备,经过查找可以看出,平台设备定义在arch\arm\plat-s3c24xx\devs.c这个文件中,具体实现如下:
/* Touchscreen */
struct platform_device s3c_device_ts = {
	.name		  = "s3c2410-ts",
	.id		  = -1,
};

EXPORT_SYMBOL(s3c_device_ts);

static struct s3c2410_ts_mach_info s3c2410ts_info;

void __init set_s3c2410ts_info(struct s3c2410_ts_mach_info *hard_s3c2410ts_info)
{
	memcpy(&s3c2410ts_info, hard_s3c2410ts_info, sizeof(struct s3c2410_ts_mach_info));
	s3c_device_ts.dev.platform_data = &s3c2410ts_info;
}

从上面可以看出,除了定义一个平台设备之外,还定义了一个设置平台设备平台数据的函数,这个函数在arch\arm\plat-s3c24xx\common-smdk.c这个文件当中被调用,添加的这个平台数据的内容如下所示:

static struct s3c2410_ts_mach_info s3c2410_ts_cfg = {
        .delay = 10000,
        .presc = 49,
        .oversampling_shift = 2,
};

这个结构体当中,主要包括三个数据成员,delay主要用来表示每次ADC转换时的延迟时间,presc表示ADC的分频系数,oversampling_shift表示采样时优化采样值。


2、LInux输入子系统和硬件相关

从平台设备驱动模型可以知道整个驱动程序匹配完成后将调用平台驱动的probe函数,这个函数的原型为:s3c2410ts_probe(),它里面主要完成了两部分工作:输入子系统的实现和硬件相关的操作:
2.1 输入子系统的具体实现如下
a、分配一个input_dev的结构体指针变量

input_dev = input_allocate_device();
b、设置这个结构体指针变量

ts.dev = input_dev;
ts.dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS);	// 设置支持同步类、按键类、绝对位移类事件
ts.dev->keybit[LONG(BTN_TOUCH)] = BIT(BTN_TOUCH);		// 支持触摸按键事件
input_set_abs_params(ts.dev, ABS_X, 0, 0x3FF, 0, 0);		// 支持X方向绝对位移事件
input_set_abs_params(ts.dev, ABS_Y, 0, 0x3FF, 0, 0);		// 支持Y方向绝对位移事件
input_set_abs_params(ts.dev, ABS_PRESSURE, 0, 1, 0, 0);		// 支持压力的绝对位移事件

ts.dev->private = &ts;						// 进行其它相关的设置
ts.dev->name = s3c2410ts_name;
ts.dev->id.bustype = BUS_RS232;
ts.dev->id.vendor = 0xDEAD;
ts.dev->id.product = 0xBEEF;
ts.dev->id.version = S3C2410TSVERSION;
c、注册这个结构体指针变量
input_register_device(ts.dev);
以上几步实现了触摸屏驱动程序的输入子系统化,完成了以上几步就可以实现当发生触摸相关的事件时直接提交相关输入事件就可以了。
2.2 硬件相关的操作

在了解具体的硬件操作之前,先要看看涉及到那些寄存器。

电源管理相关的寄存器,因为Linux内核在上电时把ADC的时钟给关闭了,所以需要重新打开时钟,这个寄存器的主要内容如下所示:



ADC相关的寄存器:

基于Linux2.6.22.6内核的S3C2440的触摸屏驱动程序分析_第1张图片

具体的硬件操作如下:

a、使能ADC的时钟,如下所示:

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);
这部分代码的具体实现就是将CLKCON寄存器的bit15设为1.

b、映射IO操作的寄存器

base_addr=ioremap(S3C2410_PA_ADC,0x20);
将ADC的物理地址映射为Linux当中可以访问的虚拟地址。
c、具体的寄存器操作

/* Configure GPIOs */
s3c2410_ts_connect();	// 配置触摸屏和S3C2440连接的GPIO引脚

if ((info->presc&0xff) > 0)	// 对ADC进行时钟分频,分频系数来自平台设备传入的平台数据
	iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(info->presc&0xFF),\
		base_addr+S3C2410_ADCCON);
else
	iowrite32(0,base_addr+S3C2410_ADCCON);


/* Initialise registers */
if ((info->delay&0xffff) > 0)	// 设置ADC开始转换的延时
	iowrite32(info->delay & 0xffff,  base_addr+S3C2410_ADCDLY);

// 等待触摸屏按下
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);	
上面这些代码的主要功能是:首先配置和触摸屏相连的GPIO引脚;然后对ADC进行时钟分频,分频后ADC的时钟为1MHz;接下来为了减小触摸值的误差,设置了以下ADC的转换延时,最后等待触摸屏按下的操作。
d、注册触摸屏响应中断和ADC转换完成中断

/* Get irqs */
	if (request_irq(IRQ_ADC, stylus_action, IRQF_SAMPLE_RANDOM | SA_SHIRQ,
		"s3c2410_action", ts.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", ts.dev)) {
		printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_TC !\n");
		iounmap(base_addr);
		return -EIO;
	}
实现了这两个中断的基本作用是:当触摸屏按下时会调用触摸屏响应中断函数stylus_updown(),这是会启动ADC转换,当ADC转换完毕后要会启动ADC转换中断,通过ADC转换中断把触摸值通过输入子系统提交。

3、触摸屏和ADC中断事件的处理
  和触摸屏以及ADC中断事件相关的函数主要有三个:

a、和触摸屏中断相关的处理函数,实现如下:

static irqreturn_t stylus_updown(int irq, void *dev_id)
{
	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));	

	/* TODO we should never get an interrupt with updown set while
	 * the timer is running, but maybe we ought to verify that the
	 * timer isn't running anyways. */

	if (updown)	// 如果触摸屏按下则调用touch_timer_fire()这个函数
		touch_timer_fire(0);

	return IRQ_HANDLED;
}

b、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 (ts.count != 0) {	// 如果count值不为0,则将触摸按下事件提交,如果为0,则启动ADC并进入测量模式
			long tmp;  /* add by www.100ask.net */

			tmp = ts.xp;
			ts.xp = ts.yp;
			ts.yp = tmp;

 			ts.xp >>= ts.shift;
 			ts.yp >>= ts.shift;

#ifdef CONFIG_TOUCHSCREEN_S3C2410_DEBUG
 			{
 				struct timeval tv;
 				do_gettimeofday(&tv);
 				printk(DEBUG_LVL "T: %06d, X: %03ld, Y: %03ld\n", (int)tv.tv_usec, ts.xp, ts.yp);
 			}
#endif

 			input_report_abs(ts.dev, ABS_X, ts.xp);
 			input_report_abs(ts.dev, ABS_Y, ts.yp);

 			input_report_key(ts.dev, BTN_TOUCH, 1);
 			input_report_abs(ts.dev, ABS_PRESSURE, 1);
 			input_sync(ts.dev);
 		}

 		ts.xp = 0;
 		ts.yp = 0;
 		ts.count = 0;

//		if (!down_trylock(&gADClock)) {
//			bADCForTS = 1;
  		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 {
 		ts.count = 0;

 		input_report_key(ts.dev, BTN_TOUCH, 0);
 		input_report_abs(ts.dev, ABS_PRESSURE, 0);
 		input_sync(ts.dev);

 		iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
 	}
}

c、和ADC相关的中断函数的实现如下:


static irqreturn_t stylus_action(int irq, void *dev_id)
{
	unsigned long data0;
	unsigned long data1;

//	if (bADCForTS) {
	
		data0 = ioread32(base_addr+S3C2410_ADCDAT0);
		data1 = ioread32(base_addr+S3C2410_ADCDAT1);
	
		ts.xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;
		ts.yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;
		ts.count++;

//		bADCForTS = 0;
//		up(&gADClock);
	    /* 对ADC获得的值进行过滤优化,优化参数来自平台设备的设备数据成员 */		
	    if (ts.count < (1<	// 如果优化完毕则重新设置定时器的超时时间,并设置触摸屏为等待松开模式
			mod_timer(&touch_timer, jiffies+1);
			iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC);
		}
//	}
	return IRQ_HANDLED;
}

对以上三个函数进行分析可以看出:当触摸屏被按下时,stylus_updown()这个函数会被执行并判断触摸屏有没有按下,如果没按下则直接退出不执行,否者执行touch_timer_fire()这个函数,首先也是判断触摸屏是否按下,如果没有按下则上报没有按下事件,并重新设置触摸屏进入等待按下模式,如果按下则首先判断count值等不等于0,如果不等于则上报按下事件,之后启动ADC,当ADC转换完毕后会调用stylus_action()这个函数,这个函数会对count值进行加一操作,并判断count值和平台设备的平台数据传入的值进行比较,如果小于这个值则再次启动ADC转换,否者修改定时器的超时时间并设置触摸屏进入等待松开模式,等超时时间到来后会再次调用touch_timer_fire()这个函数,然后继续按照之前的执行关系继续下去,直到松开触摸屏为止。

整个过程大致分析如上面所示,由于本人能力有限以及行文仓促,文中难免有错误和不足的地方,还请见谅和指教。



你可能感兴趣的:(Linux设备驱动开发,基于JZ2440的设备驱动开发)