申明:本文章是看完韦东山老师的触摸屏驱动视频所写的关于触摸屏的驱动,因而假如有相关内容与其他网友相同,敬请宽恕。同时我还是想说本文只是总结自己的学习所得,同时也将自己所学到的知识写出来,所以假如这篇文章对你有帮助,那是我的荣幸。

本文主要介绍韦东山老师视频中所讲的触摸屏驱动,同时就会将s3c2410_ts.c这个驱动程序讲一下。其实老师讲的驱动是对这个驱动程序的精简和整理,但内核的程序我们还是要自己读懂比较好,这样之后自己写其他内核驱动程序时会很有帮助。

在这儿我介绍一篇不错的讲解s3c2410_ts.c的文章:linux嵌入式系统开发之触摸屏—驱动篇(下/源码剖析)

而我在自己的上一篇文章中早已翻译了s3c2440数据指南中的ADC与触摸屏这章:S3C2440-ADC触摸屏,所以有硬件方面不清楚的可以看这一章了解触摸屏寄存器的工作模式以及怎样设置。

下边我们言归正传开始讲触摸屏的驱动程序:

触摸屏驱动程序是输入子系统中的一类,所以我们按输入子系统的方式写驱动程序,所以步骤如下:

分配一个input_dev结构体设置这个结构体用于触摸屏驱动注册这个结构体做硬件相关的设置

而前面步骤1,2,3所对应的程序为:

/* 1. 分配一个input_dev结构体 */
	s3c_ts_dev = input_allocate_device();
	/* 1.1 初始化定时器 */
	init_timer(&ts_timer);
	ts_timer.function = s3c_ts_timer_func; //这个为定时器的处理函数
	add_timer(&ts_timer);
	
	/* 2.  设置这个结构体 */
	/* 2.1 设置他产生哪类事件 */
	set_bit(EV_KEY, s3c_ts_dev->evbit);      //按键类事件
	set_bit(EV_ABS,s3c_ts_dev->evbit);	//绝对位移事件
	
	/* 2.2 设置其产生这类事件中的那个具体事件 */
	set_bit(BTN_TOUCH,s3c_ts_dev->keybit);      //按键类事件中的触摸屏按键,此处将触摸屏抽象为按键,笔按下时为		                                       //键被按下,笔抬起时为键被抬起 /

	input_set_abs_params(s3c_ts_dev,ABS_X,0,0x3ff,0,0);  //绝对位移中的x方向,此处的0x3ff是因为触摸屏为10位ADC
	input_set_abs_params(s3c_ts_dev,ABS_Y,0,0x3ff,0,0);  //绝对位移中的y方向,此处的0x3ff是因为触摸屏为10位ADC
	input_set_abs_params(s3c_ts_dev,ABS_PRESSURE,0,1,0,0);//绝对位移中的压力方向,此处只有抬起和按下两种情况
	/* 3.  注册这个结构体 */
	input_register_device(s3c_ts_dev);

如今已将input_dev结构体设置好了,接出来就是对触摸屏硬件的设置了,而在设置硬件之前要先向你们介绍一下触摸屏的使用过程:

按下触摸屏形成中断

在触摸屏中断处理程序中,将通过ADC采集的电流值转化为XY方向的座标值,之后启动ADC。

AD转化结束形成ADC中断。

在ADC中断处理函数中上报风波,并启动定时器(用于处理长按和滑动风波)

定时器时间到,跳到步骤2继续往下执行

直至握住触摸屏

下边是硬件寄存器和中断的设置,为里面触摸屏的操作做打算,程序为:

