Linux的電源管理-休眠與喚醒

寫在前面

爲了理清新平臺系統休眠和喚醒的流程,通過學習其他平臺的電源管理方法,曲徑通幽, 達到目的.

剛接手新平臺,且相應的資料不多,很容易讓人力不從心;我在網上尋找了學習資源,發現韋東山對S3C2440的驅動講解有相關的內容,口碑也不錯,可以作爲一個切入點.

不同平臺一定會有平臺的差異,不同的Linux內核版本之間也會有相應的差異,但思路是一致的,可以總結出來,幫助理解電源管理的開發使用.

本文總結關於系統休眠和喚醒的流程以及開發方法.

Linux電源管理基本框架

下圖所示爲Linux電源管理基本框架. 詳細請參考Linux電源管理(1)_整體架構
本文重點介紹關於Generic PM的使用.

圖1
Generic PM,就是Power Management,如Power Off、
Suspend to RAM、Suspend to Disk、Hibernate等.

關於Suspend, Linux內核提供了四種Suspend: Freeze、Standby、STR(Suspend to RAM)和STD(Suspend to Disk),如下表 . 在用戶空間向”/sys/power/state”文件分別寫入”freeze”、”standby”、”mem”和"disk",即可觸發它們。

模式 描述
freeze 凍結I/O設備,將它們置於低功耗狀態,使處理器進入空閒狀態,喚醒最快,耗電比其它standby, mem, disk方式高
standby 除了凍結I/O設備外,還會暫停系統,喚醒較快,耗電比其它 mem, disk方式高
mem 將運行狀態數據存到內存,並關閉外設,進入等待模式,喚醒較慢,耗電比disk方式高
disk 將運行狀態數據存到硬盤,然後關機,喚醒最慢. 對於嵌入式系統,由於沒有硬盤,所以一般不支持

Linux內核提供的電源管理框架引入了面向對象的思想,理解起來會困難.爲此,很有必要在拋開復雜框架的基礎上,通過實驗驗證suspend的結果,總結suspend的流程後,幫助理解linux的supend流程.

S3C2440 Sleep模式進入與喚醒

對於S3C2440 的power manager有三種模式,如下圖所示,
在這裏插入圖片描述

我們選擇在u-boot中實現Sleep mode作爲suspend, 也就是對應Linux內核實現的mem模式的suspend. 實驗的效果是:在u-boot命令行運行suspend指令後系統進入Sleep Mode; 當按下按鍵後,系統喚醒,繼續接着休眠前的地方執行下去.

進入Sleep Mode的流程

在這裏插入圖片描述

實現代碼如下:

/*
 * [email protected], www.100ask.net
 *
 */

#include <common.h>
#include <command.h>
#include <def.h>
#include <nand.h>
#include <s3c24x0.h>

extern void s3c2440_cpu_suspend(void);
int do_suspend (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]);

