linux嵌入式系统开发之触摸屏---驱动篇(上/硬件原理)
时间:3月20日
地点:咖啡厅
“小涛哥,你说上节课讲的触摸屏应用的部分,从测试代码来看确实也没啥,但一个触摸屏确实很神秘啊,确实吸引人,也确实引的那么多的Iphone迷,要不你今天把驱动篇给我讲讲吧,我请你喝咖啡..”(听着声音,看这神情,不用我介绍,大家也知道是谁啦..)
“切,你呢?还不了解你啊.每次都这样,有问题时就乖的很,开心时就把我忘到脑袋后啦,而且还常欺负人,就你着咖啡啊,那次不是付账时,你就淘气着赖着我负啊”我愤愤不平的吼道。
“咋了,我还没说啥呢,眼睛有红了,唉,怕你了”我还没说话,小王就..我也是个软心肠。“既然你提到了触摸屏的驱动,那今天就讲讲,我想这样的,话说触摸屏驱动啊,内容有点多,分上下节吧,上节讲讲硬件原理,下节再讲驱动源码”。好,开始 linux嵌入式系统开发之触摸屏---驱动篇(上/硬件原理):
按照触摸屏的工作原理和传输信息的介质而言,我们把触摸屏分为4种:电阻式,电容式,红外线式和表面声波式。鉴于现在电阻式的触摸屏比较简单常见,给我了让我的精力更多的集中到驱动程序这块而非硬件这块,所以我这里主要就讲电阻式,其余的咋办?小王啊,这样的问题还问啊,百度一下呗(帅哥找不到,她肯定知道用百度一下的).. 电阻式触摸屏利用压力感应进行控制,包含上下叠合的两个透明层,通常还要用一种弹性材料来将两层隔开。在触摸某点时,两层会在此点接通。四线和八线触摸屏由两层具有相同表面电阻的透明材料组成,五线和七线触摸屏由一个阻性层和一个导电层组成(如图一所示)。
原理:触摸屏包含上下叠合的两个透明层,四线和八线触摸屏由两层具有相同表面电阻的透明阻性材料组成,五线和七线触摸屏由一个阻性层和一个导电层组成,通常还要用一种弹性材料来将两层隔开。当触摸屏表面受到的压力(如通过笔尖或手指进行按压)足够大时,顶层与底层之间会产生接触。所有的电阻式触摸屏都采用分压器原理来产生代表X坐标和Y坐标的电压。如图一图二所示,分压器是通过将两个电阻进行串联来实现的。上面的电阻(R1)连接正参考电压(VREF),下面的电阻(R2)接地。两个电阻连接点处的电压测量值与下面那个电阻的阻值成正比。为了在电阻式触摸屏上的特定方向测量一个坐标,需要对一个阻性层进行偏置:将它的一边接VREF,另一边接地。同时,将未偏置的那一层连接到一个ADC的高阻抗输入端。当触摸屏上的压力足够大,使两层之间发生接触时,电阻性表面被分隔为两个电阻。它们的阻值与触摸点到偏置边缘的距离成正比。触摸点与接地边之间的电阻相当于分压器中下面的那个电阻。因此,在未偏置层上测得的电压与触摸点到接地边之间的距离成正比。
四线触摸屏包含两个阻性层。其中一层在屏幕的左右边缘各有一条垂直总线,另一层在屏幕的底部和顶部各有一条 水平总线,见图三。为了在X轴方向进行测量,将左侧总线偏置为0V,右侧总线偏置为VREF。将顶部或底部总线连接到ADC,当顶层和底层相接触时即可作一次测量。为了在Y轴方向进行测量,将顶部总线偏置为VREF,底部总线偏置为0V。将ADC输入端接左侧总线或右侧总线,当顶层与底层相接触时即可对电压进行测量。图三显示了四线触摸屏在两层相接触时的简化模型。对于四线触摸屏,最理想的连接方法是将偏置为VREF的总线接ADC的正参考输入端,并将设置为0V的总线接ADC的负参考输入端。
五线触摸屏使用了一个阻性层和一个导电层。导电层有一个触点,通常在其一侧的边缘。阻性层的四个角上各有一个触点。为了在X轴方向进行测量,将左上角和左下角偏置到VREF,右上角和右下角接地。由于左、右角为同一电压,其效果与连接左右侧的总线差不多,类似于四线触摸屏中采用的方法。为了沿Y轴方向进行测量,将左上角和右上角偏置为VREF, 左下角和右下角偏置为0V。由于上、下角分别为同一电压,其效果与连接顶部和底部边缘的总线大致相同,类似于在四线触 摸屏中采用的方法。这种测量算法的优点在于它使左上角和右下角的电压保持不变;但如果采用栅格坐标,X轴和Y轴需要反 向。对于五线触摸屏,最佳的连接方法是将左上角(偏置为VREF)接ADC的正参考输入端,将左下角(偏置为0V)接ADC的负 参考输入端。如图四所示 。
七线触摸屏七线触摸屏的实现方法除了在左上角和右下角各增加一根线之外,与五线触摸屏相同。执行屏幕测量时,将左上角的一根线连到VREF,另一根线接SAR ADC的正参考端。同时,右下角的一根线接0V,另一根线连接SAR ADC的负参考端。导电层仍用来测量分压器的电压。
八线触摸屏除了在每条总线上各增加一根线之外,八线触摸屏的实现方法与四线触摸屏相同。对于VREF总线,将一根线用来连接VREF,另一根线作为SAR ADC的数模转换器的正参考输入。对于0V总线,将一根线用来连接0V,另一根线作为SAR ADC的数模转换器的负参考输入。未偏置层上的四根线中,任何一根都可用来测量分压器的电压。
检测接触所有的触摸屏都能检测到是否有触摸发生,其方法是用一个弱上拉电阻将其中一层上拉,而用一个强下拉电阻来将另一层下拉。如果上拉层的测量电压大于某个逻辑阈值,就表明没有触摸,反之则有触摸。这种方法存在的问题在于触摸屏是一个巨大的电容器,此外还可能需要增加触摸屏引线的电容,以便滤除LCD引入的噪声。弱上拉电阻与大电容器相连会使上升时间变长,可能导致检测到虚假的触摸。四线和八线触摸屏可以测量出接触电阻,即图5中的RTOUCH。RTOUCH与触摸压力近似成正比。要测量触摸压力,需要知道触摸屏中一层或两层的电阻。图6中的公式给出了计算方法。需要注意的是,如果Z1的测量值接近或等于0(在测量过程中当触摸点靠近接地的X总线时),计算将出现一些问题,通过采用弱上拉方法可以有效改善这个问题。
附图:
图一 图二 图三 图四 图五
“小王呢?这些硬件知识看的不是很懂吧,其实没关心,具体电路时,会有详细说明的,这里只是给你个大致的意思,懂了就可以的”我拍拍小王的头说。
“嗯嗯,小王乖乖的点了头,只是..只是这些我好像在哪儿看过,过年前你都说要讲触摸屏方面的东西了,我想提前准备点,掌握着,就是想不起来了”小王苦想到。
“呵呵,说的好,这些确实我在网上找到的,是百度百科里的,鲁迅谓之---拿来主义..哈哈”
转自:http://www.cnblogs.com/hanyan225/archive/2011/03/27/1989657.html
===================================================================================================================
linux嵌入式系统开发之触摸屏---驱动篇(下/源码分析)
“啥叫旷日持久,你的一篇烂触摸屏的驱动,愣是唧唧歪歪讲了三篇还没到正题,哼”小王啊,小王,总是喜欢在那里撅着个嘴瞎抱怨。
“啊?小姐啊,你是坐着不腰疼吧,你来讲讲试试 ..真是的”一向老好人的我咋也扛不住了呢.”好好,我失礼了,今天就是最后一个小结了,是有关源码分析的”。
“好好,看来不对你来点激将法,你是不会着急的了”小王破怒而笑的说道。
“呵呵..”我苦笑到,其实小王哪里知道,今天是一个值得回忆的日子的啊,今天的课基本上是前面的一个句号,有关字符设备驱动的基本就结束了,后面就要开始有关其它部分的讲解了(比如块设备等),很是伤感,可这些又怎么能跟小王说了。
罢了,罢了,所有的我自己心里藏,开始今天的课---linux嵌入式系统开发之触摸屏---驱动篇(下/源码分析)
还记得我上节给你讲的input输入设备驱动的架构吗,今天的源码分析就是照着那个步骤来的:
必要的头文件#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 <linux/gpio.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <plat/regs-adc.h>
#include <mach/regs-gpio.h>
static struct input_dev *dev; //输入设备结构体
static struct clk *adc_clock; //保存从平台时钟列表中获取的ADC时钟
static void __iomem *base_addr; //用来保存经过虚拟映射后的内存地址
static long xp;//记录转换后的X坐标值和Y坐标值
static long yp;
static int count; //用于计数对触摸屏压下或抬起时模拟输入转换的次数
static int OwnADC = 0; //做为一个标签,只有对触摸屏操作后才对X和Y坐标进行转换
//定义一个外部的信号量ADC_LOCK,因为ADC_LOCK在ADC驱动程序中已申明这样就能保证ADC资源在ADC驱动和触摸屏驱动中进行互斥访问
extern struct semaphore ADC_LOCK;
//定义一个WAIT4INT宏,该宏将对ADC触摸屏控制寄存器进行操作S3C2410_ADCTSC_YM_SEN这些宏都定义在regs-adc.h中
#define WAIT4INT(x) (((x)<<8) | \
S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
S3C2410_ADCTSC_XY_PST(3))
//定义一个AUTOPST宏,将ADC触摸屏控制寄存器设置成自动转换模式
#define AUTOPST (S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
S3C2410_ADCTSC_AUTO_PST | S3C2410_ADCTSC_XY_PST(0))
static char *s3c2440ts_name = "s3c2440Tscreen"; //设备名
#define S3C2410TSVERSION 0x0101 //版本
static void touch_timer_fire(unsigned long data)
{
unsigned long data0; //用于记录这一次AD转换后的值
unsigned long data1;
int updown; //记录触摸屏操作状态是按下还是抬起
data0 = ioread32(base_addr + S3C2410_ADCDAT0); //读取这一次AD转换后的值,注意这次主要读的是状态
data1 = ioread32(base_addr + S3C2410_ADCDAT1);
//记录这一次对触摸屏是压下还是抬起,该状态保存在数据寄存器的第15位,所以与上S3C2410_ADCDAT0_UPDOWN
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
if(updown){ //判断触摸屏的操作状态
if(count != 0){ //如果是按下,并且ADC已经转换了报告事件和数据
long tmp;
tmp = xp;
xp = yp;
yp = tmp;
xp >>= 2;
yp >>= 2;
input_report_abs(dev, ABS_X, xp); //报告X,Y的绝对坐标值
input_report_abs(dev, ABS_Y, yp);
input_report_key(dev, BTN_TOUCH, 1); //报告按键事件,键值为1(代表触摸屏对应的按键被按下)
input_report_abs(dev, ABS_PRESSURE, 1); //报告触摸屏的状态,1表明触摸屏被按下
input_sync(dev); //等待接收方受到数据后回复确认,用于同步
}
xp = 0; //如果状态是按下,并且ADC还没有开始转换就启动ADC进行转换
yp = 0;
count = 0;
//设置触摸屏的模式为自动转换模式
iowrite32(S3C2410_ADCTSC_PULL_UP_DISABLE | AUTOPST, base_addr+S3C2410_ADCTSC);
//启动ADC转换
iowrite32(ioread32(base_addr+S3C2410_ADCCON) | S3C2410_ADCCON_ENABLE_START, base_addr+S3C2410_ADCCON);
}else{ //状态是抬起状态
count = 0;
input_report_key(dev, BTN_TOUCH, 0); //报告按键事件,键值为0(代表触摸屏对应的按键被释放)
input_report_abs(dev, ABS_PRESSURE, 0); //报告触摸屏的状态,0表明触摸屏没被按下
input_sync(dev); //等待接收方受到数据后回复确认,用于同步
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC); //将触摸屏重新设置为等待中断状态
if(OwnADC){ //如果触摸屏抬起,就意味着这一次的操作结束,所以就释放ADC资源的占有
OwnADC = 0;
up(&ADC_LOCK);
}
}
}
//定义并初始化了一个定时器touch_timer,定时器服务程序为touch_timer_fire
static struct timer_list touch_timer = TIMER_INITIALIZER(touch_timer_fire, 0, 0);
static irqreturn_t stylus_action(int irq, void *dev_id)
{ //ADC中断服务程序,AD转换完成后触发执行
unsigned long data0; //记录这一次AD转换后的值
unsigned long data1;
if(OwnADC){ //读取这一次AD转换后的值,注意这次主要读的是坐标
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
//记录这一次通过AD转换后的X坐标值和Y坐标值,根据数据手册可知,X和Y坐标转换数值
//分别保存在数据寄存器0和1的第0-9位,所以这里与上S3C2410_ADCDAT0_XPDATA_MASK就是取0-9位的值
xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;
yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;
count++; //记数这一次AD转换的次数
if(count < (1<< 2)){ //如果转换的次数小于4,则重新启动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{ //否则,启动1个时间滴答的定时器,这是就会去执行定时器服务程序上报事件和数据
mod_timer(&touch_timer, jiffies+1); //修改定时器的expire
iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC);
}
}
return IRQ_HANDLED;
}
static irqreturn_t stylus_updown(int irq, void *dev_id)
{//触摸屏中断服务程序,对触摸屏按下或提笔时触发执行
unsigned long data0; //用于记录这一次AD转换后的值
unsigned long data1;
int updown; //用于记录触摸屏操作状态是按下还是抬起
if(down_trylock(&ADC_LOCK) == 0) { //ADC资源可以获取,即上锁
OwnADC = 1; //标识对触摸屏进行了操作
data0 = ioread32(base_addr+S3C2410_ADCDAT0);//读取这一次AD转换后的值,注意这次主要读的是状态
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
//记录这一次对触摸屏是压下还是抬起,该状态保存在数据寄存器的第15位,所以与上S3C2410_ADCDAT0_UPDOWN
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
if (updown) { //如果是按下状态
touch_timer_fire(0); //如果是按下状态,则调用touch_timer_fire函数来启动ADC转换
} else {
OwnADC = 0; //如果是抬起状态,就结束了这一次的操作,释放ADC资源的占有
up(&ADC_LOCK);
}
}
return IRQ_HANDLED;
}
static inline void s3c2440_ts_connect(void)
{
s3c2410_gpio_cfgpin(S3C2410_GPG(12), S3C2410_GPG12_XMON);
s3c2410_gpio_cfgpin(S3C2410_GPG(13), S3C2410_GPG13_nXPON);
s3c2410_gpio_cfgpin(S3C2410_GPG(14), S3C2410_GPG14_YMON);
s3c2410_gpio_cfgpin(S3C2410_GPG(15), S3C2410_GPG15_nYPON);
}
static int __init s3c2440ts_init(void)
{
struct input_dev *input_dev;
//从平台时钟队列中获取ADC的时钟,这里之所以要获得这个时钟是因为ADC的转换频率
//跟时钟有关。系统的一些时钟定义在arch/arm/plat-s3c24xx/s3c2410-clock.c中
adc_clock = clk_get(NULL, "adc");
if(!adc_clock){
printk(KERN_ERR "failed to get adc lock source\n");
return -ENOENT;
}
//使能时钟,就预开启,clk_enable定义在arch/arm/plat-s3c/clock.c
clk_enable(adc_clock);
//将ADC的IO端口占用的这段IO空间映射到内存的虚拟地址S3C2410_PA_ADC
//是ADC控制器的基地址,定义在mach-s3c2410/include/mach/map.h中,0x20是虚拟地址长度大小
base_addr = ioremap(S3C2410_PA_ADC, 0x20);
if(base_addr == NULL){
printk(KERN_ERR "Failed to remap register block\n");
return -ENOMEM;
}
//配置GPIOS
s3c2440_ts_connect();
iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(0xFF),base_addr+S3C2410_ADCCON); //这些的函数都是系统提供的
iowrite32(0xffff, base_addr+S3C2410_ADCDLY);
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
input_dev = input_allocate_device();
if(!input_dev){
printk(KERN_ERR "Unable to allocate the input device!!\n");
return -ENOMEM;
}
dev = input_dev;
//设置输入设备的相关信息,evbit字段用于描述支持的事件,这里支持
//同步事件、按键事件、绝对坐标事件,BIT宏实际就是对1进行位操作,定义在linux/bitops.h中
dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS);
//keybit字段用于描述按键的类型,在input.h中定义了很多,这里用BTN_TOUCH类型来表示触摸屏的点击
dev->keybit[BITS_TO_LONGS(BTN_TOUCH)] = BIT(BTN_TOUCH);
//触摸屏使用的是绝对坐标系统。这里设置该坐标系统中X和Y坐标的最小值和最大值(0-1023范围)
//ABS_X和ABS_Y就表示X坐标和Y坐标,ABS_PRESSURE就表示触摸屏是按下还是抬起状态
input_set_abs_params(dev, ABS_X, 0, 0x3ff, 0, 0);
input_set_abs_params(dev, ABS_Y, 0, 0x3ff, 0, 0);
input_set_abs_params(dev, ABS_PRESSURE, 0, 1, 0, 0);
dev->name = s3c2440ts_name;
dev->id.bustype = BUS_RS232;
dev->id.vendor = 0xDEAD; /*经销商ID号*/
dev->id.product = 0xBEEF; /*产品ID号*/
dev->id.version = S3C2410TSVERSION;/*版本ID号*/
//申请ADC中断,AD转换完成后触发。这里使用共享中断IRQF_SHARED
//是因为该中断号在ADC驱动中也使用了,
if(request_irq(IRQ_ADC, stylus_action, IRQF_SHARED|IRQF_SAMPLE_RANDOM, "s3c2440_action", 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|IRQF_SHARED, "s3c2410_action", dev)){
printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_TC !\n");
iounmap(base_addr);
return -EIO;
}
printk(KERN_ERR "%s successfully loaded\n", s3c2440ts_name);
//一些都准备就绪,现在就把ts_dev触摸屏设备注册到输入子系统中
input_register_device(dev);
return 0;
}
static void __exit s3c2440ts_exit(void)
{
disable_irq(IRQ_ADC); //屏蔽和释放
disable_irq(IRQ_TC);
free_irq(IRQ_TC, dev);
free_irq(IRQ_ADC, dev);
if(adc_clock){ //屏蔽和销毁时钟
clk_disable(adc_clock);
clk_put(adc_clock);
adc_clock = NULL;
}
input_unregister_device(dev); //将触摸屏设备从输入子系统中注销
iounmap(base_addr); //释放虚拟地址映射空间
}
module_init(s3c2440ts_init);
module_exit(s3c2440ts_exit);
说明:这后来的很多驱动,慢慢就需要编译内核了,因为买的板子带的内核都包含了驱动,所以为了保证我们的驱动不受它的干扰,所以..不说你也明白是吧。
“小王,洋洋洒洒一大堆,我再来给你捋捋顺序吧”
1)如果触摸屏感觉到触摸,则触发触摸屏中断即进入stylus_updown,获取ADC_LOCK后判断触摸屏状态为按下,则调用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)如果触摸抬起了,则上报释放事件,并将触摸屏重新设置为等待中断状态。
“小王呢,有关触摸屏的相关部分经过三次专题讲解,终于讲完了。总体来说,前边的有关字符设备驱动的相关理论和实例也都讲完了。从下次起,我们就要开始讲解有关设备驱动的其他几个大的环节了,比如块设备啦, 音频设备,网络设备等等”我如释重负而心里遗憾的说道。
“嗯,好好,后面难度大吗?”小王似乎充满期待而又满脸担心。
“放心吧,驱动相关的理论基础都差不多了,后面的其他大块,无非是设备本身的复杂性要求我们更多更巧妙的利用前边学习的理论基础,再说啦,有我呢”我啊,胸脯一排。
“那行,有你我就不怕….”