《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动

时间:2022-04-30 10:26:15

13.1 触摸屏简介


触摸屏( touch screen)又称为“触控屏”、“触控面板”,是一种可接收触头等输入讯号的感应式液晶显示装置,当接触了屏幕上的图形按钮时,屏幕上的触觉反馈系统可根据预先编程的程式驱动各种连结装置,可用以取代机械式的按钮面板,并借由液晶显示画面制造出生动的影音效果.使用最广最多的有下面的两种触摸屏(资料来自百度):

电阻式触摸屏

这种触摸屏利用压力感应进行控制触摸屏触摸屏.电阻触摸屏的主要部分是一块与显示器表面非常配合的电阻薄膜屏,这是一种多层的复合薄膜,它以一层玻璃或硬塑料平板作为基层,表面涂有一层透明氧化金属(透明的导电电阻)导电层,上面再盖有一层外表面硬化处理、光滑防擦的塑料层、它的内表面也涂有一层涂层、在他们之间有许多细小的(小于 1/1000 英寸)的透明隔离点把两层导电层隔开绝缘。当手指触摸屏幕时,两层导电层在触摸点位置就有了接触,电阻发生变化,在 X 和 Y 两个方向上产生信号,然后送触摸屏控制器。控制器侦测到这一接触并计算出( X, Y)的位置,再根据模拟鼠标的方式运作。这就是电阻技术触摸屏的最基本的原理.一般驱动这种屏幕需要使用到处理器的 AD 引脚进行模式转化.电阻触摸屏的精度只取决于 A/D 转换的精度.

电容式触摸屏

是利用人体的电流感应进行工作的。电容式触摸屏是一块四层复合玻璃屏,玻璃屏的内表面和夹层各涂有一层 ITO,最外层是一薄层矽土玻璃保护层,夹层ITO 涂层作为工作面,四个角上引出四个电极,内层 ITO 为屏蔽层以保证良好的工作环境。当手指触摸在金属层上时,由于人体电场,用户和触摸屏表面形成以一个耦合电容,对于高频电流来说,电容是直接导体,于是手指从接触点吸走一个很小的电流。这个电流分从触摸屏的四角上的电极中流出,并且流经这四个电极的电流与手指到四角的距离成正比,控制器通过对这四个电流比例的精确计算,得出触摸点的位置.电容屏的缺点是用戴手套的手或手持不导电的物体触摸时没有反应,这是因为增加了更为绝缘的介质 .电容触摸屏采用的这种四个角的自定义极坐标系还没有坐标上的原点,漂移后控制器不能察觉和恢复,而且, 4 个A/D 完成后,由四个分流量的值到触摸点在直角坐标系上的 X、 Y 坐标值的计算过程复杂。由于没有原点,电容屏的漂移是累积的,在工作现场也经常需要校准。电容触摸屏最外面的矽土保护玻璃防刮擦性很好,但是怕指甲或硬物的敲击,敲出一个小洞就会伤及夹层 ITO,不管是伤及夹层 ITO 还是安装运输过程中伤及内表面 ITO 层,电容屏就不能正常工作了。

压电式触摸屏

电阻式设计简单,成本低,但电阻式触控较受制于其物理局限性,如透光率较低,高线数的大侦测面积造成处理器负担,其应用特性使之易老化从而影响使用寿命。电容式触控支持多点触控功能,拥有更高的透光率、更低的整体功耗,其接触面硬度高,无需按压,使用寿命较长,但精准度不足,不支持手写笔操控。于是衍生了压电式触摸屏。 压电式触控技术介于电阻式与电容式触控技术之间。压电式传感器的触控屏幕同电容式触控屏一样支持多点触控,而且支持任何物体触控,不像电容屏只支持类皮肤的材质触控。这样,压电式触控屏幕可以同时具有电容屏幕的多点触控触感,又具有电阻屏的精准。 压电式触控在耗电特性上更接近电容式触控特性,即没有触摸的动作,就不产生耗电,而电阻式则时刻产生耗电。在接口支持上,压电式触控也同样支持串口、 I2C 和 USB 接口。从工艺成本上看,电阻式触控制程转到压电式触控制程需要变更生产线设备,而同电容式的 ITO 和掩模结合的制程相比,压电式触控制程成本约在其 80-90%之间。压电触摸屏的工作原理相当于 TFT,制造工艺部分像电容式触摸屏,物理结构又像电阻式触摸屏,是三种成熟技术的揉和。所以采用新技术的压电式触摸屏集合并增强了电阻式和电容式的优点,又避免了二者的缺点。压电触摸屏一般为硬塑料平板(或有机玻璃)底材多层复合膜,硬塑料平板(或有机玻璃)作为基层,表面涂有一层透明的导电层,上面再盖有一层外表面经过硬化处理、光滑防刮的塑料层,它的表面也涂有一层透明的导电层,在两层导电层之间有许多细小的透明隔离点。屏体的透光度略低于玻璃。 压电式触摸屏的代表作是智器 Ten(即T10),压电式 IPS 硬屏,近乎达到了 iPad 同级的显示效果和触控体验,同时成本更低,表现非常不错。

网蜂采用的是带 i2c 接口的 i2c 电容触摸屏。 webee210 采用的是 FT5X06 的电容式触摸屏驱动芯片。他以下特性:
《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动转换
最大支持 8'9 寸
支持多点触摸,最多 10 点