U_BOOT_CMD(
    suspend,    1,    0,    do_suspend,
    "suspend - suspend the board\n",
    " - suspend the board"
);
int do_suspend (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
    /* 休眠的實現: */
    
    /* 對於NAND啓動: 要設置EINT23,22,21爲輸入引腳 */
    rGPGCON &= ~((3<<30) | (3<<28) | (3<<26));
   /* 1. 配置GPIO模式,用於喚醒CPU的引腳要設爲中斷功能 */
    /* JZ2440只有S2/S3/S4可用作喚醒源,設置它們對應的GPIO用於中斷模式 */
    /*這幾個IO對於着按鍵,也就是實現按鍵喚醒*/
    /* EINT0 EINT2*/                                                                                                                                                               
    rGPFCON &= ~((3<<0) | (3<<4));
    rGPFCON |= ((2<<0) | (2<<4));
    /*EINT11*/
    rGPGCON &= ~(3<<6);
    rGPGCON |= (2<<6);

    /* 2. 設置INTMSK屏蔽所有中斷: 在sleep模式下,這些引腳只是用於喚醒系統,當CPU正常運行時可以重新設置INTMSK讓這些引腳用於中斷功能 */
    rINTMSK = ~0;

    /* 3. 配置喚醒源 , 爲了能在休眠模式下被喚醒*/
    rEXTINT0 |= (6<<0) | (6<<8); /* EINT0,2雙邊沿觸發 */
    rEXTINT1 |= (6<<12);   /* EINT11雙邊沿觸發 */

    /* 4. 設置MISCCR[13:12]=11b, 使得USB模塊進入休眠 */
    rMISCCR |= (3<<12);

    /* 5. 在GSTATUS[4:3]保存某值, 它們可以在系統被喚醒時使用 */
    //rGSTATUS3 = ;  /* 喚醒時首先執行的函數的地址 */
    //rGSTATUS4 = ;  /*  */

    /* 6. 設置 MISCCR[1:0] 使能數據總線的上拉電阻 */
    rMISCCR &= ~(3);

    /* 7. 清除 LCDCON1.ENVID 以停止LCD */
    rLCDCON1 &= ~1;

    /* 8~12使用匯編在s3c2440_cpu_suspend函數來實現,參考內核源碼:
     *    arch\arm\mach-s3c2410\sleep.S
    */

    /* 8. 讀這2個寄存器: rREFRESH and rCLKCON, 以便填充TLB
     *    如果不使用MMU的話,這個目的可以忽略
     */
/* 9. 設置 REFRESH[22]=1b,讓SDRAM進入self-refresh mode */

    /* 10. 等待SDRAM成功進入self-refresh mode  */

    /* 11.設置 MISCCR[19:17]=111b以保護SDRAM信號(SCLK0,SCLK1 and SCKE) */

    /* 12. 設置CLKCON的SLEEP位讓系統進入sleep mode */
    printf("suspend ...");
    delay(1000000); //保證printf打印完整
    s3c2440_cpu_suspend();  /* 執行到這裏就不會返回,直到CPU被喚醒 */

    /* 恢復運行: 重新初始化硬件 */
    serial_init();
    printf("wake up\n");


    return 0;
}

代碼解釋

  1. 對於進入Sleep Mode的第5步,主要目的是保存休眠前的PC指針到GSTATUS寄存器,以便喚醒後恢復現場. 這一步的實現,放到 s3c2440_cpu_suspend函數中.

其中s3c2440_cpu_suspend函數的功能是讓系統真正進入休眠狀態,實現流程如下:

    /* suspend.S  [email protected], www.100ask.net
     * s3c2410_cpu_suspend
     * put the cpu into sleep mode
    */
#define S3C2440_REFRESH_SELF        (1<<22)
#define S3C2440_MISCCR_SDSLEEP        (7<<17)
#define S3C2440_CLKCON_POWER         (1<<3)

#define GSTATUS2       (0x560000B4)
#define GSTATUS3       (0x560000B8)
#define GSTATUS4       (0x560000BC)

#define REFRESH        (0x48000024)
#define MISCCR         (0x56000080)
#define CLKCON         (0x4C00000C)

.globl s3c2440_cpu_suspend @將s3c2440_cpu_suspend函數聲明爲全局函數
    @@ prepare cpu to sleep
s3c2440_cpu_suspend:
    stmdb    sp!, { r4-r12,lr }  @保存寄存器r4 - r12到棧中

    /* GSTATUS3中存放喚醒時要執行的函數 */
    ldr r0, =s3c2440_do_resume
    ldr r1, =GSTATUS3
    str r0, [r1]
    /* GSTATUS4中存放休眠前的棧信息 */
    ldr r1, =GSTATUS4
    str sp, [r1]
  /* 8. 讀這2個寄存器: rREFRESH and rCLKCON, 以便填充TLB
     *    如果不使用MMU的話,這個目的可以忽略
     */
 /* 9. 設置 REFRESH[22]=1b,讓SDRAM進入self-refresh mode */

    /* 10. 等待SDRAM成功進入self-refresh mode  */

    /* 11.設置 MISCCR[19:17]=111b以保護SDRAM信號(SCLK0,SCLK1 and SCKE) */

    /* 12. 設置CLKCON的SLEEP位讓系統進入sleep mode */
    ldr    r4, =REFRESH
    ldr    r5, =MISCCR
    ldr    r6, =CLKCON
    ldr    r7, [ r4 ]        @ get REFRESH
    ldr    r8, [ r5 ]        @ get MISCCR
    ldr    r9, [ r6 ]        @ get CLKCON

    orr    r7, r7, #S3C2440_REFRESH_SELF    @ SDRAM sleep command
    orr    r8, r8, #S3C2440_MISCCR_SDSLEEP @ SDRAM power-down signals
    orr    r9, r9, #S3C2440_CLKCON_POWER    @ power down command

    teq    pc, #0            @ first as a trial-run to load cache
    bl    s3c2440_do_sleep
    teq    r0, r0            @ now do it for real
    b    s3c2440_do_sleep    @   

    @@ align next bit of code to cache line
    .align    5   
