触摸屏的基本原理是,用手指或其他物体触摸安装在显示器前端的触控屏时,所触摸的位置(以坐标形式)由触摸屏控制器检测,并通过接口(如RS-232串行口)送到CPU,从而确定输入的信息。
触摸屏系统一般包括触摸屏控制器(卡)和触摸检测装置两个部分:
电阻类触摸屏的关键在于材料科技。电阻屏根据引出线数多少,分为四线、五线、六线等多线电阻触摸屏。下面以四线电阻式触摸屏为例介绍。
四线电阻式触摸屏的结构如图,在玻璃或丙烯酸基板上覆盖有两层透平,均匀导电的ITO层,分别做为X电极和Y电极,它们之间由均匀排列的透明格点分开绝缘。其中下层的ITO与玻璃基板附着,上层的ITO附着在PET薄膜上。X电极和Y电极的正负端由“导电条”(图中黑色条形部分)分别从两端引出,且X电极和Y电极导电条的位置相互垂直。引出端X-,X+,Y-,Y+一共四条线,这就是四线电阻式触摸屏名称的由来。
当有物体接触触摸屏表面并施以一定的压力时,上层的ITO导电层发生形变与下层ITO发生接触,该结构可以等效为上图中的电路。
计算触点的X,Y坐标分为如下两步:
所以:
$$x=\frac{V_{x_+}}{V_{driver}} \times width_{screen}$$
$$y=\frac{V_{y_+}}{V_{driver}} \times height_{screen}$$
测得的电压通常由ADC转化为数字信号,再进行简单处理就可以做为坐标判断触点的实际位置。
四线电阻式触摸屏除了可以得到触点的X/Y坐标,还可以测得触点的压力,这是因为top layer施压后,上下层ITO发生接触,在触点上实际是有电阻存在的,如图3的Rtouch。压力越大,接触越充分,电阻越小,通过测量这个电阻的大小可以量化压力大小。
由于我们Mini2440开发板使用的S3C2440 SOC,只支持四线电阻式触摸屏。这里我们使用的LCD型号为LCD-T35(TD035STEB4),该4线连接在S3C2440的AIN4~AIN7引脚上,该引脚专门是用来接收模拟输入信号:
引脚说明:
如下图, 把XP接3.3V 、XM接0V,YP和YM悬空。我们以按压X坐标的中间位置,X层和Y层便闭合,此时YP就会输出当前X坐标值的1.66V给CPU 。
如下图, 把YP接3.3V , YM接0V,XP和XM悬空,我们以按压X坐标的中间位置, X层和Y层便闭合了,此时XP就会输出当前X坐标值的1.66V给CPU 。
查看s3c2440手册第16章ADC & TOUCH SCREEN INTERFACE章节,触摸屏A/D转换器和触摸屏接口框图如下:
触摸屏接口模式共有四种:
ADC的工作频率最大为2.5MHZ,需要设置寄存器ADCCON->PRSCVL更改分频系数。A/D转换时间:
当PCLK频率在50MHz并且预分频器的值为49时,10位的转换时间为:
$$A/D转换器频率=\frac{50MHZ}{49+1}=1MHz$$
$$转换时间=frac{1}{1M/5}=\frac{1}{200k}=5us$$
ADC控制寄存器(ADCCON)
寄存器信息:
寄存器名 |
地址 |
是否读写 |
描述 |
复位值 |
ADCCON |
0x58000000 |
R/W |
ADC控制寄存器 |
0x3FC4 |
寄存器位信息:
ADCCON | 位 | 描述 | 初始状态 |
ECFLG | [15] |
转换结束标志位(只读) 0 = A/D正在转换 1 = A/D转换已结束 |
0 |
PRSCEN | [14] |
A/D转换器预分频器使能 0 = 禁止 1 = 使能 |
0 |
PRSCVL | [13:6] |
A/D转换器预分频值 数值范围: 0~255 注意:ADC频率应该设置为低于PCLK的1/5 |
0xFF |
SEL_MUX | [5:3] |
模拟输入通道选择 000 = AIN0 001= AIN1 010 = AIN2 011 = AIN3 |
0 |
STDBM | [2] |
待机模式选择 0 = 正常工作模式 1 = 待机模式 |
1 |
READ_ START | [1] |
读启动 A/D 转换 0 = 禁止读启动操作 1 = 使能读启动操作 |
0 |
ENABLE_START | [0] |
使能A/D转换启动。如果 READ_START 为使能,则此值无效 0 = 无操作 1 = A/D 转换启动且此位在启动后被清零 |
0 |
ADC触摸屏控制寄存器(ADCTSC)
寄存器信息:
寄存器名 |
地址 |
是否读写 |
描述 |
复位值 |
ADCTSC |
0x58000004 |
R/W |
ADC触摸屏控制寄存器 |
0x58 |
寄存器位信息:
ADCTSC | 位 | 描述 | 初始状态 |
UD_SEB | [8] |
检测笔尖起落状态 0 = 检测笔尖落下中断信号 1 = 检测笔尖抬起中断信号 |
0 |
YM_SEN | [7] |
YM 开关使能 0 = YM 输出驱动器禁止 1 = YM 输出驱动器使能 |
0 |
YP_SEN | [6] |
YP 开关使能 0 = YP 输出驱动器禁止 1 = YP 输出驱动器使能 |
1 |
XM_SEN | [5] |
XM 开关使能 0 = XM 输出驱动器禁止 1 = XM 输出驱动器使能 |
0 |
XP_SEN | [4] |
XP 开关使能 0 = XP 输出驱动器禁止 1 = XP 输出驱动器使能 |
1 |
PULL_UP | [3] |
上拉开关使能 0 = XP 上拉使能 1 = XP 上拉禁止 |
1 |
AUTO_PST | [2] |
0 = XP 上拉使能 1 = XP 上拉禁止 0 = 正常 ADC 转换 1 = 自动顺序X方向和Y方向测量 |
0 |
XY_PST | [1:0] |
手动测量X方或Y方向 00 = 无操作模式 01 = X 方向测量 10 = Y 方向测量 11 = 等待中断模式 |
0 |
ADC转换数据寄存器(ADCDAT0)
寄存器信息:
寄存器名 |
地址 |
是否读写 |
描述 |
复位值 |
ADCDAT0 |
0x5800000C |
R |
ADC转换数据寄存器 |
- |
寄存器位信息:
ADCDAT0 | 位 | 描述 | 初始状态 |
UPDOWN | [15] |
等待中断模式中笔尖的起落状态 0 = 笔尖落下态 1 = 笔尖抬起态 |
- |
AUTO_PST | [14] |
自动顺序 X 方向和 Y 方向转换 0 = 正常 ADC 转换 1 = 顺序 X 方向、Y 方向测量 |
- |
XY_PST | [13:12] |
手动 X 方向或 Y 方向测量 00 = 无操作模式 01 = X 方向测量 10 = Y 方向测量 11 = 等待中断模式 |
- |
保留 | [11:10] |
保留 |
- |
XPDATA | [9:0] |
X 方向转换数值(包括正常 ADC 转换数值) 数值范围:0 至 3FF |
- |
ADC转换数据寄存器(ADCDAT1)
寄存器信息:
寄存器名 |
地址 |
是否读写 |
描述 |
复位值 |
ADCDAT1 |
0x58000010 |
R |
ADC转换数据寄存器 |
- |
寄存器位信息:
ADCDAT1 | 位 | 描述 | 初始状态 |
UPDOWN | [15] |
等待中断模式中笔尖的起落状态 0 = 笔尖落下态 1 = 笔尖抬起态 |
- |
AUTO_PST | [14] |
自动顺序X方向和Y方向转换 0 = 正常ADC 转换 1 = 顺序X方向、Y方向测量 |
- |
XY_PST | [13:12] |
手动X方向或Y方向测量 00 = 无操作模式 01 = X 方向测量 10 = Y 方向测量 11 = 等待中断模式 |
- |
保留 | [11:10] |
保留 |
- |
YPDATA | [9:0] |
Y方向转换数值(包括正常 ADC 转换数值) 数值范围:0 至 3FF |
- |
对于触摸屏驱动,也是使用输入子系统框架进行编写,输入子系统相关内容在linux驱动移植-输入子系统示例里详细分析了:
右边的驱动事件处理,内核是已经写好了的,所以我们的触摸屏只需要写具体的驱动设备,然后内核会与触摸屏驱动tsdev.c自动连接 。
(1) 我们首先通过input_allocate_device动态创建struct input_dev结构对象dev;
(2) 初始化dev;
(3) 然后调用input_register_device注册这个设备;
(4) 初始化触摸屏相关的硬件:
在/work/sambashare/drivers路径下创建12.lcd_touch项目,并创建lcd_touch_dev.c和Makefile文件。
#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/io.h> #include <linux/uaccess.h> #include <linux/gpio.h> #include <linux/irq.h> // 包含了mach/irqs.h #include <linux/interrupt.h> #include <linux/gpio/machine.h> #include <mach/gpio-samsung.h> #include <linux/input.h> #include <linux/timer.h> #include <linux/clk.h> #define IRQF_SAMPLE_RANDOM 0x00000040 /* * s3c2440 APC和触摸屏相关寄存器 */ struct s3c_ts_regs{ unsigned long adccon; unsigned long adctsc; unsigned long adcdly; unsigned long adcdat0; unsigned long adcdat1; unsigned long adcupdn; }; /* 定义一个input_dev结构体 */ static struct input_dev *s3c_ts_dev; /* 寄存器 */ static struct s3c_ts_regs *s3c_ts_regs; /* 定时器 */ static struct timer_list s3c_ts_timer; /* 进入等待笔尖按下中断模式 */ static void enter_wait_pen_down_mode(void) { /* 设置寄存器ADCTSC=0x0d3,开启INT_TC中断,笔尖按下触发*/ s3c_ts_regs->adctsc = 1 << 7 | 1 << 6 | 1<<4 | 3 << 0; } /* 进入等待笔尖抬起中断模式 */ static void enter_wait_pen_up_mode(void) { /* 设置寄存器ADCTSC=0x01d3,开启IRQ_TC中断,笔尖抬起触发*/ s3c_ts_regs->adctsc = 1<< 8 | 1 << 7 | 1 << 6 | 1<<4 | 3 << 0; } /* 进入XY自动转换模式,ADC转换完成后,触发INT_ADC中断 */ static void enter_measure_xy_mode(void) { /* 启动XY自动转换 */ s3c_ts_regs->adctsc = (1<<3)|(1<<2); /* 启动1次ADC转换,开启一次ADC转换,当ADC转换成功该位清0 */ s3c_ts_regs->adccon |= (1<<0); } /* * IRQ_TC中断处理服务 * 笔尖按下或者抬起进入 */ static irqreturn_t tc_handler(int irq, void *dev_id) { //如果触摸屏当前为松开状态 if (s3c_ts_regs->adcdat0 & (1<<15)) { /*上报压力值为0,等价 input_event(s3c_ts_dev, EV_ABS, ABS_PRESSURE, 0) */ input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0); /*上报BTN_TOUCH按键值松开,等价 input_event(s3c_ts_dev, EV_KEY, BTN_TOUCH, 0) */ input_report_key(s3c_ts_dev, BTN_TOUCH, 0); /*上报同步事件,通知系统有事件上报 */ input_sync(s3c_ts_dev); /* 进入等待笔尖按下中断模式 */ enter_wait_pen_down_mode(); printk("tc_handler up\n"); } else { /* 进入测量X/Y坐标模式并启动一次ADC */ enter_measure_xy_mode(); printk("tc_handler down\n"); } return IRQ_RETVAL(IRQ_HANDLED); } /* * 计算若干次的采样值是否存在较大误差,如果存在较大误差,放弃这次采样 */ 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; } /* * IRQ_ADC中断处理服务 获取x、y坐标 * 自动顺序X/Y方向转换模式,ADC转换成功后,会进入IRQ_ADC中断函数(系统自动把X坐标写入到ADCDAT0、把Y坐标写入ADCDAT1) */ static irqreturn_t adc_handler(int irq, void *dev_id) { /* 计数 */ static int cnt=0; /* 保存x、y */ static int x[4],y[4]; //如果触摸屏当前为松开状态 if (s3c_ts_regs->adcdat0 & (1<<15)) { /*上报压力值为0,等价 input_event(s3c_ts_dev, EV_ABS, ABS_PRESSURE, 0) */ input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0); /*上报BTN_TOUCH按键值松开,等价 input_event(s3c_ts_dev, EV_KEY, BTN_TOUCH, 0) */ input_report_key(s3c_ts_dev, BTN_TOUCH, 0); /*上报同步事件,通知系统有事件上报 */ input_sync(s3c_ts_dev); /* 进入等待笔尖按下中断模式 */ enter_wait_pen_down_mode(); cnt = 0; } else { // 笔尖按下后 连续测量4次 // x坐标 x[cnt]=s3c_ts_regs->adcdat0 & 0x3ff; // y y[cnt]=s3c_ts_regs->adcdat1 & 0x3ff; cnt++; // 4次求平均 if(cnt == 4){ // 计算若干次的采样值是否存在较大误差,如果存在较大误差,放弃这次采样 if (s3c_filter_ts(x, y)){ //上报X方向值 input_report_abs(s3c_ts_dev, ABS_X, (x[0]+x[1]+x[2]+x[3])/4); //上报Y方向值 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); //上报BTN_TOUCH按键按下 input_report_key(s3c_ts_dev, BTN_TOUCH, 1); //上报同步事件,通知系统有事件上报 input_sync(s3c_ts_dev); printk("X: %04d,y: %04d \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(); /* 启动定时器处理长按/滑动的情况 10ms后如果仍然没有笔尖抬起,再次进行测量坐标 */ mod_timer(&s3c_ts_timer, jiffies + HZ/100); } else { /* 进入测量X/Y坐标模式并启动一次ADC */ enter_measure_xy_mode(); } } return IRQ_RETVAL(IRQ_HANDLED); } /* * 定时器超时函数 * 将输入转换为转换为统一事件形式 */ static void s3c_ts_timer_function(struct timer_list *t) { /* 如果触摸屏当前为松开状态 */ if (s3c_ts_regs->adcdat0 & (1<<15)) { /* 上报压力值为0,等价 input_event(s3c_ts_dev, EV_ABS, ABS_PRESSURE, 0) */ input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0); /*上报BTN_TOUCH按键值松开,等价 input_event(s3c_ts_dev, EV_KEY, BTN_TOUCH, 0) */ input_report_key(s3c_ts_dev, BTN_TOUCH, 0); /*上报同步事件,通知系统有事件上报 */ input_sync(s3c_ts_dev); /* 进入等待笔尖按下中断模式 */ enter_wait_pen_down_mode(); } else { /* 进入测量X/Y坐标模式并启动一次ADC */ enter_measure_xy_mode(); } } /* * 入口函数 */ static int s3c_ts_init(void) { int err; struct clk * clk; printk("touch screen driver init\n"); /* 向内核 申请input_dev结构体 */ s3c_ts_dev = input_allocate_device(); /* 设置input_dev、 输入事件code定义在include/uapi/linux/input-event-codes.h */ set_bit(EV_KEY,s3c_ts_dev->evbit); // 支持按键事件 set_bit(EV_ABS,s3c_ts_dev->evbit); // 支持绝对位移事件 input_set_capability(s3c_ts_dev,EV_KEY,BTN_TOUCH); //触摸屏笔尖按下 input_set_abs_params(s3c_ts_dev, ABS_X, 0, 0x3FF, 0, 0);//s3c2440手册ADC是10位,所以第四个参数设置为3FF 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); /* 注册input_dev */ err = input_register_device(s3c_ts_dev); if (err) { printk("input touch screen driver registration failed\n"); /* 释放驱动结构体 */ input_free_device(s3c_ts_dev); return err; } else { printk("input touch screen driver registered successfully\n"); } /* 使能时钟(设置CLKCON[15]) */ clk = clk_get(NULL,"adc"); clk_prepare_enable(clk); /* 设置s3c2440的ADC/ts寄存器 */ s3c_ts_regs = ioremap(0x58000000, sizeof(struct s3c_ts_regs)); s3c_ts_regs->adccon = (1<<14)|(49<<6); // 预分频器使能,分频值为49 1MHZ s3c_ts_regs->adcdly = 0xffff; /* 注册中断,这里指定了中断线程化处理函数 */ request_irq(IRQ_TC, tc_handler, IRQF_SAMPLE_RANDOM, "ts_tc", 0); request_irq(IRQ_ADC, adc_handler, IRQF_SAMPLE_RANDOM, "ts_adc", 0); /* 初始化定时器 */ timer_setup(&s3c_ts_timer,s3c_ts_timer_function,0); add_timer(&s3c_ts_timer); /* 进入等待笔尖按下中断模式*/ enter_wait_pen_down_mode(); return 0; } /* * 出口函数 */ static void __exit s3c_ts_exit(void) { printk("touch screen driver exit\n"); /* 删除定时器 */ del_timer(&s3c_ts_timer); /* 释放中断 */ 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); return; } module_init(s3c_ts_init); module_exit(s3c_ts_exit); MODULE_LICENSE("GPL");
KERN_DIR :=/work/sambashare/linux-5.2.8 all: make -C $(KERN_DIR) M=`pwd` modules clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order obj-m += lcd_touch_dev.o
我们在linux驱动移植-LCD驱动分析这一节的基础上进行试验。
配置内核,移除将内核自带的触摸屏驱动:
root@zhengyang:/work/sambashare/linux-5.2.8# make menuconfig
Device Drivers --->
保存文件,输入文件名s3c2440_defconfig,在当前路径下生成s3c2440_defconfig:存档:
mv s3c2440_defconfig ./arch/arm/configs/
此时重新执行:
make distclean make s3c2440_defconfig make uImage V=1
将uImage复制到tftp服务器路径下:
cp /work/sambashare/linux-5.2.8/arch/arm/boot/uImage /work/tftpboot/
开发板uboot启动完成后,内核启动前,按下任意键,进入uboot,下载内核到内存,并写NAND FLASH:
tftp 30000000 uImage nand erase.part kernel nand write 30000000 kernel
下载完成后,重启开发板,内核启动完成后会在显示屏上看到启动logo。
在12.lcd_touch_dev路径下编译:
root@zhengyang:/work/sambashare/drivers/12.lcd_touch_dev# cd /work/sambashare/drivers/12.lcd_touch_dev root@zhengyang:/work/sambashare/drivers/12.lcd_touch_dev# make
拷贝驱动文件到nfs文件系统:
root@zhengyang:/work/sambashare/drivers/12.lcd_touch_dev# cp lcd_touch_dev.ko /work/nfs_root/rootfs/
重启开发板,加载lcd驱动,执行如下命令:
insmod lcd_touch_dev.ko
运行命令cat /proc/interrupts可以查看当前系统有哪些中断服务:
[root@zy:/]# cat /proc/interrupts CPU0 29: 21110 s3c 13 Edge samsung_time_irq 32: 0 s3c 16 Edge s3c2410-lcd 42: 0 s3c 26 Edge ohci_hcd:usb1 43: 0 s3c 27 Edge s3c2440-i2c.0 55: 1460 s3c-ext 7 Edge eth0 74: 14 s3c-level 0 Edge s3c2440-uart 75: 117 s3c-level 1 Edge s3c2440-uart 83: 0 s3c-level 9 Edge ts_tc 84: 0 s3c-level 10 Edge ts_adc 87: 0 s3c-level 13 Edge s3c2410-wdt
查看设备节点文件:
[root@zy:/]# ls /dev/input -l total 0 crw-rw---- 1 0 0 13, 64 Jan 1 00:00 event0
执行如下命令:
hexdump /dev/input/event0
此时再点一下触摸屏,串口端输入如下:
第1列表示hexdump序列号(如0000000)
第2、3列表示秒(如013c 0000)
第4、5列表示微妙(如c7c6 0000)
第6列表示type(如0003表示ABS绝对位移类型,0001表示按键类型)
第7列表示code(如0000表示x方向ABS_X,0001表示y方向ABS_Y,0018表示ABS_PRESSURE,014a表示BTN_TOUCH)
第8、9列表示对应type的对应code值(如020b 0000)
参考文章
[1]十三、Linux驱动之触摸屏驱动
[2]18.Llinux-触摸屏驱动(详解)
[3]四线电阻触摸屏工作原理的示意图
[4]四线电阻触摸屏原理
[5]常用低成本:四线电阻式触摸屏原理