WINCE6.0+S3C6410睡眠和唤醒的实现

 

原文:http://cky0612.blog.163.com/blog/static/27478916201161011450822/

WINCE6.0+S3C6410睡眠和唤醒的实现

********************************LoongEmbedded*****************
作者:LoongEmbedded(kandi)
时间:2011.07.10

类别:WINCE bootloader开发
********************************LoongEmbedded*****************

 

备注:本文基于Real6410开发板来实现

PmSetSystemPowerState_I()->PlatformSetSystemPowerState()

1.      睡眠模式及唤醒

1.1睡眠模式描述

下面见datasheet的描述

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图1

在睡眠模式下,除了ALIVE和RTC模块外的所有硬件逻辑是通过外部电源调节器关闭的。睡眠模式支持最长的待机时间,在这种模式下,用户软件必须保存所有的内部状态到外部存储设备中。ALIVE模块等待一个外部唤醒事件并且RTC保存时间信息。用户软件可以配置唤醒源和I/O引脚的状态。

 

在睡眠模式下,系统电源域如下图:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图2

在此我们看CPU的datasheet是怎么描述ALIVE模块的:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图3

 

1.2进入睡眠模式

系统如何进入睡眠模式呢,我们还是来看datasheet的描述吧:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图4

备注:这里的SYSCON是指system controller,也即系统控制器。

1)      用户软件通过设置PWR_CFG[6:5]=2’b11配置ARM1176 STANDBYWFI进入睡眠模式。

2)      用户软件通过MCR指令(MCR p5,0,Rd,c7,c0,4)来产生STANDBYWFI信号。

3)      系统控制器请求总线控制器完成当前的AHB总线的处理。

4)      AHB总线控制器当前的总线处理完成后,发送应答信息给系统控制器。

5)      系统控制器请求DOMAIN-V完成当前的AXI总线处理。

6)      AXI总线控制器当前的总线处理完成后,发送应答信息给系统控制器。

7)      因为外部存储器(这里是指SDRAM,我的理解)的内容在睡眠模式下必须要保存起来(如果不保存起来,系统在唤醒的时候就无法从原来进入睡眠的地方唤醒),系统控制器请求外部存储控制器进入自刷新模式(这样就可以让保存在SDRAM中的内容得以保持)。

8)      当存储控制器进入自刷新模式后发送确认信息。

9)      如果PLL还在使用,系统控制器改变时钟源由PLL输出改为外部振荡器输出。

10)   系统控制器关闭PLL操作和晶体振荡器。

11)   最后,系统控制器通过让XPWRRGTON引脚维持在低电平来禁用外部电源源,XPWRRGTON信号控制外部调节器。

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图5

 

1.3唤醒源

下图描述了系统各种模式下的唤醒源

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图6

 

1.4退出睡眠模式

当我们激活唤醒源后,系统会进入退出睡眠模式的流程,如下所示:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图7

1)      系统控制器通过维持XPWRRGTON引脚为高电平来启动外部电源,并且通过配置PWR_STABLE寄存器来配置等待时钟稳定输出的时间。 

2)      系统控制器产生系统时钟,包括HCLK、PCLK和ARMCLK。

3)      系统控制器释放复位信号,包括HRESETn和PRESETn。

4)      系统控制器释放ARM复位信号。

 

2.      供电电路

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图8

2.1   ALIVE模块供电电路

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图9

ALIVE模块的电压范围是1.15到1.25V

2.2   RTC模块供电电路

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图10

RTC模块工作电压范围是1.7到3.3V

 

Memory interface VDDm0工作电压范围是1.7到3.6,VDDm1是1.75到2.7V

2.3    

 

 

 

3.      睡眠和唤醒的具体实现

3.1睡眠的过程

3.1.1进入睡眠的几种方式

一般情况下,我们有两种方式让系统进入睡眠状态,如下:

1)      在系统没有用户相互并且没有其他工作的情况下,在时间timeout后系统自动进入睡眠模式,见common.reg中相关的注册表信息:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power\Timeouts]

; @CESYSGEN IF PM_PM_DEFAULT_PDD

    "ACUserIdle"=dword:3c               ; in seconds

    "ACSystemIdle"=dword:12c    ; in seconds

    "ACSuspend"=dword:0                 ; in seconds

    "BattUserIdle"=dword:3c             ; in seconds

    "BattSystemIdle"=dword:b4   ; in seconds

    "BattSuspend"=dword:12c             ; in seconds

; @CESYSGEN ENDIF ; PM_PM_DEFAULT_PDD

; @CESYSGEN IF PM_PM_PDA_PDD

; @CESYSGEN ENDIF ; PM_PM_PDA_PDD

当然了,如果"BattSuspend"=dword:12c 该为"BattSuspend"=dword:0,那么系统就不会自动进入睡眠模式。从上面的信息可知,如果没有用户的动作,1分钟后系统会进入user idle;此后三分钟进入system idle;接着过了6分钟进入suspend idle,也就是说第11分钟开始的时候进入suspend idle。

2)      选择”开始->挂起”。

3)      我们在应用程序和驱动中调用下面函数

SetSystemPowerState( NULL, POWER_STATE_SUSPEND, POWER_FORCE )

起始这种方式和2)本质是一样的。

3.1.2 BSP包中进入睡眠的流程

我们以上面提到的第3)中方式来学习和描述,在按键驱动中我们检测到按下GPL9/EINT17按下时,我们调用SetSystemPowerState函数来让系统进入睡眠模式:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图11

