当前位置: 首页 > news >正文

Linux驱动学习笔记(七)

设备树

1.Linux内核源码中,之前充斥着大量的平台相关配置(platform_device),而这些代码大多是杂乱且重复的,这使得ARM体系结构的代码维护者和内核维护者在发布一个新的版本的时候有大量的工作要做,为了方便维护和管理引入了设备树的概念(在x86平台上,通常不使用设备树,而是依赖于ACPI(高级配置与电源接口,Advanced Configurationand Power Interface)和其他传统的硬件发现机制)。设备树是描述硬件的文本文件,因为语法结构像树一样,所以被称为设备树。与设备树相关的概念和文件有:

  • DT:Device Tree ,即设备树
  • FDT:Flattened Device Tree ,即开放设备树,起源于OpenFirmware(OF)
  • dts:device tree source的缩写,表示设备树源码
  • dtsi:device tree source include的缩写,表示通用的设备树源码
  • dtb:device tree blob的缩写,是编译设备树源码得到的文件
  • dtc:device tree compiler的缩写,指的是设备树编译器

dts、dtsi经过dtc编译之后即可得到dtb。设备树源码dts和dtsi一般位于内核源码目录下的/arch/$(ARCH)/boot/dts/目录下。当dts文件中通过#include包含了C语言的.h文件时,dtc是无法直接编译处理的,需要先用GCC处理再用dtc编译,否则会报错。

2.内核源码顶层Makefile中的CONFIG_DTC被设置成y时,dtc编译器会在内核编译完成之后被生成,存放在内核源码目录下的/scripts/dtc/目录下(dtc编译器也是通过该目录下的源文件编译生成的)。dtc -I dts -O dtb -o xxx.dtb xxx.dts命令用于编译设备树,-I指明源文件类型,-O指明目标文件类型,-o指出目标文件名,这个命令从dts源文件编译生成dtb文件;dtc -I dtb -O dts -o xxx.dts xxx.dtb命令用于反编译设备树,这个命令从dtb文件生成dts文件。在内核源码目录下执行make dtbs可编译/arch/$(ARCH)/boot/dts/目录下的设备树源文件。

3.设备树基础语法:

  • 根节点是设备树必须要包含的节点,根节点的名字是/。另外设备树的第一行必须标明版本信息。设备树的注释和C语言一样,可使用//或/* */。
  • 设备树子节点格式为:

    [label:]node-name[@unit-address]{

           [properties definitions]

           [child nodes]

    };

    注意,同级节点下的节点名称不能相同,不同级节点名称可以相同。在对节点进行命名时,一般要体现设备的类型,比如网口一般命名成ethernet,串口一般命名成uart,对于名称一般要遵循下面的命令格式:[标签]:<名称>[@<设备地址>],其中[标签]和[@<设备地址>]是可选项,<名称>是必选项,另外这里的<设备地址>没有实际意义,只是让节点名称更人性化,更方便阅读。[标签]可以看成是设备节点的一个别名。
  • reg属性可以来描述地址信息,比如寄存器的地址。reg属性的格式为:reg =<addressl lengthl address2 length2 address3 length3…>,reg属性中地址和长度的具体数量由上一级中的#address-cells和#size-cells属性指出(即由当前层级的{}之外的相邻层级中的这两个属性值指出)。当使用reg属性时节点名称必须给出[@<设备地址>],否则编译会产生警告。
  • model属性的值是一个字符串,一般用model描述一些信息,比如设备的名称,名字等。
  • status属性是和设备的状态有关系的,status的属性值是字符串。属性值有下面几个状态可选:okay表示设备是可用状态;disabled表示设备是不可用状态;fail表示设备是不可用状态并且设备检测到了错误;fail-sss表示设备是不可用状态并且设备检测到了错误,sss是错误内容。
  • compatible属性用来和驱动进行匹配,匹配成功以后会执行驱动中的probe函数。例如:compatible ="xunwei","xunwei-board”;,在匹配的时候会先使用第一个值xunwei进行匹配,如果没有就会使用第二个值xunwei-board进行匹配。
  • 除了在对节点命名时添加标签来命名别名,还可以使用特殊节点aliases用来定义别名,定义别名的目的就是为了方便引用节点。在aliases中定义别名可以使用:别名=&节点名;别名=“节点绝对路径”两种定义方式。在使用了aliases节点的dts文件经过编译再反编译重新得到的dts文件中,aliases节点中的别名定义方式会统一通过绝对路径的定义方式呈现。
  • chosen节点用来帮助uboot给内核传递参数,例如传递bootargs参数,chosen节点必须是根节点的子节点。
  • 在某些设备树文件中,可以看到device_type属性,device_type属性的值是字符串,只能用于对cpu节点或memory节点进行描述。
  • 设备树中规定的属性有时候并不能满足我们的需求,这时候可以自定义属性。

