Linux I2C总线、驱动、设备知识点笔记

##I2C
I2C驱动三部分:I2C核心、I2C总线驱动、I2C设备驱动

##I2C设备结构体 i2c-client初始化、加载流程
由于每个挂载在i2c总线上的设备均会有一个i2c-client结构体,但是一直没有搞清楚,这个设备对应的结构体是怎么来的?下载这篇文章给了几种i2c-client的初始化流程。可以洗洗琢磨下。

https://blog.csdn.net/lugandong/article/details/48092397

另外,内核的文档Documentation\i2c\instantiating-devices   也介绍了实例化i2c设备的几种方法。

从内核代码看,i2c设备是如何注册的?
在板级目录中,比如kernel/linux/arch/*目录中,grep -rn “i2c_register_board_info” ./ 搜索发现许多的地方会调用i2c_register_board_info注册设备。而调用这个函数的方式一般是一些初始化的函数,而这些初始化的函数多是通过宏定义来声明,方式如下:

以mini2240为例:/mach-s3c24xx/mach-mini2440.c

static void __init mini2440_init(void)
{
	……
	s3c_i2c0_set_platdata(NULL);

	i2c_register_board_info(0, mini2440_i2c_devs,
				ARRAY_SIZE(mini2440_i2c_devs));   //注册多个i2c设备。
    ……
	if (features.count)	/* the optional features */
		platform_add_devices(features.optional, features.count);

}


/*注意这个宏定义,每个开发板都有一个这样定义的静态结构体,用于系统体系架构相关部分的初始化*/
MACHINE_START(MINI2440, "MINI2440")
	/* Maintainer: Michel Pollet <buserror@gmail.com> */
	.atag_offset	= 0x100,
	.map_io		= mini2440_map_io,
	.init_machine	= mini2440_init,
	.init_irq	= s3c2440_init_irq,
	.init_time	= mini2440_init_time,
MACHINE_END

其实看到这里很迷茫,因为依据我们的理解总要有个地方来从上到下逐步调用这个mini2440_init初始化函数啊,但是在代码里面搜索,找不到调用mini2440_init的地方,其实这就是linux内核代码的高明之处,通过一系列的宏定义,来实现将特定功能的代码在程序链接时放在特定的段(通过*lds文件指定)中,从而实现了分散式的注册。
而对于该例子,特定的宏定义就是 MACHINE_START 和 MACHINE_END。

#define MACHINE_START(_type,_name)			\
static const struct machine_desc __mach_desc_##_type	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_##_type,		\
	.name		= _name,
 
#define MACHINE_END				\
};

对于该例子,上述宏定义展开后,其实就是定义了一个machine_desc类型的结构体__mach_desc__MINI2440,。
__attribute__((__section__(".arch.info.init"))) 是告诉编译器,链接时将这个结构体放在段 .arch.info.init中。
static const struct machine_desc __mach_desc__MINI2440	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE__MINI2440,		\
	.name		= "MINI2440",
 	.atag_offset	= 0x100,
	.map_io		= mini2440_map_io,
	.init_machine	= mini2440_init,
	.init_irq	= s3c2440_init_irq,
	.init_time	= mini2440_init_time,
#define MACHINE_END				\
};

__used  :告诉编译器,这个函数或者变量是有用的。即便是没有地方引用,那么也不能优化掉。
__unused :告诉编译器,这个函数或者变量可能没有用到,这样编译器就不会产生告警了。

链接脚本:
由__section__(".arch.info.init")) 可知static const struct machine_desc __mach_desc__MINI2440 变量
放在arch/arm/kernel/vmlinux.lds链接脚本的.arch.info.init段里的。
vmlinux.lds文本如下:
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;

至于这个变量的__mach_desc__MINI2440 何时会用到呢?

start_kernel
	setup_arch(&command_line);
		setup_machine(machine_arch_type)
			list = lookup_machine_type(nr); ---该函数会根据入参 nr 找到本开发板所对应的 __mach_desc__MINI2440结构体。一般nr是uboot传过来的参数(采用ATAGS方式)。如果采用设备树的方式,和这个不一样的。具体可以看下韦东山博客:
(http://wiki.100ask.org/%E7%AC%AC%E4%B8%89%E8%AF%BE:%E5%86%85%E6%A0%B8%E5%AF%B9%E8%AE%BE%E5%A4%87%E6%A0%91%E7%9A%84%E5%A4%84%E7%90%86)

	init_machine = mdesc->init_machine;  //将初始化的函数指针赋值给全局变量init_machine

然后:

static int __init customize_machine(void)
{
	/* customizes platform devices, or adds new ones */
	if (init_machine)
		init_machine();   //执行函数mini2440_init,添加设备以及相关的初始化。
	return 0;
}
arch_initcall(customize_machine);

到了此处,发现又是一个宏arch_initcall,这种方式在内核中多次使用。那么arch_initcall

类似的宏定义位于linux/include/linux/init.h中。具体展开后,依然是声明了特定的函数并且放在特定的代码段中。

#define pure_initcall(fn)		__define_initcall(fn, 0)

#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)		__define_initcall(fn, 1s)
#define postcore_initcall(fn)		__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define fs_initcall_sync(fn)		__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)     //通过module_init注册的设备或者驱动在这儿的
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)		__define_initcall(fn, 7s)

上面这个特定代码段的函数是如何被调用的呢?
start_kernel
rest_init() --该函数是start_kernel函数的最后一行,比较靠后。
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);创建了一个init线程。

线程init:

init
	do_basic_setup
		do_initcalls();--该函数会调用到 customize_machine 。


static void __init do_initcalls(void)
{
initcall_t *call;
int count = preempt_count();

for (call = __initcall_start; call < __initcall_end; call++) {
	char *msg = NULL;
	char msgbuf[40];
	int result;

	if (initcall_debug) {
		printk("Calling initcall 0x%p", *call);
		print_fn_descriptor_symbol(": %s()",
				(unsigned long) *call);
		printk("\n");
	}

	result = (*call)();

	if (result && result != -ENODEV && initcall_debug) {
		sprintf(msgbuf, "error code %d", result);
		msg = msgbuf;
	}
	if (preempt_count() != count) {
		msg = "preemption imbalance";
		preempt_count() = count;
	}
	if (irqs_disabled()) {
		msg = "disabled interrupts";
		local_irq_enable();
	}
	if (msg) {
		printk(KERN_WARNING "initcall at 0x%p", *call);
		print_fn_descriptor_symbol(": %s()",
				(unsigned long) *call);
		printk(": returned with %s\n", msg);
	}
}

/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_work();
}

如上述代码,该函数会取出__initcall_start到__initcall_end之间所有注册的函数指针(其实就是那十来个宏定义所放进去的东西)。

写到这里,总算是把i2c_register_board_info函数如何被调用的搞清楚了,扯的有点远了,但是对内核的启动和架构增加了一点点的认识。下面回到正题上来。

至此,	i2c_register_board_info 函数将i2c设备加入到链表 __i2c_board_list 中。那么何时会用到这个链表。往下看。

###内核如何创建i2c-client
####方法1
i2c_add_adapter --该函数会被适配器驱动的probe函数调用,向内核注册这个适配器的驱动。具体可见适配器驱动目录:linux/drivers/i2c/busses,该目录下每个.c文件是适配器的驱动,对应一个或者多个适配器硬件,然后每个.c的probe函数会获取适配器的硬件信息,来实例化一个适配器,并向内核注册。
i2c_register_adapter :注册适配器
i2c_scan_static_board_info:扫描链表__i2c_board_list,遍历每一个设备
i2c_new_device:调用这个函数申请i2c-client结构体,并对这个结构体的关键成员进行赋值,比如该设备所依赖的适配器adapter。

####方法2:

上面分析的基本上都是老的设备信息初始化的方式,内核引入了设备树的概念,对于使用设备树的方式,i2c-clinet的创建方式如下:

i2c_add_numbered_adapter  
   __i2c_add_numbered_adapter
       i2c_register_adapter 
           of_i2c_register_devices(adap);   ---drivers/i2c/i2c-core.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-core.c这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口。

i2c-dev.c实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备。通过适配器访设备时的主设备号都为89,次设备号为0-255。I2c-dev.c并没有针对特定的设备而设计,只是提供了通用的read(),write(),和ioctl()等接口,应用层可以借用这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。

busses文件夹这个文件中包含了一些I2C总线的驱动(其实就是适配器的驱动,该目录下是各个公司的适配器所对应的驱动),如针对S3C2410,S3C2440,S3C6410等处理器的I2C控制器驱动为i2c-s3c2410.c.

algos文件夹实现了一些I2C总线适配器的algorithm.

##设备和驱动对应的代码

不管是对于哪种类型的驱动以及驱动对应的平台,
驱动的目录位于:linux/drivers 设备对应的驱动位于这个目录中。
设备的目录位于:kernel/linux/arch,一般来讲,设备的信息都定义在这个目录下的。当然由于这部分代码会非常庞大,不易于一直并且冗余等原因,所以引出了设备树,所以设备的信息在新的内核中会采用设备树的语法格式给出。具体在:linux/arch/[CPU类型]/boot/dts。