这里调用了SetSystemPowerState函数之后,在BSP包中的流程如下:

⑴电源管理器(PM)会根据common.reg中下面的内容

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power\State\Suspend\{98C5250D-C29A-4985-AE5F-AFE5367E5006}]

"Default"=dword:4           ; D4

获取到系统睡眠(也即挂起)状态对应的系统电源level为D4,这样,PM就通知支持电源管理的驱动先对这个电源状态做相应的处理,比如对于背光驱动来说,因为其支持电源管理,所以PM会以控制码调用BKL_IOControl函数IOCTL_POWER_SET,见下图

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图12

那么背光驱动BackLightSetState函数是如何响应这个电源请求的呢?见其实现:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图13

一般系统中有多个驱动支持电源管理,那么先调用哪个呢?系统根据每个驱动注册表信息的Order值来调用的,这个值越大,就越先被调用,处理完支持电源管理驱动的XXX_IOControl()之后,根据同样的依据原则来调用每个驱动的XXX_PowerDown函数,此后,就会调用OEMPowerOff()来让系统进入睡眠模式。

 

⑵调用OEMPowerOff()函数

此函数在s3c6410_sec_v1\oal\power\off.c下定义,其实现如下:

1)      调用BSPPowerOff函数

此函数的动作如下:

①调用VFL_Sync函数来等待NAND FLASH擦除或写操作完成。

②调用ChangeClockDivider()函数来改变时钟,包括MFC、PCLK、HCLK*2和ARMCLK的时钟。

③关闭RTC控制,当然了,如果是需要通过RTC来唤醒的,这里就不能关闭。

④调用BSPConfigGPIOforPowerOff函数来根据硬件设计的具体情况配置GPIO口为相应的模式,原则就是减少功耗。

⑤调用S3C6410_WakeUpSource_Configure函数来配置唤醒源,这里的唤醒源是GPN11/EINT11,配置为下降沿触发,此函数体如下:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图14

结合图6,可知MMC0到MMC2不是睡眠模式下的唤醒源,故关闭,在本设计中,只用到按键作为唤醒源。

 

2)      保存VIC(Vectored Interrupt Controller,矢量中断控制器)寄存器。

3)      保存DMAC(DMA控制器)寄存器。

4)      保存GPIO寄存器。

5)      保存系统控制器寄存器,也即从0x7E00F000到0x7E00FA0C地址对应的寄存器。

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图15

6)      控制USB 电源

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图16

下面我来看LCD接口的配置图:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图17

7)      调用OALCPUPowerOff函数让CPU进入睡眠模式

此函数在src\oal\oallib\startup.s中定义,下面按照调用的顺序来学习这个函数

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图18

1)      保存SVC模式下的寄存器到堆栈中。

这里先大概学习一下啊堆栈,堆栈是在RAM存储器(这里是指SDRAM)中开辟的一个特定的存储区域,在这个区域,信息的存入(入栈)和取出(出栈)是按照“后进先出”的原则进行存取的。在子程序调用时要保存返回地址,在中断处理过程中要保存断点地址,进入子程序和中断处理后还要保留通用寄存器的值。子程序执行完毕和中断处理完毕返回时,又要恢复通用寄存器的值,并分别将返回地址或断点地址恢复到指令指针寄存器中。这些功能都要通过堆栈来实现。

 

堆栈的一端是固定的(也就是堆栈有个基地址),另一端是浮动的(但有堆栈的上限地址)。堆栈固定端是堆栈的底部,称为栈底,堆栈浮动端可以存入或取出数据。向堆栈存入数据时,新存入的数据存放在之前存入数据的上面,而最先存入的数据被存到堆栈底部,最后存入的数据堆放在堆栈顶部,这称为栈顶。

 

由于堆栈顶部是浮动的,为了指示现在堆栈中存放数据的位置,就需要一个堆栈指针SP(R13),它始终指向堆栈的顶部,这样,堆栈中数据的存入和取出都由SP来指示。堆栈指针通常可以指向不同的位置,堆栈可以分为满栈和空栈两种,当栈指针指向栈顶元素,也即最后一个入栈的数据元素时,称为满(Full)栈;当栈指针指向与栈顶元素相邻的一个可用数据单元时,称为空(Empty)栈。另外根据数据栈的增长方向分为递增栈和递减栈两种,当数据栈向内存地址减少的方向增长时,称为递减(Descending)栈;当数据栈向内存地址增加的方向增长时,称为递增(Ascending)栈。综合这两种特点可有一下4中数据栈:

满递减栈:FD(Full Descending)

空递减栈:ED(Empty Descending)

满递增栈:FA(Full Ascending)

空递增栈:EA(Empty Ascending)

 

由于要遵守ATPCS规则(ARM-Thumb Produce Call Standard,ARM和Thumb程序调用基本规则),而ATPCS规定数据栈为FD(Full Descending,满栈递减)类型,所以当一个数据(32位)入栈时,SP(R13)的值-4向下浮动指向下一个地址,即新的栈顶;当数据出栈时,SP(R13)的值+4向上浮动指向下新的栈顶。ARM中入栈和出栈操作都是以字(32位)为单位的。

 

