触摸屏驱动

前言

       之前有篇文章简单介绍过触摸屏的工作原理,可以参考一下:电阻触摸屏原理简述。测出某个点的电压值后,我们就可以通过启动ADC模块,将电压转换为X,Y坐标了。

       现在我们想写一个基于Linux系统的触摸屏驱动程序,先把程序的步骤写出来:

触摸屏的使用过程:
1、按下,产生中断
2、在中断程序里,启动ADC转换x,y坐标
3、ADC结束,产生ADC中断
4、在ADC中断处理函数里,上报(input_event),启动定时器
5、定时器时间到,又回到第二步(处理长按和滑动)
6、松开

后面就会按照上面的步骤写出我们的驱动程序。

正文

先贴出写好的代码,再做解释

#include 
#include 

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

/*
触摸屏的使用过程:
1、按下,产生中断
2、在中断程序里,启动ADC转换x,y坐标
3、ADC结束,产生ADC中断
4、在ADC中断处理函数里,上报(input_event),启动定时器
5、定时器时间到,又回到第二步(处理长按和滑动)
6、松开
*/

static struct input_dev *s3c_ts_dev;
struct s3c_ts_regs {
	unsigned long adccon;
	unsigned long adctsc;
	unsigned long adcdly;
	unsigned long adcdat0;
	unsigned long adcdat1;
	unsigned long adcupdn;
};

static volatile struct s3c_ts_regs *s3c_ts_regs;
static struct timer_list ts_timer;

static enter_wait_pen_down_mode()
{
	/* 进入等待触摸笔按下的模式 */
	s3c_ts_regs->adctsc = 0xd3;
}

static enter_wait_pen_up_mode()
{
	/* 进入等待触摸笔松开的模式 */
	s3c_ts_regs->adctsc = 0x1d3;
}

static void enter_measure_xy_mode(void)
{
	/* 进入XY坐标的测量模式 */
	s3c_ts_regs->adctsc = (1<<2) | (1<<3);
}

static start_adc()
{
	/* 启动ADC进行电压测量 */
	s3c_ts_regs->adccon |= (1<<0);
}

static irqreturn_t pen_down_up_irq(int irq, void *dev_id)
{
	/* 进入处理触摸笔松开或按下的中断 */
	if (s3c_ts_regs->adcdat0 & (1<<15)) {
		//printk("pen up\n");
		input_report_key(s3c_ts_dev, BTN_TOUCH, 0);
 		input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);
		input_sync(&s3c_ts_dev);
		enter_wait_pen_down_mode();
	} else {
		//printk("pen down\n");
		//enter_wait_pen_up_mode();
		enter_measure_xy_mode();
		start_adc();/*成功启动后,会进入中断模式,也就是调用adc_irq函数*/
	}
	return IRQ_HANDLED;
}

static int s3c_filter_ts(int x[], int y[])
{
#define DEVIATION 10
	int avr_x, avr_y;
	int det_x, det_y;
	
	avr_x = (x[0] + x[1])/2;
	avr_y = (y[0] + y[1])/2;

	det_x = (avr_x > x[2])?(avr_x - x[2]):(x[2] - avr_x);
	det_y = (avr_y > y[2])?(avr_y - y[2]):(y[2] - avr_y);
	if (det_x > DEVIATION || det_y > DEVIATION)
		return 0;

	avr_x = (x[1] + x[2])/2;
	avr_y = (y[1] + y[2])/2;

	det_x = (avr_x > x[3])?(avr_x - x[3]):(x[3] - avr_x);
	det_y = (avr_y > y[3])?(avr_y - y[3]):(y[3] - avr_y);
	if (det_x > DEVIATION || det_y > DEVIATION)
		return 0;
	return 1;
}

