1. 基本概念
常用的触摸屏类型有两种:阻性触摸屏和容性触摸屏。阻性触摸屏是一种传感器,它将矩形区域中触摸点(X, Y)的物理位置转换为代表X坐标和Y坐标的电压。触摸屏包含上下叠合的两个透明层阻性材料,中间由一种弹性材料隔开。当触摸屏表面受到压力时,顶层和底层之间会产生触碰。所用的电阻式触摸屏都采用分压器原理来产生代表X坐标和Y坐标的电压。如下图所示,分压器是通过将两个电阻进行串联来实现的。上面的电阻R1连接正参考电压Vref,下面的电阻R2接地。连个电阻连接点处的电压测量值与下面那个电阻的阻值成正比。
四线触摸屏示意图如下:
四线触摸屏包含两个阻性层。其中一层在屏幕的左右边各有一条垂直总线,另一层在屏幕的底部和顶部各有一条水平总线,见上图。为了X轴方向进行测量,将左侧总线偏置为0V,右侧总线偏置为Vref。将顶部或底部总线连接到ADC,当顶层和底层相接触时即可做一次测量。
2. 分析设备
2.1 引脚连接
本人使用的是JZ2440v3开发板,该开发板CPU使用的是S3C2440A,使用的是4线触摸屏,该4线连接在2440的AIN4~AIN7引脚上,如图:
2.2 s3c2440的触摸屏接口
查看s3c2440手册ADC & TOUCH SCREEN INTERFACE触摸屏章节,触摸屏A/D转换器和触摸屏接口框图如下:
值得注意的是两个中断产生的时间,INT_TC在点击触摸屏时发生,INT_ADC在数模转换完成时发生。手册中还提到了A/D的转换时间,如下公式:
ADC的工作频率最大为2.5MHZ,正确使用需要设置寄存器ADCCON->PRSCVL更改分频系数,对于JZ2440这里要设置ADC的工作频率为1MHZ(即设置ADCCON的bit6为49)。
s3c2440手册提到了触摸屏接口的4种模式:
1. 正常转换模式
2. 分离式X/Y位置转换方式
3. 自动X/Y位置转换模式
4. 等待中断模式
从数据手册中我们可以得到设置模式相关信息:
1. 设置为自动X/Y位置转换模式
设置寄存器ADCTSC=0x0c,系统会把X坐标存入寄存器ADCDAT0,把Y坐标存入ADCDAT1。
2. 设置为等待中断模式
设置寄存器ADCTSC=0xd3(设置等待按下中断信号到来)
设置寄存器ADCTSC=0x1d3(设置等待松开中断信号到来)
触摸屏工作流程:
1. 选择XY坐标转换模式(独立转换/连续转换)
2. 设置触摸屏到等待状态
3. 如果中断发生,启动ADC转换(转换获得的坐标值会存入相应的寄存器中)
4. 获得XY坐标,返回到步骤2
3. 编写代码
对于触摸屏驱动,也是使用输入子系统框架进行编写,输入子系统相关内容在九、Linux驱动之输入子系统分析里详细分析了,内核已经写好了驱动事件处理部分(触摸屏即对应tsdev.c),所以我们只需要写具体的驱动设备部分,然后内核会将两部分自动连接。先整理代码整体流程。
3.1 代码流程
3.1.1 在init入口函数中
1. 分配一个input_dev结构体
2. 设置input_dev的成员
2.1 设置input_dev->evbit,即设置支持按键事件,绝对位移事件
2.2 设置input_dev-> keybit,支持按键事件的触摸屏笔尖按下事件
2.3 设置input_dev-> absbit,支持ABS_X、ABS_Y、 ABS_PRESSURE
2.3.1 input_set_abs_params(ts.dev, ABS_X, 0, 0x3FF, 0, 0); //ADC是10位,所以第四个参数设置为3FF
2.3.2 input_set_abs_params(ts.dev, ABS_Y, 0, 0x3FF, 0, 0); //ADC是10位,所以第四个参数设置为3FF
2.3.3 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启动延时时间设为最大值,使触摸电压稳定后再发出IRQ_TC中断
4.4 开启IRQ_TC中断、开启IRQ_ADC中断
4.5 初始化定时器,增加触摸滑动功能
4.6 最后设置寄存器ADCTSC=0x0d3,开启IRQ_TC中断,即设置触摸屏进入等待按下中断模式(当触摸屏被按下,进入IRQ_TC中断函数)
3.1.2 在出口函数中
1. 注销内核里的input_dev
2. 释放input_dev
3. 释放中断、删除定时器、iounmap注销地址
3.1.3 在IRQ_TC中断函数中
1. 若判断触摸屏当前为松开状态,设置寄存器ADCTSC =0XD3,即设置触摸屏为等待按下模式
2. 若判断触摸屏当前为按下状态,设置为XY自动转换模式,启动一次ADC转换(ADC转换成功后,会进入IRQ_ADC中断函数)
3.1.4 在IRQ_ADC中断函数中
1. 若判断触摸屏当前为松开状态,设置寄存器ADCTSC =0XD3,即设置触摸屏为等待按下模式
2. 若判断触摸屏当前为按下状态,获取ADCDAT0/1的低10位,计算出X/Y方向坐标值。多次测量(多次设置为XY自动转换模式并启动ADC转换,转换完成就会再次进入IRQ_ADC中断函数),测量4次后判断精度,满足则上报数据,否则设置成等待松开模式,10ms后启动定时器超时函数
3.1.5 在定时器超时函数中
1. 若判断触摸屏当前为松开状态,设置寄存器ADCTSC =0XD3,即设置触摸屏为等待按下模式
2. 若判断触摸屏当前为按下状态,设置为XY自动转换模式,启动一次ADC转换(ADC转换成功后,会进入IRQ_ADC中断函数)
3.2 编写代码
触摸屏驱动程序s3c_ts.c完整代码如下:
#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 *s3c_ts_dev;
struct s3c_ts_regs{
unsigned long adccon;
unsigned long adctsc;
unsigned long adcdly;
unsigned long adcdat0;
unsigned long adcdat1;
unsigned long adcupdn;
};
static struct s3c_ts_regs * s3c_ts_regs;
static struct timer_list ts_timer;
/*进入等待按下模式*/
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;
}
/*进入XY自动转换模式*/
static void enter_measure_xy_mode(void)
{
s3c_ts_regs->adctsc = (1<<3)|(1<<2);
}
/*启动一次ADC转换*/
static void start_adc(void)
{
s3c_ts_regs->adccon |=(1<<0);
}
static void s3c_ts_timer_function(unsigned long data)
{
if (s3c_ts_regs->adcdat0 & (1<<15)) //如果触摸屏当前为松开状态
{
input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);//上报压力值为0
input_report_key(s3c_ts_dev, BTN_TOUCH, 0);//上报BTN_TOUCH按键值松开
input_sync(s3c_ts_dev);//上报同步事件,通知系统有事件上报
enter_wait_pen_down_mode();///*进入等待按下模式*/
}
else
{
/* 进入测量X/Y坐标模式并启动一次ADC */
enter_measure_xy_mode();
start_adc();
}
}
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;
}
static irqreturn_t pen_down_up_irq(int irq, void *dev_id)
{
if (s3c_ts_regs->adcdat0 & (1<<15)) //如果触摸屏当前为松开状态
{
input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);//上报压力值为0
input_report_key(s3c_ts_dev, BTN_TOUCH, 0);//上报BTN_TOUCH按键值松开
input_sync(s3c_ts_dev);//上报同步事件,通知系统有事件上报
enter_wait_pen_down_mode();///*进入等待按下模式*/
}
else
{
/* 进入测量X/Y坐标模式并启动一次ADC */
enter_measure_xy_mode();
start_adc();
}
return IRQ_HANDLED;
}
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt=0;
static int x[4],y[4];
if (s3c_ts_regs->adcdat0 & (1<<15)) //如果触摸屏当前为松开状态
{
input_report_abs(s3c_ts_dev, ABS_PRESSURE, 0);//上报压力值为0
input_report_key(s3c_ts_dev, BTN_TOUCH, 0);//上报BTN_TOUCH按键值松开
input_sync(s3c_ts_dev);//上报同步事件,通知系统有事件上报
enter_wait_pen_down_mode();///*进入等待按下模式*/
cnt=0;
}
else
{
x[cnt]=s3c_ts_regs->adcdat0 & 0x3ff;
y[cnt]=s3c_ts_regs->adcdat1 & 0x3ff;
cnt++;
if(cnt==4)
{
if (s3c_filter_ts(x, y))
{
input_report_abs(s3c_ts_dev, ABS_X, (x[0]+x[1]+x[2]+x[3])/4);//上报X方向值
input_report_abs(s3c_ts_dev, ABS_Y, (y[0]+y[1]+y[2]+y[3])/4);//上报Y方向值
input_report_abs(s3c_ts_dev, ABS_PRESSURE, 1);//上报压力方向值
input_report_key(s3c_ts_dev, BTN_TOUCH, 1);//上报BTN_TOUCH按键值按下
input_sync(s3c_ts_dev);//上报同步事件,通知系统有事件上报
}
cnt=0;
enter_wait_pen_up_mode();
/* 启动定时器处理长按/滑动的情况 */
mod_timer(&ts_timer, jiffies + HZ/100);
}
else
{
/* 进入测量X/Y坐标模式并启动一次ADC */
enter_measure_xy_mode();
start_adc();
}
}
return IRQ_HANDLED;
}
static int s3c_ts_init(void)
{
struct clk* clk;
/*1. 分配一个input_dev结构体*/
s3c_ts_dev=input_allocate_device();
/*2. 设置*/
/*2.1. 能产生哪类事件*/
set_bit(EV_KEY, s3c_ts_dev->evbit);
set_bit(EV_ABS, s3c_ts_dev->evbit);
/*2.2. 能产生这类事件的哪些事件*/
set_bit(BTN_TOUCH, s3c_ts_dev->keybit);
input_set_abs_params(s3c_ts_dev, ABS_X, 0, 0x3FF, 0, 0);//s3c2410手册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);
/*3. 注册*/
input_register_device(s3c_ts_dev);
/*4. 硬件相关操作*/
/*4.1. 使能时钟(设置CLKCON[15])*/
clk=clk_get(NULL,"adc");
clk_enable(clk);
/*4.2. 设置s3c2440的ADC/ts寄存器*/
s3c_ts_regs=ioremap(0x58000000, sizeof(struct s3c_ts_regs));
s3c_ts_regs->adccon = (1<<14)|(49<<6);
/*
*必须得进入等待按下中断或者等待松
*开中断模式"ts_pen"这个中断才能在触摸
*屏被按下或松开时被触发。
*/
request_irq(IRQ_TC, pen_down_up_irq, IRQF_SAMPLE_RANDOM, "ts_pen", NULL);
request_irq(IRQ_ADC, adc_irq, IRQF_SAMPLE_RANDOM, "adc", NULL);
s3c_ts_regs->adcdly = 0xffff; //设置ADCDLY为最大值, 这使得电压稳定后再发出IRQ_TC中断
init_timer(&ts_timer);
ts_timer.function = s3c_ts_timer_function;
add_timer(&ts_timer);
enter_wait_pen_down_mode();
return 0;
}
static void s3c_ts_exit(void)
{
input_unregister_device(s3c_ts_dev);
input_free_device(s3c_ts_dev);
free_irq(IRQ_TC,NULL);
free_irq(IRQ_ADC,NULL);
iounmap(s3c_ts_regs);
del_timer(&ts_timer);
}
module_init(s3c_ts_init);
module_exit(s3c_ts_exit);
MODULE_LICENSE("GPL");
Makefile完整代码如下:
KERN_DIR = /work/system/linux-2.6.22.6 //内核目录
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += s3c_ts.o
4. 测试
内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10
4.1 配置内核
1. 重新配置内核,将内核自带的触摸屏驱动配置为模块。在linux-2.6.22.6内核目录下执行:
make menuconfig
2. 配置步骤如下:
Device Drivers --->
Input device support --->
[*] Touchscreens --->
< > S3C2410/S3C2440 touchscreens (将内核自带的触摸屏驱动去掉)
4.2 重烧内核
1. 编译内核与模块
make uImage
2. 将linux-2.6.22.6/arch/arm/boot下的uImage烧写到开发板,网络文件系统启动。
4.3 测试
首先安装新驱动。在开发板目录上执行:
Insmod s3c_ts.ko
ls /dev/event*
此时新出现了/dev/event0这个设备,就是我们写的触摸屏驱动。继续执行:
hexdump /dev/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)
4.4 安装tslib
1. 将tslib-1.4.tar.gz放到服务器上,在ubuntu执行以下命令:
tar xzf tslib-1.4.tar.gz (解压文件)
cd tslib
./autogen.sh
mkdir tmp
echo "ac_cv_func_malloc_0_nonnull=yes" >arm-linux.cache
./configure --host=arm-linux --cache-file=arm-linux.cache --prefix=$(pwd)/tmp
make (编译)
make install (安装到tmp)
cp tmp /work/nfs_root/first_fs/ts_dir -rfd (将tmp目录复制到网络文件系统目录)
2. 网络文件系统启动开发板,进入刚才安装好的tslib目录,执行:
cd /ts_dir/tmp
cp * / -rfd (再将该目录里所有内容拷贝到开发板的根目录里)
3. 安装lcd相关驱动,lcd相关内容参考十二、Linux驱动之LCD驱动,开发板上执行命令如下:
insmod cfbcopyarea.ko
insmod cfbfillrect.ko
insmod cfbimgblt.ko
insmod lcd.ko (安装lcd相关驱动)
insmod s3c_ts.ko (安装触摸屏驱动)
4. 修改 /etc/ts.conf文件,开发板上执行命令如下:
vi /etc/ts.conf
改为(去掉第二行的#号和第一个空格):
5. 设置触摸屏的环境变量。开发板上执行命令如下:
export TSLIB_TSDEVICE=/dev/event0 (触摸屏是哪个设备就写哪个)
export TSLIB_CALIBFILE=/etc/pointercal
export TSLIB_CONFFILE=/etc/ts.conf
export TSLIB_PLUGINDIR=/lib/ts
export TSLIB_CONSOLEDEVICE=none
export TSLIB_FBDEVICE=/dev/fb0 (lcd设备)
6. 校验。开发板上执行如下:
ts_calibrate (开发板上此时会出现校验触摸点)
7. 测试 。开发板上执行如下:
ts_test (就可以在lcd上绘画了!)
PS:在第1点中将tmp目录复制到网络文件系统目录时,将一些测试程序如ts_calibrate 、ts_test 等拷贝到了网络文件系统bin目录下。