li
r3,0
<3,7>:mtspr
SPRN_SPRG_RTAS,r3 // 0 =>
not in RTAS
SPRN_SPRG_RTAS宏代表的是SPRG2寄存器,<3,7>初始化该寄存器值为0。在我们的MPC8641HPCN平台的.config文件中没有配置CONFIG_PPC_RTAS,所以我们不会使用SPRG2寄存器中的这个值。
<3,8>:lis
r1,init_thread_union@ha
<3,9>:addi
r1,r1,init_thread_union@l
解释:
init_task进程使用init_thread_union数据结构描述的内存区域作为该进程的堆栈空间,并且和自身的thread_info参数公用这一内存空间空间,其数据结构的定义如下(linux-
2.6.38/include/linux/sched.h)
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
init_thread_union变量在arch/powerpc/kernel/init_task.c中定义,代码如下:
union thread_union init_thread_union
__init_task_data
=
{ INIT_THREAD_INFO(init_task) };
该变量用INIT_THREAD_INFO(init_task)把init_thread_union变量初始化为init_task进程描述符的thread_info。
备注:__init_task_data并不是一个变量,它只是一个宏。该宏在include/linux/init_task.h中定义:
#define __init_task_data
__attribute__((__section__(".data..init_task")))
该宏把init_thread_union变量放入.data..init_task段中。
INIT_THREAD_INFO在arch/powerpc/include/asm/thread_info.h中定义,其代码如下:
#define INIT_THREAD_INFO(tsk)
\
{
\
.task =
&tsk,
\
<3,9,1>
.exec_domain =
&default_exec_domain, \
.cpu =
0,
\
<3,9,2>
.preempt_count = INIT_PREEMPT_COUNT,
\
.restart_block = {
\
.fn = do_no_restart_syscall, \
},
\
.flags =
0,
\
}
所以0号进程init_task的init_thread_union 描述符中的tsk成员指向init_task进程描述符,cpu成员赋值为0,表示指出init_task进程目前在MPC8641D的core 0上运行。
thread_union结构体定义在include/linux/sched.h文件中,代码如下:
union thread_union
{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
#define init_thread_info
(init_thread_union.thread_info)
#define init_stack
(init_thread_union.stack)
从代码中我们可以看出为了方便使用,PowerPC
linux定义了两个宏:init_thread_info和init_stack,分别指向init_task进程的thread_info和stack,针对我们的MPC8641D平台,THREAD_SIZE为8K
<3,10>:li
r0,0
解释:将r0寄存器初始化为0
<3,11>:stwu
r0, THREAD_SIZE-STACK_FRAME_OVERHEAD(r1)
解释:<3,9>和<3,10>是为init_task进程创建堆栈空间。stwu为init_task分配一个最小的栈帧(即16个字节),因为init_task进程的函数体不带参数且不返回,所以最小栈帧就足够用的了。
备注:r1指针是PowerPC linux中的栈指针,在执行
stwu
r0, THREAD_SIZE-STACK_FRAME_OVERHEAD(r1)
之前,r1指向8K栈空间的基地址addr(为了方便描述,假设为addr);当执行完这条指令后r1指向addr+8K-16,并且addr+8K-16指向的内存值为0。
执行到此处init_task进程的进程描述符,代码段,数据段和堆栈段已经建立起来了,但是init_task的地址空间此刻还没有建立,接下来的代码一个非常重要的功能就是创建init_task进程的地址空间。下面我们将依次分析machine_init()、_save_cpu_setup()以及MMU_init()函数。
<3,12>:mr
r3,r31
<3,13>:mr
r4,r30
解释:
首先我们看一下_start函数的入口处的指令序列:
__start:
cmpwi
0,r5,0
beq
1f
1:
mr r31,
r3
mr r30,
r4
。。。。。。
通用寄存器r3:指向OF Tree的物理地址
通用寄存器r4:指令Linux内核所在的物理地址
这两个值都是UBoot传递给Linux-smp内核的。
从上面的代码我们可以看出:
这两个值均备份在r31,r30寄存器中,现在要恢复r3,r4的值,其中r3的值是作为machine_init()函数的参数!
下面我们将详细的分析这些过程:
<3,14>:bl
machine_init
输入参数:r3中的值(OF Tree的物理地址)
该值是有boot
loader(比如在MPC8641HPCN开发板,我使用的是U-boot)传过来的OF设备树的地址。
该函数在arch/powerpc/kernel/setup_32.c中定义,主要实现两个功能:
一:分析OF树的结构,获得当前处理器的内存使用情况,创建LMB结构,同时获得当前CPU在OF树中的硬件信息;
二:确认当前处理器系统的ppc_md结构,ppc_md结构定义了当前linux-smp的一系列钩子函数,设置ppc_md函数的目的是为了进一步优化linux-smp系统源代码结构。
machine_init()函数的代码如下:
notrace void __init machine_init(unsigned long
dt_ptr)
{
lockdep_init();
<3,13,1>
//Enable early debugging if any specified (see udbg.h)
udbg_early_init();
<3,13,2>
//Do some early initialization based on the flat device
tree
early_init_devtree(__va(dt_ptr)); <3,13,3>
probe_machine();
<3,13,4>
setup_kdump_trampoline();<3,13,5>
#ifdef CONFIG_6xx
if (cpu_has_feature(CPU_FTR_CAN_DOZE) ||
cpu_has_feature(CPU_FTR_CAN_NAP))
ppc_md.power_save = ppc6xx_idle;<3,13,6>
#endif
#ifdef CONFIG_E500<3,13,7>
if (cpu_has_feature(CPU_FTR_CAN_DOZE) ||
cpu_has_feature(CPU_FTR_CAN_NAP))
ppc_md.power_save = e500_idle;
#endif
if (ppc_md.progress)
ppc_md.progress("id mach(): done", 0x200);
<3,13,8>
}
分析:
<3,13,1>:lockdep_init()
在我们的MPC8641HPCN平台lockdep_init()函数是一个宏函数,在include/lockdep.h中被定义为
# define
lockdep_init()
do { } while (0)
<3,13,2>:udbg_early_init()
在我们的MPC8641HPCN平台是一个空函数。
备注:
关于lockdep_init(),udbg_early_init()这两个函数,前者用于启动Lock Dependency Validator(内核依赖的关系表),本质上就是建立两个散列表calsshash_table和chainhash_table,并初始化全局变量lockdep_initialized,标志已初始化完成。后者用于初始化早期调试输出,可以通过配置config文件使能其中的一个,一般都是NS16550的串口打印调试。这两个函数在编译内核是都是可配置的,我在编译linux-smp内核映象均没有配置,所以将不分析这两个函数。
<3,13,3>:early_init_devtree(__va(dt_ptr))
该函数就是我们上面所说的machine_init()函数的第一个功能,即分析OF树的结构,获得当前处理器的内存使用情况,创建LMB结构,同时获得当前CPU在OF树中的硬件信息,比如CPU主频、内部寄存器级基地址等等。
这里需要说明一下__va(dt_ptr)函数,既然dt_ptr中存放着设备树所在的物理地址处,为啥要将这个地址再转换成有效地址EA呢?因为我们的MMU已经开启了,MMU硬件会自动把有效地址EA映射到物理地址PA。
如下图所示r31中存放设备树物理地址,__va()会把它转换到虚拟地址,放在r3寄存器中。
为了能清楚的说明Linux-smp如何利用OF设备树获取MPC8641HPCN的硬件信息(比如CPU主频、内部寄存器基地址等等),我不得不介绍一下扁平设备树的来历:
设备树DT(Device
Tree)是描述目标板硬件的数据结构,我们可以简单的设想一下如果我们在Linux kernel中硬编码硬件的基本信息,那么如果目标板做出更改的话,我们必须重新编译整个Linux kernel,才能把修改的信息通知Linux。如果我们把对硬件的描述信息存放在设备树DT中,我们只需要修改DT,然后把DT传递给Linux即可,这样做的使得Linux尽可能地降低硬件依赖,从而利于加速支持包的开发,降低硬件带来的变化需求和成本,降低对内核设计和编译的要求。
关于DT的详细信息我们可以参考:
IBM、Sun 等厂家的服务器最初都采用了Firmware(一种嵌入到硬件设备中的程序,用于提供软件和硬件之间的接口,类似于X86上的BIOS),用于初始化系统配置,提供操作系统软件和硬件之间的接口,启动和运行系统。后来为了标准化和兼容性,IBM、Sun 等联合推出了固件接口IEEE
1275标准,让他们的服务器如IBM PowerPC pSeries,Apple PowerPC,Sun SPARC 等均采用OpenFirmware,在运行时构建系统硬件的设备树信息传递给内核,进行系统的启动运行。从而减少内核对系统硬件的严重依赖,利于加速支持包的开发,降低硬件带来的变化需求和成本,降低对内核设计和编译的要求。
随着Linux/ppc64
内核的发展,内核代码从原来的arch/ppc32 和arch/ppc64
逐渐迁移到统一的arch/powerpc 目录(例如我们使用的Linux-2.6.38内核就是如此,基于PowerPC的平台代码都集中在arch/powerpc目录下),并在内核代码引入Open Firmware API
以使用标准固件接口(参考:Power.org(TM) Standard for Embedded Power Architecture(TM)
Platform Requirements (ePAPR))。Linux 内核在运行时需要知道硬件的一些相关信息,当我们使用ARCH=powerpc 参数编译的内核镜像,这些信息基于Open
Firmware 规范,以设备树的形式存在(我编译的基于PowerPC的linux-2.6.38-smp内核映像就使用了这个参数)。这样内核在启动时扫描Open Firmware 提供的设备树,从而读取平台的硬件设备信息,搜索匹配的设备驱动程序并将该驱动程序绑定到设备
在嵌入式PowerPC
中,一般使用U-Boot来引导系统。早期的U-Boot的内核不支持Open Firmware,只有使用include/asm-ppc/u-boot.h 中的静态数据结构struct
bd_t 将板子基本信息传递给内核,其余的由内核处理。这样的接口不够灵活,硬件发生变化就需要重新定制编译烧写UBoot代码和内核,为了适应内核的发展及嵌入式PowerPC平台的千变万化,吸收标准Open Firmware
的优点,U-Boot 引入了扁平设备树FDT((Flatted Device Tree)这样的一个动态接口,使用一个单独的FDT blob(二进制大对象,是一个可以存储二进制文件的容器)存储传递给内核的参数。一些确定信息,例如cache 大小、中断路由等直接由设备树提供,而其他的信息,例如eTSEC 的MAC 地址、频率、PCI
总线数目等由U-Boot
在运行时修改。U-Boot
使用扁平设备树取代了bd_t,而且也不再保证对bd_t 的后向兼容。
大家应该记得在我们的MPC8641HPCN平台,U-boot使用通用寄存器r3,r4,r5,r6,r7传递参数给linux-smp内核映象的入口__start()函数
由于早期的U-Boot的内核不支持Open Firmware,只有使用include/asm-ppc/u-boot.h 中的静态数据结构struct
bd_t 将板子基本信息传递给内核,其余的由内核处理。这样的话r3至r7寄存器传递的数值如下:
r3:bd_info的地址
r4:initrd的起始地址
r5:initrd的结束地址
r6:命令行参数的起始地址
r7:命令行参数的结束地址
而现在的U-boot,比如我用来引导Linux-2.6.38内核的UBoot-2010.09版本,不但支持Open Firmware,而且引入了扁平设备树FDT((Flatted Device Tree)这样的一个动态接口,使用一个单独的FDT blob(二进制大对象,是一个可以存储二进制文件的容器)存储传递给内核的参数,这样的话r3至r7寄存器传递的数值如下:
r3:指向dtb(设备树二进制文件,也成为OF设备树)的物理地址
r4:指向linux-2.6.38-smp内核映象所在的物理地址
r5:空值NULL
r6:空值NULL
r7:空值NULL
正因为如此我们才可以在_start函数的<2>处,根据r5寄存器的值来判断U-boot传递给linux内核的参数类型,在MPC8641D平台,U-boot支持OF结构,所有只使用了r3,r4寄存器来传递参数,r5至r7均为NULL。所有CPU会跳转到标签“1:”处运行。
<3,13,3>
early_init_devtree(__va(dt_ptr))
扁平设备树FDT(Flatted Device Tree)的构成
我们在本文的开始处已经说过:U-boot把Linux-smp映象和目标板的OF设备树加载到内存的不同位置,并把两个位置通过r3,r4寄存器传递给linux-smp的__start()函数,最后把CPU的控制权交给了linux-smp。
例如:针对我们的MPC8641HPCN目标板来说,U-boot把linux-smp映象加载内内存0x0000 0000起始的内存区域,然后把MPC8641d的OF设备树加载的内存0x007 fa000;然后通过r3和r4寄存器,U-boot分别把0x007fa000和0x0000 0000分别传递给linux-2.6.38-smp内核。
这里的OF设备树描述了MPC8641HPCN开发板的信息,我们称之为FDT(Flattened Device Tree)
或者DTB(Device Tree
Blob),FDT是一个单一线性数据结构,主要包含四个部分:
FDT头结构(FDT header)、内存保留块(Memory Reservation BlockStructure Block)以及字符块(Strings Block)。
示意图如下:
第一个组件:头结构
FDT头结构:描述设备树的一些基本信息,例如设备树大小,结构块偏移地址,字符串块偏移地址等,在Linux-2.6.38中linux-2.6.38/include/linux/of_fdt.h相关代码如下:
struct fdt_header {
__be32
magic;
__be32
totalsize;
__be32
off_dt_struct;
__be32
off_dt_strings;
__be32
off_mem_rsvmap;
__be32
version;
__be32
last_comp_version;
__be32
boot_cpuid_phys;
__be32
dt_strings_size;
__be32
dt_struct_size;
};
解释:
magic:FDT设备树幻数标识,FDT采用大端法存储,其幻数标识为:0xd00dfeed
totalsize:这个FDT设备树的大小,如上图所示包含FDT头结构(FDT header)、内存保留块(Memory Reservation Block)、结构块(Structure Block)、字符块(Strings Block)以及每个块前后的空闲空间(Free Space)
off_dt_struct:保存结构块(Structure Block)从FDT头结构所在的地址处到结构块(Structure Block)自身所在位置的偏移量,以字节位单位
off_dt_strings:保存字符块(Strings Block)从FDT头结构所在的地址处到字符块(Strings Block)自身所在位置的偏移量,以字节位单位
off_mem_rsvmap:保存内存保留块(Memory Reservation Block)从FDT头结构所在的地址处到内存保留块(Memory Reservation Block)自身所在位置的偏移量,以字节位单位
version:保存设备树版本
last_comp_version:该设备树版本向下兼容的最低版本号
boot_cpuid_phys:在Linux SMP中有意义,Linux SMP将CPU分成BSP(Boot Stap CPU)和AP(Application CPU),其中BSP用于Linux SMP的初始化和引导其他的AP,该编号和FDT中的CPU的reg号一致。比如MPC8641HPCN平台上,引导CPU是core 0,而core 0的reg 为<0>,所以boot_cpuid_phys的值为0
dt_strings_size:FDT中的字符块(Strings Block)的长度
dt_struct_size:FDT设备树的大小,以字节位单位
第二个组件:内存保留块(Memory
Reservation Block)
内存保留块给操作系统提供了一条内存保留块的链表,该链表所指向的内存块不作为普通的内存分配使用,该区域用来保存一个关键的数据结构不会被操作系统的所修改。
内存保留块链表的结构体类型如下:
struct
fdt_reserve_entry {
uint64_t address;
uint64_t size;
};
address:指向了内存保留区块的首地址
size:支持内存保留区块的尺寸
需要注意的是在内存区块链表中,需要用一个单元来表示链表的结束标示,只需将该单元的address和size均设置为0即可
第三个组件:结构块(Structure
Block)
扁平设备树结构块是一个线性化的结构体,正如上图所示FDT是一个线性化的结构体,是由一系列的片段构成,这些片段组织成一个线性的树状结构体,是设备树的主体。FDT以节点(node)的形式保存了目标单板上的设备信息。下面介绍这些节点信息在扁平设备树中的存储格式。
在结构块中以宏FDT_BEGIN_NODE和FDT_END_NODE标识一个节点的开始和结束,具体的说由以下几个部分组成:
section1:节点开始标志FDT_BEGIN_NODE
section2:节点路径或者节点的单元名
section3:填充字段(对齐到四字节)
section4:节点属性。每个属性以宏OF_DT_PROP开始,
后面依次为该属性的属性名称长度、属性名称在字符
块中的偏移量、属性值和填充
section5如果存在子节点,则定义子节点
section6:节点结束标志FDT_END_NODE
FDT(Flattened Device Tree)或者DTB(Device Tree Blob)中设备树由device node组成,这些device node又可以由更多的子device node组成!OF使用device tree对一个处理器系统进行描述,我们选取linux-2.6.38/arch/powerpc/boot/dts/
mpc8641_hpcn.dts文件中的部分内容进行描述,内容如下:
/ {
model = "MPC8641HPCN";
compatible = "fsl,mpc8641hpcn";
#address-cells =
<1>;
#size-cells = <1>;
cpus {
#address-cells =
<1>;
#size-cells = <0>;
PowerPC,8641@0 {
device_type = "cpu";
reg = <0>;
d-cache-line-size =
<32>;
i-cache-line-size =
<32>;
d-cache-size = <32768>;
// L1
i-cache-size = <32768>;
// L1
timebase-frequency = <0>;
// From uboot
bus-frequency = <0>;
// From uboot
clock-frequency = <0>;
// From uboot
};
PowerPC,8641@1 {
device_type = "cpu";
reg = <1>;
d-cache-line-size =
<32>;
i-cache-line-size =
<32>;
d-cache-size =
<32768>;
i-cache-size =
<32768>;
timebase-frequency = <0>;
// From uboot
bus-frequency = <0>;
// From uboot
clock-frequency = <0>;
// From uboot
};
};
memory {
device_type = "memory";
reg = <0x00000000
0x40000000>;
// 1G at 0x0
};
以上的device
tree结构分别描述处理器系统中的CPU和Memory,该设备树中包含唯一的一个根节点/,在根节点中包含cpus和memory节点,cpus又包含两个子节点PowerPC,8641@0和PowerPC,8641@1。
待续。。。。。。。