s3c2440_do_sleep:
    streq    r7, [ r4 ]            @ SDRAM sleep command
    streq    r8, [ r5 ]            @ SDRAM power-down config
    streq    r9, [ r6 ]            @ CPU sleep
1:    beq    1b
    mov    pc, r14

s3c2440_do_resume:
    /* 從start.S的wake_up返回到s3c2440_do_resume函數 */
    /* 從GSTATUS4取出棧信息 */
    ldr r1, =GSTATUS4
    ldr sp, [r1]
  /*恢復棧*/
    ldmia    sp!,     { r4-r12,pc }

喚醒流程

在這裏插入圖片描述
從以上的描述中的第2點可以得知, 系統從Sleep模式退出是需要重新啓動u-boot的, 區別是power-up還是wake-up的方法是讀取GSTATUS2寄存器.如果值爲1,代表是從Sleep Mode過來的,就會執行相應的流程恢復休眠前的現場; 反之,則執行正常啓動流程.
在這裏插入圖片描述
下面是關於u-boot啓動代碼的修改

/*cpu/arm920t/start.S*/
+#define GSTATUS2       (0x560000B4)
+#define GSTATUS3       (0x560000B8)
+#define GSTATUS4       (0x560000BC)
+
+#define REFRESH        (0x48000024)
+#define MISCCR         (0x56000080)
+#define CLKCON         (0x4C00000C)
 
 .globl _start
 _start:        b       reset
@@ -167,6 +178,25 @@ reset:
 #endif
 #endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */
 
+#ifndef CONFIG_SKIP_LOWLEVEL_INIT
+               /* 在u-boot未改動前,clock_init C函數的執行
+                  需要在SDRAM設置棧以後, 由於
+            Sleep模式SDRAM處於自刷新狀態,爲了避免破環狀態,
+            我們提前調用clock_init, 
+                   並把棧設置在片內內存,
+                   設置SP指向片內內存SRAM */
+               /*對於nand啓動,可以這麼設置*/
+               ldr sp, =4092
+              /*通過判斷指向4092的內存能否讀寫成功來判斷是nand啓動還是nor啓動.如果是nand啓動,sp指向4092,如果是nor啓動,sp指向0x40000000+4096*/
+               ldr r0, =0x12345678
+               str r0, [sp]
+               ldr r1, [sp]
+               cmp r0, r1
+         /*對於nor啓動, 可以這麼設置*/
+               ldrne sp, =0x40000000+4096
+               bl clock_init
+#endif    
+
+       /* 2. 根據 GSTATUS2[1]判斷是復位還是喚醒 */     
+       ldr r0, =GSTATUS2
+       ldr r1, [r0]
+       tst r1, #(1<<1)  /* r1 & (1<<1) */
+       bne wake_up     
+
+
+
        /*
         * we do sys-critical inits only at reboot,
         * not when booting from ram!
@@ -190,7 +220,7 @@ stack_setup:
        sub     sp, r0, #12             /* leave 3 words for abort-stack    */
 
 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
-    bl clock_init
+//    bl clock_init
 #endif    
 
 #ifndef CONFIG_SKIP_RELOCATE_UBOOT
@@ -260,6 +290,31 @@ SetLoadFlag:
 
 _start_armboot:        .word start_armboot
 
+/* 1. 按下按鍵 */
+wake_up:
      str r1, [r0]  /* clear GSTATUS2 */
