第19章 Linux电源管理的系统架构和驱动之挂起到RAM

2019-07-14 01:36发布

19.9 挂起到RAM        Linux支持STANDBY(浅度休眠,唤醒延迟小)挂起到RAM(中度休眠,SDRAM自刷新,简称S2RAM)挂起到硬盘(深度休眠,最省电,唤醒延迟大,简称STD)等形式的待机,如图19.9所示。

图19.9 Linux的待机模式
    一般的嵌入式产品只实现挂起到RAM,即将系统的状态保存于内存中,并将SDRAM置于自刷新状态,待用户按键等操作后再重新恢复系统。少数嵌入式Linux系统会实现挂起到硬盘,挂起到硬盘与挂起到RAM的不同是:挂起到RAM并不关机,挂起到硬盘则把系统的状态保持于磁盘,然后关闭整个系统。    在Linux下,这些行为通常是由用户空间触发的,通过向/sys/power/state设备节点写入mem可开始挂起到RAM的流程。许多Linux产品会有一个按键,一按就进入挂起到RAM。这通常是由于与这个按键对应的输入设备驱动汇报了一个和电源相关的input_event,用户空间的电源管理daemon进程收到这个事件后,再触发挂起到RAM的。内核也有一个INPUT_APMPOWER(APM:高级电源管理)驱动,位于drivers/input/apm-power.c下,这个文件在内核级别侦听EV_PWR类事件,并通过apm_queue_event(APM_USER_SUSPEND)自动引发挂起到RAM(S2RAM):
static void system_power_event(unsigned int keycode)
{
        switch (keycode) {
        case KEY_SUSPEND:
                apm_queue_event(APM_USER_SUSPEND); //自动引发挂起到RAM(S2RAM)
                pr_info("Requesting system suspend... ");
                break;
        default:
                break;
        }
}
static void apmpower_event(struct input_handle *handle, unsigned int type,
                           unsigned int code, int value)
{
        /* only react on key down events */
        if (value != 1)
                return;        switch (type) {
        case EV_PWR: // 核级别侦听EV_PWR类事件
                system_power_event(code);
                break;
        default:
                break;
        }
}
drivers/char/apm-emulation.c
void apm_queue_event(apm_event_t event)
{
        unsigned long flags;

        spin_lock_irqsave(&kapmd_queue_lock, flags); //获取自旋锁(锁定期间不允许阻塞)、关闭中断、保存标志
        queue_add_event(&kapmd_queue, event);
        spin_unlock_irqrestore(&kapmd_queue_lock, flags);
        wake_up_interruptible(&kapmd_wait);//释放自旋锁、使能中断、恢复标志
}
EXPORT_SYMBOL(apm_queue_event);
在Linux内核中,挂起到RAM的挂起和恢复大致的流程如图19.10所示(操作包括同步文件系统、freeze进程、设备驱动挂起以及系统的挂起入口等)。

图19.10 Linux挂起到RAM流程
linux/device.h
struct device_driver {
        const char              *name;
        struct bus_type         *bus;
        struct module           *owner;
        const char              *mod_name;      /* used for built-in modules */
        bool suppress_bind_attrs;       /* disables bind/unbind via sysfs */
        const struct of_device_id       *of_match_table;
        int (*probe) (struct device *dev);
        int (*remove) (struct device *dev);
        void (*shutdown) (struct device *dev);
        int (*suspend) (struct device *dev, pm_message_t state);
        int (*resume) (struct device *dev);
        const struct attribute_group **groups;
        const struct dev_pm_ops *pm;
        struct driver_private *p;
};在dev_pm_ops结构体中,封装挂起到RAM和挂起到硬盘所需的回调函数,如代码清单19.13所示。代码清单19.13 dev_pm_ops结构体
linux/pm.h
struct dev_pm_ops {
        int (*prepare)(struct device *dev);
        void (*complete)(struct device *dev);
        int (*suspend)(struct device *dev);
        int (*resume)(struct device *dev);
        int (*freeze)(struct device *dev);
        int (*thaw)(struct device *dev);
        int (*poweroff)(struct device *dev);
        int (*restore)(struct device *dev);
        int (*suspend_noirq)(struct device *dev);
        int (*resume_noirq)(struct device *dev);
        int (*freeze_noirq)(struct device *dev);
        int (*thaw_noirq)(struct device *dev);
        int (*poweroff_noirq)(struct device *dev);
        int (*restore_noirq)(struct device *dev);
        int (*runtime_suspend)(struct device *dev);
        int (*runtime_resume)(struct device *dev);
        int (*runtime_idle)(struct device *dev);
};
图19.10给出挂起到RAM的时候,这些PM回调函数的调用时机。
目前比较推荐的做法是在platform_driver、i2c_driver和spi_driver等xxx_driver结构体实例的driver成员中,以上述结构体的形式封装PM回调函数,并赋值到driver的pm属性成员。例如在drivers/spi/spi-s3c64xx.c中,platform_driver中的pm成员被赋值。
代码清单19.14 设备驱动中的pm成员
#ifdef CONFIG_PM_SLEEP
static int s3c64xx_spi_suspend(struct device *dev)
{
struct spi_master *master = dev_get_drvdata(dev);
struct s3c64xx_spi_driver_data *sdd = spi_master_get_devdata(master);

int ret = spi_master_suspend(master);
if (ret)
return ret;

ret = pm_runtime_force_suspend(dev);
if (ret < 0)
return ret;

sdd->cur_speed = 0; /* Output Clock is stopped */

return 0;
}

