2018-07-17 18:06:59 Egean 阅读数 2028
  • I2C总线和触摸屏驱动移植实战-linux驱动开发第9部分

    本课程是linux驱动开发的第9个课程,主要内容是linux的I2C子系统以及电容触摸屏驱动的移植。学习本课程的核心点在于I2C子系统,目标是对I2C驱动框架的彻底理解和熟练运用。本课程承袭前面platform平台总线的讲解思路和基础,真正做到了对I2C总线做透彻清晰的讲解。

    9760 人正在学习 去看看 朱有鹏

介绍

这里实现一种不涉及硬件的I2C总线和设备驱动,然后分析I2C驱动在系统中的注册过程。这里实现的驱动类似于之前的FIFO字符设备驱动,与之不同的是这里使用了I2C驱动框架,设备文件写入和读取将通过i2c_client传递到指定的i2c_adaper,然后在i2c_adapter中操作一段内存。这个驱动非常简单,甚至没有使用FIFO这种硬件结构,只能进行读和写,毕竟内存的操作不是重点,重点是熟悉Linux系统的I2C驱动框架。

首先需要注意的是I2C驱动分为总线驱动设备驱动,不能混为一谈。I2C总线驱动主要针对i2c_adapter和i2c_algorithm进行设置,一个i2c_adapter对应一个SOC中的I2C适配器。比如全志H3芯片有三个I2C接口,那么就对应三个I2C适配器,在I2C总线驱动中,主要完成对I2C适配器的配置工作,如写哪个寄存器配置I2C时钟,写哪个寄存器准备发送,接收到数据后从哪个寄存器读取等。可以看出这都是和平台相关的代码,主要由移植操作系统工程师实现。

I2C设备驱动是针对外设的驱动,这是在默认系统的I2C适配器已经能够正常工作情况下,指定调用哪个I2C适配器发送相应的数据给外设芯片。比如i2c-0上接了一个加速度传感器,那么我就可以调用i2c-0的适配器来向该外设发送相应的数据来操作它的寄存器,完成传感器的初始化、读取等操作。


相关结构体

  • i2c_adapter对应一个I2C适配器,其中的i2c_algorithm决定了该适配器是如何工作的。
struct i2c_adapter {
    struct module *owner;
    const struct i2c_algorithm *algo; /* the algorithm to access the bus */
    void *algo_data;
    ...
    struct device dev;      /* the adapter device */

    int nr;
    char name[48];
    ...
    struct list_head userspace_clients;
    ...
};
  • i2c_algorithm指定了i2c_adapter具体的传输方法,一个i2c_adapter绑定一个i2c_algorithm。
