Linux设备树——Linux驱动开发

参考正点原子i.mx6u Linux驱动开发指南

一、什么是设备树

设备树(Device Tree) ,将这个词分开就是"设备"和"树",描述设备树的文件叫做 DTS(Device Tree Source) ,这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、 IIC 接口上接了哪些设备、 SPI 接口上接了哪些设备等等,如图


树的主干就是系统总线,IIC 控制器、 GPIO 控制器、 SPI 控制器等都是接 到系统主线上的分支。 IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02 这两个 IIC 设备, IIC2 上只接了 MPU6050 这个设备。 DTS 文件的主要功能就是按照图 43.1.1 所示的结构来描述板子上的设备信息, DTS 文件描述设备信息是有相应的语法规则要求的,稍 后我们会详细的讲解 DTS 语法规则。
在 3.x 版本 ( 具体哪个版本笔者也无从考证 ) 以前的 Linux 内核中 ARM 架构并没有采用设备 树。在没有设备树的时候 Linux 是如何描述 ARM 架构中的板级信息呢?在 Linux 内核源码中 大量的 arch/arm/mach-xxx 和 arch/arm/plat-xxx 文件夹,这些文件夹里面的文件就是对应平台下 的板级信息。比如在 arch/arm/mach-smdk2440.c 中有如下内容 ( 有缩减 ):

cpp 复制代码
static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {

	.lcdcon5 = S3C2410_LCDCON5_FRM565 |
			   S3C2410_LCDCON5_INVVLINE |
			   S3C2410_LCDCON5_INVVFRAME |
			   S3C2410_LCDCON5_PWREN |
			   S3C2410_LCDCON5_HWSWP,
	......};
static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
	.displays = &smdk2440_lcd_cfg,
	.num_displays = 1,
	.default_display = 0,
	......};

static struct platform_device *smdk2440_devices[] __initdata = {
	&s3c_device_ohci,
	&s3c_device_lcd,
	&s3c_device_wdt,
	&s3c_device_i2c0,
	&s3c_device_iis,
};

上述代码中的结构体变量 smdk2440_fb_info 就是描述 SMDK2440 这个开发板上的 LCD 信 息的,结构体指针数组 smdk2440_devices 描述的 SMDK2440 这个开发板上的所有平台相关信 息。这个仅仅是使用 2440 这个芯片的 SMDK2440 开发板下的 LCD 信息, SMDK2440 开发板 还有很多的其他外设硬件和平台硬件信息。使用 2440 这个芯片的板子有很多,每个板子都有描 述相应板级信息的文件,这仅仅只是一个 2440 。随着智能手机的发展,每年新出的 ARM 架构 芯片少说都在数十、数百款, Linux 内核下板级信息文件将会成指数级增长!这些板级信息文件 都是 .c 或 .h 文件,都会被硬编码进 Linux 内核中,导致 Linux 内核"虚胖"。就好比你喜欢吃自 助餐,然后花了 100 多到一家宣传看着很不错的自助餐厅,结果你想吃的牛排、海鲜、烤肉基 本没多少,全都是一些凉菜、炒面、西瓜、饮料等小吃,相信你此时肯定会脱口而出一句" F*k! "、 "骗子!"。同样的,当 Linux 之父 linus 看到 ARM 社区向 Linux 内核添加了大量"无用"、冗余 的板级信息文件,不禁的发出了一句" This whole ARM thing is a f*cking pain in the ass "。从此以 后 ARM 社区就引入了 PowerPC 等架构已经采用的设备树 (Flattened Device Tree) ,将这些描述 板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts 。一个 SOC 可以作出很多不同的板子,这些不同的板子肯 定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的 .dts 文件直接引 用这个通用文件即可,这个通用文件就是 .dtsi 文件,类似于 C 语言中的头文件。一般 .dts 描述 板级信息 ( 也就是开发板上有哪些 IIC 设备、 SPI 设备等 ) , .dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。
这个就是设备树的由来,简而言之就是,Linux 内核中 ARM 架构下有太多的冗余的垃圾板 级信息文件,导致 linus 震怒,然后 ARM 社区引入了设备树。

二、dts、dtb、dtc

上一小节说了,设备树源文件扩展名为.dts ,但是我们在前面移植 Linux 的时候却一直在使 用 .dtb 文件,那么 DTS 和 DTB 这两个文件是什么关系呢? DTS 是设备树源码文件, DTB 是将 DTS 编译以后得到的二进制文件。将 .c 文件编译为 .o 需要用到 gcc 编译器,那么将 .dts 编译为 .dtb 需要什么工具呢?需要用到 DTC 工具! DTC 工具源码在 Linux 内核的 scripts/dtc 目录下, scripts/dtc/Makefile 文件内容如下:

cpp 复制代码
hostprogs-y := dtc
always := $(hostprogs-y)

dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
			srcpos.o checks.o util.o
dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o
......

