BeagleBone Black 从零到一 (2 MLO、U-Boot)

摘自:http://jexbat.com/2016/BBB-Uboot/


BeagleBone Black 从零到一 (2 MLO、U-Boot)

更新:2016-04-01

什么是 U-Boot

熟悉嵌入式开发的应该都听过它,U-boot 就是启动系统前的一段引导程序,虽然是引导程序,但是功能非常强大。

这一篇主要讲解如何从无到有运行 U-Boot,关于 U-Boot 引导 Linux 的部分放在另外一篇文章讲解。

U-Boot 之前的版本以版本号命名如:0.1.0, 0.2.0 这几年改为了以时间和日期命名:U-Boot 2016.03。

使用 git 获得 U-Boot 的源码:

1
git clone git://git.denx.de/u-boot.git

目前我使用的是 2016.02 的版本。

MLO 及其启动过程

上一篇文章,我们了解了 BeagleBone 有个 SPL 过程,就在这个时候读取 MLO 文件,MLO 文件其实是个精简版的 U-Boot,也是由 U-Boot 生成,但是功能有限,只初始化了部分资源如 DDR,然后启动 U-Boot。

MLO 文件是如何编译出来的

分析 MLO 的编译过程之前需要知道编译原理和 Makefile 等相关知识。
我们先找找 Makefile 看看能不能找到什么。建议使用 Sublime 编辑器。用全局查找功能查找 MLO 关键字。

找到 u-boot/scripts/Makefile.spl 文件 117行

u-boot/scripts/Makefile.spl
1
2
MLO MLO.byteswap: $(obj)/u-boot-spl.bin FORCE
	$(call if_changed,mkimage)

可以看到 MLO 文件是由 u-boot-spl.bin 文件通过 mkimage 命令生成的。
再查到 u-boot/Makefile 文件 1310 行

u-boot/Makefile
1
2
3
4
spl/u-boot-spl.bin: spl/u-boot-spl
	@:
spl/u-boot-spl: tools prepare $(if $(CONFIG_OF_SEPARATE),dts/dt.dtb)
	$(Q)$(MAKE) obj=spl -f $(srctree)/scripts/Makefile.spl all

u-boot-spl.bin 文件是还是由 u-boot/scripts/Makefile.spl 文件生成。
文件 u-boot/scripts/Makefile.spl 168 行 定义了 u-boot-spl.bin 的生成:

u-boot/scripts/Makefile.spl
1
2
3
4
5
6
7
8
9
10
11
ifeq ($(CONFIG_SPL_OF_CONTROL),y)
$(obj)/$(SPL_BIN)-dtb.bin: $(obj)/$(SPL_BIN)-nodtb.bin $(obj)/$(SPL_BIN)-pad.bin \
		$(obj)/$(SPL_BIN).dtb FORCE
	$(call if_changed,cat)

$(obj)/$(SPL_BIN).bin: $(obj)/$(SPL_BIN)-dtb.bin FORCE
	$(call if_changed,copy)
else
$(obj)/$(SPL_BIN).bin: $(obj)/$(SPL_BIN)-nodtb.bin FORCE
	$(call if_changed,copy)
endif

因为 SPL_BIN 在 第32行 定义为 u-boot-spl:

u-boot/scripts/Makefile.spl
1
2
3
4
5
ifeq ($(CONFIG_TPL_BUILD),y)
SPL_BIN := u-boot-tpl
else
SPL_BIN := u-boot-spl
endif

由 168 行 上面的定义可以知道 u-boot-spl.bin 和 u-boot-spl-nodtb.bin 有关系。

接着查找到第223行

u-boot/scripts/Makefile.spl
1
2
$(obj)/$(SPL_BIN)-nodtb.bin: $(obj)/$(SPL_BIN) FORCE
	$(call if_changed,objcopy)

u-boot-spl-nodtb.bin 是通过 objcopy 命令由 u-boot-spl 生成。

再看第246行:

u-boot/scripts/Makefile.spl
1
2
$(obj)/$(SPL_BIN): $(u-boot-spl-init) $(u-boot-spl-main) $(obj)/u-boot-spl.lds FORCE
	$(call if_changed,u-boot-spl)

所以u-boot-spl 是由 u-boot-spl.lds 链接文件生成的 ,但是目录下面有几个u-boot-spl.lds文件,到底是哪个 lds 文件呢,上面是 $(obj)/u-boot-spl.lds, obj 在 1310 行 编译 u-boot-spl.bin 的时候赋值为 obj=spl,所以我们需要看 u-boot/spl/u-boot-spl.lds 这个文件,但是如果你之前没有编译过这个文件是没有的。这个文件是如何生成的呢?我们稍后再看,先看 lds 文件的内容:

u-boot/spl/u-boot-spl.lds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
MEMORY { .sram : ORIGIN = 0x402F0400, LENGTH = (0x4030B800 - 0x402F0400) }
MEMORY { .sdram : ORIGIN = 0x80a00000, LENGTH = 0x80000 }
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 .text :
 {
  __start = .;
  *(.vectors)
  arch/arm/cpu/armv7/start.o (.text)
  *(.text*)
 } >.sram
 . = ALIGN(4);
 .rodata : { *(SORT_BY_ALIGNMENT(.rodata*)) } >.sram
 . = ALIGN(4);
 .data : { *(SORT_BY_ALIGNMENT(.data*)) } >.sram
 .u_boot_list : {
  KEEP(*(SORT(.u_boot_list*)));
 } >.sram
 . = ALIGN(4);
 __image_copy_end = .;
 .end :
 {
  *(.__end)
 } >.sram
 .bss :
 {
  . = ALIGN(4);
  __bss_start = .;
  *(.bss*)
  . = ALIGN(4);
  __bss_end = .;
 } >.sdram
}

链接文件里面说明了内存布局,arch/arm/cpu/armv7/start.o 代码段都放在 SRAM 中,所以 arch/arm/cpu/armv7/start.S 就是我们要找的东西了。

lds 链接文件的生成

u-boot/spl/u-boot-spl.lds 这个文件的生成在 u-boot/scripts/Makefile.spl 有解释:

u-boot/scripts/Makefile.spl
1
2
$(obj)/u-boot-spl.lds: $(LDSCRIPT) FORCE
	$(call if_changed_dep,cpp_lds)

LDSCRIPT 的定义:

u-boot/scripts/Makefile.spl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Linker Script
ifdef CONFIG_SPL_LDSCRIPT
# need to strip off double quotes
LDSCRIPT := $(addprefix $(srctree)/,$(CONFIG_SPL_LDSCRIPT:"%"=%))
endif

ifeq ($(wildcard $(LDSCRIPT)),)
	LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot-spl.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
	LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot-spl.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
	LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot-spl.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
$(error could not find linker script)
endif

可见 Makefile.spl 文件中先是判断有没有指定的 lds 文件,如果没有指定的,就查找 board 文件夹中目标板目录下面有没有 lds 文件,如果没有就查找相应的 cpu 目录,因为我们目标器件是 am335x,所以发现有 u-boot/arch/arm/cpu/armv7/am33xx/u-boot-spl.lds 再通过 cpp_lds 命令编译成,cpp_lds 是一组命令的集合,具体定义还是在 Makefile.spl 文件中,我们查看 u-boot/arch/arm/cpu/armv7/am33xx/u-boot-spl.lds 也发现 MLO 文件代码是在 start.S 文件中。

MLO 程序分析

查看 start.S 分析下 MLO 程序具体的执行流程,MLO 的 makefile 会根据 CONFIG_SPL_BUILD 编译不同的源文件,同样的在源码内也通过 CONFIG_SPL_BUILD 控制不同的代码执行,前面一部分 MLO 文件和 U-Boot 是类似的,进入到 _main 函数中两个程序的功能就开始出现差异了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
reset //(arch/arm/cpu/armv7/start.S)
save_boot_params_ret //(arch/arm/cpu/armv7/start.S)
  |- disable interrupts 
  |- cpu_init_cp15 //(arch/arm/cpu/armv7/start.S)
  |   |- Invalidate L1 I/D
  |   |- disable MMU stuff and caches
  |- cpu_init_crit //(arch/arm/cpu/armv7/start.S)
  |   |- lowlevel_init //(arch/arm/cpu/armv7/lowlevel_init.S)
  |       |- Setup a temporary stack
  |       |- Set up global data 
  |       |- s_init //(arch/arm/cpu/armv7/am33xx/board.c)
  |           |- watchdog_disable
  |           |- set_uart_mux_conf
  |           |- setup_clocks_for_console
  |           |- uart_soft_reset
  |- _main //(arch/arm/lib/crt0.S)
  	  
      |(MLO)如果是 MLO 文件
      |- board_init_f //(arch/arm/cpu/armv7/am33xx/board.c)
      |   |- board_early_init_f //(arch/arm/cpu/armv7/am33xx/board.c)
      |   |   |- prcm_init
      |   |   |- set_mux_conf_regs
      |   |- sdram_init //(board/ti/am335x/board.c) 初始化 DDR
      |- spl_relocate_stack_gd
      |- board_init_r //(common/spl/spl.c)
          |- ...
          |- spl_load_image //根据不同的启动方式加载 u-boot 镜像,
          |- jump_to_image_no_args //进入u-boot代码运行
  	  
  
      |(U-Boot)如果是U-Boot 镜像
      |- board_init_f //(common/board_f.c)
      |   |- ...
      |   |- initcall_run_list(init_sequence_f)   
      |   |- ...   
      |   
      |- relocate_code //(arch/arm/lib/relocate.S) 代码重定位
      |- relocate_vectors //(arch/arm/lib/relocate.S) 向量表重定义
      |- Set up final (full) environment 
      |- board_init_r //(common/board_r.c)
          |- initcall_run_list(init_sequence_r)//初始化各种外设
              |- main_loop()

