Linux驱动开发之I2C体系

 

体系概述

I2C(Inter-Integrated Circuit)是一种串行通信协议,用于在芯片之间进行通信。Linux内核支持I2C总线,可以通过I2C总线访问连接到系统的各种设备,如温度传感器、加速度计、数字电压计等。

在Linux内核中,I2C体系由以下几个部分组成:

  1. I2C核心代码:I2C核心提供了I2C总线驱动和设备驱动的注册、注销方法,I2C通信方法(即Algorithm)上层的与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等

  2. I2C总线驱动程序:I2C总线驱动程序负责管理I2C总线的物理层和数据链路层,实现I2C总线的读写操作。I2C总线驱动主要包含I2C适配器数据结构i2c_adapter、I2C适配器的Algorithm数据结构i2c_algorithm和控制I2C适配器产生通信信号的函数。

  3. I2C设备驱动程序:I2C设备驱动(也称为客户驱动)是对I2C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。I2C设备驱动主要包含数据结构i2c_driver和i2c_client,我们需要根据具体设备实现其中的成员函数。

体系

核心层

在Linux 2.6内核中,所有的I2C设备都在sysfs文件系统中显示,存于/sys/bus/i2c/目录下,以适配器地址和芯片地址的形式列出,例如:


在Linux内核源代码中的drivers目录下有一个i2c目录,而在i2c目录下又包含如下文件和文件夹。

  1. i2c-core.c这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口。

  2. i2c-dev.c实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备。通过适配器访问设备时的主设备号都为89,次设备号为0~255。应用程序通过“i2c-%d”(i2c-0,i2c-1,…,i2c-10,…)文件名并使用文件操作接口open()、write()、read()、ioctl()和close()等来访问这个设备。i2c-dev.c并不是针对特定的设备而设计的,只是提供了通用的read()、write()和ioctl()等接口,应用层可以借用这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。

  3. busses文件夹这个文件包含了一些I2C主机控制器的驱动,如i2c-tegra.c、i2c-omap.c、i2c-versatile.c、i2c-s3c2410.c等。

  4. algos文件夹实现了一些I2C总线适配器的通信方法。

内核中的include/linux/i2c.h头文件对i2c_adapter、i2c_algorithm、i2c_driver和i2c_client这4个数据结构进行了定义。

 1 struct i2c_adapter {
 2        struct module *owner;
 3        unsigned int class;                 /* classes to allow probing for */
 4        const struct i2c_algorithm *algo;   /* the algorithm to access the bus */
 5        void *algo_data;
 6
 7        /* data fields that are valid for all devices   */
 8        struct rt_mutex bus_lock;
 9
10        int timeout;                        /* in jiffies */
11        int retries;
12        struct device dev;                  /* the adapter device */
13
14        int nr;
15        char name[48];
16        struct completion dev_released;
17
18        struct mutex userspace_clients_lock;
19        struct list_head userspace_clients;
20
21        struct i2c_bus_recovery_info *bus_recovery_info;
22 };
 1 struct i2c_algorithm {
 2        /* If an adapter algorithm can't do I2C-level access, set master_xfer
 3           to NULL. If an adapter algorithm can do SMBus access, set
 4           smbus_xfer. If set to NULL, the SMBus protocol is simulated
 5           using common I2C messages */
 6        /* master_xfer should return the number of messages successfully
 7           processed, or a negative value on error */
 8        int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
 9                           int num);
