驱动代码部分,以全志 sunxi-3.4 代码以及 linux-3.4.2 的 samsung 的 s3c2410 代码为例
1. PM 核心模块 platform_suspend_ops->enter()
全志的代码
/*
*********************************************************************************************************
* 全志 aw_pm_enter 代码注释(截取部分主体代码)
*Arguments : state 系统睡眠状态;
*Return : 成功时返回 0 值;
*********************************************************************************************************
*/
static int aw_pm_enter(suspend_state_t state)
{
normal_standby_func standby;
standby = (int (*)(struct aw_pm_info *arg))SRAM_FUNC_START; /* 指向 SRAM 的起始位置,该位置的代码由全志 standby.bin 文件决定,可以参考其中的 Makefile 以及链接文件 */
//把 standby 代码拷贝到 SRAM 起始位置
memcpy((void *)SRAM_FUNC_START, (void *)&standby_bin_start, (int)&standby_bin_end - (int)&standby_bin_start);
/* 配置唤醒事件类型 */
if(PM_SUSPEND_MEM == state || PM_SUSPEND_STANDBY == state){
standby_info.standby_para.axp_src = AXP_MEM_WAKEUP;
}else if(PM_SUSPEND_BOOTFAST == state){
standby_info.standby_para.axp_src = AXP_BOOTFAST_WAKEUP;
}
standby_info.standby_para.event_enable = (SUSPEND_WAKEUP_SRC_EXINT | SUSPEND_WAKEUP_SRC_ALARM); //唤醒源为外部中断或者 alarm
if (standby_timeout != 0)
{
standby_info.standby_para.event_enable = (SUSPEND_WAKEUP_SRC_EXINT | SUSPEND_WAKEUP_SRC_ALARM | SUSPEND_WAKEUP_SRC_TIMEOFF);
standby_info.standby_para.time_off = standby_timeout;
}
/* 跳转到 SRAM 运行 standby 代码,此时会正式进入休眠状态 */
standby(&standby_info);
/* 到这里已经从休眠状态返回了,重新使能看门狗 */
dogMode = pm_enable_watchdog();
return 0;
}
搜索 standby_bin_start 得到在 arch\arm\mach-sun7i\pm\standby.S
里面有
.globl standby_bin_start standby_bin_start: .incbin "arch/arm/mach-sun7i/pm/standby/standby.bin" // 包含二进制文件 .globl standby_bin_end standby_bin_end: .align 2
进入 arch/arm/mach-sun7i/pm/standby/ 查看链接文件得知 standby.bin 的主函数为该目录下的 standby.c 文件中的 main 函数
/*
*********************************************************************************************************
* standby 主处理程序段(截取部分主体代码)
* Description: standby main process entry.
* Arguments : arg pointer to the parameter that transfered from sys_pwm module.
* Returns : none
* Note : 该函数的参数是上面 standby(&standby_info); 一句传入的结构体
*********************************************************************************************************
*/
int main(struct aw_pm_info *arg)
{
char *tmpPtr;
tmpPtr = (char *)&__bss_start;
/* 冲洗指令与数据 TLB,分别有 32 项数据 TLB 与 指令 TLB吗, TLB 通常是轮流分配的。最旧的输入条目总是在下一次分配 */
mem_flush_tlb();
/* 重新加载 TLB */
mem_preload_tlb();
/* 清除 bss 段 */
do{*tmpPtr ++ = 0;}while(tmpPtr <= (char *)&__bss_end);
/* 从 DRAM 拷贝传入的结构体 */
standby_memcpy(&pm_info, arg, sizeof(pm_info));
pm_info.standby_para.event = 0;
/* copy standby code & data to load tlb */
//standby_memcpy((char *)&__standby_end, (char *)&__standby_start, (char *)&__bss_end - (char *)&__bss_start);
/* backup dram traning area */
standby_memcpy((char *)dram_traning_area_back, (char *)DRAM_BASE_ADDR, DRAM_TRANING_SIZE);
/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */
/* init module before dram enter selfrefresh */
/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */
/* initialise standby modules */
standby_clk_init();
standby_clk_apbinit();
mem_int_init();
standby_tmr_init();
standby_power_init(pm_info.standby_para.axp_src);
/* 根据前面的设置,选择性初始化唤醒源 */
if(pm_info.standby_para.event_enable & SUSPEND_WAKEUP_SRC_EXINT){
mem_enable_int(INT_SOURCE_EXTNMI);
}
... ...
/* 保存栈指针, 转换为 sram 的栈空间 */
sp_backup = save_sp();
/* 使能 dram 进入自刷新模式 */
dram_power_save_process(0);
//mctl_self_refresh_entry();
/* 进入 standby 模式,里面采用汇编的 WFI 指令实现暂停运行 */
standby();
/* 使能看门狗,保护 dram 的恢复过程 */
standby_tmr_enable_watchdog();
/* 恢复 dram 的运行 */
//dram_power_up_process();
//mctl_self_refresh_exit();
init_DRAM(&pm_info.dram_para);
/* 关闭看门狗 */
standby_tmr_disable_watchdog();
/* 恢复备份的栈指针,重新转到 dram */
restore_sp(sp_backup);
/* 根据设置选择性恢复模块 */
if(pm_info.standby_para.event_enable & SUSPEND_WAKEUP_SRC_USB){
standby_usb_exit();
}
... ...
standby_power_exit(pm_info.standby_para.event_enable);
standby_tmr_exit();
mem_int_exit();
standby_clk_apbexit();
standby_clk_exit();
/* restore dram traning area */
standby_memcpy((char *)DRAM_BASE_ADDR, (char *)dram_traning_area_back, DRAM_TRANING_SIZE);
/* 报告唤醒源 */
arg->standby_para.event = pm_info.standby_para.event;
return 0;
}
samsung 的代码
这里只是列举了 platform_suspend_ops->enter 部分的代码,其余的不是很重要,代码里面做的事情有
- 检查是否配置了唤醒源
- 保存一些寄存器的状态。比如 gpio、uart 等
- 配置唤醒源
- 做一些 cpu 的准备工作
- 清 cache,把 cpu 控制的各个模块关掉,如 uart 等等,给第 3 位写 1 的时候会进入 sleep 模式
- 保存栈,通用寄存器等,cpu 进入休眠状态
- 从 sleep 模式恢复的时候恢复 uart、gpio 等的状态
/* s3c_pm_enter
* sleep/resume 核心处理模块
*/
static int s3c_pm_enter(suspend_state_t state)
{
/* 检查是否有唤醒源 */
if (!any_allowed(s3c_irqwake_intmask, s3c_irqwake_intallow) &&
!any_allowed(s3c_irqwake_eintmask, s3c_irqwake_eintallow)) {
printk(KERN_ERR "%s: No wake-up sources!\n", __func__);
printk(KERN_ERR "%s: Aborting sleep\n", __func__);
return -EINVAL;
}
/* 保存所有没被驱动涉及的必需的核心寄存器 */
samsung_pm_save_gpios();
samsung_pm_saved_gpios();
s3c_pm_save_uarts();
s3c_pm_save_core();
/* 设置中断唤醒模块 */
s3c_pm_configure_extint();
s3c_pm_arch_prepare_irqs();
/* 见下面注释 */
pm_cpu_prep();
/* 清除 cache */
flush_cache_all();
s3c_pm_check_store(); //校验一下 crc
/* 使 cpu 进入 sleep 模式 */
s3c_pm_arch_stop_clocks(); // 停止模块运行
cpu_suspend(0, pm_cpu_sleep); // 正式进入 sleep 模式,代码注释见下面
/* 恢复系统状态 */
s3c_pm_restore_core();
s3c_pm_restore_uarts();
samsung_pm_restore_gpios();
s3c_pm_restored_gpios();
/* check what irq (if any) restored the system */
s3c_pm_arch_show_resume_irqs();
S3C_PMDBG("%s: post sleep, preparing to return\n", __func__);
/* LEDs should now be 1110 */
s3c_pm_debug_smdkled(1 << 1, 0);
s3c_pm_check_restore();
/* ok, let's return from sleep */
S3C_PMDBG("S3C PM Resume (post-restore)\n");
return 0;
}
下面函数对应 enter 函数里面的 pm_cpu_prep();
一句,这里面主要做的事情是把恢复后要运行的第一个函数的地址保存到 S3C2410_GSTATUS3
寄存器里面,这个寄存器是约定好的恢复函数的存放位置,里面存放的是物理地址,使用 virt_to_phy()
来转换。
static void s3c2410_pm_prepare(void)
{
/* GSTATUS3 寄存器里面保存唤醒后要执行的代码位置,这里就是 s3c_cpu_resume 函数 */
__raw_writel(virt_to_phys(s3c_cpu_resume), S3C2410_GSTATUS3);
/* 不同的芯片各有相应的微调设置 */
if (machine_is_h1940()) {
void *base = phys_to_virt(H1940_SUSPEND_CHECK);
unsigned long ptr;
unsigned long calc = 0;
/* generate check for the bootloader to check on resume */
for (ptr = 0; ptr < 0x40000; ptr += 0x400)
calc += __raw_readl(base+ptr);
__raw_writel(calc, phys_to_virt(H1940_SUSPEND_CHECKSUM));
}
... ...
}
cpu_suspend(0, pm_cpu_sleep);
里面会调用到 __cpu_suspend(0, pm_cpu_sleep);
该函数如下所示。执行完该函数之后,该部分的栈空间保存的东西为
高地址 lr cpu_suspend 函数内的返回地址 r11 通用寄存器 ... r4 cpu_suspend_size cpu suspend 指定的寄存器 phys resume fn 恢复函数的物理地址(cpu_arm920_do_resume) sp 当前栈的虚拟地址 pgd 一个结构体,page global directory 低地址
sleep_save_sp 指向的内存空间保存的是 sp 的物理地址,它指向 pgd
最后的 ldmfd sp!, {r0, pc}
跳转执行 s3c2410_cpu_suspend
值得一提的是 __cpu_suspend_save
里面的 virt_to_phys(cpu_do_resume);
相关的代码为
#define cpu_do_resume __glue(CPU_NAME,_do_resume)
#define __glue(name,fn) ____glue(name,fn)
#define ____glue(name,fn) name##fn
#ifdef CONFIG_CPU_ARM920T
# ifdef CPU_NAME
# undef MULTI_CPU
# define MULTI_CPU
# else
# define CPU_NAME cpu_arm920
# endif
#endif
展开后 cpu_do_resume 等同于 cpu_arm920_do_resume ,该函数在 proc-arm920.S
里面定义
/*
* 保存 cpu 状态。保存 cpu 通用寄存器,在内核栈上分配空间来保存 cpu 指定寄存器和其它唤醒时用到的数据
* r0 = suspend 函数参数
* r1 = suspend 函数的地址
*/
ENTRY(__cpu_suspend)
stmfd sp!, {r4 - r11, lr} @保存 r4-r11,lr 到栈里面
#ifdef MULTI_CPU
ldr r10, =processor
ldr r4, [r10, #CPU_SLEEP_SIZE] @ size of CPU sleep state
#else
ldr r4, =cpu_suspend_size
#endif
mov r5, sp @ 当前虚拟 SP 地址
add r4, r4, #12 @ pgd, virt sp, phys resume fn 空间
sub sp, sp, r4 @ allocate CPU state on stack
stmfd sp!, {r0, r1} @ save suspend func arg and pointer
add r0, sp, #8 @ save pointer to save block
mov r1, r4 @ size of save block
mov r2, r5 @ virtual SP
ldr r3, =sleep_save_sp
#ifdef CONFIG_SMP
ALT_SMP(mrc p15, 0, lr, c0, c0, 5)
ALT_UP(mov lr, #0)
and lr, lr, #15
add r3, r3, lr, lsl #2
#endif
bl __cpu_suspend_save @根据 r0-r3 的值正式进行保存工作
adr lr, BSYM(cpu_suspend_abort) @BSYM,thumb 指令集中对 adr 的使用
ldmfd sp!, {r0, pc} @ 调用 suspend 函数
ENDPROC(__cpu_suspend)
调用下面的函数之后就可以真正的进入 sleep 模式了,里面实现了 SDRAM 自刷新以及保护信号的开启,最终往芯片的 CLKCON 寄存器的第三位写入 1 进入休眠
ENTRY(s3c2410_cpu_suspend)
@@ prepare cpu to sleep
ldr r4, =S3C2410_REFRESH
ldr r5, =S3C24XX_MISCCR
ldr r6, =S3C2410_CLKCON
ldr r7, [ r4 ] @ get REFRESH (and ensure in TLB)
ldr r8, [ r5 ] @ get MISCCR (and ensure in TLB)
ldr r9, [ r6 ] @ get CLKCON (and ensure in TLB)
orr r7, r7, #S3C2410_REFRESH_SELF @ SDRAM 自刷新
orr r8, r8, #S3C2410_MISCCR_SDSLEEP @ SDRAM 保护信号
orr r9, r9, #S3C2410_CLKCON_POWER @ 断电
teq pc, #0 @ 试执行使指令数据加载到 cache 以及 TLB 里面
@ 在自刷新与断电之间还有指令要执行,所以才要试执行
bl s3c2410_do_sleep
teq r0, r0 @ 正式执行指令
b s3c2410_do_sleep @ 进入睡眠状态
@@ align next bit of code to cache line
.align 5
s3c2410_do_sleep:
streq r7, [ r4 ] @ SDRAM sleep command
streq r8, [ r5 ] @ SDRAM power-down config
streq r9, [ r6 ] @ CPU sleep
1: beq 1b
mov pc, r14
恢复过程
设置 cpsr_c 也就是 cpu 运行模式,调用 cpu_resume
ENTRY(s3c_cpu_resume)
mov r0, #PSR_I_BIT | PSR_F_BIT | SVC_MODE
msr cpsr_c, r0
@@ load UART to allow us to print the two characters for
@@ resume debug
mov r2, #S3C24XX_PA_UART & 0xff000000
orr r2, r2, #S3C24XX_PA_UART & 0xff000
#if 0
/* SMDK2440 LED set */
mov r14, #S3C24XX_PA_GPIO
ldr r12, [ r14, #0x54 ]
bic r12, r12, #3<<4
orr r12, r12, #1<<7
str r12, [ r14, #0x54 ]
#endif
#ifdef CONFIG_DEBUG_RESUME
mov r3, #'L'
strb r3, [ r2, #S3C2410_UTXH ]
1001:
ldrb r14, [ r3, #S3C2410_UTRSTAT ]
tst r14, #S3C2410_UTRSTAT_TXE
beq 1001b
#endif /* CONFIG_DEBUG_RESUME */
b cpu_resume
cpu_resume 函数提取被保存在 sleep_save_sp 的 sp 物理地址,取出 phys resume fn、sp、pgd 分别放入 pc、sp、r1 里面,于是代码继续跳转到 cpu_arm920_do_resume
执行
/*
* Note: 下面部分代码被放到了 .data 段里面,这样可以保证当我们不能依靠 MMU 的时候也可以访问到 sleep_save_sp。当然也可以把 sleep_save_sp 放到 .text 段,但是可能有一些设置使得 .text 段变得只读,由于休眠唤醒过程需要写入操作,所以还是放到 .data 段
*/
.data
.align
ENTRY(cpu_resume)
#ifdef CONFIG_SMP
adr r0, sleep_save_sp
ALT_SMP(mrc p15, 0, r1, c0, c0, 5)
ALT_UP(mov r1, #0)
and r1, r1, #15
ldr r0, [r0, r1, lsl #2] @ stack phys addr
#else
ldr r0, sleep_save_sp @ 被保存的栈的物理地址
#endif
setmode PSR_I_BIT | PSR_F_BIT | SVC_MODE, r1 @ set SVC, irqs off
@ load phys pgd, stack, resume fn
ARM( ldmia r0!, {r1, sp, pc} )
THUMB( ldmia r0!, {r1, r2, r3} )
THUMB( mov sp, r2 )
THUMB( bx r3 )
ENDPROC(cpu_resume)
sleep_save_sp:
.rept CONFIG_NR_CPUS
.long 0 @ preserve stack phys ptr here
.endr
执行上面的代码会依次调用到
cpu_arm920_do_resume 使无效 TLB,cache cpu_resume_mmu 开启 mmu,icache cpu_resume_after_mmu cpu 初始化,ldmfd sp!, {r4 - r11, pc} 取回休眠前保存的值,返回到 cpu_suspend 函数继续执行
s3c2440 手册指出的睡眠过程
1. 配置 GPIO 以适应 SLEEP 模式
2. 在 INTMSK 寄存器中屏蔽所有中断,因为在 sleep 模式下引脚不起中断作用,只用于唤醒系统
3. 配置唤醒源,包括 RTC alarm (唤醒源对应的 EINTMASK 位不能被屏蔽,以便 SRCPND 或者 EINTPEND 相应位能够被设置)
4. 设置 USB 为 suspend 模式 (MISCCR[13:12]=11b)
5. 保存一些有用的值到 GSTATUS[4:3] 寄存器里面. 这些寄存器在 SLEEP 模式下会被保护
6. 设置 MISCCR[1:0] 使数据总线上拉,D[31:0]. 如果有外部总线,比如 74LVCH162245,要关闭其上拉电阻.否则的话就开启上拉电阻.此外,内存相关引脚被设置为两种状态,一种是 Hi-z,另一种是 Inactive 状态
7. 清 LCDCON1.ENVID 位以关闭 LCD
8. 读 rREFRESH 和 rCLKCON 寄存器来填充 TLB.
9. 设置 REFRESH[22]=1b 使 SDRAM 进入自刷新模式
10. 等待 SDRAM 正式进入自刷新模式
11. 设置 MISCCR[19:17]=111b 来保护 SDRAM 信号(SCLK0,SCLK1 and SCKE)
12. 设置 CLKCON 寄存器相应的 SLEEP 模式位从睡眠模式返回
1. 当唤醒源被触发的时候会导致 reset 事件发生,等同于按下系统复位键
2. 检查 GSTATUS2[2] 来判断是按下了复位键还是从 sleep 模式返回
3. 设置 MISCCR[19:17]=000b 来释放 SDRAM 信号保护
4. 配置 SDRAM 控制器
5. 等待 SDRAM 自刷新模式被释放,一般需要一个行刷新周期
6. 使用 GSTATUS[3:4] 里面存放的信息,sleep 模式下这两个寄存器被保护起来了
7. 判断唤醒源