嵌入式Linux之我行——ARM MMU工作原理剖析

从PL320到MHU:ARM核间通信硬件演进与设计哲学 本文探讨了ARM核间通信硬件从PL320到MHU的演进历程与设计哲学。分析了PL320多邮箱架构和MHU轻量级模块化设计的优缺点,重点介绍了基于AHB总线的IPC机制如何提升异构SoC的通信效率、降低延迟与功耗,为多核系统设计提供关键见解。 阅读详情
 
嵌入式Linux之我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。

一、MMU的产生

      许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序。但随着图形界面的兴起还用用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题出现在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序,通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段。覆盖块0首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由OS完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。人们必须找到更好的办法从根本上解决这个问题。不久人们找到了一个办法,这就是虚拟存储器(virtual memory).虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上比如对一个16MB的程序和一个内存只有4MB的机器,OS通过选择,可以决定各个时刻将哪4M的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个16M的程序运行在一个只具有4M内存机器上了。而这个16M的程序在运行前不必由程序员进行分割。


      任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由CPU的位数决定,例如一个32位的CPU,它的地址范围是0~0xFFFFFFFF (4G)而对于一个64位的CPU,它的地址范围为0~0xFFFFFFFFFFFFFFFF (64T),这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集,这里举一个最简单的例子直观地说明这两者,对于一台内存为256MB的32bit x86主机来说,它的虚拟地址空间范围是0~0xFFFFFFFF(4G),而物理地址空间范围是0x000000000~0x0FFFFFFF(256MB)。


      在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写。而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到内存管理单元——MMU(主角终于出现了)。他由一个或一组芯片组成,一般存在与协处理器中,其功能是把虚拟地址映射为物理地址。

 

二、MMU工作过程

      大多数使用虚拟存储器的系统都使用一种称为分页(paging)。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页框(frame).页和页框的大小必须相同。接下来配合图片我以一个例子说明页与页框之间在MMU的调度下是如何进行映射的:

      在这个例子中我们有一台可以生成16位地址的机器,它的虚拟地址范围从0x0000~0xFFFF(64K),而这台机器只有32K的物理地址,因此他可以运行64K的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放64K程序的外部存储器(例如磁盘或是FLASH)以保证程序片段在需要时可以被调用。在这个例子中,页的大小为4K,页框大小与页相同(这点是必须保证的,内存和外围存储器之间的传输总是以页为单位的),对应64K的虚拟地址和32K的物理存储器,他们分别包含了16个页和8个页框。


      我们先根据上图解释一下分页后要用到的几个术语,在上面我们已经接触了页和页框,上图中绿色部分是物理空间,其中每一格表示一个物理页框。橘黄色部分是虚拟空间,每一格表示一个页,它由两部分组成,分别是Frame Index(页框索引)和位p(present 存在位),Frame Index的意义很明显,它指出本页是往哪个物理页框进行映射的,位p的意义则是指出本页的映射是否有效,如上图,当某个页并没有被映射时(或称映射无效,Frame Index部分为X),该位为0,映射有效则该位为1。


    我们执行下面这些指令(本例子的指令不针对任何特定机型,都是伪指令)
例1:
    MOVE REG,0 //将0号地址的值传递进寄存器REG.
    虚拟地址0将被送往MMU,MMU看到该虚地址落在页0范围内(页0范围是0到4095),从上图我们看到页0所对应(映射)的页框为2(页框2的地址范围是8192到12287),因此MMU将该虚拟地址转化为物理地址8192,并把地址8192送到地址总线上。内存对MMU的映射一无所知,它只看到一个对地址8192的读请求并执行它。MMU从而把0到4096的虚拟地址映射到8192到12287的物理地址。


例2:
    MOVE REG,8192
    被转换为
    MOVE REG,24576
    因为虚拟地址8192在页2中,而页2被映射到页框6(物理地址从24576到28671)


