寫在前面
爲了理清新平臺系統休眠和喚醒的流程,通過學習其他平臺的電源管理方法,曲徑通幽, 達到目的.
剛接手新平臺,且相應的資料不多,很容易讓人力不從心;我在網上尋找了學習資源,發現韋東山對S3C2440的驅動講解有相關的內容,口碑也不錯,可以作爲一個切入點.
不同平臺一定會有平臺的差異,不同的Linux內核版本之間也會有相應的差異,但思路是一致的,可以總結出來,幫助理解電源管理的開發使用.
本文總結關於系統休眠和喚醒的流程以及開發方法.
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 Sleep模式進入與喚醒
對於S3C2440 的power manager有三種模式,如下圖所示,
我們選擇在u-boot中實現Sleep mode作爲suspend, 也就是對應Linux內核實現的mem模式的suspend. 實驗的效果是:在u-boot命令行運行suspend指令後系統進入Sleep Mode; 當按下按鍵後,系統喚醒,繼續接着休眠前的地方執行下去.
進入Sleep Mode的流程
實現代碼如下:
/*
* [email protected], www.100ask.net
*
*/
#include <common.h>
#include <command.h>
#include <def.h>
#include <nand.h>
#include <s3c24x0.h>
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;
}
代碼解釋
- 對於進入Sleep Mode的第5步,主要目的是保存休眠前的PC指針到GSTATUS寄存器,以便喚醒後恢復現場. 這一步的實現,放到 s3c2440_cpu_suspend函數中.
其中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 Generic PM之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電源管理
suspend流程
如下圖所示,實際上寫入文件調用的函數就是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
Platform dependent PM(針對CPU芯片相關)
//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流程
驅動程序裏喚醒相關的電源管理函數的調用過程: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 <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/irq.h>
#include <asm/gpio.h>
#include <asm/io.h>
//#include <asm/arch/regs-gpio.h>
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設置寄存器使能中斷功能. (注意:這些中斷引腳是支持喚醒功能的)
Device PM(針對每一個驅動)
上面針對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");
代碼分析
- 在LCD驅動的基礎上註冊了一個平臺設備和驅動,當系統進入休眠模式時會調用lcd_suspend函數關閉燈光,當喚醒退出休眠模式時會調用lcd_resume打開燈光.
- 重點在於第13行的結構體.
總結
上面介紹的是系統休眠或喚醒模式下的電源管理, 對於不同平臺,都需要按照DataSheet的說明選擇休眠模式,修改u-boot和kernel.
那在正常運行模式下,能否單獨對設備進行電源管理呢,請期待下一節更新Linux電源管理-Runtime PM