Linux驱动框架之i2c驱动框架解析

一、引言

       本文会通过简要的文字描述和源码分析,为大家分析梳理出Linux下的I2C驱动框架。
      (文本所有代码都是经过删减的,所以不要纠结和源代码有出入的地方)。

二、I2C总线

       I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。
       它只需要两根线(时钟线和数据线)即可在连接于总线上的器件之间传送信息。

三、Linux下的I2C驱动开发思路

       目前,在Linux下开发i2c驱动,主要有两种途径:
       - 将I2C当成普通字符设备来写,一切靠自己,要十分熟悉I2C协议和I2C操作才行;
       - 借助Linux封装好的I2C驱动框架来写,需要了解I2C框架,熟悉它提供的标准接口。
       只要接触学习过一段时间Linux驱动,相信都不会选择第一种方案。原因很简单。首先,不是所有人都对I2C协议和I2C操作很了解,因此不能保证其可靠性,其次,第一种方案写出来的驱动程序基本不具备可移植性。

四、Linux下的I2C驱动框架

       Linux下的I2C驱动框架被分为三部分:I2C设备驱动、I2C总线驱动和I2C核心。

1、I2C核心

       I2C核心是I2c总线和I2c设备驱动的中间枢纽,它以通用的、与平台无关的接口实现了I2C中设备与适配器的沟通,提供了I2C总线驱动和设备驱动的注册、注销方法,I2C通信方法(即“algorithm”)。

2、I2C总线驱动

       I2C总线驱动是对I2C硬件体系结构中适配器的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。
       I2C总线驱动主要包含了I2C适配器的数据结构(i2c_adapter)、I2C适配器的通信算法的数据结构(i2c_algorithm)等。
       经由I2C总线驱动的代码,我们可以控制I2C适配器以主控制方式产生开始、停止位、读写周期,以及以从设备方式读写、产生ACK等。

3、I2C设备驱动

       I2C设备驱动(也称为客户端驱动)是对I2C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。
       I2C设备驱动主要包含了数据结构体i2c_driver和i2c_client。

4、i2c_adapter和i2c_client。

       i2c_adapter:即I2C适配器。简单来说,一根i2c总线就是一个i2c_adapter。
       i2c_client:即I2C客户端。我们知道,一根i2c总线可以挂载多个从设备,所以,一个从设备就是一个i2c_client,对于实际项目而言,一般一根i2c总线就挂载1~2个从设备。

五、源码分析

1、I2C核心

       I2C核心源码路径:linux/driver/i2c。
       I2C核心源码是linux内核开发者所维护的,不需要我们关心。因为它是I2c总线和I2c设备驱动的中间枢纽,所以我们通过对I2c总线和I2c设备驱动来学习了解它。

2、I2C总线驱动

       I2c总线驱动一般由硬件厂商所写,不需要我们普通驱动开发者关心,需要我们全权负责的,是I2C设备驱动的开发。
       I2c总线驱动源码路径:linux/driver/i2c/busses。
       因为我这里的平台是全志,所以我的I2C总线驱动为i2c-sunxi.c。
       浏览驱动,从驱动入口看起。

static const struct of_device_id sunxi_i2c_match[] = {
	{ .compatible = "allwinner,sun8i-twi", },
	{},
};

static struct platform_driver sunxi_i2c_driver = {
	.probe		= sunxi_i2c_probe,
	.remove		= sunxi_i2c_remove,
	.driver		= {
		.name	= SUNXI_TWI_DEV_NAME,
		.owner	= THIS_MODULE,
		.pm		= SUNXI_I2C_DEV_PM_OPS,
		.of_match_table = sunxi_i2c_match,
	},
};

static int __init sunxi_i2c_adap_init(void)
{
	return platform_driver_register(&sunxi_i2c_driver);
}

static void __exit sunxi_i2c_adap_exit(void)
{
	platform_driver_unregister(&sunxi_i2c_driver);
}