设备树源码示例如下,设备树源码中的每个语句后面需要加;,具体参考讯为Linux驱动视频第七期P5、P6:

4.在设备树源码的中断控制器节点中,必须有一个属性#interrupt-cells,表示其他节点如果使用这个中断控制器需要几个数据来表示使用哪一个中断;在中断控制器中,必须有一个属性interrupt-controller,表示他是中断控制器;在设备树中使用中断,需要使用属性interrupt-parent=<&XXXX>表是中断信号链接的是哪个中断控制器,接着使用interrupts属性来表示中断引脚和触发方式。interrupts里有几个数据(cell),是由interrupt-parent对应的中断控制器里面的#interrupt-cells属性决定。下图是RK处理器的一个例子:

还有其他一些语法用来定义中断控制器节点,如下图:

ft5x@38节点中并没有interrupt-parent=<&XXXX>来指明使用哪个中断控制器,此时它会直接使用父节点中指明的中断控制器节点。下图展示了中断控制器的级联:

下图使用interrupt-extended属性来表示多组中断。

5.绝大部分的外设工作都需要时钟,时钟一般以时钟树的形式呈现。在arm平台中可以使用设备树来描述时钟树,如时钟的结构,时钟的属性等。再由驱动来解析设备树中时钟树的信息,从而完成时钟的初始化和使用。在设备树中,时钟分为消费者(时钟被谁使用)和生产者(时钟由谁产生)。生产者的属性:#clock-cells属性代表时钟输出的路数,当#clock-cells值为0时,代表仅有1路时钟输出,当#clock-cells值大于等于1时,代表输出多路时钟;clock-output-names属性定义了输出时钟的名字;clock-frequency属性可以指定时钟频率的大小;assigned-clocks和assigned-clock-rates一般成对使用,当输出多路时钟时,assigned-clocks指明每个时钟所使用的时钟源,assigned-clock-rates指明所使用的每个时钟源的频率;clock-indices属性可以指定索引号(index),如果不提供这个属性,那么clock-output-names和index的对应关系就是0,1,2,…,如果这个对应关系不是线性的,那么可以通过clock-indices属性来定义映射到clock-output-names的index;assigned-clock-parents属性可以用来设置时钟源的父时钟。消费者属性:clocks属性和clock-names属性用来指定使用的时钟源和消费者中时钟的名字。示例如下:

6.在设备树中,cpu层次结构通过不同的节点来描述系统中物理cpu的布局。主要包括以下几类节点:cpus节点里面包含物理cpu的布局,也就是所有cpu的布局全部在此节点下描述;cpu-map节点主要用在描述大小核架构处理器中,描述单核处理器不需要使用cpu-map节点。cpu-map的节点名称必须是cpu-map,cpu-map节点的父节点必须是cpus节点,子节点必须是一个或者多个的cluster和socket节点;socket节点描述的是主板上的CPU插槽,主板上有几个CPU插槽,就有几个socket节点。socket节点的子节点必须是一个或者多个c1uster节点。当有多个CPU插槽时,socket节点的命名方式必须是socketN,N=0,1,2,…;cluster节点用来描述CPU的集群,比如RK3399的架构是双核A72+四核A53,双核A72是一个集群,用一个cluster节点来描述,四核A53也是一个集群,用一个cluster节点来描述。cluster节点的命名方式必须是clusterN,N=0,1,2,…,cluster节点的子节点必须是一个或多个cluster节点或者一个或多个core节点;core节点用来描述一个cpu,如果是单核cpu,则core节点就是cpus节点的子节点。core节点的命名方式必须是coreN,N=0,1,2,…,core节点的子节点必须是一个或者多个thread节点;thread节点用来描述处理的的线程(超线程技术),thread节点的命名方式必须是threadN,N=0,1,2,…。示例如下:

7.在GPIO控制器中,必须有一个属性#gpio-cells,表示其他节点如果使用这个GPIO控制器需要几个数据(cell)来表示使用哪一个GPIO;在GPIO控制器中,必须有一个属性gpio-controller,表示他是GPIO控制器;在设备树中使用GPIO,需要使用属性例如data-gpios=<&gpio1 12 0>来指定具体的GPIO引脚。data-gpios属性可以为自定义属性(如下图中的reset-gpios、touch-gpio)。示例如下:

GPIO其他的属性:ngpios表示可用的GPIO数量,下图中的gpio-line-names指出了可用的GPIO,下图中gpio-reserved-ranges指出0、1、2、3四个GPIO和12、13这两个GPIO是保留的:

下图中的gpio-ranges用于建立映射关系:

8.BGA封装是一种表面贴装技术,其特点是在封装底部形成一个规则的球状引脚阵列。 这种封装方式具有引脚间距大、热性能好、信号传输性能优越等优点,因此在高速、高性能的集成电路上得到了广泛应用。引脚的定位通过行列坐标来表示,如下图中列坐标为1-28,行坐标为A-AH:

CPU 中有很多未使用的引脚和电源/地引脚,分布更多的电源/地引脚可以减小每个引脚的负担,避免过热和电源不稳定的问题。Pinmux(即PinMultiplexing的缩写)是指 引脚复用,用于配置芯片上各个引脚以支持不同的功能或接口。Pinmux 可在一个硬件引脚上根据需要选择其不同的功能模式,从而使得同一个引脚可以根据不同需求,连接到不同的外设或模块。如下图中的引脚共有fun0-fun7八种功能,但同一时刻只能使用其中一种功能:

Linux内核提供了pinctrl子系统,pinctrl是pincontroller的缩写,目的是为了统一各芯片原厂的pin脚管理。pinctrl的语法可以看作是由两个部分构成,一部分是客户端,另一部分是服务端,客户端的语法格式是固定的,服务端的语法格式是由各个芯片自己决定的。客户端语法如下图:

使用pinctrl-names表示设备的状态,状态可以有多个,用逗号隔开,且这些状态被一次默认排序为0,1,2,…,pinctrl-0表示第0个状态对应的引脚配置在&后面的节点中配置,pinctrl-1表示第1个状态对应的引脚配置在&后面的节点中配置,依此类推,配置信息可以放在多个节点中。服务器端如恩智浦的语法如下图:

再如服务器端瑞芯微的语法如下图(可参考讯为Linux驱动视频第七期P14):

一般设备树代码的编写可以直接参考对应内核源码中的例子,如果没有参考节点(概率不大),可以查看kernel/Documentation/devicetree/bindings/下的文档(例如对于瑞芯微的pinctrl节点的设备树语法可以参考该目录下的/pinctrl/rockchip,pinctrl.txt文档)。

9.dtb文件格式主要分为四个部分:small header(头部)、memory reservation block(内存预留块)、structure block(结构块)、strings block(字符串块),另外还有free space(自由空间)用于对齐但不一定存在。示意图如下:

其中fdt_header结构体如下图所示:

magic是魔数,必须为0xd00dfeed,dtb文件是大端模式的;totalsize表示dtb文件的大小;off_dt_struct表示结构块在dtb文件中的偏移量;off_dt_strings表示字符串块在dtb文件中的偏移量;off_mem_rsvmap表示内存预留块在dtb文件中的偏移地址;version表示设备树数据结构的版本,即在dts文件中定义的版本;last_comp_version表示向后兼容的版本;boot_cpuid _phys的值应与设备树文件dts中CPU节点下的reg属性值相等;size_dt_strings表示字符串块的大小;size_dt_struct表示结构块的大小。如果在dts文件中使用了memreserve属性描述保留的内存(语法为:/memreserve/ <address> <length>,例如:/memreserve/ 0x10000000 0x4000),保留的内存大小信息会保存在内存预留块这部分,该块格式如下图:

结构块描述的是设备树的结构,也就是设备树的节点,使用0x00000001表示节点的开始,然后跟上节点名字(根节点是的名字用0表示),然后使用0x00000003表示一个属性的开始(每表示一个节点,都要用0x00000003表示开始)属性的名字和值用结构体表示,该结构体有两个成员,分别为len表示属性值长度(属性具体值直接跟在这个结构体之后存储),nameoff表示属性名在字符串块的偏移量。使用0x00000002表示节点的结束,使用0x00000009表示根节点的结束,也就是整个结构块的结束。字符串块用来连续存放属性的名字(可参考讯为Linux驱动视频第七期P16和dtb官方手册devicetree-specification-changebars-v0.2)。

