2014-04-03 11:28:12 wujiangguizhen 阅读数 3645
  • Linux内核分析

    1.Bootloader概述 2.Linux内核分析初步 3.Linux内核配置 4.Linux文件系统

    1952课时 0分钟 294人学习 沈寒
    免费试看

linux嵌入式系统上不认u盘的解决方法


在 embedded linux 下插上一个 U 盘,在 /dev/scsi/ 目录下,出现了 4 个 part 。把该 U 盘插在 pc 机,在 windows 下正常。在 Redhat linux 系统下 mount 的时候出现错误。在使用 fdisk –l 命令。显示问题如下。

Disk /dev/sdb: 1023 MB, 1023409152 bytes
32 heads, 61 sectors/track, 1023 cylinders
Units = cylinders of 1952 * 512 = 999424 bytes
This doesn't look like a partition table
Probably you selected the wrong device.

Device Boot    Start       End    Blocks Id   System
/dev/sdb1 ?    398636    983425 570754815+   72   Unknown
Partition 1 has different physical/logical beginnings (non-Linux?):
    phys=(357, 116, 40) logical=(398635, 6, 23)
Partition 1 has different physical/logical endings:
    phys=(357, 32, 45) logical=(983424, 30, 61)
Partition 1 does not end on cylinder boundary.
/dev/sdb2 ?    86419     1078237 968014120 65   Novell Netware 386
Partition 2 has different physical/logical beginnings (non-Linux?):
    phys=(288, 115, 43) logical=(86418, 26, 1)
Partition 2 has different physical/logical endings:
    phys=(367, 114, 50) logical=(1078236, 17, 53)
Partition 2 does not end on cylinder boundary.
/dev/sdb3 ?    957932     1949749 968014096 79   Unknown
Partition 3 has different physical/logical beginnings (non-Linux?):
    phys=(366, 32, 33) logical=(957931, 2, 32)
Partition 3 has different physical/logical endings:
    phys=(357, 32, 43) logical=(1949748, 25, 36)
Partition 3 does not end on cylinder boundary.
/dev/sdb4 ?     1478321     1478349    27749+ d   Unknown
Partition 4 has different physical/logical beginnings (non-Linux?):
    phys=(372, 97, 50) logical=(1478320, 8, 25)
Partition 4 has different physical/logical endings:
    phys=(0, 10, 0) logical=(1478348, 22, 13)
Partition 4 does not end on cylinder boundary.


Partition table entries are not in disk order

经过网上查找和自己动手,找到解决方法。

fdisk /dev/sdb1 ( 此处改为 u 盘的 Device Boot )

The number of cylinders for this disk is set to 9729.
There is nothing wrong with that, but this is larger than 1024,
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
(e.g., DOS FDISK, OS/2 FDISK)

Command (m for help): p

显示 目前 U 盘状态。

Command (m for help): d
Partition number (1-4): 4

Command (m for help): d
Partition number (1-4): 3

Command (m for help): d
Partition number (1-4): 2

Command (m for help): d

Command (m for help): p
Disk /dev/sdb: 1023 MB, 1023409152 bytes
255 heads, 63 sectors/track, 9729 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Device Boot    Start       End    Blocks Id   System

Command (m for help): n
Command action
l logical (5 or over)
p primary partition (1-4)

P

Partition number (1-4): 1

Command (m for help): p

显示目前的 U 盘状态。

Command (m for help): x
Expert command (m for help): m
Command action
b move beginning of data in a partition
c change number of cylinders
d print the raw data in the partition table
e list extended partitions
f fix partition order
g create an IRIX (SGI) partition table
h change number of heads
m print this menu
p print the partition table
q quit without saving changes
r return to main menu
s change number of sectors/track
v verify the partition table
w write table to disk and exit

Expert command (m for help): f
Done.

Expert command (m for help): r

Command (m for help): p

显示 U 盘当前状态。

Command (m for help): w // 保存你所做的工作并且退出。

以上就是对 U 盘进行重新分区。把 4 part 合为一个。

这时的 U 盘还没有格式化。你需要对 U 盘进行格式化。

先卸载 u 盘
#umount /dev/sdb1
# 注意 /dev/ 后面的设备要根据你的实际情况而定
格式化并建立 VFAT 文件系统
#mkfs.vfat /dev/sdb1
最后再 mount 上就成了 , 或者把 U 盘拨了再插上 , 系统可能会自动 mount 上 , 就可以用 U 盘了

这样在 pc linux 系统和 embedded linux 系统下就可以一个 U 盘对应一个设备名了。并且可以正常使用。

2018-09-24 09:32:25 caihaitao2000 阅读数 422
  • Linux内核分析

    1.Bootloader概述 2.Linux内核分析初步 3.Linux内核配置 4.Linux文件系统

    1952课时 0分钟 294人学习 沈寒
    免费试看

1.ARM设备树起源

在过去的ARM Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,很多代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。读者若有兴趣,可以统计一下常见的s3c2410、s3c6410等板级目录,代码量在数万行。

设备树是一种描述硬件的数据结构,它起源于OpenFirmware(OF)。在Linux2.6中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx中,采用设备树后,许多硬件的细节可以直接通过它传递给Linux,而不再需要在内核中进行大量的冗余编码。

设备树由一系列被命名的节点(Node)和属性(Property)组成,而节点本身可以包含子节点。所谓属性,其实就是成对出现的名称和值。在设备树中,可描述的信息包括(原先这些信息大多被硬编码在内核中):

  • CPU的数量和类别。
  • 内存基地址和大小。
  • 总线和桥。
  • 外设连接。
  • 中断控制器和中断使用情况。
  • GPIO控制器和GPIO使用情况。
  • 时钟控制器和时钟使用情况。

它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。

1.2.设备树的组成和结构

整个设备树牵涉面比较广,即增加了新的用于描述设备硬件信息的文本格式,又增加了编译这个文本的工具,同时Bootloader也需要支持将编译后的设备树传递给Linux内核。

1.2.1.DTS、DTC和DTB等

DTS:文件.dts是一种ASCII文本格式的设备树描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在ARM Linux中,一个.dts文件对应一个ARM的设备,一般放置在内核的arch/arm/boot/dts/目录中。值得注意的是,在arch/powerpc/boot/dts、arch/powerpc/boot/dts、arch/c6x/boot/dts、arch/openrisc/boot/dts等目录中,也存在大量的.dts文件,这证明DTS绝对不是ARM的专利。

由于一个SoC可能对应多个设备(一个SoC可以对应多个产品和电路板),这些.dts文件势必须包含许多共同的部分,Linux内核为了简化,把SoC公用的部分或者多个设备共同的部分一般提炼为.dtsi,类似于C语言的头文件。其他的设备对应的.dts就包括这个.dtsi。譬如,对于VEXPRESS而言,vexpress-v2m.dtsi就被vexpress-v2p-ca9.dts所引用,vexpress-v2p-ca9.dts有如下一行代码:

/include/ "vexpress-v2m.dtsi"

当然,和C语言的头文件类似,.dtsi也可以包括其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi。

文件.dts(或者其包括的.dtsi)的基本元素即为前文所述的节点和属性,代码清单1给出了一个设备树结构的模版。

/* 代码清单1设备树结构模版 */
/{
	node1{
		a-string-property = "A string";
		a-string-list-property = "first string", "second string";
		a-byte-data-property = [0x01 0x23 0x34 0x56];
		child-node1 {
			first-child-property;
			second-child-property = <1>;
			a-string-property = "Hello, world";
		};
		child-node2 {
		
		};
	};
	node2{
		an-empty-property;
		a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
		child-node1 {
		
		};
	};
};

上述.dts文件并没有什么真实的用途,但它基本表征了一个设备树源文件的结构:

1个root节点"/";root节点下面含一系列子节点,本例中为node1和node2;节点node1下又含有一系列子节点,本例中为child-node1和child-node2;各节点都有一系列属性。这些属性可能为空,如an-empty-property;可能为字符串,如a-string-property;可能为字符串数组,如a-string-list-property;可能为Cells(由u32整数组成),如second-child-property;可能为二进制数,如a-byte-data-property。

下面以一个最简单的设备为例来看如何写一个.dts文件。如图1所示,假设此设备的配置如下:

                                                 图1设备树参考硬件结构图

1个双核ARM Cortex-A932位处理器;ARM本地总线上的内存映射区域分布有两个串口(分别位于0x101F1000和0x101F2000)、GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10170000)、中断控制器(位于0x10140000)和一个外部总线桥;外部总线桥上又连接了SMC SMC91111以太网(位于0x10100000)、I2C控制器(位于0x10160000)、64MB NOR Flash(位于0x30000000);外部总线桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58)。

对于图1所示硬件结构图,如果用".dts"描述,则其对应的".dts"文件如代码清单2所示。

/* 代码清单2参考硬件的设备树文件 */
/{
	compatible = "acme,coyotes-revenge";
	#address-cells = <1>;
	#size-cells = <1>;
	interrupt-parent = <&intc>;

	cpus {
		#address-cells = <1>;
		#size-cells = <0>;
		cpu@0 {
			compatible = "arm,cortex-a9";
			reg = <0>;
		};
		cpu@1 {
			compatible = "arm,cortex-a9";
			reg = <1>;
		};
	};

	serial@101f0000 {
		compatible = "arm,pl011";
		reg = <0x101f0000 0x1000>;
		interrupts = <1 0>;
	};

	serial@101f2000 {
		compatible = "arm,pl011";
		reg = <0x101f2000 0x1000>;
		interrupts = <2 0>;
	};

	gpio@101f3000 {
		compatible = "arm,pl061";
		reg = <0x101f3000 0x1000
			   0x101f4000 0x0010>;
		interrupts = <3 0>;
	};

	intc: interrupt-controller@10140000 {
		compatible = "arm,pl190";
		reg = <0x10140000 0x1000>;
		interrupt-controller;
		#interrupt-cells = <2>;
	};

	spi@10115000 {
		compatible = "arm,pl022";
		reg = <0x10115000 0x1000>;
		interrupts = <4 0>;
	};

	external-bus {
		#address-cells = <2>
		#size-cells = <1>;
		ranges = <0 0 0x10100000 0x10000 	 //Chipselect 1, Ethernet
				  1 0 0x10160000 0x10000 	 //Chipselect 2, i2c controller
				  2 0 0x30000000 0x1000000>; //Chipselect 3, NOR Flash

		ethernet@0,0 {
			compatible = "smc,smc91c111";
			reg = <0 0 0x1000>;
			interrupts = <5 2>;
		};

		i2c@1,0 {
			compatible = "acme,a1234-i2c-bus";
			#address-cells = <1>;
			#size-cells = <0>;
			reg = <1 0 0x1000>;
			interrupts = <6 2>;
			rtc@58 {
				compatible = "maxim,ds1338";
				reg = <58>;
				interrupts = <7 3>;
			};
		};

		flash@2,0 {
			compatible = "samsung,k8f1315ebm", "cfi-flash";
			reg = <2 0 0x4000000>;
		};
	};
};

在上述.dts文件中,可以看出external-bus是根节点的子节点,而I2C又是external-bus的子节点,RTC又进一步是I2C的子节点。每一级节点都有一些属性信息,本章后续部分会进行详细解释。

DTC:DTC是将.dts编译为.dtb的工具。DTC的源代码位于内核的scripts/dtc目录中,在Linux内核使能了设备树的情况下,编译内核的时候主机工具DTC会被编译出来,对应于scripts/dtc/Makefile中"hostprogs-y:=dtc"这一hostprogs的编译目标。

