触摸屏驱动是用输入子系统那一套实现的。
先介绍一下电阻触摸屏的原理。触摸屏巧妙的使用了欧姆定律,根据分压原理,根据电阻的大小再得到电压的大小,再通过adc输出数字量。如下图:
要注意,LCD和触摸屏是完全不同的东西,它们只是恰好大小相同,恰好放在一起。触摸屏得到的只是电压的大小,与LCD的坐标完全没有关系。至于触摸屏和LCD如何建立联系,稍后会介绍。
可以先想像一下触摸屏的使用过程:
①按下,产生中断;
②在中断处理程序中,启动adc转换x,y坐标;
③adc工作结束,产生adc中断;
④在adc中断处理函数里,上报事件(input_event),并启动定时器
⑤定时器时间到,启动adc。
⑥松开
引入定时器的目的是为了处理长按和滑动。
先写一个驱动程序的框架,即入口函数,出口函数,以及修饰。在入口函数中要完成一下操作:
1. 分配一个input_dev结构体
2. 设置
3. 注册
4. 硬件相关的操作
首先是分配一个input_dev结构体:
static struct input_dev *s3c_ts_dev;
s3c_ts_dev = input_allocate_device();
第二个是设置,包括两个方面,即:
/* 能产生哪类事件 */
set_bit(EV_KEY, s3c_ts_dev->evbit);
set_bit(EV_ABS, s3c_ts_dev->evbit);
/* 能产生这类事件里的哪些事件 */
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);
也就是能够产生按键类事件,还有绝对位移;可以产生触摸的事件,参数为x坐标,y坐标,以及压力方向(只有0和1)。为什么最大值是3FF呢?因为这个触摸屏采用的adc转换器是10位的。
然后是注册
input_register_device(s3c_ts_dev);
接下来是硬件相关的操作。
内核启动过程中,为了省电,把很多暂时用不到的模块都给屏蔽了,通过操作寄存器CLKCON可以使能adc&touch screen。因此首先要使能时钟(CLKCON[15])
clk = clk_get(NULL, "adc");
clk_enable(clk);
然后要设置S3C2440的ADC/TS寄存器,对寄存器进行操作之前,要先进行映射,具体如何映射和上一节一样,不再赘述。adc的时钟要求低于2.5MHz,但mini2440的PCLK为50MHz,所以要进行分频。
/* 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);
然后是申请中断,等待触摸笔按下:
request_irq(IRQ_TC, pen_down_up_irq, IRQF_SAMPLE_RANDOM, "ts_pen", NULL);
enter_wait_pen_down_mode();
pen_down_up_irq()是中断处理函数,具体代码如下:
static irqreturn_t pen_down_up_irq(int irq, void *dev_id)
{
//ADCDAT0的bit15,如果为0表示按下,1表示松开
if (s3c_ts_regs->adcdat0 & (1<<15))
{
printk("pen up\n");
enter_wait_pen_down_mode();
}
else
{
printk("pen down\n");
enter_wait_pen_up_mode();
}
return IRQ_HANDLED;
}
enter_wait_pen_down_mode和enter_wait_pen_up_mode如何实现呢?在2440手册中有提示,进入等待中断模式的时候,可以往ADCTSC寄存器中写“0xd3”,ADCTSC寄存器的bit8为0的时候,表示检测按下的中断,1表示松开的中断。
static void enter_wait_pen_down_mode(void)
{
s3c_ts_regs->adctsc = 0xd3;
}
static void enter_wait_pen_up_mode(void)
{
s3c_ts_regs->adctsc = 0x1d3;
}
然后在出口函数中按相反方向注销注册分配的东西,这样,一个最简单的触摸屏驱动程序就写好了。加载驱动后,但按下触摸屏会显示“pen down”,松开的时候会显示“pen up”。此时只能识别触摸笔按下或者松开。
接下来要对这个驱动程序进行改进。
我们可以在检测到触摸笔按下的时候,得到电压值,并启动adc
static irqreturn_t pen_down_up_irq(int irq, void *dev_id)
{
if (s3c_ts_regs->adcdat0 & (1<<15))
{
printk("pen up\n");
enter_wait_pen_down_mode();
}
else
{
//printk("pen down\n");
//enter_wait_pen_up_mode();
enter_measure_xy_mode();
start_adc();
}
return IRQ_HANDLED;
}
其中,enter_measure_xy_mode()可以使触摸屏转换到自动测量xy坐标的模式,需要在adctsc寄存器的bit2写1,同时要求上拉电阻失能,即bit3写1:
static void enter_measure_xy_mode(void)
{
s3c_ts_regs->adctsc = (1<<3)|(1<<2);
}
在adccon的bit0写1,可以启动adc开始工作
static void start_adc(void)
{
s3c_ts_regs->adccon |= (1<<0);
}
当adc工作完成之后,会进入adc中断处理函数,在这个中断处理函数中,我们就可以把xy值打印出来,它们分别存放在adcdat0和adcdat1 的bit[0-9]中,打印完成之后别忘了让它进入等待松开模式,以便连续操作
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
printk("adc_irq cnt = %d, x = %d, y = %d\n", ++cnt, s3c_ts_regs->adcdat0 & 0x3ff, s3c_ts_regs->adcdat1 & 0x3ff);
enter_wait_pen_up_mode();
return IRQ_HANDLED;
}
把驱动编译,加载之后,可以看到能够打印出xy的值。
接下来我们进行第一个优化,把值变得更精确一点,可以等待电压稳定时再去获取电压值,在入口函数中设置ADCDLY为最大值, 这使得电压稳定后再发出IRQ_TC中断
s3c_ts_regs->adcdly = 0xffff;
第二个优化,在adc中断处理函数中,如果发现adc启动完成后触摸笔已经松开,要舍弃此次数据:
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
int adcdat0, adcdat1;
/* 如果ADC完成时, 发现触摸笔已经松开, 则丢弃此次结果 */
adcdat0 = s3c_ts_regs->adcdat0;
adcdat1 = s3c_ts_regs->adcdat1;
if (s3c_ts_regs->adcdat0 & (1<<15))
{
/* 已经松开 */
enter_wait_pen_down_mode();
}
else
{
printk("adc_irq cnt = %d, x = %d, y = %d\n", ++cnt, adcdat0 & 0x3ff, adcdat1 & 0x3ff);
enter_wait_pen_up_mode();
}
return IRQ_HANDLED;
}
第三个优化,我们可以采用多次测量求平均值:
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
static int x[4], y[4];
int adcdat0, adcdat1;
/* 优化措施2: 如果ADC完成时, 发现触摸笔已经松开, 则丢弃此次结果 */
adcdat0 = s3c_ts_regs->adcdat0;
adcdat1 = s3c_ts_regs->adcdat1;
if (s3c_ts_regs->adcdat0 & (1<<15))
{
/* 已经松开 */
cnt = 0;
enter_wait_pen_down_mode();
}
else
{
// printk("adc_irq cnt = %d, x = %d, y = %d\n", ++cnt, adcdat0 & 0x3ff, adcdat1 & 0x3ff);
/* 优化措施3: 多次测量求平均值 */
x[cnt] = adcdat0 & 0x3ff;
y[cnt] = adcdat1 & 0x3ff;
++cnt;
if (cnt == 4)
{
printk("x = %d, y = %d\n", (x[0]+x[1]+x[2]+x[3])/4, (y[0]+y[1]+y[2]+y[3])/4);
cnt = 0;
enter_wait_pen_up_mode();
}
else
{
enter_measure_xy_mode();
start_adc();
}
}
return IRQ_HANDLED;
}
第四个优化,软件过滤。由于x和y各有四个值,我们可以把前两个值取平均值,与第三个值相比较,然后把中间两个值取平均值,与第四个值比较,只要其中一个差值大于某一个限定,就舍弃这组数据。把这个函数放在adc中断处理函数里,当cnt等于4时,添加如果满足过滤条件之后再打印出坐标。
static int s3c_filter_ts(int x[], int y[])
{
#define ERR_LIMIT 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 = (x[2] > avr_x) ? (x[2] - avr_x) : (avr_x - x[2]);
det_y = (y[2] > avr_y) ? (y[2] - avr_y) : (avr_y - y[2]);
if ((det_x > ERR_LIMIT) || (det_y > ERR_LIMIT))
return 0;
avr_x = (x[1] + x[2])/2;
avr_y = (y[1] + y[2])/2;
det_x = (x[3] > avr_x) ? (x[3] - avr_x) : (avr_x - x[3]);
det_y = (y[3] > avr_y) ? (y[3] - avr_y) : (avr_y - y[3]);
if ((det_x > ERR_LIMIT) || (det_y > ERR_LIMIT))
return 0;
return 1;
}
第五个优化,处理长按和滑动。可以添加一个定时器。当发现触摸笔按下之后,如果在10ms之后还没有松开,那么再次启动adc读取数据。调用时机在软件过滤之后(mod_timer(&ts_timer, jiffies + HZ/100);)。当然在入口函数要注册一个定时器,
static void s3c_ts_timer_function(unsigned long data)
{
if (s3c_ts_regs->adcdat0 & (1<<15))
{
/* 已经松开 */
enter_wait_pen_down_mode();
}
else
{
/* 测量X/Y坐标 */
enter_measure_xy_mode();
start_adc();
}
}
到此为止,这个驱动程序基本就算完成了,我们只需要把所有的打印语句换成上报事件就可以了。当按下时,
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);
松开时,
input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);
input_report_key(s3c_ts_dev, BTN_TOUCH, 0);
input_sync(s3c_ts_dev);
这样驱动程序就算完成了。
若想使触摸屏和LCD结合起来,需要借助talib库,安装好之后就可以了。