为了理清新平台系统休眠和唤醒的流程,通过学习其他平台的电源管理方法,曲径通幽, 达到目的.
刚接手新平台,且相应的资料不多,很容易让人力不从心;我在网上寻找了学习资源,发现韦东山对S3C2440的驱动讲解有相关的内容,口碑也不错,可以作为一个切入点.
不同平台一定会有平台的差异,不同的Linux内核版本之间也会有相应的差异,但思路是一致的,可以总结出来,帮助理解电源管理的开发使用.
本文总结关于系统休眠和唤醒的流程以及开发方法.
下图所示为Linux电源管理基本框架. 详细请参考Linux电源管理(1)_整体架构
本文重点介绍关于Generic PM的使用.
Generic PM,就是Power Management,如Power Off、
Suspend to RAM、Suspend to Disk、Hibernate等.
关于Suspend, Linux内核提供了四种Suspend: Freeze、Standby、STR(Suspend to RAM)和STD(Suspend to Disk),如下表 . 在用户空间向”/sys/power/state”文件分别写入”freeze”、”standby”、”mem”和"disk",即可触发它们。
模式 | 描述 |
---|---|
freeze | 冻结I/O设备,将它们置于低功耗状态,使处理器进入空闲状态,唤醒最快,耗电比其它standby, mem, disk方式高 |
standby | 除了冻结I/O设备外,还会暂停系统,唤醒较快,耗电比其它 mem, disk方式高 |
mem | 将运行状态数据存到内存,并关闭外设,进入等待模式,唤醒较慢,耗电比disk方式高 |
disk | 将运行状态数据存到硬盘,然后关机,唤醒最慢. 对于嵌入式系统,由于没有硬盘,所以一般不支持 |
Linux内核提供的电源管理框架引入了面向对象的思想,理解起来会困难.为此,很有必要在抛开复杂框架的基础上,通过实验验证suspend的结果,总结suspend的流程后,帮助理解linux的supend流程.
对于S3C2440 的power manager有三种模式,如下图所示,
我们选择在u-boot中实现Sleep mode作为suspend, 也就是对应Linux内核实现的mem模式的suspend. 实验的效果是:在u-boot命令行运行suspend指令后系统进入Sleep Mode; 当按下按键后,系统唤醒,继续接着休眠前的地方执行下去.
实现代码如下:
/*
* [email protected], www.100ask.net
*
*/
#include
#include
#include
#include
#include
extern void s3c2440_cpu_suspend(void);
int do_suspend (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]);
U_BOOT_CMD(
suspend, 1, 0, do_suspend,
"suspend - suspend the board\n",
" - suspend the board"
);
int do_suspend (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
/* 休眠的实现: */
/* 对于NAND启动: 要设置EINT23,22,21为输入引脚 */
rGPGCON &= ~((3<<30) | (3<<28) | (3<<26));
/* 1. 配置GPIO模式,用于唤醒CPU的引脚要设为中断功能 */
/* JZ2440只有S2/S3/S4可用作唤醒源,设置它们对应的GPIO用于中断模式 */
/*这几个IO对于着按键,也就是实现按键唤醒*/
/* EINT0 EINT2*/
rGPFCON &= ~((3<<0) | (3<<4));
rGPFCON |= ((2<<0) | (2<<4));
/*EINT11*/
rGPGCON &= ~(3<<6);
rGPGCON |= (2<<6);
/* 2. 设置INTMSK屏蔽所有中断: 在sleep模式下,这些引脚只是用于唤醒系统,当CPU正常运行时可以重新设置INTMSK让这些引脚用于中断功能 */
rINTMSK = ~0;
/* 3. 配置唤醒源 , 为了能在休眠模式下被唤醒*/
rEXTINT0 |= (6<<0) | (6<<8); /* EINT0,2双边沿触发 */
rEXTINT1 |= (6<<12); /* EINT11双边沿触发 */
/* 4. 设置MISCCR[13:12]=11b, 使得USB模块进入休眠 */
rMISCCR |= (3<<12);
/* 5. 在GSTATUS[4:3]保存某值, 它们可以在系统被唤醒时使用 */
//rGSTATUS3 = ; /* 唤醒时首先执行的函数的地址 */
//rGSTATUS4 = ; /* */
/* 6. 设置 MISCCR[1:0] 使能数据总线的上拉电阻 */
rMISCCR &= ~(3);
/* 7. 清除 LCDCON1.ENVID 以停止LCD */
rLCDCON1 &= ~1;
/* 8~12使用汇编在s3c2440_cpu_suspend函数来实现,参考内核源码:
* arch\arm\mach-s3c2410\sleep.S
*/
/* 8. 读这2个寄存器: rREFRESH and rCLKCON, 以便填充TLB
* 如果不使用MMU的话,这个目的可以忽略
*/
/* 9. 设置 REFRESH[22]=1b,让SDRAM进入self-refresh mode */
/* 10. 等待SDRAM成功进入self-refresh mode */
/* 11.设置 MISCCR[19:17]=111b以保护SDRAM信号(SCLK0,SCLK1 and SCKE) */
/* 12. 设置CLKCON的SLEEP位让系统进入sleep mode */
printf("suspend ...");
delay(1000000); //保证printf打印完整
s3c2440_cpu_suspend(); /* 执行到这里就不会返回,直到CPU被唤醒 */
/* 恢复运行: 重新初始化硬件 */
serial_init();
printf("wake up\n");
return 0;
}
代码解释
其中s3c2440_cpu_suspend函数的功能是让系统真正进入休眠状态,实现流程如下:
/* suspend.S [email protected], www.100ask.net
* s3c2410_cpu_suspend
* put the cpu into sleep mode
*/
#define S3C2440_REFRESH_SELF (1<<22)
#define S3C2440_MISCCR_SDSLEEP (7<<17)
#define S3C2440_CLKCON_POWER (1<<3)
#define GSTATUS2 (0x560000B4)
#define GSTATUS3 (0x560000B8)
#define GSTATUS4 (0x560000BC)
#define REFRESH (0x48000024)
#define MISCCR (0x56000080)
#define CLKCON (0x4C00000C)
.globl s3c2440_cpu_suspend @将s3c2440_cpu_suspend函数声明为全局函数
@@ prepare cpu to sleep
s3c2440_cpu_suspend:
stmdb sp!, { r4-r12,lr } @保存寄存器r4 - r12到栈中
/* GSTATUS3中存放唤醒时要执行的函数 */
ldr r0, =s3c2440_do_resume
ldr r1, =GSTATUS3
str r0, [r1]
/* GSTATUS4中存放休眠前的栈信息 */
ldr r1, =GSTATUS4
str sp, [r1]
/* 8. 读这2个寄存器: rREFRESH and rCLKCON, 以便填充TLB
* 如果不使用MMU的话,这个目的可以忽略
*/
/* 9. 设置 REFRESH[22]=1b,让SDRAM进入self-refresh mode */
/* 10. 等待SDRAM成功进入self-refresh mode */
/* 11.设置 MISCCR[19:17]=111b以保护SDRAM信号(SCLK0,SCLK1 and SCKE) */
/* 12. 设置CLKCON的SLEEP位让系统进入sleep mode */
ldr r4, =REFRESH
ldr r5, =MISCCR
ldr r6, =CLKCON
ldr r7, [ r4 ] @ get REFRESH
ldr r8, [ r5 ] @ get MISCCR
ldr r9, [ r6 ] @ get CLKCON
orr r7, r7, #S3C2440_REFRESH_SELF @ SDRAM sleep command
orr r8, r8, #S3C2440_MISCCR_SDSLEEP @ SDRAM power-down signals
orr r9, r9, #S3C2440_CLKCON_POWER @ power down command
teq pc, #0 @ first as a trial-run to load cache
bl s3c2440_do_sleep
teq r0, r0 @ now do it for real
b s3c2440_do_sleep @
@@ align next bit of code to cache line
.align 5
s3c2440_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
s3c2440_do_resume:
/* 从start.S的wake_up返回到s3c2440_do_resume函数 */
/* 从GSTATUS4取出栈信息 */
ldr r1, =GSTATUS4
ldr sp, [r1]
/*恢复栈*/
ldmia sp!, { r4-r12,pc }
从以上的描述中的第2点可以得知, 系统从Sleep模式退出是需要重新启动u-boot的, 区别是power-up还是wake-up的方法是读取GSTATUS2寄存器.如果值为1,代表是从Sleep Mode过来的,就会执行相应的流程恢复休眠前的现场; 反之,则执行正常启动流程.
下面是关于u-boot启动代码的修改
/*cpu/arm920t/start.S*/
+#define GSTATUS2 (0x560000B4)
+#define GSTATUS3 (0x560000B8)
+#define GSTATUS4 (0x560000BC)
+
+#define REFRESH (0x48000024)
+#define MISCCR (0x56000080)
+#define CLKCON (0x4C00000C)
.globl _start
_start: b reset
@@ -167,6 +178,25 @@ reset:
#endif
#endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */
+#ifndef CONFIG_SKIP_LOWLEVEL_INIT
+ /* 在u-boot未改动前,clock_init C函数的执行
+ 需要在SDRAM设置栈以后, 由于
+ Sleep模式SDRAM处于自刷新状态,为了避免破环状态,
+ 我们提前调用clock_init,
+ 并把栈设置在片内内存,
+ 设置SP指向片内内存SRAM */
+ /*对于nand启动,可以这么设置*/
+ ldr sp, =4092
+ /*通过判断指向4092的内存能否读写成功来判断是nand启动还是nor启动.如果是nand启动,sp指向4092,如果是nor启动,sp指向0x40000000+4096*/
+ ldr r0, =0x12345678
+ str r0, [sp]
+ ldr r1, [sp]
+ cmp r0, r1
+ /*对于nor启动, 可以这么设置*/
+ ldrne sp, =0x40000000+4096
+ bl clock_init
+#endif
+
+ /* 2. 根据 GSTATUS2[1]判断是复位还是唤醒 */
+ ldr r0, =GSTATUS2
+ ldr r1, [r0]
+ tst r1, #(1<<1) /* r1 & (1<<1) */
+ bne wake_up
+
+
+
/*
* we do sys-critical inits only at reboot,
* not when booting from ram!
@@ -190,7 +220,7 @@ stack_setup:
sub sp, r0, #12 /* leave 3 words for abort-stack */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
- bl clock_init
+// bl clock_init
#endif
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
@@ -260,6 +290,31 @@ SetLoadFlag:
_start_armboot: .word start_armboot
+/* 1. 按下按键 */
+wake_up:
str r1, [r0] /* clear GSTATUS2 */
+ /* 3. 设置 MISCCR[19:17]=000b, 以释放SDRAM信号 */
+ ldr r0, =MISCCR
+ ldr r1, [r0]
+ bic r1, r1, #(7<<17)
+ str r1, [r0]
+
+ /* 4. 配置s3c2440的memory controller */
+ bl cpu_init_crit
+
+ /* 5. 等待SDRAM退出self-refresh mode */
+ mov r0, #1000
+1: subs r0, r0, #1
+ cmp r0, #0
+ bne 1b
+
+ /* 6. 根据GSTATUS[3:4]的值来运行休眠前的函数 PC指针指向 s3c2440_do_resume
+ */
+ ldr r0, =GSTATUS3
+ ldr r1, [r0]
+ mov pc, r1
+
+
在u-boot命令行下,
进入suspend模式,休眠.
按下按键,退出suspend模式,唤醒.
在了解S3C2440 u-boot下添加休眠和唤醒的流程后,总结下来也就是两幅图:
对于Linux的Generic PM框架,本质是也就是把上面这部分与板级(platform)有关的内容放置到框架的相应位置(也就是下图的Platform dependent PM的位置),就可以实现在Linux下的suspend和resume了.
介绍一下Generic PM框架,参考Linux电源管理(6)_Generic PM之Suspend功能
下面以suspend to RAM为例,分析从用户空间ehco > mem /sys/power/state进入休眠再到按键执行唤醒的过程. 参考Linux电源管理
如下图所示,实际上写入文件调用的函数就是state_store.
驱动程序里休眠相关的电源管理函数的调用过程:prepare—>suspend—>suspend_late—>suspend_noirq
其中图中最下面的一行:suspend_ops->enter就是对应platform dependent PM相关的代码,把u-boot中进入休眠模式的代码移植过来即可.
在S3C2440中对应的platform depend PM代码在arch/arm/plat-samsung/pm.c
//arch/arm/plat-samsung/pm.c kernel version:3.4.2
...
static const struct platform_suspend_ops s3c_pm_ops = {
.enter = s3c_pm_enter,
.prepare = s3c_pm_prepare, //对应图中的suspend_ops->enter,系统进入休眠模式调用的platform dependent PM相关的代码
.finish = s3c_pm_finish,
.valid = suspend_valid_only_mem,
};
static int s3c_pm_enter(suspend_state_t state)
{
/* ensure the debug is initialised (if enabled) */
s3c_pm_debug_init();
S3C_PMDBG("%s(%d)\n", __func__, state);
if (pm_cpu_prep == NULL || pm_cpu_sleep == NULL) {
printk(KERN_ERR "%s: error: no cpu sleep function\n", __func__);
return -EINVAL;
}
/* check if we have anything to wake-up with... bad things seem
* to happen if you suspend with no wakeup (system will often
* require a full power-cycle)
*/
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;
}
/* save all necessary core registers not covered by the drivers */
samsung_pm_save_gpios();
samsung_pm_saved_gpios();
s3c_pm_save_uarts();
s3c_pm_save_core();
/* set the irq configuration for wake */
s3c_pm_configure_extint();
S3C_PMDBG("sleep: irq wakeup masks: %08lx,%08lx\n",
s3c_irqwake_intmask, s3c_irqwake_eintmask);
s3c_pm_arch_prepare_irqs();
/* call cpu specific preparation */
pm_cpu_prep();
/* flush cache back to ram */
flush_cache_all();
s3c_pm_check_store();
/* send the cpu to sleep... */
s3c_pm_arch_stop_clocks();
/* this will also act as our return point from when
* we resume as it saves its own register state and restores it
* during the resume. */
cpu_suspend(0, pm_cpu_sleep);
/* restore the system state */
s3c_pm_restore_core();
s3c_pm_restore_uarts();
samsung_pm_restore_gpios();
s3c_pm_restored_gpios();
s3c_pm_debug_init();
/* 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;
}
代码说明:
其中27-32行, 检查是否设置了唤醒源,如果没有,就直接退出,禁止进入休眠模式. 对于设置唤醒源,在后面按键驱动中会介绍如何设置唤醒源.
34-67行, 按照进入Sleep Mode的流程执行相应的工作,一旦调用到67行,跳转到pm_cpu_sleep, 系统进入休眠状态. 直到有唤醒源,才能唤醒,继续往下执行.
驱动程序里唤醒相关的电源管理函数的调用过程:resume_noirq—>resume_early—>resume->complete
对于上面arch/arm/plat-samsung/pm.c中的代码的71-90行,就是完成上图的resume过程.
/* 参考drivers\input\keyboard\gpio_keys.c */
/*参考www.100ask.com*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//#include
struct pin_desc{
int irq;
char *name;
unsigned int pin;
unsigned int key_val;
};
struct pin_desc pins_desc[4] = {
{IRQ_EINT0, "S2", S3C2410_GPF(0), KEY_L},
{IRQ_EINT2, "S3", S3C2410_GPF(2), KEY_S},
{IRQ_EINT11, "S4", S3C2410_GPG(3), KEY_ENTER},
{IRQ_EINT19, "S5", S3C2410_GPG(11), KEY_LEFTSHIFT},
};
static struct input_dev *buttons_dev;
static struct pin_desc *irq_pd;
static struct timer_list buttons_timer;
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
/* 10ms后启动定时器 */
irq_pd = (struct pin_desc *)dev_id;
mod_timer(&buttons_timer, jiffies+HZ/100);
return IRQ_RETVAL(IRQ_HANDLED);
}
static void buttons_timer_function(unsigned long data)
{
struct pin_desc * pindesc = irq_pd;
unsigned int pinval;
if (!pindesc)
return;
pinval = s3c2410_gpio_getpin(pindesc->pin);
if (pinval)
{
/* 松开 : 最后一个参数: 0-松开, 1-按下 */
input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
input_sync(buttons_dev);
}
else
{
/* 按下 */
input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
input_sync(buttons_dev);
}
}
static int buttons_init(void)
{
int i;
/* 1. 分配一个input_dev结构体 */
buttons_dev = input_allocate_device();;
/* 2. 设置 */
/* 2.1 能产生哪类事件 */
set_bit(EV_KEY, buttons_dev->evbit);
set_bit(EV_REP, buttons_dev->evbit);
/* 2.2 能产生这类操作里的哪些事件: L,S,ENTER,LEFTSHIT */
set_bit(KEY_L, buttons_dev->keybit);
set_bit(KEY_S, buttons_dev->keybit);
set_bit(KEY_ENTER, buttons_dev->keybit);
set_bit(KEY_LEFTSHIFT, buttons_dev->keybit);
/* 3. 注册 */
input_register_device(buttons_dev);
/* 4. 硬件相关的操作 */
init_timer(&buttons_timer);
buttons_timer.function = buttons_timer_function;
add_timer(&buttons_timer);
for (i = 0; i < 4; i++)
{
request_irq(pins_desc[i].irq, buttons_irq, (IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING), pins_desc[i].name, &pins_desc[i]);
}
/* 指定这些中断可以用于唤醒系统 */
irq_set_irq_wake(IRQ_EINT0, 1);
irq_set_irq_wake(IRQ_EINT2, 1);
irq_set_irq_wake(IRQ_EINT11, 1);
return 0;
}
static void buttons_exit(void)
{
int i;
irq_set_irq_wake(IRQ_EINT0, 0);
irq_set_irq_wake(IRQ_EINT2, 0);
irq_set_irq_wake(IRQ_EINT11, 0);
for (i = 0; i < 4; i++)
{
free_irq(pins_desc[i].irq, &pins_desc[i]);
}
del_timer(&buttons_timer);
input_unregister_device(buttons_dev);
input_free_device(buttons_dev);
}
module_init(buttons_init);
module_exit(buttons_exit);
MODULE_LICENSE("GPL");
代码说明
设置唤醒源最主要的是105 -107行的函数irq_set_irq_wake.
这个函数本质上是通过api设置寄存器使能中断功能. (注意:这些中断引脚是支持唤醒功能的)
上面针对suspend流程的实现是修改Platform dependent PM相关的代码(针对CPU芯片相关), 在休眠时只是执行datasheet中的必要要求,对于一些外设,比如网卡、声卡等并没有关闭.
对于外设级别的电源管理,Linux Generic PM框架提供了相应的接口,只要在注册设备时加入相应参数,即可实现系统休眠或者唤醒时会调用各个驱动所注册的suspend或者resume回调函数.
下面以LCD为例,加入外设电源管理,使得在系统进入Mem休眠模式后,能关闭LCD背光灯,唤醒时打开LCD背光灯重新显示.
//Lcd.c
...//头文件
static struct dev_pm_ops lcd_pm = {
.suspend = lcd_suspend,
.resume = lcd_resume,
};
struct platform_driver lcd_drv = {
.probe = lcd_probe,
.remove = lcd_remove,
.driver = {
.name = "mylcd",
.pm = &lcd_pm,
}
};
static int lcd_probe(struct platform_device *pdev)
{
return 0;
}
static int lcd_remove(struct platform_device *pdev)
{
return 0;
}
static int lcd_suspend(struct device *dev)
{
...
*gpbdat &= ~1; /* 关闭背光 */
return 0;
}
static int lcd_resume(struct device *dev)
{
...
*gpbdat |= 1; /* 输出高电平, 使能背光 */
return 0;
}
static int lcd_init(void)
{
platform_device_register(&lcd_dev);
platform_driver_register(&lcd_drv);
...//LCD相应的初始化
return 0;
}
static void lcd_exit(void)
{
...//LCD相应的注销
platform_device_unregister(&lcd_dev);
platform_driver_unregister(&lcd_drv);
}
module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");
代码分析
上面介绍的是系统休眠或唤醒模式下的电源管理, 对于不同平台,都需要按照DataSheet的说明选择休眠模式,修改u-boot和kernel.
那在正常运行模式下,能否单独对设备进行电源管理呢,请期待下一节更新Linux电源管理-Runtime PM