可以看出,DTC 工具依赖于 dtc.c 、 flattree.c 、 fstree.c 等文件,最终编译并链接出 DTC 这 个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命 令:
make dtbs
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一 个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对 应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile ,有如下内容:


可以看出,当选中 I.MX6ULL 这个 SOC 以后 (CONFIG_SOC_IMX6ULL=y) ,所有使用到 I.MX6ULL 这个 SOC 的板子对应的 .dts 文件都会被编译为 .dtb 。如果我们使用 I.MX6ULL 新做 了一个板子,只需要新建一个此板子对应的 .dts 文件,然后将对应的 .dtb 文件名添加到 dtb- $(CONFIG_SOC_IMX6ULL) 下,这样在编译设备树的时候就会将对应的 .dts 编译为二进制的 .dtb 文件。 比如上图中笔者在 Linux移植教程------基于 I.MX6ULL 开发板中添加的dtb文件。

三、dts 语法

虽然我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的 .dts 文件上进行修改。但是 DTS 文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改 .dts 文件。大家不要看到要学习新的语法就觉得会很复杂, DTS 语法非常的人性化,是一种 ASCII 文本文件,不管是阅读还是修改都很方便。

3.1 dtsi 头文件

和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为 .dtsi 。在 imx6ull-lyh- emmc.dts 中有如下所示内容:


在.dts 设备树文件中,可以通过 " #include "来引用 .h 、 .dtsi 和 .dts 文件。只是,我们在编写设备树头文件的时候最好选择 .dtsi 后 缀。
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART 、 IIC 等等。比如 imx6ull.dtsi 就是描述 I.MX6ULL 这颗 SOC 内部外设情况信息 的,部分内容如下:


cpu0 这个设备节点信息,这个节点信息描述了 I.MX6ULL 这颗 SOC 所使用的 CPU 信息,比如架构是 cortex-A7 ,频率支持 996MHz 、 792MHz 、 528MHz 、 396MHz 和 198MHz 等等。在 imx6ull.dtsi 文件中不仅仅描述了 cpu0 这一个节点信息, I.MX6ULL 这颗 SOC 所有的外设都描述的清清楚楚,比如 ecspi1~4 、 uart1~8 、 usbphy1~2 、 i2c1~4 等等。

3.2 设备节点

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设 备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键---值对。以下是从 imx6ull.dtsi 文件中缩减出来的设备树文件内容:

/ {
	aliases
	{
		can0 = &flexcan1;
	};

	cpus
	{
#address - cells = < 1>;
#size - cells = < 0>;

	cpu0:
		cpu @0
		{
			compatible = "arm,cortex-a7";
			device_type = "cpu";
			reg = <0>;
		};
	};

intc:
	interrupt - controller @00a01000
	{
		compatible = "arm,cortex-a7-gic";
#interrupt - cells = < 3>;
		interrupt - controller;
		reg = <0x00a01000 0x1000>,
		<0x00a02000 0x100>;
	};
}

第 1 行," / "是根节点,每个设备树文件只有一个根节点。 imx6ull.dtsi 和 imx6ull-lyh-emmc.dts 这两个文件都有一个" / "根节点,这样不会出错吗?不会的,因为 这两个" / "根节点的内容会合并成一个根节点。
第 2 、 6 和 17 行, aliases 、 cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:
node-name@unit-address
其中"node-name "是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的 功能,比如" uart1 "就表示这个节点是 UART1 外设。" unit-address "一般表示设备的地址或寄 存器首地址,如果某个节点没有地址或者寄存器的话" unit-address "可以不要,比如" cpu@0 "、 " interrupt-controller@00a01000 "。
但是我们在示例代码 中我们看到的节点命名却如下所示:
cpu0:cpu@0
上述命令并不是"node-name@unit-address "这样的格式,而是用":"隔开成了两部分,":" 前面的是节点标签 (label),":"后面的才是节点名字,格式如下所示:
label: node-name@unit-address
引入 label 的目的就是为了方便访问节点,可以直接通过 &label 来访问这个节点,比如通过 &cpu0 就可以访问" cpu@0 "这个节点,而不需要输入完整的节点名字。再比如节点 " intc: interrupt-controller@00a01000 ",节点 label 是 intc ,而节点名字就很长了,为" interrupt- controller@00a01000 "。很明显通过 &intc 来访问" interrupt-controller@00a01000 "这个节点要方 便很多!
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任 意的字节流。设备树源码中常用的几种数据形式如下所示
①、字符串
compatible = "arm,cortex-a7";
上述代码设置 compatible 属性的值为字符串" arm,cortex-a7 "。
②、32 位无符号整数
reg = <0>;
上述代码设置 reg 属性的值为 0 , reg 的值也可以设置为一组值,比如:
reg = <0 0x123456 100>;
③、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用" , "隔开,如下所示:
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
上述代码设置属性 compatible 的值为" fsl,imx6ull-gpmi-nand "和" fsl, imx6ul-gpmi-nand "。

