参考资料:
1、Linux IIC 驱动分析 — 框架分析 - 知乎 (zhihu.com)
2、《Linux驱动开发指南》第十一章
3、《正点原子 I.MX6U嵌入式Linux驱动开发指南 V1.6》
4、《Linux设备驱动开发详解》
代码版本:Linux4.1.15
阅读本文需要先有一定的I2C基础以及Linux驱动基础。
I2C协议视频教程:DAY3-1 IIC总线概述_哔哩哔哩_bilibili
IIC接口的设备以一些传感器为多,如常见的AT24C02,I2C在硬件上比较简单,总线上只有两根数据线,数据线SDA和时钟线SCL,
但是到了Linux驱动就变得复杂一些,在Linux系统中,I2C驱动由3部分组成:I2C核心、I2C总线驱动、I2C设备驱动,按照Linux软硬件分离的思想,当硬件发生变化时,上层的控制逻辑能保持不变。
如下图所示,Linux驱动将硬件设备抽象成了一系列的结构体:
其中:
非红色部分:实际的I2C设备的硬件挂载情况
红色部分:Linux抽象出来的软件结构
结构体的表示如下:
1、i2c_adatper:描述一个物理的 I2C 控制器
2、i2c_algorithm:描述一个物理的I2C控制器的一组IIC控制器操作的集合,如特定SOC的IIC模块产生通信波形方法。
3、i2c_bus_type:描述IIC总线的结构体,根据Linux的设备、驱动、总线的思想,Linux设备应该通过Linux的I2C总线挂载上去,再通过固定的driver进行匹配。4、i2c_client:描述一个挂接到 I2C 总线上的具体物理设备,也是我们需要驱动的传感器
5、i2c_driver:用于描述一个 I2C 设备的驱动......
【Tip】在《Linux驱动开发指南》中,有另外的理解:适配器指的是主机,客户端指的是从机!这个解释会更加浅显易懂。在主机想要对从机进行读写操作时,需要先注册到总线上去,从机也是一样,需要注册到总线上,I2C系统也是遵从了platform设备模型的。
整个I2C系统的结构图如下:
架构是驱动的核心,I2C已经搭建了很完善的驱动了,Linux系统对I2C进行分层,分为硬件层、内核层、用户层。
用户层:通过标准的open、read、write、close调用IIC设备操作。
内核层:向上提供用户调用接口,核心为i2c_core,提供了总线、驱动、通信方式的注册和钩子函数的设置,起到一个承上启下的作用,driver、device、控制驱动程序我们都比较熟悉,那么什么是I2C子系统的核心(接口)呢?
硬件层:具体的硬件设备以及控制器。
前面我们提到了,在Linux系统中,I2C驱动由3部分组成:I2C核心、I2C总线驱动、I2C设备驱动。我们先简要介绍下三部分各是什么。
(1)I2C设备驱动【从机】:也叫客户驱动,是对I2C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU进行数据交互。I2C设备驱动主要包含数据结构:i2c_driver和i2c_client。
(2)I2C总线驱动【主机】:是对I2C适配器端的实现,适配器可以由CPU控制(甚至直接集成在CPU内部),I2C适配器的数据结构主要是i2c_adapter和、i2c_algorithm、以及控制I2C适配器产生通信信号的函数。
(3)I2C核心:提供了I2C总线驱动和设备驱动的注册、注销、通信方法(Algorithm)上层与具体适配器无关的代码以及其他上层代码。
上图为Linux下I2C的驱动框架,我们在实现时,需要使用具体的结构体进行描述,可以将其替换成下述的具体结构体!
驱动的核心在于其结构,了解了I2C的大致框架之后,我们再来看一下具体的结构体。I2C结构体之间会有相互联系,如下图所示。
这么多的结构体,那么我们需要优先或重点关注什么结构体呢?
1、i2c_adapter:一般是由半导体厂商编写的,比如I.MX6U的I2C适配器驱动(i2c_adatper)已经由NXP编写好了,不需要用户去编写。因此I2C总线驱动对于SOC使用者来说是被屏蔽的。
2、i2c_client、i2c_driver:用户需要专注于自己挂载在总线上的I2C设备驱动/从机(i2c_client、i2c_driver)。
3、i2c_core:不依赖硬件平台的接口函数,是I2C总线驱动和设备驱动的纽带,一般不需要修改,但是需要理解其中的主函数。如:增加/删除i2c_adapter、增加/删除i2c_driver、I2C传输,发送和接收。
因此,我们需要做的是先实现自己挂载的I2C设备的i2c_client和i2c_driver结构体。
就如上图所示,一个设备对应一个i2c_client,每检测到一个I2C设备就会给其分配一个i2c_client。其结构体如下图所示:
struct i2c_client {
unsigned short flags; /* div., see below */
#define I2C_CLIENT_PEC 0x04 /* Use Packet Error Checking */
#define I2C_CLIENT_TEN 0x10 /* we have a ten bit chip address */
/* Must equal I2C_M_TEN below */
#define I2C_CLIENT_SLAVE 0x20 /* we are the slave */
#define I2C_CLIENT_HOST_NOTIFY 0x40 /* We want to use I2C host notify */
#define I2C_CLIENT_WAKE 0x80 /* for board_info; true iff can wake */
#define I2C_CLIENT_SCCB 0x9000 /* Use Omnivision SCCB protocol */
/* Must match I2C_M_STOP|IGNORE_NAK */
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 init_irq; /* irq set at initialization */
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
void *devres_group_id; /* ID of probe devres group */
};
主要成员说明如下:
1、 addr : 设备地址,7bit2、 name : 设备名称,设备树I2C子节点中的节点名@前面便是I2C设备名3、 adapter : 适配器,实际上指的是主机,从机指的是客户端。4、 dev : 设备对象
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>;
};
};
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
int (*probe_new)(struct i2c_client *client);
void (*shutdown)(struct i2c_client *client);
void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol,
unsigned int data);
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver;
const struct i2c_device_id *id_table;
int (*detect)(struct i2c_client *client, struct i2c_board_info *info);
const unsigned short *address_list;
struct list_head clients;
};
我们主要关注的是probe、remove、shutdown、driver即可,其他的很少使用到。
probe:在驱动与设备匹配时,会执行probe函数,从而对该设备进行初始化和注册操作。
id_table:传统的、未使用设备树的设备匹配的ID表
device_driver:使用设备树的要,需要设置device_driver的id_table成员。
/* i2c 驱动结构体 */
static struct i2c_driver xxx_driver = {
.probe = xxx_probe,
.remove = xxx_remove,
.driver = {
.owner = THIS_MODULE,
.name = "xxx",
.of_match_table = xxx_of_match, /* 设备树匹配方式列表 */
},
.id_table = xxx_id, /* 传统匹配方式ID列表 */
}
/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx" },
{ /* Sentinel */ }
};
/* 驱动入口函数 */
static int __init xxx_init(void){
int ret = 0;
ret = i2c_add_driver(&xxx_driver);
return ret;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void){
i2c_del_driver(&xxx_driver);
}
上述是i2c_driver的部分模板,在编写完i2c_driver结构体之后,正如在驱动入口函数所见,我们可以使用i2c_register_driver或者i2c_add_driver向内核注册该I2C_driver,实际上是注册到I2C核心(i2c_core)。正如上述所说的,实际上是使用了i2c_core的核心函数,增加/删除i2c_adapter,我们稍后再对i2c_core进行介绍,我们先讲一下I2C总线的结构体。
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};
其中match函数就是总线的设备和驱动匹配函数,如设备树匹配、ACPI形式匹配等。
在上文提到,I2C核心很少需要被修改,但是我们需要了解他的主要函数和功能:
(1)增加/删除i2c_driver
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
int i2c_add_driver (struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
上述的注册与删除函数,我们可以在i2c_driver的驱动入口函数和出口函数见到。
(2)增加/删除i2c_adapter
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
void i2c_del_adapter(struct i2c_adapter * adap)
i2c_adapter的内容一般不需要我们实现,我们先跳过,放到最后学习。
(3)I2C传输、发送和接收
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int i2c_master_send(struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);
i2c_transfer用于I2C适配器与设备之间进行一组信息交互,其中第二个参数是指向一个i2c_msg数组的指针,所以i2c_transfer一次性可以传输多个i2c_msg。
对于时序简单的外设,i2c_master_send和i2c_master_recv会调用i2c_transfer分别完成一条写消息和读一条消息,但是i2c_transfer本身不具备驱动适配器物理硬件以完成信息交互的能力,只是寻找与i2c_adapter对应的i2c_algorithm,使用i2c_algorithm的x_fer()函数真正驱动硬件流程。
接下来,我们先了解一下,I2C设备是怎么发送与接收数据的。
Linux内核为I2C数据传输实现了两套通信方式:
(1)传统的I2C通信方式
(2)SMBus(System Management Bus)通信方式,是较为推荐的一种方式,在1995年Inter提出的,该总线与I2C基本一致,但是在I2C总线上进行了扩展。
但是!本文只讲解传统方式下的I2C数据传输。
Linux在内核提供了两个函数供给用户使用,即i2c_master_send和i2c_master_recv,前者发送数据,后者接收数据。上面已经给出了这两个函数具体的函数声明。
Linux内核还提供了一种传输方式,该函数既可以传输数据,也可以接收数据:i2c_transfer,但是,其参数已经不像前面两个函数一样,是简单的buf了,而是i2c_msg结构体。
struct i2c_msg {
__u16 addr;
__u16 flags;
#define I2C_M_RD 0x0001 /* guaranteed to be 0x0001! */
#define I2C_M_TEN 0x0010 /* use only if I2C_FUNC_10BIT_ADDR */
#define I2C_M_DMA_SAFE 0x0200 /* use only in kernel space */
#define I2C_M_RECV_LEN 0x0400 /* use only if I2C_FUNC_SMBUS_READ_BLOCK_DATA */
#define I2C_M_NO_RD_ACK 0x0800 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* use only if I2C_FUNC_NOSTART */
#define I2C_M_STOP 0x8000 /* use only if I2C_FUNC_PROTOCOL_MANGLING */
__u16 len;
__u8 *buf;
};
addr:I2C客户端地址,即从设备地址
flag:信息标志,即传输方向,0写数据,1(I2C_M_RD )读数据,其他标志位看上面代码的宏定义
len:表示数据传输长度
buf:所传输的数据的首地址
如下面的代码就实现了一个简单的数据发送过程。
int i2c_write_and_read_data(i2c_client *client, unsigned int *send_buf, unsigned int *recv_buf){
struct i2c_msg msgs[2];
msgs[0].addr = client->addr; //获取客户端(从机地址)
msgs[0].len = sizeof(send_buf);
msgs[0].buf = &send_buf;
msgs[0].flags = 0; //传输为发送数据
msgs[1].addr = client->addr; //获取客户端(从机地址)
msgs[1].len = 10;
msgs[1].buf = recv_buf;
msgs[1].flags = I2C_M_RD; //传输为接收数据
ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
if (ret < 0)
return ret;
else if (ret != ARRAY_SIZE(msgs))
return -EIO;
return 0;
}
而最后 i2c_transfer()函数会调用i2c_algorithm的master_xfer()函数和functionality()函数真正驱动硬件流程,i2c_transfer函数如下所示:
其中,functionality()函数很简单,返回algorithm所支持的通信协议,如I2C_FUNC_I2C、I2C_FUNC_10BIT_ADDR等。
master_xfer()函数在I2C适配器上完成传递给它的i2c_msg数组中的每个I2C消息。函数模板如下:
master_xfer()函数中的i2c_adapter_xxx_start()、i2c_adapter_xxx_setaddr()、i2c_adapter_xxx_wait_ack()、i2c_adapter_xxx_readbytes()、i2c_adapter_xxx_writebytes()都是和硬件直接相关的,需要工程师根据芯片手册来实现。
其实,上述所说到的functionality、master_xfer都是属于i2c_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 *);
};
可以看到,master_xfer 函数用于数据传送和读取,functionality 用来获取 IIC 控制器支持情况,(下面会讲到)在调用i2c_adapter的probe 的时候,已经将其操作集 i2c_algorithm 挂接到了 i2c_adapter 结构,一并注册到了 i2c 核心。那么我们就来看一下i2c_adapter的内容吧。
I2C适配器驱动其实就是I2C控制器驱动,一般都是由SOC厂商去编写。比如NXP就编写好了I.MX6U的I2C适配器驱动
Linux总线、设备、驱动模型实际上是一个树形结构,每个节点虽然可能成为别人的总控制器,都是自己也被认为是从上一级总线枚举出来的,所以I2C总控制器通常是在内存上,尽管它给别人提供了总线,都是它也被认为是接在platform总线上的一个客户,需要通过platform_driver和platform_device的匹配来执行。
/* I2C1 控制器节点 */
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = ;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
}
根据compatible值,我们在Linux源码里面可以找到对应的驱动文件:drivers/i2c/busses/i2c-imx.c
static struct platform_device_id imx_i2c_devtype[] = {
{
.name = "imx1-i2c",
.driver_data = (kernel_ulong_t)&imx1_i2c_hwdata,
}, {
.name = "imx21-i2c",
.driver_data = (kernel_ulong_t)&imx21_i2c_hwdata,
}, {
/* sentinel */
}
};
MODULE_DEVICE_TABLE(platform, imx_i2c_devtype);
static const struct of_device_id i2c_imx_dt_ids[] = {
{ .compatible = "fsl,imx1-i2c", .data = &imx1_i2c_hwdata, },
{ .compatible = "fsl,imx21-i2c", .data = &imx21_i2c_hwdata, },
{ .compatible = "fsl,vf610-i2c", .data = &vf610_i2c_hwdata, },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, i2c_imx_dt_ids);
......
static struct platform_driver i2c_imx_driver = {
.probe = i2c_imx_probe,
.remove = i2c_imx_remove,
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
.of_match_table = i2c_imx_dt_ids,
.pm = IMX_I2C_PM,
},
.id_table = imx_i2c_devtype,
};
static int __init i2c_adap_imx_init(void){
return platform_driver_register(&i2c_imx_driver);
}
subsys_initcall(i2c_adap_imx_init);
static void __exit i2c_adap_imx_exit(void){
platform_driver_unregister(&i2c_imx_driver);
}
module_exit(i2c_adap_imx_exit);
可以看出,I2C适配器就是一个标准的platform驱动。因此,当设备和驱动匹配之后,就会执行probe函数,我们可以在该函数中完成I2C适配器的初始化工作。主要完成以下工作:
1、初始化 i2c_adapter ,设置 i2c_algorithm 为 i2c_imx_algo ,最后向 Linux 内核注册i2c_adapter 。2、初始化 I2C1 控制器的相关寄存器。
static int i2c_imx_probe(struct platform_device *pdev){
irq = platform_get_irq(pdev, 0);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx),GFP_KERNEL);
/* Setup i2c_imx driver structure */
i2c_imx->adapter.owner = THIS_MODULE;
i2c_imx->adapter.algo = &i2c_imx_algo;
i2c_imx->adapter.dev.parent = &pdev->dev;
i2c_imx->adapter.nr = pdev->id;
i2c_imx->adapter.dev.of_node = pdev->dev.of_node;
i2c_imx->base = base;
/* Request IRQ */
ret = devm_request_irq(&pdev->dev, irq, i2c_imx_isr,
IRQF_NO_SUSPEND, pdev->name, i2c_imx);
/* Set up clock divider */
i2c_imx->bitrate = IMX_I2C_BIT_RATE;
ret = of_property_read_u32(pdev->dev.of_node,"clock-frequency", &i2c_imx->bitrate);
/* Set up chip registers to defaults */
imx_i2c_write_reg(i2c_imx->hwdata->i2cr_ien_opcode ^ I2CR_IEN,i2c_imx,IMX_I2C_I2CR);
imx_i2c_write_reg(i2c_imx->hwdata->i2sr_clr_opcode, i2c_imx,IMX_I2C_I2SR);
/* Add I2C adapter */
ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
/* Init DMA config if supported */
i2c_imx_dma_request(i2c_imx, phy_addr);
}
我们需要给驱动层留一个访问i2c的接口,所以有了i2c_dev,i2c 核心只是做了一些管理和提供接口的工作,那么具体的支撑用户层进行访问的算是这个 i2c_dev 了。接下来的内容就和字符设备驱动差不多了。
static int __init i2c_dev_init(void)
{
int res;
res = register_chrdev(I2C_MAJOR, "i2c", &i2cdev_fops);
i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");
res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);
/* Bind to already existing adapters right away */
i2c_for_each_dev(NULL, i2cdev_attach_adapter);
return 0;
}
同时提供了fops的用户层的操作集合:
static const struct file_operations i2cdev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = i2cdev_read,
.write = i2cdev_write,
.unlocked_ioctl = i2cdev_ioctl,
.open = i2cdev_open,
.release = i2cdev_release,
};
那么,比如在read函数中,就会使用到i2c_master_recv()函数
static ssize_t i2cdev_read(struct file *file, char __user *buf, size_t count, loff_t *offset){
......
ret = i2c_master_recv(client, tmp, count);
if (ret >= 0)
ret = copy_to_user(buf, tmp, count) ? -EFAULT : ret;
}
最后,我们再来看IIC框架的结构图,就会清晰很多了。
还没整理好,后续有空再补写。
-----------------2023.12.11-------------
驱动和QT都写好了,发现开发板上没有所需的引脚,还是先画下PCB底板再来。