具有 i2c 和 SPI 两种数据输出接口


注意:现在 webee210 的屏幕有两种,一种是 FT5206,一种是 FT5306. FT5306因为停产的原因,所以之后我们将采用 FT5206 的。他们的区别在于触摸分辨率
和 Y 坐标 FT5206 和 FT5306 是相反的。

《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动

13.2 linux i2c 子系统


13.2.1 linux i2c 子系统简介


我们的触摸屏采用的是 ft5x06 的 i2c 接口,所以我需要使用到 linux 的 i2c 设备驱动。先来看看 linux 的 i2c 模型是怎么回事。图 13.2.1 是 linux i2c 系统框架图:
《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动

Linux 的 I2C 驱动框架属于典型的总线-驱动-设备模型。 所以要看懂这部分的内容,请补一下关于这个 linux 驱动模型的相关知识。 Linux 的 I2C 驱动框架中的主要数据结构包括: i2c_driver、 i2c_client、 i2c_adapter 和 i2c_algorithm。i2c_adapter 对应于物理上的一个适配器(i2c_adaptor),这个适配器是基于不同的平台的,一个 I2C 适配器需要 i2c_algorithm 中提供的通信函数来控制适配器,因此 i2c_adapter 中包含其使用的 i2c_algorithm 的指针。 i2c_algorithm 中的关键函数 master_xfer()以 i2c_msg 为单位产生 I2C 访问需要的信号。不同的平台所对应的 master_xfer()是不同的,开发人员需要根据所用平台的硬件特性实现自己的 XXX_xfer()方法以填充 i2c_algorithm 的 master_xfer 指针。 i2c driver 对应一套驱动方法,不对应于任何的物理实体。 i2c_client 对应于真实的物理设备,每个 I2C 设备都需要一个 i2c_client 来描述。 i2c_client 依附于 i2c_adpater,这与 I2C 硬件体系中适配器和设备的关系一致。 i2c_driver 提供了 i2c-client 与i2c-adapter 产生联系的函数。

13.2.2 linux i2c-core


类似前面的 USB, MTD 子设备系统, linux 的 i2c 系统也有一个 core,用来实现关键的函数和结构体。这个 i2c-core的重要函数全在 driver/i2c/i2c-core.c下。I2C 核心提供了一组不依赖于硬件平台的接口函数, I2C 总线驱动和设备驱动之间依赖于 I2C 核心作为纽带。 I2C 核心提供了 i2c_adapter 的增加和删除函数、i2c_driver 的增加和删除函数、 i2c_client 的依附和脱离函数以及 i2c 传输、发送和接收函数。


通过 postcore_initcall(i2c_init)(这是内核的初始化标量,这些宏定义在文件include/linux/init.h 中) i2c 总线将被注册。这也是整个 i2c 系统的入口。不过我们不需要去管他,我们先来看看这个 i2c 核心的一些重要结构体和函数。

1. i2c adapter i2c algorithm 结构体


i2c_adapter 对应于物理上的一个适配器, i2c_algorithm 对应一套通信方法。一个适配器需要 i2c_algorithm 提供的通信函数来控制适配器上产生特定的访问周期和传输协议,缺少 i2c_algorithm, i2c_adapter 啥也做不了。 i2c_adapter包含一个 i2c_algorithm 指针。而 i2c_algorithm 中包含一个指向 i2c_adapter 的指针。

/* struct i2c_adapter 代表主芯片所支持的一个 i2c 主设备 */
struct i2c_adapter {
struct module *owner;
unsigned int class; /* classes to allow probing for */
const struct i2c_algorithm *algo;/*该 i2c 主设备传输数据的一种算法*/
void *algo_data;
/*互斥锁,用于加锁数据区域*/
struct rt_mutex bus_lock;
int timeout; /* in jiffies */
int retries;
struct device dev; /* the adapter device */
int nr; /*该成员描述了总线号*/
char name[48]; /*设备名*/
struct completion dev_released;
struct mutex userspace_clients_lock;
/* i2c_client 结构链表,该结构包含 device, driver 和 adapter 结构*/

struct list_head userspace_clients;
};
/*
* The following structs are for those who like to implement new bus drivers:
* i2c_algorithm is the interface to a class of hardware solutions which can
* be addressed using the same bus algorithms - i.e. bit-banging or the
PCF8584
* to name two of the most common.
*/
struct i2c_algorithm {
/* If an adapter algorithm can't do I2C-level access, set master_xfer to NULL. If an adapter algorithm can do SMBus access, set smbus_xfer. If set to NULL, the SMBus protocol is simulated using common I2C messages */
/* master_xfer should return the number of messages successfully processed, or a negative value on error */
/*用于产生 i2c 访问周期需要的信号*/
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);
/*指针指向 i2c 适配器驱动程序模块实现的 i2c 通信协议或者 smbus 通信
协议*/
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,unsigned short flags, char read_write,u8 command, int size, union i2c_smbus_data *data);
/* To determine what the adapter supports */
u32 (*functionality) (struct i2c_adapter *);
};


2. i2c_driver i2c_client 结构体