static int s3c64xx_spi_resume(struct device *dev)
{
struct spi_master *master = dev_get_drvdata(dev);
struct s3c64xx_spi_driver_data *sdd = spi_master_get_devdata(master);
struct s3c64xx_spi_info *sci = sdd->cntrlr_info;
int ret;

if (sci->cfg_gpio)
sci->cfg_gpio();

ret = pm_runtime_force_resume(dev);
if (ret < 0)
return ret;

s3c64xx_spi_hwinit(sdd, sdd->port_id);

return spi_master_resume(master);
}
#endif /* CONFIG_PM_SLEEP */


#ifdef CONFIG_PM
static int s3c64xx_spi_runtime_suspend(struct device *dev)
{
struct spi_master *master = dev_get_drvdata(dev);
struct s3c64xx_spi_driver_data *sdd = spi_master_get_devdata(master);

clk_disable_unprepare(sdd->clk);
clk_disable_unprepare(sdd->src_clk);
clk_disable_unprepare(sdd->ioclk);

return 0;
}

static int s3c64xx_spi_runtime_resume(struct device *dev)
{
struct spi_master *master = dev_get_drvdata(dev);
struct s3c64xx_spi_driver_data *sdd = spi_master_get_devdata(master);
int ret;

if (sdd->port_conf->clk_ioclk) {
ret = clk_prepare_enable(sdd->ioclk);
if (ret != 0)
return ret;
}

ret = clk_prepare_enable(sdd->src_clk);
if (ret != 0)
goto err_disable_ioclk;

ret = clk_prepare_enable(sdd->clk);
if (ret != 0)
goto err_disable_src_clk;

return 0;

err_disable_src_clk:
clk_disable_unprepare(sdd->src_clk);
err_disable_ioclk:
clk_disable_unprepare(sdd->ioclk);
return ret;
}
#endif /* CONFIG_PM */
static const struct dev_pm_ops s3c64xx_spi_pm = {
SET_SYSTEM_SLEEP_PM_OPS(s3c64xx_spi_suspend, s3c64xx_spi_resume)
SET_RUNTIME_PM_OPS(s3c64xx_spi_runtime_suspend,
  s3c64xx_spi_runtime_resume, NULL)
};
static const struct of_device_id s3c64xx_spi_dt_match[] = {
{ .compatible = "samsung,s3c2443-spi",
.data = (void *)&s3c2443_spi_port_config,
},
{ },
};
MODULE_DEVICE_TABLE(of, s3c64xx_spi_dt_match);
static struct platform_driver s3c64xx_spi_driver = {
.driver = {
.name = "s3c64xx-spi",
.pm = &s3c64xx_spi_pm,
.of_match_table = of_match_ptr(s3c64xx_spi_dt_match),
},
.probe = s3c64xx_spi_probe,
.remove = s3c64xx_spi_remove,
.id_table = s3c64xx_spi_driver_ids,
};
module_platform_driver(s3c64xx_spi_driver);
分析:s3c64xx_spi_suspend()完成时钟的禁止,s3c64xx_spi_resume()则完成硬件的重新初始化、时钟的使能等工作。宏SET_SYSTEM_SLEEP_PM_OPS完成suspend、resume等成员函数的赋值:#define SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn)
            .suspend = suspend_fn,
            .resume = resume_fn,
            .freeze = suspend_fn,
            .thaw = resume_fn,
            .poweroff = suspend_fn,
            .restore = resume_fn,
        除上述推行的做法外,在platform_driver、i2c_driver、spi_driver等xxx_driver结构体中仍保留过时的suspend、resume入口函数(目前不再推荐使用过时的接口,而是推荐赋值xxx_driver中的driver的pm成员),代码清单19.15 设备驱动中过时的PM成员函数。