下面来分析这部分代码,我们知道R13常用作堆栈指针(sp),用于保存当前堆栈地址。SVC:表示处理器模式为管理模式。Stm指令是多寄存器存储指令,在此表示把r4到r12这9个寄存器中的值存储到基址寄存器(sp)所指示的一片连续存储器中,stm后面的db表示每次传送前地址值减(-4);!表示数据加载与存储完毕之后,将最后的地址写入基址寄存器,如不使用!,则基址寄存器的内容不改变。假设sp=0x90050,stmdb   sp!, {r4-r12}的操作如下:

第一步:在存储前,0x90050的值减4(对于ARM指令是4,对Thumb指令是2),也即为0x9004c。

第二步:把寄存器r12的值存储到0x9004c指向的存储单元。

第三步:0x9004c-4=0x90048。

第四步:把寄存器r11的值存储到0x90048指向的存储单元。

……………………………………

最后一步:就是sp=0x90030

 

lr:R14寄存器也称为子程序连接器(Subroutine Link Register)或连接寄存器LR。Lr得到R15(程序计数器PC)的备份,也即用于保存子程序的返回地址,以便子程序调用完成后利用此地址返回到调用处,也就是OEMPowerOff()函数调用OALCPUPowerOff()函数的地方。

 

2)      保存唤醒函数到指定的内存地址处

IMAGE_SLEEP_DATA_UA_START表示SDRAM中睡眠数据区域的基地址,这里= 0xA0000000+ 0x00028000=0xA0028000,也就是说睡眠模式下的数据保存在此基地址的起始地址处,那么这块区域是多大呢?这个基地址和这块区域的大小是在config.bib下定义的,如下:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图19

当然了,我们在image_cfg.inc下要根据这些值来设定好,这里我们把WakeUp_Address函数的地址保存在0xA0028000指向的存储单元处,为后面的唤醒做好准备,WakeUp_Address函数后面再介绍。

 

3)      把控制寄存器C1的SBZ位清零及SBO置位后C1寄存器的值保存在睡眠数据区域

mrc        p15, 0, r2, c1, c0, 0

把协处理器CP15的控制寄存器C1的值读取到r2中,这里先来学习一下C1:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图20

ldr        r0, =SYSCTL_SBZ_MASK    ; Should Be Zero Mask for System Control Register

bic        r2, r2, r0

SYSCTL_SBZ_MASK的值为0xCC1A0000,上面的代码主要是对C1寄存器的SBZ(should be zero)位清零,结合下图对C1寄存器位的描述理解:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图21

ldr        r0, =SYSCTL_SBO_MASK    ; Should Be One Mask for System Control Register

orr        r2, r2, r0

SYSCTL_SBO_MASK= 0x00000070,上面的代码是对C1寄存器的SBO(should be one)位置1,见图21的描述。

str        r2, [r3], #4                ; [SleepState_SYSCTL]

把对C1寄存器相关位修改之后的值保存到0xA0028004存储单元处。

 

4)      读取TTB寄存器的值并且对其SBZ清零后保存在睡眠数据区域

mrc         p15, 0, r2, c2, c0, 0        ; load r2 with TTB Register0

读取协处理器CP15的TTBR0(Translation Table Base Register 0)寄存器的值到r2寄存器中,这个寄存器用于保存第一级转换表的物理地址,下面是TTBR0寄存器的描述

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图22

根据我的理解,TTB寄存器保存的第一级转换表的物理地址就是g_oalAddressTable[DATA]数据的基地址,此数组在oemaddrtab_cfg.inc文件中定义,此数组是相当的重要。

ldr        r0, =MMUTTB_SBZ_MASK    ; Should Be Zero Mask for TTB Register0

bic        r2, r2, r0

str        r2, [r3], #4                ; [SleepState_MMUTTB0]

MMUTTB_SBZ_MASK=0x00001FE0,上面的代码是对TTB寄存器的SBZ清零,清零之后TTB寄存器的值保存在0xA0028008内存单元处。

 

5)      读取TTBR1寄存器的值保存在睡眠数据区域

mrc        p15, 0, r2, c2, c0, 1        ; load r2 with TTB Register1

str        r2, [r3], #4                ; [SleepState_MMUTTB1]

读取协处理器CP15的TTBR1(Translation Table Base Register 1)寄存器的值到r2寄存器中,这个寄存器用于保存第一级页表的物理地址,下面是TTBR1寄存器的描述

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图23

把TTBR1寄存器的值保存在0xA002800C内存单元处。

 

6)      读取TTBCR寄存器的值保存在睡眠数据区域

mrc         p15, 0, r2, c2, c0, 2        ; load r2 with TTB Control Register

str        r2, [r3], #4                ; [SleepState_MMUTTBCTL]

读取TTBCR(Translation Table Base control Register)的值保存在0xA0028010内存单元处,下面见TTBCR的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图24

 

 

7)      读取DACR寄存器的值保存在睡眠数据区域

mrc        p15, 0, r2, c3, c0, 0        ; load r2 with Domain Access Control Register

str        r2, [r3], #4                ; [SleepState_MMUDOMAIN]

读取DACR(Domain Access Control Register)的值保存在0xA0028014内存单元处,此寄存器定义了ARM处理器的16个域的访问权限。

 

下面继续学习OALCPUPowerOff函数

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图25

8)      保存SVC模式下的堆栈指针SP(也即寄存器R13的内容)和SPSR寄存器的值保存到睡眠数据区域

