初めに
mach-imx/pm-imx6 のコードを基に、省電力モード実装するときに何をするべきか、というのをまとめてみる。
TL;DR
- Linux Kernel 4.1の、i.mx6をベースに省電力実装方法を簡単に確認してみた。
- sleep to memを実行し、メモリをセルフリフレッシュ状態に入れてWFIする実装を確認。
- 用意していた関数を、専用のSRAMへコピーする関数は、Linux側が提供しているものを使う。
arch/arm/mach-imx/pm-imx6.c
static const struct platform_suspend_ops imx6q_pm_ops = {
.enter = imx6q_pm_enter,
.valid = imx6q_pm_valid,
};
imx6q_pm_valid
サポートしているのは、STANDBYとMEM。
arch/arm/mach-imx/pm-imx6.c
static int imx6q_pm_valid(suspend_state_t state)
{
return (state == PM_SUSPEND_STANDBY || state == PM_SUSPEND_MEM);
}
imx6q_pm_enter
enterの中身も非常にシンプルでわかりやすい!
arch/arm/mach-imx/pm-imx6.c
static int imx6q_pm_enter(suspend_state_t state)
{
switch (state) {
case PM_SUSPEND_STANDBY:
imx6q_set_lpm(STOP_POWER_ON);
imx6q_set_int_mem_clk_lpm(true);
imx_gpc_pre_suspend(false);
if (cpu_is_imx6sl())
imx6sl_set_wait_clk(true);
/* Zzz ... */
cpu_do_idle();
if (cpu_is_imx6sl())
imx6sl_set_wait_clk(false);
imx_gpc_post_resume();
imx6q_set_lpm(WAIT_CLOCKED);
break;
case PM_SUSPEND_MEM:
imx6q_set_lpm(STOP_POWER_OFF);
imx6q_set_int_mem_clk_lpm(false);
imx6q_enable_wb(true);
/*
* For suspend into ocram, asm code already take care of
* RBC setting, so we do NOT need to do that here.
*/
if (!imx6_suspend_in_ocram_fn)
imx6_enable_rbc(true);
imx_gpc_pre_suspend(true);
imx_anatop_pre_suspend();
/* Zzz ... */
cpu_suspend(0, imx6q_suspend_finish);
if (cpu_is_imx6q() || cpu_is_imx6dl())
imx_smp_prepare();
imx_anatop_post_resume();
imx_gpc_post_resume();
imx6_enable_rbc(false);
imx6q_enable_wb(false);
imx6q_set_int_mem_clk_lpm(true);
imx6q_set_lpm(WAIT_CLOCKED);
break;
default:
return -EINVAL;
}
return 0;
}
cpu_do_idle()
cpu_do_idle();
は、wfiを呼び出すだけのシンプルな実装。
arch/arm64/mm/proc.S
/*
* cpu_do_idle()
*
* Idle the processor (wait for interrupt).
*/
ENTRY(cpu_do_idle)
dsb sy // WFI may enter a low-power mode
wfi
ret
ENDPROC(cpu_do_idle)
cpu_suspend()
一方、cpu_suspend()
は引数で与えた関数を実行するらしい。
arch/arm/kernel/suspend.c
# ifdef CONFIG_MMU
int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))
{
struct mm_struct *mm = current->active_mm;
u32 __mpidr = cpu_logical_map(smp_processor_id());
int ret;
if (!idmap_pgd)
return -EINVAL;
/*
* Provide a temporary page table with an identity mapping for
* the MMU-enable code, required for resuming. On successful
* resume (indicated by a zero return code), we need to switch
* back to the correct page tables.
*/
ret = __cpu_suspend(arg, fn, __mpidr);
if (ret == 0) {
cpu_switch_mm(mm->pgd, mm);
local_flush_bp_all();
local_flush_tlb_all();
}
return ret;
}
# else
int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))
{
u32 __mpidr = cpu_logical_map(smp_processor_id());
return __cpu_suspend(arg, fn, __mpidr);
}
# define idmap_pgd NULL
# endif
imx6q_suspend_finish()
この時の引数になるのは、imx6q_suspend_finish()
と。
arch/arm/mach-imx/pm-imx6.c
static int imx6q_suspend_finish(unsigned long val)
{
if (!imx6_suspend_in_ocram_fn) {
cpu_do_idle();
} else {
/*
* call low level suspend function in ocram,
* as we need to float DDR IO.
*/
local_flush_tlb_all();
imx6_suspend_in_ocram_fn(suspend_ocram_base);
}
return 0;
}
imx6_suspend_in_orcam_fn()
ここから呼び出されている、imx6_suspend_in_orcam_fn()
は、関数ポインタで定義されている。
arch/arm/mach-imx/pm-imx6.c
static void __iomem *ccm_base;
static void __iomem *suspend_ocram_base;
static void (*imx6_suspend_in_ocram_fn)(void __iomem *ocram_vbase);
起動時の初期化時点で、ocram上にimx6_suspend関数をコピーしている。
ocramは、コードを見てもわかる通り、mmi-sram、つまり内蔵SRAM。
ここに置かれているデータは基本的にはDDRがメモリリフレッシュ状態になっても参照できる。
ここにスリープの実装が入っている、と。
arch/arm/mach-imx/pm-imx6.c
static int __init imx6q_suspend_init(const struct imx6_pm_socdata *socdata)
{
node = of_find_compatible_node(NULL, NULL, "mmio-sram");
if (!node) {
pr_warn("%s: failed to find ocram node!\n", __func__);
return -ENODEV;
}
pdev = of_find_device_by_node(node);
if (!pdev) {
pr_warn("%s: failed to find ocram device!\n", __func__);
ret = -ENODEV;
goto put_node;
}
ocram_pool = dev_get_gen_pool(&pdev->dev);
if (!ocram_pool) {
pr_warn("%s: ocram pool unavailable!\n", __func__);
ret = -ENODEV;
goto put_node;
}
ocram_base = gen_pool_alloc(ocram_pool, MX6Q_SUSPEND_OCRAM_SIZE);
if (!ocram_base) {
pr_warn("%s: unable to alloc ocram!\n", __func__);
ret = -ENOMEM;
goto put_node;
}
ocram_pbase = gen_pool_virt_to_phys(ocram_pool, ocram_base);
suspend_ocram_base = __arm_ioremap_exec(ocram_pbase,
MX6Q_SUSPEND_OCRAM_SIZE, false);
<略>
imx6_suspend_in_ocram_fn = fncpy(
suspend_ocram_base + sizeof(*pm_info),
&imx6_suspend,
MX6Q_SUSPEND_OCRAM_SIZE - sizeof(*pm_info));
fncpy()
関数を別メモリアドレス上にコピーする fncpy()
は、汎用的に使える関数として定義されている。
arch/arm/include/asm/fncpy.h
/*
* Minimum alignment requirement for the source and destination addresses
* for function copying.
*/
# define FNCPY_ALIGN 8
# define fncpy(dest_buf, funcp, size) ({ \
uintptr_t __funcp_address; \
typeof(funcp) __result; \
\
asm("" : "=r" (__funcp_address) : "0" (funcp)); \
\
/* \
* Ensure alignment of source and destination addresses, \
* disregarding the function's Thumb bit: \
*/ \
BUG_ON((uintptr_t)(dest_buf) & (FNCPY_ALIGN - 1) || \
(__funcp_address & ~(uintptr_t)1 & (FNCPY_ALIGN - 1))); \
\
memcpy(dest_buf, (void const *)(__funcp_address & ~1), size); \
flush_icache_range((unsigned long)(dest_buf), \
(unsigned long)(dest_buf) + (size)); \
\
asm("" : "=r" (__result) \
: "0" ((uintptr_t)(dest_buf) | (__funcp_address & 1))); \
\
__result; \
})
imx6_suspend()
`imx6_suspend‘関数は、アセンブラとして入っている。
おお、自分でメモリリフレッシュに入れたりとかしている。参考になる。
arch/arm/mach-imx/suspend-imx6.S
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]
<略>
/*
* put DDR explicitly into self-refresh and
* disable automatic power savings.
*/
ldr r7, [r11, #MX6Q_MMDC_MAPSR]
orr r7, r7, #0x1
str r7, [r11, #MX6Q_MMDC_MAPSR]
<略>
/* Zzz, enter stop mode */
wfi
nop
nop
nop
nop
<略>
まとめ(再掲)
- Linux Kernel 4.1の、i.mx6をベースに省電力実装方法を簡単に確認してみた。
- sleep to memを実行し、メモリをセルフリフレッシュ状態に入れてWFIする実装を確認。
- 用意していた関数を、専用のSRAMへコピーする関数は、Linux側が提供しているものを使う。
以上です。