uboot启动流程 - 运行到 main_loop

说明:本文参考很多博主的文章,此笔记仅用于记录自己的学习过程

U-Boot启动内核的过程可以分为两个阶段,两个阶段的功能如下:

(1)第一阶段的功能

硬件设备初始化(设置SVC管理模式、关看门狗、关中断、设置时钟频率、RAM初始化、关MMU等)
加载U-Boot第二阶段代码到RAM空间
设置好栈
跳转到第二阶段代码入口
(2)第二阶段的功能

初始化本阶段使用的硬件设备
检测系统内存映射
将内核从Flash读取到RAM中
为内核设置启动参数
调用内核

分析 uboot 的启动流程,首先要找到“入口”。uboot 的链接脚本为arch/arm/cpu/u-boot.lds。编译uboot后,会在 uboot 根目录下生成 u-boot.lds文件。

一、u-boot.lds 分析

路径:/arch/arm/cpu/u-boot.lds

#include <config.h>
 
//指定输出可执行文件是elf格式,32位ARM指令,小端
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")  
OUTPUT_ARCH(arm)                     //指定输出可执行文件的平台为ARM
ENTRY(_start)                        //入口函数 ,指定输出可执行文件的起始代码段为_start
SECTIONS
{
        /* 指定可执行image文件的全局入口点,通常这个地址都放在ROM(flash)0x0位置。必须使编译器知道这个地址,通常都是修改此处来完成 */
        . = 0x00000000;        //起始地址 从0x0位置开始
 
        . = ALIGN(4);          //代码以4字节对齐
        .text :                //文本段
        {
                *(.__image_copy_start) //变量__image_copy_start  映像文件复制起始地址
                *(.vectors)            //异常向量表  .vectors标记的代码段 (uboot 的起始地址)
                CPUDIR/start.o (.text*)//启动函数,arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面
                *(.text*)              //剩余的文本段
        }
 
        ......                         //安全相关
        
        . = ALIGN(4);
        .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } //ro数据段
 
        . = ALIGN(4);
        .data : {                                                  //RW数据段
                *(.data*)
        }
 
        . = ALIGN(4);
 
        . = .;
 
        . = ALIGN(4);
        .u_boot_list : {                  //存放uboot自定义命令
                KEEP(*(SORT(.u_boot_list*)));
        }
 
        . = ALIGN(4);
 
        .image_copy_end :
        {
                *(.__image_copy_end)       //变量__image_copy_end
        }
 
        .rel_dyn_start :
        {
                *(.__rel_dyn_start)
        }
 
        .rel.dyn : {
                *(.rel*)
        }
 
        .rel_dyn_end :
        {
                *(.__rel_dyn_end)
        }
 
        .end :
        {
                *(.__end)
        }
         _image_binary_end = .;
 
        /*
         * Deprecated: this MMU section is used by pxa at present but
         * should not be used by new boards/CPUs.
         */
        . = ALIGN(4096);
        .mmutable : {
                *(.mmutable)
        }
 
/*
 * Compiler-generated __bss_start and __bss_end, see arch/arm/lib/bss.c
 * __bss_base and __bss_limit are for linker only (overlay ordering)
 */
 
        .bss_start __rel_dyn_start (OVERLAY) : {
                KEEP(*(.__bss_start));
                __bss_base = .;
        }
 
        .bss __bss_base (OVERLAY) : {
                *(.bss*)
                 . = ALIGN(4);
                 __bss_limit = .;
        }
 
        .bss_end __bss_limit (OVERLAY) : {
                KEEP(*(.__bss_end));
        }
 
        .dynsym _image_binary_end : { *(.dynsym) }
        .dynbss : { *(.dynbss) }
        .dynstr : { *(.dynstr*) }
        .dynamic : { *(.dynamic*) }
        .plt : { *(.plt*) }
        .interp : { *(.interp*) }
        .gnu.hash : { *(.gnu.hash) }
        .gnu : { *(.gnu*) }
        .ARM.exidx : { *(.ARM.exidx*) }
        .gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}