struct i2c_algorithm{
     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);
     u32 (*functionality) (struct i2c_adapter *);
}
  • i2c_client对应一个I2C的外设,属于I2C设备驱动部分,其中包括了外设的名称、从机地址、挂接的适配器等信息。
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;
    ...
};
  • i2c_driver主要包含一个probe函数,当i2c_client和i2c_driver匹配时,probe函数就会执行,这一点和platform驱动是相似的。
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 *);
    int (*probe_new)(struct i2c_client *);
    void (*shutdown)(struct i2c_client *);
    void (*alert)(struct i2c_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;

    /* 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总线驱动

1. 驱动代码

由于这里使用了platform总线框架,因此这里只贴出platform_driver部分,platform_device部分只包含一个简单的注册并且在第2节有相关代码参考,所以不再贴出来了。

#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/uaccess.h>
#include <linux/i2c.h>

#define PAGE_LEN 4096

struct myi2c_dev {
    char *mem;
    struct i2c_adapter adapter;
    struct i2c_client *client;
};

// 返回-EAGAIN重试发送
static int i2c_test_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
                int num)
{
    struct myi2c_dev *dev = i2c_get_adapdata(adap);
    int i;

    for (i=0; i<num; i++) {
        if (msgs[i].len > PAGE_LEN)
            msgs[i].len = PAGE_LEN;
        if (msgs[i].flags & I2C_M_RD) {
            // 读数据
            printk(KERN_INFO "read data from %p to %p\n", dev->mem, 
                    msgs[i].buf);
            if (copy_to_user(msgs[i].buf, dev->mem, msgs[i].len)) {
                printk(KERN_INFO "i2c_adapter: fail to read\n");
                return -EFAULT;
            }
        } else {
            // 写数据
            printk(KERN_INFO "write data from %p to %p\n", msgs[i].buf, 
                    dev->mem);
            if (copy_from_user(dev->mem, msgs[i].buf, msgs[i].len)) {
                printk(KERN_INFO "i2c_adapter: fail to write\n");
                return -EFAULT;
            }
        }
    }

    return 0;
}

static u32 i2c_test_func(struct i2c_adapter *adap)
{
    return I2C_FUNC_I2C;  
}

static const struct i2c_algorithm i2c_test_algo = {
    .master_xfer = i2c_test_xfer,
    .functionality = i2c_test_func  // 决定适配器支持什么类型的i2c器件
};

static struct i2c_board_info i2c_mem_dev = {
    //I2C_BOARD_INFO("24c08", 0x50),
    //.platform_data = &at24c08
    .type = "i2c_mem",
    .addr = 0x23
};

static int i2c_test_probe(struct platform_device *pdev)
{
    struct myi2c_dev *dev;
    int ret;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;
    dev->mem = kzalloc(PAGE_LEN, GFP_KERNEL);
    if (!dev->mem)
        return -ENOMEM;

    // 设置适配器私有数据
    i2c_set_adapdata(&dev->adapter, dev);
    // 添加i2c适配器, 使用指定的总线号
    dev->adapter.owner = THIS_MODULE;
    //dev->adapter.class = I2C_CLASS_DEPRECATED;
    strcpy(dev->adapter.name, "i2c test adapter");
    dev->adapter.algo = &i2c_test_algo;
    dev->adapter.dev.parent = &pdev->dev;
    dev->adapter.nr = pdev->id;
    ret = i2c_add_numbered_adapter(&dev->adapter);

    // 手动添加i2c_client,本来应该在板级文件或设备树中添加
    dev->client = i2c_new_device(&dev->adapter, &i2c_mem_dev);
    if (!dev->client) {
        printk(KERN_INFO "fail to register i2c_client\n");
        return -ENOMEM;
    }

    platform_set_drvdata(pdev, dev);
    printk(KERN_INFO "test probe!\n");

    return 0;
}

static int i2c_test_remove(struct platform_device *pdev)
{
    struct myi2c_dev *dev = platform_get_drvdata(pdev);
    i2c_unregister_device(dev->client);
    i2c_del_adapter(&dev->adapter);
    kfree(dev->mem);
    kfree(dev);
    printk(KERN_INFO "i2c test remove!\n");
    return 0;
}


static struct platform_driver i2c_test_driver = {
    .probe = i2c_test_probe,
    .remove = i2c_test_remove,
    .driver = {
        .name = "i2c_test_module",
        .owner = THIS_MODULE
    }
};
module_platform_driver(i2c_test_driver);

MODULE_AUTHOR("colourfate <hzy1q84@foxmail.com>");
MODULE_LICENSE("GPL v2");

2. 内核实现分析

i2c_adapter的添加:

这里使用了platform驱动框架,我们直接看probe函数。这里首先使用i2c_set_adapdata()函数设置i2c_adapter的私有数据,可以看到i2c_adapter实际上是device的子类,而这个私有数据也是设置在device中的:

static inline void i2c_set_adapdata(struct i2c_adapter *dev, void *data)
{
    dev_set_drvdata(&dev->dev, data);
}

接下来对i2c_adapter进行配置,其中配置了dev->adapter.algo = &i2c_test_algo,这很重要,因为它决定了i2c适配器是如何工作的,同时还配置了dev->adapter.nr = pdev->id,这是i2c适配器的编号,通过编号可以找到唯一指定的适配器。
接下来使用i2c_add_numbered_adapter()函数将适配器添加到系统。该函数指定了i2c_adapter()的总线号,同时可以使用i2c_add_adapter()函数让系统自动分配总线号。

--- drivers --- i2c --- i2c_core.c --- i2c_add_numbered_adapter(   --- if (adap->nr == -1)
             |                      |    struct i2c_adapter *adap)  |      i2c_add_adapter(adap);
             |                      |                               |- __i2c_add_numbered_adapter(adap);
             |                      |- __i2c_add_numbered_adapter( --- id = idr_alloc(&i2c_adapter_idr,adap,adap->nr,adap->nr+1,GFP_KERNEL)
             |                      |    struct i2c_adapter *adap)  |- i2c_register_adapter(adap)
             |                      |- i2c_register_adapter(       --- INIT_LIST_HEAD(&adap->userspace_clients)
             |                      |    struct i2c_adapter *adap)  |- dev_set_name(&adap->dev, "i2c-%d", adap->nr)
             |                      |                               |- adap->dev.bus = &i2c_bus_type
             |                      |                               |- adap->dev.type = &i2c_adapter_type
             |                      |                               |- device_register(&adap->dev)
             |                      |                               |- bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);
             |                      |- __process_new_adapter(     --- i2c_do_add_adapter(to_i2c_driver(d), data)
             |                      |    struct device_driver *d,
             |                      |    void *data)
             |                      |- i2c_do_add_adapter(         --- i2c_detect(adap, driver)
             |                      |    struct i2c_driver *driver, |- 这里是废弃的driver->attach_adapter方法
             |                      |    struct i2c_adapter *adap)
             |                      |- i2c_detect(                   --- address_list = driver->address_list
             |                      |    struct i2c_adapter *adapter, |- if (!driver->detect || !address_list) 
             |                      |    struct i2c_driver *driver)   |       return 0;
             |                      |                                 |- temp_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL)
             |                      |                                 |- temp_client->adapter = adapter
             |                      |                                 |- for (i = 0; address_list[i] != I2C_CLIENT_END; i += 1) {
             |                      |                                 |      temp_client->addr = address_list[i];
             |                      |                                 |      err = i2c_detect_address(temp_client, driver);
             |                      |                                 |      err不是错误码,则break;
             |                      |                                 |  }
             |                      |                                 |- kfree(temp_client)
             |                      |- i2c_detect_address(              --- struct i2c_board_info info
             |                      |    struct i2c_client *temp_client, |- adapter = temp_client->adapter
             |                      |    struct i2c_driver *driver)      |- addr = temp_client->addr
             |                      |                                    |- info.addr = addr
             |                      |                                    |- driver->detect(temp_client, &info)
             |                      |                                    |- if (info.type[0] == '\0')
             |                      |                                    |      报错;
             |                      |                                    |  else {
             |                      |                                    |      struct i2c_client *client;
             |                      |                                    |      client = i2c_new_device(adapter, &info);
             |                      |                                    |      list_add_tail(&client->detected, &driver->clients);
             |                      |                                    |  }
             |  
             |- base --- bus.c --- bus_for_each_drv(            --- while ((drv = next_driver(&i)) && !error)
                                     struct bus_type *bus,       |-     error = fn(drv, data);
                                     struct device_driver *start,
                                     void *data,
                                     int (*fn)(struct device_driver *, void *))

i2c_add_numbered_adapter展开后涉及很多函数,首先可以追到i2c_register_adapter()函数,该函数首先使用dev_set_name()设定了总线名称为i2c-%d,其中%d为总线号,然后设定了总线名称和从总线类型,并用device_register()函数注册设备,该函数在platform驱动中已经详细分析,这里不展开,最后遍历总线上的所有驱动,并执行__process_new_adapter()函数。

__process_new_adapter()函数直接调用了i2c_do_add_adapter()函数,其中to_i2c_driver宏是由device_driver父类得到i2c_driver子类。

i2c_do_add_adapter()函数实际上就是一个i2c_detect()函数,下面废弃的函数我们不分析。

i2c_detect()中,如果没有定义driver->address_listdriver->detect,则直接跳出,i2c_driver是下面讲到的外设驱动部分,这里没有定义这两个变量,所以到这里添加适配器就完成,由于我们没有在驱动中定义address_listdetect,所以不会有i2c_client添加进来,所以这里是手动添加的i2c_client。我们也可以继续分析。

接下来申请一个temp_client,然后将适配器与之绑定,然后遍历address_list,并将address_list[i]绑定到temp_client上,然后执行i2c_detect_address

进入i2c_detect_address()函数,该函数实际上是声明了一个struct i2c_board_info变量,然后使用driver->detect填充该变量,然后使用i2c_new_device()新申请了一个i2c_client,将adapter与之绑定,并使用info的信息初始化它,这个函数下来会分析。最后使用list_add_tail()添加到链表完成client的添加工作。

i2c_client的添加:

这里实际就用了一个i2c_new_device()函数完成client的添加

--- drivers --- i2c --- i2c_core.c --- i2c_new_device(                      --- client = kzalloc(sizeof *client, GFP_KERNEL)
                                         struct i2c_adapter *adap,           |- client->adapter = adap
                                         struct i2c_board_info const *info)  |- client->dev.platform_data = info->platform_data
                                                                             |- client->flags = info->flags
                                                                             |- client->addr = info->addr
                                                                             |- client->irq = info->irq
                                                                             |- strlcpy(client->name, info->type, sizeof(client->name))
                                                                             |- client->dev.bus = &i2c_bus_type
                                                                             |- client->dev.type = &i2c_client_type
                                                                             |- device_register(&client->dev)

我不准备具体分析接下来的代码了,因为代码树列出了函数调用中的关键部分,再结合linux内核源码应该能很容易看懂(毕竟内核源码非常规范),之前的分析只是照着源码复述了一遍,意义不大。当然如果有容易搞错的关键部分我还是会说明。引用linus名言:

Read the fucking source code!


I2C设备驱动

1. 驱动代码

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/miscdevice.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>


struct i2c_memery_dev {
    struct miscdevice miscdev;
    struct i2c_client *client;
};

ssize_t i2c_mem_read(struct file *filp, char __user *buf, 
                size_t count, loff_t *ppos)
{
    // misc注册时自动将miscdevice的指针放到private_data中,这就是
    // 为什么在i2c_memery_dev中要使用miscdevice的实体,而不是指针
    struct i2c_memery_dev *dev = container_of(filp->private_data,
                    struct i2c_memery_dev, miscdev);
    struct i2c_msg msg;
    int ret;
    // 构建i2c_msg
    msg.addr = dev->client->addr;
    msg.flags = I2C_M_RD;
    msg.buf = buf;
    msg.len = count;
    // 调用适配器发送, 返回值为client->adapter->algo->master_xfer函数的
    // 返回值
    ret = i2c_transfer(dev->client->adapter, &msg, 1);
    if (ret < 0) {
        printk(KERN_INFO "fail to transfer\n");
        return 0;
    }
    printk(KERN_INFO "count=%d\n", count);

    return count;
}

ssize_t i2c_mem_write (struct file *filp, const char __user *buf,
            size_t count, loff_t *ppos)
{
    struct i2c_memery_dev *dev = container_of(filp->private_data,
                    struct i2c_memery_dev, miscdev);
    struct i2c_msg msg;
    int ret;
    // 构建i2c_msg
    msg.addr = dev->client->addr;
    msg.flags = 0;
    msg.buf = buf;
    msg.len = count;
    // 调用适配器发送
    ret = i2c_transfer(dev->client->adapter, &msg, 1);
    if (ret < 0) { 
        printk(KERN_INFO "fail to transfer\n");
        return 0;
    }
    printk(KERN_INFO "count=%d\n", count);

    return count;
}

static int i2c_mem_release(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "i2c_mem release\n");
    return 0;
}

static int i2c_mem_open(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "i2c_mem open\n");
    return 0;
}

static const struct file_operations i2c_mem_fops = {
    .write = i2c_mem_write,
    .read = i2c_mem_read,
    .open = i2c_mem_open,
    .release = i2c_mem_release
};

static int i2c_mem_probe(struct i2c_client *client, 
                const struct i2c_device_id *id)
{
    struct i2c_memery_dev *dev;
    int ret;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        printk(KERN_INFO "i2c_mem_probe:fail to kzalloc\n");
        return -ENOMEM;
    }

    // 设置杂散类设备驱动
    dev->miscdev.minor = MISC_DYNAMIC_MINOR;
    dev->miscdev.name = "i2c_mem";
    dev->miscdev.fops = &i2c_mem_fops;
    // 设置私有数据
    dev->client = client;
    dev_set_drvdata(&client->dev, dev);

    ret = misc_register(&dev->miscdev);
    if (ret < 0) {
        return ret;
    }

    return 0;
}

static int i2c_mem_remove(struct i2c_client *client)
{
    struct i2c_memery_dev *dev = dev_get_drvdata(&client->dev);
    misc_deregister(&dev->miscdev);
    return 0;
}

static const struct i2c_device_id i2c_mem_ids[] = {
    {"i2c_mem", 0}
};

static struct i2c_driver i2c_mem_driver = {
    .driver = {
        .name = "i2c_mem",
        .owner = THIS_MODULE,
    },
    .probe = i2c_mem_probe,
    .remove = i2c_mem_remove,
    .id_table = i2c_mem_ids,
};

static int __init i2c_mem_test_init(void)
{
    return i2c_add_driver(&i2c_mem_driver);
}
module_init(i2c_mem_test_init);

static void __exit i2c_mem_test_exit(void)
{
    i2c_del_driver(&i2c_mem_driver);
}
module_exit(i2c_mem_test_exit);

MODULE_AUTHOR("colourfate <hzy1q84@foxmail.com>");
MODULE_LICENSE("GPL v2");

2. 内核实现分析

设备驱动主要是向系统注册i2c_driver这个结构,然后在总线驱动中,我们注册了一个i2c_client,此时注册驱动就会遍历总线所有设备,然后对所有设备和驱动调用bus->match这个函数来判断设备和驱动是否匹配,这个地方和platform驱动非常相似(毕竟它们都是总线驱动),只是这个地方的bus->matchi2c_bus_type的匹配函数。
匹配过后就可以执行probe函数进行初始化,这里设备驱动使用了杂散类设备驱动框架,只实现了其中的read和write函数

i2c_driver的注册:

注册主要调用的是i2c_add_driver()宏,这个宏调用了i2c_register_driver()函数。

#define i2c_add_driver(driver) \
    i2c_register_driver(THIS_MODULE, driver)

接下来分析i2c_register_driver函数

--- drivers --- i2c --- i2c_core.c --- i2c_register_driver(         --- driver->driver.bus = &i2c_bus_type
                                    |    struct module *owner,       |- INIT_LIST_HEAD(&driver->clients)
                                    |    struct i2c_driver *driver)  |- driver_register(&driver->driver)
                                    |                                |- i2c_for_each_dev(driver, __process_new_driver)
                                    |- i2c_for_each_dev(                    --- bus_for_each_dev(&i2c_bus_type, NULL, data, fn)
                                    |    void *data,
                                    |    int (*fn)(struct device *, void *))
                                    |- __process_new_driver( --- if (dev->type != &i2c_adapter_type)
                                         struct device *dev,  |      return 0;
                                         void *data)          |- i2c_do_add_adapter(data, to_i2c_adapter(dev));

可以看到这里面有两个关键函数都已经分析过了,一个是在platform驱动中分析过的driver_register()函数,一个是刚刚分析过的i2c_do_add_adapter()函数。这里其实就是向系统注册驱动,并遍历i2c_bus上的所有设备,然后再对所有设备执行i2c_do_add_adapter,尝试生成i2c_client,由于这里没有定义detect函数,而是手动添加的i2c_client,所以全部返回0.

driver_register()函数中完成了i2c_clienti2c_driver的匹配,但是这里调用的是i2c_bus_type->match函数,下面看看它们是如何匹配的。先找到其定义,再找到定义总绑定的函数。

// driver/i2c/i2c_core.c
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;

    /* Attempt an OF style match */
    if (i2c_of_match_device(drv->of_match_table, client))
        return 1;

    /* Then ACPI style match */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    driver = to_i2c_driver(drv);

    /* Finally an I2C match */
    if (i2c_match_id(driver->id_table, client))
        return 1;

    return 0;
}