当然,DTC也可以在Ubuntu中单独安装,命令如下:

sudo apt-get install device-tree-compiler

在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来,如与VEXPRESS对应的.dtb包括:

dtb-$(CONfiG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb \
	vexpress-v2p-ca9.dtb \
	vexpress-v2p-ca15-tc1.dtb \
	vexpress-v2p-ca15_a7.dtb \
	xenvm-4.2.dtb

在Linux下,我们可以单独编译设备树文件。当我们在Linux内核下运行make dtbs时,若我们之前选择了ARCH_VEXPRESS,上述.dtb都会由对应的.dts编译出来,因为arch/arm/Makefile中含有一个.dtbs编译目标项目。

DTC除了可以编译.dts文件以外,其实也可以"反汇编".dtb文件为.dts文件,其指令格式为:

./scripts/dtc/dtc -I dtb -O dts -o xxx.dts arch/arm/boot/dts/xxx.dtb

DTB(Device Tree Blob):文件.dtb是.dts被DTC编译后的二进制格式的设备树描述,可由Linux内核解析,当然U-Boot这样的bootloader也是可以识别.dtb的。

通常在我们为电路板制作NAND、SD启动映像时,会为.dtb文件单独留下一个很小的区域以存放之,之后bootloader在引导内核的过程中,会先读取该.dtb到内存。

Linux内核也支持一种变通的模式,可以不把.dtb文件单独存放,而是直接和zImage绑定在一起做成一个映像文件,类似cat zImage xxx.dtb>zImage_with_dtb的效果。当然内核编译时候要使能CONFIG_ARM_APPENDED_DTB这个选项,以支持"Use appended device tree blob to zImage"(见Linux内核中的菜单)。

绑定(Binding):对于设备树中的节点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。在这个.txt文件中,需要描述对应节点的兼容性、必需的属性和可选的属性。

这些文档位于内核的Documentation/devicetree/bindings目录下,其下又分为很多子目录。譬如,Documentation/devicetree/bindings/i2c/i2c-xiic.txt描述了Xilinx的I2C控制器,其内容如下:

Xilinx IIC controller:
Required properties:
	- compatible : Must be "xlnx,xps-iic-2.00.a"
	- reg : IIC register location and length
	- interrupts : IIC controller unterrupt
	- #address-cells = <1>
	- #size-cells = <0>
Optional properties:
	- Child nodes conforming to i2c bus binding
Example:
	axi_iic_0: i2c@40800000 {
		#address-cells = <1>;
		#size-cells = <0>;
		reg = <0x40800000 0x10000>;
		compatible = "xlnx,xps-iic-2.00.a";
		interrupts = <1 2>;
	};

基本可以看出,设备树绑定文档的主要内容包括:

  • 关于该模块最基本的描述。
  • 必需属性(Required Properties)的描述。
  • 可选属性(Optional Properties)的描述。
  • 一个实例。

Linux内核下的scripts/checkpatch.pl会运行一个检查,如果有人在设备树中新添加了compatible字符串,而没有添加相应的文档进行解释,checkpatch程序会报出警告: UNDOCUMENTED_DT_STRINGDT compatible string xxx appears un-documented,因此程序员要养成及时写DT Binding文档的习惯。

Bootloader:Uboot设备从v1.1.3开始支持设备树,其对ARM的支持则是和ARM内核支持设备树同期完成。为了使能设备树,需要在编译Uboot的时候在config文件中加入:

#define CONfiG_OF_LIBFDT

在Uboot中,可以从NAND、SD或者TFTP等任意介质中将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在Uboot中运行fdt addr命令设置.dtb的地址,如:

UBoot> fdt addr 0x71000000

fdt的其他命令就变得可以使用,如fdt resize、fdt print等。

对于ARM来讲,可以通过bootz kernel_addr initrd_address dtb_address的命令来启动内核,即dtb_address作为bootz或者bootm的最后一次参数,第一个参数为内核映像的地址,第二个参数为initrd的地址,若不存在initrd,可以用"-"符号代替。

1.2.2.根节点兼容性

上述.dts文件中,第2行根节点"/"的兼容属性compatible="acme, coyotes-revenge";定义了整个系统(设备级别)的名称,它的组织形式为:<manufacturer>, <model>。

Linux内核通过根节点"/"的兼容属性即可判断它启动的是什么设备。在真实项目中,这个顶层设备的兼容属性一般包括两个或者两个以上的兼容性字符串,首个兼容性字符串是板子级别的名字,后面一个兼容性是芯片级别(或者芯片系列级别)的名字。

譬如板子arch/arm/boot/dts/vexpress-v2p-ca9.dts兼容于arm,vexpress,v2p-ca9和"arm,vexpress":

compatible = "arm,vexpress,v2p-ca9", "arm,vexpress";

板子arch/arm/boot/dts/vexpress-v2p-ca5s.dts的兼容性则为:

compatible = "arm,vexpress,v2p-ca5s", "arm,vexpress";

板子arch/arm/boot/dts/vexpress-v2p-ca15_a7.dts的兼容性为:

compatible = "arm,vexpress,v2p-ca15_a7", "arm,vexpress";

可以看出,上述各个电路板的共性是兼容于arm,vexpress,而特性是分别兼容于arm,vexpress,v2pca9、arm,vexpress,v2p-ca5s和arm,vexpress,v2p-ca15_a7。

进一步地看,arch/arm/boot/dts/exynos4210-origen.dts的兼容性字段如下:

compatible = "insignal,origen", "samsung,exynos4210", "samsung,exynos4";

第一个字符串是板子名字(很特定),第2个字符串是芯片名字(比较特定),第3个字段是芯片系列的名字(比较通用)。

作为类比,arch/arm/boot/dts/exynos4210-universal_c210.dts的兼容性字段则如下:

compatible = "samsung,universal_c210", "samsung,exynos4210", "samsung,exynos4";

由此可见,它与exynos4210-origen.dts的区别只在于第1个字符串(特定的板子名字)不一样,后面芯片名和芯片系列的名字都一样。

在Linux 2.6内核中,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个设备的一系列回调函数,如代码清单3所示。

代码清单3 ARM Linux 2.6时代的设备

MACHINE_START(VEXPRESS, "ARM-Versatile Express")
	.atag_offset = 0x100,
	.smp = smp_ops(vexpress_smp_ops),
	.map_io = v2m_map_io,
	.init_early = v2m_init_early,
	.init_irq = v2m_init_irq,
	.timer = &v2m_timer,
	.handle_irq = gic_handle_irq,
	.init_machine = v2m_init,
	.restart = vexpress_restart,
MACHINE_END

这些不同的设备会有不同的MACHINE ID,Uboot在启动Linux内核时会将MACHINE ID存放在R1寄存器,Linux启动时会匹配Bootloader传递的MACHINE ID和MACHINE_START声明的MACHINE ID,然后执行相应设备的一系列初始化函数。

ARM Linux 3.x在引入设备树之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的设备与.dts中根节点的兼容属性兼容关系。如果Bootloader传递给内核的设备树中根节点的兼容属性出现在某设备的.dt_compat表中,相关的设备就与对应的兼容匹配,从而引发这一设备的一系列初始化函数被执行。一个典型的DT_MACHINE如代码清单4所示。

代码清单4 ARM Linux 3.x时代的设备

static const char * const v2m_dt_match[] __initconst = {
	"arm,vexpress",
	"xen,xenvm",
	NULL,
};

DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")
	.dt_compat = v2m_dt_match,
	.smp = smp_ops(vexpress_smp_ops),
	.map_io = v2m_dt_map_io,
	.init_early = v2m_dt_init_early,
	.init_irq = v2m_dt_init_irq,
	.timer = &v2m_dt_timer,
	.init_machine = v2m_dt_init,
	.handle_irq = gic_handle_irq,
	.restart = vexpress_restart,
MACHINE_END

Linux倡导针对多个SoC、多个电路板的通用DT设备,即一个DT设备的.dt_compat包含多个电路板.dts文件的根节点兼容属性字符串。之后,如果这多个电路板的初始化序列不一样,可以通过int of_machine_is_compatible(const char*compat)API判断具体的电路板是什么。在Linux内核中,常常使用如下API来判断根节点的兼容性:

int of_machine_is_compatible(const char *compat);

此API判断目前运行的板子或者SoC的兼容性,它匹配的是设备树根节点下的兼容属性。例如drivers/cpufreq/exynos-cpufreq.c中就有判断运行的CPU类型是exynos4210、exynos4212、exynos4412还是exynos5250的代码,进而分别处理,如代码清单5所示。

代码清单5 of_machine_is_compatible()的案例

static int exynos_cpufreq_probe(struct platform_device *pdev)
{
	int ret = -EINVAL;
	exynos_info = kzalloc(sizeof(*exynos_info), GFP_KERNEL);
	if (!exynos_info)
		return -ENOMEM;

	exynos_info->dev = &pdev->dev;

	if (of_machine_is_compatible("samsung,exynos4210")) {
		exynos_info->type = EXYNOS_SOC_4210;
		ret = exynos4210_cpufreq_init(exynos_info);
	} else if (of_machine_is_compatible("samsung,exynos4212")) {
		exynos_info->type = EXYNOS_SOC_4212;
		ret = exynos4x12_cpufreq_init(exynos_info);
	} else if (of_machine_is_compatible("samsung,exynos4412")) {
		exynos_info->type = EXYNOS_SOC_4412;
		ret = exynos4x12_cpufreq_init(exynos_info);
	} else if (of_machine_is_compatible("samsung,exynos5250")) {
		exynos_info->type = EXYNOS_SOC_5250;
		ret = exynos5250_cpufreq_init(exynos_info);
	} else {
		pr_err("%s: Unknown SoC type\n", __func__);
		return -ENODEV;
	}
	...
}

如果一个兼容包含多个字符串,譬如对于前面介绍的根节点兼容compatible="samsung,universal_c210","samsung,exynos4210","samsung,exynos4"的情况,如下3个表达式都是成立的。

of_machine_is_compatible("samsung,universal_c210")
of_machine_is_compatible("samsung,exynos4210")
of_machine_is_compatible("samsung,exynos4")

1.2.3.设备节点兼容性

在.dts文件的每个设备节点中,都有一个兼容属性,兼容属性用于驱动和设备的绑定。兼容属性是一个字符串的列表,列表中的第一个字符串表征了节点代表的确切设备,形式为"<manufacturer>,<model>",其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。如在vexpress-v2m.dtsi中的Flash节点如下:

flash@0,00000000 {
	compatible = "arm,vexpress-flash", "cfi-flash";
	reg = <0 0x00000000 0x04000000>,
	<1 0x00000000 0x04000000>;
	bank-width = <4>;
};

兼容属性的第2个字符串"cfi-flash"明显比第1个字符串"arm,vexpress-flash"涵盖的范围更广。

再如,Freescale MPC8349SoC含一个串口设备,它实现了国家半导体(National Sem-iconductor)的NS16550寄存器接口。则MPC8349串口设备的兼容属性为compatible="fsl,mpc8349-uart","ns16550"。其中,fsl,mpc8349-uart指代了确切的设备,ns16550代表该设备与NS16550UART保持了寄存器兼容。因此,设备节点的兼容性和根节点的兼容性是类似的,都是"从具体到抽象"。

使用设备树后,驱动需要与.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的"acme,a1234-i2c-bus"兼容I2C控制器节点的OF匹配表,具体代码清单6所示。

代码清单6 platform设备驱动中的of_match_table

static const struct of_device_id a1234_i2c_of_match[] = {
	{.compatible = "acme,a1234-i2c-bus",},
	{},
};
MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);

