内存管理单元 MMU

    内存管理单元(Memory Management Unit)简称 MMU,它负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。现代的多用户多进程操作系统通过 MMU 使得各个用户进程都拥有自己独立的地址空间:地址隐射功能使得各进程拥有“看起来”一样的地址空间,而内存访问权限的检查可以保护每个进程所用的内存不会被其他进程破坏。


S3C2440 MMU地址变化过程

地址的分类
    以前的程序是非常小的,可以全部装入内存中。随着技术的发展,出现了一些两种情况。
    有的程序很大,它所需求的内存空间超过了内存总容量,不能一次性装入内存;
    系统中有很多程序需要同时执行,它们要求的内存空间超过了内存总容量,不能把所有程序全部装入内存。
实际上,一个程序在运行之前,没有必要全部装入内存,而仅需要将那么当前要运行的部分先装入内存,其余部分再用到时再从磁盘调入,而当内存耗光时再将暂时不用的部分调出到磁盘。这使得一个大程序可以在较小的内存空间中运行,也使得内存中可以同时装入更多的程序并发执行,从用户的角度看,该系统所具有的内存容量将比实际内存容量大得多,人们把这样的存储器称为虚拟存储器。
    虚拟存储器从逻辑上对内存容量进行了扩充,用户看到的大容量只是一种感觉,是虚的,在 32 位的 CPU 系统中,这个虚拟内存地址范围为 0 ~ 0xFFFFFFFF, 我们把这个地址范围称为虚拟地址空间,其中的每个地址称为虚拟地址。虚拟地址空间对应物理地址空间,虚拟地址对应物理地址。物理地址空间与物理地址对应实际的内存。
虚拟地址最终需要转换为物理地址才能读写实际的数据,这通过将虚拟地址空间、物理地址空间划分为同样大小的一块块小空间(称为段或页),然后为这两类小空间建立映射关系。由于虚拟地址空间远大于物理空间,有可能多块虚拟地址空间映射到同一块物理地址空间,或者有些虚拟地址空间没有映射到具体的物理地址空间上去(可以在使用到时在映射)。
    ARM CPU 上的地址转换过程涉及 3 个概念:虚拟地址(VA, Virtual Address)、变换后的虚拟地址(MVA, Modified Virtual Address)、物理地址(PA, physical Address)。
    没启动 MMU 时,CPU 核、cache、MMU 、外设等所有部件使用的都是物理地址。
    启动 MMU 后,CPU 核对外发出 VA, VA 被转换为 MVA 供 cache、 MMU 使用(VA 到 MVA 的转换是硬件自动完成的),MMU 将 MVA 转换成 PA ,最后使用 PA 读写实际设备(S3C2440 内部寄存器或外接的设备)。


虚拟地址到物理地址的转换过程
    将一个虚拟地址转换为物理地址,一般有两个办法:用一个确定的数学公式进行转换或用表格存储虚拟地址对应的物理地址。这类表格称为页表(Page table),页表由一个个条目(Entry)组成;每个条目存储了一段虚拟地址对应的物理地址及其访问权限,或者下一级页表的地址。
    在 ARM CPU 中使用第二种方法。 S3C2440 最多会用达到两级页表:以段(Section 1MB)的方式进行转换时只用到一级页表,以页(Page)的方式进行转换时用到两级页表。页的大小有3种:大页(64KB)、小页(4KB)、极小页(1KB)。条目也称为描述符(Descriptor),有:段描述符、大页描述符、小页描述符、极小页描述符--它们保存段、大页、小页或极小页的起始物理地址;粗页表描述符、细页表描述符--它们保存二级页表的物理地址。