一般而言,setup_arch新增驱动的话,就在这两个目录下,仿照其他类似的芯片驱动、设备代码书写格式仿照就可以了。“

##设备和驱动的匹配代码流程

当我们向总线注册驱动或者设备时,均会触发匹配过程,匹配的思想是将当前的设备或者驱动与总线上已经注册的驱动或者设备逐个进行比较。当成功时,驱动的probe函数会被调用,进行一些初始化的操作。现在根据代码阅读以下这个流程,以i2c为例。

i2c_register_driver
	driver_register
		bus_add_driver  
			klist_add_tail --添加驱动到bus的驱动链表
			driver_attach
				bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);


bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);  --遍历每一个设备,并调用__driver_attach函数处理。
	__driver_attach
		driver_match_device	--该函数将会调用总线的match函数,即i2c_device_match,来判断当前驱动和设备是否匹配,如果不匹配,返回。
		driver_probe_device(drv, dev);
			really_probe(dev, drv)
				dev->driver = drv;  将设备指向驱动,其他重要的额功能如下:


static int really_probe(struct device *dev, struct device_driver *drv)
{
	
	……
	if (dev->bus->probe) {        /*如果总线的probe函数存在,则调用总线的probe*/
		ret = dev->bus->probe(dev);
		if (ret)
			goto probe_failed;
	} else if (drv->probe) {     /*总线probe不存在,调用drv的prob*/
		ret = drv->probe(dev);
		if (ret)
			goto probe_failed;
	}
	……
}

搞不明白的是,当总线的probe函数存在时,驱动的probe函数不会被调用了,那么对于总线而言,probe函数都是存在的啊,这样驱动的probe函数就不会被调用了。
这样的疑惑其实是错误的。那么既然调用了总线的probe函数,那我们就看下总线的probe函数干了些什么吧。以i2c的为例。如下:

static int i2c_device_probe(struct device *dev)
{
	struct i2c_client	*client = i2c_verify_client(dev);
	struct i2c_driver	*driver;
	int status;

	if (!client)
		return 0;

	if (!client->irq) {
		int irq = -ENOENT;

		if (dev->of_node) {
			irq = of_irq_get_byname(dev->of_node, "irq");
			if (irq == -EINVAL || irq == -ENODATA)
				irq = of_irq_get(dev->of_node, 0);
		} else if (ACPI_COMPANION(dev)) {
			irq = acpi_dev_gpio_irq_get(ACPI_COMPANION(dev), 0);
		}
		if (irq == -EPROBE_DEFER)
			return irq;
		if (irq < 0)
			irq = 0;

		client->irq = irq;
	}

	driver = to_i2c_driver(dev->driver);
	if (!driver->probe || !driver->id_table)
		return -ENODEV;

	if (client->flags & I2C_CLIENT_WAKE) {
		int wakeirq = -ENOENT;

		if (dev->of_node) {
			wakeirq = of_irq_get_byname(dev->of_node, "wakeup");
			if (wakeirq == -EPROBE_DEFER)
				return wakeirq;
		}

		device_init_wakeup(&client->dev, true);

		if (wakeirq > 0 && wakeirq != client->irq)
			status = dev_pm_set_dedicated_wake_irq(dev, wakeirq);
		else if (client->irq > 0)
			status = dev_pm_set_wake_irq(dev, client->irq);
		else
			status = 0;

		if (status)
			dev_warn(&client->dev, "failed to set up wakeup irq\n");
	}

	dev_dbg(dev, "probe\n");

	status = of_clk_set_defaults(dev->of_node, false);
	if (status < 0)
		goto err_clear_wakeup_irq;

	status = dev_pm_domain_attach(&client->dev, true);
	if (status == -EPROBE_DEFER)
		goto err_clear_wakeup_irq;

	status = driver->probe(client, i2c_match_id(driver->id_table, client));  ---注意这一行,这一行不就是调用了驱动的probe函数嘛。
	if (status)
		goto err_detach_pm_domain;

	return 0;

err_detach_pm_domain:
	dev_pm_domain_detach(&client->dev, true);
err_clear_wakeup_irq:
	dev_pm_clear_wake_irq(&client->dev);
	device_init_wakeup(&client->dev, false);
	return status;
}

请注意上面函数的一行代码:

status = driver->probe(client, i2c_match_id(driver->id_table, client));

这一行代码的作用就是调用具体设备的驱动。至此这个问题就搞明白了。
上面的流程是当注册驱动,匹配设备的过程。当注册设备,匹配驱动的时候,流程大体上和上面一致,这里先不研究了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值