static struct platform_driver i2c_a1234_driver = {
	.driver = {
		.name = "a1234-i2c-bus",
		.owner = THIS_MODULE,
		.of_match_table = a1234_i2c_of_match,
	},
	.probe = i2c_a1234_probe,
	.remove = i2c_a1234_remove,
};
module_platform_driver(i2c_a1234_driver);

对于I2C和SPI从设备而言,同样也可以通过of_match_table添加匹配的.dts中的相关节点的兼容属性,如sound/soc/codecs/wm8753.c中的针对WolfsonWM8753的of_match_table,具体如代码清单7所示。

代码清单7 I2C、SPI设备驱动中的of_match_table

static const struct of_device_id wm8753_of_match[] = {
	{.compatible = "wlf,wm8753", },
	{ }
};
MODULE_DEVICE_TABLE(of, wm8753_of_match);

static struct spi_driver wm8753_spi_driver = {
	.driver = {
		.name = "wm8753",
		.owner = THIS_MODULE,
		.of_match_table = wm8753_of_match,
	},
	.probe = wm8753_spi_probe,
	.remove = wm8753_spi_remove,
};

static struct i2c_driver wm8753_i2c_driver = {
	.driver = {
		.name = "wm8753",
		.owner = THIS_MODULE,
		.of_match_table = wm8753_of_match,
	},
	.probe = wm8753_i2c_probe,
	.remove = wm8753_i2c_remove,
	.id_table = wm8753_i2c_id,
};

上述代码中的第2行显示WM8753的供应商是"wlf",它其实是对应于Wolfson Microe-lectronics的前缀。详细的前缀可见于内核文档:Documentation/devicetree/bindings/vendor-prefixes.txt。

对于I2C、SPI还有一点需要提醒的是,I2C和SPI外设驱动和设备树中设备节点的兼容属性还有一种弱式匹配方法,就是"别名"匹配。兼容属性的组织形式为<manufacturer>,<model>,别名其实就是去掉兼容属性中manufacturer前缀后的<model>部分。关于这一点,可查看drivers/spi/spi.c的源代码,函数spi_match_device()暴露了更多的细节,如果别名出现在设备spi_driver的id_table里面,或者别名与spi_driver的name字段相同,SPI设备和驱动都可以匹配上,代码清单8显示了SPI的别名匹配。

代码清单8 SPI的别名匹配

static int spi_match_device(struct device *dev, struct device_driver *drv)
{
	const struct spi_device *spi = to_spi_device(dev);
	const struct spi_driver *sdrv = to_spi_driver(drv);

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

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

	if (sdrv->id_table)
		return !!spi_match_id(sdrv->id_table, spi);
	return strcmp(spi->modalias, drv->name) == 0;
}

static const struct spi_device_id *spi_match_id(const struct spi_device_id *id,const struct spi_device *sdev)
{
	while (id->name[0]) {
		if (!strcmp(sdev->modalias, id->name))
			return id;
		id++;
	}
	return NULL;
}

通过这个别名匹配,实际上,SPI和I2C的外设驱动即使没有of_match_table,还是可以和设备树中的节点匹配上的。

一个驱动可以在of_match_table中兼容多个设备,在Linux内核中常常使用如下API来判断具体的设备是什么:

int of_device_is_compatible(const struct device_node *device,const char *compat);

此函数用于判断设备节点的兼容属性是否包含compat指定的字符串。这个API多用于一个驱动支持两个以上设备的时候。

当一个驱动支持两个或多个设备的时候,这些不同.dts文件中设备的兼容属性都会写入驱动OF匹配表。因此驱动可以通过Bootloader传递给内核设备树中的真正节点的兼容属性以确定究竟是哪一种设备,从而根据不同的设备类型进行不同的处理。如arch/powerpc/platforms/83xx/usb.c中的mpc831x_usb_cfg()就进行了类似处理:

if (immr_node && (of_device_is_compatible(immr_node, "fsl,mpc8315-immr") ||
				  of_device_is_compatible(immr_node, "fsl,mpc8308-immr")))
	clrsetbits_be32(immap + MPC83XX_SCCR_OFFS,MPC8315_SCCR_USB_MASK,MPC8315_SCCR_USB_DRCM_01);
else
	clrsetbits_be32(immap + MPC83XX_SCCR_OFFS,MPC83XX_SCCR_USB_MASK,MPC83XX_SCCR_USB_DRCM_11);

/* Configure pin mux for ULPI. There is no pin mux for UTMI */
if (prop && !strcmp(prop, "ulpi")) {
	if (of_device_is_compatible(immr_node, "fsl,mpc8308-immr")) {
		clrsetbits_be32(immap + MPC83XX_SICRH_OFFS,MPC8308_SICRH_USB_MASK,MPC8308_SICRH_USB_ULPI);
	} else if (of_device_is_compatible(immr_node, "fsl,mpc8315-immr")) {
		clrsetbits_be32(immap + MPC83XX_SICRL_OFFS,MPC8315_SICRL_USB_MASK,MPC8315_SICRL_USB_ULPI);
		clrsetbits_be32(immap + MPC83XX_SICRH_OFFS,MPC8315_SICRH_USB_MASK,MPC8315_SICRH_USB_ULPI);
	} else {
		clrsetbits_be32(immap + MPC83XX_SICRL_OFFS,MPC831X_SICRL_USB_MASK,MPC831X_SICRL_USB_ULPI);
		clrsetbits_be32(immap + MPC83XX_SICRH_OFFS,MPC831X_SICRH_USB_MASK,MPC831X_SICRH_USB_ULPI);
	}
}

它根据具体的设备是fsl,mpc8315-immr和fsl,mpc8308-immr中的哪一种来进行不同的处理。

当一个驱动可以兼容多种设备的时候,除了of_device_is_compatible()这种判断方法以外,还可以采用在驱动的of_device_id表中填充.data成员的形式。譬如,arch/arm/mm/cache-l2x0.c支持"arm,l210-cache" "arm,pl310-cache" "arm,l220-cache"等多种设备,其of_device_id表如代码清单9所示。

代码清单9 支持多个兼容性以及.data成员的of_device_id表

#define L2C_ID(name, fns) {.compatible = name, .data = (void *)&fns}

static const struct of_device_id l2x0_ids[] __initconst = {
	L2C_ID("arm,l210-cache", of_l2c210_data),
	L2C_ID("arm,l220-cache", of_l2c220_data),
	L2C_ID("arm,pl310-cache", of_l2c310_data),
	L2C_ID("brcm,bcm11351-a2-pl310-cache", of_bcm_l2x0_data),
	L2C_ID("marvell,aurora-outer-cache", of_aurora_with_outer_data),8 L2C_ID("marvell,aurora-system-cache", of_aurora_no_outer_data),
	L2C_ID("marvell,tauros3-cache", of_tauros3_data),
	/* Deprecated IDs */
	L2C_ID("bcm,bcm11351-a2-pl310-cache", of_bcm_l2x0_data),
	{}
};

在驱动中,通过如代码清单10的方法拿到了对应于L2缓存类型的.data成员,其中主要用到了of_match_node()这个API。

代码清单10 通过of_match_node()找到.data

int __init l2x0_of_init(u32 aux_val, u32 aux_mask)
{
	const struct l2c_init_data *data;
	struct device_node *np;

	np = of_find_matching_node(NULL, l2x0_ids);
	if (!np)
		return -ENODEV;
	…
	
	data = of_match_node(l2x0_ids, np)->data;
}

如果电路板的.dts文件中L2缓存是arm,pl310-cache,那么上述代码第10行找到的data就是of_l2c310_data,它是l2c_init_data结构体的一个实例。l2c_init_data是一个由L2缓存驱动自定义的数据结构,在其定义中既可以保护数据成员,又可以包含函数指针,如代码清单11所示。

代码清单11 与兼容对应的特定data实例

struct l2c_init_data {
	const char *type;
	unsigned way_size_0;
	unsigned num_lock;
	void (*of_parse)(const struct device_node *, u32 *, u32 *);
	void (*enable)(void __iomem *, u32, unsigned);
	void (*fixup)(void __iomem *, u32, struct outer_cache_fns *);
	void (*save)(void __iomem *);
	struct outer_cache_fns outer_cache;
};

通过这种方法,驱动可以把与某个设备兼容的私有数据寻找出来,如此体现了一种面向对象的设计思想,避免了大量的if,else或者switch,case语句。

1.2.4.设备节点及label的命名

代码清单2的.dts文件中,根节点"/"的cpus子节点下面又包含两个cpu子节点,描述了此设备上的两个CPU,并且两者的兼容属性为:"arm,cortex-a9"。

注意cpus和cpus的两个cpu子节点的命名,它们遵循的组织形式为<name>[@<unit-address>],<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述节点对应的设备类型,如3comEthernet适配器对应的节点name宜为ethernet,而不是3com509。如果一个节点描述的设备有地址,则应该给出@unit-address。多个相同类型设备节点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名节点。设备的unit-address地址也经常在其对应节点的reg属性中给出。

对于挂在内存空间的设备而言,@字符后跟的一般就是该设备在内存空间的基地址,譬如arch/arm/boot/dts/exynos4210.dtsi中存在的:

sysram@02020000 {
	compatible = "mmio-sram";
	reg = <0x02020000 0x20000>;
	…
}

上述节点的reg属性的开始位置与@后面的地址一样。

对于挂在I2C总线上的外设而言,@后面一般跟的是从设备的I2C地址,譬如arch/arm/boot/dts/exynos4210-trats.dts中的mms114-touchscreen:

i2c@13890000 {
	… 
	mms114-touchscreen@48 {
		compatible = "melfas,mms114";
		reg = <0x48>;
		…
	};
};

上述节点的reg属性标示的I2C从地址与@后面的地址一样。

具体的节点命名规范可见ePAPR(embedded Power Architecture Platform Reference)标准,在https://www.power.org中可下载该标准。

我们还可以给一个设备节点添加label,之后可以通过&label的形式访问这个label,这种引用是通过phandle(pointer handle)进行的。

例如,在arch/arm/boot/dts/omap5.dtsi中,第3组GPIO有gpio3这个label,如代码清单12所示。

代码清单12 在设备树中定义label

gpio3: gpio@48057000 {
	compatible = "ti,omap4-gpio";
	reg = <0x48057000 0x200>;
	interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
	ti,hwmods = "gpio3";
	gpio-controller;
	#gpio-cells = <2>;
	interrupt-controller;
	#interrupt-cells = <2>;
};

而hsusb2_phy这个USB的PHY复位GPIO用的是这组GPIO中的一个,所以它通过phandle引用了"gpio3",如代码清单13所示。

代码清单13 通过phandle引用其他节点

/* HS USB Host PHY on PORT 2 */
hsusb2_phy: hsusb2_phy {
	compatible = "usb-nop-xceiv";
	reset-gpios = <&gpio3 12 GPIO_ACTIVE_LOW>; /* gpio3_76 HUB_RESET */
};

代码清单12第1行的gpio3是gpio@48057000节点的label,而代码清单13的hsusb2_phy则通过&gpio3引用了这个节点,表明自己要使用这一组GPIO中的第12个GPIO。很显然,这种phandle引用其实表明硬件之间的一种关联性。

再举一例,在arch/arm/boot/dts/omap5.dtsi中,我们可以看到类似如下的label,从这些实例可以看出,label习惯以<设备类型><index>进行命名:

i2c1: i2c@48070000 {

}
i2c2: i2c@48072000 {

}
i2c3: i2c@48060000 {

}

