移植u-boot-2018.09到OK6410 (2) --- 代码移植

参考上一节的内容,建立了smdk6410的关联路径, 并把和路径关联的几个宏配置OK后,编译命令"make O=build smdk6410_defconfig;make O=build "就能通过了,这表示smdk6410项目框架搭起来了,后续就是要porting各块的代码。
Samsung支持smdk6410的uboot版本是uboot1.1.6 , 也就是飞凌资料包提供的,现在就是要把上面的相关代码移植到新的uboot上。

代码移植

代码Porting的大概次序是:

  • arm平台相关的。比如cpu型号的配置,MMU/cache
  • board初始化相关的。比如DDR,WDT,中断,CLOCK
  • 串口功能的代码。为后续其他功能的移植提供调试手段
  • 其他功能的驱动代码

1. 平台相关的代码

OK6410使用的SOC是内核为ARM1176JZF-S的S3C6410。在arch/arm/Kconfig里面定义config ARCH_S3C64XX一项中,需要选择CPU_ARM1176。uboot里面平台相关的代码都位于arch/目录下, arm1176的平台代码位于arch/arm/cpu/arm11(arm1176)和arch/arm/lib中,后者是arm平台的通用部分,不同arm核亦是相同;前者则存放arm1176核的平台差异代码,一般直接沿用uboot的代码即可。
arm1176平台的入口代码: arch/arm/cpu/arm1176/start.S,以下是只保留了关键部分的代码:

reset:
	/* set the cpu to SVC32 mode
	 */
	mrs	r0, cpsr
	bic	r0, r0, #0x3f
	orr	r0, r0, #0xd3
	msr	cpsr, r0

cpu_init_crit:
	/* flush v4 I/D caches
	 */
	mov	r0, #0
	mcr	p15, 0, r0, c7, c7, 0	/* flush v3/v4 cache */
	mcr	p15, 0, r0, c8, c7, 0	/* flush v4 TLB */

	/* disable MMU stuff and caches
	 */
	mrc	p15, 0, r0, c1, c0, 0
	bic	r0, r0, #0x00002300	@ clear bits 13, 9:8 (--V- --RS)
	bic	r0, r0, #0x00000087	@ clear bits 7, 2:0 (B--- -CAM)
	orr	r0, r0, #0x00000002	@ set bit 1 (A) Align
	orr	r0, r0, #0x00001000	@ set bit 12 (I) I-Cache

	/* Prepare to disable the MMU */
	adr	r2, mmu_disable_phys
	sub	r2, r2, #(CONFIG_SYS_PHY_UBOOT_BASE - CONFIG_SYS_TEXT_BASE)
	b	mmu_disable

	.align 5
	/* Run in a single cache-line */
mmu_disable:
	mcr	p15, 0, r0, c1, c0, 0
	nop
	nop
	mov	pc, r2
mmu_disable_phys:

	/* Go setup Memory and board specific bits prior to relocation.
	 */
	bl	lowlevel_init		/* go setup pll,mux,memory */

	bl	_main

	mov	pc, lr

从注释上可以看出来,这段代码的作用。如果要移植,需要关注两点:
    1)配置宏 CONFIG_SYS_PHY_UBOOT_BASE、CONFIG_SYS_TEXT_BASE。这个值,即为uboot运行时的入口地址
    2)lowlevel_init的实现。这个函数涉及系统时钟以及评估板相关的硬件初始化,将在下一步说下说明。

2. board初始化相关的代码