+       /* 3. 設置 MISCCR[19:17]=000b, 以釋放SDRAM信號 */
+       ldr r0, =MISCCR
+       ldr r1, [r0]
+       bic r1, r1, #(7<<17)
+       str r1, [r0]
+               
+       /* 4. 配置s3c2440的memory controller */
+       bl      cpu_init_crit
+       
+       /* 5. 等待SDRAM退出self-refresh mode */
+       mov r0, #1000
+1:     subs r0, r0, #1
+       cmp r0, #0
+       bne 1b
+       
+       /* 6. 根據GSTATUS[3:4]的值來運行休眠前的函數 PC指針指向 s3c2440_do_resume
+        */
+       ldr r0, =GSTATUS3
+       ldr r1, [r0]
+       mov pc, r1
+       
+

實驗現象

在u-boot命令行下,
進入suspend模式,休眠.
在這裏插入圖片描述
按下按鍵,退出suspend模式,喚醒.
在這裏插入圖片描述

S3C2440 Generic PM之suspend

在瞭解S3C2440 u-boot下添加休眠和喚醒的流程後,總結下來也就是兩幅圖:
在這裏插入圖片描述
在這裏插入圖片描述
對於Linux的Generic PM框架,本質是也就是把上面這部分與板級(platform)有關的內容放置到框架的相應位置(也就是下圖的Platform dependent PM的位置),就可以實現在Linux下的suspend和resume了.

介紹一下Generic PM框架,參考Linux電源管理(6)_Generic PM之Suspend功能
在這裏插入圖片描述
下面以suspend to RAM爲例,分析從用戶空間ehco > mem /sys/power/state進入休眠再到按鍵執行喚醒的過程. 參考Linux電源管理

suspend流程

如下圖所示,實際上寫入文件調用的函數就是state_store.
在這裏插入圖片描述
驅動程序裏休眠相關的電源管理函數的調用過程:prepare—>suspend—>suspend_late—>suspend_noirq
其中圖中最下面的一行:suspend_ops->enter就是對應platform dependent PM相關的代碼,把u-boot中進入休眠模式的代碼移植過來即可.
在S3C2440中對應的platform depend PM代碼在arch/arm/plat-samsung/pm.c

Platform dependent PM(針對CPU芯片相關)

//arch/arm/plat-samsung/pm.c  kernel version:3.4.2
...
static const struct platform_suspend_ops s3c_pm_ops = {
    .enter        = s3c_pm_enter,
    .prepare    = s3c_pm_prepare, //對應圖中的suspend_ops->enter,系統進入休眠模式調用的platform dependent PM相關的代碼
    .finish        = s3c_pm_finish,
    .valid        = suspend_valid_only_mem,
};
static int s3c_pm_enter(suspend_state_t state)
{
    /* ensure the debug is initialised (if enabled) */

    s3c_pm_debug_init();

    S3C_PMDBG("%s(%d)\n", __func__, state);

    if (pm_cpu_prep == NULL || pm_cpu_sleep == NULL) {
        printk(KERN_ERR "%s: error: no cpu sleep function\n", __func__);
        return -EINVAL;
    }

    /* check if we have anything to wake-up with... bad things seem
     * to happen if you suspend with no wakeup (system will often
     * require a full power-cycle)
    */

    if (!any_allowed(s3c_irqwake_intmask, s3c_irqwake_intallow) &&
        !any_allowed(s3c_irqwake_eintmask, s3c_irqwake_eintallow)) {
        printk(KERN_ERR "%s: No wake-up sources!\n", __func__);
        printk(KERN_ERR "%s: Aborting sleep\n", __func__);
        return -EINVAL;
    }

    /* save all necessary core registers not covered by the drivers */

    samsung_pm_save_gpios();
    samsung_pm_saved_gpios();
    s3c_pm_save_uarts();
    s3c_pm_save_core();

    /* set the irq configuration for wake */

    s3c_pm_configure_extint();

    S3C_PMDBG("sleep: irq wakeup masks: %08lx,%08lx\n",
        s3c_irqwake_intmask, s3c_irqwake_eintmask);

    s3c_pm_arch_prepare_irqs();

    /* call cpu specific preparation */
    pm_cpu_prep();

    /* flush cache back to ram */

    flush_cache_all();

    s3c_pm_check_store();

    /* send the cpu to sleep... */

    s3c_pm_arch_stop_clocks();

    /* this will also act as our return point from when
     * we resume as it saves its own register state and restores it
     * during the resume.  */

    cpu_suspend(0, pm_cpu_sleep);

    /* restore the system state */

    s3c_pm_restore_core();
    s3c_pm_restore_uarts();
    samsung_pm_restore_gpios();
    s3c_pm_restored_gpios();

    s3c_pm_debug_init();

    /* check what irq (if any) restored the system */

    s3c_pm_arch_show_resume_irqs();

    S3C_PMDBG("%s: post sleep, preparing to return\n", __func__);

    /* LEDs should now be 1110 */
    s3c_pm_debug_smdkled(1 << 1, 0);
 s3c_pm_check_restore();

    /* ok, let's return from sleep */

    S3C_PMDBG("S3C PM Resume (post-restore)\n");
    return 0;
}