读者也许发现了一个奇怪的现象,就是代码清单13中居然引用了GPIO_ACTIVE_LOW这个类似C语言的宏。文件.dts的编译过程确实支持C的预处理,相应的.dts文件也包括了包含GPIO_ACTIVE_LOW这个宏定义的头文件:

#include <dt-bindings/gpio/gpio.h>

对于ARM而言,dt-bindings头文件位于内核的arch/arm/boot/dts/include/dt-bindings目录中。观察该目录的属性,它实际上是一个符号链接:

baohua@baohua-VirtualBox:~/develop/linux/arch/arm/boot/dts/include$ ls -l dtbindings
lrwxrwxrwx 1 baohua baohua 34 11月 28 00:16 dt-bindings -> ../../../../../
include/dt-bindings

从内核的scripts/Makefile.lib这个文件可以看出,文件.dts的编译过程确实是支持C预处理的。

cmd_dtc = $(CPP) $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $< ; \
	$(objtree)/scripts/dtc/dtc -O dtb -o $@ -b 0 \
	-i $(dir $<) $(DTC_FLAGS) \
	-d $(depfile).dtc.tmp $(dtc-tmp) ; \
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)

它是先做了$(CPP) $(dtc_cpp_flags) -x assembler-with-cpp-o$(dtc-tmp) $<,再做的.dtc编译。

1.2.5.地址编码

可寻址的设备使用如下信息在设备树中编码地址信息:

reg
#address-cells
#size-cells

其中,reg的组织形式为reg=<address1length1[address2length2][address3length3]...>,其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length的意义则意味着从address到address+length–1的地址范围都属于该节点。若#size-cells=0,则length字段为空。

address和length字段是可变长的,父节点的#address-cells和#size-cells分别决定了子节点reg属性的address和length字段的长度。

在代码清单2中,根节点的#address-cells=<1>;和#size-cells=<1>;决定了serial、gpio、spi等节点的address和length字段的长度分别为1。

cpus节点的#address-cells=<1>;和#size-cells=<0>;决定了两个cpu子节点的address为1,而length为空,于是形成了两个cpu的reg=<0>;和reg=<1>;。

external-bus节点的#address-cells=<2>和#size-cells=<1>;决定了其下的ethernet、i2c、flash的reg字段形如reg=<0 0 0x1000>;、reg=<1 0 0x1000>;和reg=<2 0 0x4000000>;。其中,address字段长度为2,开始的第一个cell(即"<"后的0、 1、 2)是对应的片选,第2个cell(即<0 0 0x1000>、<1 0 0x1000>和<2 0 0x1000000>中间的0,0,0)是相对该片选的基地址,第3个cell(即">"前的0x1000、0x1000、0x1000000)为length。

特别要留意的是i2c节点中定义的#address-cells=<1>;和#size-cells=<0>;,其作用到了I2C总线上连接的RTC,它的address字段为0x58,是RTC设备的I2C地址。

根节点的直接子节点描述的是CPU的视图,因此根子节点的address区域就直接位于CPU的内存区域。但是,经过总线桥后的address往往需要经过转换才能对应CPU的内存映射。external-bus的ranges属性定义了经过external-bus桥后的地址范围如何映射到CPU的内存区域。

ranges = <0 0 0x10100000 0x10000 		//Chipselect 1, Ethernet
		 1 0 0x10160000 0x10000 		//Chipselect 2, i2c controller
		 2 0 0x30000000 0x1000000>; 	//Chipselect 3, NOR Flash

ranges是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。映射表中的子地址、父地址分别采用子地址空间的#address-cells和父地址空间的#address-cells大小。对于本例而言,子地址空间的#address-cells为2,父地址空间的#address-cells值为1,因此0 0 0x101000000 x10000的前2个cell为external-bus桥后external-bus上片选0偏移0,第3个cell表示external-bus上片选0偏移0的地址空间被映射到CPU的本地总线的0x10100000位置,第4个cell表示映射的大小为0x10000。ranges后面两个项目的含义可以类推。

备注:#address-cells=<2>和#size-cells=<1>的情况还不是太明白。还需要查找资料学习。

1.2.6.中断连接

设备树中还可以包含中断连接信息,对于中断控制器而言,它提供如下属性:

interrupt-controller–这个属性为空,中断控制器应该加上此属性表明自己的身份;

#interrupt-cells-与#address-cells和#size-cells相似,它表明连接此中断控制器的设备的中断属性的cell大小。

在整个设备树中,与中断相关的属性还包括:

interrupt-parent:设备节点通过它来指定它所依附的中断控制器的phandle,当节点没有指定interruptparent时,则从父级节点继承。对于本例(代码清单2)而言,根节点指定了interrupt-parent= <&intc>;,其对应于intc:interrupt-controller@10140000,而根节点的子节点并未指定interrupt-parent,因此它们都继承了intc,即位于0x10140000的中断控制器中。

interrupts–用到了中断的设备节点,通过它指定中断号、触发方法等,这个属性具体含有多少个cell,由它依附的中断控制器节点的#interrupt-cells属性决定。而每个cell具体又是什么含义,一般由驱动的实现决定,而且也会在设备树的绑定文档中说明。譬如,对于ARM GIC中断控制器而言,#interrupt-cells为3,3个cell的具体含义在Documentation/devicetree/bindings/arm/gic.txt中就有如下文字说明:

The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI interrupts.
The 2nd cell contains the interrupt number for the interrupt type.
	SPI interrupts are in the range [0-987]. PPI interrupts are in the range [0-15].
The 3rd cell is the flags, encoded as follows:
bits[3:0] trigger type and level flags.
	1 = low-to-high edge triggered
	2 = high-to-low edge triggered
	4 = active high level-sensitive
	8 = active low level-sensitive
bits[15:8] PPI interrupt cpu mask. Each bit corresponds to each of
the 8 possible cpus attached to the GIC. A bit set to '1' indicated
the interrupt is wired to that CPU. Only valid for PPI interrupts.

另外,值得注意的是,一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168号、169号两个中断,而且都是高电平触发,则该设备节点的中断属性可定义为interrupts=<0 168 4>,<0 169 4>;。

对于平台设备而言,简单的通过如下API就可以指定想取哪一个中断,其中的参数num就是中断的index。

int platform_get_irq(struct platform_device *dev, unsigned int num);

当然在.dts文件中可以对中断进行命名,而后在驱动中通过platform_get_irq_byname()来获取对应的中断号。譬如代码清单14演示了在drivers/dma/fsl-edma.c中通过platform_get_irq_byname()获取IRQ,以及arch/arm/boot/dts/vf610.dtsi与fsl-edma驱动对应节点的中断描述。

代码清单14 设备树中的中断名称以及驱动获取中断

static int fsl_edma_irq_init(struct platform_device *pdev,struct fsl_edma_engine *fsl_edma)
{
	fsl_edma->txirq = platform_get_irq_byname(pdev, "edma-tx");
	fsl_edma->errirq = platform_get_irq_byname(pdev, "edma-err");
}

edma0: dma-controller@40018000 {
	#dma-cells = <2>;
	compatible = "fsl,vf610-edma";
	reg = <0x40018000 0x2000>,
		  <0x40024000 0x1000>,
		  <0x40025000 0x1000>;
	interrupts = <0 8 IRQ_TYPE_LEVEL_HIGH>,<0 9 IRQ_TYPE_LEVEL_HIGH>;
	interrupt-names = "edma-tx", "edma-err";
	dma-channels = <32>;
	clock-names = "dmamux0", "dmamux1";
	clocks = <&clks VF610_CLK_DMAMUX0>,<&clks VF610_CLK_DMAMUX1>;
};

第4行、第5行的platform_get_irq_byname()的第2个参数与.dts中的interrupt-names是一致的。

1.2.7.GPIO、时钟、pinmux连接

除了中断以外,在ARM Linux中时钟、GPIO、pinmux都可以通过.dts中的节点和属性进行描述。

GPIO:

譬如,对于GPIO控制器而言,其对应的设备节点需声明gpio-controller属性,并设置#gpio-cells的大小。譬如,对于兼容性为fsl,imx28-pinctrl的pinctrl驱动而言,其GPIO控制器的设备节点类似于:

pinctrl@80018000 {
	compatible = "fsl,imx28-pinctrl", "simple-bus";
	reg = <0x80018000 2000>;
	gpio0: gpio@0 {
		compatible = "fsl,imx28-gpio";
		interrupts = <127>;
		gpio-controller;
		#gpio-cells = <2>;
		interrupt-controller;
		#interrupt-cells = <2>;
	};
	gpio1: gpio@1 {
		compatible = "fsl,imx28-gpio";
		interrupts = <126>;
		gpio-controller;
		#gpio-cells = <2>;
		interrupt-controller;
		#interrupt-cells = <2>;
	};
	...
};

其中,#gpio-cells为2,第1个cell为GPIO号,第2个为GPIO的极性。为0的时候是高电平有效,为1的时候则是低电平有效。

使用GPIO的设备则通过定义命名xxx-gpios属性来引用GPIO控制器的设备节点,如:

sdhci@c8000400 {
	status = "okay";
	cd-gpios = <&gpio01 0>;
	wp-gpios = <&gpio02 0>;
	power-gpios = <&gpio03 0>;
	bus-width = <4>;
};

而具体的设备驱动则通过类似如下的方法来获取GPIO:

cd_gpio = of_get_named_gpio(np, "cd-gpios", 0);
wp_gpio = of_get_named_gpio(np, "wp-gpios", 0);
power_gpio = of_get_named_gpio(np, "power-gpios", 0);

of_get_named_gpio()这个API的原型如下:

static inline int of_get_named_gpio(struct device_node *np,const char *propname, int index);

在.dts和设备驱动不关心GPIO名字的情况下,也可以直接通过of_get_gpio()获取GPIO,此函数原型为:

static inline int of_get_gpio(struct device_node *np, int index);

如对于compatible="gpio-control-nand"的基于GPIO的NAND控制器而言,在.dts中会定义多个gpio属性:

gpio-nand@1,0 {
	compatible = "gpio-control-nand";
	reg = <1 0x0000 0x2>;
	#address-cells = <1>;
	#size-cells = <1>;
	gpios = <&banka 1 0 /* rdy */
			 &banka 2 0 /* nce */
			 &banka 3 0 /* ale */
			 &banka 4 0 /* cle */
			 0 			/* nwp */>;
	partition@0 {
		...
	};
};

在相应的驱动代码drivers/mtd/nand/gpio.c中是这样获取这些GPIO的:

plat->gpio_rdy = of_get_gpio(dev->of_node, 0);
plat->gpio_nce = of_get_gpio(dev->of_node, 1);
plat->gpio_ale = of_get_gpio(dev->of_node, 2);
plat->gpio_cle = of_get_gpio(dev->of_node, 3);
plat->gpio_nwp = of_get_gpio(dev->of_node, 4);

时钟:

时钟和GPIO也是类似的,时钟控制器的节点被使用时钟的模块引用:

clocks = <&clks 138>, <&clks 140>, <&clks 141>;
clock-names = "uart", "general", "noc";

而驱动中则使用上述的clock-names属性作为clk_get()或devm_clk_get()的第二个参数来申请时钟,譬如获取第2个时钟:

devm_clk_get(&pdev->dev, "general");

<&clks 138>里的138这个index是与相应时钟驱动中clk的表的顺序对应的,很多开发者也认为这种数字出现在设备树中不太好,因此他们把clk的index作为宏定义到了arch/arm/boot/dts/include/dt-bindings/clock中。譬如include/dt-bindings/clock/imx6qdl-clock.h中存在这样的宏:

#define IMX6QDL_CLK_STEP 	16
#define IMX6QDL_CLK_PLL1_SW 17
#define IMX6QDL_CLK_ARM 	104

而arch/arm/boot/dts/imx6q.dtsi则是这样引用它们的:

clocks = <&clks IMX6QDL_CLK_ARM>,
		 <&clks IMX6QDL_CLK_PLL2_PFD2_396M>,
		 <&clks IMX6QDL_CLK_STEP>,
		 <&clks IMX6QDL_CLK_PLL1_SW>,
		 <&clks IMX6QDL_CLK_PLL1_SYS>;

pinmux:

在设备树中,某个设备节点使用的pinmux的引脚群是通过phandle来指定的。譬如在arch/arm/boot/dts/atlas6.dtsi的pinctrl节点中包含所有引脚群的描述,如代码清单15所示。

代码清单15 设备树中pinctrl控制器的引脚群

gpio: pinctrl@b0120000 {
	#gpio-cells = <2>;
	#interrupt-cells = <2>;
	compatible = "sirf,atlas6-pinctrl";
	…
	lcd_16pins_a: lcd0@0 {
		lcd {
			sirf,pins = "lcd_16bitsgrp";
			sirf,function = "lcd_16bits";
		};
	};
	…
	spi0_pins_a: spi0@0 {
		spi {
			sirf,pins = "spi0grp";
			sirf,function = "spi0";
		};
	};
	spi1_pins_a: spi1@0 {
		spi {
			sirf,pins = "spi1grp";
			sirf,function = "spi1";
		};
	};
	…
};

而SPI0这个硬件实际上需要用到spi0_pins_a对应的spi0grp这一组引脚,因此在atlas6-evb.dts中通过pinctrl-0引用了它,如代码清单16所示。

代码清单16 给设备节点指定引脚群

spi@b00d0000 {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&spi0_pins_a>;
	…
};

到目前为止,我们可以勾勒出一个设备树的全局视图,图2显示了设备树中的节点、属性、label以及phandle等信息。

图2. 设备树的全景视图

1.3.由设备树引发的BSP和驱动变更

有了设备树后,不再需要大量的板级信息,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx中实施如下事情。

1.注册platform_device,绑定resource,即内存、IRQ等板级信息

通过设备树后,形如:

static struct resource xxx_resources[] = {
	[0] = {
		.start = …,
		.end = …,
		.flags = IORESOURCE_MEM,
	},
	[1] = {
		.start = …,
		.end = …,
		.flags = IORESOURCE_IRQ,
	},
};
static struct platform_device xxx_device = {
	.name = "xxx",
	.id = -1,
	.dev = {
		.platform_data = &xxx_data,
	},
	.resource = xxx_resources,
	.num_resources = ARRAY_SIZE(xxx_resources),
};

之类的platform_device代码都不再需要,其中platform_device会由内核自动展开。而这些resource实际来源于.dts中设备节点的reg、interrupts属性。

典型的,大多数总线都与"simple_bus"兼容,而在与SoC对应的设备的.init_machine成员函数中,调用of_platform_bus_probe(NULL,xxx_of_bus_ids, NULL);即可自动展开所有的platform_device。

2.注册i2c_board_info,指定IRQ等板级信息

形如:

static struct i2c_board_info __initdata afeb9260_i2c_devices[] = {
	{
		I2C_BOARD_INFO("tlv320aic23", 0x1a),
	}, 
	{
		I2C_BOARD_INFO("fm3130", 0x68),
	}, 
	{
		I2C_BOARD_INFO("24c64", 0x50),
	},
};

之类的i2c_board_info代码目前不再需要出现,现在只需要把tlv320aic23、fm3130、24c64这些设备节点填充作为相应的I2C控制器节点的子节点即可,类似于前面的代码:

i2c@1,0 {
	compatible = "acme,a1234-i2c-bus";
	rtc@58 {
		compatible = "maxim,ds1338";
		reg = <58>;
		interrupts = <7 3>;
	};
};

设备树中的I2C客户端会通过在I2C host驱动的probe()函数中调用的of_i2c_register_devices(&i2c_dev->adapter)被自动展开。

在过去,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围的设备,引入设备树之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的设备与.dts中根节点的兼容属性的兼容关系。

这样可以显著改善代码的结构并减少冗余的代码, 在不支持设备树的情况下,光是一个S3C24xx就存在多个板文件,譬如mach-amlm5900.c、mach-gta02.c、mach-smdk2410.c、mach-qt2410.c、mach-rx3715.c等,其累计的代码量是相当大的,板级信息都用C语言来实现。而采用设备树后,我们可以对多个SoC和板子使用同一个DT_MACHINE和板文件,板子和板子之间的差异更多只是通过不同的.dts文件来体现。

使用设备树后,驱动需要与在.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。新的驱动、设备的匹配变成了设备树节点的兼容属性和设备驱动中的OF匹配表的匹配。

常用的OF API:

除了前文介绍的of_machine_is_compatible()、of_device_is_compatible()等常用函数以外,在Linux的BSP和驱动代码中,经常会使用到一些Linux中其他设备树的API,这些API通常被冠以of_前缀,它们的实现代码位于内核的drivers/of目录下。

这些常用的API包括下面内容。

1.寻找节点

struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible);

根据兼容属性,获得设备节点。遍历设备树中的设备节点,看看哪个节点的类型、兼容属性与本函数的输入参数匹配,在大多数情况下,from、type为NULL,则表示遍历了所有节点。

2.读取属性

int of_property_read_u8_array(const struct device_node *np,const char *propname, u8 *out_values, size_t sz);
int of_property_read_u16_array(const struct device_node *np,const char *propname, u16 *out_values, size_t sz);
int of_property_read_u32_array(const struct device_node *np,const char *propname, u32 *out_values, size_t sz);
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value); 

读取设备节点np的属性名,为propname,属性类型为8、16、32、64位整型数组。对于32位处理器来讲,最常用的是of_property_read_u32_array()。

如在arch/arm/mm/cache-l2x0.c中,通过如下语句可读取L2cache的"arm,data-latency"属性:

of_property_read_u32_array(np, "arm,data-latency",data, ARRAY_SIZE(data));

在arch/arm/boot/dts/vexpress-v2p-ca9.dts中,对应的含有"arm,data-latency"属性的L2cache节点如下:

L2: cache-controller@1e00a000 {
	compatible = "arm,pl310-cache";
	reg = <0x1e00a000 0x1000>;
	interrupts = <0 43 4>;
	cache-level = <2>;
	arm,data-latency = <1 1 1>;
	arm,tag-latency = <1 1 1>;
}

在有些情况下,整型属性的长度可能为1,于是内核为了方便调用者,又在上述API的基础上封装出了更加简单的读单一整形属性的API,它们为int of_property_read_u8()、of_property_read_u16()等,实现于include/linux/of.h中,如代码清单19所示。

代码清单19 设备树中整型属性的读取API

static inline int of_property_read_u8(const struct device_node *np,const char *propname,u8 *out_value)
{
	return of_property_read_u8_array(np, propname, out_value, 1);
}

static inline int of_property_read_u16(const struct device_node *np,const char *propname,u16 *out_value)
{
	return of_property_read_u16_array(np, propname, out_value, 1);
}

static inline int of_property_read_u32(const struct device_node *np,const char *propname,u32 *out_value)
{
	return of_property_read_u32_array(np, propname, out_value, 1);
}

除了整型属性外,字符串属性也比较常用,其对应的API包括:

int of_property_read_string(struct device_node *np, const char *propname,const char **out_string);
int of_property_read_string_index(struct device_node *np, const char *propname,int index, const char **output);

前者读取字符串属性,后者读取字符串数组属性中的第index个字符串。如drivers/clk/clk.c中的of_clk_get_parent_name()函数就通过of_property_read_string_index()遍历clkspec节点的所有"clockoutput-names"字符串数组属性。

内存映射:

void __iomem *of_iomap(struct device_node *node, int index);

上述API可以直接通过设备节点进行设备内存区间的ioremap(),index是内存段的索引。若设备节点的reg属性有多段,可通过index标示要ioremap()的是哪一段,在只有1段的情况,index为0。采用设备树后,一些设备驱动通过of_iomap()而不再通过传统的ioremap()进行映射,当然,传统的ioremap()的用户也不少。

int of_address_to_resource(struct device_node *dev, int index,struct resource *r);

上述API通过设备节点获取与它对应的内存资源的resource结构体。其本质是分析reg属性以获取内存基地址、大小等信息并填充到struct resource*r参数指向的结构体中。

解析中断:

unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

通过设备树获得设备的中断号,实际上是从.dts中的interrupts属性里解析出中断号。若设备使用了多个中断,index指定中断的索引号。

获取与节点对应的platform_device:

struct platform_device *of_find_device_by_node(struct device_node *np);

在可以拿到device_node的情况下,如果想反向获取对应的platform_device,可使用上述API。
当然,在已知platform_device的情况下,想获取device_node则易如反掌,例如:

static int sirfsoc_dma_probe(struct platform_device *op)
{
	struct device_node *dn = op->dev.of_node;
	…
}

总结:

充斥着ARM社区的大量垃圾代码导致Linus盛怒,因此该社区在2011~2012年进行了大量的修整工作。ARM Linux开始围绕设备树展开,设备树有自己的独立语法,它的源文件为.dts,编译后得到.dtb,Bootloader在引导Linux内核的时候会将.dtb地址告知内核。之后内核会展开设备树并创建和注册相关的设备,因此arch/arm/mach-xxx和arch/arm/plat-xxx中的大量用于注册platform、I2C、SPI等板级信息的代码被删除,而驱动也以新的方式与在.dts中定义的设备节点进行匹配。

2013-01-01 17:32:36 21cnbao 阅读数 236087
  • Linux内核分析

    1.Bootloader概述 2.Linux内核分析初步 3.Linux内核配置 4.Linux文件系统

    1952课时 0分钟 294人学习 沈寒
    免费试看

本文部分案例和文字英文原版来源于 http://devicetree.org/Device_Tree_Usage

更多精华文章请扫描下方二维码关注Linux阅码场

1.    ARM Device Tree起源

Linus Torvalds在2011年3月17日的ARM Linux邮件列表宣称“this whole ARM thing is a f*cking pain in the ass”,引发ARM Linux社区的地震,随后ARM社区进行了一系列的重大修正。在过去的ARM Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。读者有兴趣可以统计下常见的s3c2410、s3c6410等板级目录,代码量在数万行。
社区必须改变这种局面,于是PowerPC等其他体系架构下已经使用的Flattened Device Tree(FDT)进入ARM社区的视野。Device Tree是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。在Linux 2.6中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,采用Device Tree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。
Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):

  • CPU的数量和类别
  • 内存基地址和大小
  • 总线和桥
  • 外设连接
  • 中断控制器和中断使用情况
  • GPIO控制器和GPIO使用情况
  • Clock控制器和Clock使用情况

它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。

2.    Device Tree组成和结构

整个Device Tree牵涉面比较广,即增加了新的用于描述设备硬件信息的文本格式,又增加了编译这一文本的工具,同时Bootloader也需要支持将编译后的Device Tree传递给Linux内核。

DTS (device tree source)

