S3C2410 bootloader vivi阅读笔记

导读:
  建议读一读《嵌入式系统Boot Loader技术内幕》(詹荣开著),google一下就会找到一片。什么是Bootloader就不再这里废话了,看看上面的文章就明了了。 Bootloader有很多种,如本文将要阅读的vivi,除此之外还有uboot,redboot,lilo等等。Vivi 是韩国mizi公司专门为三星s3c2410芯片设计的Bootloader。
  先来看看vivi的源码树:
  vivi-+-arch-+-s3c2410
  |-Documentation
  |-drivers-+-serial
  | ‘-mtd-+-maps
  | |-nor
  | ‘-nand
  |-include-+-platform
  | |-mtd
  | ‘-proc
  |-init
  |-lib-+-priv_data
  |-scripts-+-lxdialog
  |-test
  |-util
  可以google一下,搜到源码vivi.tar.gz。
  前面提到的文件已经系统的分析了bootloader的,这里就按源代码来具体说事。vivi也可以分为2个阶段,阶段1的代码在arch/s3c2410/head.S中,阶段2的代码从init/main.c的main函数开始。
  阶段1
  阶段1从程序arch/s3c2410/head.S开始,按照head.S的代码执行顺序,一次完成了下面几个任务:
  1、关WATCH DOG (disable watch dog timer)
  上电后,WATCH DOG默认是开着的
  2、禁止所有中断 (disable all interrupts)
  vivi中不会用到中断,中断是系统的事,bootloader可不能去干这事的(不过这段代码实在多余,上电后中断默认是关闭的)
  3、初始化系统时钟(initialise system clocks)
  启动MPLL,FCLK=200MHz,HCLK=100MHz,PCLK=50MHz,“CPU bus mode”改为“Asynchronous bus mode”。
  4、初始化内存控制寄存器(memsetup)
  S3c2410共有15个寄存器,在此开始初始化13个寄存器。
  5、检查是否从掉电模式唤醒(Check if this is a wake-up from sleep)
  若是,则调用WakeupStart函数进行处理。
  6、点亮所有LED (All LED on)
  点一下灯,通知外面的同志,告诉他们有情况发生。
  7、初始化UART0 (set GPIO for UART &InitUART)
  a.设置GPIO,选择UART0使用的引脚
  b.初始化UART0,设置工作方式(使用FIFO)、波特率115200 8N1、无流控等。这可是使用串口与s3c2410通信的条件啊,在终端也要如此设置。
  8、跳到内存测试函数(simple memory test to find some DRAM flaults)
  当然要定义了CONFIG_BOOTUP_MEMTEST这个参数才会跳到内存测试。
  9、如果定义了以Nand flash方式启动(#ifdef CONFIG_S3C2410_NAND_BOOT),则此时要将vivi所有代码(包括阶段1和阶段2)从Nand flash复制到SDRAM中(因为在Nand flash中是不能执行程序的,它只能做为程序和数据的存储器,而Nor flash可就不同了,Nor flash可以执行程序,但贵是它发展得瓶颈):
  a.设置nand flash控制寄存器
  b.设置堆栈指针
  c.设置即将调用的函数nand_read_ll的参数:r0=目的地址(SDRAM的地址),r1=源地址(nand flash的地址),r2=复制的长度(以字节为单位)
  d.调用nand_read_ll进行复制
  10、跳到bootloader的阶段2运行,亦即调用init/main.c中的main函数(get read to call C functions)
  a.重新设置堆栈
  b.设置main函数的参数
  c.调用main函数
  head.S有900多行,都是些arm汇编,看的云山雾罩,汇编看来是忘的差不多了,所以这部分代码也看的相当糙,只知道大概在干什么,至于个中缘由就不是很了解。先学学arm汇编再回来看。
  阶段2
  从init/main.c中的main函数开始,终于步入C语言的世界了。Main函数总共有8步(8 steps),先看看源代码:
  int main(int argc, char *argv[])
  {
  int ret;
  /*
  * Step 1:
  */
  putstr("/r/n");
  putstr(vivi_banner); //vivi_banner是vivi执行开始的显示信息,vivi_banner在文件version.c中定义
  reset_handler();
  /*
  * Step 2:
  */
  ret = board_init();
  if (ret) {
  putstr("Failed a board_init() procedure/r/n");
  error();
  }
  /*
  * Step 3:
  */
  mem_map_init();
  mmu_init();
  putstr("Succeed memory mapping./r/n");
  /*
  * Now, vivi is running on the ram. MMU is enabled.
  * Step 4:
  */
  /* initialize the heap area*/
  ret = heap_init();
  if (ret) {
  putstr("Failed initailizing heap region/r/n");
  error();
  }
  /* Step 5:
  * MTD
  */
  ret = mtd_dev_init();
  /* Step 6:
  */
  init_priv_data();
  /* Step 7:
  */
  misc();
  init_builtin_cmds();
  /* Step 8:
  */
  boot_or_vivi();
  return 0;
  }
  下面按照上面的步骤逐步来分析一下。
  1、Step 1:reset_handler()
  reset_handler用于将内存清零,代码在lib/reset_handle.c中。
  1 void
  2 reset_handler(void)
  3 {
  4 int pressed;
  5 pressed = is_pressed_pw_btn(); /*判断是硬件复位还是软件复位*/
  6 if (pressed == PWBT_PRESS_LEVEL) {
  7 DPRINTK("HARD RESET/r/n");
  8 hard_reset_handle(); /*调用clear_mem对SDRAM清0*/
  9 } else {
  10 DPRINTK("SOFT RESET/r/n");
  11 soft_reset_handle(); /*此函数为空*/
  12 }
  13 }
  在上电后,reset_handler调用第8行的hard_reset_handle(),此函数在lib/reset_handle.c中:
  [main(int argc, char *argv[]) -> reset_handler() -> hard_reset_handle()]
  1 static void
  2 hard_reset_handle(void)
  3 {
  4 #if 0
  5 clear_mem((unsigned long)(DRAM_BASE + VIVI_RAM_ABS_POS), /
  6 (unsigned long)(DRAM_SIZE - VIVI_RAM_ABS_POS));
  7 #endif
  /*lib/memory.c,将起始地址为USER_RAM_BASE,长度为USER_RAM_SIZE的内存清0*/
  8 clear_mem((unsigned long)USER_RAM_BASE, (unsigned long) USER_RAM_SIZE);
  9 }
  先写到这儿吧。
  (未完待续)
  S3C2410 bootloader ----VIVI阅读笔记2(续笔记1)
  2、Step 2:board_init()
  board_init调用2个函数用于初始化定时器和设置各GPIO引脚功能,代码在arch/s3c2410/smdk.c中:
  [main(int argc, char *argv[]) >board_init()]
  1 int board_init(void)
  2 {
  3 init_time(); /*arch/s3c2410/proc.c*/
  4 set_gpios(); /*arch/s3c2410/smdk.c */
  5 return 0;
  6 }
  init_time() 这个函数对寄存器进行了简单的操作:
  void init_time(void)
  {
  TCFG0 = (TCFG0_DZONE(0) | TCFG0_PRE1(15) | TCFG0_PRE0(0));
  /*s3c2410 data sheet P298*/
  /*TCFG0 = 0 | 0xf00 | 0 */
  }
  寄存器TCFG0由三部分组成,prescaler0,prescaler1,deadzone和reserve四部分,前三部分分别对应 TCFG0_PRE0、TCFG0_PRE1、TCFG0_DZONE,TCFG0_PRE0(0)实际值为0x00,TCFG0_PRE1(15)实际值为0x0f00,而TCFG0_DZONE(0)实际值为 0x000000。实际中,vivi并未使用定时器,这个函数就可以忽略。set_gpios()用于选择GPA至GPH端口各引脚的功能及是否使用各引脚的内部上拉电阻,并设置外部中断源寄存器EXTINT0-2(vivi中未使用外部中断)。
  1 void set_gpios(void)
  2 {
  3 GPACON = vGPACON;
  4 GPBCON = vGPBCON;
  5 GPBUP = vGPBUP;
  6 GPCCON = vGPCCON;
  7 GPCUP = vGPCUP;
  8 GPDCON = vGPDCON;
  9 GPDUP = vGPDUP;
  10 GPECON = vGPECON;
  11 GPEUP = vGPEUP;
  12 GPFCON = vGPFCON;
  13 GPFUP = vGPFUP;
  14 GPGCON = vGPGCON;
  15 GPGUP = vGPGUP;
  16 GPHCON = vGPHCON;
  17 GPHUP = vGPHUP;
  18 EXTINT0 = vEXTINT0;
  19 EXTINT1 = vEXTINT1;
  20 EXTINT2 = vEXTINT2;
  21 }
  以第三行为例,vGPACON的值为0x007fffff,查找s3c2410用户手册可知,该参数将GPACON的23位全部置1。各位功能需察看s3c2410用户手册
  3、Step 3:建立页表和启动MMU
  mem_map_init();
  mmu_init();
  mem_map_init函数用于建立页表,vivi使用段式页表,只需要一级页表。它调用3个函数,代码在arch/s3c2410/mmu.c中:
  [main(int argc, char *argv[]) >mem_map_init(void)]
  1 void mem_map_init(void)
  2 {
  3 #ifdef CONFIG_S3C2410_NAND_BOOT
  /*CONFIG_S3C2410_NAND_BOOT = y ,在文件include/autoconf.h中定义*/
  4 mem_map_nand_boot();
  /* 最终调用mem_mepping_linear, 建立页表 */
  5 #else
  6 mem_map_nor();
  7 #endif
  8 cache_clean_invalidate();/* 清空cache,使无效cache */
  9 tlb_invalidate(); /* 使无效快表TLB */
  10 }
  第9、10行的两个函数可以不用管它,他们做的事情在下面的mmu_init函数里又重复了一遍。对于本开发板,在.config中定义了 CONFIG_S3C2410_NAND_BOOT。mem_map_nand_boot()函数调用mem_mapping_linear()函数来最终完成建立页表的工作。页表存放在SDRAM物理地址0x33dfc000开始处,共16K:一个页表项4字节,共有4096个页表项;每个页表项对应 1M地址空间,共4G。mem_map_init先将4G虚拟地址映射到相同的物理地址上,NCNB(不使用cache,不使用write buffer)——这样,对寄存器的操作跟未启动MMU时是一样的;再将SDRAM对应的64M空间的页表项修改为使用cache。 mem_mapping_linear函数的代码在arch/s3c2410/mmu.c中:
  [main(int argc, char *argv[]) >mem_map_init(void) >mem_map_nand_boot( ) >mem_mapping_linear(void)]
  1 static inline void mem_mapping_linear(void)
  2 {
  3 unsigned long pageoffset, sectionNumber;
  4 putstr_hex("MMU table base address = 0x", (unsigned long)
  mmu_tlb_base);
  5 /* 4G 虚拟地址映射到相同的物理地址. not cacacheable, not bufferable */
  6 /* mmu_tlb_base = 0x33dfc000*/
  7 for (sectionNumber = 0; sectionNumber <4096; sectionNumber++) {
  8 pageoffset = (sectionNumber <<20);
  9 *(mmu_tlb_base + (pageoffset >>20)) = pageoffset |
  MMU_SECDESC;
  10 }
  11 /* make dram cacheable */
  12 /* SDRAM物理地址0x3000000-0x33ffffff,
  13 DRAM_BASE=0x30000000,DRAM_SIZE=64M
  14 */
  15 for (pageoffset = DRAM_BASE; pageoffset <(DRAM_BASE+DRAM_SIZE); /
  16 pageoffset += SZ_1M) {
  17 //DPRINTK(3, "Make DRAM section cacheable: 0x%08lx/n", pageoffset);
  18 *(mmu_tlb_base + (pageoffset >>20)) = /
  pageoffset | MMU_SECDESC | MMU_CACHEABLE;
  19 }
  20 }
  mmu_init()函数用于启动MMU,它直接调用arm920_setup()函数。arm920_setup()的代码在arch/s3c2410/mmu.c中:
  [main(int argc, char *argv[]) >mmu_init( ) >arm920_setup( )]
  1 static inline void arm920_setup(void)
  2 {
  3 unsigned long ttb = MMU_TABLE_BASE;
  /* MMU_TABLE_BASE = 0x33dfc000 */
  4 __asm__(
  5 /* Invalidate caches */
  6 "mov r0, #0/n"
  7 "mcr p15, 0, r0, c7, c7, 0/n" /* invalidate I,D caches on v4 */
  8 "mcr p15, 0, r0, c7, c10, 4/n" /* drain write buffer on v4 */
  9 "mcr p15, 0, r0, c8, c7, 0/n" /* invalidate I,D TLBs on v4 */
  10 /* Load page table pointer */
  11 "mov r4, %0/n"
  12 "mcr p15, 0, r4, c2, c0, 0/n" /* load page table pointer */
  13 /* Write domain id (cp15_r3) */
  14 "mvn r0, #0/n" /* Domains 0b01 = client, 0b11=Manager*/
  15 "mcr p15, 0, r0, c3, c0, 0/n"
  /* load domain access register,write domain 15:0, 用户手册P548(access permissions)*/
  16 /* Set control register v4 */
  17 "mrc p15, 0, r0, c1, c0, 0/n" /* get control register v4 */
  /*数据手册P545:read control register */
  18 /* Clear out 'unwanted' bits (then put them in if we need them) */
  19 /* ..VI ..RS B... .CAM */ /*这些位的含义在数据手册P546*/
  20 "bic r0, r0, #0x3000/n" /* ..11 .... .... .... */
  /*I(bit[12])=0 = Instruction cache disabled*/
  21 /*V[bit[13]](Base location of exception registers)=0 = Low addresses = 0x0000 0000*/
  22 "bic r0, r0, #0x0300/n" /* .... ..11 .... .... */
  23 /*R(ROM protection bit[9])=0*/
  /*S(System protection bit[8])=0*/
  /*由于TTB中AP=0b11(line141),所以RS位不使用(P579)*/
  24 "bic r0, r0, #0x0087/n" /* 0x0000000010000111 */
  /*M(bit[0])=0 = MMU disabled*/
  /*A(bit[1])=0 =Data address alignment fault checking disable*/
  /*C(bit[2])=0 = Data cache disabled*/
  /*B(bit[7])=0= Little-endian operation*/
  25 /* Turn on what we want */
  26 /* Fault checking enabled */
  27 "orr r0, r0, #0x0002/n" /* .... .... .... ..10 */
  /*A(bit[1])=1 = Data address alignment fault checking enable*/
  28 #ifdef CONFIG_CPU_D_CACHE_ON /*is not set*/
  29 "orr r0, r0, #0x0004/n" /* .... .... .... .100 */
  /*C(bit[2])=1 = Data cache enabled*/
  30 #endif
  31 #ifdef CONFIG_CPU_I_CACHE_ON /*is not set*/
  32 "orr r0, r0, #0x1000/n" /* ...1 .... .... .... */
  /*I(bit[12])=1 = Instruction cache enabled*/
  33 #endif
  34 /* MMU enabled */
  35 "orr r0, r0, #0x0001/n" /* .... .... .... ...1 */
  /*M(bit[0])=1 = MMU enabled*/
  36 "mcr p15, 0, r0, c1, c0, 0/n" /* write control register */
  /*数据手册P545*/
  37 : /* no outputs */
  38 : "r" (ttb) );
  39 }
  [未完]
  S3C2410 bootloader ----VIVI阅读笔记3
  4、Step 4:heap_init()
  第4步调用了heap_init(void)函数,并返回值。该值是函数heap_init()调用的mmalloc_init()函数的返回值。其实,这步就是申请一块内存区域。
  [lib/heap.c->heap_init(void)]
  1 int heap_init(void)
  2 {
  3 return mmalloc_init((unsigned char *)(HEAP_BASE), HEAP_SIZE);
  4 }
  内存动态分配函数mmalloc就是从heap(堆)中划出一块空闲内存。相应的mfree函数则将动态分配的某块内存释放回heap中。
  heap_init函数在SDRAM中指定了一块1M大小的内存作为heap(起始地址HEAP_BASE = 0x33e00000),并在heap的开头定义了一个数据结构blockhead。事实上,heap就是使用一系列的blockhead数据结构来描述和操作的。每个blockhead数据结构对应着一块heap内存,假设一个blockhead数据结构的存放位置为A,则它对应的可分配内存地址为“A + sizeof(blockhead)”到“A + sizeof(blockhead) + size - 1”。blockhead数据结构在lib/heap.c中定义:
  1 typedef struct blockhead_t {
  2 int32 signature; //固定为BLOCKHEAD_SIGNATURE
  3 bool allocated; //此区域是否已经分配出去:0-N,1-Y
  4 unsigned long size; //此区域大小
  5 struct blockhead_t *next; //链表指针
  6 struct blockhead_t *prev; //链表指针
  7 } blockhead;
  现在来看看heap是如何运作的(如果您不关心heap实现的细节,这段可以跳过)。vivi对heap的操作比较简单,vivi中有一个全局变量 static blockhead *gHeapBase,它是heap的链表头指针,通过它可以遍历所有blockhead数据结构。假设需要动态申请一块sizeA大小的内存,则 mmalloc函数从gHeapBase开始搜索blockhead数据结构,如果发现某个blockhead满足:
  (1) allocated = 0 //表示未分配
  (2) size >sizeA,则找到了合适的blockhead,
  满足上述条件后,进行如下操作:
  a.allocated设为1
  b.如果size – sizeA >sizeof(blockhead),则将剩下的内存组织成一个新的blockhead,放入链表中
  c.返回分配的内存的首地址释放内存的操作更简单,直接将要释放的内存对应的blockhead数据结构的allocated设为0即可。
  heap_init函数直接调用mmalloc_init函数进行初始化,此函数代码在lib/heap.c中,比较简单,初始化gHeapBase即可:
  [main(int argc, char *argv[]) >heap_init(void) >mmalloc_init(unsigned char *heap, unsigned long size)]
  1 static inline int mmalloc_init(unsigned char *heap, unsigned long size)
  2 {
  3 if (gHeapBase != NULL) return -1;
  4 DPRINTK("malloc_init(): initialize heap area at 0x%08lx, size = 0x%08lx/n", heap, size);
  5 gHeapBase = (blockhead *)(heap);
  6 gHeapBase->allocated=FALSE;
  7 gHeapBase->signature=BLOCKHEAD_SIGNATURE;
  8 gHeapBase->next=NULL;
  9 gHeapBase->prev=NULL;
  10 gHeapBase->size = size - sizeof(blockhead);
  11 return 0;
  12 }
  static blockhead *gHeapBase = NULL; 这个就是上面称赞的全局变量了,定义在lib/heap.c中。上面就是个链表操作,数据结构,看来搞这个也得好好学数据结构啊,不然内存搞的溢出、浪费可就哭都来不及了。
  5、Step 5:mtd_dev_init()
  所谓MTD(Memory Technology Device)相关的技术。在linux系统中,我们通常会用到不同的存储设备,特别是FLASH设备。为了在使用新的存储设备时,我们能更简便地提供它的驱动程序,在上层应用和硬件驱动的中间,抽象出MTD设备层。驱动层不必关心存储的数据格式如何,比如是FAT32、ETX2还是FFS2或其它。它仅仅提供一些简单的接口,比如读写、擦除及查询。如何组织数据,则是上层应用的事情。MTD层将驱动层提供的函数封装起来,向上层提供统一的接口。这样,上层即可专注于文件系统的实现,而不必关心存储设备的具体操作。这段乱七八糟的话也许比较让人晕,也可以这样理解在设备驱动(此处指存储设备)和上层应用之间还存在着一层,共三层,这个中间层就是MTD技术的产物。通常可以将它视为驱动的一部分,叫做上层驱动,而那些实现设备的读、写操作的驱动称为下层驱动,上层驱动将下层驱动封装,并且留给其上层应用一些更加容易简单的接口。
  在我们即将看到的代码中,使用mtd_info数据结构表示一个MTD设备,使用nand_chip数据结构表示一个nand flash芯片。在mtd_info结构中,对nand_flash结构作了封装,向上层提供统一的接口。比如,它根据nand_flash提供的 read_data(读一个字节)、read_addr(发送要读的扇区的地址)等函数,构造了一个通用的读函数read,将此函数的指针作为自己的一个成员。而上层要读写flash时,执行mtd_info中的read、write函数即可。
  mtd_dev_init()用来扫描所使用的NAND Flash的型号,构造MTD设备,即构造一个mtd_info的数据结构。对于S3C2410来说,它直接调用mtd_init(),mtd_init 又调用smc_init(),此函数在drivers/mtd/maps/s3c2410_flash.c中:
  [main(int argc,char *argv[])>mtd_dev_init()>mtd_init()]
  1 int mtd_init(void)
  2 {
  3 int ret;
  4 #ifdef CONFIG_MTD_CFI /*is not set*/
  5 ret = cfi_init();
  6 #endif
  7 #ifdef CONFIG_MTD_SMC9 /* =y */
  8 ret = smc_init();
  9 #endif
  10 #ifdef CONFIG_S3C2410_AMD_BOOT /*is not set*/
  11 ret = amd_init();
  12 #endif
  13 if (ret) {
  14 mymtd = NULL;
  15 return ret;
  16 }
  17 return 0;
  18 }
  显而易见,该函数应取第二项,这项在autoconf.h中定义了。
  [main(int argc, char *argv[]) >mtd_dev_init() >mtd_init() >smc_init()]
  1 static int
  2 smc_init(void)
  3 {
  /*struct mtd_info *mymtd,数据类型在include/mtd/mtd.h*/
  /*strcut nand_chip在include/mtd/nand.h中定义*/
  4 struct nand_chip *this;
  5 u_int16_t nfconf;
  /* Allocate memory for MTD device structure and private data */
  6 mymtd = mmalloc(sizeof(struct mtd_info) + sizeof(struct nand_chip));
  7 if (!mymtd) {
  8 printk("Unable to allocate S3C2410 NAND MTD device structure./n");
  9 return -ENOMEM;
  10 }
  /* Get pointer to private data */
  11 this = (struct nand_chip *)(&mymtd[1]);
  /* Initialize structures */
  12 memset((char *)mymtd, 0, sizeof(struct mtd_info));
  13 memset((char *)this, 0, sizeof(struct nand_chip));
  /* Link the private data with the MTD structure */
  14 mymtd->priv = this;
  /* set NAND Flash controller */
  15 nfconf = NFCONF;
  /* NAND Flash controller enable */
  16 nfconf |= NFCONF_FCTRL_EN;
  /* Set flash memory timing */
  17 nfconf &= ~NFCONF_TWRPH1; /* 0x0 */
  18 nfconf |= NFCONF_TWRPH0_3; /* 0x3 */
  19 nfconf &= ~NFCONF_TACLS; /* 0x0 */
  20 NFCONF = nfconf;
  /* Set address of NAND IO lines */
  21 this->hwcontrol = smc_hwcontrol;
  22 this->write_cmd = write_cmd;
  23 this->write_addr = write_addr;
  24 this->read_data = read_data;
  25 this->write_data = write_data;
  26 this->wait_for_ready = wait_for_ready;
  /* Chip Enable -> RESET -> Wait for Ready -> Chip Disable */
  27 this->hwcontrol(NAND_CTL_SETNCE);
  28 this->write_cmd(NAND_CMD_RESET);
  29 this->wait_for_ready();
  30 this->hwcontrol(NAND_CTL_CLRNCE);
  31 smc_insert(this);
  32 return 0;
  33 }
  6-14行构造了一个mtd_info结构和nand_flash结构,前者对应MTD设备,后者对应nand flash芯片(如果您用的是其他类型的存储器件,比如nor flash,这里的nand_flash结构应该换为其他类型的数据结构)。MTD设备是具体存储器件的抽象,那么在这些代码中这种关系如何体现呢——第 14行的代码把两者连结在一起了。事实上,mtd_info结构中各成员的实现(比如read、write函数),正是由priv变量所指向的 nand_flash的各类操作函数(比如read_addr、read_data等)来实现的。
  15-20行是初始化S3C2410上的NAND FLASH控制器。前面分配的nand_flash结构还是空的,现在当然就是填满它的各类成员了,这正是21-26行做的事情。27-30行对这块 nand flash作了一下复位操作。最后,也是最复杂的部分,根据刚才填充的nand_flash结构,构造mtd_info结构,这由31行的 smc_insert函数调用smc_scan完成。
  这才是VIVI启动的第5步,还有三步就完成了启动了,同时我的这篇阅读笔记也就OVER了。
  发表于: 2007-08-23,修改于: 2007-08-23 22:42 已浏览258次,有评论0条推荐投诉

本文转自
http://blog.chinaunix.net/u1/40912/showart_366404.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值