3.3 标准属性

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以 自定义属性。除了用户自定义属性,有很多属性是标准属性, Linux 下的很多外设驱动都会使用 这些标准属性,本节我们就来学习一下几个常用的标准属性。
1 compatible 属性
compatible 属性也叫做"兼容性"属性,这是非常重要的一个属性! compatible 属性的值是 一个字符串列表, compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要 使用的驱动程序, compatible 属性的值格式如下所示:
"manufacturer,model"
其中 manufacturer 表示厂商, model 一般是模块对应的驱动名字。比如 imx6ull-lyh
-emmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点, I.MX6U-ALPHA 开发板上 的音频芯片采用的欧胜 (WOLFSON) 出品的 WM8960 , sound 节点的 compatible 属性值如下:
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,分别为"fsl,imx6ul-evk-wm8960 "和" fsl,imx-audio-wm8960 ",其中" fsl " 表示厂商是飞思卡尔," imx6ul-evk-wm8960 "和" imx-audio-wm8960 "表示驱动模块名字。 sound 这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件, 如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设 备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个 驱动。比如在文件 imx-wm8960.c 中有如下内容:

cpp 复制代码
static const struct of_device_id imx_wm8960_dt_ids[] = {
	{
		.compatible = "fsl,imx-audio-wm8960",
	},
	{/* sentinel */}};
MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);

static struct platform_driver imx_wm8960_driver = {
	.driver = {
		.name = "imx-wm8960",
		.pm = &snd_soc_pm_ops,
		.of_match_table = imx_wm8960_dt_ids,
	},
	.probe = imx_wm8960_probe,
	.remove = imx_wm8960_remove,
};

数组 imx_wm8960_dt_ids 就是 imx-wm8960.c 这个驱动文件的匹配表,此 匹配表只有一个匹配值" fsl,imx-audio-wm8960 "。如果在设备树中有哪个节点的 compatible 属 性值与此相等,那么这个节点就会使用此驱动文件。
wm8960 采用了 platform_driver 驱动模式,关于 platform_driver 驱动后面会讲 解。此行设置 .of_match_table 为 imx_wm8960_dt_ids ,也就是设置这个 platform_driver 所使用的 OF 匹配表。
2 model 属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比如:
model = "wm8960-audio";
3 status 属性
status 属性看名字就知道是和设备状态有关的, status 属性值也是字符串,字符串是设备的 状态信息,可选的状态如表 43.3.3.1 所示:


4 #address-cells #size-cells 属性
这两个属性的值都是无符号 32 位整形, #address-cells 和 #size-cells 这两个属性可以用在任 何拥有子节点的设备中,用于描述子节点的地址信息。 #address-cells 属性值决定了子节点 reg 属 性中地址信息所占用的字长 (32 位 ) , #size-cells 属性值决定了子节点 reg 属性中长度信息所占的 字长 (32 位 ) 。 #address-cells 和 #size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性 都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度, reg 属性的格式一为:
reg = <address1 length1 address2 length2 address3 length3 ...... >
每个"address length "组合表示一个地址范围,其中 address 是起始地址, length 是地址长 度, #address-cells 表明 address 这个数据所占用的字长, #size-cells 表明 length 这个数据所占用 的字长,比如 :

cpp 复制代码
spi4
{
	compatible = "spi-gpio";
	#address - cells = <1>;
	#size - cells = <0>;

	gpio_spi:gpio_spi @0
	{
		compatible = "fairchild,74hc595";
		reg = <0>;
	};
};

aips3 : aips - bus @02200000
{
	compatible = "fsl,aips-bus", "simple-bus";
	#address - cells = <1>;
	#size - cells = <1>;

	dcp:dcp @02280000
	{
		compatible = "fsl,imx6sl-dcp";
		reg = <0x02280000 0x4000>;
	};
};

5 reg 属性
reg 属性前面已经提到过了, reg 属性的值一般是 (address , length) 对。 reg 属性一般用于描 述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在 imx6ull.dtsi 中有 如下内容:

cpp 复制代码
uart1: serial@02020000 {
	compatible = "fsl,imx6ul-uart",
				"fsl,imx6q-uart", "fsl,imx21-uart";
	reg = <0x02020000 0x4000>;
	interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_UART1_IPG>,
			<&clks IMX6UL_CLK_UART1_SERIAL>;
	clock-names = "ipg", "per";
	status = "disabled";
};

