linux设备树-linux内核对设备树的处理
----------------------------------------------------------------------------------------------------------------------------
内核版本:linux 5.2.8
根文件系统:busybox 1.25.0
u-boot:2016.05
----------------------------------------------------------------------------------------------------------------------------
前面几节内容我们介绍了设备树的结构,以及在linux内核中移植设备树。这一节将对linux内核源码进行介绍,分析内核对设备的处理逻辑。
一、汇编阶段
1.1 uboot引导内核启动
在linux设备树-linux内核设备树移植(一)中我们介绍了uboot引导linux内核的启动流程。uboot会为内核设置启动参数,最终并跳转到内核地址、启动内核。
uboot在启动内核时会设置三个参数,这三个参数会依次赋值给寄存器r0、r1、r2:
- r0一般设置为0;
- r1一般设置为machine id,在使用设备树的时候,这个参数就没有意义了;
- r2一般设置为tag的开始地址,或者设置为dtb文件的开始地址;
当我们使用了设备树的时候,我们主要关注r2参数即可。
1.2 head.S
内核启动,首先执行的是arch/arm/kernel/head.S处的汇编代码,内核head.S主要做了以下工作:
- 执行子程序__lookup_processor_type,使用汇编指令读取CPU ID, 根据该ID找到对应的proc_info_list结构体(里面含有这类CPU的初始化函数、信息);
- 执行子程序__vet_atags,判断是否存在可用的tag或dtb;
- 执行子程序__create_page_tables:创建页表, 即创建虚拟地址和物理地址的映射关系;
- 执行子程序__enable_mmu:使能MMU, 以后就要使用虚拟地址了;
- 执行子程序__mmap_switched:__enable_mmu内部会调转到这里执行:
__mmap_switched子程序定义在是arch/arm/kernel/head-common.S文件中,该段子程序会调用C函数start_kernel。
以上的内核大概了解一下即可,这里不去分析具体源码了。head.S和head-common.S最终效果:
- 把uboot传来的r1值,赋给了C变量 __machine_arch_type;
- 把uboot传来的r2值,赋给了C变量__atags_pointer;
- 然后执行C函数start_kernel。
二、内核启动
start_kernel函数位于init/main.c文件:
asmlinkage __visible void __init start_kernel(void) { char *command_line; char *after_dashes; set_task_stack_end_magic(&init_task); smp_setup_processor_id(); debug_objects_early_init(); cgroup_init_early(); local_irq_disable(); early_boot_irqs_disabled = true; /* * Interrupts are still disabled. Do necessary setups, then * enable them. */ boot_cpu_init(); page_address_init(); pr_notice("%s", linux_banner); setup_arch(&command_line); mm_init_cpumask(&init_mm); setup_command_line(command_line); setup_nr_cpu_ids(); setup_per_cpu_areas(); smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */ boot_cpu_hotplug_init(); build_all_zonelists(NULL); page_alloc_init(); pr_notice("Kernel command line: %s\n", boot_command_line); /* parameters may set static keys */ jump_label_init(); parse_early_param(); after_dashes = parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, -1, -1, NULL, &unknown_bootoption); if (!IS_ERR_OR_NULL(after_dashes)) parse_args("Setting init args", after_dashes, NULL, 0, -1, -1, NULL, set_init_arg); /* * These use large bootmem allocations and must precede * kmem_cache_init() */ setup_log_buf(0); vfs_caches_init_early(); sort_main_extable(); trap_init(); mm_init(); ftrace_init(); /* trace_printk can be enabled here */ early_trace_init(); /* * Set up the scheduler prior starting any interrupts (such as the * timer interrupt). Full topology setup happens at smp_init() * time - but meanwhile we still have a functioning scheduler. */ sched_init(); /* * Disable preemption - early bootup scheduling is extremely * fragile until we cpu_idle() for the first time. */ preempt_disable(); if (WARN(!irqs_disabled(), "Interrupts were enabled *very* early, fixing it\n")) local_irq_disable(); radix_tree_init(); /* * Set up housekeeping before setting up workqueues to allow the unbound * workqueue to take non-housekeeping into account. */ housekeeping_init(); /* * Allow workqueue creation and work item queueing/cancelling * early. Work item execution depends on kthreads and starts after * workqueue_init(). */ workqueue_init_early(); rcu_init(); /* Trace events are available after this */ trace_init(); if (initcall_debug) initcall_debug_enable(); context_tracking_init(); /* init some links before init_ISA_irqs() */ early_irq_init(); init_IRQ(); tick_init(); rcu_init_nohz(); init_timers(); hrtimers_init(); softirq_init(); timekeeping_init(); /* * For best initial stack canary entropy, prepare it after: * - setup_arch() for any UEFI RNG entropy and boot cmdline access * - timekeeping_init() for ktime entropy used in rand_initialize() * - rand_initialize() to get any arch-specific entropy like RDRAND * - add_latent_entropy() to get any latent entropy * - adding command line entropy */ rand_initialize(); add_latent_entropy(); add_device_randomness(command_line, strlen(command_line)); boot_init_stack_canary(); time_init(); printk_safe_init(); perf_event_init(); profile_init(); call_function_init(); WARN(!irqs_disabled(), "Interrupts were enabled early\n"); early_boot_irqs_disabled = false; local_irq_enable(); kmem_cache_init_late(); /* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init(); if (panic_later) panic("Too many boot %s vars at `%s'", panic_later, panic_param); lockdep_init(); /* * Need to run this when irqs are enabled, because it wants * to self-test [hard/soft]-irqs on/off lock inversion bugs * too: */ locking_selftest(); /* * This needs to be called before any devices perform DMA * operations that might use the SWIOTLB bounce buffers. It will * mark the bounce buffers as decrypted so that their usage will * not cause "plain-text" data to be decrypted when accessed. */ mem_encrypt_init(); #ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok && page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) { pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n", page_to_pfn(virt_to_page((void *)initrd_start)), min_low_pfn); initrd_start = 0; } #endif kmemleak_init(); setup_per_cpu_pageset(); numa_policy_init(); acpi_early_init(); if (late_time_init) late_time_init(); sched_clock_init(); calibrate_delay(); pid_idr_init(); anon_vma_init(); #ifdef CONFIG_X86 if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_enter_virtual_mode(); #endif thread_stack_cache_init(); cred_init(); fork_init(); proc_caches_init(); uts_ns_init(); buffer_init(); key_init(); security_init(); dbg_late_init(); vfs_caches_init(); pagecache_init(); signals_init(); seq_file_init(); proc_root_init(); nsfs_init(); cpuset_init(); cgroup_init(); taskstats_init_early(); delayacct_init(); poking_init(); check_bugs(); acpi_subsystem_init(); arch_post_acpi_subsys_init(); sfi_init_late(); /* Do the rest non-__init'ed, we're now alive */ arch_call_rest_init(); }
可以看出,start_kernel函数是整个Linux内核启动过程中的一个关键函数,它负责调用其他函数来逐步初始化各个子系统,最终启动用户空间。
start_kernel的调用过程如下:
2.1 setup_arch
start_kernel内部调用setup_arch函数,用于在内核启动期间对硬件进行初始化,包括设置 CPU、内存、设备树等等。该函数的实现是针对特定架构的,因此不同的架构会有不同的 setup_arch实现。在内核启动期间,setup_arch函数是被显式调用的,它的返回值是 0 或者一个错误码,用于指示初始化是否成功。
setup_arch(&command_line); // arch/arm/kernel/setup.c mdesc = setup_machine_fdt(__atags_pointer); // arch/arm/kernel/devtree.c early_init_dt_verify(phys_to_virt(dt_phys) // 判断是否有效的dtb文件, drivers/of/fdt.c initial_boot_params = params; mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); // 找到最匹配的machine_desc, drivers/of/fdt.c early_init_dt_scan_nodes(); machine_desc = mdesc; ......
2.1.1 DT_MACHINE_START
一个编译成uImage的内核镜像文件,可以支持多个单板,比如支持SMDK2440、SMDK2443、以及我们在linux设备树-linux内核设备树移植中加入的MINI2440 using devicetree。
这些板子的配置稍有不同,需要做一些单独的初始化,在内核里面,对于这些单板,都构造了一个machine_desc结构体。
比如我们新增的arch/arm/mach-s3c24xx/mach-smdk2440-dt.c文件:
static const char *const s3c2440_dt_compat[] __initconst = { "samsung,s3c2440", "samsung,mini2440", NULL }; DT_MACHINE_START(S3C2440_DT, "Samsung S3C2440 (Flattened Device Tree)") /* Maintainer: Heiko Stuebner <heiko@sntech.de> */ .dt_compat = s3c2440_dt_compat, .map_io = s3c2440_dt_map_io, .init_irq = irqchip_init, .init_machine = s3c2440_dt_machine_init, MACHINE_END
宏DT_MACHINE_START定义在arch/arm/include/asm/mach/arch.h,如下:
#define DT_MACHINE_START(_name, _namestr) \ static const struct machine_desc __mach_desc_##_name \ __used \ __attribute__((__section__(".arch.info.init"))) = { \ .nr = ~0, \ .name = _namestr, #endif
这里attribute((section(“.arch.info.init”)))就是利用了编译器的特性,把machine_desc放到了.arch.info.init段。
2.1.2 of_flat_dt_match_machine
当我们的uboot不使用tag传参数,而使用dtb文件时,那么这时内核是如何选择对应的machine_desc呢?
在设备树文件的根节点里,有如下两行:
model = "Samsung S3C2440 SoC"; compatible = "samsung,s3c2440","samsung,mini2440";
这里的compatible属性声明想要什么machine_desc,属性值可以是一系列字符串,依次与machine_desc匹配。
内核最好支持samsung,s3c2440,如果不支持,再尝试是否支持samsung,mini2440。
of_flat_dt_match_machine函数就是通过遍历所有的machine_desc,找到与设备树中compatible最匹配的一个machine_desc,如果没有找到匹配的机器,则返回默认的机器指针;函数定义在drivers/of/fdt.c中;
/** * of_flat_dt_match_machine - Iterate match tables to find matching machine. * * @default_match: A machine specific ptr to return in case of no match. * @get_next_compat: callback function to return next compatible match table. * * Iterate through machine match tables to find the best match for the machine * compatible string in the FDT. */ const void * __init of_flat_dt_match_machine(const void *default_match, const void * (*get_next_compat)(const char * const**)) { const void *data = NULL; const void *best_data = default_match; const char *const *compat; unsigned long dt_root; unsigned int best_score = ~1, score = 0; dt_root = of_get_flat_dt_root(); while ((data = get_next_compat(&compat))) { score = of_flat_dt_match(dt_root, compat); if (score > 0 && score < best_score) { best_data = data; best_score = score; } } if (!best_data) { const char *prop; int size; pr_err("\n unrecognized device tree list:\n[ "); prop = of_get_flat_dt_prop(dt_root, "compatible", &size); if (prop) { while (size > 0) { printk("'%s' ", prop); size -= strlen(prop) + 1; prop += strlen(prop) + 1; } } printk("]\n\n"); return NULL; } pr_info("Machine model: %s\n", of_flat_dt_get_machine_name()); return best_data; }
总结如下:
- 设备树根节点的compatible属性列出了一系列的字符串,表示它兼容的单板名,从"最兼容"到次之;
- 内核中有多个machine_desc,其中有dt_compat成员,,它指向一个字符串数组,,里面表示该machine_desc支持哪些单板;
2.1.3 early_init_dt_scan_nodes
early_init_dt_scan_nodes函数位于drivers/of/fdt.c文件:
void __init early_init_dt_scan_nodes(void) { int rc = 0; /* Retrieve various information from the /chosen node */ rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); if (!rc) pr_warn("No chosen node found, continuing without\n"); /* Initialize {size,address}-cells info */ of_scan_flat_dt(early_init_dt_scan_root, NULL); /* Setup memory, calling early_init_dt_add_memory_arch */ of_scan_flat_dt(early_init_dt_scan_memory, NULL); }
里面主要对三种类型的信息进行处理,分别是:
- /chosen节点中bootargs属性:/chosen节点中bootargs属性就是内核启动的命令行参数,它里面可以指定根文件系统在哪里,第一个运行的应用程序是哪一个,指定内核的打印信息从哪个设备里打印出来;这里将 /chosen节点中bootargs属性的值, 存入全局变量boot_command_line
- 根节点的 #address-cells 和 #size-cells属性:根节点的#address-cells和#size-cells属性指定属性参数的位数;存入全局变量: dt_root_addr_cells, dt_root_size_cells;
- /memory中的reg属性:/memory中的reg属性指定了不同板子内存的大小和起始地址;
2.1.4 设备节点转换为device_node
我们先想一个问题,我们的uboot把设备树dtb文件随便放到内存的某一个地方就可以使用,为什么内核运行中,他不会去覆盖dtb所占用的那块内存呢?
在前面我们讲解设备树结构时,我们知道,在设备树文件中,可以使用/memreserve/指定一块内存,这块内存就是保留的内存,内核不会占用它。即使你没有指定这块内存,当我们内核启动时,他也会把设备树所占用的区域保留下来。
如下就是函数调用过程:
start_kernel // init/main.c setup_arch(&command_line); // arch/arm/kernel/setup.c arm_memblock_init(mdesc); // arch/arm/kernel/setup.c early_init_fdt_reserve_self(); /* Reserve the dtb region */ // 把dtb所占区域保留下来, 即调用: memblock_reserve early_init_dt_reserve_memory_arch(__pa(initial_boot_params), fdt_totalsize(initial_boot_params), 0); early_init_fdt_scan_reserved_mem(); // 根据dtb中的memreserve信息, 调用memblock_reserve unflatten_device_tree(); // arch/arm/kernel/setup.c __unflatten_device_tree(initial_boot_params, NULL, &of_root, early_init_dt_alloc_memory_arch, false); // drivers/of/fdt.c /* First pass, scan for size */ size = unflatten_dt_nodes(blob, NULL, dad, NULL); /* Allocate memory for the expanded device tree */ mem = dt_alloc(size + 4, __alignof__(struct device_node)); /* Second pass, do actual unflattening */ unflatten_dt_nodes(blob, mem, dad, mynodes); populate_node np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl, __alignof__(struct device_node)); np->full_name = fn = ((char *)np) + sizeof(*np); populate_properties pp = unflatten_dt_alloc(mem, sizeof(struct property), __alignof__(struct property)); pp->name = (char *)pname; pp->length = sz; pp->value = (__be32 *)val;
可以看到,先把dtb中的memreserve信息告诉内核,把这块内存区域保留下来,不占用它。
然后解析dtb文件,转换成节点是device_node的树状结构,这里涉及两个结构体:
- struct device_node:Linux内核使用device_node结构体来描述一个设备节点,此结构体定义在文件 include/linux/of.h 中;
- struct property: Linux内核中使用结构体property表示节点属性,此结构体同样定义在文件include/linux/of.h中;
在dts文件里,每个大括号{}代表一个节点,比如根节点里有个大括号,对应一个device_node结构体。节点里面有各种属性,也可能里面还有子节点,所以它们还有一些父子关系。
根节点下的memory、chosen、uart、rtc等节点是并列关系,兄弟关系。对于父子关系、兄弟关系,在device_node结构体里有成员来描述这些关系。
2.2 arch_call_rest_init
start_kernel函数最后调用arch_call_rest_init函数,arch_call_rest_init函数是定义在init/main.c中的一个函数,用于调用没有被内核启动代码显式调用的初始化函数。
在内核启动过程中,当调用完所有的显式初始化函数后,arch_call_rest_init函数会被调用,以便调用其它的初始化函数,例如驱动程序注册、文件系统初始化、网络初始化等等。这些函数都由模块或子系统定义,并且它们的调用顺序可以在模块或子系统的 Makefile文件中进行配置。
需要注意的是,setup_arch主要负责硬件初始化,而arch_call_rest_init则主要负责软件初始化。它们的调用时机和作用不同,但都是Linux内核启动过程中不可或缺的环节。
在具体实现中,arch_call_rest_init函数会调用名为rest_init 的函数。而在 rest_init 函数中,会使用 fork系统调用创建两个新进程,分别为进程 1 和进程 2。其中:
- 进程1会执行内核的初始化工作,比如初始化文件系统、内存管理等子系统;
- 进程2则会负责启动 idle 进程并运行调度器;
在内核初始化完毕后,进程 1 会退出,而进程 2 则会成为系统中唯一的用户进程。
rest_init 函数位于init/main.c文件:
noinline void __ref rest_init(void) { struct task_struct *tsk; int pid; rcu_scheduler_starting(); /* * We need to spawn init first so that it obtains pid 1, however * the init task will end up wanting to create kthreads, which, if * we schedule it before we create kthreadd, will OOPS. */ pid = kernel_thread(kernel_init, NULL, CLONE_FS); /* * Pin init on the boot CPU. Task migration is not properly working * until sched_init_smp() has been run. It will set the allowed * CPUs for init to the non isolated CPUs. */ rcu_read_lock(); tsk = find_task_by_pid_ns(pid, &init_pid_ns); set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id())); rcu_read_unlock(); numa_default_policy(); pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); rcu_read_lock(); kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); rcu_read_unlock(); /* * Enable might_sleep() and smp_processor_id() checks. * They cannot be enabled earlier because with CONFIG_PREEMPT=y * kernel_thread() would trigger might_sleep() splats. With * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled * already, but it's stuck on the kthreadd_done completion. */ system_state = SYSTEM_SCHEDULING; complete(&kthreadd_done); /* * The boot idle thread must execute schedule() * at least once to get things moving: */ schedule_preempt_disabled(); /* Call into cpu_idle with preempt disabled */ cpu_startup_entry(CPUHP_ONLINE); }
2.2.1 kernel_init
进程1执行的是kernel_init函数,kernel_init函数位于init/main.c文件:
static int __ref kernel_init(void *unused) { int ret; kernel_init_freeable(); /* need to finish all async __init code before freeing the memory */ async_synchronize_full(); ftrace_free_init_mem(); free_initmem(); mark_readonly(); /* * Kernel mappings are now finalized - update the userspace page-table * to finalize PTI. */ pti_finalize(); system_state = SYSTEM_RUNNING; numa_default_policy(); rcu_end_inkernel_boot(); if (ramdisk_execute_command) { ret = run_init_process(ramdisk_execute_command); if (!ret) return 0; pr_err("Failed to execute %s (error %d)\n", ramdisk_execute_command, ret); } /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); } if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/admin-guide/init.rst for guidance."); }
函数调用流程:
kernel_init kernel_init_freeable(); do_basic_setup(); do_initcalls(); for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) do_initcall_level(level); // 比如 do_initcall_level(3) for (fn = initcall_levels[3]; fn < initcall_levels[3+1]; fn++) do_one_initcall(initcall_from_entry(fn)); // 就是调用"arch_initcall_sync(fn)"中定义的fn函数
do_initcall_level 是 Linux 内核中负责执行注册的初始化函数的函数之一。在内核启动过程中,各个子系统需要进行初始化,而这些初始化函数是通过调用 module_init、subsys_initcall、device_initcall 等宏定义的函数实现的。
这些宏实际上是向内核注册相关的初始化函数,然后将其存放到一个特定的段中,例如 __initcall_start 到 __initcall_end。
在内核初始化期间,do_initcall_level 函数按照预设的顺序(由参数 level 指定)执行这些初始化函数:
leveltatic initcall_entry_t *initcall_levels[] __initdata = { __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end, }; /* Keep these in sync with initcalls in include/linux/init.h */ static const char *initcall_level_names[] __initdata = { "pure", "core", "postcore", "arch", "subsys", "fs", "device", "late", }; static void __init do_initcall_level(int level) { initcall_entry_t *fn; strcpy(initcall_command_line, saved_command_line); parse_args(initcall_level_names[level], initcall_command_line, __start___param, __stop___param - __start___param, level, level, NULL, &repair_env_string); trace_initcall_level(initcall_level_names[level]); for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(initcall_from_entry(fn)); }
该函数会检查 __initcall_start 到 __initcall_end 这个段中的所有函数,并将标记了 __initcall 的函数按照指定的级别(level)依次执行。
每个级别都以 __initcall[0-7]_start 标记开头,并且在 System.map 文件中可以看到相应的条目,比如:
c06e687c T __initcall0_start
c06e687c t __initcall_ipc_ns_init0
c06e6880 t __initcall_init_mmap_min_addr0
c06e6884 t __initcall_net_ns_init0
c06e6888 T __initcall1_start
c06e6888 t __initcall_ptrace_break_init1
c06e688c t __initcall_s3c2442_core_init1
c06e6890 t __initcall_s3c2440_core_init1
c06e6894 t __initcall_samsung_gpiolib_init1
c06e6898 t __initcall_wq_sysfs_init1
c06e689c t __initcall_ksysfs_init1
......
c06e6940 t __initcall_of_platform_default_populate_init3s
c06e6944 T __initcall4_start
c06e6944 t __initcall_topology_init4
......
System.map 是在 Linux 内核编译过程中生成的一个文件。它是内核的一个符号表,用于记录内核二进制映像中各个符号(变量、函数等)的地址,以便调试时能够快速定位到这些符号。通过 System.map 文件,开发人员可以查询内核二进制映像中每个符号的地址,以及符号所属的模块或函数名等信息。
2..2.2 of_platform_default_populate_init
在System.map中可以找到__initcall_of_platform_default_populate_init3s,也就是在执行do_initcall_level(3)时,会执行到of_platform_default_populate_init函数。
of_platform_default_populate_init函数定义在drivers/of/platform.c:
static int __init of_platform_default_populate_init(void) { struct device_node *node; if (!of_have_populated_dt()) return -ENODEV; /* * Handle certain compatibles explicitly, since we don't want to create * platform_devices for every node in /reserved-memory with a * "compatible", */ for_each_matching_node(node, reserved_mem_matches) of_platform_device_create(node, NULL, NULL); node = of_find_node_by_path("/firmware"); if (node) { of_platform_populate(node, NULL, NULL, NULL); of_node_put(node); } /* Populate everything else. */ of_platform_default_populate(NULL, NULL, NULL); return 0; } arch_initcall_sync(of_platform_default_populate_init);
可以看到of_platform_default_populate_init初始化函数是通过arch_initcall宏定义的实现的。
在这个函数内部,它会遍历设备节点树,检查设备节点上是否存在compatible属性,如果存在,就会为这个设备节点分配一个platform_device结构体,并将这些platform_device注册到Linux内核中,以便与其它驱动程序进行匹配和绑定。
并非所有的device_node都会转换为platform_device,只有以下的device_node会转换;
(1) 该节点必须含有compatible属性;根节点是例外的,生成platfrom_device时,即使有compatible属性也不会处理。
(2) 根节点的子节点(节点必须含有compatible属性);
(3) 含有特殊compatible属性的节点的子节点(子节点必须含有compatible属性),这些特殊的compatible属性为:
"simple-bus","simple-mfd","isa","arm,amba-bus "
比如dts文件配置如下:
/ { mytest { compatible = "mytest", "simple-bus"; mytest@0 { compatible = "mytest_0"; }; }; i2c { compatible = "samsung,i2c"; at24c02 { compatible = "at24c02"; }; }; spi { compatible = "samsung,spi"; flash@0 { compatible = "winbond,w25q32dw"; spi-max-frequency = <25000000>; reg = <0>; }; }; };
其中:
- /mytest会被转换为platform_device, 因为它兼容"simple-bus",它的子节点/mytest/mytest@0 也会被转换为platform_device;
-
/i2c节点一般表示I2C控制器,,它会被转换为platform_device, 在内核中有对应的platform_driver;/i2c/at24c02节点不会被转换为platform_device,它被如何处理完全由父节点的platform_driver决定,一般是被创建为一个i2c_client;
-
类似的也有/spi节点,,它一般也是用来表示SPI控制器,它会被转换为platform_device,在内核中有对应的platform_driver;/spi/flash@0节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个spi_device;
参考文章
[1]linux设备树学习笔记(二、linux内核对设备树的处理)(部分转载)