/* 4.  硬件相关 */
	/* 4.1 使能时钟(CLKCON[15])使能ADC模块 */
	clk = clk_get(NULL,"adc");
	clk_enable(clk);
	
	/* 4.2 设置S3c的ADC寄存器 */
	s3c_ts_regs = ioremap(0x58000000, sizeof(struct s3c_ts_reg));
	/*
	*ADCCON:
	*bit[14]    PRSCEN :A/D converter 预分频使能
	*				1 = Enable
	*bit[13:6]  PRSCVL :预分频系数
	*				49:	PLCK/(PRSCVL+1) = 50MHZ/(49+1) = 1MHZ
	*bit[0]     ENABLE_START :ADC开始转化使能
	*				此处先设为0
	*/
	s3c_ts_regs->ADCCON = (1<<14) | (49<<6) | (0<<0);
	/* ADCDLY

触摸屏驱动程序_linux触摸屏驱动_触摸屏驱动安装教程

*bit[15:0] DELAY :ADC转化开始延迟值 * =0xffff; */ s3c_ts_regs->ADCDLY = 0xffff; /* 4.3 注册中断,按下触摸屏产生中断 */ request_irq(IRQ_TC,pen_down_up_irq,IRQF_SAMPLE_RANDOM,"ts_pen",NULL); /* 4.5 ADC转化结束,产生ADC中断 */ request_irq(IRQ_ADC,adc_irq,IRQF_SAMPLE_RANDOM,"ts_adc",NULL); enter_wait_pen_down_mode();

里面程序中的s3c_ts_regs是触摸屏寄存器的一个总的集合,并将其装入一个结构体中,代码为:

struct s3c_ts_reg {
	unsigned long ADCCON;
	unsigned long ADCTSC;
	unsigned long ADCDLY;
	unsigned long ADCDAT0;
	unsigned long ADCDAT1;
	unsigned long ADCUPDN;
};

里面是打算代码,下边我们将模仿触摸屏按下的过程来剖析代码:

假定我们程序早已写好linux触摸屏驱动,但是在开发版上运行正常,当你拿起笔按在触摸屏上的一点,由于在之前我们早已注册了IRQ_TC中断:

request_irq(IRQ_TC,pen_down_up_irq,IRQF_SAMPLE_RANDOM,"ts_pen",NULL); 

所以当你按下屏幕时触发中断,因而处理相应的中断处理函数:pen_down_up_irq,详尽的代码为:

static irqreturn_t pen_down_up_irq(int irq,void *dev_id)
{
	if(s3c_ts_regs->ADCDAT0 & (1<<15)){      //判断屏幕是否还是处于按下状态
		//printk("pen upn");     //处于抬起状态,上报抬起事件
		input_report_abs(s3c_ts_dev,ABS_PRESSURE,0);
		input_report_key(s3c_ts_dev,BTN_TOUCH,0);
		input_sync(s3c_ts_dev);
		enter_wait_pen_down_mode();  //进入等待按下模式
	}else{
		//此时屏幕还处于按下状态
		/* 4.4 在中断中启动ADC,转化XY坐标 */
	//	printk("pen downn");
	//	enter_wait_pen_up_mode();
		enter_measure_xy_mode();
		start_adc();
	}
	return IRQ_HANDLED;
}

如前面程序所示,此时若果你松开了你的笔,程序将上报风波,而假如你还处于按下状态,我们将继续跟随程序走:

因为你早已注册了ADC中断:

request_irq(IRQ_ADC,adc_irq,IRQF_SAMPLE_RANDOM,"ts_adc",NULL); 

而里面的程序早已开启了ADC:

start_adc()

,所以我们将步入ADC的中断处理函数adc_irq,详尽的代码:

static irqreturn_t adc_irq(int irq,void *dev_id)
{
	static int cnt;
	static int x[4],y[4];
	
	x[cnt] = ((s3c_ts_regs->ADCDAT0)&0x3ff);
	y[cnt] = ((s3c_ts_regs->ADCDAT1)&0x3ff);
	cnt++;
	if(s3c_ts_regs->ADCDAT0 & (1<<15)){
		//printk("pen upn");
		input_report_abs(s3c_ts_dev,ABS_PRESSURE,0);
		input_report_key(s3c_ts_dev,BTN_TOUCH,0);
		input_sync(s3c_ts_dev);
		enter_wait_pen_down_mode();
	}else{
		if(cnt == 4){
			//printk("adc_irq x= %d, y=%d n",(x[0]+x[1]+x[2]+x[3])/4,(y[0]+y[1]+y[2]+y[3])/4);
			input_report_abs(s3c_ts_dev,ABS_X,(x[0]+x[1]+x[2]+x[3])/4);
			input_report_abs(s3c_ts_dev,ABS_Y,(y[0]+y[1]+y[2]+y[3])/4);
			input_report_abs(s3c_ts_dev,ABS_PRESSURE,1);
			input_report_key(s3c_ts_dev,BTN_TOUCH,1);
			input_sync(s3c_ts_dev);
			
			cnt = 0;
			/* 4.6 在ADC中断处理函数中上报(input_event),启动定时器 */
			mod_timer(&ts_timer,jiffies + HZ/100);
			
			enter_wait_pen_up_mode();
		}else{
			enter_measure_xy_mode();
			start_adc();
		}
	}
mod_timer(&ts_timer,jiffies + HZ/100);
return IRQ_HANDLED;}

而通过前面的程序,我们晓得了触摸屏的处理过程,此时,触摸屏驱动早已基本写好了。而下边就是对特殊的处理,如当长按或则滑动时,此时我们将通过定时器来处理这样的问题,加入此时你还没有松手,触摸屏还处于按下状态。(你可能感觉如何如此长时间了笔还没举起啊,虽然里面剖析的可能麻烦,但在程序中前面的代码还是跑的很快的)。这么,我们接出来就是对定时器的剖析了,由于在上文中我们启动了定时器:mod_timer(&ts_timer,jiffies+HZ/100);

当定时时间(此处我们设置定时时间为10ms)到时linux触摸屏驱动,步入定时器处理函数:

void s3c_ts_timer_func(unsigned long data)
{
	/* 4.7 定时器时间到,在定时器处理函数中,重复4.4的步骤(用于处理长按和滑动) */
	if(s3c_ts_regs->ADCDAT0 & (1<<15)){   
		//printk("pen upn");
		input_report_abs(s3c_ts_dev,ABS_PRESSURE,0);
		input_report_key(s3c_ts_dev,BTN_TOUCH,0);
		input_sync(s3c_ts_dev);
		enter_wait_pen_down_mode();
	}else{
		
		/* 4.4 在中断中启动ADC,转化XY坐标 */
	//	printk("pen downn");
	//	enter_wait_pen_up_mode();
		enter_measure_xy_mode();
		start_adc();
	}
}

在前面的程序中,可以看出定时时间到后定时器又会启动ADC之后将接着步骤2继续执行,仍然这样循环,直至抬起,而这样也就可以处理长按或则滑动了,当你长按或则滑动时,定时时间到后会采集你所在的位置的XY座标,之后上报,之后继续步入定时器,继续步入ADC,继续采集上报,直至你握住触摸屏。

好了linux虚拟主机,里面就是触摸屏驱动的大致流程和代码剖析了。

下边我们即将按着上面所讲的方式和步骤去剖析s3c2410_ts.c这个驱动文件。

首先剖析一个驱动文件,第一个要看的就是他的入口函数:

platform_driver_register(&s3c2410ts_driver);

这个入口函数中注册了一个platform_driver的结构体,而这个结构体的probe函数就是当配对成功时,首先步入的函数:

下边我们进probe中剖析:

static int __init s3c2410ts_probe(struct platform_device *pdev)

触摸屏驱动安装教程_触摸屏驱动程序_linux触摸屏驱动

在s3c2410_ts.c中首先对硬件的操作:

	adc_clock = clk_get(NULL, "adc");
	if (!adc_clock) {
		printk(KERN_ERR "failed to get adc clock sourcen");
		return -ENOENT;
	}
	clk_enable(adc_clock);

这个是对CLKCON的寄存器中ADC模块的使能。