上述代码是节点 uart1 , uart1 节点描述了 I.MX6ULL 的 UART1 相关信息,重点是第 326 行的 reg 属性。其中 uart1 的父节点 aips1: aips-bus@02000000 设置了 #address-cells = <1> 、 #size- cells = <1> ,因此 reg 属性中 address=0x02020000 , length=0x4000 。查阅《 I.MX6ULL 参考手册》 可知, I.MX6ULL 的 UART1 寄存器首地址为 0x02020000 ,但是 UART1 的地址长度 ( 范围 ) 并没 有 0x4000 这么多,这里我们重点是获取 UART1 寄存器首地址。
6 ranges 属性
ranges属性值可以为空或者按照 (child-bus-address,parent-bus-address,length) 格式编写的数字 矩阵, ranges 是一个地址映射 / 转换表, ranges 属性每个项目由子地址、父地址和地址空间长度 这三部分组成:
child-bus-address :子总线地址空间的物理地址,由父节点的 #address-cells 确定此物理地址所占用的字长。
parent-bus-address 父总线地址空间的物理地址,同样由父节点的 #address-cells 确定此物理地址所占用的字长。
length 子地址空间的长度,由父节点的 #size-cells 确定此地址长度所占用的字长。如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换, 对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在 imx6ull.dtsi 中找到大量的值为空的 ranges 属性,如下所示:

cpp 复制代码
soc {
	compatible = "simple-bus";
	#address-cells = <1>;
	#size-cells = <1>;
	ranges = <0x0 0xe0000000 0x00100000>;

	serial {
		device_type = "serial";
		compatible = "ns16550";
		reg = <0x4600 0x100>;
		clock-frequency = <0>;
		interrupts = <0xA 0x8>;
		interrupt-parent = <&ipic>;
	};
};

第 5 行,节点 soc 定义的 ranges 属性,值为 <0x0 0xe0000000 0x00100000> ,此属性值指定 了一个 1024KB(0x00100000) 的地址范围,子地址空间的物理起始地址为 0x0 ,父地址空间的物 理起始地址为 0xe0000000 。
第 10 行, serial 是串口设备节点, reg 属性定义了 serial 设备寄存器的起始地址为 0x4600 , 寄存器长度为 0x100 。经过地址转换, serial 设备可以从 0xe0004600 开始进行读写操作, 0xe0004600=0x4600+0xe0000000 。
7 name 属性
name 属性值为字符串, name 属性用于记录节点名字, name 属性已经被弃用,不推荐使用 name 属性,一些老的设备树文件可能会使用此属性。
8 device_type 属性
device_type 属性值为字符串, IEEE 1275 会用到此属性,用于描述设备的 FCode ,但是设 备树没有 FCode ,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。 imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:

cpp 复制代码
cpu0: cpu@0 {
	compatible = "arm,cortex-a7";
	device_type = "cpu";
	reg = <0>;
	......
};

关于标准属性就讲解这么多,其他的比如中断、IIC 、 SPI 等使用的标准属性等到具体的例 程再讲解。

3.4 根节点compatible属性

每个节点都有 compatible 属性,根节点" / "也不例外, imx6ull-alientek-emmc.dts 文件中根 节点的 compatible 属性内容如下所示:

cpp 复制代码
/ {
	model = "Freescale i.MX6 ULL 14x14 EVK Board";
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
	......

可以看出,compatible 有两个值:" fsl,imx6ull-14x14-evk "和" fsl,imx6ull "。前面我们说了, 设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,那么根节点中的 compatible 属性是为了做什么工作的? 通过根节点的 compatible 属性可以知道我们所使用的设备,一般第 一个值描述了所使用的硬件设备名字,比如这里使用的是" imx6ull-14x14-evk "这个设备,第二 个值描述了设备所使用的 SOC ,比如这里使用的是" imx6ull "这颗 SOC 。 Linux 内核会通过根 节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。接下来 我们就来学习一下 Linux 内核在使用设备树前后是如何判断是否支持某款设备的。
1 、使用设备树之前设备匹配方法
在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值, machine id 也就是设备 ID ,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。 Linux 内核是 支持很多设备的,针对每一个设备 ( 板子 ) , Linux 内核都用 MACHINE_START 和 MACHINE_END 来定义一个 machine_desc 结构体来描述这个设备,比如在文件 arch/arm/mach-imx/mach- mx35_3ds.c 中有如下定义:

cpp 复制代码
MACHINE_START(MX35_3DS, "Freescale MX35PDK")
	/* Maintainer: Freescale Semiconductor, Inc */
	.atag_offset = 0x100,
	.map_io = mx35_map_io,
	.init_early = imx35_init_early,
	.init_irq = mx35_init_irq,
	.init_time = mx35pdk_timer_init,
	.init_machine = mx35_3ds_init,
	.reserve = mx35_3ds_reserve,
	.restart = mxc_restart,
MACHINE_END

根据 MACHINE_START 和 MACHINE_END 的宏定义,将示例代码 43.3.4.2 展开后如下所 示:

cpp 复制代码
static const struct machine_desc __mach_desc_MX35_3DS \
	__used \
	__attribute__((__section__(".arch.info.init"))) = {
	.nr = MACH_TYPE_MX35_3DS, 
	.name = "Freescale MX35PDK",
	/* Maintainer: Freescale Semiconductor, Inc */
	.atag_offset = 0x100,
	.map_io = mx35_map_io,
	.init_early = imx35_init_early,
	.init_irq = mx35_init_irq,
	.init_time = mx35pdk_timer_init,
	.init_machine = mx35_3ds_init,
	.reserve = mx35_3ds_reserve,
	.restart = mxc_restart,
};

这里定义了一个 machine_desc 类型的结构体变量 __mach_desc_MX35_3DS , 这 个 变 量 存 储 在 " .arch.info.init " 段 中 。 第 4 行 的 MACH_TYPE_MX35_3DS 就 是 " Freescale MX35PDK " 这 个 板 子 的 machine id 。 MACH_TYPE_MX35_3DS 定义在文件 include/generated/mach-types.h 中,此文件定义了大量的 machine id ,内容如下所示:

cpp 复制代码
15 #define MACH_TYPE_EBSA110 0
16 #define MACH_TYPE_RISCPC 1
17 #define MACH_TYPE_EBSA285 4
18 #define MACH_TYPE_NETWINDER 5
19 #define MACH_TYPE_CATS 6
20 #define MACH_TYPE_SHARK 15
21 #define MACH_TYPE_BRUTUS 16
22 #define MACH_TYPE_PERSONAL_SERVER 17
......
287 #define MACH_TYPE_MX35_3DS 1645
......
1000 #define MACH_TYPE_PFLA03 4575

第 287 行就是 MACH_TYPE_MX35_3DS 的值,为 1645 。 前面说了, uboot 会给 Linux 内核传递 machine id 这个参数, Linux 内核会检查这个 machine id ,其实就是将 machine id 与示例代码 43.3.4.3 中的这些 MACH_TYPE_XXX 宏进行对比,看 看有没有相等的,如果相等的话就表示 Linux 内核支持这个设备,如果不支持的话那么这个设 备就没法启动 Linux 内核。
2 、使用设备树以后的设备匹配方法
当 Linux 内 核 引 入 设 备 树 以 后 就 不 再 使 用 MACHINE_START 了 , 而 是 换 为 了 DT_MACHINE_START 。 DT_MACHINE_START 也定义在文件 arch/arm/include/asm/mach/arch.h 里面,定义如下:

cpp 复制代码
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
 	.nr = ~0, \
 	.name = _namestr,

在 DT_MACHINE_START 里面直接将 .nr 设置为 ~0 。说明引入设备树以后不会再根据 machine id 来检查 Linux 内核是否支持某个设备了。
打开文件 arch/arm/mach-imx/mach-imx6ul.c ,有如下所示内容:

cpp 复制代码
static const char *imx6ul_dt_compat[] __initconst = {
	"fsl,imx6ul",
	"fsl,imx6ull",
	NULL,
};

DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
	.map_io = imx6ul_map_io,
	.init_irq = imx6ul_init_irq,
	.init_machine = imx6ul_init_machine,
	.init_late = imx6ul_init_late,
	.dt_compat = imx6ul_dt_compat,
MACHINE_END

machine_desc 结构体中有个 .dt_compat 成员变量,此成员变量保存着本设备兼容属性,示 例代码 中设置 .dt_compat = imx6ul_dt_compat , imx6ul_dt_compat 表里面有 "fsl,imx6ul" 和 "fsl,imx6ull" 这两个兼容值。只要某个设备 ( 板子 ) 根节点" / "的 compatible 属性值与 imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。
compatible = "fsl,imx6ull-14x14-evk", " fsl,imx6ull ";
其中"fsl,imx6ull "与 imx6ul_dt_compat 中的" fsl,imx6ull "匹配,因此 I.MX6U-ALPHA 开 发板可以正常启动 Linux 内核。

3.5 向节点追加或修改内容

示例代码 就是 I.MX6ULL 的 I2C1 节点,现在要在 i2c1 节点下创建一个子节点, 这个子节点就是 fxls8471 ,最简单的方法就是在 i2c1 下直接添加一个名为 fxls8471 的子节点, 如下所示:

i2c1 : i2c @021a0000{

#address - cells = < 1>;

#size - cells = < 0>;

compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";

reg = <0x021a0000 0x4000>;

interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;

clocks = <&clks IMX6UL_CLK_I2C1>;

status = "disabled";

//fxls8471 子节点
fxls8471 @1e
{
compatible = "fsl,fxls8471";
reg = <0x1e>;
};

};
但是这样会有个问题!i2c1 节 点是定义在 imx6ull.dtsi 文件中的,而 imx6ull.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL 这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在 其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊!因此,按 照示例代码 这样写肯定是不行的。
这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向 i2c1 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 I.MX6ULL 的板子。 I.MX6U-ALPHA 开发板使用的设备树文件为 imx6ull-alientek-emmc.dts ,因此我们需要在 imx6ull-lyh-emmc.dts 文件中完成数据追加的内容,方式如下:

&i2c1{

clock - frequency = <100000>;

pinctrl - names = "default";

pinctrl - 0 = <&pinctrl_i2c1>;

status = "okay";

mag3110 @0e

{

compatible = "fsl,mag3110";

reg = <0x0e>;

position = <2>;

};

fxls8471 @1e
{
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt - parent = <&gpio5>;
interrupts = <0 8>;
};

};
因为示例代码 中的内容是 imx6ull-lyh-emmc.dts 这个文件内的,所以不会对 使用 I.MX6ULL 这颗 SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点 就是通过 &label 来访问节点,然后直接在里面编写要追加或者修改的内容。

四、设备树在系统中的体现

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的 /proc/device- tree 目录下根据节点名字创建不同文件夹,如图 所示:


1 、根节点" / "各个属性
在上图 中,根节点属性属性表现为一个个的文件 ( 图中细字体文件 ) ,比如图 中 的" #address-cells "、" #size-cells "、" compatible "、" model "和" name "这 5 个文件,它们在设 备树中就是根节点的 5 个属性。既然是文件那么肯定可以查看其内容,输入 cat 命令来查看 model 和 compatible 这两个文件的内容,结果如图 所示:


2 、根节点" / "各子节点
图 中各个文件夹 ( 图中粗字体文件夹 ) 就是根节点" / "的各个子节点,比如" aliases "、
" backlight "、" chosen "和" clocks "等等。大家可以查看一下 imx6ull-lyh-emmc.dts 和 imx6ull.dtsi 这两个文件,看看根节点的子节点都有哪些,看看是否和图 中的一致。

五、特殊节点

在根节点"/ "中有两个特殊的子节点: aliases 和 chosen ,我们接下来看一下这两个特殊的 子节点。

4.1 aliases

打开 imx6ull.dtsi 文件, aliases 节点内容如下所示:

cpp 复制代码
aliases {
	can0 = &flexcan1;
	can1 = &flexcan2;
	ethernet0 = &fec1;
	ethernet1 = &fec2;
	gpio0 = &gpio1;
	gpio1 = &gpio2;
......
	spi0 = &ecspi1;
	spi1 = &ecspi2;
	spi2 = &ecspi3;
	spi3 = &ecspi4;
	usbphy0 = &usbphy1;
	usbphy1 = &usbphy2;
};

单词 aliases 的意思是"别名",因此 aliases 节点的主要功能就是定义别名,定义别名的目 的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label ,然后通过 &label 来访问节点,这样也很方便,而且设备树里面大量的使用 &label 的形式来访问节点。

4.2 chosen

chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重 点是 bootargs 参数。一般 .dts 文件中 chosen 节点通常为空或者内容很少, imx6ull-lyh- emmc.dts 中 chosen 节点内容如下所示:

cpp 复制代码
chosen {
        stdout-path = &uart1;
}; 

chosen 节点仅仅设置了属性" stdout-path ",表示标准输 出使用 uart1 。但是当我们进入到 /proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个 属性,如图 43.6.2.1 所示:


uboot 中的 fdt_chosen 函数在设备树的 chosen 节点中加入了 bootargs 属性,并且还设置了 bootargs 属性值。接下来我们顺着 fdt_chosen 函数一点点的抽丝剥茧,看 看都有哪些函数调用了 fdt_chosen ,一直找到最终的源头。 大致流程:

六、Linux内核解析DTB

Linux 内核在启动的时候会解析 DTB 文件,然后在 /proc/device-tree 目录下生成相应的设备 树节点文件。接下来我们简单分析一下 Linux 内核是如何解析 DTB 文件的,流程如图 所 示:


在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工 作的函数为 unflatten_dt_node 。

七、绑定信息文档

设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属 性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在 Linux 内核源码中有详细的 .txt 文档描述了如何添加节点,这些 .txt 文档叫做绑定文档,路径为: Linux 源码目录 /Documentation/devicetree/bindings
比如我们现在要想在 I.MX6ULL 这颗 SOC 的 I2C 下添加一个节点,那么就可以查看 Documentation/devicetree/bindings/i2c/i2c-imx.txt ,此文档详细的描述了 I.MX 系列的 SOC 如何 在设备树中添加 I2C 设备节点。

八、of操作函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的, 我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存 器地址为 0X02005482 ,长度为 0X400 ,我们在编写驱动的时候需要获取到 reg 属性的 0X02005482 和 0X400 这两个值,然后初始化外设。 Linux 内核给我们提供了一系列的函数来获 取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀" of_ ",所以在很多资 料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。
Linux 内核使用 device_node 结构体来描述一个节点,此结构体定 义在文件 include/linux/of.h 中,定义如下:

cpp 复制代码
struct device_node {
	const char *name; /* 节点名字 */
	const char *type; /* 设备类型 */
	phandle phandle;
	const char *full_name; /* 节点全名 */
	struct fwnode_handle fwnode;

	struct property *properties; /* 属性 */
	struct property *deadprops; /* removed 属性 */
	struct device_node *parent; /* 父节点 */
	struct device_node *child; /* 子节点 */
	struct device_node *sibling;
	struct kobject kobj;
	unsigned long _flags;
	void *data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};

8.1 查找节点

1 of_find_node_by_name 函数
of_find_node_by_name 函数通过节点名字查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
函数参数和返回值含义如下:
from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name :要查找的节点名字。
返回值: 找到的节点,如果为 NULL 表示查找失败。
2 of_find_node_by_type 函数
of_find_node_by_type 函数通过 device_type 属性查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
函数参数和返回值含义如下:
from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type :要查找的节点对应的 type 字符串,也就是 device_type 属性值。
返回值: 找到的节点,如果为 NULL 表示查找失败。
3 of_find_compatible_node 函数
of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点,
函数原型如下:
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
函数参数和返回值含义如下:
from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type :要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL ,表示
忽略掉 device_type 属性。
compatible 要查找的节点所对应的 compatible 属性列表。
返回值: 找到的节点,如果为 NULL 表示查找失败
4 of_find_matching_node_and_match 函数
of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点,函数原
型如下:

struct device_node *of_find_matching_node_and_match(struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match)
函数参数和返回值含义如下:
from :开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
matches : of_device_id 匹配表,也就是在此匹配表里面查找节点。
match 找到的匹配的 of_device_id 。
返回值: 找到的节点,如果为 NULL 表示查找失败
5 of_find_node_by_path 函数
of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:
inline struct device_node *of_find_node_by_path(const char *path)
函数参数和返回值含义如下:
path :带有全路径的节点名,可以使用节点的别名,比如" /backlight "就是 backlight 这个
节点的全路径。
返回值: 找到的节点,如果为 NULL 表示查找失败

8.2 查找父子节点

1 of_get_parent 函数
of_get_parent 函数用于获取指定节点的父节点 ( 如果有父节点的话 ) ,函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
函数参数和返回值含义如下:
node :要查找的父节点的节点。
返回值: 找到的父节点。
2 of_get_next_child 函数
of_get_next_child 函数用迭代的方式查找子节点,函数原型如下:
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
函数参数和返回值含义如下:
node :父节点。
prev :前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为
NULL ,表示从第一个子节点开始。
返回值: 找到的下一个子节点。

8.3 提取属性

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内 核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,内容如下:

cpp 复制代码
struct property {
	char *name; /* 属性名字 */
	int length; /* 属性长度 */
	void *value; /* 属性值 */
	struct property *next; /* 下一个属性 */
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
};

Linux 内核也提供了提取属性值的 OF 函数,我们依次来看一下。
1 of_find_property 函数
of_find_property 函数用于查找指定的属性,函数原型如下:
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
函数参数和返回值含义如下:
np :设备节点。
name : 属性名字。
lenp :属性值的字节数
返回值: 找到的属性。
2 of_property_count_elems_of_size 函数
of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如 reg 属性值是一个
数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
int of_property_count_elems_of_size(const struct device_node *np,
const char
*propname,
int
elem_size)
函数参数和返回值含义如下:
np :设备节点。
proname : 需要统计元素数量的属性名字。
elem_size :元素长度。
返回值: 得到的属性元素数量。
3 of_property_read_u32_index 函数
of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值 ( 无符号 32
位 ) ,比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此
函数原型如下:
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
函数参数和返回值含义如下:
np :设备节点。
proname : 要读取的属性名字。
index :要读取的值标号。
out_value :读取到的值
返回值: 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有
要读取的数据, -EOVERFLOW 表示属性值列表太小。
4 of_property_read_u8_array 函数
of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数
这 4 个函数分别是读取属性中 u8 、 u16 、 u32 和 u64 类型的数组数据,比如大多数的 reg 属
性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。这四个函数的原型
如下:

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_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
函数参数和返回值含义如下:
np :设备节点。
proname : 要读取的属性名字。
out_value :读取到的数组值,分别为 u8 、 u16 、 u32 和 u64 。
sz 要读取的数组元素数量。
返回值: 0 ,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没
有要读取的数据, -EOVERFLOW 表示属性值列表太小。
5 of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用
于读取 u8 、 u16 、 u32 和 u64 类型属性值,函数原型如下:
int of_property_read_u8(const struct device_node *np, const char *propname, u8 *out_value)
int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value)
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value)
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value)
函数参数和返回值含义如下:
np :设备节点。
proname : 要读取的属性名字。
out_value :读取到的数组值。
返回值: 0 ,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没
有要读取的数据, -EOVERFLOW 表示属性值列表太小。
6 of_property_read_string 函数
of_property_read_string 函数用于读取属性中字符串值,函数原型如下:
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
函数参数和返回值含义如下:
np :设备节点。
proname : 要读取的属性名字。
out_string :读取到的字符串值。
返回值: 0 ,读取成功,负值,读取失败。
7 of_n_addr_cells 函数
of_n_addr_cells 函数用于获取 #address-cells 属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
函数参数和返回值含义如下:
np :设备节点。
返回值: 获取到的 #address-cells 属性值。
8 of_n_size_cells 函数
of_size_cells 函数用于获取 #size-cells 属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下:
np :设备节点。
返回值: 获取到的 #size-cells 属性值。