例3:
    MOVE REG,20500
    被转换为
    MOVE REG,12308
    虚拟地址20500在虚页5(虚拟地址范围是20480到24575)距开头20个字节处,虚页5映射到页框3(页框3的地址范围是 12288到16383),于是被映射到物理地址12288+20=12308。


      通过适当的设置MMU,可以把16个虚页隐射到8个页框中的任何一个,但是这个方法并没有有效的解决虚拟地址空间比物理地址空间大的问题。从上图中我们可以看到,我们只有8个页框(物理地址),但我们有16个页(虚拟地址),所以我们只能把16个页中的8个进行有效的映射。我们看看例4会发生什么情况

      MOV REG,32780
      虚拟地址32780落在页8的范围内,从上图总我们看到页8没有被有效的进行映射(该页被打上X),这是又会发生什么?MMU注意到这个页没有被映射,于是通知CPU发生一个缺页故障(page fault).这种情况下操作系统必须处理这个页故障,它必须从8个物理页框中找到1个当前很少被使用的页框并把该页框的内容写入外围存储器(这个动作被称为page copy),随后把需要引用的页(例4中是页8)映射到刚才释放的页框中(这个动作称为修改映射关系),然后从新执行产生故障的指令(MOV REG,32780)。假设操作系统决定释放页框1,那么它将把虚页8装入物理地址的4-8K,并做两处修改:首先把标记虚页1未被映射(原来虚页1是被影射到页框1的),以使以后任何对虚拟地址4K到8K的访问都引起页故障而使操作系统做出适当的动作(这个动作正是我们现在在讨论的),其次他把虚页8对应的页框号由X变为1,因此重新执行MOV REG,32780时,MMU将把32780映射为4108。


      我们大致了解了MMU在我们的机器中扮演了什么角色以及它基本的工作内容是什么,下面我们将举例子说明它究竟是如何工作的(注意,本例中的MMU并无针对某种特定的机型,它是所有MMU工作的一个抽象)。


      首先明确一点,MMU的主要工作只有一个,就是把虚拟地址映射到物理地址。
我们已经知道,大多数使用虚拟存储器的系统都使用一种称为分页(paging)的技术,就象我们刚才所举的例子,虚拟地址空间被分成大小相同的一组页,每个页有一个用来标示它的页号(这个页号一般是它在该组中的索引,这点和C/C++中的数组相似)。在上面的例子中0~4K的页号为0,4~8K的页号为1,8~12K的页号为2,以此类推。而虚拟地址(注意:是一个确定的地址,不是一个空间)被MMU分为2个部分,第一部分是页号索引(page Index),第二部分则是相对该页首地址的偏移量(offset). 。我们还是以刚才那个16位机器结合下图进行一个实例说明,该实例中,虚拟地址8196被送进MMU,MMU把它映射成物理地址。16位的CPU总共能产生的地址范围是0~64K,按每页4K的大小计算,该空间必须被分成16个页。而我们的虚拟地址第一部分所能够表达的范围也必须等于16(这样才能索引到该页组中的每一个页),也就是说这个部分至少需要4个bit。一个页的大小是4K(4096),也就是说偏移部分必须使用12个bit来表示(2^12=4096,这样才能访问到一个页中的所有地址),8196的二进制码如下图所示:

      该地址的页号索引为0010(二进制码),既索引的页为页2,第二部分为000000000100(二进制),偏移量为4。页2中的页框号为6(页2映射在页框6,见上图),我们看到页框6的物理地址是24~28K。于是MMU计算出虚拟地址8196应该被映射成物理地址24580(页框首地址+偏移量=24576+4=24580)。同样的,若我们对虚拟地址1026进行读取,1026的二进制码为0000010000000010,page index="0000"=0,offset=010000000010=1026。页号为0,该页映射的页框号为2,页框2的物理地址范围是8192~12287,故MMU将虚拟地址1026映射为物理地址9218(页框首地址+偏移量=8192+1026=9218)。以上就是MMU的工作过程。

