Linux下的I2C驱动框架以及代码实现

参考资料:

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硬件抽象出的软件结构

IIC接口的设备以一些传感器为多,如常见的AT24C02,I2C在硬件上比较简单,总线上只有两根数据线,数据线SDA和时钟线SCL,

但是到了Linux驱动就变得复杂一些,在Linux系统中,I2C驱动由3部分组成:I2C核心、I2C总线驱动、I2C设备驱动,按照Linux软硬件分离的思想,当硬件发生变化时,上层的控制逻辑能保持不变。

如下图所示,Linux驱动将硬件设备抽象成了一系列的结构体:

Linux下的I2C驱动框架以及代码实现_第1张图片 I2C硬件抽象出的软件结构体——知乎

其中:

非红色部分:实际的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核心、I2C总线驱动、I2C设备驱动

整个I2C系统的结构图如下:

Linux下的I2C驱动框架以及代码实现_第2张图片 I2C子系统结构图——《Linux驱动开发》

 架构是驱动的核心,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驱动框架以及代码实现_第3张图片 I2C体系结构——Linux设备驱动开发详解

 上图为Linux下I2C的驱动框架,我们在实现时,需要使用具体的结构体进行描述,可以将其替换成下述的具体结构体!

Linux下的I2C驱动框架以及代码实现_第4张图片 I2C系统结构图——知乎

 驱动的核心在于其结构,了解了I2C的大致框架之后,我们再来看一下具体的结构体。I2C结构体之间会有相互联系,如下图所示。

Linux下的I2C驱动框架以及代码实现_第5张图片 I2C驱动各种结构体的关系——Linux设备驱动开发详解

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_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 设备地址,7bit
2、  name 设备名称,设备树I2C子节点中的节点名@前面便是I2C设备名
3、  adapter 适配器,实际上指的是主机,从机指的是客户端。
4、  dev 设备对象
I2C客户端结构体比较简单,因为其规范很成熟了,特别是对于某些自带I2C控制器的传感器而言,I2C接口只是用来传输信息,用户要做的只是对寄存器进行读写而已。
I2C_Client代表一个物理的I2C设备,因此,我们需要在设备树中添加对应的设备节点,比如在I.MX6ULL的I2C控制器1下,增加一个I2C设备:mag3110磁力计,因此,我们需要在设备树文件下的&i2c1节点下进行添加:
 &i2c1 {
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    ap3216c@1e {
    compatible = "alientek,ap3216c";
    reg = <0x1e>;
    };
};

i2c_driver

该结构体是我们I2C设备驱动处理的重点,我们需要做的就是在构建该结构体之后,向Linux内核使用 i2c_register_driver进行注册。
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总线的结构体。

i2c_bus_type

我们知道I2C设备去驱动的匹配遵循了Linux的设备、总线、驱动原则,那么上述我们介绍了设备和驱动结构体,还需要总线的结构体。I2C设备和驱动的匹配过程是由I2C总线完成的,其结构体为i2c_bus_type。
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_core

在上文提到,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设备收发数据流程

接下来,我们先了解一下,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结构体。

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函数如下所示:

Linux下的I2C驱动框架以及代码实现_第6张图片 i2c_transfer()函数——Linux设备驱动开发详解

 其中,functionality()函数很简单,返回algorithm所支持的通信协议,如I2C_FUNC_I2C、I2C_FUNC_10BIT_ADDR等。

master_xfer()函数在I2C适配器上完成传递给它的i2c_msg数组中的每个I2C消息。函数模板如下:

Linux下的I2C驱动框架以及代码实现_第7张图片 master_xfer()函数——Linux设备驱动开发详解

 master_xfer()函数中的i2c_adapter_xxx_start()、i2c_adapter_xxx_setaddr()、i2c_adapter_xxx_wait_ack()、i2c_adapter_xxx_readbytes()、i2c_adapter_xxx_writebytes()都是和硬件直接相关的,需要工程师根据芯片手册来实现。

i2c_algorithm

其实,上述所说到的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_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_dev

我们需要给驱动层留一个访问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;
	
}

I2C结构体框图与调用流程

最后,我们再来看IIC框架的结构图,就会清晰很多了。

Linux下的I2C驱动框架以及代码实现_第8张图片 IIC大致结构体之间的注册和初始化——知乎

Linux下的I2C驱动框架以及代码实现_第9张图片 用户层调用流程——知乎

Linux下IIC驱动代码实践

还没整理好,后续有空再补写。

-----------------2023.12.11-------------

驱动和QT都写好了,发现开发板上没有所需的引脚,还是先画下PCB底板再来。

你可能感兴趣的:(Linux驱动,嵌入式,IIC协议,Linux驱动开发)