内存的访问权限检查
    内存的访问权限检查是 MMU 的主要功能之一,简单地说,它就是决定一块内存是否运行读、是否允许写。这由 CP15 寄存器 C3(域访问控制)、 描述符的域(Domain)、 CP15 寄存器 C1 的 R/S/A 位、 描述符的 AP 位等联合作用。
    CP15 寄存器 C1 中打的 A 位表示是否对地址进行对齐检查。所谓对齐检查就是,访问字(4 字节的数据)时地址是否为 4 字节对齐,访问半字(2 字节的数据)时地址是否 2 字节对齐,如果地址不对齐则产生 “Alignment fault”异常。无论 MMU 是否被开启,都可以进行对齐检查。CPU 读取指令时不进行对齐检查,     以字节为单位访问时也进行对齐检查。对齐检查在 MMU 的权限检查、地址映射前进行。
    内存的访问权限检查可以概况为以下两点。
   “域”决定是否对某块内存进行权限检查。
   “AP”决定如何对某块内存进行权限检查。


TLB的作用
    从虚拟地址到物理地址的转换过程可知:使用一级页表进行地址转换时,每次读/写数据需要访问两次内存,第一次访问一级页表获得物理地址,第二次才是真正的读/写数据:使用两级页表时,每次读/写数据需要访问 3 次内存,访问两次页表(一级页表和二级页表)获得物理地址,第三次才是真正的读/写数据。
    上述的地址转换过程大大降低了 CPU 的性能,有没有办法改进呢?程序执行过程中,所用到的指令、数据的地址往往集中在一个很小的范围内,其中的地址、数据经常多次使用,这称为程序访问的局部性。由此,通过使用一个高速、容量相对较小的存储器来存储近期用到的页表条目(段/大页/小页/极小页描述符),以避免每次地址转换时都到主存去查找,这样可以大幅度地提高性能。这个存储器用来帮助快速的进行地址转换,称为“转译查找缓存”(Translation Lookaside Buffers, TLB)。
    当 CPU 发出一个虚拟地址时, MMU 首先访问 TLB。 如果 TLB 中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查;否则 MMU 访问页表找到描述符后再进行地址转换和权限检查,并将这个描述符填入 TLB 中(如果 TLB 已满,则利用 round-robin 算法找到一个条目,然后覆盖它),下次再使用这个虚拟地址时就可以直接使用 TLB 中的描述符了。
    使用 TLB 需要保证 TLB 中的内容与页表一致,在启动 MMU 之前、在页表中的内容发生变化后,尤其要注意这点。 S3C2440 可以使无效(Invalidate)整个 TLB,或者通过某个虚拟地址使无效 TLB 中的某个条目。一般的做法是:启动 MMU 之前使无效整个 TLB,改表页表时,使无效所涉及的虚拟地址对应的 TLB 中的条目。


Cache 的作用
    同样基于程序访问的局部性,在主寸和 CPU 通用寄存器直接设置一个高速的、容量相对较小的存储器,把正则执行的指令地址附件的一部分指令或数据从主存调入这个存储器,工 CPU 在一段时间内使用,这对提高程序的运行速度有很大的作用。这个介于主存和 CPU 之间的高速小容量存储器称作高速缓冲存储器(Cache)。
    启用 Cache 后, CPU 读取数据时,如果 Cache 中有这个数据的副本则直接返回,否则从注册中读入数据,并存入 Cache 中,下次在使用(读/写) 这个数据时,可以直接使用 Cache 中的副本。CPU 写数据时有写穿式和回写穿式两种方式。
    写穿式(Write Through)
    任一从 CPU 发出的写信号送到 Cache 的同时,也写入主存,以保证主存的数据能同步地更新。它的优点是操作简单,但由于主存的慢速,降低了系统的写速度并占用了总线的时间。
    回写式(Write Back)
    为了克服写穿式中每次数据写入时都要访问主存,从而导致系统写速度降低并占用总线时间,尽量减少对主存的访问次数,又有了回写式。
    它是这样工作的:数据一般只写到 Cache,这样有可能出现 Cache 中的数据得到更新而主存中的数据不变(数据陈旧)的情况。但此时可在 Cache 中设一标志地址及数据陈旧的信息,只有当 Cache 中的数据被换出或强制进行"清空"操作时,才将原更新的数据写入主存相应的单元中。这样保证了 Cache 和主存中的数据保持一致。
    Cache的两个操作:
清空(clean): 把 Cache 或 Write buffer 中已经脏的(修改过,但未写入主存)数据写入主存。
使无效(Invalidate):使之不再能使用,并不将脏的数据写入主存。


实际编写程序时,要注意如下下几点:
    开启 MMU 前,使无效 ICaches 、DCaches 和 Write buffer。
    关闭 MMU 前,清空 ICaches 、 DCaches ,即将 “脏” 数据写到主存上。
    如果代码有变,使无效 ICaches , 这样 CPU 取指时会重新读取主存。
    使用 DMA 操作可以被 Cache 的内存时:将内存的数据发送出去时,要清空 Cache;将内存的数据读入时,要使无效 Cache。
    改变页表中地址映射关系时也要慎重考虑。
    开启 ICaches 或 DCaches 时,要考虑 ICaches 或 DCaches 中的内容是否与主存保存一致。
    对于 I/O 地址空间,不使用 Cache 和 Wirte buffer。 所谓 I/O 地址空间,就是对于其中的地址连续两次的写操作不能合并在一起,每次读/写操作都必须直接访问设备,否则程序的运行结果无法预料。比如寄存器、非内存的外设(扩展串口、网卡等)


MMU 使用实例:地址映射
    JZ2440 开发板 SDRAM 的物理地址范围处于 0x30000000 ~ 0x33FFFFFF , S3C2440 的寄存器地址返回处于 0x48000000 ~ 0x5FFFFFFF , 通过 GPFCON 和 GPFDAT 这两个寄存器的物理地址 0x56000050 、 0x56000054 写入特定的数据来驱动 LED 灯。
    本实例将开启 MMU,并将虚拟地址空间 0xA0000000 ~ 0xA0100000 映射到物理地址空间 0x56000000 ~ 0x56100000 上,这样,就可以通过操作地址 0xA0000050、0xA0000054 来达到驱动 LED 同样的效果。
    将虚拟地址空间 0xB0000000 ~ 0xB3FFFFFF 映射到物理地址空间 0x30000000 ~ 0x33FFFFFF 上,并在连接程序时将一部分代码的运行地址指定为 0xB0004000(因为前 16KB 用来存放一级页表描述符),看看能否令程序跳转到 0xB0004000 处执行。
    本程序使用一级页表,以段的方式进行地址映射。32位CPU的虚拟地址空间达到 4GB, 一级页表中使用 4096 个描述符来表示这 4GB 空间(每个描述符对应 1MB 的虚拟地址),每个描述符占用 4 字节, 所以一级页表占 16KB。本实例使用 SDRAM 的开始 16KB来存放一级页表,所以剩下的内存开始物理地址为 0x30004000。
将程序代码分为两部分:第一部分的运行地址设为 0,它用来初始化 SDRAM、复制第二部分的代码到 SDRAM 中(存放在 0x30004000 开始处)、设置页表、启动 MMU,最后跳到 SDRAM 中(地址 0xB0004000)去继续执行;第二部分的运行地址设为 0xB0004000,它用来驱动 LED。
程序流程图如下:

                        


代码如下:

@*************************************************************************
@ File:head.S
@ 功能:设置SDRAM,将第二部分代码复制到SDRAM,设置页表,启动MMU,
@       然后跳到SDRAM继续执行
@************************************************************************* 

.text
.global  _start
_start:
			ldr sp,	=4096									@ 设置栈指针,一下都是C函数,调用前需要设好栈
			bl	disable_watch_dog					@ 关闭 WATCHDOG, 否则 CPU 会不断重启
			bl  memsetup									@	设置存储控制器以使用 SDRAM
			bl  copy_2th_to_sdram					@ 将第二部分代码复制到 SDRAM
			bl  create_page_table					@ 设置页表
			bl  mmu_init									@ 启动 MMU
			ldr sp, =0xB4000000						@ 重设栈指针,指向 SDRAM 顶端(使用虚拟地址)
			ldr pc, =0xB0004000						@ 跳到 SDRAM 中继续执行第二部分代码
