当前位置:   article > 正文

U-BOOT移植过程详解: SPL_uboot移植 spl

uboot移植 spl
    

        U-BOOT移植过程详解: SPL             

分类:             U-BOOT移植                   641人阅读     评论(0)     收藏     举报    

申明

  本着学习交流的原则, 将个人移植u-boot的过程做一个记录. 文章参考了csdn blog里面的很多内容, 有的已经记不得出处了, 只好把当时的摘要直接贴出来. 如果冒犯, 还请见谅. 如有侵权, 请与我邮件联系. 谢谢!


SPL

SPL是uboot第一阶段执行的代码. 主要负责搬移uboot第二阶段的代码到内存中运行. SPL是由固化在芯片内部的ROM引导的. 我们知道很多芯片厂商固化的ROM支持从nandflash, SDCARD等外部介质启动. 所谓启动, 就是从这些外部介质中搬移一段固定大小(4K/8K/16K等)的代码到内部RAM中运行. 这里搬移的就是SPL. 在最新版本的uboot中, 可以看到SPL也支持nandflash, SDCARD等多种启动方式.  当SPL本身被搬移到内部RAM中运行时, 它会从nandflash, SDCARD等外部介质中搬移uboot第二阶段的代码到外部内存中.

SPL的文件组成

当我们在uboot下执行make命令的时候, 它最核心的功能是执行Makefile中的all目标编译出相应的文件. 我们来看看这个all目标
  1. all:        $(ALL-y) $(SUBDIR_EXAMPLES)  
all:		$(ALL-y) $(SUBDIR_EXAMPLES)