.dts文件是一种ASCII 文本格式的Device Tree描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在ARM Linux在,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录。由于一个SoC可能对应多个machine(一个SoC可以对应多个产品和电路板),势必这些.dts文件需包含许多共同的部分,Linux内核为了简化,把SoC公用的部分或者多个machine共同的部分一般提炼为.dtsi,类似于C语言的头文件。其他的machine对应的.dts就include这个.dtsi。譬如,对于VEXPRESS而言,vexpress-v2m.dtsi就被vexpress-v2p-ca9.dts所引用, vexpress-v2p-ca9.dts有如下一行:
/include/ "vexpress-v2m.dtsi"
当然,和C语言的头文件类似,.dtsi也可以include其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi。
.dts(或者其include的.dtsi)基本元素即为前文所述的结点和属性:

/ {
    node1 {
        a-string-property = "A string";
        a-string-list-property = "first string", "second string";
        a-byte-data-property = [0x01 0x23 0x34 0x56];
        child-node1 {
            first-child-property;
            second-child-property = <1>;
            a-string-property = "Hello, world";
        };
        child-node2 {
        };
    };
    node2 {
        an-empty-property;
        a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
        child-node1 {
        };
    };
};

上述.dts文件并没有什么真实的用途,但它基本表征了一个Device Tree源文件的结构:
1个root结点"/";
root结点下面含一系列子结点,本例中为"node1" 和 "node2";
结点"node1"下又含有一系列子结点,本例中为"child-node1" 和 "child-node2";
各结点都有一系列属性。这些属性可能为空,如" an-empty-property";可能为字符串,如"a-string-property";可能为字符串数组,如"a-string-list-property";可能为Cells(由u32整数组成),如"second-child-property",可能为二进制数,如"a-byte-data-property"。
下面以一个最简单的machine为例来看如何写一个.dts文件。假设此machine的配置如下:
1个双核ARM Cortex-A9 32位处理器;
ARM的local bus上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10170000)、中断控制器(位于0x10140000)和一个external bus桥;
External bus桥上又连接了SMC SMC91111 Ethernet(位于0x10100000)、I2C控制器(位于0x10160000)、64MB NOR Flash(位于0x30000000);
External bus桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58)。
其对应的.dts文件为:

/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&intc>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
        cpu@1 {
            compatible = "arm,cortex-a9";
            reg = <1>;
        };
    };

    serial@101f0000 {
        compatible = "arm,pl011";
        reg = <0x101f0000 0x1000 >;
        interrupts = < 1 0 >;
    };

    serial@101f2000 {
        compatible = "arm,pl011";
        reg = <0x101f2000 0x1000 >;
        interrupts = < 2 0 >;
    };

    gpio@101f3000 {
        compatible = "arm,pl061";
        reg = <0x101f3000 0x1000
               0x101f4000 0x0010>;
        interrupts = < 3 0 >;
    };

    intc: interrupt-controller@10140000 {
        compatible = "arm,pl190";
        reg = <0x10140000 0x1000 >;
        interrupt-controller;
        #interrupt-cells = <2>;
    };

    spi@10115000 {
        compatible = "arm,pl022";
        reg = <0x10115000 0x1000 >;
        interrupts = < 4 0 >;
    };

    external-bus {
        #address-cells = <2>
        #size-cells = <1>;
        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
                  2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash

        ethernet@0,0 {
            compatible = "smc,smc91c111";
            reg = <0 0 0x1000>;
            interrupts = < 5 2 >;
        };

        i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            #address-cells = <1>;
            #size-cells = <0>;
            reg = <1 0 0x1000>;
            interrupts = < 6 2 >;
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <58>;
                interrupts = < 7 3 >;
            };
        };

        flash@2,0 {
            compatible = "samsung,k8f1315ebm", "cfi-flash";
            reg = <2 0 0x4000000>;
        };
    };
};

上述.dts文件中,root结点"/"的compatible 属性compatible = "acme,coyotes-revenge";定义了系统的名称,它的组织形式为:<manufacturer>,<model>。Linux内核透过root结点"/"的compatible 属性即可判断它启动的是什么machine。
在.dts文件的每个设备,都有一个compatible 属性,compatible属性用户驱动和设备的绑定。compatible 属性是一个字符串的列表,列表中的第一个字符串表征了结点代表的确切设备,形式为"<manufacturer>,<model>",其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。如在arch/arm/boot/dts/vexpress-v2m.dtsi中的Flash结点:

flash@0,00000000 {
     compatible = "arm,vexpress-flash", "cfi-flash";
     reg = <0 0x00000000 0x04000000>,
     <1 0x00000000 0x04000000>;
     bank-width = <4>;
 };

compatible属性的第2个字符串"cfi-flash"明显比第1个字符串"arm,vexpress-flash"涵盖的范围更广。
再比如,Freescale MPC8349 SoC含一个串口设备,它实现了国家半导体(National Semiconductor)的ns16550 寄存器接口。则MPC8349串口设备的compatible属性为compatible = "fsl,mpc8349-uart", "ns16550"。其中,fsl,mpc8349-uart指代了确切的设备, ns16550代表该设备与National Semiconductor 的16550 UART保持了寄存器兼容。
接下来root结点"/"的cpus子结点下面又包含2个cpu子结点,描述了此machine上的2个CPU,并且二者的compatible 属性为"arm,cortex-a9"。
注意cpus和cpus的2个cpu子结点的命名,它们遵循的组织形式为:<name>[@<unit-address>],<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述结点对应的设备类型,如3com Ethernet适配器对应的结点name宜为ethernet,而不是3com509。如果一个结点描述的设备有地址,则应该给出@unit-address。多个相同类型设备结点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名结点。设备的unit-address地址也经常在其对应结点的reg属性中给出。ePAPR标准给出了结点命名的规范。
可寻址的设备使用如下信息来在Device Tree中编码地址信息:

  •     reg
  •     #address-cells
  •     #size-cells

其中reg的组织形式为reg = <address1 length1 [address2 length2] [address3 length3] ... >,其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length则为cell的列表或者为空(若#size-cells = 0)。address 和 length 字段是可变长的,父结点的#address-cells和#size-cells分别决定了子结点的reg属性的address和length字段的长度。在本例中,root结点的#address-cells = <1>;和#size-cells = <1>;决定了serial、gpio、spi等结点的address和length字段的长度分别为1。cpus 结点的#address-cells = <1>;和#size-cells = <0>;决定了2个cpu子结点的address为1,而length为空,于是形成了2个cpu的reg = <0>;和reg = <1>;。external-bus结点的#address-cells = <2>和#size-cells = <1>;决定了其下的ethernet、i2c、flash的reg字段形如reg = <0 0 0x1000>;、reg = <1 0 0x1000>;和reg = <2 0 0x4000000>;。其中,address字段长度为0,开始的第一个cell(0、1、2)是对应的片选,第2个cell(0,0,0)是相对该片选的基地址,第3个cell(0x1000、0x1000、0x4000000)为length。特别要留意的是i2c结点中定义的 #address-cells = <1>;和#size-cells = <0>;又作用到了I2C总线上连接的RTC,它的address字段为0x58,是设备的I2C地址。
root结点的子结点描述的是CPU的视图,因此root子结点的address区域就直接位于CPU的memory区域。但是,经过总线桥后的address往往需要经过转换才能对应的CPU的memory映射。external-bus的ranges属性定义了经过external-bus桥后的地址范围如何映射到CPU的memory区域。

        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
                  2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash

ranges是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。映射表中的子地址、父地址分别采用子地址空间的#address-cells和父地址空间的#address-cells大小。对于本例而言,子地址空间的#address-cells为2,父地址空间的#address-cells值为1,因此0 0  0x10100000   0x10000的前2个cell为external-bus后片选0上偏移0,第3个cell表示external-bus后片选0上偏移0的地址空间被映射到CPU的0x10100000位置,第4个cell表示映射的大小为0x10000。ranges的后面2个项目的含义可以类推。
Device Tree中还可以中断连接信息,对于中断控制器而言,它提供如下属性:
interrupt-controller – 这个属性为空,中断控制器应该加上此属性表明自己的身份;
#interrupt-cells – 与#address-cells 和 #size-cells相似,它表明连接此中断控制器的设备的interrupts属性的cell大小。
在整个Device Tree中,与中断相关的属性还包括:
interrupt-parent – 设备结点透过它来指定它所依附的中断控制器的phandle,当结点没有指定interrupt-parent 时,则从父级结点继承。对于本例而言,root结点指定了interrupt-parent = <&intc>;其对应于intc: interrupt-controller@10140000,而root结点的子结点并未指定interrupt-parent,因此它们都继承了intc,即位于0x10140000的中断控制器。
interrupts – 用到了中断的设备结点透过它指定中断号、触发方法等,具体这个属性含有多少个cell,由它依附的中断控制器结点的#interrupt-cells属性决定。而具体每个cell又是什么含义,一般由驱动的实现决定,而且也会在Device Tree的binding文档中说明。譬如,对于ARM GIC中断控制器而言,#interrupt-cells为3,它3个cell的具体含义Documentation/devicetree/bindings/arm/gic.txt就有如下文字说明:

01   The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI
02   interrupts.
03
04   The 2nd cell contains the interrupt number for the interrupt type.
05   SPI interrupts are in the range [0-987].  PPI interrupts are in the
06   range [0-15].
07
08   The 3rd cell is the flags, encoded as follows:
09         bits[3:0] trigger type and level flags.
10                 1 = low-to-high edge triggered
11                 2 = high-to-low edge triggered
12                 4 = active high level-sensitive
13                 8 = active low level-sensitive
14         bits[15:8] PPI interrupt cpu mask.  Each bit corresponds to each of
15         the 8 possible cpus attached to the GIC.  A bit set to '1' indicated
16         the interrupt is wired to that CPU.  Only valid for PPI interrupts.

另外,值得注意的是,一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,而言都是高电平触发,则该设备结点的interrupts属性可定义为:interrupts = <0 168 4>, <0 169 4>;
除了中断以外,在ARM Linux中clock、GPIO、pinmux都可以透过.dts中的结点和属性进行描述。

DTC (device tree compiler)

将.dts编译为.dtb的工具。DTC的源代码位于内核的scripts/dtc目录,在Linux内核使能了Device Tree的情况下,编译内核的时候主机工具dtc会被编译出来,对应scripts/dtc/Makefile中的“hostprogs-y := dtc”这一hostprogs编译target。
在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来,如与VEXPRESS对应的.dtb包括:

dtb-$(CONFIG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb \
        vexpress-v2p-ca9.dtb \
        vexpress-v2p-ca15-tc1.dtb \
        vexpress-v2p-ca15_a7.dtb \
        xenvm-4.2.dtb

在Linux下,我们可以单独编译Device Tree文件。当我们在Linux内核下运行make dtbs时,若我们之前选择了ARCH_VEXPRESS,上述.dtb都会由对应的.dts编译出来。因为arch/arm/Makefile中含有一个dtbs编译target项目。

Device Tree Blob (.dtb)

.dtb是.dts被DTC编译后的二进制格式的Device Tree描述,可由Linux内核解析。通常在我们为电路板制作NAND、SD启动image时,会为.dtb文件单独留下一个很小的区域以存放之,之后bootloader在引导kernel的过程中,会先读取该.dtb到内存。

Binding

对于Device Tree中的结点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。这些文档位于内核的Documentation/devicetree/bindings目录,其下又分为很多子目录。

Bootloader

Uboot mainline 从 v1.1.3开始支持Device Tree,其对ARM的支持则是和ARM内核支持Device Tree同期完成。
为了使能Device Tree,需要编译Uboot的时候在config文件中加入
#define CONFIG_OF_LIBFDT
在Uboot中,可以从NAND、SD或者TFTP等任意介质将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在Uboot运行命令fdt addr命令设置.dtb的地址,如:
U-Boot> fdt addr 0x71000000
fdt的其他命令就变地可以使用,如fdt resize、fdt print等。
对于ARM来讲,可以透过bootz kernel_addr initrd_address dtb_address的命令来启动内核,即dtb_address作为bootz或者bootm的最后一次参数,第一个参数为内核映像的地址,第二个参数为initrd的地址,若不存在initrd,可以用 -代替。

3.    Device Tree引发的BSP和驱动变更

有了Device Tree后,大量的板级信息都不再需要,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx实施的如下事情:
1.    注册platform_device,绑定resource,即内存、IRQ等板级信息。

透过Device Tree后,形如

 

90 static struct resource xxx_resources[] = {
91         [0] = {
92                 .start  = …,
93                 .end    = …,
94                 .flags  = IORESOURCE_MEM,
95         },
96         [1] = {
97                 .start  = …,
98                 .end    = …,
99                 .flags  = IORESOURCE_IRQ,
100         },
101 };
102
103 static struct platform_device xxx_device = {
104         .name           = "xxx",
105         .id             = -1,
106         .dev            = {
107                                 .platform_data          = &xxx_data,
108         },
109         .resource       = xxx_resources,
110         .num_resources  = ARRAY_SIZE(xxx_resources),
111 };

之类的platform_device代码都不再需要,其中platform_device会由kernel自动展开。而这些resource实际来源于.dts中设备结点的reg、interrupts属性。典型地,大多数总线都与“simple_bus”兼容,而在SoC对应的machine的.init_machine成员函数中,调用of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);即可自动展开所有的platform_device。譬如,假设我们有个XXX SoC,则可在arch/arm/mach-xxx/的板文件中透过如下方式展开.dts中的设备结点对应的platform_device:

 

 

18 static struct of_device_id xxx_of_bus_ids[] __initdata = {
19         { .compatible = "simple-bus", },
20         {},
21 };
22
23 void __init xxx_mach_init(void)
24 {
25         of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);
26 }
32
33 #ifdef CONFIG_ARCH_XXX
38
39 DT_MACHINE_START(XXX_DT, "Generic XXX (Flattened Device Tree)")
41         …
45         .init_machine   = xxx_mach_init,
46         …
49 MACHINE_END
50 #endif

 

 

 