从代码可知:

  1. ENTRY(_start):入口函数名,在arch/arm/lib/vectors.S文件中定义

  2. __image_copy_start、__image_copy_end: 用于u-boot搬移本身image到指定的ddr地址处。uboot
    拷贝的首地址0x17800000,在uboot.map文件中可查得,通过CONFIG_SYS_TEXT_BASE来设置。

  3. __rel_dyn_start、__rel_dyn_end用于重定位代码

  4. __bss_start、__bss_end是bss段的开始、结束地址


二、_start
路径:arch/arm/lib/vectors.S
在这里插入图片描述

...
        .macro ARM_VECTORS        //中断向量表的宏定义
#ifdef CONFIG_ARCH_K3
    ldr     pc, _reset
#else
    b    reset        //中断向量表,跳转到reset
#endif
    ldr    pc, _undefined_instruction
    ldr    pc, _software_interrupt
    ldr    pc, _prefetch_abort
    ldr    pc, _data_abort
    ldr    pc, _not_used
    ldr    pc, _irq
    ldr    pc, _fiq
    .endm
 
...

_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
    .word    CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
    ARM_VECTORS    //中断向量表的宏
#endif /* !defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) */

从代码可知:_start 调用了 ARM_VECTORS,最终调用 b reset,跳转到reset。

三、reset
路径:arch/arm/cpu/arm920t/start.S
在这里插入图片描述

arch/arm/cpu/arm920t/start.S
 
/*
 *************************************************************************
 *
 - Startup Code (called from the ARM reset exception vector)
 -  3. do important init only if we don't start from memory!
 - relocate armboot to ram
 - setup stack
 - jump to second stage
 -  *************************************************************************
 */
 
    .globl    reset
reset:
    /*
     * 设置CPU进入管理模式
     */
    mrs    r0, cpsr        @将cpsr寄存器的内容传送到r0寄存器
    bic    r0, r0, #0x1f    @工作模式位清零
    orr    r0, r0, #0xd3    @设置为SVC 管理模式
    msr    cpsr, r0        @将r0的值赋给cpsr
 
#ifdef CONFIG_S3C24X0
    /* turn off the watchdog */
# if defined(CONFIG_S3C2400)
#  define pWTCON    0x15300000
#  define INTMSK    0x14400008    /* Interrupt-Controller base addresses */
#  define CLKDIVN    0x14800014    /* clock divisor register */
#else

     /* 关看门狗 */
#  define pWTCON    0x53000000
#  define INTMSK    0x4A000008    /* Interrupt-Controller base addresses */
#  define INTSUBMSK    0x4A00001C
#  define CLKDIVN    0x4C000014    /* clock divisor register */
# endif
 
    ldr    r0, =pWTCON
    mov    r1, #0x0
    str    r1, [r0]
    /*
     * mask all IRQs by setting all bits in the INTMR - default
     */
      /* 关中断 */
    mov    r1, #0xffffffff
    ldr    r0, =INTMSK
    str    r1, [r0]            //关闭所有中断
# if defined(CONFIG_S3C2410)
    ldr    r1, =0x3ff
    ldr    r0, =INTSUBMSK
    str    r1, [r0]
# endif

    /* 设置时钟频率, FCLK:HCLK:PCLK = 1:2:4, 而FCLK默认为120Mhz */

    ldr    r0, =CLKDIVN
    mov    r1, #3
    str    r1, [r0]
#endif    /* CONFIG_S3C24X0 */
    /*
     * we do sys-critical inits only at reboot,
     * not when booting from ram!
     */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
    bl    cpu_init_crit                //关闭mmu,并初始化各个bank
#endif
    bl    _main
/*------------------------------------------------------------------------------*/
 
    .globl    c_runtime_cpu_setup
c_runtime_cpu_setup:
    mov    pc, lr

以上代码主要功能:
1、设置SVC模式、关看门狗,中断、设置PLL;
2、跳转 cpu_init_crit 函数;

#ifndef CONFIG_SKIP_LOWLEVEL_INIT
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
    mcr    p15, 0, r0, c1, c0, 0

#ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY
    /*
     * before relocating, we have to setup RAM timing
     * because memory timing is board-dependend, you will
     * find a lowlevel_init.S in your board directory.
     */
    mov    ip, lr

    bl    lowlevel_init
    mov    lr, ip
#endif
    mov    pc, lr
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */
  • 清空I-Cache、D-Cache;
  • 关闭MMU Stuff及cachei;
  • 跳转到lowlevel_init;

3、跳转_main函数

四、lowlevel_init

.globl lowlevel_init
lowlevel_init:
        /* memory control configuration */
        /* make r0 relative the current location so that it */
        /* reads SMRDATA out of FLASH rather than memory ! */
        ldr     r0, =SMRDATA
        ldr     r1, =CONFIG_SYS_TEXT_BASE
        sub     r0, r0, r1
        ldr     r1, =BWSCON     /* Bus Width Status Controller */
        add     r2, r0, #13*4
0:
        ldr     r3, [r0], #4
        str     r3, [r1], #4
        cmp     r2, r0
        bne     0b  
 
        /* everything is fine now */
        mov     pc, lr
 
        .ltorg
/* the literal pools origin */
 
SMRDATA:
    .word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28))
    .word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC))
    .word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC))
    .word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC))
    .word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC))
    .word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC))
    .word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC))
    .word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN))
    .word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN))
    .word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)
    .word 0x32
    .word 0x30
    .word 0x30

五、_main
路径: arch/arm/lib/crt0.S


ENTRY(_main)
/*
 * Set up initial C runtime environment and call board_init_f(0).
*/
/*
这里首先为调用board_init_f准备一个临时堆栈,CONFIG_SYS_INIT_SP_ADDR 这个宏就是cpu片上内存的高地址(片上内存的 大小减去GD_SIZE)。然后将堆栈初始的地址保存在r9,所以r9就是gd的起始地址,后面需要靠r9访问gd的成员。然后将r0赋值成0,r0就是要调用的board_init_f函数的第一个参数!
CONFIG_SYS_INIT_SP_ADDR = IRAM大小 - sizeof(GD)
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
  ldr sp, =(CONFIG_SPL_STACK)
#else
  ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
  bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
  sub sp, sp, #GD_SIZE /* allocate one GD above SP */
  bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
  mov r9, sp /* GD is above SP */
  mov r0, #0
  bl board_init_f
 
#if ! defined(CONFIG_SPL_BUILD)
/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_moni). Trick here is that we'll return
 * 'here' but relocated.
*/
/*
*这段代码的主要功能就是将uboot搬移到内存的高地址去执行,为kernel腾出低端空间,防止kernel解压覆盖uboot。
• adr lr, here
• ldr r0, [r9, #GD_RELOC_OFF]
• add lr, lr, r0
• 功能就是,将relocate后的here标号的地址保存到lr寄存器,这样等到relocate完成后,就可以直接跳到relocate后的here标号去执行了。
*relocate_code函数的原理及流程,是 uboot 的重要代码,下面详解!
*/
ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
add lr, lr, r0
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code                                    //重定位
here:
 
/*
*relocate完成后,uboot的代码被搬到了内存的顶部,所以必须重新设置异常向量表的
*地址,c_runtime_cpu_setup这个函数的主要功能就是重新设置异常向量表的地址。
*/
bl relocate_vectors
 
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* we still call old routine here */
 
 
/*
*清空bss段。
*/
/*
*在relocate的过程中,并没有去搬移bss段。bss段是auto-relocated的!为什么?
*可以自己思考一下,又或许看完我后面介绍的relocate的原理后你会明白!
*/
ldr r0, =__bss_start /* this is auto-relocated! */
ldr r1, =__bss_end /* this is auto-relocated! */
mov r2, #0x00000000 /* prepare zero to clear BSS */
 
clbss_l:
    cmp r0, r1 /* while not at end of BSS */
    strlo r2, [r0] /* clear 32-bit BSS word */
    addlo r0, r0, #4 /* move to next */
    blo clbss_l
 
 
