1. 程式人生 > >Linux的suspend和resume

Linux的suspend和resume

等待 add span post 內核函數 printk 恢復 devices pat

參考:

www.wowotech.net/linux_kenrel/suspend_and_resume.html
www.wowotech.net/linux_kenrel/pm_interface.html

一、基本介紹

1.Window下的睡眠就是Suspend to RAM, 休眠就是Suspend to Disk,Ubuntu中Suspend就是Stand by(沒有實現Suspend to RAM),Hibernate就是Suspend to Disk。
2.設備驅動若是關註睡眠和喚醒功能就要實現suspend和resume函數
3.Linux系統Suspend實現:
  cat /sys/power/state 打印支持的電源管理方式,echo 的時候會讓內核進入某中休眠模式, eg: echo mem > /sys/power/state 對應的內核函數在/sys/power/state中的讀寫方法在kernel/power/main.c中.

power_attr(state);展開:
static struct kobj_attribute state_attr = {
    .attr    = {
        .name = "state",
        .mode = 0644,
    },
    .show    = state_show,
    .store    = state_store,
}

struct kobj_attribute state_attr --> struct attribute * g[] --> struct attribute_group attr_group --> pm_init()
power_kobj 
= kobject_create_and_add("power", NULL); //在/sys目錄下創建了一個power目錄 sysfs_create_group(power_kobj, &attr_group); //在power目錄中創建了這個state文件

二、suspend/resume流程分析

state_store //(kernel/power/main.c)
    pm_suspend //(kernel/power/suspend.c)
        enter_state //(kernel/power/suspend.c)
            valid_state //(kernel/power/suspend.c) 檢查單板是否支持電源管理,就是全局suspend_ops有沒有被賦值,並調用其suspend_ops->valid()