三、S3C24XX的MMU工作过程


    下面我们针对s3c2410的MMU(注1)进行讲解。
    S3c2410总共有4种内存映射方式,分别是:
    1.Fault (无映射)
    2.Coarse Page (粗表)
    3.Section (段)
    4.Fine Page (细表)
    我们以Section(段)进行说明。
    ARM920T是一个32bit的CPU,它的虚拟地址空间为2^32=4G。而在Section模式,这4G的虚拟空间被分成一个一个称为段(Section)的单位(与我们上面讲的页在本质上其实是一致的),每个段的长度是1M (而我们之前所使用的页的长度是4K)。4G的虚拟内存总共可以被分成4096个段(1M*4096=4G),因此我们必须用4096个描述符来对这组段进行描述,每个描述符占用4个Byte,故这组描述符的大小为16KB (4byte*4096),这4096个描述符构为一个表格,我们称其为Tralaton Table.

    上图是描述符的结构
    Section base address:段基地址(相当于页框号首地址)
    AP: 访问控制位Access Permission
    Domain: 访问控制寄存器的索引。Domain与AP配合使用,对访问权限进行检查
    C:当C被置1时为write-through (WT)模式
    B: 当B被置1时为write-back (WB)模式(C,B两个位在同一时刻只能有一个被置1)
    下面是s3c2410内存映射后的一个示意图:

      我的s3c2410上配置的SDRSAM大小为64M,该SDRAM的物理地址范围是0x3000 0000~0x33FF FFFF(属于Bank 6),由于1个Section的大小是1M,所以该物理空间可以被分成64个物理段(页框).


      在Section模式下,送进MMU的虚拟地址(注1)被分为两部分(这点和我们上面举的例子是一样的),这两部分为 Descriptor Index(相当于上面例子的Page Index)和 Offset,descript index长度为12bit(2^12=4096,从这个关系式你能看出什么?:) ),Offset长度为20bit(2^20=1M,你又能看出什么?:)).观察一下一个描述符(Descriptor)中的Section Base Address部分,它长度为12 bit,里面的值是该虚拟段(页)映射成的物理段(页框)的物理地址前12bit,由于每一个物理段的长度都是1M,所以物理段首地址的后20bit总是为0x00000(每个Section都是以1M对齐),确定一个物理地址的方法是 物理页框基地址+虚拟地址中的偏移部分=Section Base Address<<20+Offset ,呵呵,可能你有点糊涂了,还是举一个实际例子说明吧。

      假设现在执行指令MOV REG, 0x30000012,虚拟地址的二进制码为00110000 00000000 00000000 00010010,前12位是Descriptor Index= 00110000 0000=768,故在Translation Table里面找到第768号描述符,该描述的Section Base Address="0x0300",也就是说描述符所描述的虚拟段(页)所映射的物理段(页框)的首地址为0x3000 0000(物理段(页框)的基地址=Section Base Address左移20bit=0x0300<<20=0x3000 0000),而Offset=000000 00000000 00010010=0x12,故虚拟地址0x30000012映射成的物理地址=0x3000 0000+0x12=0x3000 0012(物理页框基地址+虚拟地址中的偏移)。你可能会问怎么这个虚拟地址和映射后的物理地址一样?这是由我们定义的映射规则所决定的。在这个例子中我们定义的映射规则是把虚拟地址映射成和他相等的物理地址。我们这样书写映射关系的代码:
void mem_mapping_linear(void)
{
    unsigned long descriptor_index, section_base, sdram_base, sdram_size;
    sdram_base=0x30000000;
    sdram_size=0x 4000000;
    for (section _base= sdram_base,descriptor_index = section _base>>20;
         section _base < sdram_base+ sdram_size;
         descriptor_index+=1;section _base +=0x100000)
    {
         *(mmu_tlb_base + (descriptor_index)) = (section _base>>20) | MMU_OTHER_SECDESC;
    }
}


      上面的这段段代码把虚拟空间0x3000 0000~0x33FF FFFF映射到物理空间0x3000 0000~0x33FF FFFF,由于虚拟空间与物理空间空间相吻合,所以虚拟地址与他们各自对应的物理地址在值上是一致的。当初始完Translation Table之后,记得要把Translation Table的首地址(第0号描述符的地址)加载进协处理器CP15的Control Register2(2号控制寄存器)中,该控制寄存器的名称叫做Translation table base (TTB) register。


      以上讨论的是descriptor中的Section Base Address以及虚拟地址和物理地址的映射关系,然而MMU还有一个重要的功能,那就是访问控制机制(Access Permission )。简单说访问控制机制就是CPU通过某种方法判断当前程序对内存的访问是否合法(是否有权限对该内存进行访问),如果当前的程序并没有权限对即将访问的内存区域进行操作,则CPU将引发一个异常,s3c2410称该异常为Permission fault,x86架构则把这种异常称之为通用保护异常(General Protection),什么情况会引起Permission fault呢?比如处于User级别的程序要对一个System级别的内存区域进行写操作,这种操作是越权的,应该引起一个Permission fault,搞过x86架构的朋友应该听过保护模式(Protection Mode),保护模式就是基于这种思想进行工作的,于是我们也可以这么说:s3c2410的访问控制机制其实就是一种保护机制。那s3c2410的访问控制机制到底是由什么元素去参与完成的呢?它们间是怎么协调工作的呢?这些元素总共有:
    1.协处理器CP15中Control Register3:DOMAIN ACCESS CONTROL REGISTER
    2.段描述符中的AP位和Domain位
    3.协处理器CP15中Control Register1(控制寄存器1)中的S bit和R bit
    4.协处理器CP15中Control Register5(控制寄存器5)
    5.协处理器CP15中Control Register6(控制寄存器6)
    DOMAIN ACCESS CONTROL REGISTER 是访问控制寄存器,该寄存器有效位为32,被分成16个区域,每个区域由两个位组成,他们说明了当前内存的访问权限检查的级别,如下图所示:


      每区域可以填写的值有4个,分别为00,01,10,11(二进制),他们的意义如下所示:


    00:当前级别下,该内存区域不允许被访问,任何的访问都会引起一个domain fault
    01:当前级别下,该内存区域的访问必须配合该内存区域的段描述符中AP位进行权检查
    10:保留状态(我们最好不要填写该值,以免引起不能确定的问题)
    11:当前级别下,对该内存区域的访问都不进行权限检查。
      我们再来看看discriptor中的Domain区域,该区域总共有4个bit,里面的值是对DOMAIN ACCESS CONTROL REGISTER中16个区域的索引.而AP位配合S bit和A bit对当前描述符描述的内存区域被访问权限的说明,他们的配合关系如下图所示:


    AP位也是有四个值,我结合实例对其进行说明.
    在下面的例子中,我们的DOMAIN ACCESS CONTROL REGISTER都被初始化成0xFFFF BDCF,如下图所示:


例1:
Discriptor 中的domain=4,AP=10(这种情况下S bit ,A bit 被忽略)
假设现在我要对该描述符描述的内存区域进行访问:
由于domain=4,而DOMAIN ACCESS CONTROL REGISTER中field 4的值是01,系统会对该访问进行访问权限的检查。
假设当前CPU处于Supervisor模式下,则程序可以对该描述符描述的内存区域进行读写操作。
假设当前CPU处于User模式下,则程序可以对该描述符描述的内存进行读访问,若对其进行写操作则引起一个permission fault.

例2:
Discriptor 中的domain=0,AP=10(这种情况下S bit ,A bit 被忽略)
domain=0,而DOMAIN ACCESS CONTROL REGISTER中field 0的值是11,系统对任何内存区域的访问都不进行访问权限的检查。
由于统对任何内存区域的访问都不进行访问权限的检查,所以无论CPU处于合种模式下(Supervisor模式或是User模式),程序对该描述符描述的内存都可以顺利地进行读写操作

