本文来简单的分析一下Linux2.6.22.6内核自带的S3C2440的触摸屏驱动程序。
驱动程序在内核中的路径与文件名为:drivers\input\touchscreen\s3c2410_ts.c,这个文件的完整代码实现如下所示:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* For ts.dev.id.version */
#define S3C2410TSVERSION 0x0101
#define WAIT4INT(x) (((x)<<8) | \
S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
S3C2410_ADCTSC_XY_PST(3))
#define AUTOPST (S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | \
S3C2410_ADCTSC_AUTO_PST | S3C2410_ADCTSC_XY_PST(0))
#define DEBUG_LVL KERN_DEBUG
MODULE_AUTHOR("Arnaud Patard ");
MODULE_DESCRIPTION("s3c2410 touchscreen driver");
MODULE_LICENSE("GPL");
/*
* Definitions & global arrays.
*/
//static volatile int bADCForTS;
//struct semaphore gADClock;
//EXPORT_SYMBOL(gADClock);
static char *s3c2410ts_name = "s3c2410 TouchScreen";
/*
* Per-touchscreen data.
*/
struct s3c2410ts {
struct input_dev *dev;
long xp;
long yp;
int count;
int shift;
};
static struct s3c2410ts ts;
static void __iomem *base_addr;
static inline void s3c2410_ts_connect(void)
{
s3c2410_gpio_cfgpin(S3C2410_GPG12, S3C2410_GPG12_XMON);
s3c2410_gpio_cfgpin(S3C2410_GPG13, S3C2410_GPG13_nXPON);
s3c2410_gpio_cfgpin(S3C2410_GPG14, S3C2410_GPG14_YMON);
s3c2410_gpio_cfgpin(S3C2410_GPG15, S3C2410_GPG15_nYPON);
}
static void touch_timer_fire(unsigned long data)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
if (updown) {
if (ts.count != 0) {
long tmp; /* add by www.100ask.net */
tmp = ts.xp;
ts.xp = ts.yp;
ts.yp = tmp;
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);
}
#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;
ts.yp = 0;
ts.count = 0;
// if (!down_trylock(&gADClock)) {
// bADCForTS = 1;
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 {
ts.count = 0;
input_report_key(ts.dev, BTN_TOUCH, 0);
input_report_abs(ts.dev, ABS_PRESSURE, 0);
input_sync(ts.dev);
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
}
}
static struct timer_list touch_timer =
TIMER_INITIALIZER(touch_timer_fire, 0, 0);
static irqreturn_t stylus_updown(int irq, void *dev_id)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_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(0);
return IRQ_HANDLED;
}
static irqreturn_t stylus_action(int irq, void *dev_id)
{
unsigned long data0;
unsigned long data1;
// if (bADCForTS) {
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
ts.xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;
ts.yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;
ts.count++;
// bADCForTS = 0;
// up(&gADClock);
if (ts.count < (1<dev.platform_data;
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");
if (!adc_clock) {
printk(KERN_ERR "failed to get adc clock source\n");
return -ENOENT;
}
clk_enable(adc_clock);
#ifdef CONFIG_TOUCHSCREEN_S3C2410_DEBUG
printk(DEBUG_LVL "got and enabled clock\n");
#endif
base_addr=ioremap(S3C2410_PA_ADC,0x20);
if (base_addr == NULL) {
printk(KERN_ERR "Failed to remap register block\n");
return -ENOMEM;
}
/* Configure GPIOs */
s3c2410_ts_connect();
if ((info->presc&0xff) > 0) // 对ADC进行时钟分频
iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(info->presc&0xFF),\
base_addr+S3C2410_ADCCON);
else
iowrite32(0,base_addr+S3C2410_ADCCON);
/* Initialise registers */
if ((info->delay&0xffff) > 0) // 设置ADC开始转换的延时
iowrite32(info->delay & 0xffff, base_addr+S3C2410_ADCDLY);
// 等待触摸屏按下
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
/* Initialise input stuff */
memset(&ts, 0, sizeof(struct s3c2410ts));
input_dev = input_allocate_device();
if (!input_dev) {
printk(KERN_ERR "Unable to allocate the input device !!\n");
return -ENOMEM;
}
ts.dev = input_dev;
ts.dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS);
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);
ts.dev->private = &ts;
ts.dev->name = s3c2410ts_name;
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, IRQF_SAMPLE_RANDOM | SA_SHIRQ,
"s3c2410_action", ts.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,
"s3c2410_action", ts.dev)) {
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;
}
static int s3c2410ts_remove(struct platform_device *pdev)
{
disable_irq(IRQ_ADC);
disable_irq(IRQ_TC);
free_irq(IRQ_TC,ts.dev);
free_irq(IRQ_ADC,ts.dev);
if (adc_clock) {
clk_disable(adc_clock);
clk_put(adc_clock);
adc_clock = NULL;
}
input_unregister_device(ts.dev);
iounmap(base_addr);
return 0;
}
static struct platform_driver s3c2410ts_driver = {
.driver = {
.name = "s3c2410-ts",
.owner = THIS_MODULE,
},
.probe = s3c2410ts_probe,
.remove = s3c2410ts_remove,
};
static int __init s3c2410ts_init(void)
{
// init_MUTEX(&gADClock);
return platform_driver_register(&s3c2410ts_driver);
}
static void __exit s3c2410ts_exit(void)
{
platform_driver_unregister(&s3c2410ts_driver);
}
module_init(s3c2410ts_init);
module_exit(s3c2410ts_exit);
1、平台、总线、设备驱动模型
2、Linux输入子系统和硬件相关
3、触摸屏和ADC中断事件的处理
下面按照上面提出的三个部分逐渐分析:
1、平台、总线、设备驱动模型
看一个驱动程序,一般从入口和出口的地方开始看,本驱动的入口函数如下所示:
static int __init s3c2410ts_init(void)
{
// init_MUTEX(&gADClock);
return platform_driver_register(&s3c2410ts_driver);
}
从里面可以看出它注册了一个平台驱动,这个平台驱动的原型如下所示:
static struct platform_driver s3c2410ts_driver = {
.driver = {
.name = "s3c2410-ts",
.owner = THIS_MODULE,
},
.probe = s3c2410ts_probe,
.remove = s3c2410ts_remove,
};
有了这个平台驱动,要想让这个驱动程序能够发挥作用,必须定义一个同名的平台设备,经过查找可以看出,平台设备定义在arch\arm\plat-s3c24xx\devs.c这个文件中,具体实现如下:
/* Touchscreen */
struct platform_device s3c_device_ts = {
.name = "s3c2410-ts",
.id = -1,
};
EXPORT_SYMBOL(s3c_device_ts);
static struct s3c2410_ts_mach_info s3c2410ts_info;
void __init set_s3c2410ts_info(struct s3c2410_ts_mach_info *hard_s3c2410ts_info)
{
memcpy(&s3c2410ts_info, hard_s3c2410ts_info, sizeof(struct s3c2410_ts_mach_info));
s3c_device_ts.dev.platform_data = &s3c2410ts_info;
}
从上面可以看出,除了定义一个平台设备之外,还定义了一个设置平台设备平台数据的函数,这个函数在arch\arm\plat-s3c24xx\common-smdk.c这个文件当中被调用,添加的这个平台数据的内容如下所示:
static struct s3c2410_ts_mach_info s3c2410_ts_cfg = {
.delay = 10000,
.presc = 49,
.oversampling_shift = 2,
};
这个结构体当中,主要包括三个数据成员,delay主要用来表示每次ADC转换时的延迟时间,presc表示ADC的分频系数,oversampling_shift表示采样时优化采样值。
从平台设备驱动模型可以知道整个驱动程序匹配完成后将调用平台驱动的probe函数,这个函数的原型为:s3c2410ts_probe(),它里面主要完成了两部分工作:输入子系统的实现和硬件相关的操作:
2.1 输入子系统的具体实现如下
a、分配一个input_dev的结构体指针变量
input_dev = input_allocate_device();
b、设置这个结构体指针变量
ts.dev = input_dev;
ts.dev->evbit[0] = BIT(EV_SYN) | BIT(EV_KEY) | BIT(EV_ABS); // 设置支持同步类、按键类、绝对位移类事件
ts.dev->keybit[LONG(BTN_TOUCH)] = BIT(BTN_TOUCH); // 支持触摸按键事件
input_set_abs_params(ts.dev, ABS_X, 0, 0x3FF, 0, 0); // 支持X方向绝对位移事件
input_set_abs_params(ts.dev, ABS_Y, 0, 0x3FF, 0, 0); // 支持Y方向绝对位移事件
input_set_abs_params(ts.dev, ABS_PRESSURE, 0, 1, 0, 0); // 支持压力的绝对位移事件
ts.dev->private = &ts; // 进行其它相关的设置
ts.dev->name = s3c2410ts_name;
ts.dev->id.bustype = BUS_RS232;
ts.dev->id.vendor = 0xDEAD;
ts.dev->id.product = 0xBEEF;
ts.dev->id.version = S3C2410TSVERSION;
c、注册这个结构体指针变量
input_register_device(ts.dev);
以上几步实现了触摸屏驱动程序的输入子系统化,完成了以上几步就可以实现当发生触摸相关的事件时直接提交相关输入事件就可以了。
在了解具体的硬件操作之前,先要看看涉及到那些寄存器。
电源管理相关的寄存器,因为Linux内核在上电时把ADC的时钟给关闭了,所以需要重新打开时钟,这个寄存器的主要内容如下所示:
ADC相关的寄存器:
具体的硬件操作如下:
a、使能ADC的时钟,如下所示:
adc_clock = clk_get(NULL, "adc");
if (!adc_clock) {
printk(KERN_ERR "failed to get adc clock source\n");
return -ENOENT;
}
clk_enable(adc_clock);
这部分代码的具体实现就是将CLKCON寄存器的bit15设为1.
b、映射IO操作的寄存器
base_addr=ioremap(S3C2410_PA_ADC,0x20);
将ADC的物理地址映射为Linux当中可以访问的虚拟地址。
/* Configure GPIOs */
s3c2410_ts_connect(); // 配置触摸屏和S3C2440连接的GPIO引脚
if ((info->presc&0xff) > 0) // 对ADC进行时钟分频,分频系数来自平台设备传入的平台数据
iowrite32(S3C2410_ADCCON_PRSCEN | S3C2410_ADCCON_PRSCVL(info->presc&0xFF),\
base_addr+S3C2410_ADCCON);
else
iowrite32(0,base_addr+S3C2410_ADCCON);
/* Initialise registers */
if ((info->delay&0xffff) > 0) // 设置ADC开始转换的延时
iowrite32(info->delay & 0xffff, base_addr+S3C2410_ADCDLY);
// 等待触摸屏按下
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
上面这些代码的主要功能是:首先配置和触摸屏相连的GPIO引脚;然后对ADC进行时钟分频,分频后ADC的时钟为1MHz;接下来为了减小触摸值的误差,设置了以下ADC的转换延时,最后等待触摸屏按下的操作。
/* Get irqs */
if (request_irq(IRQ_ADC, stylus_action, IRQF_SAMPLE_RANDOM | SA_SHIRQ,
"s3c2410_action", ts.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,
"s3c2410_action", ts.dev)) {
printk(KERN_ERR "s3c2410_ts.c: Could not allocate ts IRQ_TC !\n");
iounmap(base_addr);
return -EIO;
}
实现了这两个中断的基本作用是:当触摸屏按下时会调用触摸屏响应中断函数stylus_updown(),这是会启动ADC转换,当ADC转换完毕后要会启动ADC转换中断,通过ADC转换中断把触摸值通过输入子系统提交。
a、和触摸屏中断相关的处理函数,实现如下:
static irqreturn_t stylus_updown(int irq, void *dev_id)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_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;
}
static void touch_timer_fire(unsigned long data)
{
unsigned long data0;
unsigned long data1;
int updown;
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
updown = (!(data0 & S3C2410_ADCDAT0_UPDOWN)) && (!(data1 & S3C2410_ADCDAT0_UPDOWN));
/* 判断触摸屏是按下还是松开 */
if (updown) { // 触摸屏是按下
if (ts.count != 0) { // 如果count值不为0,则将触摸按下事件提交,如果为0,则启动ADC并进入测量模式
long tmp; /* add by www.100ask.net */
tmp = ts.xp;
ts.xp = ts.yp;
ts.yp = tmp;
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);
}
#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;
ts.yp = 0;
ts.count = 0;
// if (!down_trylock(&gADClock)) {
// bADCForTS = 1;
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 {
ts.count = 0;
input_report_key(ts.dev, BTN_TOUCH, 0);
input_report_abs(ts.dev, ABS_PRESSURE, 0);
input_sync(ts.dev);
iowrite32(WAIT4INT(0), base_addr+S3C2410_ADCTSC);
}
}
static irqreturn_t stylus_action(int irq, void *dev_id)
{
unsigned long data0;
unsigned long data1;
// if (bADCForTS) {
data0 = ioread32(base_addr+S3C2410_ADCDAT0);
data1 = ioread32(base_addr+S3C2410_ADCDAT1);
ts.xp += data0 & S3C2410_ADCDAT0_XPDATA_MASK;
ts.yp += data1 & S3C2410_ADCDAT1_YPDATA_MASK;
ts.count++;
// bADCForTS = 0;
// up(&gADClock);
/* 对ADC获得的值进行过滤优化,优化参数来自平台设备的设备数据成员 */
if (ts.count < (1< // 如果优化完毕则重新设置定时器的超时时间,并设置触摸屏为等待松开模式
mod_timer(&touch_timer, jiffies+1);
iowrite32(WAIT4INT(1), base_addr+S3C2410_ADCTSC);
}
// }
return IRQ_HANDLED;
}
整个过程大致分析如上面所示,由于本人能力有限以及行文仓促,文中难免有错误和不足的地方,还请见谅和指教。