ARM技术论坛
直播中

张静

8年用户 1500经验值
私信 关注
[经验]

ARM Cortex-A7的CPU下电和上电流程分析

本文主要分析平台相关的CPU睡眠和唤醒,即下电和上电流程,以及ARM底层汇编代码实现。

内核版本:3.1.0

CPU:ARM Cortex-A7

1 平台相关函数执行流程

上图最后调入suspend_ops->enter,这是个平台相关的函数。

平台相关的cpu suspend_enter函数:

如果有中断挂起,则直接返回;

读取设备的idle状态;

设置CPU0的热跳转寄存器,睡眠唤醒后,跳转到这个地址

允许cpu0睡眠

不屏蔽SCU断电、自断电功能

不屏蔽CPU自断电功能,当CPU进入WFI状态,CPU自动断电

改变CPU的频率、电压输出

调用cpu_suspend函数

恢复CPU的频率、电压输出

屏蔽自断电功能

屏蔽SCU断电、自断电功能

不允许CPU0睡眠

cpu_suspend函数调用流程图

2 睡眠过程详细分析

cpu_suspend:(arch/arm/kernel/suspend.c)

int  cpu_suspend(unsigned long arg, int (*fn)(unsigned long))

函数携带两个参数,第二个参数是函数指针(参数是unsigned long型的,返回值是int型的),第一个参数就是前面的函数指针被调用时需要的参数,故为unsigned long型的

if (!idmap_pgd)                  //**非常有意思的一个变量,稍后再解释**

                   return -EINVAL;

调用 __cpu_suspend

__cpu_suspend:(arch/arm/kernel/sleep.S)

携带的参数就是调用cpu_suspend函数时传入的参数。

R0(unsigned long arg,实际上是个地址,地址存放的类型是suspend_args,是个参数,供R1函数调用时使用)

R1(睡眠函数,执行时需要的参数就是R0)

#define cpu_suspend_size                __glue(CPU_NAME,_suspend_size) arch/arm/include/asm/glue-proc.h

                   #define __glue(name,fn)          ____glue(name,fn)                                                          arch/arm/include/asm/glue.h

                   #define ____glue(name,fn)     name##fn

define CPU_NAME cpu_v7      

                   .equ cpu_v7_suspend_size, 4 * 8arch/arm/mm/proc.v7.S)

故 cpu_suspend_size == cpu_v7_suspend_size

入栈R4-R11,LR

没有定义MULTI_CPU,R4赋值cpu_suspend_size,为32

R5就是入栈后堆栈的地址,图中的1处

R4加上12

然后将堆栈地址减去(32+12),就是让开11个寄存器的值,图中的3处

入栈R0,R1

R0赋值堆栈地址加上8,图中的3处

R1赋值R4,就是44

R2赋值R5,就是图中的1处

R3赋值临时栈地址,文件下面定义了数个(看内核配置了多少个CPU来定)unsigned long 型的地址空间

定义了多核,根据CPU的ID,获取当前CPU的临时栈地址

跳转到__cpu_suspend_save

__cpu_suspend_save:(arch/arm/kernel/suspend.c)

R3即当前CPU的临时栈地址,存入R0的值(物理的地址),图中的3处

以R0为基地址,入栈idmap_pgd的物理地址、入栈当前的堆栈(图中的1处,是个虚拟地址)、入栈唤醒函数( cpu_do_resume 的物理地址)

cpu_do_suspend(glue-proc.h),-> cpu_v7_do_suspend (arch/arm/mm/proc.v7.S)携带的参数R0就是刚入完3个寄存器后的堆栈地址

入栈R4-R10,LR;

以传入的参数R0为栈基地址,入栈R4、R5(PID、线程ID)

入栈R6-R11(域ID,页表基地址寄存器1、页表控制寄存器、系统控制寄存器、辅助寄存器、协处理器访问控制寄存器),加上上面的2个正好是8个;

弹出R4-R10,PC;

刷新cache、二级cache,保证数据确实写到了内存,栈空间也是内存的一部分

LR赋值cpu_suspend_abort

弹出r0,PC ,就是跳转到上面的fn函数指针处

若这个fn函数执行过程中返回了,则调转到lr处,就是cpu_suspend_abort

cpu_suspend_abort:

此时的SP就是图中的3处,弹出到R1,R2,R3。

判断R0的值,若不是0,则将其赋值1

堆栈SP赋值R2的值,图中的1处

堆栈弹出R4-R11,PC。实际是返回到cpu_suspend函数中,调用__cpu_suspend函数的地方。

fn函数执行过程,若最终执行成功,则执行wfi指令,CPU顺利下电

清除Icache,刷dcache。

关闭SMP位

关闭对应的CCI端口

然后进入WFI睡眠,低功耗状态

如果中间出现了差错,则直接返回1后,接着下面的cpu_suspend_abort执行,仍然能够返回到cpu_suspend函数,其中__cpu_suspend函数的返回值强行变为了1

3 唤醒过程详细分析

低功耗模式被唤醒后,跳转到唤醒地址

设置SVC模式,关闭IRQ、FIQ

使能对应的CCI端口

清除SMP位,清除跳转预测等,开启I cache

跳转到cpu_resume的物理地址

cpu_resume:

获取当前CPU的临时栈地址,保存到R0中,图中的3处

设置SVC模式,关闭I、F

以R0为基地址,弹出R1、SP、PC,R1就是 idmap_pgd

跳转到cpu_do_resume,就是cpu_v7_do_resume

cpu_v7_do_resume

清除TLB、I cache、上下文ID

再次以R0为基地址,弹出R4-R5,并恢复PID、线程ID,