suspend_prepare //(kernel/power/suspend.c) pm_prepare_console //(kernel/power/console.c) pm_notifier_call_chain(PM_SUSPEND_PREPARE) //(kernel/power/main.c) 通知所有關心這個消息的驅動程序!依次調用靜態全局鏈表pm_chain_head中的每一個函數 suspend_freeze_processes //(kernel/power/power.h) 凍結App和內核線程 suspend_devices_and_enter //(kernel/power/suspend.c) 讓設備進入suspend狀態 suspend_ops->begin 如果平臺相關的代碼有begin函數就去調用它,例如Renesas的平臺進入suspend時需要一些預先準備工作,就可以實現這個begin函數 suspend_console //(kernel/printk/printk.c) 串口suspend狀態,此時串口就用不了了 dpm_suspend_start //(drivers/base/power/main.c) dpm_prepare //(drivers/base/power/main.c) 對全局鏈表dpm_list(drivers/base/power/power.c)中的每一個設備都調用其prepare函數,在這裏面可以做一些準備工作 dev->pm_domain->ops.prepare 或 [struct dev_pm_ops ops] dev->type->pm->prepare 或 [struct dev_pm_ops *pm] dev->class->pm->prepare 或 [struct dev_pm_ops *pm] dev->bus->pm->prepare 或 [struct dev_pm_ops *pm] dev->driver->pm->prepare [struct dev_pm_ops *pm] [struct device_driver中的在這,優先級最低] dpm_suspend //(drivers/base/power/main.c) 對全局鏈表dpm_prepared_list中的每一個設備都調用device_suspend() device_suspend //(drivers/base/power/main.c) dpm_wait_for_children //(drivers/base/power/main.c) 等待其每一個孩子進入suspend狀態 dev->pm_domain->ops->suspend 或 dev->type->pm->suspend 或 dev->class->pm->suspend 或 dev->bus->pm->suspend 或 因此自己在寫驅動的時候可以在其pm_domain中或type->pm中或class->pm中或bus->pm中加入suspend函數 suspend_enter //(kernel/power/suspend.c) 設備都進入suspend狀態了接下來就是CPU了 suspend_ops->prepare 單板的prepare函數若存在就調用 dpm_suspend_end //(drivers/base/power/main.c) dpm_suspend_late //(drivers/base/power/main.c) 對全局靜態鏈表dpm_suspended_list中的每一個條目都調用device_suspend_late() device_suspend_late() //(drivers/base/power/main.c) 調用此設備的 dev->pm_domain->ops->suspend_late 或 dev->type->pm->suspend_late 或 dev->class->pm->suspend_late 或 dev->bus->pm->suspend_late 或 dev->driver->pm->suspend_late 或 suspend_ops->prepare_late 調用單板相關的函數,可以做一些清理,若單板不需要也可以不實現它 disable_nonboot_cpus //(kernel/cpu.c) 多核Soc中非用於啟動內核的CPU叫做nonboot_cpu,停止non-boot CPU arch_suspend_disable_irqs //(include/linux/suspend.h)//關閉中斷,extern的,Renesas上沒有實現 syscore_suspend 關閉核心模塊 suspend_ops->enter 調用單板相關的函數,這裏真正進入suspend狀態了,如三星的是s3c_pm_enter(),裏面通過any_allowed()檢測有沒有設置喚醒源 若沒有設置是不允許睡眠的。這個函數下面單獨列出 ======================================================上面是休眠,下面就是喚醒操作了===================================================== 當我們按下某個按鍵並且這個按鍵是喚醒源的話,就會喚醒CPU,從Uboot開始執行 按鍵喚醒源 --> Uboot --> 讀寄存器GSTATUS3 --> 就會執行s3c_cpu_resume() syscore_resume //(drivers/base/syscore.c) 對全局鏈表syscore_ops_list中的每一個node都調用其resume() arch_suspend_enable_irqs //(include/linux/suspend.h) enable_nonboot_cpus //(kernel/cpu.c) suspend_ops->wake 如果單板有對應的wake()就調用 dpm_resume_start(PMSG_RESUME) //(drivers/base/power/main.c) dpm_resume_noirq(state); //(drivers/base/power/main.c) 對全局鏈表dpm_noirq_list中的每一個設備都執行device_resume_noirq device_resume_noirq //(drivers/base/power/main.c) 對每一個設備都調用 dev->pm_domain->ops->resume_noirq 或 dev->type->pm->resume_noirq 或 dev->class->pm->resume_noirq 或 dev->bus->pm->resume_noirq 或 dev->driver->pm->resume_noirq 或 執行完resume_noirq的所有設備都會被放在全局鏈表dpm_late_early_list中 resume_device_irqs //(kernel/irq/pm.c) resume_irqs //(kernel/irq/pm.c) __enable_irq //(kernel/irq/pm.c) 對全局數組irq_desc中的每一個irq都調用__enable_irq,但是Renesas的BSP沒有實現,裏面還有一個野指針 dpm_resume_early(state); //(drivers/base/power/main.c) 對全局鏈表dpm_late_early_list中的每一個元素都執行device_resume_early device_resume_early //(drivers/base/power/main.c) dev->pm_domain->ops->resume_early 或 dev->type->pm->resume_early 或 dev->class->pm->resume_early 或 dev->bus->pm->resume_early 或 dev->driver->pm->resume_early 或 suspend_ops->finish() 如果單板有對應的finish()就調用,三星的對應下面 suspend_test_start //(kernel/power/suspend_test.c) dpm_resume_end(PMSG_RESUME); //(drivers/base/power/main.c) dpm_resume //(drivers/base/power/main.c) 對全局鏈表dpm_suspended_list中的每一個dev都調用device_resume() device_resume //(drivers/base/power/main.c) dev->pm_domain->ops->resume 或 dev->type->pm->resume 或 dev->class->pm->resume 或 dev->bus->pm->resume 或 dev->driver->pm->resume 或 然後將所有的設備移動到全局鏈表dpm_prepared_list中 dpm_complete //(drivers/base/power/main.c) 對全局鏈表dpm_prepared_list中的每一個設備都調用device_complete() device_complete //(drivers/base/power/main.c) dev->pm_domain->ops.complete 或 dev->type->pm.complete 或 dev->class->pm.complete 或 dev->bus->pm.complete 或 dev->driver->pm.complete 或 suspend_test_finish //(kernel/power/suspend_test.c) 打印一些log出來 resume_console //(kernel/printk.c) console_unlock //(kernel/printk.c) call_console_drivers //(kernel/printk.c) 關閉本地中斷獲取spin鎖後調用控制臺打印函數以poll方式打印內核log suspend_ops->end() 如果單板有對應的end()就調用 suspend_ops->recover() 如果dpm_suspend_start失敗或者suspend_test失敗,單板有對應的recover()就調用 suspend_finish //(kernel/power/suspend.c) suspend_thaw_processes(); //(kernel/power/power.h)喚醒應用程序 pm_notifier_call_chain(PM_POST_SUSPEND); //(kernel/power/main.c) 通知關註這個事件的App程序,對全局pm_chain_head->head中的每一個都調用其notifier_call() pm_restore_console(); //(kernel/power/console.c)
返回用戶空間
s3c_pm_enter 展開:
    any_allowed 檢測有沒有設置喚醒源
    samsung_pm_save_gpios(); 保存一些狀態,gpio uart狀態
    samsung_pm_saved_gpios();
    s3c_pm_save_uarts();
    s3c_pm_save_core();
    s3c_pm_configure_extint 配置一下喚醒源
    pm_cpu_prep s3c2410_pm_add()中對這個函數指針賦值
        s3c2410_pm_prepare 這個函數中將s3c_cpu_resume()這個函數的物理地址寫入了GSTATUS3寄存器中
            GSTATUS3 = s3c_cpu_resume() 當系統被喚醒的時候會去執行Uboot,Uboot中會去讀GSTATUS3得到一個函數的地址然後去執行它
    s3c_pm_arch_stop_clocks 關閉時鐘
    cpu_suspend(0, pm_cpu_sleep) 這個是最重要的suspend函數了,參數2 pm_cpu_sleep 作為回調函數
        __cpu_suspend(arg, pm_cpu_sleep)  arch/arm/kernel/sleep.S 中是個匯編代碼,pm_cpu_sleep作為第二個參數傳入,就保存在r1裏面
            stmfd    sp!, {r0, r1}        @ save suspend func arg and pointer
            ldmfd    sp!, {r0, pc}        @ call suspend fn  恢復的時候把r1裏面的值恢復到pc指針中! 相當於執行了pm_cpu_sleep
        pm_cpu_sleep = ENTRY(s3c2410_cpu_suspend) /arch/arm/mach-s3c24xx/s3c2410.S
            先讀一遍這幾個寄存器,讀的原因:休眠的時候通過寫這個REFRESH寄存器把SDRAM給關掉,關閉掉後還要通過寫寄存器CLKCON把
            CPU給停掉,但是當內核去訪問這些寄存器的時候,內核需要使用虛擬地址,就要使用到SDRAM上的頁表,但是現在已經要先關閉SDRAM了,
            就不能再訪問SDRAM上的頁表了,因此需要先讀取一下那些寄存器,緩存這些寄存器的頁表到TLB裏面,之後通過TLB得到翻譯後的這些寄存
            器的物理地址。
            ldr    r4, =S3C2410_REFRESH 
            ldr    r5, =S3C24XX_MISCCR
            ldr    r6, =S3C2410_CLKCON
            下面先去dummy執行一遍,pc指針肯定也不等於0,目前先把這些指令緩存在I cache裏面。
            teq    pc, #0            @ first as a trial-run to load cache
            bl    s3c2410_do_sleep
            這裏就可以從Icache中獲取這些指令,執行SDRAM的關閉和CPU的睡眠操作了
            s3c2410_do_sleep:
                streq    r7, [ r4 ]            @ SDRAM sleep command
                streq    r8, [ r5 ]            @ SDRAM power-down config
                streq    r9, [ r6 ]            @ CPU sleep
    ========================上面是休眠,下面就是喚醒操作了===========================
    s3c_pm_restore_core();
    s3c_pm_restore_uarts();
    samsung_pm_restore_gpios();
    s3c_pm_restored_gpios();

