触摸屏设备驱动程序
由于触摸屏设备简单、价格低廉,到处应用
在消费电子商品、工业控制系统、甚至航空领域都有应用
触摸屏作为一种最新的电脑输入设备,是目前最简单、方便、自然的的一种人机交互方式,具有坚固耐用、反应速度快、节省空间、易于交流等许多优点。
事实上,触摸屏是一个使多媒体信息系统改头换面的设备,它赋予多媒体系统以崭新的面貌,是极富有吸引力的全新多媒体交互设备
从技术原理来区别触摸屏,可分为5类:
1,矢量压力传感技术触摸屏
2,电阻式触摸屏
3,电容式触摸屏
4,红外线技术触摸屏
5,表面声波技术触摸屏
矢量压力传感技术触摸屏已经退出历史舞台
红外线技术触摸屏价格低廉,但其外框易碎,容易产生光干扰,曲面情况下失真
电容技术触摸屏设计构思合理,但其图像失真问题很难得到解决
电阻技术触摸屏的定位准确,但其价格颇高,且怕刮易损
表面声波触摸屏解决了以往触摸屏的各种缺陷,清晰且不容易被破坏,适用于各种场合,缺点是:屏幕表面如果有水滴和尘土会使触摸屏变得迟钝
电阻式触摸屏的屏体部分是一块与显示器表面相匹配的多层复合薄膜,由一层玻璃或有机玻璃作为底层,表面涂有一层透明的导电层,上面再盖有一层外表面硬化处理、光滑防刮的塑料层,它的内表面也涂有一层透明导电层,在两层导电层之间有很多细小(小于千分之一英寸)的透明隔离点把它们隔开绝缘。
四线触摸屏包含两个阻性层。其中一层在屏幕的左右边缘各有一条垂直总线,另一层在屏幕的底部和顶部各有一条水平总线,见图4。为了在X轴方向进行测量,将左侧总线偏置为0V,右侧总线偏置为VREF。将顶部或底部总线连接到ADC,当顶层和底层相接触时即可作一次测量。 为了在Y轴方向进行测量,将顶部总线偏置为VREF,底部总线偏置为0V。将ADC输入端接左 侧总线或右侧总线,当顶层与底层相接触时即可对电压进行测量。图5显示了四线触摸屏在两层相接触时的简化模型。对于四线触摸屏,最理想的连接方法是将偏置 为VREF的总线接ADC的正参考输入端,并将设置为0V的总线接ADC的负参考输入端。
s3c2440芯片支持触摸屏接口。这个触摸屏接口包括一个外部晶体管控制逻辑和一个模数转换器ADC,s3c2440芯片具有一个8通道的10位CMOS模数转换器(ADC)。2.5MHz下A/D转换器可达500KSPS,A/D支持片上采样和保持功能,并支持掉电模式
s3c2440触摸屏特点:
1,分辨率,10位
2,微分线性误差:1.0LSB
3,积分线性误差:2.0LSB
4,最大转换速率:500KSPS
5,低功耗
6,供电电压:3.3V
7,输入模拟电压范围:0~3.3V
8,片上采样保持功能
9,普通转换模式
10,分离X/Y轴坐标转换模式
11,自动(连续)X/Y轴坐标转换模式
12,等待中断模式
S3C2440触摸屏4种工作模式:
1,正常转换模式
在不使用触摸屏设备时,可以单独使用触摸屏接口中共用的模数转换器ADC。这这种模式下,可以通过设置ADCCON寄存器启动普通的A/D转韩,当转换结束时,结果被写到ADCCDAT0寄存器中
2,等待中断模式
当设置触摸屏接口控制器的ADCTSC寄存器为0XD3时,触摸屏就处于等待中断模式。这时触摸屏等待触摸信号的到来。当触摸信号到来时,触摸屏接口控制器将通过INT_TC线产生中断信号,表示有触摸动作发生。当中断发生,触摸屏可以转换为其他两种状态来读取触摸点的位置(x,y)。这两种模式是独立的X/Y位置转换模式和自动X/Y位置转换模式
3,独立X/Y位置转换模式
独立的X/Y位置转换模式由两个子模式组成,分别是X位置模式和Y位置模式。X位置模式将转换后的X坐标写到ADCDAT0寄存器的XPDATA位。转换后,触摸屏接口控制器会通过INT_ADC中断线产生中断信号,由中断处理函数来处理。Y位置模式将转换后的Y坐标写到ADCDAT1寄存器的YPDATA位。同样转换后,触摸屏接口控制器会通过INT_ADC中断线产生中断信号,由中断处理函数来处理
4,自动X/Y位置转化模式
这种模式触摸屏控制器自动转换X位置和Y位置。当位置转换后,模式触摸屏接口控制器自动写转换后的X坐标写到ADCDAT0寄存器的XPDATA位;写转换后的Y坐标到ADCDAT1寄存器的YPDATA位。当转换完后,触摸屏接口控制器会通过INT_AC中断行产生中断信号
寄存器:
ADCCON:模数转换控制寄存器,用于控制AD转换、是否使用分频、设置分频系数、读取AD转换器的状态
ADCDLY:ADC延时寄存器,用于正常模式下和等待中断模式下的延时操作
ADCDAT0:ADC转换数据寄存器0,用于存储触摸屏的点击状态、工作模式、X坐标等
ADCDAT1:ADC转换数据寄存器1,用于存储触摸屏的点击状态、工作模式、Y坐标等
注意:ADCDAT0和ADCDAT1的第15位,表示X和Y方向上检测到的触摸屏是否被按下。只有当ADCDAT0和ADCDAT1寄存器的第15位,即两个寄存器的15位都等于0时,才表示触摸屏被按下,或者有触笔点击触摸屏。
ADCTSC:ADC触摸屏控制寄存器,用于控制触摸屏的YMON,nYPON,nXPON和XMON等状态
触摸屏设备驱动程序
struct s3c2410ts {
struct input_dev dev;
long xp;
long yp;
int count;
int shift;
char phys[32];
};
触摸屏设备驱动程序的初始化函数、退出函数、和中断处理函数关系如图:
1,当模块加载时,会调用初始化函数s3c2410ts_init()。在该函数中会调用probe()函数,该函数中会进一步调用request_irq()函数注册两个中断。这两个中断的处理函数分别是stylus_updown()和stylus_action()。request_irq()函数会操作内核中的一个中断描述符数组结构irq_dest。该数组结构比较复杂,主要功能就是记录中断号对应的中断处理函数
2,当中断到来时,会到中断描述符数组中询问中断号对应的处理函数,然后执行该函数。在本例中,这两个中断的处理函数分别是stylus_updown()和stylus_action()。
3,卸载模块时,会调用退出函数s3c2410ts_exit()。在该函数中,会调用free_irq()来释放设备所使用的中断号。free_irq()函数也会操作中断描述符数组结构irq_desc,将该设备对应的中断处理函数删除
4,中断处理函数stylus_updown()中会调用touch_timer_fire()。这个函数也被定时器触发,触发的条件是,缓冲区中的数据不为0,也就是有触摸时间产生
加载和卸载函数:
int __init s3c2410ts_init(void) //加载函数
{
return driver_register(&s3c2410ts_driver);
//注册一个触摸屏设备驱动程序,之后内核会以s3c2410ts_driver中的name成员为依据,在系统查找已经注册的相同name的设备。如果
找到相应的设备,就调用s3c2410ts_driver中定义的探测函数probe()。driver_register()函数是Linux设备驱动
模型中最常用的函数
}
void __exit s3c2410ts_exit(void) //卸载函数
{
driver_unregister(&s3c2410ts_driver); //卸载触摸屏设备驱动程序
}
下面来看看导火线:s3c2410ts_driver:
static struct device_driver s3c2410ts_driver = {
.name = "s3c2410-ts", //名字
.bus = &platform_bus_type, //总线
.probe = s3c2410ts_probe, //探测函数,该函数在模块加载函数完成且设备匹配成功后执行
.remove = s3c2410ts_remove, //移除函数
};
probe()对应的s3c2410ts_probe()。这个函数在触摸屏设备的初始化过程中,检查设备是否准备就绪、映射物理地址到虚拟地址、配置GPIO引脚、注册相应的中断和注册输入设备等,大多数Linux设备驱动程序中,当执行完模块加载后,就执行probe():
static int __init s3c2410ts_probe(struct device *dev)
{
struct s3c2410_ts_mach_info *info; //定义了触摸屏接口相关硬件的配置信息结构体指针
info = ( struct s3c2410_ts_mach_info
*)dev->platform_data;
//从device的平台数据dev->platform_data中获得指向struct
s3c2410_ts_match_info结构体的指针
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"); //获得ADC的时钟源,并赋值给adc_clock指针
if (!adc_clock) { //如果没有正确的获得ADC时钟源,错误退出
printk(KERN_ERR "failed to get adc clock source\n");
return -ENOENT;
}
clk_use(adc_clock); //增加时钟源的使用计数
clk_enable(adc_clock); //启动ADC时钟源
#ifdef CONFIG_TOUCHSCREEN_S3C2410_DEBUG //配置调试信息选项的话打印调试信息
printk(DEBUG_LVL "got and enabled clock\n");
#endif
base_addr=ioremap(S3C2410_PA_ADC,0x20); //把一个ADC控制寄存器的物理地址内存映射到一个虚拟地址。这段被映射的长度是0x20。
#define S3C2410_PA_ADC (0x58000000) //这是触摸屏一组寄存器的首地址
if (base_addr == NULL) { //映射失败退出
printk(KERN_ERR "Failed to remap register block\n");
return -ENOMEM;
}
/* Configure GPIOs */
s3c2410_ts_connect(); //配置处理器的GPIO。这个函数配置端口G的12,13,14,15引脚为XMON,nXPON,YMON,nPON。
/* Set the prscvl*/
if ((info->presc&0xff) >
0)
//设置AD转换的分频系数。如果info->presc的低8位的值大于0,那么配置ADCCON寄存器
位14:是否使用AD转换器的预分频功能,位6-13位共8位表示AD转换的预分频器的数值
writel(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(info->presc&0xFF),\
base_addr+S3C2410_ADCCON);
else
writel(0,base_addr+S3C2410_ADCCON); //不使用预分频功能,将ADCCON寄存器各位都设置为0
/* Initialise the adcdly registers */
if ((info->delay&0xffff) > 0) //如果需要延时。写延时时间到ADCDLY寄存器
writel(info->delay & 0xffff, base_addr+S3C2410_ADCDLY);
writel(WAIT4INT(0),
base_addr+S3C2410_ADCTSC);
//写ADC触摸屏设备的ADCTSC寄存器,使触摸屏设备 处于等待中断模式
/* Initialise input stuff */
memset(&ts, 0, sizeof(struct s3c2410ts)); //清零全局变量
init_input_dev(&ts.dev); //申请并初始化一个输入设备。通过这个输入设备,驱动程序才能和用户交互
ts.dev.evbit[0] = ts.dev.evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); //下面是初始化ts变量
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);
sprintf(ts.phys, "ts0");
ts.dev.private = &ts;
ts.dev.name = s3c2410ts_name;
ts.dev.phys = ts.phys;
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, SA_SAMPLE_RANDOM,
"s3c2410_action", &ts.dev)) { //申请ADC中断,并设置中断处理函数为stylus_action
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, SA_SAMPLE_RANDOM,
"s3c2410_action", &ts.dev)) { //申请触摸屏中断,并设置中断处理函数为stylus_updown
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;
}
s3c2410_ts_mach_info是s3c2410触摸屏接口相关硬件的配置信息
struct s3c2410_ts_mach_info
{
int delay; //延时时间
int presc; //预分频值
int oversampling_shift; //输入数据的缓冲区大小
};
下面介绍触摸屏设备配置,具体的硬件知识,请参考相关资料:
s3c2410_ts_connect()用来处理GPG端口12,13,14,15引脚,设置为与触摸屏相关的状态。这里分别设为XMON,nXPON,YMON,和nYOPN状态:
static inline void s3c2410_ts_connect(void)
{
s3c2410_gpio_cfgpin(S3C2410_GPG12, S3C2410_GPG12_XMON); //GPG的端口12引脚设置为读写触摸屏设备XM引脚的值,EINT【20】连接到触摸屏XM引脚上
s3c2410_gpio_cfgpin(S3C2410_GPG13, S3C2410_GPG13_nXPON); //GPG的端口13引脚设置为读写触摸屏设备nXPON引脚的值,EINT[21]连接到触摸屏的nXPON上
s3c2410_gpio_cfgpin(S3C2410_GPG14, S3C2410_GPG14_YMON); //GPG的端口13引脚设置为读写触摸屏设备YMON引脚的值,EINT[22]连接到触摸屏的YMON上
s3c2410_gpio_cfgpin(S3C2410_GPG15, S3C2410_GPG15_nYPON); //GPG的端口14引脚设置为读写触摸屏设备nYPON引脚的值,EINT[23]连接到触摸屏的nYPON上
}
触摸屏设备中断处理函数
当触摸屏设备驱动的探测函数执行完之后,驱动程序处于等待状态。在等待状态中,驱动程序可以接受两个中断信号,并触发中断处理函数。这两个中断是触摸屏中断(IRQ_TC)和ADC中断(IRQ_ADC)。
stylus_updown():
当触摸屏被按下时,会产生触摸中断信号IRQ_TC,该函数会激发stylus_updown()函数的调用:
static irqreturn_t stylus_updown(int irq, void *dev_id, struct pt_regs *regs)
{
unsigned long data0;
unsigned long data1;
int updown; //用来表示触摸屏是否被按下。按下是1,否则是0
/********************************debug************************************/
printk(KERN_INFO "You touch the screen\n");
/*************************************************************************/
data0 = readl(base_addr+S3C2410_ADCDAT0); //读取ADCDATA0寄存器的值
data1 = readl(base_addr+S3C2410_ADCDAT1); //读取ADCDATA1寄存器的值
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT1_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;
}
touch_timer_fire()函数:继续处理触摸中断:
static void touch_timer_fire(unsigned long data)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = readl(base_addr+S3C2410_ADCDAT0);
data1 = readl(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1
& S3C2410_ADCDAT1_UPDOWN)); //上面的和stylus_updown()函数中前面的一样
if (updown) {
if (ts.count != 0) { //用来向输入子系统报告当前触摸笔的位置
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);
printk(KERN_INFO "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; //将x,y坐标和count设置为0,表示缓冲区中没有数据,也就是没有触屏按下的事件发生
ts.yp = 0;
ts.count = 0;
writel(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST,
base_addr+S3C2410_ADCTSC);
//将AD转换模式设置为自动转模式
// #define S3C2410_ADCTSC_PULL_UP_DISABLE (1 << 3) 表示将XP上拉,另外
// #define AUTOPST (S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN
// | S3C2410_ADCTSC_AUTO_PST | S3C2410_ADCTSC_XY_PST(0)
ADCOPST宏主要任务是将AD转换器设置为自动XY坐标转换模式。S3C2410_ADCTSC_YM_SEN表示将YMON输出为1;S3C2410_ADCTSC_YP_SEN表示将nYPON输出为1;S3C2410_ADCTSC_XP_SEN表示将nXPOM输出为1;S3C2410_ADCTSC_ATUO_PST表示将ADCTSC寄存器的第二位设置为1,即将AD转换器设置为自动XY坐标转换模式。S3C2410_ADCTSC_XY_PST(0)为0,表示AD转换器没有任何操作
writel(readl(base_addr+S3C2410_ADCCON) |
S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON);
//将ADCCON寄存器的第0位设置为1.其中S3C2410_ADCCON_ENABLE_START宏表示第0位为1.这句话的代码功能是启动AD转换功能,当启动后,ADCCON第0位自动设置为0
} else {
//表示触摸屏没有被按下的操作。此时调用input_report_key()函数向输入子系统报告触摸屏被弹起事件。input_report_key()函数的第三个参数传递0,表示按键被释放。input_report_abs()函数发送触摸屏的一个绝对坐标。它们最终表现在文件系统中,应用程序可以访问Linux的文件系统,从而得到触摸屏的信息。事实上,不使用这两个函数驱动程序仍然能够控制触摸屏设备,但是需要通过其他方法访问触摸屏设备的状态
ts.count = 0;
input_report_key(&ts.dev, BTN_TOUCH, 0);
input_report_abs(&ts.dev, ABS_PRESSURE, 0);
input_sync(&ts.dev); //通知事件发送者发送一个完整的报告。这里BTN_TOUCH和ABS_PRESSURE事件不能单独发送,必须同时发送
writel(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
//将触摸屏重新设置为等待中断模式,等待触摸屏被按下。这些工作是通过设置触摸屏控制寄存器ADCTSC来完成的。WAIT4INT是需要写入的ADCTSC寄存器的值,定义:
// #define WAIT4INT(x) (((x) << 8) | S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | S3C2410_ADCTSC_XY_PST(3))
}
}
stylus_action函数:
触摸屏设备产生的另一个中断是ADC中断(IRQ_ADC)。触摸屏在自动X/Y位置转换模式和独立的X/Y位置转换模式时,当坐标数据转换之后会产生IRQ_ADC中断。中断产生之后会调用stylus_action()函数:
static irqreturn_t stylus_action(int irq, void *dev_id, struct pt_regs *regs)
{
unsigned long data0;
unsigned long data1;
data0 = readl(base_addr+S3C2410_ADCDAT0);
data1 = readl(base_addr+S3C2410_ADCDAT1);
ts.xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK; //获得触摸点的X坐标,把它加到ts.xp上。ADCDAT0寄存器的[0:9]表示X坐标。该坐标在ADC中断产生前存入ADCDAT0
ts.yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK; //获得触摸点的Y坐标,把它加到ts.yp上。ADCDAT0寄存器的[0:9]表示Y坐标。该坐标在ADC中断产生前存入ADCDAT1
ts.count++; //计数器加1
if (ts.count < (1<<ts.shift)) { //如果缓冲区为满,再次激活ADC转换器
writel(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC);
writel(readl(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON);
} else {
mod_timer(&touch_timer, jiffies+1); //修改定时器,将其时间延后一个单位。在下一个定时器时刻将调用touch_timer定时器指定 的函数
writel(WAIT4INT(1), base_addr+S3C2410_ADCTSC); //将触摸屏设置为等待中断模式
}
return IRQ_HANDLED;
}
touch_timer定时器:
touch_timer定时器用来当缓冲区不为空时,不断触发touch_timer_fire()函数。touch_timer_fire()函数读取触摸屏的坐标信息,并传递给内核子系统。
全局变量:
static struct timer_list touch_timer =
TIMER_INITIALIZER(touch_timer_fire, 0, 0);
s3c2440触摸屏驱动模块的remove()函数:
remove()函数是Linux设备驱动程序中一个非常重要的函数,这个函数实现了与probe()函数相反的功能,体现了Linux内核中,资源分配和释放的思想 。资源应该在使用时分配,在不使用时释放。这个函数释放了申请的中断、时钟、内存。
static int s3c2410ts_remove(struct device *dev)
{
disable_irq(IRQ_ADC); //关闭中断,并释放中断 #define IRQ_ADC S3C2410_IRQ(70)
disable_irq(IRQ_TC); //#define IRQ_TC (0x0)
free_irq(IRQ_TC,&ts.dev);
free_irq(IRQ_ADC,&ts.dev);
if (adc_clock) { //关闭并释放ADC时钟源
clk_disable(adc_clock);
clk_unuse(adc_clock);
clk_put(adc_clock);
adc_clock = NULL;
}
input_unregister_device(&ts.dev); //注销触摸屏输入设备
iounmap(base_addr); //释放映射的虚拟内存地址
return 0;
}