Linux的电源管理-休眠与唤醒

写在前面

为了理清新平台系统休眠和唤醒的流程,通过学习其他平台的电源管理方法,曲径通幽, 达到目的.

刚接手新平台,且相应的资料不多,很容易让人力不从心;我在网上寻找了学习资源,发现韦东山对S3C2440的驱动讲解有相关的内容,口碑也不错,可以作为一个切入点.

不同平台一定会有平台的差异,不同的Linux内核版本之间也会有相应的差异,但思路是一致的,可以总结出来,帮助理解电源管理的开发使用.

本文总结关于系统休眠和唤醒的流程以及开发方法.

Linux电源管理基本框架

下图所示为Linux电源管理基本框架. 详细请参考Linux电源管理(1)_整体架构
本文重点介绍关于Generic PM的使用.

Linux的电源管理-休眠与唤醒_第1张图片
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有三种模式,如下图所示,
Linux的电源管理-休眠与唤醒_第2张图片

我们选择在u-boot中实现Sleep mode作为suspend, 也就是对应Linux内核实现的mem模式的suspend. 实验的效果是:在u-boot命令行运行suspend指令后系统进入Sleep Mode; 当按下按键后,系统唤醒,继续接着休眠前的地方执行下去.

进入Sleep Mode的流程

Linux的电源管理-休眠与唤醒_第3张图片

实现代码如下:

/*
 * [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;
}

代码解释

  1. 对于进入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 }

唤醒流程

Linux的电源管理-休眠与唤醒_第4张图片
从以上的描述中的第2点可以得知, 系统从Sleep模式退出是需要重新启动u-boot的, 区别是power-up还是wake-up的方法是读取GSTATUS2寄存器.如果值为1,代表是从Sleep Mode过来的,就会执行相应的流程恢复休眠前的现场; 反之,则执行正常启动流程.
Linux的电源管理-休眠与唤醒_第5张图片
下面是关于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的电源管理-休眠与唤醒_第6张图片
Linux的电源管理-休眠与唤醒_第7张图片
对于Linux的Generic PM框架,本质是也就是把上面这部分与板级(platform)有关的内容放置到框架的相应位置(也就是下图的Platform dependent PM的位置),就可以实现在Linux下的suspend和resume了.

介绍一下Generic PM框架,参考Linux电源管理(6)_Generic PM之Suspend功能
Linux的电源管理-休眠与唤醒_第8张图片
下面以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流程

Linux的电源管理-休眠与唤醒_第9张图片
驱动程序里唤醒相关的电源管理函数的调用过程: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设置寄存器使能中断功能. (注意:这些中断引脚是支持唤醒功能的)

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");

代码分析

  1. 在LCD驱动的基础上注册了一个平台设备和驱动,当系统进入休眠模式时会调用lcd_suspend函数关闭灯光,当唤醒退出休眠模式时会调用lcd_resume打开灯光.
  2. 重点在于第13行的结构体.

总结

上面介绍的是系统休眠或唤醒模式下的电源管理, 对于不同平台,都需要按照DataSheet的说明选择休眠模式,修改u-boot和kernel.
那在正常运行模式下,能否单独对设备进行电源管理呢,请期待下一节更新Linux电源管理-Runtime PM

你可能感兴趣的:(【驱动开发】,linux,电源管理,驱动,低功耗)