代碼說明:
其中27-32行, 檢查是否設置了喚醒源,如果沒有,就直接退出,禁止進入休眠模式. 對於設置喚醒源,在後面按鍵驅動中會介紹如何設置喚醒源.
34-67行, 按照進入Sleep Mode的流程執行相應的工作,一旦調用到67行,跳轉到pm_cpu_sleep, 系統進入休眠狀態. 直到有喚醒源,才能喚醒,繼續往下執行.

resume流程

在這裏插入圖片描述
驅動程序裏喚醒相關的電源管理函數的調用過程:resume_noirq—>resume_early—>resume->complete
對於上面arch/arm/plat-samsung/pm.c中的代碼的71-90行,就是完成上圖的resume過程.

修改按鍵驅動中增加喚醒源

/* 參考drivers\input\keyboard\gpio_keys.c */
/*參考www.100ask.com*/
#include <linux/module.h>
#include <linux/version.h>

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/irq.h>

#include <asm/gpio.h>
#include <asm/io.h>
//#include <asm/arch/regs-gpio.h>

struct pin_desc{
    int irq;
    char *name;
    unsigned int pin;
    unsigned int key_val;
};

struct pin_desc pins_desc[4] = {
    {IRQ_EINT0,  "S2", S3C2410_GPF(0),   KEY_L},
    {IRQ_EINT2,  "S3", S3C2410_GPF(2),   KEY_S},
    {IRQ_EINT11, "S4", S3C2410_GPG(3),   KEY_ENTER},
    {IRQ_EINT19, "S5",  S3C2410_GPG(11), KEY_LEFTSHIFT},
};

static struct input_dev *buttons_dev;
static struct pin_desc *irq_pd;
static struct timer_list buttons_timer;

static irqreturn_t buttons_irq(int irq, void *dev_id)
{
    /* 10ms後啓動定時器 */
    irq_pd = (struct pin_desc *)dev_id;
    mod_timer(&buttons_timer, jiffies+HZ/100);
    return IRQ_RETVAL(IRQ_HANDLED);
}

static void buttons_timer_function(unsigned long data)
{
struct pin_desc * pindesc = irq_pd;
    unsigned int pinval;

    if (!pindesc)
        return;

    pinval = s3c2410_gpio_getpin(pindesc->pin);

    if (pinval)
    {
        /* 鬆開 : 最後一個參數: 0-鬆開, 1-按下 */
        input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
        input_sync(buttons_dev);
    }
    else
    {
        /* 按下 */
        input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
        input_sync(buttons_dev);
    }
}

static int buttons_init(void)
{
    int i;

    /* 1. 分配一個input_dev結構體 */
    buttons_dev = input_allocate_device();;

    /* 2. 設置 */
    /* 2.1 能產生哪類事件 */
    set_bit(EV_KEY, buttons_dev->evbit);
    set_bit(EV_REP, buttons_dev->evbit);

    /* 2.2 能產生這類操作裏的哪些事件: L,S,ENTER,LEFTSHIT */
    set_bit(KEY_L, buttons_dev->keybit);
    set_bit(KEY_S, buttons_dev->keybit);
    set_bit(KEY_ENTER, buttons_dev->keybit);
    set_bit(KEY_LEFTSHIFT, buttons_dev->keybit);

    /* 3. 註冊 */
    input_register_device(buttons_dev);

    /* 4. 硬件相關的操作 */
    init_timer(&buttons_timer);
    buttons_timer.function = buttons_timer_function;   
    add_timer(&buttons_timer);

    for (i = 0; i < 4; i++)
    {
        request_irq(pins_desc[i].irq, buttons_irq, (IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING), pins_desc[i].name, &pins_desc[i]);
    }

    /* 指定這些中斷可以用於喚醒系統 */
    irq_set_irq_wake(IRQ_EINT0, 1);
    irq_set_irq_wake(IRQ_EINT2, 1);
    irq_set_irq_wake(IRQ_EINT11, 1);

    return 0;
}