module_init(sunxi_i2c_adap_init);
module_exit(sunxi_i2c_adap_exit);

       从代码中,我们可以看到驱动的入口为sunxi_i2c_adap_init函数,sunxi_i2c_adap_init函数做的事情很简单,就是一个简单的平台驱动注册。
       我们向Linux内核注册平台驱动之后,每次Linux内核启动起来,就会通过sunxi_i2c_match这个表里的compatible去设备树里寻找,找到一个就会调用一次probe函数。
       我们这里的compatible为“allwinner,sun8i-twi”,所以会在设备树里找compatible="allwinner,sun8i-twi"的设备节点。

twi@0x05002000 {
             #address-cells = <0x1>;
             #size-cells = <0x0>;
             compatible = "allwinner,sun8i-twi";
             device_type = "twi0";
             reg = <0x0 0x5002000 0x0 0x400>;
             interrupts = <0x0 0x2e 0x4>;
             clocks = <0x48>;
             clock-frequency = <0x61a80>;
             pinctrl-names = "default", "sleep";
             status = "okay";
             pinctrl-0 = <0x120>;
             pinctrl-1 = <0x121>;
};

       现在已经从设备树里找到了对应的节点,也就是说有对应的设备,所以进入probe函数。

static int sunxi_i2c_probe(struct platform_device *pdev)
{
    /* 获取设备节点 */
	struct device_node *np = pdev->dev.of_node;
	if (np == NULL) {
		I2C_ERR("I2C failed to get of node\n");
		return -ENODEV;
	}

    /* 获取twi总线id,也就是第几条总线 */
    /* 这里说一下twi总线,twi总线是对i2c总线的继承和发展,可以简单理解成i2c总线 */
	pdev->id = of_alias_get_id(np, "twi");
	if (pdev->id < 0) {
		I2C_ERR("I2C failed to get alias id\n");
		ret = -EINVAL;
		goto emem;
	}

    /* 内存映射 */
	i2c->base_addr = ioremap(mem_res->start, resource_size(mem_res));
	if (!i2c->base_addr) {
		ret = -EIO;
		goto eiomap;
	}

	/* 获取中断号 */
	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
		I2C_ERR("[i2c%d] failed to get irq\n", pdev->id);
		ret = -EINVAL;
		goto eiomap;
	}

    /* 获取时钟频率 */
	ret = of_property_read_u32(np, "clock-frequency", &pdata->frequency);
	if (ret) {
		I2C_ERR("[i2c%d] failed to get clock frequency\n", pdev->id);
		ret = -EINVAL;
		goto eiomap;
	}

	/* 初始化i2c适配器 */
	i2c->adap.owner   = THIS_MODULE;
	i2c->adap.nr      = pdata->bus_num;
	i2c->adap.retries = 3;
	i2c->adap.timeout = 5*HZ;
	i2c->adap.class   = I2C_CLASS_HWMON | I2C_CLASS_SPD;
	snprintf(i2c->adap.name, sizeof(i2c->adap.name), SUNXI_TWI_DEV_NAME"%u", i2c->adap.nr);

    /* 获取时钟 */
	i2c->mclk = of_clk_get(np, 0);
	if (IS_ERR_OR_NULL(i2c->mclk)) {
		I2C_ERR("[i2c%d] request TWI clock failed\n", i2c->bus_num);
		ret = -EIO;
		goto eremap;
	}

	/* 设置 i2c_algorithm */
	i2c->adap.algo = &sunxi_i2c_algorithm;

	/* 注册中断 */
	ret = request_irq(irq, sunxi_i2c_handler, int_flag, i2c->adap.name, i2c);
	if (ret) {
		I2C_ERR("[i2c%d] requeset irq failed!\n", i2c->bus_num);
		goto ereqirq;
	}

    /* 硬件初始化:初始化时钟、申请gpio等 */
    if (sunxi_i2c_hw_init(i2c, pdata)) {
		ret = -EIO;
		goto ehwinit;
	}

    return 0;
}

       可以看到,代码中有设置i2c->adap.algo = &sunxi_i2c_algorithm。