例3:Discriptor 中的domain=4,AP=11(这种情况下S bit ,A bit 被忽略)
由于domain=4,而DOMAIN ACCESS CONTROL REGISTER中field 4的值是01,系统会对该访问进行访问权限的检查。
由于AP=11,所以无论CPU处于合种模式下(Supervisor模式或是User模式),程序对该描述符描述的内存都可以顺利地进行读写操作

例4:
Discriptor 中的domain=4,AP=00, S bit="0",A bit="0"
由于domain=4,而DOMAIN ACCESS CONTROL REGISTER中field 4的值是01,系统会对该访问进行访问权限的检查。
由于AP=00,S bit="0",A bit="0",所以无论CPU处于合种模式下(Supervisor模式或是User模式),程序对该描述符描述的内存都只能进行读操作,否则引起permission fault.
通过以上4个例子我们得出两个结论:
1.对某个内存区域的访问是否需要进行权限检查是由该内存区域的描述符中的Domain域决定的。
2.某个内存区域的访问权限是由该内存区域的描述符中的AP位和协处理器CP15中Control Register1(控制寄存器1)中的S bit和R bit所决定的。

Arm MHU寄存器架构与中断控制详解 寄存器是处理器与外围设备交互的核心接口,其设计直接影响系统性能和可靠性。Arm架构中的Message Handling Unit(MHU)通过精心设计的寄存器组实现高效的处理器间通信,采用32位统一编址和标准访问模式(RW/RO/WO)。在中断控制方面,MHU提供多级状态监控和精细使能配置,支持124个独立通道的中断管理。通过分析MHUv2的寄存器布局和同步机制,开发者可以优化多核系统中的数据传输延迟,实测显示合理配置可使吞吐量提升38%。该技术广泛应用于需要高实时性的异构计算场景,如自动驾驶域控制器和5G 阅读详情

相关推荐

Trusted Firmware-A (TF-A) 项目维护团队

Trusted Firmware-A (TF-A)是一个开源的ARM架构安全固件项目,采用开放治理模式,由全球开发者社区维护。项目每6个月发布一个新版本,包含代码冻结、测试和正式发布等阶段。核心维护团队负责代码审查和技术决策,模块化分工覆盖架构、安全服务、驱动和框架等。TF-A采用三段式版本号(主.次.补丁),并提供长期支持版本。项目制定了明确的接口废弃策略,确保向后兼容性。开发者可通过GitHub参与贡献,重点关注废弃接口的迁移计划。

Linux内核架构师 646

ARM MMU工作原理剖析

嵌入式Linux我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。 •共享资源,欢迎转载:http://hbhuanggang.cublog.cn 一、MMU的产生

zhongnanjun_3的专栏 1748

深入ARM-MCU开发:从基础到实践

ARM-MCU,即ARM微控制器,是基于ARM架构的微控制器单元。它们通常用于各种嵌入式系统中,这类系统需要在小尺寸、低功耗和高性能之间实现平衡。ARM-MCU集成了处理器核心、存储器和其他外设接口,能高效运行操作系统,支持丰富的网络和安全功能,是现代智能设备的核心部件。Cortex-M系列是ARM公司专门为微控制器设计的处理器核心,它包括Cortex-M0、Cortex-M1、Cortex-M3、Cortex-M4、Cortex-M7和最新的Cortex-M55等型号。

weixin_35973118的博客 1755

Linux Kernel编程 --- Mailbox驱动框架

(1)driver 通过mbox_controller_register 注册controller;(2)client 通过mbox_request_channel调用driver->startup;(3)client 通过mbox_send_message调用driver->send_data,并等待txdone;(4)driver 收到remote的中断读取数据调用mbox_chan_received_data将数据放到 client->rx_buffer中;

kunkliu的博客 5212

ARM底层如何实现Message(消息模式)

