一個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值佔用了保留位,所以這是不合法的。

結語

最後的分析原因,可能不一定就是從那個位置返回,不過我們旨在是瞭解整個流程,到這裏我們就結束了這系列的文章。

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