这个函数实际上和platform_bus_type->match大同小异,先匹配设备树,然后匹配id,就是说如果i2c_clienti2c_driver的名字设置相同的话就能够完成匹配。


I2C驱动框架分析

如果认真分析了以上I2C驱动实现的具体代码,我们会发现Linux的I2C驱动框架实际上分三层,分别是:I2C设备驱动、I2C核心、I2C总线驱动。

I2C设备驱动定义了外设的交互方式,对与不同的I2C外设需要不同的设备驱动。I2C设备驱动对上和用户应用程序打交道,对下和I2C核心对接。
I2C核心是I2C驱动框架的主体,它定义了一系列API供设备驱动和总线驱动使用,相当于一个标准的函数调用规范。
I2C总线驱动定义了平台上I2C的工作方式,只要平台不变这部分代码就可以通用。I2C总线驱动是最底层的驱动,它上面是I2C核心,下面是SOC中的I2C适配器硬件,I2C适配器硬件上面连接了的我们的I2C外设。

总体来看这个框架如图所示:
这里写图片描述


驱动测试

分别编译好三个驱动模块,然后插入到系统,可以看到/dev/目录下出现了i2c_mem设备文件。写两个读和写app来测试驱动,这里写的app是通用的,以后也会使用这些它们来测试其他驱动。

首先是read.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int fd, count, ret;
    char buf[128] = {0};

    if (argc != 3) {
        printf("usage: ./read <file> <count>\n");
        return -1;
    }

    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    count = atoi(argv[2]);
    if (count > 128) {
        count = 128;
        printf("warn: count > 128\n");
    } else if (count < 1) {
        count = 1;
        printf("warn: count < 1\n");
    }

    ret = read(fd, buf, count);
    if (ret < 0) {
        perror("read");
        return -1;
    }

    printf("read %d byte: %s\n", ret, buf);
    close(fd);

    return 0;
}

然后是write.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    int fd, count, ret;

    if (argc != 3) {
        printf("usage: ./write <file> <string>\n");
        return -1;
    }

    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    ret = write(fd, argv[2], strlen(argv[2]));
    if (ret < 0) {
        perror("write");
        return -1;
    }

    printf("write %d byte\n", ret);
    close(fd);

    return 0;
}

编译好之后执行./write /dev/i2c_mem helloworld写入数据,然后使用./read /dev/i2c_mem 10读出数据。我这里是直接在开发板上测试的。
这里写图片描述

2019-12-13 15:19:22 qq_38008379 阅读数 10
  • I2C总线和触摸屏驱动移植实战-linux驱动开发第9部分

    本课程是linux驱动开发的第9个课程,主要内容是linux的I2C子系统以及电容触摸屏驱动的移植。学习本课程的核心点在于I2C子系统,目标是对I2C驱动框架的彻底理解和熟练运用。本课程承袭前面platform平台总线的讲解思路和基础,真正做到了对I2C总线做透彻清晰的讲解。

    9760 人正在学习 去看看 朱有鹏

学习(整理)嵌入式Linux开发教程

I2C 总线是板级内部总线。由于 I2C 总线简单、便捷,在嵌入式系统中应用比较广泛。
 

  • I2C 子系统的设计思路

I2C 子系统要处理的问题主要有两个:

(1) 控制总线的 I2C 控制器,驱动 I2C 控制器,以实现 I2C 总线上的通信

(2) 总线上的从机器件,使 I2C 总线上的从机器件能很好地工作起来。

  • 1.驱动每个 I2C 控制器

I2C 控制器是实现 I2C 总线通信的硬件操作接口。软件系统是通过 I2C 控制器实现在 I2C总线上收/发数据。每一个 I2C 控制器连接一路 I2C 总线, I2C 控制器与 I2C 总线的连接如图6.1所示。嵌入式处理器内部通常集成有多路 I2C 控制器,以连接多路 I2C 总线。
 

I2C 子系统需要为每个 I2C 控制器在/dev/目录下实现设备文件。通过这些设备文件,应用程序就可以在指定的 I2C 总线上实现收/发数据。 I2C 子系统在/dev/目录下生成的设备文件名通常为: i2c-0、 i2c-1、 i2c-2„„i2c-n。这些设备文件和 I2C 控制器的关系如图 6.2 所示。
 

虽然 I2C 子系统在/dev/目录下生成的设备文件有多个,但这些设备文件的操作接口是一样的,I2C 子系统使用一个用户层接口驱动实现这些统一的接口。

I2C 子系统需要为每个 I2C 控制器实现一个 I2C 适配器。 I2C 适配器能驱动 I2C 控制器,实现主机数据在 I2C 总线的收/发。 I2C 适配器不关心要向总线发送的数据是从哪里来,也不关心从总线接收的数据如何处理。

每当 I2C 子系统添加了一个 I2C 适配器,都会被已有的用户层接口驱动探测到。同样用户层接口驱动加载时,也会探测 I2C 子系统中已有的 I2C 适配器。用户层接口驱动每探测到一个 I2C 适配器,都会为之生成设备文件。用户层接口驱动和 I2C 适配器的关系如图 6.3 所示。

 

2. 驱动每个 I2C 从机器件

I2C 总线上可以接多个从机器件。而这些从机器件很多是需要实现内核态的驱动程序。以图 6.4 为例, I2C 总线上PCF8563 是 RTC 芯片,需要实现 RTC 驱动; CAT9555 是 I2C 转GPIO 芯片,需要实现 GPIO 驱动; OV3640 是摄像头模块,通过 I2C 接口配置内部寄存器,需要实现摄像头驱动„„ 这些驱动程序和从机器件进行通信时,通信数据必然是通过 I2C控制器。

 

I2C 子系统为总线上的每个从机都实现一个 I2C 设备。I2C 设备都包含了控制从机器件的 I2C 适配器,以及从机地址、从机器件名字的信息。从机器件驱动只要获得从机器件的 I2C 设备,并借助 I2C 子系统提供的操作函数,就可以轻松地建立和从机器件的通信,如图 6.5 所示。

 

驱动和设备匹配, I2C 子系统要求从机器件驱动实现一个 I2C 驱动。每个 I2C 驱动都包含了它能驱动的 I2C 设备的信息当一个 I2C 驱动添加到 I2C 子系统时,就会扫描子系统中所有的 I2C 设备,探测否有 I2C 设备能被自己驱动。同样当一个 I2C 设备添加到 I2C 子系统时,就会扫描子系统中所有的 I2C 驱动,探测否有可以驱动自己的 I2C 驱动。这种 I2C 设备和 I2C驱动的互相探测的机制,使从机器件驱动比较容易获得对应的 I2C 设备,从而建立和从机器件的通信,如图 6.6 所示。
 

  • I2C 子系统在/sys 文件系统的信息

I2C 子系统在/sys 文件系统的信息在/sys/bus/i2c/目录下。在/sys/bus/i2c/目录下有 devices和 drivers 目录,如下所示:

devices目录下包含了I2C子系统里所有的I2C控制器和I2C设备的属性文件;

上述的i2c-0目录 / i2c-1目录 / i2c-2目录 / i2c-3目录分别表示I2C0和I2C1和I2C2和I2C3总线上I2C控制器的属性文件目录;
其它目录都是 I2C 设备的属性文件目录。

以 1-0008 目录名为例,“1”表示从机器件在 I2C1 总线;“0008”表示从机地址为 0x08。
进入 1-0008 目录,可以看到各文件如下所示:

I2C 设备的属性文件目录下的 name 属性文件是 I2C 设备的名称。若 I2C 设备的属性文件目录下有 driver 文件,表示该 I2C 设备已经被 I2C 驱动所匹配到。

 

drivers目录下包含了 I2C 子系统里所有的 I2C 驱动的属性文件。 

该目录下是各 I2C 驱动的属性文件的目录。以wm8960 目录为例,该目录下
属性文件如下所示:

可以该看到该目录下有 I2C 设备的属性文件目录的链接,这表示该驱动已经匹配到 I2C设备。

通过这些属性文件的信息,可以查看 I2C 设备是否添加; I2C 设备信息是否正确; I2C 设备是否匹配到驱动; I2C 驱动是否正确注册; I2C 驱动是否匹配到 I2C 设备。这在驱动的开发调试阶段十分有用。

2020-01-05 20:19:23 qq_37596943 阅读数 21
  • I2C总线和触摸屏驱动移植实战-linux驱动开发第9部分

    本课程是linux驱动开发的第9个课程,主要内容是linux的I2C子系统以及电容触摸屏驱动的移植。学习本课程的核心点在于I2C子系统,目标是对I2C驱动框架的彻底理解和熟练运用。本课程承袭前面platform平台总线的讲解思路和基础,真正做到了对I2C总线做透彻清晰的讲解。

    9760 人正在学习 去看看 朱有鹏


Linux内核i2c驱动编程

i2c总线特性

I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件.在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。

I2C总线特点可以概括如下:

  • (1)在硬件上,I2C总线只需要一根数据线和一根时钟线两根线,总线接口已经集成在芯片内部,不需要特殊的接口电路,而且片上接口电路的滤波器可以滤去总线数据上的毛刺.因此I2C总线简化了硬件电路PCB布线,降低了系统成本,提高了系统可靠性。因为I2C芯片除了这两根线和少量中断线,与系统再没有连接的线,用户常用IC可以很容易形成标准化和模块化,便于重复利用。
  • (2)I2C总线是一个真正的多主机总线,如果两个或多个主机同时初始化数据传输,可以通过冲突检测和仲裁防止数据破坏,每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。数据传输和地址设定由软件设定,非常灵活。总线上的器件增加和删除不影响其他器件正常工作。
  • (3)I2C总线可以通过外部连线进行在线检测,便于系统故障诊断和调试,故障可以立即被寻址,软件也利于标准化和模块化,缩短开发时间。
  • (4)连接到相同总线上的IC数量只受总线最大电容的限制,串行的8位双向数据传输位速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
  • (5)总线具有极低的电流消耗.抗高噪声干扰,增加总线驱动器可以使总线电容扩大10倍,传输距离达到15m;兼容不同电压等级的器件,工作温度范围宽。

字节格式

发送到SDA 线上的每个字节必须为8 位,每次传输可以发送的字节数量不受限制。每个字节后必须跟一个响应位。首先传输的是数据的最高位(MSB),如果从机要完成一些其他功能后(例如一个内部中断服务程序)才能接收或发送下一个完整的数据字节,可以使时钟线SCL 保持低电平,迫使主机进入等待状态,当从机准备好接收下一个数据字节并释放时钟线SCL 后数据传输继续。

应答响应

