轉自:https://www.cnblogs.com/hellokitty2/p/9942026.html
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");