i.mx6ベースで、Linux KernelのVendor依存な省電力実装方法を確認。

# 初めに
https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.1.52.tar.gz

mach-imx/pm-imx6 のコードを基に、省電力モード実装するときに何をするべきか、というのをまとめてみる。

# TL;DR
- Linux Kernel 4.1の、i.mx6をベースに省電力実装方法を簡単に確認してみた。
- sleep to memを実行し、メモリをセルフリフレッシュ状態に入れてWFIする実装を確認。
- 用意していた関数を、専用のSRAMへコピーする関数は、Linux側が提供しているものを使う。

```c: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。

```c: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の中身も非常にシンプルでわかりやすい！

```c: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を呼び出すだけのシンプルな実装。

```c: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()` は引数で与えた関数を実行するらしい。

```c: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()` と。

```c: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()` は、関数ポインタで定義されている。

```c: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がメモリリフレッシュ状態になっても参照できる。
ここにスリープの実装が入っている、と。

```c: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()`は、汎用的に使える関数として定義されている。

```c: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‘関数は、アセンブラとして入っている。

おお、自分でメモリリフレッシュに入れたりとかしている。参考になる。

```c: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側が提供しているものを使う。

以上です。