弹出R6-R11,

恢复域ID

将R1的内容设置到页表基地址寄存器0,为开启MMU做准备

恢复页表基地址寄存器1,页表控制寄存器

恢复辅助寄存器,协处理器访问控制寄存器

设置内存属性间接寄存器

将系统控制寄存器R8内容赋值与R0,跳转到cpu_resume_mmu

cpu_resume_mmu:

将R0设置系统控制器寄存器,开启MMU后,跳转到下面的虚拟地址,cpu_resume_after_mmu

cpu_resume_after_mmu:

cpu_init 执行CPU的初始化

R0赋值为0,

此时的SP是虚拟的,然后弹出R4-R11,PC。接着返回到cpu_suspend函数,__cpu_suspend函数的返回值是0

cpu_switch_mm(mm->pgd, mm);

cpu_v7_switch_mmproc-v7-2level.S

                        设置context ID

                        设置TTB,页表转换基地址  将基地址由idmap_pgd变为mm->pgd

4、idmap_pgd变量

pgd_t *idmap_pgd;

typedef pmdval_t pgd_t\[2\];

typedef u32 pmdval_t;

故 pgd_t是一个无符号32位的数组, idmap_pgd 是个指针,指向有两个变量的的数组,变量类型是无符号32位的数

init_static_idmap函数

idmap_pgd = pgd_alloc(&init_mm);赋值重新分配后的一个内存地址,4K的大小,且里面已经填充了很多东西了

idmap_start获取__idmap_text_start对应的物理地址

immap_end获取__idmap_text_end对应的物理地址

identity_mapping_add(idmap_pgd, idmap_start, idmap_end); 建立1:1的隐射,从idmap_start开始到idmap_end结束

从我编译的内核来看,是下面的这4段代码,都是跟开启、关闭MMU相关的。

c055dcb8 T __idmap_text_start

c055dcb8 T __kprobes_text_end

c055dcb8 T __kprobes_text_start

c055dcb8 T __turn_mmu_on
head.s 开启MMU

c055dcd8 t __turn_mmu_on_end

c055dcd8 Tcpu_resume_mmu sleep.s 有开启MMU的过程

c055dcfc Tcpu_v7_reset proc-v7.s 关闭MMU

c055dd40 Tcomip_mmu_off sleep.s

c055dd80 T __idmap_text_end

idmap_pgd指向内存中的4K大小的一段空间,实际是页表基地址c000 4000对应的物理地址开始的一个备份(部分的是一样的),而且还将开启、关闭MMU的代码都1:1映射了。这样在cpu_switch_mm之前,就可以启用MMU了,有页表基地址idmap_pgd,且建立了隐射。

其实CPU0在从uboot跳转到压缩内核处,解压缩完毕,跳转到arch/arm/kernel/head.s 。

__create_page_tables时,也曾经为__turn_mmu_on到__turn_mmu_on_end这一段,建立了1:1的隐射

在init_static_idmap函数的结尾打印c000 4000开始的页表地址内容,备份的页表地址idmap_pgd的内容,如下所示,只打印不是0的内容。

c000 4000开始的内容如下:










0xd7c6b570 : 1c01140e         1c11140e                   1c21140e                   1c31140e

0xd7c6b580 : 1c41140e        1c51140e                   1c61140e                   1c71140e

0xd7c6b590 : 1c81140e         1c91140e                   1ca1140e                   1cb1140e

0xd7c6b5a0 : 1cc1140e          1cd1140e                   1ce1140e                   1cf1140e

0xd7c6b5b0 : 1d01140e         1d11140e         1d21140e         1d31140e

0xd7c6b5c0 : 1d41140e         1d51140e         1d61140e         1d71140e

0xd7c6b5d0 : 1d81140e         1d91140e         1da1140e         1db1140e

0xd7c6b5e0 : 1dc1140e         1dd1140e         1de1140e         1df1140e

0xd7c6b5f0 :   1e01140e         1e11140e         1e21140e         1e31140e

0xd7c6b600 : 1e41140e         1e51140e         1e61140e         1e71140e

0xd7c6b610 : 1e81140e         1e91140e         1ea1140e         1eb1140e

0xd7c6b620 : 1ec1140e           1ed1140e         1ee1140e         1ef1140e

0xd7c6b640 : 1e023811         1e023c11                   00000000         00000000

0xd7c6be00 : a0011452         a0111452         a0211452         a0311452

0xd7c6be10 : a0411452         a0511452         a0611452         a0711452

0xd7c6be20 : a0811452         a0911452         a0a11452                  00000000

0xd7c6bf40 :   e1011452         e1111452         00000000         00000000

0xd7c6bff0 :    00000000         00000000         1effe821           1effec21

可以看出,两者的内容除了0xd7c681a0处,两个隐射不一样外,其它内容都是相同的。

而0xd7c681a0处,减去0xd7c68000,算出来对应的虚拟地址是0680 0000开始的2M内容,而此处实际的物理地址填充的是06800402 06900402,则真好是1:1的段隐射。

在cpu_suspend函数时,先判断idmap_pgd是否为空,若不为空,则CPU在上电完成后开启MMU时,用到的页表基地址就是idmap_pgd。后面判断ret是否为0,若为0,则CPU经历了下电又上电,需要替换页表基地址。

其实多核CPU的启动,其它CPU启动,开启MMU时,用到的页表基地址也是idmap_pgd,跳转到C语言的secondary_start_kernel函数后,执行cpu_switch_mm(mm->pgd, mm),这个也是为了替换页表基地址.

原作者:wangyw

更多回帖

发帖
×
20
完善资料,
赚取积分