halt_loop:
			b		halt_loop
			 
@ 在第 15 行开启 MMU 之后,无论是 CPU 取值还是 CPU 读写数据,使用的都是虚拟地址。
@ 在第 14 行设置页表时, 在 create_page_table 函数中令 head.S 、 init.c 程序所在
@ 的内存的虚拟地址和物理地址一样,这使得 head.S 和 init.c 中的代码在开启 MMU 后能够
@ 没有任何障碍地继续运行。


/*
 * init.c: 进行一些初始化,在Steppingstone中运行
 * 它和head.S同属第一部分程序,此时MMU未开启,使用物理地址
 */ 
 
/* WATCHDOG寄存器 */
#define WTCON			(*(volatile unsigned long *)0x53000000)
 
/* 存储控制器的寄存器起始地址 */
#define MEM_CTL_BASE	0x48000000

/*
 * 关闭WATCHDOG,否则CPU会不断重启
 */
void disable_watch_dog(void){
 		WTCON = 0;  // 关闭WATCHDOG很简单,往这个寄存器写0即可
}


/*
 * 设置存储控制器以使用SDRAM
 */
void memsetup(void){
		/* SDRAM 13个寄存器的值 */
	 	unsigned long const mem_cfg_val[] = {
				0x22011110,     //BWSCON
				0x00000700,     //BANKCON0
				0x00000700,     //BANKCON1
				0x00000700,     //BANKCON2
				0x00000700,     //BANKCON3  
				0x00000700,     //BANKCON4
				0x00000700,     //BANKCON5
				0x00018005,     //BANKCON6
				0x00018005,     //BANKCON7
				0x008C07A3,     //REFRESH
				0x000000B1,     //BANKSIZE
				0x00000030,     //MRSRB6
				0x00000030,     //MRSRB7	
	 	};
	 	int i = 0;
	 	volatile unsigned long *p = (volatile unsigned long *)MEM_CTL_BASE;
	 	for(; i < sizeof(mem_cfg_val)/ sizeof(mem_cfg_val[0]); ++i)
	 			p[i] = mem_cfg_val[i];
}


/*
 copy_2th_to_sdram 函数用来将第二部分代码(即由 leds.c 编译得来的代码)
 从 Steppingstone 中复制到 SDRAM 中。在连接程序时,
 第二部分代码的加载地址被指定为2048,
 重定位地址为 0xB0004000。所以系统从 NAND Flash 启动后,
 第二部分代码就存储在 Steppingstone 中地址 2048 之后,
 需要把它复制到 0x30004000 处(此处尚未开启 MMU,虚拟地址 0xB0004000 对应的物理
 地址在后面设为 0x30004000)。 Steppingstone 总大小为 4KB,不妨把地址 2048 之后
 的所有数据复制到 SDRAM 中,所以源数据的结束地址为 4096。
 */
void copy_2th_to_sdram(void){
		unsigned	int *pdwSrc  = (unsigned int *)2048;
		unsigned  int *pdwDest = (unsigned int *)0x30004000;
		
		while(pdwSrc < (unsigned int *)4096){
			*pdwDest = *pdwSrc;
			pdwDest++;
			pdwSrc++;
		}
}	


/*
 * 设置页表
 */
