Part17: 触摸屏驱动程序[基于输入子系统实现]
0 电阻屏原理
一张图足矣,如下
备注:可结合《嵌入式Linux应用开发完全手册》理解
电阻屏的原理不难理解,但有两点需注意
1.使用电阻屏前,需要校验以确定坐标转换关系
2.需根据s3c2440手册对应章节编写触摸屏相关硬件操作
由于下面使用自动测量X/Y坐标模式,YM\YP XM/XP 还有上拉电阻的开闭自动完成
下面直接看代码,看看触摸屏基本驱动程序如何编写(基于输入子系统实现)
备注:代码已上传到https://blog.youkuaiyun.com/qq_42800075/article/details/105670841
1 基本框架:打印测量的电压值
头文件:s3c_ts.h(适用本博文全部代码,只贴一遍)
#ifndef _S3C_TS_H
#define _S3C_TS_H
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/input.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/io.h>
#include <plat/adc.h>
#include <plat/regs-adc.h>
#include <plat/ts.h>
struct s3c_ts_regs{
unsigned long adccon;
unsigned long adctsc;
unsigned long adcdly;
unsigned long adcdat0;
unsigned long adcdat1;
unsigned long adcupdn;
};
#endif
TS驱动文件:s3c_ts.c
#include "s3c_ts.h"
static struct s3c_ts_regs *s3c_ts_regs;
static struct input_dev *s3c_ts_dev;
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;
}
static void enter_measure_xy_mode(void) // 进入自动测量xy模式
{
s3c_ts_regs->adctsc = (1<<3) | (1<<2);
}
static void start_adc(void) // 开启ADC转换
{
s3c_ts_regs->adccon |= (1<<0);
}
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
printk("adc_irq cnt = %d, x = %d, y = %d\n", ++cnt, s3c_ts_regs->adcdat0 & 0x3ff,
s3c_ts_regs->adcdat1 & 0x3ff);
enter_wait_pen_up_mode();
return IRQ_HANDLED;
}
static irqreturn_t pen_down_up_irq(int irq, void *dev_id)
{
if(s3c_ts_regs->adcdat0 & (1<<15))
{
printk("pen up\n"); // 松开,等待按下
enter_wait_pen_down_mode();
}else{
enter_measure_xy_mode(); // 按下,进入自动测量模式,且开启ADC转换
start_adc();
}
return IRQ_HANDLED;
}
static int s3c_ts_init(void)
{
struct clk* clk;
/* 1 分配一个input_dev 结构体*/
s3c_ts_dev = input_allocate_device();
/* 2 设置input_dev */
set_bit(EV_KEY, s3c_ts_dev->evbit);
set_bit(EV_ABS, s3c_ts_dev->evbit);
input_set_abs_params(s3c_ts_dev, ABS_X, 0, 0x3FF, 0, 0);
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 使能ADC时钟 */
clk = clk_get(NULL, "adc");
clk_enable(clk);
/* 4.2 ioremap */
ioremap(0x58000000, sizeof(s3c_ts_regs));
/* bit[14] : 1-A/D converter prescaler enable
* bit[13:6]: A/D converter prescaler value,
* 49, ADCCLK=PCLK/(49+1)=50MHz/(49+1)=1MHz
* bit[0]: A/D conversion starts by enable. 先设为0
*/
s3c_ts_regs->adccon = (1<<14)|(49<<6);
/* 4.3 注册中断 */
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);
/* 4.4 进入等待按下模式 */
enter_wait_pen_down_mode();
return 0;
}
static void s3c_ts_exit(void)
{
free_irq(IRQ_TC, NULL);
iounmap(s3c_ts_regs);
input_unregister_device(s3c_ts_dev);
input_free_device(s3c_ts_dev);
}
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
程序执行流程:
触笔点击触发中断,进入自动测量xy坐标模式且开启ADC转换,待转换完成后,触发ADC中断,
打印xy坐标的“电压值”(注意不是屏幕的实际坐标值)并进入等待触笔松开模式。如果松开,触发触摸屏
中断,但只打印“pen up“, 然后进入等待触笔点击模式。
注意:触摸屏中断 与 ADC 中断 是不同的
套路:
1)申请一个input_dev结构体: input_allocate_device
2)设置input_dev(支持哪类事件、哪类事情的哪些事件)set_bit / input_set_abs_params
3) 在入口函数中注册设备驱动程序:input_register_device()
4)硬件相关操作:a. 使能ADC b.ioremap 即 设置ADC输入时钟源频率 c.注册中断 d.进入等待按键按下模式
5)编写硬件相关函数:1.触摸屏中断函数中判断按下/松开,并动作(这里只是打印,后面会启动ADC转换)
2.进入等待按键按下或松开模式的函数
6)在出口函数中注销设备驱动程序: 逆序释放(free_irq/iounmap/input_unregister_device/input_free_device)
7) module_init() / module_exit() --->实际将入口/出口函数放在内核初始化段,别忘了 MODULE_LICENSE("GPL")
2 完善一:电压稳定后再发出IRQ_TC中断
问题:上面代码经测试存在不稳定的电压值输出(如从左往右,y的电压值应该依次增大,但出现减小的一些值)
解决:待ADC转换输出的电压值稳定后再发出IRQ_TC中断,再启动测量
代码修改如下,其它不变
// 在s3c_ts.c 的 s3c_ts_init()入口函数增加
/* 优化错施1:
* 设置ADCDLY为最大值, 这使得电压稳定后再发出IRQ_TC中断
*/
s3c_ts_regs->adcdly = 0xffff;
/* 4.4 进入等待按下模式 */
enter_wait_pen_down_mode();
测试效果:
输出的电压值基本没有不正常的跳动值(已测试验证)
3 完善二:若触笔已经松开, 则丢弃此次测量结果
问题:除了异常值,发现触笔松开了也打印出一些电压值
解决:如果测量结束后(即在adr_irq中断处理里),如果触笔已松开,则丢弃这次测量结果
代码修改如下,其它不变
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
int adcdat0, adcdat1;
/* 优化措施2: 如果ADC完成时, 发现触摸笔已经松开, 则丢弃此次结果 */
adcdat0 = s3c_ts_regs->adcdat0;
adcdat1 = s3c_ts_regs->adcdat1;
if(s3c_ts_regs->adcdat0 & (1<<15))
{
enter_wait_pen_down_mode(); // 如果转换结束后,触笔已松开,不打印,只需进入等待点击模式即可
}else{
printk("adc_irq cnt = %d, x = %d, y = %d\n", ++cnt, s3c_ts_regs->adcdat0 & 0x3ff,
s3c_ts_regs->adcdat1 & 0x3ff);
enter_wait_pen_up_mode();
}
return IRQ_HANDLED;
}
测试效果:
触笔松开时,能够不打印电压值
4 完善三:同一点多次测量取平均值
问题:点击同一点时,电压值浮动稍微大。且靠近两点的电压值变化幅度有些会较大
解决:对于某一点测量,多次测量后取平均值,这样同一点的电压值变化不大,基本稳定。
代码修改如下,其它没变
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
static int x[4], y[4];
int adcdat0, adcdat1;
/* 优化措施2: 如果ADC完成时, 发现触摸笔已经松开, 则丢弃此次结果 */
adcdat0 = s3c_ts_regs->adcdat0;
adcdat1 = s3c_ts_regs->adcdat1;
if(s3c_ts_regs->adcdat0 & (1<<15))
{
cnt = 0;
enter_wait_pen_down_mode();
}else{
/* 优化措施3: 多次测量求平均值 */
x[cnt] = adcdat0 & 0x3ff;
y[cnt] = adcdat1 & 0x3ff;
++cnt;
if (cnt == 4) // 如果测量了4次,取平均值后再打印
{
printk("x = %d, y = %d\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();
}
else // 如果测量<4次,只需再进入测量即可
{
enter_measure_xy_mode();
start_adc();
}
}
return IRQ_HANDLED;
}
测试效果:
点击同一点,电压值稳定。且相邻点的电压值变化幅度小,稳定
5 完善四:软件过滤,去除异常电压值
优化:对于一次4次测量结果中,如果某些值变化较大,则丢弃此次的4次测量结果
怎样判断某些值变化较大呢?
具体来讲,就是如果测量的第3个值超出前两个测量的平均值10,或者,第4个值
超出前面两个测量的平均值10,则都认为此次测量结果不稳定,无效
备注1:10 只是个粗略值,可多次实验取一个较好的
备注2:上面的优化很粗糙,可参考tslib 的优化策略
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 adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
static int x[4], y[4];
int adcdat0, adcdat1;
/* 优化措施2: 如果ADC完成时, 发现触摸笔已经松开, 则丢弃此次结果 */
adcdat0 = s3c_ts_regs->adcdat0;
adcdat1 = s3c_ts_regs->adcdat1;
if(s3c_ts_regs->adcdat0 & (1<<15))
{
cnt = 0;
enter_wait_pen_down_mode();
}else{
/* 优化措施3: 多次测量求平均值 */
x[cnt] = adcdat0 & 0x3ff;
y[cnt] = adcdat1 & 0x3ff;
++cnt;
if (cnt == 4) // 如果测量了4次,取平均值后再打印
{
/* 优化措施4: 软件过滤 */
if(s3c_filter_ts(x, y))
{
printk("x = %d, y = %d\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();
}
else // 如果测量<4次,只需再进入测量即可
{
enter_measure_xy_mode();
start_adc();
}
}
return IRQ_HANDLED;
}
实验效果:
相比上次,发现相邻点的电压值变化幅度为1,即输出的电压值更可靠
6 完善五:支持长按、滑动测量(使用定时器)
问题:触笔点击某一个点并不会连续输出打印电压值,且滑动时不会连续打印电压值
解决:增加一个定时器,在触笔点击触发的中断函数里开启定时器,如果时间到了,
在定时器中断函数中判断是否还按下,如果是则继续测量并打印,滑动同理。
static void s3c_ts_timer_function(unsigned long data)
{
if (s3c_ts_regs->adcdat0 & (1<<15))
{
/* 已经松开, 等待按下 */
enter_wait_pen_down_mode();
}
else
{
/* 未松开,则继续测量X/Y坐标 */
enter_measure_xy_mode();
start_adc();
}
}
static irqreturn_t adc_irq(int irq, void *dev_id)
{
static int cnt = 0;
static int x[4], y[4];
int adcdat0, adcdat1;
/* 优化措施2: 如果ADC完成时, 发现触摸笔已经松开, 则丢弃此次结果 */
adcdat0 = s3c_ts_regs->adcdat0;
adcdat1 = s3c_ts_regs->adcdat1;
if(s3c_ts_regs->adcdat0 & (1<<15))
{
cnt = 0;
enter_wait_pen_down_mode();
}else{
/* 优化措施3: 多次测量求平均值 */
x[cnt] = adcdat0 & 0x3ff;
y[cnt] = adcdat1 & 0x3ff;
++cnt;
if (cnt == 4) // 如果测量了4次,取平均值后再打印
{
/* 优化措施4: 软件过滤 */
if(s3c_filter_ts(x, y))
{
printk("x = %d, y = %d\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();
/* 启动定时器处理长按/滑动的情况 */
mod_timer(&timer_ts, jiffies + HZ/100);
}
else // 如果测量<4次,只需再进入测量即可
{
enter_measure_xy_mode();
start_adc();
}
}
return IRQ_HANDLED;
}
static struct time_list timer_ts; // 定义一个定时器
static int s3c_ts_init(void)
{
/* .... */
init_timer(&timer_ts);
timer_ts.function = s3c_ts_timer_function;
add_timer(&timer_ts);
return 0;
}
测试效果:
长按和滑动时均有连续电压值输出,且电压值稳定变化
7 完善六:把打印改为上报事件,并用tslib测试
修改三处,代码如下
// 在触摸屏中断处理函数中,修改如下
/* 优化措施4: 软件过滤 */
static irqreturn_t adc_irq(int irq, void *dev_id)
{
/* ... */
if(s3c_filter_ts(x, y))
{
// 修改一:点击测量4次取平均值后的上报
input_report_abs(s3c_ts_dev, ABS_X, (x[0]+x[1]+x[2]+x[3])/4);
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);
input_report_key(s3c_ts_dev, BTN_TOUCH, 1);
input_sync(s3c_ts_dev); // 注意同步,表示上面上报的数据(x,y,压力值,按下)是一组数据
}
/* ... */
}
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);
input_report_key(s3c_ts_dev, BTN_TOUCH, 0);
input_sync(s3c_ts_dev);
enter_wait_pen_down_mode();
}else{
enter_measure_xy_mode();
start_adc();
}
return IRQ_HANDLED;
}
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);
input_report_key(s3c_ts_dev, BTN_TOUCH, 0);
input_sync(s3c_ts_dev);
enter_wait_pen_down_mode();
}
else
{
/* 为松开,则继续测量X/Y坐标 */
enter_measure_xy_mode();
start_adc();
}
}
下面是tslib库的安装与测试:
1)tslib的安装
a. 编译
tar xzf tslib-1.4.tar.gz
cd tslib
./autogen.sh
解决办法:
sudo apt-get install autoconf
sudo apt-get install automake
sudo apt-get install libtool
b. 安装
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
最终结果:
2) 将tslib下的tmp目录复杂到开发板的根文件系统目录下
cd tmp
cp * -rfd ~/hardware/first_fs(这个改成你自己的根文件系统目录)
注意:复制时使用rfd三个选项,r: 递归复制 f:force 强制复制 d:复制时保持符号链接
3)一些配置
1.修改 /etc/ts.conf第1行(去掉#号和第一个空格):
# module_raw input
改为:
module_raw input
2. 注意:下面的event0 要看插入触摸屏驱动模块后确定(ls -l /dev/event* ),是哪个事件,我这里是event0
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
备注:fb0是LCD设备,可使用内核提供的
4)测试
测试一:因为电阻屏都需要校验,故先测验 ts_calibrate以生成校验文件,如下图所示
测试二:ts_test
1.十字架跟着触笔走
2.绘图
---> 原谅字丑:)
然后其它的测试就不一 一细讲啦