鉴于当前做的项目中有低功耗的需求,因此查探了一番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);