10        int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
11                        unsigned short flags, char read_write,
12                        u8 command, int size, union i2c_smbus_data *data);
13
14        /* To determine what the adapter supports */
15        u32 (*functionality) (struct i2c_adapter *);
16 };
 1 struct i2c_driver {
 2        unsigned int class;
 3
 4        /* Notifies the driver that a new bus has appeared. You should avoid
 5         * using this, it will be removed in a near future.
 6         */
 7        int (*attach_adapter)(struct i2c_adapter *) __deprecated;
 8
 9        /* Standard driver model interfaces */
10        int (*probe)(struct i2c_client *, const struct i2c_device_id *);
11        int (*remove)(struct i2c_client *);
12
13        /* driver model interfaces that don't relate to enumeration  */
14        void (*shutdown)(struct i2c_client *);
15        int (*suspend)(struct i2c_client *, pm_message_t mesg);
16        int (*resume)(struct i2c_client *);
17
18        /* Alert callback, for example for the SMBus alert protocol.
19         * The format and meaning of the data value depends on the protocol.
20         * For the SMBus alert protocol, there is a single bit of data passed
21         * as the alert response's low bit ("event flag").
22         */
23        void (*alert)(struct i2c_client *, unsigned int data);
24
25        /* a ioctl like command that can be used to perform specific functions
26         * with the device.
27         */
28        int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
29
30        struct device_driver driver;
31        const struct i2c_device_id *id_table;
32
33        /* Device detection callback for automatic device creation */
34        int (*detect)(struct i2c_client *, struct i2c_board_info *);
35        const unsigned short *address_list;
36        struct list_head clients;
37 };
 1 struct i2c_client {
 2        unsigned short flags;         /* div., see below            */
 3        unsigned short addr;          /* chip address - NOTE: 7bit    */
 4                                      /* addresses are stored in the  */
 5                                      /* _LOWER_ 7 bits             */
 6        char name[I2C_NAME_SIZE];
 7        struct i2c_adapter *adapter;  /* the adapter we sit on        */
 8        struct device dev;            /* the device structure         */
 9        int irq;                      /* irq issued by device         */
10        struct list_head detected;
11 };

下面分析i2c_adapter、i2c_algorithm、i2c_driver和i2c_client这4个数据结构的作用及其盘根错节的关系。

  1. 下面分析i2c_adapter、i2c_algorithm、i2c_driver和i2c_client这4个数据结构的作用及其盘根错节的关系。

    i2c_adapter对应于物理上的一个适配器,而i2c_algorithm对应一套通信方法。一个I2C适配器需要i2c_algorithm提供的通信函数来控制适配器产生特定的访问周期。缺少i2c_algorithm的i2c_adapter什么也做不了,因此i2c_adapter中包含所使用的i2c_algorithm的指针。

    i2c_algorithm中的关键函数master_xfer()用于产生I2C访问周期需要的信号,以i2c_msg(即I2C消息)为单位。i2c_msg结构体也是非常重要的,它定义于include/uapi/linux/i2c.h(在uapi目录下,证明用户空间的应用也可能使用这个结构体)中,代码清单15.5给出了它的定义,其中的成员表明了I2C的传输地址、方向、缓冲区、缓冲区长度等信息。

     1 struct i2c_msg {
     2        __u16 addr;                     /* slave address          */
     3        __u16 flags;
     4 #define I2C_M_TEN             0x0010   /* this is a ten bit chip address */
     5 #define I2C_M_RD              0x0001   /* read data, from slave to master */
     6 #define I2C_M_STOP            0x8000   /* if I2C_FUNC_PROTOCOL_MANGLING */
     7 #define I2C_M_NOSTART         0x4000   /* if I2C_FUNC_NOSTART */
     8 #define I2C_M_REV_DIR_ADDR    0x2000   /* if I2C_FUNC_PROTOCOL_MANGLING */
     9 #define I2C_M_IGNORE_NAK      0x1000   /* if I2C_FUNC_PROTOCOL_MANGLING */
     10 #define I2C_M_NO_RD_ACK       0x0800   /* if I2C_FUNC_PROTOCOL_MANGLING */
     11 #define I2C_M_RECV_LEN        0x0400   /* length will be first received byte */
     12         __u16 len;                     /* msg length                 */
     13         __u8 *buf;                     /* pointer to msg data          */
     14 };
    
  2. i2c_driver与i2c_client

    i2c_driver对应于一套驱动方法,其主要成员函数是probe()、remove()、suspend()、resume()等,另外,struct i2c_device_id形式的id_table是该驱动所支持的I2C设备的ID表。i2c_client对应于真实的物理设备,每个I2C设备都需要一个i2c_client来描述。i2c_driver与i2c_client的关系是一对多,一个i2c_driver可以支持多个同类型的i2c_client。

  3. i2c_adpater与i2c_client

    i2c_adpater与i2c_client的关系与I2C硬件体系中适配器和设备的关系一致,即i2c_client依附于i2c_adpater。由于一个适配器可以连接多个I2C设备,所以一个i2c_adpater也可以被多个i2c_client依附,i2c_adpater中包括依附于它的i2c_client的链表。

假设I2C总线适配器xxx上有两个使用相同驱动程序的yyy I2C设备,在打开该I2C总线的设备节点后,相关数据结构之间的逻辑组织关系将如下所示。