/*
*这两行代码无视之,点灯什么的,和这里要讲的uboot的原理及过程没有半毛钱关系。
*/
bl coloured_LED_init
bl red_led_on
 
 
/*
*将relocate后的gd的地址保存到r1,然后调用board_init_r函数,进入uboot的新天地!
*/
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
ldr pc, =board_init_r /* this is auto-relocated! */
/* we should not return here. */
#endif
ENDPROC(_main)

以上代码主要功能:

  • 设置SP,C运行时会用到
  • board_init_f(0)
  • relocate_code/relocate_vectors/c_runtime_cpu_setup
  • 清空BSS
  • board_init_r

1、board_init_f(0) ,路径:common/board_f.c
第一阶段启动

函数主要功能:

  • 初始化一系列外设,比如串口、定时器等。

  • 初始化 gd 的各个成员变量, uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 uboot,将 DRAM前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置, malloc内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。

void board_init_f(ulong boot_flags)
{
    gd->flags = boot_flags;
    gd->have_console = 0;
// 设置global_data里面的一些标志位
 
    if (initcall_run_list(init_sequence_f))
        hang();
// 调用initcall_run_list依次执行init_sequence_f函数数组里面的函数
// 一旦init_sequence_f的函数出错,会导致initcall_run_list返回不为0,而从卡掉
}

init_sequence_f中定义了一系列初始化函数

static init_fnc_t init_sequence_f[] = {
setup_mon_len,
       // 计算整个镜像的长度gd->mon_len, 此处为__bss_end -_start,也就是整个代码的长度。
initf_malloc,
        // early malloc的内存池的设定。 初始化 gd 中跟 malloc 有关的成员变量,比如 malloc_limit内存池大小
initf_console_record,// console的log的缓存
arch_cpu_init, 
      // cpu的一些特殊的初始化
initf_dm,
arch_cpu_init_dm,
mark_bootstage, /* need timer, go after init dm */
/* TODO: can any of this go into arch_cpu_init()? */
env_init,            // 环境变量的初始化,设置 gd 的成员变量 env_addr,也就是环境变量的保存地址
init_baud_rate,      // 波特率的初始化
serial_init,         // 串口的初始化
console_init_f,      // console的初始化
print_cpuinfo,       // 打印CPU的信息
init_func_i2c,
init_func_spi,       // i2c和spi的初始化
 
dram_init, /* configure available RAM banks */
// ddr的初始化,最重要的是ddr ram size的设置!!!!gd->ram_size
// 如果说uboot是在ROM、flash中运行的话,那么这里就必须要对DDR进行初始化
setup_dest_addr,     //设置目的地址, 设置gd->ram_size(ram大小), gd->ram_top(ram最高位置), gd->relocaddr(重定位后的最高地址)
reserve_round_4k,    //对 gd->relocaddr 做 4KB 对 齐
reserve_trace,       //留出跟踪调试的内存
setup_machine,       //设置机器 ID, linux 启动的时候会和这个机器 ID 匹配(无效函数,新版本使用设备树)
reserve_global_data, //保留出 gd_t 的内存区域, gd_t 结构体大小为 248B
reserve_fdt,         //留出设备树相关的内存区域
reserve_arch,
reserve_stacks,      //留出栈空间
// ==以上部分是对relocate区域的规划,具体参考《[uboot] (番外篇)uboot relocation介绍》
setup_dram_config,   //设置 gd->bd->bi_dram[0].start 和gd->bd->bi_dram[0].size,告诉 linux 内核 DRAM 的起始地址和大小
show_dram_config,    //用于显示 DRAM 的配置
display_new_sp,      //显示新的 sp 位置,也就是 gd->start_addr_sp
reloc_fdt,           //用于重定位 fdt,没有用到
setup_reloc,         //设置 gd 的其他一些成员变量,供后面重定位的时候使用
// relocation之后gd一些成员的设置
NULL,
};

2、relocate_code,路径:arch/arm/lib/relocate.S

  • uboot image在编译链接时已经链接到固定的地址处;
  • image首先从flash上被copy到0地址处,然后根据链接地址重新将code搬移到链接时制定的DDR地址处;
  • code搬移完成之后,PC会跳转到下一条指令地址处(已经是搬完code之后的新的地址),继续执行;

