【I2C】Linux I2C子系统分析

一、I2C体系架构

主要由部分组成:

  1. I2C核心层
    提供I2C Adapter、I2C设备驱动的注册和注销方法,I2C Algorithm通信方法,与适配器无关的代码以及探测设备等。
  2. I2C Adapter控制器驱动
    它一般是由芯片原厂来完成编写,根据Device Tree里面的I2C硬件资源来配置新的I2C Adapter,然后将它注册到I2C核心层。
  3. I2C设备驱动
    I2C设备驱动它包含:I2C Driver和I2C Client。i2c_driver会提供该类I2C硬件的驱动方法,I2C Client会提供当前真实I2C物理设备信息,如:I2C设备地址、挂载的哪条I2C总线(I2C Adapter)等。

在这里插入图片描述

二、主要的结构体

1. i2c_adapter

一般i2c adapter的注册都是由芯片原厂来完成,它一般存放在kernel\drivers\i2c\busses\目录下。==i2c_adapter==的核心是i2c_algorithm,主要是它提供了I2C read/write收发数据的时方法。

struct i2c_adapter {
	const struct i2c_algorithm *algo; /* 存放I2C传输数据的方法 */
	...
	int nr; // I2C adapter的编号,默认从0开始。
	char name[48]; // adapter的name,如:i2c@21a4000
	...
};

下面以kernel\drivers\i2c\busses\i2c-imx.c注册一个I2C Adapter的为例,简答分析它如何注册的一个adapter的。
设备树里面的compatible和驱动里面的of_device_id.compatible匹配成功后,就调用驱动里面的probe函数。
probe函数主要完成如下几个步骤:

  1. 给i2c adapter申请空间
  2. 初始化i2c adapter
  3. 将i2c adapter注册到i2c核心层
struct imx_i2c_struct {
	struct i2c_adapter	adapter;
	...
};

static int i2c_imx_probe(struct platform_device *pdev)
{
	...
	/* 1.给i2c adapter申请空间 */
	i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
	/* 2.初始化i2c adapter */
	strlcpy(i2c_imx->adapter.name, pdev->name, sizeof(i2c_imx->adapter.name));
	i2c_imx->adapter.owner		= THIS_MODULE;
	i2c_imx->adapter.algo		= &i2c_imx_algo; // 这里就是imx实现的I2C传输数据方法
	i2c_imx->adapter.dev.parent	= &pdev->dev;
	i2c_imx->adapter.nr		= pdev->id; // 配置adaper的编号
	i2c_imx->adapter.dev.of_node	= pdev->dev.of_node;
	...
	/* 3.将i2c adapter注册到i2c核心层 */
	ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
	...
}