因为CPU的每种工作模式(7种)均有自己独立的物理寄存器R13,一般都要初始化每种模式下的R13,使其指向该工作模式的栈空间,这样,当程序的运进入异常模式时,可以将需要保护的寄存器存入R13指向的堆栈,而当程序从异常模式返回时,则从对应的堆栈中恢复,采用这样的方式可以保证异常发生后程序的正常执行。这里就是把堆栈指针SP(也即寄存器R13的内容)保存到0xA0028018内存单元处。

 

接下来把SPSR寄存器的内容保存在0xA002801C内存单元处,这里CPSR和SPSR寄存器及相关知识:

ARM体系结构中包含一个当前程序状态寄存器CPSR(Current Program Status Resigter)和5个备份的程序状态寄存器(SPSRs,Saved Program Status Resigter)。

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 图26

CPSR可在任何工作模式下被访问,用来保存ALU(运算器)中的当前操作信息、控制使能和禁止中断、设置处理器的工作模式等。而备份的程序状态寄存器用来进行异常处理,程序状态寄存器的格式如下:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图27

每种工作模式下(除了用户模式和系统模式,因为这两种模式不属于异常模式)都有一个专用的SPSR,当异常发生时,SPSR用户保存CPSR的当前直接,从异常退出时则可由SPSR来恢复CPSR,因为SVC模式是异常模式,所以需要保存SPSR,保存在0xA002801C内存单元处。

9)      把FIQ模式下的寄存器值保存在睡眠数据区域

mov        r1, #Mode_FIQ | NOINT        ; Enter FIQ mode, no interrupts

msr        cpsr, r1

Mode_FIQ=0x11,NOINT=0xC0,结合图27理解,在此是让处理器进入管理模式(SVC),并且禁止FIQ和IRQ中断。

mrs        r2, spsr                    ; Status Register

stmia    r3!, {r2, r8-r12, sp, lr}        ; Store FIQ mode registers [SleepState_FIQ_SPSR~SleepState_FIQ_LR]

把FIQ模式下的SPSR,r8-r12,sp和lr分别寄存器保存到0xA002801C到0xA002803C内存单元处。

10)   把ABT模式下的寄存器值保存在睡眠数据区域

进入终止模式(ABT),并且并且禁止FIQ和IRQ中断,然后把此模式下的spsr,sp和lr寄存器保存在0xA0028040到0xA0028048内存单元处。

 

11)   把IRQ模式下的寄存器值保存在睡眠数据区域

进入中断请求模式(IRQ),并且并且禁止FIQ和IRQ中断,然后把此模式下的spsr,sp和lr寄存器保存在0xA0028040到0xA0028048内存单元处。

12)   把UND模式下的寄存器值保存在睡眠数据区域

进入未定义模式(UND),并且并且禁止FIQ和IRQ中断,然后把此模式下的spsr,sp和lr寄存器保存在0xA002804C到0xA0028054内存单元处。

13)   把系统模式下的寄存器值保存在睡眠数据区域

进入系统模式(SYS),并且并且禁止FIQ和IRQ中断,然后把此模式下的spsr和lr寄存器保存在0xA0028058到0xA002805C内存单元处。

 

14)   把fpscr和fpscr寄存器保存在睡眠数据区域

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 图28

Fpscr(Floating-Point Status and Control Register),fpscr(Floating-point exception register)

接着调用下面的

fstmiax    r3!,    {d0-d15}

这里还不清楚是什么意思,有待学习

15)   返回SVC模式,计算睡眠区域数据的校验码,并且把校验码保存在INFORM寄存器中。

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图29

下面是CPU对INFORM寄存器的描述

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图30

此寄存器一般用于保存用户的信息。

 

16)   清除TLB和刷新cache

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图31

先来学习一下TLB:

①TLB简要学习

TLB:translationlookaside buffer,在CPU发出VA请求读取数据的时候,TLB接收到该地址。TLB是MMU中的一块高速缓存(也是一种cache),它缓存最近查找过的VA对应的页表项,如果TLB里缓存了当前VA的页表项就不必做Translation Table Walk了,否则去物理内存中读取页表项保存在TLB中,TLB缓存可以减少访问物理内存的次数。

 

TLB根据功能可以译为快表,直译可以为旁路转换缓冲,也可以理解成页表缓冲,里面存放的是一些页表文件(虚拟地址到物理地址的转换表)。当处理器要在主内存寻址时,不是直接在内存的物理地址里查找,而是通过一组虚拟地址转换到主内存的物理地址,TLB就是负责将虚拟内存地址转化为实际的物理内存地址,而CPU寻址时会优先在TLB中进行寻址,处理器的性能和寻址的命中率有很大的关系。

 

 

②为什么要使用TLB

映射机制必须使一个程序能断言某个地址在其自己的进程空间或地址空间内,并且能够高效的将其转换为真实的物理地址以访问内存。一个方法是使用一个含有整个空间内所有页的入口(entry)的表(也即页表),每个入口包含这个页的正确物理地址,这是个相当大的数据结构,因为不得不存放于主存中。

 

由于CPU首先接到的是由程序传递过来的虚拟内存地址,所以CPU必须先到屋里内存中取页表,然后对应程序传来的虚拟页面号,在表里找到对应的物理页面号,最后才能访问实际的物理内存地址,也就是说要访问两次物理内存(实际上访问的次数可能更多)。因此,为了减少CPU访问物理内存的次数,引入TLB(是一种cache,高速缓存)。通常在ARM的实现中每个内存接口有一个TLB,特点如下:

I:有一个存储器接口的系统通常有一个唯一的TLB。