linux/platform_device.h
struct platform_driver {
         int (*probe)(struct platform_device *);
         int (*remove)(struct platform_device *);
         void (*shutdown)(struct platform_device *);
        int (*suspend)(struct platform_device *, pm_message_t state);
         int (*resume)(struct platform_device *);
         struct device_driver driver;
         const struct platform_device_id *id_table;
};
在Linux的核心层中,实际上是优先选择执行xxx_driver.driver.pm.suspend()成员函数,在前者不存在的情况下,执行过时的xxx_driver.suspend(),如platform_pm_suspend()的逻辑如代码清单19.16所示。
代码清单19.16 驱动核心层寻找PM回调的顺序
 drivers/base/platform.c
#ifdef CONFIG_SUSPEND
int platform_pm_suspend(struct device *dev)
{
        struct device_driver *drv = dev->driver;
        int ret = 0;

        if (!drv)
                return 0;

        if (drv->pm) {
                if (drv->pm->suspend)
                        ret = drv->pm->suspend(dev);
        } else {
                ret = platform_legacy_suspend(dev, PMSG_SUSPEND);
        }

        return ret;
}

int platform_pm_resume(struct device *dev)
{
        struct device_driver *drv = dev->driver;
        int ret = 0;

        if (!drv)
                return 0;

        if (drv->pm) {
                if (drv->pm->resume)
                        ret = drv->pm->resume(dev);
        } else {
                ret = platform_legacy_resume(dev);
        }

        return ret;
}

#endif /* CONFIG_SUSPEND */
       一般来讲,在设备驱动的挂起入口函数中,会关闭设备、关闭该设备的时钟输入,甚至是关闭设备的电源,在恢复时则完成相反的的操作。在挂起到RAM的挂起和恢复过程中,系统恢复后要求所有设备的驱动都工作正常。为调试这个过程,可以使能内核的PM_DEBUG选项,如果在挂起和恢复的过程中,看到内核的打印信息以关注具体的详细流程,在Bootloader传递给内核的bootargs中设置标志no_console_suspend
在将Linux移植到一个新的ARM SoC的过程中,最终系统挂起的入口需由芯片供应商在相应的
arch/arm/mach-xxx中实现platform_suspend_ops的成员函数,一般主要实现其中的enter和valid成员,并将整个platform_suspend_ops结构体通过内核通用API suspend_set_ops()注册进系统,如arch/arm/mach-prima2/pm.c中prima2 SoC级挂起流程的逻辑如代码清单19.17所示。代码清单19.17 系统挂起到RAM的SoC级代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#include "pm.h"

/*
 * suspend asm codes will access these to make DRAM become self-refresh and
 * system sleep
 */
u32 sirfsoc_pwrc_base;
void __iomem *sirfsoc_memc_base;

static void sirfsoc_set_wakeup_source(void)
{
        u32 pwr_trigger_en_reg;
        pwr_trigger_en_reg = sirfsoc_rtc_iobrg_readl(sirfsoc_pwrc_base + SIRFSOC_PWRC_TRIGGER_EN);
#define X_ON_KEY_B (1 << 0)
#define RTC_ALARM0_B (1 << 2)
#define RTC_ALARM1_B (1 << 3)
        sirfsoc_rtc_iobrg_writel(pwr_trigger_en_reg | X_ON_KEY_B |
                RTC_ALARM0_B | RTC_ALARM1_B,
                sirfsoc_pwrc_base + SIRFSOC_PWRC_TRIGGER_EN);
}

static void sirfsoc_set_sleep_mode(u32 mode)
{
        u32 sleep_mode = sirfsoc_rtc_iobrg_readl(sirfsoc_pwrc_base + SIRFSOC_PWRC_PDN_CTRL);
        sleep_mode &= ~(SIRFSOC_SLEEP_MODE_MASK << 1);
        sleep_mode |= mode << 1;
        sirfsoc_rtc_iobrg_writel(sleep_mode, sirfsoc_pwrc_base +
                SIRFSOC_PWRC_PDN_CTRL);
}

//将系统恢复回来后重新开始执行的物理地址存入与SoC相关的寄存器中static int sirfsoc_pre_suspend_power_off(void)
{
        u32 wakeup_entry = __pa_symbol(cpu_resume);
        sirfsoc_rtc_iobrg_writel(wakeup_entry, sirfsoc_pwrc_base + SIRFSOC_PWRC_SCRATCH_PAD1);
        sirfsoc_set_wakeup_source();
        sirfsoc_set_sleep_mode(SIRFSOC_DEEP_SLEEP_MODE);// 深度休眠模式

        return 0;
}