10.设备树源文件经过编译之后成为二进制的dtb文件,然后dtb文件会和内核镜像一起被打包成带有dtb的内核镜像(例如在RK3568中会被打包在一起,但也有的芯片不将他们一起打包),然后uboot将内核和设备树加载到内存的某个地址上,并通过寄存器告诉内核设备树的加载地址,而后设备树被展开为device_node,如下图:

device node结构体定义在内核源码目录下的include/linux/of.h文件当中,如下图:

其中child指针指向当前设备节点的第一个子节点,properties指针指向当前设备节点下的第一个属性,其他属性与该属性链表相连,properties也在include/linux/of.h文件中定义,如下图:

11.内核源码目录下的文件/init/main.c文件中的start_kernel函数是内核启动阶段的入口,在执行start_kernel函数之前已经通过汇编代码完成了一些初始化,这个函数类似于一般C程序中的main函数。在start_kernel函数中有很多的子函数,这些函数都是完成Linux内核初始化的函数,其中setup_arch(&command_line)函数(不同架构有不同定义,如该函数其中一个定义路径为/arch/arm64/kernel/setup.c)用于完成dtb文件到device_node的转换。如下图:

/arch/arm64/kernel/setup.c中setup_arch函数的定义如下图:

其中*cmdline_p = boot_command_line;中的boot_command_line是一个数组,记录了uboot传递给内核的command_line,Linux中定义其大小为4096,如果uboot传递给内核的command_line的大小超过4096,则要修改这个数组。函数setup_machine_fdt(__fdt_pointer);中的参数__fdt_pointer是dtb位于内存中的地址(这个dtb的地址在/arch/arm64/kernel/head.S文件中通过汇编指令从寄存器x0传递给了__fdt_pointer,x0里面存放dtb地址是规定好的)。setup_machine_fdt函数定义在/arch/arm64/kernel/setup.c中,它会读取dtb内容,内核执行到setup_machine_fdt函数时mmu已经打开了,所以要进行虚拟地址到物理地址的映射,如下图:

在unflatten_device_tree函数中会调用__unflatten_device_tree函数,__unflatten_device_tree函数定义如下图:

__unflatten_device_tree函数通过两次调用unflatten_dt_nodes完成两次扫描,第一次扫描统计设备树转换为device_node所需要的内存大小(传入的第二个参数NULL),然后分配所需的内存,第二次扫描完成所有device_node的构建(传入的第二个参数为分配的内存地址)。整个dtb转换为device_node过程中的主要函数调用如下图(可参考讯为Linux驱动视频第七期P18):

12.设备树替换了平台总线模型当中对硬件资源描述的device部分,即platform_device部分,内核最终会将内核认识的device_node树转换platform_device。但是并不是所有的节点都会被转换成platform_device,只有满足要求的才会转换成platform_device,转换成platform_device的节点可以在/sys/bus/platform/devices下查看,该目录下的每个子目录对应一个设备节点,每个设备节点对应的子目录下还有一个of_node子目录,of_node子目录中的几个文件存储了设备节点的name、compatible等信息,如下图是leds设备节点对应的子目录:

被转换成platform_device的设备节点应满足一下要求:

  • 根节点下包含compatible属性的一级子节点(直接子节点)会被转换成platform_device
  • 如果节点中的compatible属性包含simple-bus、simple-mfd、isa其中之一,那么该节点下包含compatible属性的子节点会被转换成platform_device
  • 如果节点的compatible属性包含”arm,primecell”值,则对应的节点会被转换成amba设备而不会被转换成platform_device
  • 会转换成platform_device的节点,必须含有compatible属性,根节点不会被转换成platfrom_device

of_platform_default_populate_init函数定义在/drivers/of/platform.c中,如下图:

Linux通过of_platform_default_populate_init函数实现从device_node到platform_device的转换,在内核启动的时候会执行该函数,无法找到该函数被调用的语句,是因为这个函数被arch_initcall_sync修饰:arch_initcall_sync(of_platform_default_populate_init);,arch_initcall_sync是一个宏定义:#define arch_initcall_sync(fn)     __define_initcall(fn,3s),参考第一期驱动在内核启动时被调用的过程可知of_platform_default_populate_init函数会在内核启动时被调用。通过一系列调用of_platform_default_populate_init函数->of_platform_default_populate函数->of_platform_bus_create函数(这个函数会被自己递归调用)->of_platform_device_create_pdata函数->of_device_alloc函数(在这个函数中根据device_node中设备节点的属性来设置plat_device中的resource),实现device_node到platform_device的转换(可参考讯为Linux驱动视频第七期P20)。