void create_page_table(void){
/* 
 * 用于段描述符的一些宏定义
 */ 

#define MMU_FULL_ACCESS				(3 << 10)  /* 访问权限 */
#define	MMU_DOMAIN						(0 << 5)	 /* 属于哪个域 */
#define MMU_SPECIAL						(1 << 4)	 /* 必须是 1 */
#define MMU_CACHEABLE					(1 << 3)	 /* cacheable */
#define MMU_BUFFERABLE				(1 << 2)   /* bufferable */
#define MMU_SECTION						(2)				 /* 表示这是段描述符 */
#define MMU_SECDESC						(MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | MMU_SECTION)
#define MMU_SECDESC_WB				(MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | MMU_CACHEABLE | MMU_BUFFERABLE | MMU_SECTION)
#define MMU_SECTION_SIZE			0x00100000

		
	
		/*
     * Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0,
     * 为了在开启MMU后仍能运行第一部分的程序,
     * 将0~1M的虚拟地址映射到同样的物理地址
     */
    unsigned long virtualaddr = 0, physicaladdr = 0;
		unsigned long *mmu_tlb_base = (unsigned long *)0x30000000;
     *(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) | \
                                            MMU_SECDESC_WB;
    
    
    /*
     * 0x56000000是GPIO寄存器的起始物理地址,
     * GPFCON和GPFDAT这两个寄存器的物理地址0x56000050、0x56000054,
     * 为了在第二部分程序中能以地址0xA0000050、0xA0000054来操作GPFCON、GPFDAT,
     * 把从0xA0000000开始的1M虚拟地址空间映射到从0x56000000开始的1M物理地址空间
     */
    virtualaddr = 0xA0000000;
    physicaladdr = 0x56000000;
    *(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) | MMU_SECDESC;
    
    /*
     * SDRAM的物理地址范围是0x30000000~0x33FFFFFF,
     * 将虚拟地址0xB0000000~0xB3FFFFFF映射到物理地址0x30000000~0x33FFFFFF上,
     * 总共64M,涉及64个段描述符
     */
    virtualaddr = 0xB0000000;
    physicaladdr = 0x30000000;
    while(virtualaddr < 0xB4000000){
    	*(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) | MMU_SECDESC_WB;
    	
    	virtualaddr += 0x100000;
    	physicaladdr += 0x100000;
    } 
}


/*
 * 启动MMU
 */
void mmu_init(void){
		unsigned long ttb = 0x30000000;
	
__asm__(
    "mov    r0, #0\n"
    "mcr    p15, 0, r0, c7, c7, 0\n"    /* 使无效ICaches和DCaches */
    
    "mcr    p15, 0, r0, c7, c10, 4\n"   /* drain write buffer on v4 */
    "mcr    p15, 0, r0, c8, c7, 0\n"    /* 使无效指令、数据TLB */
    
    "mov    r4, %0\n"                   /* r4 = 页表基址 */
    "mcr    p15, 0, r4, c2, c0, 0\n"    /* 设置页表基址寄存器 */
    
    "mvn    r0, #0\n"                   
    "mcr    p15, 0, r0, c3, c0, 0\n"    /* 域访问控制寄存器设为0xFFFFFFFF,
                                         * 不进行权限检查 
                                         */    
    /* 
     * 对于控制寄存器,先读出其值,在这基础上修改感兴趣的位,
     * 然后再写入
     */
    "mrc    p15, 0, r0, c1, c0, 0\n"    /* 读出控制寄存器的值 */
    
    /* 控制寄存器的低16位含义为:.RVI ..RS B... .CAM
     * R : 表示换出Cache中的条目时使用的算法,
     *     0 = Random replacement;1 = Round robin replacement
     * V : 表示异常向量表所在的位置,
     *     0 = Low addresses = 0x00000000;1 = High addresses = 0xFFFF0000
     * I : 0 = 关闭ICaches;1 = 开启ICaches
     * R、S : 用来与页表中的描述符一起确定内存的访问权限
     * B : 0 = CPU为小字节序;1 = CPU为大字节序
     * C : 0 = 关闭DCaches;1 = 开启DCaches
     * A : 0 = 数据访问时不进行地址对齐检查;1 = 数据访问时进行地址对齐检查
     * M : 0 = 关闭MMU;1 = 开启MMU
     */
    
    /*  
     * 先清除不需要的位,往下若需要则重新设置它们    
     */
                                        /* .RVI ..RS B... .CAM */ 
    "bic    r0, r0, #0x3000\n"          /* ..11 .... .... .... 清除V、I位 */
    "bic    r0, r0, #0x0300\n"          /* .... ..11 .... .... 清除R、S位 */
    "bic    r0, r0, #0x0087\n"          /* .... .... 1... .111 清除B/C/A/M */

    /*
     * 设置需要的位
     */
    "orr    r0, r0, #0x0002\n"          /* .... .... .... ..1. 开启对齐检查 */
    "orr    r0, r0, #0x0004\n"          /* .... .... .... .1.. 开启DCaches */
    "orr    r0, r0, #0x1000\n"          /* ...1 .... .... .... 开启ICaches */
    "orr    r0, r0, #0x0001\n"          /* .... .... .... ...1 使能MMU */
    
    "mcr    p15, 0, r0, c1, c0, 0\n"    /* 将修改的值写入控制寄存器 */
    : /* 无输出 */
    : "r" (ttb) );
}