all依赖于 $(ALL-y) 和 $(SUBDIR_EXAMPLES), 这里我只关注ALL-y, 如下:
  1. # Always append ALL so that arch config.mk's can add custom ones  
  2. ALL-y += $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map  
  3.   
  4. ALL-$(CONFIG_NAND_U_BOOT) += $(obj)u-boot-nand.bin  
  5. ALL-$(CONFIG_ONENAND_U_BOOT) += $(obj)u-boot-onenand.bin  
  6. ALL-$(CONFIG_SPL) += $(obj)spl/u-boot-spl.bin  
  7. ALL-$(CONFIG_SPL_FRAMEWORK) += $(obj)u-boot.img  
  8. ALL-$(CONFIG_TPL) += $(obj)tpl/u-boot-tpl.bin  
  9. ALL-$(CONFIG_OF_SEPARATE) += $(obj)u-boot.dtb $(obj)u-boot-dtb.bin  
  10. ifneq ($(CONFIG_SPL_TARGET),)  
  11. ALL-$(CONFIG_SPL) += $(obj)$(subst ",,$(CONFIG_SPL_TARGET))  
  12. endif  
  13.   
  14. # enable combined SPL/u-boot/dtb rules for tegra  
  15. ifneq ($(CONFIG_TEGRA),)  
  16. ifeq ($(CONFIG_OF_SEPARATE),y)  
  17. ALL-y += $(obj)u-boot-dtb-tegra.bin  
  18. else  
  19. ALL-y += $(obj)u-boot-nodtb-tegra.bin  
  20. endif  
  21. endif  
# Always append ALL so that arch config.mk's can add custom ones
ALL-y += $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map

ALL-$(CONFIG_NAND_U_BOOT) += $(obj)u-boot-nand.bin
ALL-$(CONFIG_ONENAND_U_BOOT) += $(obj)u-boot-onenand.bin
ALL-$(CONFIG_SPL) += $(obj)spl/u-boot-spl.bin
ALL-$(CONFIG_SPL_FRAMEWORK) += $(obj)u-boot.img
ALL-$(CONFIG_TPL) += $(obj)tpl/u-boot-tpl.bin
ALL-$(CONFIG_OF_SEPARATE) += $(obj)u-boot.dtb $(obj)u-boot-dtb.bin
ifneq ($(CONFIG_SPL_TARGET),)
ALL-$(CONFIG_SPL) += $(obj)$(subst ",,$(CONFIG_SPL_TARGET))
endif

# enable combined SPL/u-boot/dtb rules for tegra
ifneq ($(CONFIG_TEGRA),)
ifeq ($(CONFIG_OF_SEPARATE),y)
ALL-y += $(obj)u-boot-dtb-tegra.bin
else
ALL-y += $(obj)u-boot-nodtb-tegra.bin
endif
endif

因为本节是讨论SPL, 所以我们只关注其中的一句 ALL-$(CONFIG_SPL) += $(obj)spl/u-boot-spl.bin
这句话表明
  • 必须定义CONFIG_SPL才能编译出spl的bin: 一般在"include/configs/${CONFIG_NAME}.h"中定义
  • SPL的bin依赖于u-boot-spl.bin
接着往下看
  1. $(obj)spl/u-boot-spl.bin:   $(SUBDIR_TOOLS) depend  
  2.         $(MAKE) -C spl all  
$(obj)spl/u-boot-spl.bin:	$(SUBDIR_TOOLS) depend
		$(MAKE) -C spl all
这里可以发现, u-boot-spl.bin依赖于 $(SUBDIR_TOOLS) depend
  • $(SUBDIR_TOOLS) : 暂不分析
  • depend: 参考附录中的depend
  • 进入spl目录, 执行make all
接下来进入spl目录, 看看它的Makefile : 这里只分析与SPL相关的部分
  1. CONFIG_SPL_BUILD := y  
  2. export CONFIG_SPL_BUILD  
CONFIG_SPL_BUILD := y
export CONFIG_SPL_BUILD
  • export CONFIG_SPL_BUILD: 在接下来的编译中, 这个变量为y. 从后面的分析中可以看到, uboot的stage1, stage2阶段的代码用的是同一个Start.S, 只不过在Start.S中用#ifdef CONFIG_SPL_BUILD这种条件编译来区分. 类似的还有其他一些文件.
  1. HAVE_VENDOR_COMMON_LIB = $(if $(wildcard $(SRCTREE)/board/$(VENDOR)/common/Makefile),y,n)  
HAVE_VENDOR_COMMON_LIB = $(if $(wildcard $(SRCTREE)/board/$(VENDOR)/common/Makefile),y,n)
    1. 如果board/$(VENDOR)/common目录中有Makefile文件,则HAVE_VENDOR_COMMON_LIB为y否则为n  
    如果board/$(VENDOR)/common目录中有Makefile文件,则HAVE_VENDOR_COMMON_LIB为y否则为n
  1. ifdef   CONFIG_SPL_START_S_PATH  
  2. START_PATH := $(subst ",,$(CONFIG_SPL_START_S_PATH))  
  3. else  
  4. START_PATH := $(CPUDIR)  
  5. endif  
ifdef	CONFIG_SPL_START_S_PATH
START_PATH := $(subst ",,$(CONFIG_SPL_START_S_PATH))
else
START_PATH := $(CPUDIR)
endif
  • 我们这里没有定义CONFIG_SPL_START_S_PATH, 所以START_PATH := $(CPUDIR)
  1. START := $(START_PATH)/start.o  
START := $(START_PATH)/start.o
  • 依赖start.o, 综合来看, 就是要把CPUDIR下的start.S编译进来.
  1. LIBS-y += arch/$(ARCH)/lib/lib$(ARCH).o  
LIBS-y += arch/$(ARCH)/lib/lib$(ARCH).o
  • 依赖lib$(ARCH).o, 具体来看, 就是依赖arch/arm/lib/libarm.o
  1. LIBS-y += $(CPUDIR)/lib$(CPU).o  
LIBS-y += $(CPUDIR)/lib$(CPU).o
  • 依赖lib$(CPU).o, 具体来看, 就是依赖arch/arm/cpu/armv7/libarmv7.o
  1. ifdef SOC  
  2. LIBS-y += $(CPUDIR)/$(SOC)/lib$(SOC).o  
  3. endif  
ifdef SOC
LIBS-y += $(CPUDIR)/$(SOC)/lib$(SOC).o
endif
  • 如果定义了SOC, 则依赖lib$(SOC).o, 具体来看, 就是依赖arch/arm/cpu/s5pc1xx/libs5pc1xx.o
  1. LIBS-y += board/$(BOARDDIR)/lib$(BOARD).o  
LIBS-y += board/$(BOARDDIR)/lib$(BOARD).o
  • 依赖lib$(BOARD).o, 具体来看, 就是依赖board/samsung/tiny210/libtiny210.o
  1. LIBS-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/lib$(VENDOR).o  
LIBS-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/lib$(VENDOR).o
  • 如果HAVE_VENDOR_COMMON_LIB为y, 则依赖lib$(VENDOR).o, 具体来看, 就是依赖board/samsung/common/libsamsung.o
  1. LIBS-$(CONFIG_SPL_FRAMEWORK) += common/spl/libspl.o  
  2. LIBS-$(CONFIG_SPL_LIBCOMMON_SUPPORT) += common/libcommon.o  
  3. LIBS-$(CONFIG_SPL_LIBDISK_SUPPORT) += disk/libdisk.o  
  4. LIBS-$(CONFIG_SPL_I2C_SUPPORT) += drivers/i2c/libi2c.o  
  5. LIBS-$(CONFIG_SPL_GPIO_SUPPORT) += drivers/gpio/libgpio.o  
  6. LIBS-$(CONFIG_SPL_MMC_SUPPORT) += drivers/mmc/libmmc.o  
  7. LIBS-$(CONFIG_SPL_SERIAL_SUPPORT) += drivers/serial/libserial.o  
  8. LIBS-$(CONFIG_SPL_SPI_FLASH_SUPPORT) += drivers/mtd/spi/libspi_flash.o  
  9. LIBS-$(CONFIG_SPL_SPI_SUPPORT) += drivers/spi/libspi.o  
  10. LIBS-$(CONFIG_SPL_FAT_SUPPORT) += fs/fat/libfat.o  
  11. LIBS-$(CONFIG_SPL_LIBGENERIC_SUPPORT) += lib/libgeneric.o  
  12. LIBS-$(CONFIG_SPL_POWER_SUPPORT) += drivers/power/libpower.o \  
  13.     drivers/power/pmic/libpmic.o  
  14. LIBS-$(CONFIG_SPL_NAND_SUPPORT) += drivers/mtd/nand/libnand.o  
  15. LIBS-$(CONFIG_SPL_ONENAND_SUPPORT) += drivers/mtd/onenand/libonenand.o  
  16. LIBS-$(CONFIG_SPL_DMA_SUPPORT) += drivers/dma/libdma.o  
  17. LIBS-$(CONFIG_SPL_POST_MEM_SUPPORT) += post/drivers/memory.o  
  18. LIBS-$(CONFIG_SPL_NET_SUPPORT) += net/libnet.o  
  19. LIBS-$(CONFIG_SPL_ETH_SUPPORT) += drivers/net/libnet.o  
  20. LIBS-$(CONFIG_SPL_ETH_SUPPORT) += drivers/net/phy/libphy.o  
  21. LIBS-$(CONFIG_SPL_USBETH_SUPPORT) += drivers/net/phy/libphy.o  
  22. LIBS-$(CONFIG_SPL_MUSB_NEW_SUPPORT) += drivers/usb/musb-new/libusb_musb-new.o  
  23. LIBS-$(CONFIG_SPL_USBETH_SUPPORT) += drivers/usb/gadget/libusb_gadget.o  
  24. LIBS-$(CONFIG_SPL_WATCHDOG_SUPPORT) += drivers/watchdog/libwatchdog.o  
LIBS-$(CONFIG_SPL_FRAMEWORK) += common/spl/libspl.o
LIBS-$(CONFIG_SPL_LIBCOMMON_SUPPORT) += common/libcommon.o
LIBS-$(CONFIG_SPL_LIBDISK_SUPPORT) += disk/libdisk.o
LIBS-$(CONFIG_SPL_I2C_SUPPORT) += drivers/i2c/libi2c.o
LIBS-$(CONFIG_SPL_GPIO_SUPPORT) += drivers/gpio/libgpio.o
LIBS-$(CONFIG_SPL_MMC_SUPPORT) += drivers/mmc/libmmc.o
LIBS-$(CONFIG_SPL_SERIAL_SUPPORT) += drivers/serial/libserial.o
LIBS-$(CONFIG_SPL_SPI_FLASH_SUPPORT) += drivers/mtd/spi/libspi_flash.o
LIBS-$(CONFIG_SPL_SPI_SUPPORT) += drivers/spi/libspi.o
LIBS-$(CONFIG_SPL_FAT_SUPPORT) += fs/fat/libfat.o
LIBS-$(CONFIG_SPL_LIBGENERIC_SUPPORT) += lib/libgeneric.o
LIBS-$(CONFIG_SPL_POWER_SUPPORT) += drivers/power/libpower.o \
	drivers/power/pmic/libpmic.o
LIBS-$(CONFIG_SPL_NAND_SUPPORT) += drivers/mtd/nand/libnand.o
LIBS-$(CONFIG_SPL_ONENAND_SUPPORT) += drivers/mtd/onenand/libonenand.o
LIBS-$(CONFIG_SPL_DMA_SUPPORT) += drivers/dma/libdma.o
LIBS-$(CONFIG_SPL_POST_MEM_SUPPORT) += post/drivers/memory.o
LIBS-$(CONFIG_SPL_NET_SUPPORT) += net/libnet.o
LIBS-$(CONFIG_SPL_ETH_SUPPORT) += drivers/net/libnet.o
LIBS-$(CONFIG_SPL_ETH_SUPPORT) += drivers/net/phy/libphy.o
LIBS-$(CONFIG_SPL_USBETH_SUPPORT) += drivers/net/phy/libphy.o
LIBS-$(CONFIG_SPL_MUSB_NEW_SUPPORT) += drivers/usb/musb-new/libusb_musb-new.o
LIBS-$(CONFIG_SPL_USBETH_SUPPORT) += drivers/usb/gadget/libusb_gadget.o
LIBS-$(CONFIG_SPL_WATCHDOG_SUPPORT) += drivers/watchdog/libwatchdog.o
  • 根据具体配置, 选择相应的依赖关系
  1. ifeq ($(SOC),exynos)  
  2. LIBS-y += $(CPUDIR)/s5p-common/libs5p-common.o  
  3. endif  
ifeq ($(SOC),exynos)
LIBS-y += $(CPUDIR)/s5p-common/libs5p-common.o
endif
  • 如果SOC为exynos, 则依赖libs5p-common.o, 我们这里的SOC为s5pc1xx, 所以不依赖
  1. START := $(addprefix $(SPLTREE)/,$(START))  
  2. LIBS := $(addprefix $(SPLTREE)/,$(sort $(LIBS-y)))  
START := $(addprefix $(SPLTREE)/,$(START))
LIBS := $(addprefix $(SPLTREE)/,$(sort $(LIBS-y)))
  • 给START和LIBS加上前缀, $(SPLTREE), 具体来看, 就是编译过程中生成的.o文件都会放到spl/目录下面
  1. # Linker Script  
  2. ifdef CONFIG_SPL_LDSCRIPT  
  3. # need to strip off double quotes  
  4. LDSCRIPT := $(addprefix $(SRCTREE)/,$(subst ",,$(CONFIG_SPL_LDSCRIPT)))  
  5. endif  
  6.   
  7. ifeq ($(wildcard $(LDSCRIPT)),)  
  8.     LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot-spl.lds  
  9. endif  
  10. ifeq ($(wildcard $(LDSCRIPT)),)  
  11.     LDSCRIPT := $(TOPDIR)/$(CPUDIR)/u-boot-spl.lds  
  12. endif  
  13. ifeq ($(wildcard $(LDSCRIPT)),)  
  14.     LDSCRIPT := $(TOPDIR)/arch/$(ARCH)/cpu/u-boot-spl.lds  
  15. endif  
  16. ifeq ($(wildcard $(LDSCRIPT)),)  
  17. $(error could not find linker script)  
  18. endif  
# Linker Script
ifdef CONFIG_SPL_LDSCRIPT
# need to strip off double quotes
LDSCRIPT := $(addprefix $(SRCTREE)/,$(subst ",,$(CONFIG_SPL_LDSCRIPT)))
endif

ifeq ($(wildcard $(LDSCRIPT)),)
	LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot-spl.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
	LDSCRIPT := $(TOPDIR)/$(CPUDIR)/u-boot-spl.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
	LDSCRIPT := $(TOPDIR)/arch/$(ARCH)/cpu/u-boot-spl.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
$(error could not find linker script)
endif
  • 找到spl的链接配置文件, 具体来看, 用的是arch/arm/cpu下的u-boot-spl.lds
  1. ALL-y   += $(obj)$(SPL_BIN).bin  
  2.   
  3. ifdef CONFIG_SAMSUNG  
  4. ALL-y   += $(obj)$(BOARD)-spl.bin  
  5. endif  
  6.   
  7. all:    $(ALL-y)  
  8.   
  9. ifdef CONFIG_SAMSUNG  
  10. $(obj)$(BOARD)-spl.bin: $(obj)u-boot-spl.bin  
  11.     $(OBJTREE)/tools/mk$(BOARD)spl \  
  12.         $(obj)u-boot-spl.bin $(obj)$(BOARD)-spl.bin  
  13. endif  
  14.   
  15. $(obj)$(SPL_BIN).bin:   $(obj)$(SPL_BIN)  
  16.     $(OBJCOPY) $(OBJCFLAGS) -O binary $< $@  
  17.   
  18. GEN_UBOOT = \  
  19.     cd $(obj) && $(LD) $(LDFLAGS) $(LDFLAGS_$(@F)) $(__START) \  
  20.         --start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \  
  21.         -Map $(SPL_BIN).map -o $(SPL_BIN)  
  22.   
  23. $(obj)$(SPL_BIN):   depend $(START) $(LIBS) $(obj)u-boot-spl.lds  
  24.     $(GEN_UBOOT)  
  25.   
  26. $(START):   depend  
  27.     $(MAKE) -C $(SRCTREE)/$(START_PATH) $@  
  28.   
  29. $(LIBS):    depend  
  30.     $(MAKE) -C $(SRCTREE)$(dir $(subst $(SPLTREE),,$@))  
  31.   
  32. $(obj)u-boot-spl.lds: $(LDSCRIPT) depend  
  33.     $(CPP) $(CPPFLAGS) $(LDPPFLAGS) -I$(obj). -ansi -D__ASSEMBLY__ -P - < $< > $@  
  34.   
  35. depend: $(obj).depend  
  36. .PHONY: depend  
ALL-y	+= $(obj)$(SPL_BIN).bin

ifdef CONFIG_SAMSUNG
ALL-y	+= $(obj)$(BOARD)-spl.bin
endif

all:	$(ALL-y)

ifdef CONFIG_SAMSUNG
$(obj)$(BOARD)-spl.bin: $(obj)u-boot-spl.bin
	$(OBJTREE)/tools/mk$(BOARD)spl \
		$(obj)u-boot-spl.bin $(obj)$(BOARD)-spl.bin
endif

$(obj)$(SPL_BIN).bin:	$(obj)$(SPL_BIN)
	$(OBJCOPY) $(OBJCFLAGS) -O binary $< $@

GEN_UBOOT = \
	cd $(obj) && $(LD) $(LDFLAGS) $(LDFLAGS_$(@F)) $(__START) \
		--start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \
		-Map $(SPL_BIN).map -o $(SPL_BIN)

$(obj)$(SPL_BIN):	depend $(START) $(LIBS) $(obj)u-boot-spl.lds
	$(GEN_UBOOT)

$(START):	depend
	$(MAKE) -C $(SRCTREE)/$(START_PATH) $@

$(LIBS):	depend
	$(MAKE) -C $(SRCTREE)$(dir $(subst $(SPLTREE),,$@))

$(obj)u-boot-spl.lds: $(LDSCRIPT) depend
	$(CPP) $(CPPFLAGS) $(LDPPFLAGS) -I$(obj). -ansi -D__ASSEMBLY__ -P - < $< > $@

depend:	$(obj).depend
.PHONY: depend

  • all: $(ALL-y), 还记得本篇上面说的, 进入到spl目录下之后干啥事吗? 没错, 执行make all, 其实执行的就是这里的all目标. 它依赖$(ALL-y). 具体的依赖关系就不赘述了, 顺藤摸瓜即可
  • ifdef CONFIG_SAMSUNG: 这个是针对Samsung平台的特殊之处. 
    • 利用tools/mk$(BOARD)spl这个工具, 将u-boot-spl.bin转为$(BOARD)-spl.bin
      • 转换的本质, 就是在u-boot-spl.bin前面加入head info.关于head info是什么, 可以参考文档S5PV210_iROM_ApplicationNote_Preliminary_20091126.pdf. 顺便可以理解一下samsung芯片的启动流程
    • tools/mk$(BOARD)spl这个工具是在tools/Makefile里面生成的.
OK, 分析结束, 接下来, 就基于我们上面的分析开始分析代码了.

SPL代码分析

u-boot-spl.lds: 它的位置在上文中我们分析了
  • 根据u-boot-spl.lds中的规则, 我们知道CPUDIR/start.o被放在了最前面. 它所对应的文件就是arch/arm/cpu/armv7/start.S

start.S

下面我们看看start.S
  1. .globl _start  
  2. _start: b   reset  
  3.     ldr pc, _undefined_instruction  
  4.     ldr pc, _software_interrupt  
  5.     ldr pc, _prefetch_abort  
  6.     ldr pc, _data_abort  
  7.     ldr pc, _not_used  
  8.     ldr pc, _irq  
  9.     ldr pc, _fiq  
.globl _start
_start: b	reset
	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
  • _start是我们在lds里面指定的ENTRY(_start)
  • 首先会跳转到reset处
  • ldr  pc, _xxx定义的是中断向量表
  1. #ifdef CONFIG_SPL_BUILD  
  2. _undefined_instruction: .word _undefined_instruction  
  3. _software_interrupt:    .word _software_interrupt  
  4. _prefetch_abort:    .word _prefetch_abort  
  5. _data_abort:        .word _data_abort  
  6. _not_used:      .word _not_used  
  7. _irq:           .word _irq  
  8. _fiq:           .word _fiq  
  9. _pad:           .word 0x12345678 /* now 16*4=64 */  
  10. #else  
  11. _undefined_instruction: .word undefined_instruction  
  12. _software_interrupt:    .word software_interrupt  
  13. _prefetch_abort:    .word prefetch_abort  
  14. _data_abort:        .word data_abort  
  15. _not_used:      .word not_used  
  16. _irq:           .word irq  
  17. _fiq:           .word fiq  
  18. _pad:           .word 0x12345678 /* now 16*4=64 */  
  19. #endif  /* CONFIG_SPL_BUILD */  
#ifdef CONFIG_SPL_BUILD
_undefined_instruction: .word _undefined_instruction
_software_interrupt:	.word _software_interrupt
_prefetch_abort:	.word _prefetch_abort
_data_abort:		.word _data_abort
_not_used:		.word _not_used
_irq:			.word _irq
_fiq:			.word _fiq
_pad:			.word 0x12345678 /* now 16*4=64 */
#else
_undefined_instruction: .word undefined_instruction
_software_interrupt:	.word software_interrupt
_prefetch_abort:	.word prefetch_abort
_data_abort:		.word data_abort
_not_used:		.word not_used
_irq:			.word irq
_fiq:			.word fiq
_pad:			.word 0x12345678 /* now 16*4=64 */
#endif	/* CONFIG_SPL_BUILD */
  • 当CONFIG了SPL_BUILD之后, 一旦发生异常中断, 就会进入死循环. 所以我们的SPL里面不允许出发异常中断
  • 不过正常的uboot(即stage2阶段)还是可以处理异常中断的.
reset
  1. /*  
  2.  * the actual reset code  
  3.  */  
  4.   
  5. reset:  
  6.     bl  save_boot_params  
  7.     /*  
  8.      * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,  
  9.      * except if in HYP mode already  
  10.      */  
  11.     mrs r0, cpsr  
  12.     and r1, r0, #0x1f       @ mask mode bits  
  13.     teq r1, #0x1a       @ test for HYP mode  
  14.     bicne   r0, r0, #0x1f       @ clear all mode bits  
  15.     orrne   r0, r0, #0x13       @ set SVC mode  
  16.     orr r0, r0, #0xc0       @ disable FIQ and IRQ  
  17.     msr cpsr,r0  
  18.   
  19.         /* ........ */  
  20.     /* the mask ROM code should have PLL and others stable */  
  21. #ifndef CONFIG_SKIP_LOWLEVEL_INIT  
  22.     bl  cpu_init_cp15  
  23.     bl  cpu_init_crit  
  24. #endif  
  25.   
  26.     bl  _main  
/*
 * the actual reset code
 */

reset:
	bl	save_boot_params
	/*
	 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
	 * except if in HYP mode already
	 */
	mrs	r0, cpsr
	and	r1, r0, #0x1f		@ mask mode bits
	teq	r1, #0x1a		@ test for HYP mode
	bicne	r0, r0, #0x1f		@ clear all mode bits
	orrne	r0, r0, #0x13		@ set SVC mode
	orr	r0, r0, #0xc0		@ disable FIQ and IRQ
	msr	cpsr,r0

        /* ........ */
	/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
	bl	cpu_init_cp15
	bl	cpu_init_crit
#endif

	bl	_main
当初次上电或者复位时, Uboot最新运行的就是这里的代码
  • bl save_boot_params: 如果没有重新定义save_boot_params,则使用<arch/arm/cpu/armv7/start.S>中的save_boot_params。其不做任何事情,直接返回
  • 禁止FIQ, IRQ; 设置CPU工作在SVC32模式
  • bl cpu_init_cp15: (I/D-Cache, MMU, TLBs),具体见下面代码中注释
  • bl    cpu_init_crit    :  主要是设置CPU的PLL, GPIO管脚复用, memory等. 具体见下面
  • bl    _main              :  跳转到<arch/arm/lib/crt0.S>中的_main. 具体见下面
cpu_init_cp15
  1. /*************************************************************************  
  2.  *  
  3.  * cpu_init_cp15  
  4.  *  
  5.  * Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on unless  
  6.  * CONFIG_SYS_ICACHE_OFF is defined.  
  7.  *  
  8.  *************************************************************************/  
  9. ENTRY(cpu_init_cp15)  
  10.     /*  
  11.      * Invalidate L1 I/D  
  12.      */  
  13.     mov r0, #0          @ set up for MCR  
  14.     mcr p15, 0, r0, c8, c7, 0   @ invalidate TLBs  
  15.     mcr p15, 0, r0, c7, c5, 0   @ invalidate icache  
  16.     mcr p15, 0, r0, c7, c5, 6   @ invalidate BP array  
  17.     mcr     p15, 0, r0, c7, c10, 4  @ DSB  
  18.     mcr     p15, 0, r0, c7, c5, 4   @ ISB  
  19.   
  20.     /*  
  21.      * disable MMU stuff and caches  
  22.      */  
  23.     mrc p15, 0, r0, c1, c0, 0  
  24.     bic r0, r0, #0x00002000 @ clear bits 13 (--V-)  
  25.     bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)  
  26.     orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align  
  27.     orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB  
  28. #ifdef CONFIG_SYS_ICACHE_OFF  
  29.     bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache  
  30. #else  
  31.     orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache  
  32. #endif  
  33.     mcr p15, 0, r0, c1, c0, 0  
  34.   
  35. #ifdef CONFIG_ARM_ERRATA_716044  
  36.     mrc p15, 0, r0, c1, c0, 0   @ read system control register  
  37.     orr r0, r0, #1 << 11  @ set bit #11  
  38.     mcr p15, 0, r0, c1, c0, 0   @ write system control register  
  39. #endif  
  40.   
  41. #ifdef CONFIG_ARM_ERRATA_742230  
  42.     mrc p15, 0, r0, c15, c0, 1  @ read diagnostic register  
  43.     orr r0, r0, #1 << 4       @ set bit #4  
  44.     mcr p15, 0, r0, c15, c0, 1  @ write diagnostic register  
  45. #endif  
  46.   
  47. #ifdef CONFIG_ARM_ERRATA_743622  
  48.     mrc p15, 0, r0, c15, c0, 1  @ read diagnostic register  
  49.     orr r0, r0, #1 << 6       @ set bit #6  
  50.     mcr p15, 0, r0, c15, c0, 1  @ write diagnostic register  
  51. #endif  
  52.   
  53. #ifdef CONFIG_ARM_ERRATA_751472  
  54.     mrc p15, 0, r0, c15, c0, 1  @ read diagnostic register  
  55.     orr r0, r0, #1 << 11  @ set bit #11  
  56.     mcr p15, 0, r0, c15, c0, 1  @ write diagnostic register  
  57. #endif  
  58.   
  59.     mov pc, lr          @ back to my caller  
  60. ENDPROC(cpu_init_cp15)  
/*************************************************************************
 *
 * cpu_init_cp15
 *
 * Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on unless
 * CONFIG_SYS_ICACHE_OFF is defined.
 *
 *************************************************************************/
ENTRY(cpu_init_cp15)
	/*
	 * Invalidate L1 I/D
	 */
	mov	r0, #0			@ set up for MCR
	mcr	p15, 0, r0, c8, c7, 0	@ invalidate TLBs
	mcr	p15, 0, r0, c7, c5, 0	@ invalidate icache
	mcr	p15, 0, r0, c7, c5, 6	@ invalidate BP array
	mcr     p15, 0, r0, c7, c10, 4	@ DSB
	mcr     p15, 0, r0, c7, c5, 4	@ ISB

	/*
	 * disable MMU stuff and caches
	 */
	mrc	p15, 0, r0, c1, c0, 0
	bic	r0, r0, #0x00002000	@ clear bits 13 (--V-)
	bic	r0, r0, #0x00000007	@ clear bits 2:0 (-CAM)
	orr	r0, r0, #0x00000002	@ set bit 1 (--A-) Align
	orr	r0, r0, #0x00000800	@ set bit 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
	bic	r0, r0, #0x00001000	@ clear bit 12 (I) I-cache
#else
	orr	r0, r0, #0x00001000	@ set bit 12 (I) I-cache
#endif
	mcr	p15, 0, r0, c1, c0, 0

#ifdef CONFIG_ARM_ERRATA_716044
	mrc	p15, 0, r0, c1, c0, 0	@ read system control register
	orr	r0, r0, #1 << 11	@ set bit #11
	mcr	p15, 0, r0, c1, c0, 0	@ write system control register
#endif

#ifdef CONFIG_ARM_ERRATA_742230
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 4		@ set bit #4
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif

#ifdef CONFIG_ARM_ERRATA_743622
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 6		@ set bit #6
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif

#ifdef CONFIG_ARM_ERRATA_751472
	mrc	p15, 0, r0, c15, c0, 1	@ read diagnostic register
	orr	r0, r0, #1 << 11	@ set bit #11
	mcr	p15, 0, r0, c15, c0, 1	@ write diagnostic register
#endif

	mov	pc, lr			@ back to my caller
ENDPROC(cpu_init_cp15)
cpu_init_crit
  1. #ifndef CONFIG_SKIP_LOWLEVEL_INIT  
  2. /*************************************************************************  
  3.  *  
  4.  * CPU_init_critical registers  
  5.  *  
  6.  * setup important registers  
  7.  * setup memory timing  
  8.  *  
  9.  *************************************************************************/  
  10. ENTRY(cpu_init_crit)  
  11.     /*  
  12.      * Jump to board specific initialization...  
  13.      * The Mask ROM will have already initialized  
  14.      * basic memory. Go here to bump up clock rate and handle  
  15.      * wake up conditions.  
  16.      */  
  17.     b   lowlevel_init       @ go setup pll,mux,memory  
  18. ENDPROC(cpu_init_crit)  
  19. #endif  
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
/*************************************************************************
 *
 * CPU_init_critical registers
 *
 * setup important registers
 * setup memory timing
 *
 *************************************************************************/
ENTRY(cpu_init_crit)
	/*
	 * Jump to board specific initialization...
	 * The Mask ROM will have already initialized
	 * basic memory. Go here to bump up clock rate and handle
	 * wake up conditions.
	 */
	b	lowlevel_init		@ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
#endif
  • b lowlevel_init : 跳转到<arch/arm/cpu/armv7/lowlevel_init.S>中的lowlevel_init

lowlevel_init.S

lowlevel_init
  1. #include <asm-offsets.h>  
  2. #include <config.h>  
  3. #include <linux/linkage.h>  
  4.   
  5. ENTRY(lowlevel_init)  
  6.     /*  
  7.      * Setup a temporary stack  
  8.      */  
  9.     ldr sp, =CONFIG_SYS_INIT_SP_ADDR  
  10.     bic sp, sp, #7 /* 8-byte alignment for ABI compliance */  
  11. #ifdef CONFIG_SPL_BUILD  
  12.     ldr r9, =gdata  
  13. #else  
  14.     sub sp, #GD_SIZE  
  15.     bic sp, sp, #7  
  16.     mov r9, sp  
  17. #endif  
  18.     /*  
  19.      * Save the old lr(passed in ip) and the current lr to stack  
  20.      */  
  21.     push    {ip, lr}  
  22.   
  23.     /*  
  24.      * go setup pll, mux, memory  
  25.      */  
  26.     bl  s_init  
  27.     pop {ip, pc}  
#include <asm-offsets.h>
#include <config.h>
#include <linux/linkage.h>

ENTRY(lowlevel_init)
	/*
	 * Setup a temporary stack
	 */
	ldr	sp, =CONFIG_SYS_INIT_SP_ADDR
	bic	sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_BUILD
	ldr	r9, =gdata
#else
	sub	sp, #GD_SIZE
	bic	sp, sp, #7
	mov	r9, sp
#endif
	/*
	 * Save the old lr(passed in ip) and the current lr to stack
	 */
	push	{ip, lr}

	/*
	 * go setup pll, mux, memory
	 */
	bl	s_init
	pop	{ip, pc}
以前老版本的uboot, lowlevel_init一般都是在board/xxx下面的板级文件夹下面实现的. 现在直接放到CPUDIR下面了, 那它做了什么事情呢
  • 对stack pointer赋值成CONFIG_SYS_INIT_SP_ADDR
  • 确保sp是8字节对齐
  • 将gdata的地址存入到r9寄存器中
  • 跳转到 s_init: 这个s_init就需要芯片厂商或者我们自己在板级文件里面实现了. 它主要做的事情
    • setup pll, mux, memory
我个人感觉, 新版本的uboot在CPUDIR下实现了一个lowlevel_init.S文件, 主要目标是初始化sp, 这样s_init就可以用C语言实现了. 而以前的老版本里面, s_init里面要做的事情都是用汇编做的.

crt0.S

_main
  1. ENTRY(_main)  
  2.   
  3. /*  
  4.  * Set up initial C runtime environment and call board_init_f(0).  
  5.  */  
  6.   
  7. #if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)  
  8.     ldr sp, =(CONFIG_SPL_STACK)  
  9. #else  
  10.     ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)  
  11. #endif  
  12.     bic sp, sp, #7  /* 8-byte alignment for ABI compliance */  
  13.     sub sp, #GD_SIZE    /* allocate one GD above SP */  
  14.     bic sp, sp, #7  /* 8-byte alignment for ABI compliance */  
  15.     mov r9, sp      /* GD is above SP */  
  16.     mov r0, #0  
  17.     bl  board_init_f  
ENTRY(_main)

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */

#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, #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
  • 重新对SP赋值, 确认sp是8字对齐
  • 在栈顶保留一个global_data的大小, 这个global_data是uboot里面的一个全局数据, 很多地方都会用到. 俗称 gd_t
  • 确认更新后的sp是8字对齐
  • r9指向global_data, 后面别的地方想用global_data时候, 可以直接从r9里面获取地址.
  • r0赋值0
  • bl board_init_f: 跳转到board_init_f. 在编译SPL时, 分析Makefile可以看出, 该函数的实现是在<arch/arm/lib/spl.c>. 

arch/arm/lib/spl.c

board_init_f
  1. /*  
  2.  * In the context of SPL, board_init_f must ensure that any clocks/etc for  
  3.  * DDR are enabled, ensure that the stack pointer is valid, clear the BSS  
  4.  * and call board_init_f.  We provide this version by default but mark it  
  5.  * as __weak to allow for platforms to do this in their own way if needed.  
  6.  */  
  7. void __weak board_init_f(ulong dummy)  
  8. {  
  9.     /* Clear the BSS. */  
  10.     memset(__bss_start, 0, __bss_end - __bss_start);  
  11.   
  12.     /* Set global data pointer. */  
  13.     gd = &gdata;  
  14.   
  15.     board_init_r(NULL, 0);  
  16. }  
/*
 * In the context of SPL, board_init_f must ensure that any clocks/etc for
 * DDR are enabled, ensure that the stack pointer is valid, clear the BSS
 * and call board_init_f.  We provide this version by default but mark it
 * as __weak to allow for platforms to do this in their own way if needed.
 */
void __weak board_init_f(ulong dummy)
{
	/* Clear the BSS. */
	memset(__bss_start, 0, __bss_end - __bss_start);

	/* Set global data pointer. */
	gd = &gdata;

	board_init_r(NULL, 0);
}
  • __weak: 表明该函数可以被重新定义
  • 对BSS段进行清零操作
  • gd = &gdata;
    • gd的定义在DECLARE_GLOBAL_DATA_PTR <arch/arm/include/asm/global_data.h>
      • #define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r9")
        • 还记得r9这个寄存器吗, 在上面初始化过了
    • gdata的定义在本文件中: gd_t gdata __attribute__ ((section(".data")));  
      • 它是一个 gd_t 也就是global_data类型的变量
      • __attribute__表示这个变量会被放到".data"这个输入段中. 连接器会把输入段按照链接脚本(u-boot-spl.lds)里面指定的规则存放到输出段.
    • 为什么会有这个赋值操作, 不太明白...
  • board_init_r : 在编译SPL时, 分析Makefile可以看出, 该函数的实现是在<common/spl/spl.c>

common/spl/spl.c

board_init_r
  1. #ifdef CONFIG_SYS_SPL_MALLOC_START  
  2.     mem_malloc_init(CONFIG_SYS_SPL_MALLOC_START,  
  3.             CONFIG_SYS_SPL_MALLOC_SIZE);  
  4. #endif  
#ifdef CONFIG_SYS_SPL_MALLOC_START
	mem_malloc_init(CONFIG_SYS_SPL_MALLOC_START,
			CONFIG_SYS_SPL_MALLOC_SIZE);
#endif
  • 如果定义了:CONFIG_SYS_SPL_MALLOC_START, 则进行memory的malloc池初始化. 以后调用malloc就在这个池子里面分配内存
  1. #ifndef CONFIG_PPC  
  2.     /*  
  3.      * timer_init() does not exist on PPC systems. The timer is initialized  
  4.      * and enabled (decrementer) in interrupt_init() here.  
  5.      */  
  6.     timer_init();  
  7. #endif  
#ifndef CONFIG_PPC
	/*
	 * timer_init() does not exist on PPC systems. The timer is initialized
	 * and enabled (decrementer) in interrupt_init() here.
	 */
	timer_init();
#endif
  • 如果没有定义:CONFIG_PPC, 则进行timer的初始化. <arch/arm/cpu/armv7/s5p-common/timer.c>里面定义
  1. #ifdef CONFIG_SPL_BOARD_INIT  
  2.     spl_board_init();  
  3. #endif  
#ifdef CONFIG_SPL_BOARD_INIT
	spl_board_init();
#endif
  • SPL阶段, 如果还需要做什么初始化动作, 可以放在这里. 具体的实现可以在BOARDDIR下面.
  1. boot_device = spl_boot_device();  
  2.     debug("boot device - %d\n", boot_device);  
boot_device = spl_boot_device();
	debug("boot device - %d\n", boot_device);
  • 必须实现spl_boot_device, 返回是从哪个外部设备启动的(NAND/SDCARD/NOR...). 可以厂商或者自己在BOARDDIR下面实现
  1.     switch (boot_device) {  
  2. #ifdef CONFIG_SPL_RAM_DEVICE  
  3.     case BOOT_DEVICE_RAM:  
  4.         spl_ram_load_image();  
  5.         break;  
  6. #endif  
  7. #ifdef CONFIG_SPL_MMC_SUPPORT  
  8.     case BOOT_DEVICE_MMC1:  
  9.     case BOOT_DEVICE_MMC2:  
  10.     case BOOT_DEVICE_MMC2_2:  
  11.         spl_mmc_load_image();  
  12.         break;  
  13. #endif  
  14. #ifdef CONFIG_SPL_NAND_SUPPORT  
  15.     case BOOT_DEVICE_NAND:  
  16.         spl_nand_load_image();  
  17.         break;  
  18. #endif  
  19. #ifdef CONFIG_SPL_ONENAND_SUPPORT  
  20.     case BOOT_DEVICE_ONENAND:  
  21.         spl_onenand_load_image();  
  22.         break;  
  23. #endif  
  24. #ifdef CONFIG_SPL_NOR_SUPPORT  
  25.     case BOOT_DEVICE_NOR:  
  26.         spl_nor_load_image();  
  27.         break;  
  28. #endif  
  29. #ifdef CONFIG_SPL_YMODEM_SUPPORT  
  30.     case BOOT_DEVICE_UART:  
  31.         spl_ymodem_load_image();  
  32.         break;  
  33. #endif  
  34. #ifdef CONFIG_SPL_SPI_SUPPORT  
  35.     case BOOT_DEVICE_SPI:  
  36.         spl_spi_load_image();  
  37.         break;  
  38. #endif  
  39. #ifdef CONFIG_SPL_ETH_SUPPORT  
  40.     case BOOT_DEVICE_CPGMAC:  
  41. #ifdef CONFIG_SPL_ETH_DEVICE  
  42.         spl_net_load_image(CONFIG_SPL_ETH_DEVICE);  
  43. #else  
  44.         spl_net_load_image(NULL);  
  45. #endif  
  46.         break;  
  47. #endif  
  48. #ifdef CONFIG_SPL_USBETH_SUPPORT  
  49.     case BOOT_DEVICE_USBETH:  
  50.         spl_net_load_image("usb_ether");  
  51.         break;  
  52. #endif  
  53.     default:  
  54.         debug("SPL: Un-supported Boot Device\n");  
  55.         hang();  
  56.     }  
	switch (boot_device) {
#ifdef CONFIG_SPL_RAM_DEVICE
	case BOOT_DEVICE_RAM:
		spl_ram_load_image();
		break;
#endif
#ifdef CONFIG_SPL_MMC_SUPPORT
	case BOOT_DEVICE_MMC1:
	case BOOT_DEVICE_MMC2:
	case BOOT_DEVICE_MMC2_2:
		spl_mmc_load_image();
		break;
#endif
#ifdef CONFIG_SPL_NAND_SUPPORT
	case BOOT_DEVICE_NAND:
		spl_nand_load_image();
		break;
#endif
#ifdef CONFIG_SPL_ONENAND_SUPPORT
	case BOOT_DEVICE_ONENAND:
		spl_onenand_load_image();
		break;
#endif
#ifdef CONFIG_SPL_NOR_SUPPORT
	case BOOT_DEVICE_NOR:
		spl_nor_load_image();
		break;
#endif
#ifdef CONFIG_SPL_YMODEM_SUPPORT
	case BOOT_DEVICE_UART:
		spl_ymodem_load_image();
		break;
#endif
#ifdef CONFIG_SPL_SPI_SUPPORT
	case BOOT_DEVICE_SPI:
		spl_spi_load_image();
		break;
#endif
#ifdef CONFIG_SPL_ETH_SUPPORT
	case BOOT_DEVICE_CPGMAC:
#ifdef CONFIG_SPL_ETH_DEVICE
		spl_net_load_image(CONFIG_SPL_ETH_DEVICE);
#else
		spl_net_load_image(NULL);
#endif
		break;
#endif
#ifdef CONFIG_SPL_USBETH_SUPPORT
	case BOOT_DEVICE_USBETH:
		spl_net_load_image("usb_ether");
		break;
#endif
	default:
		debug("SPL: Un-supported Boot Device\n");
		hang();
	}
  • 将image从具体的外部设备中load到ram中. 这里暂时先不分析具体的load过程.
  1.     switch (spl_image.os) {  
  2.     case IH_OS_U_BOOT:  
  3.         debug("Jumping to U-Boot\n");  
  4.         break;  
  5. #ifdef CONFIG_SPL_OS_BOOT  
  6.     case IH_OS_LINUX:  
  7.         debug("Jumping to Linux\n");  
  8.         spl_board_prepare_for_linux();  
  9.         jump_to_image_linux((void *)CONFIG_SYS_SPL_ARGS_ADDR);  
  10. #endif  
  11.     default:  
  12.         debug("Unsupported OS image.. Jumping nevertheless..\n");  
  13.     }  
  14.     jump_to_image_no_args(&spl_image);  
	switch (spl_image.os) {
	case IH_OS_U_BOOT:
		debug("Jumping to U-Boot\n");
		break;
#ifdef CONFIG_SPL_OS_BOOT
	case IH_OS_LINUX:
		debug("Jumping to Linux\n");
		spl_board_prepare_for_linux();
		jump_to_image_linux((void *)CONFIG_SYS_SPL_ARGS_ADDR);
#endif
	default:
		debug("Unsupported OS image.. Jumping nevertheless..\n");
	}
	jump_to_image_no_args(&spl_image);
  • 判断image的类型
    • 如果是u-boot,则直接break, 去运行u-boot
    • 如果是Linux,则启动Linux
至此,SPL结束它的生命,控制权交于u-boot或Linux

在接下来的一篇中, 我们会分析当控制权交给u-boot之后, uboot的运行流程

总结

SPL移植注意点

  • s_init: C语言实现此函数, 必须的. 如果是厂商提供的, 一般在arch/arm/cpu/xxx下面. 如果厂商没有提供, 我们可以在BOARDDIR下面实现. 主要完成以下功能
    • 设置CPU的PLL, GPIO管脚复用, memory等
  • spl_board_init: C语言实现, 可选的.  如果是厂商提供的, 一般在arch/arm/cpu/xxx下面. 如果厂商没有提供, 我们可以在BOARDDIR下面实现. 主要完成的功能自己决定
  • spl_boot_device: C语言实现, 必须的. 如果是厂商提供的, 一般在arch/arm/cpu/xxx下面. 如果厂商没有提供, 我们可以在BOARDDIR下面实现. 主要完成的功能
    • 返回是从什么设备启动的(NAND/SDCARD/Nor ...). 像Atmel, Samsung的芯片, 都是有办法做这个事情的.
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/130022
推荐阅读
相关标签
  

闽ICP备14008679号