static irqreturn_t adc_irq(int irq, void *dev_id)
{
	/* 进入ADC测量的中断模式 */
	static int x[4], y[4];
	int adcdata0, adcdata1;
	int avg_x, avg_y;
	static int cnt = 0;

	/* 优化措施2:
	  *当松开时,不进行测量,并抛弃这个按键值,
	  *因为测量过程中就松开,测出来的是不准确的
        */
    adcdata0 = s3c_ts_regs->adcdat0;/*读取X坐标的值*/
	adcdata1 = s3c_ts_regs->adcdat1;/*读取Y坐标的值*/

	if (s3c_ts_regs->adcdat0 & (1<<15)) {
		/* 已经松开了 */
		cnt  = 0;
		input_report_key(s3c_ts_dev, BTN_TOUCH, 0);
 		input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);
		input_sync(&s3c_ts_dev);
		enter_wait_pen_down_mode();
	} else {
		/*优化措施3:
		  *取多次读取结果的平均值
		  */
		x[cnt] = adcdata0 & 0x3ff;
		y[cnt] = adcdata1 & 0x3ff;
		cnt++;
		if (4 == cnt) {
			/*优化措施4:
			  *读取到四次值后,如果读取到的值
			  *和它前面两次的平均值的差值大于某个值,
			  *我们就认为这次的读取不精确,从头
			  *再读取
			  */
			if (s3c_filter_ts(x, y)) {
				avg_x = (x[0]+x[1]+x[2]+x[3])/4;
				avg_y = (y[0]+y[1]+y[2]+y[3])/4;
				//printk("adc_irq x = %d, y = %d\n", avg_x, avg_y);
				input_report_abs(s3c_ts_dev, ABS_X, avg_x);
				input_report_abs(s3c_ts_dev, ABS_Y, avg_y);

				input_report_key(s3c_ts_dev, BTN_TOUCH, 1);
 				input_report_abs(s3c_ts_dev, ABS_PRESSURE, 1);
				input_sync(&s3c_ts_dev);
			}
			cnt = 0;
			enter_wait_pen_up_mode();
			/*10ms后再启动定时器*/
			mod_timer(&ts_timer, jiffies + HZ/100);
		} else {
			enter_measure_xy_mode();
			start_adc();
		}
	}
	return IRQ_HANDLED;
}

static void s3c_ts_timer_function(unsigned long data)
{
	if (s3c_ts_regs->adcdat0 & (1<<15)) {
		/*松开的情况*/
		input_report_key(s3c_ts_dev, BTN_TOUCH, 0);
 		input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);
		input_sync(&s3c_ts_dev);
		enter_wait_pen_down_mode();
	} else {
		/* 如果是按下的情况 */
		enter_measure_xy_mode();
		start_adc();
	}
}

static int s3c_ts_init(void)
{
	int err;
	static struct clk *s3c_ts_clk;
	
	/* 1. 分配input_dev结构体 */
	s3c_ts_dev = input_allocate_device();
	if (NULL == s3c_ts_dev) {
		printk("allocate input device failed\n");
		err = -ENOMEM;
		goto err1;
	}
	/* 2. 设置 */
	/* 2.1 能产生这类事件中的哪一类事件 */
	set_bit(EV_KEY, s3c_ts_dev->evbit);
	set_bit(EV_ABS, s3c_ts_dev->evbit);
	set_bit(EV_SYN, 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);
	input_set_abs_params(s3c_ts_dev, ABS_Y, 0, 0x3FF, 0, 0);
	input_set_abs_params(s3c_ts_dev, ABS_PRESSURE, 0, 1, 0, 0);
	
	/* 3. 注册 */
	err = input_register_device(s3c_ts_dev);
	if (err) {
		printk("input register device fail\n");
		goto err2;
	}
	
	/* 4. 硬件操作 */
	/* 4.1 使能时钟(CLKCON[15]),使ADC模块能工作 */
	s3c_ts_clk = clk_get(NULL, "adc"); /* 这个是设置adc模块 */
	if (!s3c_ts_clk) {
		printk(KERN_ERR "failed to get adc clock source\n");
		return -ENOENT;
	}
	clk_enable(s3c_ts_clk);

	/* 4.2  设置S3C2440的ADC/TS寄存器*/
	s3c_ts_regs = ioremap(0x58000000, sizeof(struct s3c_ts_regs));
	printk("&s3c_ts_regs = 0x%x, &s3c_ts_regs->adctsc = 0x%x\n", s3c_ts_regs, &s3c_ts_regs->adctsc);
	/*
	  *ADCCON:
	  *bit[14] : 1-A/D converter prescaler enable
	  *bit[13:6] : A/D converter prescaler value
	  *			  49, ADCCLK = PCLK/(49+1) = 50MHz/(49+1)=1MHz
	  *bit[0] : A/D conversion starts by enable.先设置为0
	  */
	s3c_ts_regs->adccon = (1<<14) | (49<<6);

	if (request_irq(IRQ_TC, pen_down_up_irq, IRQF_SAMPLE_RANDOM, "ts_pen", NULL)
		|| request_irq(IRQ_ADC, adc_irq, IRQF_SAMPLE_RANDOM,"adc", NULL)) {
		printk("request_irq failed!\n");
		goto err3;
	}
	/* 
	  *优化措施1: 延时一段时间后再进行ADC测量,
	  *避免电压还没稳定就测量,造成结果不准确
	  */
	s3c_ts_regs->adcdly = 0xffff; //延时设为最大值

	/*优化措施5:
	  *启动定时器,处理滑动或长按的情况
	  */
	init_timer(&ts_timer);
	ts_timer.function = s3c_ts_timer_function; /*定时器到时间后执行这个函数*/
	add_timer(&ts_timer);

	enter_wait_pen_down_mode();
	return 0;
err3:
	iounmap(s3c_ts_regs);
err2:
	input_unregister_device(s3c_ts_dev);
err1:
	input_free_device(s3c_ts_dev);
}