数据传输必须带响应,相关的响应时钟脉冲由主机产生。在响应的时钟脉冲期间发送器释放SDA 线(高)。
在响应的时钟脉冲期间,接收器必须将SDA 线拉低,使它在这个时钟脉冲的高电平期间保持稳定的低电平。
通常被寻址的接收器在接收到的每个字节后,除了用CBUS 地址开头的数据,必须产生一个响应。当从机不能响应从机地址时(例如它正在执行一些实时函数不能接收或发送),从机必须使数据线保持高电平,主机然后产生一个停止条件终止传输或者产生重复起始条件开始新的传输。
如果从机接收器响应了从机地址,但是在传输了一段时间后不能接收更多数据字节,主机必须再一次终止传输。这个情况用从机在第一个字节后没有产生响应来表示。从机使数据线保持高电平,主机产生一个停止或重复起始条件。
如果传输中有主机接收器,它必须通过在从机发出的最后一个字节时产生一个响应,向从机发送器通知数据结束。从机发送器必须释放数据线,允许主机产生一个停止或重复起始条件。

时钟同步

所有主机在SCL线上产生它们自己的时钟来传输I2C总线上的报文。数据只在时钟的高电平周期有效,因此需要一个确定的时钟进行逐位仲裁。
时钟同步通过线与连接I2C 接口到SCL 线来执行。这就是说SCL 线的高到低切换会使器件开始数它们的低电平周期,而且一旦器件的时钟变低电平,它会使SCL 线保持这种状态直到到达时钟的高电平。但是如果另一个时钟仍处于低电平周期,这个时钟的低到高切换不会改变SCL 线的状态。因此SCL 线被有最长低电平周期的器件保持低电平。此时低电平周期短的器件会进入高电平的等待状态。
当所有有关的器件数完了它们的低电平周期后,时钟线被释放并变成高电平。之后,器件时钟和SCL线的状态没有差别,而且所有器件会开始数它们的高电平周期。首先完成高电平周期的器件会再次将SCL线拉低。
这样产生的同步SCL 时钟的低电平周期由低电平时钟周期最长的器件决定,而高电平周期由高电平时钟周期最短的器件决定。
注意:数据在时钟低电平期间改变而高电平期间保持,也就是低放高取(低电平期间改变传输数据,高电平期间由从机从总线上读取数据)。

Linux内核i2c驱动

Linux内核i2c驱动包含两部分:i2c总线驱动和i2c设备驱动
在这里插入图片描述

i2c总线驱动

i2c总线驱动操作的硬件对象仅仅是芯片内的I2C控制器,该部分在内核中已经由各个芯片厂家完成,在进行驱动开发中只需要配置上相应的Linux内核选项即可。

make menuconfig
Device Drivers->
	I2C supports->
		I2C Hardware Bus support  --->
  			  <*> Slsiap I2C  //S5P6818 I2C控制器的驱动
  							  //也就是I2C总线驱动的支持

i2c设备驱动

i2c设备驱动操作的硬件对象就是i2c外设本身。i2c设备驱动就是发起i2c外设所需的时序从而控制i2c往外设。当然,这种时序最终还是由芯片内的i2c控制器完成。

Linux内核i2c驱动框架

i2c总线驱动和i2c设备驱动是如何配合的呢,以CPU获取MMA8653三轴加速度传感器ID为例说明(CPU通过i2c总线读取MMA8653片内寄存器0x0D的数据(0x5A))

在这里插入图片描述

  • 在应用层空间,使用简单的读取设备文件的方式:
struct mma8653 {
	unsigned char addr; //片内寄存器地址
   	unsigned char data; //片内寄存器数据
};			

struct mma8653 mma; //分配用户缓冲区
mma.addr = 0x0D; //指定要访问的片内寄存器地址
mma.data = ?  	   			
ioctl(fd, MMA8653_READ, &mma);//发起读取MMA8653动作
printf("ID=%#x\n", mma.data); //mma.data=0x5A
  • 而在i2c设备驱动层,需要完成ioctl对应的驱动接口,如下内容:
long mma8653_ioctl(file, cmd, arg) {
//1.分配内核缓冲区
	struct mma8653 kmma;
  						
//2.拷贝用户数据到内核缓冲区
	copy_from_user(&kmma, (struct mma8653*)arg, sizeof(kmma));
//此时:kmma.addr=0x0D;kmma.data=?
  						
//3.I2C设备驱动发起硬件操作时序要求
//  此要求最终由I2C总线驱动来完成
//  I2C总线驱动操作I2C控制器发起
// I2C设备驱动要求的时序, I2C设备驱动只需调用内核提供的SMBUS
//接口函数即可完成相关的请求:
    kmma.data = i2c_smbus_*(...,0x0D); 
//kmma.data = 0x5A
  					  
//4.拷贝内核缓冲区的数据到用户缓冲区
   copy_to_user((strut mma8653*)arg, &kmma, sizeof(kmma));
	return 0;
}
  • 这其中,SMBUS接口层由内核提供,作为i2c设备驱动和i2c总线驱动的桥梁,此接口都是为i2c设备驱动层使用。
  • i2c总线驱动层,是芯片厂家根据SMBUS标准进行编写,已提供各个需要用到的i2c时序操作,根据i2c设备驱动的要求发起操作i2c控制器,最终产生符合外设要求的时序情况。
    在这里插入图片描述

编写i2c设备驱动程序

如何编写i2c设备驱动程序呢?

  • 编写i2c设备驱动程序同样是利用内核的分离思想,但是不是platform机制,但是同样是设备-总线-驱动编程模型。如图:
    在这里插入图片描述

实现原理

  1. 首先,内核已经定义好了一个虚拟总线叫 i2c_bus_type,在这个总线上维护着两个链表:dev链表和drv链表。
  2. dev链表上每一个节点描述的都是 i2c 外设的纯硬件信息,对应的数据结构为 struct i2c_client,每当向dev链表添加一个 i2c 外设的硬件信息节点时,只需用此数据结构定义初始化一个对象即可,然后向 dev 链表添加,一旦添加完毕,内核会帮你遍历 drv 链表,从 drv 链表上取出每一个节点与当前这个要注册的硬件节点进行匹配,内核通过调用总线提供的 match 函数进行比较。比较 i2c_client 的name 和 i2c_driver 的id_table的 name ,如果匹配成功,硬件找到了对应的驱动节点,内核会调用 i2c_driver 匹配成功的节点内的probe函数,并且把匹配成功的硬件节点的首地址传递给probe函数,最终完成硬件和软件的再次结合。
  3. drv 链表上每一个节点描述的 i2c 外设的纯软件信息对应的数据结构为 struct i2c_driver ,每当向 drv 链表添加一个i2c外设的软件信息节点时,只需要用此数据结构定义初始化一个对象即可,然后向 drv 链表添加节点,一旦添加完毕,内核就会遍历 dev 链表,从 dev 链表上取出每一个硬件节点跟这个要注册的软件节点进行匹配,内核通过调用总线提供的 match 函数进行比较匹配。如果匹配成功,软件找到了对应的硬件,内核会自动调用 i2c_driver 的probe函数,并且把匹配成功的硬件节点的首地址传递给 probe 函数,最终完成硬件和软件的再次结合。
  4. 驱动开发者要实现一个 i2c 设备驱动,只需要关注以下两个数据结构:
struct i2c_client;
struct i2c_driver;

i2c_client

 struct i2c_client {
 			unsigned short addr;
 			char name[I2C_NAME_SIZE];
 			struct device dev;
 			int irq;
 			...
 };

功能:用于描述 i2c 外设的纯硬件信息。
成员:

  • addr:I2C外设的设备地址,用于找外设,必须初始化!
  • name:用于匹配,必须初始化!
  • dev:重点关注其中的void *platform_data字段,此字段将来用于装载自定义的用于描述I2C外设的硬件信息。
  • irq:如果I2C外设和CPU之间有中断,此字段保存对应的中断号。

切记: Linux内核对 i2c_client 的操作和 platform_device还是有所区别的。驱动开发者不用自己去定义初始化和注册一个 struct i2c_client 硬件节点对象,定义初始化和注册过程统一由内核完成。驱动开发者主需要利用如下数据结构将需要初始化的信息注册到内核中即可,也就是告诉内核将来要初始化的具体信息,内核根据提供的信息来完成初始化。

 struct i2c_client硬件节点对象:
 
 struct i2c_board_info {
	char	type[I2C_NAME_SIZE];
	unsigned short	addr;
	void		*platform_data;
	int		irq;
 		...
 };

