前面我们分析了linux触摸屏驱动的input子系统机制,本章节分析linux触摸屏驱动的i2c机制。
驱动源码路径:
kernel/drivers/input/touchscreen/ft5x06_touch.c
kernel/drivers/input/touchscreen/ft5x06_firmware_1024_600.h
kernel/drivers/input/touchscreen/ft5x06_firmware_800_480.h复制代码
在探测函数ft5x06_ts_probe中,定义了两个结构体指针adapter和client,分别指向i2c的主设备结构体i2c_adapter和i2c的从设备结构体i2c_client。另外定义了一个记录i2c从设备信息的结构体info,用于记录从设备的名称和设备地址。
首先通过i2c_get_adapter函数获得一个i2c_adapter指针:
adapter = i2c_get_adapter(FT5X06_I2C_BUS);复制代码
函数的传入参数为I2C的通道,这里为通道1,因为x4412开发板上对应的触摸屏芯片接在I2C的1通道。
再将从设备的名称和地址填充到结构体info:
memset(&info, 0, sizeof(struct i2c_board_info));
info.addr = FT5X06_I2C_ADDRESS;//填充i2c_board_info结构体中I2C的地址
strlcpy(info.type, "ft5x06-iic", I2C_NAME_SIZE);复制代码
注意,这里的设备名称为ft5x06-iic,后面在i2c_driver结构体中一定要有和它匹配的名称,否则i2c驱动的probe函数将无法执行。
接着使用i2c_new_device函数将i2c的主设备和从设备关联起来,组成一个客户端,并返回一个指向i2c_client的结构体client。这个结构体非常重要,它将通过i2c_add_driver函数传给i2c对应的probe函数ft5x06_iic_probe,后面整个i2c操作都离不开它。
前面通过i2c_get_adapter函数获得了adapter结构体后,一旦通过adapter获得了从设备的结构体client,它的任务即已经完成,需要使用i2c_put_adapter函数释放该指针:
i2c_put_adapter(adapter);复制代码
最后通过i2c_add_driver函数注册一个i2c驱动:
ret = i2c_add_driver(&ft5x06_iic_driver);
ft5x06_iic_driver对应的结构体为:
static struct i2c_driver ft5x06_iic_driver = {
.driver = {
.name = "ft5x06-iic",//结构体中已经存在id_table,故匹配名称时以id_table为准
},
.probe = ft5x06_iic_probe,
.remove = ft5x06_iic_remove,
.suspend = ft5x06_iic_suspend,
.resume = ft5x06_iic_resume,
.id_table = ft5x06_iic_id,
};复制代码
注意,该结构体中已经存在id_talbe,platform匹配时,将不再认成员driver中的name,而是会在id_table中查找是否有和前面我们定义的i2c名称“ft5x06_iic_probe”相同。
id_table对应内容如下:
static const struct i2c_device_id ft5x06_iic_id[] = {
{ "ft5x06-iic", 0},//该名称与i2c_board_info结构体中的驱动名称匹配,则调用probe函数
{ }
};复制代码
可见,内核将会成功匹配,i2c的探测函数ft5x06_iic_probe得以运行。在该探测函数中,首先通过i2c_check_functionality函数检查i2c主设备的驱动能力:
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WORD_DATA))//查看I2C适配器的能力
return -ENODEV;复制代码
再将函数中定义的指向ft5x06_ts结构体的指针ts的数据存放在client->dev->p->driver_data,以备调用:
ts->client = client;//将从设备i2c_client数据赋值给ts
i2c_set_clientdata(client, ts);复制代码
在配置完触摸屏的input子系统以及触摸屏中断后,就开始通过i2c读取和写入数据了。
函数ft5x06_read_fw_ver用于读取FT5x06芯片的firmware版本:
uc_reg_value = ft5x06_read_fw_ver(client);复制代码
这里传入了前面申请的结构体client,其函数原型如下:
static uint8_t ft5x06_read_fw_ver(struct i2c_client * client)
{
uint8_t ver;
ft5x06_read_reg(client, FT5X0X_REG_FIRMID, &ver);
return (ver);
}
static int ft5x06_read_reg(struct i2c_client * client, uint8_t addr, uint8_t * data)
{
uint8_t buf[2];
struct i2c_msg msgs[2];
int ret;
buf[0] = addr;
msgs[0].addr = client->addr;//i2c芯片地址
msgs[0].flags = 0; //0表示写
msgs[0].len = 1; //要写的字节数为1
msgs[0].buf = buf; //需要读的寄存器地址
msgs[1].addr = client->addr;//i2c芯片地址
msgs[1].flags = I2C_M_RD;//1表示读,I2C_M_RD=1
msgs[1].len = 1; //要读的字节数为1
msgs[1].buf = buf; //读取的数据保存到buf
ret = i2c_transfer(client->adapter, msgs, 2);//传输2个msg
if(ret < 0)
printk("msg i2c read error\n");
*data = buf[0];
return ret;
}复制代码
真正干活的是函数ft5x06_read_reg,它通过i2c_transfer函数读取寄存器值。传入参数addr对应需要读取的寄存器的地址,*data返回从寄存器中读取的寄存器值。ft5x06_read_reg函数是一个典型的利用i2c_transfer读取寄存器值的模板,它定义了两个i2c_msg结构体msgs,msgs[0]用于写寄存器的地址,msgs[1]用于读该寄存器的值。I2C每次读寄存器的值,都需要先对该寄存器的地址发写命令,再发读命令返回写入寄存器的地址对应的寄存器值。
i2c_msg结构体内容如下:
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags;
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* read data, from slave to master */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};复制代码
这里addr表示从设备的地址,flags表示I2C需要执行的动作,为0表示写,为1表示读。len表示写入或读出的寄存器的字节数。当为写时,buf表示需要写入的寄存器地址,当为读时,buf返回从寄存器读出的值。i2c_transfer函数的第三个参数表示传入的msg的个数,通常读一个寄存器时,该值为2,写一个寄存器时,该值为1。
ft5x06_write_reg函数用于写寄存器,其源码如下:
static int ft5x06_write_reg(struct i2c_client * client, uint8_t addr, uint8_t data)
{
uint8_t buf[3];
int ret = -1;
buf[0] = addr;
buf[1] = data;
ret = ft5x06_i2c_txdata(client, buf, 2);
if (ret < 0)
{
printk("write reg failed! %#x ret: %d", buf[0], ret);
return -1;
}
return 0;
}
static int ft5x06_i2c_txdata(struct i2c_client * client, char * txdata, int length)
{
int ret;
struct i2c_msg msg[] = {
{
.addr = client->addr, //i2c芯片地址
.flags = 0, //0表示写寄存器
.len = length, //要写入的数据的长度为2
.buf = txdata, //寄存器地址和该地址要写入的数据
},
};
ret = i2c_transfer(client->adapter, msg, 1);//传输1个msg
if (ret < 0)
printk("%s i2c write error: %d\n", __func__, ret);
return ret;
}复制代码
在ft5x06_write_reg函数中,通过形参将要写入的寄存器的地址和数据保存到buf数组,在ft5x06_i2c_txdata函数中定义了一个i2c_msg结构体msg,它的flags标志为0,表示写寄存器,buf[0]和buf[1]分别为要写入的寄存器的地址和数据,写入的长度len为2。最后通过i2c_transfer函数写入寄存器。
在触摸屏芯片第一次执行时,需要通过I2C给芯片里面的固件升级,这时需要成批的操作寄存器。
FTS_BOOLbyte_write函数用于批量写寄存器,其原型如下:
static FTS_BOOL byte_write(struct i2c_client * client, FTS_BYTE* pbt_buf, FTS_DWRD dw_len)
{
return i2c_write_interface(client, FT5X06_I2C_ADDRESS, pbt_buf, dw_len);
}
static FTS_BOOL i2c_write_interface(struct i2c_client * client, FTS_BYTE bt_ctpm_addr, FTS_BYTE* pbt_buf, FTS_DWRD dw_lenth)
{
int ret;
ret = i2c_master_send(client, pbt_buf, dw_lenth);//一次性写入多个寄存器
if(ret<=0)
{
printk("[FTS]i2c_write_interface error line = %d, ret = %d\n", __LINE__, ret);
return FTS_FALSE;
}
return FTS_TRUE;
}复制代码
FTS_BOOLbyte_write函数的传入参数中,pbt_buf指向要写入的寄存器值表,通常对应一个头文件,如ft5x06_firmware_1024_600.h。dw_len表示一次性要写入的寄存器的数量。
byte_read函数用于批量读寄存器,其原型如下:
static FTS_BOOL byte_read(struct i2c_client * client, FTS_BYTE* pbt_buf, FTS_BYTE bt_len)
{
return i2c_read_interface(client, FT5X06_I2C_ADDRESS, pbt_buf, bt_len);
}
static FTS_BOOL i2c_read_interface(struct i2c_client * client, FTS_BYTE bt_ctpm_addr, FTS_BYTE* pbt_buf, FTS_DWRD dw_lenth)
{
int ret;
ret = i2c_master_recv(client, pbt_buf, dw_lenth);//一次性读取多个寄存器
if(ret<=0)
{
printk("[FTS]i2c_read_interface error\n");
return FTS_FALSE;
}
return FTS_TRUE;
}复制代码
这里pbt_buf用于存储批量读取的寄存器值,bt_len表示一次性读取的寄存器数。
通过本实例可以一句话总结出使用linux内核的I2C机制编写I2C设备驱动的方法,首先需要获取一个i2c_client结构体,再通过i2c_transfer函数读写寄存器,或通过i2c_master_send和i2c_master_recv函数批量读写寄存器。