13.设备树中只能有一个根节点,如果在一个文件中有根节点,该文件include了另一个包含根节点的文件,那么在dtc编译设备树源文件时根节点会被合并。在自己向设备树源码中添加设备节点时例如一个led可以像下面这样添加代码:

添加两层节点是因为防止#address-cells和#size-cells影响根节点下面的其他节点,使用compatible=”simple-bus”是为了保证myled节点被转换成platform_device。在没有使用设备树而只使用了平台总线架构时,platform_device和platform_driver之间通过struct platform_driver中的id_table和driver.name和struct platform_device中的name进行匹配(id_table优先级更高,可参考第六期笔记),platform_driver结构体定义如下:

在使用设备树之后,通过platform_driver中的driver.of_match_table和struct platform_device中的name进行匹配,此时匹配的优先级为:driver.of_match_table>id_table>driver.name。如果设备树源码中的设备节点被成功加载,则可以在目录/proc/device-tree/下看到对应的设备节点文件(根节点的直接子节点)。

14.Linux提供了一组of函数来获取设备节点的信息和设备节点中属性的信息,以下函数声明在内核源码目录下的/include/linux/of.h文件中:

  • struct device_node *of_find_node_by_name(struct device _node *from, const char *name):该函数可通过节点的名字来查找指定节点,查找成功返回指向目标节点的struct device_node指针,失败返回NULL。其中from指出从哪个节点开始查找,传入NULL则从根节点开始查找;name是要查找的节点的名字
  • struct device_node *of_find_node_by_path(const char *path):该函数可通过节点的绝对路径(从根节点开始)来查找指定节点,查找成功返回指向目标节点的struct device_node指针,失败返回NULL。其中path即为其绝对路径的字符串
  • struct device_node *of_get__parent(const struct device_node *node):该函数用于获取指定节点的父节点,查找成功返回指向父节点的struct device_node指针,失败返回NULL
  • struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev):该函数用于查找下一个子节点,查找成功返回指向目标节点的struct device_node指针,失败返回NULL。其中node为想要查找的子节点的父节点;prev指出从哪一个子节点开始查找,传入NULL表示查找第一个子节点
  • struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat):该函数通过device_type和compatible属性来查找指定的节点,查找成功返回指向目标节点的struct device_node指针,失败返回NULL。其中from指出从哪个节点开始查找,传入NULL则从根节点开始查找;type指出要查找的节点的device_type属性,传入NULL表示忽略device_type属性;compat指出要查找的节点的compatible列表
  • static inline 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):该函数通过compatible属性列表来查找指定的节点,查找成功返回指向目标节点的struct device_node指针,失败返回NULL。其中from指出从哪个节点开始查找,传入NULL则从根节点开始查找;matches是of_device_id匹配表,从这个匹配表里面查找节点;match用于存储查找到的of_device_id
  • struct property *of_find_property(const struct device_node *np, const char *name, int *lenp):该函数用于查找指定节点np的属性。查找成功返回指向节点属性的struct property指针,失败返回NULL。其中name为要查找的属性的名字;lenp用于存储查找到的属性的长度
  • int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size):该函数用于获取节点np中某个属性中元素的数量。查找成功返回等到元素的数量。其中propname为要查找的属性的名字;elem_size为该属性中每个元素的大小
  • static inline int of_property_read_u64_index(const struct device_node *np, const char *propname, u32 index, u64 *out_value):该函数从节点np中指定的属性中获取指定标号的u64类型的数据值。成功返回0。其中propname为要查找的属性的名字;index为索引号,从0开始;out_value用于存储读取到的值(同样的函数还有of_property_read_u32_index,二者区别只是读取的数据大小不一样)
  • int of_property_read_variable_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz_min, size_t sz_max):该函数用于从节点np中指定的属性中获取u64类型的数组值。成功返回0。其中propname为要查找的属性的名字;out_value用于存储读取到的数组值;sz_min为读取的最少数组成员数量;sz_max为读取的最多数组成员数量(这样的函数是一组,用于读取不同成员大小的数组值,可将u64替换为u8、u16、u32)
  • static inline int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string):该函数用于从节点np中指定的属性中获取字符串值。成功返回0。其中propname为要查找的属性的名字;out_string用来存储读取到的字符串值