i2c_driver 对应一套驱动方法,纯粹用于辅助作用的数据结构,不对应任何物理实体, i2c_client 对应于真实的物理设备(外设),每个 i2c 设备都用一个i2c_client 描述,一般被包含在具体字符设备的私有信息结构体中。i2c_adpater 与 i2c_client 的关系与 i2c 硬件体系中的适配器与设备的关系一致,即 i2c_client 依赖于 i2c_adpater,一个适配器上可连接多个 I2C 设备,所以 i2c_adpater 中包括依赖于它的 i2c_client 的链表。

struct i2c_client {
unsigned short flags; //标志
unsigned short addr; //低 7 位为芯片地址
char name[I2C_NAME_SIZE]; //设备名称
struct i2c_adapter *adapter; //依附的 i2c_adapter
struct i2c_driver *driver; //依附的 i2c_driver
struct device dev; //设备结构体
int irq; //设备所要使用的中断号
struct list_head detected; //链表头
};


struct i2c_driver {
/*挂载的 i2c 设备类型(for detect)*/
unsigned int class;
/*挂载到 adapter 和从 adapter 上卸载的函数*/
int (*attach_adapter)(struct i2c_adapter *) __deprecated;
int (*detach_adapter)(struct i2c_adapter *) __deprecated;
/* 设备的 probe 和 remove 函数*/
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
/* 关闭,休眠,唤醒设备函数,与系统相关 */
void (*shutdown)(struct i2c_client *);
int (*suspend)(struct i2c_client *, pm_message_t mesg);
int (*resume)(struct i2c_client *);
/*SMBus 警告函数*/
void (*alert)(struct i2c_client *, unsigned int data);
/* ioctrl 传送控制命令*/
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
/*设备驱动模型结构体*/
struct device_driver driver;
/*设备 id 列表,根据名字用于探测挂载*/
const struct i2c_device_id *id_table;
/*探测挂载函数*/
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
/*i2c clients 链表*/
struct list_head clients;
};


3. 一些重要函数

/*增加/删除 i2c_adapter*/
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_del_adapter(struct i2c_adapter *adap)
/*增加/删除 i2c_driver*/
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
/*i2c_client 依附/脱离*/
int i2c_attach_client(struct i2c_client *client)
/*增加/删除 i2c_driver*/
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
/*i2c_client 依附/脱离*/
int i2c_attach_client(struct i2c_client *client)
int i2c_detach_client(struct i2c_client *client)
/*I2C 传输,发送和接收*/
int i2c_master_send(struct i2c_client *client,const char *buf ,int count)
int i2c_master_recv(struct i2c_client *client, char *buf ,int count)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)


13.2.3 linux i2c 总线驱动和 linux i2c 设备驱动


Linux i2c 系统可以分为下面四层:
第一层:提供 i2c adapter 的硬件驱动,探测、初始化 i2c adapter(如申请i2c的 io地址和中断号),驱动 soc控制的 i2c adapter在硬件上产生信号( start、stop、 ack)以及处理 i2c 中断。覆盖图中的硬件实现层
第二层:提供 i2c adapter 的 algorithm,用具体适配器的 xxx_xferf()函数来填充 i2c_algorithm 的 master_xfer 函数指针,并把赋值后的 i2c_algorithm再赋值给 i2c_adapter 的 algo 指针。
第三层:实现 i2c 设备驱动中的 i2c_driver 接口,用具体的 i2c device 设备的 attach_adapter()、 detach_adapter()方法赋值给 i2c_driver 的成员函数指针。实现设备 device 与总线(或者叫 adapter)的挂接。
第四层:实现 i2c 设备所对应的具体 device 的驱动, i2c_driver 只是实现设备与总线的挂接,而挂接在总线上的设备则是千差万别的,所以要实现具体设备 device 的 write()、 read()、 ioctl()等方法,赋值给 file_operations,然后注册字符设备(多数是字符设备)。


一般第一、二层由 i2c 总线驱动完成,一般不用写在目录 drivers/i2c/busses 下面可以找得到。而第三,四由需要根据使用的 i2c 设备编写完成,一般都要自己写。如我们的触摸屏驱动,在目录 drivers/input/touchscreen。


整个 linux i2c 系统最难得部分在于设备是如何被检测到,然后被挂载,找到对应的驱动程序。这个部分等我们做完后面的实验后,有一些亲身体会后再去看。那我们先看下一个部分的内容吧。

13.3 动手编写 linux i2c 驱动


13.3.1 linux i2c 总线驱动


其实在 linux3.8 里 drivers/i2c/busses 目录下有 i2c-s3c2410.c,他支持s3c2410,s3c6410,s5pc110 等 Samsung 系列的芯片多种芯片的 i2c。经过测试,他是支持 s5pv210 的 i2c 接口,所以这个总线驱动是可以用的。你可以不用自己写。只要在编译内核的时候将其打开即可,或是自己编译挂载。现在我们写一个精简版的总线驱动,只因为比较容易理解。如果要写一个稳定的 i2c 的总线驱动还是要参考 i2c-s3c2410.c 的架构来写。


下面是 webee210 的 LCD 接口图, i2c 接口触摸屏就是接到 Xi2CSCL2 和Xi2cSDL2.的 i2c 接口的。这里还要用到一个外部中断口, XEINT14,用于做触摸屏的事件触发响应,这个后面会用到,这里要先记住。
《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动

打开我们的简单版 i2c 总线驱动,这部分的内容需要 i2c 裸机部分的知识基础,所以关于 i2c 接口的内容不再说了。