8.4 其他常用函数

1 of_device_is_compatible 函数
of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字
符串,也就是检查设备节点的兼容性,函数原型如下:
int of_device_is_compatible(const struct device_node *device, const char *compat)
函数参数和返回值含义如下:
device :设备节点。
compat :要查看的字符串。
返回值: 0 ,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible
属性中包含 compat 指定的字符串。
2 of_get_address 函数
of_get_address 函数用于获取地址相关属性,主要是" reg "或者" assigned-addresses "属性
值,函数原型如下:
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)
函数参数和返回值含义如下:
dev :设备节点。
index :要读取的地址标号。
size :地址长度。
flags :参数,比如 IORESOURCE_IO 、 IORESOURCE_MEM 等
返回值: 读取到的地址数据首地址,为 NULL 的话表示读取失败。
3 of_translate_address 函数
of_translate_address 函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
函数参数和返回值含义如下:
dev :设备节点。
in_addr :要转换的地址。
返回值: 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
4 of_address_to_resource 函数
IIC、 SPI 、 GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间, Linux 内核使用 resource 结构体来描述一段内存空间," resource "翻译出来就是"资源",因此用 resource 结构体描述的都是设备资源信息, resource 结构体定义在文件 include/linux/ioport.h 中,定义如 下:

cpp 复制代码
struct resource {
	resource_size_t start;
	resource_size_t end;
	const char *name;
	unsigned long flags;
	struct resource *parent, *sibling, *child;
};

