文章目录
最近公司项目用到I2C陀螺仪,之前也学习过I2C子系统这块,但是稍微淡忘了些,所以特地来补一下这块,顺便整理成博客。
自己画的思维导图:
关于I2C通信时序,可用直接看第三章节中的链接
1.I2C驱动框架简介
IIC驱动框架,也是一个标准的platform驱动,其中分为I2C总线驱动和I2C设备驱动
I2C总线驱动:( 适配器驱动 )一般是原厂维护,主要是提供读写等API
I2C设备驱动:是针对具体I2C设备所编写的驱动
IIC总线驱动也遵循驱动、设备、总线的匹配规则,设备和驱动匹配成功后,probe函数便会执行,probe函数在i2c_driver中,我们主要需要实现的也是driver这部分。
其中i2c_device_match完成IIC总线的设备和驱动匹配。
i2c_client:描述设备信息
i2c_driver:描述驱动信息
i2c_adapter:I2C总线驱动
这里借用一张其他地方看到的图,描述的比较清晰
1.1 I2C总线驱动(适配器驱动)
i2c总线驱动一般由原厂完成,主要工作是初始化i2c_adapter结构体,设置i2c_algorithm中的master_xfer函数(i2c适配器传输函数,该函数完成IIC通信),再通过i2c_add_adapter向系统注册i2c_adapter结构体。
1.1.1 重要结构体
i2c_adapter:i2c适配器
struct i2c_adapter {struct module *owner;unsigned int class; /* classes to allow probing for */const struct i2c_algorithm *algo; /* the algorithm to access the bus */void *algo_data;/* data fields that are valid for all devices */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;struct list_head userspace_clients;struct i2c_bus_recovery_info *bus_recovery_info;const struct i2c_adapter_quirks *quirks;
};
ii2c_algorithm:总线访问算法
struct i2c_algorithm {/* If an adapter algorithm can't do I2C-level access, set master_xferto NULL. If an adapter algorithm can do SMBus access, setsmbus_xfer. If set to NULL, the SMBus protocol is simulatedusing common I2C messages *//* master_xfer should return the number of messages successfullyprocessed, or a negative value on error */int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);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 *);};
functionality:返回I2C适配器支持什么样的通信协议
1.1.2 重要函数
/// 向Linux内核添加i2c适配器
/// 返回值0成功,负值失败
int i2c_add_adapter(struct i2c_adapter *adapter) /// 动态总线号
int i2c_add_numbered_adapter(struct i2c_adapter *adap) /// 静态总线号
/// 删除Linux内核适配器
void i2c_del_adapter(struct i2c_adapter * adap)
1.2 I2C设备驱动
1.2.1 重要结构体
i2c_client:描述设备信息,每检测到一个i2c设备,分配一个i2c_client
struct i2c_client {unsigned short flags; /* div., see below */unsigned short addr; /* chip address - NOTE: 7bit *//* addresses are stored in the *//* _LOWER_ 7 bits */char name[I2C_NAME_SIZE];struct i2c_adapter *adapter; /* the adapter we sit on */struct device dev; /* the device structure */int irq; /* irq issued by device */struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};
i2c_driver:描述驱动信息,重点处理的结构体,其中包括probe函数,remove函数等
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").*/void (*alert)(struct i2c_client *, 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_msg:I2C传参结构体,用于i2c_transfer传输数据
device_driver:其中包含设备树匹配方式of_match_table(compatible属性在其中),以及acpi_match_table
id_table:未使用设备树的设备匹配ID表
1.2.2 重要函数
#define i2c_add_driver(driver) \i2c_register_driver(THIS_MODULE, driver)
/// 注册i2c driver,当已经注册过时,需要先进行del
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);
/// 注销i2c_driver
void i2c_del_driver(struct i2c_driver *driver)
/// 发送I2C数据
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
adap:所使用的 I2C 适配器,i2c_client 会保存其对应的 i2c_adapter。
msgs:I2C 要发送的一个或多个消息。
num:消息数量,也就是 msgs 的数量。
返回值:负值,失败,其他非负值,发送的 msgs 数量。
其中i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数,i2c驱动具有中间大两边小的特点。中间你无论是哪个厂家,无论你使用什么算法,最终提供给驱动开发人员的接口一定是i2c_transfer()接口,向下使用的文件一定是i2c-algo-bit.c文件中的函数产生波形
1.3 I2C设备和驱动匹配过程
I2C设备和驱动的匹配过程由I2C总线来完成,drivers/i2c/i2c-core.c、i2c_bus_type是i2c总线的核心部分,调用其中的i2c_device_match函数完成匹配。
struct bus_type i2c_bus_type = {.name = "i2c",.match = i2c_device_match,.probe = i2c_device_probe,.remove = i2c_device_remove,.shutdown = i2c_device_shutdown,
};static int i2c_device_match(struct device *dev, struct device_driver *drv)
{struct i2c_client *client = i2c_verify_client(dev);struct i2c_driver *driver;if (!client)return 0;/* Attempt an OF style match */if (of_driver_match_device(dev, drv))return 1;/* Then ACPI style match */if (acpi_driver_match_device(dev, drv))return 1;driver = to_i2c_driver(drv);/* match on an id table if there is one */if (driver->id_table)return i2c_match_id(driver->id_table, client) != NULL;return 0;
}
依旧是匹配三部曲:设备树compatible属性匹配->acpi形式匹配->无设备树的id_table name属性方式匹配
2.I2C设备驱动编写
现在主流基本都会使用设备树来管理,如果不使用设备树的话,则是借助i2c_board_info结构体来描述I2C设备信息,当使用设备树则按照下面的步骤,一步步完成一个I2C设备驱动编写/适配!
2.1 确认原理图引脚及pinctrl子系统引脚配置信息
这里我使用的I2C设备是AP3216C,一款光感sensor,原理图如下。
其中sensor使用的是I2C1_SCL以及I2C1_SDA,所以先确定这两个引脚的引脚复用,也就是pinctrl子系统配置是否正确。
&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>; ///< pinctrl设备节点为pinctrl_i2c1status = "okay";..... //省略
}
在imx6ull-14x14-emmc-4.3-480x272-c.dts设备树文件下搜索后能看到引脚复用信息如下,已经复用为I2C1_SCL以及I2C1_SDA,所以不用修改。而如果这里的引脚复用未配置,则需要进行修改,需根据自己的实际情况。
pinctrl_i2c1: i2c1grp {fsl,pins = <MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0>;
};
2.2 确认设备树I2C节点信息
&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";codec: wm8960@1a {compatible = "wlf,wm8960";reg = <0x1a>;clocks = <&clks IMX6UL_CLK_SAI2>;clock-names = "mclk";wlf,shared-lrclk;};mag3110@0e {compatible = "fsl,mag3110";reg = <0x0e>;position = <2>;status = "disabled";};ap3216c@1e {compatible = "alientek,ap3216c";reg = <0x1e>;};
};
添加ap3216c节点信息,ap3216c@1e是节点名字,0e/1e这种则是I2C器件地址,compatibel属性为ap3216c,用这个属性进行设备和驱动的匹配,reg属性也是I2C器件地址,时钟频率为100KHz。
如果设备树编写的没问题,在替换完dtb文件到开发板后,能cat到相应的属性。这里我是从虚拟机拷贝dtb文件到板子的emmc上,具体操作需要根据情况而定。
scp imx6ull-14x14-emmc-4.3-480x272-c.dtb root@169.254.113.91:/run/media/mmcblk1p1
root@ATK-IMX6U:/run/media/mmcblk1p1# cat /sys/bus/i2c/devices/0-001e/name
ap3216c
其中001e是设备树中器件的节点地址,name为ap3216c
2.3 编写主体框架代码
主要是实现module_init、module_exit、probe函数、remove函数、以及file_operations操作函数等,这个几乎所有驱动框架是通用的
2.4 实现module_init、exit
在module_init时,进行i2c_add_driver,exit时,则是进行i2c_del_driver的操作
/// 传统方式匹配
static const struct i2c_device_id ap3216c_id[] = {{"alientek,ap3216c", 0}, {}
};/// 设备树方式匹配
static const struct of_device_id ap3216c_of_match[] = {{.compatible = "alientek,ap3216c"}
};static struct i2c_driver ap3216c_driver = {.probe = ap3216c_probe,.remove = ap3216c_remove,.driver = {.owner = THIS_MODULE,.name = "ap3216c",.of_match_table = ap3216c_of_match,},.id_table = ap3216c_id,
};static int __init ap3216c_init(void)
{int ret = 0;printk("ap3216c driver init begin\n");ret = i2c_add_driver(&ap3216c_driver);printk("ap3216c driver init done, ret:%d\n", ret);return ret;
}static int __exit ap3216c_exit(void)
{printk("ap3216c driver exit begin\n"); i2c_del_driver(&ap3216c_driver);printk("ap3216c driver exit done\n"); return 0;
}module_init(ap3216c_init);
module_exit(ap3216c_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("xuzhangxin");
2.5 实现probe、remove函数
probe中进行申请设备号,cdev_init、cdev_add、class_create、device_create等
static struct file_operations ap3216c_fops = {.owner = THIS_MODULE,.open = ap3216c_open,.release = ap3216c_close,.read = ap3216c_read,
};int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{printk("ap3216c driver probe begin\n");///1.申请设备号if (ap3216c_dev.major) {ap3216c_dev.devid = MKDEV(ap3216c_dev.major, 0);register_chrdev_region(ap3216c_dev.devid, 1, AP3216C_NAME);} else {alloc_chrdev_region(&ap3216c_dev.devid, 0, 1, AP3216C_NAME);ap3216c_dev.major = MAJOR(ap3216c_dev.devid);}///2.cdev_init、cdev_addcdev_init(&ap3216c_dev.m_cdev, &ap3216c_fops);cdev_add(&ap3216c_dev.m_cdev, ap3216c_dev.devid, 1);///3.class_createap3216c_dev.m_class = class_create(THIS_MODULE, AP3216C_NAME);if (IS_ERR(ap3216c_dev.m_class)) {return PTR_ERR(ap3216c_dev.m_class);}///4.device_create ap3216c_dev.m_dev = device_create(ap3216c_dev.m_class, NULL, ap3216c_dev.devid, NULL, AP3216C_NAME);if (IS_ERR(ap3216c_dev.m_dev)) {return PTR_ERR(ap3216c_dev.m_dev);}ap3216c_dev.client = client;printk("ap3216c driver probe done\n");return 0;
}int ap3216c_remove(struct i2c_client *client)
{printk("ap3216c driver remove begin\n");///cdev_delcdev_del(&ap3216c_dev.m_cdev);///注销设备号unregister_chrdev(ap3216c_dev.devid, AP3216C_NAME);///device_destroydevice_destroy(ap3216c_dev.m_class, ap3216c_dev.devid);///class_desroyclass_destroy(ap3216c_dev.m_class);printk("ap3216c driver remove done\n");return 0;
}
2.6 实现open、close、read等操作函数
从这边开始的话,就要去看器件相应的寄存器手册了,这里我将整个的思路都尽可能写清楚,强化自己也方便大家,不需要了解这么详细的话也可以直接看下面的源码。
1)实现写i2c数据接口
寄存器手册中,有描述该器件I2C 写数据的协议。
首先是发送1bit开始信号+7bit的slave address(从设备地址),
此时从设备回1 bitACK,主设备继续发送8bit register address(寄存器地址),每收到一个字节数据,从设备拉低数据线产生一个应答,最后主设备再发送停止位
根据这里的i2c时序,可以先封装出一个i2c_write_reg的函数,用于设置System Configuration寄存器
static int ap3216c_write_reg(struct ap3216c_dev_t *dev, u8 reg, u8 *data, u16 len)
{int ret = 0;struct i2c_client* client = (struct i2c_client*)dev->client;struct i2c_msg msg;msg.addr = client->addr; ///< 设备地址msg.flags = 0; ///< 写数据msg.buf[0] = reg; ///< 操作寄存器地址memcpy(&msg.buf[1], data, len); ///< 写寄存器的数据msg.len = len + 1 ; ///< 1byte寄存器地址+data的长度 return i2c_transfer(client->adapter, &msg, 1);
}
2)实现读i2c数据接口
1bit开始信号+7bit的slave address(从设备地址)+1bit写数据位,此时从设备拉低数据线,给一个应答位,再发送8it的reg addr寄存器地址,从设备应答,主设备再次发送1bit的开始信号+7bit slave address+1bit读数据位+8bit寄存器地址,最后主设备作为接收方,收到最后一个字节数据后,主设备拉高SDA发送一个NACK信号,通知发送端结束数据发送,最后发送一个停止信号,完成数据的读取
static int ap3216c_read_reg(struct ap3216c_dev_t *dev, u8 reg, u8 *data, u16 len)
{int ret = 0;struct i2c_client* client = (struct i2c_client*)dev->client;struct i2c_msg msg[2];msg[0].addr = client->addr; ///< 设备地址msg[0].flags = 0; ///< 写数据msg[0].buf = ® ///< 操作寄存器地址msg[0].len = 1 ; ///< 1byte寄存器地址 msg[1].addr = client->addr; ///< 设备地址msg[1].flags = I2C_M_RD; ///< 读数据msg[1].buf = data; ///< 读取数据缓冲区msg[1].len = len; ///< 读取数据长度 return i2c_transfer(client->adapter, &msg, 2);
}
3)实现fops->open接口,接口中配置设备工作模式
主要看我们需要采集哪些数据,来决定配置的模式
首先找到System Configuration寄存器,这个寄存器用于配置器件的工作模式,bit0-bit2有效,bit3-bit7无效
关于它的不同工作模式,我找到的这篇博客:https://developer.aliyun.com/article/1083178
直接选用ALS+PS+IR 模式,同时对光强度及接近程度测量,也就是把mode配置为011。
另外根据寄存器手册中的说明,设置完SW reset后,需要等待10ms,再设置真正的工作模式
所以最终open的代码如下:
static int ap3216c_open(struct inode *node, struct file *file)
{u8 value = 0x00;printk("ap3216c driver open\n");file->private_data = (void *)&ap3216c_dev; ///< 设置私有数据ap3216c_write_reg(file->private_data, AP3216C_SYSTEMCONG, &value, 1); ///< set sw resetmdelay(20); ///< 最少等待10msvalue = 0x03;ap3216c_write_reg(file->private_data, AP3216C_SYSTEMCONG, &value, 1); ///< 设置为ALS and PS+IR模式printk("ap3216c driver open success\n");return 0;
}
4)实现fpos->close接口
static int ap3216c_close(struct inode *node, struct file *file)
{u8 value = 0x00;printk("ap3216c driver close\n");file->private_data = (void *)&ap3216c_dev; ///< 设置私有数据ap3216c_write_reg(file->private_data, AP3216C_SYSTEMCONG, &value, 1); ///< set sw resetprintk("ap3216c driver close success\n");return 0;
}
5)实现fops->read接口
读以下几个寄存器,获取IR、ALS、PS寄存器的值,其中若IR Data Low寄存器中bit7的值为1,则代表IR数据无效,否则代表数据有效。
所以这里读出IR Data Low寄存器的值val & 0x80,判断最高位是否为1,为1则IR寄存器的数据无效,为0则数据有效。PS寄存器同样也有这种机制,判断方法是一样的
static ssize_t ap3216c_read(struct file *file, char __user *buf, size_t cnt, loff_t *off)
{u8 data[3] = {0};u8 read_buf[AP3216C_DATA_REG_NUM] = {0};u8 i = 0;int ret = 0;printk("ap3216c driver read\n");file->private_data = (void *)&ap3216c_dev; ///< 设置私有数据/// 循环将IR、ALS、PS中的寄存器数据读取for (i = 0; i < AP3216C_DATA_REG_NUM; i++) {ret = ap3216c_read_reg(file->private_data, AP3216C_IRDATALOW + i, read_buf, 6);printk("ap3216c driver read ret:%d, reg:%x, value:%d\n", ret, AP3216C_IRDATALOW + i, read_buf[i]);}if (read_buf[0] & 0x80) {data[0] = 0;printk("ap3216c driver read ir data error\n");} else {data[0] = (read_buf[1] << 2) | read_buf[0];printk("ap3216c driver read ir data success, data:%x\n", data[0]);}data[1] = (read_buf[3] << 8) | read_buf[2];printk("ap3216c driver read als data success, data:%x\n", data[1]);if (read_buf[4] & 0x80) {data[2] = 0;printk("ap3216c driver read ps data error\n");} else {data[2] = ((read_buf[5] & 0x3f) << 4) | (read_buf[4] & 0x0f);printk("ap3216c driver read ir data success, data:%x\n", data[0]);}printk("ap3216c driver read als data success, data:%x\n", data[1]);if (copy_to_user(buf, data, sizeof(data) / sizeof(data[0])) != 0) {return -1;}printk("ap3216c driver read sucess, ret:%d\n", ret);return ret;
}
2.7 测试
这里写了一个测试代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"int main(int argc, char **argv)
{char r_buf[3] = {0};int count = 100;int fd = open("/dev/ap3216c", O_RDWR);if (fd < 0){printf("open ap3216c failed\n");return -1;}printf("open ap3216c success\n");while (count--) {read(fd, r_buf, sizeof(r_buf) / sizeof(r_buf[0]));printf("read ap3216c ir:%d\n", r_buf[0]);printf("read ap3216c als:%d\n", r_buf[1]);printf("read ap3216c ps:%d\n", r_buf[2]);usleep(200 * 1000);}close(fd);return 0;
}
insmod i2c_ap3216.ko
./test
3.i2c通信时序
这里暂时自己还没有写相关博客,可以参考下其他人写的
I2C通信时序