再进一步分析i2c_add_numbered_adapter,它是如何注册一个adapter的?
它们的调用关系大致如下:
在这里插入图片描述

  • idr_alloc函数:
    新的adapter将根据总线number添加到I2C Core的全局变量i2c_adapter_idr中,为以后get对应的adapter做准备。
    在这里插入图片描述
    后面的所有的adapter的注册都会挂载在i2c_adapter_idr上,最终的结果大致如下:
    在这里插入图片描述

  • device_register函数:
    这里主要是在I2C总线下,注册一个i2c adapter类型的设备。部分代码如下:
    在这里插入图片描述
    device_register函数注册成功后,会在/sys/bus/i2c/devices目录下创建对应的文件夹。如下:
    在这里插入图片描述
    device_register函数非常的关键,在adapter被添加时会导致i2c-dev.c中的i2cdev_notifier_call被调用。

    device_register
    -->device_add
       -->blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
      				     BUS_NOTIFY_ADD_DEVICE, dev);
    

    因为在i2c_dev_init有注册一个notifier

    static int i2cdev_notifier_call(struct notifier_block *nb, unsigned long action,
      		 void *data)
    {
      	struct device *dev = data;
      
      	switch (action) {
      	case BUS_NOTIFY_ADD_DEVICE:
      		return i2cdev_attach_adapter(dev, NULL);
      	case BUS_NOTIFY_DEL_DEVICE:
      		return i2cdev_detach_adapter(dev, NULL);
      	}
      
      	return 0;
    }
      
    static struct notifier_block i2cdev_notifier = {
      	.notifier_call = i2cdev_notifier_call,
    };
      
    static int __init i2c_dev_init(void)
    {
    	...
    	res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);
    	...
    }
    

    接收到通知后,i2cdev_attach_adapter就会被调用,这里创建/dev/i2c-*字符设备。代码如下:
    在这里插入图片描述
    所以,当成功添加一个i2c adapter后,都会在/dev/目录下创建i2c-*设备。结果如下:
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/27e0f756d3534474b493f9f149f468e5.png

  • of_i2c_register_devices函数:
    在这里插入图片描述

    • of_i2c_get_board_info:它会解析dts里面I2C子节点数据。
      &i2c1 {
      clock-frequency = <100000>;
      pinctrl-names = "default";
      pinctrl-0 = <&pinctrl_i2c1>;
      status = "okay";
      
      mag3110@e {
      	compatible = "fsl,mag3110";
      	reg = <0x0e>;
      	position = <2>;
      };
      
    • i2c_new_device :它创建一个i2c_client设备,实际对应外围I2C设备。
      /* 下面是i2c_new_device的调用关系 */
      i2c_new_device
      -->i2c_new_client_device
         -->i2c_dev_set_name
         -->device_register
      
      static void i2c_dev_set_name(struct i2c_adapter *adap,
      		     struct i2c_client *client,
      		     struct i2c_board_info const *info)
      {
      	...
      	dev_set_name(&client->dev, "%d-%04x", i2c_adapter_id(adap),
      		     i2c_encode_flags_to_addr(client));
      }
      
      i2c_dev_set_name设置I2C设备的名字,例如:0-000e,0:代表i2c adapter0 (i2c1),000e:代表I2C设备从机地址。device_register函数会创建该设备,然后我们就可以在/sys/bus/i2c/devices目录创建对应的目录。
      在这里插入图片描述
  • i2c_scan_static_board_info函数:
    __i2c_board_list保存了通过i2c_register_board_info函数静态注册设备信息,然后根据board info信息通过i2c_new_device来创建一个i2c_client设备。
    在这里插入图片描述
    文件路径:kernel\drivers\i2c\i2c-boardinfo.c

    int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len)
    {
    	...
    	list_add_tail(&devinfo->list, &__i2c_board_list);
    	...
    }
    

2. i2c_algorithm

i2c_algorithm是根据Soc的I2C硬件寄存器来实现read、write通信的方法,适配器需要通过i2c_algorithm提供的通信函数来产生对应的访问时序。所以i2c_adapter中包含i2c_algorithm的指针。

i2c_algorithm使用master_xfer()来产生I2C时序,以i2c_msg为单位,i2c_msg代表一次传输的数据。

//I2C传输方法
struct i2c_algorithm {
    //i2c传输函数指针
    int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
			   int num);
    //smbus传输函数指针  
    int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
			  unsigned short flags, char read_write,
			  u8 command, int size, union i2c_smbus_data *data);
	...
};

???问题:收发数据的函数master_xfer是如何被调用的???
我们可以通过i2c_master_sendi2c_master_recv函数来收发I2C设备数据,从而分析它的调用关系如下:
在这里插入图片描述

3. i2c_driver

i2c_dirver就是i2c标准总线设备驱动模型中的驱动部分,它主要是现实该I2C设备具体初始化、操作函数接口等的实现,等待app程序的对该I2C设备实现业务上面的逻辑。i2c_dirver这部分的就是需要我们自己的来实现的。

// I2C设备驱动
struct i2c_driver {
	...
	/* Standard driver model interfaces */
	int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
	int (*remove)(struct i2c_client *client);
	struct device_driver driver; // 驱动的name和匹配device tree
	const struct i2c_device_id *id_table; //该设备所支持的设备ID表
};

下面是举例的一个模板代码:

static const struct of_device_id of_match_ids_xxxx[] = {
	{ .compatible = "xxxx,xxxx",		.data = NULL },
	{ /* END OF LIST */ },
};

static const struct i2c_device_id xxxx_ids[] = {
	{ "xxxx",	(kernel_ulong_t)NULL },
	{ /* END OF LIST */ }
};

static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static int ap3216c_remove(struct i2c_client *client)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static struct i2c_driver i2c_xxxx_driver = {
	.driver = {
		.name = "xxxx",
		.of_match_table = of_match_ids_xxxx,
	},
	.probe = xxxx_probe,
	.remove = xxxx_remove,
	.id_table = xxxx_ids,
};


static int __init i2c_driver_xxxx_init(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return i2c_add_driver(&i2c_xxxx_driver);
}

static void __exit i2c_driver_xxxx_exit(void)
{
	i2c_del_driver(&i2c_xxxx_driver);
}

module_init(i2c_driver_xxxx_init);
module_exit(i2c_driver_xxxx_exit);
MODULE_LICENSE("GPL");

4. i2c_client

i2c_clent对应真实的物理设备,每个i2c设备都需要一个i2c_client来描述。i2c_driveri2c_client是一对多的关系,一个i2c_driver上可以支持多个同类型的i2c_client

struct i2c_client {
	unsigned short flags;		/* 标记,标记相关属性		*/
	unsigned short addr;		/* 芯片地址,7bit地址	*/
	char name[I2C_NAME_SIZE];   /* i2c设备的名称 */
	struct i2c_adapter *adapter;	/* 依附的i2c_adapter	*/
	struct device dev;		/* 该设备相关属性配置	*/
	int irq;			/* 设备使用的中断号		*/
	...
};

创建i2c_clent种方式,具体如下:

4.1 方式一:通过I2C bus number静态方式来创建

i2c_client可以通过i2c_register_board_info函数来创建,并且会注册到I2C总线。重要提示:这个方式最大的缺陷是就是一定要编译进内核,并且该驱动一定要在i2c adapter驱动之前就要加载,否则会导致该设备未注册从而导致对应的驱动不会被加载。 解决方法:可以使用arch_initcall将该模块提前被加载。

static int __init i2c_client_ap3216c_init(void)
{
	static struct i2c_board_info board_info[] = {
	  {I2C_BOARD_INFO("ap3216c", 0x1e),},
	};
	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* register I2C device, 0:代表i2c-0总线 */
    i2c_register_board_info(0, board_info, ARRAY_SIZE(board_info));
	
	return 0;
}

arch_initcall(i2c_client_ap3216c_init);

i2c_register_board_info其实就是添加board info到__i2c_board_list全局链接,然后在添加i2c adapter时,遍历该链接将挂载当前adapter的设备进行创建。(备注:上面在介绍adapter的时候已经分析过这块,如需要可以向上回看。)

4.2 方式二:通过Device Tree来创建

这种方式是目前使用最多的方式,将需要的I2C设备信息添加到对应的I2C总线下。一般只需要包括2个单元:

  1. compatible:该name将被用来匹配i2c driver。
  2. reg :挂载的I2C Slave设备地址,这里是7Bit地址,不包括读写位。

(备注:上面在介绍adapter的时候已经分析过这块,如需要可以向上回看。)

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";
	
	mag3110@e {
		compatible = "fsl,mag3110";
		reg = <0x0e>;
		position = <2>;
	};
};

4.3 方式三:直接通过i2c_new_device来创建

其实其它方式都会最终会调用i2c_new_device来创建I2C Client设备,我们可以通过它来直接创建。使用该函数需要知道i2c_adapter,我们可以通过i2c_get_adapter函数来获取。具体参考代码如下:

static struct i2c_client *ap3216c_client;

static int __init i2c_client_ap3216c_init(void)
{
    struct i2c_adapter *adapter;

	static struct i2c_board_info board_info = {
	  I2C_BOARD_INFO("ap3216c", 0x1e),
	};
	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* register I2C device, 0:代表i2c-0总线 */
	adapter = i2c_get_adapter(0);
	ap3216c_client = i2c_new_device(adapter, &board_info);
	i2c_put_adapter(adapter);
	
	return 0;
}