static const struct i2c_algorithm sunxi_i2c_algorithm = {
	.master_xfer	  = sunxi_i2c_xfer,
	.functionality	  = sunxi_i2c_functionality,
};

       i2c_algorithm方法就是i2c总线驱动为i2c设备驱动提供的通信方法,也就是通讯接口。利用这个通讯接口,i2c设备驱动可以访问这个i2c_adapter上的所有设备。其中sunxi_i2c_xfer接口用于收发消息,sunxi_i2c_functionality接口用于查询适配器支持什么类型。

3、I2C设备驱动

       还是老规矩,看驱动代码,首先从模块入口看起。

static const struct i2c_device_id sensor_id[] = {
	{"mipi_max9288", 0},
	{}
};

static struct i2c_driver sensor_driver = {
	.driver = {
		   .owner = THIS_MODULE,
		   .name = SENSOR_NAME,
		   },
	.probe = sensor_probe,
	.remove = sensor_remove,
	.id_table = sensor_id,
};

static __init int init_sensor(void)
{
	return i2c_add_driver(sensor_driver);
}

static __exit void exit_sensor(void)
{
	i2c_del_driver(sensor_driver);
}

module_init(init_sensor);
module_exit(exit_sensor);

       看到上面的代码,首先,在module_init函数中,向内核注册了一个i2c驱动,注册的i2c_driver结构体为sensor_driver。
       我们看到sensor_driver。
       一般字符设备都是通过设备树来进行匹配,所以在.driver里面会有一个.of_match_table,而这里明显没有,不过细心的我们应该发现,在下面还有一个.id_table,看到这儿,大家应该就已经明白,没错,这就是i2c驱动match的一种方式。

static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
	struct i2c_client	*client = i2c_verify_client(dev);
	struct i2c_driver	*driver;

	if (!client)
		return 0;

	/* Attempt an OF style match */
	if (of_driver_match_device(dev, drv))
		return 1;

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

	driver = to_i2c_driver(drv);
	/* match on an id table if there is one */
	if (driver->id_table)
		return i2c_match_id(driver->id_table, client) != NULL;

	return 0;
}

       上面就是I2C驱动的三种匹配方式,我们这里用的是第三种,i2c_match_id。

static const struct i2c_device_id *i2c_match_id(const struct i2c_device_id *id, const struct i2c_client *client)
{
	while (id->name[0]) {
		if (strcmp(client->name, id->name) == 0)
			return id;
		id++;
	}
	return NULL;
}

       i2c_match_id的实现其实非常简单,就是单纯地比较i2c_device_id中的name和i2c_client中的name是否相同,相同则代表匹配成功。
       那么i2c_device_id已经实现,i2c_client又在哪里创建的呢?继续看代码。

if (type == VIN_MODULE_TYPE_I2C) {
    struct v4l2_subdev *sd = NULL;
    /* 为bussel(twi6)获取适配器 */
    struct i2c_adapter *adapter = i2c_get_adapter(bus_sel);
    if (adapter == NULL) {
        vin_err("%s request i2c%d adapter failed!\n", name, bus_sel);
        return NULL;
    }

    /* 创建i2c_client并初始化name */
    sd = v4l2_i2c_new_subdev(v4l2_dev, adapter, name, addr, NULL);
}

       因为我们的I2C设备驱动是V4L2设备的一个子设备,所以我们的id_table在V4L2父设备中被创建和初始化。
       首先,我们看到i2c_get_adapter,这个函数通过id获取i2c_adapter,而在总线驱动中,大概浏览过代码的朋友就应该有印象,在那里是获取过twi的id的,没有印象的可以回去看一下。
       接下来,我们来简单看一下v4l2_i2c_new_subdev这个函数中大概做了些什么。

/* 使用设备类型和设备地址设置i2c板信息 */
struct v4l2_subdev *v4l2_i2c_new_subdev(struct v4l2_device *v4l2_dev, struct i2c_adapter *adapter, const char *client_type, u8 addr, const unsigned short *probe_addrs)
{
	struct i2c_board_info info;

	memset(&info, 0, sizeof(info));

