一个suspend的问题分析

前言

最近遇到一个待机问题,系统一直无法正常suspend,跟了一下代码,发现在PSCI的最后阶段返回了一个错误码。这篇文章我们是延续上一篇《Linux系统的suspend流程分析》,继续揭开psci的神秘面纱。

正文

《Linux系统的suspend流程分析》一文我们可以看到,suspend的最后会调用到下面的函数

static int psci_cpu_suspend(u32 state, unsigned long entry_point)
{
    int err;
    u32 fn;

    fn = psci_function_id[PSCI_FN_CPU_SUSPEND]; /* 对应到ATF(arm trusted firmware)的CPU_SUSPEND函数 */
    err = invoke_psci_fn(fn, state, entry_point, 0);  /* 调用CPU_SUSPEND函数 */
    return psci_to_linux_errno(err); /* 返回错误码 */
}

psci_cpu_suspend第一个参数state,对应的是DTS文件中的“arm,psci-suspend-param”值:

        idle-states {
            entry-method = "arm,psci";
            SYSTEM_SLEEP_0: system-sleep-0 {
                compatible = "arm,idle-state";
                arm,psci-suspend-param = <0x0020000>; /* 对应power state */
                local-timer-stop;
                entry-latency-us = <0x3fffffff>;
                exit-latency-us = <0x40000000>;
                min-residency-us = <0xffffffff>;
            };
        };

正常情况下,invoke_psci_fn返回的是PSCI_RET_SUCCESS,如下所示。但是我遇到的问题却返回了PSCI_RET_INVALID_PARAMS。

static int psci_to_linux_errno(int errno)
{
    switch (errno) {
    case PSCI_RET_SUCCESS:
        return 0;
    case PSCI_RET_NOT_SUPPORTED:
        return -EOPNOTSUPP;
    case PSCI_RET_INVALID_PARAMS:
    case PSCI_RET_INVALID_ADDRESS:
        return -EINVAL;
    case PSCI_RET_DENIED:
        return -EPERM;
    };

    return -EINVAL;
}

为了解释为什么返回了PSCI_RET_INVALID_PARAMS,我们继续跟一下suspend流程。psci_function_id[PSCI_FN_CPU_SUSPEND]对应的回调函数如下:

static void __init psci_0_2_set_functions(void)
{
    ...
    psci_function_id[PSCI_FN_CPU_SUSPEND] =
                    PSCI_FN_NATIVE(0_2, CPU_SUSPEND);
    psci_ops.cpu_suspend = psci_cpu_suspend;
    ...
}

PSCI_FN_NATIVE的宏定义如下

#ifdef CONFIG_64BIT
#define PSCI_FN_NATIVE(version, name)    PSCI_##version##_FN64_##name
#else
#define PSCI_FN_NATIVE(version, name)    PSCI_##version##_FN_##name
#endif

因为我的是64位的,所以最后映射为:PSCI_0_2_FN64_CPU_SUSPEND。它的定义在include\uapi\linux\psci.h找到了原形。

/* PSCI v0.2 interface */
#define PSCI_0_2_FN_BASE            0x84000000
#define PSCI_0_2_64BIT                0x40000000

#define PSCI_0_2_FN64_BASE            \
                    (PSCI_0_2_FN_BASE + PSCI_0_2_64BIT)

#define PSCI_0_2_FN64(n)            (PSCI_0_2_FN64_BASE + (n))

#define PSCI_0_2_FN64_CPU_SUSPEND        PSCI_0_2_FN64(1)

最终我们可以得到:

psci_function_id[PSCI_FN_CPU_SUSPEND] = 0xC4000001

如果不了解PSCI规范的人,可以会对这个地址值很疑惑,我们可以看一下PSCI规范中的定义:

Power_State_Coordination_Interface_PDD_v1_1_DEN0022D.pdf

说白了,就是规定了0xC4000001这个值对应的是arm trusted firmware中的CPU_SUSPEND。可能很多人也不明白什么是ATF,可以参考下面的链接:
https://blog.csdn.net/shuaifengyun/article/details/72468109

现在armv8架构的待机流程已经非常复杂,涉及的知识点也很多,简单的说我这次的待机过程,就是通过SMC(secure monitor call),从EL1(kernel) -> EL3(secure monitor)。玩过在armv8-a板子上跑Linux系统的同学应该都知道,现在烧录的uboot会包括BL1、BL2、BL31、BL32和BL33几个阶段,我们可能会比较熟悉BL33这个阶段,因为就是在这个阶段引导Linux系统的。而从上面链接的内容可知,我所说的待机就是通过smc指令,触发在bl1中设定的smc异常来将cpu权限交给bl31并跳转到bl31中执行。

回到我们的问题,0xC4000001这个值对应的函数就是在BL31阶段,所以我们需要进入到这个阶段的代码中看。但是很可惜,我这个平台并没有提供除BL33之外的其它BLx阶段的源码。我在网上找了一个其它平台的代码,虽然不能完全对应的上,但是也能知道个大概。

/*******************************************************************************
 * PSCI top level handler for servicing SMCs.
 ******************************************************************************/
uint64_t psci_smc_handler(uint32_t smc_fid,
              uint64_t x1,
              uint64_t x2,
              uint64_t x3,
              uint64_t x4,
              void *cookie,
              void *handle,
              uint64_t flags)
{
    if (is_caller_secure(flags))
        SMC_RET1(handle, SMC_UNK);

    /* Check the fid against the capabilities */
    if (!(psci_caps & define_psci_cap(smc_fid)))
        SMC_RET1(handle, SMC_UNK);

    if (((smc_fid >> FUNCID_CC_SHIFT) & FUNCID_CC_MASK) == SMC_32) {
        /* 32-bit PSCI function, clear top parameter bits */
        
        ...
        
    } else {
        /* 64-bit PSCI function */

        switch (smc_fid) {
        case PSCI_CPU_SUSPEND_AARCH64:  /* 对应我们的CPU_SUSPEND */
            SMC_RET1(handle, psci_cpu_suspend(x1, x2, x3));
        ...
        }
    }

    WARN("Unimplemented PSCI Call: 0x%x \n", smc_fid);
    SMC_RET1(handle, SMC_UNK);
}

接着调用

int psci_cpu_suspend(unsigned int power_state,
             unsigned long entrypoint,
             unsigned long context_id)
{
    ...
    /* Check SBZ bits in power state are zero */
    if (psci_validate_power_state(power_state))
        return PSCI_E_INVALID_PARAMS;
    ...
}

其中,

#define psci_validate_power_state(pstate) (pstate & PSTATE_VALID_MASK)

#define PSTATE_VALID_MASK     0xFCFE0000

所以我们发现,因为 0x0020000 & 0xFCFE0000 = 0x0020000,所以返回了PSCI_E_INVALID_PARAMS,也就对应上我一开始提到的问题,返回了PSCI_RET_INVALID_PARAMS错误码。

那为什么要和0xFCFE0000这个值进行与操作,从PSCI规范我们可以看到,对于32bit的power state参数,它对每一位都是做了严格要求的:

bit filed                    description
31:26                        reserved.must be zero
25:24                        power level
23:17                        reserved.must be zero
16                           state type
15:0                         state ID

因为我们的arm,psci-suspend-param值占用了保留位,所以这是不合法的。

结语

最后的分析原因,可能不一定就是从那个位置返回,不过我们旨在是了解整个流程,到这里我们就结束了这系列的文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章