Linux I2C驱动架构

@

博客说明

| 撰写日期 | 2019.11.20
---|:--😐---
| 完稿日期 | 2019.11.21
| 最近维护 | 暂无
| 本文作者 | multimicro
| 联系方式 | multimicro@qq.com
| 资料链接 | 本文无附件资料
| GitHub| https://github.com/wifialan/drivers
| 原文链接| https://blog.csdn.net/multimicro/article/details/103164746

开发环境

环境说明 详细信息 备注信息
操作系统 Ubunut 18.04
开发板 JZ2440-V3
u-boot uboot-2012.04.01
busybox busybox-1.22.1
u-boot和busybox编译器 arm-linux-gcc (4.4.3)
Linux内核 linux-4.19-rc3
Linux内核编译器 arm-linux-gnueabi-gcc (4.9.4)

1. Linux I2C 体系结构

i2c的编程应用流程请参考我的上一篇文章Linux 设备树学习——基于i2c总线分析,这篇文章我是基于i2c总线而写的,里面包含注册和匹配方式的介绍,和相应的模板。

参考宋宝华 《Linux设备驱动开发详解》 第15章内容。


Linux的I2C体系结构分为3个组成部分:

  1. I2C核心
  2. I2C总线驱动(适配器驱动)
  3. I2C设备驱动

1.1 Linux I2C核心

借鉴 宋宝华 《Linux设备驱动开发详解》 第15章第2节内容。

I2C核心(drivers/i2c/i2c-core-base.c)中提供了一组不依赖于硬件平台的接口函数,这个文件一般不需要被工程师修改,但是理解其中的主要函数非常关键,因为I2C总线驱动和设备驱动之间以I2C核心作为纽带。I2C核心中的主要函数如下。

1.1.1 增加 / 删除 i2c_adapter

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

在有设备树版本中,内部都封装了从设备树中获取i2c设备并注册为i2c client的方法,如下:

/i2c节点一般表示i2c控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
   platform_driver的probe函数中会调用i2c_add_numbered_adapter:
   
      
   i2c_add_numbered_adapter// drivers/i2c/i2c-core-base.c
			 __i2c_add_numbered_adapter
           			 i2c_register_adapter
                			of_i2c_register_devices(adap);   // drivers/i2c/i2c-core-of.c
                   					 for_each_available_child_of_node(bus, node) {
                       						 client = of_i2c_register_device(adap, node);
                                        			client = i2c_new_device(adap, &info);   // 设备树中的i2c子节点被转换为i2c_client
                    }

上述第一个函数i2c_add_numbered_adapter内容如下,通过Source Insight软件,可以一步一步定位分析,可以很好的帮助理解设备树汇总的i2c子设备节点是如何转换为i2c client的。

int i2c_add_numbered_adapter(struct i2c_adapter *adap)
{
	if (adap->nr == -1) /* -1 means dynamically assign bus id */
		return i2c_add_adapter(adap);

	return __i2c_add_numbered_adapter(adap);
}

1.1.2 增加 / 删除 i2c_driver

int i2c_register_driver(struct module *owner, struct i2c_driver *driver);
void i2c_del_driver(struct i2c_driver *driver);
#define i2c_add_driver(driver)		\
				i2c_register_driver(THIS_MODULE, driver)

这些函数一般用在编写i2c设备driver中,如
drivers/char/at24c256.c

static int __init at24_init(void)
{
    return i2c_add_driver(&at24c256_driver);
}

static void __exit at24_exit(void)
{
    i2c_del_driver(&at24c256_driver);
    printk(DRV_NAME "\tRemove i2c driver success\n");
}

module_init(at24_init);
module_exit(at24_exit);

1.1.3 I2C传输、发送和接收

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
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一次可以传输多个i2c_msg
i2c_master_send 一次只能发送一个i2c_msg
i2c_master_recv 一次只能接收一个i2c_msg

在开发比较复杂的i2c时序时,采用最多的是i2c_transfer。参考 Code 1

Code 1

	int ret;
    struct i2c_msg msg[2];
    char buffer_data[100];

    memset(buffer_data,0,sizeof(buffer_data));
    buffer_data[0] = (char)0x00;
    buffer_data[1] = (char)0x00;
    buffer_data[2] = (char)0xAA;
    buffer_data[3] = (char)0xAB;
    buffer_data[4] = (char)0xAC;
    
    /* 1st write data */
    msg[0].addr = at24_dev->client->addr | 0x01;	//这个是i2c从设备的地址
    msg[0].flags = at24_dev->client->flags & 0;		//这个是对i2c从设备的读写标志位,和上面的addr组成i2c的第一个8bit数据
    msg[0].buf = &buffer_data[0];					//这个是i2c从设备地址后的数据,数组中,每一个数据都是一个8bit数据
    msg[0].len = 5;		//指定发送除地址位外的多少个数据,上述buffer_data中,前两个是at24c256的从设备地址,后3个是写入的数据
    /* 2nd write data */
    msg[1].addr = at24_dev->client->addr | 0x01;
    msg[1].flags = I2C_M_RD;
    msg[1].buf = &buffer_data[5];	//指定接受的数据存放位置,这里从buffer_data中的第6个位置中开始存储读到的数据,
    msg[1].len = 3;					//指定读的数据个数
    
    //开始i2c传输,发送成功后,会返回发送的次数,由最后一个参数为2可以,需要开启两次传输,那么发送成功后就会返回2
    ret = i2c_transfer(at24_dev->client->adapter, &msg[0], 2);	
    tmp = (ret == 2) ? msg[1].len : ret;