II:指令和数据的内存接口分开的系统通常有分开的指令TLB和数据TLB。

 

当存储器中的转换表被改变或选中了不同的转换表(通过写CP15的寄存器C2),先前在TLB中的转换表遍历结果将不再有效。MMU结构提供了刷新TLB的操作,也允许特定的转换表遍历结果被锁定在一个TLB中,这就保证了对相关的存储器区域的访问绝不会导致转换表遍历,这也对那些指令和数据锁定在高速缓存中的实时代码有相同的好处。

 

当存储器被重新映射时必须使与旧的映射相关的TLB入口无效,如果不这样,可能会进入两个TLB入口覆盖虚拟地址范围的状态。在最好的情况下访问这样的覆盖虚拟地址范围会有不可预料的结果;在某些实现中甚至会物理损坏MMU,所以强烈建议在重新映射存储器时要加倍小心使TLB适当地失效,OALClearDTLB和OALClearITLB函数在\WINCE600\PLATFORM\COMMON\SRC\ARM\COMMON\CACHE\cleardtlb.s和cleardilb.s中定过,如下:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图32

OALClearITLB函数

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图33

我们可以结合ARM11内核对C8的描述来理解:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图34

下图是TLB和cache在访问内存中的位置及关系:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图35

 

17)   设置晶振pad和电源稳定计数器

;-----------------------------------------------------

;    6. Set Oscillation pad and Power Stable Counter

;-----------------------------------------------------

 

        ldr     r0, =vOSC_STABLE

        ldr     r1, =0x1

        str     r1, [r0]

 

        ldr     r0, =vPWR_STABLE

        ldr     r1, =0x1

               str     r1, [r0]

见者两个寄存器的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图36

 

18)   设置PWR_CFG寄存器让系统进入睡眠模式

ldr        r0, =vPWR_CFG

ldr        r2, [r0]

bic        r2, r2, #0x60            ; Clear STANDBYWFI

orr        r2, r2, #0x60            ; Enter SLEEP mode

str        r2, [r0]

见下图对配置电源管理器寄存器的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图37

 

19)   进入睡眠模式后,通过设置SLEEP_CFG来禁止X-tal oscillator pad

ldr        r0, =vSLEEP_CFG

ldr        r2, [r0]

bic        r2, r2, #0x61            ; Disable OSC_EN (Disable X-tal Osc Pad in Sleep mode)

str        r2, [r0]

见下图对此寄存器的描述

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图38

20)   调用System_WaitForInterrupt函数让系统进入等中断发生的状态

bl        System_WaitForInterrupt

b        .

System_WaitForInterrupt函数在COMMON\SRC\SOC\S3C6410_SEC_V1_ideal\OAL\SYSTEM\

s3c6410_system.s中定义,如下:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图39

 

后面就是唤醒的过程了。

21)    

3.2唤醒的过程

3.2.1唤醒时的程序执行入口

在本设计中,在睡眠的模式下,当我们按下GPN11/EINT11按键时,系统会唤醒,那么唤醒的时候程序的执行入口是哪个函数呢?我们见CPU的描述

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图40

从图40可知,唤醒也是属于复位的一种,在系统进入睡眠模式后,内部硬件状态不在有效,所以需要初始化,所以可知唤醒时程序执行的入口是\Src\Bootloader\Stepldr\statup.s文件的入口StartUp (因为我们是用Real6410开发板,这里对应的是SMDK6410\SRC\BOOTLOADER\NBL1.LSB\startup.s中)

 

3.2.1 唤醒的流程

3.2.1.1 stelpdr部分的唤醒流程

由上面可知,唤醒的入口函数是StartUp,该函数会调用ResetHandler函数,下面就来学习ResetHandler函数。

1)      使能指令cache

;    Enable Instruction Cache

mov        r0, #0

mcr        p15, 0, r0, c7, c7, 0            ; Invalidate Entire I&D Cache

见下图ARM内核手册中对此指令的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图41

mrc        p15, 0, r0, c1, c0, 0            ; Enable I Cache

orr        r0, r0, #R1_I

mcr        p15, 0, r0, c1, c0, 0

使能指令cache,见ARM内核手册的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图42

 

2)      外围端口设置

;    Peripheral Port Setup

ldr        r0, =0x70000013        ; Base Addres : 0x70000000, Size : 256 MB (0x13)

mcr        p15,0,r0,c15,c2,4

3)      禁止看门狗

;------------------------------------

;    Disable WatchDog Timer

;------------------------------------

 

ldr        r0, =WTCON

ldr        r1, =0x0

    str        r1, [r0]

4)      通过对中断使能清除寄存器置位来禁止所有的中断

;------------------------------------

;    Interrupt Disable

;------------------------------------

 

        ldr        r0, =VIC0INTENCLEAR

        ldr        r1, =0xFFFFFFFF;

        str        r1, [r0]

 

        ldr        r0, =VIC1INTENCLEAR

        ldr        r1, =0xFFFFFFFF;

        str        r1, [r0]

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图43

 

5)      设置时钟输出pad输出APLL时钟

;-----------------------------------------

;Set Clock Out Pad to clock out APLL CLK

; For Testing

;---------------------------------------

        ldr     r0, =0x7f0080a0

        ldr     r1, [r0]

        orr     r1, r1, #0x30000000

        str     r1, [r0]

