第七章  内存管理单元MMU


先明确一点,MMU的主要工作只有一个,就是把虚拟地址映射到物理地址。

第七章 <wbr> <wbr>内存管理单元MMU

ARM CPU 上的地址转换过程涉及3个概念:虚拟地址VA、变换后的虚拟地址MVA、物理地址PA

没启动mmu时cpu核、cache、mmu、外设都使用物理地址

启动mmu后,cup对外发出va;va完全有硬件自动转换成mva供cache、MMU使用转换成pa;最后使用pa读写实际设备

第七章 <wbr> <wbr>内存管理单元MMU

如果VA<32M,use pid(read cp15‘s c13(具体在哪里?))to change to MVA:auto change

if(va<32M) then 

     mva=va|(pid<<25

else

     mva=va




虚拟地址到物理地址的转换过程

1.用一个确定公式进行转换

2.用表格存储虚拟地址对应的物理地址及其访问权限

第七章 <wbr> <wbr>内存管理单元MMU

第七章 <wbr> <wbr>内存管理单元MMU



TTB base代表一级页表的首地址,将它写入协处理器CP15(ARM920T的MMU和Cache都集成在CP15协处理器中)的寄存器C2(称为页表基址寄存器)(固定?)即可,一级页表的地址是16K对齐,使用[31:14]存储页表基址,[13:0]为0

 一级页表使用4096个描述符来表示4GB空间,每个描述符对应1MB的虚拟地址,存储它对应的1MB物理空间的起始地址,或者存储下一级页表的地址。使用MVA[31:20]来

索引一级页表(20-31一共12位,2^12=4096,所以是4096个描述符),得到一个描述符,每个描述符占4个字节。

*(((c2[31:14])+(MVA[31:20]>>18))&0B00)=一级页表描述符

TTB的[31:14]=c2的[31:14]可以理解为x86里面的段地址

TTB的[13:2]=MVA[31:20]可以理解为x86里面的偏移地址

TTB的[1:0]=0b00 

一级页表描述符格式如下:

第七章 <wbr> <wbr>内存管理单元MMU

根据一级描述符的最低两位[1:0],分为下面四种:

00=无效 

01=粗页表基址

[31:10]=粗页表基址(了解为二级页表地址的段地址),[9:2](偏移地址,共2^8=259个,4kb/个)[1:0]=0B01

10=段

  [31:20]为段基址,、此描述符低20位填充0后就是一块1MB物理地址空间的起始地址。MVA[19:0],用来在这1MB空间中寻址。描述符的位[31:20]和MVA[19:0]构成了这个虚拟地址MVA对应的物址

以段的方式进行映射时,虚拟地址MVA到物理地址PA的转换过程如下:

①页表基址寄存器位[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到段描述符

②取出段描述符的位[31:20](段基址),它和MVA[19:0]组成一个32位的物理地址(这就是MVA对应的PA)

第七章 <wbr> <wbr>内存管理单元MMU

*(((c2[31:14])+(MVA[31:20]>>18))&0B00)=段描述符

段描述符[31:20]+MVA[19:2]=PA



0B11=细表页

[31:12]细页表基址,[11:2](理解为偏移地址,2^10=1024个  1kb/个)共1mb


以大页(64KB),小页(4KB)或极小页(1KB)进行地址映射时,需要用到二级页表,二级页表有粗页表、细页表两种,二级页表描述符格式如下:

第七章 <wbr> <wbr>内存管理单元MMU

二级描述符最低两位:

0b00:无效

0b01:大页描述符

 [31:16]为大页基址,此描述符的低16位填充0后就是一块64KB物理地址空间的起始地址粗页表中的每个条目只能表示4KB物理空间,如果大页描述符保存在粗页表中,则连续16个条目都保存同一个大页描述符。类似的,细页表中每个条目只能表示1KB的物理空间,如果大页描述符保存在细页表中,则连续64个条目都保存同一个大页描述符。

下面以保存在粗页表中的大页描述符为例,说明地址转化那过程

①页表基址寄存器[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到粗页表描述符

②取出粗页表描述符的[31:10](即粗页表基址),它和MVA[19:12]组成一个低两位为0的32位物理地址,通过这个地址找到大页描述符

③取出大页描述符的[31:16](即大页基址),它和MVA[15:0]组成一个32位的物理地址,即MVA对应的PA

步骤②和③中,用于在粗页表中索引的MVA[19:12]、用于在大页内寻址的MVA[15:0]有重合的位[15:12],当位[15:12]从0b0000变化到0b1111时,步骤②得到的大页描述符相同,所以粗页表中有连续16个条目保存同一个大页描述符



*(((c2[31:14])+(MVA[31:20]>>18))&0B00)=粗表描述符(一级描述符)

*((粗页表描述符[31:10]+(MVA[19:12]>>10))&0xfffffffd)=大页描述符

((大页描述符[31:16]+MVA[15:0]))=PA

2^16=64kb

第七章 <wbr> <wbr>内存管理单元MMU


*(((c2[31:14])+(MVA[31:20]>>18))&0B00)=粗表描述符(一级描述符)

*((粗页表描述符[31:10]+(MVA[19:16]>>6))&0B00)=大页描述符

((大页描述符[31:16]+MVA[15:0]))=PA



0b10:小页描述符

[31:12]为小页基址(Small page base address),此描述符的低12位填充0后就是一块4kb([11:0],一共12位,2^12=4096)物理地址空间的起始地址。粗页表中每个条目表示4kb的物理空间,如果小页描述符保存在粗页表中,则只需要用一个条目来保存一个小页描述符。类似的,细页表中每个条目只能表示1kb的物理空间,如果小页保存在细页表中,则连续4个条目都保存同一个小页描述符。

下面以保存在粗页表中的小页描述符为例,说明地址转换过程:
①页表基址[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到粗页表描述符

②取出粗页表描述符[31:10](即粗页表基址),它和MVA[19:12]组成一个低两位为0的32位物理地址,用这个地址找到小页描述符

③取出小页描述符的位[31:12](即小页基址),它和MVA[11:0]组成一个32位物理地址(即MVA对应的PA)
第七章 <wbr> <wbr>内存管理单元MMU
*((c2[31:14]+MVA[31:20]>>18)&0XFFFFFFD)=粗页描述符
*((粗页描述符[31:10]+MVA[19:12]>>10)&0XFFFFFFFD)=小页描述符
小页描述符[31:12]+MVA[11:0]=PA


0b11:极小页描述符
      [31:10]为极小页基址(Tiny page base address),此描述符的低10位填充0后就是一块1KB物理地址空间的起始地址。极小页描述符只能保存在细页表中,用一个条目来保存一耳光极小页描述符下面是极小页的地址转换过程
①页表基址寄存器[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU通过这个地址找到细页表描述符
②取出细页表描述符[31:12](即细页表基址),它和MVA[19:10]组成一个低两位为0的32位物理地址,通过这个地址即可找到极小页描述符
③取出极小页描述符[31:10](即极小页基址),它和MVA[9:0]组成一个32位的物理地址(即MVA对应的PA)
第七章 <wbr> <wbr>内存管理单元MMU
*((c2[31:14]+MVA[31:20]>>18)&0XFFFFFFD)=细页描述符
*((细页描述符[31:12]+MVA[19:10]>>10)&0XFFFFFFFD)=小页描述符
小页描述符[31:10]+MVA[9:0]=PA


从段、大页、小页、极小页的地址转换过程可知

①以段进行映射时,通过MVA[31:20]结合页表得到一段(1MB)的起始物理地址,MVA[19:0]用来在段中寻址

②以大页进行映射时,通过MVA[31:16]结合页表得到一个大页(64KB)的起始物理地址,MVA[15:0]用来在小页中寻址

③以小页进行映射时,通过MVA[31:12]结合页表得到一个小页(4KB)的起始物理地址,MVA[11:0]用来在小页中寻址

④以极小页进行映射时,通过MVA[31:10]结合页表得到一个极小页(1KB)的起始物理地址,MVA[9:0]用来在极小页中寻址

7.1.3内存的访问权限检查
它决定一块内存是否允许读、是否允许写。这由CP15寄存器C3(域访问控制)、描述符的域(Domain)、CP15寄存器C1的R/S/A位、描述符的AP位共同决定。
“域”决定是否对某块内存进行权限检查,“AP”决定如何对某块内容进行权限检查。S3C2440有16个域,CP15寄存器C3中每两位对应一个域(一共32位),用来示这个域是否进行权限检查
每两位数据的含义
00:无访问权限(任何访问都将导致“Domain fault”异常)
01:客户模式(使用段描述符、页描述符进行权限检查)
10:保留(保留,目前相当于“无访问权限”)
11:管理模式(不进行权限检查,允许任何访问) Domain占用4位,用来表示内存属于0-15,哪一个域
例如: ①段描述符中的“Domain”为0b0010,表示1MB内存属于域2,如果域访问控制寄存器的[5:4]等于0b00,则访问这1MB空间都会产生“Domain fault”异常,如果等于0b01,则使用描述符中的“Ap”位进行权限检查
②粗页表中的“Domain”为0b1010,表示1MB内存属于域10,如果域访问控制寄存器的[21:20]等于0b01,则使用二级页表中的大页/小页描述符中的"ap3"、"ap2"、"ap1"、"ap0"位进行权限检查,如果等于0b11,则允许任何访问,不进行权限检查。
如下图:
一级页表描述符
第七章 <wbr> <wbr>内存管理单元MMU
二级页表描述符
第七章 <wbr> <wbr>内存管理单元MMU
AP,ap3、ap2、ap1、ap0结合cp15寄存器c1的r/s位,决定如何进行访问检查
段描述符中AP控制整个段(1MB)访问权限;大页描述符每个apx(0-3)控制一个大页(64KB)中1/4内存的访问权限,即ap3对应大页高端的16KB,ap0对应大页低端的16KB;小页描述符与大页描述符类似,每个apx(0-3)控制一个小页(4KB)的1/4内存的访问权限;极小页中的ap控制整个极小页(1KB)的访问权限。
下表为AP、S、R的对照表
AP S R 特权模式 用户模式 说明
00 0 0 无访问权限 无访问权限 任何访问将产生“Permission fault”异常
00 1 0 只读 无访问权限 在超级权限下可以进行读操作
00 0 1 只读 只读 任何写操作将产生”Permission fault“异常
00 1 1 保留 - -
01 x x 读/写 无访问权限 只允许在超级模式下访问
10 x x 读/写 只读 在用户模式下进行写操作将产生"Permission fault"异常
11 x x 读/写 读/写 在所有模式下允许任何访问
xx 1 1 保留 - -

 3、TLB的作用
当CPU发出一个虚拟地址时,MMU首先访问TLB。如果TLB中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查,否则MMU访问页表找到描述符后再进行地址转换和权限检查,并将这个描述符填入TLB中,下次再使用这个虚拟地址时就直接使用TLB用的描述符。
使用TLB需要保证TLB中的内容与页表一致,在启动MMU之前,页表中的内容发生变化后,尤其要注意。一般的做法是在启动MMU之前使整个TLB无效,改变页表时,使所涉及的虚拟地址对应的TLB中条目无效。

4、Cache的作用
 

同样基于程序访问的局部性,在主存和CPU通用寄存器之间设置一个高速的、容量相对较小的存储器,把正在执行的指令地址附近的一部分指令或数据从主存调入这个存储器,供CPU在一段时间内使用,对提高程序的运行速度有很大作用。这个cache一般称为高速缓存。

①写穿式(Write Through)

任一CPU发出写信号送到Cache的同时,也写入主存,保证主存的数据同步更新。优点是操作简单,但由于主存速度慢,降低了系统的写速度并占用了总线的时间。

②回写式(Write Back)

数据一般只写到Cache,这样可能出现Cache中的数据得到更新而主存中的数据不变(数据陈旧)的情况。此时可在Cache中设一个标志地址及数据陈旧的信息,只有当Cache中的数据被换出或强制进行”清空“操作时,才将原更新的数据写入主存响应的单元中,保证了Cache和主存中数据一致。

 

Cache有以下两个操作:

①”清空“(clean):把Cache或Write buffer中已经脏的(修改过,但未写入主存)数据写入主存

②”使无效“(Invalidate):使之不能再使用,并不将脏的数据写入主存。

 

S2C2440内置了指令Cache(ICaches)、数据Cache(DCaches)、写缓存(Write buffer),需要用到描述符中的C位(Ctt)和B位(Btt)

1)指令Cache(ICaches)

系统刚上电或复位时,ICaches中的内容是无效的,并且ICaches功能关闭。
Icr位(CP15协处理器中寄存器1的第12位)确定是不开启ICaches功能
写1可以启动ICaches,写0停止ICaches


ICaches一般在MMU开启后使用, 此时描述符的C位用来表示一段内存是否可以被Cache。 若Ctt=1,允许Cache,否则不允许。 如果MMU没有开启,ICaches也可以被使用, 此时CPU读取指令时所涉及的内存都被当做允许Cache

描述符c位确定是否允许cache, 若Ctt=1,允许Cache,否则不允许


ICaches关闭时,CPU每次取指都要读取主存,性能低,所以通常尽早启动ICaches


ICaches开启后,CPU每次取指时都会先在ICaches中查看是否能找到所用指令,而不管Ctt是0还是1。如果找到成为Cache命中,找不到称为Cache丢失,ICaches被开启后,CPU的取指有如下三种情况:
①Cache命中且Ctt为1时,从ICaches中取指,返回CPU
②Cache丢失且Ctt为1时,CPU从主存中取指,并且把指令缓存到Cache中
③Ctt为0时,CPU从主存中取指

2)数据Cache(DCaches)

与ICaches相似,系统刚上电或复位时,DCaches中的内容无效,并且DCaches功能关闭,Write buffer中的内容也是被废弃不用的。有专门的控制来开启和停止它
往Ccr位(CP15协处理器 中寄存器1的第二位)写1启动DCaches,写0停止DCaches。Write buffer和DCaches紧密结合,

与ICaches不同,DCaches功能必须在MMU开启之后才能被使用。

DCaches被关闭时,CPU每次都去内存取数据。

DCaches被开启后,CPU每次读写数据时都会先在DCaches中查看是否能找到所要的数据,不管Ctt是0还是1,找到了成为Cache命中,找不到成为Cache丢失。

通过下表可知DCaches和Write buffer在Ccr,Ctt,Btt各种取值下,如何工作,Ctt and Ccr 意为 Ctt与Ccr进行逻辑与后的值

 

Ctt and Ccr Btt DCaches、Write buffer 和主存的访问方式
0 0

Non-cached,non-buffered(NCNB)

读写数据时都是直接操作主存,并且可以被外设中止;

写数据时不使用Write buffer,CPU会等待写操作完成;

不会出现Cache命中

0 1

Non-Cached buffered(NCB)

读数据时都是直接操作主存;

不会出现Cache命中;

写数据时,数据线存入Write buffer,并在随后写入主存;

数据存入Write buffer后,CPU立即继续执行;

读数据时,可以被外设中止;

写数据时,无法被外设中止

1 0

Cached,write-through(写通)mode

读数据时,如果Cache命中则从Cache中返回数据,不读取主存;

读数据时,如果Cache丢失则从读主存中返回数据,并导致“linefill”的动作;

写数据时,数据先存入Write buffer,并在随后写入主存;

数据存入Write buffer后,CPU立即继续执行;

写数据时,如果Cache命中则新数据也写入Cache中;

写数据时,无法被外设中止

1 1

Cached,write-back(写回) mode

读数据时,如果Cache命中则从Cache中返回数据,不读取主存;

读数据时,如果Cache丢失则从读主存中返回数据,并导致“linefile”的动作;

写数据时,如果Cache丢失则将数据先存入Write buffer,存储完毕后CPu立即继续执行,这些数据在随后写入主存;

写数据时,如果Cache命中则在Cache中更新数据,并设置这些数据为”脏的“,但是不会写入主存;

无论Cache命中与否,写数据都无法被外设中止

 使用Cache时需要保证Cache、Write buffer的内容和主存内容一致,保证下面两个原则:

①清空DCaches,使主存数据得到更新(理解为剪切到主存)

②使无效ICaches,使CPU取指时重新读取主存

在实际编写程序时,要注意如下几点:

①开启MMU前,使无效ICaches,DCaches和Write buffer

②关闭MMU前,清空ICaches、DCaches,即将”脏“数据写到主存上

③如果代码有变,使无效ICaches,这样CPU取指时会从新读取主存

④使用DMA操作可以被Cache的内存时,将内存的数据发送出去时,要清空Cache;将内存的数据读入时,要使无效Cache

⑤改变页表中地址映射关系时也要慎重考虑

⑥开启ICaches或DCaches时,要考虑ICaches或DCaches中的内容是否与主存保持一

⑦对于I/O地址空间,不使用Cache和Write buffer


S3C2440 MMU、TLB、Cache的控制指令
7.1.6 

S3C2440除了ARM920T的CPU核心外,还有若干个协处理器,用来帮助主CPu完成一些特殊功能。对MMU、TLB、Cache等的操作涉及到协处理器。

{条件} 协处理器编码,协处理器操作码1,目的寄存器,源寄存器1,源寄存器2,协处理器操作码2

{cond} p#,,Rd,cn,cm{,<<wbr>expression2>}

MRC        //从协处理器获得数据,传给ARM920T CPU核心寄存器

MCR        //数据从ARM920T CPU核心寄存器传给协处理器

{cond}        //执行条件,省略时表示无条件执行

p#        //协处理器序号

        //一个常数

Rd        //ARM920T CPU核心的寄存器

cn和cm        //协处理器中的寄存器

        //一个常数

其中,、cn、cm、<<wbr>expression2>仅供协处理器使用,它们的作用如何取决于具体的协处理器



7.2 MMu实例:地址映射

这个实例将开启MMU,并将虚拟地址0xA0000000-0xA0100000映射到物理地址0x56000000-0x56100000(GPBCON物理地址为0x56000010,GPBDAT物理地址为0x56000014),来驱动LED。

将虚拟地址0xB0000000-0xB3FFFFFF映射到物理地址0x30000000-0x33FFFFFF,在连接程序时,将一部分代码的运行地址指定为0xB0004000.

这个程序只使用一级页表,以段的方式进行地址映射,32位CPU虚拟地址空间达到4G,一级页表使用4096个描述符来表示4G空间(每个描述符对应1MB),每个描述符占4字节,所以一级页表占16KB。这个程序使用SDRAM的开始16KB存放一级页表,所以剩下的内存开始地址就为0x30004000,这个地址最终会对应虚拟地址0xB0004000(所以代码运行地址为0xB0004000)

程序分为两部分:第一部分的运行地址为0,它用来初始化SDRAM,复制第二部分的代码到SDRAM中(存放在0x30004000)、设置页表、启动MMU,最后跳到SDRAM中(地址0xB0004000),第二部分运行地址设为0xB0004000,用来驱动LED

第七章 <wbr> <wbr>内存管理单元MMU
先看连接文件mmu.lds
SECTIONS {
     first  0x00000000:{head.o init.o}
     second 0xB0004000: AT(2048) {leds.o}  //AT(2048) 指定这个段在编译出来的映像文件中的加载地址如果不使用这个选项,则加载地址等于运行地址
}
程序分两个段:first和second。first由head.o和init.o组成,加载和运行地址都是0,second由leds.o组成,加载地址为2048,重定位地址为0xB0004000。


head.s:
 1 .text
  2 .global _start
  3 _start:
         ldr sp,=4096         
         bl disable_watch_dog   
         bl memsetup    
         bl copy_2th_to_sdram   
         bl mmu_init     
         ldr sp,=0xB4000000     
10         ldr pc,=0xB0004000     
11
12 halt_loop:
13         b halt_loop

INIT.C:       



#define WTCON        (*(volatile unsigned long *)0x53000000)   //关门狗地址


#define MEM_CTL_BASE        0x48000000                 //存储器寄存器地址


void disable_watch_dog(void)
{
        WTCON=0;                                                   //关闭看门狗
}

void memsetup(void)
{
        unsigned long const  mem_cfg_val[]=  {0x22011110,  0x00000700,0x00000700, 0x00000700,0x00000700, 0x00000700,0x00000700, 0x00018005,0x00018005, 0x008c07A3,0x000000B1, 0X00000030,0x00000030};   //写入13个存储器寄存器值   
        int i=0;
        volatile unsigned long *p=(volatile unsigned long *)MEM_CTL_BASE;  
//=(volatile unsigned long *)MEM_CTL_BASE;//32bit
        for(;i<13;i++)
        {
          p[i]=mem_cfg_val[i];
        }
}


void copy_2th_to_sdram(void)
{
        unsigned int *pdwSrc=(unsigned int *)2048;    //steppingstone
        unsigned int *pdwDest=(unsigned int *)0x30004000;//sdram
       
        while(pdwSrc<(unsigned int *)4096)
        {
                  *pdwDest=*pdwSrc;
                  pdwDest++;
                  pdwSrc++;
        }
}



void create_page_table(void)
{
       
        #define MMU_FULL_ACCESS                  (3<<10)   //AP 11 允许访问权限
      #define MMU_DOMAIN                  (0<<5)  // DOMAIN (在段,粗表细表描述符中[8:5]分别用来确定0~15哪一个域,而cp15的c3每两位对应一个域用来确定权限 )
第七章 <wbr> <wbr>内存管理单元MMU
        #define MMU_SPECIAL                      (1<<4) 
        #define MMU_CACHEABLE                  (1<<3)
        virtuladdr=0;
        physicaladdr=0;
       
        *(mmu_tlb_base+(virtuladdr>> 20))=(physicaladdr&0xFFF00000) |MMU_SECDESC_WB;//段描述符
      
        virtuladdr=0xA0000000;
        physicaladdr=0x56000000;
        *(mmu_tlb_base+(virtuladdr>> 20)) =(physicaladdr&0xFFF00000)| MMU_SECDESC;//不设置c b表示不使用cache
         
        virtuladdr=0xB0000000;
        physicaladdr=0x30000000;
        while (virtuladdr<0xB4000000)
             
          *(mmu_tlb_base+(virtuladdr>> 20))=(physicaladdr&0xFFF00000) |MMU_SECDESC_WB;
                  virtuladdr+=0x100000; //每个描述符1mb
                  physicaladdr+=0x100000;
        }

}


void mmu_init(void)
{
        unsigned long ttb=0x30000000;
__asm__(
// 2012年9月10日 22:53:01  后面都是arm的cp15协处理器的指令,应该停一停去看<<wbr>arm体系结构》了,在看一下《[嵌入式Linux.C语言应用程序设计].华清远见培训.pdf - 快捷方式》学习完全在linux下编程学习Makefile 再来看下去!!!
        "mov r0,#0\n"
        "mcr p15,0,r0,c7,c7,0\n"        //使无效 ICache DCache 
       
        "mcr p15,0,r0,c7,c10,4\n"        //drain write buffer on v4
        "mcr p15,0,r0,c8,c7,0\n"        //是无效数据tbl
       
        "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"   //读出控制寄存器的值
       

        "bic r0,r0,#0x3000\n"
        "bic r0,r0,#0x0300\n"
        "bic r0,r0,#0x0087\n"


        "orr r0,r0,#0x0002\n"
        "orr r0,r0,#0x0004\n"
        "orr r0,r0,#0x1000\n"
        "orr r0,r0,#0x0001\n"
       

        "mcr p15,0,r0,c1,c0,0\n"

        :
        :"r"(ttb)
);
       
}

leds.c:
#define GPBCON (*(volatile unsigned long *) 0xA0000010)  //pa=0x56000010
#define GPBDAT (*(volatile unsigned long *) 0xA0000014 ) //pa-0x56000014

#define GPB5_OUT (1<<(5*2))
#define GPB6_OUT (1<<(6*2))
#define GPB7_OUT (1<<(7*2))
#define GPB8_OUT (1<<(8*2))      //01=OUT

static inline void wait(unsigned
...
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值