static void s3c_ts_exit(void)
{
	disable_irq(IRQ_TC);
	disable_irq(IRQ_ADC);
	free_irq(IRQ_TC, NULL);
	free_irq(IRQ_ADC, NULL);
	iounmap(s3c_ts_regs);
	input_unregister_device(s3c_ts_dev);
	input_free_device(s3c_ts_dev);
	del_timer(&ts_timer);
}

module_init(s3c_ts_init);
module_exit(s3c_ts_exit);
MODULE_LICENSE("GPL");


/*
hexdump /dev/event0的打印值如下:
                       秒           微妙 type  code       value
 0000000 17a1 0000 a51c 000e 0003 0000 0176 0000
 0000010 17a1 0000 a528 000e 0003 0001 00d3 0000
 0000020 17a1 0000 a52e 000e 0001 014a 0001 0000
 0000030 17a1 0000 a530 000e 0003 0018 0001 0000
 0000040 17a1 0000 e308 000e 0003 0000 0175 0000
 0000050 17a1 0000 e31f 000e 0003 0001 00d5 0000
 0000060 17a2 0000 042c 0000 0001 014a 0000 0000
 0000070 17a2 0000 043a 0000 0003 0018 0000 0000
 */

1、

      我们先看一下入口的init函数。我们是利用了input子系统来分发产生的触摸屏事件,input子系统在我前面的文章也介绍过(input子系统的架构分析及应用)。input子系统的书写步骤主要有3步:

(1)分配一个input_dev结构体:input_allocate_device

(2)设置产生哪一类事件:set_bit()。我们这里是设置为按键类,绝对位移和同步类事件

(3)设置产生的按键类型:这里用到了input_set_abs_params()函数,可以设置绝对位移事件中X和Y中的最大最小值,以及压力值等等的一些参数。

(4)最后就是注册已经设置好的input_dev结构体

2、

接下来是硬件相关的操作,其实也就是要使用触摸屏,所必须的寄存器设置。

(1)我们这款S3C2440芯片的ADC模块默认是开机不工作的,所以要使用它就必须在初始化的时候开启

s3c_ts_clk = clk_get(NULL, "adc"); /* 这个是设置adc模块 */
clk_enable(s3c_ts_clk);

(2)

       要记住一点就是,Linux系统并没有自动帮我们对外设的寄存器的地址进行映射,需要我们手动ioremap一下。这样才能对寄存器进行设置。因为我们的寄存器地址都是连续的,所以可以一次性进行映射。

struct s3c_ts_regs {
	unsigned long adccon; //0x58000000
	unsigned long adctsc;
	unsigned long adcdly;
	unsigned long adcdat0;
	unsigned long adcdat1;
	unsigned long adcupdn;
};

...

s3c_ts_regs = ioremap(0x58000000, sizeof(struct s3c_ts_regs));

(3)

我们这里的大部分寄存器都使用默认值,但是这里的配置寄存器ADCCON需要设置一下,使得开启了分频并且分频系数为49

/*
	  *ADCCON:
	  *bit[14] : 1-A/D converter prescaler enable
	  *bit[13:6] : A/D converter prescaler value
	  *			  49, ADCCLK = PCLK/(49+1) = 50MHz/(49+1)=1MHz
	  *bit[0] : A/D conversion starts by enable.先设置为0
	  */
	s3c_ts_regs->adccon = (1<<14) | (49<<6);

3、

       然后就是注册中断函数了,这里我们注册了两个中断处理函数。一个是用来处理触摸屏的中断事件,另一个是处理ADC中断事件。这里的中断号我是参考了代码中已有的:drivers/input/touchscreen/s3c2410_ts.c。

4、

先略过一些优化的代码,看一下下面的这个函数。

enter_wait_pen_down_mode();

       我们在init初始化阶段的最后调用这个函数,并且设置adctsc寄存器为0xd3,是为了使触摸屏进入等待按下的状态,这是芯片手册中说明的,我们不用过于纠结:

触摸屏驱动_第1张图片

 5、

       初始化的动作都做完了,触摸屏就静静的等待按下的事件了。假设此时触摸屏检测到按下的事件,就会触发调用我们的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 up\n");
		input_report_key(s3c_ts_dev, BTN_TOUCH, 0);
 		input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);
		input_sync(&s3c_ts_dev);
		enter_wait_pen_down_mode();
	} else {
		//printk("pen down\n");
		//enter_wait_pen_up_mode();
		enter_measure_xy_mode();
		start_adc();/*成功启动后,会进入中断模式,也就是调用adc_irq函数*/
	}
	return IRQ_HANDLED;
}

        我们可以看到函数比较简单,通过读取寄存器adcdat0的第16位的值来判断按下还是松开。

       如果是0,我们就进入X和Y坐标的测量;如果是1,证明是松开了触摸屏,我们就上报上次读取到的数据,并再次进入等待触摸屏按下的状态。 

       假如检测到的是按下触摸屏的状态,那么就会调用enter_measure_xy_mode()函数,进行X、Y坐标的测量。下面图14.5是等待状态的等效电路图,