流程分析結論:
1.PM Core會依次調用“prepare-->suspend-->suspend_late-->suspend_noirq-------wakeup--------->resume_noirq-->resume_early-->resume-->complete”
總體流程就是先讓你準備一下,然後再讓你休眠,休眠和喚醒是反著來的。在suspend之前可以做一些準備工作,suspend之後可以做一些清理函數,若是認
2.為沒有什麽好準備或清理的,設備驅動中可以不實現prepare函數和suspend_late函數,只實現suspend函數。
3.可以參考函數dpm_show_time()裏面的實現來在內核中打印時間
struct device *dev;
list_for_each_entry(dev, &dpm_suspended_list, power.entry)
4.list_for_each_entry:dummy dev的power.entry實體掛載在鏈表dpm_suspended_list上,返回dev實體

三、查看三星單板suspend功能實現

/arch/arm/plat-samsung/pm.c 中的struct platform_suspend_ops s3c_pm_ops
s3c_pm_init
suspend_set_ops(&s3c_pm_ops);

設置喚醒源:配置GPIO引腳工作於中斷模式,設置它的觸發方式
s3c_pm_enter --> s3c_pm_configure_extint()

使用哪個交叉編譯工具鏈只需要在PATH中設置其路徑即可!有多個不同版本的話設置一個自己想要的版本的路徑到PATH中,就選用了這個交叉工具鏈。