当 U-Boot 重定位好代码、向量表之后,运行 board_init_r 函数,此函数会调用 init_sequence_r 列表里面的函数初始化各种外设驱动,最后在 main_loop() 函数中运行,U-Boot 有个 bootdelay 延时启动,如果不手动停止 U-Boot 会自动运行 bootcmd 包含的命令。

内核引导这部分放在另外一篇文章详细讲解。

U-Boot 编译

编译 U-Boot

编译 U-Boot 前我们需要安装交叉编译器:

1
# sudo apt-get install gcc-arm-linux-gnueabihf

下载 U-Boot 源码:

1
# git clone git://git.denx.de/u-boot.git

因为 U-Boot 官方已经支持了 Beaglebone Black 所以配置文件也已经自带了,编译输入如下命令:

1
2
3
# make distclean
# make am335x_boneblack_defconfig
# ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- make

片刻后会生成 MLO 和 u-boot.img 文件。

配置 U-Boot 参数

有两种方式可以配置 U-Boot 的一些参数,分别是 uEnv.txt 和 boot.src 文件。
U-Boot 启动的时候会在启动分区寻找这两个文件。

boot.scr: This file is a U-Boot script. It contains instructions for U-Boot. Using these instruction, the kernel is loaded into memory, and (optionally) a ramdisk is loaded. boot.scr can also pass parameters to the kernel. This file is a compiled script, and cannot be edited directly. In some cases, boot.scr loads further instructions and configuration parameters from a text file.

uEnv.txt: A file with additional boot parameters. This file can be read by boot.scr, or by the boot sequence if there is no script file. uEnv.txt is a regular text file that can be edited. This file should have Unix line ending, so a compatible program must be used when editing this file.

U-Boot 启动的时候如果不打断会调用 bootcmd 包含的命令来执行,通常 bootcmd 会调用 bootscript 脚本也就是 boot.scr里面的命令进行执行, boot.scr 通常也会先读取 uEnv.txt 确定额外参数,因为 boot.src 文件必须通过 boot.cmd 文件编译而来, uEnv.txt 则是可以任意编辑,这样可配置性就大大提高了。如果没有 boot.src 文件,U-Boot 有默认配置的 bootcmd 命令。

在 Beagelbone Black 中我们不需要额外的 boot.scr 文件,用默认的命令即可,默认的命令为:

1
2
3
#define CONFIG_BOOTCOMMAND \
	"run findfdt; " \
	"run distro_bootcmd"

run distro_bootcmd 最终会调用 run mmcboot 命令加载 uEnv.txt 文件,并且会运行 uEnv.txt 文件里面 uenvcmd 指代的命令。

uEnv.txt 从网络启动例子:

1
2
3
4
5
6
7
console=ttyO0,115200n8
ipaddr=192.168.23.2
serverip=192.168.23.1
rootpath=/exports/rootfs
netargs=setenv bootargs console=${console} ${optargs} root=/dev/nfs nfsroot=${serverip}:${rootpath},${nfsopts} rw ip=${ipaddr}:${serverip}:192.168.23.1:255.255.255.0:beaglebone:eth0:none:192.168.23.1
netboot=echo Booting from network ...; tftp ${loadaddr} ${bootfile}; tftp ${fdtaddr} ${fdtfile}; run netargs; bootz ${loadaddr} - ${fdtaddr}
uenvcmd=run netboot

制作 U-Boot 的 SD 启动卡