2.    注册i2c_board_info,指定IRQ等板级信息。

形如

 

145 static struct i2c_board_info __initdata afeb9260_i2c_devices[] = {
146         {
147                 I2C_BOARD_INFO("tlv320aic23", 0x1a),
148         }, {
149                 I2C_BOARD_INFO("fm3130", 0x68),
150         }, {
151                 I2C_BOARD_INFO("24c64", 0x50),
152         },
153 };

之类的i2c_board_info代码,目前不再需要出现,现在只需要把tlv320aic23、fm3130、24c64这些设备结点填充作为相应的I2C controller结点的子结点即可,类似于前面的

 

 

      i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            …
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <58>;
                interrupts = < 7 3 >;
            };
        };

Device Tree中的I2C client会透过I2C host驱动的probe()函数中调用of_i2c_register_devices(&i2c_dev->adapter);被自动展开。

 

3.    注册spi_board_info,指定IRQ等板级信息。

形如

 

79 static struct spi_board_info afeb9260_spi_devices[] = {
80         {       /* DataFlash chip */
81                 .modalias       = "mtd_dataflash",
82                 .chip_select    = 1,
83                 .max_speed_hz   = 15 * 1000 * 1000,
84                 .bus_num        = 0,
85         },
86 };

之类的spi_board_info代码,目前不再需要出现,与I2C类似,现在只需要把mtd_dataflash之类的结点,作为SPI控制器的子结点即可,SPI host驱动的probe函数透过spi_register_master()注册master的时候,会自动展开依附于它的slave。

 

4.    多个针对不同电路板的machine,以及相关的callback。

过去,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个machine的一系列callback,譬如:

 

373 MACHINE_START(VEXPRESS, "ARM-Versatile Express")
374         .atag_offset    = 0x100,
375         .smp            = smp_ops(vexpress_smp_ops),
376         .map_io         = v2m_map_io,
377         .init_early     = v2m_init_early,
378         .init_irq       = v2m_init_irq,
379         .timer          = &v2m_timer,
380         .handle_irq     = gic_handle_irq,
381         .init_machine   = v2m_init,
382         .restart        = vexpress_restart,
383 MACHINE_END

这些不同的machine会有不同的MACHINE ID,Uboot在启动Linux内核时会将MACHINE ID存放在r1寄存器,Linux启动时会匹配Bootloader传递的MACHINE ID和MACHINE_START声明的MACHINE ID,然后执行相应machine的一系列初始化函数。

 

引入Device Tree之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的machine与.dts中root结点的compatible属性兼容关系。如果Bootloader传递给内核的Device Tree中root结点的compatible属性出现在某machine的.dt_compat表中,相关的machine就与对应的Device Tree匹配,从而引发这一machine的一系列初始化函数被执行。

 

489 static const char * const v2m_dt_match[] __initconst = {
490         "arm,vexpress",
491         "xen,xenvm",
492         NULL,
493 };
495 DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")
496         .dt_compat      = v2m_dt_match,
497         .smp            = smp_ops(vexpress_smp_ops),
498         .map_io         = v2m_dt_map_io,
499         .init_early     = v2m_dt_init_early,
500         .init_irq       = v2m_dt_init_irq,
501         .timer          = &v2m_dt_timer,
502         .init_machine   = v2m_dt_init,
503         .handle_irq     = gic_handle_irq,
504         .restart        = vexpress_restart,
505 MACHINE_END

Linux倡导针对多个SoC、多个电路板的通用DT machine,即一个DT machine的.dt_compat表含多个电路板.dts文件的root结点compatible属性字符串。之后,如果的电路板的初始化序列不一样,可以透过int of_machine_is_compatible(const char *compat) API判断具体的电路板是什么。

 

    譬如arch/arm/mach-exynos/mach-exynos5-dt.c的EXYNOS5_DT machine同时兼容"samsung,exynos5250"和"samsung,exynos5440":

 

158 static char const *exynos5_dt_compat[] __initdata = {
159         "samsung,exynos5250",
160         "samsung,exynos5440",
161         NULL
162 };
163
177 DT_MACHINE_START(EXYNOS5_DT, "SAMSUNG EXYNOS5 (Flattened Device Tree)")
178         /* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
179         .init_irq       = exynos5_init_irq,
180         .smp            = smp_ops(exynos_smp_ops),
181         .map_io         = exynos5_dt_map_io,
182         .handle_irq     = gic_handle_irq,
183         .init_machine   = exynos5_dt_machine_init,
184         .init_late      = exynos_init_late,
185         .timer          = &exynos4_timer,
186         .dt_compat      = exynos5_dt_compat,
187         .restart        = exynos5_restart,
188         .reserve        = exynos5_reserve,
189 MACHINE_END

     它的.init_machine成员函数就针对不同的machine进行了不同的分支处理:

 

126 static void __init exynos5_dt_machine_init(void)
127 {
128         …
149
150         if (of_machine_is_compatible("samsung,exynos5250"))
151                 of_platform_populate(NULL, of_default_bus_match_table,
152                                      exynos5250_auxdata_lookup, NULL);
153         else if (of_machine_is_compatible("samsung,exynos5440"))
154                 of_platform_populate(NULL, of_default_bus_match_table,
155                                      exynos5440_auxdata_lookup, NULL);
156 }

 

 

 

使用Device Tree后,驱动需要与.dts中描述的设备结点进行匹配,从而引发驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的"acme,a1234-i2c-bus"兼容I2C控制器结点的OF匹配表可以是:

 

436 static const struct of_device_id a1234_i2c_of_match[] = {
437         { .compatible = "acme,a1234-i2c-bus ", },
438         {},
439 };
440 MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);
441
442 static struct platform_driver i2c_a1234_driver = {
443         .driver = {
444                 .name = "a1234-i2c-bus ",
445                 .owner = THIS_MODULE,
449                 .of_match_table = a1234_i2c_of_match,
450         },
451         .probe = i2c_a1234_probe,
452         .remove = i2c_a1234_remove,
453 };
454 module_platform_driver(i2c_a1234_driver);

 

对于I2C和SPI从设备而言,同样也可以透过of_match_table添加匹配的.dts中的相关结点的compatible属性,如sound/soc/codecs/wm8753.c中的:

 

1533 static const struct of_device_id wm8753_of_match[] = {
1534         { .compatible = "wlf,wm8753", },
1535         { }
1536 };
1537 MODULE_DEVICE_TABLE(of, wm8753_of_match);
1587 static struct spi_driver wm8753_spi_driver = {
1588         .driver = {
1589                 .name   = "wm8753",
1590                 .owner  = THIS_MODULE,
1591                 .of_match_table = wm8753_of_match,
1592         },
1593         .probe          = wm8753_spi_probe,
1594         .remove         = wm8753_spi_remove,
1595 };
1640 static struct i2c_driver wm8753_i2c_driver = {
1641         .driver = {
1642                 .name = "wm8753",
1643                 .owner = THIS_MODULE,
1644                 .of_match_table = wm8753_of_match,
1645         },
1646         .probe =    wm8753_i2c_probe,
1647         .remove =   wm8753_i2c_remove,
1648         .id_table = wm8753_i2c_id,
1649 };

不过这边有一点需要提醒的是,I2C和SPI外设驱动和Device Tree中设备结点的compatible 属性还有一种弱式匹配方法,就是别名匹配。compatible 属性的组织形式为<manufacturer>,<model>,别名其实就是去掉compatible 属性中逗号前的manufacturer前缀。关于这一点,可查看drivers/spi/spi.c的源代码,函数spi_match_device()暴露了更多的细节,如果别名出现在设备spi_driver的id_table里面,或者别名与spi_driver的name字段相同,SPI设备和驱动都可以匹配上:

 

 

 

90 static int spi_match_device(struct device *dev, struct device_driver *drv)
91 {
92         const struct spi_device *spi = to_spi_device(dev);
93         const struct spi_driver *sdrv = to_spi_driver(drv);
94
95         /* Attempt an OF style match */
96         if (of_driver_match_device(dev, drv))
97                 return 1;
98
99         /* Then try ACPI */
100         if (acpi_driver_match_device(dev, drv))
101                 return 1;
102
103         if (sdrv->id_table)
104                 return !!spi_match_id(sdrv->id_table, spi);
105
106         return strcmp(spi->modalias, drv->name) == 0;
107 }
71 static const struct spi_device_id *spi_match_id(const struct spi_device_id *id,
72                                                 const struct spi_device *sdev)
73 {
74         while (id->name[0]) {
75                 if (!strcmp(sdev->modalias, id->name))
76                         return id;
77                 id++;
78         }
79         return NULL;
80 }

 

4.    常用OF API

在Linux的BSP和驱动代码中,还经常会使用到Linux中一组Device Tree的API,这些API通常被冠以of_前缀,它们的实现代码位于内核的drivers/of目录。这些常用的API包括:

int of_device_is_compatible(const struct device_node *device,const char *compat);

判断设备结点的compatible 属性是否包含compat指定的字符串。当一个驱动支持2个或多个设备的时候,这些不同.dts文件中设备的compatible 属性都会进入驱动 OF匹配表。因此驱动可以透过Bootloader传递给内核的Device Tree中的真正结点的compatible 属性以确定究竟是哪一种设备,从而根据不同的设备类型进行不同的处理。如drivers/pinctrl/pinctrl-sirf.c即兼容于"sirf,prima2-pinctrl",又兼容于"sirf,prima2-pinctrl",在驱动中就有相应分支处理:

 

