I2C是很常见的一种总线协议,I2C是NXP公司设计的,I2C使用两条线在主控制器和从机之间进行数据通信。一条是SCL(串行时钟线),另外一条是SDA(串行数据线),因为I2C这两条数据线是开漏输出的,所以需要接上拉电阻,总线空闲的时候SCL和SDA处于高电平。I2C总线标准模式下速度可以达到100Kb/s,快速模式下可以达到400kb/s。如果大家玩过51单片机,肯定对模拟I2C时序这个操作并不陌生,但是在Linux上,还需要我们继续来模拟I2C的时序吗,答案是不需要的,cpu会自带I2C控制器,有了这个I2C控制器后,就不需要模拟时序了,只需要关心怎么把数据写到寄存器和怎么从寄存器读数据即可,具体的时序都是由I2C控制器来帮我们自动完成。
Linux把I2C控制器抽象成一个i2c_adapter,我们只要来分配这个i2c_adapter,就可以得到一个I2C控制器。我们可以先来体验一下,在Linux上操作I2C是多模任意,先来看一下系统里面都有哪些I2C的节点,在开发板串口输入:
ls /dev/i2c-*
查看i2c节点:
Linux有一个非常重要的概念是一切皆文件,那么我们能不能在应用层通过open这些节点来操作I2C来跟外设I2C通信的芯片进行数据交互呢?当然是可以的,我们来以前看一下,这里我们以7寸RGB屏幕上的触摸芯片FT5X06为例。
通过原理图来确定FT5X06使用的是哪个I2C,通过下面的截图我们可以得到在开发板上,触摸芯片FT5X06使用的是I2C2,对应的节点是dev下面的i2c-1。那么跟触摸芯片FT5X06进行通信,是不是操作dev下的i2c-1这个节点就可以了?
怎么在应用层操作I2C呢,应用层操作I2C是以数据包进行交流的,所有我们在应用层就要进行封包的操作。数据包对应的结构体是i2c_rdwr_ioctl_data,这个结构体在include\uapi\linux\i2c-dev.h下面,定义如下:
struct i2c_rdwr_ioctl_data {
struct i2c_msg __user *msgs; /* pointers to i2c_msgs 要发送的数据包的指针*/
__u32 nmsgs; /* number of i2c_msgs发送数据包的个数 */
};
再来看一下i2c_msg结构体的定义,这个结构体是定义在include\uapi\linux\i2c.h下面,定义如下:
struct i2c_msg {
__u16 addr; /* slave addres 从机地址*/
__u16 flags ;/*读写标志位,为1表示为读,反之为0,则为写*/
#define I2C_M_RD 0x0001 /* read data, from slave to master */
/* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
__u16 len; /* msg length 为buf的大小,单位是字节*/
__u8 *buf; /* pointer to msg data 当flags为1是,buf是要接受的数据,当flags为0,就是要发送的数据*/
};
那么怎么设计程序呢,首先要看一下触摸芯片的数据手册:
了解相关的寄存器后,就可以开始写程序了
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/inpuit.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#incldue <sys/ioctl.h>
?
int fd;
?
int i2c_read_data(unsigned int slave_addr, unsigned char reg_addr)
{
unsigned char data;
struct i2c_rdwr_ioctl_data i2c_read_lcd;
struct i2c_msg msg[2] = {
[0] = {//第一个数据包先写要操作的寄存器的地址
.addr = slave_addr,
.flags = 0,
.buf = ®_addr,
.len = sizeof(reg_addr)
},
[1] = {//第二个数据包再读这个寄存器的数据
.addr = slave_addr,
.flags = 1,
.buf = &data,
.len = sizeof(data)
},
};
i2c_read_lcd.msgs = msg;
i2c_read_lcd.nmsgs = 2;
ret = ioctl(fd, I2C_RDWR, &i2c_read_lcd);
if (ret < 0) {
perror("ioctl errror is:");
return ret;
}
return data;
}
?
int main(int argc, char *argv[])
{
int TD_STATUS;
fd = open("/dev/i2c-1", O_RDWR);
if (fd < 0) {
perror("open error");
return fd;
}
while (1) {
TD_STATUS = i2c_read_data(0x38,0x02);
printf("TD_STATUS value is %d\n", TD_STATUS);
sleep(1);
}
return 0;
}
编译并在开发板上运行这个app:
Linux中的I2C也是按照平台总线模型设计的,既然也是按照平台总线模型设计的,是不是也分为一个device和一个driver呢?但是I2C这里的device不叫device,也叫client。platform是虚拟出来的一条总线,目的是未来实现总线、设备、驱动框架。对于I2C而言,不需要虚拟出一条总线,直接使用I2C总线即可。
同样,这里先从非设备树开始,先看一下再没有设备树之前怎么实现的I2C的device部分,也就是client部分。然后再学习有了设备树之后,我们的client是怎么编写的,按照Linux的发展路径来学习。
在没有使用设备树之前,我们使用的是i2c_board_info这个结构体来描述一个I2C设备的, i2c_board_info 这个结构体如下,在include/linux/i2c.h:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short flags;
unsigned short addr;
void *platform_data;
struct dev_archdata *archdata;
struct device_node *of_node;
struct fwnode_handle *fwnode;
int irq;
};
在上面的这个结构体中,type和addr这两个成员变量是必须要设置的,一个是I2C设备的名字,这个名字就是用来进行匹配用的,一个是I2C设备的器件地址。也可以使用宏,在include/linux/i2c.h::
#define I2C_BOARD_INFO(dev_type, dev_addr) \
.type = dev_type, .addr = (dev_addr)
可以看出,I2C_BOARD_INFO宏其实就是设置i2c_board_info的 type 和 addr这两个成员变量。
I2C设备和驱动的匹配过程是由I2C核心来完成的,在Linux源码的drivers/i2c/i2c-core.c就是I2C的核心部分,I2C核心提供了一些与具体硬件无关的函数,如下:
作用:获得一个I2C适配器。
struct i2c_adapter *i2c_get_adapter(int nr);
参数:
nr:要获得的哪个I2C适配器的编号。
返回值:失败返回NULL。
作用:释放I2C适配器。
void i2c_put_adapter(struct i2c_adapter *adap);
参数:
adap:要释放I2C适配器。
返回值:失败返回NULL。
作用:把I2C适配器和I2C器件关联起来。
struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info);
参数:
adap:I2C适配器。
info:i2c_board_info的指针。
返回值:失败返回NULL。
作用:注销一个client。
void i2c_unregister_device(struct i2c_client *client);
client:i2c_client的指针。
在使用设备树以后,就不用这么复杂了,使用给设备树的时候只要在对应的I2C节点下创建相应设备的节点即可,比如我想添加一个触摸芯片FT5X06的设备,我就可以在对应的I2C的节点下这样写:
注意:这里使用的是10.1寸的触摸芯片gt911,4.3寸触摸芯片是tsc2007.其他都是ft5426。
查看对应设备树节点:
注意:我们使用的是I2C2,上图设备树节点看是1-0038,为啥是1呢,因为平台上的是I2C2,是从1开始计数的,而这里是从0开始计数的。
下面演示不用设备树的方法进行实验,
注释掉之后编译内核源码,并烧写至系统,会发现看不到设备节点了
现在运行编译脚本就不会把这个设备驱动编译进去了
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
?
//分配一个I2C适配器指针
struct i2c_adapter *i2c_ada;
?
//分配一个i2c_client指针
struct i2c_client *i2c_client;
?
//支持的I2C的设备列表
struct ic_board_info ft5x06_info[] = {
//每一项都代表一个I2C设备,这句话的意思就是说这个设备的名字代表ft5x06_test,器件地址是0x38
{I2C_BOARD_INFO("ft5x06_test", 0x38)},
};
?
static int ft5x06_client_init(void)
{
//调用 i2c_get_adapter ,获得一个I2C总线,因为ft5x06是挂载动力I2C2上,所以这个参数就是1,所以这句代码的意思就是把这个触摸触摸芯片挂载到i2c2上.
i2c_ada = i2c_get_adapter(1);
//把I2C适配器和I2C器件关联起来
i2c_new_device(i2c_ada, ft5x06_info);
//释放I2C控制器
i2c_put_adapter(i2c_ada);
printk("This is ft5x06_client_init\n");
return0;
}
?
static void ft5x06_client_exit(void)
{
i2c_unsigned_device(i2c_client);
printk("This is ft5x06_client_exit\n");
}
?
module_init(ft5x06_client_init);
module_exit(ft5x06_client_exit);
MODULE_LICENSE("GPL");
挂载驱动,可以看到有对应的设备节点了:
上面实现了client部分,然后我们再来看driver部分。不管是使用设备树还是非设备树,driver部分就比较复杂了。和注册一个杂项设备或者是字符设备的套路一样,也是要先顶一个i2c_driver的结构体,然后再对他进行初始化,下面先看一下这个结构体的定义,如下图所示:
struct i2c_driver {
unsigned int class;
?
/* Notifies the driver that a new bus has appeared. You should avoid
* using this, it will be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *) __deprecated;
?
/* Standard driver model interfaces */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
?
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);
?
/* Alert callback, for example for the SMBus alert protocol.
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
* For the SMBus Host Notify protocol, the data corresponds to the
* 16-bit payload data reported by the slave device acting as master.
*/
void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol,
? ? unsigned int data);
?
/* a ioctl like command that can be used to perform specific functions
* with the device.
*/
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
?
struct device_driver driver;
const struct i2c_device_id *id_table;
?
/* Device detection callback for automatic device creation */
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
struct list_head clients;
};
初始化完成以后就是把i2c_driver注册进内核,注册进内核我们使用的是i2c_add_driver。
作用:注册一个i2c驱动。
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
参数:
driver: struct i2c_driver的指针。
返回值:失败返回负值。
作用:删除一个i2c驱动。
extern void i2c_del_driver(struct i2c_driver *);
参数:driver: struct i2c_driver的指针。
返回值:失败返回负值。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
?
static const struct i2c_device_id ft5x06_id_ts[] = {
{"xxx",0},
};
?
static const struct of_device_id ft5x06_id[] = {
{.compatible = "edt,edt-ft5306", 0},
{.compatible = "edt,edt-ft5x06", 0},
{.compatible = "edt,edt-ft5406", 0},
};
?
int ft5x06_probe(struct i2c_client *i2c_client, const struct i2c_device_id *id)
{
printk("This is ft5x06_probe\n");
//注册一个杂项设备 或 注册一个字符设备
return 0;
}
?
int ft5x06_remove(struct i2c_client *i2c_client)
{
return 0;
}
static struct i2c_driver ft5x06_driver = {
.driver = {
.owner = YHIS_MODULE,
.name = "ft5x06_test",
.of_match_table = ft5x06_id,
},
.probe = ft5x06_probe,
.remove = ft5x06_remove,
.id_table = ft5x06_pid_ts
};
?
static int ft5x06_driver_init(void)
{
int ret;
ret = i2c_add_driver(&ft5x06_driver);
if (ret < 0) {
printk("i2c_add_driver is error\n");
return ret;
}
printk("This is ft5x06_driver_init\n");
return 0;
}
?
static void ft5x06_driver_exit(void)
{
i2c_del_driver(&ft5x06_driver);
printk("This is ft5x06_driver_exit\n");
}
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");
makefile:
编译驱动,加载驱动前需要取消设备树相关内容注释并烧写到板子上:
在上面程序的基础上修改:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
?
static const struct i2c_device_id ft5x06_id_ts[] = {
{"xxx",0},
};
?
static const struct of_device_id ft5x06_id[] = {
{.compatible = "edt,edt-ft5306", 0},
{.compatible = "edt,edt-ft5x06", 0},
{.compatible = "edt,edt-ft5406", 0},
};
?
static struct i2c_client *ft5x06_client;
static int ft5x06_read_reg(u8 reg_addr);
static void ft5x06_write_reg(u8 reg_addr, u8 data, u8 len);
?
static void ft5x06_read_reg(u8 reg_addr)
{
u8 data;
struct i2c_msg msgs[] {
//第一个数据包,写
[0] = {
.addr = ft5x06_client->addr,
.flags = 0,
.len = sizeof(reg_addr),
.buf = ®_addr,
},
//第二个数据包,读
[1] = {
.addr = ft5x06_client->addr,
.flags = 0,
.len = sizeof(data),
.buf = &data,
},
};
i2c_transfer(ft5x06_client->adapter, msgs, 2);
return data;
}
?
static void ft5x06_write_reg(u8 reg_addr, u8 data, u8 len)
{
u8 buff[256];
struct i2c_msg msgs[] = {
[0] = {
.addr = ft5x06_client->addr,
.flags = 0,
.len = len+1,
.buf = buff,
}
};
buff[0] = reg_addr;
memcpy(&buff[1], &data, len);
i2c_transfer(ft5x06_client->adapter, msgs, 1);
}
?
int ft5x06_probe(struct i2c_client *i2c_client, const struct i2c_device_id *id)
{
printk("This is ft5x06_probe\n");
ft5x06_client = client;//因为我们再别的函数里面用到这个client,所以我们要把他复制出来。
//往地址为0x80的寄存器里面写入0x4b
ft5x06_write_reg(0x80, 0x4b, 1);
//读取寄存器地址为0x80的数据
ret = ft5x06_read_reg(0x80);
printk("ret is %#x\n",ret);
return 0;
}
?
int ft5x06_remove(struct i2c_client *i2c_client)
{
return 0;
}
static struct i2c_driver ft5x06_driver = {
.driver = {
.owner = YHIS_MODULE,
.name = "ft5x06_test",
.of_match_table = ft5x06_id,
},
.probe = ft5x06_probe,
.remove = ft5x06_remove,
.id_table = ft5x06_pid_ts
};
?
static int ft5x06_driver_init(void)
{
int ret;
ret = i2c_add_driver(&ft5x06_driver);
if (ret < 0) {
printk("i2c_add_driver is error\n");
return ret;
}
printk("This is ft5x06_driver_init\n");
return 0;
}
?
static void ft5x06_driver_exit(void)
{
i2c_del_driver(&ft5x06_driver);
printk("This is ft5x06_driver_exit\n");
}
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");
编译加载驱动:
改成0x02:
重新编译加载驱动:这个是没有放手指头的
放一个手指头在屏幕上,重新加载驱动: