7. Linux - 触摸屏(电阻屏)驱动程序实现
一、触摸屏概述
触摸屏作为一种输入设备,是目前最简单、方便、自然的一种人机交互方式。按照触摸屏的工作原理和传输信息的介质,可以将触摸屏分为四种:电阻式、电容感应式、红外线式和表面声波式。每一种触摸屏都有其各自的优缺点,要了解哪种触摸屏适用于哪种场合,关键就在要了解每一类触摸屏的工作原理和特点。本节我们主要介绍的是4线电阻式触摸屏。
1.1 电阻式触摸屏
电阻式触摸屏利用压力感应进行控制。电阻触摸屏的主要部分是一块与显示器表面紧密结合的电阻薄膜屏,这是一种多层的复合薄膜,以一层玻璃或硬塑料平板作为基层,表面涂有一层透明氧化金属(透明的导电电阻)导电层,上面再盖有一层外表面硬化处理、光滑防擦的塑料层、它的内表面也涂有一层涂层、在他们之间有许多细小的的透明隔离点把两层导电层隔开绝缘。所有的电阻式触摸屏都采用分压器原理来产生代表X坐标和Y坐标的电压。分压器是通过将两个电阻进行串联来实现的。电阻R1连接正参考电压VREF,电阻R2接地。两个电阻连接点处的电压测量值与R2的阻值成正比。当手指触摸屏幕时,两层导电层在触摸点位置就有了接触,电阻发生变化,在X和Y两个方向上产生信号,然后送触摸屏控制器。控制器侦测到这一接触并计算出(X,Y)的位置,再根据模拟鼠标的方式运作。这就是电阻技术触摸屏的最基本原理。实际上就是欧姆定律的巧妙运用!
1.2 电阻式触摸屏信号测量
4线触摸屏包含了两个阻性层,如下图所示:
当没有触摸按下时,X层和Y层是分离的,此时就测不到电压。
X坐标方向值测量:
将XP接到3.3V , XM接0V, YP和YM悬空,我们以按压X坐标的中间位置, X层和Y层便闭合了,此时触摸屏控制器 (ADC)就可测量到YP输出的当前X坐标值1.66V,如下图:
Y坐标方向值测量:
将YP接3.3V , YM接0V, XP和XM悬空,我们以按压X坐标的中间位置, X层和Y层便闭合了,此时触摸屏控制器 (ADC)就可测量到XP输出的当前Y坐标值1.66V,如下图:
二、触摸屏驱动模型
Linux内核中触摸屏驱动实现基于输入子系统架构,具体请移步 Linux - 输入子系统框架详解 ;
对于触摸屏驱动开发者,核心层和事件处理层由Linux内核提供,主要实现xxx_ts.c,
1)分配一个input_dev结构体
2)设置input_dev的成员
3)注册input_dev 驱动设备到内核中
4)设置触摸屏相关的硬件
触摸屏使用过程简单处理简述如下(流程):
- 按下,产生中断;
- 在中断服务程序里,启动ADC,转换X,Y坐标;
- ADC结束,产生ADC中断;
- 在ADC中断处理函数里上报(input_event),启动定时器;
- 定时器时间到,执行2再次启动ADC,转换X,Y坐标;(处理长按、滑动)
- 松开
二、触摸屏驱动相关的重要数据结构和函数
在输入子系统架构中,会将设备抽象出一个input_dev结构体;它是驱动的主体。每个struct input_dev代表一个输入设备。
2.1 struct input_dev结构体
struct input_dev {
void *private;
const char *name; //设备名字
const char *phys; //文件路径,比如 input/buttons
const char *uniq;
struct input_id id;
unsigned long evbit[NBITS(EV_MAX)]; //表示支持哪类事件,常用有以下几种事件(可以多选)
//EV_SYN 同步事件,当使用input_event()函数后,就要使用这个上报个同步事件
//EV_KEY 键盘事件
//EV_REL (relative)相对坐标事件,比如鼠标
//EV_ABS (absolute)绝对坐标事件,比如摇杆、触摸屏感应
//EV_MSC 其他事件,功能
//EV_LED LED灯事件
//EV_SND (sound)声音事件
//EV_REP 重复键盘按键事件
//(内部会定义一个定时器,若有键盘按键事件一直按下/松开,就重复定时,时间一到就上报事件)
//EV_FF 受力事件
//EV_PWR 电源事件
//EV_FF_STATUS 受力状态事件
unsigned long keybit[NBITS(KEY_MAX)]; //存放支持的键盘按键值
//键盘变量定义在:include/linux/input.h, 比如: KEY_L(按键L)、BTN_TOUCH(触摸屏的按键)
unsigned long relbit[NBITS(REL_MAX)]; //存放支持的相对坐标值
unsigned long absbit[NBITS(ABS_MAX)]; //存放支持的绝对坐标值,存放下面4个absxxx[]
unsigned long mscbit[NBITS(MSC_MAX)]; //存放支持的其它事件,也就是功能
unsigned long ledbit[NBITS(LED_MAX)]; //存放支持的各种状态LED
unsigned long sndbit[NBITS(SND_MAX)]; //存放支持的各种声音
unsigned long ffbit[NBITS(FF_MAX)]; //存放支持的受力设备
unsigned long swbit[NBITS(SW_MAX)]; //存放支持的开关功能
... ...
/*以下4个数组都会保存在上面成员absbit[]里,数组号为:ABS_xx ,位于include/linux/input.h */
/*比如数组0,标志就是ABS_X,以下4个的absXXX[0]就是表示绝对位移X方向的最大值、最小值... */
/*对于触摸屏常用的标志有:
ABS_X(X坐标方向), ABS_Y(Y坐标方向), ABS_PRESSURE(压力方向,比如绘图,越用力线就越粗)* /
int absmax[ABS_MAX + 1]; //绝对坐标的最大值
int absmin[ABS_MAX + 1]; //绝对坐标的最小值
int absfuzz[ABS_MAX + 1]; //绝对坐标的干扰值,默认为0,
int absflat[ABS_MAX + 1]; //绝对坐标的平焊位置,默认为0
... ...
2.2 分配/释放input_dev结构体
函数原型:struct input_dev *input_allocate_device(void)
参数说明 :无
返回值 : 分配到的input_dev结构体
函数原型:input_free_device(struct input_dev dev)
参数说明 :dev 待释放的input_dev结构体
2.3 注册/注销input_dev设备
函数原型:input_register_device(struct input_dev *dev)
参数说明 :*dev 注册的input_dev结构体
说 明 :注册一个input_dev,若有对应的驱动事件,则在/sys/class/input下创建这个类设备。
函数原型:input_unregister_device(struct input_dev *dev)
参数说明 :*dev 注销的input_dev结构体
说 明 :卸载/sys/class/input目录下的input_dev这个类设备;
2.4 事件支持(初始化)
函数原型:int set_bit(int nr,long * addr)
参数说明 :设置某个结构体成员 * addr里面的某位等于nr,支持这个功能
说 明 :告诉input输入子系统支持哪些事件,哪些按键;例如:
set_bit(EV_KEY,button_dev.evbit) (其中button_dev是struct input_dev类型)
struct input_dev中有两个成员为:
evbit:
事件类型(EV_RST,EV_REL,EV_MSC,EV_KEY,EV_ABS,EV_REP等)
keybit:
按键类型(当事件类型为EV_KEY时包括BTN_LEFT,BTN_0,BTN_1,BTN_MIDDLE等)
2.5 设置绝对位移的支持参数
函数原型:input_set_abs_params(struct input_dev *dev, int axis, int min, int max, int fuzz, int flat)
参数说明 :*dev : 需要设置的input_dev结构体
axis : 需要设置的数组号,常用的有: ABS_X(X坐标方向), ABS_Y(Y坐标方向), ABS_PRESSURE(压力方向)
min : axis方向的最小值
max : axis方向的最大值
fuzz : axis方向的干扰值
flat : axis方向的平焊位置
2.5 上报EV_ABS事件
函数原型:input_report_abs(struct input_dev *dev, unsigned int code, int value);
参数说明 :*dev : 要上报哪个input_dev驱动设备的事件
code : EV_ABS事件里支持的哪个方向,比如X坐标方向则填入: ABS_X
value : 对应的方向的值,比如X坐标126
说 明 :该函数实际就是调用的input_event(dev, EV_ABS, code, value);
2.6 上报EV_KEY事件
函数原型:input_report_key(struct input_dev *dev, unsigned int code, int value);
2.7 同步事件通知,通知系统有事件上报
函数原型:input_sync(struct input_dev *dev) ;
参数说明 :*dev : 要上报哪个input_dev驱动设备的事件
说 明 :同步用于告诉input core子系统报告结束
三、触摸屏驱动实现
3.1 在init入口函数中:
1)分配一个input_dev结构体
2)设置input_dev的成员
-> 2.1)设置input_dev->evbit支持按键事件,绝对位移事件
(触摸屏:通过按键BTN_TOUCH获取按下/松开,通过绝对位移获取坐标)
-> 2.2)设置input_dev-> keybit支持BTN_TOUCH触摸屏笔尖按下
-> 2.3)设置input_dev-> absbit 支持ABS_X、ABS_Y、 ABS_PRESSURE
input_set_abs_params(ts.dev, ABS_X, 0, 0x3FF, 0, 0);
input_set_abs_params(ts.dev, ABS_Y, 0, 0x3FF, 0, 0); // 0x3FF:最大值为10位ADC,
input_set_abs_params(ts.dev, ABS_PRESSURE, 0, 1, 0, 0); //压力最多就是1
3)注册input_dev 驱动设备到内核中
4)设置触摸屏相关的硬件
-> 4.1)开启ADC时钟,使用clk_get ()和clk_enable()函数
-> 4.2) ioremap获取寄存器地址,设置寄存器ADCCON =(1<<14)|(49<<6),分频
->4.3)设置寄存器ADCDLY=0xffff,ADC启动延时时间设为最大值,使触摸按压更加稳定
->4.4)开启IRQ_TC笔尖中断、开启IRQ_ADC中断获取XY坐标
-> 4.5)初始化定时器,增加触摸滑动功能
->4.6)最后设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断
3.2 在出口函数中:
1)注销内核里的input_dev、
2)释放中断、删除定时器、iounmap注销地址、
3)释放input_dev、
3.3 在IRQ_TC中断函数中:
1)若判断笔尖为松开,设置寄存器ADCTSC =0XD3(按下中断)
2)若判断笔尖按下,设置为XY自动转换模式,启动一次ADC转换,ADC转换成功,会进入ADC中断
3.4 在IRQ_ADC中断函数中:
1)获取ADCDAT0的位[9:0],来算出XY方向坐标值
2)测量n次值保存在数组中,然后再次设置为XY自动转换模式,启动ADC
(PS:要启动ADC转换之前必须设置一次XY为自动转换模式,不然获取的数据会不准)
3)采集完毕,使用快速排序将n次值排序后,以最小值为基准,如有误差非常大的数,则舍弃,如果没有则打印数组的中间值,实现中值滤波。
(PS: 使用快速排序,比冒泡更快,详解 )
4)打印数据后,必须设置寄存器ADCTSC =0X1D3(松开中断IRQ_TC)
(PS:在ADC采样模式下是判断不到ADCDAT0的bit15位的,因为ADCDAT0已被自动设置为X坐标的采样值)
5)设置定时器10ms超时时间
3.5 在定时器超时函数中:
1)判断ADCDAT0的bit15位,若还在按下再次启动ADC转换(实现触摸滑动功能)
2)若松开,设置寄存器ADCTSC =0XD3(按下中断)
代码如下:
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/input.h>
#include <linux/init.h>
#include <linux/serio.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/plat-s3c24xx/ts.h>
#include <asm/arch/regs-adc.h>
#include <asm/arch/regs-gpio.h>
static struct input_dev *ts_dev;
static struct clk *ADC_CLK; //adc时钟
static struct timer_list ts_timer; //定时器
struct adc_regs{
unsigned long adccon;
unsigned long adctsc;
unsigned long adcdly;
unsigned long adcdat0;
unsigned long adcdat1;
unsigned long adcupdn;
};
static volatile struct adc_regs *adc_regs;
/*启动TC 函数*/
static void set_pen_down(void)
{
/* 设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断*/
adc_regs->adctsc = 0xd3;
}
static void set_pen_up(void)
{
/* 设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断*/
adc_regs->adctsc = 0x1d3;
}
/*启动ADC 转换函数*/
static void adc_start(void)
{
adc_regs->adctsc= (1<<3)| (1<<2); //启动XY自动转换
adc_regs->adccon|=(1<<0); //启动1次ADC转换
}
/*快速排序,比冒泡更快*/
/*快速排序详解:http://www.cnblogs.com/lifexy/p/7597276.html*/
static void find_frst(int *s,int lift,int right)
{
int i=lift,j=right,temp; //(1)初始化i、j
if(lift>=right)
return ;
temp=s[i]; //(2)以第一个数组为比较值,保存到temp中
while(i<j)
{
while(j>i&&s[j]>=temp) //(3)j--,找小值
j--;
s[i]= s[j]; //保存小值,到s[i]上
while(i<j&&s[i]<=temp) //(4)i++,找大值
i++;
s[j--]=s[i]; //保存大值 到s[j]上
}
s[i]=temp; //(5)将比较值放在s[i]上
/*(6)拆分成两个数组 s[0,i-1]、s[i+1,n-1]又开始排序 */
find_frst(s,lift,i-1); //左
find_frst(s,i+1,right); //右
}
/*查找X Y坐标偏移值是否太大*/
/*return: 0误差大, 1误差小 */
static int find_xy_offset(int x[], int y[],int n)
{
int i;
for(i=n;i>=1;i--)
{
if(x[i]-x[i-1]>10) //判断是否大于误差10,
return 0;
if(y[i]-y[i-1]>10) //判断是否大于误差10,
return 0;
}
return 1;
}
/*定时器函数,实现触摸滑动功能 */
void pen_updown_timer(unsigned long cnt)
{
if((adc_regs->adcdat0>>15)&0x01) //此时笔尖已经抬起
{
set_pen_down(); //设置TC中断
}
else
{
adc_start(); //启动一次ADC转换
}
}
/*触摸中断IRQ_TC */
static irqreturn_t tc_handler(int irq, void *dev_id)
{
if((adc_regs->adcdat0>>15)&0x01) //此时笔尖已经抬起
{
set_pen_down();
}
else
{
adc_start(); //启动一次ADC转换
}
return IRQ_HANDLED;
}
/*ADC中断IRQ_ADC:测XY坐标 */
static irqreturn_t adc_handler(int irq, void *dev_id)
{
static int adc_x[5],adc_y[5]; //保存XY坐标
static unsigned char xy_cnt=0; //计数
adc_y[xy_cnt] =adc_regs->adcdat1&0x3ff; //10位ADC
adc_x[xy_cnt] =adc_regs->adcdat0&0x3ff; //10位ADC
if (adc_regs->adcdat0 & (1<<15))
{
/* 已经松开 */
xy_cnt = 0;
set_pen_down();
}
else{
xy_cnt++;
if(xy_cnt>=5)
{
xy_cnt=0;
find_frst(adc_x,0,4); // 快速排序X
find_frst(adc_y,0,4); // 快速排序y
if(find_xy_offset(adc_x,adc_y,4))
{
printk("X: %04d,y: %04d \n",adc_x[2],adc_y[2]); //中值滤波
}
set_pen_up();
mod_timer(&ts_timer ,jiffies+HZ/100); //启动定时10ms
}
else //在测一次
{
adc_start(); //启动 ADC
}
}
return IRQ_HANDLED;
}
/*入口函数*/
static int myts_init(void)
{
/*1. 申请input_dev */
ts_dev=input_allocate_device();
/*2. 设置input_dev*/
set_bit(EV_ABS, ts_dev->evbit);
set_bit(EV_KEY, ts_dev->evbit);
set_bit(BTN_TOUCH, ts_dev->keybit);
input_set_abs_params(ts_dev, ABS_X , 0 , 0x3ff , 0 , 0); //adc是个10位的,所以为0X3FF
input_set_abs_params(ts_dev, ABS_Y , 0 , 0x3ff , 0 , 0); //adc是个10位的,所以为0X3FF
input_set_abs_params(ts_dev, ABS_PRESSURE, 0 , 1 , 0 , 0); //adc是个10位的,所以为0X3FF
/*3.注册input_dev 驱动设备到内核中*/
input_register_device(ts_dev);
/*4.设置触摸屏相关的硬件*/
/*4.1 开启ADC时钟 */
ADC_CLK =clk_get(0,"adc");
clk_enable(ADC_CLK);
/*4.2 设置寄存器ADCCON分频,*/
adc_regs=ioremap(0x58000000, sizeof(struct adc_regs));
adc_regs->adccon=(1<<14)|(49<<6); //50Mhz/(49+1)=1Mhz
/*4.3 设置中断IRQ_TC IRQ_ADC */
request_irq(IRQ_TC , tc_handler, IRQF_SAMPLE_RANDOM, "pen_updown", 0);
request_irq(IRQ_ADC , adc_handler, IRQF_SAMPLE_RANDOM, "adc" , 0);
/*4.4设置寄存器ADCDLY=0xffff */
adc_regs->adcdly =0xffff;
/*4.5 初始化定时器*/
init_timer(&ts_timer);
ts_timer.function =pen_updown_timer;
add_timer(&ts_timer);
/*4.6设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断*/
set_pen_down();
return 0;
}
/*出口函数*/
static void myts_exit(void)
{
/*1.注销内核里的input_dev、*/
input_unregister_device(ts_dev);
/*2.释放中断、删除定时器、iounmap注销地址、*/
free_irq(IRQ_TC, NULL);
free_irq(IRQ_ADC, NULL);
del_timer(&ts_timer);
iounmap(adc_regs);
/*3.释放input_dev、*/
input_free_device(ts_dev);
}
module_init(myts_init);
module_exit(myts_exit);
MODULE_LICENSE("GPL");