《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动

......
struct webee210_i2c_xfer_data {
struct i2c_msg *msgs;
int msn_num;
int cur_msg;
int cur_ptr;
int state;
int err;
wait_queue_head_t wait;
};
static struct webee210_i2c_xfer_data webee210_i2c_xfer_data;
static struct webee210_i2c_regs *webee210_i2c_regs;
static unsigned long *gpd1con;
static unsigned long *gpd1pud;
static unsigned long *clk_gate_ip3;
static void webee210_i2c_start(void)
{
webee210_i2c_xfer_data.state = STATE_START;
/* 读 */
if (webee210_i2c_xfer_data.msgs->flags & I2C_M_RD)
{
webee210_i2c_regs->iicds =
webee210_i2c_xfer_data.msgs->addr << 1;
printk("slave = %x\n",webee210_i2c_xfer_data.msgs->addr);
/* 主机接收,启动*/
webee210_i2c_regs->iicstat = 0xb0;
}
/* 写 */
else
{
webee210_i2c_regs->iicds =
webee210_i2c_xfer_data.msgs->addr << 1;
//webee210_i2c_regs->iicds = webee210_i2c_xfer_data.msgs->addr;
/* 主机发送,启动 */
webee210_i2c_regs->iicstat = 0xf0;
}
}



static void webee210_i2c_stop(int err)
{
webee210_i2c_xfer_data.state = STATE_STOP;
webee210_i2c_xfer_data.err = err;
PRINTK("STATE_STOP, err = %d\n", err);
if (webee210_i2c_xfer_data.msgs->flags & I2C_M_RD) /* 读 */
{
// 下面两行恢复 I2C 操作,发出 P 信号
webee210_i2c_regs->iicstat = 0x90;
webee210_i2c_regs->iiccon = 0xaf;
// 等待一段时间以便 P 信号已经发出
ndelay(50);
}
else /* 写 */
{
// 下面两行用来恢复 I2C 操作,发出 P 信号
webee210_i2c_regs->iicstat = 0xd0;
webee210_i2c_regs->iiccon = 0xaf;
// 等待一段时间以便 P 信号已经发出
ndelay(50);
}
/* 唤醒 */
wake_up(&webee210_i2c_xfer_data.wait);

}


static int webee210_i2c_xfer(struct i2c_adapter *adap,struct i2c_msg *msgs, int num)
{
unsigned long timeout;
/* 把 num 个 msg 的 I2C 数据发送出去/读进来 */
webee210_i2c_xfer_data.msgs = msgs;
webee210_i2c_xfer_data.msn_num = num;
webee210_i2c_xfer_data.cur_msg = 0;
webee210_i2c_xfer_data.cur_ptr = 0;
webee210_i2c_xfer_data.err = -ENODEV;
// printk("...............................................\n");
PRINTK("s3c2440_i2c_xfer \n");

webee210_i2c_start();
/* 休眠 */
timeout = wait_event_timeout(webee210_i2c_xfer_data.wait,(webee210_i2c_xfer_data.state == STATE_STOP), HZ * 5);
if (0 == timeout)
{
printk("webee210_i2c_xfer time out\n");
return -ETIMEDOUT;
//return webee210_i2c_xfer_data.err;
}
else
{
return webee210_i2c_xfer_data.err;
}
}
static u32 webee210_i2c_func(struct i2c_adapter *adap)
{
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_PROTOCOL_MANGLING;
}
static const struct i2c_algorithm webee210_i2c_algo = {
// .smbus_xfer = ,
.master_xfer = webee210_i2c_xfer,
.functionality = webee210_i2c_func,
};
/* 1. 分配/设置 i2c_adapter
*/
static struct i2c_adapter webee210_i2c_adapter = {
.name = "webee210_100ask",
.algo = &webee210_i2c_algo,
.owner = THIS_MODULE,
};
static int isLastMsg(void)
{
return (webee210_i2c_xfer_data.cur_msg == webee210_i2c_xfer_data.msn_num - 1);
}