static void __exit i2c_client_ap3216c_exit(void)
{
    i2c_unregister_device(ap3216c_client);
}

module_init(i2c_client_ap3216c_init);
module_exit(i2c_client_ap3216c_exit);
MODULE_LICENSE("GPL");

4.3 方式四:通过用户空间(user-space)手动来创建

调试时、或者不方便通过代码明确地生成i2c_client时,可以通过用户空间来生成。

#创建一个i2c_client, .name = "ap3216c", .addr=0x1e, .adapter是i2c-0
echo ap3216c 0x1e > /sys/bus/i2c/devices/i2c-0/new_device
  
#删除一个i2c_client
echo 0x1e > /sys/bus/i2c/devices/i2c-0/delete_device

这里所使用的new_devicedelete_device是在添加i2c adapter时创建的,i2c_adapter_type包含了该设备的相关属性。
在这里插入图片描述
下面代码可以看到i2c_adapter_type就包含了new_devicedelete_device相关属性,如果使用文件系统操作new_device节点,那么就会调用i2c_sysfs_new_device->i2c_new_client_device函数来创建i2c_client设备。

static ssize_t
i2c_sysfs_new_device(struct device *dev, struct device_attribute *attr,
		     const char *buf, size_t count)
{
	struct i2c_client *client;
	...
	client = i2c_new_client_device(adap, &info);
	...
}
static DEVICE_ATTR(new_device, S_IWUSR, NULL, i2c_sysfs_new_device);

static struct attribute *i2c_adapter_attrs[] = {
	&dev_attr_name.attr,
	&dev_attr_new_device.attr,
	&dev_attr_delete_device.attr,
	NULL
};
ATTRIBUTE_GROUPS(i2c_adapter);

struct device_type i2c_adapter_type = {
	.groups		= i2c_adapter_groups,
	.release	= i2c_adapter_dev_release,
};
EXPORT_SYMBOL_GPL(i2c_adapter_type);

下面是i2c adapter创建成功后,进入文件系统后我们可以看到的相关属性的节点。
在这里插入图片描述

5. i2c_msg

在Linux驱动里面使用i2c收发函数最终都会调用到i2c_transfer,它传输数据的单元基本都是i2c_msg

//I2C传输数据结构体,代表一个消息数据
struct i2c_msg {
	__u16 addr;	 //设备地址
	__u16 flags;  //标志
	__u16 len;	//消息长度
	__u8 *buf;	//消息数据
};

下面是是直接使用i2c_msg 来收发数据的伪代码如下:

struct i2c_client *xxx_client= NULL;

static int i2c_read_bytes(unsigned char *reg, char *data, int data_len)
{
    struct i2c_msg msgs[] = {
        {
            .addr    = xxx_client->addr,
            .flags    = 0,
            .len    = 1,
            .buf    = reg,
        },{
            .addr    = dev_addr,
            .flags    = I2C_M_RD,
            .len    = data_len,
            .buf    = data,
        }
    };

    if (i2c_transfer(xxx_client->adapter, msgs, 2) < 0)
        return -1;
    else
        return 0;

}

static int i2c_write__bytes(unsigned char *buf, int len)
{
    struct i2c_msg msg[] = {
        {
            .addr    = xxx_client->addr,
            .flags    = 0,
            .len    = len,
            .buf    = buf,
        }
    };

    if (i2c_transfer(xxx_client->adapter, msg, 1) < 0)
        return -1;
    else
        return 0;
}

static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	xxx_client = client;
	
	return 0;
}

三、参考资料

1:Linux驱动之I2C驱动架构
https://blog.51cto.com/u_11866419/4935228
2:Linux i2c 学习 -2 i2c adapter 注册
https://blog.csdn.net/Royal2012/article/details/116081632

四、验证测试时使用

测试验证四种创建i2c client的方法代码:
在这里插入图片描述
参考代码下载地址:
https://download.csdn.net/download/ZHONGCAI0901/87855699

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值