对于 32 位的 SOC 来说, resource_size_t 是 u32 类型的。其中 start 表示开始地址, end 表示 结束地址, name 是这个资源的名字, flags 是资源标志位,一般表示资源类型,可选的资源标志 定义在文件 include/linux/ioport.h 中,如下所示:

cpp 复制代码
#define IORESOURCE_BITS 0x000000ff 
#define IORESOURCE_TYPE_BITS 0x00001f00 
#define IORESOURCE_IO 0x00000100 
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300 
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000 
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000 
#define IORESOURCE_STARTALIGN 0x00080000 
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000
......

大 家 一 般 最 常 见 的 资 源 标 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和 IORESOURCE_IRQ 等。接下来我们回到 of_address_to_resource 函数,此函数看名字像是从设 备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型, 函数原型如下所示
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
函数参数和返回值含义如下:
dev :设备节点。
index :地址资源标号。
r :得到的 resource 类型的资源值。
返回值: 0 ,成功;负值,失败。
5 of_iomap 函数
of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地
址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,
不需要使用 ioremap 函数了。当然了,你也可以使用 ioremap 函数来完成物理地址到虚拟地址
的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap 函数了。 of_iomap 函数本
质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参
数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
函数参数和返回值含义如下:
np :设备节点。
index : reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0 。
返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
关于设备树常用的 OF 函数就先讲解到这里, Linux 内核中关于设备树的 OF 函数不仅仅只 有前面讲的这几个,还有很多 OF 函数我们并没有讲解,这些没有讲解的 OF 函数要结合具体 的驱动,比如获取中断号的 OF 函数、获取 GPIO 的 OF 函数等等,这些 OF 函数我们在后面的 驱动实验中再详细的讲解。
关于设备树就讲解到这里,关于设备树我们重点要了解一下几点内容:
①、 DTS 、 DTB 和 DTC 之间的区别,如何将 .dts 文件编译为 .dtb 文件。
②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。
③、设备树的几个特殊子节点。
④、关于设备树的 OF 操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、 GPIO 信息、中断信息等等。要想在 驱动中读取设备树的属性值,那么就必须使用 Linux 内核提供的众多的 OF 函数。

相关推荐
玉树临风江流儿9 分钟前
Linux驱动开发(速记版)--设备模型
linux·驱动开发
杰哥在此23 分钟前
Python知识点:如何使用Multiprocessing进行并行任务管理
linux·开发语言·python·面试·编程
枫叶丹42 小时前
【在Linux世界中追寻伟大的One Piece】进程信号
linux·运维·服务器
刻词梨木3 小时前
ubuntu中挂载点内存不足,分配不合理后使用软链接的注意事项
linux·运维·ubuntu
灯火不休ᝰ3 小时前
[win7] win7系统的下载及在虚拟机中详细安装过程(附有下载文件)
linux·运维·服务器
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
powerfulzyh7 小时前
Ubuntu24.04远程开机
linux·ubuntu·远程工作
ulimpid7 小时前
Command | Ubuntu 个别实用命令记录(新建用户、查看网速等)
linux·ubuntu·command
HHoao7 小时前
Ubuntu启动后第一次需要很久才能启动GTK应用问题
linux·运维·ubuntu
小灰兔的小白兔7 小时前
【Ubuntu】Ubuntu常用命令
linux·运维·ubuntu