MMU代码分析(作者:wogoyixikexie@gliet)

MMU代码分析(作者:wogoyixikexie@gliet

    ——以前我一直误以为MMU在OAL才开的,在bootloader是不开的,真是大错特错。这个也不怪我,因为有很多书都说在bootloader阶段是不开的。现在看看代码真是感慨万千。

————————————————————————

    这两天,重新看了MMU和cache,对它有了一点了解,现在再把疑问放到论坛上来,希望大家帮忙(前几天参与押宝,现在分数还没有回来,没有分发帖了,以后有分再补回吧,sorry)
——————————————————————————————————————
我看了的ADS bootloader部分,发现一个很奇怪的问题,它居然只是对一级页表做了配置,并且把虚拟内存和物理内存映射成相等
不知道为什么会这样。这不是多此一举吗?

看如下代码(三星提供)

C/C++ code
   
   
/* *********************************************** NAME : MMU.C DESC : Revision: 2002.2.28 ver 0.0 *********************************************** */ #include " def.h " #include " option.h " #include " 2440addr.h " #include " 2440lib.h " #include " 2440slib.h " #include " mmu.h " // 1) Only the section table is used. // 2) The cachable/non-cachable area can be changed by MMT_DEFAULT value. // The section size is 1MB. void MMU_Init( void ) { int i,j; // ========================== IMPORTANT NOTE ========================= // The current stack and code area can't be re-mapped in this routine. // If you want memory map mapped freely, your own sophiscated MMU // initialization code is needed. // =================================================================== MMU_DisableDCache(); MMU_DisableICache(); // If write-back is used,the DCache should be cleared. for (i = 0 ;i < 64 ;i ++ ) for (j = 0 ;j < 8 ;j ++ ) MMU_CleanInvalidateDCacheIndex((i << 26 ) | (j << 5 )); MMU_InvalidateICache(); #if 0 // To complete MMU_Init() fast, Icache may be turned on here. MMU_EnableICache(); #endif MMU_DisableMMU(); MMU_InvalidateTLB(); // MMU_SetMTT(int vaddrStart,int vaddrEnd,int paddrStart,int attr) MMU_SetMTT( 0x00000000 , 0x07f00000 , 0x00000000 ,RW_CNB); // bank0 MMU_SetMTT( 0x08000000 , 0x0ff00000 , 0x08000000 ,RW_CNB); // bank1 MMU_SetMTT( 0x10000000 , 0x17f00000 , 0x10000000 ,RW_NCNB); // bank2 MMU_SetMTT( 0x18000000 , 0x1ff00000 , 0x18000000 ,RW_NCNB); // bank3 // MMU_SetMTT(0x20000000,0x27f00000,0x20000000,RW_CB); // bank4 MMU_SetMTT( 0x20000000 , 0x27f00000 , 0x20000000 ,RW_CNB); // bank4 for STRATA Flash MMU_SetMTT( 0x28000000 , 0x2ff00000 , 0x28000000 ,RW_NCNB); // bank5 MMU_SetMTT( 0x30000000 , 0x30f00000 , 0x30000000 ,RW_CB); // bank6-1 MMU_SetMTT( 0x31000000 , 0x33e00000 , 0x31000000 ,RW_NCNB); // bank6-2 MMU_SetMTT( 0x33f00000 , 0x33f00000 , 0x33f00000 ,RW_CB); // bank6-3 MMU_SetMTT( 0x38000000 , 0x3ff00000 , 0x38000000 ,RW_NCNB); // bank7 MMU_SetMTT( 0x40000000 , 0x47f00000 , 0x40000000 ,RW_NCNB); // SFR MMU_SetMTT( 0x48000000 , 0x5af00000 , 0x48000000 ,RW_NCNB); // SFR MMU_SetMTT( 0x5b000000 , 0x5b000000 , 0x5b000000 ,RW_NCNB); // SFR MMU_SetMTT( 0x5b100000 , 0xfff00000 , 0x5b100000 ,RW_FAULT); // not used MMU_SetTTBase(_MMUTT_STARTADDRESS); MMU_SetDomain( 0x55555550 | DOMAIN1_ATTR | DOMAIN0_ATTR); // DOMAIN1: no_access, DOMAIN0,2~15=client(AP is checked) MMU_SetProcessId( 0x0 ); MMU_EnableAlignFault(); MMU_EnableMMU(); MMU_EnableICache(); MMU_EnableDCache(); // DCache should be turned on after MMU is turned on. } // attr=RW_CB,RW_CNB,RW_NCNB,RW_FAULT void ChangeRomCacheStatus( int attr) { int i,j; MMU_DisableDCache(); MMU_DisableICache(); // If write-back is used,the DCache should be cleared. for (i = 0 ;i < 64 ;i ++ ) for (j = 0 ;j < 8 ;j ++ ) MMU_CleanInvalidateDCacheIndex((i << 26 ) | (j << 5 )); MMU_InvalidateICache(); MMU_DisableMMU(); MMU_InvalidateTLB(); MMU_SetMTT( 0x00000000 , 0x07f00000 , 0x00000000 ,attr); // bank0 MMU_SetMTT( 0x08000000 , 0x0ff00000 , 0x08000000 ,attr); // bank1 MMU_EnableMMU(); MMU_EnableICache(); MMU_EnableDCache(); } void MMU_SetMTT( int vaddrStart, int vaddrEnd, int paddrStart, int attr) { volatile U32 * pTT; volatile int i,nSec; pTT = (U32 * )_MMUTT_STARTADDRESS + (vaddrStart >> 20 ); nSec = (vaddrEnd >> 20 ) - (vaddrStart >> 20 ); for (i = 0 ;i <= nSec;i ++ ) * pTT ++= attr | (((paddrStart >> 20 ) + i) << 20 ); }



但是在eboot中使用 的映射就是使用OEMAddresstable来初始化页表,这是wince真正的虚实映射
说道这里OEMAddresstable的虚拟起始地址0x80000000是wince规定的还是本来ARM决定的?
我觉得这个虚拟地址是可以改变的,所以才会可以把物理地址和虚拟地址映射成相等的?
——————————————————————————————————————————

引用 1 楼 xajhuang 的回复:
个人认为,虚拟内存和物理内存是否相等完全没有关系,ADS下没有操作系统,不需要那么复杂的保护操作,映射成相等可以方便使用,开启了MMU后的cache对系统性能提高不止一点,为的就在开启了MMU的情况下还能向使用物理地址一样使用寄存器地址。



有道理。
——————
并且由于ADS 下没有操作系统,所以只需要使用一级页表就完全够用了,
eboot调用了大量微软的函数库,以及源代码,所以不得不和微软的映射方法相同。
不过我觉得既然可以禁用MMU,为什么还要使用它呢?据我所知,禁用MMU之后虚拟地址和物理地址是相等的。why?
——————————————————————————————————————————

开启 MMU, 主要利用它的告诉 CACHE,这样可以大大加快系统速度,你不信比较下不开MMU时侯的系统运行的情况就明白了

——————————————————————————————————————————

wince的架构已经固定了,所以这个映射表肯定是固定的,因为微软是不开源的,我们在里面自己来一套自定义映射表,就是自找麻烦了。

——谢谢大家的积极参与
关于这个MMU和cache我已经写了两三篇博客了,不过我在文章中大量引用英文,只是加了少许注释,对这个文章很不满意,相当于写给自己看的。
下面这些书讲的比较好,大家如果想了解MMU、cache的工作原理以及硬件结构。看了他们真的会很有帮助。
ARM System Developer's Guide: Designing and Optimizing System Software
——ARM System Developer's Guide: Designing and Optimizing System Software——ARM嵌入式系统开发:软件设计与优化的英文原版——我个人感觉这是国内翻译ARM书籍最好的一本之一,比杜XX的ARM体系结构与编程好千倍。 本书虽然说软件设计与优化,但是讲的硬件也很多,比如MMU和cache等,讲的精彩纷呈:我刚才想写关于MMU和cache的博客,发现太庞大,看来这段时间要重新看看这本书才能写。
下载地址:http://download.csdn.net/source/904273

ARM920T Technical Reference Manual——不多说了,想了解2440等的bootloader的人一定要看这个东西了,一些协处理器指令讲的很详细
下载地址:http://download.csdn.net/source/903240

ARM Architecture Reference Manual(2nd Edition) ——比较有价值的英文ARM书籍
下载地址http://download.csdn.net/source/901433
————————继续,看看二级页表是怎么设置的。

 

————————————————————————————————————————

 

引用 14 楼 Ricky_hu 的回复:
0x80000000 - 0x9FFFFFFF represents a CACHED address.
0xA0000000 - 0xBFFFFFFF represents and UNCACHED address.

这是wince里面的定义!bsp里面直接引用就可以了吧,没有找到定义的地方!



这个完全是由OEMAddresstable决定了,至于为什么UNCACHED 要加上0x20000000而不是别的,我还是我原来的看法。
就是这个是由于硬件原因。这个ARM貌似没有说明。知道的来知会一声。
这个再次证明了我的观点OEMAddresstable是可以自己定义的,但是微软他定了这套标准,并且所有的代码都是围绕着这个转的
有些还不开源,所以我们是不能自己乱改的。
OEMAddresstable是以cached 形式写的,在程序中我们使用unchache的时候就要自己手动加上即可,这个微软都有源代码转换的
请看C:/WINCE500/PLATFORM/COMMON/SRC/INC/oal_memory.C

————————————————————————————————————————

哈哈,越来越明朗了
——————————————————
在这里,我声明我以前在论坛的一次错误回答:有个人问bootloader下的map.a(2440 4.2BSP)和OAL下的map.a是一模一样的,问我是否有用,我回答说:没有用,现在更正。——因为我的5.0 BSP 的bootloader下是没有 OEMAddresstable的,刚才看了一下sources文件,发现他链接了OEMAddresstable进来,——我在这里为我的粗心感到抱歉,希望他也能看到这个帖子。
————还有好多呢,继续,继续。
咱们CSDN的程序员最缺乏的就是硬件的一些东西了。

————————————————————————————————————————

引用 24 楼 singlerace 的回复:
0x80000000当然是CE的设计要求的,ARM架构没理由做这个限制。CE这样设计的目的是简化底层代码的开发。如果你仔细看wince设置页表的代码(在startup.s中),可以发现OEMAddresstable中相同的物理地址被映射了两遍,第一遍是cached address,第二遍是uncached address。 (这个的确如此,我看了。不过操作系统后面可以自己访问的时候设置的。)
在一个有操作系统的平台上,使用虚拟地址有很多好处:一个是通过限制进程的可以访问地址空间把不同的进程隔离开,防止它们互相影响;另外一个是可以捕捉程序错误,比如说C/C++程序中很常见的一个错误是访问空指针的内容。访问空指针其实是访问地址零里的内容,在ARM架构中物理地址零是一个有效地址(否则系统无法启动),访问地址零不会引发错误;另一方面一般在C/C++应用程序中访问空指针基本上肯定是程序员的错误,这种错误使用物理地址无法捕捉。启用虚拟地址后,一般进程空间的虚拟地址零不会映射到任何物理页面,程序对空指针地址的访问就会触发Page Fault异常,导致操作系统把它终结。当然启用cache也是另外一个好处 。(学习,这个指针这里的确是有这么一手)



————谢谢 singlerace  学习了。
————————————————————————————
这么吧,我把cache以及MMU的作用贴上来吧(免得大伙还以为我想问cache以及MMU的作用呢。)
cache:一个和CPU很近的高速存储器,用来存储一些不是经常变化的数据,提高速度。在经常改变的数据的时候不适合启用,否则效率会更低
比如我们访问GPIO等不能使用cached 地址,就是这个原因,经常替换,效率很低的。(这个东西,也是我们PC的CPU的重要指标)
MMU:用在多任务操作系统中,给每个任务提供独立的虚拟地址空间,其实现原理是:在主存中存贮页表等数据,通过MMU映射到CPU,然后CPU就可以使用虚拟地址调度任务,访问外设等,虚拟地址和物理地址映射是固定的,这样操作系统比较安全稳定。

————————————————————————————————————————————

引用 43 楼 gooogleman 的回复:
我已经找到二级页表设置的地方,哎,可惜是元旦,还要出去购物



在 OAL的startup.s里面有bl KernelStart
然后在C:/WINCE500/PRIVATE/WINCEOS/COREOS/NK/KERNEL/ARM/armtrap.s(386):; KernelStart - kernel main entry point
找到我二级页表设置的地方。先看看先吧。哈哈。

————————————————————————————————————————————

Assembly code
   
   
; ——在OAL的startup.s有如下: ; add r0, pc, #g_oalAddressTable - (. + 8) ; bl KernelStart ; ——以前真的没有仔细看过这些汇编,看过才能知道这个wince的流程到底是怎么回事。 ; 这相当于一到OAL阶段就要重新初始化页表,MMU,cache使能控制等 ; 在C:/WINCE500/PRIVATE/WINCEOS/COREOS/NK/KERNEL/ARM/armtrap.s有KernelStart,现在好好分析一下。 ; ---------------------------------------------------------------------------------------- ; KernelStart - kernel main entry point ; ; The OEM layer will setup any platform or CPU specific configuration that is ; required for the kernel to have access to ROM and DRAM and jump here to start up ; the system. Any processor specific cache or MMU initialization should be completed. ; The MMU and caches should not enabled. ; ; This routine will initialize the first-level page table based up the contents of ; the MemoryMap array and enable the MMU and caches. ; ; NOTE: Until the MMU is enabled, kernel symbolic addresses are not valid and must be ; translated via the MemoryMap array to find the correct physical address. ; ; Entry (r0) = pointer to MemoryMap array in physical memory ; Exit returns if MemoryMap is invalid ; ------------------------------------------------------------------------------- LEAF_ENTRY KernelStart ; add r0, pc, #g_oalAddressTable - (. + 8) mov r11, r0 ; (r11) = &MemoryMap (save pointer) ; figure out the virtual address of OEMAddressTable mov r1, r11 ; (r1) = &MemoryMap (2nd argument to VaFromPa) bl VaFromPa mov r6, r0 ; (r6) = VA of MemoryMap ; convert base of PTs to Physical address ldr r4, =PTs ; (r4) = virtual address of FirstPT mov r0, r4 ; (r0) = virtual address of FirstPT mov r1, r11 ; (r1) = &MemoryMap (2nd argument to PaFromVa) bl PaFromVa mov r10, r0 ; (r10) = ptr to FirstPT (physical) ; Zero out page tables & kernel data page mov r0, # 0 ; (r0-r3) = 0's to store mov r1, # 0 mov r2, # 0 mov r3, # 0 mov r4, r10 ; (r4) = first address to clear add r5, r10, #KDEnd-PTs ; (r5) = last address + 1 18 stmia r4!, {r0-r3} stmia r4!, {r0-r3} cmp r4, r5 blo %B18 ; Setup 2nd level page table to map the high memory area which contains the ; first level page table, 2nd level page tables, kernel data page, etc. add r4, r10, #HighPT-PTs ; (r4) = ptr to high page table orr r0, r10, #0x051 ; (r0) = PTE for 64K, kr/w kr/w r/o r/o page, uncached unbuffered str r0, [r4, #0xD0* 4 ] ; store the entry into 8 consecutive slots str r0, [r4, #0xD1* 4 ] str r0, [r4, #0xD2* 4 ] str r0, [r4, #0xD3* 4 ] add r8, r10, #ExceptionVectors-PTs ; (r8) = ptr to vector page bl OEMARMCacheMode ; places C and B bit values in r0 as set by OEM mov r2, r0 orr r0, r8, #0x002 ; construct the PTE orr r0, r0, r2 str r0, [r4, #0xF0* 4 ] ; store entry for exception vectors orr r0, r0, #0x500 ; (r0) = PTE for 4k r/o r/o kr/w kr/w C+B page str r0, [r4, #0xF4* 4 ] ; store entry for abort stack str r0, [r4, #0xF6* 4 ] ; store entry for FIQ stack (access permissions overlap for abort and FIQ stacks, same 1k) orr r0, r8, #0x042 orr r0, r0, r2 ; (r0)= PTE for 4K r/o kr/w r/o r/o (C+B as set by OEM) str r0, [r4, #0xF2* 4 ] ; store entry for interrupt stack add r9, r10, #KPage-PTs ; (r9) = ptr to kdata page orr r0, r9, #0x002 orr r0, r0, r2 ; (r0)=PTE for 4K (C+B as set by OEM) orr r0, r0, #0x250 ; (r0) = set perms kr/w kr/w kr/w+ur/o r/o str r0, [r4, #0xFC* 4 ] ; store entry for kernel data page orr r0, r4, #0x001 ; (r0) = 1st level PTE for high memory section add r1, r10, #0x4000 str r0, [r1, #- 4 ] ; store PTE in last slot of 1st level table IF {FALSE} mov r0, r4 mov r1, # 256 ; dump 256 words CALL WriteHex ENDIF ; Fill in first level page table entries to create "un-mapped" regions ; from the contents of the MemoryMap array. ; ; (r9) = ptr to KData page ; (r10) = ptr to 1st level page table ; (r11) = ptr to MemoryMap array add r10, r10, #0x2000 ; (r10) = ptr to 1st PTE for "unmapped space" mov r7, # 2 ; (r7) = pass counter mov r0, #0x02 orr r0, r0, r2 ; (r0)=PTE for 0: 1MB (C+B as set by OEM) orr r0, r0, #0x400 ; set kernel r/w permission 20 mov r1, r11 ; (r1) = ptr to MemoryMap array 25 ldr r2, [r1], # 4 ; (r2) = virtual address to map Bank at ldr r3, [r1], # 4 ; (r3) = physical address to map from ldr r4, [r1], # 4 ; (r4) = num MB to map cmp r4, # 0 ; End of table? beq %F29 ldr r5, =0x1FF00000 and r2, r2, r5 ; VA needs 512MB, 1MB aligned. ldr r5, =0xFFF00000 and r3, r3, r5 ; PA needs 4GB, 1MB aligned. add r2, r10, r2, LSR # 18 add r0, r0, r3 ; (r0) = PTE for next physical page 28 str r0, [r2], # 4 add r0, r0, #0x00100000 ; (r0) = PTE for next physical page sub r4, r4, # 1 ; Decrement number of MB left cmp r4, # 0 bne %B28 ; Map next MB bic r0, r0, #0xF0000000 ; Clear Section Base Address Field bic r0, r0, #0x0FF00000 ; Clear Section Base Address Field b %B25 ; Get next element 29 bic r0, r0, #0x0C ; clear cachable & bufferable bits in PTE add r10, r10, #0x0800 ; (r10) = ptr to 1st PTE for "unmapped uncached space" subs r7, r7, # 1 ; decrement pass counter bne %B20 ; go setup PTEs for uncached space if we're not done sub r10, r10, #0x3000 ; (r10) = restore address of 1st level page table IF {FALSE} mov r0, r10 mov r1, # 4096 ; dump 4096 words CALL WriteHex ENDIF

————————————————————————————————————————————

http://www.cnblogs.com/we-hjb/archive/2008/10/12/1309596.html
——这篇博客也讲得比较好,值得参考。

引用 73 楼 Reallyu 的回复:
这个问题也一直弄的不是很清楚,趁着这个机会学习下.据我的理解,OEMAddresstable并不是一个完整的页表,它只把虚拟地址的一部分映射到物理地址.
系统为每一个进程都会维护一份页表,在进程切换的时候把不同的页表基地址传MMU.OEMAddresstable的作用是系统在想访问某个确认的物理地址的时候,可以很容易的找到它的虚拟地址,而不需要关心mmu里现在放的是哪个页表.



谢谢Reallyu 老兄,好久不见你。
————————————————
我仔细看了几天,发现这个OEMAddresstable只是用来初始化一级页表,就是所谓的段(section)描述,每个段是1MB,分为4096个段,总共4G——虚拟内存空间4G就是由此而来。
并且这个OEMAddresstable可以用在查表法中用来转换虚拟地址、物理地址(相互转换都可以),这个在上面的代码已经体现出来了。在memory.c也有C语言用它来实现虚拟地址、物理地址之间的相互转换。
并且,这个一级页表可以用来存储二级页表的目录,看上面的图就知道是怎么实现的,在armtrap.s中就使用了这种方法。
——————
对于一些操作系统所谓的64K/4K分页等,我还是不是很明白,估计现在还没有哪能力强制吞下去。armtrap.s使用了一些莫名奇妙的宏指令,给看代码造成很大麻烦,现在再看看。也许就是一步之遥了。哈哈。Come on!

——————————————————————————————————————————————

MACRO
        mtc15   $cpureg, $cp15reg
        mcr     p15,0,$cpureg,$cp15reg,c0,0
        MEND         MACRO
        mfc15   $cpureg, $cp15reg
        mrc     p15,0,$cpureg,$cp15reg,c0,0
        MEND
  
  
; The page tables and exception vectors are setup. Initialize the MMU and turn it on. ; 页表和异常向量表建立之后,初始化MMU并打开 mov r1, # 1 mtc15 r1, c3 ; Setup access to domain 0 and clear other ; domains including 15 for PSL calls (see above) mtc15 r10, c2 ; ;mtc15 是宏定义 把TTB(L1转换基地址放到C2) mov r0, # 0 mcr p15, 0 , r0, c8, c7, 0 ; Flush the I&D TLBs mfc15 r1, c1 ; mfc15 宏定义 读出C1的值到r1 orr r1, r1, #0x007F ; changed to read-mod-write for ARM920 Enable: MMU, Align, DCache, WriteBuffer orr r1, r1, #0x3200 ; vector adjust, ICache, ROM protection ldr r0, VirtualStart ; 这个很关键在代码中有 VirtualStart DCD VStart cmp r0, # 0 ; make sure no stall on "mov pc,r0" below mtc15 r1, c1 ; enable the MMU & Caches mov pc, r0 ; & jump to new virtual address 跳到VStart运行 nop




总结:填写好二级页表,把二级页表的首地址存放到一级页表的表项最后最后;根据OEMAddresstable初始化L1页表;把L1的转换表的基地址放到协处理器的c1的寄存器。启动MMU等功能即可。
——现在基本没有什么疑问了,完成博客,今天晚上回去结贴。——目前对于一些domain、AP的设置等还是不是很明白。不过都是一个样,看看数据手册即可。阿门!大功告成。

引用 77 楼 iwillbeback008 的回复:
总结:填写好二级页表,把二级页表的首地址存放到一级页表的表项最后最后;根据OEMAddresstable初始化L1页表;把L1的转换表的基地址放到协处理器的c1的寄存器。启动MMU等功能即可。
------
那本《Windows CE工程实践完全解析》中有讲到"二级页表的首地址存放到一级页表的表项最后"!
在第32、第61~第62页



噢噢,my god!这本书我上一周购买了,我看是六点0的,就没有看。准备储藏到明年才看。
噢噢,害我看了好几天啊。不过这样也好,锻炼一下,塞翁失马,焉知非福。

转载请标明:作者wogoyixikexie@gliet.桂林电子科技大学一系科协,原文地址:http://blog.csdn.net/gooogleman——如有错误,希望能够留言指出;如果你有更加好的方法,也请在博客后面留言,我会感激你的批评和分享。

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
EL1和CA76是指ARM处理器的执行等级和架构版本。使能MMU代码则是用于在ARM处理器上启用内存管理单元(MMU)的代码。 在ARM处理器架构中,MMU起着重要的作用,它负责将虚拟地址转换为物理地址,并提供对内存访问权限的控制。启用MMU可以使程序能够使用虚拟地址空间,提供更好的内存管理和保护机制。 以下是一个简单的示例代码片段来使能MMU: ```C void enable_mmu(void) { unsigned int ttbr0, ttbcr; // 设置页表基址和控制寄存器 ttbr0 = /* 设置页表基址 */; asm volatile("mcr p15, 0, %0, c2, c0, 0" :: "r" (ttbr0)); ttbcr = /* 设置控制寄存器 */; asm volatile("mcr p15, 0, %0, c2, c0, 2" :: "r" (ttbcr)); // 使能MMU asm volatile("mrc p15, 0, r0, c1, c0, 0"); asm volatile("orr r0, r0, #0x1"); asm volatile("mcr p15, 0, r0, c1, c0, 0"); } ``` 上述代码的作用是设置页表基址和控制寄存器,然后通过修改协处理器中的寄存器来启用MMU。具体的代码实现可能会根据处理器的型号和架构版本有所不同。 启用MMU之后,程序可以使用虚拟地址来访问内存,MMU会负责将虚拟地址转换为物理地址,并进行访问权限的控制。这可以提高内存管理的灵活性和安全性,使程序运行更加可靠和高效。 总之,以上代码片段是一个简单的示例,用于在ARM处理器上启用MMU。具体的代码实现可能会因处理器型号和架构版本而有所不同。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值