鉴于当前做的项目中有低功耗的需求,因此查探了一番Linux的睡眠及唤醒的机制。
当前网络上已经有很多关于睡眠唤醒的分析文章,有的分析也非常透彻,因此本文只从寄存器以及汇编处理和CPU架构方面来补充一下。
睡眠总体上可以分为浅睡及深睡眠,从PM管理上来说,主要是电源域和时钟控制的不同,如下IMX6介绍:
Linux进入睡眠的方式,在应用层可以直接操作/sys/power/state文件,cat此文件可以查看支持睡眠的种类,我的imx6开发板只有3种模式:
root@mys6ull14x14:~# cat /sys/power/state
freeze standby mem
- freeze: 冻结I/O设备,将它们置于低功耗状态,使处理器进入空闲状态,唤醒最快,耗电比其它standby, mem, disk方式高
- standby:除了冻结I/O设备外,还会暂停系统,唤醒较快,耗电比其它 mem, disk方式高
- mem:将运行状态数据存到内存,并关闭外设,进入等待模式,唤醒较慢,耗电比disk方式高
- disk: 将运行状态数据存到硬盘,然后关机,唤醒最慢
在我的IMX6UL板子上,外设只有一个wifi而且是禁用状态,5V正常工作时,电流170ma左右,进入mem状态,大概20ma
看一下睡眠和唤醒的过程日志:
root@mys6ull14x14:~# echo mem > /sys/power/state
PM: Syncing filesystems ... done.
Freezing user space processes ... (elapsed 0.002 seconds) done.
Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done.
Suspending console(s) (use no_console_suspend to debug)
----此时已进入睡眠,调试串口无打印
--以下为唤醒过程的输出:
RTW: suspend start
PM: suspend of devices complete after 12.485 msecs
PM: suspend devices took 0.010 seconds
PM: late suspend of devices complete after 2.056 msecs
PM: noirq suspend of devices complete after 2.235 msecs
Disabling non-boot CPUs ...
PM: noirq resume of devices complete after 1.326 msecs
PM: early resume of devices complete after 1.394 msecs
gpmi-nand 1806000.gpmi-nand: enable the asynchronous EDO mode 5
RTW: resume start
==> rtl8188e_iol_efuse_patch
RTW: wlan0- hw port(0) mac_addr =a0:2c:36:e7:23:5e
RTW: rtw_resume_common:0 in 590 ms
PM: resume of devices complete after 659.844 msecs
PM: resume devices took 0.660 seconds
Restarting tasks ... done.
root@mys6ull14x14:~#
Linux将各种外设进入低功耗模式之后,关闭各域的Power,对于ARM core,直接使用WFI指令让其进入低功耗模式,此后等待外部唤醒。
对于唤醒机制,首先得确认CPU支持哪些唤醒源,针对IMX6,唤醒源如下:
对于Other wakeup source,主要是一些外设,比如UART,SD卡,网络等等,可以直接在外设寄存器中查看。
对于唤醒源的设定,主要是进入低功耗模式之前,屏蔽其中断,睡眠后当此中断发生时,arm core会自动唤醒。
关于设定方式,主要在DTS中配置,如gpio:
user {
label = "User Button";
gpios = <&gpio5 0 GPIO_ACTIVE_HIGH>;
gpio-key,wakeup;
linux,code = <KEY_1>;
};
其中gpio-key,wakeup即设定为支持唤醒功能。在驱动中,关于此配置最主要的操作为enable_irq_wake,最后会调用到cpu层接口
static struct irq_chip imx_gpc_chip = {
.name = "GPC",
.irq_eoi = irq_chip_eoi_parent,
.irq_mask = imx_gpc_irq_mask,
.irq_unmask = imx_gpc_irq_unmask,
.irq_retrigger = irq_chip_retrigger_hierarchy,
.irq_set_wake = imx_gpc_irq_set_wake,
.irq_set_type = irq_chip_set_type_parent,
#ifdef CONFIG_SMP
.irq_set_affinity = irq_chip_set_affinity_parent,
#endif
};
在imx_gpc_irq_set_wake把唤醒中断ID记录到gpc_wake_irqs,
static int imx_gpc_irq_set_wake(struct irq_data *d, unsigned int on)
{
unsigned int idx = d->hwirq / 32;
unsigned long flags;
u32 mask;
mask = 1 << d->hwirq % 32;
spin_lock_irqsave(&gpc_lock, flags);
gpc_wake_irqs[idx] = on ? gpc_wake_irqs[idx] | mask :
gpc_wake_irqs[idx] & ~mask;
spin_unlock_irqrestore(&gpc_lock, flags);
/*
* Do *not* call into the parent, as the GIC doesn't have any
* wake-up facility...
*/
return 0;
}
登记之后在进入睡眠之前应用imx_gpc_pre_suspend:
void imx_gpc_pre_suspend(bool arm_power_off)
{
void __iomem *reg_imr1 = gpc_base + GPC_IMR1;
int i;
if (cpu_is_imx6q() && imx_get_soc_revision() == IMX_CHIP_REVISION_2_0)
_imx6q_pm_pu_power_off(&imx6q_pu_domain.base);
/* power down the mega-fast power domain */
if ((cpu_is_imx6sx() || cpu_is_imx6ul() || cpu_is_imx6ull()) && arm_power_off)
imx_gpc_mf_mix_off();
/* Tell GPC to power off ARM core when suspend */
if (arm_power_off)
imx_gpc_set_arm_power_in_lpm(arm_power_off);
for (i = 0; i < IMR_NUM; i++) {
gpc_saved_imrs[i] = readl_relaxed(reg_imr1 + i * 4);
writel_relaxed(~gpc_wake_irqs[i], reg_imr1 + i * 4);
}
}
writel_relaxed(~gpc_wake_irqs[i], reg_imr1 + i * 4); 写入中断屏蔽寄存器即可.
睡眠后,cpu大部分功能不可用,包括中断控制器,当中断发生时,不能进入正常的中断响应程序,而且被另一套独立的“中断控制器”所接管,跳入指定的地址执行,在imx6上此地址可以在如下寄存器中配置:
SRC_GPR1为睡眠后中断发生时跳入的地址,SRC_GPR2为相关参数
在Linux中使用如下,文件arch/arm/mach-imx/suspend-imx6.S 中函数ENTRY(imx6_suspend)会保存相关寄存器
ENTRY(imx6_suspend)
ldr r1, [r0, #PM_INFO_PBASE_OFFSET]
ldr r2, [r0, #PM_INFO_RESUME_ADDR_OFFSET]
ldr r3, [r0, #PM_INFO_DDR_TYPE_OFFSET]
ldr r4, [r0, #PM_INFO_PM_INFO_SIZE_OFFSET]
/*
* counting the resume address in iram
* to set it in SRC register.
*/
ldr r6, =imx6_suspend
ldr r7, =resume
sub r7, r7, r6
add r8, r1, r4
add r9, r8, r7
/*
* make sure TLB contain the addr we want,
* as we will access them after MMDC IO floated.
*/
ldr r11, [r0, #PM_INFO_MX6Q_CCM_V_OFFSET]
ldr r6, [r11, #0x0]
ldr r11, [r0, #PM_INFO_MX6Q_GPC_V_OFFSET]
ldr r6, [r11, #0x0]
ldr r11, [r0, #PM_INFO_MX6Q_IOMUXC_V_OFFSET]
ldr r6, [r11, #0x0]
/* use r11 to store the IO address */
ldr r11, [r0, #PM_INFO_MX6Q_SRC_V_OFFSET]
/* store physical resume addr and pm_info address. */
str r9, [r11, #MX6Q_SRC_GPR1]
str r1, [r11, #MX6Q_SRC_GPR2]
关于保存的位置和内容,文件中有相关注释:
/*
* ==================== low level suspend ====================
*
* Better to follow below rules to use ARM registers:
* r0: pm_info structure address;
* r1 ~ r4: for saving pm_info members;
* r5 ~ r10: free registers;
* r11: io base address.
*
* suspend ocram space layout:
* ======================== high address ======================
* .
* .
* .
* ^
* ^
* ^
* imx6_suspend code
* PM_INFO structure(imx6_cpu_pm_info)
* ======================== low address =======================
*/
关于恢复,resume地址此前已经存入R9 即SRC_GPR1:
resume:
/* invalidate L1 I-cache first */
mov r6, #0x0
mcr p15, 0, r6, c7, c5, 0
mcr p15, 0, r6, c7, c5, 6
/* enable the Icache and branch prediction */
mov r6, #0x1800
mcr p15, 0, r6, c1, c0, 0
isb
/* restore it with 0x1f if use ldo bypass mode.*/
ldr r11, [r0, #PM_INFO_MX6Q_ANATOP_P_OFFSET]
ldr r7, [r11, #MX6Q_ANATOP_CORE]
and r7, r7, #0x1f
cmp r7, #0x1e
bne ldo_check_done3
ldr r7, [r11, #MX6Q_ANATOP_CORE]
orr r7, r7, #0x1f
str r7, [r11, #MX6Q_ANATOP_CORE]
ldo_check_done3:
/* get physical resume address from pm_info. */
ldr lr, [r0, #PM_INFO_RESUME_ADDR_OFFSET]
/* clear core0's entry and parameter */
ldr r11, [r0, #PM_INFO_MX6Q_SRC_P_OFFSET]
mov r7, #0x0
str r7, [r11, #MX6Q_SRC_GPR1]
str r7, [r11, #MX6Q_SRC_GPR2]
ldr r3, [r0, #PM_INFO_DDR_TYPE_OFFSET]
mov r5, #0x1
/* check whether it supports Mega/Fast off */
ldr r6, [r0, #PM_INFO_MMDC_NUM_OFFSET]
cmp r6, #0x0
beq dsm_only_resume_io
resume_mmdc_io
b dsm_resume_mmdc_done
dsm_only_resume_io:
ldr r3, [r0, #PM_INFO_DDR_TYPE_OFFSET]
resume_io
dsm_resume_mmdc_done:
ret lr
关于结构体imx6_cpu_pm_info与汇编中保存位置的定义,需要一一对应:
/*
* This structure is for passing necessary data for low level ocram
* suspend code(arch/arm/mach-imx/suspend-imx6.S), if this struct
* definition is changed, the offset definition in
* arch/arm/mach-imx/suspend-imx6.S must be also changed accordingly,
* otherwise, the suspend to ocram function will be broken!
*/
struct imx6_cpu_pm_info {
phys_addr_t pbase; /* The physical address of pm_info. */
phys_addr_t resume_addr; /* The physical resume address for asm code */
u32 ddr_type;
u32 pm_info_size; /* Size of pm_info. */
struct imx6_pm_base mmdc0_base;
struct imx6_pm_base mmdc1_base;
struct imx6_pm_base src_base;
struct imx6_pm_base iomuxc_base;
struct imx6_pm_base ccm_base;
struct imx6_pm_base gpc_base;
struct imx6_pm_base l2_base;
struct imx6_pm_base anatop_base;
u32 ttbr1; /* Store TTBR1 */
u32 mmdc_io_num; /* Number of MMDC IOs which need saved/restored. */
u32 mmdc_io_val[MX6_MAX_MMDC_IO_NUM][2]; /* To save offset and value */
u32 mmdc_num; /* Number of MMDC registers which need saved/restored. */
u32 mmdc_val[MX6_MAX_MMDC_NUM][2];
} __aligned(8);