static int isEndData(void)
{
return (webee210_i2c_xfer_data.cur_ptr >= webee210_i2c_xfer_data.msgs->len);
}
static int isLastData(void)
{
return (webee210_i2c_xfer_data.cur_ptr == webee210_i2c_xfer_data.msgs->len - 1);
}
static irqreturn_t webee210_i2c_xfer_irq(int irq, void *dev_id)
{
unsigned int iicSt;
iicSt = webee210_i2c_regs->iicstat;
if(iicSt & 0x8){ printk("Bus arbitration failed\n\r"); }
PRINTK("webee210_i2c_xfer_irq \n");
switch (webee210_i2c_xfer_data.state)
{
case STATE_START : /* 发出 S 和设备地址后,产生中断 */
{
PRINTK("Start\n");
/* 如果没有 ACK, 返回错误 */
if (iicSt & S3C2410_IICSTAT_LASTBIT)
{
webee210_i2c_stop(-ENODEV);
break;
}
if (isLastMsg() && isEndData())
{
webee210_i2c_stop(0);
break;
}
/* 进入下一个状态 */
if (webee210_i2c_xfer_data.msgs->flags & I2C_M_RD) /* 读 */
{
webee210_i2c_xfer_data.state = STATE_READ;

goto next_read;
}
else
{
webee210_i2c_xfer_data.state = STATE_WRITE;
}
}
case STATE_WRITE:
{
PRINTK("STATE_WRITE\n");
/* 如果没有 ACK, 返回错误 */
if (iicSt & S3C2410_IICSTAT_LASTBIT)
{
webee210_i2c_stop(-ENODEV);
break;
}
/* 如果当前 msg 还有数据要发送 */
if (!isEndData())
{
webee210_i2c_regs->iicds =webee210_i2c_xfer_data.msgs->buf[webee210_i2c_xfer_data.cur_ptr];
webee210_i2c_xfer_data.cur_ptr++;
// 将数据写入 IICDS 后,需要一段时间才能出现在 SDA 线上
ndelay(50);
// 恢复 I2C 传输
webee210_i2c_regs->iiccon = 0xaf;
break;
}
else if (!isLastMsg())
{
/* 开始处理下一个消息 */
webee210_i2c_xfer_data.msgs++;
webee210_i2c_xfer_data.cur_msg++;
webee210_i2c_xfer_data.cur_ptr = 0;
webee210_i2c_xfer_data.state = STATE_START;
/* 发出 START 信号和发出设备地址 */
webee210_i2c_start();
break;
}
else
{
/* 是最后一个消息的最后一个数据 */

webee210_i2c_stop(0);
break;
}
break;
}
case STATE_READ:
{
PRINTK("STATE_READ\n");
/* 读出数据 */
webee210_i2c_xfer_data.msgs->buf[webee210_i2c_xfer_data.cur_ptr] =webee210_i2c_regs->iicds;
webee210_i2c_xfer_data.cur_ptr++;
next_read:
/* 如果数据没读写, 继续发起读操作 */
if (!isEndData())
{
/* 如果即将读的数据是最后一个, 不发 ack */
if (isLastData()) {
webee210_i2c_regs->iiccon = 0x2f; // 恢复 I2C 传输,
接收到下一数据时无 ACK
}
else
{
webee210_i2c_regs->iiccon = 0xaf; // 恢复 I2C 传输,
接收到下一数据时发出 ACK
}
break;
}
else if (!isLastMsg())
{
/* 开始处理下一个消息 */
webee210_i2c_xfer_data.msgs++;
webee210_i2c_xfer_data.cur_msg++;
webee210_i2c_xfer_data.cur_ptr = 0;
webee210_i2c_xfer_data.state = STATE_START;
/* 发出 START 信号和发出设备地址 */
webee210_i2c_start();
break;
}
else
{

/* 是最后一个消息的最后一个数据 */
webee210_i2c_stop(0);
break;
}
break;
}
default: break;
}
/* 清中断 */
webee210_i2c_regs->iiccon &= ~(S3C2410_IICCON_IRQPEND);
return IRQ_HANDLED;
}
/*
* I2C 初始化
*/
static void webee210_i2c_init(void)
{
/*使能时钟*/
*clk_gate_ip3 = 0xffffffff;
// 选择引脚功能: GPE15:IICSDA, GPE14:IICSCL
*gpd1con |= 0x22<<16;
*gpd1pud |= 0x5<<8;
// GPH1CON |= 0xf<<24;//EINT14 ENABLE
/* bit[7] = 1, 使能 ACK
* bit[6] = 0, IICCLK = PCLK/16
* bit[5] = 1, 使能中断
* bit[3:0] = 0xf, Tx clock = IICCLK/16
* PCLK = 50MHz, IICCLK = 3.125MHz, Tx Clock = 0.195MHz
*/
webee210_i2c_regs->iiccon = (1<<7) | (0<<6) | (1<<5) | (0xf); // 0xaf
webee210_i2c_regs->iicadd = 0x10; // S3C24xx slave address = [7:1]
webee210_i2c_regs->iicstat = 0x10; // I2C 串行输出使能(Rx/Tx)
}


static int i2c_bus_webee210_init(void)
{
/* 硬件相关的设置 */
webee210_i2c_regs = ioremap(0xE1A00000, sizeof(struct webee210_i2c_regs));
gpd1con = ioremap(0xE02000C0,4);
gpd1pud = ioremap(0xE02000C8,4);
clk_gate_ip3 = ioremap(0xE010046C,4);
/*I2C 初始化,寄存器, io 口 */
webee210_i2c_init();
/*为 i2c 申请中断*/
if(request_irq(IRQ_IIC2, webee210_i2c_xfer_irq, 0, "webee210-i2c",NULL))
{
printk("request_irq failed");
return -EAGAIN;
}
init_waitqueue_head(&webee210_i2c_xfer_data.wait);
/* 注册 i2c_adapter */
i2c_add_adapter(&webee210_i2c_adapter);
return 0;
}
static void i2c_bus_webee210_exit(void)
{
i2c_del_adapter(&webee210_i2c_adapter);
free_irq(IRQ_IIC, NULL);
iounmap(webee210_i2c_regs);
}
module_init(i2c_bus_webee210_init);
module_exit(i2c_bus_webee210_exit);
MODULE_LICENSE("GPL");


注意:这个总线驱动写得不规范,有 bug。 请不要用于工程项目应用。请参考i2c-s3c2410.c 编写规范的 i2c 总线驱动。

上面的东西可以用图 13.3.2 表示
《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动