配置GPF14为CLKOUT[0],这里主要是看唤醒的流程及时钟输出是否正常

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图44

        ldr     r0, =0x7f0080a8

        ldr     r1, [r0]

        bic     r1, r1, #0x03000000

        str     r1, [r0]

这里不知道为什么要禁止GPF12引脚的上拉/下拉功能,应该是GPF14引脚才对啊,应该在此搞错了。

        ldr     r0, =0x7e00f02c

        mov     r1, #0x10000

        str     r1, [r0]

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图45

6)      提高内存端口1的驱动强度

;----------------------------------------------------------

;   Set the mem1drvcon to raise drive strength  for steploader ecc error

;----------------------------------------------------------

 

        ldr        r0, =MEM1DRVCON

;        ldr        r1, =0xFFFFFFFF

        ldr        r1, =0x55555555

        str        r1, [r0]

下面看CPU对memory port 0和memory port 1的描述

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图46

上图是内存子系统的架构图,结合下图去理解

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图47

7)      设置系统为同步模式或者是异步模式

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图48

8)      改变PLL的值,设置时钟分割值,设置PML并且使能PLL时钟输出

;------------------------------------

;    Change PLL Value

;------------------------------------

 

ldr        r1, =0xffff            ;Lock Time : 0x4b1 (100us @Fin12MHz) for APLL/MPLL

ldr        r2, =0xE13            ; Lock Time : 0xe13 (300us @Fin12MHz) for EPLL

 

 ldr        r0, =APLL_LOCK

str        r1, [r0]                ; APLL Lock Time

  str        r1, [r0, #0x4]            ; MPLL Lock Time

str        r2, [r0, #0x8]            ; EPLL Lock Time

 

;------------------------------------

;    Set System Clock Divider

;------------------------------------

    [{FALSE}

        ldr        r0, =CLK_DIV0

        ldr        r1, [r0]

        bic        r1, r1, #0x30000

        bic        r1, r1, #0xff00

        bic        r1, r1, #0xff

 

        ldr        r2, =((Startup_PCLK_DIV<<12)+(Startup_HCLKx2_DIV<<9)+(Startup_HCLK_DIV<<8)+(MPLL_DIV<<4)+(Startup_APLL_DIV<<0))

 

        orr        r1, r1, r2

        str        r1, [r0]

    ]

;-------------------------------------

;    Set PMS Values

;-------------------------------------

        ldr        r0, =APLL_CON

        ldr        r1, =((1<<31)+(Startup_APLL_MVAL<<16)+(Startup_APLL_PVAL<<8)+(Startup_APLL_SVAL))

        str        r1, [r0]

 

        ldr        r0, =MPLL_CON

        ldr        r1, =((1<<31)+(MPLL_MVAL<<16)+(MPLL_PVAL<<8)+(MPLL_SVAL))

        str        r1, [r0]

 

        ldr        r0, =EPLL_CON1

        ldr        r1, =EPLL_KVAL

        str        r1, [r0]

 

        ldr        r0, =EPLL_CON0

        ldr        r1, =((1<<31)+(EPLL_MVAL<<16)+(EPLL_PVAL<<8)+(EPLL_SVAL))

        str        r1, [r0]

       

;------------------------------------

;    Enable PLL Clock Out

;------------------------------------

 

        ldr        r0, =CLK_SRC

        ldr        r1, [r0]

        orr        r1, r1, #0x7            ; PLL  Clockout

        str        r1, [r0]                ; System will be waiting for PLL unlocked after this instruction

上面这部分内容可以参考我的另一篇博文:http://blog.csdn.net/loongembedded/article/details/6554680

 

9)      扩展端口1为32位

;------------------------------------

;    Expand Memory Port 1 to x32

;------------------------------------

 

        ldr        r0, =MEM_SYS_CFG

        ldr        r1, [r0]

        bic        r1, r1, #0x80            ; ADDR_EXPAND to "0"

        str        r1, [r0]

见电路图中memory port 1的设计

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图49

 

10)   初始化内存端口1的CKE(clock enable,时钟使能)初始值配置

;------------------------------------

;    CKE_INIT Configuration

;------------------------------------

 

        ldr        r0, =0x7F008880        ; SPCONSLP

        ldr        r1, [r0]

        orr        r1, r1, #0x10            ; SPCONSLP[4] = 1

        str        r1, [r0]

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图50

 

11)   初始化动态内存控制器

;------------------------------------

;    Initialize Dynamic Memory Controller

;------------------------------------

 

        bl        InitDMC

这部分后面学习DMC和DDR内存部分的时候再深入了。

 

12)   初始化堆栈及堆栈大小,图27

;--------------------------------------------------

;    Initialize Stack

;    Stack size and location information is in "image_cfg.inc"

;--------------------------------------------------

 

        mrs        r0, cpsr

 

        bic        r0, r0, #Mode_MASK

        orr        r1, r0, #Mode_IRQ | NOINT

        msr        cpsr_cxsf, r1                ; IRQMode

        ldr        sp, =IRQStack_PA            ; IRQStack

 

        bic        r0, r0, #Mode_MASK | NOINT

        orr        r1, r0, #Mode_SVC

        msr        cpsr_cxsf, r1                ; SVCMode

        ldr        sp, =SVCStack_PA            ; SVCStack

这里主要来看SVC模式下堆栈基地址和大小的确定,SVC模式下堆栈基地址为SVCStack_PA,见image_cfg.inc下的定义:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图51