guanxi

当工程师拿到实际的电路板时,面对复杂的Linux I2C子系统,应该如何下手写驱动呢?

一方面,适配器驱动可能是Linux内核本身还不包含的;另一方面,挂接在适配器上的具体设备驱动可能也是Linux内核还不包含的。因此,工程师要实现的主要工作如下。

  1. 提供I2C适配器的硬件驱动,探测、初始化I2C适配器(如申请I2C的I/O地址和中断号)、驱动CPU控制的I2C适配器从硬件上产生各种信号以及处理I2C中断等。

  2. 提供I2C适配器的Algorithm,用具体适配器的xxx_xfer()函数填充i2c_algorithm的master_xfer指针,并把i2c_algorithm指针赋值给i2c_adapter的algo指针。

  3. 实现I2C设备驱动中的i2c_driver接口,用具体设备yyy的yyy_probe()、yyy_remove()、yyy_suspend()、yyy_resume()函数指针和i2c_device_id设备ID表赋值给i2c_driver的probe、remove、suspend、resume和id_table指针。

  4. 实现I2C设备所对应类型的具体驱动,i2c_driver只是实现设备与总线的挂接,而挂接在总线上的设备则千差万别。例如,如果是字符设备,就实现文件操作接口,即实现具体设备yyy的yyy_read()、yyy_write()和yyy_ioctl()函数等;如果是声卡,就实现ALSA驱动。

上述工作中前两个属于I2C总线驱动,后两个属于I2C设备驱动。实际上前两个工作已经由芯片厂商做好了,我们主要做后两个工作

设备驱动

以正点原子的ap3216c为例

修改设备树

IO 修改或添加

首先肯定是要修改 IO,AP3216C 用到了 I2C1 接口,I.MX6U-ALPHA 开发板上的 I2C1 接 口使用到了 UART4_TXD 和 UART4_RXD,因此肯定要在设备树里面设置这两个 IO。如果要用 到 AP3216C 的中断功能的话还需要初始化 AP_INT 对应的 GIO1_IO01 这个 IO,本章实验我们 不使用中断功能。因此只需要设置 UART4_TXD 和 UART4_RXD 这两个 IO,NXP 其实已经将 他这两个 IO 设置好了,打开 imx6ull-alientek-emmc.dts,然后找到如下内容: 示例代码 61.5.1.1 pinctrl_i2c1 子节点

pinctrl_i2c1: i2c1grp {
    fsl,pins = <
        MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
        MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
    >;
};

pinctrl_i2c1 就是 I2C1 的 IO 节点,这里将 UART4_TXD 和 UART4_RXD 这两个 IO 分别 复用为 I2C1_SCL 和 I2C1_SDA,电气属性都设置为 0x4001b8b0。

在 i2c1 节点追加 ap3216c 子节点

AP3216C 是连接到 I2C1 上的,因此需要在 i2c1 节点下添加 ap3216c 的设备子节点,在 imx6ull-alientek-emmc.dts 文件中找到 i2c1 节点,此节点默认内容如下:

1 &i2c1 {
2     clock-frequency = <100000>;
3     pinctrl-names = "default";
4     pinctrl-0 = <&pinctrl_i2c1>;
5     status = "okay";
6 
7     mag3110@0e {
8         compatible = "fsl,mag3110";
9         reg = <0x0e>;
10        position = <2>;
11    };
12
13    fxls8471@1e {
14        compatible = "fsl,fxls8471";
15        reg = <0x1e>;
16        position = <0>;
17        interrupt-parent = <&gpio5>;
18        interrupts = <0 8>;
19    };
20 };

第 2 行,clock-frequency 属性为 I2C 频率,这里设置为 100KHz。 第 4 行,pinctrl-0 属性指定 I2C 所使用的 IO 为示例代码 61.5.1.1 中的 pinctrl_i2c1 子节 点. 第 7~11 行,mag3110 是个磁力计,NXP 官方的 EVK 开发板上接了 mag3110,因此 NXP 在 i2c1 节点下添加了 mag3110 这个子节点。正点原子的 I.MX6U-ALPHA 开发板上没有用到 mag3110,因此需要将此节点删除掉。 第 13~19 行,NXP 官方 EVK 开发板也接了一个 fxls8471,正点原子的 I.MX6U-ALPHA 开发板同样没有此器件,所以也要将其删除掉