功能:驱动开发者利用此数据结果将 i2c 外设的硬件信息告诉Linux内核,将来内核根据提供的 i2c 外设的硬件信息定义初始化和注册一个 i2c_client 硬件节点对象到 dev 链表。
成员:

  • type:指定硬件节点的名称,此字段将来会自动的赋值给i2c_client的name,将来用于匹配, 所以必须初始化!
  • addr:指定I2C外设的设备地址,此字段将来会自动赋值给i2c_client的addr,将来用于找外设,所以必须初始化!
  • platform_data:用于装载自定义的硬件信息,将来会自动赋值给i2c_client.dev.platform_data。
  • irq:如果CPU和外设需要中断,此字段用来指定中断号,将来会赋值给i2c_client.irq

配套函数:

 i2c_register_board_info(int busnum,struct i2c_board_info const *info, unsigned len)

函数功能:注册 i2c 外设硬件信息到内核,将来Linux内核会帮你定义初始化和注册 i2c_client 硬件节点到 dev 链表,内核初始化 i2c_client 所需的内容都是根据 i2c_board_info 来进行提供。
参数:

  • busnum:I2C外设所在的CPU的I2C总线编号必须根据原理图获取。例如:X6818开发板上的mma8653所对应的I2C总线编号为2(第三路I2C总线)
  • info:传递要注册的I2C硬件信息
  • len:用i2c_board_info描述的硬件信息的个数

切记: struct i2c_board_info 的定义、初始化和注册不能采用 insmod / rmmod 进行,必须将代码和 uImage 编译在一起,一般要写到对应的平台文件(内核源码/arch/arm/plat-s5p6818/x6818/device.c)

i2c_driver

struct i2c_driver {
	int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
	int (*remove)(struct i2c_client *client);
	const struct i2c_device_id *id_table;
	...
};

说明:描述 I2C 外设的软件信息
成员:

  • probe:硬件节点和软件节点匹配成功,内核调用形参client指针指向匹配成功的I2C外设的硬件节点。
    • //心里念叨:此client指针里面的内容都是i2c_board_info提供。
      • client->addr //获取I2C外设的设备地址
      • client->irq //获取I2C外设的中断号
      • client->dev.platform_data //获取自定义的硬件信息
  • remove:卸载软件节点,内核调用此函数, 形参client指针指向匹配成功的I2C外设的硬件节点
    • //心里念叨:此client指针里面的内容都是i2c_board_info提供
      • client->addr //获取I2C外设的设备地址
      • client->irq //获取I2C外设的中断号
      • client->dev.platform_data //获取自定义的硬件信息
  • id_table:重点关注其中的name字段,此字段将来用于匹配

配套函数:
i2c_add_driver(&软件节点对象)

  • 向内核drv链表注册添加I2C外设软件节点对象,将来内核会帮你遍历,匹配,调用probe函数,传递参数。

i2c_del_driver(&软件节点对象)

  • 从内核drv链表删除软件节点对象,内核会帮你调用remove函数。

SMBUS接口函数的使用步骤

  1. 打开SMBUS接口函数的说明使用文档,在内核源码的Documentation\i2c\smbus-protocol,打开此文件
  2. 再打开MMA8653的芯片手册,找到对应的读时序图
  3. 根据读时序图在文档smbus-protocol中找到对应的实现函数
  4. 找到对应的函数以后,在sourceinsight中找到这个函数的定义,获取到函数的参数和返回值。

注意: smbus接口函数中的client指针一定要传递匹配成功的硬件节点对象指针。

示例(MMA8653三轴加速度传感器使用)

  • 首先去除官方的MMA8653的三轴加速度传感器驱动。

```bash
cd /opt/kernel
make menuconfig
Deivce Drivers->
	Hardware Monitoring support->
  	//按N键去除
  		<*>Freescale MMA865X 3-Axis Accelerometer 
	```

# 保存退出
make uImage
cp arch/arm/boot/uImage /tftpboot

#重启下位机,进入uboot
tftp 48000000 uImage
bootm 48000000
  • 向内核添加MMA8653硬件信息
cd /opt/kernel
vim arch/arm/plat-s5p6818/x6818/device.c 
# 在文件的最开头添加如下代码:
#	定义初始化MMA8653外设的硬件信息对象
static struct i2c_board_info mma8653[] = {
	{   
	.type = "mma8653",//用于匹配
	//会赋值给i2c_client.name
	.addr = 0x1D //用于找外设
	//会赋值给i2c_client.addr
	}
};
  • 然后函数nxp_board_devs_register,在函数最开头添加:
i2c_register_board_info(2, mma8653, ARRAY_SIZE(mma8653));
  • 只要这条语句执行,内核就会定义初始化和注册一个i2c_client硬件节点对象到dev链表,并且开始各种遍历、匹配、调用、传递参数。i2c_client对象中的所有成员都是我这注册的i2c_board_info来提供的(i2c_client.name=type, i2c_client.addr,i2c_client.dev.platform_data=platform_data, i2c_client.irq = irq)
  • i2c设备驱动 i2c_driver 部分可以通过驱动模块加载的方式加载,当我们加载i2c软件驱动部分时,内核就会完成匹配并自动调用驱动中的probe函数完成初始化。

具体代码如下:

  • arch/arm/plat-s5p6818/x6818/device.c
    在这里插入图片描述
    在这里插入图片描述

  • mma8653_drv.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h> //struct i2c_driver ...
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/delay.h>

//声明描述MMA8653三轴加速度信息数据结构
struct mma8653_data {
    short x; //X轴的加速值
    short y; //Y轴的加速值
    short z; //Z轴的加速值
};

//将来用于匹配的对象
static struct i2c_device_id mma8653_id[] = {
    {"mma8653", 0}, //"mma8653"用于匹配
};

static struct i2c_client *g_client; //全局指针

//client指针指向匹配成功的硬件节点对象
static void mma8653_hw_init(struct i2c_client *client)
{
    int ret = 0;

    //I2C设备驱动调用SMBUS接口函数来操作I2C控制器
    //最终发起硬件操作时序
    //SMBUS接口函数的使用步骤:
    // 1.打开SMBUS接口函数的说明使用文档,在内核源码的Documentation\i2c\smbus-protocol
    // 打开此文件
    // 2.再打开MMA8653的芯片手册,找到对应的读时序图
    // 3.根据读时序图在文档smbus-protocol中找到对应的实现函数
    // 4.找到对应的函数以后,在sourceinsight中找到这个函数的定义
    //获取到函数的参数和返回值
    //注意:smbus接口函数中的client指针一定要传递匹配成功的硬件节点对象指针
    
   //读片内寄存器0x0D的数据
    ret = i2c_smbus_read_byte_data(client, 0x0D);
    printk("%s:addr = %#x, Read ID value is :%#x\n",
                        __func__, client->addr, ret);

    i2c_smbus_write_byte_data(client, 0x2A, 0); //省电模式
    i2c_smbus_write_byte_data(client, 0x0E,0); //设置测量范围+-2g
}

static void mma8653_read_data(struct mma8653_data *mma)
{
	unsigned char tmp_data[7];

       //判断新的数据是否合法有效
        while(!(i2c_smbus_read_byte_data(g_client, 0x00) & 0x08)) {
            printk("data is not ready!\n");
        }

       //将加速度值读取
        i2c_smbus_read_i2c_block_data(g_client, 0x01, 7, tmp_data);


	mma->x = ((tmp_data[0] << 8) & 0xff00) | tmp_data[1];
	mma->y = ((tmp_data[2] << 8) & 0xff00) | tmp_data[3];
	mma->z = ((tmp_data[4] << 8) & 0xff00) | tmp_data[5];

	mma->x = (mma->x) >> 6;
	mma->y = (mma->y) >> 6;
	mma->z = (mma->z) >> 6;

        msleep(20);
}