base_addr=ioremap(S3C2410_PA_ADC,0x20);  //对s3c2410z中寄存器的remap
	if (base_addr == NULL) {
		printk(KERN_ERR "Failed to remap register blockn");
		return -ENOMEM;
	}
	/* 对触摸屏寄存器的操作 */
	s3c2410_ts_connect();
	if ((info->presc&0xff) > 0)
		iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(info->presc&0xFF),
			     base_addr+S3C2410_ADCCON);
	else
		iowrite32(0,base_addr+S3C2410_ADCCON);
	/* 初始化寄存器 */
	if ((info->delay&0xffff) > 0)
		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();      //分配一个input_dev结构体
	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);         //绝对位移在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;
	ts.shift = info->oversampling_shift;

而接出来就是对中断的设置了:

	if (request_irq(IRQ_ADC, stylus_action, IRQF_SAMPLE_RANDOM | SA_SHIRQ,

触摸屏驱动安装教程_linux触摸屏驱动_触摸屏驱动程序

"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; }

当里面所有的工作做完后我们就可以注册这个设备了:

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

而接出来就是和前面一样对触摸屏按下和举起过程进行剖析了:

我们同样先将整个过程写出来,让你们有个大致过程的认识:

1)按下触摸屏,触发触摸屏中断之后步入stylus_updown,假如触摸屏状态为按下,则调用touch_timer_fire启动ADC转换;

2)当ADC启动后,触发ADC中断即步入stylus_action,假如转换的次数大于4,则重新启动ADC进行转换,假如4次完毕后,启动1个时间滴答的定时器,

停止ADC转换,也就是说在这个时间滴答内,ADC转换是停止的;这儿为何要在1个时间滴答到来之前停止ADC的转换呢?这是为了避免屏幕晃动。

3)假如1个时间滴答到来则步入定时器服务程序touch_timer_fire,判定触摸屏依旧处于按下状态则上报风波和转换的数据,并重启ADC转换,重复第(2)步;

4)假如触摸举起了,则上报释放风波,并将触摸屏重新设置为等待中断状态。

里面的步骤是从linux嵌入式系统开发之触摸屏—驱动篇(下/源码剖析),中抄来的。

下边就带着这个过程用代码剖析:

首先触摸屏按下,触发触摸屏中断处理函数:

static irqreturn_t stylus_updown(int irq, void *dev_id)
{
	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));
	/* 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);      //调用touch_timer_fire函数
	return IRQ_HANDLED;
}

我们假定如今还处于按下状态,所以将继续调用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));
 	if (updown) {   //屏幕被按下
 		if (ts.count != 0) {   //判断是否采集的次数为0

linux触摸屏驱动_触摸屏驱动安装教程_触摸屏驱动程序

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; /* 将采集的到的平均值上报 */ 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); } /* 如果还没有开启ADC采集,则设置XP,yp,count为0,并开启ADC */ ts.xp = 0; ts.yp = 0; ts.count = 0; /* 开启ADC */ 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,并上报事件。 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); } }

从里面我们晓得想要得到XY的座标值要开启ADC后在ADC中断处理函数中能够采集XY的值,所以下边剖析ADC中断函数:

static irqreturn_t stylus_action(int irq, void *dev_id)
{
	unsigned long data0;
	unsigned long data1;
	
	data0 = ioread32(base_addr+S3C2410_ADCDAT0);
	data1 = ioread32(base_addr+S3C2410_ADCDAT1);
	/* 采集XY的坐标值,并多次测量,而在后面求平均值 */
	ts.xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;  //因为ADC为10位,所以要使用有效位
	ts.yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;
	ts.count++;   //此处count就是采集的次数
			
	 if (ts.count < (1<<ts.shift)) {  //当没有采集到约定的值时,开启ADC继续采集
		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 {  //而当采集到约定的次数时,进入开启定时器,然后进定时器处理函数中对XY坐标进	                   //行处理
			mod_timer(&touch_timer, jiffies+1);
			iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC);
		}
	return IRQ_HANDLED;
}

通过前面的剖析我们可以看出,老师所讲的方式和内核所提供的方式linux 论坛,她们大致的内容是一样的,不同的地方只是具体内容在不同的位置设定。

Tagged:
Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复