diff --git a/arch/arm/boot/dts/imx6ull-alientek-emmc.dts b/arch/arm/boot/dts/imx6ull-alientek-emmc.dts
index de41a93d..6cabc4b4 100644
--- a/arch/arm/boot/dts/imx6ull-alientek-emmc.dts
+++ b/arch/arm/boot/dts/imx6ull-alientek-emmc.dts
@@ -297,18 +297,9 @@
        pinctrl-0 = <&pinctrl_i2c1>;
        status = "okay";
 
-       mag3110@0e {
-               compatible = "fsl,mag3110";
-               reg = <0x0e>;
-               position = <2>;
-       };
-
-       fxls8471@1e {
-               compatible = "fsl,fxls8471";
+       ap3216c@1e {
+               compatible = "alientek,ap3216c";
                reg = <0x1e>;
-               position = <0>;
-               interrupt-parent = <&gpio5>;
-               interrupts = <0 8>;
        };
 };

设备驱动代码

#include "ap3216c_reg.h"
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/types.h>

#define AP3216C_NAME "ap3216c"

struct ap3216c_dev {
    struct miscdevice miscdev;
    void *private_data;
    unsigned short ir, als, ps;
};
static struct ap3216c_dev ap3216cdev;

void ap3216c_readdata(struct ap3216c_dev *dev)
{
    unsigned char buf[6];

    struct i2c_client *client = (struct i2c_client *)dev->private_data;
    i2c_smbus_read_i2c_block_data(client, AP3216C_IRDATALOW, 6, buf);

    if (buf[0] & 0X80) /* IR_OF位为1,则数据无效 */
        dev->ir = 0;
    else /* 读取IR传感器的数据 */
        dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);

    dev->als = ((unsigned short)buf[3] << 8) | buf[2]; /* 读取ALS传感器的数据 */

    if (buf[4] & 0x40) /* IR_OF位为1,则数据无效 */
        dev->ps = 0;
    else /* 读取PS传感器的数据    */
        dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}

static int ap3216c_open(struct inode *inode, struct file *filp)
{
    struct i2c_client *client = NULL;
    filp->private_data = &ap3216cdev;
    client = (struct i2c_client *)ap3216cdev.private_data;
    /* 初始化AP3216C */
    i2c_smbus_write_byte_data(client, AP3216C_SYSTEMCONG, 0x04); /* 复位AP3216C */
    mdelay(50); /* AP3216C复位最少10ms */
    i2c_smbus_write_byte_data(client, AP3216C_SYSTEMCONG, 0X03); /* 开启ALS、PS+IR */
    return 0;
}

static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    short data[3];
    long err = 0;

    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;
}

static int ap3216c_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* AP3216C操作函数 */
static const struct file_operations ap3216c_ops = {
    .owner = THIS_MODULE,
    .open = ap3216c_open,
    .read = ap3216c_read,
    .release = ap3216c_release,
};

static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret = 0;
    printk("%s(): probe enter\n", __func__);
    ap3216cdev.miscdev.minor = MISC_DYNAMIC_MINOR;
    ap3216cdev.miscdev.name = AP3216C_NAME;
    ap3216cdev.miscdev.fops = &ap3216c_ops;
    ret = misc_register(&ap3216cdev.miscdev);
    ap3216cdev.private_data = client;
    i2c_set_clientdata(client, &ap3216cdev);
    printk("%s(): probe exit\n", __func__);
    return ret;
}

static int ap3216c_remove(struct i2c_client *client)
{
    struct ap3216c_dev *dev = i2c_get_clientdata(client);
    misc_deregister(&dev->miscdev);
    return 0;
}

// clang-format off
static const struct i2c_device_id ap3216c_id[] = {
    { "ap3216c", 0 },
    { /* Sentinel */ } 
};

static const struct of_device_id ap3216c_of_match[] = {
    { .compatible = "alientek,ap3216c" },
    { /* Sentinel */ }
};

static struct i2c_driver ap3216c_driver = {
    .probe = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = AP3216C_NAME,
        .of_match_table = ap3216c_of_match, 
    },
    .id_table = ap3216c_id,
};
// clang-format on

static int __init ap3216c_init(void)
{
    int ret = 0;
    ret = i2c_add_driver(&ap3216c_driver);
    return ret;
}

static void __exit ap3216c_exit(void)
{
    i2c_del_driver(&ap3216c_driver);
}

module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wtp");