BOOL GetMsg(MSG_ID MsgId) { //    UINT16 imask_tmp;          if (Msg[MsgId / 32] & (1     {      //        GLOBAL_INTERRUPT_DISABLE;         IntMasterDisable();         Msg[MsgId / 32] &=

Norton的专栏 1011

核间通信--Mailbox原理及内核驱动框架

核间通信--Mailbox原理及内核驱动框架

qq_41483419的博客 1万+

深入了解Linux内核MMU管理机制

MMU:存储器管理单元虚拟内存空间到物理存储空间的映射。在ARM中采用了页式虚拟内存管理。它把虚拟地址空间分为一个个大小固定的块,每一块称为一页,把物理内存的地址空间也分成同样大小的页。页的大小可以分为粗粒度和细粒度两种,MMU就要实现从虚拟地址到物理地址的转换。*存储器访问权限的控制。*设置虚拟存储空间的缓冲特性。

m0_74282605的博客 1405

探秘 ARM 嵌入式设计:从基础到实战

在确定处理器后,进行电路原理图设计,包括电源电路、时钟电路、存储器电路、外设接口电路等。驱动程序开发是为了使硬件设备能够被操作系统识别和控制,针对不同的外设,如传感器、通信模块等,编写相应的驱动程序,实现设备的初始化、数据读写等功能。随着物联网、人工智能、5G 等技术的不断发展,ARM 嵌入式系统的应用场景将更加广泛,对其性能、功耗、智能化程度也提出了更高的要求。外设接口是 ARM 嵌入式系统与外部设备进行通信和交互的桥梁,通过各种接口,系统可以连接传感器、显示器、通信模块等外部设备,实现丰富的功能。

pinkeui的博客 474

ARM存储:存储管理单元MMU

在创建多任务嵌入式系统时,最好有一个简单的方式来编写、装载及运行各自独立的任务。目前大多数的嵌入式系统不再使用自己定制的控制系统,而使用操作系统来简化这个过程。较高级的操作系统采用基于硬件的存储管理单元MMU来实现上述操作。 MMU提供的一个关键服务是使各个任务作为各自独立的程序在其自己的私有存储空间中运行。在带MMU的操作系统控制下,运行的任务无须知道其他与之无关的任务的存储需求情况,这就简化...

huofengfeihu的博客 3052

ARM SCP-firmware 代码解析

目录 1.scp 代码结构 1.1scp 目录结构 2 scp module 2.1.所有module信息的维护 2.2.module功能的提供 2.2.1 module的绑定 3.scp 应用初始化及boot流程 3.1 scp 应用初始化流程 3.2 scp boot 3.2.1ARM Trusted Firmware 3.2.2 scp firmware boot 流程 4.scmi 消息接收、处理、执行流程 4.1 agent到platform的消息流程 4.2 s...

9516

Cache与MMU的爱恨纠缠

cache与MMU之间的关系

Setul 3651

ARM-LINUXMMU

一.虚拟地址与物理地址 操作系统如LINUX采用虚拟内存管理机制,需要内存管理单元(MMU)的支持;arm920t内部由内核、高速缓存、MMU组成,协处理器CP15专门用于MMU与cache。 cpu发出虚拟地址(VA),MMU抓取VA转成物理地址(PA): 二.虚拟内存管理的好处 1.让每个进程都有独立的地址空间,方便程序编写。 2.方便内存的分配与释放。 3.使各进程使用的

wangzibigan的博客 537

深入ARM体系架构与编程实践

ARM,即Advanced RISC Machines,是一家领先的半导体知识产权提供商。其架构起源于1980年代末期,最初由Acorn Computers、Apple Computer和VLSI Technology合作开发。ARM处理器以其高效能和低能耗特性闻名,成为移动通信和嵌入式应用的首选。

weixin_42518874的博客 1127
上一篇: ARM编程进阶之一 —— ARM汇编伪指令
下一篇: android常用命令
sfrysh
博客等级 码龄17年 358粉丝 37原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值