1682 if (of_device_is_compatible(np, "sirf,marco-pinctrl"))
1683      is_marco = 1;

struct device_node *of_find_compatible_node(struct device_node *from,

 

         const char *type, const char *compatible);

 

根据compatible属性,获得设备结点。遍历Device Tree中所有的设备结点,看看哪个结点的类型、compatible属性与本函数的输入参数匹配,大多数情况下,from、type为NULL。

int of_property_read_u8_array(const struct device_node *np,

                     const char *propname, u8 *out_values, size_t sz);

int of_property_read_u16_array(const struct device_node *np,

                      const char *propname, u16 *out_values, size_t sz);

int of_property_read_u32_array(const struct device_node *np,

                      const char *propname, u32 *out_values, size_t sz);

int of_property_read_u64(const struct device_node *np, const char

*propname, u64 *out_value);

读取设备结点np的属性名为propname,类型为8、16、32、64位整型数组的属性。对于32位处理器来讲,最常用的是of_property_read_u32_array()。如在arch/arm/mm/cache-l2x0.c中,透过如下语句读取L2 cache的"arm,data-latency"属性:

 

534         of_property_read_u32_array(np, "arm,data-latency",
535                                    data, ARRAY_SIZE(data));

在arch/arm/boot/dts/vexpress-v2p-ca9.dts中,含有"arm,data-latency"属性的L2 cache结点如下:

 

137         L2: cache-controller@1e00a000 {
138                 compatible = "arm,pl310-cache";
139                 reg = <0x1e00a000 0x1000>;
140                 interrupts = <0 43 4>;
141                 cache-level = <2>;
142                 arm,data-latency = <1 1 1>;
143                 arm,tag-latency = <1 1 1>;
144         }


有些情况下,整形属性的长度可能为1,于是内核为了方便调用者,又在上述API的基础上封装出了更加简单的读单一整形属性的API,它们为int of_property_read_u8()、of_property_read_u16()等,实现于include/linux/of.h:

 

 

513 static inline int of_property_read_u8(const struct device_node *np,
514                                        const char *propname,
515                                        u8 *out_value)
516 {
517         return of_property_read_u8_array(np, propname, out_value, 1);
518 }
519
520 static inline int of_property_read_u16(const struct device_node *np,
521                                        const char *propname,
522                                        u16 *out_value)
523 {
524         return of_property_read_u16_array(np, propname, out_value, 1);
525 }
526
527 static inline int of_property_read_u32(const struct device_node *np,
528                                        const char *propname,
529                                        u32 *out_value)
530 {
531         return of_property_read_u32_array(np, propname, out_value, 1);
532 }


int of_property_read_string(struct device_node *np, const char

 

*propname, const char **out_string);

int of_property_read_string_index(struct device_node *np, const char

    *propname, int index, const char **output);

前者读取字符串属性,后者读取字符串数组属性中的第index个字符串。如drivers/clk/clk.c中的of_clk_get_parent_name()透过of_property_read_string_index()遍历clkspec结点的所有"clock-output-names"字符串数组属性。

 

1759 const char *of_clk_get_parent_name(struct device_node *np, int index)
1760 {
1761         struct of_phandle_args clkspec;
1762         const char *clk_name;
1763         int rc;
1764
1765         if (index < 0)
1766                 return NULL;
1767
1768         rc = of_parse_phandle_with_args(np, "clocks", "#clock-cells", index,
1769                                         &clkspec);
1770         if (rc)
1771                 return NULL;
1772
1773         if (of_property_read_string_index(clkspec.np, "clock-output-names",
1774                                   clkspec.args_count ? clkspec.args[0] : 0,
1775                                           &clk_name) < 0)
1776                 clk_name = clkspec.np->name;
1777
1778         of_node_put(clkspec.np);
1779         return clk_name;
1780 }
1781 EXPORT_SYMBOL_GPL(of_clk_get_parent_name);


static inline bool of_property_read_bool(const struct device_node *np,

 

                                         const char *propname);

 

如果设备结点np含有propname属性,则返回true,否则返回false。一般用于检查空属性是否存在。

 

void __iomem *of_iomap(struct device_node *node, int index);

 

通过设备结点直接进行设备内存区间的 ioremap(),index是内存段的索引。若设备结点的reg属性有多段,可通过index标示要ioremap的是哪一段,只有1段的情况,index为0。采用Device Tree后,大量的设备驱动通过of_iomap()进行映射,而不再通过传统的ioremap。

 

unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

透过Device Tree或者设备的中断号,实际上是从.dts中的interrupts属性解析出中断号。若设备使用了多个中断,index指定中断的索引号。

 

还有一些OF API,这里不一一列举,具体可参考include/linux/of.h头文件。

5.    总结

ARM社区一贯充斥的大量垃圾代码导致Linus盛怒,因此社区在2011年到2012年进行了大量的工作。ARM Linux开始围绕Device Tree展开,Device Tree有自己的独立的语法,它的源文件为.dts,编译后得到.dtb,Bootloader在引导Linux内核的时候会将.dtb地址告知内核。之后内核会展开Device Tree并创建和注册相关的设备,因此arch/arm/mach-xxx和arch/arm/plat-xxx中大量的用于注册platform、I2C、SPI板级信息的代码被删除,而驱动也以新的方式和.dts中定义的设备结点进行匹配。

更多精华文章请扫描下方二维码关注Linux阅码场

2014-04-16 20:19:05 zmc1216 阅读数 2776
  • Linux内核分析

    1.Bootloader概述 2.Linux内核分析初步 3.Linux内核配置 4.Linux文件系统

    1952课时 0分钟 294人学习 沈寒
    免费试看

U盘插入开发板后,有以下打印:

usb 1-1: configuration #1 chosen from 1 choice

scsi1 : SCSI emulation for USB Mass Storage devices
scsi 1:0:0:0: Direct-Access     Kingston DT 101 G2        PMAP PQ: 0 ANSI: 0 CCS
sd 1:0:0:0: [sda] 7669824 512-byte hardware sectors: (3.92 GB/3.65 GiB)
sd 1:0:0:0: [sda] Write Protect is off
sd 1:0:0:0: [sda] Assuming drive cache: write through
sd 1:0:0:0: [sda] Assuming drive cache: write through
 sda: sda1
sd 1:0:0:0: [sda] Attached SCSI removable disk

但是查看/dev/下面并没有,意思到我的Linux并没有加入mdev机制,所以需要手动创建结点。

在我PC上ls /dev/ -l

brw-rw---- 1 root disk 8,  0  4月 16 08:31 /dev/sda
brw-rw---- 1 root disk 8,  1  4月 16 08:31 /dev/sda1
brw-rw---- 1 root disk 8,  2  4月 16 08:31 /dev/sda2
brw-rw---- 1 root disk 8,  5  4月 16 08:31 /dev/sda5
brw-rw---- 1 root disk 8,  6  4月 16 08:31 /dev/sda6
brw-rw---- 1 root disk 8,  7  4月 16 08:31 /dev/sda7
brw-rw---- 1 root disk 8,  8  4月 16 08:31 /dev/sda8
brw-rw---- 1 root disk 8, 16  4月 16 20:03 /dev/sdb
brw-rw---- 1 root disk 8, 17  4月 16 20:03 /dev/sdb1
U盘的主设备号8,次设备号16,

在开发板上

mknod /dev/sda b 8 1

mknod /dev/sda1 b 8 2

mount /dev/sda /mnt

U盘挂载成功


注:

mdev是busybox自带的一个简化版的udev,适合于嵌入式的应用场合。其具有使用简单的特点。它的作用,就是在系统启动和热插拔或者动态加载驱动程序时,自动产生驱动程序所需的节点文件。


2012-12-29 22:20:40 czh52911 阅读数 17034
  • Linux内核分析

    1.Bootloader概述 2.Linux内核分析初步 3.Linux内核配置 4.Linux文件系统

    1952课时 0分钟 294人学习 沈寒
    免费试看
一、土八路做法:
SD 卡一旦插入系统,内核会自动在/dev/下创建设备文件:sdcard。 但有时可能时用户在拨出卡前并没有umount的话,第二次插卡进去后系统创建的就不是sdcard设备文件了,而是mmcblk0, mmcblk1p1, mmcblk2p1, 或mmcblk3p1.

所以只需用if ( fopen("/dev/sdcard", "r") == NULL ) 来检测SD卡是否已经被内核捉到。然后就可以mount ....以及你想干的事情了。

如何得知自己已经有mount 了呢?
cat /proc/mounts 之后就会有:
rootfs / rootfs rw 0 0
/dev/root / yaffs rw,relatime 0 0
none /proc proc rw,relatime 0 0
none /sys sysfs rw,relatime 0 0
none /dev ramfs rw,relatime 0 0
none /dev/pts devpts rw,relatime,mode=622 0 0
tmpfs /dev/shm tmpfs rw,relatime 0 0
none /tmp ramfs rw,relatime 0 0
none /var ramfs rw,relatime 0 0
/dev/sdcard /mnt vfat rw,relatime,fmask=0022,dmask=0000,allow_utime=0022,codepag
e=cp437,iocharset=iso8859-1 0 0
/dev/mmcblk1p1 /mnt vfat rw,relatime,fmask=0022,dmask=0000,allow_utime=0022,code
page=cp437,iocharset=iso8859-1 0 0

哈哈。。。 最后两个就是我刚刚mount的。

二、专业的mdev方法(还有udev):

mdev是busybox中的一个udev管理程序的一个精简版,他也可以实现设备节点的自动创建和设备的自动挂载,只是在实现的过程中有点差异,在发生热插拔时间的时候,mdev是被hotplug直接调用,这时mdev通过环境变量中的 ACTION 和 DEVPATH,来确定此次热插拔事件的动作以及影响了/sys中的那个目录。接着会看看这个目录中是否有“dev”的属性文件,如果有就利用这些信息为这个设备在/dev 下创建设备节点文件。

1.在使用busybox制作根文件系统的时候,选择支持mdev

Linux System Utilities  --->   
           [*] mdev      
           [*]   Support /etc/mdev.conf
           [*]     Support command execution at device addition/removal

2.在文件系统添加如下内容

Vim /etc/init.d/rcS
        mount -t tmpfs mdev /dev 
        mount -t sysfs sysfs /sys
        mkdir /dev/pts
        mount -t devpts devpts /dev/pts

echo /sbin/mdev>/proc/sys/kernel/hotplug
        mdev –s

这些语句的添加在mdev的手册中可以找到。

3.添加对热插拔事件的响应,实现U盘和SD卡的自动挂载。

Vim /etc/mdev.conf
        mmcblk[0-9]p[0-9] 0:0 666 @ /etc/sd_card_inserting
        mmcblk[0-9] 0:0 666 $ /etc/sd_card_removing
        sd[a-z] [0-9] 0:0 666 @ /etc/usb/usb_inserting
        sd[a-z] 0:0 666 $ /etc/usb/usb_removing

灰色部分,是一个脚本,脚本内容可以根据我们的需要定制,可以实现挂载,卸载或其他一些功能。

如下是自动挂载和卸载的脚本:

/etc/sd_card_inserting
        #!/bin/sh
        mount -t vfat /dev/mmcblk0p1 /mnt/sd

/etc/sd_card_removing
        #!/bin/sh
        sync
        umount /mnt/sd