13.3.2 linux i2c 设备驱动


1. i2c 设备的注册方式


在我们开始编写 i2c 设备驱动之前,我们先来了解一些关于 Linux 中注册 I2C 设备的方法,这个内容可以在 Documentation/i2c/instantiating-devices 文件中找到。下面的内容基于 linux 3.8 的内核:


通过总线号声明设备( Declare the I2C devices by bus number)
立即探测设备( Instantiate the devices explicitly)
通过 Probe 探测 i2c 总线( Probe an I2C bus for certain devices)
在用户空间立即探测( Instantiate from user-space)


简单来说,第一种方式一般应用在嵌入式设备中。因为对于嵌入式设备来说,外围器件基本都是固定的,只需提供有限几款器件的支持即可。使用这种方式的时候,需要在板级配置文件中定义并设置 i2c_board_info 这个结构体的内容。其中需要配置设备名称和设备地址,此外设备中断和私有数据结构也可以选择设置。然后使用 i2c_register_board_info 这个接口对设置的设备进行注册使用。需要注意的是这种方法注册的设备是在注册 I2C 总线驱动时进行驱动适配的。


第二种方法可以通过给定的 I2C 适配器以及相应的 I2C 板级结构体,自行通过i2c_new_device 或 i2c_new_probe_device 接口进行添加注册所需的设备。这种方法灵活性要较第一种方法大,可以很方便的在模块中使用。也可以枚举挂载i2c_client.不需要在内核的 match-xx.c 上写。这免去了重新编译内核的麻烦。


第三种方法是 2.6 内核之前的做法,使用 detect 方法去探测总线上的设备驱动。因为探测机制的原因,会导致一些副作用的发生,所以不建议使用,除非真的没有别的办法。


第 四 种 方 法 是 在 Linux 的 控 制 台 上 , 在 用 户 空 间 通 过 sysfs , 使 用/sys/bus/i2c/devices/i2c-3/new_device 节点进行设备的添加注册。


这里推荐采用方式一或方式二,因为这也是现在内核所推荐和广泛使用的方式。我们的驱动使用的是方式二。所以这里仔细看一下这个根据 Documentation/i2c/instantiating-devices 的描述,我在这里整理一下。

《网蜂A8实战演练》——11.Linux 电容式触摸屏驱动


static struct i2c_board_info sfe4001_hwmon_info = {
I2C_BOARD_INFO("max6647", 0x4e),
};
int sfe4001_init(struct efx_nic *efx)
{
(...)
efx->board_info.hwmon_client =
i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);
(...)
}


如果连 i2c 设备的地址都是不固定的,甚至在不同的板子上有不同的地址,可以提供一个地址列表供系统探测。 此时应该使用的函数是 i2c_new_probe_device.文档如下: A variant of this is when you don't know for sure if an I2C device ispresent or not (for example for an optional feature which is not present oncheap variants of a board but you have no way to tell them apart), or it mayhave different addresses from one board to the next (manufacturer changingits design without notice). In this case, you can call i2c_new_probed_device()instead of i2c_new_device().

获取 i2c_adapter 指针的函数是:

struct i2c_adapter i2c_get_adapter(int id); //它的参数是 i2c 总线编号

使用完要释放:


void i2c_put_adapter(struct i2c_adapter adap);

static const unsigned short normal_i2c[] = { 0x2c, 0x2d, I2C_CLIENT_END };
static int usb_hcd_nxp_probe(struct platform_device *pdev)
{
(...)
struct i2c_adapter *i2c_adap;
struct i2c_board_info i2c_info;
(...)
i2c_adap = i2c_get_adapter(2);
memset(&i2c_info, 0, sizeof(struct i2c_board_info));
strlcpy(i2c_info.type, "isp1301_nxp", I2C_NAME_SIZE);
isp1301_i2c_client = i2c_new_probed_device(i2c_adap, &i2c_info,
normal_i2c, NULL);
i2c_put_adapter(i2c_adap);
(...)
}


关于 i2c 总线号的获取,可以在你 insmod i2c 总线驱动后,进入/sys/class/i2c-dev下查看,如我在 insmod 完上面的 i2c-webee210.c 总线驱动之后:

[Webee210]\# pwd
/sys/class/i2c-dev
[Webee210]\# ls
i2c-3
[Webee210]\#


这里的 3 就是 i2c 总线号了。

2. 注册i2c 设备


现在就来写一个程序,用于将我们的 ft5x06 i2c 设备注册到 i2c bus 上。如在 arm/arch/mach-s5pv210/mach-smdk210.c 中,有下面的代码:


static struct i2c_board_info smdkv210_i2c_devs0[] __initdata = {
{ I2C_BOARD_INFO("24c08", 0x50), }, /* Samsung S524AD0XD1 */
};
/////////////////////////////////////////////
#include
static struct i2c_board_info smdkv210_i2c_devs1[] __initdata = {
/* To Be Updated */
{
I2C_BOARD_INFO("edt_ft5x06", (0x70 >> 1)),
},
};
static struct i2c_board_info smdkv210_i2c_devs2[] __initdata = {
/* To Be Updated */
};


我们在 arm/arch/mach-s5pv210/mach-smdk210.c 没有我们的设备,所以使用i2c_new_probe_device 自己注册一个。

/dev/dev.c

