linux内核将I2C驱动分为:I2C总线驱动(包括核心层和总线驱动层) 和 I2C设备驱动两部分,我们编写的是设备驱动。
以编写一个三合一光照传感器(ap3216c)的I2C驱动为例: AP3216C 模块的核心就是这个芯片本身。这颗芯片集成了光强传感器(ALS: Ambient Light Sensor),接近传感器(PS: Proximity Sensor),还有一个红外LED(IR LED)。这个芯片设计的用途是给手机之类的使用,比如:返回当前环境光强以便调整屏幕亮度;用户接听电话时,将手机放置在耳边后,自动关闭屏幕避免用户误触碰。该芯片通过I2C接口作为slave与主控制器相连,支持中断。
修改设备树
首先修改设备树,在 iomuxc节点中添加一个新的子节点借助Pictrl子系统来描述三合一传感器所使用的 IIC引脚,也就是根据硬件原理图配置引脚的复用功能和电气属性(一般已经被厂商配置好了)。
以i2c1为例
I2C_SCL: UART4_TXD 复用 ALT2
I2C_SDA: UART4_RXD 复用 ALT2
&iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
…………(省略)
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
…………(省略)
}
在I2C节点下添加设备子节点,重要的是compatible属性和设备的器件地址reg,用于和设备驱动匹配。
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>;
};
};
reg内容查看ap3216c中的i2c从机地址 i2c slave address
此地址由厂商设置,用于开机设备树匹配从机,需要保证设备树reg和厂商一致
i2c匹配
定义 I2C 驱动之前,用户首先要定义变量 of_device_id 和 i2c_device_id。(虽然传统匹配表和设备树匹配表只需要配置一个就可以匹配成功,但是在使用设备树匹配的时候需要同时实现传统匹配表(主要依靠compatible匹配),不然无法匹配成功,无法运行probe函数,详细参考:i2c中probe函数实现必须要传统匹配和设备树匹配同时实现)
//传统匹配表
static const struct i2c_device_id ap3216c_id[] = {
{"alientek,ap3216c", 0},
{}
};
//设备树匹配表
static const struct of_device_id ap3216c_of_match[] = {
{.compatible = "alientek,ap3216c"},
{/* Sentinel */}
};
iic_driver结构体
在I2C设备驱动程序中,首先构建一个iic_driver结构体,实现probe/remove函数以及指定驱动和设备的匹配方法。
static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.name = "ap3216c",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(ap3216c_of_match),
},
.id_table = ap3216c_id,
};
配置成功后使用i2c_add_driver函数
在调用 i2c_add_driver 注册 I2C 驱动时,会遍历 I2C 设备,如果该驱动支持所遍历到的设备,即匹配成功后,则会调用该驱动的=== probe ==函数。
i2c_add_driver(&ap3216c_driver) -> .probe = ap3216c_probe
在驱动加载的时候遇到同名的i2c_board_info就会将i2c_client和driver绑定,并且执行driver的probe函数。
iic_probe
在probe函数中,执行就是字符设备那一套。
1)、分配设备号。
2)、初始化 cdev 结构体(cdev_init)【字符设备结构体】然后使用 cdev_add 函数,向Linux 系统添加这个字符设备。
3)、自动创建设备节点:a、创建一个 class 类:class_create(owner, name)在这个类下创建设备:b、device_create(struct class *class,struct device *parent……)
这样,在用户空间的dev目录下,就会生成一个设备节点,通过对这个设备节点进行操作,尽可以实现对底层硬件的操作。
定义ap3216c结构体,将需要用到的数据全部放进去。
struct ap3216c_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
void *private_data;
unsigned short ir, als, ps;
};
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
//i2c_client 结构体 该结构体定义了挂载在I2C总线下的slave设备,一个结构体对象代表一个slave设备 ,而此函数参数有两个表明i2c_device_id必须实现
{
printk("ap3216c probe \r\n"); //如果不执行则没有匹配成功
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (ap3216cdev.major) { /* 定义了设备号 */
ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME); /* 申请设备号 */
ap3216cdev.major = MAJOR(ap3216cdev.devid); /* 获取分配号的主设备号 */
ap3216cdev.minor = MINOR(ap3216cdev.devid); /* 获取分配号的次设备号 */
}
printk("ap3216cdev major=%d,minor=%d\r\n",ap3216cdev.major, ap3216cdev.minor);
/* 2、初始化cdev */
ap3216cdev.cdev.owner = THIS_MODULE;
cdev_init(&ap3216cdev.cdev, &ap3216c_fops);
/* 3、添加一个cdev */
cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);
/* 4、创建类 */
ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
if (IS_ERR(ap3216cdev.class)) {
return PTR_ERR(ap3216cdev.class);
}
/* 5、创建设备 */
ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
if (IS_ERR(ap3216cdev.device)) {
return PTR_ERR(ap3216cdev.device);
}
ap3216cdev.private_data = client;
return 0;
}
其中在初始化cdev中会初始化操作函数
cdev_init(&ap3216cdev.cdev, &ap3216c_fops);
static struct file_operations ap3216c_fops = { //操作函数,打开读写操作
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
.release = ap3216c_release,
};
读写操作
match过程
i2c_add_driver–>i2c_register_driver–>i2c_bus_type–>.match->i2c_device_match–>of_driver_match_device/i2c_match_id
(比较i2c_driver->id_table->name和client->name,如果相同,则匹配上,匹配上之后,运行driver_register调用driver_probe_device进行设备与驱动绑定。
i2c_client实例化过程
在Linux启动的时候会将信息进行收集,i2c适配器会扫描已经静态注册的i2c_board_info,通过调用i2c_register_board_info函数将包含所有I2C设备的i2c_board_info信息的i2c_devinfo变量加入到__i2c_board_list链表中,并调用i2c_new_device为其实例化一个i2c_client。
i2c_client 代表一个挂载到i2c总线上的i2c从设备,包含该设备所需要的数据
{
struct i2c_adapter *adapter 该i2c从设备所依附的i2c控制器
struct i2c_driver *driver 该i2c从设备的驱动程序
addr 该i2c从设备的访问地址
name 该i2c从设备的名称
}
用户只需要提供相应的 I2C 设备信息,Linux 就会根据所提供的信息构造 i2c_client 结构体。
字符设备文件操作函数集合的初始化,也就是read\write等函数,具体实现就是:根据三合一光照传感器的寄存器地址,借助I2C 适配器中 i2c_algorithm结构体里面的收发(master_xfer)函数,实现对设备IIC设备的初始化和读写操作;
根据i2c读操作流程第一次从机地址,发送写操作,寄存器地址;第二次为发送从机地址,读操作,读数据。
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len) //dev要操作的结构体,reg寄存器首地址,保存到val,读len个寄存器
{
struct i2c_client *client = (struct i2c_client*)dev->private_data;//之前定义了将client放在private_data中,
struct i2c_msg msg[2] = {
{
.addr = client->addr, //msg0 是发送要读取寄存器的首地址,即ap3216c的地址
.flags = 0, //表示要发送数据
.buf = ®, //要发送数据的寄存器地址
.len = 1, //要发送的寄存器地址长度为1
},{
.addr = client->addr, //msg1 读取寄存器的首地址,即ap3216c的地址
.flags = I2C_M_RD, //表示要发送数据
.buf = val, //接收到从机发送的数据
.len = len, //要读取的寄存器长度
}
}; //定义了两个msg ,将i2c的读操作分为两个阶段,i2c流程!!!
return i2c_transfer(client->adapter, msg, 2);
}
函数操作流程:
return i2c_transfer(client->adapter, msg, 2);
->i2c_adapter
->i2c_algorithm
->master_xfer(adap,msgs,num);
写操作
static int ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
u8 b[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client*)dev->private_data
//构建要发送的数据,也就是寄存器首地址+实际的数据
b[0] = reg;
memcpy(&b[1], buf, len);
msg.addr = client->addr; //msg0 是发送要读取寄存器的首地址,即ap3216c的地址
msg.flags = 0; //表示要发送数据
msg.buf = b; //要发送数据,寄存器地址+实际数据
msg.len = len + 1; //要发送的数据长度,寄存器地址长度+实际数据长度
return i2c_transfer(client->adapter, &msg, 1);
}
流程同上
接下来就是读写操作函数,对于ap3216c来说最主要的是打开初始化和读数据并显示初始化流程如下:1. 复位(设置0X00寄存器为0X04)2. 设置工作模式(如0X03,开启ALS+PS+IR)
static int ap3216c_open(struct inode *inode, struct file *filp)
{
unsigned char value = 0;
filp->private_data = &ap3216cdev; /* 设置私有数据 */
printk("ap3216c_open\r\n");
//初始化ap3216c
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X4); //复位
mdelay(50); /* AP33216C复位至少10ms */
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X3); //判断是否初始化成功
value = ap3216c_read_reg(&ap3216cdev, AP3216C_SYSTEMCONG);
printk("AP3216C_SYSTEMCONG = %#x\r\n", value);
return 0;
}
下面是数据读取
ap3216c硬件图,其中SDA和SDL为I2C信号线,INT为中断信号线,可以不接
ap32316c的寄存器列表,以及说明
寄存器的名字和地址现在 ap3216c.h 中定义
#define AP3216C_SYSTEMCONG 0x00 /* 配置寄存器 */
#define AP3216C_INTSTATUS 0X01 /* 中断状态寄存器 */
#define AP3216C_INTCLEAR 0X02 /* 中断清除寄存器 */
#define AP3216C_IRDATALOW 0x0A /* IR数据低字节 */
#define AP3216C_IRDATAHIGH 0x0B /* IR数据高字节 */
#define AP3216C_ALSDATALOW 0x0C /* ALS数据低字节 */
#define AP3216C_ALSDATAHIGH 0X0D /* ALS数据高字节 */
#define AP3216C_PSDATALOW 0X0E /* PS数据低字节 */
#define AP3216C_PSDATAHIGH 0X0F /* PS数据高字节 */
寄存器AP3216C_IRDATALOW到AP3216C_PSDATAHIGH一共6个寄存器,将这个6个寄存器的值顺序读取,分别放到ir,ps,als变量中
void ap3216c_readdata(struct ap3216c_dev *dev)
{
//als:环境光强度 ps:接近距离 ir:红外线强度
unsigned char buf[6];
unsigned char i = 0;
for(i = 0; i < 6; i++){
buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);
}
if(buf[0] & 0X80){ /* IR_OF位为1,则数据无效 */
dev->ir = 0;
dev->ps = 0;
}else{
dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0x03);
dev->ps = (((unsigned short)buf[5] & 0x3F) << 4) | (buf[4] & 0x0F);
}
dev->als = ((unsigned short)buf[3] << 8) | buf[2];
}
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
long err = 0;
short data[3];
struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;
ap3216c_readdata(dev);
data[0] = dev->ir;
data[1] = dev->als;
data[2] = dev->ps;
err = copy_to_user(buf, data, sizeof(data));
return 0;
}
寄存器AP3216C_IRDATALOW到AP3216C_PSDATAHIGH一共6个寄存器,将这个6个寄存器的值顺序读取,分别放到ir,ps,als变量中。
不同的工作模式读取时间是有差别的,读取的太快可能无法得到当前准确数值。
最后就是应用
int main(int argc, char *argv[])
{
int fd;
int err = 0;
char *filename;
unsigned short data[3];
unsigned short ir, ps, als;
filename = argv[1];
if(argc != 2) { //输出参数为2个
printf("Error Usage!\r\n");
return -1;
}
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
//als:环境光强度 ps:接近距离 ir:红外线强度
while(1){
err = read(fd, data, sizeof(data));
if(err == 0){
ir = data[0];
als = data[1];
ps = data[2];
printf("ap3216c ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
}
usleep(200000);
}
close(fd);
return 0;
}
至此,整个基于i2c的ap3216c的驱动程序就写完了,输出结果如下:
/lib/modules/4.1.15 # depmod
/lib/modules/4.1.15 # modprobe ap3216cplus.ko
ap3216c probe
ap3216cdev major=249,minor=0
/lib/modules/4.1.15 # ./ap3216cAPP /dev/ap3216ci2c
ap3216c_open
AP3216C_SYSTEMCONG = 0x3
ap3216c ir = 0, als = 0, ps = 0
ap3216c ir = 0, als = 501, ps = 136
ap3216c ir = 36, als = 453, ps = 51
ap3216c ir = 0, als = 507, ps = 57
ap3216c ir = 30, als = 516, ps = 94
ap3216c ir = 0, als = 460, ps = 0
ap3216c ir = 25, als = 496, ps = 0
ap3216c ir = 0, als = 451, ps = 67
ap3216c ir = 4, als = 501, ps = 124
ap3216c ir = 12, als = 434, ps = 80
ap3216c ir = 28, als = 444, ps = 0
ap3216c ir = 0, als = 150, ps = 57
ap3216c ir = 5, als = 41, ps = 225
ap3216c ir = 0, als = 380, ps = 133
ap3216c ir = 0, als = 29, ps = 1023
ap3216c ir = 20, als = 0, ps = 1023