文章目录
最近公司项目用到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_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 */
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_i2c1
status = "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 0x4001b8b0
MX6UL_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_add
cdev_init(&ap3216c_dev.m_cdev, &ap3216c_fops);
cdev_add(&ap3216c_dev.m_cdev, ap3216c_dev.devid, 1);
///3.class_create
ap3216c_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_del
cdev_del(&ap3216c_dev.m_cdev);
///注销设备号
unregister_chrdev(ap3216c_dev.devid, AP3216C_NAME);
///device_destroy
device_destroy(ap3216c_dev.m_class, ap3216c_dev.devid);
///class_desroy
class_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 reset
mdelay(20); ///< 最少等待10ms
value = 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 reset
printk("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通信时序