uboot将评估板启动初始化相关的代码放到了board/目录下,这一点上,不管是旧的版本还是新版本,都没有改变。 可以直接将原uboot1.1.6的board/samsung/smdk6410移植过来,前面提到的lowlevel_init ,就在其下的汇编文件lowlevel_init.S中。移植中,需要注意的是,头文件的引用位置 和 u-boot.lds文件的修改
   1)头文件的存放位置和引用。默认的uboot头文件目录有:
       ./include
       ./arch/arm/include
       ./arch/arm/mach-xxxxx/include/mach
       前两个和平常引用没有分别,需要注意的是最后一个的目录,在编译过程中,会临时创建一个软链接指向该目录,这个链接的路径: ./include/asm/arch ,引用头文件的方式类似:
       #include <asm/arch/uart.h>
   2)LDS文件的修改
      LDS文件定义了uboot编译之后的连接过程,决定了一个可执行程序的各个段的存储位置。新版本由于代码结构和段落属性都有很大的改变,所以再沿用旧LDS文件不是很合适。 
     在顶层Makefile中,定义了LDSCRIPT值的选择顺序。可以看出board/samsung/smdk6410/u-boot.lds是第一优先级,若存在,将被选择。可以将arch/arm/cpu/arm1176/u-boot.lds复制到该位置。若以后要修改LDS文件,可将改动维持在项目相关的目录中,便于维护。

 579 # If there is no specified link script, we look in a number of places for it
 580 ifndef LDSCRIPT
 581     ifeq ($(wildcard $(LDSCRIPT)),)
 582         LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds
 583     endif
 584     ifeq ($(wildcard $(LDSCRIPT)),)
 585         LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot.lds
 586     endif
 587     ifeq ($(wildcard $(LDSCRIPT)),)
 588         LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.lds
 589     endif
 590 endif

3. 串口功能的代码

UART部分 在lowlevel_init.S 中也有初始化代码(uart_asm_init),默认设置uart0的波特率是115200,只是调试不方便,做不到运行时的顺序打印,只能显示最后一个字符。
   在lowlevel_init.S 添加如下汇编代码,这样在早期就可以使用类似C中puts(char *string) 和int数值的十六进制打印功能,这两个函数做了寄存器的栈保存,所以也允许在C代码中被调用
         extern void asm_puts(char *s);
         extern void asm_print_hex(ulong val);

asm_putc:
	ldr	r1, =ELFIN_UART_BASE