下面这组irq_of函数用来获取设备节点属性中interrupts属性相关的信息,即中断相关的信息,他们大多数定义在内核源码目录下的/include/linux/of_irq.h中:

  • unsigned int irq_of_parse_and_map(struct device_node *dev, int index):该函数从设备节点dev获取索引号为index的中断的中断号,获取成功返回对应的中断号
  • struct irq_data *irq_get_irq_data(unsigned int irq):函数根据中断号irq获取对应的irq_data信息,成功返回指向struct irq_data的指针
  • u32 irqd_get_trigger_type(struct irq_data *d)函数根据传入的struct irq_data指针d获取中断的类型,例如上升沿还是下降沿触发,成功返回对应的中断类型标志
  • int gpio_to_irq(unsigned int gpio):该函数根据传入的gpio编号获取对应的中断号,该函数定义在/include/linux /gpio.h
  • int of_irq_get(struct device_node *dev, int index):该函数用于获取设备节点dev中索引号为index的中断的中断号,获取成功返回对应的中断号
  • int platform_get_irq(struct platform_device *dev, unsigned int_num):该函数用于获取设备节点dev中索引号为int_num的中断的中断号,获取成功返回对应的中断号,该函数声明在/include/linux /platform_device.h,一般用于平台总线架构,在设备树架构中也可以使用

因为设备树中的设备节点会转换为device_node再转换为platform_device,所以在使用设备树时,依然可以通过platform_get_resource函数(参考第六期笔记)来获取设备节点的信息,但是在下面的代码中,若没有添加ranges;这句代码platform_get_resource函数会调用失败(Linux内核在通过device_node构建platform_device时,会将platform_device结构体中的num_resources赋值为num_irq+num_reg,而当父节点没有ranges属性时子节点地址转换失败num_reg为0不会被增加,所以此时platform_get_resource函数会返回NULL,具体可参考讯为Linux驱动视频第七期P24):

ranges属性提供了子节点地址空间和父地址空间的映射(转换)方法,该属性有两种格式:

  • ranges=<child-bus-address parent-bus-address length>;:ranges 的值不为空,按照映射规则进行映射。其中child-bus-address是子地址物理空间的起始地址,由ranges所在节点“#address-cells”属性决定该地址所占的字长;parent-bus-address是父地址物理空间的起始地址,由ranges的父节点“#address-cells”属性决定该地址所占的字长;length是映射的大小,由ranges的父节点“#size-cells”属性决定该地址所占的字长。下图是一个例子:

  • ranges;:ranges的值为空,代表是 1:1映射,是内存区域

ranges的使用是因为可以把设备分为内存映射型设备和非内存映射型设备,内存映射型设备是指通过内存地址空间与处理器进行交互的设备,这些设备的寄存器或控制区域直接映射到系统的内存地址空间,当 CPU 或其他组件需要与这些设备通信时,它们直接通过内存地址读取或写入数据。非内存映射型设备则不直接映射到系统的内存地址空间。若某个节点的父节点具有ranges属性,则该节点时内存映射型设备,CPU可以直接访问该设备,否则该设备只能被其父节点的设备访问(具体可参考讯为Linux驱动视频第七期P25)。

相关文章:

  • IDEA加载项目时依赖无法更新
  • Visual Studio 2022 QT5.14.2 新建项目无法打开QT的ui文件,出现闪退情况
  • Headscale-Admin-Pro
  • Mysql 概念
  • 如何在大型项目中组织和管理 Vue 3 Hooks?
  • 如何让 -webkit-slider-thumb 生效
  • 火语言RPA--Sqlite-执行SQL
  • DAPP实战篇:规划下我们的开发线路
  • Jupyter notebook定制字体
  • 2025-04-06 Unity Editor 实践 1 —— Editor 窗体框架
  • 1-linux的基础知识
  • 「精华版」Doris VS Elasticsearch全方位对比和落地实践指导
  • Redis 连接:深入解析与优化实践
  • C++中的堆和栈
  • LabVIEW 长期项目开发
  • 蓝桥杯嵌入式第十四届模拟二(PWM、USART)
  • 云服务器实战:用 Nginx 搭建高性能 API 网关与反向代理服务(附完整配置流程)
  • 整数编码 - 华为OD统一考试(A卷、Java)
  • 【PFPGA学习】状态机思想编程HDLbitsFPGA练习
  • Go语言的测试框架
  • 国外网站代做/策划营销
  • 石家庄城市建设投资中心网站/seo网站优化知识
  • 推荐家居网站建设/培训机构招生7个方法
  • 完成网站建设成本/聚名网
  • 域名注册好了如何做网站/网络营销做得比较成功的企业
  • wordpress linux 重装/企业网站排名优化公司