static void mma8653_config_mode(void)
{
    unsigned char data;
    data =  i2c_smbus_read_byte_data(g_client, 0x2A);
    data |= 1;
    i2c_smbus_write_byte_data(g_client, 0x2A, data);
}

#define GS_MMA8653_GETXYZ_CMD   0x100001

static long mma8653_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct mma8653_data mma; //分配内核缓冲区

    switch(cmd) {
        case GS_MMA8653_GETXYZ_CMD:
            mma8653_config_mode(); //激活MMA8653
            mma8653_read_data(&mma); //读取寄存器的加速度值
            copy_to_user((struct mma8653_data *)arg, &mma, sizeof(mma));
            break;
        default:
            return -1;
    }
    return 0;    
}

//定义初始化硬件操作接口
static struct file_operations mma8653_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = mma8653_ioctl
};

//定义初始化混杂设备对象
static struct miscdevice mma8653_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "mma8653",
    .fops = &mma8653_fops
};

//client指针指向匹配成功的硬件节点对象
static int mma8653_probe(
							struct i2c_client *client, 
							const struct i2c_device_id *id)
{
	printk("MMA8653 设备地址 = %d\n", client->addr);

   //注册混杂设备对象,给用户提供访问操作接口
    misc_register(&mma8653_misc);   
   //把局部进行全局化
    g_client = client;
   
   //初始化mma8653硬件信息
    mma8653_hw_init(client);
    return 0;
}

static int mma8653_remove(struct i2c_client *client)
{
    misc_deregister(&mma8653_misc);    
    return 0;
}

//定义初始化I2C外设的软件节点对象
static struct i2c_driver mma8653_drv = {
    .driver = {
        .name = "tarena" //不重要
    },
    .id_table = mma8653_id, //其中的name用于匹配
    .probe = mma8653_probe, //匹配成功调用
    .remove = mma8653_remove //删除调用
};

static int mma8653_init(void)
{
   //注册软件节点到drv
    i2c_add_driver(&mma8653_drv);
    return 0;
}

static void mma8653_exit(void)
{
   //从drv删除软件节点
    i2c_del_driver(&mma8653_drv);
}

module_init(mma8653_init);
module_exit(mma8653_exit);
MODULE_LICENSE("GPL");

  • mma8653_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#define GS_MMA8653_GETXYZ_CMD   0x100001

struct mma8653_data {
    short x;
    short y;
    short z;
};

int main(void)
{
    int fd;
    struct mma8653_data info; //分配用户缓冲区

    fd = open("/dev/mma8653", O_RDWR);
    if (fd < 0)
        return -1;

    while(1) {
        ioctl(fd, GS_MMA8653_GETXYZ_CMD, &info);
        printf("%d    %d    %d\n", info.x, info.y, info.z);
        //usleep(50000);
    }
    
    close(fd);
    return 0;
}

  • Makefile
obj-m += mma8653_drv.o
#obj-m += mma8653fc.o
all:
	make -C  /opt/x6818_linux/kernel SUBDIRS=$(PWD) modules
clean:
	make -C  /opt/x6818_linux/kernel SUBDIRS=$(PWD) clean 

  • 执行结果:
    在这里插入图片描述
2018-02-27 11:55:37 WY_stutdy 阅读数 188
  • I2C总线和触摸屏驱动移植实战-linux驱动开发第9部分

    本课程是linux驱动开发的第9个课程,主要内容是linux的I2C子系统以及电容触摸屏驱动的移植。学习本课程的核心点在于I2C子系统,目标是对I2C驱动框架的彻底理解和熟练运用。本课程承袭前面platform平台总线的讲解思路和基础,真正做到了对I2C总线做透彻清晰的讲解。

    9760 人正在学习 去看看 朱有鹏
  1. 概述

    本文档以STM32F767平台为例,详细介绍SylixOS上GPIO模仿I2C总线的驱动开发流程。

     

  2. 初始化

    GPIO模仿的I2C总线的初始化,实际上是I2C总线的SDA和SCL的GPIO管脚初始化。初始化流程如图 2.1所示。

    2.1 I2C初始化流程图

    代码实现,如程序清单 2.1所示。I2C总线的SDA和SCL两个GPIO管脚的GPIO速度要设置成快速模式,输出模式需要设置成推挽输出模式。

    程序清单 2.1 I2C初始化代码

        /*
         *  申请 I2C 1 通道的 SCL 的 GPIO
         */
        if (ERROR_NONE != API_GpioRequest(I2C1_CHANNEL_SCL, I2C1_SCL_GPIO_NAME)) {
            return  (PX_ERROR);
        }
    
        /*
         *  设置上拉
         */
        if (ERROR_NONE != API_GpioSetPull(I2C1_CHANNEL_SCL, GPIO_PUPD_PU)) {
            return  (PX_ERROR);
        }
    
        /*
         *  设置为推挽输出模式,且 GPIO 速度为快速
         */
        if (ERROR_NONE != API_GpioDirectionOutput(I2C1_CHANNEL_SCL,
                                                  		   (GPIO_SPEED_SET 	|
                                                  		    GPIO_OTYPE_SET 	|
                                                   	    LW_GPIOF_INIT_HIGH))) {
            return  (PX_ERROR);
        }
    
        /*
         *  申请 I2C 1 通道的 SDA 的 GPIO
         */
        if (ERROR_NONE !=  API_GpioRequest(I2C1_CHANNEL_SDA, I2C1_SDA_GPIO_NAME)) {
            return  (PX_ERROR);
        }
        if (ERROR_NONE != API_GpioSetPull(I2C1_CHANNEL_SDA, GPIO_PUPD_PU)) {
            return  (PX_ERROR);
        }
        if (ERROR_NONE != API_GpioDirectionOutput(I2C1_CHANNEL_SDA,
                                                  		   (GPIO_SPEED_SET 	|
                                                  		    GPIO_OTYPE_SET 	|
                                                   	    LW_GPIOF_INIT_HIGH))) {
            return  (PX_ERROR);
        }
  3. 传输流程

    GPIO模拟I2C总线驱动和普通的I2C总线驱动的最大区别是普通的I2C总线驱动的数据传输只要将要传输的数据写入寄存器即可,而GPIO模拟I2C总线驱动的数据传输是直接通过GPIO管脚将电平拉高拉低(拉高是1,拉低是0)传输数据。

  4. 写数据流程

    如程序清单 3.1所示,I2C的写数据流程如下:

    1. 主设备发送开始信号;
    2. 主设备发送7位从设备地址和1位写操作位;
    3. 从设备发送应答信号;
    4. 主设备发送要写的8位从设备内部地址;
    5. 从设备发送应答信号;
    6. 主设备开始对从设备写操作;
    7. 主设备发送结束信号。

    程序清单 3.1 I2C的写数据流程

    static INT  __i2cXferWrite (UINT             uiChannel,
                                PLW_I2C_MESSAGE  pI2cMsg,
                                INT              iLength)
    {
        INT  iIndex;
    
        __i2cStart(uiChannel);                                              /*  发送开始信号                */
    
        /*
         *  发送 7 位器件地址和 1 位写操作位
         */
        __i2cSendByte((pI2cMsg->I2CMSG_usAddr & I2C_ADDR_MASK), uiChannel);
    
        if (__i2cWaitAck(uiChannel)) {                                      /*  等待设备的 ACK 信号         */
            _DebugHandle(__ERRORMESSAGE_LEVEL, "__i2cXferWrite(): Timeout to wait ack!\r\n");
    
            return  (PX_ERROR);
        }
    
        /*
         *  发送要读的设备的内部地址
         */
        __i2cSendByte(((pI2cMsg->I2CMSG_usAddr) & I2C_INTER_ADDR_MASK), uiChannel);
        if (__i2cWaitAck(uiChannel)) {                                      /*  等待设备的 ACK 信号         */
            _DebugHandle(__ERRORMESSAGE_LEVEL, "__i2cXferWrite(): Timeout to wait ack!\r\n");
    
            return  (PX_ERROR);
        }
    
        for (iIndex = 0; iIndex < iLength; iIndex++) {
            __i2cSendByte(*(pI2cMsg->I2CMSG_pucBuffer)++, uiChannel);       /*  发送字节                    */
            if (__i2cWaitAck(uiChannel)) {                                  /*  等待设备的 ACK 信号         */
                _DebugHandle(__ERRORMESSAGE_LEVEL, "__i2cXferWrite(): Timeout to wait ack!\r\n");
    
                return  (PX_ERROR);
            }
        }
    
        __i2cStop(uiChannel);                                               /*  产生一个停止信号            */
    
        udelay(I2C_WRITE_BYTE_DELAY);
    
    
        return (ERROR_NONE);
    }

  5. 读数据流程

    如程序清单 3.2所示,I2C的读数据流程如下:

    1. 写模式,主设备发送开始信号;
    1. 主设备发送7位从设备地址和1位写操作位;
    2. 从设备发送应答信号;
    3. 主设备发送要写的8位从设备内部地址;
    4. 从设备发送应答信号;
    5. 进入读取模式,设备再次发送开始信号;
    6. 主设备发送7位从设备地址和1位读操作位;
    7. 从设备发送应答信号;
    8. 主设备开始对从设备读操作;
    9. 主设备发送结束信号。

    程序清单 3.2 I2C读数据流程