i2c_transfer发送失败会返回特定的负数,详情参考linux i2c 的通信函数i2c_transfer在什么情况下出现错误

1.2 Linux I2C适配器驱动

1.2.1 I2C适配器驱动的注册与注销

借鉴 宋宝华 《Linux设备驱动开发详解》 第15章第3节内容。

由于I2C总线控制器通常是在内存上的,所以它本身也连接在platform总线上,要通过platform_driver和platform_device的匹配来执行。因此尽管I2C适配器给别人提供了总线,它自己也被认为是接在platform总线上的一个客户。Linux的总线、设备和驱动模型实际上是一个树形结构,每个节点虽然可能成为别的总线控制器,但是自己也被认为是从上一级总线枚举出来的。

也就是说,I2C适配器的初始化,需要在platform_driverprobe()函数中完成。这部分大多由芯片厂商提供好了,自己不需要编写。

1.2.2 I2C总线的通信方法

只有I2C适配器驱动是没有灵魂的,需要给它注入一个通信算法i2c_algorithm,才能然I2C运作起来。

可以说I2C适配器驱动是来配置I2C硬件,而i2c_algorithm是来操作I2C硬件产生对应的I2C时序波形,完成基于I2C总线的数据传输。

我在开发时,没有自己编写通信方法,但是也可以实现通信,猜测这个也是芯片厂商给写好的,直接调用接口

1.3 Linux I2C设备驱动

这方面主要是完成I2C设备驱动模块的加载与卸载,并实现数据传输,模块的加载与卸载就是经常编写的驱动开发流程,详情参考附录中github代码。

在编写i2c设备driver中的i2c设备读写函数中,如下,我将AT24C256这个i2c设备注册成了字符设备,调用了file_operations结构体里面的读写方法。参考 Code 2Code 3

drivers/char/at24c256.c
Code 2

static ssize_t at24_write( struct file *filp, const char __user *buffer, size_t size, loff_t *f_pos )
{
    int ret,tmp;
    char i;
    struct i2c_msg msg;
    char buffer_data[100];

    copy_from_user(buffer_data, buffer, 10);

    memset(buffer_data,0,sizeof(buffer_data));
    buffer_data[0] = (char)0x00;
    buffer_data[1] = (char)0x00;

    for (i = 0; i < 64; ++i)
    {
        buffer_data[i+2] = i;
    }

    msg.addr = at24_dev->client->addr | 0x01;			//这个是i2c从设备的地址
    msg.flags = at24_dev->client->flags & 0;
    msg.buf = &buffer_data[0];							 //这个是i2c从设备地址后的数据
    msg.len = 66;

    ret = i2c_transfer(at24_dev->client->adapter,&msg,1);
    tmp = (ret == 1) ? msg.len : ret;

    printk("i2c code: %d  return code: %d addr: 0x%02x%02x ",\
        ret,tmp,buffer_data[0],buffer_data[1]);

    for (i = 0; i < 64; ++i)
    {
        printk("Write Data: 0x%02x",buffer_data[i+2]);
    }
    
    return 0;
}

Code 3

static ssize_t at24_read( struct file *filp, const char __user *buffer, size_t size, loff_t *f_pos )
{
    int ret,tmp;
    unsigned long i;
    struct i2c_msg msg[3];
    char buffer_data[100];

    memset(buffer_data,0,sizeof(buffer_data));
    
    msg[0].addr = at24_dev->client->addr | 0x01;
    msg[0].flags = I2C_M_RD;
    msg[0].buf = &buffer_data[2];
    msg[0].len = 64;

    ret = i2c_transfer(at24_dev->client->adapter, &msg[0], 1);
    tmp = (ret == 1) ? msg[0].len : ret;

    printk("i2c code: %d  return code: %d addr: 0x%02x%02x ",\
        ret,tmp,buffer_data[0],buffer_data[1]);

    for (i = 0; i < 64; ++i)
    {
        printk("Read Data: 0x%02x",buffer_data[i+2]);
    }

    return 0;
}

附录

  1. Github代码:at24c256.c
posted @ 2019-11-21 14:54  multimicro  阅读(608)  评论(0编辑  收藏  举报