static void buttons_exit(void)
{
    int i;

    irq_set_irq_wake(IRQ_EINT0, 0);
    irq_set_irq_wake(IRQ_EINT2, 0);
    irq_set_irq_wake(IRQ_EINT11, 0);

    for (i = 0; i < 4; i++)
    {
        free_irq(pins_desc[i].irq, &pins_desc[i]);
    }

    del_timer(&buttons_timer);
    input_unregister_device(buttons_dev);
    input_free_device(buttons_dev);
}

module_init(buttons_init);

module_exit(buttons_exit);

MODULE_LICENSE("GPL");
                  

代碼說明
設置喚醒源最主要的是105 -107行的函數irq_set_irq_wake.
這個函數本質上是通過api設置寄存器使能中斷功能. (注意:這些中斷引腳是支持喚醒功能的)

Device PM(針對每一個驅動)

上面針對suspend流程的實現是修改Platform dependent PM相關的代碼(針對CPU芯片相關), 在休眠時只是執行datasheet中的必要要求,對於一些外設,比如網卡、聲卡等並沒有關閉.

對於外設級別的電源管理,Linux Generic PM框架提供了相應的接口,只要在註冊設備時加入相應參數,即可實現系統休眠或者喚醒時會調用各個驅動所註冊的suspend或者resume回調函數.

下面以LCD爲例,加入外設電源管理,使得在系統進入Mem休眠模式後,能關閉LCD背光燈,喚醒時打開LCD背光燈重新顯示.

//Lcd.c
...//頭文件
static struct dev_pm_ops lcd_pm = {
    .suspend = lcd_suspend,
    .resume  = lcd_resume,                                                                                                                                                         
};

struct platform_driver lcd_drv = { 
    .probe        = lcd_probe,
    .remove        = lcd_remove,
    .driver        = { 
        .name    = "mylcd",
        .pm     = &lcd_pm,
    }   
};
static int lcd_probe(struct platform_device *pdev)
{
    return 0;
}
static int lcd_remove(struct platform_device *pdev)
{
    return 0;
}
static int lcd_suspend(struct device *dev)
{
   ...
    *gpbdat &= ~1;     /* 關閉背光 */
    return 0;
}
static int lcd_resume(struct device *dev)
{
  ...
    *gpbdat |= 1;     /* 輸出高電平, 使能背光 */
    return 0;
}
static int lcd_init(void)
{
    platform_device_register(&lcd_dev);
    platform_driver_register(&lcd_drv);
    ...//LCD相應的初始化
    return 0;
}
static void lcd_exit(void)
{
  ...//LCD相應的註銷
   platform_device_unregister(&lcd_dev);
    platform_driver_unregister(&lcd_drv);
}

module_init(lcd_init);
module_exit(lcd_exit);

MODULE_LICENSE("GPL");

代碼分析

  1. 在LCD驅動的基礎上註冊了一個平臺設備和驅動,當系統進入休眠模式時會調用lcd_suspend函數關閉燈光,當喚醒退出休眠模式時會調用lcd_resume打開燈光.
  2. 重點在於第13行的結構體.

總結

上面介紹的是系統休眠或喚醒模式下的電源管理, 對於不同平臺,都需要按照DataSheet的說明選擇休眠模式,修改u-boot和kernel.
那在正常運行模式下,能否單獨對設備進行電源管理呢,請期待下一節更新Linux電源管理-Runtime PM

發佈了29 篇原創文章 · 獲贊 64 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章