...
/*ft5x06 的子设备号为 0x38 */
static const unsigned short addr_list[] = {0x38, 0x1C, 0x70, 0xe,I2C_CLIENT_END};
static struct i2c_client *ft5x06_client;
static int ft5x06_dev_init(void)
{
struct i2c_adapter *i2c_adap;
struct i2c_board_info ft5x06_info;
memset(&ft5x06_info, 0, sizeof(struct i2c_board_info));
/*我们自己建一个 i2c_board_info,名字叫 ft5x06,i2c_client 将根据这个名
字寻找驱动*/
strlcpy(ft5x06_info.type, "ft5x06", I2C_NAME_SIZE);
/*这个根据自己系统的 i2c 总线的 adapter 号进行修改*/
i2c_adap = i2c_get_adapter(3);
if(!i2c_adap)
printk("i2c_adap_fail\n");
ft5x06_client = i2c_new_probed_device(i2c_adap, &ft5x06_info, addr_list,NULL);
i2c_put_adapter(i2c_adap);
if(!ft5x06_client)
printk("<0> ft5x06_client_fail\n");
return 0;
}
static void ft5x06_dev_exit(void)
{
i2c_unregister_device(ft5x06_client);
}
module_init(ft5x06_dev_init);
module_exit(ft5x06_dev_exit);
MODULE_LICENSE("GPL");


这程序只是将我们的 i2c_client 设备信息注册到总线上而已,为的是能让i2c_driver 匹配到。然后成功加载设备驱动程序。这里我们需要记住我们的设备叫“ft5x060” ,因为 probe 函数是根据这个名字寻找设备,然后挂载驱动程序的。Linux 3.X 强推 probe 的设备和驱动绑定方式,不推荐旧版的 detect 的方式。关于这部分的内容还请读者自行读/Documentation/i2c/writing-clients。接下来,我们来注册我们的 i2c_driver.


3. 注册i2c_driver input_dev


在此之前,我们需要看一下, linux 的输入子系统。因为这个触摸屏正好有用到,所以请看一下前面的 linux 输入子系统。我们直接上代码,这个代码是为 ft5306 而写得,如你的屏幕是 ft5206 需要把 Y 坐标的值做转置处理。


drv/drv.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/errno.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <linux/slab.h>
#include <linux/input.h>
#include <linux/init.h>
#include <linux/serio.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#define DOWN 1
#define UP 0
#define SCREEN_MAX_X 800
#define SCREEN_MAX_Y 480
#define PRESS_MAX 255
#define FT5X0X_NAME "ft5x06"
static struct i2c_client *ft5x06_client;

static unsigned long count = 1;
static struct input_dev *ts_test_dev;
unsigned long *gph1con;
unsigned int isdetect = 0;
//unsigned long *VICVECT1PRIORITY19;
//static struct tasklet_struct my_tasklet;
//static DECLARE_WAIT_QUEUE_HEAD(key_waitq);
//volatile unsigned int ev_press=0;
struct work_struct ft5x06_wq;
static unsigned int ts_status = DOWN;
static unsigned int get_x_coordinate(void)
{
unsigned int xh,xl;
xh = i2c_smbus_read_byte_data(ft5x06_client, 0x3);
xl = i2c_smbus_read_byte_data(ft5x06_client, 0x4);
return (((xh&0xf)<<8)|xl);
}
static unsigned int get_y_coordinate(void)
{
unsigned int yh,yl;
yh = i2c_smbus_read_byte_data(ft5x06_client, 0x5);
yl = i2c_smbus_read_byte_data(ft5x06_client, 0x6);
return (((yh&0xf)<<8)|yl);
}
/*up or down
*up : 1 down : 0
*/
static unsigned int get_ts_status(void)
{
unsigned int status;
status = i2c_smbus_read_byte_data(ft5x06_client, 0x3);
status = (status>>6)&3;
return status;
}
static void webee210_ft5x06_init(void)
{
i2c_smbus_write_byte_data(ft5x06_client, 0, 0);
i2c_smbus_write_byte_data(ft5x06_client, 0xa4, 0);
i2c_smbus_write_byte_data(ft5x06_client, 0xa0, 0);

printk("ft50x6 init ...\n");
}
static void touch_detect(void)
{
unsigned int data_x, data_y;
unsigned int x_data[5], y_data[5];
unsigned int i;
while(1)
{
if(ts_status == UP)
{
printk("\n up!!!!!!!!!!!!!!!!!!!!!\n");
input_report_abs(ts_test_dev, ABS_PRESSURE, 0);
input_report_key(ts_test_dev, BTN_TOUCH, 0);
input_sync(ts_test_dev);
return;
}
if(ts_status == DOWN )
{
printk("\n down !!!!!!!!!!!!!!!!!!!!!\n");
for(i = 0; i < 2; i++)
{
x_data[i] = get_x_coordinate();
y_data[i] = get_y_coordinate();
}
data_x = (x_data[0] + x_data[1])/2;
data_y = (y_data[0] + y_data[1])/2;
if(250< data_x && data_x <400)
data_x = data_x-40;
if(400< data_x && data_x <800)
data_x = data_x+30;
printk("x = %d\n",data_x);
printk("y = %d\n",data_y);
input_report_abs(ts_test_dev, ABS_X, data_x);
input_report_abs(ts_test_dev, ABS_Y, data_y);
input_report_abs(ts_test_dev, ABS_PRESSURE, PRESS_MAX);
input_report_key(ts_test_dev, BTN_TOUCH, 1);

input_sync(ts_test_dev);
}
printk("count %d\n",count);
if(count == 65535)
count = 1;
}
//printk("tasklet\n");
}
static irqreturn_t pen_up_down_handler(int irq, void *dev_id)
{
if((count++)%2 == DOWN)
{
ts_status = DOWN;
schedule_work(&ft5x06_wq);
}
else
{
ts_status = UP;
}
return IRQ_HANDLED;
}
static int ft5x06_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
unsigned int ret;
ft5x06_client = client;
/*分配一个 input_dev 结构体*/
ts_test_dev = input_allocate_device();
/*设置支持哪些事件*/
set_bit(EV_KEY, ts_test_dev->evbit);
set_bit(EV_ABS, ts_test_dev->evbit);
/*设置支持事件中的那种事件*/
set_bit(BTN_TOUCH, ts_test_dev->keybit);
input_set_abs_params(ts_test_dev, ABS_X, 0, SCREEN_MAX_X, 0, 0);

