zoukankan      html  css  js  c++  java
  • Linux驱动开发之IIC开发

    2020-02-19

    关键字:IIC通信协议


    嵌入式设备中常见的IIC从设备有:

    1、CMOS 摄像头

    2、触摸屏

    3、重力传感器

    4、EEPROM

    5、HDMI

    IIC通信协议是一种主从通信模式的协议,在进行IIC驱动开发的时候,我们都是站在主机的角度来开发的,所有要驱动的设备在我们的驱动程序看来都是从设备。

    IIC通信协议

    IIC是使用 2 根数据线来完成通信目的的。其通信方式如下图所示:

    这两根线中一根是时钟线:SCL 。另一根是数据线:SDA 。

    其中,时钟线只输出标准的正弦方波,用于数据在通信过程中的同步。而数据线就是主从双方用于通信用的了。因为IIC只有一能数据线,因此IIC协议最多只能是半双工通信模式。

    IIC通信协议的格式如下图所示:

    IIC的通信协议是按位区分的,一个时钟周期内允许产生 1 位的数据。其中第 1 位为开始位,开始位的标志是主设备在时钟信号为高电平时将数据线电平从高拉至低。IIC通信的最后一位是停止位,停止位的标志是主设备或从设备在时钟信号为高电平时将数据线的电平由低拉至高。需要强调的是,仅开始位与停止位的数据电平信号是在时钟信号为高电平时转换,其余所有位的信号都只能在时钟信号为低电平时转换。这是区分IIC协议开始与停止的唯一可靠办法。主设备在发送完开始位信号以后立即发送从设备地址,用以表示当前要与哪一个从设备通信。从设备地址是一个 7 位长度的数据,由从设备的长度可知在没有级联的IIC中,最多允许挂载127个从设备。7位的从设备信号发完以后主设备再发送 1 位的读写信号位。读写信号位发完后主设备需要得到从设备的反馈,即将数据信号线电平拉高,在有从设备成功回应时(地址相符),从设备会将数据信号线的电平拉低,主设备可以感知到数据线被拉低了,由此表明当前IIC通信可以继续进行下去。再接下来就是数据信号了,数据信号每传送8位需要等待一个反馈信号,直至最后信号位的到来。

    IIC通信写数据的过程如下:

    首先主设备发送一个开始信号,然后是7位的从设备地址,第8位是写信号,接着等待从设备的反馈。当得到反馈以后即开始写8位(或16位,总之每8位需要一个从设备的反馈信号)的从设备内部的地址,这个从设备内部地址就是你总得告诉从设备我要往哪里写数据吧的意思。在从设备内部的地址发送完成并得到反馈以后,即开始写数据信号,仍旧是每8位需要等待一个从设备的反馈信号。直到最后数据写完由主设备发送信号位信号。

    IIC通信读数据的过程如下:

    首先一个开始信号,接着从设备地址,从设备地址之后是一个写标志位信号,在得到从设备的反馈以后开始由主设备开始发送从设备的内部地址,8位或16位。这个的意思是说我要告诉从设备我要读你里面哪个地址的数据。在得到从设备的反馈信号以后主设备再立即重新发送一个开始标志位信号,接着发送7位的从设备地址,以及一个读标志位信号。发送完读标志位信号并得到从设备的反馈信号以后主设备就进入接收数据状了,此时是由从设备开始往数据线中每8位再加一个位来自主设备的反馈信号的形式来让主设备读取数据。直到最后从设备将所有数据发送完毕,由从设备发起一个停止位信号。

    以上就是IIC通信协议。

    Linux中的IIC开发框架

    在Linux中,IIC通信协议是有被封装成一个框架以便于开发者进行IIC开发的。这个IIC框架大致可以分为以下几个层次:

    1、应用层

    2、IIC驱动层

    这一层对下要注册自己的信息以供核心层传递数据上来,对上封装了一套自己的 file_operations 用于支持应用层的 open() , read() , write() , close() 操作。

    在这一层还会有一个结构体:I2C_Driver,它内部有一个名字,这个名字只要与适配器层的 I2C_Client 结构体中的名字一致就表示它们互相匹配,可以进行通信。

    在这一层的代码中,会有一个 probe() 函数用于在匹配到相应适配器层以后调用,并将适配器层的 I2C_Client 结构体对象传递过来以将应用层的数据传递下去。

    3、IIC核心层

    这一层内部维护着IIC Bus,与驱动开发中的平台总线类似。

    4、IIC控制器层/适配器层

    这一层内部抽象出了一个 I2C_Client 结构体,这个结构体内部有一个名字,它只与驱动层中名字与它一致的对象通信。

    这一层是直接与硬件通信的,并且在整个IIC框架中,真正的IIC通信协议是在这一层产生并与硬件通信的。

    5、硬件层

    这一层就是IIC适配器以及它通过SCL和SDA两根线去连接着的各种从设备。

    不同的CPU所支持的IIC总线组数也是不同的,通常都会同时支持好几组IIC总线,不同组的IIC以不同的地址来表示,通常在 /sys/devices 下可以看到。例如 rk3128 上的 i2c 信息就如下所示:

    drwxr-xr-x root     root              1970-01-01 08:00 20056000.i2c
    drwxr-xr-x root     root              1970-01-01 08:00 2005a000.i2c

    在开发IIC驱动之前,还必须结合原理图来得知当前设备所接的IIC是哪一组,再结合芯片手册来得知该组IIC的地址。拿到了这组IIC在CPU中的地址以后才能进行后面的驱动开发。

    在应用IIC子系统框架开发驱动时,很重要的一步就是在相应的 dts 中添加要适配的从设备节点信息,其模板可以参考如下:

    i2c@138B0000{/*i2c adapter信息*/
        #address-cells = <0>;
        #size-cells = <0>;
        samsung,i2c-sda-delay = <100>;
        samsung,i2c-max-bus-freq = <20000>;
        pinctrl-0 = <&i2c5_bus>;
        pinctrl-names = "default";
        status = "okay";
        
        mpu6050@68{/*i2c client信息*/
            compatible = "invensense,mpu6050";
            reg = <0x68>
        };
    };

    在你的设备树文件的根节点下添加这个信息并按需修改即可。其中,i2c@138B0000 信息,前面的部分不要更改,后面的部分是IIC组号的地址,地址在芯片数据手册中查即可,也可以在上一级dtsi中查找。然后是pinctrl-0的信息要写对IIC组别号。这个节点信息是会被应用在IIC适配器中的。前面有提到在IIC适配器中会去创建一个IIC_Client结构体对象,这个结构体对象的信息就以子节点的形式被记录在外面这个节点中。在 mpu6050@68 中,第一部分可以随便写,一般写设备名称。第二部分是从设备地址,这个可以在相应的设备数据手册中查到。子节点里面的 compatible 是用来标识驱动名称的,名称可以随便写,但一般是以厂商名称+设备名称标识。reg 填的也是从设备地址。

    在上面的设备树信息配置好以后就可以在 /sys/bus/platform/devices 目录下发现有 138B0000.i2c 的目录来了,这个就是我们刚配置好的信息。

    Linux中的IIC框架也可以参考如下笔记图:

    IIC子系统驱动开发

    嵌入式Linux驱动中IIC驱动的开发步骤大致如下:

    1、在设备树中描述好IIC设备信息

    2、构建IIC Driver并注册到IIC总线

    3、实现 probe() 函数

    申请设备号,实现file_operations

    创建设备节点文件

    通过IIC的接口去初始化IIC从设备

     

    IIC Driver 结构体的主要成员如下所示:

    struct i2c_driver {
        int (*probe)(struct i2c_client *, const struct i2c_device_id *);
        int (*remove)(struct i2c_client *);
        struct device_driver driver;
        const struct of_device_id *of_match_table;
        const struct i2c_device_id *id_table; //在非设备树情况下用于作匹配用的。
    };

    IIC Driver的注册和注销原型:

    int i2c_add_driver(struct i2c_driver *driver);
    void i2c_del_driver(struct i2c_driver *driver);
    struct i2c_client{
        unsigned short addr;//来自于设备树中的reg标签。
        char name[I2C_NAME_SIZE];//用于和i2c_driver中的of_match_table匹配用的,来自于设备树中的compatible。
        struct i2c_adapter *adapter;//指向当前从设备所存在的i2c adapter。
        struct device dev;
    };

    这个结构体的对象不需要开发者创建,在 i2c_adapter 创建时会自动去创建:

    struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info);
    struct i2c_adapter{
        const struct i2c_algorithm *algo;//算法[int *(master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)]
        struct device dev;
        int nr; //编号,就是第几组IIC
    };

    i2c_adapter结构体对象也不需要开发者去创建,内核会创建,这个结构体对象的注册与注销函数签名如下:

    int i2c_add_adapter(struct i2c_adapter *adapter);
    void i2c_del_adapter(struct i2c_adapter *adap);

    还有一个结构体对象,用于描述一个从设备要发送的数据的数据包,它的原型如下:

    struct i2c_msg {
        __u16 addr; //接收数据的那个设备的地址。
        __u16 flags; //读写标志位,读为1,写为0。
        __u16 len; //数据的长度
        __u8 *buf; //指向数据的指针
    };

    发送IIC消息的函数签名如下:

    int i2c_master_send(const struct i2c_client *client, const char *buf, int count);
    int i2c_master_recv(const struct i2c_client *client, char *buf, int count);

    以上两个函数的内部都调用了i2c_transfer()函数:

    int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);

    Linux中的 ioctl() 其实就是一个由Linux封装出来给开发者根据不同的整型数来传达不同的意图的一个普通函数,有了这个函数开发者在实现多种意图时就不需要从上至下分别编写多个函数接口,可以一定程度上降低开发复杂度。ioctl() 函数的签名如下:

    ioctl(fd, cmd, args);

    参数1就是文件描述符。

    参数2是命令,用整型数表示,用户可以任意定义,但通常为了不与系统定义的命令冲突,会用系统提供的接口来创建:1、_IO(x,y);2、_IOR(x,y,z);3、_IOW(x,y,z);参数x是一个magic数,参数y是一个类型,用于区分不同类型的命令,参数z是数据的类型。

    参数3比较自由,可以用于传递结果值,例如把一个指针地址给传进来。

    以下是一个编写I2C驱动的示例源码,可供参考:

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/input.h>
    #include <linux/interrupt.h>
    #include <linux/slab.h>
    #include <linux/of.h>
    #include <linux/of_irq.h>
    #include <linux/of_gpio.h>
    #include <linux/i2c.h>
    
    #include <asm/io.h>
    #include <asm/uaccess.h>
    
    
    union mpu6050_data{
        struct{
            short x;
        }accel;
    };
    
    #define IOC_GET_ACCEL _IOR('M', 0x34, union mpu6050_data)
    
    struct device_driver driver;
    const struct i2c_device_id *id_table;
    const struct of_device_id of_mpu6050_id[] = {
        {
            .compatible = "invensense,mpu6050", //这个名字一定要跟设备树中的一致。
        },
        {/**/}, //在这末尾最好加上一个空对象表示结束。
    };
    const struct i2c_device_id mpu_id_table[] = {
        {},
        //do nothing.
    };
    
    struct mpu_sensor{
        int dev_major;
        struct device *dev;
        struct class *cls;
        struct i2c_client *client;
    };
    
    struct mpu_sensor *mpu_dev;
    
    int mpu6050_drv_open(struct inode *inode, struct file *fp)
    {
        return 0;
    }
    
    int mpu6050_drv_close(struct inode *inode, struct file *fp)
    {
        return 0;
    }
    
    long mpu6050_drv_ioctl(struct file *fp, unsigned int cmd, unsigned long args)
    {
        union mpu6050_data data;
        switch(cmd)
        {
            case IOC_GET_ACCEL:{
                //读数据。
                data.accel.x = mpu6050_read_reg_byte(mpu_dev_client, 0x43/*从设备内部地址*/);
            }break;
        }
        
        //读取完毕就要将数据交给用户。
        copy_to_user((void __user *)args/*long型完全可以放的下指针地址*/, &data, sizeof(data));
        
        return 0;
    }
    
    const struct file_operations mpu6050_fops = {
        .open = mpu6050_drv_open,
        .release = mpu6050_drv_close,
        .unlocked_ioctl = mpu6050_drv_ioctl,
    };
    
    /*
        自己实现I2C的读写功能。
    */
    int mpu6050_write_bytes(struct i2c_client *client, char *buf, int count)
    {
        struct i2c_adapter *adapter = client->adapter;
        struct i2c_msg msg;
        
        msg.addr = client->addr;
        msg.flags = 0;
        msg.len = count;
        msg.buf = buf;
        
        int ret = i2c_transfer(adapter, &msg, 1/*指消息msg的个数*/);
        
        return ret == 1 ? count : ret;
    }
    
    int mpu6050_read_bytes(struct i2c_client *client, char *buf, int count)
    {
        struct i2c_adapter *adapter = client->adapter;
        struct i2c_msg msg;
        
        msg.addr = client->addr;
        msg.flags = 1;
        msg.len = count;
        msg.buf = buf;
        
        int ret = i2c_transfer(adapter, &msg, 1/*指消息msg的个数*/);
        
        return ret == 1 ? count : ret;
    }
    
    //读取指定寄存器的地址,然后返回值。
    int mpu6050_read_reg_byte(struct i2c_client *client, char reg)
    {
        struct i2c_adapter *adapter = client->adapter;
        struct i2c_msg msg[2];
        
        msg[0].addr = client->addr;
        msg[0].flags = 0;
        msg[0].len = 1;
        msg[0].buf = &reg;
        
        char rxbuf[1];
        msg[1].addr = client->addr;
        msg[1].flags = 1;
        msg[1].len = 1;
        msg[1].buf = rxbuf;
        
        int ret = i2c_transfer(adapter, msg, 2/*指消息msg的个数*/);
        if(ret < 0)
        {
            return ret;
        }
        
        return rxbuf[0];
    }
    
    int mpu6050_drv_probe(struct i2c_client *client, const struct i2c_device_id *devid)
    {
        mpu_dev = kzalloc(sizeof(struct mpu_sensor), GFP_KERNEL);
        mpu_dev->client = client;
        
        //申请设备号。
        mpu_dev->dev_major = register_chrdev(0, "mpu_drv", &mpu6050_fops);
        
        //创建设备节点。
        mpu_dev->cls = class_create(THIS_MODULE, "mpu_cls");
        mpu_dev->dev = device_create(mpu_dev->cls, NULL, MKDEV(mpu_dev->dev_major, 0), NULL, "mpu_sensor");
        
        //通过接口初始化I2C设备。
        // i2c_master_send(client, ); //可以直接通过这个函数来给从设备发数据。
        
        char buf[2] = {0x43/*从设备内部地址*/, 0x00/*要写进从设备的值*/};
        mpu6050_write_bytes(mpu_dev->client, buf, 2);//用自己实现的函数来给从设备写数据。
        
        return 0;
    }
    
    int mpu6050_drv_remove(struct i2c_client *client)
    {
        device_destroy(mpu_dev->cls, MKDEV(mpu_dev->dev_major, 0));
        class_destroy(mpu_dev->cls);
        unregister_chrdev(mpu_dev->dev_major, "mpu_drv");
        kfree(mpu_dev);
    }
    
    struct i2c_driver mpu6050_drv = {
        .probe = mpu6050_drv_probe,
        .remove = mpu6050_drv_remove,
        .driver ={
            .name = "mpu6050_drv"; //将出现在 /sys/bus/i2c/driver 目录下。
            .of_match_table = of_match_ptr(of_mpu6050_id); //用于跟i2c_client中的name匹配用的。
        },
        .id_table = {}, //非设备树环境下的匹配,现在已经用不上这个成员了。
    };
    
    static int __init mpu6050_drv_init()
    {
        return i2c_add_driver(&mpu6050_drv);
    }
    
    static void __exit mpu6050_drv_exit()
    {
        i2c_del_driver(&mpu6050_drv);
    }
    
    module_init(mpu6050_drv_init);
    module_exit(mpu6050_drv_exit);
    MODULE_LICENSE("GPL");

  • 相关阅读:
    ExtJs中ComboBox使用之技巧
    Ethernet帧结构
    Sqlite修改表
    Solr系列:Linux下部署Solr
    Ethernet帧的接收流程
    使用SQLite作为Quartz.Net的Job Store
    Quartz.Net的一个Bug
    获取程序集的Public key token
    SQLite数据类型
    shell取出多列文件中某一列重复的数据
  • 原文地址:https://www.cnblogs.com/chorm590/p/12323850.html
Copyright © 2011-2022 走看看