static INT  __i2cXferRead (UINT             uiChannel,
                           PLW_I2C_MESSAGE  pI2cMsg,
                           INT              iLength)
{
    INT  iIndex;

    __i2cStart(uiChannel);                                              /*  发送开始信号                */

    /*
     *  发送 7 位器件地址和 1 位写操作位,(I2CMSG_usAddr 中的 9-15 位为器件地址)
     */
    __i2cSendByte(((pI2cMsg->I2CMSG_usAddr >> 8) & I2C_ADDR_MASK), uiChannel);

    if (__i2cWaitAck(uiChannel)) {                                      /*  等待设备的 ACK 信号         */
        _DebugHandle(__ERRORMESSAGE_LEVEL, "__i2cXferWrite(): Timeout to wait ack!\r\n");

        return  (PX_ERROR);
    }

    /*
     *  发送要读的设备的内部地址
     */
    __i2cSendByte(((pI2cMsg->I2CMSG_usAddr) & I2C_INTER_ADDR_MASK), uiChannel);
    if (__i2cWaitAck(uiChannel)) {                                      /*  等待设备的 ACK 信号         */
        _DebugHandle(__ERRORMESSAGE_LEVEL, "__i2cXferWrite(): Timeout to wait ack!\r\n");

        return  (PX_ERROR);
    }

    /*
     *  进入读取模式
     */
    __i2cStart(uiChannel);                                              /*  发送开始信号                */

    /*
     *  发送 7 位器件地址和 1 位读操作位,(I2CMSG_usAddr 中的 8-15 位为器件地址和读写位)
     */
    __i2cSendByte(((pI2cMsg->I2CMSG_usAddr >> 8) & I2C_ADDR_MASK) | LW_I2C_M_RD, uiChannel);
    if (__i2cWaitAck(uiChannel)) {                                      /*  等待设备的 ACK 信号         */
        _DebugHandle(__ERRORMESSAGE_LEVEL, "__i2cXferWrite(): Timeout to wait ack!\r\n");

        return  (PX_ERROR);
    }

    for (iIndex = 0; iIndex < iLength - 1; iIndex++) {

        /*
         *  读取设备发来的 1 个字节数据
         */
        *(pI2cMsg->I2CMSG_pucBuffer)++ = __i2cReadByte(I2C_ACK_SEND, uiChannel);
    }

    *(pI2cMsg->I2CMSG_pucBuffer) = __i2cReadByte(I2C_NACK_SEND, uiChannel);

    __i2cStop(uiChannel);                                               /*  产生停止信号                */


    return  (ERROR_NONE);
}

2019-04-16 18:52:18 qq_38131812 阅读数 142
  • I2C总线和触摸屏驱动移植实战-linux驱动开发第9部分

    本课程是linux驱动开发的第9个课程,主要内容是linux的I2C子系统以及电容触摸屏驱动的移植。学习本课程的核心点在于I2C子系统,目标是对I2C驱动框架的彻底理解和熟练运用。本课程承袭前面platform平台总线的讲解思路和基础,真正做到了对I2C总线做透彻清晰的讲解。

    9760 人正在学习 去看看 朱有鹏

I2C总线经常挂载eeprom、温度传感器、湿度传感器等设备。I2C总线下可以挂载多个设备,识别设备采用一个地址,这个地址在一条i2c总线是独一无二的。I2C总线驱动与spi总线驱动框架是一致的,都是采用控制器、core、设备三层驱动。
内核中I2C 的处理已经做好了,我们只需要做设备驱动程序相关的内容。总线处理好了I2C 协议,即总线知道如何收发数据,而不知道数据的含义,我们要做的只是设备相关层的代码。
I2C 协议中,先发出7bit“设备地址”,然后是1 位“写”或“读”的标志位。然后接着是每发出8 位数据有一个ACK 位。

一般I2C 驱动分为两层
总线层:知道设备如何读写。芯片厂家会帮我们做好,操作寄存器。
设备驱动层:知道数据的含义。

一、4个数据结构的作用及区别

1、i2c_adapteri2c_algorithm

i2c_adapter:对应于物理上的适配器
i2c_algorithm:对应于一套通信方法
缺少了i2c_algorithmi2c_adapter什么也做不了。
i2c_algorithm中的关键函数master_xfer用于产生i2c访问周期所需要的信号,以i2c_msg为单位,i2c_msg非常重要,它包含了i2c的传输地址、方向、缓冲区、缓冲区长度。

2、i2c_driveri2c_client

i2c_driver:对应一套驱动方法
i2c_client:对应于真实的物理设备
每个i2c设备都需要一个i2c_client来描述。i2c_driveri2c_client是一对多的关系,一个i2c_driver可以支持多个同类型的i2c_client

3、i2c_clienti2c_adapter

i2c_client依赖于i2c_adapteri2c_adapter可以被多个i2c_client依赖。

二、总线驱动

I2C总线控制器通常是在内存上,所以它本身是在platform总线上。使用总线驱动模型,通过platform_driverplatform_device的匹配来执行。

(1)适配器驱动的初始化

platform_driverprobe函数中完成两个工作:
1、初始化I2C适配器的硬件资源,申请IO、中断号、时钟等;
2、使用i2c_add_adapter()添加i2c_adapter数据结构,并且在此之前i2c_adapter已经被初始化。

static int xxx_i2c_probe(struct platform_device *pdev)
{
	struct i2c_adapter *adap;
	...
	xxx_adapter_hw_init();
	,,,
	i2c_add_adapter(adap);
}

platform_driverremove函数中完成相反的两个工作。

(2)i2c总线的通信方法

主要是实现i2c_algorithmfunctionalitymaster_xfer函数。

三、设备驱动

1、初始化i2c_driver 和模块的加载和卸载

(同大部分总线驱动一样,不再赘述)

2、数据传输

在i2c设备上读写数据的时序且数据常通过i2c_msg组织,最后由i2c_transfer()发送

struct i2c_msg msg[2];
//第一条是写消息
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].len = 1;
msg[0].buf = &offs;

//第二条是读消息
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].len = sizeof(buf);
msg[1].buf = &buf[0];

i2c_transfer(client->adapter, msg, 2);

更详细示例的参考drivers/i2c/busses/i2c-tegra.c

没有更多推荐了,返回首页