static int sirfsoc_pm_enter(suspend_state_t state)
{
        switch (state) {
        case PM_SUSPEND_MEM:
                sirfsoc_pre_suspend_power_off();
                outer_disable();
                /* go zzz */
                cpu_suspend(0, sirfsoc_finish_suspend);
                outer_resume();
                break;
        default:
                return -EINVAL;
        }
        return 0;
}


static const struct platform_suspend_ops sirfsoc_pm_ops = {
        .enter = sirfsoc_pm_enter,
        .valid = suspend_valid_only_mem,
};

static const struct of_device_id pwrc_ids[] = {
        { .compatible = "sirf,prima2-pwrc" },
        {}
};

static int __init sirfsoc_of_pwrc_init(void)
{
        struct device_node *np;        np = of_find_matching_node(NULL, pwrc_ids);
        if (!np) {
                pr_err("unable to find compatible sirf pwrc node in dtb ");
                return -ENOENT;
        }

        /*
         * pwrc behind rtciobrg is not located in memory space
         * though the property is named reg. reg only means base
         * offset for pwrc. then of_iomap is not suitable here.
         */
        if (of_property_read_u32(np, "reg", &sirfsoc_pwrc_base))
                panic("unable to find base address of pwrc node in dtb ");

        of_node_put(np);

        return 0;
}

static const struct of_device_id memc_ids[] = {
        { .compatible = "sirf,prima2-memc" },
        {}
};

static int sirfsoc_memc_probe(struct platform_device *op)
{
        struct device_node *np = op->dev.of_node;

        sirfsoc_memc_base = of_iomap(np, 0);
        if (!sirfsoc_memc_base)
                panic("unable to map memc registers ");

        return 0;
}

static struct platform_driver sirfsoc_memc_driver = {
        .probe          = sirfsoc_memc_probe,
        .driver = {
                .name = "sirfsoc-memc",
                .of_match_table = memc_ids,
        },
};

static int __init sirfsoc_memc_init(void)
{
        return platform_driver_register(&sirfsoc_memc_driver);
}

int __init sirfsoc_pm_init(void)
{
        sirfsoc_of_pwrc_init();
        sirfsoc_memc_init();
        suspend_set_ops(&sirfsoc_pm_ops);
        return 0;
}
分析:   在系统重新恢复中,会执行cpu_resume这段汇编,并进行设置唤醒源等操作。而cpu_suspend(0, sirfsoc_finish_suspend);以及其中调用的与SoC相关的用汇编实现的函数sirfsoc_finish_suspend()真正完成最后的待机并将系统置于深度睡眠,同时置SDRAM于自刷新状态的过程,具体的代码高度依赖于特定的芯片,其实现一般是一段汇编。arch/arm/mach-prima2/sleep.S/*
 * sleep mode for CSR SiRFprimaII
 *
 * Copyright (c) 2011 Cambridge Silicon Radio Limited, a CSR plc group company.
 *
 * Licensed under GPLv2 or later.
 */

#include
#include
#include

#include "pm.h"

#define DENALI_CTL_22_OFF       0x58
#define DENALI_CTL_112_OFF      0x1c0

        .text

ENTRY(sirfsoc_finish_suspend) @入口
        @ r5:   mem controller
        ldr     r0, =sirfsoc_memc_base
        ldr     r5, [r0]
        @ r6:   pwrc base offset
        ldr     r0, =sirfsoc_pwrc_base
        ldr     r6, [r0]
        @ r7:   rtc iobrg controller
        ldr     r0, =sirfsoc_rtciobrg_base
        ldr     r7, [r0]

        @ Read the power control register and set the
        @ sleep force bit.
        add     r0, r6, #SIRFSOC_PWRC_PDN_CTRL
        bl      __sirfsoc_rtc_iobrg_readl
        orr     r0,r0,#SIRFSOC_PWR_SLEEPFORCE
        add     r1, r6, #SIRFSOC_PWRC_PDN_CTRL
        bl      sirfsoc_rtc_iobrg_pre_writel
        mov     r1, #0x1

        @ read the MEM ctl register and set the self
        @ refresh bit

        ldr     r2, [r5, #DENALI_CTL_22_OFF]
        orr     r2, r2, #0x1

        @ Following code has to run from cache since
        @ the RAM is going to self refresh mode
        .align 5
        str     r2, [r5, #DENALI_CTL_22_OFF]

1:
        ldr     r4, [r5, #DENALI_CTL_112_OFF]
        tst     r4, #0x1
        bne     1b

        @ write SLEEPFORCE through rtc iobridge

        str     r1, [r7]
        @ wait rtc io bridge sync
1:
        ldr     r3, [r7]
        tst     r3, #0x01
        bne     1b
        b .