是make uImage

修改按鍵驅動在request_irq()之後調用irq_set_irq_wake()來指定此中斷為喚醒源(沒有喚醒源是不允許進入睡眠模式的)

四、修改驅動支持電源管理
a.通知notifier
①由上流程分析可知在凍結App之前,使用pm_notifier_call_chain(PM_SUSPEND_PREPARE)來通知應用程序
②由上流程分析可知在重啟App之後,使用pm_notifier_call_chain(PM_POST_SUSPEND);
如果驅動在凍結App之前之後有些事情要做,可以使用register_pm_notifier()註冊這兩個notifier
一般驅動進入休眠是在凍結進程之後進行休眠的,以免驅動休眠之後還有App訪問到它!

b.添加suspend,resume函數,可以參考s3c2410fb.c
1.若在struct platform_driver裏面加的話只有suspend和resume兩個選項,但是這裏是內核即將遺棄的,平臺(一般)設備沒有pm_domain、type、class,但是有總線bus

static struct platform_driver s3c2412fb_driver = {
    .probe        = s3c2412fb_probe,
    .remove        = s3c2410fb_remove,
    .suspend    = s3c2410fb_suspend,
    .resume        = s3c2410fb_resume,
    .driver        = { /*若此內部有.pm域,優先調用driver.pm,而不是platform_driver.suspend*/
        .name    = "s3c2412-lcd",
    },
};

平臺驅動struct platform_driver中註冊了suspend,resume函數何時被調用:
    dev->pm_domain->ops.suspend
    dev->type->pm->suspend
    dev->class->pm->suspend
    dev->bus->pm->suspend
    dev->driver->pm->suspend

__platform_driver_register()中drv->driver.bus = &platform_bus_type;[全局]
由上面的調用邏輯dev->bus->pm->suspend會被調用,而platform_bus_type;[全局]有.pm = &platform_dev_pm_ops,
platform_pm_suspend() --> platform_legacy_suspend() --> platform_driver.suspend.

2.添加suspend,resume函數
老方法:在platform_driver中實現suspend/resume方法
新方法:在platform_driver.driver.pm中實現suspend/resume方法,[可參考ac97c.c]

3.實驗現象
實現中休眠之前LCD上的圖像在休眠後LCD上的圖像不見了,因為在喚醒時
LCD做串口控制臺被清理了,在menuconfig中將<*> Framebuffer Console support去掉即可!讓LCD不做控制臺

五、代碼附錄

1.notifier

static int lcd_suspend_notifier(struct notifier_block *nb, unsigned long event, void *dummy)
{
    switch (event) {
    case PM_SUSPEND_PREPARE:
        printk("lcd suspend notifiler test: PM_SUSPEND_PREPARE\n");
        return NOTIFY_OK;
    case PM_POST_SUSPEND:
        printk("lcd suspend notifiler test: PM_POST_SUSPEND\n");
        return NOTIFY_OK;

    default:
        return NOTIFY_DONE;
    }
}

static struct notifier_block lcd_pm_notif_block = {
    .notifier_call = lcd_suspend_notifier,
};

static int lcd_init(void)
{
    ......
    register_pm_notifier(&lcd_pm_notif_block); //由上流程可知在pm_notifier_call_chain時會傳入不同的參數調用這個函數。
    ......
    return 0;
}

static void lcd_exit(void)
{
        ......
    unregister_pm_notifier(&lcd_pm_notif_block);
        ......
}

module_init(lcd_init);
module_exit(lcd_exit); 

2.suspend/resume實現

static int lcd_suspend(struct device *dev)
{
    /*1.保存LCD相關寄存器狀態*/

    /*2.關閉時鐘,斷電進入suspend狀態*/

    return 0;
}

static int lcd_resume(struct device *dev)
{
    /*1.還原LCD相關寄存器狀態*/

    /*2.使能時鐘,上電進入suspend狀態*/

    return 0;
}

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_init(void)
{
    register_pm_notifier(&lcd_pm_notif_block);
    platform_driver_register(&lcd_drv);
    return 0;
}

static void lcd_exit(void)
{
    unregister_pm_notifier(&lcd_pm_notif_block);
    platform_device_unregister(&lcd_dev);
    platform_driver_unregister(&lcd_drv);
}

module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");

Linux的suspend和resume