触摸屏驱动_第2张图片

下面的两个图分别是测量X和Y坐标的等效电路图:

触摸屏驱动_第3张图片

       我们可以看到此时都是断开了S5的上拉电阻,所以我们enter_measure_xy_mode()函数中也将ADC控制寄存器ADCTSC的下面两位设置为1,使其上面电阻断开,并且进入自动测量XY坐标的模式。

触摸屏驱动_第4张图片

 然后就是调用start_adc()函数,启动ADC,并进入ADC中断处理函数adc_irq()。

6、

       现在我们看一下ADC中断处理函数adc_irq()。其实如果我们先忽略优化措施部分的代码,是可以写的很简单的。芯片已经提供了寄存器adcdat0和adcdat1给我们直接读取X和Y坐标的值,我们只用负责上报从寄存器读取到的坐标值就可以了。

延伸

       上面写到的流程,已经是一个比较完整的触摸屏驱动程序的流程了。但是如果想有更精确的、更友好的触摸体验,我们还必须加上一些优化的措施,也就是我代码中提到的“优化措施1~5“。

1、

       优化措施1比较简单,我们只是简单的设置adcdly寄存器为最大值。这个寄存器的含义就是什么时候才开始ADC的测量

2、

       优化措施2是在开始ADC测量前,我们先检查一下触摸屏是按下还是松开的状态。如果是松开的话,我们是直接上报坐标值0的,因为ADC测量的过程是需要时间的,如果在测量过程中就松开了,此时测量出的结果可能是不精确的,所以我们在上报坐标值前有一个判断时候按下触摸屏的动作。

3、

       优化措施3的原理也很简单,取多次的坐标值取平均值,可以使结果更加接近精确值,比如我们代码中取4次坐标值的平均值

4、

       优化措施4的原理也非常简单,我们在读取到的坐标值的时候,有可能会出现某一次的误差比较大,所以我们这里加了一个过滤函数s3c_filter_ts(),如果读取到某次的坐标值误差比较大,我们直接从头来。

5、

       现在我们重点来讲一下优化措施5。我们一开始写的驱动程序是不支持长按和滑动屏幕的,所以我们在代码中加入了定时器。定时器的用途很广泛,而且我们也不陌生,在我们的文章:input子系统的架构分析及应用中,曾经也使用过定时器来防止按键抖动。

        这里我们看一下怎么使用定时器来支持长按和滑动。定时器的引入也是有固定的套路的:

/*1、定义一个定时器*/
static struct timer_list ts_timer;

/*2、初始化定时器*/
init_timer(&ts_timer);

/*3、定义定时器到时间后调用的函数*/
ts_timer.function = s3c_ts_timer_function;

/*4、将我们定义的定时器加入调用的队列*/
add_timer(&ts_timer);

/*5、延长定时器倒计时时间,10ms后再结束*/
mod_timer(&ts_timer, jiffies + HZ/100);

       我们这里重点看一下定时器的第5步的使用,在我们的驱动程序中,我们将mod_timer()放到了ADC测量结束上报坐标值之后,并且规定10ms后我们就调用定时器函数s3c_ts_timer_function(),判断如果还是按下的状态的话,再次进入XY坐标的测量并启动ADC转换,直到判断到是松开的状态,我们上报坐标0值并进入等待按下的状态。

结语

       使用命令hexdump /dev/event0,调试我们的驱动程序,可以看到下面的打印,如果调试过类似的驱动程序的朋友应该不会陌生。

             秒        微妙  type  code   value
 0000000 17a1 0000 a51c 000e 0003 0000 0176 0000
 0000010 17a1 0000 a528 000e 0003 0001 00d3 0000
 0000020 17a1 0000 a52e 000e 0001 014a 0001 0000
 0000030 17a1 0000 a530 000e 0003 0018 0001 0000
 0000040 17a1 0000 e308 000e 0003 0000 0175 0000
 0000050 17a1 0000 e31f 000e 0003 0001 00d5 0000
 0000060 17a2 0000 042c 0000 0001 014a 0000 0000
 0000070 17a2 0000 043a 0000 0003 0018 0000 0000

        写到这里,触摸屏驱动程序就算写完了。很多的细节方面可能对于不同的芯片会有所差异,但是思路和一些套路都是大同小异的,我们只需要借鉴这部分的就可以了。

你可能感兴趣的:(驱动程序,Linux驱动)