3、relocate_vectors
函数 relocate_vectors用于重定位向量表

4、board_init_r,路径: common/board_r.c
第二阶段启动。
前面,board_init_f 函数会调用一系列的函数来初始化外设和 gd 的成员变量。但是 board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的。

void board_init_r(gd_t *new_gd, ulong dest_addr)
{
if (initcall_run_list(init_sequence_r))
hang();
// 调用initcall_run_list依次执行init_sequence_r函数数组里面的函数,initcall_run_list这里不深究
// 一旦init_sequence_r的函数出错,会导致initcall_run_list返回不为0,而从卡掉
 
/* NOTREACHED - run_main_loop() does not return */
hang();
// uboot要求在这个函数里面终止一切工作,或者进入死循环,一旦试图返回,则直接hang。
}

init_sequence_r

init_fnc_t init_sequence_r[] = {
initr_trace,            // trace调试跟踪相关的初始化
initr_reloc,            // 设置 gd->flags,标记重定位完成
initr_reloc_global_data,
// relocate(重定位)之后,gd中一些的成员的重新设置
initr_malloc,            // malloc内存池的设置
initr_console_record,    //初始化控制台相关的内容
bootstage_relocate,        //启动状态重定位
initr_bootstage,            //初始化 bootstage
#if defined(CONFIG_ARM) || defined(CONFIG_NDS32)
board_init, /* Setup chipselects */
// 板级自己需要的特殊的初始化函数,如board/samsung/tiny210/board.c中定义了board_init这个函数
#endif
stdio_init_tables,        //stdio 相关初始化
initr_serial,            // 串口初始化
initr_announce,            // 打印uboot运行位置的log
initr_logbuffer,            // logbuffer的初始化
power_init_board,
#ifdef CONFIG_CMD_NAND
initr_nand,                // 如果使用nand flash,那么这里需要对nand进行初始化
#endif
#ifdef CONFIG_GENERIC_MMC
initr_mmc,                // 如果使用emmc,那么这里需要对nand进行初始化
#endif
initr_env,                // 初始化环境变量
initr_secondary_cpu,      //初始化其他 CPU 核,单核没用
stdio_add_devices,        //各种输入输出设备的初始化,如 LCD driver, I.MX6ULL使用 drv_video_init 函数初始化 LCD。
initr_jumptable,            //初始化跳转表
console_init_r,            //控制台初始化,初始化完成以后此函数会调用stdio_print_current_devices函数来打印出当前的控制台设备
interrupt_init,            // 初始化中断
#if defined(CONFIG_ARM) || defined(CONFIG_AVR32)
initr_enable_interrupts,    // 使能中断
#endif
run_main_loop,            // 进入一个死循环,在死循环里面处理终端命令。
};

最终,uboot运行到了run_main_loop,并且在run_main_loop进入命令行状态,等待终端输入命令以及对命令进行处理。

1)run_main_loop函数

static int run_main_loop(void)
{
    /* initialize uboot log */
    init_write_log();
 
    /* main_loop() can return to retry autoboot, if so just run it again */
    for (;;)
        main_loop();
    return 0;
}

2)main_loop(),路径:common/main.c

main_loop()
    >>>s = bootdelay_process();
           >>>s = getenv("bootcmd");        //处理 bootcmd 启动命令
                  >>>from GD
     >>>autoboot_command(s)
            >>>run_command_list(s, -1, 0);
                    >>>cli_simple_run_command_list()
                           while (*next) {
                                     >>>cli_simple_run_command()
                                     >>>cmd_process()
                                     >>>cmd_call()
                                     >>>result = (cmdtp->cmd)(cmdtp, flag, argc, argv);
                                              cmd_tbl_t/U_BOOT_CMD -----> do_cboot()
                             }
              >>>cli_loop(); //进入命令行模式
              >>> cli_simple_loop()
              >>>run_command_repeatable()
              >>>cli_simple_run_command()
              >>>cmd_process()

到此,uboot运行到main_loop函数。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值