1:	ldr   r2, [r1, #UTRSTAT_OFFSET]
	tst   r2, #2
	beq  1b
	str	r0, [r1, #UTXH_OFFSET]
	mov	pc, lr
	
	.globl asm_puts
asm_puts:
	stmfd	sp!, {r1-r3, lr}
	mov 	r3, r0
0:	ldrb 		r0, [r3]
	cmp 	r0,#0
	beq	2f
	cmp 	r0, # '\n'
	bne 1f
	mov 	r0, # '\r'
	bl asm_putc
	mov 	r0, # '\n'
1:
	bl asm_putc
	add  	r3, r3, #1
	b   0b
2:
	ldmfd	sp!, {r1-r3, lr}	
	mov 	pc, lr

	.globl asm_print_hex
asm_print_hex:
	stmfd	sp!, {r1-r4, lr}
	/* mov r5,lr */
	mov r3,r0

	mov r0,#'0'
	bl    asm_putc
	mov r0,#'x'
	bl    asm_putc

	mov r4,#8
1:	mov r3,r3, ror #28
	and r0, r3,#0x0F
	cmp r0,#9
	addls r0,r0,#'0'
	addhi r0,r0,#55
	bl    asm_putc
	subs r4, r4,#1
	bne 1b
	/* mov lr,r5 */
	ldmfd	sp!, {r1-r4, lr}	
	mov pc,lr

4. 其他功能的代码

   在新版本的uboot中,引入了DM(Driver Model)的设备驱动模型。
   下面关于DM的概念介绍, 引用自:https://www.cnblogs.com/wahaha02/p/5987350.html

    uboot设备模型中udevice为核心对象,以树型模型组织(如下),其为dm的顶层结构

单个udevice建模如下,详细对象定义参见《附:核心数据结构》小节。

所有对象可以按udevice或者uclass进行遍历。
DM初始化流程包括:

  • 模型初始化
  • 静态对象初始化
  • 运行态对象初始化
  • 设备组公共初始化
  • 设备初始化

DM初始化的总入口接口dm_init_and_scan(),其主要由以下三块组成:

  • dm_init():创建udevice和uclass空链表,创建根设备(root device)
  • dm_scan_platdata():扫描U_BOOT_DEVICE定义的设备,创建对应的udevice和uclass对象,查找并绑定相应driver,并调用probe流程。
  • dm_scan_fdt():扫描由FDT设备树文件定义的设备,创建对应的udevice和uclass对象,查找并绑定相应driver,并调用probe流程。

 

这样,按DM的驱动模块,就可以添加uart、timer、sdhc、DMA、USB等驱动,具体的添加过程,这里就不在说明,以后视情况再专门介绍。

编译错误

编译uboot过程中, 有几个编译错误需要注意,花了些时间,最后才明白原因,有记录的价值:

编译错误1:lds文件添加指定文件后,引起的重复定义。

  修改u-boot.lds文件, 新增第25行后

 20     .text :
 21     {
 22         *(.__image_copy_start)
 23         *(.vectors)
 24         CPUDIR/start.o  (.text*)
 25         board/samsung/smdk6410/lowlevel_init.o (.text*)
 26     }

  出现如下编译错误:
board/samsung/smdk6410/built-in.o: In function `lowlevel_init':
/home/golden/workspace/uboot/uboot-201809/build/../board/samsung/smdk6410/lowlevel_init.S:46: multiple definition of `lowlevel_init'
board/samsung/smdk6410/lowlevel_init.o:/home/golden/workspace/uboot/uboot-201809/build/../board/samsung/smdk6410/lowlevel_init.S:46: first defined here
board/samsung/smdk6410/built-in.o: In function `test_uart_puthex':
/home/golden/workspace/uboot/uboot-201809/build/../board/samsung/smdk6410/lowlevel_init.S:390: multiple definition of `test_uart_puthex'
board/samsung/smdk6410/lowlevel_init.o:/home/golden/workspace/uboot/uboot-201809/build/../board/samsung/smdk6410/lowlevel_init.S:390: first defined here
board/samsung/smdk6410/built-in.o: In function `asm_puts':
/home/golden/workspace/uboot/uboot-201809/build/../board/samsung/smdk6410/lowlevel_init.S:370: multiple definition of `asm_puts'

   解决方法: Makefile中将obj-y := lowlevel_init.o  改成extra-y := lowlevel_init.o

编译错误2: 提示定义冲突

In file included from ../include/common.h:519:0,
                 from include/asm/arch/cpu.h:35,
                 from ../include/configs/smdk6410.h:18,
                 from include/config.h:5,
                 from ../arch/arm/include/asm/string.h:4,
                 from ../include/linux/string.h:21,
                 from ../include/efi.h:19,
                 from ../arch/arm/lib/reloc_arm_efi.c:14:
../include/net.h: In function ‘net_read_ip’:
../include/net.h:723:2: warning: implicit declaration of function ‘memcpy’ [-Wimplicit-function-declaration]
  memcpy((void *)&ip, (void *)from, sizeof(ip));
  ^~~~~~
In file included from ../include/linux/string.h:21:0,
                 from ../include/efi.h:19,
                 from ../arch/arm/lib/reloc_arm_efi.c:14:
../arch/arm/include/asm/string.h: At top level:
../arch/arm/include/asm/string.h:20:15: error: conflicting types for ‘memcpy’
 extern void * memcpy(void *, const void *, __kernel_size_t);
               ^~~~~~
   原因是:定义声明的头文件string.h首先被包含,然后逐层向下引用头文件。
        而include/asm/arch/cpu.h 包含了的common.h,里面重复调用了string.h ,
        由于string.h已被引用过,所以这时不会展开实际内容,而前一次的引用,这时还没展开,
        导致common.h中引用net.h时,调用memcpy找不到它的外部声明。 
   解决办法: 去掉include/asm/arch/cpu.h的 common.h引用语句

SPL部分 

与旧版本相比,新版本的uboot引入了u-boot-spl。编译结果或者过程中, 除了u-boot.bin的生成,还会有u-boot-spl.bin。开始时,我不是很明白这个东西和u-boot.bin的差别, 后来百度后,才明白它的含义。
  SPL(Secondary Program Loader)指第二阶段程序加载器,这里所谓的第二阶段是相对于SOC中的BROM来说的。u-boot-spl相当于轻量级的bootloader,它的作用就是加载u-boot.bin, 是不是很疑惑这种设计? 如果结合SoC的启动过程,那就很好理解了。现在大多数SoC的存储介质是不支持片上引导的(Nandflash,eMMC等),所以需要加载到RAM中,才能运行,但在SoC上电启动后,外部的RAM或DDR在此时还未初始化,无法使用,这时SoC的IRAM(Internal RAM)就是程序运行介质。SoC开机后,片上的固化程序BROM会加载存储介质的执行程序到IRAM中, 但IRAM的大小有限,往往就只有几KB,无法容纳u-boot.bin,这时u-boot-spl轻量级就排上用场了,它先初始化DDR时序、系统时钟等,准备好外部的运行环境,再加载u-boot.bin,并将CPU的控制权交给u-boot.

接下来,介绍下u-boot-spl的smdk6410配置过程。

1.在defconfig 配置CONFIG_SPL

 除了smdk6410_defconfig添加CONFIG_SPL之外,注意还需要在Kconfig 文件里面配置 CONFIG_SUPPORT_SPL
 
FILE: arch/arm/mach-s3c64xx/Kconfig

  config TARGET_OK6410
      default y
      bool "Support smdk6410 board"
      select OF_CONTROL
      select SUPPORT_SPL

  因为CONFIG_SPL依赖于CONFIG_SUPPORT_SPL,见

FILE: ./common/spl/Kconfig

 config SPL
     bool
     depends on SUPPORT_SPL
     prompt "Enable SPL"

2. 另外,代码的控制实际是通过 CONFIG_SPL_BUILD来控制。

  见源码的宏控制分支,和各个Makefile中obj-$(CONFIG_SPL_BUILD)和宏控制分支
执行Makefiel,该宏定义来源于:

FILE: ./scripts/Makefile.autoconf

 spl/u-boot.cfg: include/config.h FORCE
     $(Q)mkdir -p $(dir $@)
     $(call cmd,u_boot_cfg,-DCONFIG_SPL_BUILD)
 
 tpl/u-boot.cfg: include/config.h FORCE
     $(Q)mkdir -p $(dir $@)
     $(call cmd,u_boot_cfg,-DCONFIG_SPL_BUILD -DCONFIG_TPL_BUILD)

  编译C源码,该宏定义来源于  :

FILE: ./scripts/Makefile.spl

 24 
 25 KBUILD_CPPFLAGS += -DCONFIG_SPL_BUILD
 26 ifeq ($(CONFIG_TPL_BUILD),y)
 27 KBUILD_CPPFLAGS += -DCONFIG_TPL_BUILD
 28 endif
    ...
 36 ifdef CONFIG_SPL_BUILD
 37 SPL_ := SPL_
 38 ifeq ($(CONFIG_TPL_BUILD),y)
 39 SPL_TPL_ := TPL_
 40 else
 41 SPL_TPL_ := SPL_
 42 endif
 43 else
 44 SPL_ :=
 45 SPL_TPL_ :=
 46 endif

 上述文件中, SPL_ 和SPL_TPL_宏也在此定义. ,这样的宏被运用于生成u-boot-spl.bin 或 u-boot-tpl.bin的宏选择,
 比如 drivers/Makfile : obj-$(CONFIG_$(SPL_TPL_)CLK) += mtd/nand/
 但注意,这对应生成u-boot.bin并无影响, 因为u-boot.bin 的编译是由顶层Makefile直接定义的,不关心二级目录的Makefie.
 比如 ./Makefile : libs-$(CONFIG_CMD_NAND) += drivers/mtd/nand/

FILE: ./Makefile

 681 libs-y += lib/
 682 libs-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/
 683 libs-$(CONFIG_OF_EMBED) += dts/
 684 libs-y += fs/
 685 libs-y += net/
 686 libs-y += disk/
 687 libs-y += drivers/
 688 libs-y += drivers/dma/
 689 libs-y += drivers/gpio/
 690 libs-y += drivers/i2c/
 691 libs-y += drivers/mtd/
 692 libs-$(CONFIG_CMD_NAND) += drivers/mtd/nand/
 693 libs-y += drivers/mtd/onenand/
 694 libs-$(CONFIG_CMD_UBI) += drivers/mtd/ubi/

  所以u-boot.bin和u-boot-spl.bin、u-boot-tpl.bin是不同的编译过程,第一个是默认必选;后两者可选,分别由CONFIG_SPL、CONFIG_TPL 控制。
  默认情况下,u-boot-spl.bin的text base 是基于0x000000。 如果SoC加载地址不是0x00000,那么编译时就需要根据实际加载情况配置CONFIG_SPL_TEXT_BASE。
  在samsung S3C6410 SoC上, 有个叫‘Steppingstone’的内部SRAM,物理起始地址为0x0C00_0000。 SoC上电后,执行内部固化的ROM程序,会根据boot_config选择boot device,并将读取开头的部分代码到‘Steppingstone’,之后将CPU控制权交给这部分代码。 
  如果代码的执行是位置无关的,则没有问题,否则需要确保代码的TEXT_BASE和实际加载地址一致。 S3C6410上,根据添加的打印信息,默认将SDHC卡代码读取到SRAM的一个起始地址,故定义如下:
  #define CONFIG_SPL_TEXT_BASE  0x0C000000 

  SPL代码里,可利用BROM提供的mmc接口函数CopyMovitoMem(),将u-boot.bin从SDHC卡中,移到DDR中。
修改arch/arm/cpu/arm1176/start.S, 并在smdk6410_defconfig中添加宏 CONFIG_SPL_BOOT_MMC

	/*
	 * Go setup Memory and board specific bits prior to relocation.
	 */
	bl	lowlevel_init		/* go setup pll,mux,memory */

#if  CONFIG_IS_ENABLED(BOOT_MMC)
	ldr	sp, =CONFIG_SYS_PHY_UBOOT_BASE
	bl	mmc_bl2_copy
	cmp r0, #0
1:  beq 1b 
	ldr pc, =CONFIG_SYS_PHY_UBOOT_BASE 
#else
	bl	_main
#endif

添加board/samsumg/smdk6410/bl2_copy.c

#include <common.h>
#include <asm/io.h>
#include <asm/arch/sdhc.h>

#define HSMMC_CHANNEL		0
#define MOVI_INIT_REQUIRED	0

#define CopyMovitoMem(a,b,c,d,e)	(((int(*)(int, uint, ushort, uint *, int))(*((uint *)(TCM_BASE + 0x8))))(a,b,c,d,e))

#define TCM_BASE	0x0C004000
#define BL2_BASE		CONFIG_SYS_UBOOT_BASE

/* size information */
#define SS_SIZE			(8 * 1024)      //bl1 size
#define eFUSE_SIZE		(1 * 1024)	// 0.5k eFuse, 0.5k reserved`

/* movinand definitions */
#define MOVI_BLKSIZE		512

#define MOVI_TOTAL_BLKCNT	*((volatile unsigned int*)(TCM_BASE - 0x4))
#define MOVI_HIGH_CAPACITY	*((volatile unsigned int*)(TCM_BASE - 0x8))

/* partition information */
#define PART_SIZE_BL		(512 * 1024)
#define PART_SIZE_KERNEL	(4 * 1024 * 1024)
#define PART_SIZE_ROOTFS	(8 * 1024 * 1024)

#define MOVI_LAST_BLKPOS	(MOVI_TOTAL_BLKCNT - (eFUSE_SIZE / MOVI_BLKSIZE))
#define MOVI_BL1_BLKCNT		(SS_SIZE / MOVI_BLKSIZE)
#define MOVI_ENV_BLKCNT		(CONFIG_ENV_SIZE / MOVI_BLKSIZE)
#define MOVI_BL2_BLKCNT		(PART_SIZE_BL / MOVI_BLKSIZE)
#define MOVI_ZIMAGE_BLKCNT	(PART_SIZE_KERNEL / MOVI_BLKSIZE)
#define MOVI_BL2_POS		(MOVI_LAST_BLKPOS - MOVI_BL1_BLKCNT - MOVI_BL2_BLKCNT - MOVI_ENV_BLKCNT)
#define MOVI_ROOTFS_BLKCNT	(PART_SIZE_ROOTFS / MOVI_BLKSIZE)

int mmc_bl2_copy(void)
{
	int ret;

	/* Set current driver to  9mA */
	writel(readl(HM_CONTROL4) | (0x3 << 16), HM_CONTROL4);
	ret = CopyMovitoMem(HSMMC_CHANNEL, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
						(uint *)BL2_BASE, MOVI_INIT_REQUIRED);
	return ret;
}

代码烧写

最后就是SDHC卡的代码烧写。S3C6410的SD/MMC加载原理,这里就不多说了,网上有很多。
底下附上我自己写的烧录脚本,放到uboot代码的根目录下,注意,我的uboot 编译结果位于./build ,如果你不是,请修改脚本里面的文件路径。烧录方法,只要插上SDHC卡的U盘(一定要用U盘读卡器),执行这个脚本即可。

#!/bin/bash
#
# Created by Golden.Chen
# Date: 2018/12/23 15:56
#

SDCARD=/dev/sdb
SPL_BOOT=build/spl/u-boot-spl.bin
UBOOT=build/u-boot.bin


if [ `whoami` != "root" ];then
    echo "Need root permision to execute the script!"
    exit
fi

echo "SD Card Writer program V1.0"
echo
if [ "$1" != "" ]; then
    SDCARD=$1
fi
if [ "$2" == "sd" ]; then
    RES_SZ=1
else
    RES_SZ=1025
fi

echo "sd/mmc: $SDCARD"
if [ ! -b "$SDCARD" ]; then
cat << EOF
no device found

Usage: $0 <sd-dev> [sd-type]
     defalutly, <sd-dev> is /dev/sdb, sd-type is "sdhc" in ("sdhc","sd")
EOF
    exit 1        
fi

# Check SDCARD Sectors
# SDCARD_SECTORS=`fdisk -l -u $SDCARD | grep sectors | head -n 1 | \
#                cut -d',' -f4 | cut -d' ' -f3`

SDCARD_SECTORS=`cat /sys/block/${SDCARD##*/}/size`

# unit: sectors -512 bytes
SECTOR_SIZE=512
# RES_SZ=1 or 1025 sectors
SIG_SZ=1
BL1_SZ=16
BL2_SZ=1024
ENV_SZ=32

SIZE_FAT=$(($SDCARD_SECTORS-$FL210_FREE-$FL210_FREE-2))

START_BL1_POS=$(($SDCARD_SECTORS-$RES_SZ-$SIG_SZ-$BL1_SZ))
START_BL2_POS=$(($START_BL1_POS-$ENV_SZ-$BL2_SZ))

print_success()
{
    if [ "$1" == 0 ]; then
        echo "success"
    else
        echo "failed"
        exit -1
    fi
}

# Ask for correct calculation
cat <<EOF
sd/mmc device  : $SDCARD
byte per sector: $SECTOR_SIZE
total sectors  : $SDCARD_SECTORS

fusing sd/mmc acticon in the unit: sector
EOF
#echo "Do you want to continue(yes/no): "
#read ANS
#if [ "$ANS" != "yes" ]; then
#    exit -1
#fi

#size 1M
# echo -n "mmc.bin : "
# dd bs=512 seek=1 if=/dev/zero of=$SDCARD count=2048 > /dev/null 2>&1
# dd bs=512 seek=1 if=./mmc.bin of=$SDCARD > /dev/null 2>&1

echo -n "write bl1 to sd/mmc at offset: $START_BL1_POS size: $BL1_SZ ..."
dd bs=512 seek=$START_BL1_POS if=/dev/zero of=$SDCARD count=$BL1_SZ  > /dev/null 2>&1
dd bs=512 seek=$START_BL1_POS if=$SPL_BOOT of=$SDCARD count=$BL1_SZ  > /dev/null 2>&1
print_success "$?"

echo -n "write bl2 to sd/mmc at offset: $START_BL2_POS size: $BL2_SZ ..."
dd bs=512 seek=$START_BL2_POS if=/dev/zero of=$SDCARD count=$BL2_SZ > /dev/null 2>&1
dd bs=512 seek=$START_BL2_POS if=$UBOOT of=$SDCARD count=$BL2_SZ  > /dev/null 2>&1
print_success "$?"

sync
exit  0
################ the end #########################

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值