	/* 设置type类型为client_type,在这里,传过来的client_type为mipi_max9288 */
	strlcpy(info.type, client_type, sizeof(info.type));
	info.addr = addr;

	return v4l2_i2c_new_subdev_board(v4l2_dev, adapter, &info, probe_addrs);
}
struct v4l2_subdev *v4l2_i2c_new_subdev_board(struct v4l2_device *v4l2_dev, struct i2c_adapter *adapter, struct i2c_board_info *info, const unsigned short *probe_addrs)
{
	struct v4l2_subdev *sd = NULL;
	struct i2c_client *client;

	/* 创建一个i2c_client,提示:这里传进来的probe_addrs为null */
	if (info->addr == 0 && probe_addrs)
		client = i2c_new_probed_device(adapter, info, probe_addrs, NULL);
	else
        /* 创建i2c_client */
		client = i2c_new_device(adapter, info);

    sd = i2c_get_clientdata(client);

	if (v4l2_device_register_subdev(v4l2_dev, sd))
		sd = NULL;

    return sd;
}

       经过几次函数调用,i2c_client创建成功,并且它的名字也被赋值为了“mipi_max9288”,和i2c_device_id匹配起来了。
       在这里,I2C设备驱动的probe函数我们就不分析了,无非就是V4L2子设备的一些初始化,将i2c_client设置为V4L2子设备的私有数据和一些字符设备节点的创建等。
       我们来看看数据的读写具体是怎么进行的。

int read(struct v4l2_subdev *sd, unsigned char addr, unsigned char *value)
{
	unsigned char data[2];
	struct i2c_msg msg[2];
	int ret;
	struct i2c_client *client = v4l2_get_subdevdata(sd);

	data[0] = addr;
	data[1] = 0xee;
	
    /* Send out the register address */
	msg[0].addr = client->addr;
	msg[0].flags = 0;
	msg[0].len = 1;
	msg[0].buf = &data[0];
	
    /* then read back the result */
	msg[1].addr = client->addr;
	msg[1].flags = I2C_M_RD;
	msg[1].len = 1;
	msg[1].buf = &data[1];

	ret = i2c_transfer(client->adapter, msg, 2);
	if (ret >= 0) {
		*value = data[1];
		ret = 0;
	}
	return ret;
}

       可以看到,读函数大概分为三步,第一步:设置你要读取的设备地址,第二步:设置flags为读,且设置你的读buffer,第三步,调用i2c_transfer。这里,在i2c_transfer函数中会去调用adapter的master_xfer方法。
       接下来,我们再来看一下写函数。

int write(struct v4l2_subdev *sd, unsigned char addr, unsigned char value)
{
	struct i2c_msg msg;
	unsigned char data[2];
	int ret;
	struct i2c_client *client = v4l2_get_subdevdata(sd);

	data[0] = addr;
	data[1] = value;

	msg.addr = client->addr;
	msg.flags = 0;
	msg.len = 2;
	msg.buf = data;

	ret = i2c_transfer(client->adapter, &msg, 1);
	if (ret >= 0) {
		ret = 0;
	} else {
		cci_err("%s error! slave = 0x%x, addr = 0x%2x, value = 0x%2x\n ",
					__func__, client->addr, addr, value);
	}
	return ret;
}

       读函数相比于写函数来说,步骤要少一些,在这里,只需要设置msg的地址、flag等,然后再调用i2c_transfer就可以了。

六、总结

       看完上面的内容,想来大家对Linux下的I2C驱动框架也有一定的了解了。
       Linux下的I2C驱动框架,总共分为三层:I2C核心、I2C总线驱动、I2C设备驱动。I2C核心是负责联络I2C总线驱动和I2C设备驱动的媒婆,I2C总线驱动和I2C设备驱动则是一对相爱相杀的恋人。I2C总线驱动注册好特定总线的i2c_adapter,并设置好其中的i2c_algorithm通信方法。而I2C设备驱动则由注册在i2c_adapter上的i2c_client,通过i2c_algorithm与这条i2c总线上的各个设备完成感情的联络。

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值