input_set_abs_params(ts_test_dev, ABS_Y, 0, SCREEN_MAX_Y, 0, 0);
input_set_abs_params(ts_test_dev, ABS_PRESSURE, 0, PRESS_MAX,0, 0);
/*注册*/
ret = input_register_device(ts_test_dev);
/*还记得我们电路图的那个 XENT14 脚吗?他就在这里使用了 */
ret = request_irq(IRQ_EINT(14), pen_up_down_handler,IRQ_TYPE_EDGE_BOTH, "ts_test", NULL);
printk("ret_eint14_irq = %d\n", ret);
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/*
init_timer(&timer_ts);
timer_ts.function = timer_ts_handler;
add_timer(&timer_ts);
*/
INIT_WORK(&ft5x06_wq, touch_detect);
webee210_ft5x06_init();
return 0;
}
static int ft5x06_remove(struct i2c_client *client)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
free_irq(IRQ_EINT(14),NULL);
return 0;
}
/* 这里的名字要和我们 dev.c 注册的名字相同,不然会 probe 找不到的。 */
static const struct i2c_device_id ft5x06_id_table[] = {
{ FT5X0X_NAME, 0 },
{}
};
/* 分配/设置 i2c_driver */
static struct i2c_driver ft5x06_driver = {
.driver = {
.name = FT5X0X_NAME,
.owner = THIS_MODULE,
},
.probe = ft5x06_probe,
.remove = ft5x06_remove,

.id_table = ft5x06_id_table,
};
static int ft5x06_drv_init(void)
{
/* 2. 注册 i2c_driver */
i2c_add_driver(&ft5x06_driver);
return 0;
}
static void ft5x06_drv_exit(void)
{
i2c_del_driver(&ft5x06_driver);
}
module_init(ft5x06_drv_init);
module_exit(ft5x06_drv_exit);
MODULE_LICENSE("GPL");


这段代码比较简单,也没什么可讲。不过这个代码有许多地方还可以优化,还有多点触控的模式没有实现。大家可以参考内核进行规范化再编写。

13.4 实验结果


在附录里的实验源代码里有三个文件夹   bus,dev,drv.

.

|-- bus

| |-- bus.c

| |-- i2c-s3c2410.c

|

|-- dev

| `-- dev.c

|-- drv

| |-- drv.c

|


进入目录,分别 make.然后把 bus.ko,dev.ko,drv.ko.按顺序 insmod。然后打开webee 的 qt 程序进行测试。

注意:如果你的总线使用的是 i2c-2410.c 的话。他会打印下面的内容:
Console: switching to colour frame buffer device 100x30
s3c-i2c s3c2440-i2c.0: slave address 0x10
s3c-i2c s3c2440-i2c.0: bus frequency set to 65 KHz
s3c-i2c s3c2440-i2c.0: i2c-0: S3C I2C adapter
s3c-i2c s3c2440-i2c.1: slave address 0x10
s3c-i2c s3c2440-i2c.1: bus frequency set to 65 KHz
s3c-i2c s3c2440-i2c.1: i2c-1: S3C I2C adapter
s3c-i2c s3c2440-i2c.2: slave address 0x10
s3c-i2c s3c2440-i2c.2: bus frequency set to 65 KHz
s3c-i2c s3c2440-i2c.2: i2c-2: S3C I2C adapter


这里你需要选择总线号为 2 的 adapter,即要把 dev.c 修改如下:
i2c_adap = i2c_get_adapter(2);


如果挂载的是 bus.c 编译的结果:则把 dev.c 修改如下:
i2c_adap = i2c_get_adapter(3);


在进行总线实验时,在编译 linux 内核时,请不要开启内核的 i2c 总线。不然可能出现不能挂载的现象。

13.5 本章总结

本章看起来是讲一个触摸屏驱动,实际上最重要的知识都是与 i2c 设备相关的。 i2c 总线设备驱动是 linux 最常用到的设备类型之一,要学好他,需要多多参考内核的 i2c 设备驱动,而且要注意在不同的内核版本里,其实现的不同。至于触摸屏本身,其可变性较强。需要根据不同的触摸屏芯片来换算坐标的值。所以阅读 datasheet 的本领一定要会。