制作 SD 启动卡之前首先需要为 SD 卡分区, ROM Code 启动的时候如果是从 MMC 设备加载启动代码,ROM Code 会从第一个活动分区寻找名为 “MLO” 的文件,并且此分区必须为 FAT文件系统。所以制作 U-Boot 的启动卡只需要一个带有 MLO 和 U-Boot 镜像的 FAT 格式的 SD 卡,如果需要启动 Linux 内核还需要别的分区,我们以后再讲。

有两种方式可以制作包含 U-Boot 的可启动的 SD 卡,一种是用 RAW Mode 的方式,还有一种是用 FTA 的方式。

RAW Mode 和烧写方式在这篇文章里面有讲:解析 BeagleBone Black 官方镜像

FTA 模式下只要建立一个 FTA 分区再把 MLO 和 uboot.img 文件拷贝进去即可。

我是使用的 USB 读卡器,插入后 Linux /dev/ 目录会显示 /dev/sd* 设备,我这里多出两个设备分别显示 /dev/sdb 和 /dev/sdb1 ,其中 /dev/sdb 表示一整个物理磁盘, /dev/sdb1 表示的是具体的分区。

使用命令 sudo fdisk /dev/sdb 管理磁盘:

a : toggle a bootable flag(设置或取消启动表示)

b : edit bsd disklabel(编辑 bsd disklabel)

c : toggle the dos compatibility flag

d : delete a partition (删除一个分区)

l : list known partition types (列出已知的分区类型)

m : print this menu (打印次列表)

n : add a new partition (增加一个新分区)

o : create a new empty DOS partition table (建立一个新的空 DOS 分区表)

p : print the partition table (打印分区表)

q : quit without saving changes (不保存退出)

s : create a new empty Sun disklabel

t : change a partition’s system id

u : change display/entry units

v : verify the partition table (验证分区表)

w : write table to disk and exit (把分区表写入磁盘)

x : extra functionality (experts only) (额外的功能)

新建启动分区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
Command (m for help): p

Disk /dev/sdb: 7746 MB, 7746879488 bytes
24 heads, 20 sectors/track, 31522 cylinders, total 15130624 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1   *        2048    15130623     7564288    c  W95 FAT32 (LBA)

Command (m for help): m
Command action
   a   toggle a bootable flag
   b   edit bsd disklabel
   c   toggle the dos compatibility flag
   d   delete a partition
   l   list known partition types
   m   print this menu
   n   add a new partition
   o   create a new empty DOS partition table
   p   print the partition table
   q   quit without saving changes
   s   create a new empty Sun disklabel
   t   change a partition's system id
   u   change display/entry units
   v   verify the partition table
   w   write table to disk and exit
   x   extra functionality (experts only)

Command (m for help): d
Selected partition 1

Command (m for help): p

Disk /dev/sdb: 7746 MB, 7746879488 bytes
24 heads, 20 sectors/track, 31522 cylinders, total 15130624 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

   Device Boot      Start         End      Blocks   Id  System

Command (m for help): n
Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p): p
Partition number (1-4, default 1): 
Using default value 1
First sector (2048-15130623, default 2048): 
Using default value 2048
Last sector, +sectors or +size{K,M,G} (2048-15130623, default 15130623): 
Using default value 15130623

Command (m for help): p

Disk /dev/sdb: 7746 MB, 7746879488 bytes
24 heads, 20 sectors/track, 31522 cylinders, total 15130624 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1            2048    15130623     7564288   83  Linux

Command (m for help): t
Selected partition 1
Hex code (type L to list codes): c
Changed system type of partition 1 to c (W95 FAT32 (LBA))

Command (m for help): a
Partition number (1-4): 1

Command (m for help): p

Disk /dev/sdb: 7746 MB, 7746879488 bytes
24 heads, 20 sectors/track, 31522 cylinders, total 15130624 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1   *        2048    15130623     7564288    c  W95 FAT32 (LBA)

Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.

WARNING: If you have created or modified any DOS 6.x
partitions, please see the fdisk manual page for additional
information.
Syncing disks.

建立好新的分区之后需要命名并格式化:

1
# sudo mkfs.vfat -F 32 -n boot /dev/sdb1

格式化之后挂载磁盘并把 MLO 文件和 u-boot.img 文件拷贝进去:

1
2
3
4
5
6
7
8
9
# sudo mount /dev/sdb1 /media/jg/boot 
# sudo cp MLO /media/jg/boot/MLO
# ls /media/jg/boot             
MLO
# sudo cp u-boot.img /media/jg/boot/u-boot.img
# ls /media/jg/boot 
MLO  u-boot.img
# sync 
# sudo umount /media/jg/boot

接着把 SD 卡插入 Beaglebone Black 并且按着 S2 按钮上电,从串口打印出的信息我们可以看到 U-Boot 已经可以正常启动了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
U-Boot SPL 2016.03-rc2-00084-g595af9d (Feb 29 2016 - 22:21:20)
Trying to boot from MMC
Card doesn't support part_switch
MMC partition switch failed
*** Warning - MMC partition switch failed, using default environment

reading u-boot.img
reading u-boot.img

U-Boot 2016.03-rc2-00084-g595af9d (Feb 29 2016 - 22:21:20 +0800)

       Watchdog enabled
I2C:   ready
DRAM:  512 MiB
MMC:   OMAP SD/MMC: 0, OMAP SD/MMC: 1
*** Warning - bad CRC, using default environment

Net:   <ethaddr> not set. Validating first E-fuse MAC
cpsw, usb_ether
Press SPACE to abort autoboot in 2 seconds
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
reading boot.scr
** Unable to read file boot.scr **
reading uEnv.txt
** Unable to read file uEnv.txt **
** File not found /boot/zImage **
switch to partitions #0, OK
mmc1(part 0) is current device
Scanning mmc 1:1...
switch to partitions #0, OK
mmc1(part 0) is current device
SD/MMC found on device 1
reading boot.scr
** Unable to read file boot.scr **
reading uEnv.txt
** Unable to read file uEnv.txt **
** File not found /boot/zImage **
## Error: "bootcmd_nand0" not defined
cpsw Waiting for PHY auto negotiation to complete......... TIMEOUT !
BOOTP broadcast 1
BOOTP broadcast 2
BOOTP broadcast 3
BOOTP broadcast 4

启动之后,前面一段打印信息是 MLO 程序打印出来的,读取 U-Boot 之后开始运行完整的 U-Boot,之后程序扫描各个设备读取 boot.scr 和 uEnv.txt 文件,接着再读取是否有 Linux 内核可以运行。

参考资料

### BeagleBone Black Web Development Resources #### Overview of BeagleBone Black BeagleBone Black represents an advanced addition to the BeagleBoard family, utilizing a cost-effective Sitara XAM3359AZCZ100 Cortex A8 ARM processor from Texas Instruments[^1]. This device offers extensive expansion capabilities while maintaining affordability. #### Setting Up Environment for Web Development To engage in web development activities on BeagleBone Black, setting up a suitable environment is crucial. One common approach involves building images specifically tailored for this platform and booting them via SD card[^2]. For instance, creating custom Linux distributions that include necessary packages like Node.js or Python can facilitate rapid prototyping and deployment of web applications. Additionally, configuring network interfaces properly ensures seamless connectivity during development phases. #### Updating Bootloader (U-Boot) Before diving into actual coding tasks, ensuring the bootloader (U-Boot) is updated correctly plays a vital role in achieving stable operation. Commands such as `sudo dd if=MLO of=/dev/mmcblk0 count=1 seek=1 conv=notrunc bs=128k` followed by `sudo dd if=u-boot.img of=/dev/mmcblk0 count=2 seek=1 conv=notrunc bs=384k` are used for updating U-Boot on BeagleBone Black devices[^3]. ```bash sudo dd if=MLO of=/dev/mmcblk0 count=1 seek=1 conv=notrunc bs=128k sudo dd if=u-boot.img of=/dev/mmcblk0 count=2 seek=1 conv=notrunc bs=384k ``` #### Recommended Tools & Frameworks Several tools and frameworks support efficient web application creation on embedded systems: - **Node.js**: Ideal for server-side scripting due to its event-driven architecture. - **Express.js**: Lightweight framework built atop Node.js simplifying RESTful API construction. - **Python Flask/Django**: Suitable alternatives offering robust solutions depending upon project requirements. Moreover, leveraging cloud services through APIs allows developers to integrate external functionalities effortlessly within their projects hosted locally on BeagleBone Blacks. --related questions-- 1. How does one install Node.js on BeagleBone Black? 2. What steps should be taken when preparing an SD card image for web development purposes? 3. Can you provide examples of successful web-based IoT projects implemented using BeagleBone Black? 4. Which version control system works best with BeagleBone Black for collaborative web development efforts?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值