历史上的今天
今天是:2024年12月14日(星期六)
2021年12月14日 | Exynos4412 内核移植(六)—— 设备树解析
2021-12-14 来源:eefocus
一、描述
ARM Device Tree起源于OpenFirmware (OF),在过去的Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。为了改变这种局面,Linux社区的大牛们参考了PowerPC等体系架构中使用的Flattened Device Tree(FDT),也采用了Device Tree结构,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。
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等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
通常由.dts文件以文本方式对系统设备树进行描述,经过Device Tree Compiler(dtc)将dts文件转换成二进制文件binary device tree blob(dtb),.dtb文件可由Linux内核解析,有了device tree就可以在不改动Linux内核的情况下,对不同的平台实现无差异的支持,只需更换相应的dts文件,即可满足。
二、相关结构体
1、U-Boot需要将设备树在内存中的存储地址传给内核。该树主要由三大部分组成:头(Header)、结构块(Structure block)、字符串块(Strings block)。
设备树在内存中的存储布局图:

1.1 头(header)
头主要描述设备树的一些基本信息,例如设备树大小,结构块偏移地址,字符串块偏移地址等。偏移地址是相对于设备树头的起始地址计算的。
struct boot_param_header {
__be32 magic; //设备树魔数,固定为0xd00dfeed
__be32 totalsize; //整个设备树的大小
__be32 off_dt_struct; //保存结构块在整个设备树中的偏移
__be32 off_dt_strings; //保存的字符串块在设备树中的偏移
__be32 off_mem_rsvmap; //保留内存区,该区保留了不能被内核动态分配的内存空间
__be32 version; //设备树版本
__be32 last_comp_version; //向下兼容版本号
__be32 boot_cpuid_phys; //为在多核处理器中用于启动的主cpu的物理id
__be32 dt_strings_size; //字符串块大小
__be32 dt_struct_size; //结构块大小
};
1.2 结构块(struct block)
设备树结构块是一个线性化的结构体,是设备树的主体,以节点node的形式保存了目标单板上的设备信息。
在结构块中以宏OF_DT_BEGIN_NODE标志一个节点的开始,以宏OF_DT_END_NODE标识一个节点的结束,整个结构块以宏OF_DT_END结束。一个节点主要由以下几部分组成。
(1)节点开始标志:一般为OF_DT_BEGIN_NODE。
(2)节点路径或者节点的单元名(ersion<3以节点路径表示,version>=0x10以节点单元名表示)
(3)填充字段(对齐到四字节)
(4)节点属性。每个属性以宏OF_DT_PROP开始,后面依次为属性值的字节长度(4字节)、属性名称在字符串块中的偏移量(4字节)、属性值和填充(对齐到四字节)。
(5)如果存在子节点,则定义子节点。
(6)节点结束标志OF_DT_END_NODE。
1.3 字符串块
通过节点的定义知道节点都有若干属性,而不同的节点的属性又有大量相同的属性名称,因此将这些属性名称提取出一张表,当节点需要应用某个属性名称时直接在属性名字段保存该属性名称在字符串块中的偏移量。
1.4 设备树源码 DTS 表示
设备树源码文件(.dts)以可读可编辑的文本形式描述系统硬件配置设备树,支持 C/C++方式的注释,该结构有一个唯一的根节点“/”,每个节点都有自己的名字并可以包含多个子节点。设备树的数据格式遵循了 Open Firmware IEEE standard 1275。这个设备树中有很多节点,每个节点都指定了节点单元名称。每一个属性后面都给出相应的值。以双引号引出的内容为 ASCII 字符串,以尖括号给出的是 32 位的16进制值。这个树结构是启动 Linux 内核所需节点和属性简化后的集合,包括了根节点的基本模式信息、CPU 和物理内存布局,它还包括通过/chosen 节点传递给内核的命令行参数信息。
1.5 machine_desc结构
内核提供了一个重要的结构体struct machine_desc ,这个结构体在内核移植中起到相当重要的作用,内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。machine_desc结构体通过MACHINE_START宏来初始化,在代码中, 通过在start_kernel->setup_arch中调用setup_machine_fdt来获取。
struct machine_desc {
unsigned int nr; /* architecture number */
const char *name; /* architecture name */
unsigned long atag_offset; /* tagged list (relative) */
const char *const *dt_compat; /* array of device tree* 'compatible' strings */
unsigned int nr_irqs; /* number of IRQs */
#ifdef CONFIG_ZONE_DMA
phys_addr_t dma_zone_size; /* size of DMA-able area */
#endif
unsigned int video_start; /* start of video RAM */
unsigned int video_end; /* end of video RAM */
unsigned char reserve_lp0 :1; /* never has lp0 */
unsigned char reserve_lp1 :1; /* never has lp1 */
unsigned char reserve_lp2 :1; /* never has lp2 */
enum reboot_mode reboot_mode; /* default restart mode */
struct smp_operations *smp; /* SMP operations */
bool (*smp_init)(void);
void (*fixup)(struct tag *, char **,struct meminfo *);
void (*init_meminfo)(void);
void (*reserve)(void);/* reserve mem blocks */
void (*map_io)(void);/* IO mapping function */
void (*init_early)(void);
void (*init_irq)(void);
void (*init_time)(void);
void (*init_machine)(void);
void (*init_late)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
void (*handle_irq)(struct pt_regs *);
#endif
void (*restart)(enum reboot_mode, const char *);
};
1.6 设备节点结构体
struct device_node {
const char *name; //设备name
const char *type; //设备类型
phandle phandle;
const char *full_name; //设备全称,包括父设备名
struct property *properties; //设备属性链表
struct property *deadprops; //removed properties
struct device_node *parent; //指向父节点
struct device_node *child; //指向子节点
struct device_node *sibling; //指向兄弟节点
struct device_node *next; //相同设备类型的下一个节点
struct device_node *allnext; //next in list of all nodes
struct proc_dir_entry *pde; //该节点对应的proc
struct kref kref;
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
};
1.7 属性结构体
struct property {
char *name; //属性名
int length; //属性值长度
void *value; //属性值
struct property *next; //指向下一个属性
unsigned long _flags; //标志
unsigned int unique_id;
};
三、设备树初始化及解析
分析Linux内核的源码,可以看到其对扁平设备树的解析流程如下:
(1)首先在内核入口处将从u-boot传递过来的镜像基地址。
(2)通过调用early_init_dt_scan()函数来获取内核前期初始化所需的bootargs,cmd_line等系统引导参数。
(3)根据bootargs,cmd_line等系统引导参数进入start_kernel()函数,进行内核的第二阶段初始化。
(4)调用unflatten_device_tree()函数来解析dtb文件,构建一个由device_node结构连接而成的单项链表,并使用全局变量of_allnodes指针来保存这个链表的头指针。
(5)内核调用OF提供的API函数获取of_allnodes链表信息来初始化内核其他子系统、设备等。
//kernel 初始化的代码(init/main.c)
asmlinkage void __init start_kernel(void)
{
...
//这个setup_arch就是各个架构自己的设置函数,哪个参与了编译就调用哪个,arm架构应当是arch/arm/kernel/setup.c中的 setup_arch。
setup_arch(&command_line);
...
}
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc;
setup_processor();
//setup_machine_fdt函数获取内核前期初始化所需的bootargs,cmd_line等系统引导参数
mdesc = setup_machine_fdt(__atags_pointer);//__atags_pointer是bootloader传递参数的物理地址
if (!mdesc)
mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
machine_desc = mdesc;
machine_name = mdesc->name;
if (mdesc->reboot_mode != REBOOT_HARD)
reboot_mode = mdesc->reboot_mode;
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk = (unsigned long) _end;
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
parse_early_param();
sort(&meminfo.bank, meminfo.nr_banks, sizeof(meminfo.bank[0]), meminfo_cmp, NULL);
early_paging_init(mdesc, lookup_processor_type(read_cpuid_id()));
setup_dma_zone(mdesc);
sanity_check_meminfo();
arm_memblock_init(&meminfo, mdesc);
paging_init(mdesc);
request_standard_resources(mdesc);
if (mdesc->restart)
arm_pm_restart = mdesc->restart;
//解析设备树
unflatten_device_tree();
......
}
(一) 函数获取内核前期初始化所需的bootargs,cmd_line等系统引导参数
1. setup_machine_fdt()函数获取内核前期初始化所需的bootargs,cmd_line等系统引导参数。
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
#ifdef CONFIG_ARCH_MULTIPLATFORM
DT_MACHINE_START(GENERIC_DT, "Generic DT based system")
MACHINE_END
mdesc_best = &__mach_desc_GENERIC_DT;
#endif
//bootloader传递参数的物理地址不为空,并将物理地址转化为虚拟地址,
//通过函数early_init_dt_scan从设备树中读出bootargs,cmd_line等系统引导参数。
if (!dt_phys || !early_init_dt_scan(phys_to_virt(dt_phys)))
return NULL;
//根据设备树中根节点属性"compatible"的属性描述,找到系统中定义的最匹配的machine_desc结构,该结构控制系统体系架构相关部分的初始化
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
if (!mdesc) {
const char *prop;
long size;
unsigned long dt_root;
early_print("nError: unrecognized/unsupported ""device tree compatible list:n[ ");
//找到设备树的根节点,dt_root指向根节点的属性地址处
dt_root = of_get_flat_dt_root();
//读出根节点的"compatible"属性的属性值
prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
//将根节点的"compatible"属性的属性值打印出来
while (size > 0) {
史海拾趣
|
定时/计数器都有定时或对外部事件计数的功能。通常,定时/计数器归纳起来一般有3种类型: 硬件计数器 可在简单软件控制下计数,特点是成本低,使用方便,但是专用性强,使用不广泛。 软件定时/计数器 特点是几乎没有硬件费用,但他占用cpu的 ...… 查看全部问答> |
|
能不能用VC,根据无线网SSID和自己填写的密码,迅速验证密码是否正确 英特尔的无线网卡,能不能用VC做一个程序,根据无线网SSID(网名)和自己填写的密码,迅速验证密码是否正确呀? 初步设计的界面如下 无线网卡的管理工具也可以验证,但是太慢了,而且每次只能验证最多4个密码。不好 不知这个问题是不是应该在 ...… 查看全部问答> |
|
本月11日,中国嵌入式系统产业联盟“百家e坛”嵌入式技术主题论坛第三期如期召 开,延续前两期活动的火爆场面,本期活动同样吸引了众多嵌入式技术爱好者的热情参与。 本期活动可谓是名家云集,三位主讲嘉宾都是嵌入式业界知名的技术专家,因此吸引 ...… 查看全部问答> |
|
ARM芯片上有一些管脚没有寄存器对其进行控制,在WinCE驱动程序中,请问这样的管脚应该如何操作呢?如对扩展总线进行读写控制的nROE、nRWE,还有扩展的地址线RADDR(GPA有些管脚是RADDR,可以操作,但其它的应如何赋值并操作呢?)等。 … 查看全部问答> |
|
我们的ODM产品需要增加一个短信提示功能。客户的需求是,采取C/S架构的通信。Client端是若干个GPRS终端,Server端是一台拥有公网IP的PC。要求c、s能够双向通信。 在和客户的构图中,目前提出有3种方案: &n ...… 查看全部问答> |