/*
 * leds.c: 循环点亮3个LED
 * 属于第二部分程序,此时MMU已开启,使用虚拟地址
 */ 

#define GPFCON      (*(volatile unsigned long *)0xA0000050)     // 物理地址0x56000050
#define GPFDAT      (*(volatile unsigned long *)0xA0000054)     // 物理地址0x56000054

#define	GPF4_out	(1<<(4*2))
#define	GPF5_out	(1<<(5*2))
#define	GPF6_out	(1<<(6*2))

/*
 * wait函数加上“static inline”是有原因的,
 * 这样可以使得编译leds.c时,wait嵌入main中,编译结果中只有main一个函数。
 * 于是在连接时,main函数的地址就是由连接文件指定的运行时装载地址。
 * 而连接文件mmu.lds中,指定了leds.o的运行时装载地址为0xB4004000,
 * 这样,head.S中的“ldr pc, =0xB0004000”就是跳去执行main函数。
 */
static inline void wait(unsigned long dly)
{
    for(; dly > 0; dly--);
}

int main(void)
{
	unsigned long i = 0;
	
	GPFCON = GPF4_out|GPF5_out|GPF6_out;		// 将LED1,2,4对应的GPF4/5/6三个引脚设为输出

	while(1){
		wait(30000);
		GPFDAT = (~(i<<4));	 	// 根据i的值,点亮LED1,2,4
		if(++i == 8)
			i = 0;
	}

	return 0;
}

Makefile:

objs := head.o init.o leds.o

mmu.bin : $(objs)
	arm-linux-ld -Tmmu.lds -o mmu_elf $^
	arm-linux-objcopy -O binary -S mmu_elf $@
	arm-linux-objdump -D -m arm mmu_elf > mmu.dis
	
%.o:%.c
	arm-linux-gcc -Wall -O2 -c -o $@ $<
	
%.o:%.S
	arm-linux-gcc -Wall -O2 -c -o $@ $<
	
clean:
	rm -f mmu.dis mmu_elf mmu.bin *.o

mmu.lds

SECTIONS {
	firtst		0x00000000	: { head.o init.o }
	second		0xB0004000	: AT(2048) { leds.o }
}













  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
分页内存管理单元MMU)是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制等。MMU使用虚拟地址中的重要位来索引转换表中的条目,并确定正在访问的内存块。通过MMU的地址转换功能,系统能够运行多个任务,每个任务在自己的私有虚拟内存空间中运行,而不需要了解系统的物理内存映射或其他同时运行的程序。MMU还可以控制每个内存区域的内存访问权限、内存顺序和缓存策略。总之,分页内存管理单元是实现虚拟内存系统的关键硬件组件。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *3* [内存管理单元——MMU](https://blog.csdn.net/weixin_43834468/article/details/130138697)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [操作系统 内存管理单元MMU TLB](https://blog.csdn.net/u014099894/article/details/127342188)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值