所以可以知道系统分配的堆栈范围是0x500FC00到0x50100000,但是我们实际使用的较少,在这里要注意的是堆栈类型是FD类型的,所以堆栈的基地址对应于0x50100000。

 

 

13)   根据复位状态寄存器来判断复位类型,从而执行不同的复位及启动流程

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图52

下面就来分析0x5010000对应哪个函数的入口地址,

这个地址就是Bootloader把NandFlash里的数据装载完毕后,跳转执行的地址。那么在这里,跳转到0x50100000这个地址后,WINCE系统就会被装载了,也就是说WINCE的操作系统被唤醒了,见下面的内容

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图53

从上图可知NK.bin的image start=0x80100000,这就是物理地址0x5010000对应的虚拟地址,也就是直接跳转到WINCE操作系统内核映像的入口处执行,那么NK.bin的入口函数是哪个呢?我们先来了解一下NK.bin包含了哪些模块?通过viewbin–t nk.bin>aoutput.txt中关于module的一部分内容如下:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图54

由上图可知nk.exe是NK.bin的第一部分,也就是说NK.bin的入口地址就是NK.exe的入口地址,而NK.exe就是OAL.exe,为什么呢?我们看看release目录下ce.bib的如下内容

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图55

而OAL.exe的入口函数就是startup.s中的startup函数,下面就来学习这个函数

 

3.2.1.2 OAL部分的唤醒流程

这部分主要是oal.exe中的startup函数参与的唤醒流程,而startup函数的入口就是直接跳转到ResetHandler函数处执行,所以下面重点来学习这个函数:

1)内容如下,这部分可以参考唤醒部分的3.2.1.1部分的内容

ResetHandler

 

        LED_ON 0x1

 

;------------------------------------

;    Enable Instruction Cache

;------------------------------------

 

        mov        r0, #0

        mcr        p15, 0, r0, c7, c7, 0            ; Invalidate Entire I&D Cache

        bl        System_EnableICache            ; Enable I Cache

 

;------------------------------------

;    Peripheral Port Setup

;------------------------------------

 

        ldr        r0, =0x70000013        ; Base Addres : 0x70000000, Size : 256 MB (0x13)

        mcr        p15,0,r0,c15,c2,4

 

;------------------------------------

;    Interrupt Disable

;------------------------------------

 

        ldr        r0, =VIC0INTENCLEAR

        ldr        r1, =0xFFFFFFFF;

        str        r1, [r0]

 

        ldr        r0, =VIC1INTENCLEAR

        ldr        r1, =0xFFFFFFFF;

        str        r1, [r0]

 

;------------------------------------

;    Disable WatchDog Timer

;------------------------------------

 

        ldr        r0, =WTCON

        ldr        r1, =0x0

        str        r1, [r0]

 

2) 在内核中改变时钟配置,下面只列出一部分代码,其他部分可以参考另一篇博文学习:

http://blog.csdn.net/loongembedded/article/details/6554680

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图56

CHANGE_PLL_CLKDIV_ON_KERNEL的定义如下图:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图57

图56的内容主要是根据寄存器OTHERS的值来设置系统是同步还是异步模式。

 

3)      扩展端口1为32位

;------------------------------------

;    Expand Memory Port 1 to x32

;------------------------------------

 

        ldr        r0, =MEM_SYS_CFG

        ldr        r1, [r0]

        bic        r1, r1, #0x80            ; ADDR_EXPAND to "0"

        str        r1, [r0]

这部分见上面3.2.1.1的第9)部分。

4)      保存BSP数据

;------------------------------------

;    Store BSP Data

;------------------------------------

 

        ldr        r0, =INFORM0

        ldr        r1, =0x64107618        ; June 18, 2007

        str        r1, [r0]

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图58

INFORM0~3寄存器用于保存用户自动以的信息,在插入XnRESET复位信号时,会清除掉这个四个寄存器的值,这里是把0x64107618这个值保存到INFORM0寄存器中。

 

5)      通过协处理器访问控制寄存器(C1)使能VFP

;------------------------------------

; Enable VFP via Coprocessor Access Cotrol Register

;------------------------------------

        mrc        p15, 0, r0, c1, c0, 2

        orr        r0, r0, #0x00F00000

        mcr        p15, 0, r0, c1, c0, 2

VFP(Vector Floating-point,矢量浮点运算),下面见ARM11内核对VFP的相关描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图59

先是协处理器访问控制寄存器的值到r0寄存器中, 然后修改r0的值后回写到C1中

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图60

;------------------------------------

; Add following: SISO added

; Enable FPEXC enable bit to enable VFP

;------------------------------------

        MOV        r1, #0

        MCR        p15, 0, r1, c7, c5, 4

        MOV        r0,#VFPEnable

        FMXR       FPEXC, r0       ; FPEXC = r0

        nop

        nop

        nop

        nop

        nop

FPEXC(Floating-Point Exception Register,浮点异常寄存器),其作用如下面的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图61

那么我们是如何使能VFP呢?见下面对FPEXC寄存器的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图62

 

6)      通过RST_STAT寄存器的值来判断当前是哪种类型的复位

ldr        r0, =RST_STAT

        ldr        r1, [r0]

        and        r1, r1, #0x3F

        cmp        r1, #0x8

        bne        BringUp_WinCE_from_Reset            ; Normal Mode Booting

 

        LED_ON    0xC

先来看RST_STAT寄存器的描述:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图63

如果当前不是是从睡眠模式中唤醒,则BringUp_WinCE_from_Reset函数进入normal启动的流程;如果是,则会计算睡眠之前保存在睡眠数据区域的数据的校验码,看下面

 

7)      计算睡眠数据的校验码并且和睡眠前计算的校验码比较来决定不同的启动流程。

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图64

如果校验码不一致,则调用CheckSum_Corrupted函数:

WINCE6.0+S3C6410睡眠和唤醒的实现 - 男儿当自强 - 男儿当自强的博客

 

图65

8)      从睡眠数据区域恢复CPU寄存器的值

WakeUp_Address

 

;-----------------------------------------------------

;    1. Restore CPU Register from Sleep Data Area in DRAM

;-----------------------------------------------------

 

        VLED_ON    0x3

 

        ldr        r3, =IMAGE_SLEEP_DATA_UA_START    ; Sleep Data Area Base Address

 

        ;----------------------

        ; FIQ mode CPU Registers

 

        mov        r1, #Mode_FIQ | NOINT                ; Enter FIQ mode, no interrupts

        msr        cpsr, r1

 

        ldr        r0,    [r3, #SleepState_FIQ_SPSR]

        msr        spsr, r0

        ldr        r8,    [r3, #SleepState_FIQ_R8]

        ldr        r9,    [r3, #SleepState_FIQ_R9]

        ldr        r10,    [r3, #SleepState_FIQ_R10]

        ldr        r11,    [r3, #SleepState_FIQ_R11]

        ldr        r12,    [r3, #SleepState_FIQ_R12]

        ldr        sp,    [r3, #SleepState_FIQ_SP]

        ldr        lr,    [r3, #SleepState_FIQ_LR]

 

        ;-----------------------

        ; Abort mode CPU Registers

 

        mov        r1, #Mode_ABT | I_Bit                ; Enter ABT mode, no IRQ - FIQ is available

        msr        cpsr, r1

 

        ldr        r0,    [r3, #SleepState_ABT_SPSR]

        msr        spsr, r0

        ldr        sp,    [r3, #SleepState_ABT_SP]

        ldr        lr,    [r3, #SleepState_ABT_LR]

 

        ;----------------------

        ; IRQ mode CPU Registers

 

        mov        r1, #Mode_IRQ | I_Bit                ; Enter IRQ mode, no IRQ - FIQ is available

        msr        cpsr, r1

 

        ldr        r0,    [r3, #SleepState_IRQ_SPSR]

        msr        spsr, r0

        ldr        sp,    [r3, #SleepState_IRQ_SP]

        ldr        lr,    [r3, #SleepState_IRQ_LR]

 

        ;---------------------------

        ; Undefined mode CPU Registers

 

        mov        r1, #Mode_UND | I_Bit                ; Enter UND mode, no IRQ - FIQ is available

        msr        cpsr, r1

 

        ldr        r0,    [r3, #SleepState_UND_SPSR]

        msr        spsr, r0

        ldr        sp,    [r3, #SleepState_UND_SP]

        ldr        lr,    [r3, #SleepState_UND_LR]

 

        ;------------------------------

        ; System(User) mode CPU Registers

 

        mov        r1, #Mode_SYS | I_Bit                ; Enter SYS mode, no IRQ - FIQ is available

        msr        cpsr, r1

 

        ldr        sp,    [r3, #SleepState_SYS_SP]

        ldr        lr,    [r3, #SleepState_SYS_LR]

 

        ;----------------------------

        ; Supervisor mode CPU Registers

 

        mov        r1, #Mode_SVC | I_Bit                ; Enter SVC mode, no IRQ - FIQ is available

        msr        cpsr, r1

 

        ldr        r0, [r3, #SleepState_SVC_SPSR]

        msr        spsr, r0

        ldr        sp, [r3, #SleepState_SVC_SP]

 

        ;----------------------------------------

             ; Add following: SISO added

           ; 1-1 Restore VFP system control registers

           ;----------------------------------------

 

        ;--------------------------------------

        ;FMRX{cond} Rd, VFPsysreg     VFPsysreg -> Rd

        ; FMXR{cond} VFPsysreg, Rd         Rd -> VFPsysreg

        ;      FPSCR

        ldr        r0, [r3, #SleepState_VFP_FPSCR]

        fmxr    fpscr, r0

 

        ;------------------------------------------

        ;    Floating Point Exception Register

        ldr        r0, [r3, #SleepState_VFP_FPEXC]

        fmxr    fpexc, r0

9)      从堆栈中恢复SVC模式下的寄存器r4到r12的值

;----------------------------------

;    2. Pop SVC Register from our Stack

;----------------------------------

 

        ldr        lr, [sp], #4

        ldmia    sp!, {r4-r12}

见3.2.1.1的第1)部分描述,结合这边的代码可以看出入栈和出栈的顺序是相反的,我们先从堆栈中把OALCPUPowerOff()函数的返回地址恢复到lr寄存器中,然后再恢复r4到r12寄存器的值

10)   返回到OEMPowerOff函数调用OALCPUPowerOff()函数的地方

;--------------------------------------

;    3. Return to Caller of OALCPUPowerOff()

;--------------------------------------

 

        mov     pc, lr                          ; and now back to our sponsors

 

        ENTRY_END

由上面的描述可知,系统这是就回到了OEMPowerOff函数中,接下来继续这个函数后面的调用,后面的流程就不做描述了,相信大家很容易看懂后面的流程部分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值